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::Union(_) => false,
113        Type::Quotation(effect) => {
114            // Check if var occurs in quotation's input or output stack types
115            occurs_in_stack(var, &effect.inputs) || occurs_in_stack(var, &effect.outputs)
116        }
117        Type::Closure { effect, captures } => {
118            // Check if var occurs in closure's effect or any captured types
119            occurs_in_stack(var, &effect.inputs)
120                || occurs_in_stack(var, &effect.outputs)
121                || captures.iter().any(|t| occurs_in_type(var, t))
122        }
123    }
124}
125
126/// Check if a row variable occurs in a stack type (for occurs check)
127fn occurs_in_stack(var: &str, stack: &StackType) -> bool {
128    match stack {
129        StackType::Empty => false,
130        StackType::RowVar(name) => name == var,
131        StackType::Cons { rest, top: _ } => {
132            // Row variables only occur in stack positions, not in type positions
133            // So we only need to check the rest of the stack
134            occurs_in_stack(var, rest)
135        }
136    }
137}
138
139/// Unify two types, returning a substitution or an error
140pub fn unify_types(t1: &Type, t2: &Type) -> Result<Subst, String> {
141    match (t1, t2) {
142        // Same concrete types unify
143        (Type::Int, Type::Int)
144        | (Type::Float, Type::Float)
145        | (Type::Bool, Type::Bool)
146        | (Type::String, Type::String)
147        | (Type::Symbol, Type::Symbol)
148        | (Type::Channel, Type::Channel) => Ok(Subst::empty()),
149
150        // Union types unify if they have the same name
151        (Type::Union(name1), Type::Union(name2)) => {
152            if name1 == name2 {
153                Ok(Subst::empty())
154            } else {
155                Err(format!(
156                    "Type mismatch: cannot unify Union({}) with Union({})",
157                    name1, name2
158                ))
159            }
160        }
161
162        // Type variable unifies with anything (with occurs check)
163        (Type::Var(name), ty) | (ty, Type::Var(name)) => {
164            // If unifying a variable with itself, no substitution needed
165            if matches!(ty, Type::Var(ty_name) if ty_name == name) {
166                return Ok(Subst::empty());
167            }
168
169            // Occurs check: prevent infinite types
170            if occurs_in_type(name, ty) {
171                return Err(format!(
172                    "Occurs check failed: cannot unify {:?} with {:?} (would create infinite type)",
173                    Type::Var(name.clone()),
174                    ty
175                ));
176            }
177
178            let mut subst = Subst::empty();
179            subst.types.insert(name.clone(), ty.clone());
180            Ok(subst)
181        }
182
183        // Quotation types unify if their effects unify
184        (Type::Quotation(effect1), Type::Quotation(effect2)) => {
185            // Unify inputs
186            let s_in = unify_stacks(&effect1.inputs, &effect2.inputs)?;
187
188            // Apply substitution to outputs and unify
189            let out1 = s_in.apply_stack(&effect1.outputs);
190            let out2 = s_in.apply_stack(&effect2.outputs);
191            let s_out = unify_stacks(&out1, &out2)?;
192
193            // Compose substitutions
194            Ok(s_in.compose(&s_out))
195        }
196
197        // Closure types unify if their effects unify (ignoring captures)
198        // Captures are an implementation detail determined by the type checker,
199        // not part of the user-visible type
200        (
201            Type::Closure {
202                effect: effect1, ..
203            },
204            Type::Closure {
205                effect: effect2, ..
206            },
207        ) => {
208            // Unify inputs
209            let s_in = unify_stacks(&effect1.inputs, &effect2.inputs)?;
210
211            // Apply substitution to outputs and unify
212            let out1 = s_in.apply_stack(&effect1.outputs);
213            let out2 = s_in.apply_stack(&effect2.outputs);
214            let s_out = unify_stacks(&out1, &out2)?;
215
216            // Compose substitutions
217            Ok(s_in.compose(&s_out))
218        }
219
220        // Closure <: Quotation (subtyping)
221        // A Closure can be used where a Quotation is expected
222        // The runtime will dispatch appropriately
223        (Type::Quotation(quot_effect), Type::Closure { effect, .. })
224        | (Type::Closure { effect, .. }, Type::Quotation(quot_effect)) => {
225            // Unify the effects (ignoring captures - they're an implementation detail)
226            let s_in = unify_stacks(&quot_effect.inputs, &effect.inputs)?;
227
228            // Apply substitution to outputs and unify
229            let out1 = s_in.apply_stack(&quot_effect.outputs);
230            let out2 = s_in.apply_stack(&effect.outputs);
231            let s_out = unify_stacks(&out1, &out2)?;
232
233            // Compose substitutions
234            Ok(s_in.compose(&s_out))
235        }
236
237        // Different concrete types don't unify
238        _ => Err(format!("Type mismatch: cannot unify {} with {}", t1, t2)),
239    }
240}
241
242/// Unify two stack types, returning a substitution or an error
243pub fn unify_stacks(s1: &StackType, s2: &StackType) -> Result<Subst, String> {
244    match (s1, s2) {
245        // Empty stacks unify
246        (StackType::Empty, StackType::Empty) => Ok(Subst::empty()),
247
248        // Row variable unifies with any stack (with occurs check)
249        (StackType::RowVar(name), stack) | (stack, StackType::RowVar(name)) => {
250            // If unifying a row var with itself, no substitution needed
251            if matches!(stack, StackType::RowVar(stack_name) if stack_name == name) {
252                return Ok(Subst::empty());
253            }
254
255            // Occurs check: prevent infinite stack types
256            if occurs_in_stack(name, stack) {
257                return Err(format!(
258                    "Occurs check failed: cannot unify {} with {} (would create infinite stack type)",
259                    StackType::RowVar(name.clone()),
260                    stack
261                ));
262            }
263
264            let mut subst = Subst::empty();
265            subst.rows.insert(name.clone(), stack.clone());
266            Ok(subst)
267        }
268
269        // Cons cells unify if tops and rests unify
270        (
271            StackType::Cons {
272                rest: rest1,
273                top: top1,
274            },
275            StackType::Cons {
276                rest: rest2,
277                top: top2,
278            },
279        ) => {
280            // Unify the tops
281            let s_top = unify_types(top1, top2)?;
282
283            // Apply substitution to rests and unify
284            let rest1_subst = s_top.apply_stack(rest1);
285            let rest2_subst = s_top.apply_stack(rest2);
286            let s_rest = unify_stacks(&rest1_subst, &rest2_subst)?;
287
288            // Compose substitutions
289            Ok(s_top.compose(&s_rest))
290        }
291
292        // Empty doesn't unify with Cons
293        _ => Err(format!(
294            "Stack shape mismatch: cannot unify {} with {}",
295            s1, s2
296        )),
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303
304    #[test]
305    fn test_unify_concrete_types() {
306        assert!(unify_types(&Type::Int, &Type::Int).is_ok());
307        assert!(unify_types(&Type::Bool, &Type::Bool).is_ok());
308        assert!(unify_types(&Type::String, &Type::String).is_ok());
309
310        assert!(unify_types(&Type::Int, &Type::Bool).is_err());
311    }
312
313    #[test]
314    fn test_unify_type_variable() {
315        let subst = unify_types(&Type::Var("T".to_string()), &Type::Int).unwrap();
316        assert_eq!(subst.types.get("T"), Some(&Type::Int));
317
318        let subst = unify_types(&Type::Bool, &Type::Var("U".to_string())).unwrap();
319        assert_eq!(subst.types.get("U"), Some(&Type::Bool));
320    }
321
322    #[test]
323    fn test_unify_empty_stacks() {
324        assert!(unify_stacks(&StackType::Empty, &StackType::Empty).is_ok());
325    }
326
327    #[test]
328    fn test_unify_row_variable() {
329        let subst = unify_stacks(
330            &StackType::RowVar("a".to_string()),
331            &StackType::singleton(Type::Int),
332        )
333        .unwrap();
334
335        assert_eq!(subst.rows.get("a"), Some(&StackType::singleton(Type::Int)));
336    }
337
338    #[test]
339    fn test_unify_cons_stacks() {
340        // ( Int ) unifies with ( Int )
341        let s1 = StackType::singleton(Type::Int);
342        let s2 = StackType::singleton(Type::Int);
343
344        assert!(unify_stacks(&s1, &s2).is_ok());
345    }
346
347    #[test]
348    fn test_unify_cons_with_type_var() {
349        // ( T ) unifies with ( Int ), producing T := Int
350        let s1 = StackType::singleton(Type::Var("T".to_string()));
351        let s2 = StackType::singleton(Type::Int);
352
353        let subst = unify_stacks(&s1, &s2).unwrap();
354        assert_eq!(subst.types.get("T"), Some(&Type::Int));
355    }
356
357    #[test]
358    fn test_unify_row_poly_stack() {
359        // ( ..a Int ) unifies with ( Bool Int ), producing ..a := ( Bool )
360        let s1 = StackType::RowVar("a".to_string()).push(Type::Int);
361        let s2 = StackType::Empty.push(Type::Bool).push(Type::Int);
362
363        let subst = unify_stacks(&s1, &s2).unwrap();
364
365        assert_eq!(subst.rows.get("a"), Some(&StackType::singleton(Type::Bool)));
366    }
367
368    #[test]
369    fn test_unify_polymorphic_dup() {
370        // dup: ( ..a T -- ..a T T )
371        // Applied to: ( Int ) should work with ..a := Empty, T := Int
372
373        let input_actual = StackType::singleton(Type::Int);
374        let input_declared = StackType::RowVar("a".to_string()).push(Type::Var("T".to_string()));
375
376        let subst = unify_stacks(&input_declared, &input_actual).unwrap();
377
378        assert_eq!(subst.rows.get("a"), Some(&StackType::Empty));
379        assert_eq!(subst.types.get("T"), Some(&Type::Int));
380
381        // Apply substitution to output: ( ..a T T )
382        let output_declared = StackType::RowVar("a".to_string())
383            .push(Type::Var("T".to_string()))
384            .push(Type::Var("T".to_string()));
385
386        let output_actual = subst.apply_stack(&output_declared);
387
388        // Should be ( Int Int )
389        assert_eq!(
390            output_actual,
391            StackType::Empty.push(Type::Int).push(Type::Int)
392        );
393    }
394
395    #[test]
396    fn test_subst_compose() {
397        // s1: T := Int
398        let mut s1 = Subst::empty();
399        s1.types.insert("T".to_string(), Type::Int);
400
401        // s2: U := T
402        let mut s2 = Subst::empty();
403        s2.types.insert("U".to_string(), Type::Var("T".to_string()));
404
405        // Compose: should give U := Int, T := Int
406        let composed = s1.compose(&s2);
407
408        assert_eq!(composed.types.get("T"), Some(&Type::Int));
409        assert_eq!(composed.types.get("U"), Some(&Type::Int));
410    }
411
412    #[test]
413    fn test_occurs_check_type_var_with_itself() {
414        // Unifying T with T should succeed (no substitution needed)
415        let result = unify_types(&Type::Var("T".to_string()), &Type::Var("T".to_string()));
416        assert!(result.is_ok());
417        let subst = result.unwrap();
418        // Should be empty - no substitution needed when unifying var with itself
419        assert!(subst.types.is_empty());
420    }
421
422    #[test]
423    fn test_occurs_check_row_var_with_itself() {
424        // Unifying ..a with ..a should succeed (no substitution needed)
425        let result = unify_stacks(
426            &StackType::RowVar("a".to_string()),
427            &StackType::RowVar("a".to_string()),
428        );
429        assert!(result.is_ok());
430        let subst = result.unwrap();
431        // Should be empty - no substitution needed when unifying var with itself
432        assert!(subst.rows.is_empty());
433    }
434
435    #[test]
436    fn test_occurs_check_prevents_infinite_stack() {
437        // Attempting to unify ..a with (..a Int) should fail
438        // This would create an infinite type: ..a = (..a Int) = ((..a Int) Int) = ...
439        let row_var = StackType::RowVar("a".to_string());
440        let infinite_stack = StackType::RowVar("a".to_string()).push(Type::Int);
441
442        let result = unify_stacks(&row_var, &infinite_stack);
443        assert!(result.is_err());
444        let err = result.unwrap_err();
445        assert!(err.contains("Occurs check failed"));
446        assert!(err.contains("infinite"));
447    }
448
449    #[test]
450    fn test_occurs_check_allows_different_row_vars() {
451        // Unifying ..a with ..b should succeed (different variables)
452        let result = unify_stacks(
453            &StackType::RowVar("a".to_string()),
454            &StackType::RowVar("b".to_string()),
455        );
456        assert!(result.is_ok());
457        let subst = result.unwrap();
458        assert_eq!(
459            subst.rows.get("a"),
460            Some(&StackType::RowVar("b".to_string()))
461        );
462    }
463
464    #[test]
465    fn test_occurs_check_allows_concrete_stack() {
466        // Unifying ..a with (Int String) should succeed (no occurs)
467        let row_var = StackType::RowVar("a".to_string());
468        let concrete = StackType::Empty.push(Type::Int).push(Type::String);
469
470        let result = unify_stacks(&row_var, &concrete);
471        assert!(result.is_ok());
472        let subst = result.unwrap();
473        assert_eq!(subst.rows.get("a"), Some(&concrete));
474    }
475
476    #[test]
477    fn test_occurs_in_type() {
478        // T occurs in T
479        assert!(occurs_in_type("T", &Type::Var("T".to_string())));
480
481        // T does not occur in U
482        assert!(!occurs_in_type("T", &Type::Var("U".to_string())));
483
484        // T does not occur in Int
485        assert!(!occurs_in_type("T", &Type::Int));
486        assert!(!occurs_in_type("T", &Type::String));
487        assert!(!occurs_in_type("T", &Type::Bool));
488    }
489
490    #[test]
491    fn test_occurs_in_stack() {
492        // ..a occurs in ..a
493        assert!(occurs_in_stack("a", &StackType::RowVar("a".to_string())));
494
495        // ..a does not occur in ..b
496        assert!(!occurs_in_stack("a", &StackType::RowVar("b".to_string())));
497
498        // ..a does not occur in Empty
499        assert!(!occurs_in_stack("a", &StackType::Empty));
500
501        // ..a occurs in (..a Int)
502        let stack = StackType::RowVar("a".to_string()).push(Type::Int);
503        assert!(occurs_in_stack("a", &stack));
504
505        // ..a does not occur in (..b Int)
506        let stack = StackType::RowVar("b".to_string()).push(Type::Int);
507        assert!(!occurs_in_stack("a", &stack));
508
509        // ..a does not occur in (Int String)
510        let stack = StackType::Empty.push(Type::Int).push(Type::String);
511        assert!(!occurs_in_stack("a", &stack));
512    }
513
514    #[test]
515    fn test_quotation_type_unification_stack_neutral() {
516        // Q[..a -- ..a] should NOT unify with Q[..b -- ..b Int]
517        // because the second quotation pushes a value
518        use crate::types::Effect;
519
520        let stack_neutral = Type::Quotation(Box::new(Effect::new(
521            StackType::RowVar("a".to_string()),
522            StackType::RowVar("a".to_string()),
523        )));
524
525        let pushes_int = Type::Quotation(Box::new(Effect::new(
526            StackType::RowVar("b".to_string()),
527            StackType::RowVar("b".to_string()).push(Type::Int),
528        )));
529
530        let result = unify_types(&stack_neutral, &pushes_int);
531        // This SHOULD fail because unifying outputs would require ..a = ..a Int
532        // which is an infinite type
533        assert!(
534            result.is_err(),
535            "Unifying stack-neutral with stack-pushing quotation should fail, got {:?}",
536            result
537        );
538    }
539}