iter mq.tidy: close 4 actionable drift items from audit-mq
T1 [high-1]: refine_multi_candidate_residual rigid-var filter now requires class + type-unification via constraint_type_matches, so forall a b. prelude.Show a, userlib.Show b => ... shapes discriminate by which typevar the residual is on. Discharge-time only; synth-time twin doesn't have a residual type to unify against (fresh metavar constructed after dispatch). T2 [high-2]: qualifier_is_class_shape extracted as pub(crate) helper adjacent to parse_method_qualifier; broadened to accept PascalCase single-segment qualifiers like Show.show alongside module-qualified ones. Same-module bare-class call sites now reachable; symmetric to mq.1 canonical-form rule. T3 [medium-1]: any_candidate_class_has_instance pub(crate) helper gates the class-method-shadowed-by-fn warning closure on registry instance presence (any candidate class, any type). Conservative tightening of the spec rule per Boss Q2 decision. T4 [medium-2]: four DESIGN.md Data Model schema fragments (SuperclassRef, InstanceDef.class, Type::Con.name, Constraint.class) annotated with canonical-form cross-references. Two trip-wires fixed inline: parse_method_qualifier docstring coherence, mq3 in-crate test fixture-repair (registry instance injection — analogous to mq.3 typeclass_22b3 pattern). 5/5 tasks. 548 tests green (was 545 + 3 new mq_tidy_* unit tests). bench/compile_check.py + cross_lang.py exit 0; check.py exit 0 both runs with noise-class metric migration on latency.implicit_at_rc.* max-tail (6th-consecutive observation since audit-cma). Baseline pristine.
This commit is contained in:
+217
-14
@@ -2160,10 +2160,20 @@ pub fn refine_multi_candidate_residual(
|
||||
};
|
||||
}
|
||||
|
||||
// Rigid-var path: filter against declared constraints.
|
||||
// Rigid-var path: filter against declared constraints requiring
|
||||
// BOTH class-name match AND type-unification with the residual's
|
||||
// `type_` (per spec §"Constraint-discharge refinement" 130-138).
|
||||
// `constraint_type_matches` is the post-`subst.apply` Var/Con
|
||||
// recursive matcher; the residual's `type_` has been `subst.apply`-d
|
||||
// by the discharge caller, so rigid-var identity (Var("a") vs
|
||||
// Var("b")) is a direct name comparison.
|
||||
let survivors: Vec<String> = candidates
|
||||
.iter()
|
||||
.filter(|c| declared_constraints.iter().any(|dc| &dc.class == *c))
|
||||
.filter(|c| {
|
||||
declared_constraints.iter().any(|dc| {
|
||||
&dc.class == *c && constraint_type_matches(&dc.type_, &residual.type_)
|
||||
})
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
match survivors.len() {
|
||||
@@ -2225,14 +2235,16 @@ pub(crate) fn qualify_class_ref_in_check(class_ref: &str, caller_module: &str) -
|
||||
///
|
||||
/// - `"show"` → `("show", None)`
|
||||
/// - `"prelude.Show.show"` → `("show", Some("prelude.Show"))`
|
||||
/// - `"Show.show"` → `("show", Some("Show"))`
|
||||
/// - `"std_list.length"` → `("length", Some("std_list"))`
|
||||
///
|
||||
/// Callers gate on the qualifier shape: a class qualifier is always
|
||||
/// qualified per mq.1's canonical-form rule, so a single-segment
|
||||
/// prefix (no dot) is structurally a module name and the caller falls
|
||||
/// through to the cross-module-fn path. A multi-segment prefix
|
||||
/// (`<module>.<Class>`) signals an explicit class qualifier on a
|
||||
/// class-method call site.
|
||||
/// Callers distinguish class qualifiers from cross-module-fn
|
||||
/// qualifiers via [`qualifier_is_class_shape`]: PascalCase
|
||||
/// single-segment prefixes (e.g. `"Show"`) and multi-segment
|
||||
/// prefixes (e.g. `"prelude.Show"`) are class qualifiers per
|
||||
/// mq.1's canonical-form rule; lowercase single-segment prefixes
|
||||
/// (e.g. `"std_list"`) are module names and the caller falls
|
||||
/// through to the cross-module-fn path.
|
||||
pub(crate) fn parse_method_qualifier(name: &str) -> (&str, Option<String>) {
|
||||
if let Some(dot_idx) = name.rfind('.') {
|
||||
(&name[dot_idx + 1..], Some(name[..dot_idx].to_string()))
|
||||
@@ -2241,6 +2253,64 @@ pub(crate) fn parse_method_qualifier(name: &str) -> (&str, Option<String>) {
|
||||
}
|
||||
}
|
||||
|
||||
/// mq.tidy: predicate gating the synth Var-arm's class-method
|
||||
/// dispatch entry on the qualifier shape. Accepts:
|
||||
/// - bare-method form (qualifier absent), e.g. `"show"`.
|
||||
/// - bare-class qualifier, e.g. `"Show.show"` — class names are
|
||||
/// PascalCase per repo convention (first character uppercase).
|
||||
/// - module-qualified class qualifier, e.g. `"prelude.Show.show"`.
|
||||
/// Rejects qualifier shapes that look like cross-module-fn calls
|
||||
/// (`"std_list.length"`): qualifier starts with lowercase, no
|
||||
/// inner dot — falls through to the qualified-fn arm.
|
||||
pub(crate) fn qualifier_is_class_shape(qualifier_opt: &Option<String>) -> bool {
|
||||
match qualifier_opt {
|
||||
None => true,
|
||||
Some(q) => {
|
||||
// Contains inner dot: module-qualified class
|
||||
// (`prelude.Show`).
|
||||
if q.contains('.') {
|
||||
return true;
|
||||
}
|
||||
// Single segment: class iff the first char is uppercase
|
||||
// (PascalCase convention).
|
||||
q.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// mq.tidy: predicate for the `class-method-shadowed-by-fn`
|
||||
/// warning emission. The full spec rule (§"Class-fn collisions"
|
||||
/// 161-162) requires a class candidate WITH AN INSTANCE FOR THE
|
||||
/// ACTUAL ARG TYPE; the arg type is unavailable at the synth
|
||||
/// Var-arm where the warning fires (App-arm unification has not
|
||||
/// run yet). This conservative approximation requires only that
|
||||
/// at least one candidate class has SOME instance in the
|
||||
/// workspace registry — stricter than today's "any class
|
||||
/// declares the method" (which fires even for class methods
|
||||
/// with zero instances anywhere), looser than the full spec
|
||||
/// rule. A future tighten can defer the warning emission to a
|
||||
/// later pass where the arg type is known.
|
||||
pub(crate) fn any_candidate_class_has_instance(
|
||||
method_to_candidate_classes: &BTreeMap<String, BTreeSet<String>>,
|
||||
workspace_registry_entries: &BTreeMap<(String, String), ()>,
|
||||
method_name: &str,
|
||||
) -> bool {
|
||||
let Some(cands) = method_to_candidate_classes.get(method_name) else {
|
||||
return false;
|
||||
};
|
||||
cands.iter().any(|c| {
|
||||
// BTreeMap is ordered lexicographically on the tuple key;
|
||||
// `range((c, "")..).next()` is the first entry whose first
|
||||
// component is >= `c`. The check `k == c` rejects spillover
|
||||
// into the next class. O(log n) per candidate.
|
||||
workspace_registry_entries
|
||||
.range((c.clone(), String::new())..)
|
||||
.next()
|
||||
.map(|((k, _), _)| k == c)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
|
||||
/// Iter 22b.2 (Task 9): expand a list of declared class constraints
|
||||
/// with their one-step superclass closure (Decision 11). For every
|
||||
/// declared `(C, t)`, append `(S, t)` if class `C` has a superclass
|
||||
@@ -2479,6 +2549,26 @@ pub(crate) fn synth(
|
||||
let emit_shadow_warning_if_class_method =
|
||||
|name: &str, owner_module: &str, warnings: &mut Vec<crate::diagnostic::Diagnostic>| {
|
||||
let (method_name, _) = parse_method_qualifier(name);
|
||||
// mq.tidy: tighten emission per spec §"Class-fn
|
||||
// collisions" — fire only when at least one
|
||||
// candidate class has a registry instance (any
|
||||
// type). Without this, the warning fires on any
|
||||
// class declaring the method, including class
|
||||
// methods with zero instances anywhere in the
|
||||
// workspace.
|
||||
let registry_unit_view: BTreeMap<(String, String), ()> = env
|
||||
.workspace_registry
|
||||
.entries
|
||||
.keys()
|
||||
.map(|k| (k.clone(), ()))
|
||||
.collect();
|
||||
if !any_candidate_class_has_instance(
|
||||
&env.method_to_candidate_classes,
|
||||
®istry_unit_view,
|
||||
method_name,
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if let Some(cands) = env.method_to_candidate_classes.get(method_name) {
|
||||
let mut candidate_list: Vec<String> = cands.iter().cloned().collect();
|
||||
candidate_list.sort();
|
||||
@@ -2567,11 +2657,7 @@ pub(crate) fn synth(
|
||||
// `<module>.<Class>`), so we fall through to the
|
||||
// qualified-fn arm below in that case.
|
||||
let (method_name, qualifier_opt) = parse_method_qualifier(name);
|
||||
let qualifier_is_class_shape = match &qualifier_opt {
|
||||
None => true,
|
||||
Some(q) => q.contains('.'),
|
||||
};
|
||||
qualifier_is_class_shape
|
||||
qualifier_is_class_shape(&qualifier_opt)
|
||||
&& env.method_to_candidate_classes.contains_key(method_name)
|
||||
} {
|
||||
// mq.2: type-driven dispatch entry. The Var arm runs
|
||||
@@ -6220,11 +6306,30 @@ mod tests {
|
||||
let mut modules = BTreeMap::new();
|
||||
modules.insert("clsmod".into(), clsmod);
|
||||
modules.insert("fnmod".into(), fnmod);
|
||||
// mq.tidy T3: post-tightening, the warning fires only when at
|
||||
// least one candidate class has an instance in the workspace
|
||||
// registry. Ship a `clsmod.Show Int` instance so the fixture
|
||||
// continues to assert positive-fire behaviour under the
|
||||
// post-tidy spec rule.
|
||||
let mut registry = ailang_core::workspace::Registry::default();
|
||||
let int_h = ailang_core::canonical::type_hash(&Type::int());
|
||||
registry.entries.insert(
|
||||
("clsmod.Show".into(), int_h),
|
||||
ailang_core::workspace::RegistryEntry {
|
||||
instance: ailang_core::ast::InstanceDef {
|
||||
class: "clsmod.Show".into(),
|
||||
type_: Type::int(),
|
||||
methods: vec![],
|
||||
doc: None,
|
||||
},
|
||||
defining_module: "clsmod".into(),
|
||||
},
|
||||
);
|
||||
let ws = Workspace {
|
||||
entry: "fnmod".into(),
|
||||
modules,
|
||||
root_dir: std::path::PathBuf::from("."),
|
||||
registry: ailang_core::workspace::Registry::default(),
|
||||
registry,
|
||||
};
|
||||
let mut env = build_check_env(&ws);
|
||||
// Treat both modules as implicitly imported by `env.imports`
|
||||
@@ -6302,4 +6407,102 @@ mod tests {
|
||||
assert!(env.class_methods.contains_key(&key),
|
||||
"env.class_methods must contain {key:?}");
|
||||
}
|
||||
|
||||
/// mq.tidy T1: discharge-time rigid-var refinement must filter
|
||||
/// declared constraints by BOTH class-name match AND
|
||||
/// type-unification with the residual's `type_` (spec
|
||||
/// §"Constraint-discharge refinement" lines 130-138). Two
|
||||
/// same-class declared constraints on different typevars must
|
||||
/// be discriminated by the residual's actual typevar identity.
|
||||
#[test]
|
||||
fn mq_tidy_rigid_var_filter_uses_type_unification() {
|
||||
use ailang_core::ast::Constraint;
|
||||
|
||||
// Candidate set: two distinct classes both named "Show".
|
||||
let mut candidates = BTreeSet::new();
|
||||
candidates.insert("prelude.Show".to_string());
|
||||
candidates.insert("userlib.Show".to_string());
|
||||
|
||||
// Residual on Var "a".
|
||||
let residual = ResidualConstraint {
|
||||
class: "prelude.Show".to_string(),
|
||||
type_: Type::Var { name: "a".to_string() },
|
||||
method: "show".to_string(),
|
||||
candidates: Some(candidates),
|
||||
};
|
||||
|
||||
// Declared: prelude.Show a (matches residual type),
|
||||
// userlib.Show b (does NOT match residual type).
|
||||
let declared = vec![
|
||||
Constraint {
|
||||
class: "prelude.Show".to_string(),
|
||||
type_: Type::Var { name: "a".to_string() },
|
||||
},
|
||||
Constraint {
|
||||
class: "userlib.Show".to_string(),
|
||||
type_: Type::Var { name: "b".to_string() },
|
||||
},
|
||||
];
|
||||
|
||||
let registry: BTreeMap<(String, String), ()> = BTreeMap::new();
|
||||
let outcome = refine_multi_candidate_residual(&residual, &declared, ®istry);
|
||||
|
||||
assert_eq!(
|
||||
outcome,
|
||||
RefineOutcome::Resolved("prelude.Show".to_string()),
|
||||
"type-unification leg must drop userlib.Show because its declared type (Var b) does not match the residual type (Var a)"
|
||||
);
|
||||
}
|
||||
|
||||
/// mq.tidy T2: the synth Var-arm's class-method dispatch entry
|
||||
/// gates on a qualifier-shape predicate. The predicate must
|
||||
/// accept three shapes — bare-method (None), bare-class
|
||||
/// PascalCase qualifier (`"Show"`), module-qualified class
|
||||
/// qualifier (`"prelude.Show"`) — and reject bare-fn
|
||||
/// qualifiers (`"std_list"`, lowercase, no inner dot) which
|
||||
/// belong to the cross-module-fn arm.
|
||||
#[test]
|
||||
fn mq_tidy_bare_class_qualifier_shape_accepted() {
|
||||
assert!(qualifier_is_class_shape(&None));
|
||||
assert!(qualifier_is_class_shape(&Some("Show".to_string())));
|
||||
assert!(qualifier_is_class_shape(&Some("prelude.Show".to_string())));
|
||||
|
||||
// Bare fn qualifier (lowercase, no inner dot) — NOT class-shape.
|
||||
assert!(!qualifier_is_class_shape(&Some("std_list".to_string())));
|
||||
}
|
||||
|
||||
/// mq.tidy T3: the `class-method-shadowed-by-fn` warning fires
|
||||
/// only when at least one candidate class has a registry
|
||||
/// instance (any type) per spec §"Class-fn collisions" 161-162.
|
||||
/// Without this conservative-tightening, the warning fires
|
||||
/// unconditionally on `method_to_candidate_classes` presence,
|
||||
/// including class methods with zero instances anywhere.
|
||||
#[test]
|
||||
fn mq_tidy_shadow_warning_suppressed_when_no_instance() {
|
||||
let mut method_to_candidate_classes: BTreeMap<String, BTreeSet<String>> =
|
||||
BTreeMap::new();
|
||||
let mut foo_cands = BTreeSet::new();
|
||||
foo_cands.insert("userlib.Foo".to_string());
|
||||
method_to_candidate_classes.insert("foo".to_string(), foo_cands);
|
||||
|
||||
// Empty registry → no candidate class has an instance → false.
|
||||
let empty_registry: BTreeMap<(String, String), ()> = BTreeMap::new();
|
||||
assert!(!any_candidate_class_has_instance(
|
||||
&method_to_candidate_classes,
|
||||
&empty_registry,
|
||||
"foo",
|
||||
));
|
||||
|
||||
// Add one instance for userlib.Foo → true.
|
||||
let mut registry_with_inst: BTreeMap<(String, String), ()> = BTreeMap::new();
|
||||
registry_with_inst.insert(
|
||||
("userlib.Foo".to_string(), "Int_hash".to_string()),
|
||||
(),
|
||||
);
|
||||
assert!(any_candidate_class_has_instance(
|
||||
&method_to_candidate_classes,
|
||||
®istry_with_inst,
|
||||
"foo",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user