For the release process and tag conventions, see RELEASING.md.
-
Script mode: implicit
main>_;for bare top-level statements (ILO-439). A file no longer needs amain>_;wrapper: bare statements at the top level are collected, in source order, into a syntheticmain.prnt +2 2alone in a file prints 4. Declarations and statements mix freely as long as each statement starts its own top-level line -tri n:n>n;/(*n +n 1) 2followed byprnt tri 10on the next line prints 55, which is exactly the shape a model writes when asked for a compact program. Measured motivation (ILO-364 closed-loop benchmark): the missing-wrapper diagnostics dominated every ilo failure (ILO-P102 fired 19 times across one N=3 run) and models could not recover from them even when the hint named the fix; with script mode those collapse to near zero and failures shift to genuine type errors. Deliberate inversion of the original ticket's "verifier rejects mixed" rule, on that evidence. Guard-rails so the new acceptance cannot swallow existing diagnostics: fragments glued to a broken declaration (f>n;1e,main->n,0 -1.5, stray}, foreignlet/if/return) still route to the parser's targeted errors; a gluedname=exprafter a declaration still fires ILO-P102 (its registry text updated); a file with both an explicitmainand bare statements is rejected with new ILO-P104; the REPL asksparser::is_script_mode_inputso+1 2still evaluates to3instead of reportingdefined: main. The k-means chain that originally motivated ILO-P102 now simply runs. -
ILO-V500: unconditional-recursion detection (first code in the verifier namespace). A function whose straight-line body (no guards, matches, loops, or early returns) contains a direct call to itself can never terminate; because tail calls trampoline, it used to spin silently at runtime (each occurrence burned the full 20s timeout in the ILO-364 harness) instead of overflowing the stack. Now rejected at verify time with a hint that names both fixes: add a base-case guard, or - for the script-mode case
tri n:n>n;...;prnt tri 10where a trailing call glued to the definition line joins the body - put the call on its own line. Conservative by construction: any branching construct in the body disables the check, so guarded recursion (cd n:n>n;=n 0 0;cd -n 1), match-arm recursion, ternary-branch recursion, and fn-refs passed to HOFs are all untouched (pinned by tests).
-
CLI arguments are type-checked against the entry function's parameters (ILO-517). A shell string that didn't match its declared parameter type used to be bound as-is, because
parse_cli_arg's ladder falls through toTextfor anything non-numeric. Sotri n:n>ninvoked asilo tri.@ mainboundValue::Text("main")to anparameter: the tree-walker and VM printedNaN, the Cranelift JIT echoedmain, and every engine exited 0 — a silent wrong answer with a success exit code, which is worse than a crash for any caller that checks$?. The CLI was therefore less safe than the language it fronts, wherenum "main"returnsR n tand the type checker forces the failure to be handled. A new CLI-boundary guard (check_cli_arg_types, sibling of the existingcheck_cli_arity) now rejects unambiguously mismatched values withILO-R600, naming the parameter and showing the offending literal, and exits 1. Wired into all four dispatch sites (VM, interpreter, JIT, default) so the error contract can't drift per engine the way ILO-177 did. The guard is deliberately permissive —_accepts anything,O Tstill acceptsnil, and structural / user-defined types are waved through — so the ILO-182 single-fn pass-through keeps working (ilo greet.@ worldagainsts:tis legitimate usage, not a typo'd function name). Also closes the same hole forbparameters. Found while running the ILO-364 closed-loop benchmark, where it cost a model retries on an otherwise-correct program. -
get-streamyields lines on newline, not buffer-fill (ILO-489). The client-side streaming builtins (get-stream,get-stream-h,pst-stream,pst-stream-h, ILO-448) wrapped minreq'sResponseLazyinBufReader::lines(), which blocks on a full ~8 KiB read-buffer fill before surfacing any line. So a slow SSE upstream that flushes one short event then idles had its lines batched until the buffer filled or the connection closed - functionally correct but latency was buffer-bound, not event-bound. The line splitter now consumesResponseLazy's byte iterator incrementally and emits each line the instant its\narrives (trailing\rstripped for CRLF / chunked encoding), so each event surfaces promptly. EOF still yields a trailing newline-less partial line; mid-stream errors still surface asILO-R009 http-stream read error: .... Unblocks ILO-482's previously flaky end-to-end streaming test.
ilo httpdlazy streaming response body (ILO-482). A handler's responsebodyfield may now be a lazy line iterator (get-stream/pst-stream,for-line stdin) in addition to a plain string or an eagerL tlist. When the body is a lazy iterator,ilo httpdwrites and flushes each yielded line as its own chunked-transfer block as soon as the handler produces it, rather than materialising the whole body first. This lets a handler hold the connection open and emit chunks incrementally (true SSE / long-poll / tailing a growing source). If the client disconnects mid-stream the connection thread drops the iterator and exits cleanly with no panic. The existing string andL tbody shapes are unchanged. Unblocksilo-lang/crew'screw-serverGET /events/stream, intended as a held-open tail ofdata/feed/<day>.jsonlbut stuck on a one-shot snapshot while the body buffered. Follow-ups: atail-filesource (lazytail -f) for the file-tail case, andget-stream's 16 KiB read-buffer granularity for sub-buffer payloads. Seedocs/streaming.mdandexamples/httpd-stream.ilo.ilo httpdresolvesuseimports (ILO-481). Handler files loaded byilo httpdnow have theiruseimports resolved at startup, relative to the handler's own directory, matching the existingilo run/ilo checksemantics. Previouslyhttpdlexed, parsed, and verified only the single handler file and silently skipped import resolution, so a handler could notusea sibling module -ilo-lang/crew'screw-serverhad to inline ~140 lines of store logic to work around it. A missing module now surfaces a real import diagnostic and the server refuses to start instead of failing later with a generic verifier error. Seedocs/streaming.md.spawnbuiltin (ILO-477).spawn fn args... > _runsfn args...on a background OS thread, fire-and-forget. Returns nil immediately; errors and panics inside the thread go to stderr and the thread dies, while the parent is unaffected. Caps are inherited from the parent viaArc::clone(&env.caps), so a worker started under--allow-net/--allow-writekeeps the same policy. Unblocks daemon-style programs that need multiple concurrent loops in one process - canonical case isilo-lang/crew's per-machine agent (MCP HTTP server foreground + SSE consumer background + write-behind queue drainer background). Out of scope for v1 (separate tickets): join handles, channels, supervision, cancellation tokens, async runtime, native VM / Cranelift codegen. Tree-walker only at runtime; VM and Cranelift inherit through the existing tree bridge. Seeexamples/daemon-loops.ilo.- Client-side HTTP streaming (ILO-448). Four new builtins that return a lazy
L tline iterator over a chunked / SSE response body:get-stream url,get-stream-h url headers,pst-stream url body,pst-stream-h url body headers. Consume via@line (get-stream url){...}- one chunk-line per iteration, body never fully buffered. Cap-checked via--allow-netbefore opening the connection; mid-stream I/O errors surface asILO-R009 http-stream read error: .... WASM returnsErr. Symmetric counterpart to the server-sideilo httpd+ chunked transfer encoding shipped in ILO-46 / ILO-379; unblocksilo-lang/crew's per-machine agent daemon needing an SSE consumer. Tree + VM only in this release; Cranelift JIT follow-up. Seedocs/streaming.mdandexamples/sse-client.ilo.
First CalVer release. The versioning scheme shifts from semver to CalVer (YY.M, e.g. 26.5; patches YY.M.P). The version string now carries recency so an agent loading ilo spec --json ai knows which spec applies from the version alone, no changelog lookup needed. Token-conservative (manifesto principle 1) vs semver's 0.12.1. Last semver release was 0.12.1; there is no 0.13.x. Branching model splits: main carries stable + patch tags (26.5, 26.5.1, 26.5.1-dev.N), next carries dev tags only (26.6-dev.N). See README.md#versioning for the full release / patch flow.
The optional file version pragma (^26.5 on the first line of a .ilo file) ships with this cut. Absent pragma silently assumes the latest installed runtime, so existing 0.x files keep verifying as-is.
This release also carries everything that had queued under Unreleased since 0.12.1: the run-family overhaul, language additions (_=expr, \xNN escapes, file version pragma), the builtin batch (idxof, matvec, lstsq, OP_TAILCALL, jpar-list, get-to/pst-to, tz-offset, run2, rgxall-multi, fmod, dtparse-rel, dur-parse/dur-fmt), the cascade-dedup diagnostic overhaul, and the find_libilo_a worktree-friendly target-dir lookup. Detail entries follow.
-
run-family overhaul (ILO-35). Three new process-spawn shapes for 0.13.0:
run cmd argv stdin:t > R (M t t) t— arity-3 extension ofrunthat pipes a text string into the child's stdin. Same no-shell-no-glob security model, same 10 MiB output cap, samecode/stdout/stderrresult Map as the 2-arg form. Unblocks any persona that needs to pass data to a filter command (jq,awk,cat,wc,python -c, etc.) without writing a temp file.run2 cmd argv stdin:t > R RunResult t— arity-3 extension ofrun2with the same stdin-pipe mechanic. Returns a typedRunResultrecord (r.stdout,r.stderr,r.exit:n) for clean dot-access. Non-zero exit is NOT an error; Err only on spawn failure.run-bg cmd argv > R n t— fire-and-forget background spawn. ReturnsOk(pid:n)immediately without waiting for the child; child inherits the parent's stdout/stderr and reads/dev/nullon stdin. Use when you want to start a long-running server or worker and continue executing ilo code. Err only on spawn failure (cmd not found, permission denied, etc.). The returned pid is a positive integer. All three are tree-bridge eligible so VM and Cranelift JIT/AOT backends inherit them throughOP_CALL_BUILTIN_TREEwithout new opcodes. All three areexperimentalstability.
-
ILO-P102diagnostic for top-levelname=exprbindings outside any function declaration. Catches the "forgot themain>_;wrapper" misparse that k-means and linear-regression personas hit when chaining imperative bindings at the top level. Without the wrapper the parser used to either die on the bare=(a bareILO-P003) or, when a priorname>type;bodydecl was in scope, slurp the whole chain into that fn's body and emit a wall of misleadingILO-T005cascades anchored on the wrong line.ILO-P102collapses both shapes into a single diagnostic that names the offending binding and suggests themain>_;wrapper. Parser-only change; identical output across VM and JIT.
- New
ILO-W002warning when the foreach collection is a directjpar!orjpar!!call. Surfaces the hint pointing atjpar-list!, which asserts the top-level JSON is an array and returnsR (L _) tso the unwrap composes cleanly into@. Catches themempool-fee-estimatorfailure mode where the polymorphicjparOk type forced the wrapping function's return type toR t tand threaded?through downstream code. The@x (jpar-list! body){...}form continues to type-check silently;xs = jpar! body; @x xs{...}(the explicit-bind form) is unchanged. Diagnostic-only; no behaviour change in the runtime engines. Closes pending.md item #5f. - Cascading
ILO-T005 undefined function 'X'errors from a single parse failure now collapse to one diagnostic per parse-failed function with a cross-reference back to the originating parse error. Previously, ONE broken function body produced N undefined-function errors (one per call site), burying the root cause; the cron-explainer persona logged 286 ILO-T005, 107 ILO-P009, and 47 ILO-P001 from roughly 10 root causes in a single run. The parser now records function names whose return-type or body failed to parse onProgram.parse_failed_fns, and the verifier (1) skips type-checking those functions' bodies (their AST is poison) and (2) emits one collapsedILO-T005per parse-failed name with a hint pointing at the root parse error code. Real undefined-function errors (typos, missing imports) still surface normally with the usual suggestion text.
find_libilo_a(AOT linker helper insrc/vm/compile_cranelift.rs) now honoursCARGO_TARGET_DIRand.cargo/config.toml'sbuild.target-dirbefore falling back to$CARGO_MANIFEST_DIR/target. Fix worktrees that redirect cargo's target dir out of the tree (e.g.[build] target-dir = "/tmp/ilo-targets/...") no longer need aln -sf .../release/libilo.a target/release/libilo.aworkaround for the AOT tests to find the staticlib. Test-infrastructure only; no user-visible change toilo compile.
-
_=exprexplicit discard bind. Evaluatesexprfor side effects and drops the result without allocating a binding. The_sigil is not a real local — it cannot be read back after the statement. Primary uses: (a) silencing ILO-T033 when discarding the return value ofmset/+=/mdelis genuinely intentional, (b) calling a side-effecting function at non-tail position when the return value is irrelevant. All three engines (tree-interpreter, VM, Cranelift JIT/AOT) produce the same behaviour: the RHS expression is fully evaluated, its result is discarded with no register/slot allocation. The verifier still checks the RHS for type errors and T005 undefined-function; it does not insert_into scope so a subsequent_reference still resolves to the wildcard/nil sentinel. (ILO-36) -
idxof s sub > O nbuiltin. Returns the first Unicode code-point index ofsubins, or nil when not found. Index is in code-point units (same convention asat), not raw byte offsets. Emptysubreturns 0 (Python / JS semantics). Closes the verboseflt+lenworkaround scrapingbee-chain and tui-client personas reached for when locating substrings. Tree-bridge eligible: pure 2-arg text-in / option-n-out, no FnRef args, no I/O. VM and Cranelift inherit through the bridge without new opcodes. Part of the 0.13.0 text-utility batch (ILO-39). -
\xNNhex escape in string literals. Two hex digits after\xencode a single Unicode code point in U+0000..=U+00FF. Case-insensitive (\x1band\x1Bboth produce ESC). Non-hex digits after\xare passed through literally (lexer stays infallible). Closes the ANSI-escape friction that tui-client personas hit when embedding colour codes ("\x1b[31m"is now legal). Part of the 0.13.0 text-utility batch (ILO-39). -
matvec xm ys > L nbuiltin. Native matrix-vector product as a flat vector. Replaces theflatten matmul xm (map (y:n>L n;[y]) ys)ceremony every linear-regression-style persona was paying (three lines / ~10 tokens per use). Errors asILO-R009on dim mismatch, empty matrix, or ragged rows. Tree-bridge eligible -- VM and Cranelift inherit through the bridge without new opcodes. Closes pending.md #5an. -
lstsq xm ys > L nbuiltin. Ordinary least squares via the normal equations: returns the coefficient vectorbminimising||xm·b - ys||². Closed-form OLS as a thin wrapper aroundsolve (Xᵀ X) (Xᵀ y)- collapses the 5-line recipe (transpose+matmul+matmul+solve+ index-fiddling) into a single call, saving ~30 tokens per OLS use. Errors as ILO-R009 on rank-deficient design, underdetermined system (cols > rows), row/length mismatch, or empty input. Same precision tier assolve/inv/det(LU with partial pivoting); numerically inferior to QR/SVD for ill-conditioned designs. Tree-bridge eligible - VM and Cranelift inherit through the bridge with no new opcodes. Motivated by the linear-regression persona. -
OP_TAILCALLopcode and VM-compiler emission. When a static user-fn call sits in tail position (the function's last statement, or the last statement of any context that itself sits in tail position), the bytecode VM compiler now emitsOP_TAILCALLinstead ofOP_CALL+OP_RET. At runtime the VM reuses the currentCallFramerather than pushing a new one, so a function that recurses only in tail position runs in O(1) frame memory:count-down 5_000_000now completes in tens of milliseconds on--vm, mirroring the tree-interpreter trampoline shipped in the previous PR. Cross-function tail chains (ftail-callsgtail-callsh) work too -- the chunk index is swapped in place. Auto-unwrap (!/!!) calls stay on the normalOP_CALLpath because the post-call result probe wants to inspect the value before deciding whether to propagate. The Cranelift JIT/AOT path lowersOP_TAILCALLidentically toOP_CALLfor now (semantically correct, no host-stack TCO benefit); nativereturn_calllowering ships in a follow-up PR. -
jpar-list textbuiltin: parse a JSON string and assert the top-level value is an array. ReturnsR (L _) t.jpar-list! bodyunwraps toL _directly, so@x (jpar-list! body){...}type-checks without a binding or type annotation.jparis unchanged (returnsR _ t); usejpar-listwhen the response is known to be a JSON array. -
get-to url timeout-ms > R t tandpst-to url body timeout-ms > R t tbuiltins. HTTP GET and POST with an explicit per-request timeout in milliseconds. The timeout rounds up to the nearest whole second (minreq granularity). ReturnsErrwhen the deadline is exceeded, identical to any other connection failure from the caller's view. Both are tree-bridge eligible so VM and Cranelift JIT/AOT inherit them without new opcodes. Closes pending.md item #29. -
tz-offset tz:t epoch:n > R n tbuiltin. Returns the UTC offset in seconds for a named IANA timezone at a given Unix epoch. Handles DST transitions correctly via chrono-tz: the offset reflects the actual local time rule at that instant. Positive values are east of UTC. ReturnsErron unknown timezone name. Tree-bridge eligible (VM and Cranelift dispatch through the tree interpreter, no new opcodes). Covers London BST/GMT transitions, New York EST/EDT, Tokyo JST (no DST), and the full IANA tz database. -
run2 cmd:t args:L t > R RunResult tbuiltin. Likerunbut returns a typedRunResultrecord (r.stdout:t,r.stderr:t,r.exit:n) instead of a loose Map. The key difference:exitis a number, not text, so numeric comparisons work directly (=0 r.exit,<0 r.exit). Non-zero exit is NOT an error;Erronly on spawn failure (cmd not found, permission denied, etc.). Signal-killed processes on Unix surface asexit:-1. Same no-shell-no-glob security model, same concurrency + 10 MiB output-cap policy asrun. Tree-bridge eligible (VM and Cranelift dispatch through the interpreter arm).runis unchanged for compatibility. -
rgxall-multi pats:L t s:t > L tbuiltin. Apply multiple patterns to a single string and get one flat list of all hits in pattern order. Per-pattern semantics followrgxall1: 0 capture groups returns whole matches; 1 capture group returns capture-1 strings; 2+ capture groups errors with a hint to usergxall. Replaces the verboseflat (map (p:t>L t;rgxall1 p line) pats)workaround (~20 tokens per call site saved). Motivated by cron-explainer and historical-archeologist personas, which both needed multi-pattern scan on a single line. Tree-bridge eligible alongsidergxall1; no new opcodes. -
fmod a bbuiltin: floor-mod, always non-negative whenb > 0. Equivalent to Pythona % band JSMath.floor((a % b + b) % b). Implemented across VM, JIT, and AOT. Eliminates the(raw + 7) % 7workaround that every TZ/weekday persona needed with signedmod.modis unchanged (C-style signed remainder). -
dtparse-rel s now > R n tbuiltin. Resolves a natural-language relative-date phrase to a Unix epoch anchored atnow. Supported:today/yesterday/tomorrow,N days/weeks/months ago,in N days/weeks/months(singular + plural),last/next/this <weekday>(monday-sunday or mon-sun;last/nextnever return today), and ISO-8601YYYY-MM-DDpassthrough. Month arithmetic clamps to the last valid day (Jan 31 + 1 month = Feb 28/29). Tree-bridge eligible -- VM and Cranelift pick it up automatically. Eliminates ~40 LoC of date-arithmetic helpers per date persona (P1 #8 from the persona feedback log). -
dur-parse s > R n tanddur-fmt n > tduration builtins.dur-parseparses human-readable duration strings ("3h 30m", "1 week 2 days", "1.5 hours", "90s") into total seconds; lenient, accepts abbreviationss/m/h/d/w, full singular/plural names, decimal quantities, and mixed sequences with or without spaces between number and unit. A leading-is sticky and applies to every following token until an explicit+resets it, so"-1m 30s"parses to-90(which makes thedur-fmt -> dur-parseround-trip symmetric for negative durations). Months are deliberately unsupported because a month is not a fixed number of seconds;"3mo"/"3 months"error rather than silently misparse.dur-fmtformats seconds as human-readable text, dropping zero parts and using the largest applicable units ("2h 42m", "1 day", "30s"). Negative values emit a single leading minus. Fractional seconds are preserved with up to 3 decimal places (90.5 -> "1m 30.5s",0.5 -> "0.5s"); trailing zeros are stripped. Both are tree-bridge eligible: VM and Cranelift dispatch through the interpreter arm with no dedicated opcodes. Closes the duration-parse/format gap thatschedule-arithmeticandevent-chronologypersonas were stubbing out manually. -
File version pragma (optional). Top-of-file sigil
^26.5declares the minimum required runtime. Sigil-led (principle 4), ~3 tokens (principle 1), first-class syntax (not a magic comment). Must be on the first line with no leading whitespace when present. Verifier: absent pragma silently assumes the latest installed runtime (no diagnostic) so existing 0.x files and any unannotated file keep verifying as-is; a pragma older than the runtime with a known breaking change between fails with a migration pointer; a pragma newer than the runtime fails asking to upgrade. Tooling:ilo --version-of <file>reads the pragma and returns nothing when absent; the formatter canonicalises position when present and never inserts one. Ships with the CalVer cut.
- Added a gitleaks secret-scan gate to the release workflow. Every
v*tag push now runsgitleaks/gitleaks-action@v2against the full repo before the build, build-wasm, release, and publish jobs fire. A leaked API key, token, or PEM blocks the tag. Whitelist lives in.gitleaks.tomlat the repo root and covers the placeholder strings used byexamples/apps/*(SCRAPINGBEE_KEY_PLACEHOLDER_...,sk-PLACEHOLDER-...,YOUR_*_HERE,REPLACE_ME). Default gitleaks ruleset otherwise. Run locally withgitleaks detect --source . --no-git.
ls dirrenamed tolsd dir. Six rerun10 personas tripped ILO-P011 onls=rdl! pbecauselswas reserved; rename freeslsfor user code.walk,globunchanged.--run-treeand its--runalias removed from the public CLI. They now error with the unknown-flag guard. The tree-walker stays in-tree as the dispatch target for the HOF / regex / fmt-variadic / fmt2 / IO / sleep / ct / rsrt / closure-bind-ctx shapes the VM and Cranelift haven't lifted natively yet; the VM bails to it transparently for every op inis_tree_bridge_eligible. Use--vm(the default) for everything else.ilo tools --jsonis now an envelope{"schemaVersion":1,"tools":[...]}instead of a bare array. Indexing consumers should read.tools[0]instead of[0]. Brings the last hold-out into the uniform CLI--jsoncontract — every other emitter (run,graph,--ast,serv,spec --json) gainedschemaVersion:1additively in the same release.
--run-vmrenamed to--vm, symmetric in shape with--jitand--run-llvm(where the flag names the engine, not the action).--run-vmis retained as a hidden alias for one release; every invocation emits a one-shot stderr hinthint: --run-vm → --vm (canonical form). The --run-vm alias will be removed in 0.13.0.. Carry-forward scripts and personas that hard-coded--run-vmkeep working through 0.12.x and pick up the nudge to update. Hard removal lands in 0.13.0 with the tree-walker drop.
-
ILO-P011 now catches every builtin alias (
head,length,filter,concat,tail,sort,reverse,flatten,contains,group,average,print,trim,split,format,regex,read,readlines,readbuf,write,writelines,lset,floor,ceil,round,rand,random,rng,string,number,slice,unique,fold) as a reserved name when used as a binding LHS or user-function declaration, at all three parser sites (top-level, local, fn-decl). Previously onlyrngandrandhad per-alias guards; every other long-form alias likehead=...was accepted silently and the call-site rewrite to the canonical builtin (hd) bypassed the user binding entirely, returning empty output with no error. A singleresolve_aliascheck now covers the full alias table so new aliases land protected automatically. Surfaced by rerun-prompt-generator and changelog-validator rerun12, which both boundhead=...and got empty output. -
schemaVersion: 1uniform across every CLI--jsonenvelope. Five legacy emitters (ilo run,ilo graph,ilo --ast,ilo serv,ilo tools --json) and the newilo spec --jsonmode all now carry the field at the top level so a single routing branch in agent code handles every command. For four of those five the change is strictly additive (object envelopes get one extra field);ilo tools --jsonis the lone breaking-but-additive wrap noted above.ilo servcarriesschemaVersion:1on every line, including thereadyhandshake and every error response (request,lex,parse,verify,runtime,programphases). Full audit inJSON_OUTPUT.md. -
ilo spec --json [lang|ai]new mode wraps the markdown /ai.txtprose as{"schemaVersion":1,"format":"markdown"|"ai-txt","content":"..."}. Plain-text mode is unchanged.
prod xs > nandcprod xs > L nbuiltins.prodis the multiplicative mirror ofsum(product of all elements; empty list returns 1).cprodis the running-product mirror ofcumsum(each element i is the product of xs[0..=i]; empty list returns []). Both are fully compiled across tree, VM, JIT, and AOT. Theprod []identity-1 semantic matches the mathematical convention and avoids the ILO-R009 empty-list error thatavg []raises.wra path swrite-append builtin. Appends text to the file atpath, creating it if it does not exist. SignatureR t tmirrorswr; Ok returns the path, Err returns the OS error message. Lowers through the tree-bridge so all three engines (VM, Cranelift JIT, AOT) pick it up without a new opcode. Python codegen emitsopen(path, 'a'). Covers the "incremental log / accumulate output across steps" pattern agents reach for next afterwr.examples/apps/directory of real-world programs harvested from persona-rerun workspaces (batch-loop-orchestration, agent-repair-loop, error-budget, doc-discovery, config-shaper, ecommerce-analytics, text-mining). Each program ships with-- run:/-- out:markers and any required input lives inexamples/apps/fixtures/, so the existingtests/examples_engines.rsharness exercises them on every CI run. The harness now recurses one level into subdirectories. Complements the flatexamples/layout (per-feature pins) with end-to-end shapes that an agent actually writes.mpairs m > L (L _)builtin returns a sorted-by-key list of[k, v]2-element lists. Invariant:mpairs m == zip (mkeys m) (mvals m). Replaces the commonmap (fn k > [k (mget m k)]) (mkeys m)cascade, killing both a lambda and a per-iteration hashmap lookup. New OP_MPAIRS opcode; tree, VM and JIT all share the same sort-then-zip walk.randalias forrnd(universal short-form for random; matches C/Python/Rust/Go/JS naming). Resolves to canonicalrndafter parsing; rejected as binding or user-fn name via ILO-P011 to prevent silent shadow mis-dispatch.randomcontinues to resolve torndas before.ilo check --strictflag. Treats every warning-severity diagnostic (ILO-T032 barefmt, ILO-T033 baremset/+=/mdel, future warning codes) as a hard exit-code failure so CI harnesses can fail-on-warning. The diagnostic stream itself is unchanged: warnings still emit withseverity: "warning"in the JSON output, only the exit code is elevated. Surfaced by rerun11 ci-gating personas that ranilo check src/*.iloin CI and missed mset / fmt traps because the verifier exited 0 on warnings.mget-or m k default > vandlget-or xs i default > a. Defaulted lookups for Map and List that return the element type directly, noO vto coalesce, no OOB error forlget-or. The verifier enforces that the default matches the container's element/value type so the return shape isv/a, neverO v. Both lower through the tree-bridge, so every engine inherits semantics without new opcodes. Closes the manifesto-friction(mget m k) ?? dandi<len?at xs i:dceremony agents kept reaching for.argmax xs > n,argmin xs > n,argsort xs > L n. Index-returning aggregates with numpy naming.argmaxreturns the 0-based index of the maximum element (first occurrence wins on ties);argminthe same for minimum;argsortreturns the stable sorted-index permutation ascending (smallest to largest, empty list returns[]). All three error on empty input exceptargsort. All lower through the tree-bridge, so VM and Cranelift inherit them without new opcodes. Closes thesrt fn (enumerate xs)+ extract-first pattern agents converged on for argmax/argmin-style queries.dirname path > t,basename path > t,pathjoin parts:L t > tpath-manipulation builtins. POSIX semantics with Unix forward-slash separator (Windows backslash handling deferred to 0.13.0).dirnamereturns""(not".") for plain filenames sopathjoin [dirname p basename p]round-trips without injecting a phantom./prefix.pathjoinis list-form (not variadic) to avoid the ILO-P101 arity-inference trap. Pure text ops, no I/O, no Result wrapper, tree-bridge eligible so VM and Cranelift inherit cross-engine parity for free. Closes the four-builtincat (slc (spl p "/") 0 -1) "/"dance every filesystem persona was paying.rdin > R t tandrdinl > R (L t) t. Stdin read primitives.rdinreads all of stdin as text;rdinlreads it line by line with newlines stripped. Both return Err on I/O failure and on WASM targets (where stdin is unavailable). Both are 0-arg and lower through the tree-bridge so VM and Cranelift inherit them without new opcodes. Unblocks the Unix-pipeline persona class: programs can now receive piped input directly instead of reading a file or embedding data in argv. Closes the gap surfaced in the rerun12 lang-surface proposal (#5 rdin/rdinl ADOPT).- Math constants
pi(3.141592653589793),tau(6.283185307179586),e(2.718281828459045). Zero-arg builtins returning the canonical IEEE-754f64value. Tree-bridge-eligible, so VM and Cranelift JIT/AOT inherit with no new opcodes; Python codegen emitsmath.pi/math.tau/math.e. Stops agents hardcoding3.14159...or reconstructing pi via* 2 (atan2 0 -1)- both shapes surfaced in fft-peak rerun12. Note: becauseeis now a builtin name, any existing code usingeas a local binding will get an ILO-P011 diagnostic on upgrade; rename toev,er, or similar. default-on-err r d > Tbuiltin. UnwrapsR T EtoT, returningdif the result is Err. The Result mirror of??(nil-coalesce forO T). Kills the common?r{~v:v;^_:default}pattern when the error payload is unused. Lowers through the tree-bridge (2-arg, pure), so VM and Cranelift JIT inherit semantics without a new opcode. Verifier emits ILO-T040 when the first arg is notR T E(hint steers at??only when the first arg is Optional, avoiding misleading steers for plainn/t/bfirst args); ILO-T042 when the default's type doesn't match the Ok type (split from T040 so the agent can target the right arg); ILO-T041 when??is used on a Result value (steering todefault-on-err). T041 is intentionally suppressed when the lhs type isUnknown(e.g. type-variable params,_-typed values) to avoid false positives on generic code; regression-tested.
- ILO-T039: 0-arg user function used as a bare reference in value position. When an agent writes
v = my-fnor+my-fn 100andmy-fntakes no arguments, ilo now emits ILO-T039 with a hint pointing atmy-fn()as the correct call form. Previously the agent received a silent type error or a generic type-mismatch diagnostic with no actionable suggestion, costing one or more retries. - ILO-T006 on
lst(and itslsetalias) now carries a suggestion clarifying thatlst xs i vis "list set at index" (3 args, returns a new list with indexireplaced byv), not "last element". The 1-arg case (lst xs) points at the canonicalat xs -1for last-element intent; other arities point at the 3-arg signature without misreading the call as a last-element attempt. Surfaced by git-workflow rerun11 - agents reached forlst xsand hit an empty-suggestion arity error. - ILO-T013 on
cat "a" "b"(the string-concat instinct from Python/JS) now suggestsfmt "{}{}" a bor+a bas the canonical text-concat shapes.catis list-concat; the verifier used to flag the type error but leave the user to guess the fix, costing one round-trip on every new-write. Surfaced by scaffold rerun11.
ifreserved-word hint now suggests?expr{true:...;false:...}(semicolon-separated arms) instead of the space-separated?expr{true:... false:...}shape, which the parser rejected withILO-P003 expected Semi, got False. Agents following the hint hit a second error and burned tokens retrying. Surfaced by logs-forensics rerun11. Doc-discovery rerun11 re-confirmed. Same fix landed in SPEC.md and ai.txt. Addedexamples/conditional-shapes.iloas the canonical in-context learning example covering all four bool-conditional shapes (?h{true:a;false:b},?h{a}{b},?h a b,?h cond a b).
rsrt fn xsandrsrt fn ctx xsnow documented inSPEC.md,ai.txt,skills/ilo/ilo-builtins.md, and the site builtins reference. The key-function and ctx-arg forms have been implemented since PR #316 (verifier, interpreter, parser, VM bridge eligibility, 9 regression tests,examples/rsrt-by-key.ilo) but were absent from the canonical docs, so agents couldn't discover them without reading source. Surfaced when grepping the four doc surfaces forrsrt fnreturned zero hits.
?h cond a bsilent-truthy bug. A paren-grouped prefix-comparison in cond position (?h (> p 0.5) 1 0) was mis-parsed as a zero-param inline lambda, lifted into a synthetic decl, and silently always took the then-branch. ml-tabular rerun11's logistic-regression classifier scored 25.75% instead of 84.6%; streaming-tail and devops-sre rerun11 hit the same family. Two-layer fix: the parser now requires a;body separator at paren-depth 1 before treating(> ...)as an inline lambda, so(> p 0.5)parses as a grouped comparison; the verifier additionally rejects (ILO-T038) any ternary cond that doesn't type-check tob, catching the broader family (partial-applied fn-refs,R b Ewithout unwrap, etc.). Hint steers to bind-firstc=<expr>;?h c a bor the brace ternary?cond{...}.walk dirandglob dir patnow skip unreadable subdirectories (most commonlychmod 000or sandbox roots) instead of aborting the entire traversal. Previously the first permission-denied subdir would poison the whole walk and lose every readable path that had already been collected, breaking realistic uses likewalk /orwalk ~/. An unreadable root still returns Err so the agent can distinguish "starting point unreadable" from "descendant unreadable". Surfaced by filesystem-walk rerun11.fmtwith a literal template now rejects slot/arg-count mismatches at verify (ILO-T013), with a targeted hint when a list literal is the sole value arg for a multi-slot template. Surfaced by pdf-analyst rerun11:fmt "x={} y={}" [a, b]used to silently producex=[a, b] y={}because the list bound to the first{}and the second slot was left literal.fmtis variadic but does not splat lists - pass positional args, or use a single-slot template if you want the list rendered as one value.- AOT-compiled binaries now bind list-typed
mainparameters (main args:L t,main xs:L n, etc.) the same way the tree-walker / VM / JIT do. Previously the AOT entry shim ran every argv slot through a scalar-only parser, somain args:L t > t; cat args ","silently producednil(cat needed aL t, got at) andlen argsreturned the character count instead of the list length. The verifier passed in both cases, the divergence was runtime-only.generate_mainnow consults the entry function's declared param types and routesL _params through a newilo_aot_parse_arg_listhelper that mirrors the binary-sideparse_cli_args_typedcoercion (parses[a,b,c]literals, bare comma lists, and wraps every other shape as[value]). Surfaced by cli-builder rerun11. - CSV/TSV reader now tracks quote state across record separators. A cell containing
\n(which the writer correctly emits as a quoted multi-line field per RFC 4180) used to be re-parsed as two rows, sord path "csv"silently disagreed withwr path data "csv". The reader is now a single-pass scanner over the whole document and round-trips multi-line quoted fields, embedded quotes, and CRLF line endings byte-stably across tree and VM. Surfaced by csv-pipeline rerun10.
[Records]coverage inai.txtexpanded so the record-declaration syntax (type name{field:type;...}, space-separated constructor, strict vs.?access, nominal-typing rule, Map cross-reference, ILO-T019/T021/T022 verifier surface) is in the compact spec instead of buried in the long form. Surfaced by saas-platform / doc-discovery rerun11, where six agents had to grep the repo fortypebecauseilo help aiwas thin on the topic.skills/ilo/ilo-language.mdnow lists the reserved short-builtin names (2-char and 3-char) plus the 4+ char forward-compatibility rule. Surfaced by rerun11: agents kept bindinglst/hd/rev/rd/splitetc., tripping ILO-P011, because the reserved list lived only inSPEC.mdand never made it into the agent-loadable skill. Tight ~80-token addition, neighbouring sections trimmed to stay inside the 5000-token aggregate skill budget.JSON_OUTPUT.mdupdated to reflect that all six emitters brought into the convention in 0.12.1 (ilo run,ilo graph,ilo --ast,ilo serv,ilo tools --json,ilo spec --json) carryschemaVersion: 1. Audit table drops the "(legacy shape)" notes and adds an "Agent equivalent" column pointingilo replusers atilo servandilo specusers atilo spec ai. Conventions section updated to state that every envelope carriesschemaVersionrather than just new ones.
msetaccumulator via helper fn no longer pays a ~1000x perf cliff. The canonical DRY refactoraddto m k v > mset m k vfollowed bym = addto m k vin a loop now runs at the same speed as the inline form. New OP_MOVE_OWN / OP_CALL_OWN1 opcodes thread the first arg into the helper at the caller's RC, and a tail-position rewrite lets the helper'smset m k vfire the existing in-place fast path. 40k rows: 25.8s to 0.01s on VM; JIT and AOT linear at 1M rows.
- Parser diagnostics (ILO-P001/P003/P004/P005/P007/P009/P011/P013/P016) now render tokens using their source characters (
`>`,`{`,`>>`,identifier `foo`,number `42`) instead of parser-internalTokenKindvariant names (Greater,LBrace,PipeOp,Ident("foo"),Number(42.0)). The old wordingexpected Greater, got PipeOptold an agent nothing about what character it had typed; the newexpected `>`, got `>>`shows the offending bytes directly. Surfaced by agent-repair-loop rerun11.
randalias forrnd(universal short-form for random; matches C / Python / Rust / Go / JS naming). Closes the round-vs-random muscle-memory trap where agents reach forrndexpecting "round" (drop-vowels ofround) and silently get random floats. Canonical for random staysrnd; canonical for rounding staysrou(aliasround).
- VM is the default engine. No need to pass
--run-vm. $is now therunsigil (was the HTTPgetalias). Useget urlfor HTTP.postis nowpst. Drop the vowel like every other I/O verb (rd,wr,srt,flt).
run cmd args > R (M t t) targv-list process spawn. Returns{stdout, stderr, code}on success. Result Err only on spawn failure; non-zero exits land in thecodefield. No shell, no interpolation, so no command-injection vector.ls dir,walk dir,glob dir patfilesystem traversal. Each returnsR (L t) tso missing-dir and permission-denied are typed at the boundary.env-all > R M t treturns the full process environment as a Map, pairs withenv key.ilo run,ilo check,ilo buildverbs. Bare-arg form still works.--jsonon every subcommand. Schemas documented inJSON_OUTPUT.md.--benchJSON output includes anenginefield so you can tell which engine produced the timing numbers.- VS Code extension at
extensions/vscode/. Syntax highlighting, snippets,--comment handling, Cursor install script. - Tree-sitter grammar at
github.com/ilo-lang/tree-sitter-ilo, covers 98% of the example corpus. Wires into Neovim, Helix, Zed. - Modular skill: six pages (
ilo-language,ilo-builtins,ilo-errors,ilo-tools,ilo-engines,ilo-agent) under 5k tokens each. ILO-R015AOT runtime fault diagnostic. Hard faults emit JSON to stderr before the OS reports the exit code.- Engine audit corpus at
tests/engine-matrix/covering every engine on every feature shape. - Closed-loop benchmark harness at
research/closed-loop-bench/. - Memory-model guide at
site/docs/guide/memory-model.md.
- Closure-capturing HOFs (
srt,grp,uniqbywith inline lambdas) run natively on VM and Cranelift JIT. No more tree-bridge fallback. - RC fast paths for sole-owned values on
rev/srt/flt. In-place mutation when no other reference exists. - Error-code namespaces allocated and stable; ranges documented in SPEC.
- AOT sum types compile (cranelift string-constant interning no longer collides across functions).
- AOT default entry resolves to
maininstead of "first declared function", soilo compile file.ilo -o out && ./outworks. - AOT hard faults emit
ILO-R015JSON instead of raw SIGSEGV. - SPEC drift on closure-capture: it was claimed tree-only, but VM and JIT handled it from Phase 2 onward. AOT was the actual lag and is now documented honestly.
- HeapObj::ListView foundation and OP_WINDOW reshape to emit ListView, dropping window-construction RC traffic from O(n·k) to O(n). Bio microbenches went from 4-6s to 0.18-0.49s.
- Inline lambdas Phase 1: parenthesised function literals lift to synthetic top-level decls; closure-capture lands later in 0.12.0.
rgxall1flat-capture form,ctcount-by-predicate builtin.- Bare-bool prefix ternary
?h a b. - Source spans thread through Cranelift JIT runtime-error helpers.
- ILO-P021 rejects the
--Nprefix-binop trap. - EOF parse errors anchor on the dangling token instead of line 1 col 1.
- CLI hyphenated subcommands and non-ident positionals route to
main.
- Cranelift JIT catches panics and falls back to non-JIT engines (handles the AArch64 near-call relocation assertion seen on
rustc 1.85). - HOF tree-bridge error parity on Cranelift.
?bool{a}{b}sugar for prefix ternary, closing a five-release papercut.- Brace-block function bodies accepted; multi-line hint shows both shapes.
x!bare-ident bang fix (v0.11.4 P0 silently returning nil).flt/mapfused over window into a stride-1 in-place loop.
- Fixed Cranelift JIT
srt-after-mapTLS desync silent miscompile. - Restored documented auto-run for
mainand inline programs. OP_LISTAPPENDrebind shape routes through the in-place helper on Cranelift JIT.
- New builtin:
maprfor short-circuit Result propagation acrossmap. padl/padraccept an optional pad-char arg for zero-pad and dot-leader patterns.fmtrejects printf-style format specs ({:...}) at parse time instead of silently returning the literal.sumandavgnow work on VM and Cranelift, not just tree.- Shadow-rebind register aliasing fix on VM and Cranelift.
xs.idesugars toat xs iwheniis a bound variable.- Parser rejects builtin-named binding LHS with ILO-P011, with rename suggestion.
at xs iauto-floors fractional indices.- Lexer decodes
\f \b \v \a \0 \/escape sequences.
- Inline lambdas Phase 1: parenthesised function literals lift to synthetic decls (closure-capture in Phase 2).
- Wire
rgx,rgxall,fmt,rd,rdbthrough VM and Cranelift via tree bridge. lst xs i vpluslsetalias for list-update.chars sbuiltin: explode string into single-char strings.sleep msfor pure-ilo polling tails.frqdrops the type-prefix from output keys, matchinggrpconvention.- O(n²) → O(n)
msetaccumulator via RC=1 in-place HashMap mutation. .?returns nil on missing field, not just on nil object.- Multi-line bodies inside brackets, parens, and
>>pipe chains. hd/tl/atout-of-range errors harmonised across tree, VM, Cranelift.- Entry function returning
Value::Errexits 1.
- CLI runs single-fn files automatically, lists multi-fn files.
--astgates AST dump. ordandchrfor per-char codepoint round-trip.rgxallmulti-match capture-group extraction for HTML scraping.matcharms accept brace-block bodies.- Reserved keywords accepted as field names at dot-access.
- camelCase accepted at post-dot field access.
at s ion text no longer allocates aVecper call.
- Removed the custom ARM64 JIT backend. Cranelift JIT is the optimising path.
- New math builtins:
pow,sqrt,log,exp,sin,cos,tan,log10,log2,atan2. at xs ifor nth-element list access, with Python-style negative indexing.- Builtins as HOF args (verifier + interpreter).
!on Optional types across verifier, interpreter, VM, Cranelift.- Nested generic types like
R (L n) t. - Snake_case field names in dot-access position.
- Prefix-binop expressions accepted as call arguments.
- Scientific-notation float literals.
??accepted as a prefix operator.
- Friendly errors for identifier-confusion cases (similar-name typos suggest the right binding).
- Skill documents the three ways to run ilo from an agent.
- Fixed
slcandmsetsilent miscompilation in loops. - Release workflow publishes
pi-ilo-langto npm. - Skills-ref validate in lint job.
- AOT compilation via
ilo compile. Full AOT opcode parity with the JIT. OP_RECFLD_NAMEimplemented in JIT and AOT.- O(n²) → O(n) list-append; 5x speedup on
foreachaccumulator workloads. - SKILL.md converted to the Agent Skills spec-conformant format.
- Renamed
--run-interpto--run-tree. ai.txttracked as source, drift-checked in CI.
- Space-separated list literals and heterogeneous lists.
_type changes from nil to any/unknown.- Coverage rounds for VM, verifier, interpreter.
- Rust safety review pass: removed problematic unwraps, scoped RAII for
ACTIVE_REGISTRYpointer, debug assertions foras_heap_ref. - Enum-based builtin dispatch.
- 221 new VM tests for interpreter parity.
- Long-form aliases for builtins (e.g.
lengthforlen). - Removed unwraps and unnecessary clones in cleanup pass.
- Interactive REPL with nvim-style commands.
- Full infix operator support.
- npm WASM package for universal installation.
modbuiltin for modulo / remainder.- Guard-in-loop warning (ILO-W001).
- Idiomatic hints system.
==accepted as sugar for=(equality).- And/Or short-circuit fix on left-operand register clobbering.
- P2 data builtins:
grp,flat,sum/avg,rgx.
- Internal refactor release (see GitHub release for diff).
- JIT-arm64 handles mprotect failure.
- MCP stdin error handling no longer panics on partial reads.
- Tools JSON output no longer panics on unwrap.
- First Cranelift JIT compiler pass.
- Bytecode VM lands as a second engine alongside the tree-walker interpreter.
- Type system, verify pass, error codes (the
ILO-XXXXnamespace begins here).
- Builtins expand: collections (
map,flt,fld), text helpers, basic I/O.
- Lexer and parser bug fixes; manifest expanded.
- Initial CLI flag set:
--ai,--tools,--help. - README and SPEC drafts.
Initial public release. Tree-walker interpreter, prefix-notation language, manifesto published. Token-conservative design target set at one-third Python's tokens on the canonical example.