Skip to main content

telltale_runtime/compiler/choice_analysis/
analyzer.rs

1//! Choice propagation analyzer.
2//!
3//! Implements the core analysis logic for verifying that all roles
4//! learn which branch was selected in choice blocks.
5
6use crate::ast::{Branch, Protocol, Role};
7use crate::compiler::diagnostics::{Diagnostic, DiagnosticCode, DiagnosticCollector};
8use std::collections::{HashMap, HashSet};
9
10use super::types::{
11    BranchMessages, ChoiceAnalysisResult, ChoiceId, ChoiceKnowledge, KnowledgeSource, MessageInfo,
12};
13
14/// Analyzer for choice propagation
15#[derive(Debug)]
16pub struct ChoiceAnalyzer {
17    /// All declared roles in the choreography
18    roles: Vec<String>,
19    /// Diagnostic collector
20    diagnostics: DiagnosticCollector,
21    /// Choice index counter (per chooser)
22    choice_counts: HashMap<String, usize>,
23}
24
25impl ChoiceAnalyzer {
26    /// Create a new choice analyzer
27    pub fn new(roles: &[Role]) -> Self {
28        Self {
29            roles: roles.iter().map(|r| r.name().to_string()).collect(),
30            diagnostics: DiagnosticCollector::new(),
31            choice_counts: HashMap::new(),
32        }
33    }
34
35    /// Analyze choice propagation in a protocol
36    pub fn analyze(&mut self, protocol: &Protocol) -> ChoiceAnalysisResult {
37        let mut choices = Vec::new();
38        self.analyze_protocol(protocol, &mut choices, None);
39
40        // Generate diagnostics for uninformed roles
41        for choice in &choices {
42            for uninformed in &choice.uninformed_roles {
43                self.emit_uninformed_role_error(choice, uninformed);
44            }
45        }
46
47        ChoiceAnalysisResult {
48            choices,
49            diagnostics: self.diagnostics.take_diagnostics(),
50        }
51    }
52
53    /// Recursively analyze a protocol for choice points
54    fn analyze_protocol(
55        &mut self,
56        protocol: &Protocol,
57        choices: &mut Vec<ChoiceKnowledge>,
58        parent_choice: Option<&ChoiceId>,
59    ) {
60        match protocol {
61            Protocol::Choice { role, branches, .. } => {
62                let choice_knowledge = self.analyze_choice(role, branches, parent_choice);
63                let choice_id = choice_knowledge.choice_id.clone();
64                choices.push(choice_knowledge);
65
66                // Recursively analyze branch protocols
67                for branch in branches {
68                    self.analyze_protocol(&branch.protocol, choices, Some(&choice_id));
69                }
70            }
71            Protocol::Case { branches, .. } => {
72                for branch in branches {
73                    self.analyze_protocol(&branch.protocol, choices, parent_choice);
74                }
75            }
76            Protocol::Timeout {
77                body,
78                on_timeout,
79                on_cancel,
80                ..
81            } => {
82                self.analyze_protocol(body, choices, parent_choice);
83                self.analyze_protocol(on_timeout, choices, parent_choice);
84                if let Some(on_cancel) = on_cancel.as_deref() {
85                    self.analyze_protocol(on_cancel, choices, parent_choice);
86                }
87            }
88            Protocol::Send { continuation, .. } => {
89                self.analyze_protocol(continuation, choices, parent_choice);
90            }
91            Protocol::Broadcast { continuation, .. } => {
92                self.analyze_protocol(continuation, choices, parent_choice);
93            }
94            Protocol::Loop { body, .. } => {
95                self.analyze_protocol(body, choices, parent_choice);
96            }
97            Protocol::Parallel { protocols } => {
98                for p in protocols {
99                    self.analyze_protocol(p, choices, parent_choice);
100                }
101            }
102            Protocol::Rec { body, .. } => {
103                self.analyze_protocol(body, choices, parent_choice);
104            }
105            Protocol::Begin { continuation, .. }
106            | Protocol::Await { continuation, .. }
107            | Protocol::Resolve { continuation, .. }
108            | Protocol::Invalidate { continuation, .. }
109            | Protocol::Extension { continuation, .. }
110            | Protocol::Let { continuation, .. }
111            | Protocol::Publish { continuation, .. }
112            | Protocol::PublishAuthority { continuation, .. }
113            | Protocol::Materialize { continuation, .. }
114            | Protocol::Handoff { continuation, .. }
115            | Protocol::DependentWork { continuation, .. } => {
116                self.analyze_protocol(continuation, choices, parent_choice);
117            }
118            Protocol::Var(_) | Protocol::End => {}
119        }
120    }
121
122    /// Analyze a single choice point
123    fn analyze_choice(
124        &mut self,
125        chooser: &Role,
126        branches: &[Branch],
127        parent_choice: Option<&ChoiceId>,
128    ) -> ChoiceKnowledge {
129        let chooser_name = chooser.name().to_string();
130
131        // Get unique choice ID
132        let index = *self.choice_counts.get(&chooser_name).unwrap_or(&0);
133        self.choice_counts.insert(chooser_name.clone(), index + 1);
134
135        let choice_id = match parent_choice {
136            Some(parent) => ChoiceId::nested(parent, &chooser_name, index),
137            None => ChoiceId::new(&chooser_name, index),
138        };
139
140        // Collect branch labels
141        let branch_labels: Vec<String> = branches.iter().map(|b| b.label.to_string()).collect();
142
143        // Initialize knowledge: chooser always knows
144        let mut informed_roles: HashMap<String, KnowledgeSource> = HashMap::new();
145        informed_roles.insert(chooser_name.clone(), KnowledgeSource::Chooser);
146
147        // Analyze each branch to find message patterns
148        let branch_messages = self.collect_branch_messages(branches);
149
150        // Find roles that receive messages in all branches
151        let roles_receiving_in_all = self.find_roles_receiving_in_all_branches(&branch_messages);
152
153        // For each such role, they learn the choice
154        for (role, messages_per_branch) in roles_receiving_in_all {
155            if !informed_roles.contains_key(&role) {
156                // Find the sender (typically the chooser or someone informed)
157                if let Some(sender) = self.find_message_sender(&branch_messages, &role) {
158                    informed_roles.insert(
159                        role.clone(),
160                        KnowledgeSource::MessageFrom {
161                            sender,
162                            messages: messages_per_branch,
163                        },
164                    );
165                }
166            }
167        }
168
169        // Propagate knowledge transitively through remaining messages
170        self.propagate_knowledge(&mut informed_roles, &branch_messages, &branch_labels);
171
172        // Find roles that participate but are uninformed
173        let participating_roles = self.find_participating_roles(branches);
174        let uninformed_roles: HashSet<String> = participating_roles
175            .into_iter()
176            .filter(|r| !informed_roles.contains_key(r))
177            .collect();
178
179        // Build branch participation map
180        let branch_participation = self.build_branch_participation(&branch_messages);
181
182        ChoiceKnowledge {
183            choice_id,
184            branches: branch_labels,
185            informed_roles,
186            uninformed_roles,
187            location: None, // Could be populated from span info
188            branch_participation,
189        }
190    }
191
192    /// Build a map of which roles participate in each branch
193    fn build_branch_participation(
194        &self,
195        branch_messages: &[BranchMessages],
196    ) -> HashMap<String, HashSet<String>> {
197        branch_messages
198            .iter()
199            .map(|branch| {
200                let mut roles = HashSet::new();
201                for msg in &branch.messages {
202                    roles.insert(msg.from.clone());
203                    roles.insert(msg.to.clone());
204                }
205                (branch.label.clone(), roles)
206            })
207            .collect()
208    }
209
210    /// Collect message patterns from each branch
211    fn collect_branch_messages(&self, branches: &[Branch]) -> Vec<BranchMessages> {
212        branches
213            .iter()
214            .map(|branch| {
215                let mut messages = Vec::new();
216                self.collect_messages_from_protocol(&branch.protocol, &mut messages);
217                BranchMessages {
218                    label: branch.label.to_string(),
219                    messages,
220                }
221            })
222            .collect()
223    }
224
225    /// Recursively collect messages from a protocol
226    #[allow(clippy::only_used_in_recursion)]
227    fn collect_messages_from_protocol(&self, protocol: &Protocol, messages: &mut Vec<MessageInfo>) {
228        match protocol {
229            Protocol::Send {
230                from,
231                to,
232                message,
233                continuation,
234                ..
235            } => {
236                self.push_send_message(messages, from, to, &message.name.to_string());
237                self.collect_messages_from_protocol(continuation, messages);
238            }
239            Protocol::Broadcast {
240                from,
241                to_all,
242                message,
243                continuation,
244                ..
245            } => {
246                self.push_broadcast_messages(messages, from, to_all, &message.name.to_string());
247                self.collect_messages_from_protocol(continuation, messages);
248            }
249            Protocol::Choice { branches, .. } => {
250                self.collect_nested_choice_messages(branches, messages);
251            }
252            Protocol::Case { branches, .. } => {
253                for branch in branches {
254                    self.collect_messages_from_protocol(&branch.protocol, messages);
255                }
256            }
257            Protocol::Timeout {
258                body,
259                on_timeout,
260                on_cancel,
261                ..
262            } => {
263                self.collect_messages_from_protocol(body, messages);
264                self.collect_messages_from_protocol(on_timeout, messages);
265                if let Some(on_cancel) = on_cancel.as_deref() {
266                    self.collect_messages_from_protocol(on_cancel, messages);
267                }
268            }
269            Protocol::Loop { body, .. } => {
270                self.collect_messages_from_protocol(body, messages);
271            }
272            Protocol::Parallel { protocols } => {
273                for p in protocols {
274                    self.collect_messages_from_protocol(p, messages);
275                }
276            }
277            Protocol::Rec { body, .. } => {
278                self.collect_messages_from_protocol(body, messages);
279            }
280            Protocol::Begin { continuation, .. }
281            | Protocol::Await { continuation, .. }
282            | Protocol::Resolve { continuation, .. }
283            | Protocol::Invalidate { continuation, .. }
284            | Protocol::Extension { continuation, .. }
285            | Protocol::Let { continuation, .. }
286            | Protocol::Publish { continuation, .. }
287            | Protocol::PublishAuthority { continuation, .. }
288            | Protocol::Materialize { continuation, .. }
289            | Protocol::Handoff { continuation, .. }
290            | Protocol::DependentWork { continuation, .. } => {
291                self.collect_messages_from_protocol(continuation, messages);
292            }
293            Protocol::Var(_) | Protocol::End => {}
294        }
295    }
296
297    fn push_send_message(
298        &self,
299        messages: &mut Vec<MessageInfo>,
300        from: &Role,
301        to: &Role,
302        message_type: &str,
303    ) {
304        messages.push(MessageInfo {
305            from: from.name().to_string(),
306            to: to.name().to_string(),
307            message_type: message_type.to_string(),
308        });
309    }
310
311    fn push_broadcast_messages(
312        &self,
313        messages: &mut Vec<MessageInfo>,
314        from: &Role,
315        to_all: &crate::ast::NonEmptyVec<Role>,
316        message_type: &str,
317    ) {
318        for to in to_all {
319            self.push_send_message(messages, from, to, message_type);
320        }
321    }
322
323    fn collect_nested_choice_messages(
324        &self,
325        branches: &crate::ast::NonEmptyVec<Branch>,
326        messages: &mut Vec<MessageInfo>,
327    ) {
328        for branch in branches {
329            if let Protocol::Send {
330                from, to, message, ..
331            } = &branch.protocol
332            {
333                self.push_send_message(messages, from, to, &message.name.to_string());
334            }
335        }
336    }
337
338    /// Find roles that receive messages in ALL branches
339    fn find_roles_receiving_in_all_branches(
340        &self,
341        branch_messages: &[BranchMessages],
342    ) -> HashMap<String, Vec<String>> {
343        if branch_messages.is_empty() {
344            return HashMap::new();
345        }
346
347        // For each role, collect the message types they receive in each branch
348        let mut role_messages: HashMap<String, Vec<Option<String>>> = HashMap::new();
349
350        for (branch_idx, branch) in branch_messages.iter().enumerate() {
351            // Find first message to each role in this branch
352            let mut seen_roles: HashSet<String> = HashSet::new();
353            for msg in &branch.messages {
354                if !seen_roles.contains(&msg.to) {
355                    seen_roles.insert(msg.to.clone());
356                    role_messages
357                        .entry(msg.to.clone())
358                        .or_insert_with(|| vec![None; branch_messages.len()])[branch_idx] =
359                        Some(msg.message_type.clone());
360                }
361            }
362        }
363
364        // Filter to roles that receive in ALL branches
365        role_messages
366            .into_iter()
367            .filter_map(|(role, messages)| {
368                if messages.iter().all(|m| m.is_some()) {
369                    let msg_types: Vec<String> = messages.into_iter().map(|m| m.unwrap()).collect();
370                    Some((role, msg_types))
371                } else {
372                    None
373                }
374            })
375            .collect()
376    }
377
378    /// Find who sends to a role in a branch
379    fn find_message_sender(
380        &self,
381        branch_messages: &[BranchMessages],
382        receiver: &str,
383    ) -> Option<String> {
384        for branch in branch_messages {
385            for msg in &branch.messages {
386                if msg.to == receiver {
387                    return Some(msg.from.clone());
388                }
389            }
390        }
391        None
392    }
393
394    /// Propagate knowledge transitively through message chains
395    ///
396    /// A role only becomes informed if they receive from an informed sender
397    /// in ALL branches (not just some).
398    fn propagate_knowledge(
399        &self,
400        informed: &mut HashMap<String, KnowledgeSource>,
401        branch_messages: &[BranchMessages],
402        _branch_labels: &[String],
403    ) {
404        if branch_messages.is_empty() {
405            return;
406        }
407
408        // Fixed-point iteration: keep propagating until no changes
409        let mut changed = true;
410        while changed {
411            changed = false;
412
413            // Find roles that receive from an informed sender in ALL branches
414            // First, collect senders per receiver per branch
415            let mut receiver_senders_per_branch: HashMap<String, Vec<Option<String>>> =
416                HashMap::new();
417
418            for (branch_idx, branch) in branch_messages.iter().enumerate() {
419                // Track first informed sender to each role in this branch
420                let mut seen_receivers: HashSet<String> = HashSet::new();
421                for msg in &branch.messages {
422                    if informed.contains_key(&msg.from) && !seen_receivers.contains(&msg.to) {
423                        seen_receivers.insert(msg.to.clone());
424                        receiver_senders_per_branch
425                            .entry(msg.to.clone())
426                            .or_insert_with(|| vec![None; branch_messages.len()])[branch_idx] =
427                            Some(msg.from.clone());
428                    }
429                }
430            }
431
432            // Only mark roles as informed if they receive in ALL branches
433            for (receiver, senders) in &receiver_senders_per_branch {
434                if !informed.contains_key(receiver) && senders.iter().all(|s| s.is_some()) {
435                    // Find a consistent sender (use first branch's sender)
436                    if let Some(sender) = &senders[0] {
437                        let source = informed.get(sender).unwrap().clone();
438                        informed.insert(
439                            receiver.clone(),
440                            KnowledgeSource::Transitive {
441                                via: sender.clone(),
442                                original_source: Box::new(source),
443                            },
444                        );
445                        changed = true;
446                    }
447                }
448            }
449        }
450    }
451
452    /// Find all roles that participate in any branch
453    fn find_participating_roles(&self, branches: &[Branch]) -> HashSet<String> {
454        let mut roles = HashSet::new();
455        for branch in branches {
456            self.collect_roles_from_protocol(&branch.protocol, &mut roles);
457        }
458        roles
459    }
460
461    /// Recursively collect roles from a protocol
462    #[allow(clippy::only_used_in_recursion)]
463    fn collect_roles_from_protocol(&self, protocol: &Protocol, roles: &mut HashSet<String>) {
464        match protocol {
465            Protocol::Send {
466                from,
467                to,
468                continuation,
469                ..
470            } => {
471                roles.insert(from.name().to_string());
472                roles.insert(to.name().to_string());
473                self.collect_roles_from_protocol(continuation, roles);
474            }
475            Protocol::Broadcast {
476                from,
477                to_all,
478                continuation,
479                ..
480            } => {
481                roles.insert(from.name().to_string());
482                for to in to_all {
483                    roles.insert(to.name().to_string());
484                }
485                self.collect_roles_from_protocol(continuation, roles);
486            }
487            Protocol::Choice { role, branches, .. } => {
488                roles.insert(role.name().to_string());
489                for branch in branches {
490                    self.collect_roles_from_protocol(&branch.protocol, roles);
491                }
492            }
493            Protocol::Case { branches, .. } => {
494                for branch in branches {
495                    self.collect_roles_from_protocol(&branch.protocol, roles);
496                }
497            }
498            Protocol::Timeout {
499                role,
500                body,
501                on_timeout,
502                on_cancel,
503                ..
504            } => {
505                roles.insert(role.name().to_string());
506                self.collect_roles_from_protocol(body, roles);
507                self.collect_roles_from_protocol(on_timeout, roles);
508                if let Some(on_cancel) = on_cancel.as_deref() {
509                    self.collect_roles_from_protocol(on_cancel, roles);
510                }
511            }
512            Protocol::Loop { body, .. } => {
513                self.collect_roles_from_protocol(body, roles);
514            }
515            Protocol::Parallel { protocols } => {
516                for p in protocols {
517                    self.collect_roles_from_protocol(p, roles);
518                }
519            }
520            Protocol::Rec { body, .. } => {
521                self.collect_roles_from_protocol(body, roles);
522            }
523            Protocol::Begin { continuation, .. }
524            | Protocol::Await { continuation, .. }
525            | Protocol::Resolve { continuation, .. }
526            | Protocol::Invalidate { continuation, .. }
527            | Protocol::Extension { continuation, .. }
528            | Protocol::Let { continuation, .. }
529            | Protocol::Publish { continuation, .. }
530            | Protocol::PublishAuthority { continuation, .. }
531            | Protocol::Materialize { continuation, .. }
532            | Protocol::Handoff { continuation, .. }
533            | Protocol::DependentWork { continuation, .. } => {
534                self.collect_roles_from_protocol(continuation, roles);
535            }
536            Protocol::Var(_) | Protocol::End => {}
537        }
538    }
539
540    /// Emit error for uninformed role
541    fn emit_uninformed_role_error(&mut self, choice: &ChoiceKnowledge, role: &str) {
542        let mut diagnostic = Diagnostic::error(
543            DiagnosticCode::ChoicePropagationError,
544            format!(
545                "Role '{}' cannot determine which branch was selected in {}",
546                role,
547                choice.choice_id.display()
548            ),
549        );
550
551        // Add helpful suggestion
552        diagnostic.suggestions.push(format!(
553            "Add a message from '{}' to '{}' in each branch to inform about the choice",
554            choice.choice_id.chooser, role
555        ));
556
557        self.add_branch_participation_notes(&mut diagnostic, choice, role);
558        self.add_informed_roles_note(&mut diagnostic, choice);
559        self.add_typo_suggestion(&mut diagnostic, role);
560        diagnostic
561            .notes
562            .push(format!("Choice branches: {:?}", choice.branches));
563
564        if let Some(ref loc) = choice.location {
565            diagnostic.location = Some(loc.clone());
566        }
567
568        self.diagnostics.add(diagnostic);
569    }
570
571    fn add_branch_participation_notes(
572        &self,
573        diagnostic: &mut Diagnostic,
574        choice: &ChoiceKnowledge,
575        role: &str,
576    ) {
577        let present_branches: Vec<&str> = choice
578            .branch_participation
579            .iter()
580            .filter(|(_label, roles)| roles.contains(role))
581            .map(|(label, _)| label.as_str())
582            .collect();
583        let absent_branches: Vec<&str> = choice
584            .branch_participation
585            .iter()
586            .filter(|(_label, roles)| !roles.contains(role))
587            .map(|(label, _)| label.as_str())
588            .collect();
589
590        if !present_branches.is_empty() && !absent_branches.is_empty() {
591            diagnostic.notes.push(format!(
592                "'{}' participates in branch(es): {} but NOT in: {}",
593                role,
594                present_branches.join(", "),
595                absent_branches.join(", ")
596            ));
597        } else if !present_branches.is_empty() {
598            diagnostic.notes.push(format!(
599                "'{}' participates in: {}",
600                role,
601                present_branches.join(", ")
602            ));
603        }
604    }
605
606    fn add_informed_roles_note(&self, diagnostic: &mut Diagnostic, choice: &ChoiceKnowledge) {
607        if choice.informed_roles.is_empty() {
608            return;
609        }
610        let informed_list: Vec<String> = choice
611            .informed_roles
612            .iter()
613            .map(|(r, src)| format!("{} ({})", r, src.description()))
614            .collect();
615        diagnostic
616            .notes
617            .push(format!("Informed roles: {}", informed_list.join(", ")));
618    }
619
620    fn add_typo_suggestion(&self, diagnostic: &mut Diagnostic, role: &str) {
621        if let Some(suggestion) = self.find_similar_role(role) {
622            diagnostic
623                .suggestions
624                .push(format!("Did you mean '{}'?", suggestion));
625        }
626    }
627
628    /// Find a similar role name (for typo suggestions)
629    pub fn find_similar_role(&self, role: &str) -> Option<&String> {
630        // Use Levenshtein distance to find similar names
631        let threshold = 2; // Max edit distance
632        self.roles
633            .iter()
634            .filter(|r| r.as_str() != role)
635            .min_by_key(|r| super::levenshtein_distance(role, r))
636            .filter(|r| super::levenshtein_distance(role, r) <= threshold)
637    }
638
639    /// Get all declared roles
640    #[must_use]
641    pub fn declared_roles(&self) -> &[String] {
642        &self.roles
643    }
644
645    /// Check if a role is declared
646    #[must_use]
647    pub fn is_role_declared(&self, role: &str) -> bool {
648        self.roles.iter().any(|r| r == role)
649    }
650}