mod conversion;
mod declarations;
mod error;
mod linear;
mod lints;
mod role;
mod statement;
mod stmt_parsers;
mod types;
pub use error::{ErrorSpan, ParseError};
pub use lints::{
collect_dsl_lints, explain_lowering, render_lsp_lint_diagnostics, LintDiagnostic, LintLevel,
};
use crate::ast::{
AgreementProfileDeclaration, Annotations, Choreography, EffectInterfaceDeclaration,
ExecutionProfileDeclaration, GuestRuntimeDeclaration, OperationDeclaration, Protocol,
RegionDeclaration, Role, RoleSetDeclaration, TheoremPackDeclaration, TopologyDeclaration,
TypeDeclaration,
};
use crate::compiler::layout::preprocess_layout;
use crate::extensions::{ExtensionRegistry, ParseContext, ProtocolExtension, StatementParser};
use pest::Parser;
use pest_derive::Parser;
use proc_macro2::{Span, TokenStream};
use quote::format_ident;
use regex::Regex;
use std::collections::{HashMap, HashSet};
use conversion::{convert_statements_to_protocol, inline_calls};
use declarations::{
enforce_same_line_equals, parse_agreement_profile_decl, parse_effect_decl, parse_fragment_decl,
parse_guest_runtime_decl, parse_module_decl, parse_operation_decl, parse_profile_decl,
parse_proof_bundle_decl, parse_protocol_profiles, parse_protocol_requires, parse_protocol_uses,
parse_role_set_decl, parse_topology_decl, parse_type_decl,
};
use linear::{infer_required_proof_bundles, validate_authority_surface, validate_linear_vm_assets};
use role::parse_roles_from_pair;
use statement::{parse_local_protocol_decl, parse_protocol_body};
use types::Statement;
type ParsedExtensions = (Choreography, Vec<Box<dyn ProtocolExtension>>);
#[derive(Parser)]
#[grammar = "compiler/choreography.pest"]
struct ChoreographyParser;
pub const DEFAULT_SOURCE_EXTENSION: &str = "tell";
pub fn parse_choreography_str(input: &str) -> std::result::Result<Choreography, ParseError> {
parse_choreography_str_with_extensions(input, &ExtensionRegistry::new())
.map(|(choreo, _)| choreo)
}
pub fn parse_choreography_str_with_extensions(
input: &str,
registry: &ExtensionRegistry,
) -> std::result::Result<ParsedExtensions, ParseError> {
let dedented = strip_common_indent(input);
if registry.has_extensions() {
if let Some(parsed) = try_parse_with_extension_dispatch(&dedented, registry)? {
return Ok(parsed);
}
}
parse_choreography_str_core(&dedented)
}
#[allow(clippy::too_many_lines)]
fn parse_choreography_str_core(
dedented: &str,
) -> std::result::Result<ParsedExtensions, ParseError> {
reject_legacy_structural_braces(dedented)?;
reject_removed_legacy_surfaces(dedented)?;
let layout = preprocess_layout(dedented).map_err(|e| ParseError::Layout {
span: ErrorSpan::from_line_col(e.line, e.column, dedented),
message: e.message,
})?;
let pairs = ChoreographyParser::parse(Rule::choreography, &layout).map_err(Box::new)?;
let mut name = format_ident!("Unnamed");
let mut namespace: Option<String> = None;
let mut roles: Vec<crate::ast::Role> = Vec::new();
let mut declared_roles: HashSet<String> = HashSet::new();
let mut protocol_defs: HashMap<String, Vec<Statement>> = HashMap::new();
let mut statements: Vec<Statement> = Vec::new();
let mut theorem_packs: Vec<TheoremPackDeclaration> = Vec::new();
let mut required_bundles: Vec<String> = Vec::new();
let mut protocol_uses: Vec<String> = Vec::new();
let mut protocol_profiles: Vec<String> = Vec::new();
let mut role_sets: Vec<RoleSetDeclaration> = Vec::new();
let mut topologies: Vec<TopologyDeclaration> = Vec::new();
let mut type_declarations: Vec<TypeDeclaration> = Vec::new();
let mut effect_interface_declarations: Vec<EffectInterfaceDeclaration> = Vec::new();
let mut region_declarations: Vec<RegionDeclaration> = Vec::new();
let mut operation_declarations: Vec<OperationDeclaration> = Vec::new();
let mut guest_runtime_declarations: Vec<GuestRuntimeDeclaration> = Vec::new();
let mut execution_profile_declarations: Vec<ExecutionProfileDeclaration> = Vec::new();
let mut agreement_profile_declarations: Vec<AgreementProfileDeclaration> = Vec::new();
for pair in pairs {
if pair.as_rule() == Rule::choreography {
for inner in pair.into_inner() {
match inner.as_rule() {
Rule::module_decl => {
namespace = Some(parse_module_decl(inner, &layout)?);
}
Rule::import_decl => {
}
Rule::proof_bundle_decl => {
theorem_packs.push(parse_proof_bundle_decl(inner, &layout)?);
}
Rule::profile_decl => {
execution_profile_declarations.push(parse_profile_decl(inner, &layout)?);
}
Rule::agreement_profile_decl => {
agreement_profile_declarations
.push(parse_agreement_profile_decl(inner, &layout)?);
}
Rule::role_set_decl => {
role_sets.push(parse_role_set_decl(inner, &layout)?);
}
Rule::topology_decl => {
topologies.push(parse_topology_decl(inner, &layout)?);
}
Rule::type_decl => {
type_declarations.push(parse_type_decl(inner, &layout)?);
}
Rule::effect_decl => {
effect_interface_declarations.push(parse_effect_decl(inner, &layout)?);
}
Rule::fragment_decl => {
region_declarations.push(parse_fragment_decl(inner, &layout)?);
}
Rule::operation_decl => {
operation_declarations.push(parse_operation_decl(inner, &layout)?);
}
Rule::guest_runtime_decl => {
guest_runtime_declarations.push(parse_guest_runtime_decl(inner, &layout)?);
}
Rule::protocol_decl => {
let protocol_span = inner.as_span();
enforce_same_line_equals(
inner.as_str(),
protocol_span,
&layout,
"protocol declaration",
)?;
let mut proto_inner = inner.into_inner();
let name_pair = proto_inner.next().ok_or_else(|| ParseError::Syntax {
span: ErrorSpan::from_pest_span(protocol_span, &layout),
message: "protocol declaration is missing a name".to_string(),
})?;
name = format_ident!("{}", name_pair.as_str());
let mut header_roles: Option<Vec<crate::ast::Role>> = None;
let mut body_pair: Option<pest::iterators::Pair<Rule>> = None;
let mut where_pair: Option<pest::iterators::Pair<Rule>> = None;
for item in proto_inner {
match item.as_rule() {
Rule::header_roles => {
header_roles = Some(parse_roles_from_pair(item, &layout)?);
}
Rule::protocol_requires => {
required_bundles = parse_protocol_requires(item);
}
Rule::protocol_uses => {
protocol_uses = parse_protocol_uses(item);
}
Rule::protocol_profiles => {
protocol_profiles = parse_protocol_profiles(item);
}
Rule::protocol_body => {
body_pair = Some(item);
}
Rule::where_block => {
where_pair = Some(item);
}
_ => {}
}
}
let allow_roles_decl = header_roles.is_none();
let body_pair = body_pair.ok_or_else(|| ParseError::Syntax {
span: ErrorSpan::from_pest_span(protocol_span, &layout),
message: "protocol declaration is missing a body".to_string(),
})?;
let body_span = body_pair.as_span();
let types::ParsedBody {
roles: body_roles,
statements: body_statements,
} = parse_protocol_body(
body_pair,
&declared_roles,
&layout,
&protocol_defs,
allow_roles_decl,
)?;
if header_roles.is_some() && body_roles.is_some() {
return Err(ParseError::Syntax {
span: ErrorSpan::from_pest_span(body_span, &layout),
message: "roles cannot be declared both in the header and body"
.to_string(),
});
}
if let Some(r) = header_roles {
roles = r;
declared_roles = roles.iter().map(|r| r.name().to_string()).collect();
} else if let Some(r) = body_roles {
roles = r;
declared_roles = roles.iter().map(|r| r.name().to_string()).collect();
}
if let Some(where_block) = where_pair {
for local in where_block.into_inner().flat_map(|p| p.into_inner()) {
if local.as_rule() == Rule::local_protocol_decl {
parse_local_protocol_decl(
local,
&declared_roles,
&layout,
&mut protocol_defs,
)?;
}
}
}
validate_linear_vm_assets(&body_statements, &layout)?;
validate_authority_surface(&body_statements, &layout)?;
let mut local_protocols: Vec<_> = protocol_defs.iter().collect();
local_protocols.sort_by(|(lhs, _), (rhs, _)| lhs.cmp(rhs));
for (_, local_statements) in local_protocols {
validate_linear_vm_assets(local_statements, &layout)?;
validate_authority_surface(local_statements, &layout)?;
}
statements = inline_calls(&body_statements, &protocol_defs, &layout)?;
}
_ => {}
}
}
}
}
if roles.is_empty() {
return Err(ParseError::EmptyChoreography);
}
let protocol = convert_statements_to_protocol(&statements, &roles);
let mut choreography = Choreography {
name,
namespace,
roles,
protocol,
attrs: HashMap::new(),
};
choreography
.set_theorem_packs(&theorem_packs)
.map_err(|message| ParseError::Syntax {
span: ErrorSpan::from_line_col(1, 1, &layout),
message,
})?;
let inferred_required_bundles =
infer_required_proof_bundles(&required_bundles, &theorem_packs, &statements);
let resolved_required_bundles = if required_bundles.is_empty() {
inferred_required_bundles.clone()
} else {
required_bundles.clone()
};
choreography
.set_required_theorem_packs(&resolved_required_bundles)
.map_err(|message| ParseError::Syntax {
span: ErrorSpan::from_line_col(1, 1, &layout),
message,
})?;
choreography
.set_inferred_required_theorem_packs(&inferred_required_bundles)
.map_err(|message| ParseError::Syntax {
span: ErrorSpan::from_line_col(1, 1, &layout),
message,
})?;
choreography
.set_role_sets(&role_sets)
.map_err(|message| ParseError::Syntax {
span: ErrorSpan::from_line_col(1, 1, &layout),
message,
})?;
choreography
.set_topologies(&topologies)
.map_err(|message| ParseError::Syntax {
span: ErrorSpan::from_line_col(1, 1, &layout),
message,
})?;
choreography
.set_type_declarations(&type_declarations)
.map_err(|message| ParseError::Syntax {
span: ErrorSpan::from_line_col(1, 1, &layout),
message,
})?;
choreography
.set_effect_interface_declarations(&effect_interface_declarations)
.map_err(|message| ParseError::Syntax {
span: ErrorSpan::from_line_col(1, 1, &layout),
message,
})?;
choreography
.set_protocol_uses(&protocol_uses)
.map_err(|message| ParseError::Syntax {
span: ErrorSpan::from_line_col(1, 1, &layout),
message,
})?;
choreography
.set_region_declarations(®ion_declarations)
.map_err(|message| ParseError::Syntax {
span: ErrorSpan::from_line_col(1, 1, &layout),
message,
})?;
choreography
.set_operation_declarations(&operation_declarations)
.map_err(|message| ParseError::Syntax {
span: ErrorSpan::from_line_col(1, 1, &layout),
message,
})?;
choreography
.set_guest_runtime_declarations(&guest_runtime_declarations)
.map_err(|message| ParseError::Syntax {
span: ErrorSpan::from_line_col(1, 1, &layout),
message,
})?;
choreography
.set_execution_profile_declarations(&execution_profile_declarations)
.map_err(|message| ParseError::Syntax {
span: ErrorSpan::from_line_col(1, 1, &layout),
message,
})?;
choreography
.set_agreement_profile_declarations(&agreement_profile_declarations)
.map_err(|message| ParseError::Syntax {
span: ErrorSpan::from_line_col(1, 1, &layout),
message,
})?;
choreography
.set_protocol_execution_profiles(&protocol_profiles)
.map_err(|message| ParseError::Syntax {
span: ErrorSpan::from_line_col(1, 1, &layout),
message,
})?;
Ok((choreography, Vec::new()))
}
struct ExtensionBinding<'a> {
rule_name: String,
parser: &'a dyn StatementParser,
prefix: String,
}
struct StatementChunk {
text: String,
extension_binding: Option<usize>,
}
struct ProtocolBodySplit {
stripped_input: String,
chunks: Vec<StatementChunk>,
}
fn try_parse_with_extension_dispatch(
input: &str,
registry: &ExtensionRegistry,
) -> std::result::Result<Option<ParsedExtensions>, ParseError> {
let bindings = collect_extension_bindings(registry);
if bindings.is_empty() || !input_contains_extension_candidates(input, &bindings) {
return Ok(None);
}
let split = match split_protocol_body_chunks(input, &bindings) {
Some(split) => split,
None => return Ok(None),
};
if !split
.chunks
.iter()
.any(|chunk| chunk.extension_binding.is_some())
{
return Ok(None);
}
let (mut choreography, _) = parse_choreography_str_core(&split.stripped_input)?;
let mut extensions = Vec::new();
let mut rebuilt = Protocol::End;
for chunk in &split.chunks {
let (chunk_protocol, mut chunk_extensions) =
parse_chunk_protocol(chunk, &choreography.roles, registry, input, &bindings)?;
rebuilt = append_protocol(rebuilt, chunk_protocol);
extensions.append(&mut chunk_extensions);
}
choreography.protocol = rebuilt;
choreography
.validate()
.map_err(|message| ParseError::Syntax {
span: ErrorSpan::from_line_col(1, 1, input),
message: message.to_string(),
})?;
Ok(Some((choreography, extensions)))
}
fn parse_chunk_protocol(
chunk: &StatementChunk,
roles: &[Role],
registry: &ExtensionRegistry,
original_input: &str,
bindings: &[ExtensionBinding<'_>],
) -> std::result::Result<(Protocol, Vec<Box<dyn ProtocolExtension>>), ParseError> {
if let Some(binding_idx) = chunk.extension_binding {
let binding = &bindings[binding_idx];
let ext = binding
.parser
.parse_statement(
&binding.rule_name,
chunk.text.trim(),
&ParseContext {
declared_roles: roles,
input: original_input,
},
)
.map_err(|err| ParseError::Syntax {
span: ErrorSpan::from_line_col(1, 1, original_input),
message: err.to_string(),
})?;
ext.validate(roles).map_err(|err| ParseError::Syntax {
span: ErrorSpan::from_line_col(1, 1, original_input),
message: err.to_string(),
})?;
return Ok((
Protocol::Extension {
extension: ext.clone(),
continuation: Box::new(Protocol::End),
annotations: Annotations::default(),
},
vec![ext],
));
}
let chunk_source = build_chunk_protocol_source(roles, &chunk.text);
let (choreography, extensions) =
parse_choreography_str_with_extensions(&chunk_source, registry)?;
Ok((choreography.protocol, extensions))
}
fn append_protocol(left: Protocol, right: Protocol) -> Protocol {
match left {
Protocol::End => right,
Protocol::Begin {
operation,
args,
progress,
continuation,
} => Protocol::Begin {
operation,
args,
progress,
continuation: Box::new(append_protocol(*continuation, right)),
},
Protocol::Await {
operation,
continuation,
} => Protocol::Await {
operation,
continuation: Box::new(append_protocol(*continuation, right)),
},
Protocol::Resolve {
operation,
outcome,
continuation,
} => Protocol::Resolve {
operation,
outcome,
continuation: Box::new(append_protocol(*continuation, right)),
},
Protocol::Invalidate {
operation,
continuation,
} => Protocol::Invalidate {
operation,
continuation: Box::new(append_protocol(*continuation, right)),
},
Protocol::Send {
from,
to,
message,
continuation,
annotations,
from_annotations,
to_annotations,
} => Protocol::Send {
from,
to,
message,
continuation: Box::new(append_protocol(*continuation, right)),
annotations,
from_annotations,
to_annotations,
},
Protocol::Broadcast {
from,
to_all,
message,
continuation,
annotations,
from_annotations,
} => Protocol::Broadcast {
from,
to_all,
message,
continuation: Box::new(append_protocol(*continuation, right)),
annotations,
from_annotations,
},
Protocol::Let {
name,
mode,
expr,
linear,
continuation,
} => Protocol::Let {
name,
mode,
expr,
linear,
continuation: Box::new(append_protocol(*continuation, right)),
},
Protocol::Publish {
event,
arg,
continuation,
} => Protocol::Publish {
event,
arg,
continuation: Box::new(append_protocol(*continuation, right)),
},
Protocol::PublishAuthority {
witness,
publication_name,
continuation,
} => Protocol::PublishAuthority {
witness,
publication_name,
continuation: Box::new(append_protocol(*continuation, right)),
},
Protocol::Materialize {
proof,
publication,
continuation,
} => Protocol::Materialize {
proof,
publication,
continuation: Box::new(append_protocol(*continuation, right)),
},
Protocol::Handoff {
operation,
target,
receipt,
continuation,
} => Protocol::Handoff {
operation,
target,
receipt,
continuation: Box::new(append_protocol(*continuation, right)),
},
Protocol::DependentWork {
name,
arg,
required_for,
continuation,
} => Protocol::DependentWork {
name,
arg,
required_for,
continuation: Box::new(append_protocol(*continuation, right)),
},
Protocol::Extension {
extension,
continuation,
annotations,
} => Protocol::Extension {
extension,
continuation: Box::new(append_protocol(*continuation, right)),
annotations,
},
Protocol::Choice {
role,
branches,
annotations,
} => Protocol::Choice {
role,
branches: {
let mut iter = branches.into_vec().into_iter().map(|mut branch| {
branch.protocol = append_protocol(branch.protocol, right.clone());
branch
});
let first = iter.next().expect("choice branches are non-empty");
crate::ast::NonEmptyVec::from_head_tail(first, iter.collect())
},
annotations,
},
Protocol::Case { expr, branches } => Protocol::Case {
expr,
branches: {
let mut iter = branches.into_vec().into_iter().map(|mut branch| {
branch.protocol = append_protocol(branch.protocol, right.clone());
branch
});
let first = iter.next().expect("case branches are non-empty");
crate::ast::NonEmptyVec::from_head_tail(first, iter.collect())
},
},
Protocol::Timeout {
role,
duration_ms,
body,
on_timeout,
on_cancel,
} => Protocol::Timeout {
role,
duration_ms,
body: Box::new(append_protocol(*body, right.clone())),
on_timeout: Box::new(append_protocol(*on_timeout, right.clone())),
on_cancel: on_cancel.map(|branch| Box::new(append_protocol(*branch, right.clone()))),
},
Protocol::Loop { condition, body } => Protocol::Loop {
condition,
body: Box::new(append_protocol(*body, right)),
},
Protocol::Parallel { protocols } => Protocol::Parallel {
protocols: {
let mut iter = protocols
.into_vec()
.into_iter()
.map(|protocol| append_protocol(protocol, right.clone()));
let first = iter.next().expect("parallel branches are non-empty");
crate::ast::NonEmptyVec::from_head_tail(first, iter.collect())
},
},
Protocol::Rec { label, body } => Protocol::Rec {
label,
body: Box::new(append_protocol(*body, right)),
},
Protocol::Var(label) => Protocol::Var(label),
}
}
fn build_chunk_protocol_source(roles: &[Role], chunk: &str) -> String {
let role_list = roles
.iter()
.map(|role| role.name().to_string())
.collect::<Vec<_>>()
.join(", ");
let normalized = strip_common_indent(chunk);
let body = normalized
.lines()
.map(|line| {
if line.is_empty() {
String::new()
} else {
format!(" {line}")
}
})
.collect::<Vec<_>>()
.join("\n");
format!("protocol __ExtensionChunk =\n roles {role_list}\n{body}\n")
}
fn split_protocol_body_chunks(
input: &str,
bindings: &[ExtensionBinding<'_>],
) -> Option<ProtocolBodySplit> {
let lines: Vec<&str> = input.lines().collect();
let protocol_idx = lines
.iter()
.position(|line| line.trim_start().starts_with("protocol "))?;
let mut cursor = protocol_idx + 1;
while cursor < lines.len() && is_blank_or_comment(lines[cursor]) {
cursor += 1;
}
if cursor < lines.len() && lines[cursor].trim_start().starts_with("roles ") {
cursor += 1;
}
while cursor < lines.len() && is_blank_or_comment(lines[cursor]) {
cursor += 1;
}
if cursor >= lines.len() {
return Some(ProtocolBodySplit {
stripped_input: input.to_string(),
chunks: Vec::new(),
});
}
let body_indent = indentation_width(lines[cursor]);
let mut working_lines = lines
.iter()
.map(|line| (*line).to_string())
.collect::<Vec<_>>();
let mut chunks = Vec::new();
let mut idx = cursor;
while idx < lines.len() {
let trimmed = lines[idx].trim_start();
if trimmed.is_empty() || trimmed.starts_with("--") {
idx += 1;
continue;
}
let indent = indentation_width(lines[idx]);
if indent < body_indent || (indent == body_indent && trimmed.starts_with("where")) {
break;
}
if indent > body_indent {
idx += 1;
continue;
}
let start = idx;
idx += 1;
while idx < lines.len() {
let trimmed = lines[idx].trim_start();
if trimmed.is_empty() || trimmed.starts_with("--") {
idx += 1;
continue;
}
let indent = indentation_width(lines[idx]);
if indent < body_indent || (indent == body_indent && trimmed.starts_with("where")) {
break;
}
if indent == body_indent {
break;
}
idx += 1;
}
let chunk_text = lines[start..idx].join("\n");
let extension_binding = detect_extension_binding(lines[start], bindings);
if extension_binding.is_some() {
for line in &mut working_lines[start..idx] {
line.clear();
}
}
chunks.push(StatementChunk {
text: chunk_text,
extension_binding,
});
}
let mut stripped_input = working_lines.join("\n");
if input.ends_with('\n') {
stripped_input.push('\n');
}
Some(ProtocolBodySplit {
stripped_input,
chunks,
})
}
fn detect_extension_binding(line: &str, bindings: &[ExtensionBinding<'_>]) -> Option<usize> {
let trimmed = line.trim_start();
bindings
.iter()
.position(|binding| starts_with_statement_prefix(trimmed, &binding.prefix))
}
fn input_contains_extension_candidates(input: &str, bindings: &[ExtensionBinding<'_>]) -> bool {
input
.lines()
.any(|line| detect_extension_binding(line, bindings).is_some())
}
fn collect_extension_bindings<'a>(registry: &'a ExtensionRegistry) -> Vec<ExtensionBinding<'a>> {
let mut bindings = Vec::new();
for rule_name in registry.statement_rules() {
let Some(prefix) = extension_rule_prefix(registry, rule_name) else {
continue;
};
let Some(parser_id) = registry.get_parser_for_rule(rule_name) else {
continue;
};
let Some(parser) = registry.get_statement_parser(parser_id) else {
continue;
};
bindings.push(ExtensionBinding {
rule_name: rule_name.to_string(),
parser,
prefix,
});
}
bindings.sort_by(|left, right| right.prefix.len().cmp(&left.prefix.len()));
bindings
}
fn extension_rule_prefix(registry: &ExtensionRegistry, rule_name: &str) -> Option<String> {
registry.grammar_extensions().find_map(|extension| {
if !extension.statement_rules().contains(&rule_name) {
return None;
}
extract_rule_prefix(extension.grammar_rules(), rule_name)
})
}
fn extract_rule_prefix(grammar_rules: &str, rule_name: &str) -> Option<String> {
let pattern = format!(r#"{rule_name}\s*=\s*\{{\s*"([^"]+)""#);
let regex = Regex::new(&pattern).ok()?;
regex
.captures(grammar_rules)
.and_then(|captures| captures.get(1).map(|matched| matched.as_str().to_string()))
}
fn starts_with_statement_prefix(line: &str, prefix: &str) -> bool {
let Some(rest) = line.strip_prefix(prefix) else {
return false;
};
rest.is_empty()
|| rest
.chars()
.next()
.is_some_and(|ch| ch.is_whitespace() || matches!(ch, '(' | '{' | '"' | '\''))
}
fn indentation_width(line: &str) -> usize {
line.len() - line.trim_start_matches([' ', '\t']).len()
}
fn is_blank_or_comment(line: &str) -> bool {
let trimmed = line.trim_start();
trimmed.is_empty() || trimmed.starts_with("--")
}
fn reject_legacy_structural_braces(input: &str) -> std::result::Result<(), ParseError> {
fn line_col(input: &str, offset: usize) -> (usize, usize) {
let prefix = &input[..offset];
let line = prefix.bytes().filter(|b| *b == b'\n').count() + 1;
let column = prefix
.rsplit('\n')
.next()
.map_or(1, |segment| segment.chars().count() + 1);
(line, column)
}
fn sanitize(input: &str) -> String {
let mut out = String::with_capacity(input.len());
let chars: Vec<char> = input.chars().collect();
let mut idx = 0usize;
let mut in_string = false;
let mut in_block_comment = false;
let mut escape = false;
while idx < chars.len() {
let ch = chars[idx];
let next = chars.get(idx + 1).copied();
if in_block_comment {
if ch == '-' && next == Some('}') {
out.push(' ');
out.push(' ');
in_block_comment = false;
idx += 2;
continue;
}
out.push(if ch == '\n' { '\n' } else { ' ' });
idx += 1;
continue;
}
if in_string {
if escape {
escape = false;
out.push(' ');
idx += 1;
continue;
}
if ch == '\\' {
escape = true;
out.push(' ');
idx += 1;
continue;
}
if ch == '"' {
in_string = false;
out.push(' ');
idx += 1;
continue;
}
out.push(if ch == '\n' { '\n' } else { ' ' });
idx += 1;
continue;
}
if ch == '-' && next == Some('-') {
out.push(' ');
out.push(' ');
idx += 2;
while idx < chars.len() {
let line_ch = chars[idx];
out.push(if line_ch == '\n' { '\n' } else { ' ' });
idx += 1;
if line_ch == '\n' {
break;
}
}
continue;
}
if ch == '{' && next == Some('-') {
in_block_comment = true;
out.push(' ');
out.push(' ');
idx += 2;
continue;
}
if ch == '"' {
in_string = true;
out.push(' ');
idx += 1;
continue;
}
out.push(ch);
idx += 1;
}
out
}
let sanitized = sanitize(input);
let patterns = [
(
Regex::new(r"(?s)\bprotocol\b[^{}=\n]*=\s*\{").expect("protocol block regex"),
"legacy brace-based protocol blocks are removed; use indentation after `protocol ... =`",
),
(
Regex::new(r"(?m)\bprotocol\b[^{}=\n]*\{").expect("protocol header brace regex"),
"legacy brace-based protocol blocks are removed; keep `=` on the header line and use indentation",
),
(
Regex::new(r"(?s)\boperation\b[^{}=\n]*=\s*\{").expect("operation block regex"),
"legacy brace-based operation blocks are removed; use indentation after `operation ... =`",
),
(
Regex::new(r"(?s)\bguest\s+runtime\b[^{}=\n]*=\s*\{")
.expect("guest runtime block regex"),
"legacy brace-based guest runtime blocks are removed; use indentation after `guest runtime ... =`",
),
(
Regex::new(r"(?m)\bwhere\s*\{").expect("where block regex"),
"legacy brace-based `where` blocks are removed; use indentation for local protocol declarations",
),
(
Regex::new(r"(?s)\bchoice\b.*?\bat\s*\{").expect("choice block regex"),
"legacy brace-based choice blocks are removed; use indentation after `choice Role at`",
),
(
Regex::new(r"(?s)\bcase\b.*?\bof\s*\{").expect("case block regex"),
"legacy brace-based case blocks are removed; use indentation after `case ... of`",
),
(
Regex::new(r"(?s)=>\s*\{").expect("branch body regex"),
"legacy brace-based branch bodies are removed; place the branch body on the next indented lines after `=>`",
),
(
Regex::new(r"(?m)\bpar\s*\{").expect("par block regex"),
"legacy brace-based `par` blocks are removed; use indentation with leading `|` branches",
),
(
Regex::new(r"(?s)\brec\b[^{\n]*\{").expect("rec block regex"),
"legacy brace-based `rec` blocks are removed; use indentation after `rec Name`",
),
(
Regex::new(r"(?s)\bloop\b.*?\{").expect("loop block regex"),
"legacy brace-based loop blocks are removed; use indentation after the loop header",
),
(
Regex::new(r"(?s)\btimeout\b.*?\bat\s*\{").expect("timeout block regex"),
"legacy brace-based timeout bodies are removed; use indentation after `timeout duration Role at`",
),
(
Regex::new(r"(?s)\bon\s+timeout\s*=>\s*\{").expect("timeout branch regex"),
"legacy brace-based timeout branches are removed; use indentation after `on timeout =>`",
),
(
Regex::new(r"(?s)\bon\s+cancel\s*=>\s*\{").expect("cancel branch regex"),
"legacy brace-based cancel branches are removed; use indentation after `on cancel =>`",
),
(
Regex::new(r"(?s)\bin\s*\{").expect("let-in block regex"),
"legacy brace-based `let ... in` bodies are removed; use indentation after `in`",
),
];
for (pattern, message) in patterns {
if let Some(found) = pattern.find(&sanitized) {
let brace_offset = sanitized[found.start()..found.end()]
.rfind('{')
.map(|idx| found.start() + idx)
.unwrap_or(found.start());
let (line, column) = line_col(input, brace_offset);
return Err(ParseError::Syntax {
span: ErrorSpan::from_line_col(line, column, input),
message: message.to_string(),
});
}
}
Ok(())
}
fn reject_removed_legacy_surfaces(input: &str) -> std::result::Result<(), ParseError> {
fn line_col(input: &str, offset: usize) -> (usize, usize) {
let prefix = &input[..offset];
let line = prefix.bytes().filter(|b| *b == b'\n').count() + 1;
let column = prefix
.rsplit('\n')
.next()
.map_or(1, |segment| segment.chars().count() + 1);
(line, column)
}
let patterns = [
(
Regex::new(r"(?m)^\s*heartbeat\b").expect("heartbeat regex"),
"legacy DSL construct `heartbeat` was removed from the proof-backed language surface; model liveness with explicit `timeout`, effect outcomes, and progress contracts instead",
),
(
Regex::new(r"(?m)^\s*handshake\b").expect("handshake regex"),
"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",
),
(
Regex::new(r"(?m)^\s*retry\b").expect("retry regex"),
"legacy DSL construct `retry` was removed from the proof-backed language surface; express retry policy through effects, choices, and progress contracts",
),
(
Regex::new(r"(?m)^\s*quorum_collect\b").expect("quorum regex"),
"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",
),
(
Regex::new(r"(?m)^\s*(acquire|release|fork|join|abort|delegate|tag)\b")
.expect("protocol machine regex"),
"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",
),
(
Regex::new(r"(?m)^\s*check\s+\w+\s+for\s+").expect("protocol machine check regex"),
"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",
),
(
Regex::new(r"(?m)^\s*transfer\s+\w+\s+to\s+\w+").expect("protocol machine transfer regex"),
"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",
),
];
for (pattern, message) in patterns {
if let Some(found) = pattern.find(input) {
let (line, column) = line_col(input, found.start());
return Err(ParseError::Syntax {
span: ErrorSpan::from_line_col(line, column, input),
message: message.to_string(),
});
}
}
Ok(())
}
fn strip_common_indent(input: &str) -> String {
let lines: Vec<&str> = input.lines().collect();
let mut min_indent: Option<usize> = None;
for line in &lines {
if line.trim().is_empty() {
continue;
}
let indent = line.chars().take_while(|c| *c == ' ').count();
min_indent = Some(match min_indent {
Some(current) => current.min(indent),
None => indent,
});
}
let min_indent = min_indent.unwrap_or(0);
if min_indent == 0 {
return input.to_string();
}
let mut out = String::new();
for (idx, line) in lines.iter().enumerate() {
let stripped = line.get(min_indent..).unwrap_or(line);
out.push_str(stripped);
if idx + 1 < lines.len() {
out.push('\n');
}
}
if input.ends_with('\n') {
out.push('\n');
}
out
}
pub fn parse_choreography(input: TokenStream) -> syn::Result<Choreography> {
use syn::LitStr;
if let Ok(lit_str) = syn::parse2::<LitStr>(input.clone()) {
return Err(syn::Error::new(
lit_str.span(),
"string-literal tell! input was removed; use parse_choreography_str for DSL strings or the tell! proc macro with canonical token syntax",
));
}
Err(syn::Error::new(
proc_macro2::Span::call_site(),
"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",
))
}
pub fn parse_choreography_file(
path: &std::path::Path,
) -> std::result::Result<Choreography, ParseError> {
if path.extension().and_then(std::ffi::OsStr::to_str) != Some(DEFAULT_SOURCE_EXTENSION) {
return Err(ParseError::Syntax {
span: ErrorSpan {
line: 1,
column: 1,
line_end: 1,
column_end: 1,
snippet: format!("Unsupported source file extension: {}", path.display()),
},
message: format!(
"Telltale source files must use the .{DEFAULT_SOURCE_EXTENSION} extension"
),
});
}
let content = std::fs::read_to_string(path).map_err(|e| ParseError::Syntax {
span: ErrorSpan {
line: 1,
column: 1,
line_end: 1,
column_end: 1,
snippet: format!("Failed to read file: {}", path.display()),
},
message: e.to_string(),
})?;
parse_choreography_str(&content)
}
pub fn parse_dsl(input: &str) -> std::result::Result<Choreography, ParseError> {
parse_choreography_str(input)
}
#[must_use]
pub fn choreography_macro(input: TokenStream) -> TokenStream {
let source = input.to_string();
let choreography = match parse_choreography(input) {
Ok(c) => c,
Err(e) => return e.to_compile_error(),
};
if let Err(e) = choreography.validate() {
return syn::Error::new(Span::call_site(), e.to_string()).to_compile_error();
}
match crate::generate_protocol_module(&choreography, &source) {
Ok(tokens) => tokens,
Err(err) => err.to_compile_error(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{Condition, LocalType, Protocol, ValidationError};
use crate::compiler::parser::parse_choreography_str;
use crate::compiler::projection::project;
use proc_macro2::TokenStream;
use tempfile::tempdir;
#[test]
fn test_parse_simple_send() {
let input = r#"
protocol SimpleSend =
roles Alice, Bob
Alice -> Bob : Hello
"#;
let result = parse_choreography_str(input);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let choreo = result.unwrap();
assert_eq!(choreo.name.to_string(), "SimpleSend");
assert_eq!(choreo.roles.len(), 2);
}
#[test]
fn test_parse_with_choice() {
let input = r#"
protocol Negotiation =
roles Buyer, Seller
Buyer -> Seller : Offer
choice Seller at
| accept =>
Seller -> Buyer : Accept
| reject =>
Seller -> Buyer : Reject
"#;
let result = parse_choreography_str(input);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let choreo = result.unwrap();
assert_eq!(choreo.name.to_string(), "Negotiation");
}
#[test]
fn test_parse_choice_alias() {
let input = r#"
protocol AliasChoice =
roles A, B
choice A at
| ok =>
A -> B : Ack
| fail =>
A -> B : Nack
"#;
let result = parse_choreography_str(input);
assert!(
result.is_ok(),
"Failed to parse alias choice: {:?}",
result.err()
);
}
#[test]
fn test_parse_undefined_role() {
let input = r#"
protocol Invalid =
roles Alice
Alice -> Bob : Hello
"#;
let result = parse_choreography_str(input);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, ParseError::UndefinedRole { .. }));
let err_str = err.to_string();
assert!(err_str.contains("Undefined role"));
assert!(err_str.contains("Bob"));
}
#[test]
fn test_parse_duplicate_role() {
let input = r#"
protocol DuplicateRole =
roles Alice, Bob, Alice
Alice -> Bob : Hello
"#;
let result = parse_choreography_str(input);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, ParseError::DuplicateRole { .. }));
let err_str = err.to_string();
assert!(err_str.contains("Duplicate role"));
assert!(err_str.contains("Alice"));
}
#[test]
fn test_parse_loop_repeat() {
let input = r#"
protocol LoopProtocol =
roles Client, Server
loop repeat 3
Client -> Server : Request
Server -> Client : Response
"#;
let result = parse_choreography_str(input);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
}
#[test]
fn test_parse_loop_decide() {
let input = r#"
protocol DecideLoop =
roles Client, Server
loop decide by Client
Client -> Server : Ping
Server -> Client : Pong
"#;
let result = parse_choreography_str(input);
assert!(
result.is_ok(),
"Failed to parse decide loop: {:?}",
result.err()
);
}
#[test]
fn test_role_decides_desugaring() {
let input = r#"
protocol DecideLoop =
roles Client, Server
loop decide by Client
Client -> Server : Ping
Server -> Client : Pong
"#;
let result = parse_choreography_str(input);
assert!(
result.is_ok(),
"Failed to parse decide loop: {:?}",
result.err()
);
let choreo = result.unwrap();
match &choreo.protocol {
Protocol::Rec { label, body } => {
assert_eq!(label.to_string(), "RoleDecidesLoop");
match body.as_ref() {
Protocol::Choice { role, branches, .. } => {
assert_eq!(role.name().to_string(), "Client");
assert_eq!(branches.len(), 2);
let continue_branch = branches.first();
assert_eq!(continue_branch.label.to_string(), "Ping");
match &continue_branch.protocol {
Protocol::Send {
from,
to,
message,
continuation,
..
} => {
assert_eq!(from.name().to_string(), "Client");
assert_eq!(to.name().to_string(), "Server");
assert_eq!(message.name.to_string(), "Ping");
match continuation.as_ref() {
Protocol::Send {
from,
to,
message,
continuation,
..
} => {
assert_eq!(from.name().to_string(), "Server");
assert_eq!(to.name().to_string(), "Client");
assert_eq!(message.name.to_string(), "Pong");
match continuation.as_ref() {
Protocol::Var(label) => {
assert_eq!(label.to_string(), "RoleDecidesLoop");
}
_ => panic!(
"Expected Var for continue, got {:?}",
continuation
),
}
}
_ => panic!("Expected Send for Pong, got {:?}", continuation),
}
}
_ => {
panic!("Expected Send for Ping, got {:?}", continue_branch.protocol)
}
}
let done_branch = &branches.as_slice()[1];
assert_eq!(done_branch.label.to_string(), "Done");
match &done_branch.protocol {
Protocol::Send {
from,
to,
message,
continuation,
..
} => {
assert_eq!(from.name().to_string(), "Client");
assert_eq!(to.name().to_string(), "Server");
assert_eq!(message.name.to_string(), "Done");
assert!(matches!(continuation.as_ref(), Protocol::End));
}
_ => panic!("Expected Send for Done, got {:?}", done_branch.protocol),
}
}
_ => panic!("Expected Choice inside Rec, got {:?}", body),
}
}
_ => panic!("Expected Rec at top level, got {:?}", choreo.protocol),
}
}
#[test]
fn test_role_decides_wrong_first_sender_no_desugar() {
let input = r#"
protocol WrongSender =
roles Client, Server
loop decide by Client
Server -> Client : Response
Client -> Server : Ack
"#;
let result = parse_choreography_str(input);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let choreo = result.unwrap();
match &choreo.protocol {
Protocol::Loop { condition, .. } => match condition {
Some(Condition::RoleDecides(role)) => {
assert_eq!(role.name().to_string(), "Client");
}
_ => panic!("Expected RoleDecides condition"),
},
_ => panic!(
"Expected Loop (not desugared) when first sender doesn't match deciding role"
),
}
}
#[test]
fn test_role_decides_single_message() {
let input = r#"
protocol SingleMessage =
roles A, B
loop decide by A
A -> B : Msg
"#;
let result = parse_choreography_str(input);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let choreo = result.unwrap();
match &choreo.protocol {
Protocol::Rec { label, body } => {
assert_eq!(label.to_string(), "RoleDecidesLoop");
match body.as_ref() {
Protocol::Choice { role, branches, .. } => {
assert_eq!(role.name().to_string(), "A");
assert_eq!(branches.len(), 2);
let continue_branch = branches.first();
assert_eq!(continue_branch.label.to_string(), "Msg");
match &continue_branch.protocol {
Protocol::Send {
message,
continuation,
..
} => {
assert_eq!(message.name.to_string(), "Msg");
assert!(matches!(continuation.as_ref(), Protocol::Var(_)));
}
_ => panic!("Expected Send"),
}
let done_branch = &branches.as_slice()[1];
assert_eq!(done_branch.label.to_string(), "Done");
}
_ => panic!("Expected Choice"),
}
}
_ => panic!("Expected Rec"),
}
}
#[test]
fn test_role_decides_three_roles() {
let input = r#"
protocol ThreeRoles =
roles Client, Server, Logger
loop decide by Client
Client -> Server : Request
Server -> Logger : Log
Logger -> Client : Ack
"#;
let result = parse_choreography_str(input);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let choreo = result.unwrap();
match &choreo.protocol {
Protocol::Rec { body, .. } => {
match body.as_ref() {
Protocol::Choice { role, branches, .. } => {
assert_eq!(role.name().to_string(), "Client");
let continue_branch = branches.first();
assert_eq!(continue_branch.label.to_string(), "Request");
match &continue_branch.protocol {
Protocol::Send {
from,
to,
message,
continuation,
..
} => {
assert_eq!(from.name().to_string(), "Client");
assert_eq!(to.name().to_string(), "Server");
assert_eq!(message.name.to_string(), "Request");
match continuation.as_ref() {
Protocol::Send {
from,
to,
message,
continuation,
..
} => {
assert_eq!(from.name().to_string(), "Server");
assert_eq!(to.name().to_string(), "Logger");
assert_eq!(message.name.to_string(), "Log");
match continuation.as_ref() {
Protocol::Send {
from,
to,
message,
continuation,
..
} => {
assert_eq!(from.name().to_string(), "Logger");
assert_eq!(to.name().to_string(), "Client");
assert_eq!(message.name.to_string(), "Ack");
assert!(matches!(
continuation.as_ref(),
Protocol::Var(_)
));
}
_ => panic!("Expected Send for Ack"),
}
}
_ => panic!("Expected Send for Log"),
}
}
_ => panic!("Expected Send for Request"),
}
let done_branch = &branches.as_slice()[1];
match &done_branch.protocol {
Protocol::Send { from, to, .. } => {
assert_eq!(from.name().to_string(), "Client");
assert_eq!(to.name().to_string(), "Server");
}
_ => panic!("Expected Send in Done branch"),
}
}
_ => panic!("Expected Choice"),
}
}
_ => panic!("Expected Rec"),
}
}
#[test]
fn test_role_decides_with_type_annotation() {
let input = r#"
protocol TypedLoop =
roles Client, Server
loop decide by Client
Client -> Server : Request of builtins.String
Server -> Client : Response of builtins.U32
"#;
let result = parse_choreography_str(input);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let choreo = result.unwrap();
match &choreo.protocol {
Protocol::Rec { body, .. } => match body.as_ref() {
Protocol::Choice { branches, .. } => {
let continue_branch = branches.first();
match &continue_branch.protocol {
Protocol::Send {
message,
continuation,
..
} => {
assert_eq!(message.name.to_string(), "Request");
assert!(message.payload.is_some());
let type_str = message.payload.as_ref().unwrap().to_string();
assert!(
type_str.contains("String"),
"Expected String type, got: {}",
type_str
);
match continuation.as_ref() {
Protocol::Send { message, .. } => {
assert_eq!(message.name.to_string(), "Response");
assert!(message.payload.is_some());
let type_str = message.payload.as_ref().unwrap().to_string();
assert!(
type_str.contains("U32"),
"Expected U32 type, got: {}",
type_str
);
}
_ => panic!("Expected Send for Response"),
}
}
_ => panic!("Expected Send for Request"),
}
}
_ => panic!("Expected Choice"),
},
_ => panic!("Expected Rec"),
}
}
#[test]
fn test_role_decides_first_stmt_is_choice_no_desugar() {
let input = r#"
protocol FirstIsChoice =
roles A, B
loop decide by A
choice A at
| opt1 =>
A -> B : Msg1
| opt2 =>
A -> B : Msg2
"#;
let result = parse_choreography_str(input);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let choreo = result.unwrap();
match &choreo.protocol {
Protocol::Loop { condition, body } => {
match condition {
Some(Condition::RoleDecides(role)) => {
assert_eq!(role.name().to_string(), "A");
}
_ => panic!("Expected RoleDecides condition"),
}
match body.as_ref() {
Protocol::Choice { .. } => {}
_ => panic!("Expected Choice in body"),
}
}
_ => panic!("Expected Loop (not desugared) when first statement is not a Send"),
}
}
#[test]
fn test_role_decides_followed_by_statements() {
let input = r#"
protocol LoopThenMore =
roles A, B
loop decide by A
A -> B : Request
B -> A : Response
A -> B : Goodbye
"#;
let result = parse_choreography_str(input);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let choreo = result.unwrap();
match &choreo.protocol {
Protocol::Rec { body, .. } => {
match body.as_ref() {
Protocol::Choice { branches, .. } => {
let done_branch = &branches.as_slice()[1];
match &done_branch.protocol {
Protocol::Send {
message,
continuation,
..
} => {
assert_eq!(message.name.to_string(), "Done");
match continuation.as_ref() {
Protocol::Send { message, .. } => {
assert_eq!(message.name.to_string(), "Goodbye");
}
_ => panic!("Expected Goodbye after Done"),
}
}
_ => panic!("Expected Send in Done branch"),
}
}
_ => panic!("Expected Choice"),
}
}
_ => panic!("Expected Rec"),
}
}
#[test]
fn test_role_decides_multiple_loops() {
let input = r#"
protocol TwoLoops =
roles A, B
loop decide by A
A -> B : First
loop decide by B
B -> A : Second
"#;
let result = parse_choreography_str(input);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let choreo = result.unwrap();
match &choreo.protocol {
Protocol::Rec { label, body } => {
assert_eq!(label.to_string(), "RoleDecidesLoop");
match body.as_ref() {
Protocol::Choice { role, branches, .. } => {
assert_eq!(role.name().to_string(), "A");
let done_branch = &branches.as_slice()[1];
match &done_branch.protocol {
Protocol::Send { continuation, .. } => {
match continuation.as_ref() {
Protocol::Rec { body, .. } => match body.as_ref() {
Protocol::Choice { role, .. } => {
assert_eq!(role.name().to_string(), "B");
}
_ => panic!("Expected Choice in second loop"),
},
_ => panic!("Expected second Rec after first loop"),
}
}
_ => panic!("Expected Send in Done branch"),
}
}
_ => panic!("Expected Choice in first loop"),
}
}
_ => panic!("Expected Rec"),
}
}
#[test]
fn test_role_decides_empty_body_edge_case() {
let input = r#"
protocol EmptyBody =
roles A, B
loop decide by A
A -> B : AfterLoop
"#;
let result = parse_choreography_str(input);
if let Ok(choreo) = result {
match &choreo.protocol {
Protocol::Loop { .. } => {
}
Protocol::Send { .. } => {
}
_ => {
}
}
}
}
#[test]
fn test_role_decides_preserves_branch_label_from_message() {
let input = r#"
protocol CustomMessageName =
roles Producer, Consumer
loop decide by Producer
Producer -> Consumer : DataChunk
Consumer -> Producer : Ack
"#;
let result = parse_choreography_str(input);
assert!(result.is_ok());
let choreo = result.unwrap();
match &choreo.protocol {
Protocol::Rec { body, .. } => {
match body.as_ref() {
Protocol::Choice { branches, .. } => {
let continue_branch = branches.first();
assert_eq!(continue_branch.label.to_string(), "DataChunk");
let done_branch = &branches.as_slice()[1];
assert_eq!(done_branch.label.to_string(), "Done");
}
_ => panic!("Expected Choice"),
}
}
_ => panic!("Expected Rec"),
}
}
#[test]
fn test_role_decides_done_message_targets_same_receiver() {
let input = r#"
protocol TargetConsistency =
roles Sender, Receiver, Observer
loop decide by Sender
Sender -> Receiver : Data
Receiver -> Observer : Forward
"#;
let result = parse_choreography_str(input);
assert!(result.is_ok());
let choreo = result.unwrap();
match &choreo.protocol {
Protocol::Rec { body, .. } => {
match body.as_ref() {
Protocol::Choice { branches, .. } => {
let continue_branch = branches.first();
match &continue_branch.protocol {
Protocol::Send { to, .. } => {
assert_eq!(to.name().to_string(), "Receiver");
}
_ => panic!("Expected Send"),
}
let done_branch = &branches.as_slice()[1];
match &done_branch.protocol {
Protocol::Send { from, to, .. } => {
assert_eq!(from.name().to_string(), "Sender");
assert_eq!(to.name().to_string(), "Receiver");
}
_ => panic!("Expected Send in Done branch"),
}
}
_ => panic!("Expected Choice"),
}
}
_ => panic!("Expected Rec"),
}
}
#[test]
fn test_parse_parallel_branches() {
let input = r#"
protocol Parallel =
roles A, B, C, D
par
| A -> B : Msg1
| C -> D : Msg2
"#;
let result = parse_choreography_str(input);
assert!(
result.is_ok(),
"Failed to parse parallel: {:?}",
result.err()
);
let choreo = result.unwrap();
match choreo.protocol {
Protocol::Parallel { protocols } => {
assert_eq!(protocols.len(), 2);
}
_ => panic!("Expected top-level parallel protocol"),
}
}
#[test]
fn test_single_branch_is_error() {
let input = r#"
protocol SingleBranch =
roles A, B
par
| A -> B : Msg
"#;
let result = parse_choreography_str(input);
assert!(result.is_err());
}
#[test]
fn test_parse_timeout_branch_surface() {
let input = r#"
protocol TimedRequest =
roles Alice, Bob
timeout 5s Alice at
Alice -> Bob : Request
on timeout =>
Alice -> Bob : Cancel
"#;
let result = parse_choreography_str(input);
assert!(
result.is_ok(),
"Failed to parse timeout surface: {:?}",
result.err()
);
let choreo = result.unwrap();
assert_eq!(choreo.name.to_string(), "TimedRequest");
match &choreo.protocol {
Protocol::Timeout {
role,
duration_ms,
on_cancel,
..
} => {
assert_eq!(role.name().to_string(), "Alice");
assert_eq!(*duration_ms, 5_000);
assert!(on_cancel.is_none());
}
_ => panic!("Expected Timeout as first protocol"),
}
}
#[test]
fn test_parse_timeout_milliseconds() {
let input = r#"
protocol QuickTimeout =
roles Client, Server
timeout 500ms Client at
Server -> Client : Data
on timeout =>
Client -> Server : Abort
"#;
let result = parse_choreography_str(input);
assert!(
result.is_ok(),
"Failed to parse timeout with ms: {:?}",
result.err()
);
let choreo = result.unwrap();
match &choreo.protocol {
Protocol::Timeout { duration_ms, .. } => {
assert_eq!(*duration_ms, 500);
}
_ => panic!("Expected Timeout as first protocol"),
}
}
#[test]
fn test_parse_timeout_minutes() {
let input = r#"
protocol LongTimeout =
roles A, B
timeout 2m A at
B -> A : Complete
on timeout =>
A -> B : Timeout
"#;
let result = parse_choreography_str(input);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let choreo = result.unwrap();
match &choreo.protocol {
Protocol::Timeout { duration_ms, .. } => {
assert_eq!(*duration_ms, 120_000);
}
_ => panic!("Expected Timeout"),
}
}
#[test]
fn test_parse_heartbeat() {
let input = r#"
protocol Liveness =
roles Alice, Bob
heartbeat Alice -> Bob every 1s on_missing(3) {
Bob -> Alice : Disconnect
} body {
Alice -> Bob : Data
}
"#;
let err = parse_choreography_str(input).expect_err("heartbeat surface should fail");
assert!(err
.to_string()
.contains("legacy DSL construct `heartbeat` was removed"));
}
#[test]
fn test_parse_heartbeat_milliseconds() {
let input = r#"
protocol FastHeartbeat =
roles Client, Server
heartbeat Client -> Server every 500ms on_missing(5) {
Server -> Client : Dead
} body {
Client -> Server : Ping
}
"#;
let err =
parse_choreography_str(input).expect_err("heartbeat milliseconds surface should fail");
assert!(err
.to_string()
.contains("legacy DSL construct `heartbeat` was removed"));
}
#[test]
fn test_parse_runtime_timeout_annotation() {
let input = r#"
protocol TimedRequest =
roles Client, Server
Client { runtime_timeout : 5s } -> Server : Request
Server -> Client : Response
"#;
let result = parse_choreography_str(input);
assert!(
result.is_ok(),
"Failed to parse sender-record runtime_timeout: {:?}",
result.err()
);
let choreo = result.unwrap();
match &choreo.protocol {
Protocol::Send {
annotations,
continuation,
..
} => {
assert!(annotations.has_runtime_timeout());
let timeout = annotations.runtime_timeout().unwrap();
assert_eq!(timeout, std::time::Duration::from_secs(5));
match continuation.as_ref() {
Protocol::Send { annotations, .. } => {
assert!(!annotations.has_runtime_timeout());
}
_ => panic!("Expected Send for Response"),
}
}
_ => panic!("Expected Send for Request"),
}
}
#[test]
fn test_parse_multiline_runtime_timeout_annotation_with_closing_paren_on_own_line() {
let input = r#"
protocol TimedRequest =
roles Client, Server
Client {
runtime_timeout : 5s,
}
-> Server : Request
Server -> Client : Response
"#;
let result = parse_choreography_str(input);
assert!(
result.is_ok(),
"Failed to parse multiline sender-record runtime_timeout: {:?}",
result.err()
);
let choreo = result.unwrap();
match &choreo.protocol {
Protocol::Send { annotations, .. } => {
assert!(annotations.has_runtime_timeout());
assert_eq!(
annotations.runtime_timeout(),
Some(std::time::Duration::from_secs(5))
);
}
_ => panic!("Expected Send for Request"),
}
}
#[test]
fn test_parse_runtime_timeout_milliseconds() {
let input = r#"
protocol QuickCheck =
roles A, B
A { runtime_timeout : 100ms } -> B : Ping
"#;
let result = parse_choreography_str(input);
assert!(
result.is_ok(),
"Failed to parse sender-record runtime_timeout with ms: {:?}",
result.err()
);
let choreo = result.unwrap();
match &choreo.protocol {
Protocol::Send { annotations, .. } => {
assert!(annotations.has_runtime_timeout());
let timeout = annotations.runtime_timeout().unwrap();
assert_eq!(timeout, std::time::Duration::from_millis(100));
}
_ => panic!("Expected Send"),
}
}
#[test]
fn test_parse_parallel_annotation() {
let input = r#"
protocol Broadcast =
roles Coordinator, Worker
Coordinator { parallel : true } -> Worker : Task
"#;
let result = parse_choreography_str(input);
assert!(
result.is_ok(),
"Failed to parse sender-record parallel metadata: {:?}",
result.err()
);
let choreo = result.unwrap();
match &choreo.protocol {
Protocol::Send { annotations, .. } => {
assert!(annotations.has_parallel(), "Expected parallel annotation");
}
_ => panic!("Expected Send"),
}
}
#[test]
fn test_parse_choice_with_bar_prefixed_branches() {
let input = r#"
protocol Decision =
roles A, B
choice A at
| Accept =>
A -> B : Ok
| Reject =>
A -> B : No
"#;
let result = parse_choreography_str(input);
assert!(
result.is_ok(),
"Failed to parse choice with bar-prefixed branches: {:?}",
result.err()
);
let choreo = result.unwrap();
match &choreo.protocol {
Protocol::Choice { branches, .. } => {
assert_eq!(branches.len(), 2);
assert_eq!(branches.first().label.to_string(), "Accept");
assert_eq!(branches.as_slice()[1].label.to_string(), "Reject");
}
_ => panic!("Expected Choice"),
}
}
#[test]
fn test_parse_par_with_single_line_bar_branches() {
let input = r#"
protocol ParallelBars =
roles A, B, C, D
par
| A -> B : Left
| C -> D : Right
"#;
let result = parse_choreography_str(input);
assert!(
result.is_ok(),
"Failed to parse `par` with single-line branches: {:?}",
result.err()
);
let choreo = result.unwrap();
match &choreo.protocol {
Protocol::Parallel { protocols } => {
assert_eq!(protocols.len(), 2);
}
_ => panic!("Expected Parallel"),
}
}
#[test]
fn test_parse_par_with_block_branch() {
let input = r#"
protocol ParallelBarsBlock =
roles A, B, C, D
par
|
A -> B : Left
B -> A : Ack
| C -> D : Right
"#;
let result = parse_choreography_str(input);
assert!(
result.is_ok(),
"Failed to parse `par` with block branch: {:?}",
result.err()
);
let choreo = result.unwrap();
match &choreo.protocol {
Protocol::Parallel { protocols } => {
assert_eq!(protocols.len(), 2);
match &protocols.first() {
Protocol::Send { continuation, .. } => {
assert!(matches!(continuation.as_ref(), Protocol::Send { .. }));
}
_ => panic!("Expected first branch to be a send sequence"),
}
}
_ => panic!("Expected Parallel"),
}
}
#[test]
fn test_reject_par_without_bar_branches() {
let input = r#"
protocol ParallelMissingBars =
roles A, B, C, D
par
A -> B : Left
C -> D : Right
"#;
let result = parse_choreography_str(input);
assert!(
result.is_err(),
"`par` branches must be introduced with `|`"
);
}
#[test]
fn test_parse_sender_role_annotation_block() {
let input = r#"
protocol RoleAnnotatedSend =
roles Role, OtherRole
Role {
annotation1 : "value",
annotation2 : 100,
annotation3 : another,
} -> OtherRole : Message of crate.Type
"#;
let result = parse_choreography_str(input);
assert!(
result.is_ok(),
"Failed to parse sender role annotation block: {:?}",
result.err()
);
let choreo = result.unwrap();
match &choreo.protocol {
Protocol::Send {
from,
to,
message,
from_annotations,
..
} => {
assert_eq!(from.name().to_string(), "Role");
assert_eq!(to.name().to_string(), "OtherRole");
assert_eq!(message.name.to_string(), "Message");
assert_eq!(
message.payload.as_ref().map(ToString::to_string),
Some("crate :: Type".to_string())
);
assert_eq!(from_annotations.custom("annotation1"), Some("value"));
assert_eq!(from_annotations.custom("annotation2"), Some("100"));
assert_eq!(from_annotations.custom("annotation3"), Some("another"));
}
_ => panic!("Expected Send"),
}
}
#[test]
fn test_parse_sender_record_with_aligned_arrow_layout() {
let input = r#"
protocol StyledSend =
roles Buyer, Seller
Buyer { priority : high }
-> Seller : Request of shop.Order
"#;
let result = parse_choreography_str(input);
assert!(
result.is_ok(),
"Failed to parse aligned-arrow sender record syntax: {:?}",
result.err()
);
let choreo = result.unwrap();
match &choreo.protocol {
Protocol::Send {
from_annotations,
message,
..
} => {
assert_eq!(from_annotations.custom("priority"), Some("high"));
assert_eq!(
message.payload.as_ref().map(ToString::to_string),
Some("shop :: Order".to_string())
);
}
_ => panic!("Expected Send"),
}
}
#[test]
fn test_parse_sender_role_annotation_block_with_indexed_role() {
let input = r#"
protocol RoleAnnotatedIndexedSend =
roles Worker[N], Coordinator
Worker[0] {
shard : 0,
} -> Coordinator : Result
"#;
let result = parse_choreography_str(input);
assert!(
result.is_ok(),
"Failed to parse sender annotation block on indexed role: {:?}",
result.err()
);
let choreo = result.unwrap();
match &choreo.protocol {
Protocol::Send {
from,
from_annotations,
..
} => {
assert_eq!(from.name().to_string(), "Worker");
assert_eq!(
from.index().as_ref().map(ToString::to_string),
Some("0".to_string())
);
assert_eq!(from_annotations.custom("shard"), Some("0"));
}
_ => panic!("Expected Send"),
}
}
#[test]
fn test_parse_sender_role_annotation_block_on_broadcast() {
let input = r#"
protocol RoleAnnotatedBroadcast =
roles Coordinator, Worker
Coordinator {
batch_size : 100,
} ->* : Task
"#;
let result = parse_choreography_str(input);
assert!(
result.is_ok(),
"Failed to parse sender annotation block on broadcast: {:?}",
result.err()
);
let choreo = result.unwrap();
match &choreo.protocol {
Protocol::Broadcast {
from,
from_annotations,
..
} => {
assert_eq!(from.name().to_string(), "Coordinator");
assert_eq!(from_annotations.custom("batch_size"), Some("100"));
}
_ => panic!("Expected Broadcast"),
}
}
#[test]
fn test_parse_sender_role_annotation_ident_list_value() {
let input = r#"
protocol RoleAnnotatedSend =
roles Alice, Bob
Alice {
leak : (External, Neighbor),
} -> Bob : Message
"#;
let result = parse_choreography_str(input);
assert!(
result.is_ok(),
"Failed to parse sender annotation block with ident-list value: {:?}",
result.err()
);
let choreo = result.unwrap();
match &choreo.protocol {
Protocol::Send {
from_annotations, ..
} => {
assert_eq!(
from_annotations.custom("leak"),
Some("(External, Neighbor)")
);
}
_ => panic!("Expected Send"),
}
}
#[test]
fn test_parse_sender_role_annotation_integer_list_value() {
let input = r#"
protocol RoleAnnotatedSend =
roles Alice, Bob
Alice {
leakage_budget : [1, 0, 0],
} -> Bob : Message
"#;
let result = parse_choreography_str(input);
assert!(
result.is_ok(),
"Failed to parse sender annotation block with integer-list value: {:?}",
result.err()
);
let choreo = result.unwrap();
match &choreo.protocol {
Protocol::Send {
from_annotations, ..
} => {
assert_eq!(from_annotations.custom("leakage_budget"), Some("[1, 0, 0]"));
}
_ => panic!("Expected Send"),
}
}
#[test]
fn test_reject_sender_metadata_in_square_brackets() {
let input = r#"
protocol InvalidRoleMetadata =
roles Role, OtherRole
Role[annotation1 : "value"] -> OtherRole : Message
"#;
let result = parse_choreography_str(input);
assert!(
result.is_err(),
"square brackets must stay reserved for role indexing"
);
}
#[test]
fn test_parse_ordered_annotation() {
let input = r#"
protocol OrderedCollect =
roles Coordinator, Worker
Worker { ordered : true } -> Coordinator : Result
"#;
let result = parse_choreography_str(input);
assert!(
result.is_ok(),
"Failed to parse sender-record ordered metadata: {:?}",
result.err()
);
let choreo = result.unwrap();
match &choreo.protocol {
Protocol::Send { annotations, .. } => {
assert!(annotations.has_ordered(), "Expected ordered annotation");
}
_ => panic!("Expected Send"),
}
}
#[test]
fn test_parse_min_responses_annotation() {
let input = r#"
protocol ThresholdSign =
roles Coordinator, Signer
Signer { min_responses : 3 } -> Coordinator : Signature
"#;
let result = parse_choreography_str(input);
assert!(
result.is_ok(),
"Failed to parse sender-record min_responses: {:?}",
result.err()
);
let choreo = result.unwrap();
match &choreo.protocol {
Protocol::Send { annotations, .. } => {
assert!(
annotations.has_min_responses(),
"Expected min_responses annotation"
);
assert_eq!(annotations.min_responses(), Some(3));
}
_ => panic!("Expected Send"),
}
}
#[test]
fn test_parse_multiline_min_responses_annotation_with_closing_paren_on_own_line() {
let input = r#"
protocol ThresholdSign =
roles Coordinator, Signer
Signer {
min_responses : 3,
}
-> Coordinator : Signature
"#;
let result = parse_choreography_str(input);
assert!(
result.is_ok(),
"Failed to parse multiline sender-record min_responses: {:?}",
result.err()
);
let choreo = result.unwrap();
match &choreo.protocol {
Protocol::Send { annotations, .. } => {
assert!(
annotations.has_min_responses(),
"Expected multiline min_responses annotation"
);
assert_eq!(annotations.min_responses(), Some(3));
}
_ => panic!("Expected Send"),
}
}
#[test]
fn test_parse_combined_annotations() {
let input = r#"
protocol ParallelThreshold =
roles Coordinator, Worker
Worker {
parallel : true,
min_responses : 2,
} -> Coordinator : Vote
"#;
let result = parse_choreography_str(input);
assert!(
result.is_ok(),
"Failed to parse combined sender-record metadata: {:?}",
result.err()
);
let choreo = result.unwrap();
match &choreo.protocol {
Protocol::Send { annotations, .. } => {
assert!(annotations.has_parallel(), "Expected parallel annotation");
assert!(
annotations.has_min_responses(),
"Expected min_responses annotation"
);
assert_eq!(annotations.min_responses(), Some(2));
}
_ => panic!("Expected Send"),
}
}
#[test]
fn test_parse_proof_bundles_and_protocol_requires_metadata() {
let input = r#"
proof_bundle Base requires [guard_tokens, delegation]
proof_bundle Extra requires [knowledge_flow]
protocol WithBundles requires Base, Extra =
roles A, B
A -> B : Ping
"#;
let choreo = parse_choreography_str(input).expect("parse should succeed");
let bundles = choreo.theorem_packs();
assert_eq!(bundles.len(), 2);
assert_eq!(bundles[0].name, "Base");
assert_eq!(
bundles[0].capabilities,
vec!["guard_tokens".to_string(), "delegation".to_string()]
);
assert_eq!(bundles[1].name, "Extra");
assert_eq!(bundles[1].capabilities, vec!["knowledge_flow".to_string()]);
assert_eq!(
choreo.required_theorem_packs(),
vec!["Base".to_string(), "Extra".to_string()]
);
}
#[test]
fn test_protocol_machine_core_statements_are_rejected() {
let input = r#"
protocol VmOps =
roles A, B
acquire guard as token
transfer token to B with bundle Base
check k for B into out
A -> B : Ping
"#;
let err =
parse_choreography_str(input).expect_err("protocol-machine statements should fail");
assert!(err
.to_string()
.contains("legacy DSL construct `protocol-machine core statement` was removed"));
}
#[test]
fn test_validate_missing_required_bundle_fails() {
let input = r#"
protocol MissingBundle requires Core =
roles A, B
A -> B : Ping
"#;
let choreo = parse_choreography_str(input).expect("parse should succeed");
let err = choreo.validate().expect_err("validation should fail");
assert!(matches!(
err,
ValidationError::MissingProofBundle(ref name) if name == "Core"
));
}
#[test]
fn test_validate_missing_execution_profile_fails() {
let input = r#"
protocol NeedReplay under Replay =
roles A, B
A -> B : Ping
"#;
let choreo = parse_choreography_str(input).expect("parse should succeed");
let err = choreo.validate().expect_err("validation should fail");
assert!(err
.to_string()
.contains("undeclared execution profile `Replay`"));
}
#[test]
fn test_validate_duplicate_bundle_fails() {
let input = r#"
proof_bundle Core requires [delegation]
proof_bundle Core requires [guard_tokens]
protocol DuplicateBundle requires Core =
roles A, B
A -> B : Ping
"#;
let choreo = parse_choreography_str(input).expect("parse should succeed");
let err = choreo.validate().expect_err("validation should fail");
assert!(matches!(
err,
ValidationError::DuplicateProofBundle(ref name) if name == "Core"
));
}
#[test]
fn test_parse_guard_predicate_rejects_non_boolean_expression() {
let input = r#"
protocol GuardTypeCheck =
roles A, B
choice A at
| ok when (count + 1) =>
A -> B : Ack
| no =>
A -> B : Nack
"#;
let err = parse_choreography_str(input).expect_err("guard should fail");
assert!(matches!(err, ParseError::Syntax { .. }));
assert!(err.to_string().contains("boolean-like"));
}
#[test]
fn test_parse_loop_while_rejects_non_boolean_expression() {
let input = r#"
protocol LoopTypeCheck =
roles A, B
loop while "count + 1"
A -> B : Tick
"#;
let err = parse_choreography_str(input).expect_err("loop condition should fail");
assert!(matches!(err, ParseError::InvalidCondition { .. }));
assert!(err.to_string().contains("boolean-like"));
}
#[test]
fn test_projection_preserves_continuation_after_authority_binding() {
let input = r#"
effect Runtime
authoritative ready : Session -> Session
{
class : authoritative
progress : may_block
region : fragment
agreement_use : required
reentrancy : reject_same_fragment
}
protocol ExtensionProjection uses Runtime =
roles A, B
authoritative let witness = check Runtime.ready(session)
A -> B : Ping
"#;
let choreo = parse_choreography_str(input).expect("parse should succeed");
let role_a = choreo
.roles
.iter()
.find(|r| r.name() == "A")
.expect("role A should exist");
let projected =
crate::compiler::projection::project(&choreo, role_a).expect("projection must work");
match projected {
LocalType::Send { to, .. } => assert_eq!(to.name(), "B"),
other => panic!("expected send continuation projection, got {other:?}"),
}
}
#[test]
fn test_parse_enriched_proof_bundle_metadata() {
let input = r#"
proof_bundle Base version "1.0.0" issuer "did:example:issuer" constraint "fresh_nonce" constraint "sig_valid" requires [delegation, guard_tokens]
protocol BundleMeta requires Base =
roles A, B
A -> B : Ping
"#;
let choreo = parse_choreography_str(input).expect("parse should succeed");
let bundles = choreo.theorem_packs();
assert_eq!(bundles.len(), 1);
let bundle = &bundles[0];
assert_eq!(bundle.name, "Base");
assert_eq!(bundle.version.as_deref(), Some("1.0.0"));
assert_eq!(bundle.issuer.as_deref(), Some("did:example:issuer"));
assert_eq!(
bundle.constraints,
vec!["fresh_nonce".to_string(), "sig_valid".to_string()]
);
assert_eq!(
bundle.capabilities,
vec!["delegation".to_string(), "guard_tokens".to_string()]
);
}
#[test]
fn test_parse_execution_profiles_and_protocol_profiles() {
let input = r#"
profile Replay fairness eventual admissibility replay escalation_window bounded
protocol Inferred under Replay =
roles A, B
A -> B : Ping
"#;
let choreo = parse_choreography_str(input).expect("parse should succeed");
assert_eq!(choreo.execution_profile_declarations().len(), 1);
assert_eq!(choreo.execution_profile_declarations()[0].name, "Replay");
assert_eq!(
choreo.protocol_execution_profiles(),
vec!["Replay".to_string()]
);
assert!(choreo.validate().is_ok());
}
#[test]
fn test_parse_agreement_profiles_and_operation_attachment() {
let input = r#"
agreement_profile SoftSafe
visibility pending
rule aura_soft_safe
usable_at soft_safe
finalized_at finalized
evidence commit_fact
profile Replay fairness eventual admissibility replay escalation_window bounded
operation syncLedger(entryId : Int) at Coordinator progress LedgerProgress requires Replay within bounded on timeout => escalate on stall => diagnose agreement SoftSafe prestate ContactContext compose first_success =
publish SyncQueued(entryId)
protocol CommitLifecycle under Replay =
roles Coordinator, Worker
begin syncLedger(42) progress LedgerProgress requires Replay within bounded on timeout => escalate on stall => diagnose
Coordinator -> Worker : Prepare
"#;
let choreo = parse_choreography_str(input).expect("parse should succeed");
assert_eq!(choreo.agreement_profile_declarations().len(), 1);
let agreement = &choreo.agreement_profile_declarations()[0];
assert_eq!(agreement.name, "SoftSafe");
assert_eq!(agreement.visibility, "pending");
assert_eq!(agreement.rule, "aura_soft_safe");
assert_eq!(agreement.usable_at, "soft_safe");
assert_eq!(agreement.finalized_at, "finalized");
assert_eq!(agreement.evidence, "commit_fact");
let attachment = choreo.operation_declarations()[0]
.agreement
.clone()
.expect("operation should carry agreement metadata");
assert_eq!(attachment.profile_name, "SoftSafe");
assert_eq!(attachment.prestate.as_deref(), Some("ContactContext"));
assert!(choreo.validate().is_ok());
}
#[test]
fn test_linear_assets_reject_double_consume() {
let input = r#"
protocol LinearDoubleConsume =
roles A, B
let token = transfer Session from A to B
A -> B : Use(token)
A -> B : UseAgain(token)
"#;
let err = parse_choreography_str(input).expect_err("parse should fail");
assert!(err.to_string().contains("consumed more than once"));
}
#[test]
fn test_linear_assets_reject_branch_divergence() {
let input = r#"
protocol LinearBranchDivergence =
roles A, B
let token = transfer Session from A to B
choice A at
| consume =>
A -> B : Use(token)
| keep =>
A -> B : Skip
"#;
let err = parse_choreography_str(input).expect_err("parse should fail");
assert!(err.to_string().contains("diverge"));
}
#[test]
fn test_removed_first_class_combinators_are_rejected() {
let input = r#"
protocol Combinators =
roles A, B
handshake A <-> B : Hello
quorum_collect A -> B min 2 : Vote
A -> B : Done
retry 2 {
A -> B : Ping
}
"#;
let err = parse_choreography_str(input)
.expect_err("removed first-class combinators should fail closed");
assert!(err
.to_string()
.contains("legacy DSL construct `handshake` was removed"));
}
#[test]
fn test_parse_role_sets_and_topologies() {
let input = r#"
role_set Signers = Alice, Bob, Carol
role_set Quorum = subset(Signers, 0..2)
cluster LocalCluster = Signers, Quorum
ring RingNet = Alice, Bob, Carol
mesh FullMesh = Alice, Bob, Carol
protocol TopologyAware =
roles Alice, Bob
Alice -> Bob : Ping
"#;
let choreo = parse_choreography_str(input).expect("parse should succeed");
let role_sets = choreo.role_sets();
assert_eq!(role_sets.len(), 2);
assert_eq!(role_sets[0].name, "Signers");
assert_eq!(
role_sets[0].members,
vec!["Alice".to_string(), "Bob".to_string(), "Carol".to_string()]
);
assert_eq!(role_sets[1].subset_of.as_deref(), Some("Signers"));
assert_eq!(role_sets[1].subset_start, Some(0));
assert_eq!(role_sets[1].subset_end, Some(2));
let topologies = choreo.topologies();
assert_eq!(topologies.len(), 3);
assert_eq!(topologies[0].kind, "cluster");
assert_eq!(topologies[1].kind, "ring");
assert_eq!(topologies[2].kind, "mesh");
}
#[test]
fn test_explain_lowering_report_for_proof_backed_surface() {
let input = r#"
proof_bundle Spec requires [delegation]
protocol ExplainMe =
roles A, B
A -> B : Ping
"#;
let report = explain_lowering(input).expect("report generation should succeed");
assert!(report.contains("Proof bundles: Spec"));
assert!(report.contains("Lowering:"));
let choreo = parse_choreography_str(input).expect("parse should succeed");
let lints = collect_dsl_lints(&choreo, LintLevel::Warn);
assert!(lints.is_empty());
let lsp = render_lsp_lint_diagnostics(&choreo, LintLevel::Warn);
assert_eq!(lsp, "[]");
}
#[test]
fn test_typed_predicate_ir_rejects_if_expression() {
let input = r#"
protocol PredicateTyping =
roles A, B
choice A at
| ok when (if ready { true } else { false }) =>
A -> B : Accept
| no =>
A -> B : Reject
"#;
let err = parse_choreography_str(input).expect_err("parse should fail");
assert!(matches!(err, ParseError::Syntax { .. }));
assert!(err.to_string().contains("boolean-like"));
}
#[test]
fn test_parse_choreography_rejects_proc_macro_token_input() {
let input: TokenStream = quote::quote! {
protocol PingPong =
roles Alice, Bob
Alice -> Bob : Ping
Bob -> Alice : Pong
};
let err = parse_choreography(input).expect_err("proc-macro2 token parsing should fail");
assert!(err
.to_string()
.contains("proc-macro2 token parsing for the tell! DSL was removed"));
}
#[test]
fn test_parse_choreography_rejects_string_literal_macro_input() {
let input: TokenStream = quote::quote! {
r#"
protocol ReplicatedWrite =
roles Client, Leader, Replica0, Replica1
Client -> Leader : Put of kv.Write
"#
};
let err = parse_choreography(input).expect_err("string literal macro input should fail");
assert!(err
.to_string()
.contains("string-literal tell! input was removed"));
}
#[test]
fn test_parse_legacy_structural_braces_are_rejected() {
let input = r#"
protocol Branchy = {
roles A, B, C, D;
par {
| {
choice A at {
| Accept => {
A -> B : Ok;
}
| Reject => {
A -> B : No;
}
}
}
| B -> D : Right;
}
}
"#;
let err = parse_choreography_str(input).expect_err("legacy braces should fail");
assert!(err
.to_string()
.contains("legacy brace-based protocol blocks are removed"));
}
#[test]
fn test_parse_authority_surface_with_effects_types_and_uses() {
let input = r#"
type CommitError =
| NotReady
| TimedOut
type alias ReadyWitness =
{
epoch : Int
issuedBy : Role
}
effect Runtime
authoritative ready : Session -> Result CommitError ReadyWitness
{
class : authoritative
progress : may_block
region : fragment
agreement_use : required
reentrancy : reject_same_fragment
}
command transfer : TransferRequest -> Result TransferError TransferReceipt
{
class : best_effort
progress : immediate
region : session
agreement_use : none
reentrancy : allow
}
effect Audit
observe record : AuditEvent -> Unit
{
class : observational
progress : immediate
region : global
agreement_use : forbidden
reentrancy : allow
}
protocol CommitFlow uses Runtime, Audit =
roles Coordinator, Worker, Client
authoritative let readiness = check Runtime.ready(session)
case readiness of
| Ok(witness) =>
Coordinator -> Worker : Commit(witness)
| Err(reason) =>
Coordinator -> Client : Retry(reason)
timeout 5s Coordinator at
Worker -> Coordinator : Ready
on timeout =>
Coordinator -> Worker : Cancel
on cancel =>
Coordinator -> Client : Cancelled
choice Coordinator at
| Commit when check Runtime.ready(session) yields witness =>
Coordinator -> Worker : CommitAgain(witness)
| Abort =>
Coordinator -> Worker : Abort
"#;
let choreography = parse_choreography_str(input).expect("authority surface should parse");
assert_eq!(choreography.type_declarations().len(), 2);
assert_eq!(choreography.effect_interface_declarations().len(), 2);
assert_eq!(
choreography.protocol_uses(),
vec!["Runtime".to_string(), "Audit".to_string()]
);
let runtime_metadata = choreography.effect_contract_declarations();
assert!(
runtime_metadata.iter().any(|op| {
op.interface_name == "Runtime"
&& op.operation_name == "ready"
&& op.authority_class == crate::ast::EffectAuthorityClass::Authoritative
&& op.semantic_class == "authoritative"
&& op.progress == "may_block"
&& op.region == "fragment"
&& op.agreement_use == "required"
}),
"runtime effect metadata should carry effect authority class"
);
choreography
.validate()
.expect("declared effect uses should validate");
}
#[test]
fn test_parse_let_in_and_maybe_surface() {
let input = r#"
type alias InviteHandle =
{
id : Int
}
effect Runtime
lookupInvite : Session -> Maybe InviteHandle
{
class : best_effort
progress : immediate
region : session
agreement_use : none
reentrancy : allow
}
protocol InviteFlow uses Runtime =
roles Coordinator, Worker
let invite = check Runtime.lookupInvite(session) in
case invite of
| Just(handle) =>
Coordinator -> Worker : UseInvite(handle)
| Nothing =>
Coordinator -> Worker : MissingInvite
"#;
let choreography =
parse_choreography_str(input).expect("let-in Maybe surface should parse");
choreography
.validate()
.expect("effect invocation should validate");
}
#[test]
fn test_reject_non_exhaustive_result_case() {
let input = r#"
effect Runtime
ready : Session -> Result CommitError ReadyWitness
{
class : authoritative
progress : may_block
region : fragment
agreement_use : required
reentrancy : reject_same_fragment
}
protocol CommitFlow uses Runtime =
roles Coordinator, Worker
authoritative let readiness = check Runtime.ready(session)
case readiness of
| Ok(witness) =>
Coordinator -> Worker : Commit(witness)
"#;
let err =
parse_choreography_str(input).expect_err("non-exhaustive Result case should fail");
assert!(!err.to_string().is_empty());
}
#[test]
fn test_reject_duplicate_linear_binding_use() {
let input = r#"
protocol TransferFlow =
roles Coordinator, Worker, Client
let receipt = transfer Session from Coordinator to Worker
Coordinator -> Worker : TransferAccepted(receipt)
Coordinator -> Client : ReceiptAudit(receipt)
"#;
let err = parse_choreography_str(input).expect_err("duplicate linear use should fail");
assert!(err.to_string().contains("consumed more than once"));
}
#[test]
fn test_reject_dropped_linear_binding_use() {
let input = r#"
protocol TransferFlow =
roles Coordinator, Worker
let receipt = transfer Session from Coordinator to Worker
Coordinator -> Worker : TransferAccepted
"#;
let err = parse_choreography_str(input).expect_err("dropped linear binding should fail");
assert!(err.to_string().contains("never consumed"));
}
#[test]
fn test_reject_undeclared_protocol_use() {
let input = r#"
protocol CommitFlow uses Runtime =
roles Coordinator, Worker
Coordinator -> Worker : Ping
"#;
let choreography = parse_choreography_str(input).expect("parse should succeed");
let err = choreography
.validate()
.expect_err("undeclared effect interface should fail validation");
assert!(err.to_string().contains("undeclared effect interface"));
}
#[test]
fn test_reject_undeclared_effect_operation_invocation() {
let input = r#"
effect Runtime
ready : Session -> Result CommitError ReadyWitness
{
class : authoritative
progress : may_block
region : fragment
agreement_use : required
reentrancy : reject_same_fragment
}
protocol CommitFlow uses Runtime =
roles Coordinator, Worker
let readiness = check Runtime.lookup(session)
case readiness of
| Ok(witness) =>
Coordinator -> Worker : Commit(witness)
| Err(reason) =>
Coordinator -> Worker : Retry(reason)
"#;
let choreography = parse_choreography_str(input).expect("parse should succeed");
let err = choreography
.validate()
.expect_err("undeclared effect operation should fail validation");
assert!(err.to_string().contains("undeclared operation"));
}
#[test]
fn test_reject_duplicate_effect_declarations() {
let input = r#"
effect Runtime
ready : Session -> Result CommitError ReadyWitness
{
class : authoritative
progress : may_block
region : fragment
agreement_use : required
reentrancy : reject_same_fragment
}
effect Runtime
transfer : TransferRequest -> Result TransferError TransferReceipt
{
class : best_effort
progress : immediate
region : session
agreement_use : none
reentrancy : allow
}
protocol CommitFlow uses Runtime =
roles Coordinator, Worker
Coordinator -> Worker : Ping
"#;
let choreography = parse_choreography_str(input).expect("parse should succeed");
let err = choreography
.validate()
.expect_err("duplicate effect declarations should fail validation");
assert!(err
.to_string()
.contains("duplicate effect interface declaration"));
}
#[test]
fn test_reject_observational_effect_used_with_check() {
let input = r#"
effect Runtime
observe watchPresence : Session -> PresenceView
{
class : observational
progress : immediate
region : session
agreement_use : forbidden
reentrancy : allow
}
protocol WatchFlow uses Runtime =
roles Coordinator, Worker
let presence = check Runtime.watchPresence(session)
Coordinator -> Worker : Seen(presence)
"#;
let choreography = parse_choreography_str(input).expect("parse should succeed");
let err = choreography
.validate()
.expect_err("observational effect use should fail validation");
assert!(err.to_string().contains("observational"));
}
#[test]
fn test_reject_plain_binding_of_authoritative_check() {
let input = r#"
effect Runtime
authoritative ready : Session -> Result CommitError ReadyWitness
{
class : authoritative
progress : may_block
region : fragment
agreement_use : required
reentrancy : reject_same_fragment
}
protocol CommitFlow uses Runtime =
roles Coordinator, Worker
let readiness = check Runtime.ready(session)
Coordinator -> Worker : Continue(readiness)
"#;
let choreography = parse_choreography_str(input).expect("parse should succeed");
let err = choreography
.validate()
.expect_err("plain authoritative binding must fail validation");
assert!(err.to_string().contains("authoritative let"));
}
#[test]
fn test_reject_plain_binding_of_observe_expression() {
let input = r#"
effect Runtime
observe watchPresence : Session -> PresenceView
{
class : observational
progress : immediate
region : session
agreement_use : forbidden
reentrancy : allow
}
protocol WatchFlow uses Runtime =
roles Coordinator, Worker
let presence = observe Runtime.watchPresence(session)
Coordinator -> Worker : Seen(presence)
"#;
let choreography = parse_choreography_str(input).expect("parse should succeed");
let err = choreography
.validate()
.expect_err("plain observe binding must fail validation");
assert!(err.to_string().contains("observe let"));
}
#[test]
fn test_reject_unsupported_runtime_upgrade_surface() {
let input = r#"
protocol UpgradeFlow =
roles Coordinator, Worker
upgrade runtime to Epoch2
Coordinator -> Worker : Commit
"#;
let err = parse_choreography_str(input)
.expect_err("runtime upgrade surface must remain outside the DSL");
assert!(
err.to_string().contains("upgrade") || err.to_string().contains("unexpected"),
"unexpected parse error: {err}"
);
}
#[test]
fn test_parse_fragments_operations_and_guest_runtime_metadata() {
let input = r#"
fragment ChannelMembership(channel)
profile Replay fairness eventual admissibility replay escalation_window bounded
agreement_profile PendingPublication
visibility pending
rule no_agreement
usable_at provisional
finalized_at finalized
evidence publication
operation syncMembership(channel : ChannelId) at Worker within ChannelMembership(channel) progress MembershipProgress agreement PendingPublication prestate ChannelMembership compose threshold_success(2) =
publish SyncQueued(channel)
guest runtime MessagingGuest =
uses Runtime, Audit
entry CommitFlow
protocol CommitFlow uses Runtime, Audit under Replay =
roles Coordinator, Worker
Coordinator -> Worker : Ping
"#;
let choreography = parse_choreography_str(input).expect("surface metadata should parse");
assert_eq!(choreography.region_declarations().len(), 1);
assert_eq!(
choreography.region_declarations()[0].name,
"ChannelMembership"
);
assert_eq!(
choreography.region_declarations()[0].params,
vec!["channel"]
);
assert_eq!(choreography.operation_declarations().len(), 1);
let operation = &choreography.operation_declarations()[0];
assert_eq!(operation.name, "syncMembership");
assert_eq!(operation.owner_role, "Worker");
assert_eq!(
operation.within.as_deref(),
Some("ChannelMembership(channel)")
);
assert_eq!(
operation
.progress_contract
.as_ref()
.map(|progress| progress.contract_name.as_str()),
Some("MembershipProgress")
);
assert_eq!(
operation
.agreement
.as_ref()
.map(|agreement| agreement.profile_name.as_str()),
Some("PendingPublication")
);
assert_eq!(
operation
.child_effect_aggregation
.as_ref()
.map(|composition| composition.dsl_name()),
Some("threshold_success(2)".to_string())
);
assert_eq!(operation.params.len(), 1);
assert!(operation
.body_source
.contains("publish SyncQueued(channel)"));
assert_eq!(choreography.guest_runtime_declarations().len(), 1);
let guest_runtime = &choreography.guest_runtime_declarations()[0];
assert_eq!(guest_runtime.name, "MessagingGuest");
assert_eq!(guest_runtime.uses, vec!["Runtime", "Audit"]);
assert_eq!(guest_runtime.entry, "CommitFlow");
assert_eq!(choreography.execution_profile_declarations().len(), 1);
assert_eq!(
choreography.protocol_execution_profiles(),
vec!["Replay".to_string()]
);
}
#[test]
fn test_parse_commitment_lifecycle_and_structured_progress_metadata() {
let input = r#"
profile Replay fairness eventual admissibility replay escalation_window bounded
agreement_profile SoftSafe
visibility pending
rule aura_soft_safe
usable_at soft_safe
finalized_at finalized
evidence commit_fact
operation syncLedger(entryId : Int) at Coordinator progress LedgerProgress requires Replay within bounded on timeout => escalate on stall => diagnose agreement SoftSafe prestate LedgerState compose first_success =
publish SyncQueued(entryId)
protocol CommitLifecycle under Replay =
roles Coordinator, Worker
begin syncLedger(42) progress LedgerProgress requires Replay within bounded on timeout => escalate on stall => diagnose
Coordinator -> Worker : Prepare
await syncLedger
resolve syncLedger as Success
"#;
let choreography =
parse_choreography_str(input).expect("commitment lifecycle surface should parse");
choreography
.validate()
.expect("structured progress metadata should validate");
let operation = &choreography.operation_declarations()[0];
let progress = operation
.progress_contract
.as_ref()
.expect("operation should carry progress metadata");
assert_eq!(progress.contract_name, "LedgerProgress");
assert_eq!(progress.requires_profile.as_deref(), Some("Replay"));
assert_eq!(progress.within_window.as_deref(), Some("bounded"));
assert_eq!(progress.on_timeout.as_deref(), Some("escalate"));
assert_eq!(progress.on_stall.as_deref(), Some("diagnose"));
}
#[test]
fn test_legacy_implicit_progress_contract_is_rejected() {
let input = r#"
profile Replay fairness eventual admissibility replay escalation_window bounded
fragment ChannelMembership(channel)
agreement_profile PendingPublication
visibility pending
rule no_agreement
usable_at provisional
finalized_at finalized
evidence publication
operation syncMembership(channel : ChannelId) at Worker within ChannelMembership(channel) progress MembershipProgress agreement PendingPublication prestate ChannelMembership compose threshold_success(2) =
publish SyncQueued(channel)
protocol CommitFlow under Replay =
roles Coordinator, Worker
begin syncMembership(1) progress MembershipProgress
"#;
let choreography =
parse_choreography_str(input).expect("legacy form still parses before validation");
choreography
.validate()
.expect_err("legacy implicit progress metadata must be rejected");
}
#[test]
fn test_parse_authority_publication_materialization_and_handoff_fail_projection_closed() {
let input = r#"
protocol AcceptFlow =
roles Coordinator, Worker
authoritative let witness = check Runtime.ready(session)
observe let presence = observe Runtime.watchPresence(session)
publish witness as AcceptedPublication
materialize acceptedProof from AcceptedPublication
let receipt = transfer Session from Coordinator to Worker
handoff acceptInvite to Worker with receipt
dependent work SyncMembership(channel) required for acceptInvite
Coordinator -> Worker : Commit
"#;
let choreography = parse_choreography_str(input).expect("semantic surface should parse");
let err = project(&choreography, &choreography.roles[0])
.expect_err("new semantic forms should remain fail-closed in projection");
assert!(!err.to_string().is_empty());
}
#[test]
fn parse_choreography_file_accepts_tell_extension() {
let dir = tempdir().expect("tempdir");
let path = dir.path().join("protocol.tell");
std::fs::write(&path, "protocol Ping =\n roles A, B\n A -> B : Msg\n")
.expect("write tell fixture");
let parsed = parse_choreography_file(&path).expect("parse .tell source");
assert_eq!(parsed.name.to_string(), "Ping");
}
#[test]
fn parse_choreography_file_rejects_choreo_extension() {
let dir = tempdir().expect("tempdir");
let path = dir.path().join("protocol.choreo");
std::fs::write(&path, "protocol Ping =\n roles A, B\n A -> B : Msg\n")
.expect("write choreo fixture");
let err = parse_choreography_file(&path).expect_err("reject legacy extension");
let rendered = err.to_string();
assert!(
rendered.contains(".tell"),
"error should point to canonical .tell extension: {rendered}"
);
}
#[test]
fn reject_legacy_child_effect_aggregation_keywords() {
for keyword in ["race", "fallback", "quorum(2)"] {
let input = format!(
r#"
agreement_profile SoftSafe
visibility pending
rule aura_soft_safe
usable_at soft_safe
finalized_at finalized
evidence commit_fact
profile Replay fairness eventual admissibility replay escalation_window bounded
operation syncLedger(entryId : Int) at Coordinator progress LedgerProgress requires Replay within bounded on timeout => escalate on stall => diagnose agreement SoftSafe prestate ContactContext compose {keyword} =
publish SyncQueued(entryId)
protocol CommitLifecycle under Replay =
roles Coordinator, Worker
begin syncLedger(42) progress LedgerProgress requires Replay within bounded on timeout => escalate on stall => diagnose
Coordinator -> Worker : Prepare
"#
);
let err = parse_choreography_str(&input)
.expect_err("legacy child-effect aggregation keyword should fail");
let message = err.to_string();
assert!(
message.contains("all_success")
|| message.contains("first_success")
|| message.contains("threshold_success"),
"unexpected parser error for `{keyword}`: {message}"
);
}
}
}