//! Symbolic value expression trees. #![allow(clippy::collapsible_if)] use std::fmt; use crate::cfg; use crate::ssa::ir::{BlockId, SsaValue}; use crate::utils::snippet::truncate_at_char_boundary; /// Maximum expression tree depth before collapsing to `Unknown`. pub const MAX_EXPR_DEPTH: u32 = 32; /// Binary operator for symbolic expressions. /// /// Local to the symex module; converted from `cfg::BinOp` via `From`. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum Op { Add, Sub, Mul, Div, Mod, // Bitwise BitAnd, BitOr, BitXor, LeftShift, RightShift, // Comparison (produce 1/0 as integer values) Eq, NotEq, Lt, LtEq, Gt, GtEq, } impl From for Op { fn from(b: cfg::BinOp) -> Self { match b { cfg::BinOp::Add => Op::Add, cfg::BinOp::Sub => Op::Sub, cfg::BinOp::Mul => Op::Mul, cfg::BinOp::Div => Op::Div, cfg::BinOp::Mod => Op::Mod, cfg::BinOp::BitAnd => Op::BitAnd, cfg::BinOp::BitOr => Op::BitOr, cfg::BinOp::BitXor => Op::BitXor, cfg::BinOp::LeftShift => Op::LeftShift, cfg::BinOp::RightShift => Op::RightShift, cfg::BinOp::Eq => Op::Eq, cfg::BinOp::NotEq => Op::NotEq, cfg::BinOp::Lt => Op::Lt, cfg::BinOp::LtEq => Op::LtEq, cfg::BinOp::Gt => Op::Gt, cfg::BinOp::GtEq => Op::GtEq, } } } impl fmt::Display for Op { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Op::Add => write!(f, "+"), Op::Sub => write!(f, "-"), Op::Mul => write!(f, "*"), Op::Div => write!(f, "/"), Op::Mod => write!(f, "%"), Op::BitAnd => write!(f, "&"), Op::BitOr => write!(f, "|"), Op::BitXor => write!(f, "^"), Op::LeftShift => write!(f, "<<"), Op::RightShift => write!(f, ">>"), Op::Eq => write!(f, "=="), Op::NotEq => write!(f, "!="), Op::Lt => write!(f, "<"), Op::LtEq => write!(f, "<="), Op::Gt => write!(f, ">"), Op::GtEq => write!(f, ">="), } } } /// A symbolic expression tree representing how a value is computed. /// /// Expression trees are depth-bounded by [`MAX_EXPR_DEPTH`]; all construction /// goes through smart constructors ([`mk_binop`], [`mk_concat`], [`mk_call`], /// [`mk_phi`]) that enforce this limit. #[derive(Clone, Debug, PartialEq)] pub enum SymbolicValue { /// Known integer constant. Concrete(i64), /// Known string constant (quotes already stripped). ConcreteStr(String), /// Unconstrained symbolic input tied to an SSA value. Symbol(SsaValue), /// Arithmetic binary operation. BinOp(Op, Box, Box), /// String concatenation. Concat(Box, Box), /// Uninterpreted function application. Call(String, Vec), /// Phi merge (stored structurally; not resolved in single-path mode). Phi(Vec<(BlockId, SymbolicValue)>), // ── String operations ───────────────────────────────────── /// String substring extraction: `str.substring(start, end?)`. Substr( Box, Box, Option>, ), /// String replacement with concrete pattern/replacement: `str.replace(pat, rep)`. Replace(Box, String, String), /// To lowercase: `str.toLowerCase()`. ToLower(Box), /// To uppercase: `str.toUpperCase()`. ToUpper(Box), /// Whitespace trim: `str.trim()`. Trim(Box), /// String length (returns integer): `strlen(str)`. StrLen(Box), // ── Encoding/decoding transforms ─────────────────────────── /// Protective or representation transform applied to inner value. /// Preserves taint unconditionally, does NOT sanitize in symex. Encode(super::strings::TransformKind, Box), /// Decoding/reverse transform applied to inner value. Decode(super::strings::TransformKind, Box), /// No information (top). Unknown, } impl SymbolicValue { /// Compute the depth of this expression tree. /// /// Leaf nodes (`Concrete`, `ConcreteStr`, `Symbol`, `Unknown`) have depth 0. /// Compound nodes have depth 1 + max(children). pub fn depth(&self) -> u32 { match self { SymbolicValue::Concrete(_) | SymbolicValue::ConcreteStr(_) | SymbolicValue::Symbol(_) | SymbolicValue::Unknown => 0, SymbolicValue::BinOp(_, l, r) | SymbolicValue::Concat(l, r) => { 1 + l.depth().max(r.depth()) } SymbolicValue::Call(_, args) => 1 + args.iter().map(|a| a.depth()).max().unwrap_or(0), SymbolicValue::Phi(operands) => { 1 + operands.iter().map(|(_, v)| v.depth()).max().unwrap_or(0) } SymbolicValue::ToLower(s) | SymbolicValue::ToUpper(s) | SymbolicValue::Trim(s) | SymbolicValue::StrLen(s) | SymbolicValue::Replace(s, _, _) | SymbolicValue::Encode(_, s) | SymbolicValue::Decode(_, s) => 1 + s.depth(), SymbolicValue::Substr(s, start, end) => { let max_child = s .depth() .max(start.depth()) .max(end.as_ref().map(|e| e.depth()).unwrap_or(0)); 1 + max_child } } } /// Returns `true` if this is a known concrete value (int or string). pub fn is_concrete(&self) -> bool { matches!( self, SymbolicValue::Concrete(_) | SymbolicValue::ConcreteStr(_) ) } /// Extract a concrete integer if this is `Concrete(n)`. pub fn as_concrete_int(&self) -> Option { match self { SymbolicValue::Concrete(n) => Some(*n), _ => None, } } /// Extract a concrete string reference if this is `ConcreteStr(s)`. pub fn as_concrete_str(&self) -> Option<&str> { match self { SymbolicValue::ConcreteStr(s) => Some(s), _ => None, } } } // Smart constructors, all tree-building goes through these /// Build a binary arithmetic expression with concrete folding and depth bounding. /// /// - If both operands are `Concrete`, folds via `checked_*` arithmetic. /// Overflow or division by zero produces `Unknown`. /// - If the resulting tree exceeds `MAX_EXPR_DEPTH`, returns `Unknown`. pub fn mk_binop(op: Op, lhs: SymbolicValue, rhs: SymbolicValue) -> SymbolicValue { // Concrete folding if let (SymbolicValue::Concrete(a), SymbolicValue::Concrete(b)) = (&lhs, &rhs) { let result = match op { Op::Add => a.checked_add(*b), Op::Sub => a.checked_sub(*b), Op::Mul => a.checked_mul(*b), Op::Div => { if *b == 0 { None } else { a.checked_div(*b) } } Op::Mod => { if *b == 0 { None } else { a.checked_rem(*b) } } // Bitwise, &, |, ^ cannot overflow on i64 Op::BitAnd => Some(*a & *b), Op::BitOr => Some(*a | *b), Op::BitXor => Some(*a ^ *b), // Shifts, bounds-checked to 0..=63 (i64 width) Op::LeftShift => { if *b < 0 || *b > 63 { None } else { a.checked_shl(*b as u32) } } Op::RightShift => { if *b < 0 || *b > 63 { None } else { a.checked_shr(*b as u32) } } // Comparisons, produce 1 (true) or 0 (false) Op::Eq => Some(if *a == *b { 1 } else { 0 }), Op::NotEq => Some(if *a != *b { 1 } else { 0 }), Op::Lt => Some(if *a < *b { 1 } else { 0 }), Op::LtEq => Some(if *a <= *b { 1 } else { 0 }), Op::Gt => Some(if *a > *b { 1 } else { 0 }), Op::GtEq => Some(if *a >= *b { 1 } else { 0 }), }; return match result { Some(n) => SymbolicValue::Concrete(n), None => SymbolicValue::Unknown, }; } // Depth check let depth = 1 + lhs.depth().max(rhs.depth()); if depth > MAX_EXPR_DEPTH { return SymbolicValue::Unknown; } SymbolicValue::BinOp(op, Box::new(lhs), Box::new(rhs)) } /// Build a string concatenation expression with concrete folding and depth bounding. /// /// - If both operands are `ConcreteStr`, folds to a single `ConcreteStr`. /// - If the resulting tree exceeds `MAX_EXPR_DEPTH`, returns `Unknown`. pub fn mk_concat(lhs: SymbolicValue, rhs: SymbolicValue) -> SymbolicValue { // Concrete folding: ConcreteStr + ConcreteStr if let (SymbolicValue::ConcreteStr(a), SymbolicValue::ConcreteStr(b)) = (&lhs, &rhs) { return SymbolicValue::ConcreteStr(format!("{}{}", a, b)); } // Depth check let depth = 1 + lhs.depth().max(rhs.depth()); if depth > MAX_EXPR_DEPTH { return SymbolicValue::Unknown; } SymbolicValue::Concat(Box::new(lhs), Box::new(rhs)) } /// Build an uninterpreted function call expression with depth bounding. pub fn mk_call(name: String, args: Vec) -> SymbolicValue { let max_arg_depth = args.iter().map(|a| a.depth()).max().unwrap_or(0); if 1 + max_arg_depth > MAX_EXPR_DEPTH { return SymbolicValue::Unknown; } SymbolicValue::Call(name, args) } /// Build a phi merge expression with simplification and depth bounding. /// /// - Single operand: unwrap to the operand value. /// - All operands identical: fold to one value. /// - Otherwise: build `Phi(...)` with depth check. pub fn mk_phi(operands: Vec<(BlockId, SymbolicValue)>) -> SymbolicValue { if operands.is_empty() { return SymbolicValue::Unknown; } if operands.len() == 1 { return operands.into_iter().next().unwrap().1; } // All-same fold if operands.windows(2).all(|w| w[0].1 == w[1].1) { return operands.into_iter().next().unwrap().1; } // Depth check let max_depth = operands.iter().map(|(_, v)| v.depth()).max().unwrap_or(0); if 1 + max_depth > MAX_EXPR_DEPTH { return SymbolicValue::Unknown; } SymbolicValue::Phi(operands) } // String operation smart constructors /// Build a `Trim` expression with concrete folding and depth bounding. pub fn mk_trim(s: SymbolicValue) -> SymbolicValue { if let Some(result) = s.as_concrete_str().and_then(|cs| { super::strings::evaluate_string_op_concrete(&super::strings::StringMethod::Trim, cs) }) { return result; } if 1 + s.depth() > MAX_EXPR_DEPTH { return SymbolicValue::Unknown; } SymbolicValue::Trim(Box::new(s)) } /// Build a `ToLower` expression with concrete folding and depth bounding. pub fn mk_to_lower(s: SymbolicValue) -> SymbolicValue { if let Some(result) = s.as_concrete_str().and_then(|cs| { super::strings::evaluate_string_op_concrete(&super::strings::StringMethod::ToLower, cs) }) { return result; } if 1 + s.depth() > MAX_EXPR_DEPTH { return SymbolicValue::Unknown; } SymbolicValue::ToLower(Box::new(s)) } /// Build a `ToUpper` expression with concrete folding and depth bounding. pub fn mk_to_upper(s: SymbolicValue) -> SymbolicValue { if let Some(result) = s.as_concrete_str().and_then(|cs| { super::strings::evaluate_string_op_concrete(&super::strings::StringMethod::ToUpper, cs) }) { return result; } if 1 + s.depth() > MAX_EXPR_DEPTH { return SymbolicValue::Unknown; } SymbolicValue::ToUpper(Box::new(s)) } /// Build a `Replace` expression with concrete folding and depth bounding. pub fn mk_replace(s: SymbolicValue, pattern: String, replacement: String) -> SymbolicValue { if let Some(result) = s.as_concrete_str().and_then(|cs| { super::strings::evaluate_string_op_concrete( &super::strings::StringMethod::Replace { pattern: pattern.clone(), replacement: replacement.clone(), }, cs, ) }) { return result; } if 1 + s.depth() > MAX_EXPR_DEPTH { return SymbolicValue::Unknown; } SymbolicValue::Replace(Box::new(s), pattern, replacement) } /// Build a `Substr` expression with concrete folding and depth bounding. pub fn mk_substr( s: SymbolicValue, start: SymbolicValue, end: Option, ) -> SymbolicValue { // Concrete folding: all three are concrete if let Some(cs) = s.as_concrete_str() { if let Some(i) = start.as_concrete_int() { let i = i.max(0) as usize; match end.as_ref().and_then(|e| e.as_concrete_int()) { Some(j) => { let j = j.max(0) as usize; let result = cs.get(i..j.min(cs.len())).unwrap_or(""); return SymbolicValue::ConcreteStr(result.to_owned()); } None if end.is_none() => { let result = cs.get(i..).unwrap_or(""); return SymbolicValue::ConcreteStr(result.to_owned()); } _ => {} // end is Some but not concrete, can't fold } } } let max_child = s .depth() .max(start.depth()) .max(end.as_ref().map(|e| e.depth()).unwrap_or(0)); if 1 + max_child > MAX_EXPR_DEPTH { return SymbolicValue::Unknown; } SymbolicValue::Substr(Box::new(s), Box::new(start), end.map(Box::new)) } /// Build a `StrLen` expression with concrete folding and depth bounding. pub fn mk_strlen(s: SymbolicValue) -> SymbolicValue { if let Some(result) = s.as_concrete_str().and_then(|cs| { super::strings::evaluate_string_op_concrete(&super::strings::StringMethod::StrLen, cs) }) { return result; } if 1 + s.depth() > MAX_EXPR_DEPTH { return SymbolicValue::Unknown; } SymbolicValue::StrLen(Box::new(s)) } // ── Encoding/decoding smart constructors ─────────────────────────── /// Build an `Encode` expression with concrete folding and depth bounding. /// /// When `s` is a `ConcreteStr`, applies the encoding via witness-quality /// helpers and returns a folded `ConcreteStr`. Otherwise returns a /// structured `Encode` node. pub fn mk_encode(kind: super::strings::TransformKind, s: SymbolicValue) -> SymbolicValue { if let Some(cs) = s.as_concrete_str() { if let Some(encoded) = super::strings::encode_concrete_for_witness(kind, cs) { return SymbolicValue::ConcreteStr(encoded); } } if 1 + s.depth() > MAX_EXPR_DEPTH { return SymbolicValue::Unknown; } SymbolicValue::Encode(kind, Box::new(s)) } /// Build a `Decode` expression with concrete folding and depth bounding. pub fn mk_decode(kind: super::strings::TransformKind, s: SymbolicValue) -> SymbolicValue { if let Some(cs) = s.as_concrete_str() { if let Some(decoded) = super::strings::decode_concrete_for_witness(kind, cs) { return SymbolicValue::ConcreteStr(decoded); } } if 1 + s.depth() > MAX_EXPR_DEPTH { return SymbolicValue::Unknown; } SymbolicValue::Decode(kind, Box::new(s)) } // Display, human-readable witness strings /// Maximum length for the Display output before truncation. const MAX_DISPLAY_LEN: usize = 256; /// Maximum length for inline string constants in Display. const MAX_STR_DISPLAY_LEN: usize = 64; impl fmt::Display for SymbolicValue { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // Use an internal formatter, then truncate if needed. UTF-8-safe // truncation, `ConcreteStr` may carry localised text from source // (e.g. Cyrillic / Gurmukhi regex literals). let s = display_inner(self); if s.len() > MAX_DISPLAY_LEN { write!(f, "{}...", truncate_at_char_boundary(&s, MAX_DISPLAY_LEN)) } else { write!(f, "{}", s) } } } fn display_inner(val: &SymbolicValue) -> String { match val { SymbolicValue::Concrete(n) => format!("{}", n), SymbolicValue::ConcreteStr(s) => { if s.len() > MAX_STR_DISPLAY_LEN { format!( "\"{}...\"", truncate_at_char_boundary(s, MAX_STR_DISPLAY_LEN) ) } else { format!("\"{}\"", s) } } SymbolicValue::Symbol(v) => format!("sym(v{})", v.0), SymbolicValue::BinOp(op, l, r) => { format!("({} {} {})", display_inner(l), op, display_inner(r)) } SymbolicValue::Concat(l, r) => { format!("({} ++ {})", display_inner(l), display_inner(r)) } SymbolicValue::Call(name, args) => { let arg_strs: Vec = args.iter().map(display_inner).collect(); format!("{}({})", name, arg_strs.join(", ")) } SymbolicValue::Phi(operands) => { let parts: Vec = operands .iter() .map(|(bid, v)| format!("B{}:{}", bid.0, display_inner(v))) .collect(); format!("phi({})", parts.join(", ")) } SymbolicValue::Trim(s) => format!("{}.trim()", display_inner(s)), SymbolicValue::ToLower(s) => format!("{}.toLowerCase()", display_inner(s)), SymbolicValue::ToUpper(s) => format!("{}.toUpperCase()", display_inner(s)), SymbolicValue::Replace(s, pat, rep) => { format!("{}.replace(\"{}\", \"{}\")", display_inner(s), pat, rep) } SymbolicValue::Substr(s, start, end) => match end { Some(e) => format!( "{}.substr({}, {})", display_inner(s), display_inner(start), display_inner(e) ), None => format!("{}.substr({})", display_inner(s), display_inner(start)), }, SymbolicValue::StrLen(s) => format!("strlen({})", display_inner(s)), SymbolicValue::Encode(kind, s) => { format!("{}({})", kind.display_name(), display_inner(s)) } SymbolicValue::Decode(kind, s) => { format!("decode_{}({})", kind.display_name(), display_inner(s)) } SymbolicValue::Unknown => "?".to_string(), } } // Tests #[cfg(test)] mod tests { use super::*; #[test] fn concrete_fold_add() { assert_eq!( mk_binop( Op::Add, SymbolicValue::Concrete(3), SymbolicValue::Concrete(5) ), SymbolicValue::Concrete(8) ); } #[test] fn concrete_fold_sub() { assert_eq!( mk_binop( Op::Sub, SymbolicValue::Concrete(10), SymbolicValue::Concrete(3) ), SymbolicValue::Concrete(7) ); } #[test] fn concrete_fold_mul() { assert_eq!( mk_binop( Op::Mul, SymbolicValue::Concrete(4), SymbolicValue::Concrete(7) ), SymbolicValue::Concrete(28) ); } #[test] fn concrete_fold_div() { assert_eq!( mk_binop( Op::Div, SymbolicValue::Concrete(15), SymbolicValue::Concrete(3) ), SymbolicValue::Concrete(5) ); } #[test] fn concrete_fold_mod() { assert_eq!( mk_binop( Op::Mod, SymbolicValue::Concrete(17), SymbolicValue::Concrete(5) ), SymbolicValue::Concrete(2) ); } #[test] fn overflow_add() { assert_eq!( mk_binop( Op::Add, SymbolicValue::Concrete(i64::MAX), SymbolicValue::Concrete(1) ), SymbolicValue::Unknown ); } #[test] fn overflow_sub() { assert_eq!( mk_binop( Op::Sub, SymbolicValue::Concrete(i64::MIN), SymbolicValue::Concrete(1) ), SymbolicValue::Unknown ); } #[test] fn overflow_mul() { assert_eq!( mk_binop( Op::Mul, SymbolicValue::Concrete(i64::MAX), SymbolicValue::Concrete(2) ), SymbolicValue::Unknown ); } #[test] fn div_by_zero() { assert_eq!( mk_binop( Op::Div, SymbolicValue::Concrete(10), SymbolicValue::Concrete(0) ), SymbolicValue::Unknown ); } #[test] fn mod_by_zero() { assert_eq!( mk_binop( Op::Mod, SymbolicValue::Concrete(10), SymbolicValue::Concrete(0) ), SymbolicValue::Unknown ); } #[test] fn min_mod_neg_one() { // i64::MIN % -1 overflows assert_eq!( mk_binop( Op::Mod, SymbolicValue::Concrete(i64::MIN), SymbolicValue::Concrete(-1) ), SymbolicValue::Unknown ); } #[test] fn depth_bounding() { // Build a chain of depth 33, should collapse to Unknown let mut val = SymbolicValue::Symbol(SsaValue(0)); for _ in 0..MAX_EXPR_DEPTH { val = mk_binop(Op::Add, val, SymbolicValue::Concrete(1)); } // At depth == MAX_EXPR_DEPTH, should still be fine (depth check is >) assert_ne!(val, SymbolicValue::Unknown); assert_eq!(val.depth(), MAX_EXPR_DEPTH); // One more pushes past the limit val = mk_binop(Op::Add, val, SymbolicValue::Concrete(1)); assert_eq!(val, SymbolicValue::Unknown); } #[test] fn concat_fold() { assert_eq!( mk_concat( SymbolicValue::ConcreteStr("hello ".into()), SymbolicValue::ConcreteStr("world".into()), ), SymbolicValue::ConcreteStr("hello world".into()) ); } #[test] fn concat_no_int_coercion() { // ConcreteStr + Concrete(int) should NOT fold, no type coercion let result = mk_concat( SymbolicValue::ConcreteStr("val=".into()), SymbolicValue::Concrete(42), ); assert!(matches!(result, SymbolicValue::Concat(_, _))); } #[test] fn concat_depth_bounding() { let mut val = SymbolicValue::ConcreteStr("a".into()); for _ in 0..MAX_EXPR_DEPTH { val = mk_concat(val, SymbolicValue::Symbol(SsaValue(0))); } assert_eq!(val.depth(), MAX_EXPR_DEPTH); val = mk_concat(val, SymbolicValue::Symbol(SsaValue(0))); assert_eq!(val, SymbolicValue::Unknown); } #[test] fn phi_single_operand_unwrap() { let v = SymbolicValue::Concrete(42); assert_eq!(mk_phi(vec![(BlockId(0), v.clone())]), v); } #[test] fn phi_all_same_fold() { let v = SymbolicValue::Concrete(7); assert_eq!( mk_phi(vec![(BlockId(0), v.clone()), (BlockId(1), v.clone())]), v ); } #[test] fn phi_different_values() { let result = mk_phi(vec![ (BlockId(0), SymbolicValue::Concrete(1)), (BlockId(1), SymbolicValue::Concrete(2)), ]); assert!(matches!(result, SymbolicValue::Phi(_))); } #[test] fn phi_empty() { assert_eq!(mk_phi(vec![]), SymbolicValue::Unknown); } #[test] fn call_depth_bounding() { let deep = { let mut v = SymbolicValue::Symbol(SsaValue(0)); for _ in 0..MAX_EXPR_DEPTH { v = mk_binop(Op::Add, v, SymbolicValue::Concrete(1)); } v }; // deep has depth == MAX_EXPR_DEPTH; wrapping in Call would exceed let result = mk_call("f".into(), vec![deep]); assert_eq!(result, SymbolicValue::Unknown); } #[test] fn depth_leaf_nodes() { assert_eq!(SymbolicValue::Concrete(0).depth(), 0); assert_eq!(SymbolicValue::ConcreteStr("x".into()).depth(), 0); assert_eq!(SymbolicValue::Symbol(SsaValue(0)).depth(), 0); assert_eq!(SymbolicValue::Unknown.depth(), 0); } #[test] fn depth_nested() { let v = mk_binop( Op::Add, mk_binop( Op::Mul, SymbolicValue::Concrete(2), SymbolicValue::Symbol(SsaValue(0)), ), SymbolicValue::Concrete(1), ); assert_eq!(v.depth(), 2); } #[test] fn is_concrete_checks() { assert!(SymbolicValue::Concrete(1).is_concrete()); assert!(SymbolicValue::ConcreteStr("x".into()).is_concrete()); assert!(!SymbolicValue::Symbol(SsaValue(0)).is_concrete()); assert!(!SymbolicValue::Unknown.is_concrete()); } #[test] fn as_concrete_int_checks() { assert_eq!(SymbolicValue::Concrete(42).as_concrete_int(), Some(42)); assert_eq!( SymbolicValue::ConcreteStr("x".into()).as_concrete_int(), None ); assert_eq!(SymbolicValue::Unknown.as_concrete_int(), None); } #[test] fn as_concrete_str_checks() { assert_eq!( SymbolicValue::ConcreteStr("hi".into()).as_concrete_str(), Some("hi") ); assert_eq!(SymbolicValue::Concrete(1).as_concrete_str(), None); } #[test] fn display_concrete() { assert_eq!(format!("{}", SymbolicValue::Concrete(42)), "42"); } #[test] fn display_concrete_str() { assert_eq!( format!("{}", SymbolicValue::ConcreteStr("hello".into())), "\"hello\"" ); } #[test] fn display_symbol() { assert_eq!(format!("{}", SymbolicValue::Symbol(SsaValue(3))), "sym(v3)"); } #[test] fn display_binop() { let v = mk_binop( Op::Add, SymbolicValue::Symbol(SsaValue(1)), SymbolicValue::Concrete(2), ); assert_eq!(format!("{}", v), "(sym(v1) + 2)"); } #[test] fn display_concat() { let v = mk_concat( SymbolicValue::ConcreteStr("SELECT ".into()), SymbolicValue::Symbol(SsaValue(5)), ); assert_eq!(format!("{}", v), "(\"SELECT \" ++ sym(v5))"); } #[test] fn display_call() { let v = mk_call("parseInt".into(), vec![SymbolicValue::Symbol(SsaValue(2))]); assert_eq!(format!("{}", v), "parseInt(sym(v2))"); } #[test] fn display_phi() { let v = mk_phi(vec![ (BlockId(0), SymbolicValue::Concrete(1)), (BlockId(1), SymbolicValue::Symbol(SsaValue(3))), ]); assert_eq!(format!("{}", v), "phi(B0:1, B1:sym(v3))"); } #[test] fn display_unknown() { assert_eq!(format!("{}", SymbolicValue::Unknown), "?"); } #[test] fn display_truncation() { // Build a very long expression let mut v = SymbolicValue::Symbol(SsaValue(0)); for i in 1..30 { v = mk_binop(Op::Add, v, SymbolicValue::Symbol(SsaValue(i))); } let s = format!("{}", v); assert!(s.len() <= MAX_DISPLAY_LEN + 3); // +3 for "..." if s.len() > MAX_DISPLAY_LEN { assert!(s.ends_with("...")); } } #[test] fn display_long_string_truncation() { let long = "a".repeat(100); let v = SymbolicValue::ConcreteStr(long); let s = format!("{}", v); assert!(s.contains("...")); assert!(s.len() <= MAX_STR_DISPLAY_LEN + 6); // quotes + "..." } #[test] fn op_from_cfg_binop() { assert_eq!(Op::from(cfg::BinOp::Add), Op::Add); assert_eq!(Op::from(cfg::BinOp::Sub), Op::Sub); assert_eq!(Op::from(cfg::BinOp::Mul), Op::Mul); assert_eq!(Op::from(cfg::BinOp::Div), Op::Div); assert_eq!(Op::from(cfg::BinOp::Mod), Op::Mod); } // ── Bitwise and comparison operation tests ────────────── #[test] fn op_from_cfg_binop_extended() { assert_eq!(Op::from(cfg::BinOp::BitAnd), Op::BitAnd); assert_eq!(Op::from(cfg::BinOp::BitOr), Op::BitOr); assert_eq!(Op::from(cfg::BinOp::BitXor), Op::BitXor); assert_eq!(Op::from(cfg::BinOp::LeftShift), Op::LeftShift); assert_eq!(Op::from(cfg::BinOp::RightShift), Op::RightShift); assert_eq!(Op::from(cfg::BinOp::Eq), Op::Eq); assert_eq!(Op::from(cfg::BinOp::NotEq), Op::NotEq); assert_eq!(Op::from(cfg::BinOp::Lt), Op::Lt); assert_eq!(Op::from(cfg::BinOp::LtEq), Op::LtEq); assert_eq!(Op::from(cfg::BinOp::Gt), Op::Gt); assert_eq!(Op::from(cfg::BinOp::GtEq), Op::GtEq); } #[test] fn display_bitwise_ops() { assert_eq!(format!("{}", Op::BitAnd), "&"); assert_eq!(format!("{}", Op::BitOr), "|"); assert_eq!(format!("{}", Op::BitXor), "^"); assert_eq!(format!("{}", Op::LeftShift), "<<"); assert_eq!(format!("{}", Op::RightShift), ">>"); } #[test] fn display_comparison_ops() { assert_eq!(format!("{}", Op::Eq), "=="); assert_eq!(format!("{}", Op::NotEq), "!="); assert_eq!(format!("{}", Op::Lt), "<"); assert_eq!(format!("{}", Op::LtEq), "<="); assert_eq!(format!("{}", Op::Gt), ">"); assert_eq!(format!("{}", Op::GtEq), ">="); } #[test] fn concrete_fold_bit_and() { assert_eq!(mk_binop(Op::BitAnd, c(0xFF), c(0x0F)), c(0x0F)); assert_eq!(mk_binop(Op::BitAnd, c(-1), c(0x07)), c(0x07)); } #[test] fn concrete_fold_bit_or() { assert_eq!(mk_binop(Op::BitOr, c(0xF0), c(0x0F)), c(0xFF)); } #[test] fn concrete_fold_bit_xor() { assert_eq!(mk_binop(Op::BitXor, c(0xFF), c(0x0F)), c(0xF0)); // x ^ x = 0 assert_eq!(mk_binop(Op::BitXor, c(42), c(42)), c(0)); } #[test] fn concrete_fold_left_shift() { assert_eq!(mk_binop(Op::LeftShift, c(1), c(3)), c(8)); assert_eq!(mk_binop(Op::LeftShift, c(0x0F), c(4)), c(0xF0)); } #[test] fn concrete_fold_right_shift() { assert_eq!(mk_binop(Op::RightShift, c(16), c(2)), c(4)); assert_eq!(mk_binop(Op::RightShift, c(0xFF), c(4)), c(0x0F)); } #[test] fn left_shift_negative_amount() { assert_eq!(mk_binop(Op::LeftShift, c(1), c(-1)), SymbolicValue::Unknown); } #[test] fn left_shift_amount_64() { assert_eq!(mk_binop(Op::LeftShift, c(1), c(64)), SymbolicValue::Unknown); } #[test] fn left_shift_amount_63() { // Max valid shift, should not panic let result = mk_binop(Op::LeftShift, c(1), c(63)); assert_eq!(result, c(1i64 << 63)); } #[test] fn right_shift_negative_amount() { assert_eq!( mk_binop(Op::RightShift, c(1), c(-1)), SymbolicValue::Unknown ); } #[test] fn right_shift_amount_64() { assert_eq!( mk_binop(Op::RightShift, c(1), c(64)), SymbolicValue::Unknown ); } #[test] fn concrete_fold_eq_true() { assert_eq!(mk_binop(Op::Eq, c(5), c(5)), c(1)); } #[test] fn concrete_fold_eq_false() { assert_eq!(mk_binop(Op::Eq, c(5), c(3)), c(0)); } #[test] fn concrete_fold_neq() { assert_eq!(mk_binop(Op::NotEq, c(5), c(3)), c(1)); assert_eq!(mk_binop(Op::NotEq, c(5), c(5)), c(0)); } #[test] fn concrete_fold_lt() { assert_eq!(mk_binop(Op::Lt, c(3), c(5)), c(1)); assert_eq!(mk_binop(Op::Lt, c(5), c(3)), c(0)); assert_eq!(mk_binop(Op::Lt, c(5), c(5)), c(0)); } #[test] fn concrete_fold_lteq() { assert_eq!(mk_binop(Op::LtEq, c(3), c(3)), c(1)); assert_eq!(mk_binop(Op::LtEq, c(4), c(3)), c(0)); } #[test] fn concrete_fold_gt() { assert_eq!(mk_binop(Op::Gt, c(5), c(3)), c(1)); assert_eq!(mk_binop(Op::Gt, c(3), c(5)), c(0)); } #[test] fn concrete_fold_gteq() { assert_eq!(mk_binop(Op::GtEq, c(3), c(3)), c(1)); assert_eq!(mk_binop(Op::GtEq, c(2), c(3)), c(0)); } /// Helper: shorthand for `SymbolicValue::Concrete`. fn c(n: i64) -> SymbolicValue { SymbolicValue::Concrete(n) } // ── String operation tests ────────────────────────────── #[test] fn mk_trim_concrete_fold() { assert_eq!( mk_trim(SymbolicValue::ConcreteStr(" hello ".into())), SymbolicValue::ConcreteStr("hello".into()) ); } #[test] fn mk_trim_symbolic() { let v = mk_trim(SymbolicValue::Symbol(SsaValue(0))); assert!(matches!(v, SymbolicValue::Trim(_))); assert_eq!(v.depth(), 1); } #[test] fn mk_to_lower_concrete_fold() { assert_eq!( mk_to_lower(SymbolicValue::ConcreteStr("ABC".into())), SymbolicValue::ConcreteStr("abc".into()) ); } #[test] fn mk_to_upper_concrete_fold() { assert_eq!( mk_to_upper(SymbolicValue::ConcreteStr("abc".into())), SymbolicValue::ConcreteStr("ABC".into()) ); } #[test] fn mk_replace_concrete_fold() { assert_eq!( mk_replace( SymbolicValue::ConcreteStr("a