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    /// Count nested recursion binders at the current head position.
285    ///
286    /// Corresponds to Lean's `LocalTypeR.muHeight`.
287    #[must_use]
288    pub fn mu_height(&self) -> usize {
289        match self {
290            LocalTypeR::Mu { body, .. } => 1 + body.mu_height(),
291            _ => 0,
292        }
293    }
294
295    /// Fully unfold leading recursion until a non-`Mu` head is exposed.
296    ///
297    /// Corresponds to Lean's `LocalTypeR.fullUnfold`.
298    #[must_use]
299    pub fn full_unfold(&self) -> LocalTypeR {
300        let mut current = self.clone();
301        let fuel = self.mu_height();
302        for _ in 0..fuel {
303            current = current.unfold();
304        }
305        current
306    }
307
308    /// Compute the dual of a local type (swap send/recv).
309    ///
310    /// The dual of role A's view is role B's view when A and B are the only participants.
311    /// Corresponds to Lean's `LocalTypeR.dual`.
312    #[must_use]
313    pub fn dual(&self) -> LocalTypeR {
314        match self {
315            LocalTypeR::End => LocalTypeR::End,
316            LocalTypeR::Send { partner, branches } => LocalTypeR::Recv {
317                partner: partner.clone(),
318                branches: branches
319                    .iter()
320                    .map(|(l, vt, cont)| (l.clone(), vt.clone(), cont.dual()))
321                    .collect(),
322            },
323            LocalTypeR::Recv { partner, branches } => LocalTypeR::Send {
324                partner: partner.clone(),
325                branches: branches
326                    .iter()
327                    .map(|(l, vt, cont)| (l.clone(), vt.clone(), cont.dual()))
328                    .collect(),
329            },
330            LocalTypeR::Mu { var, body } => LocalTypeR::Mu {
331                var: var.clone(),
332                body: Box::new(body.dual()),
333            },
334            LocalTypeR::Var(t) => LocalTypeR::Var(t.clone()),
335        }
336    }
337
338    /// Check if all recursion variables are bound.
339    ///
340    /// Corresponds to Lean's `LocalTypeR.allVarsBound`.
341    #[must_use]
342    pub fn all_vars_bound(&self) -> bool {
343        self.check_vars_bound(&BTreeSet::new())
344    }
345
346    fn check_vars_bound(&self, bound: &BTreeSet<String>) -> bool {
347        match self {
348            LocalTypeR::End => true,
349            LocalTypeR::Send { branches, .. } | LocalTypeR::Recv { branches, .. } => branches
350                .iter()
351                .all(|(_, _, cont)| cont.check_vars_bound(bound)),
352            LocalTypeR::Mu { var, body } => {
353                let mut new_bound = bound.clone();
354                new_bound.insert(var.clone());
355                body.check_vars_bound(&new_bound)
356            }
357            LocalTypeR::Var(t) => bound.contains(t),
358        }
359    }
360
361    /// Check if each choice has at least one branch.
362    ///
363    /// Corresponds to Lean's `LocalTypeR.allChoicesNonEmpty`.
364    #[must_use]
365    pub fn all_choices_non_empty(&self) -> bool {
366        match self {
367            LocalTypeR::End => true,
368            LocalTypeR::Send { branches, .. } | LocalTypeR::Recv { branches, .. } => {
369                !branches.is_empty()
370                    && branches
371                        .iter()
372                        .all(|(_, _, cont)| cont.all_choices_non_empty())
373            }
374            LocalTypeR::Mu { body, .. } => body.all_choices_non_empty(),
375            LocalTypeR::Var(_) => true,
376        }
377    }
378
379    fn branches_have_unique_names(branches: &[(Label, Option<ValType>, LocalTypeR)]) -> bool {
380        let mut seen = BTreeSet::new();
381        branches
382            .iter()
383            .all(|(label, _, _)| seen.insert(label.name.clone()))
384    }
385
386    /// Check if all send/recv nodes have unique branch label names.
387    #[must_use]
388    pub fn unique_branch_labels(&self) -> bool {
389        match self {
390            LocalTypeR::End | LocalTypeR::Var(_) => true,
391            LocalTypeR::Mu { body, .. } => body.unique_branch_labels(),
392            LocalTypeR::Send { branches, .. } | LocalTypeR::Recv { branches, .. } => {
393                Self::branches_have_unique_names(branches)
394                    && branches
395                        .iter()
396                        .all(|(_, _, cont)| cont.unique_branch_labels())
397            }
398        }
399    }
400
401    /// Well-formedness predicate for local types.
402    ///
403    /// Corresponds to Lean's `LocalTypeR.wellFormed`.
404    /// A local type is well-formed if:
405    /// 1. All recursion variables are bound
406    /// 2. Each choice has at least one branch
407    /// 3. All recursion is guarded (no immediate recursion without communication)
408    #[must_use]
409    pub fn well_formed(&self) -> bool {
410        self.all_vars_bound()
411            && self.all_choices_non_empty()
412            && self.unique_branch_labels()
413            && self.is_guarded()
414    }
415
416    /// Check whether full unfolding exposes a communication head.
417    ///
418    /// Corresponds to Lean's `LocalTypeR.reachesCommunication`.
419    #[must_use]
420    pub fn reaches_communication(&self) -> bool {
421        match self.full_unfold() {
422            LocalTypeR::Send { branches, .. } | LocalTypeR::Recv { branches, .. } => {
423                !branches.is_empty()
424            }
425            LocalTypeR::End | LocalTypeR::Var(_) | LocalTypeR::Mu { .. } => false,
426        }
427    }
428
429    /// Mechanically characterized practical fragment that needs no manual `ReachesComm`.
430    ///
431    /// Corresponds to Lean's `LocalTypeR.regularPracticalFragment`.
432    #[must_use]
433    pub fn regular_practical_fragment(&self) -> bool {
434        self.well_formed() && self.reaches_communication()
435    }
436
437    /// Count the depth of a local type (for termination proofs).
438    ///
439    /// Corresponds to Lean's `LocalTypeR.depth`.
440    #[must_use]
441    pub fn depth(&self) -> usize {
442        match self {
443            LocalTypeR::End => 0,
444            LocalTypeR::Send { branches, .. } | LocalTypeR::Recv { branches, .. } => {
445                1 + branches
446                    .iter()
447                    .map(|(_, _, t)| t.depth())
448                    .max()
449                    .unwrap_or(0)
450            }
451            LocalTypeR::Mu { body, .. } => 1 + body.depth(),
452            LocalTypeR::Var(_) => 0,
453        }
454    }
455
456    /// Check if a local type is guarded (no immediate recursion).
457    ///
458    /// Corresponds to Lean's `LocalTypeR.isGuarded`.
459    #[must_use]
460    pub fn is_guarded(&self) -> bool {
461        match self {
462            LocalTypeR::End => true,
463            LocalTypeR::Send { branches, .. } | LocalTypeR::Recv { branches, .. } => {
464                branches.iter().all(|(_, _, cont)| cont.is_guarded())
465            }
466            LocalTypeR::Mu { body, .. } => match body.as_ref() {
467                LocalTypeR::Var(_) | LocalTypeR::Mu { .. } => false,
468                _ => body.is_guarded(),
469            },
470            LocalTypeR::Var(_) => true,
471        }
472    }
473
474    /// Check if a specific recursion variable is guarded in this local type.
475    ///
476    /// A variable `v` is guarded if it does not appear "bare" (i.e., every
477    /// occurrence is under a send or recv). Corresponds to Lean's
478    /// `LocalTypeR.isGuarded v`.
479    #[must_use]
480    pub fn is_var_guarded(&self, var: &str) -> bool {
481        match self {
482            LocalTypeR::End => true,
483            LocalTypeR::Var(w) => w != var,
484            LocalTypeR::Send { .. } | LocalTypeR::Recv { .. } => true,
485            LocalTypeR::Mu { var: t, body, .. } => {
486                if var == t {
487                    true
488                } else {
489                    body.is_var_guarded(var)
490                }
491            }
492        }
493    }
494
495    /// Extract labels from the outermost visible choice node.
496    ///
497    /// For recursive terms (`Mu`), this inspects the wrapped body.
498    #[must_use]
499    pub fn head_labels(&self) -> Vec<String> {
500        match self {
501            LocalTypeR::End | LocalTypeR::Var(_) => vec![],
502            LocalTypeR::Send { branches, .. } | LocalTypeR::Recv { branches, .. } => {
503                branches.iter().map(|(l, _, _)| l.name.clone()).collect()
504            }
505            LocalTypeR::Mu { body, .. } => body.head_labels(),
506        }
507    }
508
509    /// Extract all labels that appear anywhere in the local type.
510    #[must_use]
511    pub fn all_labels(&self) -> Vec<String> {
512        let mut labels = BTreeSet::new();
513        self.collect_all_labels(&mut labels);
514        labels.into_iter().collect()
515    }
516
517    fn collect_all_labels(&self, labels: &mut BTreeSet<String>) {
518        match self {
519            LocalTypeR::End | LocalTypeR::Var(_) => {}
520            LocalTypeR::Mu { body, .. } => body.collect_all_labels(labels),
521            LocalTypeR::Send { branches, .. } | LocalTypeR::Recv { branches, .. } => {
522                for (label, _, cont) in branches {
523                    labels.insert(label.name.clone());
524                    cont.collect_all_labels(labels);
525                }
526            }
527        }
528    }
529
530    /// Extract all partners from a local type.
531    ///
532    /// Corresponds to Lean's `LocalTypeR.partners`.
533    #[must_use]
534    pub fn partners(&self) -> Vec<String> {
535        let mut result = BTreeSet::new();
536        self.collect_partners(&mut result);
537        result.into_iter().collect()
538    }
539
540    fn collect_partners(&self, partners: &mut BTreeSet<String>) {
541        match self {
542            LocalTypeR::End | LocalTypeR::Var(_) => {}
543            LocalTypeR::Send { partner, branches } | LocalTypeR::Recv { partner, branches } => {
544                partners.insert(partner.clone());
545                for (_, _, cont) in branches {
546                    cont.collect_partners(partners);
547                }
548            }
549            LocalTypeR::Mu { body, .. } => body.collect_partners(partners),
550        }
551    }
552
553    /// Check if a local type mentions a specific partner.
554    #[must_use]
555    pub fn mentions_partner(&self, role: &str) -> bool {
556        self.mentions_partner_inner(role)
557    }
558
559    fn mentions_partner_inner(&self, role: &str) -> bool {
560        match self {
561            LocalTypeR::End | LocalTypeR::Var(_) => false,
562            LocalTypeR::Mu { body, .. } => body.mentions_partner_inner(role),
563            LocalTypeR::Send { partner, branches } | LocalTypeR::Recv { partner, branches } => {
564                partner == role
565                    || branches
566                        .iter()
567                        .any(|(_, _, cont)| cont.mentions_partner_inner(role))
568            }
569        }
570    }
571
572    /// Check if this is an internal choice (send)
573    #[must_use]
574    pub fn is_send(&self) -> bool {
575        matches!(self, LocalTypeR::Send { .. })
576    }
577
578    /// Check if this is an external choice (recv)
579    #[must_use]
580    pub fn is_recv(&self) -> bool {
581        matches!(self, LocalTypeR::Recv { .. })
582    }
583
584    /// Check if this is a terminated type
585    #[must_use]
586    pub fn is_end(&self) -> bool {
587        matches!(self, LocalTypeR::End)
588    }
589}
590
591#[cfg(test)]
592mod tests {
593    use super::*;
594    use crate::PayloadSort;
595    use assert_matches::assert_matches;
596    use proptest::prelude::*;
597
598    #[test]
599    fn test_simple_local_type() {
600        // !B{msg.end}
601        let lt = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::End);
602        assert!(lt.well_formed());
603        assert_eq!(lt.partners().len(), 1);
604        assert!(lt.mentions_partner("B"));
605    }
606
607    #[test]
608    fn test_recursive_local_type() {
609        // μt. !B{msg.t}
610        let lt = LocalTypeR::mu(
611            "t",
612            LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("t")),
613        );
614        assert!(lt.well_formed());
615        assert!(lt.all_vars_bound());
616    }
617
618    #[test]
619    fn test_dual() {
620        // !B{msg.end} dual is ?B{msg.end}
621        let send = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::End);
622        let recv = send.dual();
623
624        assert_matches!(recv, LocalTypeR::Recv { partner, branches } => {
625            assert_eq!(partner, "B");
626            assert_eq!(branches.len(), 1);
627            assert_eq!((branches[0].0).name, "msg");
628        });
629    }
630
631    #[test]
632    fn test_unfold() {
633        // μt. !B{msg.t} unfolds to !B{msg.(μt. !B{msg.t})}
634        let lt = LocalTypeR::mu(
635            "t",
636            LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("t")),
637        );
638        let unfolded = lt.unfold();
639
640        assert_matches!(unfolded, LocalTypeR::Send { partner, branches } => {
641            assert_eq!(partner, "B");
642            assert_matches!(branches[0].2, LocalTypeR::Mu { .. });
643        });
644    }
645
646    #[test]
647    fn test_substitute() {
648        let lt = LocalTypeR::var("t");
649        let replacement = LocalTypeR::End;
650        assert_eq!(lt.substitute("t", &replacement), LocalTypeR::End);
651        assert_eq!(lt.substitute("s", &replacement), LocalTypeR::var("t"));
652    }
653
654    #[test]
655    fn test_substitute_avoids_capture() {
656        let lt = LocalTypeR::mu("y", LocalTypeR::var("x"));
657        let result = lt.substitute("x", &LocalTypeR::var("y"));
658
659        assert_matches!(result, LocalTypeR::Mu { var, body } => {
660            assert_ne!(var, "y");
661            assert_matches!(*body, LocalTypeR::Var(ref name) => {
662                assert_eq!(name, "y");
663            });
664        });
665    }
666
667    #[test]
668    fn test_substitute_avoids_capture_with_nested_binders() {
669        let lt = LocalTypeR::mu("y", LocalTypeR::mu("y_0", LocalTypeR::var("x")));
670        let result = lt.substitute("x", &LocalTypeR::var("y"));
671        assert!(result.free_vars().contains(&"y".to_string()));
672    }
673
674    #[test]
675    fn test_unbound_variable() {
676        // !B{msg.t} where t is unbound
677        let lt = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("t"));
678        assert!(!lt.all_vars_bound());
679        assert!(!lt.well_formed());
680    }
681
682    #[test]
683    fn test_guarded() {
684        // μt. t is not guarded
685        let unguarded = LocalTypeR::mu("t", LocalTypeR::var("t"));
686        assert!(!unguarded.is_guarded());
687        assert!(!unguarded.well_formed()); // Unguarded recursion should fail well_formed()
688
689        // μt. !B{msg.t} is guarded
690        let guarded = LocalTypeR::mu(
691            "t",
692            LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("t")),
693        );
694        assert!(guarded.is_guarded());
695        assert!(guarded.well_formed()); // Guarded recursion should pass well_formed()
696    }
697
698    #[test]
699    fn test_mu_height_and_full_unfold() {
700        let lt = LocalTypeR::mu(
701            "outer",
702            LocalTypeR::mu(
703                "inner",
704                LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("inner")),
705            ),
706        );
707        assert_eq!(lt.mu_height(), 2);
708        assert_matches!(lt.full_unfold(), LocalTypeR::Send { partner, branches } => {
709            assert_eq!(partner, "B");
710            assert_eq!(branches.len(), 1);
711        });
712    }
713
714    #[test]
715    fn test_reaches_communication_matches_practical_fragment_intent() {
716        assert!(!LocalTypeR::End.reaches_communication());
717        assert!(!LocalTypeR::End.regular_practical_fragment());
718
719        let send = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::End);
720        assert!(send.reaches_communication());
721        assert!(send.regular_practical_fragment());
722
723        let guarded = LocalTypeR::mu(
724            "t",
725            LocalTypeR::recv("B", Label::new("msg"), LocalTypeR::var("t")),
726        );
727        assert!(guarded.reaches_communication());
728        assert!(guarded.regular_practical_fragment());
729
730        let unguarded = LocalTypeR::mu("t", LocalTypeR::var("t"));
731        assert!(!unguarded.reaches_communication());
732        assert!(!unguarded.regular_practical_fragment());
733    }
734
735    #[test]
736    fn test_free_vars() {
737        // μt. !B{msg.s} has free var s
738        let lt = LocalTypeR::mu(
739            "t",
740            LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("s")),
741        );
742        let free = lt.free_vars();
743        assert_eq!(free, vec!["s"]);
744    }
745
746    #[test]
747    fn test_choice_with_payload() {
748        let branches = vec![
749            (
750                Label::with_sort("accept", PayloadSort::Bool),
751                None,
752                LocalTypeR::End,
753            ),
754            (
755                Label::with_sort("data", PayloadSort::Nat),
756                None,
757                LocalTypeR::End,
758            ),
759        ];
760        let lt = LocalTypeR::recv_choice("A", branches);
761        assert!(lt.well_formed());
762        assert_eq!(lt.head_labels(), vec!["accept", "data"]);
763    }
764
765    #[test]
766    fn test_depth() {
767        let lt = LocalTypeR::send(
768            "B",
769            Label::new("outer"),
770            LocalTypeR::send("C", Label::new("inner"), LocalTypeR::End),
771        );
772        assert_eq!(lt.depth(), 2);
773    }
774
775    #[test]
776    fn test_is_send_recv() {
777        let send = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::End);
778        let recv = LocalTypeR::recv("B", Label::new("msg"), LocalTypeR::End);
779
780        assert!(send.is_send());
781        assert!(!send.is_recv());
782        assert!(recv.is_recv());
783        assert!(!recv.is_send());
784    }
785
786    #[test]
787    fn test_duplicate_branch_labels_not_well_formed() {
788        let lt = LocalTypeR::recv_choice(
789            "A",
790            vec![
791                (Label::new("msg"), None, LocalTypeR::End),
792                (Label::new("msg"), None, LocalTypeR::End),
793            ],
794        );
795        assert!(!lt.unique_branch_labels());
796        assert!(!lt.well_formed());
797    }
798
799    #[test]
800    fn test_partners_are_sorted() {
801        let lt = LocalTypeR::send(
802            "Zed",
803            Label::new("outer"),
804            LocalTypeR::recv(
805                "Alice",
806                Label::new("mid"),
807                LocalTypeR::send("Bob", Label::new("inner"), LocalTypeR::End),
808            ),
809        );
810
811        assert_eq!(lt.partners(), vec!["Alice", "Bob", "Zed"]);
812    }
813
814    #[test]
815    fn test_mentions_partner_direct_traversal_matches_expectation() {
816        let lt = LocalTypeR::mu(
817            "t",
818            LocalTypeR::send(
819                "A",
820                Label::new("x"),
821                LocalTypeR::recv("B", Label::new("y"), LocalTypeR::var("t")),
822            ),
823        );
824        assert!(lt.mentions_partner("A"));
825        assert!(lt.mentions_partner("B"));
826        assert!(!lt.mentions_partner("C"));
827    }
828
829    #[test]
830    fn test_all_labels_collects_nested_labels() {
831        let lt = LocalTypeR::send(
832            "B",
833            Label::new("outer"),
834            LocalTypeR::recv(
835                "A",
836                Label::new("inner"),
837                LocalTypeR::send("C", Label::new("leaf"), LocalTypeR::End),
838            ),
839        );
840        assert_eq!(lt.head_labels(), vec!["outer"]);
841        assert_eq!(lt.all_labels(), vec!["inner", "leaf", "outer"]);
842    }
843
844    fn arb_var_name() -> impl Strategy<Value = String> {
845        prop_oneof![
846            Just("x".to_string()),
847            Just("y".to_string()),
848            Just("z".to_string()),
849            Just("t".to_string()),
850            Just("u".to_string()),
851        ]
852    }
853
854    proptest! {
855        #[test]
856        fn prop_substitute_identity_when_var_absent(var in arb_var_name()) {
857            let lt = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::End);
858            let replacement = LocalTypeR::var("r");
859            prop_assert_eq!(lt.substitute(&var, &replacement), lt);
860        }
861
862        #[test]
863        fn prop_substitute_avoids_capture_simple(
864            binder in arb_var_name(),
865            target in arb_var_name(),
866        ) {
867            prop_assume!(binder != target);
868            let lt = LocalTypeR::mu(&binder, LocalTypeR::var(&target));
869            let replacement = LocalTypeR::var(&binder);
870            let result = lt.substitute(&target, &replacement);
871            prop_assert!(result.free_vars().contains(&binder));
872        }
873    }
874}