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