Skip to main content

yulang_monomorphize/
graph.rs

1use std::collections::{BTreeMap, BTreeSet, VecDeque};
2
3use yulang_runtime_ir::{FinalizedBinding as Binding, RuntimeType};
4use yulang_typed_ir as typed_ir;
5
6use crate::{MonomorphizeDiagnostic, MonomorphizeResult};
7
8#[derive(Debug, Default, Clone, PartialEq, Eq)]
9pub struct TypeGraph {
10    next_fresh: usize,
11    slots: BTreeMap<typed_ir::TypeVar, TypeVarBounds>,
12    constraints: Vec<SubtypeConstraint>,
13    cast_order: TypeCastOrder,
14}
15
16impl TypeGraph {
17    pub fn with_cast_order(cast_order: TypeCastOrder) -> Self {
18        Self {
19            cast_order,
20            ..Self::default()
21        }
22    }
23
24    pub fn instantiate_principal(&mut self, binding: &Binding) -> PrincipalInstance {
25        let mut renames = BTreeMap::new();
26        for param in &binding.type_params {
27            let fresh = self.fresh_var(param);
28            self.slots.entry(fresh.clone()).or_default();
29            renames.insert(param.clone(), fresh);
30        }
31        PrincipalInstance {
32            original_binding: binding.name.clone(),
33            principal_type: rename_type(&binding.scheme.body, &renames),
34            type_params: renames
35                .into_iter()
36                .map(|(original, fresh)| PrincipalTypeParam { original, fresh })
37                .collect(),
38        }
39    }
40
41    pub fn collect_runtime_bounds(
42        &mut self,
43        template: &typed_ir::Type,
44        bounds: &RuntimeBounds,
45    ) -> MonomorphizeResult<()> {
46        if let Some(lower) = &bounds.lower {
47            self.collect_runtime(template, lower, BoundSide::Lower)?;
48        }
49        if let Some(upper) = &bounds.upper {
50            self.collect_runtime(template, upper, BoundSide::Upper)?;
51        }
52        self.propagate_constraints()
53    }
54
55    pub fn collect_runtime_lower(
56        &mut self,
57        template: &typed_ir::Type,
58        lower: &RuntimeType,
59    ) -> MonomorphizeResult<()> {
60        self.collect_runtime(template, lower, BoundSide::Lower)?;
61        self.propagate_constraints()
62    }
63
64    pub fn collect_runtime_upper(
65        &mut self,
66        template: &typed_ir::Type,
67        upper: &RuntimeType,
68    ) -> MonomorphizeResult<()> {
69        self.collect_runtime(template, upper, BoundSide::Upper)?;
70        self.propagate_constraints()
71    }
72
73    pub fn fresh_hole(&mut self, prefix: &str) -> typed_ir::Type {
74        let var = self.fresh_var(&typed_ir::TypeVar(prefix.into()));
75        self.slots.entry(var.clone()).or_default();
76        typed_ir::Type::Var(var)
77    }
78
79    pub fn constrain_subtype(
80        &mut self,
81        lower: typed_ir::Type,
82        upper: typed_ir::Type,
83    ) -> MonomorphizeResult<()> {
84        let constraint = SubtypeConstraint { lower, upper };
85        if !self.constraints.contains(&constraint) {
86            self.constraints.push(constraint);
87        }
88        self.propagate_constraints()
89    }
90
91    pub fn default_unbound_lower(
92        &mut self,
93        vars: BTreeSet<typed_ir::TypeVar>,
94        lower: typed_ir::Type,
95    ) -> MonomorphizeResult<()> {
96        for var in vars {
97            let Some(slot) = self.slots.get_mut(&var) else {
98                continue;
99            };
100            if slot.lower.is_none() && slot.upper.is_none() {
101                slot.push_lower(var, lower.clone(), &self.cast_order)?;
102            }
103        }
104        self.propagate_constraints()
105    }
106
107    pub fn solve(self) -> GraphSolution {
108        let type_vars = self
109            .slots
110            .into_iter()
111            .map(|(var, bounds)| {
112                let solution = bounds.solution_ref().cloned();
113                ResolvedTypeVar {
114                    var,
115                    bounds,
116                    solution,
117                }
118            })
119            .collect();
120        GraphSolution { type_vars }
121    }
122
123    pub fn slot(&self, var: &typed_ir::TypeVar) -> Option<&TypeVarBounds> {
124        self.slots.get(var)
125    }
126
127    fn fresh_var(&mut self, source: &typed_ir::TypeVar) -> typed_ir::TypeVar {
128        let fresh = typed_ir::TypeVar(format!("{}#{}", source.0, self.next_fresh));
129        self.next_fresh += 1;
130        fresh
131    }
132
133    fn collect_runtime(
134        &mut self,
135        template: &typed_ir::Type,
136        actual: &RuntimeType,
137        side: BoundSide,
138    ) -> MonomorphizeResult<()> {
139        match actual {
140            RuntimeType::Value(actual) => self.collect_core(template, actual, side),
141            RuntimeType::Fun { param, ret } => {
142                let typed_ir::Type::Fun {
143                    param: template_param,
144                    param_effect: template_param_effect,
145                    ret_effect: template_ret_effect,
146                    ret: template_ret,
147                } = template
148                else {
149                    return Ok(());
150                };
151                let RuntimeEffectedType {
152                    value: actual_param,
153                    effect: actual_param_effect,
154                } = split_runtime_effected_type(param);
155                let RuntimeEffectedType {
156                    value: actual_ret,
157                    effect: actual_ret_effect,
158                } = split_runtime_effected_type(ret);
159                let param_side = side.flip();
160                if !(param_side == BoundSide::Lower && runtime_value_is_top(actual_param)) {
161                    self.collect_runtime(template_param, actual_param, param_side)?;
162                }
163                self.collect_runtime_effect(template_param_effect, actual_param_effect, side)?;
164                self.collect_runtime_effect(template_ret_effect, actual_ret_effect, side)?;
165                self.collect_runtime(template_ret, actual_ret, side)
166            }
167            RuntimeType::Thunk { value, .. } => self.collect_runtime(template, value, side),
168            RuntimeType::Unknown => Ok(()),
169        }
170    }
171
172    fn collect_core(
173        &mut self,
174        template: &typed_ir::Type,
175        actual: &typed_ir::Type,
176        side: BoundSide,
177    ) -> MonomorphizeResult<()> {
178        match template {
179            typed_ir::Type::Var(var) => self.record(var.clone(), actual.clone(), side).map(|_| ()),
180            typed_ir::Type::Named { path, args } => {
181                let typed_ir::Type::Named {
182                    path: actual_path,
183                    args: actual_args,
184                } = actual
185                else {
186                    return Ok(());
187                };
188                if path != actual_path || args.len() != actual_args.len() {
189                    return Ok(());
190                }
191                for (template, actual) in args.iter().zip(actual_args) {
192                    self.collect_type_arg(template, actual, side)?;
193                }
194                Ok(())
195            }
196            typed_ir::Type::Fun {
197                param,
198                param_effect,
199                ret_effect,
200                ret,
201            } => {
202                let typed_ir::Type::Fun {
203                    param: actual_param,
204                    param_effect: actual_param_effect,
205                    ret_effect: actual_ret_effect,
206                    ret: actual_ret,
207                } = actual
208                else {
209                    return Ok(());
210                };
211                let param_side = side.flip();
212                if !(param_side == BoundSide::Lower
213                    && matches!(actual_param.as_ref(), typed_ir::Type::Any))
214                {
215                    self.collect_core(param, actual_param, param_side)?;
216                }
217                self.collect_core(param_effect, actual_param_effect, side)?;
218                self.collect_core(ret_effect, actual_ret_effect, side)?;
219                self.collect_core(ret, actual_ret, side)
220            }
221            typed_ir::Type::Tuple(items) => {
222                let typed_ir::Type::Tuple(actual_items) = actual else {
223                    return Ok(());
224                };
225                if items.len() != actual_items.len() {
226                    return Ok(());
227                }
228                for (template, actual) in items.iter().zip(actual_items) {
229                    self.collect_core(template, actual, side)?;
230                }
231                Ok(())
232            }
233            typed_ir::Type::Row { items, tail } => {
234                let typed_ir::Type::Row {
235                    items: actual_items,
236                    tail: actual_tail,
237                } = actual
238                else {
239                    return Ok(());
240                };
241                self.collect_row(items, tail, actual_items, actual_tail, side)
242            }
243            typed_ir::Type::Variant(template) => {
244                let typed_ir::Type::Variant(actual) = actual else {
245                    return Ok(());
246                };
247                self.collect_variant(template, actual, side)
248            }
249            typed_ir::Type::Record(template) => {
250                let typed_ir::Type::Record(actual) = actual else {
251                    return Ok(());
252                };
253                self.collect_record(template, actual, side)
254            }
255            typed_ir::Type::Unknown
256            | typed_ir::Type::Never
257            | typed_ir::Type::Any
258            | typed_ir::Type::Union(_)
259            | typed_ir::Type::Inter(_)
260            | typed_ir::Type::Recursive { .. } => Ok(()),
261        }
262    }
263
264    fn collect_runtime_effect(
265        &mut self,
266        template: &typed_ir::Type,
267        actual: RuntimeEffectRef<'_>,
268        side: BoundSide,
269    ) -> MonomorphizeResult<()> {
270        match actual {
271            RuntimeEffectRef::Known(actual) => self.collect_core(template, actual, side),
272            RuntimeEffectRef::Pure | RuntimeEffectRef::Unknown => Ok(()),
273        }
274    }
275
276    fn propagate_constraints(&mut self) -> MonomorphizeResult<()> {
277        loop {
278            let mut changed = false;
279            for constraint in self.constraints.clone() {
280                changed |= self.apply_subtype_constraint(constraint.lower, constraint.upper)?;
281            }
282            if !changed {
283                return Ok(());
284            }
285        }
286    }
287
288    fn apply_subtype_constraint(
289        &mut self,
290        lower: typed_ir::Type,
291        upper: typed_ir::Type,
292    ) -> MonomorphizeResult<bool> {
293        self.apply_subtype_constraint_inner(lower, upper, &mut Vec::new())
294    }
295
296    fn apply_subtype_constraint_inner(
297        &mut self,
298        lower: typed_ir::Type,
299        upper: typed_ir::Type,
300        seen: &mut Vec<SubtypeConstraint>,
301    ) -> MonomorphizeResult<bool> {
302        let constraint = SubtypeConstraint {
303            lower: lower.clone(),
304            upper: upper.clone(),
305        };
306        if seen.contains(&constraint) {
307            return Ok(false);
308        }
309        seen.push(constraint);
310        if lower == upper || matches!(upper, typed_ir::Type::Any) {
311            return Ok(false);
312        }
313        match (lower, upper) {
314            (typed_ir::Type::Unknown, _) | (_, typed_ir::Type::Unknown) => Ok(false),
315            (typed_ir::Type::Var(lower), upper) => {
316                let mut changed = false;
317                if let Some(bound) = self
318                    .slots
319                    .get(&lower)
320                    .and_then(|slot| slot.lower.as_ref())
321                    .cloned()
322                    .filter(|bound| !self.lower_bound_chain_reaches(bound, &lower))
323                {
324                    changed |= self.apply_subtype_constraint_inner(bound, upper.clone(), seen)?;
325                }
326                if let typed_ir::Type::Var(upper_var) = &upper
327                    && let Some(bound) = self
328                        .slots
329                        .get(upper_var)
330                        .and_then(|slot| slot.upper.as_ref())
331                        .cloned()
332                        .filter(|bound| !self.upper_bound_chain_reaches(bound, &lower))
333                {
334                    changed |= self.apply_subtype_constraint_inner(
335                        typed_ir::Type::Var(lower.clone()),
336                        bound,
337                        seen,
338                    )?;
339                }
340                changed |= self.record(lower, upper, BoundSide::Upper)?;
341                Ok(changed)
342            }
343            (lower, typed_ir::Type::Var(upper)) => {
344                let mut changed = false;
345                let lower = self.known_lower_or_self(lower);
346                if let Some(bound) = self
347                    .slots
348                    .get(&upper)
349                    .and_then(|slot| slot.upper.as_ref())
350                    .cloned()
351                    .filter(|bound| !self.upper_bound_chain_reaches(bound, &upper))
352                {
353                    changed |= self.apply_subtype_constraint_inner(lower.clone(), bound, seen)?;
354                }
355                changed |= self.record(upper, lower, BoundSide::Lower)?;
356                Ok(changed)
357            }
358            (
359                typed_ir::Type::Fun {
360                    param: lower_param,
361                    param_effect: lower_param_effect,
362                    ret_effect: lower_ret_effect,
363                    ret: lower_ret,
364                },
365                typed_ir::Type::Fun {
366                    param: upper_param,
367                    param_effect: upper_param_effect,
368                    ret_effect: upper_ret_effect,
369                    ret: upper_ret,
370                },
371            ) => {
372                let mut changed = false;
373                changed |= self.apply_subtype_constraint_inner(*upper_param, *lower_param, seen)?;
374                changed |= self.apply_subtype_constraint_inner(
375                    *lower_param_effect,
376                    *upper_param_effect,
377                    seen,
378                )?;
379                changed |= self.apply_subtype_constraint_inner(
380                    *lower_ret_effect,
381                    *upper_ret_effect,
382                    seen,
383                )?;
384                changed |= self.apply_subtype_constraint_inner(*lower_ret, *upper_ret, seen)?;
385                Ok(changed)
386            }
387            (
388                typed_ir::Type::Named {
389                    path: lower_path,
390                    args: lower_args,
391                },
392                typed_ir::Type::Named {
393                    path: upper_path,
394                    args: upper_args,
395                },
396            ) if lower_path == upper_path && lower_args.len() == upper_args.len() => {
397                let mut changed = false;
398                for (lower, upper) in lower_args.into_iter().zip(upper_args) {
399                    changed |= self.constrain_type_arg(lower, upper, seen)?;
400                }
401                Ok(changed)
402            }
403            (typed_ir::Type::Tuple(lower), typed_ir::Type::Tuple(upper))
404                if lower.len() == upper.len() =>
405            {
406                let mut changed = false;
407                for (lower, upper) in lower.into_iter().zip(upper) {
408                    changed |= self.apply_subtype_constraint_inner(lower, upper, seen)?;
409                }
410                Ok(changed)
411            }
412            (typed_ir::Type::Union(items), upper) => {
413                let mut changed = false;
414                for item in items {
415                    changed |= self.apply_subtype_constraint_inner(item, upper.clone(), seen)?;
416                }
417                Ok(changed)
418            }
419            (lower, typed_ir::Type::Inter(items)) => {
420                let mut changed = false;
421                for item in items {
422                    changed |= self.apply_subtype_constraint_inner(lower.clone(), item, seen)?;
423                }
424                Ok(changed)
425            }
426            (
427                typed_ir::Type::Row {
428                    items: lower_items,
429                    tail: lower_tail,
430                },
431                typed_ir::Type::Row {
432                    items: upper_items,
433                    tail: upper_tail,
434                },
435            ) => self.constrain_row(lower_items, *lower_tail, upper_items, *upper_tail, seen),
436            (typed_ir::Type::Variant(lower), typed_ir::Type::Variant(upper)) => {
437                self.constrain_variant(lower, upper, seen)
438            }
439            (typed_ir::Type::Record(lower), typed_ir::Type::Record(upper)) => {
440                self.constrain_record(lower, upper, seen)
441            }
442            _ => Ok(false),
443        }
444    }
445
446    fn constrain_type_arg(
447        &mut self,
448        lower: typed_ir::TypeArg,
449        upper: typed_ir::TypeArg,
450        seen: &mut Vec<SubtypeConstraint>,
451    ) -> MonomorphizeResult<bool> {
452        match (lower, upper) {
453            (typed_ir::TypeArg::Type(lower), typed_ir::TypeArg::Type(upper)) => {
454                self.apply_subtype_constraint_inner(lower, upper, seen)
455            }
456            // `Type(t) <: Bounds(L, U)` is conservatively reduced to
457            // `t <: U`. The dual constraint `L <: t` is correct in theory but
458            // currently triggers spurious ConflictingBounds when L and U
459            // describe the same effect row at different precisions (closed
460            // Row vs Inter of open rows). Re-enable both directions once
461            // push_bound can merge those forms.
462            (typed_ir::TypeArg::Type(lower), typed_ir::TypeArg::Bounds(upper)) => {
463                if let Some(upper_upper) = upper.upper {
464                    self.apply_subtype_constraint_inner(lower, *upper_upper, seen)
465                } else {
466                    Ok(false)
467                }
468            }
469            // `Bounds(L, U) <: Type(t)` is conservatively reduced to
470            // `L <: t`. The dual direction is omitted for the same reason as
471            // the Type-vs-Bounds case above.
472            (typed_ir::TypeArg::Bounds(lower), typed_ir::TypeArg::Type(upper)) => {
473                if let Some(lower_lower) = lower.lower {
474                    self.apply_subtype_constraint_inner(*lower_lower, upper, seen)
475                } else {
476                    Ok(false)
477                }
478            }
479            (typed_ir::TypeArg::Bounds(lower), typed_ir::TypeArg::Bounds(upper)) => {
480                let mut changed = false;
481                if let (Some(lower), Some(upper)) = (lower.lower, upper.lower) {
482                    changed |= self.apply_subtype_constraint_inner(*lower, *upper, seen)?;
483                }
484                if let (Some(lower), Some(upper)) = (lower.upper, upper.upper) {
485                    changed |= self.apply_subtype_constraint_inner(*lower, *upper, seen)?;
486                }
487                Ok(changed)
488            }
489        }
490    }
491
492    fn known_lower_or_self(&self, ty: typed_ir::Type) -> typed_ir::Type {
493        let typed_ir::Type::Var(var) = &ty else {
494            return ty;
495        };
496        self.slots
497            .get(var)
498            .and_then(|slot| slot.lower.as_ref())
499            .cloned()
500            .unwrap_or(ty)
501    }
502
503    fn known_upper_or_self(&self, ty: typed_ir::Type) -> typed_ir::Type {
504        let typed_ir::Type::Var(var) = &ty else {
505            return ty;
506        };
507        self.slots
508            .get(var)
509            .and_then(|slot| slot.upper.as_ref())
510            .cloned()
511            .unwrap_or(ty)
512    }
513
514    fn collect_type_arg(
515        &mut self,
516        template: &typed_ir::TypeArg,
517        actual: &typed_ir::TypeArg,
518        side: BoundSide,
519    ) -> MonomorphizeResult<()> {
520        match (template, actual) {
521            (typed_ir::TypeArg::Type(template), typed_ir::TypeArg::Type(actual)) => {
522                self.collect_core(template, actual, side)
523            }
524            (typed_ir::TypeArg::Type(template), typed_ir::TypeArg::Bounds(actual)) => {
525                if let Some(lower) = actual.lower.as_deref() {
526                    self.collect_core(template, lower, BoundSide::Lower)?;
527                }
528                if let Some(upper) = actual.upper.as_deref() {
529                    self.collect_core(template, upper, BoundSide::Upper)?;
530                }
531                Ok(())
532            }
533            (typed_ir::TypeArg::Bounds(template), typed_ir::TypeArg::Type(actual)) => {
534                if let Some(lower) = template.lower.as_deref() {
535                    self.collect_core(lower, actual, side)?;
536                }
537                if let Some(upper) = template.upper.as_deref() {
538                    self.collect_core(upper, actual, BoundSide::Upper)?;
539                }
540                Ok(())
541            }
542            (typed_ir::TypeArg::Bounds(template), typed_ir::TypeArg::Bounds(actual)) => {
543                if let (Some(template), Some(actual)) = (&template.lower, &actual.lower) {
544                    self.collect_core(template, actual, BoundSide::Lower)?;
545                }
546                if let (Some(template), Some(actual)) = (&template.upper, &actual.upper) {
547                    self.collect_core(template, actual, BoundSide::Upper)?;
548                }
549                Ok(())
550            }
551        }
552    }
553
554    fn collect_row(
555        &mut self,
556        template_items: &[typed_ir::Type],
557        template_tail: &typed_ir::Type,
558        actual_items: &[typed_ir::Type],
559        actual_tail: &typed_ir::Type,
560        side: BoundSide,
561    ) -> MonomorphizeResult<()> {
562        let RowResidual {
563            matched,
564            unmatched_left,
565            unmatched_right,
566        } = split_row_items(template_items, actual_items);
567        if matched.is_empty() && !template_items.is_empty() && !actual_items.is_empty() {
568            return Ok(());
569        }
570        for (template, actual) in matched {
571            self.collect_core(template, actual, side)?;
572        }
573        if !unmatched_left.is_empty() {
574            return Ok(());
575        }
576        let residual = effect_row_from_items_and_tail(unmatched_right, actual_tail.clone());
577        self.collect_core(template_tail, &residual, side)
578    }
579
580    fn constrain_row(
581        &mut self,
582        lower_items: Vec<typed_ir::Type>,
583        lower_tail: typed_ir::Type,
584        upper_items: Vec<typed_ir::Type>,
585        upper_tail: typed_ir::Type,
586        seen: &mut Vec<SubtypeConstraint>,
587    ) -> MonomorphizeResult<bool> {
588        let RowResidual {
589            matched,
590            unmatched_left,
591            unmatched_right,
592        } = split_row_items(&lower_items, &upper_items);
593        if matched.is_empty() && !lower_items.is_empty() && !upper_items.is_empty() {
594            return Ok(false);
595        }
596        let mut changed = false;
597        for (lower, upper) in matched {
598            changed |= self.apply_subtype_constraint_inner(lower.clone(), upper.clone(), seen)?;
599        }
600        let lower_residual = effect_row_from_items_and_tail(unmatched_left, lower_tail);
601        let upper_residual = effect_row_from_items_and_tail(unmatched_right, upper_tail);
602        changed |= self.apply_subtype_constraint_inner(lower_residual, upper_residual, seen)?;
603        Ok(changed)
604    }
605
606    fn collect_variant(
607        &mut self,
608        template: &typed_ir::VariantType,
609        actual: &typed_ir::VariantType,
610        side: BoundSide,
611    ) -> MonomorphizeResult<()> {
612        for template_case in &template.cases {
613            let Some(actual_case) = find_variant_case(actual, &template_case.name) else {
614                if actual.tail.is_none() && side == BoundSide::Lower {
615                    for payload in &template_case.payloads {
616                        self.collect_core(payload, &typed_ir::Type::Never, BoundSide::Lower)?;
617                        self.collect_core(payload, &typed_ir::Type::Never, BoundSide::Upper)?;
618                    }
619                }
620                continue;
621            };
622            if template_case.payloads.len() != actual_case.payloads.len() {
623                continue;
624            }
625            for (template, actual) in template_case.payloads.iter().zip(&actual_case.payloads) {
626                self.collect_core(template, actual, side)?;
627            }
628        }
629        Ok(())
630    }
631
632    fn collect_record(
633        &mut self,
634        template: &typed_ir::RecordType,
635        actual: &typed_ir::RecordType,
636        side: BoundSide,
637    ) -> MonomorphizeResult<()> {
638        for template_field in &template.fields {
639            let Some(actual_field) = actual
640                .fields
641                .iter()
642                .find(|actual| actual.name == template_field.name)
643            else {
644                continue;
645            };
646            self.collect_core(&template_field.value, &actual_field.value, side)?;
647        }
648        Ok(())
649    }
650
651    fn constrain_variant(
652        &mut self,
653        lower: typed_ir::VariantType,
654        upper: typed_ir::VariantType,
655        seen: &mut Vec<SubtypeConstraint>,
656    ) -> MonomorphizeResult<bool> {
657        let mut changed = false;
658        for lower_case in &lower.cases {
659            let Some(upper_case) = find_variant_case(&upper, &lower_case.name) else {
660                continue;
661            };
662            if lower_case.payloads.len() != upper_case.payloads.len() {
663                continue;
664            }
665            for (lower, upper) in lower_case.payloads.iter().zip(&upper_case.payloads) {
666                changed |=
667                    self.apply_subtype_constraint_inner(lower.clone(), upper.clone(), seen)?;
668            }
669        }
670        if lower.tail.is_none() {
671            for upper_case in &upper.cases {
672                if find_variant_case(&lower, &upper_case.name).is_some() {
673                    continue;
674                }
675                for payload in &upper_case.payloads {
676                    changed |= self.apply_subtype_constraint_inner(
677                        typed_ir::Type::Never,
678                        payload.clone(),
679                        seen,
680                    )?;
681                    changed |= self.apply_subtype_constraint_inner(
682                        payload.clone(),
683                        typed_ir::Type::Never,
684                        seen,
685                    )?;
686                }
687            }
688        }
689        Ok(changed)
690    }
691
692    fn constrain_record(
693        &mut self,
694        lower: typed_ir::RecordType,
695        upper: typed_ir::RecordType,
696        seen: &mut Vec<SubtypeConstraint>,
697    ) -> MonomorphizeResult<bool> {
698        let mut changed = false;
699        for lower_field in &lower.fields {
700            let Some(upper_field) = upper
701                .fields
702                .iter()
703                .find(|upper| upper.name == lower_field.name)
704            else {
705                continue;
706            };
707            changed |= self.apply_subtype_constraint_inner(
708                lower_field.value.clone(),
709                upper_field.value.clone(),
710                seen,
711            )?;
712        }
713        Ok(changed)
714    }
715
716    fn record(
717        &mut self,
718        var: typed_ir::TypeVar,
719        mut ty: typed_ir::Type,
720        side: BoundSide,
721    ) -> MonomorphizeResult<bool> {
722        ty = match side {
723            BoundSide::Lower => self.known_lower_or_self(ty),
724            BoundSide::Upper => self.known_upper_or_self(ty),
725        };
726        ty = normalize_bound_form(&ty);
727        if matches!(ty, typed_ir::Type::Unknown) || is_vacuous_bound(&ty, side) {
728            return Ok(false);
729        }
730        // Reject self-bounds: chasing `known_*_or_self` can resolve a Var to a
731        // chain that lands back on `var`. Recording `Var(var)` as a bound on
732        // `var` creates a self-loop in the slot graph, and the next constraint
733        // that walks `slot[var].upper` (or `.lower`) recurses forever and
734        // overflows the stack.
735        if let typed_ir::Type::Var(other) = &ty
736            && *other == var
737        {
738            return Ok(false);
739        }
740        if let typed_ir::Type::Var(other) = &ty {
741            let cyclic = match side {
742                BoundSide::Lower => self.var_lower_bound_chain_reaches(other, &var),
743                BoundSide::Upper => self.var_upper_bound_chain_reaches(other, &var),
744            };
745            if cyclic {
746                return Ok(false);
747            }
748        }
749        if std::env::var_os("YULANG_TRACE_ANY_BOUNDS").is_some()
750            && side == BoundSide::Lower
751            && matches!(ty, typed_ir::Type::Any)
752        {
753            eprintln!(
754                "TRACE Any lower bound var={var:?}\n{}",
755                std::backtrace::Backtrace::force_capture()
756            );
757        }
758        let slot = self.slots.entry(var.clone()).or_default();
759        let changed = match side {
760            BoundSide::Lower => slot.push_lower(var.clone(), ty, &self.cast_order),
761            BoundSide::Upper => slot.push_upper(var.clone(), ty, &self.cast_order),
762        }?;
763        reject_invalid_top_bottom_bounds(&var, slot)?;
764        Ok(changed)
765    }
766
767    fn lower_bound_chain_reaches(&self, ty: &typed_ir::Type, target: &typed_ir::TypeVar) -> bool {
768        let typed_ir::Type::Var(var) = ty else {
769            return false;
770        };
771        self.var_lower_bound_chain_reaches(var, target)
772    }
773
774    fn upper_bound_chain_reaches(&self, ty: &typed_ir::Type, target: &typed_ir::TypeVar) -> bool {
775        let typed_ir::Type::Var(var) = ty else {
776            return false;
777        };
778        self.var_upper_bound_chain_reaches(var, target)
779    }
780
781    fn var_lower_bound_chain_reaches(
782        &self,
783        start: &typed_ir::TypeVar,
784        target: &typed_ir::TypeVar,
785    ) -> bool {
786        self.var_bound_chain_reaches(start, target, BoundSide::Lower)
787    }
788
789    fn var_upper_bound_chain_reaches(
790        &self,
791        start: &typed_ir::TypeVar,
792        target: &typed_ir::TypeVar,
793    ) -> bool {
794        self.var_bound_chain_reaches(start, target, BoundSide::Upper)
795    }
796
797    fn var_bound_chain_reaches(
798        &self,
799        start: &typed_ir::TypeVar,
800        target: &typed_ir::TypeVar,
801        side: BoundSide,
802    ) -> bool {
803        let mut current = start;
804        let mut seen = Vec::new();
805        loop {
806            if current == target {
807                return true;
808            }
809            if seen.iter().any(|seen| seen == current) {
810                return false;
811            }
812            seen.push(current.clone());
813            let Some(typed_ir::Type::Var(next)) =
814                self.slots.get(current).and_then(|slot| match side {
815                    BoundSide::Lower => slot.lower.as_ref(),
816                    BoundSide::Upper => slot.upper.as_ref(),
817                })
818            else {
819                return false;
820            };
821            current = next;
822        }
823    }
824}
825
826#[derive(Debug, Default, Clone, PartialEq, Eq)]
827pub struct TypeCastOrder {
828    edges: Vec<TypeCastEdge>,
829}
830
831impl TypeCastOrder {
832    pub fn from_edges(edges: Vec<(typed_ir::Type, typed_ir::Type)>) -> Self {
833        Self {
834            edges: edges
835                .into_iter()
836                .map(|(from, to)| TypeCastEdge { from, to })
837                .collect(),
838        }
839    }
840
841    fn join_lower(&self, left: &typed_ir::Type, right: &typed_ir::Type) -> Option<typed_ir::Type> {
842        let left_to_right = self.can_cast(left, right);
843        let right_to_left = self.can_cast(right, left);
844        match (left_to_right, right_to_left) {
845            (true, false) => Some(right.clone()),
846            (false, true) => Some(left.clone()),
847            _ => None,
848        }
849    }
850
851    fn meet_upper(&self, left: &typed_ir::Type, right: &typed_ir::Type) -> Option<typed_ir::Type> {
852        let left_to_right = self.can_cast(left, right);
853        let right_to_left = self.can_cast(right, left);
854        match (left_to_right, right_to_left) {
855            (true, false) => Some(left.clone()),
856            (false, true) => Some(right.clone()),
857            _ => None,
858        }
859    }
860
861    fn can_cast(&self, from: &typed_ir::Type, to: &typed_ir::Type) -> bool {
862        if lightweight_bounds_equivalent(from, to) {
863            return true;
864        }
865        let mut seen = Vec::new();
866        let mut queue = VecDeque::from([from.clone()]);
867        while let Some(current) = queue.pop_front() {
868            if seen
869                .iter()
870                .any(|seen| lightweight_bounds_equivalent(seen, &current))
871            {
872                continue;
873            }
874            seen.push(current.clone());
875            for edge in &self.edges {
876                if !lightweight_bounds_equivalent(&edge.from, &current) {
877                    continue;
878                }
879                if lightweight_bounds_equivalent(&edge.to, to) {
880                    return true;
881                }
882                queue.push_back(edge.to.clone());
883            }
884        }
885        false
886    }
887}
888
889#[derive(Debug, Clone, PartialEq, Eq)]
890struct TypeCastEdge {
891    from: typed_ir::Type,
892    to: typed_ir::Type,
893}
894
895#[derive(Debug, Clone, PartialEq, Eq)]
896struct SubtypeConstraint {
897    lower: typed_ir::Type,
898    upper: typed_ir::Type,
899}
900
901#[derive(Debug, Clone, PartialEq, Eq)]
902pub struct PrincipalInstance {
903    pub original_binding: typed_ir::Path,
904    pub principal_type: typed_ir::Type,
905    pub type_params: Vec<PrincipalTypeParam>,
906}
907
908impl PrincipalInstance {
909    pub fn original_substitutions(
910        &self,
911        solution: &GraphSolution,
912    ) -> Vec<typed_ir::TypeSubstitution> {
913        self.type_params
914            .iter()
915            .filter_map(|param| {
916                solution
917                    .solution_for(&param.fresh)
918                    .cloned()
919                    .map(|ty| typed_ir::TypeSubstitution {
920                        var: param.original.clone(),
921                        ty,
922                    })
923            })
924            .collect()
925    }
926
927    pub fn effect_only_type_params(&self) -> BTreeSet<typed_ir::TypeVar> {
928        let params = self
929            .type_params
930            .iter()
931            .map(|param| param.fresh.clone())
932            .collect::<BTreeSet<_>>();
933        let mut vars = TypePositionVars::default();
934        collect_type_position_vars(
935            &self.principal_type,
936            TypePosition::Value,
937            &params,
938            &mut vars,
939        );
940        vars.effect
941            .difference(&vars.value)
942            .cloned()
943            .collect::<BTreeSet<_>>()
944    }
945}
946
947#[derive(Debug, Clone, PartialEq, Eq)]
948pub struct PrincipalTypeParam {
949    pub original: typed_ir::TypeVar,
950    pub fresh: typed_ir::TypeVar,
951}
952
953#[derive(Debug, Default, Clone, PartialEq, Eq)]
954pub struct RuntimeBounds {
955    pub lower: Option<RuntimeType>,
956    pub upper: Option<RuntimeType>,
957}
958
959impl RuntimeBounds {
960    pub fn lower(ty: RuntimeType) -> Self {
961        Self {
962            lower: Some(ty),
963            upper: None,
964        }
965    }
966
967    pub fn upper(ty: RuntimeType) -> Self {
968        Self {
969            lower: None,
970            upper: Some(ty),
971        }
972    }
973
974    pub fn exact(ty: RuntimeType) -> Self {
975        Self {
976            lower: Some(ty.clone()),
977            upper: Some(ty),
978        }
979    }
980}
981
982#[derive(Debug, Default, Clone, PartialEq, Eq)]
983pub struct TypeVarBounds {
984    pub lower: Option<typed_ir::Type>,
985    pub upper: Option<typed_ir::Type>,
986}
987
988impl TypeVarBounds {
989    pub fn solution(self) -> Option<typed_ir::Type> {
990        self.lower.or(self.upper)
991    }
992
993    pub fn solution_ref(&self) -> Option<&typed_ir::Type> {
994        self.lower.as_ref().or(self.upper.as_ref())
995    }
996
997    fn push_lower(
998        &mut self,
999        var: typed_ir::TypeVar,
1000        ty: typed_ir::Type,
1001        cast_order: &TypeCastOrder,
1002    ) -> MonomorphizeResult<bool> {
1003        push_bound(&mut self.lower, var, ty, BoundSide::Lower, cast_order)
1004    }
1005
1006    fn push_upper(
1007        &mut self,
1008        var: typed_ir::TypeVar,
1009        ty: typed_ir::Type,
1010        cast_order: &TypeCastOrder,
1011    ) -> MonomorphizeResult<bool> {
1012        push_bound(&mut self.upper, var, ty, BoundSide::Upper, cast_order)
1013    }
1014}
1015
1016#[derive(Debug, Clone, PartialEq, Eq)]
1017pub struct GraphSolution {
1018    pub type_vars: Vec<ResolvedTypeVar>,
1019}
1020
1021#[derive(Debug, Clone, PartialEq, Eq)]
1022pub struct ResolvedTypeVar {
1023    pub var: typed_ir::TypeVar,
1024    pub bounds: TypeVarBounds,
1025    pub solution: Option<typed_ir::Type>,
1026}
1027
1028impl GraphSolution {
1029    pub fn is_complete(&self) -> bool {
1030        self.type_vars.iter().all(|var| var.solution.is_some())
1031    }
1032
1033    pub fn substitutions(&self) -> Vec<typed_ir::TypeSubstitution> {
1034        self.type_vars
1035            .iter()
1036            .filter_map(|var| {
1037                var.solution.clone().map(|ty| typed_ir::TypeSubstitution {
1038                    var: var.var.clone(),
1039                    ty,
1040                })
1041            })
1042            .collect()
1043    }
1044
1045    pub fn solution_for(&self, var: &typed_ir::TypeVar) -> Option<&typed_ir::Type> {
1046        self.type_vars
1047            .iter()
1048            .find(|resolved| &resolved.var == var)
1049            .and_then(|resolved| resolved.solution.as_ref())
1050    }
1051
1052    pub fn materialize_core(&self, ty: typed_ir::Type) -> typed_ir::Type {
1053        materialize_type(ty, &self.substitutions())
1054    }
1055}
1056
1057pub fn materialize_core_type(
1058    ty: typed_ir::Type,
1059    substitutions: &[typed_ir::TypeSubstitution],
1060) -> typed_ir::Type {
1061    normalize_bound_form(&materialize_type(ty, substitutions))
1062}
1063
1064pub fn materialize_runtime_type(
1065    ty: RuntimeType,
1066    substitutions: &[typed_ir::TypeSubstitution],
1067) -> RuntimeType {
1068    match ty {
1069        RuntimeType::Unknown => RuntimeType::Unknown,
1070        RuntimeType::Value(ty) => {
1071            runtime_type_from_core_value(normalize_bound_form(&materialize_type(ty, substitutions)))
1072        }
1073        RuntimeType::Fun { param, ret } => RuntimeType::Fun {
1074            param: Box::new(materialize_runtime_type(*param, substitutions)),
1075            ret: Box::new(materialize_runtime_type(*ret, substitutions)),
1076        },
1077        RuntimeType::Thunk { effect, value } => RuntimeType::Thunk {
1078            effect: normalize_bound_form(&materialize_type(effect, substitutions)),
1079            value: Box::new(materialize_runtime_type(*value, substitutions)),
1080        },
1081    }
1082}
1083
1084/// Convert a fully materialized typed_ir value type into a VM-shaped runtime
1085/// type. Functions become `RuntimeType::Fun` with each side wrapped in
1086/// `Thunk` when its effect row is non-empty, so the VM's `expects_thunk_arg`
1087/// check sees the right shape on every callee position.
1088pub(crate) fn runtime_type_from_core_value(ty: typed_ir::Type) -> RuntimeType {
1089    match ty {
1090        typed_ir::Type::Fun {
1091            param,
1092            param_effect,
1093            ret_effect,
1094            ret,
1095        } => RuntimeType::Fun {
1096            param: Box::new(runtime_type_from_core_value_and_effect(
1097                *param,
1098                *param_effect,
1099            )),
1100            ret: Box::new(runtime_type_from_core_value_and_effect(*ret, *ret_effect)),
1101        },
1102        ty => RuntimeType::Value(ty),
1103    }
1104}
1105
1106pub(crate) fn runtime_type_from_core_value_and_effect(
1107    value: typed_ir::Type,
1108    effect: typed_ir::Type,
1109) -> RuntimeType {
1110    let value = runtime_type_from_core_value(value);
1111    if should_thunk_effect(&effect) {
1112        RuntimeType::Thunk {
1113            effect,
1114            value: Box::new(value),
1115        }
1116    } else {
1117        value
1118    }
1119}
1120
1121pub(crate) fn should_thunk_effect(effect: &typed_ir::Type) -> bool {
1122    !effect_is_empty(effect) && !matches!(effect, typed_ir::Type::Unknown)
1123}
1124
1125pub(crate) fn effect_is_empty(effect: &typed_ir::Type) -> bool {
1126    match effect {
1127        typed_ir::Type::Never => true,
1128        typed_ir::Type::Row { items, tail } => items.is_empty() && effect_is_empty(tail),
1129        typed_ir::Type::Recursive { body, .. } => effect_is_empty(body),
1130        _ => false,
1131    }
1132}
1133
1134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1135enum BoundSide {
1136    Lower,
1137    Upper,
1138}
1139
1140impl BoundSide {
1141    fn flip(self) -> Self {
1142        match self {
1143            Self::Lower => Self::Upper,
1144            Self::Upper => Self::Lower,
1145        }
1146    }
1147}
1148
1149struct RuntimeEffectedType<'a> {
1150    value: &'a RuntimeType,
1151    effect: RuntimeEffectRef<'a>,
1152}
1153
1154#[derive(Clone, Copy)]
1155enum RuntimeEffectRef<'a> {
1156    Known(&'a typed_ir::Type),
1157    Pure,
1158    Unknown,
1159}
1160
1161fn runtime_value_is_top(ty: &RuntimeType) -> bool {
1162    matches!(ty, RuntimeType::Value(typed_ir::Type::Any))
1163}
1164
1165fn split_runtime_effected_type(ty: &RuntimeType) -> RuntimeEffectedType<'_> {
1166    match ty {
1167        RuntimeType::Thunk { effect, value } => RuntimeEffectedType {
1168            value,
1169            effect: RuntimeEffectRef::Known(effect),
1170        },
1171        RuntimeType::Unknown => RuntimeEffectedType {
1172            value: ty,
1173            effect: RuntimeEffectRef::Unknown,
1174        },
1175        RuntimeType::Value(_) | RuntimeType::Fun { .. } => RuntimeEffectedType {
1176            value: ty,
1177            effect: RuntimeEffectRef::Pure,
1178        },
1179    }
1180}
1181
1182/// Effect-row lattice merge for type-var bounds.
1183///
1184/// Both inputs must be closed-tail row-shaped types (Row with Never tail, or
1185/// `Never`, or a single effect Named — which is treated as a one-item row).
1186/// `Lower` bounds get item **union** (a var observed with two lower bounds
1187/// must allow at least both rows' items). `Upper` bounds get item
1188/// **intersection** (a var bounded above by two rows can use at most what
1189/// both rows allow).
1190///
1191/// Returns `None` when the types are not row-shaped, when either tail is
1192/// open (a `Var` or other unsolved tail), or when intersection on an upper
1193/// would yield an empty row that disagrees with prior structure.
1194///
1195/// We intentionally do not flatten `Inter` tails or recurse through unbounded
1196/// row-of-row structures — that was the stack-overflow path in the previous
1197/// experimental layer.
1198fn merge_row_bounds(
1199    previous: &typed_ir::Type,
1200    ty: &typed_ir::Type,
1201    side: BoundSide,
1202) -> Option<typed_ir::Type> {
1203    // Require at least one side to be a genuine row-shaped type (Row / Never)
1204    // so we don't accidentally treat two value-type Named bounds as a row
1205    // union just because the bare Named could *look* like a one-item row.
1206    if !is_row_shaped(previous) && !is_row_shaped(ty) {
1207        return None;
1208    }
1209    let (mut prev_items, prev_tail) = flatten_closed_row_or_atom(previous)?;
1210    let (ty_items, ty_tail) = flatten_closed_row_or_atom(ty)?;
1211    let merged_tail = merge_row_tails(&prev_tail, &ty_tail, side)?;
1212    let merged_items = match side {
1213        BoundSide::Lower => {
1214            for item in ty_items {
1215                push_unique_effect_item(&mut prev_items, item);
1216            }
1217            prev_items
1218        }
1219        BoundSide::Upper => {
1220            let mut out = Vec::new();
1221            for item in &prev_items {
1222                if ty_items.iter().any(|other| effect_items_match(item, other)) {
1223                    push_unique_effect_item(&mut out, item.clone());
1224                }
1225            }
1226            out
1227        }
1228    };
1229    Some(typed_ir::Type::Row {
1230        items: merged_items,
1231        tail: Box::new(merged_tail),
1232    })
1233}
1234
1235fn merge_row_tails(
1236    previous: &typed_ir::Type,
1237    ty: &typed_ir::Type,
1238    side: BoundSide,
1239) -> Option<typed_ir::Type> {
1240    match (side, previous, ty) {
1241        (_, typed_ir::Type::Never, typed_ir::Type::Never) => Some(typed_ir::Type::Never),
1242        (_, typed_ir::Type::Any, typed_ir::Type::Any) => Some(typed_ir::Type::Any),
1243        (BoundSide::Lower, typed_ir::Type::Any, typed_ir::Type::Never)
1244        | (BoundSide::Lower, typed_ir::Type::Never, typed_ir::Type::Any) => {
1245            Some(typed_ir::Type::Any)
1246        }
1247        (BoundSide::Upper, typed_ir::Type::Any, typed_ir::Type::Never)
1248        | (BoundSide::Upper, typed_ir::Type::Never, typed_ir::Type::Any) => {
1249            Some(typed_ir::Type::Never)
1250        }
1251        _ => None,
1252    }
1253}
1254
1255fn is_row_shaped(ty: &typed_ir::Type) -> bool {
1256    matches!(ty, typed_ir::Type::Row { .. } | typed_ir::Type::Never)
1257}
1258
1259/// Like `flatten_closed_row` but additionally promotes a bare `Type::Named`
1260/// to a singleton row. Only safe to call from `merge_row_bounds`, which
1261/// gates the promotion behind the requirement that *at least one* of the
1262/// two bounds being merged is genuinely row-shaped — otherwise we would
1263/// risk treating a pair of value-type Named bounds as rows.
1264fn flatten_closed_row_or_atom(
1265    ty: &typed_ir::Type,
1266) -> Option<(Vec<typed_ir::Type>, typed_ir::Type)> {
1267    match ty {
1268        typed_ir::Type::Named { .. } => Some((vec![ty.clone()], typed_ir::Type::Never)),
1269        _ => flatten_closed_row(ty),
1270    }
1271}
1272
1273/// Flatten a row-shaped type into `(items, tail)`. Descends through nested
1274/// `Row` tails linearly. Also collapses an `Inter` tail when every branch
1275/// flattens to the same `(items, tail)`, which is the degenerate case the
1276/// constraint solver leaves behind when an intersection target turns out to
1277/// be redundant. Returns `None` for genuinely open tails (`Var`, `Any`,
1278/// non-degenerate `Inter`).
1279///
1280/// IMPORTANT: only `Type::Row` and `Type::Never` are recognized as rows here.
1281/// A bare `Type::Named` is *not* treated as a degenerate one-item row,
1282/// because the same shape is also used in value position (e.g.
1283/// `std::var::ref<..>`) and treating those as rows would let `push_bound`
1284/// silently fold value types into row unions and explode the constraint
1285/// loop.
1286fn flatten_closed_row(ty: &typed_ir::Type) -> Option<(Vec<typed_ir::Type>, typed_ir::Type)> {
1287    match ty {
1288        typed_ir::Type::Never => Some((Vec::new(), typed_ir::Type::Never)),
1289        typed_ir::Type::Row { items, tail } => {
1290            let mut out: Vec<typed_ir::Type> = Vec::new();
1291            for item in items {
1292                push_unique_effect_item(&mut out, item.clone());
1293            }
1294            let mut current_tail: typed_ir::Type = (**tail).clone();
1295            let mut depth = 0usize;
1296            loop {
1297                if depth >= 64 {
1298                    return None;
1299                }
1300                depth += 1;
1301                match current_tail {
1302                    typed_ir::Type::Row {
1303                        items: tail_items,
1304                        tail: next_tail,
1305                    } => {
1306                        for item in tail_items {
1307                            push_unique_effect_item(&mut out, item);
1308                        }
1309                        current_tail = *next_tail;
1310                    }
1311                    typed_ir::Type::Inter(branches) => {
1312                        let collapsed = collapse_equivalent_inter(&branches)?;
1313                        current_tail = collapsed;
1314                    }
1315                    typed_ir::Type::Never => return Some((out, typed_ir::Type::Never)),
1316                    other => return Some((out, other)),
1317                }
1318            }
1319        }
1320        _ => None,
1321    }
1322}
1323
1324/// If every branch of an `Inter` flattens to the same closed row, return
1325/// that single row. Otherwise return `None`. This catches the redundant
1326/// `Inter([Row[..], Row[..]])` shapes that fall out of subtype propagation
1327/// without committing us to a full intersection lattice.
1328fn collapse_equivalent_inter(branches: &[typed_ir::Type]) -> Option<typed_ir::Type> {
1329    let (first, rest) = branches.split_first()?;
1330    let (first_items, first_tail) = flatten_closed_row(first)?;
1331    for branch in rest {
1332        let (branch_items, branch_tail) = flatten_closed_row(branch)?;
1333        if !rows_equivalent(&first_items, &first_tail, &branch_items, &branch_tail) {
1334            return None;
1335        }
1336    }
1337    Some(typed_ir::Type::Row {
1338        items: first_items,
1339        tail: Box::new(first_tail),
1340    })
1341}
1342
1343fn intersect_closed_rows(branches: &[typed_ir::Type]) -> Option<typed_ir::Type> {
1344    let (first, rest) = branches.split_first()?;
1345    let (mut items, mut tail) = flatten_closed_row(first)?;
1346    for branch in rest {
1347        let (branch_items, branch_tail) = flatten_closed_row(branch)?;
1348        let mut intersection = Vec::new();
1349        for item in &items {
1350            if branch_items
1351                .iter()
1352                .any(|other| effect_items_match(item, other))
1353            {
1354                push_unique_effect_item(&mut intersection, item.clone());
1355            }
1356        }
1357        items = intersection;
1358        tail = merge_row_tails(&tail, &branch_tail, BoundSide::Upper)?;
1359    }
1360    Some(typed_ir::Type::Row {
1361        items,
1362        tail: Box::new(tail),
1363    })
1364}
1365
1366fn rows_equivalent(
1367    a_items: &[typed_ir::Type],
1368    a_tail: &typed_ir::Type,
1369    b_items: &[typed_ir::Type],
1370    b_tail: &typed_ir::Type,
1371) -> bool {
1372    if a_items.len() != b_items.len() {
1373        return false;
1374    }
1375    if !a_items
1376        .iter()
1377        .all(|item| b_items.iter().any(|other| effect_items_match(item, other)))
1378    {
1379        return false;
1380    }
1381    a_tail == b_tail
1382        || normalize_bound_form_inner(a_tail, true) == normalize_bound_form_inner(b_tail, true)
1383}
1384
1385fn push_unique_effect_item(items: &mut Vec<typed_ir::Type>, item: typed_ir::Type) {
1386    if let typed_ir::Type::Row {
1387        items: row_items,
1388        tail,
1389    } = item
1390    {
1391        if matches!(tail.as_ref(), typed_ir::Type::Never) {
1392            for item in row_items {
1393                push_unique_effect_item(items, item);
1394            }
1395            return;
1396        }
1397        let item = typed_ir::Type::Row {
1398            items: row_items,
1399            tail,
1400        };
1401        push_unique_non_row_effect_item(items, item);
1402        return;
1403    }
1404    push_unique_non_row_effect_item(items, item);
1405}
1406
1407fn push_unique_non_row_effect_item(items: &mut Vec<typed_ir::Type>, item: typed_ir::Type) {
1408    if matches!(item, typed_ir::Type::Never) {
1409        return;
1410    }
1411    if items
1412        .iter()
1413        .any(|existing| effect_items_match(existing, &item))
1414    {
1415        return;
1416    }
1417    items.push(item);
1418}
1419
1420fn effect_items_match(left: &typed_ir::Type, right: &typed_ir::Type) -> bool {
1421    left == right
1422        || normalize_bound_form_inner(left, true) == normalize_bound_form_inner(right, true)
1423}
1424
1425fn push_bound(
1426    slot: &mut Option<typed_ir::Type>,
1427    var: typed_ir::TypeVar,
1428    ty: typed_ir::Type,
1429    side: BoundSide,
1430    cast_order: &TypeCastOrder,
1431) -> MonomorphizeResult<bool> {
1432    if let Some(previous) = slot {
1433        if bounds_are_equivalent(previous, &ty) {
1434            return Ok(false);
1435        }
1436        if let Some(merged) = merge_unknown_bounds(previous, &ty) {
1437            if bounds_are_equivalent(previous, &merged) {
1438                return Ok(false);
1439            }
1440            *previous = merged;
1441            return Ok(true);
1442        }
1443        if let Some(merged) = merge_row_bounds(previous, &ty, side) {
1444            if bounds_are_equivalent(previous, &merged) {
1445                return Ok(false);
1446            }
1447            *previous = merged;
1448            return Ok(true);
1449        }
1450        if let Some(merged) = merge_ordered_bounds(previous, &ty, side, cast_order) {
1451            if bounds_are_equivalent(previous, &merged) {
1452                return Ok(false);
1453            }
1454            *previous = merged;
1455            return Ok(true);
1456        }
1457        if matches!(
1458            (&*previous, &ty),
1459            (typed_ir::Type::Var(_), typed_ir::Type::Var(_))
1460        ) {
1461            return Ok(false);
1462        }
1463        if matches!(ty, typed_ir::Type::Var(_)) && !matches!(previous, typed_ir::Type::Var(_)) {
1464            return Ok(false);
1465        }
1466        if matches!(previous, typed_ir::Type::Var(_)) && !matches!(ty, typed_ir::Type::Var(_)) {
1467            *previous = ty;
1468            return Ok(true);
1469        }
1470        if type_contains_var(previous) || type_contains_var(&ty) {
1471            return Ok(false);
1472        }
1473        return Err(MonomorphizeDiagnostic::ConflictingBounds {
1474            var,
1475            previous: previous.clone(),
1476            next: ty,
1477        });
1478    }
1479    *slot = Some(ty);
1480    Ok(true)
1481}
1482
1483fn reject_invalid_top_bottom_bounds(
1484    var: &typed_ir::TypeVar,
1485    bounds: &TypeVarBounds,
1486) -> MonomorphizeResult<()> {
1487    let (Some(lower), Some(upper)) = (&bounds.lower, &bounds.upper) else {
1488        return Ok(());
1489    };
1490    if matches!(lower, typed_ir::Type::Any)
1491        && !matches!(upper, typed_ir::Type::Any | typed_ir::Type::Never)
1492    {
1493        return Err(MonomorphizeDiagnostic::ConflictingBounds {
1494            var: var.clone(),
1495            previous: lower.clone(),
1496            next: upper.clone(),
1497        });
1498    }
1499    Ok(())
1500}
1501
1502fn merge_ordered_bounds(
1503    previous: &typed_ir::Type,
1504    ty: &typed_ir::Type,
1505    side: BoundSide,
1506    cast_order: &TypeCastOrder,
1507) -> Option<typed_ir::Type> {
1508    match side {
1509        BoundSide::Lower
1510            if matches!(previous, typed_ir::Type::Any) || matches!(ty, typed_ir::Type::Any) =>
1511        {
1512            Some(typed_ir::Type::Any)
1513        }
1514        BoundSide::Lower if union_supertype_contains(previous, ty) => Some(previous.clone()),
1515        BoundSide::Lower if union_supertype_contains(ty, previous) => Some(ty.clone()),
1516        BoundSide::Lower => {
1517            merge_lower_union_bound(previous, ty).or_else(|| cast_order.join_lower(previous, ty))
1518        }
1519        BoundSide::Upper
1520            if matches!(previous, typed_ir::Type::Never) || matches!(ty, typed_ir::Type::Never) =>
1521        {
1522            Some(typed_ir::Type::Never)
1523        }
1524        BoundSide::Upper if bound_subtype(previous, ty) => Some(previous.clone()),
1525        BoundSide::Upper if bound_subtype(ty, previous) => Some(ty.clone()),
1526        BoundSide::Upper => cast_order.meet_upper(previous, ty),
1527    }
1528}
1529
1530fn merge_lower_union_bound(
1531    previous: &typed_ir::Type,
1532    ty: &typed_ir::Type,
1533) -> Option<typed_ir::Type> {
1534    if !matches!(previous, typed_ir::Type::Union(_)) && !matches!(ty, typed_ir::Type::Union(_)) {
1535        return None;
1536    }
1537    let mut items = Vec::new();
1538    append_lower_union_items(&mut items, previous);
1539    append_lower_union_items(&mut items, ty);
1540    Some(normalize_bound_form(&typed_ir::Type::Union(items)))
1541}
1542
1543fn append_lower_union_items(items: &mut Vec<typed_ir::Type>, ty: &typed_ir::Type) {
1544    match ty {
1545        typed_ir::Type::Union(branches) => {
1546            for branch in branches {
1547                push_lower_union_item(items, branch.clone());
1548            }
1549        }
1550        ty => push_lower_union_item(items, ty.clone()),
1551    }
1552}
1553
1554fn push_lower_union_item(items: &mut Vec<typed_ir::Type>, item: typed_ir::Type) {
1555    if items
1556        .iter()
1557        .any(|existing| bounds_are_equivalent(existing, &item))
1558    {
1559        return;
1560    }
1561    items.push(item);
1562}
1563
1564fn bound_subtype(sub: &typed_ir::Type, sup: &typed_ir::Type) -> bool {
1565    if lightweight_bounds_equivalent(sub, sup)
1566        || matches!(sub, typed_ir::Type::Never)
1567        || matches!(sup, typed_ir::Type::Any)
1568        || function_bound_subtype(sub, sup)
1569        || row_bound_subtype(sub, sup)
1570        || union_supertype_contains(sup, sub)
1571    {
1572        return true;
1573    }
1574    match (sub, sup) {
1575        (typed_ir::Type::Union(items), _) => items.iter().all(|item| bound_subtype(item, sup)),
1576        (_, typed_ir::Type::Union(items)) => items.iter().any(|item| bound_subtype(sub, item)),
1577        (typed_ir::Type::Inter(items), _) => items.iter().any(|item| bound_subtype(item, sup)),
1578        (_, typed_ir::Type::Inter(items)) => items.iter().all(|item| bound_subtype(sub, item)),
1579        (typed_ir::Type::Recursive { var, body }, _) if !type_body_mentions_var(body, var) => {
1580            bound_subtype(&normalize_bound_form(body), sup)
1581        }
1582        (_, typed_ir::Type::Recursive { var, body }) if !type_body_mentions_var(body, var) => {
1583            bound_subtype(sub, &normalize_bound_form(body))
1584        }
1585        _ => false,
1586    }
1587}
1588
1589fn function_bound_subtype(sub: &typed_ir::Type, sup: &typed_ir::Type) -> bool {
1590    let (
1591        typed_ir::Type::Fun {
1592            param: sub_param,
1593            param_effect: sub_param_effect,
1594            ret_effect: sub_ret_effect,
1595            ret: sub_ret,
1596        },
1597        typed_ir::Type::Fun {
1598            param: sup_param,
1599            param_effect: sup_param_effect,
1600            ret_effect: sup_ret_effect,
1601            ret: sup_ret,
1602        },
1603    ) = (sub, sup)
1604    else {
1605        return false;
1606    };
1607    bound_subtype(sup_param, sub_param)
1608        && bound_subtype(sub_param_effect, sup_param_effect)
1609        && bound_subtype(sub_ret_effect, sup_ret_effect)
1610        && bound_subtype(sub_ret, sup_ret)
1611}
1612
1613fn union_supertype_contains(sup: &typed_ir::Type, sub: &typed_ir::Type) -> bool {
1614    let typed_ir::Type::Union(items) = sup else {
1615        return false;
1616    };
1617    items.iter().any(|item| {
1618        !matches!(item, typed_ir::Type::Union(_))
1619            && (lightweight_bounds_equivalent(sub, item) || bound_subtype(sub, item))
1620    })
1621}
1622
1623fn row_bound_subtype(sub: &typed_ir::Type, sup: &typed_ir::Type) -> bool {
1624    let Some((sub_items, sub_tail)) = flatten_closed_row(sub) else {
1625        return false;
1626    };
1627    let Some((sup_items, sup_tail)) = flatten_closed_row(sup) else {
1628        return false;
1629    };
1630    let residual = split_row_items(&sub_items, &sup_items);
1631    let items_covered =
1632        residual.unmatched_left.is_empty() || matches!(sup_tail, typed_ir::Type::Any);
1633    items_covered && row_tail_subtype(&sub_tail, &sup_tail)
1634}
1635
1636fn row_tail_subtype(sub: &typed_ir::Type, sup: &typed_ir::Type) -> bool {
1637    sub == sup || matches!(sub, typed_ir::Type::Never) || matches!(sup, typed_ir::Type::Any)
1638}
1639
1640fn lightweight_bounds_equivalent(left: &typed_ir::Type, right: &typed_ir::Type) -> bool {
1641    left == right || core_type_is_unit_value(left) && core_type_is_unit_value(right)
1642}
1643
1644fn merge_unknown_bounds(previous: &typed_ir::Type, ty: &typed_ir::Type) -> Option<typed_ir::Type> {
1645    if !type_contains_unknown(previous) && !type_contains_unknown(ty) {
1646        return None;
1647    }
1648    merge_unknown_bounds_inner(previous, ty)
1649}
1650
1651fn merge_unknown_bounds_inner(
1652    previous: &typed_ir::Type,
1653    ty: &typed_ir::Type,
1654) -> Option<typed_ir::Type> {
1655    match (previous, ty) {
1656        (typed_ir::Type::Unknown, _) => Some(ty.clone()),
1657        (_, typed_ir::Type::Unknown) => Some(previous.clone()),
1658        (
1659            typed_ir::Type::Named {
1660                path: previous_path,
1661                args: previous_args,
1662            },
1663            typed_ir::Type::Named { path, args },
1664        ) if previous_path == path && previous_args.len() == args.len() => {
1665            Some(typed_ir::Type::Named {
1666                path: path.clone(),
1667                args: previous_args
1668                    .iter()
1669                    .zip(args)
1670                    .map(|(previous, arg)| merge_unknown_type_arg_bounds(previous, arg))
1671                    .collect::<Option<Vec<_>>>()?,
1672            })
1673        }
1674        (
1675            typed_ir::Type::Fun {
1676                param: previous_param,
1677                param_effect: previous_param_effect,
1678                ret_effect: previous_ret_effect,
1679                ret: previous_ret,
1680            },
1681            typed_ir::Type::Fun {
1682                param,
1683                param_effect,
1684                ret_effect,
1685                ret,
1686            },
1687        ) => Some(typed_ir::Type::Fun {
1688            param: Box::new(merge_unknown_bounds_inner(previous_param, param)?),
1689            param_effect: Box::new(merge_unknown_bounds_inner(
1690                previous_param_effect,
1691                param_effect,
1692            )?),
1693            ret_effect: Box::new(merge_unknown_bounds_inner(previous_ret_effect, ret_effect)?),
1694            ret: Box::new(merge_unknown_bounds_inner(previous_ret, ret)?),
1695        }),
1696        (typed_ir::Type::Tuple(previous), typed_ir::Type::Tuple(items))
1697            if previous.len() == items.len() =>
1698        {
1699            Some(typed_ir::Type::Tuple(
1700                previous
1701                    .iter()
1702                    .zip(items)
1703                    .map(|(previous, item)| merge_unknown_bounds_inner(previous, item))
1704                    .collect::<Option<Vec<_>>>()?,
1705            ))
1706        }
1707        (
1708            typed_ir::Type::Row {
1709                items: previous_items,
1710                tail: previous_tail,
1711            },
1712            typed_ir::Type::Row { items, tail },
1713        ) if previous_items.len() == items.len() => Some(typed_ir::Type::Row {
1714            items: previous_items
1715                .iter()
1716                .zip(items)
1717                .map(|(previous, item)| merge_unknown_bounds_inner(previous, item))
1718                .collect::<Option<Vec<_>>>()?,
1719            tail: Box::new(merge_unknown_bounds_inner(previous_tail, tail)?),
1720        }),
1721        _ if previous == ty => Some(previous.clone()),
1722        _ => None,
1723    }
1724}
1725
1726fn merge_unknown_type_arg_bounds(
1727    previous: &typed_ir::TypeArg,
1728    arg: &typed_ir::TypeArg,
1729) -> Option<typed_ir::TypeArg> {
1730    match (previous, arg) {
1731        (typed_ir::TypeArg::Type(previous), typed_ir::TypeArg::Type(arg)) => Some(
1732            typed_ir::TypeArg::Type(merge_unknown_bounds_inner(previous, arg)?),
1733        ),
1734        (typed_ir::TypeArg::Bounds(previous), typed_ir::TypeArg::Bounds(arg)) => {
1735            Some(typed_ir::TypeArg::Bounds(typed_ir::TypeBounds {
1736                lower: merge_optional_unknown_bound(
1737                    previous.lower.as_deref(),
1738                    arg.lower.as_deref(),
1739                )?,
1740                upper: merge_optional_unknown_bound(
1741                    previous.upper.as_deref(),
1742                    arg.upper.as_deref(),
1743                )?,
1744            }))
1745        }
1746        _ => None,
1747    }
1748}
1749
1750fn merge_optional_unknown_bound(
1751    previous: Option<&typed_ir::Type>,
1752    ty: Option<&typed_ir::Type>,
1753) -> Option<Option<Box<typed_ir::Type>>> {
1754    match (previous, ty) {
1755        (Some(previous), Some(ty)) => {
1756            Some(Some(Box::new(merge_unknown_bounds_inner(previous, ty)?)))
1757        }
1758        (Some(previous), None) => Some(Some(Box::new(previous.clone()))),
1759        (None, Some(ty)) => Some(Some(Box::new(ty.clone()))),
1760        (None, None) => Some(None),
1761    }
1762}
1763
1764fn type_contains_var(ty: &typed_ir::Type) -> bool {
1765    match ty {
1766        typed_ir::Type::Var(_) => true,
1767        typed_ir::Type::Fun {
1768            param,
1769            param_effect,
1770            ret_effect,
1771            ret,
1772        } => {
1773            type_contains_var(param)
1774                || type_contains_var(param_effect)
1775                || type_contains_var(ret_effect)
1776                || type_contains_var(ret)
1777        }
1778        typed_ir::Type::Named { args, .. } => args.iter().any(type_arg_contains_var),
1779        typed_ir::Type::Tuple(items)
1780        | typed_ir::Type::Union(items)
1781        | typed_ir::Type::Inter(items) => items.iter().any(type_contains_var),
1782        typed_ir::Type::Row { items, tail } => {
1783            items.iter().any(type_contains_var) || type_contains_var(tail)
1784        }
1785        typed_ir::Type::Record(record) => {
1786            record
1787                .fields
1788                .iter()
1789                .any(|field| type_contains_var(&field.value))
1790                || record.spread.as_ref().is_some_and(|spread| match spread {
1791                    typed_ir::RecordSpread::Head(ty) | typed_ir::RecordSpread::Tail(ty) => {
1792                        type_contains_var(ty)
1793                    }
1794                })
1795        }
1796        typed_ir::Type::Variant(variant) => {
1797            variant
1798                .cases
1799                .iter()
1800                .any(|case| case.payloads.iter().any(type_contains_var))
1801                || variant
1802                    .tail
1803                    .as_ref()
1804                    .is_some_and(|tail| type_contains_var(tail))
1805        }
1806        typed_ir::Type::Recursive { body, .. } => type_contains_var(body),
1807        typed_ir::Type::Unknown | typed_ir::Type::Never | typed_ir::Type::Any => false,
1808    }
1809}
1810
1811fn type_arg_contains_var(arg: &typed_ir::TypeArg) -> bool {
1812    match arg {
1813        typed_ir::TypeArg::Type(ty) => type_contains_var(ty),
1814        typed_ir::TypeArg::Bounds(bounds) => {
1815            bounds
1816                .lower
1817                .as_ref()
1818                .is_some_and(|ty| type_contains_var(ty))
1819                || bounds
1820                    .upper
1821                    .as_ref()
1822                    .is_some_and(|ty| type_contains_var(ty))
1823        }
1824    }
1825}
1826
1827fn type_body_mentions_var(ty: &typed_ir::Type, target: &typed_ir::TypeVar) -> bool {
1828    match ty {
1829        typed_ir::Type::Var(var) => var == target,
1830        typed_ir::Type::Fun {
1831            param,
1832            param_effect,
1833            ret_effect,
1834            ret,
1835        } => {
1836            type_body_mentions_var(param, target)
1837                || type_body_mentions_var(param_effect, target)
1838                || type_body_mentions_var(ret_effect, target)
1839                || type_body_mentions_var(ret, target)
1840        }
1841        typed_ir::Type::Named { args, .. } => args
1842            .iter()
1843            .any(|arg| type_arg_body_mentions_var(arg, target)),
1844        typed_ir::Type::Tuple(items)
1845        | typed_ir::Type::Union(items)
1846        | typed_ir::Type::Inter(items) => items
1847            .iter()
1848            .any(|item| type_body_mentions_var(item, target)),
1849        typed_ir::Type::Row { items, tail } => {
1850            items
1851                .iter()
1852                .any(|item| type_body_mentions_var(item, target))
1853                || type_body_mentions_var(tail, target)
1854        }
1855        typed_ir::Type::Record(record) => {
1856            record
1857                .fields
1858                .iter()
1859                .any(|field| type_body_mentions_var(&field.value, target))
1860                || record.spread.as_ref().is_some_and(|spread| match spread {
1861                    typed_ir::RecordSpread::Head(ty) | typed_ir::RecordSpread::Tail(ty) => {
1862                        type_body_mentions_var(ty, target)
1863                    }
1864                })
1865        }
1866        typed_ir::Type::Variant(variant) => {
1867            variant.cases.iter().any(|case| {
1868                case.payloads
1869                    .iter()
1870                    .any(|ty| type_body_mentions_var(ty, target))
1871            }) || variant
1872                .tail
1873                .as_ref()
1874                .is_some_and(|tail| type_body_mentions_var(tail, target))
1875        }
1876        typed_ir::Type::Recursive { body, .. } => type_body_mentions_var(body, target),
1877        typed_ir::Type::Unknown | typed_ir::Type::Never | typed_ir::Type::Any => false,
1878    }
1879}
1880
1881fn type_arg_body_mentions_var(arg: &typed_ir::TypeArg, target: &typed_ir::TypeVar) -> bool {
1882    match arg {
1883        typed_ir::TypeArg::Type(ty) => type_body_mentions_var(ty, target),
1884        typed_ir::TypeArg::Bounds(bounds) => {
1885            bounds
1886                .lower
1887                .as_ref()
1888                .is_some_and(|ty| type_body_mentions_var(ty, target))
1889                || bounds
1890                    .upper
1891                    .as_ref()
1892                    .is_some_and(|ty| type_body_mentions_var(ty, target))
1893        }
1894    }
1895}
1896
1897fn bounds_are_equivalent(left: &typed_ir::Type, right: &typed_ir::Type) -> bool {
1898    left == right
1899        || core_type_is_unit_value(left) && core_type_is_unit_value(right)
1900        || closed_singleton_row_item(left).is_some_and(|item| item == right)
1901        || closed_singleton_row_item(right).is_some_and(|item| item == left)
1902        || normalize_bound_form(left) == normalize_bound_form(right)
1903}
1904
1905fn core_type_is_unit_value(ty: &typed_ir::Type) -> bool {
1906    match ty {
1907        typed_ir::Type::Tuple(items) => items.is_empty(),
1908        typed_ir::Type::Named { path, args } => {
1909            args.is_empty()
1910                && path
1911                    .segments
1912                    .last()
1913                    .is_some_and(|segment| segment.0 == "unit")
1914        }
1915        _ => false,
1916    }
1917}
1918
1919fn normalize_bound_form(ty: &typed_ir::Type) -> typed_ir::Type {
1920    normalize_bound_form_inner(ty, false)
1921}
1922
1923fn normalize_bound_form_inner(ty: &typed_ir::Type, effect_atom: bool) -> typed_ir::Type {
1924    match ty {
1925        typed_ir::Type::Named { path, args } => typed_ir::Type::Named {
1926            path: path.clone(),
1927            args: if effect_atom && args.iter().any(type_arg_contains_unknown) {
1928                Vec::new()
1929            } else {
1930                args.iter()
1931                    .map(|arg| normalize_bound_arg_form(arg, effect_atom))
1932                    .collect()
1933            },
1934        },
1935        typed_ir::Type::Fun {
1936            param,
1937            param_effect,
1938            ret_effect,
1939            ret,
1940        } => typed_ir::Type::Fun {
1941            param: Box::new(normalize_bound_form_inner(param, false)),
1942            param_effect: Box::new(normalize_bound_form_inner(param_effect, false)),
1943            ret_effect: Box::new(normalize_bound_form_inner(ret_effect, false)),
1944            ret: Box::new(normalize_bound_form_inner(ret, false)),
1945        },
1946        typed_ir::Type::Tuple(items) => typed_ir::Type::Tuple(
1947            items
1948                .iter()
1949                .map(|item| normalize_bound_form_inner(item, false))
1950                .collect(),
1951        ),
1952        typed_ir::Type::Union(items) => {
1953            let mut normalized = Vec::new();
1954            for item in items
1955                .iter()
1956                .map(|item| normalize_bound_form_inner(item, false))
1957            {
1958                if matches!(item, typed_ir::Type::Any) {
1959                    return typed_ir::Type::Any;
1960                }
1961                if matches!(item, typed_ir::Type::Never) {
1962                    continue;
1963                }
1964                match item {
1965                    typed_ir::Type::Union(items) => {
1966                        for item in items {
1967                            push_normalized_union_item(&mut normalized, item);
1968                        }
1969                    }
1970                    item => {
1971                        push_normalized_union_item(&mut normalized, item);
1972                    }
1973                }
1974            }
1975            match normalized.len() {
1976                0 => typed_ir::Type::Never,
1977                1 => normalized.pop().unwrap(),
1978                _ => typed_ir::Type::Union(normalized),
1979            }
1980        }
1981        typed_ir::Type::Inter(items) => {
1982            let mut normalized = Vec::new();
1983            for item in items
1984                .iter()
1985                .map(|item| normalize_bound_form_inner(item, false))
1986            {
1987                if matches!(item, typed_ir::Type::Any) {
1988                    continue;
1989                }
1990                if matches!(item, typed_ir::Type::Never) {
1991                    return typed_ir::Type::Never;
1992                }
1993                match item {
1994                    typed_ir::Type::Inter(items) => {
1995                        for item in items {
1996                            push_normalized_inter_item(&mut normalized, item);
1997                        }
1998                    }
1999                    item => {
2000                        push_normalized_inter_item(&mut normalized, item);
2001                    }
2002                }
2003            }
2004            if let Some(row) = intersect_closed_rows(&normalized) {
2005                return row;
2006            }
2007            match normalized.len() {
2008                0 => typed_ir::Type::Any,
2009                1 => normalized.pop().unwrap(),
2010                _ => typed_ir::Type::Inter(normalized),
2011            }
2012        }
2013        typed_ir::Type::Row { items, tail } => {
2014            // Flatten nested rows that share a `Never` tail so that
2015            // `Row<[next], Row<[last]; Never>>` collapses to the flat
2016            // `Row<[next, last]; Never>` form before we normalize items.
2017            let (raw_items, raw_tail) = match flatten_closed_row(ty) {
2018                Some(flat) => flat,
2019                None => (items.clone(), (**tail).clone()),
2020            };
2021            let mut normalized_items = Vec::new();
2022            for item in raw_items
2023                .into_iter()
2024                .map(|item| normalize_bound_form_inner(&item, true))
2025            {
2026                push_unique_effect_item(&mut normalized_items, item);
2027            }
2028            // Effect rows are unordered sets; canonicalize item order so
2029            // `[next, last]` and `[last, next]` compare equal across bound merges.
2030            normalized_items.sort_by_key(|item| format!("{item:?}"));
2031            let tail = normalize_bound_form_inner(&raw_tail, false);
2032            if normalized_items.is_empty() {
2033                tail
2034            } else {
2035                typed_ir::Type::Row {
2036                    items: normalized_items,
2037                    tail: Box::new(tail),
2038                }
2039            }
2040        }
2041        typed_ir::Type::Record(record) => typed_ir::Type::Record(typed_ir::RecordType {
2042            fields: record
2043                .fields
2044                .iter()
2045                .map(|field| typed_ir::RecordField {
2046                    name: field.name.clone(),
2047                    value: normalize_bound_form_inner(&field.value, false),
2048                    optional: field.optional,
2049                })
2050                .collect(),
2051            spread: record.spread.as_ref().map(|spread| match spread {
2052                typed_ir::RecordSpread::Head(ty) => {
2053                    typed_ir::RecordSpread::Head(Box::new(normalize_bound_form_inner(ty, false)))
2054                }
2055                typed_ir::RecordSpread::Tail(ty) => {
2056                    typed_ir::RecordSpread::Tail(Box::new(normalize_bound_form_inner(ty, false)))
2057                }
2058            }),
2059        }),
2060        typed_ir::Type::Variant(variant) => typed_ir::Type::Variant(typed_ir::VariantType {
2061            cases: variant
2062                .cases
2063                .iter()
2064                .map(|case| typed_ir::VariantCase {
2065                    name: case.name.clone(),
2066                    payloads: case
2067                        .payloads
2068                        .iter()
2069                        .map(|payload| normalize_bound_form_inner(payload, false))
2070                        .collect(),
2071                })
2072                .collect(),
2073            tail: variant
2074                .tail
2075                .as_ref()
2076                .map(|tail| Box::new(normalize_bound_form_inner(tail, false))),
2077        }),
2078        typed_ir::Type::Recursive { var, body }
2079            if effect_atom && !type_body_mentions_var(body, var) =>
2080        {
2081            normalize_bound_form_inner(body, effect_atom)
2082        }
2083        typed_ir::Type::Recursive { var, body } => typed_ir::Type::Recursive {
2084            var: var.clone(),
2085            body: Box::new(normalize_bound_form_inner(body, effect_atom)),
2086        },
2087        typed_ir::Type::Var(_)
2088        | typed_ir::Type::Unknown
2089        | typed_ir::Type::Never
2090        | typed_ir::Type::Any => ty.clone(),
2091    }
2092}
2093
2094fn normalize_bound_arg_form(arg: &typed_ir::TypeArg, effect_atom: bool) -> typed_ir::TypeArg {
2095    match arg {
2096        typed_ir::TypeArg::Type(ty) => {
2097            typed_ir::TypeArg::Type(normalize_bound_form_inner(ty, effect_atom))
2098        }
2099        typed_ir::TypeArg::Bounds(bounds) => {
2100            if let Some(lower) = bounds
2101                .lower
2102                .as_deref()
2103                .filter(|lower| !matches!(lower, typed_ir::Type::Never))
2104            {
2105                return typed_ir::TypeArg::Type(normalize_bound_form_inner(lower, effect_atom));
2106            }
2107            if let Some(upper) = bounds
2108                .upper
2109                .as_deref()
2110                .filter(|upper| !matches!(upper, typed_ir::Type::Any))
2111            {
2112                return typed_ir::TypeArg::Type(normalize_bound_form_inner(upper, effect_atom));
2113            }
2114            typed_ir::TypeArg::Bounds(typed_ir::TypeBounds {
2115                lower: bounds
2116                    .lower
2117                    .as_ref()
2118                    .map(|lower| Box::new(normalize_bound_form_inner(lower, effect_atom))),
2119                upper: bounds
2120                    .upper
2121                    .as_ref()
2122                    .map(|upper| Box::new(normalize_bound_form_inner(upper, effect_atom))),
2123            })
2124        }
2125    }
2126}
2127
2128fn push_normalized_union_item(items: &mut Vec<typed_ir::Type>, item: typed_ir::Type) {
2129    if matches!(item, typed_ir::Type::Never) {
2130        return;
2131    }
2132    if matches!(item, typed_ir::Type::Any) {
2133        items.clear();
2134        items.push(item);
2135        return;
2136    }
2137    if items
2138        .iter()
2139        .any(|existing| absorption_subtype(&item, existing))
2140    {
2141        return;
2142    }
2143    items.retain(|existing| !absorption_subtype(existing, &item));
2144    items.push(item);
2145}
2146
2147fn push_normalized_inter_item(items: &mut Vec<typed_ir::Type>, item: typed_ir::Type) {
2148    if matches!(item, typed_ir::Type::Any) {
2149        return;
2150    }
2151    if matches!(item, typed_ir::Type::Never) {
2152        items.clear();
2153        items.push(item);
2154        return;
2155    }
2156    if items
2157        .iter()
2158        .any(|existing| absorption_subtype(existing, &item))
2159    {
2160        return;
2161    }
2162    items.retain(|existing| !absorption_subtype(&item, existing));
2163    items.push(item);
2164}
2165
2166fn absorption_subtype(sub: &typed_ir::Type, sup: &typed_ir::Type) -> bool {
2167    if lightweight_bounds_equivalent(sub, sup)
2168        || closed_singleton_row_item(sub).is_some_and(|item| item == sup)
2169        || closed_singleton_row_item(sup).is_some_and(|item| item == sub)
2170        || matches!(sub, typed_ir::Type::Never)
2171        || matches!(sup, typed_ir::Type::Any)
2172    {
2173        return true;
2174    }
2175    match (sub, sup) {
2176        (typed_ir::Type::Union(items), _) => items.iter().all(|item| absorption_subtype(item, sup)),
2177        (_, typed_ir::Type::Union(items)) => items.iter().any(|item| absorption_subtype(sub, item)),
2178        (typed_ir::Type::Inter(items), _) => items.iter().any(|item| absorption_subtype(item, sup)),
2179        (_, typed_ir::Type::Inter(items)) => items.iter().all(|item| absorption_subtype(sub, item)),
2180        (
2181            typed_ir::Type::Fun {
2182                param: sub_param,
2183                param_effect: sub_param_effect,
2184                ret_effect: sub_ret_effect,
2185                ret: sub_ret,
2186            },
2187            typed_ir::Type::Fun {
2188                param: sup_param,
2189                param_effect: sup_param_effect,
2190                ret_effect: sup_ret_effect,
2191                ret: sup_ret,
2192            },
2193        ) => {
2194            absorption_subtype(sup_param, sub_param)
2195                && absorption_subtype(sub_param_effect, sup_param_effect)
2196                && absorption_subtype(sub_ret_effect, sup_ret_effect)
2197                && absorption_subtype(sub_ret, sup_ret)
2198        }
2199        _ => row_bound_subtype(sub, sup),
2200    }
2201}
2202
2203fn type_arg_contains_unknown(arg: &typed_ir::TypeArg) -> bool {
2204    match arg {
2205        typed_ir::TypeArg::Type(ty) => type_contains_unknown(ty),
2206        typed_ir::TypeArg::Bounds(bounds) => {
2207            bounds
2208                .lower
2209                .as_ref()
2210                .is_some_and(|ty| type_contains_unknown(ty))
2211                || bounds
2212                    .upper
2213                    .as_ref()
2214                    .is_some_and(|ty| type_contains_unknown(ty))
2215        }
2216    }
2217}
2218
2219fn type_contains_unknown(ty: &typed_ir::Type) -> bool {
2220    match ty {
2221        typed_ir::Type::Unknown => true,
2222        typed_ir::Type::Fun {
2223            param,
2224            param_effect,
2225            ret_effect,
2226            ret,
2227        } => {
2228            type_contains_unknown(param)
2229                || type_contains_unknown(param_effect)
2230                || type_contains_unknown(ret_effect)
2231                || type_contains_unknown(ret)
2232        }
2233        typed_ir::Type::Named { args, .. } => args.iter().any(type_arg_contains_unknown),
2234        typed_ir::Type::Tuple(items)
2235        | typed_ir::Type::Union(items)
2236        | typed_ir::Type::Inter(items) => items.iter().any(type_contains_unknown),
2237        typed_ir::Type::Row { items, tail } => {
2238            items.iter().any(type_contains_unknown) || type_contains_unknown(tail)
2239        }
2240        typed_ir::Type::Record(record) => {
2241            record
2242                .fields
2243                .iter()
2244                .any(|field| type_contains_unknown(&field.value))
2245                || record.spread.as_ref().is_some_and(|spread| match spread {
2246                    typed_ir::RecordSpread::Head(ty) | typed_ir::RecordSpread::Tail(ty) => {
2247                        type_contains_unknown(ty)
2248                    }
2249                })
2250        }
2251        typed_ir::Type::Variant(variant) => {
2252            variant
2253                .cases
2254                .iter()
2255                .any(|case| case.payloads.iter().any(type_contains_unknown))
2256                || variant
2257                    .tail
2258                    .as_ref()
2259                    .is_some_and(|tail| type_contains_unknown(tail))
2260        }
2261        typed_ir::Type::Recursive { body, .. } => type_contains_unknown(body),
2262        typed_ir::Type::Var(_) | typed_ir::Type::Never | typed_ir::Type::Any => false,
2263    }
2264}
2265
2266fn closed_singleton_row_item(ty: &typed_ir::Type) -> Option<&typed_ir::Type> {
2267    let typed_ir::Type::Row { items, tail } = ty else {
2268        return None;
2269    };
2270    if items.len() == 1 && matches!(tail.as_ref(), typed_ir::Type::Never) {
2271        items.first()
2272    } else {
2273        None
2274    }
2275}
2276
2277fn is_vacuous_bound(ty: &typed_ir::Type, side: BoundSide) -> bool {
2278    matches!(
2279        (side, ty),
2280        (BoundSide::Lower, typed_ir::Type::Never) | (BoundSide::Upper, typed_ir::Type::Any)
2281    )
2282}
2283
2284struct RowResidual<'a> {
2285    matched: Vec<(&'a typed_ir::Type, &'a typed_ir::Type)>,
2286    unmatched_left: Vec<typed_ir::Type>,
2287    unmatched_right: Vec<typed_ir::Type>,
2288}
2289
2290#[derive(Default)]
2291struct TypePositionVars {
2292    value: BTreeSet<typed_ir::TypeVar>,
2293    effect: BTreeSet<typed_ir::TypeVar>,
2294}
2295
2296#[derive(Clone, Copy)]
2297enum TypePosition {
2298    Value,
2299    Effect,
2300}
2301
2302fn collect_type_position_vars(
2303    ty: &typed_ir::Type,
2304    position: TypePosition,
2305    params: &BTreeSet<typed_ir::TypeVar>,
2306    vars: &mut TypePositionVars,
2307) {
2308    match ty {
2309        typed_ir::Type::Var(var) if params.contains(var) => match position {
2310            TypePosition::Value => {
2311                vars.value.insert(var.clone());
2312            }
2313            TypePosition::Effect => {
2314                vars.effect.insert(var.clone());
2315            }
2316        },
2317        typed_ir::Type::Var(_)
2318        | typed_ir::Type::Unknown
2319        | typed_ir::Type::Never
2320        | typed_ir::Type::Any => {}
2321        typed_ir::Type::Fun {
2322            param,
2323            param_effect,
2324            ret_effect,
2325            ret,
2326        } => {
2327            collect_type_position_vars(param, TypePosition::Value, params, vars);
2328            collect_type_position_vars(param_effect, TypePosition::Effect, params, vars);
2329            collect_type_position_vars(ret_effect, TypePosition::Effect, params, vars);
2330            collect_type_position_vars(ret, TypePosition::Value, params, vars);
2331        }
2332        typed_ir::Type::Named { args, .. } => {
2333            for arg in args {
2334                collect_type_arg_position_vars(arg, params, vars);
2335            }
2336        }
2337        typed_ir::Type::Tuple(items)
2338        | typed_ir::Type::Union(items)
2339        | typed_ir::Type::Inter(items) => {
2340            for item in items {
2341                collect_type_position_vars(item, position, params, vars);
2342            }
2343        }
2344        typed_ir::Type::Row { items, tail } => {
2345            for item in items {
2346                collect_effect_item_position_vars(item, params, vars);
2347            }
2348            collect_type_position_vars(tail, TypePosition::Effect, params, vars);
2349        }
2350        typed_ir::Type::Record(record) => {
2351            for field in &record.fields {
2352                collect_type_position_vars(&field.value, TypePosition::Value, params, vars);
2353            }
2354            if let Some(spread) = &record.spread {
2355                collect_record_spread_position_vars(spread, params, vars);
2356            }
2357        }
2358        typed_ir::Type::Variant(variant) => {
2359            for case in &variant.cases {
2360                for payload in &case.payloads {
2361                    collect_type_position_vars(payload, TypePosition::Value, params, vars);
2362                }
2363            }
2364            if let Some(tail) = &variant.tail {
2365                collect_type_position_vars(tail, TypePosition::Value, params, vars);
2366            }
2367        }
2368        typed_ir::Type::Recursive { body, .. } => {
2369            collect_type_position_vars(body, position, params, vars);
2370        }
2371    }
2372}
2373
2374fn collect_effect_item_position_vars(
2375    ty: &typed_ir::Type,
2376    params: &BTreeSet<typed_ir::TypeVar>,
2377    vars: &mut TypePositionVars,
2378) {
2379    match ty {
2380        typed_ir::Type::Named { args, .. } => {
2381            for arg in args {
2382                collect_type_arg_position_vars(arg, params, vars);
2383            }
2384        }
2385        other => collect_type_position_vars(other, TypePosition::Effect, params, vars),
2386    }
2387}
2388
2389fn collect_type_arg_position_vars(
2390    arg: &typed_ir::TypeArg,
2391    params: &BTreeSet<typed_ir::TypeVar>,
2392    vars: &mut TypePositionVars,
2393) {
2394    match arg {
2395        typed_ir::TypeArg::Type(ty) => {
2396            collect_type_position_vars(ty, TypePosition::Value, params, vars);
2397        }
2398        typed_ir::TypeArg::Bounds(bounds) => {
2399            if let Some(lower) = bounds.lower.as_deref() {
2400                collect_type_position_vars(lower, TypePosition::Value, params, vars);
2401            }
2402            if let Some(upper) = bounds.upper.as_deref() {
2403                collect_type_position_vars(upper, TypePosition::Value, params, vars);
2404            }
2405        }
2406    }
2407}
2408
2409fn collect_record_spread_position_vars(
2410    spread: &typed_ir::RecordSpread,
2411    params: &BTreeSet<typed_ir::TypeVar>,
2412    vars: &mut TypePositionVars,
2413) {
2414    match spread {
2415        typed_ir::RecordSpread::Head(ty) | typed_ir::RecordSpread::Tail(ty) => {
2416            collect_type_position_vars(ty, TypePosition::Value, params, vars);
2417        }
2418    }
2419}
2420
2421fn split_row_items<'a>(
2422    left_items: &'a [typed_ir::Type],
2423    right_items: &'a [typed_ir::Type],
2424) -> RowResidual<'a> {
2425    let mut matched_right = vec![false; right_items.len()];
2426    let mut matched = Vec::new();
2427    let mut unmatched_left = Vec::new();
2428
2429    for left in left_items {
2430        let Some((index, right)) = right_items
2431            .iter()
2432            .enumerate()
2433            .find(|(index, right)| !matched_right[*index] && same_effect_head(left, right))
2434        else {
2435            unmatched_left.push(left.clone());
2436            continue;
2437        };
2438        matched_right[index] = true;
2439        matched.push((left, right));
2440    }
2441
2442    let unmatched_right = right_items
2443        .iter()
2444        .enumerate()
2445        .filter_map(|(index, right)| (!matched_right[index]).then_some(right.clone()))
2446        .collect();
2447
2448    RowResidual {
2449        matched,
2450        unmatched_left,
2451        unmatched_right,
2452    }
2453}
2454
2455fn find_variant_case<'a>(
2456    variant: &'a typed_ir::VariantType,
2457    name: &typed_ir::Name,
2458) -> Option<&'a typed_ir::VariantCase> {
2459    variant.cases.iter().find(|case| &case.name == name)
2460}
2461
2462fn same_effect_head(left: &typed_ir::Type, right: &typed_ir::Type) -> bool {
2463    match (left, right) {
2464        (
2465            typed_ir::Type::Named { path, .. },
2466            typed_ir::Type::Named {
2467                path: actual_path, ..
2468            },
2469        ) => effect_paths_match(path, actual_path),
2470        _ => false,
2471    }
2472}
2473
2474fn effect_paths_match(left: &typed_ir::Path, right: &typed_ir::Path) -> bool {
2475    left == right
2476        || qualified_prefix_effect_paths_match(left, right)
2477        || qualified_prefix_effect_paths_match(right, left)
2478}
2479
2480fn qualified_prefix_effect_paths_match(parent: &typed_ir::Path, child: &typed_ir::Path) -> bool {
2481    effect_path_can_match_child_prefix(parent)
2482        && child.segments.len() > parent.segments.len()
2483        && child.segments.starts_with(parent.segments.as_slice())
2484}
2485
2486fn effect_path_can_match_child_prefix(path: &typed_ir::Path) -> bool {
2487    path.segments.len() > 1
2488        || path.segments.first().is_some_and(|name| {
2489            name.0.starts_with('#') || name.0.starts_with('&') && name.0.contains('#')
2490        })
2491}
2492
2493fn effect_row_from_items_and_tail(
2494    items: Vec<typed_ir::Type>,
2495    tail: typed_ir::Type,
2496) -> typed_ir::Type {
2497    if items.is_empty() {
2498        return tail;
2499    }
2500    typed_ir::Type::Row {
2501        items,
2502        tail: Box::new(tail),
2503    }
2504}
2505
2506fn rename_type(
2507    ty: &typed_ir::Type,
2508    renames: &BTreeMap<typed_ir::TypeVar, typed_ir::TypeVar>,
2509) -> typed_ir::Type {
2510    match ty {
2511        typed_ir::Type::Var(var) => renames
2512            .get(var)
2513            .cloned()
2514            .map(typed_ir::Type::Var)
2515            .unwrap_or_else(|| typed_ir::Type::Var(var.clone())),
2516        typed_ir::Type::Fun {
2517            param,
2518            param_effect,
2519            ret_effect,
2520            ret,
2521        } => typed_ir::Type::Fun {
2522            param: Box::new(rename_type(param, renames)),
2523            param_effect: Box::new(rename_type(param_effect, renames)),
2524            ret_effect: Box::new(rename_type(ret_effect, renames)),
2525            ret: Box::new(rename_type(ret, renames)),
2526        },
2527        typed_ir::Type::Tuple(items) => typed_ir::Type::Tuple(
2528            items
2529                .iter()
2530                .map(|item| rename_type(item, renames))
2531                .collect(),
2532        ),
2533        typed_ir::Type::Named { path, args } => typed_ir::Type::Named {
2534            path: path.clone(),
2535            args: args
2536                .iter()
2537                .map(|arg| rename_type_arg(arg, renames))
2538                .collect(),
2539        },
2540        typed_ir::Type::Row { items, tail } => typed_ir::Type::Row {
2541            items: items
2542                .iter()
2543                .map(|item| rename_type(item, renames))
2544                .collect(),
2545            tail: Box::new(rename_type(tail, renames)),
2546        },
2547        typed_ir::Type::Union(items) => typed_ir::Type::Union(
2548            items
2549                .iter()
2550                .map(|item| rename_type(item, renames))
2551                .collect(),
2552        ),
2553        typed_ir::Type::Inter(items) => typed_ir::Type::Inter(
2554            items
2555                .iter()
2556                .map(|item| rename_type(item, renames))
2557                .collect(),
2558        ),
2559        typed_ir::Type::Record(record) => typed_ir::Type::Record(typed_ir::RecordType {
2560            fields: record
2561                .fields
2562                .iter()
2563                .map(|field| typed_ir::RecordField {
2564                    name: field.name.clone(),
2565                    value: rename_type(&field.value, renames),
2566                    optional: field.optional,
2567                })
2568                .collect(),
2569            spread: record
2570                .spread
2571                .as_ref()
2572                .map(|spread| rename_record_spread(spread, renames)),
2573        }),
2574        typed_ir::Type::Variant(variant) => typed_ir::Type::Variant(typed_ir::VariantType {
2575            cases: variant
2576                .cases
2577                .iter()
2578                .map(|case| typed_ir::VariantCase {
2579                    name: case.name.clone(),
2580                    payloads: case
2581                        .payloads
2582                        .iter()
2583                        .map(|payload| rename_type(payload, renames))
2584                        .collect(),
2585                })
2586                .collect(),
2587            tail: variant
2588                .tail
2589                .as_ref()
2590                .map(|tail| Box::new(rename_type(tail, renames))),
2591        }),
2592        typed_ir::Type::Recursive { var, body } => typed_ir::Type::Recursive {
2593            var: renames.get(var).cloned().unwrap_or_else(|| var.clone()),
2594            body: Box::new(rename_type(body, renames)),
2595        },
2596        typed_ir::Type::Unknown => typed_ir::Type::Unknown,
2597        typed_ir::Type::Never => typed_ir::Type::Never,
2598        typed_ir::Type::Any => typed_ir::Type::Any,
2599    }
2600}
2601
2602fn rename_type_arg(
2603    arg: &typed_ir::TypeArg,
2604    renames: &BTreeMap<typed_ir::TypeVar, typed_ir::TypeVar>,
2605) -> typed_ir::TypeArg {
2606    match arg {
2607        typed_ir::TypeArg::Type(ty) => typed_ir::TypeArg::Type(rename_type(ty, renames)),
2608        typed_ir::TypeArg::Bounds(bounds) => typed_ir::TypeArg::Bounds(typed_ir::TypeBounds {
2609            lower: bounds
2610                .lower
2611                .as_ref()
2612                .map(|ty| Box::new(rename_type(ty, renames))),
2613            upper: bounds
2614                .upper
2615                .as_ref()
2616                .map(|ty| Box::new(rename_type(ty, renames))),
2617        }),
2618    }
2619}
2620
2621fn rename_record_spread(
2622    spread: &typed_ir::RecordSpread,
2623    renames: &BTreeMap<typed_ir::TypeVar, typed_ir::TypeVar>,
2624) -> typed_ir::RecordSpread {
2625    match spread {
2626        typed_ir::RecordSpread::Head(ty) => {
2627            typed_ir::RecordSpread::Head(Box::new(rename_type(ty, renames)))
2628        }
2629        typed_ir::RecordSpread::Tail(ty) => {
2630            typed_ir::RecordSpread::Tail(Box::new(rename_type(ty, renames)))
2631        }
2632    }
2633}
2634
2635fn materialize_type(
2636    ty: typed_ir::Type,
2637    substitutions: &[typed_ir::TypeSubstitution],
2638) -> typed_ir::Type {
2639    materialize_type_inner(ty, substitutions, &mut Vec::new())
2640}
2641
2642fn materialize_type_inner(
2643    ty: typed_ir::Type,
2644    substitutions: &[typed_ir::TypeSubstitution],
2645    seen: &mut Vec<typed_ir::TypeVar>,
2646) -> typed_ir::Type {
2647    match ty {
2648        typed_ir::Type::Var(var) => {
2649            if seen.contains(&var) {
2650                return typed_ir::Type::Var(var);
2651            }
2652            let Some(substitution) = substitutions
2653                .iter()
2654                .find(|substitution| substitution.var == var)
2655            else {
2656                return typed_ir::Type::Var(var);
2657            };
2658            if matches!(&substitution.ty, typed_ir::Type::Var(next) if next == &var) {
2659                return typed_ir::Type::Var(var);
2660            }
2661            seen.push(var);
2662            let materialized = materialize_type_inner(substitution.ty.clone(), substitutions, seen);
2663            seen.pop();
2664            materialized
2665        }
2666        typed_ir::Type::Fun {
2667            param,
2668            param_effect,
2669            ret_effect,
2670            ret,
2671        } => typed_ir::Type::Fun {
2672            param: Box::new(materialize_type_inner(*param, substitutions, seen)),
2673            param_effect: Box::new(materialize_type_inner(*param_effect, substitutions, seen)),
2674            ret_effect: Box::new(materialize_type_inner(*ret_effect, substitutions, seen)),
2675            ret: Box::new(materialize_type_inner(*ret, substitutions, seen)),
2676        },
2677        typed_ir::Type::Tuple(items) => typed_ir::Type::Tuple(
2678            items
2679                .into_iter()
2680                .map(|item| materialize_type_inner(item, substitutions, seen))
2681                .collect(),
2682        ),
2683        typed_ir::Type::Named { path, args } => typed_ir::Type::Named {
2684            path,
2685            args: args
2686                .into_iter()
2687                .map(|arg| materialize_type_arg(arg, substitutions, seen))
2688                .collect(),
2689        },
2690        typed_ir::Type::Row { items, tail } => typed_ir::Type::Row {
2691            items: items
2692                .into_iter()
2693                .map(|item| materialize_type_inner(item, substitutions, seen))
2694                .collect(),
2695            tail: Box::new(materialize_type_inner(*tail, substitutions, seen)),
2696        },
2697        typed_ir::Type::Union(items) => typed_ir::Type::Union(
2698            items
2699                .into_iter()
2700                .map(|item| materialize_type_inner(item, substitutions, seen))
2701                .collect(),
2702        ),
2703        typed_ir::Type::Inter(items) => typed_ir::Type::Inter(
2704            items
2705                .into_iter()
2706                .map(|item| materialize_type_inner(item, substitutions, seen))
2707                .collect(),
2708        ),
2709        typed_ir::Type::Record(record) => typed_ir::Type::Record(typed_ir::RecordType {
2710            fields: record
2711                .fields
2712                .into_iter()
2713                .map(|field| typed_ir::RecordField {
2714                    name: field.name,
2715                    value: materialize_type_inner(field.value, substitutions, seen),
2716                    optional: field.optional,
2717                })
2718                .collect(),
2719            spread: record
2720                .spread
2721                .map(|spread| materialize_record_spread(spread, substitutions, seen)),
2722        }),
2723        typed_ir::Type::Variant(variant) => typed_ir::Type::Variant(typed_ir::VariantType {
2724            cases: variant
2725                .cases
2726                .into_iter()
2727                .map(|case| typed_ir::VariantCase {
2728                    name: case.name,
2729                    payloads: case
2730                        .payloads
2731                        .into_iter()
2732                        .map(|payload| materialize_type_inner(payload, substitutions, seen))
2733                        .collect(),
2734                })
2735                .collect(),
2736            tail: variant
2737                .tail
2738                .map(|tail| Box::new(materialize_type_inner(*tail, substitutions, seen))),
2739        }),
2740        typed_ir::Type::Recursive { var, body } => typed_ir::Type::Recursive {
2741            var,
2742            body: Box::new(materialize_type_inner(*body, substitutions, seen)),
2743        },
2744        ty => ty,
2745    }
2746}
2747
2748fn materialize_record_spread(
2749    spread: typed_ir::RecordSpread,
2750    substitutions: &[typed_ir::TypeSubstitution],
2751    seen: &mut Vec<typed_ir::TypeVar>,
2752) -> typed_ir::RecordSpread {
2753    match spread {
2754        typed_ir::RecordSpread::Head(ty) => {
2755            typed_ir::RecordSpread::Head(Box::new(materialize_type_inner(*ty, substitutions, seen)))
2756        }
2757        typed_ir::RecordSpread::Tail(ty) => {
2758            typed_ir::RecordSpread::Tail(Box::new(materialize_type_inner(*ty, substitutions, seen)))
2759        }
2760    }
2761}
2762
2763fn materialize_type_arg(
2764    arg: typed_ir::TypeArg,
2765    substitutions: &[typed_ir::TypeSubstitution],
2766    seen: &mut Vec<typed_ir::TypeVar>,
2767) -> typed_ir::TypeArg {
2768    match arg {
2769        typed_ir::TypeArg::Type(ty) => {
2770            typed_ir::TypeArg::Type(materialize_type_inner(ty, substitutions, seen))
2771        }
2772        typed_ir::TypeArg::Bounds(bounds) => typed_ir::TypeArg::Bounds(typed_ir::TypeBounds {
2773            lower: bounds
2774                .lower
2775                .map(|ty| Box::new(materialize_type_inner(*ty, substitutions, seen))),
2776            upper: bounds
2777                .upper
2778                .map(|ty| Box::new(materialize_type_inner(*ty, substitutions, seen))),
2779        }),
2780    }
2781}
2782
2783#[cfg(test)]
2784mod tests {
2785    use super::*;
2786    use yulang_runtime_ir::{Expr, ExprKind};
2787
2788    #[test]
2789    fn principal_type_is_freshened_into_graph() {
2790        let mut graph = TypeGraph::default();
2791        let instance = graph.instantiate_principal(&id_binding());
2792
2793        assert_eq!(instance.original_binding, path(&["id"]));
2794        assert!(matches!(
2795            instance.principal_type,
2796            typed_ir::Type::Fun { .. }
2797        ));
2798        assert_eq!(
2799            instance.type_params,
2800            vec![PrincipalTypeParam {
2801                original: typed_ir::TypeVar("a".into()),
2802                fresh: typed_ir::TypeVar("a#0".into()),
2803            }]
2804        );
2805        assert!(graph.slot(&typed_ir::TypeVar("a#0".into())).is_some());
2806    }
2807
2808    #[test]
2809    fn graph_solves_fresh_principal_var_from_lower_bound() {
2810        let mut graph = TypeGraph::default();
2811        let instance = graph.instantiate_principal(&id_binding());
2812        let typed_ir::Type::Fun { param, ret, .. } = &instance.principal_type else {
2813            panic!("expected function principal");
2814        };
2815
2816        graph
2817            .collect_runtime_lower(param, &RuntimeType::Value(int_type()))
2818            .unwrap();
2819        let solution = graph.solve();
2820
2821        assert_eq!(solution.materialize_core(ret.as_ref().clone()), int_type());
2822        assert_eq!(
2823            instance.original_substitutions(&solution),
2824            vec![typed_ir::TypeSubstitution {
2825                var: typed_ir::TypeVar("a".into()),
2826                ty: int_type(),
2827            }]
2828        );
2829        assert!(solution.is_complete());
2830    }
2831
2832    #[test]
2833    fn subtype_var_upper_chain_propagates_concrete_upper_bound() {
2834        let mut graph = TypeGraph::default();
2835        let value = typed_ir::TypeVar("value".into());
2836        let result = typed_ir::TypeVar("result".into());
2837
2838        graph
2839            .constrain_subtype(
2840                typed_ir::Type::Var(value.clone()),
2841                typed_ir::Type::Var(result.clone()),
2842            )
2843            .unwrap();
2844        graph
2845            .constrain_subtype(typed_ir::Type::Var(result), int_type())
2846            .unwrap();
2847
2848        let solution = graph.solve();
2849
2850        assert_eq!(solution.solution_for(&value), Some(&int_type()));
2851    }
2852
2853    #[test]
2854    fn subtype_function_result_upper_closes_effect_payload_var() {
2855        let mut graph = TypeGraph::default();
2856        let payload = typed_ir::TypeVar("payload".into());
2857        let result = typed_ir::TypeVar("result".into());
2858        let principal = fun_type_with_effects(
2859            typed_ir::Type::Never,
2860            effect_row(vec![
2861                effect_type_arg("sub", typed_ir::Type::Var(payload.clone())),
2862                effect_type("undet"),
2863            ]),
2864            effect_type("undet"),
2865            typed_ir::Type::Var(payload.clone()),
2866        );
2867        let expected = fun_type_with_effects(
2868            typed_ir::Type::Never,
2869            closed_row(&["sub", "undet"]),
2870            effect_type("undet"),
2871            typed_ir::Type::Var(result.clone()),
2872        );
2873
2874        graph.constrain_subtype(principal, expected).unwrap();
2875        graph
2876            .constrain_subtype(typed_ir::Type::Var(result.clone()), int_type())
2877            .unwrap();
2878
2879        let solution = graph.solve();
2880
2881        assert_eq!(solution.solution_for(&payload), Some(&int_type()));
2882    }
2883
2884    #[test]
2885    fn materialize_core_chases_substitution_chains_without_self_loop() {
2886        let source = typed_ir::TypeVar("source".into());
2887        let intermediate = typed_ir::TypeVar("intermediate".into());
2888        let self_ref = typed_ir::TypeVar("self".into());
2889        let substitutions = vec![
2890            typed_ir::TypeSubstitution {
2891                var: source.clone(),
2892                ty: typed_ir::Type::Var(intermediate.clone()),
2893            },
2894            typed_ir::TypeSubstitution {
2895                var: intermediate,
2896                ty: int_type(),
2897            },
2898            typed_ir::TypeSubstitution {
2899                var: self_ref.clone(),
2900                ty: typed_ir::Type::Var(self_ref.clone()),
2901            },
2902        ];
2903
2904        assert_eq!(
2905            materialize_core_type(typed_ir::Type::Var(source), &substitutions),
2906            int_type()
2907        );
2908        assert_eq!(
2909            materialize_core_type(typed_ir::Type::Var(self_ref.clone()), &substitutions),
2910            typed_ir::Type::Var(self_ref)
2911        );
2912    }
2913
2914    #[test]
2915    fn any_lower_bound_conflicts_with_concrete_upper_bound() {
2916        let mut graph = TypeGraph::default();
2917        let var = typed_ir::TypeVar("a".into());
2918
2919        graph
2920            .constrain_subtype(typed_ir::Type::Any, typed_ir::Type::Var(var.clone()))
2921            .unwrap();
2922        let error = graph
2923            .constrain_subtype(typed_ir::Type::Var(var.clone()), int_type())
2924            .unwrap_err();
2925
2926        assert!(matches!(
2927            error,
2928            MonomorphizeDiagnostic::ConflictingBounds {
2929                var: error_var,
2930                previous: typed_ir::Type::Any,
2931                next,
2932            } if error_var == var && next == int_type()
2933        ));
2934    }
2935
2936    #[test]
2937    fn function_runtime_lower_bound_flips_parameter_polarity() {
2938        let mut graph = TypeGraph::default();
2939        let var = typed_ir::TypeVar("a".into());
2940        let template = fun_type(typed_ir::Type::Var(var.clone()), unit_type());
2941        let actual = fun_type(int_type(), unit_type());
2942
2943        graph
2944            .collect_runtime_lower(&template, &RuntimeType::Value(actual))
2945            .unwrap();
2946
2947        let bounds = graph.slot(&var).expect("parameter var should be tracked");
2948        assert_eq!(bounds.lower, None);
2949        assert_eq!(bounds.upper, Some(int_type()));
2950    }
2951
2952    #[test]
2953    fn function_runtime_lower_bound_does_not_turn_top_parameter_into_solution() {
2954        let mut graph = TypeGraph::default();
2955        let var = typed_ir::TypeVar("a".into());
2956        let template = fun_type(typed_ir::Type::Var(var.clone()), unit_type());
2957        let actual = fun_type(typed_ir::Type::Any, unit_type());
2958
2959        graph
2960            .collect_runtime_lower(&template, &RuntimeType::Value(actual))
2961            .unwrap();
2962
2963        assert_eq!(graph.slot(&var).and_then(TypeVarBounds::solution_ref), None);
2964    }
2965
2966    #[test]
2967    fn graph_joins_subtype_lower_bound_into_union_supertype() {
2968        let mut graph = TypeGraph::default();
2969        let var = typed_ir::TypeVar("effect".into());
2970        let last = closed_row(&["last"]);
2971        let sub = closed_row(&["sub"]);
2972        let either = typed_ir::Type::Union(vec![sub, last.clone()]);
2973
2974        graph
2975            .collect_runtime_bounds(
2976                &typed_ir::Type::Var(var.clone()),
2977                &RuntimeBounds {
2978                    lower: Some(RuntimeType::Value(last)),
2979                    upper: None,
2980                },
2981            )
2982            .unwrap();
2983        graph
2984            .collect_runtime_bounds(
2985                &typed_ir::Type::Var(var.clone()),
2986                &RuntimeBounds {
2987                    lower: Some(RuntimeType::Value(either.clone())),
2988                    upper: None,
2989                },
2990            )
2991            .unwrap();
2992
2993        assert_eq!(
2994            graph.slot(&var).and_then(TypeVarBounds::solution_ref),
2995            Some(&either)
2996        );
2997    }
2998
2999    #[test]
3000    fn graph_keeps_existing_union_supertype_lower_bound() {
3001        let mut graph = TypeGraph::default();
3002        let var = typed_ir::TypeVar("effect".into());
3003        let last = closed_row(&["last"]);
3004        let sub = closed_row(&["sub"]);
3005        let either = typed_ir::Type::Union(vec![sub, last.clone()]);
3006
3007        graph
3008            .collect_runtime_bounds(
3009                &typed_ir::Type::Var(var.clone()),
3010                &RuntimeBounds {
3011                    lower: Some(RuntimeType::Value(either.clone())),
3012                    upper: None,
3013                },
3014            )
3015            .unwrap();
3016        graph
3017            .collect_runtime_bounds(
3018                &typed_ir::Type::Var(var.clone()),
3019                &RuntimeBounds {
3020                    lower: Some(RuntimeType::Value(last)),
3021                    upper: None,
3022                },
3023            )
3024            .unwrap();
3025
3026        assert_eq!(
3027            graph.slot(&var).and_then(TypeVarBounds::solution_ref),
3028            Some(&either)
3029        );
3030    }
3031
3032    #[test]
3033    fn graph_treats_named_unit_and_empty_tuple_bounds_as_equivalent() {
3034        let mut graph = TypeGraph::default();
3035        let var = typed_ir::TypeVar("unitish".into());
3036
3037        graph
3038            .collect_runtime_bounds(
3039                &typed_ir::Type::Var(var.clone()),
3040                &RuntimeBounds {
3041                    lower: Some(RuntimeType::Value(unit_type())),
3042                    upper: None,
3043                },
3044            )
3045            .unwrap();
3046        graph
3047            .collect_runtime_bounds(
3048                &typed_ir::Type::Var(var.clone()),
3049                &RuntimeBounds {
3050                    lower: Some(RuntimeType::Value(typed_ir::Type::Tuple(Vec::new()))),
3051                    upper: None,
3052                },
3053            )
3054            .unwrap();
3055
3056        assert_eq!(
3057            graph.slot(&var).and_then(TypeVarBounds::solution_ref),
3058            Some(&unit_type())
3059        );
3060    }
3061
3062    #[test]
3063    fn graph_joins_lower_bounds_using_cast_order() {
3064        let small = effect_type("small");
3065        let middle = effect_type("middle");
3066        let large = effect_type("large");
3067        let cast_order = TypeCastOrder::from_edges(vec![
3068            (small.clone(), middle),
3069            (effect_type("middle"), large.clone()),
3070        ]);
3071        let mut graph = TypeGraph::with_cast_order(cast_order);
3072        let var = typed_ir::TypeVar("value".into());
3073
3074        graph
3075            .collect_runtime_bounds(
3076                &typed_ir::Type::Var(var.clone()),
3077                &RuntimeBounds {
3078                    lower: Some(RuntimeType::Value(small)),
3079                    upper: None,
3080                },
3081            )
3082            .unwrap();
3083        graph
3084            .collect_runtime_bounds(
3085                &typed_ir::Type::Var(var.clone()),
3086                &RuntimeBounds {
3087                    lower: Some(RuntimeType::Value(large.clone())),
3088                    upper: None,
3089                },
3090            )
3091            .unwrap();
3092
3093        assert_eq!(
3094            graph.slot(&var).and_then(TypeVarBounds::solution_ref),
3095            Some(&large)
3096        );
3097    }
3098
3099    #[test]
3100    fn lower_solution_wins_over_upper_solution() {
3101        let mut graph = TypeGraph::default();
3102        let var = typed_ir::TypeVar("a".into());
3103
3104        graph
3105            .collect_runtime_bounds(
3106                &typed_ir::Type::Var(var.clone()),
3107                &RuntimeBounds {
3108                    lower: Some(RuntimeType::Value(int_type())),
3109                    upper: Some(RuntimeType::Value(number_type())),
3110                },
3111            )
3112            .unwrap();
3113        let solution = graph.solve();
3114
3115        assert_eq!(
3116            solution.substitutions(),
3117            vec![typed_ir::TypeSubstitution {
3118                var,
3119                ty: int_type(),
3120            }]
3121        );
3122    }
3123
3124    #[test]
3125    fn top_and_bottom_are_extremes_not_holes() {
3126        let mut graph = TypeGraph::default();
3127        let var = typed_ir::TypeVar("a".into());
3128
3129        graph
3130            .collect_runtime_bounds(
3131                &typed_ir::Type::Var(var.clone()),
3132                &RuntimeBounds {
3133                    lower: Some(RuntimeType::Value(any_type())),
3134                    upper: Some(RuntimeType::Value(never_type())),
3135                },
3136            )
3137            .unwrap();
3138
3139        assert_eq!(
3140            graph.slot(&var).and_then(TypeVarBounds::solution_ref),
3141            Some(&any_type())
3142        );
3143    }
3144
3145    #[test]
3146    fn bottom_lower_and_top_upper_are_vacuous_bounds() {
3147        let mut graph = TypeGraph::default();
3148        let var = typed_ir::TypeVar("a".into());
3149
3150        graph
3151            .collect_runtime_bounds(
3152                &typed_ir::Type::Var(var.clone()),
3153                &RuntimeBounds {
3154                    lower: Some(RuntimeType::Value(never_type())),
3155                    upper: Some(RuntimeType::Value(any_type())),
3156                },
3157            )
3158            .unwrap();
3159
3160        assert_eq!(graph.slot(&var).and_then(TypeVarBounds::solution_ref), None);
3161
3162        graph
3163            .collect_runtime_bounds(
3164                &typed_ir::Type::Var(var.clone()),
3165                &RuntimeBounds {
3166                    lower: Some(RuntimeType::Value(int_type())),
3167                    upper: Some(RuntimeType::Value(any_type())),
3168                },
3169            )
3170            .unwrap();
3171
3172        assert_eq!(
3173            graph.slot(&var).and_then(TypeVarBounds::solution_ref),
3174            Some(&int_type())
3175        );
3176    }
3177
3178    #[test]
3179    fn symbolic_bounds_do_not_conflict_before_concrete_bounds_arrive() {
3180        let mut graph = TypeGraph::default();
3181        let var = typed_ir::TypeVar("a".into());
3182        let first = typed_ir::Type::Var(typed_ir::TypeVar("b".into()));
3183        let second = typed_ir::Type::Var(typed_ir::TypeVar("c".into()));
3184
3185        graph
3186            .collect_runtime_bounds(
3187                &typed_ir::Type::Var(var.clone()),
3188                &RuntimeBounds {
3189                    lower: Some(RuntimeType::Value(first.clone())),
3190                    upper: None,
3191                },
3192            )
3193            .unwrap();
3194        graph
3195            .collect_runtime_bounds(
3196                &typed_ir::Type::Var(var.clone()),
3197                &RuntimeBounds {
3198                    lower: Some(RuntimeType::Value(second)),
3199                    upper: None,
3200                },
3201            )
3202            .unwrap();
3203
3204        assert_eq!(
3205            graph.slot(&var).and_then(TypeVarBounds::solution_ref),
3206            Some(&first)
3207        );
3208    }
3209
3210    #[test]
3211    fn indirect_var_lower_bound_cycle_is_not_chased_forever() {
3212        let mut graph = TypeGraph::default();
3213        let a = typed_ir::TypeVar("a".into());
3214        let b = typed_ir::TypeVar("b".into());
3215
3216        graph
3217            .collect_runtime_lower(
3218                &typed_ir::Type::Var(a.clone()),
3219                &RuntimeType::Value(typed_ir::Type::Var(b.clone())),
3220            )
3221            .unwrap();
3222        graph
3223            .collect_runtime_lower(
3224                &typed_ir::Type::Var(b.clone()),
3225                &RuntimeType::Value(typed_ir::Type::Var(a.clone())),
3226            )
3227            .unwrap();
3228        graph
3229            .constrain_subtype(typed_ir::Type::Var(a.clone()), int_type())
3230            .unwrap();
3231
3232        assert_eq!(
3233            graph.slot(&a).and_then(TypeVarBounds::solution_ref),
3234            Some(&typed_ir::Type::Var(b))
3235        );
3236    }
3237
3238    #[test]
3239    fn graph_solution_keeps_lower_and_upper_after_solving() {
3240        let mut graph = TypeGraph::default();
3241        let var = typed_ir::TypeVar("a".into());
3242
3243        graph
3244            .collect_runtime_bounds(
3245                &typed_ir::Type::Var(var.clone()),
3246                &RuntimeBounds {
3247                    lower: Some(RuntimeType::Value(int_type())),
3248                    upper: Some(RuntimeType::Value(number_type())),
3249                },
3250            )
3251            .unwrap();
3252        let solution = graph.solve();
3253
3254        assert_eq!(
3255            solution.type_vars,
3256            vec![ResolvedTypeVar {
3257                var,
3258                bounds: TypeVarBounds {
3259                    lower: Some(int_type()),
3260                    upper: Some(number_type()),
3261                },
3262                solution: Some(int_type()),
3263            }]
3264        );
3265        assert!(solution.is_complete());
3266    }
3267
3268    #[test]
3269    fn subtype_constraints_solve_curried_first_application() {
3270        let mut graph = TypeGraph::default();
3271        let a_var = typed_ir::TypeVar("a".into());
3272        let b_var = typed_ir::TypeVar("b".into());
3273        let a = typed_ir::Type::Var(a_var.clone());
3274        let b = typed_ir::Type::Var(b_var.clone());
3275        graph.slots.entry(a_var.clone()).or_default();
3276        graph.slots.entry(b_var.clone()).or_default();
3277        let r0 = graph.fresh_hole("apply");
3278        let r1 = graph.fresh_hole("apply");
3279
3280        graph
3281            .constrain_subtype(
3282                fun_type(a.clone(), fun_type(b.clone(), a.clone())),
3283                fun_type(int_type(), r0.clone()),
3284            )
3285            .unwrap();
3286        graph
3287            .constrain_subtype(r0, fun_type(bool_type(), r1.clone()))
3288            .unwrap();
3289        let solution = graph.solve();
3290
3291        assert_eq!(solution.solution_for(&a_var), Some(&int_type()));
3292        assert_eq!(solution.solution_for(&b_var), Some(&bool_type()));
3293        assert_eq!(solution.materialize_core(r1), int_type());
3294        assert!(solution.is_complete());
3295    }
3296
3297    #[test]
3298    fn subtype_constraints_propagate_after_later_bounds_arrive() {
3299        let mut graph = TypeGraph::default();
3300        let a_var = typed_ir::TypeVar("a".into());
3301        let b_var = typed_ir::TypeVar("b".into());
3302        graph.slots.entry(a_var.clone()).or_default();
3303        graph.slots.entry(b_var.clone()).or_default();
3304
3305        graph
3306            .constrain_subtype(
3307                typed_ir::Type::Var(a_var.clone()),
3308                typed_ir::Type::Var(b_var.clone()),
3309            )
3310            .unwrap();
3311        graph
3312            .collect_runtime_lower(
3313                &typed_ir::Type::Var(a_var.clone()),
3314                &RuntimeType::Value(int_type()),
3315            )
3316            .unwrap();
3317
3318        let solution = graph.solve();
3319
3320        assert_eq!(solution.solution_for(&a_var), Some(&int_type()));
3321        assert_eq!(solution.solution_for(&b_var), Some(&int_type()));
3322    }
3323
3324    #[test]
3325    fn union_lower_constraint_propagates_upper_bound_to_items() {
3326        let mut graph = TypeGraph::default();
3327        let var = typed_ir::TypeVar("a".into());
3328        graph.slots.entry(var.clone()).or_default();
3329
3330        graph
3331            .constrain_subtype(
3332                typed_ir::Type::Union(vec![typed_ir::Type::Var(var.clone()), int_type()]),
3333                int_type(),
3334            )
3335            .unwrap();
3336        let solution = graph.solve();
3337
3338        assert_eq!(solution.solution_for(&var), Some(&int_type()));
3339        assert!(solution.is_complete());
3340    }
3341
3342    #[test]
3343    fn variant_subtype_solves_present_payloads_and_bottoms_absent_upper_payloads() {
3344        let mut graph = TypeGraph::default();
3345        let ok_var = typed_ir::TypeVar("ok".into());
3346        let err_var = typed_ir::TypeVar("err".into());
3347        graph.slots.entry(ok_var.clone()).or_default();
3348        graph.slots.entry(err_var.clone()).or_default();
3349
3350        graph
3351            .constrain_subtype(
3352                variant_type(&[("ok", vec![int_type()])]),
3353                variant_type(&[
3354                    ("ok", vec![typed_ir::Type::Var(ok_var.clone())]),
3355                    ("err", vec![typed_ir::Type::Var(err_var.clone())]),
3356                    ("pending", Vec::new()),
3357                ]),
3358            )
3359            .unwrap();
3360        let solution = graph.solve();
3361
3362        assert_eq!(solution.solution_for(&ok_var), Some(&int_type()));
3363        assert_eq!(
3364            solution.solution_for(&err_var),
3365            Some(&typed_ir::Type::Never)
3366        );
3367        assert!(solution.is_complete());
3368    }
3369
3370    #[test]
3371    fn record_subtype_solves_field_type_variables() {
3372        let mut graph = TypeGraph::default();
3373        let port_var = typed_ir::TypeVar("port".into());
3374        graph.slots.entry(port_var.clone()).or_default();
3375
3376        graph
3377            .constrain_subtype(
3378                record_type(&[("port", typed_ir::Type::Var(port_var.clone()))]),
3379                record_type(&[("port", int_type())]),
3380            )
3381            .unwrap();
3382        let solution = graph.solve();
3383
3384        assert_eq!(solution.solution_for(&port_var), Some(&int_type()));
3385        assert!(solution.is_complete());
3386    }
3387
3388    #[test]
3389    fn materialized_union_drops_bottom_and_singleton() {
3390        let ty = materialize_core_type(
3391            typed_ir::Type::Union(vec![typed_ir::Type::Never, int_type()]),
3392            &[],
3393        );
3394
3395        assert_eq!(ty, int_type());
3396    }
3397
3398    #[test]
3399    fn normalized_union_absorbs_intersection_subtype() {
3400        let acc = typed_ir::Type::Var(typed_ir::TypeVar("acc".into()));
3401        let branch = typed_ir::Type::Var(typed_ir::TypeVar("branch".into()));
3402        let ty = materialize_core_type(
3403            typed_ir::Type::Union(vec![
3404                acc.clone(),
3405                typed_ir::Type::Inter(vec![acc.clone(), branch]),
3406            ]),
3407            &[],
3408        );
3409
3410        assert_eq!(ty, acc);
3411    }
3412
3413    #[test]
3414    fn normalized_intersection_absorbs_union_supertype() {
3415        let acc = typed_ir::Type::Var(typed_ir::TypeVar("acc".into()));
3416        let branch = typed_ir::Type::Var(typed_ir::TypeVar("branch".into()));
3417        let ty = materialize_core_type(
3418            typed_ir::Type::Inter(vec![
3419                typed_ir::Type::Union(vec![unit_type(), acc.clone(), branch]),
3420                acc.clone(),
3421            ]),
3422            &[],
3423        );
3424
3425        assert_eq!(ty, acc);
3426    }
3427
3428    #[test]
3429    fn runtime_function_thunks_feed_function_effect_slots() {
3430        let mut graph = TypeGraph::default();
3431        let a_var = typed_ir::TypeVar("a".into());
3432        let b_var = typed_ir::TypeVar("b".into());
3433        let param_effect_var = typed_ir::TypeVar("pe".into());
3434        let ret_effect_var = typed_ir::TypeVar("re".into());
3435        let template = fun_type_with_effects(
3436            typed_ir::Type::Var(a_var.clone()),
3437            typed_ir::Type::Var(param_effect_var.clone()),
3438            typed_ir::Type::Var(ret_effect_var.clone()),
3439            typed_ir::Type::Var(b_var.clone()),
3440        );
3441
3442        graph
3443            .collect_runtime_bounds(
3444                &template,
3445                &RuntimeBounds::exact(RuntimeType::Fun {
3446                    param: Box::new(RuntimeType::Thunk {
3447                        effect: effect_type("arg"),
3448                        value: Box::new(RuntimeType::Value(int_type())),
3449                    }),
3450                    ret: Box::new(RuntimeType::Thunk {
3451                        effect: effect_type("ret"),
3452                        value: Box::new(RuntimeType::Value(bool_type())),
3453                    }),
3454                }),
3455            )
3456            .unwrap();
3457        let solution = graph.solve();
3458
3459        assert_eq!(solution.solution_for(&a_var), Some(&int_type()));
3460        assert_eq!(solution.solution_for(&b_var), Some(&bool_type()));
3461        assert_eq!(
3462            solution.solution_for(&param_effect_var),
3463            Some(&effect_type("arg"))
3464        );
3465        assert_eq!(
3466            solution.solution_for(&ret_effect_var),
3467            Some(&effect_type("ret"))
3468        );
3469        assert!(solution.is_complete());
3470    }
3471
3472    #[test]
3473    fn singleton_closed_effect_row_bound_matches_its_atom() {
3474        let mut graph = TypeGraph::default();
3475        let var = typed_ir::TypeVar("e".into());
3476        let atom = effect_type("io");
3477        let row = typed_ir::Type::Row {
3478            items: vec![atom.clone()],
3479            tail: Box::new(typed_ir::Type::Never),
3480        };
3481
3482        graph
3483            .collect_runtime_bounds(
3484                &typed_ir::Type::Var(var.clone()),
3485                &RuntimeBounds {
3486                    lower: None,
3487                    upper: Some(RuntimeType::Value(row.clone())),
3488                },
3489            )
3490            .unwrap();
3491        graph
3492            .collect_runtime_bounds(
3493                &typed_ir::Type::Var(var.clone()),
3494                &RuntimeBounds {
3495                    lower: None,
3496                    upper: Some(RuntimeType::Value(atom)),
3497                },
3498            )
3499            .unwrap();
3500
3501        assert_eq!(
3502            graph.slot(&var).and_then(TypeVarBounds::solution_ref),
3503            Some(&row)
3504        );
3505    }
3506
3507    #[test]
3508    fn empty_effect_row_bound_matches_its_tail() {
3509        let mut graph = TypeGraph::default();
3510        let var = typed_ir::TypeVar("e".into());
3511        let open_top_row = typed_ir::Type::Row {
3512            items: Vec::new(),
3513            tail: Box::new(typed_ir::Type::Any),
3514        };
3515
3516        graph
3517            .collect_runtime_bounds(
3518                &typed_ir::Type::Var(var.clone()),
3519                &RuntimeBounds {
3520                    lower: Some(RuntimeType::Value(typed_ir::Type::Any)),
3521                    upper: None,
3522                },
3523            )
3524            .unwrap();
3525        graph
3526            .collect_runtime_bounds(
3527                &typed_ir::Type::Var(var.clone()),
3528                &RuntimeBounds {
3529                    lower: Some(RuntimeType::Value(open_top_row)),
3530                    upper: None,
3531                },
3532            )
3533            .unwrap();
3534
3535        assert_eq!(
3536            graph.slot(&var).and_then(TypeVarBounds::solution_ref),
3537            Some(&typed_ir::Type::Any)
3538        );
3539    }
3540
3541    #[test]
3542    fn never_effect_row_item_is_empty() {
3543        let mut graph = TypeGraph::default();
3544        let var = typed_ir::TypeVar("e".into());
3545        let never_item_row = typed_ir::Type::Row {
3546            items: vec![typed_ir::Type::Never],
3547            tail: Box::new(typed_ir::Type::Never),
3548        };
3549
3550        graph
3551            .collect_runtime_bounds(
3552                &typed_ir::Type::Var(var.clone()),
3553                &RuntimeBounds {
3554                    lower: Some(RuntimeType::Value(typed_ir::Type::Never)),
3555                    upper: None,
3556                },
3557            )
3558            .unwrap();
3559        graph
3560            .collect_runtime_bounds(
3561                &typed_ir::Type::Var(var.clone()),
3562                &RuntimeBounds {
3563                    lower: Some(RuntimeType::Value(never_item_row)),
3564                    upper: None,
3565                },
3566            )
3567            .unwrap();
3568
3569        assert_eq!(graph.slot(&var).and_then(TypeVarBounds::solution_ref), None);
3570    }
3571
3572    #[test]
3573    fn intersection_bound_drops_top_and_duplicate_rows() {
3574        let mut graph = TypeGraph::default();
3575        let var = typed_ir::TypeVar("e".into());
3576        let row = typed_ir::Type::Row {
3577            items: vec![effect_type("ref_update")],
3578            tail: Box::new(typed_ir::Type::Never),
3579        };
3580        let noisy = typed_ir::Type::Inter(vec![typed_ir::Type::Any, row.clone(), row.clone()]);
3581
3582        graph
3583            .collect_runtime_bounds(
3584                &typed_ir::Type::Var(var.clone()),
3585                &RuntimeBounds {
3586                    lower: Some(RuntimeType::Value(row.clone())),
3587                    upper: None,
3588                },
3589            )
3590            .unwrap();
3591        graph
3592            .collect_runtime_bounds(
3593                &typed_ir::Type::Var(var.clone()),
3594                &RuntimeBounds {
3595                    lower: Some(RuntimeType::Value(noisy)),
3596                    upper: None,
3597                },
3598            )
3599            .unwrap();
3600
3601        assert_eq!(
3602            graph.slot(&var).and_then(TypeVarBounds::solution_ref),
3603            Some(&row)
3604        );
3605    }
3606
3607    #[test]
3608    fn closed_effect_row_intersection_keeps_common_items() {
3609        let mut graph = TypeGraph::default();
3610        let var = typed_ir::TypeVar("e".into());
3611        let parse = effect_type("parse");
3612        let pick = effect_type("pick");
3613        let parse_and_pick = typed_ir::Type::Row {
3614            items: vec![parse.clone(), pick],
3615            tail: Box::new(typed_ir::Type::Never),
3616        };
3617        let parse_only = typed_ir::Type::Row {
3618            items: vec![parse],
3619            tail: Box::new(typed_ir::Type::Never),
3620        };
3621        let intersection = typed_ir::Type::Inter(vec![parse_and_pick, parse_only.clone()]);
3622
3623        graph
3624            .collect_runtime_bounds(
3625                &typed_ir::Type::Var(var.clone()),
3626                &RuntimeBounds {
3627                    lower: None,
3628                    upper: Some(RuntimeType::Value(intersection)),
3629                },
3630            )
3631            .unwrap();
3632        graph
3633            .collect_runtime_bounds(
3634                &typed_ir::Type::Var(var.clone()),
3635                &RuntimeBounds {
3636                    lower: None,
3637                    upper: Some(RuntimeType::Value(parse_only.clone())),
3638                },
3639            )
3640            .unwrap();
3641
3642        assert_eq!(
3643            graph.slot(&var).and_then(|bounds| bounds.upper.as_ref()),
3644            Some(&parse_only)
3645        );
3646    }
3647
3648    #[test]
3649    fn closed_effect_row_subtypes_open_effect_row_intersection() {
3650        let read = effect_type("read");
3651        let write = effect_type("write");
3652        let closed = typed_ir::Type::Row {
3653            items: vec![read.clone(), write.clone()],
3654            tail: Box::new(typed_ir::Type::Never),
3655        };
3656        let read_open = typed_ir::Type::Row {
3657            items: vec![read],
3658            tail: Box::new(typed_ir::Type::Any),
3659        };
3660        let write_open = typed_ir::Type::Row {
3661            items: vec![write],
3662            tail: Box::new(typed_ir::Type::Any),
3663        };
3664        let open_intersection = typed_ir::Type::Inter(vec![read_open, write_open]);
3665
3666        assert!(bound_subtype(&closed, &open_intersection));
3667    }
3668
3669    #[test]
3670    fn upper_bound_prefers_closed_row_below_open_row_intersection() {
3671        let mut graph = TypeGraph::default();
3672        let var = typed_ir::TypeVar("e".into());
3673        let state = effect_type("&s");
3674        let var_effect = effect_type("var");
3675        let closed = typed_ir::Type::Row {
3676            items: vec![state.clone(), var_effect.clone()],
3677            tail: Box::new(typed_ir::Type::Never),
3678        };
3679        let state_open = typed_ir::Type::Row {
3680            items: vec![state],
3681            tail: Box::new(typed_ir::Type::Any),
3682        };
3683        let var_open = typed_ir::Type::Row {
3684            items: vec![var_effect],
3685            tail: Box::new(typed_ir::Type::Any),
3686        };
3687        let recursive_open = typed_ir::Type::Recursive {
3688            var: typed_ir::TypeVar("tail".into()),
3689            body: Box::new(typed_ir::Type::Row {
3690                items: Vec::new(),
3691                tail: Box::new(typed_ir::Type::Any),
3692            }),
3693        };
3694        let open_intersection = typed_ir::Type::Inter(vec![state_open, var_open, recursive_open]);
3695
3696        graph
3697            .collect_runtime_bounds(
3698                &typed_ir::Type::Var(var.clone()),
3699                &RuntimeBounds {
3700                    lower: None,
3701                    upper: Some(RuntimeType::Value(open_intersection)),
3702                },
3703            )
3704            .unwrap();
3705        graph
3706            .collect_runtime_bounds(
3707                &typed_ir::Type::Var(var.clone()),
3708                &RuntimeBounds {
3709                    lower: None,
3710                    upper: Some(RuntimeType::Value(closed.clone())),
3711                },
3712            )
3713            .unwrap();
3714
3715        assert_eq!(
3716            graph.slot(&var).and_then(|bounds| bounds.upper.as_ref()),
3717            Some(&closed)
3718        );
3719    }
3720
3721    #[test]
3722    fn closed_effect_row_item_flattens_into_outer_row() {
3723        let mut graph = TypeGraph::default();
3724        let var = typed_ir::TypeVar("e".into());
3725        let nested = typed_ir::Type::Row {
3726            items: vec![typed_ir::Type::Row {
3727                items: vec![effect_type("junction")],
3728                tail: Box::new(typed_ir::Type::Never),
3729            }],
3730            tail: Box::new(typed_ir::Type::Never),
3731        };
3732        let expected = typed_ir::Type::Row {
3733            items: vec![effect_type("junction")],
3734            tail: Box::new(typed_ir::Type::Never),
3735        };
3736
3737        graph
3738            .collect_runtime_bounds(
3739                &typed_ir::Type::Var(var.clone()),
3740                &RuntimeBounds {
3741                    lower: Some(RuntimeType::Value(nested)),
3742                    upper: None,
3743                },
3744            )
3745            .unwrap();
3746
3747        assert_eq!(
3748            graph.slot(&var).and_then(TypeVarBounds::solution_ref),
3749            Some(&expected)
3750        );
3751    }
3752
3753    #[test]
3754    fn function_upper_bounds_merge_by_variance() {
3755        let mut graph = TypeGraph::default();
3756        let var = typed_ir::TypeVar("f".into());
3757        let precise = fun_type_with_effects(
3758            bool_type(),
3759            typed_ir::Type::Never,
3760            closed_row(&["parse"]),
3761            int_type(),
3762        );
3763        let broad = fun_type_with_effects(
3764            bool_type(),
3765            typed_ir::Type::Any,
3766            typed_ir::Type::Any,
3767            int_type(),
3768        );
3769
3770        graph
3771            .collect_runtime_bounds(
3772                &typed_ir::Type::Var(var.clone()),
3773                &RuntimeBounds {
3774                    lower: None,
3775                    upper: Some(RuntimeType::Value(broad)),
3776                },
3777            )
3778            .unwrap();
3779        graph
3780            .collect_runtime_bounds(
3781                &typed_ir::Type::Var(var.clone()),
3782                &RuntimeBounds {
3783                    lower: None,
3784                    upper: Some(RuntimeType::Value(precise.clone())),
3785                },
3786            )
3787            .unwrap();
3788
3789        assert_eq!(
3790            graph.slot(&var).and_then(|bounds| bounds.upper.as_ref()),
3791            Some(&precise)
3792        );
3793    }
3794
3795    #[test]
3796    fn graph_solution_reports_open_type_vars() {
3797        let mut graph = TypeGraph::default();
3798        graph.instantiate_principal(&id_binding());
3799        let solution = graph.solve();
3800
3801        assert!(!solution.is_complete());
3802    }
3803
3804    fn id_binding() -> Binding {
3805        Binding {
3806            name: path(&["id"]),
3807            type_params: vec![typed_ir::TypeVar("a".into())],
3808            scheme: typed_ir::Scheme {
3809                requirements: Vec::new(),
3810                body: typed_ir::Type::Fun {
3811                    param: Box::new(typed_ir::Type::Var(typed_ir::TypeVar("a".into()))),
3812                    param_effect: Box::new(typed_ir::Type::Never),
3813                    ret_effect: Box::new(typed_ir::Type::Never),
3814                    ret: Box::new(typed_ir::Type::Var(typed_ir::TypeVar("a".into()))),
3815                },
3816            },
3817            body: Expr::typed(ExprKind::Tuple(Vec::new()), RuntimeType::Unknown),
3818        }
3819    }
3820
3821    fn int_type() -> typed_ir::Type {
3822        typed_ir::Type::Named {
3823            path: path(&["Int"]),
3824            args: Vec::new(),
3825        }
3826    }
3827
3828    fn bool_type() -> typed_ir::Type {
3829        typed_ir::Type::Named {
3830            path: path(&["Bool"]),
3831            args: Vec::new(),
3832        }
3833    }
3834
3835    fn unit_type() -> typed_ir::Type {
3836        typed_ir::Type::Named {
3837            path: path(&["unit"]),
3838            args: Vec::new(),
3839        }
3840    }
3841
3842    fn any_type() -> typed_ir::Type {
3843        typed_ir::Type::Any
3844    }
3845
3846    fn number_type() -> typed_ir::Type {
3847        typed_ir::Type::Named {
3848            path: path(&["Number"]),
3849            args: Vec::new(),
3850        }
3851    }
3852
3853    fn never_type() -> typed_ir::Type {
3854        typed_ir::Type::Never
3855    }
3856
3857    fn variant_type(cases: &[(&str, Vec<typed_ir::Type>)]) -> typed_ir::Type {
3858        typed_ir::Type::Variant(typed_ir::VariantType {
3859            cases: cases
3860                .iter()
3861                .map(|(name, payloads)| typed_ir::VariantCase {
3862                    name: typed_ir::Name((*name).to_string()),
3863                    payloads: payloads.clone(),
3864                })
3865                .collect(),
3866            tail: None,
3867        })
3868    }
3869
3870    fn record_type(fields: &[(&str, typed_ir::Type)]) -> typed_ir::Type {
3871        typed_ir::Type::Record(typed_ir::RecordType {
3872            fields: fields
3873                .iter()
3874                .map(|(name, value)| typed_ir::RecordField {
3875                    name: typed_ir::Name((*name).to_string()),
3876                    value: value.clone(),
3877                    optional: false,
3878                })
3879                .collect(),
3880            spread: None,
3881        })
3882    }
3883
3884    fn fun_type(param: typed_ir::Type, ret: typed_ir::Type) -> typed_ir::Type {
3885        fun_type_with_effects(param, typed_ir::Type::Never, typed_ir::Type::Never, ret)
3886    }
3887
3888    fn fun_type_with_effects(
3889        param: typed_ir::Type,
3890        param_effect: typed_ir::Type,
3891        ret_effect: typed_ir::Type,
3892        ret: typed_ir::Type,
3893    ) -> typed_ir::Type {
3894        typed_ir::Type::Fun {
3895            param: Box::new(param),
3896            param_effect: Box::new(param_effect),
3897            ret_effect: Box::new(ret_effect),
3898            ret: Box::new(ret),
3899        }
3900    }
3901
3902    fn effect_type(name: &str) -> typed_ir::Type {
3903        typed_ir::Type::Named {
3904            path: path(&[name]),
3905            args: Vec::new(),
3906        }
3907    }
3908
3909    fn effect_type_arg(name: &str, arg: typed_ir::Type) -> typed_ir::Type {
3910        typed_ir::Type::Named {
3911            path: path(&[name]),
3912            args: vec![typed_ir::TypeArg::Type(arg)],
3913        }
3914    }
3915
3916    fn effect_row(items: Vec<typed_ir::Type>) -> typed_ir::Type {
3917        typed_ir::Type::Row {
3918            items,
3919            tail: Box::new(typed_ir::Type::Never),
3920        }
3921    }
3922
3923    fn closed_row(items: &[&str]) -> typed_ir::Type {
3924        effect_row(items.iter().map(|item| effect_type(item)).collect())
3925    }
3926
3927    fn path(segments: &[&str]) -> typed_ir::Path {
3928        typed_ir::Path::new(
3929            segments
3930                .iter()
3931                .map(|segment| typed_ir::Name((*segment).into()))
3932                .collect(),
3933        )
3934    }
3935}