From 17605a3327c95bf058baf4ca23e6e14194b2110f Mon Sep 17 00:00:00 2001 From: Brummel Date: Sat, 9 May 2026 18:33:01 +0200 Subject: [PATCH] iter 22b.2.8: register class methods in module globals --- crates/ail/tests/typeclass_22b2.rs | 44 +++++ crates/ailang-check/src/lib.rs | 171 +++++++++++++++--- .../test_22b2_class_method_lookup.ail.json | 34 ++++ 3 files changed, 223 insertions(+), 26 deletions(-) create mode 100644 crates/ail/tests/typeclass_22b2.rs create mode 100644 examples/test_22b2_class_method_lookup.ail.json diff --git a/crates/ail/tests/typeclass_22b2.rs b/crates/ail/tests/typeclass_22b2.rs new file mode 100644 index 0000000..9004967 --- /dev/null +++ b/crates/ail/tests/typeclass_22b2.rs @@ -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`" + ); +} diff --git a/crates/ailang-check/src/lib.rs b/crates/ailang-check/src/lib.rs index 4a91e23..6f7476f 100644 --- a/crates/ailang-check/src/lib.rs +++ b/crates/ailang-check/src/lib.rs @@ -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`; 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, + /// 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, +} + +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>> { - let mut out: BTreeMap> = BTreeMap::new(); +) -> Result> { + let mut out: BTreeMap = 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`, 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>, + module_globals: &BTreeMap, ) -> Vec { 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` 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 diff --git a/examples/test_22b2_class_method_lookup.ail.json b/examples/test_22b2_class_method_lookup.ail.json new file mode 100644 index 0000000..543311f --- /dev/null +++ b/examples/test_22b2_class_method_lookup.ail.json @@ -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" } } + } + ] + } + ] +}