1mod 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
13pub 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#[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
53struct ProjectionContext<'a> {
55 role: &'a Role,
56 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 fn role_matches(&self, protocol_role: &Role) -> Result<bool, ProjectionError> {
70 if self.role.name() != protocol_role.name() {
72 return Ok(false);
73 }
74
75 if !self.role.is_parameterized() && !protocol_role.is_parameterized() {
77 return Ok(true);
78 }
79
80 self.matches_dynamic_role(protocol_role)
82 }
83
84 fn matches_dynamic_role(&self, protocol_role: &Role) -> Result<bool, ProjectionError> {
86 match (self.role.param(), protocol_role.param()) {
87 (Some(RoleParam::Static(self_count)), Some(RoleParam::Static(proto_count))) => {
89 Ok(self_count == proto_count)
90 }
91 (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 (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 (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 (_, Some(RoleParam::Runtime)) | (Some(RoleParam::Runtime), _) => {
127 Err(ProjectionError::DynamicRoleProjection {
128 role: protocol_role.name().to_string(),
129 })
130 }
131 (Some(_), None) | (None, Some(_)) => Ok(false),
133 (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 self.project_protocol(continuation)
214 }
215 }
216 }
217
218 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 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 Ok(LocalType::Receive {
247 from: from.clone(),
248 message: message.clone(),
249 continuation: Box::new(self.project_protocol(continuation)?),
250 })
251 } else {
252 self.project_protocol(continuation)
254 }
255 }
256
257 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 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 let mut current = self.project_protocol(continuation)?;
289
290 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 Ok(LocalType::Receive {
303 from: from.clone(),
304 message: message.clone(),
305 continuation: Box::new(self.project_protocol(continuation)?),
306 })
307 } else {
308 self.project_protocol(continuation)
310 }
311 }
312
313 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 let first_sends = branches
336 .iter()
337 .all(|b| matches!(&b.protocol, Protocol::Send { .. }));
338
339 if first_sends && !branches.is_empty() {
340 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 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 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 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 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 self.merge_choice_continuations(branches)
443 }
444 }
445 }
446
447 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 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 fn project_parallel(&mut self, protocols: &[Protocol]) -> Result<LocalType, ProjectionError> {
564 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 Ok(LocalType::End)
576 }
577 1 => {
578 Ok(projections.into_iter().next().unwrap_or(LocalType::End))
580 }
581 _ => {
582 self.merge_parallel_projections(projections)
585 }
586 }
587 }
588}