From 420703d321d83747e19e1ad98177559ce4a8b4a1 Mon Sep 17 00:00:00 2001 From: Brummel Date: Sun, 31 May 2026 03:52:23 +0200 Subject: [PATCH] fix(codegen): resolve the dotted T.new user-ADT callee, symmetric to check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A monomorphic `(new Counter 42)` over a user-defined ADT that declares a `new` fn passed `ail check` but crashed `ail build --alloc=rc` with `unknown variable: Counter.new` (RED-pinned in 1eff055). Root: the `Term::New` desugar lowers a no-type-arg monomorphic `(new Counter 42)` to `(app Counter.new 42)` — a type-scoped `Var` callee `Counter.new`. The checker resolves that dotted callee via its TypeDef-first ladder (so check is clean), but codegen had no equivalent resolution: the name was absent from the import map and the current module's def list, so it fell through to `CodegenError::UnknownVar`. The user `new` fn body IS codegen'd — only the dotted call-site failed to resolve. The error surfaced first in the arg-type pre-pass (`synth_with_extras`'s dotted-Var branch), which `lower_term`'s `Term::Let` arm runs before lowering the call. Fix (crates/ailang-codegen/src/lib.rs, all `// #53`): - New `Emitter::type_home_module(type_name)`: returns the module declaring the named TypeDef (current module first, then any import target) by scanning `module_ctor_index` for a matching `CtorRef.type_name`. This mirrors the checker's TypeDef-first ladder. - A dotted `T.def` fallback (resolve via the type's home module) added to the three sites that needed it: `resolve_top_level_fn` (the fn-pointer / lower_app resolution path), `is_static_callee` (its lockstep partner), and the `synth_with_extras` dotted-Var branch (the path that actually fired for this fixture). Lockstep (CLAUDE.md `lower_app` ↔ `is_static_callee`): `is_static_callee` now returns true for a dotted `T.def` whose `T` is a user ADT declared in this or an imported module, exactly matching `resolve_top_level_fn`'s new fallback — so no name is claimed static without being lowerable. Distinct root from #51 (the polymorphic `(new T …)` type-arg drop, ee4107c) — that fix deliberately left the monomorphic app-lowering path unchanged; this completes it. Verification: the #53 pin (crates/ail/tests/user_adt_new_mono_pin.rs) is green; the original 3-module reproducer examples/fieldtest/kem_2_counter_new.ail builds, runs, prints 42; ailang-codegen/check/core suites, the intercepts bijection, and both hash-pins are green; full workspace green. closes #53 --- crates/ailang-codegen/src/lib.rs | 71 +++++++++++++++++++++++++++----- 1 file changed, 61 insertions(+), 10 deletions(-) diff --git a/crates/ailang-codegen/src/lib.rs b/crates/ailang-codegen/src/lib.rs index a77599d..685db00 100644 --- a/crates/ailang-codegen/src/lib.rs +++ b/crates/ailang-codegen/src/lib.rs @@ -2547,11 +2547,23 @@ impl<'a> Emitter<'a> { let target_module = match self.import_map.get(prefix).cloned() { Some(m) => m, None if self.module_user_fns.contains_key(prefix) => prefix.to_string(), - None => { - return Err(CodegenError::Internal(format!( - "cross-module call `{name}`: prefix `{prefix}` not in import map" - ))) - } + // #53: type-scoped `T.def` callee (e.g. `Counter.new`, + // produced by the monomorphic `Term::New` desugar). When + // `prefix` is not an import alias or module name but + // names a TypeDef in scope, the call resolves to the + // `def` fn in that type's home module — symmetric to the + // checker's TypeDef-first ladder. Keeps the documented + // `lower_app` ↔ `is_static_callee` lockstep honest: + // `is_static_callee` already returns true for every + // single-dot name, so this branch MUST resolve it. + None => match self.type_home_module(prefix) { + Some(home) => home, + None => { + return Err(CodegenError::Internal(format!( + "cross-module call `{name}`: prefix `{prefix}` not in import map" + ))) + } + }, }; // iter 23.4: codegen-time poly-call dispatch removed. Post-mono // every poly call site has been rewritten by `rewrite_mono_calls` @@ -2786,6 +2798,30 @@ impl<'a> Emitter<'a> { } + /// #53: returns the module that declares the named TypeDef — the + /// current module first, then imported modules. `None` if no module + /// in scope declares a TypeDef of that name. A type's presence in a + /// module is read from `module_ctor_index` (a TypeDef declares at + /// least one ctor, each `CtorRef` carrying its `type_name`). This + /// mirrors the checker's TypeDef-first ladder so codegen resolves a + /// type-scoped dotted callee (`T.def`) exactly where the checker + /// does. Used by `lower_app` and `resolve_top_level_fn`. + fn type_home_module(&self, type_name: &str) -> Option { + if let Some(ctors) = self.module_ctor_index.get(self.module_name) { + if ctors.values().any(|c| c.type_name == type_name) { + return Some(self.module_name.to_string()); + } + } + for target in self.import_map.values() { + if let Some(ctors) = self.module_ctor_index.get(target) { + if ctors.values().any(|c| c.type_name == type_name) { + return Some(target.clone()); + } + } + } + None + } + /// is `name` a callee that can be resolved at compile time /// (no fn-pointer needed)? True for builtin operators, the /// current-module top-level fns (mono or poly), and qualified @@ -2872,11 +2908,17 @@ impl<'a> Emitter<'a> { // a valid post-mono construct because both ends were // independently typechecked under their original module // contexts before mono ran). - let target = match self.import_map.get(prefix) { - Some(m) => m, - None => prefix, + // #53: a type-scoped `T.def` prefix (e.g. `Counter.new`) + // resolves to the type's home module, symmetric to the + // checker's TypeDef-first ladder and the matching arm in + // `lower_app`'s cross-module branch. Tried after import_map + // and the literal-module-name fallback. + let target: String = match self.import_map.get(prefix) { + Some(m) => m.clone(), + None if self.module_user_fns.contains_key(prefix) => prefix.to_string(), + None => self.type_home_module(prefix).unwrap_or_else(|| prefix.to_string()), }; - let sig = self.module_user_fns.get(target)?.get(suffix)?.clone(); + let sig = self.module_user_fns.get(&target)?.get(suffix)?.clone(); return Some((format!("@ail_{target}_{suffix}_clos"), sig)); } if let Some(sig) = self @@ -3234,6 +3276,14 @@ impl<'a> Emitter<'a> { // post-mono synthesised cross-module references // (see `resolve_top_level_fn` and the cross-module // call arm in `lower_app` for matching fallbacks). + // #53: a type-scoped `T.def` prefix (e.g. `Counter.new`, + // the monomorphic `Term::New` desugar output) resolves + // to the type's home module, after import_map and the + // literal-module-name fallback. Mirrors the checker's + // TypeDef-first ladder and the matching arms in + // `lower_app` / `resolve_top_level_fn`, so arg-type + // synthesis of the dotted callee doesn't hit `UnknownVar`. + let type_home = self.type_home_module(prefix); let target_opt: Option<&str> = self .import_map .get(prefix) @@ -3244,7 +3294,8 @@ impl<'a> Emitter<'a> { } else { None } - }); + }) + .or(type_home.as_deref()); if let Some(target) = target_opt { if let Some(ty) = self .module_def_ail_types