From 0c1ec190f0bc3c64e09128d58078b11ab0f49fcf Mon Sep 17 00:00:00 2001 From: Daniel Morris Date: Wed, 20 May 2026 23:21:07 +0100 Subject: [PATCH 1/4] add context-aware ILO-P009 hint for ternary-brace misfires When the parser hits `{` mid prefix-ternary operand parse, emit one of two hints instead of the bare "expected expression, got `{`": 1. Match-on-value shape (`?h x{0:1;_:2}`): the agent reached for Rust-style `match x { 0 => ..., _ => ... }` with a leading `?h` keyword. Hint funnels them to `?{...}` (single-token) or `?(){...}` (multi-token). 2. Three-form conditional confusion (`?h cond{body}`): the brace body is not a match arm. Hint enumerates all three canonical shapes, prefix-ternary `?h cond a b`, brace-ternary `cond{a}{b}`, and braced-conditional `cond{body}`, with the parsed condition substituted in so the agent can copy directly. Match-arm shape is detected by peeking one token past the `{` for a literal-and-colon pattern (`: ...` or `"text": ...`). Pure lookahead, no token consumption. Hint fires in both statement (parse_match_stmt) and expression (parse_prefix_ternary) positions so RHS-of-binding usage gets the same diagnostic. --- src/parser/mod.rs | 93 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 2fe17263..f2c709a2 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1412,6 +1412,20 @@ impl Parser { // expression-position branch above and the `?=cond a b` family in // `parse_prefix_ternary`. let first = self.parse_prefix_binop_operand()?; + // Before consuming the second operand, intercept a `{` to give a + // context-aware hint (match-on-value mis-parenthesisation, or the + // `?h cond{...}` vs the three canonical forms). Otherwise the + // bare ILO-P009 "expected expression, got `{`" surfaces with no + // recovery hint, costing a re-prompt or worse (cron-explainer + // burned 167k tokens partly on this shape; the date/time persona + // cluster all tripped on the `?h cond{...}` variant). + let subj_src = subject_source(subj); + let first_src = subject_source(&first); + if let Some(err) = + self.prefix_ternary_brace_hint(subj_src.as_deref(), first_src.as_deref()) + { + return Err(err); + } let second = self.parse_prefix_binop_operand()?; // `?h` general prefix-ternary: when the subject ident is literally // `h` and a third operand follows, reinterpret `?h` as a fixed @@ -1520,6 +1534,74 @@ impl Parser { false } + /// When parsing a prefix-ternary operand and the next token is `{`, + /// produce a context-aware ILO-P009 instead of the bare "expected + /// expression, got `{`". Two shapes are common: + /// + /// 1. `? {:body; ...}` — agent reached for a Rust-style + /// match on a value but used a leading prefix-ternary keyword shape + /// instead of the canonical `?{...}` (single-token subject) or + /// `?(){...}` (multi-token). Hint at the parenthesised form using + /// the parsed operand as the most-likely-intended subject. + /// 2. `?h cond{body}` (non-arm brace) — agent reached for braced- + /// conditional / brace-ternary execution but used the `?h cond a b` + /// prefix-ternary keyword. Point at the right shape for each intent + /// (drop the braces for `?h cond a b`; drop the `?h` prefix for + /// `cond{body}` braced-conditional). + /// + /// Returns `None` if peek isn't `{`, leaving the caller to fall through + /// to the normal operand parse (and its own ILO-P009 if the token is + /// invalid in a different way). + fn prefix_ternary_brace_hint( + &self, + subj_src: Option<&str>, + first_operand_src: Option<&str>, + ) -> Option { + if self.peek() != Some(&Token::LBrace) { + return None; + } + let is_match_arm_shape = self.brace_starts_with_literal_arm(); + let cond_display = first_operand_src.unwrap_or("cond"); + let (msg, hint) = if is_match_arm_shape { + // For `?h x{0:1; _:2}` the agent likely wanted to match on `x`. + // For multi-token operands (where first_operand_src is None + // because it wasn't a bare Ref), recommend the parens form on a + // placeholder so the agent fills in their own expression. + let body = match first_operand_src { + Some(name) => format!( + "match arms need a single bracketed subject. Drop `?{}` and write `?{name}{{:body; _:fallback}}`; for a multi-token subject use `?(){{...}}`", + subj_src.unwrap_or("h") + ), + None => "match arms need a single bracketed subject. Wrap a multi-token match expression in parens: `?(){:body; _:fallback}`".to_string(), + }; + ( + "expected ternary operand, got `{` (looks like match-on-value arms)".to_string(), + body, + ) + } else { + ( + "expected ternary operand, got `{`".to_string(), + format!( + "three conditional shapes: prefix-ternary `?h {cond_display} a b` (no braces), brace-ternary `{cond_display}{{a}}{{b}}` (no `?h`), braced-conditional `{cond_display}{{body}}` (no `?h`, single brace). Pick one" + ), + ) + }; + Some(self.error_hint("ILO-P009", msg, hint)) + } + + /// Peek into the `{...}` block at the cursor and decide whether the first + /// non-trivial token looks like a match-arm literal (`:` or + /// `"text":`). Pure lookahead. Assumes peek is `{`. + fn brace_starts_with_literal_arm(&self) -> bool { + debug_assert!(self.peek() == Some(&Token::LBrace)); + match self.token_at(self.pos + 1) { + Some(Token::Number(_)) | Some(Token::Text(_)) => { + self.token_at(self.pos + 2) == Some(&Token::Colon) + } + _ => false, + } + } + /// Parse `?subj{a}{b}` ternary after the subject has been consumed and /// `looks_like_brace_ternary` returned true. Consumes both brace blocks /// and produces an `Expr::Ternary`. @@ -2271,6 +2353,17 @@ impl Parser { // chokes on `sc "NONE"`. See `parse_prefix_ternary` for the // same swap on the `?=cond a b` family. let first = self.parse_prefix_binop_operand()?; + // Mirror the stmt-position hint in `parse_match_stmt`: intercept + // a `{` before the second operand so `?h cond{...}` and + // `?subj{:body;...}` get actionable hints instead of the + // bare ILO-P009. + let subj_src = subject_source(subj.as_ref()); + let first_src = subject_source(&first); + if let Some(err) = + self.prefix_ternary_brace_hint(subj_src.as_deref(), first_src.as_deref()) + { + return Err(err); + } let second = self.parse_prefix_binop_operand()?; // `?h` general prefix-ternary in expr position. See the matching // block in `parse_match_stmt` for the rationale: literal subject From 6c38e9454de98059f5485c115c7468562a25adfe Mon Sep 17 00:00:00 2001 From: Daniel Morris Date: Wed, 20 May 2026 23:21:14 +0100 Subject: [PATCH 2/4] test: cover ternary-brace hint shapes across both positions Six regression tests: - match-arm shape fires the match-on-value hint and points at the parenthesised or single-token recovery form - match-arm hint uses the parsed first operand as the suggested match subject (not the outer subject `h`) - `?h cond{body}` fires the three-shape hint and does NOT misfire as match-on-value - same shape in expr position (RHS of binding) gets the same hint - canonical `?(){...}` and `?h cond a b` still parse cleanly Parser-layer codes are front-end only, so VM exercises the same path the tree and Cranelift engines would. No engine-specific assertions needed. --- tests/regression_ternary_brace_hint.rs | 151 +++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 tests/regression_ternary_brace_hint.rs diff --git a/tests/regression_ternary_brace_hint.rs b/tests/regression_ternary_brace_hint.rs new file mode 100644 index 00000000..83b3368d --- /dev/null +++ b/tests/regression_ternary_brace_hint.rs @@ -0,0 +1,151 @@ +// Regression tests: context-aware ILO-P009 hints when a `{` appears mid +// prefix-ternary operand parse. Two shapes covered: +// +// 1. `? {:body; _:fallback}` — Rust-style match-on-value +// reached for the wrong shape. Hint should suggest the parenthesised +// `?(){...}` form (or single-token `?{...}`). +// 2. `?h cond{body}` — three-form conditional confusion (`?h cond a b` vs +// `cond{a}{b}` vs `cond{body}`). Hint should enumerate all three. +// +// Both shapes are pure parser diagnostics — exercised via `--parse` so all +// engines (tree / VM / Cranelift) share the same front-end behaviour. + +use std::process::Command; + +fn ilo() -> Command { + Command::new(env!("CARGO_BIN_EXE_ilo")) +} + +fn stderr_of(args: &[&str]) -> String { + let out = ilo().args(args).output().expect("failed to run ilo"); + String::from_utf8_lossy(&out.stderr).into_owned() +} + +// --------------------------------------------------------------------------- +// Hint #1: match-on-value shape mis-keyed with leading `?h` +// --------------------------------------------------------------------------- + +#[test] +fn ternary_match_arm_shape_has_hint() { + // `?h x{0:1; _:2}` — agent wanted `?x{0:1; _:2}` (or `?(x){0:1; _:2}`). + // After parsing subject `h` and first operand `x`, the parser hits `{` + // and would otherwise emit a bare ILO-P009 with no hint. + let src = "main x:n>n;?h x{0:1;_:2}"; + let stderr = stderr_of(&[src, "--vm", "main", "1"]); + assert!( + stderr.contains("ILO-P009"), + "expected ILO-P009, got: {stderr}" + ); + assert!( + stderr.contains("match-on-value arms"), + "expected match-on-value framing in message, got: {stderr}" + ); + assert!( + stderr.contains("?(") || stderr.contains("?x{"), + "expected hint pointing at parenthesised or single-token match form, got: {stderr}" + ); +} + +#[test] +fn ternary_match_arm_shape_uses_first_operand_in_hint() { + // The recommended single-token form should echo the operand the agent + // most likely wanted as the match subject (here: `mn`). + let src = "main x:n>n;mn=x;?h mn{0:1;_:2}"; + let stderr = stderr_of(&[src, "--vm", "main", "1"]); + assert!( + stderr.contains("ILO-P009"), + "expected ILO-P009, got: {stderr}" + ); + assert!( + stderr.contains("?mn{"), + "hint should suggest `?mn{{...}}` using the parsed operand, got: {stderr}" + ); +} + +// --------------------------------------------------------------------------- +// Hint #2: three-form conditional confusion +// --------------------------------------------------------------------------- + +#[test] +fn ternary_h_cond_brace_body_has_hint() { + // `?h ok{1}` — agent wanted one of the three canonical shapes. The + // brace body is a single expression (NOT a `:` arm), so the hint + // should enumerate all three forms rather than the match-on-value + // recommendation. + let src = "main x:n>n;ok=>x 0;?h ok{1}"; + let stderr = stderr_of(&[src, "--vm", "main", "1"]); + assert!( + stderr.contains("ILO-P009"), + "expected ILO-P009, got: {stderr}" + ); + // Should NOT misfire as match-arm shape (single non-literal-arm body) + assert!( + !stderr.contains("match-on-value arms"), + "should not flag as match-on-value when body is a bare expression, got: {stderr}" + ); + // Should cover all three canonical conditional shapes + assert!( + stderr.contains("?h ok a b"), + "hint should mention prefix-ternary `?h cond a b` form with the parsed condition, got: {stderr}" + ); + assert!( + stderr.contains("ok{a}{b}"), + "hint should mention brace-ternary `cond{{a}}{{b}}` form, got: {stderr}" + ); + assert!( + stderr.contains("ok{body}"), + "hint should mention braced-conditional `cond{{body}}` form, got: {stderr}" + ); +} + +#[test] +fn ternary_h_cond_brace_body_hint_in_expr_position() { + // Same shape but in expr position (RHS of binding). The hint must fire + // in `parse_prefix_ternary` (expression path) too, not only the + // statement-position `parse_match_stmt`. + let src = "main x:n>n;ok=>x 0;r=?h ok{1};r"; + let stderr = stderr_of(&[src, "--vm", "main", "1"]); + assert!( + stderr.contains("ILO-P009"), + "expected ILO-P009 in expr position, got: {stderr}" + ); + assert!( + stderr.contains("three conditional shapes") + || (stderr.contains("?h ok a b") && stderr.contains("ok{body}")), + "expected three-shape hint in expr position, got: {stderr}" + ); +} + +// --------------------------------------------------------------------------- +// Negative: shapes that already work must still parse cleanly +// --------------------------------------------------------------------------- + +#[test] +fn parenthesised_match_subject_still_parses() { + // Canonical `?(){...}` form must not be flagged by the new hint. + let src = "main x:n>n;?(mod x 3){0:10;1:20;_:0}"; + let out = ilo() + .args([src, "--vm", "main", "1"]) + .output() + .expect("run ilo"); + assert!( + out.status.success(), + "parenthesised match subject should parse, stderr: {}", + String::from_utf8_lossy(&out.stderr) + ); +} + +#[test] +fn prefix_ternary_three_operand_still_parses() { + // `?h cond a b` keyword form must not be flagged when no `{` follows. + let src = "main x:n>n;ok=>x 0;?h ok 1 0"; + let out = ilo() + .args([src, "--vm", "main", "1"]) + .output() + .expect("run ilo"); + assert!( + out.status.success(), + "?h cond a b should parse, stderr: {}", + String::from_utf8_lossy(&out.stderr) + ); +} From 0048ccd8cbaff5b3bc743bf18dae68e7a649173c Mon Sep 17 00:00:00 2001 From: Daniel Morris Date: Wed, 20 May 2026 23:21:21 +0100 Subject: [PATCH 3/4] docs: add match-on-value example and conditional contrast in skill examples/match-on-value.ilo pins the two shapes the new ILO-P009 hint funnels agents toward: single-token `?subj{lit:body;...}` and multi-token `?(){lit:body;...}`. The example is picked up by the examples_engines test harness so it doubles as a higher-level regression test for the canonical match forms. skills/ilo/ilo-language.md gets a one-line conditional section contrasting the three canonical shapes so the persona-side agent spec mirrors what the hint suggests. Also notes the multi-token match-subject paren form on the existing match line. --- examples/match-on-value.ilo | 34 ++++++++++++++++++++++++++++++++++ skills/ilo/ilo-language.md | 6 +++++- 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 examples/match-on-value.ilo diff --git a/examples/match-on-value.ilo b/examples/match-on-value.ilo new file mode 100644 index 00000000..37f49bab --- /dev/null +++ b/examples/match-on-value.ilo @@ -0,0 +1,34 @@ +-- Match-on-value: the ilo equivalent of Rust's `match n { 1 => ..., _ => ... }` +-- or a C `switch`. This file pins the two canonical shapes the ILO-P009 +-- "match-on-value arms" hint funnels agents toward when they reach for the +-- wrong syntax (e.g. `?h x{0:1;_:2}` thinking `?h` is a `match` keyword). +-- +-- Single-token subject — drop any leading `?h` and prefix the value with `?`. +-- Arms are `:` pairs separated by `;`; `_` is the fallback. +classify x:n>t;?x{0:"zero";1:"one";_:"other"} + +-- Multi-token subject — wrap the whole match expression in parens so the +-- parser knows where the subject ends and the arms begin. Without the parens +-- the parser reads `mod x 3` as a prefix-call sequence and never reaches the +-- match form, which is exactly the failure the ILO-P009 hint is designed to +-- redirect away from. +mod3 x:n>n;?(mod x 3){0:10;1:20;_:0} + +-- Text-valued match subject works identically — arm literals just become +-- text patterns rather than numbers. +ok lvl:t>n;?lvl{"high":2;"low":1;_:0} + +-- run: classify 1 +-- out: one +-- run: classify 5 +-- out: other +-- run: mod3 6 +-- out: 10 +-- run: mod3 7 +-- out: 20 +-- run: mod3 8 +-- out: 0 +-- run: ok "high" +-- out: 2 +-- run: ok "med" +-- out: 0 diff --git a/skills/ilo/ilo-language.md b/skills/ilo/ilo-language.md index 48189600..b102653c 100644 --- a/skills/ilo/ilo-language.md +++ b/skills/ilo/ilo-language.md @@ -29,7 +29,11 @@ Flat early returns at statement: `cls sp:n>t;>=sp 1000 "gold";>=sp 500 "silver"; ## match -`?r{~v:v;^e:^+"failed: "e;_:"unknown"}`. Arms: `"lit":body`, `42:body`, `~v:body` ok-bind, `^e:body` err-bind, `_:body` else. +`?r{~v:v;^e:^+"failed: "e;_:"unknown"}`. Arms: `"lit":body`, `42:body`, `~v:body` ok-bind, `^e:body` err-bind, `_:body` else. Multi-token subject: `?(mod x 3){0:a;1:b;_:c}`. + +## conditional + +Three shapes on a bool `c`, pick the cheapest that fits: `?h c a b` (prefix-ternary, no braces, atoms only), `c{a}{b}` (brace-ternary, two arms), `c{body}` (braced-conditional, single arm). `?h c{...}` is ALWAYS wrong, drop the `?h` or drop the braces. ## results From 39dad29da3463ace9fc8e9b7987be5462589a592 Mon Sep 17 00:00:00 2001 From: Daniel Morris Date: Wed, 20 May 2026 23:28:24 +0100 Subject: [PATCH 4/4] trim ilo-language skill to stay under 1000-token budget The conditional contrast addition pushed the file from 987 to 1096 tokens, over the per-module budget enforced by scripts/check-skill- tokens.py. Drop the bool-conditional section, the three shapes are already covered exhaustively in examples/conditional-shapes.ilo and SPEC.md (section 'Conditionals and Guards'). Keep the multi-token match-subject parens note since that's the canonical shape the new ILO-P009 hint funnels agents toward, and a one-line callout next to the match arms is the highest-value placement for it. Final: ilo-language at 999 tokens, total 6789. --- skills/ilo/ilo-language.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/skills/ilo/ilo-language.md b/skills/ilo/ilo-language.md index b102653c..2e67d137 100644 --- a/skills/ilo/ilo-language.md +++ b/skills/ilo/ilo-language.md @@ -29,11 +29,7 @@ Flat early returns at statement: `cls sp:n>t;>=sp 1000 "gold";>=sp 500 "silver"; ## match -`?r{~v:v;^e:^+"failed: "e;_:"unknown"}`. Arms: `"lit":body`, `42:body`, `~v:body` ok-bind, `^e:body` err-bind, `_:body` else. Multi-token subject: `?(mod x 3){0:a;1:b;_:c}`. - -## conditional - -Three shapes on a bool `c`, pick the cheapest that fits: `?h c a b` (prefix-ternary, no braces, atoms only), `c{a}{b}` (brace-ternary, two arms), `c{body}` (braced-conditional, single arm). `?h c{...}` is ALWAYS wrong, drop the `?h` or drop the braces. +`?r{~v:v;^e:^+"failed: "e;_:"unknown"}`. Arms: `"lit":body`, `42:body`, `~v:body` ok-bind, `^e:body` err-bind, `_:body` else. Multi-token subj wraps: `?(e){…}`. ## results