feat(cli): resolve canonical type-scoped describe form
`ail describe <Type>.<op>` 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 `<op>` 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
This commit is contained in:
+42
-11
@@ -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. `<module>.<def>` 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 `<Type>.<op>`: the left segment is
|
||||
// not a module but a known TYPE. Resolve `<op>` 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.
|
||||
|
||||
Reference in New Issue
Block a user