1use std::fmt;
9
10use serde::{Deserialize, Serialize};
11
12use crate::types::*;
13use crate::verify::Obligation;
14
15#[derive(Debug, Clone, Serialize)]
17pub struct AuditReport {
18 pub module_name: String,
19 pub entries: Vec<AuditEntry>,
20 pub summary: CoverageSummary,
21 pub verify_error_count: usize,
22 pub obligations: Vec<Obligation>,
23}
24
25#[derive(Debug, Clone, Serialize)]
27pub struct AuditEntry {
28 pub kind: SpecItemKind,
29 pub name: String,
30 pub line: usize,
31 pub parts: Vec<TracePart>,
32 pub related_obligations: Vec<String>,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
38pub enum SpecItemKind {
39 Entity,
40 Action,
41 Invariant,
42 EdgeCases,
43}
44
45#[derive(Debug, Clone, Serialize)]
47pub struct TracePart {
48 pub label: String,
50 pub ir_desc: String,
52 pub line: usize,
54}
55
56#[derive(Debug, Clone, Serialize)]
58pub struct CoverageSummary {
59 pub entities: usize,
60 pub fields: usize,
61 pub actions: usize,
62 pub params: usize,
63 pub preconditions: usize,
64 pub postconditions: usize,
65 pub properties: usize,
66 pub invariants: usize,
67 pub edge_guards: usize,
68}
69
70impl CoverageSummary {
71 pub fn total(&self) -> usize {
72 self.entities
73 + self.fields
74 + self.actions
75 + self.params
76 + self.preconditions
77 + self.postconditions
78 + self.properties
79 + self.invariants
80 + self.edge_guards
81 }
82}
83
84pub fn generate_audit(
87 source: &str,
88 module: &Module,
89 verify_errors: &[crate::verify::VerifyError],
90 obligations: &[Obligation],
91) -> AuditReport {
92 let line_index = build_line_index(source);
93 let mut entries = Vec::new();
94
95 for s in &module.structs {
97 let mut parts = Vec::new();
98 for f in &s.fields {
99 parts.push(TracePart {
100 label: format!("field:{}", f.name),
101 ir_desc: format!("{}: {}", f.name, format_ir_type(&f.ty)),
102 line: offset_to_line(&line_index, f.trace.span.start),
103 });
104 }
105 entries.push(AuditEntry {
106 kind: SpecItemKind::Entity,
107 name: s.name.clone(),
108 line: offset_to_line(&line_index, s.trace.span.start),
109 parts,
110 related_obligations: vec![],
111 });
112 }
113
114 for func in &module.functions {
116 let mut parts = Vec::new();
117 for p in &func.params {
118 parts.push(TracePart {
119 label: format!("param:{}", p.name),
120 ir_desc: format!("{}: {}", p.name, format_ir_type(&p.ty)),
121 line: offset_to_line(&line_index, p.trace.span.start),
122 });
123 }
124 for (i, pre) in func.preconditions.iter().enumerate() {
125 parts.push(TracePart {
126 label: format!("requires[{}]", i),
127 ir_desc: "precondition".into(),
128 line: offset_to_line(&line_index, pre.trace.span.start),
129 });
130 }
131 for (i, post) in func.postconditions.iter().enumerate() {
132 let (trace, desc) = match post {
133 Postcondition::Always { trace, .. } => (trace, "postcondition"),
134 Postcondition::When { trace, .. } => (trace, "conditional postcondition"),
135 };
136 parts.push(TracePart {
137 label: format!("ensures[{}]", i),
138 ir_desc: desc.into(),
139 line: offset_to_line(&line_index, trace.span.start),
140 });
141 }
142 for prop in &func.properties {
143 parts.push(TracePart {
144 label: format!("property:{}", prop.key),
145 ir_desc: format!("{}: {}", prop.key, format_prop_value(&prop.value)),
146 line: offset_to_line(&line_index, prop.trace.span.start),
147 });
148 }
149
150 let related: Vec<String> = obligations
151 .iter()
152 .filter(|o| o.action == func.name)
153 .map(|o| o.to_string())
154 .collect();
155
156 entries.push(AuditEntry {
157 kind: SpecItemKind::Action,
158 name: func.name.clone(),
159 line: offset_to_line(&line_index, func.trace.span.start),
160 parts,
161 related_obligations: related,
162 });
163 }
164
165 for inv in &module.invariants {
167 let related: Vec<String> = obligations
168 .iter()
169 .filter(|o| o.invariant == inv.name)
170 .map(|o| o.to_string())
171 .collect();
172
173 entries.push(AuditEntry {
174 kind: SpecItemKind::Invariant,
175 name: inv.name.clone(),
176 line: offset_to_line(&line_index, inv.trace.span.start),
177 parts: vec![],
178 related_obligations: related,
179 });
180 }
181
182 if !module.edge_guards.is_empty() {
184 let first_line = offset_to_line(&line_index, module.edge_guards[0].trace.span.start);
185 let mut parts = Vec::new();
186 for (i, guard) in module.edge_guards.iter().enumerate() {
187 parts.push(TracePart {
188 label: format!("guard[{}]", i),
189 ir_desc: format!("when ... => {}", guard.action),
190 line: offset_to_line(&line_index, guard.trace.span.start),
191 });
192 }
193 entries.push(AuditEntry {
194 kind: SpecItemKind::EdgeCases,
195 name: "edge_cases".into(),
196 line: first_line,
197 parts,
198 related_obligations: vec![],
199 });
200 }
201
202 let summary = CoverageSummary {
203 entities: module.structs.len(),
204 fields: module.structs.iter().map(|s| s.fields.len()).sum(),
205 actions: module.functions.len(),
206 params: module.functions.iter().map(|f| f.params.len()).sum(),
207 preconditions: module.functions.iter().map(|f| f.preconditions.len()).sum(),
208 postconditions: module
209 .functions
210 .iter()
211 .map(|f| f.postconditions.len())
212 .sum(),
213 properties: module.functions.iter().map(|f| f.properties.len()).sum(),
214 invariants: module.invariants.len(),
215 edge_guards: module.edge_guards.len(),
216 };
217
218 AuditReport {
219 module_name: module.name.clone(),
220 entries,
221 summary,
222 verify_error_count: verify_errors.len(),
223 obligations: obligations.to_vec(),
224 }
225}
226
227impl fmt::Display for SpecItemKind {
230 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231 match self {
232 SpecItemKind::Entity => write!(f, "Entity"),
233 SpecItemKind::Action => write!(f, "Action"),
234 SpecItemKind::Invariant => write!(f, "Invariant"),
235 SpecItemKind::EdgeCases => write!(f, "EdgeCases"),
236 }
237 }
238}
239
240impl AuditReport {
241 pub fn format_trace_map(&self) -> String {
243 let mut out = format!("{} — Audit Trace Map\n", self.module_name);
244
245 for entry in &self.entries {
246 out.push('\n');
247 let ir_kind = match entry.kind {
248 SpecItemKind::Entity => "Struct",
249 SpecItemKind::Action => "Function",
250 SpecItemKind::Invariant => "Invariant",
251 SpecItemKind::EdgeCases => "Guards",
252 };
253
254 if entry.kind == SpecItemKind::EdgeCases {
255 out.push_str(&format!(" EdgeCases -> {ir_kind}\n"));
256 } else {
257 out.push_str(&format!(" {} {} -> {ir_kind}", entry.kind, entry.name,));
258 out.push_str(&format!(" [L{}]\n", entry.line));
259 }
260
261 let total = entry.parts.len() + entry.related_obligations.len();
262 for (i, part) in entry.parts.iter().enumerate() {
263 let is_last = i == total - 1;
264 let prefix = if is_last { "└──" } else { "├──" };
265 out.push_str(&format!(
266 " {prefix} {}: {} [L{}]\n",
267 part.label, part.ir_desc, part.line,
268 ));
269 }
270
271 for (i, ob) in entry.related_obligations.iter().enumerate() {
272 let is_last = entry.parts.len() + i == total - 1;
273 let prefix = if is_last { "└──" } else { "├──" };
274 out.push_str(&format!(" {prefix} obligation: {ob}\n"));
275 }
276 }
277
278 out.push_str(&format!(
279 "\nCoverage: {total} spec items compiled",
280 total = self.summary.total(),
281 ));
282 if self.verify_error_count == 0 {
283 out.push_str(", verification clean\n");
284 } else {
285 out.push_str(&format!(
286 ", {} verification error(s)\n",
287 self.verify_error_count
288 ));
289 }
290 if !self.obligations.is_empty() {
291 out.push_str(&format!(
292 "Obligations: {} identified\n",
293 self.obligations.len()
294 ));
295 }
296
297 out
298 }
299
300 pub fn format_coverage(&self) -> String {
302 let s = &self.summary;
303 let mut out = format!("{} — Coverage Summary\n\n", self.module_name);
304
305 out.push_str(&format!(
306 " Entities: {:>3} ({} fields)\n",
307 s.entities, s.fields,
308 ));
309 out.push_str(&format!(
310 " Actions: {:>3} ({} params, {} requires, {} ensures, {} properties)\n",
311 s.actions, s.params, s.preconditions, s.postconditions, s.properties,
312 ));
313 out.push_str(&format!(" Invariants: {:>3}\n", s.invariants));
314 out.push_str(&format!(" Edge guards: {:>3}\n", s.edge_guards));
315 out.push_str(" ─────────────────────────────\n");
316 out.push_str(&format!(
317 " Total: {:>3} spec items compiled\n",
318 s.total(),
319 ));
320
321 out.push('\n');
322 if self.verify_error_count == 0 {
323 out.push_str(" Verification: clean (0 errors)\n");
324 } else {
325 out.push_str(&format!(
326 " Verification: {} error(s)\n",
327 self.verify_error_count,
328 ));
329 }
330
331 if self.obligations.is_empty() {
332 out.push_str(" Obligations: none\n");
333 } else {
334 out.push_str(&format!(
335 " Obligations: {} identified\n",
336 self.obligations.len(),
337 ));
338 for ob in &self.obligations {
339 out.push_str(&format!(" - {ob}\n"));
340 }
341 }
342
343 out
344 }
345}
346
347fn build_line_index(source: &str) -> Vec<usize> {
351 let mut starts = vec![0];
352 for (i, ch) in source.char_indices() {
353 if ch == '\n' {
354 starts.push(i + 1);
355 }
356 }
357 starts
358}
359
360fn offset_to_line(line_starts: &[usize], offset: usize) -> usize {
362 match line_starts.binary_search(&offset) {
363 Ok(i) => i + 1,
364 Err(i) => i,
365 }
366}
367
368fn format_ir_type(ty: &IrType) -> String {
369 match ty {
370 IrType::Named(n) => n.clone(),
371 IrType::Struct(n) => n.clone(),
372 IrType::List(inner) => format!("List<{}>", format_ir_type(inner)),
373 IrType::Set(inner) => format!("Set<{}>", format_ir_type(inner)),
374 IrType::Map(k, v) => {
375 format!("Map<{}, {}>", format_ir_type(k), format_ir_type(v))
376 }
377 IrType::Optional(inner) => format!("{}?", format_ir_type(inner)),
378 IrType::Union(variants) => variants.join(" | "),
379 IrType::Decimal(p) => format!("Decimal({p})"),
380 }
381}
382
383fn format_prop_value(v: &PropertyValue) -> String {
384 match v {
385 PropertyValue::Bool(b) => b.to_string(),
386 PropertyValue::Int(n) => n.to_string(),
387 PropertyValue::String(s) => format!("\"{s}\""),
388 PropertyValue::Ident(i) => i.clone(),
389 }
390}