From 6ff46f5f7281b5baa9f18cb892cab1b896247b57 Mon Sep 17 00:00:00 2001 From: Daniel Morris Date: Thu, 14 May 2026 12:15:05 +0100 Subject: [PATCH 1/2] docs: spell out lowercase-only identifier rule in SPEC PR #262 documented the post-dot dot-access relaxations (camelCase, leading-uppercase, snake_case, kebab-case) for field names, but the binding/call-site rule itself was only implicit. Anyone reading SPEC.md would see the dot-access relaxations and reasonably wonder what the baseline is. Adds an Identifier syntax subsection under Naming with the formal grammar (`[a-z][a-z0-9]*(-[a-z0-9]+)*`), a worked OK/ERROR table, the ILO-L003 surface in the CLI, and an explicit cross-reference to the Field names at dot-access section. Makes the binding/call-site rule findable on its own anchor. --- SPEC.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/SPEC.md b/SPEC.md index 851af971..2b22f81a 100644 --- a/SPEC.md +++ b/SPEC.md @@ -115,6 +115,24 @@ Short names everywhere. 1–3 chars. Function names follow the same rules. Field names in constructors and external tool names keep their full form — they define the public interface. +### Identifier syntax + +Identifiers are lowercase ASCII only, optionally with hyphenated segments. Formally: `[a-z][a-z0-9]*(-[a-z0-9]+)*`. Capital letters and underscores are rejected at the binding and call site. + +``` +run -- OK +run-d -- OK (hyphen separates segments) +r2 -- OK (digit after first letter) +runD -- ERROR (capital letter) +RunD -- ERROR (leading capital) +run_d -- ERROR (underscore not allowed in bindings) +-run -- ERROR (must start with a letter) +``` + +`runD` in the interactive CLI surfaces as `ILO-L003 unexpected token` with a suggestion to use `run-d` or `rund`. The constraint is intentional: a single lexical shape per identifier keeps the token stream predictable for agents and avoids style debates over camelCase vs snake_case vs kebab-case. + +The only place capital letters and underscores are accepted is **after `.` or `.?`** at field-access position, so heterogeneous JSON keys from real APIs work without rewriting. See [Field names at dot-access](#field-names-at-dot-access) for the full list of post-dot relaxations (`r.URL`, `r.AccessKey`, `r.user_name`, etc.). Binding names (`AccessKey = ...`) and function names (`AccessKey x:n>n;...`) still error. + ### Reserved words The following identifiers are reserved and cannot be used as names: `if`, `return`, `let`, `fn`, `def`, `var`, `const`. Using them produces a friendly error with the ilo equivalent: From 0aa16fc5af40d81adf853431bfce61675f06a0b3 Mon Sep 17 00:00:00 2001 From: Daniel Morris Date: Thu, 14 May 2026 12:15:09 +0100 Subject: [PATCH 2/2] docs: mirror identifier rule into SKILL.md and regenerate ai.txt SKILL.md is what agents see first when ilo loads as a Claude plugin, so the lowercase-only rule needs to land there too, with the same OK/ERROR examples (compressed) and the post-dot exception called out. ai.txt is the build.rs regeneration from the updated SPEC.md. --- ai.txt | 2 +- skills/ilo/SKILL.md | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/ai.txt b/ai.txt index 2f77abe0..6da19b10 100644 --- a/ai.txt +++ b/ai.txt @@ -1,7 +1,7 @@ INTRO: ilo is a token-optimised programming language for AI agents. Every design choice is evaluated against total token cost: generation + retries + context loading. FUNCTIONS: : ...>; No parens around params — `>` separates params from return type `;` separates statements — no newlines required Last expression is the return value (no `return` keyword) Zero-arg call: `make-id()` tot p:n q:n r:n>n;s=*p q;t=*s r;+s t TYPES: `n`=number (f64) `t`=text (string) `b`=bool `_`=any/unknown (wildcard type) `L n`=list of number `R n t`=result: ok=number, err=text `O n`=optional number (nil or n) `M t n`=map from text keys to numbers `S red green blue`=sum type — one of named text variants `F n t`=function type: takes n, returns t (used in HOF params) `order`=named type `a`=type variable — any single lowercase letter except n, t, b [Optional (`O T`)] `O T` accepts either `nil` or a value of type `T`. f x:O n>n;??x 0 -- unwrap optional or default to 0 g>O n;nil -- returns nil (valid O n) h>O n;42 -- returns 42 (valid O n) `??x default` — nil-coalesce: returns `x` if non-nil, else `default`. Unwraps `O T` to `T`. [Sum types (`S a b c`)] Closed set of named text variants. Verifier-enforced; runtime value is always `t`. color x:S red green blue > t ?x{red:"ff0000";green:"00ff00";blue:"0000ff"} Sum types are compatible with `t` — a sum value can be passed to any `t` parameter. [Map type (`M k v`)] Dynamic key-value collection. Keys are always text at runtime. mmap -- empty map mset m k v -- return new map with key k set to v mget m k -- value at key k, or nil mhas m k -- b: true if key exists mkeys m -- L t: sorted list of keys mvals m -- L v: values sorted by key mdel m k -- return new map with key k removed len m -- number of entries Example: scores>M t n m=mmap m=mset m "alice" 99 m=mset m "bob" 87 mget m "alice" -- 99 [Type variables] A single lowercase letter (other than `n`, `t`, `b`) in type position is a type variable, treated as `unknown` during verification. Used for higher-order function signatures: identity x:a>a;x apply f:F a a x:a>a;f x Type variables provide weak generics — the verifier accepts any type for `a` without consistency checking across call sites. -NAMING: Short names everywhere. 1–3 chars. `order`=`ord`=truncate `customers`=`cs`=consonants `data`=`d`=single letter `level`=`lv`=drop vowels `discount`=`dc`=initials `final`=`fin`=first 3 `items`=`its`=first 3 Function names follow the same rules. Field names in constructors and external tool names keep their full form — they define the public interface. [Reserved words] The following identifiers are reserved and cannot be used as names: `if`, `return`, `let`, `fn`, `def`, `var`, `const`. Using them produces a friendly error with the ilo equivalent: -- ERROR: `if` is a reserved word. Use: ?cond{true:... false:...} -- ERROR: `return` is a reserved word. Last expression is the return value. -- ERROR: `let` is a reserved word. Use: name = expr -- ERROR: `fn`/`def` is a reserved word. Use: name param:type > rettype; body +NAMING: Short names everywhere. 1–3 chars. `order`=`ord`=truncate `customers`=`cs`=consonants `data`=`d`=single letter `level`=`lv`=drop vowels `discount`=`dc`=initials `final`=`fin`=first 3 `items`=`its`=first 3 Function names follow the same rules. Field names in constructors and external tool names keep their full form — they define the public interface. [Identifier syntax] Identifiers are lowercase ASCII only, optionally with hyphenated segments. Formally: `[a-z][a-z0-9]*(-[a-z0-9]+)*`. Capital letters and underscores are rejected at the binding and call site. run -- OK run-d -- OK (hyphen separates segments) r2 -- OK (digit after first letter) runD -- ERROR (capital letter) RunD -- ERROR (leading capital) run_d -- ERROR (underscore not allowed in bindings) -run -- ERROR (must start with a letter) `runD` in the interactive CLI surfaces as `ILO-L003 unexpected token` with a suggestion to use `run-d` or `rund`. The constraint is intentional: a single lexical shape per identifier keeps the token stream predictable for agents and avoids style debates over camelCase vs snake_case vs kebab-case. The only place capital letters and underscores are accepted is **after `.` or `.?`** at field-access position, so heterogeneous JSON keys from real APIs work without rewriting. See [Field names at dot-access](#field-names-at-dot-access) for the full list of post-dot relaxations (`r.URL`, `r.AccessKey`, `r.user_name`, etc.). Binding names (`AccessKey = ...`) and function names (`AccessKey x:n>n;...`) still error. [Reserved words] The following identifiers are reserved and cannot be used as names: `if`, `return`, `let`, `fn`, `def`, `var`, `const`. Using them produces a friendly error with the ilo equivalent: -- ERROR: `if` is a reserved word. Use: ?cond{true:... false:...} -- ERROR: `return` is a reserved word. Last expression is the return value. -- ERROR: `let` is a reserved word. Use: name = expr -- ERROR: `fn`/`def` is a reserved word. Use: name param:type > rettype; body 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) [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 `\t`=tab `\r`=carriage return `\"`=literal double quote `\\`=literal backslash "hello\nworld" -- two-line string "col1\tcol2" -- tab-separated spl "\n" text -- split file content into lines diff --git a/skills/ilo/SKILL.md b/skills/ilo/SKILL.md index 0d4ae589..04e5d58e 100644 --- a/skills/ilo/SKILL.md +++ b/skills/ilo/SKILL.md @@ -180,6 +180,15 @@ Short names, 1-3 chars: `order`→`ord`, `customers`→`cs`, `data`→`d`, `item Function names follow the same rule. Field names in constructors keep their full form. +**Identifier syntax**: lowercase ASCII with optional hyphenated segments only. Formal grammar: `[a-z][a-z0-9]*(-[a-z0-9]+)*`. Capital letters and underscores are rejected at binding and call sites. + +``` +run, run-d, r2 -- OK +runD, RunD, run_d -- ERROR (capital or underscore) +``` + +The only place capital letters and underscores are accepted is **after `.` or `.?`** at field-access position, so JSON keys from real APIs work as-is: `r.URL`, `r.AccessKey`, `r.access_key`, `r.?MetaData`. + ## Running ```bash