test: prune duplicate tests, re-sight two blind coverage guards

Follow-up to a fan-out audit of the whole test suite (742 tests, 21
read-only assessors + synthesis). Two clusters of the audit were
actioned here; the #66 hashing-removal cohort it surfaced is left for
the #66 scope, and the RED-first doc-rot sweep is deferred.

§2 — true duplicates removed (each kept test is a strict superset or
identical fixture+assertions of the deleted one):
- ailang-check param_in_reject_message_names_allowed_set (kept the
  _renders_allowed_set_in_ailang_syntax superset)
- ailang-check cross_module_pat_ctor_typedriven_resolves (kept ct2_;
  shared cross_module_ws helper retained, 2 other callers)
- ailang-core mq1_qualified_instancedef_class_accepted (kept ct1_)
- ail e2e bool_to_str_emits_true_branch (kept the rc-stats superset)
- ail loop_recur_str adt-leg guard (the standalone heap pin owns it;
  dropped the dangling header reference too)
- ailang-check over_strict_mode_silent_when_param_is_borrow (its
  assertion is subsumed by explicit_fn_with_no_uses_is_clean; its own
  doc claimed a "borrowing body" the Lit body never provided)

§4 — meta-guards that had gone partially blind re-sighted:
- spec_drift::spec_mentions_every_def_kind now pushes Class/Instance
  exemplars and asserts their `(class `/`(instance` anchors; the
  "wait for 22b.4" exclusion was stale (anchors shipped in e809f45).
- schema_coverage now tracks Term::New (VariantTag + EXPECTED +
  visitor insert); the new_*/raw_buf_* fixtures already exercise it,
  so the "no fixture emits new" exclusion was stale.
- prose doc_wrap_widow_control_skips_when_combined_too_long was
  tautological: its 32/32/42-char words gave combined=75<=80, so
  widow control actually fired. Rebuilt the fixture (short head + two
  40-char words → combined=81>80) so it hits the real skip branch,
  and added the missing "last line stays 1 word" assertion. The
  obvious 2-word fixture does NOT work — it skips via the
  prev_words<2 branch, not the combined-over-budget branch.

ailang-check 124->121, ailang-core 55->54, e2e 102->101,
loop_recur_str 4->3; spec_drift 8 green, schema_coverage green,
prose lib 63 green.
This commit is contained in:
2026-06-02 16:59:43 +02:00
parent 426ce88d2a
commit 72d2d9c806
8 changed files with 43 additions and 203 deletions
-10
View File
@@ -3039,16 +3039,6 @@ fn bool_to_str_drop_balances_rc_stats() {
assert_eq!(live, 0, "no leaked heap-Str slabs allowed; live={live}"); assert_eq!(live, 0, "no leaked heap-Str slabs allowed; live={live}");
} }
/// stdout-smoke for the true branch — same fixture as
/// `bool_to_str_drop_balances_rc_stats` but checked here without
/// the RC-stats overhead. Pins the byte content of `ailang_bool_to_str`'s
/// "true" slab.
#[test]
fn bool_to_str_emits_true_branch() {
let out = build_and_run("bool_to_str_drop_rc.ail");
assert_eq!(out, "true\n");
}
/// stdout-smoke for the false branch. Pins the byte /// stdout-smoke for the false branch. Pins the byte
/// content of `ailang_bool_to_str`'s "false" slab. /// content of `ailang_bool_to_str`'s "false" slab.
#[test] #[test]
@@ -31,10 +31,6 @@
//! `ailang_rc_stats: allocs=4 frees=1 live=3` — the three superseded Str //! `ailang_rc_stats: allocs=4 frees=1 live=3` — the three superseded Str
//! slabs (seed "x" plus two intermediate concat results) leak while //! slabs (seed "x" plus two intermediate concat results) leak while
//! stdout is already correct (`xyyy`). //! stdout is already correct (`xyyy`).
//!
//! This file also re-pins the boxed-ADT leg (`alloc_rc_adt_*`) as a
//! GREEN guard: the f488d31 fix must stay green while the Str leg is
//! closed — fixing Str must not regress the ADT case.
use std::path::Path; use std::path::Path;
use std::process::Command; use std::process::Command;
@@ -131,28 +127,6 @@ fn alloc_rc_str_loop_binder_replaced_by_recur_does_not_leak() {
); );
} }
/// GREEN guard: the boxed-ADT leg fixed in f488d31 must stay leak-clean
/// while the Str leg is closed. Fixing Str must not regress this.
#[test]
fn alloc_rc_adt_loop_binder_replaced_by_recur_stays_leak_clean() {
let (allocs, frees, live, stdout) =
build_run_stats("loop_recur_heap_binder_no_leak_pin.ail");
assert_eq!(
stdout.trim_end(),
"2",
"ADT loop result mis-built (allocs={allocs} frees={frees} live={live})"
);
assert_eq!(
live, 0,
"the f488d31 boxed-ADT recur-dec regressed: live={live} \
(allocs={allocs} frees={frees})"
);
assert_eq!(
allocs, frees,
"ADT leg alloc/free mismatch (allocs={allocs} frees={frees} live={live})"
);
}
/// mir.4 soundness, recur-arg leg: a Str loop binder whose `recur` /// mir.4 soundness, recur-arg leg: a Str loop binder whose `recur`
/// rebinds it to a fresh static LITERAL each iteration. The seed-only /// rebinds it to a fresh static LITERAL each iteration. The seed-only
/// promotion would miss this — the recur literal "reset" must also be /// promotion would miss this — the recur literal "reset" must also be
-87
View File
@@ -6366,48 +6366,6 @@ mod tests {
assert!(diags.is_empty(), "expected green; got {diags:?}"); assert!(diags.is_empty(), "expected green; got {diags:?}");
} }
/// a bare `Pattern::Ctor` (e.g. `MkBox x`)
/// against a scrutinee of type `lib.Box<Int>` resolves through
/// the type-driven lookup keyed on `expected.name`. Previously
/// this exercised an imports-fallback; post-canonical-type-form the lookup is
/// anchored to the scrutinee's qualified TypeDef directly.
#[test]
fn cross_module_pat_ctor_typedriven_resolves() {
let consumer = Module {
schema: SCHEMA.into(),
name: "use_lib".into(),
kernel: false,
imports: vec![Import { module: "lib".into(), alias: None }],
defs: vec![fn_def(
"open",
Type::Fn {
params: vec![Type::Con {
name: "lib.Box".into(),
args: vec![Type::int()],
}],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Own,
},
vec!["b"],
Term::Match {
scrutinee: Box::new(Term::Var { name: "b".into() }),
arms: vec![Arm {
pat: Pattern::Ctor {
ctor: "MkBox".into(),
fields: vec![Pattern::Var { name: "x".into() }],
},
body: Term::Var { name: "x".into() },
}],
},
)],
};
let ws = cross_module_ws(consumer);
let diags = check_workspace(&ws);
assert!(diags.is_empty(), "expected green; got {diags:?}");
}
/// a recursive cross-module ADT (`std_list.List a` with a /// a recursive cross-module ADT (`std_list.List a` with a
/// `Cons a (List a)` ctor) round-trips through both `Term::Ctor` synth /// `Cons a (List a)` ctor) round-trips through both `Term::Ctor` synth
/// and pattern-ctor binding without unqualified-field-name unification /// and pattern-ctor binding without unqualified-field-name unification
@@ -8527,51 +8485,6 @@ 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": [], "param_modes": [],
"ret": {"k": "con", "name": "StubT",
"args": [{"k": "con", "name": "Str"}]}, "ret_mode": "own",
"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
);
}
/// #52: the allowed set in `param-not-in-restricted-set` must render in /// #52: the allowed set in `param-not-in-restricted-set` must render in
/// AILang type syntax — a brace-wrapped, comma-separated set of bare /// AILang type syntax — a brace-wrapped, comma-separated set of bare
/// type names (`{Float, Int}`) — not a Rust debug list /// type names (`{Float, Int}`) — not a Rust debug list
-24
View File
@@ -2104,30 +2104,6 @@ mod tests {
); );
} }
/// a fn with `(borrow T)` and a borrowing
/// body never fires `over-strict-mode` (the lint is gated on
/// `Own`).
#[test]
fn over_strict_mode_silent_when_param_is_borrow() {
// Body shape: literal — no use of the param at all.
let m = Module {
schema: ailang_core::SCHEMA.into(),
name: "t".into(),
kernel: false,
imports: vec![],
defs: vec![fn_with_modes(
"f",
vec![ParamMode::Borrow],
Term::Lit { lit: Literal::Int { value: 0 } },
)],
};
let diags = check_module(&m);
assert!(
!diags.iter().any(|d| d.code == "over-strict-mode"),
"no over-strict-mode expected for borrow-mode param; got {diags:?}"
);
}
/// a fn with `(own T)` whose body is /// a fn with `(own T)` whose body is
/// `(match p0 ...)` and never consumes `p0` directly nor any of /// `(match p0 ...)` and never consumes `p0` directly nor any of
/// `p0`'s sub-binders DOES fire `over-strict-mode` under the /// `p0`'s sub-binders DOES fire `over-strict-mode` under the
-34
View File
@@ -2638,40 +2638,6 @@ mod tests {
} }
} }
/// a qualified `InstanceDef.class` referencing a class in
/// an imported module is the canonical form post-canonical-class-form and is
/// accepted by `validate_canonical_type_names`. Symmetric to the canonical-form rule's
/// "qualified Type::Con resolves" positive path.
#[test]
fn mq1_qualified_instancedef_class_accepted() {
let a: Module = serde_json::from_value(serde_json::json!({
"schema": crate::SCHEMA,
"name": "A",
"imports": [],
"defs": [{
"kind": "class",
"name": "Show",
"param": "a",
"methods": []
}]
})).unwrap();
let b: Module = serde_json::from_value(serde_json::json!({
"schema": crate::SCHEMA,
"name": "B",
"imports": [{ "module": "A" }],
"defs": [{
"kind": "instance",
"class": "A.Show",
"type": { "k": "con", "name": "Int" },
"methods": []
}]
})).unwrap();
let mut modules = BTreeMap::new();
modules.insert("A".to_string(), a);
modules.insert("B".to_string(), b);
validate_canonical_type_names(&modules, &["prelude"]).expect("qualified A.Show must be accepted");
}
/// pd.1 Task 1: `load_modules_with` returns modules without injecting /// pd.1 Task 1: `load_modules_with` returns modules without injecting
/// prelude. The prelude inject is the caller's responsibility (the /// prelude. The prelude inject is the caller's responsibility (the
/// shim `load_workspace_with` does it in pd.1; surface owns it from /// shim `load_workspace_with` does it in pd.1; surface owns it from
+5 -7
View File
@@ -50,6 +50,7 @@ enum VariantTag {
TermReuseAs, TermReuseAs,
TermLoop, TermLoop,
TermRecur, TermRecur,
TermNew,
TermIntrinsic, TermIntrinsic,
// Pattern // Pattern
PatternWild, PatternWild,
@@ -97,6 +98,7 @@ const EXPECTED_VARIANTS: &[VariantTag] = &[
VariantTag::TermReuseAs, VariantTag::TermReuseAs,
VariantTag::TermLoop, VariantTag::TermLoop,
VariantTag::TermRecur, VariantTag::TermRecur,
VariantTag::TermNew,
VariantTag::TermIntrinsic, VariantTag::TermIntrinsic,
VariantTag::PatternWild, VariantTag::PatternWild,
VariantTag::PatternVar, VariantTag::PatternVar,
@@ -248,13 +250,9 @@ fn visit_term(t: &Term, observed: &mut HashSet<VariantTag>) {
} }
} }
Term::New { args, .. } => { Term::New { args, .. } => {
// prep.2 (kernel-extension-mechanics): functional construction. // Functional construction. Exercised by the `new_*` /
// The variant is intentionally NOT added to `VariantTag` / // `raw_buf_*` fixtures under examples/.
// `EXPECTED_VARIANTS` in prep.2 — no .ail fixture in the observed.insert(VariantTag::TermNew);
// current corpus emits `"t": "new"` (per the iter's
// out-of-scope note), so an EXPECTED entry would fail the
// coverage check. A future milestone that ships a Term::New
// fixture extends both the enum and the expected list.
for arg in args { for arg in args {
match arg { match arg {
NewArg::Type(t) => visit_type(t, observed), NewArg::Type(t) => visit_type(t, observed),
+19 -7
View File
@@ -14,7 +14,8 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use ailang_core::ast::{ use ailang_core::ast::{
ConstDef, Ctor, Def, FnDef, Literal, NewArg, Pattern, Suppress, Term, Type, TypeDef, ClassDef, ConstDef, Ctor, Def, FnDef, InstanceDef, Literal, NewArg, Pattern, Suppress, Term,
Type, TypeDef,
}; };
use ailang_core::FORM_A_SPEC; use ailang_core::FORM_A_SPEC;
@@ -293,10 +294,27 @@ fn spec_mentions_every_def_kind() {
drop_iterative: false, drop_iterative: false,
param_in: BTreeMap::new(), param_in: BTreeMap::new(),
}; };
let class_def = ClassDef {
name: "C".into(),
param: "a".into(),
superclass: None,
methods: vec![],
doc: None,
};
let instance_def = InstanceDef {
class: "C".into(),
type_: Type::int(),
methods: vec![],
doc: None,
};
let exemplars: Vec<(&str, Def)> = vec![ let exemplars: Vec<(&str, Def)> = vec![
("(fn ", Def::Fn(fn_def)), ("(fn ", Def::Fn(fn_def)),
("(const ", Def::Const(const_def)), ("(const ", Def::Const(const_def)),
("(data ", Def::Type(type_def)), ("(data ", Def::Type(type_def)),
// class/instance gained prose projection in e809f45 (form-a.tidy):
// FORM_A_SPEC now carries `(class ` / `(instance ` anchors.
("(class ", Def::Class(class_def)),
("(instance", Def::Instance(instance_def)),
]; ];
for (anchor, def) in exemplars { for (anchor, def) in exemplars {
@@ -304,12 +322,6 @@ fn spec_mentions_every_def_kind() {
Def::Fn(_) => "fn", Def::Fn(_) => "fn",
Def::Const(_) => "const", Def::Const(_) => "const",
Def::Type(_) => "type", Def::Type(_) => "type",
// class/instance Def variants exist but are
// not yet anchored in the prose-spec block. Once 22b.4
// adds prose projection for them, the FORM_A_SPEC text
// gains `(class ` / `(instance ` anchors and this match
// will be exercised. For 22b.1 the exemplars list above
// does not produce these variants.
Def::Class(_) => "class", Def::Class(_) => "class",
Def::Instance(_) => "instance", Def::Instance(_) => "instance",
}; };
+19 -8
View File
@@ -2238,20 +2238,31 @@ mod tests {
#[test] #[test]
fn doc_wrap_widow_control_skips_when_combined_too_long() { fn doc_wrap_widow_control_skips_when_combined_too_long() {
// If the combined length of (pulled + " " + orphan) would // Widow control pulls the penultimate line's tail word down to
// exceed budget, widow control must NOT fire — preserving // join a 1-word orphan — but only when (pulled + " " + orphan)
// the wrap budget invariant takes priority over cosmetics. // still fits the budget. When it does NOT fit, the budget
// Construct a case: prev-tail = a long word, orphan = a long // invariant wins and the orphan stays a 1-word last line.
// word — combined > 80. //
let pieces = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb \ // Construct that case at budget 80: a short head word forces the
cccccccccccccccccccccccccccccccccccccccccc"; // penultimate line to hold two words ("tail" + 40×'b'), and the
let lines = wrap_words(pieces, 80); // orphan is a 40×'c' word. Greedy wrap yields
// ["tail bbb…"(45), "ccc…"(40)]; the merge candidate is
// pulled(40) + 1 + orphan(40) = 81 > 80, so widow control must
// skip and the last line keeps its single word.
let pieces = format!("tail {} {}", "b".repeat(40), "c".repeat(40));
let lines = wrap_words(&pieces, 80);
// Sanity: the wrap produced multiple lines. // Sanity: the wrap produced multiple lines.
assert!(lines.len() >= 2); assert!(lines.len() >= 2);
// No line exceeds budget. // No line exceeds budget.
for l in &lines { for l in &lines {
assert!(l.len() <= 80, "line over budget: {l:?}"); assert!(l.len() <= 80, "line over budget: {l:?}");
} }
// Widow control did NOT fire: the orphan stays a 1-word last line.
assert_eq!(
lines.last().unwrap().split_whitespace().count(),
1,
"widow control should have skipped (merge over budget); got {lines:?}"
);
} }
// ---- Polish 5: nested match across lines ---- // ---- Polish 5: nested match across lines ----