Skip to main content

telltale_language/compiler/
projection.rs

1// Projection from global choreographies to local session types
2
3mod merge;
4mod ops;
5
6use crate::ast::{Branch, Choreography, LocalType, MessageType, Protocol, Role, RoleParam};
7use proc_macro2::{Ident, Span};
8use std::collections::HashMap;
9use std::time::Duration;
10
11pub use merge::merge_local_types;
12
13/// Project a choreography to a local session type for a specific role
14pub fn project(choreography: &Choreography, role: &Role) -> Result<LocalType, ProjectionError> {
15    let mut context = ProjectionContext::new(choreography, role);
16    context.project_protocol(&choreography.protocol)
17}
18
19/// Errors that can occur during projection
20#[derive(Debug, thiserror::Error)]
21pub enum ProjectionError {
22    #[error("cannot project choice for non-participant role")]
23    NonParticipantChoice,
24
25    #[error("parallel composition not supported for role {0}")]
26    UnsupportedParallel(String),
27
28    #[error("inconsistent projections in parallel branches")]
29    InconsistentParallel,
30
31    #[error("recursive variable {0} not in scope")]
32    UnboundVariable(String),
33
34    #[error("dynamic role {role} requires runtime context for projection")]
35    DynamicRoleProjection { role: String },
36
37    #[error("symbolic role parameter '{param}' not bound in context")]
38    UnboundSymbolic { param: String },
39
40    #[error("range role index cannot be projected to concrete local type")]
41    RangeProjection,
42
43    #[error("wildcard role index requires specialized projection context")]
44    WildcardProjection,
45
46    #[error("cannot merge branches: {0}")]
47    MergeFailure(String),
48
49    #[error("authority-local construct `{construct}` is not projectable without an explicit session-typing rule")]
50    UnsupportedAuthorityConstruct { construct: &'static str },
51}
52
53/// Context for projection algorithm
54struct ProjectionContext<'a> {
55    role: &'a Role,
56    /// Bindings for symbolic role parameters (e.g., N -> 5)
57    role_bindings: HashMap<String, u32>,
58}
59
60impl<'a> ProjectionContext<'a> {
61    fn new(_choreography: &'a Choreography, role: &'a Role) -> Self {
62        ProjectionContext {
63            role,
64            role_bindings: HashMap::new(),
65        }
66    }
67
68    /// Check if this projection role matches the given protocol role
69    fn role_matches(&self, protocol_role: &Role) -> Result<bool, ProjectionError> {
70        // First check for exact name match
71        if self.role.name() != protocol_role.name() {
72            return Ok(false);
73        }
74
75        // If both are simple roles, they match
76        if !self.role.is_parameterized() && !protocol_role.is_parameterized() {
77            return Ok(true);
78        }
79
80        // Handle dynamic role matching
81        self.matches_dynamic_role(protocol_role)
82    }
83
84    /// Check if the projection role matches a dynamic protocol role
85    fn matches_dynamic_role(&self, protocol_role: &Role) -> Result<bool, ProjectionError> {
86        match (self.role.param(), protocol_role.param()) {
87            // Static vs Static: must have same count
88            (Some(RoleParam::Static(self_count)), Some(RoleParam::Static(proto_count))) => {
89                Ok(self_count == proto_count)
90            }
91            // Static vs Symbolic: resolve symbolic and compare
92            (Some(RoleParam::Static(self_count)), Some(RoleParam::Symbolic(sym_name))) => {
93                if let Some(&resolved_count) = self.role_bindings.get(sym_name) {
94                    Ok(*self_count == resolved_count)
95                } else {
96                    Err(ProjectionError::UnboundSymbolic {
97                        param: sym_name.clone(),
98                    })
99                }
100            }
101            // Symbolic vs Static: resolve symbolic and compare
102            (Some(RoleParam::Symbolic(sym_name)), Some(RoleParam::Static(proto_count))) => {
103                if let Some(&resolved_count) = self.role_bindings.get(sym_name) {
104                    Ok(resolved_count == *proto_count)
105                } else {
106                    Err(ProjectionError::UnboundSymbolic {
107                        param: sym_name.clone(),
108                    })
109                }
110            }
111            // Symbolic vs Symbolic: resolve both and compare
112            (Some(RoleParam::Symbolic(self_sym)), Some(RoleParam::Symbolic(proto_sym))) => {
113                let self_resolved = self.role_bindings.get(self_sym).ok_or_else(|| {
114                    ProjectionError::UnboundSymbolic {
115                        param: self_sym.clone(),
116                    }
117                })?;
118                let proto_resolved = self.role_bindings.get(proto_sym).ok_or_else(|| {
119                    ProjectionError::UnboundSymbolic {
120                        param: proto_sym.clone(),
121                    }
122                })?;
123                Ok(self_resolved == proto_resolved)
124            }
125            // Runtime roles require special handling
126            (_, Some(RoleParam::Runtime)) | (Some(RoleParam::Runtime), _) => {
127                Err(ProjectionError::DynamicRoleProjection {
128                    role: protocol_role.name().to_string(),
129                })
130            }
131            // One parameterized, one not: no match
132            (Some(_), None) | (None, Some(_)) => Ok(false),
133            // Both None: already handled above
134            (None, None) => Ok(true),
135        }
136    }
137
138    fn project_protocol(&mut self, protocol: &Protocol) -> Result<LocalType, ProjectionError> {
139        match protocol {
140            Protocol::Begin { .. } => {
141                Err(ProjectionError::UnsupportedAuthorityConstruct { construct: "begin" })
142            }
143
144            Protocol::Await { .. } => {
145                Err(ProjectionError::UnsupportedAuthorityConstruct { construct: "await" })
146            }
147
148            Protocol::Resolve { .. } => Err(ProjectionError::UnsupportedAuthorityConstruct {
149                construct: "resolve",
150            }),
151
152            Protocol::Invalidate { .. } => Err(ProjectionError::UnsupportedAuthorityConstruct {
153                construct: "invalidate",
154            }),
155
156            Protocol::Send {
157                from,
158                to,
159                message,
160                continuation,
161                ..
162            } => self.project_send(from, to, message, continuation),
163
164            Protocol::Broadcast {
165                from,
166                to_all,
167                message,
168                continuation,
169                ..
170            } => self.project_broadcast(from, to_all, message, continuation),
171
172            Protocol::Choice {
173                role: choice_role,
174                branches,
175                ..
176            } => self.project_choice(choice_role, branches),
177
178            Protocol::Let { continuation, .. } => self.project_protocol(continuation),
179
180            Protocol::Case { branches, .. } => self.project_case(branches),
181
182            Protocol::Timeout {
183                role,
184                duration_ms,
185                body,
186                on_timeout,
187                on_cancel,
188            } => self.project_timeout(role, *duration_ms, body, on_timeout, on_cancel.as_deref()),
189
190            Protocol::Loop { condition, body } => self.project_loop(condition.as_ref(), body),
191
192            Protocol::Parallel { protocols } => self.project_parallel(protocols),
193
194            Protocol::Rec { label, body } => self.project_rec(label, body),
195
196            Protocol::Var(label) => self.project_var(label),
197
198            Protocol::Publish { continuation, .. }
199            | Protocol::PublishAuthority { continuation, .. }
200            | Protocol::Materialize { continuation, .. }
201            | Protocol::Handoff { continuation, .. }
202            | Protocol::DependentWork { continuation, .. } => self.project_protocol(continuation),
203
204            Protocol::End => Ok(LocalType::End),
205
206            Protocol::Extension {
207                extension: _,
208                continuation,
209                annotations: _,
210            } => {
211                // Preserve continuation structure for extension nodes.
212                // Extension-local projection can be layered later once LocalType models it.
213                self.project_protocol(continuation)
214            }
215        }
216    }
217
218    /// Project a send operation onto the local type for this role
219    ///
220    /// # Projection Rules
221    /// - If `role == from`: Project to `Send(to, message, continuation↓role)`
222    /// - If `role == to`: Project to `Receive(from, message, continuation↓role)`
223    /// - Otherwise: Project to `continuation↓role` (uninvolved party)
224    ///
225    /// This implements the standard session type projection rule where
226    /// uninvolved parties simply skip communication they don't participate in.
227    fn project_send(
228        &mut self,
229        from: &Role,
230        to: &Role,
231        message: &MessageType,
232        continuation: &Protocol,
233    ) -> Result<LocalType, ProjectionError> {
234        let is_sender = self.role_matches(from)?;
235        let is_receiver = self.role_matches(to)?;
236
237        if is_sender {
238            // We are the sender
239            Ok(LocalType::Send {
240                to: to.clone(),
241                message: message.clone(),
242                continuation: Box::new(self.project_protocol(continuation)?),
243            })
244        } else if is_receiver {
245            // We are the receiver
246            Ok(LocalType::Receive {
247                from: from.clone(),
248                message: message.clone(),
249                continuation: Box::new(self.project_protocol(continuation)?),
250            })
251        } else {
252            // We are not involved, skip to continuation
253            self.project_protocol(continuation)
254        }
255    }
256
257    /// Project a broadcast operation onto the local type for this role
258    ///
259    /// # Projection Rules
260    /// - If `role == from`: Expand into nested sends to all recipients
261    /// - If `role ∈ to_all`: Project to `Receive(from, message, continuation↓role)`  
262    /// - Otherwise: Project to `continuation↓role`
263    ///
264    /// # Implementation Note
265    /// Broadcasts are expanded into sequential sends at the sender side.
266    /// Sends are built in reverse order to create proper nesting:
267    /// `Broadcast(A, [B,C], msg) → Send(A→B, Send(A→C, continuation))`
268    fn project_broadcast(
269        &mut self,
270        from: &Role,
271        to_all: &[Role],
272        message: &MessageType,
273        continuation: &Protocol,
274    ) -> Result<LocalType, ProjectionError> {
275        let is_sender = self.role_matches(from)?;
276
277        // Check if we are a recipient using dynamic role matching
278        let mut is_receiver = false;
279        for to_role in to_all {
280            if self.role_matches(to_role)? {
281                is_receiver = true;
282                break;
283            }
284        }
285
286        if is_sender {
287            // We are broadcasting - need to send to each recipient
288            let mut current = self.project_protocol(continuation)?;
289
290            // Build sends in reverse order so they nest correctly
291            for to in to_all.iter().rev() {
292                current = LocalType::Send {
293                    to: to.clone(),
294                    message: message.clone(),
295                    continuation: Box::new(current),
296                };
297            }
298
299            Ok(current)
300        } else if is_receiver {
301            // We are receiving the broadcast
302            Ok(LocalType::Receive {
303                from: from.clone(),
304                message: message.clone(),
305                continuation: Box::new(self.project_protocol(continuation)?),
306            })
307        } else {
308            // Not involved in broadcast
309            self.project_protocol(continuation)
310        }
311    }
312
313    /// Project a choice operation onto the local type for this role
314    ///
315    /// # Projection Rules (Enhanced)
316    /// - If `role == choice_role`:
317    ///   - If branches start with Send: Project as `Select` (communicated choice)
318    ///   - Otherwise: Project as `LocalChoice` (local decision)
319    /// - If `role` receives the choice: Project as `Branch`
320    /// - Otherwise: Merge continuations (uninvolved party)
321    ///
322    /// # Implementation Notes
323    /// This enhancement supports choice branches that don't start with Send,
324    /// allowing for local decisions and more complex choreographic patterns.
325    fn project_choice(
326        &mut self,
327        choice_role: &Role,
328        branches: &[Branch],
329    ) -> Result<LocalType, ProjectionError> {
330        let is_choice_maker = self.role_matches(choice_role)?;
331
332        if is_choice_maker {
333            // We make the choice
334            // Check if this is a communicated choice (branches start with Send)
335            let first_sends = branches
336                .iter()
337                .all(|b| matches!(&b.protocol, Protocol::Send { .. }));
338
339            if first_sends && !branches.is_empty() {
340                // Communicated choice - project as Select.
341                //
342                // When the choice label matches the first message name, the
343                // select() call carries the payload and the first Send is
344                // consumed (avoids double-send).  Otherwise the choice label
345                // and the message are distinct communications.
346                let mut local_branches = Vec::new();
347
348                for branch in branches {
349                    let label_matches_msg = match &branch.protocol {
350                        Protocol::Send { message, .. } => branch.label == message.name,
351                        _ => false,
352                    };
353
354                    let local_type = if label_matches_msg {
355                        match &branch.protocol {
356                            Protocol::Send { continuation, .. } => {
357                                self.project_protocol(continuation)?
358                            }
359                            _ => return Err(ProjectionError::NonParticipantChoice),
360                        }
361                    } else {
362                        self.project_protocol(&branch.protocol)?
363                    };
364                    local_branches.push((branch.label.clone(), local_type));
365                }
366
367                // Find the recipient (from first branch's send)
368                let recipient = match &branches[0].protocol {
369                    Protocol::Send { to, .. } => to.clone(),
370                    _ => {
371                        return Err(ProjectionError::NonParticipantChoice);
372                    }
373                };
374
375                Ok(LocalType::Select {
376                    to: recipient,
377                    branches: local_branches,
378                })
379            } else {
380                // Local choice (no communication) - project as LocalChoice
381                let mut local_branches = Vec::new();
382
383                for branch in branches {
384                    let local_type = self.project_protocol(&branch.protocol)?;
385                    local_branches.push((branch.label.clone(), local_type));
386                }
387
388                Ok(LocalType::LocalChoice {
389                    branches: local_branches,
390                })
391            }
392        } else {
393            // Check if we receive the choice
394            let mut receives_choice = false;
395            let mut sender = None;
396
397            for branch in branches {
398                if let Protocol::Send { from, to, .. } = &branch.protocol {
399                    if self.role_matches(to)? {
400                        receives_choice = true;
401                        sender = Some(from.clone());
402                        break;
403                    }
404                }
405            }
406
407            if receives_choice {
408                // We receive the choice - project as Branch.
409                //
410                // When the choice label matches the first message, the
411                // Branch dispatches on the received message directly and
412                // the first Receive is consumed.  Otherwise both the
413                // label dispatch and the Receive remain separate.
414                let sender = sender.ok_or(ProjectionError::NonParticipantChoice)?;
415                let mut local_branches = Vec::new();
416
417                for branch in branches {
418                    let label_matches_msg = match &branch.protocol {
419                        Protocol::Send { message, .. } => branch.label == message.name,
420                        _ => false,
421                    };
422
423                    let local_type = if label_matches_msg {
424                        match &branch.protocol {
425                            Protocol::Send { continuation, .. } => {
426                                self.project_protocol(continuation)?
427                            }
428                            _ => self.project_protocol(&branch.protocol)?,
429                        }
430                    } else {
431                        self.project_protocol(&branch.protocol)?
432                    };
433                    local_branches.push((branch.label.clone(), local_type));
434                }
435
436                Ok(LocalType::Branch {
437                    from: sender,
438                    branches: local_branches,
439                })
440            } else {
441                // Not involved in the choice - merge continuations
442                self.merge_choice_continuations(branches)
443            }
444        }
445    }
446
447    /// Project a loop operation onto the local type for this role
448    ///
449    /// # Projection Rules
450    /// - Project the loop body
451    /// - If the role participates in the loop: Wrap in `Loop` with condition
452    /// - If the role doesn't participate: Project to End
453    ///
454    /// # Implementation Notes
455    /// Loop conditions are now preserved in the local type, allowing runtime
456    /// to make decisions about loop iteration based on the condition type.
457    fn project_loop(
458        &mut self,
459        condition: Option<&crate::ast::protocol::Condition>,
460        body: &Protocol,
461    ) -> Result<LocalType, ProjectionError> {
462        let body_projection = self.project_protocol(body)?;
463
464        // Only include Loop if the body actually involves this role
465        if body_projection == LocalType::End {
466            Ok(LocalType::End)
467        } else {
468            Ok(LocalType::Loop {
469                condition: condition.cloned(),
470                body: Box::new(body_projection),
471            })
472        }
473    }
474
475    fn project_case(
476        &mut self,
477        branches: &[crate::ast::CaseBranch],
478    ) -> Result<LocalType, ProjectionError> {
479        let mut local_branches = Vec::with_capacity(branches.len());
480        for branch in branches {
481            let label = Ident::new(&branch.pattern.constructor, Span::call_site());
482            let local_type = self.project_protocol(&branch.protocol)?;
483            local_branches.push((label, local_type));
484        }
485
486        self.project_local_branches(local_branches)
487    }
488
489    fn project_timeout(
490        &mut self,
491        timeout_role: &Role,
492        duration_ms: u64,
493        body: &Protocol,
494        on_timeout: &Protocol,
495        on_cancel: Option<&Protocol>,
496    ) -> Result<LocalType, ProjectionError> {
497        let body = self.project_protocol(body)?;
498        let on_timeout = self.project_protocol(on_timeout)?;
499        let on_cancel = on_cancel
500            .map(|branch| self.project_protocol(branch).map(Box::new))
501            .transpose()?;
502
503        let owns_timeout = self.role_matches(timeout_role)?;
504        let branch_has_effect = body != LocalType::End
505            || on_timeout != LocalType::End
506            || on_cancel
507                .as_deref()
508                .is_some_and(|branch| branch != &LocalType::End);
509
510        if owns_timeout || branch_has_effect {
511            Ok(LocalType::Timeout {
512                duration: Duration::from_millis(duration_ms),
513                body: Box::new(body),
514                on_timeout: Box::new(on_timeout),
515                on_cancel,
516            })
517        } else {
518            Ok(LocalType::End)
519        }
520    }
521
522    fn project_local_branches(
523        &self,
524        local_branches: Vec<(Ident, LocalType)>,
525    ) -> Result<LocalType, ProjectionError> {
526        let projections_only: Vec<_> = local_branches
527            .iter()
528            .map(|(_, projection)| projection)
529            .collect();
530        if projections_only
531            .windows(2)
532            .all(|window| window[0] == window[1])
533        {
534            return Ok(local_branches
535                .into_iter()
536                .next()
537                .map(|(_, projection)| projection)
538                .unwrap_or(LocalType::End));
539        }
540
541        match self.merge_local_types_labeled(local_branches.clone()) {
542            Ok(merged) => Ok(merged),
543            Err(ProjectionError::MergeFailure(_)) => Ok(LocalType::LocalChoice {
544                branches: local_branches,
545            }),
546            Err(err) => Err(err),
547        }
548    }
549
550    /// Project a parallel composition onto the local type for this role
551    ///
552    /// # Projection Rules (Enhanced)
553    /// - If role appears in 0 branches: Project to `End`
554    /// - If role appears in 1 branch: Use that projection
555    /// - If role appears in multiple branches:
556    ///   - Check for conflicts (incompatible operations)
557    ///   - If mergeable: Interleave operations
558    ///   - If conflicting: Return error with details
559    ///
560    /// # Implementation Notes
561    /// This enhancement detects conflicting parallel operations (e.g., sending
562    /// to the same recipient simultaneously) and provides better error messages.
563    fn project_parallel(&mut self, protocols: &[Protocol]) -> Result<LocalType, ProjectionError> {
564        // Project all parallel branches for this role
565        let mut projections = Vec::new();
566        for protocol in protocols {
567            if protocol.mentions_role(self.role) {
568                projections.push(self.project_protocol(protocol)?);
569            }
570        }
571
572        match projections.len() {
573            0 => {
574                // Role doesn't appear in any parallel branch
575                Ok(LocalType::End)
576            }
577            1 => {
578                // Role appears in exactly one branch - use that projection
579                Ok(projections.into_iter().next().unwrap_or(LocalType::End))
580            }
581            _ => {
582                // Role appears in multiple parallel branches
583                // Check for conflicts before merging
584                self.merge_parallel_projections(projections)
585            }
586        }
587    }
588}