From d8be95dd44c9a6a213226bb884bb166f608c7b93 Mon Sep 17 00:00:00 2001 From: Brummel Date: Mon, 1 Jun 2026 20:29:00 +0200 Subject: [PATCH] fix(check): render restricted-set diagnostic in AILang set syntax (#52) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `param-not-in-restricted-set` diagnostic rendered its allowed element set with `{allowed:?}` — a Rust `{:?}` debug print of the `Vec`, producing `["Bool", "Float", "Int"]` with quotes, brackets, and Rust list shape. AILang's audience is an LLM author, so a language-level diagnostic leaking Rust formatting into its surface is a quality defect. Fix: render the set as `{Bool, Float, Int}` via a thiserror trailing-arg `.allowed.join(", ")` wrapped in literal braces. The set is alphabetical and stable (unchanged — the source is a sorted BTreeSet projection). Design decision (set notation, not a union): the allowed set is a membership set ("one of"), and AILang has no union-type surface syntax, so brace set-notation `{A, B, C}` reads naturally without implying a type-level union the language does not have. The structured JSON detail (CheckError::detail, the `allowed` array) is left untouched — it is machine-readable and correctly an array, not the cosmetic surface. RED-first, inline: the bug is a one-line Display-string change at a single site (lib.rs ~760). The exact lines were already loaded while assessing the /boss queue, so a debugger subagent would only redo the same reading (CLAUDE.md "When NOT to delegate: context already loaded"). TDD discipline is preserved by the new RED test `param_in_reject_message_renders_allowed_set_in_ailang_syntax`, which asserts the AILang set form and the absence of Rust quotes/brackets; it failed RED on the old `{allowed:?}` and passes GREEN on the fix. The pre-existing pin `param_in_reject_message_names_allowed_set` only asserted substring presence, so it did not pin (or block) the format. Verification: full ailang-check suite green (121 lib tests + integration suites); live CLI repro from the issue now renders `(allowed: one of {Bool, Float, Int})`. closes #52 --- crates/ailang-check/src/lib.rs | 53 +++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/crates/ailang-check/src/lib.rs b/crates/ailang-check/src/lib.rs index 3a7f69a..181d93c 100644 --- a/crates/ailang-check/src/lib.rs +++ b/crates/ailang-check/src/lib.rs @@ -757,7 +757,8 @@ pub enum CheckError { /// (`allowed`, deterministically alphabetical from the BTreeSet /// source). Code: `param-not-in-restricted-set`. #[error( - "type-arg `{found}` is not in the restricted set for type-variable `{var}` of `{type_name}` (allowed: one of {allowed:?})" + "type-arg `{found}` is not in the restricted set for type-variable `{var}` of `{type_name}` (allowed: one of {{{}}})", + .allowed.join(", ") )] ParamNotInRestrictedSet { type_name: String, @@ -8527,6 +8528,56 @@ mod tests { ); } + /// #52: the allowed set in `param-not-in-restricted-set` must render in + /// AILang type syntax — a brace-wrapped, comma-separated set of bare + /// type names (`{Float, Int}`) — not a Rust debug list + /// (`["Float", "Int"]`). AILang's audience is an LLM author; a + /// language-level diagnostic must not leak Rust formatting (quotes, + /// brackets) into its surface. Order stays deterministic (alphabetical, + /// from the BTreeSet source). + #[test] + fn param_in_reject_message_renders_allowed_set_in_ailang_syntax() { + let m: Module = serde_json::from_value(serde_json::json!({ + "schema": SCHEMA, + "name": "k", + "defs": [ + {"kind": "type", "name": "StubT", "vars": ["a"], + "ctors": [{"name": "Stub", "fields": [{"k": "var", "name": "a"}]}], + "param-in": {"a": ["Int", "Float"]}}, + {"kind": "fn", "name": "bad", + "type": {"k": "fn", "params": [], + "ret": {"k": "con", "name": "StubT", + "args": [{"k": "con", "name": "Str"}]}, + "effects": []}, + "params": [], + "body": {"t": "lit", "lit": {"kind": "unit"}}} + ] + })).unwrap(); + let mut modules = BTreeMap::new(); + modules.insert("k".to_string(), m); + let ws = Workspace { + entry: "k".into(), + modules, + root_dir: std::path::PathBuf::from("."), + registry: ailang_core::workspace::Registry::default(), + }; + let diags = check_workspace(&ws); + let d = diags + .iter() + .find(|d| d.code == "param-not-in-restricted-set") + .unwrap_or_else(|| panic!("expected param-not-in-restricted-set, got {diags:?}")); + assert!( + d.message.contains("{Float, Int}"), + "allowed set must render as the AILang set `{{Float, Int}}`, got: {:?}", + d.message + ); + assert!( + !d.message.contains('"') && !d.message.contains('['), + "allowed set must not leak Rust debug formatting (quotes/brackets), got: {:?}", + d.message + ); + } + /// prep.3 (kernel-extension-mechanics): `param-in` enforcement /// accepts a type-arg inside the allowed set. Same TypeDef as /// the RED test, but `(con StubT (con Int))` — Int is in