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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user