fix(check): render restricted-set diagnostic in AILang set syntax (#52)

The `param-not-in-restricted-set` diagnostic rendered its allowed
element set with `{allowed:?}` — a Rust `{:?}` debug print of the
`Vec<String>`, 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
This commit is contained in:
2026-06-01 20:29:00 +02:00
parent 9848d4b2df
commit d8be95dd44
+52 -1
View File
@@ -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