Skip to content

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:

 (if (> 5 0) "positive" "non-positive")
"positive"

Without an else branch, if returns 0:

 (if 0 "yes")
0

Sequential Evaluation: do

do evaluates expressions in order and returns the last result. let bindings inside do are scoped to that block:

 (do
    (set x 10)
    (set y 20)
    (+ x y))
30

Variable Binding: set and let

set creates a global binding. let creates a local binding scoped to the enclosing do:

 (set x 42)
42

 (do (let y 10) y)
10

The variable y is not visible outside the do block.

Lambda Functions: fn

Create anonymous functions with fn. Parameters are listed in square brackets:

 (set add1 (fn [x] (+ x 1)))
 (add1 5)
6

Recursive lambdas use self to refer to the enclosing function:

 (set fib (fn [n] (if (<= n 1) n (+ (self (- n 1)) (self (- n 2))))))
 (fib 10)
55

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). Because lambdas do not capture closures, this is the only way to surface an outer binding from the failure branch:

 (try (raise "boom") 0)
0

 ((fn [data] (try (raise "boom") data)) 123)
123

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.

Early Return: return

return exits the innermost enclosing compiled lambda early with the given value:

 (set f (fn [x] (if (< x 0) (return -1) (+ x 1))))
 (f -5)
-1
 (f 5)
6

The zero-arg form returns null:

 ((fn [] (return)))

return works inside (try ...): the trap frame is unwound cleanly before the lambda exits.

 ((fn [] (try (return 42) (fn [e] e))))
42

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
  • println and show return null (not printed in the REPL)
 (nil? 0Nl)
true

 (+ 0Nl 1)
0Nl

 (nil? (println "hello"))
hello
true

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]

;; where returns indices matching a condition
 (where (> (til 10) 3))
[4 5 6 7 8 9]