use crate::fx::{FxHashMap as HashMap, FxHashSet as HashSet};
use crate::cfg::Cfg;
use crate::ir::build::{BlockResult, Term};
use crate::ir::expr::{BinOp, ConstVal, Expr, UnOp};
use crate::ir::stmt::{Catch, Stmt};
use crate::structure::Region;
#[derive(Debug)]
pub enum Jump {
Break(Option<String>),
Continue(Option<String>),
ContinueVia(usize, Option<String>),
RawGoto(usize),
}
#[derive(Debug)]
struct LoopCtx {
header: usize,
exits: HashSet<usize>,
label: String,
}
#[derive(Debug)]
struct SwitchCtx {
follow: Option<usize>,
label: String,
loops_depth: usize,
}
pub struct Converter<'a> {
pub cfg: &'a Cfg,
pub results: &'a Vec<BlockResult>,
pub groups: std::borrow::Cow<'a, [crate::structure::TryGroup]>,
pub dom: std::borrow::Cow<'a, crate::structure::DomInfo>,
cur_block: usize,
loops: Vec<LoopCtx>,
switches: Vec<SwitchCtx>,
used_labels: HashSet<String>,
if_follows: Vec<usize>,
label_counter: usize,
goto_is_last: bool,
expect_next: std::cell::Cell<Option<usize>>,
prev_copy: std::cell::Cell<Option<usize>>,
copied_tails: HashSet<usize>,
pub pending_labels: HashMap<usize, String>,
final_fields: HashSet<String>,
}
fn stmt_has_throw(s: &Stmt) -> bool {
match s {
Stmt::Throw(_) => true,
Stmt::Block(v) => v.iter().any(stmt_has_throw),
_ => false,
}
}
fn strip_trailing_goto(s: &mut Stmt, follow: Option<usize>) {
let Some(f) = follow else { return };
let items = match s {
Stmt::Block(v) => v,
Stmt::Goto(t) if *t as usize == f => {
*s = Stmt::Block(vec![]);
return;
}
_ => return,
};
if let Some(last) = items.last_mut() {
match last {
Stmt::Goto(t) if *t as usize == f => {
items.pop();
}
Stmt::Block(inner) if !inner.is_empty() => {
let n = inner.len();
if matches!(inner[n - 1], Stmt::Goto(t) if t as usize == f) {
inner.pop();
}
}
_ => {}
}
}
}
impl<'a> Converter<'a> {
pub fn with_copied_tails(mut self, tails: HashSet<usize>) -> Self {
self.copied_tails = tails;
self
}
pub fn with_final_fields(mut self, finals: HashSet<String>) -> Self {
self.final_fields = finals;
self
}
fn stmts_write_final(&self, v: &[Stmt]) -> bool {
v.iter().any(|s| match s {
Stmt::ExprStmt(Expr::Assign { target, .. }) => {
matches!(&**target, Expr::Field { name, .. } if self.final_fields.contains(name.as_ref()))
}
_ => false,
})
}
fn tail_chain_writes_final(&self, t: usize) -> bool {
if self.final_fields.is_empty() {
return false;
}
let mut x = t;
let mut seen: HashSet<usize> = HashSet::default();
for _ in 0..8 {
if !seen.insert(x) {
return false;
}
if self.stmts_write_final(&self.results[x].stmts) {
return true;
}
if !matches!(self.results[x].term, Term::Fallthrough | Term::Goto) {
return false;
}
let succs = &self.cfg.blocks[x].succ;
if succs.len() != 1 {
return false;
}
x = succs[0];
}
false
}
pub fn new(cfg: &'a Cfg, results: &'a Vec<BlockResult>) -> Self {
let universe: HashSet<usize> = (0..cfg.blocks.len()).collect();
let dom = crate::structure::compute_dominators(cfg, &universe, cfg.entry);
let groups = crate::structure::group_exceptions_with(cfg, Some(results));
Self::from_parts(
cfg,
results,
std::borrow::Cow::Owned(groups),
std::borrow::Cow::Owned(dom),
)
}
pub fn with_precomputed(
cfg: &'a Cfg,
results: &'a Vec<BlockResult>,
groups: Vec<crate::structure::TryGroup>,
dom: crate::structure::DomInfo,
) -> Self {
Self::from_parts(
cfg,
results,
std::borrow::Cow::Owned(groups),
std::borrow::Cow::Owned(dom),
)
}
pub fn with_precomputed_ref(
cfg: &'a Cfg,
results: &'a Vec<BlockResult>,
groups: &'a [crate::structure::TryGroup],
dom: &'a crate::structure::DomInfo,
) -> Self {
Self::from_parts(
cfg,
results,
std::borrow::Cow::Borrowed(groups),
std::borrow::Cow::Borrowed(dom),
)
}
fn from_parts(
cfg: &'a Cfg,
results: &'a Vec<BlockResult>,
groups: std::borrow::Cow<'a, [crate::structure::TryGroup]>,
dom: std::borrow::Cow<'a, crate::structure::DomInfo>,
) -> Self {
Converter {
cfg,
results,
groups,
dom,
cur_block: usize::MAX,
loops: Vec::new(),
switches: Vec::new(),
if_follows: Vec::new(),
used_labels: HashSet::default(),
label_counter: 0,
goto_is_last: false,
expect_next: std::cell::Cell::new(None),
prev_copy: std::cell::Cell::new(None),
copied_tails: HashSet::default(),
pending_labels: HashMap::default(),
final_fields: HashSet::default(),
}
}
fn next_label(&mut self) -> String {
self.label_counter += 1;
format!("L{}", self.label_counter)
}
fn groups_end(&self, gi: usize) -> Option<u32> {
self.groups.get(gi).map(|g| g.end)
}
pub fn convert(&mut self, r: Region) -> Stmt {
let mut st = self.conv(r);
prune_unused_labels(&mut st, &self.used_labels);
st
}
fn conv(&mut self, r: Region) -> Stmt {
match r {
Region::Empty => Stmt::Block(vec![]),
Region::Basic { block } => {
self.cur_block = block;
if crate::dbg_flag!("JCDC_DBG_BLOCKS") {
eprintln!(
"conv Basic {} ({} stmts)",
block,
self.results[block].stmts.len()
);
}
self.block_stmts(block)
}
Region::CopyStmts { block } => {
let mut v = self.results[block].stmts.clone();
match &self.results[block].term {
Term::Return(e) => v.push(Stmt::Return(e.clone())),
Term::Throw(e) => v.push(Stmt::Throw(e.clone())),
_ => {}
}
if v.len() == 1 {
v.into_iter().next().unwrap()
} else {
Stmt::Block(v)
}
}
Region::Seq(v) => {
let mut out = Vec::new();
let n = v.len();
let heads: Vec<usize> = v.iter().map(crate::structure::region_head_block).collect();
let prev_copies: Vec<Option<usize>> = (0..n)
.map(|k| {
if k > 0 {
match &v[k - 1] {
Region::CopyStmts { block } => Some(*block),
_ => None,
}
} else {
None
}
})
.collect();
for (k, x) in v.into_iter().enumerate() {
let save = self.goto_is_last;
self.goto_is_last = k + 1 == n;
let save_pc = self.prev_copy.get();
self.prev_copy.set(prev_copies[k]);
let save_exp = self.expect_next.get();
if k + 1 < n && heads[k + 1] != usize::MAX {
self.expect_next.set(Some(heads[k + 1]));
}
out.push(self.conv(x));
self.expect_next.set(save_exp);
self.prev_copy.set(save_pc);
self.goto_is_last = save;
}
Stmt::Block(out)
}
Region::If {
block,
cond,
then_r,
else_r,
follow,
ternary,
} => {
self.cur_block = block;
let mut head = self.block_stmts_no_term(block);
if let Some((_tv, _fv)) = ternary {
let _ = cond;
} else {
if let Some(f) = follow {
self.if_follows.push(f);
}
let save_exp = self.expect_next.get();
self.expect_next.set(follow.or(save_exp));
let then_stmt = self.conv(*then_r);
let else_stmt = self.conv(*else_r);
self.expect_next.set(save_exp);
if follow.is_some() {
self.if_follows.pop();
}
let if_stmt = Stmt::If {
cond,
then_stmt: Box::new(then_stmt),
else_stmt: if else_stmt.is_empty_block() {
None
} else {
Some(Box::new(else_stmt))
},
};
head.push(if_stmt);
}
if head.len() == 1 {
head.pop().unwrap()
} else {
Stmt::Block(head)
}
}
Region::Loop {
header,
body,
members,
exits,
} => {
let label = self.next_label();
self.loops.push(LoopCtx {
header,
exits: exits.iter().copied().collect(),
label: label.clone(),
});
let body_stmt = self.conv(*body);
let ctx = self.loops.pop().unwrap();
let _ = members;
let mut st = self.classify_loop(header, body_stmt, ctx);
if crate::dbg_flag!("JCDC_DBG_LOOP") {
let kind = match &st {
Stmt::While { .. } => "While".to_string(),
Stmt::DoWhile { .. } => "DoWhile".to_string(),
Stmt::For { .. } => "For".to_string(),
Stmt::Block(b) => format!(
"Block{}[{}]",
b.len(),
b.iter()
.map(|x| match x {
Stmt::DoWhile { .. } => "DoWhile",
Stmt::While { .. } => "While",
Stmt::Break(_) => "Break",
Stmt::Return(_) => "Return",
_ => "_",
})
.collect::<Vec<_>>()
.join(",")
),
Stmt::Labeled { .. } => "Labeled".to_string(),
_ => "Other".to_string(),
};
eprintln!("CLASSIFY-OUT header={} -> {}", header, kind);
}
st = Stmt::Labeled {
label,
body: Box::new(st),
};
st
}
Region::Switch {
block,
selector,
cases,
default,
follow,
} => {
self.cur_block = block;
let sw_label = self.next_label();
self.switches.push(SwitchCtx {
follow,
label: sw_label.clone(),
loops_depth: self.loops.len(),
});
let mut case_groups = Vec::new();
for (vals, r) in cases {
let s = self.conv(r);
case_groups.push(crate::ir::stmt::CaseGroup {
labels: vals,
string_labels: vec![],
enum_labels: vec![],
raw_labels: vec![],
guard: None,
body: stmt_to_vec(s),
});
}
let default_stmt = default.map(|d| Box::new(self.conv(*d)));
self.switches.pop();
let mut head = self.block_stmts_no_term(block);
let mut sw = Stmt::Switch {
selector,
cases: case_groups,
default: default_stmt,
on_string: false,
};
{
sw = Stmt::Labeled {
label: sw_label,
body: Box::new(sw),
};
}
head.push(sw);
if head.len() == 1 {
head.pop().unwrap()
} else {
Stmt::Block(head)
}
}
Region::Try {
group_idx,
body,
catches,
} => {
let mut body_stmt = self.conv(*body);
let gend = self.groups_end(group_idx);
let try_follow = gend.and_then(|e| self.cfg.block_at(e));
strip_trailing_goto(&mut body_stmt, try_follow);
let mut catch_stmts = Vec::new();
for (tys, _hblock, r) in catches {
if crate::dbg_flag!("JCDC_DBG_CATCH") {
let succs = &self.cfg.blocks[_hblock].succ;
let succ_terms: Vec<_> = succs
.iter()
.map(|&s| (s, self.results[s].term.clone()))
.collect();
eprintln!(
"[catch] gi={} hb={} term={:?} succ={:?} tys={:?} region={:#?}",
group_idx, _hblock, self.results[_hblock].term, succ_terms, tys, r
);
}
let mut s = self.conv(*r);
if crate::dbg_flag!("JCDC_DBG_CATCH") {
eprintln!("[catch] conv'd has_throw={}", stmt_has_throw(&s));
}
strip_trailing_goto(&mut s, try_follow);
catch_stmts.push(Catch {
exc: tys,
var: u32::MAX, var_name: None,
body: Box::new(s),
});
}
Stmt::Try {
body: Box::new(body_stmt),
catches: catch_stmts,
finally: None,
}
}
Region::Goto { target } => {
if crate::dbg_flag!("JCDC_DBG_GOTO") {
eprintln!(
"conv Goto target={} cur_block={} loops={:?} switches={} if_follows={:?}",
target,
self.cur_block,
self.loops
.iter()
.map(|l| (l.header, l.exits.len()))
.collect::<Vec<_>>(),
self.switches.len(),
self.if_follows
);
}
let inline_terminator = self.if_follows.contains(&target)
&& matches!(self.results[target].term, Term::Return(_) | Term::Throw(_))
&& !self.tail_chain_writes_final(target);
if inline_terminator {
let mut v: Vec<Stmt> = Vec::new();
let mut t = target;
let mut guard = 0;
loop {
v.extend(self.results[t].stmts.clone());
match &self.results[t].term {
Term::Return(e) => {
v.push(Stmt::Return(e.clone()));
break;
}
Term::Throw(e) => {
v.push(Stmt::Throw(e.clone()));
break;
}
Term::Fallthrough | Term::Goto
if self.cfg.blocks[t].succ.len() == 1 && guard < 8 =>
{
t = self.cfg.blocks[t].succ[0];
guard += 1;
}
_ => break,
}
}
return if v.len() == 1 {
v.into_iter().next().unwrap()
} else {
Stmt::Block(v)
};
}
if let Some(j) = self.resolve_goto(target) {
match j {
Jump::Break(lbl) => {
if let Some(l) = &lbl {
self.used_labels.insert(l.clone());
}
Stmt::Break(lbl)
}
Jump::Continue(lbl) => {
if let Some(l) = &lbl {
self.used_labels.insert(l.clone());
}
Stmt::Continue(lbl)
}
Jump::ContinueVia(t, lbl) => {
if let Some(l) = &lbl {
self.used_labels.insert(l.clone());
}
let mut cv = if self.prev_copy.get() == Some(t) {
Vec::new()
} else {
self.results[t].stmts.clone()
};
cv.push(Stmt::Continue(lbl));
if cv.len() == 1 {
cv.into_iter().next().unwrap()
} else {
Stmt::Block(cv)
}
}
Jump::RawGoto(t) => {
if crate::dbg_flag!("JCDC_DBG_GOTO") {
eprintln!(
"RAWGOTO t={} copied_tails={:?} if_follows={:?} last={}",
t, self.copied_tails, self.if_follows, self.goto_is_last
);
}
let term_copy =
matches!(self.results[t].term, Term::Return(_) | Term::Throw(_))
&& !self.tail_chain_writes_final(t)
&& !self.is_retry_loop_header(t);
if term_copy {
let mut cv = self.results[t].stmts.clone();
match &self.results[t].term {
Term::Return(e) => cv.push(Stmt::Return(e.clone())),
Term::Throw(e) => cv.push(Stmt::Throw(e.clone())),
_ => {}
}
if cv.len() == 1 {
cv.into_iter().next().unwrap()
} else {
Stmt::Block(cv)
}
} else if self
.loops
.iter()
.any(|l| l.exits.contains(&t) || l.header == t)
&& (self.goto_is_last || self.if_follows.contains(&t))
{
Stmt::Break(None)
} else if !matches!(
self.results[t].term,
Term::Return(_) | Term::Throw(_)
) && !self.results[t].stmts.is_empty()
&& !self.copied_tails.contains(&t)
&& self.prev_copy.get() != Some(t)
&& self
.expect_next
.get()
.map(|nx| {
let mut x = t;
for _ in 0..8 {
if x == nx {
return x != t;
}
if self.copied_tails.contains(&x) {
return false;
}
match self.cfg.blocks[x].succ.first() {
Some(&n) => x = n,
None => return false,
}
}
false
})
.unwrap_or(false)
&& (self
.if_follows
.iter()
.any(|f| self.cfg.blocks[t].succ.contains(f)))
{
let cv = self.results[t].stmts.clone();
if cv.len() == 1 {
cv.into_iter().next().unwrap()
} else {
Stmt::Block(cv)
}
} else if self.goto_is_last
|| self.expect_next.get() == Some(t)
|| self.reaches_copy_tail(t)
{
Stmt::Block(vec![])
} else if !matches!(
self.results[t].term,
Term::Return(_) | Term::Throw(_)
) && self.prev_copy.get() != Some(t)
&& (self
.if_follows
.iter()
.any(|f| self.cfg.blocks[t].succ.contains(f)))
{
let cv = self.results[t].stmts.clone();
if cv.len() == 1 {
cv.into_iter().next().unwrap()
} else {
Stmt::Block(cv)
}
} else {
Stmt::Goto(t as u32)
}
}
}
} else {
Stmt::Block(vec![])
}
}
}
}
fn block_stmts(&mut self, block: usize) -> Stmt {
let mut v = self.block_stmts_no_term(block);
match &self.results[block].term {
Term::Return(e) => v.push(Stmt::Return(e.clone())),
Term::Throw(e) => v.push(Stmt::Throw(e.clone())),
Term::Jsr | Term::Ret => v.push(Stmt::Comment("// jsr/ret not supported".into())),
_ => {}
}
if v.len() == 1 {
v.pop().unwrap()
} else {
Stmt::Block(v)
}
}
fn block_stmts_no_term(&self, block: usize) -> Vec<Stmt> {
self.results[block].stmts.clone()
}
fn resolve_goto(&mut self, target: usize) -> Option<Jump> {
if let Some(i) = (0..self.loops.len())
.rev()
.find(|&i| self.loops[i].header == target)
{
let depth = self.loops.len() - 1 - i;
return Some(if depth == 0 {
Jump::Continue(None)
} else {
Jump::Continue(Some(self.loops[i].label.clone()))
});
}
if let Some(li) = (0..self.loops.len()).find(|&i| self.loops[i].exits.contains(&target)) {
if self.switches.is_empty() {
let mut x = target;
let mut guard = 0;
let header_hit = loop {
if let Some(hi) = self.loops.iter().position(|l| l.header == x) {
if hi <= li {
break Some(hi);
}
}
if guard >= 8 {
break None;
}
let r = &self.results[x];
let stub = r.stmts.is_empty()
&& r.out_stack.is_empty()
&& matches!(r.term, Term::Fallthrough | Term::Goto)
&& self.cfg.blocks[x].succ.len() == 1;
if !stub {
break None;
}
x = self.cfg.blocks[x].succ[0];
guard += 1;
};
if let Some(hi) = header_hit {
let depth = self.loops.len() - 1 - hi;
return Some(if depth == 0 {
Jump::Continue(None)
} else {
Jump::Continue(Some(self.loops[hi].label.clone()))
});
}
}
let crosses_switch = !self.switches.is_empty();
return Some(if li + 1 == self.loops.len() && !crosses_switch {
Jump::Break(None)
} else {
Jump::Break(Some(self.loops[li].label.clone()))
});
}
if let Some(si) = (0..self.switches.len())
.rev()
.find(|&i| self.switches[i].follow == Some(target))
{
return Some(
if si + 1 == self.switches.len()
&& self.loops.len() == self.switches[si].loops_depth
{
Jump::Break(None)
} else {
Jump::Break(Some(self.switches[si].label.clone()))
},
);
}
if self.if_follows.last() == Some(&target) {
return None;
}
let is_any_exit = self.loops.iter().any(|l| l.exits.contains(&target));
if !is_any_exit && self.switches.is_empty() {
let mut via_label: Option<Option<String>> = None;
if self.results[target].stmts.is_empty() {
if let Some(i) = (0..self.loops.len()).rev().find(|&i| {
crate::structure::can_reach_cfg(self.cfg, target, self.loops[i].header, 4096)
}) {
let depth = self.loops.len() - 1 - i;
via_label = Some(if depth == 0 {
None
} else {
Some(self.loops[i].label.clone())
});
}
} else {
let mut x = target;
for _ in 0..8 {
if let Some(hi) = self.loops.iter().position(|l| l.header == x) {
let depth = self.loops.len() - 1 - hi;
via_label = Some(if depth == 0 {
None
} else {
Some(self.loops[hi].label.clone())
});
break;
}
if !self.results[x].stmts.is_empty() && x != target {
break;
}
if self.cfg.blocks[x].succ.len() != 1 {
break;
}
x = self.cfg.blocks[x].succ[0];
}
}
if let Some(lbl) = via_label {
return Some(if self.results[target].stmts.is_empty() {
Jump::Continue(lbl)
} else {
Jump::ContinueVia(target, lbl)
});
}
}
if !self.loops.is_empty()
&& !crate::structure::can_reach_cfg(
self.cfg,
target,
self.loops[self.loops.len() - 1].header,
4096,
)
{
return Some(Jump::Break(None));
}
Some(Jump::RawGoto(target))
}
fn is_retry_loop_header(&self, t: usize) -> bool {
self.cfg.exc_edges.iter().any(|e| {
e.from == t
&& e.to != t
&& self.cfg.blocks[e.to].pred.is_empty()
&& self.cfg.blocks[e.to].succ.len() == 1
&& self.cfg.blocks[e.to].succ[0] == t
})
}
fn reaches_copy_tail(&self, t: usize) -> bool {
let mut x = t;
for _ in 0..8 {
if self.copied_tails.contains(&x) || self.if_follows.contains(&x) {
return true;
}
if !self.results[x].stmts.is_empty() {
return false;
}
match &self.results[x].term {
Term::Fallthrough | Term::Goto => match self.cfg.blocks[x].succ.first() {
Some(&n) => x = n,
None => return false,
},
_ => return false,
}
}
false
}
#[allow(dead_code)]
fn postdominates(&self, a: usize, b: usize, universe: &HashSet<usize>) -> bool {
let mut seen = HashSet::default();
let mut q = std::collections::VecDeque::new();
q.push_back(b);
seen.insert(b);
while let Some(x) = q.pop_front() {
for &s in &self.cfg.blocks[x].succ {
if s == a {
continue;
}
if !universe.contains(&s) {
continue;
}
if seen.insert(s) {
q.push_back(s);
}
}
if self.cfg.blocks[x].succ.is_empty() && x != a && x != b {
return false;
}
}
true
}
fn classify_loop(&mut self, header: usize, body_stmt: Stmt, ctx: LoopCtx) -> Stmt {
let out = self.classify_loop_impl(header, body_stmt, ctx);
if crate::dbg_flag!("JCDC_DBG_LOOP") {
fn shape(s: &Stmt) -> String {
fn short(e: &Expr) -> String {
format!("{:?}", e).chars().take(90).collect()
}
match s {
Stmt::While { cond, body } => {
format!("While({}) body={}", short(cond), shape(body))
}
Stmt::DoWhile { cond, .. } => format!("DoWhile({})", short(cond)),
Stmt::For { cond, .. } => {
format!("For({})", cond.as_ref().map(short).unwrap_or_default())
}
Stmt::Block(v) => format!(
"Block[{}]",
v.iter()
.take(4)
.map(|x| match x {
Stmt::ExprStmt(_) => "expr".to_string(),
Stmt::LocalDef { .. } => "def".to_string(),
Stmt::If { cond, .. } => format!("if({})", short(cond)),
Stmt::While { cond, .. } => format!("while({})", short(cond)),
Stmt::DoWhile { .. } => "do".to_string(),
Stmt::Break(_) => "break".to_string(),
Stmt::Continue(l) => format!("cont{:?}", l),
Stmt::Return(_) => "ret".to_string(),
Stmt::Throw(_) => "throw".to_string(),
Stmt::Try { .. } => "try".to_string(),
_ => "…".to_string(),
})
.collect::<Vec<_>>()
.join(",")
),
_ => "…".to_string(),
}
}
eprintln!("CLASSIFY-OUT header={} {}", header, shape(&out));
}
out
}
fn classify_loop_impl(&mut self, header: usize, body_stmt: Stmt, ctx: LoopCtx) -> Stmt {
let term = self.results[header].term.clone();
let succs = self.cfg.blocks[header].succ.clone();
match term {
Term::Cond { cond } if succs.len() == 2 => {
let taken = succs[1];
let fall = succs[0];
if taken == header && ctx.exits.contains(&fall) {
if let Some((stmts, c, exit)) = extract_compound_do_while(&body_stmt) {
if matches!(exit, Stmt::Break(None)) {
return Stmt::DoWhile {
body: Box::new(Stmt::Block(stmts)),
cond: c,
};
}
return Stmt::Block(vec![
Stmt::DoWhile {
body: Box::new(Stmt::Block(stmts)),
cond: c,
},
exit,
]);
}
let (stmts, c) = match extract_trailing_do_while(&body_stmt) {
Some(x) => x,
None => (stmt_to_vec(body_stmt), cond.clone()),
};
return Stmt::DoWhile {
body: Box::new(Stmt::Block(stmts)),
cond: c,
};
}
if fall == header && ctx.exits.contains(&taken) {
let inner2 = strip_trailing_continue(body_stmt);
if cond_var_reassigned_first(&inner2, &cond) {
match try_protected_dowhile(inner2, &cond) {
Ok(dw) => return dw,
Err(inner) => {
return Stmt::While {
cond: negate(cond.clone()),
body: Box::new(inner),
};
}
}
}
return Stmt::While {
cond: negate(cond.clone()),
body: Box::new(inner2),
};
}
let taken_is_exit = ctx.exits.contains(&taken);
if crate::dbg_flag!("JCDC_DBG_LOOP") {
eprintln!(
"CLASSIFY2 header={} taken={} fall={} exits={:?}",
header, taken, fall, ctx.exits
);
}
if !taken_is_exit && !ctx.exits.contains(&fall) {
if self.results[fall].stmts.is_empty() {
if let Term::Cond { cond: cb } = &self.results[fall].term {
let bs = self.cfg.blocks[fall].succ.clone();
if bs.len() == 2 && ctx.exits.contains(&bs[1]) && bs[0] == taken {
let combined = Expr::Bin {
op: BinOp::LogOr,
l: Box::new(cond.clone()),
r: Box::new(negate(cb.clone())),
ty: None,
};
let mut parts = stmt_to_vec(body_stmt);
if matches!(parts.first(), Some(Stmt::If { .. })) {
parts.remove(0);
}
let body = if parts.is_empty() {
Stmt::Block(self.results[taken].stmts.clone())
} else {
let rest = if parts.len() == 1 {
parts.pop().unwrap()
} else {
Stmt::Block(parts)
};
strip_trailing_continue(rest)
};
if crate::dbg_flag!("JCDC_DBG_LOOP") {
eprintln!(
"CLASSIFY-CONDCHAIN header={} B={} body={} exits={:?}",
header, fall, taken, ctx.exits
);
}
return Stmt::While {
cond: combined,
body: Box::new(body),
};
}
}
}
return Stmt::While {
cond: Expr::Const(ConstVal::Int(1)),
body: Box::new(body_stmt),
};
}
let (inner, exit_stmts, stripped) = split_leading_if(&body_stmt, &cond);
if crate::dbg_flag!("JCDC_DBG_LOOP") {
eprintln!(
"CLASSIFY header={} taken_is_exit={} exit_stmts={}",
header,
taken_is_exit,
exit_stmts.len()
);
}
let inner = strip_trailing_continue(inner);
let while_cond = if taken_is_exit {
negate(cond.clone())
} else {
cond.clone()
};
let plain_exit = exit_stmts.is_empty()
|| matches!(exit_stmts.as_slice(), [Stmt::Break(_)])
|| matches!(exit_stmts.as_slice(), [Stmt::Block(b)] if b.is_empty());
if taken_is_exit && plain_exit {
match try_protected_dowhile(inner, &cond) {
Ok(dw) => dw,
Err(inner) => {
if crate::dbg_flag!("JCDC_DBG_LOOP") {
eprintln!(
"STALEDBG arm=taken_plain header={} stripped={} leading={} cond={:?}",
header,
stripped,
leading_test_present(&inner, &cond, true),
cond
);
}
let hc = if !stripped && leading_test_present(&inner, &cond, true) {
Expr::Const(ConstVal::Int(1))
} else {
while_cond
};
Stmt::While {
cond: hc,
body: Box::new(inner),
}
}
}
} else if taken_is_exit {
let mut ex = exit_stmts;
let already_term = matches!(
ex.last(),
Some(Stmt::Return(_)) | Some(Stmt::Throw(_)) | Some(Stmt::Break(_))
);
if !already_term {
ex.push(Stmt::Break(None));
}
let guard = Stmt::If {
cond: cond.clone(),
then_stmt: Box::new(Stmt::Block(ex)),
else_stmt: None,
};
let mut v = vec![guard];
v.extend(stmt_to_vec(inner));
Stmt::While {
cond: Expr::Const(ConstVal::Int(1)),
body: Box::new(Stmt::Block(v)),
}
} else {
if exit_stmts.is_empty()
|| matches!(exit_stmts.as_slice(), [Stmt::Break(_)])
|| matches!(exit_stmts.as_slice(), [Stmt::Block(b)] if b.is_empty())
{
match try_protected_dowhile(inner, &cond) {
Ok(dw) => dw,
Err(inner) => {
let hc = if !stripped && leading_test_present(&inner, &cond, false) {
Expr::Const(ConstVal::Int(1))
} else {
while_cond
};
Stmt::While {
cond: hc,
body: Box::new(inner),
}
}
}
} else {
let body2 = strip_trailing_continue(stmts_to_stmt(exit_stmts));
Stmt::While {
cond: while_cond,
body: Box::new(body2),
}
}
}
}
Term::Goto | Term::Fallthrough => {
if let Some((stmts, c, exit)) = extract_compound_do_while(&body_stmt) {
if matches!(exit, Stmt::Break(None)) {
return Stmt::DoWhile {
body: Box::new(Stmt::Block(stmts)),
cond: c,
};
}
return Stmt::Block(vec![
Stmt::DoWhile {
body: Box::new(Stmt::Block(stmts)),
cond: c,
},
exit,
]);
}
if let Some((stmts, c)) = extract_trailing_do_while(&body_stmt) {
return Stmt::DoWhile {
body: Box::new(Stmt::Block(stmts)),
cond: c,
};
}
Stmt::While {
cond: Expr::Const(ConstVal::Int(1)),
body: Box::new(body_stmt),
}
}
_ => Stmt::While {
cond: Expr::Const(ConstVal::Int(1)),
body: Box::new(body_stmt),
},
}
}
#[allow(dead_code)]
fn loop_contains(&self, header: usize, target: usize) -> bool {
can_reach(self.cfg, target, header, 8192)
}
#[allow(dead_code)]
fn loop_exit_contains(&self, header: usize, target: usize) -> bool {
can_reach(self.cfg, target, header, 4096).not()
}
}
#[allow(dead_code)]
trait NotExt {
fn not(self) -> bool;
}
impl NotExt for bool {
fn not(self) -> bool {
!self
}
}
#[allow(dead_code)]
fn can_reach(cfg: &Cfg, from: usize, to: usize, budget: usize) -> bool {
if from == to {
return true;
}
let mut seen = HashSet::default();
let mut q = std::collections::VecDeque::new();
q.push_back(from);
seen.insert(from);
let mut n = 0;
while let Some(b) = q.pop_front() {
n += 1;
if n > budget {
return false;
}
for &s in &cfg.blocks[b].succ {
if s == to {
return true;
}
if seen.insert(s) {
q.push_back(s);
}
}
}
false
}
fn stmts_to_stmt(v: Vec<Stmt>) -> Stmt {
match v.len() {
0 => Stmt::Block(vec![]),
1 => v.into_iter().next().unwrap(),
_ => Stmt::Block(v),
}
}
#[allow(clippy::result_large_err)]
fn try_protected_dowhile(inner: Stmt, cond: &Expr) -> Result<Stmt, Stmt> {
fn is_empty(s: &Stmt) -> bool {
match s {
Stmt::Block(v) => v.is_empty() || (v.len() == 1 && is_empty(&v[0])),
Stmt::Break(None) => false,
_ => false,
}
}
fn inject(s: &mut Stmt, cond: &Expr, ncond: &Expr) -> bool {
match s {
Stmt::If {
cond: c,
then_stmt,
else_stmt,
} if same_test(c, cond) => {
let then_empty = is_empty(then_stmt);
let else_empty = else_stmt.as_ref().map(|e| is_empty(e)).unwrap_or(true);
if then_empty && !else_empty {
**then_stmt = Stmt::Break(None);
true
} else if else_empty && !then_empty {
*else_stmt = Some(Box::new(Stmt::Break(None)));
true
} else {
false
}
}
Stmt::If {
cond: c,
then_stmt,
else_stmt,
} if same_test(c, ncond) => {
let else_empty = else_stmt.as_ref().map(|e| is_empty(e)).unwrap_or(true);
if else_empty && !is_empty(then_stmt) {
*else_stmt = Some(Box::new(Stmt::Break(None)));
true
} else {
false
}
}
Stmt::Try { body, .. } | Stmt::TryWithResources { body, .. } => {
inject(body, cond, ncond)
}
Stmt::Block(v) => {
for st in v.iter_mut() {
match st {
Stmt::ExprStmt(_)
| Stmt::LocalDef { .. }
| Stmt::Comment(_)
| Stmt::MonitorEnter(_)
| Stmt::MonitorExit(_) => continue,
other => return inject(other, cond, ncond),
}
}
false
}
Stmt::Labeled { body, .. } | Stmt::Synchronized { body, .. } => {
inject(body, cond, ncond)
}
_ => false,
}
}
let mut b = inner;
let ncond = negate(cond.clone());
let r = inject(&mut b, cond, &ncond);
if crate::dbg_flag!("JCDC_DBG_LOOP") {
eprintln!(
"TPD inject={} cond={} body0={}",
r,
format!("{:?}", cond).chars().take(70).collect::<String>(),
match &b {
Stmt::Block(v) => v
.iter()
.take(3)
.map(|x| match x {
Stmt::If { cond: c, then_stmt, else_stmt } => format!(
"if({} )then_empty={} else={}",
format!("{:?}", c).chars().take(50).collect::<String>(),
matches!(&**then_stmt, Stmt::Block(bb) if bb.is_empty()),
else_stmt.is_some()
),
Stmt::LocalDef { .. } => "def".to_string(),
Stmt::ExprStmt(_) => "expr".to_string(),
_ => "other".to_string(),
})
.collect::<Vec<_>>()
.join("|"),
Stmt::If { cond: c, .. } => format!("topif({:?})", c).chars().take(60).collect(),
_ => "nonblock".to_string(),
}
);
}
if r {
Ok(Stmt::DoWhile {
body: Box::new(b),
cond: Expr::Const(ConstVal::Int(1)),
})
} else {
Err(b)
}
}
fn split_leading_if(body: &Stmt, cond: &Expr) -> (Stmt, Vec<Stmt>, bool) {
let items = match body {
Stmt::Block(v) => v,
other => {
return match other {
Stmt::If {
cond: c,
then_stmt,
else_stmt,
} if c == cond => (
else_stmt
.as_ref()
.map(|e| (**e).clone())
.unwrap_or(Stmt::Block(vec![])),
stmt_to_vec((**then_stmt).clone()),
true,
),
_ => (other.clone(), vec![], false),
};
}
};
if let Some(Stmt::If {
cond: c,
then_stmt,
else_stmt,
}) = items.first()
{
{
if c == cond {
let inner = if items.len() == 1 {
else_stmt
.as_ref()
.map(|e| (**e).clone())
.unwrap_or(Stmt::Block(vec![]))
} else {
let mut rest = vec![else_stmt
.as_ref()
.map(|e| (**e).clone())
.unwrap_or(Stmt::Block(vec![]))];
rest.extend(items[1..].iter().cloned());
Stmt::Block(rest)
};
return (inner, stmt_to_vec((**then_stmt).clone()), true);
}
}
}
(body.clone(), vec![], false)
}
fn canon_test(e: &Expr) -> Expr {
fn is_zero(c: &Expr) -> bool {
matches!(c, Expr::Const(ConstVal::Int(0)))
}
match e {
Expr::Bin {
op: BinOp::Eq,
l,
r,
..
} => {
if is_zero(r) {
Expr::Un {
op: UnOp::Not,
e: Box::new(canon_test(l)),
}
} else if is_zero(l) {
Expr::Un {
op: UnOp::Not,
e: Box::new(canon_test(r)),
}
} else {
e.clone()
}
}
Expr::Bin {
op: BinOp::Ne,
l,
r,
..
} => {
if is_zero(r) {
canon_test(l)
} else if is_zero(l) {
canon_test(r)
} else {
e.clone()
}
}
Expr::Un {
op: UnOp::Not,
e: inner,
} => match canon_test(inner) {
Expr::Un {
op: UnOp::Not,
e: x,
} => *x,
other => Expr::Un {
op: UnOp::Not,
e: Box::new(other),
},
},
_ => e.clone(),
}
}
fn same_test(a: &Expr, b: &Expr) -> bool {
if a == b {
return true;
}
canon_test(a) == canon_test(b)
}
fn leading_test_present(s: &Stmt, cond: &Expr, exit_on_taken: bool) -> bool {
fn terminates(t: &Stmt) -> bool {
match t {
Stmt::Break(_) | Stmt::Return(_) | Stmt::Throw(_) => true,
Stmt::Continue(Some(_)) => true,
Stmt::Block(v) => v.last().map(terminates).unwrap_or(false),
Stmt::If {
then_stmt,
else_stmt,
..
} => {
terminates(then_stmt)
&& else_stmt
.as_ref()
.map(|e| terminates(e))
.unwrap_or(false)
}
Stmt::Labeled { body, .. } => terminates(body),
_ => false,
}
}
fn term_opt(o: &Option<Box<Stmt>>) -> bool {
o.as_ref().map(|e| terminates(e)).unwrap_or(false)
}
fn go(s: &Stmt, cond: &Expr, ncond: &Expr, exit_on_taken: bool) -> bool {
match s {
Stmt::If {
cond: c,
then_stmt,
else_stmt,
} => {
let direct = same_test(c, cond);
let flipped = !direct && same_test(c, ncond);
if !direct && !flipped {
return false;
}
let exit_is_then = direct == exit_on_taken;
if exit_is_then {
terminates(then_stmt)
} else {
term_opt(else_stmt)
}
}
Stmt::Try { body, .. }
| Stmt::TryWithResources { body, .. }
| Stmt::Labeled { body, .. }
| Stmt::Synchronized { body, .. } => go(body, cond, ncond, exit_on_taken),
Stmt::Block(v) => {
for st in v {
match st {
Stmt::ExprStmt(_)
| Stmt::LocalDef { .. }
| Stmt::Comment(_)
| Stmt::MonitorEnter(_)
| Stmt::MonitorExit(_) => continue,
other => return go(other, cond, ncond, exit_on_taken),
}
}
false
}
_ => false,
}
}
go(s, cond, &negate(cond.clone()), exit_on_taken)
}
fn cond_var_reassigned_first(s: &Stmt, cond: &Expr) -> bool {
fn cond_local(c: &Expr) -> Option<u32> {
match c {
Expr::Local { var, .. } => Some(*var),
Expr::Un {
op: UnOp::Not,
e,
} => cond_local(e),
Expr::Bin {
op: BinOp::Eq | BinOp::Ne,
l,
r,
..
} => {
if matches!(&**r, Expr::Const(ConstVal::Int(0))) {
cond_local(l)
} else if matches!(&**l, Expr::Const(ConstVal::Int(0))) {
cond_local(r)
} else {
None
}
}
_ => None,
}
}
let Some(v) = cond_local(cond) else {
return false;
};
fn go(s: &Stmt, v: u32) -> bool {
match s {
Stmt::ExprStmt(Expr::Assign { target, .. }) => {
matches!(&**target, Expr::Local { var, .. } if *var == v)
}
Stmt::LocalDef {
var, init: Some(_), ..
} => *var == v,
Stmt::Block(b) => b.first().map(|f| go(f, v)).unwrap_or(false),
Stmt::Try { body, .. }
| Stmt::TryWithResources { body, .. }
| Stmt::Labeled { body, .. }
| Stmt::Synchronized { body, .. } => go(body, v),
_ => false,
}
}
go(s, v)
}
fn strip_trailing_continue(s: Stmt) -> Stmt {
match s {
Stmt::Block(mut v) => {
while matches!(v.last(), Some(Stmt::Continue(None))) {
v.pop();
}
if v.len() == 1 && matches!(v[0], Stmt::Block(_)) {
let inner = match v.pop().unwrap() {
Stmt::Block(inner) => inner,
_ => unreachable!(),
};
return strip_trailing_continue(Stmt::Block(inner));
}
Stmt::Block(v)
}
Stmt::Continue(None) => Stmt::Block(vec![]),
other => other,
}
}
pub fn negate(e: Expr) -> Expr {
match e {
Expr::Un { op: UnOp::Not, e } => *e,
Expr::Bin {
op: op @ (crate::ir::expr::BinOp::LogAnd | crate::ir::expr::BinOp::LogOr),
l,
r,
ty,
} => {
let swapped = if matches!(op, crate::ir::expr::BinOp::LogAnd) {
crate::ir::expr::BinOp::LogOr
} else {
crate::ir::expr::BinOp::LogAnd
};
Expr::Bin {
op: swapped,
l: Box::new(negate(*l)),
r: Box::new(negate(*r)),
ty,
}
}
Expr::Bin { op, l, r, ty } => {
if let Some(inv) = op.invert() {
Expr::Bin { op: inv, l, r, ty }
} else {
Expr::Un {
op: UnOp::Not,
e: Box::new(Expr::Bin { op, l, r, ty }),
}
}
}
other => Expr::Un {
op: UnOp::Not,
e: Box::new(other),
},
}
}
fn is_continue_stmt(s: &Stmt) -> bool {
matches!(s, Stmt::Continue(None))
|| matches!(s, Stmt::Block(v) if v.len() == 1 && matches!(v[0], Stmt::Continue(None)))
}
fn extract_compound_do_while(body: &Stmt) -> Option<(Vec<Stmt>, Expr, Stmt)> {
fn flatten_seq(s: &Stmt) -> Vec<Stmt> {
match s {
Stmt::Block(v) => {
let mut out = Vec::new();
for x in v {
out.extend(flatten_seq(x));
}
out
}
other => vec![other.clone()],
}
}
let mut stmts = flatten_seq(body);
{
let mut flat: Vec<Stmt> = Vec::new();
for st in stmts.into_iter() {
let mut cur = st;
loop {
match cur {
Stmt::If {
cond,
then_stmt,
else_stmt: Some(e),
} if is_continue_stmt(&then_stmt) => {
flat.push(Stmt::If {
cond,
then_stmt,
else_stmt: None,
});
cur = *e;
}
other => {
flat.push(other);
break;
}
}
}
}
stmts = flat;
}
let n = stmts.len();
if n < 2 {
return None;
}
let (c_last, exit) = match &stmts[n - 1] {
Stmt::If {
cond,
then_stmt,
else_stmt: Some(e),
} if is_continue_stmt(then_stmt) && !is_continue_stmt(e) => (cond.clone(), (**e).clone()),
_ => return None,
};
let mut conds = vec![c_last];
let mut i = n - 1;
while i >= 1 {
match &stmts[i - 1] {
Stmt::If {
cond,
then_stmt,
else_stmt: None,
} if is_continue_stmt(then_stmt) => {
conds.push(cond.clone());
i -= 1;
}
_ => break,
}
}
let body_stmts: Vec<Stmt> = stmts[..i].to_vec();
let mut it = conds.into_iter().rev();
let mut cond = it.next().unwrap();
for c in it {
cond = Expr::Bin {
op: BinOp::LogOr,
l: Box::new(c),
r: Box::new(cond),
ty: None,
};
}
Some((body_stmts, cond, exit))
}
fn extract_trailing_do_while(body: &Stmt) -> Option<(Vec<Stmt>, Expr)> {
let mut stmts = match body {
Stmt::Block(v) => v.clone(),
other => vec![other.clone()],
};
if stmts.is_empty() {
return None;
}
let n = stmts.len() - 1;
if let Stmt::If {
cond,
then_stmt,
else_stmt,
} = &stmts[n]
{
let then_is_cont = |s: &Stmt| {
matches!(s, Stmt::Continue(None))
|| matches!(s, Stmt::Block(v) if v.len() == 1 && matches!(v[0], Stmt::Continue(None)))
};
if then_is_cont(then_stmt) && else_stmt.is_none() {
let c = cond.clone();
stmts.pop();
return Some((stmts, c));
}
if else_stmt.as_ref().map(|e| then_is_cont(e)).unwrap_or(false)
&& (matches!(&**then_stmt, Stmt::Block(v) if v.is_empty()))
{
let c = negate(cond.clone());
stmts.pop();
return Some((stmts, c));
}
}
None
}
#[allow(dead_code)]
fn extract_trailing_continue_if(body: &Stmt) -> Option<(Vec<Stmt>, Expr)> {
let mut stmts = match body {
Stmt::Block(v) => v.clone(),
other => vec![other.clone()],
};
if stmts.is_empty() {
return None;
}
let n = stmts.len() - 1;
let is_direct = match &stmts[n] {
Stmt::If {
then_stmt,
else_stmt: None,
..
} => {
matches!(&**then_stmt, Stmt::Continue(None))
|| matches!(&**then_stmt, Stmt::Block(tb) if tb.len() == 1 && matches!(tb[0], Stmt::Continue(None)))
}
_ => false,
};
if is_direct {
if let Stmt::If { cond, .. } = stmts.pop().unwrap() {
return Some((stmts, cond));
}
}
if matches!(stmts.last(), Some(Stmt::Block(_))) {
let last = stmts.pop().unwrap();
if let Some((inner, c)) = extract_trailing_continue_if(&last) {
if !inner.is_empty() {
stmts.push(Stmt::Block(inner));
}
return Some((stmts, c));
} else {
stmts.push(last);
}
}
None
}
#[allow(dead_code)]
fn split_first_block(body: Stmt) -> (Stmt, Option<usize>) {
(body, None)
}
fn stmt_to_vec(s: Stmt) -> Vec<Stmt> {
match s {
Stmt::Block(v) => v,
other => vec![other],
}
}
fn prune_unused_labels(st: &mut Stmt, used: &HashSet<String>) {
loop {
let take = matches!(st, Stmt::Labeled { label, .. } if !used.contains(label));
if !take {
break;
}
let inner = match std::mem::replace(st, Stmt::Block(Vec::new())) {
Stmt::Labeled { body, .. } => *body,
_ => unreachable!(),
};
*st = inner;
}
match st {
Stmt::Block(v) => v.iter_mut().for_each(|x| prune_unused_labels(x, used)),
Stmt::Labeled { body, .. } => prune_unused_labels(body, used),
Stmt::If { then_stmt, else_stmt, .. } => {
prune_unused_labels(then_stmt, used);
if let Some(e) = else_stmt {
prune_unused_labels(e, used);
}
}
Stmt::While { body, .. }
| Stmt::DoWhile { body, .. }
| Stmt::ForEach { body, .. }
| Stmt::Synchronized { body, .. } => prune_unused_labels(body, used),
Stmt::For { init, body, .. } => {
init.iter_mut().for_each(|x| prune_unused_labels(x, used));
prune_unused_labels(body, used);
}
Stmt::Switch { cases, default, .. } => {
for c in cases.iter_mut() {
c.body.iter_mut().for_each(|x| prune_unused_labels(x, used));
}
if let Some(d) = default {
prune_unused_labels(d, used);
}
}
Stmt::Try { body, catches, finally }
| Stmt::TryWithResources { body, catches, finally, .. } => {
prune_unused_labels(body, used);
for c in catches.iter_mut() {
prune_unused_labels(&mut c.body, used);
}
if let Some(f) = finally {
prune_unused_labels(f, used);
}
}
_ => {}
}
}