# ilo - Full Documentation
> Token-optimised programming language for AI agents.
Generated: 2026-09-13
Source: https://ilo-lang.ai
Documents: 38
---
# Collections
URL: https://ilo-lang.ai/docs/builtins/collections/
Use this when working with lists, maps, or other collection operations.
Use this when working with lists, maps, or other collection operations.
## Lists
Lists use `L` followed by the element type: `L n` (list of numbers), `L t` (list of text). Elements are separated by spaces or commas. Variables and expressions work as elements.
```ilo
nums=[1 2 3 4 5] -- L n (list of numbers)
words=["hi" "bye"] -- L t (list of text)
w="world"
greet=["hi" w] -- L t (variables work too)
args=["search" 10 true] -- L _ (mixed types)
```
### Operations
Built-in functions for working with lists:
```ilo
len nums -- → 5
hd nums -- → 1 (first element)
tl nums -- → [2 3 4 5] (rest)
nums.2 -- → 3 (dot-notation, zero-indexed)
+=nums 6 -- → [1 2 3 4 5 6] (append)
rev nums -- → [5 4 3 2 1] (reverse)
srt nums -- → [1 2 3 4 5] (sort ascending)
rev(srt nums) -- → [5 4 3 2 1] (sort descending)
```
#### `srt` / `sort` with key function
`srt` also accepts a key function for sort-by (`srt fn list`):
```ilo
neg x:n>n;-x -- negate a number
main>L n;srt neg [1 5 3 4 1] -- → [5 4 3 1 1]
```
`rsrt` mirrors `srt` and also takes a key function for descending sort-by (`rsrt fn list`):
```ilo
slen s:t>n;len s -- string length as key
main>L t;rsrt slen ["banana" "fig" "apple" "kiwi"] -- → [banana apple kiwi fig]
```
Both `srt` and `rsrt` also accept a 3-arg `fn ctx list` form where `fn` takes `(elem, ctx)`. This is the cross-engine alternative when you want explicit state without forming a closure.
`srt` is **stable**: elements (or items with equal keys, for the key-function form) keep their relative input order. This matters when merging parallel streams sorted by a shared key - e.g. timeline events that share a timestamp keep their per-source ordering inside each tie group. The guarantee holds on every engine (tree, VM, Cranelift JIT, Cranelift AOT). `argsort` is also stable; `rsrt` mirrors `srt`'s stability.
Number functions that pair well with lists:
```ilo
flr 3.7 -- → 3 (floor)
cel 3.2 -- → 4 (ceiling)
rou 3.5 -- → 4 (round)
```
### Higher-order functions
Pass functions to `map`, `flt`, and `fld` to transform, filter, and reduce lists. Most builtins have a short and long form, both work: `flt` or `filter`, `fld` or `fold`, `srt` or `sort`, etc. See the full [alias table](/docs/builtins/numbers#aliases). The examples below use short forms inline and long forms in multiline.
#### `map`
`map fn list` applies `fn` to every element, returning a new list:
```ilo
dbl x:n>n;*x 2 main xs:L n>L n;map dbl xs -- → [2 4 6 8 10]
```
Or as a file:
```ilo
double x:n > n -- double a number
*x 2 -- multiply x by 2
main nums:L n > L n -- map double over a list
map double nums -- → [2 4 6 8 10]
```
#### `flt` / `filter`
`flt fn list` keeps only elements where `fn` returns true:
```ilo
pos x:n>b;>x 0 main xs:L n>L n;flt pos xs -- → [1 2 3 4 5]
```
Or as a file:
```ilo
positive x:n > b -- is positive?
> x 0 -- x greater than 0
main nums:L n > L n -- keep only positives
filter positive nums -- → [1 2 3 4 5]
```
#### `fld` / `fold`
`fld fn list init` reduces a list to a single value. Applies `fn` to an accumulator and each element left-to-right. Like `reduce` in JavaScript or `foldl` in Haskell:
```ilo
add a:n b:n>n;+a b
main xs:L n>n;fld add xs 0 -- → 15
```
Or as a file:
```ilo
add a:n b:n > n -- add two numbers
+a b -- return a + b
main nums:L n > n -- sum via fold
fold add nums 0 -- → 15
```
#### `sum`
Shorthand for folding with addition:
```ilo
sum nums -- → 15
```
#### Inline lambdas
Instead of declaring a one-off helper, pass a function literal directly to any HOF:
```ilo
by-dist xs:L n>L n;srt (x:n>n;abs x) xs -- sort by distance from zero
nonempty ws:L t>L t;flt (s:t>b;>(len s) 0) ws -- keep non-empty strings
sumsq xs:L n>n;fld (a:n x:n>n;+a *x x) xs 0 -- sum of squares
```
Syntax is the same as a top-level function declaration, wrapped in parens, no name: `(: ...>;)`. The body can capture variables from the enclosing scope:
```ilo
above xs:L n thr:n>L n;flt (x:n>b;>x thr) xs -- captures `thr`
```
## Dot-notation indexing
Access elements by index (zero-based) using `.`. Works on lists and maps:
```text
xs.0 -- first element
xs.2 -- third element
data.users.0 -- chained access
```
Safe navigation with `.?` returns nil instead of erroring:
```text
user.?email -- nil if "email" doesn't exist
```
## Iterating with `@`
`@` is ilo's loop construct. It iterates over a list or range, and returns the last iteration's value:
```ilo
sq-last xs:L n>n;@x xs{*x x}
```
```bash
ilo 'sq-last xs:L n>n;@x xs{*x x}' 3,4,5
# → 25 (last element 5, squared)
```
Use `@` with a range to loop over numbers:
```bash
ilo 'f>n;s=0;@i 0..5{s=+s i};s' f
# → 10 (sum of 0+1+2+3+4)
```
Use braces when the body has multiple statements:
```bash
ilo 'f>L n;xs=[];@i 0..3{xs=+=xs i};xs' f
# → [0, 1, 2]
```
## Slice, contains, and append
Common operations for working with lists and text:
### `slc` - slice a list or text
`slc xs a b` returns elements from index `a` up to (but not including) `b`:
```bash
ilo 'f xs:L n>L n;slc xs 1 3' 10,20,30,40
# → [20, 30]
```
Negative indices count from the end (Python-style); bounds clamp instead of wrapping. One ergonomic exception: when the start is non-negative and the end is exactly `-1`, the end is read as `len xs` (Python/JS "to end of list/text" sugar):
```bash
ilo 'f xs:L n>L n;slc xs 2 -1' 10,20,30,40 # "to end" sugar
# → [30, 40]
ilo 'f xs:L n>L n;slc xs 0 -1' 10,20,30,40 # full list (-1 = to end)
# → [10, 20, 30, 40]
ilo 'f xs:L n>L n;slc xs -2 (len xs)' 10,20,30,40 # keep last two
# → [30, 40]
ilo 'f xs:L n>L n;slc xs -3 -1' 10,20,30,40 # negative start: Python style
# → [20, 30]
ilo 'f xs:L n>L n;slc xs 0 -2' 10,20,30,40 # end -2 keeps Python style
# → [10, 20]
```
`at xs i`, `take n xs`, and `drop n xs` follow the same Python-style rule. To "drop the last element" use `take -1 xs` (the `slc xs 0 -1` shape was repurposed as the "to end" sugar above):
```bash
ilo 'f xs:L n>L n;take -1 xs' 10,20,30,40 # drop the last
# → [10, 20, 30]
```
`drop -1 xs` keeps only the last.
### `has` - contains check
`has xs v` tests membership. Works on lists (element check) and text (substring check):
```bash
ilo 'f xs:L n>b;has xs 3' 1,2,3,4
# → true
ilo 'f s:t>b;has s "llo"' "hello"
# → true
```
### `+=` - append to list
`+=xs v` appends an element to a list:
```bash
ilo 'f xs:L n>L n;+=xs 99' 1,2,3
# → [1, 2, 3, 99]
```
### `lst` - set element at index (list-set / `lset`)
`lst xs i v` returns a new list with index `i` replaced by `v`. This is the canonical list-update builtin: same role as `lset`, `setat`, or `set-at` in other languages, and exactly what you'd write `xs[i] = v` for in Python. `lset` is the long-form alias. Lists are immutable, so rebind the result:
```bash
ilo 'f xs:L n>L n;lst xs 1 99' 10,20,30
# → [10, 99, 30]
```
Negative indices count from the end (same convention as `at`/`slc`):
```bash
ilo 'f xs:L n>L n;lst xs -1 99' 10,20,30
# → [10, 20, 99]
```
Reach for `lst` whenever you'd reach for in-place index assignment in another language. For flat-array 2D grids, combine with stride arithmetic: `grid=lst grid (+(*r w) c) v`.
## Maps (`M k v`)
Maps are key-value collections, like dictionaries in Python or objects in JavaScript. Keys are typed: text (`t`) or integer (`n`). `Int(1)` and `Text("1")` are distinct, so a numeric index map and a string-keyed map can't accidentally collide. Maps are immutable: `mset` and `mdel` return new maps rather than modifying in place.
```ilo
idx=mmap
idx=mset idx 7 "seven" -- integer key, no str conversion
mget idx 7 -- → "seven"
mhas idx "7" -- → false (Int and Text are distinct)
```
### Creating maps with `mmap` and `mset`
```ilo
scores>n
m=mmap
m=mset m "alice" 99
m=mset m "bob" 87
mget m "alice"
```
```bash
ilo 'scores>n;m=mmap;m=mset m "alice" 99;m=mset m "bob" 87;mget m "alice"' scores
# → 99
```
### Map builtins
| Call | Returns | Meaning |
|------|---------|---------|
| `mmap` | `M t _` | create empty map |
| `mset m k v` | `M k v` | new map with key set |
| `mget m k` | value or nil | value at key |
| `mget-or m k default` | `v` | value at key, or `default` if missing |
| `mhas m k` | `b` | key exists? |
| `mkeys m` | `L t` | sorted list of keys |
| `mvals m` | `L v` | values sorted by key |
| `mdel m k` | `M k v` | new map without key |
| `len m` | `n` | number of entries |
### Checking keys with `mhas`
```bash
ilo 'check>b;m=mset mmap "x" "1";mhas m "x"' check
# → true
```
## Aggregation
Functions that reduce a collection to a single value:
### `sum`, `prod`, and `avg`
```bash
ilo 'f xs:L n>n;sum xs' 1,2,3,4,5
# → 15
ilo 'f xs:L n>n;prod xs' 1,2,3,4,5
# → 120
ilo 'f xs:L n>n;avg xs' 2,4,6
# → 4
```
`sum []` returns 0 (additive identity). `prod []` returns 1 (multiplicative identity).
### `cumsum` and `cprod`
Running (prefix) accumulations over a numeric list:
```bash
ilo 'f>L n;cumsum [1,2,3,4]' f
# → [1, 3, 6, 10]
ilo 'f>L n;cprod [1,2,3,4]' f
# → [1, 2, 6, 24]
```
Output length always matches input length. Empty input returns `[]`.
### `ewm` - exponential moving average
`ewm xs a > L n` smooths a numeric list with the IIR recurrence
`ewm[0] = xs[0]`, `ewm[i] = a*xs[i] + (1-a)*ewm[i-1]`. The smoothing factor
`a` must lie in `[0, 1]`; out-of-range values raise `ILO-R009` at runtime.
```bash
ilo 'f>L n;ewm [1,2,3,4,5] 0.5' f
# → [1, 1.5, 2.25, 3.125, 4.0625]
```
Boundary cases: `a=0` freezes at `xs[0]` (constant output); `a=1` reproduces
`xs` exactly. Empty input returns `[]`; a single-element list passes through
unchanged regardless of `a`. Replaces the fold-with-running-state pattern in
one call.
### `where` - parallel-list conditional select
`where cond xs ys > L a` is the NumPy `np.where` equivalent: for each `i`,
`output[i] = xs[i] if cond[i] else ys[i]`. All three lists must be the same
length; a mismatch raises `ILO-R009` at runtime. The element type of `xs` /
`ys` is preserved in the output.
```bash
ilo 'f>L n;where [true, false, true] [1, 2, 3] [10, 20, 30]' f
# → [1, 20, 3]
```
Replaces the
`map (i:n>_;?h (at cond i) (at xs i) (at ys i)) (range 0 (len xs))` recipe in
one call. Empty input returns `[]`. Works for any element type: lists of
numbers, text, or any other ilo value type are all supported as long as
`xs` and `ys` share the same element type.
### `grp` - group by key function
`grp fn xs` groups a list by a key function, returning `M t (L a)`:
```ilo
cl x:n>t;>x 5{"big"}{"small"}
classify xs:L n>M t L n;grp cl xs
```
```bash
ilo 'cl x:n>t;>x 5{"big"}{"small"} classify xs:L n>M t L n;grp cl xs' classify 1,3,7,10,2
# → {big: [7, 10], small: [1, 3, 2]}
```
### `flat` - flatten nested lists
Flattens one level of nesting:
```bash
ilo 'f>L n;flat [[1, 2], [3], [4, 5]]' f
# → [1, 2, 3, 4, 5]
```
### `unq` - deduplicate
Removes duplicates while preserving order:
```bash
ilo 'f xs:L t>L t;unq xs' a,b,a,c,b
# → [a, b, c]
```
## Function reference
| Function | Alias | Signature | Description |
|----------|-------|-----------|-------------|
| `len` | `length` | `L _ > n` | List length |
| `hd` | `head` | `L _ > _` | First element |
| `tl` | `tail` | `L _ > L _` | All elements except first |
| `at` | | `L _ n > _` | i-th element (0-indexed; negative counts from end; float `i` auto-floors) |
| `lget-or` | | `L a n a > a` | element at index `i`, or `default` if out of range (negative indices like `at`; never errors on OOB) |
| `lst` | `lset` | `L a n a > L a` | list-set: new list with index `i` replaced by `v` (same role as `lset`/`setat` in other languages; rebind because lists are immutable) |
| `rev` | `reverse` | `L _ > L _` | Reverse a list |
| `srt` | `sort` | `L _ > L _` | Sort a list |
| `srt` | `sort` | `fn L _ > L _` | Sort by key function |
| `rsrt` | | `L _ > L _` | Sort a list descending |
| `rsrt` | | `fn L _ > L _` | Sort descending by key function |
| `slc` | `slice` | `L _ n n > L _` | Slice (start, end) |
| `flat` | `flatten` | `L L _ > L _` | Flatten one level of nesting |
| `unq` | `unique` | `L _ > L _` | Remove duplicates |
| `has` | `contains` | `L _ _ > b` | Membership |
| `map` | | `fn L _ > L _` | Apply function to each element |
| `mapr` | | `fn L _ > R (L _) _` | Map with short-circuit Result propagation: collects Ok values, returns the first Err |
| `default-on-err` | | `R T E T > T` | Unwrap `R T E` to `T`, returning the default if Err. Mirror of `??` for Result (`??` is nil-coalesce for `O T` only). Prefer over `?r{~v:v;^_:d}` when the error payload is unused. |
| `flt` | `filter` | `fn L _ > L _` | Keep elements where function returns true |
| `ct` | `count` | `fn L _ > n` | Count elements where predicate returns true. Allocation-free vs `len (flt fn xs)`. |
| `fld` | `fold` | `fn _ L _ > _` | Reduce list to single value |
| `grp` | `group` | `fn L _ > M t L _` | Group elements by function result |
| `mmap` | | `> M` | Create empty map |
| `mget` | | `M t > _` | Get value by key |
| `mset` | | `M t _ > M` | Set key-value pair |
| `mhas` | | `M t > b` | Check if key exists |
| `mkeys` | | `M > L t` | Get all keys |
| `mvals` | | `M > L _` | Get all values |
| `mdel` | | `M t > M` | Remove key, return new map |
### Variable-index dot-notation
The variable-index form `xs.i` is sugar for `at xs i`. The parser builds a field-access node and a post-parse desugar pass rewrites it whenever the field identifier resolves to a binding in the current scope (parameter, let, foreach, range, match-arm). If the identifier is also a declared field on a record type, the rewrite is skipped and the strict `.field` record-access semantics apply.
## See also
- [Text](/docs/builtins/text/) for string operations (`trm`, `spl`, `fmt`, `cat`, `rgx`)
- [Numbers](/docs/builtins/numbers/) for numeric aggregation (`min`, `max`, `median`, `stdev`)
---
# Crypto and encoding
URL: https://ilo-lang.ai/docs/builtins/crypto/
Use this when hashing, signing, encoding, or verifying secrets in ilo. Covers sha256, hmac-sha256, b64u, urlenc, hex, and ct-eq.
Use this when hashing, signing, encoding, or verifying secrets. All crypto builtins are tree-bridge eligible: pure text/bytes operations with no I/O, so VM and Cranelift inherit them automatically.
## Hashing
| Function | Signature | Description |
|----------|-----------|-------------|
| `sha256` | `t > t` | SHA-256 hex digest (lowercase, 64 chars) of the UTF-8 bytes of `s` |
| `hmac-sha256` | `t t > t` | HMAC-SHA256 of `body` under `key`, returns lowercase hex |
```ilo
sha256 "" -- e3b0c44298fc1c149afbf4c8996fb924...
sha256 "abc" -- ba7816bf8f01cfea414140de5dae2223...
len (sha256 "x") -- 64 (always)
hmac-sha256 "secret" "payload" -- lowercase hex digest
```
`sha256` always returns a 64-character lowercase hex string. Input is always treated as text (UTF-8 bytes).
`hmac-sha256` is the standard choice for webhook signature verification and API request signing. Use `ct-eq` (not `==`) to compare the result against an untrusted value.
## Constant-time comparison
| Function | Signature | Description |
|----------|-----------|-------------|
| `ct-eq` | `t t > b` | Constant-time text equality - no timing side-channel |
```ilo
-- Webhook signature verification
verify sig:t body:t>b
expected=hmac-sha256 "secret" body
ct-eq expected sig
```
Always use `ct-eq` instead of `==` when comparing secrets (HMAC digests, tokens, API keys). `==` short-circuits on the first differing byte, which leaks timing information that an attacker can exploit. `ct-eq` always reads every byte.
## Base64url encoding
| Function | Signature | Description |
|----------|-----------|-------------|
| `b64u` | `t > t` | Base64url encode (RFC 4648 section 5, no padding, URL-safe `-`/`_` alphabet). Total. |
| `b64u-dec` | `t > R t t` | Base64url decode; Err on input outside the alphabet, on `=` padding (strict no-pad round-trip), or on non-UTF-8 decoded bytes |
```ilo
b64u "hello" -- "aGVsbG8" (no padding)
b64u-dec! (b64u "hello") -- "hello"
b64u "??>" -- "Pz8-" (URL-safe; standard b64 would emit "Pz8+")
```
`b64u-dec` returns `R t t`. Use `!` to auto-unwrap or handle the `Err` arm for invalid input. The Ok branch holds a decoded text string (valid UTF-8); non-UTF-8 decoded bytes always Err.
Use `b64u`/`b64u-dec` for URL-safe base64 (JWT headers, URL parameters, webhook payloads).
## Cryptographic random
| Function | Signature | Description |
|----------|-----------|-------------|
| `rand-bytes` | `n > t` | `n` cryptographically random bytes from the platform CSPRNG (via `getrandom`), encoded as base64url-no-pad. Distinct from `rnd` / `rndn` (seedable, for simulations). Capped at 1 MiB; non-negative `n` only |
```ilo
rand-bytes 16 -- 22-char b64url, 128 bits of entropy (jti / CSRF token)
rand-bytes 32 -- 43-char b64url, 256 bits of entropy (session id)
len (rand-bytes 16) -- always 22 (output length is deterministic)
```
Use `rand-bytes` for JWT `jti` claims, CSRF tokens, session IDs, and nonces. Output is base64url-no-pad so the result drops into HTTP headers, cookies, and query strings without further encoding. Encoded length is deterministic: `ceil(n * 4 / 3)` characters with the trailing `=` chars stripped. The bytes themselves are unpredictable.
`rand-bytes` is NOT the same as `rnd`. `rnd` is a seedable PRNG suitable for simulations and Monte Carlo work, NOT for tokens or keys. If a value will be checked against an attacker-controlled comparison, reach for `rand-bytes`, never `rnd`.
## URL percent-encoding
| Function | Signature | Description |
|----------|-----------|-------------|
| `urlenc` | `t > t` | RFC 3986 percent-encode; unreserved chars (`ALPHA`/`DIGIT`/`-._~`) pass through, everything else becomes `%HH`. Total. |
| `urldec` | `t > R t t` | Inverse of `urlenc`; Err on invalid percent escape or non-UTF-8 decoded bytes |
```ilo
urlenc "a b&c=d" -- "a%20b%26c%3Dd"
urldec! "a%20b%26c%3Dd" -- "a b&c=d"
urlenc "café" -- "caf%C3%A9" (UTF-8 byte-by-byte)
```
Both are tree-bridge eligible (no I/O, no FnRef). The encoder is total; the decoder returns `Result` so malformed input (`"abc%"`, `"abc%2"`, or escapes that decode to invalid UTF-8) surfaces typed at the boundary.
## Hex encoding
| Function | Signature | Description |
|----------|-----------|-------------|
| `hex` | `t > t` | Lowercase hex encode of the UTF-8 bytes of a string (every byte becomes two hex chars) |
| `hex-rev` | `t > t` | Reverse the byte order of a hex string, byte-pair-wise; odd length errors ILO-T013 |
```ilo
hex "hello" -- "68656c6c6f"
hex-rev "abCD" -- "CDab"
```
`len (hex s)` is `2 * len s` for ASCII input. `hex-rev` preserves case and is the little-endian/big-endian conversion tool (e.g. Bitcoin txid byte order).
## Common patterns
**Webhook verification (GitHub-style):**
```ilo
verify sig:t body:t>b
expected=hmac-sha256 "your-secret" body
ct-eq expected sig
```
**JWT header construction:**
```ilo
header=b64u (jdmp (pt alg:"HS256" typ:"JWT"))
payload=b64u (jdmp data)
```
**OAuth query string:**
```ilo
q=cat [(urlenc "client_id") "=" (urlenc cid) "&" (urlenc "redirect_uri") "=" (urlenc ru)] ""
```
**Round-trip check:**
```ilo
s="hello world & friends=42"
urldec! (urlenc s) -- "hello world & friends=42"
b64u-dec! (b64u s) -- "hello world & friends=42"
```
---
# Data & I/O
URL: https://ilo-lang.ai/docs/builtins/data-io/
Use this when reading or writing files, parsing CSV/JSON, or accessing environment variables.
Use this when reading or writing files, parsing CSV/JSON, or accessing environment variables. For network I/O see [HTTP](/docs/builtins/http/).
ilo has built-in support for reading and writing files and accessing environment variables. All I/O builtins return `R` (Result) types, use `!` to auto-unwrap.
## Reading files
`rd path` reads a file with format auto-detected from the extension:
| Extension | Returns |
|-----------|---------|
| `.csv` | `R _ t` -- list of lists (grid) |
| `.tsv` | `R _ t` -- list of lists (grid) |
| `.json` | `R _ t` -- parsed JSON value |
| other | `R _ t` -- raw text string |
```ilo
load p:t>R _ t;rd p
```
Force a specific format with a second argument:
```ilo
load p:t>R _ t;rd p "json"
```
### Reading lines
`rdl path` reads a file as a list of lines:
```ilo
lines p:t>R L t t;rdl p
```
### Parsing buffers
`rdb string format` parses a string in a given format - useful for data received from HTTP responses:
```ilo
parse s:t>R _ t;rdb s "json"
```
### Listing directories
`lsd dir` returns the entries of `dir` (filenames only, not full paths), sorted lexicographically. Both file and directory entries are included. Missing directories surface as `Err`; empty directories return `[]`, not `Err`.
Renamed from `ls` in 0.12.1 so the natural `ls=rdl! p` binding for "lines" stays free for user code.
```ilo
entries d:t>R (L t) t;lsd d
```
### Recursive walk
`walk dir` does a depth-first traversal of `dir` and returns every path it finds relative to `dir`, sorted. Symlinks are not followed (matches the safer of the two `find` defaults and avoids the cycle trap that bites recursive globs in symlinked trees).
Unreadable subdirectories (most commonly permission-denied: sandbox roots, sibling-user homes, `/var/db` on macOS) are silently skipped. The locked dir's own entry is still in the result; just its contents are not enumerated. This means `walk /` or `walk ~/` produce useful output instead of aborting on the first locked sibling. An unreadable *root* still returns `Err`, so "starting point unreadable" is distinguishable from "some descendant unreadable".
```ilo
all p:t>R (L t) t;walk p
```
### Glob
`glob dir pat` returns the paths under `dir` (relative, sorted) that match a shell-style pattern. `*`/`?`/`[abc]` match within a single path segment; `**` matches any number of nested segments (including zero), so `**/*.ilo` finds every `.ilo` file in the tree, including ones in `dir` itself. No matches returns `[]`, not `Err`. Shares `walk`'s traversal, so unreadable subdirectories are skipped silently here too.
```ilo
ilo-files d:t>R (L t) t;glob d "**/*.ilo"
csv-here d:t>R (L t) t;glob d "*.csv"
```
### File metadata
Four atomic primitives sit alongside the directory enumerators. The shape is deliberately not one fat `stat` map: agents that want the size pay the size cost, agents that want a predicate pay the predicate cost, and the natural one-liners stay one token wide.
- `fsize path > R n t` returns the file size in bytes. `Err` on missing, permission-denied, or path-is-directory.
- `mtime path > R n t` returns the last modification time as Unix epoch seconds (`f64`, fractional preserved). `Err` on missing or permission-denied. Pairs with `now` for "is this file older than N seconds" checks.
- `isfile path > b` returns `true` iff `path` resolves to a regular file. Missing, permission-denied, and non-file all collapse to `false`.
- `isdir path > b` returns `true` iff `path` resolves to a directory. Same `false`-on-failure collapse as `isfile`.
The asymmetry between size/mtime (`R n t`) and the predicates (`b`) is deliberate. Predicates almost always feed into a single-token branch (`?isfile p{...}`), so collapsing the error tier into `false` is the right ergonomic - Python uses the same convention for `os.path.isfile`. Size and mtime callers usually want to distinguish "missing" from "permission-denied" from "this is a directory, not a file", so the error tier stays exposed.
All four follow symlinks (POSIX `stat`, not `lstat`). Metadata calls are leaf queries where "what does this path resolve to?" is almost always the question; `walk` and `glob` are the place where symlink-following needs to be off (for cycle safety).
```ilo
sz p:t>R n t;fsize p
isnew p:t>R b t;m=mtime! p;d=- now m;~t;f=isfile p;d=isdir p;?f{true:"file";false:?d{true:"dir";false:"other"}}
```
A common composition pattern is filtering `walk` output to keep only regular files:
```ilo
files d:t>L t;xs=walk! d;flt (p:t>b;isfile cat [d "/" p] "") xs
```
### Path manipulation
`dirname`, `basename`, and `pathjoin` are pure-text path operations. No I/O, no Result wrapper, total on all inputs. Unix forward-slash semantics; Windows backslash handling is a 0.13.0 concern.
`dirname path` returns the POSIX parent directory:
- `dirname "/a/b/c.txt"` → `"/a/b"`
- `dirname "/"` → `"/"` (root is its own parent, POSIX)
- `dirname "/a"` → `"/"` (single-component absolute path)
- `dirname "foo.txt"` → `""` (no directory component; POSIX returns `"."` here, ilo returns `""` so the cat round-trip below stays clean)
- `dirname "foo/"` → `""` (trailing slash stripped, no directory component remains)
- `dirname ""` → `""`
`basename path` returns the POSIX final path segment:
- `basename "/a/b/c.txt"` → `"c.txt"`
- `basename "/"` → `"/"` (root is its own basename, POSIX edge)
- `basename "foo/"` → `"foo"` (trailing slash stripped first)
- `basename ""` → `""`
`pathjoin parts` joins a list of segments with `/`, collapsing duplicate separators at joints and dropping empty segments. The list form (not variadic) avoids the arity-inference trap that `fmt`'s variadic shape lives with:
- `pathjoin ["a" "b" "c.txt"]` → `"a/b/c.txt"`
- `pathjoin ["a/" "/b/" "c.txt"]` → `"a/b/c.txt"` (joint slashes deduped)
- `pathjoin []` → `""`
- `pathjoin ["foo"]` → `"foo"`
- `pathjoin ["/" "a"]` → `"/a"` (leading absolute root preserved)
- `pathjoin ["" "a" ""]` → `"a"` (empty segments dropped)
Round-trip:
```ilo
-- Split a path and rejoin it; the result equals the original.
roundtrip p:t>t;pathjoin [dirname p basename p]
```
## Writing files
`wr path data` writes data to a file. Format can be specified as a third argument:
```ilo
save>R t t;wr "out.txt" "hello world"
```
Structured output with format:
```ilo
csv>R t t;wr "out.csv" [[1,2],[3,4]] "csv"
json>R t t;wr "data.json" [1,2,3] "json"
```
## Environment variables
`env key` reads an environment variable, returning `R t t`:
```ilo
home>R t t;env "HOME"
```
Auto-unwrap with `!`:
```ilo
home>t;env! "HOME"
```
`.env` and `.env.local` files in the working directory are loaded automatically.
## JSON
| Function | Signature | Description | Example |
|----------|-----------|-------------|---------|
| `rdb` | `t t > R _ t` | Parse data (format: `"json"`, `"csv"`) | `rdb data "json"` |
| `jpth` | `t t > R _ t` | Extract JSON path; Ok variant is the typed leaf (number→`n`, string→`t`, array→`L _`, object→record) | `jpth data "users.0.name"` |
| `jdmp` | `_ > t` | Dump value as JSON string | `jdmp [1,2,3]` |
| `jpar` | `t > R _ t` | Parse JSON string to value | `jpar '{"a":1}'` |
| `jpar!` | `t > _` | Parse JSON and auto-unwrap the Result. Inside an `R`-returning function, Err propagates to the caller; otherwise use `jpar!!` to panic on parse error. | `r=jpar! body;r.name` |
| `jpar-list` | `t > R (L _) t` | Parse JSON string, assert top-level is array | `@x (jpar-list! body){...}` |
## Time
| Function | Signature | Description | Example |
|----------|-----------|-------------|---------|
| `now` | `> n` | Current Unix timestamp (seconds) | `now` |
| `sleep` | `n > _` | Pause current engine for `ms` milliseconds | `sleep 100` |
| `dtfmt` | `n t > R t t` | Format Unix epoch as text (strftime, UTC) | `dtfmt 1700000000 "%Y-%m-%d"` |
| `dtparse` | `t t > R n t` | Parse text to Unix epoch (strftime, UTC) | `dtparse "2024-01-15" "%Y-%m-%d"` |
| `dtparse-rel` | `t n > R n t` | Resolve relative-date phrase to epoch anchored at `now` (today/yesterday/tomorrow, `N days/weeks/months ago`, `in N days/weeks/months`, `last/next/this `, ISO-8601 passthrough) | `dtparse-rel "last friday" (now)` |
| `dur-parse` | `t > R n t` | Parse human duration string into seconds. Accepts `s`/`m`/`h`/`d`/`w` abbreviations and full names (singular + plural), decimal quantities, mixed sequences. Months are **not** supported (variable length); use explicit day counts. A leading `-` is sticky - it applies to every following token until an explicit `+` resets it, so `"-1m 30s"` = `-90`. | `dur-parse "1.5 hours"` |
| `dur-fmt` | `n > t` | Format seconds as human-readable duration. Drops zero parts, uses largest units, preserves fractional seconds (up to 3 dp, trailing zeros stripped). Negative values get a single leading `-` which round-trips through `dur-parse`. | `dur-fmt 9720` → `"2h 42m"` |
| `add-mo` | `n n > n` | Add N calendar months to epoch, snapping to last valid day (Jan 31 + 1 = Feb 28/29). N may be negative. Returns epoch at 00:00 UTC. | `add-mo 1706659200 1` → `1709164800` |
| `last-dom` | `n > n` | Epoch of the last day of the month containing `dt`, at 00:00 UTC. | `last-dom 1707955200` → `1709164800` (Feb 29 2024) |
| `next-business-day` | `n > n` | Next weekday after `dt` (Fri→Mon, Sat→Mon, Sun→Mon, Mon-Thu→+1d). Returns 00:00 UTC. | `next-business-day 1705622400` → `1705881600` |
| `day-of-week` | `n > n` | Day of week: 0=Sun, 1=Mon, 2=Tue, 3=Wed, 4=Thu, 5=Fri, 6=Sat. | `day-of-week 1705276800` → `1` (Monday) |
| `tz-offset` | `t n > R n t` | UTC offset in seconds for a named IANA timezone at a given Unix epoch. DST-aware (chrono-tz). Positive = east of UTC. `Err` on unknown timezone name. | `tz-offset "Europe/London" 1719792000` → `3600` (BST) |
## Auto-unwrap `!`
Any function returning `R` (Result) can be called with `!` to auto-unwrap:
```ilo
r=$url -- r is R t t (Result)
v=$!url -- v is t (auto-unwrapped, propagates error)
data=rdb! r "json" -- auto-unwrap parse result
```
See [Error Handling](/docs/reference/error-handling/) for full details on `!`, `?`, `??`, and Result types.
## Aliases
| Long form | Short form |
|-----------|------------|
| `read` | `rd` |
| `write` | `wr` |
| `readbuf` | `rdb` |
---
# HTTP and process
URL: https://ilo-lang.ai/docs/builtins/http/
Use this when making HTTP requests, spawning child processes, or wiring external endpoints as typed ilo functions.
Use this when making HTTP requests, spawning child processes, or wiring external endpoints as typed ilo functions.
HTTP and process-spawn builtins require the native binary. They are not available in the npm/WASM build.
## Builtins
| Function | Signature | Description | Example |
|----------|-----------|-------------|---------|
| `get` | `t > R t t` | HTTP GET (returns Result) | `get "https://..."` |
| `get` | `t M > R t t` | HTTP GET with headers | `get url headers` |
| `get-to` | `t n > R t t` | HTTP GET with timeout (ms) | `get-to url 5000` |
| `pst` | `t t > R t t` | HTTP POST (url, body). Renamed from `post` in 0.12.0 | `pst url body` |
| `pst` | `t t M > R t t` | HTTP POST with headers | `pst url body headers` |
| `pst-to` | `t t n > R t t` | HTTP POST with timeout (ms) | `pst-to url body 5000` |
| `getx` | `t > R (M t _) t` | HTTP GET, rich response (status, headers, body) | `getx url` |
| `getx` | `t M > R (M t _) t` | HTTP GET, rich response, with request headers | `getx url headers` |
| `pstx` | `t t > R (M t _) t` | HTTP POST, rich response | `pstx url body` |
| `pstx` | `t t M > R (M t _) t` | HTTP POST, rich response, with request headers | `pstx url body headers` |
| `put` | `t t > R t t` | HTTP PUT (url, body) | `put url body` |
| `put` | `t t M > R t t` | HTTP PUT with headers | `put url body headers` |
| `pat` | `t t > R t t` | HTTP PATCH (url, body) | `pat url body` |
| `pat` | `t t M > R t t` | HTTP PATCH with headers | `pat url body headers` |
| `del` | `t > R t t` | HTTP DELETE | `del url` |
| `del` | `t M > R t t` | HTTP DELETE with headers | `del url headers` |
| `hed` | `t > R t t` | HTTP HEAD (body typically empty; success via Ok/Err) | `hed url` |
| `hed` | `t M > R t t` | HTTP HEAD with headers | `hed url headers` |
| `opt` | `t > R t t` | HTTP OPTIONS | `opt url` |
| `opt` | `t M > R t t` | HTTP OPTIONS with headers | `opt url headers` |
| `run` | `t L > R (M t t) t` | Spawn `cmd` with argv list. No shell, no glob. | `run "git" ["status"]` |
| `$` | `t L > R (M t t) t` | `run` shorthand (sugar for `run`) | `$"git" ["status"]` |
| `env` | `t > R t t` | Read environment variable | `env "API_KEY"` |
In 0.12.0 the `$` sigil was rebound from HTTP `get` (parochial - `$` for HTTP is unique to ilo) to the new `run` builtin (argv-list process spawn). `$cmd argv` compiles to `run cmd argv`. HTTP `get` is still called by name.
`post` was renamed to `pst` to bring it into line with the I/O compression family (`rd`, `wr`, `srt`, `flt`, `fld`, `fmt`). The old name no longer resolves; the verifier surfaces a did-you-mean hint pointing at `pst`.
`get-to` and `pst-to` add an explicit per-request timeout in milliseconds. The timeout rounds up to the nearest second internally (minreq granularity). On timeout, the call returns `Err` just like any other connection failure.
```ilo
-- 5-second timeout on GET
r=get-to url 5000
-- 3-second timeout on POST
r=pst-to url body 3000
```
## Rich-response variants: getx / pstx
`get` / `pst` / `get-to` / `pst-to` return `R t t` - body only. That is the right shape for fire-and-forget GETs and POSTs but blocks every workflow that needs response metadata: conditional requests (304 Not Modified branching on `If-None-Match`), redirect following, rate-limit headers (`X-RateLimit-Remaining`), pagination Link headers, cookie capture, status-code branching beyond Ok/Err.
`getx` and `pstx` are the rich variants. The success arm is a `Map[Text, _]` with three keys:
- `status` (`n`) - HTTP status code (200, 304, 404, 500, ...). Non-2xx responses are still Ok with the status surfaced; only transport failure (DNS, connection refused, timeout) returns Err.
- `headers` (`M t t`) - response headers. Header names are lowercased.
- `body` (`t`) - response body decoded as UTF-8.
Both accept the same optional trailing `M t t` request-headers map as `get`/`pst`.
```ilo
-- Conditional request: branch on 304 vs 200
fetch>R t t;h=mset mmap "if-none-match" "\"abc123\"";r=getx! "https://api.example.com/data" h;st=mget!! r "status";=st 304 ~"not modified";~mget!! r "body"
-- Read a rate-limit header
r=getx! "https://api.github.com/users/octocat";rem=mget!! (mget!! r "headers") "x-ratelimit-remaining"
```
Existing `get`/`pst` shapes are untouched. Pick `get` when body is all you need; pick `getx` when you need status or headers. Tree-bridge dispatched, so VM and Cranelift JIT inherit identical semantics.
## Process spawn
ilo provides two process-spawn primitives: `run` and `run2`. Both share the same no-shell-no-glob security model, the same concurrency and cap policy, and the same UTF-8 handling. They differ only in what the `Ok` payload looks like.
`run cmd argv` returns a loose Map with text values for stdout, stderr, and exit code.
```ilo
r=run "echo" ["hi"]
-- Ok({"stdout":"hi\n","stderr":"","code":"0"})
$"git" ["status" "--short"]
-- equivalent: $ is the sigil shortcut for run
```
`run2 cmd argv` returns a typed `RunResult` record with dot-access. `exit` is a number (`n`), not text, so numeric comparisons work directly.
```ilo
r=run2!! "echo" ["hi"]
-- RunResult{stdout:"hi\n"; stderr:""; exit:0}
=0 r.exit -- true
<0 r.exit -- false (signal-killed processes surface as exit:-1 on Unix)
```
Prefer `run2` for new code. `run` is kept for compatibility.
**No shell, no interpolation, no glob.** The argv list is passed directly to `std::process::Command::args`. There is no `sh -c`, no string concatenation between `cmd` and `argv`, and no glob expansion. This is the principled defence against shell injection.
**Non-zero exit is NOT an error.** `Err` is reserved for spawn failures (command not found, permission denied, output cap exceeded). A child that returns a non-zero exit code surfaces in the `Ok` arm; the caller inspects `exit` (run2) or `code` (run) and branches.
**Captured output is capped at 10 MiB per stream.** Either stream exceeding the cap returns an `Err` rather than partial capture.
**Inherits parent env + cwd.** No override knobs in v1. Stdin is `/dev/null`.
## Example
```ilo
fetch url:t>R t t;get url
```
Auto-unwrap with `!` propagates errors automatically:
```ilo
f url:t>R t t;r=get! url;~r
```
See [Error Handling](/docs/reference/error-handling/) for full details on `!` and Result types.
## Parsing Content-Type and other `;`-delimited headers
There is no dedicated `ct-parse` builtin. The same recipe handles `Content-Type`, `Cache-Control`, `Cookie`, `Set-Cookie` attributes, and any other header with `key=value` parameters after a `;` separator: `spl` on `;`, `trm`, lowercase, then `spl "="` for the parameter:
```ilo
raw = "application/json; charset=utf-8"
parts = spl raw ";"
m0 = at parts 0
media = lwr (trm m0) -- "application/json"
n = len parts
cs = ?(>=n 2){at parts 1}{""}
kv = spl (trm cs) "="
charset = ?(=(len kv) 2){lwr (at kv 1)}{""} -- "utf-8" or ""
```
Don't bind to `ct`, it collides with a builtin name (`ILO-P011`). Use `raw`, `ctype`, or similar.
For `,`-joined values such as `Accept: text/html, application/json`, `spl ","` first and loop the resulting list through the same recipe.
## HTTP tools (typed endpoints)
Tool declarations let you wire external HTTP endpoints as typed ilo functions. Create a `tools.json` file:
```json
{
"tools": {
"weather": {
"url": "https://api.example.com/weather"
},
"lookup": {
"url": "https://api.example.com/lookup"
}
}
}
```
Each key is the tool name. The value must include a `url` field.
```bash
ilo --tools tools.json 'get-weather city:t>R t t;weather city' "London"
```
Tools are type-checked at load time, the agent cannot call a tool with the wrong parameter types.
See also [Tools](/docs/builtins/tools/) for MCP integration.
---
# Numbers
URL: https://ilo-lang.ai/docs/builtins/numbers/
Use this when doing arithmetic, comparisons, aggregation, or numeric conversions.
Use this when doing arithmetic, comparisons, aggregation, or numeric conversions.
## Arithmetic operators
| Operator | Description | Example | Result |
|----------|-------------|---------|--------|
| `+` | Add | `+3 4` | `7` |
| `-` | Subtract | `-10 3` | `7` |
| `*` | Multiply | `*3 4` | `12` |
| `/` | Divide | `/10 2` | `5` |
| `%` | Modulo | `%10 3` | `1` |
## Bitwise operators
Work on `u32`, `u64`, and `i64` integer types.
| Operator | Description | Example |
|----------|-------------|---------|
| `band` | Bitwise AND | `band 0xF0u 0x0Fu` -> `0u` |
| `bor` | Bitwise OR | `bor 0xF0u 0x0Fu` -> `0xFFu` |
| `bxor` | Bitwise XOR | `bxor 0xFFu 0x0Fu` -> `0xF0u` |
| `bnot` | Bitwise NOT (complement) | `bnot 0u` -> `0xFFFFFFFFu` |
| `shl` | Left shift | `shl 1u 4` -> `16u` |
| `shr` | Right shift (logical for u32/u64, arithmetic for i64) | `shr 16u 4` -> `1u` |
64-bit variants (`band64`, `bor64`, `bxor64`, `bnot64`, `shl64`, `shr64`) operate on `u64` and `i64` values, allowing full 64-bit integer manipulation for crypto, binary codecs, and WASM interop.
## Comparison operators
| Operator | Description | Example |
|----------|-------------|---------|
| `==` | Equal | `==a b` |
| `!=` | Not equal | `!=a b` |
| `>` | Greater than | `>a b` |
| `<` | Less than | `=` | Greater or equal | `>=a b` |
| `<=` | Less or equal | `<=a b` |
All comparisons return `b`.
## Logic
| Operator | Description | Example |
|----------|-------------|---------|
| `&` | Logical AND | `&a b` |
| `\|` | Logical OR | `\|a b` |
| `!` | Logical NOT | `!a` |
## Math
| Function | Signature | Description | Example |
|----------|-----------|-------------|---------|
| `abs` | `n > n` | Absolute value | `abs -5` -> `5` |
| `flr` | `n > n` | Floor | `flr 3.7` -> `3` |
| `cel` | `n > n` | Ceiling | `cel 3.2` -> `4` |
| `rou` | `n > n` | Round to nearest integer (banker's rounding) | `rou 3.7` -> `4` |
| `rnd` | `> n` | Random float in `[0, 1)`. Aliases: `rand`, `random`. Read this as random, not round; for rounding use `rou`. | `rnd` -> `0.42…` |
| `rnd` | `n n > n` | Random integer in `[a, b]` inclusive | `rnd 1 6` -> dice |
| `rndn` | `n n > n` | One sample from N(mu, sigma) (Box-Muller) | `rndn 0 1` -> `0.34…` |
| `seed` | `n > _` | Set the shared PRNG state (SplitMix64). All subsequent `rnd`/`rndn` on every engine use this state. Default seed is deterministic (no wall-clock). Returns `_`. For entropy: `seed (now-ms)`. | `seed 42` |
## Aggregation
| Function | Signature | Description | Example |
|----------|-----------|-------------|---------|
| `sum` | `L n > n` | Sum a list of numbers | `sum [1,2,3]` -> `6` |
| `avg` | `L n > n` | Average of a list of numbers | `avg [2,4,6]` -> `4` |
| `min` | `L n > n` | Minimum element of a numeric list (error if empty) | `min [3,1,4,1,5]` -> `1` |
| `max` | `L n > n` | Maximum element of a numeric list (error if empty) | `max [3,1,4,1,5]` -> `5` |
| `min` | `n n > n` | Minimum of two numbers | `min 3 7` -> `3` |
| `max` | `n n > n` | Maximum of two numbers | `max 3 7` -> `7` |
| `median` | `L n > n` | Median of a numeric list | `median [1,2,3,4,5]` -> `3` |
| `stdev` | `L n > n` | Sample standard deviation (divides by N-1) | `stdev [2,4,4,4,5,5,7,9]` |
| `variance` | `L n > n` | Sample variance (divides by N-1) | `variance [...]` |
| `argmax` | `L n > n` | Index of max element (first occurrence wins on ties; errors on empty) | `argmax [3,1,9,2]` -> `2` |
| `argmin` | `L n > n` | Index of min element (first occurrence wins on ties; errors on empty) | `argmin [3,1,9,2]` -> `1` |
| `argsort` | `L n > L n` | Sorted-index permutation ascending (stable; empty returns `[]`) | `argsort [3,1,2]` -> `[1,2,0]` |
## Numeric prelude (list constructors)
| Function | Signature | Description | Example |
|----------|-----------|-------------|---------|
| `linspace` | `n n n > L n` | `n` evenly-spaced floats from `a` to `b` inclusive (numpy `endpoint=True`). `n=0` returns `[]`; `n=1` returns `[a]`. Last element pinned to `b` to avoid float-accumulated drift. | `linspace 0 10 5` -> `[0, 2.5, 5, 7.5, 10]` |
| `ones` | `n > L n` | `n` copies of `1.0`. `n=0` returns `[]`. Saves the design-matrix-column `map (i:n>n;1) (range 0 n)` cascade. | `ones 5` -> `[1, 1, 1, 1, 1]` |
| `rep` | `n T > L T` | `n` copies of any value (element type follows the value). `n=0` returns `[]`. Saves `map (i:n>T;v) (range 0 n)` for accumulator seeding and constant tables. | `rep 3 7` -> `[7, 7, 7]`, `rep 3 "x"` -> `["x", "x", "x"]` |
All three reject negative `n` with `ILO-R009` and cap at 1,000,000 elements.
## Conversions
| Function | Signature | Description | Example |
|----------|-----------|-------------|---------|
| `str` | `_ > t` | Convert to string | `str 42` -> `"42"` |
| `num` | `t\|n > R n t` | Polymorphic: parse text or identity-wrap a number as Ok. Saves the `num (str x)` roundtrip when `x` may already be numeric (e.g. from `jpar!` on a JSON number) | `num "42"` -> `Ok 42`, `num 42` -> `Ok 42` |
## Aliases
| Long form | Short form |
|-----------|------------|
| `average` | `avg` |
| `floor` | `flr` |
| `ceil` | `cel` |
| `round` | `rou` |
| `random` | `rnd` |
| `rand` | `rnd` |
| `number` | `num` |
| `string` | `str` |
---
# Text
URL: https://ilo-lang.ai/docs/builtins/text/
Use this when concatenating, formatting, splitting, searching, or matching strings.
Use this when concatenating, formatting, splitting, searching, or matching strings.
## Core operations
| Function | Signature | Description | Example |
|----------|-----------|-------------|---------|
| `cat` | `t t > t` | Concatenate two strings | `cat "hello" " world"` -> `"hello world"` |
| `len` | `t > n` | String length | `len "hello"` -> `5` |
| `trm` | `t > t` | Trim whitespace | `trm " hi "` -> `"hi"` |
| `spl` | `t t > L t` | Split string by delimiter | `spl "a,b,c" ","` -> `["a","b","c"]` |
| `has` | `t t > b` | Check if string contains substring | `has "hello" "ell"` -> `true` |
| `idxof` | `t t > O n` | First code-point index of substring, or nil. Empty needle returns 0 (Python/JS convention). Pair with `slc` for split-on-first-occurrence. (ILO-39, 26.5) | `idxof "key=value" "="` -> `3` |
## Regex
| Function | Signature | Description | Example |
|----------|-----------|-------------|---------|
| `rgx` | `t t > L t` | Regex match (returns captures) | `rgx "abc123" "[0-9]+"` -> `["123"]` |
| `rgxall` | `t t > L (L t)` | All matches with captures, one inner list per match. With no capture groups, each inner list holds the whole match. | `rgxall "(\\w+)=(\\d+)" "a=1 b=2"` -> `[["a","1"],["b","2"]]` |
| `rgxall1` | `t t > L t` | All matches of a single-group pattern, flattened. With no capture groups, returns the list of whole matches. Errors at verify-time if the pattern has 2+ capture groups, use `rgxall` instead. | `rgxall1 "(\\d+)" "a=1 b=2"` -> `["1","2"]` |
| `rgxall-multi` | `L t t > L t` | Multi-pattern flat-match. Apply each pattern in the list to the string and concatenate hits in pattern order. Per-pattern semantics follow `rgxall1` (0 groups -> whole matches; 1 group -> capture-1 strings; 2+ groups errors). | `rgxall-multi ["\\d+" "[A-Z]+"] "x=1 ERR=2"` -> `["1","2","ERR"]` |
## Formatting
| Function | Signature | Description | Example |
|----------|-----------|-------------|---------|
| `fmt` | `t ... > t` | Format string with values | `fmt "{.2f} {:5}" 3.14159 42` |
| `fmt2` | `n n > t` | Format a number to N decimal places (half-to-even rounding; digits clamped to `0..=20`). Decimal formatter, **not** a list-splat variant of `fmt`. Compose with `fmt` for template + precision. | `fmt2 3.14159 2` -> `"3.14"`, `fmt "x={}" (fmt2 v 2)` |
`fmt` is pure-functional sprintf, **not print**. A bare `fmt "..." v` statement is silently discarded on every engine. Use `prnt fmt "..." v` to print or `line=fmt "..." v` to capture. The verifier emits **ILO-T032** when `fmt`/`fmt2` is a non-tail statement with no binding (tail position, e.g. `f v:n>t;fmt "x={}" v`, is the documented "return formatted text" idiom and does not warn).
**Supported placeholder specs:**
| Spec | Meaning | Example |
|------|---------|---------|
| `{}` | Any value | `fmt "{}" 42` → `"42"` |
| `{.Nf}` / `{:.Nf}` | N decimal places | `fmt "{.2f}" 3.14159` → `"3.14"` |
| `{:N}` | Right-align to width N | `fmt "{:5}" "hi"` → `" hi"` |
| `{:Nd}` | Integer, right-aligned to width N | `fmt "{:5d}" 42.0` → `" 42"` |
| `{: t` | Convert any value to string | `str 42` -> `"42"` |
| `num` | `t\|n > R n t` | Polymorphic: parse text or identity-wrap a number as Ok. Saves the `num (str x)` roundtrip when `x` may already be numeric (e.g. from `jpar!` on a JSON number) | `num "42"` -> `Ok 42`, `num 42` -> `Ok 42` |
## Output
| Function | Signature | Description | Example |
|----------|-----------|-------------|---------|
| `prnt` | `_ > _` | Print value and return it | `prnt "hello"` |
## Aliases
| Long form | Short form |
|-----------|------------|
| `concat` | `cat` |
| `length` | `len` |
| `trim` | `trm` |
| `split` | `spl` |
| `contains` | `has` |
| `format` | `fmt` |
| `regex` | `rgx` |
| `print` | `prnt` |
---
# Tools
URL: https://ilo-lang.ai/docs/builtins/tools/
Use this when integrating ilo with MCP servers, Agent Skills, or JSON-mode for agent-driven sessions.
Use this when integrating ilo with MCP servers, Agent Skills, or JSON-mode for agent-driven sessions.
## Agent Skills
ilo can be installed as a skill in **Claude Code** and **Claude Co-work**, giving any conversation the ability to write, run, and debug ilo programs on demand.
### Claude Code
```bash
ilo skill install
```
This creates a skill at `~/.claude/skills/ilo/SKILL.md` that Claude Code picks up automatically. Once installed, invoke it with `/ilo` or just mention ilo code in conversation.
The skill provides:
- Write ilo programs from natural-language descriptions
- Run inline expressions and `.ilo` files
- Debug errors using `ilo --explain`
- Convert code from other languages into ilo
- Load the full spec with `ilo help ai`
### Example
```
> /ilo fibonacci function that returns the nth number
ilo 'fib n:n>n;<=n 1 n;a=fib -n 1;b=fib -n 2;+a b' 10
# 55
```
### Manual installation
If you prefer to install the skill by hand, create `~/.claude/skills/ilo/SKILL.md`:
```yaml
---
name: ilo
description: "Write, run, debug, and explain programs in the ilo programming language."
argument-hint: "[task or code description]"
allowed-tools:
- Bash
- Read
- Write
- Edit
- Grep
- Glob
---
```
Then add the ilo quick reference below the frontmatter. Run `ilo help ai` to get the compact spec to paste in.
### Claude Co-work
Claude Co-work (multi-agent coding sessions) works the same way. Once the skill is installed, any agent in the session can use ilo.
### Other agents
Any agent that can run shell commands can use ilo. The key ingredients:
1. **Install ilo**, `curl -fsSL https://raw.githubusercontent.com/ilo-lang/ilo/main/install.sh | sh`
2. **Load the spec**, `ilo help ai` returns a compact reference the agent can use as context
3. **Use JSON mode**, pass `-j` for structured output the agent can parse
## MCP servers
ilo can connect to any [MCP server](https://modelcontextprotocol.io), with tools type-checked end-to-end before execution.
### Configuration
Create an `mcp.json` file:
```json
{
"mcpServers": {
"myserver": {
"command": "node",
"args": ["server.js"],
"env": {}
}
}
}
```
### Usage
```bash
ilo --mcp mcp.json program.ilo funcname args
```
MCP tools become callable functions in your ilo program, with types verified at load time.
See `examples/mcp.json` in the [ilo repo](https://github.com/ilo-lang/ilo/tree/main/examples) for a working example.
## REPL JSON mode
The `-j` flag switches ilo output to structured JSON, ideal for agents that need to parse results programmatically.
### Interactive REPL
```bash
ilo repl -j
```
Every response is a single JSON object on one line:
**Success:**
```json
{"ok": 10}
```
**Error:**
```json
{"code": "ILO-T004", "message": "undefined variable 'y'", "severity": "error", "suggestion": "did you mean 'x'?"}
```
### Inline expressions
The `-j` flag also works outside the REPL:
```bash
ilo 'dbl x:n>n;*x 2' -j 5
```
```json
{"ok": 10}
```
### Why this matters for agents
Agents cannot reliably parse human-formatted output. With `-j`:
- **No ambiguity**, success is `{"ok": value}`, errors have structured `code`/`message`/`suggestion` fields
- **Error recovery**, the agent reads the `suggestion` field and retries automatically
- **Stateful sessions**, the REPL keeps function definitions across lines, so the agent can build up a program incrementally and test each step
### Agent workflow example
A typical agent loop using `repl -j`:
```
1. Agent sends: dbl x:n>n;*x 2
ilo returns: {"ok": "defined dbl"}
2. Agent sends: dbl 5
ilo returns: {"ok": 10}
3. Agent sends: dbl "hello"
ilo returns: {"code": "ILO-T003", "message": "expected n, got t", ...}
4. Agent reads error, adjusts, retries
```
## Combining
Skills, MCP servers, and HTTP tools compose. An agent can write ilo programs that call external services, all within a single conversation. See [HTTP](/docs/builtins/http/) for the HTTP-specific tool config.
---
# Languages for AI agents
URL: https://ilo-lang.ai/docs/ecosystem/languages-for-agents/
Programming languages designed for AI agents, by camp, with activity as of August 2026.
Programming languages whose intended author is a model, not a person. Three camps, by what they optimise. Taxonomy follows [agentlanguages.dev](https://agentlanguages.dev), the independent catalogue of the field; this is a selection, not the full index.
"Active" means at least one commit on the default branch in the 90 days before 2026-08-02, via the GitHub API. This field moves in bursts, so a quiet month is not the same as abandoned - but a project at 0/0 has not been touched in a quarter.
## Syntactic
Cut the tokens a model spends writing and reading code: dense aliases, minimal punctuation, one canonical form.
| Language | Design move | Active |
|---|---|---|
| **[ilo](https://github.com/ilo-lang/ilo)** | Short fixed builtin aliases; design changes argued against measured token cost; agent-session failures filed as language bugs | active |
| [Mog](https://github.com/voltropy/mog) | Embedded-only, flat operators (no precedence), host-granted capabilities; full spec in ~2.7k tokens | quiet |
| [Codong](https://github.com/brettinhere/Codong) | One canonical form per operation; JSON errors carry fix and retry fields | quiet |
| [X07](https://github.com/x07lang/x07) | No text at all: programs are canonical JSON ASTs, edits are RFC 6902 patches | active |
| [Magpie](https://github.com/magpie-lang/magpie) | SSA as the surface syntax; every value typed at definition; LLVM-backed | quiet |
| [NERD](https://github.com/Nerd-Lang/nerd-lang-core) | Every operator replaced with an English keyword; MCP client primitives | quiet |
| [LLMLang](https://github.com/paulprogrammer/llmlang) | Prefix-arity AST, De Bruijn indices, compile-time linear ownership | active |
| [Lume](https://github.com/mavboas/lume) | Conventional surface plus token-budgeted docs retrieval (`lume kb pack --max-tokens N`) | active |
## Verification
Spend tokens to buy certainty: machine-checked contracts, effect types, solvers between generated code and production.
| Language | Design move | Active |
|---|---|---|
| [Zero](https://github.com/vercel-labs/zerolang) | Agent-first systems language, tiny binaries, JSON diagnostics with repair plans | active |
| [BAML](https://github.com/BoundaryML/baml) | Close the escape hatches: runtime types with no `any` or unchecked casts, typed errors with exhaustive matching, schema-aligned parsing that repairs malformed model output instead of rejecting it. Compiler work began Nov 2025; the older prompt DSL was rebranded BAML v0 in Jul 2026, so the two release tracks share a repo | active |
| [Hyperlambda](https://github.com/polterguy/magic) | Programs are node trees rather than text, so generated code arrives as an executable AST that is validated before it runs and only whitelisted invocations execute. The containment is original rather than retrofitted: `whitelist-invocation.hl` dates to Dec 2022, while the agent positioning and the MCP server are 2026 | active |
| [Vera](https://github.com/aallan/vera) | Mandatory contracts verified by Z3; LLM inference as a typed effect | active |
| [Thermite](https://github.com/dollspace-gay/Thermite) | req/ens/fx contracts discharged by Verus, Lean and Z3; five-level assurance ladder | active |
| [AILANG](https://github.com/sunholo-data/ailang) | Row-polymorphic effect types, no loops; effects carve IO/FS/Net/Clock/AI | active |
| [NanoLang](https://github.com/jordanhubbard/nanolang) | Shadow test blocks required per function; core proved with Coq theorems | active |
| [Vow](https://github.com/vow-lang/vow) | Machine-checked vows discharged by ESBMC bounded model checking | active |
| [Aver](https://github.com/jasisz/aver) | Functions carry intent, effects and a verify block exporting to Lean 4 / Dafny | active |
| [Intent](https://github.com/lhaig/intent) | Mandatory pre/postconditions checked by Z3; one source targets Rust, JS, Wasm | active |
## Orchestration
The language coordinates agents rather than replacing their code: workflows, capability grants, deterministic replay.
| Language | Design move | Active |
|---|---|---|
| [Fabro](https://github.com/fabro-sh/fabro) | Workflow harness; `.fabro` files are Graphviz digraphs, a CSS-like sheet routes models | active |
| [Boruna](https://github.com/escapeboy/boruna) | Capability-safe deterministic workflows with hash-chained evidence bundles | active |
| [Plasm](https://github.com/PlasmTools/plasm-core) | Path expressions over typed API graphs with dry-run plans | active |
| [Lumen](https://github.com/alliecatowo/lumen) | Markdown-native sources with algebraic effects and compile-time determinism | quiet |
| [Marsha](https://github.com/alantech/marsha) | English functional files compiled to tested Python by an LLM (2023; the category's ancestor) | quiet |
Corrections welcome at [hello@ilo-lang.ai](mailto:hello@ilo-lang.ai). See also [ways to be token efficient](/docs/ecosystem/token-efficiency/).
---
# Ways to be token efficient
URL: https://ilo-lang.ai/docs/ecosystem/token-efficiency/
Token-efficiency techniques across the agent stack, by category, with projects and links.
Cost = generation + retries + context loading. Every layer of the agent stack has a lever, and they compound. A language is one of them.
## Find code without reading all of it
An agent with no index explores by reading, and reading is what costs - a single exploratory task on a large repo can burn 200k tokens before any real work starts. Five strategies, most tools combining several.
**Search the repo directly.** No index; the agent runs `grep` and `rg` and reasons over the results. Nothing to precompute or go stale, and it [reaches over 90% of RAG-level accuracy](https://www.mindstudio.ai/blog/is-rag-dead-what-ai-agents-use-instead) on code retrieval benchmarks.
- [Claude Code](https://www.claude.com/product/claude-code), [Codex CLI](https://github.com/openai/codex) and [Cline](https://github.com/cline/cline) ship with no index. Semantic search is an open request on Codex ([#5181](https://github.com/openai/codex/issues/5181), [#609](https://github.com/openai/codex/issues/609)), and third-party indexers exist for all three
**Embed and retrieve.** Chunk, embed, retrieve by similarity. Catches intent that keyword search misses; the index needs rebuilding as code changes.
- [Cursor](https://cursor.com) - chunks with [Tree-sitter](https://tree-sitter.github.io/tree-sitter/), tracks changes with a [Merkle tree](https://en.wikipedia.org/wiki/Merkle_tree), stores in [Turbopuffer](https://turbopuffer.com)
- [Kilo Code](https://kilo.ai/docs/features/codebase-indexing) and [Continue](https://docs.continue.dev/customize/deep-dives/codebase) - the same pattern, self-hostable
**Index the symbols.** Parse the AST so the agent fetches one function, not the file around it.
- [jCodeMunch](https://github.com/jgravelle/jcodemunch-mcp) - Tree-sitter symbol index served over [MCP](https://modelcontextprotocol.io)
- [Aider](https://aider.chat/docs/repomap.html) - builds a repo map ranked by importance
- [Serena](https://github.com/oraios/serena) - drives a [language server](https://microsoft.github.io/language-server-protocol/) so lookups match what an IDE knows
- [SymDex](https://github.com/husnainpk/SymDex)
**Build a code graph.** Files, symbols, imports and call chains as a traversable graph. Answers what the others cannot: what calls this, what breaks if it changes.
- [GrapeRoot](https://graperoot.dev/) - pre-loads the relevant slice into each prompt; claims 30-45% cheaper sessions
- [Prowl](https://github.com/neur0map/prowl) and [CodeGraphContext](https://github.com/CodeGraphContext/CodeGraphContext)
**Let the model write code to search.** The model writes a program that greps and filters; only the result returns to context. Recursive language models push this furthest.
- [rlm](https://github.com/alexzhang13/rlm) - the reference implementation; [DSPy ships it as a module](https://dspy.ai/api/modules/RLM/)
Full comparison, including which to reach for when: [how AI agents find code in your repo](https://danieljohnmorris.com/writing/codebase-indexing-strategies-may-2026).
## Compress what reaches the model
A proxy rewrites bulky tool output into a summary before it lands in context, keeping the full text retrievable if it turns out to matter.
- [Headroom](https://extraheadroom.com) - compresses tool results and file dumps on the way in, storing the original against a hash the agent can retrieve later. It is also how [Ponytail](#write-less-back) is distributed, so the two cover both directions
- [Caveman Engine](https://caveman.so/products/caveman-engine) - the same shape from the makers of the caveman skill, covering the input side the skill explicitly does not touch. Targets logs, JSON, tables, code and diffs, keeps the original bytes locally so a lossy replacement can be recovered, and fails closed by passing content through unchanged when it cannot parse it or cannot make it smaller. Go, closed source, in private development at the time of writing, and its own page labels reductions "inferred, never verified savings"
## Cache the repeated part of the prompt
Every request an agent sends starts with the same block of text: the system prompt, the tool definitions, the language spec. That repeated opening is the *prefix*, and caching lets you stop paying full price to send it again.
The first request that writes the cache costs more than normal - 1.25x the usual input price for a 5-minute cache, 2x for a one-hour cache. Every later request that opens with the exact same text pays about 0.1x instead. Two requests is enough to come out ahead on the short cache, three on the long one.
The catch is in the word *prefix*: the match runs from the very first character forward, and stops at the first byte that differs. A timestamp near the top of a system prompt means nothing after it can be cached, however stable the rest is. This is why a fixed skill file that is byte-identical on every task caches well, while a context pack assembled per task does not - same token count, very different bill.
This one saves money rather than tokens. The context window is just as full either way.
Every major provider offers it, with different mechanics. The split that matters is whether you place the cache breakpoints yourself or the provider infers them, and whether you are billed per cached token or for storage over time.
- [Anthropic](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) - explicit `cache_control` breakpoints, up to four per request; reads bill at roughly 0.1x
- [OpenAI](https://platform.openai.com/docs/guides/prompt-caching) - was automatic with no configuration and no write cost; since GPT-5.6 went GA in July 2026 it uses explicit breakpoints and bills writes, which puts it close to Anthropic's model
- [Google Gemini](https://ai.google.dev/gemini-api/docs/caching) - offers both implicit and explicit context caching, and bills cached content for storage duration rather than per token, so the economics turn on how long you hold the cache rather than how often you read it
- [DeepSeek](https://api-docs.deepseek.com/guides/kv_cache) - automatic, disk-backed, and the steepest relative discount of the four
Rates move often and vary by model. Treat any number you read as needing a check against the provider's own pricing page before you budget against it.
## Write less back
Output tokens are the half you control by asking. An instruction in context cuts what the model writes, with no toolchain change.
- [caveman](https://github.com/JuliusBrussee/caveman) - strips articles, filler and pleasantries from responses while keeping the technical content intact. Its [own figures](https://github.com/JuliusBrussee/caveman/blob/main/docs/HONEST-NUMBERS.md) split by workload: 65% fewer output tokens on prose (10 prompts, range 22-87%), 0% on input, and the skill itself adds roughly 1-1.5k input tokens per turn. JetBrains independently measured 8.5% across 86 SkillsBench agentic coding tasks. On terse replies or per-request billing it goes net negative, and the docs tell you to turn it off
- [Ponytail](https://extraheadroom.com/ponytail-claude-code) - aims at generated code rather than prose, pushing the model down a ladder (no code? one line? a config change?) before it writes anything; lite, full and ultra intensity levels. Ships as an add-on to [Headroom](#compress-what-reaches-the-model), which handles the input side
## Code instead of tool calls
One generated program replaces a chain of round trips, each of which carries full context. Saves both directions.
- [Programmatic tool calling](https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling) - Claude calls tools from inside code execution; intermediate results skip the context window
- [Cloudflare Code Mode](https://blog.cloudflare.com/code-mode/) - agents write code against MCP tools instead of chaining calls
- [rlm](https://github.com/alexzhang13/rlm) - the same move aimed at context rather than tools: the model writes code that processes data too large to read, calling itself over the chunks. Also listed under [finding code](#find-code-without-reading-all-of-it)
## Design the language
Change what generated code costs in the first place. The moves in use across the [agent-language field](/docs/ecosystem/languages-for-agents/):
- **Dense syntax** - short fixed aliases, minimal punctuation: [ilo](https://github.com/ilo-lang/ilo) (`mapr`, `fld`, `rdjl`), [Mog](https://github.com/voltropy/mog) flat operators
- **Small loadable spec** - bound the artifact an agent must read before it can write the language: [Zero](https://github.com/vercel-labs/zerolang) and [Mog](https://github.com/voltropy/mog) both ship one under 3k tokens
- **Structured diagnostics** - stable error codes + fix hints kill retry loops: [Zero](https://github.com/vercel-labs/zerolang) repair plans, ilo `ILO-P/T/R` codes, [Codong](https://github.com/brettinhere/Codong) fix/retry JSON
- **Repair instead of reject** - accept output that near-misses the schema rather than erroring and asking again: [BAML](https://github.com/BoundaryML/baml) schema-aligned parsing fixes unquoted strings, missing brackets, commas and colons, misnamed keys, and strips surrounding prose. Boundary's [own BFCL figures](https://boundaryml.com/blog/schema-aligned-parsing) put SAP at 92% against 87.5% for gpt-3.5-turbo function calling (n=1000, vendor-run, and SAP is scored against a compressed schema rather than JSON schema, so the columns are not strictly comparable). Note what that measures: first-pass accuracy, not a token ledger. The retries avoided are counted, the verbose surface paid on every generation is not
- **One canonical form** - one way to write each operation, no plausible-but-wrong variants: Codong, [Lume](https://github.com/mavboas/lume)
- **Budgeted doc retrieval** - Lume's `lume kb pack "" --max-tokens N` packs docs under a cap
- **Constrained decoding** - grammar masks the token distribution so invalid syntax cannot be sampled: [Axis](https://github.com/vmelnic/axis) LL(1) masks, MoonBit (ICSE 2024)
Full field: [agentlanguages.dev](https://agentlanguages.dev) and [languages for AI agents](/docs/ecosystem/languages-for-agents/). Vendor percentage claims are reported as claimed, not independently verified.
---
# Real-World Examples
URL: https://ilo-lang.ai/docs/learn/examples/
Practical ilo programs for common tasks
Practical patterns you can copy and adapt. Each example is a complete, runnable program.
## Data pipeline
Fetch JSON from an API, filter positive values, sum them:
```ilo
fetch url:t>R _ t;r=get! url;rdb! r "json"
pos x:_>b;>x 0
proc rows:L _>n;clean=flt pos rows;sum clean
```
Three functions, no boilerplate. `$!` auto-unwraps the HTTP response. `rdb!` parses JSON. `flt` keeps only elements where `pos` returns true.
## API status checker
Hit a URL and report whether it responded:
```ilo
check url:t>t
r=get url
?r{~v:fmt "{}: ok" url;^e:fmt "{}: {}" url e}
```
```bash
ilo 'check url:t>t;r=get url;?r{~v:fmt "{}: ok" url;^e:fmt "{}: {}" url e}' "http://httpbin.org/get"
# -> http://httpbin.org/get: ok
```
`get url` makes an HTTP GET (since 0.12.0 the `$` sigil is process exec, not HTTP). The result is matched: `~v` (Ok) or `^e` (Err).
## CSV processing
```bash
# Column count of the first row
ilo 'f p:t>R n t;d=rd! p;~len d.0' f data.csv
```
`rd!` reads and parses the CSV file (returns `R`, so the function must too). `d.0` indexes the first row.
## Text processing
Count unique words in a file:
```bash
ilo 'f p:t>R n t;t=rd! p "raw";ws=spl t " ";~len (unq ws)' f document.txt
```
Extract all emails from text:
```bash
ilo 'f s:t>L t;rgx "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}" s' \
"contact alice@example.com or bob@test.org"
# -> [alice@example.com, bob@test.org]
```
Sort words by length:
```ilo
wlen s:t>n;len s
by-len ws:L t>L t;srt wlen ws
```
```bash
ilo examples/sort-by-key.ilo by-len '["banana","fig","apple","kiwi"]'
# -> ["fig", "kiwi", "apple", "banana"]
```
## Environment config
Read an env var with a fallback:
```ilo
cfg>t
k=env "API_KEY"
?k{~v:v;^e:"missing API_KEY"}
```
`env` returns a `Result`. The match provides the value or a default.
## Tool interaction
Declare external tools, then compose them like regular functions:
```ilo
tool get-user"Retrieve user by ID" uid:t>R profile t timeout:5,retry:2
tool send-email"Send notification email" to:t subject:t body:t>R _ t timeout:10,retry:1
type profile{id:t;name:t;email:t;verified:b}
notify uid:t msg:t>R _ t
u=get-user! uid
!u.verified{^"user not verified"}
send-email! u.email "Notification" msg
~nil
```
Tools are type-checked at compile time. `get-user!` unwraps the result, propagating errors automatically.
## Python vs ilo
A tax-inclusive total calculation in both languages.
**Python** (93 chars, ~30 tokens):
```python
def total(price: float, quantity: int, rate: float) -> float:
sub = price * quantity
tax = sub * rate
return sub + tax
```
**ilo** (38 chars, ~10 tokens):
```ilo
tot p:n q:n r:n>n;s=*p q;t=*s r;+s t
```
Same semantics. 0.33x the tokens, 0.22x the characters. For an AI agent paying per token, this adds up fast across thousands of tool calls.
## Sum types
Classify HTTP status codes using a discriminated union:
```ilo
type status = ok(n) | redirect(n) | err(t)
describe s:status>t;?s{ok(code):fmt "OK {}" code; redirect(code):fmt "redirect {}" code; err(msg):fmt "error: {}" msg}
```
## Parallel processing
Double 10 000 numbers in parallel:
```bash
ilo 'double x:n>n;*x 2 main>L n;par-map double (range 0 10000)' main
```
## defer for cleanup
Read a file and guarantee close on exit:
```ilo
read-lines path:t>R L t t
f = open! path
defer close f
~rdl! f
```
## Error chaining with `!`
The `!` unwrap operator keeps fallible pipelines flat - each step propagates its error automatically, no nesting:
```ilo
row-total r:L t>R n t;~num! r.0
process path:t>R n t
rows=rd! path
totals=mapr row-total rows
~sum totals
```
## Pagination (tail-recursive accumulator)
Canonical pattern for iterating paged APIs: tail-recursive accumulator + empty-token termination + Result chain (`!` propagation). No stack growth regardless of page count.
```ilo
-- fetch-page: one HTTP call; returns ~[items next-token] or Err.
fetch-page url:t token:t>R (L _) t
b=get! (fmt "{}&cursor={}" url token)
items=jpar-list! b
nxt=default-on-err (jpth b "next_cursor") ""
~[items nxt]
-- fetch-all: tail-recursive; empty token signals end-of-pages.
fetch-all url:t token:t acc:L n>R (L n) t
=token "" ~acc
r=fetch-page! url token
page=at r 0
nxt=at r 1
fetch-all url nxt +acc page
-- Entry: seed with empty token and empty accumulator.
all-items>R (L n) t
fetch-all "https://api.example.com/items" "" []
```
The `fld` builtin threads a numeric accumulator if you want a running total instead of collecting all items:
```ilo
sum-all>R n t
xs=fetch-all! "https://api.example.com/items" "" []
~fld (a:n x:n>n;+a x) xs 0
```
## What's next
- Run `ilo -ai` to get the compact spec for your AI agent
- Read the full [language specification](https://github.com/ilo-lang/ilo/blob/main/SPEC.md)
- Browse [example programs](https://github.com/ilo-lang/ilo/tree/main/examples)
- Try the REPL: `ilo repl`
---
# Getting Started
URL: https://ilo-lang.ai/docs/learn/getting-started/
Use this when you want a single page that takes you from install to your first multi-function ilo program.
This page walks you from a fresh machine to a working multi-function ilo program. For install options on every platform, see [Install](/docs/learn/install/).
## Install
The fastest path on macOS or Linux:
```bash
curl -fsSL https://raw.githubusercontent.com/ilo-lang/ilo/main/install.sh | sh
```
Verify it works:
```bash
ilo 'dbl x:n>n;*x 2' 5
# 10
```
If you use Claude Code, run `/plugin marketplace add ilo-lang/ilo` then `/plugin install ilo-lang/ilo` and your agent picks up ilo automatically. See [Install](/docs/learn/install/) for npm, Windows, source builds, and other agents.
## Your first program
Create `hello.ilo`:
```ilo
greet name:t > t
+"hello " name
```
Run it:
```bash
ilo hello.ilo greet "world"
# hello world
```
That's a complete ilo program. Here is what each part means:
| Part | Meaning |
|------|---------|
| `greet` | function name |
| `name:t` | parameter `name`, type text |
| `> t` | returns text |
| `+"hello " name` | body: concatenate two strings (prefix `+`) |
Now try numbers. Create `math.ilo`:
```ilo
add a:n b:n > n -- n = number, t = text
+ a b -- prefix: + a b means a + b
```
```bash
ilo math.ilo add 3 4
# 7
```
## Inline mode
You can also run ilo without a file. Semicolons replace newlines and indentation:
```bash
ilo 'add a:n b:n>n;+a b' 3 4
# 7
```
This is the form AI agents use, same language, just compressed to one line. See the [CLI Reference](/docs/reference/cli/) for more.
## Multiple functions
ilo programs can contain multiple functions. Each function is a named declaration separated by a newline (in files) or a space (inline):
Save this as `math.ilo`:
```ilo
dbl x:n>n;*x 2
inc x:n>n;+x 1
sq x:n>n;*x x
```
### Selecting a function from the CLI
When a program has multiple functions, name the one you want to run after the filename:
```bash
ilo math.ilo dbl 5
# 10
ilo math.ilo sq 5
# 25
```
If you omit the function name, ilo runs the first one:
```bash
ilo math.ilo 5
# 10 (runs dbl, the first function)
```
Inline programs work the same way:
```bash
ilo 'dbl x:n>n;*x 2 sq x:n>n;*x x' dbl 5
# 10
ilo 'dbl x:n>n;*x 2 sq x:n>n;*x x' sq 5
# 25
```
### Functions can call each other
Functions in the same program can reference each other:
```ilo
dbl x:n > n -- double a number
*x 2 -- multiply x by 2
quad x:n > n -- quadruple a number
dbl(dbl x) -- double x, then double again
```
```bash
ilo 'dbl x:n>n;*x 2 quad x:n>n;dbl(dbl x)' quad 3
# 12
```
For splitting functions across files, see [Imports](/docs/reference/imports/).
## Where to next
- [Manifesto](/docs/learn/manifesto/) for the why
- [Real-World Examples](/docs/learn/examples/) for end-to-end programs
- [Reference](/docs/reference/language/) for the full language spec
- [Builtins](/docs/builtins/numbers/) for the stdlib
---
# Install
URL: https://ilo-lang.ai/docs/learn/install/
Use this when you need to install ilo on a specific platform (Claude Code, npm, macOS, Linux, Windows, source, or other agents).
Use this when you need to install ilo on a specific platform. Pick the section that matches who's running it.
## For agents
### Claude Code (recommended)
```bash
/plugin marketplace add ilo-lang/ilo # add marketplace (once)
/plugin install ilo-lang/ilo # install plugin + teach agent ilo
```
The agent learns ilo automatically via the bundled Agent Skill.
### Claude Cowork
Browse Plugins → Add marketplace from GitHub → `ilo-lang/ilo` → install.
Binary auto-installs via npm. Note: Cowork uses the WASM build - HTTP and process builtins (`get`, `pst`, `run`, `$`) are not yet supported.
### Agent skills (npx skills)
```bash
npx skills add ilo-lang/ilo
```
Install via the [skills](https://www.npmjs.com/package/skills) npm package (for Claude Code and AI agent workflows). Installs the ilo Agent Skill into any compatible agent environment.
### Other agents (Codex, Opencode, etc.)
Copy `skills/ilo/` into your agent's skills directory. Any tool supporting the [Agent Skills standard](https://agentskills.io) will pick it up.
## For humans
### macOS / Linux
```bash
curl -fsSL https://raw.githubusercontent.com/ilo-lang/ilo/main/install.sh | sh
```
### Windows (PowerShell)
```powershell
Invoke-WebRequest -Uri https://github.com/ilo-lang/ilo/releases/latest/download/ilo-x86_64-pc-windows-msvc.exe -OutFile ilo.exe
```
### npm / npx
```bash
# Run without installing
npx ilo-lang 'dbl x:n>n;*x 2' 5
# Install globally
npm i -g ilo-lang
```
Requires Node 20+. Uses WASM - interpreter mode only, no HTTP builtins.
### From source
```bash
# From crates.io
cargo install ilo
# From git
cargo install --git https://github.com/ilo-lang/ilo
```
## Verify installation
```bash
ilo 'dbl x:n>n;*x 2' 5
# → 10
```
---
# Introduction
URL: https://ilo-lang.ai/docs/learn/introduction/
What is ilo and why does it exist?
**ilo** is a token-optimised programming language for AI agents. Named from the [Toki Pona](https://en.wikipedia.org/wiki/Toki_Pona) word for ["tool"](https://sona.pona.la/wiki/ilo), it's designed to minimise the total token cost when AI agents write and run programs.
## The problem
AI agents pay three costs per program:
1. **Generation tokens** - producing the code
2. **Error feedback** - reading error messages when something goes wrong
3. **Retries** - regenerating after failures
Traditional languages weren't designed for this.
## The solution
Here's the same program in Python and ilo - a function that computes subtotal plus tax:
```python
def tot(p: float, q: float, r: float) -> float:
s = p * q
t = s * r
return s + t
```
```ilo
tot p:n q:n r:n>n;s=*p q;t=*s r;+s t
```
The ilo version is **0.33× the tokens** and **0.22× the characters**. Both are typed, both have named variables, both do the same thing.
The savings come from three things:
- **Prefix notation** eliminates parentheses: `+*a b c` instead of `(a * b) + c`
- **Short names** - single-char variables (`x`, `n`, `s`), short function names (`dbl`, `fac`), and builtins with compact aliases (`len`, `hd`, `flt`)
- **Type verification** catches errors before execution with compact codes (`ILO-T004`) - not a full stack trace
- **Constrained vocabulary** means fewer valid next-tokens, fewer wrong choices, fewer retries
## Run a program
```bash
# Run inline
ilo 'tot p:n q:n r:n>n;s=*p q;t=*s r;+s t' 10 20 30
# Run from file
ilo program.ilo tot 10 20 30
```
For the current release and what's new, see the [release notes on GitHub](https://github.com/ilo-lang/ilo/releases).
---
# Learning Path
URL: https://ilo-lang.ai/docs/learn/learn/
Use this when you want a guided progression from zero to fluent ilo, in order.
Use this when you want a guided progression from zero to fluent ilo.
ilo is small. You can learn it in an afternoon. Here is the order I would read the docs in.
## 1. Why ilo exists
Start with the [Manifesto](/docs/learn/manifesto/). It explains the one metric ilo optimises for (total tokens from intent to working code) and why every design choice follows from that.
If you want a faster pitch first, the [Introduction](/docs/learn/introduction/) shows the same program in Python and ilo side by side.
## 2. Get it running
[Getting Started](/docs/learn/getting-started/) takes you from install to a multi-function program in one page. If you need platform-specific install details, [Install](/docs/learn/install/) has every option.
## 3. Read real programs
[Real-World Examples](/docs/learn/examples/) shows complete, working programs: HTTP + JSON, data pipelines, file processing. Skim these to build a feel for what idiomatic ilo looks like.
## 4. Learn the language
Once you can run a program, work through the [Reference](/docs/reference/language/) section in this order:
1. [Primitives](/docs/reference/primitives/) - the type sigils
2. [Types & Functions](/docs/reference/types-and-functions/) - function syntax
3. [Prefix Notation](/docs/reference/prefix-notation/) - operators
4. [Guards & Control Flow](/docs/reference/guards/) - replaces if/else
5. [Loops](/docs/reference/loops/) - foreach, ranges, while
6. [Error Handling](/docs/reference/error-handling/) - Result, `!`, `?`
7. [Pipes](/docs/reference/pipes/) - chain transforms
8. [Memory Model](/docs/reference/memory-model/) - how data is owned
## 5. Reach for builtins
The [Builtins](/docs/builtins/numbers/) section is the stdlib reference. Each page covers one concern:
- [Numbers](/docs/builtins/numbers/), [Text](/docs/builtins/text/), [Collections](/docs/builtins/collections/)
- [Data & I/O](/docs/builtins/data-io/), [HTTP](/docs/builtins/http/), [Tools](/docs/builtins/tools/)
## 6. When you get stuck
- [Gotchas](/docs/reference/gotchas/) for the common traps
- [Diagnostics](/docs/reference/diagnostics/) for error codes
- `ilo --explain ILO-XNNN` from the CLI for any error
---
# Manifesto
URL: https://ilo-lang.ai/docs/learn/manifesto/
Every programming language was designed for people. ilo is designed for AI agents.
## The Audience Is Not Human
Every programming language in use today was designed for people. The syntax, the error messages, the tooling - all optimised for a brain that reads left-to-right, tracks visual indentation, and cares about aesthetics.
AI agents are not that brain. They produce tokens sequentially. They consume tokens from a finite context window. Every token they spend - generating, reading, retrying - costs real time and real money.
ilo is designed for them.
## The Only Metric
**Total tokens from intent to working code.**
```
Total cost = spec loading
+ generation
+ context loading
+ error feedback
+ retries
```
Every design decision is evaluated against this number. If a feature reduces it, it's in. If it increases it, it's out. No exceptions for elegance, readability, or convention.
Until agents are trained on ilo, spec clarity is itself a token cost - a confusing spec means more retries. Some decisions that look like "readability" concessions are actually optimising the spec-loading term.
## The Six Principles
### 1. Token-Conservative
The north star. Every choice evaluated against total token cost across the full loop - not just "short syntax," but including retries, error feedback, and context loading.
A named argument like `amount: 42` costs more tokens than positional `42`. Across 10 syntax variants and 4 task types, positional args scored 10/10 generation accuracy. The swap concern was unfounded. Positional args are the single biggest token saver.
:::note[What the agent cares about]
"How many tokens will this cost me end-to-end?"
The language is as terse as possible *without increasing retry rate*. Where there's a tradeoff between generation cost and error rate, we optimise for total cost.
:::
**Prefix notation** eliminates parentheses and saves tokens at every nesting level. `(a * b) + c` becomes `+*a b c` - 4 fewer characters, 1 fewer token. Deeper nesting saves more: `((a + b) * c) >= 100` becomes `>=*+a b c 100` - 7 fewer characters, 3 fewer tokens. Across 25 expression patterns, prefix notation saves 22% of tokens and 42% of characters vs infix.
**Guards instead of if/else** eliminate nesting depth. In a traditional language, conditional logic stacks:
```python
if a:
if b:
if c:
return x
```
Each level adds indentation, a closing brace, and more state for the agent to track. In ilo, guards are flat statements that return early and chain vertically:
```ilo
>=a 0 x -- if a >= 0, return x; otherwise continue
>=b 0 y -- if b >= 0, return y; otherwise continue
z -- default return
```
No nesting. No closing braces to match. Each guard is a single statement the agent can emit and forget. Depth stays constant regardless of how many conditions there are.
**Match instead of switch** eliminates fall-through - a common source of bugs in C-style languages where missing a `break` causes execution to bleed into the next case. In ilo, each match arm is independent and exhaustive. There is no fall-through because there is no execution path between arms.
**Naming rule:** prefer single-word identifiers. Across all major LLM tokenisers (OpenAI, Anthropic), common English words are 1 token. Hyphenated compounds are always 2 - the hyphen forces a token split. Every hyphen in a name doubles its token cost. Abbreviations (`uid` vs `user`) save characters but not tokens - tokenisers encode common words as single tokens either way.
### 2. Constrained
Small vocabulary. Closed world. Few ways to do things.
When an agent generates the next token, how many valid options are there? Fewer valid next-tokens means fewer wrong choices means fewer retries. This isn't about limiting expressiveness - it's about making the right token obvious.
- **Closed world.** Every callable function is known ahead of time. The agent cannot hallucinate an API that doesn't exist.
- **Small vocabulary.** Fewer keywords, fewer constructs, one way to define a function, one way to call it, one way to handle errors.
- **Verification before execution.** All calls resolve, all types align, all dependencies exist - checked before running anything.
:::note[What the agent cares about]
"At each generation step, how many valid tokens are there?"
The language becomes a set of rails. Constrained generation can feed valid next-token sets back to the agent, making it *impossible* to generate invalid code.
:::
### 3. Self-Contained
Each unit carries its own context: deps, types, rules.
An agent working on function A shouldn't need to load functions B through Z to understand what A does. The less context required per step, the fewer tokens consumed, the more of the context window is available for the actual task.
- **Explicit dependencies.** Each function declares exactly what it needs - by name, with types. No globals, no ambient state, no implicit imports.
- **Small units.** A function that fits in a few dozen tokens can be loaded, understood, and modified cheaply.
- **Spec as context.** Until foundation models are trained on ilo, agents need the spec somewhere they can access it - bundled with the program, fetched on demand, or installed locally.
:::note[What the agent cares about]
"How much context do I need to load to work on this unit?"
Minimal context loading per task. Each unit is self-describing. The agent never needs to hunt for definitions elsewhere.
:::
### 4. Language-Agnostic
Minimise dependency on English or any natural language.
Early variants used short English-derived keywords (`fn`, `let`, `match`, `for`, `if`). Experiments showed structural tokens outperform keywords entirely - the winning syntax replaced control-flow keywords with single-character sigils:
- `?` conditional/match, `!` auto-unwrap, `~` ok-wrap, `^` err/throw, `@` iterate, `>` pipe/return
- Builtins are short abbreviated names: `len`, `hd`, `tl`, `map`, `flt`, `fld`, `srt`, `cat`, `spl`. A closed, memorisable set - not open-ended English.
- Agents learned the full vocabulary from spec + examples with 10/10 accuracy.
:::note[What the agent cares about]
"Can I learn this language from its spec and examples, regardless of my training?"
The spec is small enough to bundle with any program. Keywords are learned from structure, not from natural language understanding.
:::
### 5. Graph-Reducible
When analysing code, reduce context size by loading only the relevant subgraph.
Writing code costs the same tokens regardless of program size. But *reading* code - understanding what exists, what calls what, what breaks if you change something - scales with program size. ilo makes the dependency graph explicit and queryable, so the agent loads only what it needs.
- **Typed signatures as contracts.** Every function declares its params and return type. To understand what `create-user` does, an agent only needs its signature - not the 20 other functions in the file.
- **Explicit dependencies.** `use "auth.ilo" [vld-email vld-plan]` declares exactly what's imported. No scanning, no guessing.
- **Queryable structure.** `ilo graph` extracts call graphs, type dependencies, reverse callers, and transitive subgraphs as JSON. An agent modifying one function in a 30-function program loads 6-10% of the code instead of 100%.
:::note[What the agent cares about]
"How much do I need to read before I can write?"
The agent loads the target function's source, its dependencies' signatures, and the types it references - nothing more. As programs grow, the savings compound.
:::
### 6. Structured Compiler-to-Agent Surface
Every path from the compiler to an agent is machine-readable by default.
Diagnostics, AST output, call graphs, fix plans, skill content, size reports - all of it ships as structured JSON. Not as an optional flag you might forget to pass, but as the default contract. Prose output exists for human TTYs; JSON is the agent path.
- **Every CLI subcommand has a `--json` mode.** An agent driving `ilo check`, `ilo graph`, `ilo bench`, or any other subcommand gets a typed, parseable response with no screen-scraping.
- **Every emitted artifact carries a stable `schemaVersion` field.** Schemas evolve; the version field lets agents detect and adapt to changes rather than silently misparse them.
- **Every diagnostic carries machine-readable fields:** error code, source span, candidate fixes, and related locations. An agent reading a diagnostic knows exactly what went wrong, where it went wrong, and what to try next - without parsing human prose.
- **Fix plans are typed:** each suggested repair carries a `FixSafety` classification so the agent can decide autonomously whether to apply it.
:::note[What the agent cares about]
"Can I parse the compiler's response without writing a regex?"
Zero screen-scraping, zero retry cycles caused by format ambiguity. The agent reads typed JSON, acts on it, and moves on.
:::
## Principles We Considered and Dropped
**Deterministic** - if the language is constrained (closed world, no ambient state) and self-contained (explicit deps, pure functions), determinism falls out for free. It doesn't need its own principle because it's a consequence of principles 2 and 3, not a design lever you pull independently.
**Append-only** - the idea was that agents should only add code, never edit it. But if units are small enough to fit in a few dozen tokens, regenerating one is cheaper than maintaining append-only structural constraints. Small self-contained units (principle 3) make this unnecessary.
**Immediate feedback** - fast error cycles are critical for agents, but that's a property of the runtime and tooling (`ilo verify`, typed errors, zero-startup execution), not the language design itself. It matters for the ecosystem but doesn't constrain language decisions, so it doesn't belong in the principles.
## The Name
*ilo* is the Toki Pona word for "tool."
Toki Pona is a constructed language built around radical minimalism. ~120 words. 14 phonemes. Complex ideas expressed by combining simple terms. It constrains human expression to force clarity of thought.
ilo does the same for machine programmers. A minimal, verified vocabulary. Complex programs built by composing small, self-contained units. The constraint is the feature.
## What ilo Is Not
**Not a framework for building AI agents.** There are plenty of those. ilo is a language for agents to write programs *in*.
**Not optimised for human readability.** Humans can read it - it's not obfuscated - but no decision is made because it "looks cleaner" or "reads more naturally." If a design is uglier but costs fewer total tokens, it wins.
**Not theoretical.** Every principle here addresses measured failure modes in AI-generated code: hallucinated APIs, context window exhaustion, wasted retry cycles from vague errors.
## What ilo Is
A **minimal, verified action space** - the smallest set of constructs an agent needs to express computational intent, with relationships made explicit and everything else stripped away.
---
# Benchmarks
URL: https://ilo-lang.ai/docs/reference/benchmarks/
Use this when comparing ilo runtime performance against other languages or across engines.
Use this when comparing ilo runtime performance against other languages or across engines.
Micro-benchmarks comparing ilo's execution engines against compiled, JIT, and interpreted languages. Each benchmark runs **10000 iterations** and reports the median per-call time in nanoseconds.
:::note
These are micro-benchmarks. They measure raw execution speed, not end-to-end agent workflow performance. ilo's primary optimisation target is **total token cost** (generation + errors + retries), not runtime speed.
:::
## Execution engines
ilo has three public execution backends (the tree-walking interpreter is internal-only as of 0.12.x):
| Backend | Flag | Notes |
|---------|------|-------|
| **ilo AOT** | `ilo compile` | Cranelift ahead-of-time compiler → standalone native binary |
| **ilo VM** | `ilo` *(default)* / `--vm` | Register-based bytecode virtual machine, all opcodes |
| **ilo JIT** | `ilo --jit` | Cranelift-based just-in-time compiler, opt-in for hot loops |
## Languages tested
| Category | Languages |
|----------|-----------|
| **Compiled (AOT)** | Rust (`rustc -O`), Go, C# (.NET), Kotlin (JVM) |
| **JIT** | LuaJIT, Node.js (V8), TypeScript (tsx/V8), PyPy 3 |
| **ilo** | ilo JIT, ilo VM |
| **Interpreted** | Lua, Ruby, PHP, Python 3 (CPython) |
## Results
### numeric
*sum 1..1000 (pure arithmetic loop)*
| Language | ns/call | vs fastest |
|----------|--------:|------------|
| Rust (native) | 329ns | **fastest** |
| LuaJIT | 442ns | 1.3x |
| Node/V8 | 451ns | 1.4x |
| TypeScript | 456ns | 1.4x |
| Kotlin (JVM) | 528ns | 1.6x |
| Go | 665ns | 2.0x |
| PyPy 3 | 1.5us | 4.5x |
| Lua | 4.2us | 12.7x |
| ilo JIT | 4.2us | 12.9x |
| ilo AOT | 5.3us | 16.0x |
| C# (.NET) | 5.9us | 18.0x |
| PHP | 6.7us | 20.2x |
| ilo VM | 14.7us | 44.7x |
| Ruby | 20.6us | 62.7x |
| Python 3 | 29.5us | 89.6x |
### string
*build 100-char string via concat*
| Language | ns/call | vs fastest |
|----------|--------:|------------|
| Rust (native) | 138ns | **fastest** |
| Node/V8 | 420ns | 3.0x |
| TypeScript | 433ns | 3.1x |
| PyPy 3 | 770ns | 5.6x |
| LuaJIT | 798ns | 5.8x |
| PHP | 1.3us | 9.3x |
| Python 3 | 2.1us | 15.5x |
| C# (.NET) | 2.2us | 15.9x |
| Kotlin (JVM) | 2.2us | 16.1x |
| Go | 3.4us | 24.3x |
| ilo JIT | 3.5us | 25.0x |
| ilo VM | 5.0us | 36.1x |
| Ruby | 5.1us | 36.7x |
| Lua | 5.2us | 37.7x |
| ilo AOT | 7.5us | 54.2x |
### record
*create 100 structs, sum fields*
| Language | ns/call | vs fastest |
|----------|--------:|------------|
| Go | 124ns | **fastest** |
| LuaJIT | 132ns | 1.1x |
| Kotlin (JVM) | 281ns | 2.3x |
| TypeScript | 288ns | 2.3x |
| Node/V8 | 341ns | 2.8x |
| PyPy 3 | 446ns | 3.6x |
| C# (.NET) | 449ns | 3.6x |
| ilo JIT | 743ns | 6.0x |
| ilo AOT | 814ns | 6.6x |
| ilo VM | 3.5us | 28.5x |
| PHP | 4.1us | 33.0x |
| Lua | 7.6us | 61.1x |
| Ruby | 8.6us | 69.4x |
| Python 3 | 8.6us | 69.7x |
### mixed
*build 100 records, JSON-serialise*
| Language | ns/call | vs fastest |
|----------|--------:|------------|
| TypeScript | 5.3us | **fastest** |
| Node/V8 | 5.3us | 1.0x |
| Go | 6.2us | 1.2x |
| Kotlin (JVM) | 7.8us | 1.5x |
| PHP | 8.6us | 1.6x |
| LuaJIT | 8.6us | 1.6x |
| Rust (native) | 9.7us | 1.8x |
| Ruby | 18.6us | 3.5x |
| PyPy 3 | 21.0us | 4.0x |
| Python 3 | 27.9us | 5.3x |
| C# (.NET) | 30.8us | 5.8x |
| ilo VM | 41.8us | 7.9x |
| ilo AOT | 46.4us | 8.8x |
| ilo JIT | 49.0us | 9.2x |
| Lua | 50.2us | 9.5x |
### guards
*classify 1000 values via guard chains*
| Language | ns/call | vs fastest |
|----------|--------:|------------|
| Go | 394ns | **fastest** |
| Node/V8 | 1.0us | 2.6x |
| TypeScript | 1.0us | 2.6x |
| Kotlin (JVM) | 1.1us | 2.7x |
| Rust (native) | 1.5us | 3.8x |
| LuaJIT | 1.9us | 4.8x |
| PyPy 3 | 4.4us | 11.2x |
| C# (.NET) | 6.8us | 17.3x |
| ilo JIT | 6.8us | 17.4x |
| ilo AOT | 8.7us | 22.1x |
| PHP | 26.7us | 67.7x |
| Lua | 27.9us | 70.9x |
| Ruby | 38.9us | 98.6x |
| ilo VM | 53.3us | 135.3x |
| Python 3 | 65.6us | 166.4x |
### recurse
*fibonacci(10), recursive calls*
| Language | ns/call | vs fastest |
|----------|--------:|------------|
| Rust (native) | 137ns | **fastest** |
| Go | 176ns | 1.3x |
| Kotlin (JVM) | 191ns | 1.4x |
| LuaJIT | 235ns | 1.7x |
| C# (.NET) | 362ns | 2.6x |
| TypeScript | 387ns | 2.8x |
| Node/V8 | 421ns | 3.1x |
| ilo JIT | 543ns | 4.0x |
| ilo AOT | 1.0us | 7.6x |
| PyPy 3 | 1.2us | 9.1x |
| Lua | 2.8us | 20.1x |
| Ruby | 3.1us | 23.0x |
| PHP | 5.0us | 36.5x |
| Python 3 | 6.0us | 43.9x |
| ilo VM | 6.9us | 50.5x |
### foreach
*build list of 100, sum via foreach*
| Language | ns/call | vs fastest |
|----------|--------:|------------|
| Rust (native) | 62ns | **fastest** |
| Go | 330ns | 5.3x |
| TypeScript | 473ns | 7.6x |
| PyPy 3 | 575ns | 9.3x |
| Node/V8 | 623ns | 10.0x |
| PHP | 1.0us | 16.3x |
| Kotlin (JVM) | 1.2us | 19.0x |
| C# (.NET) | 1.3us | 21.2x |
| LuaJIT | 1.4us | 21.9x |
| Python 3 | 2.1us | 33.2x |
| Ruby | 3.1us | 49.5x |
| Lua | 3.8us | 61.6x |
| ilo JIT | 11.2us | 181.3x |
| ilo AOT | 13.2us | 212.9x |
| ilo VM | 14.7us | 236.7x |
### while
*sum 0..99 via while loop*
| Language | ns/call | vs fastest |
|----------|--------:|------------|
| Rust (native) | 1ns | **fastest** |
| TypeScript | 73ns | 73.0x |
| Node/V8 | 81ns | 81.0x |
| Go | 111ns | 111.0x |
| LuaJIT | 119ns | 119.0x |
| Kotlin (JVM) | 192ns | 192.0x |
| PyPy 3 | 274ns | 274.0x |
| ilo JIT | 423ns | 423.0x |
| C# (.NET) | 557ns | 557.0x |
| PHP | 707ns | 707.0x |
| ilo AOT | 828ns | 828.0x |
| Lua | 982ns | 982.0x |
| ilo VM | 1.5us | 1496.0x |
| Ruby | 2.3us | 2341.0x |
| Python 3 | 2.6us | 2614.0x |
### pipe
*chain 4 function calls × 100 (call overhead)*
| Language | ns/call | vs fastest |
|----------|--------:|------------|
| Go | 45ns | **fastest** |
| LuaJIT | 118ns | 2.6x |
| TypeScript | 165ns | 3.7x |
| Node/V8 | 179ns | 4.0x |
| Kotlin (JVM) | 211ns | 4.7x |
| PyPy 3 | 464ns | 10.3x |
| Rust (native) | 485ns | 10.8x |
| ilo JIT | 558ns | 12.4x |
| ilo AOT | 781ns | 17.4x |
| C# (.NET) | 798ns | 17.7x |
| Lua | 4.2us | 93.2x |
| Ruby | 6.2us | 138.1x |
| PHP | 6.5us | 144.5x |
| ilo VM | 6.7us | 149.8x |
| Python 3 | 10.2us | 227.8x |
### file
*read JSON file, parse 20 records, sum scores*
| Language | ns/call | vs fastest |
|----------|--------:|------------|
| Rust (native) | 10.7us | **fastest** |
| Node/V8 | 12.0us | 1.1x |
| TypeScript | 12.7us | 1.2x |
| LuaJIT | 14.2us | 1.3x |
| PHP | 14.6us | 1.4x |
| Lua | 16.2us | 1.5x |
| ilo JIT | 16.3us | 1.5x |
| ilo VM | 16.6us | 1.6x |
| ilo AOT | 17.7us | 1.7x |
| Kotlin (JVM) | 17.7us | 1.7x |
| Ruby | 17.9us | 1.7x |
| Python 3 | 19.4us | 1.8x |
| Go | 20.5us | 1.9x |
| C# (.NET) | 22.0us | 2.1x |
| PyPy 3 | 23.1us | 2.2x |
### api
*HTTP GET JSON, parse 20 records, sum scores*
| Language | ns/call | vs fastest |
|----------|--------:|------------|
| PyPy 3 | 762ns | **fastest** |
| Python 3 | 2.2us | 2.9x |
| LuaJIT | 46.1us | 60.5x |
| Lua | 54.7us | 71.8x |
| Rust (native) | 161.2us | 211.6x |
| ilo AOT | 167.0us | 219.2x |
| PHP | 186.4us | 244.6x |
| Kotlin (JVM) | 186.4us | 244.6x |
| Go | 192.7us | 252.9x |
| C# (.NET) | 229.6us | 301.3x |
| Node/V8 | 275.6us | 361.7x |
| TypeScript | 282.8us | 371.1x |
| Ruby | 284.4us | 373.2x |
| ilo VM | 942.3us | 1236.6x |
| ilo JIT | 3.3ms | 4325.0x |
## Methodology
- All benchmarks run on the same machine (Darwin arm64) in a single session
- Each benchmark warms up before timing begins
- Compiled languages use optimised builds (`-O2` / `-O`)
- V8 and LuaJIT benefit from JIT warmup during the iteration loop
- Results are from 2026-03-13
---
# CLI Reference
URL: https://ilo-lang.ai/docs/reference/cli/
Use this when looking up an ilo CLI flag, subcommand, or invocation form.
Use this when looking up an ilo CLI flag, subcommand, or invocation form.
## Basic usage
```bash
ilo 'funcname params>type;body' args # inline
ilo file.ilo funcname args # from file
```
First argument is code or a file path (auto-detected). Remaining arguments are passed to the first function.
### Verb-noun subcommands
For consistency with `cargo`, `go`, and similar toolchains, ilo also exposes verb forms:
```bash
ilo run file.ilo arg1 arg2 # run (alias for the bare positional)
ilo check file.ilo # verify without running (exit 0 if clean)
ilo build file.ilo -o ./bin # AOT compile (alias for `ilo compile`)
ilo add package-name # install a package from the registry
ilo apply fix.json # apply a structured repair plan
ilo trace file.ilo funcname args # execution trace with per-step timing
ilo httpd file.ilo --port 8080 # serve ilo functions over HTTP
```
The bare positional forms (`ilo file.ilo`, `ilo compile ...`) remain fully supported; the verbs are aliases, not replacements. Use whichever shape you prefer.
`ilo check` is the only verb that adds new behaviour: it runs the lexer, parser, import resolver, and verifier on the input and exits 0 if the program is well-typed and verifier-clean, or 1 with diagnostics on stderr otherwise. It does not execute the program. Useful for editor save-hooks, agent inner loops, and CI gates that want fast type-only feedback without running the workload.
```bash
ilo check file.ilo # human-readable diagnostics (auto-detects ANSI/text/JSON)
ilo check file.ilo --json # NDJSON diagnostics on stderr
ilo check file.ilo --strict # warnings (ILO-T032, ILO-T033) become exit-code failures
```
On a syntactically-broken input `ilo check` still emits the parse error and exits 1 rather than crashing, so it's safe to point at half-written code.
#### `--strict` for CI
By default `ilo check` only exits 1 on error-severity diagnostics. Warning-severity diagnostics (ILO-T032 bare `fmt`, ILO-T033 bare `mset` / `+=` / `mdel`, ILO-W002 `@x (jpar! …){…}` steering to `jpar-list!`, future warning codes) are emitted on stderr but the exit code stays 0, which is appropriate for interactive use where warnings are advisory.
CI harnesses that gate merges on `ilo check` need warnings to fail the build instead. `--strict` flips the exit-code decision: any warning bumps the exit code to 1. The diagnostic stream itself is unchanged: warnings still emit with `severity: "warning"` in the JSON output, so editor integrations that route by severity keep working correctly. Only the exit code is elevated.
```bash
# In CI:
ilo check src/*.ilo --strict --json
```
### `ilo test`
`ilo test ` runs the `-- run: ` / `-- out: ` (or `-- err: `) annotations embedded in `.ilo` source files - the same format the in-tree integration harness already uses. A file path tests that one file; a directory walks `*.ilo` recursively.
```bash
ilo test program.ilo # single file
ilo test tests/ # walk a directory recursively
ilo test program.ilo --engine all # vm + jit, tag PASS/FAIL with [vm]/[jit]
```
Output format is `PASS path::fn (line N)` / `FAIL path::fn (line N) (got: X, want: Y)`. A final `N passed, M failed` summary line follows. Exit code is 0 on all-pass, 1 if any case failed or no annotations were found. Default engine is `--vm`; pass `--engine jit` to switch, or `--engine all` to run every engine and surface per-engine failures.
`-- engine-skip: vm jit` annotations in the file source skip the listed engines for that file, matching the integration harness.
Because every `examples/*.ilo` file in the repo uses this annotation format already, `ilo test examples/` doubles as a smoke test for the language - and any nearby example serves as a worked reference when you're writing tests for your own program.
```ilo
m x:n>n;+ x 1
-- run: m 41
-- out: 42
```
```bash
$ ilo test add-one.ilo
PASS add-one.ilo::m (line 3)
1 passed, 0 failed
```
Select a named function in a multi-function program:
```bash
ilo 'dbl x:n>n;*x 2 tot p:n q:n r:n>n;s=*p q;t=*s r;+s t' tot 10 20 30
```
### Subcommand dispatch
The first positional argument after the source is treated as a function name **only if** it is a valid identifier and matches a defined function. The matcher accepts hyphenated identifiers, so `ilo file.ilo foo-bar` dispatches to the `foo-bar` function.
If the first positional is not a valid identifier (e.g. a path, a number, a list literal), it is treated as an argument to `main` when `main` is defined:
```bash
ilo file.ilo /tmp/data.json # routes to main, /tmp/data.json is arg 1
ilo file.ilo 1,2,3 # routes to main, list literal is arg 1
```
This matches the default-engine heuristic: if there's only one function, or there's a `main`, no explicit dispatch is needed. The same auto-pick-main applies to the engine-selection flags (`--vm`, `--jit`) - they fall back to `main` (or the sole function) when no subcommand is supplied:
```bash
ilo file.ilo --vm 5 # runs main 5 on the VM
```
### Unknown `--flag` guard
Any token in the positional tail matching the clean long-flag shape (`--word` or `--word-with-dashes`) that isn't a recognised flag is rejected upfront with `error: unrecognised flag ''` and exit code 1. This prevents typos like `--engine tree` from silently consuming the flag as positional data and producing misleading `ILO-R012 no functions defined` or `ILO-R004 main: expected N args, got N+1` errors later on.
```bash
ilo main.ilo --engine tree
# error: unrecognised flag '--engine'. Use 'ilo --help' for valid flags.
# To pass it as a literal arg, separate with '--' first.
```
To pass a hyphen-prefixed token through as literal data, place the `--` separator first. Anything after the first `--` is data:
```bash
ilo main.ilo -- --foo # `--foo` reaches `main` as a literal string arg
```
Tokens with `=` (`--key=val`), trailing or doubled dashes (`--foo-`, `--foo--bar`), and negative numbers (`-1`) are not clean flag shapes and pass through unchanged.
## Flags
| Flag | Description |
|------|-------------|
| `-ai` | Output compact spec for LLM consumption |
| `-e`, `--expanded` | Expanded/formatted output |
| `-d`, `--dense` | Dense wire format (minimal whitespace) |
| `-a`, `--ansi` | Force ANSI colour output (default for TTY) |
| `-t`, `--text` | Plain text output (no colour) |
| `-j`, `--json` | JSON output (default for piped output) |
| `-x`, `--explain` | Explain a program or error code |
| `--bench` | Benchmark a function |
| `--verify` | Type-check without executing |
| `--emit python` | Transpile to Python |
| `--emit js` | Transpile to JavaScript (Node.js/browser) |
| `--tools tools.json` | Load HTTP tool declarations |
| `--mcp mcp.json` | Connect MCP servers |
| `--no-hints`, `-nh` | Suppress idiomatic hints |
| `--silent`, `-s` | Suppress program stdout (mainly for `--bench`; bench output still reaches stdout) |
| `--max-ast-depth N` | Cap parser nesting at N (default 256). Protects `ilo serv` and other untrusted-source paths from `((((...))))` DoS payloads; raises `ILO-P103`. Applies to every subcommand that parses source. |
| `--max-runtime SECS` | Cap wall-clock runtime for `ilo run` at SECS (default 60). Aborts with `ILO-R016` on overrun. Pass `0` to disable for batch / training workloads. |
| `--max-output-bytes BYTES` | Cap total stdout output for `ilo run` at BYTES (default ~100 MB). Aborts with `ILO-R017` on overrun. Pass `0` to disable. |
| `compile` | AOT compile to standalone native binary |
## List arguments
Pass list arguments from the command line with bare commas (no spaces, no brackets):
```bash
ilo 'f xs:L n>n;len xs' 1,2,3 # → 3
ilo 'f xs:L t>t;xs.0' 'a,b,c' # → a
```
## Higher-order function invocation from CLI
`map`, `flt`, `fld` take a function name as their first argument. Define the helper function alongside a `main` entry point and invoke `main`:
```bash
ilo 'sq x:n>n;*x x main xs:L n>L n;map sq xs' main 1,2,3,4,5
# → [1, 4, 9, 16, 25]
ilo 'pos x:n>b;>x 0 main xs:L n>L n;flt pos xs' main -3,-1,0,2,4
# → [2, 4]
ilo 'add a:n b:n>n;+a b main xs:L n>n;fld add xs 0' main 1,2,3,4,5
# → 15
```
Pipe chains work the same way:
```bash
ilo 'sq x:n>n;*x x pos x:n>b;>x 0 main xs:L n>L n;xs >> flt pos >> map sq' main -3,-1,0,2,4
# → [4, 16]
```
## Output formats
### ANSI, text, JSON
Control how results and errors are rendered:
```bash
ilo 'code' -a # ANSI colour (default when stdout/stderr is a TTY)
ilo 'code' -t # plain text (no colour)
ilo 'code' -j # JSON (default when output is piped)
```
Set `NO_COLOR=1` to disable colour globally (equivalent to `--text`).
JSON error output follows a structured schema with `severity`, `code`, `message`, `labels` (with spans), `notes`, and `suggestion` fields.
### Dense and expanded format
```bash
ilo 'code' --dense # -d dense wire format (minimal whitespace, for agents)
ilo 'code' --expanded # -e expanded human-readable format
```
**Dense** is the default canonical form - single line per declaration, operators glued to first operand:
```ilo
cls sp:n>t;>=sp 1000{"gold"};>=sp 500{"silver"};"bronze"
```
**Expanded** adds 2-space indentation and spacing for human review:
```ilo
cls sp:n > t
>= sp 1000 {
"gold"
}
>= sp 500 {
"silver"
}
"bronze"
```
Dense format is canonical: `dense(parse(dense(parse(src)))) == dense(parse(src))`.
## Package registry: `ilo add`
Install packages from the ilo package registry:
```bash
ilo add http-client # add latest version
ilo add http-client@1.2.0 # pin a version
ilo add --list # list installed packages
```
Packages are resolved into a local `ilo.lock` file and imported with `use "pkg:http-client"`.
## Fix plans: `ilo apply`
`ilo apply` consumes a structured repair plan (JSON) produced by `ilo check --json` and applies the mechanical edits:
```bash
ilo check file.ilo --json 2>fixes.json
ilo apply fixes.json file.ilo
```
Only `fixSafety: "safe"` edits are applied automatically. `likely` edits are applied with a confirmation prompt unless `--yes` is passed. `risky` and `manual` edits are reported but not touched.
## Trace: `ilo trace`
`ilo trace` records execution with per-step timing and a call tree:
```bash
ilo trace program.ilo funcname args
ilo trace program.ilo funcname args --json # machine-readable NDJSON
```
Output shows each function call, its arguments, return value, and wall-clock duration. Useful for finding hot paths without running a full benchmark harness.
## HTTP server: `ilo httpd`
`ilo httpd` exposes ilo functions as HTTP endpoints:
```bash
ilo httpd program.ilo --port 8080
```
Each function in `program.ilo` becomes a `POST /funcname` endpoint. Request body is JSON-encoded arguments; response is JSON-encoded return value. CORS headers are included by default. Useful for deploying ilo programs as lightweight API services.
## Token count: `tokcount`
`tokcount` reports the token count of ilo source using the `tiktoken-rs` tokeniser (cl100k_base by default):
```bash
ilo tokcount program.ilo # token count of file
ilo tokcount 'f x:n>n;*x 2' # token count of inline source
ilo tokcount program.ilo --model gpt-4o # use a specific tokeniser
```
Useful for comparing ilo programs against equivalent Python or JavaScript to measure token savings.
## Transpile to Python
Generate standalone Python from ilo source:
```bash
ilo 'fac n:n>n;<=n 1 1;r=fac -n 1;*n r' --emit python
```
Output is valid Python that can be saved and run directly. Useful for interop or when deploying to environments without the ilo runtime.
## Transpile to JavaScript
Generate JavaScript (Node.js/browser) from ilo source:
```bash
ilo 'fac n:n>n;<=n 1 1;r=fac -n 1;*n r' --emit js
```
Emits ES module syntax. Builtins map to idiomatic JS equivalents. The JS target is the MVP backend added in 0.13.0; coverage matches the VM for numeric, text, and list operations.
## Benchmark
Time a function over repeated runs:
```bash
ilo program.ilo --bench funcname 10 20 30
```
Reports execution time for the named function with the given arguments.
## Explain
`--explain` (or `-x`) has two modes:
**Explain an error code** - show a detailed description of any `ILO-*` diagnostic:
```bash
ilo --explain ILO-T004
```
**Explain a program** - annotate each statement with a human-readable description of what it does:
```bash
ilo 'f x:n>n;*x 2' --explain
```
## Verify
All programs are type-verified before execution. Errors are reported with stable codes, source context, and suggestions:
```bash
ilo 'f x:n>n;*y 2' 5
# error[ILO-T004]: undefined variable 'y'
# --> 1:9
# |
# 1 | f x:n>n;*y 2
# | ^^^^
# |
# = note: in function 'f'
```
Error code prefixes indicate the phase:
| Prefix | Phase |
|--------|-------|
| `ILO-L___` | Lexer (tokenisation) |
| `ILO-P___` | Parser (syntax) |
| `ILO-T___` | Type verifier (static analysis) |
| `ILO-R___` | Runtime (execution) |
The verifier provides context-aware hints: "did you mean?" suggestions (Levenshtein-based), type conversion advice, missing match arms, and arity mismatches.
## AOT compilation
Compile an ilo program to a standalone native binary:
```bash
ilo compile program.ilo # → outputs ./program
ilo compile program.ilo -o mybin # → outputs ./mybin
ilo compile 'f x:n>n;*x 2' -o dbl # inline code
ilo compile program.ilo -o bin func # compile specific function
```
The compiler uses Cranelift to emit native machine code, links with the system `cc`, and produces a self-contained executable with no runtime dependencies.
**Entry-pick.** AOT follows the same entry-pick rules as the in-process engines: an explicit positional `func` argument wins; otherwise a single user-defined function is used directly; otherwise `main` is used if defined. With multiple functions and no `main` and no explicit entry, compilation fails with `ILO-E801` and exits 1 without writing a binary - rather than silently picking the first declared function (which produced binaries that called the wrong entry symbol and SIGSEGV'd at runtime).
```bash
ilo compile 'dbl x:n>n;*x 2' -o dbl
./dbl 5
# → 10
```
AOT-compiled binaries match the in-process runners byte-for-byte: top-level `~v` prints bare `v` on stdout with exit 0; `^e` prints `^e` on stderr with exit 1; non-Result returns print plain on stdout. Output is identical whether you `ilo run` or `ilo compile && ./binary`.
**Supported surface:** the same shape as the Cranelift JIT: numeric and text arithmetic, comparisons, guards and conditionals, loops, function calls, records, lists, maps, strings, JSON, HTTP, all builtins routed through the JIT runtime, and HOFs that take a function value (`map fn xs`, `flt fn xs`, `fld fn xs init`, `grp`, `uniqby`, fn-ref return and call), including inline lambdas with Phase 2 closure capture. As of 0.12.1, AOT-compiled binaries embed a postcard-serialised `CompiledProgram` blob into `.rodata` and a runtime helper deserialises it on startup so dispatch helpers can re-enter the VM on user-fn callbacks identically to the in-process runners. Pre-0.12.1, these shapes silently returned `nil` under AOT (engine audit PR #413 gap #1).
Requires the `cranelift` feature (enabled by default in release builds).
## Top-level program output
For a program whose entry function returns a Result, ilo splits the `~`/`^` wrapper across streams and exit codes so shell consumers don't have to strip a prefix:
| Top-level return | stdout | stderr | Exit |
|------------------|--------|--------|------|
| `~v` (Ok) | `v` (bare) | - | 0 |
| `^e` (Err) | - | `^e` | 1 |
| any non-Result | `v` | - | 0 |
In `--json` mode the value is always wrapped (`{"schemaVersion": 1, "ok": v}` / `{"schemaVersion": 1, "error": ...}`) on stdout; exit codes match the table above. The contract applies uniformly to in-process runners and AOT-compiled binaries: output is byte-for-byte identical across every backend.
## JSON output across subcommands
Every subcommand that produces machine-readable output supports `--json` (or `-j`), and every envelope starts with `"schemaVersion": 1` so agents can route on the contract and the shape can evolve without breaking older consumers. Five long-standing outputs (`run`, `graph`, `--ast`, `serv`, `tools --json`) and the newly-added `ilo spec --json` mode were brought into the convention in 0.12.1.
For five of those six the change is strictly additive - the existing object envelopes gained one extra top-level field next to their existing keys. The one observable break is `ilo tools --json`: its legacy shape was a bare array, so wrapping it as `{"schemaVersion": 1, "tools": [...]}` changes the top-level type. Indexing consumers should read `.tools[0]` instead of `[0]`.
| Command | `--json` support | Versioned? |
| --------------------------- | ------------------------------- | ----------------------------- |
| `ilo run` / `ilo file.ilo` | yes (success + error envelopes) | yes (0.12.1+) |
| `ilo check` | yes (one diagnostic per line) | per-diag |
| `ilo build` / `ilo compile` | yes (`output`, `sizeBytes`, `durationMs`) | yes |
| `ilo graph` | yes (always JSON unless `--dot`) | yes (0.12.1+) |
| `ilo --ast` | yes (AST as JSON) | yes (0.12.1+) |
| `ilo explain ILO-XXXX` | yes | yes |
| `ilo skill list/get/path/show` | yes | yes |
| `ilo version` | yes (`version`, `features`) | yes |
| `ilo tools` | yes (via `--json` subflag) | yes (0.12.1+, breaking wrap) |
| `ilo serv` | yes (JSONL stdio) | yes (0.12.1+, every line) |
| `ilo spec [lang\|ai]` | yes (wraps prose, 0.12.1+) | yes (0.12.1+) |
`spec` emits markdown / `ai.txt` for humans by default; with `--json` it wraps the prose as `{"schemaVersion": 1, "format": "markdown"|"ai-txt", "content": "..."}` so the contract matches every other emitter. `repl` is interactive and stays out of the JSON contract - the JSONL-over-stdio equivalent for agents is `ilo serv`.
The full per-command schema reference lives in [`JSON_OUTPUT.md`](https://github.com/ilo-lang/ilo/blob/main/JSON_OUTPUT.md) in the ilo repo; it's locked by `tests/json_output_contracts.rs` so future changes that break a schema fail CI.
## Backend selection
ilo supports multiple execution backends. The default is the bytecode register VM. Cranelift JIT is opt-in via `--jit` for hot numeric loops:
| Flag | Backend |
|------|---------|
| *(default)* | Register VM (closure-aware, all opcodes supported) |
| `--jit` | Cranelift JIT (hot numeric loops; falls back to VM on bailout) |
| `--vm` | Register VM (canonical explicit form, symmetric with `--jit`) |
| `--run-vm` | Deprecated alias for `--vm`. Emits a one-shot stderr hint; removed in 0.13.0. |
| `--run-llvm` | LLVM JIT (requires `--features llvm` build) |
The tree-walking interpreter was removed as a user-selectable engine in 0.13.0; `--run-tree` and `--run` are no longer recognised flags. The shared runtime module stays in-tree as the internal dispatch target for the ~30 builtins routed through the VM/Cranelift tree-bridge; the VM bails to it transparently with negligible round-trip cost (ILO-234).
```bash
ilo 'fac n:n>n;<=n 1 1;r=fac -n 1;*n r' --jit fac 10
```
**Why the VM is the default.** It supports every opcode in the language (closures, listview windows, fused len-of-filter, every modern shape) without compile-and-bail cost. The pre-v0.11.9 default was Cranelift JIT with VM fallback - it paid the JIT compile cost on every program before discovering the JIT couldn't handle some opcode and falling back anyway. Opt into the JIT explicitly when a hot numeric loop justifies the compile time.
## REPL
Start an interactive session:
```bash
ilo repl # interactive session
ilo repl -j # REPL with JSON output (useful for agent integration)
```
Define functions, evaluate expressions, and accumulate state across lines. The REPL supports vim-style commands:
| Command | Description |
|---------|-------------|
| `:q` | Quit the REPL |
| `:w file.ilo` | Write current definitions to a file |
| `:defs` | Show all defined functions and types |
| `:clear` | Clear all accumulated state |
| `:help` | Show REPL help |
## Help
```bash
ilo help # usage and examples
ilo help lang # full language specification
ilo help ai # compact spec for LLM consumption
ilo -ai # same as help ai
```
## Constrained decoding (`ilo constrain`)
`ilo constrain` exports the parser grammar as JSON so an external LLM harness can apply **constrained decoding** at generation time. By mapping the grammar's per-state token masks onto the model's tokeniser logits, a host harness makes **syntactically invalid ilo unreachable at generation** — the model literally cannot produce tokens that would fail `ilo check` with an `ILO-L###` or `ILO-P###` code.
Three modes:
### `ilo constrain` (default — `--mode states`)
Emits the grammar as a JSON state machine:
```json
{
"schemaVersion": 1,
"states": {
"TopLevel": {
"transitions": {
"type": "AfterType",
"tool": "AfterTool",
"ident": "FnHeader",
"eof": "End"
}
},
"FnHeader": {
"transitions": { "<": "TypeParams", ">": "ReturnType", "ident": "ParamName" }
}
},
"initial": "TopLevel",
"accept": ["End"]
}
```
30 parse states covering declaration, type, parameter, expression, statement, match-arm, and list-literal positions. A host harness walks this token-by-token to determine the valid token set at each generation step.
### `ilo constrain --mode masks`
Emits per-state binary masks over a 58-entry token-category vocabulary:
```json
{
"schemaVersion": 1,
"vocabulary": ["type", "tool", "use", "with", "by", "L", "R", "F", "O", "M", "S",
"W", "U32", "U64", "I64", "true", "false", "nil", "num", "text", "ident", "_",
">=", "<=", "!=", "+=", ">>", "??", "!!", "..", ".?", "+", "-", "*", "/", ">",
"<", "=", "&", "|", "?", "@", "!", "^", "~", "$", ":", ";", ".", ",", "{", "}",
"(", ")", "[", "]", "\\n", "eof"],
"masks": {
"TopLevel": [1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
"AfterOp": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]
}
}
```
Each mask array has length 58. A `1` at index `i` means `vocabulary[i]` is valid in that state. Map onto the target model's BPE tokeniser to mask logits at each generation step. The masks are over **categories** (`ident`, `num`, `text`) rather than individual token values, so the vocabulary is small and stable.
### `ilo constrain --mode completions --file FILE --line LINE --col COL`
Tokenises the source file up to a 1-based line/column cursor, runs the token stream through the state machine, and returns the current parse state plus the set of valid next tokens:
```json
{
"schemaVersion": 1,
"state": "FnHeader",
"validTokens": ["<", "ident", ">"]
}
```
Useful for editor integration (autocomplete, inline hints) and for agents that need to ask "what can I write here?"
### Limitations
The state machine is **static**: it encodes the grammar's shape, not the parser's internal bookkeeping (arity tables, lambda context, declaration boundaries). It prevents lex/parse errors at generation time; type errors (`ILO-T004`, `ILO-T006`) and runtime errors are still caught by `ilo check` and `ilo run`.
ilo's prefix notation and small keyword set make the state space small (29 states, 59 categories) and the masks strong — far fewer valid tokens per position than a conventional language with operator precedence and infix expressions.
---
# Optional Contracts
URL: https://ilo-lang.ai/docs/reference/contracts/
Optional req/ens clauses for lightweight precondition checking. Prototype, pattern-based.
> **Prototype.** Contracts are experimental: parsed and stored, but the
> precondition checker uses simple structural pattern matching, not a full SMT
> solver. They may emit false positives when guard logic is too complex to match
> structurally.
ilo supports optional preconditions (`req`) and postconditions (`ens`) on
function declarations. They are **strictly opt-in** — functions without `req`/`ens`
behave exactly as before. When present, the verifier does a lightweight structural
check at call sites and emits a warning (`ILO-W030`) if it cannot prove the
precondition from preceding guards.
## Syntax
`req` and `ens` are contextual keywords placed after the return type (and any
optional effect set), before the `;`/body:
```ilo
-- Precondition only
f x:n>n req x>=0;n ens result>=0;R n t req b!=0 ens result>=0;=b 0 ^"divide by zero";~/a b
```
## How the checker works
The verifier scans every call site of a function with a `req` clause. For each
call site, it looks at **preceding braceless guard statements** for a pattern
that implies the precondition.
| Precondition | Satisfying guard | Example |
|---|---|---|
| `req b!=0` | `=b 0 ^"..."` | guard rejects zero divisor |
| `req x>=0` | `>=x 0 ^"..."` or `>x 0 ^"..."` | guard rejects negatives |
| `req x>0` | `>x 0 ^"..."` | guard rejects zero/negatives |
| `req x<=0` | `<=x 0 ^"..."` or `n ens result>=0; calc.ilo:3:12
|
3 | calc x:n y:n>R n t;div x y
| ^^^^^^^^
= hint: guard before the call: e.g. `=b!=0 ^"..."` or wrap the call in a match on R
```
**Warning-only.** The program compiles and runs. Runtime guards (`=b 0 ^"..."`)
catch the actual violation at runtime if it occurs. `ilo check --strict`
elevates ILO-W030 to an exit-code failure for CI pipelines.
## Examples
### Division with precondition
```ilo
div a:n b:n>R n t req b!=0;=b 0 ^"divide by zero";~/a b
-- Caller WITHOUT guard: ILO-W030 fires
calc x:n y:n>R n t;div x y
-- Caller WITH guard: no warning
calc-safe x:n y:n>R n t;=y 0 ^"bad arg";div x y
```
### Absolute value with postcondition
```ilo
abs x:n>n ens result>=0;=0;len xs
```
## Limitations (prototype)
- **Pattern matching only.** The checker matches guards structurally — it does
not use an SMT solver, so complex guard logic (nested conditions, indirect
reasoning) may produce false-positive warnings.
- **Literal comparisons.** Only guards comparing a variable to a literal
(`=b 0`, `>=x 0`) are matched. Guards comparing two variables (`=x y`) are
not yet supported.
- **Postconditions not verified.** `ens` clauses are stored and shown by
`ilo explain` but not checked against the function body.
- **Future.** A v2 with Z3 SMT proving behind a feature flag is planned but
not shipped.
## Stability
Experimental. The `req`/`ens` syntax and ILO-W030 diagnostic may change in a
future release. Contracts do not affect programs that don't declare them.
---
# Diagnostics
URL: https://ilo-lang.ai/docs/reference/diagnostics/
Use this when you need to decode an ILO-* error code, understand fixSafety, or look up a repair plan.
Use this when you need to decode an ILO-* error code, understand fixSafety, or look up a repair plan.
ilo verifies programs before execution. When verification fails, you get a compact error code. Runtime failures emit codes too. Use `ilo --explain ILO-XNNN` for the full explanation.
## fixSafety taxonomy
Every diagnostic carries a `fixSafety` tag that tells an agent how aggressively it may auto-correct the code without re-asking the user:
| fixSafety | Meaning | Agent behaviour |
|-----------|---------|-----------------|
| `safe` | Mechanical, no semantic change | Apply the suggested fix without confirmation. |
| `likely` | Almost always correct, but reads context | Apply and continue; surface a one-line note. |
| `risky` | Multiple valid repairs exist | Propose and ask for confirmation. |
| `manual` | Requires human judgment | Surface the diagnostic and stop. |
## Repair plans
Diagnostics with a structured repair plan attach a JSON block to the JSON-mode (`-j`) error. The plan lists ordered edits the agent can apply mechanically:
```json
{
"code": "ILO-T004",
"message": "undefined variable 'y'",
"fixSafety": "likely",
"repair": [
{"kind": "rename", "from": "y", "to": "x"}
]
}
```
See `ilo --explain ILO-XNNN` for the per-code repair contract.
## Namespace ranges
Every diagnostic ilo emits has the shape `ILO-`. The letter is the **namespace** - the phase that raised the diagnostic - so agents and tools can route on prefix without parsing the message. Numeric ranges are reserved per namespace with generous gaps, so future codes slot in cleanly. A cross-engine regression test enforces this contract.
| Range | Letter | Area | Status |
|-------|--------|------|--------|
| `ILO-L000-099` | L | Lexer / tokenisation | active |
| `ILO-P100-199` | P | Parser / syntax | active |
| `ILO-N200-299` | N | Names / resolution | reserved |
| `ILO-I300-399` | I | Imports | reserved |
| `ILO-T400-499` | T | Types | active |
| `ILO-V500-599` | V | Verifier (post-type checks) | reserved |
| `ILO-R600-699` | R | Runtime | active |
| `ILO-D700-799` | D | Deprecation warnings | reserved |
| `ILO-E800-899` | E | Engine-specific limitations | reserved |
| `ILO-S900-999` | S | Skill / spec system | reserved |
**Historical codes stay valid.** ilo shipped with flat numbering inside each namespace (`ILO-L001`, `ILO-P001`, `ILO-T001`, `ILO-R001`, `ILO-W001`, all starting at 001). Those codes remain valid forever - agents and pinned tool configs depend on them. The hundreds-block allocation applies to new codes going forward.
**Reserved namespaces.** `N`, `I`, `V`, `D`, `E`, `S` carry no codes today. They are forward declarations so the first code in each category lands in its own range without conflicting with the active namespaces. `D` is earmarked for deprecation warnings: when a feature is scheduled for removal it emits an `ILO-D7xx` warning at compile time without failing the build.
## Lexer errors (ILO-L)
| Code | Description |
|------|-------------|
| `ILO-L001` | Unexpected character |
| `ILO-L002` | Underscore in identifier, use hyphens |
| `ILO-L003` | Uppercase identifier, use lowercase |
## Parse errors (ILO-P)
| Code | Description |
|------|-------------|
| `ILO-P001` | Unexpected token at top level |
| `ILO-P002` | Unexpected end of file at top level |
| `ILO-P003` | Unexpected token |
| `ILO-P004` | Unexpected end of file |
| `ILO-P005` | Expected identifier, got token |
| `ILO-P006` | Expected identifier, got end of file |
| `ILO-P007` | Expected type annotation, got token |
| `ILO-P008` | Expected type annotation, got end of file |
| `ILO-P009` | Expected expression, got token |
| `ILO-P010` | Expected expression, got end of file |
| `ILO-P011` | Expected pattern, got token |
| `ILO-P012` | Expected pattern, got end of file |
| `ILO-P013` | Expected number literal, got token |
| `ILO-P014` | Expected number literal, got end of file |
| `ILO-P015` | Expected tool description string |
| `ILO-P016` | Unexpected token after braceless guard body |
| `ILO-P017` | `use`-import failed |
| `ILO-P018` | Variadic builtin not in trailing position |
| `ILO-P019` | `use`-import name not found |
| `ILO-P020` | Incomplete function header |
| `ILO-P021` | Double-minus prefix-binop trap rejected - `- - a b c d` for ` ∈ {+, *, /}` is rejected at parse time because it silently miscompiles into `-(a-b)`. Use `- 0 +*a b *c d` or bind the inner result first. |
| `ILO-P101` | List-literal element starts with a variadic builtin (`fmt`, `fmt2`) followed by operands - rejected at parse time because the bare form would fall through as multiple elements with the builtin name as an undefined Ref. Wrap the call in parens (`[k (fmt2 v 2)]`) or bind first (`s=fmt2 v 2;[k s]`). |
| `ILO-P103` | AST nesting depth exceeded - the parser refused source nesting more than 256 levels deep. Default cap guards `ilo serv` and other untrusted-source paths from `((((...((1+1))))...))` DoS payloads. Flatten by binding intermediates, or raise with `--max-ast-depth N`. |
## Type / verifier errors (ILO-T)
| Code | Description |
|------|-------------|
| `ILO-T001` | Duplicate type definition |
| `ILO-T002` | Duplicate function or tool definition |
| `ILO-T003` | Undefined type |
| `ILO-T004` | Undefined variable |
| `ILO-T005` | Undefined function |
| `ILO-T006` | Arity mismatch |
| `ILO-T007` | Type mismatch at call site |
| `ILO-T008` | Return type mismatch |
| `ILO-T009` | Arithmetic operator type error |
| `ILO-T010` | Comparison operator type error |
| `ILO-T011` | Append (`+=`) type error |
| `ILO-T012` | Negate type error |
| `ILO-T013` | Builtin argument type error |
| `ILO-T014` | Foreach collection type error |
| `ILO-T015` | Record missing field |
| `ILO-T016` | Record unknown field |
| `ILO-T017` | Record field type mismatch |
| `ILO-T018` | Field access on non-record type |
| `ILO-T019` | Field not found on type |
| `ILO-T020` | `with` on non-record type |
| `ILO-T021` | `with` field not found |
| `ILO-T022` | `with` field type mismatch |
| `ILO-T023` | Index access on non-list type |
| `ILO-T024` | Non-exhaustive match |
| `ILO-T025` | `!` used on non-Result call |
| `ILO-T026` | `!` used in non-Result function |
| `ILO-T027` | Braceless guard body looks like a function name |
| `ILO-T028` | `brk`/`cnt` used outside a loop |
| `ILO-T029` | Unreachable code |
| `ILO-T030` | Circular type alias |
| `ILO-T031` | Type alias shadows builtin |
| `ILO-T032` | Bare `fmt` result is discarded |
| `ILO-T033` | Bare mutation-shaped builtin result is discarded |
| `ILO-T034` | `!` / `!!` used on a non-callable value |
| `ILO-T035` | Function exceeds the 256-register VM cap |
| `ILO-T036` | Call requires too many register slots (VM cap) |
## Warnings (ILO-W)
| Code | Description |
|------|-------------|
| `ILO-W001` | Guard without else inside loop (retired) |
| `ILO-W002` | Iterating `jpar!` result, use `jpar-list!` instead |
## Runtime errors (ILO-R)
| Code | Description |
|------|-------------|
| `ILO-R001` | Undefined variable at runtime |
| `ILO-R002` | Undefined function at runtime |
| `ILO-R003` | Division by zero |
| `ILO-R004` | Runtime type error |
| `ILO-R005` | Field not found at runtime |
| `ILO-R006` | List index out of bounds |
| `ILO-R007` | `foreach` requires a list |
| `ILO-R008` | `with` requires a record |
| `ILO-R009` | Builtin argument error at runtime |
| `ILO-R010` | Compile error: undefined variable |
| `ILO-R011` | Compile error: undefined function |
| `ILO-R012` | No functions defined |
| `ILO-R013` | Internal VM error |
| `ILO-R014` | Auto-unwrap propagated Err / nil |
| `ILO-R016` | Wall-clock runtime budget exceeded - `ilo run` killed the program at the default 60 s budget. Almost always an infinite loop (missing loop increment, recursion without base case). Raise with `--max-runtime SECS` (0 disables) if a legitimate program needs longer. |
| `ILO-R017` | Stdout output budget exceeded - `ilo run` killed the program at the default ~100 MB budget. Usually a `prnt` call inside an unbounded loop. Raise with `--max-output-bytes BYTES` (0 disables) if legitimate. |
| `ILO-R026` | Panic-unwrap on Err / nil |
| `ILO-R099` | Internal runtime error |
## Deprecation warnings (ILO-D)
Reserved range. When a feature is scheduled for removal, ilo emits an `ILO-D7xx` warning at compile time without failing the build. No codes allocated yet - the first `ILO-D###` ships with the first feature deprecation.
## Engine-specific (ILO-E)
Reserved range for diagnostics that come from a particular execution engine (VM register caps surfaced ahead of time, JIT-unsupported opcode, AOT constraint). No `ILO-E###` codes yet.
## Skill / spec (ILO-S)
Reserved range for skill-bundle loader errors, manifest issues, spec-link breaks. No codes yet.
Compact error codes mean agents spend fewer tokens reading error messages and correct faster.
---
# Effects
URL: https://ilo-lang.ai/docs/reference/effects/
Compile-time side-effect sigils (/http /fs /io /net /ml /time /rand) for static capability tracking in ilo.
Effect sigils are optional annotations appended to a function signature that declare which side effects the function may perform. They are **compile-time only** — no runtime overhead, no runtime interception. The verifier checks that declared sigils cover all side-effectful calls in the body.
## Syntax
Append sigils after the return type (and after any `^effect_set`):
```ilo
fetch url:t>R t t /http
save path:t data:t>R _ t /fs
log msg:t>R _ t /io
analyze data:t>R t t /http /ml
pure-sum xs:L n>n;sum xs
```
## Sigil reference
| Sigil | Covers |
|-------|--------|
| `/http` | HTTP builtins (`get`, `post`, `get-many`, `get-stream`, `pst-stream`, `del`, `put`, `pat`, `opt`, `hed`, `pstx`, `getx`, `get-to`) and all external tools (MCP / HTTP providers) |
| `/fs` | Filesystem builtins (`rd`, `rdb`, `rdl`, `rdjl`, `wr`, `wrl`, `ls`, `walk`, `glob`, `isfile`, `isdir`) |
| `/io` | Console / environment (`prnt`, `env`, `env-all`) |
| `/net` | Subprocess execution (`run`, `run2`, `run-full-env`, `run2-full-env`) |
| `/ml` | LLM inference (reserved; no builtins tagged yet) |
| `/time` | Time builtins (`now`, `now-ms`, `sleep`) |
| `/rand` | Random builtins (`rnd`, `rndn`) |
## Rules
- **No sigils = pure.** The verifier rejects calls to side-effectful builtins or tools in a function with no effect sigils.
- **Declared must cover actual.** If the body calls a builtin or function with effect `/http`, the signature must declare `/http`.
- **Over-declaring is safe.** Declaring `/http` but never calling HTTP is fine — no warning.
- **Transitive propagation.** Calling a function declared `/http` makes the caller require `/http` too.
- **External tools are `/http`.** Tool declarations (`tool name ...`) are assigned `Effect::Http` at every call site.
## Warnings and enforcement
Undeclared effects produce **ILO-W051** — a warning by default. Promote to a build failure with `--strict`:
```bash
ilo check file.ilo --strict # ILO-W051 becomes exit-code failure
```
The warning message includes the undeclared effect(s) and a hint showing the sigil(s) to add:
```
warning[ILO-W051]: undeclared effect: function declared `pure` but body uses `/http`
--> example.ilo:2:1
= suggestion: add the effect sigil(s) to the signature: `/http` or remove the side-effectful call
```
## Examples
### Pure function (no sigils)
```ilo
add a:n b:n>n;+a b
```
No side-effectful calls — no sigils needed.
### HTTP + filesystem
```ilo
fetch-and-save url:t path:t>R _ t /http /fs
r=get! url
wr path r
```
Declares both `/http` (for `get`) and `/fs` (for `wr`). Calling `get` is covered by `/http`; calling `wr` is covered by `/fs`.
### Undeclared effect (ILO-W051)
```ilo
broken url:t path:t>R _ t /http
r=get! url
wr path r
```
`wr` requires `/fs` but the signature only declares `/http`. The verifier emits ILO-W051.
### Transitive propagation
```ilo
helper url:t>R t t /http
get! url
caller url:t>R t t /http
helper! url
```
`caller` calls `helper` which is `/http`, so `caller` must also declare `/http`. Without it: ILO-W051.
## Design notes
- Effect sigils are **not** a full algebraic effect system. There is no `perform`/`handle`/`resume`. They are compile-time annotations only.
- The effect set is **fixed** (not user-defined). The seven sigils cover the standard library's side-effectful surface.
- Effect sigils complement the existing runtime capability flags (`--allow-net`, `--allow-read`, `--allow-write`, `--allow-run`). Sigils are static (compile-time, per-function); capability flags are dynamic (runtime, per-process).
---
# Engines
URL: https://ilo-lang.ai/docs/reference/engines/
Use this when you need to pick or understand an ilo execution engine (VM, JIT, AOT).
Use this when you need to pick or understand an ilo execution engine.
ilo programs verify once and then run on either the bytecode VM or Cranelift (JIT or AOT). Both accept the same source. They differ only in start-up cost, peak throughput, and platform reach.
## Engine profiles
| Engine | Start-up | Throughput | Use when |
|--------|----------|------------|----------|
| **VM** (bytecode) | Low | Medium | Default for every workload; covers every language feature |
| **JIT** (Cranelift) | Medium | High | Hot loops, benchmarks, long-running services |
| **AOT** | Highest (build) | Highest | Shipping a single native binary with no runtime |
As of 0.13.0 ilo ships with two engines: the VM and Cranelift. The tree-walking interpreter was removed as a user-selectable engine; the shared runtime module (`src/runtime/`) stays in-tree as the internal dispatch target for ~30 builtins routed through the VM/Cranelift tree-bridge - HOF / regex / fmt / IO / sleep / ct / rsrt / closure-bind-ctx / crypto / calendar shapes the VM and Cranelift haven't lifted natively. The VM bails to it transparently; per ILO-234 the round-trip cost is negligible.
The CLI picks a sensible default per command. To force an engine, see `--vm` / `--jit` in the [CLI Reference](/docs/reference/cli/).
## Contracts every engine shares
- **Same verifier.** A program that verifies on one engine verifies on all.
- **Same builtins.** Every builtin behaves identically across engines, except where the engine lacks the underlying capability (HTTP is unavailable in the WASM build).
- **Same diagnostics.** Error codes (`ILO-T...`, `ILO-R...`) match across engines. See [Diagnostics](/docs/reference/diagnostics/).
- **Same float semantics.** All engines use IEEE-754 f64.
## Engine-specific notes
### VM
- Default engine for `ilo run` on a native binary.
- Capped at 256 registers per function (`ILO-T035`).
- Internally bails to the shared runtime module for a small set of HOFs (`map`/`flt`/`fld`/`srt`/`rsrt` with a ctx arg), regex (`rgx`/`rgxall`/`rgxall1`), variadic `fmt`, 2-arg `rd`/`rdb`, 1-arg `sleep`, and 2/3-arg `ct`/`rsrt`. Transparent: same output, same diagnostics, no user-visible difference.
### JIT (Cranelift)
- Enabled with `--features cranelift` in a source build.
- Best for hot loops and numeric kernels.
- As of 0.13.0, supports tail-call optimisation via `return_call` - recursive tail calls compile to a loop without stack growth.
- Dispatches `par-map` to the parallel scheduler and JIT-compiles the inner function before distributing work to threads.
### AOT
- Produces a standalone native binary.
- See `ilo build` in the [CLI Reference](/docs/reference/cli/).
### WASM
- Built with `--features wasm`. Targets the browser and Node.js via `--emit js` or direct WASM output.
- As of 0.13.0, HTTP fetch (`$url`) is available inside WASM builds via `fetch` API bridging.
- Most numeric, text, and list builtins are supported; filesystem builtins (`rd`, `wrt`, `rdl`) require a WASM filesystem shim.
### Cross-platform
As of 0.13.0, ilo has a cross-platform MVP: all four engines are tested on Linux x86-64, macOS arm64, and Windows x86-64 in CI. The install script (`curl -fsSL .../install.sh | sh`) auto-selects the right binary.
## Picking an engine
If you do not know, do not pick. The default is right for most programs. Reach for `--engine` only when you have measured a reason. See [Benchmarks](/docs/reference/benchmarks/) for numbers.
---
# Error Handling
URL: https://ilo-lang.ai/docs/reference/error-handling/
Use this when handling Results, optionals, auto-unwrap with `!`, or nil-coalesce with `??`.
Use this when handling Results, optionals, auto-unwrap with `!`, or nil-coalesce with `??`.
## Why not try/catch?
Try/catch is verbose. An AI agent generating `try { ... } catch (e) { ... }` pays tokens for boilerplate, and if it forgets the catch block, the error is silently swallowed. ilo makes errors part of the type system - you can't forget to handle them.
## Errors with `^`
`^` exits the function immediately with an error - and the verifier requires the function's return type to admit it. A `^` in a function declared `> n` is rejected at check time (`ILO-T008`): the signature promises a plain number, so there would be no channel for the error to travel back on. Declare the return as `R` instead:
```ilo
div a:n b:n > R n t -- number on success, text on error
= b 0 ^"divide by zero" -- ^ returns the Err arm
~/ a b -- ~ returns the Ok arm
```
```bash
ilo 'div a:n b:n>R n t;=b 0 ^"divide by zero";~/a b' div 10 2
# → 5
ilo 'div a:n b:n>R n t;=b 0 ^"divide by zero";~/a b' div 10 0
# → ^divide by zero
```
Making the error part of the type means every caller is forced to handle it - there is no crash-through path.
## Recoverable errors with `R`
If you want callers to **catch and handle** errors, use `R` as the return type. This wraps the result so the caller can inspect it:
```
R success-type error-type
```
Read it as: "**R**esult - first type is what you get on success, second is what you get on error".
For example, `R n t` breaks down as:
| Part | Meaning |
|------|---------|
| `R` | this is a Result |
| `n` | success value is a **n**umber |
| `t` | error value is **t**ext |
Common patterns:
| Type | Success | Error |
|------|---------|-------|
| `R n t` | number | text message |
| `R t t` | text | text message |
| `R _ t` | nothing (side effect) | text message |
Inside the function, `~` marks the success path and `^` marks the error path:
```ilo
div a:n b:n > R n t
= b 0 ^"divide by zero"
~ / a b
```
Now callers can match on the result with `?`:
```ilo
show a:n b:n > t
r = div a b
? r {~v: str v; ^e: e}
```
- `~v:` - if success, bind the value to `v`
- `^e:` - if error, bind the error to `e`
```bash
ilo 'div a:n b:n>R n t;=b 0 ^"divide by zero";~/a b
show a:n b:n>t;r=div a b;?r{~v:str v;^e:e}' show 10 0
# → divide by zero
```
## Auto-unwrap with `!`
Matching every Result is tedious. `!` auto-unwraps: success continues, error propagates up. The calling function must also return `R`:
```ilo
calc a:n b:n > R n t
v = div! a b
~ * v 2
```
`div! a b` = call `div`, if error return it immediately, if ok bind the value. One character (`!`) replaces a whole try/catch block.
## Panic-unwrap with `!!`
`!!` has the same shape as `!`, but on error it aborts the program with a runtime diagnostic and exit 1 instead of propagating. There is no constraint on the enclosing function's return type, so `!!` works from `main>t`, `main>n`, or any non-Result context. Use it for one-shot scripts where there is nowhere sensible to propagate to:
```ilo
main>t
xs=rdl!! "input.txt" -- read file, abort with diagnostic if missing
cat xs "\n"
```
```ilo
main>n;num!! "42" -- parse number, abort on parse error
```
On `^e` the program writes `panic-unwrap: ` to stderr and exits 1. On `O nil` it writes `panic-unwrap: expected value, got nil`. On `~v` or non-nil Optional, the inner value is extracted, identical to `!`.
Use `!` when the caller might want to react to the error (compensate, retry, log). Use `!!` when failure is a programming or environmental error the caller has no way to recover from.
## Optional type `O`
`O T` means "maybe a value of type `T`, maybe `nil`":
```ilo
f > O n; nil
g > O n; 42
```
```bash
ilo 'f>O n;nil' f
# → nil
ilo 'f>O n;42' f
# → 42
```
## Nil-coalesce with `??`
`??` provides a default when a value is nil:
```ilo
f x:O n > n; x ?? 0
```
```bash
ilo 'f x:O n>n;x??0' f 42
# → 42
ilo 'f>O n;nil' f
# → nil
```
`??` chains: `a??b??99` returns the first non-nil value, or 99.
## Defer and errdefer
`defer` schedules a statement to run when the enclosing function exits, regardless of whether it exits normally or via `^`:
```ilo
open-and-read path:t>R t t
f = open! path
defer close f -- always runs on exit
~rdl! f
```
`errdefer` runs only when the function exits via `^` (error path):
```ilo
create-file path:t>R _ t
f = create! path
errdefer delete f -- cleans up only on error
write! f "content"
~_
```
`defer` and `errdefer` stack in LIFO order. They are especially useful for resource cleanup (file handles, network connections) without try/finally boilerplate.
## Compact error codes
When type verification fails, ilo returns compact error codes:
```
ILO-T004: type mismatch: expected n, got t
```
One token, not a paragraph. Agents correct faster with less context.
---
# Gotchas
URL: https://ilo-lang.ai/docs/reference/gotchas/
Use this when something parses unexpectedly or you hit a common ilo trap.
Use this when something parses unexpectedly or you hit a common ilo trap.
## `?` and `_`: context-sensitive symbols
`?` has two meanings, `_` has two meanings:
| Context | Syntax | Meaning |
|---------|--------|---------|
| Type annotation | `x:_` | Any/unknown type (wildcard) |
| Match expression | `?x{...}` | Match on value |
| Prefix ternary | `?=x 0 10 20` | Conditional expression |
| Match wildcard | `_:"default"` | Catch-all arm |
`_` means "don't care" in both type annotations and match patterns. `?` is always a conditional/match operator.
```ilo
f x:_>t;?x{n v:"number";t v:"text";_:"other"}
```
Here `_` after `:` is the any type, `?` starts a match, and `_:` in the match is the wildcard arm.
## Subtraction spacing: `a - b` needs spaces both sides
The lexer packs a leading `-` (no preceding space) into the number token, so `a -1.5` is `Number(a), Number(-1.5)` not `a, Minus, 1.5`. This is deliberate so call args and list elements read naturally:
```ilo
mod n -2 -- mod(n, -2) - glued -2 is the call's second arg
sub 5 -3 -- sub(5, -3)
[1 -2 3] -- list of [1, -2, 3]
+a -3 -- a + (-3) - glued -3 is the binop's second operand
```
The downside: `0 -1.5` at statement position is **not** subtraction. The parser sees `Number(0), Number(-1.5)` and errors with:
```
ILO-P001: expected declaration, got number `-1.5`
hint: for subtraction, write `a - b` with spaces both sides (e.g. `0 - 1.5`).
for a negative value as an expression, wrap in parens: `(-1.5)`.
```
Three idiomatic shapes:
```ilo
0 - 1.5 -- canonical subtraction (spaces both sides)
(-1.5) -- bare negative value as an expression
mod n -2 -- glued -N as call arg / list element / binop operand (load-bearing)
```
The lexer auto-splits `-N` back into `Minus + Number` when the previous token introduces a fresh-expression position (`;`, newline, `=`, `{`, `(`, or `-`), so `-0 v` at the start of a body, `r=-1 x`, `{-1}`, and `(-1)` all work. After an Ident, `[`, or another prefix binop, the glued form is preserved.
## Ternary brace shapes: three canonical forms
`?` followed by braces is one operator with three shapes. Mixing them produces `ILO-P009`. The hint is context-aware: the parser looks at the arm shape and tells you which form you were closest to.
```ilo
?cond{T_arm; F_arm} -- if/else on a boolean (exactly 2 arms)
?expr{val:T; val:T; _:T} -- match on a value (one arm per case, `_:` catch-all)
?expr{Ok:body; Err:body} -- match on a result/option (variant tags as arm keys)
```
Common misfires:
```ilo
?x{1:"a";2:"b"} -- ILO-P009: looks like match-on-value, missing `_:` catch-all
?cond{a;b;c} -- ILO-P009: 3 unlabelled arms, ternary takes exactly 2
?r{Ok:v} -- ILO-P009: result match missing `Err:` arm
```
Pick the shape by intent: boolean -> 2 arms, no labels. Value match -> `val:` labels plus `_:` default. Variant match -> tag names as keys, all variants covered.
## Call vs binary-op: `dx=xj 0-xi`
Whitespace-juxtaposition is the call syntax in ilo. Any bare name followed by another token in an expression parses as a call, not as "name then operator". The classic case:
```ilo
f xi:n xj:n>n;dx=xj 0-xi;dx
```
This parses as `dx=(xj 0) - xi`, a call to `xj` with argument `0`, whose result is subtracted from `xi`. Verification fails with `ILO-T005` because `xj` is a number, not a function. The error message includes the call-vs-binop hint pointing at the canonical fixes.
In ilo's prefix-operator world the agent almost always meant one of:
```ilo
-xj xi -- subtract: prefix `-` means `xj - xi`
nxi=0-xi;+xj nxi -- pre-bind the operand, then add
```
The misparse comes up when an agent reaches for infix arithmetic between a parameter and a subexpression. Pre-binding the operand always resolves the ambiguity, and `ilo --explain ILO-T005` includes the full walkthrough.
## `zip` returns lists, not tuples
ilo has no tuple type. `zip xs ys` returns `L (L n)` - a list of two-element lists. Destructure pairs with `at pair 0` / `at pair 1`, not `pair.0` / `pair.1` when the pair name is unbound.
```ilo
-- DON'T: zs=zip xs ys; +tup.0 tup.1 -- 'tup' never bound; reads as tuple access
-- DO: zs=zip xs ys;
map (pair:L n>n;+at pair 0 at pair 1) zs
```
`ILO-T004` on `tup.0` / `pair.0` carries a hint naming the exact `at ` call. The field-access sugar (`pair.0`) is still valid once `pair` is bound to an `L T` parameter; the diagnostic only fires when the identifier is unbound, which is the agent-frequent shape after a `zip`.
---
# Guards & Control Flow
URL: https://ilo-lang.ai/docs/reference/guards/
Use this when expressing conditional logic without nesting (replaces if/else).
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
Inline (semicolons replace newlines):
```ilo
grd s:n>t;>=s 90 "A";>=s 80 "B";>=s 70 "C";"F"
```
Or the same thing as a file (newlines and indentation):
```ilo
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" -- fallback
```
Each guard checks a condition. If true, the function returns that value. Otherwise, execution continues.
## Why guards?
Compare the Python equivalent:
```python
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
Inline:
```ilo
chk x:n>t;==x 0 "zero";>x 0 "positive";"negative"
```
Or as a file:
```ilo
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" -- fallback
```
## Negated guards
Prefix a guard with `!` to negate the condition:
Inline:
```ilo
f x:n>n;!>x 0 0;x
```
Or as a file:
```ilo
f x:n > n -- number in, number out
!> x 0 0 -- if x not greater than 0, return 0
x -- otherwise return x
```
If x is NOT greater than 0, return 0. Otherwise return x.
```bash
ilo 'f x:n>n;!>x 0 0;x' 5
# → 5
ilo 'f x:n>n;!>x 0 0;x' -3
# → 0
```
Negated braceless guards work with any comparison. Use [`^` (throw)](/docs/reference/error-handling) to return an error instead of a value: `!<=n 0 ^"must be positive"`.
## Braced guards
The return value can be enclosed in braces. Both forms produce identical results:
```ilo
>=sp 1000 "gold" -- braceless
>=sp 1000{"gold"} -- braced (identical result)
```
Use braces when the body has multiple statements:
```ilo
>=sp 1000{a=classify sp;a}
```
## 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"`:
```text
-- braceless guard: early return, fallthrough is the else
f x:n>t;>x 0 "pos";"nonpos"
-- braced conditional: body runs, no early return; explicit ret if needed
f x:n>t;>x 0{ret "pos"};"nonpos"
-- brace ternary: value, no early return, tail expr returns
f x:n>t;>x 0{"pos"}{"nonpos"}
-- bare-bool prefix ternary: cheapest when the bool is already named
f x:n>t;c=>x 0;?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
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
`?` followed by a comparison operator, then the true and false values:
```ilo
f x:n>n;?=x 0 10 20
```
```bash
ilo 'f x:n>n;?=x 0 10 20' f 0
# → 10
ilo 'f x:n>n;?=x 0 10 20' f 5
# → 20
```
The condition must start with a comparison operator (`=`, `>`, `<`, `>=`, `<=`, `!=`). You can assign the result:
```ilo
f x:n>n;v=?=x 0 10 20;+v 1 -- v is 10 or 20, then add 1
```
**Bare-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:
```text
f x:n h:b>n;?h 10 20 -- if h then 10 else 20
g x:n>n;?pos x 1 (-1) -- pos x : b, returns 1 or (-1)
```
The braced form `?h{a}{b}` is also accepted and equivalent.
### Braced ternary
A guard with **two** brace blocks - `{then}{else}`:
```ilo
f x:n>t;=x 1{"yes"}{"no"}
```
```bash
ilo 'f x:n>t;=x 1{"yes"}{"no"}' 1
# → yes
ilo 'f x:n>t;=x 1{"yes"}{"no"}' 2
# → no
```
Supports negation: `!=x 1{"not one"}{"one"}`.
:::note
Ternary syntax is under review - one of these forms may be removed in a future release. See [issue #119](https://github.com/ilo-lang/ilo/issues/119).
:::
## Match expressions
For value matching:
Inline:
```ilo
describe x:t>t;?x{"dog":"woof";"cat":"meow";_:"unknown"}
```
Or as a file:
```ilo
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
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 to `v`
- `^e`: matches the Err variant, binds the error to `e`
Inline:
```ilo
div a:n b:n>R n t;=b 0 ^"divide by zero";~/a b
show a:n b:n>t;r=div a b;?r{~v:str v;^e:e}
```
Or as a file:
```ilo
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 match
```
`div` 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 - parenthesised:
```ilo
show a:n b:n>t;?(div a b){~v:str v;^e:e}
```
```bash
ilo 'div a:n b:n>R n t;=b 0 ^"divide by zero";~/a b
show 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 b
show a:n b:n>t;r=div a b;?r{~v:str v;^e:e}' show 10 0
# → divide by zero
```
See [Error Handling](/docs/reference/error-handling) for more on `~` (Ok), `^` (Err), and the `R` type.
### Matching on type
When the input type is unknown (`_`), match on the runtime type:
Inline:
```ilo
f x:_>t;?x{n v:"number";t v:"text";_:"other"}
```
Or as a file:
```ilo
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 match
```
Each arm specifies a type tag (`n`, `t`, `b`, `l`) followed by a binding variable.
## 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:
```ilo
f x:n>n;>x 0{ret x};0
```
Or as a file:
```ilo
f x:n > n -- number in, number out
> x 0 { ret x } -- if x > 0, return x early
0 -- fallback
```
Guards already provide early return for simple cases. Use `ret` when you need early return inside a loop or deeply nested block:
Inline:
```ilo
f xs:L n>n;@x xs{>=x 10{ret x}};0 -- return first element >= 10
```
Or as a file:
```ilo
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
```
---
# Imports
URL: https://ilo-lang.ai/docs/reference/imports/
Use this when splitting an ilo program across multiple files.
Use this when splitting an ilo program across multiple files.
Split programs across files with `use`:
```ilo
use "math-lib.ilo"
```
All declarations from the imported file merge into a flat namespace - no modules, no qualification.
## Full import
```ilo
-- math-lib.ilo
dbl n:n>n;*n 2
half n:n>n;/n 2
sq n:n>n;*n n
```
```ilo
-- main.ilo
use "math-lib.ilo"
round-trip n:n>n;h=half n;r=dbl h;+r 0
hyp-sq a:n b:n>n;sa=sq a;sb=sq b;+sa sb
```
```bash
ilo main.ilo round-trip 10
# -> 10
ilo main.ilo hyp-sq 3 4
# -> 25
```
## Scoped imports
Import only specific functions with a bracketed list:
```ilo
use "math-lib.ilo" [dbl sq]
```
Now only `dbl` and `sq` are available. `half` is not imported. Requesting a name that does not exist in the file is an error (`ILO-P019`).
## Re-exports
A file can re-export names it imported, making them available to any file that imports it:
```ilo
-- utils.ilo
use "math-lib.ilo" [dbl sq]
pub dbl sq -- re-export these names
```
Any file using `utils.ilo` automatically gets `dbl` and `sq` without needing to import `math-lib.ilo` directly.
## Conditional imports
Import a file only when a condition is met at parse time (useful for platform-specific or feature-gated code):
```ilo
use "web.ilo" if wasm
use "native-http.ilo" if !wasm
```
The `if` condition is evaluated before type-checking; only the matching branch is resolved.
## Lazy loading
## Import rules
- Paths are **relative to the importing file's directory**
- `use` must appear at the **top of the file**, before any function definitions
- Imports are **transitive**: if `a.ilo` uses `b.ilo`, then `main.ilo` using `a.ilo` gets `b.ilo`'s declarations too
- **Circular imports** are detected and reported as an error (`ILO-P018`)
- All imported functions are **type-checked** as part of the full program
- `use` in inline mode (no file context) is an error (`ILO-P017`)
## Multi-file example
```ilo
-- validators.ilo
positive n:n>b;>n 0
in-range n:n lo:n hi:n>b;a=>=n lo;b=<=n hi;?a{true:b;false:false}
```
```ilo
-- app.ilo
use "validators.ilo" [positive in-range]
check-age age:n>t;?{positive age:?(in-range age 0 150){true:"valid";false:"out of range"};false:"negative"}
```
---
# Language Reference
URL: https://ilo-lang.ai/docs/reference/language/
Use this when you want the authoritative ilo language spec or an index of every reference page.
Use this when you want the authoritative ilo language spec or an index of every reference page.
The complete language specification is maintained in [SPEC.md](https://github.com/ilo-lang/ilo/blob/main/SPEC.md) on GitHub. For the agent-optimised compact spec, run `ilo help ai`.
## Reference index
### Core syntax
- [Primitives](/docs/reference/primitives/) - type sigils (`n`, `t`, `b`, `_`, `L`, `M`, `R`, `O`, `S`)
- [Types & Functions](/docs/reference/types-and-functions/) - function declarations and signatures
- [Prefix Notation](/docs/reference/prefix-notation/) - operator syntax
- [Guards & Control Flow](/docs/reference/guards/) - flat conditional returns
- [Loops](/docs/reference/loops/) - foreach, ranges, while, `brk`, `cnt`
- [Error Handling](/docs/reference/error-handling/) - Result, `!`, `?`, `??`
- [Pipes](/docs/reference/pipes/) - left-to-right chaining
- [Imports](/docs/reference/imports/) - splitting code across files
- [Memory Model](/docs/reference/memory-model/) - ownership and copying
### 0.13.0 additions
- [Types & Functions - Sum types](/docs/reference/types-and-functions/#sum-types-discriminated-unions) - `S{ok:T; err:E}`, generics, recursive, exhaustive match
- [Types & Functions - Generics](/docs/reference/types-and-functions/#generics) - bounded and multi-bound type variables
- [Types & Functions - Brace-lambda](/docs/reference/types-and-functions/#brace-lambda-multi-statement-body) - multi-statement lambda bodies
- [Types & Functions](/docs/reference/types-and-functions/#generics) - generics, brace-lambdas, `panic`, match alternatives
- [Imports - Modules](/docs/reference/imports/#re-exports) - re-exports, conditional imports, lazy loading
- [Error Handling - defer](/docs/reference/error-handling/#defer-and-errdefer) - `defer` / `errdefer`
- [Loops - par-map](/docs/reference/loops/#parallel-map-par-map) - parallel map with chunking and native VM opcode
- [Primitives - Integer types](/docs/reference/primitives/) - `u32`, `u64`, `i64`
- [Numbers - Bitwise ops](/docs/builtins/numbers/#bitwise-operators) - `band`/`bor`/`bxor`/`bnot`/`shl`/`shr` + 64-bit variants
### Tooling
- [CLI Reference](/docs/reference/cli/) - every flag and subcommand
- [REPL](/docs/reference/repl/) - interactive sessions
- [Engines](/docs/reference/engines/) - VM, JIT, and AOT contracts
- [Benchmarks](/docs/reference/benchmarks/) - performance numbers per engine
### Errors and edge cases
- [Diagnostics](/docs/reference/diagnostics/) - error codes, fixSafety taxonomy, repair plans
- [Gotchas](/docs/reference/gotchas/) - common traps
- [Syntax Experiments](/docs/reference/syntax-experiments/) - proposals and unstable surface
See also the [10-lesson tutorial](https://github.com/ilo-lang/ilo/wiki) on the GitHub wiki.
---
# Loops
URL: https://ilo-lang.ai/docs/reference/loops/
Use this when iterating with foreach, range, while, or controlling flow with `brk` and `cnt`.
Use this when iterating with foreach, range, while, or controlling flow with `brk` and `cnt`.
ilo has three loop constructs: **foreach** (`@`), **range** (`@..`), and **while** (`wh`). All return the last iteration's body value (`nil` if the loop never executes).
## Foreach: `@binding list{body}`
Iterates over each element in a list:
Inline:
```ilo
sq-last xs:L n>n;@x xs{*x x}
```
Or as a file:
```ilo
sq-last xs:L n > n -- list of numbers in, number out
@x xs { -- for each x in xs
* x x -- square x
} -- returns last squared value
```
```bash
ilo 'sq-last xs:L n>n;@x xs{*x x}' 3,4,5
# → 25 (last element 5, squared)
```
Variables from the outer scope can be updated inside the loop body:
```ilo
total xs:L n > n -- sum a list manually
s = 0 -- accumulator
@x xs { -- for each x in xs
s = + s x -- add x to s
}
s -- return the sum
```
```bash
ilo 'total xs:L n>n;s=0;@x xs{s=+s x};s' 1,2,3
# → 6
```
The loop binding (`x`) is fresh each iteration. Outer variables (`s`) persist across iterations.
## Range: `@binding start..end{body}`
ilo's equivalent of a traditional `for` loop. Iterates from `start` (inclusive) to `end` (exclusive):
Inline:
```ilo
f>n;s=0;@i 0..5{s=+s i};s
```
Or as a file:
```ilo
f > n -- no params, returns number
s = 0 -- accumulator
@i 0..5 { -- i goes 0, 1, 2, 3, 4
s = + s i -- add i to s
}
s -- → 10
```
```bash
ilo 'f>n;s=0;@i 0..5{s=+s i};s' f
# → 10 (0+1+2+3+4)
```
Building a list with a range:
```bash
ilo 'f>L n;xs=[];@i 0..3{xs=+=xs i};xs' f
# → [0, 1, 2]
```
Both bounds can be variables or expressions:
```bash
ilo 'f n:n>n;s=0;@i 0..n{s=+s i};s' 4
# → 6 (0+1+2+3)
```
If `start >= end`, the loop never executes.
## While: `wh condition{body}`
Loops while the condition is truthy:
Inline:
```ilo
f>n;i=0;s=0;wh n -- no params, returns number
i = 0 -- counter
s = 0 -- accumulator
wh < i 5 { -- while i < 5
i = + i 1 -- increment i
s = + s i -- add i to s
}
s -- → 15
```
```bash
ilo 'f>n;i=0;s=0;wh = 60:
count += 1
return count
```
In ilo, [guards](/docs/reference/guards) inside loops keep the body flat. No indentation creep:
```ilo
count-passing scores:L n > n
c = 0
@s scores {
= s 100 { ret + c 1 } -- perfect score, return early
>= s 60 { c = + c 1 } -- passing score, increment
}
c
```
Guards return or continue without nesting. Add more conditions and the indentation stays the same: each guard is an independent flat check.
## Break and continue
`brk` exits the enclosing loop. `cnt` skips to the next iteration:
```ilo
f > n
i = 0
wh true { -- infinite loop
i = + i 1
>= i 3 { brk } -- break when i reaches 3
}
i -- → 3
```
```bash
ilo 'f>n;i=0;wh true{i=+i 1;>=i 3{brk}};i' f
# → 3
```
Continue example, skip iterations where `i >= 3`:
```bash
ilo 'f>n;i=0;s=0;wh =i 3{cnt};s=+s i};s' f
# → 3 (1+2, skips 3, 4, 5)
```
Both `brk` and `cnt` work inside guards within loops. `brk expr` accepts an optional value (currently discarded; the loop returns the last body value before the break).
## Early return from loops
Use `ret` to return from the enclosing function inside a loop:
Inline:
```ilo
first-big xs:L n>n;@x xs{>=x 10{ret x}};0
```
Or as a file:
```ilo
first-big xs:L n > n -- find first element >= 10
@x xs { -- loop over list
>= x 10 { ret x } -- if x >= 10, return it
}
0 -- fallback if none found
```
```bash
ilo 'first-big xs:L n>n;@x xs{>=x 10{ret x}};0' 3,7,12,5
# → 12
```
## Choosing the right loop
| | Foreach `@` | Range `@..` | While `wh` |
|--|-------------|-------------|------------|
| **Syntax** | `@x xs{…}` | `@i a..b{…}` | `wh cond{…}` |
| **Use when** | Iterating a list | Counting over numbers | Custom stop condition |
| **Fresh binding** | Yes | Yes | No |
For simple list transforms, prefer [`map`](/docs/builtins/collections#higher-order-functions), [`flt`](/docs/builtins/collections#higher-order-functions), and [`fld`](/docs/builtins/collections#higher-order-functions). Loops are for when you need mutable state or early exit.
## Parallel map: `par-map`
`par-map` is a parallel version of `map` that distributes work across threads. It is order-preserving, and **per-item errors surface as `Err` inside the result list** - so the return type is `L (R T t)` where `T` is the mapped function's return type, not a bare `L T`:
```ilo
double x:n>n;*x 2
main>L (R n t);par-map double (range 0 1000)
```
Default concurrency is the CPU count; override globally with the `ILO_PAR_MAP_CONCURRENCY` environment variable, or per-call with a third argument (`n=0` falls back to the CPU count):
```ilo
par-map double xs 4 -- at most 4 concurrent workers
```
`par-map` is safe only for pure functions with no shared mutable state. The verifier does not enforce this automatically; use effect sets (see `World` capability) to annotate and restrict side-effectful code.
## Tail-call optimisation
ilo deliberately has no `loop` keyword - every iteration shape that can't be written with `@` foreach should be expressed as a tail-recursive function. To make that safe, the runtime guarantees that **tail calls do not consume host-stack frames**: a function that recurses only in tail position can run to arbitrary depth, because the runtime rebinds parameters in place rather than pushing a frame.
```ilo
-- Count down from n to 0. Runs at any depth.
count-down n:n>n;=n 0 0;count-down -n 1
-- Tail-recursive sum. `acc` carries the partial result.
sum-acc xs:L n acc:n>n;empty=len xs;=empty 0 acc;sum-acc tl xs +acc hd xs
```
A call is in **tail position** when its return value is the function's return value:
- the last statement of a function body,
- the expression of a `ret` statement,
- an arm of a `?` match that is itself in tail position,
- the body of a braceless guard.
Calls inside `@` foreach, `@..` range, or `wh` loops are **not** in tail position - the loop header runs after each iteration, so the call's return doesn't become the function's return. Operands of further computation (e.g. `*n fac -n 1`) are also not in tail position.
The peephole only fires when the callee is a direct user-defined function name (not a `FnRef` in scope, not a closure, not a builtin, not a tool) and the call has no auto-unwrap (`!` / `!!`). These constraints cover the common recursive-accumulator and state-machine shapes.
Tail-call optimisation is supported across all engines. The bytecode VM (`--vm`) emits `OP_TAILCALL` and reuses the current call frame; the Cranelift JIT and AOT backends lower tail calls to `return_call` (shipped in 0.13.0, ILO-15 + ILO-45). The tree-walker interpreter has been removed from the public CLI. All three backends guarantee bounded stack depth for tail-recursive programs.
---
# Memory Model
URL: https://ilo-lang.ai/docs/reference/memory-model/
Use this when reasoning about ownership, copying, and how ilo manages memory compared to Rust, Zero, and Go.
Use this when reasoning about ownership, copying, and how ilo manages memory compared to Rust, Zero, and Go.
## Short version
ilo manages memory with reference counting. You write no memory annotations. The compiler and runtime handle it. Future versions add compile-time optimisations (escape analysis, in-place mutation) to reduce overhead, still without anything appearing in your source.
## What you write
```ilo
sum xs:L n>n;total=0;@x xs{total=+total x};total
```
No `&`, no `ref`, no `let mut`, no lifetime parameters. Memory is the runtime's problem.
## Four approaches in the wild
| Language | Where memory rules live | User writes |
|----------|------------------------|-------------|
| **Rust** | In the source | `&value`, `&mut value`, `'a` lifetimes, ownership |
| **Zero** | In the source (lighter) | `ref`, `mutref`, `owned` |
| **Go** | In the compiler | Nothing. Escape analysis at compile time |
| **ilo** | In the runtime (and future compiler) | Nothing. RC at runtime |
Each row is a deliberate choice with real costs and real benefits. Rust's borrow checker proves the absence of whole bug classes at compile time, with zero runtime overhead, at the cost of source-level annotations and a steep learning curve. Zero takes a lighter version of the same idea, asking for `ref` / `mutref` / `owned` where Rust asks for lifetimes. Go puts the work in the compiler: escape analysis decides what goes on the stack, and a tracing garbage collector handles the rest. ilo sits closest to Go in spirit, but uses reference counting instead of a tracing collector, and pushes more optimisation work onto the compiler over time.
## Why ilo chose this
**Principle 1: Token-conservative.** Every memory annotation an agent has to write is tokens spent. `&mut`, `ref`, lifetime parameters: all of these show up in source, and an agent pays for them on every generation. ilo's goal is the smallest source that gets the job done, so memory annotations sit outside the source entirely.
**Principle 4: Language-agnostic.** Borrow checking is a learned skill. Agents that have not been trained on Rust often produce code that fails the borrow checker on the first try, then retry, then retry again. Each retry is more tokens. By keeping memory rules out of the source, ilo removes a whole category of compile errors that an untrained agent would otherwise generate.
**Principle 6: Verified before execution.** ilo still wants strong guarantees, just not via the borrow checker. The verifier proves that types align, calls resolve, results are handled, and side effects are explicit. Memory safety comes from a different mechanism (RC plus a cycle detector) rather than from source-level proofs. The end-user property is the same: no use-after-free, no data races, no null dereferences.
## What this means in practice
- No use-after-free. RC keeps live values alive.
- No data races. ilo has no mutable shared state across threads in user code.
- No null dereferences. `O` and `R` must be matched before use.
- Bounds-checked list access.
- Cycles handled by ilo's cycle detector.
- Some runtime overhead vs borrow-checked languages. Typical figure is 10 to 20 percent on pointer-heavy code, less on arithmetic or string-heavy code. Real numbers depend on the workload and the engine (tree-walking, bytecode VM, or JIT).
## What's coming
Compile-time optimisation is on the roadmap. The plan is escape analysis plus automatic linearity inference: the compiler proves which values don't need RC, and skips the increment/decrement work for them. Invisible to your source, real runtime speedup.
In-place mutation for uniquely-owned values is the same idea applied to collections. If the compiler can prove a list has exactly one owner, an append doesn't need to copy. None of this surfaces as syntax. The contract with the agent stays: write the program, the runtime handles memory.
Timing depends on contributor capacity. See the public roadmap for current state.
## When ilo isn't the right choice
If you need:
- Sub-10KB native binaries
- Compile-time-proven absence of memory cycles
- Zero runtime memory overhead
- Direct control over memory layout
then ilo isn't the right tool for that part of your stack. Look at **Zero** for small native binaries with capability-checked safety, or **Rust** for full compile-time memory proofs and tight control over allocation. ilo composes well with both: an ilo program can shell out to a native binary written in either, and a host process in Rust or Zero can drive ilo as an embedded engine.
The honest framing: ilo trades a small amount of runtime cost for a much smaller surface area in the source. That tradeoff is correct for AI agents generating code in a loop. It is not correct for every workload.
## Further reading
- [Manifesto](/docs/learn/manifesto/): why ilo makes the design choices it does
- [Built-in Functions](/docs/builtins/numbers/): the closed-world API surface
- [Specification](/docs/reference/language/): the full language reference
---
# Pipes
URL: https://ilo-lang.ai/docs/reference/pipes/
Use this when chaining transforms left-to-right with the pipe operator.
Use this when chaining transforms left-to-right with the pipe operator.
Inspired by Unix shell pipes (`|`), the pipe operator `>>` chains function calls left-to-right:
```ilo
dbl x:n>n;*x 2 -- double a number
inc x:n>n;+x 1 -- add 1
transform x:n>n;(x>>dbl>>inc) -- double, then add 1
```
`x>>dbl>>inc` means: take `x`, pass to `dbl`, pass result to `inc`.
## Without pipes
```ilo
transform x:n>n;inc(dbl x)
```
## With pipes
```ilo
transform x:n>n;(x>>dbl>>inc)
```
Pipes read left-to-right, matching the data flow direction.
## Composition patterns
Pipes shine when you compose small, reusable functions into larger transforms.
### Chaining numeric transforms
```ilo
dbl x:n>n;*x 2 -- double a number
inc x:n>n;+x 1 -- add 1
sq x:n>n;*x x -- square a number
transform x:n>n;(x>>dbl>>inc>>sq) -- double, add 1, then square
```
```bash
ilo 'dbl x:n>n;*x 2 inc x:n>n;+x 1 sq x:n>n;*x x transform x:n>n;(x>>dbl>>inc>>sq)' transform 3
# → 49 (3 → 6 → 7 → 49)
```
### Chaining list operations
Pipes work naturally with list higher-order functions:
- `map fn list` applies `fn` to every element
- `flt fn list` keeps elements where `fn` returns true (filter)
- `fld fn init list` reduces a list to a single value (fold)
```ilo
sq x:n>n;*x x -- square a number
pos x:n>b;>x 0 -- is positive?
main xs:L n>L n;xs >> flt pos >> map sq -- filter positives, square each
```
Read it left to right: take `xs`, keep only positives, square each.
```bash
ilo 'sq x:n>n;*x x pos x:n>b;>x 0 main xs:L n>L n;xs >> flt pos >> map sq' main -3,-1,0,2,4
# → [4, 16] (-3,-1,0 filtered out; 2→4, 4→16)
```
### Eliminating intermediate variables
Without pipes, you need intermediate bindings:
```ilo
transform x:n>n;a=dbl x;b=inc a;sq b
```
With pipes, the same logic is a single expression:
```ilo
transform x:n>n;(x>>dbl>>inc>>sq)
```
### Pipes with auto-unwrap
Pipes combine with `!` for functions that return `R` (Result) types:
```ilo
f x:n>R n t;r=x>>g!>>h;~r
```
This desugars to `h(g!(f(x)))` - if `g` returns an error, it propagates immediately.
### Parentheses in multi-function files
In a file with multiple functions, wrap pipe chains in parentheses for non-last functions. This prevents the parser from consuming the next function's name:
```ilo
dbl-inc x:n>n;(x>>dbl>>inc) -- parens needed (not the last function)
inc-sq x:n>n;x>>inc>>sq -- last function, no parens needed
```
## Result-aware short-circuit
When the left operand of `>>` returns a `R T E` (Result) or `O T` (Optional), the pipe
auto-unwraps: `Ok(v)` passes `v` to the next stage; `Err(e)` short-circuits and propagates
`^e` out of the enclosing function via early-return. Non-Result values pass through unchanged.
This means you can chain Result-returning functions without explicit `!` on each stage:
```ilo
fetch url>>jpar>>jpth "name"
```
If `fetch` returns `^e`, both `jpar` and `jpth` are skipped, and `^e` propagates.
If `fetch` returns `~v`, `v` is unwrapped and passed to `jpar`.
The enclosing function must return `R` (or `O`) for the short-circuit to type-check,
same as `!`. Non-Result pipes (plain functions returning `n`, `t`, `L`, etc.) are
unaffected — no unwrap, no error propagation.
### How it works
The parser injects `UnwrapMode::PipePropagate` on intermediate pipe-desugared calls.
The verifier treats this leniently — non-Result returns pass through without `ILO-T025`.
The runtime checks the value's tag at each stage (`ISOK`/`ISERR`) so plain numbers,
text, and lists skip the unwrap entirely.
### Before and after
Before (manual unwrap per stage):
```ilo
fn process url:t>R t t
;r=get! url
;j=jpar! r
;n=jpth! j "name"
;n
```
After (Result-aware pipe, single line):
```ilo
fn process url:t>R t t
;url>>get>>jpar>>jpth "name"
```
Both are equivalent. The pipe form is ~60% fewer tokens and eliminates the class of
errors where an agent forgets a `!` on one of the intermediate stages.
---
# Prefix Notation
URL: https://ilo-lang.ai/docs/reference/prefix-notation/
Use this when writing arithmetic, comparisons, or any operator expression in ilo.
Use this when writing arithmetic, comparisons, or any operator expression in ilo.
Prefix notation is ilo's core token-saving device. Operators come before their operands.
## Basic examples
| Infix (traditional) | Prefix (ilo) | Chars saved |
|---|---|---|
| `(a * b) + c` | `+*a b c` | 4 |
| `((a + b) * c) >= 100` | `>=*+a b c 100` | 7 |
| `a > 0` | `>a 0` | 0 |
## Why prefix wins
Across 25 expression patterns: **22% fewer tokens, 42% fewer characters** vs infix.
Prefix eliminates:
- Parentheses for grouping
- Operator precedence ambiguity
- Nesting depth
```text
(a + b) * c -- Infix: 5 tokens, 13 chars
*+a b c -- Prefix: 4 tokens, 7 chars
```
## Reading prefix
Read inside-out, left-to-right:
| Expression | Meaning |
|------------|---------|
| `+a b` | add a and b |
| `* _ c` | multiply result and c |
## Infix also works
ilo supports infix too:
```text
a + b -- works
x * y + 1 -- works
```
:::note
ilo emits a hint to stderr (or in the JSON `hints` array) suggesting the prefix form. Suppress with `--no-hints` / `-nh`.
:::
## Complete operator table
### Binary operators
| Prefix | Infix | Meaning | Types |
|--------|-------|---------|-------|
| `+a b` | `a + b` | add / concat / list concat | `n`, `t`, `L` |
| `+=a v` | `a += v` | append to list | `L` |
| `-a b` | `a - b` | subtract | `n` |
| `*a b` | `a * b` | multiply | `n` |
| `/a b` | `a / b` | divide | `n` |
| `=a b` | `a = b` | equal | any |
| `!=a b` | `a != b` | not equal | any |
| `>a b` | `a > b` | greater than | `n`, `t` |
| `=a b` | `a >= b` | greater or equal | `n`, `t` |
| `<=a b` | `a <= b` | less or equal | `n`, `t` |
| `&a b` | `a & b` | logical AND (short-circuit) | any (truthy) |
| `\|a b` | `a \| b` | logical OR (short-circuit) | any (truthy) |
:::note
Both `=` and `==` currently mean equality in prefix position. This syntax is under review. See [issue #120](https://github.com/ilo-lang/ilo/issues/120).
:::
### Unary operators
| Op | Meaning | Types |
|----|---------|-------|
| `-x` | negate | `n` |
| `!x` | logical NOT | any (truthy) |
### Special infix
| Op | Meaning |
|----|---------|
| `a??b` | nil-coalesce (if a is nil, return b) |
| `a>>f` | pipe (desugars to `f(a)`) |
## Infix precedence
When using infix, standard mathematical precedence applies (higher binds tighter):
| Level | Operators |
|-------|-----------|
| 6 | `*` `/` |
| 5 | `+` `-` |
| 4 | `>` `<` `>=` `<=` |
| 3 | `=` `!=` |
| 2 | `&` |
| 1 | `\|` |
Function application binds tighter than all infix operators:
```text
f a + b -- means (f a) + b, NOT f(a + b)
```
## Token and character savings by pattern
Measured across 25 expression patterns using the `cl100k_base` tokenizer (used by Claude):
| Pattern | Infix | Prefix | Char savings |
|---------|-------|--------|-------------|
| Binary op | `a + b` | `+a b` | 1 char |
| Nested (2 deep) | `(a * b) + c` | `+*a b c` | 4 chars |
| Nested (3 deep) | `((a + b) * c) >= 100` | `>=*+a b c 100` | 6 chars |
| Chained comparison | `x >= 0 and x <= 100` | `&>=x 0 <=x 100` | 4 chars |
| Negation | `not (a == b)` | `!=a b` | 8 chars |
The key insight: each nested prefix operator saves 2 characters (no parentheses needed). Flat binary expressions save 1 character. The deeper the nesting, the bigger the savings.
Overall: **22% fewer tokens, 42% fewer characters** vs infix.
## More nesting examples
Prefix operators nest by each operator greedily consuming the next two operands. Those operands can themselves be operator expressions:
```text
-- Two levels: (a * b) + c
+*a b c
-- Two levels, right-heavy: a * (b + c)
*a +b c
-- Three levels: ((a + b) * c) >= 100
>=*+a b c 100
-- Symmetric: (a * b) - (c * d)
-*a b *c d
-- Four levels: (((a + b) * c) - d) / e
/-*+a b c d e
-- Mixed comparisons: (x + y) >= (a * b)
>=+x y *a b
```
### Reading deeply nested prefix
Read left-to-right. Each operator grabs the next two values (which may themselves be operator expressions):
`/-*+a b c d e`
| Step | Expression | Meaning |
|------|-----------|---------|
| 1 | `+a b` | add a and b |
| 2 | `* _ c` | multiply result and c |
| 3 | `- _ d` | subtract d from result |
| 4 | `/ _ e` | divide result by e |
This is equivalent to `(((a + b) * c) - d) / e` in infix - note how 4 pairs of parentheses disappear entirely.
### Same-precedence prefix-pair trap
The outer prefix op binds the inner prefix subexpression as its **left** operand, regardless of operator precedence. With two same-precedence ops side by side this disagrees with the natural left-to-right reading:
```text
*/a b c -- (a/b) * c ← NOT (a*b)/c
/*a b c -- (a*b) / c ← NOT (a/b)*c
+-a b c -- (a-b) + c ← NOT (a+b)-c
-+a b c -- (a+b) - c ← NOT (a-b)+c
```
The ilo runtime emits a `hint:` diagnostic on these four shapes after a successful run. To force the other grouping, either swap the operator pair or bind the inner result first:
```ilo
-- Want (a*b)/c with a=6, b=2, c=3:
r=*a b;/r c -- bind, then divide → 4
/*a b c -- equivalent, swapping the prefix-pair order
```
Different-precedence pairs like `+*a b c` (= `(a*b)+c`) and same-op repeats like `++a b c` (= `(a+b)+c`) match the left-to-right reading naturally and don't fire the hint.
### Operand rules
Operator operands must be atoms (literals, variable references, field access) or nested prefix operators. Function calls are generally not valid operands - bind their results first:
```ilo
-- WRONG: *n fac -n 1 (fac is treated as an atom, not a call)
-- RIGHT: r=fac -n 1;*n r (bind the call result, then use it)
```
**Exception: known-arity idents expand inline.** When a binop operand is an identifier whose arity is known (a defined function or a fixed-arity builtin), the parser expands it as a call by consuming the right number of following atoms - mirroring the existing `??` behaviour:
```ilo
sq x:n>n;*x x main xs:L n>n;+sum xs sq hd xs -- = +(sum xs) (sq (hd xs))
```
This keeps simple compositions paren-free without forcing a bind-first rewrite. For anything more complex, bind the call result and use the variable.
---
# Primitives
URL: https://ilo-lang.ai/docs/reference/primitives/
Use this when you need to look up an ilo type sigil or scalar/literal form.
Use this when you need to look up an ilo type sigil or scalar/literal form.
## Scalar types
| Sigil | Meaning | Literal example |
|-------|---------|-----------------|
| `n` | number (f64) | `42`, `3.14`, `-7` |
| `u32` | unsigned 32-bit integer | `42u`, `0u` |
| `u64` | unsigned 64-bit integer | `42U`, `0U` |
| `i64` | signed 64-bit integer | `42L`, `-7L` |
| `t` | text (string) | `"hello"`, `"line\n"` |
| `b` | bool | `true`, `false` |
| `_` | nil / wildcard | `nil` |
`u32`/`u64`/`i64` are native integer types with wrapping semantics and dedicated bitwise operators. Use them when you need exact integer widths (crypto, binary protocols, WASM interop).
## Container types
| Sigil | Meaning | Example |
|-------|---------|---------|
| `L T` | list of `T` | `L n`, `L t`, `L _` |
| `M K V` | map from `K` to `V` | `M t n` |
| `R T E` | result, ok=`T`, err=`E` | `R n t` |
| `O T` | optional `T` | `O n` |
| `S a b c` | sum type / discriminated union | `S red green blue` |
| `F A R` | function type | `F n t` (takes `n`, returns `t`) |
## Result and option literals
```ilo
ok=~42 -- R _ _ with ok payload 42
bad=^"oh no" -- R _ t with err payload
some=?42 -- O n with value
none=? -- O _ empty
```
## Comments
```ilo
-- single-line comment
```
There are no multi-line comments. The double-hyphen runs to end of line.
## Identifiers
- Lowercase only. `myVar` is an error (`ILO-L003`).
- Hyphens are allowed inside identifiers. Underscores are not (`ILO-L002`).
- Short by convention: 1 to 3 chars for locals, 2 to 4 for functions.
See [Types & Functions](/docs/reference/types-and-functions/) for how primitives compose into function signatures.
---
# The REPL
URL: https://ilo-lang.ai/docs/reference/repl/
Use this when running an interactive ilo session for experimenting or prototyping.
Use this when running an interactive ilo session for experimenting or prototyping.
## Start a session
```bash
ilo repl
```
You get an interactive prompt where you can define functions and evaluate expressions:
```ilo
ilo> dbl x:n>n;*x 2
ilo> dbl 5
10
ilo> inc x:n>n;+x 1
ilo> inc 10
11
```
Functions, types, and aliases persist across lines. Build up a program incrementally.
## Multi-line input
The REPL supports multi-line input for type definitions and functions. Continuation is triggered by:
- **Unclosed braces** - type definitions spanning multiple lines
- **Trailing `;`** - signals more statements to come
```
> type point{
.. x:n
.. y:n
.. }
defined type: point{x:n;y:n}
> cls sp:n>t;
.. >=sp 1000 "gold";
.. >=sp 500 "silver";
.. "bronze"
defined: cls(sp:n) -> t
```
A blank line or balanced braces submit the input.
## Pipes in the REPL
Chain functions you've already defined:
```ilo
ilo> dbl x:n>n;*x 2
ilo> inc x:n>n;+x 1
ilo> 5 >> dbl >> inc
11
```
## Commands
| Command | What it does |
|---------|-------------|
| `:defs` | Show all definitions (functions, types, aliases) |
| `:w file.ilo` | Save definitions to a file |
| `:clear` | Start fresh |
| `:help` | Show help |
| `:q` | Quit |
## JSON mode
For agent integration, use `-j` to get structured JSON output:
```bash
ilo repl -j
```
Success:
```json
{"ok": 10}
```
Error:
```json
{"code": "ILO-T004", "message": "undefined variable 'y'", "severity": "error", "suggestion": "did you mean 'x'?"}
```
The `-j` flag also works outside the REPL: `ilo 'dbl x:n>n;*x 2' -j 5`
## Tips
- Define helper functions first, then compose them
- Use `:defs` to remind yourself what's in scope
- Use `:w` to save a working session to a file when you're happy with it
---
# The Server
URL: https://ilo-lang.ai/docs/reference/serv/
Long-lived JSON request/response loop for agent integration. Use this when an agent or tool wants to compile, check, run, or trace ilo programs without spawning a new process per request.
Long-lived JSON request/response loop for agent integration. Use this when an agent or tool wants to compile, check, run, or trace ilo programs without spawning a new process per request.
Distinct from `ilo httpd`, which serves user-facing HTTP traffic on a port you choose. `ilo serv` speaks an internal JSON-RPC dialect on stdin/stdout, intended for tooling.
## Start the server
```bash
ilo serv
```
The process reads newline-delimited JSON requests from stdin and writes newline-delimited JSON responses to stdout. It keeps running until stdin closes or the host kills it.
## Request shape
Each request is one JSON object on a single line. Methods mirror the CLI subcommands:
```json
{"method":"check","source":"main>_;prnt +2 2"}
{"method":"run","source":"main>n;fib 10","func":"fib","args":[10]}
{"method":"trace","source":"...","func":"...","args":[...]}
{"method":"ast","source":"..."}
{"method":"graph","source":"..."}
```
`source` is the program text. Optional fields mirror the CLI flags.
## Response shape
Each response is one JSON object with a stable `schemaVersion`:
```json
{"schemaVersion":1,"ok":true,"result":{...}}
{"schemaVersion":1,"ok":false,"diagnostics":[{"code":"ILO-P003","message":"...","span":{...},"fixPlans":[...]}]}
```
Diagnostics carry typed error codes, source spans, and `FixSafety`-classified fix plans (per ILO-360). No screen-scraping required.
## Safety
`ilo serv` accepts untrusted source. The AST depth cap (`ILO-P103`, default 256) and the runtime / output guards (`ILO-R016`, `ILO-R017`) apply per request. Cap overrides:
```bash
ilo --max-ast-depth 512 serv
ilo --max-runtime 30 --max-output-bytes 10000000 serv
```
Capability flags (`--allow-net`, `--allow-read`, `--allow-write`, `--allow-run`) pass through to `run` requests.
## When to use it
| Use case | Reach for |
|---|---|
| Interactive prototyping by a human | `ilo repl` |
| HTTP traffic from a browser / client | `ilo httpd` |
| Agent driving the compiler/runtime | `ilo serv` |
| One-shot CLI invocations | `ilo check` / `ilo run` / `ilo build` |
The per-process startup overhead matters at scale - a tool that compiles thousands of snippets per minute should run `ilo serv` once and stream requests, not fork `ilo check` per snippet.
## Protocol stability
`schemaVersion` on every response. Schema changes bump the version. Older clients can detect and either adapt or refuse to parse. The expectation per Manifesto P6 (Structured Compiler-to-Agent Surface) is that this contract is stable enough for agents to depend on across releases.
## Related
- `ilo httpd` - user-facing HTTP server backed by a handler function
- `ilo apply` - apply a typed fix plan to source (often consumed from `ilo check` output)
- `ilo trace` - line-of-execution trace mode, useful when debugging an agent's program
---
# Syntax Experiments
URL: https://ilo-lang.ai/docs/reference/syntax-experiments/
Use this when researching syntax design decisions; nine variants benchmarked against Claude Haiku.
Use this when researching syntax design decisions; nine variants benchmarked against Claude Haiku.
Nine syntax variants for the same language, tested against Claude Haiku. Most initial assumptions about what would save tokens were wrong.
## Setup
Each variant implements the same five programs: a simple function, one with dependencies, a data transform, a tool interaction with error handling, and a multi-step workflow. Measured with cl100k_base token counts, then tested against Claude Haiku with zero prior knowledge - generating correct code from just a spec and examples.
## The variants
The same function across three variants:
**idea1** - readable, Haskell-ish:
```
total price:number quantity:number rate:number -> number
subtotal = multiply price quantity
tax = multiply subtotal rate
add subtotal tax
```
**idea7** - dense wire format:
```ilo
tot p:n q:n r:n>n;s=*p q;t=*s r;+s t
```
**idea9** - ultra-dense with short names (the winner):
```ilo
tot p:n q:n r:n>n;s=*p q;t=*s r;+s t
```
## Results
```
Idea Tokens vs Py Chars vs Py Score
--------------------------------------------------------------------
python-baseline 871 1.00x 3635 1.00x -
idea1 921 1.06x 3108 0.86x 10.0
idea1-compact 677 0.78x 2564 0.71x 10.0
idea2-tool-calling 983 1.13x 3203 0.88x 10.0
idea3-constrained-decoding 598 0.69x 2187 0.60x 10.0
idea4-ast-bytecode 584 0.67x 1190 0.33x 9.8
idea5-workflow-dag 710 0.82x 2603 0.72x 10.0
idea6-mcp-composition 956 1.10x 2978 0.82x 9.5
idea7-dense-wire 351 0.40x 1292 0.36x 10.0
idea8-ultra-dense 285 0.33x 901 0.25x 10.0
idea9-ultra-dense-short 287 0.33x 787 0.22x 10.0
```
idea9 uses 0.33x the tokens and 0.22x the characters of Python, with 10/10 generation accuracy. idea4 (a bytecode-like format) was the only one that dipped below 10.
## Surprises
**Positional arguments are the biggest win.** Going from `charge(pid:pid, amt:amt)` to just `charge pid amt` eliminates parens, colons, and repeated parameter names. The single largest token reduction across all variants. The concern about parameter-swap errors was completely unfounded - 10/10 accuracy across all task types.
**Short variable names don't save tokens.** `order` and `ord` are both single tokens in cl100k_base. The tokenizer already handles common English words efficiently. Abbreviating only saves characters, not tokens. That's why idea8 (285 tokens) and idea9 (287 tokens) are nearly identical despite idea9 being 114 characters shorter.
**Sigils beat keywords, but not by as much as you'd think.** Replacing `match` with `?` and `for` with `@` saves characters more than tokens. The real win is disambiguation - a sigil can never be confused with a variable name, which reduces generation errors.
**Spec quality trumps syntax cleverness.** Going from 8/10 to 10/10 generation accuracy just by adding better operator examples to the spec. The spec is part of the prompt. If it's ambiguous, the LLM will struggle no matter how clean your syntax is.
**The bytecode format was a trap.** idea4 (integer-ID based AST) had great character efficiency (0.33x) but was the only variant that dropped below 10/10 accuracy. Making things machine-optimal at the character level can make them harder for LLMs to work with. The model needs *some* semantic signal.
## Cold-LLM test
The real validation: give Haiku the spec and examples, then ask it to write completely new programs in unfamiliar task domains. Not reformatting - actually understanding the language and producing novel code.
```
Per-task breakdown (Full test, /10):
Idea workflow data_pipe decision api_orch
--------------------------------------------------------------------
idea7-dense-wire 10.0 10.0 10.0 10.0
idea8-ultra-dense 10.0 10.0 10.0 10.0
idea9-ultra-dense-short 10.0 10.0 10.0 10.0
idea4-ast-bytecode 10.0 10.0 9.0 10.0
idea6-mcp-composition 9.0 10.0 9.0 10.0
```
The dense formats weren't just parseable, they were *learnable*.
## Takeaway
idea1 through idea3 were designed by intuition, without proper benchmarks. The comparison harness made the difference. Measure first, design second.
---
# Shadow Test Blocks
URL: https://ilo-lang.ai/docs/reference/tests/
Compile-time self-verification for ilo functions. Assert expected return values before running.
Functions may carry inline `test` blocks that document and verify expected
behaviour at `ilo check` time. Test blocks extend ilo's verify-before-run
philosophy from type-checking to behaviour-checking.
## Syntax
```ilo
fn add a:n b:n>n;+a b
test add { ok add 2 3 5; ok add 0 0 0; ok add -1 1 0 }
fn div a:n b:n>R n t;=b 0 ^"divide by zero";~/a b
test div { ok div 10 2 5; err div 10 0 "divide by zero" }
```
| Element | Form |
|---------|------|
| Block header | `test {` |
| Ok assertion | `ok ... ;` |
| Err assertion | `err ... ;` |
| Block end | `}` |
## How it works
- **`ok`** calls `fn-name` with the given literal arguments and asserts the
return value equals `expected`. For `R`-returning functions, this checks
the `~v` (Ok) path.
- **`err`** calls `fn-name` and asserts it returns `^expected-err`. The
expected error text is a string literal.
- **Literal args only.** Test assertions accept number, text, boolean, and
nil literals, not arbitrary expressions.
- **Last literal is the expected value.** All preceding literals are call
arguments. So `ok add 2 3 5` means: call `add(2, 3)`, assert result is `5`.
- **`;`-separated.** Assertions are separated by `;` inside the braces.
Trailing `;` is optional.
## Error codes
| Code | Severity | Trigger |
|------|----------|---------|
| `ILO-T050` | Error | A test assertion failed (actual != expected) |
| `ILO-W020` | Warning | Function has no test block (under `--strict`) |
## Relationship to `ilo test`
Shadow test blocks live **inside** the language (in the source AST). They
complement the existing `-- run:` / `-- out:` / `-- err:` annotation
format used by the `ilo test` command, which lives **above** the language
(in comments the test harness reads).
| Feature | Shadow test blocks | `-- run:` / `-- out:` |
|---------|-------------------|----------------------|
| Where | In-source AST | In comments |
| When | `ilo check` time | `ilo test` command |
| Args | Literal values only | CLI args (strings, numbers, lists) |
| Engine | Verify pass | All engines (VM, JIT) |
| Assertion shape | `ok`/`err` | stdout/stderr/exit code |
## Example
```ilo
-- A function with its shadow test
fn safe-div a:n b:n>R n t
;=b 0 ^"divide by zero"
;~/a b
test safe-div {
ok safe-div 10 2 5;
ok safe-div 0 1 0;
err safe-div 10 0 "divide by zero"
}
```
Run `ilo check` to verify. If `safe-div` returns the wrong value for any
assertion, `ILO-T050` fires with the actual vs expected values. If
`safe-div` has no test block and you run `ilo check --strict`, `ILO-W020`
warns that the function lacks self-verification.
---
# Tool Policies
URL: https://ilo-lang.ai/docs/reference/tools/
Runtime safety limits on tool calls. Optional policy{domain,tokens,rate} block in tool declarations.
Runtime safety limits on tool calls. Optional `policy{...}` block after `timeout`/`retry` in a `tool` declaration.
## Syntax
```ilo
tool name "description" (args) > R _ t timeout:30,retry:2 policy{domain:"...",tokens:500,rate:10}
```
All fields are optional. A `policy{}` block with no fields is valid (no restrictions). When omitted entirely, tool calls behave as before with no limits.
## Fields
| Field | Type | Effect | Violation error |
|-------|------|--------|-----------------|
| `domain` | text | URL must match this host. Supports `*.example.com` wildcard (matches `api.example.com` and `example.com`). Checked against the first text argument, which is conventionally the URL for HTTP tools. | `^"policy: domain mismatch - url host 'evil.com' not in allowed domain 'api.weather.com'"` |
| `tokens` | number | Maximum tokens consumed per call. Intended for LLM-backed tools where the response size determines cost. | `^"policy: token budget exceeded"` |
| `rate` | number | Maximum calls per minute. Enforced at dispatch time. | `^"policy: rate limit exceeded"` |
Policy violations return a `^"policy: ..."` Result error **before** the tool call proceeds. The caller handles them with `!` propagation or `?` match like any other Result error.
## Example
```ilo
tool weather "get weather" (city:t) > R _ t timeout:30,retry:2 policy{domain:"api.weather.com",tokens:500,rate:10}
```
```ilo
fetch city:t>R t t
;r=weather! city
;?r{^e:^+"failed: "e;~v:v}
```
A call with a URL outside `api.weather.com` returns `^"policy: domain mismatch ..."` instead of hitting the endpoint.
## How it works
Policy enforcement happens at tool dispatch time in every backend (tree interpreter, register VM). The domain check extracts the host from the first text argument and matches it against the declared domain pattern. Without a `policy` block, tool calls have no restrictions (backward compatible).
## When to use
- **`domain`**: restrict HTTP tools to known-safe endpoints. Prevents an agent from accidentally calling a different URL than intended.
- **`tokens`**: cap cost on LLM-backed tools. Prevents runaway spending on autonomous loops.
- **`rate`**: throttle high-frequency tool calls. Prevents flooding an API with requests.
All three are opt-in. A tool without `policy{...}` works exactly as before.
---
# Transpile
URL: https://ilo-lang.ai/docs/reference/transpile/
Compile an ilo program to JavaScript or Python source. Use this when you need to run ilo logic in a JS or Python runtime - browser, Node, embedded Python, or any non-ilo host.
Compile an ilo program to JavaScript or Python source. Use this when you need to run ilo logic in a JS or Python runtime - browser, Node, embedded Python, or any non-ilo host.
## Targets
```bash
ilo build file.@ --emit js -o file.js # ES module
ilo build file.@ --emit python -o file.py # Python 3.8+
```
Same `ilo build` subcommand as the native AOT path; the `--emit` flag chooses the target.
## What gets emitted
- **JS**: ES module with arrow-function bodies, kebab-case → camelCase rename (`make-id` → `makeId`), Result encoded as `["ok", v]` / `["err", e]` tuples, records as plain objects with `_type` key. Builtin mapping: `len` → `.length`, `abs/min/max/flr/cel/sqrt` → `Math.*`, `prnt` → `console.log`, `jdmp/jpar` → `JSON.stringify/parse`, `rnd` → `Math.random()`, list append → spread.
- **Python**: idiomatic Python 3 source with type hints. Same Result-as-tuple shape. Records as `@dataclass`.
## Limitations
The transpiled output is **not a full ilo runtime** - it covers the canonical surface (functions, prefix expressions, guards, match, Result, records, lists, maps, common builtins). Features that don't have a clean host equivalent are deferred:
- **JS**: file I/O, `defer`/`errdefer`, `World`/capabilities, tagged sum-type match patterns, generic monomorphisation (best-effort), full builtin coverage
- **Python**: pattern-match arms that depend on ilo-specific match semantics, World/capabilities
For production code that needs full ilo semantics, ship the native binary (`ilo build`) or run via JIT (`ilo run --jit`).
## When to use it
| Use case | Reach for |
|---|---|
| Browser script | `--emit js` |
| Existing Python codebase calling ilo | `--emit python` |
| Cross-platform native binary | native AOT (no `--emit`) |
| Hot numeric loop on the server | `ilo run --jit` |
| One-off CLI invocation | `ilo run` |
## Related
- `ilo build` - native AOT compile (default target)
- Engines reference - VM, JIT, AOT comparison
---
# Types & Functions
URL: https://ilo-lang.ai/docs/reference/types-and-functions/
Use this when declaring functions, return types, or working with ilo's type sigils.
Use this when declaring functions, return types, or working with ilo's type sigils.
Every ilo function declares its parameter types and return type inline:
```text
funcname param1:type param2:type>returntype;body
```
## Type sigils
| Sigil | Type | Description |
|-------|------|-------------|
| `n` | Number | Integers and floats |
| `t` | Text | Strings |
| `b` | Boolean | `true` / `false` |
| `_` | Any | Wildcard, accepts any type |
| `L` | List | Ordered collection `[1 2 3]` |
| `M` | Map | Key-value pairs `{"key" "val"}` |
| `R` | Result | Success or error value |
| `O` | Optional | Nil or a value: `O n` |
`_` means "don't care", same as in match patterns. Use it for mixed-type lists (`L _`), results where you ignore a type (`R _ t`), or generic parameters (`x:_`).
## Examples
```ilo
dbl x:n>n;*x 2 -- number → number
greet first:t last:t>t;fmt "{} {}" first last -- text params, text return
notzero x:n>b;!=x 0 -- ! is logical NOT; returns a bool
pi>n;3.14159 -- zero-arg function
```
## Multiple statements
In a file, use newlines and indentation:
```ilo
calc a:n b:n > n -- two numbers in, number out
s = + a b -- sum
p = * a b -- product
+ s p -- return sum + product
```
Inline, use `;` instead:
```bash
ilo 'calc a:n b:n>n;s=+a b;p=*a b;+s p' 3 4
```
Each statement binds a variable or returns a value. The last expression is the return value.
## Script mode (implicit `main`)
Bare statements at the top level of a file are collected, in source order, into a synthetic `main>_;` — no wrapper needed:
```ilo
prnt +2 2 -- whole file; prints 4
```
Declarations and top-level statements mix freely, as long as each statement starts its own top-level line:
```ilo
tri n:n>n;/(*n +n 1) 2
prnt tri 10 -- own line → becomes main's body; prints 55
```
A call glued to the declaration's line (`tri n:n>n;...;prnt tri 10`) joins `tri`'s body, making `tri` call itself — rejected at verify time as unconditional recursion (`ILO-V500`). A file with both an explicit `main` and bare statements is rejected (`ILO-P104`).
## Discarding a value (`_=expr`)
Use `_=expr` to explicitly discard a value and suppress the `ILO-T033` warning for mutation-shaped builtins (`mset`, `mdel`, `+=`) used for their side effects:
```text
_=mset m "visited" true -- discard the new map; side-effect only
_=prnt debug-value -- discard return, keep the print
```
The `_` sigil tells the verifier the discard is intentional. Without it, `mset m k v` as a bare statement silently drops the returned map and fires ILO-T033. For most cases you *do* want the assignment form (`m=mset m k v`) - `_=` is reserved for genuine fire-and-forget calls.
Newlines inside `[...]` or `(...)` are treated as whitespace, not statement separators, so list literals and parenthesised expressions can span multiple lines without problems. Windows CRLF (`\r\n`) is normalised to LF before parsing, so files edited on Windows work identically.
## Sum types (discriminated unions)
`type Name = V1 | V2(payloadType) | ...` declares a sum type at top level. Each variant may carry one payload:
```ilo
type shape = circle(n) | square(n) | point
area s:shape>n;?s{circle(r):*3.14 *r r; square(d):*d d; point:0}
```
Construct with `circle 5` (payload variant) or bare `point`; match with `tag(binding):` or `tag:` arms. Sum types are exhaustively matched - the verifier emits `ILO-T024` if a branch is missing.
## Generics
Sum type declarations accept type parameters, enabling reusable polymorphic variants:
```ilo
type result = ok(a) | err(b)
type opt = some(a) | none
```
Construct and match them exactly like non-generic sum types; the concrete type is inferred from context. For functions, the `_` (any) type accepts any value:
```ilo
identity x:_>_;x
```
There are no `'a`-style type variables on functions, no bounds, and no `where` clauses.
## Brace-lambda multi-statement body
A brace-lambda (`{params> stmts}`) gives a lambda a compact multi-statement body, anywhere a single-expression lambda is accepted:
```ilo
transform xs:L n>L n;map {x> s=*x x; +s 1} xs
```
Param names are bare (types inferred as `_`) and there is no explicit return type.
### `panic`
`panic msg` aborts immediately with a message (valid in any return-type context):
```ilo
assert-positive x:n>n;>x 0 x;panic "expected positive"
```
### Match alternatives
A match arm can cover multiple patterns with `|`:
```ilo
classify x:n>t;?x{0|1|2:"small"; _:"large"}
```
As of 26.5 an or-pattern only parses in the **first** arm; a later arm containing `|` fails with `ILO-P003` ([ILO-509](https://linear.app/ilo-lang/issue/ILO-509)). Put the or-pattern first, or expand the later arms into single patterns.