Skip to content

Latest commit

 

History

History
354 lines (243 loc) · 49.8 KB

File metadata and controls

354 lines (243 loc) · 49.8 KB

Changelog

For the release process and tag conventions, see RELEASING.md.

Unreleased

Added

  • Script mode: implicit main>_; for bare top-level statements (ILO-439). A file no longer needs a main>_; wrapper: bare statements at the top level are collected, in source order, into a synthetic main. prnt +2 2 alone 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) 2 followed by prnt tri 10 on 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 }, foreign let/if/return) still route to the parser's targeted errors; a glued name=expr after a declaration still fires ILO-P102 (its registry text updated); a file with both an explicit main and bare statements is rejected with new ILO-P104; the REPL asks parser::is_script_mode_input so +1 2 still evaluates to 3 instead of reporting defined: 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 10 where 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).

Fixed

  • 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 to Text for anything non-numeric. So tri n:n>n invoked as ilo tri.@ main bound Value::Text("main") to a n parameter: the tree-walker and VM printed NaN, the Cranelift JIT echoed main, 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, where num "main" returns R n t and the type checker forces the failure to be handled. A new CLI-boundary guard (check_cli_arg_types, sibling of the existing check_cli_arity) now rejects unambiguously mismatched values with ILO-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 T still accepts nil, and structural / user-defined types are waved through — so the ILO-182 single-fn pass-through keeps working (ilo greet.@ world against s:t is legitimate usage, not a typo'd function name). Also closes the same hole for b parameters. Found while running the ILO-364 closed-loop benchmark, where it cost a model retries on an otherwise-correct program.

  • get-stream yields 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's ResponseLazy in BufReader::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 consumes ResponseLazy's byte iterator incrementally and emits each line the instant its \n arrives (trailing \r stripped for CRLF / chunked encoding), so each event surfaces promptly. EOF still yields a trailing newline-less partial line; mid-stream errors still surface as ILO-R009 http-stream read error: .... Unblocks ILO-482's previously flaky end-to-end streaming test.

Added

  • ilo httpd lazy streaming response body (ILO-482). A handler's response body field may now be a lazy line iterator (get-stream / pst-stream, for-line stdin) in addition to a plain string or an eager L t list. When the body is a lazy iterator, ilo httpd writes 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 and L t body shapes are unchanged. Unblocks ilo-lang/crew's crew-server GET /events/stream, intended as a held-open tail of data/feed/<day>.jsonl but stuck on a one-shot snapshot while the body buffered. Follow-ups: a tail-file source (lazy tail -f) for the file-tail case, and get-stream's 16 KiB read-buffer granularity for sub-buffer payloads. See docs/streaming.md and examples/httpd-stream.ilo.
  • ilo httpd resolves use imports (ILO-481). Handler files loaded by ilo httpd now have their use imports resolved at startup, relative to the handler's own directory, matching the existing ilo run / ilo check semantics. Previously httpd lexed, parsed, and verified only the single handler file and silently skipped import resolution, so a handler could not use a sibling module - ilo-lang/crew's crew-server had 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. See docs/streaming.md.
  • spawn builtin (ILO-477). spawn fn args... > _ runs fn 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 via Arc::clone(&env.caps), so a worker started under --allow-net / --allow-write keeps the same policy. Unblocks daemon-style programs that need multiple concurrent loops in one process - canonical case is ilo-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. See examples/daemon-loops.ilo.
  • Client-side HTTP streaming (ILO-448). Four new builtins that return a lazy L t line 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-net before opening the connection; mid-stream I/O errors surface as ILO-R009 http-stream read error: .... WASM returns Err. Symmetric counterpart to the server-side ilo httpd + chunked transfer encoding shipped in ILO-46 / ILO-379; unblocks ilo-lang/crew's per-machine agent daemon needing an SSE consumer. Tree + VM only in this release; Cranelift JIT follow-up. See docs/streaming.md and examples/sse-client.ilo.

26.5 — 2026-05-23

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.

Added

  • 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 of run that pipes a text string into the child's stdin. Same no-shell-no-glob security model, same 10 MiB output cap, same code/stdout/stderr result 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 of run2 with the same stdin-pipe mechanic. Returns a typed RunResult record (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. Returns Ok(pid:n) immediately without waiting for the child; child inherits the parent's stdout/stderr and reads /dev/null on 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 through OP_CALL_BUILTIN_TREE without new opcodes. All three are experimental stability.
  • ILO-P102 diagnostic for top-level name=expr bindings outside any function declaration. Catches the "forgot the main>_; 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 bare ILO-P003) or, when a prior name>type;body decl was in scope, slurp the whole chain into that fn's body and emit a wall of misleading ILO-T005 cascades anchored on the wrong line. ILO-P102 collapses both shapes into a single diagnostic that names the offending binding and suggests the main>_; wrapper. Parser-only change; identical output across VM and JIT.

Fixed

  • New ILO-W002 warning when the foreach collection is a direct jpar! or jpar!! call. Surfaces the hint pointing at jpar-list!, which asserts the top-level JSON is an array and returns R (L _) t so the unwrap composes cleanly into @. Catches the mempool-fee-estimator failure mode where the polymorphic jpar Ok type forced the wrapping function's return type to R t t and 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 on Program.parse_failed_fns, and the verifier (1) skips type-checking those functions' bodies (their AST is poison) and (2) emits one collapsed ILO-T005 per 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.

Changed

  • find_libilo_a (AOT linker helper in src/vm/compile_cranelift.rs) now honours CARGO_TARGET_DIR and .cargo/config.toml's build.target-dir before 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 a ln -sf .../release/libilo.a target/release/libilo.a workaround for the AOT tests to find the staticlib. Test-infrastructure only; no user-visible change to ilo compile.

Added

  • _=expr explicit discard bind. Evaluates expr for 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 of mset/+=/mdel is 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 n builtin. Returns the first Unicode code-point index of sub in s, or nil when not found. Index is in code-point units (same convention as at), not raw byte offsets. Empty sub returns 0 (Python / JS semantics). Closes the verbose flt+len workaround 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).

  • \xNN hex escape in string literals. Two hex digits after \x encode a single Unicode code point in U+0000..=U+00FF. Case-insensitive (\x1b and \x1B both produce ESC). Non-hex digits after \x are 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 n builtin. Native matrix-vector product as a flat vector. Replaces the flatten matmul xm (map (y:n>L n;[y]) ys) ceremony every linear-regression-style persona was paying (three lines / ~10 tokens per use). Errors as ILO-R009 on 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 n builtin. Ordinary least squares via the normal equations: returns the coefficient vector b minimising ||xm·b - ys||². Closed-form OLS as a thin wrapper around solve (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 as solve/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_TAILCALL opcode 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 emits OP_TAILCALL instead of OP_CALL + OP_RET. At runtime the VM reuses the current CallFrame rather than pushing a new one, so a function that recurses only in tail position runs in O(1) frame memory: count-down 5_000_000 now completes in tens of milliseconds on --vm, mirroring the tree-interpreter trampoline shipped in the previous PR. Cross-function tail chains (f tail-calls g tail-calls h) work too -- the chunk index is swapped in place. Auto-unwrap (! / !!) calls stay on the normal OP_CALL path because the post-call result probe wants to inspect the value before deciding whether to propagate. The Cranelift JIT/AOT path lowers OP_TAILCALL identically to OP_CALL for now (semantically correct, no host-stack TCO benefit); native return_call lowering ships in a follow-up PR.

  • jpar-list text builtin: parse a JSON string and assert the top-level value is an array. Returns R (L _) t. jpar-list! body unwraps to L _ directly, so @x (jpar-list! body){...} type-checks without a binding or type annotation. jpar is unchanged (returns R _ t); use jpar-list when the response is known to be a JSON array.

  • get-to url timeout-ms > R t t and pst-to url body timeout-ms > R t t builtins. HTTP GET and POST with an explicit per-request timeout in milliseconds. The timeout rounds up to the nearest whole second (minreq granularity). Returns Err when 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 t builtin. 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. Returns Err on 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 t builtin. Like run but returns a typed RunResult record (r.stdout:t, r.stderr:t, r.exit:n) instead of a loose Map. The key difference: exit is a number, not text, so numeric comparisons work directly (=0 r.exit, <0 r.exit). Non-zero exit is NOT an error; Err only on spawn failure (cmd not found, permission denied, etc.). Signal-killed processes on Unix surface as exit:-1. Same no-shell-no-glob security model, same concurrency + 10 MiB output-cap policy as run. Tree-bridge eligible (VM and Cranelift dispatch through the interpreter arm). run is unchanged for compatibility.

  • rgxall-multi pats:L t s:t > L t builtin. Apply multiple patterns to a single string and get one flat list of all hits in pattern order. Per-pattern semantics follow rgxall1: 0 capture groups returns whole matches; 1 capture group returns capture-1 strings; 2+ capture groups errors with a hint to use rgxall. Replaces the verbose flat (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 alongside rgxall1; no new opcodes.

  • fmod a b builtin: floor-mod, always non-negative when b > 0. Equivalent to Python a % b and JS Math.floor((a % b + b) % b). Implemented across VM, JIT, and AOT. Eliminates the (raw + 7) % 7 workaround that every TZ/weekday persona needed with signed mod. mod is unchanged (C-style signed remainder).

  • dtparse-rel s now > R n t builtin. Resolves a natural-language relative-date phrase to a Unix epoch anchored at now. 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/next never return today), and ISO-8601 YYYY-MM-DD passthrough. 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 t and dur-fmt n > t duration builtins. dur-parse parses human-readable duration strings ("3h 30m", "1 week 2 days", "1.5 hours", "90s") into total seconds; lenient, accepts abbreviations s/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 the dur-fmt -> dur-parse round-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-fmt formats 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 that schedule-arithmetic and event-chronology personas were stubbing out manually.

  • File version pragma (optional). Top-of-file sigil ^26.5 declares 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.

0.12.1

Security

  • Added a gitleaks secret-scan gate to the release workflow. Every v* tag push now runs gitleaks/gitleaks-action@v2 against 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.toml at the repo root and covers the placeholder strings used by examples/apps/* (SCRAPINGBEE_KEY_PLACEHOLDER_..., sk-PLACEHOLDER-..., YOUR_*_HERE, REPLACE_ME). Default gitleaks ruleset otherwise. Run locally with gitleaks detect --source . --no-git.

Breaking

  • ls dir renamed to lsd dir. Six rerun10 personas tripped ILO-P011 on ls=rdl! p because ls was reserved; rename frees ls for user code. walk, glob unchanged.
  • --run-tree and its --run alias 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 in is_tree_bridge_eligible. Use --vm (the default) for everything else.
  • ilo tools --json is 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 --json contract — every other emitter (run, graph, --ast, serv, spec --json) gained schemaVersion:1 additively in the same release.

Renamed

  • --run-vm renamed to --vm, symmetric in shape with --jit and --run-llvm (where the flag names the engine, not the action). --run-vm is retained as a hidden alias for one release; every invocation emits a one-shot stderr hint hint: --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-vm keep working through 0.12.x and pick up the nudge to update. Hard removal lands in 0.13.0 with the tree-walker drop.

Diagnostics

  • 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 only rng and rand had per-alias guards; every other long-form alias like head=... 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 single resolve_alias check now covers the full alias table so new aliases land protected automatically. Surfaced by rerun-prompt-generator and changelog-validator rerun12, which both bound head=... and got empty output.

  • schemaVersion: 1 uniform across every CLI --json envelope. Five legacy emitters (ilo run, ilo graph, ilo --ast, ilo serv, ilo tools --json) and the new ilo spec --json mode 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 --json is the lone breaking-but-additive wrap noted above. ilo serv carries schemaVersion:1 on every line, including the ready handshake and every error response (request, lex, parse, verify, runtime, program phases). Full audit in JSON_OUTPUT.md.

  • ilo spec --json [lang|ai] new mode wraps the markdown / ai.txt prose as {"schemaVersion":1,"format":"markdown"|"ai-txt","content":"..."}. Plain-text mode is unchanged.

Added

  • prod xs > n and cprod xs > L n builtins. prod is the multiplicative mirror of sum (product of all elements; empty list returns 1). cprod is the running-product mirror of cumsum (each element i is the product of xs[0..=i]; empty list returns []). Both are fully compiled across tree, VM, JIT, and AOT. The prod [] identity-1 semantic matches the mathematical convention and avoids the ILO-R009 empty-list error that avg [] raises.
  • wra path s write-append builtin. Appends text to the file at path, creating it if it does not exist. Signature R t t mirrors wr; 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 emits open(path, 'a'). Covers the "incremental log / accumulate output across steps" pattern agents reach for next after wr.
  • 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 in examples/apps/fixtures/, so the existing tests/examples_engines.rs harness exercises them on every CI run. The harness now recurses one level into subdirectories. Complements the flat examples/ 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 common map (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.
  • rand alias for rnd (universal short-form for random; matches C/Python/Rust/Go/JS naming). Resolves to canonical rnd after parsing; rejected as binding or user-fn name via ILO-P011 to prevent silent shadow mis-dispatch. random continues to resolve to rnd as before.
  • ilo check --strict flag. Treats every warning-severity diagnostic (ILO-T032 bare fmt, ILO-T033 bare mset/+=/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 with severity: "warning" in the JSON output, only the exit code is elevated. Surfaced by rerun11 ci-gating personas that ran ilo check src/*.ilo in CI and missed mset / fmt traps because the verifier exited 0 on warnings.
  • mget-or m k default > v and lget-or xs i default > a. Defaulted lookups for Map and List that return the element type directly, no O v to coalesce, no OOB error for lget-or. The verifier enforces that the default matches the container's element/value type so the return shape is v / a, never O v. Both lower through the tree-bridge, so every engine inherits semantics without new opcodes. Closes the manifesto-friction (mget m k) ?? d and i<len?at xs i:d ceremony agents kept reaching for.
  • argmax xs > n, argmin xs > n, argsort xs > L n. Index-returning aggregates with numpy naming. argmax returns the 0-based index of the maximum element (first occurrence wins on ties); argmin the same for minimum; argsort returns the stable sorted-index permutation ascending (smallest to largest, empty list returns []). All three error on empty input except argsort. All lower through the tree-bridge, so VM and Cranelift inherit them without new opcodes. Closes the srt fn (enumerate xs) + extract-first pattern agents converged on for argmax/argmin-style queries.
  • dirname path > t, basename path > t, pathjoin parts:L t > t path-manipulation builtins. POSIX semantics with Unix forward-slash separator (Windows backslash handling deferred to 0.13.0). dirname returns "" (not ".") for plain filenames so pathjoin [dirname p basename p] round-trips without injecting a phantom ./ prefix. pathjoin is 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-builtin cat (slc (spl p "/") 0 -1) "/" dance every filesystem persona was paying.
  • rdin > R t t and rdinl > R (L t) t. Stdin read primitives. rdin reads all of stdin as text; rdinl reads 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-754 f64 value. Tree-bridge-eligible, so VM and Cranelift JIT/AOT inherit with no new opcodes; Python codegen emits math.pi / math.tau / math.e. Stops agents hardcoding 3.14159... or reconstructing pi via * 2 (atan2 0 -1) - both shapes surfaced in fft-peak rerun12. Note: because e is now a builtin name, any existing code using e as a local binding will get an ILO-P011 diagnostic on upgrade; rename to ev, er, or similar.
  • default-on-err r d > T builtin. Unwraps R T E to T, returning d if the result is Err. The Result mirror of ?? (nil-coalesce for O 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 not R T E (hint steers at ?? only when the first arg is Optional, avoiding misleading steers for plain n/t/b first 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 to default-on-err). T041 is intentionally suppressed when the lhs type is Unknown (e.g. type-variable params, _-typed values) to avoid false positives on generic code; regression-tested.

Diagnostics

  • ILO-T039: 0-arg user function used as a bare reference in value position. When an agent writes v = my-fn or +my-fn 100 and my-fn takes no arguments, ilo now emits ILO-T039 with a hint pointing at my-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 its lset alias) now carries a suggestion clarifying that lst xs i v is "list set at index" (3 args, returns a new list with index i replaced by v), not "last element". The 1-arg case (lst xs) points at the canonical at xs -1 for 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 for lst xs and hit an empty-suggestion arity error.
  • ILO-T013 on cat "a" "b" (the string-concat instinct from Python/JS) now suggests fmt "{}{}" a b or +a b as the canonical text-concat shapes. cat is 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.

Diagnostics

  • if reserved-word hint now suggests ?expr{true:...;false:...} (semicolon-separated arms) instead of the space-separated ?expr{true:... false:...} shape, which the parser rejected with ILO-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. Added examples/conditional-shapes.ilo as 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).

Fixed (docs)

  • rsrt fn xs and rsrt fn ctx xs now documented in SPEC.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 for rsrt fn returned zero hits.

Fixed

  • ?h cond a b silent-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 to b, catching the broader family (partial-applied fn-refs, R b E without unwrap, etc.). Hint steers to bind-first c=<expr>;?h c a b or the brace ternary ?cond{...}.
  • walk dir and glob dir pat now skip unreadable subdirectories (most commonly chmod 000 or 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 like walk / or walk ~/. An unreadable root still returns Err so the agent can distinguish "starting point unreadable" from "descendant unreadable". Surfaced by filesystem-walk rerun11.
  • fmt with 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 produce x=[a, b] y={} because the list bound to the first {} and the second slot was left literal. fmt is 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 main parameters (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, so main args:L t > t; cat args "," silently produced nil (cat needed a L t, got a t) and len args returned the character count instead of the list length. The verifier passed in both cases, the divergence was runtime-only. generate_main now consults the entry function's declared param types and routes L _ params through a new ilo_aot_parse_arg_list helper that mirrors the binary-side parse_cli_args_typed coercion (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, so rd path "csv" silently disagreed with wr 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.

Docs

  • [Records] coverage in ai.txt expanded 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 for type because ilo help ai was thin on the topic.
  • skills/ilo/ilo-language.md now lists the reserved short-builtin names (2-char and 3-char) plus the 4+ char forward-compatibility rule. Surfaced by rerun11: agents kept binding lst/hd/rev/rd/split etc., tripping ILO-P011, because the reserved list lived only in SPEC.md and 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.md updated 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) carry schemaVersion: 1. Audit table drops the "(legacy shape)" notes and adds an "Agent equivalent" column pointing ilo repl users at ilo serv and ilo spec users at ilo spec ai. Conventions section updated to state that every envelope carries schemaVersion rather than just new ones.

Performance

  • mset accumulator via helper fn no longer pays a ~1000x perf cliff. The canonical DRY refactor addto m k v > mset m k v followed by m = addto m k v in 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's mset m k v fire the existing in-place fast path. 40k rows: 25.8s to 0.01s on VM; JIT and AOT linear at 1M rows.

Diagnostics

  • 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-internal TokenKind variant names (Greater, LBrace, PipeOp, Ident("foo"), Number(42.0)). The old wording expected Greater, got PipeOp told an agent nothing about what character it had typed; the new expected `>`, got `>>` shows the offending bytes directly. Surfaced by agent-repair-loop rerun11.

Added

  • rand alias for rnd (universal short-form for random; matches C / Python / Rust / Go / JS naming). Closes the round-vs-random muscle-memory trap where agents reach for rnd expecting "round" (drop-vowels of round) and silently get random floats. Canonical for random stays rnd; canonical for rounding stays rou (alias round).

0.12.0 - 2026-05-19

Breaking

  • VM is the default engine. No need to pass --run-vm.
  • $ is now the run sigil (was the HTTP get alias). Use get url for HTTP.
  • post is now pst. Drop the vowel like every other I/O verb (rd, wr, srt, flt).

Added

  • run cmd args > R (M t t) t argv-list process spawn. Returns {stdout, stderr, code} on success. Result Err only on spawn failure; non-zero exits land in the code field. No shell, no interpolation, so no command-injection vector.
  • ls dir, walk dir, glob dir pat filesystem traversal. Each returns R (L t) t so missing-dir and permission-denied are typed at the boundary.
  • env-all > R M t t returns the full process environment as a Map, pairs with env key.
  • ilo run, ilo check, ilo build verbs. Bare-arg form still works.
  • --json on every subcommand. Schemas documented in JSON_OUTPUT.md.
  • --bench JSON output includes an engine field 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-R015 AOT 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.

Changed

  • Closure-capturing HOFs (srt, grp, uniqby with 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.

Fixed

  • AOT sum types compile (cranelift string-constant interning no longer collides across functions).
  • AOT default entry resolves to main instead of "first declared function", so ilo compile file.ilo -o out && ./out works.
  • AOT hard faults emit ILO-R015 JSON 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.

0.11.6 - 2026-05-17

  • 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.
  • rgxall1 flat-capture form, ct count-by-predicate builtin.
  • Bare-bool prefix ternary ?h a b.
  • Source spans thread through Cranelift JIT runtime-error helpers.
  • ILO-P021 rejects the --N prefix-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.

0.11.5 - 2026-05-16

  • 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/map fused over window into a stride-1 in-place loop.

0.11.4 - 2026-05-16

  • Fixed Cranelift JIT srt-after-map TLS desync silent miscompile.
  • Restored documented auto-run for main and inline programs.
  • OP_LISTAPPEND rebind shape routes through the in-place helper on Cranelift JIT.

0.11.3 - 2026-05-16

  • New builtin: mapr for short-circuit Result propagation across map.
  • padl / padr accept an optional pad-char arg for zero-pad and dot-leader patterns.
  • fmt rejects printf-style format specs ({:...}) at parse time instead of silently returning the literal.
  • sum and avg now work on VM and Cranelift, not just tree.
  • Shadow-rebind register aliasing fix on VM and Cranelift.
  • xs.i desugars to at xs i when i is a bound variable.
  • Parser rejects builtin-named binding LHS with ILO-P011, with rename suggestion.
  • at xs i auto-floors fractional indices.
  • Lexer decodes \f \b \v \a \0 \/ escape sequences.

0.11.2 - 2026-05-15

  • Inline lambdas Phase 1: parenthesised function literals lift to synthetic decls (closure-capture in Phase 2).
  • Wire rgx, rgxall, fmt, rd, rdb through VM and Cranelift via tree bridge.
  • lst xs i v plus lset alias for list-update.
  • chars s builtin: explode string into single-char strings.
  • sleep ms for pure-ilo polling tails.
  • frq drops the type-prefix from output keys, matching grp convention.
  • O(n²) → O(n) mset accumulator 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/at out-of-range errors harmonised across tree, VM, Cranelift.
  • Entry function returning Value::Err exits 1.

0.11.1 - 2026-05-13

  • CLI runs single-fn files automatically, lists multi-fn files. --ast gates AST dump.
  • ord and chr for per-char codepoint round-trip.
  • rgxall multi-match capture-group extraction for HTML scraping.
  • match arms accept brace-block bodies.
  • Reserved keywords accepted as field names at dot-access.
  • camelCase accepted at post-dot field access.
  • at s i on text no longer allocates a Vec per call.

0.11.0 - 2026-05-13

  • 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 i for 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.

0.10.3 - 2026-05-11

  • Friendly errors for identifier-confusion cases (similar-name typos suggest the right binding).
  • Skill documents the three ways to run ilo from an agent.

0.10.2 - 2026-05-11

  • Fixed slc and mset silent miscompilation in loops.
  • Release workflow publishes pi-ilo-lang to npm.
  • Skills-ref validate in lint job.

0.10.1 - 2026-05-02

  • AOT compilation via ilo compile. Full AOT opcode parity with the JIT.
  • OP_RECFLD_NAME implemented in JIT and AOT.
  • O(n²) → O(n) list-append; 5x speedup on foreach accumulator workloads.
  • SKILL.md converted to the Agent Skills spec-conformant format.
  • Renamed --run-interp to --run-tree.
  • ai.txt tracked as source, drift-checked in CI.

0.10.0 - 2026-03-08

  • Space-separated list literals and heterogeneous lists.
  • _ type changes from nil to any/unknown.
  • Coverage rounds for VM, verifier, interpreter.

0.9.1 - 2026-03-08

  • Rust safety review pass: removed problematic unwraps, scoped RAII for ACTIVE_REGISTRY pointer, debug assertions for as_heap_ref.
  • Enum-based builtin dispatch.
  • 221 new VM tests for interpreter parity.

0.9.0 - 2026-03-07

  • Long-form aliases for builtins (e.g. length for len).
  • Removed unwraps and unnecessary clones in cleanup pass.

0.8.2 - 2026-03-07

  • Interactive REPL with nvim-style commands.

0.8.1 - 2026-03-07

  • Full infix operator support.
  • npm WASM package for universal installation.
  • mod builtin 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.

0.8.0 - 2026-03-07

  • P2 data builtins: grp, flat, sum/avg, rgx.

0.7.0 - 2026-03-06

0.6.0 - 2026-03-06

  • JIT-arm64 handles mprotect failure.
  • MCP stdin error handling no longer panics on partial reads.
  • Tools JSON output no longer panics on unwrap.

0.5.0 - 2026-03-04

  • First Cranelift JIT compiler pass.

0.4.0 - 2026-03-03

  • Bytecode VM lands as a second engine alongside the tree-walker interpreter.

0.3.0 - 2026-03-01

  • Type system, verify pass, error codes (the ILO-XXXX namespace begins here).

0.2.0 - 2026-03-01

  • Builtins expand: collections (map, flt, fld), text helpers, basic I/O.

0.1.2 - 2026-02-27

  • Lexer and parser bug fixes; manifest expanded.

0.1.1 - 2026-02-27

  • Initial CLI flag set: --ai, --tools, --help.
  • README and SPEC drafts.

0.1.0 - 2026-02-27

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.