use super::json::status_name;
use super::{Report, Status, TraceReason, TraceStep};
use alloc::string::String;
use alloc::vec::Vec;
use core::fmt;
use elenchus_compiler::{Origin, PlaceholderStatus, Value, kw};
impl fmt::Display for Status {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(status_name(*self))
}
}
pub(crate) fn premise_tag(o: &Origin) -> String {
let name = o.premise.as_deref().unwrap_or("-");
alloc::format!("{} ({}) [{}:{}]", name, o.kind, o.source, o.line)
}
pub(crate) fn trace_line(step: &TraceStep) -> String {
let v = match step.value {
Value::True => "TRUE",
Value::False => "FALSE",
};
match &step.reason {
TraceReason::Asserted(o) => {
alloc::format!(
"{} = {} [{} {}:{}]",
step.atom,
v,
o.kind,
o.source,
o.line
)
}
TraceReason::Derived { origin, from } => alloc::format!(
"{} = {} from {} ({}) [{}:{}] <= {}",
step.atom,
v,
origin.premise.as_deref().unwrap_or("-"),
origin.kind,
origin.source,
origin.line,
from.join(", ")
),
}
}
mod indent {
pub const ROOT: usize = 0;
pub const SECTION: usize = 2;
pub const ITEM: usize = 6;
pub const NESTED: usize = 8;
}
struct ReportWriter<'a, 'b> {
f: &'a mut fmt::Formatter<'b>,
}
impl ReportWriter<'_, '_> {
fn line(&mut self, indent: usize, args: fmt::Arguments<'_>) -> fmt::Result {
write!(self.f, "{:width$}{}", "", args, width = indent)?;
self.f.write_str("\n")
}
fn tail(&mut self, indent: usize, args: fmt::Arguments<'_>) -> fmt::Result {
write!(self.f, "{:width$}{}", "", args, width = indent)
}
}
macro_rules! emit {
($out:expr, $indent:expr, $($arg:tt)*) => {
$out.line($indent, format_args!($($arg)*))
};
}
impl Report {
fn render(&self, f: &mut fmt::Formatter<'_>, show_placeholders: bool) -> fmt::Result {
use indent::{ITEM, NESTED, ROOT, SECTION};
let mut out = ReportWriter { f };
emit!(out, ROOT, "RESULT: {}", self.status)?;
if !self.retract.is_empty() {
emit!(out, SECTION, "RETRACT your FACTs and PREMISEs are fine.")?;
emit!(
out,
ITEM,
"But these ASSUME guesses cannot all be true together."
)?;
emit!(out, ITEM, "Remove or flip ONE of them, then check again:")?;
for it in &self.retract {
emit!(
out,
ITEM,
"ASSUME {} [{}:{}]",
it.label,
it.origin.source,
it.origin.line
)?;
}
} else {
for c in &self.conflicts {
emit!(out, SECTION, "CONFLICT {}", premise_tag(&c.origin))?;
for a in &c.atoms {
emit!(out, ITEM, "{}", a)?;
}
if !c.trace.is_empty() {
emit!(out, ITEM, "why:")?;
for step in &c.trace {
emit!(out, NESTED, "{}", trace_line(step))?;
}
}
}
if !self.unsat_core.is_empty() {
emit!(
out,
SECTION,
"CORE smallest jointly-unsatisfiable set ({}):",
self.unsat_core.len()
)?;
for it in &self.unsat_core {
let name = if it.label.is_empty() { "-" } else { &it.label };
emit!(
out,
NESTED,
"{} ({}) [{}:{}]",
name,
it.origin.kind,
it.origin.source,
it.origin.line
)?;
}
}
}
let mut shown_fixes: Vec<&str> = Vec::new();
for w in &self.warnings {
emit!(out, SECTION, "WARNING {}", premise_tag(&w.origin))?;
emit!(out, ITEM, "blocked by: {}", w.blocked_by.join(", "))?;
if let Some(hint) = &w.hint
&& !shown_fixes.contains(&hint.as_str())
{
shown_fixes.push(hint);
emit!(out, ITEM, "fix: {hint}")?;
}
}
if let Some(atom) = &self.underdetermined {
emit!(out, SECTION, "UNDERDETERMINED an alternative model exists")?;
emit!(out, ITEM, "pin it down: add FACT {atom} or NOT {atom}")?;
}
for d in &self.derived {
let v = match d.value {
Value::True => "TRUE",
Value::False => "FALSE",
};
emit!(
out,
SECTION,
"DERIVED {} = {} from {}",
d.atom,
v,
premise_tag(&d.origin)
)?;
}
for h in &self.hints {
emit!(
out,
SECTION,
"HINT possible typo — '{}' and '{}' look like the same atom ({})",
h.a,
h.b,
h.reason
)?;
}
for o in &self.orphans {
let surface = if o.origin.kind == kw::ASSUME && matches!(o.value, Value::False) {
alloc::format!("{} {} {}", kw::ASSUME, kw::NOT, o.atom)
} else {
alloc::format!("{} {}", o.origin.kind, o.atom)
};
emit!(
out,
SECTION,
"ORPHAN {} — not used by any premise or rule (no effect on the result)",
surface
)?;
}
for u in &self.unused_imports {
let via = match &u.alias {
Some(a) => alloc::format!("{} AS {}", u.domain, a),
None => u.domain.clone(),
};
emit!(
out,
SECTION,
"UNUSED IMPORT {} — imported in {}:{} but never referenced (no effect on the result)",
via,
u.file,
u.line
)?;
}
if show_placeholders {
for p in &self.placeholders {
match p.status {
PlaceholderStatus::Supplied => emit!(
out,
SECTION,
"PARAM {} = {} (supplied{})",
p.key,
bool_word(p.value),
p.origin
.as_deref()
.map(|o| alloc::format!(": {o}"))
.unwrap_or_default()
)?,
PlaceholderStatus::DefaultUsed => emit!(
out,
SECTION,
"PARAM {} = {} (DEFAULT)",
p.key,
bool_word(p.value)
)?,
PlaceholderStatus::Unset => emit!(
out,
SECTION,
"PARAM {} = UNKNOWN (no value supplied, no DEFAULT)",
p.key
)?,
}
}
}
let underdetermined = usize::from(self.status == Status::Underdetermined);
emit!(
out,
ROOT,
"SUMMARY: {} conflicts, {} underdetermined, {} warnings, {} derived",
self.conflicts.len(),
underdetermined,
self.warnings.len(),
self.derived.len()
)?;
out.tail(ROOT, format_args!("EXIT_CODE: {}", self.exit_code()))
}
pub fn render_human(&self, show_placeholders: bool) -> String {
alloc::format!(
"{}",
HumanReport {
report: self,
show_placeholders
}
)
}
}
struct HumanReport<'a> {
report: &'a Report,
show_placeholders: bool,
}
impl fmt::Display for HumanReport<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.report.render(f, self.show_placeholders)
}
}
impl fmt::Display for Report {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.render(f, true)
}
}
pub(crate) fn bool_word(v: Option<bool>) -> &'static str {
match v {
Some(true) => "true",
Some(false) => "false",
None => "UNKNOWN",
}
}