#![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};
const MAX_SMT_QUERIES_PER_FINDING: u32 = 10;
const SMT_QUERY_TIMEOUT_MS: u32 = 100;
const SMT_STRING_QUERY_TIMEOUT_MS: u32 = 500;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SmtResult {
Sat,
Unsat,
Unknown,
BudgetExhausted,
}
pub struct SmtContext {
cfg: Config,
queries_used: u32,
timeout_ms: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum VarSort {
Int,
Str,
}
enum Z3Var {
Int(Z3Int),
Str(Z3Str),
}
enum Z3Expr {
Int(Z3Int),
Str(Z3Str),
}
type VarMap = HashMap<SsaValue, Z3Var>;
impl SmtContext {
pub fn new() -> Self {
SmtContext {
cfg: Config::new(),
queries_used: 0,
timeout_ms: SMT_QUERY_TIMEOUT_MS,
}
}
pub fn has_budget(&self) -> bool {
self.queries_used < MAX_SMT_QUERIES_PER_FINDING
}
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;
let base_timeout_ms = self.timeout_ms;
z3::with_z3_config(&self.cfg, || {
let solver = Solver::new();
let mut var_map: VarMap = HashMap::new();
seed_from_path_env(&solver, &mut var_map, env);
for pc in constraints {
assert_path_constraint(&solver, &mut var_map, pc, env);
}
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);
match solver.check() {
SatResult::Unsat => SmtResult::Unsat,
SatResult::Sat => SmtResult::Sat,
SatResult::Unknown => SmtResult::Unknown,
}
})
}
}
fn is_known_int(v: SsaValue, env: &PathEnv) -> bool {
let fact = env.get(v);
if fact.lo.is_some() || fact.hi.is_some() {
return true;
}
if matches!(fact.exact, Some(ConstValue::Int(_))) {
return true;
}
false
}
fn is_known_str(v: SsaValue, env: &PathEnv) -> bool {
let fact = env.get(v);
if matches!(fact.exact, Some(ConstValue::Str(_))) {
return true;
}
if fact.types.is_singleton_of(&TypeKind::String) {
return true;
}
false
}
fn ensure_int_var(var_map: &mut VarMap, v: SsaValue, env: &PathEnv) -> Option<Z3Int> {
match var_map.get(&v) {
Some(Z3Var::Int(z)) => return Some(z.clone()),
Some(Z3Var::Str(_)) => return None, None => {}
}
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
}
fn force_int_var(var_map: &mut VarMap, v: SsaValue) -> Option<Z3Int> {
match var_map.get(&v) {
Some(Z3Var::Int(z)) => return Some(z.clone()),
Some(Z3Var::Str(_)) => return None, None => {}
}
let z3_var = Z3Int::new_const(format!("v{}", v.0));
var_map.insert(v, Z3Var::Int(z3_var.clone()));
Some(z3_var)
}
fn ensure_str_var(var_map: &mut VarMap, v: SsaValue, env: &PathEnv) -> Option<Z3Str> {
match var_map.get(&v) {
Some(Z3Var::Str(z)) => return Some(z.clone()),
Some(Z3Var::Int(_)) => return None, None => {}
}
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
}
fn force_str_var(var_map: &mut VarMap, v: SsaValue) -> Option<Z3Str> {
match var_map.get(&v) {
Some(Z3Var::Str(z)) => return Some(z.clone()),
Some(Z3Var::Int(_)) => return None, None => {}
}
let z3_var = Z3Str::new_const(format!("v{}", v.0));
var_map.insert(v, Z3Var::Str(z3_var.clone()));
Some(z3_var)
}
fn seed_from_path_env(solver: &Solver, var_map: &mut VarMap, env: &PathEnv) {
for &(v, ref fact) in env.facts() {
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)));
}
for excl in &fact.excluded {
if let ConstValue::Int(n) = excl {
solver.assert(&z3_var.ne(&Z3Int::from_i64(*n)));
}
}
}
continue;
}
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) {
if let Some(ConstValue::Str(s)) = &fact.exact {
solver.assert(&z3_var.eq(Z3Str::from(s.as_str())));
}
for excl in &fact.excluded {
if let ConstValue::Str(s) = excl {
solver.assert(&z3_var.ne(Z3Str::from(s.as_str())));
}
}
}
}
}
let known_vars: Vec<SsaValue> = 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));
}
_ => {} }
}
}
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));
}
_ => {} }
}
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)),
}
}
}
}
fn assert_path_constraint(
solver: &Solver,
var_map: &mut VarMap,
pc: &PathConstraint,
env: &PathEnv,
) {
match &pc.condition {
ConditionExpr::Comparison { lhs, op, rhs } => {
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());
}
}
}
}
ConditionExpr::BoolTest { var } => {
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());
}
}
}
ConditionExpr::NullCheck { .. }
| ConditionExpr::TypeCheck { .. }
| ConditionExpr::Unknown => {}
}
}
fn operand_sort_hint(op: &Operand) -> Option<VarSort> {
match op {
Operand::Const(ConstValue::Str(_)) => Some(VarSort::Str),
Operand::Const(ConstValue::Int(_) | ConstValue::Bool(_)) => Some(VarSort::Int),
_ => None,
}
}
fn translate_operand(var_map: &mut VarMap, op: &Operand, env: &PathEnv) -> Option<Z3Expr> {
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) => {
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 => {}
}
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);
}
None
}
Operand::Const(ConstValue::Null) | Operand::Unknown => None,
}
}
fn translate_operand_with_hint(
var_map: &mut VarMap,
op: &Operand,
env: &PathEnv,
hint: Option<VarSort>,
) -> Option<Z3Expr> {
if let Some(expr) = translate_operand(var_map, op, env) {
return Some(expr);
}
if let Operand::Value(v) = op {
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 => {
return force_int_var(var_map, *v).map(Z3Expr::Int);
}
}
}
}
None
}
fn build_comparison_poly(lhs: &Z3Expr, op: CompOp, rhs: &Z3Expr) -> Option<z3::ast::Bool> {
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, }
}
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),
}
}
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),
}
}
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,
})
}
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,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::constraint::{CompOp, ConditionExpr, Operand, PathEnv};
use crate::ssa::ir::{BlockId, SsaValue};
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()))
}
#[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() {
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() {
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));
}
#[test]
fn simple_contradiction() {
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);
}
#[test]
fn cross_variable_contradiction() {
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);
}
#[test]
fn arithmetic_cross_variable() {
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);
}
#[test]
fn satisfiable_path() {
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);
}
#[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();
for _ in 0..MAX_SMT_QUERIES_PER_FINDING {
let r = ctx.check_path_feasibility(&constraints, &sym, &env);
assert_ne!(r, SmtResult::BudgetExhausted);
}
let result = ctx.check_path_feasibility(&constraints, &sym, &env);
assert_eq!(result, SmtResult::BudgetExhausted);
}
#[test]
fn path_env_seeding_interval() {
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);
}
#[test]
fn skip_unknown_sort() {
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);
}
#[test]
fn string_equality_asserted() {
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() {
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() {
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() {
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() {
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);
}
#[test]
fn path_env_string_seeding() {
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);
}
#[test]
fn mixed_int_string_no_conflict() {
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);
}
#[test]
fn negated_polarity() {
let constraints = vec![
comparison_constraint(val(0), CompOp::Gt, int_const(10), false), comparison_constraint(val(0), CompOp::Gt, int_const(20), 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 negated_string_equality() {
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);
}
#[test]
fn cross_variable_equality_contradiction() {
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);
}
#[test]
fn string_lexicographic_contradiction() {
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() {
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() {
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:?}"
);
}
}