diff --git a/SPEC.md b/SPEC.md index edae65da..05f60133 100644 --- a/SPEC.md +++ b/SPEC.md @@ -420,6 +420,7 @@ Called like functions, compiled to dedicated opcodes. | `flt fn xs` | keep elements where `fn x` is true | `L a` | | `fld fn xs init` | left fold: `fn (fn (fn init x0) x1) ...` | accumulator | | `flatmap fn xs` | map then flatten one level | `L b` | +| `mapr fn xs` | map with short-circuit Result propagation: collects Ok values, returns first Err | `R (L b) e` | | `partition fn xs` | split list into `[passing, failing]` by predicate | `L (L a)` | | `chunks n xs` | non-overlapping chunks of size `n` (final chunk may be shorter) | `L (L a)` | | `window n xs` | sliding windows of size `n` (drops trailing partial; empty if n > len) | `L (L a)` | diff --git a/ai.txt b/ai.txt index 01d96996..5134107b 100644 --- a/ai.txt +++ b/ai.txt @@ -5,7 +5,7 @@ NAMING: Short names everywhere. 1–3 chars. `order`=`ord`=truncate `customers`= COMMENTS: -- full line comment +a b -- end of line comment -- no multi-line comments; use consecutive -- lines -- like this Single-line only. `--` to end of line. No multi-line comment syntax — newlines are a human display concern, not a language concern. An entire ilo program can be one line. Use consecutive `--` lines when humans need multi-line comments. Stripped at the lexer level before parsing — comments produce no AST nodes and cost zero runtime tokens. Generating `--` costs 1 LLM token, so comments are essentially free. **Gotcha:** `--x 1` is a comment, not "negate (x minus 1)". The lexer matches `--` greedily as a comment and eats the rest of the line. To negate a subtraction, use a space or bind first: -- DON'T: --x 1 (comment, not negate-subtract) -- DO: - -x 1 (space separates the two minus operators) -- DO: r=-x 1;-r (bind first) OPERATORS: Both prefix and infix notation are supported. **Prefix is preferred** — it is the token-optimal form that eliminates parentheses and produces denser code. Infix is available for readability when needed. [Binary] `+a b`=`a + b`=add / concat / list concat=`n`, `t`, `L` `+=a v`=append to list=`L` `-a b`=`a - b`=subtract=`n` `*a b`=`a * b`=multiply=`n` `/a b`=`a / b`=divide=`n` `=a b`=`a == b`=equal (prefix `=` is preferred; `==a b` also accepted)=any `!=a b`=`a != b`=not equal=any `>a b`=`a > b`=greater than=`n`, `t` `=a b`=`a >= b`=greater or equal=`n`, `t` `<=a b`=`a <= b`=less or equal=`n`, `t` `&a b`=`a & b`=logical AND (short-circuit)=any (truthy) `|a b`=`a | b`=logical OR (short-circuit)=any (truthy) [Unary] `-x`=negate=`n` `!x`=logical NOT=any (truthy) [Special infix] `a??b`=nil-coalesce (if a is nil, return b)=any `a>>f`=pipe (desugar to `f(a)`)=any [Prefix nesting (no parens needed)] +*a b c -- (a * b) + c *a +b c -- a * (b + c) >=+x y 100 -- (x + y) >= 100 -*a b *c d -- (a * b) - (c * d) The outer prefix op binds the inner prefix subexpression as its **left** operand, regardless of operator precedence. With two same-precedence ops side by side this is easy to misread: */a b c -- (a/b) * c ← NOT (a*b)/c /*a b c -- (a*b) / c ← NOT (a/b)*c +-a b c -- (a-b) + c ← NOT (a+b)-c -+a b c -- (a+b) - c ← NOT (a-b)+c The runtime emits a `hint:` diagnostic when one of these four pairs appears at a prefix position, since the parse order disagrees with the natural left-to-right reading. To force the other grouping, swap the ops or bind the inner result first: -- Want (a*b)/c with a=6, b=2, c=3: r=*a b;/r c -- bind, then divide → 4 /*a b c -- equivalent, swapping the prefix-pair order [Infix precedence] Standard mathematical precedence (higher binds tighter): 6=`*` `/` 5=`+` `-` `+=` 4=`>` `<` `>=` `<=` 3=`=` `!=` 2=`&` 1=`|` Function application binds tighter than all infix operators: f a + b -- (f a) + b, NOT f(a + b) x * y + 1 -- (x * y) + 1 (x + y) * 2 -- parens override precedence Each nested prefix operator saves 2 tokens (no `(` `)` needed). Flat prefix like `+a b` saves 1 char vs `a + b`. Across 25 expression patterns, prefix notation saves **22% tokens** and **42% characters** vs infix. See [research/explorations/prefix-vs-infix/](research/explorations/prefix-vs-infix/) for the full benchmark. Disambiguation: `-` followed by one atom is unary negate, followed by two atoms is binary subtract. [Operands] Operator operands are **atoms** (literals, refs, field access) or **nested prefix operators**. Function calls are NOT operands — bind call results to a variable first: -- DON'T: *n fac p → parses as Multiply(n, fac) with p dangling -- DO: r=fac p;*n r **Negative literals vs binary minus**: the lexer greedily includes a leading `-` into number tokens. `-1`, `-7`, `-0` are all number literals. To subtract from zero, use a space: `- 0 v` (Minus token, then `0`, then `v`). f v:n>n;-0 v -- WRONG: -0 is Number(-0.0); v is a stray token f v:n>n;- 0 v -- OK: binary subtract: 0 - v = -v STRING LITERALS: Text values are written in double quotes. Escape sequences: `\n`=newline (0x0A) `\t`=tab (0x09) `\r`=carriage return (0x0D) `\f`=form feed (0x0C, PDF page separator) `\b`=backspace (0x08) `\v`=vertical tab (0x0B) `\a`=bell (0x07) `\0`=null (0x00) `\"`=literal double quote `\\`=literal backslash `\/`=literal forward slash (JSON passthrough) Unknown escapes (e.g. `\z`) preserve the backslash + char verbatim. "hello\nworld" -- two-line string "col1\tcol2" -- tab-separated spl text "\n" -- split file content into lines spl pdf "\f" -- split pdftotext output into pages -BUILTINS: Called like functions, compiled to dedicated opcodes. `len x`=length of string (bytes) or list (elements)=`n` `str n`=number to text (integers format without `.0`)=`t` `num t`=text to number (Err if unparseable)=`R n t` `abs n`=absolute value=`n` `min a b`=minimum of two numbers=`n` `max a b`=maximum of two numbers=`n` `mod a b`=remainder (modulo); errors on zero divisor=`n` `flr n`=floor (round toward negative infinity)=`n` `cel n`=ceiling (round toward positive infinity)=`n` `rnd`=random float in [0, 1)=`n` `rnd a b`=random integer in [a, b] (inclusive)=`n` `now`=current Unix timestamp (seconds)=`n` `get url`=HTTP GET=`R t t` `get url headers`=HTTP GET with custom headers (`M t t` map)=`R t t` `post url body`=HTTP POST with text body=`R t t` `post url body headers`=HTTP POST with body and custom headers (`M t t` map)=`R t t` `env key`=read environment variable=`R t t` `rd path`=read file; format auto-detected from extension (`.csv`/`.tsv`→grid, `.json`→graph, else text)=`R _ t` `rd path fmt`=read file with explicit format override (`"csv"`, `"tsv"`, `"json"`, `"raw"`)=`R _ t` `rdl path`=read file as list of lines=`R (L t) t` `rdb s fmt`=parse string/buffer in given format — for data from HTTP, env vars, etc.=`R _ t` `wr path s`=write text to file (overwrite)=`R t t` `wr path data "csv"`=write list-of-lists as CSV (with proper quoting)=`R t t` `wr path data "tsv"`=write list-of-lists as TSV=`R t t` `wr path data "json"`=write any value as pretty JSON=`R t t` `wrl path xs`=write list of lines to file (joins with `\n`)=`R t t` `trm s`=trim leading and trailing whitespace=`t` `spl t sep`=split text by separator=`L t` `fmt tmpl args…`=format string — `{}` placeholders filled left-to-right=`t` `cat xs sep`=join list of text with separator=`t` `has xs v`=membership test (list: element, text: substring)=`b` `hd xs`=head (first element/char) of list or text=element / `t` `tl xs`=tail (all but first) of list or text=`L` / `t` `rev xs`=reverse list or text=same type `srt xs`=sort list (all-number or all-text) or text chars=same type `srt fn xs`=sort list by key function (returns number or text key)=`L` `unq xs`=remove duplicates, preserve order (list or text chars)=same type `slc xs a b`=slice list or text from index a to b (a, b accept negative indices counting from end; bounds clamp)=same type `jpth json path`=JSON path lookup (dot-separated keys, array indices)=`R t t` `jdmp value`=serialise ilo value to JSON text=`t` `prnt value`=print value to stdout, return it unchanged (passthrough)=same type `jpar text`=parse JSON text into ilo values=`R _ t` `grp fn xs`=group list by key function=`M t (L a)` `flat xs`=flatten one level of nesting=`L a` `sum xs`=sum of numeric list (0 for empty)=`n` `avg xs`=mean of numeric list (error if empty)=`n` `rgx pat s`=regex: no groups→all matches; groups→first match captures=`L t` `mmap`=create empty map=`M t _` `mget m k`=value at key k (nil if missing)=element or nil `mset m k v`=new map with key k set to v=`M k v` `mhas m k`=true if key exists=`b` `mkeys m`=sorted list of keys=`L t` `mvals m`=values sorted by key=`L v` `mdel m k`=new map with key k removed=`M k v` `at xs i`=i-th element of list or text (0-indexed; negative counts from end)=element `lst xs i v`=new list with index `i` set to `v` (list update; alias: `lset`)=`L a` `take n xs`=first `n` elements/chars of list or text (n>=0 truncates if n>len; n<0 keeps all but the last `abs n`, Python `xs[:n]`)=same type `drop n xs`=skip first `n` elements/chars (n>=0 returns the rest; n<0 keeps only the last `abs n`, Python `xs[n:]`)=same type `rsrt xs`=sort descending (list or text chars)=same type `uniqby fn xs`=dedupe by key function (first occurrence wins)=`L a` `zip xs ys`=pairwise pairs of two lists; truncates to shorter input=`L (L _)` `enumerate xs`=pair each element with its index → `[[i, v], ...]`=`L (L _)` `range a b`=half-open numeric range `[a, a+1, ..., b-1]`; empty when `a >= b`=`L n` `map fn xs`=apply `fn` to each element=`L b` `flt fn xs`=keep elements where `fn x` is true=`L a` `fld fn xs init`=left fold: `fn (fn (fn init x0) x1) ...`=accumulator `flatmap fn xs`=map then flatten one level=`L b` `partition fn xs`=split list into `[passing, failing]` by predicate=`L (L a)` `chunks n xs`=non-overlapping chunks of size `n` (final chunk may be shorter)=`L (L a)` `window n xs`=sliding windows of size `n` (drops trailing partial; empty if n > len)=`L (L a)` `clamp x lo hi`=restrict `x` to `[lo, hi]` (lower bound wins when `lo > hi`)=`n` `cumsum xs`=running sum; output length matches input=`L n` `frq xs`=frequency map of elements (keys are bare stringified values)=`M t n` `median xs`=median of numeric list=`n` `quantile xs p`=sample quantile (linear interp; `p` clamped to `[0, 1]`)=`n` `stdev xs`=sample standard deviation (divides by N-1)=`n` `variance xs`=sample variance (divides by N-1)=`n` `setunion a b`=set union of two lists (deduped, sorted output)=`L a` `setinter a b`=set intersection (deduped, sorted)=`L a` `setdiff a b`=set difference `a - b` (deduped, sorted)=`L a` `chars s`=explode a string into single-char strings (one per Unicode scalar)=`L t` `ord s`=Unicode codepoint of the first character of `s`=`n` `chr n`=single-character string for codepoint `n`=`t` `upr s`=uppercase (ASCII)=`t` `lwr s`=lowercase (ASCII)=`t` `cap s`=capitalise first char (ASCII)=`t` `padl s w`=left-pad to width `w` with spaces (no-op if already wider)=`t` `padr s w`=right-pad to width `w` with spaces (no-op if already wider)=`t` `rgxall pat s`=every regex match as `L (L t)` (no-group: each match in a 1-elem list)=`L (L t)` `rgxsub pat repl s`=regex substitute all matches; `$1`, `$2`, ... reference capture groups=`t` `dtfmt epoch fmt`=format Unix epoch as text (strftime, UTC)=`R t t` `dtparse s fmt`=parse text to Unix epoch (strftime, UTC)=`R n t` `rdjl path`=read JSONL file as `L (R _ t)`: one parse result per non-empty line=`L (R _ t)` `get-many urls`=concurrent HTTP GET fan-out (max 10 parallel), preserves order=`L (R t t)` `sleep ms`=pause current engine for `ms` milliseconds; returns nil=`_` `rou n`=round to nearest integer (banker's rounding)=`n` `rndn mu sigma`=one sample from normal distribution `N(mu, sigma)` (Box-Muller)=`n` `pow b e`=`b` raised to power `e`=`n` `sqrt n`=square root=`n` `exp n`=natural exponent `e^n`=`n` `log n`=natural logarithm=`n` `log10 n`=base-10 logarithm=`n` `log2 n`=base-2 logarithm=`n` `sin n`=sine (radians)=`n` `cos n`=cosine (radians)=`n` `tan n`=tangent (radians)=`n` `atan2 y x`=two-argument arctangent (y, x order; radians)=`n` `transpose m`=transpose row-major matrix=`L (L n)` `matmul a b`=matrix product=`L (L n)` `dot a b`=vector dot product=`n` `solve a b`=solve `Ax = b` via LU with partial pivoting; errors on singular/non-square=`L n` `inv a`=matrix inverse; errors on singular/non-square=`L (L n)` `det a`=determinant; errors on non-square=`n` `fft xs`=discrete FFT: real samples → `L [re, im]`; zero-padded to next power of 2=`L (L n)` `ifft pairs`=inverse FFT; imaginary part dropped on return=`L n` `fmt2 x digits`=format number `x` to `digits` decimal places (half-to-even rounding; `digits` clamped to `0..=20`). Compose with `fmt` for template + precision: `fmt "x={}" (fmt2 v 2)`=`t` [Datetime (`dtfmt` / `dtparse`)] UTC only. Format strings follow strftime conventions (`%Y-%m-%d %H:%M:%S`, `%s`, etc). dtfmt 1700000000 "%Y-%m-%d" -- R t t: Ok="2023-11-14", Err if out of range dtparse "2024-01-15" "%Y-%m-%d" -- R n t: Ok=epoch seconds, Err if unparseable dtfmt! e "%H:%M:%S" -- auto-unwrap inside R-returning fn [Set operations] `setunion`, `setinter`, `setdiff` operate on lists of `t`, `n`, or `b` (same constraint as `uniqby`). Output is deduped and sorted by a type-prefixed string key, so results are deterministic across runs and engines. Sort is lexicographic on the key, not numeric — re-sort with `srt` afterwards if you need numeric order. [Linear algebra] `transpose`, `matmul`, `dot`, `solve`, `inv`, `det` operate on row-major matrices (`L (L n)`) and flat vectors (`L n`). `solve`, `inv`, `det` use LU decomposition with partial pivoting and raise on singular or non-square inputs. These ship as host-vetted builtins because hand-rolled implementations risk silent precision loss. [FFT] `fft xs` runs an iterative Cooley-Tukey radix-2 transform on real samples, zero-padding to the next power of two. Output is `L [re, im]` with one inner pair per frequency bin. `ifft pairs` is the inverse, dropping the imaginary part on return. [Builtin aliases] All builtins accept long-form names that resolve to the canonical short form after parsing. Using a long form triggers a hint suggesting the short form. This lets newcomers write readable code while learning the canonical names. `floor`=→=`flr` `ceil`=→=`cel` `round`=→=`rou` `random`=→=`rnd` `lset`=→=`lst` `regex_all`=→=`rgxall` `regex_sub`=→=`rgxsub` `string`=→=`str` `number`=→=`num` `length`=→=`len` `head`=→=`hd` `tail`=→=`tl` `reverse`=→=`rev` `sort`=→=`srt` `slice`=→=`slc` `unique`=→=`unq` `filter`=→=`flt` `fold`=→=`fld` `flatten`=→=`flat` `concat`=→=`cat` `contains`=→=`has` `group`=→=`grp` `average`=→=`avg` `print`=→=`prnt` `trim`=→=`trm` `split`=→=`spl` `format`=→=`fmt` `regex`=→=`rgx` `read`=→=`rd` `readlines`=→=`rdl` `readbuf`=→=`rdb` `write`=→=`wr` `writelines`=→=`wrl` length xs -- works, but emits: hint: `length` → `len` (canonical short form) len xs -- canonical — no hint `get` and `post` return `Ok(body)` on success, `Err(message)` on failure (connection error, timeout, DNS failure, etc). `$` is a terse alias for `get`: get url -- R t t: Ok=response body, Err=error message $url -- same as get url get! url -- auto-unwrap: Ok→body, Err→propagate to caller $!url -- same as get! url post url body -- R t t: HTTP POST with text body post url body headers -- R t t: HTTP POST with body and custom headers -- Custom headers: build an M t t map with mmap/mset h=mmap h=mset h "x-api-key" "secret" r=get url h -- GET with x-api-key header r=post url body h -- POST with x-api-key header Behind the `http` feature flag (on by default). Without the feature, `get`/`post` return `Err("http feature not enabled")`. `env` reads an environment variable by name, returning `Ok(value)` or `Err("env var 'KEY' not set")`: env key -- R t t: Ok=value, Err=not set message env! key -- auto-unwrap: Ok→value, Err→propagate to caller [JSON builtins] `jpth` extracts a value from a JSON string by dot-separated path. Array elements are accessed by numeric index: jpth json "name" -- R t t: Ok=extracted value as text, Err=error jpth json "user.name" -- nested path lookup jpth json "items.0.name" -- array index access jpth! json "name" -- auto-unwrap `jdmp` serialises any ilo value to a JSON string: jdmp 42 -- "42" jdmp "hello" -- "\"hello\"" jdmp [1 2 3] -- "[1,2,3]" jdmp (pt x:1 y:2) -- "{\"x\":1,\"y\":2}" `jpar` parses a JSON string into ilo values. JSON objects become records with type name `json`, arrays become lists, strings/numbers/bools/null map directly: jpar text -- R _ t: Ok=parsed value, Err=parse error r=jpar! "{\"x\":1}" -- r is a json record, access with r.x +BUILTINS: Called like functions, compiled to dedicated opcodes. `len x`=length of string (bytes) or list (elements)=`n` `str n`=number to text (integers format without `.0`)=`t` `num t`=text to number (Err if unparseable)=`R n t` `abs n`=absolute value=`n` `min a b`=minimum of two numbers=`n` `max a b`=maximum of two numbers=`n` `mod a b`=remainder (modulo); errors on zero divisor=`n` `flr n`=floor (round toward negative infinity)=`n` `cel n`=ceiling (round toward positive infinity)=`n` `rnd`=random float in [0, 1)=`n` `rnd a b`=random integer in [a, b] (inclusive)=`n` `now`=current Unix timestamp (seconds)=`n` `get url`=HTTP GET=`R t t` `get url headers`=HTTP GET with custom headers (`M t t` map)=`R t t` `post url body`=HTTP POST with text body=`R t t` `post url body headers`=HTTP POST with body and custom headers (`M t t` map)=`R t t` `env key`=read environment variable=`R t t` `rd path`=read file; format auto-detected from extension (`.csv`/`.tsv`→grid, `.json`→graph, else text)=`R _ t` `rd path fmt`=read file with explicit format override (`"csv"`, `"tsv"`, `"json"`, `"raw"`)=`R _ t` `rdl path`=read file as list of lines=`R (L t) t` `rdb s fmt`=parse string/buffer in given format — for data from HTTP, env vars, etc.=`R _ t` `wr path s`=write text to file (overwrite)=`R t t` `wr path data "csv"`=write list-of-lists as CSV (with proper quoting)=`R t t` `wr path data "tsv"`=write list-of-lists as TSV=`R t t` `wr path data "json"`=write any value as pretty JSON=`R t t` `wrl path xs`=write list of lines to file (joins with `\n`)=`R t t` `trm s`=trim leading and trailing whitespace=`t` `spl t sep`=split text by separator=`L t` `fmt tmpl args…`=format string — `{}` placeholders filled left-to-right=`t` `cat xs sep`=join list of text with separator=`t` `has xs v`=membership test (list: element, text: substring)=`b` `hd xs`=head (first element/char) of list or text=element / `t` `tl xs`=tail (all but first) of list or text=`L` / `t` `rev xs`=reverse list or text=same type `srt xs`=sort list (all-number or all-text) or text chars=same type `srt fn xs`=sort list by key function (returns number or text key)=`L` `unq xs`=remove duplicates, preserve order (list or text chars)=same type `slc xs a b`=slice list or text from index a to b (a, b accept negative indices counting from end; bounds clamp)=same type `jpth json path`=JSON path lookup (dot-separated keys, array indices)=`R t t` `jdmp value`=serialise ilo value to JSON text=`t` `prnt value`=print value to stdout, return it unchanged (passthrough)=same type `jpar text`=parse JSON text into ilo values=`R _ t` `grp fn xs`=group list by key function=`M t (L a)` `flat xs`=flatten one level of nesting=`L a` `sum xs`=sum of numeric list (0 for empty)=`n` `avg xs`=mean of numeric list (error if empty)=`n` `rgx pat s`=regex: no groups→all matches; groups→first match captures=`L t` `mmap`=create empty map=`M t _` `mget m k`=value at key k (nil if missing)=element or nil `mset m k v`=new map with key k set to v=`M k v` `mhas m k`=true if key exists=`b` `mkeys m`=sorted list of keys=`L t` `mvals m`=values sorted by key=`L v` `mdel m k`=new map with key k removed=`M k v` `at xs i`=i-th element of list or text (0-indexed; negative counts from end)=element `lst xs i v`=new list with index `i` set to `v` (list update; alias: `lset`)=`L a` `take n xs`=first `n` elements/chars of list or text (n>=0 truncates if n>len; n<0 keeps all but the last `abs n`, Python `xs[:n]`)=same type `drop n xs`=skip first `n` elements/chars (n>=0 returns the rest; n<0 keeps only the last `abs n`, Python `xs[n:]`)=same type `rsrt xs`=sort descending (list or text chars)=same type `uniqby fn xs`=dedupe by key function (first occurrence wins)=`L a` `zip xs ys`=pairwise pairs of two lists; truncates to shorter input=`L (L _)` `enumerate xs`=pair each element with its index → `[[i, v], ...]`=`L (L _)` `range a b`=half-open numeric range `[a, a+1, ..., b-1]`; empty when `a >= b`=`L n` `map fn xs`=apply `fn` to each element=`L b` `flt fn xs`=keep elements where `fn x` is true=`L a` `fld fn xs init`=left fold: `fn (fn (fn init x0) x1) ...`=accumulator `flatmap fn xs`=map then flatten one level=`L b` `mapr fn xs`=map with short-circuit Result propagation: collects Ok values, returns first Err=`R (L b) e` `partition fn xs`=split list into `[passing, failing]` by predicate=`L (L a)` `chunks n xs`=non-overlapping chunks of size `n` (final chunk may be shorter)=`L (L a)` `window n xs`=sliding windows of size `n` (drops trailing partial; empty if n > len)=`L (L a)` `clamp x lo hi`=restrict `x` to `[lo, hi]` (lower bound wins when `lo > hi`)=`n` `cumsum xs`=running sum; output length matches input=`L n` `frq xs`=frequency map of elements (keys are bare stringified values)=`M t n` `median xs`=median of numeric list=`n` `quantile xs p`=sample quantile (linear interp; `p` clamped to `[0, 1]`)=`n` `stdev xs`=sample standard deviation (divides by N-1)=`n` `variance xs`=sample variance (divides by N-1)=`n` `setunion a b`=set union of two lists (deduped, sorted output)=`L a` `setinter a b`=set intersection (deduped, sorted)=`L a` `setdiff a b`=set difference `a - b` (deduped, sorted)=`L a` `chars s`=explode a string into single-char strings (one per Unicode scalar)=`L t` `ord s`=Unicode codepoint of the first character of `s`=`n` `chr n`=single-character string for codepoint `n`=`t` `upr s`=uppercase (ASCII)=`t` `lwr s`=lowercase (ASCII)=`t` `cap s`=capitalise first char (ASCII)=`t` `padl s w`=left-pad to width `w` with spaces (no-op if already wider)=`t` `padr s w`=right-pad to width `w` with spaces (no-op if already wider)=`t` `rgxall pat s`=every regex match as `L (L t)` (no-group: each match in a 1-elem list)=`L (L t)` `rgxsub pat repl s`=regex substitute all matches; `$1`, `$2`, ... reference capture groups=`t` `dtfmt epoch fmt`=format Unix epoch as text (strftime, UTC)=`R t t` `dtparse s fmt`=parse text to Unix epoch (strftime, UTC)=`R n t` `rdjl path`=read JSONL file as `L (R _ t)`: one parse result per non-empty line=`L (R _ t)` `get-many urls`=concurrent HTTP GET fan-out (max 10 parallel), preserves order=`L (R t t)` `sleep ms`=pause current engine for `ms` milliseconds; returns nil=`_` `rou n`=round to nearest integer (banker's rounding)=`n` `rndn mu sigma`=one sample from normal distribution `N(mu, sigma)` (Box-Muller)=`n` `pow b e`=`b` raised to power `e`=`n` `sqrt n`=square root=`n` `exp n`=natural exponent `e^n`=`n` `log n`=natural logarithm=`n` `log10 n`=base-10 logarithm=`n` `log2 n`=base-2 logarithm=`n` `sin n`=sine (radians)=`n` `cos n`=cosine (radians)=`n` `tan n`=tangent (radians)=`n` `atan2 y x`=two-argument arctangent (y, x order; radians)=`n` `transpose m`=transpose row-major matrix=`L (L n)` `matmul a b`=matrix product=`L (L n)` `dot a b`=vector dot product=`n` `solve a b`=solve `Ax = b` via LU with partial pivoting; errors on singular/non-square=`L n` `inv a`=matrix inverse; errors on singular/non-square=`L (L n)` `det a`=determinant; errors on non-square=`n` `fft xs`=discrete FFT: real samples → `L [re, im]`; zero-padded to next power of 2=`L (L n)` `ifft pairs`=inverse FFT; imaginary part dropped on return=`L n` `fmt2 x digits`=format number `x` to `digits` decimal places (half-to-even rounding; `digits` clamped to `0..=20`). Compose with `fmt` for template + precision: `fmt "x={}" (fmt2 v 2)`=`t` [Datetime (`dtfmt` / `dtparse`)] UTC only. Format strings follow strftime conventions (`%Y-%m-%d %H:%M:%S`, `%s`, etc). dtfmt 1700000000 "%Y-%m-%d" -- R t t: Ok="2023-11-14", Err if out of range dtparse "2024-01-15" "%Y-%m-%d" -- R n t: Ok=epoch seconds, Err if unparseable dtfmt! e "%H:%M:%S" -- auto-unwrap inside R-returning fn [Set operations] `setunion`, `setinter`, `setdiff` operate on lists of `t`, `n`, or `b` (same constraint as `uniqby`). Output is deduped and sorted by a type-prefixed string key, so results are deterministic across runs and engines. Sort is lexicographic on the key, not numeric — re-sort with `srt` afterwards if you need numeric order. [Linear algebra] `transpose`, `matmul`, `dot`, `solve`, `inv`, `det` operate on row-major matrices (`L (L n)`) and flat vectors (`L n`). `solve`, `inv`, `det` use LU decomposition with partial pivoting and raise on singular or non-square inputs. These ship as host-vetted builtins because hand-rolled implementations risk silent precision loss. [FFT] `fft xs` runs an iterative Cooley-Tukey radix-2 transform on real samples, zero-padding to the next power of two. Output is `L [re, im]` with one inner pair per frequency bin. `ifft pairs` is the inverse, dropping the imaginary part on return. [Builtin aliases] All builtins accept long-form names that resolve to the canonical short form after parsing. Using a long form triggers a hint suggesting the short form. This lets newcomers write readable code while learning the canonical names. `floor`=→=`flr` `ceil`=→=`cel` `round`=→=`rou` `random`=→=`rnd` `lset`=→=`lst` `regex_all`=→=`rgxall` `regex_sub`=→=`rgxsub` `string`=→=`str` `number`=→=`num` `length`=→=`len` `head`=→=`hd` `tail`=→=`tl` `reverse`=→=`rev` `sort`=→=`srt` `slice`=→=`slc` `unique`=→=`unq` `filter`=→=`flt` `fold`=→=`fld` `flatten`=→=`flat` `concat`=→=`cat` `contains`=→=`has` `group`=→=`grp` `average`=→=`avg` `print`=→=`prnt` `trim`=→=`trm` `split`=→=`spl` `format`=→=`fmt` `regex`=→=`rgx` `read`=→=`rd` `readlines`=→=`rdl` `readbuf`=→=`rdb` `write`=→=`wr` `writelines`=→=`wrl` length xs -- works, but emits: hint: `length` → `len` (canonical short form) len xs -- canonical — no hint `get` and `post` return `Ok(body)` on success, `Err(message)` on failure (connection error, timeout, DNS failure, etc). `$` is a terse alias for `get`: get url -- R t t: Ok=response body, Err=error message $url -- same as get url get! url -- auto-unwrap: Ok→body, Err→propagate to caller $!url -- same as get! url post url body -- R t t: HTTP POST with text body post url body headers -- R t t: HTTP POST with body and custom headers -- Custom headers: build an M t t map with mmap/mset h=mmap h=mset h "x-api-key" "secret" r=get url h -- GET with x-api-key header r=post url body h -- POST with x-api-key header Behind the `http` feature flag (on by default). Without the feature, `get`/`post` return `Err("http feature not enabled")`. `env` reads an environment variable by name, returning `Ok(value)` or `Err("env var 'KEY' not set")`: env key -- R t t: Ok=value, Err=not set message env! key -- auto-unwrap: Ok→value, Err→propagate to caller [JSON builtins] `jpth` extracts a value from a JSON string by dot-separated path. Array elements are accessed by numeric index: jpth json "name" -- R t t: Ok=extracted value as text, Err=error jpth json "user.name" -- nested path lookup jpth json "items.0.name" -- array index access jpth! json "name" -- auto-unwrap `jdmp` serialises any ilo value to a JSON string: jdmp 42 -- "42" jdmp "hello" -- "\"hello\"" jdmp [1 2 3] -- "[1,2,3]" jdmp (pt x:1 y:2) -- "{\"x\":1,\"y\":2}" `jpar` parses a JSON string into ilo values. JSON objects become records with type name `json`, arrays become lists, strings/numbers/bools/null map directly: jpar text -- R _ t: Ok=parsed value, Err=parse error r=jpar! "{\"x\":1}" -- r is a json record, access with r.x LISTS: xs=[1 2 3] -- space-separated (preferred) xs=[1, 2, 3] -- commas also work mixed=["search" 10] -- heterogeneous lists allowed (type: L _) w="world" words=["hi" w] -- variables work in list literals empty=[] Elements are expressions in brackets, separated by spaces or commas. Variables and expressions are allowed as elements. Lists may contain mixed types (inferred as `L _`). Use with `@` to iterate: @x xs{+x 1} Index by integer literal or variable (dot notation): xs.0 # first element (literal index) xs.2 # third element (literal index) xs.i # i-th element when `i` is a bound variable in scope The variable-index form `xs.i` is sugar for `at xs i` — the parser builds a field-access node and a post-parse desugar pass rewrites it whenever the field identifier resolves to a binding in scope (parameter, let, foreach, range, match-arm). Record field access keeps working: if the identifier is also a declared field on any record type in the program, the rewrite is skipped and the strict `.field` semantics apply. **CLI list arguments:** Pass lists from the command line with commas (brackets also accepted): ilo 'f xs:L n>n;len xs' 1,2,3 → 3 ilo 'f xs:L t>t;xs.0' 'a,b,c' → a STATEMENTS: Guards and conditionals replace `if`/`else if`/`else`. They are flat statements — no nesting, no closing braces to match. There are three forms: **Braceless guard** (`cond expr`): early return — if condition is true, returns the expression from the function. **Braced conditional** (`cond{body}`): conditional execution — if condition is true, body runs but execution continues (no early return). Use `ret` inside the body for explicit early return. **Ternary** (`cond{then}{else}`): value expression — evaluates then or else branch, no early return. Multiple braceless guards chain vertically for guard clauses, keeping indentation depth constant. Match replaces `switch`. There is no fall-through — each arm is independent. The `_` arm is the default catch-all. `x=expr`=bind `cond{body}`=conditional execution: run body if cond true (no early return) `cond expr`=braceless guard: early return expr if cond true `cond{then}{else}`=ternary: evaluate then or else (no early return) `?cond then else`=prefix ternary: `?=x 0 10 20` (no early return) `!cond{body}`=negated conditional execution (no early return) `!cond expr`=braceless negated guard (early return) `!cond{then}{else}`=negated ternary `?x{arms}`=match named value `?{arms}`=match last result `@v list{body}`=iterate list `@i a..b{body}`=range iteration: i from a (inclusive) to b (exclusive) `ret expr`=early return from function `~expr`=return ok `^expr`=return err `func! args`=call + auto-unwrap Result, propagate Err to caller `func!! args`=call + auto-unwrap Result, abort on Err with exit 1 `wh cond{body}`=while loop `brk` / `brk expr`=exit enclosing loop (optional value) `cnt`=skip to next iteration of enclosing loop `expr>>func`=pipe: pass result as last arg to func MATCH ARMS: `"gold":body`=literal text `42:body`=literal number `~v:body`=ok — bind inner value to `v` `^e:body`=err — bind inner value to `e` `n v:body`=number — branch if value is a number, bind to `v` `t v:body`=text — branch if value is text, bind to `v` `b v:body`=bool — branch if value is a bool, bind to `v` `l v:body`=list — branch if value is a list, bind to `v` `_:body`=wildcard, binds matched subject to `_` Arms separated by `;`. First match wins. In any binding position the name `_` is permitted and binds normally — `~_:body`, `^_:body`, `n _:body` etc. expose the matched inner value to `body` under the name `_`. Bodies that don't reference `_` are unaffected. cls sp:n>t;>=sp 1000 "gold";>=sp 500 "silver";"bronze" [Braceless Guards (Early Return)] When the guard condition is a comparison or logical operator (`>=`, `<=`, `>`, `<`, `=`, `!=`, `&`, `|`) and the body is a single expression, braces are optional. **Braceless guards cause early return from the function:** cls sp:n>t;>=sp 1000 "gold";>=sp 500 "silver";"bronze" Negated braceless guards also work: `!<=n 0 ^"must be positive"`. **Comparison operators always start a guard at statement position.** You cannot use `=`, `<`, `>`, `<=`, `>=` etc. as a standalone return expression — the parser treats them as a guard condition and expects a following return value. To return a comparison result, bind it first: -- WRONG: r=has xs v;=r true -- =r true is parsed as a guard, not a return expression -- OK: r=has xs v;r -- return the bool directly (only safe as the last statement) -- OK: has xs v -- bare call is safe as last statement in last function [Braced Conditionals (No Early Return)] A braced guard `cond{body}` is **conditional execution** — the body runs if the condition is true, but execution always continues to the next statement (no early return): f x:n>n;>x 0{99};+x 1 -- {99} runs when x>0 but is discarded; always returns +x 1 This makes braced conditionals natural in loops: f xs:L n>n;m=0;@x xs{>x m{m=x}};m -- find max: update m when x > m Use `ret` inside a braced conditional for explicit early return: f x:n>n;>x 0{ret x};-x -- return x early if positive, else negate > **Common footgun.** `=cond{val}` reads like "if cond, return val" but it isn't. The braces are conditional execution: `val` is evaluated, discarded, and execution falls through to the next statement. If you want early return, use the braceless form `=cond val` (when val is a single expression) or wrap with `ret` inside the braces: `=cond{ret val}`. > > ``` > f x:n>n;=x 1{99};0 -- f 1 → 0 (99 is discarded, falls through) > f x:n>n;=x 1 99;0 -- f 1 → 99 (braceless guard: early return) > f x:n>n;=x 1{ret 99};0 -- f 1 → 99 (explicit ret inside braces) > ``` [Ternary (Guard-Else)] A guard followed by a second brace block becomes a ternary — it produces a value without early return: f x:n>t;=x 1{"yes"}{"no"} Like braced conditionals, ternary does **not** return from the function. Code after the ternary continues executing: f x:n>n;=x 0{10}{20};+x 1 -- always returns x+1, ternary value is discarded Negated ternary: `!=x 1{"not one"}{"one"}`. **Prefix ternary** uses `?` with a comparison operator for a fully prefix-style conditional: f x:n>n;?=x 0 10 20 -- if x==0 then 10 else 20 f x:n>n;v=?>x 100 1 0;v -- assign result to v The condition must start with a comparison operator (`=`, `>`, `<`, `>=`, `<=`, `!=`). [Early Return] `ret expr` explicitly returns from the current function: f x:n>n;>x 0{ret x};0 -- return x early if positive, else 0 f xs:L n>n;@x xs{>=x 10{ret x}};0 -- return first element >= 10 Braceless guards provide early return for simple cases. Use `ret` inside braced conditionals when you need early return with more complex logic or inside loops. [Range Iteration] `@i a..b{body}` iterates `i` from `a` (inclusive) to `b` (exclusive). Both bounds can be atoms, prefix-op expressions, or function calls. The index variable is a fresh binding per iteration; other variables in the body update the enclosing scope: f>n;s=0;@i 0..5{s=+s i};s -- sum 0+1+2+3+4 = 10 f>n;xs=[];@i 0..3{xs=+=xs i};xs -- [0, 1, 2] f n:n>n;s=0;@i 0..n{s=+s i};s -- dynamic end bound g xs:L n>n;s=0;@j 0..len xs{s=+s j};s -- call-form bound h i:n n:n>L n;xs=[];@j +i 2..n{xs=+=xs j};xs -- prefix-op bound [While Loop] `wh cond{body}` loops while condition is truthy: f>n;i=0;s=0;wh n;i=0;wh true{i=+i 1;>=i 3{ret i}};0 -- ret inside braced guard: early return from loop Variable rebinding inside loops updates the existing variable rather than creating a new binding. [Break and Continue] `brk` exits the enclosing `wh` or `@` loop. `cnt` skips to the next iteration: f>n;i=0;wh true{i=+i 1;>=i 3{brk}};i -- i = 3 f>n;i=0;s=0;wh =i 3{cnt};s=+s i};s -- s = 3 (skips i>=3) `brk expr` provides an optional value (currently discarded — the loop result is the last body value before the break). Both `brk` and `cnt` work inside braced conditionals within loops. Using them outside a loop is a compile-time error (no-op in current implementation). [Pipe Operator] `>>` chains calls by passing the left side as the last argument to the right side: str x>>len -- desugars to: len (str x) add x 1>>add 2 -- desugars to: add 2 (add x 1) f x>>g>>h -- desugars to: h (g (f x)) Pipes desugar at parse time — no new AST node. Works with `!` for auto-unwrap: `f x>>g!>>h`. [Safe Field Navigation] `.?` is the tolerant field accessor. It returns nil whenever the access can't yield a real value, instead of erroring: object is nil → nil object is a present record but the field is missing → nil object is not a record at all (list, text, number) → nil user.?name -- nil if user is nil, else user.name (or nil if absent) user.?addr.?city -- chained: nil propagates through chain x.?name??"unknown" -- combine with ?? for defaults r.?optMetric.?v40 -- heterogeneous JSON (jpar): optional fields stay nil Strict `.field` access still errors on missing fields, so typo detection on user-defined record types survives at verify time (ILO-T019) and at runtime (ILO-R005). Use `.field` when you want the strictness, `.?field` when the field is optional or the record shape is dynamic. [Nil-Coalesce Operator] `??` evaluates the left side; if nil, evaluates and returns the right side: x??42 -- if x is nil, returns 42 a??b??99 -- chained: first non-nil wins, else 99 mk 0??"default" -- works with function results Compiled via `OP_JMPNN` (jump if not nil) — right side is only evaluated when left is nil. Use braces when the body has multiple statements: >=sp 1000{a=classify sp;a} ?r{^e:^+"failed: "e;~v:v} diff --git a/examples/mapr.ilo b/examples/mapr.ilo new file mode 100644 index 00000000..78b99b39 --- /dev/null +++ b/examples/mapr.ilo @@ -0,0 +1,43 @@ +-- mapr fn xs : R (L b) e — short-circuiting Result-aware map. +-- +-- For each item, fn must return R b e. Encounter an Ok value (~v) and +-- mapr accumulates v; encounter an Err value (^e) and the whole call +-- returns ^e immediately, skipping the rest of the list. Pair with `!` +-- to thread the err up into a Result-returning caller without per-item +-- match boilerplate. +-- +-- Before mapr, the persona idiom was: +-- ton s:t>n;r=num s;?r{~v:v;^_:0} -- swallow-err helper, ~30 tokens +-- ns=map ton xs -- and the error is lost +-- mapr replaces both with three tokens and preserves the err shape: +-- ns=mapr num xs -- R (L n) t + +-- Happy path: every string parses cleanly, returns ~[1,2,3]. +ok>R (L n) t;mapr num ["1","2","3"] + +-- Short-circuit on the first Err: parsing "bad" surfaces ^bad and the +-- trailing "3" is never visited. Matches Rust's +-- collect::, _>>() semantics. +bail>R (L n) t;mapr num ["1","bad","3"] + +-- Pair with `!` to keep a fallible pipeline flat. Caller's return type +-- must accept Err so the propagation is well-typed. +total xs:L t>R n t;ns=mapr! num xs;~len ns + +-- Empty input is the trivial Ok case. +none>R (L n) t;mapr num [] + +-- run: ok +-- out: [1, 2, 3] + +-- run: bail +-- err: ^bad + +-- run: total ["1","2","3"] +-- out: 3 + +-- run: total ["1","bad","3"] +-- err: ^bad + +-- run: none +-- out: [] diff --git a/skills/ilo/SKILL.md b/skills/ilo/SKILL.md index ef1a4ccd..40b6c7d7 100644 --- a/skills/ilo/SKILL.md +++ b/skills/ilo/SKILL.md @@ -44,7 +44,7 @@ NAMING: Short names everywhere. 1–3 chars. `order`=`ord`=truncate `customers`= COMMENTS: -- full line comment +a b -- end of line comment -- no multi-line comments; use consecutive -- lines -- like this Single-line only. `--` to end of line. No multi-line comment syntax — newlines are a human display concern, not a language concern. An entire ilo program can be one line. Use consecutive `--` lines when humans need multi-line comments. Stripped at the lexer level before parsing — comments produce no AST nodes and cost zero runtime tokens. Generating `--` costs 1 LLM token, so comments are essentially free. **Gotcha:** `--x 1` is a comment, not "negate (x minus 1)". The lexer matches `--` greedily as a comment and eats the rest of the line. To negate a subtraction, use a space or bind first: -- DON'T: --x 1 (comment, not negate-subtract) -- DO: - -x 1 (space separates the two minus operators) -- DO: r=-x 1;-r (bind first) OPERATORS: Both prefix and infix notation are supported. **Prefix is preferred** — it is the token-optimal form that eliminates parentheses and produces denser code. Infix is available for readability when needed. [Binary] `+a b`=`a + b`=add / concat / list concat=`n`, `t`, `L` `+=a v`=append to list=`L` `-a b`=`a - b`=subtract=`n` `*a b`=`a * b`=multiply=`n` `/a b`=`a / b`=divide=`n` `=a b`=`a == b`=equal (prefix `=` is preferred; `==a b` also accepted)=any `!=a b`=`a != b`=not equal=any `>a b`=`a > b`=greater than=`n`, `t` `=a b`=`a >= b`=greater or equal=`n`, `t` `<=a b`=`a <= b`=less or equal=`n`, `t` `&a b`=`a & b`=logical AND (short-circuit)=any (truthy) `|a b`=`a | b`=logical OR (short-circuit)=any (truthy) [Unary] `-x`=negate=`n` `!x`=logical NOT=any (truthy) [Special infix] `a??b`=nil-coalesce (if a is nil, return b)=any `a>>f`=pipe (desugar to `f(a)`)=any [Prefix nesting (no parens needed)] +*a b c -- (a * b) + c *a +b c -- a * (b + c) >=+x y 100 -- (x + y) >= 100 -*a b *c d -- (a * b) - (c * d) The outer prefix op binds the inner prefix subexpression as its **left** operand, regardless of operator precedence. With two same-precedence ops side by side this is easy to misread: */a b c -- (a/b) * c ← NOT (a*b)/c /*a b c -- (a*b) / c ← NOT (a/b)*c +-a b c -- (a-b) + c ← NOT (a+b)-c -+a b c -- (a+b) - c ← NOT (a-b)+c The runtime emits a `hint:` diagnostic when one of these four pairs appears at a prefix position, since the parse order disagrees with the natural left-to-right reading. To force the other grouping, swap the ops or bind the inner result first: -- Want (a*b)/c with a=6, b=2, c=3: r=*a b;/r c -- bind, then divide → 4 /*a b c -- equivalent, swapping the prefix-pair order [Infix precedence] Standard mathematical precedence (higher binds tighter): 6=`*` `/` 5=`+` `-` `+=` 4=`>` `<` `>=` `<=` 3=`=` `!=` 2=`&` 1=`|` Function application binds tighter than all infix operators: f a + b -- (f a) + b, NOT f(a + b) x * y + 1 -- (x * y) + 1 (x + y) * 2 -- parens override precedence Each nested prefix operator saves 2 tokens (no `(` `)` needed). Flat prefix like `+a b` saves 1 char vs `a + b`. Across 25 expression patterns, prefix notation saves **22% tokens** and **42% characters** vs infix. See [research/explorations/prefix-vs-infix/](research/explorations/prefix-vs-infix/) for the full benchmark. Disambiguation: `-` followed by one atom is unary negate, followed by two atoms is binary subtract. [Operands] Operator operands are **atoms** (literals, refs, field access) or **nested prefix operators**. Function calls are NOT operands — bind call results to a variable first: -- DON'T: *n fac p → parses as Multiply(n, fac) with p dangling -- DO: r=fac p;*n r **Negative literals vs binary minus**: the lexer greedily includes a leading `-` into number tokens. `-1`, `-7`, `-0` are all number literals. To subtract from zero, use a space: `- 0 v` (Minus token, then `0`, then `v`). f v:n>n;-0 v -- WRONG: -0 is Number(-0.0); v is a stray token f v:n>n;- 0 v -- OK: binary subtract: 0 - v = -v STRING LITERALS: Text values are written in double quotes. Escape sequences: `\n`=newline (0x0A) `\t`=tab (0x09) `\r`=carriage return (0x0D) `\f`=form feed (0x0C, PDF page separator) `\b`=backspace (0x08) `\v`=vertical tab (0x0B) `\a`=bell (0x07) `\0`=null (0x00) `\"`=literal double quote `\\`=literal backslash `\/`=literal forward slash (JSON passthrough) Unknown escapes (e.g. `\z`) preserve the backslash + char verbatim. "hello\nworld" -- two-line string "col1\tcol2" -- tab-separated spl text "\n" -- split file content into lines spl pdf "\f" -- split pdftotext output into pages -BUILTINS: Called like functions, compiled to dedicated opcodes. `len x`=length of string (bytes) or list (elements)=`n` `str n`=number to text (integers format without `.0`)=`t` `num t`=text to number (Err if unparseable)=`R n t` `abs n`=absolute value=`n` `min a b`=minimum of two numbers=`n` `max a b`=maximum of two numbers=`n` `mod a b`=remainder (modulo); errors on zero divisor=`n` `flr n`=floor (round toward negative infinity)=`n` `cel n`=ceiling (round toward positive infinity)=`n` `rnd`=random float in [0, 1)=`n` `rnd a b`=random integer in [a, b] (inclusive)=`n` `now`=current Unix timestamp (seconds)=`n` `get url`=HTTP GET=`R t t` `get url headers`=HTTP GET with custom headers (`M t t` map)=`R t t` `post url body`=HTTP POST with text body=`R t t` `post url body headers`=HTTP POST with body and custom headers (`M t t` map)=`R t t` `env key`=read environment variable=`R t t` `rd path`=read file; format auto-detected from extension (`.csv`/`.tsv`→grid, `.json`→graph, else text)=`R _ t` `rd path fmt`=read file with explicit format override (`"csv"`, `"tsv"`, `"json"`, `"raw"`)=`R _ t` `rdl path`=read file as list of lines=`R (L t) t` `rdb s fmt`=parse string/buffer in given format — for data from HTTP, env vars, etc.=`R _ t` `wr path s`=write text to file (overwrite)=`R t t` `wr path data "csv"`=write list-of-lists as CSV (with proper quoting)=`R t t` `wr path data "tsv"`=write list-of-lists as TSV=`R t t` `wr path data "json"`=write any value as pretty JSON=`R t t` `wrl path xs`=write list of lines to file (joins with `\n`)=`R t t` `trm s`=trim leading and trailing whitespace=`t` `spl t sep`=split text by separator=`L t` `fmt tmpl args…`=format string — `{}` placeholders filled left-to-right=`t` `cat xs sep`=join list of text with separator=`t` `has xs v`=membership test (list: element, text: substring)=`b` `hd xs`=head (first element/char) of list or text=element / `t` `tl xs`=tail (all but first) of list or text=`L` / `t` `rev xs`=reverse list or text=same type `srt xs`=sort list (all-number or all-text) or text chars=same type `srt fn xs`=sort list by key function (returns number or text key)=`L` `unq xs`=remove duplicates, preserve order (list or text chars)=same type `slc xs a b`=slice list or text from index a to b (a, b accept negative indices counting from end; bounds clamp)=same type `jpth json path`=JSON path lookup (dot-separated keys, array indices)=`R t t` `jdmp value`=serialise ilo value to JSON text=`t` `prnt value`=print value to stdout, return it unchanged (passthrough)=same type `jpar text`=parse JSON text into ilo values=`R _ t` `grp fn xs`=group list by key function=`M t (L a)` `flat xs`=flatten one level of nesting=`L a` `sum xs`=sum of numeric list (0 for empty)=`n` `avg xs`=mean of numeric list (error if empty)=`n` `rgx pat s`=regex: no groups→all matches; groups→first match captures=`L t` `mmap`=create empty map=`M t _` `mget m k`=value at key k (nil if missing)=element or nil `mset m k v`=new map with key k set to v=`M k v` `mhas m k`=true if key exists=`b` `mkeys m`=sorted list of keys=`L t` `mvals m`=values sorted by key=`L v` `mdel m k`=new map with key k removed=`M k v` `at xs i`=i-th element of list or text (0-indexed; negative counts from end)=element `lst xs i v`=new list with index `i` set to `v` (list update; alias: `lset`)=`L a` `take n xs`=first `n` elements/chars of list or text (n>=0 truncates if n>len; n<0 keeps all but the last `abs n`, Python `xs[:n]`)=same type `drop n xs`=skip first `n` elements/chars (n>=0 returns the rest; n<0 keeps only the last `abs n`, Python `xs[n:]`)=same type `rsrt xs`=sort descending (list or text chars)=same type `uniqby fn xs`=dedupe by key function (first occurrence wins)=`L a` `zip xs ys`=pairwise pairs of two lists; truncates to shorter input=`L (L _)` `enumerate xs`=pair each element with its index → `[[i, v], ...]`=`L (L _)` `range a b`=half-open numeric range `[a, a+1, ..., b-1]`; empty when `a >= b`=`L n` `map fn xs`=apply `fn` to each element=`L b` `flt fn xs`=keep elements where `fn x` is true=`L a` `fld fn xs init`=left fold: `fn (fn (fn init x0) x1) ...`=accumulator `flatmap fn xs`=map then flatten one level=`L b` `partition fn xs`=split list into `[passing, failing]` by predicate=`L (L a)` `chunks n xs`=non-overlapping chunks of size `n` (final chunk may be shorter)=`L (L a)` `window n xs`=sliding windows of size `n` (drops trailing partial; empty if n > len)=`L (L a)` `clamp x lo hi`=restrict `x` to `[lo, hi]` (lower bound wins when `lo > hi`)=`n` `cumsum xs`=running sum; output length matches input=`L n` `frq xs`=frequency map of elements (keys are bare stringified values)=`M t n` `median xs`=median of numeric list=`n` `quantile xs p`=sample quantile (linear interp; `p` clamped to `[0, 1]`)=`n` `stdev xs`=sample standard deviation (divides by N-1)=`n` `variance xs`=sample variance (divides by N-1)=`n` `setunion a b`=set union of two lists (deduped, sorted output)=`L a` `setinter a b`=set intersection (deduped, sorted)=`L a` `setdiff a b`=set difference `a - b` (deduped, sorted)=`L a` `chars s`=explode a string into single-char strings (one per Unicode scalar)=`L t` `ord s`=Unicode codepoint of the first character of `s`=`n` `chr n`=single-character string for codepoint `n`=`t` `upr s`=uppercase (ASCII)=`t` `lwr s`=lowercase (ASCII)=`t` `cap s`=capitalise first char (ASCII)=`t` `padl s w`=left-pad to width `w` with spaces (no-op if already wider)=`t` `padr s w`=right-pad to width `w` with spaces (no-op if already wider)=`t` `rgxall pat s`=every regex match as `L (L t)` (no-group: each match in a 1-elem list)=`L (L t)` `rgxsub pat repl s`=regex substitute all matches; `$1`, `$2`, ... reference capture groups=`t` `dtfmt epoch fmt`=format Unix epoch as text (strftime, UTC)=`R t t` `dtparse s fmt`=parse text to Unix epoch (strftime, UTC)=`R n t` `rdjl path`=read JSONL file as `L (R _ t)`: one parse result per non-empty line=`L (R _ t)` `get-many urls`=concurrent HTTP GET fan-out (max 10 parallel), preserves order=`L (R t t)` `sleep ms`=pause current engine for `ms` milliseconds; returns nil=`_` `rou n`=round to nearest integer (banker's rounding)=`n` `rndn mu sigma`=one sample from normal distribution `N(mu, sigma)` (Box-Muller)=`n` `pow b e`=`b` raised to power `e`=`n` `sqrt n`=square root=`n` `exp n`=natural exponent `e^n`=`n` `log n`=natural logarithm=`n` `log10 n`=base-10 logarithm=`n` `log2 n`=base-2 logarithm=`n` `sin n`=sine (radians)=`n` `cos n`=cosine (radians)=`n` `tan n`=tangent (radians)=`n` `atan2 y x`=two-argument arctangent (y, x order; radians)=`n` `transpose m`=transpose row-major matrix=`L (L n)` `matmul a b`=matrix product=`L (L n)` `dot a b`=vector dot product=`n` `solve a b`=solve `Ax = b` via LU with partial pivoting; errors on singular/non-square=`L n` `inv a`=matrix inverse; errors on singular/non-square=`L (L n)` `det a`=determinant; errors on non-square=`n` `fft xs`=discrete FFT: real samples → `L [re, im]`; zero-padded to next power of 2=`L (L n)` `ifft pairs`=inverse FFT; imaginary part dropped on return=`L n` `fmt2 x digits`=format number `x` to `digits` decimal places (half-to-even rounding; `digits` clamped to `0..=20`). Compose with `fmt` for template + precision: `fmt "x={}" (fmt2 v 2)`=`t` [Datetime (`dtfmt` / `dtparse`)] UTC only. Format strings follow strftime conventions (`%Y-%m-%d %H:%M:%S`, `%s`, etc). dtfmt 1700000000 "%Y-%m-%d" -- R t t: Ok="2023-11-14", Err if out of range dtparse "2024-01-15" "%Y-%m-%d" -- R n t: Ok=epoch seconds, Err if unparseable dtfmt! e "%H:%M:%S" -- auto-unwrap inside R-returning fn [Set operations] `setunion`, `setinter`, `setdiff` operate on lists of `t`, `n`, or `b` (same constraint as `uniqby`). Output is deduped and sorted by a type-prefixed string key, so results are deterministic across runs and engines. Sort is lexicographic on the key, not numeric — re-sort with `srt` afterwards if you need numeric order. [Linear algebra] `transpose`, `matmul`, `dot`, `solve`, `inv`, `det` operate on row-major matrices (`L (L n)`) and flat vectors (`L n`). `solve`, `inv`, `det` use LU decomposition with partial pivoting and raise on singular or non-square inputs. These ship as host-vetted builtins because hand-rolled implementations risk silent precision loss. [FFT] `fft xs` runs an iterative Cooley-Tukey radix-2 transform on real samples, zero-padding to the next power of two. Output is `L [re, im]` with one inner pair per frequency bin. `ifft pairs` is the inverse, dropping the imaginary part on return. [Builtin aliases] All builtins accept long-form names that resolve to the canonical short form after parsing. Using a long form triggers a hint suggesting the short form. This lets newcomers write readable code while learning the canonical names. `floor`=→=`flr` `ceil`=→=`cel` `round`=→=`rou` `random`=→=`rnd` `lset`=→=`lst` `regex_all`=→=`rgxall` `regex_sub`=→=`rgxsub` `string`=→=`str` `number`=→=`num` `length`=→=`len` `head`=→=`hd` `tail`=→=`tl` `reverse`=→=`rev` `sort`=→=`srt` `slice`=→=`slc` `unique`=→=`unq` `filter`=→=`flt` `fold`=→=`fld` `flatten`=→=`flat` `concat`=→=`cat` `contains`=→=`has` `group`=→=`grp` `average`=→=`avg` `print`=→=`prnt` `trim`=→=`trm` `split`=→=`spl` `format`=→=`fmt` `regex`=→=`rgx` `read`=→=`rd` `readlines`=→=`rdl` `readbuf`=→=`rdb` `write`=→=`wr` `writelines`=→=`wrl` length xs -- works, but emits: hint: `length` → `len` (canonical short form) len xs -- canonical — no hint `get` and `post` return `Ok(body)` on success, `Err(message)` on failure (connection error, timeout, DNS failure, etc). `$` is a terse alias for `get`: get url -- R t t: Ok=response body, Err=error message $url -- same as get url get! url -- auto-unwrap: Ok→body, Err→propagate to caller $!url -- same as get! url post url body -- R t t: HTTP POST with text body post url body headers -- R t t: HTTP POST with body and custom headers -- Custom headers: build an M t t map with mmap/mset h=mmap h=mset h "x-api-key" "secret" r=get url h -- GET with x-api-key header r=post url body h -- POST with x-api-key header Behind the `http` feature flag (on by default). Without the feature, `get`/`post` return `Err("http feature not enabled")`. `env` reads an environment variable by name, returning `Ok(value)` or `Err("env var 'KEY' not set")`: env key -- R t t: Ok=value, Err=not set message env! key -- auto-unwrap: Ok→value, Err→propagate to caller [JSON builtins] `jpth` extracts a value from a JSON string by dot-separated path. Array elements are accessed by numeric index: jpth json "name" -- R t t: Ok=extracted value as text, Err=error jpth json "user.name" -- nested path lookup jpth json "items.0.name" -- array index access jpth! json "name" -- auto-unwrap `jdmp` serialises any ilo value to a JSON string: jdmp 42 -- "42" jdmp "hello" -- "\"hello\"" jdmp [1 2 3] -- "[1,2,3]" jdmp (pt x:1 y:2) -- "{\"x\":1,\"y\":2}" `jpar` parses a JSON string into ilo values. JSON objects become records with type name `json`, arrays become lists, strings/numbers/bools/null map directly: jpar text -- R _ t: Ok=parsed value, Err=parse error r=jpar! "{\"x\":1}" -- r is a json record, access with r.x +BUILTINS: Called like functions, compiled to dedicated opcodes. `len x`=length of string (bytes) or list (elements)=`n` `str n`=number to text (integers format without `.0`)=`t` `num t`=text to number (Err if unparseable)=`R n t` `abs n`=absolute value=`n` `min a b`=minimum of two numbers=`n` `max a b`=maximum of two numbers=`n` `mod a b`=remainder (modulo); errors on zero divisor=`n` `flr n`=floor (round toward negative infinity)=`n` `cel n`=ceiling (round toward positive infinity)=`n` `rnd`=random float in [0, 1)=`n` `rnd a b`=random integer in [a, b] (inclusive)=`n` `now`=current Unix timestamp (seconds)=`n` `get url`=HTTP GET=`R t t` `get url headers`=HTTP GET with custom headers (`M t t` map)=`R t t` `post url body`=HTTP POST with text body=`R t t` `post url body headers`=HTTP POST with body and custom headers (`M t t` map)=`R t t` `env key`=read environment variable=`R t t` `rd path`=read file; format auto-detected from extension (`.csv`/`.tsv`→grid, `.json`→graph, else text)=`R _ t` `rd path fmt`=read file with explicit format override (`"csv"`, `"tsv"`, `"json"`, `"raw"`)=`R _ t` `rdl path`=read file as list of lines=`R (L t) t` `rdb s fmt`=parse string/buffer in given format — for data from HTTP, env vars, etc.=`R _ t` `wr path s`=write text to file (overwrite)=`R t t` `wr path data "csv"`=write list-of-lists as CSV (with proper quoting)=`R t t` `wr path data "tsv"`=write list-of-lists as TSV=`R t t` `wr path data "json"`=write any value as pretty JSON=`R t t` `wrl path xs`=write list of lines to file (joins with `\n`)=`R t t` `trm s`=trim leading and trailing whitespace=`t` `spl t sep`=split text by separator=`L t` `fmt tmpl args…`=format string — `{}` placeholders filled left-to-right=`t` `cat xs sep`=join list of text with separator=`t` `has xs v`=membership test (list: element, text: substring)=`b` `hd xs`=head (first element/char) of list or text=element / `t` `tl xs`=tail (all but first) of list or text=`L` / `t` `rev xs`=reverse list or text=same type `srt xs`=sort list (all-number or all-text) or text chars=same type `srt fn xs`=sort list by key function (returns number or text key)=`L` `unq xs`=remove duplicates, preserve order (list or text chars)=same type `slc xs a b`=slice list or text from index a to b (a, b accept negative indices counting from end; bounds clamp)=same type `jpth json path`=JSON path lookup (dot-separated keys, array indices)=`R t t` `jdmp value`=serialise ilo value to JSON text=`t` `prnt value`=print value to stdout, return it unchanged (passthrough)=same type `jpar text`=parse JSON text into ilo values=`R _ t` `grp fn xs`=group list by key function=`M t (L a)` `flat xs`=flatten one level of nesting=`L a` `sum xs`=sum of numeric list (0 for empty)=`n` `avg xs`=mean of numeric list (error if empty)=`n` `rgx pat s`=regex: no groups→all matches; groups→first match captures=`L t` `mmap`=create empty map=`M t _` `mget m k`=value at key k (nil if missing)=element or nil `mset m k v`=new map with key k set to v=`M k v` `mhas m k`=true if key exists=`b` `mkeys m`=sorted list of keys=`L t` `mvals m`=values sorted by key=`L v` `mdel m k`=new map with key k removed=`M k v` `at xs i`=i-th element of list or text (0-indexed; negative counts from end)=element `lst xs i v`=new list with index `i` set to `v` (list update; alias: `lset`)=`L a` `take n xs`=first `n` elements/chars of list or text (n>=0 truncates if n>len; n<0 keeps all but the last `abs n`, Python `xs[:n]`)=same type `drop n xs`=skip first `n` elements/chars (n>=0 returns the rest; n<0 keeps only the last `abs n`, Python `xs[n:]`)=same type `rsrt xs`=sort descending (list or text chars)=same type `uniqby fn xs`=dedupe by key function (first occurrence wins)=`L a` `zip xs ys`=pairwise pairs of two lists; truncates to shorter input=`L (L _)` `enumerate xs`=pair each element with its index → `[[i, v], ...]`=`L (L _)` `range a b`=half-open numeric range `[a, a+1, ..., b-1]`; empty when `a >= b`=`L n` `map fn xs`=apply `fn` to each element=`L b` `flt fn xs`=keep elements where `fn x` is true=`L a` `fld fn xs init`=left fold: `fn (fn (fn init x0) x1) ...`=accumulator `flatmap fn xs`=map then flatten one level=`L b` `mapr fn xs`=map with short-circuit Result propagation: collects Ok values, returns first Err=`R (L b) e` `partition fn xs`=split list into `[passing, failing]` by predicate=`L (L a)` `chunks n xs`=non-overlapping chunks of size `n` (final chunk may be shorter)=`L (L a)` `window n xs`=sliding windows of size `n` (drops trailing partial; empty if n > len)=`L (L a)` `clamp x lo hi`=restrict `x` to `[lo, hi]` (lower bound wins when `lo > hi`)=`n` `cumsum xs`=running sum; output length matches input=`L n` `frq xs`=frequency map of elements (keys are bare stringified values)=`M t n` `median xs`=median of numeric list=`n` `quantile xs p`=sample quantile (linear interp; `p` clamped to `[0, 1]`)=`n` `stdev xs`=sample standard deviation (divides by N-1)=`n` `variance xs`=sample variance (divides by N-1)=`n` `setunion a b`=set union of two lists (deduped, sorted output)=`L a` `setinter a b`=set intersection (deduped, sorted)=`L a` `setdiff a b`=set difference `a - b` (deduped, sorted)=`L a` `chars s`=explode a string into single-char strings (one per Unicode scalar)=`L t` `ord s`=Unicode codepoint of the first character of `s`=`n` `chr n`=single-character string for codepoint `n`=`t` `upr s`=uppercase (ASCII)=`t` `lwr s`=lowercase (ASCII)=`t` `cap s`=capitalise first char (ASCII)=`t` `padl s w`=left-pad to width `w` with spaces (no-op if already wider)=`t` `padr s w`=right-pad to width `w` with spaces (no-op if already wider)=`t` `rgxall pat s`=every regex match as `L (L t)` (no-group: each match in a 1-elem list)=`L (L t)` `rgxsub pat repl s`=regex substitute all matches; `$1`, `$2`, ... reference capture groups=`t` `dtfmt epoch fmt`=format Unix epoch as text (strftime, UTC)=`R t t` `dtparse s fmt`=parse text to Unix epoch (strftime, UTC)=`R n t` `rdjl path`=read JSONL file as `L (R _ t)`: one parse result per non-empty line=`L (R _ t)` `get-many urls`=concurrent HTTP GET fan-out (max 10 parallel), preserves order=`L (R t t)` `sleep ms`=pause current engine for `ms` milliseconds; returns nil=`_` `rou n`=round to nearest integer (banker's rounding)=`n` `rndn mu sigma`=one sample from normal distribution `N(mu, sigma)` (Box-Muller)=`n` `pow b e`=`b` raised to power `e`=`n` `sqrt n`=square root=`n` `exp n`=natural exponent `e^n`=`n` `log n`=natural logarithm=`n` `log10 n`=base-10 logarithm=`n` `log2 n`=base-2 logarithm=`n` `sin n`=sine (radians)=`n` `cos n`=cosine (radians)=`n` `tan n`=tangent (radians)=`n` `atan2 y x`=two-argument arctangent (y, x order; radians)=`n` `transpose m`=transpose row-major matrix=`L (L n)` `matmul a b`=matrix product=`L (L n)` `dot a b`=vector dot product=`n` `solve a b`=solve `Ax = b` via LU with partial pivoting; errors on singular/non-square=`L n` `inv a`=matrix inverse; errors on singular/non-square=`L (L n)` `det a`=determinant; errors on non-square=`n` `fft xs`=discrete FFT: real samples → `L [re, im]`; zero-padded to next power of 2=`L (L n)` `ifft pairs`=inverse FFT; imaginary part dropped on return=`L n` `fmt2 x digits`=format number `x` to `digits` decimal places (half-to-even rounding; `digits` clamped to `0..=20`). Compose with `fmt` for template + precision: `fmt "x={}" (fmt2 v 2)`=`t` [Datetime (`dtfmt` / `dtparse`)] UTC only. Format strings follow strftime conventions (`%Y-%m-%d %H:%M:%S`, `%s`, etc). dtfmt 1700000000 "%Y-%m-%d" -- R t t: Ok="2023-11-14", Err if out of range dtparse "2024-01-15" "%Y-%m-%d" -- R n t: Ok=epoch seconds, Err if unparseable dtfmt! e "%H:%M:%S" -- auto-unwrap inside R-returning fn [Set operations] `setunion`, `setinter`, `setdiff` operate on lists of `t`, `n`, or `b` (same constraint as `uniqby`). Output is deduped and sorted by a type-prefixed string key, so results are deterministic across runs and engines. Sort is lexicographic on the key, not numeric — re-sort with `srt` afterwards if you need numeric order. [Linear algebra] `transpose`, `matmul`, `dot`, `solve`, `inv`, `det` operate on row-major matrices (`L (L n)`) and flat vectors (`L n`). `solve`, `inv`, `det` use LU decomposition with partial pivoting and raise on singular or non-square inputs. These ship as host-vetted builtins because hand-rolled implementations risk silent precision loss. [FFT] `fft xs` runs an iterative Cooley-Tukey radix-2 transform on real samples, zero-padding to the next power of two. Output is `L [re, im]` with one inner pair per frequency bin. `ifft pairs` is the inverse, dropping the imaginary part on return. [Builtin aliases] All builtins accept long-form names that resolve to the canonical short form after parsing. Using a long form triggers a hint suggesting the short form. This lets newcomers write readable code while learning the canonical names. `floor`=→=`flr` `ceil`=→=`cel` `round`=→=`rou` `random`=→=`rnd` `lset`=→=`lst` `regex_all`=→=`rgxall` `regex_sub`=→=`rgxsub` `string`=→=`str` `number`=→=`num` `length`=→=`len` `head`=→=`hd` `tail`=→=`tl` `reverse`=→=`rev` `sort`=→=`srt` `slice`=→=`slc` `unique`=→=`unq` `filter`=→=`flt` `fold`=→=`fld` `flatten`=→=`flat` `concat`=→=`cat` `contains`=→=`has` `group`=→=`grp` `average`=→=`avg` `print`=→=`prnt` `trim`=→=`trm` `split`=→=`spl` `format`=→=`fmt` `regex`=→=`rgx` `read`=→=`rd` `readlines`=→=`rdl` `readbuf`=→=`rdb` `write`=→=`wr` `writelines`=→=`wrl` length xs -- works, but emits: hint: `length` → `len` (canonical short form) len xs -- canonical — no hint `get` and `post` return `Ok(body)` on success, `Err(message)` on failure (connection error, timeout, DNS failure, etc). `$` is a terse alias for `get`: get url -- R t t: Ok=response body, Err=error message $url -- same as get url get! url -- auto-unwrap: Ok→body, Err→propagate to caller $!url -- same as get! url post url body -- R t t: HTTP POST with text body post url body headers -- R t t: HTTP POST with body and custom headers -- Custom headers: build an M t t map with mmap/mset h=mmap h=mset h "x-api-key" "secret" r=get url h -- GET with x-api-key header r=post url body h -- POST with x-api-key header Behind the `http` feature flag (on by default). Without the feature, `get`/`post` return `Err("http feature not enabled")`. `env` reads an environment variable by name, returning `Ok(value)` or `Err("env var 'KEY' not set")`: env key -- R t t: Ok=value, Err=not set message env! key -- auto-unwrap: Ok→value, Err→propagate to caller [JSON builtins] `jpth` extracts a value from a JSON string by dot-separated path. Array elements are accessed by numeric index: jpth json "name" -- R t t: Ok=extracted value as text, Err=error jpth json "user.name" -- nested path lookup jpth json "items.0.name" -- array index access jpth! json "name" -- auto-unwrap `jdmp` serialises any ilo value to a JSON string: jdmp 42 -- "42" jdmp "hello" -- "\"hello\"" jdmp [1 2 3] -- "[1,2,3]" jdmp (pt x:1 y:2) -- "{\"x\":1,\"y\":2}" `jpar` parses a JSON string into ilo values. JSON objects become records with type name `json`, arrays become lists, strings/numbers/bools/null map directly: jpar text -- R _ t: Ok=parsed value, Err=parse error r=jpar! "{\"x\":1}" -- r is a json record, access with r.x LISTS: xs=[1 2 3] -- space-separated (preferred) xs=[1, 2, 3] -- commas also work mixed=["search" 10] -- heterogeneous lists allowed (type: L _) w="world" words=["hi" w] -- variables work in list literals empty=[] Elements are expressions in brackets, separated by spaces or commas. Variables and expressions are allowed as elements. Lists may contain mixed types (inferred as `L _`). Use with `@` to iterate: @x xs{+x 1} Index by integer literal or variable (dot notation): xs.0 # first element (literal index) xs.2 # third element (literal index) xs.i # i-th element when `i` is a bound variable in scope The variable-index form `xs.i` is sugar for `at xs i` — the parser builds a field-access node and a post-parse desugar pass rewrites it whenever the field identifier resolves to a binding in scope (parameter, let, foreach, range, match-arm). Record field access keeps working: if the identifier is also a declared field on any record type in the program, the rewrite is skipped and the strict `.field` semantics apply. **CLI list arguments:** Pass lists from the command line with commas (brackets also accepted): ilo 'f xs:L n>n;len xs' 1,2,3 → 3 ilo 'f xs:L t>t;xs.0' 'a,b,c' → a STATEMENTS: Guards and conditionals replace `if`/`else if`/`else`. They are flat statements — no nesting, no closing braces to match. There are three forms: **Braceless guard** (`cond expr`): early return — if condition is true, returns the expression from the function. **Braced conditional** (`cond{body}`): conditional execution — if condition is true, body runs but execution continues (no early return). Use `ret` inside the body for explicit early return. **Ternary** (`cond{then}{else}`): value expression — evaluates then or else branch, no early return. Multiple braceless guards chain vertically for guard clauses, keeping indentation depth constant. Match replaces `switch`. There is no fall-through — each arm is independent. The `_` arm is the default catch-all. `x=expr`=bind `cond{body}`=conditional execution: run body if cond true (no early return) `cond expr`=braceless guard: early return expr if cond true `cond{then}{else}`=ternary: evaluate then or else (no early return) `?cond then else`=prefix ternary: `?=x 0 10 20` (no early return) `!cond{body}`=negated conditional execution (no early return) `!cond expr`=braceless negated guard (early return) `!cond{then}{else}`=negated ternary `?x{arms}`=match named value `?{arms}`=match last result `@v list{body}`=iterate list `@i a..b{body}`=range iteration: i from a (inclusive) to b (exclusive) `ret expr`=early return from function `~expr`=return ok `^expr`=return err `func! args`=call + auto-unwrap Result, propagate Err to caller `func!! args`=call + auto-unwrap Result, abort on Err with exit 1 `wh cond{body}`=while loop `brk` / `brk expr`=exit enclosing loop (optional value) `cnt`=skip to next iteration of enclosing loop `expr>>func`=pipe: pass result as last arg to func MATCH ARMS: `"gold":body`=literal text `42:body`=literal number `~v:body`=ok — bind inner value to `v` `^e:body`=err — bind inner value to `e` `n v:body`=number — branch if value is a number, bind to `v` `t v:body`=text — branch if value is text, bind to `v` `b v:body`=bool — branch if value is a bool, bind to `v` `l v:body`=list — branch if value is a list, bind to `v` `_:body`=wildcard, binds matched subject to `_` Arms separated by `;`. First match wins. In any binding position the name `_` is permitted and binds normally — `~_:body`, `^_:body`, `n _:body` etc. expose the matched inner value to `body` under the name `_`. Bodies that don't reference `_` are unaffected. cls sp:n>t;>=sp 1000 "gold";>=sp 500 "silver";"bronze" [Braceless Guards (Early Return)] When the guard condition is a comparison or logical operator (`>=`, `<=`, `>`, `<`, `=`, `!=`, `&`, `|`) and the body is a single expression, braces are optional. **Braceless guards cause early return from the function:** cls sp:n>t;>=sp 1000 "gold";>=sp 500 "silver";"bronze" Negated braceless guards also work: `!<=n 0 ^"must be positive"`. **Comparison operators always start a guard at statement position.** You cannot use `=`, `<`, `>`, `<=`, `>=` etc. as a standalone return expression — the parser treats them as a guard condition and expects a following return value. To return a comparison result, bind it first: -- WRONG: r=has xs v;=r true -- =r true is parsed as a guard, not a return expression -- OK: r=has xs v;r -- return the bool directly (only safe as the last statement) -- OK: has xs v -- bare call is safe as last statement in last function [Braced Conditionals (No Early Return)] A braced guard `cond{body}` is **conditional execution** — the body runs if the condition is true, but execution always continues to the next statement (no early return): f x:n>n;>x 0{99};+x 1 -- {99} runs when x>0 but is discarded; always returns +x 1 This makes braced conditionals natural in loops: f xs:L n>n;m=0;@x xs{>x m{m=x}};m -- find max: update m when x > m Use `ret` inside a braced conditional for explicit early return: f x:n>n;>x 0{ret x};-x -- return x early if positive, else negate > **Common footgun.** `=cond{val}` reads like "if cond, return val" but it isn't. The braces are conditional execution: `val` is evaluated, discarded, and execution falls through to the next statement. If you want early return, use the braceless form `=cond val` (when val is a single expression) or wrap with `ret` inside the braces: `=cond{ret val}`. > > ``` > f x:n>n;=x 1{99};0 -- f 1 → 0 (99 is discarded, falls through) > f x:n>n;=x 1 99;0 -- f 1 → 99 (braceless guard: early return) > f x:n>n;=x 1{ret 99};0 -- f 1 → 99 (explicit ret inside braces) > ``` [Ternary (Guard-Else)] A guard followed by a second brace block becomes a ternary — it produces a value without early return: f x:n>t;=x 1{"yes"}{"no"} Like braced conditionals, ternary does **not** return from the function. Code after the ternary continues executing: f x:n>n;=x 0{10}{20};+x 1 -- always returns x+1, ternary value is discarded Negated ternary: `!=x 1{"not one"}{"one"}`. **Prefix ternary** uses `?` with a comparison operator for a fully prefix-style conditional: f x:n>n;?=x 0 10 20 -- if x==0 then 10 else 20 f x:n>n;v=?>x 100 1 0;v -- assign result to v The condition must start with a comparison operator (`=`, `>`, `<`, `>=`, `<=`, `!=`). [Early Return] `ret expr` explicitly returns from the current function: f x:n>n;>x 0{ret x};0 -- return x early if positive, else 0 f xs:L n>n;@x xs{>=x 10{ret x}};0 -- return first element >= 10 Braceless guards provide early return for simple cases. Use `ret` inside braced conditionals when you need early return with more complex logic or inside loops. [Range Iteration] `@i a..b{body}` iterates `i` from `a` (inclusive) to `b` (exclusive). Both bounds can be atoms, prefix-op expressions, or function calls. The index variable is a fresh binding per iteration; other variables in the body update the enclosing scope: f>n;s=0;@i 0..5{s=+s i};s -- sum 0+1+2+3+4 = 10 f>n;xs=[];@i 0..3{xs=+=xs i};xs -- [0, 1, 2] f n:n>n;s=0;@i 0..n{s=+s i};s -- dynamic end bound g xs:L n>n;s=0;@j 0..len xs{s=+s j};s -- call-form bound h i:n n:n>L n;xs=[];@j +i 2..n{xs=+=xs j};xs -- prefix-op bound [While Loop] `wh cond{body}` loops while condition is truthy: f>n;i=0;s=0;wh n;i=0;wh true{i=+i 1;>=i 3{ret i}};0 -- ret inside braced guard: early return from loop Variable rebinding inside loops updates the existing variable rather than creating a new binding. [Break and Continue] `brk` exits the enclosing `wh` or `@` loop. `cnt` skips to the next iteration: f>n;i=0;wh true{i=+i 1;>=i 3{brk}};i -- i = 3 f>n;i=0;s=0;wh =i 3{cnt};s=+s i};s -- s = 3 (skips i>=3) `brk expr` provides an optional value (currently discarded — the loop result is the last body value before the break). Both `brk` and `cnt` work inside braced conditionals within loops. Using them outside a loop is a compile-time error (no-op in current implementation). [Pipe Operator] `>>` chains calls by passing the left side as the last argument to the right side: str x>>len -- desugars to: len (str x) add x 1>>add 2 -- desugars to: add 2 (add x 1) f x>>g>>h -- desugars to: h (g (f x)) Pipes desugar at parse time — no new AST node. Works with `!` for auto-unwrap: `f x>>g!>>h`. [Safe Field Navigation] `.?` is the tolerant field accessor. It returns nil whenever the access can't yield a real value, instead of erroring: object is nil → nil object is a present record but the field is missing → nil object is not a record at all (list, text, number) → nil user.?name -- nil if user is nil, else user.name (or nil if absent) user.?addr.?city -- chained: nil propagates through chain x.?name??"unknown" -- combine with ?? for defaults r.?optMetric.?v40 -- heterogeneous JSON (jpar): optional fields stay nil Strict `.field` access still errors on missing fields, so typo detection on user-defined record types survives at verify time (ILO-T019) and at runtime (ILO-R005). Use `.field` when you want the strictness, `.?field` when the field is optional or the record shape is dynamic. [Nil-Coalesce Operator] `??` evaluates the left side; if nil, evaluates and returns the right side: x??42 -- if x is nil, returns 42 a??b??99 -- chained: first non-nil wins, else 99 mk 0??"default" -- works with function results Compiled via `OP_JMPNN` (jump if not nil) — right side is only evaluated when left is nil. Use braces when the body has multiple statements: >=sp 1000{a=classify sp;a} ?r{^e:^+"failed: "e;~v:v} @@ -78,7 +78,6 @@ For the full specification, read [SPEC.md](../../SPEC.md). For the compact AI sp - **Same-precedence trap:** the outer prefix op binds the inner one as its **left** operand. So `*/a b c` is `(a/b)*c`, NOT `(a*b)/c`. Same for `/*`, `+-`, `-+`. The runtime emits a `hint:` on these four shapes. Swap the pair or bind the inner result first if you wanted the other grouping. - `;` separates statements, last expression is the return value - No `return`, `if`, `let`, `fn` keywords — these are reserved words -- Builtin names (`flat`, `frq`, `map`, `flt`, `cat`, `len`, `srt`, `hd`, `tl`, `ord`, `fld`, `lst`, ...) cannot be used as user-function names or local-binding names. The parser rejects with `ILO-P011` and a rename hint (`myflat`, `flatv`). Without this, calls and use sites silently mis-dispatch to the builtin and surface as a misleading `ILO-T006` arity mismatch. ## Types @@ -163,7 +162,14 @@ main xs:L n>L n;flt pos xs -- filter by predicate main xs:L n>n;fld add xs 0 -- fold/reduce ``` -All HOFs (`map`, `flt`, `fld`, `srt`, `grp`, `uniqby`, `partition`, `flatmap`) work cross-engine (tree, VM, Cranelift JIT, AOT). +All HOFs (`map`, `mapr`, `flt`, `fld`, `srt`, `grp`, `uniqby`, `partition`, `flatmap`) work cross-engine (tree, VM, Cranelift JIT, AOT). + +For a fallible per-element fn (returns `R b e`) use `mapr` instead of `map`: +``` +parse xs:L t>R (L n) t;mapr num xs -- ~[1,2,3] or first ^err +parse! xs:L t>R n t;ns=mapr! num xs;~hd ns +``` +`mapr` collects Ok values on the all-Ok path and short-circuits to the first Err. Pair with `!` to propagate the err into a Result-returning caller without per-item match boilerplate (avoid the `ton s:t>n;r=num s;?r{~v:v;^_:0}` swallow-err helper). ### Inline lambdas @@ -233,7 +239,7 @@ Keys are typed: text (`t`) or integer (`n`). Numeric keys work directly — `mse **HTTP**: `get`/`$` `post` `get-many` `env` **JSON**: `jpth` `jdmp` `jpar` **Map**: `mmap` `mget` `mset` `mhas` `mkeys` `mvals` `mdel` -**HOF**: `map` `flt` `fld` +**HOF**: `map` `flt` `fld` `mapr` **Time**: `now` `sleep` `dtfmt` `dtparse` (note: `dtparse s fmt > R n t`, `dtfmt ts fmt > R t t`, both Result-wrapped) ## Naming Convention diff --git a/src/builtins.rs b/src/builtins.rs index 41363289..2b0d847e 100644 --- a/src/builtins.rs +++ b/src/builtins.rs @@ -79,6 +79,7 @@ pub enum Builtin { Partition, Frq, Flatmap, + Mapr, // Random / time Rnd, @@ -208,6 +209,7 @@ impl Builtin { "partition" => Some(Builtin::Partition), "frq" => Some(Builtin::Frq), "flatmap" => Some(Builtin::Flatmap), + "mapr" => Some(Builtin::Mapr), "rnd" => Some(Builtin::Rnd), "rndn" => Some(Builtin::Rndn), "now" => Some(Builtin::Now), @@ -324,6 +326,7 @@ impl Builtin { Builtin::Partition => "partition", Builtin::Frq => "frq", Builtin::Flatmap => "flatmap", + Builtin::Mapr => "mapr", Builtin::Rnd => "rnd", Builtin::Rndn => "rndn", Builtin::Now => "now", @@ -446,6 +449,7 @@ impl Builtin { Builtin::Partition, Builtin::Frq, Builtin::Flatmap, + Builtin::Mapr, Builtin::Rnd, Builtin::Rndn, Builtin::Now, @@ -671,6 +675,7 @@ mod tests { "partition", "frq", "flatmap", + "mapr", "rnd", "now", "rd", @@ -882,6 +887,7 @@ mod tests { "partition", "frq", "flatmap", + "mapr", "rnd", "rndn", "now", diff --git a/src/interpreter/mod.rs b/src/interpreter/mod.rs index 24474322..c370b0dc 100644 --- a/src/interpreter/mod.rs +++ b/src/interpreter/mod.rs @@ -2732,6 +2732,59 @@ fn call_function(env: &mut Env, name: &str, args: Vec) -> Result { } return Ok(Value::List(Arc::new(result))); } + // mapr fn xs: short-circuiting Result-aware map. + // + // The callee must return R b e. On each item: + // ~v → unwrap to v and accumulate + // ^e → return ^e immediately (whole call short-circuits) + // any → runtime error (callee broke its contract) + // + // Final return on the all-Ok path is ~(L b). Pair with `!` to thread + // the err up into a Result-returning caller. Retires the + // `ton s:t>n;r=num s;?r{~v:v;^_:0}` helper that html-scraper and + // CSV-parsing personas kept writing. See ilo_assessment_feedback.md + // line 2541 for the originating entry. + // + // Deliberately 2-arity only: no closure-bind ctx variant yet. If a + // workload turns up that needs it, add it the same way `Map`/`Flt` + // have it. Keeping the surface tight until a real need surfaces. + if builtin == Some(Builtin::Mapr) && args.len() == 2 { + let fn_name = resolve_fn_ref(&args[0]).ok_or_else(|| { + RuntimeError::new( + "ILO-R009", + format!( + "mapr: first arg must be a function reference, got {:?}", + args[0] + ), + ) + })?; + let captures = closure_captures(&args[0]); + let items = match &args[1] { + Value::List(l) => l.clone(), + other => { + return Err(RuntimeError::new( + "ILO-R009", + format!("mapr: list arg must be a list, got {:?}", other), + )); + } + }; + let mut result = Vec::with_capacity(items.len()); + for item in items.iter().cloned() { + let mut call_args = vec![item]; + call_args.extend(captures.iter().cloned()); + match call_function(env, &fn_name, call_args)? { + Value::Ok(inner) => result.push(*inner), + Value::Err(e) => return Ok(Value::Err(e)), + other => { + return Err(RuntimeError::new( + "ILO-R009", + format!("mapr: fn must return a Result (~v or ^e), got {:?}", other), + )); + } + } + } + return Ok(Value::Ok(Box::new(Value::List(Arc::new(result))))); + } if builtin == Some(Builtin::Flt) && (args.len() == 2 || args.len() == 3) { let fn_name = resolve_fn_ref(&args[0]).ok_or_else(|| { RuntimeError::new( diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ee9e3d41..a12be98f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3007,6 +3007,7 @@ fn builtin_arity_tables() -> (HashMap, HashMap> ("uniqby", 2, &[0]), ("partition", 2, &[0]), ("flatmap", 2, &[0]), + ("mapr", 2, &[0]), // I/O ("prnt", 1, &[]), ("wr", 2, &[]), diff --git a/src/verify.rs b/src/verify.rs index d4e58655..e3a94423 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -380,6 +380,7 @@ const BUILTINS: &[(&str, &[&str], &str)] = &[ ("rdjl", &["t"], "L (R ? t)"), // Higher-order: map/flt/fld take a function ref as first arg (special-cased in builtin_check_args) ("map", &["fn", "list"], "list"), + ("mapr", &["fn", "list"], "result"), ("flt", &["fn", "list"], "list"), ("fld", &["fn", "list", "any"], "any"), ("grp", &["fn", "list"], "map"), @@ -1655,6 +1656,59 @@ fn builtin_check_args( }; (Ty::List(Box::new(ret_elem)), errors) } + "mapr" => { + // mapr fn:F a (R b e) xs:L a → R (L b) e + // + // Short-circuiting parallel to `map`. The fn must return a Result; + // on the first ^err encountered the whole call returns that ^err, + // otherwise the unwrapped Ok values are collected into a list and + // wrapped in a single outer ~. Pairs with `!` to thread the err + // up into a Result-returning caller without per-item match noise. + // Retires the persona-written `ton s:t>n;r=num s;?r{~v:v;^_:0}` + // helper that html-scraper + CSV-parsing rerun3s kept writing. + if let Some(fn_ty) = arg_types.first() + && !matches!(fn_ty, Ty::Fn(_, _) | Ty::Unknown) + { + errors.push(VerifyError { + code: "ILO-T013", + function: func_ctx.to_string(), + message: format!("'mapr' first arg must be a function (F ...), got {fn_ty}"), + hint: Some("pass a function name: mapr num xs".to_string()), + span, + is_warning: false, + }); + } + // fn must return a Result. Catches the obvious misuse where the + // caller picked `mapr` over `map` for a non-fallible fn. + if let Some(Ty::Fn(_, ret)) = arg_types.first() + && !matches!(ret.as_ref(), Ty::Result(_, _) | Ty::Unknown) + { + errors.push(VerifyError { + code: "ILO-T013", + function: func_ctx.to_string(), + message: format!( + "'mapr' fn must return a Result (R _ _), got {ret}; use 'map' for non-fallible fns" + ), + hint: Some( + "mapr short-circuits on the first ^err; for non-fallible fns use map".to_string(), + ), + span, + is_warning: false, + }); + } + // Return type: R (L b) e from fn's R b e, or R (L Unknown) Unknown. + let (ok_elem, err_ty) = match arg_types.first() { + Some(Ty::Fn(_, ret)) => match ret.as_ref() { + Ty::Result(ok, err) => ((**ok).clone(), (**err).clone()), + _ => (Ty::Unknown, Ty::Unknown), + }, + _ => (Ty::Unknown, Ty::Unknown), + }; + ( + Ty::Result(Box::new(Ty::List(Box::new(ok_elem))), Box::new(err_ty)), + errors, + ) + } "flt" => { // flt fn:F a b xs:L a → L a // flt fn:F a c b ctx:c xs:L a → L a (closure-bind variant) diff --git a/src/vm/mod.rs b/src/vm/mod.rs index 7730aced..6fa8a84c 100644 --- a/src/vm/mod.rs +++ b/src/vm/mod.rs @@ -407,6 +407,12 @@ pub(crate) fn is_tree_bridge_eligible(b: crate::builtins::Builtin, argc: usize) (Builtin::Uniqby, 2) => true, (Builtin::Partition, 2) => true, (Builtin::Srt, 2) => true, + // mapr fn xs: short-circuit Result-aware map. Tree bridge routes + // through the tree interpreter's Mapr arm, which handles the + // ~v/^e dispatch and ACTIVE_AST_PROGRAM user-fn callbacks the same + // way grp/uniqby/partition/srt do (PR 3b precedent). Returns + // R (L b) e; the bridge unwrap epilogue handles `!` propagation. + (Builtin::Mapr, 2) => true, // Closure-bind ctx variants. The fn receives an extra ctx arg the // native emitters don't shape today — bridge keeps semantics aligned // with the tree interpreter and adds VM/Cranelift coverage in PR 3c. @@ -422,7 +428,7 @@ pub(crate) fn is_tree_bridge_eligible(b: crate::builtins::Builtin, argc: usize) /// the auto-unwrap (`!`) protocol when called via the tree bridge. pub(crate) fn tree_bridge_returns_result(b: crate::builtins::Builtin) -> bool { use crate::builtins::Builtin; - matches!(b, Builtin::Rd | Builtin::Rdb) + matches!(b, Builtin::Rd | Builtin::Rdb | Builtin::Mapr) } pub(crate) const OP_GETMANY: u8 = 136; // R[A] = get_many(R[B]) (L t → L (R t t), concurrent fan-out) diff --git a/tests/regression_mapr.rs b/tests/regression_mapr.rs new file mode 100644 index 00000000..3dddffec --- /dev/null +++ b/tests/regression_mapr.rs @@ -0,0 +1,201 @@ +// Cross-engine regression tests for the `mapr` HOF (short-circuiting +// Result-aware map). Pins behaviour across tree / VM / Cranelift, mirroring +// the regression_hof_3b.rs shape used for the grp/uniqby/partition/srt +// tree-bridge family. +// +// `mapr fn xs` calls fn on each element, accumulates the inner Ok values +// on the all-Ok path (returning `~(L b)`), and short-circuits on the first +// Err (returning `^e` without visiting the tail). VM and Cranelift route +// through the tree-bridge (`is_tree_bridge_eligible(Mapr, 2)`), so the +// callback dispatch is the same code path as grp/uniqby/partition/srt. +// +// Originating friction: ilo_assessment_feedback.md line 2541 (html-scraper +// rerun3, persona kept writing a `ton s:t>n;r=num s;?r{~v:v;^_:0}` helper +// because `map num xs` returns `L (R n t)` with no clean unwrap path). + +use std::process::Command; + +const ENGINES: &[&str] = &["--run-tree", "--run-vm", "--run-cranelift"]; + +fn ilo() -> Command { + Command::new(env!("CARGO_BIN_EXE_ilo")) +} + +fn run_ok(src: &str, engine: &str, entry: &str, extra: &[&str]) -> String { + let mut cmd = ilo(); + cmd.arg(src).arg(engine).arg(entry); + for a in extra { + cmd.arg(a); + } + let out = cmd.output().expect("failed to run ilo"); + assert!( + out.status.success(), + "ilo {engine} failed for `{src}` entry `{entry}`: stderr={}", + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8_lossy(&out.stdout).trim().to_string() +} + +fn run_err(src: &str, engine: &str, entry: &str, extra: &[&str]) -> String { + let mut cmd = ilo(); + cmd.arg(src).arg(engine).arg(entry); + for a in extra { + cmd.arg(a); + } + let out = cmd.output().expect("failed to run ilo"); + assert!( + !out.status.success(), + "ilo {engine} unexpectedly succeeded for `{src}` entry `{entry}`: stdout={}", + String::from_utf8_lossy(&out.stdout) + ); + String::from_utf8_lossy(&out.stderr).into_owned() +} + +fn check_ok(src: &str, entry: &str, extra: &[&str], expected: &str) { + for engine in ENGINES { + let actual = run_ok(src, engine, entry, extra); + assert_eq!( + actual, expected, + "engine={engine}, src=`{src}`, entry=`{entry}` extra={extra:?}: got `{actual}`, expected `{expected}`" + ); + } +} + +fn check_err_contains(src: &str, entry: &str, extra: &[&str], needle: &str) { + for engine in ENGINES { + let stderr = run_err(src, engine, entry, extra); + assert!( + stderr.contains(needle), + "engine={engine}: stderr missing `{needle}`: {stderr}" + ); + } +} + +// ── happy path: every string parses cleanly, returns ~[1,2,3] ───────────── + +#[test] +fn mapr_all_ok_returns_inner_list() { + check_ok( + r#"f>R (L n) t;mapr num ["1","2","3"]"#, + "f", + &[], + "[1, 2, 3]", + ); +} + +// ── empty list: trivially ~[] ───────────────────────────────────────────── + +#[test] +fn mapr_empty_list_returns_empty_ok() { + check_ok(r#"f>R (L n) t;mapr num []"#, "f", &[], "[]"); +} + +// ── err at head: short-circuit, never visit tail ───────────────────────── + +#[test] +fn mapr_first_err_short_circuits() { + check_err_contains(r#"f>R (L n) t;mapr num ["bad","2","3"]"#, "f", &[], "^bad"); +} + +// ── err mid-list: same short-circuit, but the err comes from the middle ── + +#[test] +fn mapr_mid_err_short_circuits() { + check_err_contains(r#"f>R (L n) t;mapr num ["1","bad","3"]"#, "f", &[], "^bad"); +} + +// ── err at tail: same shape ────────────────────────────────────────────── + +#[test] +fn mapr_tail_err_short_circuits() { + check_err_contains(r#"f>R (L n) t;mapr num ["1","2","bad"]"#, "f", &[], "^bad"); +} + +// ── `!` auto-unwrap threads the err up into the caller ─────────────────── +// +// Bare `mapr fn xs` returns R; pairing with `!` extracts the inner list +// inside the body, and the err short-circuit propagates out of the +// surrounding function (which must also return R for `!` to typecheck). + +#[test] +fn mapr_bang_ok_propagates_to_caller() { + check_ok( + r#"count xs:L t>R n t;ns=mapr! num xs;~len ns"#, + "count", + &[r#"["1","2","3"]"#], + "3", + ); +} + +#[test] +fn mapr_bang_err_propagates_to_caller() { + check_err_contains( + r#"count xs:L t>R n t;ns=mapr! num xs;~len ns"#, + "count", + &[r#"["1","bad","3"]"#], + "^bad", + ); +} + +// ── user-defined fn returning R: dispatched through the tree-bridge ────── +// +// This is the load-bearing piece for VM and Cranelift: the user fn must be +// resolvable from the tree-bridge via ACTIVE_AST_PROGRAM (the same path +// grp/uniqby/partition/srt take in PR 3b). + +#[test] +fn mapr_user_fn_dispatch_cross_engine() { + // safe-div returns ^"divzero" when the divisor is zero, else ~/100 d. + // mapr accumulates the Ok divisions; any zero in the list bails. + check_ok( + r#"sd d:n>R n t;=d 0 ^"divzero";~/100 d +f>R (L n) t;mapr sd [2,4,5]"#, + "f", + &[], + "[50, 25, 20]", + ); + check_err_contains( + r#"sd d:n>R n t;=d 0 ^"divzero";~/100 d +f>R (L n) t;mapr sd [2,0,5]"#, + "f", + &[], + "^divzero", + ); +} + +// ── verifier rejects non-fn first arg ───────────────────────────────────── + +#[test] +fn mapr_non_fn_first_arg_rejected_cross_engine() { + check_err_contains( + r#"f>R (L n) t;mapr 42 ["1"]"#, + "f", + &[], + "'mapr' first arg must be a function", + ); +} + +// ── verifier rejects fn that doesn't return Result ─────────────────────── +// +// `mapr` is for fallible fns; reaching for it with `dbl x:n>n` is a sign +// the caller wanted plain `map`. Verifier catches it before runtime so the +// agent gets a clear redirect instead of a downstream type error. + +#[test] +fn mapr_non_result_fn_rejected_cross_engine() { + check_err_contains( + r#"dbl x:n>n;*x 2 +f xs:L n>R (L n) t;mapr dbl xs"#, + "f", + &["[1,2]"], + "'mapr' fn must return a Result", + ); +} + +// ── arity rejection: 1-arg or 3-arg mapr is not (yet) accepted ─────────── + +#[test] +fn mapr_wrong_arity_rejected() { + // 1 arg: missing list. + check_err_contains(r#"f>R (L n) t;mapr num"#, "f", &[], "mapr"); +}