//! SMT Solver Integration via Z3 with string theory support. //! //! Provides a hybrid constraint solving architecture: [`PathEnv`] handles the //! fast path (~95% of branches), and Z3 is invoked as a secondary solver for //! cases that involve relationships PathEnv cannot decide. //! //! ## Architecture //! //! - **Capability-based escalation**: SMT is invoked when accumulated path //! constraints contain comparisons that the translator can lower to Z3 //! (integer or string operands with at least one `Value`). //! - **Integer + string sorts**: Both `Z3Int` and `Z3Str` variables //! are supported. Sort is inferred from PathEnv evidence, constant operands, //! or comparison-context hints. //! - **Strict sort safety**: Z3 variables are only created when the sort is //! known with confidence. Unknown-sort variables are skipped entirely. //! Sort conflicts (same SSA value used as both int and string) cause the //! constraint to be skipped conservatively. //! - **Sound infeasibility**: Z3 `Unsat` → path is infeasible. Anything else //! (Sat, Unknown, timeout, translation failure) → continue as before. //! This can never suppress a real finding. //! //! ## Scope boundary //! //! This module supports direct `Operand`-level string guard reasoning only. //! It does NOT translate `SymbolicValue` expression trees (Concat, Substr, //! Replace, etc.) into Z3 terms. If a `SymbolicValue` was already folded to //! `ConcreteStr` by the symbolic engine, it flows through as a //! `ConstValue::Str` operand and is handled. #![allow( clippy::collapsible_if, clippy::needless_borrows_for_generic_args, clippy::new_without_default, dead_code )] use std::collections::HashMap; use z3::ast::Int as Z3Int; use z3::ast::String as Z3Str; use z3::{Config, Params, SatResult, Solver}; use crate::constraint::{CompOp, ConditionExpr, ConstValue, Operand, PathEnv, RelOp}; use crate::ssa::ir::SsaValue; use crate::ssa::type_facts::TypeKind; use super::state::{PathConstraint, SymbolicState}; // ───────────────────────────────────────────────────────────────────────────── // Constants // ───────────────────────────────────────────────────────────────────────────── /// Maximum SMT queries per finding (across all paths). const MAX_SMT_QUERIES_PER_FINDING: u32 = 10; /// Per-query timeout in milliseconds (integer-only queries). const SMT_QUERY_TIMEOUT_MS: u32 = 500; /// Per-query timeout for queries involving string theory (ms). /// String theory (especially lexicographic ordering) is more expensive. const SMT_STRING_QUERY_TIMEOUT_MS: u32 = 500; // ───────────────────────────────────────────────────────────────────────────── // Types // ───────────────────────────────────────────────────────────────────────────── /// Result of an SMT satisfiability check. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SmtResult { /// Path constraints are satisfiable (path is feasible). Sat, /// Path constraints are unsatisfiable (path is provably infeasible). Unsat, /// Solver returned unknown (timeout, resource limit, etc.). /// Treated conservatively as Sat. Unknown, /// Per-finding query budget exhausted. BudgetExhausted, } /// Z3 context and budget tracking for one finding's exploration. /// /// Created once per `explore_finding()` call, shared across all paths explored /// for that finding. A fresh `Solver` is created per `check_path_feasibility()` /// call (reset-and-replay strategy). /// /// The z3 0.19 crate uses a thread-local context model via `with_z3_config`. /// We store the `Config` and create a scoped context per query. pub struct SmtContext { cfg: Config, queries_used: u32, timeout_ms: u32, } /// Tracks the Z3 sort assigned to each SSA variable. #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum VarSort { Int, Str, } /// Polymorphic Z3 variable, either integer or string sort. enum Z3Var { Int(Z3Int), Str(Z3Str), } /// Polymorphic Z3 expression returned by operand translation. enum Z3Expr { Int(Z3Int), Str(Z3Str), } /// Variable map: SSA value → Z3 variable with implicit sort. type VarMap = HashMap; /// Pay bundled Z3 static-init cost once per process so the first real /// `check_path_feasibility()` call doesn't blow the per-query timeout. fn warm_z3() { static WARM: std::sync::OnceLock<()> = std::sync::OnceLock::new(); WARM.get_or_init(|| { let cfg = Config::new(); z3::with_z3_config(&cfg, || { let _ = Solver::new().check(); }); }); } // ───────────────────────────────────────────────────────────────────────────── // SmtContext // ───────────────────────────────────────────────────────────────────────────── impl SmtContext { /// Create a new SMT context for one finding's exploration. pub fn new() -> Self { SmtContext { cfg: Config::new(), queries_used: 0, #[cfg(not(test))] timeout_ms: SMT_QUERY_TIMEOUT_MS, #[cfg(test)] timeout_ms: 5_000, } } /// Check whether the query budget has remaining capacity. pub fn has_budget(&self) -> bool { self.queries_used < MAX_SMT_QUERIES_PER_FINDING } /// Check path feasibility using Z3. /// /// Translates accumulated path constraints and PathEnv facts into Z3 /// assertions, then checks satisfiability. Returns `Unsat` only when Z3 /// proves the constraints are contradictory. /// /// Constraints that cannot be fully translated (unknown sorts, sort /// conflicts, etc.) are silently skipped, this is sound because omitting /// a constraint can only make Z3 return `Sat` when the actual result /// might be `Unsat`, never the reverse. pub fn check_path_feasibility( &mut self, constraints: &[PathConstraint], _sym_state: &SymbolicState, env: &PathEnv, ) -> SmtResult { if !self.has_budget() { return SmtResult::BudgetExhausted; } self.queries_used += 1; warm_z3(); // Use with_z3_config to create a scoped Z3 context for this query. let base_timeout_ms = self.timeout_ms; z3::with_z3_config(&self.cfg, || { let solver = Solver::new(); // Build variable map from constraints + PathEnv. let mut var_map: VarMap = HashMap::new(); // Seed from PathEnv facts (interval bounds, exact values, string facts). seed_from_path_env(&solver, &mut var_map, env); // Translate path constraints. for pc in constraints { assert_path_constraint(&solver, &mut var_map, pc, env); } // Determine timeout: use string timeout if any string vars present. let has_string_vars = var_map.values().any(|v| matches!(v, Z3Var::Str(_))); let effective_timeout = if has_string_vars { base_timeout_ms.max(SMT_STRING_QUERY_TIMEOUT_MS) } else { base_timeout_ms }; let mut params = Params::new(); params.set_u32("timeout", effective_timeout); solver.set_params(¶ms); // Check satisfiability. match solver.check() { SatResult::Unsat => SmtResult::Unsat, SatResult::Sat => SmtResult::Sat, SatResult::Unknown => SmtResult::Unknown, } }) } } // ───────────────────────────────────────────────────────────────────────────── // Sort inference // ───────────────────────────────────────────────────────────────────────────── /// Try to determine that an SSA value is an integer from PathEnv facts. fn is_known_int(v: SsaValue, env: &PathEnv) -> bool { let fact = env.get(v); // Has interval bounds → definitely numeric. if fact.lo.is_some() || fact.hi.is_some() { return true; } // Has an exact integer value. if matches!(fact.exact, Some(ConstValue::Int(_))) { return true; } false } /// Try to determine that an SSA value is a string from PathEnv facts. fn is_known_str(v: SsaValue, env: &PathEnv) -> bool { let fact = env.get(v); // Has an exact string value. if matches!(fact.exact, Some(ConstValue::Str(_))) { return true; } // Has singleton String type evidence. if fact.types.is_singleton_of(&TypeKind::String) { return true; } false } /// Get or create a Z3 integer variable for an SSA value, but only if the /// sort is known to be Int. Returns `None` if the sort is unknown or /// conflicts with an existing string assignment. fn ensure_int_var(var_map: &mut VarMap, v: SsaValue, env: &PathEnv) -> Option { match var_map.get(&v) { Some(Z3Var::Int(z)) => return Some(z.clone()), Some(Z3Var::Str(_)) => return None, // sort conflict None => {} } // Only create if we have evidence this is an integer. if is_known_int(v, env) { let z3_var = Z3Int::new_const(format!("v{}", v.0)); var_map.insert(v, Z3Var::Int(z3_var.clone())); return Some(z3_var); } None } /// Create a Z3 integer variable unconditionally (used when context proves /// the sort, e.g., both sides of an integer comparison). /// Returns `None` on sort conflict (variable already assigned as string). fn force_int_var(var_map: &mut VarMap, v: SsaValue) -> Option { match var_map.get(&v) { Some(Z3Var::Int(z)) => return Some(z.clone()), Some(Z3Var::Str(_)) => return None, // sort conflict None => {} } let z3_var = Z3Int::new_const(format!("v{}", v.0)); var_map.insert(v, Z3Var::Int(z3_var.clone())); Some(z3_var) } /// Get or create a Z3 string variable for an SSA value, but only if the /// sort is known to be Str. Returns `None` if the sort is unknown or /// conflicts with an existing integer assignment. fn ensure_str_var(var_map: &mut VarMap, v: SsaValue, env: &PathEnv) -> Option { match var_map.get(&v) { Some(Z3Var::Str(z)) => return Some(z.clone()), Some(Z3Var::Int(_)) => return None, // sort conflict None => {} } // Only create if we have evidence this is a string. if is_known_str(v, env) { let z3_var = Z3Str::new_const(format!("v{}", v.0)); var_map.insert(v, Z3Var::Str(z3_var.clone())); return Some(z3_var); } None } /// Create a Z3 string variable unconditionally (used when comparison context /// proves the sort). Returns `None` on sort conflict. fn force_str_var(var_map: &mut VarMap, v: SsaValue) -> Option { match var_map.get(&v) { Some(Z3Var::Str(z)) => return Some(z.clone()), Some(Z3Var::Int(_)) => return None, // sort conflict None => {} } let z3_var = Z3Str::new_const(format!("v{}", v.0)); var_map.insert(v, Z3Var::Str(z3_var.clone())); Some(z3_var) } // ───────────────────────────────────────────────────────────────────────────── // PathEnv seeding // ───────────────────────────────────────────────────────────────────────────── /// Seed Z3 solver with known facts from PathEnv. /// /// Seeds both integer-typed and string-typed facts. Unknown-sort values are /// skipped. fn seed_from_path_env(solver: &Solver, var_map: &mut VarMap, env: &PathEnv) { // Interval bounds, exact values, and excluded values. for &(v, ref fact) in env.facts() { // Integer evidence path. let has_int_evidence = fact.lo.is_some() || fact.hi.is_some() || matches!(fact.exact, Some(ConstValue::Int(_))); if has_int_evidence { if let Some(z3_var) = force_int_var(var_map, v) { if let Some(lo) = fact.lo { if fact.lo_strict { solver.assert(&z3_var.gt(&Z3Int::from_i64(lo))); } else { solver.assert(&z3_var.ge(&Z3Int::from_i64(lo))); } } if let Some(hi) = fact.hi { if fact.hi_strict { solver.assert(&z3_var.lt(&Z3Int::from_i64(hi))); } else { solver.assert(&z3_var.le(&Z3Int::from_i64(hi))); } } if let Some(ConstValue::Int(n)) = &fact.exact { solver.assert(&z3_var.eq(&Z3Int::from_i64(*n))); } // Excluded integer values. for excl in &fact.excluded { if let ConstValue::Int(n) = excl { solver.assert(&z3_var.ne(&Z3Int::from_i64(*n))); } } } continue; } // String evidence path. let has_str_evidence = matches!(fact.exact, Some(ConstValue::Str(_))) || fact.types.is_singleton_of(&TypeKind::String); if has_str_evidence { if let Some(z3_var) = force_str_var(var_map, v) { // Exact string value. if let Some(ConstValue::Str(s)) = &fact.exact { solver.assert(&z3_var.eq(Z3Str::from(s.as_str()))); } // Excluded string values. for excl in &fact.excluded { if let ConstValue::Str(s) = excl { solver.assert(&z3_var.ne(Z3Str::from(s.as_str()))); } } } } } // Equality classes from UnionFind. let known_vars: Vec = var_map.keys().copied().collect(); for &v in &known_vars { let root = env.uf.find_immutable(v); if root != v { match (var_map.get(&root), var_map.get(&v)) { (Some(Z3Var::Int(r)), Some(Z3Var::Int(vi))) => { solver.assert(&vi.eq(r)); } (Some(Z3Var::Str(r)), Some(Z3Var::Str(vi))) => { solver.assert(&vi.eq(r)); } _ => {} // Sort mismatch or missing, skip. } } } // Disequalities. for &(a, b) in env.disequalities() { match (var_map.get(&a), var_map.get(&b)) { (Some(Z3Var::Int(za)), Some(Z3Var::Int(zb))) => { solver.assert(&za.ne(zb)); } (Some(Z3Var::Str(za)), Some(Z3Var::Str(zb))) => { solver.assert(&za.ne(zb)); } _ => {} // Sort mismatch or missing, skip. } } // Relational constraints (integer-domain only: Lt/Le). for &(a, op, b) in env.relational() { if let (Some(Z3Var::Int(za)), Some(Z3Var::Int(zb))) = (var_map.get(&a), var_map.get(&b)) { match op { RelOp::Lt => solver.assert(&za.lt(zb)), RelOp::Le => solver.assert(&za.le(zb)), } } } } // ───────────────────────────────────────────────────────────────────────────── // Constraint translation // ───────────────────────────────────────────────────────────────────────────── /// Translate a single path constraint into a Z3 assertion. /// /// Skips constraints that cannot be fully translated (unknown sort, sort /// conflict, etc.). This is sound, see module-level docs. fn assert_path_constraint( solver: &Solver, var_map: &mut VarMap, pc: &PathConstraint, env: &PathEnv, ) { match &pc.condition { ConditionExpr::Comparison { lhs, op, rhs } => { // Determine sort hints from the opposite operand. let lhs_hint = operand_sort_hint(rhs); let rhs_hint = operand_sort_hint(lhs); let z_lhs = translate_operand_with_hint(var_map, lhs, env, lhs_hint); let z_rhs = translate_operand_with_hint(var_map, rhs, env, rhs_hint); if let (Some(z_l), Some(z_r)) = (z_lhs, z_rhs) { if let Some(cmp) = build_comparison_poly(&z_l, *op, &z_r) { if pc.polarity { solver.assert(&cmp); } else { solver.assert(&cmp.not()); } } } // If either operand can't be translated, skip (conservative). } ConditionExpr::BoolTest { var } => { // Model as var != 0 if var is known int. if let Some(z_var) = ensure_int_var(var_map, *var, env) { let test = z_var.ne(&Z3Int::from_i64(0)); if pc.polarity { solver.assert(&test); } else { solver.assert(&test.not()); } } } // NullCheck, TypeCheck, Unknown, skip (not modeled). ConditionExpr::NullCheck { .. } | ConditionExpr::TypeCheck { .. } | ConditionExpr::Unknown => {} } } /// Infer a sort hint from a constant operand. /// /// When one side of a comparison is a known constant, it hints the sort of /// the other side (a `Value`). This is the lowest-priority evidence, used /// only when var_map and PathEnv provide no information. fn operand_sort_hint(op: &Operand) -> Option { match op { Operand::Const(ConstValue::Str(_)) => Some(VarSort::Str), Operand::Const(ConstValue::Int(_) | ConstValue::Bool(_)) => Some(VarSort::Int), _ => None, } } /// Translate a constraint operand to a polymorphic Z3 expression. /// /// Sort resolution precedence for `Value` operands: /// 1. Existing var_map entry (already assigned sort) /// 2. PathEnv evidence (`is_known_str` / `is_known_int`) /// 3. Unknown → return `None` (caller may apply context hint) fn translate_operand(var_map: &mut VarMap, op: &Operand, env: &PathEnv) -> Option { match op { Operand::Const(ConstValue::Int(n)) => Some(Z3Expr::Int(Z3Int::from_i64(*n))), Operand::Const(ConstValue::Bool(b)) => { Some(Z3Expr::Int(Z3Int::from_i64(if *b { 1 } else { 0 }))) } Operand::Const(ConstValue::Str(s)) => Some(Z3Expr::Str(Z3Str::from(s.as_str()))), Operand::Value(v) => { // 1. Existing var_map entry wins. match var_map.get(v) { Some(Z3Var::Int(z)) => return Some(Z3Expr::Int(z.clone())), Some(Z3Var::Str(z)) => return Some(Z3Expr::Str(z.clone())), None => {} } // 2. PathEnv evidence. if is_known_str(*v, env) { return force_str_var(var_map, *v).map(Z3Expr::Str); } if is_known_int(*v, env) { return force_int_var(var_map, *v).map(Z3Expr::Int); } // 3. Unknown sort, return None; caller may apply hint. None } Operand::Const(ConstValue::Null) | Operand::Unknown => None, } } /// Translate an operand with a sort hint from the comparison context. /// /// Falls back to `translate_operand` first. If that returns `None` for a /// `Value` operand, applies the sort hint. When no hint is available /// (e.g., both sides are Values), defaults to Int for backward /// compatibility with pre-string-theory behavior. fn translate_operand_with_hint( var_map: &mut VarMap, op: &Operand, env: &PathEnv, hint: Option, ) -> Option { // Try the standard path first (respects precedence 1 and 2). if let Some(expr) = translate_operand(var_map, op, env) { return Some(expr); } // Precedence 3: comparison-context hint for unresolved Value operands. if let Operand::Value(v) = op { // Only apply hint if the variable has no var_map entry yet. if !var_map.contains_key(v) { match hint { Some(VarSort::Str) => return force_str_var(var_map, *v).map(Z3Expr::Str), Some(VarSort::Int) | None => { // Default to Int when no hint is available (e.g., Value vs // Value comparisons). This preserves backward compatibility: // pre-string-theory, all Value operands were forced to Int. return force_int_var(var_map, *v).map(Z3Expr::Int); } } } } None } /// Build a Z3 boolean expression for a comparison, dispatching on sort. /// /// Returns `None` on sort mismatch (int vs string). fn build_comparison_poly(lhs: &Z3Expr, op: CompOp, rhs: &Z3Expr) -> Option { match (lhs, rhs) { (Z3Expr::Int(l), Z3Expr::Int(r)) => Some(build_comparison_int(l, op, r)), (Z3Expr::Str(l), Z3Expr::Str(r)) => Some(build_comparison_str(l, op, r)), _ => None, // sort mismatch } } /// Build a Z3 boolean expression for an integer comparison. fn build_comparison_int(lhs: &Z3Int, op: CompOp, rhs: &Z3Int) -> z3::ast::Bool { match op { CompOp::Eq => lhs.eq(rhs), CompOp::Neq => lhs.ne(rhs), CompOp::Lt => lhs.lt(rhs), CompOp::Gt => lhs.gt(rhs), CompOp::Le => lhs.le(rhs), CompOp::Ge => lhs.ge(rhs), } } /// Build a Z3 boolean expression for a string comparison. /// /// All six comparison operators are supported: /// - Eq/Neq: structural string equality /// - Lt/Le/Gt/Ge: SMT-LIB2 lexicographic ordering (str.<, str.<=, str.>, str.>=) fn build_comparison_str(lhs: &Z3Str, op: CompOp, rhs: &Z3Str) -> z3::ast::Bool { match op { CompOp::Eq => lhs.eq(rhs), CompOp::Neq => lhs.ne(rhs), CompOp::Lt => lhs.str_lt(rhs), CompOp::Le => lhs.str_le(rhs), CompOp::Gt => lhs.str_gt(rhs), CompOp::Ge => lhs.str_ge(rhs), } } // ───────────────────────────────────────────────────────────────────────────── // Escalation predicate // ───────────────────────────────────────────────────────────────────────────── /// Determine whether accumulated path constraints warrant SMT escalation. /// /// Returns `true` when the constraints contain comparisons that the SMT /// translator can lower: at least one `Value` operand, and both operands /// are translatable (Int, Bool, Str constants or Value references). /// /// BoolTest, NullCheck, TypeCheck, and Unknown conditions do not escalate. pub fn should_escalate(constraints: &[PathConstraint]) -> bool { constraints.iter().any(|c| match &c.condition { ConditionExpr::Comparison { lhs, rhs, .. } => { let has_value = matches!(lhs, Operand::Value(_)) || matches!(rhs, Operand::Value(_)); has_value && can_translate_operand(lhs) && can_translate_operand(rhs) } _ => false, }) } /// Check whether an operand can be translated to Z3 (int, bool, or string). fn can_translate_operand(op: &Operand) -> bool { match op { Operand::Value(_) => true, Operand::Const(ConstValue::Int(_) | ConstValue::Bool(_) | ConstValue::Str(_)) => true, Operand::Const(ConstValue::Null) | Operand::Unknown => false, } } // ───────────────────────────────────────────────────────────────────────────── // Tests // ───────────────────────────────────────────────────────────────────────────── #[cfg(test)] mod tests { use super::*; use crate::constraint::{CompOp, ConditionExpr, Operand, PathEnv}; use crate::ssa::ir::{BlockId, SsaValue}; /// Helper: build a Comparison PathConstraint. fn comparison_constraint( lhs: Operand, op: CompOp, rhs: Operand, polarity: bool, ) -> PathConstraint { PathConstraint { block: BlockId(0), condition: ConditionExpr::Comparison { lhs, op, rhs }, polarity, } } fn val(n: u32) -> Operand { Operand::Value(SsaValue(n)) } fn int_const(n: i64) -> Operand { Operand::Const(ConstValue::Int(n)) } fn str_const(s: &str) -> Operand { Operand::Const(ConstValue::Str(s.into())) } // ── Escalation predicate ───────────────────────────────────────────── #[test] fn escalation_fires_on_value_vs_value() { let constraints = vec![comparison_constraint(val(0), CompOp::Gt, val(1), true)]; assert!(should_escalate(&constraints)); } #[test] fn escalation_fires_on_value_vs_int_const() { // Capability-based escalation also fires on Value vs Const. let constraints = vec![comparison_constraint( val(0), CompOp::Gt, int_const(5), true, )]; assert!(should_escalate(&constraints)); } #[test] fn escalation_fires_on_value_vs_string_const() { let constraints = vec![comparison_constraint( val(0), CompOp::Eq, str_const("hello"), true, )]; assert!(should_escalate(&constraints)); } #[test] fn escalation_skips_const_vs_const() { // No Value operand → no variable to reason about. let constraints = vec![comparison_constraint( int_const(3), CompOp::Eq, int_const(5), true, )]; assert!(!should_escalate(&constraints)); } #[test] fn escalation_skips_empty() { assert!(!should_escalate(&[])); } #[test] fn escalation_skips_non_comparison() { let constraints = vec![PathConstraint { block: BlockId(0), condition: ConditionExpr::BoolTest { var: SsaValue(0) }, polarity: true, }]; assert!(!should_escalate(&constraints)); } #[test] fn escalation_skips_null_operand() { let constraints = vec![comparison_constraint( val(0), CompOp::Eq, Operand::Const(ConstValue::Null), true, )]; assert!(!should_escalate(&constraints)); } // ── Simple integer contradiction ───────────────────────────────────── #[test] fn simple_contradiction() { // x > 10 AND x < 5 → Unsat let constraints = vec![ comparison_constraint(val(0), CompOp::Gt, int_const(10), true), comparison_constraint(val(0), CompOp::Lt, int_const(5), true), ]; let mut ctx = SmtContext::new(); let env = PathEnv::empty(); let sym = SymbolicState::new(); let result = ctx.check_path_feasibility(&constraints, &sym, &env); assert_eq!(result, SmtResult::Unsat); } // ── Cross-variable contradiction (key SMT value prop) ──────────────── #[test] fn cross_variable_contradiction() { // x > y AND y > x → Unsat // PathEnv cannot detect this, it tracks per-variable intervals. let constraints = vec![ comparison_constraint(val(0), CompOp::Gt, val(1), true), comparison_constraint(val(1), CompOp::Gt, val(0), true), ]; let mut ctx = SmtContext::new(); let env = PathEnv::empty(); let sym = SymbolicState::new(); let result = ctx.check_path_feasibility(&constraints, &sym, &env); assert_eq!(result, SmtResult::Unsat); } // ── Arithmetic cross-variable ──────────────────────────────────────── #[test] fn arithmetic_cross_variable() { // x < 3 AND y < 5 AND y > 3 AND x > y → Unsat // because y ∈ (3,5), x > y means x > 3, but x < 3. let constraints = vec![ comparison_constraint(val(0), CompOp::Lt, int_const(3), true), comparison_constraint(val(1), CompOp::Lt, int_const(5), true), comparison_constraint(val(1), CompOp::Gt, int_const(3), true), comparison_constraint(val(0), CompOp::Gt, val(1), true), ]; let mut ctx = SmtContext::new(); let env = PathEnv::empty(); let sym = SymbolicState::new(); let result = ctx.check_path_feasibility(&constraints, &sym, &env); assert_eq!(result, SmtResult::Unsat); } // ── Satisfiable path ───────────────────────────────────────────────── #[test] fn satisfiable_path() { // x > 0 AND x < 100 → Sat let constraints = vec![ comparison_constraint(val(0), CompOp::Gt, int_const(0), true), comparison_constraint(val(0), CompOp::Lt, int_const(100), true), ]; let mut ctx = SmtContext::new(); let env = PathEnv::empty(); let sym = SymbolicState::new(); let result = ctx.check_path_feasibility(&constraints, &sym, &env); assert_eq!(result, SmtResult::Sat); } // ── Budget exhaustion ──────────────────────────────────────────────── #[test] fn budget_exhaustion() { let constraints = vec![comparison_constraint( val(0), CompOp::Gt, int_const(0), true, )]; let mut ctx = SmtContext::new(); let env = PathEnv::empty(); let sym = SymbolicState::new(); // Exhaust budget. for _ in 0..MAX_SMT_QUERIES_PER_FINDING { let r = ctx.check_path_feasibility(&constraints, &sym, &env); assert_ne!(r, SmtResult::BudgetExhausted); } // Next call should return BudgetExhausted. let result = ctx.check_path_feasibility(&constraints, &sym, &env); assert_eq!(result, SmtResult::BudgetExhausted); } // ── PathEnv seeding (integer) ──────────────────────────────────────── #[test] fn path_env_seeding_interval() { // Seed PathEnv with x in [10, 20], then assert x < 5 → Unsat. let mut env = PathEnv::empty(); use crate::constraint::ValueFact; let mut fact = ValueFact::top(); fact.lo = Some(10); fact.hi = Some(20); env.refine(SsaValue(0), &fact); let constraints = vec![comparison_constraint( val(0), CompOp::Lt, int_const(5), true, )]; let mut ctx = SmtContext::new(); let sym = SymbolicState::new(); let result = ctx.check_path_feasibility(&constraints, &sym, &env); assert_eq!(result, SmtResult::Unsat); } // ── Sort safety: unknown sort variables are skipped ────────────────── #[test] fn skip_unknown_sort() { // BoolTest on a variable with no int evidence → skip. // The constraint is effectively ignored, so result should be Sat. let constraints = vec![PathConstraint { block: BlockId(0), condition: ConditionExpr::BoolTest { var: SsaValue(99) }, polarity: true, }]; let mut ctx = SmtContext::new(); let env = PathEnv::empty(); let sym = SymbolicState::new(); let result = ctx.check_path_feasibility(&constraints, &sym, &env); assert_eq!(result, SmtResult::Sat); } // ── String equality ─────────────────────────────────────── #[test] fn string_equality_asserted() { // x == "hello" → Sat (string constraint is now asserted, not skipped). let constraints = vec![comparison_constraint( val(0), CompOp::Eq, str_const("hello"), true, )]; let mut ctx = SmtContext::new(); let env = PathEnv::empty(); let sym = SymbolicState::new(); let result = ctx.check_path_feasibility(&constraints, &sym, &env); assert_eq!(result, SmtResult::Sat); } #[test] fn string_equality_contradiction() { // x == "hello" AND x == "world" → Unsat let constraints = vec![ comparison_constraint(val(0), CompOp::Eq, str_const("hello"), true), comparison_constraint(val(0), CompOp::Eq, str_const("world"), true), ]; let mut ctx = SmtContext::new(); let env = PathEnv::empty(); let sym = SymbolicState::new(); let result = ctx.check_path_feasibility(&constraints, &sym, &env); assert_eq!(result, SmtResult::Unsat); } #[test] fn string_inequality_satisfiable() { // x != "hello" → Sat (x can be anything else) let constraints = vec![comparison_constraint( val(0), CompOp::Neq, str_const("hello"), true, )]; let mut ctx = SmtContext::new(); let env = PathEnv::empty(); let sym = SymbolicState::new(); let result = ctx.check_path_feasibility(&constraints, &sym, &env); assert_eq!(result, SmtResult::Sat); } #[test] fn string_eq_and_neq_contradiction() { // x == "hello" AND x != "hello" → Unsat let constraints = vec![ comparison_constraint(val(0), CompOp::Eq, str_const("hello"), true), comparison_constraint(val(0), CompOp::Neq, str_const("hello"), true), ]; let mut ctx = SmtContext::new(); let env = PathEnv::empty(); let sym = SymbolicState::new(); let result = ctx.check_path_feasibility(&constraints, &sym, &env); assert_eq!(result, SmtResult::Unsat); } #[test] fn string_cross_variable_contradiction() { // x == "hello" AND y == "world" AND x == y → Unsat let constraints = vec![ comparison_constraint(val(0), CompOp::Eq, str_const("hello"), true), comparison_constraint(val(1), CompOp::Eq, str_const("world"), true), comparison_constraint(val(0), CompOp::Eq, val(1), true), ]; let mut ctx = SmtContext::new(); let env = PathEnv::empty(); let sym = SymbolicState::new(); let result = ctx.check_path_feasibility(&constraints, &sym, &env); assert_eq!(result, SmtResult::Unsat); } // ── PathEnv string seeding ──────────────────────────────── #[test] fn path_env_string_seeding() { // Seed PathEnv with x = "safe", then assert x == "danger" → Unsat. let mut env = PathEnv::empty(); use crate::constraint::ValueFact; let mut fact = ValueFact::top(); fact.exact = Some(ConstValue::Str("safe".into())); env.refine(SsaValue(0), &fact); let constraints = vec![comparison_constraint( val(0), CompOp::Eq, str_const("danger"), true, )]; let mut ctx = SmtContext::new(); let sym = SymbolicState::new(); let result = ctx.check_path_feasibility(&constraints, &sym, &env); assert_eq!(result, SmtResult::Unsat); } // ── Mixed int + string ─────────────────────────────────────────────── #[test] fn mixed_int_string_no_conflict() { // x > 5 (int) AND y == "hello" (string) → Sat (independent) let constraints = vec![ comparison_constraint(val(0), CompOp::Gt, int_const(5), true), comparison_constraint(val(1), CompOp::Eq, str_const("hello"), true), ]; let mut ctx = SmtContext::new(); let env = PathEnv::empty(); let sym = SymbolicState::new(); let result = ctx.check_path_feasibility(&constraints, &sym, &env); assert_eq!(result, SmtResult::Sat); } // ── Negated polarity ───────────────────────────────────────────────── #[test] fn negated_polarity() { // !(x > 10) means x <= 10, combined with x > 20 → Unsat. let constraints = vec![ comparison_constraint(val(0), CompOp::Gt, int_const(10), false), // x <= 10 comparison_constraint(val(0), CompOp::Gt, int_const(20), true), // x > 20 ]; let mut ctx = SmtContext::new(); let env = PathEnv::empty(); let sym = SymbolicState::new(); let result = ctx.check_path_feasibility(&constraints, &sym, &env); assert_eq!(result, SmtResult::Unsat); } #[test] fn negated_string_equality() { // !(x == "hello") means x != "hello", combined with x == "hello" → Unsat. let constraints = vec![ comparison_constraint(val(0), CompOp::Eq, str_const("hello"), false), comparison_constraint(val(0), CompOp::Eq, str_const("hello"), true), ]; let mut ctx = SmtContext::new(); let env = PathEnv::empty(); let sym = SymbolicState::new(); let result = ctx.check_path_feasibility(&constraints, &sym, &env); assert_eq!(result, SmtResult::Unsat); } // ── Cross-variable with equality (integer) ────────────────────────── #[test] fn cross_variable_equality_contradiction() { // x == y AND x > 5 AND y < 3 → Unsat let constraints = vec![ comparison_constraint(val(0), CompOp::Eq, val(1), true), comparison_constraint(val(0), CompOp::Gt, int_const(5), true), comparison_constraint(val(1), CompOp::Lt, int_const(3), true), ]; let mut ctx = SmtContext::new(); let env = PathEnv::empty(); let sym = SymbolicState::new(); let result = ctx.check_path_feasibility(&constraints, &sym, &env); assert_eq!(result, SmtResult::Unsat); } // ── Lexicographic string ordering ───────────────────────── #[test] fn string_lexicographic_contradiction() { // x < "apple" AND x > "banana" → Unsat // (lexicographically "apple" < "banana", so nothing is < "apple" AND > "banana") // Bundled Z3 may not support str.< / str.<=, returning Unknown instead. let constraints = vec![ comparison_constraint(val(0), CompOp::Lt, str_const("apple"), true), comparison_constraint(val(0), CompOp::Gt, str_const("banana"), true), ]; let mut ctx = SmtContext::new(); let env = PathEnv::empty(); let sym = SymbolicState::new(); let result = ctx.check_path_feasibility(&constraints, &sym, &env); assert!( result == SmtResult::Unsat || result == SmtResult::Unknown, "expected Unsat or Unknown, got {result:?}" ); } #[test] fn string_lexicographic_satisfiable() { // x > "apple" AND x < "banana" → Sat (e.g., x = "avocado") // Bundled Z3 may not support str.< / str.<=, returning Unknown instead. let constraints = vec![ comparison_constraint(val(0), CompOp::Gt, str_const("apple"), true), comparison_constraint(val(0), CompOp::Lt, str_const("banana"), true), ]; let mut ctx = SmtContext::new(); let env = PathEnv::empty(); let sym = SymbolicState::new(); let result = ctx.check_path_feasibility(&constraints, &sym, &env); assert!( result == SmtResult::Sat || result == SmtResult::Unknown, "expected Sat or Unknown, got {result:?}" ); } #[test] fn string_lexicographic_le_ge() { // x <= "apple" AND x >= "banana" → Unsat // Bundled Z3 may not support str.< / str.<=, returning Unknown instead. let constraints = vec![ comparison_constraint(val(0), CompOp::Le, str_const("apple"), true), comparison_constraint(val(0), CompOp::Ge, str_const("banana"), true), ]; let mut ctx = SmtContext::new(); let env = PathEnv::empty(); let sym = SymbolicState::new(); let result = ctx.check_path_feasibility(&constraints, &sym, &env); assert!( result == SmtResult::Unsat || result == SmtResult::Unknown, "expected Unsat or Unknown, got {result:?}" ); } }