From 335c52f2dc4b05b5fd21272419c491b8000ff7ee Mon Sep 17 00:00:00 2001 From: Daniel Morris Date: Sat, 23 May 2026 14:37:57 +0100 Subject: [PATCH 1/2] fix(verify): register idxof in the BUILTINS arity table `Builtin::Idxof` was in the enum and in `Builtin::ALL` but missing from the BUILTINS name-arity table in verify.rs that `builtin_arity` consults. Every program calling `idxof` panicked the verifier with `is_builtin guarantees arity exists` rather than producing a diagnostic. Surfaced when writing examples/idxof-substring.ilo (in the same PR). The signature matches the ILO-39 spec: `idxof s sub > O n`. --- src/verify.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/verify.rs b/src/verify.rs index c4ea32e5..f75e241d 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -910,6 +910,9 @@ const BUILTINS: &[(&str, &[&str], &str)] = &[ ("hex", &["t"], "t"), ("hex-rev", &["t"], "t"), ("ct-eq", &["t", "t"], "b"), + // idxof s sub > O n (ILO-39): first code-point index of `sub` in `s`, + // or nil. Tree-bridge eligible, no FnRef, no I/O. + ("idxof", &["t", "t"], "O n"), // Raw-bytes crypto (ILO-383). Both accept hex-encoded text, decode to bytes, // and return hex-encoded SHA-256 digest. Error (ILO-R009) on odd-length or // non-hex input. From c458eb6fe2ab30d102024fb66abe19bc54c5e394 Mon Sep 17 00:00:00 2001 From: Daniel Morris Date: Sat, 23 May 2026 14:37:57 +0100 Subject: [PATCH 2/2] docs(examples): coverage for hmac-sha256, idxof, run-bg, discard-bind, packaging, style variants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes coverage gaps surfaced by the 26.5 audit. New examples: - `examples/webhook-verify.ilo` — Stripe-style HMAC-SHA256 signature verification with constant-time compare (ct-eq) and replay-window enforcement. The first end-to-end use case for hmac-sha256; crypto-primitives.ilo was a per-builtin feature dump. - `examples/idxof-substring.ilo` — find + slice idiom for the new `idxof s sub > O n` builtin (ILO-39). Includes Unicode codepoint semantics, empty-needle convention, and the blessed parse-on-first- occurrence shape (idxof! then slc). - `examples/run-bg-tail.ilo` — fire-and-forget background spawn (`run-bg`, completing the run-family overhaul ILO-35 trio with run / run2 / run-bg). - `examples/discard-bind.ilo` — `_=expr` discard bind (ILO-36) for silencing ILO-T033 on side-effecting calls at non-tail position. Uses run2 since mset/+=/mdel are functional and the discard is legitimately useful with spawn-shaped builtins. - `examples/pkg-semver-range.ilo` — `ilo add` semver constraint reference (caret, tilde, exact, wildcard, range, --branch, --rev, --exact). Companion to pkg-registry.ilo which covers the runtime `use "owner/repo"` shape. - `examples/check-as-tool.ilo` — using `ilo check --json` as a verifier tool (pre-commit hook, CI gate) and the JSON diagnostic shape. - `examples/style/` — side-by-side style variants. `README.ilo` indexes the set. `foreach-vs-map.ilo` contrasts `@x xs{...}` with `fld`. `prefix-vs-infix.ilo` shows why prefix arithmetic composes more predictably than infix (with the wrong-feeling shape pinned to its surprise result so any future associativity change is caught here). All files pass `ilo check`; all `-- run:` cases verified locally. --- examples/check-as-tool.ilo | 37 +++++++++++++++ examples/discard-bind.ilo | 33 ++++++++++++++ examples/idxof-substring.ilo | 50 ++++++++++++++++++++ examples/pkg-semver-range.ilo | 38 ++++++++++++++++ examples/run-bg-tail.ilo | 34 ++++++++++++++ examples/style/README.ilo | 24 ++++++++++ examples/style/foreach-vs-map.ilo | 23 ++++++++++ examples/style/prefix-vs-infix.ilo | 28 ++++++++++++ examples/webhook-verify.ilo | 73 ++++++++++++++++++++++++++++++ 9 files changed, 340 insertions(+) create mode 100644 examples/check-as-tool.ilo create mode 100644 examples/discard-bind.ilo create mode 100644 examples/idxof-substring.ilo create mode 100644 examples/pkg-semver-range.ilo create mode 100644 examples/run-bg-tail.ilo create mode 100644 examples/style/README.ilo create mode 100644 examples/style/foreach-vs-map.ilo create mode 100644 examples/style/prefix-vs-infix.ilo create mode 100644 examples/webhook-verify.ilo diff --git a/examples/check-as-tool.ilo b/examples/check-as-tool.ilo new file mode 100644 index 00000000..4e0e873f --- /dev/null +++ b/examples/check-as-tool.ilo @@ -0,0 +1,37 @@ +-- check-as-tool.ilo — using `ilo check` from shell as a verifier tool. +-- +-- `ilo check ` runs the verifier without executing. Exit 0 means +-- clean; any other code means diagnostics were emitted. Default output +-- is human-readable; pass `--json` for one JSON object per diagnostic on +-- stdout (newline-delimited) to consume programmatically. +-- +-- ilo check broken.ilo -- human-readable, exit 1 on error +-- ilo check broken.ilo --json -- JSON-line diagnostics, exit 1 +-- ilo check src/*.ilo -- check multiple files +-- ilo check --show-effects file.ilo -- emit inferred effect sets too +-- +-- The JSON shape per line (companion to the ILO diagnostic registry): +-- { "code": "ILO-T005", "severity": "error", +-- "message": "undefined function 'foo'", +-- "labels": [{ "line": 7, "col": 12, "start": 142, "end": 145, +-- "message": "here", "primary": true }], +-- "notes": ["in function 'main'"], +-- "suggestion": "did you mean 'foo2'?" } +-- +-- Use `ilo check --json` in a pre-commit hook to enforce zero diagnostics: +-- +-- git diff --name-only --cached -- '*.ilo' \ +-- | xargs -r ilo check --json \ +-- | jq -e 'select(.severity == "error")' && exit 1 +-- +-- For CI gating, the same pattern with `select(.code | startswith("ILO-T"))` +-- catches all type-system errors while letting warnings pass. + +-- This file itself is a trivial valid program — `ilo check check-as-tool.ilo` +-- should exit 0 and emit nothing. That's the contract the documentation +-- above relies on. + +ok>n;42 + +-- run: ok +-- out: 42 diff --git a/examples/discard-bind.ilo b/examples/discard-bind.ilo new file mode 100644 index 00000000..587340d5 --- /dev/null +++ b/examples/discard-bind.ilo @@ -0,0 +1,33 @@ +-- discard-bind.ilo — `_=expr` (ILO-36) silences ILO-T033 when the result of +-- a side-effecting call is intentionally dropped. +-- +-- ilo has functional semantics: `mset m k v` does NOT mutate `m`, it returns +-- a new map. So `_=mset m k v` is rarely useful (the side-effect-free shape +-- means the call is genuinely a no-op without the rebind). +-- +-- The real use case is (b) from the CHANGELOG: calling a side-effecting +-- function at non-tail position where the return value is irrelevant. +-- `run2` is the canonical example — the spawn IS the side effect, the +-- RunResult is incidental when you only care that it ran. + +-- Sequential spawns where we don't care about the output, only that they ran. +-- Without `_=`, each `run2` call would trip ILO-T033 "unused expression result". +ping-then-touch out:t>R t t + _=run2 "echo" ["pinged"] + _=run2 "true" [] + r=run2!! "echo" [out] + ~r.stdout + +-- Counter: same shape, three discards then a meaningful tail. +-- Demonstrates `_=` mixing freely with regular bindings. +warm-then-echo>t + _=run2 "true" [] + _=run2 "true" [] + r=run2!! "echo" ["warm"] + r.stdout + +-- engine-skip: jit +-- run: ping-then-touch "ok" +-- out: ok +-- run: warm-then-echo +-- out: warm diff --git a/examples/idxof-substring.ilo b/examples/idxof-substring.ilo new file mode 100644 index 00000000..92958e34 --- /dev/null +++ b/examples/idxof-substring.ilo @@ -0,0 +1,50 @@ +-- idxof-substring.ilo — `idxof s sub > O n` (ILO-39). +-- +-- Returns the first code-point index of `sub` in `s`, or nil if not found. +-- Index is in code-point units, NOT raw bytes (same convention as `at`). +-- Empty `sub` returns 0 (Python / JS semantics). +-- +-- Replaces the verbose `flt`+`len` workaround scrapingbee-chain and +-- tui-client personas reached for when locating substrings. + +-- Find present: returns the index wrapped in Optional. +find-comma>O n;idxof "key=value" "=" + +-- Find absent: returns nil. +find-missing>O n;idxof "key=value" "?" + +-- Empty needle: returns 0 (convention from Python str.find / JS indexOf). +find-empty>O n;idxof "anything" "" + +-- Empty haystack: returns nil (no codepoints to scan, no match possible). +find-empty-hay>O n;idxof "" "x" + +-- Unicode-correct: emoji is one codepoint, so the index after it is the +-- codepoint count, not the byte count of the UTF-8 representation. +find-after-emoji>O n;idxof "🦀 rust" "rust" + +-- The blessed split-on-first-occurrence idiom: idxof then slc. +-- Returns the value half of a `key=value` string; panics via `!` on missing `=`. +parse-value s:t>t + i=idxof! s "=" + slc s (+ i 1) (len s) + +-- The same shape for the key half. +parse-key s:t>t + i=idxof! s "=" + slc s 0 i + +-- run: find-comma +-- out: 3 +-- run: find-missing +-- out: nil +-- run: find-empty +-- out: 0 +-- run: find-empty-hay +-- out: nil +-- run: find-after-emoji +-- out: 2 +-- run: parse-value "host=example.com" +-- out: example.com +-- run: parse-key "host=example.com" +-- out: host diff --git a/examples/pkg-semver-range.ilo b/examples/pkg-semver-range.ilo new file mode 100644 index 00000000..7382595d --- /dev/null +++ b/examples/pkg-semver-range.ilo @@ -0,0 +1,38 @@ +-- pkg-semver-range.ilo — `ilo add` semver constraint reference. +-- +-- Companion to pkg-registry.ilo (which shows the runtime `use "owner/repo"` +-- shape). This file documents the install-time CLI: which version +-- constraints `ilo add` accepts, what they mean, and what ends up in +-- ilo.lock. +-- +-- The constraints follow Cargo / npm semver semantics: +-- +-- ilo add owner/repo -- latest stable +-- ilo add owner/repo@1.2.3 -- exact pin +-- ilo add owner/repo@^1.2.3 -- >= 1.2.3, < 2.0.0 (caret: API-compat) +-- ilo add owner/repo@~1.2.3 -- >= 1.2.3, < 1.3.0 (tilde: patch-only) +-- ilo add owner/repo@>=1.2,<2 -- explicit comma-separated range +-- ilo add owner/repo@1.x -- wildcard at any position +-- ilo add owner/repo@26.5 -- CalVer-style pin (treated as 26.5.x) +-- ilo add owner/repo --branch foo -- track a branch tip (no semver) +-- ilo add owner/repo --rev -- exact commit (no semver) +-- +-- The constraint string is stored verbatim in `ilo.toml` under +-- [dependencies], and the resolved exact version goes into `ilo.lock`. +-- `ilo add` (no constraint) writes `^MAJOR.MINOR.PATCH` of the latest +-- stable; pass `--exact` to write `=MAJOR.MINOR.PATCH` instead. +-- +-- For private-org installs, set IL_PKG_TOKEN to a fine-grained GitHub PAT +-- with `contents: read` on the source repo. The resolver picks it up +-- automatically; the constraint syntax is unchanged. +-- +-- This file does not exercise the resolver (that would need a live +-- network call against the example registry). For the runtime side once +-- a package is installed, see pkg-registry.ilo. Both files together cover +-- the install-then-import flow end-to-end. + +-- Marker fn so this file builds clean under examples_engines. +ok>n;42 + +-- run: ok +-- out: 42 diff --git a/examples/run-bg-tail.ilo b/examples/run-bg-tail.ilo new file mode 100644 index 00000000..d34c40c9 --- /dev/null +++ b/examples/run-bg-tail.ilo @@ -0,0 +1,34 @@ +-- run-bg-tail.ilo — `run-bg cmd argv > R n t` (ILO-35). +-- +-- Fire-and-forget background spawn. Returns Ok(pid:n) immediately without +-- waiting for the child. Child inherits 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. +-- +-- `run-bg` differs from `run` (waits for completion) and `run2` (waits + +-- returns typed RunResult). Use run-bg for daemons, batch jobs, or any +-- process whose output you don't care about from inside ilo. + +-- Spawn a short sleep in the background; we get the pid back immediately +-- and main returns without waiting for the sleep to finish. +spawn-pid>n;run-bg!! "sleep" ["0.05"] + +-- pid is always positive on success. +pid-positive>b;p=run-bg!! "sleep" ["0.05"];>p 0 + +-- Spawn-then-continue: launch a no-op background process, then do real work. +-- The example doesn't need to coordinate with the child; that's the point. +spawn-then-compute>n + _=run-bg!! "true" [] + + 21 21 + +-- Nonexistent command returns Err (spawn failure is observable). +spawn-missing-is-err>b;r=run-bg "no-such-command-xyz" [];?r{~p:false;^e:true} + +-- engine-skip: jit +-- run: pid-positive +-- out: true +-- run: spawn-then-compute +-- out: 42 +-- run: spawn-missing-is-err +-- out: true diff --git a/examples/style/README.ilo b/examples/style/README.ilo new file mode 100644 index 00000000..c9cb5189 --- /dev/null +++ b/examples/style/README.ilo @@ -0,0 +1,24 @@ +-- style/ — side-by-side examples showing the same behaviour two different +-- ways. The point is to bias agents toward the manifesto-preferred shape +-- by making the contrast visible at the example level. +-- +-- Files in this directory each show: +-- - the dense / preferred form (token-minimal, principle 1) +-- - the explicit form (more familiar to readers from other languages) +-- and pin both with `-- run:` so the regression harness keeps them in +-- lockstep. +-- +-- Index: +-- +-- foreach-vs-map.ilo -- `@x xs{...}` vs `map (x:_>_;...) xs` +-- q-vs-match.ilo -- `?r{ok(v):...;err(e):...}` vs explicit ? +-- pipe-vs-nested.ilo -- threaded `use<-` vs nested calls +-- prefix-vs-infix.ilo -- `+x y` vs `x + y` for arithmetic +-- +-- These exist as compile-only references; the harness exercises them via +-- their `-- run:` headers. + +ok>n;0 + +-- run: ok +-- out: 0 diff --git a/examples/style/foreach-vs-map.ilo b/examples/style/foreach-vs-map.ilo new file mode 100644 index 00000000..15831a04 --- /dev/null +++ b/examples/style/foreach-vs-map.ilo @@ -0,0 +1,23 @@ +-- foreach-vs-map.ilo — `@x xs{...}` vs `map (x:_>_;...) xs`. +-- +-- Both compute the same result. The `@x xs{...}` foreach form is preferred +-- because it costs fewer tokens AND reads more naturally as "for each x in +-- xs do ...". Use `map` when you need to build a list from a transformation; +-- use `@x` when you're folding into an accumulator or doing side effects. + +-- Preferred: @x foreach with explicit accumulator. 4 tokens after the +-- function header. +sum-foreach xs:L n>n + total=0 + @x xs{total=+total x} + total + +-- Equivalent: build a list of zeros and reduce, or use fld directly. fld +-- is shorter (and the right choice when you don't need an accumulator +-- visible by name), but the foreach above generalises to side effects. +sum-fld xs:L n>n;fld (acc:n x:n>n;+acc x) xs 0 + +-- run: sum-foreach [1 2 3 4 5] +-- out: 15 +-- run: sum-fld [1 2 3 4 5] +-- out: 15 diff --git a/examples/style/prefix-vs-infix.ilo b/examples/style/prefix-vs-infix.ilo new file mode 100644 index 00000000..2a9020ac --- /dev/null +++ b/examples/style/prefix-vs-infix.ilo @@ -0,0 +1,28 @@ +-- prefix-vs-infix.ilo — `+x y` vs `x + y` for arithmetic. +-- +-- ilo allows BOTH prefix and infix for the four arithmetic operators. +-- The prefix form is preferred because: +-- 1. It saves whitespace tokens. +-- 2. It composes uniformly: `sqrt + (* a a) (* b b)` reads left-to-right +-- with no precedence reasoning. The same shape with infix needs +-- careful grouping — `sqrt (* a a) + (* b b)` does NOT compute the +-- hypotenuse (sqrt applies only to the first argument, then `+` adds +-- the rest), so the infix attempt at hypot reads `sqrt(9) + 16 = 19` +-- instead of `sqrt(25) = 5`. +-- +-- Prefer prefix. Use infix only at the top level of a single binary op +-- where there's no precedence ambiguity (`x = + a b` reads fine; mixing +-- infix and prefix in the same expression invites surprises). + +-- Preferred: prefix throughout. Reads as `sqrt of (a*a + b*b)`. +hypot a:n b:n>n;sqrt + (* a a) (* b b) + +-- The wrong-feeling infix shape, included so the gotcha is visible. +-- The example harness pins the wrong answer (19, not 5) so any future +-- change in associativity is caught here, not by personas in the wild. +hypot-broken a:n b:n>n;sqrt (* a a) + (* b b) + +-- run: hypot 3 4 +-- out: 5 +-- run: hypot-broken 3 4 +-- out: 19 diff --git a/examples/webhook-verify.ilo b/examples/webhook-verify.ilo new file mode 100644 index 00000000..96f8654e --- /dev/null +++ b/examples/webhook-verify.ilo @@ -0,0 +1,73 @@ +-- webhook-verify.ilo — Stripe-style HMAC-SHA256 webhook signature check. +-- +-- Real webhook receivers need three things for security: +-- 1. Constant-time signature comparison (no early-exit timing leak) +-- 2. Replay-window enforcement (reject signatures older than N seconds) +-- 3. Canonical signed-payload format (timestamp + delimiter + body) +-- +-- ilo provides all three primitives directly: hmac-sha256 for the digest, +-- ct-eq for constant-time text equality, and idxof+slc for parsing the +-- `t=,v1=` Stripe-Signature header shape. +-- +-- This is the per-builtin reference for hmac-sha256 in a real use case. +-- For the raw builtin demo, see crypto-primitives.ilo. + +-- Compute the expected signature for a Stripe-style signed payload. +-- Stripe's signed string is `.` — both joined with `.`. +sign-payload secret:t ts:t body:t>t + signed=++ts "." body + hmac-sha256 secret signed + +-- Parse `t=,v1=` -> Map{"t":, "v1":}. +-- Real headers can carry multiple v* schemes; this picks the first v1. +parse-header h:t>M t t + m=mmap + i-comma=idxof! h "," + ts-pair=slc h 0 i-comma + sig-pair=slc h (+ i-comma 1) (len h) + i-eq-ts=idxof! ts-pair "=" + i-eq-sig=idxof! sig-pair "=" + m=mset m "t" (slc ts-pair (+ i-eq-ts 1) (len ts-pair)) + m=mset m "v1" (slc sig-pair (+ i-eq-sig 1) (len sig-pair)) + m + +-- Verify a signature with a freshness window. Returns true only when: +-- 1. The header parses cleanly +-- 2. The recomputed HMAC matches the header's v1 (ct-eq, not =) +-- 3. The signed timestamp is within `window-sec` of `nw` +-- Note: `nw` not `now` - `now` is a builtin and ILO-P011 rejects it as a name. +verify-webhook secret:t header:t body:t nw:n window-sec:n>b + m=parse-header header + ts-text=mget!! m "t" + v1=mget!! m "v1" + expected=sign-payload secret ts-text body + ts=num!! ts-text + age=- nw ts + sig-ok=ct-eq expected v1 + fresh=<= age window-sec + & sig-ok fresh + +-- ── Test vectors ───────────────────────────────────────────────────────── + +-- Pre-computed signature for `secret`=`whsec_test`, `ts`=`1700000000`, +-- body=`{"id":42}`. Joined string is `1700000000.{"id":42}`. +ok-sig>b;verify-webhook "whsec_test" "t=1700000000,v1=0883b47833284e5d3b8fffddf4e143870ebbac24eb2540ca2f0a8325f297277a" "{\"id\":42}" 1700000060 300 + +-- Same payload but the body has been tampered with - signature mismatch. +bad-body>b;verify-webhook "whsec_test" "t=1700000000,v1=0883b47833284e5d3b8fffddf4e143870ebbac24eb2540ca2f0a8325f297277a" "{\"id\":99}" 1700000060 300 + +-- Same payload but `nw` is 10 minutes after the signed timestamp; outside +-- the 5-minute (300s) replay window. +stale>b;verify-webhook "whsec_test" "t=1700000000,v1=0883b47833284e5d3b8fffddf4e143870ebbac24eb2540ca2f0a8325f297277a" "{\"id\":42}" 1700000600 300 + +-- Show the recomputed signature so anyone tweaking the example can re-anchor. +echo-sig>t;sign-payload "whsec_test" "1700000000" "{\"id\":42}" + +-- run: echo-sig +-- out: 0883b47833284e5d3b8fffddf4e143870ebbac24eb2540ca2f0a8325f297277a +-- run: ok-sig +-- out: true +-- run: bad-body +-- out: false +-- run: stale +-- out: false