Skip to main content

rml/
meta.rs

1// rml-meta — metatheorem checker over encoded systems (issue #47, C3).
2//
3// Composes the existing D12 totality, D14 coverage, D15 modes, and D13
4// termination checkers into a single Twelf-style guarantee: a relation is
5// total on its declared input domain iff every input pattern is covered
6// and every recursive call structurally decreases on a `+input` slot.
7//
8// Used as a library from `check_metatheorems(text, file)` and as a CLI via
9// the `rml-meta` binary in `src/bin/rml-meta.rs`.
10
11use crate::{
12    evaluate_with_env, format_diagnostic, is_covered, is_terminating, is_total,
13    CoverageDiagnostic, Diagnostic, Env, TerminationDiagnostic, TotalityDiagnostic,
14};
15
16/// Generic per-check diagnostic produced by the metatheorem checker. We
17/// flatten the per-checker diagnostic types ([`TotalityDiagnostic`] etc.)
18/// into a single shape so the report consumer does not have to know which
19/// underlying checker emitted the message.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct MetaDiagnostic {
22    pub code: String,
23    pub message: String,
24}
25
26impl From<TotalityDiagnostic> for MetaDiagnostic {
27    fn from(d: TotalityDiagnostic) -> Self {
28        MetaDiagnostic {
29            code: d.code,
30            message: d.message,
31        }
32    }
33}
34
35impl From<CoverageDiagnostic> for MetaDiagnostic {
36    fn from(d: CoverageDiagnostic) -> Self {
37        MetaDiagnostic {
38            code: d.code,
39            message: d.message,
40        }
41    }
42}
43
44impl From<TerminationDiagnostic> for MetaDiagnostic {
45    fn from(d: TerminationDiagnostic) -> Self {
46        MetaDiagnostic {
47            code: d.code,
48            message: d.message,
49        }
50    }
51}
52
53/// Which checker produced this entry. `Totality` and `Coverage` are
54/// reported per relation; `Termination` is reported per definition.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum CheckKind {
57    Totality,
58    Coverage,
59    Termination,
60}
61
62impl CheckKind {
63    pub fn as_str(&self) -> &'static str {
64        match self {
65            CheckKind::Totality => "totality",
66            CheckKind::Coverage => "coverage",
67            CheckKind::Termination => "termination",
68        }
69    }
70}
71
72/// One entry in a [`MetatheoremResult`]: which sub-check ran, whether it
73/// passed, and which diagnostics it emitted on failure.
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct MetatheoremCheck {
76    pub kind: CheckKind,
77    pub ok: bool,
78    pub diagnostics: Vec<MetaDiagnostic>,
79}
80
81/// Per-relation (or per-definition) outcome — combines every sub-check
82/// run for that name.
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct MetatheoremResult {
85    pub name: String,
86    pub ok: bool,
87    pub checks: Vec<MetatheoremCheck>,
88}
89
90/// Top-level structured report returned by [`check_metatheorems`]. The
91/// CLI uses [`format_report`] to render it; library consumers can read
92/// the structured fields directly.
93#[derive(Debug, Clone, PartialEq)]
94pub struct MetatheoremReport {
95    pub ok: bool,
96    pub evaluation_diagnostics: Vec<Diagnostic>,
97    pub relations: Vec<MetatheoremResult>,
98    pub definitions: Vec<MetatheoremResult>,
99}
100
101fn check_relation(env: &Env, name: &str) -> MetatheoremResult {
102    let totality = is_total(env, name);
103    let coverage = is_covered(env, name);
104    let totality_check = MetatheoremCheck {
105        kind: CheckKind::Totality,
106        ok: totality.ok,
107        diagnostics: totality.diagnostics.into_iter().map(Into::into).collect(),
108    };
109    let coverage_check = MetatheoremCheck {
110        kind: CheckKind::Coverage,
111        ok: coverage.ok,
112        diagnostics: coverage.diagnostics.into_iter().map(Into::into).collect(),
113    };
114    let ok = totality_check.ok && coverage_check.ok;
115    MetatheoremResult {
116        name: name.to_string(),
117        ok,
118        checks: vec![totality_check, coverage_check],
119    }
120}
121
122fn check_definition(env: &Env, name: &str) -> MetatheoremResult {
123    let term = is_terminating(env, name);
124    let check = MetatheoremCheck {
125        kind: CheckKind::Termination,
126        ok: term.ok,
127        diagnostics: term.diagnostics.into_iter().map(Into::into).collect(),
128    };
129    let ok = check.ok;
130    MetatheoremResult {
131        name: name.to_string(),
132        ok,
133        checks: vec![check],
134    }
135}
136
137/// Public API: evaluate `text`, then enumerate every relation that has a
138/// `(mode ...)` declaration plus matching `(relation ...)` clauses, and
139/// every definition that has a `(define ...)` declaration. Each candidate
140/// becomes a metatheorem result.
141///
142/// `file` is the source path used in evaluation diagnostics; it is
143/// optional and only affects the rendered span.
144pub fn check_metatheorems(text: &str, file: Option<&str>) -> MetatheoremReport {
145    let mut env = Env::new(None);
146    let evaluation = evaluate_with_env(text, file, &mut env);
147
148    let mut relation_names: Vec<String> = env.modes.keys().cloned().collect();
149    relation_names.sort();
150    let mut relations: Vec<MetatheoremResult> = Vec::new();
151    for name in &relation_names {
152        let clauses = env.relations.get(name);
153        // A `(mode ...)` declaration with no matching `(relation ...)` body
154        // is not a metatheorem candidate — surface it through the existing
155        // E032 path rather than fabricating a result.
156        if clauses.map_or(true, |c| c.is_empty()) {
157            continue;
158        }
159        relations.push(check_relation(&env, name));
160    }
161
162    let mut definition_names: Vec<String> = env.definitions.keys().cloned().collect();
163    definition_names.sort();
164    let mut definitions: Vec<MetatheoremResult> = Vec::new();
165    for name in &definition_names {
166        definitions.push(check_definition(&env, name));
167    }
168
169    let ok = evaluation.diagnostics.is_empty()
170        && relations.iter().all(|r| r.ok)
171        && definitions.iter().all(|d| d.ok);
172    MetatheoremReport {
173        ok,
174        evaluation_diagnostics: evaluation.diagnostics,
175        relations,
176        definitions,
177    }
178}
179
180fn format_check(check: &MetatheoremCheck) -> Vec<String> {
181    let status = if check.ok { "pass" } else { "fail" };
182    let mut lines = vec![format!("  - {}: {}", check.kind.as_str(), status)];
183    for diag in &check.diagnostics {
184        lines.push(format!("      {} {}", diag.code, diag.message).trim_end().to_string());
185    }
186    lines
187}
188
189/// Render a [`MetatheoremReport`] as the same human-readable text the CLI
190/// prints. Useful in tests that want to assert on the rendered output as
191/// well as the structured shape.
192pub fn format_report(report: &MetatheoremReport) -> String {
193    let mut lines: Vec<String> = Vec::new();
194    for diag in &report.evaluation_diagnostics {
195        lines.push(format_diagnostic(diag, None));
196    }
197    if report.relations.is_empty() && report.definitions.is_empty() {
198        lines.push(
199            "No metatheorem candidates found (no `(mode ...)` or `(define ...)` declarations)."
200                .to_string(),
201        );
202        return lines.join("\n");
203    }
204    if !report.relations.is_empty() {
205        lines.push("Relations:".to_string());
206        for rel in &report.relations {
207            let status = if rel.ok { "OK" } else { "FAIL" };
208            lines.push(format!("  {}: {}", status, rel.name));
209            for check in &rel.checks {
210                lines.extend(format_check(check));
211            }
212        }
213    }
214    if !report.definitions.is_empty() {
215        lines.push("Definitions:".to_string());
216        for def in &report.definitions {
217            let status = if def.ok { "OK" } else { "FAIL" };
218            lines.push(format!("  {}: {}", status, def.name));
219            for check in &def.checks {
220                lines.extend(format_check(check));
221            }
222        }
223    }
224    lines.push(
225        if report.ok {
226            "All metatheorems hold."
227        } else {
228            "One or more metatheorems failed."
229        }
230        .to_string(),
231    );
232    lines.join("\n")
233}