iter 22b.2.8: register class methods in module globals
This commit is contained in:
@@ -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
@@ -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" } }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user