Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions examples/match-on-value.ilo
Original file line number Diff line number Diff line change
@@ -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 `<literal>:<body>` 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
2 changes: 1 addition & 1 deletion skills/ilo/ilo-language.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +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.
`?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

Expand Down
93 changes: 93 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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. `?<subj> <oper>{<lit>:body; ...}` — agent reached for a Rust-style
/// match on a value but used a leading prefix-ternary keyword shape
/// instead of the canonical `?<subj>{...}` (single-token subject) or
/// `?(<expr>){...}` (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<ParseError> {
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}{{<lit>:body; _:fallback}}`; for a multi-token subject use `?(<expr>){{...}}`",
subj_src.unwrap_or("h")
),
None => "match arms need a single bracketed subject. Wrap a multi-token match expression in parens: `?(<expr>){<lit>: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 (`<num>:` 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`.
Expand Down Expand Up @@ -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{<lit>: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
Expand Down
151 changes: 151 additions & 0 deletions tests/regression_ternary_brace_hint.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// Regression tests: context-aware ILO-P009 hints when a `{` appears mid
// prefix-ternary operand parse. Two shapes covered:
//
// 1. `?<subj> <oper>{<lit>:body; _:fallback}` — Rust-style match-on-value
// reached for the wrong shape. Hint should suggest the parenthesised
// `?(<expr>){...}` form (or single-token `?<subj>{...}`).
// 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 `<lit>:` 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 `?(<expr>){...}` 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)
);
}
Loading