use crate::ast::{
BinaryOp, BindingDecl, Cardinality, CaseBody, CaseBranch, CaseDecl, ChoiceBranch, ChoiceDecl,
ChoiceMember, CompareOp, Declaration, DeriveBody, DeriveDecl, DomainConstraint, Effect,
EntityDecl, EnumDecl, Expectation, ExpectedValue, Expr, ExprKind, FieldDecl, FieldValue,
FixtureDecl, FragmentDecl, FragmentRef, ImportDecl, InvariantAssertion, InvariantDecl,
InvariantQuantifier, Literal, Name, NumericLiteral, Parameter, Program, RangeConstraint,
RuleDecl, RuleOverride, TypeRef, UnaryOp,
};
use crate::diagnostic::Diagnostic;
use crate::lexer::{Keyword, Token, TokenKind, lex};
use crate::source::{SourceFile, Span, Spanned};
use std::mem::discriminant;
fn is_builtin_type_name(name: &str) -> bool {
matches!(
name,
"Bool" | "Int" | "Decimal" | "String" | "Date" | "Duration"
)
}
#[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
)
}
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`, `fn`, `rule`, `fixture`, `test`, or `assert`",
);
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::Fn) {
self.parse_fn().map(Declaration::Derive)
} else if self.at_keyword(Keyword::Rule) {
self.parse_rule(None).map(Declaration::Rule)
} else if self.at_keyword(Keyword::Fixture) {
self.parse_fixture().map(Declaration::Fixture)
} 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 {
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 mut fields = Vec::new();
let mut choices = Vec::new();
while self.next_suite_item() {
if self.at_choice_declaration() {
if let Some(choice) = self.parse_choice(&mut fields) {
choices.push(choice);
}
} else if let Some(field) = self.parse_field() {
fields.push(field);
} else {
self.synchronize_line();
}
}
let end = self.end_suite("record")?;
Some(EntityDecl {
name,
fields,
choices,
span: start.merge(end),
})
}
fn parse_choice(&mut self, fields: &mut Vec<FieldDecl>) -> Option<ChoiceDecl> {
let start = self.bump().span;
let name = self.expect_identifier("expected a choice selector name")?;
let selector_start = name.span;
let ty = self.parse_type_ref()?;
let optional = self.eat_simple(&TokenKind::Question).is_some();
let selector_end = self.previous_span();
let selector_field = fields.len();
fields.push(FieldDecl {
name,
ty: ty.value,
ty_span: ty.span,
range: None,
domain: None,
optional,
span: selector_start.merge(selector_end),
});
self.begin_suite("choice header")?;
let mut branches = Vec::new();
while self.next_suite_item() {
if let Some(branch) = self.parse_choice_branch(fields) {
branches.push(branch);
} else {
self.synchronize_line();
}
}
let end = self.end_suite("choice")?;
if branches.is_empty() {
self.error_at(
"P0110",
"a choice must have at least one branch",
start.merge(end),
"add one branch for every selector enum variant",
);
}
Some(ChoiceDecl {
selector_field,
branches,
span: start.merge(end),
})
}
fn parse_choice_branch(&mut self, fields: &mut Vec<FieldDecl>) -> Option<ChoiceBranch> {
let variant = self.expect_identifier("expected an enum variant for choice branch")?;
let start = variant.span;
self.begin_suite("choice branch")?;
let mut members = Vec::new();
while self.next_suite_item() {
if self.at_choice_declaration() {
let nested_start = self.current().span;
self.error_at(
"P0111",
"nested choices are not supported",
nested_start,
"move the nested selector to the record body",
);
let mut ignored = Vec::new();
let _ = self.parse_choice(&mut ignored);
continue;
}
if let Some(mut field) = self.parse_field() {
let required_when_active = !field.optional;
field.optional = true;
let field_index = fields.len();
fields.push(field);
members.push(ChoiceMember {
field: field_index,
required_when_active,
});
} else {
self.synchronize_line();
}
}
let end = self.end_suite("choice branch")?;
if members.is_empty() {
self.error_at(
"P0112",
"a choice branch must declare at least one field",
start.merge(end),
"add an indented field below this branch",
);
}
Some(ChoiceBranch {
variant,
members,
span: start.merge(end),
})
}
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 self.at_simple(&TokenKind::LeftBracket) || self.at_simple(&TokenKind::LeftParen) {
let open = self.bump();
let start_inclusive = open.kind == TokenKind::LeftBracket;
self.skip_newlines();
let (range_start, start_span) = self.parse_numeric_literal_token()?;
self.skip_newlines();
self.expect_simple(TokenKind::Comma, "expected `,` between interval bounds")?;
self.skip_newlines();
let (range_end, end_span) = self.parse_numeric_literal_token()?;
self.skip_newlines();
let close = if self.at_simple(&TokenKind::RightBracket)
|| self.at_simple(&TokenKind::RightParen)
{
self.bump()
} else {
self.error_here(
"P0102",
"expected `]` or `)` to close interval",
"use `]` for an inclusive upper bound or `)` for an exclusive upper bound",
);
return None;
};
range = Some(RangeConstraint {
start: range_start,
end: range_end,
start_inclusive,
end_inclusive: close.kind == TokenKind::RightBracket,
span: open
.span
.merge(start_span)
.merge(end_span)
.merge(close.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,
ty_span: ty.span,
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, implicit_parameters) = if self.at_simple(&TokenKind::LeftParen) {
(self.parse_fn_parameters()?, false)
} else {
(Vec::new(), true)
};
let (return_type, implicit_return_type) = if self.at_simple(&TokenKind::Colon) {
let separator = self.bump();
self.begin_suite_after_colon("function header")?;
(Spanned::new(TypeRef::Unknown, separator.span), true)
} else {
let return_type = self.parse_type_ref()?;
self.begin_suite("function header")?;
(return_type, false)
};
if !self.next_suite_item() {
self.error_here(
"P0110",
"a function needs an expression",
"add one indented expression",
);
return None;
}
let body = if self.at_keyword(Keyword::Case) {
DeriveBody::Case(self.parse_case_body()?)
} else {
let expression = self.parse_continued_boolean_expression()?;
self.finish_line("function expression");
DeriveBody::Expression(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,
implicit_parameters,
return_type,
implicit_return_type,
body,
basis,
span: start.merge(end),
})
}
fn parse_case_body(&mut self) -> Option<CaseBody> {
let start = self.bump().span;
self.begin_suite("`case` header")?;
let mut branches = Vec::new();
while self.next_suite_item() {
let Some(branch) = self.parse_case_branch() else {
self.synchronize_line();
continue;
};
branches.push(branch);
}
let end = self.end_suite("case")?;
if branches.is_empty() {
self.error_at(
"P0112",
"a case body needs at least one branch",
start.merge(end),
"add `<name> when <condition>: <value>`",
);
}
Some(CaseBody {
branches,
span: start.merge(end),
})
}
fn parse_case_branch(&mut self) -> Option<CaseBranch> {
let name = self.expect_identifier("expected a case branch name")?;
if self.eat_keyword(Keyword::When).is_none() {
self.error_here(
"P0113",
"expected `when` after the case branch name",
"write `<name> when <condition>: <value>`",
);
return None;
}
let condition = self.parse_continued_boolean_expression()?;
if self.eat_rule_separator().is_none() && self.eat_same_depth_rule_separator().is_none() {
self.error_here(
"P0114",
"expected `:` after the case branch condition",
"put `:` between the condition and the branch value",
);
return None;
}
let value = self.parse_continued_boolean_expression()?;
self.finish_line("case branch");
Some(CaseBranch {
span: name.span.merge(value.span),
name,
condition,
value,
})
}
fn parse_fragment(&mut self) -> Option<FragmentDecl> {
let id = self.expect_qualified_identifier("expected a fragment ID")?;
let start = id.span;
let locator = 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(), id.span)
};
let mut derives = Vec::new();
let mut rules = Vec::new();
while self.next_suite_item() {
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 `fn ...:` or `rule ...:`",
);
self.synchronize_line();
}
}
let end = self.end_suite("fragment")?;
Some(FragmentDecl {
id,
locator,
text,
refs: Vec::new(),
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, implicit_parameters) = self.parse_optional_parameters()?;
self.begin_suite("rule header")?;
let basis = enclosing
.map(|id| vec![FragmentRef { id: id.clone() }])
.unwrap_or_default();
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,
implicit_parameters,
condition: Expr::new(ExprKind::Literal(Literal::Unknown), start),
effect: Effect::Invalid { span: start },
overrides: Vec::new(),
basis,
span: start.merge(end),
});
}
let condition = if self.eat_rule_separator().is_some() {
Expr::new(ExprKind::Literal(Literal::Bool(true)), self.previous_span())
} else {
let condition = self.parse_continued_boolean_expression()?;
if self.eat_rule_separator().is_none() && self.eat_same_depth_rule_separator().is_none()
{
self.error_here(
"P0202",
"expected `:` after rule condition",
"put `:` after the condition or at the start of the next rule-body line",
);
return None;
}
condition
};
let effects = if self.at_simple(&TokenKind::Newline) {
self.begin_suite_after_colon("rule effect block")?;
let mut effects = Vec::new();
while self.next_suite_item() {
let effect = self.parse_effect()?;
self.finish_line("rule effect");
effects.push(effect);
}
self.end_suite("rule effect")?;
effects
} else {
let effect = self.parse_effect()?;
self.finish_line("rule effect");
vec![effect]
};
let mut decision = None;
let mut overrides = Vec::new();
let mut first_override = None;
let mut effect_end = condition.span;
for effect in effects {
effect_end = effect_end.merge(effect_span(&effect));
match effect {
Effect::Decide { .. } if decision.is_some() => {
self.error_at(
"P0201",
"a rule may produce only one decision candidate",
effect_span(&effect),
"split candidates for different decisions into separate named rules",
);
}
Effect::Decide { .. } => decision = Some(effect),
Effect::Override { rule, span } => {
let target = RuleOverride { rule, span };
if first_override.is_none() {
first_override = Some(target);
} else {
overrides.push(target);
}
}
Effect::Invalid { .. } => {}
}
}
let effect = if let Some(decision) = decision {
if let Some(first) = first_override {
overrides.insert(0, first);
}
decision
} else if let Some(first) = first_override {
Effect::Override {
rule: first.rule,
span: first.span,
}
} else {
Effect::Invalid { span: start }
};
while self.next_suite_item() {
self.error_here(
"P0203",
"unexpected item after the rule effect",
"put multiple effects in an indented block after the condition `:`",
);
self.synchronize_line();
}
let end = self.end_suite("rule")?;
Some(RuleDecl {
name,
parameters,
implicit_parameters,
condition,
effect,
overrides,
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, implicit_arguments) = self.parse_optional_arguments()?;
self.expect_simple(TokenKind::Equal, "expected `=` in decision effect")?;
let value = self.parse_expression()?;
Some(Effect::Decide {
decision,
arguments,
implicit_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 { ... }`, `Record from fixture { ... }`, `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_fixture(&mut self) -> Option<FixtureDecl> {
let start = self.bump().span;
let name = self.expect_identifier("expected a fixture name")?;
let entity =
self.expect_qualified_identifier("expected a record name after fixture name")?;
self.begin_suite("fixture header")?;
let mut fields = Vec::new();
let mut end = entity.span;
while self.next_suite_item() {
let Some(field_name) = self.expect_identifier("expected a field name") else {
self.synchronize_line();
continue;
};
if self
.expect_simple(TokenKind::Colon, "expected `:` after field name")
.is_none()
{
self.synchronize_line();
continue;
}
let Some(value) = self.parse_expression() else {
self.synchronize_line();
continue;
};
let span = field_name.span.merge(value.span);
end = span;
fields.push(FieldValue {
name: field_name,
value,
span,
});
self.finish_line("fixture field");
}
let suite_end = self.end_suite("fixture")?;
Some(FixtureDecl {
name,
entity,
fields,
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;
let fixture = if self.eat_keyword(Keyword::From).is_some() {
Some(self.expect_qualified_identifier("expected a fixture name after `from`")?)
} else {
None
};
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,
fixture,
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, implicit_arguments) = self.parse_optional_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 expected = self.parse_expected_value()?;
let end = expected.span();
Some(Expectation {
decision,
arguments,
implicit_arguments,
operator,
expected,
span: start.merge(end),
})
}
fn parse_expected_value(&mut self) -> Option<ExpectedValue> {
let Some(open) = self.eat_simple(&TokenKind::LeftBrace) else {
return self.parse_expression().map(ExpectedValue::Scalar);
};
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(
"P0303",
"expected set values must be separated by commas",
"add `,` after the preceding value",
);
return None;
}
}
self.skip_newlines();
}
let close = self
.expect_simple(TokenKind::RightBrace, "expected `}` to close expected set")?
.span;
Some(ExpectedValue::Set {
values,
span: open.span.merge(close),
})
}
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, implicit_variables) = self.parse_optional_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_rule_separator() {
let condition = self.parse_expression()?;
let separator = self.expect_rule_separator("assertion condition")?;
let expectation = self.parse_expectation(separator.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, implicit_arguments) = self.parse_optional_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,
implicit_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,
implicit_variables,
span: start.merge(end),
assertion,
})
}
fn parse_parameters(&mut self) -> Option<Vec<Parameter>> {
self.parse_parameters_with_inferred_types(false)
}
fn parse_fn_parameters(&mut self) -> Option<Vec<Parameter>> {
self.parse_parameters_with_inferred_types(true)
}
fn parse_parameters_with_inferred_types(
&mut self,
infer_bare_identifiers: bool,
) -> Option<Vec<Parameter>> {
let open = self
.expect_simple(TokenKind::LeftParen, "expected `(` before parameter list")?
.span;
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, implicit_type, type_only) = if self.at_simple(&TokenKind::Dot) {
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, false, true)
} else if self.token_is_identifier(self.cursor) {
let ty = self.expect_qualified_identifier("expected a parameter type")?;
(first, ty, false, false)
} else if infer_bare_identifiers && !is_builtin_type_name(&first.value) {
let ty = Spanned::new(String::new(), first.span);
(first, ty, true, 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, false, true)
};
let span = name.span.merge(ty.span);
parsed.push((
Parameter {
ty,
implicit_type,
implicit_binding: false,
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();
}
let close = self
.expect_simple(TokenKind::RightParen, "expected `)` after parameter list")?
.span;
if parsed.is_empty() {
self.error_at(
"P0553",
"empty parameter lists are not part of Tess source syntax",
open.merge(close),
"omit `()`; a header without a parameter list infers its inputs",
);
}
if !infer_bare_identifiers && 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_optional_parameters(&mut self) -> Option<(Vec<Parameter>, bool)> {
if self.at_simple(&TokenKind::LeftParen) {
self.parse_parameters()
.map(|parameters| (parameters, false))
} else {
Some((Vec::new(), true))
}
}
fn parse_arguments(&mut self) -> Option<Vec<Expr>> {
let open = self
.expect_simple(TokenKind::LeftParen, "expected `(` before argument list")?
.span;
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();
}
let close = self
.expect_simple(TokenKind::RightParen, "expected `)` after argument list")?
.span;
if arguments.is_empty() {
self.error_at(
"P0554",
"empty argument lists are not part of Tess source syntax",
open.merge(close),
"omit `()`; a bare name denotes a zero-input function or decision",
);
}
Some(arguments)
}
fn parse_optional_arguments(&mut self) -> Option<(Vec<Expr>, bool)> {
if self.at_simple(&TokenKind::LeftParen) {
self.parse_arguments().map(|arguments| (arguments, false))
} else {
Some((Vec::new(), true))
}
}
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_boolean_expression(false)
}
fn parse_continued_boolean_expression(&mut self) -> Option<Expr> {
self.parse_boolean_expression(true)
}
fn parse_boolean_expression(&mut self, allow_boolean_continuations: bool) -> Option<Expr> {
let mut left = self.parse_conjunction(allow_boolean_continuations)?;
loop {
if allow_boolean_continuations {
self.skip_same_depth_boolean_line_break();
}
if self.eat_simple(&TokenKind::Pipe).is_none() {
break;
}
let right = self.parse_conjunction(allow_boolean_continuations)?;
let span = left.span.merge(right.span);
left = Expr::new(
ExprKind::Binary {
left: Box::new(left),
operator: BinaryOp::Or,
right: Box::new(right),
},
span,
);
}
Some(left)
}
fn parse_conjunction(&mut self, allow_boolean_continuations: bool) -> Option<Expr> {
let mut left = self.parse_postfix_not()?;
loop {
if allow_boolean_continuations {
self.skip_same_depth_boolean_line_break();
}
if self.eat_simple(&TokenKind::Ampersand).is_none() {
break;
}
let right = self.parse_postfix_not()?;
let span = left.span.merge(right.span);
left = Expr::new(
ExprKind::Binary {
left: Box::new(left),
operator: BinaryOp::And,
right: Box::new(right),
},
span,
);
}
Some(left)
}
fn parse_postfix_not(&mut self) -> Option<Expr> {
let mut expression = self.parse_precedence(0)?;
while let Some(operator) = self.eat_keyword(Keyword::X) {
let span = expression.span.merge(operator.span);
expression = Expr::new(
ExprKind::Unary {
operator: UnaryOp::Not,
operand: Box::new(expression),
},
span,
);
}
Some(expression)
}
fn parse_precedence(&mut self, minimum_binding_power: u8) -> Option<Expr> {
let mut left = self.parse_prefix()?;
let mut comparison_tail: Option<Expr> = None;
while let Some((operator, left_power, right_power)) = self.current_binary_operator() {
if left_power < minimum_binding_power {
break;
}
self.bump();
let right = self.parse_precedence(right_power)?;
let comparison_left = if is_comparison(operator) {
comparison_tail.clone().unwrap_or_else(|| left.clone())
} else {
left.clone()
};
let comparison_span = comparison_left.span.merge(right.span);
let comparison = Expr::new(
ExprKind::Binary {
left: Box::new(comparison_left),
operator,
right: Box::new(right.clone()),
},
comparison_span,
);
if is_comparison(operator) && comparison_tail.is_some() {
let span = left.span.merge(right.span);
left = Expr::new(
ExprKind::Binary {
left: Box::new(left),
operator: BinaryOp::And,
right: Box::new(comparison),
},
span,
);
} else {
left = comparison;
}
comparison_tail = is_comparison(operator).then_some(right);
}
Some(left)
}
fn parse_prefix(&mut self) -> Option<Expr> {
if let Some(operator) = self.eat_simple(&TokenKind::Minus) {
let operand = self.parse_precedence(7)?;
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()?;
loop {
if 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,
implicit_receiver: false,
},
span,
);
continue;
}
let TokenKind::Identifier(name) = self.current().kind.clone() else {
break;
};
let callee_span = self.bump().span;
let callee = self.finish_qualified_identifier(name, callee_span)?;
let span = expression.span.merge(callee.span);
expression = Expr::new(
ExprKind::Call {
callee,
arguments: vec![expression],
implicit_arguments: false,
postfix: true,
},
span,
);
}
Some(expression)
}
fn skip_same_depth_boolean_line_break(&mut self) {
let mut cursor = self.cursor;
if !matches!(
self.tokens.get(cursor).map(|token| &token.kind),
Some(TokenKind::Newline)
) {
return;
}
while matches!(
self.tokens.get(cursor).map(|token| &token.kind),
Some(TokenKind::Newline)
) {
cursor += 1;
}
if matches!(
self.tokens.get(cursor).map(|token| &token.kind),
Some(TokenKind::Ampersand | TokenKind::Pipe)
) {
while self.cursor < cursor {
self.bump();
}
}
}
fn eat_same_depth_line_token(&mut self, kind: &TokenKind) -> Option<Token> {
let mut cursor = self.cursor;
if !matches!(
self.tokens.get(cursor).map(|token| &token.kind),
Some(TokenKind::Newline)
) {
return None;
}
while matches!(
self.tokens.get(cursor).map(|token| &token.kind),
Some(TokenKind::Newline)
) {
cursor += 1;
}
if self
.tokens
.get(cursor)
.is_none_or(|token| discriminant(&token.kind) != discriminant(kind))
{
return None;
}
while self.cursor < cursor {
self.bump();
}
self.eat_simple(kind)
}
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::O) => {
self.bump();
Some(Expr::new(
ExprKind::Literal(Literal::Bool(true)),
token.span,
))
}
TokenKind::Keyword(Keyword::X) => {
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::Colon
) {
self.bump();
}
None
}
}
}
fn current_binary_operator(&self) -> Option<(BinaryOp, u8, u8)> {
Some(match &self.current().kind {
TokenKind::Equal => (BinaryOp::Equal, 1, 2),
TokenKind::NotEqual => (BinaryOp::NotEqual, 1, 2),
TokenKind::Greater => (BinaryOp::Greater, 1, 2),
TokenKind::GreaterEqual => (BinaryOp::GreaterEqual, 1, 2),
TokenKind::Less => (BinaryOp::Less, 1, 2),
TokenKind::LessEqual => (BinaryOp::LessEqual, 1, 2),
TokenKind::Plus => (BinaryOp::Add, 3, 4),
TokenKind::Minus => (BinaryOp::Subtract, 3, 4),
TokenKind::Star => (BinaryOp::Multiply, 5, 6),
TokenKind::Slash => (BinaryOp::Divide, 5, 6),
_ => 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 eat_rule_separator(&mut self) -> Option<Token> {
if self.at_simple(&TokenKind::Colon) {
Some(self.bump())
} else {
None
}
}
fn expect_rule_separator(&mut self, context: &str) -> Option<Token> {
self.eat_rule_separator().or_else(|| {
self.error_here(
"P0001",
&format!("expected `:` after {context}"),
"put `:` between the condition and its effect",
);
None
})
}
fn eat_same_depth_rule_separator(&mut self) -> Option<Token> {
self.eat_same_depth_line_token(&TokenKind::Colon)
}
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 line_contains_rule_separator(&self) -> bool {
self.line_contains(&TokenKind::Colon)
}
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::Dot)
{
cursor += 1;
if !self.token_is_identifier(cursor) {
return false;
}
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 at_choice_declaration(&self) -> bool {
self.at_keyword(Keyword::Choice)
&& !self
.tokens
.get(self.cursor + 1)
.is_some_and(|token| token.kind == TokenKind::Colon)
}
fn eat_keyword(&mut self, keyword: Keyword) -> Option<Token> {
self.at_keyword(keyword).then(|| self.bump())
}
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::Dot).is_some() {
let segment = self.expect_identifier("expected a name after `.`")?;
value.push('.');
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> {
let qualified_call = self.qualified_call_follows();
self.bump();
let qualified = if qualified_call {
self.finish_qualified_identifier(name, span)?
} else {
Spanned::new(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,
implicit_arguments: false,
postfix: false,
},
qualified.span.merge(end),
))
} else {
Some(Expr::new(ExprKind::Name(qualified.value), qualified.span))
}
}
fn qualified_call_follows(&self) -> bool {
let mut cursor = self.cursor + 1;
while self
.tokens
.get(cursor)
.is_some_and(|token| token.kind == TokenKind::Dot)
{
cursor += 1;
if !self.token_is_identifier(cursor) {
return false;
}
cursor += 1;
}
self.tokens
.get(cursor)
.is_some_and(|token| token.kind == TokenKind::LeftParen)
}
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_record_choices_into_a_flat_schema_with_branch_metadata() {
let output = parse_text(
r"enum Kind:
Direct
Derived
record Claim:
text: String
choice method Kind?:
Direct:
fact: Bool
note: String?
Derived:
premise: Bool
",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let declarations = output.program.unwrap().declarations;
let Declaration::Entity(entity) = &declarations[1] else {
panic!("expected record");
};
assert_eq!(
entity
.fields
.iter()
.map(|field| (field.name.value.as_str(), field.optional))
.collect::<Vec<_>>(),
[
("text", false),
("method", true),
("fact", true),
("note", true),
("premise", true),
]
);
let choice = &entity.choices[0];
assert_eq!(choice.selector_field, 1);
assert_eq!(choice.branches[0].variant.value, "Direct");
assert_eq!(choice.branches[0].members[0].field, 2);
assert!(choice.branches[0].members[0].required_when_active);
assert!(!choice.branches[0].members[1].required_when_active);
assert_eq!(choice.branches[1].members[0].field, 4);
}
#[test]
fn rejects_nested_record_choices() {
let output = parse_text(
r"enum Kind:
A
record Request:
choice outer Kind:
A:
choice inner Kind:
A:
value: Bool
",
);
assert!(
output
.diagnostics
.iter()
.any(|diagnostic| diagnostic.code == "P0111"),
"{:?}",
output.diagnostics
);
}
#[test]
fn parses_new_boolean_interval_context_and_exact_set_surface() {
let output = parse_text(
r"record 점수표:
폐구간: Int [0, 100]
좌개방: Decimal (0, 1]
우개방: Int [0, 100)
개구간: Decimal (0, 1)
fn 평가가능:
총점 >= 90 X & 예외 | O
fn 명시호출:
평가가능(학생)
assert 감사표시일치:
청구금액 > 승인한도: 감사표시 = {승인한도초과, 고액청구}
test 표시없음:
점수표 { 폐구간: 50 }
expect 감사표시 = {}
",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let declarations = output.program.unwrap().declarations;
let Declaration::Entity(record) = &declarations[0] else {
panic!("expected record");
};
let inclusivity = record
.fields
.iter()
.map(|field| {
let range = field.range.as_ref().expect("expected interval");
(range.start_inclusive, range.end_inclusive)
})
.collect::<Vec<_>>();
assert_eq!(
inclusivity,
[(true, true), (false, true), (true, false), (false, false)]
);
let Declaration::Derive(derive) = &declarations[1] else {
panic!("expected function");
};
assert!(derive.implicit_parameters);
let ExprKind::Binary {
left,
operator: BinaryOp::Or,
..
} = &derive.expression().expect("expression body").kind
else {
panic!("expected disjunction");
};
let ExprKind::Binary {
left: negated,
operator: BinaryOp::And,
..
} = &left.kind
else {
panic!("expected conjunction");
};
assert!(matches!(
negated.kind,
ExprKind::Unary {
operator: UnaryOp::Not,
..
}
));
let Declaration::Derive(explicit_call) = &declarations[2] else {
panic!("expected function");
};
assert!(matches!(
explicit_call.expression().expect("expression body").kind,
ExprKind::Call {
implicit_arguments: false,
..
}
));
let Declaration::Invariant(invariant) = &declarations[3] else {
panic!("expected assertion");
};
assert!(invariant.implicit_variables);
let InvariantAssertion::Implication { expectation, .. } = &invariant.assertion else {
panic!("expected implication");
};
assert!(matches!(
&expectation.expected,
ExpectedValue::Set { values, .. } if values.len() == 2
));
let Declaration::Case(case) = &declarations[4] else {
panic!("expected test");
};
assert!(matches!(
&case.expectations[0].expected,
ExpectedValue::Set { values, .. } if values.is_empty()
));
}
#[test]
fn parses_records_constraints_functions_and_cardinality_assertions() {
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\
assert reasons_cardinality(request Request):\n reasons(request)*\n\n\
assert note_cardinality(request Request):\n note(request)?\n\n\
assert bare_cardinality(request Request):\n note?\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(_)));
let Declaration::Invariant(reasons) = &declarations[3] else {
panic!("expected cardinality assertion");
};
assert!(matches!(
reasons.assertion,
InvariantAssertion::Cardinality {
cardinality: Cardinality::Many,
..
}
));
let Declaration::Invariant(note) = &declarations[4] else {
panic!("expected cardinality assertion");
};
assert!(matches!(
note.assertion,
InvariantAssertion::Cardinality {
cardinality: Cardinality::ZeroOrOne,
..
}
));
let Declaration::Invariant(bare) = &declarations[5] else {
panic!("expected bare cardinality assertion");
};
assert!(matches!(
&bare.assertion,
InvariantAssertion::Cardinality {
cardinality: Cardinality::ZeroOrOne,
arguments,
implicit_arguments: true,
..
} if arguments.is_empty()
));
}
#[test]
fn parses_space_parameters_and_single_type_shorthand() {
let output = parse_text(
"rule shorthand(domain.Request):\n : result(Request) = O\n\n\
rule explicit(request domain.Request):\n : result(request) = O\n",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let declarations = output.program.unwrap().declarations;
let Declaration::Rule(shorthand) = &declarations[0] else {
panic!("expected rule");
};
assert_eq!(shorthand.parameters[0].name.value, "Request");
assert_eq!(shorthand.parameters[0].ty.value, "domain.Request");
let Declaration::Rule(explicit) = &declarations[1] else {
panic!("expected rule");
};
assert_eq!(explicit.parameters[0].name.value, "request");
}
#[test]
fn parses_inferred_and_explicit_function_return_types() {
let output = parse_text(
"fn inferred(value Decimal):\n value * 2\n\n\
fn explicit(value Decimal) Decimal:\n value * 2\n",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let declarations = output.program.unwrap().declarations;
let Declaration::Derive(inferred) = &declarations[0] else {
panic!("expected inferred function");
};
assert!(inferred.implicit_return_type);
assert_eq!(inferred.return_type.value, TypeRef::Unknown);
let Declaration::Derive(explicit) = &declarations[1] else {
panic!("expected explicitly typed function");
};
assert!(!explicit.implicit_return_type);
assert_eq!(explicit.return_type.value, TypeRef::Decimal);
}
#[test]
fn parses_named_case_function_branches() {
let output = parse_text(
"fn threshold(request Request):\n case:\n day when request.day: 39\n night when request.night\n & request.active\n : 34.00\n",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let Declaration::Derive(derive) = &output.program.unwrap().declarations[0] else {
panic!("expected function");
};
let DeriveBody::Case(case) = &derive.body else {
panic!("expected case body");
};
assert_eq!(case.branches.len(), 2);
assert_eq!(case.branches[0].name.value, "day");
assert_eq!(case.branches[1].name.value, "night");
assert!(matches!(
case.branches[1].condition.kind,
ExprKind::Binary {
operator: BinaryOp::And,
..
}
));
}
#[test]
fn parses_postfix_predicate_calls() {
let output = parse_text(
"fn accepted(state State) Bool:\n state = Allowed\n\n\
fn policy(request Request) Bool:\n request.status accepted & request.fallback policy.rejected\n",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let declarations = output.program.unwrap().declarations;
let Declaration::Derive(policy) = &declarations[1] else {
panic!("expected policy function");
};
let ExprKind::Binary { left, right, .. } =
&policy.expression().expect("expression body").kind
else {
panic!("expected conjunction");
};
for (expression, expected) in [
(left.as_ref(), "accepted"),
(right.as_ref(), "policy.rejected"),
] {
let ExprKind::Call {
callee,
arguments,
implicit_arguments: false,
postfix: true,
} = &expression.kind
else {
panic!("expected postfix predicate call");
};
assert_eq!(callee.value, expected);
assert_eq!(arguments.len(), 1);
}
}
#[test]
fn parses_fn_only_inferred_parameter_types_and_keeps_other_parameter_grammar() {
let output = parse_text(
"fn inferred(base, rate Decimal, domain.Request):\n base * rate\n\n\
rule unchanged(Left, Right):\n : result(Left, Right) = O\n",
);
let program = output.program.unwrap();
let Declaration::Derive(derive) = &program.declarations[0] else {
panic!("expected fn");
};
assert_eq!(derive.parameters.len(), 3);
assert!(derive.parameters[0].implicit_type);
assert!(derive.parameters[0].ty.value.is_empty());
assert_eq!(derive.parameters[0].name.value, "base");
assert!(!derive.parameters[1].implicit_type);
assert_eq!(derive.parameters[1].ty.value, "Decimal");
assert!(!derive.parameters[2].implicit_type);
assert_eq!(derive.parameters[2].ty.value, "domain.Request");
assert_eq!(
output
.diagnostics
.iter()
.filter(|diagnostic| diagnostic.code == "P0552")
.count(),
2
);
}
#[test]
fn distinguishes_dotted_calls_from_record_field_access() {
let output = parse_text(
"fn check(request domain.Request) Bool:\n policy.rules.allowed(request) & request.enabled\n",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let Declaration::Derive(derive) = &output.program.unwrap().declarations[0] else {
panic!("expected function");
};
let ExprKind::Binary { left, right, .. } =
&derive.expression().expect("expression body").kind
else {
panic!("expected conjunction");
};
assert!(matches!(
&left.kind,
ExprKind::Call { callee, .. } if callee.value == "policy.rules.allowed"
));
assert!(matches!(&right.kind, ExprKind::Field { field, .. } if field.value == "enabled"));
}
#[test]
fn parses_implicit_rule_bindings_and_lowers_comparison_chains_pairwise() {
let output = parse_text(
r"rule 구간무료:
10_000 < 주문.금액 <= 50_000: 배송정책 = 무료
",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let declarations = output.program.unwrap().declarations;
let Declaration::Rule(rule) = &declarations[0] else {
panic!("expected rule");
};
assert!(rule.implicit_parameters);
assert!(rule.parameters.is_empty());
let Effect::Decide {
arguments,
implicit_arguments,
..
} = &rule.effect
else {
panic!("expected decision effect");
};
assert!(*implicit_arguments);
assert!(arguments.is_empty());
let ExprKind::Binary {
left,
operator: BinaryOp::And,
right,
} = &rule.condition.kind
else {
panic!("expected a pairwise comparison conjunction");
};
let ExprKind::Binary {
right: first_middle,
operator: BinaryOp::Less,
..
} = &left.kind
else {
panic!("expected the first comparison");
};
let ExprKind::Binary {
left: second_middle,
operator: BinaryOp::LessEqual,
..
} = &right.kind
else {
panic!("expected the second comparison");
};
assert_eq!(first_middle.span, second_middle.span);
}
#[test]
fn distinguishes_implicit_and_explicit_expectation_arguments() {
let output = parse_text(
r"test 간결한기대:
주문 { 금액: 100_000 }
expect 배송정책 = 무료
expect 주문처리방법(주문) = 상담원확인
assert 전체주문(주문):
주문.금액 > 0: 배송정책 = 무료
",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let declarations = output.program.unwrap().declarations;
let Declaration::Case(case) = &declarations[0] else {
panic!("expected test");
};
assert!(case.expectations[0].implicit_arguments);
assert!(case.expectations[0].arguments.is_empty());
assert!(!case.expectations[1].implicit_arguments);
assert_eq!(case.expectations[1].arguments.len(), 1);
let Declaration::Invariant(invariant) = &declarations[1] else {
panic!("expected assertion");
};
let InvariantAssertion::Implication { expectation, .. } = &invariant.assertion else {
panic!("expected implication assertion");
};
assert!(expectation.implicit_arguments);
assert!(expectation.arguments.is_empty());
}
#[test]
fn rejects_removed_surface_separators() {
for source in [
"rule old:\n O => result = O\n",
"fn old: Bool:\n O\n",
"policy::source:\n old source\n",
] {
let output = parse_text(source);
assert!(!output.diagnostics.is_empty(), "{source}");
}
}
#[test]
fn rejects_explicit_empty_source_lists() {
for (source, code) in [
("fn old() Bool:\n O\n", "P0553"),
("rule old():\n : result = O\n", "P0553"),
("assert old():\n result\n", "P0553"),
("rule old:\n : result() = O\n", "P0554"),
("test old:\n expect result() = O\n", "P0554"),
] {
let output = parse_text(source);
assert!(
output
.diagnostics
.iter()
.any(|diagnostic| diagnostic.code == code),
"{source}: {:?}",
output.diagnostics
);
}
}
#[test]
fn rejects_multiple_type_only_parameters() {
let output = parse_text("rule ambiguous(Left, Right):\n : result(Left, Right) = O\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_source() {
let output = parse_text(
r"law.residence:
신청인은 국내에 주소를 두어야 한다.
# raw source, not a comment
rule residence(request Request):
request.domestic X: result(request) = X
law.date:
기준일은 신청일이다.
",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let declarations = output.program.unwrap().declarations;
let Declaration::Fragment(fragment) = &declarations[0] else {
panic!("expected fragment");
};
assert_eq!(fragment.id.value, "law.residence");
assert_eq!(fragment.locator.value, "law.residence");
assert!(fragment.text.value.contains("# raw source"));
assert!(fragment.refs.is_empty());
assert_eq!(fragment.rules[0].basis.len(), 1);
assert_eq!(fragment.rules[0].basis[0].id.value, "law.residence");
}
#[test]
fn parses_fragment_functions_with_implicit_sources() {
let output = parse_text(
r"law.formula:
The amount is seventy percent of the stated value.
fn inferred(request Request):
request.value
fn amount(request Request) Decimal:
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(), 2);
assert_eq!(fragment.derives[0].name.value, "inferred");
assert!(fragment.derives[0].implicit_return_type);
assert_eq!(fragment.derives[0].return_type.value, TypeRef::Unknown);
assert_eq!(fragment.derives[1].name.value, "amount");
assert!(!fragment.derives[1].implicit_return_type);
assert_eq!(fragment.derives[1].basis.len(), 1);
assert_eq!(fragment.derives[1].basis[0].id.value, "law.formula");
}
#[test]
fn parses_unconditional_and_override_rules() {
let output = parse_text(
r"policy.default:
Default policy.
rule base(request Request):
: result(request) = O
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 parses_one_candidate_with_multiple_atomic_overrides() {
let output = parse_text(
r"rule approved:
eligible:
shipping = Free
override remote
override fallback
",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let Declaration::Rule(rule) = &output.program.unwrap().declarations[0] else {
panic!("expected rule");
};
assert!(matches!(rule.effect, Effect::Decide { .. }));
assert_eq!(rule.overrides.len(), 2);
assert_eq!(rule.overrides[0].rule.value, "remote");
assert_eq!(rule.overrides[1].rule.value, "fallback");
}
#[test]
fn rejects_multiple_candidates_in_one_rule() {
let output = parse_text(
r"rule ambiguous:
eligible:
shipping = Free
grade = Gold
",
);
assert!(
output
.diagnostics
.iter()
.any(|diagnostic| diagnostic.code == "P0201")
);
}
#[test]
fn reports_a_rule_without_a_body() {
let output = parse_text(
r"policy.default:
Policy text.
rule empty:
",
);
assert!(
output
.diagnostics
.iter()
.any(|diagnostic| diagnostic.code == "L0013"),
"{:?}",
output.diagnostics
);
}
#[test]
fn uses_fragment_ids_as_source_labels() {
let output = parse_text(
"kr.article:\n 한국 법령.\n\n\
us.fair_use:\n United States law.\n\n\
jp.tort:\n 日本法.\n\n\
contract.term:\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,
["kr.article", "us.fair_use", "jp.tort", "contract.term",]
);
}
#[test]
fn parses_tests_and_assertions_with_defaults() {
let output = parse_text(
r"test basic:
shared.Request { domestic: O }
expect result(Request) = O
assert total(request Request):
result(request)
assert some exception(request Request):
request.special: result(request) != X
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_typed_partial_fixtures_and_binding_overrides() {
let output = parse_text(
r"fixture 기본요청 shared.Request:
domestic: O
test basic:
shared.Request from 기본요청 { urgent: O }
",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let declarations = output.program.unwrap().declarations;
let Declaration::Fixture(fixture) = &declarations[0] else {
panic!("expected fixture");
};
assert_eq!(fixture.name.value, "기본요청");
assert_eq!(fixture.entity.value, "shared.Request");
assert_eq!(fixture.fields.len(), 1);
let Declaration::Case(test) = &declarations[1] else {
panic!("expected test");
};
assert_eq!(
test.bindings[0]
.fixture
.as_ref()
.map(|name| name.value.as_str()),
Some("기본요청")
);
assert_eq!(test.bindings[0].fields.len(), 1);
}
#[test]
fn parses_same_depth_boolean_continuations_and_rule_effect_line() {
let output = parse_text(
r"fn eligible(request Request) Bool:
request.active
& request.verified
| request.override_allowed
rule allow(request Request):
request.active
& (request.verified | request.override_allowed)
: result(request) = O
",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let declarations = output.program.unwrap().declarations;
let Declaration::Derive(derive) = &declarations[0] else {
panic!("expected function");
};
assert!(matches!(
derive.expression().expect("expression body").kind,
ExprKind::Binary {
operator: BinaryOp::Or,
..
}
));
let Declaration::Rule(rule) = &declarations[1] else {
panic!("expected rule");
};
let ExprKind::Binary {
operator: BinaryOp::And,
right,
..
} = &rule.condition.kind
else {
panic!("expected outer and");
};
assert!(matches!(
right.kind,
ExprKind::Binary {
operator: BinaryOp::Or,
..
}
));
}
#[test]
fn parses_multiline_parenthesized_assertion_condition() {
let output = parse_text(
r"record Request:
enabled: Bool
approved: Bool
assert approved_request(request Request):
(request.enabled
& request.approved): result(request) = O
",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let Declaration::Invariant(assertion) = &output.program.unwrap().declarations[1] else {
panic!("expected assertion");
};
assert!(matches!(
assertion.assertion,
InvariantAssertion::Implication { .. }
));
}
#[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().expect("expression body").kind,
ExprKind::Binary {
operator: BinaryOp::Add,
..
}
));
}
#[test]
fn declaration_keywords_are_not_implicit_fragment_ids() {
for keyword in ["rule", "test", "assert"] {
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());
}
}