Skip to main content

intent_lang_core/
analysis.rs

1//! Requirements-modeling analyses (RFC: A4, B2, B3, B4, B5).
2//!
3//! These analyses operate on the AST and produce reports for humans
4//! and downstream tools (popsicle Skills). All analyses are
5//! deterministic: pure AST → text/JSON, no Z3 invocation.
6
7use std::collections::{BTreeMap, HashMap, HashSet};
8
9use intent_lang_syntax::ast::*;
10use serde::Serialize;
11
12// ── Pretty-printer for Expr (used in reports) ───────────────────
13
14pub 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
56// ── @asis / @tobe annotation lookup (RFC A2) ────────────────────
57
58pub 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` — describes legacy code; excluded from primary consistency checks.
73    AsIs,
74    /// `@tobe` — describes target state; included in primary consistency checks.
75    ToBe,
76    /// No annotation — treated as current truth, included by default.
77    Current,
78}
79
80// ── B2: Coverage analysis ──────────────────────────────────────
81
82#[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
96/// For each `coverage` declaration, enumerate the cartesian product of
97/// declared dimension values. A combination is considered "covered" if
98/// some safety invariant or intent clause syntactically mentions every
99/// value name in the combination.
100///
101/// This is intentionally syntactic; it gives an honest first-pass
102/// signal, not formal completeness. False negatives are expected and
103/// acceptable per RFC §4 (under-engineering until dogfood).
104pub 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(); // (owner_name, text)
115    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        // Cartesian product over dimensions.
140        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            // Combo is covered if some clause text mentions every value.
158            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// ── B3: Testspec generation ────────────────────────────────────
209
210#[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    /// "happy-path", "violates require[0]", "violates ensure[1]", ...
226    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            // Happy path: all requires hold; expect all ensures hold.
256            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            // Negative cases: violate each require in turn.
267            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            // Boundary cases: each ensure interpreted as separate post.
276            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// ── A4 / B4: Diff & Impact ─────────────────────────────────────
301
302#[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    /// Conditions weakened — accepts strict superset of states. Backward compatible.
339    Loosened,
340    /// Conditions strengthened — rejects states previously accepted. Potentially breaking.
341    Tightened,
342    /// Both directions or unclassifiable structural change.
343    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/// Per-decl signature used for diffing.
400#[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    // Heuristic: if requires got *fewer/weaker* (subset of old) and
520    // ensures/invariants stayed/strengthened (superset of old), → Loosened.
521    // If requires grew (superset) or ensures shrank, → Tightened.
522    // Otherwise Reshaped.
523    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/// RFC B4: impact analysis. Walk goal.realized_by + coverage witnesses
563/// to find downstream effects of a diff.
564#[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    // For each modified/removed intent or safety, find goals referencing it.
575    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                // Coverage is affected if any touched intent had clauses that
596                // reference any dimension value.
597                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// ── B5: Explain ────────────────────────────────────────────────
636
637#[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
654/// Generate a plain-English explanation of an intent/safety/goal.
655/// This is template-based — no LLM call. The output is meant to be
656/// human-reviewed and is consumed by popsicle's adversarial-reviewer Skill.
657pub 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                    // Placeholders: real witnesses require Z3 model — TODO Phase 2.
691                    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
754/// Very simple naturalizer — replaces operators with English-like phrases.
755/// It is intentionally crude: humans review and refine. This is *not* an LLM.
756fn 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}