Control Flow & Error Handling
Conditionals, variable binding, lambdas, error handling, null semantics, and higher-order functions in Rayfall.
Conditionals: if
The if special form evaluates a condition and returns the corresponding branch:
Without an else branch, if returns 0:
Sequential Evaluation: do
do evaluates expressions in order and returns the last result. let bindings inside do are scoped to that block:
Iteration: while
while evaluates cond, and while it is truthy evaluates each body expression
in order, then tests again. It always returns null — it is a statement form,
run for effect.
It is the only iteration form that can stop early. map, fold, scan and
prior all consume their whole input, so a "repeat until done" loop written as
a fold over a fixed range pays that range's full length on every call, however
early the work finishes. while stops when the condition says stop, allocates
no range, and does not recurse — so it is not bounded by the stack depth a
recursive loop would hit.
The body may be omitted, in which case a condition with side effects is the whole loop. That is the natural shape when there is no sequence to iterate over in the first place:
Unlike do, while pushes no scope of its own. A let in the body binds in
the enclosing frame and therefore survives the iteration, which is what makes a
let usable as a loop variable inside a lambda:
When a fresh binding per pass is wanted instead, wrap the body in do, which
does push a scope:
A loop whose condition never goes false runs until interrupted; Ctrl-C breaks out of one at the REPL.
Bounded Iteration: times
times runs the body a fixed number of times and returns null.
The count is evaluated once, on entry, so the bound is fixed however the body mutates whatever produced it:
A count of zero or less runs the body zero times rather than raising — a bound
that computes to empty is a no-op, not an error. A non-integer count is a type
error. Like while, times pushes no scope of its own, so a let in the body
binds in the enclosing frame; wrap the body in do when a fresh binding per
pass is wanted.
Reach for times when the number of passes is known up front and for while
when it is not. Neither allocates a sequence, so neither pays for a range that
exists only to be counted.
Variable Binding: set and let
set creates a global binding. let creates a local binding scoped to the enclosing do:
The variable y is not visible outside the do block.
Lambda Functions: fn
Create anonymous functions with fn. Parameters are listed in square brackets:
Recursive lambdas use self to refer to the enclosing function:
Error Handling: try / raise
raise throws an error with an arbitrary value. try catches it and passes the value to a handler function:
‣ (try (raise 42) (fn [e] e))
42
‣ (try (raise 42) (fn [e] (+ e 1)))
43
‣ (try (raise "boom") (fn [e] "caught"))
"caught"
If no error is raised, try returns the result of the body expression normally. Works inside lambdas compiled to bytecode.
Fallback value
If the second argument is not a function, it is returned as-is as the fallback value on error (evaluated only when the body fails):
A handler must accept the single error argument, so only a lambda or a unary builtin is called with the error; any other value (including a multi-argument builtin) is treated as a fallback value.
Nested lambdas capture visible lexical bindings when they are created:
Early Return: return
return exits the innermost enclosing compiled lambda early with the given value:
The zero-arg form returns null:
return works inside (try ...): the trap frame is unwound cleanly before the lambda exits.
return only exits the lambda it is lexically inside — not any outer lambdas. There is no non-local return.
Null Semantics
Nulls are sentinel-encoded in the payload (INT64_MIN for i64, NaN for
f64, and so on), not a separate bitmap. Typed null literals produce the
sentinel for their type:
| Literal | Type |
|---|---|
0Nl |
i64 null |
0Nf |
f64 null |
0Ni |
i32 null |
0Nh |
i16 null |
0Nd |
date null |
0Nt |
time null |
0Np |
timestamp null |
Null rules:
nil?checks for null:(nil? 0Nl)→true- All null forms are falsy in
if - All null forms are equal via
==:(== 0Nl 0Nl)→true - Typed nulls propagate through arithmetic:
(+ 0Nl 1)→0Nl printlnandshowreturn null (not printed in the REPL)
Higher-Order Functions
Lambdas are auto-mapped over vectors when called directly. Use map for explicit element-wise application, fold for reductions, and scan for running accumulations:
;; map applies a function to each element, returning a list
‣ (map (fn [x] (* x x)) [1 2 3 4 5])
; => (1 4 9 16 25)
;; lambdas auto-map over vectors
‣ ((fn [x] (* x x)) (til 5))
; => [0 1 4 9 16]
;; fold reduces a vector with a binary function
‣ (fold + 0 (til 10))
; => 45
;; scan produces running accumulations
‣ (scan + [1 2 3 4 5])
; => [1 3 6 10 15]
;; fold-while stops as soon as the running result fails the predicate,
;; leaving the rest of the collection untouched
‣ (fold-while (fn [acc] (< acc 100)) + 0 (til 1000))
; => 105
;; where returns indices matching a condition
‣ (where (> (til 10) 3))
; => [4 5 6 7 8 9]
map, fold, scan and prior all consume their whole input. fold-while
is the one member of the family that can stop: the accumulator is offered to
the predicate before each step, and a falsy answer ends the fold and yields the
accumulator as it stands. The test happens before the first element too, so a
predicate that is false at the start returns the initial value untouched.
Elements are pulled one at a time, so a fold that stops after three steps costs
three elements however long the collection is.