Control Flow

Overview

rill provides singular control flow with no exceptions and no try/catch. Errors halt execution. guard captures halts as invalid values for explicit recovery. retry retries a body when it returns an invalid result.

SyntaxDescription
cond ? then ! elseConditional (if-else)
$val -> ? then ! elsePiped conditional (uses $ as cond)
while (cond) do { body }While loop (cond is bool, evaluated before each iteration)
do { body } while (cond)Do-while (body first, cond after)
do<limit: N> { body }Construct option: sets iteration limit
break / $val -> breakExit loop
return / $val -> returnExit block
assert cond / assert cond "msg"Validate condition, halt on failure
error "msg" / $val -> errorHalt execution with error message
guard { body }Run body; replace halt with invalid value
retry<N> { body }Retry body up to N times on invalid result

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 condition

Piped 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 condition

Optional Else

The else branch (! ...) is optional:

true ? "executed"                   # only runs if true
false ? "skipped"                   # returns empty string

Else-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 $.

Loop bodies cannot modify outer-scope variables. Use $ to carry all state. For multiple values, pack them in a dict.

Syntax

while (condition) do { body }
initial -> while (condition) do { body }

Basic Usage

# Count to 5
0 -> while ($ < 5) do { $ + 1 }
# Result: 5
# String accumulation
"" -> while (.len < 5) do { "{$}x" }
# Result: "xxxxx"

Condition Forms

0 -> while ($ < 10) do { $ + 1 }
# Result: 10
"" -> while (.len < 4) do { "{$}x" }
# Result: "xxxx"

Pipe-Seeded While Loop

Pipe a seed value to while to prime the accumulator $:

0 -> while ($ < 3) do { $ + 1 }
# Result: 3
"start" -> while ($ != "done") do {
  ($ == "start") ? "middle" ! "done"
}
# Result: "done"

Infinite Loop with Break

0 -> while (true) do {
  $ + 1 -> ($ > 5) ? break ! $
}
# Result: 6

Iteration Limit

Use do<limit: N> as the construct option to set maximum iterations (default: 10,000):

0 -> while ($ < 10) do<limit: 100> { $ + 1 }
# Result: 10

Exceeding the limit throws RuntimeError with code RUNTIME_LIMIT_EXCEEDED.

The do<limit: N> construct option applies only to that specific loop evaluation, not to the body closure.

Multiple State Values

When you need to track multiple values across iterations, use $ as a state dict:

use<ext:app> => $app

# Track iteration count, text, and done flag
[iter: 0, text: $input, done: false]
  -> while (!$.done && $.iter < 3) do {
    $.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, $.iter

This 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

do { body } while (condition)
initial -> do { body } while (condition)

Basic Usage

# Execute at least once, continue while condition holds
0 -> do { $ + 1 } while ($ < 5)
# Result: 5
# String accumulation
"" -> do { "{$}x" } while (.len < 3)
# Result: "xxx"

Pipe-Seeded Do-While Loop

Pipe a seed value to prime the accumulator $ before the first iteration:

0 -> do { $ + 1 } while ($ < 3)
# Result: 3
"" -> do { "{$}!" } while (.len < 4)
# Result: "!!!!"

When to Use

  • While while (condition) do { body }: condition checked BEFORE body (may execute 0 times)
  • Do-while do { body } while (condition): condition checked AFTER body (executes at least once)

Retry Pattern

Do-while is ideal for retry patterns:

use<ext:app> => $app

do<limit: 5> {
  $app.prompt("Perform operation")
} while (.contains("RETRY"))
# Loop exits when result doesn't contain RETRY

Iteration Limit

0 -> do<limit: 100> { $ + 1 } while ($ < 10)
# Result: 10

Break

Exit a loop early. Returns the value piped to break, or current $ if bare.

Syntax

break                    # exit with current $
$value -> break          # exit with value

In seq Loop

[1, 2, 3, 4, 5] -> seq({
  ($ > 3) ? ("found {$}" -> break)
  $
})
# Returns "found 4"

In While Loop

0 -> while (true) do {
  ($ + 1) -> ($ > 3) ? break ! $
}
# Returns 4

Break Value

In seq, break returns partial results collected before the break:

["a", "b", "STOP", "c"] -> seq({
  ($ == "STOP") ? break
  $
})
# Returns ["a", "b"] (partial results before break)

acc also catches break and returns the partial results list collected up to that point.

Break Not Allowed

break is not supported in fan, filter, or fold:

[1, 2, 3] -> fan({ break })    # ERROR: break not supported in fan

Return

Exit a block early. Returns the value piped to return, or current $ if bare.

Syntax

return                   # exit with current $
$value -> return         # exit with value

In Blocks

5 => $x
($x > 3) ? ("big" -> return)
"small"
# Returns "big"

Multi-Phase Pipeline

"content" => $content
$content -> .contains("ERROR") ? ("Read failed" -> return)
"processed: {$content}"
# 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 condition

Basic 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 failed

Custom 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 failed

In Loops

Assert validates each iteration. The loop halts on the first failing assertion:

[1, 2, 3] -> seq({
  assert ($ > 0) "Must be positive"
})
# Returns [1, 2, 3] (all elements valid)
[1, 0, 3] -> seq({
  assert ($ > 0) "Must be positive"
})
# Error: Must be positive

Pipe 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:

ConditionError CodeMessage
Condition is falseRUNTIME_ASSERTION_FAILEDCustom message or “Assertion failed”
Condition is not booleanRUNTIME_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 failed

Validation Patterns

Guard clauses at function start:

|data| {
  assert $data:?list "Expected list"
  assert !$data.empty "List cannot be empty"
  $data -> seq({ $ * 2 })
} => $process
true

Multi-step validation:

use<ext:app> => $app

$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 form

Basic Usage

Use error with a string literal to halt with a message:

error "Something went wrong"
# Halts execution with: Something went wrong

The 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 failed

The 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: 404

Piping non-string values throws a type error:

42 -> error                      # Error: error requires string, got number

String Interpolation

Use interpolation for dynamic error messages:

404 => $code
error "Unexpected status: {$code}"
# Halts with: Unexpected status: 404
3 => $step
"timeout" => $reason
error "Failed at step {$step}: {$reason}"
# Halts with: Failed at step 3: timeout

Conditional 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 empty

In 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_age
true

Error Behavior

Error throws RuntimeError with code RUNTIME_ERROR_RAISED:

PatternHalts WithMessage Source
error "msg"RUNTIME_ERROR_RAISEDString literal
$val -> errorRUNTIME_ERROR_RAISEDPiped value (must be string)
error ""RUNTIME_ERROR_RAISEDEmpty message
error 123Parse errorPARSE_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

StatementConditionBehavior
assert cond "msg"Validates conditionHalts if condition is false, passes through if true
error "msg"NoneAlways 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] -> seq({
  ($ == 2) ? { error "Halted at 2" }
  $ * 2
})
# Halts on second iteration with: Halted at 2

Pass

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] -> fan({ ($ > 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() ! pass

Pass Behavior

PatternReturnsContext
cond ? pass ! alt$ if true, alt if falseConditional branch
cond ? alt ! passalt if true, $ if falseConditional branch
[key: pass]Dict with $ as valueDict construction
-> { pass }$Block body

Note: pass requires pipe context. Using pass without $ bound throws an error.


Operational Recovery with guard

guard { body } executes its body. When the body halts (via error, assert, or a throwing extension call), guard replaces the halt with an invalid value instead of stopping execution. The invalid value carries a :atom value identifying the failure.

use<ext:app> => $app

guard { $app.fetch("https://api.example.com/data") } => $result
$result.? ? $result.data ! error "fetch failed: {$result.!}"

The .? operator tests whether $result is valid. The .! operator extracts the :atom value when it is not.

Invalid Value

An invalid value is a first-class rill value that carries a :atom value. It is not an exception. It propagates through pipes and dict fields without halting, until an explicit check uses .? or .!.

use<ext:app> => $app

guard { $app.risky_call() } => $r
$r.!              # :atom value (e.g. #TIMEOUT, #AUTH, #ok if valid)
$r.?              # bool: true if valid

Using an invalid value as an operand in arithmetic or type assertions halts execution. Test with .? before use.

See Error Handling for full patterns and Error Reference for pre-registered atoms.


Bounded Retry with retry<N>

retry<N> { body } runs its body up to N times. Each attempt that returns an invalid value triggers the next attempt. The result is the first valid value, or the final invalid value when all attempts fail.

use<ext:app> => $app

retry<3> { $app.fetch("https://api.example.com/data") } => $result

retry is strictly bounded; N must be a literal integer. Use guard inside retry to convert halts to invalid values that retry can detect.

use<ext:app> => $app

retry<3> {
  guard { $app.fetch("https://api.example.com/data") }
} => $result
$result.? ? $result ! error "all retries failed: {$result.!}"

When to Use retry vs. do-while

PatternUse when
retry<N> { guard { ... } }Fixed maximum attempts, automatic backoff not needed
do { } while (cond)Custom retry logic, delay, or condition-based exit

See Error Handling for retry with backoff patterns.


Control Flow Summary

StatementScopeEffect
breakLoopExit loop with current $
$val -> breakLoopExit loop with value
returnBlock/ScriptExit block or script with current $
$val -> returnBlock/ScriptExit block or script with value
passAnyReturns current $ unchanged
assert condAnyHalt if condition false, pass through on success
assert cond "msg"AnyHalt with custom message if condition false
error "msg"AnyAlways halt with error message
$val -> errorAnyAlways halt with piped error message (must be string)
guard { body }AnyReplace halt with invalid value
retry<N> { body }AnyRetry up to N times on invalid result

Patterns

Guard Clauses

Exit early on invalid conditions (assumes host provides error()):

use<ext:app> => $app

|data| {
  $data -> .empty ? $app.error("Empty input")
  $data -> :?list ? $ ! $app.error("Expected list")
  $data -> seq({ $ * 2 })
} => $process

Retry with Limit

use<ext:app> => $app

do<limit: 3> {
  $app.prompt("Try operation")
} while (.contains("RETRY"))

.contains("SUCCESS") ? [0, "Done"] ! list[1, "Failed"]

State Machine

"start" -> while ($ != "done") do {
  ($ == "start") ? "processing" ! ($ == "processing") ? "validating" ! ($ == "validating") ? "done" ! $
}
# Walks through states: start -> processing -> validating -> done

Find First Match

[1, 2, 3, 4, 5] -> seq({
  ($ > 3) ? ($ -> break)
  $
})
# Returns 4 (first element > 3)

Loop Syntax Migration

rill previously used @ as the loop operator with ^(limit: N) annotations. The current syntax uses while/do keywords with the do<limit: N> construct option.

Old syntaxNew syntax
initial -> (cond) @ { body }initial -> while (cond) do { body }
initial -> @ { body } ? (cond)initial -> do { body } while (cond)
initial -> (cond) @ ^(limit: N) { body }initial -> while (cond) do<limit: N> { body }
initial -> @ ^(limit: N) { body } ? (cond)initial -> do<limit: N> { body } while (cond)

See CHANGELOG.md for the full change history.


See Also