fix(codegen): resolve the dotted T.new user-ADT callee, symmetric to check
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 in1eff055). 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 <elem> …)` 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
This commit is contained in:
@@ -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<String> {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user