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