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