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('.') {
|
if let Some(idx) = name.find('.') {
|
||||||
let mod_name = &name[..idx];
|
let mod_name = &name[..idx];
|
||||||
let def_name = &name[idx + 1..];
|
let def_name = &name[idx + 1..];
|
||||||
let m = ws.modules.get(mod_name).with_context(|| {
|
// Precedence: a literal module wins. `<module>.<def>` resolves
|
||||||
format!("no module `{mod_name}` in workspace `{}`", ws.entry)
|
// strictly through that module.
|
||||||
})?;
|
if let Some(m) = ws.modules.get(mod_name) {
|
||||||
let def = m
|
let def = m
|
||||||
.defs
|
.defs
|
||||||
.iter()
|
.iter()
|
||||||
.find(|d| d.name() == def_name)
|
.find(|d| d.name() == def_name)
|
||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
format!("no def `{def_name}` in module `{mod_name}`")
|
format!("no def `{def_name}` in module `{mod_name}`")
|
||||||
})?;
|
})?;
|
||||||
return Ok((mod_name.to_string(), def));
|
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.
|
// Bare name: entry module first.
|
||||||
|
|||||||
Reference in New Issue
Block a user