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 source = input.to_string();
1193 let choreography = match parse_choreography(input) {
1194 Ok(c) => c,
1195 Err(e) => return e.to_compile_error(),
1196 };
1197
1198 if let Err(e) = choreography.validate() {
1200 return syn::Error::new(Span::call_site(), e.to_string()).to_compile_error();
1201 }
1202
1203 match crate::generate_protocol_module(&choreography, &source) {
1204 Ok(tokens) => tokens,
1205 Err(err) => err.to_compile_error(),
1206 }
1207}
1208
1209#[cfg(test)]
1210mod tests {
1211 use super::*;
1212 use crate::ast::{Condition, LocalType, Protocol, ValidationError};
1213 use crate::compiler::parser::parse_choreography_str;
1214 use crate::compiler::projection::project;
1215 use proc_macro2::TokenStream;
1216 use tempfile::tempdir;
1217
1218 #[test]
1221 fn test_parse_simple_send() {
1222 let input = r#"
1223protocol SimpleSend =
1224 roles Alice, Bob
1225 Alice -> Bob : Hello
1226"#;
1227
1228 let result = parse_choreography_str(input);
1229 assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
1230
1231 let choreo = result.unwrap();
1232 assert_eq!(choreo.name.to_string(), "SimpleSend");
1233 assert_eq!(choreo.roles.len(), 2);
1234 }
1235
1236 #[test]
1237 fn test_parse_with_choice() {
1238 let input = r#"
1239protocol Negotiation =
1240 roles Buyer, Seller
1241 Buyer -> Seller : Offer
1242 choice Seller at
1243 | accept =>
1244 Seller -> Buyer : Accept
1245 | reject =>
1246 Seller -> Buyer : Reject
1247"#;
1248
1249 let result = parse_choreography_str(input);
1250 assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
1251
1252 let choreo = result.unwrap();
1253 assert_eq!(choreo.name.to_string(), "Negotiation");
1254 }
1255
1256 #[test]
1257 fn test_parse_choice_alias() {
1258 let input = r#"
1259protocol AliasChoice =
1260 roles A, B
1261 choice A at
1262 | ok =>
1263 A -> B : Ack
1264 | fail =>
1265 A -> B : Nack
1266"#;
1267
1268 let result = parse_choreography_str(input);
1269 assert!(
1270 result.is_ok(),
1271 "Failed to parse alias choice: {:?}",
1272 result.err()
1273 );
1274 }
1275
1276 #[test]
1277 fn test_parse_undefined_role() {
1278 let input = r#"
1279protocol Invalid =
1280 roles Alice
1281 Alice -> Bob : Hello
1282"#;
1283
1284 let result = parse_choreography_str(input);
1285 assert!(result.is_err());
1286 let err = result.unwrap_err();
1287 assert!(matches!(err, ParseError::UndefinedRole { .. }));
1288
1289 let err_str = err.to_string();
1291 assert!(err_str.contains("Undefined role"));
1292 assert!(err_str.contains("Bob"));
1293 }
1294
1295 #[test]
1296 fn test_parse_duplicate_role() {
1297 let input = r#"
1298protocol DuplicateRole =
1299 roles Alice, Bob, Alice
1300 Alice -> Bob : Hello
1301"#;
1302
1303 let result = parse_choreography_str(input);
1304 assert!(result.is_err());
1305 let err = result.unwrap_err();
1306 assert!(matches!(err, ParseError::DuplicateRole { .. }));
1307
1308 let err_str = err.to_string();
1310 assert!(err_str.contains("Duplicate role"));
1311 assert!(err_str.contains("Alice"));
1312 }
1313
1314 #[test]
1315 fn test_parse_loop_repeat() {
1316 let input = r#"
1317protocol LoopProtocol =
1318 roles Client, Server
1319 loop repeat 3
1320 Client -> Server : Request
1321 Server -> Client : Response
1322"#;
1323
1324 let result = parse_choreography_str(input);
1325 assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
1326 }
1327
1328 #[test]
1329 fn test_parse_loop_decide() {
1330 let input = r#"
1331protocol DecideLoop =
1332 roles Client, Server
1333 loop decide by Client
1334 Client -> Server : Ping
1335 Server -> Client : Pong
1336"#;
1337
1338 let result = parse_choreography_str(input);
1339 assert!(
1340 result.is_ok(),
1341 "Failed to parse decide loop: {:?}",
1342 result.err()
1343 );
1344 }
1345
1346 #[test]
1347 fn test_role_decides_desugaring() {
1348 let input = r#"
1358protocol DecideLoop =
1359 roles Client, Server
1360 loop decide by Client
1361 Client -> Server : Ping
1362 Server -> Client : Pong
1363"#;
1364
1365 let result = parse_choreography_str(input);
1366 assert!(
1367 result.is_ok(),
1368 "Failed to parse decide loop: {:?}",
1369 result.err()
1370 );
1371
1372 let choreo = result.unwrap();
1373 match &choreo.protocol {
1374 Protocol::Rec { label, body } => {
1375 assert_eq!(label.to_string(), "RoleDecidesLoop");
1376 match body.as_ref() {
1377 Protocol::Choice { role, branches, .. } => {
1378 assert_eq!(role.name().to_string(), "Client");
1379 assert_eq!(branches.len(), 2);
1380
1381 let continue_branch = branches.first();
1383 assert_eq!(continue_branch.label.to_string(), "Ping");
1384
1385 match &continue_branch.protocol {
1387 Protocol::Send {
1388 from,
1389 to,
1390 message,
1391 continuation,
1392 ..
1393 } => {
1394 assert_eq!(from.name().to_string(), "Client");
1395 assert_eq!(to.name().to_string(), "Server");
1396 assert_eq!(message.name.to_string(), "Ping");
1397
1398 match continuation.as_ref() {
1400 Protocol::Send {
1401 from,
1402 to,
1403 message,
1404 continuation,
1405 ..
1406 } => {
1407 assert_eq!(from.name().to_string(), "Server");
1408 assert_eq!(to.name().to_string(), "Client");
1409 assert_eq!(message.name.to_string(), "Pong");
1410
1411 match continuation.as_ref() {
1413 Protocol::Var(label) => {
1414 assert_eq!(label.to_string(), "RoleDecidesLoop");
1415 }
1416 _ => panic!(
1417 "Expected Var for continue, got {:?}",
1418 continuation
1419 ),
1420 }
1421 }
1422 _ => panic!("Expected Send for Pong, got {:?}", continuation),
1423 }
1424 }
1425 _ => {
1426 panic!("Expected Send for Ping, got {:?}", continue_branch.protocol)
1427 }
1428 }
1429
1430 let done_branch = &branches.as_slice()[1];
1432 assert_eq!(done_branch.label.to_string(), "Done");
1433 match &done_branch.protocol {
1434 Protocol::Send {
1435 from,
1436 to,
1437 message,
1438 continuation,
1439 ..
1440 } => {
1441 assert_eq!(from.name().to_string(), "Client");
1442 assert_eq!(to.name().to_string(), "Server");
1443 assert_eq!(message.name.to_string(), "Done");
1444 assert!(matches!(continuation.as_ref(), Protocol::End));
1445 }
1446 _ => panic!("Expected Send for Done, got {:?}", done_branch.protocol),
1447 }
1448 }
1449 _ => panic!("Expected Choice inside Rec, got {:?}", body),
1450 }
1451 }
1452 _ => panic!("Expected Rec at top level, got {:?}", choreo.protocol),
1453 }
1454 }
1455
1456 #[test]
1457 fn test_role_decides_wrong_first_sender_no_desugar() {
1458 let input = r#"
1461protocol WrongSender =
1462 roles Client, Server
1463 loop decide by Client
1464 Server -> Client : Response
1465 Client -> Server : Ack
1466"#;
1467
1468 let result = parse_choreography_str(input);
1469 assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
1470
1471 let choreo = result.unwrap();
1472 match &choreo.protocol {
1474 Protocol::Loop { condition, .. } => match condition {
1475 Some(Condition::RoleDecides(role)) => {
1476 assert_eq!(role.name().to_string(), "Client");
1477 }
1478 _ => panic!("Expected RoleDecides condition"),
1479 },
1480 _ => panic!(
1481 "Expected Loop (not desugared) when first sender doesn't match deciding role"
1482 ),
1483 }
1484 }
1485
1486 #[test]
1487 fn test_role_decides_single_message() {
1488 let input = r#"
1490protocol SingleMessage =
1491 roles A, B
1492 loop decide by A
1493 A -> B : Msg
1494"#;
1495
1496 let result = parse_choreography_str(input);
1497 assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
1498
1499 let choreo = result.unwrap();
1500 match &choreo.protocol {
1501 Protocol::Rec { label, body } => {
1502 assert_eq!(label.to_string(), "RoleDecidesLoop");
1503 match body.as_ref() {
1504 Protocol::Choice { role, branches, .. } => {
1505 assert_eq!(role.name().to_string(), "A");
1506 assert_eq!(branches.len(), 2);
1507
1508 let continue_branch = branches.first();
1510 assert_eq!(continue_branch.label.to_string(), "Msg");
1511 match &continue_branch.protocol {
1512 Protocol::Send {
1513 message,
1514 continuation,
1515 ..
1516 } => {
1517 assert_eq!(message.name.to_string(), "Msg");
1518 assert!(matches!(continuation.as_ref(), Protocol::Var(_)));
1520 }
1521 _ => panic!("Expected Send"),
1522 }
1523
1524 let done_branch = &branches.as_slice()[1];
1526 assert_eq!(done_branch.label.to_string(), "Done");
1527 }
1528 _ => panic!("Expected Choice"),
1529 }
1530 }
1531 _ => panic!("Expected Rec"),
1532 }
1533 }
1534
1535 #[test]
1536 fn test_role_decides_three_roles() {
1537 let input = r#"
1539protocol ThreeRoles =
1540 roles Client, Server, Logger
1541 loop decide by Client
1542 Client -> Server : Request
1543 Server -> Logger : Log
1544 Logger -> Client : Ack
1545"#;
1546
1547 let result = parse_choreography_str(input);
1548 assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
1549
1550 let choreo = result.unwrap();
1551 match &choreo.protocol {
1552 Protocol::Rec { body, .. } => {
1553 match body.as_ref() {
1554 Protocol::Choice { role, branches, .. } => {
1555 assert_eq!(role.name().to_string(), "Client");
1556
1557 let continue_branch = branches.first();
1558 assert_eq!(continue_branch.label.to_string(), "Request");
1559
1560 match &continue_branch.protocol {
1562 Protocol::Send {
1563 from,
1564 to,
1565 message,
1566 continuation,
1567 ..
1568 } => {
1569 assert_eq!(from.name().to_string(), "Client");
1570 assert_eq!(to.name().to_string(), "Server");
1571 assert_eq!(message.name.to_string(), "Request");
1572
1573 match continuation.as_ref() {
1574 Protocol::Send {
1575 from,
1576 to,
1577 message,
1578 continuation,
1579 ..
1580 } => {
1581 assert_eq!(from.name().to_string(), "Server");
1582 assert_eq!(to.name().to_string(), "Logger");
1583 assert_eq!(message.name.to_string(), "Log");
1584
1585 match continuation.as_ref() {
1586 Protocol::Send {
1587 from,
1588 to,
1589 message,
1590 continuation,
1591 ..
1592 } => {
1593 assert_eq!(from.name().to_string(), "Logger");
1594 assert_eq!(to.name().to_string(), "Client");
1595 assert_eq!(message.name.to_string(), "Ack");
1596 assert!(matches!(
1597 continuation.as_ref(),
1598 Protocol::Var(_)
1599 ));
1600 }
1601 _ => panic!("Expected Send for Ack"),
1602 }
1603 }
1604 _ => panic!("Expected Send for Log"),
1605 }
1606 }
1607 _ => panic!("Expected Send for Request"),
1608 }
1609
1610 let done_branch = &branches.as_slice()[1];
1612 match &done_branch.protocol {
1613 Protocol::Send { from, to, .. } => {
1614 assert_eq!(from.name().to_string(), "Client");
1615 assert_eq!(to.name().to_string(), "Server");
1616 }
1617 _ => panic!("Expected Send in Done branch"),
1618 }
1619 }
1620 _ => panic!("Expected Choice"),
1621 }
1622 }
1623 _ => panic!("Expected Rec"),
1624 }
1625 }
1626
1627 #[test]
1628 fn test_role_decides_with_type_annotation() {
1629 let input = r#"
1631protocol TypedLoop =
1632 roles Client, Server
1633 loop decide by Client
1634 Client -> Server : Request of builtins.String
1635 Server -> Client : Response of builtins.U32
1636"#;
1637
1638 let result = parse_choreography_str(input);
1639 assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
1640
1641 let choreo = result.unwrap();
1642 match &choreo.protocol {
1643 Protocol::Rec { body, .. } => match body.as_ref() {
1644 Protocol::Choice { branches, .. } => {
1645 let continue_branch = branches.first();
1646 match &continue_branch.protocol {
1647 Protocol::Send {
1648 message,
1649 continuation,
1650 ..
1651 } => {
1652 assert_eq!(message.name.to_string(), "Request");
1653 assert!(message.payload.is_some());
1654 let type_str = message.payload.as_ref().unwrap().to_string();
1655 assert!(
1656 type_str.contains("String"),
1657 "Expected String type, got: {}",
1658 type_str
1659 );
1660
1661 match continuation.as_ref() {
1662 Protocol::Send { message, .. } => {
1663 assert_eq!(message.name.to_string(), "Response");
1664 assert!(message.payload.is_some());
1665 let type_str = message.payload.as_ref().unwrap().to_string();
1666 assert!(
1667 type_str.contains("U32"),
1668 "Expected U32 type, got: {}",
1669 type_str
1670 );
1671 }
1672 _ => panic!("Expected Send for Response"),
1673 }
1674 }
1675 _ => panic!("Expected Send for Request"),
1676 }
1677 }
1678 _ => panic!("Expected Choice"),
1679 },
1680 _ => panic!("Expected Rec"),
1681 }
1682 }
1683
1684 #[test]
1685 fn test_role_decides_first_stmt_is_choice_no_desugar() {
1686 let input = r#"
1689protocol FirstIsChoice =
1690 roles A, B
1691 loop decide by A
1692 choice A at
1693 | opt1 =>
1694 A -> B : Msg1
1695 | opt2 =>
1696 A -> B : Msg2
1697"#;
1698
1699 let result = parse_choreography_str(input);
1700 assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
1701
1702 let choreo = result.unwrap();
1703 match &choreo.protocol {
1705 Protocol::Loop { condition, body } => {
1706 match condition {
1707 Some(Condition::RoleDecides(role)) => {
1708 assert_eq!(role.name().to_string(), "A");
1709 }
1710 _ => panic!("Expected RoleDecides condition"),
1711 }
1712 match body.as_ref() {
1714 Protocol::Choice { .. } => {}
1715 _ => panic!("Expected Choice in body"),
1716 }
1717 }
1718 _ => panic!("Expected Loop (not desugared) when first statement is not a Send"),
1719 }
1720 }
1721
1722 #[test]
1725 fn test_role_decides_followed_by_statements() {
1726 let input = r#"
1728protocol LoopThenMore =
1729 roles A, B
1730 loop decide by A
1731 A -> B : Request
1732 B -> A : Response
1733 A -> B : Goodbye
1734"#;
1735
1736 let result = parse_choreography_str(input);
1737 assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
1738
1739 let choreo = result.unwrap();
1740 match &choreo.protocol {
1742 Protocol::Rec { body, .. } => {
1743 match body.as_ref() {
1744 Protocol::Choice { branches, .. } => {
1745 let done_branch = &branches.as_slice()[1];
1747 match &done_branch.protocol {
1748 Protocol::Send {
1749 message,
1750 continuation,
1751 ..
1752 } => {
1753 assert_eq!(message.name.to_string(), "Done");
1754 match continuation.as_ref() {
1756 Protocol::Send { message, .. } => {
1757 assert_eq!(message.name.to_string(), "Goodbye");
1758 }
1759 _ => panic!("Expected Goodbye after Done"),
1760 }
1761 }
1762 _ => panic!("Expected Send in Done branch"),
1763 }
1764 }
1765 _ => panic!("Expected Choice"),
1766 }
1767 }
1768 _ => panic!("Expected Rec"),
1769 }
1770 }
1771
1772 #[test]
1773 fn test_role_decides_multiple_loops() {
1774 let input = r#"
1776protocol TwoLoops =
1777 roles A, B
1778 loop decide by A
1779 A -> B : First
1780 loop decide by B
1781 B -> A : Second
1782"#;
1783
1784 let result = parse_choreography_str(input);
1785 assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
1786
1787 let choreo = result.unwrap();
1788 match &choreo.protocol {
1790 Protocol::Rec { label, body } => {
1791 assert_eq!(label.to_string(), "RoleDecidesLoop");
1792 match body.as_ref() {
1793 Protocol::Choice { role, branches, .. } => {
1794 assert_eq!(role.name().to_string(), "A");
1795
1796 let done_branch = &branches.as_slice()[1];
1798 match &done_branch.protocol {
1799 Protocol::Send { continuation, .. } => {
1800 match continuation.as_ref() {
1802 Protocol::Rec { body, .. } => match body.as_ref() {
1803 Protocol::Choice { role, .. } => {
1804 assert_eq!(role.name().to_string(), "B");
1805 }
1806 _ => panic!("Expected Choice in second loop"),
1807 },
1808 _ => panic!("Expected second Rec after first loop"),
1809 }
1810 }
1811 _ => panic!("Expected Send in Done branch"),
1812 }
1813 }
1814 _ => panic!("Expected Choice in first loop"),
1815 }
1816 }
1817 _ => panic!("Expected Rec"),
1818 }
1819 }
1820
1821 #[test]
1822 fn test_role_decides_empty_body_edge_case() {
1823 let input = r#"
1826protocol EmptyBody =
1827 roles A, B
1828 loop decide by A
1829 A -> B : AfterLoop
1830"#;
1831
1832 let result = parse_choreography_str(input);
1835 if let Ok(choreo) = result {
1837 match &choreo.protocol {
1839 Protocol::Loop { .. } => {
1840 }
1842 Protocol::Send { .. } => {
1843 }
1845 _ => {
1846 }
1848 }
1849 }
1850 }
1852
1853 #[test]
1854 fn test_role_decides_preserves_branch_label_from_message() {
1855 let input = r#"
1857protocol CustomMessageName =
1858 roles Producer, Consumer
1859 loop decide by Producer
1860 Producer -> Consumer : DataChunk
1861 Consumer -> Producer : Ack
1862"#;
1863
1864 let result = parse_choreography_str(input);
1865 assert!(result.is_ok());
1866
1867 let choreo = result.unwrap();
1868 match &choreo.protocol {
1869 Protocol::Rec { body, .. } => {
1870 match body.as_ref() {
1871 Protocol::Choice { branches, .. } => {
1872 let continue_branch = branches.first();
1874 assert_eq!(continue_branch.label.to_string(), "DataChunk");
1875
1876 let done_branch = &branches.as_slice()[1];
1878 assert_eq!(done_branch.label.to_string(), "Done");
1879 }
1880 _ => panic!("Expected Choice"),
1881 }
1882 }
1883 _ => panic!("Expected Rec"),
1884 }
1885 }
1886
1887 #[test]
1888 fn test_role_decides_done_message_targets_same_receiver() {
1889 let input = r#"
1891protocol TargetConsistency =
1892 roles Sender, Receiver, Observer
1893 loop decide by Sender
1894 Sender -> Receiver : Data
1895 Receiver -> Observer : Forward
1896"#;
1897
1898 let result = parse_choreography_str(input);
1899 assert!(result.is_ok());
1900
1901 let choreo = result.unwrap();
1902 match &choreo.protocol {
1903 Protocol::Rec { body, .. } => {
1904 match body.as_ref() {
1905 Protocol::Choice { branches, .. } => {
1906 let continue_branch = branches.first();
1908 match &continue_branch.protocol {
1909 Protocol::Send { to, .. } => {
1910 assert_eq!(to.name().to_string(), "Receiver");
1911 }
1912 _ => panic!("Expected Send"),
1913 }
1914
1915 let done_branch = &branches.as_slice()[1];
1917 match &done_branch.protocol {
1918 Protocol::Send { from, to, .. } => {
1919 assert_eq!(from.name().to_string(), "Sender");
1920 assert_eq!(to.name().to_string(), "Receiver");
1921 }
1922 _ => panic!("Expected Send in Done branch"),
1923 }
1924 }
1925 _ => panic!("Expected Choice"),
1926 }
1927 }
1928 _ => panic!("Expected Rec"),
1929 }
1930 }
1931
1932 #[test]
1933 fn test_parse_parallel_branches() {
1934 let input = r#"
1935protocol Parallel =
1936 roles A, B, C, D
1937 par
1938 | A -> B : Msg1
1939 | C -> D : Msg2
1940"#;
1941
1942 let result = parse_choreography_str(input);
1943 assert!(
1944 result.is_ok(),
1945 "Failed to parse parallel: {:?}",
1946 result.err()
1947 );
1948
1949 let choreo = result.unwrap();
1950 match choreo.protocol {
1951 Protocol::Parallel { protocols } => {
1952 assert_eq!(protocols.len(), 2);
1953 }
1954 _ => panic!("Expected top-level parallel protocol"),
1955 }
1956 }
1957
1958 #[test]
1959 fn test_single_branch_is_error() {
1960 let input = r#"
1961protocol SingleBranch =
1962 roles A, B
1963 par
1964 | A -> B : Msg
1965"#;
1966
1967 let result = parse_choreography_str(input);
1968 assert!(result.is_err());
1969 }
1970
1971 #[test]
1972 fn test_parse_timeout_branch_surface() {
1973 let input = r#"
1974protocol TimedRequest =
1975 roles Alice, Bob
1976 timeout 5s Alice at
1977 Alice -> Bob : Request
1978 on timeout =>
1979 Alice -> Bob : Cancel
1980"#;
1981
1982 let result = parse_choreography_str(input);
1983 assert!(
1984 result.is_ok(),
1985 "Failed to parse timeout surface: {:?}",
1986 result.err()
1987 );
1988
1989 let choreo = result.unwrap();
1990 assert_eq!(choreo.name.to_string(), "TimedRequest");
1991
1992 match &choreo.protocol {
1993 Protocol::Timeout {
1994 role,
1995 duration_ms,
1996 on_cancel,
1997 ..
1998 } => {
1999 assert_eq!(role.name().to_string(), "Alice");
2000 assert_eq!(*duration_ms, 5_000);
2001 assert!(on_cancel.is_none());
2002 }
2003 _ => panic!("Expected Timeout as first protocol"),
2004 }
2005 }
2006
2007 #[test]
2008 fn test_parse_timeout_milliseconds() {
2009 let input = r#"
2010protocol QuickTimeout =
2011 roles Client, Server
2012 timeout 500ms Client at
2013 Server -> Client : Data
2014 on timeout =>
2015 Client -> Server : Abort
2016"#;
2017
2018 let result = parse_choreography_str(input);
2019 assert!(
2020 result.is_ok(),
2021 "Failed to parse timeout with ms: {:?}",
2022 result.err()
2023 );
2024
2025 let choreo = result.unwrap();
2026 match &choreo.protocol {
2027 Protocol::Timeout { duration_ms, .. } => {
2028 assert_eq!(*duration_ms, 500);
2029 }
2030 _ => panic!("Expected Timeout as first protocol"),
2031 }
2032 }
2033
2034 #[test]
2035 fn test_parse_timeout_minutes() {
2036 let input = r#"
2037protocol LongTimeout =
2038 roles A, B
2039 timeout 2m A at
2040 B -> A : Complete
2041 on timeout =>
2042 A -> B : Timeout
2043"#;
2044
2045 let result = parse_choreography_str(input);
2046 assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
2047
2048 let choreo = result.unwrap();
2049 match &choreo.protocol {
2050 Protocol::Timeout { duration_ms, .. } => {
2051 assert_eq!(*duration_ms, 120_000);
2052 }
2053 _ => panic!("Expected Timeout"),
2054 }
2055 }
2056
2057 #[test]
2058 fn test_parse_heartbeat() {
2059 let input = r#"
2060protocol Liveness =
2061 roles Alice, Bob
2062 heartbeat Alice -> Bob every 1s on_missing(3) {
2063 Bob -> Alice : Disconnect
2064 } body {
2065 Alice -> Bob : Data
2066 }
2067"#;
2068
2069 let err = parse_choreography_str(input).expect_err("heartbeat surface should fail");
2070 assert!(err
2071 .to_string()
2072 .contains("legacy DSL construct `heartbeat` was removed"));
2073 }
2074
2075 #[test]
2076 fn test_parse_heartbeat_milliseconds() {
2077 let input = r#"
2078protocol FastHeartbeat =
2079 roles Client, Server
2080 heartbeat Client -> Server every 500ms on_missing(5) {
2081 Server -> Client : Dead
2082 } body {
2083 Client -> Server : Ping
2084 }
2085"#;
2086
2087 let err =
2088 parse_choreography_str(input).expect_err("heartbeat milliseconds surface should fail");
2089 assert!(err
2090 .to_string()
2091 .contains("legacy DSL construct `heartbeat` was removed"));
2092 }
2093
2094 #[test]
2095 fn test_parse_runtime_timeout_annotation() {
2096 let input = r#"
2097protocol TimedRequest =
2098 roles Client, Server
2099 Client { runtime_timeout : 5s } -> Server : Request
2100 Server -> Client : Response
2101"#;
2102
2103 let result = parse_choreography_str(input);
2104 assert!(
2105 result.is_ok(),
2106 "Failed to parse sender-record runtime_timeout: {:?}",
2107 result.err()
2108 );
2109
2110 let choreo = result.unwrap();
2111 match &choreo.protocol {
2112 Protocol::Send {
2113 annotations,
2114 continuation,
2115 ..
2116 } => {
2117 assert!(annotations.has_runtime_timeout());
2119 let timeout = annotations.runtime_timeout().unwrap();
2120 assert_eq!(timeout, std::time::Duration::from_secs(5));
2121
2122 match continuation.as_ref() {
2124 Protocol::Send { annotations, .. } => {
2125 assert!(!annotations.has_runtime_timeout());
2126 }
2127 _ => panic!("Expected Send for Response"),
2128 }
2129 }
2130 _ => panic!("Expected Send for Request"),
2131 }
2132 }
2133
2134 #[test]
2135 fn test_parse_multiline_runtime_timeout_annotation_with_closing_paren_on_own_line() {
2136 let input = r#"
2137protocol TimedRequest =
2138 roles Client, Server
2139 Client {
2140 runtime_timeout : 5s,
2141 }
2142 -> Server : Request
2143 Server -> Client : Response
2144"#;
2145
2146 let result = parse_choreography_str(input);
2147 assert!(
2148 result.is_ok(),
2149 "Failed to parse multiline sender-record runtime_timeout: {:?}",
2150 result.err()
2151 );
2152
2153 let choreo = result.unwrap();
2154 match &choreo.protocol {
2155 Protocol::Send { annotations, .. } => {
2156 assert!(annotations.has_runtime_timeout());
2157 assert_eq!(
2158 annotations.runtime_timeout(),
2159 Some(std::time::Duration::from_secs(5))
2160 );
2161 }
2162 _ => panic!("Expected Send for Request"),
2163 }
2164 }
2165
2166 #[test]
2167 fn test_parse_runtime_timeout_milliseconds() {
2168 let input = r#"
2169protocol QuickCheck =
2170 roles A, B
2171 A { runtime_timeout : 100ms } -> B : Ping
2172"#;
2173
2174 let result = parse_choreography_str(input);
2175 assert!(
2176 result.is_ok(),
2177 "Failed to parse sender-record runtime_timeout with ms: {:?}",
2178 result.err()
2179 );
2180
2181 let choreo = result.unwrap();
2182 match &choreo.protocol {
2183 Protocol::Send { annotations, .. } => {
2184 assert!(annotations.has_runtime_timeout());
2185 let timeout = annotations.runtime_timeout().unwrap();
2186 assert_eq!(timeout, std::time::Duration::from_millis(100));
2187 }
2188 _ => panic!("Expected Send"),
2189 }
2190 }
2191
2192 #[test]
2193 fn test_parse_parallel_annotation() {
2194 let input = r#"
2195protocol Broadcast =
2196 roles Coordinator, Worker
2197 Coordinator { parallel : true } -> Worker : Task
2198"#;
2199
2200 let result = parse_choreography_str(input);
2201 assert!(
2202 result.is_ok(),
2203 "Failed to parse sender-record parallel metadata: {:?}",
2204 result.err()
2205 );
2206
2207 let choreo = result.unwrap();
2208 match &choreo.protocol {
2209 Protocol::Send { annotations, .. } => {
2210 assert!(annotations.has_parallel(), "Expected parallel annotation");
2211 }
2212 _ => panic!("Expected Send"),
2213 }
2214 }
2215
2216 #[test]
2217 fn test_parse_choice_with_bar_prefixed_branches() {
2218 let input = r#"
2219protocol Decision =
2220 roles A, B
2221 choice A at
2222 | Accept =>
2223 A -> B : Ok
2224 | Reject =>
2225 A -> B : No
2226"#;
2227
2228 let result = parse_choreography_str(input);
2229 assert!(
2230 result.is_ok(),
2231 "Failed to parse choice with bar-prefixed branches: {:?}",
2232 result.err()
2233 );
2234
2235 let choreo = result.unwrap();
2236 match &choreo.protocol {
2237 Protocol::Choice { branches, .. } => {
2238 assert_eq!(branches.len(), 2);
2239 assert_eq!(branches.first().label.to_string(), "Accept");
2240 assert_eq!(branches.as_slice()[1].label.to_string(), "Reject");
2241 }
2242 _ => panic!("Expected Choice"),
2243 }
2244 }
2245
2246 #[test]
2247 fn test_parse_par_with_single_line_bar_branches() {
2248 let input = r#"
2249protocol ParallelBars =
2250 roles A, B, C, D
2251 par
2252 | A -> B : Left
2253 | C -> D : Right
2254"#;
2255
2256 let result = parse_choreography_str(input);
2257 assert!(
2258 result.is_ok(),
2259 "Failed to parse `par` with single-line branches: {:?}",
2260 result.err()
2261 );
2262
2263 let choreo = result.unwrap();
2264 match &choreo.protocol {
2265 Protocol::Parallel { protocols } => {
2266 assert_eq!(protocols.len(), 2);
2267 }
2268 _ => panic!("Expected Parallel"),
2269 }
2270 }
2271
2272 #[test]
2273 fn test_parse_par_with_block_branch() {
2274 let input = r#"
2275protocol ParallelBarsBlock =
2276 roles A, B, C, D
2277 par
2278 |
2279 A -> B : Left
2280 B -> A : Ack
2281 | C -> D : Right
2282"#;
2283
2284 let result = parse_choreography_str(input);
2285 assert!(
2286 result.is_ok(),
2287 "Failed to parse `par` with block branch: {:?}",
2288 result.err()
2289 );
2290
2291 let choreo = result.unwrap();
2292 match &choreo.protocol {
2293 Protocol::Parallel { protocols } => {
2294 assert_eq!(protocols.len(), 2);
2295 match &protocols.first() {
2296 Protocol::Send { continuation, .. } => {
2297 assert!(matches!(continuation.as_ref(), Protocol::Send { .. }));
2298 }
2299 _ => panic!("Expected first branch to be a send sequence"),
2300 }
2301 }
2302 _ => panic!("Expected Parallel"),
2303 }
2304 }
2305
2306 #[test]
2307 fn test_reject_par_without_bar_branches() {
2308 let input = r#"
2309protocol ParallelMissingBars =
2310 roles A, B, C, D
2311 par
2312 A -> B : Left
2313 C -> D : Right
2314"#;
2315
2316 let result = parse_choreography_str(input);
2317 assert!(
2318 result.is_err(),
2319 "`par` branches must be introduced with `|`"
2320 );
2321 }
2322
2323 #[test]
2324 fn test_parse_sender_role_annotation_block() {
2325 let input = r#"
2326protocol RoleAnnotatedSend =
2327 roles Role, OtherRole
2328 Role {
2329 annotation1 : "value",
2330 annotation2 : 100,
2331 annotation3 : another,
2332 } -> OtherRole : Message of crate.Type
2333"#;
2334
2335 let result = parse_choreography_str(input);
2336 assert!(
2337 result.is_ok(),
2338 "Failed to parse sender role annotation block: {:?}",
2339 result.err()
2340 );
2341
2342 let choreo = result.unwrap();
2343 match &choreo.protocol {
2344 Protocol::Send {
2345 from,
2346 to,
2347 message,
2348 from_annotations,
2349 ..
2350 } => {
2351 assert_eq!(from.name().to_string(), "Role");
2352 assert_eq!(to.name().to_string(), "OtherRole");
2353 assert_eq!(message.name.to_string(), "Message");
2354 assert_eq!(
2355 message.payload.as_ref().map(ToString::to_string),
2356 Some("crate :: Type".to_string())
2357 );
2358 assert_eq!(from_annotations.custom("annotation1"), Some("value"));
2359 assert_eq!(from_annotations.custom("annotation2"), Some("100"));
2360 assert_eq!(from_annotations.custom("annotation3"), Some("another"));
2361 }
2362 _ => panic!("Expected Send"),
2363 }
2364 }
2365
2366 #[test]
2367 fn test_parse_sender_record_with_aligned_arrow_layout() {
2368 let input = r#"
2369protocol StyledSend =
2370 roles Buyer, Seller
2371 Buyer { priority : high }
2372 -> Seller : Request of shop.Order
2373"#;
2374
2375 let result = parse_choreography_str(input);
2376 assert!(
2377 result.is_ok(),
2378 "Failed to parse aligned-arrow sender record syntax: {:?}",
2379 result.err()
2380 );
2381
2382 let choreo = result.unwrap();
2383 match &choreo.protocol {
2384 Protocol::Send {
2385 from_annotations,
2386 message,
2387 ..
2388 } => {
2389 assert_eq!(from_annotations.custom("priority"), Some("high"));
2390 assert_eq!(
2391 message.payload.as_ref().map(ToString::to_string),
2392 Some("shop :: Order".to_string())
2393 );
2394 }
2395 _ => panic!("Expected Send"),
2396 }
2397 }
2398
2399 #[test]
2400 fn test_parse_sender_role_annotation_block_with_indexed_role() {
2401 let input = r#"
2402protocol RoleAnnotatedIndexedSend =
2403 roles Worker[N], Coordinator
2404 Worker[0] {
2405 shard : 0,
2406 } -> Coordinator : Result
2407"#;
2408
2409 let result = parse_choreography_str(input);
2410 assert!(
2411 result.is_ok(),
2412 "Failed to parse sender annotation block on indexed role: {:?}",
2413 result.err()
2414 );
2415
2416 let choreo = result.unwrap();
2417 match &choreo.protocol {
2418 Protocol::Send {
2419 from,
2420 from_annotations,
2421 ..
2422 } => {
2423 assert_eq!(from.name().to_string(), "Worker");
2424 assert_eq!(
2425 from.index().as_ref().map(ToString::to_string),
2426 Some("0".to_string())
2427 );
2428 assert_eq!(from_annotations.custom("shard"), Some("0"));
2429 }
2430 _ => panic!("Expected Send"),
2431 }
2432 }
2433
2434 #[test]
2435 fn test_parse_sender_role_annotation_block_on_broadcast() {
2436 let input = r#"
2437protocol RoleAnnotatedBroadcast =
2438 roles Coordinator, Worker
2439 Coordinator {
2440 batch_size : 100,
2441 } ->* : Task
2442"#;
2443
2444 let result = parse_choreography_str(input);
2445 assert!(
2446 result.is_ok(),
2447 "Failed to parse sender annotation block on broadcast: {:?}",
2448 result.err()
2449 );
2450
2451 let choreo = result.unwrap();
2452 match &choreo.protocol {
2453 Protocol::Broadcast {
2454 from,
2455 from_annotations,
2456 ..
2457 } => {
2458 assert_eq!(from.name().to_string(), "Coordinator");
2459 assert_eq!(from_annotations.custom("batch_size"), Some("100"));
2460 }
2461 _ => panic!("Expected Broadcast"),
2462 }
2463 }
2464
2465 #[test]
2466 fn test_parse_sender_role_annotation_ident_list_value() {
2467 let input = r#"
2468protocol RoleAnnotatedSend =
2469 roles Alice, Bob
2470 Alice {
2471 leak : (External, Neighbor),
2472 } -> Bob : Message
2473"#;
2474
2475 let result = parse_choreography_str(input);
2476 assert!(
2477 result.is_ok(),
2478 "Failed to parse sender annotation block with ident-list value: {:?}",
2479 result.err()
2480 );
2481
2482 let choreo = result.unwrap();
2483 match &choreo.protocol {
2484 Protocol::Send {
2485 from_annotations, ..
2486 } => {
2487 assert_eq!(
2488 from_annotations.custom("leak"),
2489 Some("(External, Neighbor)")
2490 );
2491 }
2492 _ => panic!("Expected Send"),
2493 }
2494 }
2495
2496 #[test]
2497 fn test_parse_sender_role_annotation_integer_list_value() {
2498 let input = r#"
2499protocol RoleAnnotatedSend =
2500 roles Alice, Bob
2501 Alice {
2502 leakage_budget : [1, 0, 0],
2503 } -> Bob : Message
2504"#;
2505
2506 let result = parse_choreography_str(input);
2507 assert!(
2508 result.is_ok(),
2509 "Failed to parse sender annotation block with integer-list value: {:?}",
2510 result.err()
2511 );
2512
2513 let choreo = result.unwrap();
2514 match &choreo.protocol {
2515 Protocol::Send {
2516 from_annotations, ..
2517 } => {
2518 assert_eq!(from_annotations.custom("leakage_budget"), Some("[1, 0, 0]"));
2519 }
2520 _ => panic!("Expected Send"),
2521 }
2522 }
2523
2524 #[test]
2525 fn test_reject_sender_metadata_in_square_brackets() {
2526 let input = r#"
2527protocol InvalidRoleMetadata =
2528 roles Role, OtherRole
2529 Role[annotation1 : "value"] -> OtherRole : Message
2530"#;
2531
2532 let result = parse_choreography_str(input);
2533 assert!(
2534 result.is_err(),
2535 "square brackets must stay reserved for role indexing"
2536 );
2537 }
2538
2539 #[test]
2540 fn test_parse_ordered_annotation() {
2541 let input = r#"
2542protocol OrderedCollect =
2543 roles Coordinator, Worker
2544 Worker { ordered : true } -> Coordinator : Result
2545"#;
2546
2547 let result = parse_choreography_str(input);
2548 assert!(
2549 result.is_ok(),
2550 "Failed to parse sender-record ordered metadata: {:?}",
2551 result.err()
2552 );
2553
2554 let choreo = result.unwrap();
2555 match &choreo.protocol {
2556 Protocol::Send { annotations, .. } => {
2557 assert!(annotations.has_ordered(), "Expected ordered annotation");
2558 }
2559 _ => panic!("Expected Send"),
2560 }
2561 }
2562
2563 #[test]
2566 fn test_parse_min_responses_annotation() {
2567 let input = r#"
2568protocol ThresholdSign =
2569 roles Coordinator, Signer
2570 Signer { min_responses : 3 } -> Coordinator : Signature
2571"#;
2572
2573 let result = parse_choreography_str(input);
2574 assert!(
2575 result.is_ok(),
2576 "Failed to parse sender-record min_responses: {:?}",
2577 result.err()
2578 );
2579
2580 let choreo = result.unwrap();
2581 match &choreo.protocol {
2582 Protocol::Send { annotations, .. } => {
2583 assert!(
2584 annotations.has_min_responses(),
2585 "Expected min_responses annotation"
2586 );
2587 assert_eq!(annotations.min_responses(), Some(3));
2588 }
2589 _ => panic!("Expected Send"),
2590 }
2591 }
2592
2593 #[test]
2594 fn test_parse_multiline_min_responses_annotation_with_closing_paren_on_own_line() {
2595 let input = r#"
2596protocol ThresholdSign =
2597 roles Coordinator, Signer
2598 Signer {
2599 min_responses : 3,
2600 }
2601 -> Coordinator : Signature
2602"#;
2603
2604 let result = parse_choreography_str(input);
2605 assert!(
2606 result.is_ok(),
2607 "Failed to parse multiline sender-record min_responses: {:?}",
2608 result.err()
2609 );
2610
2611 let choreo = result.unwrap();
2612 match &choreo.protocol {
2613 Protocol::Send { annotations, .. } => {
2614 assert!(
2615 annotations.has_min_responses(),
2616 "Expected multiline min_responses annotation"
2617 );
2618 assert_eq!(annotations.min_responses(), Some(3));
2619 }
2620 _ => panic!("Expected Send"),
2621 }
2622 }
2623
2624 #[test]
2625 fn test_parse_combined_annotations() {
2626 let input = r#"
2627protocol ParallelThreshold =
2628 roles Coordinator, Worker
2629 Worker {
2630 parallel : true,
2631 min_responses : 2,
2632 } -> Coordinator : Vote
2633"#;
2634
2635 let result = parse_choreography_str(input);
2636 assert!(
2637 result.is_ok(),
2638 "Failed to parse combined sender-record metadata: {:?}",
2639 result.err()
2640 );
2641
2642 let choreo = result.unwrap();
2643 match &choreo.protocol {
2644 Protocol::Send { annotations, .. } => {
2645 assert!(annotations.has_parallel(), "Expected parallel annotation");
2646 assert!(
2647 annotations.has_min_responses(),
2648 "Expected min_responses annotation"
2649 );
2650 assert_eq!(annotations.min_responses(), Some(2));
2651 }
2652 _ => panic!("Expected Send"),
2653 }
2654 }
2655
2656 #[test]
2657 fn test_parse_proof_bundles_and_protocol_requires_metadata() {
2658 let input = r#"
2659proof_bundle Base requires [guard_tokens, delegation]
2660proof_bundle Extra requires [knowledge_flow]
2661protocol WithBundles requires Base, Extra =
2662 roles A, B
2663 A -> B : Ping
2664"#;
2665
2666 let choreo = parse_choreography_str(input).expect("parse should succeed");
2667 let bundles = choreo.theorem_packs();
2668 assert_eq!(bundles.len(), 2);
2669 assert_eq!(bundles[0].name, "Base");
2670 assert_eq!(
2671 bundles[0].capabilities,
2672 vec!["guard_tokens".to_string(), "delegation".to_string()]
2673 );
2674 assert_eq!(bundles[1].name, "Extra");
2675 assert_eq!(bundles[1].capabilities, vec!["knowledge_flow".to_string()]);
2676 assert_eq!(
2677 choreo.required_theorem_packs(),
2678 vec!["Base".to_string(), "Extra".to_string()]
2679 );
2680 }
2681
2682 #[test]
2683 fn test_protocol_machine_core_statements_are_rejected() {
2684 let input = r#"
2685protocol VmOps =
2686 roles A, B
2687 acquire guard as token
2688 transfer token to B with bundle Base
2689 check k for B into out
2690 A -> B : Ping
2691"#;
2692
2693 let err =
2694 parse_choreography_str(input).expect_err("protocol-machine statements should fail");
2695 assert!(err
2696 .to_string()
2697 .contains("legacy DSL construct `protocol-machine core statement` was removed"));
2698 }
2699
2700 #[test]
2701 fn test_validate_missing_required_bundle_fails() {
2702 let input = r#"
2703protocol MissingBundle requires Core =
2704 roles A, B
2705 A -> B : Ping
2706"#;
2707
2708 let choreo = parse_choreography_str(input).expect("parse should succeed");
2709 let err = choreo.validate().expect_err("validation should fail");
2710 assert!(matches!(
2711 err,
2712 ValidationError::MissingProofBundle(ref name) if name == "Core"
2713 ));
2714 }
2715
2716 #[test]
2717 fn test_validate_missing_execution_profile_fails() {
2718 let input = r#"
2719protocol NeedReplay under Replay =
2720 roles A, B
2721 A -> B : Ping
2722"#;
2723
2724 let choreo = parse_choreography_str(input).expect("parse should succeed");
2725 let err = choreo.validate().expect_err("validation should fail");
2726 assert!(err
2727 .to_string()
2728 .contains("undeclared execution profile `Replay`"));
2729 }
2730
2731 #[test]
2732 fn test_validate_duplicate_bundle_fails() {
2733 let input = r#"
2734proof_bundle Core requires [delegation]
2735proof_bundle Core requires [guard_tokens]
2736protocol DuplicateBundle requires Core =
2737 roles A, B
2738 A -> B : Ping
2739"#;
2740
2741 let choreo = parse_choreography_str(input).expect("parse should succeed");
2742 let err = choreo.validate().expect_err("validation should fail");
2743 assert!(matches!(
2744 err,
2745 ValidationError::DuplicateProofBundle(ref name) if name == "Core"
2746 ));
2747 }
2748
2749 #[test]
2750 fn test_parse_guard_predicate_rejects_non_boolean_expression() {
2751 let input = r#"
2752protocol GuardTypeCheck =
2753 roles A, B
2754 choice A at
2755 | ok when (count + 1) =>
2756 A -> B : Ack
2757 | no =>
2758 A -> B : Nack
2759"#;
2760
2761 let err = parse_choreography_str(input).expect_err("guard should fail");
2762 assert!(matches!(err, ParseError::Syntax { .. }));
2763 assert!(err.to_string().contains("boolean-like"));
2764 }
2765
2766 #[test]
2767 fn test_parse_loop_while_rejects_non_boolean_expression() {
2768 let input = r#"
2769protocol LoopTypeCheck =
2770 roles A, B
2771 loop while "count + 1"
2772 A -> B : Tick
2773"#;
2774
2775 let err = parse_choreography_str(input).expect_err("loop condition should fail");
2776 assert!(matches!(err, ParseError::InvalidCondition { .. }));
2777 assert!(err.to_string().contains("boolean-like"));
2778 }
2779
2780 #[test]
2781 fn test_projection_preserves_continuation_after_authority_binding() {
2782 let input = r#"
2783effect Runtime
2784 authoritative ready : Session -> Session
2785 {
2786 class : authoritative
2787 progress : may_block
2788 region : fragment
2789 agreement_use : required
2790 reentrancy : reject_same_fragment
2791 }
2792
2793protocol ExtensionProjection uses Runtime =
2794 roles A, B
2795 authoritative let witness = check Runtime.ready(session)
2796 A -> B : Ping
2797"#;
2798
2799 let choreo = parse_choreography_str(input).expect("parse should succeed");
2800 let role_a = choreo
2801 .roles
2802 .iter()
2803 .find(|r| r.name() == "A")
2804 .expect("role A should exist");
2805 let projected =
2806 crate::compiler::projection::project(&choreo, role_a).expect("projection must work");
2807
2808 match projected {
2809 LocalType::Send { to, .. } => assert_eq!(to.name(), "B"),
2810 other => panic!("expected send continuation projection, got {other:?}"),
2811 }
2812 }
2813
2814 #[test]
2815 fn test_parse_enriched_proof_bundle_metadata() {
2816 let input = r#"
2817proof_bundle Base version "1.0.0" issuer "did:example:issuer" constraint "fresh_nonce" constraint "sig_valid" requires [delegation, guard_tokens]
2818protocol BundleMeta requires Base =
2819 roles A, B
2820 A -> B : Ping
2821"#;
2822
2823 let choreo = parse_choreography_str(input).expect("parse should succeed");
2824 let bundles = choreo.theorem_packs();
2825 assert_eq!(bundles.len(), 1);
2826 let bundle = &bundles[0];
2827 assert_eq!(bundle.name, "Base");
2828 assert_eq!(bundle.version.as_deref(), Some("1.0.0"));
2829 assert_eq!(bundle.issuer.as_deref(), Some("did:example:issuer"));
2830 assert_eq!(
2831 bundle.constraints,
2832 vec!["fresh_nonce".to_string(), "sig_valid".to_string()]
2833 );
2834 assert_eq!(
2835 bundle.capabilities,
2836 vec!["delegation".to_string(), "guard_tokens".to_string()]
2837 );
2838 }
2839
2840 #[test]
2841 fn test_parse_execution_profiles_and_protocol_profiles() {
2842 let input = r#"
2843profile Replay fairness eventual admissibility replay escalation_window bounded
2844protocol Inferred under Replay =
2845 roles A, B
2846 A -> B : Ping
2847"#;
2848
2849 let choreo = parse_choreography_str(input).expect("parse should succeed");
2850 assert_eq!(choreo.execution_profile_declarations().len(), 1);
2851 assert_eq!(choreo.execution_profile_declarations()[0].name, "Replay");
2852 assert_eq!(
2853 choreo.protocol_execution_profiles(),
2854 vec!["Replay".to_string()]
2855 );
2856 assert!(choreo.validate().is_ok());
2857 }
2858
2859 #[test]
2860 fn test_parse_agreement_profiles_and_operation_attachment() {
2861 let input = r#"
2862agreement_profile SoftSafe
2863 visibility pending
2864 rule aura_soft_safe
2865 usable_at soft_safe
2866 finalized_at finalized
2867 evidence commit_fact
2868
2869profile Replay fairness eventual admissibility replay escalation_window bounded
2870
2871operation syncLedger(entryId : Int) at Coordinator progress LedgerProgress requires Replay within bounded on timeout => escalate on stall => diagnose agreement SoftSafe prestate ContactContext compose first_success =
2872 publish SyncQueued(entryId)
2873
2874protocol CommitLifecycle under Replay =
2875 roles Coordinator, Worker
2876 begin syncLedger(42) progress LedgerProgress requires Replay within bounded on timeout => escalate on stall => diagnose
2877 Coordinator -> Worker : Prepare
2878"#;
2879
2880 let choreo = parse_choreography_str(input).expect("parse should succeed");
2881 assert_eq!(choreo.agreement_profile_declarations().len(), 1);
2882 let agreement = &choreo.agreement_profile_declarations()[0];
2883 assert_eq!(agreement.name, "SoftSafe");
2884 assert_eq!(agreement.visibility, "pending");
2885 assert_eq!(agreement.rule, "aura_soft_safe");
2886 assert_eq!(agreement.usable_at, "soft_safe");
2887 assert_eq!(agreement.finalized_at, "finalized");
2888 assert_eq!(agreement.evidence, "commit_fact");
2889
2890 let attachment = choreo.operation_declarations()[0]
2891 .agreement
2892 .clone()
2893 .expect("operation should carry agreement metadata");
2894 assert_eq!(attachment.profile_name, "SoftSafe");
2895 assert_eq!(attachment.prestate.as_deref(), Some("ContactContext"));
2896 assert!(choreo.validate().is_ok());
2897 }
2898
2899 #[test]
2900 fn test_linear_assets_reject_double_consume() {
2901 let input = r#"
2902protocol LinearDoubleConsume =
2903 roles A, B
2904 let token = transfer Session from A to B
2905 A -> B : Use(token)
2906 A -> B : UseAgain(token)
2907"#;
2908
2909 let err = parse_choreography_str(input).expect_err("parse should fail");
2910 assert!(err.to_string().contains("consumed more than once"));
2911 }
2912
2913 #[test]
2914 fn test_linear_assets_reject_branch_divergence() {
2915 let input = r#"
2916protocol LinearBranchDivergence =
2917 roles A, B
2918 let token = transfer Session from A to B
2919 choice A at
2920 | consume =>
2921 A -> B : Use(token)
2922 | keep =>
2923 A -> B : Skip
2924"#;
2925
2926 let err = parse_choreography_str(input).expect_err("parse should fail");
2927 assert!(err.to_string().contains("diverge"));
2928 }
2929
2930 #[test]
2931 fn test_removed_first_class_combinators_are_rejected() {
2932 let input = r#"
2933protocol Combinators =
2934 roles A, B
2935 handshake A <-> B : Hello
2936 quorum_collect A -> B min 2 : Vote
2937 A -> B : Done
2938 retry 2 {
2939 A -> B : Ping
2940 }
2941"#;
2942
2943 let err = parse_choreography_str(input)
2944 .expect_err("removed first-class combinators should fail closed");
2945 assert!(err
2946 .to_string()
2947 .contains("legacy DSL construct `handshake` was removed"));
2948 }
2949
2950 #[test]
2951 fn test_parse_role_sets_and_topologies() {
2952 let input = r#"
2953role_set Signers = Alice, Bob, Carol
2954role_set Quorum = subset(Signers, 0..2)
2955cluster LocalCluster = Signers, Quorum
2956ring RingNet = Alice, Bob, Carol
2957mesh FullMesh = Alice, Bob, Carol
2958protocol TopologyAware =
2959 roles Alice, Bob
2960 Alice -> Bob : Ping
2961"#;
2962
2963 let choreo = parse_choreography_str(input).expect("parse should succeed");
2964 let role_sets = choreo.role_sets();
2965 assert_eq!(role_sets.len(), 2);
2966 assert_eq!(role_sets[0].name, "Signers");
2967 assert_eq!(
2968 role_sets[0].members,
2969 vec!["Alice".to_string(), "Bob".to_string(), "Carol".to_string()]
2970 );
2971 assert_eq!(role_sets[1].subset_of.as_deref(), Some("Signers"));
2972 assert_eq!(role_sets[1].subset_start, Some(0));
2973 assert_eq!(role_sets[1].subset_end, Some(2));
2974
2975 let topologies = choreo.topologies();
2976 assert_eq!(topologies.len(), 3);
2977 assert_eq!(topologies[0].kind, "cluster");
2978 assert_eq!(topologies[1].kind, "ring");
2979 assert_eq!(topologies[2].kind, "mesh");
2980 }
2981
2982 #[test]
2983 fn test_explain_lowering_report_for_proof_backed_surface() {
2984 let input = r#"
2985proof_bundle Spec requires [delegation]
2986protocol ExplainMe =
2987 roles A, B
2988 A -> B : Ping
2989"#;
2990
2991 let report = explain_lowering(input).expect("report generation should succeed");
2992 assert!(report.contains("Proof bundles: Spec"));
2993 assert!(report.contains("Lowering:"));
2994
2995 let choreo = parse_choreography_str(input).expect("parse should succeed");
2996 let lints = collect_dsl_lints(&choreo, LintLevel::Warn);
2997 assert!(lints.is_empty());
2998 let lsp = render_lsp_lint_diagnostics(&choreo, LintLevel::Warn);
2999 assert_eq!(lsp, "[]");
3000 }
3001
3002 #[test]
3003 fn test_typed_predicate_ir_rejects_if_expression() {
3004 let input = r#"
3005protocol PredicateTyping =
3006 roles A, B
3007 choice A at
3008 | ok when (if ready { true } else { false }) =>
3009 A -> B : Accept
3010 | no =>
3011 A -> B : Reject
3012"#;
3013
3014 let err = parse_choreography_str(input).expect_err("parse should fail");
3015 assert!(matches!(err, ParseError::Syntax { .. }));
3016 assert!(err.to_string().contains("boolean-like"));
3017 }
3018
3019 #[test]
3020 fn test_parse_choreography_rejects_proc_macro_token_input() {
3021 let input: TokenStream = quote::quote! {
3022 protocol PingPong =
3023 roles Alice, Bob
3024 Alice -> Bob : Ping
3025 Bob -> Alice : Pong
3026 };
3027
3028 let err = parse_choreography(input).expect_err("proc-macro2 token parsing should fail");
3029 assert!(err
3030 .to_string()
3031 .contains("proc-macro2 token parsing for the tell! DSL was removed"));
3032 }
3033
3034 #[test]
3035 fn test_parse_choreography_rejects_string_literal_macro_input() {
3036 let input: TokenStream = quote::quote! {
3037 r#"
3038protocol ReplicatedWrite =
3039 roles Client, Leader, Replica0, Replica1
3040 Client -> Leader : Put of kv.Write
3041"#
3042 };
3043
3044 let err = parse_choreography(input).expect_err("string literal macro input should fail");
3045 assert!(err
3046 .to_string()
3047 .contains("string-literal tell! input was removed"));
3048 }
3049
3050 #[test]
3051 fn test_parse_legacy_structural_braces_are_rejected() {
3052 let input = r#"
3053protocol Branchy = {
3054 roles A, B, C, D;
3055 par {
3056 | {
3057 choice A at {
3058 | Accept => {
3059 A -> B : Ok;
3060 }
3061 | Reject => {
3062 A -> B : No;
3063 }
3064 }
3065 }
3066 | B -> D : Right;
3067 }
3068}
3069"#;
3070
3071 let err = parse_choreography_str(input).expect_err("legacy braces should fail");
3072 assert!(err
3073 .to_string()
3074 .contains("legacy brace-based protocol blocks are removed"));
3075 }
3076
3077 #[test]
3080 fn test_parse_authority_surface_with_effects_types_and_uses() {
3081 let input = r#"
3082type CommitError =
3083 | NotReady
3084 | TimedOut
3085
3086type alias ReadyWitness =
3087{
3088 epoch : Int
3089 issuedBy : Role
3090}
3091
3092effect Runtime
3093 authoritative ready : Session -> Result CommitError ReadyWitness
3094 {
3095 class : authoritative
3096 progress : may_block
3097 region : fragment
3098 agreement_use : required
3099 reentrancy : reject_same_fragment
3100 }
3101 command transfer : TransferRequest -> Result TransferError TransferReceipt
3102 {
3103 class : best_effort
3104 progress : immediate
3105 region : session
3106 agreement_use : none
3107 reentrancy : allow
3108 }
3109
3110effect Audit
3111 observe record : AuditEvent -> Unit
3112 {
3113 class : observational
3114 progress : immediate
3115 region : global
3116 agreement_use : forbidden
3117 reentrancy : allow
3118 }
3119
3120protocol CommitFlow uses Runtime, Audit =
3121 roles Coordinator, Worker, Client
3122 authoritative let readiness = check Runtime.ready(session)
3123 case readiness of
3124 | Ok(witness) =>
3125 Coordinator -> Worker : Commit(witness)
3126 | Err(reason) =>
3127 Coordinator -> Client : Retry(reason)
3128 timeout 5s Coordinator at
3129 Worker -> Coordinator : Ready
3130 on timeout =>
3131 Coordinator -> Worker : Cancel
3132 on cancel =>
3133 Coordinator -> Client : Cancelled
3134 choice Coordinator at
3135 | Commit when check Runtime.ready(session) yields witness =>
3136 Coordinator -> Worker : CommitAgain(witness)
3137 | Abort =>
3138 Coordinator -> Worker : Abort
3139"#;
3140
3141 let choreography = parse_choreography_str(input).expect("authority surface should parse");
3142 assert_eq!(choreography.type_declarations().len(), 2);
3143 assert_eq!(choreography.effect_interface_declarations().len(), 2);
3144 assert_eq!(
3145 choreography.protocol_uses(),
3146 vec!["Runtime".to_string(), "Audit".to_string()]
3147 );
3148 let runtime_metadata = choreography.effect_contract_declarations();
3149 assert!(
3150 runtime_metadata.iter().any(|op| {
3151 op.interface_name == "Runtime"
3152 && op.operation_name == "ready"
3153 && op.authority_class == crate::ast::EffectAuthorityClass::Authoritative
3154 && op.semantic_class == "authoritative"
3155 && op.progress == "may_block"
3156 && op.region == "fragment"
3157 && op.agreement_use == "required"
3158 }),
3159 "runtime effect metadata should carry effect authority class"
3160 );
3161 choreography
3162 .validate()
3163 .expect("declared effect uses should validate");
3164 }
3165
3166 #[test]
3167 fn test_parse_let_in_and_maybe_surface() {
3168 let input = r#"
3169type alias InviteHandle =
3170{
3171 id : Int
3172}
3173
3174effect Runtime
3175 lookupInvite : Session -> Maybe InviteHandle
3176 {
3177 class : best_effort
3178 progress : immediate
3179 region : session
3180 agreement_use : none
3181 reentrancy : allow
3182 }
3183
3184protocol InviteFlow uses Runtime =
3185 roles Coordinator, Worker
3186 let invite = check Runtime.lookupInvite(session) in
3187 case invite of
3188 | Just(handle) =>
3189 Coordinator -> Worker : UseInvite(handle)
3190 | Nothing =>
3191 Coordinator -> Worker : MissingInvite
3192"#;
3193
3194 let choreography =
3195 parse_choreography_str(input).expect("let-in Maybe surface should parse");
3196 choreography
3197 .validate()
3198 .expect("effect invocation should validate");
3199 }
3200
3201 #[test]
3202 fn test_reject_non_exhaustive_result_case() {
3203 let input = r#"
3204effect Runtime
3205 ready : Session -> Result CommitError ReadyWitness
3206 {
3207 class : authoritative
3208 progress : may_block
3209 region : fragment
3210 agreement_use : required
3211 reentrancy : reject_same_fragment
3212 }
3213
3214protocol CommitFlow uses Runtime =
3215 roles Coordinator, Worker
3216 authoritative let readiness = check Runtime.ready(session)
3217 case readiness of
3218 | Ok(witness) =>
3219 Coordinator -> Worker : Commit(witness)
3220"#;
3221
3222 let err =
3223 parse_choreography_str(input).expect_err("non-exhaustive Result case should fail");
3224 assert!(!err.to_string().is_empty());
3225 }
3226
3227 #[test]
3228 fn test_reject_duplicate_linear_binding_use() {
3229 let input = r#"
3230protocol TransferFlow =
3231 roles Coordinator, Worker, Client
3232 let receipt = transfer Session from Coordinator to Worker
3233 Coordinator -> Worker : TransferAccepted(receipt)
3234 Coordinator -> Client : ReceiptAudit(receipt)
3235"#;
3236
3237 let err = parse_choreography_str(input).expect_err("duplicate linear use should fail");
3238 assert!(err.to_string().contains("consumed more than once"));
3239 }
3240
3241 #[test]
3242 fn test_reject_dropped_linear_binding_use() {
3243 let input = r#"
3244protocol TransferFlow =
3245 roles Coordinator, Worker
3246 let receipt = transfer Session from Coordinator to Worker
3247 Coordinator -> Worker : TransferAccepted
3248"#;
3249
3250 let err = parse_choreography_str(input).expect_err("dropped linear binding should fail");
3251 assert!(err.to_string().contains("never consumed"));
3252 }
3253
3254 #[test]
3255 fn test_reject_undeclared_protocol_use() {
3256 let input = r#"
3257protocol CommitFlow uses Runtime =
3258 roles Coordinator, Worker
3259 Coordinator -> Worker : Ping
3260"#;
3261
3262 let choreography = parse_choreography_str(input).expect("parse should succeed");
3263 let err = choreography
3264 .validate()
3265 .expect_err("undeclared effect interface should fail validation");
3266 assert!(err.to_string().contains("undeclared effect interface"));
3267 }
3268
3269 #[test]
3270 fn test_reject_undeclared_effect_operation_invocation() {
3271 let input = r#"
3272effect Runtime
3273 ready : Session -> Result CommitError ReadyWitness
3274 {
3275 class : authoritative
3276 progress : may_block
3277 region : fragment
3278 agreement_use : required
3279 reentrancy : reject_same_fragment
3280 }
3281
3282protocol CommitFlow uses Runtime =
3283 roles Coordinator, Worker
3284 let readiness = check Runtime.lookup(session)
3285 case readiness of
3286 | Ok(witness) =>
3287 Coordinator -> Worker : Commit(witness)
3288 | Err(reason) =>
3289 Coordinator -> Worker : Retry(reason)
3290"#;
3291
3292 let choreography = parse_choreography_str(input).expect("parse should succeed");
3293 let err = choreography
3294 .validate()
3295 .expect_err("undeclared effect operation should fail validation");
3296 assert!(err.to_string().contains("undeclared operation"));
3297 }
3298
3299 #[test]
3300 fn test_reject_duplicate_effect_declarations() {
3301 let input = r#"
3302effect Runtime
3303 ready : Session -> Result CommitError ReadyWitness
3304 {
3305 class : authoritative
3306 progress : may_block
3307 region : fragment
3308 agreement_use : required
3309 reentrancy : reject_same_fragment
3310 }
3311
3312effect Runtime
3313 transfer : TransferRequest -> Result TransferError TransferReceipt
3314 {
3315 class : best_effort
3316 progress : immediate
3317 region : session
3318 agreement_use : none
3319 reentrancy : allow
3320 }
3321
3322protocol CommitFlow uses Runtime =
3323 roles Coordinator, Worker
3324 Coordinator -> Worker : Ping
3325"#;
3326
3327 let choreography = parse_choreography_str(input).expect("parse should succeed");
3328 let err = choreography
3329 .validate()
3330 .expect_err("duplicate effect declarations should fail validation");
3331 assert!(err
3332 .to_string()
3333 .contains("duplicate effect interface declaration"));
3334 }
3335
3336 #[test]
3337 fn test_reject_observational_effect_used_with_check() {
3338 let input = r#"
3339effect Runtime
3340 observe watchPresence : Session -> PresenceView
3341 {
3342 class : observational
3343 progress : immediate
3344 region : session
3345 agreement_use : forbidden
3346 reentrancy : allow
3347 }
3348
3349protocol WatchFlow uses Runtime =
3350 roles Coordinator, Worker
3351 let presence = check Runtime.watchPresence(session)
3352 Coordinator -> Worker : Seen(presence)
3353"#;
3354
3355 let choreography = parse_choreography_str(input).expect("parse should succeed");
3356 let err = choreography
3357 .validate()
3358 .expect_err("observational effect use should fail validation");
3359 assert!(err.to_string().contains("observational"));
3360 }
3361
3362 #[test]
3363 fn test_reject_plain_binding_of_authoritative_check() {
3364 let input = r#"
3365effect Runtime
3366 authoritative ready : Session -> Result CommitError ReadyWitness
3367 {
3368 class : authoritative
3369 progress : may_block
3370 region : fragment
3371 agreement_use : required
3372 reentrancy : reject_same_fragment
3373 }
3374
3375protocol CommitFlow uses Runtime =
3376 roles Coordinator, Worker
3377 let readiness = check Runtime.ready(session)
3378 Coordinator -> Worker : Continue(readiness)
3379"#;
3380
3381 let choreography = parse_choreography_str(input).expect("parse should succeed");
3382 let err = choreography
3383 .validate()
3384 .expect_err("plain authoritative binding must fail validation");
3385 assert!(err.to_string().contains("authoritative let"));
3386 }
3387
3388 #[test]
3389 fn test_reject_plain_binding_of_observe_expression() {
3390 let input = r#"
3391effect Runtime
3392 observe watchPresence : Session -> PresenceView
3393 {
3394 class : observational
3395 progress : immediate
3396 region : session
3397 agreement_use : forbidden
3398 reentrancy : allow
3399 }
3400
3401protocol WatchFlow uses Runtime =
3402 roles Coordinator, Worker
3403 let presence = observe Runtime.watchPresence(session)
3404 Coordinator -> Worker : Seen(presence)
3405"#;
3406
3407 let choreography = parse_choreography_str(input).expect("parse should succeed");
3408 let err = choreography
3409 .validate()
3410 .expect_err("plain observe binding must fail validation");
3411 assert!(err.to_string().contains("observe let"));
3412 }
3413
3414 #[test]
3415 fn test_reject_unsupported_runtime_upgrade_surface() {
3416 let input = r#"
3417protocol UpgradeFlow =
3418 roles Coordinator, Worker
3419 upgrade runtime to Epoch2
3420 Coordinator -> Worker : Commit
3421"#;
3422
3423 let err = parse_choreography_str(input)
3424 .expect_err("runtime upgrade surface must remain outside the DSL");
3425 assert!(
3426 err.to_string().contains("upgrade") || err.to_string().contains("unexpected"),
3427 "unexpected parse error: {err}"
3428 );
3429 }
3430
3431 #[test]
3432 fn test_parse_fragments_operations_and_guest_runtime_metadata() {
3433 let input = r#"
3434fragment ChannelMembership(channel)
3435
3436profile Replay fairness eventual admissibility replay escalation_window bounded
3437agreement_profile PendingPublication
3438 visibility pending
3439 rule no_agreement
3440 usable_at provisional
3441 finalized_at finalized
3442 evidence publication
3443
3444operation syncMembership(channel : ChannelId) at Worker within ChannelMembership(channel) progress MembershipProgress agreement PendingPublication prestate ChannelMembership compose threshold_success(2) =
3445 publish SyncQueued(channel)
3446
3447guest runtime MessagingGuest =
3448 uses Runtime, Audit
3449 entry CommitFlow
3450
3451protocol CommitFlow uses Runtime, Audit under Replay =
3452 roles Coordinator, Worker
3453 Coordinator -> Worker : Ping
3454"#;
3455
3456 let choreography = parse_choreography_str(input).expect("surface metadata should parse");
3457 assert_eq!(choreography.region_declarations().len(), 1);
3458 assert_eq!(
3459 choreography.region_declarations()[0].name,
3460 "ChannelMembership"
3461 );
3462 assert_eq!(
3463 choreography.region_declarations()[0].params,
3464 vec!["channel"]
3465 );
3466
3467 assert_eq!(choreography.operation_declarations().len(), 1);
3468 let operation = &choreography.operation_declarations()[0];
3469 assert_eq!(operation.name, "syncMembership");
3470 assert_eq!(operation.owner_role, "Worker");
3471 assert_eq!(
3472 operation.within.as_deref(),
3473 Some("ChannelMembership(channel)")
3474 );
3475 assert_eq!(
3476 operation
3477 .progress_contract
3478 .as_ref()
3479 .map(|progress| progress.contract_name.as_str()),
3480 Some("MembershipProgress")
3481 );
3482 assert_eq!(
3483 operation
3484 .agreement
3485 .as_ref()
3486 .map(|agreement| agreement.profile_name.as_str()),
3487 Some("PendingPublication")
3488 );
3489 assert_eq!(
3490 operation
3491 .child_effect_aggregation
3492 .as_ref()
3493 .map(|composition| composition.dsl_name()),
3494 Some("threshold_success(2)".to_string())
3495 );
3496 assert_eq!(operation.params.len(), 1);
3497 assert!(operation
3498 .body_source
3499 .contains("publish SyncQueued(channel)"));
3500
3501 assert_eq!(choreography.guest_runtime_declarations().len(), 1);
3502 let guest_runtime = &choreography.guest_runtime_declarations()[0];
3503 assert_eq!(guest_runtime.name, "MessagingGuest");
3504 assert_eq!(guest_runtime.uses, vec!["Runtime", "Audit"]);
3505 assert_eq!(guest_runtime.entry, "CommitFlow");
3506 assert_eq!(choreography.execution_profile_declarations().len(), 1);
3507 assert_eq!(
3508 choreography.protocol_execution_profiles(),
3509 vec!["Replay".to_string()]
3510 );
3511 }
3512
3513 #[test]
3514 fn test_parse_commitment_lifecycle_and_structured_progress_metadata() {
3515 let input = r#"
3516profile Replay fairness eventual admissibility replay escalation_window bounded
3517agreement_profile SoftSafe
3518 visibility pending
3519 rule aura_soft_safe
3520 usable_at soft_safe
3521 finalized_at finalized
3522 evidence commit_fact
3523
3524operation syncLedger(entryId : Int) at Coordinator progress LedgerProgress requires Replay within bounded on timeout => escalate on stall => diagnose agreement SoftSafe prestate LedgerState compose first_success =
3525 publish SyncQueued(entryId)
3526
3527protocol CommitLifecycle under Replay =
3528 roles Coordinator, Worker
3529 begin syncLedger(42) progress LedgerProgress requires Replay within bounded on timeout => escalate on stall => diagnose
3530 Coordinator -> Worker : Prepare
3531 await syncLedger
3532 resolve syncLedger as Success
3533"#;
3534
3535 let choreography =
3536 parse_choreography_str(input).expect("commitment lifecycle surface should parse");
3537 choreography
3538 .validate()
3539 .expect("structured progress metadata should validate");
3540
3541 let operation = &choreography.operation_declarations()[0];
3542 let progress = operation
3543 .progress_contract
3544 .as_ref()
3545 .expect("operation should carry progress metadata");
3546 assert_eq!(progress.contract_name, "LedgerProgress");
3547 assert_eq!(progress.requires_profile.as_deref(), Some("Replay"));
3548 assert_eq!(progress.within_window.as_deref(), Some("bounded"));
3549 assert_eq!(progress.on_timeout.as_deref(), Some("escalate"));
3550 assert_eq!(progress.on_stall.as_deref(), Some("diagnose"));
3551 }
3552
3553 #[test]
3554 fn test_legacy_implicit_progress_contract_is_rejected() {
3555 let input = r#"
3556profile Replay fairness eventual admissibility replay escalation_window bounded
3557fragment ChannelMembership(channel)
3558agreement_profile PendingPublication
3559 visibility pending
3560 rule no_agreement
3561 usable_at provisional
3562 finalized_at finalized
3563 evidence publication
3564
3565operation syncMembership(channel : ChannelId) at Worker within ChannelMembership(channel) progress MembershipProgress agreement PendingPublication prestate ChannelMembership compose threshold_success(2) =
3566 publish SyncQueued(channel)
3567
3568protocol CommitFlow under Replay =
3569 roles Coordinator, Worker
3570 begin syncMembership(1) progress MembershipProgress
3571"#;
3572
3573 let choreography =
3574 parse_choreography_str(input).expect("legacy form still parses before validation");
3575 choreography
3576 .validate()
3577 .expect_err("legacy implicit progress metadata must be rejected");
3578 }
3579
3580 #[test]
3581 fn test_parse_authority_publication_materialization_and_handoff_fail_projection_closed() {
3582 let input = r#"
3583protocol AcceptFlow =
3584 roles Coordinator, Worker
3585 authoritative let witness = check Runtime.ready(session)
3586 observe let presence = observe Runtime.watchPresence(session)
3587 publish witness as AcceptedPublication
3588 materialize acceptedProof from AcceptedPublication
3589 let receipt = transfer Session from Coordinator to Worker
3590 handoff acceptInvite to Worker with receipt
3591 dependent work SyncMembership(channel) required for acceptInvite
3592 Coordinator -> Worker : Commit
3593"#;
3594
3595 let choreography = parse_choreography_str(input).expect("semantic surface should parse");
3596 for role in &choreography.roles {
3597 project(&choreography, role)
3598 .unwrap_or_else(|err| panic!("project {}: {err}", role.name()));
3599 }
3600 }
3601
3602 #[test]
3605 fn parse_choreography_file_accepts_tell_extension() {
3606 let dir = tempdir().expect("tempdir");
3607 let path = dir.path().join("protocol.tell");
3608 std::fs::write(&path, "protocol Ping =\n roles A, B\n A -> B : Msg\n")
3609 .expect("write tell fixture");
3610
3611 let parsed = parse_choreography_file(&path).expect("parse .tell source");
3612 assert_eq!(parsed.name.to_string(), "Ping");
3613 }
3614
3615 #[test]
3616 fn parse_choreography_file_rejects_choreo_extension() {
3617 let dir = tempdir().expect("tempdir");
3618 let path = dir.path().join("protocol.choreo");
3619 std::fs::write(&path, "protocol Ping =\n roles A, B\n A -> B : Msg\n")
3620 .expect("write choreo fixture");
3621
3622 let err = parse_choreography_file(&path).expect_err("reject legacy extension");
3623 let rendered = err.to_string();
3624 assert!(
3625 rendered.contains(".tell"),
3626 "error should point to canonical .tell extension: {rendered}"
3627 );
3628 }
3629
3630 #[test]
3631 fn reject_legacy_child_effect_aggregation_keywords() {
3632 for keyword in ["race", "fallback", "quorum(2)"] {
3633 let input = format!(
3634 r#"
3635agreement_profile SoftSafe
3636 visibility pending
3637 rule aura_soft_safe
3638 usable_at soft_safe
3639 finalized_at finalized
3640 evidence commit_fact
3641
3642profile Replay fairness eventual admissibility replay escalation_window bounded
3643
3644operation syncLedger(entryId : Int) at Coordinator progress LedgerProgress requires Replay within bounded on timeout => escalate on stall => diagnose agreement SoftSafe prestate ContactContext compose {keyword} =
3645 publish SyncQueued(entryId)
3646
3647protocol CommitLifecycle under Replay =
3648 roles Coordinator, Worker
3649 begin syncLedger(42) progress LedgerProgress requires Replay within bounded on timeout => escalate on stall => diagnose
3650 Coordinator -> Worker : Prepare
3651"#
3652 );
3653
3654 let err = parse_choreography_str(&input)
3655 .expect_err("legacy child-effect aggregation keyword should fail");
3656 let message = err.to_string();
3657 assert!(
3658 message.contains("all_success")
3659 || message.contains("first_success")
3660 || message.contains("threshold_success"),
3661 "unexpected parser error for `{keyword}`: {message}"
3662 );
3663 }
3664 }
3665}