fix(check): param-not-in-restricted-set names the allowed set (closes #48)

The ParamNotInRestrictedSet diagnostic named the offending type, the
type-variable and the home type, but not the allowed set {Int, Float,
Bool} that design/models/0007 §4 promises it names. A downstream author
was told what is wrong but not what is right, and the kernel source that
carries the set is not available to a downstream consumer.

The allowed set already travels on the error struct (it flows into the
JSON `details.allowed`); only the human-readable `#[error]` render
dropped it. Append it to the Display string. JSON details unchanged.

Surfaced by the raw-buf fieldtest (finding B3, docs/specs/0058).
RED-first: param_in_reject_message_names_allowed_set pins the message
content before the one-line render fix.
This commit is contained in:
2026-05-30 17:13:40 +02:00
parent 8e9f0f06a6
commit 75109f171d
+46 -1
View File
@@ -747,7 +747,7 @@ 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}`"
"type-arg `{found}` is not in the restricted set for type-variable `{var}` of `{type_name}` (allowed: one of {allowed:?})"
)]
ParamNotInRestrictedSet {
type_name: String,
@@ -8221,6 +8221,51 @@ mod tests {
);
}
/// fieldtest B3 (raw-buf, #7): the rendered `param-not-in-restricted-set`
/// message must name the *allowed set*, not only the offending type.
/// Property per design/models/0007-kernel-extensions.md §4
/// ("ParamNotInRestrictedSet names the offending type AND the allowed
/// set"): a downstream author rejected for `(con StubT (con Str))`
/// under `param-in = {"a": {"Int", "Float"}}` must be told `Int` and
/// `Float` are the permitted choices, so the message is actionable.
#[test]
fn param_in_reject_message_names_allowed_set() {
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("Int") && d.message.contains("Float"),
"message must name the allowed set {{Int, Float}}, 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