use anstyle::{AnsiColor, Style};
use std::borrow::Cow;
use std::collections::{BTreeMap, BTreeSet};
use std::fmt::{Arguments, Write as _};
use std::path::Path;
use tess::ast::{Cardinality, Effect};
use tess::compiler::normalize_name;
use tess::engine::{
Candidate, DecisionStatus, Evaluation, Input, RuleOutcome, RuleTrace, SuppressionReason,
};
use tess::value::Value;
use tess::{CompiledProgram, SourceFile, Span};
const CASE_STYLE: Style = AnsiColor::Cyan.on_default().bold();
const NAME_STYLE: Style = AnsiColor::Blue.on_default().bold();
const SUCCESS_STYLE: Style = AnsiColor::Green.on_default().bold();
const INCOMPLETE_STYLE: Style = AnsiColor::Yellow.on_default().bold();
const CONFLICT_STYLE: Style = AnsiColor::Red.on_default().bold();
const SECTION_STYLE: Style = AnsiColor::Cyan.on_default().bold();
const LABEL_STYLE: Style = AnsiColor::BrightBlack.on_default();
const LOCATION_STYLE: Style = AnsiColor::BrightBlack.on_default();
const MARKER_STYLE: Style = AnsiColor::Cyan.on_default().bold();
const METADATA_VALUE_COLUMN: usize = 15;
#[derive(Clone, Copy)]
struct Styles {
color: bool,
}
impl Styles {
const fn new(color: bool) -> Self {
Self { color }
}
fn write(self, output: &mut String, style: Style, value: Arguments<'_>) {
if self.color {
let _ = write!(output, "{style}{value}{style:#}");
} else {
let _ = output.write_fmt(value);
}
}
}
pub(crate) fn render_evaluation(
program: &CompiledProgram,
input: &Input,
evaluation: &Evaluation,
case_name: Option<&str>,
color: bool,
) -> String {
render_evaluations(
program,
input,
std::slice::from_ref(evaluation),
case_name,
color,
)
}
pub(crate) fn render_evaluations(
program: &CompiledProgram,
input: &Input,
evaluations: &[Evaluation],
case_name: Option<&str>,
color: bool,
) -> String {
let styles = Styles::new(color);
let mut output = String::new();
write_context(&mut output, input, case_name, styles);
for evaluation in evaluations {
output.push('\n');
write_evaluation(&mut output, program, evaluation, styles);
}
while output.ends_with("\n\n") {
output.pop();
}
if !output.ends_with('\n') {
output.push('\n');
}
output
}
fn write_context(output: &mut String, input: &Input, case_name: Option<&str>, styles: Styles) {
let heading = case_name.unwrap_or("input");
styles.write(output, CASE_STYLE, format_args!("{heading}"));
output.push('\n');
if input.bindings.is_empty() {
output.push_str(" (no entity bindings)\n");
return;
}
for (binding_name, binding) in &input.bindings {
write_indent(output, 2);
output.push_str("let ");
styles.write(output, NAME_STYLE, format_args!("{binding_name}"));
output.push_str(" = ");
styles.write(output, NAME_STYLE, format_args!("{}", binding.entity));
if binding.fields.is_empty() {
output.push_str(" {}\n");
continue;
}
output.push_str(" {\n");
for (field, value) in &binding.fields {
let _ = writeln!(output, " {field}: {value},");
}
output.push_str(" }\n");
}
}
fn write_evaluation(
output: &mut String,
program: &CompiledProgram,
evaluation: &Evaluation,
styles: Styles,
) {
write_status(output, evaluation, styles);
write_contract(output, evaluation, styles);
let undefined_decision = match evaluation.result.status {
DecisionStatus::Undefined => program.decision(&evaluation.result.decision),
_ => None,
};
if let Some(decision) = undefined_decision {
output.push('\n');
write_snippet(
output,
program.source(),
decision.span,
"this decision requires exactly one value, but no candidate survived",
4,
styles,
);
}
let mut explained_rules = BTreeSet::new();
write_support(output, program, evaluation, &mut explained_rules, styles);
write_suppressed(output, program, evaluation, &mut explained_rules, styles);
write_blocked_rules(output, program, evaluation, &mut explained_rules, styles);
write_other_applied_rules(output, program, evaluation, &explained_rules, styles);
write_skipped_rules(output, program, evaluation, styles);
write_help(output, evaluation, styles);
}
fn write_status(output: &mut String, evaluation: &Evaluation, styles: Styles) {
let (style, status) = match &evaluation.result.status {
DecisionStatus::Resolved { values } => (
SUCCESS_STYLE,
format!("✓ {} = {}", evaluation.query, resolved_values(values)),
),
DecisionStatus::Undefined => (
INCOMPLETE_STYLE,
format!("â—‹ {} = undefined", evaluation.query),
),
DecisionStatus::Unknown { .. } => (
INCOMPLETE_STYLE,
format!("? {} = unknown", evaluation.query),
),
DecisionStatus::Conflict { values, .. } => (
CONFLICT_STYLE,
format!(
"✗ {} = conflict [{}]",
evaluation.query,
display_values(values)
),
),
};
write_indent(output, 2);
styles.write(output, style, format_args!("{status}"));
output.push('\n');
}
fn write_contract(output: &mut String, evaluation: &Evaluation, styles: Styles) {
let distinct = evaluation
.result
.candidates
.iter()
.map(|candidate| &candidate.value)
.collect::<BTreeSet<_>>()
.len();
let support = evaluation.result.candidates.len();
write_metadata(
output,
4,
"cardinality",
format_args!("{}", cardinality_contract(evaluation.result.cardinality)),
styles,
);
write_metadata(
output,
4,
"candidates",
format_args!(
"{} · {} · {} overridden",
counted(support, "rule candidate", "rule candidates"),
counted(distinct, "distinct value", "distinct values"),
evaluation.result.suppressed.len()
),
styles,
);
match &evaluation.result.status {
DecisionStatus::Resolved { values } if values.is_empty() => write_metadata(
output,
4,
"reason",
format_args!("no value remained, which this cardinality permits"),
styles,
),
DecisionStatus::Resolved { .. } if support > distinct => write_metadata(
output,
4,
"reason",
format_args!(
"{} agreed on {}; equal values were merged",
counted(support, "rule", "rules"),
counted(distinct, "distinct value", "distinct values")
),
styles,
),
DecisionStatus::Resolved { .. } => write_metadata(
output,
4,
"reason",
format_args!(
"{} remained after explicit overrides",
counted(distinct, "distinct value", "distinct values")
),
styles,
),
DecisionStatus::Undefined => write_metadata(
output,
4,
"reason",
format_args!("no candidate survived for a decision that requires one"),
styles,
),
DecisionStatus::Unknown { missing } => {
write_metadata(
output,
4,
"missing",
format_args!("{}", missing.join(", ")),
styles,
);
write_metadata(
output,
4,
"reason",
format_args!("missing input can still change which rules or overrides apply"),
styles,
);
}
DecisionStatus::Conflict { reasons, .. } => {
for reason in reasons {
write_metadata(output, 4, "reason", format_args!("{reason}"), styles);
}
write_metadata(
output,
4,
"note",
format_args!("source order never chooses between different values"),
styles,
);
}
}
}
fn write_support(
output: &mut String,
program: &CompiledProgram,
evaluation: &Evaluation,
explained_rules: &mut BTreeSet<String>,
styles: Styles,
) {
if evaluation.result.candidates.is_empty() {
return;
}
let mut groups = BTreeMap::<&Value, Vec<&Candidate>>::new();
for candidate in &evaluation.result.candidates {
groups.entry(&candidate.value).or_default().push(candidate);
}
for (value, candidates) in groups {
output.push('\n');
let merged = candidates.len() > 1;
let qualifier = match evaluation.result.status {
DecisionStatus::Unknown { .. } => "provisional candidate",
DecisionStatus::Conflict { .. } => "conflicting candidate",
DecisionStatus::Resolved { .. } if merged => "merged candidate",
DecisionStatus::Resolved { .. } | DecisionStatus::Undefined => "selected candidate",
};
write_section(output, 4, format_args!("{qualifier} `{value}`"), styles);
for candidate in &candidates {
explained_rules.insert(normalize_name(&candidate.rule));
let (span, exact_effect) =
candidate_location(program, &candidate.rule, &evaluation.result.decision);
let label = if exact_effect {
format!("proposed `{value}` by rule `{}`", candidate.rule)
} else {
format!("rule `{}` supports `{value}`", candidate.rule)
};
if let Some(span) = span {
write_snippet(output, program.source(), span, &label, 6, styles);
} else {
write_metadata(
output,
6,
"rule",
format_args!("{} proposed `{value}`", candidate.rule),
styles,
);
}
write_when_note(output, evaluation, &candidate.rule, styles);
if let Some(source) = &candidate.source {
write_metadata(output, 6, "source", format_args!("{source}"), styles);
}
}
if merged {
write_metadata(
output,
6,
"note",
format_args!("equal values are merged and count once toward cardinality"),
styles,
);
}
}
}
fn write_suppressed(
output: &mut String,
program: &CompiledProgram,
evaluation: &Evaluation,
explained_rules: &mut BTreeSet<String>,
styles: Styles,
) {
for suppressed in &evaluation.result.suppressed {
output.push('\n');
let candidate = &suppressed.candidate;
explained_rules.insert(normalize_name(&candidate.rule));
let SuppressionReason::ExplicitOverride { by } = &suppressed.reason;
explained_rules.insert(normalize_name(by));
write_section(
output,
4,
format_args!("overridden candidate `{}`", candidate.value),
styles,
);
let (candidate_span, exact_effect) =
candidate_location(program, &candidate.rule, &evaluation.result.decision);
let candidate_label = if exact_effect {
format!("proposed by rule `{}`", candidate.rule)
} else {
format!("candidate came from rule `{}`", candidate.rule)
};
if let Some(span) = candidate_span {
write_snippet(output, program.source(), span, &candidate_label, 6, styles);
}
write_when_note(output, evaluation, &candidate.rule, styles);
if let Some(source) = &candidate.source {
write_metadata(output, 6, "source", format_args!("{source}"), styles);
}
if let Some(span) = override_location(program, by, &candidate.rule) {
write_snippet(
output,
program.source(),
span,
&format!("rule `{by}` removed this candidate"),
6,
styles,
);
} else {
write_metadata(output, 6, "overridden by", format_args!("{by}"), styles);
}
write_when_note(output, evaluation, by, styles);
}
}
fn write_blocked_rules(
output: &mut String,
program: &CompiledProgram,
evaluation: &Evaluation,
explained_rules: &mut BTreeSet<String>,
styles: Styles,
) {
let blocked = evaluation
.rules
.iter()
.filter(|trace| {
matches!(
trace.outcome,
RuleOutcome::Blocked | RuleOutcome::Conflicted
)
})
.collect::<Vec<_>>();
if blocked.is_empty() {
return;
}
output.push('\n');
write_section(output, 4, format_args!("blocked rules"), styles);
for trace in blocked {
explained_rules.insert(normalize_name(&trace.rule));
let effect_blocked = trace.condition == tess::value::TruthValue::True;
let span = if effect_blocked {
candidate_location(program, &trace.rule, &evaluation.result.decision).0
} else {
program.rule(&trace.rule).map(|rule| rule.condition.span)
};
let state = match trace.outcome {
RuleOutcome::Conflicted => "conflicting evidence prevented this rule",
RuleOutcome::Blocked if trace.missing.is_empty() => "this rule could not be resolved",
RuleOutcome::Blocked => "this rule needs additional input",
_ => unreachable!(),
};
if let Some(span) = span {
write_snippet(
output,
program.source(),
span,
&format!("{state}: `{}`", trace.rule),
6,
styles,
);
}
write_metadata(
output,
6,
"when",
format_args!("{} → {}", trace.condition_expression, trace.condition),
styles,
);
if !trace.missing.is_empty() {
write_metadata(
output,
6,
"missing",
format_args!("{}", trace.missing.join(", ")),
styles,
);
}
if let Some(source) = &trace.source {
write_metadata(output, 6, "source", format_args!("{source}"), styles);
}
}
}
fn write_other_applied_rules(
output: &mut String,
program: &CompiledProgram,
evaluation: &Evaluation,
explained_rules: &BTreeSet<String>,
styles: Styles,
) {
let other = evaluation
.rules
.iter()
.filter(|trace| {
matches!(
trace.outcome,
RuleOutcome::Applied | RuleOutcome::OverrideApplied
) && !explained_rules.contains(&normalize_name(&trace.rule))
})
.collect::<Vec<_>>();
if other.is_empty() {
return;
}
output.push('\n');
write_section(output, 4, format_args!("other applied rules"), styles);
for trace in other {
if let Some(rule) = program.rule(&trace.rule) {
write_snippet(
output,
program.source(),
rule.condition.span,
&format!("condition evaluated to true for rule `{}`", trace.rule),
6,
styles,
);
}
if let Some(source) = &trace.source {
write_metadata(output, 6, "source", format_args!("{source}"), styles);
}
}
}
fn write_skipped_rules(
output: &mut String,
program: &CompiledProgram,
evaluation: &Evaluation,
styles: Styles,
) {
let skipped = evaluation
.rules
.iter()
.filter(|trace| trace.outcome == RuleOutcome::Skipped)
.collect::<Vec<_>>();
if skipped.is_empty() {
return;
}
output.push('\n');
write_section(
output,
4,
format_args!(
"not applicable: {}",
counted(skipped.len(), "rule", "rules")
),
styles,
);
for trace in skipped {
if let Some(rule) = program.rule(&trace.rule) {
write_snippet(
output,
program.source(),
rule.condition.span,
&format!(
"condition evaluated to false; rule `{}` was skipped",
trace.rule
),
6,
styles,
);
} else {
write_metadata(
output,
6,
"skipped",
format_args!("`{}`: {} → false", trace.rule, trace.condition_expression),
styles,
);
}
if let Some(source) = &trace.source {
write_metadata(output, 6, "source", format_args!("{source}"), styles);
}
}
}
fn write_help(output: &mut String, evaluation: &Evaluation, styles: Styles) {
let help = match &evaluation.result.status {
DecisionStatus::Undefined => Some(
"add a rule covering this input, or use `zero or one` if no answer is valid".into(),
),
DecisionStatus::Unknown { missing } => {
Some(format!("provide the missing input: {}", missing.join(", ")))
}
DecisionStatus::Conflict { .. } => {
Some("make the conditions mutually exclusive or add an explicit named override".into())
}
DecisionStatus::Resolved { .. } => None,
};
if let Some(help) = help {
output.push('\n');
write_metadata(output, 4, "help", format_args!("{help}"), styles);
}
}
fn write_when_note(output: &mut String, evaluation: &Evaluation, rule_name: &str, styles: Styles) {
if let Some(trace) = trace_for_rule(evaluation, rule_name) {
write_metadata(
output,
6,
"when",
format_args!("{} → {}", trace.condition_expression, trace.condition),
styles,
);
}
}
fn trace_for_rule<'a>(evaluation: &'a Evaluation, rule_name: &str) -> Option<&'a RuleTrace> {
let rule_name = normalize_name(rule_name);
evaluation
.rules
.iter()
.find(|trace| normalize_name(&trace.rule) == rule_name)
}
fn candidate_location(
program: &CompiledProgram,
rule_name: &str,
decision_name: &str,
) -> (Option<Span>, bool) {
let Some(rule) = program.rule(rule_name) else {
return (None, false);
};
let decision_name = normalize_name(decision_name);
let spans = rule
.effects
.iter()
.filter_map(|effect| match effect {
Effect::Decide { decision, span, .. }
if normalize_name(&decision.value) == decision_name =>
{
Some(*span)
}
Effect::Decide { .. } | Effect::Override { .. } => None,
})
.collect::<Vec<_>>();
match spans.as_slice() {
[span] => (Some(*span), true),
_ => (Some(rule.condition.span), false),
}
}
fn override_location(program: &CompiledProgram, by: &str, target: &str) -> Option<Span> {
let rule = program.rule(by)?;
let target = normalize_name(target);
let spans = rule
.effects
.iter()
.filter_map(|effect| match effect {
Effect::Override { rule, span } if normalize_name(&rule.value) == target => Some(*span),
Effect::Override { .. } | Effect::Decide { .. } => None,
})
.collect::<Vec<_>>();
match spans.as_slice() {
[span] => Some(*span),
_ => None,
}
}
fn write_snippet(
output: &mut String,
source: &SourceFile,
span: Span,
label: &str,
indent: usize,
styles: Styles,
) {
let (line, column) = source.line_col(span.start);
let text = source.line_text_at(span.start);
let gutter = line.to_string().len().max(3);
let source_name = display_source_name(source.name_at(span.start));
write_indent(output, indent);
styles.write(
output,
LOCATION_STYLE,
format_args!("--> {source_name}:{line}:{column}"),
);
output.push('\n');
write_indent(output, indent);
styles.write(output, LOCATION_STYLE, format_args!("{:>gutter$} |", ""));
output.push('\n');
write_indent(output, indent);
styles.write(output, LOCATION_STYLE, format_args!("{line:>gutter$} | "));
output.push_str(text);
output.push('\n');
let selected_width = source
.text
.get(span.start..span.end)
.and_then(|selected| selected.lines().next())
.map_or(1, |selected| selected.chars().count().max(1));
let remaining_width = text
.chars()
.count()
.saturating_sub(column.saturating_sub(1))
.max(1);
let marker_width = selected_width.min(remaining_width);
write_indent(output, indent);
styles.write(
output,
LOCATION_STYLE,
format_args!("{:>gutter$} | {}", "", " ".repeat(column.saturating_sub(1))),
);
styles.write(
output,
MARKER_STYLE,
format_args!("{}", "^".repeat(marker_width)),
);
let _ = writeln!(output, " {label}");
}
fn write_section(output: &mut String, indent: usize, value: Arguments<'_>, styles: Styles) {
write_indent(output, indent);
styles.write(output, SECTION_STYLE, value);
output.push('\n');
}
fn display_source_name(name: &str) -> Cow<'_, str> {
let path = Path::new(name);
if !path.is_absolute() {
return Cow::Borrowed(name);
}
let Ok(current_directory) = std::env::current_dir() else {
return Cow::Borrowed(name);
};
path.strip_prefix(current_directory)
.ok()
.and_then(Path::to_str)
.map_or_else(|| Cow::Borrowed(name), Cow::Borrowed)
}
fn write_metadata(
output: &mut String,
indent: usize,
label: &str,
value: Arguments<'_>,
styles: Styles,
) {
write_indent(output, indent);
styles.write(output, LABEL_STYLE, format_args!("{label}"));
output.push_str(&" ".repeat(METADATA_VALUE_COLUMN.saturating_sub(label.len()).max(2)));
let _ = output.write_fmt(value);
output.push('\n');
}
fn write_indent(output: &mut String, indent: usize) {
output.push_str(&" ".repeat(indent));
}
fn resolved_values(values: &[Value]) -> String {
match values {
[] => "[]".to_owned(),
[value] => value.to_string(),
values => format!("[{}]", display_values(values)),
}
}
fn display_values(values: &[Value]) -> String {
values
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
}
const fn cardinality_contract(cardinality: Cardinality) -> &'static str {
match cardinality {
Cardinality::ExactlyOne => "exactly one distinct value is required",
Cardinality::ZeroOrOne => "zero or one distinct value is allowed",
Cardinality::Many => "any number of distinct values is allowed",
}
}
fn counted(count: usize, singular: &str, plural: &str) -> String {
if count == 1 {
format!("1 {singular}")
} else {
format!("{count} {plural}")
}
}