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