tinymist_analysis/ty/
simplify.rs

1#![allow(unused)]
2
3use ecow::EcoVec;
4
5use crate::syntax::UnaryOp;
6use crate::{syntax::DeclExpr, ty::prelude::*};
7
8/// A compact type.
9#[derive(Default)]
10struct CompactTy {
11    equiv_vars: HashSet<DefId>,
12    primitives: HashSet<Ty>,
13    recursives: HashMap<DefId, CompactTy>,
14    signatures: Vec<Interned<SigTy>>,
15
16    is_final: bool,
17}
18
19#[allow(clippy::mutable_key_type)]
20fn collect_input_type_vars(ty: &Ty, vars: &mut FxHashSet<DeclExpr>, traversed: &mut FxHashSet<Ty>) {
21    if !traversed.insert(ty.clone()) {
22        return;
23    }
24
25    match ty {
26        Ty::Var(var) => {
27            vars.insert(var.def.clone());
28        }
29        Ty::Func(_) | Ty::With(_) => {}
30        Ty::Param(param) => collect_input_type_vars(&param.ty, vars, traversed),
31        Ty::Union(types) | Ty::Tuple(types) => {
32            for ty in types.iter() {
33                collect_input_type_vars(ty, vars, traversed);
34            }
35        }
36        Ty::Let(bounds) => {
37            for ty in bounds.lbs.iter().chain(&bounds.ubs) {
38                collect_input_type_vars(ty, vars, traversed);
39            }
40        }
41        Ty::Dict(record) => {
42            for ty in record.types.iter() {
43                collect_input_type_vars(ty, vars, traversed);
44            }
45        }
46        Ty::Array(elem) => collect_input_type_vars(elem, vars, traversed),
47        Ty::Args(sig) | Ty::Pattern(sig) => {
48            for input in sig.inputs() {
49                collect_input_type_vars(input, vars, traversed);
50            }
51        }
52        Ty::Select(select) => collect_input_type_vars(&select.ty, vars, traversed),
53        Ty::Unary(unary) => collect_input_type_vars(&unary.lhs, vars, traversed),
54        Ty::Binary(binary) => {
55            let [lhs, rhs] = binary.operands();
56            collect_input_type_vars(lhs, vars, traversed);
57            collect_input_type_vars(rhs, vars, traversed);
58        }
59        Ty::If(if_ty) => {
60            collect_input_type_vars(&if_ty.cond, vars, traversed);
61            collect_input_type_vars(&if_ty.then, vars, traversed);
62            collect_input_type_vars(&if_ty.else_, vars, traversed);
63        }
64        Ty::Any | Ty::Boolean(_) | Ty::Builtin(_) | Ty::Value(_) => {}
65    }
66}
67
68impl TypeInfo {
69    /// Simplifies (canonicalizes) the given type with the given type scheme.
70    pub fn simplify(&self, ty: Ty, principal: bool) -> Ty {
71        let mut cache = self.cano_cache.lock();
72        let cache = &mut *cache;
73        let mut signature_binders = FxHashSet::default();
74
75        cache.transform_cache.clear();
76        cache.cano_local_cache.clear();
77        cache.positives.clear();
78        cache.negatives.clear();
79
80        let mut worker = TypeSimplifier {
81            principal,
82            vars: &self.vars,
83            cano_cache: &mut cache.cano_cache,
84            transform_cache: &mut cache.transform_cache,
85            cano_local_cache: &mut cache.cano_local_cache,
86            analyze_cache: FxHashSet::default(),
87            input_var_cache: FxHashSet::default(),
88
89            positives: &mut cache.positives,
90            negatives: &mut cache.negatives,
91        };
92
93        worker.simplify(ty, principal, &mut signature_binders)
94    }
95}
96
97/// A simplifier to simplify a type.
98struct TypeSimplifier<'a, 'b> {
99    principal: bool,
100
101    vars: &'a FxHashMap<DeclExpr, TypeVarBounds>,
102
103    cano_cache: &'b mut FxHashMap<(Ty, bool), Ty>,
104    transform_cache: &'b mut FxHashMap<(Ty, bool), Ty>,
105    cano_local_cache: &'b mut FxHashMap<(DeclExpr, bool), Ty>,
106    analyze_cache: FxHashSet<(Ty, bool)>,
107    input_var_cache: FxHashSet<Ty>,
108    negatives: &'b mut FxHashSet<DeclExpr>,
109    positives: &'b mut FxHashSet<DeclExpr>,
110}
111
112impl TypeSimplifier<'_, '_> {
113    /// Simplifies the given type.
114    fn simplify(
115        &mut self,
116        ty: Ty,
117        principal: bool,
118        signature_binders: &mut FxHashSet<DeclExpr>,
119    ) -> Ty {
120        if let Some(cano) = self.cano_cache.get(&(ty.clone(), principal)) {
121            return cano.clone();
122        }
123
124        self.analyze(&ty, true, signature_binders);
125        let cano = self.transform(&ty, true, signature_binders);
126        self.cano_cache.insert((ty, principal), cano.clone());
127        cano
128    }
129
130    /// Analyzes the given type.
131    fn analyze(&mut self, ty: &Ty, pol: bool, signature_binders: &mut FxHashSet<DeclExpr>) {
132        if !self.analyze_cache.insert((ty.clone(), pol)) {
133            return;
134        }
135
136        match ty {
137            Ty::Var(var) => {
138                if self.principal && signature_binders.contains(&var.def) {
139                    return;
140                }
141                let Some(w) = self.vars.get(&var.def) else {
142                    return;
143                };
144
145                let inserted = if pol {
146                    self.positives.insert(var.def.clone())
147                } else {
148                    self.negatives.insert(var.def.clone())
149                };
150                if !inserted {
151                    return;
152                }
153
154                match &w.bounds {
155                    FlowVarKind::Strong(w) | FlowVarKind::Weak(w) => {
156                        let bounds = w.read();
157                        if pol {
158                            for lb in bounds.lbs.iter() {
159                                self.analyze(lb, pol, signature_binders);
160                            }
161                        } else {
162                            for ub in bounds.ubs.iter() {
163                                self.analyze(ub, pol, signature_binders);
164                            }
165                        }
166                    }
167                }
168            }
169            Ty::Func(func) => {
170                if self.principal {
171                    for input in func.inputs() {
172                        collect_input_type_vars(
173                            input,
174                            signature_binders,
175                            &mut self.input_var_cache,
176                        );
177                    }
178                }
179                for input_ty in func.inputs() {
180                    self.analyze(input_ty, !pol, signature_binders);
181                }
182                if let Some(ret_ty) = &func.body {
183                    self.analyze(ret_ty, pol, signature_binders);
184                }
185            }
186            Ty::Dict(record) => {
187                for member in record.types.iter() {
188                    self.analyze(member, pol, signature_binders);
189                }
190            }
191            Ty::Tuple(elems) => {
192                for elem in elems.iter() {
193                    self.analyze(elem, pol, signature_binders);
194                }
195            }
196            Ty::Array(arr) => {
197                self.analyze(arr, pol, signature_binders);
198            }
199            Ty::With(with) => {
200                self.analyze(&with.sig, pol, signature_binders);
201                for input in with.with.inputs() {
202                    self.analyze(input, pol, signature_binders);
203                }
204            }
205            Ty::Args(args) => {
206                for input in args.inputs() {
207                    self.analyze(input, pol, signature_binders);
208                }
209            }
210            Ty::Pattern(pat) => {
211                if self.principal {
212                    for input in pat.inputs() {
213                        collect_input_type_vars(
214                            input,
215                            signature_binders,
216                            &mut self.input_var_cache,
217                        );
218                    }
219                }
220                for input in pat.inputs() {
221                    self.analyze(input, pol, signature_binders);
222                }
223            }
224            Ty::Unary(unary) => self.analyze(&unary.lhs, pol, signature_binders),
225            Ty::Binary(binary) => {
226                let [lhs, rhs] = binary.operands();
227                self.analyze(lhs, pol, signature_binders);
228                self.analyze(rhs, pol, signature_binders);
229            }
230            Ty::If(if_expr) => {
231                self.analyze(&if_expr.cond, pol, signature_binders);
232                self.analyze(&if_expr.then, pol, signature_binders);
233                self.analyze(&if_expr.else_, pol, signature_binders);
234            }
235            Ty::Union(types) => {
236                for ty in types.iter() {
237                    self.analyze(ty, pol, signature_binders);
238                }
239            }
240            Ty::Select(select) => {
241                self.analyze(&select.ty, pol, signature_binders);
242            }
243            Ty::Let(bounds) => {
244                for lb in bounds.lbs.iter() {
245                    self.analyze(lb, !pol, signature_binders);
246                }
247                for ub in bounds.ubs.iter() {
248                    self.analyze(ub, pol, signature_binders);
249                }
250            }
251            Ty::Param(param) => {
252                self.analyze(&param.ty, pol, signature_binders);
253            }
254            Ty::Value(_v) => {}
255            Ty::Any => {}
256            Ty::Boolean(_) => {}
257            Ty::Builtin(_) => {}
258        }
259    }
260
261    /// Transforms the given type.
262    fn transform(&mut self, ty: &Ty, pol: bool, signature_binders: &FxHashSet<DeclExpr>) -> Ty {
263        let cache_key = (ty.clone(), pol);
264        if let Some(cano) = self.transform_cache.get(&cache_key) {
265            return cano.clone();
266        }
267
268        let cano = match ty {
269            Ty::Let(bounds) => self.transform_let(
270                bounds.lbs.iter(),
271                bounds.ubs.iter(),
272                None,
273                pol,
274                signature_binders,
275            ),
276            Ty::Var(var) => {
277                if self.principal && signature_binders.contains(&var.def) {
278                    return Ty::Var(var.clone());
279                }
280                let Some(bounds) = self.vars.get(&var.def) else {
281                    return Ty::Var(var.clone());
282                };
283                if let Some(cano) = self
284                    .cano_local_cache
285                    .get(&(var.def.clone(), self.principal))
286                {
287                    return cano.clone();
288                }
289                // todo: avoid cycle
290                self.cano_local_cache
291                    .insert((var.def.clone(), self.principal), Ty::Any);
292
293                let res = match &bounds.bounds {
294                    FlowVarKind::Strong(w) | FlowVarKind::Weak(w) => {
295                        let w = w.read();
296
297                        self.transform_let(
298                            w.lbs.iter(),
299                            w.ubs.iter(),
300                            Some(&var.def),
301                            pol,
302                            signature_binders,
303                        )
304                    }
305                };
306
307                self.cano_local_cache
308                    .insert((var.def.clone(), self.principal), res.clone());
309
310                res
311            }
312            Ty::Func(func) => Ty::Func(self.transform_sig(func, pol, signature_binders)),
313            Ty::Dict(record) => {
314                let mut mutated = record.as_ref().clone();
315                mutated.types = self.transform_seq(&mutated.types, pol, signature_binders);
316
317                Ty::Dict(mutated.into())
318            }
319            Ty::Tuple(tup) => self.transform_tuple(tup, pol, signature_binders),
320            Ty::Array(arr) => Ty::Array(self.transform(arr, pol, signature_binders).into()),
321            Ty::With(with) => {
322                let sig = self.transform(&with.sig, pol, signature_binders).into();
323                // Negate the pol to make correct covariance
324                let mutated = self.transform_sig(&with.with, !pol, signature_binders);
325
326                Ty::With(SigWithTy::new(sig, mutated))
327            }
328            // Negate the pol to make correct covariance
329            // todo: negate?
330            Ty::Args(args) => Ty::Args(self.transform_sig(args, !pol, signature_binders)),
331            Ty::Pattern(pat) => Ty::Pattern(self.transform_sig(pat, !pol, signature_binders)),
332            Ty::Unary(unary) => self.transform_unary(unary, pol, signature_binders),
333            Ty::Binary(binary) => {
334                let [lhs, rhs] = binary.operands();
335                let lhs = self.transform(lhs, pol, signature_binders);
336                let rhs = self.transform(rhs, pol, signature_binders);
337
338                Ty::Binary(TypeBinary::new(binary.op, lhs, rhs))
339            }
340            Ty::If(if_ty) => Ty::If(IfTy::new(
341                self.transform(&if_ty.cond, pol, signature_binders).into(),
342                self.transform(&if_ty.then, pol, signature_binders).into(),
343                self.transform(&if_ty.else_, pol, signature_binders).into(),
344            )),
345            Ty::Union(types) => {
346                let seq = types
347                    .iter()
348                    .map(|ty| self.transform(ty, pol, signature_binders));
349                let seq_no_any = seq.filter(|ty| !matches!(ty, Ty::Any));
350                let seq = seq_no_any.collect::<Vec<_>>();
351                Ty::from_types(seq.into_iter())
352            }
353            Ty::Param(param) => {
354                let mut param = param.as_ref().clone();
355                param.ty = self.transform(&param.ty, pol, signature_binders);
356
357                Ty::Param(param.into())
358            }
359            Ty::Select(sel) => {
360                let mut sel = sel.as_ref().clone();
361                sel.ty = self.transform(&sel.ty, pol, signature_binders).into();
362
363                Ty::Select(sel.into())
364            }
365
366            Ty::Value(ins_ty) => Ty::Value(ins_ty.clone()),
367            Ty::Any => Ty::Any,
368            Ty::Boolean(truthiness) => Ty::Boolean(*truthiness),
369            Ty::Builtin(ty) => Ty::Builtin(ty.clone()),
370        };
371
372        self.transform_cache.insert(cache_key, cano.clone());
373        cano
374    }
375
376    /// Transforms the given sequence of types.
377    fn transform_seq(
378        &mut self,
379        types: &[Ty],
380        pol: bool,
381        signature_binders: &FxHashSet<DeclExpr>,
382    ) -> Interned<Vec<Ty>> {
383        let seq = types
384            .iter()
385            .map(|ty| self.transform(ty, pol, signature_binders));
386        seq.collect::<Vec<_>>().into()
387    }
388
389    /// Transforms the given let type.
390    #[allow(clippy::mutable_key_type)]
391    fn transform_let<'a>(
392        &mut self,
393        lbs_iter: impl ExactSizeIterator<Item = &'a Ty>,
394        ubs_iter: impl ExactSizeIterator<Item = &'a Ty>,
395        decl: Option<&DeclExpr>,
396        pol: bool,
397        signature_binders: &FxHashSet<DeclExpr>,
398    ) -> Ty {
399        let mut lbs = HashSet::with_capacity(lbs_iter.len());
400        let mut ubs = HashSet::with_capacity(ubs_iter.len());
401
402        crate::log_debug_ct!("transform let [principal={}]", self.principal);
403
404        if !self.principal || ((pol) && !decl.is_some_and(|decl| self.negatives.contains(decl))) {
405            for lb in lbs_iter {
406                lbs.insert(self.transform(lb, pol, signature_binders));
407            }
408        }
409        if !self.principal || ((!pol) && !decl.is_some_and(|decl| self.positives.contains(decl))) {
410            for ub in ubs_iter {
411                ubs.insert(self.transform(ub, !pol, signature_binders));
412            }
413        }
414
415        if ubs.is_empty() {
416            if lbs.len() == 1 {
417                return lbs.into_iter().next().unwrap();
418            }
419            if lbs.is_empty() {
420                return Ty::Any;
421            }
422        } else if lbs.is_empty() && ubs.len() == 1 {
423            return ubs.into_iter().next().unwrap();
424        }
425
426        // todo: bad performance
427        let mut lbs: Vec<_> = lbs.into_iter().collect();
428        lbs.sort();
429        let mut ubs: Vec<_> = ubs.into_iter().collect();
430        ubs.sort();
431
432        Ty::Let(TypeBounds { lbs, ubs }.into())
433    }
434
435    fn transform_tuple(
436        &mut self,
437        tup: &[Ty],
438        pol: bool,
439        signature_binders: &FxHashSet<DeclExpr>,
440    ) -> Ty {
441        let mut types = Vec::with_capacity(tup.len());
442
443        for elem in tup.iter() {
444            let elem = self.transform(elem, pol, signature_binders);
445            if !Self::push_spread_tuple_elements(&mut types, &elem) {
446                types.push(elem);
447            }
448        }
449
450        Ty::Tuple(types.into())
451    }
452
453    fn push_spread_tuple_elements(types: &mut Vec<Ty>, ty: &Ty) -> bool {
454        let Ty::Unary(unary) = ty else {
455            return false;
456        };
457        if unary.op != UnaryOp::Spread {
458            return false;
459        }
460
461        match &unary.lhs {
462            Ty::Tuple(elems) => {
463                types.extend(elems.iter().cloned());
464                true
465            }
466            Ty::Args(args) => {
467                types.extend(args.positional_params().cloned());
468                if let Some(rest) = args.rest_param()
469                    && !Self::push_spread_tuple_elements(
470                        types,
471                        &Ty::Unary(TypeUnary::new(UnaryOp::Spread, rest.clone())),
472                    )
473                {
474                    types.push(Ty::Unary(TypeUnary::new(UnaryOp::Spread, rest.clone())));
475                }
476                true
477            }
478            _ => false,
479        }
480    }
481
482    fn transform_unary(
483        &mut self,
484        unary: &TypeUnary,
485        pol: bool,
486        signature_binders: &FxHashSet<DeclExpr>,
487    ) -> Ty {
488        let lhs = self.transform(&unary.lhs, pol, signature_binders);
489        if unary.op == UnaryOp::ElementOf
490            && let Some(elem) = Self::known_element_type(&lhs)
491        {
492            return elem;
493        }
494
495        Ty::Unary(TypeUnary::new(unary.op, lhs))
496    }
497
498    fn known_element_type(ty: &Ty) -> Option<Ty> {
499        match ty {
500            Ty::Array(elem) => Some(elem.as_ref().clone()),
501            Ty::Tuple(elems) => Self::known_tuple_element_type(elems),
502            Ty::Args(args) => Self::known_args_element_type(args),
503            Ty::Let(bounds) => Self::known_element_types(bounds.lbs.iter()),
504            Ty::Union(types) => Self::known_element_types(types.iter()),
505            _ => None,
506        }
507    }
508
509    fn known_element_types<'a>(types: impl Iterator<Item = &'a Ty>) -> Option<Ty> {
510        let types = types
511            .filter_map(Self::known_element_type)
512            .collect::<Vec<_>>();
513        (!types.is_empty()).then(|| Ty::from_types(types.into_iter()))
514    }
515
516    fn known_tuple_element_type(elems: &[Ty]) -> Option<Ty> {
517        let mut types = vec![];
518        for elem in elems {
519            if let Ty::Unary(unary) = elem
520                && unary.op == UnaryOp::Spread
521            {
522                if let Some(elem) = Self::known_element_type(&unary.lhs) {
523                    types.push(elem);
524                }
525                continue;
526            }
527
528            types.push(elem.clone());
529        }
530
531        (!types.is_empty()).then(|| Ty::from_types(types.into_iter()))
532    }
533
534    fn known_args_element_type(args: &ArgsTy) -> Option<Ty> {
535        let mut types = args.positional_params().cloned().collect::<Vec<_>>();
536        if let Some(rest) = args.rest_param()
537            && let Some(elem) = Self::known_element_type(rest)
538        {
539            types.push(elem);
540        }
541
542        (!types.is_empty()).then(|| Ty::from_types(types.into_iter()))
543    }
544
545    /// Transforms the given signature.
546    fn transform_sig(
547        &mut self,
548        sig: &SigTy,
549        pol: bool,
550        signature_binders: &FxHashSet<DeclExpr>,
551    ) -> Interned<SigTy> {
552        let mut sig = sig.clone();
553        sig.inputs = self.transform_seq(&sig.inputs, !pol, signature_binders);
554        if let Some(ret) = &sig.body {
555            sig.body = Some(self.transform(ret, pol, signature_binders));
556        }
557
558        // todo: we can reduce one clone by early compare on sig.types
559        sig.into()
560    }
561}
562
563#[cfg(test)]
564mod tests {
565    use super::*;
566    use crate::syntax::Decl;
567
568    /// See https://github.com/typst/typst/issues/6285
569    #[test]
570    fn test_simplify_sort() {
571        fn ch(it: &str) -> Ty {
572            Ty::Value(InsTy::new(Value::Str(it.into())))
573        }
574
575        fn val(it: Value) -> Ty {
576            Ty::Value(InsTy::new(it))
577        }
578
579        fn test_sort_ty(mut tys: Vec<Ty>) {
580            tys.sort();
581        }
582
583        let abcdef = vec![ch("a"), ch("b"), ch("c"), ch("d"), ch("e"), ch("f")];
584
585        let mut res = vec![];
586        res.extend(abcdef.clone());
587        res.extend(abcdef.clone());
588        res.extend(abcdef.clone());
589        res.extend(vec![ch("c"), val(Value::None), ch("a")]);
590
591        test_sort_ty(res);
592    }
593
594    #[test]
595    #[allow(clippy::mutable_key_type)]
596    fn test_analyze_memoizes_shared_type_dag() {
597        const DEPTH: usize = 16;
598
599        let mut shared = Ty::Any;
600        for _ in 0..DEPTH {
601            shared = Ty::Tuple(vec![shared.clone(), shared].into());
602        }
603
604        let info = TypeInfo::default();
605        let mut cano_cache = FxHashMap::default();
606        let mut transform_cache = FxHashMap::default();
607        let mut cano_local_cache = FxHashMap::default();
608        let mut positives = FxHashSet::default();
609        let mut negatives = FxHashSet::default();
610        let mut worker = TypeSimplifier {
611            principal: true,
612            vars: &info.vars,
613            cano_cache: &mut cano_cache,
614            transform_cache: &mut transform_cache,
615            cano_local_cache: &mut cano_local_cache,
616            analyze_cache: FxHashSet::default(),
617            input_var_cache: FxHashSet::default(),
618            positives: &mut positives,
619            negatives: &mut negatives,
620        };
621        let mut signature_binders = FxHashSet::default();
622
623        worker.analyze(&shared, true, &mut signature_binders);
624
625        assert_eq!(worker.analyze_cache.len(), DEPTH + 1);
626    }
627
628    fn var(name: &str) -> TypeVarBounds {
629        TypeVarBounds::new(
630            TypeVar {
631                name: name.into(),
632                def: Decl::lit(name).into(),
633            },
634            DynTypeBounds::default(),
635        )
636    }
637
638    fn recursive_fun(root: &Interned<TypeVar>, depth: usize) -> Ty {
639        let mut body = Ty::Var(root.clone());
640        for _ in 0..depth {
641            body = Ty::Func(SigTy::unary(Ty::Any, body));
642        }
643        body
644    }
645
646    #[test]
647    fn test_recursive_cycle_union_is_not_aligned_like_simple_sub() {
648        let mut info = TypeInfo::default();
649
650        let one = var("one");
651        let two = var("two");
652
653        let one_ty = one.as_type();
654        let two_ty = two.as_type();
655
656        info.vars.insert(one.var.def.clone(), one.clone());
657        info.vars.insert(two.var.def.clone(), two.clone());
658
659        info.vars
660            .get(&one.var.def)
661            .unwrap()
662            .bounds
663            .bounds()
664            .write()
665            .lbs
666            .insert_mut(recursive_fun(&one.var, 1));
667        info.vars
668            .get(&two.var.def)
669            .unwrap()
670            .bounds
671            .bounds()
672            .write()
673            .lbs
674            .insert_mut(recursive_fun(&two.var, 2));
675
676        let merged = Ty::from_types([one_ty, two_ty].into_iter());
677        let simplified = info.simplify(merged, true);
678        assert_eq!(
679            format!("{simplified:?}"),
680            "((Any) => Any | (Any) => (Any) => Any)"
681        );
682    }
683
684    #[test]
685    fn test_simplify_populates_top_level_cache() {
686        let mut info = TypeInfo::default();
687        let one = var("one");
688        let one_ty = one.as_type();
689        info.vars.insert(one.var.def.clone(), one.clone());
690        info.vars
691            .get(&one.var.def)
692            .unwrap()
693            .bounds
694            .bounds()
695            .write()
696            .lbs
697            .insert_mut(recursive_fun(&one.var, 1));
698
699        let _ = info.simplify(one_ty.clone(), true);
700        let first_cache_len = info.cano_cache.lock().cano_cache.len();
701        let _ = info.simplify(one_ty, true);
702        let second_cache_len = info.cano_cache.lock().cano_cache.len();
703        assert!(
704            first_cache_len > 0,
705            "simplify should memoize the top-level result"
706        );
707        assert_eq!(first_cache_len, second_cache_len);
708    }
709
710    #[test]
711    fn test_signature_inputs_are_principal_binders() {
712        let binder = TypeVar::new("body".into(), Decl::lit("body").into());
713        let binder_ty = Ty::Var(binder.clone());
714        let content = Ty::Builtin(BuiltinTy::Content(None));
715        let sig = SigTy::unary(binder_ty.clone(), binder_ty);
716
717        let sig_ty = Ty::Func(sig);
718        let mut dynamic_bounds = DynTypeBounds::default();
719        dynamic_bounds.ubs.insert_mut(content);
720        let mut info = TypeInfo::default();
721        info.vars.insert(
722            binder.def.clone(),
723            TypeVarBounds::new(binder.as_ref().clone(), dynamic_bounds),
724        );
725        assert_eq!(
726            format!("{:?}", info.simplify(sig_ty.clone(), false)),
727            "(Content) => Content"
728        );
729        assert_eq!(
730            format!("{:?}", info.simplify(sig_ty, true)),
731            "(@body) => @body"
732        );
733        assert_eq!(info.vars.len(), 1);
734    }
735
736    #[test]
737    fn test_principal_simplify_preserves_unused_signature_binder() {
738        let binder = TypeVar::new("body".into(), Decl::lit("body").into());
739        let binder_ty = Ty::Var(binder.clone());
740        let sig = SigTy::unary(binder_ty, Ty::Builtin(BuiltinTy::Color));
741
742        let mut dynamic_bounds = DynTypeBounds::default();
743        dynamic_bounds
744            .ubs
745            .insert_mut(Ty::Builtin(BuiltinTy::Content(None)));
746        let mut info = TypeInfo::default();
747        info.vars.insert(
748            binder.def.clone(),
749            TypeVarBounds::new(binder.as_ref().clone(), dynamic_bounds),
750        );
751
752        assert_eq!(
753            format!("{:?}", info.simplify(Ty::Func(sig), true)),
754            "(@body) => Color"
755        );
756    }
757}