Skip to content

CLI Reference

Use this when looking up an ilo CLI flag, subcommand, or invocation form.

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

For consistency with cargo, go, and similar toolchains, ilo also exposes verb forms:

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

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

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.

Shell
# In CI:
ilo check src/*.ilo --strict --json

ilo test <path> runs the -- run: <fn> <args> / -- out: <expected> (or -- err: <stderr>) 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.

Shell
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
Shell
$ 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:

Shell
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

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:

Shell
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:

Shell
ilo file.ilo --vm 5 # runs main 5 on the VM

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

Shell
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:

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

FlagDescription
-aiOutput compact spec for LLM consumption
-e, --expandedExpanded/formatted output
-d, --denseDense wire format (minimal whitespace)
-a, --ansiForce ANSI colour output (default for TTY)
-t, --textPlain text output (no colour)
-j, --jsonJSON output (default for piped output)
-x, --explainExplain a program or error code
--benchBenchmark a function
--verifyType-check without executing
--emit pythonTranspile to Python
--emit jsTranspile to JavaScript (Node.js/browser)
--tools tools.jsonLoad HTTP tool declarations
--mcp mcp.jsonConnect MCP servers
--no-hints, -nhSuppress idiomatic hints
--silent, -sSuppress program stdout (mainly for --bench; bench output still reaches stdout)
--max-ast-depth NCap 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 SECSCap 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 BYTESCap total stdout output for ilo run at BYTES (default ~100 MB). Aborts with ILO-R017 on overrun. Pass 0 to disable.
compileAOT compile to standalone native binary

Pass list arguments from the command line with bare commas (no spaces, no brackets):

Shell
ilo 'f xs:L n>n;len xs' 1,2,3 # → 3
ilo 'f xs:L t>t;xs.0' 'a,b,c' # → a

map, flt, fld take a function name as their first argument. Define the helper function alongside a main entry point and invoke main:

Shell
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:

Shell
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]

Control how results and errors are rendered:

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

Shell
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)).

Install packages from the ilo package registry:

Shell
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".

ilo apply consumes a structured repair plan (JSON) produced by ilo check --json and applies the mechanical edits:

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

ilo trace records execution with per-step timing and a call tree:

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

ilo httpd exposes ilo functions as HTTP endpoints:

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

tokcount reports the token count of ilo source using the tiktoken-rs tokeniser (cl100k_base by default):

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

Generate standalone Python from ilo source:

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

Generate JavaScript (Node.js/browser) from ilo source:

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

Time a function over repeated runs:

Shell
ilo program.ilo --bench funcname 10 20 30

Reports execution time for the named function with the given arguments.

--explain (or -x) has two modes:

Explain an error code - show a detailed description of any ILO-* diagnostic:

Shell
ilo --explain ILO-T004

Explain a program - annotate each statement with a human-readable description of what it does:

Shell
ilo 'f x:n>n;*x 2' --explain

All programs are type-verified before execution. Errors are reported with stable codes, source context, and suggestions:

Shell
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:

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

Compile an ilo program to a standalone native binary:

Shell
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).

Shell
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).

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 returnstdoutstderrExit
~v (Ok)v (bare)0
^e (Err)^e1
any non-Resultv0

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.

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 supportVersioned?
ilo run / ilo file.iloyes (success + error envelopes)yes (0.12.1+)
ilo checkyes (one diagnostic per line)per-diag
ilo build / ilo compileyes (output, sizeBytes, durationMs)yes
ilo graphyes (always JSON unless --dot)yes (0.12.1+)
ilo --astyes (AST as JSON)yes (0.12.1+)
ilo explain ILO-XXXXyesyes
ilo skill list/get/path/showyesyes
ilo versionyes (version, features)yes
ilo toolsyes (via --json subflag)yes (0.12.1+, breaking wrap)
ilo servyes (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 in the ilo repo; it’s locked by tests/json_output_contracts.rs so future changes that break a schema fail CI.

ilo supports multiple execution backends. The default is the bytecode register VM. Cranelift JIT is opt-in via --jit for hot numeric loops:

FlagBackend
(default)Register VM (closure-aware, all opcodes supported)
--jitCranelift JIT (hot numeric loops; falls back to VM on bailout)
--vmRegister VM (canonical explicit form, symmetric with --jit)
--run-vmDeprecated alias for --vm. Emits a one-shot stderr hint; removed in 0.13.0.
--run-llvmLLVM 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).

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

Start an interactive session:

Shell
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:

CommandDescription
:qQuit the REPL
:w file.iloWrite current definitions to a file
:defsShow all defined functions and types
:clearClear all accumulated state
:helpShow REPL help
Shell
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