iter 22b.2.8: register class methods in module globals

This commit is contained in:
2026-05-09 18:33:01 +02:00
parent 6869bd5d0c
commit 17605a3327
3 changed files with 223 additions and 26 deletions
+44
View File
@@ -0,0 +1,44 @@
//! Iter 22b.2 typeclass typecheck arms — integration tests.
//!
//! This file is shared by 22b.2 tasks: it exists from Task 8
//! (class methods register into module globals) and is extended by
//! Tasks 9 (`missing-constraint`) and 10 (`no-instance`).
use std::path::{Path, PathBuf};
fn examples_dir() -> PathBuf {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
Path::new(manifest_dir)
.parent()
.unwrap()
.parent()
.unwrap()
.join("examples")
}
/// Property protected: a class method declared in a `Def::Class` must be
/// reachable as a name in the module's `ModuleGlobals` so that pass-2
/// (`Term::Var { name: "show" }`) does not fire `unbound-var` for
/// well-formed class-method calls. The lookup must remember which class
/// the method belongs to so Task 9 can build the residual constraint at
/// the call site.
#[test]
fn class_method_is_in_module_globals() {
let entry = examples_dir().join("test_22b2_class_method_lookup.ail.json");
let ws = ailang_core::workspace::load_workspace(&entry)
.expect("workspace must load");
let globals = ailang_check::build_module_globals(&ws)
.expect("globals build");
let mod_globals = globals
.get("test_22b2_class_method_lookup")
.expect("module globals present");
assert!(
mod_globals.has_class_method("show"),
"class method `show` must appear in module globals"
);
assert_eq!(
mod_globals.class_method_class("show"),
Some("Show"),
"class method `show` must remember its class is `Show`"
);
}
+145 -26
View File
@@ -839,15 +839,82 @@ fn build_module_types(
out
}
/// Iter 22b.2: per-module top-level symbol table. Holds the classic
/// fn/const/type-name-to-type mapping in [`Self::fns`] and class-method
/// names in [`Self::class_methods`]. The two channels are kept separate
/// because class-method resolution at a call site needs more context
/// than a plain `Type` lookup (the owning class, the class param name,
/// and the defining module — see [`ClassMethodEntry`]). Pre-22b.2 the
/// inner shape was `IndexMap<String, Type>`; 22b.2 wraps that in this
/// struct so call sites that only need fn lookup can keep using
/// `mod_globals.fns` while typeclass code goes through `class_methods`.
#[derive(Debug, Default, Clone)]
pub struct ModuleGlobals {
/// Top-level fn / const / type symbols, by name. The pre-22b.2 inner
/// shape — every call site that only needs HM lookup goes through
/// here.
pub fns: IndexMap<String, Type>,
/// Class-method names, by method name. Populated in
/// [`build_module_globals`] from `Def::Class` entries; consulted by
/// the 22b.2 `missing-constraint` / `no-instance` arms (Tasks 9 / 10).
pub class_methods: BTreeMap<String, ClassMethodEntry>,
}
impl ModuleGlobals {
/// True if `name` is a class method declared in this module.
pub fn has_class_method(&self, name: &str) -> bool {
self.class_methods.contains_key(name)
}
/// The class that declared the method `name`, or `None` if `name`
/// is not a class method in this module.
pub fn class_method_class(&self, name: &str) -> Option<&str> {
self.class_methods
.get(name)
.map(|e| e.class_name.as_str())
}
}
/// Iter 22b.2: per-class-method metadata stored in
/// [`ModuleGlobals::class_methods`]. Carries everything the 22b.2
/// typecheck arms (`missing-constraint`, `no-instance`) need to resolve
/// a `Term::Var { name }` that refers to a class method into a residual
/// class constraint at the call site.
#[derive(Debug, Clone)]
pub struct ClassMethodEntry {
/// The class that declared the method (e.g. `Show` for `show`).
pub class_name: String,
/// The class's single type parameter name (e.g. `a` in
/// `class Show a`). The class param appears at this name inside
/// [`Self::method_ty`].
pub class_param: String,
/// The method's declared signature — a `Type::Fn` whose param /
/// return positions may contain `Type::Var { name: class_param }`.
/// Task 9 instantiates the class param with a fresh metavar at the
/// call site to build the residual constraint.
pub method_ty: Type,
/// Module that declared the class. Tasks 9 / 10 use this to find
/// the matching `Registry` entries when checking constraints.
pub defining_module: String,
}
/// Builds the top-level symbol table per module (for cross-module lookup),
/// without checking bodies. Duplicates and dot-in-def names are reported
/// here as errors immediately — they would taint all further diagnostics.
fn build_module_globals(
///
/// Iter 22b.2 (Task 8): also collects class-method names from `Def::Class`
/// into [`ModuleGlobals::class_methods`], so that pass-2 lookup of a
/// class method via `Term::Var { name }` resolves to a known global
/// rather than firing `unbound-var`. The class-method channel is kept
/// separate from the fn / const / type channel because resolution
/// downstream needs the owning class and class-param name to build a
/// residual constraint at the call site.
pub fn build_module_globals(
ws: &Workspace,
) -> Result<BTreeMap<String, IndexMap<String, Type>>> {
let mut out: BTreeMap<String, IndexMap<String, Type>> = BTreeMap::new();
) -> Result<BTreeMap<String, ModuleGlobals>> {
let mut out: BTreeMap<String, ModuleGlobals> = BTreeMap::new();
for (mname, m) in &ws.modules {
let mut globals = IndexMap::new();
let mut globals = ModuleGlobals::default();
for def in &m.defs {
let def_name = def.name();
if def_name.contains('.') {
@@ -858,25 +925,66 @@ fn build_module_globals(
}),
));
}
if globals.contains_key(def_name) {
return Err(CheckError::Def(
def_name.to_string(),
Box::new(CheckError::DuplicateDef(def_name.to_string())),
));
// Iter 22b.2: pre-22b.2 the dup check ran across the single
// `IndexMap<String, Type>`, but class / instance defs early-
// `continue`d before the insert, so they never participated
// in dup detection (and `instance Foo` re-using the class
// name is legal). Preserving that, dup detection here is
// scoped to the `fns` channel exactly as before. Class /
// instance coherence (orphan / duplicate-instance /
// missing-method / method-name-collision) is enforced
// earlier at `workspace::build_registry`.
match def {
Def::Fn(_) | Def::Const(_) | Def::Type(_) => {
if globals.fns.contains_key(def_name) {
return Err(CheckError::Def(
def_name.to_string(),
Box::new(CheckError::DuplicateDef(def_name.to_string())),
));
}
}
Def::Class(_) | Def::Instance(_) => {}
}
match def {
Def::Fn(f) => {
globals.fns.insert(def_name.to_string(), f.ty.clone());
}
Def::Const(c) => {
globals.fns.insert(def_name.to_string(), c.ty.clone());
}
Def::Type(_) => {
globals.fns.insert(
def_name.to_string(),
Type::Con {
name: def_name.to_string(),
args: vec![],
},
);
}
Def::Class(cd) => {
// Iter 22b.2 (Task 8): register every method
// declared by the class as a class-method global,
// with enough metadata for the typecheck arms in
// Tasks 9 / 10 to build a residual constraint at
// the call site.
for meth in &cd.methods {
globals.class_methods.insert(
meth.name.clone(),
ClassMethodEntry {
class_name: cd.name.clone(),
class_param: cd.param.clone(),
method_ty: meth.ty.clone(),
defining_module: mname.clone(),
},
);
}
}
Def::Instance(_) => {
// Instances do not contribute globals; coherence
// is enforced earlier at
// `workspace::build_registry`.
}
}
let ty = match def {
Def::Fn(f) => f.ty.clone(),
Def::Const(c) => c.ty.clone(),
Def::Type(_) => Type::Con {
name: def_name.to_string(),
args: vec![],
},
// Iter 22b.1: class/instance defs do not contribute
// monomorphic globals to the workspace symbol table
// before 22b.3 monomorphisation runs. Skip them here.
Def::Class(_) | Def::Instance(_) => continue,
};
globals.insert(def_name.to_string(), ty);
}
out.insert(mname.clone(), globals);
}
@@ -899,7 +1007,7 @@ fn build_module_globals(
fn check_in_workspace(
m: &Module,
ws: &Workspace,
module_globals: &BTreeMap<String, IndexMap<String, Type>>,
module_globals: &BTreeMap<String, ModuleGlobals>,
) -> Vec<CheckError> {
let mut env = Env::new();
builtins::install(&mut env);
@@ -939,9 +1047,12 @@ fn check_in_workspace(
}
}
// Take local globals from the previously built table.
// Take local globals from the previously built table. Iter 22b.2:
// class methods sit in `g.class_methods` and reach the env through
// a separate channel in Tasks 9 / 10; here we only seed the HM
// monomorphic name table from `g.fns`.
if let Some(g) = module_globals.get(&m.name) {
for (n, t) in g {
for (n, t) in &g.fns {
env.globals.insert(n.clone(), t.clone());
}
}
@@ -958,7 +1069,15 @@ fn check_in_workspace(
import_map.insert(key, imp.module.clone());
}
env.imports = import_map;
env.module_globals = module_globals.clone();
// Iter 22b.2: project to the legacy `IndexMap<String, Type>` shape
// so the cross-module qualified-var resolution path keeps its
// pre-22b.2 form. Class methods are not addressable through the
// qualified-var channel — they go through the per-module
// `class_methods` table consulted in the 22b.2 typecheck arms.
env.module_globals = module_globals
.iter()
.map(|(k, v)| (k.clone(), v.fns.clone()))
.collect();
env.module_types = build_module_types(ws);
env.current_module = m.name.clone();
// Workspace isn't directly needed in the env; cross-module lookup uses
@@ -0,0 +1,34 @@
{
"schema": "ailang/v0",
"name": "test_22b2_class_method_lookup",
"imports": [],
"defs": [
{
"kind": "class",
"name": "Show",
"param": "a",
"methods": [
{
"name": "show",
"type": {
"k": "fn",
"params": [{ "k": "var", "name": "a" }],
"ret": { "k": "con", "name": "Str" },
"effects": []
}
}
]
},
{
"kind": "instance",
"class": "Show",
"type": { "k": "con", "name": "Int" },
"methods": [
{
"name": "show",
"body": { "t": "lit", "lit": { "kind": "str", "value": "n" } }
}
]
}
]
}