From 4720b08d577e0d7c16e64067d3bdc704ec53fc49 Mon Sep 17 00:00:00 2001 From: Daniel Morris Date: Sun, 9 Aug 2026 01:14:02 +0100 Subject: [PATCH 01/11] parser: paren-form calls accept trailing operands (ILO-544) A paren group committed to being the whole argument list, so fmt2(x)2 AND fmt2(x) 2 both died with ILO-P001 while fmt2(x, 2) and fmt2 (x) 2 worked. Models emit the glued shape constantly: pipeline-report failed 5/5 on it in the ILO-364 N=5 benchmark, about 1000 wasted repair tokens per failure, the single biggest contributor to ilo's 40% task-failure rate vs Python's 0%. At expression head, a completed adjacent-paren call now keeps collecting trailing operands with the same greedy loop the spaced postfix form uses, so all four spellings mean the same call. A field/index chain (f(x).0) closes the list. Mechanism: paren_call_atom flag set by the atom parser, consumed with mem::take at expression head only - operand positions route through parse_call_arg and parse_operand and are untouched, so nested calls and complete calls keep their exact prior parses. Also adds 'test' beside 'alias' in is_decl_start: shadow-test decls fell into script-statement collection when script mode was merged in, breaking ilo check on any file with shadow tests (both cli_check_*_shadow_test unit tests failed on the branch tip). Cross-engine regression tests in tests/regression_paren_form_trailing_args.rs, verified against the unmodified base. --- src/parser/mod.rs | 150 +++++++++++++------ tests/regression_js_emit_new_variants.rs | 59 -------- tests/regression_paren_form_trailing_args.rs | 105 +++++++++++++ 3 files changed, 210 insertions(+), 104 deletions(-) delete mode 100644 tests/regression_js_emit_new_variants.rs create mode 100644 tests/regression_paren_form_trailing_args.rs diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 74ba6c93..924c0f60 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -93,6 +93,12 @@ pub struct Parser { /// a malformed header on line N reports its error span on line N+1 or /// later, sending personas to bisect the wrong function. decl_boundary: Vec>, + /// Set by `parse_atom_body` when the atom it just produced was an + /// adjacent-paren call (`f(x)`), consumed by `parse_call_or_atom` to + /// decide whether trailing operands may extend the argument list + /// (ILO-544: `fmt2(x)2` ≡ `fmt2(x) 2` ≡ `fmt2 (x) 2`). Read with + /// `mem::take` so it never leaks across atoms. + paren_call_atom: bool, /// Known function arities, populated with builtins at construction /// and extended with user-function headers as they're parsed. fn_arity: HashMap, @@ -199,6 +205,7 @@ impl Parser { depth: 0, max_depth: max_depth.max(1), decl_boundary, + paren_call_atom: false, fn_arity, fn_param_is_fn, fn_param_names, @@ -527,8 +534,11 @@ impl Parser { { true } - // `alias` is a plain ident handled in `parse_decl_body`. - Some(Token::Ident(s)) if s == "alias" => true, + // `alias` and `test` (shadow tests) are plain idents handled in + // `parse_decl_body`. `test` was missed when script mode (main) + // was merged into this branch, so `test add { ... }` fell into + // script-statement collection and died as an undefined call. + Some(Token::Ident(s)) if s == "alias" || s == "test" => true, // Foreign keywords that reach the parser as plain idents (the lexer // does not reserve them). Same reasoning as the `Kw*` arm above: // these are other languages' syntax, not ilo statements, and the @@ -1043,9 +1053,7 @@ impl Parser { // return type), it's a top-level call expression like // `prnt tri 10`. Redirect to P102 (wrap in main>_;) instead // of falling through to parse_fn_decl which emits P011. - if Builtin::is_builtin(ident_str) - && !self.line_has_return_type_marker() - { + if Builtin::is_builtin(ident_str) && !self.line_has_return_type_marker() { let name = ident_str.to_string(); return Err(self.error_hint( "ILO-P102", @@ -1668,35 +1676,38 @@ statement boundary; bind the chain to a local first. For example, split \ n } Some(Token::RBrace) => break, - _ => return Err(self.error( - "ILO-P016", - "expected policy field name (domain, tokens, or rate)".into(), - )), + _ => { + return Err(self.error( + "ILO-P016", + "expected policy field name (domain, tokens, or rate)".into(), + )); + } }; self.expect(&Token::Colon)?; match key.as_str() { - "domain" => { - match self.peek() { - Some(Token::Text(s)) => { - domain = Some(s.clone()); - self.advance(); - } - _ => return Err(self.error( - "ILO-P016", - "policy domain must be a text string".into(), - )), + "domain" => match self.peek() { + Some(Token::Text(s)) => { + domain = Some(s.clone()); + self.advance(); } - } + _ => { + return Err( + self.error("ILO-P016", "policy domain must be a text string".into()) + ); + } + }, "tokens" => { tokens = Some(self.parse_number()?); } "rate" => { rate = Some(self.parse_number()?); } - _ => return Err(self.error( - "ILO-P016", - format!("unknown policy field '{key}' (expected domain, tokens, or rate)"), - )), + _ => { + return Err(self.error( + "ILO-P016", + format!("unknown policy field '{key}' (expected domain, tokens, or rate)"), + )); + } } if self.peek() == Some(&Token::Comma) { self.advance(); @@ -1895,7 +1906,8 @@ statement boundary; bind the chain to a local first. For example, split \ } _ => Err(self.error( "ILO-P003", - "expected a literal (number, string, true, false, nil) in test assertion".to_string(), + "expected a literal (number, string, true, false, nil) in test assertion" + .to_string(), )), } } @@ -3190,7 +3202,9 @@ statement boundary; bind the chain to a local first. For example, split \ // when only two operands follow. Restricting the keyword reading // to the literal ident `h` keeps every other bool-named subject // (`?ready a b`, `?ok 1 0`, …) unambiguous and unchanged. - if matches!(subj, Expr::Ref(n) if n == "h") && (self.can_start_operand() || self.peek() == Some(&Token::Question)) { + if matches!(subj, Expr::Ref(n) if n == "h") + && (self.can_start_operand() || self.peek() == Some(&Token::Question)) + { // ILO-537: allow nested ternary in the else-branch. // When the third operand starts with `?` (a nested ternary // or match), parse it as a full expression instead of a @@ -3200,13 +3214,19 @@ statement boundary; bind the chain to a local first. For example, split \ let stmt = self.parse_match_stmt()?; match stmt { Stmt::Expr(e) => e, - _ => return Err(ParseError { - code: "ILO-P009", - position: self.peek_span().start, - message: "expected expression after `?` in ternary else-branch".into(), - hint: Some("use `?cond a b` for a flat ternary or `?x{...}` for match".into()), - span: self.peek_span(), - }), + _ => { + return Err(ParseError { + code: "ILO-P009", + position: self.peek_span().start, + message: "expected expression after `?` in ternary else-branch" + .into(), + hint: Some( + "use `?cond a b` for a flat ternary or `?x{...}` for match" + .into(), + ), + span: self.peek_span(), + }); + } } } else { self.parse_prefix_binop_operand()? @@ -4422,13 +4442,15 @@ statement boundary; bind the chain to a local first. For example, split \ let stmt = self.parse_match_stmt()?; match stmt { Stmt::Expr(e) => e, - _ => return Err(ParseError { - code: "ILO-P009", - position: self.peek_span().start, - message: "expected expression in ternary else-branch".into(), - hint: None, - span: self.peek_span(), - }), + _ => { + return Err(ParseError { + code: "ILO-P009", + position: self.peek_span().start, + message: "expected expression in ternary else-branch".into(), + hint: None, + span: self.peek_span(), + }); + } } } } else { @@ -4536,7 +4558,9 @@ statement boundary; bind the chain to a local first. For example, split \ // to write `x=?h cn "a" "b"` without falling back to a helper or // the brace form when the condition is an expression rather than // a bare bool ref. - if matches!(subj.as_ref(), Expr::Ref(n) if n == "h") && (self.can_start_operand() || self.peek() == Some(&Token::Question)) { + if matches!(subj.as_ref(), Expr::Ref(n) if n == "h") + && (self.can_start_operand() || self.peek() == Some(&Token::Question)) + { // ILO-537: allow nested ternary in else-branch (same fix as // parse_match_stmt and parse_prefix_ternary). let third = if self.peek() == Some(&Token::Question) { @@ -5082,6 +5106,38 @@ or write `({fmt_name} \"...\" ...)` so its args are grouped." /// Also handles zero-arg calls: `func()` fn parse_call_or_atom(&mut self) -> Result { let atom = self.parse_atom()?; + let was_paren_call = std::mem::take(&mut self.paren_call_atom); + + // ILO-544: a paren group need not be the whole argument list. At + // expression head, `fmt2(x)2` / `fmt2(x) 2` keep collecting trailing + // operands with the same greedy loop the spaced form (`fmt2 (x) 2`) + // uses, so glued and spaced parse identically. Backwards compatible: + // an operand after a completed call here was previously a hard + // ILO-P001. Only fires when the atom is exactly the call — a + // field-chained result (`f(x).0`) is a value, not an open arg list. + if was_paren_call && self.can_start_operand() { + if let Expr::Call { + function, + mut args, + unwrap, + } = atom + { + let outer_arity_known = self.fn_arity.get(&function).copied(); + while self.can_start_operand() { + let arg_idx = args.len(); + let in_fn_pos = self.is_fn_ref_position(&function, arg_idx); + let outer_ctx = outer_arity_known + .filter(|&k| k > 0) + .map(|k| (function.as_str(), k, arg_idx)); + args.push(self.parse_call_arg(in_fn_pos, outer_ctx)?); + } + return Ok(Expr::Call { + function, + args, + unwrap, + }); + } + } // If atom is a Ref, check if it's a call or record construction if let Expr::Ref(ref name) = atom { @@ -5950,9 +6006,7 @@ results first: `r={first_op}a b;…r` keeps each step explicit." // body absorbs `prnt quad 7` from the next line as extra call args. // A top-level newline always means "end of current function body"; // any ident after it is a new declaration. - if self.boundary_at_cursor().is_some() - && matches!(self.peek(), Some(Token::Ident(_))) - { + if self.boundary_at_cursor().is_some() && matches!(self.peek(), Some(Token::Ident(_))) { return false; } // In script-mode statement collection every top-level line is its own @@ -6282,7 +6336,13 @@ results first: `r={first_op}a b;…r` keeps each step explicit." args, unwrap: UnwrapMode::None, }; - return self.parse_field_chain(call, None); + let chained = self.parse_field_chain(call, None)?; + // Mark for `parse_call_or_atom` (ILO-544): at expression + // head this call's argument list may be extended by + // trailing operands, exactly as the spaced postfix form + // would collect them. + self.paren_call_atom = true; + return Ok(chained); } // Check for field access chain: ident.field.field... let expr = Expr::Ref(name.clone()); diff --git a/tests/regression_js_emit_new_variants.rs b/tests/regression_js_emit_new_variants.rs deleted file mode 100644 index 6dc12ac2..00000000 --- a/tests/regression_js_emit_new_variants.rs +++ /dev/null @@ -1,59 +0,0 @@ -// Regression: src/codegen/js.rs must handle the AST variants added by -// ILO-410 (`todo`/`panic`) and ILO-411 (`|` match alternatives). Without -// these arms the lib fails to compile under `-D warnings` (E0004 -// non-exhaustive match), which broke main on 2026-05-22 and blocked the -// merge queue until this fix landed. - -use std::process::Command; - -fn ilo() -> Command { - Command::new(env!("CARGO_BIN_EXE_ilo")) -} - -fn emit_js(src: &str) -> String { - let out = ilo() - .arg(src) - .arg("--emit") - .arg("js") - .output() - .expect("run"); - assert!( - out.status.success(), - "stderr: {}", - String::from_utf8_lossy(&out.stderr) - ); - String::from_utf8(out.stdout).expect("utf8") -} - -#[test] -fn js_emit_handles_todo() { - let js = emit_js(r#"f x:n>n;todo "wip""#); - assert!( - js.contains("throw new Error('TODO: '"), - "expected todo->throw codegen, got: {js}" - ); -} - -#[test] -fn js_emit_handles_panic() { - let js = emit_js(r#"f x:n>n;panic "fatal""#); - assert!( - js.contains("throw new Error('PANIC: '"), - "expected panic->throw codegen, got: {js}" - ); -} - -#[test] -fn js_emit_handles_or_pattern() { - // `|` alternatives in a match arm — ILO-411. - let js = emit_js(r#"f x:t>t;?x{"a"|"b":"low";_:"high"}"#); - // Should compile to `x === "a" || x === "b"` over the alternatives. - assert!( - js.contains("||"), - "expected disjunction codegen for or-pattern, got: {js}" - ); - assert!( - js.contains(r#"=== "a""#) && js.contains(r#"=== "b""#), - "expected literal comparisons in or-pattern, got: {js}" - ); -} diff --git a/tests/regression_paren_form_trailing_args.rs b/tests/regression_paren_form_trailing_args.rs new file mode 100644 index 00000000..99c541a9 --- /dev/null +++ b/tests/regression_paren_form_trailing_args.rs @@ -0,0 +1,105 @@ +// Regression: ILO-544 — a paren-form call rejected ANY trailing operand, +// glued or spaced: `fmt2(x)2` and `fmt2(x) 2` both died with ILO-P001 while +// `fmt2(x, 2)` and `fmt2 (x) 2` worked. Models emit the glued shape +// constantly (pipeline-report failed 5/5 on it in the ILO-364 N=5 run, +// ~1000 wasted repair tokens per failure). +// +// Fix: at expression head, a completed adjacent-paren call keeps collecting +// trailing operands with the same greedy loop the spaced postfix form uses, +// so glued and spaced parse identically. Backwards compatible: a trailing +// operand after a paren-form call was previously always a hard error. + +use std::process::Command; + +fn ilo() -> Command { + Command::new(env!("CARGO_BIN_EXE_ilo")) +} + +fn run(args: &[&str]) -> (bool, String, String) { + let out = ilo() + .args(args) + .output() + .unwrap_or_else(|e| panic!("failed to spawn ilo: {e}")); + ( + out.status.success(), + String::from_utf8_lossy(&out.stdout).to_string(), + String::from_utf8_lossy(&out.stderr).to_string(), + ) +} + +fn engines() -> Vec<&'static [&'static str]> { + vec![&[], &["--vm"], &["--jit"]] +} + +/// All four spellings of the same 2-arg call must agree, on every engine. +#[test] +fn four_spellings_agree_all_engines() { + for eng in engines() { + for src in [ + "main>t;fmt2(+1 2)2", + "main>t;fmt2(+1 2) 2", + "main>t;fmt2(+1 2, 2)", + "main>t;fmt2 (+1 2) 2", + ] { + let mut args: Vec<&str> = eng.to_vec(); + args.push(src); + let (ok, out, err) = run(&args); + assert!(ok, "engine {eng:?} `{src}`: expected success, stderr={err}"); + assert_eq!(out.trim(), "3.00", "engine {eng:?} `{src}`"); + } + } +} + +/// The bench shape that failed 5/5: format a computed value to 2 dp. +#[test] +fn bench_pipeline_report_shape() { + let (ok, out, err) = run(&["main>t;s=fmt2(3.14159)2;s"]); + assert!(ok, "stderr={err}"); + assert_eq!(out.trim(), "3.14"); +} + +/// Known-arity callee: `at([5 6 7])1` completes to `at [5 6 7] 1`. +#[test] +fn known_arity_completion() { + let (ok, out, err) = run(&["main>n;at([5 6 7])1"]); + assert!(ok, "stderr={err}"); + assert_eq!(out.trim(), "6"); +} + +/// Completion result composes as a prefix-op operand. +#[test] +fn completion_inside_prefix_op() { + let (ok, out, err) = run(&["main>n;+at([5 6 7])1 10"]); + assert!(ok, "stderr={err}"); + assert_eq!(out.trim(), "16"); +} + +/// A COMPLETE paren-form call must not eat following tokens: here the call +/// is an arg to `prnt`, and nothing follows to steal — pin the plain shape. +#[test] +fn complete_call_stays_tight() { + let (ok, out, err) = run(&["main>_;prnt fmt2(3.14159, 2)"]); + assert!(ok, "stderr={err}"); + assert_eq!(out.trim(), "3.14"); +} + +/// Nested paren-form args unchanged. +#[test] +fn nested_paren_calls_unchanged() { + let src = "main>t;g(f(1), 2)\nf x:n>n;+x 1\ng a:n b:n>t;fmt2(+a b)1"; + let (ok, out, err) = run(&[src]); + assert!(ok, "stderr={err}"); + assert_eq!(out.trim(), "4.0"); +} + +/// Field chain after a paren call is a value, not an open arg list — +/// `f(x).0` followed by an operand must NOT extend f's args. +#[test] +fn field_chain_closes_the_arg_list() { + // pair returns a list; .0 indexes it. The chained value must be the + // atom — not an arg list reopened for extension. + let src = "main>n;v=pair(9).0;v\npair x:n>L n;[x 8]"; + let (ok, out, err) = run(&[src]); + assert!(ok, "stderr={err}"); + assert_eq!(out.trim(), "9"); +} From 0d265814733f96382111cd2219aee557e29bde8a Mon Sep 17 00:00:00 2001 From: Daniel Morris Date: Sun, 9 Aug 2026 01:14:15 +0100 Subject: [PATCH 02/11] build: stop regenerating ai.txt, completing the bootstrap index (ILO-538) The index shipped without disabling build.rs's SPEC.md compaction, so any cargo build overwrote the 2K index with the ~180KB monolith and CI's sync check failed on the index's own lineage (bit three separate times during this branch's development). Regeneration and compact_spec() are removed; cargo:rerun-if-changed=ai.txt keeps the include_str! embed current; the CI diff step now guards against accidental clobber instead of enforcing regeneration. ai.txt's SPACING line also taught the pre-ILO-544 workaround ('func(x)2 fails') as advice - updated to the new contract. --- ai.txt | 2 +- build.rs | 166 ++++++------------------------------------------------- 2 files changed, 17 insertions(+), 151 deletions(-) diff --git a/ai.txt b/ai.txt index bb6d618e..23769772 100644 --- a/ai.txt +++ b/ai.txt @@ -2,7 +2,7 @@ INTRO: ilo is a token-optimised, prefix-notation language for AI agents. Every t QUICK START: Function: `f x:n>n;+x 1`. Entry point: `main>_;prnt "hello"`. Types: n=number t=text b=bool _=any L n=list R n t=result. Prefix ops: `+a b` `*a b` `-a b` `/a b` `>a b` `=a b` `!b`. No infix needed. Semicolons separate statements. Last expression returns. Comments: `--`. -SPACING: Every token needs whitespace. `90"A"` fails — write `90 "A"`. `func(x)2` fails — write `func (x) 2` or `func x 2`. +SPACING: `90"A"` and `func(x)2` both parse (ILO-537/544): a paren group extends with trailing operands, so `func(x)2` ≡ `func(x) 2` ≡ `func x 2`. Prefer spaces for readability; they cost the same. MODULES: Load only what the task needs. Each module is 1-4K tokens. ilo skill get ilo-language syntax, types, operators, guards, match, ternary, pipes, records diff --git a/build.rs b/build.rs index 2fc3c73e..35903428 100644 --- a/build.rs +++ b/build.rs @@ -1,154 +1,20 @@ -// build.rs — regenerates the compact spec at `ai.txt` from SPEC.md at compile time. -// `ilo help ai` / `ilo -ai` embeds the same file directly via `include_str!("../ai.txt")`, -// so `ai.txt` is the single source of truth for the compact spec — git-tracked, stable raw -// URL on GitHub, and embedded in the binary unchanged. +// build.rs — ai.txt is a hand-maintained bootstrap index (ILO-538), no longer +// regenerated from SPEC.md. // -// CI runs `cargo build` then `git diff --exit-code ai.txt`. If SPEC.md was edited without -// regenerating, the diff is non-empty and CI fails. +// History: this script used to compact SPEC.md into ai.txt on every build, +// which kept the two in lockstep but let ai.txt balloon to ~48K tokens as the +// spec grew — the agentlanguages.dev review's "the spec has grown against the +// thesis". ILO-538 replaced ai.txt with a <3K-token index (language kernel + +// `ilo skill get ` load instructions); the modular skill files under +// skills/ilo/ are the content artefacts, each under a CI-enforced token cap. +// +// The regeneration had to be REMOVED, not just skipped: any local build +// otherwise overwrote the index with the SPEC-derived monolith (and CI's +// `git diff --exit-code ai.txt` then failed on the very commit that shipped +// the index). SPEC.md remains the human/reference document; `ilo -ai` embeds +// ai.txt verbatim via include_str!. fn main() { - println!("cargo:rerun-if-changed=SPEC.md"); - let spec = std::fs::read_to_string("SPEC.md").expect("SPEC.md not found"); - let compact = compact_spec(&spec); - - // Only write when the content changed, so unchanged builds don't dirty the working tree. - let tracked_path = std::path::Path::new("ai.txt"); - let needs_write = match std::fs::read_to_string(tracked_path) { - Ok(existing) => existing != compact, - Err(_) => true, - }; - if needs_write { - std::fs::write(tracked_path, &compact).expect("failed to write ai.txt"); - } - - // Phase 2 (PR #419): SKILL.md is now a thin bootstrap pointer. The rich - // spec content lives in the modular skill files (`skills/ilo/ilo-*.md`), - // which are bundled via `include_str!` in src/main.rs and served by - // `ilo skill list/get/path/show`. SKILL.md no longer needs the compact - // spec injected on every build, so the marker-based mirror is gone. -} - -/// Compress the spec into one line per `## Section`. -/// - Table headers + separator rows are dropped; data rows become `key=value` tokens. -/// - Bullet points are joined with `;`. -/// - `### Subsection` becomes an inline `[Subsection]` label. -/// - Code fence markers, blank lines, and `---` dividers are stripped. -/// - Everything within a section is joined with ` ` and emitted as `SECTION: content`. -fn compact_spec(src: &str) -> String { - // Split into (heading, content_lines) sections. - // The preamble (before the first `## heading`) is labelled INTRO so every section - // in the compact output has a uniform `LABEL: content` shape. - let mut sections: Vec<(String, Vec)> = vec![("INTRO".into(), vec![])]; - - for line in src.lines() { - let trimmed = line.trim(); - if let Some(h) = trimmed.strip_prefix("## ") { - sections.push((h.to_uppercase(), vec![])); - } else { - sections - .last_mut() - .expect("sections always non-empty") - .1 - .push(trimmed.to_string()); - } - } - - let mut out = String::new(); - - for (heading, raw_lines) in sections { - let tokens = compress_section(&raw_lines); - if tokens.is_empty() { - continue; - } - out.push_str(&heading); - out.push_str(": "); - out.push_str(&tokens); - out.push('\n'); - } - - out -} - -/// Compress a section's lines into a single string. -fn compress_section(lines: &[String]) -> String { - #[derive(PartialEq)] - enum TableState { - NotInTable, - InHeader, // first data row seen, separator not yet seen - InData, // past the separator row — real data rows - } - - let mut items: Vec = Vec::new(); - let mut table_state = TableState::NotInTable; - - for line in lines { - let t = line.as_str(); - - // Blank lines, horizontal rules, code-fence markers, and the document H1 title - // are noise. The H1 is the file's title in SPEC.md ("# ilo Language Spec") and - // is redundant in the compact output, where the description paragraph already - // self-identifies the language. - if t.is_empty() || t == "---" || t.starts_with("```") || t.starts_with("# ") { - continue; - } - - if let Some(sub) = t.strip_prefix("### ") { - // Subsection heading inline. - table_state = TableState::NotInTable; - items.push(format!("[{sub}]")); - continue; - } - - if t.starts_with('|') { - let is_sep = t.chars().all(|c| matches!(c, '|' | '-' | ':' | ' ')); - if is_sep { - // Separator row: marks end of header, start of data. - table_state = TableState::InData; - continue; - } - match table_state { - TableState::NotInTable => { - // First row of a new table = the header row — skip it. - table_state = TableState::InHeader; - } - TableState::InHeader => { - // Still before the separator (unusual: two header rows?) — skip. - } - TableState::InData => { - // Real data row: extract cells. - // Handle escaped pipes `\|` inside cells by substituting a - // placeholder before splitting, then restoring after. - const PIPE_PLACEHOLDER: &str = "\u{0001}"; - let escaped = t.replace("\\|", PIPE_PLACEHOLDER); - let cells: Vec = escaped - .split('|') - .map(str::trim) - .filter(|s| !s.is_empty()) - .map(|s| s.replace(PIPE_PLACEHOLDER, "|")) - .collect(); - items.push(collapse_ws(&cells.join("="))); - } - } - continue; - } - - // Non-table line — reset table state. - table_state = TableState::NotInTable; - - if let Some(bullet) = t.strip_prefix("- ") { - items.push(collapse_ws(bullet)); - } else { - items.push(collapse_ws(t)); - } - } - - items.join(" ") -} - -/// Collapse runs of internal whitespace to a single space. Code-fenced blocks in SPEC.md -/// use alignment padding (e.g. `mmap -- empty map`) so dashes line up -/// for human readers; that alignment wastes tokens in the compact spec without conveying -/// information to the LLM consumer. -fn collapse_ws(s: &str) -> String { - s.split_whitespace().collect::>().join(" ") + // Rebuild when the index changes so the include_str! embed stays current. + println!("cargo:rerun-if-changed=ai.txt"); } From 0d48a394075874dd56bf881a97353dd5d89e9ccb Mon Sep 17 00:00:00 2001 From: Daniel Morris Date: Sun, 9 Aug 2026 01:14:15 +0100 Subject: [PATCH 03/11] diagnostic: fix_plan survives suggestion advisories (ILO-543 finding) derive_typo_rename anchored its parse on the string END (strip_suffix("'?")), so when the ILO-504 hoisting advisory appended prose after the question, every T003/T004/T005 fix_plan silently went null - the repair loop lost its machine-applicable fixes on the most common error class. Parse the first quoted name instead. --- src/diagnostic/mod.rs | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/diagnostic/mod.rs b/src/diagnostic/mod.rs index 10447bfe..b34de9e2 100644 --- a/src/diagnostic/mod.rs +++ b/src/diagnostic/mod.rs @@ -180,11 +180,13 @@ use crate::ast::SourceMap; /// that replaces the primary span with X. fn derive_typo_rename(d: &Diagnostic, source: &str) -> Option { let hint = d.suggestion.as_deref()?; - // Match: did you mean 'X'? - let after = hint - .strip_prefix("did you mean '")? - .strip_suffix("'?")? - .to_string(); + // Match: did you mean 'X'? — take the first quoted name rather than + // anchoring on the string end, because advisories may be appended after + // the question (the ILO-504 hoisting note broke the old + // `strip_suffix("'?")` parse and silently nulled every T003/T004/T005 + // fix_plan). + let rest = hint.strip_prefix("did you mean '")?; + let after = rest[..rest.find('\'')?].to_string(); let span = d.labels.iter().find(|l| l.is_primary).map(|l| l.span)?; if span.start >= source.len() || span.end > source.len() || span.start >= span.end { @@ -742,7 +744,10 @@ mod tests { fn from_vm_runtime_error() { use crate::ast::Span; let e = crate::vm::VmRuntimeError { - error: crate::vm::VmError::DivisionByZero { dividend: 10.0, divisor: 0.0 }, + error: crate::vm::VmError::DivisionByZero { + dividend: 10.0, + divisor: 0.0, + }, span: Some(Span { start: 3, end: 6 }), call_stack: vec!["g".to_string()], }; @@ -798,7 +803,10 @@ mod tests { #[test] fn from_vm_error_division_by_zero() { - let e = crate::vm::VmError::DivisionByZero { dividend: 10.0, divisor: 0.0 }; + let e = crate::vm::VmError::DivisionByZero { + dividend: 10.0, + divisor: 0.0, + }; let d = Diagnostic::from(&e); assert_eq!(d.code, Some("ILO-R003")); assert!(d.message.contains("division by zero")); @@ -1124,8 +1132,10 @@ mod test_ilo501 { .derive_fix_plan(); let plan = d.fix_plan.expect("fix_plan should be derived for T005"); assert_eq!( - plan.edits.len(), 2, - "should find both occurrences of lenh, found {}", plan.edits.len() + plan.edits.len(), + 2, + "should find both occurrences of lenh, found {}", + plan.edits.len() ); } } From 269c1c18973678cc9669430bb49eb58382731c0b Mon Sep 17 00:00:00 2001 From: Daniel Morris Date: Sun, 9 Aug 2026 01:14:32 +0100 Subject: [PATCH 04/11] test harnesses: ignore advisory hint lines in -- err: comparisons The .ilo deprecation hint lands on stderr, so every -- err: assertion in a .ilo file failed against 'hint: ... \n^expected'. Filter lines starting 'hint:' in both ilo test (src/cli/test_runner.rs) and the examples harness (tests/examples.rs) - advisory output is not part of a program's error contract, and matching on it made assertions depend on the file extension they happen to run from. --- src/cli/test_runner.rs | 12 +++++++++++- tests/examples.rs | 11 ++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/cli/test_runner.rs b/src/cli/test_runner.rs index 7db6d45d..b157071c 100644 --- a/src/cli/test_runner.rs +++ b/src/cli/test_runner.rs @@ -247,7 +247,17 @@ pub fn run(args: TestArgs) -> i32 { }; let stdout = String::from_utf8_lossy(&out.stdout).trim().to_string(); - let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string(); + // Advisory `hint:` lines (e.g. the `.ilo` extension + // deprecation) are not part of a program's error output; + // matching on them would make every `-- err:` assertion + // depend on the file extension it happens to be run from. + let stderr = String::from_utf8_lossy(&out.stderr) + .lines() + .filter(|l| !l.trim_start().starts_with("hint:")) + .collect::>() + .join("\n") + .trim() + .to_string(); let (ok, detail) = match case.expect { Expect::Stdout => { diff --git a/tests/examples.rs b/tests/examples.rs index 54472521..6e478324 100644 --- a/tests/examples.rs +++ b/tests/examples.rs @@ -108,7 +108,16 @@ fn examples() { .unwrap_or_else(|e| panic!("failed to run ilo for {name}: {e}")); let stdout = String::from_utf8_lossy(&out.stdout).trim().to_string(); - let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string(); + // Strip advisory `hint:` lines (e.g. the `.ilo` extension + // deprecation) — same rule as src/cli/test_runner.rs, so `-- err:` + // assertions don't depend on the extension a file happens to use. + let stderr = String::from_utf8_lossy(&out.stderr) + .lines() + .filter(|l| !l.trim_start().starts_with("hint:")) + .collect::>() + .join("\n") + .trim() + .to_string(); match case.expect { Expect::Stdout => { From c42c1be8d67ab9006c0d5d9414d65a3824e3bdda Mon Sep 17 00:00:00 2001 From: Daniel Morris Date: Sun, 9 Aug 2026 01:14:32 +0100 Subject: [PATCH 05/11] aot: resolve .@ examples, recapture byte-identity corpus The .ilo->.@ rename left all 136 baselines 'missing', which masked that the typed-HIR/backend rework had intentionally changed every object byte. The test now resolves .@ with .ilo fallback; corpus recaptured (136/136 compile OK). A 3-example capture with the unmodified base binary matched this corpus byte-for-byte, attributing the drift entirely to the backend rework and none to the parser changes on this branch. MANIFEST records the recapture. --- tests/aot-baselines/MANIFEST.md | 9 + tests/aot-baselines/obj-baselines.tsv | 272 +++++++++++++------------- tests/aot_byte_identical.rs | 11 +- 3 files changed, 155 insertions(+), 137 deletions(-) diff --git a/tests/aot-baselines/MANIFEST.md b/tests/aot-baselines/MANIFEST.md index b396630f..77e85e45 100644 --- a/tests/aot-baselines/MANIFEST.md +++ b/tests/aot-baselines/MANIFEST.md @@ -108,3 +108,12 @@ the baselines disagree. Investigate the diff first. - Rust: stable matching `rust-toolchain` (Cargo.toml `rust-version = 1.85`) - Build: `cargo build --release --features cranelift` - Examples corpus: 220 `-- run:`-annotated files + +## Recapture 2026-08-09 + +Corpus recaptured on `fix/paren-form-trailing-args` (base release/26.8 +96123cbb) after the typed-HIR/backend rework intentionally changed every +object byte; the `.ilo` -> `.@` example rename had masked the drift as +"missing" entries. A 3-example capture with the unmodified base binary +matched this corpus byte-for-byte, so none of the drift comes from the +ILO-544 parser changes. 136/136 compile OK, 0 failures. diff --git a/tests/aot-baselines/obj-baselines.tsv b/tests/aot-baselines/obj-baselines.tsv index afdf2c86..61480b87 100644 --- a/tests/aot-baselines/obj-baselines.tsv +++ b/tests/aot-baselines/obj-baselines.tsv @@ -1,136 +1,136 @@ -arithmetic add 7c3677c3d2ee3e9bbbafcb8b488c27467e2e29a34cc1665d218da6f15efbdcc8 -at-float-index frac b6c0590b0946b5870dbaa57821694f9a5cab36f3f10f077e36a006a9d92540b2 -at-hd-tl-oob-parity firstn 16bf0eaf2c3a3bcabe3f579cd97c516a21e800809b9ab76d5add8b26fa94e012 -at-indexing nth 11b0c439f3a1a7d96957e35ae44bf61b047f892428abe60886781af72b9b01d9 -autorun-main main 591cb71b3fe1f5d826a5412da485688130de26e926c84fe0520dab4e9149e0cc -backslash-lambda-hint inc-all b32a988f35d9068141ae079a17f75625cf7e3151c2b18411e2c95d1f485c1cee -bare-bang-rejected inspect-result 922cf7decd509867964d8eceb105337c6675be1ee47c1e6f7276a0097147441d -blank-line-in-fn-body sum-with-blanks c53a4c4a55d13d3e29845fa4b490d487fe06f6d933157a7a5808453994a99655 -builtin-binding-name-rename main 724fce531fe1e0c837280b482e4b1f094bf3ca26219ba3253f27e02a46e5a288 -builtin-fn-name-rename main 68c01fb650a376d94474ebc6658ebb4cb59cef2a8a1d000b5ed5bb766d1c4729 -builtins digs deb16287536bd8b247adea4d445d4b4b91fb1f8eb535715203396a77c13510c7 -builtins-as-hof mx ad226d30b7c1e528a34a71dedc88b0369031c6a8ef7683b44a12212969b0290b -chained-nilcoalesce lookup 4e87b5ee8627c908acb88104925d192e30d6dbcd91b1622e14ff3ba0dd5edefc -chunks basic 598de8a21069b4002599f8448b5b2b899da708f0a14911acff84b2a6740d5d02 -cl-divzero safediv c9c7ee252aaf9dffde95e3fde9e3bd67aa7e64b0acae51cdc74a6f16d85d3a81 -clamp into a639d9c65c079179639bdceb5286b6c02cb87effa2f5c782bbb2000b29dbcf44 -cli-arity-strict inc c594820fe09e34c070b74fd29f327fa2402d62a39f77fb2a4d277866ce9852b6 -cli-text-arg parse-or-default 8be6421ef274cdafae8837a2227e66fc5fb17addbc6f37e47a1f89f16a7a22ed -cond-vs-ret fall 34b7e838fa8f09f28d7376e49535cb3808d016da56fca7c522c284a660ee3b88 -cranelift-error-span firstn 9d08149e4e30c658ea75c7a933a19624120884e0c89caff98385a67afae7d49f -cranelift-panic-fallback main ee3454ea03dfe9eda4a80050aaa6c4f05ffa892c1c315aa6e15cd00e04ea8b6d -ct-count-by-predicate pcount b75dffde08c086d61ed4be83eece960f46bc826481cc97e60c0ea326563b4dd0 -cumsum running c0de4b2c4297df62d2706ac0707f074a951b9a8e84c2ba9a7d94d17de4da86fe -dot-index frst 995b388042cd0e22ce5862918108b8e1ddb194b39dc13d3ed75c2c461fc7a3d3 -dot-paren-hint plus1 62884ab40538856de083446b3a90e385d03e516da82ae84563d5159f44822a43 -dot-var-index pick cc484effdbde5ef1fcd6193bee2a38ac7c2cce51ea58a9cbc7afbbacf6dda490 -double-minus-trap damped-a e758b4d5604fbcdf4905b8e7c8efa4cdc7d82a830db9c7c94cf9be549ffb0446 -early-return find-ge 6b6d912135e7c95e07b4f13484c5bca85f6401f9121cb7e4f4d1017574b50b7f -engine-flag-automain main 4eda9dd25a7475d394e980eeca286be235e3c7905478b8e39d8fc4c37613aaf1 -engine-flag-non-ident-positional main 774bd9aea5e476e44867539b7b2682b31b146364939b6a4e22817c7679ab92a6 -enumerate idx 75bee2045c5eebbba62227ecb1ee4ee1d0d93f5bdc8e9d6d9c557205ce0a3494 -fft dc-spectrum 87e2253627875f9a82150edfd5f699ae91390a9f4df52255c92737ed2834fe1c -flat basic 1e9527ffdd3a896d8a15150f6bd6e93cb0776f5141ff80bd4d83295f26334463 -flatmap expand d3c99405d13edb7074e17a0a5ef9b0fa40b4e7158c7f37f707f9f5c6e0b2cd9d -fld-reserved-rename countup 06766850e1453f16b86748f2d7c6712dee065dfb4b6669d4f1788b44845c46aa -fld-sum main baf335714cb387d9be0bd5e4efae0be6d9bbe5f009b135c0d41661328097acb1 -flt-basics main b8d370d8d99e4042b22e4f172c3aa0e2a3c9cfc40ea7ce794fa6cc5c7f85ddb8 -fmt-format-spec pct b121bc91d7776161cca44056a483b4008184d55f9c96312dbb222808af4b5d64 -fmt2 pi-2 782df1d5cc828712cbafb760fa5b215107e4ea7f4ed456cc8694f3e5c9e7bb9c -fn-body-forms suma 02d609659c3ead7b0319d4b03641816740538db97cb4212e8daa473e0ac9308b -fn-reserved-binding-rename main bacc66581d67f290fa9f16d38a8ed113cecaccb80142a83b746e1eff3bf039ec -fnref-plumbing mku ef82e40e32828a8aa2fac4b1be63d6f59f32bb3627206f17de3e11115bfefdbe -fnref-var-call viaref 50690965368df59ecc8e86e40e372b1455b47a2abc7644cf01fbfb4a9c990750 -function-as-call-arg show 8fb79eae4fb7875dfc1681f8d4e92cdc08fcf32e407736fe463c14c3d1a12a1f -grp-basics by-parity 3afaf28c05695fafecdd7da8106892e888b64143ff3275041fd2facb471e2b24 -guards cls 0108c094465ec4df9426ca2b58e0a7194a0005ca1d7c69a11e3dd8d7a177a2b6 -hof-callback-error-parity by-srt f62be0725e999e4642e4a9f50901df569bb7f2b3236257125994098074e62429 -ident-suggest-skip-strings fmtstr d76eb0cb370bbc7b7e7fa3c3726ee35c67f3677fe9402739caf3efc731ab811c -imports round-trip 22d47e2e974debfee49abadea3a0cc473c9f4ac5a301e3eab21e20f4546bc0a2 -infix add cb3e5ce9cd83bfe57d40072ddb8f0375fbe53b505c3fb2334c19f728f2fea193 -inline-lambda by-dist f6b9ca5ccf8e5fc493b04b7d962188c26c6495cd34b396a9e9256ba2e49933b3 -inline-lambda-typevar id-map a379ff223b2064e1bc8d6e74b6f2c6661edf82e3c42d7cf1247fe5481990f3a0 -inverse-trig-haversine hav 29589bd36b98c34d67b0fddba7b3f59493b22e6faffeb03f80b942841c4a16dd -jit-nil-sweep-batch6 median-list 578f84cb7e63e41b4862019be140ebbb16fa826582c3ca4f89a05193af55d804 -jpar-stream n-lines 50aed2a27acf9fbd3bd1d8055a47008c17782d1112c7bff1c2445a1fc2409837 -jpth-jsonpath-diagnostic probe 7abccbdaedfa3c9cc5cab49e897a28b8a2a6dc6927cdd61aad78b24fc7a2b84b -json dump b7d3e6238b29d3cf70ec6bbece105830ec933ef68b3ac74a44980aa440a4964e -kebab-vs-subtract sub-explicit 26772f727cc479a5f2678b98ba9c99818a916b76de4b834315a48766e985e95e -large-list-literal big-len 806afe2f2bfecb31280ec10941c45081bc03ae62f7853b77e3bdcc9eac97ade7 -large-record-literal hit-f140 58472cee48d0396cb783071a35777c152e4fe201c3aa2aef442b6b0bb4f21d6f -large-record-with upd-f140 9c525fcfff6a0f5d6f8fa9b10bbb16bcf6a144f8db6dbb26186ea5c2cff51a7b -linalg-advanced de 4f79c472309778d0a501b9d3864edf57163ef8e3979a5a6c29de49c0a2f9e8cd -linalg-basic trn d711da9214378fd5496318303a5074ac8c9988c833ce154e6bfcafacf3bbcffc -list-accumulator-tree build-range b819aaf8716db6732c89126a98377829485c8fa606cb88e9a5449a07adf17adc -list-append-pure accumulator 39997946a0251d11612314d5458d36e54c30d7ea12cf4c139f29743468cf75d5 -list-literal-refs trio c08b9e0997776c99c470044fc47badece372d1ae941a47d702b0533d794b0b67 -list-ops first 20e0291c5acb1aa514bdf0413171bc0e8eedeb6a563a5adfc44e6c8fdc721b1d -listappend-large-inplace demo-5k e4db73b3b3ff886455af1403afedc54be4fb331332597edb7412bf14139769d5 -listappend-non-rebind-alias preserve-source 8d7ff0aa153959c308dd95351dfb85cd1a61e9ddec5b8dca1df58af4a7aff7d3 -listlit-fnref-greedy protein f9a2ce5d3c046abb83646ad93ff13e057d1814aa55832542a578329be1cc8301 -lists fst f0e7953286cf02b6a36ef7c89187bcd863d6430204d37ec4d3f19d83ce15f98a -loops wh-sum 06322f0dfa6ded7a8c836f979462cdc0e3929ffaf5081aa475b0c1e0eb2cd010 -main-err-exit-code parse d9c599da8af4b6790d37ee516381c70f022aba6289a33dee0067072d0955751a -map-fnref main 36ee8b51bf53fc9603d623502f95db4279aedf264a5ab2731d96d0dfa92bb0cc -match-in-loop evens 47cde9a14bc6f5431283cf5c6d5f4f55898e10d159d167daecb2a7146f3c590b -math dist 7638b29fbfcdbc9c1c2b98401052d7ae6ae5d4aba937681863f2677c480c6592 -math-extra phase ecb83f724f852ffacf8efdb43b5d2e685734270c8b974bb736c11966f8de1066 -min-max-list lo afab4eee7f1e5a457fca0c827b516e834614c05df297656bad135588fac4ac3a -minus-prefix-call both-calls c54c14441aa31204134195f5f169fe302da2222f44ee51d48ba9095ec13ce5b4 -minus-zero-decl sub-neg 670f39123cece2af14079276d9d91784aa54cd7f45e31f3b06a8524473b6ba5f -multiline-bodies nums 08c296de26208ace6efb08da77a9f9f66b08e60a61dc51f4813d04a74b3cd0e2 -multiline-body-spans sumto db3ba16fdfe06194be8dab34483ead3a1362bb5c511424ad445f70a50ff8bf60 -multiline-fn greet bfefe62915a4cd62f8bddaf23a147c94e1cc81dcc3e6af52f2063033fc2783fd -neg-literal-papercut ab 87ccdb487e44f35ca113f4acb36d38997390e221cc87fb47f772ab49a86b692f -negative-after-op below 14d2529d76294328ad76988c8cae08a24fcb6ac59b270eb85aa13e74c6c68897 -negative-indices last-element 6025888d154388212828272502ae51deba90a730dd4c601c8e44afcccdde55ce -nested-generic-types nz 514fa70124499fcde3ce009d7d423d2daa6830ee0363b578a4215f90b1d63c46 -optional unwrap d474b4bd870eac1a1274ac655e139146a15738bd143430059beb9620bce43421 -param-short-names inc-sm 108e5d3a5ca9e34cb1731518dc7a004df3d80c63c5dfee9d1b351643ae5819cd -paren-field-access pick-col-1 12be58a42b4e54330fc2d386f25bf344fc323eb576d1ad39981083957bbf4eba -persona-diagnostic-batch-2 main eefae7a732338485f49090b934336e86485c7872dae4a8ab52d00f091c8aea8f -pipes dbl-inc feb169fc779c1b650f9d419774216186472759beb7565da4517a72c80da9d4cd -plus-literal-operand-order plus-lit-first e9f85e6bdece3a76c1c8a0fe34601f1af9805676dd54481a846faa3a490a3c3a -prefix-arg slice2 b943d6a5adb0e08c80ebe77a8089b8e90b13d2c1c063fc95b68f0ac2b46babb3 -prefix-chain-arity deeparity 6e825467c0341514e239111fd14f15648f221c090169ef55fe0539092b4b247d -prefix-minus-mixed period a899df244dfd77e3e1b7bbba50fc0e098003b67c58038699f5465af313a49be1 -prefix-mul-div mul-div-trap d5048e29f8e833f6ea3b651ea8a733ca8e0eb9c40dc68d5fd42c46293e141792 -prefix-nil-coalesce dflt 0a92d03112dbd0c74737baa642d7856a08c63dcb6dffb2121bd427e68b1ccfb4 -prefix-pair-in-parens rate 97c3b4732385f4f6f72919b69226c2346ebd2a6bcdd8e556aea35f94166816e8 -print-loop print-one 82afe2e0fb10e1f2d6a58a8cb98a70df18a9eddfa21c1b25dbe139ec33ab05c9 -range basic 04657f610647834a7cf1573caea5cded5f2f34b0dc93a95f53bbf3e10c11f3a2 -range-call-bounds sum-indices efa93ea3387dbd07db0b5ec1b508438071cc31fa4b8ab08480056c0d64d0aee6 -range-expr skip-first-two 7126037b34caedf971312716f80f01921c2fed63a2c10da2bf5836fcb80dba95 -recursion fac 3dd2d215a5179b4fe238670b9cf5ec59edf3951e9f2e827c55fc6f2a23034cec -reserved-names main 9d1a17db5afa4fa6260fcc0f4d62f6aba1d668c186add007ce592aff6f5ee91a -results div 2fcff6ebc2b301bf294a4c01f6bacbed025f6a270baefe4fa6d0821d801d0ec5 -rndn mc-mean-ok 47e04c7fce3ebc2a5449cedf36505e4f37f578618627b840b8a64a575c3ed944 -rsrt top-nums 97b02969b7b80689fe76dddf9bb4c18df3784925ae428b28982dbe304fe00b35 -rsrt-by-key worst-by-abs 3dca777daf6d61133fd25534e2cc1c328dacd188b0de94a189eebdcb669a6d16 -scientific-notation deficit 0c5557f8ba5542f11f4c201645388ac99eccdd5ccd6157b963cf8f161dd8dfea -setops shared fe86780985d0f698daba4b816f979d8ca5a870ed6097e3eab743b557529442a7 -sibling-fns main 1d33692308c789e12eb4ca4ae7236bfc559de61781edc7e1ff73d97e7b0ebb9b -sleep-builtin after-sleep e20d023f1efd47a6f03e57e9cad9bac8032af34d48da30f1fbdf76648ddc742d -sort-by-key by-dist cd3f19bccd84200099c0c5b457c603a0a03d9ee0fb213422a57c5db301d84ad2 -srt-by-key by-abs 454b0859798c3e80d0f7c3cbab074a427e0646a5f3b93f93b9bd2a7dffea57c5 -stats mid-odd 551e22c868f919b5879ad9789815713c478b13fdc0f53b4c96424938fc667504 -string-large-at upper-count 2fc8463b1e51a8e439488e08f76ab535b093d053b500f0466b2ce373ec61d5a6 -string-ops first-ch 3de12b6a117864828a784e58cf9c6d99db3515fbdc449848b40dce1c3bca79c8 -sum-avg total fe6fd853895509f3080a40c77f5c42cb4e337b15ae5d4dbda34d211d98280366 -tail-alias-comment ltail 708ffbf4c73979832a57307062f38b544bf7e322c809e511ac72b4b23a57933b -take-drop first-two 621d2b05e7465223756ec7b0ec1907977afbfe12cd8d2f723fc165325da32e45 -timing positive cca0247c6013a2ada98674b5b434016627d24737ab5e5be38b427ad4c6d2939e -trm trm-demo 8cb045e1af31db28c7f249761a50d36291b2245ab7b6604efdd00705c4aab077 -uniqby by-parity ce5d3127a22d82bf16cddc717bc85a3ea5b8390652147e571583e4e6caadffe2 -unknown-flag-equals-form main 139192803b8bde5a14f601af37c4c77df9542a7527ccd9988b277dd3da0da725 -unknown-flag-guard main e142ef33e19a907a5fba2c5675da9544c7acf06d56fd6c042c134c3d7ca3c72a -unknown-subcommand-listing main 3b2e75f49085e207e3f9f8c0d251b46101849c43eae6c8dae43f6753ca10838b -unq-numbers basic 9eddefe039894d16ea671a15fb543447a14a77d63780eed2fd2b065517b15925 -vm-default-engine windows-len 1a55185f5b3aa22019c5152c0753928c49089de4717e31deda0c83065c82364c -wh-gt-condition dec f7184bcfffa71a1b7f0f3e9f75b6986eab692110011cfa1955977aee90cb077a -wh-prefix-call drain-tail a99f29c03607dc5a3ac3d69dfe2be770a0139683a300d126a54dc3f01eb5868b -window basic 033c8fba9ba1651b5339bd9b1c5ef5185ca4421bd0516de88b674c0c0d0987ee -window-cranelift-jit basic 4e6bad2a6dc8a8042e2a58c1aafe01f7f1222258b06665e7d3c19fc8b77edfc4 -wr-json dump cbbdffaa6d6c2aba1af6958dfb72bc18bbb283239adf76db2a8ae1720a4bc716 -zero-arg-call take-list 9da524afeb021aecbb6561e5f006bde64c2384437e4801827a4a2d6dbbfa69db -zip pairs aeef4a9d56cda7e1a85592390be9372167d861c045ded47fa356bca8a06dc9d9 +arithmetic add 3166d59cc5745f4d663aa8590bcf48cbb38810569b27985575c5d552fca41fa3 +at-float-index frac 2a8faf2750a41ffb7627cce6edc38b3fd0103a22fe57078843d1940ce34c8868 +at-hd-tl-oob-parity firstn 290570699148a9e720d3525e62a3bad805b73435167eb7ec6e76d8bc11642a4f +at-indexing nth 795e6950b20bc062ea021d11d93eb6ef1a31986064b540ff1e2f7034457c2875 +autorun-main main f2b30835b14e48d4f342a7745d3bcc4d22f1760995b23a84b160416b5205b103 +backslash-lambda-hint inc-all f4d2e8db72e3dc3348389c427a643dcc689b2fa89877cba82618a7d769cdd38b +bare-bang-rejected inspect-result e23dac4ce2a9d1d1063dd5c141a58ee1ac93bc046f5d04bca653d81d8e66ebca +blank-line-in-fn-body sum-with-blanks 08ffc31f50878f6f748e6f22939e88f555d1f74bf89f0626b7ebc4cc7c45b323 +builtin-binding-name-rename main c68dbfb5d6ca7cec908e7cf5bac289d3ad990c4caffe5fc82598caa91a782b9c +builtin-fn-name-rename main 7aba38398ca1ad030b4cb94470689887d21cccb3684faad7de139f3504253548 +builtins digs b97f9bc0a8db819a33d31f9bd7c018e15a3282d64b945a0b95580f9bd0671ace +builtins-as-hof mx 3320089d76f0b7e2ef1ca2c01ed42015681e577e9e466ab1195428920ac7162c +chained-nilcoalesce lookup e018586e82092185930a63a768af059d0d9884f77a6341eceaa51641d1c4be82 +chunks basic 3b64080ef0e2e5b88078056ef0e4b59cc8b4f1c2f027921b66783945853b1f31 +cl-divzero safediv 21a51ea36cc747f43f13d95aeaf80c394def7049cad6bd242fb71b87592b7c12 +clamp into 6b86c24e3bf30301e83816c0f738fd2117499c971f92cf7f39d386458b3a74ac +cli-arity-strict inc bdf40cc910ea3a7ca1f09073b742832f1ce35266495299336a112373843988ad +cli-text-arg parse-or-default 94e45052ac6b0228849d76365563f20dcb84b99b1c3b39975db6650b24d52751 +cond-vs-ret fall 0789583268d242e12b407ad872aa9b0ec2a4d0122835eff7f0cd41ee034a12c7 +cranelift-error-span firstn 6bf158168c49b0f0adc0f67ec85bad293da6df1c8beb1913c3b313a3568e733d +cranelift-panic-fallback main 96a6f8e107a49f77b46b49cf2a7bc55f211a53553c2aab0cf21e3c8b640981ce +ct-count-by-predicate pcount 9e1bce4e451c3170442a9544620eb7026741dc7c250028ab459922394c9848a8 +cumsum running 0d8d48b1a9147a1241fc55eb885137cf2bd04de1813daf798f98db6891b17af3 +dot-index frst 62989c7a9bf8b255a48658849bbde5a78719fa5c3a18ea76bfb2e0e3d0228850 +dot-paren-hint plus1 32f52ec630c6d5f5a8b275d7c002d4a3ad371340888724e87786a866ac77f7fa +dot-var-index pick c4805f601f8ef234d019aa7b0734f29e4f84776354c8800956abd8589006f386 +double-minus-trap damped-a 7476676cfab3d6b9ac94c1d0da61d29a1f48c8c7176efa64d7abbbd26c1a58d4 +early-return find-ge bab868433d4ab72258b5026d3227ffe47689c650af6cb38e9f094a7832c4a74a +engine-flag-automain main 8608f6093dc54c00876a42f97fcdda93ce79fd0831419369d800d6fb0bc403f0 +engine-flag-non-ident-positional main 295df95333a0c40c92e7fe2cb13fc2153a0663a9927d9d22d4b06357dedbce1b +enumerate idx 9bc9430960fd98339d912e74ea5cf72dd62f47e0a070fd5fb669d27b65c51fb4 +fft dc-spectrum e921ab24ccb6fe349b7a37872f024162207618d05b6c6d6e4fda9aa7b75805ef +flat basic 9f207dba9426a4f6e3eff69da8e26cad2a6fe2ad7ea0ebaa1e28c65c98eb85b7 +flatmap expand 380b9445a310467ab69621cd6c40cc267380d9dd3aa51f870124f7ab43500f58 +fld-reserved-rename countup aebabbd4d16acb376b6cc4ceb8f5133e257f4f9017a186ea27a25e3a9d6a7153 +fld-sum main 6ba56d2ecdcff5cfb2d2fb9f4ac67e441b762ee9b9f0b759ca31f3be644685e8 +flt-basics main bfab93f7654410e8ab1d77f65b84003f9c8caa828e7fa679671bd28010662f58 +fmt-format-spec pct bf53534d693a9b8e35c3307a8d5f7065bb74e2e75190d23f1fa53b0d902ee123 +fmt2 pi-2 a763f44bcc08350ed34b75aa755f4e0478936cd3611a12f0aa34be52518720e1 +fn-body-forms suma 1258bed8e9bd2d1173b3c1a007d241d4c7713109c36234231a34890dd7f31ee1 +fn-reserved-binding-rename main 8f976f6bba504757a385162e6c9147ca9ca9256996ae9afcad870fb091a6442e +fnref-plumbing mku 2a28fa72c615d6efcefa495582403b736d948707825e5b463e50ef74f4120f99 +fnref-var-call viaref 33020bc189ef8d4ce79a23e623943d5d80b8e88c3a8cfa12ebb9ac6c993a8386 +function-as-call-arg show 8052e8b1c73c1fc50acabed30f59185d695642260c42c29034b263d0284520d8 +grp-basics by-parity 74ff9ca00cffdfe0c1feedc281caf7671a9045f244667f6a260a9538cc7f7691 +guards cls 86379e648ad8fa315f5f0aadecd0325dc901c70398de244e113cb65389f8e063 +hof-callback-error-parity by-srt a17ea51c9c870f93bfaea727a7d987ac39a624802a4fc9ed8facd3febc22ec9e +ident-suggest-skip-strings fmtstr 0ea71f657020ff67eec1d171118da72c7070fa77613168bebd9e1f148ad4fd12 +imports round-trip 4703bdb6c9f3128d5631fe2ee04ee1ed303a0bddbb03653013c7620aba6bef5b +infix add ff64e7d8f44fcceb5c6ebdcbca9fa8b97158496394033d68b4808e0dbacc115f +inline-lambda by-dist 5a9b5ebcab4def07b396d08efbd65f154d1ed1f5c4579e0b418435eb04a3bb2f +inline-lambda-typevar id-map bb5bae4aa057e12c57bd8c3a77b70a19babde51fb13676b5d689e5d6080b66c0 +inverse-trig-haversine hav 4e77e980d3929038306cd6fba54137ab6019c8498194d3d12ab27d523325a18b +jit-nil-sweep-batch6 median-list af8bf2c72a9c9fa377e177a8fd7062c620944349e07b38a90874ff549dcadc77 +jpar-stream n-lines c6c8f37ea53d71b9512afb68b3a56ec9332e11e297956b1f69d97e56d18bf429 +jpth-jsonpath-diagnostic probe bd70e989cce3768af1329700a3ac3f817cca38e37fc9fbfbe75d475acf3960b3 +json dump c0e6241691276de806d810657429d6b895d139f627c37e2603f1026e75bc7389 +kebab-vs-subtract sub-explicit 404de19a7c903372633bbf55a36bb19e5d121ff9fc10fa9686bc7e0725faf22b +large-list-literal big-len 21ab0def4b190efe1b3977f92108a556aa9f5af948bf008e3c567dc3737f254e +large-record-literal hit-f140 d4ecb64a89c633db8f605b527994452f4be48b7c83c14e8cc0f3929f00f7bffc +large-record-with upd-f140 5af8297318ce58e75c2096d521f555917a58a90f60402a8e6c7b6a23d4d06474 +linalg-advanced de 68aa96f9737568aa7a8855f200bb3b9391ab763a2ad4530d433da86aed658614 +linalg-basic trn f0e498c77621fb794e34f97db5c84bd7a0527dcf6bf45073ad5eb2dfab914456 +list-accumulator-tree build-range 09b015eaac106154ba7431c327f0e23562b5c2493ba84eceeb5dd8c4ad3e63d6 +list-append-pure accumulator dbb0e383cdd48466f124b0ff3fb96b60171043af1649fa22715133eb61ecb3ee +list-literal-refs trio 2edccc173b2a78f84174c56639fdc526d4b2c134ae2fab842905b92680bd7de7 +list-ops first 2c080d786347d4905301c5ed13f64ccff2d998736e24fbd28edaa17230cac176 +listappend-large-inplace demo-5k c4f0e0ebe9a76cdcb82be172924eb360a6a17f9d16da59531dc114be018fd478 +listappend-non-rebind-alias preserve-source d5b4ecaaaf20912c976865d5054345601a9fadd9b63b2771c4ff0b4885ebf188 +listlit-fnref-greedy protein c34109245e381b21063cb7ad557179fa06af7ccde9cb6f16779a5b48928fc6f5 +lists fst 5c89b774b208f820ea816173248f62a34d1a8043567443ea7aad84fa8201e82c +loops wh-sum cb415a2b24041a89940416ac8ecdf6c38e8d665d4c4833ae67c7fb39f62e307f +main-err-exit-code parse 472b59f354c5b21ebe68dd8b40833a9213a9c765777bd0ee6f03005c315d1445 +map-fnref main d9efd333c00750465560f9e43a9d93ed2992d0c5980b51200eec368854794d0a +match-in-loop evens 84f68d1a35a3cb3d6544456718752906735156cf86431da5f611517d801562ce +math dist 2f83eb5af02607d694f2f4f2ca3b112aa847e2e7d04945c1c73983fe43876aea +math-extra phase c9279fbcda16f2419449d26fbdf6c1fcbf4f2949a17fc126349757758233453a +min-max-list lo 3196841059c3490843bc68cf5feb0f9328aa25467fd264077008398689b27630 +minus-prefix-call both-calls f7a0fd290834f17fbfadc38cfe238bf22880302fca0edb7d3ee8e25ae57daa9b +minus-zero-decl sub-neg 661f9f41ac77e8e2e0cb0a4e4dfda83d6261b596c8f82224d4b34f6cfc98736a +multiline-bodies nums 98e1e0533ba4e53236e77e9ef6f977848a8f9f75ce8584bd9f24998fe3bed0de +multiline-body-spans sumto 012a02966701bce5fa6f0c0976ce0dafd8ce80fa5e6927e10f21fb40abd75ff6 +multiline-fn greet 3d10f59808f643cbfcf4751c57ae1c604222c81b5df09cc38bde2ca0e9272978 +neg-literal-papercut ab 47701a0ee5027e7a0ecd2d5493883fc00411b4a462cc2ef2a3f8c5585040fdc7 +negative-after-op below 5c8aafed5d9d544a37f6defc7038c7cf5e78339470a1a6f1872a928516b8f671 +negative-indices last-element 349ea35acdadf5a29541a827e72d0fc24b30e83d0a8fdf5377ab846e62bfde03 +nested-generic-types nz b10f13c428f13c6921cdd528024da305241b3c33f1236daf798308ee74a95062 +optional unwrap 96dd655319ef3d7beab3689571fdcc1b582fccf2ff791e050eefc9b9503210a4 +param-short-names inc-sm 9023c474b5cc42695257864abf3931584ddf0bdb3866a230668ecbc0ee47ae1e +paren-field-access pick-col-1 1ee0124df295934b14c6dd4ff29d0bfd0058bf11773c8b039e75ada5f8be67b2 +persona-diagnostic-batch-2 main 0ede200f00817d9002fb8d7a23669c2be3395eae4983affbf71497e56b932a0a +pipes dbl-inc ba2b040b9d39ace7d8596e7306227faa4bfe39468990d2546e5c5cb944114ed3 +plus-literal-operand-order plus-lit-first 0e503412f2adec93219f054659ca2947046007748ef862df3655a78dd792710d +prefix-arg slice2 6616f35f1a8072e7662673f5f5f191b2c138ba189a16ed9f2e0bc06ccb7dfdda +prefix-chain-arity deeparity c94c9e0744ca13b2c4a688e19f6d6c5acd76788395b520a1ac807a1653de4c2b +prefix-minus-mixed period d245fc7b8c4175094ec5a338beb7923d8945b38de7abd7bfacda87ea8c3c120b +prefix-mul-div mul-div-trap 697a63d2c562caba41668424c4624769407dd9cacfe14dc29dcad121b4d060fe +prefix-nil-coalesce dflt 6e8b0515537f5e8bd293d0a3a216b35abfeef4d65100622629ed3ff51b161bd1 +prefix-pair-in-parens rate 48850dd31a78d97f456c1d23d7e3a6717f409ba864a476b26596eda24fce04f3 +print-loop print-one 930b3d11324b387432b78b39a8f3a3eaec169bd32e6bcb0e0657ea9ca7630474 +range basic b83e25846cb7f0e0c8f59b1bcf74b4cec40fc6b6dc471b7ee5284364bdff9af8 +range-call-bounds sum-indices 87f69936d80f2be2348b7039ac114ffa5ed9683444ad47682e4e766567758baf +range-expr skip-first-two 3a86affc409e1ac0ebd23fa509c886f955e91e8a1d14c14c069378d87e9145b5 +recursion fac acd44ff7515ba2798c709aef917b7ee22a111c87a0dbeea40b4e99a25b94ff30 +reserved-names main 41eb0db3641aa93bf2b69ecfd87f1a48db49b8fa78671500fe664731427a3f9d +results div be8e404a44233641bbb6d1ddbbbbee0f0773ee123dcfd1a159a94afb273a8f67 +rndn mc-mean-ok c11f0218666b3c2034cd6afd556e3488e62df2db69899b7058ce09a0fc0e9708 +rsrt top-nums db090a566c74cb2c798b4b054f7f4255dd01e0a000dab5b4e6a665fdd526c03b +rsrt-by-key worst-by-abs 09ef7d5574caab797729e279a7fe5431c5dfa185843fa644a7b6731ea0e58735 +scientific-notation deficit 2b2c089eefd6d8e95596e8f9c2281adba343d59cf5770fde60be2c0bfb3395a7 +setops shared 131c27522731c37b90ec415a9a09a363f221f2b133863100d2f617d21d654a29 +sibling-fns main 866f090c21f4e02df93ed6376050e4ea2c0f652a50ab8fb98ce017eb8f074b63 +sleep-builtin after-sleep c0c1ef607e89764bf838f61dbe40ac41916e0d2d6170ce50d2a0f1825e8b8159 +sort-by-key by-dist f296f59ecf95472151f27daa13343164eee2b40759c510430a9ff882f730b413 +srt-by-key by-abs b8d504ddca36cba451c9249961b02df75906f38c528b87d90e2f6adb163f8f1c +stats mid-odd 75d0c273d4dd392c819b1b7acf209cd90083acff401d8eb24b4865dd391a1252 +string-large-at upper-count 3b2dbb17d9882c6141a40eaf48bfbd828648260f57284c2197bc4b2d129c9586 +string-ops first-ch ad87769e65a8bf7c0759fcca87c7422c87d871c8d065d9757443a78bc597d6fa +sum-avg total 5bf2a51ea219bb49a70c5cf925de0b32f19f65cd3cd6cff15a29f3f8eab38aa2 +tail-alias-comment ltail 96889bbd5de3b7f82b8e5fedf824a4829b46e7ce92b82b62f5cf361246b7c4d5 +take-drop first-two fea23bb25e399ccfabd090cdcfec9734dad391e4577a26a6b240c5b73d163be2 +timing positive b1121c342d628dcc5b80a1b5570a62139fe307383275a4aab325e7d1f1648ce3 +trm trm-demo c995fcd8a97efec246c9de0430fd265eb1b01eba849741fc7b06598506e8f557 +uniqby by-parity 769afdec46bc141c3c7884312f57e48fb74df05cd44ae727d1e7e293c9664561 +unknown-flag-equals-form main 832a317cdef5cd5e4f90b4f5258cc560d166214247d879cfa7d09071cd97537c +unknown-flag-guard main 9bc0559450e664958407279c7e93a08f39901aa1df990c3cf7274f731c6624b8 +unknown-subcommand-listing main 2f109ea6459b169a1a7bcd95f59716e97cd821be3961c460214125b502e4ee6d +unq-numbers basic 9e5ccc89c949362d0fe72b5d1aa613bb890529d7e68d1990d90a6b7f17b1c840 +vm-default-engine windows-len 044f02a6fada915f4a26ea7d300be5af8e285cbfb6e761523ac3946b87573cd1 +wh-gt-condition dec 5e628979ccc4389817d4798f1c7ae6c0e99f4aedc366fc6e2dab4acb461c4a26 +wh-prefix-call drain-tail 66e4bd34751df5c889fe49890c351ffc9fd1c1c3a8d843ebc2392b10dc4f91d5 +window basic 5d751c5223163a5f74c5bf830c68f6f2b5068e31895d9f02f9d623237703006a +window-cranelift-jit basic fcb227edbd885645752d471de22cf3e94be3857bc91e824779291ff33f486aff +wr-json dump 5e3e3b8a62fd5a292fdbf60fdf31a147f3b1503e3533b680b30fa386b6225a7b +zero-arg-call take-list 17e3170d65543477c95fa3672315b08c33bfef0d161f2148ec7912929529be07 +zip pairs ba4a4663a2549bf50819dc4bc0cda1f10767ebdc9dc47556f2df6901f96001b3 diff --git a/tests/aot_byte_identical.rs b/tests/aot_byte_identical.rs index 3a5ec170..22dd5a41 100644 --- a/tests/aot_byte_identical.rs +++ b/tests/aot_byte_identical.rs @@ -116,7 +116,16 @@ fn cranelift_aot_object_file_byte_identical_to_baselines() { let mut ok = 0; for entry in &entries { - let example = format!("examples/{}.ilo", entry.name); + // Examples migrated to the canonical `.@` extension; fall back to + // `.ilo` for any stragglers so the corpus survives the rename. + let example = { + let at = format!("examples/{}.@", entry.name); + if Path::new(&at).exists() { + at + } else { + format!("examples/{}.ilo", entry.name) + } + }; if !Path::new(&example).exists() { missing.push(entry.name.clone()); continue; From 10d9755d2b434d4a75bc52746d70b0f30c46f029 Mon Sep 17 00:00:00 2001 From: Daniel Morris Date: Sun, 9 Aug 2026 01:14:32 +0100 Subject: [PATCH 06/11] python-emit: recapture 3 drifted baselines post-backend-rework bangbang-panic-unwrap, chunks, bang-propagation-result drifted with the emitter rework (587->799 etc). Regenerated per the test's own instructions. --- tests/python-baselines/bang-propagation-result.ilo.py | 4 ++-- tests/python-baselines/bangbang-panic-unwrap.ilo.py | 4 ++-- tests/python-baselines/chunks.ilo.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/python-baselines/bang-propagation-result.ilo.py b/tests/python-baselines/bang-propagation-result.ilo.py index 64aaf017..ed79f884 100644 --- a/tests/python-baselines/bang-propagation-result.ilo.py +++ b/tests/python-baselines/bang-propagation-result.ilo.py @@ -4,11 +4,11 @@ def _ilo_unwrap(r): raise RuntimeError(r[1]) def parse_ok() -> tuple[str, float | str]: - v = _ilo_unwrap((lambda s: ("ok", float(s)) if s.replace('.','',1).replace('-','',1).isdigit() else ("err", s))("42")) + v = _ilo_unwrap(((lambda v: ("ok", float(v)) if isinstance(v, (int, float)) and not isinstance(v, bool) else (lambda s: ("ok", float(s)) if s.strip().replace('.','',1).replace('-','',1).isdigit() else ("err", s))(v))("42"))) return ("ok", v) def parse_err() -> tuple[str, float | str]: - v = _ilo_unwrap((lambda s: ("ok", float(s)) if s.replace('.','',1).replace('-','',1).isdigit() else ("err", s))("abc")) + v = _ilo_unwrap(((lambda v: ("ok", float(v)) if isinstance(v, (int, float)) and not isinstance(v, bool) else (lambda s: ("ok", float(s)) if s.strip().replace('.','',1).replace('-','',1).isdigit() else ("err", s))(v))("abc"))) return ("ok", v) def fmt_err() -> tuple[str, str | str]: diff --git a/tests/python-baselines/bangbang-panic-unwrap.ilo.py b/tests/python-baselines/bangbang-panic-unwrap.ilo.py index c4d0465c..b8e951b7 100644 --- a/tests/python-baselines/bangbang-panic-unwrap.ilo.py +++ b/tests/python-baselines/bangbang-panic-unwrap.ilo.py @@ -4,10 +4,10 @@ def _ilo_unwrap(r): raise RuntimeError(r[1]) def parse_ok() -> float: - return _ilo_unwrap((lambda s: ("ok", float(s)) if s.replace('.','',1).replace('-','',1).isdigit() else ("err", s))("42")) + return _ilo_unwrap(((lambda v: ("ok", float(v)) if isinstance(v, (int, float)) and not isinstance(v, bool) else (lambda s: ("ok", float(s)) if s.strip().replace('.','',1).replace('-','',1).isdigit() else ("err", s))(v))("42"))) def parse_err() -> float: - return _ilo_unwrap((lambda s: ("ok", float(s)) if s.replace('.','',1).replace('-','',1).isdigit() else ("err", s))("abc")) + return _ilo_unwrap(((lambda v: ("ok", float(v)) if isinstance(v, (int, float)) and not isinstance(v, bool) else (lambda s: ("ok", float(s)) if s.strip().replace('.','',1).replace('-','',1).isdigit() else ("err", s))(v))("abc"))) def mget_hit() -> float: m = mset(mmap(), "k", 7) diff --git a/tests/python-baselines/chunks.ilo.py b/tests/python-baselines/chunks.ilo.py index 7486377d..16db67f7 100644 --- a/tests/python-baselines/chunks.ilo.py +++ b/tests/python-baselines/chunks.ilo.py @@ -7,7 +7,7 @@ def exact() -> list[list[float]]: def big() -> list[list[float]]: return chunks(10, [1, 2, 3]) -def ones() -> list[list[float]]: +def singles() -> list[list[float]]: return chunks(1, [1, 2, 3]) def empty() -> list[list[float]]: From 03e075527af9d1a46fd93a92fd5ab3ed9182a329 Mon Sep 17 00:00:00 2001 From: Daniel Morris Date: Sun, 9 Aug 2026 01:14:48 +0100 Subject: [PATCH 07/11] tests: update contracts that predated 26.8 features - help_ai_* pinned the retired SPEC-compactor format (FUNCTIONS:/no blank lines); they only passed because the build clobber kept resurrecting the monolith. Now pin the ILO-538 index contract (QUICK START, MODULES, ilo skill get, RESERVED NAMES) plus a 12KB regrowth cap. - json P011 fix_plan test pinned the pre-ILO-501 single-edit shape; renames are occurrence-complete now, so var=5;var carries two edits. - cross-engine parity tests had shadowing duplicate src/path pairs from the .@ rename whose second version used a bare tail call - the documented TCO shape that drops the caller note. Keep the non-tail originals. - fs-builtin fixtures lacked /fs effect sigils, so W051 warnings polluted stderr ahead of the asserted ^err prefix. --- tests/eval_inline.rs | 39 ++++++++++++------- tests/json_output_contracts.rs | 13 +++++-- tests/regression_cross_engine_error_parity.rs | 10 +++-- tests/regression_fs_builtins.rs | 28 ++++++------- 4 files changed, 55 insertions(+), 35 deletions(-) diff --git a/tests/eval_inline.rs b/tests/eval_inline.rs index 2893da24..188a32a3 100644 --- a/tests/eval_inline.rs +++ b/tests/eval_inline.rs @@ -895,18 +895,22 @@ fn help_ai_and_ai_flag_produce_same_output() { } #[test] -fn help_ai_contains_no_blank_lines() { +fn help_ai_is_bootstrap_index_sized() { + // ILO-538: `help ai` serves a hand-maintained bootstrap index (language + // kernel + `ilo skill get` module table), not the SPEC-compacted + // monolith. The old no-blank-lines invariant belonged to the compactor; + // the invariant that matters now is the token budget. Cap generously in + // bytes (~3K tokens) so the index cannot silently regrow toward the + // ~180KB monolith this replaced. let out = ilo() .args(["help", "ai"]) .output() .expect("failed to run ilo"); - let stdout = String::from_utf8_lossy(&out.stdout); - for line in stdout.lines() { - assert!( - !line.trim().is_empty(), - "unexpected blank line in compact spec" - ); - } + let n = out.stdout.len(); + assert!( + n < 12_000, + "bootstrap index has regrown to {n} bytes — it replaced a 180KB monolith for a reason (ILO-538)" + ); } #[test] @@ -947,11 +951,20 @@ fn help_ai_preserves_key_content() { .output() .expect("failed to run ilo"); let stdout = String::from_utf8_lossy(&out.stdout); - // Core syntax constructs must be present - assert!(stdout.contains("fac n:n>n"), "missing factorial pattern"); - assert!(stdout.contains("FUNCTIONS:"), "missing FUNCTIONS section"); - assert!(stdout.contains("TYPES:"), "missing TYPES section"); - assert!(stdout.contains("OPERATORS:"), "missing OPERATORS section"); + // ILO-538 bootstrap-index contract: the kernel one-pager plus the + // module-loading instructions must be present. Content details live in + // the modular skills, so we pin the index's load-bearing parts, not + // section names from the retired SPEC compactor. + assert!(stdout.contains("QUICK START"), "missing kernel quick start"); + assert!(stdout.contains("MODULES"), "missing module table"); + assert!( + stdout.contains("ilo skill get"), + "missing module load instructions" + ); + assert!( + stdout.contains("RESERVED NAMES"), + "missing reserved-names warning" + ); } #[test] diff --git a/tests/json_output_contracts.rs b/tests/json_output_contracts.rs index 3caa8ea8..2f14b463 100644 --- a/tests/json_output_contracts.rs +++ b/tests/json_output_contracts.rs @@ -377,10 +377,15 @@ fn check_json_p011_fix_plan_reserved_rename() { let plan = &p011["fix_plan"]; assert!(!plan.is_null(), "ILO-P011 should carry a fix_plan"); let edits = plan["edits"].as_array().expect("fix_plan.edits array"); - assert_eq!(edits.len(), 1); - assert_eq!(edits[0]["before"], "var", "before is the reserved keyword"); - assert_eq!(edits[0]["after"], "var2", "after is the renamed identifier"); - assert!(edits[0]["line_range"].is_array()); + // ILO-501: renames are occurrence-complete — `var=5;var` has two + // occurrences, so the plan carries two edits (the old single-edit + // contract left the program broken after applying the fix). + assert_eq!(edits.len(), 2, "one edit per occurrence"); + for e in edits { + assert_eq!(e["before"], "var", "before is the reserved keyword"); + assert_eq!(e["after"], "var2", "after is the renamed identifier"); + assert!(e["line_range"].is_array()); + } } /// ILO-T041 nil-coalesce on Result: fix_plan rewrites to `?val{~v:v;^_:default}`. diff --git a/tests/regression_cross_engine_error_parity.rs b/tests/regression_cross_engine_error_parity.rs index 47731060..b2e685c4 100644 --- a/tests/regression_cross_engine_error_parity.rs +++ b/tests/regression_cross_engine_error_parity.rs @@ -136,9 +136,11 @@ fn lst_oob_has_rich_message_and_r009_on_every_engine() { // `vm_error_call_stack_drops_tail_caller` in src/vm/mod.rs tests. #[test] fn call_stack_notes_match_across_engines_two_levels() { + // NOTE: the `.@` rename once duplicated this pair with a shadowing + // second version whose call to `g` was a bare tail call — which is + // exactly the documented TCO shape that drops "called from 'main'". + // Keep the single non-tail form. let src = "g xs:L n>n;at xs 99\nmain>n;xs=[1,2,3];r=g xs;+ r 0\n"; - let path = write_src(src, "callstack_two_levels.ilo"); - let src = "g xs:L n>n;at xs 99\nmain>n;xs=[1,2,3];g xs\n"; let path = write_src(src, "callstack_two_levels.@"); for (engine, stderr) in run_on_all_engines(&path, "main") { assert!( @@ -162,9 +164,9 @@ fn call_stack_notes_match_across_engines_two_levels() { // already non-tail (the original `a=g xs;+ a 1` shape). #[test] fn call_stack_notes_match_across_engines_three_levels() { + // Same shadowing-duplicate history as two_levels above — keep the + // single non-tail form so main's frame survives TCO. let src = "g xs:L n>n;at xs 99\nh xs:L n>n;a=g xs;+ a 1\nmain>n;xs=[1,2,3];r=h xs;+ r 0\n"; - let path = write_src(src, "callstack_three_levels.ilo"); - let src = "g xs:L n>n;at xs 99\nh xs:L n>n;a=g xs;+ a 1\nmain>n;xs=[1,2,3];h xs\n"; let path = write_src(src, "callstack_three_levels.@"); for (engine, stderr) in run_on_all_engines(&path, "main") { for expected in [ diff --git a/tests/regression_fs_builtins.rs b/tests/regression_fs_builtins.rs index e5882b78..01538fdc 100644 --- a/tests/regression_fs_builtins.rs +++ b/tests/regression_fs_builtins.rs @@ -82,7 +82,7 @@ fn run_err(engine: &str, src: &str, args: &[&str]) -> String { fn ls_basic_cross_engine() { let fix = make_fixture(); let root = fix.path().to_str().unwrap(); - let src = "f d:t>R (L t) t;lsd d"; + let src = "f d:t>R (L t) t /fs;lsd d"; for engine in ENGINES_ALL { let out = run_ok(engine, src, &["f", root]); assert_eq!(out, "[a.txt, b.txt, sub]", "{engine}: ls basic"); @@ -94,7 +94,7 @@ fn ls_basic_cross_engine() { fn ls_empty_dir_cross_engine() { let dir = tempdir().unwrap(); let root = dir.path().to_str().unwrap(); - let src = "f d:t>R (L t) t;lsd d"; + let src = "f d:t>R (L t) t /fs;lsd d"; for engine in ENGINES_ALL { let out = run_ok(engine, src, &["f", root]); assert_eq!(out, "[]", "{engine}: ls empty"); @@ -106,7 +106,7 @@ fn ls_empty_dir_cross_engine() { /// the exit code is non-zero — same shape as `rd` on a missing file. #[test] fn ls_missing_dir_cross_engine() { - let src = "f d:t>R (L t) t;lsd d"; + let src = "f d:t>R (L t) t /fs;lsd d"; for engine in ENGINES_ALL { let out = run_err( engine, @@ -127,7 +127,7 @@ fn ls_missing_dir_cross_engine() { fn walk_recursive_cross_engine() { let fix = make_fixture(); let root = fix.path().to_str().unwrap(); - let src = "f d:t>R (L t) t;walk d"; + let src = "f d:t>R (L t) t /fs;walk d"; for engine in ENGINES_ALL { let out = run_ok(engine, src, &["f", root]); // Sort order is lexicographic on the relative paths. @@ -141,7 +141,7 @@ fn walk_recursive_cross_engine() { /// `walk` on a non-existent root surfaces as Err. #[test] fn walk_missing_dir_cross_engine() { - let src = "f d:t>R (L t) t;walk d"; + let src = "f d:t>R (L t) t /fs;walk d"; for engine in ENGINES_ALL { let out = run_err( engine, @@ -161,7 +161,7 @@ fn walk_missing_dir_cross_engine() { fn glob_star_single_segment_cross_engine() { let fix = make_fixture(); let root = fix.path().to_str().unwrap(); - let src = "f d:t p:t>R (L t) t;glob d p"; + let src = "f d:t p:t>R (L t) t /fs;glob d p"; for engine in ENGINES_ALL { let out = run_ok(engine, src, &["f", root, "*.txt"]); assert_eq!(out, "[a.txt, b.txt]", "{engine}: glob *.txt"); @@ -175,7 +175,7 @@ fn glob_star_single_segment_cross_engine() { fn glob_double_star_recursive_cross_engine() { let fix = make_fixture(); let root = fix.path().to_str().unwrap(); - let src = "f d:t p:t>R (L t) t;glob d p"; + let src = "f d:t p:t>R (L t) t /fs;glob d p"; for engine in ENGINES_ALL { let out = run_ok(engine, src, &["f", root, "**/*.txt"]); // Sorted lexicographically; nested e.txt appears, d.log does not. @@ -191,7 +191,7 @@ fn glob_double_star_recursive_cross_engine() { fn glob_char_class_cross_engine() { let fix = make_fixture(); let root = fix.path().to_str().unwrap(); - let src = "f d:t p:t>R (L t) t;glob d p"; + let src = "f d:t p:t>R (L t) t /fs;glob d p"; for engine in ENGINES_ALL { let out = run_ok(engine, src, &["f", root, "[ab].txt"]); assert_eq!(out, "[a.txt, b.txt]", "{engine}: glob [ab].txt"); @@ -201,7 +201,7 @@ fn glob_char_class_cross_engine() { /// `glob` on a non-existent root surfaces as Err — same shape as `walk`. #[test] fn glob_missing_dir_cross_engine() { - let src = "f d:t p:t>R (L t) t;glob d p"; + let src = "f d:t p:t>R (L t) t /fs;glob d p"; for engine in ENGINES_ALL { let out = run_err( engine, @@ -221,7 +221,7 @@ fn glob_missing_dir_cross_engine() { fn glob_no_matches_returns_empty_cross_engine() { let fix = make_fixture(); let root = fix.path().to_str().unwrap(); - let src = "f d:t p:t>R (L t) t;glob d p"; + let src = "f d:t p:t>R (L t) t /fs;glob d p"; for engine in ENGINES_ALL { let out = run_ok(engine, src, &["f", root, "no-such-pattern-*.xyzzy"]); assert_eq!(out, "[]", "{engine}: glob empty match"); @@ -235,7 +235,7 @@ fn glob_no_matches_returns_empty_cross_engine() { fn walk_single_file_cross_engine() { let dir = tempdir().unwrap(); fs::write(dir.path().join("only.txt"), "x").unwrap(); - let src = "f d:t>R (L t) t;walk d"; + let src = "f d:t>R (L t) t /fs;walk d"; for engine in ENGINES_ALL { let out = run_ok(engine, src, &["f", dir.path().to_str().unwrap()]); assert_eq!(out, "[only.txt]", "{engine}: walk single file"); @@ -296,7 +296,7 @@ fn restore_perm_fixture(fix: &tempfile::TempDir) { fn walk_skips_permission_denied_subdir_cross_engine() { let fix = make_perm_fixture(); let root = fix.path().to_str().unwrap(); - let src = "f d:t>R (L t) t;walk d"; + let src = "f d:t>R (L t) t /fs;walk d"; for engine in ENGINES_ALL { let out = run_ok(engine, src, &["f", root]); assert_eq!( @@ -315,7 +315,7 @@ fn walk_skips_permission_denied_subdir_cross_engine() { fn glob_skips_permission_denied_subdir_cross_engine() { let fix = make_perm_fixture(); let root = fix.path().to_str().unwrap(); - let src = "f d:t p:t>R (L t) t;glob d p"; + let src = "f d:t p:t>R (L t) t /fs;glob d p"; for engine in ENGINES_ALL { let out = run_ok(engine, src, &["f", root, "**/*.txt"]); assert_eq!( @@ -342,7 +342,7 @@ fn walk_unreadable_root_is_err_cross_engine() { perm.set_mode(0o000); fs::set_permissions(&locked, perm).unwrap(); - let src = "f d:t>R (L t) t;walk d"; + let src = "f d:t>R (L t) t /fs;walk d"; for engine in ENGINES_ALL { let out = run_err(engine, src, &["f", locked.to_str().unwrap()]); assert!( From a760f89973a16d7e9506dd82e99bac179eb62f24 Mon Sep 17 00:00:00 2001 From: Daniel Morris Date: Sun, 9 Aug 2026 01:15:07 +0100 Subject: [PATCH 08/11] skills: bring ilo-language and ilo-agent back under token caps Both modules had drifted over their CI caps (2553/1950 and 2010/1700) via accumulated additions. Cuts are dedupes and factual fixes, not lost content: the reserved-builtin list appeared twice; the Running block listed both extension eras side by side; the 'spacing (CRITICAL)' section taught pre-ILO-544 behaviour as gospel (updated to the new contract); adjacent-pair trap, TCO, effects, guards, contracts, pipes prose compressed. Now 1943/1950 and under 1700; TOTAL 13680 under the 14000 cap. --- skills/ilo/ilo-agent.md | 28 ++++-------------- skills/ilo/ilo-language.md | 60 ++++++++++---------------------------- 2 files changed, 20 insertions(+), 68 deletions(-) diff --git a/skills/ilo/ilo-agent.md b/skills/ilo/ilo-agent.md index 6b263720..1626b0fa 100644 --- a/skills/ilo/ilo-agent.md +++ b/skills/ilo/ilo-agent.md @@ -25,16 +25,11 @@ Every skill subcommand accepts `--json` (short alias `-j`, ILO-442). `ilo skill ## Running ``` -ilo file.ilo auto-pick main -ilo file.ilo func a b call named fn -ilo 'f x:n>n;+x 1' 5 inline source -ilo --jit file.ilo --bench main JIT + bench -ilo file.ilo --bench main --json bench output as NDJSON -ilo file.ilo --bench main --json --silent suppress program stdout ilo file.@ auto-pick main ilo file.@ func a b call named fn -ilo 'f x:n>n;+x 1' 5 inline source -ilo --jit file.@ --bench main JIT + bench +ilo 'f x:n>n;+x 1' 5 inline source +ilo --jit file.@ --bench main JIT + bench +ilo file.@ --bench main --json bench NDJSON (--silent to drop program stdout) ``` `--silent` / `-s` mutes program-level `prnt` (and `prnv` / `jprn` / JIT prints) for the run. Paired with `--bench --json` it gives agent harnesses (e.g. persona cost rollup) a clean JSON stream on stdout instead of 10k+ lines of benchmarked output. Stderr is never silenced. @@ -50,7 +45,7 @@ First positional dispatches to a fn when it has ident shape. Otherwise (paths, n AOT-compiled binaries (`ilo compile`) follow the same contract byte-for-byte. -**Auto-echo suppression.** An entry-fn ending in a bare `prnt` call, a tail loop with no early return, or — when the body has an unconditional top-level `prnt` — a wrapped string-literal tail `~"text"` / `^"text"` (status sentinel) does NOT auto-echo its return value. The collision-avoidance rules let you write `m>R t t;prnt "report";~"ok"` and get clean `report\n` on stdout instead of `report\nok\n`. A no-prnt function returning `~"ok"` (e.g. `addtask`) still emits `ok` — the wrapped literal IS the output. `~v` where `v` is a binding or call always auto-echoes; only string LITERAL sentinels are dropped. +**Auto-echo suppression.** After an unconditional `prnt` (or a bare-`prnt`/tail-loop ending), a string-LITERAL sentinel tail `~"ok"`/`^"err"` is not echoed: `m>R t t;prnt "report";~"ok"` prints just `report`. Without a `prnt`, the literal IS the output; `~v` for bindings/calls always echoes. ## Testing @@ -101,20 +96,7 @@ main>_ ## Constrained decoding -`ilo constrain` exports the parser grammar so an external LLM harness can apply logit masks at generation time, making syntactically invalid ilo unreachable before `ilo check` ever runs. - -``` -ilo constrain grammar state machine as JSON (default) -ilo constrain --mode masks per-state binary masks over token vocabulary -ilo constrain --mode completions --file foo.ilo --line 3 --col 12 valid tokens at cursor -``` - -Three JSON shapes: -- `--mode states`: `{"schemaVersion":1,"states":{"TopLevel":{"transitions":{...}},...},"initial":"TopLevel","accept":["End"]}`. 29 parse states. -- `--mode masks`: `{"schemaVersion":1,"vocabulary":["type","tool",...],"masks":{"TopLevel":[1,1,1,...0],...}}`. 59 token categories. -- `--mode completions`: `{"schemaVersion":1,"state":"FnHeader","validTokens":["<","ident",">",...]}`. - -The state machine is static (grammar shape, not parser bookkeeping). Prevents lex/parse errors at generation; type errors and runtime errors still caught by `ilo check` and `ilo run`. +`ilo constrain` exports the parser grammar for generation-time logit masking — syntactically invalid ilo becomes unreachable before `ilo check` runs. Modes: default `states` (29-state machine JSON), `--mode masks` (per-state binary masks, 59 token categories), `--mode completions --file f --line N --col M` (valid tokens at cursor). Static grammar shape only; type/runtime errors still caught downstream. ## Branching diff --git a/skills/ilo/ilo-language.md b/skills/ilo/ilo-language.md index d33d18e2..ca6e30ae 100644 --- a/skills/ilo/ilo-language.md +++ b/skills/ilo/ilo-language.md @@ -11,47 +11,29 @@ Prefix-notation, strongly-typed, verified pre-run. Bodies `;`-separated or newli `tot p:n q:n r:n>n;s=*p q;t=*s r;+s t`. No param parens. `>` returns, `;` separates, last expr returns. Zero-arg: `make-id()`. -Single-line: `f x:n>n;+x 1`. Brace-block: `f x:n>n { s=+x 1; *s s }` (same semantics, braces wrap whole body). Multi-line: `f x:n>n` then indented body, newline = `;` (PR #501 also normalises CRLF). Trailing `;` on header (`f x:n>n;\n a=+x 1\n *a 2`) is optional; both forms parse. Multi-step: bind intermediates then tail expr: `add-and-double x:n y:n>n;s=+x y;*s 2`. Early return: braceless guard `>=x 0 val` or `ret val`. Result unwrap mid-body: `v=call!;use v`. +Single-line `f x:n>n;+x 1`; brace-block `f x:n>n { ... }`; multi-line = header then indented body (newline = `;`, trailing header `;` optional). Bind intermediates then tail expr. Early return: braceless guard `>=x 0 val` or `ret val`. Result unwrap mid-body: `v=call!;use v`. ## types `n` num, `t` text, `b` bool, `_` nil/any. `L n` list, `M t n` map, `R n t` result, `O n` optional, `S a b c` sum (closed, runtime `t`), `F n t` fn-type. Named: `order`. Type vars: any letter except `n t b`. `?? x d` nil-coalesce; unwraps `O T` only. For `R T E` use `default-on-err r d`. -## spacing (CRITICAL) +## spacing -**Every token needs whitespace separation.** ilo has no implicit concatenation or adjacency. A number immediately followed by a string (`90"A"`) is a parse error; use `90 "A"`. A closing paren followed by a number (`)2`) is a parse error; use `) 2`. This is the single most common model mistake. +Adjacent tokens parse (ILO-537/544): `90"A"` ok, `f(x)2` extends the call (`f(x)2` ≡ `f(x) 2` ≡ `f x 2`). Prefer spaces for readability. ## operators Binary `+ - * / % < > <= >= = !=`, bool `& | !`, append `+=`. Nest `+*a b c`=`(a*b)+c`; outer binds inner LEFT. Atoms/nested-ops not calls; bind first: `r=fac -n 1;*n r`. No compound `<=a b`. Glued `-n` = neg literal; bare `0 -1` errs ILO-P001. **`??` precedence**: `+a ??d b`=`a + (d ?? b)`, NOT `(a??d)+b`. For `(a??d)+b` bind first (`x=a??d;+x b`) or wrap (`+(a??d) b`). -### `*/` `/*` `+-` `-+` — adjacent-prefix-pair trap (READ THIS) +### `*/` `/*` `+-` `-+` — adjacent-prefix-pair trap -`*/` is **NOT** a 3-arg compound multiply-then-divide. It is two **separate** prefix ops `*` then `/`, parsed by the standard "outer binds inner LEFT" rule. So: +NOT 3-arg compounds — two separate prefix ops, outer binds inner LEFT: ``` -*/a b c -- parses as (a/b)*c ← b is the DIVISOR (2nd arg), not the 3rd -/*a b c -- parses as (a*b)/c ← c is the divisor -+-a b c -- parses as (a-b)+c --+a b c -- parses as (a+b)-c +*/a b c = (a/b)*c /*a b c = (a*b)/c +-a b c = (a-b)+c -+a b c = (a+b)-c ``` -This is the most-asked-about gotcha in agent feedback: `*/ sz 0.3 0` looks like "scale `sz` by 0.3, then divide by 0" but actually evaluates `(sz / 0.3) * 0` — and if the second arg is `0` you get a runtime divide-by-zero from the `/`, not from the trailing `0`. The runtime fires a `hint:` diagnostic naming the parse order for all four pairs (`*/`, `/*`, `+-`, `-+`) at prefix position. - -To get **multiply-then-divide** `(a*b)/c` (the common percentage-scaling shape), pick one: - -``` -/*a b c -- swap the prefix-pair order (terse) -r=*a b;/r c -- bind the product, then divide (explicit) -``` - -Worked example — scale `sz` by 30% with explicit divisor: - -``` --- DON'T: */ sz 0.3 100 parses as (sz / 0.3) * 100 = sz * 333.33... --- DO: /*sz 0.3 100 parses as (sz * 0.3) / 100 = sz * 0.003 --- DO: r=*sz 0.3;/r 100 -``` +So `*/ sz 0.3 0` is `(sz/0.3)*0` — div-by-zero comes from the `/`, not the trailing 0 (runtime hints the parse order). For multiply-then-divide `(a*b)/c`: `/*a b c`, or bind first `r=*a b;/r c`. ## idents @@ -59,7 +41,7 @@ Worked example — scale `sz` by 30% with explicit divisor: ## guards & conditionals -Three distinct shapes. `cond expr` early return (`>=sp 1000 "gold"`); `cond{body}` runs body NO early return; `cond{a}{b}` value no early return. Ternary: `?h cond a b` (3-arg: bool cond, true-value, false-value — requires spaces between ALL operands: `?h >=x 90 "A" "B"`). `?h cond{...}` illegal. `!` negates all. **Nested ternaries supported**: `?h >=x 90 "A" ?h >=x 80 "B" "C"`. For 3+ branches, match is clearer: `?x{90:"A";80:"B";70:"C";_:"F"}` or guard chain. Bare comparison IS a guard; bind to return a bool: `r=>a b;r`. +`cond expr` early-returns (`>=sp 1000 "gold"`); `cond{body}` no early return; `cond{a}{b}` value form. Ternary `?h cond a b`, spaces between ALL operands; nests: `?h >=x 90 "A" ?h >=x 80 "B" "C"`; `?h cond{...}` illegal; 3+ branches read better as match `?x{90:"A";_:"F"}`. `!` negates. Bare comparison IS a guard; bind for a bool: `r=>a b;r`. ## match @@ -71,11 +53,11 @@ Three distinct shapes. `cond expr` early return (`>=sp 1000 "gold"`); `cond{body ## contracts (prototype) -Optional `req` (precondition) and `ens` (postcondition) after return type, before `;`/body. `div a:n b:n>R n t req b!=0;=b 0 ^"divide by zero";~/a b`. `ens result>=0` stored, shown by `ilo explain`. Warning-only: ILO-W030 fires at call sites where the verifier can't find a matching preceding guard. Pattern-based (guards like `=b 0 ^"..."` satisfy `req b!=0`), not SMT. +Optional `req`/`ens` after return type: `div a:n b:n>R n t req b!=0;...`. Warning-only ILO-W030 at call sites lacking a matching guard; pattern-based, not SMT. ## optional vs result -Two distinct types, two distinct unwraps. `O T` = maybe-value (`nil` or `T`), no error payload; unwrap with `?? x d`. `R T E` = ok-or-err with payload; unwrap with `~`/`^` match arms, `!`, `!!`, or `default-on-err r d`. Using `??` on `R T E` is ILO-T041; using `default-on-err` on `O T` is ILO-T040. +`O T` = nil-or-value, unwrap `?? x d`. `R T E` = ok-or-err with payload, unwrap `~`/`^` arms, `!`, `!!`, or `default-on-err r d`. `??` on `R` = ILO-T041; `default-on-err` on `O` = ILO-T040. `O t`: `name = ?? name-opt "default"` — nil-coalesce, `O t -> t`. `R t t`: `name = default-on-err r "fallback"`, or `?r{~v:v;^_:"fallback"}` — Result unwrap, `R t e -> t`. @@ -86,27 +68,17 @@ Two distinct types, two distinct unwraps. `O T` = maybe-value (`nil` or `T`), no ## tail-call optimisation -Tail calls do not consume host-stack frames. A function that recurses in tail position runs to arbitrary depth — use tail-recursive accumulators for iteration beyond what `@` covers. No `loop` keyword by design. Tail position = last stmt of body, `ret` expr, an arm of a tail-position `?` match, body of a braceless guard. Peephole fires on direct user-fn name calls with no `!`/`!!`. Tree + VM trampoline today; JIT/AOT pending. Example: `count-down n:n>n;=n 0 0;count-down -n 1`. +Tail calls don't consume stack — tail recursion runs to arbitrary depth (no `loop` keyword by design). Tail position = last body stmt, `ret` expr, tail-`?` arm, braceless-guard body. Direct user-fn calls only, no `!`/`!!`. ## effects -Optional sigils after return type track side effects at verify time. No runtime cost. - -`/http /fs /io /net /ml /time /rand` after the return type (and after `^effect_set` if present): - -``` -fetch url:t>R t t /http -save path:t data:t>R _ t /fs /http -pure-sum xs:L n>n;sum xs -``` - -No sigils = pure. Verifier rejects side-effectful calls in pure fns (ILO-W051, warning; `--strict` to fail). Declared must cover actual (transitive: calling a `/http` fn makes caller `/http`). Over-declaring safe. Tools (external calls) count as `/http`. +Optional sigils `/http /fs /io /net /ml /time /rand` after the return type: `fetch url:t>R t t /http`. No sigils = pure; side-effectful calls in pure fns fire ILO-W051 (warning; `--strict` fails). Declared must cover actual, transitively. Over-declaring safe. Tools count as `/http`. ## pipes `xs >> flt pos >> map sq` desugars left-to-right. Wrap `()` for non-last fns. -**Result-aware short-circuit (ILO-510).** When the left side returns `R T E` or `O T`, `>>` auto-unwraps: `~v` passes inner value to next stage; `^e` short-circuits and propagates out of enclosing fn. Non-Result values pass through unchanged. No explicit `!` needed per stage: `get url>>jpar>>jpth "name"` — if any stage returns `^e`, remaining stages skip and `^e` propagates. Enclosing fn must return `R`/`O`. +Result-aware: when a stage returns `R`/`O`, `>>` auto-unwraps `~v` into the next stage and short-circuits `^e` out of the enclosing fn (which must return `R`/`O`): `get url>>jpar>>jpth "name"`. ## lambdas @@ -130,11 +102,9 @@ Bare top-level statements auto-wrap into a synthetic `main>_;`. `prnt +2 2` alon ## reserved names (DO NOT use as bindings) -**All 1-3 char lowercase identifiers are likely reserved builtins.** If you need a local variable, use 4+ chars: `total` not `tl`, `avg-v` not `av`, `count` not `ct`. Reserved: `at hd pi tl rd wr ct` (2-char) and `abs avg b64 cap cat cel chr cos del det dot env exp fft fld flr flt fmt frq get grp has hed inv len log lst lwr map max min mod now num opt ord pat pow pst put rdb rdl rep rev rgx rng rnd rou run sin slc spl srt str sum tan tau trm unq upr wra wrl zip` (3-char). Also avoid `avg` `tl` `len` `sum` `map` `cat` `str` `fmt` `fld` `flt` `srt` `at` `hd` `pi` `rd` `wr` `num` `ord` `rev` `run` `sin` `mod` `now` — the model reaches for these most. - -Fn/binding shadowing builtin/alias fires `ILO-P011`. 2-char safe; 4+ safe except `take drop mget mset flat range`; 3-char safe. +**All 1-3 char lowercase identifiers are likely reserved builtins.** If you need a local variable, use 4+ chars: `total` not `tl`, `avg-v` not `av`, `count` not `ct`. Reserved: `at hd pi tl rd wr ct` and `abs avg b64 cap cat cel chr cos del det dot env exp fft fld flr flt fmt frq get grp has hed inv len log lst lwr map max min mod now num opt ord pat pow pst put rdb rdl rep rev rgx rng rnd rou run sin slc spl srt str sum tan tau trm unq upr wra wrl zip`. -`e` `at hd pi tl rd wr ct` `abs avg cap cat cel chr cos det dot env exp fft fld flr flt fmt frq get grp has inv len log lsd lst lwr map max min mod now num ord pow pst rdb rdl rev rgx rng rnd rou run sin slc spl srt str sum tan tau trm unq upr wrl zip` +Shadowing a builtin/alias fires `ILO-P011`. 4+ chars safe except `take drop mget mset flat range`. ## cross-lang gotchas From 39b5a9b24da953fa4971b2936f484dd3cbf6e207 Mon Sep 17 00:00:00 2001 From: Daniel Morris Date: Sun, 9 Aug 2026 01:15:07 +0100 Subject: [PATCH 09/11] marketplace: single version key at 26.8.0 The plugin entry carried DUPLICATE version keys (26.5.0 shadowed by a pre-CalVer 0.13.0), so the marketplace advertised a version scheme retired in May. The 26.8 release cut missed it; marketplace_version_matches_cargo_toml now passes. --- .claude-plugin/marketplace.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 0c4e4bd6..fa199727 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -11,8 +11,7 @@ "name": "ilo", "source": "./", "description": "Write, run, debug, and explain programs in ilo — a token-optimised programming language for AI agents", - "version": "26.5.0", - "version": "0.13.0", + "version": "26.8.0", "author": { "name": "Daniel Morris" }, From a99c040c4c803f50e567008d852f081e96ee2e61 Mon Sep 17 00:00:00 2001 From: Daniel Morris Date: Sun, 9 Aug 2026 01:15:27 +0100 Subject: [PATCH 10/11] chore: rustfmt sweep + silence three stray warnings The branch tip had unformatted files from the recent rebase (fmt CI would fail every PR); this is the mechanical cargo fmt pass plus: unused Arc import in http_provider cfg-gated to the tools feature that uses it, and two unused variables underscore-prefixed. --- src/ast/mod.rs | 44 +++++-- src/backend/python/emit.rs | 5 +- src/builtins.rs | 39 ++++-- src/codegen/explain.rs | 5 +- src/codegen/fmt.rs | 2 +- src/constrain.rs | 186 +++++++++++++-------------- src/hir/lower.rs | 25 +++- src/interpreter/mod.rs | 63 ++++++--- src/lexer/mod.rs | 12 +- src/lib.rs | 2 +- src/main.rs | 212 +++++++++++++++++++++---------- src/tools/http_provider.rs | 1 + src/verify.rs | 246 ++++++++++++++++++++++-------------- src/vm/compile_cranelift.rs | 12 +- src/vm/jit_cranelift.rs | 12 +- src/vm/mod.rs | 56 ++++++-- 16 files changed, 595 insertions(+), 327 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 0cfa302e..d4ac870f 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -233,7 +233,10 @@ fn extract_url_host(url: &str) -> String { } else { url }; - rest.split(['/', '?', '#']).next().unwrap_or(rest).to_string() + rest.split(['/', '?', '#']) + .next() + .unwrap_or(rest) + .to_string() } /// True if `pattern` matches `host`. @@ -1591,7 +1594,8 @@ mod tests { name: "f".to_string(), params: vec![], return_type: Type::Number, - effect_set: None, effect_sigils: vec![], + effect_set: None, + effect_sigils: vec![], precondition: None, postcondition: None, body: vec![Spanned::unknown(Stmt::Expr(Expr::Literal( @@ -1628,7 +1632,8 @@ mod tests { name: "f".to_string(), params: vec![], return_type: Type::Number, - effect_set: None, effect_sigils: vec![], + effect_set: None, + effect_sigils: vec![], precondition: None, postcondition: None, body: vec![Spanned::unknown(Stmt::While { @@ -1680,7 +1685,8 @@ mod tests { name: "f".to_string(), params: vec![], return_type: Type::Number, - effect_set: None, effect_sigils: vec![], + effect_set: None, + effect_sigils: vec![], precondition: None, postcondition: None, body: vec![Spanned::unknown(Stmt::Return(Expr::Call { @@ -1714,7 +1720,8 @@ mod tests { name: "f".to_string(), params: vec![], return_type: Type::Number, - effect_set: None, effect_sigils: vec![], + effect_set: None, + effect_sigils: vec![], precondition: None, postcondition: None, body: vec![Spanned::unknown(Stmt::Destructure { @@ -1755,7 +1762,8 @@ mod tests { name: "f".to_string(), params: vec![], return_type: Type::Number, - effect_set: None, effect_sigils: vec![], + effect_set: None, + effect_sigils: vec![], precondition: None, postcondition: None, body: vec![Spanned::unknown(Stmt::Break(Some(Expr::Call { @@ -1789,7 +1797,8 @@ mod tests { name: "f".to_string(), params: vec![], return_type: Type::Number, - effect_set: None, effect_sigils: vec![], + effect_set: None, + effect_sigils: vec![], precondition: None, postcondition: None, body: vec![ @@ -1816,7 +1825,8 @@ mod tests { name: "f".to_string(), params: vec![], return_type: Type::Number, - effect_set: None, effect_sigils: vec![], + effect_set: None, + effect_sigils: vec![], precondition: None, postcondition: None, body: vec![Spanned::unknown(Stmt::Expr(Expr::NilCoalesce { @@ -1864,7 +1874,8 @@ mod tests { name: "f".to_string(), params: vec![], return_type: Type::Number, - effect_set: None, effect_sigils: vec![], + effect_set: None, + effect_sigils: vec![], precondition: None, postcondition: None, body: vec![Spanned::unknown(Stmt::Expr(Expr::Record { @@ -1907,7 +1918,8 @@ mod tests { name: "f".to_string(), params: vec![], return_type: Type::Number, - effect_set: None, effect_sigils: vec![], + effect_set: None, + effect_sigils: vec![], precondition: None, postcondition: None, body: vec![Spanned::unknown(Stmt::Expr(Expr::Match { @@ -1961,7 +1973,8 @@ mod tests { name: "f".to_string(), params: vec![], return_type: Type::Number, - effect_set: None, effect_sigils: vec![], + effect_set: None, + effect_sigils: vec![], precondition: None, postcondition: None, body: vec![Spanned::unknown(Stmt::Expr(Expr::With { @@ -2015,7 +2028,8 @@ mod tests { ty: Type::Number, }], return_type: Type::Number, - effect_set: None, effect_sigils: vec![], + effect_set: None, + effect_sigils: vec![], precondition: None, postcondition: None, body: vec![Spanned::unknown(Stmt::Expr(Expr::Ref("x".to_string())))], @@ -2043,7 +2057,8 @@ mod tests { name: "f".to_string(), params: vec![], return_type: Type::Number, - effect_set: None, effect_sigils: vec![], + effect_set: None, + effect_sigils: vec![], precondition: None, postcondition: None, body: vec![Spanned::unknown(Stmt::Match { @@ -2086,7 +2101,8 @@ mod tests { name: "f".to_string(), params: vec![], return_type: Type::Number, - effect_set: None, effect_sigils: vec![], + effect_set: None, + effect_sigils: vec![], precondition: None, postcondition: None, body: vec![Spanned::unknown(Stmt::Expr(Expr::Match { diff --git a/src/backend/python/emit.rs b/src/backend/python/emit.rs index c577f212..33cd262d 100644 --- a/src/backend/python/emit.rs +++ b/src/backend/python/emit.rs @@ -311,7 +311,7 @@ fn emit_decl(out: &mut String, decl: &Decl, level: usize) { Decl::Use { .. } => {} // resolved before codegen — skip Decl::VersionPragma { .. } => {} // pragma — no Python output Decl::Error { .. } => {} // poison node — skip - Decl::Test { .. } => {} // shadow test block — skip + Decl::Test { .. } => {} // shadow test block — skip } } @@ -2278,7 +2278,8 @@ mod tests { ty: Type::Text, }], return_type: Type::Number, - effect_set: None, effect_sigils: vec![], + effect_set: None, + effect_sigils: vec![], precondition: None, postcondition: None, body: vec![Spanned::unknown(Stmt::Expr(Expr::Call { diff --git a/src/builtins.rs b/src/builtins.rs index e777fe5e..c6a1bd77 100644 --- a/src/builtins.rs +++ b/src/builtins.rs @@ -1098,15 +1098,34 @@ impl Builtin { use crate::ast::Effect; match self { // HTTP builtins - Builtin::Get | Builtin::Post | Builtin::GetMany | Builtin::GetTo - | Builtin::Getx | Builtin::GetStream | Builtin::GetStreamH - | Builtin::PostStream | Builtin::PostStreamH - | Builtin::Del | Builtin::Hed | Builtin::Opt | Builtin::Pstx - | Builtin::Put | Builtin::Pat => Some(Effect::Http), + Builtin::Get + | Builtin::Post + | Builtin::GetMany + | Builtin::GetTo + | Builtin::Getx + | Builtin::GetStream + | Builtin::GetStreamH + | Builtin::PostStream + | Builtin::PostStreamH + | Builtin::Del + | Builtin::Hed + | Builtin::Opt + | Builtin::Pstx + | Builtin::Put + | Builtin::Pat => Some(Effect::Http), // Filesystem builtins - Builtin::Rd | Builtin::RdJson | Builtin::Rdl | Builtin::Rdjl | Builtin::Rdb - | Builtin::Wr | Builtin::Wrl | Builtin::Ls | Builtin::Walk | Builtin::Glob - | Builtin::Isfile | Builtin::Isdir => Some(Effect::Fs), + Builtin::Rd + | Builtin::RdJson + | Builtin::Rdl + | Builtin::Rdjl + | Builtin::Rdb + | Builtin::Wr + | Builtin::Wrl + | Builtin::Ls + | Builtin::Walk + | Builtin::Glob + | Builtin::Isfile + | Builtin::Isdir => Some(Effect::Fs), // Console / env I/O Builtin::Prnt | Builtin::Env | Builtin::EnvOr | Builtin::EnvAll => Some(Effect::Io), // Time builtins @@ -1114,7 +1133,9 @@ impl Builtin { // Random builtins Builtin::Rnd | Builtin::Rndn => Some(Effect::Rand), // Process / external execution - Builtin::Run | Builtin::Run2 | Builtin::RunFullEnv | Builtin::Run2FullEnv => Some(Effect::Net), + Builtin::Run | Builtin::Run2 | Builtin::RunFullEnv | Builtin::Run2FullEnv => { + Some(Effect::Net) + } // Everything else is pure _ => None, } diff --git a/src/codegen/explain.rs b/src/codegen/explain.rs index d6353240..0f727239 100644 --- a/src/codegen/explain.rs +++ b/src/codegen/explain.rs @@ -27,7 +27,10 @@ pub fn explain(program: &Program, filename: Option<&str>) -> String { // Compute the snippet to append for this declaration, or None to skip it. let snippet: Option = match decl { // Resolved before codegen / poison nodes — skip silently - Decl::Use { .. } | Decl::Error { .. } | Decl::VersionPragma { .. } | Decl::Test { .. } => None, + Decl::Use { .. } + | Decl::Error { .. } + | Decl::VersionPragma { .. } + | Decl::Test { .. } => None, Decl::Function { name, diff --git a/src/codegen/fmt.rs b/src/codegen/fmt.rs index 3164c116..7c78c467 100644 --- a/src/codegen/fmt.rs +++ b/src/codegen/fmt.rs @@ -223,7 +223,7 @@ fn fmt_decl(out: &mut String, decl: &Decl, mode: FmtMode) { out.push('\n'); } Decl::Error { .. } => {} // poison node — skip - Decl::Test { .. } => {} // shadow test block — not emitted as output code + Decl::Test { .. } => {} // shadow test block — not emitted as output code } } diff --git a/src/constrain.rs b/src/constrain.rs index 89323c1a..8cb291b4 100644 --- a/src/constrain.rs +++ b/src/constrain.rs @@ -32,7 +32,7 @@ //! the actual parser state, including arity tables and context. use crate::lexer::Token; -use serde_json::{json, Value}; +use serde_json::{Value, json}; // ── Token vocabulary ─────────────────────────────────────────────────────── @@ -46,11 +46,11 @@ use serde_json::{json, Value}; #[repr(u8)] pub enum TokenCat { // ── Keywords ── - Type, // `type` - Tool, // `tool` - Use, // `use` - With, // `with` - By, // `by` + Type, // `type` + Tool, // `tool` + Use, // `use` + With, // `with` + By, // `by` // ── Type sigils ── ListType, // `L` ResultType, // `R` @@ -63,54 +63,54 @@ pub enum TokenCat { U64Type, // `U64` I64Type, // `I64` // ── Literals ── - True, // `true` - False, // `false` - Nil, // `nil` - Number, // any numeric literal - Text, // any string literal + True, // `true` + False, // `false` + Nil, // `nil` + Number, // any numeric literal + Text, // any string literal // ── Identifiers ── Ident, // any lowercase identifier Underscore, // `_` // ── Multi-char operators ── - GreaterEq, // `>=` - LessEq, // `<=` - NotEq, // `!=` - PlusEq, // `+=` - PipeOp, // `>>` + GreaterEq, // `>=` + LessEq, // `<=` + NotEq, // `!=` + PlusEq, // `+=` + PipeOp, // `>>` NilCoalesce, // `??` - BangBang, // `!!` - DotDot, // `..` + BangBang, // `!!` + DotDot, // `..` DotQuestion, // `.?` // ── Single-char operators ── - Plus, // `+` - Minus, // `-` - Star, // `*` - Slash, // `/` - Greater, // `>` - Less, // `<` - Eq, // `=` or `==` - Amp, // `&` - Pipe, // `|` - Question, // `?` - At, // `@` - Bang, // `!` - Caret, // `^` - Tilde, // `~` - Dollar, // `$` + Plus, // `+` + Minus, // `-` + Star, // `*` + Slash, // `/` + Greater, // `>` + Less, // `<` + Eq, // `=` or `==` + Amp, // `&` + Pipe, // `|` + Question, // `?` + At, // `@` + Bang, // `!` + Caret, // `^` + Tilde, // `~` + Dollar, // `$` // ── Punctuation ── - Colon, // `:` - Semi, // `;` - Dot, // `.` - Comma, // `,` - LBrace, // `{` - RBrace, // `}` - LParen, // `(` - RParen, // `)` - LBracket, // `[` - RBracket, // `]` + Colon, // `:` + Semi, // `;` + Dot, // `.` + Comma, // `,` + LBrace, // `{` + RBrace, // `}` + LParen, // `(` + RParen, // `)` + LBracket, // `[` + RBracket, // `]` // ── Control ── - Newline, // `\n` - Eof, // end of input + Newline, // `\n` + Eof, // end of input } impl TokenCat { @@ -310,8 +310,13 @@ impl TokenCat { Token::RBracket => TokenCat::RBracket, Token::Newline => TokenCat::Newline, // Reserved-keyword tokens from other languages — not valid output. - Token::KwIf | Token::KwReturn | Token::KwLet | Token::KwFn - | Token::KwDef | Token::KwVar | Token::KwConst => return None, + Token::KwIf + | Token::KwReturn + | Token::KwLet + | Token::KwFn + | Token::KwDef + | Token::KwVar + | Token::KwConst => return None, // Reserved words for tool-decl fields — not valid in code output. Token::Timeout | Token::Retry => return None, }) @@ -489,9 +494,7 @@ impl ParseState { ], // After `use` keyword — expecting a module path (ident). - ParseState::AfterUse => &[ - (TokenCat::Ident, ParseState::AfterIdent), - ], + ParseState::AfterUse => &[(TokenCat::Ident, ParseState::AfterIdent)], // Function header: after the name, expecting `<` type params, // `:` for param type, or `>` for return type (no-param case). @@ -509,9 +512,7 @@ impl ParseState { ], // After a param name — expecting `:` for type annotation. - ParseState::ParamName => &[ - (TokenCat::Colon, ParseState::ParamType), - ], + ParseState::ParamName => &[(TokenCat::Colon, ParseState::ParamType)], // After `:` in a param — expecting a type. ParseState::ParamType => &[ @@ -577,30 +578,30 @@ impl ParseState { // Statement start: bindings, loops, match, return, break, continue, expressions. ParseState::StmtStart => &[ - (TokenCat::Ident, ParseState::AfterIdent), // binding or call - (TokenCat::At, ParseState::AfterAt), // foreach/for-range + (TokenCat::Ident, ParseState::AfterIdent), // binding or call + (TokenCat::At, ParseState::AfterAt), // foreach/for-range (TokenCat::Question, ParseState::AfterQuestion), // match or ternary - (TokenCat::LBrace, ParseState::StmtStart), // brace block or destructure - (TokenCat::LBracket, ParseState::ListElem), // list literal + (TokenCat::LBrace, ParseState::StmtStart), // brace block or destructure + (TokenCat::LBracket, ParseState::ListElem), // list literal (TokenCat::Number, ParseState::AfterNumber), (TokenCat::Text, ParseState::AfterText), (TokenCat::True, ParseState::AfterIdent), (TokenCat::False, ParseState::AfterIdent), (TokenCat::Nil, ParseState::AfterIdent), - (TokenCat::Minus, ParseState::AfterOp), // negative number + (TokenCat::Minus, ParseState::AfterOp), // negative number (TokenCat::Underscore, ParseState::AfterIdent), // `_=expr` discard - (TokenCat::Tilde, ParseState::AfterTilde), // `~v` Ok constructor + (TokenCat::Tilde, ParseState::AfterTilde), // `~v` Ok constructor (TokenCat::Caret, ParseState::AfterCaretMatch), // `^e` Err constructor - (TokenCat::Bang, ParseState::AfterBang), // `!` auto-unwrap - (TokenCat::Dollar, ParseState::AfterIdent), // `$` special - (TokenCat::Semi, ParseState::StmtStart), // empty statement - (TokenCat::RBrace, ParseState::End), // end of body + (TokenCat::Bang, ParseState::AfterBang), // `!` auto-unwrap + (TokenCat::Dollar, ParseState::AfterIdent), // `$` special + (TokenCat::Semi, ParseState::StmtStart), // empty statement + (TokenCat::RBrace, ParseState::End), // end of body (TokenCat::Eof, ParseState::End), ], // After `@` — expecting loop variable (ident) or range start (number). ParseState::AfterAt => &[ - (TokenCat::Ident, ParseState::AfterIdent), // loop var name + (TokenCat::Ident, ParseState::AfterIdent), // loop var name (TokenCat::Number, ParseState::AfterNumber), // range start ], @@ -612,7 +613,7 @@ impl ParseState { (TokenCat::True, ParseState::AfterIdent), (TokenCat::False, ParseState::AfterIdent), (TokenCat::LBracket, ParseState::ListElem), - (TokenCat::Bang, ParseState::AfterBang), // `?!expr` unwrap then ternary + (TokenCat::Bang, ParseState::AfterBang), // `?!expr` unwrap then ternary (TokenCat::Tilde, ParseState::AfterTilde), (TokenCat::Caret, ParseState::AfterCaretMatch), ], @@ -630,8 +631,8 @@ impl ParseState { (TokenCat::Star, ParseState::AfterOp), (TokenCat::Slash, ParseState::AfterOp), (TokenCat::LBracket, ParseState::ListElem), - (TokenCat::LParen, ParseState::ExprStart), // parenthesised expression/lambda - (TokenCat::LBrace, ParseState::StmtStart), // brace lambda or block + (TokenCat::LParen, ParseState::ExprStart), // parenthesised expression/lambda + (TokenCat::LBrace, ParseState::StmtStart), // brace lambda or block (TokenCat::Tilde, ParseState::AfterTilde), (TokenCat::Caret, ParseState::AfterCaretMatch), (TokenCat::Bang, ParseState::AfterBang), @@ -648,7 +649,7 @@ impl ParseState { (TokenCat::True, ParseState::AfterIdent), (TokenCat::False, ParseState::AfterIdent), (TokenCat::Nil, ParseState::AfterIdent), - (TokenCat::Minus, ParseState::AfterOp), // nested prefix + (TokenCat::Minus, ParseState::AfterOp), // nested prefix (TokenCat::Plus, ParseState::AfterOp), (TokenCat::Star, ParseState::AfterOp), (TokenCat::Slash, ParseState::AfterOp), @@ -678,15 +679,15 @@ impl ParseState { (TokenCat::PlusEq, ParseState::AfterOp), (TokenCat::PipeOp, ParseState::AfterPipe), (TokenCat::NilCoalesce, ParseState::AfterOp), - (TokenCat::Bang, ParseState::AfterBang), // `ident!expr` unwrap + (TokenCat::Bang, ParseState::AfterBang), // `ident!expr` unwrap (TokenCat::BangBang, ParseState::AfterBang), // `ident!!` panic unwrap (TokenCat::Dot, ParseState::AfterDot), (TokenCat::DotQuestion, ParseState::AfterDot), - (TokenCat::DotDot, ParseState::AfterOp), // range + (TokenCat::DotDot, ParseState::AfterOp), // range // Statement/body terminators (TokenCat::Semi, ParseState::StmtStart), (TokenCat::RBrace, ParseState::End), - (TokenCat::Comma, ParseState::ExprStart), // in arg list or list + (TokenCat::Comma, ParseState::ExprStart), // in arg list or list (TokenCat::RParen, ParseState::AfterIdent), // closing paren expr (TokenCat::RBracket, ParseState::AfterIdent), // closing list (TokenCat::Eof, ParseState::End), @@ -730,22 +731,20 @@ impl ParseState { // Match arm: expecting a pattern. ParseState::MatchArm => &[ - (TokenCat::Text, ParseState::MatchArmBody), // literal pattern + (TokenCat::Text, ParseState::MatchArmBody), // literal pattern (TokenCat::Number, ParseState::MatchArmBody), (TokenCat::True, ParseState::MatchArmBody), (TokenCat::False, ParseState::MatchArmBody), (TokenCat::Nil, ParseState::MatchArmBody), - (TokenCat::Tilde, ParseState::AfterTilde), // `~v` ok-bind - (TokenCat::Caret, ParseState::AfterCaretMatch), // `^e` err-bind - (TokenCat::Ident, ParseState::MatchArmBody), // named pattern + (TokenCat::Tilde, ParseState::AfterTilde), // `~v` ok-bind + (TokenCat::Caret, ParseState::AfterCaretMatch), // `^e` err-bind + (TokenCat::Ident, ParseState::MatchArmBody), // named pattern (TokenCat::Underscore, ParseState::MatchArmBody), // wildcard - (TokenCat::Pipe, ParseState::MatchArm), // or-pattern + (TokenCat::Pipe, ParseState::MatchArm), // or-pattern ], // After a match arm pattern — expecting `:` then body. - ParseState::MatchArmBody => &[ - (TokenCat::Colon, ParseState::ExprStart), - ], + ParseState::MatchArmBody => &[(TokenCat::Colon, ParseState::ExprStart)], // After `~` (Ok bind) in match arm. ParseState::AfterTilde => &[ @@ -766,12 +765,12 @@ impl ParseState { (TokenCat::False, ParseState::AfterIdent), (TokenCat::Nil, ParseState::AfterIdent), (TokenCat::Minus, ParseState::AfterOp), - (TokenCat::LBracket, ParseState::ListElem), // nested list + (TokenCat::LBracket, ParseState::ListElem), // nested list (TokenCat::LParen, ParseState::ExprStart), (TokenCat::Tilde, ParseState::AfterTilde), (TokenCat::Caret, ParseState::AfterCaretMatch), - (TokenCat::RBracket, ParseState::AfterIdent), // close list - (TokenCat::Comma, ParseState::ListElem), // next element + (TokenCat::RBracket, ParseState::AfterIdent), // close list + (TokenCat::Comma, ParseState::ListElem), // next element (TokenCat::Bang, ParseState::AfterBang), ], @@ -783,14 +782,12 @@ impl ParseState { ], // After `>>` pipe operator — expecting a function name. - ParseState::AfterPipe => &[ - (TokenCat::Ident, ParseState::AfterIdent), - ], + ParseState::AfterPipe => &[(TokenCat::Ident, ParseState::AfterIdent)], // After `!` — auto-unwrap or start of `!!`. ParseState::AfterBang => &[ (TokenCat::Ident, ParseState::AfterIdent), - (TokenCat::Bang, ParseState::AfterBang), // `!!` panic-unwrap + (TokenCat::Bang, ParseState::AfterBang), // `!!` panic-unwrap ], // After a number literal. @@ -847,9 +844,7 @@ impl ParseState { ], // End state — only EOF. - ParseState::End => &[ - (TokenCat::Eof, ParseState::End), - ], + ParseState::End => &[(TokenCat::Eof, ParseState::End)], } } @@ -909,7 +904,10 @@ pub fn state_machine_json() -> Value { /// Build the token vocabulary as a JSON array of strings. pub fn vocabulary_json() -> Value { - let vocab: Vec = TokenCat::ALL.iter().map(|c| c.as_str().to_string()).collect(); + let vocab: Vec = TokenCat::ALL + .iter() + .map(|c| c.as_str().to_string()) + .collect(); Value::Array(vocab.into_iter().map(Value::String).collect()) } @@ -1004,7 +1002,11 @@ pub fn completions_at_cursor(source: &str, line: usize, col: usize) -> Value { } // Get valid next tokens at the current state. - let valid: Vec = state.valid_tokens().iter().map(|c| c.as_str().to_string()).collect(); + let valid: Vec = state + .valid_tokens() + .iter() + .map(|c| c.as_str().to_string()) + .collect(); json!({ "schemaVersion": 1, @@ -1272,4 +1274,4 @@ mod tests { assert_eq!(line_col_to_offset("abc", 1, 10), 3); assert_eq!(line_col_to_offset("abc", 5, 1), 3); } -} \ No newline at end of file +} diff --git a/src/hir/lower.rs b/src/hir/lower.rs index 17a917c4..fd87bb50 100644 --- a/src/hir/lower.rs +++ b/src/hir/lower.rs @@ -59,7 +59,8 @@ pub fn lower(ast: &ast::Program, _verify_out: &VerifyResult) -> Result { let lowered_body = lower_function_body(body); decls.push(Decl::Function { @@ -84,7 +85,8 @@ pub fn lower(ast: &ast::Program, _verify_out: &VerifyResult) -> Result { decls.push(Decl::Tool { name: name.clone(), @@ -101,7 +103,9 @@ pub fn lower(ast: &ast::Program, _verify_out: &VerifyResult) -> Result continue, // Parse-recovery poison. A correctly sequenced caller verified the // program first and bailed on errors; reaching us is a bug. - ast::Decl::Test { .. } | ast::Decl::VersionPragma { .. } | ast::Decl::SumType { .. } => continue, + ast::Decl::Test { .. } + | ast::Decl::VersionPragma { .. } + | ast::Decl::SumType { .. } => continue, ast::Decl::Error { .. } => return Err(LowerError::PoisonDecl), } } @@ -226,7 +230,8 @@ fn lower_stmt(stmt: &ast::Spanned) -> Stmt { start, end, body, - .. } => Stmt::ForRange { + .. + } => Stmt::ForRange { binding: binding.clone(), start: lower_expr(start), end: lower_expr(end), @@ -253,7 +258,11 @@ fn lower_stmt(stmt: &ast::Spanned) -> Stmt { ast::Stmt::Continue => Stmt::Continue { span }, ast::Stmt::Defer { .. } => Stmt::Expr { - value: Expr::Ref { name: "nil".to_string(), ty: Ty::Unknown, span }, + value: Expr::Ref { + name: "nil".to_string(), + ty: Ty::Unknown, + span, + }, span, }, ast::Stmt::Destructure { bindings, value } => Stmt::Destructure { @@ -445,7 +454,11 @@ fn lower_expr(e: &ast::Expr) -> Expr { ty: Ty::Unknown, span, }, - _ => Expr::Ref { name: "nil".to_string(), ty: Ty::Unknown, span }, + _ => Expr::Ref { + name: "nil".to_string(), + ty: Ty::Unknown, + span, + }, } } diff --git a/src/interpreter/mod.rs b/src/interpreter/mod.rs index b0cf890e..cde17600 100644 --- a/src/interpreter/mod.rs +++ b/src/interpreter/mod.rs @@ -5179,7 +5179,10 @@ fn call_function(env: &mut Env, name: &str, args: Vec) -> Result { return match (&args[0], &args[1]) { (Value::Number(a), Value::Number(b)) => { if *b == 0.0 { - Err(RuntimeError::new("ILO-R003", format!("mod by zero: {} % {}", a, b))) + Err(RuntimeError::new( + "ILO-R003", + format!("mod by zero: {} % {}", a, b), + )) } else { Ok(Value::Number(a % b)) } @@ -8535,10 +8538,12 @@ fn call_function(env: &mut Env, name: &str, args: Vec) -> Result { let mut decompressed = String::new(); match decoder.read_to_string(&mut decompressed) { Ok(_) => return Ok(Value::Text(Arc::new(decompressed))), - Err(e) => return Err(RuntimeError::new( - "ILO-R009", - format!("zgunzip: decompression failed: {}", e), - )), + Err(e) => { + return Err(RuntimeError::new( + "ILO-R009", + format!("zgunzip: decompression failed: {}", e), + )); + } } } @@ -9974,9 +9979,7 @@ fn call_function(env: &mut Env, name: &str, args: Vec) -> Result { if let Some(ref pol) = policy { if let Some(crate::interpreter::Value::Text(url)) = args.first() { if let Err(msg) = pol.check_domain(url) { - return Ok(Value::Err(Box::new(Value::Text( - std::sync::Arc::new(msg) - )))); + return Ok(Value::Err(Box::new(Value::Text(std::sync::Arc::new(msg))))); } } } @@ -11080,7 +11083,11 @@ fn eval_expr(env: &mut Env, expr: &Expr) -> Result { None if *safe => Ok(Value::Nil), None => Err(RuntimeError::new( "ILO-R005", - format!("no field '{}' on record (fields: {:?})", field, fields.keys().collect::>()), + format!( + "no field '{}' on record (fields: {:?})", + field, + fields.keys().collect::>() + ), )), }, // World field access: .net .read .write .run → Bool @@ -11415,7 +11422,10 @@ fn eval_binop(op: &BinOp, left: &Value, right: &Value) -> Result { (BinOp::Multiply, Value::Number(a), Value::Number(b)) => Ok(Value::Number(a * b)), (BinOp::Divide, Value::Number(a), Value::Number(b)) => { if *b == 0.0 { - Err(RuntimeError::new("ILO-R003", format!("division by zero: {}/{}", a, b))) + Err(RuntimeError::new( + "ILO-R003", + format!("division by zero: {}/{}", a, b), + )) } else { Ok(Value::Number(a / b)) } @@ -14320,7 +14330,8 @@ mod tests { ty: Type::Number, }], return_type: Type::Result(Box::new(Type::Number), Box::new(Type::Text)), - effect_set: None, effect_sigils: vec![], + effect_set: None, + effect_sigils: vec![], precondition: None, postcondition: None, body: inner_body, @@ -14334,7 +14345,8 @@ mod tests { ty: Type::Number, }], return_type: Type::Result(Box::new(Type::Number), Box::new(Type::Text)), - effect_set: None, effect_sigils: vec![], + effect_set: None, + effect_sigils: vec![], precondition: None, postcondition: None, body: vec![ @@ -14408,7 +14420,8 @@ mod tests { ty: Type::Number, }], return_type: rnt.clone(), - effect_set: None, effect_sigils: vec![], + effect_set: None, + effect_sigils: vec![], precondition: None, postcondition: None, body: vec![Spanned::unknown(Stmt::Expr(Expr::Err(Box::new( @@ -14424,7 +14437,8 @@ mod tests { ty: Type::Number, }], return_type: rnt.clone(), - effect_set: None, effect_sigils: vec![], + effect_set: None, + effect_sigils: vec![], precondition: None, postcondition: None, body: unwrap_body("c"), @@ -14438,7 +14452,8 @@ mod tests { ty: Type::Number, }], return_type: rnt, - effect_set: None, effect_sigils: vec![], + effect_set: None, + effect_sigils: vec![], precondition: None, postcondition: None, body: unwrap_body("b"), @@ -18207,11 +18222,23 @@ mod tests { #[test] fn interpret_round_with_digits() { // rou x digits: round to N decimal places (ILO-535) - let r1 = run_str("f x:n y:n>n;rou x y", Some("f"), vec![Value::Number(3.14159), Value::Number(2.0)]); + let r1 = run_str( + "f x:n y:n>n;rou x y", + Some("f"), + vec![Value::Number(3.14159), Value::Number(2.0)], + ); assert_eq!(r1, Value::Number(3.14)); - let r2 = run_str("f x:n y:n>n;rou x y", Some("f"), vec![Value::Number(3.14159), Value::Number(4.0)]); + let r2 = run_str( + "f x:n y:n>n;rou x y", + Some("f"), + vec![Value::Number(3.14159), Value::Number(4.0)], + ); assert_eq!(r2, Value::Number(3.1416)); - let r3 = run_str("f x:n y:n>n;rou x y", Some("f"), vec![Value::Number(2.5), Value::Number(0.0)]); + let r3 = run_str( + "f x:n y:n>n;rou x y", + Some("f"), + vec![Value::Number(2.5), Value::Number(0.0)], + ); assert_eq!(r3, Value::Number(3.0)); } diff --git a/src/lexer/mod.rs b/src/lexer/mod.rs index 5a4045ca..cccd918f 100644 --- a/src/lexer/mod.rs +++ b/src/lexer/mod.rs @@ -1109,7 +1109,8 @@ fn lex_normalized(normalized: &str) -> Result } if scan_end > span.start { // Merge: replace the last Ident token with the full name - let full_name = normalized[tokens.last().unwrap().1.start..scan_end].to_string(); + let full_name = + normalized[tokens.last().unwrap().1.start..scan_end].to_string(); let full_span = tokens.last().unwrap().1.start..scan_end; let bump = scan_end.saturating_sub(span.end); if bump > 0 { @@ -1403,8 +1404,13 @@ fn lex_normalized(normalized: &str) -> Result // After a dot, keywords like `with`, `type`, `use` are field-name // fragments (e.g. `d.overlap_with_ilo`, `r.user_type`). // Merge them into the snake_case run. - Token::With | Token::Type | Token::Use | Token::Tool - | Token::Timeout | Token::Retry | Token::Policy => { + Token::With + | Token::Type + | Token::Use + | Token::Tool + | Token::Timeout + | Token::Retry + | Token::Policy => { has_underscore = true; j += 2; } diff --git a/src/lib.rs b/src/lib.rs index 881e71c6..ec354851 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,8 +10,8 @@ pub mod codegen; pub mod constrain; pub mod diagnostic; pub mod graph; -pub mod rng; pub mod hir; +pub mod rng; // `interpreter` is soft-deprecated as a user-selectable engine but stays as // the internal runtime for HOF callbacks that VM/Cranelift bail to, plus // shared runtime primitives (Value, MapKey, RuntimeError, math helpers). diff --git a/src/main.rs b/src/main.rs index bdc608f7..c50d87de 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2572,10 +2572,14 @@ fn mcp_cmd() -> i32 { "jsonrpc": "2.0", "id": id, "error": {"code": -32601, "message": "method not found"} - }) + }), }; - println!("{}", serde_json::to_string(&response).unwrap_or_else(|_| "{\"error\":\"serialization failed\"}".into())); + println!( + "{}", + serde_json::to_string(&response) + .unwrap_or_else(|_| "{\"error\":\"serialization failed\"}".into()) + ); } 0 } @@ -2587,15 +2591,18 @@ fn mcp_handle_tool(name: &str, args: &serde_json::Value) -> serde_json::Value { let source = args.get("source").and_then(|v| v.as_str()).unwrap_or(""); let (diags, _) = run_check_internal(source); let clean = diags.is_empty(); - let diag_json: Vec<_> = diags.iter().map(diag_to_json).collect(); + let _diag_json: Vec<_> = diags.iter().map(diag_to_json).collect(); let text = if clean { format!("OK: program is valid ({} diagnostics)", diags.len()) } else { - let lines: Vec<_> = diags.iter().map(|d| { - let code = d.code.unwrap_or(""); - let msg = &d.message; - format!("{}: {}", code, msg) - }).collect(); + let lines: Vec<_> = diags + .iter() + .map(|d| { + let code = d.code.unwrap_or(""); + let msg = &d.message; + format!("{}: {}", code, msg) + }) + .collect(); lines.join("\n") }; serde_json::json!({ @@ -2606,9 +2613,14 @@ fn mcp_handle_tool(name: &str, args: &serde_json::Value) -> serde_json::Value { "run" => { let source = args.get("source").and_then(|v| v.as_str()).unwrap_or(""); let func = args.get("func").and_then(|v| v.as_str()).unwrap_or("main"); - let cli_args: Vec = args.get("args") + let cli_args: Vec = args + .get("args") .and_then(|v| v.as_array()) - .map(|a| a.iter().filter_map(|x| x.as_str().map(String::from)).collect()) + .map(|a| { + a.iter() + .filter_map(|x| x.as_str().map(String::from)) + .collect() + }) .unwrap_or_default(); // Write source to temp file, run it @@ -2621,13 +2633,17 @@ fn mcp_handle_tool(name: &str, args: &serde_json::Value) -> serde_json::Value { } let mut cmd_args = vec![path.to_string_lossy().to_string()]; - if func != "main" { cmd_args.push(func.to_string()); } + if func != "main" { + cmd_args.push(func.to_string()); + } cmd_args.extend(cli_args); // Use the existing run pipeline - let output = std::process::Command::new(std::env::current_exe().unwrap_or_else(|_| "ilo".into())) - .args(&cmd_args) - .output(); + let output = std::process::Command::new( + std::env::current_exe().unwrap_or_else(|_| "ilo".into()), + ) + .args(&cmd_args) + .output(); let _ = std::fs::remove_file(&path); @@ -2650,7 +2666,7 @@ fn mcp_handle_tool(name: &str, args: &serde_json::Value) -> serde_json::Value { Err(e) => serde_json::json!({ "content": [{"type": "text", "text": format!("Failed to spawn: {}", e)}], "isError": true - }) + }), } } "explain" => { @@ -2658,21 +2674,35 @@ fn mcp_handle_tool(name: &str, args: &serde_json::Value) -> serde_json::Value { // Reuse the explain lookup let text = match diagnostic::registry::lookup(code) { Some(entry) => entry.long.to_string(), - None => format!("Unknown error code: {}. Codes have the form ILO-L001, ILO-P001, ILO-T001, ILO-R001.", code), + None => format!( + "Unknown error code: {}. Codes have the form ILO-L001, ILO-P001, ILO-T001, ILO-R001.", + code + ), }; serde_json::json!({ "content": [{"type": "text", "text": text}] }) } "constrain" => { - let mode = args.get("mode").and_then(|v| v.as_str()).unwrap_or("states"); + let mode = args + .get("mode") + .and_then(|v| v.as_str()) + .unwrap_or("states"); // Export grammar — reuse the constrain logic let result = match mode { "masks" => ilo::constrain::logit_masks_json(), "completions" => { let file = args.get("file").and_then(|v| v.as_str()); - let line = args.get("line").and_then(|v| v.as_u64()).map(|n| n as usize).unwrap_or(1); - let col = args.get("col").and_then(|v| v.as_u64()).map(|n| n as usize).unwrap_or(1); + let line = args + .get("line") + .and_then(|v| v.as_u64()) + .map(|n| n as usize) + .unwrap_or(1); + let col = args + .get("col") + .and_then(|v| v.as_u64()) + .map(|n| n as usize) + .unwrap_or(1); if let Some(f) = file { if let Ok(source) = std::fs::read_to_string(f) { ilo::constrain::completions_at_cursor(&source, line, col) @@ -2682,7 +2712,7 @@ fn mcp_handle_tool(name: &str, args: &serde_json::Value) -> serde_json::Value { } else { serde_json::json!({"error": "file required for completions mode"}) } - }, + } _ => ilo::constrain::state_machine_json(), }; let text = serde_json::to_string_pretty(&result).unwrap_or_else(|_| result.to_string()); @@ -2702,7 +2732,10 @@ fn mcp_handle_tool(name: &str, args: &serde_json::Value) -> serde_json::Value { } let (diags_before, _) = run_check_internal(source); - let fixable: Vec<_> = diags_before.iter().filter(|d| d.fix_plan.is_some()).collect(); + let fixable: Vec<_> = diags_before + .iter() + .filter(|d| d.fix_plan.is_some()) + .collect(); let fixes_applied = fixable.len(); // Apply fixes to the temp file content @@ -2725,13 +2758,23 @@ fn mcp_handle_tool(name: &str, args: &serde_json::Value) -> serde_json::Value { }) } "skill" => { - let action = args.get("action").and_then(|v| v.as_str()).unwrap_or("list"); + let action = args + .get("action") + .and_then(|v| v.as_str()) + .unwrap_or("list"); let name = args.get("name").and_then(|v| v.as_str()); match action { "list" => { - let items: Vec<_> = SKILLS.iter().map(|s| { - format!("- {}: {}", s.name, &s.description[..s.description.len().min(80)]) - }).collect(); + let items: Vec<_> = SKILLS + .iter() + .map(|s| { + format!( + "- {}: {}", + s.name, + &s.description[..s.description.len().min(80)] + ) + }) + .collect(); serde_json::json!({ "content": [{"type": "text", "text": items.join("\n")}] }) @@ -2752,13 +2795,13 @@ fn mcp_handle_tool(name: &str, args: &serde_json::Value) -> serde_json::Value { _ => serde_json::json!({ "content": [{"type": "text", "text": "Unknown action. Use 'list' or 'get'."}], "isError": true - }) + }), } } _ => serde_json::json!({ "content": [{"type": "text", "text": format!("Unknown tool: {}", name)}], "isError": true - }) + }), } } @@ -3374,7 +3417,10 @@ fn decl_name(decl: &ast::Decl) -> Option<&str> { ast::Decl::TypeDef { name, .. } => Some(name), ast::Decl::Alias { name, .. } => Some(name), ast::Decl::SumType { name, .. } => Some(name), - ast::Decl::Use { .. } | ast::Decl::VersionPragma { .. } | ast::Decl::Error { .. } | ast::Decl::Test { .. } => None, + ast::Decl::Use { .. } + | ast::Decl::VersionPragma { .. } + | ast::Decl::Error { .. } + | ast::Decl::Test { .. } => None, } } @@ -4106,10 +4152,16 @@ fn maybe_autoinstall_skills() { ]; for dir in &targets { - if dir.join("SKILL.md").exists() { continue; } + if dir.join("SKILL.md").exists() { + continue; + } let parent = dir.parent().unwrap_or(Path::new(".")); - if !parent.exists() { continue; } - if fs::create_dir_all(dir).is_err() { continue; } + if !parent.exists() { + continue; + } + if fs::create_dir_all(dir).is_err() { + continue; + } let _ = fs::write(dir.join("SKILL.md"), include_str!("../skills/ilo/SKILL.md")); for skill in SKILLS { let fname = format!("{}.md", skill.name); @@ -5065,7 +5117,15 @@ fn run_check_internal(source_arg: &str) -> (Vec, bool) { let token_spans: Vec<(lexer::Token, ast::Span)> = tokens .into_iter() - .map(|(t, r)| (t, ast::Span { start: r.start, end: r.end })) + .map(|(t, r)| { + ( + t, + ast::Span { + start: r.start, + end: r.end, + }, + ) + }) .collect(); let (mut program, parse_errors) = parser::parse(token_spans); @@ -5119,7 +5179,10 @@ fn run_check_internal(source_arg: &str) -> (Vec, bool) { /// Apply structured fix_plan edits from diagnostics to source files. fn fix_cmd(source_arg: &str, write: bool, mode: OutputMode) -> i32 { if !std::path::Path::new(source_arg).is_file() { - eprintln!("Error: {} is not a file. ilo fix requires a file path.", source_arg); + eprintln!( + "Error: {} is not a file. ilo fix requires a file path.", + source_arg + ); return 2; } @@ -5138,7 +5201,10 @@ fn fix_cmd(source_arg: &str, write: bool, mode: OutputMode) -> i32 { } else { eprintln!("No fixable diagnostics found."); if !diags.is_empty() { - eprintln!("{} diagnostic(s) remain (no structured fix plans available).", diags.len()); + eprintln!( + "{} diagnostic(s) remain (no structured fix plans available).", + diags.len() + ); } } return if diags.is_empty() { 0 } else { 1 }; @@ -5168,10 +5234,10 @@ fn fix_cmd(source_arg: &str, write: bool, mode: OutputMode) -> i32 { } // Apply the replacement: find `before` in the line range, replace with `after` - let region: String = modified_lines[line_start..=line_end.min(modified_lines.len() - 1)] - .join("\n"); + let region: String = + modified_lines[line_start..=line_end.min(modified_lines.len() - 1)].join("\n"); - if let Some(idx) = region.find(&edit.before) { + if let Some(_idx) = region.find(&edit.before) { let new_region = region.replacen(&edit.before, &edit.after, 1); let new_lines: Vec<&str> = new_region.split('\n').collect(); @@ -5212,7 +5278,10 @@ fn fix_cmd(source_arg: &str, write: bool, mode: OutputMode) -> i32 { // Re-check to count remaining diagnostics let (post_diags, post_had_errors) = run_check_internal(source_arg); - let remaining = post_diags.iter().filter(|d| d.severity == Severity::Error).count(); + let remaining = post_diags + .iter() + .filter(|d| d.severity == Severity::Error) + .count(); if mode == OutputMode::Json { println!( @@ -5234,7 +5303,11 @@ fn fix_cmd(source_arg: &str, write: bool, mode: OutputMode) -> i32 { report_diagnostic(d, mode); } - if remaining > 0 || post_had_errors { 1 } else { 0 } + if remaining > 0 || post_had_errors { + 1 + } else { + 0 + } } fn check_cmd( @@ -5386,20 +5459,30 @@ fn check_cmd( // function through the tree interpreter and comparing the result. if !had_errors { for d in &program.declarations { - let ast::Decl::Test { fn_name, assertions, .. } = d else { + let ast::Decl::Test { + fn_name, + assertions, + .. + } = d + else { continue; }; for assertion in assertions { let (afn, args, is_err, expected_lit) = match assertion { - ast::TestAssertion::Ok { fn_name, args, expected, .. } => { - (fn_name, args, false, expected) - } - ast::TestAssertion::Err { fn_name, args, expected_err, .. } => { - (fn_name, args, true, expected_err) - } + ast::TestAssertion::Ok { + fn_name, + args, + expected, + .. + } => (fn_name, args, false, expected), + ast::TestAssertion::Err { + fn_name, + args, + expected_err, + .. + } => (fn_name, args, true, expected_err), }; - let values: Vec = - args.iter().map(literal_to_value).collect(); + let values: Vec = args.iter().map(literal_to_value).collect(); let arg_desc: String = args .iter() .map(format_literal) @@ -5433,7 +5516,9 @@ fn check_cmd( &enrich( Diagnostic::error(format!( "test '{}': expected error ^{} but got {}", - fn_name, format_literal(expected_lit), result + fn_name, + format_literal(expected_lit), + result )) .with_code("ILO-T050"), ), @@ -5778,7 +5863,7 @@ fn dispatch_run( ); } 2 - } else if r.dense { + } else if r.dense { println!( "{}", codegen::fmt::format(&program, codegen::fmt::FmtMode::Dense) @@ -8498,7 +8583,8 @@ mod tests { name: "myfunc".into(), params: vec![], return_type: ast::Type::Number, - effect_set: None, effect_sigils: vec![], + effect_set: None, + effect_sigils: vec![], precondition: None, postcondition: None, body: vec![], @@ -8908,7 +8994,8 @@ mod tests { name: "f".into(), params: vec![], return_type: ast::Type::Number, - effect_set: None, effect_sigils: vec![], + effect_set: None, + effect_sigils: vec![], precondition: None, postcondition: None, body: vec![], @@ -9458,7 +9545,8 @@ mod tests { ast::Expr::Literal(ast::Literal::Number(42.0)), ))], span: ast::Span::UNKNOWN, - effect_set: None, effect_sigils: vec![], + effect_set: None, + effect_sigils: vec![], precondition: None, postcondition: None, }; @@ -9516,7 +9604,8 @@ mod tests { unwrap: ast::UnwrapMode::None, }))], span: ast::Span::UNKNOWN, - effect_set: None, effect_sigils: vec![], + effect_set: None, + effect_sigils: vec![], precondition: None, postcondition: None, }; @@ -10333,11 +10422,7 @@ mod tests { #[test] fn cli_check_failing_shadow_test_exits_nonzero() { let file = std::env::temp_dir().join("ilo_shadow_fail_test.ilo"); - std::fs::write( - &file, - "add a:n b:n>n;+a b\ntest add { ok add 2 3 99 }\n", - ) - .unwrap(); + std::fs::write(&file, "add a:n b:n>n;+a b\ntest add { ok add 2 3 99 }\n").unwrap(); let out = std::process::Command::new(ilo_bin()) .args(["check", file.to_str().unwrap()]) .output() @@ -10365,11 +10450,7 @@ mod tests { #[test] fn cli_check_passing_shadow_test_exits_zero() { let file = std::env::temp_dir().join("ilo_shadow_pass_test.ilo"); - std::fs::write( - &file, - "add a:n b:n>n;+a b\ntest add { ok add 2 3 5 }\n", - ) - .unwrap(); + std::fs::write(&file, "add a:n b:n>n;+a b\ntest add { ok add 2 3 5 }\n").unwrap(); let out = std::process::Command::new(ilo_bin()) .args(["check", file.to_str().unwrap()]) .output() @@ -10913,7 +10994,8 @@ mod tests { ty: Type::Number, }], return_type: Type::Number, - effect_set: None, effect_sigils: vec![], + effect_set: None, + effect_sigils: vec![], precondition: None, postcondition: None, body: vec![], diff --git a/src/tools/http_provider.rs b/src/tools/http_provider.rs index c28a005d..a9ca6209 100644 --- a/src/tools/http_provider.rs +++ b/src/tools/http_provider.rs @@ -3,6 +3,7 @@ use crate::interpreter::Value; use std::collections::HashMap; use std::future::Future; use std::pin::Pin; +#[cfg(feature = "tools")] use std::sync::Arc; #[derive(Debug, Clone, serde::Deserialize)] diff --git a/src/verify.rs b/src/verify.rs index f213f9a8..1d5c5060 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -62,27 +62,23 @@ fn guard_satisfies(guards: &[GuardPattern], precond: &Expr) -> bool { _ => return false, }; match op { - BinOp::NotEquals => { - guards.iter().any(|g| matches!(g, GuardPattern::NotEqual(v, va) if *v == var && *va == val)) - } - BinOp::GreaterOrEqual => { - guards.iter().any(|g| { - matches!(g, GuardPattern::GreaterEq(v, va) if *v == var && *va == val) - || matches!(g, GuardPattern::Greater(v, va) if *v == var && *va == val) - }) - } - BinOp::GreaterThan => { - guards.iter().any(|g| matches!(g, GuardPattern::Greater(v, va) if *v == var && *va == val)) - } - BinOp::LessOrEqual => { - guards.iter().any(|g| { - matches!(g, GuardPattern::LessEq(v, va) if *v == var && *va == val) - || matches!(g, GuardPattern::Less(v, va) if *v == var && *va == val) - }) - } - BinOp::LessThan => { - guards.iter().any(|g| matches!(g, GuardPattern::Less(v, va) if *v == var && *va == val)) - } + BinOp::NotEquals => guards + .iter() + .any(|g| matches!(g, GuardPattern::NotEqual(v, va) if *v == var && *va == val)), + BinOp::GreaterOrEqual => guards.iter().any(|g| { + matches!(g, GuardPattern::GreaterEq(v, va) if *v == var && *va == val) + || matches!(g, GuardPattern::Greater(v, va) if *v == var && *va == val) + }), + BinOp::GreaterThan => guards + .iter() + .any(|g| matches!(g, GuardPattern::Greater(v, va) if *v == var && *va == val)), + BinOp::LessOrEqual => guards.iter().any(|g| { + matches!(g, GuardPattern::LessEq(v, va) if *v == var && *va == val) + || matches!(g, GuardPattern::Less(v, va) if *v == var && *va == val) + }), + BinOp::LessThan => guards + .iter() + .any(|g| matches!(g, GuardPattern::Less(v, va) if *v == var && *va == val)), _ => false, } } @@ -333,7 +329,12 @@ fn collect_stmt_effects( Stmt::Return(expr) => { collect_expr_effects(expr, fn_effects, functions, out); } - Stmt::Guard { condition, body, else_body, .. } => { + Stmt::Guard { + condition, + body, + else_body, + .. + } => { collect_expr_effects(condition, fn_effects, functions, out); for s in body { collect_stmt_effects(&s.node, fn_effects, functions, out); @@ -354,7 +355,9 @@ fn collect_stmt_effects( } } } - Stmt::ForEach { collection, body, .. } => { + Stmt::ForEach { + collection, body, .. + } => { collect_expr_effects(collection, fn_effects, functions, out); for s in body { collect_stmt_effects(&s.node, fn_effects, functions, out); @@ -369,7 +372,13 @@ fn collect_stmt_effects( Stmt::Destructure { value, .. } => { collect_expr_effects(value, fn_effects, functions, out); } - Stmt::ForRange { start, end, step, body, .. } => { + Stmt::ForRange { + start, + end, + step, + body, + .. + } => { collect_expr_effects(start, fn_effects, functions, out); collect_expr_effects(end, fn_effects, functions, out); if let Some(s) = step { @@ -460,7 +469,11 @@ fn collect_expr_effects( collect_expr_effects(v, fn_effects, functions, out); } } - Expr::Ternary { condition, then_expr, else_expr } => { + Expr::Ternary { + condition, + then_expr, + else_expr, + } => { collect_expr_effects(condition, fn_effects, functions, out); collect_expr_effects(then_expr, fn_effects, functions, out); collect_expr_effects(else_expr, fn_effects, functions, out); @@ -5056,7 +5069,7 @@ impl VerifyContext { Decl::Use { .. } => {} // resolved before verify — skip Decl::VersionPragma { .. } => {} // pragma — no verification needed Decl::Error { .. } => {} // poison node — skip silently - Decl::Test { .. } => {} // shadow test block — skip during verify + Decl::Test { .. } => {} // shadow test block — skip during verify } } @@ -5334,13 +5347,25 @@ impl VerifyContext { // Build a map of function name -> declared effect sigils for transitive checks. let mut fn_effects: HashMap> = HashMap::new(); for decl in &program.declarations { - if let Decl::Function { name, effect_sigils, .. } = decl { + if let Decl::Function { + name, + effect_sigils, + .. + } = decl + { fn_effects.insert(name.clone(), effect_sigils.iter().copied().collect()); } } for decl in &program.declarations { - let Decl::Function { name, body, effect_sigils, span, .. } = decl else { + let Decl::Function { + name, + body, + effect_sigils, + span, + .. + } = decl + else { continue; }; if self.parse_failed_fns.contains_key(name) { @@ -5420,7 +5445,12 @@ impl VerifyContext { // Build a map of function name → precondition expression. let mut preconds: HashMap = HashMap::new(); for decl in &program.declarations { - if let Decl::Function { name, precondition: Some(pc), .. } = decl { + if let Decl::Function { + name, + precondition: Some(pc), + .. + } = decl + { preconds.insert(name.clone(), pc); } } @@ -5429,16 +5459,16 @@ impl VerifyContext { } for decl in &program.declarations { - if let Decl::Function { name: caller_name, body, .. } = decl { + if let Decl::Function { + name: caller_name, + body, + .. + } = decl + { if self.parse_failed_fns.contains_key(caller_name) { continue; } - self.check_preconditions_in_stmts( - caller_name, - body, - &preconds, - &[], - ); + self.check_preconditions_in_stmts(caller_name, body, &preconds, &[]); } } } @@ -5455,92 +5485,109 @@ impl VerifyContext { let mut guards: Vec = prior_guards.to_vec(); for spanned in stmts { match &spanned.node { - Stmt::Guard { condition, body: guard_body, braceless: true, .. } => { + Stmt::Guard { + condition, + body: guard_body, + braceless: true, + .. + } => { // Braceless guard: `=b 0 ^"..."` — extracts a guard pattern if let Some(gp) = extract_guard_pattern(condition) { guards.push(gp); } // Also scan the guard body for calls - self.check_preconditions_in_stmts( - caller, guard_body, preconds, &guards, - ); + self.check_preconditions_in_stmts(caller, guard_body, preconds, &guards); } - Stmt::Guard { condition: _, body: guard_body, .. } => { + Stmt::Guard { + condition: _, + body: guard_body, + .. + } => { // Braced guard — condition is conditional, not early-return. // Doesn't establish a guard for subsequent statements. - self.check_preconditions_in_stmts( - caller, guard_body, preconds, &guards, - ); + self.check_preconditions_in_stmts(caller, guard_body, preconds, &guards); } Stmt::Let { value, .. } => { self.check_preconditions_in_expr( - caller, value, spanned.span, preconds, &guards, + caller, + value, + spanned.span, + preconds, + &guards, ); } Stmt::Expr(expr) => { // Bare expression as statement (e.g. return value) - self.check_preconditions_in_expr( - caller, expr, spanned.span, preconds, &guards, - ); + self.check_preconditions_in_expr(caller, expr, spanned.span, preconds, &guards); } Stmt::Return(expr) => { - self.check_preconditions_in_expr( - caller, expr, spanned.span, preconds, &guards, - ); + self.check_preconditions_in_expr(caller, expr, spanned.span, preconds, &guards); } Stmt::Break(Some(expr)) => { - self.check_preconditions_in_expr( - caller, expr, spanned.span, preconds, &guards, - ); + self.check_preconditions_in_expr(caller, expr, spanned.span, preconds, &guards); } Stmt::Destructure { value, .. } => { self.check_preconditions_in_expr( - caller, value, spanned.span, preconds, &guards, + caller, + value, + spanned.span, + preconds, + &guards, ); } Stmt::Match { subject, arms, .. } => { if let Some(s) = subject { self.check_preconditions_in_expr( - caller, s, spanned.span, preconds, &guards, + caller, + s, + spanned.span, + preconds, + &guards, ); } for arm in arms { - self.check_preconditions_in_stmts( - caller, &arm.body, preconds, &guards, - ); + self.check_preconditions_in_stmts(caller, &arm.body, preconds, &guards); } } - Stmt::ForEach { collection, body, .. } => { + Stmt::ForEach { + collection, body, .. + } => { self.check_preconditions_in_expr( - caller, collection, spanned.span, preconds, &guards, - ); - self.check_preconditions_in_stmts( - caller, body, preconds, &guards, + caller, + collection, + spanned.span, + preconds, + &guards, ); + self.check_preconditions_in_stmts(caller, body, preconds, &guards); } - Stmt::ForRange { start, end, body, .. } => { - self.check_preconditions_in_expr( - caller, start, spanned.span, preconds, &guards, - ); + Stmt::ForRange { + start, end, body, .. + } => { self.check_preconditions_in_expr( - caller, end, spanned.span, preconds, &guards, - ); - self.check_preconditions_in_stmts( - caller, body, preconds, &guards, + caller, + start, + spanned.span, + preconds, + &guards, ); + self.check_preconditions_in_expr(caller, end, spanned.span, preconds, &guards); + self.check_preconditions_in_stmts(caller, body, preconds, &guards); } - Stmt::While { condition, body, .. } => { + Stmt::While { + condition, body, .. + } => { self.check_preconditions_in_expr( - caller, condition, spanned.span, preconds, &guards, - ); - self.check_preconditions_in_stmts( - caller, body, preconds, &guards, + caller, + condition, + spanned.span, + preconds, + &guards, ); + self.check_preconditions_in_stmts(caller, body, preconds, &guards); } Stmt::Defer { expr, .. } => { - self.check_preconditions_in_expr( - caller, expr, spanned.span, preconds, &guards, - ); + self.check_preconditions_in_expr(caller, expr, spanned.span, preconds, &guards); } // Stmt::Continue, Stmt::Break(None) — no expressions to check _ => {} @@ -6399,10 +6446,11 @@ impl VerifyContext { '(x:t>t;{name} x)' (paren form) or '{{x> {name} x}}' (brace form)" )) } else { - let base_hint = kebab_subtract_hint(name, candidates.iter()).or_else(|| { - closest_match(name, candidates.iter()) - .map(|s| format!("did you mean '{s}'?")) - }); + let base_hint = + kebab_subtract_hint(name, candidates.iter()).or_else(|| { + closest_match(name, candidates.iter()) + .map(|s| format!("did you mean '{s}'?")) + }); // ILO-504: append hoisting advisory for all undefined // variables. Nested fn declarations are silently // hoisted to siblings in single-line form; if the @@ -9900,7 +9948,8 @@ mod tests { ty: Type::Number, }], return_type: rnt.clone(), - effect_set: None, effect_sigils: vec![], + effect_set: None, + effect_sigils: vec![], precondition: None, postcondition: None, body: vec![Spanned::unknown(Stmt::Expr(Expr::Ok(Box::new(Expr::Ref( @@ -9916,7 +9965,8 @@ mod tests { ty: Type::Number, }], return_type: rnt, - effect_set: None, effect_sigils: vec![], + effect_set: None, + effect_sigils: vec![], precondition: None, postcondition: None, body: vec![ @@ -9962,7 +10012,8 @@ mod tests { ty: Type::Number, }], return_type: Type::Number, - effect_set: None, effect_sigils: vec![], + effect_set: None, + effect_sigils: vec![], precondition: None, postcondition: None, body: vec![Spanned::unknown(Stmt::Expr(Expr::Ref("x".to_string())))], @@ -9976,7 +10027,8 @@ mod tests { ty: Type::Number, }], return_type: Type::Result(Box::new(Type::Number), Box::new(Type::Text)), - effect_set: None, effect_sigils: vec![], + effect_set: None, + effect_sigils: vec![], precondition: None, postcondition: None, body: vec![Spanned::unknown(Stmt::Expr(Expr::Call { @@ -10015,7 +10067,8 @@ mod tests { ty: Type::Number, }], return_type: rnt, - effect_set: None, effect_sigils: vec![], + effect_set: None, + effect_sigils: vec![], precondition: None, postcondition: None, body: vec![Spanned::unknown(Stmt::Expr(Expr::Ok(Box::new(Expr::Ref( @@ -10031,7 +10084,8 @@ mod tests { ty: Type::Number, }], return_type: Type::Number, - effect_set: None, effect_sigils: vec![], + effect_set: None, + effect_sigils: vec![], precondition: None, postcondition: None, body: vec![Spanned::unknown(Stmt::Expr(Expr::Call { @@ -12420,7 +12474,8 @@ mod tests { ty: Type::List(Box::new(Type::Text)), }], return_type: Type::Text, - effect_set: None, effect_sigils: vec![], + effect_set: None, + effect_sigils: vec![], precondition: None, postcondition: None, body: vec![Spanned::unknown(Stmt::Match { @@ -12658,7 +12713,8 @@ mod tests { ty: Type::Number, }], return_type: Type::Number, - effect_set: None, effect_sigils: vec![], + effect_set: None, + effect_sigils: vec![], precondition: None, postcondition: None, body: vec![Spanned::unknown(Stmt::Match { @@ -12705,7 +12761,8 @@ mod tests { name: "f".to_string(), params: vec![], return_type: Type::Any, - effect_set: None, effect_sigils: vec![], + effect_set: None, + effect_sigils: vec![], precondition: None, postcondition: None, body: vec![Spanned::unknown(Stmt::Expr(Expr::Literal(Literal::Nil)))], @@ -12740,7 +12797,8 @@ mod tests { ty: Type::Number, }], return_type: Type::Text, - effect_set: None, effect_sigils: vec![], + effect_set: None, + effect_sigils: vec![], precondition: None, postcondition: None, body: vec![Spanned::unknown(Stmt::Expr(Expr::Ternary { diff --git a/src/vm/compile_cranelift.rs b/src/vm/compile_cranelift.rs index af2d42cf..ebb9a231 100644 --- a/src/vm/compile_cranelift.rs +++ b/src/vm/compile_cranelift.rs @@ -1568,7 +1568,8 @@ fn compile_function_body( builder.switch_to_block(zero_block); let fref = get_func_ref(&mut builder, module, helpers.raise_divzero); - let bv_i64 = builder.ins().bitcast(I64, mf, bf); let call_inst = builder.ins().call(fref, &[bv_i64, span_arg]); + let bv_i64 = builder.ins().bitcast(I64, mf, bf); + let call_inst = builder.ins().call(fref, &[bv_i64, span_arg]); let nil_res = builder.inst_results(call_inst)[0]; builder.ins().jump(merge_block, &[nil_res]); @@ -1598,7 +1599,8 @@ fn compile_function_body( let span_bits = super::jit_cranelift::pack_span_bits(chunk.spans[ip]); let span_arg = builder.ins().iconst(I64, span_bits); let fref = get_func_ref(&mut builder, module, helpers.raise_divzero); - let bv_i64 = builder.ins().bitcast(I64, mf, bf); let call_inst = builder.ins().call(fref, &[bv_i64, span_arg]); + let bv_i64 = builder.ins().bitcast(I64, mf, bf); + let call_inst = builder.ins().call(fref, &[bv_i64, span_arg]); let result = builder.inst_results(call_inst)[0]; builder.def_var(vars[a_idx], result); if a_idx < reg_count && reg_always_num[a_idx] { @@ -1662,7 +1664,8 @@ fn compile_function_body( builder.switch_to_block(zero_block); let fref = get_func_ref(&mut builder, module, helpers.raise_divzero); - let bv_i64 = builder.ins().bitcast(I64, mf, bf); let call_inst = builder.ins().call(fref, &[bv_i64, span_arg]); + let bv_i64 = builder.ins().bitcast(I64, mf, bf); + let call_inst = builder.ins().call(fref, &[bv_i64, span_arg]); let nil_res = builder.inst_results(call_inst)[0]; builder.ins().jump(merge_div, &[nil_res]); @@ -1737,7 +1740,8 @@ fn compile_function_body( builder.switch_to_block(zero_block_div); let fref = get_func_ref(&mut builder, module, helpers.raise_divzero); - let bv_i64 = builder.ins().bitcast(I64, mf, bf); let call_inst = builder.ins().call(fref, &[bv_i64, span_arg]); + let bv_i64 = builder.ins().bitcast(I64, mf, bf); + let call_inst = builder.ins().call(fref, &[bv_i64, span_arg]); let nil_res = builder.inst_results(call_inst)[0]; builder.ins().jump(merge_block, &[nil_res]); diff --git a/src/vm/jit_cranelift.rs b/src/vm/jit_cranelift.rs index d8817ad5..32c0b1b6 100644 --- a/src/vm/jit_cranelift.rs +++ b/src/vm/jit_cranelift.rs @@ -1493,7 +1493,8 @@ fn compile_function_body( builder.switch_to_block(zero_block); let fref = get_func_ref(&mut builder, module, helpers.raise_divzero); - let bv_i64 = builder.ins().bitcast(I64, mf, bf); let call_inst = builder.ins().call(fref, &[bv_i64, span_arg]); + let bv_i64 = builder.ins().bitcast(I64, mf, bf); + let call_inst = builder.ins().call(fref, &[bv_i64, span_arg]); let nil_res = builder.inst_results(call_inst)[0]; builder.ins().jump(merge_block, &[nil_res]); @@ -1581,7 +1582,8 @@ fn compile_function_body( let span_bits = pack_span_bits(chunk.spans[ip]); let span_arg = builder.ins().iconst(I64, span_bits); let fref = get_func_ref(&mut builder, module, helpers.raise_divzero); - let bv_i64 = builder.ins().bitcast(I64, mf, bf); let call_inst = builder.ins().call(fref, &[bv_i64, span_arg]); + let bv_i64 = builder.ins().bitcast(I64, mf, bf); + let call_inst = builder.ins().call(fref, &[bv_i64, span_arg]); let result = builder.inst_results(call_inst)[0]; builder.def_var(vars[a_idx], result); if a_idx < reg_count && reg_always_num[a_idx] { @@ -1645,7 +1647,8 @@ fn compile_function_body( builder.switch_to_block(zero_block); let fref = get_func_ref(&mut builder, module, helpers.raise_divzero); - let bv_i64 = builder.ins().bitcast(I64, mf, bf); let call_inst = builder.ins().call(fref, &[bv_i64, span_arg]); + let bv_i64 = builder.ins().bitcast(I64, mf, bf); + let call_inst = builder.ins().call(fref, &[bv_i64, span_arg]); let nil_res = builder.inst_results(call_inst)[0]; builder.ins().jump(merge_div, &[nil_res]); @@ -1722,7 +1725,8 @@ fn compile_function_body( builder.switch_to_block(zero_block_div); let fref = get_func_ref(&mut builder, module, helpers.raise_divzero); - let bv_i64 = builder.ins().bitcast(I64, mf, bf); let call_inst = builder.ins().call(fref, &[bv_i64, span_arg]); + let bv_i64 = builder.ins().bitcast(I64, mf, bf); + let call_inst = builder.ins().call(fref, &[bv_i64, span_arg]); let nil_res = builder.inst_results(call_inst)[0]; builder.ins().jump(merge_block, &[nil_res]); diff --git a/src/vm/mod.rs b/src/vm/mod.rs index 9aec0ea9..6086549a 100644 --- a/src/vm/mod.rs +++ b/src/vm/mod.rs @@ -2552,7 +2552,10 @@ impl RegCompiler { is_defer_fn.push(body_has_defer(body)); } Decl::Tool { - name, return_type, policy, .. + name, + return_type, + policy, + .. } => { self.func_names.push(name.clone()); self.func_return_types.push(return_type.clone()); @@ -9507,7 +9510,10 @@ impl<'a> VM<'a> { if bv.is_number() && cv.is_number() { let dv = cv.as_number(); if dv == 0.0 { - vm_err!(VmError::DivisionByZero { dividend: bv.as_number(), divisor: dv }); + vm_err!(VmError::DivisionByZero { + dividend: bv.as_number(), + divisor: dv + }); } reg_set!(a, NanVal::number(bv.as_number() / dv)); } else { @@ -11001,9 +11007,8 @@ impl<'a> VM<'a> { if let Some(Some(pol)) = self.program.tool_policies.get(func_idx as usize) { if let Some(Value::Text(url)) = value_args.first() { if let Err(msg) = pol.check_domain(url) { - let result = Value::Err(Box::new(Value::Text( - std::sync::Arc::new(msg) - ))); + let result = + Value::Err(Box::new(Value::Text(std::sync::Arc::new(msg)))); let nan_result = NanVal::from_value(&result); reg_set!(base + a as usize, nan_result); continue; @@ -11017,7 +11022,9 @@ impl<'a> VM<'a> { if let Some(rt) = self.tokio_runtime { rt.block_on(_provider.call(_tool_name, value_args)) .unwrap_or_else(|e| { - Value::Err(Box::new(Value::Text(Arc::new(e.to_string())))) + Value::Err(Box::new(Value::Text(Arc::new( + e.to_string(), + )))) }) } else { let _ = value_args; @@ -11736,7 +11743,10 @@ impl<'a> VM<'a> { let kv = unsafe { *nan_consts.get_unchecked(c) }; let dv = kv.as_number(); if dv == 0.0 { - vm_err!(VmError::DivisionByZero { dividend: reg!(b).as_number(), divisor: dv }); + vm_err!(VmError::DivisionByZero { + dividend: reg!(b).as_number(), + divisor: dv + }); } let result = NanVal::number(reg!(b).as_number() / dv); unsafe { @@ -11789,7 +11799,10 @@ impl<'a> VM<'a> { // SAFETY: same as OP_SUB_NN. let dv = reg!(c).as_number(); if dv == 0.0 { - vm_err!(VmError::DivisionByZero { dividend: reg!(b).as_number(), divisor: dv }); + vm_err!(VmError::DivisionByZero { + dividend: reg!(b).as_number(), + divisor: dv + }); } let result = NanVal::number(reg!(b).as_number() / dv); unsafe { @@ -16047,8 +16060,18 @@ pub(crate) extern "C" fn jit_mul(a: u64, b: u64, span_bits: u64) -> u64 { #[unsafe(no_mangle)] pub(crate) extern "C" fn jit_raise_divzero(dividend_boxed: u64, span_bits: u64) -> u64 { let dv = NanVal(dividend_boxed); - let dividend = if dv.is_number() { dv.as_number() } else { f64::NAN }; - jit_set_runtime_error_with_span(VmError::DivisionByZero { dividend, divisor: 0.0 }, span_bits); + let dividend = if dv.is_number() { + dv.as_number() + } else { + f64::NAN + }; + jit_set_runtime_error_with_span( + VmError::DivisionByZero { + dividend, + divisor: 0.0, + }, + span_bits, + ); TAG_NIL } @@ -16060,7 +16083,13 @@ pub(crate) extern "C" fn jit_div(a: u64, b: u64, span_bits: u64) -> u64 { if av.is_number() && bv.is_number() { let dv = bv.as_number(); if dv == 0.0 { - jit_set_runtime_error_with_span(VmError::DivisionByZero { dividend: av.as_number(), divisor: dv }, span_bits); + jit_set_runtime_error_with_span( + VmError::DivisionByZero { + dividend: av.as_number(), + divisor: dv, + }, + span_bits, + ); return TAG_NIL; } return NanVal::number(av.as_number() / dv).0; @@ -24291,7 +24320,8 @@ mod tests { params, body: vec![], return_type: Type::Number, - effect_set: None, effect_sigils: vec![], + effect_set: None, + effect_sigils: vec![], precondition: None, postcondition: None, span: Span::UNKNOWN, @@ -36331,7 +36361,7 @@ f>n;r=mk 10 20;+r.x r.y"; is_defer_fn: vec![false], ast: None, defer_fns: std::collections::HashSet::new(), - tool_policies: Vec::new(), + tool_policies: Vec::new(), }; let result = run(&program, Some("f"), vec![]).expect("math op on nil should not error"); match result { From d81c3b7005cf6d2edacccf493347171edf8b4dda Mon Sep 17 00:00:00 2001 From: Daniel Morris Date: Sun, 9 Aug 2026 01:15:27 +0100 Subject: [PATCH 11/11] docs: paren-form trailing operands in SPEC + changelog for the branch fixes --- CHANGELOG.md | 8 ++++++++ SPEC.md | 12 ++++++++++++ 2 files changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95eca4fd..8a9d4840 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -92,6 +92,14 @@ against Phase 6. ### Fixed +- **Paren-form calls accept trailing operands (ILO-544).** A paren group committed to being the whole argument list, so `fmt2(x)2` AND `fmt2(x) 2` both died with ILO-P001 while `fmt2(x, 2)` and `fmt2 (x) 2` worked — and models emit the glued shape constantly (pipeline-report failed 5/5 on it in the ILO-364 N=5 benchmark, ~1000 wasted repair tokens per failure; the single biggest contributor to ilo's 40% task-failure rate vs Python's 0%). At expression head, a completed adjacent-paren call now keeps collecting trailing operands with the same greedy loop the spaced postfix form uses, so all four spellings mean the same call. A field/index chain (`f(x).0`) closes the list. Mechanism: a `paren_call_atom` flag set by the atom parser, consumed with `mem::take` at expression head only — operand positions route elsewhere and are untouched, so nested calls and complete calls keep their exact prior parses. Cross-engine regression tests in `tests/regression_paren_form_trailing_args.rs`. + +- **Shadow-test declarations survive script mode.** When script mode (main) was merged into this branch, `is_decl_start` knew `alias` but not `test`, so `test add { ok add 2 3 5 }` fell into script-statement collection and died as an undefined call — `ilo check` on any file with shadow tests exited 1 and both `cli_check_*_shadow_test` unit tests failed on the branch tip. `test` now routes to `parse_decl` beside `alias`. + +- **ai.txt bootstrap index no longer clobbered by builds (ILO-538 completion).** The index shipped without disabling build.rs's SPEC.md→ai.txt regeneration, so any `cargo build` overwrote the 2K index with the ~180KB monolith and CI's sync check failed on the index's own lineage. Regeneration and `compact_spec()` are removed; `cargo:rerun-if-changed=ai.txt` keeps the `include_str!` embed current; the CI diff step now guards against accidental clobber. The index's SPACING line also taught the pre-ILO-544 workaround as advice — updated to the new contract. + +- **AOT byte-identity corpus recaptured post-backend-rework.** The `.ilo`→`.@` example rename left `aot_byte_identical` reporting all 136 baselines "missing", which masked that the typed-HIR/backend rework had (intentionally) changed every object byte. The test now resolves `.@` with `.ilo` fallback, and the corpus is recaptured. A 3-example capture on the unmodified branch tip produced byte-identical hashes to this branch's, attributing the drift entirely to the backend rework and none to the parser changes here. + - **CLI arguments are type-checked against the entry function's parameters (ILO-517).** A shell string that didn't match its declared parameter type used to be bound as-is, because `parse_cli_arg`'s ladder falls through to `Text` for anything non-numeric. So `tri n:n>n` invoked as `ilo tri.@ main` bound `Value::Text("main")` to a `n` parameter: the tree-walker and VM printed `NaN`, the Cranelift JIT echoed `main`, and every engine exited **0** — a silent wrong answer with a success exit code, which is worse than a crash for any caller that checks `$?`. The CLI was therefore less safe than the language it fronts, where `num "main"` returns `R n t` and the type checker forces the failure to be handled. A new CLI-boundary guard (`check_cli_arg_types`, sibling of the existing `check_cli_arity`) now rejects unambiguously mismatched values with `ILO-R600`, naming the parameter and showing the offending literal, and exits 1. Wired into all four dispatch sites (VM, interpreter, JIT, default) so the error contract can't drift per engine the way ILO-177 did. The guard is deliberately permissive — `_` accepts anything, `O T` still accepts `nil`, and structural / user-defined types are waved through — so the ILO-182 single-fn pass-through keeps working (`ilo greet.@ world` against `s:t` is legitimate usage, not a typo'd function name). Also closes the same hole for `b` parameters. Found while running the ILO-364 closed-loop benchmark, where it cost a model retries on an otherwise-correct program. - **`get-stream` yields lines on newline, not buffer-fill (ILO-489).** The client-side streaming builtins (`get-stream`, `get-stream-h`, `pst-stream`, `pst-stream-h`, ILO-448) wrapped minreq's `ResponseLazy` in `BufReader::lines()`, which blocks on a full ~8 KiB read-buffer fill before surfacing any line. So a slow SSE upstream that flushes one short event then idles had its lines batched until the buffer filled or the connection closed - functionally correct but latency was buffer-bound, not event-bound. The line splitter now consumes `ResponseLazy`'s byte iterator incrementally and emits each line the instant its `\n` arrives (trailing `\r` stripped for CRLF / chunked encoding), so each event surfaces promptly. EOF still yields a trailing newline-less partial line; mid-stream errors still surface as `ILO-R009 http-stream read error: ...`. Unblocks ILO-482's previously flaky end-to-end streaming test. diff --git a/SPEC.md b/SPEC.md index 9fd3ad36..340dbe37 100644 --- a/SPEC.md +++ b/SPEC.md @@ -2044,6 +2044,18 @@ f(g(x), h(y)) -- nested paren-calls also work **Trailing commas are accepted:** `f(a, b,)` is valid (Rust/JS convention). +**Trailing operands extend the call (ILO-544).** At expression head, a +paren group need not be the whole argument list — operands after the `)` +are collected exactly as the spaced postfix form would collect them, so +all four spellings mean the same 2-arg call: + +``` +fmt2(x)2 fmt2(x) 2 fmt2(x, 2) fmt2 (x) 2 +``` + +A field/index chain closes the list: `f(x).0` is a value, and an operand +after it is an error as before. + **Postfix stays canonical.** `ilo fmt` does not rewrite paren-form to postfix; both styles are accepted everywhere. ### Call Arguments