seqc/unification.rs
1//! Type unification for Seq
2//!
3//! Implements Hindley-Milner style unification with support for:
4//! - Type variables (T, U, V)
5//! - Row variables (..a, ..rest)
6//! - Concrete types (Int, Bool, String)
7
8use crate::types::{StackType, Type};
9use std::collections::HashMap;
10
11/// Substitutions for type variables
12pub type TypeSubst = HashMap<String, Type>;
13
14/// Substitutions for row variables (stack type variables)
15pub type RowSubst = HashMap<String, StackType>;
16
17/// Combined substitution environment
18#[derive(Debug, Clone, PartialEq)]
19pub struct Subst {
20 pub types: TypeSubst,
21 pub rows: RowSubst,
22}
23
24impl Subst {
25 /// Create an empty substitution
26 pub fn empty() -> Self {
27 Subst {
28 types: HashMap::new(),
29 rows: HashMap::new(),
30 }
31 }
32
33 /// Apply substitutions to a Type
34 pub fn apply_type(&self, ty: &Type) -> Type {
35 match ty {
36 Type::Var(name) => self.types.get(name).cloned().unwrap_or(ty.clone()),
37 _ => ty.clone(),
38 }
39 }
40
41 /// Apply substitutions to a StackType
42 pub fn apply_stack(&self, stack: &StackType) -> StackType {
43 match stack {
44 StackType::Empty => StackType::Empty,
45 StackType::Cons { rest, top } => {
46 let new_rest = self.apply_stack(rest);
47 let new_top = self.apply_type(top);
48 StackType::Cons {
49 rest: Box::new(new_rest),
50 top: new_top,
51 }
52 }
53 StackType::RowVar(name) => self.rows.get(name).cloned().unwrap_or(stack.clone()),
54 }
55 }
56
57 /// Compose two substitutions (apply other after self)
58 /// Result: (other ∘ self) where self is applied first, then other
59 pub fn compose(&self, other: &Subst) -> Subst {
60 let mut types = HashMap::new();
61 let mut rows = HashMap::new();
62
63 // Apply other to all of self's type substitutions
64 for (k, v) in &self.types {
65 types.insert(k.clone(), other.apply_type(v));
66 }
67
68 // Add other's type substitutions (applying self to other's values)
69 for (k, v) in &other.types {
70 let v_subst = self.apply_type(v);
71 types.insert(k.clone(), v_subst);
72 }
73
74 // Apply other to all of self's row substitutions
75 for (k, v) in &self.rows {
76 rows.insert(k.clone(), other.apply_stack(v));
77 }
78
79 // Add other's row substitutions (applying self to other's values)
80 for (k, v) in &other.rows {
81 let v_subst = self.apply_stack(v);
82 rows.insert(k.clone(), v_subst);
83 }
84
85 Subst { types, rows }
86 }
87}
88
89/// Check if a type variable occurs in a type (for occurs check)
90///
91/// Prevents infinite types like: T = List<T>
92///
93/// NOTE: Currently we only have simple types (Int, String, Bool).
94/// When parametric types are added (e.g., List<T>, Option<T>), this function
95/// must be extended to recursively check type arguments:
96///
97/// ```ignore
98/// Type::Named { name: _, args } => {
99/// args.iter().any(|arg| occurs_in_type(var, arg))
100/// }
101/// ```
102fn occurs_in_type(var: &str, ty: &Type) -> bool {
103 match ty {
104 Type::Var(name) => name == var,
105 // Concrete types contain no type variables
106 Type::Int
107 | Type::Float
108 | Type::Bool
109 | Type::String
110 | Type::Symbol
111 | Type::Channel
112 | Type::Socket
113 | Type::Union(_)
114 | Type::Variant => false,
115 Type::Quotation(effect) => {
116 // Check if var occurs in quotation's input or output stack types
117 occurs_in_stack(var, &effect.inputs) || occurs_in_stack(var, &effect.outputs)
118 }
119 Type::Closure { effect, captures } => {
120 // Check if var occurs in closure's effect or any captured types
121 occurs_in_stack(var, &effect.inputs)
122 || occurs_in_stack(var, &effect.outputs)
123 || captures.iter().any(|t| occurs_in_type(var, t))
124 }
125 }
126}
127
128/// Check if a row variable occurs in a stack type (for occurs check)
129fn occurs_in_stack(var: &str, stack: &StackType) -> bool {
130 match stack {
131 StackType::Empty => false,
132 StackType::RowVar(name) => name == var,
133 StackType::Cons { rest, top: _ } => {
134 // Row variables only occur in stack positions, not in type positions
135 // So we only need to check the rest of the stack
136 occurs_in_stack(var, rest)
137 }
138 }
139}
140
141/// Unify two types, returning a substitution or an error
142pub fn unify_types(t1: &Type, t2: &Type) -> Result<Subst, String> {
143 match (t1, t2) {
144 // Same concrete types unify
145 (Type::Int, Type::Int)
146 | (Type::Float, Type::Float)
147 | (Type::Bool, Type::Bool)
148 | (Type::String, Type::String)
149 | (Type::Symbol, Type::Symbol)
150 | (Type::Channel, Type::Channel)
151 | (Type::Socket, Type::Socket) => Ok(Subst::empty()),
152
153 // Union types unify if they have the same name
154 (Type::Union(name1), Type::Union(name2)) => {
155 if name1 == name2 {
156 Ok(Subst::empty())
157 } else {
158 Err(format!(
159 "Type mismatch: cannot unify Union({}) with Union({})",
160 name1, name2
161 ))
162 }
163 }
164
165 // Variant matches itself
166 (Type::Variant, Type::Variant) => Ok(Subst::empty()),
167
168 // Union <: Variant relaxation — a named union value is a variant.
169 // This lets `variant.*` builtins (typed against `Variant`) accept
170 // user values typed as `Union(name)` without losing union safety
171 // elsewhere: the rule applies only when one side is the bare
172 // `Variant` placeholder. Mirrors the Closure <: Quotation rule
173 // below; the symmetric form is a minor unsoundness in the reverse
174 // direction (a `Variant` flowing back into a `Union(name)` slot)
175 // that we accept for now.
176 //
177 // TODO: tighten to a directional rule once the typechecker tracks
178 // which side of a unification is "expected" vs "actual". Today a
179 // `Variant` (e.g. the result of `variant.append`) silently
180 // satisfies a `Union(name)` constraint without checking the tag —
181 // intended pragmatic loophole, not a permanent stance.
182 (Type::Union(_), Type::Variant) | (Type::Variant, Type::Union(_)) => Ok(Subst::empty()),
183
184 // Type variable unifies with anything (with occurs check)
185 (Type::Var(name), ty) | (ty, Type::Var(name)) => {
186 // If unifying a variable with itself, no substitution needed
187 if matches!(ty, Type::Var(ty_name) if ty_name == name) {
188 return Ok(Subst::empty());
189 }
190
191 // Occurs check: prevent infinite types
192 if occurs_in_type(name, ty) {
193 return Err(format!(
194 "Occurs check failed: cannot unify {:?} with {:?} (would create infinite type)",
195 Type::Var(name.clone()),
196 ty
197 ));
198 }
199
200 let mut subst = Subst::empty();
201 subst.types.insert(name.clone(), ty.clone());
202 Ok(subst)
203 }
204
205 // Quotation types unify if their effects unify
206 (Type::Quotation(effect1), Type::Quotation(effect2)) => {
207 // Unify inputs
208 let s_in = unify_stacks(&effect1.inputs, &effect2.inputs)?;
209
210 // Apply substitution to outputs and unify
211 let out1 = s_in.apply_stack(&effect1.outputs);
212 let out2 = s_in.apply_stack(&effect2.outputs);
213 let s_out = unify_stacks(&out1, &out2)?;
214
215 // Compose substitutions
216 Ok(s_in.compose(&s_out))
217 }
218
219 // Closure types unify if their effects unify (ignoring captures)
220 // Captures are an implementation detail determined by the type checker,
221 // not part of the user-visible type
222 (
223 Type::Closure {
224 effect: effect1, ..
225 },
226 Type::Closure {
227 effect: effect2, ..
228 },
229 ) => {
230 // Unify inputs
231 let s_in = unify_stacks(&effect1.inputs, &effect2.inputs)?;
232
233 // Apply substitution to outputs and unify
234 let out1 = s_in.apply_stack(&effect1.outputs);
235 let out2 = s_in.apply_stack(&effect2.outputs);
236 let s_out = unify_stacks(&out1, &out2)?;
237
238 // Compose substitutions
239 Ok(s_in.compose(&s_out))
240 }
241
242 // Closure <: Quotation (subtyping)
243 // A Closure can be used where a Quotation is expected
244 // The runtime will dispatch appropriately
245 (Type::Quotation(quot_effect), Type::Closure { effect, .. })
246 | (Type::Closure { effect, .. }, Type::Quotation(quot_effect)) => {
247 // Unify the effects (ignoring captures - they're an implementation detail)
248 let s_in = unify_stacks("_effect.inputs, &effect.inputs)?;
249
250 // Apply substitution to outputs and unify
251 let out1 = s_in.apply_stack("_effect.outputs);
252 let out2 = s_in.apply_stack(&effect.outputs);
253 let s_out = unify_stacks(&out1, &out2)?;
254
255 // Compose substitutions
256 Ok(s_in.compose(&s_out))
257 }
258
259 // Different concrete types don't unify
260 _ => Err(format!("Type mismatch: cannot unify {} with {}", t1, t2)),
261 }
262}
263
264/// Unify two stack types, returning a substitution or an error
265pub fn unify_stacks(s1: &StackType, s2: &StackType) -> Result<Subst, String> {
266 match (s1, s2) {
267 // Empty stacks unify
268 (StackType::Empty, StackType::Empty) => Ok(Subst::empty()),
269
270 // Row variable unifies with any stack (with occurs check)
271 (StackType::RowVar(name), stack) | (stack, StackType::RowVar(name)) => {
272 // If unifying a row var with itself, no substitution needed
273 if matches!(stack, StackType::RowVar(stack_name) if stack_name == name) {
274 return Ok(Subst::empty());
275 }
276
277 // Occurs check: prevent infinite stack types
278 if occurs_in_stack(name, stack) {
279 return Err(format!(
280 "Occurs check failed: cannot unify {} with {} (would create infinite stack type)",
281 StackType::RowVar(name.clone()),
282 stack
283 ));
284 }
285
286 let mut subst = Subst::empty();
287 subst.rows.insert(name.clone(), stack.clone());
288 Ok(subst)
289 }
290
291 // Cons cells unify if tops and rests unify
292 (
293 StackType::Cons {
294 rest: rest1,
295 top: top1,
296 },
297 StackType::Cons {
298 rest: rest2,
299 top: top2,
300 },
301 ) => {
302 // Unify the tops
303 let s_top = unify_types(top1, top2)?;
304
305 // Apply substitution to rests and unify
306 let rest1_subst = s_top.apply_stack(rest1);
307 let rest2_subst = s_top.apply_stack(rest2);
308 let s_rest = unify_stacks(&rest1_subst, &rest2_subst)?;
309
310 // Compose substitutions
311 Ok(s_top.compose(&s_rest))
312 }
313
314 // Empty doesn't unify with Cons
315 _ => Err(format!(
316 "Stack shape mismatch: cannot unify {} with {}",
317 s1, s2
318 )),
319 }
320}
321
322#[cfg(test)]
323mod tests;