Skip to main content

trident/verify/report/
mod.rs

1//! Machine-readable JSON verification reports for LLM consumption and CI/CD.
2//!
3//! Serializes `VerificationReport` and `ConstraintSystem` into a structured
4//! JSON format designed for automated tooling. Uses manual JSON formatting
5//! (no serde) following the same pattern as `cost.rs`.
6
7use crate::solve::{format_constraint, Counterexample, Verdict, VerificationReport};
8#[cfg(test)]
9use crate::sym::SymValue;
10use crate::sym::{Constraint, ConstraintSystem};
11
12// ─── Data Structures ───────────────────────────────────────────────
13
14/// Machine-readable verification report in JSON format.
15/// Designed for LLM consumption and CI/CD integration.
16pub struct JsonReport {
17    pub version: u32,
18    pub file: String,
19    pub verdict: String,
20    pub summary: JsonSummary,
21    pub constraints: Vec<JsonConstraint>,
22    pub counterexamples: Vec<JsonCounterexample>,
23    pub redundant_assertions: Vec<usize>,
24    pub suggestions: Vec<JsonSuggestion>,
25}
26
27pub struct JsonSummary {
28    pub total_constraints: usize,
29    pub active_constraints: usize,
30    pub variables: usize,
31    pub pub_inputs: usize,
32    pub divine_inputs: usize,
33    pub pub_outputs: usize,
34    pub static_violations: usize,
35    pub random_violations: usize,
36    pub bmc_violations: usize,
37}
38
39pub struct JsonConstraint {
40    pub index: usize,
41    pub kind: String,
42    pub expression: String,
43    pub is_trivial: bool,
44    pub is_violated: bool,
45}
46
47pub struct JsonCounterexample {
48    pub constraint_index: usize,
49    pub constraint_desc: String,
50    pub source: String,
51    pub assignments: Vec<(String, u64)>,
52}
53
54pub struct JsonSuggestion {
55    pub kind: String,
56    pub message: String,
57    pub constraint_index: Option<usize>,
58}
59
60// ─── JSON Helpers ──────────────────────────────────────────────────
61
62/// Escape a string for JSON output.
63fn json_escape(s: &str) -> String {
64    let mut out = String::with_capacity(s.len());
65    for ch in s.chars() {
66        match ch {
67            '"' => out.push_str("\\\""),
68            '\\' => out.push_str("\\\\"),
69            '\n' => out.push_str("\\n"),
70            '\r' => out.push_str("\\r"),
71            '\t' => out.push_str("\\t"),
72            c if (c as u32) < 0x20 => {
73                out.push_str(&format!("\\u{:04x}", c as u32));
74            }
75            c => out.push(c),
76        }
77    }
78    out
79}
80
81/// Write an indented JSON string value: `"key": "value"`.
82fn json_str(indent: usize, key: &str, value: &str) -> String {
83    let pad = " ".repeat(indent);
84    format!("{}\"{}\": \"{}\"", pad, key, json_escape(value))
85}
86
87/// Write an indented JSON integer value: `"key": value`.
88fn json_uint(indent: usize, key: &str, value: usize) -> String {
89    let pad = " ".repeat(indent);
90    format!("{}\"{}\": {}", pad, key, value)
91}
92
93/// Write an indented JSON boolean value: `"key": true/false`.
94fn json_bool(indent: usize, key: &str, value: bool) -> String {
95    let pad = " ".repeat(indent);
96    format!(
97        "{}\"{}\": {}",
98        pad,
99        key,
100        if value { "true" } else { "false" }
101    )
102}
103
104// ─── Constraint Formatting ─────────────────────────────────────────
105
106/// Format a single constraint as a `JsonConstraint`.
107pub fn format_json_constraint(c: &Constraint, index: usize) -> JsonConstraint {
108    let kind = match c {
109        Constraint::Equal(..) => "equal",
110        Constraint::AssertTrue(..) => "assert_true",
111        Constraint::Conditional(..) => "conditional",
112        Constraint::RangeU32(..) => "range_u32",
113        Constraint::DigestEqual(..) => "digest_equal",
114    };
115    JsonConstraint {
116        index,
117        kind: kind.to_string(),
118        expression: format_constraint(c),
119        is_trivial: c.is_trivial(),
120        is_violated: c.is_violated(),
121    }
122}
123
124/// Convert a `Counterexample` from the solver into a `JsonCounterexample`.
125fn convert_counterexample(ce: &Counterexample, source: &str) -> JsonCounterexample {
126    let mut assignments: Vec<(String, u64)> = ce
127        .assignments
128        .iter()
129        .filter(|(k, _)| !k.starts_with("__"))
130        .map(|(k, v)| (k.clone(), *v))
131        .collect();
132    assignments.sort_by(|(a, _), (b, _)| a.cmp(b));
133    JsonCounterexample {
134        constraint_index: ce.constraint_index,
135        constraint_desc: ce.constraint_desc.clone(),
136        source: source.to_string(),
137        assignments,
138    }
139}
140
141// --- Suggestion Generation ---
142
143pub(super) mod suggestions;
144pub use suggestions::generate_suggestions;
145
146// ─── Report Generation ─────────────────────────────────────────────
147
148/// Create a full JSON verification report.
149pub fn generate_json_report(
150    file_name: &str,
151    system: &ConstraintSystem,
152    report: &VerificationReport,
153) -> String {
154    let verdict_str = match report.verdict {
155        Verdict::Safe => "safe",
156        Verdict::StaticViolation | Verdict::RandomViolation | Verdict::BmcViolation => "unsafe",
157    };
158
159    let constraints: Vec<JsonConstraint> = system
160        .constraints
161        .iter()
162        .enumerate()
163        .map(|(i, c)| format_json_constraint(c, i))
164        .collect();
165
166    let mut counterexamples: Vec<JsonCounterexample> = Vec::new();
167    for ce in &report.random_result.counterexamples {
168        counterexamples.push(convert_counterexample(ce, "random"));
169    }
170    for ce in &report.bmc_result.counterexamples {
171        counterexamples.push(convert_counterexample(ce, "bmc"));
172    }
173
174    let suggestions = generate_suggestions(system, report);
175
176    let json_report = JsonReport {
177        version: 1,
178        file: file_name.to_string(),
179        verdict: verdict_str.to_string(),
180        summary: JsonSummary {
181            total_constraints: system.constraints.len(),
182            active_constraints: system.active_constraints(),
183            variables: system.num_variables as usize,
184            pub_inputs: system.pub_inputs.len(),
185            divine_inputs: system.divine_inputs.len(),
186            pub_outputs: system.pub_outputs.len(),
187            static_violations: report.static_violations.len(),
188            random_violations: report.random_result.counterexamples.len(),
189            bmc_violations: report.bmc_result.counterexamples.len(),
190        },
191        constraints,
192        counterexamples,
193        redundant_assertions: report.redundant_assertions.clone(),
194        suggestions,
195    };
196
197    serialize_report(&json_report)
198}
199
200// ─── JSON Serialization ────────────────────────────────────────────
201
202fn serialize_report(r: &JsonReport) -> String {
203    let mut out = String::with_capacity(4096);
204    out.push_str("{\n");
205    out.push_str(&json_uint(2, "version", r.version as usize));
206    out.push_str(",\n");
207    out.push_str(&json_str(2, "file", &r.file));
208    out.push_str(",\n");
209    out.push_str(&json_str(2, "verdict", &r.verdict));
210    out.push_str(",\n");
211
212    // summary
213    out.push_str("  \"summary\": {\n");
214    out.push_str(&json_uint(
215        4,
216        "total_constraints",
217        r.summary.total_constraints,
218    ));
219    out.push_str(",\n");
220    out.push_str(&json_uint(
221        4,
222        "active_constraints",
223        r.summary.active_constraints,
224    ));
225    out.push_str(",\n");
226    out.push_str(&json_uint(4, "variables", r.summary.variables));
227    out.push_str(",\n");
228    out.push_str(&json_uint(4, "pub_inputs", r.summary.pub_inputs));
229    out.push_str(",\n");
230    out.push_str(&json_uint(4, "divine_inputs", r.summary.divine_inputs));
231    out.push_str(",\n");
232    out.push_str(&json_uint(4, "pub_outputs", r.summary.pub_outputs));
233    out.push_str(",\n");
234    out.push_str(&json_uint(
235        4,
236        "static_violations",
237        r.summary.static_violations,
238    ));
239    out.push_str(",\n");
240    out.push_str(&json_uint(
241        4,
242        "random_violations",
243        r.summary.random_violations,
244    ));
245    out.push_str(",\n");
246    out.push_str(&json_uint(4, "bmc_violations", r.summary.bmc_violations));
247    out.push('\n');
248    out.push_str("  },\n");
249
250    // constraints
251    out.push_str("  \"constraints\": [\n");
252    for (i, c) in r.constraints.iter().enumerate() {
253        out.push_str(&serialize_constraint(c));
254        if i + 1 < r.constraints.len() {
255            out.push(',');
256        }
257        out.push('\n');
258    }
259    out.push_str("  ],\n");
260
261    // counterexamples
262    out.push_str("  \"counterexamples\": [\n");
263    for (i, ce) in r.counterexamples.iter().enumerate() {
264        out.push_str(&serialize_counterexample(ce));
265        if i + 1 < r.counterexamples.len() {
266            out.push(',');
267        }
268        out.push('\n');
269    }
270    out.push_str("  ],\n");
271
272    // redundant_assertions
273    out.push_str("  \"redundant_assertions\": [");
274    for (i, idx) in r.redundant_assertions.iter().enumerate() {
275        if i > 0 {
276            out.push_str(", ");
277        }
278        out.push_str(&idx.to_string());
279    }
280    out.push_str("],\n");
281
282    // suggestions
283    out.push_str("  \"suggestions\": [\n");
284    for (i, s) in r.suggestions.iter().enumerate() {
285        out.push_str(&serialize_suggestion(s));
286        if i + 1 < r.suggestions.len() {
287            out.push(',');
288        }
289        out.push('\n');
290    }
291    out.push_str("  ]\n");
292
293    out.push_str("}\n");
294    out
295}
296
297fn serialize_constraint(c: &JsonConstraint) -> String {
298    let mut out = String::new();
299    out.push_str("    {\n");
300    out.push_str(&json_uint(6, "index", c.index));
301    out.push_str(",\n");
302    out.push_str(&json_str(6, "kind", &c.kind));
303    out.push_str(",\n");
304    out.push_str(&json_str(6, "expression", &c.expression));
305    out.push_str(",\n");
306    out.push_str(&json_bool(6, "is_trivial", c.is_trivial));
307    out.push_str(",\n");
308    out.push_str(&json_bool(6, "is_violated", c.is_violated));
309    out.push('\n');
310    out.push_str("    }");
311    out
312}
313
314fn serialize_counterexample(ce: &JsonCounterexample) -> String {
315    let mut out = String::new();
316    out.push_str("    {\n");
317    out.push_str(&json_uint(6, "constraint_index", ce.constraint_index));
318    out.push_str(",\n");
319    out.push_str(&json_str(6, "constraint_desc", &ce.constraint_desc));
320    out.push_str(",\n");
321    out.push_str(&json_str(6, "source", &ce.source));
322    out.push_str(",\n");
323    out.push_str("      \"assignments\": {\n");
324    for (i, (name, value)) in ce.assignments.iter().enumerate() {
325        let pad = "        ";
326        out.push_str(&format!("{}\"{}\": {}", pad, json_escape(name), value));
327        if i + 1 < ce.assignments.len() {
328            out.push(',');
329        }
330        out.push('\n');
331    }
332    out.push_str("      }\n");
333    out.push_str("    }");
334    out
335}
336
337fn serialize_suggestion(s: &JsonSuggestion) -> String {
338    let mut out = String::new();
339    out.push_str("    {\n");
340    out.push_str(&json_str(6, "kind", &s.kind));
341    out.push_str(",\n");
342    out.push_str(&json_str(6, "message", &s.message));
343    out.push_str(",\n");
344    let pad = "      ";
345    match s.constraint_index {
346        Some(idx) => out.push_str(&format!("{}\"constraint_index\": {}\n", pad, idx)),
347        None => out.push_str(&format!("{}\"constraint_index\": null\n", pad)),
348    }
349    out.push_str("    }");
350    out
351}
352
353// ─── Tests ─────────────────────────────────────────────────────────
354
355#[cfg(test)]
356mod tests;