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