From d565ed87a30fb3cc5ea9d59a51d2a6af848a184f Mon Sep 17 00:00:00 2001 From: Brummel Date: Tue, 2 Jun 2026 14:17:34 +0200 Subject: [PATCH] feat(cli): resolve canonical type-scoped describe form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ail describe .` is the canonical form for a type-associated operation (model 0007 §1), but resolve_describe_name only ever looked up the left segment as a workspace module, so `Series.push` errored `no module \`Series\`` even though `series.push` and bare `push` resolved fine. The dotted branch now falls through on a module miss: if no module is named by the left segment, scan the workspace for the `Def::Type` named by it and resolve `` through that type's home module — returning the identical `Def` the module-scoped and bare forms return (verified: same hash d5278789db5f267a for `Series.push` / `series.push`). A literal module still wins (precedence preserved); the bare-name and ambiguous-name paths are untouched. The genuinely-unknown-prefix diagnostic is tightened to `no module or type \`X\` in workspace`, and a type-found-but-op-missing case reports `no def \`op\` in module \`home\` (home of type \`Type\`)`. Fix is CLI-local (crates/ail/src/main.rs); ailang-check untouched (a direct ws.modules scan sufficed, no env.module_types needed). Full workspace suite green (738); the RED pin describe_resolves_type_scoped_op (8d58c83) is now GREEN. closes #64 --- crates/ail/src/main.rs | 53 +++++++++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/crates/ail/src/main.rs b/crates/ail/src/main.rs index 780abf1..4915e28 100644 --- a/crates/ail/src/main.rs +++ b/crates/ail/src/main.rs @@ -1886,17 +1886,48 @@ fn resolve_describe_name<'ws>( if let Some(idx) = name.find('.') { let mod_name = &name[..idx]; let def_name = &name[idx + 1..]; - let m = ws.modules.get(mod_name).with_context(|| { - format!("no module `{mod_name}` in workspace `{}`", ws.entry) - })?; - let def = m - .defs - .iter() - .find(|d| d.name() == def_name) - .with_context(|| { - format!("no def `{def_name}` in module `{mod_name}`") - })?; - return Ok((mod_name.to_string(), def)); + // Precedence: a literal module wins. `.` resolves + // strictly through that module. + if let Some(m) = ws.modules.get(mod_name) { + let def = m + .defs + .iter() + .find(|d| d.name() == def_name) + .with_context(|| { + format!("no def `{def_name}` in module `{mod_name}`") + })?; + return Ok((mod_name.to_string(), def)); + } + // Canonical type-scoped form `.`: the left segment is + // not a module but a known TYPE. Resolve `` through the + // type's HOME module (the module that defines the `Def::Type` + // named `mod_name`) — same `Def` the module-scoped and bare + // forms return. First home module in iteration order wins; in + // practice a type name is unique across a workspace. + for (home_mod, m) in &ws.modules { + let defines_type = m + .defs + .iter() + .any(|d| matches!(d, ailang_core::Def::Type(_)) && d.name() == mod_name); + if !defines_type { + continue; + } + let def = m + .defs + .iter() + .find(|d| d.name() == def_name) + .with_context(|| { + format!( + "no def `{def_name}` in module `{home_mod}` \ + (home of type `{mod_name}`)" + ) + })?; + return Ok((home_mod.clone(), def)); + } + return Err(anyhow::anyhow!( + "no module or type `{mod_name}` in workspace `{}`", + ws.entry + )); } // Bare name: entry module first.