# 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.