Skip to main content

telltale_language/compiler/parser/
mod.rs

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