Guards & Control Flow
Use this when expressing conditional logic without nesting (replaces if/else).
ilo uses guards instead of if/else. Guards are flat statements that return early - no nesting, no closing braces.
Basic guards
Section titled “Basic guards”Inline (semicolons replace newlines):
grd s:n>t;>=s 90 "A";>=s 80 "B";>=s 70 "C";"F"Or the same thing as a file (newlines and indentation):
grade score:n > t -- number in, text out >= score 90 "A" -- if score >= 90, return "A" >= score 80 "B" -- if score >= 80, return "B" >= score 70 "C" -- if score >= 70, return "C" "F" -- fallbackEach guard checks a condition. If true, the function returns that value. Otherwise, execution continues.
Why guards?
Section titled “Why guards?”Compare the Python equivalent:
if score >= 90: return "A"elif score >= 80: return "B"elif score >= 70: return "C"else: return "F"Guards eliminate if/elif/else keywords, braces, and deep nesting - fewer tokens to generate, fewer places for AI agents to make mistakes. Every guard chain must end with a fallback value, so you can’t have an open-ended switch that silently returns nothing.
Boolean guards
Section titled “Boolean guards”Inline:
chk x:n>t;==x 0 "zero";>x 0 "positive";"negative"Or as a file:
check x:n > t -- number in, text out == x 0 "zero" -- if x equals 0, return "zero" > x 0 "positive" -- if x > 0, return "positive" "negative" -- fallbackNegated guards
Section titled “Negated guards”Prefix a guard with ! to negate the condition:
Inline:
f x:n>n;!>x 0 0;xOr as a file:
f x:n > n -- number in, number out !> x 0 0 -- if x not greater than 0, return 0 x -- otherwise return xIf x is NOT greater than 0, return 0. Otherwise return x.
ilo 'f x:n>n;!>x 0 0;x' 5# → 5
ilo 'f x:n>n;!>x 0 0;x' -3# → 0Negated braceless guards work with any comparison. Use ^ (throw) to return an error instead of a value: !<=n 0 ^"must be positive".
Braced guards
Section titled “Braced guards”The return value can be enclosed in braces. Both forms produce identical results:
>=sp 1000 "gold" -- braceless>=sp 1000{"gold"} -- braced (identical result)Use braces when the body has multiple statements:
>=sp 1000{a=classify sp;a}The three conditional forms (side by side)
Section titled “The three conditional forms (side by side)”ilo has three syntactically distinct conditional shapes. They look similar at a glance and agents regularly mix them up, so it pays to learn the differences once.
| Form | Shape | Semantics | When to reach for it |
|---|---|---|---|
| Braceless guard | cond expr | Early return expr from the function if cond is true; otherwise fall through to the next statement. | Single-expression early exit inside a fn body. |
| Braced conditional | cond{body} | Run body if cond is true. No early return - execution continues to the next statement after the brace. | Side-effects mid-function (e.g. prnt), or ret inside a loop. |
| Brace ternary | cond{then}{else} | Evaluate then or else. No early return - produces a value. | Value with two arms, statement or expression position. |
Prefix ternary (?h) | ?h cond a b | Value: if cond then a else b. | Cheapest shape when the cond is a bool ref or complex bool expr. |
Worked side-by-side. The condition is > x 0; the arms are "pos" and "nonpos":
-- braceless guard: early return, fallthrough is the elsef x:n>t;>x 0 "pos";"nonpos"
-- braced conditional: body runs, no early return; explicit ret if neededf x:n>t;>x 0{ret "pos"};"nonpos"
-- brace ternary: value, no early return, tail expr returnsf x:n>t;>x 0{"pos"}{"nonpos"}
-- prefix ternary keyword: cheapest when the bool is already namedf x:n>t;c=>x 0;?h c "pos" "nonpos"Common mistake. ?h cond{...} is not a legal shape - ?h is the keyword prefix-ternary, so braces after the condition trigger ILO-P009. The parser hint enumerates the three canonical forms; the fix is to drop ?h (for cond{...} or cond{a}{b}) or to drop the braces (for ?h cond a b).
Negation: a leading ! works on every form - !cond expr, !cond{body}, !cond{a}{b}, !=x 1{"not one"}{"one"}.
Ternary
Section titled “Ternary”Like x == 0 ? 10 : 20 in JS/C/Go, a ternary produces a value. Unlike guards, it does not return early - code after it keeps running, unless it’s the last expression in the function.
Prefix ternary
Section titled “Prefix ternary”? followed by a comparison operator, then the true and false values:
f x:n>n;?=x 0 10 20ilo 'f x:n>n;?=x 0 10 20' f 0# → 10
ilo 'f x:n>n;?=x 0 10 20' f 5# → 20The condition must start with a comparison operator (=, >, <, >=, <=, !=). You can assign the result:
f x:n>n;v=?=x 0 10 20;+v 1 -- v is 10 or 20, then add 1Bare-bool prefix ternary. When the condition is already a boolean - a bound b variable or a call returning b - drop the comparison and use ? directly:
f x:n h:b>n;?h 10 20 -- if h then 10 else 20g x:n>n;?pos x 1 -1 -- pos x : b, returns 1 or -1The braced form ?h{a}{b} is also accepted and equivalent.
Braced ternary
Section titled “Braced ternary”A guard with two brace blocks - {then}{else}:
f x:n>t;=x 1{"yes"}{"no"}ilo 'f x:n>t;=x 1{"yes"}{"no"}' 1# → yes
ilo 'f x:n>t;=x 1{"yes"}{"no"}' 2# → noSupports negation: !=x 1{"not one"}{"one"}.
Match expressions
Section titled “Match expressions”For value matching:
Inline:
describe x:t>t;?x{"dog":"woof";"cat":"meow";_:"unknown"}Or as a file:
describe x:t > t -- text in, text out ? x { -- match on x "dog": "woof" -- if x is "dog", return "woof" "cat": "meow" -- if x is "cat", return "meow" _: "unknown" -- wildcard: catches everything else } -- end match_ is the wildcard arm. No fall-through; each arm is independent.
Matching on Result types
Section titled “Matching on Result types”Functions that can fail return R ok_type err_type (a Result). A Result isn’t a parameter type. It comes from calling a fallible function and capturing the return value. Use match to handle both cases:
~v: matches the Ok variant, binds the inner value tov^e: matches the Err variant, binds the error toe
Inline:
div a:n b:n>R n t;=b 0 ^"divide by zero";~/a bshow a:n b:n>t;r=div a b;?r{~v:str v;^e:e}Or as a file:
div a:n b:n > R n t -- two numbers in, Result out = b 0 ^"divide by zero" -- if b is 0, return Err ~/a b -- otherwise return Ok(a / b)
show a:n b:n > t -- two numbers in, text out r = div a b -- call div, capture Result in r ? r { -- match on the Result ~v: str v -- Ok: convert number to text ^e: e -- Err: return error message } -- end matchdiv returns R n t: either an Ok number or an Err string. show captures the Result in r (without auto-unwrapping) and matches on it.
You can also skip the rebind and put the call directly in the scrutinee slot — either bare or parenthesised, both parse the same:
show a:n b:n>t;?div a b{~v:str v;^e:e}show a:n b:n>t;?(div a b){~v:str v;^e:e}ilo 'div a:n b:n>R n t;=b 0 ^"divide by zero";~/a bshow a:n b:n>t;r=div a b;?r{~v:str v;^e:e}' show 10 2# → 5
ilo 'div a:n b:n>R n t;=b 0 ^"divide by zero";~/a bshow a:n b:n>t;r=div a b;?r{~v:str v;^e:e}' show 10 0# → divide by zeroSee Error Handling for more on ~ (Ok), ^ (Err), and the R type.
Matching on type
Section titled “Matching on type”When the input type is unknown (_), match on the runtime type:
Inline:
f x:_>t;?x{n v:"number";t v:"text";_:"other"}Or as a file:
f x:_ > t -- any type in, text out ? x { -- match on runtime type n v: "number" -- if x is a number t v: "text" -- if x is text _: "other" -- anything else } -- end matchEach arm specifies a type tag (n, t, b, l) followed by a binding variable.
Early return with ret
Section titled “Early return with ret”The last expression in a function is its return value, no return keyword needed. Guards also return early when a condition matches. Use ret when you need an explicit early return from inside a loop or braced block:
Inline:
f x:n>n;>x 0{ret x};0Or as a file:
f x:n > n -- number in, number out > x 0 { ret x } -- if x > 0, return x early 0 -- fallbackGuards already provide early return for simple cases. Use ret when you need early return inside a loop or deeply nested block:
Inline:
f xs:L n>n;@x xs{>=x 10{ret x}};0 -- return first element >= 10Or as a file:
f xs:L n > n -- list of numbers in, number out @x xs { -- loop over list >= x 10 { ret x } -- if x >= 10, return it early } -- end loop 0 -- fallback if none found