Skip to content

Commit 77ba191

Browse files
claudehyperpolymath
authored andcommitted
feat(check): real type schemes — let-polymorphism via generalize/instantiate
Replaces the shared-variable pseudo-polymorphism (the module header claimed let-polymorphism, but the code pinned a single type var across all uses of a binding) with proper Hindley-Milner generalization: - new `Scheme { vars, ty }`; env bindings are now schemes. `bind()` still takes a `Type` and wraps a monomorphic scheme, so its many callers are unchanged; `bind_scheme()` is added for polymorphic bindings. - `generalize(ty)` quantifies the type vars free in `ty` but not free in the environment; `instantiate(scheme)` freshens the quantified vars at each use. - top-level `Item::Let` generalizes before binding; `Expr::Var` instantiates on lookup; the recursive-let placeholder reads the scheme body. - `to_string` is now seeded as `forall a. a -> String` (genuinely polymorphic). 2 new tests (independent instantiation; `to_string` at Int and String in one scope — the old approximation would reject the second). 43 bet-check tests pass; lib clippy-clean. Completes the checker-hardening pass (occurs check + type schemes). https://claude.ai/code/session_01QGi8GND5yNWgDyfReVEPYs
1 parent 431420f commit 77ba191

1 file changed

Lines changed: 193 additions & 15 deletions

File tree

compiler/bet-check/src/lib.rs

Lines changed: 193 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,22 @@ use std::collections::HashMap;
2727
// Type Environment
2828
// ============================================
2929

30+
/// A (possibly polymorphic) type scheme: `∀ vars. ty`. An empty `vars` is a
31+
/// monomorphic type. Used for let-generalization — each use site of a
32+
/// polymorphic binding instantiates the quantified variables afresh.
33+
#[derive(Debug, Clone)]
34+
pub struct Scheme {
35+
/// Quantified type-variable ids.
36+
pub vars: Vec<u32>,
37+
/// The body type (may mention `vars` and/or monomorphic free vars).
38+
pub ty: Type,
39+
}
40+
3041
/// Type environment with scoping and type variable generation.
3142
#[derive(Debug, Clone)]
3243
pub struct CheckEnv {
33-
/// Variable-to-type bindings in current scope.
34-
bindings: HashMap<String, Type>,
44+
/// Variable-to-scheme bindings in current scope.
45+
bindings: HashMap<String, Scheme>,
3546
/// Parent scope (for lexical scoping).
3647
parent: Option<Box<CheckEnv>>,
3748
/// Counter for generating fresh type variables.
@@ -67,18 +78,132 @@ impl CheckEnv {
6778
}
6879
}
6980

70-
/// Bind a name to a type in the current scope.
81+
/// Bind a name to a *monomorphic* type in the current scope.
7182
pub fn bind(&mut self, name: String, ty: Type) {
72-
self.bindings.insert(name, ty);
83+
self.bindings.insert(name, Scheme { vars: Vec::new(), ty });
84+
}
85+
86+
/// Bind a name to a (possibly polymorphic) type scheme.
87+
pub fn bind_scheme(&mut self, name: String, scheme: Scheme) {
88+
self.bindings.insert(name, scheme);
7389
}
7490

75-
/// Look up a name, searching parent scopes.
76-
pub fn lookup(&self, name: &str) -> Option<&Type> {
91+
/// Look up a name's scheme, searching parent scopes.
92+
pub fn lookup(&self, name: &str) -> Option<&Scheme> {
7793
self.bindings
7894
.get(name)
7995
.or_else(|| self.parent.as_ref().and_then(|p| p.lookup(name)))
8096
}
8197

98+
/// Instantiate a scheme: replace each quantified variable with a fresh one,
99+
/// so every use site of a polymorphic binding is independent.
100+
pub fn instantiate(&mut self, scheme: &Scheme) -> Type {
101+
if scheme.vars.is_empty() {
102+
return scheme.ty.clone();
103+
}
104+
let mut mapping = HashMap::new();
105+
for &v in &scheme.vars {
106+
let fresh = self.fresh_var();
107+
mapping.insert(v, fresh);
108+
}
109+
Self::subst_vars(&scheme.ty, &mapping)
110+
}
111+
112+
/// Generalize a type into a scheme by quantifying over the type variables
113+
/// free in it but not free in the environment (HM let-generalization).
114+
pub fn generalize(&self, ty: &Type) -> Scheme {
115+
let resolved = self.resolve(ty);
116+
let mut ty_vars = std::collections::BTreeSet::new();
117+
Self::collect_vars(&resolved, &mut ty_vars);
118+
let env_vars = self.env_free_vars();
119+
let vars: Vec<u32> = ty_vars
120+
.into_iter()
121+
.filter(|v| !env_vars.contains(v))
122+
.collect();
123+
Scheme { vars, ty: resolved }
124+
}
125+
126+
/// Substitute the mapped type variables throughout a type.
127+
fn subst_vars(ty: &Type, m: &HashMap<u32, Type>) -> Type {
128+
match ty {
129+
Type::Var(id) => m.get(id).cloned().unwrap_or_else(|| ty.clone()),
130+
Type::Fun(a, b) => Type::Fun(
131+
Box::new(Self::subst_vars(a, m)),
132+
Box::new(Self::subst_vars(b, m)),
133+
),
134+
Type::Dist(t) => Type::Dist(Box::new(Self::subst_vars(t, m))),
135+
Type::List(t) => Type::List(Box::new(Self::subst_vars(t, m))),
136+
Type::Set(t) => Type::Set(Box::new(Self::subst_vars(t, m))),
137+
Type::Option(t) => Type::Option(Box::new(Self::subst_vars(t, m))),
138+
Type::Echo(t) => Type::Echo(Box::new(Self::subst_vars(t, m))),
139+
Type::EchoR(t) => Type::EchoR(Box::new(Self::subst_vars(t, m))),
140+
Type::Map(k, v) => Type::Map(
141+
Box::new(Self::subst_vars(k, m)),
142+
Box::new(Self::subst_vars(v, m)),
143+
),
144+
Type::Result(a, b) => Type::Result(
145+
Box::new(Self::subst_vars(a, m)),
146+
Box::new(Self::subst_vars(b, m)),
147+
),
148+
Type::Tuple(es) => Type::Tuple(es.iter().map(|e| Self::subst_vars(e, m)).collect()),
149+
_ => ty.clone(),
150+
}
151+
}
152+
153+
/// Collect the type variables appearing in a type.
154+
fn collect_vars(ty: &Type, out: &mut std::collections::BTreeSet<u32>) {
155+
match ty {
156+
Type::Var(id) => {
157+
out.insert(*id);
158+
}
159+
Type::Fun(a, b) => {
160+
Self::collect_vars(a, out);
161+
Self::collect_vars(b, out);
162+
}
163+
Type::Dist(t)
164+
| Type::List(t)
165+
| Type::Set(t)
166+
| Type::Option(t)
167+
| Type::Echo(t)
168+
| Type::EchoR(t) => Self::collect_vars(t, out),
169+
Type::Map(k, v) => {
170+
Self::collect_vars(k, out);
171+
Self::collect_vars(v, out);
172+
}
173+
Type::Result(a, b) => {
174+
Self::collect_vars(a, out);
175+
Self::collect_vars(b, out);
176+
}
177+
Type::Tuple(es) => {
178+
for e in es {
179+
Self::collect_vars(e, out);
180+
}
181+
}
182+
_ => {}
183+
}
184+
}
185+
186+
/// Type variables free in the environment (this scope and parents),
187+
/// excluding each binding's own quantified variables.
188+
fn env_free_vars(&self) -> std::collections::BTreeSet<u32> {
189+
let mut out = std::collections::BTreeSet::new();
190+
let mut cur = Some(self);
191+
while let Some(e) = cur {
192+
for scheme in e.bindings.values() {
193+
let resolved = self.resolve(&scheme.ty);
194+
let mut vs = std::collections::BTreeSet::new();
195+
Self::collect_vars(&resolved, &mut vs);
196+
for v in vs {
197+
if !scheme.vars.contains(&v) {
198+
out.insert(v);
199+
}
200+
}
201+
}
202+
cur = e.parent.as_deref();
203+
}
204+
out
205+
}
206+
82207
/// Generate a fresh type variable.
83208
pub fn fresh_var(&mut self) -> Type {
84209
let id = self.next_var;
@@ -245,11 +370,18 @@ fn seed_builtins(env: &mut CheckEnv) {
245370
"print".to_string(),
246371
Type::Fun(Box::new(Type::String), Box::new(Type::Unit)),
247372
);
248-
// to_string : 'a -> String (polymorphic, approximated with a var)
249-
let a = env.fresh_var();
250-
env.bind(
373+
// to_string : ∀a. a -> String (properly polymorphic via a type scheme,
374+
// so each use site instantiates a fresh carrier instead of sharing one var).
375+
let a_id = match env.fresh_var() {
376+
Type::Var(id) => id,
377+
_ => unreachable!("fresh_var always returns Type::Var"),
378+
};
379+
env.bind_scheme(
251380
"to_string".to_string(),
252-
Type::Fun(Box::new(a), Box::new(Type::String)),
381+
Scheme {
382+
vars: vec![a_id],
383+
ty: Type::Fun(Box::new(Type::Var(a_id)), Box::new(Type::String)),
384+
},
253385
);
254386
}
255387

@@ -356,7 +488,10 @@ fn check_item(item: &Item, env: &mut CheckEnv) -> CompileResult<()> {
356488
Item::Let(def) => {
357489
let ty = check_let_def(def, env)?;
358490
let name = def.name.node.to_string();
359-
env.bind(name, ty);
491+
// Let-generalization: a top-level binding is polymorphic over the
492+
// type variables free in its type but not free in the environment.
493+
let scheme = env.generalize(&ty);
494+
env.bind_scheme(name, scheme);
360495
Ok(())
361496
}
362497
Item::TypeDef(_) => {
@@ -414,8 +549,8 @@ fn check_let_def(def: &LetDef, env: &mut CheckEnv) -> CompileResult<Type> {
414549

415550
// For recursive defs, unify the placeholder with the result.
416551
if def.is_rec {
417-
if let Some(placeholder) = inner_env.lookup(&def.name.node.to_string()) {
418-
let placeholder = placeholder.clone();
552+
if let Some(scheme) = inner_env.lookup(&def.name.node.to_string()) {
553+
let placeholder = scheme.ty.clone();
419554
inner_env.unify(&placeholder, &result, Some(def.name.span))?;
420555
}
421556
}
@@ -457,8 +592,8 @@ fn check_expr(expr: &Spanned<Expr>, env: &mut CheckEnv) -> CompileResult<Type> {
457592
// shadowed by a user binding of the same name); otherwise fall back
458593
// to the polymorphic echo-type operations, which are freshly
459594
// instantiated per use site.
460-
if let Some(ty) = env.lookup(&n).cloned() {
461-
Ok(ty)
595+
if let Some(scheme) = env.lookup(&n).cloned() {
596+
Ok(env.instantiate(&scheme))
462597
} else if let Some(ty) = echo_builtin_type(&n, env) {
463598
Ok(ty)
464599
} else {
@@ -1621,4 +1756,47 @@ mod tests {
16211756
env.unify(&a, &Type::Fun(Box::new(b), Box::new(Type::Int)), None)
16221757
.expect("'a = ('b -> Int) is a finite type");
16231758
}
1759+
1760+
// ---- Type schemes / let-polymorphism (generalize + instantiate) ----
1761+
1762+
/// A generalized scheme instantiates independently at each use, so the same
1763+
/// polymorphic value can be applied at two different types in one scope.
1764+
#[test]
1765+
fn test_generalize_instantiate_independent() {
1766+
let mut env = CheckEnv::new();
1767+
let a = env.fresh_var();
1768+
let id_ty = Type::Fun(Box::new(a.clone()), Box::new(a));
1769+
let scheme = env.generalize(&id_ty);
1770+
assert_eq!(scheme.vars.len(), 1, "the free var should be generalized");
1771+
let i1 = env.instantiate(&scheme);
1772+
let i2 = env.instantiate(&scheme);
1773+
env.unify(&i1, &Type::Fun(Box::new(Type::Int), Box::new(Type::Int)), None)
1774+
.expect("first instance specialises to Int -> Int");
1775+
env.unify(
1776+
&i2,
1777+
&Type::Fun(Box::new(Type::String), Box::new(Type::String)),
1778+
None,
1779+
)
1780+
.expect("second instance specialises to String -> String independently");
1781+
}
1782+
1783+
/// The seeded `to_string : ∀a. a -> String` is genuinely polymorphic: it
1784+
/// type-checks at `Int` and at `String` in the same scope. (The old
1785+
/// shared-variable approximation pinned it to the first use and would have
1786+
/// rejected the second.)
1787+
#[test]
1788+
fn test_to_string_is_polymorphic_across_uses() {
1789+
let mut env = CheckEnv::new();
1790+
seed_builtins(&mut env);
1791+
let at_int = call("to_string", vec![dummy(Expr::Int(1))]);
1792+
assert_eq!(
1793+
check_expr(&at_int, &mut env).expect("to_string 1 : String"),
1794+
Type::String
1795+
);
1796+
let at_str = call("to_string", vec![dummy(Expr::String("x".into()))]);
1797+
assert_eq!(
1798+
check_expr(&at_str, &mut env).expect("to_string \"x\" : String"),
1799+
Type::String
1800+
);
1801+
}
16241802
}

0 commit comments

Comments
 (0)