Skip to main content

telltale_language/compiler/parser/
mod.rs

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