use crate::ast::{
ActionDecl, BinaryOp, BindingDecl, Cardinality, CaseDecl, CompareOp, DecisionDecl, Declaration,
DeriveDecl, DomainConstraint, Effect, EntityDecl, EnumDecl, Expectation, Expr, ExprKind,
FieldDecl, FieldValue, ImportDecl, InvariantAssertion, InvariantDecl, InvariantQuantifier,
Literal, NumericLiteral, Parameter, Program, RangeConstraint, RuleDecl, SourceDecl, SourceRef,
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>,
}
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 Some(_) = self.expect_keyword(Keyword::Module, "a Tess file must start with `module`")
else {
return ParseOutput {
program: None,
diagnostics: self.diagnostics,
};
};
let Some(module) = self.expect_identifier("expected a module name after `module`") else {
return ParseOutput {
program: None,
diagnostics: self.diagnostics,
};
};
self.finish_line("module declaration");
let mut imports = Vec::new();
self.skip_newlines();
while self.at_keyword(Keyword::Import) {
if let Some(import) = self.parse_import() {
imports.push(import);
}
self.skip_newlines();
}
let mut declarations = Vec::new();
while !self.at_simple(&TokenKind::Eof) {
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 a declaration with `enum`, `entity`, `state`, `derive`, `decision`, `action`, `transition`, `source`, `rule`, `case`, `invariant`, or `trace`",
);
self.synchronize_declaration();
}
self.skip_newlines();
}
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 `import`")?;
let span = start.merge(path.span);
self.finish_line("import declaration");
Some(ImportDecl { path, span })
}
fn parse_declaration(&mut self) -> Option<Declaration> {
if self.at_keyword(Keyword::Enum) {
self.parse_enum().map(Declaration::Enum)
} else if self.at_keyword(Keyword::Entity) {
self.parse_entity().map(Declaration::Entity)
} else if self.at_keyword(Keyword::State) {
self.parse_state().map(Declaration::State)
} else if self.at_keyword(Keyword::Derive) {
self.parse_derive().map(Declaration::Derive)
} else if self.at_keyword(Keyword::Decision) {
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_source_declaration_start() {
self.parse_source().map(Declaration::Source)
} else if self.at_keyword(Keyword::Rule) {
self.parse_rule().map(Declaration::Rule)
} else if self.at_keyword(Keyword::Case) {
self.parse_case().map(Declaration::Case)
} else if self.at_keyword(Keyword::Invariant) {
self.parse_invariant().map(Declaration::Invariant)
} else if self.at_keyword(Keyword::Trace) {
self.parse_trace().map(Declaration::Trace)
} else if self.at_removed_check_declaration_start() {
self.error_here(
"P0004",
"`check` declarations were renamed to `invariant`",
"replace `check NAME:` with `invariant NAME:`",
);
self.synchronize_declaration();
None
} else {
None
}
}
fn parse_enum(&mut self) -> Option<EnumDecl> {
let start = self.bump().span;
let name = self.expect_identifier("expected an enum name")?;
self.expect_simple(TokenKind::LeftBrace, "expected `{` after enum name")?;
self.skip_newlines();
let mut variants = Vec::new();
while !self.at_simple(&TokenKind::RightBrace) && !self.at_simple(&TokenKind::Eof) {
let Some(variant) = self.expect_identifier("expected an enum variant") else {
self.synchronize_block_item();
continue;
};
variants.push(variant);
if self.eat_simple(&TokenKind::Comma).is_none() {
self.skip_newlines();
if !self.at_simple(&TokenKind::RightBrace) {
self.error_here(
"P0101",
"enum variants must be separated by commas",
"add `,` after the preceding variant",
);
}
}
self.skip_newlines();
}
if variants.is_empty() {
self.diagnostics.push(
Diagnostic::error(
"P0103",
"an enum must declare at least one variant",
name.span,
)
.with_help("add a variant inside the enum block"),
);
}
let end = self
.expect_simple(TokenKind::RightBrace, "expected `}` to close enum")?
.span;
self.finish_line("enum declaration");
Some(EnumDecl {
name,
variants,
span: start.merge(end),
})
}
fn parse_entity(&mut self) -> Option<EntityDecl> {
let start = self.bump().span;
let name = self.expect_identifier("expected an entity name")?;
self.expect_simple(TokenKind::LeftBrace, "expected `{` after entity name")?;
self.skip_newlines();
let mut fields = Vec::new();
while !self.at_simple(&TokenKind::RightBrace) && !self.at_simple(&TokenKind::Eof) {
let before = self.cursor;
if let Some(field) = self.parse_field_decl() {
fields.push(field);
}
if self.cursor == before {
self.synchronize_block_item();
}
self.skip_newlines();
}
let end = self
.expect_simple(TokenKind::RightBrace, "expected `}` to close entity")?
.span;
self.finish_line("entity declaration");
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.expect_simple(TokenKind::LeftBrace, "expected `{` after state name")?;
self.skip_newlines();
let mut fields = Vec::new();
while !self.at_simple(&TokenKind::RightBrace) && !self.at_simple(&TokenKind::Eof) {
let before = self.cursor;
if let Some(field) = self.parse_field_decl() {
fields.push(field);
}
if self.cursor == before {
self.synchronize_block_item();
}
self.skip_newlines();
}
let end = self
.expect_simple(TokenKind::RightBrace, "expected `}` to close state")?
.span;
self.finish_line("state declaration");
Some(StateDecl {
name,
fields,
span: start.merge(end),
})
}
fn parse_field_decl(&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 mut optional = self.eat_keyword(Keyword::Optional).is_some()
|| self.eat_simple(&TokenKind::Question).is_some();
let mut range = None;
if self.eat_keyword(Keyword::Range).is_some() {
let (range_start, start_span) = self.parse_numeric_literal()?;
self.expect_simple(TokenKind::Range, "expected `..` in range constraint")?;
let (range_end, end_span) = self.parse_numeric_literal()?;
range = Some(RangeConstraint {
start: range_start,
end: range_end,
span: start_span.merge(end_span),
});
}
let mut domain = None;
if let Some(keyword) = self.eat_keyword(Keyword::Domain) {
self.expect_simple(TokenKind::LeftBrace, "expected `{` after `domain`")?;
self.skip_newlines();
let mut values = Vec::new();
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(
"P0103",
"domain values must be separated by commas",
"add `,` after the preceding value",
);
}
}
self.skip_newlines();
}
let end = self
.expect_simple(
TokenKind::RightBrace,
"expected `}` to close domain constraint",
)?
.span;
domain = Some(DomainConstraint {
values,
span: keyword.span.merge(end),
});
}
optional |= self.eat_keyword(Keyword::Optional).is_some()
|| self.eat_simple(&TokenKind::Question).is_some();
let end = self.previous_span();
if self.eat_simple(&TokenKind::Comma).is_none() {
self.skip_newlines();
if !self.at_simple(&TokenKind::RightBrace) {
self.error_here(
"P0102",
"entity fields must be separated by commas",
"add `,` after the preceding field",
);
}
}
Some(FieldDecl {
name,
ty: ty.value,
range,
domain,
optional,
span: start.merge(end),
})
}
fn parse_derive(&mut self) -> Option<DeriveDecl> {
let start = self.bump().span;
let name = self.expect_identifier("expected a derive name")?;
let parameters = self.parse_parameters()?;
self.expect_simple(TokenKind::Arrow, "expected `->` before derive return type")?;
let return_type = self.parse_type_ref()?;
self.expect_simple(TokenKind::Colon, "expected `:` before derive expression")?;
self.skip_newlines();
let expression = self.parse_expression()?;
let end = expression.span;
self.finish_line("derive declaration");
Some(DeriveDecl {
name,
parameters,
return_type,
expression,
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 has_cardinality_keyword = self.eat_keyword(Keyword::Cardinality).is_some();
let cardinality = if has_cardinality_keyword
|| self.at_keyword(Keyword::Exactly)
|| self.at_keyword(Keyword::Zero)
|| self.at_keyword(Keyword::Many)
{
self.parse_required_cardinality()?
} 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.expect_simple(TokenKind::Colon, "expected `:` after transition header")?;
self.require_line_break("transition header");
let mut condition = None;
let mut updates = Vec::new();
let mut end = action.span;
loop {
self.skip_newlines();
if self.at_declaration_start() || self.at_simple(&TokenKind::Eof) {
break;
}
if let Some(keyword) = self.eat_keyword(Keyword::When) {
let expression = self.parse_expression()?;
end = expression.span;
if condition.replace(expression).is_some() {
self.diagnostics.push(Diagnostic::error(
"P0250",
"a transition may declare only one `when` clause",
keyword.span,
));
}
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 {
span: keyword.span.merge(value.span),
receiver,
field,
value,
});
self.finish_line("state update");
} else {
self.error_here(
"P0251",
"expected a transition clause",
"transition clauses start with `when` or `set`",
);
self.synchronize_line();
}
}
let condition = condition.unwrap_or_else(|| {
self.diagnostics.push(Diagnostic::error(
"P0252",
"transition is missing a `when` clause",
name.span,
));
Expr::new(ExprKind::Literal(Literal::Bool(false)), name.span)
});
Some(TransitionDecl {
name,
parameters,
action,
action_arguments,
condition,
updates,
span: start.merge(end),
})
}
fn parse_source(&mut self) -> Option<SourceDecl> {
let start = self.bump().span;
let name = self.expect_identifier("expected a source name")?;
self.expect_simple(TokenKind::LeftBrace, "expected `{` after source name")?;
self.skip_newlines();
let mut title = None;
let mut section = None;
let mut version = None;
let mut uri = None;
while !self.at_simple(&TokenKind::RightBrace) && !self.at_simple(&TokenKind::Eof) {
let Some(field) = self.expect_identifier("expected a source metadata field") else {
self.synchronize_block_item();
continue;
};
if self
.expect_simple(TokenKind::Colon, "expected `:` after source metadata field")
.is_none()
{
self.synchronize_block_item();
continue;
}
let Some(value) = self.expect_string("expected a string source metadata value") else {
self.synchronize_block_item();
continue;
};
let slot = match field.value.as_str() {
"title" => Some(&mut title),
"section" => Some(&mut section),
"version" => Some(&mut version),
"uri" => Some(&mut uri),
_ => {
self.diagnostics.push(
Diagnostic::error(
"P0150",
format!("unknown source metadata field `{}`", field.value),
field.span,
)
.with_help("use `title`, `section`, `version`, or `uri`"),
);
None
}
};
if let Some(previous) = slot.and_then(|slot| slot.replace(value)) {
self.diagnostics.push(
Diagnostic::error(
"P0151",
format!("duplicate source metadata field `{}`", field.value),
field.span,
)
.with_label(previous.span, "first value is here")
.with_help("remove one of the duplicate fields"),
);
}
if self.eat_simple(&TokenKind::Comma).is_none() {
self.skip_newlines();
if !self.at_simple(&TokenKind::RightBrace) {
self.error_here(
"P0153",
"source metadata fields must be separated by commas",
"add `,` after the preceding field",
);
}
}
self.skip_newlines();
}
let end = self
.expect_simple(
TokenKind::RightBrace,
"expected `}` to close source metadata",
)?
.span;
self.finish_line("source declaration");
let title = title.unwrap_or_else(|| {
self.diagnostics.push(
Diagnostic::error(
"P0152",
format!("source `{}` is missing required field `title`", name.value),
name.span,
)
.with_help("add `title: \"...\"` inside the source block"),
);
Spanned::new(String::new(), name.span)
});
Some(SourceDecl {
name,
title,
section,
version,
uri,
span: start.merge(end),
})
}
fn parse_rule(&mut self) -> Option<RuleDecl> {
let start = self.bump().span;
let name = self.expect_identifier("expected a rule name")?;
if !self.at_simple(&TokenKind::LeftParen) {
self.error_here(
"P0201",
"rule parameters must be explicit",
"write the parameter list after the rule name, for example `(학생 s)`",
);
}
let parameters = self.parse_parameters()?;
self.expect_simple(TokenKind::Colon, "expected `:` after rule parameters")?;
self.require_line_break("rule header");
let mut condition = None;
let mut effects = Vec::new();
let mut sources = Vec::new();
let mut end = name.span;
loop {
self.skip_newlines();
if self.at_declaration_start() || self.at_simple(&TokenKind::Eof) {
break;
}
if let Some(keyword) = self.eat_keyword(Keyword::When) {
if condition.is_some() {
self.diagnostics.push(Diagnostic::error(
"P0202",
"a rule may contain only one `when` clause",
keyword.span,
));
}
if let Some(parsed) = self.parse_expression() {
end = parsed.span;
condition = Some(parsed);
}
self.finish_line("when clause");
} else if let Some(keyword) = self.eat_keyword(Keyword::Then) {
if self.at_keyword(Keyword::Override) {
self.bump();
if let Some(rule) =
self.expect_qualified_identifier("expected a rule name after `override`")
{
end = rule.span;
effects.push(Effect::Override {
span: keyword.span.merge(rule.span),
rule,
});
}
} else if self.at_keyword(Keyword::Continue) {
let unsupported = self.bump();
self.diagnostics.push(
Diagnostic::error(
"P0203",
"`then continue` is not supported",
keyword.span.merge(unsupported.span),
)
.with_help("Tess evaluates all applicable rules; remove this clause"),
);
end = unsupported.span;
} else if let Some(effect) = self.parse_decide_effect(keyword.span) {
end = effect_span(&effect);
effects.push(effect);
}
self.finish_line("then clause");
} else if let Some(keyword) = self.eat_keyword(Keyword::Source) {
let token = self.current().clone();
match token.kind {
TokenKind::Identifier(_) => {
if let Some(value) = self.expect_qualified_identifier(
"expected a source name or legacy string after `source`",
) {
end = value.span;
sources.push(SourceRef::Named(value));
}
}
TokenKind::String(value) => {
self.bump();
end = token.span;
sources.push(SourceRef::Inline(Spanned::new(value, token.span)));
}
_ => {
self.diagnostics.push(
Diagnostic::error(
"P0208",
"expected a source name or legacy string after `source`",
token.span,
)
.with_help(
"reference a declared source name, for example `source 정책_3_1`",
),
);
end = keyword.span;
}
}
self.finish_line("source clause");
} else {
self.error_here(
"P0206",
"expected a rule clause",
"rule clauses start with `when`, `then`, or `source`",
);
self.synchronize_line();
}
}
let condition = condition.unwrap_or_else(|| {
self.diagnostics.push(
Diagnostic::error("P0207", "rule is missing a `when` clause", name.span)
.with_help("add `when <condition>` to the rule body"),
);
Expr::new(ExprKind::Literal(Literal::Unknown), name.span)
});
if effects.is_empty() {
self.diagnostics.push(
Diagnostic::error("P0209", "rule has no effects", name.span)
.with_help("add a `then` decision or `then override` clause"),
);
}
Some(RuleDecl {
name,
parameters,
condition,
effects,
sources,
span: start.merge(end),
})
}
fn parse_decide_effect(&mut self, start: Span) -> Option<Effect> {
let decision = self.expect_qualified_identifier("expected a decision name after `then`")?;
let arguments = self.parse_arguments()?;
self.expect_simple(TokenKind::Equal, "expected `=` in decision effect")?;
let value = self.parse_expression()?;
let span = start.merge(value.span);
Some(Effect::Decide {
decision,
arguments,
value,
span,
})
}
fn parse_case(&mut self) -> Option<CaseDecl> {
let start = self.bump().span;
let name = self.expect_identifier("expected a case name")?;
self.expect_simple(TokenKind::Colon, "expected `:` after case name")?;
self.require_line_break("case header");
let mut bindings = Vec::new();
let mut expectations = Vec::new();
let mut end = name.span;
loop {
self.skip_newlines();
if self.at_declaration_start() || self.at_simple(&TokenKind::Eof) {
break;
}
if self.eat_keyword(Keyword::Let).is_some() {
if let Some(binding) = self.parse_binding() {
end = binding.span;
bindings.push(binding);
}
self.finish_line("let 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("expectation");
} else {
self.error_here(
"P0301",
"expected a case clause",
"case clauses start with `let` or `expect`",
);
self.synchronize_line();
}
}
Some(CaseDecl {
name,
bindings,
expectations,
span: start.merge(end),
})
}
fn parse_binding(&mut self) -> Option<BindingDecl> {
let name = self.expect_identifier("expected a binding name after `let`")?;
let start = name.span;
self.expect_simple(TokenKind::Equal, "expected `=` in let binding")?;
let entity = self.expect_qualified_identifier("expected an entity name in let binding")?;
self.expect_simple(TokenKind::LeftBrace, "expected `{` after entity name")?;
self.skip_newlines();
let mut fields = Vec::new();
while !self.at_simple(&TokenKind::RightBrace) && !self.at_simple(&TokenKind::Eof) {
let Some(field_name) = self.expect_identifier("expected a field name") else {
self.synchronize_block_item();
continue;
};
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(
"P0302",
"entity 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 entity 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(
"P0303",
"expected `=` or `!=` in expectation",
"compare the decision result with an expected value",
);
return None;
};
let value = self.parse_expression()?;
Some(Expectation {
decision,
arguments,
operator,
span: start.merge(value.span),
value,
})
}
fn parse_invariant(&mut self) -> Option<InvariantDecl> {
let start = self.bump().span;
let name = self.expect_identifier("expected an invariant name")?;
self.expect_simple(TokenKind::Colon, "expected `:` after invariant name")?;
self.require_line_break("invariant header");
self.skip_newlines();
self.expect_keyword(
Keyword::For,
"expected `for all` or `for some` in invariant",
)?;
let quantifier = if self.eat_keyword(Keyword::All).is_some() {
InvariantQuantifier::All
} else if self.eat_keyword(Keyword::Some).is_some() {
InvariantQuantifier::Some
} else {
self.error_here(
"P0400",
"expected `all` or `some` after `for`",
"use `for all` when every binding must satisfy the assertion, or `for some` when at least one binding must satisfy it",
);
self.synchronize_declaration();
return None;
};
let mut variables = Vec::new();
loop {
let variable_name = self.expect_identifier("expected a quantified variable name")?;
self.expect_simple(TokenKind::Colon, "expected `:` after quantified variable")?;
let variable_type =
self.expect_qualified_identifier("expected an entity type after `:`")?;
variables.push(Parameter {
span: variable_name.span.merge(variable_type.span),
ty: variable_type,
name: variable_name,
});
if self.eat_simple(&TokenKind::Comma).is_none() {
break;
}
}
self.finish_line("invariant quantifier");
self.skip_newlines();
let assertion = if self.at_keyword(Keyword::Cardinality)
|| self.at_keyword(Keyword::Exactly)
|| self.at_keyword(Keyword::Zero)
|| self.at_keyword(Keyword::Many)
{
let assertion_start = self.current().span;
let _ = self.eat_keyword(Keyword::Cardinality);
let cardinality = self.parse_required_cardinality()?;
let decision =
self.expect_qualified_identifier("expected a decision name after cardinality")?;
let arguments = self.parse_arguments()?;
let assertion_end = self.previous_span();
self.finish_line("invariant assertion");
InvariantAssertion::Cardinality {
cardinality,
decision,
arguments,
span: assertion_start.merge(assertion_end),
}
} else if let Some(if_keyword) = self.eat_keyword(Keyword::If) {
let condition = self.parse_expression()?;
self.finish_line("invariant condition");
self.skip_newlines();
let then =
self.expect_keyword(Keyword::Then, "expected `then` after invariant condition")?;
let expectation = self.parse_expectation(then.span)?;
let assertion_span = if_keyword.span.merge(expectation.span);
self.finish_line("invariant implication");
InvariantAssertion::Implication {
condition,
expectation,
span: assertion_span,
}
} else {
self.error_here(
"P0401",
"expected an invariant assertion",
"use `exactly one <decision>(...)` or an `if`/`then` implication",
);
self.synchronize_declaration();
return None;
};
let end = invariant_assertion_span(&assertion);
Some(InvariantDecl {
name,
quantifier,
variables,
assertion,
span: start.merge(end),
})
}
fn parse_trace(&mut self) -> Option<TraceDecl> {
let start = self.bump().span;
let name = self.expect_identifier("expected a trace name")?;
self.expect_simple(TokenKind::Colon, "expected `:` after trace name")?;
self.require_line_break("trace header");
self.skip_newlines();
self.expect_keyword(Keyword::For, "expected `for all` in trace")?;
self.expect_keyword(Keyword::All, "expected `all` after `for`")?;
let variable_name = self.expect_identifier("expected a state variable")?;
self.expect_simple(TokenKind::Colon, "expected `:` after state variable")?;
let variable_type = self.expect_qualified_identifier("expected a state type")?;
let variable = Parameter {
span: variable_name.span.merge(variable_type.span),
ty: variable_type,
name: variable_name,
};
self.finish_line("trace quantifier");
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;
loop {
self.skip_newlines();
if self.at_declaration_start() || self.at_simple(&TokenKind::Eof) {
break;
}
if let Some(keyword) = self.eat_keyword(Keyword::Initially) {
let expression = self.parse_expression()?;
end = expression.span;
if initial.replace(expression).is_some() {
self.diagnostics.push(Diagnostic::error(
"P0500",
"a trace may declare only one `initially` clause",
keyword.span,
));
}
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 invariant");
} 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.diagnostics.push(Diagnostic::error(
"P0501",
"a trace may declare only one termination condition",
keyword.span,
));
}
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.diagnostics.push(Diagnostic::error(
"P0502",
"duplicate `no dead ends` clause",
keyword.span,
));
}
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()?;
self.expect_keyword(Keyword::Steps, "expected `steps` after trace bound")?;
let NumericLiteral::Int(steps) = steps else {
self.diagnostics.push(Diagnostic::error(
"P0503",
"trace bound must be an integer",
steps_span,
));
continue;
};
let Ok(steps) = usize::try_from(steps) else {
self.diagnostics.push(Diagnostic::error(
"P0503",
"trace bound must be a non-negative integer",
steps_span,
));
continue;
};
if within.replace(Spanned::new(steps, steps_span)).is_some() {
self.diagnostics.push(Diagnostic::error(
"P0504",
"a trace may declare only one `within` clause",
keyword.span,
));
}
end = self.previous_span();
self.finish_line("trace bound");
} else {
self.error_here(
"P0505",
"expected a trace clause",
"use `initially`, `always`, `terminates when`, `no dead ends`, or `within N steps`",
);
self.synchronize_line();
}
}
let initial = initial.unwrap_or_else(|| {
self.diagnostics.push(Diagnostic::error(
"P0506",
"trace is missing an `initially` clause",
name.span,
));
Expr::new(ExprKind::Literal(Literal::Bool(false)), name.span)
});
let within = within.unwrap_or_else(|| {
self.diagnostics.push(Diagnostic::error(
"P0507",
"trace is missing a `within N steps` clause",
name.span,
));
Spanned::new(0, name.span)
});
Some(TraceDecl {
name,
variable,
initial,
always,
terminal,
no_dead_ends,
within,
span: start.merge(end),
})
}
fn parse_parameters(&mut self) -> Option<Vec<Parameter>> {
self.expect_simple(TokenKind::LeftParen, "expected `(` before parameter list")?;
let mut parameters = Vec::new();
self.skip_newlines();
while !self.at_simple(&TokenKind::RightParen) && !self.at_simple(&TokenKind::Eof) {
let ty = self.expect_qualified_identifier("expected a parameter type")?;
let name = self.expect_identifier("expected a parameter name after its type")?;
let span = ty.span.merge(name.span);
parameters.push(Parameter { ty, name, span });
if self.eat_simple(&TokenKind::Comma).is_none() {
self.skip_newlines();
if !self.at_simple(&TokenKind::RightParen) {
self.error_here(
"P0501",
"parameters must be separated by commas",
"add `,` after the preceding parameter",
);
}
}
self.skip_newlines();
}
self.expect_simple(TokenKind::RightParen, "expected `)` after parameter list")?;
Some(parameters)
}
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(
"P0502",
"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_required_cardinality(&mut self) -> Option<Cardinality> {
if self.eat_keyword(Keyword::Exactly).is_some() {
self.expect_keyword(Keyword::One, "expected `one` after `exactly`")?;
Some(Cardinality::ExactlyOne)
} else if self.eat_keyword(Keyword::Zero).is_some() {
self.expect_keyword(Keyword::Or, "expected `or` after `zero`")?;
self.expect_keyword(Keyword::One, "expected `one` after `zero or`")?;
Some(Cardinality::ZeroOrOne)
} else if self.eat_keyword(Keyword::Many).is_some() {
Some(Cardinality::Many)
} else {
self.error_here(
"P0503",
"expected a decision cardinality",
"use `exactly one`, `zero or one`, or `many`",
);
None
}
}
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 is_contextual_keyword(keyword) => {
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::Eof
| TokenKind::RightBrace
| TokenKind::RightParen
| TokenKind::Comma
) {
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(&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",
"range bounds must be literal numbers",
);
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 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_source_declaration_start(&self) -> bool {
self.at_keyword(Keyword::Source)
&& matches!(
(
self.tokens.get(self.cursor + 1).map(|token| &token.kind),
self.tokens.get(self.cursor + 2).map(|token| &token.kind),
),
(Some(TokenKind::Identifier(_)), Some(TokenKind::LeftBrace))
)
}
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 is_contextual_keyword(keyword) => {
keyword.as_str().to_owned()
}
_ => {
self.diagnostics.push(
Diagnostic::error("P0001", message, token.span)
.with_help("use a Unicode identifier that is not a reserved keyword"),
);
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_some() {
self.skip_newlines();
} else if !self.at_simple(&TokenKind::Eof) {
self.diagnostics.push(
Diagnostic::error(
"P0002",
format!("expected a newline after {context}"),
self.current().span,
)
.with_help("put the following clause on its own line"),
);
self.synchronize_line();
}
}
fn finish_line(&mut self, context: &str) {
if let Some(semicolon) = self.eat_simple(&TokenKind::Semicolon) {
self.diagnostics.push(
Diagnostic::error(
"P0004",
"semicolons are not valid statement terminators",
semicolon.span,
)
.with_help("remove the semicolon; Tess uses newlines between clauses"),
);
}
if self.eat_simple(&TokenKind::Newline).is_some() {
return;
}
if !self.at_simple(&TokenKind::Eof) && !self.at_simple(&TokenKind::RightBrace) {
self.diagnostics.push(
Diagnostic::error(
"P0002",
format!("unexpected token after {context}"),
self.current().span,
)
.with_help("start the next clause on a new line"),
);
self.synchronize_line();
}
}
fn error_here(&mut self, code: &str, message: &str, help: &str) {
self.diagnostics
.push(Diagnostic::error(code, message, self.current().span).with_help(help));
}
fn synchronize_line(&mut self) {
while !self.at_simple(&TokenKind::Newline) && !self.at_simple(&TokenKind::Eof) {
self.bump();
}
self.skip_newlines();
}
fn synchronize_block_item(&mut self) {
while !self.at_simple(&TokenKind::Comma)
&& !self.at_simple(&TokenKind::Newline)
&& !self.at_simple(&TokenKind::RightBrace)
&& !self.at_simple(&TokenKind::Eof)
{
self.bump();
}
let _ = self.eat_simple(&TokenKind::Comma);
self.skip_newlines();
}
fn synchronize_declaration(&mut self) {
self.synchronize_line();
while !self.at_declaration_start() && !self.at_simple(&TokenKind::Eof) {
self.synchronize_line();
}
}
fn at_declaration_start(&self) -> bool {
self.at_source_declaration_start()
|| self.at_removed_check_declaration_start()
|| matches!(
self.current().kind,
TokenKind::Keyword(
Keyword::Enum
| Keyword::Entity
| Keyword::State
| Keyword::Derive
| Keyword::Decision
| Keyword::Action
| Keyword::Transition
| Keyword::Rule
| Keyword::Case
| Keyword::Invariant
| Keyword::Trace
)
)
}
fn at_removed_check_declaration_start(&self) -> bool {
matches!(&self.current().kind, TokenKind::Identifier(name) if name == "check")
}
}
#[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, .. } => *span,
}
}
fn invariant_assertion_span(assertion: &InvariantAssertion) -> Span {
match assertion {
InvariantAssertion::Cardinality { span, .. }
| InvariantAssertion::Implication { 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)
)
}
const fn is_contextual_keyword(keyword: Keyword) -> bool {
matches!(
keyword,
Keyword::State
| Keyword::Action
| Keyword::Transition
| Keyword::On
| Keyword::Set
| Keyword::Trace
| Keyword::Initially
| Keyword::Always
| Keyword::Terminates
| Keyword::Within
| Keyword::Steps
| Keyword::No
| Keyword::Dead
| Keyword::Ends
)
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_text(text: &str) -> ParseOutput {
parse(SourceFile::new("test.tes", text))
}
#[test]
fn parses_enum_and_entity_with_ranges() {
let output = parse_text(
"module 성적\n\n\
enum 등급 {\n A,\n F,\n}\n\n\
entity 학생 {\n 점수: Int range 0..100,\n 승인: Bool optional,\n}\n",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let declarations = &output.program.unwrap().declarations;
assert_eq!(declarations.len(), 2);
let Declaration::Entity(entity) = &declarations[1] else {
panic!("expected entity");
};
assert_eq!(entity.fields.len(), 2);
assert!(entity.fields[1].optional);
}
#[test]
fn parses_finite_string_and_decimal_domains() {
let output = parse_text(
"module m\nentity E {\n status: String domain {\"draft\", \"approved\"},\n rate: Decimal domain {0.05, 0.10},\n}\n",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let program = output.program.unwrap();
let Declaration::Entity(entity) = &program.declarations[0] else {
panic!("expected entity");
};
assert_eq!(entity.fields[0].domain.as_ref().unwrap().values.len(), 2);
assert_eq!(entity.fields[1].domain.as_ref().unwrap().values.len(), 2);
}
#[test]
fn parses_state_actions_transitions_and_traces() {
let output = parse_text(
"module workflow\n\
enum Phase { Idle, Running }\n\
state Machine { phase: Phase, retries: Int range 0..2, enabled: Bool }\n\
action advance(Machine machine)\n\
transition begin(Machine machine) on advance(machine):\n\
when machine.enabled and machine.phase = Idle\n\
set machine.phase = Running\n\
set machine.retries = machine.retries + 1\n\
trace eventually_running:\n\
for all machine: Machine\n\
initially machine.phase = Idle\n\
always machine.retries >= 0\n\
terminates when machine.phase = Running\n\
no dead ends\n\
within 3 steps\n",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let declarations = &output.program.unwrap().declarations;
assert!(matches!(declarations[1], Declaration::State(_)));
assert!(matches!(declarations[2], Declaration::Action(_)));
let Declaration::Transition(transition) = &declarations[3] else {
panic!("expected transition");
};
assert_eq!(transition.updates.len(), 2);
let Declaration::Trace(trace) = &declarations[4] else {
panic!("expected trace");
};
assert_eq!(trace.always.len(), 1);
assert!(trace.terminal.is_some());
assert!(trace.no_dead_ends);
assert_eq!(trace.within.value, 3);
}
#[test]
fn parses_qualified_package_symbol_references() {
let output = parse_text(
"module app\n\
derive imported(common::Subject subject) -> Bool: common::enabled(subject)\n\
case package_case:\n\
let subject = common::Subject { enabled: true }\n\
expect common::outcome(subject) = Yes\n",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let declarations = &output.program.unwrap().declarations;
let Declaration::Derive(derive) = &declarations[0] else {
panic!("expected derive");
};
assert_eq!(derive.parameters[0].ty.value, "common::Subject");
let ExprKind::Call { callee, .. } = &derive.expression.kind else {
panic!("expected call");
};
assert_eq!(callee.value, "common::enabled");
let Declaration::Case(case) = &declarations[1] else {
panic!("expected case");
};
assert_eq!(case.bindings[0].entity.value, "common::Subject");
assert_eq!(case.expectations[0].decision.value, "common::outcome");
}
#[test]
fn state_syntax_words_remain_valid_identifiers_in_identifier_positions() {
let output = parse_text(
"module state\n\
entity action { no: Bool, within: Bool }\n\
derive always(action state) -> Bool: state.no and state.within\n\
decision trace(action transition) -> Bool\n\
rule no(action within):\n\
when always(within)\n\
then trace(within) = true\n",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let program = output.program.unwrap();
assert_eq!(program.module.value, "state");
let Declaration::Rule(rule) = &program.declarations[3] else {
panic!("expected rule");
};
assert_eq!(rule.name.value, "no");
assert_eq!(rule.parameters[0].name.value, "within");
}
#[test]
fn parses_and_normalizes_numeric_digit_separators() {
assert_eq!(
parse_numeric_literal("100_000"),
Some(NumericLiteral::Int(100_000))
);
assert_eq!(
parse_numeric_literal("-1_234.50_01"),
Some(NumericLiteral::Decimal("-1234.5001".into()))
);
for invalid in ["_100", "100_", "1__00", "1_.0", "1._0", ".5", "1."] {
assert_eq!(parse_numeric_literal(invalid), None, "{invalid}");
}
}
#[test]
fn invalid_digit_separator_recovers_without_a_range_error() {
let output = parse_text("module m\nderive n() -> Int: 1__000\n");
assert!(output.diagnostics.iter().any(|value| value.code == "L0005"));
assert!(!output.diagnostics.iter().any(|value| value.code == "P0601"));
}
#[test]
fn parses_imports_between_module_header_and_declarations() {
let output = parse_text(
"module 배송\n\
import \"model/order.tes\"\n\
import \"rules/shipping.tes\"\n\n\
enum 결과 { 무료, 유료, }\n",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let program = output.program.unwrap();
assert_eq!(program.imports.len(), 2);
assert_eq!(program.imports[0].path.value, "model/order.tes");
assert_eq!(program.imports[1].path.value, "rules/shipping.tes");
assert_eq!(program.declarations.len(), 1);
}
#[test]
fn import_after_a_declaration_is_rejected() {
let output = parse_text("module m\nenum E { A, }\nimport \"late.tes\"\n");
assert!(output.diagnostics.iter().any(|value| value.code == "P0003"));
}
#[test]
fn pratt_parser_respects_arithmetic_and_boolean_precedence() {
let output = parse_text(
"module m\n\
derive 계산(학생 s) -> Decimal: s.a + s.b * 0.3\n\
derive 판정(학생 s) -> Bool: s.a > 1 and s.b < 2 or false\n",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let program = output.program.unwrap();
let Declaration::Derive(first) = &program.declarations[0] else {
panic!("expected derive");
};
let ExprKind::Binary {
operator, right, ..
} = &first.expression.kind
else {
panic!("expected binary");
};
assert_eq!(*operator, BinaryOp::Add);
assert!(matches!(
right.kind,
ExprKind::Binary {
operator: BinaryOp::Multiply,
..
}
));
let Declaration::Derive(second) = &program.declarations[1] else {
panic!("expected derive");
};
assert!(matches!(
second.expression.kind,
ExprKind::Binary {
operator: BinaryOp::Or,
..
}
));
}
#[test]
fn parses_explicit_rule_parameters_and_effects() {
let output = parse_text(
"module m\n\
rule 출석미달(학생 s):\n\
when s.결석 >= 5\n\
then 최종등급(s) = F\n\
then override 일반등급\n\
source \"제4조\"\n",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let program = output.program.unwrap();
let Declaration::Rule(rule) = &program.declarations[0] else {
panic!("expected rule");
};
assert_eq!(rule.parameters[0].name.value, "s");
assert_eq!(rule.effects.len(), 2);
let SourceRef::Inline(source) = &rule.sources[0] else {
panic!("expected an inline source");
};
assert_eq!(source.value, "제4조");
}
#[test]
fn parses_structured_sources_and_named_rule_references() {
let output = parse_text(
"module m\n\
source 정책_3_1 {\n\
version: \"2026-07-01\",\n\
title: \"배송 운영 정책\",\n\
uri: \"https://example.test/policy\",\n\
section: \"3.1\",\n\
}\n\
rule 배송():\n\
when true\n\
then 결과() = true\n\
source 정책_3_1\n\
source \"legacy\"\n\
source 후속근거 { title: \"후속 근거\" }\n",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let declarations = output.program.unwrap().declarations;
let Declaration::Source(source) = &declarations[0] else {
panic!("expected a source declaration");
};
assert_eq!(source.title.value, "배송 운영 정책");
assert_eq!(source.section.as_ref().unwrap().value, "3.1");
assert_eq!(source.version.as_ref().unwrap().value, "2026-07-01");
assert_eq!(
source.uri.as_ref().unwrap().value,
"https://example.test/policy"
);
let Declaration::Rule(rule) = &declarations[1] else {
panic!("expected a rule");
};
assert!(matches!(&rule.sources[0], SourceRef::Named(value) if value.value == "정책_3_1"));
assert!(matches!(&rule.sources[1], SourceRef::Inline(value) if value.value == "legacy"));
assert!(matches!(declarations[2], Declaration::Source(_)));
}
#[test]
fn diagnoses_invalid_source_metadata() {
let output = parse_text(
"module m\n\
source 잘못된근거 {\n\
section: \"1\",\n\
section: \"2\",\n\
typo: \"x\",\n\
}\n",
);
let codes = output
.diagnostics
.iter()
.map(|diagnostic| diagnostic.code.as_str())
.collect::<Vec<_>>();
assert!(codes.contains(&"P0150"));
assert!(codes.contains(&"P0151"));
assert!(codes.contains(&"P0152"));
}
#[test]
fn rejects_implicit_rule_parameter_and_recovers() {
let output = parse_text(
"module m\nrule 잘못됨:\n when true\n then 결과() = true\ndecision 결과() -> Bool exactly one\n",
);
assert!(output.diagnostics.iter().any(|value| value.code == "P0201"));
let program = output.program.unwrap();
assert!(
program
.declarations
.iter()
.any(|declaration| matches!(declaration, Declaration::Decision(_)))
);
}
#[test]
fn defaults_to_exactly_one_and_parses_explicit_cardinalities() {
let output = parse_text(
"module m\n\
decision 기본() -> Bool\n\
decision 하나() -> Bool exactly one\n\
decision 선택() -> Bool zero or one\n\
decision 여럿() -> Bool many\n",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let program = output.program.unwrap();
let values: Vec<_> = program
.declarations
.iter()
.map(|value| match value {
Declaration::Decision(value) => value.cardinality,
_ => panic!("expected decision"),
})
.collect();
assert_eq!(
values,
vec![
Cardinality::ExactlyOne,
Cardinality::ExactlyOne,
Cardinality::ZeroOrOne,
Cardinality::Many
]
);
}
#[test]
fn parses_case_bindings_and_expectations() {
let output = parse_text(
"module m\n\
case 정상:\n\
let s = 학생 {\n 점수: 90,\n 승인: false,\n }\n\
expect 최종등급(s) = A\n",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let program = output.program.unwrap();
let Declaration::Case(case) = &program.declarations[0] else {
panic!("expected case");
};
assert_eq!(case.bindings[0].fields.len(), 2);
assert_eq!(case.expectations.len(), 1);
}
#[test]
fn parses_cardinality_invariant() {
let output = parse_text(
"module m\n\
invariant 유일함:\n\
for all s: 학생\n\
exactly one 최종등급(s)\n",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let program = output.program.unwrap();
let Declaration::Invariant(invariant) = &program.declarations[0] else {
panic!("expected invariant");
};
assert_eq!(invariant.quantifier, InvariantQuantifier::All);
assert!(matches!(
invariant.assertion,
InvariantAssertion::Cardinality { .. }
));
}
#[test]
fn parses_existential_invariant() {
let output = parse_text(
"module m\n\
invariant 예외가_있음:\n\
for some s: 학생\n\
if s.예외승인\n\
then 최종등급(s) = A\n",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let program = output.program.unwrap();
let Declaration::Invariant(invariant) = &program.declarations[0] else {
panic!("expected invariant");
};
assert_eq!(invariant.quantifier, InvariantQuantifier::Some);
assert!(matches!(
invariant.assertion,
InvariantAssertion::Implication { .. }
));
}
#[test]
fn diagnoses_missing_invariant_quantifier() {
let output = parse_text(
"module m\n\
invariant invalid:\n\
for each s: Subject\n\
exactly one result(s)\n\
enum Recovered { Yes }\n",
);
let diagnostic = output
.diagnostics
.iter()
.find(|diagnostic| diagnostic.code == "P0400")
.expect("quantifier diagnostic");
assert!(diagnostic.message.contains("`all` or `some`"));
assert!(
diagnostic
.help
.as_deref()
.is_some_and(|help| help.contains("at least one binding"))
);
assert!(output.program.is_some_and(|program| {
matches!(program.declarations.as_slice(), [Declaration::Enum(value)] if value.name.value == "Recovered")
}));
}
#[test]
fn keeps_trace_quantification_universal() {
let output = parse_text(
"module m\n\
state S { done: Bool }\n\
trace invalid:\n\
for some s: S\n\
initially not s.done\n\
within 1 steps\n",
);
assert!(output.diagnostics.iter().any(|diagnostic| {
diagnostic.code == "P0001" && diagnostic.message.contains("expected `all`")
}));
}
#[test]
fn parses_multiple_quantified_invariant_variables() {
let output = parse_text(
"module m\n\
invariant 유일함:\n\
for all o: 주문, c: 고객\n\
exactly one 배송정책(o, c)\n",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let program = output.program.unwrap();
let Declaration::Invariant(invariant) = &program.declarations[0] else {
panic!("expected invariant");
};
assert_eq!(invariant.variables.len(), 2);
assert_eq!(invariant.variables[0].name.value, "o");
assert_eq!(invariant.variables[0].ty.value, "주문");
assert_eq!(invariant.variables[1].name.value, "c");
assert_eq!(invariant.variables[1].ty.value, "고객");
}
#[test]
fn parses_implication_invariant() {
let output = parse_text(
"module m\n\
invariant 출석:\n\
for all s: 학생\n\
if s.결석 >= 5 and not s.승인\n\
then 최종등급(s) != A\n",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let program = output.program.unwrap();
let Declaration::Invariant(invariant) = &program.declarations[0] else {
panic!("expected invariant");
};
assert!(matches!(
invariant.assertion,
InvariantAssertion::Implication { .. }
));
}
#[test]
fn rejects_the_renamed_check_declaration_with_migration_help() {
let output = parse_text(
"module m\n\
check total:\n\
for all s: Subject\n\
exactly one result(s)\n",
);
let diagnostic = output
.diagnostics
.iter()
.find(|diagnostic| diagnostic.code == "P0004")
.expect("renamed declaration diagnostic");
assert!(diagnostic.message.contains("renamed to `invariant`"));
assert!(
diagnostic
.help
.as_deref()
.is_some_and(|help| help.contains("invariant NAME:"))
);
}
#[test]
fn reports_missing_commas_without_losing_next_declaration() {
let output = parse_text("module m\nenum E {\n A\n B,\n}\ndecision d() -> E exactly one\n");
assert!(output.diagnostics.iter().any(|value| value.code == "P0101"));
assert_eq!(output.program.unwrap().declarations.len(), 2);
}
#[test]
fn reports_unsupported_continue() {
let output = parse_text("module m\nrule r(학생 s):\n when true\n then continue\n");
assert!(output.diagnostics.iter().any(|value| value.code == "P0203"));
}
#[test]
fn rejects_removed_priority_clause() {
let output =
parse_text("module m\nrule r(X x):\n when true\n then d(x) = true\n priority 10\n");
assert!(output.diagnostics.iter().any(|value| value.code == "P0206"));
}
#[test]
fn missing_module_is_fatal_but_diagnostic_is_returned() {
let output = parse_text("enum E { A, }");
assert!(output.program.is_none());
assert!(!output.diagnostics.is_empty());
}
#[test]
fn accepts_derive_expression_on_the_next_line() {
let output = parse_text(
"module m\nderive 총점(학생 s) -> Decimal:\n s.시험 * 0.7 + s.과제 * 0.3\n",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
assert!(matches!(
output.program.unwrap().declarations[0],
Declaration::Derive(_)
));
}
#[test]
fn rejects_chained_comparison_and_semicolon() {
let output = parse_text("module m\nderive 잘못됨(X x) -> Bool: 0 < x.a < 10;\n");
assert!(output.diagnostics.iter().any(|value| value.code == "P0604"));
assert!(output.diagnostics.iter().any(|value| value.code == "P0004"));
}
#[test]
fn accepts_default_decision_cardinality_and_rule_without_priority() {
let output = parse_text(
"module m\ndecision d(X x) -> Bool\nrule r(X x):\n when true\n then d(x) = true\n",
);
assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics);
let program = output.program.unwrap();
let Declaration::Decision(decision) = &program.declarations[0] else {
panic!("expected decision");
};
assert_eq!(decision.cardinality, Cardinality::ExactlyOne);
}
}