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
|
/// (`allowed`, deterministically alphabetical from the BTreeSet
|
||||||
/// source). Code: `param-not-in-restricted-set`.
|
/// source). Code: `param-not-in-restricted-set`.
|
||||||
#[error(
|
#[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 {
|
ParamNotInRestrictedSet {
|
||||||
type_name: String,
|
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
|
/// prep.3 (kernel-extension-mechanics): `param-in` enforcement
|
||||||
/// accepts a type-arg inside the allowed set. Same TypeDef as
|
/// accepts a type-arg inside the allowed set. Same TypeDef as
|
||||||
/// the RED test, but `(con StubT (con Int))` — Int is in
|
/// the RED test, but `(con StubT (con Int))` — Int is in
|
||||||
|
|||||||
Reference in New Issue
Block a user