Skip to main content

xlog_logic/hypergraph/
explain.rs

1//! Stable textual explain output for a (hypergraph, eligibility,
2//! variable order) triple.
3//!
4//! The format is deterministic and intended for snapshot tests in
5//! PR 1, debugging in PR 2+, and a CLI subcommand later. The format
6//! is **not** a stable API for downstream tools yet — keep
7//! consumers inside the workspace.
8//!
9//! Format:
10//! ```text
11//! rule head=<predicate>
12//!   vertices: [<v0> <v1> ...]
13//!   hyperedges:
14//!     <predicate>(<arg0>, <arg1>, ...)
15//!     ...
16//!   filters: <comparison_count>
17//!   eligibility: <Eligible | Ineligible>
18//!     <boundary> ...
19//!   variable-order(<name>): [<v0> <v1> ...]
20//! ```
21//! where each `<arg>` is either `?<varname>` for a variable position
22//! or `_` for a constant / anonymous wildcard. Vertices and the
23//! variable-order line use names rather than `VertexId`s so the
24//! output is stable across construction-order changes that don't
25//! change the source rule.
26
27use super::eligibility::{Boundary, Eligibility};
28use super::ir::HypergraphRule;
29use super::var_order::VariableOrder;
30use std::fmt::Write;
31
32/// Render a stable textual explanation of `hg` plus its eligibility
33/// verdict and a variable order computed via `vo`. Pure: no IO, no
34/// hidden state.
35pub fn explain(hg: &HypergraphRule, eligibility: &Eligibility, vo: &dyn VariableOrder) -> String {
36    let mut out = String::new();
37
38    writeln!(out, "rule head={}", hg.head_predicate).unwrap();
39
40    // Vertices.
41    if hg.vertices.is_empty() {
42        writeln!(out, "  vertices: []").unwrap();
43    } else {
44        let names: Vec<&str> = hg.vertices.iter().map(|v| v.name.as_str()).collect();
45        writeln!(out, "  vertices: [{}]", names.join(" ")).unwrap();
46    }
47
48    // Hyperedges.
49    if hg.hyperedges.is_empty() {
50        writeln!(out, "  hyperedges: <none>").unwrap();
51    } else {
52        writeln!(out, "  hyperedges:").unwrap();
53        for edge in &hg.hyperedges {
54            let args: Vec<String> = edge
55                .vertex_positions
56                .iter()
57                .map(|p| match p {
58                    Some(vid) => format!("?{}", hg.vertex(*vid).name),
59                    None => "_".to_string(),
60                })
61                .collect();
62            writeln!(out, "    {}({})", edge.predicate, args.join(", ")).unwrap();
63        }
64    }
65
66    // Filters.
67    writeln!(out, "  filters: {}", hg.comparison_count).unwrap();
68
69    // Eligibility.
70    match eligibility {
71        Eligibility::Eligible => {
72            writeln!(out, "  eligibility: Eligible").unwrap();
73        }
74        Eligibility::Ineligible(boundaries) => {
75            writeln!(out, "  eligibility: Ineligible").unwrap();
76            for b in boundaries {
77                writeln!(out, "    {}", format_boundary(b)).unwrap();
78            }
79        }
80    }
81
82    // Variable order.
83    let order = vo.order(hg);
84    let order_names: Vec<&str> = order
85        .iter()
86        .map(|vid| hg.vertex(*vid).name.as_str())
87        .collect();
88    writeln!(
89        out,
90        "  variable-order({}): [{}]",
91        vo.name(),
92        order_names.join(" ")
93    )
94    .unwrap();
95
96    out
97}
98
99fn format_boundary(b: &Boundary) -> String {
100    match b {
101        Boundary::GroundFact => "GroundFact".to_string(),
102        Boundary::HeadAggregation => "HeadAggregation".to_string(),
103        Boundary::BodyNegation => "BodyNegation".to_string(),
104        Boundary::BodyIsExpr => "BodyIsExpr".to_string(),
105        Boundary::InsufficientPositiveAtoms { positive_count } => {
106            format!("InsufficientPositiveAtoms(positive_count={positive_count})")
107        }
108        Boundary::JoinKeysExceedBinaryFallbackLimit {
109            context,
110            count,
111            limit,
112        } => {
113            format!(
114                "JoinKeysExceedBinaryFallbackLimit(context={context:?}, count={count}, limit={limit})"
115            )
116        }
117        Boundary::UnsupportedKeyType { var, ty } => {
118            format!("UnsupportedKeyType(var={var}, ty={ty:?})")
119        }
120    }
121}