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