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    /// Backward-compatible alias for `head_labels`.
510    #[must_use]
511    pub fn labels(&self) -> Vec<String> {
512        self.head_labels()
513    }
514
515    /// Extract all labels that appear anywhere in the local type.
516    #[must_use]
517    pub fn all_labels(&self) -> Vec<String> {
518        let mut labels = BTreeSet::new();
519        self.collect_all_labels(&mut labels);
520        labels.into_iter().collect()
521    }
522
523    fn collect_all_labels(&self, labels: &mut BTreeSet<String>) {
524        match self {
525            LocalTypeR::End | LocalTypeR::Var(_) => {}
526            LocalTypeR::Mu { body, .. } => body.collect_all_labels(labels),
527            LocalTypeR::Send { branches, .. } | LocalTypeR::Recv { branches, .. } => {
528                for (label, _, cont) in branches {
529                    labels.insert(label.name.clone());
530                    cont.collect_all_labels(labels);
531                }
532            }
533        }
534    }
535
536    /// Extract all partners from a local type.
537    ///
538    /// Corresponds to Lean's `LocalTypeR.partners`.
539    #[must_use]
540    pub fn partners(&self) -> Vec<String> {
541        let mut result = BTreeSet::new();
542        self.collect_partners(&mut result);
543        result.into_iter().collect()
544    }
545
546    fn collect_partners(&self, partners: &mut BTreeSet<String>) {
547        match self {
548            LocalTypeR::End | LocalTypeR::Var(_) => {}
549            LocalTypeR::Send { partner, branches } | LocalTypeR::Recv { partner, branches } => {
550                partners.insert(partner.clone());
551                for (_, _, cont) in branches {
552                    cont.collect_partners(partners);
553                }
554            }
555            LocalTypeR::Mu { body, .. } => body.collect_partners(partners),
556        }
557    }
558
559    /// Check if a local type mentions a specific partner.
560    #[must_use]
561    pub fn mentions_partner(&self, role: &str) -> bool {
562        self.mentions_partner_inner(role)
563    }
564
565    fn mentions_partner_inner(&self, role: &str) -> bool {
566        match self {
567            LocalTypeR::End | LocalTypeR::Var(_) => false,
568            LocalTypeR::Mu { body, .. } => body.mentions_partner_inner(role),
569            LocalTypeR::Send { partner, branches } | LocalTypeR::Recv { partner, branches } => {
570                partner == role
571                    || branches
572                        .iter()
573                        .any(|(_, _, cont)| cont.mentions_partner_inner(role))
574            }
575        }
576    }
577
578    /// Check if this is an internal choice (send)
579    #[must_use]
580    pub fn is_send(&self) -> bool {
581        matches!(self, LocalTypeR::Send { .. })
582    }
583
584    /// Check if this is an external choice (recv)
585    #[must_use]
586    pub fn is_recv(&self) -> bool {
587        matches!(self, LocalTypeR::Recv { .. })
588    }
589
590    /// Check if this is a terminated type
591    #[must_use]
592    pub fn is_end(&self) -> bool {
593        matches!(self, LocalTypeR::End)
594    }
595}
596
597#[cfg(test)]
598mod tests {
599    use super::*;
600    use crate::PayloadSort;
601    use assert_matches::assert_matches;
602    use proptest::prelude::*;
603
604    #[test]
605    fn test_simple_local_type() {
606        // !B{msg.end}
607        let lt = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::End);
608        assert!(lt.well_formed());
609        assert_eq!(lt.partners().len(), 1);
610        assert!(lt.mentions_partner("B"));
611    }
612
613    #[test]
614    fn test_recursive_local_type() {
615        // μt. !B{msg.t}
616        let lt = LocalTypeR::mu(
617            "t",
618            LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("t")),
619        );
620        assert!(lt.well_formed());
621        assert!(lt.all_vars_bound());
622    }
623
624    #[test]
625    fn test_dual() {
626        // !B{msg.end} dual is ?B{msg.end}
627        let send = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::End);
628        let recv = send.dual();
629
630        assert_matches!(recv, LocalTypeR::Recv { partner, branches } => {
631            assert_eq!(partner, "B");
632            assert_eq!(branches.len(), 1);
633            assert_eq!((branches[0].0).name, "msg");
634        });
635    }
636
637    #[test]
638    fn test_unfold() {
639        // μt. !B{msg.t} unfolds to !B{msg.(μt. !B{msg.t})}
640        let lt = LocalTypeR::mu(
641            "t",
642            LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("t")),
643        );
644        let unfolded = lt.unfold();
645
646        assert_matches!(unfolded, LocalTypeR::Send { partner, branches } => {
647            assert_eq!(partner, "B");
648            assert_matches!(branches[0].2, LocalTypeR::Mu { .. });
649        });
650    }
651
652    #[test]
653    fn test_substitute() {
654        let lt = LocalTypeR::var("t");
655        let replacement = LocalTypeR::End;
656        assert_eq!(lt.substitute("t", &replacement), LocalTypeR::End);
657        assert_eq!(lt.substitute("s", &replacement), LocalTypeR::var("t"));
658    }
659
660    #[test]
661    fn test_substitute_avoids_capture() {
662        let lt = LocalTypeR::mu("y", LocalTypeR::var("x"));
663        let result = lt.substitute("x", &LocalTypeR::var("y"));
664
665        assert_matches!(result, LocalTypeR::Mu { var, body } => {
666            assert_ne!(var, "y");
667            assert_matches!(*body, LocalTypeR::Var(ref name) => {
668                assert_eq!(name, "y");
669            });
670        });
671    }
672
673    #[test]
674    fn test_substitute_avoids_capture_with_nested_binders() {
675        let lt = LocalTypeR::mu("y", LocalTypeR::mu("y_0", LocalTypeR::var("x")));
676        let result = lt.substitute("x", &LocalTypeR::var("y"));
677        assert!(result.free_vars().contains(&"y".to_string()));
678    }
679
680    #[test]
681    fn test_unbound_variable() {
682        // !B{msg.t} where t is unbound
683        let lt = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("t"));
684        assert!(!lt.all_vars_bound());
685        assert!(!lt.well_formed());
686    }
687
688    #[test]
689    fn test_guarded() {
690        // μt. t is not guarded
691        let unguarded = LocalTypeR::mu("t", LocalTypeR::var("t"));
692        assert!(!unguarded.is_guarded());
693        assert!(!unguarded.well_formed()); // Unguarded recursion should fail well_formed()
694
695        // μt. !B{msg.t} is guarded
696        let guarded = LocalTypeR::mu(
697            "t",
698            LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("t")),
699        );
700        assert!(guarded.is_guarded());
701        assert!(guarded.well_formed()); // Guarded recursion should pass well_formed()
702    }
703
704    #[test]
705    fn test_mu_height_and_full_unfold() {
706        let lt = LocalTypeR::mu(
707            "outer",
708            LocalTypeR::mu(
709                "inner",
710                LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("inner")),
711            ),
712        );
713        assert_eq!(lt.mu_height(), 2);
714        assert_matches!(lt.full_unfold(), LocalTypeR::Send { partner, branches } => {
715            assert_eq!(partner, "B");
716            assert_eq!(branches.len(), 1);
717        });
718    }
719
720    #[test]
721    fn test_reaches_communication_matches_practical_fragment_intent() {
722        assert!(!LocalTypeR::End.reaches_communication());
723        assert!(!LocalTypeR::End.regular_practical_fragment());
724
725        let send = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::End);
726        assert!(send.reaches_communication());
727        assert!(send.regular_practical_fragment());
728
729        let guarded = LocalTypeR::mu(
730            "t",
731            LocalTypeR::recv("B", Label::new("msg"), LocalTypeR::var("t")),
732        );
733        assert!(guarded.reaches_communication());
734        assert!(guarded.regular_practical_fragment());
735
736        let unguarded = LocalTypeR::mu("t", LocalTypeR::var("t"));
737        assert!(!unguarded.reaches_communication());
738        assert!(!unguarded.regular_practical_fragment());
739    }
740
741    #[test]
742    fn test_free_vars() {
743        // μt. !B{msg.s} has free var s
744        let lt = LocalTypeR::mu(
745            "t",
746            LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("s")),
747        );
748        let free = lt.free_vars();
749        assert_eq!(free, vec!["s"]);
750    }
751
752    #[test]
753    fn test_choice_with_payload() {
754        let branches = vec![
755            (
756                Label::with_sort("accept", PayloadSort::Bool),
757                None,
758                LocalTypeR::End,
759            ),
760            (
761                Label::with_sort("data", PayloadSort::Nat),
762                None,
763                LocalTypeR::End,
764            ),
765        ];
766        let lt = LocalTypeR::recv_choice("A", branches);
767        assert!(lt.well_formed());
768        assert_eq!(lt.head_labels(), vec!["accept", "data"]);
769        assert_eq!(lt.labels(), vec!["accept", "data"]);
770    }
771
772    #[test]
773    fn test_depth() {
774        let lt = LocalTypeR::send(
775            "B",
776            Label::new("outer"),
777            LocalTypeR::send("C", Label::new("inner"), LocalTypeR::End),
778        );
779        assert_eq!(lt.depth(), 2);
780    }
781
782    #[test]
783    fn test_is_send_recv() {
784        let send = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::End);
785        let recv = LocalTypeR::recv("B", Label::new("msg"), LocalTypeR::End);
786
787        assert!(send.is_send());
788        assert!(!send.is_recv());
789        assert!(recv.is_recv());
790        assert!(!recv.is_send());
791    }
792
793    #[test]
794    fn test_duplicate_branch_labels_not_well_formed() {
795        let lt = LocalTypeR::recv_choice(
796            "A",
797            vec![
798                (Label::new("msg"), None, LocalTypeR::End),
799                (Label::new("msg"), None, LocalTypeR::End),
800            ],
801        );
802        assert!(!lt.unique_branch_labels());
803        assert!(!lt.well_formed());
804    }
805
806    #[test]
807    fn test_partners_are_sorted() {
808        let lt = LocalTypeR::send(
809            "Zed",
810            Label::new("outer"),
811            LocalTypeR::recv(
812                "Alice",
813                Label::new("mid"),
814                LocalTypeR::send("Bob", Label::new("inner"), LocalTypeR::End),
815            ),
816        );
817
818        assert_eq!(lt.partners(), vec!["Alice", "Bob", "Zed"]);
819    }
820
821    #[test]
822    fn test_mentions_partner_direct_traversal_matches_expectation() {
823        let lt = LocalTypeR::mu(
824            "t",
825            LocalTypeR::send(
826                "A",
827                Label::new("x"),
828                LocalTypeR::recv("B", Label::new("y"), LocalTypeR::var("t")),
829            ),
830        );
831        assert!(lt.mentions_partner("A"));
832        assert!(lt.mentions_partner("B"));
833        assert!(!lt.mentions_partner("C"));
834    }
835
836    #[test]
837    fn test_all_labels_collects_nested_labels() {
838        let lt = LocalTypeR::send(
839            "B",
840            Label::new("outer"),
841            LocalTypeR::recv(
842                "A",
843                Label::new("inner"),
844                LocalTypeR::send("C", Label::new("leaf"), LocalTypeR::End),
845            ),
846        );
847        assert_eq!(lt.head_labels(), vec!["outer"]);
848        assert_eq!(lt.all_labels(), vec!["inner", "leaf", "outer"]);
849    }
850
851    fn arb_var_name() -> impl Strategy<Value = String> {
852        prop_oneof![
853            Just("x".to_string()),
854            Just("y".to_string()),
855            Just("z".to_string()),
856            Just("t".to_string()),
857            Just("u".to_string()),
858        ]
859    }
860
861    proptest! {
862        #[test]
863        fn prop_substitute_identity_when_var_absent(var in arb_var_name()) {
864            let lt = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::End);
865            let replacement = LocalTypeR::var("r");
866            prop_assert_eq!(lt.substitute(&var, &replacement), lt);
867        }
868
869        #[test]
870        fn prop_substitute_avoids_capture_simple(
871            binder in arb_var_name(),
872            target in arb_var_name(),
873        ) {
874            prop_assume!(binder != target);
875            let lt = LocalTypeR::mu(&binder, LocalTypeR::var(&target));
876            let replacement = LocalTypeR::var(&binder);
877            let result = lt.substitute(&target, &replacement);
878            prop_assert!(result.free_vars().contains(&binder));
879        }
880    }
881}