use crate::ast::{
ActionDecl, BinaryOp, BindingDecl, CaseDecl, DecisionDecl, Declaration, DeriveDecl, Effect,
EntityDecl, EnumDecl, Expectation, Expr, ExprKind, FieldDecl, FragmentDecl, FragmentRef,
InvariantAssertion, InvariantDecl, Literal, NumericLiteral, Parameter, Program, RuleDecl,
StateDecl, TraceDecl, TransitionDecl, TypeRef, UnaryOp,
};
use crate::diagnostic::{Diagnostic, Severity};
use crate::parser::ParseOutput;
use crate::source::{SourceFile, Span, Spanned};
use crate::value::{TruthValue, Value, parse_decimal};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use unicode_normalization::UnicodeNormalization;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(tag = "kind", content = "name", rename_all = "snake_case")]
pub enum ValueType {
Bool,
Int,
Decimal,
String,
Date,
Duration,
Enum(String),
Entity(String),
State(String),
Unknown,
Error,
}
impl ValueType {
#[must_use]
pub const fn is_numeric(&self) -> bool {
matches!(self, Self::Int | Self::Decimal)
}
#[must_use]
pub const fn is_error_or_unknown(&self) -> bool {
matches!(self, Self::Error | Self::Unknown)
}
#[must_use]
pub fn accepts(&self, actual: &Self) -> bool {
self == actual
|| matches!((self, actual), (Self::Decimal, Self::Int))
|| self.is_error_or_unknown()
|| actual.is_error_or_unknown()
}
#[must_use]
pub fn display_name(&self) -> &str {
match self {
Self::Bool => "Bool",
Self::Int => "Int",
Self::Decimal => "Decimal",
Self::String => "String",
Self::Date => "Date",
Self::Duration => "Duration",
Self::Enum(name) | Self::Entity(name) | Self::State(name) => name,
Self::Unknown => "unknown",
Self::Error => "<error>",
}
}
}
#[derive(Clone, Debug)]
pub struct CompiledProgram {
pub source: SourceFile,
pub ast: Program,
pub enums: BTreeMap<String, EnumDecl>,
pub entities: BTreeMap<String, EntityDecl>,
pub states: BTreeMap<String, StateDecl>,
pub derives: BTreeMap<String, DeriveDecl>,
pub decisions: BTreeMap<String, DecisionDecl>,
pub actions: BTreeMap<String, ActionDecl>,
pub transitions: BTreeMap<String, TransitionDecl>,
pub fragments: BTreeMap<String, FragmentDecl>,
pub rules: BTreeMap<String, RuleDecl>,
pub cases: BTreeMap<String, CaseDecl>,
pub invariants: BTreeMap<String, InvariantDecl>,
pub traces: BTreeMap<String, TraceDecl>,
expression_types: BTreeMap<(usize, usize), ValueType>,
}
impl CompiledProgram {
#[must_use]
pub const fn source(&self) -> &SourceFile {
&self.source
}
#[must_use]
pub const fn ast(&self) -> &Program {
&self.ast
}
#[must_use]
pub const fn enums(&self) -> &BTreeMap<String, EnumDecl> {
&self.enums
}
#[must_use]
pub const fn entities(&self) -> &BTreeMap<String, EntityDecl> {
&self.entities
}
#[must_use]
pub const fn states(&self) -> &BTreeMap<String, StateDecl> {
&self.states
}
#[must_use]
pub const fn derives(&self) -> &BTreeMap<String, DeriveDecl> {
&self.derives
}
#[must_use]
pub const fn decisions(&self) -> &BTreeMap<String, DecisionDecl> {
&self.decisions
}
#[must_use]
pub const fn actions(&self) -> &BTreeMap<String, ActionDecl> {
&self.actions
}
#[must_use]
pub const fn transitions(&self) -> &BTreeMap<String, TransitionDecl> {
&self.transitions
}
#[must_use]
pub const fn fragments(&self) -> &BTreeMap<String, FragmentDecl> {
&self.fragments
}
#[must_use]
pub const fn rules(&self) -> &BTreeMap<String, RuleDecl> {
&self.rules
}
#[must_use]
pub const fn cases(&self) -> &BTreeMap<String, CaseDecl> {
&self.cases
}
#[must_use]
pub const fn invariants(&self) -> &BTreeMap<String, InvariantDecl> {
&self.invariants
}
#[must_use]
pub const fn traces(&self) -> &BTreeMap<String, TraceDecl> {
&self.traces
}
#[must_use]
pub fn enum_decl(&self, name: &str) -> Option<&EnumDecl> {
self.enums.get(&normalize_name(name))
}
#[must_use]
pub fn entity(&self, name: &str) -> Option<&EntityDecl> {
self.entities.get(&normalize_name(name))
}
#[must_use]
pub fn state(&self, name: &str) -> Option<&StateDecl> {
self.states.get(&normalize_name(name))
}
#[must_use]
pub fn derive(&self, name: &str) -> Option<&DeriveDecl> {
self.derives.get(&normalize_name(name))
}
#[must_use]
pub fn decision(&self, name: &str) -> Option<&DecisionDecl> {
self.decisions.get(&normalize_name(name))
}
#[must_use]
pub fn action(&self, name: &str) -> Option<&ActionDecl> {
self.actions.get(&normalize_name(name))
}
#[must_use]
pub fn transition(&self, name: &str) -> Option<&TransitionDecl> {
self.transitions.get(&normalize_name(name))
}
#[must_use]
pub fn fragment(&self, id: &str) -> Option<&FragmentDecl> {
self.fragments.get(&normalize_name(id))
}
#[must_use]
pub fn render_basis(&self, basis: &[FragmentRef]) -> Option<String> {
let rendered = basis
.iter()
.filter_map(|reference| {
self.fragment(&reference.id.value)
.map(|fragment| fragment.text.value.trim_end_matches('\n').to_owned())
})
.collect::<Vec<_>>();
(!rendered.is_empty()).then(|| rendered.join("\n\n"))
}
#[must_use]
pub fn rule(&self, name: &str) -> Option<&RuleDecl> {
self.rules.get(&normalize_name(name))
}
#[must_use]
pub fn case(&self, name: &str) -> Option<&CaseDecl> {
self.cases.get(&normalize_name(name))
}
#[must_use]
pub fn invariant(&self, name: &str) -> Option<&InvariantDecl> {
self.invariants.get(&normalize_name(name))
}
#[must_use]
pub fn trace(&self, name: &str) -> Option<&TraceDecl> {
self.traces.get(&normalize_name(name))
}
#[must_use]
pub fn type_of_span(&self, span: Span) -> Option<&ValueType> {
self.expression_types.get(&(span.start, span.end))
}
#[must_use]
pub fn field(&self, entity: &str, field: &str) -> Option<&FieldDecl> {
let field = normalize_name(field);
self.entity(entity)?
.fields
.iter()
.find(|candidate| normalize_name(&candidate.name.value) == field)
}
#[must_use]
pub fn state_field(&self, state: &str, field: &str) -> Option<&FieldDecl> {
let field = normalize_name(field);
self.state(state)?
.fields
.iter()
.find(|candidate| normalize_name(&candidate.name.value) == field)
}
#[must_use]
pub fn enum_has_variant(&self, enum_name: &str, variant: &str) -> bool {
let variant = normalize_name(variant);
self.enum_decl(enum_name).is_some_and(|declaration| {
declaration
.variants
.iter()
.any(|candidate| normalize_name(&candidate.value) == variant)
})
}
}
#[derive(Clone, Debug)]
pub struct CompileOutput {
pub program: Option<CompiledProgram>,
pub diagnostics: Vec<Diagnostic>,
}
impl CompileOutput {
#[must_use]
pub fn has_errors(&self) -> bool {
self.diagnostics
.iter()
.any(|diagnostic| diagnostic.severity == Severity::Error)
}
}
#[must_use]
pub fn compile(parsed: ParseOutput) -> CompileOutput {
let ParseOutput {
program,
mut diagnostics,
} = parsed;
let Some(ast) = program else {
sort_diagnostics(&mut diagnostics);
return CompileOutput {
program: None,
diagnostics,
};
};
if !ast.imports_resolved {
diagnostics.extend(ast.imports.iter().map(|import| {
Diagnostic::error(
"M0006",
format!(
"use path `{}` requires a project-aware loader",
import.path.value
),
import.path.span,
)
.with_help(
"compile an entry `.tes` file with `compile_project`, or pass a project directory to the Tess CLI",
)
}));
}
let mut compiler = Compiler::new(&ast);
compiler.run();
let parts = compiler.finish();
diagnostics.extend(parts.diagnostics);
sort_diagnostics(&mut diagnostics);
let has_errors = diagnostics
.iter()
.any(|diagnostic| diagnostic.severity == Severity::Error);
let program = (!has_errors).then(|| CompiledProgram {
source: ast.source.clone(),
ast,
enums: parts.enums,
entities: parts.entities,
states: parts.states,
derives: parts.derives,
decisions: parts.decisions,
actions: parts.actions,
transitions: parts.transitions,
fragments: parts.fragments,
rules: parts.rules,
cases: parts.cases,
invariants: parts.invariants,
traces: parts.traces,
expression_types: parts.expression_types,
});
CompileOutput {
program,
diagnostics,
}
}
fn sort_diagnostics(diagnostics: &mut [Diagnostic]) {
diagnostics.sort_by(|left, right| {
(
left.primary.start,
left.primary.end,
severity_rank(left.severity),
left.code.as_str(),
left.message.as_str(),
)
.cmp(&(
right.primary.start,
right.primary.end,
severity_rank(right.severity),
right.code.as_str(),
right.message.as_str(),
))
});
}
const fn severity_rank(severity: Severity) -> u8 {
match severity {
Severity::Error => 0,
Severity::Warning => 1,
}
}
#[must_use]
pub fn normalize_name(name: &str) -> String {
name.nfc().collect()
}
#[must_use]
pub fn display_symbol_name(name: &str) -> String {
name.strip_prefix("@pkg_")
.map_or_else(|| name.to_owned(), str::to_owned)
}
fn qualify_fragment_rule(fragment_id: &str, mut rule: RuleDecl) -> RuleDecl {
rule.name.value = format!("{fragment_id}::{}", rule.name.value);
if let Effect::Override { rule: target, .. } = &mut rule.effect {
if !target.value.contains("::") {
target.value = format!("{fragment_id}::{}", target.value);
}
}
rule
}
fn qualify_fragment_derive(
fragment_id: &str,
local_derives: &BTreeSet<String>,
mut derive: DeriveDecl,
) -> DeriveDecl {
derive.name.value = format!("{fragment_id}::{}", derive.name.value);
qualify_fragment_derive_calls(&mut derive.expression, fragment_id, local_derives);
derive
}
fn qualify_fragment_rule_calls(
fragment_id: &str,
local_derives: &BTreeSet<String>,
rule: &mut RuleDecl,
) {
qualify_fragment_derive_calls(&mut rule.condition, fragment_id, local_derives);
if let Effect::Decide {
arguments, value, ..
} = &mut rule.effect
{
for argument in arguments {
qualify_fragment_derive_calls(argument, fragment_id, local_derives);
}
qualify_fragment_derive_calls(value, fragment_id, local_derives);
}
}
fn qualify_fragment_derive_calls(
expression: &mut Expr,
fragment_id: &str,
local_derives: &BTreeSet<String>,
) {
match &mut expression.kind {
ExprKind::Literal(_) | ExprKind::Name(_) => {}
ExprKind::Field { receiver, .. } => {
qualify_fragment_derive_calls(receiver, fragment_id, local_derives);
}
ExprKind::Call { callee, arguments } => {
if !callee.value.contains("::")
&& local_derives.contains(&normalize_name(&callee.value))
{
callee.value = format!("{fragment_id}::{}", callee.value);
}
for argument in arguments {
qualify_fragment_derive_calls(argument, fragment_id, local_derives);
}
}
ExprKind::Unary { operand, .. } => {
qualify_fragment_derive_calls(operand, fragment_id, local_derives);
}
ExprKind::Binary { left, right, .. } => {
qualify_fragment_derive_calls(left, fragment_id, local_derives);
qualify_fragment_derive_calls(right, fragment_id, local_derives);
}
}
}
#[derive(Default)]
struct CompileParts {
diagnostics: Vec<Diagnostic>,
enums: BTreeMap<String, EnumDecl>,
entities: BTreeMap<String, EntityDecl>,
states: BTreeMap<String, StateDecl>,
derives: BTreeMap<String, DeriveDecl>,
decisions: BTreeMap<String, DecisionDecl>,
actions: BTreeMap<String, ActionDecl>,
transitions: BTreeMap<String, TransitionDecl>,
fragments: BTreeMap<String, FragmentDecl>,
rules: BTreeMap<String, RuleDecl>,
cases: BTreeMap<String, CaseDecl>,
invariants: BTreeMap<String, InvariantDecl>,
traces: BTreeMap<String, TraceDecl>,
expression_types: BTreeMap<(usize, usize), ValueType>,
}
struct Compiler<'program> {
ast: &'program Program,
parts: CompileParts,
type_names: BTreeMap<String, TypeKind>,
callable_names: BTreeMap<String, CallableKind>,
variant_owners: BTreeMap<String, Vec<String>>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum TypeKind {
Enum,
Entity,
State,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum CallableKind {
Derive,
Decision,
}
impl<'program> Compiler<'program> {
fn new(ast: &'program Program) -> Self {
Self {
ast,
parts: CompileParts::default(),
type_names: BTreeMap::new(),
callable_names: BTreeMap::new(),
variant_owners: BTreeMap::new(),
}
}
fn run(&mut self) {
self.collect_declarations();
self.propagate_derive_basis_to_rules();
self.collect_enum_variants();
self.validate_enums();
self.validate_entities();
self.validate_states();
self.validate_derives();
self.validate_decisions();
self.validate_actions();
self.validate_transitions();
self.validate_fragments();
self.validate_rules();
self.validate_cases();
self.validate_invariants();
self.validate_traces();
self.validate_derive_cycles();
self.validate_override_cycles();
self.validate_static_rule_interactions();
}
fn finish(self) -> CompileParts {
self.parts
}
#[allow(clippy::too_many_lines)]
fn collect_declarations(&mut self) {
let mut type_spans = BTreeMap::<String, Span>::new();
let mut callable_spans = BTreeMap::<String, Span>::new();
let declarations = self.ast.declarations.clone();
for declaration in declarations {
match declaration {
Declaration::Enum(value) => {
let key = normalize_name(&value.name.value);
if self.insert_namespace_name(&mut type_spans, &key, value.name.span, "type") {
self.type_names.insert(key.clone(), TypeKind::Enum);
insert_declaration(
&mut self.parts.enums,
key,
value,
&mut self.parts.diagnostics,
"enum",
);
}
}
Declaration::Entity(value) => {
let key = normalize_name(&value.name.value);
if self.insert_namespace_name(&mut type_spans, &key, value.name.span, "type") {
self.type_names.insert(key.clone(), TypeKind::Entity);
insert_declaration(
&mut self.parts.entities,
key,
value,
&mut self.parts.diagnostics,
"record",
);
}
}
Declaration::State(value) => {
let key = normalize_name(&value.name.value);
if self.insert_namespace_name(&mut type_spans, &key, value.name.span, "type") {
self.type_names.insert(key.clone(), TypeKind::State);
insert_declaration(
&mut self.parts.states,
key,
value,
&mut self.parts.diagnostics,
"state",
);
}
}
Declaration::Derive(value) => {
let key = normalize_name(&value.name.value);
if self.insert_namespace_name(
&mut callable_spans,
&key,
value.name.span,
"callable",
) {
self.callable_names
.insert(key.clone(), CallableKind::Derive);
insert_declaration(
&mut self.parts.derives,
key,
value,
&mut self.parts.diagnostics,
"fn",
);
}
}
Declaration::Decision(value) => {
let key = normalize_name(&value.name.value);
if self.insert_namespace_name(
&mut callable_spans,
&key,
value.name.span,
"callable",
) {
self.callable_names
.insert(key.clone(), CallableKind::Decision);
insert_declaration(
&mut self.parts.decisions,
key,
value,
&mut self.parts.diagnostics,
"dec",
);
}
}
Declaration::Action(value) => {
let key = normalize_name(&value.name.value);
insert_declaration(
&mut self.parts.actions,
key,
value,
&mut self.parts.diagnostics,
"action",
);
}
Declaration::Transition(value) => {
let key = normalize_name(&value.name.value);
insert_declaration(
&mut self.parts.transitions,
key,
value,
&mut self.parts.diagnostics,
"transition",
);
}
Declaration::Fragment(value) => {
let key = normalize_name(&value.id.value);
let local_derives = value
.derives
.iter()
.map(|derive| normalize_name(&derive.name.value))
.collect::<BTreeSet<_>>();
let derives = value
.derives
.iter()
.cloned()
.map(|derive| {
qualify_fragment_derive(&value.id.value, &local_derives, derive)
})
.collect::<Vec<_>>();
let rules = value
.rules
.iter()
.cloned()
.map(|rule| {
let mut rule = qualify_fragment_rule(&value.id.value, rule);
qualify_fragment_rule_calls(&value.id.value, &local_derives, &mut rule);
rule
})
.collect::<Vec<_>>();
insert_declaration(
&mut self.parts.fragments,
key,
value,
&mut self.parts.diagnostics,
"fragment",
);
for derive in derives {
let key = normalize_name(&derive.name.value);
if self.insert_namespace_name(
&mut callable_spans,
&key,
derive.name.span,
"callable",
) {
self.callable_names
.insert(key.clone(), CallableKind::Derive);
insert_declaration(
&mut self.parts.derives,
key,
derive,
&mut self.parts.diagnostics,
"fn",
);
}
}
for rule in rules {
let key = normalize_name(&rule.name.value);
insert_declaration(
&mut self.parts.rules,
key,
rule,
&mut self.parts.diagnostics,
"rule",
);
}
}
Declaration::Rule(value) => {
let key = normalize_name(&value.name.value);
insert_declaration(
&mut self.parts.rules,
key,
value,
&mut self.parts.diagnostics,
"rule",
);
}
Declaration::Case(value) => {
let key = normalize_name(&value.name.value);
insert_declaration(
&mut self.parts.cases,
key,
value,
&mut self.parts.diagnostics,
"test",
);
}
Declaration::Invariant(value) => {
let key = normalize_name(&value.name.value);
insert_declaration(
&mut self.parts.invariants,
key,
value,
&mut self.parts.diagnostics,
"assertion",
);
}
Declaration::Trace(value) => {
let key = normalize_name(&value.name.value);
insert_declaration(
&mut self.parts.traces,
key,
value,
&mut self.parts.diagnostics,
"trace",
);
}
}
}
}
fn insert_namespace_name(
&mut self,
namespace: &mut BTreeMap<String, Span>,
key: &str,
span: Span,
namespace_name: &str,
) -> bool {
if let Some(previous) = namespace.get(key) {
self.parts.diagnostics.push(
Diagnostic::error(
"C0001",
format!("duplicate name `{key}` in the {namespace_name} namespace"),
span,
)
.with_label(*previous, "first declaration is here")
.with_help(
"rename one of the declarations; names are compared after NFC normalization",
),
);
false
} else {
namespace.insert(key.to_owned(), span);
true
}
}
fn propagate_derive_basis_to_rules(&mut self) {
let effective_basis = self
.parts
.derives
.keys()
.map(|name| {
let mut visited = BTreeSet::new();
let mut seen_basis = BTreeSet::new();
let mut basis = Vec::new();
collect_derive_basis(
name,
&self.parts.derives,
&self.parts.fragments,
&mut visited,
&mut seen_basis,
&mut basis,
);
(name.clone(), basis)
})
.collect::<BTreeMap<_, _>>();
for rule in self.parts.rules.values_mut() {
let mut seen_basis = rule
.basis
.iter()
.map(|basis| normalize_name(&basis.id.value))
.collect::<BTreeSet<_>>();
let mut calls = BTreeSet::new();
collect_calls(&rule.condition, &mut calls);
if let Effect::Decide {
arguments, value, ..
} = &rule.effect
{
for argument in arguments {
collect_calls(argument, &mut calls);
}
collect_calls(value, &mut calls);
}
for call in calls {
let Some(basis) = effective_basis.get(&call) else {
continue;
};
for reference in basis {
if seen_basis.insert(normalize_name(&reference.id.value)) {
rule.basis.push(reference.clone());
}
}
}
}
}
fn collect_enum_variants(&mut self) {
for (enum_name, declaration) in &self.parts.enums {
for variant in &declaration.variants {
self.variant_owners
.entry(normalize_name(&variant.value))
.or_default()
.push(enum_name.clone());
}
}
for owners in self.variant_owners.values_mut() {
owners.sort();
owners.dedup();
}
}
fn validate_enums(&mut self) {
let declarations = self.parts.enums.values().cloned().collect::<Vec<_>>();
for declaration in declarations {
if declaration.variants.is_empty() {
self.parts.diagnostics.push(Diagnostic::error(
"C0100",
format!("enum `{}` has no variants", declaration.name.value),
declaration.span,
));
}
let mut variants = BTreeMap::<String, Span>::new();
for variant in declaration.variants {
let key = normalize_name(&variant.value);
if let Some(previous) = variants.insert(key.clone(), variant.span) {
self.parts.diagnostics.push(
Diagnostic::error(
"C0101",
format!("duplicate enum variant `{key}`"),
variant.span,
)
.with_label(previous, "first variant is here"),
);
}
}
}
}
fn validate_entities(&mut self) {
let declarations = self.parts.entities.values().cloned().collect::<Vec<_>>();
for declaration in declarations {
let mut fields = BTreeMap::<String, Span>::new();
for field in declaration.fields {
let key = normalize_name(&field.name.value);
if let Some(previous) = fields.insert(key.clone(), field.name.span) {
self.parts.diagnostics.push(
Diagnostic::error(
"C0200",
format!("duplicate field `{key}`"),
field.name.span,
)
.with_label(previous, "first field is here"),
);
}
let field_type = self.resolve_type_ref(&field.ty, field.span);
if matches!(field_type, ValueType::Entity(_) | ValueType::State(_)) {
self.parts.diagnostics.push(
Diagnostic::error(
"C0204",
"record- and state-valued fields are not supported",
field.span,
)
.with_help("store a scalar identifier or flatten the nested record fields"),
);
}
if let Some(range) = &field.range {
self.validate_range(range, &field_type);
}
if field.domain.is_some() {
self.validate_domain(&field, &field_type);
}
}
}
}
fn validate_states(&mut self) {
let declarations = self.parts.states.values().cloned().collect::<Vec<_>>();
for declaration in declarations {
let mut fields = BTreeMap::<String, Span>::new();
for field in declaration.fields {
let key = normalize_name(&field.name.value);
if let Some(previous) = fields.insert(key.clone(), field.name.span) {
self.parts.diagnostics.push(
Diagnostic::error(
"C1000",
format!("duplicate state field `{key}`"),
field.name.span,
)
.with_label(previous, "first field is here"),
);
}
let field_type = self.resolve_type_ref(&field.ty, field.span);
let finite = match &field_type {
ValueType::Bool | ValueType::Enum(_) | ValueType::Error => true,
ValueType::Int => field.range.is_some(),
_ => false,
};
if !finite {
self.parts.diagnostics.push(
Diagnostic::error(
"C1001",
"state fields must be Bool, enum, or finite ranged Int",
field.span,
)
.with_help("keep mutable state finite for bounded trace analysis"),
);
}
if field.optional {
self.parts.diagnostics.push(Diagnostic::error(
"C1002",
"state fields cannot be optional",
field.span,
));
}
if field.domain.is_some() {
self.parts.diagnostics.push(Diagnostic::error(
"C1003",
"explicit String/Decimal domains are not state fields",
field.span,
));
}
if let Some(range) = &field.range {
self.validate_range(range, &field_type);
}
}
}
}
fn validate_actions(&mut self) {
let declarations = self.parts.actions.values().cloned().collect::<Vec<_>>();
for declaration in declarations {
let scope = self.parameter_scope(&declaration.parameters);
if declaration.parameters.len() != 1 {
self.parts.diagnostics.push(Diagnostic::error(
"C1010",
"an action must receive exactly one state parameter",
declaration.span,
));
continue;
}
let parameter = &declaration.parameters[0];
if !matches!(
scope.get(&normalize_name(¶meter.name.value)),
Some(ValueType::State(_) | ValueType::Error)
) {
self.parts.diagnostics.push(Diagnostic::error(
"C1011",
"an action parameter must have a state type",
parameter.span,
));
}
}
}
fn validate_transitions(&mut self) {
let declarations = self.parts.transitions.values().cloned().collect::<Vec<_>>();
for declaration in declarations {
let scope = self.parameter_scope(&declaration.parameters);
if declaration.parameters.len() != 1 {
self.parts.diagnostics.push(Diagnostic::error(
"C1020",
"a transition must receive exactly one state parameter",
declaration.span,
));
continue;
}
let parameter = &declaration.parameters[0];
let state_name = match scope.get(&normalize_name(¶meter.name.value)) {
Some(ValueType::State(name)) => Some(name.clone()),
Some(ValueType::Error) => None,
_ => {
self.parts.diagnostics.push(Diagnostic::error(
"C1021",
"a transition parameter must have a state type",
parameter.span,
));
None
}
};
let action_key = normalize_name(&declaration.action.value);
if let Some(action) = self.parts.actions.get(&action_key).cloned() {
let expected_types = self.parameter_types(&action.parameters);
let actual_types = declaration
.action_arguments
.iter()
.map(|argument| self.type_expression(argument, &scope))
.collect::<Vec<_>>();
if expected_types != actual_types
&& !expected_types.iter().any(ValueType::is_error_or_unknown)
&& !actual_types.iter().any(ValueType::is_error_or_unknown)
{
self.parts.diagnostics.push(
Diagnostic::error(
"C1022",
format!(
"transition action `{}` has incompatible arguments",
declaration.action.value
),
declaration.action.span,
)
.with_label(action.name.span, "action is declared here"),
);
}
} else {
self.unknown_name(
"C1023",
"action",
&declaration.action.value,
declaration.action.span,
self.parts.actions.keys().cloned().collect(),
);
}
let condition = self.type_expression(&declaration.condition, &scope);
self.require_assignable(
&ValueType::Bool,
&condition,
declaration.condition.span,
"transition condition",
);
let mut updated = BTreeMap::<String, Span>::new();
for update in &declaration.updates {
if normalize_name(&update.receiver.value) != normalize_name(¶meter.name.value) {
self.parts.diagnostics.push(Diagnostic::error(
"C1024",
"state update must target the transition state parameter",
update.receiver.span,
));
}
let field_key = normalize_name(&update.field.value);
if let Some(previous) = updated.insert(field_key.clone(), update.field.span) {
self.parts.diagnostics.push(
Diagnostic::error(
"C1025",
format!("state field `{field_key}` is updated more than once"),
update.field.span,
)
.with_label(previous, "first update is here"),
);
}
let Some(state_name) = &state_name else {
continue;
};
let Some(state) = self.parts.states.get(state_name).cloned() else {
continue;
};
let Some(field) = state
.fields
.iter()
.find(|field| normalize_name(&field.name.value) == field_key)
else {
self.unknown_name(
"C1026",
"state field",
&update.field.value,
update.field.span,
state
.fields
.iter()
.map(|field| normalize_name(&field.name.value))
.collect(),
);
continue;
};
let expected = self.resolve_type_ref(&field.ty, field.span);
let actual = self.type_expression_expected(&update.value, &scope, Some(&expected));
self.require_assignable(&expected, &actual, update.value.span, "state update");
}
}
}
fn validate_traces(&mut self) {
let declarations = self.parts.traces.values().cloned().collect::<Vec<_>>();
for declaration in declarations {
let variable_type = self.resolve_parameter_type(&declaration.variable.ty);
if !matches!(variable_type, ValueType::State(_) | ValueType::Error) {
self.parts.diagnostics.push(Diagnostic::error(
"C1030",
"a trace variable must have a state type",
declaration.variable.span,
));
}
if declaration.within.value == 0 {
self.parts.diagnostics.push(Diagnostic::error(
"C1031",
"a trace bound must be greater than zero",
declaration.within.span,
));
}
if declaration.always.is_empty()
&& declaration.terminal.is_none()
&& !declaration.no_dead_ends
{
self.parts.diagnostics.push(Diagnostic::error(
"C1032",
"trace declares no safety, termination, or dead-end property",
declaration.span,
));
}
let scope = BTreeMap::from([(
normalize_name(&declaration.variable.name.value),
variable_type,
)]);
for (label, expression) in std::iter::once(("initial condition", &declaration.initial))
.chain(
declaration
.always
.iter()
.map(|expression| ("always condition", expression)),
)
.chain(
declaration
.terminal
.iter()
.map(|expression| ("termination condition", expression)),
)
{
let actual = self.type_expression(expression, &scope);
self.require_assignable(&ValueType::Bool, &actual, expression.span, label);
}
}
}
fn validate_derives(&mut self) {
let declarations = self.parts.derives.values().cloned().collect::<Vec<_>>();
for declaration in declarations {
let scope = self.parameter_scope(&declaration.parameters);
let expected =
self.resolve_type_ref(&declaration.return_type.value, declaration.return_type.span);
let actual =
self.type_expression_expected(&declaration.expression, &scope, Some(&expected));
self.require_assignable(&expected, &actual, declaration.expression.span, "fn result");
let mut basis_references = BTreeMap::<String, Span>::new();
for reference in &declaration.basis {
let key = normalize_name(&reference.id.value);
if !self.parts.fragments.contains_key(&key) {
self.unknown_name(
"C0410",
"fragment",
&reference.id.value,
reference.id.span,
self.parts.fragments.keys().cloned().collect(),
);
}
if let Some(previous) = basis_references.insert(key, reference.span()) {
self.parts.diagnostics.push(
Diagnostic::error(
"C0411",
format!("duplicate basis reference `{}`", reference.id.value),
reference.span(),
)
.with_label(previous, "first reference is here")
.with_help("remove one of the duplicate basis references"),
);
}
}
}
}
fn validate_decisions(&mut self) {
let declarations = self.parts.decisions.values().cloned().collect::<Vec<_>>();
for declaration in declarations {
let scope = self.parameter_scope(&declaration.parameters);
self.require_entity_parameters(
"decision",
&declaration.name.value,
&declaration.parameters,
&scope,
);
let return_type =
self.resolve_type_ref(&declaration.return_type.value, declaration.return_type.span);
if matches!(return_type, ValueType::Entity(_) | ValueType::State(_)) {
self.parts.diagnostics.push(
Diagnostic::error(
"C0303",
"decision results must be scalar values",
declaration.return_type.span,
)
.with_help("return Bool, Int, Decimal, String, Date, Duration, or an enum"),
);
}
}
}
fn validate_fragments(&mut self) {
let declarations = self.parts.fragments.values().cloned().collect::<Vec<_>>();
for declaration in declarations {
let mut references = BTreeMap::<String, Span>::new();
for reference in &declaration.refs {
let key = normalize_name(&reference.id.value);
if !self.parts.fragments.contains_key(&key) {
self.unknown_name(
"C0412",
"fragment",
&reference.id.value,
reference.id.span,
self.parts.fragments.keys().cloned().collect(),
);
}
if let Some(previous) = references.insert(key, reference.span()) {
self.parts.diagnostics.push(
Diagnostic::error(
"C0413",
format!("duplicate fragment reference `{}`", reference.id.value),
reference.span(),
)
.with_label(previous, "first reference is here")
.with_help("remove one of the duplicate `ref` items"),
);
}
}
}
}
fn validate_rules(&mut self) {
let declarations = self.parts.rules.values().cloned().collect::<Vec<_>>();
for declaration in declarations {
let scope = self.parameter_scope(&declaration.parameters);
self.require_entity_parameters(
"rule",
&declaration.name.value,
&declaration.parameters,
&scope,
);
let condition_type = self.type_expression(&declaration.condition, &scope);
self.require_assignable(
&ValueType::Bool,
&condition_type,
declaration.condition.span,
"rule condition",
);
if let Some(constant) = constant_truth(&declaration.condition) {
let (code, message) = if constant == TruthValue::False {
(
"W0400",
format!(
"rule `{}` has an always-false condition",
declaration.name.value
),
)
} else {
(
"W0401",
format!(
"rule `{}` has an always-true condition",
declaration.name.value
),
)
};
let intentional_constant_decision =
constant == TruthValue::True && declaration.parameters.is_empty();
if matches!(constant, TruthValue::True | TruthValue::False)
&& !intentional_constant_decision
{
self.parts.diagnostics.push(Diagnostic::warning(
code,
message,
declaration.condition.span,
));
}
}
let mut basis_references = BTreeMap::<String, Span>::new();
for reference in &declaration.basis {
let key = normalize_name(&reference.id.value);
if !self.parts.fragments.contains_key(&key) {
self.unknown_name(
"C0410",
"fragment",
&reference.id.value,
reference.id.span,
self.parts.fragments.keys().cloned().collect(),
);
}
if let Some(previous) = basis_references.insert(key, reference.span()) {
self.parts.diagnostics.push(
Diagnostic::error(
"C0411",
format!("duplicate basis reference `{}`", reference.id.value),
reference.span(),
)
.with_label(previous, "first reference is here")
.with_help("remove one of the duplicate basis references"),
);
}
}
match &declaration.effect {
Effect::Decide {
decision,
arguments,
value,
span,
} => {
self.validate_decision_use(decision, arguments, Some(value), *span, &scope);
}
Effect::Override { rule, span } => {
let target_key = normalize_name(&rule.value);
if let Some(target) = self.parts.rules.get(&target_key).cloned() {
let source_types = self.parameter_types(&declaration.parameters);
let target_types = self.parameter_types(&target.parameters);
if source_types != target_types
&& !source_types.iter().any(ValueType::is_error_or_unknown)
&& !target_types.iter().any(ValueType::is_error_or_unknown)
{
self.parts.diagnostics.push(
Diagnostic::error(
"C0402",
format!(
"rule `{}` cannot override `{}` because their parameter types differ",
declaration.name.value, target.name.value
),
*span,
)
.with_label(target.name.span, "target rule is declared here"),
);
}
} else {
self.unknown_name(
"C0401",
"rule",
&rule.value,
rule.span,
self.parts.rules.keys().cloned().collect(),
);
}
}
Effect::Invalid { .. } => {}
}
}
}
fn require_entity_parameters(
&mut self,
declaration_kind: &str,
declaration_name: &str,
parameters: &[Parameter],
scope: &BTreeMap<String, ValueType>,
) {
for parameter in parameters {
let key = normalize_name(¶meter.name.value);
let Some(parameter_type) = scope.get(&key) else {
continue;
};
if !matches!(parameter_type, ValueType::Entity(_) | ValueType::Error) {
self.parts.diagnostics.push(
Diagnostic::error(
"C0302",
format!(
"{declaration_kind} `{declaration_name}` parameter `{}` must have a record type",
parameter.name.value
),
parameter.span,
)
.with_help("use scalar parameters on `fn` declarations instead"),
);
}
}
}
fn validate_cases(&mut self) {
let declarations = self.parts.cases.values().cloned().collect::<Vec<_>>();
for declaration in declarations {
let mut scope = BTreeMap::new();
let mut binding_spans = BTreeMap::<String, Span>::new();
for binding in &declaration.bindings {
let key = normalize_name(&binding.name.value);
if let Some(previous) = binding_spans.insert(key.clone(), binding.name.span) {
self.parts.diagnostics.push(
Diagnostic::error(
"C0600",
format!("duplicate test binding `{key}`"),
binding.name.span,
)
.with_label(previous, "first binding is here")
.with_help(
"when a test needs the same record type twice, write explicit aliases such as `let left = Record { ... }` and `let right = Record { ... }`",
),
);
}
let entity_name = normalize_name(&binding.entity.value);
if self.parts.entities.contains_key(&entity_name) {
scope.insert(key, ValueType::Entity(entity_name));
} else {
self.unknown_name(
"C0601",
"record",
&binding.entity.value,
binding.entity.span,
self.parts.entities.keys().cloned().collect(),
);
scope.insert(key, ValueType::Error);
}
}
for binding in &declaration.bindings {
self.validate_binding(binding, &scope);
}
for expectation in &declaration.expectations {
self.validate_expectation(expectation, &scope);
}
if declaration.expectations.is_empty() {
self.parts.diagnostics.push(Diagnostic::warning(
"W0600",
format!("test `{}` has no expectations", declaration.name.value),
declaration.span,
));
}
}
}
fn validate_binding(&mut self, binding: &BindingDecl, scope: &BTreeMap<String, ValueType>) {
let entity_name = normalize_name(&binding.entity.value);
let Some(entity) = self.parts.entities.get(&entity_name).cloned() else {
return;
};
let fields = entity
.fields
.iter()
.map(|field| (normalize_name(&field.name.value), field.clone()))
.collect::<BTreeMap<_, _>>();
let mut provided = BTreeMap::<String, Span>::new();
for value in &binding.fields {
let key = normalize_name(&value.name.value);
if let Some(previous) = provided.insert(key.clone(), value.name.span) {
self.parts.diagnostics.push(
Diagnostic::error(
"C0602",
format!("field `{key}` is initialized more than once"),
value.name.span,
)
.with_label(previous, "first initializer is here"),
);
continue;
}
let Some(field) = fields.get(&key) else {
self.unknown_name(
"C0603",
"field",
&value.name.value,
value.name.span,
fields.keys().cloned().collect(),
);
continue;
};
let expected = self.resolve_type_ref(&field.ty, field.span);
let actual = self.type_expression_expected(&value.value, scope, Some(&expected));
self.require_assignable(&expected, &actual, value.value.span, "field initializer");
if let Some(range) = &field.range {
self.validate_constant_in_range(&value.value, range, &expected);
}
self.validate_constant_in_domain(&value.value, field, &expected);
}
for (name, field) in fields {
if !field.optional && !provided.contains_key(&name) {
self.parts.diagnostics.push(
Diagnostic::error(
"C0604",
format!("missing required field `{name}` in `{entity_name}` value"),
binding.span,
)
.with_label(field.name.span, "field is declared as required here"),
);
}
}
}
fn validate_invariants(&mut self) {
let declarations = self.parts.invariants.values().cloned().collect::<Vec<_>>();
for declaration in declarations {
let scope = self.parameter_scope(&declaration.variables);
for variable in &declaration.variables {
let variable_type = self.resolve_parameter_type(&variable.ty);
if !matches!(variable_type, ValueType::Entity(_) | ValueType::Error) {
self.parts.diagnostics.push(Diagnostic::error(
"C0700",
"an assertion parameter must have a record type",
variable.ty.span,
));
}
}
match &declaration.assertion {
InvariantAssertion::Cardinality {
decision,
arguments,
span,
..
} => {
self.validate_invariant_arguments(arguments, &scope);
self.validate_decision_use(decision, arguments, None, *span, &scope);
}
InvariantAssertion::Implication {
condition,
expectation,
..
} => {
let actual = self.type_expression(condition, &scope);
self.require_assignable(
&ValueType::Bool,
&actual,
condition.span,
"assertion condition",
);
self.validate_invariant_arguments(&expectation.arguments, &scope);
self.validate_expectation(expectation, &scope);
}
}
}
}
fn validate_invariant_arguments(
&mut self,
arguments: &[Expr],
scope: &BTreeMap<String, ValueType>,
) {
for argument in arguments {
let is_quantified_entity = match &argument.kind {
ExprKind::Name(name) => matches!(
scope.get(&normalize_name(name)),
Some(ValueType::Entity(_) | ValueType::Error)
),
_ => false,
};
if !is_quantified_entity {
self.parts.diagnostics.push(
Diagnostic::error(
"C0701",
"an assertion decision argument must be a record parameter",
argument.span,
)
.with_help(
"pass a parameter declared in this assertion's `(name Record)` header directly",
),
);
}
}
}
fn validate_expectation(
&mut self,
expectation: &Expectation,
scope: &BTreeMap<String, ValueType>,
) {
self.validate_decision_use(
&expectation.decision,
&expectation.arguments,
Some(&expectation.value),
expectation.span,
scope,
);
}
fn validate_decision_use(
&mut self,
decision_name: &Spanned<String>,
arguments: &[Expr],
value: Option<&Expr>,
span: Span,
scope: &BTreeMap<String, ValueType>,
) {
let key = normalize_name(&decision_name.value);
let Some(decision) = self.parts.decisions.get(&key).cloned() else {
self.unknown_name(
"C0500",
"decision",
&decision_name.value,
decision_name.span,
self.parts.decisions.keys().cloned().collect(),
);
for argument in arguments {
self.type_expression(argument, scope);
}
if let Some(value) = value {
self.type_expression(value, scope);
}
return;
};
self.validate_arguments(&decision.parameters, arguments, span, scope);
if let Some(value) = value {
let expected =
self.resolve_type_ref(&decision.return_type.value, decision.return_type.span);
let actual = self.type_expression_expected(value, scope, Some(&expected));
self.require_assignable(&expected, &actual, value.span, "decision value");
}
}
fn validate_arguments(
&mut self,
parameters: &[Parameter],
arguments: &[Expr],
span: Span,
scope: &BTreeMap<String, ValueType>,
) {
if parameters.len() != arguments.len() {
self.parts.diagnostics.push(Diagnostic::error(
"C0304",
format!(
"expected {} argument{}, found {}",
parameters.len(),
if parameters.len() == 1 { "" } else { "s" },
arguments.len()
),
span,
));
}
for (parameter, argument) in parameters.iter().zip(arguments) {
let expected = self.resolve_parameter_type(¶meter.ty);
let actual = self.type_expression_expected(argument, scope, Some(&expected));
self.require_assignable(&expected, &actual, argument.span, "argument");
}
for argument in arguments.iter().skip(parameters.len()) {
self.type_expression(argument, scope);
}
}
fn parameter_scope(&mut self, parameters: &[Parameter]) -> BTreeMap<String, ValueType> {
let mut scope = BTreeMap::new();
let mut spans = BTreeMap::<String, Span>::new();
for parameter in parameters {
let key = normalize_name(¶meter.name.value);
if let Some(previous) = spans.insert(key.clone(), parameter.name.span) {
self.parts.diagnostics.push(
Diagnostic::error(
"C0300",
format!("duplicate parameter `{key}`"),
parameter.name.span,
)
.with_label(previous, "first parameter is here"),
);
}
scope.insert(key, self.resolve_parameter_type(¶meter.ty));
}
scope
}
fn parameter_types(&mut self, parameters: &[Parameter]) -> Vec<ValueType> {
parameters
.iter()
.map(|parameter| self.resolve_parameter_type(¶meter.ty))
.collect()
}
fn resolve_parameter_type(&mut self, ty: &Spanned<String>) -> ValueType {
self.resolve_named_type(&ty.value, ty.span)
}
fn resolve_type_ref(&mut self, ty: &TypeRef, span: Span) -> ValueType {
match ty {
TypeRef::Bool => ValueType::Bool,
TypeRef::Int => ValueType::Int,
TypeRef::Decimal => ValueType::Decimal,
TypeRef::String => ValueType::String,
TypeRef::Date => ValueType::Date,
TypeRef::Duration => ValueType::Duration,
TypeRef::Named(name) => self.resolve_named_type(name, span),
TypeRef::Unknown => ValueType::Error,
}
}
fn resolve_named_type(&mut self, name: &str, span: Span) -> ValueType {
let key = normalize_name(name);
let builtin = match key.as_str() {
"Bool" => Some(ValueType::Bool),
"Int" => Some(ValueType::Int),
"Decimal" => Some(ValueType::Decimal),
"String" => Some(ValueType::String),
"Date" => Some(ValueType::Date),
"Duration" => Some(ValueType::Duration),
_ => None,
};
if let Some(builtin) = builtin {
return builtin;
}
match self.type_names.get(&key) {
Some(TypeKind::Enum) => ValueType::Enum(key),
Some(TypeKind::Entity) => ValueType::Entity(key),
Some(TypeKind::State) => ValueType::State(key),
None => {
let mut candidates = self.type_names.keys().cloned().collect::<Vec<_>>();
candidates.extend(
["Bool", "Int", "Decimal", "String", "Date", "Duration"]
.into_iter()
.map(str::to_owned),
);
self.unknown_name("C0301", "type", name, span, candidates);
ValueType::Error
}
}
}
fn type_expression(
&mut self,
expression: &Expr,
scope: &BTreeMap<String, ValueType>,
) -> ValueType {
self.type_expression_expected(expression, scope, None)
}
fn type_expression_expected(
&mut self,
expression: &Expr,
scope: &BTreeMap<String, ValueType>,
expected: Option<&ValueType>,
) -> ValueType {
let inferred = match &expression.kind {
ExprKind::Literal(literal) => self.type_literal(literal, expression.span),
ExprKind::Name(name) => self.resolve_value_name(name, expression.span, scope, expected),
ExprKind::Field { receiver, field } => {
let receiver_type = self.type_expression(receiver, scope);
if let ValueType::Entity(type_name) | ValueType::State(type_name) = receiver_type {
let fields = self
.parts
.entities
.get(&type_name)
.map(|declaration| declaration.fields.clone())
.or_else(|| {
self.parts
.states
.get(&type_name)
.map(|declaration| declaration.fields.clone())
});
if let Some(fields) = fields {
if let Some(declaration) = fields.iter().find(|candidate| {
normalize_name(&candidate.name.value) == normalize_name(&field.value)
}) {
self.resolve_type_ref(&declaration.ty, declaration.span)
} else {
self.unknown_name(
"C0302",
"field",
&field.value,
field.span,
fields
.iter()
.map(|candidate| normalize_name(&candidate.name.value))
.collect(),
);
ValueType::Error
}
} else {
ValueType::Error
}
} else if receiver_type.is_error_or_unknown() {
receiver_type
} else {
self.parts.diagnostics.push(Diagnostic::error(
"C0303",
format!(
"cannot access field `{}` on {}",
field.value,
receiver_type.display_name()
),
expression.span,
));
ValueType::Error
}
}
ExprKind::Call { callee, arguments } => {
self.type_call(callee, arguments, expression.span, scope)
}
ExprKind::Unary { operator, operand } => {
let operand_type = self.type_expression(operand, scope);
match operator {
UnaryOp::Not => {
self.require_assignable(
&ValueType::Bool,
&operand_type,
operand.span,
"operand of `not`",
);
ValueType::Bool
}
UnaryOp::Negate => {
if operand_type.is_numeric() || operand_type.is_error_or_unknown() {
operand_type
} else {
self.expected_numeric(&operand_type, operand.span);
ValueType::Error
}
}
}
}
ExprKind::Binary {
left,
operator,
right,
} => self.type_binary(left, *operator, right, scope),
};
self.parts.expression_types.insert(
(expression.span.start, expression.span.end),
inferred.clone(),
);
inferred
}
fn type_literal(&mut self, literal: &Literal, span: Span) -> ValueType {
match literal {
Literal::Bool(_) => ValueType::Bool,
Literal::Number(NumericLiteral::Int(_)) => ValueType::Int,
Literal::Number(NumericLiteral::Decimal(text)) => match parse_decimal(text) {
Ok(_) => ValueType::Decimal,
Err(error) => {
self.parts.diagnostics.push(Diagnostic::error(
"C0800",
error.to_string(),
span,
));
ValueType::Error
}
},
Literal::String(_) => ValueType::String,
Literal::Unknown => ValueType::Unknown,
}
}
fn resolve_value_name(
&mut self,
name: &str,
span: Span,
scope: &BTreeMap<String, ValueType>,
expected: Option<&ValueType>,
) -> ValueType {
let key = normalize_name(name);
if let Some(ty) = scope.get(&key) {
return ty.clone();
}
if let Some(ValueType::Enum(expected_enum)) = expected {
if self.enum_contains_variant(expected_enum, &key) {
return ValueType::Enum(expected_enum.clone());
}
}
match self.variant_owners.get(&key) {
Some(owners) if owners.len() == 1 => ValueType::Enum(owners[0].clone()),
Some(owners) => {
self.parts.diagnostics.push(
Diagnostic::error(
"C0801",
format!("enum variant `{name}` is ambiguous"),
span,
)
.with_help(format!(
"the variant is declared by {}; use it where the expected enum type is known",
owners.join(", ")
)),
);
ValueType::Error
}
None => {
let mut candidates = scope.keys().cloned().collect::<Vec<_>>();
candidates.extend(self.variant_owners.keys().cloned());
self.unknown_name("C0802", "value", name, span, candidates);
ValueType::Error
}
}
}
fn type_call(
&mut self,
callee: &Spanned<String>,
arguments: &[Expr],
span: Span,
scope: &BTreeMap<String, ValueType>,
) -> ValueType {
let key = normalize_name(&callee.value);
match key.as_str() {
"date" => {
self.validate_builtin_arguments(arguments, &[ValueType::String], span, scope);
ValueType::Date
}
"days" | "hours" | "minutes" => {
self.validate_builtin_arguments(arguments, &[ValueType::Int], span, scope);
ValueType::Duration
}
"abs" => {
if arguments.len() != 1 {
self.parts.diagnostics.push(Diagnostic::error(
"C0810",
format!(
"builtin `abs` expects 1 argument, found {}",
arguments.len()
),
span,
));
}
let value_type = arguments.first().map_or(ValueType::Error, |argument| {
let value_type = self.type_expression(argument, scope);
if !value_type.is_numeric() && !value_type.is_error_or_unknown() {
self.expected_numeric(&value_type, argument.span);
}
value_type
});
for argument in arguments.iter().skip(1) {
self.type_expression(argument, scope);
}
value_type
}
"max" | "min" => {
if arguments.len() != 2 {
self.parts.diagnostics.push(Diagnostic::error(
"C0810",
format!(
"builtin `{key}` expects 2 arguments, found {}",
arguments.len()
),
span,
));
}
let types = arguments
.iter()
.map(|argument| self.type_expression(argument, scope))
.collect::<Vec<_>>();
for (argument, ty) in arguments.iter().zip(&types) {
if !ty.is_numeric() && !ty.is_error_or_unknown() {
self.expected_numeric(ty, argument.span);
}
}
numeric_result(types.first(), types.get(1))
}
_ => {
if let Some(declaration) = self.parts.derives.get(&key).cloned() {
self.validate_arguments(&declaration.parameters, arguments, span, scope);
self.resolve_type_ref(
&declaration.return_type.value,
declaration.return_type.span,
)
} else if let Some(declaration) = self.parts.decisions.get(&key).cloned() {
self.validate_arguments(&declaration.parameters, arguments, span, scope);
self.parts.diagnostics.push(
Diagnostic::error(
"C0809",
format!(
"decision `{}` cannot be called inside an expression",
declaration.name.value
),
span,
)
.with_help(
"use a pure `fn` for intermediate computation; query `dec` declarations only in rule effects, tests, and assertions",
),
);
self.resolve_type_ref(
&declaration.return_type.value,
declaration.return_type.span,
)
} else {
let mut candidates = self.callable_names.keys().cloned().collect::<Vec<_>>();
candidates.extend(
["date", "days", "hours", "minutes", "abs", "max", "min"]
.into_iter()
.map(str::to_owned),
);
self.unknown_name("C0803", "callable", &callee.value, callee.span, candidates);
for argument in arguments {
self.type_expression(argument, scope);
}
ValueType::Error
}
}
}
}
fn validate_builtin_arguments(
&mut self,
arguments: &[Expr],
expected: &[ValueType],
span: Span,
scope: &BTreeMap<String, ValueType>,
) {
if arguments.len() != expected.len() {
self.parts.diagnostics.push(Diagnostic::error(
"C0810",
format!(
"expected {} argument(s), found {}",
expected.len(),
arguments.len()
),
span,
));
}
for (argument, expected) in arguments.iter().zip(expected) {
let actual = self.type_expression_expected(argument, scope, Some(expected));
self.require_assignable(expected, &actual, argument.span, "builtin argument");
}
for argument in arguments.iter().skip(expected.len()) {
self.type_expression(argument, scope);
}
}
fn type_binary(
&mut self,
left: &Expr,
operator: BinaryOp,
right: &Expr,
scope: &BTreeMap<String, ValueType>,
) -> ValueType {
let left_type = self.type_expression(left, scope);
let right_type = self.type_expression_expected(right, scope, Some(&left_type));
match operator {
BinaryOp::And | BinaryOp::Or => {
self.require_assignable(&ValueType::Bool, &left_type, left.span, "left operand");
self.require_assignable(&ValueType::Bool, &right_type, right.span, "right operand");
ValueType::Bool
}
BinaryOp::Equal | BinaryOp::NotEqual => {
if !types_compatible(&left_type, &right_type) {
self.parts.diagnostics.push(Diagnostic::error(
"C0804",
format!(
"cannot compare {} with {}",
left_type.display_name(),
right_type.display_name()
),
left.span.merge(right.span),
));
}
ValueType::Bool
}
BinaryOp::Greater | BinaryOp::GreaterEqual | BinaryOp::Less | BinaryOp::LessEqual => {
let comparable = (left_type.is_numeric() && right_type.is_numeric())
|| (left_type == right_type
&& matches!(
left_type,
ValueType::String | ValueType::Date | ValueType::Duration
))
|| left_type.is_error_or_unknown()
|| right_type.is_error_or_unknown();
if !comparable {
self.parts.diagnostics.push(Diagnostic::error(
"C0805",
format!(
"values of type {} and {} are not order-comparable",
left_type.display_name(),
right_type.display_name()
),
left.span.merge(right.span),
));
}
ValueType::Bool
}
BinaryOp::Add | BinaryOp::Subtract | BinaryOp::Multiply | BinaryOp::Divide => {
for (ty, operand) in [(&left_type, left), (&right_type, right)] {
if !ty.is_numeric() && !ty.is_error_or_unknown() {
self.expected_numeric(ty, operand.span);
}
}
if operator == BinaryOp::Divide {
ValueType::Decimal
} else {
numeric_result(Some(&left_type), Some(&right_type))
}
}
}
}
fn validate_range(&mut self, range: &crate::ast::RangeConstraint, ty: &ValueType) {
if !ty.is_numeric() && !ty.is_error_or_unknown() {
self.parts.diagnostics.push(Diagnostic::error(
"C0201",
format!(
"range constraints cannot be applied to {}",
ty.display_name()
),
range.span,
));
return;
}
let start = numeric_literal_decimal(&range.start);
let end = numeric_literal_decimal(&range.end);
if matches!(ty, ValueType::Int)
&& (matches!(range.start, NumericLiteral::Decimal(_))
|| matches!(range.end, NumericLiteral::Decimal(_)))
{
self.parts.diagnostics.push(Diagnostic::error(
"C0202",
"an Int range must use integer bounds",
range.span,
));
}
match (start, end) {
(Ok(start), Ok(end)) if start > end => self.parts.diagnostics.push(
Diagnostic::error("C0203", "range start is greater than range end", range.span)
.with_help("swap the bounds or choose a non-empty inclusive range"),
),
(Err(message), _) | (_, Err(message)) => self
.parts
.diagnostics
.push(Diagnostic::error("C0204", message, range.span)),
_ => {}
}
}
fn validate_domain(&mut self, field: &FieldDecl, ty: &ValueType) {
let Some(domain) = &field.domain else {
return;
};
if field.range.is_some() {
self.parts.diagnostics.push(
Diagnostic::error(
"C0205",
"a field cannot declare both an inclusive range and a finite value set",
domain.span,
)
.with_help("use one closed input constraint"),
);
}
if !matches!(
ty,
ValueType::String | ValueType::Decimal | ValueType::Error
) {
self.parts.diagnostics.push(
Diagnostic::error(
"C0206",
"a `{...}` value set is currently available for String and Decimal fields",
domain.span,
)
.with_help("Bool and enum are already closed; use `start..end` for Int"),
);
}
if domain.values.is_empty() {
self.parts.diagnostics.push(Diagnostic::error(
"C0207",
"a field domain must contain at least one value",
domain.span,
));
return;
}
let mut seen = BTreeMap::<String, Span>::new();
for value in &domain.values {
let actual = self.type_expression_expected(value, &BTreeMap::new(), Some(ty));
self.require_assignable(ty, &actual, value.span, "domain value");
let key = constant_domain_key(value, ty);
let Some(key) = key else {
self.parts.diagnostics.push(
Diagnostic::error(
"C0208",
"domain values must be compile-time literals",
value.span,
)
.with_help("list String or Decimal literal values directly"),
);
continue;
};
if let Some(previous) = seen.insert(key, value.span) {
self.parts.diagnostics.push(
Diagnostic::error("C0209", "duplicate value in field domain", value.span)
.with_label(previous, "first value is here"),
);
}
}
}
fn validate_constant_in_range(
&mut self,
expression: &Expr,
range: &crate::ast::RangeConstraint,
ty: &ValueType,
) {
let Some(value) = constant_numeric(expression) else {
return;
};
let (Ok(start), Ok(end)) = (
numeric_literal_decimal(&range.start),
numeric_literal_decimal(&range.end),
) else {
return;
};
if value < start || value > end {
let start = Value::decimal(start);
let end = Value::decimal(end);
self.parts.diagnostics.push(
Diagnostic::error(
"C0605",
format!(
"{} value is outside the inclusive range {start}..{end}",
ty.display_name()
),
expression.span,
)
.with_label(range.span, "range is declared here"),
);
}
}
fn validate_constant_in_domain(
&mut self,
expression: &Expr,
field: &FieldDecl,
ty: &ValueType,
) {
let Some(domain) = &field.domain else {
return;
};
let Some(actual) = constant_domain_key(expression, ty) else {
return;
};
if !domain
.values
.iter()
.filter_map(|value| constant_domain_key(value, ty))
.any(|value| value == actual)
{
self.parts.diagnostics.push(
Diagnostic::error(
"C0610",
format!(
"{} value is outside the declared field domain",
ty.display_name()
),
expression.span,
)
.with_label(domain.span, "allowed values are declared here"),
);
}
}
fn validate_derive_cycles(&mut self) {
let mut graph = BTreeMap::<String, BTreeSet<String>>::new();
for (name, declaration) in &self.parts.derives {
let mut calls = BTreeSet::new();
collect_calls(&declaration.expression, &mut calls);
calls.retain(|callee| self.parts.derives.contains_key(callee));
graph.insert(name.clone(), calls);
}
for component in cyclic_components(&graph) {
let primary_name = &component[0];
let primary = self.parts.derives[primary_name].name.span;
let mut diagnostic = Diagnostic::error(
"C0900",
format!("fn cycle: {}", component.join(" -> ")),
primary,
);
for name in component.iter().skip(1) {
diagnostic = diagnostic.with_label(
self.parts.derives[name].name.span,
format!("`{name}` participates in this cycle"),
);
}
self.parts.diagnostics.push(diagnostic);
}
}
fn validate_override_cycles(&mut self) {
let mut graph = BTreeMap::<String, BTreeSet<String>>::new();
for (name, declaration) in &self.parts.rules {
let mut targets = BTreeSet::new();
if let Effect::Override { rule, .. } = &declaration.effect {
let target = normalize_name(&rule.value);
if self.parts.rules.contains_key(&target) {
targets.insert(target);
}
}
graph.insert(name.clone(), targets);
}
for component in cyclic_components(&graph) {
let primary_name = &component[0];
let primary = self.parts.rules[primary_name].name.span;
let mut diagnostic = Diagnostic::error(
"C0901",
format!("override cycle: {}", component.join(" -> ")),
primary,
);
for name in component.iter().skip(1) {
diagnostic = diagnostic.with_label(
self.parts.rules[name].name.span,
format!("`{name}` participates in this cycle"),
);
}
self.parts.diagnostics.push(diagnostic);
}
}
#[allow(clippy::too_many_lines)]
fn validate_static_rule_interactions(&mut self) {
let mut guaranteed_overrides = BTreeSet::new();
for rule in self.parts.rules.values() {
let condition = expression_fingerprint(&rule.condition);
if let Effect::Override { rule: target, .. } = &rule.effect {
let target_name = normalize_name(&target.value);
let Some(target_rule) = self.parts.rules.get(&target_name) else {
continue;
};
if condition == expression_fingerprint(&target_rule.condition) {
guaranteed_overrides.insert((condition.clone(), target_name));
}
}
}
let mut summaries = Vec::new();
for (rule_name, rule) in &self.parts.rules {
let condition = expression_fingerprint(&rule.condition);
if let Effect::Decide {
decision,
arguments,
value,
span,
} = &rule.effect
{
summaries.push(DecisionEffectSummary {
rule_name: rule_name.clone(),
condition: condition.clone(),
decision: normalize_name(&decision.value),
arguments: arguments.iter().map(expression_fingerprint).collect(),
value: expression_fingerprint(value),
effect_span: *span,
});
}
}
summaries.sort_by(|left, right| {
(
left.rule_name.as_str(),
left.effect_span.start,
left.effect_span.end,
)
.cmp(&(
right.rule_name.as_str(),
right.effect_span.start,
right.effect_span.end,
))
});
for left_index in 0..summaries.len() {
for right in &summaries[(left_index + 1)..] {
let left = &summaries[left_index];
if left.condition != right.condition
|| left.decision != right.decision
|| left.arguments != right.arguments
{
continue;
}
if guaranteed_overrides.contains(&(left.condition.clone(), left.rule_name.clone()))
|| guaranteed_overrides
.contains(&(right.condition.clone(), right.rule_name.clone()))
{
continue;
}
if left.value == right.value {
self.parts.diagnostics.push(
Diagnostic::warning(
"W0500",
format!(
"rules `{}` and `{}` produce the same candidate under the same condition",
left.rule_name, right.rule_name
),
right.effect_span,
)
.with_label(left.effect_span, "equivalent effect is here"),
);
} else if !self
.parts
.decisions
.get(&left.decision)
.is_some_and(|decision| decision.cardinality == crate::ast::Cardinality::Many)
{
self.parts.diagnostics.push(
Diagnostic::error(
"C0501",
format!(
"rules `{}` and `{}` produce conflicting `{}` candidates under the same condition",
left.rule_name, right.rule_name, left.decision
),
right.effect_span,
)
.with_label(left.effect_span, "conflicting candidate is here")
.with_help(
"make the rule conditions mutually exclusive or add an explicit override",
),
);
}
}
}
let rules = self.parts.rules.values().cloned().collect::<Vec<_>>();
for rule in rules {
let condition = expression_fingerprint(&rule.condition);
if let Effect::Override { rule: target, span } = &rule.effect {
let target_key = normalize_name(&target.value);
let Some(target_rule) = self.parts.rules.get(&target_key) else {
continue;
};
if condition == expression_fingerprint(&target_rule.condition) {
self.parts.diagnostics.push(
Diagnostic::warning(
"W0502",
format!(
"rule `{}` is always overridden by `{}` when its condition holds",
target_rule.name.value, rule.name.value
),
target_rule.name.span,
)
.with_label(*span, "override is declared here"),
);
}
}
}
}
fn require_assignable(
&mut self,
expected: &ValueType,
actual: &ValueType,
span: Span,
context: &str,
) {
if !expected.accepts(actual) {
self.parts.diagnostics.push(Diagnostic::error(
"C0806",
format!(
"{context} expects {}, found {}",
expected.display_name(),
actual.display_name()
),
span,
));
}
}
fn expected_numeric(&mut self, actual: &ValueType, span: Span) {
self.parts.diagnostics.push(Diagnostic::error(
"C0807",
format!("expected a numeric value, found {}", actual.display_name()),
span,
));
}
fn enum_contains_variant(&self, enum_name: &str, variant: &str) -> bool {
self.parts.enums.get(enum_name).is_some_and(|declaration| {
declaration
.variants
.iter()
.any(|candidate| normalize_name(&candidate.value) == variant)
})
}
fn unknown_name(
&mut self,
code: &str,
kind: &str,
name: &str,
span: Span,
candidates: Vec<String>,
) {
let mut diagnostic = Diagnostic::error(code, format!("unknown {kind} `{name}`"), span);
if let Some(suggestion) = closest_name(name, candidates) {
diagnostic = diagnostic.with_help(format!("did you mean `{suggestion}`?"));
}
self.parts.diagnostics.push(diagnostic);
}
}
fn insert_declaration<T>(
map: &mut BTreeMap<String, T>,
key: String,
value: T,
diagnostics: &mut Vec<Diagnostic>,
kind: &str,
) where
T: NamedDeclaration,
{
if let Some(previous) = map.get(&key) {
diagnostics.push(
Diagnostic::error(
"C0001",
format!("duplicate {kind} `{key}`"),
value.name_span(),
)
.with_label(previous.name_span(), "first declaration is here")
.with_help("names are compared after NFC normalization"),
);
} else {
map.insert(key, value);
}
}
trait NamedDeclaration {
fn name_span(&self) -> Span;
}
macro_rules! impl_named_declaration {
($($type:ty),+ $(,)?) => {
$(
impl NamedDeclaration for $type {
fn name_span(&self) -> Span {
self.name.span
}
}
)+
};
}
impl_named_declaration!(
EnumDecl,
EntityDecl,
DeriveDecl,
DecisionDecl,
RuleDecl,
CaseDecl,
InvariantDecl,
StateDecl,
ActionDecl,
TransitionDecl,
TraceDecl,
);
impl NamedDeclaration for FragmentDecl {
fn name_span(&self) -> Span {
self.id.span
}
}
fn numeric_result(left: Option<&ValueType>, right: Option<&ValueType>) -> ValueType {
if matches!(left, Some(ValueType::Error)) || matches!(right, Some(ValueType::Error)) {
ValueType::Error
} else if matches!(left, Some(ValueType::Decimal)) || matches!(right, Some(ValueType::Decimal))
{
ValueType::Decimal
} else if matches!(left, Some(ValueType::Unknown)) || matches!(right, Some(ValueType::Unknown))
{
ValueType::Unknown
} else {
ValueType::Int
}
}
fn types_compatible(left: &ValueType, right: &ValueType) -> bool {
left.accepts(right) || right.accepts(left)
}
fn numeric_literal_decimal(literal: &NumericLiteral) -> Result<Decimal, String> {
match literal {
NumericLiteral::Int(value) => Ok(Decimal::from(*value)),
NumericLiteral::Decimal(value) => parse_decimal(value).map_err(|error| error.to_string()),
}
}
fn constant_numeric(expression: &Expr) -> Option<Decimal> {
match &expression.kind {
ExprKind::Literal(Literal::Number(value)) => numeric_literal_decimal(value).ok(),
ExprKind::Unary {
operator: UnaryOp::Negate,
operand,
} => Decimal::ZERO.checked_sub(constant_numeric(operand)?),
_ => None,
}
}
fn constant_domain_key(expression: &Expr, ty: &ValueType) -> Option<String> {
match ty {
ValueType::String => match &expression.kind {
ExprKind::Literal(Literal::String(value)) => Some(format!("s:{value}")),
_ => None,
},
ValueType::Decimal => {
constant_numeric(expression).map(|value| format!("d:{}", value.normalize()))
}
ValueType::Error => Some(format!("error:{}", expression.span.start)),
_ => None,
}
}
fn constant_truth(expression: &Expr) -> Option<TruthValue> {
match &expression.kind {
ExprKind::Literal(Literal::Bool(value)) => Some((*value).into()),
ExprKind::Literal(Literal::Unknown) => Some(TruthValue::Unknown),
ExprKind::Unary {
operator: UnaryOp::Not,
operand,
} => Some(constant_truth(operand)?.not()),
ExprKind::Binary {
left,
operator: BinaryOp::And,
right,
} => Some(constant_truth(left)?.and(constant_truth(right)?)),
ExprKind::Binary {
left,
operator: BinaryOp::Or,
right,
} => Some(constant_truth(left)?.or(constant_truth(right)?)),
_ => None,
}
}
fn collect_calls(expression: &Expr, calls: &mut BTreeSet<String>) {
match &expression.kind {
ExprKind::Call { callee, arguments } => {
calls.insert(normalize_name(&callee.value));
for argument in arguments {
collect_calls(argument, calls);
}
}
ExprKind::Field { receiver, .. } => collect_calls(receiver, calls),
ExprKind::Unary { operand, .. } => collect_calls(operand, calls),
ExprKind::Binary { left, right, .. } => {
collect_calls(left, calls);
collect_calls(right, calls);
}
ExprKind::Literal(_) | ExprKind::Name(_) => {}
}
}
fn collect_derive_basis(
name: &str,
derives: &BTreeMap<String, DeriveDecl>,
fragments: &BTreeMap<String, FragmentDecl>,
visited: &mut BTreeSet<String>,
seen_basis: &mut BTreeSet<String>,
basis: &mut Vec<FragmentRef>,
) {
if !visited.insert(name.to_owned()) {
return;
}
let Some(derive) = derives.get(name) else {
return;
};
for reference in &derive.basis {
let id = normalize_name(&reference.id.value);
if fragments.contains_key(&id) && seen_basis.insert(id) {
basis.push(reference.clone());
}
}
let mut calls = BTreeSet::new();
collect_calls(&derive.expression, &mut calls);
for call in calls {
if derives.contains_key(&call) {
collect_derive_basis(&call, derives, fragments, visited, seen_basis, basis);
}
}
}
fn cyclic_components(graph: &BTreeMap<String, BTreeSet<String>>) -> Vec<Vec<String>> {
let mut remaining = graph.keys().cloned().collect::<BTreeSet<_>>();
let mut components = Vec::new();
while let Some(start) = remaining.first().cloned() {
let component = remaining
.iter()
.filter(|node| reachable(graph, &start, node) && reachable(graph, node, &start))
.cloned()
.collect::<Vec<_>>();
for node in &component {
remaining.remove(node);
}
let cyclic = component.len() > 1
|| component
.first()
.is_some_and(|node| graph.get(node).is_some_and(|edges| edges.contains(node)));
if cyclic {
components.push(component);
}
}
components
}
fn reachable(graph: &BTreeMap<String, BTreeSet<String>>, start: &str, target: &str) -> bool {
let mut stack = vec![start.to_owned()];
let mut visited = BTreeSet::new();
while let Some(node) = stack.pop() {
if !visited.insert(node.clone()) {
continue;
}
if node == target {
return true;
}
if let Some(edges) = graph.get(&node) {
stack.extend(edges.iter().rev().cloned());
}
}
false
}
#[derive(Clone, Debug)]
struct DecisionEffectSummary {
rule_name: String,
condition: String,
decision: String,
arguments: Vec<String>,
value: String,
effect_span: Span,
}
fn expression_fingerprint(expression: &Expr) -> String {
match &expression.kind {
ExprKind::Literal(Literal::Bool(value)) => format!("bool:{value}"),
ExprKind::Literal(Literal::Number(NumericLiteral::Int(value))) => {
format!("int:{value}")
}
ExprKind::Literal(Literal::Number(NumericLiteral::Decimal(value))) => {
let normalized =
parse_decimal(value).map_or_else(|_| value.clone(), |decimal| decimal.to_string());
format!("decimal:{normalized}")
}
ExprKind::Literal(Literal::String(value)) => format!("string:{value:?}"),
ExprKind::Literal(Literal::Unknown) => "unknown".to_owned(),
ExprKind::Name(name) => format!("name:{}", normalize_name(name)),
ExprKind::Field { receiver, field } => format!(
"field:{}:{}",
expression_fingerprint(receiver),
normalize_name(&field.value)
),
ExprKind::Call { callee, arguments } => format!(
"call:{}({})",
normalize_name(&callee.value),
arguments
.iter()
.map(expression_fingerprint)
.collect::<Vec<_>>()
.join(",")
),
ExprKind::Unary { operator, operand } => {
format!("unary:{operator:?}:{}", expression_fingerprint(operand))
}
ExprKind::Binary {
left,
operator,
right,
} => format!(
"binary:{operator:?}:{}:{}",
expression_fingerprint(left),
expression_fingerprint(right)
),
}
}
fn closest_name(name: &str, candidates: Vec<String>) -> Option<String> {
let normalized = normalize_name(name);
let mut candidates = candidates;
candidates.sort();
candidates.dedup();
candidates
.into_iter()
.map(|candidate| {
let distance = levenshtein(&normalized, &candidate);
(distance, candidate)
})
.filter(|(distance, candidate)| {
let length = normalized.chars().count().max(candidate.chars().count());
*distance <= 2 || (*distance <= 3 && length >= 6)
})
.min_by(std::cmp::Ord::cmp)
.map(|(_, candidate)| candidate)
}
fn levenshtein(left: &str, right: &str) -> usize {
let right = right.chars().collect::<Vec<_>>();
let mut previous = (0..=right.len()).collect::<Vec<_>>();
for (left_index, left_character) in left.chars().enumerate() {
let mut current = vec![left_index + 1];
for (right_index, right_character) in right.iter().enumerate() {
current.push(
(current[right_index] + 1)
.min(previous[right_index + 1] + 1)
.min(previous[right_index] + usize::from(left_character != *right_character)),
);
}
previous = current;
}
previous[right.len()]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse;
fn compile_text(text: &str) -> CompileOutput {
let parsed = parse(SourceFile::new("test.tes", text));
assert!(
parsed.diagnostics.is_empty(),
"compiler test fixture must parse cleanly: {:?}",
parsed.diagnostics
);
compile(parsed)
}
fn codes(output: &CompileOutput) -> Vec<&str> {
output
.diagnostics
.iter()
.map(|diagnostic| diagnostic.code.as_str())
.collect()
}
#[test]
fn compiles_minimal_program_and_exposes_stable_indexes() {
let output = compile_text(
r"mod M
enum Grade:
A
F
record Student:
score: Int 0..100
dec grade(s Student) -> Grade
",
);
let program = output.program.unwrap();
assert!(program.entity("Student").is_some());
assert!(program.decision("grade").is_some());
assert!(program.enum_has_variant("Grade", "A"));
}
#[test]
fn compiles_state_workflow_declarations_and_exposes_indexes() {
let output = compile_text(
r"mod Workflow
enum Phase:
Idle
Running
state Machine:
phase: Phase
retries: Int 0..2
enabled: Bool
action advance(machine Machine)
transition begin(machine Machine) on advance(machine):
when machine.enabled and machine.phase = Idle
set machine.phase = Running
set machine.retries = machine.retries + 1
trace eventually_running(machine Machine):
initially machine.phase = Idle
always machine.retries >= 0
terminates when machine.phase = Running
no dead ends
within 3 steps
",
);
assert!(!output.has_errors(), "{:?}", output.diagnostics);
let program = output.program.unwrap();
assert!(program.state("Machine").is_some());
assert!(program.action("advance").is_some());
assert!(program.transition("begin").is_some());
assert!(program.trace("eventually_running").is_some());
}
#[test]
fn compiles_state_syntax_words_as_contextual_identifiers() {
let output = compile_text(
r"mod state
record action:
no: Bool
within: Bool
fn always(state action) -> Bool:
state.no and state.within
dec trace(transition action) -> Bool
policy::contextual @contextual:
Contextual keyword policy.
rule no(within action):
always(within) => trace(within) = true
",
);
assert!(!output.has_errors(), "{:?}", output.diagnostics);
let program = output.program.unwrap();
assert!(program.entity("action").is_some());
assert!(program.derive("always").is_some());
assert!(program.decision("trace").is_some());
assert!(program.rule("policy::contextual::no").is_some());
}
#[test]
fn validates_state_workflow_declarations() {
let output = compile_text(
r"mod Invalid
state Infinite:
count: Int
action tick()
transition advance(state Infinite) on missing(state):
when true
set other.count = false
trace broken(state Infinite):
initially true
always true
within 0 steps
",
);
let codes = codes(&output);
for expected in ["C1001", "C1010", "C1023", "C1024", "C1031"] {
assert!(codes.contains(&expected), "missing {expected}: {codes:?}");
}
}
#[test]
fn resolves_and_renders_fragment_basis_with_current_first() {
let output = compile_text(
r"mod M
dec result() -> Bool
source::approval @승인 2.4:
고객 지원 승인 절차
source::operation @운영 5.2:
배송 운영 정책
rule r():
basis source::approval
true => result() = true
",
);
assert!(!output.has_errors(), "{:?}", output.diagnostics);
let program = output.program.unwrap();
assert!(program.fragment("source::operation").is_some());
assert!(program.fragment("운영 5.2").is_none());
assert_eq!(
program.render_basis(&program.rule("source::operation::r").unwrap().basis),
Some("배송 운영 정책\n\n고객 지원 승인 절차".to_owned())
);
}
#[test]
fn rejects_unknown_and_duplicate_basis_references() {
let output = compile_text(
r"mod M
dec result() -> Bool
source::policy @정책:
정책 원문
rule r():
basis source::policy
basis source::missing
basis source::missing
true => result() = true
",
);
assert!(codes(&output).contains(&"C0410"));
assert_eq!(
output
.diagnostics
.iter()
.filter(|diagnostic| diagnostic.code == "C0411")
.count(),
2
);
assert!(output.program.is_none());
}
#[test]
fn direct_compile_rejects_unresolved_imports() {
let output = compile_text("mod M\nuse \"model.tes\"\n");
assert!(codes(&output).contains(&"M0006"));
assert!(output.program.is_none());
}
#[test]
fn nfc_equivalent_type_names_are_duplicates() {
let output = compile_text("mod M\nenum é:\n A\nenum e\u{301}:\n B\n");
assert!(codes(&output).contains(&"C0001"));
}
#[test]
fn reports_unknown_type_with_suggestion() {
let output = compile_text("mod M\nrecord Student:\n score: Intr\n");
let diagnostic = output
.diagnostics
.iter()
.find(|diagnostic| diagnostic.code == "C0301")
.unwrap();
assert!(diagnostic.help.as_deref().unwrap().contains("Int"));
}
#[test]
fn rejects_reversed_range() {
let output = compile_text("mod M\nrecord Student:\n score: Int 100..0\n");
assert!(codes(&output).contains(&"C0203"));
}
#[test]
fn rejects_decimal_bounds_on_int_range() {
let output = compile_text("mod M\nrecord Student:\n score: Int 0.5..100\n");
assert!(codes(&output).contains(&"C0202"));
}
#[test]
fn detects_derive_cycle() {
let output = compile_text("mod M\nfn a() -> Int:\n b()\nfn b() -> Int:\n a()\n");
assert!(codes(&output).contains(&"C0900"));
}
#[test]
fn detects_override_cycle() {
let output = compile_text(
r"mod M
policy::cycle @cycle:
Cycle policy.
rule a():
=> override b
rule b():
=> override a
",
);
assert!(codes(&output).contains(&"C0901"));
}
#[test]
fn detects_same_condition_static_conflict() {
let output = compile_text(
r"mod M
enum Grade:
A
F
record Student:
score: Int
dec grade(s Student) -> Grade
policy::grading @grading:
Grading policy.
rule a(s Student):
s.score >= 0 => grade(s) = A
rule f(s Student):
s.score >= 0 => grade(s) = F
",
);
assert!(codes(&output).contains(&"C0501"));
}
#[test]
fn many_decision_allows_distinct_candidates_under_the_same_condition() {
let output = compile_text(
r"mod M
enum Label:
A
B
C
record Subject:
flag: Bool
dec labels(s Subject) -> Label*
policy::labels @labels:
Label policy.
rule first_a(s Subject):
s.flag => labels(s) = A
rule first_b(s Subject):
s.flag => labels(s) = B
rule second(s Subject):
s.flag => labels(s) = C
",
);
assert!(!codes(&output).contains(&"C0501"));
assert!(!output.has_errors(), "{:?}", output.diagnostics);
}
#[test]
fn zero_or_one_decision_rejects_distinct_candidates_under_the_same_condition() {
let output = compile_text(
r"mod M
enum Label:
A
B
record Subject:
flag: Bool
dec label(s Subject) -> Label?
policy::labels @labels:
Optional label policy.
rule first(s Subject):
s.flag => label(s) = A
rule second(s Subject):
s.flag => label(s) = B
",
);
assert!(codes(&output).contains(&"C0501"));
}
#[test]
fn allows_a_guaranteed_explicit_override() {
let output = compile_text(
r"mod M
enum Grade:
A
F
record Student:
score: Int
dec grade(s Student) -> Grade
policy::grading @grading:
Grading exception policy.
rule base(s Student):
s.score >= 0 => grade(s) = F
rule exception(s Student):
s.score >= 0 => grade(s) = A
rule exception_override(s Student):
s.score >= 0 => override base
",
);
assert!(!codes(&output).contains(&"C0501"));
}
#[test]
fn validates_date_and_duration_builtins() {
let output = compile_text(
r#"mod M
fn epoch() -> Date:
date("2026-07-11")
fn grace() -> Duration:
days(3)
fn magnitude(value Int) -> Int:
abs(value)
"#,
);
assert!(!output.has_errors(), "{:?}", output.diagnostics);
}
#[test]
fn rejects_wrong_builtin_argument_type() {
let output = compile_text("mod M\nfn grace() -> Duration:\n days(\"three\")\n");
assert!(codes(&output).contains(&"C0806"));
}
#[test]
fn rejects_missing_required_case_field() {
let output = compile_text(
r"mod M
enum Grade:
A
record Student:
score: Int
excused: Bool?
dec grade(s Student) -> Grade
test sample:
let s = Student { excused: false }
expect grade(s) = A
",
);
assert!(codes(&output).contains(&"C0604"));
}
#[test]
fn accepts_int_for_decimal_field_and_checks_range() {
let output = compile_text(
r"mod M
enum Grade:
A
record Student:
score: Decimal 0..100
dec grade(s Student) -> Grade
test sample:
let s = Student { score: 10 }
expect grade(s) = A
",
);
assert!(!output.has_errors(), "{:?}", output.diagnostics);
}
#[test]
fn validates_finite_string_and_decimal_domains() {
let valid = compile_text(
r#"mod m
record E:
status: String {"draft", "approved"}
rate: Decimal {0.05, 0.10}
test ok:
let e = E { status: "draft", rate: 0.05 }
"#,
);
assert!(!valid.has_errors(), "{:?}", valid.diagnostics);
let invalid = compile_text(
r#"mod m
record E:
status: String {"draft", "draft"}
rate: Decimal {0.05, 0.10}
test bad:
let e = E { status: "other", rate: 0.20 }
"#,
);
let codes = codes(&invalid);
assert!(codes.contains(&"C0209"), "{codes:?}");
assert_eq!(codes.iter().filter(|code| **code == "C0610").count(), 2);
}
#[test]
fn rejects_case_literal_outside_declared_range() {
let output = compile_text(
r"mod M
enum Grade:
A
record Student:
score: Int 0..100
dec grade(s Student) -> Grade
test sample:
let s = Student { score: 101 }
expect grade(s) = A
",
);
assert!(codes(&output).contains(&"C0605"));
}
#[test]
fn diagnostic_order_is_source_stable() {
let output = compile_text("mod M\nrecord E:\n a: Missing\n b: AlsoMissing\n");
let starts = output
.diagnostics
.iter()
.map(|diagnostic| diagnostic.primary.start)
.collect::<Vec<_>>();
assert!(starts.windows(2).all(|pair| pair[0] <= pair[1]));
}
#[test]
fn rejects_scalar_decision_and_rule_parameters() {
let output = compile_text(
r"mod M
dec d(value Int) -> Bool
policy::scalar @scalar:
Scalar policy.
rule r(value Int):
=> d(value) = true
",
);
assert!(codes(&output).contains(&"C0302"));
}
#[test]
fn allows_an_unconditional_zero_argument_constant_decision() {
let output = compile_text(
r"mod Hello
enum Greeting:
Hello
dec greeting() -> Greeting
policy::hello @hello:
Hello policy.
rule hello():
=> greeting() = Hello
test first:
expect greeting() = Hello
",
);
assert!(!output.has_errors(), "{:?}", output.diagnostics);
assert!(!codes(&output).contains(&"W0401"));
}
#[test]
fn rejects_entity_valued_fields_and_decision_results() {
let output = compile_text(
r"mod M
record Child:
value: Int
record Parent:
child: Child
dec d(p Parent) -> Child
",
);
assert!(codes(&output).contains(&"C0204"));
assert!(codes(&output).contains(&"C0303"));
}
#[test]
fn validates_every_quantified_invariant_variable() {
let valid = compile_text(
r"mod M
record Order:
amount: Int
record Customer:
member: Bool
dec eligible(o Order, c Customer) -> Bool
assert total(o Order, c Customer):
eligible(o, c)
",
);
assert!(!valid.has_errors(), "{:?}", valid.diagnostics);
let invalid = compile_text(
r"mod M
record Order:
amount: Int
dec eligible(o Order) -> Bool
assert invalid(o Order, n Int):
eligible(o)
",
);
assert!(codes(&invalid).contains(&"C0700"));
}
#[test]
fn validates_existential_invariant_variables_and_arguments() {
let valid = compile_text(
r"mod M
enum Result:
Yes
No
record Order:
amount: Int
dec eligible(o Order) -> Result
assert some witness(o Order):
o.amount > 0 => eligible(o) = Yes
",
);
assert!(!valid.has_errors(), "{:?}", valid.diagnostics);
assert_eq!(
valid
.program
.as_ref()
.and_then(|program| program.invariant("witness"))
.map(|invariant| invariant.quantifier),
Some(crate::ast::InvariantQuantifier::Some)
);
let invalid = compile_text(
r"mod M
enum Result:
Yes
record Order:
amount: Int
fn identity(o Order) -> Order:
o
dec eligible(o Order) -> Result
assert some witness(o Order):
eligible(identity(o))
",
);
let diagnostic = invalid
.diagnostics
.iter()
.find(|diagnostic| diagnostic.code == "C0701")
.expect("direct existential binding diagnostic");
assert!(
diagnostic
.help
.as_deref()
.is_some_and(|help| help.contains("assertion's `(name Record)` header"))
);
}
#[test]
fn rejects_duplicate_quantified_invariant_variables() {
let output = compile_text(
r"mod M
record Order:
amount: Int
dec eligible(o Order) -> Bool
assert duplicate(o Order, o Order):
eligible(o)
",
);
assert!(codes(&output).contains(&"C0300"));
}
#[test]
fn rejects_computed_invariant_decision_arguments_before_runtime() {
let output = compile_text(
r"mod M
enum Result:
Yes
record E:
value: Int
fn identity(e E) -> E:
e
dec d(e E) -> Result
assert totality(e E):
d(identity(e))
assert expected_result(e E):
true => d(identity(e)) = Yes
",
);
assert_eq!(
output
.diagnostics
.iter()
.filter(|diagnostic| diagnostic.code == "C0701")
.count(),
2
);
}
#[test]
fn rejects_decision_calls_inside_expressions() {
let output = compile_text(
r"mod M
record E:
value: Int
dec first(s E) -> Bool
fn chained(s E) -> Bool:
first(s)
",
);
assert!(codes(&output).contains(&"C0809"));
}
#[test]
fn namespaces_nested_rules_by_fragment_id_and_resolves_overrides() {
let output = compile_text(
r"record Request:
enabled: Bool
dec result(request Request) -> Bool
policy::a:
Source A.
rule base(request Request):
request.enabled => result(request) = true
rule sibling(request Request):
not request.enabled => override base
policy::b:
Source B.
ref policy::a
rule base(request Request):
not request.enabled => result(request) = false
rule cross(request Request):
request.enabled => override policy::a::base
",
);
assert!(!output.has_errors(), "{:?}", output.diagnostics);
let program = output.program.unwrap();
assert!(program.rule("policy::a::base").is_some());
assert!(program.rule("policy::b::base").is_some());
let Effect::Override { rule, .. } = &program.rule("policy::a::sibling").unwrap().effect
else {
panic!("expected override");
};
assert_eq!(rule.value, "policy::a::base");
let Effect::Override { rule, .. } = &program.rule("policy::b::cross").unwrap().effect
else {
panic!("expected override");
};
assert_eq!(rule.value, "policy::a::base");
}
#[test]
fn namespaces_fragment_functions_and_resolves_local_and_qualified_calls() {
let output = compile_text(
r"record Request:
value: Decimal
dec result(request Request) -> Bool
law::definitions:
Definitions.
law::formula:
The adjusted amount is seventy percent of the stated value.
fn adjusted(request Request) -> Decimal:
basis law::definitions
request.value * 0.7
fn eligible(request Request) -> Bool:
adjusted(request) >= 10
rule local(request Request):
eligible(request) => result(request) = true
rule external(request Request):
law::formula::eligible(request) => result(request) = true
",
);
assert!(!output.has_errors(), "{:?}", output.diagnostics);
let program = output.program.unwrap();
let adjusted = program.derive("law::formula::adjusted").unwrap();
assert_eq!(adjusted.basis.len(), 2);
assert_eq!(adjusted.basis[0].id.value, "law::formula");
assert_eq!(adjusted.basis[1].id.value, "law::definitions");
let eligible = program.derive("law::formula::eligible").unwrap();
let ExprKind::Binary { left, .. } = &eligible.expression.kind else {
panic!("expected comparison");
};
let ExprKind::Call { callee, .. } = &left.kind else {
panic!("expected local function call");
};
assert_eq!(callee.value, "law::formula::adjusted");
let local = program.rule("law::formula::local").unwrap();
let ExprKind::Call { callee, .. } = &local.condition.kind else {
panic!("expected local rule call");
};
assert_eq!(callee.value, "law::formula::eligible");
assert_eq!(
local
.basis
.iter()
.map(|basis| basis.id.value.as_str())
.collect::<Vec<_>>(),
["law::formula", "law::definitions"]
);
assert_eq!(
program
.rule("external")
.unwrap()
.basis
.iter()
.map(|basis| basis.id.value.as_str())
.collect::<Vec<_>>(),
["law::formula", "law::definitions"]
);
}
#[test]
fn rejects_duplicate_nested_rule_and_fragment_references() {
let output = compile_text(
r"policy::a:
Source A.
ref policy::missing
ref policy::missing
rule duplicate():
=> result() = true
rule duplicate():
=> result() = false
",
);
let codes = codes(&output);
assert!(codes.contains(&"C0001"), "{codes:?}");
assert!(codes.contains(&"C0412"), "{codes:?}");
assert!(codes.contains(&"C0413"), "{codes:?}");
}
}