Skip to main content

rustyfi_lang/
unify.rs

1//! Structural unification over [`crate::types::MonoType`], mirroring
2//! v0.0.6's `unify_sub` (`src/frontend/typechecker.ml:360-522`): occurs
3//! check (extended to look through rows, since rows are first-class here —
4//! see the module doc comment on `crate::types::Row`), record/row
5//! unification, and the `Kind::Record` bridging case.
6//!
7//! `unify`'s signature takes no `TypeContext`; see `crate::types::FRESH_ID`
8//! for how it still mints correctly-leveled fresh variables when it needs
9//! to extend an open row.
10
11use crate::types::{
12    self, resolve, resolve_row, CmdArgType, Kind, MonoType, Row, RowVarRef, TyVarRef,
13};
14use std::collections::BTreeSet;
15
16#[derive(Debug, thiserror::Error)]
17pub enum UnifyError {
18    #[error("type mismatch: expected `{expected}`, found `{found}`")]
19    Mismatch { expected: MonoType, found: MonoType },
20
21    #[error("occurs check failed: the type would be infinite")]
22    OccursCheck,
23
24    #[error("record is missing label `{label}` (required for `{ty}`)")]
25    MissingLabel { label: String, ty: MonoType },
26
27    #[error("arity mismatch: expected {expected} element(s), found {found}")]
28    ArityMismatch { expected: usize, found: usize },
29
30    #[error("command argument optionality mismatch for `{ty}` (`?` on one side only)")]
31    OptionalMismatch { ty: MonoType },
32
33    /// SATySFi 0.1's closed command optional-label map is INVARIANT under
34    /// unification (upstream `subtype_label_map_with_equal_domain`,
35    /// `signatureSubtyping.ml:482,511-516`): a supplied/declared label set
36    /// mismatch — either side has a label the other doesn't — is always
37    /// rejected, never widened/narrowed. `expected`/`found` are each a
38    /// formatted `?(l1 : τ1, …)` rendering of the two sides' label sets (or
39    /// `?()` for an empty one) for a readable message.
40    #[error(
41        "command optional-argument label set mismatch: expected `{expected}`, found `{found}`"
42    )]
43    CmdLabelMismatch { expected: String, found: String },
44}
45
46/// Unify two monomorphic types in place: free variables get linked
47/// (mutating their union-find cell) so that both `a` and `b` — and anything
48/// else sharing those variables — subsequently `resolve` to the same type.
49pub fn unify(a: &MonoType, b: &MonoType) -> Result<(), UnifyError> {
50    let ra = resolve(a);
51    let rb = resolve(b);
52    match (&*ra, &*rb) {
53        (MonoType::Var(v1), MonoType::Var(v2)) if v1.same(v2) => Ok(()),
54        (MonoType::Var(v), _) => bind_var(v, (*rb).clone()),
55        (_, MonoType::Var(v)) => bind_var(v, (*ra).clone()),
56
57        (MonoType::Base(x), MonoType::Base(y)) => {
58            if x == y {
59                Ok(())
60            } else {
61                Err(UnifyError::Mismatch {
62                    expected: (*ra).clone(),
63                    found: (*rb).clone(),
64                })
65            }
66        }
67
68        (MonoType::Func(r1, d1, c1), MonoType::Func(r2, d2, c2)) => {
69            unify_row(r1, r2)?;
70            unify(d1, d2)?;
71            unify(c1, c2)
72        }
73
74        (MonoType::Product(ts1), MonoType::Product(ts2)) => {
75            if ts1.len() != ts2.len() {
76                return Err(UnifyError::ArityMismatch {
77                    expected: ts1.len(),
78                    found: ts2.len(),
79                });
80            }
81            for (x, y) in ts1.iter().zip(ts2) {
82                unify(x, y)?;
83            }
84            Ok(())
85        }
86
87        (MonoType::List(x), MonoType::List(y)) => unify(x, y),
88        (MonoType::Ref(x), MonoType::Ref(y)) => unify(x, y),
89        (MonoType::Code(x), MonoType::Code(y)) => unify(x, y),
90
91        (MonoType::Record(r1), MonoType::Record(r2)) => unify_row(r1, r2),
92
93        (MonoType::Variant(n1, a1), MonoType::Variant(n2, a2)) => {
94            if n1 != n2 {
95                return Err(UnifyError::Mismatch {
96                    expected: (*ra).clone(),
97                    found: (*rb).clone(),
98                });
99            }
100            if a1.len() != a2.len() {
101                return Err(UnifyError::ArityMismatch {
102                    expected: a1.len(),
103                    found: a2.len(),
104                });
105            }
106            for (x, y) in a1.iter().zip(a2) {
107                unify(x, y)?;
108            }
109            Ok(())
110        }
111
112        (MonoType::InlineCmd(c1), MonoType::InlineCmd(c2))
113        | (MonoType::BlockCmd(c1), MonoType::BlockCmd(c2))
114        | (MonoType::MathCmd(c1), MonoType::MathCmd(c2)) => unify_cmd_args(c1, c2),
115
116        _ => Err(UnifyError::Mismatch {
117            expected: (*ra).clone(),
118            found: (*rb).clone(),
119        }),
120    }
121}
122
123fn unify_cmd_args(a: &[CmdArgType], b: &[CmdArgType]) -> Result<(), UnifyError> {
124    if a.len() != b.len() {
125        return Err(UnifyError::ArityMismatch {
126            expected: a.len(),
127            found: b.len(),
128        });
129    }
130    for (x, y) in a.iter().zip(b) {
131        if x.optional != y.optional {
132            return Err(UnifyError::OptionalMismatch { ty: x.ty.clone() });
133        }
134        // SATySFi 0.1 closed command optional-label map: EQUAL DOMAIN (both
135        // sides kept sorted by label at every producer — `command_scheme`'s
136        // harvest, `lower_type_atom`'s sig lowering). A label present on only
137        // one side makes the lengths or the pairwise names diverge here,
138        // exactly upstream's "invariant label set under a seal"
139        // (`signatureSubtyping.ml:511-516`). A no-op when both sides are `[]`
140        // — every 0.0.6-reachable `CmdArgType`.
141        if x.opt_labels.len() != y.opt_labels.len()
142            || x.opt_labels
143                .iter()
144                .zip(&y.opt_labels)
145                .any(|((lx, _), (ly, _))| lx != ly)
146        {
147            return Err(UnifyError::CmdLabelMismatch {
148                expected: fmt_opt_label_set(&x.opt_labels),
149                found: fmt_opt_label_set(&y.opt_labels),
150            });
151        }
152        for ((_, tx), (_, ty2)) in x.opt_labels.iter().zip(&y.opt_labels) {
153            unify(tx, ty2)?;
154        }
155        unify(&x.ty, &y.ty)?;
156    }
157    Ok(())
158}
159
160/// Render a closed optional-label set as `?(l1 : τ1, …)` (or `?()` if empty)
161/// for a [`UnifyError::CmdLabelMismatch`] message.
162fn fmt_opt_label_set(labels: &[(String, MonoType)]) -> String {
163    let mut s = String::from("?(");
164    for (i, (l, t)) in labels.iter().enumerate() {
165        if i > 0 {
166            s.push_str(", ");
167        }
168        s.push_str(&format!("{l} : {t}"));
169    }
170    s.push(')');
171    s
172}
173
174// ============================================================================
175// Binding a type variable (v0.0.6's `TypeVariable(_)` branches,
176// typechecker.ml:436-519).
177// ============================================================================
178
179fn bind_var(v: &TyVarRef, ty: MonoType) -> Result<(), UnifyError> {
180    if let MonoType::Var(v2) = &ty {
181        if v.same(v2) {
182            return Ok(());
183        }
184    }
185    if occurs_var(v, &ty) {
186        return Err(UnifyError::OccursCheck);
187    }
188    match v.kind() {
189        Kind::Universal => {
190            v.bind(ty);
191            Ok(())
192        }
193        Kind::Record(required) => match &ty {
194            MonoType::Var(v2) => {
195                let merged = match v2.kind() {
196                    Kind::Universal => Kind::Record(required.clone()),
197                    Kind::Record(r2) => Kind::Record(required.union(&r2).cloned().collect()),
198                };
199                v2.set_kind(merged);
200                v.bind(ty);
201                Ok(())
202            }
203            MonoType::Record(row) => {
204                for label in &required {
205                    row_require_label(row, label)?;
206                }
207                v.bind(ty);
208                Ok(())
209            }
210            other => Err(UnifyError::Mismatch {
211                expected: MonoType::Var(v.clone()),
212                found: other.clone(),
213            }),
214        },
215    }
216}
217
218/// Confirm `row` contains `label` (mirrors v0.0.6's
219/// `Assoc.domain_included`/`Assoc.intersection` check at
220/// typechecker.ml:480-500), extending an open row with a fresh field if
221/// necessary.
222fn row_require_label(row: &Row, label: &str) -> Result<(), UnifyError> {
223    match &*resolve_row(row) {
224        Row::Empty => Err(UnifyError::MissingLabel {
225            label: label.to_string(),
226            ty: MonoType::Record(Row::Empty),
227        }),
228        Row::Cons(l, _, rest) => {
229            if l == label {
230                Ok(())
231            } else {
232                row_require_label(&rest, label)
233            }
234        }
235        Row::Var(v) => {
236            let level = v.level().unwrap_or(0);
237            let field = types::new_ty_var(level);
238            let remainder = types::new_row_var(level);
239            let extended = Row::Cons(
240                label.to_string(),
241                Box::new(MonoType::Var(field)),
242                Box::new(Row::Var(remainder)),
243            );
244            bind_row_var(&v, extended)
245        }
246    }
247}
248
249// ============================================================================
250// Row unification (Rémy-style label extraction/subsumption).
251// ============================================================================
252
253fn unify_row(a: &Row, b: &Row) -> Result<(), UnifyError> {
254    // Owned, unlike `unify` above: the arms below destructure the row apart.
255    // Rows are tiny (`Empty` for every 0.0.6 function type), so this copy is
256    // nothing like the whole-type copy `resolve` avoids.
257    let ra = resolve_row(a).into_owned();
258    let rb = resolve_row(b).into_owned();
259    match (ra, rb) {
260        (Row::Empty, Row::Empty) => Ok(()),
261        (Row::Var(v1), Row::Var(v2)) if v1.same(&v2) => Ok(()),
262
263        (Row::Cons(l, t, rest), other) => {
264            let (t2, rest2) = row_extract(&other, &l)?;
265            unify(&t, &t2)?;
266            unify_row(&rest, &rest2)
267        }
268        (other, Row::Cons(l, t, rest)) => {
269            let (t2, rest2) = row_extract(&other, &l)?;
270            unify(&t2, &t)?;
271            unify_row(&rest2, &rest)
272        }
273
274        (Row::Empty, Row::Var(v)) | (Row::Var(v), Row::Empty) => {
275            if let Some(label) = v.kind().iter().next().cloned() {
276                return Err(UnifyError::MissingLabel {
277                    label,
278                    ty: MonoType::Record(Row::Empty),
279                });
280            }
281            bind_row_var(&v, Row::Empty)
282        }
283
284        (Row::Var(v1), Row::Var(v2)) => {
285            let union: BTreeSet<String> = v1.kind().union(&v2.kind()).cloned().collect();
286            v2.set_kind(union);
287            bind_row_var(&v1, Row::Var(v2))
288        }
289    }
290}
291
292/// Extract the field labeled `label` out of `row`, returning its type and
293/// the row of everything else. If `row` is an open row var that doesn't
294/// (yet) mention `label`, this *extends* it in place, returning a fresh
295/// field/remainder pair. This is what realizes label subsumption against an
296/// open row as "unify one field at a time, leaving a fresh remainder row
297/// variable" instead of requiring the whole row to match up front.
298fn row_extract(row: &Row, label: &str) -> Result<(MonoType, Row), UnifyError> {
299    // Owned for the same reason as `unify_row`: this rebuilds the row.
300    match resolve_row(row).into_owned() {
301        Row::Empty => Err(UnifyError::MissingLabel {
302            label: label.to_string(),
303            ty: MonoType::Record(Row::Empty),
304        }),
305        Row::Cons(l, t, rest) => {
306            if l == label {
307                Ok((*t, *rest))
308            } else {
309                let (t2, rest2) = row_extract(&rest, label)?;
310                Ok((t2, Row::Cons(l, t, Box::new(rest2))))
311            }
312        }
313        Row::Var(v) => {
314            let level = v.level().unwrap_or(0);
315            let field = types::new_ty_var(level);
316            let remainder = types::new_row_var(level);
317            let extended = Row::Cons(
318                label.to_string(),
319                Box::new(MonoType::Var(field.clone())),
320                Box::new(Row::Var(remainder.clone())),
321            );
322            bind_row_var(&v, extended)?;
323            Ok((MonoType::Var(field), Row::Var(remainder)))
324        }
325    }
326}
327
328fn bind_row_var(v: &RowVarRef, row: Row) -> Result<(), UnifyError> {
329    if let Row::Var(v2) = &row {
330        if v.same(v2) {
331            return Ok(());
332        }
333    }
334    if occurs_rowvar_in_row(v, &row) {
335        return Err(UnifyError::OccursCheck);
336    }
337    v.bind(row);
338    Ok(())
339}
340
341// ============================================================================
342// Occurs checks. As in v0.0.6 (typechecker.ml:179-261), these double as a
343// level-lowering pass: any free variable found strictly deeper than the
344// variable being bound has its level pulled up (lowered numerically) to
345// match, which is what keeps `generalize` from over-generalizing a
346// variable that unification has linked to something from an outer scope.
347// ============================================================================
348
349fn occurs_var(tv: &TyVarRef, ty: &MonoType) -> bool {
350    match &*resolve(ty) {
351        MonoType::Var(v) => {
352            if v.same(tv) {
353                return true;
354            }
355            if let (Some(tv_level), Some(v_level)) = (tv.level(), v.level()) {
356                if tv_level < v_level {
357                    v.set_level(tv_level);
358                }
359            }
360            false
361        }
362        MonoType::Base(_) => false,
363        MonoType::Func(row, a, b) => {
364            occurs_var_in_row(tv, &row) || occurs_var(tv, &a) || occurs_var(tv, &b)
365        }
366        MonoType::Product(ts) => ts.iter().any(|t| occurs_var(tv, t)),
367        MonoType::List(t) | MonoType::Ref(t) | MonoType::Code(t) => occurs_var(tv, &t),
368        MonoType::Record(row) => occurs_var_in_row(tv, &row),
369        MonoType::Variant(_, args) => args.iter().any(|t| occurs_var(tv, t)),
370        // Both `MonoType`-bearing fields of a slot. `opt_labels` (0.1's
371        // closed `?(l : τ, …)` map) is easy to forget and was: an occurs
372        // cycle, or a variable needing its level lowered, hiding inside an
373        // optional-label bundle escaped this check entirely. It cannot use
374        // `crate::visit`'s derived traversal — that one is structural, and
375        // this walk must `resolve` through bound variables at every level.
376        MonoType::InlineCmd(cs) | MonoType::BlockCmd(cs) | MonoType::MathCmd(cs) => cs
377            .iter()
378            .any(|c| c.opt_labels.iter().any(|(_, t)| occurs_var(tv, t)) || occurs_var(tv, &c.ty)),
379    }
380}
381
382fn occurs_var_in_row(tv: &TyVarRef, row: &Row) -> bool {
383    match &*resolve_row(row) {
384        Row::Empty => false,
385        Row::Var(_) => false,
386        Row::Cons(_, t, rest) => occurs_var(tv, &t) || occurs_var_in_row(tv, &rest),
387    }
388}
389
390fn occurs_rowvar_in_type(rv: &RowVarRef, ty: &MonoType) -> bool {
391    match &*resolve(ty) {
392        MonoType::Var(_) => false,
393        MonoType::Base(_) => false,
394        MonoType::Func(row, a, b) => {
395            occurs_rowvar_in_row(rv, &row)
396                || occurs_rowvar_in_type(rv, &a)
397                || occurs_rowvar_in_type(rv, &b)
398        }
399        MonoType::Product(ts) => ts.iter().any(|t| occurs_rowvar_in_type(rv, t)),
400        MonoType::List(t) | MonoType::Ref(t) | MonoType::Code(t) => occurs_rowvar_in_type(rv, &t),
401        MonoType::Record(row) => occurs_rowvar_in_row(rv, &row),
402        MonoType::Variant(_, args) => args.iter().any(|t| occurs_rowvar_in_type(rv, t)),
403        // See `occurs_var`'s twin arm: `opt_labels` counts too.
404        MonoType::InlineCmd(cs) | MonoType::BlockCmd(cs) | MonoType::MathCmd(cs) => {
405            cs.iter().any(|c| {
406                c.opt_labels
407                    .iter()
408                    .any(|(_, t)| occurs_rowvar_in_type(rv, t))
409                    || occurs_rowvar_in_type(rv, &c.ty)
410            })
411        }
412    }
413}
414
415fn occurs_rowvar_in_row(rv: &RowVarRef, row: &Row) -> bool {
416    match &*resolve_row(row) {
417        Row::Empty => false,
418        Row::Var(v) => {
419            if v.same(rv) {
420                return true;
421            }
422            if let (Some(rv_level), Some(v_level)) = (rv.level(), v.level()) {
423                if rv_level < v_level {
424                    v.set_level(rv_level);
425                }
426            }
427            false
428        }
429        Row::Cons(_, t, rest) => occurs_rowvar_in_type(rv, &t) || occurs_rowvar_in_row(rv, &rest),
430    }
431}