Skip to main content

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(_)
113        | Type::Variant => false,
114        Type::Quotation(effect) => {
115            // Check if var occurs in quotation's input or output stack types
116            occurs_in_stack(var, &effect.inputs) || occurs_in_stack(var, &effect.outputs)
117        }
118        Type::Closure { effect, captures } => {
119            // Check if var occurs in closure's effect or any captured types
120            occurs_in_stack(var, &effect.inputs)
121                || occurs_in_stack(var, &effect.outputs)
122                || captures.iter().any(|t| occurs_in_type(var, t))
123        }
124    }
125}
126
127/// Check if a row variable occurs in a stack type (for occurs check)
128fn occurs_in_stack(var: &str, stack: &StackType) -> bool {
129    match stack {
130        StackType::Empty => false,
131        StackType::RowVar(name) => name == var,
132        StackType::Cons { rest, top: _ } => {
133            // Row variables only occur in stack positions, not in type positions
134            // So we only need to check the rest of the stack
135            occurs_in_stack(var, rest)
136        }
137    }
138}
139
140/// Unify two types, returning a substitution or an error
141pub fn unify_types(t1: &Type, t2: &Type) -> Result<Subst, String> {
142    match (t1, t2) {
143        // Same concrete types unify
144        (Type::Int, Type::Int)
145        | (Type::Float, Type::Float)
146        | (Type::Bool, Type::Bool)
147        | (Type::String, Type::String)
148        | (Type::Symbol, Type::Symbol)
149        | (Type::Channel, Type::Channel) => Ok(Subst::empty()),
150
151        // Union types unify if they have the same name
152        (Type::Union(name1), Type::Union(name2)) => {
153            if name1 == name2 {
154                Ok(Subst::empty())
155            } else {
156                Err(format!(
157                    "Type mismatch: cannot unify Union({}) with Union({})",
158                    name1, name2
159                ))
160            }
161        }
162
163        // Variant matches itself
164        (Type::Variant, Type::Variant) => Ok(Subst::empty()),
165
166        // Union <: Variant relaxation — a named union value is a variant.
167        // This lets `variant.*` builtins (typed against `Variant`) accept
168        // user values typed as `Union(name)` without losing union safety
169        // elsewhere: the rule applies only when one side is the bare
170        // `Variant` placeholder. Mirrors the Closure <: Quotation rule
171        // below; the symmetric form is a minor unsoundness in the reverse
172        // direction (a `Variant` flowing back into a `Union(name)` slot)
173        // that we accept for now.
174        //
175        // TODO: tighten to a directional rule once the typechecker tracks
176        // which side of a unification is "expected" vs "actual". Today a
177        // `Variant` (e.g. the result of `variant.append`) silently
178        // satisfies a `Union(name)` constraint without checking the tag —
179        // intended pragmatic loophole, not a permanent stance.
180        (Type::Union(_), Type::Variant) | (Type::Variant, Type::Union(_)) => Ok(Subst::empty()),
181
182        // Type variable unifies with anything (with occurs check)
183        (Type::Var(name), ty) | (ty, Type::Var(name)) => {
184            // If unifying a variable with itself, no substitution needed
185            if matches!(ty, Type::Var(ty_name) if ty_name == name) {
186                return Ok(Subst::empty());
187            }
188
189            // Occurs check: prevent infinite types
190            if occurs_in_type(name, ty) {
191                return Err(format!(
192                    "Occurs check failed: cannot unify {:?} with {:?} (would create infinite type)",
193                    Type::Var(name.clone()),
194                    ty
195                ));
196            }
197
198            let mut subst = Subst::empty();
199            subst.types.insert(name.clone(), ty.clone());
200            Ok(subst)
201        }
202
203        // Quotation types unify if their effects unify
204        (Type::Quotation(effect1), Type::Quotation(effect2)) => {
205            // Unify inputs
206            let s_in = unify_stacks(&effect1.inputs, &effect2.inputs)?;
207
208            // Apply substitution to outputs and unify
209            let out1 = s_in.apply_stack(&effect1.outputs);
210            let out2 = s_in.apply_stack(&effect2.outputs);
211            let s_out = unify_stacks(&out1, &out2)?;
212
213            // Compose substitutions
214            Ok(s_in.compose(&s_out))
215        }
216
217        // Closure types unify if their effects unify (ignoring captures)
218        // Captures are an implementation detail determined by the type checker,
219        // not part of the user-visible type
220        (
221            Type::Closure {
222                effect: effect1, ..
223            },
224            Type::Closure {
225                effect: effect2, ..
226            },
227        ) => {
228            // Unify inputs
229            let s_in = unify_stacks(&effect1.inputs, &effect2.inputs)?;
230
231            // Apply substitution to outputs and unify
232            let out1 = s_in.apply_stack(&effect1.outputs);
233            let out2 = s_in.apply_stack(&effect2.outputs);
234            let s_out = unify_stacks(&out1, &out2)?;
235
236            // Compose substitutions
237            Ok(s_in.compose(&s_out))
238        }
239
240        // Closure <: Quotation (subtyping)
241        // A Closure can be used where a Quotation is expected
242        // The runtime will dispatch appropriately
243        (Type::Quotation(quot_effect), Type::Closure { effect, .. })
244        | (Type::Closure { effect, .. }, Type::Quotation(quot_effect)) => {
245            // Unify the effects (ignoring captures - they're an implementation detail)
246            let s_in = unify_stacks(&quot_effect.inputs, &effect.inputs)?;
247
248            // Apply substitution to outputs and unify
249            let out1 = s_in.apply_stack(&quot_effect.outputs);
250            let out2 = s_in.apply_stack(&effect.outputs);
251            let s_out = unify_stacks(&out1, &out2)?;
252
253            // Compose substitutions
254            Ok(s_in.compose(&s_out))
255        }
256
257        // Different concrete types don't unify
258        _ => Err(format!("Type mismatch: cannot unify {} with {}", t1, t2)),
259    }
260}
261
262/// Unify two stack types, returning a substitution or an error
263pub fn unify_stacks(s1: &StackType, s2: &StackType) -> Result<Subst, String> {
264    match (s1, s2) {
265        // Empty stacks unify
266        (StackType::Empty, StackType::Empty) => Ok(Subst::empty()),
267
268        // Row variable unifies with any stack (with occurs check)
269        (StackType::RowVar(name), stack) | (stack, StackType::RowVar(name)) => {
270            // If unifying a row var with itself, no substitution needed
271            if matches!(stack, StackType::RowVar(stack_name) if stack_name == name) {
272                return Ok(Subst::empty());
273            }
274
275            // Occurs check: prevent infinite stack types
276            if occurs_in_stack(name, stack) {
277                return Err(format!(
278                    "Occurs check failed: cannot unify {} with {} (would create infinite stack type)",
279                    StackType::RowVar(name.clone()),
280                    stack
281                ));
282            }
283
284            let mut subst = Subst::empty();
285            subst.rows.insert(name.clone(), stack.clone());
286            Ok(subst)
287        }
288
289        // Cons cells unify if tops and rests unify
290        (
291            StackType::Cons {
292                rest: rest1,
293                top: top1,
294            },
295            StackType::Cons {
296                rest: rest2,
297                top: top2,
298            },
299        ) => {
300            // Unify the tops
301            let s_top = unify_types(top1, top2)?;
302
303            // Apply substitution to rests and unify
304            let rest1_subst = s_top.apply_stack(rest1);
305            let rest2_subst = s_top.apply_stack(rest2);
306            let s_rest = unify_stacks(&rest1_subst, &rest2_subst)?;
307
308            // Compose substitutions
309            Ok(s_top.compose(&s_rest))
310        }
311
312        // Empty doesn't unify with Cons
313        _ => Err(format!(
314            "Stack shape mismatch: cannot unify {} with {}",
315            s1, s2
316        )),
317    }
318}
319
320#[cfg(test)]
321mod tests;