1#![allow(clippy::expect_used)]
4mod conversion;
17mod declarations;
18mod error;
19mod linear;
20mod lints;
21mod role;
22mod statement;
23mod stmt_parsers;
24mod types;
25
26pub use error::{ErrorSpan, ParseError};
28pub use lints::{
29 collect_dsl_lints, explain_lowering, render_lsp_lint_diagnostics, LintDiagnostic, LintLevel,
30};
31
32use crate::ast::{
33 AgreementProfileDeclaration, Annotations, Choreography, EffectInterfaceDeclaration,
34 ExecutionProfileDeclaration, GuestRuntimeDeclaration, OperationDeclaration, Protocol,
35 RegionDeclaration, Role, RoleSetDeclaration, TheoremPackDeclaration, TopologyDeclaration,
36 TypeDeclaration,
37};
38use crate::compiler::layout::preprocess_layout;
39use crate::extensions::{ExtensionRegistry, ParseContext, ProtocolExtension, StatementParser};
40use pest::Parser;
41use pest_derive::Parser;
42use proc_macro2::{Span, TokenStream};
43use quote::format_ident;
44use regex::Regex;
45use std::collections::{HashMap, HashSet};
46
47use conversion::{convert_statements_to_protocol, inline_calls};
48use declarations::{
49 enforce_same_line_equals, parse_agreement_profile_decl, parse_effect_decl, parse_fragment_decl,
50 parse_guest_runtime_decl, parse_module_decl, parse_operation_decl, parse_profile_decl,
51 parse_proof_bundle_decl, parse_protocol_profiles, parse_protocol_requires, parse_protocol_uses,
52 parse_role_set_decl, parse_topology_decl, parse_type_decl,
53};
54use linear::{infer_required_proof_bundles, validate_authority_surface, validate_linear_vm_assets};
55use role::parse_roles_from_pair;
56use statement::{parse_local_protocol_decl, parse_protocol_body};
57use types::Statement;
58
59type ParsedExtensions = (Choreography, Vec<Box<dyn ProtocolExtension>>);
60
61#[derive(Parser)]
62#[grammar = "compiler/choreography.pest"]
63struct ChoreographyParser;
64
65pub const DEFAULT_SOURCE_EXTENSION: &str = "tell";
67
68pub fn parse_choreography_str(input: &str) -> std::result::Result<Choreography, ParseError> {
70 parse_choreography_str_with_extensions(input, &ExtensionRegistry::new())
71 .map(|(choreo, _)| choreo)
72}
73
74pub fn parse_choreography_str_with_extensions(
76 input: &str,
77 registry: &ExtensionRegistry,
78) -> std::result::Result<ParsedExtensions, ParseError> {
79 let dedented = strip_common_indent(input);
80 if registry.has_extensions() {
81 if let Some(parsed) = try_parse_with_extension_dispatch(&dedented, registry)? {
82 return Ok(parsed);
83 }
84 }
85 parse_choreography_str_core(&dedented)
86}
87
88#[allow(clippy::too_many_lines)]
89fn parse_choreography_str_core(
90 dedented: &str,
91) -> std::result::Result<ParsedExtensions, ParseError> {
92 reject_legacy_structural_braces(dedented)?;
93 reject_removed_legacy_surfaces(dedented)?;
94 let layout = preprocess_layout(dedented).map_err(|e| ParseError::Layout {
95 span: ErrorSpan::from_line_col(e.line, e.column, dedented),
96 message: e.message,
97 })?;
98 let pairs = ChoreographyParser::parse(Rule::choreography, &layout).map_err(Box::new)?;
99
100 let mut name = format_ident!("Unnamed");
101 let mut namespace: Option<String> = None;
102 let mut roles: Vec<crate::ast::Role> = Vec::new();
103 let mut declared_roles: HashSet<String> = HashSet::new();
104 let mut protocol_defs: HashMap<String, Vec<Statement>> = HashMap::new();
105 let mut statements: Vec<Statement> = Vec::new();
106 let mut theorem_packs: Vec<TheoremPackDeclaration> = Vec::new();
107 let mut required_bundles: Vec<String> = Vec::new();
108 let mut protocol_uses: Vec<String> = Vec::new();
109 let mut protocol_profiles: Vec<String> = Vec::new();
110 let mut role_sets: Vec<RoleSetDeclaration> = Vec::new();
111 let mut topologies: Vec<TopologyDeclaration> = Vec::new();
112 let mut type_declarations: Vec<TypeDeclaration> = Vec::new();
113 let mut effect_interface_declarations: Vec<EffectInterfaceDeclaration> = Vec::new();
114 let mut region_declarations: Vec<RegionDeclaration> = Vec::new();
115 let mut operation_declarations: Vec<OperationDeclaration> = Vec::new();
116 let mut guest_runtime_declarations: Vec<GuestRuntimeDeclaration> = Vec::new();
117 let mut execution_profile_declarations: Vec<ExecutionProfileDeclaration> = Vec::new();
118 let mut agreement_profile_declarations: Vec<AgreementProfileDeclaration> = Vec::new();
119
120 for pair in pairs {
121 if pair.as_rule() == Rule::choreography {
122 for inner in pair.into_inner() {
123 match inner.as_rule() {
124 Rule::module_decl => {
125 namespace = Some(parse_module_decl(inner, &layout)?);
126 }
127 Rule::import_decl => {
128 }
130 Rule::proof_bundle_decl => {
131 theorem_packs.push(parse_proof_bundle_decl(inner, &layout)?);
132 }
133 Rule::profile_decl => {
134 execution_profile_declarations.push(parse_profile_decl(inner, &layout)?);
135 }
136 Rule::agreement_profile_decl => {
137 agreement_profile_declarations
138 .push(parse_agreement_profile_decl(inner, &layout)?);
139 }
140 Rule::role_set_decl => {
141 role_sets.push(parse_role_set_decl(inner, &layout)?);
142 }
143 Rule::topology_decl => {
144 topologies.push(parse_topology_decl(inner, &layout)?);
145 }
146 Rule::type_decl => {
147 type_declarations.push(parse_type_decl(inner, &layout)?);
148 }
149 Rule::effect_decl => {
150 effect_interface_declarations.push(parse_effect_decl(inner, &layout)?);
151 }
152 Rule::fragment_decl => {
153 region_declarations.push(parse_fragment_decl(inner, &layout)?);
154 }
155 Rule::operation_decl => {
156 operation_declarations.push(parse_operation_decl(inner, &layout)?);
157 }
158 Rule::guest_runtime_decl => {
159 guest_runtime_declarations.push(parse_guest_runtime_decl(inner, &layout)?);
160 }
161 Rule::protocol_decl => {
162 let protocol_span = inner.as_span();
163 enforce_same_line_equals(
164 inner.as_str(),
165 protocol_span,
166 &layout,
167 "protocol declaration",
168 )?;
169 let mut proto_inner = inner.into_inner();
170 let name_pair = proto_inner.next().ok_or_else(|| ParseError::Syntax {
171 span: ErrorSpan::from_pest_span(protocol_span, &layout),
172 message: "protocol declaration is missing a name".to_string(),
173 })?;
174 name = format_ident!("{}", name_pair.as_str());
175
176 let mut header_roles: Option<Vec<crate::ast::Role>> = None;
177 let mut body_pair: Option<pest::iterators::Pair<Rule>> = None;
178 let mut where_pair: Option<pest::iterators::Pair<Rule>> = None;
179
180 for item in proto_inner {
181 match item.as_rule() {
182 Rule::header_roles => {
183 header_roles = Some(parse_roles_from_pair(item, &layout)?);
184 }
185 Rule::protocol_requires => {
186 required_bundles = parse_protocol_requires(item);
187 }
188 Rule::protocol_uses => {
189 protocol_uses = parse_protocol_uses(item);
190 }
191 Rule::protocol_profiles => {
192 protocol_profiles = parse_protocol_profiles(item);
193 }
194 Rule::protocol_body => {
195 body_pair = Some(item);
196 }
197 Rule::where_block => {
198 where_pair = Some(item);
199 }
200 _ => {}
201 }
202 }
203
204 let allow_roles_decl = header_roles.is_none();
205 let body_pair = body_pair.ok_or_else(|| ParseError::Syntax {
206 span: ErrorSpan::from_pest_span(protocol_span, &layout),
207 message: "protocol declaration is missing a body".to_string(),
208 })?;
209 let body_span = body_pair.as_span();
210 let types::ParsedBody {
211 roles: body_roles,
212 statements: body_statements,
213 } = parse_protocol_body(
214 body_pair,
215 &declared_roles,
216 &layout,
217 &protocol_defs,
218 allow_roles_decl,
219 )?;
220
221 if header_roles.is_some() && body_roles.is_some() {
222 return Err(ParseError::Syntax {
223 span: ErrorSpan::from_pest_span(body_span, &layout),
224 message: "roles cannot be declared both in the header and body"
225 .to_string(),
226 });
227 }
228
229 if let Some(r) = header_roles {
230 roles = r;
231 declared_roles = roles.iter().map(|r| r.name().to_string()).collect();
232 } else if let Some(r) = body_roles {
233 roles = r;
234 declared_roles = roles.iter().map(|r| r.name().to_string()).collect();
235 }
236
237 if let Some(where_block) = where_pair {
238 for local in where_block.into_inner().flat_map(|p| p.into_inner()) {
239 if local.as_rule() == Rule::local_protocol_decl {
240 parse_local_protocol_decl(
241 local,
242 &declared_roles,
243 &layout,
244 &mut protocol_defs,
245 )?;
246 }
247 }
248 }
249
250 validate_linear_vm_assets(&body_statements, &layout)?;
251 validate_authority_surface(&body_statements, &layout)?;
252
253 let mut local_protocols: Vec<_> = protocol_defs.iter().collect();
254 local_protocols.sort_by_key(|(lhs, _)| *lhs);
255 for (_, local_statements) in local_protocols {
256 validate_linear_vm_assets(local_statements, &layout)?;
257 validate_authority_surface(local_statements, &layout)?;
258 }
259
260 statements = inline_calls(&body_statements, &protocol_defs, &layout)?;
261 }
262 _ => {}
263 }
264 }
265 }
266 }
267
268 if roles.is_empty() {
269 return Err(ParseError::EmptyChoreography);
270 }
271
272 let protocol = convert_statements_to_protocol(&statements, &roles);
273
274 let mut choreography = Choreography {
275 name,
276 namespace,
277 roles,
278 protocol,
279 attrs: HashMap::new(),
280 };
281
282 choreography
283 .set_theorem_packs(&theorem_packs)
284 .map_err(|message| ParseError::Syntax {
285 span: ErrorSpan::from_line_col(1, 1, &layout),
286 message,
287 })?;
288 let inferred_required_bundles =
289 infer_required_proof_bundles(&required_bundles, &theorem_packs, &statements);
290 let resolved_required_bundles = if required_bundles.is_empty() {
291 inferred_required_bundles.clone()
292 } else {
293 required_bundles.clone()
294 };
295
296 choreography
297 .set_required_theorem_packs(&resolved_required_bundles)
298 .map_err(|message| ParseError::Syntax {
299 span: ErrorSpan::from_line_col(1, 1, &layout),
300 message,
301 })?;
302 choreography
303 .set_inferred_required_theorem_packs(&inferred_required_bundles)
304 .map_err(|message| ParseError::Syntax {
305 span: ErrorSpan::from_line_col(1, 1, &layout),
306 message,
307 })?;
308 choreography
309 .set_role_sets(&role_sets)
310 .map_err(|message| ParseError::Syntax {
311 span: ErrorSpan::from_line_col(1, 1, &layout),
312 message,
313 })?;
314 choreography
315 .set_topologies(&topologies)
316 .map_err(|message| ParseError::Syntax {
317 span: ErrorSpan::from_line_col(1, 1, &layout),
318 message,
319 })?;
320 choreography
321 .set_type_declarations(&type_declarations)
322 .map_err(|message| ParseError::Syntax {
323 span: ErrorSpan::from_line_col(1, 1, &layout),
324 message,
325 })?;
326 choreography
327 .set_effect_interface_declarations(&effect_interface_declarations)
328 .map_err(|message| ParseError::Syntax {
329 span: ErrorSpan::from_line_col(1, 1, &layout),
330 message,
331 })?;
332 choreography
333 .set_protocol_uses(&protocol_uses)
334 .map_err(|message| ParseError::Syntax {
335 span: ErrorSpan::from_line_col(1, 1, &layout),
336 message,
337 })?;
338 choreography
339 .set_region_declarations(®ion_declarations)
340 .map_err(|message| ParseError::Syntax {
341 span: ErrorSpan::from_line_col(1, 1, &layout),
342 message,
343 })?;
344 choreography
345 .set_operation_declarations(&operation_declarations)
346 .map_err(|message| ParseError::Syntax {
347 span: ErrorSpan::from_line_col(1, 1, &layout),
348 message,
349 })?;
350 choreography
351 .set_guest_runtime_declarations(&guest_runtime_declarations)
352 .map_err(|message| ParseError::Syntax {
353 span: ErrorSpan::from_line_col(1, 1, &layout),
354 message,
355 })?;
356 choreography
357 .set_execution_profile_declarations(&execution_profile_declarations)
358 .map_err(|message| ParseError::Syntax {
359 span: ErrorSpan::from_line_col(1, 1, &layout),
360 message,
361 })?;
362 choreography
363 .set_agreement_profile_declarations(&agreement_profile_declarations)
364 .map_err(|message| ParseError::Syntax {
365 span: ErrorSpan::from_line_col(1, 1, &layout),
366 message,
367 })?;
368 choreography
369 .set_protocol_execution_profiles(&protocol_profiles)
370 .map_err(|message| ParseError::Syntax {
371 span: ErrorSpan::from_line_col(1, 1, &layout),
372 message,
373 })?;
374
375 Ok((choreography, Vec::new()))
376}
377
378struct ExtensionBinding<'a> {
379 rule_name: String,
380 parser: &'a dyn StatementParser,
381 prefix: String,
382}
383
384struct StatementChunk {
385 text: String,
386 extension_binding: Option<usize>,
387}
388
389struct ProtocolBodySplit {
390 stripped_input: String,
391 chunks: Vec<StatementChunk>,
392}
393
394fn try_parse_with_extension_dispatch(
395 input: &str,
396 registry: &ExtensionRegistry,
397) -> std::result::Result<Option<ParsedExtensions>, ParseError> {
398 let bindings = collect_extension_bindings(registry);
399 if bindings.is_empty() || !input_contains_extension_candidates(input, &bindings) {
400 return Ok(None);
401 }
402
403 let split = match split_protocol_body_chunks(input, &bindings) {
404 Some(split) => split,
405 None => return Ok(None),
406 };
407 if !split
408 .chunks
409 .iter()
410 .any(|chunk| chunk.extension_binding.is_some())
411 {
412 return Ok(None);
413 }
414
415 let (mut choreography, _) = parse_choreography_str_core(&split.stripped_input)?;
416 let mut extensions = Vec::new();
417 let mut rebuilt = Protocol::End;
418
419 for chunk in &split.chunks {
420 let (chunk_protocol, mut chunk_extensions) =
421 parse_chunk_protocol(chunk, &choreography.roles, registry, input, &bindings)?;
422 rebuilt = append_protocol(rebuilt, chunk_protocol);
423 extensions.append(&mut chunk_extensions);
424 }
425
426 choreography.protocol = rebuilt;
427 choreography
428 .validate()
429 .map_err(|message| ParseError::Syntax {
430 span: ErrorSpan::from_line_col(1, 1, input),
431 message: message.to_string(),
432 })?;
433
434 Ok(Some((choreography, extensions)))
435}
436
437fn parse_chunk_protocol(
438 chunk: &StatementChunk,
439 roles: &[Role],
440 registry: &ExtensionRegistry,
441 original_input: &str,
442 bindings: &[ExtensionBinding<'_>],
443) -> std::result::Result<(Protocol, Vec<Box<dyn ProtocolExtension>>), ParseError> {
444 if let Some(binding_idx) = chunk.extension_binding {
445 let binding = &bindings[binding_idx];
446 let ext = binding
447 .parser
448 .parse_statement(
449 &binding.rule_name,
450 chunk.text.trim(),
451 &ParseContext {
452 declared_roles: roles,
453 input: original_input,
454 },
455 )
456 .map_err(|err| ParseError::Syntax {
457 span: ErrorSpan::from_line_col(1, 1, original_input),
458 message: err.to_string(),
459 })?;
460 ext.validate(roles).map_err(|err| ParseError::Syntax {
461 span: ErrorSpan::from_line_col(1, 1, original_input),
462 message: err.to_string(),
463 })?;
464 return Ok((
465 Protocol::Extension {
466 extension: ext.clone(),
467 continuation: Box::new(Protocol::End),
468 annotations: Annotations::default(),
469 },
470 vec![ext],
471 ));
472 }
473
474 let chunk_source = build_chunk_protocol_source(roles, &chunk.text);
475 let (choreography, extensions) =
476 parse_choreography_str_with_extensions(&chunk_source, registry)?;
477 Ok((choreography.protocol, extensions))
478}
479
480fn append_protocol(left: Protocol, right: Protocol) -> Protocol {
481 match left {
482 Protocol::End => right,
483 Protocol::Begin {
484 operation,
485 args,
486 progress,
487 continuation,
488 } => Protocol::Begin {
489 operation,
490 args,
491 progress,
492 continuation: Box::new(append_protocol(*continuation, right)),
493 },
494 Protocol::Await {
495 operation,
496 continuation,
497 } => Protocol::Await {
498 operation,
499 continuation: Box::new(append_protocol(*continuation, right)),
500 },
501 Protocol::Resolve {
502 operation,
503 outcome,
504 continuation,
505 } => Protocol::Resolve {
506 operation,
507 outcome,
508 continuation: Box::new(append_protocol(*continuation, right)),
509 },
510 Protocol::Invalidate {
511 operation,
512 continuation,
513 } => Protocol::Invalidate {
514 operation,
515 continuation: Box::new(append_protocol(*continuation, right)),
516 },
517 Protocol::Send {
518 from,
519 to,
520 message,
521 continuation,
522 annotations,
523 from_annotations,
524 to_annotations,
525 } => Protocol::Send {
526 from,
527 to,
528 message,
529 continuation: Box::new(append_protocol(*continuation, right)),
530 annotations,
531 from_annotations,
532 to_annotations,
533 },
534 Protocol::Broadcast {
535 from,
536 to_all,
537 message,
538 continuation,
539 annotations,
540 from_annotations,
541 } => Protocol::Broadcast {
542 from,
543 to_all,
544 message,
545 continuation: Box::new(append_protocol(*continuation, right)),
546 annotations,
547 from_annotations,
548 },
549 Protocol::Let {
550 name,
551 mode,
552 expr,
553 linear,
554 continuation,
555 } => Protocol::Let {
556 name,
557 mode,
558 expr,
559 linear,
560 continuation: Box::new(append_protocol(*continuation, right)),
561 },
562 Protocol::Publish {
563 event,
564 arg,
565 continuation,
566 } => Protocol::Publish {
567 event,
568 arg,
569 continuation: Box::new(append_protocol(*continuation, right)),
570 },
571 Protocol::PublishAuthority {
572 witness,
573 publication_name,
574 continuation,
575 } => Protocol::PublishAuthority {
576 witness,
577 publication_name,
578 continuation: Box::new(append_protocol(*continuation, right)),
579 },
580 Protocol::Materialize {
581 proof,
582 publication,
583 continuation,
584 } => Protocol::Materialize {
585 proof,
586 publication,
587 continuation: Box::new(append_protocol(*continuation, right)),
588 },
589 Protocol::Handoff {
590 operation,
591 target,
592 receipt,
593 continuation,
594 } => Protocol::Handoff {
595 operation,
596 target,
597 receipt,
598 continuation: Box::new(append_protocol(*continuation, right)),
599 },
600 Protocol::DependentWork {
601 name,
602 arg,
603 required_for,
604 continuation,
605 } => Protocol::DependentWork {
606 name,
607 arg,
608 required_for,
609 continuation: Box::new(append_protocol(*continuation, right)),
610 },
611 Protocol::Extension {
612 extension,
613 continuation,
614 annotations,
615 } => Protocol::Extension {
616 extension,
617 continuation: Box::new(append_protocol(*continuation, right)),
618 annotations,
619 },
620 Protocol::Choice {
621 role,
622 branches,
623 annotations,
624 } => Protocol::Choice {
625 role,
626 branches: {
627 let mut iter = branches.into_vec().into_iter().map(|mut branch| {
628 branch.protocol = append_protocol(branch.protocol, right.clone());
629 branch
630 });
631 let first = iter.next().expect("choice branches are non-empty");
632 crate::ast::NonEmptyVec::from_head_tail(first, iter.collect())
633 },
634 annotations,
635 },
636 Protocol::Case { expr, branches } => Protocol::Case {
637 expr,
638 branches: {
639 let mut iter = branches.into_vec().into_iter().map(|mut branch| {
640 branch.protocol = append_protocol(branch.protocol, right.clone());
641 branch
642 });
643 let first = iter.next().expect("case branches are non-empty");
644 crate::ast::NonEmptyVec::from_head_tail(first, iter.collect())
645 },
646 },
647 Protocol::Timeout {
648 role,
649 duration_ms,
650 body,
651 on_timeout,
652 on_cancel,
653 } => Protocol::Timeout {
654 role,
655 duration_ms,
656 body: Box::new(append_protocol(*body, right.clone())),
657 on_timeout: Box::new(append_protocol(*on_timeout, right.clone())),
658 on_cancel: on_cancel.map(|branch| Box::new(append_protocol(*branch, right.clone()))),
659 },
660 Protocol::Loop { condition, body } => Protocol::Loop {
661 condition,
662 body: Box::new(append_protocol(*body, right)),
663 },
664 Protocol::Parallel { protocols } => Protocol::Parallel {
665 protocols: {
666 let mut iter = protocols
667 .into_vec()
668 .into_iter()
669 .map(|protocol| append_protocol(protocol, right.clone()));
670 let first = iter.next().expect("parallel branches are non-empty");
671 crate::ast::NonEmptyVec::from_head_tail(first, iter.collect())
672 },
673 },
674 Protocol::Rec { label, body } => Protocol::Rec {
675 label,
676 body: Box::new(append_protocol(*body, right)),
677 },
678 Protocol::Var(label) => Protocol::Var(label),
679 }
680}
681
682fn build_chunk_protocol_source(roles: &[Role], chunk: &str) -> String {
683 let role_list = roles
684 .iter()
685 .map(|role| role.name().to_string())
686 .collect::<Vec<_>>()
687 .join(", ");
688 let normalized = strip_common_indent(chunk);
689 let body = normalized
690 .lines()
691 .map(|line| {
692 if line.is_empty() {
693 String::new()
694 } else {
695 format!(" {line}")
696 }
697 })
698 .collect::<Vec<_>>()
699 .join("\n");
700 format!("protocol __ExtensionChunk =\n roles {role_list}\n{body}\n")
701}
702
703fn split_protocol_body_chunks(
704 input: &str,
705 bindings: &[ExtensionBinding<'_>],
706) -> Option<ProtocolBodySplit> {
707 let lines: Vec<&str> = input.lines().collect();
708 let protocol_idx = lines
709 .iter()
710 .position(|line| line.trim_start().starts_with("protocol "))?;
711
712 let mut cursor = protocol_idx + 1;
713 while cursor < lines.len() && is_blank_or_comment(lines[cursor]) {
714 cursor += 1;
715 }
716 if cursor < lines.len() && lines[cursor].trim_start().starts_with("roles ") {
717 cursor += 1;
718 }
719 while cursor < lines.len() && is_blank_or_comment(lines[cursor]) {
720 cursor += 1;
721 }
722 if cursor >= lines.len() {
723 return Some(ProtocolBodySplit {
724 stripped_input: input.to_string(),
725 chunks: Vec::new(),
726 });
727 }
728
729 let body_indent = indentation_width(lines[cursor]);
730 let mut working_lines = lines
731 .iter()
732 .map(|line| (*line).to_string())
733 .collect::<Vec<_>>();
734 let mut chunks = Vec::new();
735 let mut idx = cursor;
736
737 while idx < lines.len() {
738 let trimmed = lines[idx].trim_start();
739 if trimmed.is_empty() || trimmed.starts_with("--") {
740 idx += 1;
741 continue;
742 }
743 let indent = indentation_width(lines[idx]);
744 if indent < body_indent || (indent == body_indent && trimmed.starts_with("where")) {
745 break;
746 }
747 if indent > body_indent {
748 idx += 1;
749 continue;
750 }
751
752 let start = idx;
753 idx += 1;
754 while idx < lines.len() {
755 let trimmed = lines[idx].trim_start();
756 if trimmed.is_empty() || trimmed.starts_with("--") {
757 idx += 1;
758 continue;
759 }
760 let indent = indentation_width(lines[idx]);
761 if indent < body_indent || (indent == body_indent && trimmed.starts_with("where")) {
762 break;
763 }
764 if indent == body_indent {
765 break;
766 }
767 idx += 1;
768 }
769
770 let chunk_text = lines[start..idx].join("\n");
771 let extension_binding = detect_extension_binding(lines[start], bindings);
772 if extension_binding.is_some() {
773 for line in &mut working_lines[start..idx] {
774 line.clear();
775 }
776 }
777 chunks.push(StatementChunk {
778 text: chunk_text,
779 extension_binding,
780 });
781 }
782
783 let mut stripped_input = working_lines.join("\n");
784 if input.ends_with('\n') {
785 stripped_input.push('\n');
786 }
787 Some(ProtocolBodySplit {
788 stripped_input,
789 chunks,
790 })
791}
792
793fn detect_extension_binding(line: &str, bindings: &[ExtensionBinding<'_>]) -> Option<usize> {
794 let trimmed = line.trim_start();
795 bindings
796 .iter()
797 .position(|binding| starts_with_statement_prefix(trimmed, &binding.prefix))
798}
799
800fn input_contains_extension_candidates(input: &str, bindings: &[ExtensionBinding<'_>]) -> bool {
801 input
802 .lines()
803 .any(|line| detect_extension_binding(line, bindings).is_some())
804}
805
806fn collect_extension_bindings<'a>(registry: &'a ExtensionRegistry) -> Vec<ExtensionBinding<'a>> {
807 let mut bindings = Vec::new();
808 for rule_name in registry.statement_rules() {
809 let Some(prefix) = extension_rule_prefix(registry, rule_name) else {
810 continue;
811 };
812 let Some(parser_id) = registry.get_parser_for_rule(rule_name) else {
813 continue;
814 };
815 let Some(parser) = registry.get_statement_parser(parser_id) else {
816 continue;
817 };
818 bindings.push(ExtensionBinding {
819 rule_name: rule_name.to_string(),
820 parser,
821 prefix,
822 });
823 }
824 bindings.sort_by_key(|right| std::cmp::Reverse(right.prefix.len()));
825 bindings
826}
827
828fn extension_rule_prefix(registry: &ExtensionRegistry, rule_name: &str) -> Option<String> {
829 registry.grammar_extensions().find_map(|extension| {
830 if !extension.statement_rules().contains(&rule_name) {
831 return None;
832 }
833 extract_rule_prefix(extension.grammar_rules(), rule_name)
834 })
835}
836
837fn extract_rule_prefix(grammar_rules: &str, rule_name: &str) -> Option<String> {
838 let pattern = format!(r#"{rule_name}\s*=\s*\{{\s*"([^"]+)""#);
839 let regex = Regex::new(&pattern).ok()?;
840 regex
841 .captures(grammar_rules)
842 .and_then(|captures| captures.get(1).map(|matched| matched.as_str().to_string()))
843}
844
845fn starts_with_statement_prefix(line: &str, prefix: &str) -> bool {
846 let Some(rest) = line.strip_prefix(prefix) else {
847 return false;
848 };
849 rest.is_empty()
850 || rest
851 .chars()
852 .next()
853 .is_some_and(|ch| ch.is_whitespace() || matches!(ch, '(' | '{' | '"' | '\''))
854}
855
856fn indentation_width(line: &str) -> usize {
857 line.len() - line.trim_start_matches([' ', '\t']).len()
858}
859
860fn is_blank_or_comment(line: &str) -> bool {
861 let trimmed = line.trim_start();
862 trimmed.is_empty() || trimmed.starts_with("--")
863}
864
865fn reject_legacy_structural_braces(input: &str) -> std::result::Result<(), ParseError> {
866 fn line_col(input: &str, offset: usize) -> (usize, usize) {
867 let prefix = &input[..offset];
868 let line = prefix.bytes().filter(|b| *b == b'\n').count() + 1;
869 let column = prefix
870 .rsplit('\n')
871 .next()
872 .map_or(1, |segment| segment.chars().count() + 1);
873 (line, column)
874 }
875
876 fn sanitize(input: &str) -> String {
877 let mut out = String::with_capacity(input.len());
878 let chars: Vec<char> = input.chars().collect();
879 let mut idx = 0usize;
880 let mut in_string = false;
881 let mut in_block_comment = false;
882 let mut escape = false;
883
884 while idx < chars.len() {
885 let ch = chars[idx];
886 let next = chars.get(idx + 1).copied();
887
888 if in_block_comment {
889 if ch == '-' && next == Some('}') {
890 out.push(' ');
891 out.push(' ');
892 in_block_comment = false;
893 idx += 2;
894 continue;
895 }
896 out.push(if ch == '\n' { '\n' } else { ' ' });
897 idx += 1;
898 continue;
899 }
900
901 if in_string {
902 if escape {
903 escape = false;
904 out.push(' ');
905 idx += 1;
906 continue;
907 }
908 if ch == '\\' {
909 escape = true;
910 out.push(' ');
911 idx += 1;
912 continue;
913 }
914 if ch == '"' {
915 in_string = false;
916 out.push(' ');
917 idx += 1;
918 continue;
919 }
920 out.push(if ch == '\n' { '\n' } else { ' ' });
921 idx += 1;
922 continue;
923 }
924
925 if ch == '-' && next == Some('-') {
926 out.push(' ');
927 out.push(' ');
928 idx += 2;
929 while idx < chars.len() {
930 let line_ch = chars[idx];
931 out.push(if line_ch == '\n' { '\n' } else { ' ' });
932 idx += 1;
933 if line_ch == '\n' {
934 break;
935 }
936 }
937 continue;
938 }
939
940 if ch == '{' && next == Some('-') {
941 in_block_comment = true;
942 out.push(' ');
943 out.push(' ');
944 idx += 2;
945 continue;
946 }
947
948 if ch == '"' {
949 in_string = true;
950 out.push(' ');
951 idx += 1;
952 continue;
953 }
954
955 out.push(ch);
956 idx += 1;
957 }
958
959 out
960 }
961
962 let sanitized = sanitize(input);
963 let patterns = [
964 (
965 Regex::new(r"(?s)\bprotocol\b[^{}=\n]*=\s*\{").expect("protocol block regex"),
966 "legacy brace-based protocol blocks are removed; use indentation after `protocol ... =`",
967 ),
968 (
969 Regex::new(r"(?m)\bprotocol\b[^{}=\n]*\{").expect("protocol header brace regex"),
970 "legacy brace-based protocol blocks are removed; keep `=` on the header line and use indentation",
971 ),
972 (
973 Regex::new(r"(?s)\boperation\b[^{}=\n]*=\s*\{").expect("operation block regex"),
974 "legacy brace-based operation blocks are removed; use indentation after `operation ... =`",
975 ),
976 (
977 Regex::new(r"(?s)\bguest\s+runtime\b[^{}=\n]*=\s*\{")
978 .expect("guest runtime block regex"),
979 "legacy brace-based guest runtime blocks are removed; use indentation after `guest runtime ... =`",
980 ),
981 (
982 Regex::new(r"(?m)\bwhere\s*\{").expect("where block regex"),
983 "legacy brace-based `where` blocks are removed; use indentation for local protocol declarations",
984 ),
985 (
986 Regex::new(r"(?s)\bchoice\b.*?\bat\s*\{").expect("choice block regex"),
987 "legacy brace-based choice blocks are removed; use indentation after `choice Role at`",
988 ),
989 (
990 Regex::new(r"(?s)\bcase\b.*?\bof\s*\{").expect("case block regex"),
991 "legacy brace-based case blocks are removed; use indentation after `case ... of`",
992 ),
993 (
994 Regex::new(r"(?s)=>\s*\{").expect("branch body regex"),
995 "legacy brace-based branch bodies are removed; place the branch body on the next indented lines after `=>`",
996 ),
997 (
998 Regex::new(r"(?m)\bpar\s*\{").expect("par block regex"),
999 "legacy brace-based `par` blocks are removed; use indentation with leading `|` branches",
1000 ),
1001 (
1002 Regex::new(r"(?s)\brec\b[^{\n]*\{").expect("rec block regex"),
1003 "legacy brace-based `rec` blocks are removed; use indentation after `rec Name`",
1004 ),
1005 (
1006 Regex::new(r"(?s)\bloop\b.*?\{").expect("loop block regex"),
1007 "legacy brace-based loop blocks are removed; use indentation after the loop header",
1008 ),
1009 (
1010 Regex::new(r"(?s)\btimeout\b.*?\bat\s*\{").expect("timeout block regex"),
1011 "legacy brace-based timeout bodies are removed; use indentation after `timeout duration Role at`",
1012 ),
1013 (
1014 Regex::new(r"(?s)\bon\s+timeout\s*=>\s*\{").expect("timeout branch regex"),
1015 "legacy brace-based timeout branches are removed; use indentation after `on timeout =>`",
1016 ),
1017 (
1018 Regex::new(r"(?s)\bon\s+cancel\s*=>\s*\{").expect("cancel branch regex"),
1019 "legacy brace-based cancel branches are removed; use indentation after `on cancel =>`",
1020 ),
1021 (
1022 Regex::new(r"(?s)\bin\s*\{").expect("let-in block regex"),
1023 "legacy brace-based `let ... in` bodies are removed; use indentation after `in`",
1024 ),
1025 ];
1026
1027 for (pattern, message) in patterns {
1028 if let Some(found) = pattern.find(&sanitized) {
1029 let brace_offset = sanitized[found.start()..found.end()]
1030 .rfind('{')
1031 .map(|idx| found.start() + idx)
1032 .unwrap_or(found.start());
1033 let (line, column) = line_col(input, brace_offset);
1034 return Err(ParseError::Syntax {
1035 span: ErrorSpan::from_line_col(line, column, input),
1036 message: message.to_string(),
1037 });
1038 }
1039 }
1040
1041 Ok(())
1042}
1043
1044fn reject_removed_legacy_surfaces(input: &str) -> std::result::Result<(), ParseError> {
1045 fn line_col(input: &str, offset: usize) -> (usize, usize) {
1046 let prefix = &input[..offset];
1047 let line = prefix.bytes().filter(|b| *b == b'\n').count() + 1;
1048 let column = prefix
1049 .rsplit('\n')
1050 .next()
1051 .map_or(1, |segment| segment.chars().count() + 1);
1052 (line, column)
1053 }
1054
1055 let patterns = [
1056 (
1057 Regex::new(r"(?m)^\s*heartbeat\b").expect("heartbeat regex"),
1058 "legacy DSL construct `heartbeat` was removed from the proof-backed language surface; model liveness with explicit `timeout`, effect outcomes, and progress contracts instead",
1059 ),
1060 (
1061 Regex::new(r"(?m)^\s*handshake\b").expect("handshake regex"),
1062 "legacy DSL construct `handshake` was removed from the proof-backed language surface; express coordination through protocol sends/choices plus explicit semantic handoff or publication when needed",
1063 ),
1064 (
1065 Regex::new(r"(?m)^\s*retry\b").expect("retry regex"),
1066 "legacy DSL construct `retry` was removed from the proof-backed language surface; express retry policy through effects, choices, and progress contracts",
1067 ),
1068 (
1069 Regex::new(r"(?m)^\s*quorum_collect\b").expect("quorum regex"),
1070 "legacy DSL construct `quorum_collect` was removed from the proof-backed language surface; express threshold participation through named agreement profiles plus explicit protocol steps or child-effect aggregation",
1071 ),
1072 (
1073 Regex::new(r"(?m)^\s*(acquire|release|fork|join|abort|delegate|tag)\b")
1074 .expect("protocol machine regex"),
1075 "legacy DSL construct `protocol-machine core statement` was removed from the proof-backed language surface; keep protocol-machine instructions in the runtime model, not in DSL source",
1076 ),
1077 (
1078 Regex::new(r"(?m)^\s*check\s+\w+\s+for\s+").expect("protocol machine check regex"),
1079 "legacy DSL construct `protocol-machine core statement` was removed from the proof-backed language surface; keep protocol-machine instructions in the runtime model, not in DSL source",
1080 ),
1081 (
1082 Regex::new(r"(?m)^\s*transfer\s+\w+\s+to\s+\w+").expect("protocol machine transfer regex"),
1083 "legacy DSL construct `protocol-machine core statement` was removed from the proof-backed language surface; keep protocol-machine instructions in the runtime model, not in DSL source",
1084 ),
1085 ];
1086
1087 for (pattern, message) in patterns {
1088 if let Some(found) = pattern.find(input) {
1089 let (line, column) = line_col(input, found.start());
1090 return Err(ParseError::Syntax {
1091 span: ErrorSpan::from_line_col(line, column, input),
1092 message: message.to_string(),
1093 });
1094 }
1095 }
1096
1097 Ok(())
1098}
1099
1100fn strip_common_indent(input: &str) -> String {
1101 let lines: Vec<&str> = input.lines().collect();
1102 let mut min_indent: Option<usize> = None;
1103
1104 for line in &lines {
1105 if line.trim().is_empty() {
1106 continue;
1107 }
1108 let indent = line.chars().take_while(|c| *c == ' ').count();
1109 min_indent = Some(match min_indent {
1110 Some(current) => current.min(indent),
1111 None => indent,
1112 });
1113 }
1114
1115 let min_indent = min_indent.unwrap_or(0);
1116 if min_indent == 0 {
1117 return input.to_string();
1118 }
1119
1120 let mut out = String::new();
1121 for (idx, line) in lines.iter().enumerate() {
1122 let stripped = line.get(min_indent..).unwrap_or(line);
1123 out.push_str(stripped);
1124 if idx + 1 < lines.len() {
1125 out.push('\n');
1126 }
1127 }
1128
1129 if input.ends_with('\n') {
1130 out.push('\n');
1131 }
1132
1133 out
1134}
1135
1136pub fn parse_choreography(input: TokenStream) -> syn::Result<Choreography> {
1138 use syn::LitStr;
1139
1140 if let Ok(lit_str) = syn::parse2::<LitStr>(input.clone()) {
1141 return Err(syn::Error::new(
1142 lit_str.span(),
1143 "string-literal tell! input was removed; use parse_choreography_str for DSL strings or the tell! proc macro with canonical token syntax",
1144 ));
1145 }
1146
1147 Err(syn::Error::new(
1148 proc_macro2::Span::call_site(),
1149 "proc-macro2 token parsing for the tell! DSL was removed; use parse_choreography_str for DSL text or the tell! proc macro with canonical indentation-based token syntax",
1150 ))
1151}
1152
1153pub fn parse_choreography_file(
1155 path: &std::path::Path,
1156) -> std::result::Result<Choreography, ParseError> {
1157 if path.extension().and_then(std::ffi::OsStr::to_str) != Some(DEFAULT_SOURCE_EXTENSION) {
1158 return Err(ParseError::Syntax {
1159 span: ErrorSpan {
1160 line: 1,
1161 column: 1,
1162 line_end: 1,
1163 column_end: 1,
1164 snippet: format!("Unsupported source file extension: {}", path.display()),
1165 },
1166 message: format!(
1167 "Telltale source files must use the .{DEFAULT_SOURCE_EXTENSION} extension"
1168 ),
1169 });
1170 }
1171
1172 let content = std::fs::read_to_string(path).map_err(|e| ParseError::Syntax {
1173 span: ErrorSpan {
1174 line: 1,
1175 column: 1,
1176 line_end: 1,
1177 column_end: 1,
1178 snippet: format!("Failed to read file: {}", path.display()),
1179 },
1180 message: e.to_string(),
1181 })?;
1182
1183 parse_choreography_str(&content)
1184}
1185
1186pub fn parse_dsl(input: &str) -> std::result::Result<Choreography, ParseError> {
1188 parse_choreography_str(input)
1189}
1190
1191#[must_use]
1193pub fn choreography_macro(input: TokenStream) -> TokenStream {
1194 let source = input.to_string();
1195 let choreography = match parse_choreography(input) {
1196 Ok(c) => c,
1197 Err(e) => return e.to_compile_error(),
1198 };
1199
1200 if let Err(e) = choreography.validate() {
1202 return syn::Error::new(Span::call_site(), e.to_string()).to_compile_error();
1203 }
1204
1205 match crate::generate_protocol_module(&choreography, &source) {
1206 Ok(tokens) => tokens,
1207 Err(err) => err.to_compile_error(),
1208 }
1209}
1210
1211#[cfg(test)]
1212mod tests {
1213 use super::*;
1214 use crate::ast::{Condition, LocalType, Protocol, ValidationError};
1215 use crate::compiler::parser::parse_choreography_str;
1216 use crate::compiler::projection::project;
1217 use proc_macro2::TokenStream;
1218 use tempfile::tempdir;
1219
1220 #[test]
1223 fn test_parse_simple_send() {
1224 let input = r#"
1225protocol SimpleSend =
1226 roles Alice, Bob
1227 Alice -> Bob : Hello
1228"#;
1229
1230 let result = parse_choreography_str(input);
1231 assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
1232
1233 let choreo = result.unwrap();
1234 assert_eq!(choreo.name.to_string(), "SimpleSend");
1235 assert_eq!(choreo.roles.len(), 2);
1236 }
1237
1238 #[test]
1239 fn test_parse_with_choice() {
1240 let input = r#"
1241protocol Negotiation =
1242 roles Buyer, Seller
1243 Buyer -> Seller : Offer
1244 choice Seller at
1245 | accept =>
1246 Seller -> Buyer : Accept
1247 | reject =>
1248 Seller -> Buyer : Reject
1249"#;
1250
1251 let result = parse_choreography_str(input);
1252 assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
1253
1254 let choreo = result.unwrap();
1255 assert_eq!(choreo.name.to_string(), "Negotiation");
1256 }
1257
1258 #[test]
1259 fn test_parse_choice_alias() {
1260 let input = r#"
1261protocol AliasChoice =
1262 roles A, B
1263 choice A at
1264 | ok =>
1265 A -> B : Ack
1266 | fail =>
1267 A -> B : Nack
1268"#;
1269
1270 let result = parse_choreography_str(input);
1271 assert!(
1272 result.is_ok(),
1273 "Failed to parse alias choice: {:?}",
1274 result.err()
1275 );
1276 }
1277
1278 #[test]
1279 fn test_parse_undefined_role() {
1280 let input = r#"
1281protocol Invalid =
1282 roles Alice
1283 Alice -> Bob : Hello
1284"#;
1285
1286 let result = parse_choreography_str(input);
1287 assert!(result.is_err());
1288 let err = result.unwrap_err();
1289 assert!(matches!(err, ParseError::UndefinedRole { .. }));
1290
1291 let err_str = err.to_string();
1293 assert!(err_str.contains("Undefined role"));
1294 assert!(err_str.contains("Bob"));
1295 }
1296
1297 #[test]
1298 fn test_parse_duplicate_role() {
1299 let input = r#"
1300protocol DuplicateRole =
1301 roles Alice, Bob, Alice
1302 Alice -> Bob : Hello
1303"#;
1304
1305 let result = parse_choreography_str(input);
1306 assert!(result.is_err());
1307 let err = result.unwrap_err();
1308 assert!(matches!(err, ParseError::DuplicateRole { .. }));
1309
1310 let err_str = err.to_string();
1312 assert!(err_str.contains("Duplicate role"));
1313 assert!(err_str.contains("Alice"));
1314 }
1315
1316 #[test]
1317 fn test_parse_loop_repeat() {
1318 let input = r#"
1319protocol LoopProtocol =
1320 roles Client, Server
1321 loop repeat 3
1322 Client -> Server : Request
1323 Server -> Client : Response
1324"#;
1325
1326 let result = parse_choreography_str(input);
1327 assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
1328 }
1329
1330 #[test]
1331 fn test_parse_loop_decide() {
1332 let input = r#"
1333protocol DecideLoop =
1334 roles Client, Server
1335 loop decide by Client
1336 Client -> Server : Ping
1337 Server -> Client : Pong
1338"#;
1339
1340 let result = parse_choreography_str(input);
1341 assert!(
1342 result.is_ok(),
1343 "Failed to parse decide loop: {:?}",
1344 result.err()
1345 );
1346 }
1347
1348 #[test]
1349 fn test_role_decides_desugaring() {
1350 let input = r#"
1360protocol DecideLoop =
1361 roles Client, Server
1362 loop decide by Client
1363 Client -> Server : Ping
1364 Server -> Client : Pong
1365"#;
1366
1367 let result = parse_choreography_str(input);
1368 assert!(
1369 result.is_ok(),
1370 "Failed to parse decide loop: {:?}",
1371 result.err()
1372 );
1373
1374 let choreo = result.unwrap();
1375 match &choreo.protocol {
1376 Protocol::Rec { label, body } => {
1377 assert_eq!(label.to_string(), "RoleDecidesLoop");
1378 match body.as_ref() {
1379 Protocol::Choice { role, branches, .. } => {
1380 assert_eq!(role.name().to_string(), "Client");
1381 assert_eq!(branches.len(), 2);
1382
1383 let continue_branch = branches.first();
1385 assert_eq!(continue_branch.label.to_string(), "Ping");
1386
1387 match &continue_branch.protocol {
1389 Protocol::Send {
1390 from,
1391 to,
1392 message,
1393 continuation,
1394 ..
1395 } => {
1396 assert_eq!(from.name().to_string(), "Client");
1397 assert_eq!(to.name().to_string(), "Server");
1398 assert_eq!(message.name.to_string(), "Ping");
1399
1400 match continuation.as_ref() {
1402 Protocol::Send {
1403 from,
1404 to,
1405 message,
1406 continuation,
1407 ..
1408 } => {
1409 assert_eq!(from.name().to_string(), "Server");
1410 assert_eq!(to.name().to_string(), "Client");
1411 assert_eq!(message.name.to_string(), "Pong");
1412
1413 match continuation.as_ref() {
1415 Protocol::Var(label) => {
1416 assert_eq!(label.to_string(), "RoleDecidesLoop");
1417 }
1418 _ => panic!(
1419 "Expected Var for continue, got {:?}",
1420 continuation
1421 ),
1422 }
1423 }
1424 _ => panic!("Expected Send for Pong, got {:?}", continuation),
1425 }
1426 }
1427 _ => {
1428 panic!("Expected Send for Ping, got {:?}", continue_branch.protocol)
1429 }
1430 }
1431
1432 let done_branch = &branches.as_slice()[1];
1434 assert_eq!(done_branch.label.to_string(), "Done");
1435 match &done_branch.protocol {
1436 Protocol::Send {
1437 from,
1438 to,
1439 message,
1440 continuation,
1441 ..
1442 } => {
1443 assert_eq!(from.name().to_string(), "Client");
1444 assert_eq!(to.name().to_string(), "Server");
1445 assert_eq!(message.name.to_string(), "Done");
1446 assert!(matches!(continuation.as_ref(), Protocol::End));
1447 }
1448 _ => panic!("Expected Send for Done, got {:?}", done_branch.protocol),
1449 }
1450 }
1451 _ => panic!("Expected Choice inside Rec, got {:?}", body),
1452 }
1453 }
1454 _ => panic!("Expected Rec at top level, got {:?}", choreo.protocol),
1455 }
1456 }
1457
1458 #[test]
1459 fn test_role_decides_wrong_first_sender_no_desugar() {
1460 let input = r#"
1463protocol WrongSender =
1464 roles Client, Server
1465 loop decide by Client
1466 Server -> Client : Response
1467 Client -> Server : Ack
1468"#;
1469
1470 let result = parse_choreography_str(input);
1471 assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
1472
1473 let choreo = result.unwrap();
1474 match &choreo.protocol {
1476 Protocol::Loop { condition, .. } => match condition {
1477 Some(Condition::RoleDecides(role)) => {
1478 assert_eq!(role.name().to_string(), "Client");
1479 }
1480 _ => panic!("Expected RoleDecides condition"),
1481 },
1482 _ => panic!(
1483 "Expected Loop (not desugared) when first sender doesn't match deciding role"
1484 ),
1485 }
1486 }
1487
1488 #[test]
1489 fn test_role_decides_single_message() {
1490 let input = r#"
1492protocol SingleMessage =
1493 roles A, B
1494 loop decide by A
1495 A -> B : Msg
1496"#;
1497
1498 let result = parse_choreography_str(input);
1499 assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
1500
1501 let choreo = result.unwrap();
1502 match &choreo.protocol {
1503 Protocol::Rec { label, body } => {
1504 assert_eq!(label.to_string(), "RoleDecidesLoop");
1505 match body.as_ref() {
1506 Protocol::Choice { role, branches, .. } => {
1507 assert_eq!(role.name().to_string(), "A");
1508 assert_eq!(branches.len(), 2);
1509
1510 let continue_branch = branches.first();
1512 assert_eq!(continue_branch.label.to_string(), "Msg");
1513 match &continue_branch.protocol {
1514 Protocol::Send {
1515 message,
1516 continuation,
1517 ..
1518 } => {
1519 assert_eq!(message.name.to_string(), "Msg");
1520 assert!(matches!(continuation.as_ref(), Protocol::Var(_)));
1522 }
1523 _ => panic!("Expected Send"),
1524 }
1525
1526 let done_branch = &branches.as_slice()[1];
1528 assert_eq!(done_branch.label.to_string(), "Done");
1529 }
1530 _ => panic!("Expected Choice"),
1531 }
1532 }
1533 _ => panic!("Expected Rec"),
1534 }
1535 }
1536
1537 #[test]
1538 fn test_role_decides_three_roles() {
1539 let input = r#"
1541protocol ThreeRoles =
1542 roles Client, Server, Logger
1543 loop decide by Client
1544 Client -> Server : Request
1545 Server -> Logger : Log
1546 Logger -> Client : Ack
1547"#;
1548
1549 let result = parse_choreography_str(input);
1550 assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
1551
1552 let choreo = result.unwrap();
1553 match &choreo.protocol {
1554 Protocol::Rec { body, .. } => {
1555 match body.as_ref() {
1556 Protocol::Choice { role, branches, .. } => {
1557 assert_eq!(role.name().to_string(), "Client");
1558
1559 let continue_branch = branches.first();
1560 assert_eq!(continue_branch.label.to_string(), "Request");
1561
1562 match &continue_branch.protocol {
1564 Protocol::Send {
1565 from,
1566 to,
1567 message,
1568 continuation,
1569 ..
1570 } => {
1571 assert_eq!(from.name().to_string(), "Client");
1572 assert_eq!(to.name().to_string(), "Server");
1573 assert_eq!(message.name.to_string(), "Request");
1574
1575 match continuation.as_ref() {
1576 Protocol::Send {
1577 from,
1578 to,
1579 message,
1580 continuation,
1581 ..
1582 } => {
1583 assert_eq!(from.name().to_string(), "Server");
1584 assert_eq!(to.name().to_string(), "Logger");
1585 assert_eq!(message.name.to_string(), "Log");
1586
1587 match continuation.as_ref() {
1588 Protocol::Send {
1589 from,
1590 to,
1591 message,
1592 continuation,
1593 ..
1594 } => {
1595 assert_eq!(from.name().to_string(), "Logger");
1596 assert_eq!(to.name().to_string(), "Client");
1597 assert_eq!(message.name.to_string(), "Ack");
1598 assert!(matches!(
1599 continuation.as_ref(),
1600 Protocol::Var(_)
1601 ));
1602 }
1603 _ => panic!("Expected Send for Ack"),
1604 }
1605 }
1606 _ => panic!("Expected Send for Log"),
1607 }
1608 }
1609 _ => panic!("Expected Send for Request"),
1610 }
1611
1612 let done_branch = &branches.as_slice()[1];
1614 match &done_branch.protocol {
1615 Protocol::Send { from, to, .. } => {
1616 assert_eq!(from.name().to_string(), "Client");
1617 assert_eq!(to.name().to_string(), "Server");
1618 }
1619 _ => panic!("Expected Send in Done branch"),
1620 }
1621 }
1622 _ => panic!("Expected Choice"),
1623 }
1624 }
1625 _ => panic!("Expected Rec"),
1626 }
1627 }
1628
1629 #[test]
1630 fn test_role_decides_with_type_annotation() {
1631 let input = r#"
1633protocol TypedLoop =
1634 roles Client, Server
1635 loop decide by Client
1636 Client -> Server : Request of builtins.String
1637 Server -> Client : Response of builtins.U32
1638"#;
1639
1640 let result = parse_choreography_str(input);
1641 assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
1642
1643 let choreo = result.unwrap();
1644 match &choreo.protocol {
1645 Protocol::Rec { body, .. } => match body.as_ref() {
1646 Protocol::Choice { branches, .. } => {
1647 let continue_branch = branches.first();
1648 match &continue_branch.protocol {
1649 Protocol::Send {
1650 message,
1651 continuation,
1652 ..
1653 } => {
1654 assert_eq!(message.name.to_string(), "Request");
1655 assert!(message.payload.is_some());
1656 let type_str = message.payload.as_ref().unwrap().to_string();
1657 assert!(
1658 type_str.contains("String"),
1659 "Expected String type, got: {}",
1660 type_str
1661 );
1662
1663 match continuation.as_ref() {
1664 Protocol::Send { message, .. } => {
1665 assert_eq!(message.name.to_string(), "Response");
1666 assert!(message.payload.is_some());
1667 let type_str = message.payload.as_ref().unwrap().to_string();
1668 assert!(
1669 type_str.contains("U32"),
1670 "Expected U32 type, got: {}",
1671 type_str
1672 );
1673 }
1674 _ => panic!("Expected Send for Response"),
1675 }
1676 }
1677 _ => panic!("Expected Send for Request"),
1678 }
1679 }
1680 _ => panic!("Expected Choice"),
1681 },
1682 _ => panic!("Expected Rec"),
1683 }
1684 }
1685
1686 #[test]
1687 fn test_role_decides_first_stmt_is_choice_no_desugar() {
1688 let input = r#"
1691protocol FirstIsChoice =
1692 roles A, B
1693 loop decide by A
1694 choice A at
1695 | opt1 =>
1696 A -> B : Msg1
1697 | opt2 =>
1698 A -> B : Msg2
1699"#;
1700
1701 let result = parse_choreography_str(input);
1702 assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
1703
1704 let choreo = result.unwrap();
1705 match &choreo.protocol {
1707 Protocol::Loop { condition, body } => {
1708 match condition {
1709 Some(Condition::RoleDecides(role)) => {
1710 assert_eq!(role.name().to_string(), "A");
1711 }
1712 _ => panic!("Expected RoleDecides condition"),
1713 }
1714 match body.as_ref() {
1716 Protocol::Choice { .. } => {}
1717 _ => panic!("Expected Choice in body"),
1718 }
1719 }
1720 _ => panic!("Expected Loop (not desugared) when first statement is not a Send"),
1721 }
1722 }
1723
1724 #[test]
1727 fn test_role_decides_followed_by_statements() {
1728 let input = r#"
1730protocol LoopThenMore =
1731 roles A, B
1732 loop decide by A
1733 A -> B : Request
1734 B -> A : Response
1735 A -> B : Goodbye
1736"#;
1737
1738 let result = parse_choreography_str(input);
1739 assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
1740
1741 let choreo = result.unwrap();
1742 match &choreo.protocol {
1744 Protocol::Rec { body, .. } => {
1745 match body.as_ref() {
1746 Protocol::Choice { branches, .. } => {
1747 let done_branch = &branches.as_slice()[1];
1749 match &done_branch.protocol {
1750 Protocol::Send {
1751 message,
1752 continuation,
1753 ..
1754 } => {
1755 assert_eq!(message.name.to_string(), "Done");
1756 match continuation.as_ref() {
1758 Protocol::Send { message, .. } => {
1759 assert_eq!(message.name.to_string(), "Goodbye");
1760 }
1761 _ => panic!("Expected Goodbye after Done"),
1762 }
1763 }
1764 _ => panic!("Expected Send in Done branch"),
1765 }
1766 }
1767 _ => panic!("Expected Choice"),
1768 }
1769 }
1770 _ => panic!("Expected Rec"),
1771 }
1772 }
1773
1774 #[test]
1775 fn test_role_decides_multiple_loops() {
1776 let input = r#"
1778protocol TwoLoops =
1779 roles A, B
1780 loop decide by A
1781 A -> B : First
1782 loop decide by B
1783 B -> A : Second
1784"#;
1785
1786 let result = parse_choreography_str(input);
1787 assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
1788
1789 let choreo = result.unwrap();
1790 match &choreo.protocol {
1792 Protocol::Rec { label, body } => {
1793 assert_eq!(label.to_string(), "RoleDecidesLoop");
1794 match body.as_ref() {
1795 Protocol::Choice { role, branches, .. } => {
1796 assert_eq!(role.name().to_string(), "A");
1797
1798 let done_branch = &branches.as_slice()[1];
1800 match &done_branch.protocol {
1801 Protocol::Send { continuation, .. } => {
1802 match continuation.as_ref() {
1804 Protocol::Rec { body, .. } => match body.as_ref() {
1805 Protocol::Choice { role, .. } => {
1806 assert_eq!(role.name().to_string(), "B");
1807 }
1808 _ => panic!("Expected Choice in second loop"),
1809 },
1810 _ => panic!("Expected second Rec after first loop"),
1811 }
1812 }
1813 _ => panic!("Expected Send in Done branch"),
1814 }
1815 }
1816 _ => panic!("Expected Choice in first loop"),
1817 }
1818 }
1819 _ => panic!("Expected Rec"),
1820 }
1821 }
1822
1823 #[test]
1824 fn test_role_decides_empty_body_edge_case() {
1825 let input = r#"
1828protocol EmptyBody =
1829 roles A, B
1830 loop decide by A
1831 A -> B : AfterLoop
1832"#;
1833
1834 let result = parse_choreography_str(input);
1837 if let Ok(choreo) = result {
1839 match &choreo.protocol {
1841 Protocol::Loop { .. } => {
1842 }
1844 Protocol::Send { .. } => {
1845 }
1847 _ => {
1848 }
1850 }
1851 }
1852 }
1854
1855 #[test]
1856 fn test_role_decides_preserves_branch_label_from_message() {
1857 let input = r#"
1859protocol CustomMessageName =
1860 roles Producer, Consumer
1861 loop decide by Producer
1862 Producer -> Consumer : DataChunk
1863 Consumer -> Producer : Ack
1864"#;
1865
1866 let result = parse_choreography_str(input);
1867 assert!(result.is_ok());
1868
1869 let choreo = result.unwrap();
1870 match &choreo.protocol {
1871 Protocol::Rec { body, .. } => {
1872 match body.as_ref() {
1873 Protocol::Choice { branches, .. } => {
1874 let continue_branch = branches.first();
1876 assert_eq!(continue_branch.label.to_string(), "DataChunk");
1877
1878 let done_branch = &branches.as_slice()[1];
1880 assert_eq!(done_branch.label.to_string(), "Done");
1881 }
1882 _ => panic!("Expected Choice"),
1883 }
1884 }
1885 _ => panic!("Expected Rec"),
1886 }
1887 }
1888
1889 #[test]
1890 fn test_role_decides_done_message_targets_same_receiver() {
1891 let input = r#"
1893protocol TargetConsistency =
1894 roles Sender, Receiver, Observer
1895 loop decide by Sender
1896 Sender -> Receiver : Data
1897 Receiver -> Observer : Forward
1898"#;
1899
1900 let result = parse_choreography_str(input);
1901 assert!(result.is_ok());
1902
1903 let choreo = result.unwrap();
1904 match &choreo.protocol {
1905 Protocol::Rec { body, .. } => {
1906 match body.as_ref() {
1907 Protocol::Choice { branches, .. } => {
1908 let continue_branch = branches.first();
1910 match &continue_branch.protocol {
1911 Protocol::Send { to, .. } => {
1912 assert_eq!(to.name().to_string(), "Receiver");
1913 }
1914 _ => panic!("Expected Send"),
1915 }
1916
1917 let done_branch = &branches.as_slice()[1];
1919 match &done_branch.protocol {
1920 Protocol::Send { from, to, .. } => {
1921 assert_eq!(from.name().to_string(), "Sender");
1922 assert_eq!(to.name().to_string(), "Receiver");
1923 }
1924 _ => panic!("Expected Send in Done branch"),
1925 }
1926 }
1927 _ => panic!("Expected Choice"),
1928 }
1929 }
1930 _ => panic!("Expected Rec"),
1931 }
1932 }
1933
1934 #[test]
1935 fn test_parse_parallel_branches() {
1936 let input = r#"
1937protocol Parallel =
1938 roles A, B, C, D
1939 par
1940 | A -> B : Msg1
1941 | C -> D : Msg2
1942"#;
1943
1944 let result = parse_choreography_str(input);
1945 assert!(
1946 result.is_ok(),
1947 "Failed to parse parallel: {:?}",
1948 result.err()
1949 );
1950
1951 let choreo = result.unwrap();
1952 match choreo.protocol {
1953 Protocol::Parallel { protocols } => {
1954 assert_eq!(protocols.len(), 2);
1955 }
1956 _ => panic!("Expected top-level parallel protocol"),
1957 }
1958 }
1959
1960 #[test]
1961 fn test_single_branch_is_error() {
1962 let input = r#"
1963protocol SingleBranch =
1964 roles A, B
1965 par
1966 | A -> B : Msg
1967"#;
1968
1969 let result = parse_choreography_str(input);
1970 assert!(result.is_err());
1971 }
1972
1973 #[test]
1974 fn test_parse_timeout_branch_surface() {
1975 let input = r#"
1976protocol TimedRequest =
1977 roles Alice, Bob
1978 timeout 5s Alice at
1979 Alice -> Bob : Request
1980 on timeout =>
1981 Alice -> Bob : Cancel
1982"#;
1983
1984 let result = parse_choreography_str(input);
1985 assert!(
1986 result.is_ok(),
1987 "Failed to parse timeout surface: {:?}",
1988 result.err()
1989 );
1990
1991 let choreo = result.unwrap();
1992 assert_eq!(choreo.name.to_string(), "TimedRequest");
1993
1994 match &choreo.protocol {
1995 Protocol::Timeout {
1996 role,
1997 duration_ms,
1998 on_cancel,
1999 ..
2000 } => {
2001 assert_eq!(role.name().to_string(), "Alice");
2002 assert_eq!(*duration_ms, 5_000);
2003 assert!(on_cancel.is_none());
2004 }
2005 _ => panic!("Expected Timeout as first protocol"),
2006 }
2007 }
2008
2009 #[test]
2010 fn test_parse_timeout_milliseconds() {
2011 let input = r#"
2012protocol QuickTimeout =
2013 roles Client, Server
2014 timeout 500ms Client at
2015 Server -> Client : Data
2016 on timeout =>
2017 Client -> Server : Abort
2018"#;
2019
2020 let result = parse_choreography_str(input);
2021 assert!(
2022 result.is_ok(),
2023 "Failed to parse timeout with ms: {:?}",
2024 result.err()
2025 );
2026
2027 let choreo = result.unwrap();
2028 match &choreo.protocol {
2029 Protocol::Timeout { duration_ms, .. } => {
2030 assert_eq!(*duration_ms, 500);
2031 }
2032 _ => panic!("Expected Timeout as first protocol"),
2033 }
2034 }
2035
2036 #[test]
2037 fn test_parse_timeout_minutes() {
2038 let input = r#"
2039protocol LongTimeout =
2040 roles A, B
2041 timeout 2m A at
2042 B -> A : Complete
2043 on timeout =>
2044 A -> B : Timeout
2045"#;
2046
2047 let result = parse_choreography_str(input);
2048 assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
2049
2050 let choreo = result.unwrap();
2051 match &choreo.protocol {
2052 Protocol::Timeout { duration_ms, .. } => {
2053 assert_eq!(*duration_ms, 120_000);
2054 }
2055 _ => panic!("Expected Timeout"),
2056 }
2057 }
2058
2059 #[test]
2060 fn test_parse_heartbeat() {
2061 let input = r#"
2062protocol Liveness =
2063 roles Alice, Bob
2064 heartbeat Alice -> Bob every 1s on_missing(3) {
2065 Bob -> Alice : Disconnect
2066 } body {
2067 Alice -> Bob : Data
2068 }
2069"#;
2070
2071 let err = parse_choreography_str(input).expect_err("heartbeat surface should fail");
2072 assert!(err
2073 .to_string()
2074 .contains("legacy DSL construct `heartbeat` was removed"));
2075 }
2076
2077 #[test]
2078 fn test_parse_heartbeat_milliseconds() {
2079 let input = r#"
2080protocol FastHeartbeat =
2081 roles Client, Server
2082 heartbeat Client -> Server every 500ms on_missing(5) {
2083 Server -> Client : Dead
2084 } body {
2085 Client -> Server : Ping
2086 }
2087"#;
2088
2089 let err =
2090 parse_choreography_str(input).expect_err("heartbeat milliseconds surface should fail");
2091 assert!(err
2092 .to_string()
2093 .contains("legacy DSL construct `heartbeat` was removed"));
2094 }
2095
2096 #[test]
2097 fn test_parse_runtime_timeout_annotation() {
2098 let input = r#"
2099protocol TimedRequest =
2100 roles Client, Server
2101 Client { runtime_timeout : 5s } -> Server : Request
2102 Server -> Client : Response
2103"#;
2104
2105 let result = parse_choreography_str(input);
2106 assert!(
2107 result.is_ok(),
2108 "Failed to parse sender-record runtime_timeout: {:?}",
2109 result.err()
2110 );
2111
2112 let choreo = result.unwrap();
2113 match &choreo.protocol {
2114 Protocol::Send {
2115 annotations,
2116 continuation,
2117 ..
2118 } => {
2119 assert!(annotations.has_runtime_timeout());
2121 let timeout = annotations.runtime_timeout().unwrap();
2122 assert_eq!(timeout, std::time::Duration::from_secs(5));
2123
2124 match continuation.as_ref() {
2126 Protocol::Send { annotations, .. } => {
2127 assert!(!annotations.has_runtime_timeout());
2128 }
2129 _ => panic!("Expected Send for Response"),
2130 }
2131 }
2132 _ => panic!("Expected Send for Request"),
2133 }
2134 }
2135
2136 #[test]
2137 fn test_parse_multiline_runtime_timeout_annotation_with_closing_paren_on_own_line() {
2138 let input = r#"
2139protocol TimedRequest =
2140 roles Client, Server
2141 Client {
2142 runtime_timeout : 5s,
2143 }
2144 -> Server : Request
2145 Server -> Client : Response
2146"#;
2147
2148 let result = parse_choreography_str(input);
2149 assert!(
2150 result.is_ok(),
2151 "Failed to parse multiline sender-record runtime_timeout: {:?}",
2152 result.err()
2153 );
2154
2155 let choreo = result.unwrap();
2156 match &choreo.protocol {
2157 Protocol::Send { annotations, .. } => {
2158 assert!(annotations.has_runtime_timeout());
2159 assert_eq!(
2160 annotations.runtime_timeout(),
2161 Some(std::time::Duration::from_secs(5))
2162 );
2163 }
2164 _ => panic!("Expected Send for Request"),
2165 }
2166 }
2167
2168 #[test]
2169 fn test_parse_runtime_timeout_milliseconds() {
2170 let input = r#"
2171protocol QuickCheck =
2172 roles A, B
2173 A { runtime_timeout : 100ms } -> B : Ping
2174"#;
2175
2176 let result = parse_choreography_str(input);
2177 assert!(
2178 result.is_ok(),
2179 "Failed to parse sender-record runtime_timeout with ms: {:?}",
2180 result.err()
2181 );
2182
2183 let choreo = result.unwrap();
2184 match &choreo.protocol {
2185 Protocol::Send { annotations, .. } => {
2186 assert!(annotations.has_runtime_timeout());
2187 let timeout = annotations.runtime_timeout().unwrap();
2188 assert_eq!(timeout, std::time::Duration::from_millis(100));
2189 }
2190 _ => panic!("Expected Send"),
2191 }
2192 }
2193
2194 #[test]
2195 fn test_parse_parallel_annotation() {
2196 let input = r#"
2197protocol Broadcast =
2198 roles Coordinator, Worker
2199 Coordinator { parallel : true } -> Worker : Task
2200"#;
2201
2202 let result = parse_choreography_str(input);
2203 assert!(
2204 result.is_ok(),
2205 "Failed to parse sender-record parallel metadata: {:?}",
2206 result.err()
2207 );
2208
2209 let choreo = result.unwrap();
2210 match &choreo.protocol {
2211 Protocol::Send { annotations, .. } => {
2212 assert!(annotations.has_parallel(), "Expected parallel annotation");
2213 }
2214 _ => panic!("Expected Send"),
2215 }
2216 }
2217
2218 #[test]
2219 fn test_parse_choice_with_bar_prefixed_branches() {
2220 let input = r#"
2221protocol Decision =
2222 roles A, B
2223 choice A at
2224 | Accept =>
2225 A -> B : Ok
2226 | Reject =>
2227 A -> B : No
2228"#;
2229
2230 let result = parse_choreography_str(input);
2231 assert!(
2232 result.is_ok(),
2233 "Failed to parse choice with bar-prefixed branches: {:?}",
2234 result.err()
2235 );
2236
2237 let choreo = result.unwrap();
2238 match &choreo.protocol {
2239 Protocol::Choice { branches, .. } => {
2240 assert_eq!(branches.len(), 2);
2241 assert_eq!(branches.first().label.to_string(), "Accept");
2242 assert_eq!(branches.as_slice()[1].label.to_string(), "Reject");
2243 }
2244 _ => panic!("Expected Choice"),
2245 }
2246 }
2247
2248 #[test]
2249 fn test_parse_par_with_single_line_bar_branches() {
2250 let input = r#"
2251protocol ParallelBars =
2252 roles A, B, C, D
2253 par
2254 | A -> B : Left
2255 | C -> D : Right
2256"#;
2257
2258 let result = parse_choreography_str(input);
2259 assert!(
2260 result.is_ok(),
2261 "Failed to parse `par` with single-line branches: {:?}",
2262 result.err()
2263 );
2264
2265 let choreo = result.unwrap();
2266 match &choreo.protocol {
2267 Protocol::Parallel { protocols } => {
2268 assert_eq!(protocols.len(), 2);
2269 }
2270 _ => panic!("Expected Parallel"),
2271 }
2272 }
2273
2274 #[test]
2275 fn test_parse_par_with_block_branch() {
2276 let input = r#"
2277protocol ParallelBarsBlock =
2278 roles A, B, C, D
2279 par
2280 |
2281 A -> B : Left
2282 B -> A : Ack
2283 | C -> D : Right
2284"#;
2285
2286 let result = parse_choreography_str(input);
2287 assert!(
2288 result.is_ok(),
2289 "Failed to parse `par` with block branch: {:?}",
2290 result.err()
2291 );
2292
2293 let choreo = result.unwrap();
2294 match &choreo.protocol {
2295 Protocol::Parallel { protocols } => {
2296 assert_eq!(protocols.len(), 2);
2297 match &protocols.first() {
2298 Protocol::Send { continuation, .. } => {
2299 assert!(matches!(continuation.as_ref(), Protocol::Send { .. }));
2300 }
2301 _ => panic!("Expected first branch to be a send sequence"),
2302 }
2303 }
2304 _ => panic!("Expected Parallel"),
2305 }
2306 }
2307
2308 #[test]
2309 fn test_reject_par_without_bar_branches() {
2310 let input = r#"
2311protocol ParallelMissingBars =
2312 roles A, B, C, D
2313 par
2314 A -> B : Left
2315 C -> D : Right
2316"#;
2317
2318 let result = parse_choreography_str(input);
2319 assert!(
2320 result.is_err(),
2321 "`par` branches must be introduced with `|`"
2322 );
2323 }
2324
2325 #[test]
2326 fn test_parse_sender_role_annotation_block() {
2327 let input = r#"
2328protocol RoleAnnotatedSend =
2329 roles Role, OtherRole
2330 Role {
2331 annotation1 : "value",
2332 annotation2 : 100,
2333 annotation3 : another,
2334 } -> OtherRole : Message of crate.Type
2335"#;
2336
2337 let result = parse_choreography_str(input);
2338 assert!(
2339 result.is_ok(),
2340 "Failed to parse sender role annotation block: {:?}",
2341 result.err()
2342 );
2343
2344 let choreo = result.unwrap();
2345 match &choreo.protocol {
2346 Protocol::Send {
2347 from,
2348 to,
2349 message,
2350 from_annotations,
2351 ..
2352 } => {
2353 assert_eq!(from.name().to_string(), "Role");
2354 assert_eq!(to.name().to_string(), "OtherRole");
2355 assert_eq!(message.name.to_string(), "Message");
2356 assert_eq!(
2357 message.payload.as_ref().map(ToString::to_string),
2358 Some("crate :: Type".to_string())
2359 );
2360 assert_eq!(from_annotations.custom("annotation1"), Some("value"));
2361 assert_eq!(from_annotations.custom("annotation2"), Some("100"));
2362 assert_eq!(from_annotations.custom("annotation3"), Some("another"));
2363 }
2364 _ => panic!("Expected Send"),
2365 }
2366 }
2367
2368 #[test]
2369 fn test_parse_sender_record_with_aligned_arrow_layout() {
2370 let input = r#"
2371protocol StyledSend =
2372 roles Buyer, Seller
2373 Buyer { priority : high }
2374 -> Seller : Request of shop.Order
2375"#;
2376
2377 let result = parse_choreography_str(input);
2378 assert!(
2379 result.is_ok(),
2380 "Failed to parse aligned-arrow sender record syntax: {:?}",
2381 result.err()
2382 );
2383
2384 let choreo = result.unwrap();
2385 match &choreo.protocol {
2386 Protocol::Send {
2387 from_annotations,
2388 message,
2389 ..
2390 } => {
2391 assert_eq!(from_annotations.custom("priority"), Some("high"));
2392 assert_eq!(
2393 message.payload.as_ref().map(ToString::to_string),
2394 Some("shop :: Order".to_string())
2395 );
2396 }
2397 _ => panic!("Expected Send"),
2398 }
2399 }
2400
2401 #[test]
2402 fn test_parse_sender_role_annotation_block_with_indexed_role() {
2403 let input = r#"
2404protocol RoleAnnotatedIndexedSend =
2405 roles Worker[N], Coordinator
2406 Worker[0] {
2407 shard : 0,
2408 } -> Coordinator : Result
2409"#;
2410
2411 let result = parse_choreography_str(input);
2412 assert!(
2413 result.is_ok(),
2414 "Failed to parse sender annotation block on indexed role: {:?}",
2415 result.err()
2416 );
2417
2418 let choreo = result.unwrap();
2419 match &choreo.protocol {
2420 Protocol::Send {
2421 from,
2422 from_annotations,
2423 ..
2424 } => {
2425 assert_eq!(from.name().to_string(), "Worker");
2426 assert_eq!(
2427 from.index().as_ref().map(ToString::to_string),
2428 Some("0".to_string())
2429 );
2430 assert_eq!(from_annotations.custom("shard"), Some("0"));
2431 }
2432 _ => panic!("Expected Send"),
2433 }
2434 }
2435
2436 #[test]
2437 fn test_parse_sender_role_annotation_block_on_broadcast() {
2438 let input = r#"
2439protocol RoleAnnotatedBroadcast =
2440 roles Coordinator, Worker
2441 Coordinator {
2442 batch_size : 100,
2443 } ->* : Task
2444"#;
2445
2446 let result = parse_choreography_str(input);
2447 assert!(
2448 result.is_ok(),
2449 "Failed to parse sender annotation block on broadcast: {:?}",
2450 result.err()
2451 );
2452
2453 let choreo = result.unwrap();
2454 match &choreo.protocol {
2455 Protocol::Broadcast {
2456 from,
2457 from_annotations,
2458 ..
2459 } => {
2460 assert_eq!(from.name().to_string(), "Coordinator");
2461 assert_eq!(from_annotations.custom("batch_size"), Some("100"));
2462 }
2463 _ => panic!("Expected Broadcast"),
2464 }
2465 }
2466
2467 #[test]
2468 fn test_parse_sender_role_annotation_ident_list_value() {
2469 let input = r#"
2470protocol RoleAnnotatedSend =
2471 roles Alice, Bob
2472 Alice {
2473 leak : (External, Neighbor),
2474 } -> Bob : Message
2475"#;
2476
2477 let result = parse_choreography_str(input);
2478 assert!(
2479 result.is_ok(),
2480 "Failed to parse sender annotation block with ident-list value: {:?}",
2481 result.err()
2482 );
2483
2484 let choreo = result.unwrap();
2485 match &choreo.protocol {
2486 Protocol::Send {
2487 from_annotations, ..
2488 } => {
2489 assert_eq!(
2490 from_annotations.custom("leak"),
2491 Some("(External, Neighbor)")
2492 );
2493 }
2494 _ => panic!("Expected Send"),
2495 }
2496 }
2497
2498 #[test]
2499 fn test_parse_sender_role_annotation_integer_list_value() {
2500 let input = r#"
2501protocol RoleAnnotatedSend =
2502 roles Alice, Bob
2503 Alice {
2504 leakage_budget : [1, 0, 0],
2505 } -> Bob : Message
2506"#;
2507
2508 let result = parse_choreography_str(input);
2509 assert!(
2510 result.is_ok(),
2511 "Failed to parse sender annotation block with integer-list value: {:?}",
2512 result.err()
2513 );
2514
2515 let choreo = result.unwrap();
2516 match &choreo.protocol {
2517 Protocol::Send {
2518 from_annotations, ..
2519 } => {
2520 assert_eq!(from_annotations.custom("leakage_budget"), Some("[1, 0, 0]"));
2521 }
2522 _ => panic!("Expected Send"),
2523 }
2524 }
2525
2526 #[test]
2527 fn test_reject_sender_metadata_in_square_brackets() {
2528 let input = r#"
2529protocol InvalidRoleMetadata =
2530 roles Role, OtherRole
2531 Role[annotation1 : "value"] -> OtherRole : Message
2532"#;
2533
2534 let result = parse_choreography_str(input);
2535 assert!(
2536 result.is_err(),
2537 "square brackets must stay reserved for role indexing"
2538 );
2539 }
2540
2541 #[test]
2542 fn test_parse_ordered_annotation() {
2543 let input = r#"
2544protocol OrderedCollect =
2545 roles Coordinator, Worker
2546 Worker { ordered : true } -> Coordinator : Result
2547"#;
2548
2549 let result = parse_choreography_str(input);
2550 assert!(
2551 result.is_ok(),
2552 "Failed to parse sender-record ordered metadata: {:?}",
2553 result.err()
2554 );
2555
2556 let choreo = result.unwrap();
2557 match &choreo.protocol {
2558 Protocol::Send { annotations, .. } => {
2559 assert!(annotations.has_ordered(), "Expected ordered annotation");
2560 }
2561 _ => panic!("Expected Send"),
2562 }
2563 }
2564
2565 #[test]
2568 fn test_parse_min_responses_annotation() {
2569 let input = r#"
2570protocol ThresholdSign =
2571 roles Coordinator, Signer
2572 Signer { min_responses : 3 } -> Coordinator : Signature
2573"#;
2574
2575 let result = parse_choreography_str(input);
2576 assert!(
2577 result.is_ok(),
2578 "Failed to parse sender-record min_responses: {:?}",
2579 result.err()
2580 );
2581
2582 let choreo = result.unwrap();
2583 match &choreo.protocol {
2584 Protocol::Send { annotations, .. } => {
2585 assert!(
2586 annotations.has_min_responses(),
2587 "Expected min_responses annotation"
2588 );
2589 assert_eq!(annotations.min_responses(), Some(3));
2590 }
2591 _ => panic!("Expected Send"),
2592 }
2593 }
2594
2595 #[test]
2596 fn test_parse_multiline_min_responses_annotation_with_closing_paren_on_own_line() {
2597 let input = r#"
2598protocol ThresholdSign =
2599 roles Coordinator, Signer
2600 Signer {
2601 min_responses : 3,
2602 }
2603 -> Coordinator : Signature
2604"#;
2605
2606 let result = parse_choreography_str(input);
2607 assert!(
2608 result.is_ok(),
2609 "Failed to parse multiline sender-record min_responses: {:?}",
2610 result.err()
2611 );
2612
2613 let choreo = result.unwrap();
2614 match &choreo.protocol {
2615 Protocol::Send { annotations, .. } => {
2616 assert!(
2617 annotations.has_min_responses(),
2618 "Expected multiline min_responses annotation"
2619 );
2620 assert_eq!(annotations.min_responses(), Some(3));
2621 }
2622 _ => panic!("Expected Send"),
2623 }
2624 }
2625
2626 #[test]
2627 fn test_parse_combined_annotations() {
2628 let input = r#"
2629protocol ParallelThreshold =
2630 roles Coordinator, Worker
2631 Worker {
2632 parallel : true,
2633 min_responses : 2,
2634 } -> Coordinator : Vote
2635"#;
2636
2637 let result = parse_choreography_str(input);
2638 assert!(
2639 result.is_ok(),
2640 "Failed to parse combined sender-record metadata: {:?}",
2641 result.err()
2642 );
2643
2644 let choreo = result.unwrap();
2645 match &choreo.protocol {
2646 Protocol::Send { annotations, .. } => {
2647 assert!(annotations.has_parallel(), "Expected parallel annotation");
2648 assert!(
2649 annotations.has_min_responses(),
2650 "Expected min_responses annotation"
2651 );
2652 assert_eq!(annotations.min_responses(), Some(2));
2653 }
2654 _ => panic!("Expected Send"),
2655 }
2656 }
2657
2658 #[test]
2659 fn test_parse_proof_bundles_and_protocol_requires_metadata() {
2660 let input = r#"
2661proof_bundle Base requires [guard_tokens, delegation]
2662proof_bundle Extra requires [knowledge_flow]
2663protocol WithBundles requires Base, Extra =
2664 roles A, B
2665 A -> B : Ping
2666"#;
2667
2668 let choreo = parse_choreography_str(input).expect("parse should succeed");
2669 let bundles = choreo.theorem_packs();
2670 assert_eq!(bundles.len(), 2);
2671 assert_eq!(bundles[0].name, "Base");
2672 assert_eq!(
2673 bundles[0].capabilities,
2674 vec!["guard_tokens".to_string(), "delegation".to_string()]
2675 );
2676 assert_eq!(bundles[1].name, "Extra");
2677 assert_eq!(bundles[1].capabilities, vec!["knowledge_flow".to_string()]);
2678 assert_eq!(
2679 choreo.required_theorem_packs(),
2680 vec!["Base".to_string(), "Extra".to_string()]
2681 );
2682 }
2683
2684 #[test]
2685 fn test_protocol_machine_core_statements_are_rejected() {
2686 let input = r#"
2687protocol VmOps =
2688 roles A, B
2689 acquire guard as token
2690 transfer token to B with bundle Base
2691 check k for B into out
2692 A -> B : Ping
2693"#;
2694
2695 let err =
2696 parse_choreography_str(input).expect_err("protocol-machine statements should fail");
2697 assert!(err
2698 .to_string()
2699 .contains("legacy DSL construct `protocol-machine core statement` was removed"));
2700 }
2701
2702 #[test]
2703 fn test_validate_missing_required_bundle_fails() {
2704 let input = r#"
2705protocol MissingBundle requires Core =
2706 roles A, B
2707 A -> B : Ping
2708"#;
2709
2710 let choreo = parse_choreography_str(input).expect("parse should succeed");
2711 let err = choreo.validate().expect_err("validation should fail");
2712 assert!(matches!(
2713 err,
2714 ValidationError::MissingProofBundle(ref name) if name == "Core"
2715 ));
2716 }
2717
2718 #[test]
2719 fn test_validate_missing_execution_profile_fails() {
2720 let input = r#"
2721protocol NeedReplay under Replay =
2722 roles A, B
2723 A -> B : Ping
2724"#;
2725
2726 let choreo = parse_choreography_str(input).expect("parse should succeed");
2727 let err = choreo.validate().expect_err("validation should fail");
2728 assert!(err
2729 .to_string()
2730 .contains("undeclared execution profile `Replay`"));
2731 }
2732
2733 #[test]
2734 fn test_validate_duplicate_bundle_fails() {
2735 let input = r#"
2736proof_bundle Core requires [delegation]
2737proof_bundle Core requires [guard_tokens]
2738protocol DuplicateBundle requires Core =
2739 roles A, B
2740 A -> B : Ping
2741"#;
2742
2743 let choreo = parse_choreography_str(input).expect("parse should succeed");
2744 let err = choreo.validate().expect_err("validation should fail");
2745 assert!(matches!(
2746 err,
2747 ValidationError::DuplicateProofBundle(ref name) if name == "Core"
2748 ));
2749 }
2750
2751 #[test]
2752 fn test_parse_guard_predicate_rejects_non_boolean_expression() {
2753 let input = r#"
2754protocol GuardTypeCheck =
2755 roles A, B
2756 choice A at
2757 | ok when (count + 1) =>
2758 A -> B : Ack
2759 | no =>
2760 A -> B : Nack
2761"#;
2762
2763 let err = parse_choreography_str(input).expect_err("guard should fail");
2764 assert!(matches!(err, ParseError::Syntax { .. }));
2765 assert!(err.to_string().contains("boolean-like"));
2766 }
2767
2768 #[test]
2769 fn test_parse_loop_while_rejects_non_boolean_expression() {
2770 let input = r#"
2771protocol LoopTypeCheck =
2772 roles A, B
2773 loop while "count + 1"
2774 A -> B : Tick
2775"#;
2776
2777 let err = parse_choreography_str(input).expect_err("loop condition should fail");
2778 assert!(matches!(err, ParseError::InvalidCondition { .. }));
2779 assert!(err.to_string().contains("boolean-like"));
2780 }
2781
2782 #[test]
2783 fn test_projection_preserves_continuation_after_authority_binding() {
2784 let input = r#"
2785effect Runtime
2786 authoritative ready : Session -> Session
2787 {
2788 class : authoritative
2789 progress : may_block
2790 region : fragment
2791 agreement_use : required
2792 reentrancy : reject_same_fragment
2793 }
2794
2795protocol ExtensionProjection uses Runtime =
2796 roles A, B
2797 authoritative let witness = check Runtime.ready(session)
2798 A -> B : Ping
2799"#;
2800
2801 let choreo = parse_choreography_str(input).expect("parse should succeed");
2802 let role_a = choreo
2803 .roles
2804 .iter()
2805 .find(|r| r.name() == "A")
2806 .expect("role A should exist");
2807 let projected =
2808 crate::compiler::projection::project(&choreo, role_a).expect("projection must work");
2809
2810 match projected {
2811 LocalType::Send { to, .. } => assert_eq!(to.name(), "B"),
2812 other => panic!("expected send continuation projection, got {other:?}"),
2813 }
2814 }
2815
2816 #[test]
2817 fn test_parse_enriched_proof_bundle_metadata() {
2818 let input = r#"
2819proof_bundle Base version "1.0.0" issuer "did:example:issuer" constraint "fresh_nonce" constraint "sig_valid" requires [delegation, guard_tokens]
2820protocol BundleMeta requires Base =
2821 roles A, B
2822 A -> B : Ping
2823"#;
2824
2825 let choreo = parse_choreography_str(input).expect("parse should succeed");
2826 let bundles = choreo.theorem_packs();
2827 assert_eq!(bundles.len(), 1);
2828 let bundle = &bundles[0];
2829 assert_eq!(bundle.name, "Base");
2830 assert_eq!(bundle.version.as_deref(), Some("1.0.0"));
2831 assert_eq!(bundle.issuer.as_deref(), Some("did:example:issuer"));
2832 assert_eq!(
2833 bundle.constraints,
2834 vec!["fresh_nonce".to_string(), "sig_valid".to_string()]
2835 );
2836 assert_eq!(
2837 bundle.capabilities,
2838 vec!["delegation".to_string(), "guard_tokens".to_string()]
2839 );
2840 }
2841
2842 #[test]
2843 fn test_parse_execution_profiles_and_protocol_profiles() {
2844 let input = r#"
2845profile Replay fairness eventual admissibility replay escalation_window bounded
2846protocol Inferred under Replay =
2847 roles A, B
2848 A -> B : Ping
2849"#;
2850
2851 let choreo = parse_choreography_str(input).expect("parse should succeed");
2852 assert_eq!(choreo.execution_profile_declarations().len(), 1);
2853 assert_eq!(choreo.execution_profile_declarations()[0].name, "Replay");
2854 assert_eq!(
2855 choreo.protocol_execution_profiles(),
2856 vec!["Replay".to_string()]
2857 );
2858 assert!(choreo.validate().is_ok());
2859 }
2860
2861 #[test]
2862 fn test_parse_agreement_profiles_and_operation_attachment() {
2863 let input = r#"
2864agreement_profile SoftSafe
2865 visibility pending
2866 rule aura_soft_safe
2867 usable_at soft_safe
2868 finalized_at finalized
2869 evidence commit_fact
2870
2871profile Replay fairness eventual admissibility replay escalation_window bounded
2872
2873operation syncLedger(entryId : Int) at Coordinator progress LedgerProgress requires Replay within bounded on timeout => escalate on stall => diagnose agreement SoftSafe prestate ContactContext compose first_success =
2874 publish SyncQueued(entryId)
2875
2876protocol CommitLifecycle under Replay =
2877 roles Coordinator, Worker
2878 begin syncLedger(42) progress LedgerProgress requires Replay within bounded on timeout => escalate on stall => diagnose
2879 Coordinator -> Worker : Prepare
2880"#;
2881
2882 let choreo = parse_choreography_str(input).expect("parse should succeed");
2883 assert_eq!(choreo.agreement_profile_declarations().len(), 1);
2884 let agreement = &choreo.agreement_profile_declarations()[0];
2885 assert_eq!(agreement.name, "SoftSafe");
2886 assert_eq!(agreement.visibility, "pending");
2887 assert_eq!(agreement.rule, "aura_soft_safe");
2888 assert_eq!(agreement.usable_at, "soft_safe");
2889 assert_eq!(agreement.finalized_at, "finalized");
2890 assert_eq!(agreement.evidence, "commit_fact");
2891
2892 let attachment = choreo.operation_declarations()[0]
2893 .agreement
2894 .clone()
2895 .expect("operation should carry agreement metadata");
2896 assert_eq!(attachment.profile_name, "SoftSafe");
2897 assert_eq!(attachment.prestate.as_deref(), Some("ContactContext"));
2898 assert!(choreo.validate().is_ok());
2899 }
2900
2901 #[test]
2902 fn test_linear_assets_reject_double_consume() {
2903 let input = r#"
2904protocol LinearDoubleConsume =
2905 roles A, B
2906 let token = transfer Session from A to B
2907 A -> B : Use(token)
2908 A -> B : UseAgain(token)
2909"#;
2910
2911 let err = parse_choreography_str(input).expect_err("parse should fail");
2912 assert!(err.to_string().contains("consumed more than once"));
2913 }
2914
2915 #[test]
2916 fn test_linear_assets_reject_branch_divergence() {
2917 let input = r#"
2918protocol LinearBranchDivergence =
2919 roles A, B
2920 let token = transfer Session from A to B
2921 choice A at
2922 | consume =>
2923 A -> B : Use(token)
2924 | keep =>
2925 A -> B : Skip
2926"#;
2927
2928 let err = parse_choreography_str(input).expect_err("parse should fail");
2929 assert!(err.to_string().contains("diverge"));
2930 }
2931
2932 #[test]
2933 fn test_removed_first_class_combinators_are_rejected() {
2934 let input = r#"
2935protocol Combinators =
2936 roles A, B
2937 handshake A <-> B : Hello
2938 quorum_collect A -> B min 2 : Vote
2939 A -> B : Done
2940 retry 2 {
2941 A -> B : Ping
2942 }
2943"#;
2944
2945 let err = parse_choreography_str(input)
2946 .expect_err("removed first-class combinators should fail closed");
2947 assert!(err
2948 .to_string()
2949 .contains("legacy DSL construct `handshake` was removed"));
2950 }
2951
2952 #[test]
2953 fn test_parse_role_sets_and_topologies() {
2954 let input = r#"
2955role_set Signers = Alice, Bob, Carol
2956role_set Quorum = subset(Signers, 0..2)
2957cluster LocalCluster = Signers, Quorum
2958ring RingNet = Alice, Bob, Carol
2959mesh FullMesh = Alice, Bob, Carol
2960protocol TopologyAware =
2961 roles Alice, Bob
2962 Alice -> Bob : Ping
2963"#;
2964
2965 let choreo = parse_choreography_str(input).expect("parse should succeed");
2966 let role_sets = choreo.role_sets();
2967 assert_eq!(role_sets.len(), 2);
2968 assert_eq!(role_sets[0].name, "Signers");
2969 assert_eq!(
2970 role_sets[0].members,
2971 vec!["Alice".to_string(), "Bob".to_string(), "Carol".to_string()]
2972 );
2973 assert_eq!(role_sets[1].subset_of.as_deref(), Some("Signers"));
2974 assert_eq!(role_sets[1].subset_start, Some(0));
2975 assert_eq!(role_sets[1].subset_end, Some(2));
2976
2977 let topologies = choreo.topologies();
2978 assert_eq!(topologies.len(), 3);
2979 assert_eq!(topologies[0].kind, "cluster");
2980 assert_eq!(topologies[1].kind, "ring");
2981 assert_eq!(topologies[2].kind, "mesh");
2982 }
2983
2984 #[test]
2985 fn test_explain_lowering_report_for_proof_backed_surface() {
2986 let input = r#"
2987proof_bundle Spec requires [delegation]
2988protocol ExplainMe =
2989 roles A, B
2990 A -> B : Ping
2991"#;
2992
2993 let report = explain_lowering(input).expect("report generation should succeed");
2994 assert!(report.contains("Proof bundles: Spec"));
2995 assert!(report.contains("Lowering:"));
2996
2997 let choreo = parse_choreography_str(input).expect("parse should succeed");
2998 let lints = collect_dsl_lints(&choreo, LintLevel::Warn);
2999 assert!(lints.is_empty());
3000 let lsp = render_lsp_lint_diagnostics(&choreo, LintLevel::Warn);
3001 assert_eq!(lsp, "[]");
3002 }
3003
3004 #[test]
3005 fn test_typed_predicate_ir_rejects_if_expression() {
3006 let input = r#"
3007protocol PredicateTyping =
3008 roles A, B
3009 choice A at
3010 | ok when (if ready { true } else { false }) =>
3011 A -> B : Accept
3012 | no =>
3013 A -> B : Reject
3014"#;
3015
3016 let err = parse_choreography_str(input).expect_err("parse should fail");
3017 assert!(matches!(err, ParseError::Syntax { .. }));
3018 assert!(err.to_string().contains("boolean-like"));
3019 }
3020
3021 #[test]
3022 fn test_parse_choreography_rejects_proc_macro_token_input() {
3023 let input: TokenStream = quote::quote! {
3024 protocol PingPong =
3025 roles Alice, Bob
3026 Alice -> Bob : Ping
3027 Bob -> Alice : Pong
3028 };
3029
3030 let err = parse_choreography(input).expect_err("proc-macro2 token parsing should fail");
3031 assert!(err
3032 .to_string()
3033 .contains("proc-macro2 token parsing for the tell! DSL was removed"));
3034 }
3035
3036 #[test]
3037 fn test_parse_choreography_rejects_string_literal_macro_input() {
3038 let input: TokenStream = quote::quote! {
3039 r#"
3040protocol ReplicatedWrite =
3041 roles Client, Leader, Replica0, Replica1
3042 Client -> Leader : Put of kv.Write
3043"#
3044 };
3045
3046 let err = parse_choreography(input).expect_err("string literal macro input should fail");
3047 assert!(err
3048 .to_string()
3049 .contains("string-literal tell! input was removed"));
3050 }
3051
3052 #[test]
3053 fn test_parse_legacy_structural_braces_are_rejected() {
3054 let input = r#"
3055protocol Branchy = {
3056 roles A, B, C, D;
3057 par {
3058 | {
3059 choice A at {
3060 | Accept => {
3061 A -> B : Ok;
3062 }
3063 | Reject => {
3064 A -> B : No;
3065 }
3066 }
3067 }
3068 | B -> D : Right;
3069 }
3070}
3071"#;
3072
3073 let err = parse_choreography_str(input).expect_err("legacy braces should fail");
3074 assert!(err
3075 .to_string()
3076 .contains("legacy brace-based protocol blocks are removed"));
3077 }
3078
3079 #[test]
3082 fn test_parse_authority_surface_with_effects_types_and_uses() {
3083 let input = r#"
3084type CommitError =
3085 | NotReady
3086 | TimedOut
3087
3088type alias ReadyWitness =
3089{
3090 epoch : Int
3091 issuedBy : Role
3092}
3093
3094effect Runtime
3095 authoritative ready : Session -> Result CommitError ReadyWitness
3096 {
3097 class : authoritative
3098 progress : may_block
3099 region : fragment
3100 agreement_use : required
3101 reentrancy : reject_same_fragment
3102 }
3103 command transfer : TransferRequest -> Result TransferError TransferReceipt
3104 {
3105 class : best_effort
3106 progress : immediate
3107 region : session
3108 agreement_use : none
3109 reentrancy : allow
3110 }
3111
3112effect Audit
3113 observe record : AuditEvent -> Unit
3114 {
3115 class : observational
3116 progress : immediate
3117 region : global
3118 agreement_use : forbidden
3119 reentrancy : allow
3120 }
3121
3122protocol CommitFlow uses Runtime, Audit =
3123 roles Coordinator, Worker, Client
3124 authoritative let readiness = check Runtime.ready(session)
3125 case readiness of
3126 | Ok(witness) =>
3127 Coordinator -> Worker : Commit(witness)
3128 | Err(reason) =>
3129 Coordinator -> Client : Retry(reason)
3130 timeout 5s Coordinator at
3131 Worker -> Coordinator : Ready
3132 on timeout =>
3133 Coordinator -> Worker : Cancel
3134 on cancel =>
3135 Coordinator -> Client : Cancelled
3136 choice Coordinator at
3137 | Commit when check Runtime.ready(session) yields witness =>
3138 Coordinator -> Worker : CommitAgain(witness)
3139 | Abort =>
3140 Coordinator -> Worker : Abort
3141"#;
3142
3143 let choreography = parse_choreography_str(input).expect("authority surface should parse");
3144 assert_eq!(choreography.type_declarations().len(), 2);
3145 assert_eq!(choreography.effect_interface_declarations().len(), 2);
3146 assert_eq!(
3147 choreography.protocol_uses(),
3148 vec!["Runtime".to_string(), "Audit".to_string()]
3149 );
3150 let runtime_metadata = choreography.effect_contract_declarations();
3151 assert!(
3152 runtime_metadata.iter().any(|op| {
3153 op.interface_name == "Runtime"
3154 && op.operation_name == "ready"
3155 && op.authority_class == crate::ast::EffectAuthorityClass::Authoritative
3156 && op.semantic_class == "authoritative"
3157 && op.progress == "may_block"
3158 && op.region == "fragment"
3159 && op.agreement_use == "required"
3160 }),
3161 "runtime effect metadata should carry effect authority class"
3162 );
3163 choreography
3164 .validate()
3165 .expect("declared effect uses should validate");
3166 }
3167
3168 #[test]
3169 fn test_parse_let_in_and_maybe_surface() {
3170 let input = r#"
3171type alias InviteHandle =
3172{
3173 id : Int
3174}
3175
3176effect Runtime
3177 lookupInvite : Session -> Maybe InviteHandle
3178 {
3179 class : best_effort
3180 progress : immediate
3181 region : session
3182 agreement_use : none
3183 reentrancy : allow
3184 }
3185
3186protocol InviteFlow uses Runtime =
3187 roles Coordinator, Worker
3188 let invite = check Runtime.lookupInvite(session) in
3189 case invite of
3190 | Just(handle) =>
3191 Coordinator -> Worker : UseInvite(handle)
3192 | Nothing =>
3193 Coordinator -> Worker : MissingInvite
3194"#;
3195
3196 let choreography =
3197 parse_choreography_str(input).expect("let-in Maybe surface should parse");
3198 choreography
3199 .validate()
3200 .expect("effect invocation should validate");
3201 }
3202
3203 #[test]
3204 fn test_reject_non_exhaustive_result_case() {
3205 let input = r#"
3206effect Runtime
3207 ready : Session -> Result CommitError ReadyWitness
3208 {
3209 class : authoritative
3210 progress : may_block
3211 region : fragment
3212 agreement_use : required
3213 reentrancy : reject_same_fragment
3214 }
3215
3216protocol CommitFlow uses Runtime =
3217 roles Coordinator, Worker
3218 authoritative let readiness = check Runtime.ready(session)
3219 case readiness of
3220 | Ok(witness) =>
3221 Coordinator -> Worker : Commit(witness)
3222"#;
3223
3224 let err =
3225 parse_choreography_str(input).expect_err("non-exhaustive Result case should fail");
3226 assert!(!err.to_string().is_empty());
3227 }
3228
3229 #[test]
3230 fn test_reject_duplicate_linear_binding_use() {
3231 let input = r#"
3232protocol TransferFlow =
3233 roles Coordinator, Worker, Client
3234 let receipt = transfer Session from Coordinator to Worker
3235 Coordinator -> Worker : TransferAccepted(receipt)
3236 Coordinator -> Client : ReceiptAudit(receipt)
3237"#;
3238
3239 let err = parse_choreography_str(input).expect_err("duplicate linear use should fail");
3240 assert!(err.to_string().contains("consumed more than once"));
3241 }
3242
3243 #[test]
3244 fn test_reject_dropped_linear_binding_use() {
3245 let input = r#"
3246protocol TransferFlow =
3247 roles Coordinator, Worker
3248 let receipt = transfer Session from Coordinator to Worker
3249 Coordinator -> Worker : TransferAccepted
3250"#;
3251
3252 let err = parse_choreography_str(input).expect_err("dropped linear binding should fail");
3253 assert!(err.to_string().contains("never consumed"));
3254 }
3255
3256 #[test]
3257 fn test_reject_undeclared_protocol_use() {
3258 let input = r#"
3259protocol CommitFlow uses Runtime =
3260 roles Coordinator, Worker
3261 Coordinator -> Worker : Ping
3262"#;
3263
3264 let choreography = parse_choreography_str(input).expect("parse should succeed");
3265 let err = choreography
3266 .validate()
3267 .expect_err("undeclared effect interface should fail validation");
3268 assert!(err.to_string().contains("undeclared effect interface"));
3269 }
3270
3271 #[test]
3272 fn test_reject_undeclared_effect_operation_invocation() {
3273 let input = r#"
3274effect Runtime
3275 ready : Session -> Result CommitError ReadyWitness
3276 {
3277 class : authoritative
3278 progress : may_block
3279 region : fragment
3280 agreement_use : required
3281 reentrancy : reject_same_fragment
3282 }
3283
3284protocol CommitFlow uses Runtime =
3285 roles Coordinator, Worker
3286 let readiness = check Runtime.lookup(session)
3287 case readiness of
3288 | Ok(witness) =>
3289 Coordinator -> Worker : Commit(witness)
3290 | Err(reason) =>
3291 Coordinator -> Worker : Retry(reason)
3292"#;
3293
3294 let choreography = parse_choreography_str(input).expect("parse should succeed");
3295 let err = choreography
3296 .validate()
3297 .expect_err("undeclared effect operation should fail validation");
3298 assert!(err.to_string().contains("undeclared operation"));
3299 }
3300
3301 #[test]
3302 fn test_reject_duplicate_effect_declarations() {
3303 let input = r#"
3304effect Runtime
3305 ready : Session -> Result CommitError ReadyWitness
3306 {
3307 class : authoritative
3308 progress : may_block
3309 region : fragment
3310 agreement_use : required
3311 reentrancy : reject_same_fragment
3312 }
3313
3314effect Runtime
3315 transfer : TransferRequest -> Result TransferError TransferReceipt
3316 {
3317 class : best_effort
3318 progress : immediate
3319 region : session
3320 agreement_use : none
3321 reentrancy : allow
3322 }
3323
3324protocol CommitFlow uses Runtime =
3325 roles Coordinator, Worker
3326 Coordinator -> Worker : Ping
3327"#;
3328
3329 let choreography = parse_choreography_str(input).expect("parse should succeed");
3330 let err = choreography
3331 .validate()
3332 .expect_err("duplicate effect declarations should fail validation");
3333 assert!(err
3334 .to_string()
3335 .contains("duplicate effect interface declaration"));
3336 }
3337
3338 #[test]
3339 fn test_reject_observational_effect_used_with_check() {
3340 let input = r#"
3341effect Runtime
3342 observe watchPresence : Session -> PresenceView
3343 {
3344 class : observational
3345 progress : immediate
3346 region : session
3347 agreement_use : forbidden
3348 reentrancy : allow
3349 }
3350
3351protocol WatchFlow uses Runtime =
3352 roles Coordinator, Worker
3353 let presence = check Runtime.watchPresence(session)
3354 Coordinator -> Worker : Seen(presence)
3355"#;
3356
3357 let choreography = parse_choreography_str(input).expect("parse should succeed");
3358 let err = choreography
3359 .validate()
3360 .expect_err("observational effect use should fail validation");
3361 assert!(err.to_string().contains("observational"));
3362 }
3363
3364 #[test]
3365 fn test_reject_plain_binding_of_authoritative_check() {
3366 let input = r#"
3367effect Runtime
3368 authoritative ready : Session -> Result CommitError ReadyWitness
3369 {
3370 class : authoritative
3371 progress : may_block
3372 region : fragment
3373 agreement_use : required
3374 reentrancy : reject_same_fragment
3375 }
3376
3377protocol CommitFlow uses Runtime =
3378 roles Coordinator, Worker
3379 let readiness = check Runtime.ready(session)
3380 Coordinator -> Worker : Continue(readiness)
3381"#;
3382
3383 let choreography = parse_choreography_str(input).expect("parse should succeed");
3384 let err = choreography
3385 .validate()
3386 .expect_err("plain authoritative binding must fail validation");
3387 assert!(err.to_string().contains("authoritative let"));
3388 }
3389
3390 #[test]
3391 fn test_reject_plain_binding_of_observe_expression() {
3392 let input = r#"
3393effect Runtime
3394 observe watchPresence : Session -> PresenceView
3395 {
3396 class : observational
3397 progress : immediate
3398 region : session
3399 agreement_use : forbidden
3400 reentrancy : allow
3401 }
3402
3403protocol WatchFlow uses Runtime =
3404 roles Coordinator, Worker
3405 let presence = observe Runtime.watchPresence(session)
3406 Coordinator -> Worker : Seen(presence)
3407"#;
3408
3409 let choreography = parse_choreography_str(input).expect("parse should succeed");
3410 let err = choreography
3411 .validate()
3412 .expect_err("plain observe binding must fail validation");
3413 assert!(err.to_string().contains("observe let"));
3414 }
3415
3416 #[test]
3417 fn test_reject_unsupported_runtime_upgrade_surface() {
3418 let input = r#"
3419protocol UpgradeFlow =
3420 roles Coordinator, Worker
3421 upgrade runtime to Epoch2
3422 Coordinator -> Worker : Commit
3423"#;
3424
3425 let err = parse_choreography_str(input)
3426 .expect_err("runtime upgrade surface must remain outside the DSL");
3427 assert!(
3428 err.to_string().contains("upgrade") || err.to_string().contains("unexpected"),
3429 "unexpected parse error: {err}"
3430 );
3431 }
3432
3433 #[test]
3434 fn test_parse_fragments_operations_and_guest_runtime_metadata() {
3435 let input = r#"
3436fragment ChannelMembership(channel)
3437
3438profile Replay fairness eventual admissibility replay escalation_window bounded
3439agreement_profile PendingPublication
3440 visibility pending
3441 rule no_agreement
3442 usable_at provisional
3443 finalized_at finalized
3444 evidence publication
3445
3446operation syncMembership(channel : ChannelId) at Worker within ChannelMembership(channel) progress MembershipProgress agreement PendingPublication prestate ChannelMembership compose threshold_success(2) =
3447 publish SyncQueued(channel)
3448
3449guest runtime MessagingGuest =
3450 uses Runtime, Audit
3451 entry CommitFlow
3452
3453protocol CommitFlow uses Runtime, Audit under Replay =
3454 roles Coordinator, Worker
3455 Coordinator -> Worker : Ping
3456"#;
3457
3458 let choreography = parse_choreography_str(input).expect("surface metadata should parse");
3459 assert_eq!(choreography.region_declarations().len(), 1);
3460 assert_eq!(
3461 choreography.region_declarations()[0].name,
3462 "ChannelMembership"
3463 );
3464 assert_eq!(
3465 choreography.region_declarations()[0].params,
3466 vec!["channel"]
3467 );
3468
3469 assert_eq!(choreography.operation_declarations().len(), 1);
3470 let operation = &choreography.operation_declarations()[0];
3471 assert_eq!(operation.name, "syncMembership");
3472 assert_eq!(operation.owner_role, "Worker");
3473 assert_eq!(
3474 operation.within.as_deref(),
3475 Some("ChannelMembership(channel)")
3476 );
3477 assert_eq!(
3478 operation
3479 .progress_contract
3480 .as_ref()
3481 .map(|progress| progress.contract_name.as_str()),
3482 Some("MembershipProgress")
3483 );
3484 assert_eq!(
3485 operation
3486 .agreement
3487 .as_ref()
3488 .map(|agreement| agreement.profile_name.as_str()),
3489 Some("PendingPublication")
3490 );
3491 assert_eq!(
3492 operation
3493 .child_effect_aggregation
3494 .as_ref()
3495 .map(|composition| composition.dsl_name()),
3496 Some("threshold_success(2)".to_string())
3497 );
3498 assert_eq!(operation.params.len(), 1);
3499 assert!(operation
3500 .body_source
3501 .contains("publish SyncQueued(channel)"));
3502
3503 assert_eq!(choreography.guest_runtime_declarations().len(), 1);
3504 let guest_runtime = &choreography.guest_runtime_declarations()[0];
3505 assert_eq!(guest_runtime.name, "MessagingGuest");
3506 assert_eq!(guest_runtime.uses, vec!["Runtime", "Audit"]);
3507 assert_eq!(guest_runtime.entry, "CommitFlow");
3508 assert_eq!(choreography.execution_profile_declarations().len(), 1);
3509 assert_eq!(
3510 choreography.protocol_execution_profiles(),
3511 vec!["Replay".to_string()]
3512 );
3513 }
3514
3515 #[test]
3516 fn test_parse_commitment_lifecycle_and_structured_progress_metadata() {
3517 let input = r#"
3518profile Replay fairness eventual admissibility replay escalation_window bounded
3519agreement_profile SoftSafe
3520 visibility pending
3521 rule aura_soft_safe
3522 usable_at soft_safe
3523 finalized_at finalized
3524 evidence commit_fact
3525
3526operation syncLedger(entryId : Int) at Coordinator progress LedgerProgress requires Replay within bounded on timeout => escalate on stall => diagnose agreement SoftSafe prestate LedgerState compose first_success =
3527 publish SyncQueued(entryId)
3528
3529protocol CommitLifecycle under Replay =
3530 roles Coordinator, Worker
3531 begin syncLedger(42) progress LedgerProgress requires Replay within bounded on timeout => escalate on stall => diagnose
3532 Coordinator -> Worker : Prepare
3533 await syncLedger
3534 resolve syncLedger as Success
3535"#;
3536
3537 let choreography =
3538 parse_choreography_str(input).expect("commitment lifecycle surface should parse");
3539 choreography
3540 .validate()
3541 .expect("structured progress metadata should validate");
3542
3543 let operation = &choreography.operation_declarations()[0];
3544 let progress = operation
3545 .progress_contract
3546 .as_ref()
3547 .expect("operation should carry progress metadata");
3548 assert_eq!(progress.contract_name, "LedgerProgress");
3549 assert_eq!(progress.requires_profile.as_deref(), Some("Replay"));
3550 assert_eq!(progress.within_window.as_deref(), Some("bounded"));
3551 assert_eq!(progress.on_timeout.as_deref(), Some("escalate"));
3552 assert_eq!(progress.on_stall.as_deref(), Some("diagnose"));
3553 }
3554
3555 #[test]
3556 fn test_legacy_implicit_progress_contract_is_rejected() {
3557 let input = r#"
3558profile Replay fairness eventual admissibility replay escalation_window bounded
3559fragment ChannelMembership(channel)
3560agreement_profile PendingPublication
3561 visibility pending
3562 rule no_agreement
3563 usable_at provisional
3564 finalized_at finalized
3565 evidence publication
3566
3567operation syncMembership(channel : ChannelId) at Worker within ChannelMembership(channel) progress MembershipProgress agreement PendingPublication prestate ChannelMembership compose threshold_success(2) =
3568 publish SyncQueued(channel)
3569
3570protocol CommitFlow under Replay =
3571 roles Coordinator, Worker
3572 begin syncMembership(1) progress MembershipProgress
3573"#;
3574
3575 let choreography =
3576 parse_choreography_str(input).expect("legacy form still parses before validation");
3577 choreography
3578 .validate()
3579 .expect_err("legacy implicit progress metadata must be rejected");
3580 }
3581
3582 #[test]
3583 fn test_parse_authority_publication_materialization_and_handoff_fail_projection_closed() {
3584 let input = r#"
3585protocol AcceptFlow =
3586 roles Coordinator, Worker
3587 authoritative let witness = check Runtime.ready(session)
3588 observe let presence = observe Runtime.watchPresence(session)
3589 publish witness as AcceptedPublication
3590 materialize acceptedProof from AcceptedPublication
3591 let receipt = transfer Session from Coordinator to Worker
3592 handoff acceptInvite to Worker with receipt
3593 dependent work SyncMembership(channel) required for acceptInvite
3594 Coordinator -> Worker : Commit
3595"#;
3596
3597 let choreography = parse_choreography_str(input).expect("semantic surface should parse");
3598 for role in &choreography.roles {
3599 project(&choreography, role)
3600 .unwrap_or_else(|err| panic!("project {}: {err}", role.name()));
3601 }
3602 }
3603
3604 #[test]
3607 fn parse_choreography_file_accepts_tell_extension() {
3608 let dir = tempdir().expect("tempdir");
3609 let path = dir.path().join("protocol.tell");
3610 std::fs::write(&path, "protocol Ping =\n roles A, B\n A -> B : Msg\n")
3611 .expect("write tell fixture");
3612
3613 let parsed = parse_choreography_file(&path).expect("parse .tell source");
3614 assert_eq!(parsed.name.to_string(), "Ping");
3615 }
3616
3617 #[test]
3618 fn parse_choreography_file_rejects_choreo_extension() {
3619 let dir = tempdir().expect("tempdir");
3620 let path = dir.path().join("protocol.choreo");
3621 std::fs::write(&path, "protocol Ping =\n roles A, B\n A -> B : Msg\n")
3622 .expect("write choreo fixture");
3623
3624 let err = parse_choreography_file(&path).expect_err("reject legacy extension");
3625 let rendered = err.to_string();
3626 assert!(
3627 rendered.contains(".tell"),
3628 "error should point to canonical .tell extension: {rendered}"
3629 );
3630 }
3631
3632 #[test]
3633 fn reject_legacy_child_effect_aggregation_keywords() {
3634 for keyword in ["race", "fallback", "quorum(2)"] {
3635 let input = format!(
3636 r#"
3637agreement_profile SoftSafe
3638 visibility pending
3639 rule aura_soft_safe
3640 usable_at soft_safe
3641 finalized_at finalized
3642 evidence commit_fact
3643
3644profile Replay fairness eventual admissibility replay escalation_window bounded
3645
3646operation syncLedger(entryId : Int) at Coordinator progress LedgerProgress requires Replay within bounded on timeout => escalate on stall => diagnose agreement SoftSafe prestate ContactContext compose {keyword} =
3647 publish SyncQueued(entryId)
3648
3649protocol CommitLifecycle under Replay =
3650 roles Coordinator, Worker
3651 begin syncLedger(42) progress LedgerProgress requires Replay within bounded on timeout => escalate on stall => diagnose
3652 Coordinator -> Worker : Prepare
3653"#
3654 );
3655
3656 let err = parse_choreography_str(&input)
3657 .expect_err("legacy child-effect aggregation keyword should fail");
3658 let message = err.to_string();
3659 assert!(
3660 message.contains("all_success")
3661 || message.contains("first_success")
3662 || message.contains("threshold_success"),
3663 "unexpected parser error for `{keyword}`: {message}"
3664 );
3665 }
3666 }
3667}