Skip to main content

intent_lang_core/
smt.rs

1use std::collections::{HashMap, HashSet};
2
3use intent_lang_syntax::ast::*;
4use z3::{Config, SatResult, Solver, with_z3_config};
5
6use crate::vcgen::{VcKind, VerificationCondition};
7
8// ── Result type ──────────────────────────────────────────────
9
10#[derive(Debug, Clone)]
11pub enum VerifyResult {
12    Verified,
13    Failed { counterexample: String },
14    Unknown { reason: String },
15    Error { message: String },
16}
17
18// ── Type info for encoding ───────────────────────────────────
19
20#[derive(Debug, Clone)]
21struct FieldInfo {
22    sort: String,
23}
24
25struct TypeInfo {
26    /// struct_name -> { field_name -> smt_sort }
27    structs: HashMap<String, HashMap<String, FieldInfo>>,
28    enums: HashSet<String>,
29}
30
31impl TypeInfo {
32    fn from_program(prog: &Program) -> Self {
33        let mut structs = HashMap::new();
34        let mut enums = HashSet::new();
35        for decl in &prog.declarations {
36            match &decl.node {
37                Declaration::Type(t) => {
38                    let mut fields = HashMap::new();
39                    for f in &t.fields {
40                        fields.insert(
41                            f.name.clone(),
42                            FieldInfo {
43                                sort: type_expr_to_smt(&f.ty),
44                            },
45                        );
46                    }
47                    structs.insert(t.name.clone(), fields);
48                }
49                Declaration::Enum(e) => {
50                    enums.insert(e.name.clone());
51                }
52                _ => {}
53            }
54        }
55        TypeInfo { structs, enums }
56    }
57
58    fn field_sort(&self, struct_name: &str, field: &str) -> Option<&str> {
59        self.structs
60            .get(struct_name)
61            .and_then(|f| f.get(field))
62            .map(|fi| fi.sort.as_str())
63    }
64}
65
66fn type_expr_to_smt(te: &TypeExpr) -> String {
67    match te {
68        TypeExpr::Named(n) => match n.as_str() {
69            "Int" => "Int".to_string(),
70            "Bool" => "Bool".to_string(),
71            "String" => "String".to_string(),
72            other => other.to_string(),
73        },
74        TypeExpr::Qualified(module, name) => {
75            // Use mangled name for SMT: module_Type
76            let full = format!("{module}_{name}");
77            match full.as_str() {
78                "Int" | "Bool" | "String" => full,
79                _ => full,
80            }
81        }
82        TypeExpr::Generic(name, args) => match name.as_str() {
83            "Seq" => format!("(Array Int {})", type_expr_to_smt(&args[0])),
84            "Set" => format!("(Array {} Bool)", type_expr_to_smt(&args[0])),
85            _ => name.clone(),
86        },
87    }
88}
89
90// ── SMT-LIB2 encoder ────────────────────────────────────────
91
92pub struct SmtEncoder {
93    /// Lines of SMT output, built top-down.
94    lines: Vec<String>,
95    declared: HashSet<String>,
96    type_info: TypeInfo,
97    /// param_name -> type_name (for struct parameters)
98    param_types: HashMap<String, String>,
99}
100
101impl SmtEncoder {
102    pub fn new(prog: &Program) -> Self {
103        Self {
104            lines: Vec::new(),
105            declared: HashSet::new(),
106            type_info: TypeInfo::from_program(prog),
107            param_types: HashMap::new(),
108        }
109    }
110
111    pub fn encode_vc(&mut self, vc: &VerificationCondition, prog: &Program) {
112        self.lines.clear();
113        self.declared.clear();
114        self.param_types.clear();
115
116        self.emit("(set-logic ALL)");
117
118        // Declare enum datatypes
119        for decl in &prog.declarations {
120            if let Declaration::Enum(e) = &decl.node {
121                let variants: Vec<String> = e.variants.iter().map(|v| format!("({v})")).collect();
122                self.emit(&format!(
123                    "(declare-datatype {} ({}))",
124                    e.name,
125                    variants.join(" ")
126                ));
127                self.declared.insert(e.name.clone());
128            }
129        }
130
131        // Flatten struct parameters into individual field constants
132        for d in &vc.declarations {
133            match d {
134                crate::vcgen::SmtDecl::DeclareConst(name, ty) => {
135                    let type_name = match ty {
136                        TypeExpr::Named(n) => n.clone(),
137                        TypeExpr::Qualified(m, n) => format!("{m}.{n}"),
138                        TypeExpr::Generic(n, _) => n.clone(),
139                    };
140
141                    if self.type_info.structs.contains_key(&type_name) {
142                        let field_entries: Vec<(String, String)> = self.type_info.structs
143                            [&type_name]
144                            .iter()
145                            .map(|(fname, fi)| (fname.clone(), fi.sort.clone()))
146                            .collect();
147                        self.param_types.insert(name.clone(), type_name.clone());
148                        for (field_name, sort) in &field_entries {
149                            let const_name = format!("{name}_{field_name}");
150                            self.declare_const(&const_name, sort);
151                            self.declare_const(&format!("{const_name}_prime"), sort);
152                        }
153                    } else {
154                        let sort = type_expr_to_smt(ty);
155                        self.declare_const(name, &sort);
156                        self.declare_const(&format!("{name}_prime"), &sort);
157                    }
158                }
159                _ => {}
160            }
161        }
162
163        match vc.kind {
164            VcKind::Intent => {
165                // Assert assumes (requires + ensures + invariant-pre)
166                for e in &vc.assumes {
167                    let smt = self.expr_to_smt(e);
168                    self.emit(&format!("(assert {smt})"));
169                }
170                // Negate conjunction of goals (invariants-post)
171                if !vc.goals.is_empty() {
172                    let goal_parts: Vec<String> =
173                        vc.goals.iter().map(|e| self.expr_to_smt(e)).collect();
174                    let conj = if goal_parts.len() == 1 {
175                        goal_parts[0].clone()
176                    } else {
177                        format!("(and {})", goal_parts.join(" "))
178                    };
179                    self.emit(&format!("(assert (not {conj}))"));
180                }
181            }
182            VcKind::Theorem => {
183                // Theorems with struct-typed quantifiers are not yet supported.
184                // For now, we skip encoding if struct types appear in quantifiers.
185                if let Some(body) = vc.goals.first() {
186                    let smt = self.expr_to_smt(body);
187                    self.emit(&format!("(assert (not {smt}))"));
188                }
189            }
190        }
191
192        self.emit("(check-sat)");
193        // get-model only makes sense when sat; Z3 will error on unsat.
194        // We handle this by checking the first line of output.
195    }
196
197    pub fn get_output(&self) -> String {
198        self.lines.join("\n")
199    }
200
201    fn declare_const(&mut self, name: &str, sort: &str) {
202        if self.declared.insert(name.to_string()) {
203            self.emit(&format!("(declare-const {name} {sort})"));
204        }
205    }
206
207    fn emit(&mut self, line: &str) {
208        self.lines.push(line.to_string());
209    }
210
211    // ── Expression encoding ──────────────────────────────
212
213    fn expr_to_smt(&mut self, expr: &Spanned<Expr>) -> String {
214        match &expr.node {
215            Expr::IntLit(v) => {
216                if *v < 0 {
217                    format!("(- {})", -v)
218                } else {
219                    v.to_string()
220                }
221            }
222            Expr::BoolLit(b) => b.to_string(),
223            Expr::StringLit(s) => format!("\"{s}\""),
224
225            Expr::Ident(name) => {
226                // Check if it's an enum variant
227                for decl_name in &self.type_info.enums {
228                    if let Some(fields) = self.type_info.structs.get(decl_name) {
229                        // not an enum
230                        let _ = fields;
231                    }
232                }
233                self.sanitize(name)
234            }
235
236            Expr::Prime(inner) => {
237                let base = self.expr_to_smt(inner);
238                format!("{base}_prime")
239            }
240
241            Expr::FieldAccess(base, field) => {
242                let b = self.expr_to_smt(base);
243                let name = format!("{b}_{field}");
244                // Ensure this field constant is declared
245                if !self.declared.contains(&name) {
246                    // Try to find the sort from type info
247                    let sort = self
248                        .resolve_field_sort(&b, field)
249                        .unwrap_or_else(|| "Int".to_string());
250                    self.declare_const(&name, &sort);
251                    // Also declare primed
252                    self.declare_const(&format!("{name}_prime"), &sort);
253                }
254                name
255            }
256
257            Expr::Index(base, idx) => {
258                let b = self.expr_to_smt(base);
259                let i = self.expr_to_smt(idx);
260                format!("(select {b} {i})")
261            }
262
263            Expr::BinOp(lhs, op, rhs) => {
264                let l = self.expr_to_smt(lhs);
265                let r = self.expr_to_smt(rhs);
266                match op {
267                    BinOp::Add => format!("(+ {l} {r})"),
268                    BinOp::Sub => format!("(- {l} {r})"),
269                    BinOp::Mul => format!("(* {l} {r})"),
270                    BinOp::Div => format!("(div {l} {r})"),
271                    BinOp::Mod => format!("(mod {l} {r})"),
272                    BinOp::Eq => format!("(= {l} {r})"),
273                    BinOp::Neq => format!("(not (= {l} {r}))"),
274                    BinOp::Lt => format!("(< {l} {r})"),
275                    BinOp::Le => format!("(<= {l} {r})"),
276                    BinOp::Gt => format!("(> {l} {r})"),
277                    BinOp::Ge => format!("(>= {l} {r})"),
278                    BinOp::And => format!("(and {l} {r})"),
279                    BinOp::Or => format!("(or {l} {r})"),
280                    BinOp::Implies => format!("(=> {l} {r})"),
281                }
282            }
283
284            Expr::UnaryOp(op, operand) => {
285                let o = self.expr_to_smt(operand);
286                match op {
287                    UnaryOp::Not => format!("(not {o})"),
288                    UnaryOp::Neg => format!("(- {o})"),
289                }
290            }
291
292            Expr::IfThenElse(c, t, e) => {
293                let sc = self.expr_to_smt(c);
294                let st = self.expr_to_smt(t);
295                let se = self.expr_to_smt(e);
296                format!("(ite {sc} {st} {se})")
297            }
298
299            Expr::Forall(vars, body) => {
300                let bindings: Vec<String> = vars
301                    .iter()
302                    .map(|v| {
303                        let sort = type_expr_to_smt(&v.ty);
304                        format!("({} {})", self.sanitize(&v.name), sort)
305                    })
306                    .collect();
307                let b = self.expr_to_smt(body);
308                format!("(forall ({}) {})", bindings.join(" "), b)
309            }
310
311            Expr::Exists(vars, body) => {
312                let bindings: Vec<String> = vars
313                    .iter()
314                    .map(|v| {
315                        let sort = type_expr_to_smt(&v.ty);
316                        format!("({} {})", self.sanitize(&v.name), sort)
317                    })
318                    .collect();
319                let b = self.expr_to_smt(body);
320                format!("(exists ({}) {})", bindings.join(" "), b)
321            }
322
323            Expr::Call(name, args) => {
324                if args.is_empty() {
325                    self.sanitize(name)
326                } else {
327                    let smt_args: Vec<String> = args.iter().map(|a| self.expr_to_smt(a)).collect();
328                    format!("({} {})", self.sanitize(name), smt_args.join(" "))
329                }
330            }
331
332            Expr::Paren(inner) => self.expr_to_smt(inner),
333        }
334    }
335
336    fn resolve_field_sort(&self, base_var: &str, field: &str) -> Option<String> {
337        // Find what struct type this variable is
338        if let Some(type_name) = self.param_types.get(base_var) {
339            return self
340                .type_info
341                .field_sort(type_name, field)
342                .map(|s| s.to_string());
343        }
344        // Try to find nested: e.g., base_var = "home_frontDoor", field = "locked"
345        // Walk through all structs to find a match
346        for (_, fields) in &self.type_info.structs {
347            for (fname, fi) in fields {
348                let compound = format!("{base_var}_{fname}");
349                if compound == format!("{base_var}_{field}") {
350                    return Some(fi.sort.clone());
351                }
352            }
353        }
354        None
355    }
356
357    fn sanitize(&self, name: &str) -> String {
358        match name {
359            "and" | "or" | "not" | "true" | "false" | "ite" | "let" | "forall" | "exists" => {
360                format!("intent_{name}")
361            }
362            _ => name.to_string(),
363        }
364    }
365}
366
367// ── Z3 invocation (in-process, statically linked via `z3` crate) ──
368
369/// Strip solver commands from SMT-LIB2; `Solver::from_string` only accepts declarations/assertions.
370fn smt_for_solver(smt_input: &str) -> String {
371    smt_input
372        .lines()
373        .filter(|line| {
374            let t = line.trim();
375            !t.starts_with("(check-sat)") && !t.starts_with("(get-model)")
376        })
377        .collect::<Vec<_>>()
378        .join("\n")
379}
380
381pub fn run_z3(smt_input: &str) -> VerifyResult {
382    let mut cfg = Config::new();
383    cfg.set_timeout_msec(5_000);
384    let smt = smt_for_solver(smt_input);
385
386    with_z3_config(&cfg, || {
387        let solver = Solver::new();
388        solver.from_string(smt.as_bytes());
389
390        match solver.check() {
391            SatResult::Unsat => VerifyResult::Verified,
392            SatResult::Sat => {
393                let counterexample = solver
394                    .get_model()
395                    .map(|model| parse_z3_model(&model.to_string()))
396                    .unwrap_or_default();
397                VerifyResult::Failed { counterexample }
398            }
399            SatResult::Unknown => VerifyResult::Unknown {
400                reason: "Z3 returned unknown (timeout or undecidable fragment)".to_string(),
401            },
402        }
403    })
404}
405
406/// Parse Z3 model output into human-readable assignments.
407/// Input looks like: (model (define-fun sender_balance () Int 100) ...)
408fn parse_z3_model(raw: &str) -> String {
409    let mut assignments = Vec::new();
410    // Match patterns like (define-fun <name> () <sort> <value>)
411    let mut remaining = raw;
412    while let Some(pos) = remaining.find("define-fun ") {
413        remaining = &remaining[pos + 11..];
414        // Parse name
415        let name_end = remaining
416            .find(|c: char| c.is_whitespace())
417            .unwrap_or(remaining.len());
418        let name = &remaining[..name_end];
419        remaining = &remaining[name_end..];
420
421        // Skip past "() <sort> " to get value
422        if let Some(paren_pos) = remaining.find("()") {
423            remaining = &remaining[paren_pos + 2..].trim_start();
424            // Skip sort
425            let sort_end = remaining
426                .find(|c: char| c.is_whitespace())
427                .unwrap_or(remaining.len());
428            remaining = &remaining[sort_end..].trim_start();
429            // Read value until closing paren
430            let value = extract_smt_value(remaining);
431
432            // Convert flattened name back: sender_balance → sender.balance
433            let display_name = name.replace('_', ".");
434            // Skip primed versions — only show original values
435            if !name.ends_with("_prime") {
436                assignments.push(format!("{display_name} = {value}"));
437            }
438        }
439    }
440
441    if assignments.is_empty() {
442        raw.to_string()
443    } else {
444        assignments.join(", ")
445    }
446}
447
448/// Extract a value from SMT-LIB2 output (handles negative numbers like (- 1))
449fn extract_smt_value(s: &str) -> String {
450    let s = s.trim();
451    if s.starts_with('(') {
452        // Could be (- N) for negative number or complex expression
453        if let Some(close) = find_matching_paren(s) {
454            let inner = &s[1..close].trim();
455            if inner.starts_with("- ") {
456                return format!("-{}", inner[2..].trim());
457            }
458            return inner.to_string();
459        }
460        // Fallback
461        s.chars().take(30).collect()
462    } else {
463        // Simple value: number, true, false, string literal
464        s.split(|c: char| c == ')' || c == '\n')
465            .next()
466            .unwrap_or(s)
467            .trim()
468            .to_string()
469    }
470}
471
472fn find_matching_paren(s: &str) -> Option<usize> {
473    let mut depth = 0;
474    for (i, c) in s.char_indices() {
475        match c {
476            '(' => depth += 1,
477            ')' => {
478                depth -= 1;
479                if depth == 0 {
480                    return Some(i);
481                }
482            }
483            _ => {}
484        }
485    }
486    None
487}
488
489/// Verify a single VC: encode → call Z3 → return result.
490pub fn verify_vc(vc: &VerificationCondition, prog: &Program) -> VerifyResult {
491    // Intents with no goals are trivially verified (nothing to prove).
492    if vc.kind == VcKind::Intent && vc.goals.is_empty() {
493        return VerifyResult::Verified;
494    }
495
496    let mut encoder = SmtEncoder::new(prog);
497    encoder.encode_vc(vc, prog);
498    let smt = encoder.get_output();
499    run_z3(&smt)
500}