Skip to main content

ddc_dec/
method.rs

1//! Per-method decompilation pipeline:
2//! DEX code units → CFG → register→IR lifting (fixpoint with merge phis) →
3//! `jdc-core` structuring → statement conversion → refinement passes.
4
5use std::collections::HashMap;
6
7use ddc_dex::insn::InsnKind;
8use jdc_core::convert::Converter;
9use jdc_core::ir::build::BlockResult;
10use jdc_core::ir::expr::{Expr, TypeRef};
11use jdc_core::ir::stmt::Stmt;
12use jdc_core::structure::{reverse_postorder, Structurer};
13use jdc_core::types::{JavaType, MethodDescriptor};
14use jdc_core::var::VarTable;
15
16use crate::cfg::DexCfg;
17use crate::lift::{entry_regs, Lifter, MethodEnv, OutState, Reg};
18use crate::passes;
19use crate::{DexPool, PoolClass, PoolMethod};
20
21pub struct MethodBody {
22    pub body: Stmt,
23    pub vt: VarTable,
24    pub desc: MethodDescriptor,
25}
26
27/// Catch types for the ranges a block handles (drives move-exception typing).
28fn handler_types_for(cfg: &DexCfg, bid: usize) -> Vec<Option<String>> {
29    cfg.blocks[bid]
30        .handlers
31        .iter()
32        .filter_map(|&ri| cfg.exc_ranges.get(ri).map(|r| r.catch_type.clone()))
33        .collect()
34}
35
36/// Decompile one method. `Err` only for malformed input; unsupported
37/// constructs degrade to comments inside the statement tree.
38pub fn decompile_method(
39    pool: &DexPool,
40    class: &PoolClass,
41    m: &PoolMethod,
42) -> Result<Option<MethodBody>, String> {
43    let t0 = std::time::Instant::now();
44    if m.is_abstract_or_native() {
45        return Ok(None);
46    }
47    let desc = m
48        .parsed_desc()
49        .ok_or_else(|| format!("bad descriptor {}", m.desc))?;
50    let dex = pool.dex(m.dex_idx).ok_or("missing dex image")?;
51    let dex = &*dex;
52    let Some(mut code) = dex.code_at(m.code_off) else {
53        return Ok(None);
54    };
55    let insn_count = code.insns.len();
56    let param_names = dex.parameter_names(m.debug_info_off);
57    let cfg = DexCfg::build(&mut code, &|ty| dex.class_name(ty));
58    let n = cfg.blocks.len();
59    let env = MethodEnv {
60        pool,
61        dex,
62        code: &code,
63        class_name: class.name.clone(),
64        method_name: m.name.clone(),
65        desc: desc.clone(),
66        is_static: m.is_static(),
67        code_units: cfg.code_units,
68    };
69    let mut vt = VarTable::default();
70    let entry = entry_regs(&mut vt, &env, &param_names);
71    if n == 0 {
72        return Ok(None);
73    }
74    // Pathological inputs (R8-merged model classes, obfuscated switch
75    // cascades) can explode the copied-tail rendering; guard up front.
76    if insn_count > 30_000 || n > 8_000 {
77        return Ok(Some(stub_body(
78            format!(
79                "$DDC: method too large to decompile ({} insns, {} blocks)",
80                insn_count, n
81            ),
82            desc,
83        )));
84    }
85
86    // Visiting order: reverse postorder, then any stragglers (so every block
87    // is built at least once even when unreachable).
88    let core_for_order = cfg.to_core();
89    let universe: std::collections::HashSet<usize> = (0..n).collect();
90    let mut order = reverse_postorder(&core_for_order, core_for_order.entry, &universe);
91    {
92        let mut seen: std::collections::HashSet<usize> = order.iter().copied().collect();
93        for b in &cfg.blocks {
94            if seen.insert(b.id) {
95                order.push(b.id);
96            }
97        }
98    }
99
100    // Handler blocks: entry state approximated by the try-entry state (the
101    // dominant pattern: catch reads values established before the try).
102    let mut handler_entry_block: HashMap<usize, usize> = HashMap::new();
103    for b in &cfg.blocks {
104        if b.handlers.is_empty() {
105            continue;
106        }
107        let mut best: Option<(u32, usize)> = None;
108        for &ri in &b.handlers {
109            if let Some(r) = cfg.exc_ranges.get(ri) {
110                if best.map(|(s, _)| r.start < s).unwrap_or(true) {
111                    if let Some(eb) = cfg.block_at(r.start) {
112                        best = Some((r.start, eb));
113                    }
114                }
115            }
116        }
117        if let Some((_, eb)) = best {
118            handler_entry_block.insert(b.id, eb);
119        }
120    }
121
122    // ---- fixpoint: register states across blocks ----
123    let mut results: Vec<BlockResult> = Vec::with_capacity(n);
124    results.resize_with(n, || BlockResult {
125        stmts: vec![],
126        out_stack: vec![],
127        term: jdc_core::ir::build::Term::Return(None),
128    });
129    let mut out_states: Vec<Option<OutState>> = vec![None; n];
130    // Stable (block, register) → var ids across worklist rebuilds: keeps
131    // re-lifts id-deterministic so the fixpoint CONVERGES instead of
132    // cascading (was: 30M block-lifts on weibo, ~60 per block).
133    let mut stable_vars: HashMap<(usize, u16), u32> = HashMap::new();
134    // Method feature flags OR-merged from every block lift: gates
135    // post-lift passes that can only match if the bytecode contained the
136    // feature (most methods contain none).
137    let mut mflags = crate::lift::MethodFlags::none();
138    // Try-entry INPUT snapshots (for handler-block approximation) — only
139    // try-entry starts (rare) keep their input state; no persistent
140    // per-block copies.
141    let mut try_entry_snapshots: HashMap<usize, Vec<Reg>> = HashMap::new();
142    let mut built_once = vec![false; n];
143    let mut errors: HashMap<usize, String> = HashMap::new();
144    // (merge block, register) → phi var id.
145    let mut phis: HashMap<(usize, u16), u32> = HashMap::new();
146    // pred → (write_pc, phi var, value) materializations.
147
148    let entry_is_handler = handler_entry_block.contains_key(&0);
149
150    // Per-visit costs hoisted out of the worklist loop: the try-entry scan
151    // (all exc ranges × a binary search per range) and the handler catch
152    // types (String clones) re-ran on EVERY visit — visits average 2.5×
153    // blocks on real code, far more on monsters.
154    let try_entry_flags: Vec<bool> = (0..n)
155        .map(|b| {
156            !cfg.blocks[b].handlers.is_empty()
157                || cfg
158                    .exc_ranges
159                    .iter()
160                    .any(|r| cfg.block_at(r.start) == Some(b))
161        })
162        .collect();
163    let handler_types: Vec<Vec<Option<String>>> =
164        (0..n).map(|b| handler_types_for(&cfg, b)).collect();
165
166    // Worklist fixpoint with version stamps: a block rebuilds only when the
167    // OUT-state versions of its inputs changed. This avoids the previous
168    // every-round full pass, which deep-cloned and deep-compared the whole
169    // register state (Expr trees) of every block — the dominant cost of
170    // large methods.
171    let mut ver: Vec<u64> = vec![0; n]; // out-state version per block
172    let mut built_ver: Vec<u64> = vec![u64::MAX; n]; // input signature when built
173                                                     // input signature: entry=0; otherwise XOR/sum of (pred, ver[pred]) pairs.
174    fn input_sig(cfg: &DexCfg, bid: usize, ver: &[u64]) -> u64 {
175        let mut h: u64 = 1;
176        for &p in &cfg.blocks[bid].pred {
177            h = h
178                .wrapping_mul(31)
179                .wrapping_add(p as u64)
180                .wrapping_mul(31)
181                .wrapping_add(ver[p]);
182        }
183        // Handler blocks also depend on their try-entry block's input.
184        h
185    }
186    let mut queue: std::collections::VecDeque<usize> = std::collections::VecDeque::new();
187    let mut queued: Vec<bool> = vec![false; n];
188    for &bid in &order {
189        queue.push_back(bid);
190        queued[bid] = true;
191    }
192    // merge bid → [(pred, write_pc, phi var, value)] — re-recorded whole on
193    // each visit of the merge block (stale rounds must not accumulate).
194    let mut appends: HashMap<usize, Vec<(usize, u32, u32, Expr)>> = HashMap::new();
195    let mut total_visits: usize = 0;
196    let visit_cap: usize = 64 * n + 256;
197
198    while let Some(bid) = queue.pop_front() {
199        queued[bid] = false;
200        total_visits += 1;
201        if total_visits > visit_cap {
202            CAP_HITS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
203            CAP_INSNS.fetch_add(
204                insn_count as u64 * 1000 + n as u64,
205                std::sync::atomic::Ordering::Relaxed,
206            );
207            break; // degenerate oscillation guard
208        }
209        let is_handler = handler_entry_block.contains_key(&bid);
210        // 1. Signature gate FIRST: a visit whose input versions are
211        //    unchanged skips the (clone-heavy) input-state computation
212        //    entirely — that clone was the fixpoint's dominant cost.
213        let sig = input_sig(&cfg, bid, &ver)
214            ^ (if is_handler {
215                let eb = handler_entry_block[&bid];
216                (eb as u64) << 32
217            } else {
218                0
219            });
220        let need_build = !built_once[bid] || built_ver[bid] != sig;
221        if !need_build {
222            // Handler blocks depend on their try-entry block's INPUT —
223            // when that entry is re-visited, the handler is re-queued; a
224            // skipped visit here means the recorded state is current.
225            continue;
226        }
227        // 2. Input register state (only on rebuild).
228        let ins: Vec<Reg> = if bid == cfg.entry {
229            if cfg.blocks[0].pred.is_empty() || entry_is_handler {
230                entry.clone()
231            } else {
232                // A loop back into pc 0: params are one side of the merge.
233                // Sides are BORROWED (cloning each pred's state here was a
234                // top fixpoint cost).
235                let mut sides: Vec<&[Reg]> = vec![entry.as_slice()];
236                for p in cfg.blocks[bid].pred.clone() {
237                    if let Some(o) = &out_states[p] {
238                        sides.push(o.regs.as_slice());
239                    }
240                }
241                let merged = merge_states(&mut vt, &mut phis, bid, &sides);
242                // The entry side's phi contributions become leading
243                // assignments in the entry block (virtual pred). The entry
244                // block may be re-visited by the worklist — each visit
245                // replaces (not appends) its records.
246                let mut recs: Vec<(usize, u32, u32, Expr)> = Vec::new();
247                for (r, st) in entry.iter().enumerate() {
248                    if let Some(&phi) = phis.get(&(bid, r as u16)) {
249                        let value = match st {
250                            Reg::Live(v) if *v == phi => continue,
251                            Reg::Live(v) => Expr::Local {
252                                var: *v,
253                                ty: vt.var(*v).ty.clone(),
254                            },
255                            Reg::Pending(e) | Reg::PendingCall(e) => e.clone(),
256                            Reg::Undef | Reg::WideHi => continue,
257                        };
258                        recs.push((usize::MAX, 0, phi, value));
259                    }
260                }
261                appends.insert(bid, recs);
262                merged
263            }
264        } else if is_handler {
265            let eb = handler_entry_block[&bid];
266            try_entry_snapshots
267                .get(&eb)
268                .cloned()
269                .unwrap_or_else(|| vec![Reg::Undef; env.code.registers_size as usize])
270        } else {
271            // Borrowed sides; the single-pred fast path clones once (the
272            // lifter needs an owned copy anyway).
273            let mut sides: Vec<&[Reg]> = Vec::new();
274            for p in cfg.blocks[bid].pred.clone() {
275                if let Some(o) = &out_states[p] {
276                    sides.push(o.regs.as_slice());
277                }
278            }
279            if sides.is_empty() {
280                vec![Reg::Undef; env.code.registers_size as usize]
281            } else if sides.len() == 1 {
282                sides[0].to_vec()
283            } else {
284                merge_states(&mut vt, &mut phis, bid, &sides)
285            }
286        };
287
288        // 3. Phi materialization records for predecessors — recomputed for
289        //    this block only; later pred rebuilds re-visit the merge and
290        //    refresh them (the last visit wins).
291        if phis_for_merge(bid, &phis) {
292            let mut recs: Vec<(usize, u32, u32, Expr)> = Vec::new();
293            record_appends(&cfg, bid, &out_states, &phis, &vt, &mut recs);
294            // Entry-merge visits also carry the entry-side contributions —
295            // preserve those (recorded above when bid == entry).
296            if bid == cfg.entry {
297                if let Some(prev) = appends.get(&bid) {
298                    for (p, pc, v, e) in prev.iter() {
299                        if *p == usize::MAX {
300                            recs.push((*p, *pc, *v, e.clone()));
301                        }
302                    }
303                }
304            }
305            appends.insert(bid, recs);
306        }
307
308        // 4. Build (signature known changed). The lifter CONSUMES `ins`
309        //    (moved, not cloned); try-entry snapshots preserve the input
310        //    state for the handler blocks approximating from it.
311        built_once[bid] = true;
312        built_ver[bid] = sig;
313        if try_entry_flags[bid] {
314            try_entry_snapshots.insert(bid, ins.clone());
315        }
316        BUILD_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
317        INSN_LIFTED.fetch_add(
318            cfg.block_ins(&cfg.blocks[bid]).len() as u64,
319            std::sync::atomic::Ordering::Relaxed,
320        );
321        let htypes = &handler_types[bid];
322        let block_ins = cfg.block_ins(&cfg.blocks[bid]);
323        let lifter = Lifter::new(&env, &mut vt, ins, bid, &mut stable_vars, &mut mflags);
324        let rebuilt = match lifter.build_block(block_ins, htypes) {
325            Ok((r, out)) => {
326                results[bid] = r;
327                let changed = match &out_states[bid] {
328                    Some(prev) => prev.regs != out.regs,
329                    None => true,
330                };
331                out_states[bid] = Some(out);
332                changed
333            }
334            Err(msg) => {
335                results[bid] = BlockResult {
336                    stmts: vec![Stmt::Comment(format!("$DDC-BLOCK-ERROR: {msg}"))],
337                    out_stack: vec![],
338                    term: jdc_core::ir::build::Term::Return(None),
339                };
340                out_states[bid] = Some(OutState {
341                    regs: vec![Reg::Undef; env.code.registers_size as usize],
342                    write_pc: vec![0; env.code.registers_size as usize],
343                });
344                errors.insert(bid, msg);
345                false
346            }
347        };
348        if rebuilt {
349            ver[bid] += 1;
350            // Successors re-check their input signature.
351            for &succ in &cfg.blocks[bid].succ {
352                if !queued[succ] && succ < n {
353                    queue.push_back(succ);
354                    queued[succ] = true;
355                }
356            }
357            // Handler blocks watch their try-entry block's INPUT — cheap
358            // approximation: also queue the handler of every range this
359            // block belongs to.
360            if is_handler {
361                let eb = handler_entry_block[&bid];
362                if !queued[eb] && eb < n {
363                    queue.push_back(eb);
364                    queued[eb] = true;
365                }
366            }
367            // Try-entry inputs: when this block feeds a handler entry via a
368            // try range, queue that handler.
369            for &ri in &cfg.blocks[bid].handlers {
370                if let Some(r) = cfg.exc_ranges.get(ri) {
371                    if let Some(hb) = cfg.block_at(r.handler) {
372                        if !queued[hb] && hb < n {
373                            queue.push_back(hb);
374                            queued[hb] = true;
375                        }
376                    }
377                }
378            }
379        }
380    }
381
382    // Entry-side phi contributions: carried as declaration-with-init and
383    // prepended to the method body AFTER conversion (the entry block may be
384    // a loop header — its statements re-execute per iteration).
385    // Entry-side phi contributions (the virtual pred): captured for the
386    // method-top declarations; the REAL preds' records stay in `appends`
387    // for the commit loop (removing the whole entry key would lose the
388    // loop-body assignments — the phis would collapse to their entry
389    // values).
390    let mut entry_phi_inits: Vec<(u32, Expr)> = Vec::new();
391    if let Some(recs) = appends.get_mut(&cfg.entry) {
392        let mut entry_side: Vec<(u32, Expr)> = Vec::new();
393        recs.retain(|(p, _, v, e)| {
394            if *p == usize::MAX {
395                entry_side.push((*v, e.clone()));
396                false
397            } else {
398                true
399            }
400        });
401        entry_side.sort_by_key(|(v, _)| *v);
402        entry_phi_inits = entry_side;
403    }
404
405    // Commit phi materializations: each pred assigns its outgoing value to
406    // the merge var, ordered by the pc of the instruction that produced it.
407    // Values that REFERENCE a sibling phi are snapshotted into a fresh temp
408    // first: register rotations (`a = b; b = a % b`) would otherwise read
409    // the already-reassigned phi (stale capture).
410    // Regroup: merge-keyed records → per-pred statement lists.
411    let mut per_pred: HashMap<usize, Vec<(u32, u32, Expr)>> = HashMap::new();
412    for recs in appends.values() {
413        for (p, pc, v, e) in recs {
414            per_pred.entry(*p).or_default().push((*pc, *v, e.clone()));
415        }
416    }
417    for (p, mut adds) in per_pred {
418        if p == usize::MAX {
419            continue;
420        }
421        adds.sort_by_key(|(pc, _, _)| *pc);
422        let term_is_exit = matches!(
423            results[p].term,
424            jdc_core::ir::build::Term::Return(_) | jdc_core::ir::build::Term::Throw(_)
425        );
426        if term_is_exit {
427            continue;
428        }
429        let phi_set: std::collections::HashSet<u32> = adds.iter().map(|(_, v, _)| *v).collect();
430        let mut snapshots: Vec<Stmt> = Vec::new();
431        // (phi, value) pairs with snapshot temps substituted.
432        let mut emitted: Vec<(u32, Expr)> = Vec::new();
433        for (_, v, e) in &adds {
434            let mut e = e.clone();
435            let mut refs_phi = false;
436            visit_phi_refs(&e, &phi_set, &mut refs_phi);
437            if refs_phi {
438                // Snapshot: emit `temp = value` BEFORE any phi assignment.
439                let ty = e.type_ref();
440                let id = vt.vars.len() as u32;
441                let name = format!("v{}", id);
442                vt.vars.push(jdc_core::var::VarInfo {
443                    id,
444                    slot: u16::MAX,
445                    name,
446                    ty: ty.clone(),
447                    is_param: false,
448                    range_start: 0,
449                    range_end: u16::MAX,
450                    synthetic_name: true,
451                });
452                snapshots.push(Stmt::LocalDef {
453                    var: id,
454                    init: Some(e),
455                    is_final: false,
456                    force_type: true,
457                });
458                e = Expr::Local { var: id, ty };
459            }
460            emitted.push((*v, e));
461        }
462        for st in snapshots {
463            results[p].stmts.push(st);
464        }
465        for (v, e) in emitted {
466            results[p].stmts.push(Stmt::ExprStmt(Expr::Assign {
467                target: Box::new(Expr::Local {
468                    var: v,
469                    ty: vt.var(v).ty.clone(),
470                }),
471                op: jdc_core::ir::expr::AssignOp::Plain,
472                value: Box::new(e),
473            }));
474        }
475    }
476
477    let t_fix = t0;
478    phase_hit_n(0, n, t_fix);
479    // ---- single-block fast path ----
480    // A lone basic block is straight-line code (any branch/switch/handler
481    // splits the graph): no merges, no phis, no control flow to structure.
482    // Bypassing the structurer + converter + the control-flow passes for
483    // these (over half of R8-produced methods) cuts the dominant cost.
484    if n == 1 && !entry_is_handler && cfg.blocks[0].pred.is_empty() {
485        let r = std::mem::replace(
486            &mut results[0],
487            BlockResult {
488                stmts: vec![],
489                out_stack: vec![],
490                term: jdc_core::ir::build::Term::Goto,
491            },
492        );
493        let mut body = r.stmts;
494        match r.term {
495            jdc_core::ir::build::Term::Return(Some(e)) => {
496                body.push(Stmt::Return(Some(e)));
497            }
498            jdc_core::ir::build::Term::Return(None) => {}
499            jdc_core::ir::build::Term::Throw(e) => body.push(Stmt::Throw(e)),
500            _ => {}
501        }
502        let mut body = Stmt::Block(body);
503        passes::fused_expr_rewrites(&mut body, &vt);
504        if mflags.has_sb() {
505            passes::fold_string_builders(&mut body, &vt);
506        }
507        passes::forward_single_use(&mut body, &vt);
508        passes::cleanup(&mut body);
509        passes::inline_accessors(&mut body, pool);
510        passes::infer_types(&mut vt, &mut body, &desc.ret, &env);
511        passes::apply_local_names(&mut vt, &body);
512        passes::remove_kotlin_checks(&mut body);
513        passes::platform_constants(&mut body);
514        passes::ensure_declared(&mut body, &vt);
515        passes::strip_trailing_void_return(&mut body);
516        passes::cleanup(&mut body);
517        if !errors.is_empty() {
518            passes::prepend_comment(
519                &mut body,
520                format!("$DDC: {} block(s) failed to decompile", errors.len()),
521            );
522        }
523        phase_hit_n(2, n, t_fix);
524        record_bucket(n, t0);
525        return Ok(Some(MethodBody { body, vt, desc }));
526    }
527
528    // ---- structure + convert ----
529    // jcdc's copy-budget degradation: a rendering whose statement tree
530    // explodes past the guard retries at a halved copy budget (the
531    // structurer's tail-copying is the exponential mechanism).
532    const TREE_GUARD: usize = 20_000;
533    let core_cfg = cfg.to_core();
534    #[allow(unused_assignments)]
535    let mut body: Option<Stmt> = None;
536    // Copy budget scales with graph size: many-block methods are where the
537    // exponential tail-copy blowups live (weibo: 389 methods >100 blocks
538    // = 40% of all CPU at budget 512). Starting lower converges without
539    // the 6-round halving.
540    let mut budget: u32 = if n > 100 {
541        64
542    } else if n > 20 {
543        128
544    } else {
545        512
546    };
547    // Shared once-per-method artifacts: the O(groups²) exception-group
548    // scan and the full-graph dominator tree used to run TWICE per
549    // method (Structurer + Converter) and once more per copy-budget
550    // retry — monster classes (gson adapters) spent the bulk of their
551    // time there.
552    let groups = jdc_core::structure::group_exceptions_with(&core_cfg, Some(&results));
553    let dom_universe: std::collections::HashSet<usize> = (0..core_cfg.blocks.len()).collect();
554    let dom = jdc_core::structure::compute_dominators(&core_cfg, &dom_universe, core_cfg.entry);
555    if std::env::var("DDC_TRACE").is_ok() {
556        eprintln!("[mshape] {}.{} blocks={}", class.name, m.name, n);
557    }
558    #[cfg(feature = "visit-stats")]
559    {
560        jdc_core::structure::reset_visit_stats();
561    }
562    // Walk-visit budget, proportional to the block count: walk() runs
563    // roughly once per structured scope for sane CFGs, so 40n+512 is
564    // generous headroom — but the EXPONENTIAL explorations (Telegram
565    // SendMessagesHelper family: deep if/loop/try nesting whose scope
566    // count multiplies) consume visits geometrically and get cut to a
567    // Goto, which terminates the walk. Without this a single <16k-insn
568    // method can recurse for effectively forever while the worker (and
569    // the process) never finishes — Telegram's full run wrote every file
570    // yet hung for 300+ seconds on 25 such classes.
571    let walk_budget: u64 = std::env::var("DDC_WALKBUDGET")
572        .ok()
573        .and_then(|v| v.parse().ok())
574        .unwrap_or(8 * n as u64 + 128);
575    // Wall-clock guard for the walk: legit methods finish far under this
576    // (weibo's largest legit monster ~150ms); the exponential explorations
577    // (Telegram sendMessage family) cut to Gotos, same degradation as the
578    // visit budget. Covers cases where per-visit cost makes a visit-count
579    // budget too slow (6ms/visit × 14k visits = minutes).
580    let walk_deadline = std::time::Instant::now()
581        + std::time::Duration::from_millis(
582            std::env::var("DDC_WALKMS")
583                .ok()
584                .and_then(|v| v.parse().ok())
585                .unwrap_or(1500),
586        );
587    loop {
588        jdc_core::structure::set_budget_override(Some(budget));
589        jdc_core::structure::set_walk_visit_budget(Some(walk_budget));
590        jdc_core::structure::set_walk_deadline(Some(walk_deadline));
591        let mut st = Structurer::with_precomputed_groups(
592            &core_cfg,
593            &results,
594            groups.clone(),
595            Default::default(),
596            Default::default(),
597        );
598        let region = st.structure_method();
599        let mut converter = Converter::with_precomputed(
600            &core_cfg,
601            &results,
602            groups.clone(),
603            jdc_core::structure::DomInfo {
604                idom: dom.idom.clone(),
605            },
606        );
607        let candidate = converter.convert(region);
608        jdc_core::structure::set_budget_override(None);
609        jdc_core::structure::set_walk_visit_budget(None);
610        jdc_core::structure::set_walk_deadline(None);
611        let size = {
612            let mut c = 0usize;
613            passes::count_stmts_deep(&candidate, &mut c);
614            c
615        };
616        if std::env::var("DDC_TRACE").is_ok() {
617            static TRACE_ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
618            if *TRACE_ON.get_or_init(|| std::env::var("DDC_TRACE").is_ok()) {
619                eprintln!(
620                    "[guard] {}.{} size={} budget={}",
621                    class.name, m.name, size, budget
622                );
623            }
624        }
625        if size <= TREE_GUARD {
626            body = Some(candidate);
627            break;
628        }
629        if budget <= 16 {
630            return Ok(Some(stub_body(
631                format!(
632                    "$DDC: statement tree exploded ({} nodes) even at copy budget 16",
633                    size
634                ),
635                desc,
636            )));
637        }
638        budget /= 2;
639    }
640    #[cfg(feature = "visit-stats")]
641    if std::env::var("DDC_TRACE").is_ok() && n > 3 {
642        let used = jdc_core::structure::walk_visits_consumed();
643        eprintln!(
644            "[visits] {}.{} blocks={} consumed={} ratio={:.1}",
645            class.name,
646            m.name,
647            n,
648            used,
649            used as f64 / n as f64
650        );
651    }
652    phase_hit_n(1, n, t_fix);
653    let mut body = body.unwrap();
654    if !entry_phi_inits.is_empty() {
655        let head: Vec<Stmt> = entry_phi_inits
656            .into_iter()
657            .map(|(v, e)| Stmt::LocalDef {
658                var: v,
659                init: Some(e),
660                is_final: false,
661                force_type: true,
662            })
663            .collect();
664        body = match body {
665            Stmt::Block(v) => {
666                let mut nv = head;
667                nv.extend(v);
668                Stmt::Block(nv)
669            }
670            other => {
671                let mut nv = head;
672                nv.push(other);
673                Stmt::Block(nv)
674            }
675        };
676    }
677
678    passes::bind_catches(&mut body, &mut vt);
679    passes::prune_unreachable(&mut body);
680    // One traversal for cmp residuals + null compares + const-first flips
681    // (three separate full-tree walks before).
682    passes::fused_expr_rewrites(&mut body, &vt);
683    if mflags.has_sb() {
684        passes::fold_string_builders(&mut body, &vt);
685    }
686    passes::ternary_fold(&mut body);
687    if mflags.has_monitor() {
688        passes::fold_synchronized(&mut body);
689    }
690    passes::forward_single_use(&mut body, &vt);
691    passes::cleanup(&mut body);
692    passes::inline_accessors(&mut body, pool);
693    passes::infer_types(&mut vt, &mut body, &desc.ret, &env);
694    passes::booleanize(&mut vt, &mut body);
695    passes::apply_local_names(&mut vt, &body);
696    passes::remove_kotlin_checks(&mut body);
697    passes::platform_constants(&mut body);
698    passes::ensure_declared(&mut body, &vt);
699    passes::strip_trailing_void_return(&mut body);
700    passes::cleanup(&mut body);
701
702    if !errors.is_empty() {
703        passes::prepend_comment(
704            &mut body,
705            format!("$DDC: {} block(s) failed to decompile", errors.len()),
706        );
707    }
708
709    phase_hit_n(2, n, t_fix);
710    record_bucket(n, t0);
711    Ok(Some(MethodBody { body, vt, desc }))
712}
713
714static PHASE_MICROS: [std::sync::atomic::AtomicU64; 5] = [
715    std::sync::atomic::AtomicU64::new(0),
716    std::sync::atomic::AtomicU64::new(0),
717    std::sync::atomic::AtomicU64::new(0),
718    std::sync::atomic::AtomicU64::new(0),
719    std::sync::atomic::AtomicU64::new(0),
720];
721/// Phase × bucket attribution (5 phases × 5 buckets, row-major).
722static PHASE_BUCKET_MICROS: [std::sync::atomic::AtomicU64; 25] = [
723    std::sync::atomic::AtomicU64::new(0),
724    std::sync::atomic::AtomicU64::new(0),
725    std::sync::atomic::AtomicU64::new(0),
726    std::sync::atomic::AtomicU64::new(0),
727    std::sync::atomic::AtomicU64::new(0),
728    std::sync::atomic::AtomicU64::new(0),
729    std::sync::atomic::AtomicU64::new(0),
730    std::sync::atomic::AtomicU64::new(0),
731    std::sync::atomic::AtomicU64::new(0),
732    std::sync::atomic::AtomicU64::new(0),
733    std::sync::atomic::AtomicU64::new(0),
734    std::sync::atomic::AtomicU64::new(0),
735    std::sync::atomic::AtomicU64::new(0),
736    std::sync::atomic::AtomicU64::new(0),
737    std::sync::atomic::AtomicU64::new(0),
738    std::sync::atomic::AtomicU64::new(0),
739    std::sync::atomic::AtomicU64::new(0),
740    std::sync::atomic::AtomicU64::new(0),
741    std::sync::atomic::AtomicU64::new(0),
742    std::sync::atomic::AtomicU64::new(0),
743    std::sync::atomic::AtomicU64::new(0),
744    std::sync::atomic::AtomicU64::new(0),
745    std::sync::atomic::AtomicU64::new(0),
746    std::sync::atomic::AtomicU64::new(0),
747    std::sync::atomic::AtomicU64::new(0),
748];
749static PHASE_ON: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
750
751fn bucket_of(n: usize) -> usize {
752    if n <= 1 {
753        0
754    } else if n <= 5 {
755        1
756    } else if n <= 20 {
757        2
758    } else if n <= 100 {
759        3
760    } else {
761        4
762    }
763}
764
765pub(crate) fn phase_hit_n(i: usize, n: usize, t: std::time::Instant) {
766    if !PHASE_ON.load(std::sync::atomic::Ordering::Relaxed) {
767        return;
768    }
769    let us = t.elapsed().as_micros() as u64;
770    PHASE_MICROS[i].fetch_add(us, std::sync::atomic::Ordering::Relaxed);
771    PHASE_BUCKET_MICROS[i * 5 + bucket_of(n)].fetch_add(us, std::sync::atomic::Ordering::Relaxed);
772}
773
774pub(crate) fn phase_hit(i: usize, t: std::time::Instant) {
775    if !PHASE_ON.load(std::sync::atomic::Ordering::Relaxed) {
776        return;
777    }
778    PHASE_MICROS[i].fetch_add(
779        t.elapsed().as_micros() as u64,
780        std::sync::atomic::Ordering::Relaxed,
781    );
782}
783
784/// Enable phase timers (DDC_PHASES=1).
785pub fn phases_enable() {
786    PHASE_ON.store(true, std::sync::atomic::Ordering::Relaxed);
787}
788
789/// (fixpoint, structure+convert, passes, print, total) in micros.
790pub fn phases_dump() -> [u64; 5] {
791    let mut out = [0u64; 5];
792    for i in 0..5 {
793        out[i] = PHASE_MICROS[i].load(std::sync::atomic::Ordering::Relaxed);
794    }
795    out
796}
797
798/// Per-bucket phase micros: 5 phases × 5 buckets, row-major.
799pub fn phases_bucket_dump() -> [[u64; 5]; 5] {
800    let mut out = [[0u64; 5]; 5];
801    for i in 0..5 {
802        for b in 0..5 {
803            out[i][b] = PHASE_BUCKET_MICROS[i * 5 + b].load(std::sync::atomic::Ordering::Relaxed);
804        }
805    }
806    out
807}
808
809static CAP_HITS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
810static CAP_INSNS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
811
812static BUILD_COUNT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
813static INSN_LIFTED: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
814
815static BUCKET_COUNT: [std::sync::atomic::AtomicU64; 5] = [
816    std::sync::atomic::AtomicU64::new(0),
817    std::sync::atomic::AtomicU64::new(0),
818    std::sync::atomic::AtomicU64::new(0),
819    std::sync::atomic::AtomicU64::new(0),
820    std::sync::atomic::AtomicU64::new(0),
821];
822static BUCKET_MICROS: [std::sync::atomic::AtomicU64; 5] = [
823    std::sync::atomic::AtomicU64::new(0),
824    std::sync::atomic::AtomicU64::new(0),
825    std::sync::atomic::AtomicU64::new(0),
826    std::sync::atomic::AtomicU64::new(0),
827    std::sync::atomic::AtomicU64::new(0),
828];
829
830/// Dump build/insn counters.
831pub fn dump_builds() -> (u64, u64) {
832    (
833        BUILD_COUNT.load(std::sync::atomic::Ordering::Relaxed),
834        INSN_LIFTED.load(std::sync::atomic::Ordering::Relaxed),
835    )
836}
837
838/// (cap-hit methods, sum of insns*1000+blocks for cap-hit methods).
839pub fn dump_caps() -> (u64, u64) {
840    (
841        CAP_HITS.load(std::sync::atomic::Ordering::Relaxed),
842        CAP_INSNS.load(std::sync::atomic::Ordering::Relaxed),
843    )
844}
845
846/// Dump DDC_BUCKETS statistics (global atomics).
847pub fn dump_buckets() -> [(u64, u64); 5] {
848    let mut out = [(0u64, 0u64); 5];
849    for i in 0..5 {
850        out[i] = (
851            BUCKET_COUNT[i].load(std::sync::atomic::Ordering::Relaxed),
852            BUCKET_MICROS[i].load(std::sync::atomic::Ordering::Relaxed),
853        );
854    }
855    out
856}
857
858/// Dominator-recompute counters from jdc-core (perf diagnostics).
859pub fn dom_counters() -> (u64, u64) {
860    (
861        jdc_core::structure::DOM_CALLS.load(std::sync::atomic::Ordering::Relaxed),
862        jdc_core::structure::DOM_BLOCKS.load(std::sync::atomic::Ordering::Relaxed),
863    )
864}
865
866/// Enable dominator-recompute counters (diagnostics; jdc-core gate).
867pub fn dom_counters_enable() {
868    jdc_core::structure::set_dom_counters(true);
869}
870
871/// A minimal body carrying a diagnostic comment.
872fn stub_body(msg: String, desc: MethodDescriptor) -> MethodBody {
873    MethodBody {
874        body: Stmt::Block(vec![Stmt::Comment(msg)]),
875        vt: VarTable::default(),
876        desc,
877    }
878}
879
880/// True when any phi belongs to merge block `bid`.
881fn phis_for_merge(bid: usize, phis: &HashMap<(usize, u16), u32>) -> bool {
882    phis.keys().any(|(b, _)| *b == bid)
883}
884
885/// Phi materialization records for `bid`'s predecessors (pred-tagged).
886fn record_appends(
887    cfg: &DexCfg,
888    bid: usize,
889    out_states: &[Option<OutState>],
890    phis: &HashMap<(usize, u16), u32>,
891    vt: &jdc_core::var::VarTable,
892    out: &mut Vec<(usize, u32, u32, Expr)>,
893) {
894    for p in cfg.blocks[bid].pred.clone() {
895        let Some(o) = &out_states[p] else { continue };
896        for (r, st) in o.regs.iter().enumerate() {
897            if let Some(&phi) = phis.get(&(bid, r as u16)) {
898                let value = match st {
899                    Reg::Live(v) if *v == phi => continue,
900                    Reg::Live(v) => Expr::Local {
901                        var: *v,
902                        ty: vt.var(*v).ty.clone(),
903                    },
904                    Reg::Pending(e) | Reg::PendingCall(e) => e.clone(),
905                    Reg::Undef | Reg::WideHi => continue,
906                };
907                out.push((p, o.write_pc.get(r).copied().unwrap_or(0), phi, value));
908            }
909        }
910    }
911}
912
913#[allow(unused)]
914fn record_bucket(n: usize, t0: std::time::Instant) {
915    // (Perf: env::var is a lock+lookup — per-METHOD cost on 716k-method
916    // runs; resolve once via OnceLock.)
917    static BUCKETS_ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
918    if !*BUCKETS_ON.get_or_init(|| std::env::var("DDC_BUCKETS").is_ok()) {
919        return;
920    }
921    let bucket = if n <= 1 {
922        0
923    } else if n <= 5 {
924        1
925    } else if n <= 20 {
926        2
927    } else if n <= 100 {
928        3
929    } else {
930        4
931    };
932    BUCKET_COUNT[bucket].fetch_add(1, std::sync::atomic::Ordering::Relaxed);
933    BUCKET_MICROS[bucket].fetch_add(
934        t0.elapsed().as_micros() as u64,
935        std::sync::atomic::Ordering::Relaxed,
936    );
937}
938
939/// True when the expression references any of the phi vars.
940fn visit_phi_refs(e: &Expr, phis: &std::collections::HashSet<u32>, hit: &mut bool) {
941    if *hit {
942        return;
943    }
944    if let Expr::Local { var, .. } = e {
945        if phis.contains(var) {
946            *hit = true;
947        }
948        return;
949    }
950    let mut kids: Vec<&Expr> = Vec::new();
951    match e {
952        Expr::Un { e, .. } | Expr::Cast { e, .. } | Expr::InstanceOf { e, .. } => kids.push(e),
953        Expr::Bin { l, r, .. } => {
954            kids.push(l);
955            kids.push(r);
956        }
957        Expr::Cond { c, t, f } => {
958            kids.push(c);
959            kids.push(t);
960            kids.push(f);
961        }
962        Expr::Assign { target, value, .. } => {
963            kids.push(target);
964            kids.push(value);
965        }
966        Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => kids.push(e),
967        Expr::Field { owner: Some(o), .. } => kids.push(o),
968        Expr::Method {
969            owner: Some(o),
970            args,
971            ..
972        } => {
973            kids.push(o);
974            kids.extend(args.iter());
975        }
976        Expr::ArrayIndex { array, index } => {
977            kids.push(array);
978            kids.push(index);
979        }
980        Expr::New { args, .. } => kids.extend(args.iter()),
981        Expr::NewArray { dims, init, .. } => {
982            kids.extend(dims.iter());
983            if let Some(v) = init {
984                kids.extend(v.iter());
985            }
986        }
987        Expr::NewMultiArray { dims, .. } => kids.extend(dims.iter()),
988        _ => {}
989    }
990    for k in kids {
991        visit_phi_refs(k, phis, hit);
992        if *hit {
993            return;
994        }
995    }
996}
997
998/// Merge several predecessor register states: pass identical values
999/// through, otherwise materialize into a phi var for `(block, register)`.
1000fn merge_states(
1001    vt: &mut VarTable,
1002    phis: &mut HashMap<(usize, u16), u32>,
1003    bid: usize,
1004    sides: &[&[Reg]],
1005) -> Vec<Reg> {
1006    let len = sides.iter().map(|s| s.len()).max().unwrap_or(0);
1007    let mut out = vec![Reg::Undef; len];
1008    for (r, slot) in out.iter_mut().enumerate() {
1009        let mut all_eq = true;
1010        let mut first: Option<&Reg> = None;
1011        for s in sides {
1012            let v = s.get(r);
1013            match (first, v) {
1014                (None, v) => first = v,
1015                (Some(f), Some(v)) if f == v => {}
1016                _ => all_eq = false,
1017            }
1018        }
1019        if all_eq {
1020            *slot = first.cloned().unwrap_or(Reg::Undef);
1021            continue;
1022        }
1023        // Diverged: phi var.
1024        let key = (bid, r as u16);
1025        let phi = match phis.get(&key) {
1026            Some(&v) => v,
1027            None => {
1028                let ty = join_side_types(vt, sides, r);
1029                let id = vt.vars.len() as u32;
1030                let name = format!("v{}", id);
1031                vt.vars.push(jdc_core::var::VarInfo {
1032                    id,
1033                    slot: r as u16,
1034                    name,
1035                    ty,
1036                    is_param: false,
1037                    range_start: 0,
1038                    range_end: u16::MAX,
1039                    synthetic_name: true,
1040                });
1041                while vt.by_slot.len() <= r {
1042                    vt.by_slot.push(Vec::new());
1043                }
1044                vt.by_slot[r].push((0, u16::MAX, id));
1045                phis.insert(key, id);
1046                id
1047            }
1048        };
1049        *slot = Reg::Live(phi);
1050    }
1051    out
1052}
1053
1054/// Type of a phi var: join of the non-null side values' erased types.
1055fn join_side_types(vt: &VarTable, sides: &[&[Reg]], r: usize) -> TypeRef {
1056    let mut ty: Option<JavaType> = None;
1057    for s in sides {
1058        let st = s.get(r).cloned().unwrap_or(Reg::Undef);
1059        let e = match st {
1060            Reg::Pending(e) | Reg::PendingCall(e) => e,
1061            Reg::Live(v) => Expr::Local {
1062                var: v,
1063                ty: vt.var(v).ty.clone(),
1064            },
1065            _ => continue,
1066        };
1067        let t = e.type_ref().erased();
1068        ty = Some(match ty {
1069            None => t,
1070            Some(prev) => join_types(&prev, &t),
1071        });
1072    }
1073    TypeRef::J(ty.unwrap_or(JavaType::Int))
1074}
1075
1076fn join_types(a: &JavaType, b: &JavaType) -> JavaType {
1077    if a == b {
1078        return a.clone();
1079    }
1080    let num = |t: &JavaType| {
1081        matches!(
1082            t,
1083            JavaType::Boolean
1084                | JavaType::Byte
1085                | JavaType::Char
1086                | JavaType::Short
1087                | JavaType::Int
1088                | JavaType::Long
1089                | JavaType::Float
1090                | JavaType::Double
1091        )
1092    };
1093    if num(a) && num(b) {
1094        // Numeric promotion.
1095        return match (a, b) {
1096            (JavaType::Double, _) | (_, JavaType::Double) => JavaType::Double,
1097            (JavaType::Float, _) | (_, JavaType::Float) => JavaType::Float,
1098            (JavaType::Long, _) | (_, JavaType::Long) => JavaType::Long,
1099            _ => JavaType::Int,
1100        };
1101    }
1102    if let (JavaType::Object(x), JavaType::Object(y)) = (a, b) {
1103        if x == y {
1104            return a.clone();
1105        }
1106        // Char/int ternaries join to int; otherwise objects join to Object.
1107        return JavaType::Object("java/lang/Object".into());
1108    }
1109    JavaType::Object("java/lang/Object".into())
1110}
1111
1112/// True when the block's terminator exits (used by callers to skip appends).
1113#[allow(dead_code)]
1114fn is_exit_term(term: &jdc_core::ir::build::Term) -> bool {
1115    matches!(
1116        term,
1117        jdc_core::ir::build::Term::Return(_) | jdc_core::ir::build::Term::Throw(_)
1118    )
1119}
1120
1121#[allow(dead_code)]
1122fn unused(_: &InsnKind) {}