#![allow(clippy::collapsible_if)]
use crate::cfg::NodeInfo;
use crate::ssa::const_prop::ConstLattice;
use crate::ssa::ir::{BlockId, SsaBody, SsaValue};
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use std::collections::HashMap;
use super::domain::ConstValue;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Operand {
Value(SsaValue),
Const(ConstValue),
Unknown,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum CompOp {
Eq,
Neq,
Lt,
Gt,
Le,
Ge,
}
impl CompOp {
pub fn flip(self) -> Self {
match self {
Self::Lt => Self::Gt,
Self::Gt => Self::Lt,
Self::Le => Self::Ge,
Self::Ge => Self::Le,
other => other, }
}
pub fn negate(self) -> Self {
match self {
Self::Eq => Self::Neq,
Self::Neq => Self::Eq,
Self::Lt => Self::Ge,
Self::Ge => Self::Lt,
Self::Gt => Self::Le,
Self::Le => Self::Gt,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum ConditionExpr {
Comparison {
lhs: Operand,
op: CompOp,
rhs: Operand,
},
NullCheck { var: SsaValue, is_null: bool },
TypeCheck {
var: SsaValue,
type_name: String,
positive: bool,
},
BoolTest { var: SsaValue },
Unknown,
}
impl ConditionExpr {
pub fn negate(&self) -> Self {
match self {
Self::Comparison { lhs, op, rhs } => Self::Comparison {
lhs: lhs.clone(),
op: op.negate(),
rhs: rhs.clone(),
},
Self::NullCheck { var, is_null } => Self::NullCheck {
var: *var,
is_null: !is_null,
},
Self::TypeCheck {
var,
type_name,
positive,
} => Self::TypeCheck {
var: *var,
type_name: type_name.clone(),
positive: !positive,
},
Self::BoolTest { .. } | Self::Unknown => self.clone(),
}
}
}
pub fn lower_condition(
cond_info: &NodeInfo,
ssa: &SsaBody,
branch_block: BlockId,
const_values: Option<&HashMap<SsaValue, ConstLattice>>,
) -> ConditionExpr {
let text = match cond_info.condition_text.as_deref() {
Some(t) if !t.is_empty() => t,
_ => return ConditionExpr::Unknown,
};
if cond_info.condition_vars.is_empty() {
return ConditionExpr::Unknown;
}
let resolved = resolve_condition_vars(&cond_info.condition_vars, ssa, branch_block);
let var_lookup: HashMap<&str, SsaValue> = resolved
.iter()
.map(|(name, val)| (name.as_str(), *val))
.collect();
let const_lookup: HashMap<SsaValue, ConstValue> = if let Some(cv) = const_values {
resolved
.iter()
.filter_map(|(_, v)| {
cv.get(v)
.and_then(ConstValue::from_const_lattice)
.map(|c| (*v, c))
})
.collect()
} else {
HashMap::new()
};
let lower = text.to_ascii_lowercase();
let expr = try_lower_null_check(text, &lower, &var_lookup)
.or_else(|| try_lower_type_check(text, &lower, &var_lookup))
.or_else(|| try_lower_comparison(text, &var_lookup, &const_lookup))
.unwrap_or_else(|| {
if resolved.len() == 1 {
ConditionExpr::BoolTest { var: resolved[0].1 }
} else {
ConditionExpr::Unknown
}
});
if cond_info.condition_negated {
expr.negate()
} else {
expr
}
}
pub fn lower_condition_with_stacks(
cond_info: &NodeInfo,
var_stacks: &HashMap<String, Vec<SsaValue>>,
) -> ConditionExpr {
let text = match cond_info.condition_text.as_deref() {
Some(t) if !t.is_empty() => t,
_ => return ConditionExpr::Unknown,
};
if cond_info.condition_vars.is_empty() {
return ConditionExpr::Unknown;
}
let resolved: Vec<(String, SsaValue)> = cond_info
.condition_vars
.iter()
.filter_map(|name| {
var_stacks
.get(name)
.and_then(|stack| stack.last().copied())
.map(|v| (name.clone(), v))
})
.collect();
if resolved.is_empty() {
return ConditionExpr::Unknown;
}
let var_lookup: HashMap<&str, SsaValue> = resolved
.iter()
.map(|(name, val)| (name.as_str(), *val))
.collect();
let const_lookup: HashMap<SsaValue, super::domain::ConstValue> = HashMap::new();
let lower = text.to_ascii_lowercase();
let expr = try_lower_null_check(text, &lower, &var_lookup)
.or_else(|| try_lower_type_check(text, &lower, &var_lookup))
.or_else(|| try_lower_comparison(text, &var_lookup, &const_lookup))
.unwrap_or_else(|| {
if resolved.len() == 1 {
ConditionExpr::BoolTest { var: resolved[0].1 }
} else {
ConditionExpr::Unknown
}
});
if cond_info.condition_negated {
expr.negate()
} else {
expr
}
}
pub fn resolve_condition_vars(
vars: &[String],
ssa: &SsaBody,
block: BlockId,
) -> SmallVec<[(String, SsaValue); 4]> {
let mut result = SmallVec::new();
for var_name in vars {
if let Some(val) = resolve_single_var(var_name, ssa, block) {
result.push((var_name.clone(), val));
}
}
result
}
fn resolve_single_var(var_name: &str, ssa: &SsaBody, block: BlockId) -> Option<SsaValue> {
let mut best_in_block: Option<SsaValue> = None;
let mut best_outside: Option<SsaValue> = None;
for (idx, vd) in ssa.value_defs.iter().enumerate() {
if vd.var_name.as_deref() != Some(var_name) {
continue;
}
let v = SsaValue(idx as u32);
if vd.block == block {
best_in_block = Some(match best_in_block {
Some(existing) if existing.0 > v.0 => existing,
_ => v,
});
} else {
best_outside = Some(match best_outside {
Some(existing) if existing.0 > v.0 => existing,
_ => v,
});
}
}
best_in_block.or(best_outside)
}
fn try_lower_null_check(
text: &str,
lower: &str,
var_lookup: &HashMap<&str, SsaValue>,
) -> Option<ConditionExpr> {
let is_null;
let var_name;
if lower.contains(" is not none") {
is_null = false;
var_name = extract_before(text, " is not ");
} else if lower.contains(" is none") {
is_null = true;
var_name = extract_before(text, " is ");
}
else if let Some((lhs, rhs, negated)) = try_split_equality(text) {
let lhs_t = lhs.trim();
let rhs_t = rhs.trim();
let lhs_lower = lhs_t.to_ascii_lowercase();
let rhs_lower = rhs_t.to_ascii_lowercase();
let (null_side, var_side) =
if lhs_lower == "null" || lhs_lower == "nil" || lhs_lower == "none" {
(true, rhs_t)
} else if rhs_lower == "null" || rhs_lower == "nil" || rhs_lower == "none" {
(true, lhs_t)
} else {
return None; };
if !null_side {
return None;
}
var_name = Some(var_side);
is_null = !negated;
} else {
return None;
}
let var_name = var_name?;
let var_name_trimmed = var_name.trim();
let ssa_val = var_lookup.get(var_name_trimmed)?;
Some(ConditionExpr::NullCheck {
var: *ssa_val,
is_null,
})
}
fn try_lower_type_check(
text: &str,
lower: &str,
var_lookup: &HashMap<&str, SsaValue>,
) -> Option<ConditionExpr> {
if lower.starts_with("typeof ") {
let rest = &text[7..]; let (lhs, rhs, negated) = try_split_equality(rest)?;
let var_part = lhs.trim();
let type_part = rhs.trim();
let type_name = strip_quotes(type_part)?;
let ssa_val = var_lookup.get(var_part)?;
return Some(ConditionExpr::TypeCheck {
var: *ssa_val,
type_name: type_name.to_string(),
positive: !negated,
});
}
if lower.starts_with("isinstance(") && lower.ends_with(')') {
let inner = &text[11..text.len() - 1]; let comma = inner.find(',')?;
let var_part = inner[..comma].trim();
let type_part = inner[comma + 1..].trim();
let ssa_val = var_lookup.get(var_part)?;
return Some(ConditionExpr::TypeCheck {
var: *ssa_val,
type_name: type_part.to_string(),
positive: true,
});
}
if lower.starts_with("is_") && lower.ends_with(')') {
if let Some(paren) = text.find('(') {
let type_part = &text[3..paren]; let var_part = text[paren + 1..text.len() - 1].trim();
let ssa_val = var_lookup.get(var_part)?;
return Some(ConditionExpr::TypeCheck {
var: *ssa_val,
type_name: type_part.to_string(),
positive: true,
});
}
}
if let Some(pos) = lower.find(" instanceof ") {
let var_part = text[..pos].trim();
let type_part = text[pos + " instanceof ".len()..].trim();
if let Some(ssa_val) = var_lookup.get(var_part) {
return Some(ConditionExpr::TypeCheck {
var: *ssa_val,
type_name: type_part.to_string(),
positive: true,
});
}
}
for method in &[".is_a?(", ".kind_of?("] {
if let Some(dot_pos) = lower.find(method) {
let var_part = text[..dot_pos].trim();
let after = dot_pos + method.len();
if let Some(close) = text[after..].find(')') {
let type_part = text[after..after + close].trim();
if let Some(ssa_val) = var_lookup.get(var_part) {
return Some(ConditionExpr::TypeCheck {
var: *ssa_val,
type_name: type_part.to_string(),
positive: true,
});
}
}
}
}
None
}
fn try_lower_comparison(
text: &str,
var_lookup: &HashMap<&str, SsaValue>,
const_lookup: &HashMap<SsaValue, ConstValue>,
) -> Option<ConditionExpr> {
let operators = ["===", "!==", "==", "!=", ">=", "<=", ">", "<"];
let mut found_op = None;
let mut found_pos = 0;
let mut found_len = 0;
for op_str in &operators {
if let Some(pos) = text.find(op_str) {
if found_op.is_none() || op_str.len() > found_len {
found_op = Some(*op_str);
found_pos = pos;
found_len = op_str.len();
}
}
}
let op_str = found_op?;
let lhs = text[..found_pos].trim();
let rhs = text[found_pos + found_len..].trim();
let op = match op_str {
"===" | "==" => CompOp::Eq,
"!==" | "!=" => CompOp::Neq,
"<" => CompOp::Lt,
">" => CompOp::Gt,
"<=" => CompOp::Le,
">=" => CompOp::Ge,
_ => return None,
};
let lhs_op = resolve_operand(lhs, var_lookup, const_lookup);
let rhs_op = resolve_operand(rhs, var_lookup, const_lookup);
if matches!(&lhs_op, Operand::Unknown) && matches!(&rhs_op, Operand::Unknown) {
return None;
}
Some(ConditionExpr::Comparison {
lhs: lhs_op,
op,
rhs: rhs_op,
})
}
fn resolve_operand(
text: &str,
var_lookup: &HashMap<&str, SsaValue>,
const_lookup: &HashMap<SsaValue, ConstValue>,
) -> Operand {
if let Some(&ssa_val) = var_lookup.get(text) {
if let Some(cv) = const_lookup.get(&ssa_val) {
return Operand::Const(cv.clone());
}
return Operand::Value(ssa_val);
}
if let Some(cv) = ConstValue::parse_literal(text) {
return Operand::Const(cv);
}
if let Some(s) = strip_quotes(text) {
return Operand::Const(ConstValue::Str(s.to_string()));
}
Operand::Unknown
}
fn try_split_equality(text: &str) -> Option<(&str, &str, bool)> {
for (op, negated) in &[("!==", true), ("===", false), ("!=", true), ("==", false)] {
if let Some(pos) = text.find(op) {
return Some((&text[..pos], &text[pos + op.len()..], *negated));
}
}
None
}
fn extract_before<'a>(text: &'a str, marker: &str) -> Option<&'a str> {
let lower = text.to_ascii_lowercase();
let marker_lower = marker.to_ascii_lowercase();
lower.find(&marker_lower).map(|pos| &text[..pos])
}
fn strip_quotes(text: &str) -> Option<&str> {
let t = text.trim();
if t.len() >= 2 {
if (t.starts_with('"') && t.ends_with('"'))
|| (t.starts_with('\'') && t.ends_with('\''))
|| (t.starts_with('`') && t.ends_with('`'))
{
return Some(&t[1..t.len() - 1]);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cfg::{NodeInfo, StmtKind};
use crate::ssa::ir::{BlockId, SsaBlock, SsaBody, SsaValue, Terminator, ValueDef};
use petgraph::graph::NodeIndex;
use smallvec::SmallVec;
fn make_ssa_body(var_names: &[&str]) -> SsaBody {
let value_defs: Vec<ValueDef> = var_names
.iter()
.enumerate()
.map(|(i, name)| ValueDef {
var_name: Some(name.to_string()),
cfg_node: NodeIndex::new(i),
block: BlockId(0),
})
.collect();
SsaBody {
blocks: vec![SsaBlock {
id: BlockId(0),
phis: vec![],
body: vec![],
terminator: Terminator::Return(None),
preds: SmallVec::new(),
succs: SmallVec::new(),
}],
entry: BlockId(0),
value_defs,
cfg_node_map: std::collections::HashMap::new(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
}
}
fn make_cond_info(text: &str, vars: &[&str]) -> NodeInfo {
NodeInfo {
kind: StmtKind::If,
condition_text: Some(text.to_string()),
condition_vars: vars.iter().map(|v| v.to_string()).collect(),
..Default::default()
}
}
#[test]
fn lower_instanceof_string() {
let ssa = make_ssa_body(&["x"]);
let info = make_cond_info("x instanceof String", &["x"]);
let expr = lower_condition(&info, &ssa, BlockId(0), None);
match expr {
ConditionExpr::TypeCheck {
var,
type_name,
positive,
} => {
assert_eq!(var, SsaValue(0));
assert_eq!(type_name, "String");
assert!(positive);
}
other => panic!("expected TypeCheck, got {:?}", other),
}
}
#[test]
fn lower_is_a_integer() {
let ssa = make_ssa_body(&["user_id"]);
let info = make_cond_info("user_id.is_a?(Integer)", &["user_id"]);
let expr = lower_condition(&info, &ssa, BlockId(0), None);
match expr {
ConditionExpr::TypeCheck {
var,
type_name,
positive,
} => {
assert_eq!(var, SsaValue(0));
assert_eq!(type_name, "Integer");
assert!(positive);
}
other => panic!("expected TypeCheck, got {:?}", other),
}
}
#[test]
fn lower_kind_of_float() {
let ssa = make_ssa_body(&["x"]);
let info = make_cond_info("x.kind_of?(Float)", &["x"]);
let expr = lower_condition(&info, &ssa, BlockId(0), None);
match expr {
ConditionExpr::TypeCheck {
var,
type_name,
positive,
} => {
assert_eq!(var, SsaValue(0));
assert_eq!(type_name, "Float");
assert!(positive);
}
other => panic!("expected TypeCheck, got {:?}", other),
}
}
}