use crate::ast::{
ActionDecl, BinaryOp, BindingDecl, CaseDecl, DecisionDecl, Declaration, DeriveDecl, Effect,
EntityDecl, EnumDecl, Expectation, Expr, ExprKind, FieldDecl, FixtureDecl, 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;
const BUILTIN_TYPE_NAMES: [&str; 6] = ["Bool", "Int", "Decimal", "String", "Date", "Duration"];
const BUILTIN_CALLABLE_NAMES: [&str; 7] = ["date", "days", "hours", "minutes", "abs", "max", "min"];
fn is_builtin_type_name(name: &str) -> bool {
BUILTIN_TYPE_NAMES.contains(&name)
}
fn is_builtin_callable_name(name: &str) -> bool {
BUILTIN_CALLABLE_NAMES.contains(&name)
}
#[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 fixtures: BTreeMap<String, FixtureDecl>,
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 fixtures(&self) -> &BTreeMap<String, FixtureDecl> {
&self.fixtures
}
#[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 fixture(&self, name: &str) -> Option<&FixtureDecl> {
self.fixtures.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)
}
}
#[derive(Clone, Debug)]
struct SymbolCandidate {
name: String,
span: Span,
}
#[derive(Default)]
struct NameIndex {
exact: BTreeMap<String, SymbolCandidate>,
by_leaf: BTreeMap<String, Vec<SymbolCandidate>>,
}
enum NameResolution {
Found(String),
Ambiguous(Vec<SymbolCandidate>),
Missing,
}
impl NameIndex {
fn insert(&mut self, name: &str, span: Span) {
let candidate = SymbolCandidate {
name: name.to_owned(),
span,
};
self.exact
.entry(normalize_name(name))
.or_insert_with(|| candidate.clone());
if is_private_package_symbol(name) {
return;
}
let leaf = name.rsplit('.').next().unwrap_or(name);
self.by_leaf
.entry(normalize_name(leaf))
.or_default()
.push(candidate);
}
fn finish(&mut self) {
for candidates in self.by_leaf.values_mut() {
candidates.sort_by(|left, right| {
normalize_name(&left.name)
.cmp(&normalize_name(&right.name))
.then_with(|| left.name.cmp(&right.name))
});
candidates
.dedup_by(|left, right| normalize_name(&left.name) == normalize_name(&right.name));
}
}
fn resolve(&self, name: &str) -> NameResolution {
let normalized = normalize_name(name);
if let Some(candidate) = self.exact.get(&normalized) {
return NameResolution::Found(candidate.name.clone());
}
if name.contains('.') {
return NameResolution::Missing;
}
match self.by_leaf.get(&normalized).map(Vec::as_slice) {
Some([candidate]) => NameResolution::Found(candidate.name.clone()),
Some(candidates) if candidates.len() > 1 => {
NameResolution::Ambiguous(candidates.to_vec())
}
_ => NameResolution::Missing,
}
}
}
fn is_private_package_symbol(name: &str) -> bool {
name.split('.').any(|segment| segment.starts_with("@pkg_"))
}
#[derive(Default)]
struct ProgramNameIndexes {
types: NameIndex,
enums: NameIndex,
entities: NameIndex,
callables: NameIndex,
decisions: NameIndex,
actions: NameIndex,
rules: NameIndex,
fixtures: NameIndex,
fragments: NameIndex,
}
impl ProgramNameIndexes {
fn collect(program: &Program) -> Self {
let mut indexes = Self::default();
for declaration in &program.declarations {
match declaration {
Declaration::Enum(value) => {
indexes.types.insert(&value.name.value, value.name.span);
indexes.enums.insert(&value.name.value, value.name.span);
}
Declaration::Entity(value) => {
indexes.types.insert(&value.name.value, value.name.span);
indexes.entities.insert(&value.name.value, value.name.span);
}
Declaration::State(value) => {
indexes.types.insert(&value.name.value, value.name.span);
}
Declaration::Derive(value) => {
indexes.callables.insert(&value.name.value, value.name.span);
}
Declaration::Decision(value) => {
indexes.callables.insert(&value.name.value, value.name.span);
indexes.decisions.insert(&value.name.value, value.name.span);
}
Declaration::Action(value) => {
indexes.actions.insert(&value.name.value, value.name.span);
}
Declaration::Fragment(value) => {
indexes.fragments.insert(&value.id.value, value.id.span);
for derive in &value.derives {
indexes.callables.insert(
&format!("{}.{}", value.id.value, derive.name.value),
derive.name.span,
);
}
for rule in &value.rules {
indexes.rules.insert(
&format!("{}.{}", value.id.value, rule.name.value),
rule.name.span,
);
}
}
Declaration::Rule(value) => {
indexes.rules.insert(&value.name.value, value.name.span);
}
Declaration::Fixture(value) => {
indexes.fixtures.insert(&value.name.value, value.name.span);
}
Declaration::Transition(_)
| Declaration::Case(_)
| Declaration::Invariant(_)
| Declaration::Trace(_) => {}
}
}
indexes.types.finish();
indexes.enums.finish();
indexes.entities.finish();
indexes.callables.finish();
indexes.decisions.finish();
indexes.actions.finish();
indexes.rules.finish();
indexes.fixtures.finish();
indexes.fragments.finish();
indexes
}
}
fn canonicalize_short_symbol_references(
program: &mut Program,
diagnostics: &mut Vec<Diagnostic>,
) -> BTreeSet<(usize, usize)> {
let indexes = ProgramNameIndexes::collect(program);
let mut ambiguous_spans = BTreeSet::new();
for declaration in &mut program.declarations {
canonicalize_declaration_symbol_references(
declaration,
&indexes,
diagnostics,
&mut ambiguous_spans,
);
}
ambiguous_spans
}
fn canonicalize_declaration_symbol_references(
declaration: &mut Declaration,
indexes: &ProgramNameIndexes,
diagnostics: &mut Vec<Diagnostic>,
ambiguous_spans: &mut BTreeSet<(usize, usize)>,
) {
match declaration {
Declaration::Enum(_) => {}
Declaration::Entity(value) => {
for field in &mut value.fields {
canonicalize_field_symbol_references(
field,
indexes,
diagnostics,
ambiguous_spans,
None,
);
}
}
Declaration::State(value) => {
for field in &mut value.fields {
canonicalize_field_symbol_references(
field,
indexes,
diagnostics,
ambiguous_spans,
None,
);
}
}
Declaration::Derive(value) => canonicalize_derive_symbol_references(
value,
indexes,
diagnostics,
ambiguous_spans,
None,
),
Declaration::Decision(value) => {
canonicalize_parameters(&mut value.parameters, indexes, diagnostics, ambiguous_spans);
canonicalize_type_ref(
&mut value.return_type.value,
value.return_type.span,
indexes,
diagnostics,
ambiguous_spans,
);
}
Declaration::Action(value) => {
canonicalize_parameters(&mut value.parameters, indexes, diagnostics, ambiguous_spans);
}
Declaration::Transition(value) => {
canonicalize_parameters(&mut value.parameters, indexes, diagnostics, ambiguous_spans);
canonicalize_spanned_reference(
&mut value.action,
&indexes.actions,
"action",
&[],
diagnostics,
ambiguous_spans,
);
for argument in &mut value.action_arguments {
canonicalize_expression_symbol_references(
argument,
indexes,
diagnostics,
ambiguous_spans,
None,
);
}
canonicalize_expression_symbol_references(
&mut value.condition,
indexes,
diagnostics,
ambiguous_spans,
None,
);
for update in &mut value.updates {
canonicalize_expression_symbol_references(
&mut update.value,
indexes,
diagnostics,
ambiguous_spans,
None,
);
}
}
Declaration::Fragment(value) => {
for reference in &mut value.refs {
canonicalize_spanned_reference(
&mut reference.id,
&indexes.fragments,
"fragment",
&[],
diagnostics,
ambiguous_spans,
);
}
let local_callables = value
.derives
.iter()
.map(|derive| {
(
normalize_name(&derive.name.value),
format!("{}.{}", value.id.value, derive.name.value),
)
})
.collect::<BTreeMap<_, _>>();
let local_rules = value
.rules
.iter()
.map(|rule| {
(
normalize_name(&rule.name.value),
format!("{}.{}", value.id.value, rule.name.value),
)
})
.collect::<BTreeMap<_, _>>();
for derive in &mut value.derives {
canonicalize_derive_symbol_references(
derive,
indexes,
diagnostics,
ambiguous_spans,
Some(&local_callables),
);
}
for rule in &mut value.rules {
canonicalize_rule_symbol_references(
rule,
indexes,
diagnostics,
ambiguous_spans,
Some(&local_callables),
Some(&local_rules),
);
}
}
Declaration::Rule(value) => canonicalize_rule_symbol_references(
value,
indexes,
diagnostics,
ambiguous_spans,
None,
None,
),
Declaration::Fixture(value) => {
canonicalize_spanned_reference(
&mut value.entity,
&indexes.entities,
"record",
&[],
diagnostics,
ambiguous_spans,
);
for field in &mut value.fields {
canonicalize_expression_symbol_references(
&mut field.value,
indexes,
diagnostics,
ambiguous_spans,
None,
);
}
}
Declaration::Case(value) => {
for binding in &mut value.bindings {
canonicalize_spanned_reference(
&mut binding.entity,
&indexes.entities,
"record",
&[],
diagnostics,
ambiguous_spans,
);
if let Some(fixture) = &mut binding.fixture {
canonicalize_spanned_reference(
fixture,
&indexes.fixtures,
"fixture",
&[],
diagnostics,
ambiguous_spans,
);
}
for field in &mut binding.fields {
canonicalize_expression_symbol_references(
&mut field.value,
indexes,
diagnostics,
ambiguous_spans,
None,
);
}
}
for expectation in &mut value.expectations {
canonicalize_expectation_symbol_references(
expectation,
indexes,
diagnostics,
ambiguous_spans,
);
}
}
Declaration::Invariant(value) => {
canonicalize_parameters(&mut value.variables, indexes, diagnostics, ambiguous_spans);
match &mut value.assertion {
InvariantAssertion::Cardinality {
decision,
arguments,
..
} => {
canonicalize_spanned_reference(
decision,
&indexes.decisions,
"decision",
&[],
diagnostics,
ambiguous_spans,
);
for argument in arguments {
canonicalize_expression_symbol_references(
argument,
indexes,
diagnostics,
ambiguous_spans,
None,
);
}
}
InvariantAssertion::Implication {
condition,
expectation,
..
} => {
canonicalize_expression_symbol_references(
condition,
indexes,
diagnostics,
ambiguous_spans,
None,
);
canonicalize_expectation_symbol_references(
expectation,
indexes,
diagnostics,
ambiguous_spans,
);
}
}
}
Declaration::Trace(value) => {
canonicalize_parameter(&mut value.variable, indexes, diagnostics, ambiguous_spans);
canonicalize_expression_symbol_references(
&mut value.initial,
indexes,
diagnostics,
ambiguous_spans,
None,
);
for expression in &mut value.always {
canonicalize_expression_symbol_references(
expression,
indexes,
diagnostics,
ambiguous_spans,
None,
);
}
if let Some(expression) = &mut value.terminal {
canonicalize_expression_symbol_references(
expression,
indexes,
diagnostics,
ambiguous_spans,
None,
);
}
}
}
}
fn canonicalize_field_symbol_references(
field: &mut FieldDecl,
indexes: &ProgramNameIndexes,
diagnostics: &mut Vec<Diagnostic>,
ambiguous_spans: &mut BTreeSet<(usize, usize)>,
local_callables: Option<&BTreeMap<String, String>>,
) {
canonicalize_type_ref(
&mut field.ty,
field.span,
indexes,
diagnostics,
ambiguous_spans,
);
if let Some(domain) = &mut field.domain {
for value in &mut domain.values {
canonicalize_expression_symbol_references(
value,
indexes,
diagnostics,
ambiguous_spans,
local_callables,
);
}
}
}
fn canonicalize_derive_symbol_references(
derive: &mut DeriveDecl,
indexes: &ProgramNameIndexes,
diagnostics: &mut Vec<Diagnostic>,
ambiguous_spans: &mut BTreeSet<(usize, usize)>,
local_callables: Option<&BTreeMap<String, String>>,
) {
canonicalize_parameters(
&mut derive.parameters,
indexes,
diagnostics,
ambiguous_spans,
);
canonicalize_type_ref(
&mut derive.return_type.value,
derive.return_type.span,
indexes,
diagnostics,
ambiguous_spans,
);
canonicalize_expression_symbol_references(
&mut derive.expression,
indexes,
diagnostics,
ambiguous_spans,
local_callables,
);
for reference in &mut derive.basis {
canonicalize_spanned_reference(
&mut reference.id,
&indexes.fragments,
"fragment",
&[],
diagnostics,
ambiguous_spans,
);
}
}
fn canonicalize_rule_symbol_references(
rule: &mut RuleDecl,
indexes: &ProgramNameIndexes,
diagnostics: &mut Vec<Diagnostic>,
ambiguous_spans: &mut BTreeSet<(usize, usize)>,
local_callables: Option<&BTreeMap<String, String>>,
local_rules: Option<&BTreeMap<String, String>>,
) {
canonicalize_parameters(&mut rule.parameters, indexes, diagnostics, ambiguous_spans);
canonicalize_expression_symbol_references(
&mut rule.condition,
indexes,
diagnostics,
ambiguous_spans,
local_callables,
);
match &mut rule.effect {
Effect::Decide {
decision,
arguments,
value,
..
} => {
canonicalize_spanned_reference(
decision,
&indexes.decisions,
"decision",
&[],
diagnostics,
ambiguous_spans,
);
for argument in arguments {
canonicalize_expression_symbol_references(
argument,
indexes,
diagnostics,
ambiguous_spans,
local_callables,
);
}
canonicalize_expression_symbol_references(
value,
indexes,
diagnostics,
ambiguous_spans,
local_callables,
);
}
Effect::Override { rule, .. } => {
let local = if rule.value.contains('.') {
None
} else {
local_rules.and_then(|rules| rules.get(&normalize_name(&rule.value)))
};
if let Some(local) = local {
rule.value.clone_from(local);
} else {
canonicalize_spanned_reference(
rule,
&indexes.rules,
"rule",
&[],
diagnostics,
ambiguous_spans,
);
}
}
Effect::Invalid { .. } => {}
}
for reference in &mut rule.basis {
canonicalize_spanned_reference(
&mut reference.id,
&indexes.fragments,
"fragment",
&[],
diagnostics,
ambiguous_spans,
);
}
}
fn canonicalize_expectation_symbol_references(
expectation: &mut Expectation,
indexes: &ProgramNameIndexes,
diagnostics: &mut Vec<Diagnostic>,
ambiguous_spans: &mut BTreeSet<(usize, usize)>,
) {
canonicalize_spanned_reference(
&mut expectation.decision,
&indexes.decisions,
"decision",
&[],
diagnostics,
ambiguous_spans,
);
for argument in &mut expectation.arguments {
canonicalize_expression_symbol_references(
argument,
indexes,
diagnostics,
ambiguous_spans,
None,
);
}
canonicalize_expression_symbol_references(
&mut expectation.value,
indexes,
diagnostics,
ambiguous_spans,
None,
);
}
fn canonicalize_parameters(
parameters: &mut [Parameter],
indexes: &ProgramNameIndexes,
diagnostics: &mut Vec<Diagnostic>,
ambiguous_spans: &mut BTreeSet<(usize, usize)>,
) {
for parameter in parameters {
canonicalize_parameter(parameter, indexes, diagnostics, ambiguous_spans);
}
}
fn canonicalize_parameter(
parameter: &mut Parameter,
indexes: &ProgramNameIndexes,
diagnostics: &mut Vec<Diagnostic>,
ambiguous_spans: &mut BTreeSet<(usize, usize)>,
) {
canonicalize_spanned_reference(
&mut parameter.ty,
&indexes.types,
"type",
&BUILTIN_TYPE_NAMES,
diagnostics,
ambiguous_spans,
);
}
fn canonicalize_type_ref(
ty: &mut TypeRef,
span: Span,
indexes: &ProgramNameIndexes,
diagnostics: &mut Vec<Diagnostic>,
ambiguous_spans: &mut BTreeSet<(usize, usize)>,
) {
let TypeRef::Named(name) = ty else {
return;
};
canonicalize_string_reference(
name,
span,
&indexes.types,
"type",
&BUILTIN_TYPE_NAMES,
diagnostics,
ambiguous_spans,
);
}
fn canonicalize_expression_symbol_references(
expression: &mut Expr,
indexes: &ProgramNameIndexes,
diagnostics: &mut Vec<Diagnostic>,
ambiguous_spans: &mut BTreeSet<(usize, usize)>,
local_callables: Option<&BTreeMap<String, String>>,
) {
match &mut expression.kind {
ExprKind::Literal(_) | ExprKind::Name(_) => {}
ExprKind::Field { receiver, .. } => canonicalize_expression_symbol_references(
receiver,
indexes,
diagnostics,
ambiguous_spans,
local_callables,
),
ExprKind::Call { callee, arguments } => {
let local = if callee.value.contains('.') {
None
} else {
local_callables.and_then(|callables| callables.get(&normalize_name(&callee.value)))
};
if let Some(local) = local {
callee.value.clone_from(local);
} else {
canonicalize_spanned_reference(
callee,
&indexes.callables,
"callable",
&BUILTIN_CALLABLE_NAMES,
diagnostics,
ambiguous_spans,
);
}
for argument in arguments {
canonicalize_expression_symbol_references(
argument,
indexes,
diagnostics,
ambiguous_spans,
local_callables,
);
}
}
ExprKind::Unary { operand, .. } => canonicalize_expression_symbol_references(
operand,
indexes,
diagnostics,
ambiguous_spans,
local_callables,
),
ExprKind::Binary { left, right, .. } => {
canonicalize_expression_symbol_references(
left,
indexes,
diagnostics,
ambiguous_spans,
local_callables,
);
canonicalize_expression_symbol_references(
right,
indexes,
diagnostics,
ambiguous_spans,
local_callables,
);
}
}
}
fn canonicalize_spanned_reference(
reference: &mut Spanned<String>,
index: &NameIndex,
kind: &str,
reserved: &[&str],
diagnostics: &mut Vec<Diagnostic>,
ambiguous_spans: &mut BTreeSet<(usize, usize)>,
) {
canonicalize_string_reference(
&mut reference.value,
reference.span,
index,
kind,
reserved,
diagnostics,
ambiguous_spans,
);
}
fn canonicalize_string_reference(
reference: &mut String,
span: Span,
index: &NameIndex,
kind: &str,
reserved: &[&str],
diagnostics: &mut Vec<Diagnostic>,
ambiguous_spans: &mut BTreeSet<(usize, usize)>,
) {
if reserved
.iter()
.any(|reserved| normalize_name(reserved) == normalize_name(reference))
{
return;
}
match index.resolve(reference) {
NameResolution::Found(canonical) => reference.clone_from(&canonical),
NameResolution::Ambiguous(candidates) => {
let names = candidates
.iter()
.map(|candidate| format!("`{}`", display_symbol_name(&candidate.name)))
.collect::<Vec<_>>()
.join(", ");
let mut diagnostic =
Diagnostic::error("C0808", format!("ambiguous {kind} `{reference}`"), span)
.with_help(format!("use a qualified name; candidates are {names}"));
for candidate in candidates {
diagnostic = diagnostic.with_label(
candidate.span,
format!(
"candidate `{}` is declared here",
display_symbol_name(&candidate.name)
),
);
}
diagnostics.push(diagnostic);
ambiguous_spans.insert((span.start, span.end));
}
NameResolution::Missing => {}
}
}
#[derive(Clone)]
struct ImplicitEntityName {
qualified: String,
local: String,
}
fn elaborate_implicit_bindings(program: &mut Program, diagnostics: &mut Vec<Diagnostic>) {
let entities = program
.declarations
.iter()
.filter_map(|declaration| match declaration {
Declaration::Entity(entity) => Some(ImplicitEntityName {
qualified: entity.name.value.clone(),
local: entity
.name
.value
.rsplit('.')
.next()
.expect("an entity name has a final segment")
.to_owned(),
}),
_ => None,
})
.collect::<Vec<_>>();
for_each_rule_mut(program, |rule, _| {
if !rule.implicit_parameters {
return;
}
let mut parameters = rule.parameters.clone();
collect_implicit_parameters(&rule.condition, &entities, &mut parameters);
if let Effect::Decide {
arguments, value, ..
} = &rule.effect
{
for argument in arguments {
collect_implicit_argument_parameter(argument, &entities, &mut parameters);
}
collect_implicit_parameters(value, &entities, &mut parameters);
}
rule.parameters = parameters;
});
let deciding_rules = collect_rules(program);
for declaration in &mut program.declarations {
let Declaration::Decision(decision) = declaration else {
continue;
};
if !decision.implicit_parameters {
continue;
}
let decision_key = normalize_name(&decision.name.value);
let mut signatures = deciding_rules
.iter()
.filter_map(|rule| {
let Effect::Decide {
decision: target,
arguments,
implicit_arguments,
span,
..
} = &rule.effect
else {
return None;
};
if normalize_name(&target.value) != decision_key {
return None;
}
let inherits_decision_signature =
*implicit_arguments && rule.implicit_parameters && rule.parameters.is_empty();
let signature = if *implicit_arguments {
Some(rule.parameters.clone())
} else {
parameters_for_explicit_arguments(rule, arguments)
};
Some((
signature,
*span,
rule.name.value.clone(),
inherits_decision_signature,
))
})
.collect::<Vec<_>>();
signatures.sort_by_key(|signature| signature.1.start);
let has_deciding_rule = !signatures.is_empty();
let only_inherited_signatures = signatures.iter().all(|signature| signature.3);
let mut inferred: Option<Vec<Parameter>> = None;
for (signature, span, rule_name, inherits_decision_signature) in signatures {
if inherits_decision_signature {
continue;
}
let Some(signature) = signature else {
diagnostics.push(
Diagnostic::error(
"C0305",
format!(
"cannot infer decision `{}` inputs from rule `{rule_name}`",
decision.name.value
),
span,
)
.with_help(
"declare the decision parameters explicitly or pass rule parameters directly",
),
);
continue;
};
if let Some(previous) = &inferred {
if parameter_type_names(previous) != parameter_type_names(&signature) {
diagnostics.push(
Diagnostic::error(
"C0305",
format!(
"rules infer incompatible inputs for decision `{}`",
decision.name.value
),
span,
)
.with_label(decision.name.span, "this decision omits its input signature")
.with_help(
"write an explicit decision parameter list and explicit effect arguments",
),
);
}
} else {
inferred = Some(signature);
}
}
if !has_deciding_rule {
diagnostics.push(
Diagnostic::error(
"C0305",
format!(
"cannot infer inputs for decision `{}` without a deciding rule",
decision.name.value
),
decision.name.span,
)
.with_help(
"add a rule that exposes the input records, declare the parameters explicitly, or use `()` for a true zero-argument decision",
),
);
} else if inferred.as_ref().is_some_and(Vec::is_empty) || only_inherited_signatures {
diagnostics.push(
Diagnostic::error(
"C0305",
format!(
"decision `{}` omitted its inputs, but its rules expose none",
decision.name.value
),
decision.name.span,
)
.with_help(
"declare the decision and rule parameters explicitly, or use `()` on both for a true zero-argument decision",
),
);
}
decision.parameters = inferred.unwrap_or_default();
}
let decisions = program
.declarations
.iter()
.filter_map(|declaration| match declaration {
Declaration::Decision(decision) => {
Some((normalize_name(&decision.name.value), decision.clone()))
}
_ => None,
})
.collect::<BTreeMap<_, _>>();
let rule_signatures = collect_rule_signatures(program);
for_each_rule_mut(program, |rule, enclosing_fragment| {
if !rule.implicit_parameters {
return;
}
let required = match &rule.effect {
Effect::Decide { decision, .. } => decisions
.get(&normalize_name(&decision.value))
.map(|decision| decision.parameters.as_slice()),
Effect::Override { rule: target, .. } => {
let target = if target.value.contains('.') {
target.value.clone()
} else if let Some(fragment) = enclosing_fragment {
format!("{fragment}.{}", target.value)
} else {
target.value.clone()
};
rule_signatures
.get(&normalize_name(&target))
.map(Vec::as_slice)
}
Effect::Invalid { .. } => None,
};
if let Some(required) = required {
if let Some(ambiguous_type) = merge_required_parameters(&mut rule.parameters, required)
{
diagnostics.push(
Diagnostic::error(
"C0306",
format!(
"cannot infer inputs for rule `{}` because `{ambiguous_type}` occurs more than once",
rule.name.value
),
rule.span,
)
.with_help(
"write the rule parameter list and decision arguments explicitly when the same record type has more than one role",
),
);
}
}
});
for_each_rule_mut(program, |rule, _| {
let Effect::Decide {
decision,
arguments,
implicit_arguments,
span,
..
} = &mut rule.effect
else {
return;
};
if !*implicit_arguments {
return;
}
let Some(target) = decisions.get(&normalize_name(&decision.value)) else {
return;
};
let mut inferred = Vec::new();
let mut used = BTreeSet::new();
for (index, parameter) in target.parameters.iter().enumerate() {
let matching_name =
rule.parameters
.iter()
.enumerate()
.find(|(candidate_index, candidate)| {
!used.contains(candidate_index)
&& normalize_name(&candidate.name.value)
== normalize_name(¶meter.name.value)
&& normalize_name(&candidate.ty.value)
== normalize_name(¶meter.ty.value)
});
let same_type = rule
.parameters
.iter()
.enumerate()
.filter(|(candidate_index, _)| !used.contains(candidate_index))
.filter(|candidate| {
normalize_name(&candidate.1.ty.value) == normalize_name(¶meter.ty.value)
})
.collect::<Vec<_>>();
let selected = matching_name.or_else(|| (same_type.len() == 1).then(|| same_type[0]));
let Some((selected_index, selected)) = selected else {
diagnostics.push(
Diagnostic::error(
"C0306",
format!(
"cannot infer argument {} for decision `{}` in rule `{}`",
index + 1,
decision.value,
rule.name.value
),
*span,
)
.with_label(target.name.span, "decision signature is declared here")
.with_help("write the decision arguments explicitly in this rule effect"),
);
continue;
};
used.insert(selected_index);
inferred.push(Expr::new(
ExprKind::Name(selected.name.value.clone()),
selected.name.span,
));
}
*arguments = inferred;
});
for_each_rule_mut(program, |rule, _| {
if rule.implicit_parameters && rule.parameters.is_empty() {
diagnostics.push(
Diagnostic::error(
"C0305",
format!("cannot infer inputs for rule `{}`", rule.name.value),
rule.name.span,
)
.with_help(
"reference a record input in the rule, declare the parameters explicitly, or use `()` for a true zero-argument rule",
),
);
}
});
}
fn for_each_rule_mut(program: &mut Program, mut visit: impl FnMut(&mut RuleDecl, Option<&str>)) {
for declaration in &mut program.declarations {
match declaration {
Declaration::Rule(rule) => visit(rule, None),
Declaration::Fragment(fragment) => {
let fragment_id = fragment.id.value.clone();
for rule in &mut fragment.rules {
visit(rule, Some(&fragment_id));
}
}
_ => {}
}
}
}
fn collect_rule_signatures(program: &Program) -> BTreeMap<String, Vec<Parameter>> {
let mut signatures = BTreeMap::new();
for declaration in &program.declarations {
match declaration {
Declaration::Rule(rule) => {
signatures.insert(normalize_name(&rule.name.value), rule.parameters.clone());
}
Declaration::Fragment(fragment) => {
for rule in &fragment.rules {
signatures.insert(
normalize_name(&format!("{}.{}", fragment.id.value, rule.name.value)),
rule.parameters.clone(),
);
}
}
_ => {}
}
}
signatures
}
fn collect_rules(program: &Program) -> Vec<RuleDecl> {
program
.declarations
.iter()
.flat_map(|declaration| match declaration {
Declaration::Rule(rule) => vec![rule.clone()],
Declaration::Fragment(fragment) => fragment.rules.clone(),
_ => Vec::new(),
})
.collect()
}
fn collect_implicit_parameters(
expression: &Expr,
entities: &[ImplicitEntityName],
parameters: &mut Vec<Parameter>,
) {
match &expression.kind {
ExprKind::Literal(_) | ExprKind::Name(_) => {}
ExprKind::Field { receiver, .. } => {
collect_implicit_field_receiver(receiver, entities, parameters);
}
ExprKind::Call { arguments, .. } => {
for argument in arguments {
collect_implicit_argument_parameter(argument, entities, parameters);
}
}
ExprKind::Unary { operand, .. } => {
collect_implicit_parameters(operand, entities, parameters);
}
ExprKind::Binary { left, right, .. } => {
collect_implicit_parameters(left, entities, parameters);
collect_implicit_parameters(right, entities, parameters);
}
}
}
fn collect_implicit_field_receiver(
expression: &Expr,
entities: &[ImplicitEntityName],
parameters: &mut Vec<Parameter>,
) {
match &expression.kind {
ExprKind::Name(name) => {
add_implicit_entity_name(name, expression.span, entities, parameters);
}
ExprKind::Field { receiver, .. } => {
collect_implicit_field_receiver(receiver, entities, parameters);
}
_ => collect_implicit_parameters(expression, entities, parameters),
}
}
fn collect_implicit_argument_parameter(
expression: &Expr,
entities: &[ImplicitEntityName],
parameters: &mut Vec<Parameter>,
) {
if let ExprKind::Name(name) = &expression.kind {
add_implicit_entity_name(name, expression.span, entities, parameters);
} else {
collect_implicit_parameters(expression, entities, parameters);
}
}
fn add_implicit_entity_name(
name: &str,
span: Span,
entities: &[ImplicitEntityName],
parameters: &mut Vec<Parameter>,
) {
let normalized = normalize_name(name);
let exact = entities
.iter()
.find(|entity| normalize_name(&entity.qualified) == normalized);
let entity = if let Some(exact) = exact {
exact
} else {
let mut matches = entities.iter().filter(|entity| {
!is_private_package_symbol(&entity.qualified)
&& normalize_name(&entity.local) == normalized
});
let Some(entity) = matches.next() else {
return;
};
if matches.next().is_some() {
return;
}
entity
};
if parameters
.iter()
.any(|parameter| normalize_name(¶meter.name.value) == normalized)
{
return;
}
parameters.push(Parameter {
ty: Spanned::new(entity.qualified.clone(), span),
name: Spanned::new(name.to_owned(), span),
span,
});
}
fn parameters_for_explicit_arguments(
rule: &RuleDecl,
arguments: &[Expr],
) -> Option<Vec<Parameter>> {
arguments
.iter()
.map(|argument| {
let ExprKind::Name(name) = &argument.kind else {
return None;
};
rule.parameters
.iter()
.find(|parameter| normalize_name(¶meter.name.value) == normalize_name(name))
.cloned()
})
.collect()
}
fn parameter_type_names(parameters: &[Parameter]) -> Vec<String> {
parameters
.iter()
.map(|parameter| normalize_name(¶meter.ty.value))
.collect()
}
fn merge_required_parameters(
parameters: &mut Vec<Parameter>,
required: &[Parameter],
) -> Option<String> {
for parameter in required {
let exact_name = parameters.iter().any(|candidate| {
normalize_name(&candidate.name.value) == normalize_name(¶meter.name.value)
&& normalize_name(&candidate.ty.value) == normalize_name(¶meter.ty.value)
});
if exact_name {
continue;
}
let required_type_count = required
.iter()
.filter(|candidate| {
normalize_name(&candidate.ty.value) == normalize_name(¶meter.ty.value)
})
.count();
if required_type_count > 1 {
return Some(parameter.ty.value.clone());
}
let available_type_count = parameters
.iter()
.filter(|candidate| {
normalize_name(&candidate.ty.value) == normalize_name(¶meter.ty.value)
})
.count();
if available_type_count == 0 {
parameters.push(parameter.clone());
} else if available_type_count > 1 {
return Some(parameter.ty.value.clone());
}
}
None
}
#[must_use]
pub fn compile(parsed: ParseOutput) -> CompileOutput {
let ParseOutput {
program,
mut diagnostics,
} = parsed;
let Some(mut ast) = program else {
sort_and_dedup_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 ambiguous_reference_spans =
canonicalize_short_symbol_references(&mut ast, &mut diagnostics);
canonicalize_qualified_enum_values(&mut ast, &mut diagnostics, &mut ambiguous_reference_spans);
elaborate_implicit_bindings(&mut ast, &mut diagnostics);
let mut compiler = Compiler::new(&ast, ambiguous_reference_spans);
compiler.run();
let mut parts = compiler.finish();
apply_inferred_derive_types(&mut ast, &parts.derives);
apply_inferred_fragment_derive_types(&mut parts.fragments, &parts.derives);
diagnostics.extend(parts.diagnostics);
sort_and_dedup_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,
fixtures: parts.fixtures,
cases: parts.cases,
invariants: parts.invariants,
traces: parts.traces,
expression_types: parts.expression_types,
});
CompileOutput {
program,
diagnostics,
}
}
fn apply_inferred_derive_types(program: &mut Program, derives: &BTreeMap<String, DeriveDecl>) {
for declaration in &mut program.declarations {
match declaration {
Declaration::Derive(derive) => {
let key = normalize_name(&derive.name.value);
copy_inferred_derive_type(derive, derives.get(&key));
}
Declaration::Fragment(fragment) => {
for derive in &mut fragment.derives {
let key =
normalize_name(&format!("{}.{}", fragment.id.value, derive.name.value));
copy_inferred_derive_type(derive, derives.get(&key));
}
}
_ => {}
}
}
}
fn apply_inferred_fragment_derive_types(
fragments: &mut BTreeMap<String, FragmentDecl>,
derives: &BTreeMap<String, DeriveDecl>,
) {
for fragment in fragments.values_mut() {
for derive in &mut fragment.derives {
let key = normalize_name(&format!("{}.{}", fragment.id.value, derive.name.value));
copy_inferred_derive_type(derive, derives.get(&key));
}
}
}
fn copy_inferred_derive_type(derive: &mut DeriveDecl, resolved: Option<&DeriveDecl>) {
if !derive.implicit_return_type {
return;
}
let Some(resolved) = resolved else {
return;
};
if resolved.return_type.value != TypeRef::Unknown {
derive.return_type.value = resolved.return_type.value.clone();
}
}
fn canonicalize_qualified_enum_values(
program: &mut Program,
diagnostics: &mut Vec<Diagnostic>,
ambiguous_spans: &mut BTreeSet<(usize, usize)>,
) {
let indexes = ProgramNameIndexes::collect(program);
let enums = program
.declarations
.iter()
.filter_map(|declaration| {
let Declaration::Enum(declaration) = declaration else {
return None;
};
Some((
normalize_name(&declaration.name.value),
declaration
.variants
.iter()
.map(|variant| normalize_name(&variant.value))
.collect::<BTreeSet<_>>(),
))
})
.collect::<BTreeMap<_, _>>();
if enums.is_empty() {
return;
}
let mut resolver = EnumValueResolver {
enums: &enums,
types: &indexes.types,
diagnostics,
ambiguous_spans,
};
for declaration in &mut program.declarations {
canonicalize_declaration_enum_values(declaration, &mut resolver);
}
}
struct EnumValueResolver<'a> {
enums: &'a BTreeMap<String, BTreeSet<String>>,
types: &'a NameIndex,
diagnostics: &'a mut Vec<Diagnostic>,
ambiguous_spans: &'a mut BTreeSet<(usize, usize)>,
}
fn canonicalize_declaration_enum_values(
declaration: &mut Declaration,
resolver: &mut EnumValueResolver<'_>,
) {
match declaration {
Declaration::Enum(_) | Declaration::Decision(_) | Declaration::Action(_) => {}
Declaration::Entity(declaration) => {
canonicalize_field_domains(&mut declaration.fields, resolver);
}
Declaration::State(declaration) => {
canonicalize_field_domains(&mut declaration.fields, resolver);
}
Declaration::Derive(declaration) => canonicalize_enum_value_expression(
&mut declaration.expression,
resolver,
¶meter_names(&declaration.parameters),
),
Declaration::Transition(declaration) => {
let scope = parameter_names(&declaration.parameters);
for argument in &mut declaration.action_arguments {
canonicalize_enum_value_expression(argument, resolver, &scope);
}
canonicalize_enum_value_expression(&mut declaration.condition, resolver, &scope);
for update in &mut declaration.updates {
canonicalize_enum_value_expression(&mut update.value, resolver, &scope);
}
}
Declaration::Fragment(declaration) => {
for derive in &mut declaration.derives {
canonicalize_enum_value_expression(
&mut derive.expression,
resolver,
¶meter_names(&derive.parameters),
);
}
for rule in &mut declaration.rules {
canonicalize_rule_enum_values(rule, resolver);
}
}
Declaration::Rule(declaration) => canonicalize_rule_enum_values(declaration, resolver),
Declaration::Fixture(declaration) => {
for field in &mut declaration.fields {
canonicalize_enum_value_expression(&mut field.value, resolver, &BTreeSet::new());
}
}
Declaration::Case(declaration) => {
let scope = declaration
.bindings
.iter()
.map(|binding| normalize_name(&binding.name.value))
.collect::<BTreeSet<_>>();
for binding in &mut declaration.bindings {
for field in &mut binding.fields {
canonicalize_enum_value_expression(&mut field.value, resolver, &scope);
}
}
for expectation in &mut declaration.expectations {
for argument in &mut expectation.arguments {
canonicalize_enum_value_expression(argument, resolver, &scope);
}
canonicalize_enum_value_expression(&mut expectation.value, resolver, &scope);
}
}
Declaration::Invariant(declaration) => {
let scope = parameter_names(&declaration.variables);
match &mut declaration.assertion {
InvariantAssertion::Cardinality { arguments, .. } => {
for argument in arguments {
canonicalize_enum_value_expression(argument, resolver, &scope);
}
}
InvariantAssertion::Implication {
condition,
expectation,
..
} => {
canonicalize_enum_value_expression(condition, resolver, &scope);
for argument in &mut expectation.arguments {
canonicalize_enum_value_expression(argument, resolver, &scope);
}
canonicalize_enum_value_expression(&mut expectation.value, resolver, &scope);
}
}
}
Declaration::Trace(declaration) => {
let scope = BTreeSet::from([normalize_name(&declaration.variable.name.value)]);
canonicalize_enum_value_expression(&mut declaration.initial, resolver, &scope);
for condition in &mut declaration.always {
canonicalize_enum_value_expression(condition, resolver, &scope);
}
if let Some(condition) = &mut declaration.terminal {
canonicalize_enum_value_expression(condition, resolver, &scope);
}
}
}
}
fn canonicalize_field_domains(fields: &mut [FieldDecl], resolver: &mut EnumValueResolver<'_>) {
for field in fields {
if let Some(domain) = &mut field.domain {
for value in &mut domain.values {
canonicalize_enum_value_expression(value, resolver, &BTreeSet::new());
}
}
}
}
fn canonicalize_rule_enum_values(rule: &mut RuleDecl, resolver: &mut EnumValueResolver<'_>) {
let scope = parameter_names(&rule.parameters);
canonicalize_enum_value_expression(&mut rule.condition, resolver, &scope);
if let Effect::Decide {
arguments, value, ..
} = &mut rule.effect
{
for argument in arguments {
canonicalize_enum_value_expression(argument, resolver, &scope);
}
canonicalize_enum_value_expression(value, resolver, &scope);
}
}
fn parameter_names(parameters: &[Parameter]) -> BTreeSet<String> {
parameters
.iter()
.map(|parameter| normalize_name(¶meter.name.value))
.collect()
}
fn canonicalize_enum_value_expression(
expression: &mut Expr,
resolver: &mut EnumValueResolver<'_>,
scope: &BTreeSet<String>,
) {
if let Some(path) = dotted_expression_path(expression) {
let root = path.split('.').next().unwrap_or(&path);
if !scope.contains(&normalize_name(root)) {
if let Some((enum_name, variant)) = path.rsplit_once('.') {
let mut canonical_enum = enum_name.to_owned();
let was_ambiguous = resolver
.ambiguous_spans
.contains(&(expression.span.start, expression.span.end));
canonicalize_string_reference(
&mut canonical_enum,
expression.span,
resolver.types,
"type",
&BUILTIN_TYPE_NAMES,
resolver.diagnostics,
resolver.ambiguous_spans,
);
let is_ambiguous = !was_ambiguous
&& resolver
.ambiguous_spans
.contains(&(expression.span.start, expression.span.end));
if is_ambiguous {
expression.kind = ExprKind::Name(path);
return;
}
if resolver
.enums
.get(&normalize_name(&canonical_enum))
.is_some_and(|variants| variants.contains(&normalize_name(variant)))
{
expression.kind = ExprKind::Name(format!("{canonical_enum}.{variant}"));
return;
}
}
}
}
match &mut expression.kind {
ExprKind::Literal(_) | ExprKind::Name(_) => {}
ExprKind::Field { receiver, .. } => {
canonicalize_enum_value_expression(receiver, resolver, scope);
}
ExprKind::Call { arguments, .. } => {
for argument in arguments {
canonicalize_enum_value_expression(argument, resolver, scope);
}
}
ExprKind::Unary { operand, .. } => {
canonicalize_enum_value_expression(operand, resolver, scope);
}
ExprKind::Binary { left, right, .. } => {
canonicalize_enum_value_expression(left, resolver, scope);
canonicalize_enum_value_expression(right, resolver, scope);
}
}
}
fn dotted_expression_path(expression: &Expr) -> Option<String> {
match &expression.kind {
ExprKind::Name(name) => Some(name.clone()),
ExprKind::Field { receiver, field } => Some(format!(
"{}.{}",
dotted_expression_path(receiver)?,
field.value
)),
_ => None,
}
}
fn sort_and_dedup_diagnostics(diagnostics: &mut Vec<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(),
))
});
diagnostics.dedup();
}
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>,
fixtures: BTreeMap<String, FixtureDecl>,
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>>,
checked_inferred_derives: BTreeSet<String>,
ambiguous_reference_spans: BTreeSet<(usize, usize)>,
}
#[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, ambiguous_reference_spans: BTreeSet<(usize, usize)>) -> Self {
Self {
ast,
parts: CompileParts::default(),
type_names: BTreeMap::new(),
callable_names: BTreeMap::new(),
variant_owners: BTreeMap::new(),
checked_inferred_derives: BTreeSet::new(),
ambiguous_reference_spans,
}
}
fn run(&mut self) {
self.collect_declarations();
self.propagate_derive_basis_to_rules();
self.collect_enum_variants();
self.infer_derive_return_types();
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_fixtures();
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_type_name(&mut type_spans, &key, value.name.span) {
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_type_name(&mut type_spans, &key, value.name.span) {
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_type_name(&mut type_spans, &key, value.name.span) {
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_callable_name(&mut callable_spans, &key, value.name.span) {
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_callable_name(&mut callable_spans, &key, value.name.span) {
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_callable_name(&mut callable_spans, &key, derive.name.span) {
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::Fixture(value) => {
let key = normalize_name(&value.name.value);
insert_declaration(
&mut self.parts.fixtures,
key,
value,
&mut self.parts.diagnostics,
"fixture",
);
}
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 insert_type_name(
&mut self,
namespace: &mut BTreeMap<String, Span>,
key: &str,
span: Span,
) -> bool {
if is_builtin_type_name(key) {
self.reserved_name(key, span, "type");
false
} else {
self.insert_namespace_name(namespace, key, span, "type")
}
}
fn insert_callable_name(
&mut self,
namespace: &mut BTreeMap<String, Span>,
key: &str,
span: Span,
) -> bool {
if is_builtin_callable_name(key) {
self.reserved_name(key, span, "callable");
false
} else {
self.insert_namespace_name(namespace, key, span, "callable")
}
}
fn reserved_name(&mut self, key: &str, span: Span, namespace_name: &str) {
self.parts.diagnostics.push(
Diagnostic::error(
"C0002",
format!("`{key}` is a reserved builtin {namespace_name} name"),
span,
)
.with_help("rename this declaration so builtin names keep one unambiguous meaning"),
);
}
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 infer_derive_return_types(&mut self) {
let mut pending = self
.parts
.derives
.iter()
.filter(|(_, declaration)| {
declaration.implicit_return_type
&& declaration.return_type.value == TypeRef::Unknown
})
.map(|(name, _)| name.clone())
.collect::<BTreeSet<_>>();
loop {
let ready = pending
.iter()
.filter(|name| {
let declaration = &self.parts.derives[*name];
let mut calls = BTreeSet::new();
collect_calls(&declaration.expression, &mut calls);
calls.is_disjoint(&pending)
})
.cloned()
.collect::<Vec<_>>();
if ready.is_empty() {
let graph = pending
.iter()
.map(|name| {
let mut calls = BTreeSet::new();
collect_calls(&self.parts.derives[name].expression, &mut calls);
calls.retain(|callee| pending.contains(callee));
(name.clone(), calls)
})
.collect::<BTreeMap<_, _>>();
let cycles = cyclic_components(&graph);
if cycles.is_empty() {
break;
}
for name in cycles.into_iter().flatten() {
pending.remove(&name);
let declaration = self.parts.derives[&name].clone();
self.cannot_infer_derive_return_type(&declaration);
}
continue;
}
for name in ready {
pending.remove(&name);
let declaration = self.parts.derives[&name].clone();
let diagnostic_start = self.parts.diagnostics.len();
let scope = self.parameter_scope(&declaration.parameters);
let inferred = self.type_expression(&declaration.expression, &scope);
self.checked_inferred_derives.insert(name.clone());
if let Some(return_type) = type_ref_from_value_type(&inferred) {
self.parts
.derives
.get_mut(&name)
.expect("pending fn must still exist")
.return_type
.value = return_type;
} else if inferred == ValueType::Unknown
|| (matches!(&declaration.expression.kind, ExprKind::Name(_))
&& self.parts.diagnostics[diagnostic_start..]
.iter()
.any(|diagnostic| diagnostic.code == "C0801"))
{
self.cannot_infer_derive_return_type(&declaration);
}
}
}
for name in pending {
let declaration = self.parts.derives[&name].clone();
self.cannot_infer_derive_return_type(&declaration);
}
}
fn cannot_infer_derive_return_type(&mut self, declaration: &DeriveDecl) {
self.parts.diagnostics.push(
Diagnostic::error(
"C0310",
format!(
"cannot infer return type for fn `{}`",
declaration.name.value
),
declaration.return_type.span,
)
.with_label(
declaration.expression.span,
"this expression does not determine a concrete return type",
)
.with_help(format!(
"add an explicit return type, for example `fn {}(...) Type:`",
declaration.name.value
)),
);
}
fn validate_derives(&mut self) {
let declarations = self
.parts
.derives
.iter()
.map(|(name, declaration)| (name.clone(), declaration.clone()))
.collect::<Vec<_>>();
for (name, declaration) in declarations {
if !self.checked_inferred_derives.contains(&name) {
let scope = self.parameter_scope(&declaration.parameters);
if declaration.return_type.value == TypeRef::Unknown {
self.type_expression(&declaration.expression, &scope);
} else {
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_fixtures(&mut self) {
let declarations = self.parts.fixtures.values().cloned().collect::<Vec<_>>();
let scope = BTreeMap::new();
for declaration in declarations {
let entity_name = normalize_name(&declaration.entity.value);
let Some(entity) = self.parts.entities.get(&entity_name).cloned() else {
self.unknown_name(
"C0620",
"record",
&declaration.entity.value,
declaration.entity.span,
self.parts.entities.keys().cloned().collect(),
);
continue;
};
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 &declaration.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(
"C0623",
format!("field `{key}` is initialized more than once in fixture"),
value.name.span,
)
.with_label(previous, "first initializer is here"),
);
continue;
}
let Some(field) = fields.get(&key) else {
self.unknown_name(
"C0624",
"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,
"fixture 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);
}
}
}
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();
if let Some(fixture_ref) = &binding.fixture {
let fixture_name = normalize_name(&fixture_ref.value);
match self.parts.fixtures.get(&fixture_name).cloned() {
Some(fixture)
if normalize_name(&fixture.entity.value)
== normalize_name(&binding.entity.value) =>
{
provided.extend(
fixture
.fields
.iter()
.map(|value| (normalize_name(&value.name.value), value.name.span)),
);
}
Some(fixture) => {
self.parts.diagnostics.push(
Diagnostic::error(
"C0622",
format!(
"fixture `{}` has record type `{}`, not `{}`",
fixture_ref.value, fixture.entity.value, binding.entity.value
),
fixture_ref.span,
)
.with_label(fixture.entity.span, "fixture record type is declared here"),
);
}
None => self.unknown_name(
"C0621",
"fixture",
&fixture_ref.value,
fixture_ref.span,
self.parts.fixtures.keys().cloned().collect(),
),
}
}
let mut local_provided = BTreeMap::<String, Span>::new();
for value in &binding.fields {
let key = normalize_name(&value.name.value);
if let Some(previous) = local_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;
}
provided.insert(key.clone(), value.name.span);
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((enum_name, variant)) = key.rsplit_once('.') {
if matches!(self.type_names.get(enum_name), Some(TypeKind::Enum))
&& self.enum_contains_variant(enum_name, variant)
{
return ValueType::Enum(enum_name.to_owned());
}
}
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>,
) {
if self
.ambiguous_reference_spans
.contains(&(span.start, span.end))
{
return;
}
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,
FixtureDecl,
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 type_ref_from_value_type(value_type: &ValueType) -> Option<TypeRef> {
match value_type {
ValueType::Bool => Some(TypeRef::Bool),
ValueType::Int => Some(TypeRef::Int),
ValueType::Decimal => Some(TypeRef::Decimal),
ValueType::String => Some(TypeRef::String),
ValueType::Date => Some(TypeRef::Date),
ValueType::Duration => Some(TypeRef::Duration),
ValueType::Enum(name) | ValueType::Entity(name) | ValueType::State(name) => {
Some(TypeRef::Named(name.clone()))
}
ValueType::Unknown | ValueType::Error => None,
}
}
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 rejects_declarations_that_shadow_builtin_types_and_functions() {
let output = compile_text(
r"mod M
enum Int:
Value
fn date(value String):
date(value)
dec max(): Bool
",
);
assert_eq!(
output
.diagnostics
.iter()
.filter(|diagnostic| diagnostic.code == "C0002")
.count(),
3
);
assert!(!codes(&output).contains(&"C0900"));
assert!(output.program.is_none());
}
#[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 infers_fn_return_types_through_forward_calls() {
let output = compile_text(
r"mod M
record Student:
score: Int
fn total(s Student):
adjusted(s)
fn adjusted(s Student):
s.score + 1
",
);
assert!(!output.has_errors(), "{:?}", output.diagnostics);
let program = output.program.unwrap();
for name in ["total", "adjusted"] {
let derive = program.derive(name).unwrap();
assert!(derive.implicit_return_type);
assert_eq!(derive.return_type.value, TypeRef::Int);
}
let Declaration::Derive(ast_total) = &program.ast.declarations[1] else {
panic!("expected the first fn declaration");
};
assert_eq!(ast_total.return_type.value, TypeRef::Int);
}
#[test]
fn infers_operator_and_enum_result_types() {
let output = compile_text(
r"mod M
enum Result:
Pass
Fail
fn ratio(value Int):
value / 2
fn passing():
Result.Pass
",
);
assert!(!output.has_errors(), "{:?}", output.diagnostics);
let program = output.program.unwrap();
assert_eq!(
program.derive("ratio").unwrap().return_type.value,
TypeRef::Decimal
);
assert_eq!(
program.derive("passing").unwrap().return_type.value,
TypeRef::Named("Result".to_owned())
);
}
#[test]
fn requires_an_annotation_when_fn_return_type_is_not_concrete() {
let output = compile_text("mod M\nfn unavailable():\n unknown\n");
let diagnostic = output
.diagnostics
.iter()
.find(|diagnostic| diagnostic.code == "C0310")
.expect("return type inference diagnostic");
assert!(diagnostic.message.contains("unavailable"));
assert!(
diagnostic
.help
.as_deref()
.is_some_and(|help| help.contains(") Type:"))
);
}
#[test]
fn requires_an_annotation_for_an_ambiguous_inferred_enum_variant() {
let output = compile_text(
r"mod M
enum First:
Same
enum Second:
Same
fn ambiguous():
Same
",
);
assert!(codes(&output).contains(&"C0801"));
assert!(codes(&output).contains(&"C0310"));
}
#[test]
fn does_not_cascade_inference_errors_from_an_invalid_expression() {
let output = compile_text("mod M\nfn leaf():\n misspelled\nfn caller():\n leaf()\n");
assert!(codes(&output).contains(&"C0802"));
assert!(!codes(&output).contains(&"C0310"));
}
#[test]
fn deduplicates_invalid_callee_signature_diagnostics_during_inference() {
let output = compile_text("mod M\nfn bad(x Missing):\n x\nfn caller():\n bad(1)\n");
assert_eq!(
output
.diagnostics
.iter()
.filter(|diagnostic| diagnostic.code == "C0301")
.count(),
1
);
}
#[test]
fn limits_inference_errors_to_the_inferred_fn_cycle() {
let output =
compile_text("mod M\nfn outside():\n a() > 0\nfn a():\n b()\nfn b():\n a()\n");
assert_eq!(
output
.diagnostics
.iter()
.filter(|diagnostic| diagnostic.code == "C0310")
.count(),
2
);
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 fixture_is_partial_and_binding_override_completes_it() {
let output = compile_text(
r"mod M
record Student:
score: Int
active: Bool
fixture active_student Student:
active: true
test sample:
Student from active_student { score: 90 }
",
);
assert!(!output.has_errors(), "{:?}", output.diagnostics);
let program = output.program.unwrap();
assert_eq!(program.fixture("active_student").unwrap().fields.len(), 1);
}
#[test]
fn fixture_reference_must_exist_and_match_the_binding_record() {
let output = compile_text(
r"mod M
record Student:
score: Int
record Teacher:
score: Int
fixture teacher Teacher:
score: 1
test wrong_type:
Student from teacher { score: 2 }
test missing:
Student from absent { score: 3 }
",
);
let codes = codes(&output);
assert!(codes.contains(&"C0622"), "{codes:?}");
assert!(codes.contains(&"C0621"), "{codes:?}");
}
#[test]
fn fixture_fields_are_validated_but_need_not_be_complete() {
let output = compile_text(
r"mod M
record Student:
score: Int
active: Bool
fixture invalid Student:
missing: true
active: true
active: false
",
);
let codes = codes(&output);
assert!(codes.contains(&"C0624"), "{codes:?}");
assert!(codes.contains(&"C0623"), "{codes:?}");
assert!(!codes.contains(&"C0604"), "{codes:?}");
}
#[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 omitted_parentheses_never_silently_mean_zero_arguments() {
let decision_without_rules = compile_text(
r"enum Result:
Yes
dec result: Result
",
);
assert!(codes(&decision_without_rules).contains(&"C0305"));
let empty_inference_cycle = compile_text(
r"enum Result:
Yes
dec result: Result
rule always:
: result = Yes
",
);
assert!(codes(&empty_inference_cycle).contains(&"C0305"));
}
#[test]
fn implicit_effect_rejects_indistinguishable_same_type_inputs() {
let output = compile_text(
r"record 주문:
금액: Int
dec 비교(left 주문, right 주문): Bool
rule 양수:
주문.금액 > 0: 비교 = true
",
);
assert!(codes(&output).contains(&"C0306"));
}
#[test]
fn implicit_rule_infers_a_record_exposed_only_as_a_function_argument() {
let output = compile_text(
r"enum 등급:
A
record 학생:
출석률: Int
dec 최종등급: 등급
강의계획서.출석규정:
출석률이 기준 이상이어야 성적을 평가할 수 있다.
fn 성적평가가능(학생) Bool:
학생.출석률 >= 80
rule A등급:
성적평가가능(학생): 최종등급 = A
",
);
assert!(!output.has_errors(), "{:?}", output.diagnostics);
let program = output.program.unwrap();
let decision = program.decision("최종등급").unwrap();
assert_eq!(decision.parameters.len(), 1);
assert_eq!(decision.parameters[0].ty.value, "학생");
let rule = program.rule("A등급").unwrap();
assert_eq!(rule.parameters.len(), 1);
assert_eq!(rule.parameters[0].name.value, "학생");
let ExprKind::Call { callee, arguments } = &rule.condition.kind else {
panic!("expected a function call");
};
assert_eq!(callee.value, "강의계획서.출석규정.성적평가가능");
assert!(
matches!(arguments.as_slice(), [Expr { kind: ExprKind::Name(name), .. }] if name == "학생")
);
let Effect::Decide { arguments, .. } = &rule.effect else {
panic!("expected a decision effect");
};
assert!(
matches!(arguments.as_slice(), [Expr { kind: ExprKind::Name(name), .. }] if name == "학생")
);
}
#[test]
fn implicit_rule_infers_a_record_used_only_by_the_effect_value() {
let output = compile_text(
r"record Order:
amount: Int
fn adjusted(Order) Int:
Order.amount + 1
dec total: Int
rule copied:
: total = adjusted(Order)
",
);
assert!(!output.has_errors(), "{:?}", output.diagnostics);
let program = output.program.unwrap();
let decision = program.decision("total").unwrap();
assert_eq!(decision.parameters.len(), 1);
assert_eq!(decision.parameters[0].ty.value, "Order");
let rule = program.rule("copied").unwrap();
assert_eq!(rule.parameters.len(), 1);
assert_eq!(rule.parameters[0].ty.value, "Order");
}
#[test]
fn implicit_unconditional_fallback_inherits_the_decision_signature() {
let output = compile_text(
r"enum Result:
Specific
Fallback
record Order:
amount: Int
dec outcome: Result
rule specific:
Order.amount > 0: outcome = Specific
rule fallback:
: outcome = Fallback
",
);
assert!(!output.has_errors(), "{:?}", output.diagnostics);
let program = output.program.unwrap();
let decision = program.decision("outcome").unwrap();
assert_eq!(decision.parameters.len(), 1);
assert_eq!(decision.parameters[0].ty.value, "Order");
let fallback = program.rule("fallback").unwrap();
assert_eq!(fallback.parameters.len(), 1);
assert_eq!(fallback.parameters[0].ty.value, "Order");
let Effect::Decide { arguments, .. } = &fallback.effect else {
panic!("expected a decision effect");
};
assert!(
matches!(arguments.as_slice(), [Expr { kind: ExprKind::Name(name), .. }] if name == "Order")
);
let explicit_zero_argument_fallback = compile_text(
r"enum Result:
Specific
Fallback
record Order:
amount: Int
dec outcome: Result
rule specific:
Order.amount > 0: outcome = Specific
rule fallback():
: outcome = Fallback
",
);
assert!(codes(&explicit_zero_argument_fallback).contains(&"C0305"));
}
#[test]
fn fragment_local_implicit_overrides_use_the_qualified_target_signature() {
let output = compile_text(
r"record A:
enabled: Bool
record B:
enabled: Bool
dec a_result(a A): Bool
dec b_result(b B): Bool
policy.a:
A policy.
rule base(a A):
a.enabled: a_result(a) = true
rule exception:
not A.enabled: override base
policy.b:
B policy.
rule base(b B):
b.enabled: b_result(b) = true
",
);
assert!(!output.has_errors(), "{:?}", output.diagnostics);
let program = output.program.unwrap();
let exception = program.rule("policy.a.exception").unwrap();
assert_eq!(exception.parameters.len(), 1);
assert_eq!(exception.parameters[0].ty.value, "A");
}
#[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):
basis law.definitions
request.value * 0.7
fn eligible(request Request):
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.return_type.value, TypeRef::Decimal);
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();
assert_eq!(eligible.return_type.value, TypeRef::Bool);
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 canonicalizes_a_unique_short_callable_to_its_full_name() {
let output = compile_text(
r"mod M
record Student:
active: Bool
dec result(s Student): Bool
policy.attendance:
Attendance policy.
fn eligible(s Student) Bool:
s.active
rule outside(s Student):
eligible(s): result(s) = true
",
);
assert!(!output.has_errors(), "{:?}", output.diagnostics);
let program = output.program.unwrap();
let condition = &program.rule("outside").unwrap().condition;
let ExprKind::Call { callee, .. } = &condition.kind else {
panic!("expected a function call");
};
assert_eq!(callee.value, "policy.attendance.eligible");
}
#[test]
fn reports_one_sorted_ambiguity_diagnostic_for_a_short_callable() {
let output = compile_text(
r"mod M
policy.a:
Policy A.
fn eligible() Bool:
true
policy.b:
Policy B.
fn eligible() Bool:
false
fn selected() Bool:
eligible()
",
);
let ambiguity = output
.diagnostics
.iter()
.filter(|diagnostic| diagnostic.code == "C0808")
.collect::<Vec<_>>();
assert_eq!(ambiguity.len(), 1, "{:?}", output.diagnostics);
let help = ambiguity[0].help.as_deref().unwrap();
assert!(help.contains("`policy.a.eligible`, `policy.b.eligible`"));
assert!(!codes(&output).contains(&"C0803"));
}
#[test]
fn exact_and_fragment_local_callables_win_over_leaf_fallback() {
let output = compile_text(
r"mod M
fn eligible() Bool:
true
policy.a:
Policy A.
fn eligible() Bool:
false
fn local() Bool:
eligible()
policy.b:
Policy B.
fn eligible() Bool:
false
fn external() Bool:
eligible()
",
);
assert!(!output.has_errors(), "{:?}", output.diagnostics);
let program = output.program.unwrap();
let ExprKind::Call { callee, .. } =
&program.derive("policy.a.local").unwrap().expression.kind
else {
panic!("expected local call");
};
assert_eq!(callee.value, "policy.a.eligible");
let ExprKind::Call { callee, .. } = &program.derive("external").unwrap().expression.kind
else {
panic!("expected external call");
};
assert_eq!(callee.value, "eligible");
}
#[test]
fn canonicalizes_short_decision_type_fragment_and_basis_references() {
let mut parsed = parse(SourceFile::new(
"test.tes",
r"mod M
enum Result:
Yes
No
record Student:
active: Bool
dec eligibility(s Student): Result
law.attendance:
Attendance source.
law.grading:
Grading source.
ref attendance
fn identity(s Student) Student:
basis attendance
s
rule yes(s Student):
s.active: eligibility(s) = Yes
test short_names:
Student { active: true }
expect eligibility(Student) = Yes
",
));
assert!(parsed.diagnostics.is_empty(), "{:?}", parsed.diagnostics);
let declarations = &mut parsed.program.as_mut().unwrap().declarations;
let Declaration::Enum(result) = &mut declarations[0] else {
panic!("expected enum");
};
result.name.value = "model.Result".into();
let Declaration::Entity(student) = &mut declarations[1] else {
panic!("expected record");
};
student.name.value = "model.Student".into();
let Declaration::Decision(eligibility) = &mut declarations[2] else {
panic!("expected decision");
};
eligibility.name.value = "policy.eligibility".into();
let output = compile(parsed);
assert!(!output.has_errors(), "{:?}", output.diagnostics);
let program = output.program.unwrap();
let decision = program.decision("policy.eligibility").unwrap();
assert_eq!(decision.parameters[0].ty.value, "model.Student");
assert_eq!(
decision.return_type.value,
TypeRef::Named("model.Result".into())
);
let fragment = program.fragment("law.grading").unwrap();
assert_eq!(fragment.refs[0].id.value, "law.attendance");
let derive = program.derive("law.grading.identity").unwrap();
assert_eq!(derive.parameters[0].ty.value, "model.Student");
assert_eq!(derive.basis[1].id.value, "law.attendance");
let Effect::Decide { decision, .. } = &program.rule("law.grading.yes").unwrap().effect
else {
panic!("expected decision effect");
};
assert_eq!(decision.value, "policy.eligibility");
assert_eq!(
program.case("short_names").unwrap().bindings[0]
.entity
.value,
"model.Student"
);
assert_eq!(
program.case("short_names").unwrap().expectations[0]
.decision
.value,
"policy.eligibility"
);
}
#[test]
fn private_package_symbols_are_not_short_name_candidates() {
let mut index = NameIndex::default();
index.insert("@pkg_hidden.secret", Span::default());
index.finish();
assert!(matches!(index.resolve("secret"), NameResolution::Missing));
assert!(matches!(
index.resolve("@pkg_hidden.secret"),
NameResolution::Found(name) if name == "@pkg_hidden.secret"
));
}
#[test]
fn canonicalizes_a_short_enum_owner_in_a_dotted_variant() {
let mut parsed = parse(SourceFile::new(
"test.tes",
r"mod M
enum Result:
Yes
No
fn selected() Result:
Result.Yes
",
));
assert!(parsed.diagnostics.is_empty(), "{:?}", parsed.diagnostics);
let declarations = &mut parsed.program.as_mut().unwrap().declarations;
let Declaration::Enum(result) = &mut declarations[0] else {
panic!("expected enum");
};
result.name.value = "model.Result".into();
let output = compile(parsed);
assert!(!output.has_errors(), "{:?}", output.diagnostics);
let selected = output.program.unwrap().derive("selected").unwrap().clone();
assert_eq!(
selected.return_type.value,
TypeRef::Named("model.Result".into())
);
let ExprKind::Name(value) = selected.expression.kind else {
panic!("expected canonical enum value");
};
assert_eq!(value, "model.Result.Yes");
}
#[test]
fn implicit_record_receiver_prefers_an_exact_top_level_name() {
let mut parsed = parse(SourceFile::new(
"test.tes",
r"mod M
record Order:
amount: Int
record Imported:
remote: Bool
dec fee: Bool
rule paid:
Order.amount > 0: fee = true
",
));
assert!(parsed.diagnostics.is_empty(), "{:?}", parsed.diagnostics);
let declarations = &mut parsed.program.as_mut().unwrap().declarations;
let Declaration::Entity(imported) = &mut declarations[1] else {
panic!("expected imported record");
};
imported.name.value = "package.Order".into();
let output = compile(parsed);
assert!(!output.has_errors(), "{:?}", output.diagnostics);
let program = output.program.unwrap();
assert_eq!(
program.rule("paid").unwrap().parameters[0].ty.value,
"Order"
);
assert_eq!(
program.decision("fee").unwrap().parameters[0].ty.value,
"Order"
);
}
#[test]
fn ambiguous_short_types_and_decisions_do_not_cascade_to_unknown_names() {
let mut parsed = parse(SourceFile::new(
"test.tes",
r"mod M
enum Left:
Yes
enum Right:
No
record Holder:
value: Result
",
));
assert!(parsed.diagnostics.is_empty(), "{:?}", parsed.diagnostics);
let declarations = &mut parsed.program.as_mut().unwrap().declarations;
let Declaration::Enum(left) = &mut declarations[0] else {
panic!("expected first enum");
};
left.name.value = "alpha.Result".into();
let Declaration::Enum(right) = &mut declarations[1] else {
panic!("expected second enum");
};
right.name.value = "beta.Result".into();
let output = compile(parsed);
assert_eq!(
output
.diagnostics
.iter()
.filter(|diagnostic| diagnostic.code == "C0808")
.count(),
1,
"{:?}",
output.diagnostics
);
assert!(!codes(&output).contains(&"C0301"));
let mut parsed = parse(SourceFile::new(
"test.tes",
r"mod M
dec first(): Bool
dec second(): Bool
rule choose():
true: result() = true
",
));
assert!(parsed.diagnostics.is_empty(), "{:?}", parsed.diagnostics);
let declarations = &mut parsed.program.as_mut().unwrap().declarations;
let Declaration::Decision(first) = &mut declarations[0] else {
panic!("expected first decision");
};
first.name.value = "alpha.result".into();
let Declaration::Decision(second) = &mut declarations[1] else {
panic!("expected second decision");
};
second.name.value = "beta.result".into();
let output = compile(parsed);
assert_eq!(
output
.diagnostics
.iter()
.filter(|diagnostic| diagnostic.code == "C0808")
.count(),
1,
"{:?}",
output.diagnostics
);
assert!(!codes(&output).contains(&"C0500"));
}
#[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:?}");
}
}