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