From 923dd8c02fcfd5a63e85775d01821d813f9d86e0 Mon Sep 17 00:00:00 2001 From: Brummel Date: Mon, 11 May 2026 14:56:27 +0200 Subject: [PATCH] =?UTF-8?q?iter=2023.4-prep.2:=20check=20=E2=80=94=20bare-?= =?UTF-8?q?name=20fall-through=20to=20implicit-imported=20free=20fns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/ailang-check/src/lib.rs | 91 ++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/crates/ailang-check/src/lib.rs b/crates/ailang-check/src/lib.rs index 2b609c7..0e5ffef 100644 --- a/crates/ailang-check/src/lib.rs +++ b/crates/ailang-check/src/lib.rs @@ -1806,6 +1806,34 @@ pub(crate) fn synth( method: name.clone(), }); 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 { let (prefix, suffix) = name.split_once('.').expect("checked"); let target_module = match env.imports.get(prefix) { @@ -4935,4 +4963,67 @@ mod tests { let diags = check_workspace(&ws); 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:?}" + ); + } }