Skip to main content

telltale_types/
local.rs

1//! Local Session Types for Multiparty Protocols
2//!
3//! This module defines local types that describe protocols from a single participant's
4//! perspective. Local types are obtained by projecting global types onto specific roles.
5//!
6//! Based on: "A Very Gentle Introduction to Multiparty Session Types" (Yoshida & Gheri)
7//!
8//! # Lean Correspondence
9//!
10//! The core `LocalTypeR` enum mirrors `lean/SessionTypes/LocalTypeR/Core/Base.lean`:
11//! - `LocalTypeR::End` ↔ Lean's `LocalTypeR.end`
12//! - `LocalTypeR::Send` ↔ Lean's `LocalTypeR.send`
13//! - `LocalTypeR::Recv` ↔ Lean's `LocalTypeR.recv`
14//! - `LocalTypeR::Mu` ↔ Lean's `LocalTypeR.mu`
15//! - `LocalTypeR::Var` ↔ Lean's `LocalTypeR.var`
16
17use crate::{Label, ValType};
18use serde::{Deserialize, Serialize};
19use std::collections::BTreeSet;
20
21/// Core local type matching Lean's `LocalTypeR`.
22///
23/// This is the minimal type used for validation and correspondence proofs.
24/// For code generation, see the extended `LocalType` in the DSL crate.
25///
26/// # Examples
27///
28/// ```
29/// use telltale_types::{LocalTypeR, Label};
30///
31/// // Simple send: !B{msg.end}
32/// let lt = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::End);
33/// assert!(lt.well_formed());
34///
35/// // Recursive type: μt. !B{msg.t}
36/// let rec = LocalTypeR::mu(
37///     "t",
38///     LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("t")),
39/// );
40/// assert!(rec.well_formed());
41/// ```
42#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
43pub enum LocalTypeR {
44    /// Protocol termination
45    End,
46    /// Internal choice: send to partner with choice of continuations
47    Send {
48        partner: String,
49        branches: Vec<(Label, Option<ValType>, LocalTypeR)>,
50    },
51    /// External choice: receive from partner with offered continuations
52    Recv {
53        partner: String,
54        branches: Vec<(Label, Option<ValType>, LocalTypeR)>,
55    },
56    /// Recursive type: μt.T binds variable t in body T
57    Mu { var: String, body: Box<LocalTypeR> },
58    /// Type variable: reference to enclosing μ-binder
59    Var(String),
60}
61
62impl LocalTypeR {
63    fn collect_all_var_names(&self, names: &mut BTreeSet<String>) {
64        match self {
65            LocalTypeR::End => {}
66            LocalTypeR::Send { branches, .. } | LocalTypeR::Recv { branches, .. } => {
67                for (_, _, cont) in branches {
68                    cont.collect_all_var_names(names);
69                }
70            }
71            LocalTypeR::Mu { var, body } => {
72                names.insert(var.clone());
73                body.collect_all_var_names(names);
74            }
75            LocalTypeR::Var(t) => {
76                names.insert(t.clone());
77            }
78        }
79    }
80
81    fn all_var_names(&self) -> BTreeSet<String> {
82        let mut names = BTreeSet::new();
83        self.collect_all_var_names(&mut names);
84        names
85    }
86
87    fn fresh_var(base: &str, avoid: &BTreeSet<String>) -> String {
88        let mut idx = 0usize;
89        loop {
90            // bounded: idx increments until fresh name found (finite avoid set)
91            let candidate = format!("{base}_{idx}");
92            if !avoid.contains(&candidate) {
93                return candidate;
94            }
95            idx += 1;
96        }
97    }
98
99    /// Create a simple send with one label
100    #[must_use]
101    pub fn send(partner: impl Into<String>, label: Label, cont: LocalTypeR) -> Self {
102        LocalTypeR::Send {
103            partner: partner.into(),
104            branches: vec![(label, None, cont)],
105        }
106    }
107
108    /// Create a send with multiple branches
109    #[must_use]
110    pub fn send_choice(
111        partner: impl Into<String>,
112        branches: Vec<(Label, Option<ValType>, LocalTypeR)>,
113    ) -> Self {
114        LocalTypeR::Send {
115            partner: partner.into(),
116            branches,
117        }
118    }
119
120    /// Create a simple recv with one label
121    #[must_use]
122    pub fn recv(partner: impl Into<String>, label: Label, cont: LocalTypeR) -> Self {
123        LocalTypeR::Recv {
124            partner: partner.into(),
125            branches: vec![(label, None, cont)],
126        }
127    }
128
129    /// Create a recv with multiple branches
130    #[must_use]
131    pub fn recv_choice(
132        partner: impl Into<String>,
133        branches: Vec<(Label, Option<ValType>, LocalTypeR)>,
134    ) -> Self {
135        LocalTypeR::Recv {
136            partner: partner.into(),
137            branches,
138        }
139    }
140
141    /// Create a recursive type
142    #[must_use]
143    pub fn mu(var: impl Into<String>, body: LocalTypeR) -> Self {
144        LocalTypeR::Mu {
145            var: var.into(),
146            body: Box::new(body),
147        }
148    }
149
150    /// Create a type variable
151    #[must_use]
152    pub fn var(name: impl Into<String>) -> Self {
153        LocalTypeR::Var(name.into())
154    }
155
156    /// Extract free type variables from a local type.
157    ///
158    /// Corresponds to Lean's `LocalTypeR.freeVars`.
159    #[must_use]
160    pub fn free_vars(&self) -> Vec<String> {
161        let mut result = Vec::new();
162        let mut bound = BTreeSet::new();
163        self.collect_free_vars(&mut result, &mut bound);
164        result
165    }
166
167    fn collect_free_vars(&self, free: &mut Vec<String>, bound: &mut BTreeSet<String>) {
168        match self {
169            LocalTypeR::End => {}
170            LocalTypeR::Send { branches, .. } | LocalTypeR::Recv { branches, .. } => {
171                for (_, _, cont) in branches {
172                    cont.collect_free_vars(free, bound);
173                }
174            }
175            LocalTypeR::Mu { var, body } => {
176                bound.insert(var.clone());
177                body.collect_free_vars(free, bound);
178                bound.remove(var);
179            }
180            LocalTypeR::Var(t) => {
181                if !bound.contains(t) && !free.contains(t) {
182                    free.push(t.clone());
183                }
184            }
185        }
186    }
187
188    /// Substitute a local type for a variable.
189    ///
190    /// Corresponds to Lean's `LocalTypeR.substitute`.
191    #[must_use]
192    pub fn substitute(&self, var_name: &str, replacement: &LocalTypeR) -> LocalTypeR {
193        match self {
194            LocalTypeR::End => LocalTypeR::End,
195            LocalTypeR::Send { partner, branches } => {
196                Self::substitute_branching(partner, branches, var_name, replacement, true)
197            }
198            LocalTypeR::Recv { partner, branches } => {
199                Self::substitute_branching(partner, branches, var_name, replacement, false)
200            }
201            LocalTypeR::Mu { var, body } => Self::substitute_mu(var, body, var_name, replacement),
202            LocalTypeR::Var(t) => {
203                if t == var_name {
204                    replacement.clone()
205                } else {
206                    LocalTypeR::Var(t.clone())
207                }
208            }
209        }
210    }
211
212    fn substitute_branching(
213        partner: &str,
214        branches: &[(Label, Option<ValType>, LocalTypeR)],
215        var_name: &str,
216        replacement: &LocalTypeR,
217        is_send: bool,
218    ) -> LocalTypeR {
219        let substituted = branches
220            .iter()
221            .map(|(l, vt, cont)| {
222                (
223                    l.clone(),
224                    vt.clone(),
225                    cont.substitute(var_name, replacement),
226                )
227            })
228            .collect();
229        if is_send {
230            LocalTypeR::Send {
231                partner: partner.to_string(),
232                branches: substituted,
233            }
234        } else {
235            LocalTypeR::Recv {
236                partner: partner.to_string(),
237                branches: substituted,
238            }
239        }
240    }
241
242    fn substitute_mu(
243        var: &str,
244        body: &LocalTypeR,
245        var_name: &str,
246        replacement: &LocalTypeR,
247    ) -> LocalTypeR {
248        if var == var_name {
249            return LocalTypeR::Mu {
250                var: var.to_string(),
251                body: Box::new(body.clone()),
252            };
253        }
254        let replacement_free = replacement.free_vars();
255        if replacement_free.iter().any(|v| v == var) {
256            // Alpha-rename binder to avoid capture before descending.
257            let mut avoid = body.all_var_names();
258            avoid.extend(replacement.all_var_names());
259            avoid.insert(var_name.to_string());
260            let fresh = Self::fresh_var(var, &avoid);
261            let renamed_body = body.substitute(var, &LocalTypeR::Var(fresh.clone()));
262            return LocalTypeR::Mu {
263                var: fresh,
264                body: Box::new(renamed_body.substitute(var_name, replacement)),
265            };
266        }
267        LocalTypeR::Mu {
268            var: var.to_string(),
269            body: Box::new(body.substitute(var_name, replacement)),
270        }
271    }
272
273    /// Unfold one level of recursion: μt.T ↦ T[μt.T/t]
274    ///
275    /// Corresponds to Lean's `LocalTypeR.unfold`.
276    #[must_use]
277    pub fn unfold(&self) -> LocalTypeR {
278        match self {
279            LocalTypeR::Mu { var, body } => body.substitute(var, self),
280            _ => self.clone(),
281        }
282    }
283
284    /// Compute the dual of a local type (swap send/recv).
285    ///
286    /// The dual of role A's view is role B's view when A and B are the only participants.
287    /// Corresponds to Lean's `LocalTypeR.dual`.
288    #[must_use]
289    pub fn dual(&self) -> LocalTypeR {
290        match self {
291            LocalTypeR::End => LocalTypeR::End,
292            LocalTypeR::Send { partner, branches } => LocalTypeR::Recv {
293                partner: partner.clone(),
294                branches: branches
295                    .iter()
296                    .map(|(l, vt, cont)| (l.clone(), vt.clone(), cont.dual()))
297                    .collect(),
298            },
299            LocalTypeR::Recv { partner, branches } => LocalTypeR::Send {
300                partner: partner.clone(),
301                branches: branches
302                    .iter()
303                    .map(|(l, vt, cont)| (l.clone(), vt.clone(), cont.dual()))
304                    .collect(),
305            },
306            LocalTypeR::Mu { var, body } => LocalTypeR::Mu {
307                var: var.clone(),
308                body: Box::new(body.dual()),
309            },
310            LocalTypeR::Var(t) => LocalTypeR::Var(t.clone()),
311        }
312    }
313
314    /// Check if all recursion variables are bound.
315    ///
316    /// Corresponds to Lean's `LocalTypeR.allVarsBound`.
317    #[must_use]
318    pub fn all_vars_bound(&self) -> bool {
319        self.check_vars_bound(&BTreeSet::new())
320    }
321
322    fn check_vars_bound(&self, bound: &BTreeSet<String>) -> bool {
323        match self {
324            LocalTypeR::End => true,
325            LocalTypeR::Send { branches, .. } | LocalTypeR::Recv { branches, .. } => branches
326                .iter()
327                .all(|(_, _, cont)| cont.check_vars_bound(bound)),
328            LocalTypeR::Mu { var, body } => {
329                let mut new_bound = bound.clone();
330                new_bound.insert(var.clone());
331                body.check_vars_bound(&new_bound)
332            }
333            LocalTypeR::Var(t) => bound.contains(t),
334        }
335    }
336
337    /// Check if each choice has at least one branch.
338    ///
339    /// Corresponds to Lean's `LocalTypeR.allChoicesNonEmpty`.
340    #[must_use]
341    pub fn all_choices_non_empty(&self) -> bool {
342        match self {
343            LocalTypeR::End => true,
344            LocalTypeR::Send { branches, .. } | LocalTypeR::Recv { branches, .. } => {
345                !branches.is_empty()
346                    && branches
347                        .iter()
348                        .all(|(_, _, cont)| cont.all_choices_non_empty())
349            }
350            LocalTypeR::Mu { body, .. } => body.all_choices_non_empty(),
351            LocalTypeR::Var(_) => true,
352        }
353    }
354
355    fn branches_have_unique_names(branches: &[(Label, Option<ValType>, LocalTypeR)]) -> bool {
356        let mut seen = BTreeSet::new();
357        branches
358            .iter()
359            .all(|(label, _, _)| seen.insert(label.name.clone()))
360    }
361
362    /// Check if all send/recv nodes have unique branch label names.
363    #[must_use]
364    pub fn unique_branch_labels(&self) -> bool {
365        match self {
366            LocalTypeR::End | LocalTypeR::Var(_) => true,
367            LocalTypeR::Mu { body, .. } => body.unique_branch_labels(),
368            LocalTypeR::Send { branches, .. } | LocalTypeR::Recv { branches, .. } => {
369                Self::branches_have_unique_names(branches)
370                    && branches
371                        .iter()
372                        .all(|(_, _, cont)| cont.unique_branch_labels())
373            }
374        }
375    }
376
377    /// Well-formedness predicate for local types.
378    ///
379    /// Corresponds to Lean's `LocalTypeR.wellFormed`.
380    /// A local type is well-formed if:
381    /// 1. All recursion variables are bound
382    /// 2. Each choice has at least one branch
383    /// 3. All recursion is guarded (no immediate recursion without communication)
384    #[must_use]
385    pub fn well_formed(&self) -> bool {
386        self.all_vars_bound()
387            && self.all_choices_non_empty()
388            && self.unique_branch_labels()
389            && self.is_guarded()
390    }
391
392    /// Count the depth of a local type (for termination proofs).
393    ///
394    /// Corresponds to Lean's `LocalTypeR.depth`.
395    #[must_use]
396    pub fn depth(&self) -> usize {
397        match self {
398            LocalTypeR::End => 0,
399            LocalTypeR::Send { branches, .. } | LocalTypeR::Recv { branches, .. } => {
400                1 + branches
401                    .iter()
402                    .map(|(_, _, t)| t.depth())
403                    .max()
404                    .unwrap_or(0)
405            }
406            LocalTypeR::Mu { body, .. } => 1 + body.depth(),
407            LocalTypeR::Var(_) => 0,
408        }
409    }
410
411    /// Check if a local type is guarded (no immediate recursion).
412    ///
413    /// Corresponds to Lean's `LocalTypeR.isGuarded`.
414    #[must_use]
415    pub fn is_guarded(&self) -> bool {
416        match self {
417            LocalTypeR::End => true,
418            LocalTypeR::Send { branches, .. } | LocalTypeR::Recv { branches, .. } => {
419                branches.iter().all(|(_, _, cont)| cont.is_guarded())
420            }
421            LocalTypeR::Mu { body, .. } => match body.as_ref() {
422                LocalTypeR::Var(_) | LocalTypeR::Mu { .. } => false,
423                _ => body.is_guarded(),
424            },
425            LocalTypeR::Var(_) => true,
426        }
427    }
428
429    /// Check if a specific recursion variable is guarded in this local type.
430    ///
431    /// A variable `v` is guarded if it does not appear "bare" (i.e., every
432    /// occurrence is under a send or recv). Corresponds to Lean's
433    /// `LocalTypeR.isGuarded v`.
434    #[must_use]
435    pub fn is_var_guarded(&self, var: &str) -> bool {
436        match self {
437            LocalTypeR::End => true,
438            LocalTypeR::Var(w) => w != var,
439            LocalTypeR::Send { .. } | LocalTypeR::Recv { .. } => true,
440            LocalTypeR::Mu { var: t, body, .. } => {
441                if var == t {
442                    true
443                } else {
444                    body.is_var_guarded(var)
445                }
446            }
447        }
448    }
449
450    /// Extract labels from the outermost visible choice node.
451    ///
452    /// For recursive terms (`Mu`), this inspects the wrapped body.
453    #[must_use]
454    pub fn head_labels(&self) -> Vec<String> {
455        match self {
456            LocalTypeR::End | LocalTypeR::Var(_) => vec![],
457            LocalTypeR::Send { branches, .. } | LocalTypeR::Recv { branches, .. } => {
458                branches.iter().map(|(l, _, _)| l.name.clone()).collect()
459            }
460            LocalTypeR::Mu { body, .. } => body.head_labels(),
461        }
462    }
463
464    /// Backward-compatible alias for `head_labels`.
465    #[must_use]
466    pub fn labels(&self) -> Vec<String> {
467        self.head_labels()
468    }
469
470    /// Extract all labels that appear anywhere in the local type.
471    #[must_use]
472    pub fn all_labels(&self) -> Vec<String> {
473        let mut labels = BTreeSet::new();
474        self.collect_all_labels(&mut labels);
475        labels.into_iter().collect()
476    }
477
478    fn collect_all_labels(&self, labels: &mut BTreeSet<String>) {
479        match self {
480            LocalTypeR::End | LocalTypeR::Var(_) => {}
481            LocalTypeR::Mu { body, .. } => body.collect_all_labels(labels),
482            LocalTypeR::Send { branches, .. } | LocalTypeR::Recv { branches, .. } => {
483                for (label, _, cont) in branches {
484                    labels.insert(label.name.clone());
485                    cont.collect_all_labels(labels);
486                }
487            }
488        }
489    }
490
491    /// Extract all partners from a local type.
492    ///
493    /// Corresponds to Lean's `LocalTypeR.partners`.
494    #[must_use]
495    pub fn partners(&self) -> Vec<String> {
496        let mut result = BTreeSet::new();
497        self.collect_partners(&mut result);
498        result.into_iter().collect()
499    }
500
501    fn collect_partners(&self, partners: &mut BTreeSet<String>) {
502        match self {
503            LocalTypeR::End | LocalTypeR::Var(_) => {}
504            LocalTypeR::Send { partner, branches } | LocalTypeR::Recv { partner, branches } => {
505                partners.insert(partner.clone());
506                for (_, _, cont) in branches {
507                    cont.collect_partners(partners);
508                }
509            }
510            LocalTypeR::Mu { body, .. } => body.collect_partners(partners),
511        }
512    }
513
514    /// Check if a local type mentions a specific partner.
515    #[must_use]
516    pub fn mentions_partner(&self, role: &str) -> bool {
517        self.mentions_partner_inner(role)
518    }
519
520    fn mentions_partner_inner(&self, role: &str) -> bool {
521        match self {
522            LocalTypeR::End | LocalTypeR::Var(_) => false,
523            LocalTypeR::Mu { body, .. } => body.mentions_partner_inner(role),
524            LocalTypeR::Send { partner, branches } | LocalTypeR::Recv { partner, branches } => {
525                partner == role
526                    || branches
527                        .iter()
528                        .any(|(_, _, cont)| cont.mentions_partner_inner(role))
529            }
530        }
531    }
532
533    /// Check if this is an internal choice (send)
534    #[must_use]
535    pub fn is_send(&self) -> bool {
536        matches!(self, LocalTypeR::Send { .. })
537    }
538
539    /// Check if this is an external choice (recv)
540    #[must_use]
541    pub fn is_recv(&self) -> bool {
542        matches!(self, LocalTypeR::Recv { .. })
543    }
544
545    /// Check if this is a terminated type
546    #[must_use]
547    pub fn is_end(&self) -> bool {
548        matches!(self, LocalTypeR::End)
549    }
550}
551
552#[cfg(test)]
553mod tests {
554    use super::*;
555    use crate::PayloadSort;
556    use assert_matches::assert_matches;
557    use proptest::prelude::*;
558
559    #[test]
560    fn test_simple_local_type() {
561        // !B{msg.end}
562        let lt = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::End);
563        assert!(lt.well_formed());
564        assert_eq!(lt.partners().len(), 1);
565        assert!(lt.mentions_partner("B"));
566    }
567
568    #[test]
569    fn test_recursive_local_type() {
570        // μt. !B{msg.t}
571        let lt = LocalTypeR::mu(
572            "t",
573            LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("t")),
574        );
575        assert!(lt.well_formed());
576        assert!(lt.all_vars_bound());
577    }
578
579    #[test]
580    fn test_dual() {
581        // !B{msg.end} dual is ?B{msg.end}
582        let send = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::End);
583        let recv = send.dual();
584
585        assert_matches!(recv, LocalTypeR::Recv { partner, branches } => {
586            assert_eq!(partner, "B");
587            assert_eq!(branches.len(), 1);
588            assert_eq!((branches[0].0).name, "msg");
589        });
590    }
591
592    #[test]
593    fn test_unfold() {
594        // μt. !B{msg.t} unfolds to !B{msg.(μt. !B{msg.t})}
595        let lt = LocalTypeR::mu(
596            "t",
597            LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("t")),
598        );
599        let unfolded = lt.unfold();
600
601        assert_matches!(unfolded, LocalTypeR::Send { partner, branches } => {
602            assert_eq!(partner, "B");
603            assert_matches!(branches[0].2, LocalTypeR::Mu { .. });
604        });
605    }
606
607    #[test]
608    fn test_substitute() {
609        let lt = LocalTypeR::var("t");
610        let replacement = LocalTypeR::End;
611        assert_eq!(lt.substitute("t", &replacement), LocalTypeR::End);
612        assert_eq!(lt.substitute("s", &replacement), LocalTypeR::var("t"));
613    }
614
615    #[test]
616    fn test_substitute_avoids_capture() {
617        let lt = LocalTypeR::mu("y", LocalTypeR::var("x"));
618        let result = lt.substitute("x", &LocalTypeR::var("y"));
619
620        assert_matches!(result, LocalTypeR::Mu { var, body } => {
621            assert_ne!(var, "y");
622            assert_matches!(*body, LocalTypeR::Var(ref name) => {
623                assert_eq!(name, "y");
624            });
625        });
626    }
627
628    #[test]
629    fn test_substitute_avoids_capture_with_nested_binders() {
630        let lt = LocalTypeR::mu("y", LocalTypeR::mu("y_0", LocalTypeR::var("x")));
631        let result = lt.substitute("x", &LocalTypeR::var("y"));
632        assert!(result.free_vars().contains(&"y".to_string()));
633    }
634
635    #[test]
636    fn test_unbound_variable() {
637        // !B{msg.t} where t is unbound
638        let lt = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("t"));
639        assert!(!lt.all_vars_bound());
640        assert!(!lt.well_formed());
641    }
642
643    #[test]
644    fn test_guarded() {
645        // μt. t is not guarded
646        let unguarded = LocalTypeR::mu("t", LocalTypeR::var("t"));
647        assert!(!unguarded.is_guarded());
648        assert!(!unguarded.well_formed()); // Unguarded recursion should fail well_formed()
649
650        // μt. !B{msg.t} is guarded
651        let guarded = LocalTypeR::mu(
652            "t",
653            LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("t")),
654        );
655        assert!(guarded.is_guarded());
656        assert!(guarded.well_formed()); // Guarded recursion should pass well_formed()
657    }
658
659    #[test]
660    fn test_free_vars() {
661        // μt. !B{msg.s} has free var s
662        let lt = LocalTypeR::mu(
663            "t",
664            LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("s")),
665        );
666        let free = lt.free_vars();
667        assert_eq!(free, vec!["s"]);
668    }
669
670    #[test]
671    fn test_choice_with_payload() {
672        let branches = vec![
673            (
674                Label::with_sort("accept", PayloadSort::Bool),
675                None,
676                LocalTypeR::End,
677            ),
678            (
679                Label::with_sort("data", PayloadSort::Nat),
680                None,
681                LocalTypeR::End,
682            ),
683        ];
684        let lt = LocalTypeR::recv_choice("A", branches);
685        assert!(lt.well_formed());
686        assert_eq!(lt.head_labels(), vec!["accept", "data"]);
687        assert_eq!(lt.labels(), vec!["accept", "data"]);
688    }
689
690    #[test]
691    fn test_depth() {
692        let lt = LocalTypeR::send(
693            "B",
694            Label::new("outer"),
695            LocalTypeR::send("C", Label::new("inner"), LocalTypeR::End),
696        );
697        assert_eq!(lt.depth(), 2);
698    }
699
700    #[test]
701    fn test_is_send_recv() {
702        let send = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::End);
703        let recv = LocalTypeR::recv("B", Label::new("msg"), LocalTypeR::End);
704
705        assert!(send.is_send());
706        assert!(!send.is_recv());
707        assert!(recv.is_recv());
708        assert!(!recv.is_send());
709    }
710
711    #[test]
712    fn test_duplicate_branch_labels_not_well_formed() {
713        let lt = LocalTypeR::recv_choice(
714            "A",
715            vec![
716                (Label::new("msg"), None, LocalTypeR::End),
717                (Label::new("msg"), None, LocalTypeR::End),
718            ],
719        );
720        assert!(!lt.unique_branch_labels());
721        assert!(!lt.well_formed());
722    }
723
724    #[test]
725    fn test_partners_are_sorted() {
726        let lt = LocalTypeR::send(
727            "Zed",
728            Label::new("outer"),
729            LocalTypeR::recv(
730                "Alice",
731                Label::new("mid"),
732                LocalTypeR::send("Bob", Label::new("inner"), LocalTypeR::End),
733            ),
734        );
735
736        assert_eq!(lt.partners(), vec!["Alice", "Bob", "Zed"]);
737    }
738
739    #[test]
740    fn test_mentions_partner_direct_traversal_matches_expectation() {
741        let lt = LocalTypeR::mu(
742            "t",
743            LocalTypeR::send(
744                "A",
745                Label::new("x"),
746                LocalTypeR::recv("B", Label::new("y"), LocalTypeR::var("t")),
747            ),
748        );
749        assert!(lt.mentions_partner("A"));
750        assert!(lt.mentions_partner("B"));
751        assert!(!lt.mentions_partner("C"));
752    }
753
754    #[test]
755    fn test_all_labels_collects_nested_labels() {
756        let lt = LocalTypeR::send(
757            "B",
758            Label::new("outer"),
759            LocalTypeR::recv(
760                "A",
761                Label::new("inner"),
762                LocalTypeR::send("C", Label::new("leaf"), LocalTypeR::End),
763            ),
764        );
765        assert_eq!(lt.head_labels(), vec!["outer"]);
766        assert_eq!(lt.all_labels(), vec!["inner", "leaf", "outer"]);
767    }
768
769    fn arb_var_name() -> impl Strategy<Value = String> {
770        prop_oneof![
771            Just("x".to_string()),
772            Just("y".to_string()),
773            Just("z".to_string()),
774            Just("t".to_string()),
775            Just("u".to_string()),
776        ]
777    }
778
779    proptest! {
780        #[test]
781        fn prop_substitute_identity_when_var_absent(var in arb_var_name()) {
782            let lt = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::End);
783            let replacement = LocalTypeR::var("r");
784            prop_assert_eq!(lt.substitute(&var, &replacement), lt);
785        }
786
787        #[test]
788        fn prop_substitute_avoids_capture_simple(
789            binder in arb_var_name(),
790            target in arb_var_name(),
791        ) {
792            prop_assume!(binder != target);
793            let lt = LocalTypeR::mu(&binder, LocalTypeR::var(&target));
794            let replacement = LocalTypeR::var(&binder);
795            let result = lt.substitute(&target, &replacement);
796            prop_assert!(result.free_vars().contains(&binder));
797        }
798    }
799}