Skip to content

Commit 431420f

Browse files
claudehyperpolymath
authored andcommitted
fix(check): occurs check in unify (reject infinite types)
unify previously bound a type variable to any type without checking whether the variable occurred inside it, so `'a = 'a -> Int` (or `'a = Echo 'a`) was accepted and made `resolve` recurse forever. Adds `CheckEnv::occurs(id, ty)`, which walks the resolved type through every former (Fun, Dist, List, Set, Option, Map, Result, Tuple, Echo, EchoR), and refuses the binding with a clear error when the variable occurs. 3 new tests (infinite via Fun, infinite via Echo, distinct-vars-still-unify); 41 bet-check tests pass; lib is clippy-clean. First half of the checker-hardening pass; real type schemes (generalise / instantiate at let-boundaries) follow. https://claude.ai/code/session_01QGi8GND5yNWgDyfReVEPYs
1 parent a4e1866 commit 431420f

1 file changed

Lines changed: 74 additions & 1 deletion

File tree

compiler/bet-check/src/lib.rs

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,26 @@ impl CheckEnv {
118118
}
119119
}
120120

121+
/// Occurs check: does the type variable `id` appear anywhere inside `ty`
122+
/// (after resolution)? Used by `unify` to refuse constructing an infinite
123+
/// type (e.g. `'a = 'a -> Int`), which would otherwise make `resolve` loop.
124+
fn occurs(&self, id: u32, ty: &Type) -> bool {
125+
match self.resolve(ty) {
126+
Type::Var(v) => v == id,
127+
Type::Fun(a, b) => self.occurs(id, &a) || self.occurs(id, &b),
128+
Type::Dist(t)
129+
| Type::List(t)
130+
| Type::Set(t)
131+
| Type::Option(t)
132+
| Type::Echo(t)
133+
| Type::EchoR(t) => self.occurs(id, &t),
134+
Type::Map(k, v) => self.occurs(id, &k) || self.occurs(id, &v),
135+
Type::Result(a, b) => self.occurs(id, &a) || self.occurs(id, &b),
136+
Type::Tuple(elems) => elems.iter().any(|e| self.occurs(id, e)),
137+
_ => false,
138+
}
139+
}
140+
121141
/// Unify two types, updating substitutions. Returns an error on mismatch.
122142
pub fn unify(&mut self, a: &Type, b: &Type, span: Option<Span>) -> CompileResult<()> {
123143
let a = self.resolve(a);
@@ -127,12 +147,27 @@ impl CheckEnv {
127147
// Identical types always unify.
128148
_ if a == b => Ok(()),
129149

130-
// A type variable unifies with anything (occurs check omitted for simplicity).
150+
// A type variable unifies with anything *except* a type that
151+
// contains it (occurs check) — that would be an infinite type.
131152
(Type::Var(id), _) => {
153+
if self.occurs(*id, &b) {
154+
return Err(CompileError::TypeMismatch {
155+
expected: format!("a finite type not containing '{}", id),
156+
found: format!("{:?}", b),
157+
span,
158+
});
159+
}
132160
self.substitutions.insert(*id, b);
133161
Ok(())
134162
}
135163
(_, Type::Var(id)) => {
164+
if self.occurs(*id, &a) {
165+
return Err(CompileError::TypeMismatch {
166+
expected: format!("a finite type not containing '{}", id),
167+
found: format!("{:?}", a),
168+
span,
169+
});
170+
}
136171
self.substitutions.insert(*id, a);
137172
Ok(())
138173
}
@@ -1548,4 +1583,42 @@ mod tests {
15481583
Type::Echo(Box::new(Type::Int))
15491584
);
15501585
}
1586+
1587+
// ---- Occurs check (no infinite types; unify must not loop) ----
1588+
1589+
/// Unifying `'a` with a type that contains `'a` is rejected as an infinite
1590+
/// type, rather than being silently accepted (which would make `resolve`
1591+
/// loop).
1592+
#[test]
1593+
fn test_occurs_check_rejects_infinite_type() {
1594+
let mut env = CheckEnv::new();
1595+
let a = env.fresh_var();
1596+
let result = env.unify(
1597+
&a,
1598+
&Type::Fun(Box::new(a.clone()), Box::new(Type::Int)),
1599+
None,
1600+
);
1601+
assert!(result.is_err(), "'a = ('a -> Int) must be rejected");
1602+
}
1603+
1604+
/// The occurs check sees through the Echo former too (`'a = Echo 'a`).
1605+
#[test]
1606+
fn test_occurs_check_through_echo() {
1607+
let mut env = CheckEnv::new();
1608+
let a = env.fresh_var();
1609+
assert!(env
1610+
.unify(&a, &Type::Echo(Box::new(a.clone())), None)
1611+
.is_err());
1612+
}
1613+
1614+
/// Two *distinct* variables unify fine even when one is nested — the occurs
1615+
/// check must not reject legitimate bindings.
1616+
#[test]
1617+
fn test_occurs_check_allows_distinct_vars() {
1618+
let mut env = CheckEnv::new();
1619+
let a = env.fresh_var();
1620+
let b = env.fresh_var();
1621+
env.unify(&a, &Type::Fun(Box::new(b), Box::new(Type::Int)), None)
1622+
.expect("'a = ('b -> Int) is a finite type");
1623+
}
15511624
}

0 commit comments

Comments
 (0)