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