use crate::ast::{
ActionDecl, BinaryOp, BindingDecl, Cardinality, CaseDecl, CompareOp, DecisionDecl, Declaration,
DeriveDecl, DomainConstraint, Effect, EntityDecl, EnumDecl, Expectation, Expr, ExprKind,
FieldDecl, FieldValue, FragmentDecl, FragmentRef, ImportDecl, InvariantAssertion,
InvariantDecl, InvariantQuantifier, Literal, Name, NumericLiteral, Parameter, Program,
RangeConstraint, RuleDecl, StateDecl, StateUpdate, TraceDecl, TransitionDecl, TypeRef, UnaryOp,
};
use crate::diagnostic::Diagnostic;
use crate::lexer::{Keyword, Token, TokenKind, lex};
use crate::source::{SourceFile, Span, Spanned};
use std::mem::discriminant;
#[derive(Clone, Debug)]
pub struct ParseOutput {
pub program: Option<Program>,
pub diagnostics: Vec<Diagnostic>,
}
#[must_use]
pub fn parse_numeric_literal(text: &str) -> Option<NumericLiteral> {
let unsigned = text.strip_prefix('-').unwrap_or(text);
if unsigned.is_empty() || unsigned.matches('.').count() > 1 {
return None;
}
let bytes = unsigned.as_bytes();
if bytes.iter().enumerate().any(|(index, value)| match value {
b'0'..=b'9' | b'.' => false,
b'_' => {
!index
.checked_sub(1)
.and_then(|previous| bytes.get(previous))
.is_some_and(u8::is_ascii_digit)
|| !bytes.get(index + 1).is_some_and(u8::is_ascii_digit)
}
_ => true,
}) {
return None;
}
let mut parts = unsigned.split('.');
if parts.next().is_none_or(str::is_empty) || parts.next().is_some_and(str::is_empty) {
return None;
}
let normalized = text.replace('_', "");
if normalized.contains('.') {
Some(NumericLiteral::Decimal(normalized))
} else {
normalized.parse().ok().map(NumericLiteral::Int)
}
}
fn parse_lexed_numeric_literal(text: &str) -> Option<NumericLiteral> {
parse_numeric_literal(text).or_else(|| parse_numeric_literal(&text.replace('_', "")))
}
fn effect_span(effect: &Effect) -> Span {
match effect {
Effect::Decide { span, .. } | Effect::Override { span, .. } | Effect::Invalid { span } => {
*span
}
}
}
const fn is_comparison(operator: BinaryOp) -> bool {
matches!(
operator,
BinaryOp::Equal
| BinaryOp::NotEqual
| BinaryOp::Greater
| BinaryOp::GreaterEqual
| BinaryOp::Less
| BinaryOp::LessEqual
)
}
fn is_comparison_expression(expression: &Expr) -> bool {
matches!(
&expression.kind,
ExprKind::Binary { operator, .. } if is_comparison(*operator)
)
}
impl ParseOutput {
#[must_use]
pub fn has_errors(&self) -> bool {
!self.diagnostics.is_empty()
}
}
#[must_use]
pub fn parse(source: SourceFile) -> ParseOutput {
let lexed = lex(&source);
Parser::new(source, lexed.tokens, lexed.diagnostics).parse_program()
}
struct Parser {
source: SourceFile,
tokens: Vec<Token>,
cursor: usize,
diagnostics: Vec<Diagnostic>,
}
impl Parser {
fn new(source: SourceFile, tokens: Vec<Token>, diagnostics: Vec<Diagnostic>) -> Self {
Self {
source,
tokens,
cursor: 0,
diagnostics,
}
}
fn parse_program(mut self) -> ParseOutput {
self.skip_newlines();
let module = if self.eat_keyword(Keyword::Mod).is_some() {
let name = self
.expect_identifier("expected a module name after `mod`")
.unwrap_or_else(|| Spanned::new("main".to_owned(), self.current().span));
self.finish_line("module declaration");
name
} else {
Spanned::new("main".to_owned(), Span::new(0, 0))
};
self.skip_newlines();
let mut imports = Vec::new();
while self.at_keyword(Keyword::Use) {
if let Some(import) = self.parse_import() {
imports.push(import);
}
self.skip_newlines();
}
let mut declarations = Vec::new();
while !self.at_simple(&TokenKind::Eof) {
self.skip_newlines();
if self.at_simple(&TokenKind::Eof) {
break;
}
if self.eat_simple(&TokenKind::Dedent).is_some() {
self.error_at(
"P0003",
"unexpected dedent at the top level",
self.previous_span(),
"align this declaration with the left margin",
);
continue;
}
let before = self.cursor;
if let Some(declaration) = self.parse_declaration() {
declarations.push(declaration);
}
if self.cursor == before {
self.error_here(
"P0003",
"expected a declaration",
"start with a fragment ID or `enum`, `record`, `state`, `fn`, `dec`, `action`, `transition`, `rule`, `test`, `assert`, or `trace`",
);
self.synchronize_line();
}
}
self.diagnostics.sort_by_key(|diagnostic| {
(
diagnostic.primary.start,
diagnostic.primary.end,
diagnostic.code.clone(),
)
});
ParseOutput {
program: Some(Program {
source: self.source,
module,
imports,
imports_resolved: false,
declarations,
}),
diagnostics: self.diagnostics,
}
}
fn parse_import(&mut self) -> Option<ImportDecl> {
let start = self.bump().span;
let path = self.expect_string("expected a quoted path after `use`")?;
let span = start.merge(path.span);
self.finish_line("use declaration");
Some(ImportDecl { path, span })
}
fn parse_declaration(&mut self) -> Option<Declaration> {
if self.at_fragment_header() {
self.parse_fragment().map(Declaration::Fragment)
} else if self.at_keyword(Keyword::Enum) {
self.parse_enum().map(Declaration::Enum)
} else if self.at_keyword(Keyword::Record) {
self.parse_record().map(Declaration::Entity)
} else if self.at_keyword(Keyword::State) {
self.parse_state().map(Declaration::State)
} else if self.at_keyword(Keyword::Fn) {
self.parse_fn().map(Declaration::Derive)
} else if self.at_keyword(Keyword::Dec) {
self.parse_decision().map(Declaration::Decision)
} else if self.at_keyword(Keyword::Action) {
self.parse_action().map(Declaration::Action)
} else if self.at_keyword(Keyword::Transition) {
self.parse_transition().map(Declaration::Transition)
} else if self.at_keyword(Keyword::Rule) {
self.parse_rule(None).map(Declaration::Rule)
} else if self.at_keyword(Keyword::Test) {
self.parse_test().map(Declaration::Case)
} else if self.at_keyword(Keyword::Assert) {
self.parse_assertion().map(Declaration::Invariant)
} else if self.at_keyword(Keyword::Trace) {
self.parse_trace().map(Declaration::Trace)
} else {
None
}
}
fn parse_enum(&mut self) -> Option<EnumDecl> {
let start = self.bump().span;
let name = self.expect_identifier("expected an enum name")?;
self.begin_suite("enum header")?;
let mut variants = Vec::new();
while self.next_suite_item() {
if let Some(variant) = self.expect_identifier("expected an enum variant") {
variants.push(variant);
self.finish_line("enum variant");
} else {
self.synchronize_line();
}
}
let end = self.end_suite("enum")?;
if variants.is_empty() {
self.error_at(
"P0100",
"an enum must have at least one variant",
name.span,
"add an indented variant below the enum header",
);
}
Some(EnumDecl {
name,
variants,
span: start.merge(end),
})
}
fn parse_record(&mut self) -> Option<EntityDecl> {
let start = self.bump().span;
let name = self.expect_identifier("expected a record name")?;
self.begin_suite("record header")?;
let fields = self.parse_fields();
let end = self.end_suite("record")?;
Some(EntityDecl {
name,
fields,
span: start.merge(end),
})
}
fn parse_state(&mut self) -> Option<StateDecl> {
let start = self.bump().span;
let name = self.expect_identifier("expected a state name")?;
self.begin_suite("state header")?;
let fields = self.parse_fields();
let end = self.end_suite("state")?;
Some(StateDecl {
name,
fields,
span: start.merge(end),
})
}
fn parse_fields(&mut self) -> Vec<FieldDecl> {
let mut fields = Vec::new();
while self.next_suite_item() {
if let Some(field) = self.parse_field() {
fields.push(field);
} else {
self.synchronize_line();
}
}
fields
}
fn parse_field(&mut self) -> Option<FieldDecl> {
let name = self.expect_identifier("expected a field name")?;
let start = name.span;
self.expect_simple(TokenKind::Colon, "expected `:` after field name")?;
let ty = self.parse_type_ref()?;
let optional = self.eat_simple(&TokenKind::Question).is_some();
let mut range = None;
let mut domain = None;
if matches!(self.current().kind, TokenKind::Number(_)) || self.at_simple(&TokenKind::Minus)
{
let (range_start, start_span) = self.parse_numeric_literal_token()?;
self.expect_simple(TokenKind::Range, "expected `..` between range bounds")?;
let (range_end, end_span) = self.parse_numeric_literal_token()?;
range = Some(RangeConstraint {
start: range_start,
end: range_end,
span: start_span.merge(end_span),
});
} else if let Some(open) = self.eat_simple(&TokenKind::LeftBrace) {
let mut values = Vec::new();
self.skip_newlines();
while !self.at_simple(&TokenKind::RightBrace) && !self.at_simple(&TokenKind::Eof) {
values.push(self.parse_expression()?);
if self.eat_simple(&TokenKind::Comma).is_none() {
self.skip_newlines();
if !self.at_simple(&TokenKind::RightBrace) {
self.error_here(
"P0101",
"domain values must be separated by commas",
"add `,` after the preceding value",
);
}
}
self.skip_newlines();
}
let close = self
.expect_simple(TokenKind::RightBrace, "expected `}` to close field domain")?
.span;
domain = Some(DomainConstraint {
values,
span: open.span.merge(close),
});
}
let end = self.previous_span();
self.finish_line("field declaration");
Some(FieldDecl {
name,
ty: ty.value,
range,
domain,
optional,
span: start.merge(end),
})
}
fn parse_fn(&mut self) -> Option<DeriveDecl> {
self.parse_fn_with_basis(Vec::new())
}
fn parse_fn_with_basis(&mut self, basis: Vec<FragmentRef>) -> Option<DeriveDecl> {
let start = self.bump().span;
let name = self.expect_identifier("expected a function name")?;
let parameters = self.parse_parameters()?;
self.expect_simple(
TokenKind::Arrow,
"expected `->` before function return type",
)?;
let return_type = self.parse_type_ref()?;
self.begin_suite("function header")?;
let mut basis = basis;
while self.next_suite_item() && self.at_keyword(Keyword::Basis) {
self.bump();
let id =
self.expect_qualified_identifier("expected a qualified fragment ID after `basis`")?;
basis.push(FragmentRef { id });
self.finish_line("basis clause");
}
if !self.next_suite_item() {
self.error_here(
"P0110",
"a function needs an expression",
"add one indented expression",
);
return None;
}
let expression = self.parse_expression()?;
self.finish_line("function expression");
while self.next_suite_item() {
self.error_here(
"P0111",
"a function body contains more than one expression",
"combine the calculation into one expression",
);
self.synchronize_line();
}
let end = self.end_suite("function")?;
Some(DeriveDecl {
name,
parameters,
return_type,
expression,
basis,
span: start.merge(end),
})
}
fn parse_decision(&mut self) -> Option<DecisionDecl> {
let start = self.bump().span;
let name = self.expect_identifier("expected a decision name")?;
let parameters = self.parse_parameters()?;
self.expect_simple(
TokenKind::Arrow,
"expected `->` before decision return type",
)?;
let return_type = self.parse_type_ref()?;
let cardinality = if self.eat_simple(&TokenKind::Question).is_some() {
Cardinality::ZeroOrOne
} else if self.eat_simple(&TokenKind::Star).is_some() {
Cardinality::Many
} else {
Cardinality::ExactlyOne
};
let end = self.previous_span();
self.finish_line("decision declaration");
Some(DecisionDecl {
name,
parameters,
return_type,
cardinality,
span: start.merge(end),
})
}
fn parse_action(&mut self) -> Option<ActionDecl> {
let start = self.bump().span;
let name = self.expect_identifier("expected an action name")?;
let parameters = self.parse_parameters()?;
let end = self.previous_span();
self.finish_line("action declaration");
Some(ActionDecl {
name,
parameters,
span: start.merge(end),
})
}
fn parse_transition(&mut self) -> Option<TransitionDecl> {
let start = self.bump().span;
let name = self.expect_identifier("expected a transition name")?;
let parameters = self.parse_parameters()?;
self.expect_keyword(Keyword::On, "expected `on` after transition parameters")?;
let action = self.expect_qualified_identifier("expected an action name after `on`")?;
let action_arguments = self.parse_arguments()?;
self.begin_suite("transition header")?;
let mut condition = None;
let mut updates = Vec::new();
let mut end = action.span;
while self.next_suite_item() {
if let Some(keyword) = self.eat_keyword(Keyword::When) {
let expression = self.parse_expression()?;
end = expression.span;
if condition.replace(expression).is_some() {
self.error_at(
"P0250",
"a transition may have only one `when` clause",
keyword.span,
"combine conditions with `and` or `or`",
);
}
self.finish_line("transition condition");
} else if let Some(keyword) = self.eat_keyword(Keyword::Set) {
let receiver = self.expect_identifier("expected a state variable after `set`")?;
self.expect_simple(TokenKind::Dot, "expected `.` after state variable")?;
let field = self.expect_identifier("expected a state field after `.`")?;
self.expect_simple(TokenKind::Equal, "expected `=` in state update")?;
let value = self.parse_expression()?;
end = value.span;
updates.push(StateUpdate {
receiver,
field,
span: keyword.span.merge(value.span),
value,
});
self.finish_line("state update");
} else {
self.error_here(
"P0251",
"expected a transition clause",
"use `when` or `set`",
);
self.synchronize_line();
}
}
let suite_end = self.end_suite("transition")?;
let condition = condition.unwrap_or_else(|| {
self.error_at(
"P0252",
"transition is missing a `when` clause",
name.span,
"add an indented `when <condition>` line",
);
Expr::new(ExprKind::Literal(Literal::Bool(false)), name.span)
});
Some(TransitionDecl {
name,
parameters,
action,
action_arguments,
condition,
updates,
span: start.merge(end.merge(suite_end)),
})
}
fn parse_fragment(&mut self) -> Option<FragmentDecl> {
let id = self.expect_qualified_identifier("expected a fragment ID")?;
let start = id.span;
let locator = match self.current().clone() {
Token {
kind: TokenKind::Locator(value),
span,
} => {
self.bump();
Spanned::new(value, span)
}
_ => Spanned::new(id.value.clone(), id.span),
};
self.begin_suite("fragment header")?;
let raw = self.current().clone();
let text = if let TokenKind::RawText(value) = raw.kind {
self.bump();
Spanned::new(value, raw.span)
} else {
self.error_here(
"P0150",
"a fragment must start with raw source text",
"write the source text directly below the fragment header",
);
Spanned::new(String::new(), locator.span)
};
let mut refs = Vec::new();
let mut derives = Vec::new();
let mut rules = Vec::new();
while self.next_suite_item() {
if self.eat_keyword(Keyword::Ref).is_some() {
if let Some(id) =
self.expect_qualified_identifier("expected a qualified fragment ID after `ref`")
{
refs.push(FragmentRef { id });
}
self.finish_line("fragment reference");
} else if self.at_keyword(Keyword::Rule) {
if let Some(rule) = self.parse_rule(Some(&id)) {
rules.push(rule);
}
} else if self.at_keyword(Keyword::Fn) {
let basis = vec![FragmentRef { id: id.clone() }];
if let Some(derive) = self.parse_fn_with_basis(basis) {
derives.push(derive);
}
} else {
self.error_here(
"P0151",
"expected a fragment item",
"after a blank line, use `ref <fragment-id>`, `fn ...:`, or `rule ...:`",
);
self.synchronize_line();
}
}
let end = self.end_suite("fragment")?;
Some(FragmentDecl {
id,
locator,
text,
refs,
derives,
rules,
span: start.merge(end),
})
}
fn parse_rule(&mut self, enclosing: Option<&Spanned<String>>) -> Option<RuleDecl> {
let start = self.bump().span;
let name = self.expect_identifier("expected a rule name")?;
let parameters = self.parse_parameters()?;
self.begin_suite("rule header")?;
let mut basis = enclosing
.map(|id| vec![FragmentRef { id: id.clone() }])
.unwrap_or_default();
while self.next_suite_item() && self.at_keyword(Keyword::Basis) {
self.bump();
let id =
self.expect_qualified_identifier("expected a qualified fragment ID after `basis`")?;
basis.push(FragmentRef { id });
self.finish_line("basis clause");
}
if self.at_simple(&TokenKind::Dedent) || self.at_simple(&TokenKind::Eof) {
self.error_at(
"P0200",
"rule has no effect line",
name.span,
"add `<condition> => <decision>` or `=> <decision>`",
);
let end = self.end_suite("rule")?;
return Some(RuleDecl {
name,
parameters,
condition: Expr::new(ExprKind::Literal(Literal::Unknown), start),
effect: Effect::Invalid { span: start },
basis,
span: start.merge(end),
});
}
let condition = if self.eat_simple(&TokenKind::FatArrow).is_some() {
Expr::new(ExprKind::Literal(Literal::Bool(true)), self.previous_span())
} else {
let condition = self.parse_expression()?;
self.expect_simple(TokenKind::FatArrow, "expected `=>` after rule condition")?;
condition
};
let effect = self.parse_effect()?;
let effect_end = effect_span(&effect);
self.finish_line("rule effect");
while self.next_suite_item() {
self.error_here(
"P0201",
"a rule may have only one effect line",
"split additional outcomes into separate named rules",
);
self.synchronize_line();
}
let end = self.end_suite("rule")?;
Some(RuleDecl {
name,
parameters,
condition,
effect,
basis,
span: start.merge(effect_end.merge(end)),
})
}
fn parse_effect(&mut self) -> Option<Effect> {
let start = self.current().span;
if self.eat_keyword(Keyword::Override).is_some() {
let rule = self.expect_qualified_identifier("expected a rule name after `override`")?;
return Some(Effect::Override {
span: start.merge(rule.span),
rule,
});
}
let decision = self.expect_qualified_identifier("expected a decision name after `=>`")?;
let arguments = self.parse_arguments()?;
self.expect_simple(TokenKind::Equal, "expected `=` in decision effect")?;
let value = self.parse_expression()?;
Some(Effect::Decide {
decision,
arguments,
span: start.merge(value.span),
value,
})
}
fn parse_test(&mut self) -> Option<CaseDecl> {
let start = self.bump().span;
let name = self.expect_identifier("expected a test name")?;
self.begin_suite("test header")?;
let mut bindings = Vec::new();
let mut expectations = Vec::new();
let mut end = name.span;
while self.next_suite_item() {
if self.eat_keyword(Keyword::Let).is_some() {
if let Some(binding) = self.parse_explicit_binding() {
end = binding.span;
bindings.push(binding);
}
self.finish_line("test binding");
} else if let Some(keyword) = self.eat_keyword(Keyword::Expect) {
if let Some(expectation) = self.parse_expectation(keyword.span) {
end = expectation.span;
expectations.push(expectation);
}
self.finish_line("test expectation");
} else if self.token_is_identifier(self.cursor) {
if let Some(binding) = self.parse_implicit_binding() {
end = binding.span;
bindings.push(binding);
}
self.finish_line("test binding");
} else {
self.error_here(
"P0300",
"expected a test clause",
"write `Record { ... }`, `let name = Record { ... }`, or `expect ...`",
);
self.synchronize_line();
}
}
let suite_end = self.end_suite("test")?;
Some(CaseDecl {
name,
bindings,
expectations,
span: start.merge(end.merge(suite_end)),
})
}
fn parse_explicit_binding(&mut self) -> Option<BindingDecl> {
let name = self.expect_identifier("expected a binding name after `let`")?;
self.expect_simple(TokenKind::Equal, "expected `=` in test binding")?;
let entity = self.expect_qualified_identifier("expected a record name in test binding")?;
self.parse_binding_record(name, entity)
}
fn parse_implicit_binding(&mut self) -> Option<BindingDecl> {
let entity = self.expect_qualified_identifier("expected a record name in test binding")?;
let name = Spanned::new(
entity
.value
.rsplit("::")
.next()
.expect("a qualified record name has a final segment")
.to_owned(),
entity.span,
);
self.parse_binding_record(name, entity)
}
fn parse_binding_record(
&mut self,
name: Spanned<Name>,
entity: Spanned<Name>,
) -> Option<BindingDecl> {
let start = name.span;
self.expect_simple(TokenKind::LeftBrace, "expected `{` after record name")?;
self.skip_newlines();
let mut fields = Vec::new();
while !self.at_simple(&TokenKind::RightBrace) && !self.at_simple(&TokenKind::Eof) {
let field_name = self.expect_identifier("expected a field name")?;
self.expect_simple(TokenKind::Colon, "expected `:` after field name")?;
let value = self.parse_expression()?;
let span = field_name.span.merge(value.span);
fields.push(FieldValue {
name: field_name,
value,
span,
});
if self.eat_simple(&TokenKind::Comma).is_none() {
self.skip_newlines();
if !self.at_simple(&TokenKind::RightBrace) {
self.error_here(
"P0301",
"record literal fields must be separated by commas",
"add `,` after the preceding field",
);
}
}
self.skip_newlines();
}
let close = self
.expect_simple(
TokenKind::RightBrace,
"expected `}` to close record literal",
)?
.span;
Some(BindingDecl {
name,
entity,
fields,
span: start.merge(close),
})
}
fn parse_expectation(&mut self, start: Span) -> Option<Expectation> {
let decision = self.expect_qualified_identifier("expected a decision name")?;
let arguments = self.parse_arguments()?;
let operator = if self.eat_simple(&TokenKind::Equal).is_some() {
CompareOp::Equal
} else if self.eat_simple(&TokenKind::NotEqual).is_some() {
CompareOp::NotEqual
} else {
self.error_here(
"P0302",
"expected `=` or `!=` in expectation",
"compare the decision with an expected value",
);
return None;
};
let value = self.parse_expression()?;
Some(Expectation {
decision,
arguments,
operator,
span: start.merge(value.span),
value,
})
}
fn parse_assertion(&mut self) -> Option<InvariantDecl> {
let start = self.bump().span;
let quantifier = if self.eat_keyword(Keyword::Some).is_some() {
InvariantQuantifier::Some
} else {
InvariantQuantifier::All
};
let name = self.expect_identifier("expected an assertion name")?;
let variables = self.parse_parameters()?;
self.begin_suite("assertion header")?;
if !self.next_suite_item() {
self.error_at(
"P0400",
"assertion body is empty",
name.span,
"add a decision call or implication",
);
return None;
}
let assertion = if self.line_contains(&TokenKind::FatArrow) {
let condition = self.parse_expression()?;
let arrow = self.expect_simple(
TokenKind::FatArrow,
"expected `=>` after assertion condition",
)?;
let expectation = self.parse_expectation(arrow.span)?;
let span = condition.span.merge(expectation.span);
InvariantAssertion::Implication {
condition,
expectation,
span,
}
} else {
let decision = self.expect_qualified_identifier("expected a decision name")?;
let arguments = self.parse_arguments()?;
let cardinality = if self.eat_simple(&TokenKind::Question).is_some() {
Cardinality::ZeroOrOne
} else if self.eat_simple(&TokenKind::Star).is_some() {
Cardinality::Many
} else {
Cardinality::ExactlyOne
};
let span = decision.span.merge(self.previous_span());
InvariantAssertion::Cardinality {
cardinality,
decision,
arguments,
span,
}
};
self.finish_line("assertion");
while self.next_suite_item() {
self.error_here(
"P0401",
"an assertion may contain only one assertion line",
"combine conditions or split this into named assertions",
);
self.synchronize_line();
}
let end = self.end_suite("assertion")?;
Some(InvariantDecl {
name,
quantifier,
variables,
span: start.merge(end),
assertion,
})
}
fn parse_trace(&mut self) -> Option<TraceDecl> {
let start = self.bump().span;
let name = self.expect_identifier("expected a trace name")?;
let mut parameters = self.parse_parameters()?;
let variable = if parameters.len() == 1 {
parameters.remove(0)
} else {
self.error_at(
"P0500",
"a trace needs exactly one state parameter",
name.span,
"write one parameter such as `(state: OrderState)`",
);
parameters.into_iter().next().unwrap_or_else(|| Parameter {
ty: Spanned::new("Unknown".to_owned(), name.span),
name: Spanned::new("state".to_owned(), name.span),
span: name.span,
})
};
self.begin_suite("trace header")?;
let mut initial = None;
let mut always = Vec::new();
let mut terminal = None;
let mut no_dead_ends = false;
let mut within = None;
let mut end = variable.span;
while self.next_suite_item() {
if let Some(keyword) = self.eat_keyword(Keyword::Initially) {
let expression = self.parse_expression()?;
end = expression.span;
if initial.replace(expression).is_some() {
self.error_at(
"P0501",
"a trace may have only one `initially` clause",
keyword.span,
"remove the duplicate clause",
);
}
self.finish_line("trace initial condition");
} else if self.eat_keyword(Keyword::Always).is_some() {
let expression = self.parse_expression()?;
end = expression.span;
always.push(expression);
self.finish_line("trace condition");
} else if let Some(keyword) = self.eat_keyword(Keyword::Terminates) {
self.expect_keyword(Keyword::When, "expected `when` after `terminates`")?;
let expression = self.parse_expression()?;
end = expression.span;
if terminal.replace(expression).is_some() {
self.error_at(
"P0502",
"a trace may have only one termination condition",
keyword.span,
"remove the duplicate clause",
);
}
self.finish_line("trace termination condition");
} else if let Some(keyword) = self.eat_keyword(Keyword::No) {
self.expect_keyword(Keyword::Dead, "expected `dead` after `no`")?;
self.expect_keyword(Keyword::Ends, "expected `ends` after `no dead`")?;
if no_dead_ends {
self.error_at(
"P0503",
"duplicate `no dead ends` clause",
keyword.span,
"remove the duplicate clause",
);
}
no_dead_ends = true;
end = self.previous_span();
self.finish_line("trace dead-end clause");
} else if let Some(keyword) = self.eat_keyword(Keyword::Within) {
let (steps, steps_span) = self.parse_numeric_literal_token()?;
self.expect_keyword(Keyword::Steps, "expected `steps` after trace bound")?;
let NumericLiteral::Int(steps) = steps else {
self.error_at(
"P0504",
"trace bound must be an integer",
steps_span,
"use a non-negative integer",
);
self.finish_line("trace bound");
continue;
};
let Ok(steps) = usize::try_from(steps) else {
self.error_at(
"P0504",
"trace bound must be non-negative",
steps_span,
"use a non-negative integer",
);
self.finish_line("trace bound");
continue;
};
if within.replace(Spanned::new(steps, steps_span)).is_some() {
self.error_at(
"P0505",
"a trace may have only one `within` clause",
keyword.span,
"remove the duplicate clause",
);
}
end = self.previous_span();
self.finish_line("trace bound");
} else {
self.error_here(
"P0506",
"expected a trace clause",
"use `initially`, `always`, `terminates when`, `no dead ends`, or `within N steps`",
);
self.synchronize_line();
}
}
let suite_end = self.end_suite("trace")?;
let initial = initial.unwrap_or_else(|| {
self.error_at(
"P0507",
"trace is missing an `initially` clause",
name.span,
"add an initial condition",
);
Expr::new(ExprKind::Literal(Literal::Bool(false)), name.span)
});
let within = within.unwrap_or_else(|| {
self.error_at(
"P0508",
"trace is missing a `within N steps` clause",
name.span,
"add a finite trace bound",
);
Spanned::new(0, name.span)
});
Some(TraceDecl {
name,
variable,
initial,
always,
terminal,
no_dead_ends,
within,
span: start.merge(end.merge(suite_end)),
})
}
fn parse_parameters(&mut self) -> Option<Vec<Parameter>> {
self.expect_simple(TokenKind::LeftParen, "expected `(` before parameter list")?;
let mut parsed = Vec::new();
self.skip_newlines();
while !self.at_simple(&TokenKind::RightParen) && !self.at_simple(&TokenKind::Eof) {
let first = self.expect_identifier("expected a parameter name or type")?;
let (name, ty, type_only) = if self.at_simple(&TokenKind::DoubleColon) {
let ty = self.finish_qualified_identifier(first.value, first.span)?;
let name = Spanned::new(
ty.value
.rsplit("::")
.next()
.expect("a qualified type has a final segment")
.to_owned(),
ty.span,
);
(name, ty, true)
} else if self.token_is_identifier(self.cursor) {
let ty = self.expect_qualified_identifier("expected a parameter type")?;
(first, ty, false)
} else {
let ty = first;
let name = Spanned::new(
ty.value
.rsplit("::")
.next()
.expect("a type has a final segment")
.to_owned(),
ty.span,
);
(name, ty, true)
};
let span = name.span.merge(ty.span);
parsed.push((Parameter { ty, name, span }, type_only));
if self.eat_simple(&TokenKind::Comma).is_none() {
self.skip_newlines();
if !self.at_simple(&TokenKind::RightParen) {
self.error_here(
"P0550",
"parameters must be separated by commas",
"add `,` after the preceding parameter",
);
}
}
self.skip_newlines();
}
self.expect_simple(TokenKind::RightParen, "expected `)` after parameter list")?;
if parsed.len() > 1 {
for (parameter, type_only) in &parsed {
if *type_only {
self.error_at(
"P0552",
"type-only shorthand is valid only for one parameter",
parameter.span,
"give every parameter a name, for example `(left Type, right Type)`",
);
}
}
}
Some(parsed.into_iter().map(|(parameter, _)| parameter).collect())
}
fn parse_arguments(&mut self) -> Option<Vec<Expr>> {
self.expect_simple(TokenKind::LeftParen, "expected `(` before argument list")?;
let mut arguments = Vec::new();
self.skip_newlines();
while !self.at_simple(&TokenKind::RightParen) && !self.at_simple(&TokenKind::Eof) {
arguments.push(self.parse_expression()?);
if self.eat_simple(&TokenKind::Comma).is_none() {
self.skip_newlines();
if !self.at_simple(&TokenKind::RightParen) {
self.error_here(
"P0551",
"arguments must be separated by commas",
"add `,` after the preceding argument",
);
}
}
self.skip_newlines();
}
self.expect_simple(TokenKind::RightParen, "expected `)` after argument list")?;
Some(arguments)
}
fn parse_type_ref(&mut self) -> Option<Spanned<TypeRef>> {
let name = self.expect_qualified_identifier("expected a type name")?;
let value = match name.value.as_str() {
"Bool" => TypeRef::Bool,
"Int" => TypeRef::Int,
"Decimal" => TypeRef::Decimal,
"String" => TypeRef::String,
"Date" => TypeRef::Date,
"Duration" => TypeRef::Duration,
_ => TypeRef::Named(name.value),
};
Some(Spanned::new(value, name.span))
}
fn parse_expression(&mut self) -> Option<Expr> {
self.parse_precedence(0)
}
fn parse_precedence(&mut self, minimum_binding_power: u8) -> Option<Expr> {
let mut left = self.parse_prefix()?;
while let Some((operator, left_power, right_power)) = self.current_binary_operator() {
if left_power < minimum_binding_power {
break;
}
let operator_token = self.bump();
if is_comparison(operator) && is_comparison_expression(&left) {
self.diagnostics.push(
Diagnostic::error(
"P0604",
"comparison operators cannot be chained",
operator_token.span,
)
.with_help("join comparisons explicitly with `and`"),
);
}
let right = self.parse_precedence(right_power)?;
let span = left.span.merge(right.span);
left = Expr::new(
ExprKind::Binary {
left: Box::new(left),
operator,
right: Box::new(right),
},
span,
);
}
Some(left)
}
fn parse_prefix(&mut self) -> Option<Expr> {
if let Some(operator) = self.eat_keyword(Keyword::Not) {
let operand = self.parse_precedence(11)?;
let span = operator.span.merge(operand.span);
return Some(Expr::new(
ExprKind::Unary {
operator: UnaryOp::Not,
operand: Box::new(operand),
},
span,
));
}
if let Some(operator) = self.eat_simple(&TokenKind::Minus) {
let operand = self.parse_precedence(11)?;
let span = operator.span.merge(operand.span);
return Some(Expr::new(
ExprKind::Unary {
operator: UnaryOp::Negate,
operand: Box::new(operand),
},
span,
));
}
let mut expression = self.parse_primary()?;
while self.eat_simple(&TokenKind::Dot).is_some() {
let field = self.expect_identifier("expected a field name after `.`")?;
let span = expression.span.merge(field.span);
expression = Expr::new(
ExprKind::Field {
receiver: Box::new(expression),
field,
},
span,
);
}
Some(expression)
}
fn parse_primary(&mut self) -> Option<Expr> {
let token = self.current().clone();
match token.kind {
TokenKind::Number(value) => {
self.bump();
let literal = parse_lexed_numeric_literal(&value).unwrap_or_else(|| {
self.diagnostics.push(Diagnostic::error(
"P0601",
"numeric literal is outside the supported range",
token.span,
));
NumericLiteral::Int(0)
});
Some(Expr::new(
ExprKind::Literal(Literal::Number(literal)),
token.span,
))
}
TokenKind::String(value) => {
self.bump();
Some(Expr::new(
ExprKind::Literal(Literal::String(value)),
token.span,
))
}
TokenKind::Keyword(Keyword::True) => {
self.bump();
Some(Expr::new(
ExprKind::Literal(Literal::Bool(true)),
token.span,
))
}
TokenKind::Keyword(Keyword::False) => {
self.bump();
Some(Expr::new(
ExprKind::Literal(Literal::Bool(false)),
token.span,
))
}
TokenKind::Keyword(Keyword::Unknown) => {
self.bump();
Some(Expr::new(ExprKind::Literal(Literal::Unknown), token.span))
}
TokenKind::Identifier(name) => self.parse_identifier_expression(name, token.span),
TokenKind::Keyword(keyword) if keyword.is_contextual() => {
self.parse_identifier_expression(keyword.as_str().to_owned(), token.span)
}
TokenKind::LeftParen => {
self.bump();
self.skip_newlines();
let mut inner = self.parse_expression()?;
self.skip_newlines();
let close = self
.expect_simple(TokenKind::RightParen, "expected `)` after expression")?
.span;
inner.span = token.span.merge(close);
Some(inner)
}
_ => {
self.diagnostics.push(
Diagnostic::error("P0602", "expected an expression", token.span).with_help(
"use a literal, name, call, field access, unary operation, or parenthesized expression",
),
);
if !matches!(
token.kind,
TokenKind::Newline
| TokenKind::Indent
| TokenKind::Dedent
| TokenKind::Eof
| TokenKind::RightBrace
| TokenKind::RightParen
| TokenKind::Comma
| TokenKind::FatArrow
) {
self.bump();
}
None
}
}
}
fn current_binary_operator(&self) -> Option<(BinaryOp, u8, u8)> {
Some(match &self.current().kind {
TokenKind::Keyword(Keyword::Or) => (BinaryOp::Or, 1, 2),
TokenKind::Keyword(Keyword::And) => (BinaryOp::And, 3, 4),
TokenKind::Equal => (BinaryOp::Equal, 5, 6),
TokenKind::NotEqual => (BinaryOp::NotEqual, 5, 6),
TokenKind::Greater => (BinaryOp::Greater, 5, 6),
TokenKind::GreaterEqual => (BinaryOp::GreaterEqual, 5, 6),
TokenKind::Less => (BinaryOp::Less, 5, 6),
TokenKind::LessEqual => (BinaryOp::LessEqual, 5, 6),
TokenKind::Plus => (BinaryOp::Add, 7, 8),
TokenKind::Minus => (BinaryOp::Subtract, 7, 8),
TokenKind::Star => (BinaryOp::Multiply, 9, 10),
TokenKind::Slash => (BinaryOp::Divide, 9, 10),
_ => return None,
})
}
fn parse_numeric_literal_token(&mut self) -> Option<(NumericLiteral, Span)> {
let minus = self.eat_simple(&TokenKind::Minus);
let token = self.current().clone();
let TokenKind::Number(value) = token.kind else {
self.error_here(
"P0603",
"expected a numeric literal",
"use a literal number here",
);
return None;
};
self.bump();
let text = if minus.is_some() {
format!("-{value}")
} else {
value
};
let span = minus.map_or(token.span, |minus| minus.span.merge(token.span));
let Some(value) = parse_lexed_numeric_literal(&text) else {
self.diagnostics.push(Diagnostic::error(
"P0601",
"numeric literal is outside the supported range",
span,
));
return None;
};
Some((value, span))
}
fn begin_suite(&mut self, context: &str) -> Option<()> {
self.expect_simple(TokenKind::Colon, &format!("expected `:` after {context}"))?;
self.begin_suite_after_colon(context)
}
fn begin_suite_after_colon(&mut self, context: &str) -> Option<()> {
self.require_line_break(context);
self.skip_newlines();
self.expect_simple(
TokenKind::Indent,
&format!("expected an indented block after {context}"),
)?;
Some(())
}
fn next_suite_item(&mut self) -> bool {
self.skip_newlines();
!self.at_simple(&TokenKind::Dedent) && !self.at_simple(&TokenKind::Eof)
}
fn end_suite(&mut self, context: &str) -> Option<Span> {
self.skip_newlines();
self.expect_simple(
TokenKind::Dedent,
&format!("expected the end of the {context} block"),
)
.map(|token| token.span)
}
fn line_contains(&self, kind: &TokenKind) -> bool {
let mut depth = 0usize;
for token in &self.tokens[self.cursor..] {
match token.kind {
TokenKind::LeftParen | TokenKind::LeftBrace => depth += 1,
TokenKind::RightParen | TokenKind::RightBrace => {
depth = depth.saturating_sub(1);
}
TokenKind::Newline if depth > 0 => continue,
TokenKind::Newline | TokenKind::Dedent | TokenKind::Eof => break,
_ => {}
}
if discriminant(&token.kind) == discriminant(kind) {
return true;
}
}
false
}
fn at_fragment_header(&self) -> bool {
let mut cursor = self.cursor;
if !self
.tokens
.get(cursor)
.is_some_and(|token| matches!(token.kind, TokenKind::Identifier(_)))
{
return false;
}
cursor += 1;
while self
.tokens
.get(cursor)
.is_some_and(|token| token.kind == TokenKind::DoubleColon)
{
cursor += 1;
if !self.token_is_identifier(cursor) {
return false;
}
cursor += 1;
}
if self
.tokens
.get(cursor)
.is_some_and(|token| matches!(token.kind, TokenKind::Locator(_)))
{
cursor += 1;
}
self.tokens
.get(cursor)
.is_some_and(|token| token.kind == TokenKind::Colon)
}
fn token_is_identifier(&self, cursor: usize) -> bool {
match self.tokens.get(cursor).map(|token| &token.kind) {
Some(TokenKind::Identifier(_)) => true,
Some(TokenKind::Keyword(keyword)) => keyword.is_contextual(),
_ => false,
}
}
fn current(&self) -> &Token {
&self.tokens[self.cursor.min(self.tokens.len().saturating_sub(1))]
}
fn bump(&mut self) -> Token {
let token = self.current().clone();
if !matches!(token.kind, TokenKind::Eof) {
self.cursor += 1;
}
token
}
fn previous_span(&self) -> Span {
self.tokens
.get(self.cursor.saturating_sub(1))
.map_or_else(|| self.current().span, |token| token.span)
}
fn at_simple(&self, expected: &TokenKind) -> bool {
discriminant(&self.current().kind) == discriminant(expected)
}
fn eat_simple(&mut self, expected: &TokenKind) -> Option<Token> {
self.at_simple(expected).then(|| self.bump())
}
fn expect_simple(&mut self, expected: TokenKind, message: &str) -> Option<Token> {
if self.at_simple(&expected) {
Some(self.bump())
} else {
self.diagnostics.push(
Diagnostic::error("P0001", message, self.current().span)
.with_help(format!("expected {} here", expected.description())),
);
None
}
}
fn at_keyword(&self, keyword: Keyword) -> bool {
matches!(self.current().kind, TokenKind::Keyword(value) if value == keyword)
}
fn eat_keyword(&mut self, keyword: Keyword) -> Option<Token> {
self.at_keyword(keyword).then(|| self.bump())
}
fn expect_keyword(&mut self, keyword: Keyword, message: &str) -> Option<Token> {
if self.at_keyword(keyword) {
Some(self.bump())
} else {
self.diagnostics.push(
Diagnostic::error("P0001", message, self.current().span)
.with_help(format!("expected `{}` here", keyword.as_str())),
);
None
}
}
fn expect_identifier(&mut self, message: &str) -> Option<Spanned<String>> {
let token = self.current().clone();
let value = match token.kind {
TokenKind::Identifier(value) => value,
TokenKind::Keyword(keyword) if keyword.is_contextual() => keyword.as_str().to_owned(),
_ => {
self.diagnostics.push(
Diagnostic::error("P0001", message, token.span)
.with_help("use a Unicode identifier that is not reserved here"),
);
return None;
}
};
self.bump();
Some(Spanned::new(value, token.span))
}
fn expect_qualified_identifier(&mut self, message: &str) -> Option<Spanned<String>> {
let first = self.expect_identifier(message)?;
self.finish_qualified_identifier(first.value, first.span)
}
fn finish_qualified_identifier(
&mut self,
mut value: String,
mut span: Span,
) -> Option<Spanned<String>> {
while self.eat_simple(&TokenKind::DoubleColon).is_some() {
let segment = self.expect_identifier("expected a name after `::`")?;
value.push_str("::");
value.push_str(&segment.value);
span = span.merge(segment.span);
}
Some(Spanned::new(value, span))
}
fn parse_identifier_expression(&mut self, name: String, span: Span) -> Option<Expr> {
self.bump();
let qualified = self.finish_qualified_identifier(name, span)?;
let callee = qualified.clone();
if self.at_simple(&TokenKind::LeftParen) {
let arguments = self.parse_arguments()?;
let end = self.previous_span();
Some(Expr::new(
ExprKind::Call { callee, arguments },
qualified.span.merge(end),
))
} else {
Some(Expr::new(ExprKind::Name(qualified.value), qualified.span))
}
}
fn expect_string(&mut self, message: &str) -> Option<Spanned<String>> {
let token = self.current().clone();
if let TokenKind::String(value) = token.kind {
self.bump();
Some(Spanned::new(value, token.span))
} else {
self.diagnostics
.push(Diagnostic::error("P0001", message, token.span));
None
}
}
fn skip_newlines(&mut self) {
while self.at_simple(&TokenKind::Newline) {
self.bump();
}
}
fn require_line_break(&mut self, context: &str) {
if self.eat_simple(&TokenKind::Newline).is_none() {
self.error_here(
"P0002",
&format!("expected a newline after {context}"),
"put the indented block on the following line",
);
self.synchronize_line();
}
}
fn finish_line(&mut self, context: &str) {
if let Some(semicolon) = self.eat_simple(&TokenKind::Semicolon) {
self.error_at(
"P0004",
"semicolons are not valid statement terminators",
semicolon.span,
"remove the semicolon",
);
}
if self.eat_simple(&TokenKind::Newline).is_some()
|| self.at_simple(&TokenKind::Dedent)
|| self.at_simple(&TokenKind::Eof)
{
return;
}
self.error_here(
"P0002",
&format!("unexpected token after {context}"),
"start the next item on a new line",
);
self.synchronize_line();
}
fn error_here(&mut self, code: &str, message: &str, help: &str) {
self.error_at(code, message, self.current().span, help);
}
fn error_at(&mut self, code: &str, message: &str, span: Span, help: &str) {
self.diagnostics
.push(Diagnostic::error(code, message, span).with_help(help));
}
fn synchronize_line(&mut self) {
while !self.at_simple(&TokenKind::Newline)
&& !self.at_simple(&TokenKind::Dedent)
&& !self.at_simple(&TokenKind::Eof)
{
self.bump();
}
let _ = self.eat_simple(&TokenKind::Newline);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_text(text: &str) -> ParseOutput {
parse(SourceFile::new("test.tes", text))
}
#[test]
fn module_defaults_and_use_is_optional() {
let output = parse_text("use \"more.tes\"\n\nenum Result:\n Yes\n No\n");
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let program = output.program.unwrap();
assert_eq!(program.module.value, "main");
assert_eq!(program.imports[0].path.value, "more.tes");
}
#[test]
fn parses_records_constraints_functions_and_cardinality_suffixes() {
let output = parse_text(
"mod policy\n\
enum Result:\n Yes\n No\n\n\
record Request:\n score: Int 0..100\n note: String?\n kind: String {\"normal\", \"special\"}\n\n\
fn eligible(request Request) -> Bool:\n request.score >= 70\n\n\
dec result(request Request) -> Result\n\
dec reasons(request Request) -> String*\n\
dec note(request Request) -> String?\n",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let declarations = output.program.unwrap().declarations;
let Declaration::Entity(record) = &declarations[1] else {
panic!("expected record");
};
assert!(record.fields[1].optional);
assert!(record.fields[0].range.is_some());
assert!(record.fields[2].domain.is_some());
assert!(matches!(declarations[2], Declaration::Derive(_)));
assert!(matches!(
&declarations[4],
Declaration::Decision(value) if value.cardinality == Cardinality::Many
));
assert!(matches!(
&declarations[5],
Declaration::Decision(value) if value.cardinality == Cardinality::ZeroOrOne
));
}
#[test]
fn parses_space_parameters_and_single_type_shorthand() {
let output = parse_text(
"dec shorthand(domain::Request) -> Bool\n\
dec explicit(request domain::Request) -> Bool\n",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let declarations = output.program.unwrap().declarations;
let Declaration::Decision(shorthand) = &declarations[0] else {
panic!("expected decision");
};
assert_eq!(shorthand.parameters[0].name.value, "Request");
assert_eq!(shorthand.parameters[0].ty.value, "domain::Request");
let Declaration::Decision(explicit) = &declarations[1] else {
panic!("expected decision");
};
assert_eq!(explicit.parameters[0].name.value, "request");
}
#[test]
fn rejects_multiple_type_only_parameters() {
let output = parse_text("dec ambiguous(Left, Right) -> Bool\n");
let codes = output
.diagnostics
.iter()
.map(|diagnostic| diagnostic.code.as_str())
.collect::<Vec<_>>();
assert_eq!(codes.iter().filter(|code| **code == "P0552").count(), 2);
}
#[test]
fn parses_fragment_raw_text_and_implicit_basis() {
let output = parse_text(
r"dec result(request Request) -> Bool
law::residence @조 4 / 항 1:
신청인은 국내에 주소를 두어야 한다.
# raw source, not a comment
ref law::date
rule residence(request Request):
basis law::date
not request.domestic => result(request) = false
law::date @조 3 / 항 2:
기준일은 신청일이다.
",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let declarations = output.program.unwrap().declarations;
let Declaration::Fragment(fragment) = &declarations[1] else {
panic!("expected fragment");
};
assert_eq!(fragment.id.value, "law::residence");
assert_eq!(fragment.locator.value, "조 4 / 항 1");
assert!(fragment.text.value.contains("# raw source"));
assert_eq!(fragment.refs[0].id.value, "law::date");
assert_eq!(fragment.rules[0].basis.len(), 2);
assert_eq!(fragment.rules[0].basis[0].id.value, "law::residence");
assert_eq!(fragment.rules[0].basis[1].id.value, "law::date");
}
#[test]
fn parses_fragment_functions_with_implicit_and_explicit_basis() {
let output = parse_text(
r"law::formula:
The amount is seventy percent of the stated value.
fn amount(request Request) -> Decimal:
basis law::definitions
request.value * 0.7
",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let Declaration::Fragment(fragment) = &output.program.unwrap().declarations[0] else {
panic!("expected fragment");
};
assert_eq!(fragment.derives.len(), 1);
assert_eq!(fragment.derives[0].name.value, "amount");
assert_eq!(fragment.derives[0].basis.len(), 2);
assert_eq!(fragment.derives[0].basis[0].id.value, "law::formula");
assert_eq!(fragment.derives[0].basis[1].id.value, "law::definitions");
}
#[test]
fn parses_unconditional_and_override_rules() {
let output = parse_text(
r"policy::default:
Default policy.
rule base(request Request):
=> result(request) = true
rule exception(request Request):
request.blocked => override base
",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let Declaration::Fragment(fragment) = &output.program.unwrap().declarations[0] else {
panic!("expected fragment");
};
assert!(matches!(
fragment.rules[0].condition.kind,
ExprKind::Literal(Literal::Bool(true))
));
assert!(matches!(fragment.rules[1].effect, Effect::Override { .. }));
}
#[test]
fn recovers_a_rule_without_an_effect_with_a_single_placeholder() {
let output = parse_text(
r"policy::default:
Policy text.
rule empty():
basis policy::default
",
);
assert!(
output
.diagnostics
.iter()
.any(|diagnostic| diagnostic.code == "P0200"),
"{:?}",
output.diagnostics
);
let Declaration::Rule(rule) = &output.program.unwrap().declarations[1] else {
panic!("expected recovered rule");
};
assert!(matches!(rule.effect, Effect::Invalid { .. }));
}
#[test]
fn keeps_jurisdiction_neutral_locators_verbatim() {
let output = parse_text(
"kr::article @조 4 / 항 1:\n 한국 법령.\n\n\
us::fair_use @USC 17 / § 107 / [a] 🏛:\n United States law.\n\n\
jp::tort @民法 / 第709条:\n 日本法.\n\n\
contract::term @서비스계약 / 8.2 / (a):\n Contract term.\n",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let locators = output
.program
.unwrap()
.declarations
.into_iter()
.map(|declaration| match declaration {
Declaration::Fragment(fragment) => fragment.locator.value,
_ => panic!("expected fragment"),
})
.collect::<Vec<_>>();
assert_eq!(
locators,
[
"조 4 / 항 1",
"USC 17 / § 107 / [a] 🏛",
"民法 / 第709条",
"서비스계약 / 8.2 / (a)",
]
);
}
#[test]
fn parses_tests_and_assertions_with_defaults() {
let output = parse_text(
r"test basic:
shared::Request { domestic: true }
expect result(Request) = true
assert total(request Request):
result(request)
assert some exception(request Request):
request.special => result(request) != false
assert optional(request Request):
result(request)?
",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let declarations = output.program.unwrap().declarations;
let Declaration::Case(test) = &declarations[0] else {
panic!("expected test");
};
assert_eq!(test.bindings[0].name.value, "Request");
assert_eq!(test.bindings[0].entity.value, "shared::Request");
let Declaration::Invariant(total) = &declarations[1] else {
panic!("expected assertion");
};
assert_eq!(total.quantifier, InvariantQuantifier::All);
let Declaration::Invariant(exception) = &declarations[2] else {
panic!("expected assertion");
};
assert_eq!(exception.quantifier, InvariantQuantifier::Some);
assert!(matches!(
exception.assertion,
InvariantAssertion::Implication { .. }
));
assert!(matches!(
&declarations[3],
Declaration::Invariant(InvariantDecl {
assertion: InvariantAssertion::Cardinality {
cardinality: Cardinality::ZeroOrOne,
..
},
..
})
));
}
#[test]
fn parses_multiline_parenthesized_assertion_condition() {
let output = parse_text(
r"record Request:
enabled: Bool
approved: Bool
dec result(request Request) -> Bool
assert approved_request(request Request):
(request.enabled
and request.approved) => result(request) = true
",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let Declaration::Invariant(assertion) = &output.program.unwrap().declarations[2] else {
panic!("expected assertion");
};
assert!(matches!(
assertion.assertion,
InvariantAssertion::Implication { .. }
));
}
#[test]
fn parses_state_transition_and_trace() {
let output = parse_text(
r"state Flow:
done: Bool
tries: Int 0..3
action advance(flow Flow)
transition advance_once(flow Flow) on advance(flow):
when not flow.done
set flow.tries = flow.tries + 1
trace completes(flow Flow):
initially not flow.done
always flow.tries >= 0
terminates when flow.done
no dead ends
within 3 steps
",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let declarations = output.program.unwrap().declarations;
assert!(matches!(declarations[2], Declaration::Transition(_)));
let Declaration::Trace(trace) = &declarations[3] else {
panic!("expected trace");
};
assert_eq!(trace.within.value, 3);
assert!(trace.no_dead_ends);
}
#[test]
fn numeric_literals_and_precedence_are_stable() {
assert_eq!(
parse_numeric_literal("-1_234.50_01"),
Some(NumericLiteral::Decimal("-1234.5001".to_owned()))
);
let output =
parse_text("fn score(request Request) -> Decimal:\n request.a + request.b * 0.3\n");
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let Declaration::Derive(value) = &output.program.unwrap().declarations[0] else {
panic!("expected fn");
};
assert!(matches!(
value.expression.kind,
ExprKind::Binary {
operator: BinaryOp::Add,
..
}
));
}
#[test]
fn declaration_keywords_are_not_implicit_fragment_ids() {
for keyword in ["state", "rule", "test", "assert", "trace"] {
let output = parse_text(&format!("{keyword}:\n source\n"));
assert!(!output.diagnostics.is_empty(), "{keyword}");
assert!(
output
.program
.unwrap()
.declarations
.iter()
.all(|declaration| { !matches!(declaration, Declaration::Fragment(_)) })
);
}
let typo = parse_text("rul unexpected:\n");
assert!(!typo.diagnostics.is_empty());
assert!(typo.program.unwrap().declarations.is_empty());
}
}