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