1use std::collections::{BTreeMap, HashMap, HashSet};
8
9use intent_lang_syntax::ast::*;
10use serde::Serialize;
11
12pub fn expr_to_text(e: &Spanned<Expr>) -> String {
15 fn go(e: &Spanned<Expr>) -> String {
16 match &e.node {
17 Expr::IntLit(n) => n.to_string(),
18 Expr::BoolLit(b) => b.to_string(),
19 Expr::StringLit(s) => format!("\"{s}\""),
20 Expr::Ident(n) => n.clone(),
21 Expr::Prime(inner) => format!("{}'", go(inner)),
22 Expr::FieldAccess(b, f) => format!("{}.{}", go(b), f),
23 Expr::Index(b, i) => format!("{}[{}]", go(b), go(i)),
24 Expr::BinOp(l, op, r) => format!("({} {} {})", go(l), op, go(r)),
25 Expr::UnaryOp(UnaryOp::Not, o) => format!("!{}", go(o)),
26 Expr::UnaryOp(UnaryOp::Neg, o) => format!("-{}", go(o)),
27 Expr::IfThenElse(c, t, el) => {
28 format!("if {} then {} else {}", go(c), go(t), go(el))
29 }
30 Expr::Forall(vs, body) => {
31 let vars = vs
32 .iter()
33 .map(|v| format!("{}: {}", v.name, v.ty))
34 .collect::<Vec<_>>()
35 .join(", ");
36 format!("forall {vars}, {}", go(body))
37 }
38 Expr::Exists(vs, body) => {
39 let vars = vs
40 .iter()
41 .map(|v| format!("{}: {}", v.name, v.ty))
42 .collect::<Vec<_>>()
43 .join(", ");
44 format!("exists {vars}, {}", go(body))
45 }
46 Expr::Call(n, args) => {
47 let a = args.iter().map(go).collect::<Vec<_>>().join(", ");
48 format!("{n}({a})")
49 }
50 Expr::Paren(inner) => format!("({})", go(inner)),
51 }
52 }
53 go(e)
54}
55
56pub fn intent_lifecycle(intent: &IntentDecl) -> Lifecycle {
59 let has = |n: &str| intent.annotations.iter().any(|a| a.name == n);
60 if has("asis") {
61 Lifecycle::AsIs
62 } else if has("tobe") {
63 Lifecycle::ToBe
64 } else {
65 Lifecycle::Current
66 }
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
70#[serde(rename_all = "kebab-case")]
71pub enum Lifecycle {
72 AsIs,
74 ToBe,
76 Current,
78}
79
80#[derive(Debug, Clone, Serialize)]
83pub struct CoverageReport {
84 pub coverages: Vec<CoverageScenarioReport>,
85}
86
87#[derive(Debug, Clone, Serialize)]
88pub struct CoverageScenarioReport {
89 pub name: String,
90 pub total_combinations: usize,
91 pub covered: usize,
92 pub uncovered: Vec<BTreeMap<String, String>>,
93 pub witnessed_by: BTreeMap<String, Vec<String>>,
94}
95
96pub fn coverage_report(prog: &Program) -> CoverageReport {
105 let coverages: Vec<&CoverageDecl> = prog
106 .declarations
107 .iter()
108 .filter_map(|d| match &d.node {
109 Declaration::Coverage(c) => Some(c),
110 _ => None,
111 })
112 .collect();
113
114 let mut all_clauses: Vec<(String, String)> = Vec::new(); for d in &prog.declarations {
116 match &d.node {
117 Declaration::Intent(i) => {
118 for cl in &i.clauses {
119 let e = match &cl.node {
120 Clause::Require(e) | Clause::Ensure(e) | Clause::Invariant(e) => e,
121 };
122 all_clauses.push((i.name.clone(), expr_to_text(e)));
123 }
124 }
125 Declaration::Safety(s) => {
126 for inv in &s.invariants {
127 all_clauses.push((s.name.clone(), expr_to_text(inv)));
128 }
129 }
130 Declaration::Theorem(t) => {
131 all_clauses.push((t.name.clone(), expr_to_text(&t.body)));
132 }
133 _ => {}
134 }
135 }
136
137 let mut scenarios = Vec::new();
138 for cov in coverages {
139 let dims: Vec<(String, Vec<String>)> = cov
141 .dimensions
142 .iter()
143 .map(|d| {
144 (
145 d.name.clone(),
146 d.values.iter().map(expr_to_text).collect::<Vec<_>>(),
147 )
148 })
149 .collect();
150
151 let combos = cartesian(&dims);
152 let total = combos.len();
153 let mut uncovered = Vec::new();
154 let mut witnessed_by: BTreeMap<String, Vec<String>> = BTreeMap::new();
155
156 for combo in &combos {
157 let key = combo
159 .iter()
160 .map(|(k, v)| format!("{k}={v}"))
161 .collect::<Vec<_>>()
162 .join(",");
163
164 let witness: Vec<String> = all_clauses
165 .iter()
166 .filter(|(_, text)| combo.iter().all(|(_, v)| text.contains(v)))
167 .map(|(n, _)| n.clone())
168 .collect();
169
170 if witness.is_empty() {
171 uncovered.push(combo.clone());
172 } else {
173 witnessed_by.insert(key, witness);
174 }
175 }
176
177 scenarios.push(CoverageScenarioReport {
178 name: cov.name.clone(),
179 total_combinations: total,
180 covered: total - uncovered.len(),
181 uncovered,
182 witnessed_by,
183 });
184 }
185
186 CoverageReport {
187 coverages: scenarios,
188 }
189}
190
191fn cartesian(dims: &[(String, Vec<String>)]) -> Vec<BTreeMap<String, String>> {
192 if dims.is_empty() {
193 return vec![BTreeMap::new()];
194 }
195 let (head_name, head_vals) = &dims[0];
196 let tail = cartesian(&dims[1..]);
197 let mut out = Vec::with_capacity(head_vals.len() * tail.len());
198 for v in head_vals {
199 for t in &tail {
200 let mut m = t.clone();
201 m.insert(head_name.clone(), v.clone());
202 out.push(m);
203 }
204 }
205 out
206}
207
208#[derive(Debug, Clone, Serialize)]
211pub struct TestSpec {
212 pub intents: Vec<IntentTestSpec>,
213}
214
215#[derive(Debug, Clone, Serialize)]
216pub struct IntentTestSpec {
217 pub intent: String,
218 pub lifecycle: Lifecycle,
219 pub params: Vec<String>,
220 pub scenarios: Vec<ScenarioRow>,
221}
222
223#[derive(Debug, Clone, Serialize)]
224pub struct ScenarioRow {
225 pub label: String,
227 pub assumptions: Vec<String>,
228 pub expected: String,
229}
230
231pub fn testspec(prog: &Program) -> TestSpec {
232 let mut intents = Vec::new();
233
234 for d in &prog.declarations {
235 if let Declaration::Intent(i) = &d.node {
236 let mut scenarios = Vec::new();
237
238 let requires: Vec<String> = i
239 .clauses
240 .iter()
241 .filter_map(|c| match &c.node {
242 Clause::Require(e) => Some(expr_to_text(e)),
243 _ => None,
244 })
245 .collect();
246 let ensures: Vec<String> = i
247 .clauses
248 .iter()
249 .filter_map(|c| match &c.node {
250 Clause::Ensure(e) => Some(expr_to_text(e)),
251 _ => None,
252 })
253 .collect();
254
255 scenarios.push(ScenarioRow {
257 label: "happy-path".to_string(),
258 assumptions: requires.clone(),
259 expected: if ensures.is_empty() {
260 "(no postconditions)".to_string()
261 } else {
262 ensures.join(" && ")
263 },
264 });
265
266 for (idx, r) in requires.iter().enumerate() {
268 scenarios.push(ScenarioRow {
269 label: format!("violates require[{idx}]"),
270 assumptions: vec![format!("!({r})")],
271 expected: "behavior unspecified — caller error".to_string(),
272 });
273 }
274
275 for (idx, e) in ensures.iter().enumerate() {
277 scenarios.push(ScenarioRow {
278 label: format!("witnesses ensure[{idx}]"),
279 assumptions: requires.clone(),
280 expected: e.clone(),
281 });
282 }
283
284 intents.push(IntentTestSpec {
285 intent: i.name.clone(),
286 lifecycle: intent_lifecycle(i),
287 params: i
288 .params
289 .iter()
290 .map(|p| format!("{}: {}", p.name, p.ty))
291 .collect(),
292 scenarios,
293 });
294 }
295 }
296
297 TestSpec { intents }
298}
299
300#[derive(Debug, Clone, Serialize)]
303pub struct DiffReport {
304 pub changes: Vec<Change>,
305 pub summary: DiffSummary,
306}
307
308#[derive(Debug, Clone, Serialize)]
309pub struct DiffSummary {
310 pub added: usize,
311 pub removed: usize,
312 pub modified: usize,
313 pub potentially_breaking: usize,
314}
315
316#[derive(Debug, Clone, Serialize)]
317#[serde(tag = "kind", rename_all = "kebab-case")]
318pub enum Change {
319 Added {
320 decl_kind: String,
321 name: String,
322 },
323 Removed {
324 decl_kind: String,
325 name: String,
326 },
327 Modified {
328 decl_kind: String,
329 name: String,
330 classification: ModificationKind,
331 details: Vec<String>,
332 },
333}
334
335#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
336#[serde(rename_all = "kebab-case")]
337pub enum ModificationKind {
338 Loosened,
340 Tightened,
342 Reshaped,
344}
345
346pub fn diff(old: &Program, new: &Program) -> DiffReport {
347 let old_idx = index_decls(old);
348 let new_idx = index_decls(new);
349
350 let mut changes = Vec::new();
351 let (mut added, mut removed, mut modified, mut breaking) = (0, 0, 0, 0);
352
353 for (k, sig) in &new_idx {
354 if !old_idx.contains_key(k) {
355 changes.push(Change::Added {
356 decl_kind: k.0.clone(),
357 name: k.1.clone(),
358 });
359 added += 1;
360 } else {
361 let old_sig = &old_idx[k];
362 if old_sig != sig {
363 let cls = classify(old_sig, sig);
364 if cls == ModificationKind::Tightened || cls == ModificationKind::Reshaped {
365 breaking += 1;
366 }
367 changes.push(Change::Modified {
368 decl_kind: k.0.clone(),
369 name: k.1.clone(),
370 classification: cls,
371 details: structural_details(old_sig, sig),
372 });
373 modified += 1;
374 }
375 }
376 }
377 for k in old_idx.keys() {
378 if !new_idx.contains_key(k) {
379 changes.push(Change::Removed {
380 decl_kind: k.0.clone(),
381 name: k.1.clone(),
382 });
383 removed += 1;
384 breaking += 1;
385 }
386 }
387
388 DiffReport {
389 changes,
390 summary: DiffSummary {
391 added,
392 removed,
393 modified,
394 potentially_breaking: breaking,
395 },
396 }
397}
398
399#[derive(Debug, Clone, PartialEq, Eq)]
401struct DeclSig {
402 requires: Vec<String>,
403 ensures: Vec<String>,
404 invariants: Vec<String>,
405 body: Vec<String>,
406}
407
408fn index_decls(prog: &Program) -> HashMap<(String, String), DeclSig> {
409 let mut out = HashMap::new();
410 for d in &prog.declarations {
411 let (k, sig) = match &d.node {
412 Declaration::Intent(i) => {
413 let mut r = vec![];
414 let mut e = vec![];
415 let mut iv = vec![];
416 for cl in &i.clauses {
417 match &cl.node {
418 Clause::Require(x) => r.push(expr_to_text(x)),
419 Clause::Ensure(x) => e.push(expr_to_text(x)),
420 Clause::Invariant(x) => iv.push(expr_to_text(x)),
421 }
422 }
423 (
424 ("intent".to_string(), i.name.clone()),
425 DeclSig {
426 requires: r,
427 ensures: e,
428 invariants: iv,
429 body: vec![],
430 },
431 )
432 }
433 Declaration::Safety(s) => (
434 ("safety".to_string(), s.name.clone()),
435 DeclSig {
436 requires: vec![],
437 ensures: vec![],
438 invariants: s.invariants.iter().map(expr_to_text).collect(),
439 body: vec![],
440 },
441 ),
442 Declaration::Theorem(t) => (
443 ("theorem".to_string(), t.name.clone()),
444 DeclSig {
445 requires: vec![],
446 ensures: vec![],
447 invariants: vec![],
448 body: vec![expr_to_text(&t.body)],
449 },
450 ),
451 Declaration::Goal(g) => (
452 ("goal".to_string(), g.name.clone()),
453 DeclSig {
454 requires: vec![],
455 ensures: vec![],
456 invariants: g.realized_by.clone(),
457 body: vec![
458 g.rationale.clone().unwrap_or_default(),
459 g.measure.clone().unwrap_or_default(),
460 ],
461 },
462 ),
463 Declaration::Coverage(c) => {
464 let body: Vec<String> = c
465 .dimensions
466 .iter()
467 .map(|d| {
468 format!(
469 "{}={}",
470 d.name,
471 d.values
472 .iter()
473 .map(expr_to_text)
474 .collect::<Vec<_>>()
475 .join("|")
476 )
477 })
478 .collect();
479 (
480 ("coverage".to_string(), c.name.clone()),
481 DeclSig {
482 requires: vec![],
483 ensures: vec![],
484 invariants: vec![],
485 body,
486 },
487 )
488 }
489 Declaration::Type(t) => (
490 ("type".to_string(), t.name.clone()),
491 DeclSig {
492 requires: vec![],
493 ensures: vec![],
494 invariants: vec![],
495 body: t
496 .fields
497 .iter()
498 .map(|f| format!("{}: {}", f.name, f.ty))
499 .collect(),
500 },
501 ),
502 Declaration::Enum(e) => (
503 ("enum".to_string(), e.name.clone()),
504 DeclSig {
505 requires: vec![],
506 ensures: vec![],
507 invariants: vec![],
508 body: e.variants.clone(),
509 },
510 ),
511 _ => continue,
512 };
513 out.insert(k, sig);
514 }
515 out
516}
517
518fn classify(old: &DeclSig, new: &DeclSig) -> ModificationKind {
519 let old_req: HashSet<_> = old.requires.iter().collect();
524 let new_req: HashSet<_> = new.requires.iter().collect();
525 let old_ens: HashSet<_> = old.ensures.iter().collect();
526 let new_ens: HashSet<_> = new.ensures.iter().collect();
527
528 let req_subset = new_req.is_subset(&old_req);
529 let req_superset = new_req.is_superset(&old_req);
530 let ens_superset = new_ens.is_superset(&old_ens);
531 let ens_subset = new_ens.is_subset(&old_ens);
532
533 if req_subset && ens_superset && (new_req != old_req || new_ens != old_ens) {
534 ModificationKind::Loosened
535 } else if req_superset && ens_subset && (new_req != old_req || new_ens != old_ens) {
536 ModificationKind::Tightened
537 } else {
538 ModificationKind::Reshaped
539 }
540}
541
542fn structural_details(old: &DeclSig, new: &DeclSig) -> Vec<String> {
543 let mut out = Vec::new();
544 diff_set("require", &old.requires, &new.requires, &mut out);
545 diff_set("ensure", &old.ensures, &new.ensures, &mut out);
546 diff_set("invariant", &old.invariants, &new.invariants, &mut out);
547 diff_set("body", &old.body, &new.body, &mut out);
548 out
549}
550
551fn diff_set(label: &str, old: &[String], new: &[String], out: &mut Vec<String>) {
552 let o: HashSet<_> = old.iter().cloned().collect();
553 let n: HashSet<_> = new.iter().cloned().collect();
554 for s in n.difference(&o) {
555 out.push(format!("+ {label}: {s}"));
556 }
557 for s in o.difference(&n) {
558 out.push(format!("- {label}: {s}"));
559 }
560}
561
562#[derive(Debug, Clone, Serialize)]
565pub struct ImpactReport {
566 pub diff: DiffReport,
567 pub affected_goals: Vec<String>,
568 pub affected_coverages: Vec<String>,
569}
570
571pub fn impact(old: &Program, new: &Program) -> ImpactReport {
572 let d = diff(old, new);
573
574 let mut touched_names: HashSet<String> = HashSet::new();
576 for c in &d.changes {
577 match c {
578 Change::Modified { name, .. } | Change::Removed { name, .. } => {
579 touched_names.insert(name.clone());
580 }
581 _ => {}
582 }
583 }
584
585 let mut affected_goals = Vec::new();
586 let mut affected_coverages = Vec::new();
587 for decl in &new.declarations {
588 match &decl.node {
589 Declaration::Goal(g) => {
590 if g.realized_by.iter().any(|n| touched_names.contains(n)) {
591 affected_goals.push(g.name.clone());
592 }
593 }
594 Declaration::Coverage(c) => {
595 let dim_values: HashSet<String> = c
598 .dimensions
599 .iter()
600 .flat_map(|d| d.values.iter().map(expr_to_text))
601 .collect();
602 let mut touched_clauses_text: Vec<String> = Vec::new();
603 for old_d in &old.declarations {
604 if let Declaration::Intent(i) = &old_d.node {
605 if touched_names.contains(&i.name) {
606 for cl in &i.clauses {
607 let e = match &cl.node {
608 Clause::Require(e)
609 | Clause::Ensure(e)
610 | Clause::Invariant(e) => e,
611 };
612 touched_clauses_text.push(expr_to_text(e));
613 }
614 }
615 }
616 }
617 if dim_values
618 .iter()
619 .any(|v| touched_clauses_text.iter().any(|t| t.contains(v)))
620 {
621 affected_coverages.push(c.name.clone());
622 }
623 }
624 _ => {}
625 }
626 }
627
628 ImpactReport {
629 diff: d,
630 affected_goals,
631 affected_coverages,
632 }
633}
634
635#[derive(Debug, Clone, Serialize)]
638pub struct ExplainReport {
639 pub target: String,
640 pub kind: String,
641 pub plain_english: String,
642 pub satisfying_example: Option<String>,
643 pub violating_example: Option<String>,
644 pub clauses: Vec<ClauseExplanation>,
645}
646
647#[derive(Debug, Clone, Serialize)]
648pub struct ClauseExplanation {
649 pub kind: String,
650 pub formal: String,
651 pub natural: String,
652}
653
654pub fn explain(prog: &Program, name: &str) -> Option<ExplainReport> {
658 for d in &prog.declarations {
659 match &d.node {
660 Declaration::Intent(i) if i.name == name => {
661 let mut clauses = Vec::new();
662 for cl in &i.clauses {
663 let (kind, e) = match &cl.node {
664 Clause::Require(e) => ("precondition", e),
665 Clause::Ensure(e) => ("postcondition", e),
666 Clause::Invariant(e) => ("invariant", e),
667 };
668 let formal = expr_to_text(e);
669 let natural = naturalize(&formal);
670 clauses.push(ClauseExplanation {
671 kind: kind.to_string(),
672 formal,
673 natural,
674 });
675 }
676 let plain = format!(
677 "Intent `{}` describes the operation that takes {} and constrains its behavior via {} clause(s).",
678 i.name,
679 i.params
680 .iter()
681 .map(|p| format!("{}: {}", p.name, p.ty))
682 .collect::<Vec<_>>()
683 .join(", "),
684 clauses.len()
685 );
686 return Some(ExplainReport {
687 target: i.name.clone(),
688 kind: "intent".to_string(),
689 plain_english: plain,
690 satisfying_example: Some(
692 "(run `intent check` and inspect Z3 model — automated witnesses pending)"
693 .to_string(),
694 ),
695 violating_example: Some(
696 "(violation witness requires running with --counterexample flag)"
697 .to_string(),
698 ),
699 clauses,
700 });
701 }
702 Declaration::Safety(s) if s.name == name => {
703 let mut clauses = Vec::new();
704 for inv in &s.invariants {
705 let formal = expr_to_text(inv);
706 let natural = naturalize(&formal);
707 clauses.push(ClauseExplanation {
708 kind: "invariant".to_string(),
709 formal,
710 natural,
711 });
712 }
713 let plain = format!(
714 "Safety rule `{}` declares {} invariant(s) that must hold across every intent that touches its parameters.",
715 s.name,
716 clauses.len()
717 );
718 return Some(ExplainReport {
719 target: s.name.clone(),
720 kind: "safety".to_string(),
721 plain_english: plain,
722 satisfying_example: None,
723 violating_example: None,
724 clauses,
725 });
726 }
727 Declaration::Goal(g) if g.name == name => {
728 let plain = format!(
729 "Goal \"{}\" — rationale: {}; measured by: {}; realized by: {}.",
730 g.name,
731 g.rationale.clone().unwrap_or_else(|| "(none)".to_string()),
732 g.measure.clone().unwrap_or_else(|| "(none)".to_string()),
733 if g.realized_by.is_empty() {
734 "(unlinked)".to_string()
735 } else {
736 g.realized_by.join(", ")
737 }
738 );
739 return Some(ExplainReport {
740 target: g.name.clone(),
741 kind: "goal".to_string(),
742 plain_english: plain,
743 satisfying_example: None,
744 violating_example: None,
745 clauses: vec![],
746 });
747 }
748 _ => {}
749 }
750 }
751 None
752}
753
754fn naturalize(formal: &str) -> String {
757 formal
758 .replace("==>", "implies")
759 .replace("&&", "and")
760 .replace("||", "or")
761 .replace("==", "equals")
762 .replace("!=", "is not equal to")
763 .replace(">=", "is at least")
764 .replace("<=", "is at most")
765 .replace("forall", "for every")
766 .replace("exists", "there exists")
767}