Skip to main content

telltale_language/ast/
execution_hints.rs

1//! Execution Hints for Choreographic Protocols
2//!
3//! This module defines execution hints that are extracted from choreography annotations
4//! and used during code generation. Hints are separate from the pure session types
5//! (LocalType) to maintain Lean correspondence.
6//!
7//! # Design Rationale
8//!
9//! Annotations like `@parallel` and `@min_responses(N)` are deployment concerns,
10//! not protocol semantics. Keeping them separate from LocalType:
11//! - Preserves Lean correspondence (LocalType matches LocalTypeR)
12//! - Aligns with spatial type theory (Intent vs Deployment layers)
13//! - Allows hints to evolve independently of type theory
14//!
15//! # Architecture
16//!
17//! ```text
18//! Choreography AST                    ExecutionHints
19//! (with annotations)                  (extracted separately)
20//!        │                                   │
21//!        │ projection                        │ (passed through)
22//!        ▼                                   ▼
23//!    LocalType              +          ExecutionHints
24//!   (pure types)                      (keyed by path)
25//!        │                                   │
26//!        └──────────────┬────────────────────┘
27//!                       │ codegen
28//!                       ▼
29//!               Generated Code
30//!          (consults hints for parallel)
31//! ```
32
33use super::choreography::Choreography;
34use super::protocol::Protocol;
35use super::Annotations;
36use std::collections::HashMap;
37use std::fmt;
38
39/// A step in the path to an operation within a local type tree.
40///
41/// Used to identify specific operations for hint application.
42#[derive(Debug, Clone, PartialEq, Eq, Hash)]
43pub enum OperationStep {
44    /// Send operation at index N in sequence
45    Send(usize),
46    /// Receive operation at index N in sequence
47    Recv(usize),
48    /// Branch with given label
49    Branch(String),
50    /// Select with given label
51    Select(String),
52    /// Loop iteration at index N
53    Loop(usize),
54    /// Recursion point with given label
55    Rec(String),
56}
57
58impl fmt::Display for OperationStep {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        match self {
61            OperationStep::Send(n) => write!(f, "send:{}", n),
62            OperationStep::Recv(n) => write!(f, "recv:{}", n),
63            OperationStep::Branch(label) => write!(f, "branch:{}", label),
64            OperationStep::Select(label) => write!(f, "select:{}", label),
65            OperationStep::Loop(n) => write!(f, "loop:{}", n),
66            OperationStep::Rec(label) => write!(f, "rec:{}", label),
67        }
68    }
69}
70
71/// Path to an operation in the local type tree.
72///
73/// A sequence of steps that uniquely identifies an operation.
74/// For example, `[Send(0), Branch("Accept"), Recv(0)]` identifies
75/// the first receive inside the "Accept" branch after the first send.
76#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
77pub struct OperationPath(Vec<OperationStep>);
78
79impl OperationPath {
80    /// Create an empty path (root).
81    #[must_use]
82    pub fn new() -> Self {
83        Self(Vec::new())
84    }
85
86    /// Create a path from steps.
87    #[must_use]
88    pub fn from_steps(steps: Vec<OperationStep>) -> Self {
89        Self(steps)
90    }
91
92    /// Append a step to the path, returning a new path.
93    #[must_use]
94    pub fn push(&self, step: OperationStep) -> Self {
95        let mut steps = self.0.clone();
96        steps.push(step);
97        Self(steps)
98    }
99
100    /// Get the steps in this path.
101    #[must_use]
102    pub fn steps(&self) -> &[OperationStep] {
103        &self.0
104    }
105
106    /// Check if this path is empty (root).
107    #[must_use]
108    pub fn is_empty(&self) -> bool {
109        self.0.is_empty()
110    }
111
112    /// Get the length of the path.
113    #[must_use]
114    pub fn len(&self) -> usize {
115        self.0.len()
116    }
117}
118
119impl fmt::Display for OperationPath {
120    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121        if self.0.is_empty() {
122            write!(f, "<root>")
123        } else {
124            let parts: Vec<String> = self.0.iter().map(|s| s.to_string()).collect();
125            write!(f, "{}", parts.join("/"))
126        }
127    }
128}
129
130/// Execution hints for a single operation.
131///
132/// These hints control how code is generated for broadcast/collect operations.
133#[derive(Debug, Clone, Default)]
134pub struct OperationHints {
135    /// Execute broadcast/collect operations in parallel using `join_all`.
136    pub parallel: bool,
137
138    /// Minimum number of responses required for collect operations.
139    /// If None, all responses are required.
140    pub min_responses: Option<u32>,
141
142    /// Preserve message ordering (relevant for parallel operations).
143    /// When true, results are returned in the order roles were resolved.
144    pub ordered: bool,
145}
146
147impl OperationHints {
148    /// Create hints for parallel execution.
149    #[must_use]
150    pub fn parallel() -> Self {
151        Self {
152            parallel: true,
153            ..Default::default()
154        }
155    }
156
157    /// Create hints with minimum responses requirement.
158    #[must_use]
159    pub fn with_min_responses(min: u32) -> Self {
160        Self {
161            min_responses: Some(min),
162            ..Default::default()
163        }
164    }
165
166    /// Create hints for ordered parallel execution.
167    #[must_use]
168    pub fn parallel_ordered() -> Self {
169        Self {
170            parallel: true,
171            ordered: true,
172            ..Default::default()
173        }
174    }
175
176    /// Builder: enable parallel execution.
177    #[must_use]
178    pub fn with_parallel(mut self) -> Self {
179        self.parallel = true;
180        self
181    }
182
183    /// Builder: disable parallel execution (sequential).
184    #[must_use]
185    pub fn sequential(mut self) -> Self {
186        self.parallel = false;
187        self
188    }
189
190    /// Builder: set minimum responses.
191    #[must_use]
192    pub fn set_min_responses(mut self, min: Option<u32>) -> Self {
193        self.min_responses = min;
194        self
195    }
196
197    /// Builder: enable ordered execution.
198    #[must_use]
199    pub fn with_ordered(mut self) -> Self {
200        self.ordered = true;
201        self
202    }
203
204    /// Builder: disable ordered execution.
205    #[must_use]
206    pub fn unordered(mut self) -> Self {
207        self.ordered = false;
208        self
209    }
210
211    /// Merge with another hints, taking non-default values.
212    #[must_use]
213    pub fn merge(&self, other: &Self) -> Self {
214        Self {
215            parallel: self.parallel || other.parallel,
216            min_responses: self.min_responses.or(other.min_responses),
217            ordered: self.ordered || other.ordered,
218        }
219    }
220}
221
222/// Collection of execution hints for a protocol.
223///
224/// Maps operation paths to their execution hints. Operations without
225/// explicit hints use default behavior (sequential, all responses required).
226#[derive(Debug, Clone, Default)]
227pub struct ExecutionHints {
228    /// Hints keyed by operation path.
229    hints: HashMap<OperationPath, OperationHints>,
230
231    /// Role name this hints collection is for (after projection).
232    role: Option<String>,
233}
234
235impl ExecutionHints {
236    /// Create an empty hints collection.
237    #[must_use]
238    pub fn new() -> Self {
239        Self::default()
240    }
241
242    /// Create hints for a specific role.
243    #[must_use]
244    pub fn for_role(role: impl Into<String>) -> Self {
245        Self {
246            hints: HashMap::new(),
247            role: Some(role.into()),
248        }
249    }
250
251    /// Get the role this hints collection is for.
252    #[must_use]
253    pub fn role(&self) -> Option<&str> {
254        self.role.as_deref()
255    }
256
257    /// Insert hints for an operation path.
258    pub fn insert(&mut self, path: OperationPath, hints: OperationHints) {
259        self.hints.insert(path, hints);
260    }
261
262    /// Get hints for an operation path.
263    #[must_use]
264    pub fn get(&self, path: &OperationPath) -> Option<&OperationHints> {
265        self.hints.get(path)
266    }
267
268    /// Check if an operation should use parallel execution.
269    #[must_use]
270    pub fn is_parallel(&self, path: &OperationPath) -> bool {
271        self.get(path).map(|h| h.parallel).unwrap_or(false)
272    }
273
274    /// Get the minimum responses requirement for an operation.
275    #[must_use]
276    pub fn min_responses(&self, path: &OperationPath) -> Option<u32> {
277        self.get(path).and_then(|h| h.min_responses)
278    }
279
280    /// Check if an operation should preserve ordering.
281    #[must_use]
282    pub fn is_ordered(&self, path: &OperationPath) -> bool {
283        self.get(path).map(|h| h.ordered).unwrap_or(false)
284    }
285
286    /// Check if there are any hints.
287    #[must_use]
288    pub fn is_empty(&self) -> bool {
289        self.hints.is_empty()
290    }
291
292    /// Get the number of hints.
293    #[must_use]
294    pub fn len(&self) -> usize {
295        self.hints.len()
296    }
297
298    /// Iterate over all hints.
299    pub fn iter(&self) -> impl Iterator<Item = (&OperationPath, &OperationHints)> {
300        self.hints.iter()
301    }
302
303    /// Merge with another hints collection.
304    #[must_use]
305    pub fn merge(&self, other: &Self) -> Self {
306        let mut merged = self.clone();
307        for (path, hints) in &other.hints {
308            merged
309                .hints
310                .entry(path.clone())
311                .and_modify(|h| *h = h.merge(hints))
312                .or_insert_with(|| hints.clone());
313        }
314        merged
315    }
316
317    /// Extract execution hints from a Protocol tree.
318    ///
319    /// Walks the protocol and extracts `@parallel`, `@min_responses`, and `@ordered`
320    /// annotations into an ExecutionHints collection.
321    #[must_use]
322    pub fn extract_from_protocol(protocol: &Protocol) -> Self {
323        let mut hints = Self::new();
324        let mut counters = HintExtractionCounters::default();
325        Self::extract_recursive(protocol, &OperationPath::new(), &mut hints, &mut counters);
326        hints
327    }
328
329    /// Recursive helper for hint extraction.
330    fn extract_recursive(
331        protocol: &Protocol,
332        path: &OperationPath,
333        hints: &mut ExecutionHints,
334        counters: &mut HintExtractionCounters,
335    ) {
336        match protocol {
337            Protocol::Begin { continuation, .. }
338            | Protocol::Await { continuation, .. }
339            | Protocol::Resolve { continuation, .. }
340            | Protocol::Invalidate { continuation, .. } => {
341                Self::extract_recursive(continuation, path, hints, counters);
342            }
343            Protocol::Send {
344                annotations,
345                continuation,
346                ..
347            } => {
348                let send_path = path.push(OperationStep::Send(counters.send_count));
349                counters.send_count += 1;
350
351                if let Some(op_hints) = Self::hints_from_annotations(annotations) {
352                    hints.insert(send_path.clone(), op_hints);
353                }
354
355                Self::extract_recursive(continuation, &send_path, hints, counters);
356            }
357
358            Protocol::Broadcast {
359                annotations,
360                continuation,
361                ..
362            } => {
363                let send_path = path.push(OperationStep::Send(counters.send_count));
364                counters.send_count += 1;
365
366                if let Some(op_hints) = Self::hints_from_annotations(annotations) {
367                    hints.insert(send_path.clone(), op_hints);
368                }
369
370                Self::extract_recursive(continuation, &send_path, hints, counters);
371            }
372
373            Protocol::Choice {
374                branches,
375                annotations,
376                ..
377            } => {
378                // Check for annotations on the choice itself
379                if let Some(op_hints) = Self::hints_from_annotations(annotations) {
380                    hints.insert(path.clone(), op_hints);
381                }
382
383                for branch in branches.as_slice() {
384                    let branch_path = path.push(OperationStep::Branch(branch.label.to_string()));
385                    let mut branch_counters = HintExtractionCounters::default();
386                    Self::extract_recursive(
387                        &branch.protocol,
388                        &branch_path,
389                        hints,
390                        &mut branch_counters,
391                    );
392                }
393            }
394            Protocol::Case { branches, .. } => {
395                for branch in branches.as_slice() {
396                    let branch_path =
397                        path.push(OperationStep::Branch(branch.pattern.constructor.clone()));
398                    let mut branch_counters = HintExtractionCounters::default();
399                    Self::extract_recursive(
400                        &branch.protocol,
401                        &branch_path,
402                        hints,
403                        &mut branch_counters,
404                    );
405                }
406            }
407            Protocol::Timeout {
408                body,
409                on_timeout,
410                on_cancel,
411                ..
412            } => {
413                Self::extract_recursive(body, path, hints, counters);
414                let timeout_path = path.push(OperationStep::Branch("timeout".to_string()));
415                let mut timeout_counters = HintExtractionCounters::default();
416                Self::extract_recursive(on_timeout, &timeout_path, hints, &mut timeout_counters);
417                if let Some(on_cancel) = on_cancel.as_deref() {
418                    let cancel_path = path.push(OperationStep::Branch("cancel".to_string()));
419                    let mut cancel_counters = HintExtractionCounters::default();
420                    Self::extract_recursive(on_cancel, &cancel_path, hints, &mut cancel_counters);
421                }
422            }
423
424            Protocol::Loop { body, .. } => {
425                let loop_path = path.push(OperationStep::Loop(counters.loop_count));
426                counters.loop_count += 1;
427                let mut loop_counters = HintExtractionCounters::default();
428                Self::extract_recursive(body, &loop_path, hints, &mut loop_counters);
429            }
430
431            Protocol::Rec { label, body } => {
432                let rec_path = path.push(OperationStep::Rec(label.to_string()));
433                let mut rec_counters = HintExtractionCounters::default();
434                Self::extract_recursive(body, &rec_path, hints, &mut rec_counters);
435            }
436            Protocol::Publish { continuation, .. }
437            | Protocol::PublishAuthority { continuation, .. }
438            | Protocol::Materialize { continuation, .. }
439            | Protocol::Handoff { continuation, .. }
440            | Protocol::DependentWork { continuation, .. } => {
441                Self::extract_recursive(continuation, path, hints, counters);
442            }
443
444            Protocol::Parallel { protocols } => {
445                for (i, proto) in protocols.as_slice().iter().enumerate() {
446                    let parallel_path = path.push(OperationStep::Loop(i)); // Reuse Loop for parallel branches
447                    let mut parallel_counters = HintExtractionCounters::default();
448                    Self::extract_recursive(proto, &parallel_path, hints, &mut parallel_counters);
449                }
450            }
451
452            Protocol::Extension {
453                annotations,
454                continuation,
455                ..
456            } => {
457                if let Some(op_hints) = Self::hints_from_annotations(annotations) {
458                    hints.insert(path.clone(), op_hints);
459                }
460                Self::extract_recursive(continuation, path, hints, counters);
461            }
462            Protocol::Let { continuation, .. } => {
463                Self::extract_recursive(continuation, path, hints, counters);
464            }
465
466            Protocol::Var(_) | Protocol::End => {
467                // No hints to extract from terminal nodes
468            }
469        }
470    }
471
472    /// Extract OperationHints from Annotations if any relevant ones are present.
473    fn hints_from_annotations(annotations: &Annotations) -> Option<OperationHints> {
474        let parallel = annotations.has_parallel();
475        let ordered = annotations.has_ordered();
476        let min_responses = annotations.min_responses();
477
478        if parallel || ordered || min_responses.is_some() {
479            Some(OperationHints {
480                parallel,
481                ordered,
482                min_responses,
483            })
484        } else {
485            None
486        }
487    }
488}
489
490/// Counters for tracking position during hint extraction.
491#[derive(Default)]
492struct HintExtractionCounters {
493    send_count: usize,
494    loop_count: usize,
495}
496
497/// A choreography paired with its extracted execution hints.
498///
499/// This struct holds a choreography and the execution hints extracted from
500/// its annotations. The hints are separate from the choreography to maintain
501/// clean separation between protocol semantics and deployment configuration.
502#[derive(Debug)]
503pub struct ChoreographyWithHints {
504    /// The parsed choreography
505    pub choreography: Choreography,
506    /// Execution hints extracted from annotations
507    pub hints: ExecutionHints,
508}
509
510impl ChoreographyWithHints {
511    /// Create a new choreography with hints from a choreography.
512    ///
513    /// Extracts execution hints from the choreography's protocol annotations.
514    #[must_use]
515    pub fn from_choreography(choreography: Choreography) -> Self {
516        let hints = ExecutionHints::extract_from_protocol(&choreography.protocol);
517        Self {
518            choreography,
519            hints,
520        }
521    }
522
523    /// Create from a choreography with pre-computed hints.
524    #[must_use]
525    pub fn new(choreography: Choreography, hints: ExecutionHints) -> Self {
526        Self {
527            choreography,
528            hints,
529        }
530    }
531}
532
533/// Builder for constructing ExecutionHints.
534#[derive(Debug, Default)]
535pub struct ExecutionHintsBuilder {
536    hints: ExecutionHints,
537    current_path: OperationPath,
538}
539
540impl ExecutionHintsBuilder {
541    /// Create a new builder.
542    #[must_use]
543    pub fn new() -> Self {
544        Self::default()
545    }
546
547    /// Create a builder for a specific role.
548    #[must_use]
549    pub fn for_role(role: impl Into<String>) -> Self {
550        Self {
551            hints: ExecutionHints::for_role(role),
552            current_path: OperationPath::new(),
553        }
554    }
555
556    /// Set the current path context.
557    #[must_use]
558    pub fn at_path(mut self, path: OperationPath) -> Self {
559        self.current_path = path;
560        self
561    }
562
563    /// Add a parallel hint at the current path.
564    #[must_use]
565    pub fn parallel(mut self) -> Self {
566        let hints = self
567            .hints
568            .hints
569            .entry(self.current_path.clone())
570            .or_default();
571        hints.parallel = true;
572        self
573    }
574
575    /// Add a min_responses hint at the current path.
576    #[must_use]
577    pub fn min_responses(mut self, min: u32) -> Self {
578        let hints = self
579            .hints
580            .hints
581            .entry(self.current_path.clone())
582            .or_default();
583        hints.min_responses = Some(min);
584        self
585    }
586
587    /// Add an ordered hint at the current path.
588    #[must_use]
589    pub fn ordered(mut self) -> Self {
590        let hints = self
591            .hints
592            .hints
593            .entry(self.current_path.clone())
594            .or_default();
595        hints.ordered = true;
596        self
597    }
598
599    /// Add hints at a specific path.
600    #[must_use]
601    pub fn with_hints(mut self, path: OperationPath, hints: OperationHints) -> Self {
602        self.hints.insert(path, hints);
603        self
604    }
605
606    /// Build the ExecutionHints.
607    #[must_use]
608    pub fn build(self) -> ExecutionHints {
609        self.hints
610    }
611}
612
613#[cfg(test)]
614mod tests {
615    use super::*;
616
617    #[test]
618    fn test_operation_path_display() {
619        let path = OperationPath::new();
620        assert_eq!(path.to_string(), "<root>");
621
622        let path = path.push(OperationStep::Send(0));
623        assert_eq!(path.to_string(), "send:0");
624
625        let path = path.push(OperationStep::Branch("Accept".to_string()));
626        assert_eq!(path.to_string(), "send:0/branch:Accept");
627
628        let path = path.push(OperationStep::Recv(1));
629        assert_eq!(path.to_string(), "send:0/branch:Accept/recv:1");
630    }
631
632    #[test]
633    fn test_execution_hints_basic() {
634        let mut hints = ExecutionHints::new();
635        let path = OperationPath::from_steps(vec![OperationStep::Send(0)]);
636
637        hints.insert(path.clone(), OperationHints::parallel());
638
639        assert!(hints.is_parallel(&path));
640        assert!(!hints.is_ordered(&path));
641        assert_eq!(hints.min_responses(&path), None);
642    }
643
644    #[test]
645    fn test_execution_hints_min_responses() {
646        let mut hints = ExecutionHints::new();
647        let path = OperationPath::from_steps(vec![OperationStep::Recv(0)]);
648
649        hints.insert(
650            path.clone(),
651            OperationHints::with_min_responses(3).with_parallel(),
652        );
653
654        assert!(hints.is_parallel(&path));
655        assert_eq!(hints.min_responses(&path), Some(3));
656    }
657
658    #[test]
659    fn test_execution_hints_builder() {
660        let hints = ExecutionHintsBuilder::for_role("Coordinator")
661            .at_path(OperationPath::from_steps(vec![OperationStep::Send(0)]))
662            .parallel()
663            .at_path(OperationPath::from_steps(vec![OperationStep::Recv(0)]))
664            .min_responses(3)
665            .ordered()
666            .build();
667
668        let send_path = OperationPath::from_steps(vec![OperationStep::Send(0)]);
669        let recv_path = OperationPath::from_steps(vec![OperationStep::Recv(0)]);
670
671        assert!(hints.is_parallel(&send_path));
672        assert!(!hints.is_ordered(&send_path));
673
674        assert!(!hints.is_parallel(&recv_path));
675        assert!(hints.is_ordered(&recv_path));
676        assert_eq!(hints.min_responses(&recv_path), Some(3));
677    }
678
679    #[test]
680    fn test_operation_hints_merge() {
681        let h1 = OperationHints {
682            parallel: true,
683            min_responses: None,
684            ordered: false,
685        };
686        let h2 = OperationHints {
687            parallel: false,
688            min_responses: Some(3),
689            ordered: true,
690        };
691
692        let merged = h1.merge(&h2);
693        assert!(merged.parallel); // true || false
694        assert_eq!(merged.min_responses, Some(3)); // None.or(Some(3))
695        assert!(merged.ordered); // false || true
696    }
697
698    #[test]
699    fn test_execution_hints_default_values() {
700        let hints = ExecutionHints::new();
701        let path = OperationPath::from_steps(vec![OperationStep::Send(0)]);
702
703        // Default: not parallel, no min_responses, not ordered
704        assert!(!hints.is_parallel(&path));
705        assert_eq!(hints.min_responses(&path), None);
706        assert!(!hints.is_ordered(&path));
707    }
708
709    #[test]
710    fn test_extract_from_protocol_with_parallel() {
711        use crate::ast::annotation::Annotations;
712        use crate::ast::role::Role;
713        use crate::ast::MessageType;
714        use proc_macro2::Ident;
715        use proc_macro2::Span;
716
717        // Create a protocol: @parallel A -> B : Msg
718        let mut annotations = Annotations::new();
719        annotations.push(crate::ast::ProtocolAnnotation::Parallel);
720
721        let protocol = Protocol::Send {
722            from: Role::new(Ident::new("A", Span::call_site())).unwrap(),
723            to: Role::new(Ident::new("B", Span::call_site())).unwrap(),
724            message: MessageType {
725                name: Ident::new("Msg", Span::call_site()),
726                type_annotation: None,
727                payload: None,
728            },
729            continuation: Box::new(Protocol::End),
730            annotations,
731            from_annotations: Annotations::new(),
732            to_annotations: Annotations::new(),
733        };
734
735        let hints = ExecutionHints::extract_from_protocol(&protocol);
736        let path = OperationPath::from_steps(vec![OperationStep::Send(0)]);
737
738        assert!(hints.is_parallel(&path));
739        assert!(!hints.is_ordered(&path));
740        assert_eq!(hints.min_responses(&path), None);
741    }
742
743    #[test]
744    fn test_extract_from_protocol_with_min_responses() {
745        use crate::ast::annotation::Annotations;
746        use crate::ast::role::Role;
747        use crate::ast::MessageType;
748        use proc_macro2::Ident;
749        use proc_macro2::Span;
750
751        // Create a protocol: @min_responses(3) A -> B : Msg
752        let mut annotations = Annotations::new();
753        annotations.push(crate::ast::ProtocolAnnotation::MinResponses(3));
754
755        let protocol = Protocol::Send {
756            from: Role::new(Ident::new("A", Span::call_site())).unwrap(),
757            to: Role::new(Ident::new("B", Span::call_site())).unwrap(),
758            message: MessageType {
759                name: Ident::new("Msg", Span::call_site()),
760                type_annotation: None,
761                payload: None,
762            },
763            continuation: Box::new(Protocol::End),
764            annotations,
765            from_annotations: Annotations::new(),
766            to_annotations: Annotations::new(),
767        };
768
769        let hints = ExecutionHints::extract_from_protocol(&protocol);
770        let path = OperationPath::from_steps(vec![OperationStep::Send(0)]);
771
772        assert!(!hints.is_parallel(&path));
773        assert_eq!(hints.min_responses(&path), Some(3));
774    }
775
776    #[test]
777    fn test_extract_from_protocol_combined() {
778        use crate::ast::annotation::Annotations;
779        use crate::ast::role::Role;
780        use crate::ast::MessageType;
781        use proc_macro2::Ident;
782        use proc_macro2::Span;
783
784        // Create a protocol: @parallel @ordered @min_responses(2) A -> B : Msg
785        let mut annotations = Annotations::new();
786        annotations.push(crate::ast::ProtocolAnnotation::Parallel);
787        annotations.push(crate::ast::ProtocolAnnotation::Ordered);
788        annotations.push(crate::ast::ProtocolAnnotation::MinResponses(2));
789
790        let protocol = Protocol::Send {
791            from: Role::new(Ident::new("A", Span::call_site())).unwrap(),
792            to: Role::new(Ident::new("B", Span::call_site())).unwrap(),
793            message: MessageType {
794                name: Ident::new("Msg", Span::call_site()),
795                type_annotation: None,
796                payload: None,
797            },
798            continuation: Box::new(Protocol::End),
799            annotations,
800            from_annotations: Annotations::new(),
801            to_annotations: Annotations::new(),
802        };
803
804        let hints = ExecutionHints::extract_from_protocol(&protocol);
805        let path = OperationPath::from_steps(vec![OperationStep::Send(0)]);
806
807        assert!(hints.is_parallel(&path));
808        assert!(hints.is_ordered(&path));
809        assert_eq!(hints.min_responses(&path), Some(2));
810    }
811
812    #[test]
813    fn test_extract_no_hints_when_no_annotations() {
814        use crate::ast::annotation::Annotations;
815        use crate::ast::role::Role;
816        use crate::ast::MessageType;
817        use proc_macro2::Ident;
818        use proc_macro2::Span;
819
820        // Create a protocol with no execution annotations: A -> B : Msg
821        let protocol = Protocol::Send {
822            from: Role::new(Ident::new("A", Span::call_site())).unwrap(),
823            to: Role::new(Ident::new("B", Span::call_site())).unwrap(),
824            message: MessageType {
825                name: Ident::new("Msg", Span::call_site()),
826                type_annotation: None,
827                payload: None,
828            },
829            continuation: Box::new(Protocol::End),
830            annotations: Annotations::new(),
831            from_annotations: Annotations::new(),
832            to_annotations: Annotations::new(),
833        };
834
835        let hints = ExecutionHints::extract_from_protocol(&protocol);
836
837        // Should have no hints
838        assert!(hints.is_empty());
839    }
840}