Skip to main content

telltale_language/ast/
convert.rs

1//! Conversion utilities between DSL AST types and theory types.
2//!
3//! This module provides bidirectional conversion between choreography DSL types
4//! (Protocol, LocalType) and the formal theory types (GlobalType, LocalTypeR)
5//! from `telltale-types`.
6//!
7//! ## Supported Conversions
8//!
9//! - `Protocol → GlobalType`: For protocols using the common subset (Send, Choice, Rec, Var, End)
10//! - `LocalType → LocalTypeR`: For comparison of projection results
11//!
12//! ## Unsupported DSL Features
13//!
14//! The following DSL features cannot be converted to theory types:
15//! - `Broadcast`: Use `desugar_broadcast` first to convert to nested Sends
16//! - `Loop`: Use `desugar_loop` first to convert to Rec/Var
17//! - `Parallel`: No theory equivalent
18//! - `Extension`: No theory equivalent
19//! - `LocalChoice`: DSL-only feature for local decisions
20
21use super::{Branch, Choreography, LocalType, MessageType, NonEmptyVec, Protocol};
22use crate::ast::Condition;
23use telltale_types::{GlobalType as GlobalTypeCore, Label, LocalTypeR, PayloadSort};
24use thiserror::Error;
25
26/// Errors that can occur during conversion.
27#[derive(Debug, Clone, Error)]
28pub enum ConversionError {
29    /// Protocol contains unsupported DSL feature
30    #[error("unsupported DSL feature: {feature}. {hint}")]
31    UnsupportedFeature { feature: String, hint: String },
32
33    /// Empty choice branches
34    #[error("choice has no branches")]
35    EmptyChoice,
36
37    /// Invalid choice structure (branches don't start with Send from decider)
38    #[error("invalid choice: branch '{label}' does not start with Send from decider")]
39    InvalidChoice { label: String },
40
41    /// Inconsistent choice receivers
42    #[error("choice has inconsistent receivers: expected all branches to send to {expected}, but branch '{label}' sends to {actual}")]
43    InconsistentReceivers {
44        expected: String,
45        actual: String,
46        label: String,
47    },
48}
49
50/// Result type for conversion operations.
51pub type ConversionResult<T> = Result<T, ConversionError>;
52
53// ============================================================================
54// Protocol → GlobalType conversion
55// ============================================================================
56
57/// Convert a Choreography to a GlobalType (theory type).
58///
59/// This converts the common subset of protocols that are expressible in both
60/// the DSL and the formal theory.
61///
62/// # Errors
63///
64/// Returns an error if the protocol uses DSL-only features.
65pub fn choreography_to_global(choreography: &Choreography) -> ConversionResult<GlobalTypeCore> {
66    protocol_to_global(&choreography.protocol)
67}
68
69/// Convert a Protocol to a GlobalType (theory type).
70///
71/// # Supported Protocol Variants
72///
73/// - `Send`: Converts to `GlobalType::Comm` with single branch
74/// - `Choice`: Converts to `GlobalType::Comm` with multiple branches
75/// - `Rec`: Converts to `GlobalType::Mu`
76/// - `Var`: Converts to `GlobalType::Var`
77/// - `End`: Converts to `GlobalType::End`
78///
79/// # Errors
80///
81/// Returns `UnsupportedFeature` for: Broadcast, Loop, Parallel, Extension
82// RECURSION_SAFE: structural recursion over finite protocol AST depth.
83pub fn protocol_to_global(protocol: &Protocol) -> ConversionResult<GlobalTypeCore> {
84    match protocol {
85        Protocol::End => Ok(GlobalTypeCore::End),
86
87        Protocol::Var(ident) => Ok(GlobalTypeCore::var(ident.to_string())),
88
89        Protocol::Rec { label, body } => {
90            let body_global = protocol_to_global(body)?;
91            Ok(GlobalTypeCore::mu(label.to_string(), body_global))
92        }
93
94        Protocol::Send {
95            from,
96            to,
97            message,
98            continuation,
99            ..
100        } => convert_protocol_send(from, to, message, continuation),
101
102        Protocol::Choice {
103            role: decider,
104            branches,
105            ..
106        } => convert_protocol_choice(decider, branches),
107
108        Protocol::Broadcast { .. } => Err(ConversionError::UnsupportedFeature {
109            feature: "Broadcast".to_string(),
110            hint: "Desugar to nested Sends using desugar_broadcast() first".to_string(),
111        }),
112
113        Protocol::Loop { condition, body } => convert_protocol_loop(condition.as_ref(), body),
114
115        Protocol::Parallel { .. } => Err(ConversionError::UnsupportedFeature {
116            feature: "Parallel".to_string(),
117            hint: "Parallel composition has no theory equivalent".to_string(),
118        }),
119
120        Protocol::Begin { .. }
121        | Protocol::Await { .. }
122        | Protocol::Resolve { .. }
123        | Protocol::Invalidate { .. } => Err(ConversionError::UnsupportedFeature {
124            feature: "CommitmentLifecycle".to_string(),
125            hint:
126                "Explicit commitment lifecycle requires protocol-machine lowering instead of theory conversion"
127                    .to_string(),
128        }),
129
130        Protocol::Let { continuation, .. } => protocol_to_global(continuation),
131
132        Protocol::Case { .. } => Err(ConversionError::UnsupportedFeature {
133            feature: "Case".to_string(),
134            hint: "Authority-local case/of must be lowered to explicit protocol choices first"
135                .to_string(),
136        }),
137
138        Protocol::Timeout { .. } => Err(ConversionError::UnsupportedFeature {
139            feature: "Timeout".to_string(),
140            hint: "Timeout blocks must be lowered to explicit protocol choices before theory conversion"
141                .to_string(),
142        }),
143
144        Protocol::Publish { .. } => Err(ConversionError::UnsupportedFeature {
145            feature: "Publish".to_string(),
146            hint: "Publication surfaces must be erased or lowered before theory conversion"
147                .to_string(),
148        }),
149
150        Protocol::PublishAuthority { .. } => Err(ConversionError::UnsupportedFeature {
151            feature: "PublishAuthority".to_string(),
152            hint: "Canonical publication requires protocol-machine lowering before theory conversion"
153                .to_string(),
154        }),
155
156        Protocol::Materialize { .. } => Err(ConversionError::UnsupportedFeature {
157            feature: "Materialize".to_string(),
158            hint:
159                "Materialization requires protocol-machine lowering before theory conversion"
160                    .to_string(),
161        }),
162
163        Protocol::Handoff { .. } => Err(ConversionError::UnsupportedFeature {
164            feature: "Handoff".to_string(),
165            hint: "Semantic handoff must be lowered before theory conversion".to_string(),
166        }),
167
168        Protocol::DependentWork { .. } => Err(ConversionError::UnsupportedFeature {
169            feature: "DependentWork".to_string(),
170            hint: "Dependent work declarations must be lowered before theory conversion"
171                .to_string(),
172        }),
173
174        Protocol::Extension { .. } => Err(ConversionError::UnsupportedFeature {
175            feature: "Extension".to_string(),
176            hint: "Protocol extensions have no theory equivalent".to_string(),
177        }),
178    }
179}
180
181fn convert_protocol_send(
182    from: &super::Role,
183    to: &super::Role,
184    message: &MessageType,
185    continuation: &Protocol,
186) -> ConversionResult<GlobalTypeCore> {
187    let cont_global = protocol_to_global(continuation)?;
188    let label = message_to_label(message);
189    Ok(GlobalTypeCore::send(
190        from.name().to_string(),
191        to.name().to_string(),
192        label,
193        cont_global,
194    ))
195}
196
197fn convert_protocol_choice(
198    decider: &super::Role,
199    branches: &NonEmptyVec<Branch>,
200) -> ConversionResult<GlobalTypeCore> {
201    let first_receiver = extract_receiver(&branches[0], decider)?;
202    let mut global_branches = Vec::with_capacity(branches.len());
203    for branch in branches {
204        let (label, cont) = convert_choice_branch(branch, decider, &first_receiver)?;
205        global_branches.push((label, cont));
206    }
207    Ok(GlobalTypeCore::comm(
208        decider.name().to_string(),
209        first_receiver,
210        global_branches,
211    ))
212}
213
214/// Extract the receiver from a choice branch's initial Send.
215fn extract_receiver(branch: &Branch, decider: &super::Role) -> ConversionResult<String> {
216    match &branch.protocol {
217        Protocol::Send { from, to, .. } => {
218            if from.name() != decider.name() {
219                return Err(ConversionError::InvalidChoice {
220                    label: branch.label.to_string(),
221                });
222            }
223            Ok(to.name().to_string())
224        }
225        _ => Err(ConversionError::InvalidChoice {
226            label: branch.label.to_string(),
227        }),
228    }
229}
230
231/// Convert a choice branch to a (Label, GlobalType) pair.
232fn convert_choice_branch(
233    branch: &Branch,
234    decider: &super::Role,
235    expected_receiver: &str,
236) -> ConversionResult<(Label, GlobalTypeCore)> {
237    match &branch.protocol {
238        Protocol::Send {
239            from,
240            to,
241            message,
242            continuation,
243            ..
244        } => {
245            if from.name() != decider.name() {
246                return Err(ConversionError::InvalidChoice {
247                    label: branch.label.to_string(),
248                });
249            }
250            if *to.name() != expected_receiver {
251                return Err(ConversionError::InconsistentReceivers {
252                    expected: expected_receiver.to_string(),
253                    actual: to.name().to_string(),
254                    label: branch.label.to_string(),
255                });
256            }
257
258            let label = Label::new(branch.label.to_string());
259            let cont_global = if branch.label == message.name {
260                protocol_to_global(continuation)?
261            } else {
262                protocol_to_global(&branch.protocol)?
263            };
264            Ok((label, cont_global))
265        }
266        _ => Err(ConversionError::InvalidChoice {
267            label: branch.label.to_string(),
268        }),
269    }
270}
271
272fn append_global_continuation(
273    global: GlobalTypeCore,
274    continuation: GlobalTypeCore,
275) -> GlobalTypeCore {
276    match global {
277        GlobalTypeCore::End => continuation,
278        GlobalTypeCore::Comm {
279            sender,
280            receiver,
281            branches,
282        } => GlobalTypeCore::Comm {
283            sender,
284            receiver,
285            branches: branches
286                .into_iter()
287                .map(|(label, branch)| {
288                    (
289                        label,
290                        append_global_continuation(branch, continuation.clone()),
291                    )
292                })
293                .collect(),
294        },
295        GlobalTypeCore::Mu { var, body } => GlobalTypeCore::Mu {
296            var,
297            body: Box::new(append_global_continuation(*body, continuation)),
298        },
299        GlobalTypeCore::Var(var) => GlobalTypeCore::Var(var),
300    }
301}
302
303fn repeat_global(body: &Protocol, iterations: usize) -> ConversionResult<GlobalTypeCore> {
304    if iterations == 0 {
305        return Ok(GlobalTypeCore::End);
306    }
307
308    let body_global = protocol_to_global(body)?;
309    let mut repeated = GlobalTypeCore::End;
310    for _ in 0..iterations {
311        repeated = append_global_continuation(body_global.clone(), repeated);
312    }
313    Ok(repeated)
314}
315
316fn convert_protocol_loop(
317    condition: Option<&Condition>,
318    body: &Protocol,
319) -> ConversionResult<GlobalTypeCore> {
320    match condition {
321        Some(Condition::Count(iterations))
322        | Some(Condition::Fuel(iterations))
323        | Some(Condition::YieldAfter(iterations)) => repeat_global(body, *iterations),
324        Some(Condition::RoleDecides(_)) => Err(ConversionError::UnsupportedFeature {
325            feature: "Loop".to_string(),
326            hint: "Role-decides loops should be desugared to choice+recursion before theory conversion"
327                .to_string(),
328        }),
329        Some(Condition::Custom(_)) | Some(Condition::YieldWhen(_)) | None => {
330            Err(ConversionError::UnsupportedFeature {
331                feature: "Loop".to_string(),
332                hint: "Only finite counted loops currently lower to GlobalType; use recursion for open-ended loops"
333                    .to_string(),
334            })
335        }
336    }
337}
338
339/// Convert a MessageType to a Label.
340fn message_to_label(message: &MessageType) -> Label {
341    // Use the message name as the label name
342    // If there's a type annotation, we could map it to PayloadSort
343    let name = message.name.to_string();
344
345    // Default to Unit sort; type annotations would map to other PayloadSort variants
346    Label::with_sort(name, PayloadSort::Unit)
347}
348
349// ============================================================================
350// LocalType → LocalTypeR conversion
351// ============================================================================
352
353/// Convert a LocalType (DSL) to a LocalTypeR (theory type).
354///
355/// # Supported LocalType Variants
356///
357/// - `Send`: Converts to `LocalTypeR::Send`
358/// - `Receive`: Converts to `LocalTypeR::Recv`
359/// - `Select`: Converts to `LocalTypeR::Send` (internal choice)
360/// - `Branch`: Converts to `LocalTypeR::Recv` (external choice)
361/// - `Mu`/`Var`: Convert directly
362/// - `End`: Converts to `LocalTypeR::End`
363///
364/// # Errors
365///
366/// Returns error for: `LocalChoice`, `Loop` (DSL-only features)
367// RECURSION_SAFE: structural recursion over finite local-type AST depth.
368pub fn local_to_local_r(local: &LocalType) -> ConversionResult<LocalTypeR> {
369    match local {
370        LocalType::End => Ok(LocalTypeR::End),
371
372        LocalType::Var(ident) => Ok(LocalTypeR::Var(ident.to_string())),
373
374        LocalType::Send {
375            to,
376            message,
377            continuation,
378        } => convert_local_send(to, message, continuation),
379
380        LocalType::Receive {
381            from,
382            message,
383            continuation,
384        } => convert_local_receive(from, message, continuation),
385
386        LocalType::Select { to, branches } => convert_local_select(to, branches),
387
388        LocalType::Branch { from, branches } => convert_local_branch(from, branches),
389
390        LocalType::Rec { label, body } => {
391            let body_r = local_to_local_r(body)?;
392            Ok(LocalTypeR::mu(label.to_string(), body_r))
393        }
394
395        LocalType::LocalChoice { .. } => Err(ConversionError::UnsupportedFeature {
396            feature: "LocalChoice".to_string(),
397            hint: "LocalChoice (local decisions without communication) has no theory equivalent"
398                .to_string(),
399        }),
400
401        LocalType::Loop { condition, body } => convert_local_loop(condition.as_ref(), body),
402
403        LocalType::Timeout { .. } => Err(ConversionError::UnsupportedFeature {
404            feature: "Timeout".to_string(),
405            hint: "Timeout is a DSL extension with no theory equivalent".to_string(),
406        }),
407    }
408}
409
410fn convert_local_send(
411    to: &super::Role,
412    message: &MessageType,
413    continuation: &LocalType,
414) -> ConversionResult<LocalTypeR> {
415    let cont_r = local_to_local_r(continuation)?;
416    let label = message_to_label(message);
417    Ok(LocalTypeR::send(to.name().to_string(), label, cont_r))
418}
419
420fn convert_local_receive(
421    from: &super::Role,
422    message: &MessageType,
423    continuation: &LocalType,
424) -> ConversionResult<LocalTypeR> {
425    let cont_r = local_to_local_r(continuation)?;
426    let label = message_to_label(message);
427    Ok(LocalTypeR::recv(from.name().to_string(), label, cont_r))
428}
429
430fn convert_local_select(
431    to: &super::Role,
432    branches: &[(proc_macro2::Ident, LocalType)],
433) -> ConversionResult<LocalTypeR> {
434    let mut r_branches = Vec::with_capacity(branches.len());
435    for (ident, cont) in branches {
436        // Flatten nested Send that duplicates the choice label
437        // DSL produces: Select { Accept -> Send(Accept, End) }
438        // Theory expects: Send { (Accept, End) }
439        let cont_r = match cont {
440            LocalType::Send {
441                to: send_to,
442                message,
443                continuation,
444            } if send_to.name() == to.name() && message.name == *ident => {
445                local_to_local_r(continuation)?
446            }
447            _ => local_to_local_r(cont)?,
448        };
449        r_branches.push((Label::new(ident.to_string()), None, cont_r));
450    }
451    Ok(LocalTypeR::Send {
452        partner: to.name().to_string(),
453        branches: r_branches,
454    })
455}
456
457fn convert_local_branch(
458    from: &super::Role,
459    branches: &[(proc_macro2::Ident, LocalType)],
460) -> ConversionResult<LocalTypeR> {
461    let mut r_branches = Vec::with_capacity(branches.len());
462    for (ident, cont) in branches {
463        // Flatten nested Receive that duplicates the choice label
464        // DSL produces: Branch { Accept -> Recv(Accept, End) }
465        // Theory expects: Recv { (Accept, End) }
466        let cont_r = match cont {
467            LocalType::Receive {
468                from: recv_from,
469                message,
470                continuation,
471            } if recv_from.name() == from.name() && message.name == *ident => {
472                local_to_local_r(continuation)?
473            }
474            _ => local_to_local_r(cont)?,
475        };
476        r_branches.push((Label::new(ident.to_string()), None, cont_r));
477    }
478    Ok(LocalTypeR::Recv {
479        partner: from.name().to_string(),
480        branches: r_branches,
481    })
482}
483
484fn append_local_r_continuation(local: LocalTypeR, continuation: LocalTypeR) -> LocalTypeR {
485    match local {
486        LocalTypeR::End => continuation,
487        LocalTypeR::Send { partner, branches } => LocalTypeR::Send {
488            partner,
489            branches: branches
490                .into_iter()
491                .map(|(label, payload, branch)| {
492                    (
493                        label,
494                        payload,
495                        append_local_r_continuation(branch, continuation.clone()),
496                    )
497                })
498                .collect(),
499        },
500        LocalTypeR::Recv { partner, branches } => LocalTypeR::Recv {
501            partner,
502            branches: branches
503                .into_iter()
504                .map(|(label, payload, branch)| {
505                    (
506                        label,
507                        payload,
508                        append_local_r_continuation(branch, continuation.clone()),
509                    )
510                })
511                .collect(),
512        },
513        LocalTypeR::Mu { var, body } => LocalTypeR::Mu {
514            var,
515            body: Box::new(append_local_r_continuation(*body, continuation)),
516        },
517        LocalTypeR::Var(var) => LocalTypeR::Var(var),
518    }
519}
520
521fn repeat_local_r(body: &LocalType, iterations: usize) -> ConversionResult<LocalTypeR> {
522    if iterations == 0 {
523        return Ok(LocalTypeR::End);
524    }
525
526    let body_r = local_to_local_r(body)?;
527    let mut repeated = LocalTypeR::End;
528    for _ in 0..iterations {
529        repeated = append_local_r_continuation(body_r.clone(), repeated);
530    }
531    Ok(repeated)
532}
533
534fn convert_local_loop(
535    condition: Option<&Condition>,
536    body: &LocalType,
537) -> ConversionResult<LocalTypeR> {
538    match condition {
539        Some(Condition::Count(iterations))
540        | Some(Condition::Fuel(iterations))
541        | Some(Condition::YieldAfter(iterations)) => repeat_local_r(body, *iterations),
542        Some(Condition::RoleDecides(_)) => Err(ConversionError::UnsupportedFeature {
543            feature: "Loop".to_string(),
544            hint: "Role-decides loops should be desugared to choice+recursion before theory conversion"
545                .to_string(),
546        }),
547        Some(Condition::Custom(_)) | Some(Condition::YieldWhen(_)) | None => {
548            Err(ConversionError::UnsupportedFeature {
549                feature: "Loop".to_string(),
550                hint: "Only finite counted loops currently lower to LocalTypeR; use recursion for open-ended loops"
551                    .to_string(),
552            })
553        }
554    }
555}
556
557// ============================================================================
558// Comparison utilities
559// ============================================================================
560
561/// Compare two LocalTypeR values for structural equivalence.
562///
563/// This compares the structure, ignoring payload sorts (which are often Unit
564/// in DSL-generated types).
565// RECURSION_SAFE: synchronized structural recursion over finite type trees.
566pub fn local_types_equivalent(lt1: &LocalTypeR, lt2: &LocalTypeR) -> bool {
567    match (lt1, lt2) {
568        (LocalTypeR::End, LocalTypeR::End) => true,
569
570        (LocalTypeR::Var(v1), LocalTypeR::Var(v2)) => v1 == v2,
571
572        (LocalTypeR::Mu { var: v1, body: b1 }, LocalTypeR::Mu { var: v2, body: b2 }) => {
573            // Syntactic equality (not alpha-equivalence); requires matching variable names
574            v1 == v2 && local_types_equivalent(b1, b2)
575        }
576
577        (
578            LocalTypeR::Send {
579                partner: p1,
580                branches: bs1,
581            },
582            LocalTypeR::Send {
583                partner: p2,
584                branches: bs2,
585            },
586        ) => {
587            p1 == p2
588                && bs1.len() == bs2.len()
589                && bs1
590                    .iter()
591                    .zip(bs2.iter())
592                    .all(|((l1, _vt1, c1), (l2, _vt2, c2))| {
593                        l1.name == l2.name && local_types_equivalent(c1, c2)
594                    })
595        }
596
597        (
598            LocalTypeR::Recv {
599                partner: p1,
600                branches: bs1,
601            },
602            LocalTypeR::Recv {
603                partner: p2,
604                branches: bs2,
605            },
606        ) => {
607            p1 == p2
608                && bs1.len() == bs2.len()
609                && bs1
610                    .iter()
611                    .zip(bs2.iter())
612                    .all(|((l1, _vt1, c1), (l2, _vt2, c2))| {
613                        l1.name == l2.name && local_types_equivalent(c1, c2)
614                    })
615        }
616
617        _ => false,
618    }
619}
620
621#[cfg(test)]
622mod tests {
623    use super::super::annotation::Annotations;
624    use super::super::NonEmptyVec;
625    use super::*;
626    use proc_macro2::Ident;
627    use proc_macro2::Span;
628
629    fn ident(s: &str) -> Ident {
630        Ident::new(s, Span::call_site())
631    }
632
633    fn role(name: &str) -> super::super::Role {
634        super::super::Role::new(ident(name)).unwrap()
635    }
636
637    fn message(name: &str) -> MessageType {
638        MessageType {
639            name: ident(name),
640            type_annotation: None,
641            payload: None,
642        }
643    }
644
645    #[test]
646    fn test_simple_send_conversion() {
647        // A -> B: msg. end
648        let protocol = Protocol::Send {
649            from: role("A"),
650            to: role("B"),
651            message: message("msg"),
652            continuation: Box::new(Protocol::End),
653            annotations: Annotations::new(),
654            from_annotations: Annotations::new(),
655            to_annotations: Annotations::new(),
656        };
657
658        let global = protocol_to_global(&protocol).unwrap();
659        assert!(
660            matches!(global, GlobalTypeCore::Comm { sender, receiver, .. }
661            if sender == "A" && receiver == "B")
662        );
663    }
664
665    #[test]
666    fn test_recursive_conversion() {
667        // rec Loop { A -> B: msg. Loop }
668        let protocol = Protocol::Rec {
669            label: ident("Loop"),
670            body: Box::new(Protocol::Send {
671                from: role("A"),
672                to: role("B"),
673                message: message("msg"),
674                continuation: Box::new(Protocol::Var(ident("Loop"))),
675                annotations: Annotations::new(),
676                from_annotations: Annotations::new(),
677                to_annotations: Annotations::new(),
678            }),
679        };
680
681        let global = protocol_to_global(&protocol).unwrap();
682        assert!(matches!(global, GlobalTypeCore::Mu { var, .. } if var == "Loop"));
683    }
684
685    #[test]
686    fn test_broadcast_unsupported() {
687        let protocol = Protocol::Broadcast {
688            from: role("A"),
689            to_all: NonEmptyVec::from_head_tail(role("B"), vec![role("C")]),
690            message: message("msg"),
691            continuation: Box::new(Protocol::End),
692            annotations: Annotations::new(),
693            from_annotations: Annotations::new(),
694        };
695
696        let result = protocol_to_global(&protocol);
697        assert!(
698            matches!(result, Err(ConversionError::UnsupportedFeature { feature, .. }) if feature == "Broadcast")
699        );
700    }
701
702    #[test]
703    fn test_loop_unsupported() {
704        let protocol = Protocol::Loop {
705            condition: None,
706            body: Box::new(Protocol::End),
707        };
708        let result = protocol_to_global(&protocol);
709        assert!(
710            matches!(result, Err(ConversionError::UnsupportedFeature { feature, .. }) if feature == "Loop")
711        );
712    }
713
714    #[test]
715    fn test_parallel_unsupported() {
716        let protocol = Protocol::Parallel {
717            protocols: NonEmptyVec::from_head_tail(Protocol::End, vec![Protocol::End]),
718        };
719        let result = protocol_to_global(&protocol);
720        assert!(
721            matches!(result, Err(ConversionError::UnsupportedFeature { feature, .. }) if feature == "Parallel")
722        );
723    }
724
725    #[test]
726    fn test_local_type_conversion() {
727        // Send to B: msg. End
728        let local = LocalType::Send {
729            to: role("B"),
730            message: message("msg"),
731            continuation: Box::new(LocalType::End),
732        };
733
734        let local_r = local_to_local_r(&local).unwrap();
735        assert!(matches!(local_r, LocalTypeR::Send { partner, .. } if partner == "B"));
736    }
737
738    #[test]
739    fn test_localchoice_unsupported() {
740        let local = LocalType::LocalChoice {
741            branches: vec![(ident("L"), LocalType::End)],
742        };
743        let result = local_to_local_r(&local);
744        assert!(
745            matches!(result, Err(ConversionError::UnsupportedFeature { feature, .. }) if feature == "LocalChoice")
746        );
747    }
748
749    #[test]
750    fn test_timeout_unsupported() {
751        let local = LocalType::Timeout {
752            duration: std::time::Duration::from_millis(100),
753            body: Box::new(LocalType::End),
754            on_timeout: Box::new(LocalType::End),
755            on_cancel: None,
756        };
757        let result = local_to_local_r(&local);
758        assert!(
759            matches!(result, Err(ConversionError::UnsupportedFeature { feature, .. }) if feature == "Timeout")
760        );
761    }
762
763    #[test]
764    fn test_message_conversion_uses_unit_payload_sort() {
765        let protocol = Protocol::Send {
766            from: role("A"),
767            to: role("B"),
768            message: MessageType {
769                name: ident("payload_msg"),
770                type_annotation: None,
771                payload: Some(quote::quote! { Vec<u8> }),
772            },
773            continuation: Box::new(Protocol::End),
774            annotations: Annotations::new(),
775            from_annotations: Annotations::new(),
776            to_annotations: Annotations::new(),
777        };
778
779        let global = protocol_to_global(&protocol).expect("conversion should succeed");
780        match global {
781            GlobalTypeCore::Comm { branches, .. } => {
782                let (label, _) = &branches[0];
783                assert_eq!(label.sort, PayloadSort::Unit);
784            }
785            _ => panic!("expected comm"),
786        }
787    }
788
789    #[test]
790    fn test_equivalence_check() {
791        let lt1 = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::End);
792        let lt2 = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::End);
793        let lt3 = LocalTypeR::send("C", Label::new("msg"), LocalTypeR::End);
794
795        assert!(local_types_equivalent(&lt1, &lt2));
796        assert!(!local_types_equivalent(&lt1, &lt3));
797    }
798}