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