iter 23.4-prep.2: check — bare-name fall-through to implicit-imported free fns

This commit is contained in:
2026-05-11 14:56:27 +02:00
parent 0caaced7e8
commit 923dd8c02f
+91
View File
@@ -1806,6 +1806,34 @@ pub(crate) fn synth(
method: name.clone(), method: name.clone(),
}); });
return Ok(inst_ty); return Ok(inst_ty);
} else if let Some((owner_module, raw_ty)) = env
.imports
.values()
.find_map(|mod_name| {
env.module_globals
.get(mod_name)
.and_then(|mg| mg.get(name).map(|ty| (mod_name.clone(), ty.clone())))
})
{
// Iter 23.4-prep: bare-name fall-through to free fns of
// implicitly-imported modules. Today only the prelude is
// implicitly imported (see `build_check_env` and
// `check_in_workspace`'s prelude-injection at this file's
// ~1170 / ~1285), so the first match is unambiguous. If a
// second implicit import lands, the single-match-wins rule
// becomes a real ambiguity and this branch needs a
// duplicate-detection diagnostic.
//
// Qualify any bare type-cons referring to a type defined in
// the owning module — same treatment the dot-qualified arm
// below applies — so signatures cross the module boundary
// with fully-qualified Type::Cons.
let owner_types = env
.module_types
.get(&owner_module)
.cloned()
.unwrap_or_default();
qualify_local_types(&raw_ty, &owner_module, &owner_types)
} else if name.matches('.').count() == 1 { } else if name.matches('.').count() == 1 {
let (prefix, suffix) = name.split_once('.').expect("checked"); let (prefix, suffix) = name.split_once('.').expect("checked");
let target_module = match env.imports.get(prefix) { let target_module = match env.imports.get(prefix) {
@@ -4935,4 +4963,67 @@ mod tests {
let diags = check_workspace(&ws); let diags = check_workspace(&ws);
assert!(diags.is_empty(), "expected green; got {diags:?}"); assert!(diags.is_empty(), "expected green; got {diags:?}");
} }
/// Iter 23.4-prep Task 2: bare-name resolution falls through to free
/// fns of implicitly-imported modules. The consumer calls bare `dbl`,
/// declared in `prelude` (the singleton implicit import today). The
/// pre-fix behaviour is `unbound-var` because the `Term::Var` lookup
/// ladder consults locals → local globals → class_methods →
/// dot-qualified, but never the flat free-fn tables of implicit
/// imports.
#[test]
fn bare_name_resolves_through_implicit_import_to_free_fn() {
let prelude = Module {
schema: SCHEMA.into(),
name: "prelude".into(),
imports: vec![],
defs: vec![fn_def(
"dbl",
Type::Fn {
params: vec![Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
vec!["n"],
Term::Var { name: "n".into() },
)],
};
let consumer = Module {
schema: SCHEMA.into(),
name: "m".into(),
imports: vec![],
defs: vec![fn_def(
"main",
Type::Fn {
params: vec![],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
vec![],
Term::App {
callee: Box::new(Term::Var { name: "dbl".into() }),
args: vec![Term::Lit { lit: Literal::Int { value: 5 } }],
tail: false,
},
)],
};
let mut modules = BTreeMap::new();
modules.insert("prelude".into(), prelude);
modules.insert("m".into(), consumer);
let ws = Workspace {
entry: "m".into(),
modules,
root_dir: std::path::PathBuf::from("."),
registry: ailang_core::workspace::Registry::default(),
};
let diags = check_workspace(&ws);
assert!(
diags.is_empty(),
"expected bare `dbl` to resolve through implicit prelude import; got {diags:?}"
);
}
} }