Skip to main content

telltale_types/
global.rs

1//! Global Types for Multiparty Session Type Protocols
2//!
3//! This module defines global types that describe protocols from a bird's-eye view.
4//! Global types specify the complete interaction pattern between all participants,
5//! including message exchanges, choices, and recursive behavior.
6//!
7//! Based on: "A Very Gentle Introduction to Multiparty Session Types" (Yoshida & Gheri)
8//!
9//! # Lean Correspondence
10//!
11//! This module mirrors the definitions in `lean/SessionTypes/GlobalType.lean`:
12//! - `PayloadSort` ↔ Lean's `PayloadSort`
13//! - `GlobalType` ↔ Lean's `GlobalType`
14
15use crate::Label;
16use serde::{Deserialize, Serialize};
17use std::collections::BTreeSet;
18
19/// Payload sort types for message payloads.
20///
21/// Corresponds to Lean's `PayloadSort` inductive type.
22/// These represent the data types that can be sent in messages.
23///
24/// # Examples
25///
26/// ```
27/// use telltale_types::PayloadSort;
28///
29/// let unit = PayloadSort::Unit;
30/// assert!(unit.is_simple());
31///
32/// let pair = PayloadSort::prod(PayloadSort::Nat, PayloadSort::Bool);
33/// assert!(!pair.is_simple());
34/// ```
35#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
36pub enum PayloadSort {
37    /// Unit type (no payload)
38    #[default]
39    Unit,
40    /// Natural numbers
41    Nat,
42    /// Booleans
43    Bool,
44    /// Strings
45    String,
46    /// Real numbers (floating point physical quantities)
47    Real,
48    /// Fixed-size vectors (e.g. configuration space, phase space)
49    Vector(usize),
50    /// Product types (pairs)
51    Prod(Box<PayloadSort>, Box<PayloadSort>),
52}
53
54impl PayloadSort {
55    /// Create a product sort
56    #[must_use]
57    pub fn prod(left: PayloadSort, right: PayloadSort) -> Self {
58        PayloadSort::Prod(Box::new(left), Box::new(right))
59    }
60
61    /// Create a vector sort
62    #[must_use]
63    pub fn vector(n: usize) -> Self {
64        PayloadSort::Vector(n)
65    }
66
67    /// Check if this is a simple (non-compound) sort
68    #[must_use]
69    pub fn is_simple(&self) -> bool {
70        !matches!(self, PayloadSort::Prod(_, _) | PayloadSort::Vector(_))
71    }
72}
73
74impl std::fmt::Display for PayloadSort {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        match self {
77            PayloadSort::Unit => write!(f, "Unit"),
78            PayloadSort::Nat => write!(f, "Nat"),
79            PayloadSort::Bool => write!(f, "Bool"),
80            PayloadSort::String => write!(f, "String"),
81            PayloadSort::Real => write!(f, "Real"),
82            PayloadSort::Vector(n) => write!(f, "Vector({})", n),
83            PayloadSort::Prod(l, r) => write!(f, "({} × {})", l, r),
84        }
85    }
86}
87
88/// Global types describe protocols from the bird's-eye view.
89///
90/// Corresponds to Lean's `GlobalType` inductive type.
91///
92/// # Syntax
93///
94/// - `End`: Protocol termination
95/// - `Comm { sender, receiver, branches }`: Communication with labeled branches
96/// - `Mu { var, body }`: Recursive type μt.G
97/// - `Var(t)`: Type variable reference
98///
99/// The `Comm` variant models `p → q : {l₁(S₁).G₁, l₂(S₂).G₂, ...}`
100/// where the sender p chooses which branch to take.
101///
102/// # Examples
103///
104/// ```
105/// use telltale_types::{GlobalType, Label};
106///
107/// // Simple protocol: A -> B: hello. end
108/// let g = GlobalType::send("A", "B", Label::new("hello"), GlobalType::End);
109/// assert!(g.well_formed());
110///
111/// // Recursive protocol: μt. A -> B: msg. t
112/// let rec = GlobalType::mu(
113///     "t",
114///     GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("t")),
115/// );
116/// assert!(rec.well_formed());
117/// ```
118#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
119pub enum GlobalType {
120    /// Protocol termination
121    End,
122    /// Communication: sender → receiver with choice of labeled continuations
123    Comm {
124        sender: String,
125        receiver: String,
126        branches: Vec<(Label, GlobalType)>,
127    },
128    /// Recursive type: μt.G binds variable t in body G
129    Mu { var: String, body: Box<GlobalType> },
130    /// Type variable: reference to enclosing μ-binder
131    Var(String),
132}
133
134impl GlobalType {
135    fn collect_all_var_names(&self, names: &mut BTreeSet<String>) {
136        match self {
137            GlobalType::End => {}
138            GlobalType::Comm { branches, .. } => {
139                for (_, cont) in branches {
140                    cont.collect_all_var_names(names);
141                }
142            }
143            GlobalType::Mu { var, body } => {
144                names.insert(var.clone());
145                body.collect_all_var_names(names);
146            }
147            GlobalType::Var(t) => {
148                names.insert(t.clone());
149            }
150        }
151    }
152
153    fn all_var_names(&self) -> BTreeSet<String> {
154        let mut names = BTreeSet::new();
155        self.collect_all_var_names(&mut names);
156        names
157    }
158
159    fn fresh_var(base: &str, avoid: &BTreeSet<String>) -> String {
160        let mut idx = 0usize;
161        loop {
162            // bounded: idx increments until fresh name found (finite avoid set)
163            let candidate = format!("{base}_{idx}");
164            if !avoid.contains(&candidate) {
165                return candidate;
166            }
167            idx += 1;
168        }
169    }
170
171    /// Create a simple send without choice
172    #[must_use]
173    pub fn send(
174        sender: impl Into<String>,
175        receiver: impl Into<String>,
176        label: Label,
177        cont: GlobalType,
178    ) -> Self {
179        GlobalType::Comm {
180            sender: sender.into(),
181            receiver: receiver.into(),
182            branches: vec![(label, cont)],
183        }
184    }
185
186    /// Create a communication with multiple branches
187    #[must_use]
188    pub fn comm(
189        sender: impl Into<String>,
190        receiver: impl Into<String>,
191        branches: Vec<(Label, GlobalType)>,
192    ) -> Self {
193        GlobalType::Comm {
194            sender: sender.into(),
195            receiver: receiver.into(),
196            branches,
197        }
198    }
199
200    /// Create a recursive type
201    #[must_use]
202    pub fn mu(var: impl Into<String>, body: GlobalType) -> Self {
203        GlobalType::Mu {
204            var: var.into(),
205            body: Box::new(body),
206        }
207    }
208
209    /// Create a type variable
210    #[must_use]
211    pub fn var(name: impl Into<String>) -> Self {
212        GlobalType::Var(name.into())
213    }
214
215    /// Extract all role names from this global type.
216    ///
217    /// Corresponds to Lean's `GlobalType.roles`.
218    #[must_use]
219    pub fn roles(&self) -> Vec<String> {
220        let mut result = BTreeSet::new();
221        self.collect_roles(&mut result);
222        result.into_iter().collect()
223    }
224
225    fn collect_roles(&self, roles: &mut BTreeSet<String>) {
226        match self {
227            GlobalType::End => {}
228            GlobalType::Comm {
229                sender,
230                receiver,
231                branches,
232            } => {
233                roles.insert(sender.clone());
234                roles.insert(receiver.clone());
235                for (_, cont) in branches {
236                    cont.collect_roles(roles);
237                }
238            }
239            GlobalType::Mu { body, .. } => body.collect_roles(roles),
240            GlobalType::Var(_) => {}
241        }
242    }
243
244    /// Extract free type variables from this global type.
245    ///
246    /// Corresponds to Lean's `GlobalType.freeVars`.
247    #[must_use]
248    pub fn free_vars(&self) -> Vec<String> {
249        let mut result = Vec::new();
250        let mut bound = BTreeSet::new();
251        self.collect_free_vars(&mut result, &mut bound);
252        result
253    }
254
255    fn collect_free_vars(&self, free: &mut Vec<String>, bound: &mut BTreeSet<String>) {
256        match self {
257            GlobalType::End => {}
258            GlobalType::Comm { branches, .. } => {
259                for (_, cont) in branches {
260                    cont.collect_free_vars(free, bound);
261                }
262            }
263            GlobalType::Mu { var, body } => {
264                bound.insert(var.clone());
265                body.collect_free_vars(free, bound);
266                bound.remove(var);
267            }
268            GlobalType::Var(t) => {
269                if !bound.contains(t) && !free.contains(t) {
270                    free.push(t.clone());
271                }
272            }
273        }
274    }
275
276    /// Substitute a global type for a variable.
277    ///
278    /// Corresponds to Lean's `GlobalType.substitute`.
279    #[must_use]
280    pub fn substitute(&self, var_name: &str, replacement: &GlobalType) -> GlobalType {
281        match self {
282            GlobalType::End => GlobalType::End,
283            GlobalType::Comm {
284                sender,
285                receiver,
286                branches,
287            } => GlobalType::Comm {
288                sender: sender.clone(),
289                receiver: receiver.clone(),
290                branches: branches
291                    .iter()
292                    .map(|(l, cont)| (l.clone(), cont.substitute(var_name, replacement)))
293                    .collect(),
294            },
295            GlobalType::Mu { var, body } => {
296                if var == var_name {
297                    // Variable is shadowed by this binder
298                    GlobalType::Mu {
299                        var: var.clone(),
300                        body: body.clone(),
301                    }
302                } else {
303                    let replacement_free = replacement.free_vars();
304                    if replacement_free.iter().any(|v| v == var) {
305                        // Alpha-rename binder to avoid capture before descending.
306                        let mut avoid = body.all_var_names();
307                        avoid.extend(replacement.all_var_names());
308                        avoid.insert(var_name.to_string());
309                        let fresh = Self::fresh_var(var, &avoid);
310                        let renamed_body = body.substitute(var, &GlobalType::Var(fresh.clone()));
311                        return GlobalType::Mu {
312                            var: fresh,
313                            body: Box::new(renamed_body.substitute(var_name, replacement)),
314                        };
315                    }
316                    GlobalType::Mu {
317                        var: var.clone(),
318                        body: Box::new(body.substitute(var_name, replacement)),
319                    }
320                }
321            }
322            GlobalType::Var(t) => {
323                if t == var_name {
324                    replacement.clone()
325                } else {
326                    GlobalType::Var(t.clone())
327                }
328            }
329        }
330    }
331
332    /// Unfold one level of recursion: μt.G ↦ G[μt.G/t]
333    ///
334    /// Corresponds to Lean's `GlobalType.unfold`.
335    #[must_use]
336    pub fn unfold(&self) -> GlobalType {
337        match self {
338            GlobalType::Mu { var, body } => body.substitute(var, self),
339            _ => self.clone(),
340        }
341    }
342
343    /// Check if all recursion variables are bound.
344    ///
345    /// Corresponds to Lean's `GlobalType.allVarsBound`.
346    #[must_use]
347    pub fn all_vars_bound(&self) -> bool {
348        self.check_vars_bound(&BTreeSet::new())
349    }
350
351    fn check_vars_bound(&self, bound: &BTreeSet<String>) -> bool {
352        match self {
353            GlobalType::End => true,
354            GlobalType::Comm { branches, .. } => branches
355                .iter()
356                .all(|(_, cont)| cont.check_vars_bound(bound)),
357            GlobalType::Mu { var, body } => {
358                let mut new_bound = bound.clone();
359                new_bound.insert(var.clone());
360                body.check_vars_bound(&new_bound)
361            }
362            GlobalType::Var(t) => bound.contains(t),
363        }
364    }
365
366    /// Check if each communication has at least one branch.
367    ///
368    /// Corresponds to Lean's `GlobalType.allCommsNonEmpty`.
369    #[must_use]
370    pub fn all_comms_non_empty(&self) -> bool {
371        match self {
372            GlobalType::End => true,
373            GlobalType::Comm { branches, .. } => {
374                !branches.is_empty() && branches.iter().all(|(_, cont)| cont.all_comms_non_empty())
375            }
376            GlobalType::Mu { body, .. } => body.all_comms_non_empty(),
377            GlobalType::Var(_) => true,
378        }
379    }
380
381    fn branches_have_unique_names(branches: &[(Label, GlobalType)]) -> bool {
382        let mut seen = BTreeSet::new();
383        branches
384            .iter()
385            .all(|(label, _)| seen.insert(label.name.clone()))
386    }
387
388    /// Check if all communication nodes have unique branch label names.
389    #[must_use]
390    pub fn unique_branch_labels(&self) -> bool {
391        match self {
392            GlobalType::End | GlobalType::Var(_) => true,
393            GlobalType::Mu { body, .. } => body.unique_branch_labels(),
394            GlobalType::Comm { branches, .. } => {
395                Self::branches_have_unique_names(branches)
396                    && branches.iter().all(|(_, cont)| cont.unique_branch_labels())
397            }
398        }
399    }
400
401    /// Check if sender and receiver are different in each communication.
402    ///
403    /// Corresponds to Lean's `GlobalType.noSelfComm`.
404    #[must_use]
405    pub fn no_self_comm(&self) -> bool {
406        match self {
407            GlobalType::End => true,
408            GlobalType::Comm {
409                sender,
410                receiver,
411                branches,
412            } => sender != receiver && branches.iter().all(|(_, cont)| cont.no_self_comm()),
413            GlobalType::Mu { body, .. } => body.no_self_comm(),
414            GlobalType::Var(_) => true,
415        }
416    }
417
418    /// Well-formedness predicate for global types.
419    ///
420    /// Corresponds to Lean's `GlobalType.wellFormed`.
421    /// A global type is well-formed if:
422    /// 1. All recursion variables are bound
423    /// 2. Each communication has at least one branch
424    /// 3. Sender ≠ receiver in each communication
425    /// 4. All recursion is guarded (no immediate recursion without communication)
426    #[must_use]
427    pub fn well_formed(&self) -> bool {
428        self.all_vars_bound()
429            && self.all_comms_non_empty()
430            && self.unique_branch_labels()
431            && self.no_self_comm()
432            && self.is_guarded()
433    }
434
435    /// Check if a role participates in the global type.
436    ///
437    /// Corresponds to Lean's `GlobalType.mentionsRole`.
438    #[must_use]
439    pub fn mentions_role(&self, role: &str) -> bool {
440        self.mentions_role_inner(role)
441    }
442
443    fn mentions_role_inner(&self, role: &str) -> bool {
444        match self {
445            GlobalType::End | GlobalType::Var(_) => false,
446            GlobalType::Mu { body, .. } => body.mentions_role_inner(role),
447            GlobalType::Comm {
448                sender,
449                receiver,
450                branches,
451            } => {
452                sender == role
453                    || receiver == role
454                    || branches
455                        .iter()
456                        .any(|(_, cont)| cont.mentions_role_inner(role))
457            }
458        }
459    }
460
461    /// Count the depth of a global type (for termination proofs).
462    ///
463    /// Corresponds to Lean's `GlobalType.depth`.
464    #[must_use]
465    pub fn depth(&self) -> usize {
466        match self {
467            GlobalType::End => 0,
468            GlobalType::Comm { branches, .. } => {
469                1 + branches.iter().map(|(_, g)| g.depth()).max().unwrap_or(0)
470            }
471            GlobalType::Mu { body, .. } => 1 + body.depth(),
472            GlobalType::Var(_) => 0,
473        }
474    }
475
476    /// Check if a global type is guarded (no immediate recursion without communication).
477    ///
478    /// Corresponds to Lean's `GlobalType.isGuarded`.
479    #[must_use]
480    pub fn is_guarded(&self) -> bool {
481        match self {
482            GlobalType::End => true,
483            GlobalType::Comm { branches, .. } => branches.iter().all(|(_, cont)| cont.is_guarded()),
484            GlobalType::Mu { body, .. } => match body.as_ref() {
485                GlobalType::Var(_) | GlobalType::Mu { .. } => false,
486                _ => body.is_guarded(),
487            },
488            GlobalType::Var(_) => true,
489        }
490    }
491
492    /// Consume a communication from a global type.
493    ///
494    /// Corresponds to Lean's `GlobalType.consume`.
495    /// G \ p →ℓ q represents the global type after the communication p →ℓ q
496    /// has been performed.
497    #[must_use]
498    pub fn consume(&self, sender: &str, receiver: &str, label: &str) -> Option<GlobalType> {
499        self.consume_with_fuel(sender, receiver, label, self.depth() + 1)
500    }
501
502    fn consume_with_fuel(
503        &self,
504        sender: &str,
505        receiver: &str,
506        label: &str,
507        fuel: usize,
508    ) -> Option<GlobalType> {
509        if fuel == 0 {
510            return None;
511        }
512        match self {
513            GlobalType::Comm {
514                sender: s,
515                receiver: r,
516                branches,
517            } => {
518                if s == sender && r == receiver {
519                    branches
520                        .iter()
521                        .find(|(l, _)| l.name == label)
522                        .map(|(_, cont)| cont.clone())
523                } else {
524                    None
525                }
526            }
527            GlobalType::Mu { var, body } => {
528                // Unfold and try again
529                body.substitute(var, self)
530                    .consume_with_fuel(sender, receiver, label, fuel - 1)
531            }
532            _ => None,
533        }
534    }
535}
536
537#[cfg(test)]
538mod tests {
539    use super::*;
540    use assert_matches::assert_matches;
541    use proptest::prelude::*;
542
543    #[test]
544    fn test_simple_protocol() {
545        // A -> B: hello. end
546        let g = GlobalType::send("A", "B", Label::new("hello"), GlobalType::End);
547        assert!(g.well_formed());
548        assert_eq!(g.roles().len(), 2);
549        assert!(g.mentions_role("A"));
550        assert!(g.mentions_role("B"));
551    }
552
553    #[test]
554    fn test_recursive_protocol() {
555        // μt. A -> B: msg. t
556        let g = GlobalType::mu(
557            "t",
558            GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("t")),
559        );
560        assert!(g.well_formed());
561        assert!(g.all_vars_bound());
562    }
563
564    #[test]
565    fn test_unbound_variable() {
566        // A -> B: msg. t (t is unbound)
567        let g = GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("t"));
568        assert!(!g.all_vars_bound());
569        assert!(!g.well_formed());
570    }
571
572    #[test]
573    fn test_self_communication() {
574        // A -> A: msg. end (self-communication)
575        let g = GlobalType::send("A", "A", Label::new("msg"), GlobalType::End);
576        assert!(!g.no_self_comm());
577        assert!(!g.well_formed());
578    }
579
580    #[test]
581    fn test_unfold() {
582        // μt. A -> B: msg. t unfolds to A -> B: msg. (μt. A -> B: msg. t)
583        let g = GlobalType::mu(
584            "t",
585            GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("t")),
586        );
587        let unfolded = g.unfold();
588        assert_matches!(unfolded, GlobalType::Comm { sender, receiver, branches } => {
589            assert_eq!(sender, "A");
590            assert_eq!(receiver, "B");
591            assert_eq!(branches.len(), 1);
592            // Continuation should be the original recursive type
593            assert_matches!(branches[0].1, GlobalType::Mu { .. });
594        });
595    }
596
597    #[test]
598    fn test_substitute() {
599        let g = GlobalType::var("t");
600        let replacement = GlobalType::End;
601        assert_eq!(g.substitute("t", &replacement), GlobalType::End);
602        assert_eq!(g.substitute("s", &replacement), GlobalType::var("t"));
603    }
604
605    #[test]
606    fn test_substitute_avoids_capture() {
607        // Substituting x := y under μy must alpha-rename the binder first.
608        let g = GlobalType::mu("y", GlobalType::var("x"));
609        let result = g.substitute("x", &GlobalType::var("y"));
610
611        assert_matches!(result, GlobalType::Mu { var, body } => {
612            assert_ne!(var, "y");
613            assert_matches!(*body, GlobalType::Var(ref name) => {
614                assert_eq!(name, "y");
615            });
616        });
617    }
618
619    #[test]
620    fn test_substitute_avoids_capture_with_nested_binders() {
621        // Freshening should avoid collisions with existing binder names.
622        let g = GlobalType::mu("y", GlobalType::mu("y_0", GlobalType::var("x")));
623        let result = g.substitute("x", &GlobalType::var("y"));
624        assert!(result.free_vars().contains(&"y".to_string()));
625    }
626
627    #[test]
628    fn test_consume() {
629        let g = GlobalType::comm(
630            "A",
631            "B",
632            vec![
633                (Label::new("accept"), GlobalType::End),
634                (Label::new("reject"), GlobalType::End),
635            ],
636        );
637
638        assert_eq!(g.consume("A", "B", "accept"), Some(GlobalType::End));
639        assert_eq!(g.consume("A", "B", "reject"), Some(GlobalType::End));
640        assert_eq!(g.consume("A", "B", "unknown"), None);
641        assert_eq!(g.consume("B", "A", "accept"), None);
642    }
643
644    #[test]
645    fn test_consume_unguarded_recursion_returns_none() {
646        let unguarded = GlobalType::mu("t", GlobalType::var("t"));
647        assert_eq!(unguarded.consume("A", "B", "msg"), None);
648    }
649
650    #[test]
651    fn test_consume_guarded_recursion_still_works() {
652        let g = GlobalType::mu(
653            "t",
654            GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("t")),
655        );
656        let out = g.consume("A", "B", "msg");
657        assert!(matches!(out, Some(GlobalType::Mu { .. })));
658    }
659
660    #[test]
661    fn test_payload_sort() {
662        let sort = PayloadSort::prod(PayloadSort::Nat, PayloadSort::Bool);
663        assert!(!sort.is_simple());
664
665        let label = Label::with_sort("data", sort);
666        assert_eq!(label.name, "data");
667    }
668
669    #[test]
670    fn test_guarded() {
671        // μt. t is not guarded (immediate recursion)
672        let unguarded = GlobalType::mu("t", GlobalType::var("t"));
673        assert!(!unguarded.is_guarded());
674        assert!(!unguarded.well_formed()); // Unguarded recursion should fail well_formed()
675
676        // μt. A -> B: msg. t is guarded
677        let guarded = GlobalType::mu(
678            "t",
679            GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("t")),
680        );
681        assert!(guarded.is_guarded());
682        assert!(guarded.well_formed()); // Guarded recursion should pass well_formed()
683    }
684
685    #[test]
686    fn test_duplicate_branch_labels_not_well_formed() {
687        let g = GlobalType::comm(
688            "A",
689            "B",
690            vec![
691                (Label::new("msg"), GlobalType::End),
692                (Label::new("msg"), GlobalType::End),
693            ],
694        );
695        assert!(!g.unique_branch_labels());
696        assert!(!g.well_formed());
697    }
698
699    #[test]
700    fn test_roles_are_sorted() {
701        let g = GlobalType::comm(
702            "Zed",
703            "Alice",
704            vec![(
705                Label::new("start"),
706                GlobalType::send("Bob", "Carol", Label::new("next"), GlobalType::End),
707            )],
708        );
709
710        assert_eq!(g.roles(), vec!["Alice", "Bob", "Carol", "Zed"]);
711    }
712
713    #[test]
714    fn test_mentions_role_direct_traversal_matches_expectation() {
715        let g = GlobalType::mu(
716            "t",
717            GlobalType::comm(
718                "A",
719                "B",
720                vec![(
721                    Label::new("x"),
722                    GlobalType::send("B", "C", Label::new("y"), GlobalType::var("t")),
723                )],
724            ),
725        );
726        assert!(g.mentions_role("A"));
727        assert!(g.mentions_role("B"));
728        assert!(g.mentions_role("C"));
729        assert!(!g.mentions_role("D"));
730    }
731
732    fn arb_var_name() -> impl Strategy<Value = String> {
733        prop_oneof![
734            Just("x".to_string()),
735            Just("y".to_string()),
736            Just("z".to_string()),
737            Just("t".to_string()),
738            Just("u".to_string()),
739        ]
740    }
741
742    proptest! {
743        #[test]
744        fn prop_substitute_identity_when_var_absent(var in arb_var_name()) {
745            let g = GlobalType::send("A", "B", Label::new("msg"), GlobalType::End);
746            let replacement = GlobalType::var("r");
747            prop_assert_eq!(g.substitute(&var, &replacement), g);
748        }
749
750        #[test]
751        fn prop_substitute_avoids_capture_simple(
752            binder in arb_var_name(),
753            target in arb_var_name(),
754        ) {
755            prop_assume!(binder != target);
756            let g = GlobalType::mu(&binder, GlobalType::var(&target));
757            let replacement = GlobalType::var(&binder);
758            let result = g.substitute(&target, &replacement);
759            prop_assert!(result.free_vars().contains(&binder));
760        }
761    }
762}