Skip to main content

ddc_dec/
passes.rs

1//! Post-convert refinement passes for DEX-lifted statement trees.
2//!
3//! The converter hands back a correct-but-raw tree; these passes restore the
4//! source shapes a register machine loses: catch parameter binding,
5//! copy-forwarding (single-use temporaries), if/else→ternary folding,
6//! `StringBuilder` chain → `+` concatenation, `synchronized` recovery from
7//! the d8 monitor pattern, erased-type inference, boolean and null
8//! comparisons, and declaration hygiene.
9
10// The tree-walker match arms intentionally mirror the statement grammar
11// one level at a time; collapsing the nested `if let`s into outer match
12// arms would trade per-arm clarity for lint silence.
13#![allow(clippy::collapsible_match)]
14
15use jdc_core::FxHashSet as HashSet;
16
17// The tree-walker match arms intentionally mirror the statement grammar
18// one level at a time; collapsing the nested `if let`s into outer match
19// arms would trade per-arm clarity for lint silence.
20use jdc_core::ir::build::has_side_effects;
21use jdc_core::emit::is_java_keyword;
22use jdc_core::ir::expr::{AssignOp, BinOp, ConcatPart, ConstVal, Expr, TypeRef, UnOp};
23use jdc_core::ir::stmt::{CaseGroup, Catch, Stmt};
24use jdc_core::types::{JavaType, MethodDescriptor};
25use jdc_core::var::VarTable;
26
27use crate::lift::MethodEnv;
28use crate::{access, desc_type, DexPool};
29use ddc_dex::insn::{Insn, InsnKind, InvokeKind};
30use jdc_core::types::parse_method_descriptor;
31
32// ---------------------------------------------------------------------------
33// Generic tree walking
34// ---------------------------------------------------------------------------
35
36/// Deep expression rewrite over a statement tree.
37pub fn rewrite_exprs<F: FnMut(&mut Expr)>(s: &mut Stmt, f: &mut F) {
38    walk_stmt_exprs(s, f);
39}
40
41pub(crate) fn walk_stmt_exprs<F: FnMut(&mut Expr)>(s: &mut Stmt, f: &mut F) {
42    match s {
43        Stmt::Block(v) => {
44            for x in v.iter_mut() {
45                walk_stmt_exprs(x, f);
46            }
47        }
48        Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
49            f(e);
50        }
51        Stmt::Return(Some(e)) => f(e),
52        Stmt::LocalDef { init, .. } => {
53            if let Some(e) = init {
54                f(e);
55            }
56        }
57        Stmt::If {
58            cond,
59            then_stmt,
60            else_stmt,
61        } => {
62            f(cond);
63            walk_stmt_exprs(then_stmt, f);
64            if let Some(e) = else_stmt {
65                walk_stmt_exprs(e, f);
66            }
67        }
68        Stmt::While { cond, body } => {
69            f(cond);
70            walk_stmt_exprs(body, f);
71        }
72        Stmt::DoWhile { body, cond } => {
73            walk_stmt_exprs(body, f);
74            f(cond);
75        }
76        Stmt::For {
77            init,
78            cond,
79            update,
80            body,
81        } => {
82            for x in init.iter_mut() {
83                walk_stmt_exprs(x, f);
84            }
85            if let Some(c) = cond {
86                f(c);
87            }
88            for u in update.iter_mut() {
89                f(u);
90            }
91            walk_stmt_exprs(body, f);
92        }
93        Stmt::ForEach { iterable, body, .. } => {
94            f(iterable);
95            walk_stmt_exprs(body, f);
96        }
97        Stmt::Switch {
98            selector,
99            cases,
100            default,
101            ..
102        } => {
103            f(selector);
104            for c in cases {
105                for x in c.body.iter_mut() {
106                    walk_stmt_exprs(x, f);
107                }
108                if let Some(g) = &mut c.guard {
109                    f(g);
110                }
111            }
112            if let Some(d) = default {
113                walk_stmt_exprs(d, f);
114            }
115        }
116        Stmt::Try {
117            body,
118            catches,
119            finally,
120        } => {
121            walk_stmt_exprs(body, f);
122            for c in catches {
123                walk_stmt_exprs(&mut c.body, f);
124            }
125            if let Some(fl) = finally {
126                walk_stmt_exprs(fl, f);
127            }
128        }
129        Stmt::TryWithResources {
130            resources,
131            body,
132            catches,
133            finally,
134        } => {
135            for x in resources.iter_mut() {
136                walk_stmt_exprs(x, f);
137            }
138            walk_stmt_exprs(body, f);
139            for c in catches {
140                walk_stmt_exprs(&mut c.body, f);
141            }
142            if let Some(fl) = finally {
143                walk_stmt_exprs(fl, f);
144            }
145        }
146        Stmt::Assert { cond, msg } => {
147            f(cond);
148            if let Some(m) = msg {
149                f(m);
150            }
151        }
152        Stmt::Synchronized { lock, body } => {
153            f(lock);
154            walk_stmt_exprs(body, f);
155        }
156        Stmt::TernaryValue { e } => f(e),
157        Stmt::Labeled { body, .. } => walk_stmt_exprs(body, f),
158        Stmt::Goto(_) | Stmt::Label(_) | Stmt::Break(_) | Stmt::Continue(_) => {}
159        _ => {}
160    }
161}
162
163/// Visit every expression (immutable).
164fn visit_exprs<F: FnMut(&Expr)>(e: &Expr, f: &mut F) {
165    f(e);
166    for_each_child(e, &mut |c| visit_exprs(c, f));
167}
168
169fn for_each_child<F: FnMut(&Expr)>(e: &Expr, f: &mut F) {
170    match e {
171        Expr::Un { e, .. } | Expr::Cast { e, .. } | Expr::InstanceOf { e, .. } => f(e),
172        Expr::Bin { l, r, .. } => {
173            f(l);
174            f(r);
175        }
176        Expr::Cond { c, t, f: fe } => {
177            f(c);
178            f(t);
179            f(fe);
180        }
181        Expr::Assign { target, value, .. } => {
182            f(target);
183            f(value);
184        }
185        Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => f(e),
186        Expr::Field { owner, .. } => {
187            if let Some(o) = owner {
188                f(o);
189            }
190        }
191        Expr::Method { owner, args, .. } => {
192            if let Some(o) = owner {
193                f(o);
194            }
195            for a in args {
196                f(a);
197            }
198        }
199        Expr::ArrayIndex { array, index } => {
200            f(array);
201            f(index);
202        }
203        Expr::New { args, .. } => {
204            for a in args {
205                f(a);
206            }
207        }
208        Expr::NewArray { dims, init, .. } => {
209            for d in dims {
210                f(d);
211            }
212            if let Some(v) = init {
213                for x in v {
214                    f(x);
215                }
216            }
217        }
218        Expr::NewMultiArray { dims, .. } => {
219            for d in dims {
220                f(d);
221            }
222        }
223        Expr::StringConcat(parts) => {
224            for p in parts {
225                if let ConcatPart::Str(x) = p {
226                    f(x);
227                }
228            }
229        }
230        Expr::Invokedynamic { args, .. } => {
231            for a in args {
232                f(a);
233            }
234        }
235        Expr::AnonNew { args, .. } => {
236            for a in args {
237                f(a);
238            }
239        }
240        _ => {}
241    }
242}
243
244/// Mutable child walk (one level).
245fn for_each_child_mut<F: FnMut(&mut Expr)>(e: &mut Expr, f: &mut F) {
246    match e {
247        Expr::Un { e, .. } | Expr::Cast { e, .. } | Expr::InstanceOf { e, .. } => f(e),
248        Expr::Bin { l, r, .. } => {
249            f(l);
250            f(r);
251        }
252        Expr::Cond { c, t, f: fe } => {
253            f(c);
254            f(t);
255            f(fe);
256        }
257        Expr::Assign { target, value, .. } => {
258            f(target);
259            f(value);
260        }
261        Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => f(e),
262        Expr::Field { owner, .. } => {
263            if let Some(o) = owner {
264                f(o);
265            }
266        }
267        Expr::Method { owner, args, .. } => {
268            if let Some(o) = owner {
269                f(o);
270            }
271            for a in args.iter_mut() {
272                f(a);
273            }
274        }
275        Expr::ArrayIndex { array, index } => {
276            f(array);
277            f(index);
278        }
279        Expr::New { args, .. } => {
280            for a in args.iter_mut() {
281                f(a);
282            }
283        }
284        Expr::NewArray { dims, init, .. } => {
285            for d in dims.iter_mut() {
286                f(d);
287            }
288            if let Some(v) = init {
289                for x in v.iter_mut() {
290                    f(x);
291                }
292            }
293        }
294        Expr::NewMultiArray { dims, .. } => {
295            for d in dims.iter_mut() {
296                f(d);
297            }
298        }
299        Expr::StringConcat(parts) => {
300            for p in parts {
301                if let ConcatPart::Str(x) = p {
302                    f(x);
303                }
304            }
305        }
306        Expr::Invokedynamic { args, .. } => {
307            for a in args.iter_mut() {
308                f(a);
309            }
310        }
311        Expr::AnonNew { args, .. } => {
312            for a in args.iter_mut() {
313                f(a);
314            }
315        }
316        _ => {}
317    }
318}
319
320/// Deep mutable expression rewrite.
321pub(crate) fn deep_rewrite<F: FnMut(&mut Expr)>(e: &mut Expr, f: &mut F) {
322    f(e);
323    for_each_child_mut(e, &mut |c| deep_rewrite(c, f));
324}
325
326/// `deep_rewrite` that respects WRITE positions: a `Local` appearing as
327/// an assignment target or a `++`/`--` operand never reaches `f`.
328/// Value-forwarding closures (`*x = value`) must use this — plain
329/// deep_rewrite turned a forwarded `vX = 25` into `25 = 25` (target
330/// replaced, and drop_defs then no longer recognized the statement, so
331/// the garbage survived into the output; reqable a4/e). Compound
332/// targets (`a[i] = …`, `o.f = …`) still recurse: owners and indices
333/// ARE reads.
334fn deep_rewrite_reads<F: FnMut(&mut Expr)>(e: &mut Expr, f: &mut F) {
335    f(e);
336    match e {
337        Expr::Assign { target, value, .. } => {
338            if !matches!(**target, Expr::Local { .. }) {
339                deep_rewrite_reads(target, f);
340            }
341            deep_rewrite_reads(value, f);
342        }
343        Expr::PreIncDec { e: inner, .. } | Expr::PostIncDec { e: inner, .. } => {
344            if !matches!(**inner, Expr::Local { .. }) {
345                deep_rewrite_reads(inner, f);
346            }
347        }
348        _ => for_each_child_mut(e, &mut |c| deep_rewrite_reads(c, f)),
349    }
350}
351
352fn collect_vars(e: &Expr, out: &mut HashSet<u32>) {
353    visit_exprs(e, &mut |x| {
354        if let Expr::Local { var, .. } = x {
355            out.insert(*var);
356        }
357    });
358}
359
360/// Collect variable references (reads; `assignments` adds assignment
361/// targets). Single-pass recursion — the previous shape wrapped a
362/// self-recursive closure in `walk_all`, visiting every subtree twice over
363/// (exponential in nesting depth).
364fn stmt_collect_vars(s: &Stmt, out: &mut HashSet<u32>, assignments: bool) {
365    match s {
366        Stmt::Block(v) => {
367            for x in v {
368                stmt_collect_vars(x, out, assignments);
369            }
370        }
371        Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
372            if let Expr::Local { var, .. } = &**target {
373                if assignments {
374                    out.insert(*var);
375                }
376            } else {
377                collect_vars(target, out);
378            }
379            collect_vars(value, out);
380        }
381        Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
382            collect_vars(e, out)
383        }
384        Stmt::Return(Some(e)) => collect_vars(e, out),
385        Stmt::LocalDef { var, init, .. } => {
386            if assignments {
387                out.insert(*var);
388            }
389            if let Some(e) = init {
390                collect_vars(e, out);
391            }
392        }
393        Stmt::If {
394            cond,
395            then_stmt,
396            else_stmt,
397        } => {
398            collect_vars(cond, out);
399            stmt_collect_vars(then_stmt, out, assignments);
400            if let Some(e) = else_stmt {
401                stmt_collect_vars(e, out, assignments);
402            }
403        }
404        Stmt::While { cond, body } => {
405            collect_vars(cond, out);
406            stmt_collect_vars(body, out, assignments);
407        }
408        Stmt::DoWhile { body, cond } => {
409            stmt_collect_vars(body, out, assignments);
410            collect_vars(cond, out);
411        }
412        Stmt::For {
413            init,
414            cond,
415            update,
416            body,
417        } => {
418            for x in init {
419                stmt_collect_vars(x, out, assignments);
420            }
421            if let Some(c) = cond {
422                collect_vars(c, out);
423            }
424            for u in update {
425                collect_vars(u, out);
426            }
427            stmt_collect_vars(body, out, assignments);
428        }
429        Stmt::ForEach {
430            var,
431            iterable,
432            body,
433            ..
434        } => {
435            if assignments {
436                out.insert(*var);
437            }
438            collect_vars(iterable, out);
439            stmt_collect_vars(body, out, assignments);
440        }
441        Stmt::Switch {
442            selector,
443            cases,
444            default,
445            ..
446        } => {
447            collect_vars(selector, out);
448            for c in cases {
449                for x in &c.body {
450                    stmt_collect_vars(x, out, assignments);
451                }
452            }
453            if let Some(d) = default {
454                stmt_collect_vars(d, out, assignments);
455            }
456        }
457        Stmt::Try {
458            body,
459            catches,
460            finally,
461        } => {
462            stmt_collect_vars(body, out, assignments);
463            for c in catches {
464                stmt_collect_vars(&c.body, out, assignments);
465            }
466            if let Some(f) = finally {
467                stmt_collect_vars(f, out, assignments);
468            }
469        }
470        Stmt::TryWithResources {
471            resources,
472            body,
473            catches,
474            finally,
475        } => {
476            for x in resources {
477                stmt_collect_vars(x, out, assignments);
478            }
479            stmt_collect_vars(body, out, assignments);
480            for c in catches {
481                stmt_collect_vars(&c.body, out, assignments);
482            }
483            if let Some(f) = finally {
484                stmt_collect_vars(f, out, assignments);
485            }
486        }
487        Stmt::Synchronized { lock, body } => {
488            collect_vars(lock, out);
489            stmt_collect_vars(body, out, assignments);
490        }
491        Stmt::Assert { cond, msg } => {
492            collect_vars(cond, out);
493            if let Some(m) = msg {
494                collect_vars(m, out);
495            }
496        }
497        Stmt::Labeled { body, .. } => stmt_collect_vars(body, out, assignments),
498        _ => {}
499    }
500}
501
502fn walk_all<F: FnMut(&Stmt)>(s: &Stmt, f: &mut F) {
503    f(s);
504    match s {
505        Stmt::Block(v) => {
506            for x in v {
507                walk_all(x, f);
508            }
509        }
510        Stmt::If {
511            then_stmt,
512            else_stmt,
513            ..
514        } => {
515            walk_all(then_stmt, f);
516            if let Some(e) = else_stmt {
517                walk_all(e, f);
518            }
519        }
520        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => walk_all(body, f),
521        Stmt::For { init, body, .. } => {
522            for x in init {
523                walk_all(x, f);
524            }
525            walk_all(body, f);
526        }
527        Stmt::ForEach { body, .. } => walk_all(body, f),
528        Stmt::Switch { cases, default, .. } => {
529            for c in cases {
530                for x in &c.body {
531                    walk_all(x, f);
532                }
533            }
534            if let Some(d) = default {
535                walk_all(d, f);
536            }
537        }
538        Stmt::Try {
539            body,
540            catches,
541            finally,
542        } => {
543            walk_all(body, f);
544            for c in catches {
545                walk_all(&c.body, f);
546            }
547            if let Some(fl) = finally {
548                walk_all(fl, f);
549            }
550        }
551        Stmt::TryWithResources {
552            resources,
553            body,
554            catches,
555            finally,
556        } => {
557            for x in resources {
558                walk_all(x, f);
559            }
560            walk_all(body, f);
561            for c in catches {
562                walk_all(&c.body, f);
563            }
564            if let Some(fl) = finally {
565                walk_all(fl, f);
566            }
567        }
568        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => walk_all(body, f),
569        _ => {}
570    }
571}
572
573// ---------------------------------------------------------------------------
574// Passes
575// ---------------------------------------------------------------------------
576
577/// Bind catch parameters: the handler's first statement (a bare LocalDef
578/// from move-exception) becomes the catch variable.
579pub fn bind_catches(s: &mut Stmt, vt: &mut VarTable) {
580    // Vars defined anywhere in the method (params + LocalDefs +
581    // assign targets): a catch body reading a var OUTSIDE this set is
582    // the unmaterialized move-exception register (the lifter minted it
583    // without a defining statement).
584    let mut defined: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
585    for v in &vt.vars {
586        if v.is_param {
587            defined.insert(v.id);
588        }
589    }
590    collect_defined_locals(s, &mut defined);
591    let reads_all = count_locals_stmts(std::slice::from_ref(s));
592    // Vars with a real assignment (LocalDef-with-init or assign target):
593    // a var only ever READ is either the unmaterialized move-exception
594    // register or a bare declaration awaiting its hoisted assignment.
595    let mut assigned: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
596    collect_assigned_locals(s, &mut assigned);
597    let mut fallback_bound: Vec<u32> = Vec::new();
598    bind_catches_walk(s, vt, &defined, &reads_all, &assigned, &mut fallback_bound);
599    // The catch parameter IS the declaration now: drop the bare
600    // `Throwable th;` hoists for vars the fallback bound.
601    if !fallback_bound.is_empty() {
602        remove_bare_decls(s, &fallback_bound);
603    }
604}
605
606fn collect_defined_locals(s: &Stmt, out: &mut jdc_core::FxHashSet<u32>) {
607    crate::passes::walk_all(s, &mut |st| {
608        match st {
609            Stmt::LocalDef { var, .. } => {
610                out.insert(*var);
611            }
612            Stmt::ExprStmt(Expr::Assign { target, .. }) => {
613                if let Expr::Local { var, .. } = &**target {
614                    out.insert(*var);
615                }
616            }
617            _ => {}
618        }
619    });
620}
621
622
623/// First read of a local that is defined nowhere in the method (and is
624/// not a parameter) inside the catch body — the unmaterialized
625/// move-exception register.
626/// Vars carrying a real assignment (init or write target).
627fn collect_assigned_locals(s: &Stmt, out: &mut jdc_core::FxHashSet<u32>) {
628    crate::passes::walk_all(s, &mut |st| {
629        match st {
630            Stmt::LocalDef { var, init: Some(_), .. } => {
631                out.insert(*var);
632            }
633            Stmt::ExprStmt(Expr::Assign { target, .. }) => {
634                if let Expr::Local { var, .. } = &**target {
635                    out.insert(*var);
636                }
637            }
638            _ => {}
639        }
640    });
641}
642
643/// First `throw <local>` var in the catch body that is never assigned
644/// anywhere and is exception-typed — the unmaterialized move-exception.
645fn first_thrown_unassigned_local(
646    body: &Stmt,
647    vt: &VarTable,
648    assigned: &jdc_core::FxHashSet<u32>,
649) -> Option<u32> {
650    let mut found: Option<u32> = None;
651    let c = body.clone();
652    walk_all(&c, &mut |st| {
653        if let Stmt::Throw(th) = st {
654            if let Expr::Local { var, .. } = th {
655                let v = *var;
656                if !assigned.contains(&v) {
657                    if let JavaType::Object(o) = vt.var(v).ty.erased() {
658                        if o.as_ref() == "java/lang/Throwable" {
659                            found = Some(v);
660                        }
661                    }
662                }
663            }
664        }
665    });
666    found
667}
668
669/// Remove bare `LocalDef{var, init: None}` declarations for the given
670/// vars (the catch parameter is their declaration now).
671fn remove_bare_decls(s: &mut Stmt, vars: &[u32]) {
672    let vars: jdc_core::FxHashSet<u32> = vars.iter().copied().collect();
673    walk_mut_deep(s, &mut |st| {
674        if let Stmt::Block(v) = st {
675            v.retain(|x| {
676                !matches!(x, Stmt::LocalDef { var, init: None, .. } if vars.contains(var))
677            });
678        }
679    });
680}
681
682fn first_undefined_local_read(
683    body: &Stmt,
684    defined: &jdc_core::FxHashSet<u32>,
685) -> Option<u32> {
686    let mut found: Option<u32> = None;
687    let mut c = body.clone();
688    crate::passes::walk_stmt_exprs(&mut c, &mut |e| {
689        if found.is_none() {
690            deep_rewrite(e, &mut |x| {
691                if found.is_none() {
692                    if let Expr::Local { var, .. } = x {
693                        if !defined.contains(var) {
694                            found = Some(*var);
695                        }
696                    }
697                }
698            });
699        }
700    });
701    found
702}
703
704#[allow(clippy::too_many_arguments)]
705fn bind_catches_walk(
706    s: &mut Stmt,
707    vt: &mut VarTable,
708    defined: &jdc_core::FxHashSet<u32>,
709    reads_all: &std::collections::HashMap<u32, usize>,
710    assigned: &jdc_core::FxHashSet<u32>,
711    fallback_bound: &mut Vec<u32>,
712) {
713    match s {
714        Stmt::Try {
715            body,
716            catches,
717            finally,
718        } => {
719            bind_catches_walk(body, vt, defined, reads_all, assigned, fallback_bound);
720            for c in catches.iter_mut() {
721                if c.var == u32::MAX {
722                    let stored = match c.body.as_ref() {
723                        Stmt::Block(v) => match v.first() {
724                            Some(Stmt::LocalDef { var, .. }) => Some(*var),
725                            Some(Stmt::ExprStmt(Expr::Assign { target, .. })) => match &**target {
726                                Expr::Local { var, .. } => Some(*var),
727                                _ => None,
728                            },
729                            _ => None,
730                        },
731                        _ => None,
732                    };
733                    if let Some(v) = stored {
734                        let exc = c
735                            .exc
736                            .first()
737                            .cloned()
738                            .unwrap_or_else(|| "java/lang/Throwable".into());
739                        let slot = vt.var(v).slot;
740                        let name = "e".to_string();
741                        let new_var =
742                            vt.add_catch_var(slot, name, TypeRef::J(JavaType::Object(exc)));
743                        if let Stmt::Block(vs) = c.body.as_mut() {
744                            vs.remove(0);
745                        }
746                        rewrite_local_refs(c.body.as_mut(), v, new_var);
747                        c.var = new_var;
748                    } else if let Some(v) = first_undefined_local_read(c.body.as_ref(), defined)
749                        .filter(|v| {
750                            // Bind only a var read NOWHERE outside this
751                            // catch: ensure_declared would hoist an
752                            // outside-read var to a method local, and
753                            // consuming it as the catch parameter would
754                            // scope it too narrowly.
755                            let in_catch = count_locals_stmts(std::slice::from_ref(c.body.as_ref()));
756                            reads_all.get(v).copied().unwrap_or(0)
757                                == in_catch.get(v).copied().unwrap_or(0)
758                        })
759                        .filter(|v| {
760                            // And only an EXCEPTION-typed var: the
761                            // move-exception registers carry the handler
762                            // type — a plain local pending its hoisted
763                            // declaration (StringBuilder sb) must not be
764                            // consumed as the catch parameter (reqable
765                            // amazon: the declaration vanished and its
766                            // outer readers broke).
767                            let ty = vt.var(*v).ty.erased();
768                            let exc = c
769                                .exc
770                                .first()
771                                .cloned()
772                                .unwrap_or_else(|| "java/lang/Throwable".into());
773                            matches!(&ty, JavaType::Object(o)
774                                if o.as_ref() == exc.as_ref()
775                                    || o.as_ref() == "java/lang/Throwable")
776                        })
777                    {
778                        // The handler's first statement is not the
779                        // move-exception def (the d8 synchronized pattern
780                        // leads with MonitorExit): the register the lifter
781                        // minted for move-exception is still READ in the
782                        // body (`throw th;`) with no defining statement
783                        // anywhere. Bind IT as the catch parameter —
784                        // otherwise emit printed `catch (Throwable ignored)`
785                        // over an undeclared `throw th` (definite-assignment
786                        // failure, 6.4k weibo sites).
787                        c.var = v;
788                        fallback_bound.push(v);
789                    } else if let Some(v) =
790                        first_thrown_unassigned_local(c.body.as_ref(), vt, assigned)
791                            .filter(|v| {
792                                let in_catch =
793                                    count_locals_stmts(std::slice::from_ref(c.body.as_ref()));
794                                reads_all.get(v).copied().unwrap_or(0)
795                                    == in_catch.get(v).copied().unwrap_or(0)
796                            })
797                            .filter(|v| {
798                                // Same exception-type gate: the hoisted
799                                // `Throwable th;` declares the var but the
800                                // move-exception never assigned it.
801                                let ty = vt.var(*v).ty.erased();
802                                let exc = c
803                                    .exc
804                                    .first()
805                                    .cloned()
806                                    .unwrap_or_else(|| "java/lang/Throwable".into());
807                                matches!(&ty, JavaType::Object(o)
808                                    if o.as_ref() == exc.as_ref()
809                                        || o.as_ref() == "java/lang/Throwable")
810                            })
811                    {
812                        // Declared-never-assigned: `Throwable th;` hoisted
813                        // by an earlier phase, the catch body's `throw th`
814                        // its only use — the move-exception assignment was
815                        // never materialized. Bind th as the catch
816                        // parameter and drop the bare declaration.
817                        c.var = v;
818                        fallback_bound.push(v);
819                    }
820                }
821                bind_catches_walk(&mut c.body, vt, defined, reads_all, assigned, fallback_bound);
822            }
823            if let Some(f) = finally {
824                bind_catches_walk(f.as_mut(), vt, defined, reads_all, assigned, fallback_bound);
825            }
826        }
827        Stmt::Block(v) => {
828            for x in v.iter_mut() {
829                bind_catches_walk(x, vt, defined, reads_all, assigned, fallback_bound);
830            }
831        }
832        Stmt::If {
833            then_stmt,
834            else_stmt,
835            ..
836        } => {
837            bind_catches_walk(then_stmt, vt, defined, reads_all, assigned, fallback_bound);
838            if let Some(e) = else_stmt {
839                bind_catches_walk(e, vt, defined, reads_all, assigned, fallback_bound);
840            }
841        }
842        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => bind_catches_walk(body, vt, defined, reads_all, assigned, fallback_bound),
843        Stmt::For { init, body, .. } => {
844            for x in init.iter_mut() {
845                bind_catches_walk(x, vt, defined, reads_all, assigned, fallback_bound);
846            }
847            bind_catches_walk(body, vt, defined, reads_all, assigned, fallback_bound);
848        }
849        Stmt::ForEach { body, .. }
850        | Stmt::Labeled { body, .. }
851        | Stmt::Synchronized { body, .. } => bind_catches_walk(body, vt, defined, reads_all, assigned, fallback_bound),
852        Stmt::Switch { cases, default, .. } => {
853            for c in cases.iter_mut() {
854                for x in c.body.iter_mut() {
855                    bind_catches_walk(x, vt, defined, reads_all, assigned, fallback_bound);
856                }
857            }
858            if let Some(d) = default {
859                bind_catches_walk(d, vt, defined, reads_all, assigned, fallback_bound);
860            }
861        }
862        _ => {}
863    }
864}
865
866fn rewrite_local_refs(s: &mut Stmt, from: u32, to: u32) {
867    rewrite_exprs(s, &mut |e| {
868        deep_rewrite(e, &mut |x| {
869            if let Expr::Local { var, .. } = x {
870                if *var == from {
871                    *var = to;
872                }
873            }
874        });
875    });
876}
877
878/// Flatten blocks, drop empty ones and trailing no-op statements.
879pub fn cleanup(s: &mut Stmt) {
880    jdc_core::ir::stmt::flatten(s);
881    strip_empty(s);
882    merge_singleton_stmts(s);
883}
884
885fn strip_empty(s: &mut Stmt) {
886    match s {
887        Stmt::Block(v) => {
888            v.retain(|x| !x.is_empty_block());
889            for x in v.iter_mut() {
890                strip_empty(x);
891            }
892        }
893        Stmt::If {
894            then_stmt,
895            else_stmt,
896            ..
897        } => {
898            strip_empty(then_stmt);
899            if let Some(e) = else_stmt {
900                strip_empty(e);
901                if e.is_empty_block() {
902                    *else_stmt = None;
903                }
904            }
905        }
906        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => strip_empty(body),
907        Stmt::For { init, body, .. } => {
908            init.retain(|x| !x.is_empty_block());
909            strip_empty(body);
910        }
911        Stmt::ForEach { body, .. } => strip_empty(body),
912        Stmt::Switch { cases, default, .. } => {
913            for c in cases {
914                c.body.retain(|x| !x.is_empty_block());
915            }
916            if let Some(d) = default {
917                strip_empty(d);
918            }
919        }
920        Stmt::Try {
921            body,
922            catches,
923            finally,
924        } => {
925            strip_empty(body);
926            for c in catches {
927                strip_empty(&mut c.body);
928            }
929            if let Some(f) = finally {
930                strip_empty(f);
931            }
932        }
933        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => strip_empty(body),
934        _ => {}
935    }
936}
937
938fn merge_singleton_stmts(s: &mut Stmt) {
939    if let Stmt::Block(v) = s {
940        for x in v.iter_mut() {
941            merge_singleton_stmts(x);
942        }
943    }
944}
945
946/// Drop statements after a definite terminator within a block.
947pub fn prune_unreachable(s: &mut Stmt) {
948    if let Stmt::Block(v) = s {
949        let mut cut: Option<usize> = None;
950        for (i, x) in v.iter().enumerate() {
951            if matches!(x, Stmt::Return(_) | Stmt::Throw(_)) {
952                cut = Some(i + 1);
953                break;
954            }
955        }
956        if let Some(c) = cut {
957            if c < v.len() {
958                v.truncate(c);
959            }
960        }
961        for x in v.iter_mut() {
962            prune_unreachable(x);
963        }
964    } else {
965        walk_mut(s, &mut prune_unreachable);
966    }
967}
968
969fn walk_mut<F: FnMut(&mut Stmt)>(s: &mut Stmt, f: &mut F) {
970    match s {
971        Stmt::Block(v) => {
972            for x in v.iter_mut() {
973                f(x);
974            }
975        }
976        Stmt::If {
977            then_stmt,
978            else_stmt,
979            ..
980        } => {
981            f(then_stmt);
982            if let Some(e) = else_stmt {
983                f(e);
984            }
985        }
986        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => f(body),
987        Stmt::For { init, body, .. } => {
988            for x in init.iter_mut() {
989                f(x);
990            }
991            f(body);
992        }
993        Stmt::ForEach { body, .. } => f(body),
994        Stmt::Switch { cases, default, .. } => {
995            for c in cases {
996                for x in c.body.iter_mut() {
997                    f(x);
998                }
999            }
1000            if let Some(d) = default {
1001                f(d);
1002            }
1003        }
1004        Stmt::Try {
1005            body,
1006            catches,
1007            finally,
1008        } => {
1009            f(body);
1010            for c in catches {
1011                f(&mut c.body);
1012            }
1013            if let Some(fl) = finally {
1014                f(fl);
1015            }
1016        }
1017        Stmt::TryWithResources {
1018            resources,
1019            body,
1020            catches,
1021            finally,
1022        } => {
1023            for r in resources.iter_mut() {
1024                f(r);
1025            }
1026            f(body);
1027            for c in catches {
1028                f(&mut c.body);
1029            }
1030            if let Some(fl) = finally {
1031                f(fl);
1032            }
1033        }
1034        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => f(body),
1035        _ => {}
1036    }
1037}
1038
1039/// Count LocalDef statements (pipeline diagnostics).
1040pub fn count_localdefs(s: &Stmt, out: &mut usize) {
1041    if matches!(s, Stmt::LocalDef { .. }) {
1042        *out += 1;
1043    }
1044    walk_all(s, &mut |x| {
1045        if matches!(x, Stmt::LocalDef { .. }) {
1046            *out += 1;
1047        }
1048    });
1049}
1050
1051/// Drop a trailing `return;` (d8's explicit return-void at method end).
1052pub fn strip_trailing_void_return(s: &mut Stmt) {
1053    if let Stmt::Block(v) = s {
1054        loop {
1055            match v.last_mut() {
1056                Some(Stmt::Return(None)) => {
1057                    v.pop();
1058                }
1059                // The structurer can close a region in a bare nested
1060                // block; a `return;` at its tail is still a dangling
1061                // statement (inside a static initializer it is illegal
1062                // outright — clinit's closing return).
1063                Some(Stmt::Block(_)) => {
1064                    let last = v.last_mut().unwrap();
1065                    let before = match last {
1066                        Stmt::Block(b) => b.len(),
1067                        _ => 0,
1068                    };
1069                    strip_trailing_void_return(last);
1070                    let after = match last {
1071                        Stmt::Block(b) => b.len(),
1072                        _ => 0,
1073                    };
1074                    if after == before {
1075                        break; // nothing stripped inside
1076                    }
1077                }
1078                _ => break,
1079            }
1080        }
1081    }
1082}
1083
1084/// `if (c) { } else { B }` → `if (!c) { B }` — the empty-then shape is
1085/// how the structurer lands an inverted diamond, and source never writes
1086/// it (jadx prints the positive form). negate() carries De Morgan so
1087/// composed conditions stay clean.
1088pub fn invert_empty_thens(s: &mut Stmt) {
1089    walk_mut_deep(s, &mut |st| {
1090        if let Stmt::If {
1091            cond,
1092            then_stmt,
1093            else_stmt,
1094        } = st
1095        {
1096            let then_empty = matches!(&**then_stmt, Stmt::Block(v) if v.is_empty());
1097            if then_empty {
1098                if let Some(els) = else_stmt.take() {
1099                    let c = std::mem::replace(cond, Expr::Const(ConstVal::Int(0)));
1100                    *cond = jdc_core::convert::negate(c);
1101                    *then_stmt = els;
1102                }
1103            }
1104        }
1105    });
1106}
1107
1108/// Fold if-diamonds back into short-circuit conditions (DAD's
1109/// short_circuit_struct, statement level). The structurer emits nested
1110/// ifs for `a && b` / `a || b` bytecode diamonds; javac source almost
1111/// never nests them, and jadx folds all four shapes:
1112///   if (c1) { if (c2) {T} else {F} } else {F}  →  if (c1 && c2) {T} else {F}
1113///   if (c1) { if (c2) {F} else {T} } else {F}  →  if (c1 && !c2) {T} else {F}
1114///   if (c1) {T} else { if (c2) {T} else {F} }  →  if (c1 || c2) {T} else {F}
1115///   if (c1) {T} else { if (c2) {F} else {T} }  →  if (c1 || !c2) {T} else {F}
1116/// Branch identity is deep structural equality (Stmt: PartialEq);
1117/// iterate to a fixpoint so chained diamonds `(a && b) && c` collapse.
1118pub fn fold_short_circuits(s: &mut Stmt) {
1119    for _ in 0..8 {
1120        let mut changed = false;
1121        walk_mut_deep(s, &mut |st| {
1122            if try_fold_diamond(st) {
1123                changed = true;
1124            }
1125        });
1126        if !changed {
1127            break;
1128        }
1129    }
1130}
1131
1132fn try_fold_diamond(st: &mut Stmt) -> bool {
1133    enum Shape {
1134        ThenAnd,
1135        ThenAndNot,
1136        ElseOr,
1137        ElseOrNot,
1138        BareAnd,
1139        BareAndNot,
1140    }
1141    // Probe the shape on an immutable borrow first (the fold takes the
1142    // whole If apart; matching and rebuilding inside one borrow is not
1143    // expressible).
1144    let shape = match &*st {
1145        // One-sided diamonds: the shared branch is the implicit empty
1146        // fall-through (`if (c1) { if (c2) {T} }` → `if (c1 && c2) {T}`).
1147        Stmt::If {
1148            then_stmt,
1149            else_stmt: None,
1150            ..
1151        } => match &**then_stmt {
1152            Stmt::If {
1153                then_stmt: t2,
1154                else_stmt: None,
1155                ..
1156            } if !matches!(&**t2, Stmt::Block(v) if v.is_empty()) => Some(Shape::BareAnd),
1157            Stmt::If {
1158                then_stmt: t2,
1159                else_stmt: Some(_),
1160                ..
1161            } if matches!(&**t2, Stmt::Block(v) if v.is_empty()) => Some(Shape::BareAndNot),
1162            _ => None,
1163        },
1164        Stmt::If {
1165            then_stmt,
1166            else_stmt: Some(els),
1167            ..
1168        } => match (&**then_stmt, &**els) {
1169            (Stmt::If { else_stmt: Some(f2), .. }, _) if f2.as_ref() == els.as_ref() => {
1170                Some(Shape::ThenAnd)
1171            }
1172            (Stmt::If { then_stmt: t2, else_stmt: Some(_), .. }, _)
1173                if t2.as_ref() == els.as_ref() =>
1174            {
1175                Some(Shape::ThenAndNot)
1176            }
1177            (_, Stmt::If { then_stmt: t2, else_stmt: Some(_), .. })
1178                if t2.as_ref() == then_stmt.as_ref() =>
1179            {
1180                Some(Shape::ElseOr)
1181            }
1182            (_, Stmt::If { else_stmt: Some(f2), .. })
1183                if f2.as_ref() == then_stmt.as_ref() =>
1184            {
1185                Some(Shape::ElseOrNot)
1186            }
1187            _ => None,
1188        },
1189        _ => None,
1190    };
1191    let Some(shape) = shape else {
1192        return false;
1193    };
1194
1195    let taken = std::mem::replace(st, Stmt::Block(vec![]));
1196    // Bare (no-else) diamonds fold without an else at all.
1197    if matches!(shape, Shape::BareAnd | Shape::BareAndNot) {
1198        let Stmt::If {
1199            cond: c1,
1200            then_stmt,
1201            else_stmt: None,
1202        } = taken
1203        else {
1204            unreachable!()
1205        };
1206        let Stmt::If {
1207            cond: c2,
1208            then_stmt: t2,
1209            else_stmt: f2,
1210        } = *then_stmt
1211        else {
1212            unreachable!()
1213        };
1214        let sc = |op: BinOp, l: Expr, r: Expr| Expr::Bin {
1215            op,
1216            l: Box::new(l),
1217            r: Box::new(r),
1218            ty: None,
1219        };
1220        match shape {
1221            // if (c1 && c2) {T}
1222            Shape::BareAnd => {
1223                *st = Stmt::If {
1224                    cond: sc(BinOp::LogAnd, c1, c2),
1225                    then_stmt: t2,
1226                    else_stmt: None,
1227                };
1228            }
1229            // if (c1) { if (c2) {} else {T} }  →  if (c1 && !c2) {T}
1230            _ => {
1231                let body = f2.unwrap();
1232                *st = Stmt::If {
1233                    cond: sc(
1234                        BinOp::LogAnd,
1235                        c1,
1236                        Expr::Un {
1237                            op: UnOp::Not,
1238                            e: Box::new(c2),
1239                        },
1240                    ),
1241                    then_stmt: body,
1242                    else_stmt: None,
1243                };
1244            }
1245        }
1246        return true;
1247    }
1248    let Stmt::If {
1249        cond: c1,
1250        then_stmt,
1251        else_stmt: Some(els),
1252    } = taken
1253    else {
1254        unreachable!("shape probe guaranteed an If with else")
1255    };
1256    let sc = |op: BinOp, l: Expr, r: Expr| Expr::Bin {
1257        op,
1258        l: Box::new(l),
1259        r: Box::new(r),
1260        ty: None,
1261    };
1262    let not = |e: Expr| Expr::Un {
1263        op: UnOp::Not,
1264        e: Box::new(e),
1265    };
1266    match shape {
1267        Shape::BareAnd | Shape::BareAndNot => unreachable!("handled above"),
1268        Shape::ThenAnd | Shape::ThenAndNot => {
1269            let Stmt::If {
1270                cond: c2,
1271                then_stmt: t2,
1272                else_stmt: Some(f2),
1273            } = *then_stmt
1274            else {
1275                unreachable!()
1276            };
1277            let (nc, body, alt) = match shape {
1278                // if (c1 && c2) {T} else {F}
1279                Shape::ThenAnd => (sc(BinOp::LogAnd, c1, c2), t2, f2),
1280                // if (c1 && !c2) {T} else {F}   (inner: {F} else {T})
1281                _ => (sc(BinOp::LogAnd, c1, not(c2)), f2, t2),
1282            };
1283            *st = Stmt::If {
1284                cond: nc,
1285                then_stmt: body,
1286                else_stmt: Some(alt),
1287            };
1288        }
1289        Shape::ElseOr | Shape::ElseOrNot => {
1290            let Stmt::If {
1291                cond: c2,
1292                then_stmt: t2,
1293                else_stmt: Some(f2),
1294            } = *els
1295            else {
1296                unreachable!()
1297            };
1298            // The surviving then-body is the OUTER then (identical to the
1299            // matching inner branch by the probe).
1300            let body = then_stmt;
1301            let (nc, alt) = match shape {
1302                // if (c1 || c2) {T} else {F}   (inner: {T} else {F})
1303                Shape::ElseOr => (sc(BinOp::LogOr, c1, c2), f2),
1304                // if (c1 || !c2) {T} else {F}  (inner: {F} else {T})
1305                _ => (sc(BinOp::LogOr, c1, not(c2)), t2),
1306            };
1307            *st = Stmt::If {
1308                cond: nc,
1309                then_stmt: body,
1310                else_stmt: Some(alt),
1311            };
1312        }
1313    }
1314    true
1315}
1316
1317/// A static initializer cannot contain `return` in source form — the
1318/// clinit's closing return is a method-level artifact, but the
1319/// structurer can leave it nested inside loops/branches where the
1320/// top-level strip cannot see it (x2/a: `static { while { ...; return;
1321/// } }` — "return outside method", 184 sites on reqable).
1322pub fn strip_clinit_returns(s: &mut Stmt) {
1323    walk_mut_deep(s, &mut |st| {
1324        if let Stmt::Block(v) = st {
1325            v.retain(|x| !matches!(x, Stmt::Return(None)));
1326        }
1327    });
1328}
1329
1330pub fn prepend_comment(s: &mut Stmt, text: String) {
1331    let stmt = Stmt::Comment(text);
1332    match s {
1333        Stmt::Block(v) => v.insert(0, stmt),
1334        other => {
1335            let inner = std::mem::replace(other, Stmt::Block(vec![]));
1336            *other = Stmt::Block(vec![stmt, inner]);
1337        }
1338    }
1339}
1340
1341/// Fused expression rewrites that each used to walk the whole tree:
1342/// residual cmp sentinels, object-null comparisons, const-first compares.
1343/// One traversal, three rules — the separate walks were a top profile cost.
1344/// True when `e` evaluates to an object reference — a legal null-compare
1345/// side: locals via the reference table, everything else via its own type
1346/// (fields like `pi.versionName != 0` and call results are references too;
1347/// constants never are).
1348fn is_obj_expr(e: &Expr, obj_var: &dyn Fn(u32) -> bool) -> bool {
1349    match e {
1350        Expr::Local { var, .. } => obj_var(*var),
1351        Expr::Const(_) => false,
1352        _ => e.type_ref().erased().is_reference(),
1353    }
1354}
1355
1356/// `obj == 0` / `0 == obj` → `obj == null`: replace the CONST-0 side.
1357/// The previous shape replaced the LOCAL side instead — every object
1358/// null-check in the corpus rendered as `null != 0` (4,598 hits in
1359/// reqable alone) and the real operand vanished from the condition.
1360fn null_side_rewrite(x: &mut Expr, obj_var: &dyn Fn(u32) -> bool) {
1361    if let Expr::Bin { op, l, r, .. } = x {
1362        if !matches!(op, BinOp::Eq | BinOp::Ne) {
1363            return;
1364        }
1365        let l_zero = matches!(&**l, Expr::Const(ConstVal::Int(0)));
1366        let r_zero = matches!(&**r, Expr::Const(ConstVal::Int(0)));
1367        if l_zero && !r_zero && is_obj_expr(r, obj_var) {
1368            **l = Expr::Const(ConstVal::Null);
1369        } else if r_zero && !l_zero && is_obj_expr(l, obj_var) {
1370            **r = Expr::Const(ConstVal::Null);
1371        }
1372    }
1373}
1374
1375pub fn fused_expr_rewrites(s: &mut Stmt, vt: &VarTable) {
1376    // var ids are dense: a byte table beats a HashSet lookup.
1377    let mut obj_vars: Vec<bool> = Vec::with_capacity(vt.vars.len());
1378    for v in &vt.vars {
1379        obj_vars.push(v.ty.erased().is_reference());
1380    }
1381    rewrite_exprs(s, &mut |e| {
1382        deep_rewrite(e, &mut |x| {
1383            // 1. cmp sentinels.
1384            if let Expr::Invokedynamic { name, args, .. } = x {
1385                if name.starts_with('\0') && args.len() == 2 {
1386                    let (cls, ty) = match name.as_str() {
1387                        "\0cmp-long" => ("java/lang/Long", JavaType::Long),
1388                        "\0cmpl-float" | "\0cmpg-float" => ("java/lang/Float", JavaType::Float),
1389                        _ => ("java/lang/Double", JavaType::Double),
1390                    };
1391                    let desc = MethodDescriptor {
1392                        args: vec![ty.clone(), ty],
1393                        ret: JavaType::Int,
1394                    };
1395                    *x = Expr::Method {
1396                        owner: None,
1397                        cls: cls.into(),
1398                        name: "compare".into(),
1399                        desc: std::sync::Arc::new(desc),
1400                        args: args.clone(),
1401                        is_static: true,
1402                        is_interface: false,
1403                        is_special: false,
1404                        is_super: false,
1405                        is_dynamic: false,
1406                        type_args: vec![],
1407                    };
1408                    return;
1409                }
1410            }
1411            // 2. null compares (no any_obj gate: the object side may be
1412            // a field/call whose type is not in the local table).
1413            null_side_rewrite(x, &|v| obj_vars.get(v as usize).copied().unwrap_or(false));
1414            // 3. const-first compares.
1415            if let Expr::Bin { op, l, r, .. } = x {
1416                if matches!(
1417                    op,
1418                    BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
1419                ) {
1420                    let l_const = matches!(&**l, Expr::Const(_));
1421                    let r_var = matches!(&**r, Expr::Local { .. });
1422                    if l_const && r_var {
1423                        std::mem::swap(l, r);
1424                    }
1425                }
1426            }
1427        });
1428    });
1429    // 4. null into reference-typed assignment targets. The register
1430    //    machine stores null as const-0; return/throw/field-store/invoke
1431    //    argument paths were normalized in the lifter, but a null that
1432    //    MATERIALIZES into a local was rendered as `str = 0;` — javac
1433    //    "int cannot be converted to String" (the single biggest error
1434    //    family on real corpora: d8 emits null locals this way after
1435    //    every `x = null` branch).
1436    walk_mut_deep(s, &mut |st| {
1437        if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = st {
1438            let tgt_is_obj = match &**target {
1439                Expr::Local { var, .. } => {
1440                    obj_vars.get(*var as usize).copied().unwrap_or(false)
1441                }
1442                other => other.type_ref().erased().is_reference(),
1443            };
1444            if tgt_is_obj {
1445                if let Expr::Const(ConstVal::Int(0)) = &**value {
1446                    **value = Expr::Const(ConstVal::Null);
1447                }
1448            }
1449        } else if let Stmt::LocalDef { var, init, .. } = st {
1450            if obj_vars.get(*var as usize).copied().unwrap_or(false) {
1451                if let Some(iv) = init {
1452                    if let Expr::Const(ConstVal::Int(0)) = iv {
1453                        *iv = Expr::Const(ConstVal::Null);
1454                    }
1455                }
1456            }
1457        }
1458    });
1459}
1460
1461/// Replace residual cmp sentinels (`\0cmp*` Invokedynamic markers that never
1462/// reached a condition) with library compare calls.
1463#[allow(dead_code)]
1464pub fn desugar_cmp_residuals(s: &mut Stmt) {
1465    rewrite_exprs(s, &mut |e| {
1466        deep_rewrite(e, &mut |x| {
1467            if let Expr::Invokedynamic { name, args, .. } = x {
1468                if name.starts_with('\0') && args.len() == 2 {
1469                    let (cls, ty) = match name.as_str() {
1470                        "\0cmp-long" => ("java/lang/Long", JavaType::Long),
1471                        "\0cmpl-float" | "\0cmpg-float" => ("java/lang/Float", JavaType::Float),
1472                        _ => ("java/lang/Double", JavaType::Double),
1473                    };
1474                    let desc = jdc_core::types::MethodDescriptor {
1475                        args: vec![ty.clone(), ty],
1476                        ret: JavaType::Int,
1477                    };
1478                    *x = Expr::Method {
1479                        owner: None,
1480                        cls: cls.into(),
1481                        name: "compare".into(),
1482                        desc: std::sync::Arc::new(desc),
1483                        args: args.clone(),
1484                        is_static: true,
1485                        is_interface: false,
1486                        is_special: false,
1487                        is_super: false,
1488                        is_dynamic: false,
1489                        type_args: vec![],
1490                    };
1491                }
1492            }
1493        });
1494    });
1495}
1496
1497/// `if (c) { x = A; } else { x = B; }` → `x = c ? A : B;`
1498pub fn ternary_fold(s: &mut Stmt) {
1499    let mut changed = true;
1500    let mut guard = 0;
1501    while changed && guard < 8 {
1502        changed = false;
1503        guard += 1;
1504        ternary_fold_walk(s, &mut changed);
1505        strip_empty(s);
1506    }
1507}
1508
1509fn ternary_fold_walk(s: &mut Stmt, changed: &mut bool) {
1510    if let Stmt::Block(v) = s {
1511        for x in v.iter_mut() {
1512            ternary_fold_walk(x, changed);
1513        }
1514        return;
1515    }
1516    let Stmt::If {
1517        cond,
1518        then_stmt,
1519        else_stmt,
1520    } = s
1521    else {
1522        return;
1523    };
1524    ternary_fold_walk(then_stmt, changed);
1525    if let Some(e) = else_stmt {
1526        ternary_fold_walk(e, changed);
1527    }
1528    let Some(else_b) = else_stmt else { return };
1529    let target_of = |st: &Stmt| -> Option<u32> {
1530        let Stmt::ExprStmt(Expr::Assign { target, .. }) = st else {
1531            return None;
1532        };
1533        if let Expr::Local { var, .. } = &**target {
1534            Some(*var)
1535        } else {
1536            None
1537        }
1538    };
1539    let (Stmt::Block(tv), Stmt::Block(ev)) = (then_stmt.as_mut(), else_b.as_mut()) else {
1540        return;
1541    };
1542    if tv.len() != 1 || ev.len() != 1 {
1543        return;
1544    }
1545    let (Some(vt_), Some(ve)) = (target_of(&tv[0]), target_of(&ev[0])) else {
1546        return;
1547    };
1548    if vt_ != ve {
1549        return;
1550    }
1551    // The condition and both values must not re-assign the target between
1552    // the fold — they are single statements, so a mention is a re-read at
1553    // worst; re-assignment only happens via assignments inside them.
1554    let mut touched = HashSet::default();
1555    let (Stmt::ExprStmt(a_then), Stmt::ExprStmt(a_else)) = (&tv[0], &ev[0]) else {
1556        return;
1557    };
1558    if let Expr::Assign { value, .. } = a_then {
1559        collect_vars(value, &mut touched);
1560    }
1561    if let Expr::Assign { value, .. } = a_else {
1562        collect_vars(value, &mut touched);
1563    }
1564    if touched.contains(&vt_) {
1565        return;
1566    }
1567    let value_then = match a_then {
1568        Expr::Assign { value, .. } => value.clone(),
1569        _ => return,
1570    };
1571    let value_else = match a_else {
1572        Expr::Assign { value, .. } => value.clone(),
1573        _ => return,
1574    };
1575    let cond_e = cond.clone();
1576    let folded = Stmt::ExprStmt(Expr::Assign {
1577        target: Box::new(Expr::Local {
1578            var: vt_,
1579            ty: vt_join_ty(&value_then, &value_else),
1580        }),
1581        op: AssignOp::Plain,
1582        value: Box::new(Expr::Cond {
1583            c: Box::new(cond_e),
1584            t: value_then,
1585            f: value_else,
1586        }),
1587    });
1588    *s = Stmt::Block(vec![folded]);
1589    *changed = true;
1590}
1591
1592fn vt_join_ty(a: &Expr, b: &Expr) -> TypeRef {
1593    let ta = a.type_ref();
1594    let tb = b.type_ref();
1595    if ta.erased() == tb.erased() {
1596        return ta;
1597    }
1598    TypeRef::J(JavaType::Object("java/lang/Object".into()))
1599}
1600
1601/// Fold `new StringBuilder(...).append(x)...toString()` chains into
1602/// `StringConcat`. Single-assignment temporaries carry the chain.
1603///
1604/// d8 drops append results (statement-form calls), so a builder whose chain
1605/// has STATEMENT appends attached is left alone: folding its `toString`
1606/// with only the ctor arguments would misstate the contents.
1607pub fn fold_string_builders(s: &mut Stmt, vt: &VarTable) {
1608    // var → assigned value, for vars assigned exactly once.
1609    let n_vars = vt.vars.len().max(1);
1610    let mut counts = vec![0usize; n_vars];
1611    count_assignments(s, &mut counts);
1612    let mut values: Vec<Option<Expr>> = vec![None; n_vars];
1613    record_assignments(s, &counts, &mut values);
1614
1615    // Builders with statement-form appends (their chains are incomplete).
1616    let mut appended_stmts: HashSet<u32> = HashSet::default();
1617    walk_all(s, &mut |st| {
1618        if let Stmt::ExprStmt(Expr::Method {
1619            cls,
1620            name,
1621            owner,
1622            args,
1623            ..
1624        }) = st
1625        {
1626            if name.as_ref() == "append" && is_string_builder(cls) && args.len() == 1 {
1627                if let Some(Expr::Local { var, .. }) = owner.as_deref() {
1628                    appended_stmts.insert(*var);
1629                }
1630            }
1631        }
1632    });
1633
1634    rewrite_exprs(s, &mut |e| {
1635        fold_concat_in_expr(e, &values, &appended_stmts);
1636    });
1637
1638    // Drop now-unused builder temporaries (their only consumers were
1639    // folded); the drop COUNT drives iteration — no statement counting
1640    // walks at all.
1641    for _ in 0..8 {
1642        let mut reads: HashSet<u32> = HashSet::default();
1643        stmt_collect_vars(s, &mut reads, false);
1644        if drop_unused_assigns(s, &reads) == 0 {
1645            break;
1646        }
1647    }
1648}
1649
1650/// Deep statement count (pipeline guard input).
1651pub fn count_stmts_deep(s: &Stmt, out: &mut usize) {
1652    *out += 1;
1653    walk_all(s, &mut |_| {
1654        *out += 1;
1655    });
1656}
1657
1658/// Fused per-variable analysis gathered in one statement-tree walk.
1659struct VarAnalysis {
1660    assigns: Vec<usize>,
1661    reads: Vec<usize>,
1662    /// Value of the (single) assignment, when recorded.
1663    values: Vec<Option<Expr>>,
1664}
1665
1666fn analyze_vars(s: &Stmt, a: &mut VarAnalysis) {
1667    walk_all(s, &mut |st| {
1668        let exprs: Vec<&Expr> = match st {
1669            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
1670                if let Expr::Local { var, .. } = &**target {
1671                    grow_to(&mut a.assigns, *var);
1672                    let idx = *var as usize;
1673                    a.assigns[idx] += 1;
1674                    if a.assigns[idx] == 1 {
1675                        if idx >= a.values.len() {
1676                            a.values.resize(idx + 1, None);
1677                        }
1678                        a.values[idx] = Some((**value).clone());
1679                    } else if idx < a.values.len() {
1680                        a.values[idx] = None;
1681                    }
1682                }
1683                let mut v: Vec<&Expr> = vec![value];
1684                if !matches!(&**target, Expr::Local { .. }) {
1685                    v.push(target);
1686                }
1687                v
1688            }
1689            Stmt::ExprStmt(e @ (Expr::PreIncDec { .. } | Expr::PostIncDec { .. })) => {
1690                // `v++` is a read-modify-WRITE of v: counting it as a
1691                // plain read makes v a single-assign/single-read forward
1692                // candidate, and value-forwarding then fabricates `5++`
1693                // (or drops v's def and leaves ++ on an undefined name).
1694                let inner = match e {
1695                    Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => e,
1696                    _ => unreachable!(),
1697                };
1698                if let Expr::Local { var, .. } = &**inner {
1699                    grow_to(&mut a.assigns, *var);
1700                    let idx = *var as usize;
1701                    a.assigns[idx] += 1;
1702                    if idx < a.values.len() {
1703                        a.values[idx] = None;
1704                    }
1705                    return;
1706                }
1707                vec![e]
1708            }
1709            Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
1710                vec![e]
1711            }
1712            Stmt::Return(Some(e)) => vec![e],
1713            Stmt::LocalDef {
1714                var, init: Some(e), ..
1715            } => {
1716                grow_to(&mut a.assigns, *var);
1717                let idx = *var as usize;
1718                a.assigns[idx] += 1;
1719                if a.assigns[idx] == 1 {
1720                    if idx >= a.values.len() {
1721                        a.values.resize(idx + 1, None);
1722                    }
1723                    a.values[idx] = Some(e.clone());
1724                } else if idx < a.values.len() {
1725                    a.values[idx] = None;
1726                }
1727                vec![e]
1728            }
1729            Stmt::If { cond, .. } => vec![cond],
1730            Stmt::While { cond, .. } => vec![cond],
1731            Stmt::DoWhile { cond, .. } => vec![cond],
1732            _ => Vec::new(),
1733        };
1734        for e in exprs {
1735            visit_exprs(e, &mut |x| {
1736                if let Expr::Local { var, .. } = x {
1737                    grow_to(&mut a.reads, *var);
1738                    a.reads[*var as usize] += 1;
1739                }
1740            });
1741        }
1742    });
1743    // Keep the three views length-aligned.
1744    let n = a.assigns.len().max(a.reads.len()).max(a.values.len());
1745    a.assigns.resize(n, 0);
1746    a.reads.resize(n, 0);
1747    a.values.resize(n, None);
1748}
1749
1750fn grow_to(v: &mut Vec<usize>, var: u32) {
1751    if var as usize >= v.len() {
1752        v.resize(var as usize + 1, 0);
1753    }
1754}
1755
1756fn count_assignments(s: &Stmt, counts: &mut Vec<usize>) {
1757    walk_all(s, &mut |st| match st {
1758        Stmt::ExprStmt(Expr::Assign { target, .. }) => {
1759            if let Expr::Local { var, .. } = &**target {
1760                grow_to(counts, *var);
1761                counts[*var as usize] += 1;
1762            }
1763        }
1764        Stmt::LocalDef {
1765            var, init: Some(_), ..
1766        } => {
1767            grow_to(counts, *var);
1768            counts[*var as usize] += 1;
1769        }
1770        _ => {}
1771    });
1772}
1773
1774fn record_assignments(s: &Stmt, counts: &[usize], out: &mut Vec<Option<Expr>>) {
1775    walk_all(s, &mut |st| match st {
1776        Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
1777            if let Expr::Local { var, .. } = &**target {
1778                if counts.get(*var as usize).copied().unwrap_or(0) == 1 {
1779                    if *var as usize >= out.len() {
1780                        out.resize(*var as usize + 1, None);
1781                    }
1782                    out[*var as usize] = Some((**value).clone());
1783                }
1784            }
1785        }
1786        Stmt::LocalDef {
1787            var, init: Some(e), ..
1788        } if counts.get(*var as usize).copied().unwrap_or(0) == 1 => {
1789            if *var as usize >= out.len() {
1790                out.resize(*var as usize + 1, None);
1791            }
1792            out[*var as usize] = Some(e.clone());
1793        }
1794        _ => {}
1795    });
1796}
1797
1798fn fold_concat_in_expr(e: &mut Expr, values: &[Option<Expr>], appended: &HashSet<u32>) {
1799    deep_rewrite(e, &mut |x| {
1800        if let Expr::Method {
1801            cls,
1802            name,
1803            args,
1804            owner,
1805            ..
1806        } = x
1807        {
1808            if name.as_ref() == "toString" && args.is_empty() && is_string_builder(cls) {
1809                if let Some(o) = owner {
1810                    // Statement-form appends attached to any chain var mean
1811                    // the parts are incomplete — keep the call.
1812                    let mut has_stmt_appends = false;
1813                    visit_exprs(o, &mut |y| {
1814                        if let Expr::Local { var, .. } = y {
1815                            if appended.contains(var) {
1816                                has_stmt_appends = true;
1817                            }
1818                        }
1819                    });
1820                    if has_stmt_appends {
1821                        return;
1822                    }
1823                    let resolved = resolve_local(o, values);
1824                    if let Some(parts) = collect_sb_parts(&resolved, values, 0) {
1825                        // NEVER fold an empty chain: d8 splits builder
1826                        // chains across alias registers (`v2 = v0.append
1827                        // (x); v2.append(y); v0.toString()`) — the traced
1828                        // chain sees a bare `new StringBuilder` and would
1829                        // fold toString() to "" (a wrong VALUE, silently:
1830                        // lab package Obf.label). With zero parts the
1831                        // call stays — always correct, usually folded
1832                        // elsewhere once the parts are visible.
1833                        if !parts.is_empty() {
1834                            *x = Expr::StringConcat(parts);
1835                        }
1836                    }
1837                }
1838            }
1839        }
1840    });
1841}
1842
1843fn is_string_builder(cls: &str) -> bool {
1844    cls == "java/lang/StringBuilder" || cls == "java/lang/StringBuffer"
1845}
1846
1847fn resolve_local(e: &Expr, values: &[Option<Expr>]) -> Expr {
1848    match e {
1849        Expr::Local { var, ty } => values
1850            .get(*var as usize)
1851            .and_then(|o| o.clone())
1852            .unwrap_or_else(|| Expr::Local {
1853                var: *var,
1854                ty: ty.clone(),
1855            }),
1856        other => other.clone(),
1857    }
1858}
1859
1860/// Collect concat parts from a StringBuilder chain; depth caps cycles.
1861fn collect_sb_parts(e: &Expr, values: &[Option<Expr>], depth: u32) -> Option<Vec<ConcatPart>> {
1862    if depth > 16 {
1863        return None;
1864    }
1865    match e {
1866        Expr::Method {
1867            cls,
1868            name,
1869            args,
1870            owner,
1871            ..
1872        } if name.as_ref() == "append" && is_string_builder(cls) && args.len() == 1 => {
1873            let mut parts =
1874                collect_sb_parts(&resolve_local(owner.as_deref()?, values), values, depth + 1)?;
1875            parts.push(ConcatPart::Str(args[0].clone()));
1876            Some(parts)
1877        }
1878        Expr::New {
1879            cls,
1880            args,
1881            raw: false,
1882            ..
1883        } if is_string_builder(cls) => {
1884            let mut parts = Vec::new();
1885            for a in args {
1886                if let Expr::Const(ConstVal::Str(sv)) = a {
1887                    parts.push(ConcatPart::Const(sv.to_string()));
1888                } else {
1889                    parts.push(ConcatPart::Str(a.clone()));
1890                }
1891            }
1892            Some(parts)
1893        }
1894        _ => None,
1895    }
1896}
1897
1898fn drop_unused_assigns(s: &mut Stmt, reads: &HashSet<u32>) -> usize {
1899    let mut dropped = 0usize;
1900    walk_mut_deep(s, &mut |st| {
1901        if let Stmt::Block(v) = st {
1902            let before = v.len();
1903            v.retain(|x| match x {
1904                // Drop only when unread AND a builder chain (folded
1905                // consumer; `new StringBuilder` chains cannot NPE).
1906                Stmt::ExprStmt(Expr::Assign { target, value, .. }) => match &**target {
1907                    Expr::Local { var, .. } => reads.contains(var) || !is_sbish(value),
1908                    _ => true,
1909                },
1910                Stmt::LocalDef { var, init, .. } => {
1911                    reads.contains(var) || !init.as_ref().map(is_sbish).unwrap_or(false)
1912                }
1913                _ => true,
1914            });
1915            dropped += before - v.len();
1916        }
1917    });
1918    dropped
1919}
1920
1921fn is_sbish(e: &Expr) -> bool {
1922    match e {
1923        Expr::New {
1924            cls, raw: false, ..
1925        } => is_string_builder(cls),
1926        Expr::Method { name, cls, .. } => name.as_ref() == "append" && is_string_builder(cls),
1927        _ => false,
1928    }
1929}
1930
1931fn walk_mut_deep<F: FnMut(&mut Stmt)>(s: &mut Stmt, f: &mut F) {
1932    f(s);
1933    walk_mut(s, &mut |x| walk_mut_deep(x, f));
1934}
1935
1936/// Drop orphaned raw `new-instance` statements. R8 sometimes separates
1937/// `new-instance vR` from its `<init>` invoke by a register copy
1938/// (`move-object vS, vR; invoke vS.<init>(..)` — dex-legal: the init
1939/// initializes the object both registers name). The fold pairs the init
1940/// with the COPY, and the structurer's branch duplication re-materializes
1941/// the original raw marker as a bare `new C();` statement on paths whose
1942/// uses were rewired to the folded side (weixin sns/storage/c0: four
1943/// `new s1();` beside the real `s1 s1x = new s1(..)` — s1 has no no-arg
1944/// ctor, "无法将类 C的构造器 C应用到给定类型" ×195). A raw New in
1945/// statement position binds no variable and initializes nothing — pure
1946/// verifier-artifact dead code.
1947pub fn drop_dead_raw_news(body: &mut Stmt) {
1948    fn is_orphan(s: &Stmt) -> bool {
1949        matches!(s, Stmt::ExprStmt(Expr::New { raw: true, .. }))
1950    }
1951    walk_mut_deep(body, &mut |st| match st {
1952        Stmt::Block(v) => v.retain(|x| !is_orphan(x)),
1953        Stmt::Switch { cases, .. } => {
1954            for c in cases.iter_mut() {
1955                c.body.retain(|x| !is_orphan(x));
1956            }
1957        }
1958        Stmt::For { init, .. } => init.retain(|x| !is_orphan(x)),
1959        Stmt::TryWithResources { resources, .. } => resources.retain(|x| !is_orphan(x)),
1960        _ => {}
1961    });
1962}
1963
1964/// Recover `synchronized` from the d8 monitor pattern:
1965/// `monitorenter(e); try { body } catch (Throwable) { monitorexit(e); throw t; }`
1966/// (optionally followed by `monitorexit(e)`).
1967pub fn fold_synchronized(s: &mut Stmt) {
1968    fold_sync_walk(s);
1969}
1970
1971fn fold_sync_walk(s: &mut Stmt) {
1972    walk_mut_deep(s, &mut |st| {
1973        let Stmt::Block(v) = st else { return };
1974        let mut i = 0;
1975        while i < v.len() {
1976            if i + 1 < v.len() {
1977                if let Some(sync) = try_sync_at(v, i) {
1978                    let _ = v.drain(i..i + 2);
1979                    v.insert(i, sync);
1980                    continue;
1981                }
1982            }
1983            i += 1;
1984        }
1985    });
1986}
1987
1988fn try_sync_at(v: &[Stmt], i: usize) -> Option<Stmt> {
1989    let Stmt::MonitorEnter(lock) = &v[i] else {
1990        return None;
1991    };
1992    let Stmt::Try {
1993        body,
1994        catches,
1995        finally,
1996    } = &v[i + 1]
1997    else {
1998        return None;
1999    };
2000    if finally.is_some() || catches.len() != 1 {
2001        return None;
2002    }
2003    let c = &catches[0];
2004    if !c.exc.is_empty() {
2005        return None;
2006    }
2007    // The catch-all body: monitorexit(lock) then rethrow.
2008    let Stmt::Block(cv) = c.body.as_ref() else {
2009        return None;
2010    };
2011    let exit_matches = cv.len() >= 2
2012        && matches!(&cv[0], Stmt::MonitorExit(e) if expr_local_var(e) == expr_local_var(lock));
2013    let throws = cv.len() >= 2 && matches!(&cv[1], Stmt::Throw(_));
2014    if !(exit_matches && throws) {
2015        return None;
2016    }
2017    Some(Stmt::Synchronized {
2018        lock: lock.clone(),
2019        body: body.clone(),
2020    })
2021}
2022
2023fn expr_local_var(e: &Expr) -> Option<u32> {
2024    if let Expr::Local { var, .. } = e {
2025        Some(*var)
2026    } else {
2027        None
2028    }
2029}
2030
2031/// Copy-forward single-use temporaries: `v = e; ... v ...` inlines `e` at
2032/// the (single) use and drops the assignment when safe. Pure values inline
2033/// anywhere; impure values (calls, array reads) only when the use is the
2034/// statement immediately after the definition.
2035pub fn forward_single_use(s: &mut Stmt, _vt: &VarTable) {
2036    // Fused single traversal: assignment counts, read counts and the
2037    // single-assignment values all come out of ONE walk (three separate
2038    // full-tree walks dominated the pass cost on large methods).
2039    let mut analysis = VarAnalysis {
2040        assigns: Vec::new(),
2041        reads: Vec::new(),
2042        values: Vec::new(),
2043    };
2044    analyze_vars(s, &mut analysis);
2045    let mut assigns = analysis.assigns;
2046    let mut reads = analysis.reads;
2047    // Var ids beyond the table (synthetic test shapes) are covered by the
2048    // counters' on-demand growth.
2049    let n_vars = assigns.len().max(_vt.vars.len()).max(1);
2050    assigns.resize(n_vars, 0);
2051    reads.resize(n_vars, 0);
2052    let mut values = analysis.values;
2053    values.resize(n_vars, None);
2054
2055    // Candidates: assigned exactly once, read exactly once. Additionally
2056    // the inlined VALUE must not reference a multi-assigned var (phi vars):
2057    // moving the read across the phi's reassignment would be a stale
2058    // capture (register rotations snapshot through temps for this reason).
2059    let mut cand = vec![false; n_vars];
2060    let mut edges: Vec<Vec<u32>> = vec![Vec::new(); n_vars];
2061    for (v, cand_v) in cand.iter_mut().enumerate() {
2062        if assigns.get(v).copied().unwrap_or(0) != 1 || reads.get(v).copied().unwrap_or(0) != 1 {
2063            continue;
2064        }
2065        if let Some(val) = values.get(v).and_then(|o| o.as_ref()) {
2066            let mut refs = HashSet::default();
2067            collect_vars(val, &mut refs);
2068            if refs.iter().any(|r| assigns[*r as usize] > 1) {
2069                continue;
2070            }
2071            // Growth-graph edge set, built in the same walk: inlining v
2072            // inserts a clone of its value, and every Local inside the
2073            // clone is replaced in turn.
2074            edges[v] = refs.into_iter().collect();
2075        }
2076        *cand_v = true;
2077    }
2078
2079    // Cycle rejection on the growth graph. A def-reference cycle
2080    // (v = f(w), w = g(v) — loop-carried register rotation reaching the
2081    // pass as two mutually-referencing single-assign/single-read locals)
2082    // re-introduces the other cycle Local at every replacement level, so
2083    // deep_rewrite grows the tree one layer per level and never
2084    // terminates: weixin's com/tencent/mm/plugin/appbrand/widget/input/b4
2085    // exhausted a 64MB worker stack at ~300k recursion frames. Edges into
2086    // non-candidates are harmless (they are never replaced, so growth
2087    // dies there — and they carry no outgoing edges).
2088    //
2089    // Three-color DFS, iterative: a legit 60k-long single-use chain must
2090    // not trade one stack overflow for another. A GRAY child closes a
2091    // cycle; `bad[v]` (v on a cycle, or v's inlined closure grows into
2092    // one) then taints the whole current path — everything on it reaches
2093    // the cycle. Black verdicts memoize: a node whose subtree was proven
2094    // clean cannot grow a cycle later.
2095    let mut color = vec![0u8; n_vars]; // 0 white, 1 gray, 2 black
2096    let mut bad = vec![false; n_vars];
2097    for root in 0..n_vars {
2098        if color[root] != 0 || edges[root].is_empty() {
2099            continue;
2100        }
2101        color[root] = 1;
2102        let mut stack: Vec<(usize, usize)> = vec![(root, 0)];
2103        while let Some(top) = stack.last_mut() {
2104            let v = top.0;
2105            let i = top.1;
2106            top.1 += 1;
2107            if i < edges[v].len() {
2108                let r = edges[v][i] as usize;
2109                if color[r] == 1 {
2110                    for &(pn, _) in &stack {
2111                        bad[pn] = true;
2112                    }
2113                } else if color[r] == 0 {
2114                    color[r] = 1;
2115                    stack.push((r, 0));
2116                } else if bad[r] {
2117                    for &(pn, _) in &stack {
2118                        bad[pn] = true;
2119                    }
2120                }
2121            } else {
2122                color[v] = 2;
2123                stack.pop();
2124            }
2125        }
2126    }
2127    let single: Vec<bool> = (0..n_vars).map(|v| cand[v] && !bad[v]).collect();
2128    if !single.iter().any(|&b| b) {
2129        return;
2130    }
2131    let pure: Vec<bool> = (0..n_vars)
2132        .map(|v| {
2133            single[v]
2134                && values
2135                    .get(v)
2136                    .and_then(|o| o.as_ref())
2137                    .map(|e| !has_side_effects(e))
2138                    .unwrap_or(false)
2139        })
2140        .collect();
2141    let impure: Vec<bool> = (0..n_vars).map(|v| single[v] && !pure[v]).collect();
2142
2143    // 1. Pure values: inline anywhere.
2144    if pure.iter().any(|&b| b) {
2145        let vals: Vec<Option<Expr>> = (0..n_vars)
2146            .map(|v| {
2147                if pure[v] {
2148                    values.get(v).cloned().flatten()
2149                } else {
2150                    None
2151                }
2152            })
2153            .collect();
2154        rewrite_exprs(s, &mut |e| {
2155            deep_rewrite_reads(e, &mut |x| {
2156                if let Expr::Local { var, .. } = x {
2157                    if let Some(Some(v)) = vals.get(*var as usize) {
2158                        *x = v.clone();
2159                    }
2160                }
2161            });
2162        });
2163        drop_defs(s, &pure);
2164    }
2165
2166    // 2. Impure values: inline only into the immediately following
2167    //    statement.
2168    if impure.iter().any(|&b| b) {
2169        forward_adjacent_impure(s, &values, &impure);
2170    }
2171}
2172
2173/// Count READS of variables (assignment targets excluded).
2174fn count_reads(s: &Stmt, out: &mut Vec<usize>) {
2175    walk_all(s, &mut |st| {
2176        let exprs: Vec<&Expr> = match st {
2177            // Assign must match BEFORE the generic ExprStmt arm (the
2178            // assignment target is a write, not a read).
2179            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
2180                let mut v: Vec<&Expr> = vec![value];
2181                if !matches!(&**target, Expr::Local { .. }) {
2182                    v.push(target);
2183                }
2184                v
2185            }
2186            // `v++` writes v — not a read (a compound `a[i]++` still
2187            // reads its owner/index subtree).
2188            Stmt::ExprStmt(e @ (Expr::PreIncDec { .. } | Expr::PostIncDec { .. })) => {
2189                let inner = match e {
2190                    Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => e,
2191                    _ => unreachable!(),
2192                };
2193                if matches!(**inner, Expr::Local { .. }) {
2194                    Vec::new()
2195                } else {
2196                    vec![e]
2197                }
2198            }
2199            Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
2200                vec![e]
2201            }
2202            Stmt::Return(Some(e)) => vec![e],
2203            Stmt::LocalDef { init: Some(e), .. } => vec![e],
2204            Stmt::If { cond, .. } => vec![cond],
2205            Stmt::While { cond, .. } => vec![cond],
2206            Stmt::DoWhile { cond, .. } => vec![cond],
2207            // Read sites that are NOT statement children (walk_all recurses
2208            // into bodies but these payloads are expressions on the node
2209            // itself). Omitting them under-counts reads, which would let
2210            // drop_dead_locals over-prune a var whose only use is a switch
2211            // selector / loop condition / lock / iterable.
2212            Stmt::Switch { selector, .. } => vec![selector],
2213            Stmt::ForEach { iterable, .. } => vec![iterable],
2214            Stmt::Synchronized { lock, .. } => vec![lock],
2215            Stmt::For { cond, update, .. } => {
2216                let mut v: Vec<&Expr> = Vec::with_capacity(update.len() + 1);
2217                if let Some(c) = cond {
2218                    v.push(c);
2219                }
2220                v.extend(update.iter());
2221                v
2222            }
2223            Stmt::Assert { cond, msg } => {
2224                let mut v: Vec<&Expr> = vec![cond];
2225                if let Some(m) = msg {
2226                    v.push(m);
2227                }
2228                v
2229            }
2230            _ => Vec::new(),
2231        };
2232        for e in exprs {
2233            visit_exprs(e, &mut |x| {
2234                if let Expr::Local { var, .. } = x {
2235                    grow_to(out, *var);
2236                    out[*var as usize] += 1;
2237                }
2238            });
2239        }
2240    });
2241}
2242
2243fn drop_defs(s: &mut Stmt, vars: &[bool]) {
2244    walk_mut_deep(s, &mut |st| {
2245        if let Stmt::Block(v) = st {
2246            v.retain(|x| match x {
2247                Stmt::ExprStmt(Expr::Assign { target, .. }) => match &**target {
2248                    Expr::Local { var, .. } => !vars.get(*var as usize).copied().unwrap_or(false),
2249                    _ => true,
2250                },
2251                Stmt::LocalDef { var, .. } => !vars.get(*var as usize).copied().unwrap_or(false),
2252                _ => true,
2253            });
2254        }
2255    });
2256}
2257
2258/// For each statement list: `v = IMPURE; NEXT(v)` folds NEXT's reference.
2259fn forward_adjacent_impure(s: &mut Stmt, values: &[Option<Expr>], impure: &[bool]) {
2260    if let Stmt::Block(v) = s {
2261        let mut i = 0;
2262        while i + 1 < v.len() {
2263            let def_var = match &v[i] {
2264                Stmt::ExprStmt(Expr::Assign { target, .. }) => match &**target {
2265                    Expr::Local { var, .. } => Some(*var),
2266                    _ => None,
2267                },
2268                Stmt::LocalDef { var, .. } => Some(*var),
2269                _ => None,
2270            };
2271            if let Some(var) = def_var {
2272                if impure.get(var as usize).copied().unwrap_or(false) {
2273                    // Does the NEXT statement reference var?
2274                    let mut reads_here = vec![0usize; values.len().max(1)];
2275                    count_reads(&v[i + 1], &mut reads_here);
2276                    if reads_here.get(var as usize).copied().unwrap_or(0) == 1 {
2277                        // The def statement's CURRENT value (earlier inlines
2278                        // in this walk may have rewritten it — the pre-built
2279                        // values map would be stale).
2280                        let val = match &v[i] {
2281                            Stmt::ExprStmt(Expr::Assign { value, .. }) => (**value).clone(),
2282                            Stmt::LocalDef { init: Some(e), .. } => (*e).clone(),
2283                            _ => {
2284                                i += 1;
2285                                continue;
2286                            }
2287                        };
2288                        rewrite_exprs(&mut v[i + 1], &mut |e| {
2289                            deep_rewrite_reads(e, &mut |x| {
2290                                if let Expr::Local { var: v2, .. } = x {
2291                                    if *v2 == var {
2292                                        *x = val.clone();
2293                                    }
2294                                }
2295                            });
2296                        });
2297                        v.remove(i);
2298                        // The PREVIOUS definition may now be adjacent to its
2299                        // (shifted) use — re-examine it.
2300                        i = i.saturating_sub(1);
2301                        continue;
2302                    }
2303                }
2304            }
2305            i += 1;
2306        }
2307    }
2308    walk_mut(s, &mut |x| forward_adjacent_impure(x, values, impure));
2309}
2310
2311// ---------------------------------------------------------------------------
2312// Type inference, booleanization, null comparisons, declarations
2313// ---------------------------------------------------------------------------
2314
2315/// Evidence-based type inference for synthetic (non-parameter) vars.
2316pub fn infer_types(vt: &mut VarTable, body: &mut Stmt, ret: &JavaType, env: &MethodEnv) {
2317    let n = vt.vars.len();
2318    // (type, strong) — DIRECTIONAL evidence. Strong facts (an assigned
2319    // value's type, the declared return/throw/monitor type) DEFINE the
2320    // variable; weak facts (call argument expectations, receiver
2321    // contexts, numeric literals) only constrain a use. jadx's bound
2322    // system carries the same direction; a flat pool let a weak
2323    // expectation (`v26 = p1` with boolean p1 beside `v26 = 0` sugar)
2324    // outvote the definition.
2325    let mut evidence: Vec<Vec<(JavaType, bool)>> = vec![Vec::new(); n];
2326    let ev = |evidence: &mut Vec<Vec<(JavaType, bool)>>, var: u32, t: JavaType, strong: bool| {
2327        if (var as usize) < evidence.len() {
2328            evidence[var as usize].push((t, strong));
2329        }
2330    };
2331
2332    walk_all(body, &mut |st| match st {
2333        Stmt::LocalDef { var, init, .. } => {
2334            if let Some(e) = init {
2335                expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
2336                // The initializer DEFINES the declared type: `long v = l(...)`
2337                // must not stay `int v` (silent 64-bit truncation in the
2338                // eyes of a reader; javac rejects it as lossy). Null
2339                // carries no type (it would DOWNGRADE String to Object).
2340                // Literals are weak (0/1 is boolean sugar half the time);
2341                // typed producers and parameter sources are strong;
2342                // local-to-local copies are weak (register reuse).
2343                let strong = match e {
2344                    Expr::Const(_) => false,
2345                    Expr::Local { var: src, .. } => {
2346                        let src_ty = vt.vars.get(*src as usize).map(|v| v.ty.erased());
2347                        let tgt_ty = vt.vars.get(*var as usize).map(|v| v.ty.erased());
2348                        match (src_ty, tgt_ty) {
2349                            (Some(s), Some(g)) => {
2350                                (s.is_numeric() && g.is_numeric())
2351                                    || (s.is_reference() && g.is_reference())
2352                            }
2353                            _ => false,
2354                        }
2355                    }
2356                    _ => true,
2357                };
2358                ev(&mut evidence, *var, e.type_ref().erased(), strong);
2359            }
2360        }
2361        Stmt::ExprStmt(e) => {
2362            // Assignment targets: a typed PRODUCER (method/new/field) or a
2363            // PARAMETER source is a STRONG definition (`v26 = p1` with
2364            // boolean p1). A local-to-local copy is weak — register reuse
2365            // and merge materialization emit exactly that shape with
2366            // mismatched types (`sb7 = compareTo10` in a Kotlin when).
2367            // Literals stay weak.
2368            if let Expr::Assign { target, value, .. } = e {
2369                // A field WRITE through a local (`v.f = x`, dex iput)
2370                // proves v is a reference of the field's declaring
2371                // class AT THAT POINT — an iput on a boolean register
2372                // cannot exist in valid dex. Strong: it must outrank a
2373                // strong boolean copy chain into the same reused slot
2374                // (weixin coroutine state machines).
2375                if let Expr::Field { owner: Some(o), cls, is_static: false, .. } = &**target {
2376                    if cls.is_empty() {
2377                        // array-length pseudo-field: no declaring class
2378                    } else if let Expr::Local { var, .. } = &**o {
2379                        ev(
2380                            &mut evidence,
2381                            *var,
2382                            JavaType::Object(cls.clone()),
2383                            true,
2384                        );
2385                    }
2386                }
2387                if let Expr::Local { var, .. } = &**target {
2388                    let strong = match &**value {
2389                        Expr::Const(_) => false,
2390                        Expr::Local { var: src, .. } => {
2391                            // A local copy is strong when the source and
2392                            // the target's register type are the same
2393                            // FAMILY (numeric↔numeric, reference↔
2394                            // reference) — a cross-family copy (int into
2395                            // a StringBuilder local) is merge-material
2396                            // residue and stays weak.
2397                            let src_ty = vt.vars.get(*src as usize).map(|v| v.ty.erased());
2398                            let tgt_ty = vt.vars.get(*var as usize).map(|v| v.ty.erased());
2399                            match (src_ty, tgt_ty) {
2400                                (Some(s), Some(g)) => {
2401                                    (s.is_numeric() && g.is_numeric())
2402                                        || (s.is_reference() && g.is_reference())
2403                                }
2404                                _ => false,
2405                            }
2406                        }
2407                        _ => true,
2408                    };
2409                    ev(&mut evidence, *var, value.type_ref().erased(), strong);
2410                }
2411            }
2412            expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
2413        }
2414        Stmt::Throw(e) => {
2415            expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
2416            if let Expr::Local { var, .. } = e {
2417                ev(
2418                    &mut evidence,
2419                    *var,
2420                    JavaType::Object("java/lang/Throwable".into()),
2421                    true,
2422                );
2423            }
2424        }
2425        Stmt::Return(Some(e)) => {
2426            expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
2427            if let Expr::Local { var, .. } = e {
2428                ev(&mut evidence, *var, ret.clone(), true);
2429            }
2430        }
2431        Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
2432            if let Expr::Local { var, .. } = e {
2433                ev(
2434                    &mut evidence,
2435                    *var,
2436                    JavaType::Object("java/lang/Object".into()),
2437                    true,
2438                );
2439            }
2440        }
2441        Stmt::ForEach {
2442            var,
2443            iterable,
2444            is_array,
2445            ..
2446        } => {
2447            if let Expr::Local { var: v0, .. } = iterable {
2448                if *is_array {
2449                    ev(&mut evidence, *v0, JavaType::Array(Box::new(JavaType::Int)), true);
2450                } else {
2451                    ev(
2452                        &mut evidence,
2453                        *v0,
2454                        JavaType::Object("java/lang/Object".into()),
2455                        true,
2456                    );
2457                }
2458            }
2459            let _ = var;
2460        }
2461        _ => {}
2462    });
2463    let _ = env;
2464
2465    // Return-position locals take the declared return type (also inside
2466    // nested returns — handled by the walk above).
2467
2468        // Vars dereferenced as field/method receivers or arrays: their
2469    // resolved type must be a dereferenceable class — a boxed Boolean
2470    // in the strong set (Kotlin Result registers hold `Boolean | impl`
2471    // across coroutine case arms) must lose to the real class even
2472    // when the Boolean evidence comes first (weixin ry0/h2's
2473    // `Boolean bool19` receiving `bool19.d = x`).
2474    let mut derefed: Vec<bool> = vec![false; vt.vars.len()];
2475    mark_derefs(body, &mut derefed);
2476    // Array-indexed vars: their resolved type must be an ARRAY when
2477    // the evidence has one (`num4[v68]` with Integer-typed num4 —
2478    // 需要数组但找到Integer family); member derefs keep the class
2479    // preference below.
2480    let mut arr_derefed: Vec<bool> = vec![false; vt.vars.len()];
2481    visit_all_exprs(body, &mut |x| {
2482        if let Expr::ArrayIndex { array, .. } = x {
2483            if let Expr::Local { var, .. } = &**array {
2484                if (*var as usize) < arr_derefed.len() {
2485                    arr_derefed[*var as usize] = true;
2486                }
2487            }
2488        }
2489        // `v != null` is STRONG reference evidence: the lifter only
2490        // renders the null form when the dex value view held a
2491        // reference register at that pc — an int-typed generation
2492        // compared against null is phi-confluence residue (yq0/g1's
2493        // `v95_g12 != null`, 546-line 二元运算符 family). Strong Object
2494        // makes the resolver retype and split_generations separate the
2495        // int writes into their own generation.
2496        if let Expr::Bin { op, l, r, .. } = x {
2497            if matches!(op, BinOp::Eq | BinOp::Ne)
2498                && (matches!(&**r, Expr::Const(ConstVal::Null))
2499                    || matches!(&**l, Expr::Const(ConstVal::Null)))
2500            {
2501                for side in [l, r] {
2502                    if let Expr::Local { var, .. } = &**side {
2503                        ev(
2504                            &mut evidence,
2505                            *var,
2506                            JavaType::Object("java/lang/Object".into()),
2507                            true,
2508                        );
2509                    }
2510                }
2511            }
2512        }
2513    });
2514    const FINAL_JDK: &[&str] = &[
2515        "java/lang/Boolean",
2516        "java/lang/String",
2517        "java/lang/Integer",
2518        "java/lang/Long",
2519        "java/lang/Short",
2520        "java/lang/Byte",
2521        "java/lang/Character",
2522        "java/lang/Float",
2523        "java/lang/Double",
2524    ];
2525    for (i, evs) in evidence.iter().enumerate() {
2526        let info = &vt.vars[i];
2527        if info.is_param {
2528            continue;
2529        }
2530        let strong: Vec<JavaType> = evs
2531            .iter()
2532            .filter(|(_, s)| *s)
2533            .map(|(t, _)| t.clone())
2534            .collect();
2535        let all: Vec<JavaType> = evs.iter().map(|(t, _)| t.clone()).collect();
2536        if info.ty.erased().is_reference() {
2537            // Materialization residue: no strong definition, and the weak
2538            // pool names ≥2 DIFFERENT classes (a Kotlin `when` lowered
2539            // every branch's value into one register slot) — neither
2540            // class can win, every assignment needs boxing headroom:
2541            // widen to Object (uses get receiver casts).
2542            if strong.is_empty() {
2543                let refs: std::collections::HashSet<&str> = all
2544                    .iter()
2545                    .filter_map(|t| match t {
2546                        JavaType::Object(n) if n.as_ref() != "java/lang/Object" => {
2547                            Some(n.as_ref())
2548                        }
2549                        _ => None,
2550                    })
2551                    .collect();
2552                let mixed = all.iter().any(|t| t.is_numeric());
2553                if refs.len() >= 2 || (mixed && !refs.is_empty()) || (mixed && all.iter().any(|t| matches!(t, JavaType::Array(_)))) {
2554                    vt.vars[i].ty =
2555                        TypeRef::J(JavaType::Object("java/lang/Object".into()));
2556                    continue;
2557                }
2558            }
2559                        // Array-dereferenced vars prefer array evidence (an
2560            // Integer producer cannot be indexed).
2561            if arr_derefed[i] {
2562                let arr = strong
2563                    .iter()
2564                    .chain(all.iter())
2565                    .find(|t| matches!(t, JavaType::Array(_)))
2566                    .cloned();
2567                if let Some(t) = arr {
2568                    vt.vars[i].ty = TypeRef::J(t);
2569                    continue;
2570                }
2571            }
2572            // Strong definitions outrank weak expectations. A
2573            // dereferenced var prefers a real class over final JDK
2574            // boxes (Boolean can never carry `.d = x`).
2575            let deref_pick = || {
2576                if !derefed[i] {
2577                    return None;
2578                }
2579                strong
2580                    .iter()
2581                    .find(|t| {
2582                        matches!(t, JavaType::Object(n)
2583                            if !n.is_empty()
2584                                && n.as_ref() != "java/lang/Object"
2585                                && !FINAL_JDK.contains(&n.as_ref()))
2586                    })
2587                    .cloned()
2588            };
2589            if let Some(t) = deref_pick()
2590                .or_else(|| pick_object(&strong))
2591                .or_else(|| pick_object(&all))
2592            {
2593                vt.vars[i].ty = TypeRef::J(t);
2594            }
2595        } else if let Some(t) = pick_object(&strong) {
2596            // A strong reference definition (typed producer or field
2597            // write) outranks a strong boolean copy: Boolean assigns
2598            // INTO an Object slot legally, never the reverse.
2599            vt.vars[i].ty = TypeRef::J(t);
2600        } else if strong.contains(&JavaType::Boolean) {
2601            // A boolean-typed SOURCE assigned into the variable is a
2602            // definition (`v26 = p1` with boolean p1); the 0/1 literals
2603            // that share the pool are boolean sugar as often as not.
2604            vt.vars[i].ty = TypeRef::J(JavaType::Boolean);
2605        } else if !all.is_empty() && all.iter().all(|t| t.is_numeric()) {
2606            if let Some(t) = pick_numeric(&all) {
2607                vt.vars[i].ty = TypeRef::J(t);
2608            }
2609        } else {
2610            // Mixed-family materialization on a numeric-declared slot
2611            // (int slot receiving StringBuilders — the register was the
2612            // `when` accumulator): Object, same as the reference side.
2613            let refs = all.iter().filter(|t| t.is_reference()).count();
2614            let nums = all.iter().filter(|t| t.is_numeric()).count();
2615            if refs > 0 && nums > 0 {
2616                vt.vars[i].ty =
2617                    TypeRef::J(JavaType::Object("java/lang/Object".into()));
2618            } else if let Some(t) = pick_object(&strong).or_else(|| pick_object(&all)) {
2619                vt.vars[i].ty = TypeRef::J(t);
2620            }
2621        }
2622    }
2623
2624    // Rewrite embedded Local types. Borrowed view + write-only-on-change:
2625    // the table clone was n_vars JavaType clones per method, and the
2626    // rewrite cloned a type into EVERY Local node — most already carry
2627    // the right one (JavaType::Object clones allocate).
2628    let types: Vec<&TypeRef> = vt.vars.iter().map(|v| &v.ty).collect();
2629    rewrite_exprs(body, &mut |e| {
2630        deep_rewrite(e, &mut |x| {
2631            if let Expr::Local { var, ty } = x {
2632                if let Some(want) = types.get(*var as usize) {
2633                    if ty != *want {
2634                        *ty = (*want).clone();
2635                    }
2636                }
2637            }
2638        });
2639    });
2640
2641    // Numeric literals take their variable's declared width (`double v = 0L`
2642    // prints as `0.0`; `int x = 5L` as `5`).
2643    coerce_num_consts(body, &types);
2644}
2645
2646fn coerce_num_consts(body: &mut Stmt, types: &[&TypeRef]) {
2647    walk_mut_deep(body, &mut |st| {
2648        let (var, val): (u32, &mut Expr) = match st {
2649            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => match &mut **target {
2650                Expr::Local { var, .. } => (*var, value),
2651                _ => return,
2652            },
2653            Stmt::LocalDef {
2654                var, init: Some(e), ..
2655            } => (*var, e),
2656            _ => return,
2657        };
2658        let want = match types.get(var as usize) {
2659            Some(TypeRef::J(t)) => t.clone(),
2660            _ => return,
2661        };
2662        let rewritten = match &*val {
2663            Expr::Const(ConstVal::Long(l)) => match &want {
2664                JavaType::Double => Some(Expr::Const(ConstVal::Double(*l as f64))),
2665                JavaType::Float => Some(Expr::Const(ConstVal::Float(*l as f32))),
2666                _ => None,
2667            },
2668            Expr::Const(ConstVal::Int(i)) => match &want {
2669                JavaType::Double => Some(Expr::Const(ConstVal::Double(*i as f64))),
2670                JavaType::Float => Some(Expr::Const(ConstVal::Float(*i as f32))),
2671                _ => None,
2672            },
2673            _ => None,
2674        };
2675        if let Some(e) = rewritten {
2676            *val = e;
2677        }
2678    });
2679}
2680
2681fn pick_object(evs: &[JavaType]) -> Option<JavaType> {
2682    evs.iter()
2683        .find(|t| {
2684            t.is_reference()
2685                && !matches!(t, JavaType::Object(n) if n.is_empty() || n.as_ref() == "java/lang/Object")
2686        })
2687        .cloned()
2688}
2689
2690fn pick_numeric(evs: &[JavaType]) -> Option<JavaType> {
2691    if evs.is_empty() {
2692        return None;
2693    }
2694    let mut t = evs[0].clone();
2695    for x in &evs[1..] {
2696        t = join_numeric(&t, x);
2697    }
2698    Some(t)
2699}
2700
2701fn join_numeric(a: &JavaType, b: &JavaType) -> JavaType {
2702    if a == b {
2703        return a.clone();
2704    }
2705    match (a, b) {
2706        (JavaType::Double, _) | (_, JavaType::Double) => JavaType::Double,
2707        (JavaType::Float, _) | (_, JavaType::Float) => JavaType::Float,
2708        (JavaType::Long, _) | (_, JavaType::Long) => JavaType::Long,
2709        (JavaType::Int, x) if x.is_integral() => JavaType::Int,
2710        (x, JavaType::Int) if x.is_integral() => JavaType::Int,
2711        (x, y) if x.is_integral() && y.is_integral() => JavaType::Int,
2712        _ => JavaType::Int,
2713    }
2714}
2715
2716/// Gather per-var type evidence from one expression.
2717fn expr_evidence<F: FnMut(u32, JavaType)>(e: &Expr, f: &mut F) {
2718    visit_exprs(e, &mut |x| match x {
2719        Expr::Method {
2720            owner,
2721            args,
2722            desc,
2723            is_static,
2724            cls,
2725            ..
2726        } => {
2727            if !*is_static {
2728                if let Some(o) = owner {
2729                    if let Expr::Local { var, .. } = &**o {
2730                        f(*var, JavaType::Object(cls.clone()));
2731                    }
2732                }
2733            }
2734            for (i, a) in args.iter().enumerate() {
2735                if let Expr::Local { var, .. } = a {
2736                    if let Some(t) = desc.args.get(i) {
2737                        f(*var, t.clone());
2738                    }
2739                }
2740            }
2741        }
2742        Expr::Field {
2743            owner,
2744            cls,
2745            ty,
2746            is_static,
2747            ..
2748        } => {
2749            if !*is_static && !cls.is_empty() {
2750                if let Some(o) = owner {
2751                    if let Expr::Local { var, .. } = &**o {
2752                        // The field ref's declaring class is dex-exact
2753                        // evidence — the old hardcoded Object let a
2754                        // boolean copy chain outvote it (weixin ry0/h2's
2755                        // `Boolean bool19` receiving `bool19.d = x` —
2756                        // the 339-line 找不到符号 变量 d family).
2757                        // EMPTY cls = the array-length pseudo-field
2758                        // (lift models `a.length` as Field{cls:""}) —
2759                        // Object("") evidence poisoned pick_object into
2760                        // empty-typed declarations and `() x` casts.
2761                        f(*var, JavaType::Object(cls.clone()));
2762                    }
2763                }
2764            }
2765            let _ = ty;
2766        }
2767        // `v != null` / `v == null`: the register holds a reference at
2768        // that point — weak Object evidence keeps int-typed generations
2769        // from winning the pool (yq0/g1's `v95_g12 != null` with int
2770        // v95_g12 — 二元运算符 '!=' 546-line family).
2771        Expr::Bin { op, l, r, .. }
2772            if matches!(op, BinOp::Eq | BinOp::Ne)
2773                && (matches!(&**r, Expr::Const(ConstVal::Null))
2774                    || matches!(&**l, Expr::Const(ConstVal::Null))) =>
2775        {
2776            for side in [l, r] {
2777                if let Expr::Local { var, .. } = &**side {
2778                    f(*var, JavaType::Object("java/lang/Object".into()));
2779                }
2780            }
2781        }
2782        Expr::ArrayIndex { array, .. } => {
2783            if let Expr::Local { var, ty, .. } = &**array {
2784                // Reinforce the local's KNOWN array type: the hardcoded
2785                // int[] fallback contradicted a typed array local (a
2786                // register reused for byte[], int[] then byte[] across
2787                // one clinit labeled the byte[] defs `int[]`).
2788                let t = ty.erased();
2789                if matches!(t, JavaType::Array(_)) {
2790                    f(*var, t);
2791                } else {
2792                    f(*var, JavaType::Array(Box::new(JavaType::Int)));
2793                }
2794            }
2795        }
2796        Expr::Assign { .. } => {
2797            // Covered by the typed strong-evidence walk in infer_types;
2798            // keeping a weak duplicate here let expectations outvote it.
2799        }
2800        Expr::Cast { e: inner, .. } => {
2801            // A cast is an EXPLICIT narrowing at the use site — it must
2802            // NOT feed the cast target as evidence for the variable
2803            // (`Object get2 = list.get(2); j((String) get2);` had get2
2804            // retyped to String, making the assignment incompatible).
2805            // jadx's bounds carry direction: a USE bound narrows nothing.
2806            let _ = inner;
2807        }
2808        Expr::InstanceOf { e: inner, .. } => {
2809            let _ = inner;
2810        }
2811        _ => {}
2812    });
2813}
2814
2815/// Mark locals that OWN a non-static field access and whose inferred type
2816/// is a concrete `$<digits>` class (a d8/R8-desugared lambda or a Kotlin
2817/// suspend-lambda). Emit demotes such a type to its SAM interface in
2818/// DECLARED positions (`Function2 v = new Outer$..$1(..)`) for phi-friendly
2819/// readability — but interfaces carry no instance fields, so the Kotlin
2820/// coroutine `create()` capture writes (`v.L$0 = obj`, also I$/J$) render
2821/// "找不到符号 变量 L$0" (weibo MutableScatterMap$..$iterator$1, ~1k).
2822/// Forcing the concrete declared type keeps the field access legal; the phi
2823/// case is excluded because a disagreed merge stays Object (wide_stack_vars),
2824/// never a specific concrete class. Gate mirrors emit's demotion exactly
2825/// (last `$` segment all digits) so nothing else changes.
2826pub fn mark_field_owner_concrete(vt: &mut VarTable, body: &mut Stmt) {
2827    let mut owners: HashSet<u32> = HashSet::default();
2828    rewrite_exprs(body, &mut |e| {
2829        visit_exprs(e, &mut |x| {
2830            if let Expr::Field {
2831                owner: Some(o),
2832                is_static: false,
2833                ..
2834            } = x
2835            {
2836                if let Expr::Local { var, .. } = &**o {
2837                    owners.insert(*var);
2838                }
2839            }
2840        });
2841    });
2842    for var in owners {
2843        let i = var as usize;
2844        if i >= vt.vars.len() {
2845            continue;
2846        }
2847        if let JavaType::Object(cls) = vt.vars[i].ty.erased() {
2848            if let Some(last) = cls.rsplit('$').next() {
2849                if !last.is_empty() && last.chars().all(|c| c.is_ascii_digit()) {
2850                    vt.force_concrete_vars.insert(var);
2851                }
2852            }
2853        }
2854    }
2855    // EVERY local whose resolved type is a concrete `$<digits>` class
2856    // keeps its binary name, not just field owners: when one side of an
2857    // assignment falls back to the SAM/super (`ContinuationImpl v7`) and
2858    // its sibling stays concrete (`$reportWhenComplete$1 v9`), the pair
2859    // is an uncastable "ContinuationImpl无法转换为…$1" (weibo coroutine
2860    // prologues ×282). The anon fallback still serves INTERFACE-typed
2861    // phi slots (branches minting different lambdas) — their vt type is
2862    // the join, never the $N class.
2863    let concrete: Vec<u32> = vt
2864        .vars
2865        .iter()
2866        .filter(|v| {
2867            if let JavaType::Object(cls) = v.ty.erased() {
2868                cls.rsplit('$')
2869                    .next()
2870                    .is_some_and(|l| !l.is_empty() && l.chars().all(|c| c.is_ascii_digit()))
2871            } else {
2872                false
2873            }
2874        })
2875        .map(|v| v.id)
2876        .collect();
2877    for var in concrete {
2878        vt.force_concrete_vars.insert(var);
2879    }
2880}
2881
2882/// Type inference can leave a specific-reference-typed target assigned
2883/// from a `java/lang/Object`-typed value: a phi that merged String and
2884/// Object then settled on String (`String str4; ... str4 = obj;` where
2885/// `obj` is an Object field — y5/n.java), or an Object local flowing to a
2886/// typed field. Java requires a narrowing cast there; insert `(T) value`
2887/// at the USE site (round-40 philosophy: narrow at use, never retype the
2888/// declaration). Guarded tightly: only when the value's resolved static
2889/// type is EXACTLY `java/lang/Object` (the untyped top — a known subtype
2890/// is never re-cast), the value is not a null const (assignable to any
2891/// ref) nor already a cast, and the target is a specific reference type
2892/// (Object→int/boolean is unboxing, a different problem left alone).
2893pub fn insert_object_narrowing_casts(
2894    vt: &VarTable,
2895    body: &mut Stmt,
2896    pool: &DexPool,
2897    ret: &JavaType,
2898) {
2899    // Resolve a value's static type through the VarTable for locals (the
2900    // embedded Local ty can lag infer_types), else the expr's own type.
2901    let value_ty = |e: &Expr| -> JavaType {
2902        match e {
2903            Expr::Local { var, .. } => vt.var(*var).ty.erased(),
2904            other => other.type_ref().erased(),
2905        }
2906    };
2907    let is_top_object = |e: &Expr| -> bool {
2908        value_ty(e) == JavaType::Object("java/lang/Object".into())
2909    };
2910    // A specific reference target type (a class other than
2911    // java/lang/Object, or an array — `String[] v = obj` needs the
2912    // cast exactly like `String v = obj`).
2913    let specific_ref = |ty: &TypeRef| -> Option<TypeRef> {
2914        match ty.erased() {
2915            JavaType::Object(c) if c.as_ref() != "java/lang/Object" => Some(ty.clone()),
2916            JavaType::Array(_) => Some(ty.clone()),
2917            _ => None,
2918        }
2919    };
2920    let castable = |e: &Expr| -> bool {
2921        is_top_object(e)
2922            && !matches!(e, Expr::Const(_) | Expr::Cast { .. } | Expr::InstanceOf { .. })
2923    };
2924    // Downcast witness: a SPECIFIC-ref value whose static type does not
2925    // fit the specific-ref target (`v9($1) = v7` with v7:
2926    // ContinuationImpl — weibo coroutine prologue ×282). The dex move
2927    // was verifier-proven against the target type, so the cast states
2928    // the bytecode truth; an actually-unconvertible static pair (both
2929    // classes, unrelated — the value view conflated) rides the
2930    // emitter's (Object) relay and still compiles. Framework types the
2931    // pool cannot order take the cast too: a downcast renders directly,
2932    // an unrelated pair relays.
2933    let needs_downcast = |e: &Expr, tt: &JavaType| -> bool {
2934        if matches!(e, Expr::Const(_) | Expr::Cast { .. } | Expr::InstanceOf { .. }) {
2935            return false;
2936        }
2937        let v = value_ty(e);
2938        let (JavaType::Object(vn), JavaType::Object(tn)) = (&v, tt) else {
2939            return false;
2940        };
2941        if vn.as_ref() == "java/lang/Object" || tn.as_ref() == "java/lang/Object" {
2942            return false;
2943        }
2944        vn != tn && !pool.is_subtype(vn, tn)
2945    };
2946    walk_mut_deep(body, &mut |st| match st {
2947        Stmt::ExprStmt(Expr::Assign {
2948            target,
2949            value,
2950            op: AssignOp::Plain,
2951            ..
2952        }) => {
2953            let tgt = match &**target {
2954                Expr::Local { var, .. } => vt.var(*var).ty.clone(),
2955                Expr::Field { ty, .. } => ty.clone(),
2956                // Array-element store `arr[i] = value`: the target type is
2957                // the ELEMENT type. Without this arm an Object→element
2958                // assignment (`String[] a; a[0] = pair.first` — Pair.first
2959                // erases to Object) skipped the cast and failed "Object
2960                // 无法转换为String" (weixin ×~150). Resolve the array's type
2961                // through the vt (the embedded Local ty can lag infer_types).
2962                Expr::ArrayIndex { array, .. } => {
2963                    let aty = match &**array {
2964                        Expr::Local { var, .. } => vt.var(*var).ty.erased(),
2965                        other => other.type_ref().erased(),
2966                    };
2967                    match aty {
2968                        JavaType::Array(inner) => TypeRef::J(*inner),
2969                        _ => return,
2970                    }
2971                }
2972                _ => return,
2973            };
2974            if let Some(t) = specific_ref(&tgt) {
2975                if castable(value) || needs_downcast(value, &tgt.erased()) {
2976                    let v = std::mem::replace(value, Box::new(Expr::This));
2977                    **value = Expr::Cast { ty: t, e: v };
2978                }
2979            }
2980        }
2981        Stmt::LocalDef { var, init: Some(value), .. } => {
2982            if let Some(t) = specific_ref(&vt.var(*var).ty) {
2983                let tt = vt.var(*var).ty.erased();
2984                if castable(value) || needs_downcast(value, &tt) {
2985                    let v = std::mem::replace(value, Expr::This);
2986                    *value = Expr::Cast {
2987                        ty: t,
2988                        e: Box::new(v),
2989                    };
2990                }
2991            }
2992        }
2993        Stmt::Return(Some(value)) => {
2994            if let Some(t) = specific_ref(&TypeRef::J(ret.clone())) {
2995                if castable(value) || needs_downcast(value, ret) {
2996                    let v = std::mem::replace(value, Expr::This);
2997                    *value = Expr::Cast { ty: t, e: Box::new(v) };
2998                }
2999            }
3000        }
3001        _ => {}
3002    });
3003    // Descriptor-exact formal casts at CALL args: the dex already
3004    // resolved the target method, so casting a top-Object actual to
3005    // its declared formal cannot shift overload resolution (the r57
3006    // args lesson applies to GUESSED casts, not descriptor-exact
3007    // ones). `"source_type:".concat(obj2)`, `new my0.j(obj2, ..)` —
3008    // Object无法转换为String family.
3009    walk_stmt_exprs(body, &mut |e| {
3010        deep_rewrite(e, &mut |x| {
3011            let (args, arg_tys): (&mut Vec<Expr>, &[JavaType]) = match x {
3012                Expr::Method { desc, args, is_dynamic: false, .. } => {
3013                    (args, desc.args.as_slice())
3014                }
3015                // Ctor calls are their own variant — the dex resolved the
3016                // exact <init>, so formal casts are just as safe here
3017                // (`new r(obj)` — Object无法转换为r family).
3018                Expr::New { arg_tys, args, .. } => (args, arg_tys.as_slice()),
3019                _ => return,
3020            };
3021            for (i, a) in args.iter_mut().enumerate() {
3022                let Some(formal) = arg_tys.get(i) else {
3023                    continue;
3024                };
3025                let Some(t) = specific_ref(&TypeRef::J(formal.clone())) else {
3026                    continue;
3027                };
3028                if !(castable(a) || needs_downcast(a, formal)) {
3029                    continue;
3030                }
3031                let v = std::mem::replace(a, Expr::Const(ConstVal::Null));
3032                *a = Expr::Cast { ty: t, e: Box::new(v) };
3033            }
3034        });
3035    });
3036}
3037
3038/// Numeric narrowing/widening mismatches at Assign / LocalDef / Return
3039/// positions get an explicit cast: dex int-to-byte / long-to-int flows
3040/// are implicit in the register machine, Java rejects them ("从long
3041/// 转换到int可能会有损失" — okio Buffer ×321 weibo lines). Casts at
3042/// these three positions are descriptor-faithful and cannot shift
3043/// overload resolution (the r57 args-position lesson). Boolean/numeric
3044/// mixes are NOT castable in Java and stay with the generation-split
3045/// machinery.
3046pub fn fix_primitive_assign_casts(vt: &VarTable, body: &mut Stmt, ret_ty: &JavaType) {
3047    fn numlike(t: &JavaType) -> bool {
3048        matches!(
3049            t,
3050            JavaType::Byte
3051                | JavaType::Short
3052                | JavaType::Int
3053                | JavaType::Char
3054                | JavaType::Long
3055                | JavaType::Float
3056                | JavaType::Double
3057        )
3058    }
3059    fn val_ty(e: &Expr, vt: &VarTable) -> JavaType {
3060        match e {
3061            Expr::Local { var, .. } if (*var as usize) < vt.vars.len() => {
3062                vt.var(*var).ty.erased()
3063            }
3064            other => other.type_ref().erased(),
3065        }
3066    }
3067    fn wrap(ty: &JavaType, value: &mut Expr) {
3068        if matches!(value, Expr::Cast { .. }) {
3069            return;
3070        }
3071        if val_ty_static(value) == *ty {
3072            return;
3073        }
3074        let taken = std::mem::replace(value, Expr::Const(ConstVal::Null));
3075        *value = Expr::Cast {
3076            ty: TypeRef::J(ty.clone()),
3077            e: Box::new(taken),
3078        };
3079    }
3080    fn val_ty_static(e: &Expr) -> JavaType {
3081        e.type_ref().erased()
3082    }
3083    /// Target-typed coercion for one assignment value. Numeric mixes
3084    /// cast; boolean mixes CONVERT (Java has no cast between boolean
3085    /// and any numeric): `d = b` → `(double) (b ? 1 : 0)`, `b = i` →
3086    /// `i != 0` — mirrors fix_primitive_arg_bridges' formal-side arms.
3087    /// The generation split cannot own these: at an if-join the
3088    /// one-sided split is undone (rename_gen_back) and the incompatible
3089    /// assign re-exposes ("boolean无法转换为double", weixin s9/t01-f).
3090    fn coerce(tt: &JavaType, value: &mut Expr, vt: &VarTable) {
3091        // Raw const bits against a floating target are the FLOAT value,
3092        // not an int to widen (`float f = <0x41000000 bits>` is 8.0f;
3093        // the (float) cast path would silently store 1.09e9f). Mirror of
3094        // fix_primitive_arg_bridges' descriptor-formal reinterpretation.
3095        match (&*value, tt) {
3096            (Expr::Const(ConstVal::Int(b)), JavaType::Float) => {
3097                *value = Expr::Const(ConstVal::Float(f32::from_bits(*b as u32)));
3098                return;
3099            }
3100            (Expr::Const(ConstVal::Long(b)), JavaType::Double) => {
3101                *value = Expr::Const(ConstVal::Double(f64::from_bits(*b as u64)));
3102                return;
3103            }
3104            (Expr::Const(ConstVal::Float(f)), JavaType::Int) => {
3105                *value = Expr::Const(ConstVal::Int(f.to_bits() as i32));
3106                return;
3107            }
3108            (Expr::Const(ConstVal::Double(d)), JavaType::Long) => {
3109                *value = Expr::Const(ConstVal::Long(d.to_bits() as i64));
3110                return;
3111            }
3112            _ => {}
3113        }
3114        let vt_val = val_ty(value, vt);
3115        let tt_bool = matches!(tt, JavaType::Boolean);
3116        let val_bool = matches!(vt_val, JavaType::Boolean);
3117        if tt_bool && !val_bool && numlike(&vt_val) {
3118            if matches!(
3119                value,
3120                Expr::Const(_)
3121                    | Expr::Bin { op: BinOp::Eq | BinOp::Ne, .. }
3122                    | Expr::Un { op: UnOp::Not, .. }
3123            ) {
3124                // Bare 0/1 constants render as false/true through the
3125                // emitter's bool coercion — wrapping them in `!= 0`
3126                // produced `0 != 0` noise the value-diamond fold then
3127                // had to re-interpret.
3128                return;
3129            }
3130            let taken = std::mem::replace(value, Expr::Const(ConstVal::Null));
3131            *value = Expr::Bin {
3132                op: BinOp::Ne,
3133                l: Box::new(taken),
3134                r: Box::new(Expr::Const(ConstVal::Int(0))),
3135                ty: Some(TypeRef::J(JavaType::Boolean)),
3136            };
3137        } else if !tt_bool && val_bool && numlike(tt) {
3138            if matches!(value, Expr::Cast { .. }) {
3139                return;
3140            }
3141            let taken = std::mem::replace(value, Expr::Const(ConstVal::Null));
3142            *value = Expr::Cast {
3143                ty: TypeRef::J(tt.clone()),
3144                e: Box::new(Expr::Cond {
3145                    c: Box::new(taken),
3146                    t: Box::new(Expr::Const(ConstVal::Int(1))),
3147                    f: Box::new(Expr::Const(ConstVal::Int(0))),
3148                }),
3149            };
3150        } else if numlike(tt) && numlike(&vt_val) {
3151            wrap(tt, value);
3152        }
3153    }
3154    walk_mut_deep(body, &mut |st| match st {
3155        Stmt::ExprStmt(Expr::Assign {
3156            target,
3157            op: AssignOp::Plain,
3158            value,
3159        }) => {
3160            let tt = match &**target {
3161                Expr::Local { var, .. } if (*var as usize) < vt.vars.len() => {
3162                    vt.var(*var).ty.erased()
3163                }
3164                Expr::Field { ty, .. } => ty.erased(),
3165                // Array store: the element type is the target type
3166                // (`sFormatStr[i] = v7` into char[] — "从int转换到char
3167                // 可能会有损失", weibo TimeUtils).
3168                Expr::ArrayIndex { array, .. } => match array.type_ref().erased() {
3169                    JavaType::Array(inner) => *inner,
3170                    _ => return,
3171                },
3172                _ => return,
3173            };
3174            coerce(&tt, value, vt);
3175        }
3176        Stmt::LocalDef {
3177            var,
3178            init: Some(value),
3179            ..
3180        } => {
3181            if (*var as usize) >= vt.vars.len() {
3182                return;
3183            }
3184            let tt = vt.var(*var).ty.erased();
3185            coerce(&tt, value, vt);
3186        }
3187        Stmt::Return(Some(e)) => {
3188            if !numlike(ret_ty) || !numlike(&val_ty(e, vt)) {
3189                return;
3190            }
3191            wrap(ret_ty, e);
3192        }
3193        _ => {}
3194    });
3195}
3196
3197/// Subclass-field downcast witness + assign-cast narrowing.
3198///
3199/// R8 pushes fields DOWN into leaf classes (weibo: coroutine `label`
3200/// lives on each `Foo$bar$1` continuation, NOT on the rendered
3201/// BaseContinuationImpl/ContinuationImpl supers) while a check-cast in
3202/// the same flow types the receiver as the SUPERCLASS. The dex field
3203/// ref names the leaf (`$1->label` — verifier-legal through the
3204/// instance-of branch type), but Java resolves `((ContinuationImpl) c)
3205/// .label` against ContinuationImpl — "找不到符号 变量 label" (weibo
3206/// ×420). When the field's dex owner is a strict subtype of the
3207/// receiver's static type and the receiver's hierarchy does NOT
3208/// declare the name, re-target the receiver cast to the owner (the
3209/// runtime object IS the owner type — the verifier proved it).
3210///
3211/// Second rule: a `(T) e` value assigned into an S-typed target where
3212/// S <: T strictly ("ContinuationImpl无法转换为$reportWhenComplete$1")
3213/// narrows the cast to S — same verifier guarantee, and an assignment
3214/// position has no overload resolution to perturb.
3215pub fn fix_field_owner_downcasts(body: &mut Stmt, vt: &VarTable, pool: &DexPool) {
3216    fn owner_internal(o: &Expr, vt: &VarTable) -> Option<std::sync::Arc<str>> {
3217        let t = match o {
3218            Expr::Local { var, .. } => vt.var(*var).ty.erased(),
3219            other => other.type_ref().erased(),
3220        };
3221        match t {
3222            JavaType::Object(n) => Some(n),
3223            _ => None,
3224        }
3225    }
3226    fn declares_up(pool: &DexPool, class: &str, name: &str) -> bool {
3227        let mut cur = Some(class.to_string());
3228        let mut hops = 0;
3229        while let Some(c) = cur {
3230            hops += 1;
3231            if hops > 64 {
3232                return false;
3233            }
3234            let Some(pc) = pool.get(&c) else { return false };
3235            if pc
3236                .instance_fields
3237                .iter()
3238                .chain(pc.static_fields.iter())
3239                .any(|f| f.name.as_str() == name)
3240            {
3241                return true;
3242            }
3243            cur = pc.super_name.clone();
3244        }
3245        false
3246    }
3247    walk_stmt_exprs(body, &mut |e| {
3248        deep_rewrite(e, &mut |x| {
3249            if let Expr::Field {
3250                owner: Some(o),
3251                cls,
3252                name,
3253                is_static: false,
3254                ..
3255            } = x
3256            {
3257                let Some(ot) = owner_internal(o, vt) else {
3258                    return;
3259                };
3260                if ot.as_ref() == cls.as_ref() || !pool.is_subtype(cls, &ot) {
3261                    return;
3262                }
3263                if declares_up(pool, &ot, name) || !declares_up(pool, cls, name) {
3264                    return;
3265                }
3266                let ty = TypeRef::J(JavaType::Object(cls.clone()));
3267                if let Expr::Cast { ty: ct, .. } = &mut **o {
3268                    *ct = ty;
3269                } else {
3270                    let taken = std::mem::replace(&mut **o, Expr::This);
3271                    **o = Expr::Cast { ty, e: Box::new(taken) };
3272                }
3273            }
3274            // Rule 2: `(T) e` into a strict-subtype target narrows to S.
3275            let (tgt_ty, value) = match x {
3276                Expr::Assign { target, op: AssignOp::Plain, value } => {
3277                    let t = match &**target {
3278                        Expr::Local { var, .. } => vt.var(*var).ty.erased(),
3279                        Expr::Field { ty, .. } => ty.erased(),
3280                        _ => return,
3281                    };
3282                    (t, value)
3283                }
3284                _ => return,
3285            };
3286            let JavaType::Object(sn) = tgt_ty else { return };
3287            if let Expr::Cast { ty, .. } = &mut **value {
3288                if let TypeRef::J(JavaType::Object(tn)) = ty {
3289                    if tn.as_ref() != sn.as_ref() && pool.is_subtype(&sn, tn) {
3290                        *ty = TypeRef::J(JavaType::Object(sn.clone()));
3291                    }
3292                }
3293            }
3294        });
3295    });
3296    // LocalDef inits: same narrowing against the declared var type.
3297    walk_mut_deep(body, &mut |st| {
3298        if let Stmt::LocalDef { var, init: Some(value), .. } = st {
3299            let tt = if (*var as usize) < vt.vars.len() {
3300                vt.vars[*var as usize].ty.erased()
3301            } else {
3302                return;
3303            };
3304            let JavaType::Object(sn) = tt else { return };
3305            if let Expr::Cast { ty, .. } = value {
3306                if let TypeRef::J(JavaType::Object(tn)) = ty {
3307                    if tn.as_ref() != sn.as_ref() && pool.is_subtype(&sn, tn) {
3308                        *ty = TypeRef::J(JavaType::Object(sn.clone()));
3309                    }
3310                }
3311            }
3312        }
3313    });
3314}
3315
3316/// `==`/`!=` between UNRELATED class types is a compile error in Java
3317/// ("不可比较的类型: f0和a") while dex if-eqObj happily compares any two
3318/// references (weixin fn1/w1 ×298 files). One side takes an `(Object)`
3319/// witness cast — legal against anything. Interfaces and subtype-related
3320/// pairs are already comparable and stay untouched; java/lang/Object and
3321/// arrays-of it likewise.
3322pub fn fix_incomparable_equality(body: &mut Stmt, vt: &VarTable, pool: &crate::DexPool) {
3323    fn side_ty(e: &Expr, vt: &VarTable) -> JavaType {
3324        match e {
3325            Expr::Local { var, .. } if (*var as usize) < vt.vars.len() => {
3326                vt.var(*var).ty.erased()
3327            }
3328            other => other.type_ref().erased(),
3329        }
3330    }
3331    let incomparable = |a: &JavaType, b: &JavaType| -> bool {
3332        use JavaType::{Array, Object};
3333        match (a, b) {
3334            (Object(x), Object(y)) => {
3335                if x == y || x.as_ref() == "java/lang/Object" || y.as_ref() == "java/lang/Object"
3336                {
3337                    return false;
3338                }
3339                let xi = pool.get(x).map(|c| c.is_interface()).unwrap_or(true);
3340                let yi = pool.get(y).map(|c| c.is_interface()).unwrap_or(true);
3341                // Unknown classes (framework, not in pool): a cast is
3342                // harmless but usually unnecessary — treat as comparable
3343                // to stay conservative.
3344                if xi || yi {
3345                    return false;
3346                }
3347                !pool.is_subtype(x, y) && !pool.is_subtype(y, x)
3348            }
3349            (Array(_), Object(y)) | (Object(y), Array(_)) => {
3350                y.as_ref() != "java/lang/Object"
3351                    && y.as_ref() != "java/lang/Cloneable"
3352                    && y.as_ref() != "java/io/Serializable"
3353                    && !pool.get(y).map(|c| c.is_interface()).unwrap_or(false)
3354            }
3355            _ => false,
3356        }
3357    };
3358    let is_ref = |t: &JavaType| matches!(t, JavaType::Object(_) | JavaType::Array(_));
3359    walk_stmt_exprs(body, &mut |e| {
3360        deep_rewrite(e, &mut |x| {
3361            if let Expr::Bin {
3362                op: BinOp::Eq | BinOp::Ne,
3363                l,
3364                r,
3365                ..
3366            } = x
3367            {
3368                let lt = side_ty(l, vt);
3369                let rt = side_ty(r, vt);
3370                // `obj != 0` → `obj != null` with FINAL types: the
3371                // lift-time null_side_rewrite ran before infer_types
3372                // (its obj table saw the pre-inference int), leaving
3373                // `sQLiteClosable != 0` — "二元运算符 '!=' 的操作数
3374                // 类型错误" ×700 weixin.
3375                if is_ref(&lt) && matches!(&**r, Expr::Const(ConstVal::Int(0))) {
3376                    **r = Expr::Const(ConstVal::Null);
3377                    return;
3378                }
3379                if is_ref(&rt) && matches!(&**l, Expr::Const(ConstVal::Int(0))) {
3380                    **l = Expr::Const(ConstVal::Null);
3381                    return;
3382                }
3383                if incomparable(&lt, &rt) {
3384                    let taken = std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
3385                    **l = Expr::Cast {
3386                        ty: TypeRef::J(JavaType::Object("java/lang/Object".into())),
3387                        e: taken,
3388                    };
3389                }
3390            }
3391        });
3392    });
3393}
3394
3395/// Primitive bridges at CALL/CTOR argument positions. Dex slots are
3396/// untyped category-1 integers: the verifier happily passes a Z-slot to
3397/// a `(B)` formal (weibo's Meituan-Robust hotpatch boilerplate boxes
3398/// boolean params via `new Byte(zreg)` — 4.2k+ `对于Byte(boolean),
3399/// 找不到合适的构造器`), and obfuscated/Kotlin code passes 0/1 slots to
3400/// `(Z)` formals (`int无法转换为boolean` at call sites). Java source
3401/// cannot express either — bridge at the IR level:
3402///   Boolean actual → numeric formal:  `(byte) (b ? 1 : 0)`
3403///   numeric actual → Boolean formal:  `x != 0`
3404/// The target method is DESCRIPTOR-bound (bytecode method_ref), so a
3405/// cast/compare to the exact formal type cannot flip overload
3406/// resolution the way r57's inferred object casts did — the rendered
3407/// call selects the same signature the dex named.
3408pub fn fix_primitive_arg_bridges(body: &mut Stmt, vt: &VarTable, pool: &crate::DexPool) {
3409    fn val_ty(e: &Expr, vt: &VarTable) -> JavaType {
3410        match e {
3411            Expr::Local { var, .. } => vt.var(*var).ty.erased(),
3412            other => other.type_ref().erased(),
3413        }
3414    }
3415    #[allow(clippy::type_complexity)]
3416    let is_bool = |t: &JavaType| matches!(t, JavaType::Boolean);
3417    let is_num = |t: &JavaType| {
3418        matches!(
3419            t,
3420            JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
3421                | JavaType::Long | JavaType::Float | JavaType::Double
3422        )
3423    };
3424    // BORROWED-arg form: returns the bridged replacement without
3425    // consuming the original (a `mem::replace` + `Option` dance dropped
3426    // the argument on the None path — every Boolean→Boolean arg became
3427    // a literal `null`: 61k 引用不明确 in one battery run).
3428    fn bridge(actual: &JavaType, formal: &JavaType, arg: &Expr) -> Option<Expr> {
3429        // A dex const carries the CALLEE's BITS: `const v, #0x41000000`
3430        // feeding a float formal IS 8.0f. Rendering the raw int literal
3431        // (a) mis-selects overloads — a competing `a(Context,int):void`
3432        // beats the int→float-widened `a(Context,float):int` in javac's
3433        // phase order ("不兼容的类型: void无法转换为int", weibo ju.a
3434        // ×1,259) — and (b) corrupts the VALUE (widening turns the bits
3435        // into 1.09e9f). The descriptor formal is dex ground truth:
3436        // reinterpret. Symmetric for the float-const-into-int-formal
3437        // direction (caller's bits, callee's reading).
3438        match (arg, formal) {
3439            (Expr::Const(ConstVal::Int(b)), JavaType::Float) => {
3440                return Some(Expr::Const(ConstVal::Float(f32::from_bits(*b as u32))));
3441            }
3442            (Expr::Const(ConstVal::Long(b)), JavaType::Double) => {
3443                return Some(Expr::Const(ConstVal::Double(f64::from_bits(*b as u64))));
3444            }
3445            (Expr::Const(ConstVal::Float(f)), JavaType::Int) => {
3446                return Some(Expr::Const(ConstVal::Int(f.to_bits() as i32)));
3447            }
3448            (Expr::Const(ConstVal::Double(d)), JavaType::Long) => {
3449                return Some(Expr::Const(ConstVal::Long(d.to_bits() as i64)));
3450            }
3451            _ => {}
3452        }
3453        if matches!(actual, JavaType::Boolean)
3454            && matches!(
3455                formal,
3456                JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
3457                    | JavaType::Long | JavaType::Float | JavaType::Double
3458            )
3459        {
3460            Some(Expr::Cast {
3461                ty: TypeRef::J(formal.clone()),
3462                e: Box::new(Expr::Cond {
3463                    c: Box::new(arg.clone()),
3464                    t: Box::new(Expr::Const(ConstVal::Int(1))),
3465                    f: Box::new(Expr::Const(ConstVal::Int(0))),
3466                }),
3467            })
3468        } else if matches!(formal, JavaType::Boolean)
3469            && matches!(
3470                actual,
3471                JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
3472                    | JavaType::Long | JavaType::Float | JavaType::Double
3473            )
3474        {
3475            Some(Expr::Bin {
3476                op: BinOp::Ne,
3477                l: Box::new(arg.clone()),
3478                r: Box::new(Expr::Const(ConstVal::Int(0))),
3479                ty: Some(TypeRef::J(JavaType::Boolean)),
3480            })
3481        } else if matches!(
3482            actual,
3483            JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
3484                | JavaType::Long | JavaType::Float | JavaType::Double
3485        ) && matches!(
3486            formal,
3487            JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
3488                | JavaType::Long | JavaType::Float | JavaType::Double
3489        ) && !implicit_widening(actual, formal)
3490        {
3491            // Numeric NARROWING into a formal (int arg → char formal):
3492            // javac rejects the lossy conversion, or — worse when the
3493            // class has no wider overload — silently resolves a
3494            // DIFFERENT overload than the dex descriptor names
3495            // (SpannableStringBuilder.append(int) doesn't exist;
3496            // `append(char)` is "从int转换到char可能会有损失", weibo ×76).
3497            // The descriptor formal is the dex ground truth; casting to
3498            // it pins the overload the bytecode actually invoked.
3499            Some(Expr::Cast {
3500                ty: TypeRef::J(formal.clone()),
3501                e: Box::new(arg.clone()),
3502            })
3503        } else {
3504            None
3505        }
3506    }
3507    /// A bare `null` argument against a SPECIFIC reference formal takes
3508    /// the descriptor cast: `this(context, null)` matching both
3509    /// Builder(Context,Notification) and Builder(Context,String) is
3510    /// "对Builder的引用不明确" (weibo NotificationCompat; protobuf
3511    /// `new FieldSet.Builder(null)` ×54). The descriptor names the
3512    /// exact invoked ctor, so the cast is dex ground truth — the
3513    /// descriptor-exact doctrine, not a guessed overload shift. Bare
3514    /// null stays for Object formals (casting those would add noise
3515    /// without resolving anything javac could not already pick).
3516    /// Framework classes javac resolves through android.jar even when
3517    /// the dex pool lacks them — org.json.JSONObject is provided by the
3518    /// platform, so `publishGlobalEventToTopPage(long,String,null)`
3519    /// against a JSONObject formal is cast-resolvable (weixin ambiguous
3520    /// ×14) despite failing the pool lookup.
3521    fn fw_resolvable(n: &str) -> bool {
3522        n.starts_with("java/")
3523            || n.starts_with("javax/")
3524            || n.starts_with("android/")
3525            || n.starts_with("dalvik/")
3526            || n.starts_with("junit/")
3527            || n.starts_with("org/json/")
3528            || n.starts_with("org/w3c/")
3529            || n.starts_with("org/xml/")
3530            || n.starts_with("org/xmlpull/")
3531            || n.starts_with("org/apache/http/")
3532    }
3533    fn null_arg_cast(a: &mut Expr, f: &JavaType, pool: &DexPool) -> bool {
3534        if !matches!(&*a, Expr::Const(ConstVal::Null)) {
3535            return false;
3536        }
3537        if let JavaType::Object(n) = f {
3538            // Cast only to RESOLVABLE types: a phantom formal (R8 kept
3539            // the ref, dropped the class — androidx.window.sidecar)
3540            // would turn a compiling bare `null` into a fresh
3541            // cannot-find. Pool classes render into the output;
3542            // java/javax/android/dalvik resolve through android.jar.
3543            let resolvable = pool.get(n).is_some() || fw_resolvable(n);
3544            if n.as_ref() != "java/lang/Object" && resolvable {
3545                *a = Expr::Cast {
3546                    ty: TypeRef::J(f.clone()),
3547                    e: Box::new(Expr::Const(ConstVal::Null)),
3548                };
3549                return true;
3550            }
3551        } else if let JavaType::Array(_) = f {
3552            // Descriptor-exact ARRAY cast: the dex named this exact
3553            // overload; a bare null beside sibling overloads is javac-
3554            // ambiguous (`a(String,b2)` + `a(String,String[])` both match
3555            // — weibo org/a/c/b, 对a的引用不明确 family). Same phantom
3556            // gate on the element class: casting to a phantom-element
3557            // array would trade the ambiguity for a cannot-find.
3558            let mut e: &JavaType = f;
3559            while let JavaType::Array(inner) = e {
3560                e = &**inner;
3561            }
3562            let resolvable = match e {
3563                JavaType::Object(n) => pool.get(n).is_some() || fw_resolvable(n),
3564                _ => true, // primitive-element array
3565            };
3566            if resolvable {
3567                *a = Expr::Cast {
3568                    ty: TypeRef::J(f.clone()),
3569                    e: Box::new(Expr::Const(ConstVal::Null)),
3570                };
3571                return true;
3572            }
3573        }
3574        // Bare null deliberately stays (Object formal / phantom type):
3575        // the caller's ambiguity pin may still act on it.
3576        false
3577    }
3578    /// JLS 5.1.2 widening primitive conversion (char joins the chain
3579    /// only at int; byte/short do NOT widen to char).
3580    fn implicit_widening(a: &JavaType, f: &JavaType) -> bool {
3581        use JavaType::*;
3582        match (a, f) {
3583            (Byte, Short) | (Byte, Int) | (Byte, Long) | (Byte, Float) | (Byte, Double) => true,
3584            (Short, Int) | (Short, Long) | (Short, Float) | (Short, Double) => true,
3585            (Char, Int) | (Char, Long) | (Char, Float) | (Char, Double) => true,
3586            (Int, Long) | (Int, Float) | (Int, Double) => true,
3587            (Long, Float) | (Long, Double) => true,
3588            (Float, Double) => true,
3589            (x, y) => x == y,
3590        }
3591    }
3592    // Does the owner class declare ANOTHER method with the same name
3593    // and arity but a different signature? Then javac's most-specific
3594    // overload resolution can steal the call from the dex descriptor's
3595    // target (weibo 对a的引用不明确 ×391: `c.a(null)` against
3596    // a(ExecuteResult)/a(Exception)/a(Object) — the descriptor names
3597    // a(Object), but bare null leaves the two specific overloads
3598    // ambiguously most-specific). Framework owners are not in the
3599    // pool — conservatively report no competition.
3600    let overload_competes = |cls: &str, name: &str, args: &[JavaType]| -> bool {
3601        let Some(c) = pool.get(cls) else {
3602            return false;
3603        };
3604        c.all_methods().any(|m| {
3605            &*m.name == name
3606                && m.parsed_desc()
3607                    .is_some_and(|d| d.args.len() == args.len() && d.args != args)
3608        })
3609    };
3610    // Ctors: formal types via the pool (unanimous across same-arity
3611    // overloads, else skip — ambiguity must not guess).
3612    let ctor_formals = |cls: &str, n: usize| -> Option<Vec<JavaType>> {
3613        let c = pool.get(cls)?;
3614        let mut out: Option<Vec<JavaType>> = None;
3615        for m in c.all_methods() {
3616            if &*m.name != "<init>" {
3617                continue;
3618            }
3619            let Some(d) = m.parsed_desc() else { continue };
3620            if d.args.len() != n {
3621                continue;
3622            }
3623            match &out {
3624                None => out = Some(d.args.clone()),
3625                Some(prev) if *prev != d.args => return None,
3626                Some(_) => {}
3627            }
3628        }
3629        out
3630    };
3631    walk_stmt_exprs(body, &mut |e| {
3632        deep_rewrite(e, &mut |x| match x {
3633            Expr::Method {
3634                cls,
3635                name,
3636                desc,
3637                args,
3638                is_static,
3639                ..
3640            } => {
3641                if args.len() != desc.args.len() {
3642                    return;
3643                }
3644                let _ = is_static;
3645                let mut competes: Option<bool> = None;
3646                for (a, f) in args.iter_mut().zip(desc.args.iter()) {
3647                    if null_arg_cast(a, f, pool) {
3648                        continue;
3649                    }
3650                    if matches!(a, Expr::Cast { .. }) {
3651                        continue;
3652                    }
3653                    // Descriptor-exact ambiguity pin: a bare null
3654                    // against an Object formal (null_arg_cast
3655                    // deliberately leaves those bare — safe only
3656                    // WITHOUT competing specific overloads) or a
3657                    // strict-subtype arg can match several declared
3658                    // overloads; casting to the descriptor formal
3659                    // leaves exactly the dex-invoked method
3660                    // applicable (JLS 15.12.2 — the descriptor is
3661                    // ground truth, so the cast pins resolution
3662                    // instead of shifting it).
3663                    let subtype_arg = match (&val_ty(a, vt), f) {
3664                        (JavaType::Object(an), JavaType::Object(fn_)) => {
3665                            // java/lang/Object is the root: ANY reference
3666                            // arg is a strict subtype of it — pool.is_subtype
3667                            // cannot see framework chains (String -> Object),
3668                            // so the Object formal gets the direct rule.
3669                            (an != fn_ && pool.is_subtype(an, fn_))
3670                                || (fn_.as_ref() == "java/lang/Object"
3671                                    && an.as_ref() != "java/lang/Object")
3672                        }
3673                        (JavaType::Array(_), JavaType::Object(fn_))
3674                            if fn_.as_ref() == "java/lang/Object" =>
3675                        {
3676                            true
3677                        }
3678                        _ => false,
3679                    };
3680                    // Bare null against java/lang/Object itself: the
3681                    // descriptor method exists (dex resolved it), so
3682                    // `(Object) null` is always legal and excludes every
3683                    // more-specific sibling overload from applicability.
3684                    // Pool owners take the competes gate (emitted
3685                    // declarations are raw, so the cast cannot disturb
3686                    // inference). FRAMEWORK owners only via allowlist of
3687                    // the classic non-generic null-ambiguous JDK methods
3688                    // (StringBuilder.append ×66 lark): a blanket cast
3689                    // breaks GENERIC framework methods whose descriptor
3690                    // formal is an ERASED type variable —
3691                    // `ofFloat((Object) null, View.ALPHA, v)` killed the
3692                    // T=View inference bare null provided (weibo +4).
3693                    let null_obj =
3694                        matches!(a, Expr::Const(ConstVal::Null)) && matches!(f, JavaType::Object(_));
3695                    let framework_owner = pool.get(cls.as_ref()).is_none();
3696                    let fw_null_ambiguous = framework_owner
3697                        && matches!(
3698                            (cls.as_ref(), name.as_ref()),
3699                            ("java/lang/StringBuilder", "append")
3700                                | ("java/lang/StringBuffer", "append")
3701                                | ("java/io/PrintStream", "println")
3702                                | ("java/io/PrintStream", "print")
3703                                | ("java/io/PrintWriter", "println")
3704                                | ("java/io/PrintWriter", "print")
3705                                | ("java/lang/String", "valueOf")
3706                        );
3707                    // Framework owners with famously overload-heavy
3708                    // methods take the subtype pin too: the arg's
3709                    // rendered type implements SEVERAL formal types
3710                    // (weibo putExtra ×41: floatMsgData is both
3711                    // Parcelable and Serializable; schedule/submit:
3712                    // Runnable-and-Callable workers) and the competes
3713                    // gate cannot enumerate framework overloads.
3714                    // Formal == java/lang/Object stays excluded — that
3715                    // is an erased type variable and the cast would
3716                    // kill generic inference (ofFloat lesson).
3717                    let fw_subtype_pinnable = framework_owner
3718                        && subtype_arg
3719                        && !matches!(f, JavaType::Object(fn_) if fn_.as_ref() == "java/lang/Object")
3720                        && matches!(
3721                            (cls.as_ref(), name.as_ref()),
3722                            ("android/content/Intent", "putExtra")
3723                                | ("java/util/concurrent/ScheduledExecutorService", "schedule")
3724                                | ("java/util/concurrent/ExecutorService", "submit")
3725                                | ("java/util/concurrent/ExecutorService", "invokeAll")
3726                        );
3727                    if fw_subtype_pinnable
3728                        || (subtype_arg
3729                            && *competes.get_or_insert_with(|| {
3730                                overload_competes(cls, name, &desc.args)
3731                            }))
3732                        || (null_obj
3733                            && (fw_null_ambiguous
3734                                || *competes.get_or_insert_with(|| {
3735                                    overload_competes(cls, name, &desc.args)
3736                                })))
3737                    {
3738                        let old =
3739                            std::mem::replace(a, Expr::Const(ConstVal::Null));
3740                        *a = Expr::Cast {
3741                            ty: TypeRef::J(f.clone()),
3742                            e: Box::new(old),
3743                        };
3744                        continue;
3745                    }
3746                    if !is_bool(&val_ty(a, vt)) && !is_num(&val_ty(a, vt)) {
3747                        continue;
3748                    }
3749                    if !is_bool(f) && !is_num(f) {
3750                        continue;
3751                    }
3752                    if let Some(b) = bridge(&val_ty(a, vt), f, a) {
3753                        *a = b;
3754                    }
3755                }
3756            }
3757            Expr::New { cls, args, arg_tys, .. } => {
3758                let formals_owned = if arg_tys.len() == args.len() && !arg_tys.is_empty() {
3759                    Some(arg_tys.clone())
3760                } else {
3761                    ctor_formals(cls, args.len())
3762                };
3763                if let Some(formals) = formals_owned {
3764                    for (a, f) in args.iter_mut().zip(formals.iter()) {
3765                        if null_arg_cast(a, f, pool) {
3766                            continue;
3767                        }
3768                        if matches!(a, Expr::Cast { .. }) {
3769                            continue;
3770                        }
3771                        if !is_bool(f) && !is_num(f) {
3772                            continue;
3773                        }
3774                        if let Some(b) = bridge(&val_ty(a, vt), f, a) {
3775                            *a = b;
3776                        }
3777                    }
3778                }
3779            }
3780            _ => {}
3781        });
3782    });
3783}
3784
3785/// `b ^ 1` / `1 ^ b` with a BOOLEAN operand is the bytecode's boolean
3786/// negation (`!b`) — weibo kotlin-stdlib `return v2 ^ 1;` ("二元运算符
3787/// '^' 的操作数类型错误"), and `x ^ true` shapes. Java only types `^`
3788/// for same-category operands.
3789/// Rename locals whose name equals a ROOT PACKAGE first segment: a
3790/// package-qualified render (`v2.n.a` — the FQN fallback a twin-shadowed
3791/// same-package ref must take) binds its first identifier lexically, and
3792/// a local `v2` in scope captures it (weixin v2/i: 找不到符号 变量 n on a
3793/// legit `v2.n.a(this, 0)`). Method-scope renames carry no registry
3794/// surface — every use renders through this same vt — unlike class/field
3795/// renames, whose reference coverage blew up five times before.
3796pub fn deshadow_locals(vt: &mut VarTable, pool: &crate::DexPool, class_name: &str, body: &Stmt) {
3797    let segs = pool.root_pkg_segs();
3798    // Import-layer simples: the file renders `import p.h;` + bare `h.e`
3799    // for obscured refs — a local `h` captures that simple name (locals
3800    // beat imports in expression position; 无法取消引用int family).
3801    let obscured = crate::classdec::obscured_simples_snapshot();
3802    // Same-package static-ref simples: a static member ref renders its
3803    // class qualifier as the SIMPLE name (same package — no import/FQN),
3804    // and a local with that name captures it (`a2.changeQuickRedirect`
3805    // bound to a String local `a2` — "找不到符号 变量 changeQuickRedirect
3806    // 位置: 类型为String的变量 a2", weibo robust-field refs ×317). Only
3807    // STATIC refs qualify (an instance ref's qualifier IS the local).
3808    let self_pkg = class_name.rfind('/').map(|i| &class_name[..i]).unwrap_or("");
3809    let mut same_pkg_simples: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
3810    visit_all_exprs(body, &mut |x| {
3811        let cls = match x {
3812            Expr::Field { cls, is_static: true, .. } => Some(cls.as_ref()),
3813            Expr::Method { cls, is_static: true, .. } => Some(cls.as_ref()),
3814            _ => None,
3815        };
3816        if let Some(c) = cls {
3817            let pkg = c.rfind('/').map(|i| &c[..i]).unwrap_or("");
3818            if pkg == self_pkg {
3819                let simple = &c[c.rfind('/').map(|i| i + 1).unwrap_or(0)..];
3820                let head = simple.split('$').next().unwrap_or(simple);
3821                if !head.is_empty() {
3822                    same_pkg_simples.insert(head.to_string());
3823                }
3824            }
3825        }
3826    });
3827    let hit = |name: &str| {
3828        segs.contains(name) || obscured.contains(name) || same_pkg_simples.contains(name)
3829    };
3830    if (segs.is_empty() && obscured.is_empty() && same_pkg_simples.is_empty())
3831        || !vt.vars.iter().any(|v| hit(&v.name))
3832    {
3833        return;
3834    }
3835    let mut taken: jdc_core::FxHashSet<String> =
3836        vt.vars.iter().map(|v| v.name.clone()).collect();
3837    for v in vt.vars.iter_mut() {
3838        if !hit(&v.name) {
3839            continue;
3840        }
3841        let mut k = 0u32;
3842        let cand = loop {
3843            k += 1;
3844            let c = if k == 1 {
3845                format!("{}x", v.name)
3846            } else {
3847                format!("{}x{}", v.name, k)
3848            };
3849            if !taken.contains(&c) {
3850                break c;
3851            }
3852        };
3853        taken.insert(cand.clone());
3854        v.name = cand;
3855    }
3856}
3857
3858/// Receiver rescue: a Field/Method receiver whose final vt type is
3859/// PRIMITIVE can never hold the member (structurer tail-copy regions
3860/// can map the receiver register onto a boolean/int generation —
3861/// wcdb HandleOperation's `v10_g2.finalizeStatement()` where the
3862/// statement var is right there; weixin deref-int/bool families,
3863/// ~2.5k lines). The member descriptor's owner class is authoritative:
3864/// when EXACTLY ONE var in the method has that type (or a subtype),
3865/// swap the receiver to it. The pre-rescue output is a guaranteed
3866/// compile error AND runtime NPE, so the unambiguous swap cannot make
3867/// it worse; ambiguous sites stay untouched.
3868/// Anonymous/inner-class outer reads: the dex reads `this$0` off the
3869/// instance register, which copy materialization can route through a
3870/// local (`v41 = this; ... v41.this$0` — weibo HorseRaceDetector$1,
3871/// 2,326-line 找不到符号 变量 this$0 family). When the field's
3872/// declaring class IS the class being decompiled, the only instance in
3873/// scope is `this` — strip the owner so it renders the bare declared
3874/// field.
3875/// Fold single-assigned `this`-copy locals in ctors back to `this`.
3876/// R8 materializes the receiver with `move-object/from16 v1, p0` and
3877/// runs every iput through the copy; the copy renders as a local and a
3878/// final-field store through it is "无法为 final 变量 g 分配值" (Java
3879/// allows blank-final writes only via `this`/simple name — hf/n2 ×29,
3880/// weixin final-var residual). Transitively: `b = a; c = b` chains
3881/// collapse. Single-assignment gate: a var ever written from another
3882/// value is not an alias.
3883pub fn fix_ctor_this_aliases(body: &mut Stmt, vt: &VarTable) {
3884    let Some(this_var) = vt
3885        .vars
3886        .iter()
3887        .find(|v| v.is_param && v.name == "this")
3888        .map(|v| v.id)
3889    else {
3890        return;
3891    };
3892    let mut analysis = VarAnalysis {
3893        assigns: Vec::new(),
3894        reads: Vec::new(),
3895        values: Vec::new(),
3896    };
3897    analyze_vars(body, &mut analysis);
3898    let n = vt.vars.len().max(analysis.assigns.len());
3899    let mut alias = vec![false; n];
3900    for _ in 0..4 {
3901        let mut changed = false;
3902        for v in 0..n as u32 {
3903            if alias[v as usize] {
3904                continue;
3905            }
3906            if analysis.assigns.get(v as usize).copied().unwrap_or(0) != 1 {
3907                continue;
3908            }
3909            let Some(Expr::Local { var: src, .. }) = analysis.values.get(v as usize).and_then(|o| o.as_ref()) else {
3910                continue;
3911            };
3912            if *src == this_var
3913                || (*src < n as u32 && alias[*src as usize])
3914            {
3915                alias[v as usize] = true;
3916                changed = true;
3917            }
3918        }
3919        if !changed {
3920            break;
3921        }
3922    }
3923    if !alias.iter().any(|&b| b) {
3924        return;
3925    }
3926    walk_stmt_exprs(body, &mut |e| {
3927        deep_rewrite_reads(e, &mut |x| {
3928            if let Expr::Local { var, .. } = x {
3929                if (*var as usize) < n && alias[*var as usize] {
3930                    *x = Expr::This;
3931                }
3932            }
3933        });
3934    });
3935}
3936
3937pub fn fix_this0_owners(body: &mut Stmt, class_name: &str) {
3938    walk_stmt_exprs(body, &mut |e| {
3939        deep_rewrite(e, &mut |x| {
3940            if let Expr::Field { owner: Some(_), cls, name, .. } = x {
3941                if name.as_ref() == "this$0" && cls.as_ref() == class_name {
3942                    if let Expr::Field { owner, .. } = x {
3943                        *owner = None;
3944                    }
3945                }
3946            }
3947        });
3948    });
3949}
3950
3951pub fn rescue_primitive_receivers(body: &mut Stmt, vt: &VarTable, pool: &crate::DexPool) {
3952    fn prim(t: &JavaType) -> bool {
3953        match t {
3954            JavaType::Int
3955            | JavaType::Long
3956            | JavaType::Short
3957            | JavaType::Byte
3958            | JavaType::Char
3959            | JavaType::Float
3960            | JavaType::Double
3961            | JavaType::Boolean => true,
3962            // Boxed JDK finals carry no app members either (`bool19.d`
3963            // where bool19 came out java/lang/Boolean — the coroutine
3964            // Result-register conflation).
3965            JavaType::Object(n) => matches!(
3966                n.as_ref(),
3967                "java/lang/Boolean"
3968                    | "java/lang/String"
3969                    | "java/lang/Integer"
3970                    | "java/lang/Long"
3971                    | "java/lang/Short"
3972                    | "java/lang/Byte"
3973                    | "java/lang/Character"
3974                    | "java/lang/Float"
3975                    | "java/lang/Double"
3976            ),
3977            _ => false,
3978        }
3979    }
3980    walk_stmt_exprs(body, &mut |e| {
3981        deep_rewrite(e, &mut |x| {
3982            // Phase 1 (read-only): primitive-typed Local receiver?
3983            let recv_var = {
3984                let o: &Expr = match &*x {
3985                    Expr::Method { owner: Some(o), .. } => o,
3986                    Expr::Field { owner: Some(o), is_static: false, .. } => o,
3987                    _ => return,
3988                };
3989                let Expr::Local { var, ty } = o else {
3990                    return;
3991                };
3992                let vt_ty = if (*var as usize) < vt.vars.len() {
3993                    vt.var(*var).ty.erased()
3994                } else {
3995                    ty.erased()
3996                };
3997                if !prim(&vt_ty) {
3998                    return;
3999                }
4000                *var
4001            };
4002            let cls: std::sync::Arc<str> = match &*x {
4003                Expr::Method { cls, .. } | Expr::Field { cls, .. } => cls.clone(),
4004                _ => return,
4005            };
4006            // Phase 2: vars of the owner type (exact or subtype).
4007            let mut cands: Vec<u32> = Vec::new();
4008            for v in &vt.vars {
4009                if let JavaType::Object(n) = v.ty.erased() {
4010                    if n.as_ref() == cls.as_ref()
4011                        || pool.is_subtype(n.as_ref(), cls.as_ref())
4012                    {
4013                        cands.push(v.id);
4014                    }
4015                }
4016            }
4017            let id = if cands.len() == 1 {
4018                cands[0]
4019            } else if cands.len() > 1 {
4020                // Multiple owner-typed vars: prefer the same-SLOT one
4021                // (register lineage), latest generation wins — the
4022                // receiver position is scope-insensitive (any method-
4023                // scope var renders, the decl hoist covers it).
4024                let slot = if (recv_var as usize) < vt.vars.len() {
4025                    Some(vt.var(recv_var).slot)
4026                } else {
4027                    None
4028                };
4029                let mut hits: Vec<u32> = match slot {
4030                    Some(sl) => cands
4031                        .iter()
4032                        .copied()
4033                        .filter(|&c| {
4034                            c != recv_var
4035                                && (c as usize) < vt.vars.len()
4036                                && vt.var(c).slot == sl
4037                        })
4038                        .collect(),
4039                    None => Vec::new(),
4040                };
4041                if hits.is_empty() {
4042                    // No same-slot twin: fall back to the latest
4043                    // owner-typed generation (max id).
4044                    hits = cands
4045                        .iter()
4046                        .copied()
4047                        .filter(|&c| c != recv_var)
4048                        .collect();
4049                }
4050                hits.sort_unstable();
4051                hits.dedup();
4052                if hits.is_empty() {
4053                    return;
4054                }
4055                *hits.last().unwrap()
4056            } else {
4057                // Lineage disambiguation by SLOT: register reuse makes
4058                // the Boolean/primitive twin and the object version of
4059                // one dex register share VarInfo.slot (ry0/h2's
4060                // `bool19.d` — the v1-typed twin rides the same
4061                // register). Multiple slot matches stay untouched
4062                // (deterministic conservatism).
4063                if cands.is_empty() {
4064                    return;
4065                }
4066                let slot = if (recv_var as usize) < vt.vars.len() {
4067                    vt.var(recv_var).slot
4068                } else {
4069                    return;
4070                };
4071                let mut hits: Vec<u32> = cands
4072                    .into_iter()
4073                    .filter(|&c| {
4074                        c != recv_var
4075                            && (c as usize) < vt.vars.len()
4076                            && vt.var(c).slot == slot
4077                    })
4078                    .collect();
4079                hits.sort_unstable();
4080                hits.dedup();
4081                if hits.len() != 1 {
4082                    return;
4083                }
4084                hits[0]
4085            };
4086            // Phase 3: swap.
4087            let o: &mut Box<Expr> = match x {
4088                Expr::Method { owner: Some(o), .. } => o,
4089                Expr::Field { owner: Some(o), is_static: false, .. } => o,
4090                _ => return,
4091            };
4092            if let Expr::Local { var, ty } = &mut **o {
4093                if *var == recv_var {
4094                    *var = id;
4095                    *ty = vt.vars[id as usize].ty.clone();
4096                }
4097            }
4098        });
4099    });
4100
4101}
4102
4103/// Scope-safe arg/return swaps for mistyped generations — runs AFTER
4104/// ensure_declared hoists every declaration to the method top, so the
4105/// top_ids visibility precondition holds by construction (the r134
4106/// falsification: swapping in a block-scoped var broke lexically).
4107/// Same-slot + type-match + uniqueness gates as the receiver rescue.
4108pub fn rescue_arg_return_swaps(
4109    body: &mut Stmt,
4110    vt: &VarTable,
4111    pool: &crate::DexPool,
4112    ret_ty: &JavaType,
4113) {
4114    // SCOPE-SAFE arg/return swap: a primitive-typed actual against a
4115    // specific-reference formal (or a reference-returning `return`) is
4116    // a mistyped generation — the r134 falsification showed the swap
4117    // target MUST be lexically visible at the use site, so candidates
4118    // are restricted to params + TOP-LEVEL declared locals (a hoisted
4119    // decl is visible to javac name resolution everywhere in the
4120    // method) AND same-slot (same dex register lineage) AND unique.
4121    let mut top_ids: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
4122    for v in &vt.vars {
4123        if v.is_param {
4124            top_ids.insert(v.id);
4125        }
4126    }
4127    if let Stmt::Block(stmts) = body {
4128        for st in stmts.iter() {
4129            if let Stmt::LocalDef { var, .. } = st {
4130                top_ids.insert(*var);
4131            }
4132        }
4133    }
4134    let ret_wants_ref = match ret_ty {
4135        JavaType::Object(n) => n.as_ref() != "java/lang/Object",
4136        JavaType::Array(_) => true,
4137        _ => false,
4138    };
4139    let try_swap = |a: &mut Expr, want: &JavaType| {
4140        let Expr::Local { var, ty } = &mut *a else {
4141            return;
4142        };
4143        if (*var as usize) >= vt.vars.len() {
4144            return;
4145        }
4146        let avt = vt.var(*var).ty.erased();
4147        let avt_ref = matches!(avt, JavaType::Object(_) | JavaType::Array(_));
4148        let want_arr = matches!(want, JavaType::Array(_));
4149        // Only swap a primitive-typed actual (an Object actual gets the
4150        // descriptor-exact cast instead — never both).
4151        if avt_ref && !(avt == JavaType::Object("java/lang/Object".into())) {
4152            return;
4153        }
4154        if avt_ref && !matches!(want, JavaType::Object(_)) {
4155            return;
4156        }
4157        let slot = vt.var(*var).slot;
4158        let mut hits: Vec<u32> = Vec::new();
4159        for v in &vt.vars {
4160            if v.slot != slot || !top_ids.contains(&v.id) || v.id == *var {
4161                continue;
4162            }
4163            let t = v.ty.erased();
4164            let ok = match (&t, want) {
4165                (JavaType::Object(n), JavaType::Object(w)) => {
4166                    n.as_ref() == w.as_ref()
4167                        || pool.is_subtype(n.as_ref(), w.as_ref())
4168                }
4169                (JavaType::Array(_), JavaType::Array(_)) => want_arr,
4170                _ => false,
4171            };
4172            if ok {
4173                hits.push(v.id);
4174            }
4175        }
4176        hits.sort_unstable();
4177        hits.dedup();
4178        if hits.is_empty() {
4179            return;
4180        }
4181        // Multiple same-slot candidates: the LATEST generation (max id —
4182        // split mints in walk order ≈ program order) is the closest
4183        // approximation of the register's value at the use site. A
4184        // wrong pick still compiles (type-matched by construction);
4185        // leaving the primitive actual never does.
4186        let id = *hits.last().unwrap();
4187        *var = id;
4188        *ty = vt.vars[id as usize].ty.clone();
4189    };
4190    walk_stmt_exprs(body, &mut |e| {
4191        deep_rewrite(e, &mut |x| {
4192            match x {
4193                Expr::Method { desc, args, is_dynamic: false, .. } => {
4194                    for (i, a) in args.iter_mut().enumerate() {
4195                        if let Some(f) = desc.args.get(i) {
4196                            if matches!(f, JavaType::Object(n) if n.as_ref() != "java/lang/Object")
4197                                || matches!(f, JavaType::Array(_))
4198                            {
4199                                try_swap(a, f);
4200                            }
4201                        }
4202                    }
4203                }
4204                Expr::New { arg_tys, args, .. } => {
4205                    for (i, a) in args.iter_mut().enumerate() {
4206                        if let Some(f) = arg_tys.get(i) {
4207                            if matches!(f, JavaType::Object(n) if n.as_ref() != "java/lang/Object")
4208                                || matches!(f, JavaType::Array(_))
4209                            {
4210                                try_swap(a, f);
4211                            }
4212                        }
4213                    }
4214                }
4215                _ => {}
4216            }
4217        });
4218    });
4219    if ret_wants_ref {
4220        walk_mut_deep(body, &mut |st| {
4221            if let Stmt::Return(Some(e)) = st {
4222                try_swap(e, ret_ty);
4223            }
4224        });
4225    }
4226}
4227
4228/// Reference-array initializers take null, not 0: dex fill-array-data
4229/// over a String[]/Object[] slot carries the zero word, and the lifted
4230/// `new String[] {0}` is int无法转换为String (qs0/b family). A Const
4231/// Int(0) element of a reference-typed array init is the null ref.
4232/// `refVar = const 0` (dex `const/4 vN, 0` into a reference register) is
4233/// the NULL sentinel, not an int. By the time this runs infer_types has
4234/// typed the var a reference (its `.close()`/`.length`/method reads
4235/// proved it), so retype the const to Null BEFORE split_generations —
4236/// otherwise the ref/primitive mismatch splits off an int generation and
4237/// orphans the reference reads (u7.l try-with-resources finally
4238/// `v16_g3 = 0; if (v16_g3 != 0) v16_g3.close();` → "无法取消引用int").
4239/// `fix_incomparable_equality` already retypes the `refVar != 0`
4240/// COMPARISON side (but runs after the split); this covers the ASSIGN
4241/// side, which is what triggers the bad split.
4242pub fn fix_ref_null_assigns(vt: &VarTable, body: &mut Stmt) {
4243    let is_ref_var = |var: u32| -> bool {
4244        (var as usize) < vt.vars.len()
4245            && matches!(
4246                vt.vars[var as usize].ty.erased(),
4247                JavaType::Object(_) | JavaType::Array(_)
4248            )
4249    };
4250    walk_mut_deep(body, &mut |st| match st {
4251        Stmt::ExprStmt(Expr::Assign {
4252            target,
4253            value,
4254            op: AssignOp::Plain,
4255            ..
4256        }) => {
4257            if let Expr::Local { var, .. } = &**target {
4258                if is_ref_var(*var) && matches!(&**value, Expr::Const(ConstVal::Int(0))) {
4259                    **value = Expr::Const(ConstVal::Null);
4260                }
4261            }
4262        }
4263        Stmt::LocalDef {
4264            var,
4265            init: Some(e),
4266            ..
4267        } => {
4268            if is_ref_var(*var) && matches!(e, Expr::Const(ConstVal::Int(0))) {
4269                *e = Expr::Const(ConstVal::Null);
4270            }
4271        }
4272        _ => {}
4273    });
4274}
4275
4276pub fn fix_ref_array_null_consts(body: &mut Stmt) {
4277    walk_stmt_exprs(body, &mut |e| {
4278        deep_rewrite(e, &mut |x| {
4279            if let Expr::NewArray { elem, init: Some(list), .. } = x {
4280                if matches!(elem.erased(), JavaType::Object(_) | JavaType::Array(_)) {
4281                    for slot in list.iter_mut() {
4282                        if matches!(&*slot, Expr::Const(ConstVal::Int(0))) {
4283                            *slot = Expr::Const(ConstVal::Null);
4284                        }
4285                    }
4286                }
4287            }
4288        });
4289    });
4290}
4291
4292// ---------------------------------------------------------------------------
4293// Null-sentinel repair: const-0 registers in reference-typed slots
4294// ---------------------------------------------------------------------------
4295
4296/// A dex `const/4 vN, 0` register is ambiguous between int 0 and the
4297/// null reference; the value view types the bare constant as Int.
4298/// Kotlin coroutine state machines lean on this heavily — captured
4299/// object slots are zero-initialized ahead of the label switch, then
4300/// stored into `Object` fields and passed as `Continuation` ctor args
4301/// (weixin y01/s9 q8: `new e6(s94x, v287)` "int无法转换为Continuation";
4302/// uf5/l0 `throw 0;`; wcdb RepairKit `setOnCancelListener(0)`). The
4303/// dex verifier guarantees a const-0 register reaching a reference-
4304/// typed slot holds null, so in those slots the rendering must be the
4305/// null literal.
4306///
4307/// Two rewrites, both gated on the slot's expected type being a
4308/// reference (descriptor formal types, field type, array component,
4309/// cast target, throw, method return):
4310///   1. a literal `0` in a ref slot → `null`. SKIPPED for call-arg
4311///      slots whose formal is exactly `java/lang/Object`: `f(0)` there
4312///      compiles TODAY (boxing, or an int-overload pick like
4313///      `String.valueOf(0)`), and `f(null)` could newly fail on
4314///      overload ambiguity (`valueOf(Object)` vs `valueOf(char[])`) —
4315///      a guaranteed-error slot becomes a compile, a compiling slot
4316///      must not become an error.
4317///   2. a proven null-sentinel LOCAL: an Int-typed var whose every
4318///      write is `= 0` or `= <another sentinel>` and whose every read
4319///      sits in a ref slot (or feeds another sentinel's write — a dead
4320///      chain) → `null` at each read. The same Object-formal skip
4321///      applies at arg slots. A read in ANY int/unknown context
4322///      (binop, cond test, receiver deref, selector, anon-ctor arg,
4323///      lambda capture…) disqualifies the var — register reuse across
4324///      generations keeps its int rendering.
4325///
4326/// Runs after fix_ref_null_assigns (which converts writes of REF-typed
4327/// vars) and before split_generations (sentinel writes must not mint
4328/// int generations). Writes of confirmed sentinels stay as harmless
4329/// dead int stores for drop_dead_locals.
4330pub fn fix_null_sentinels(body: &mut Stmt, vt: &VarTable, ret: &JavaType) {
4331    #[derive(Clone, Copy, PartialEq)]
4332    enum WK {
4333        Zero,
4334        Copy(u32),
4335        Other,
4336    }
4337    fn is_ref_jt(t: &JavaType) -> bool {
4338        matches!(t, JavaType::Object(_) | JavaType::Array(_))
4339    }
4340    fn cls_write(e: &Expr) -> WK {
4341        match e {
4342            Expr::Const(ConstVal::Int(0)) => WK::Zero,
4343            Expr::Local { var, .. } => WK::Copy(*var),
4344            _ => WK::Other,
4345        }
4346    }
4347    fn expr_writes(e: &Expr, writes: &mut std::collections::HashMap<u32, Vec<WK>>) {
4348        visit_exprs(e, &mut |x| match x {
4349            Expr::Assign { target, op, value } => {
4350                if let Expr::Local { var, .. } = &**target {
4351                    let k = if matches!(op, AssignOp::Plain) {
4352                        cls_write(value)
4353                    } else {
4354                        WK::Other
4355                    };
4356                    writes.entry(*var).or_default().push(k);
4357                }
4358            }
4359            Expr::PreIncDec { e: inner, .. } | Expr::PostIncDec { e: inner, .. } => {
4360                if let Expr::Local { var, .. } = &**inner {
4361                    writes.entry(*var).or_default().push(WK::Other);
4362                }
4363            }
4364            _ => {}
4365        });
4366    }
4367    fn collect_writes(
4368        s: &Stmt,
4369        writes: &mut std::collections::HashMap<u32, Vec<WK>>,
4370        has_zero: &mut bool,
4371    ) {
4372        fn note(e: &Expr, has_zero: &mut bool, writes: &mut std::collections::HashMap<u32, Vec<WK>>) {
4373            visit_exprs(e, &mut |x| {
4374                if let Expr::Const(ConstVal::Int(0)) = x {
4375                    *has_zero = true;
4376                }
4377            });
4378            expr_writes(e, writes);
4379        }
4380        match s {
4381            Stmt::Block(v) => {
4382                for x in v {
4383                    collect_writes(x, writes, has_zero);
4384                }
4385            }
4386            Stmt::LocalDef { var, init, .. } => {
4387                if let Some(e) = init {
4388                    if matches!(e, Expr::Const(ConstVal::Int(0))) {
4389                        *has_zero = true;
4390                    }
4391                    writes.entry(*var).or_default().push(cls_write(e));
4392                }
4393            }
4394            Stmt::ExprStmt(e) => note(e, has_zero, writes),
4395            Stmt::Return(e) => {
4396                if let Some(x) = e {
4397                    note(x, has_zero, writes);
4398                }
4399            }
4400            Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
4401                note(e, has_zero, writes);
4402            }
4403            Stmt::TernaryValue { e } => note(e, has_zero, writes),
4404            Stmt::If { cond, then_stmt, else_stmt } => {
4405                note(cond, has_zero, writes);
4406                collect_writes(then_stmt, writes, has_zero);
4407                if let Some(b) = else_stmt {
4408                    collect_writes(b, writes, has_zero);
4409                }
4410            }
4411            Stmt::While { cond, body } | Stmt::DoWhile { body, cond } => {
4412                note(cond, has_zero, writes);
4413                collect_writes(body, writes, has_zero);
4414            }
4415            Stmt::For { init, cond, update, body } => {
4416                for x in init {
4417                    collect_writes(x, writes, has_zero);
4418                }
4419                if let Some(c) = cond {
4420                    note(c, has_zero, writes);
4421                }
4422                for u in update {
4423                    note(u, has_zero, writes);
4424                }
4425                collect_writes(body, writes, has_zero);
4426            }
4427            Stmt::ForEach { var, iterable, body, .. } => {
4428                writes.entry(*var).or_default().push(WK::Other);
4429                note(iterable, has_zero, writes);
4430                collect_writes(body, writes, has_zero);
4431            }
4432            Stmt::Switch { selector, cases, default, .. } => {
4433                note(selector, has_zero, writes);
4434                for c in cases {
4435                    if let Some(g) = &c.guard {
4436                        note(g, has_zero, writes);
4437                    }
4438                    for x in &c.body {
4439                        collect_writes(x, writes, has_zero);
4440                    }
4441                }
4442                if let Some(d) = default {
4443                    collect_writes(d, writes, has_zero);
4444                }
4445            }
4446            Stmt::Try { body, catches, finally } => {
4447                collect_writes(body, writes, has_zero);
4448                for c in catches {
4449                    writes.entry(c.var).or_default().push(WK::Other);
4450                    collect_writes(&c.body, writes, has_zero);
4451                }
4452                if let Some(f) = finally {
4453                    collect_writes(f, writes, has_zero);
4454                }
4455            }
4456            Stmt::TryWithResources { resources, body, catches, finally, .. } => {
4457                for r in resources {
4458                    collect_writes(r, writes, has_zero);
4459                }
4460                collect_writes(body, writes, has_zero);
4461                for c in catches {
4462                    writes.entry(c.var).or_default().push(WK::Other);
4463                    collect_writes(&c.body, writes, has_zero);
4464                }
4465                if let Some(f) = finally {
4466                    collect_writes(f, writes, has_zero);
4467                }
4468            }
4469            Stmt::Assert { cond, msg } => {
4470                note(cond, has_zero, writes);
4471                if let Some(m) = msg {
4472                    note(m, has_zero, writes);
4473                }
4474            }
4475            Stmt::Synchronized { lock, body } => {
4476                note(lock, has_zero, writes);
4477                collect_writes(body, writes, has_zero);
4478            }
4479            Stmt::Labeled { body, .. } => collect_writes(body, writes, has_zero),
4480            _ => {}
4481        }
4482    }
4483
4484    let mut writes: std::collections::HashMap<u32, Vec<WK>> =
4485        std::collections::HashMap::new();
4486    let mut has_zero = false;
4487    collect_writes(body, &mut writes, &mut has_zero);
4488
4489    // Write-proven eligibility fixpoint (copy chains: `v = u`).
4490    let mut eligible: HashSet<u32> = HashSet::default();
4491    let int_typed = |v: u32| {
4492        (v as usize) < vt.vars.len() && vt.vars[v as usize].ty.erased() == JavaType::Int
4493    };
4494    loop {
4495        let mut changed = false;
4496        for (&v, ws) in writes.iter() {
4497            if eligible.contains(&v) || ws.is_empty() || !int_typed(v) {
4498                continue;
4499            }
4500            if ws.iter().all(|k| match k {
4501                WK::Zero => true,
4502                WK::Copy(u) => *u != v && eligible.contains(u),
4503                WK::Other => false,
4504            }) {
4505                eligible.insert(v);
4506                changed = true;
4507            }
4508        }
4509        if !changed {
4510            break;
4511        }
4512    }
4513    if eligible.is_empty() && !has_zero {
4514        return; // nothing this pass can do
4515    }
4516
4517    #[derive(Clone, Copy, PartialEq)]
4518    enum Mode {
4519        Scan,
4520        Rewrite,
4521    }
4522    struct St<'a> {
4523        mode: Mode,
4524        vt: &'a VarTable,
4525        eligible: &'a HashSet<u32>,
4526        ref_read: HashSet<u32>,
4527        dead: HashSet<u32>,
4528        sentinels: HashSet<u32>,
4529        n_lit: usize,
4530        n_var: usize,
4531    }
4532    // A call-arg slot whose formal is exactly java/lang/Object keeps
4533    // its current rendering (see the fn doc — boxing compiles today,
4534    // null could newly ambiguize).
4535    fn arg_ctx(t: &JavaType) -> Option<JavaType> {
4536        match t {
4537            JavaType::Object(n) if n.as_ref() == "java/lang/Object" => None,
4538            t if is_ref_jt(t) => Some(t.clone()),
4539            _ => None,
4540        }
4541    }
4542    fn walk_expr(e: &mut Expr, ctx: Option<JavaType>, st: &mut St) {
4543        walk_expr_c(e, ctx, false, st);
4544    }
4545    /// `cast_arg`: the slot is a call/ctor ARGUMENT — a rewritten null
4546    /// gets an explicit formal-type cast. Bare `f(null)` re-triggers
4547    /// overload ambiguity where `f(0)` used to pick an int overload
4548    /// (`in(null)` "引用不明确", `ContentValues.put(String,null)` vs its
4549    /// 9 overloads); `(String) null` is unambiguous and jadx-shaped.
4550    fn walk_expr_c(e: &mut Expr, ctx: Option<JavaType>, cast_arg: bool, st: &mut St) {
4551        let slot_ref = ctx.as_ref().is_some_and(is_ref_jt);
4552        fn mk_null(ctx: &Option<JavaType>, cast_arg: bool) -> Expr {
4553            match (cast_arg, ctx) {
4554                (true, Some(t)) => Expr::Cast {
4555                    ty: TypeRef::J(t.clone()),
4556                    e: Box::new(Expr::Const(ConstVal::Null)),
4557                },
4558                _ => Expr::Const(ConstVal::Null),
4559            }
4560        }
4561        match e {
4562            Expr::Const(ConstVal::Int(0)) if slot_ref => {
4563                if st.mode == Mode::Rewrite {
4564                    *e = mk_null(&ctx, cast_arg);
4565                    st.n_lit += 1;
4566                }
4567                return;
4568            }
4569            Expr::Local { var, .. } => {
4570                if st.eligible.contains(var) {
4571                    if slot_ref {
4572                        st.ref_read.insert(*var);
4573                    } else {
4574                        st.dead.insert(*var);
4575                    }
4576                }
4577                if st.mode == Mode::Rewrite && slot_ref && st.sentinels.contains(var) {
4578                    *e = mk_null(&ctx, cast_arg);
4579                    st.n_var += 1;
4580                }
4581                return;
4582            }
4583            _ => {}
4584        }
4585        match e {
4586            Expr::Method { owner, desc, args, .. } => {
4587                if let Some(o) = owner {
4588                    walk_expr(o, None, st);
4589                }
4590                for (i, a) in args.iter_mut().enumerate() {
4591                    let c = desc.args.get(i).and_then(arg_ctx);
4592                    walk_expr_c(a, c, true, st);
4593                }
4594            }
4595            Expr::Invokedynamic { desc, args, .. } => {
4596                for (i, a) in args.iter_mut().enumerate() {
4597                    let c = desc.args.get(i).and_then(arg_ctx);
4598                    walk_expr_c(a, c, true, st);
4599                }
4600            }
4601            Expr::New { args, arg_tys, .. } => {
4602                for (i, a) in args.iter_mut().enumerate() {
4603                    let c = arg_tys.get(i).and_then(arg_ctx);
4604                    walk_expr_c(a, c, true, st);
4605                }
4606            }
4607            Expr::AnonNew { args, .. } => {
4608                for a in args.iter_mut() {
4609                    walk_expr(a, None, st);
4610                }
4611            }
4612            Expr::Assign { target, value, .. } => {
4613                // Sentinel-to-sentinel copy: the whole store is dead;
4614                // its RHS is by the write gate a const-0 or a bare
4615                // local, so no nested reads can hide in it.
4616                if let Expr::Local { var: u, .. } = &**target {
4617                    if st.eligible.contains(u) {
4618                        return;
4619                    }
4620                }
4621                let vctx = match &**target {
4622                    Expr::Field { ty, .. } => {
4623                        let t = ty.erased();
4624                        is_ref_jt(&t).then_some(t)
4625                    }
4626                    Expr::ArrayIndex { array, .. } => match array.type_ref().erased() {
4627                        JavaType::Array(inner) if is_ref_jt(&inner) => Some(*inner),
4628                        _ => None,
4629                    },
4630                    _ => None,
4631                };
4632                walk_expr(target, None, st);
4633                walk_expr(value, vctx, st);
4634            }
4635            Expr::Cast { ty, e: inner } => {
4636                let t = ty.erased();
4637                let c = is_ref_jt(&t).then_some(t);
4638                walk_expr(inner, c, st);
4639            }
4640            Expr::InstanceOf { e: inner, .. } => {
4641                walk_expr(inner, Some(JavaType::Object("java/lang/Object".into())), st);
4642            }
4643            Expr::Cond { c, t, f } => {
4644                walk_expr(c, None, st);
4645                let tc = ctx.clone();
4646                walk_expr(t, tc, st);
4647                walk_expr(f, ctx, st);
4648            }
4649            Expr::NewArray { elem, dims, init, .. } => {
4650                for d in dims.iter_mut() {
4651                    walk_expr(d, None, st);
4652                }
4653                if let Some(list) = init {
4654                    let et = elem.erased();
4655                    let c = is_ref_jt(&et).then_some(et);
4656                    for slot in list.iter_mut() {
4657                        walk_expr(slot, c.clone(), st);
4658                    }
4659                }
4660            }
4661            Expr::NewMultiArray { dims, .. } => {
4662                for d in dims.iter_mut() {
4663                    walk_expr(d, None, st);
4664                }
4665            }
4666            Expr::ArrayIndex { array, index } => {
4667                walk_expr(array, None, st);
4668                walk_expr(index, None, st);
4669            }
4670            Expr::Field { owner, .. } => {
4671                if let Some(o) = owner {
4672                    walk_expr(o, None, st);
4673                }
4674            }
4675            Expr::Bin { l, r, .. } => {
4676                walk_expr(l, None, st);
4677                walk_expr(r, None, st);
4678            }
4679            Expr::Un { e: inner, .. } => walk_expr(inner, None, st),
4680            Expr::PreIncDec { e: inner, .. } | Expr::PostIncDec { e: inner, .. } => {
4681                walk_expr(inner, None, st);
4682            }
4683            Expr::StringConcat(parts) => {
4684                for p in parts.iter_mut() {
4685                    if let ConcatPart::Str(x) = p {
4686                        walk_expr(x, None, st);
4687                    }
4688                }
4689            }
4690            Expr::Lambda(l) => {
4691                for c in l.captures.iter_mut() {
4692                    walk_expr(c, None, st);
4693                }
4694            }
4695            _ => {}
4696        }
4697    }
4698    fn walk_stmt(s: &mut Stmt, st: &mut St, ret: &JavaType) {
4699        match s {
4700            Stmt::Block(v) => {
4701                for x in v.iter_mut() {
4702                    walk_stmt(x, st, ret);
4703                }
4704            }
4705            Stmt::ExprStmt(e) => walk_expr(e, None, st),
4706            Stmt::LocalDef { var, init, .. } => {
4707                if let Some(e) = init {
4708                    let ctx = if (*var as usize) < st.vt.vars.len() {
4709                        let t = st.vt.vars[*var as usize].ty.erased();
4710                        is_ref_jt(&t).then_some(t)
4711                    } else {
4712                        None
4713                    };
4714                    walk_expr(e, ctx, st);
4715                }
4716            }
4717            Stmt::Return(e) => {
4718                if let Some(x) = e {
4719                    let c = is_ref_jt(ret).then(|| ret.clone());
4720                    walk_expr(x, c, st);
4721                }
4722            }
4723            Stmt::Throw(e) => {
4724                walk_expr(e, Some(JavaType::Object("java/lang/Throwable".into())), st);
4725            }
4726            Stmt::If { cond, then_stmt, else_stmt } => {
4727                walk_expr(cond, None, st);
4728                walk_stmt(then_stmt, st, ret);
4729                if let Some(b) = else_stmt {
4730                    walk_stmt(b, st, ret);
4731                }
4732            }
4733            Stmt::While { cond, body } => {
4734                walk_expr(cond, None, st);
4735                walk_stmt(body, st, ret);
4736            }
4737            Stmt::DoWhile { body, cond } => {
4738                walk_stmt(body, st, ret);
4739                walk_expr(cond, None, st);
4740            }
4741            Stmt::For { init, cond, update, body } => {
4742                for x in init.iter_mut() {
4743                    walk_stmt(x, st, ret);
4744                }
4745                if let Some(c) = cond {
4746                    walk_expr(c, None, st);
4747                }
4748                for u in update.iter_mut() {
4749                    walk_expr(u, None, st);
4750                }
4751                walk_stmt(body, st, ret);
4752            }
4753            Stmt::ForEach { iterable, body, .. } => {
4754                walk_expr(iterable, None, st);
4755                walk_stmt(body, st, ret);
4756            }
4757            Stmt::Switch { selector, cases, default, .. } => {
4758                walk_expr(selector, None, st);
4759                for c in cases.iter_mut() {
4760                    if let Some(g) = &mut c.guard {
4761                        walk_expr(g, None, st);
4762                    }
4763                    for x in c.body.iter_mut() {
4764                        walk_stmt(x, st, ret);
4765                    }
4766                }
4767                if let Some(d) = default {
4768                    walk_stmt(d, st, ret);
4769                }
4770            }
4771            Stmt::Try { body, catches, finally } => {
4772                walk_stmt(body, st, ret);
4773                for c in catches.iter_mut() {
4774                    walk_stmt(&mut c.body, st, ret);
4775                }
4776                if let Some(f) = finally {
4777                    walk_stmt(f, st, ret);
4778                }
4779            }
4780            Stmt::TryWithResources { resources, body, catches, finally, .. } => {
4781                for r in resources.iter_mut() {
4782                    walk_stmt(r, st, ret);
4783                }
4784                walk_stmt(body, st, ret);
4785                for c in catches.iter_mut() {
4786                    walk_stmt(&mut c.body, st, ret);
4787                }
4788                if let Some(f) = finally {
4789                    walk_stmt(f, st, ret);
4790                }
4791            }
4792            Stmt::Assert { cond, msg } => {
4793                walk_expr(cond, None, st);
4794                if let Some(m) = msg {
4795                    walk_expr(m, None, st);
4796                }
4797            }
4798            Stmt::Synchronized { lock, body } => {
4799                walk_expr(lock, Some(JavaType::Object("java/lang/Object".into())), st);
4800                walk_stmt(body, st, ret);
4801            }
4802            Stmt::TernaryValue { e } => walk_expr(e, None, st),
4803            Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
4804                walk_expr(e, Some(JavaType::Object("java/lang/Object".into())), st);
4805            }
4806            Stmt::Labeled { body, .. } => walk_stmt(body, st, ret),
4807            _ => {}
4808        }
4809    }
4810
4811    let mut st = St {
4812        mode: Mode::Scan,
4813        vt,
4814        eligible: &eligible,
4815        ref_read: HashSet::default(),
4816        dead: HashSet::default(),
4817        sentinels: HashSet::default(),
4818        n_lit: 0,
4819        n_var: 0,
4820    };
4821    if !eligible.is_empty() {
4822        walk_stmt(body, &mut st, ret);
4823        st.sentinels = eligible
4824            .iter()
4825            .copied()
4826            .filter(|v| st.ref_read.contains(v) && !st.dead.contains(v))
4827            .collect();
4828    }
4829    if st.sentinels.is_empty() && !has_zero {
4830        return;
4831    }
4832    st.mode = Mode::Rewrite;
4833    walk_stmt(body, &mut st, ret);
4834    if std::env::var_os("DDC_SENT").is_some() {
4835        eprintln!(
4836            "[sent] elig={} confirmed={} lit={} var={}",
4837            eligible.len(),
4838            st.sentinels.len(),
4839            st.n_lit,
4840            st.n_var
4841        );
4842    }
4843}
4844
4845/// Int-context operand bridge: a BOOLEAN-typed side of an int-kind Bin
4846/// (bitwise, arithmetic, comparison against a numeric) becomes
4847/// `(b ? 1 : 0)` — the dex-level truth (booleans ARE 0/1 ints there;
4848/// register reuse puts a bool-typed var into an int expression:
4849/// `v5x | 4`, `b == 0` — "boolean无法转换为int" / "二元运算符操作数
4850/// 类型错误" families). Runs AFTER fix_bool_xor so boolean-SINK chains
4851/// were already converted to all-boolean form and are skipped here.
4852pub fn fix_int_operand_bridges(body: &mut Stmt, vt: &VarTable) {
4853    fn side_bool(e: &Expr, vt: &VarTable) -> bool {
4854        matches!(
4855            match e {
4856                Expr::Local { var, .. } => vt.var(*var).ty.erased(),
4857                other => other.type_ref().erased(),
4858            },
4859            JavaType::Boolean
4860        )
4861    }
4862    fn side_int(e: &Expr, vt: &VarTable) -> bool {
4863        matches!(
4864            match e {
4865                Expr::Local { var, .. } => vt.var(*var).ty.erased(),
4866                other => other.type_ref().erased(),
4867            },
4868            JavaType::Int | JavaType::Short | JavaType::Byte | JavaType::Char | JavaType::Long
4869        )
4870    }
4871    fn wrap(e: Box<Expr>) -> Box<Expr> {
4872        Box::new(Expr::Cond {
4873            c: e,
4874            t: Box::new(Expr::Const(ConstVal::Int(1))),
4875            f: Box::new(Expr::Const(ConstVal::Int(0))),
4876        })
4877    }
4878    walk_stmt_exprs(body, &mut |e| {
4879        deep_rewrite(e, &mut |x| {
4880            // Field / array-store positions: the target's type is
4881            // descriptor-backed (authoritative); a boolean value into
4882            // an int slot bridges `(b ? 1 : 0)` (with a narrowing cast
4883            // for byte/short/char slots), an int value into a boolean
4884            // slot bridges `v != 0` (weixin `this.L[v3x] = max3`,
4885            // `g2x.d = p1x` — 785 field + ~700 array lines).
4886            if let Expr::Assign { target, value, op: AssignOp::Plain } = x {
4887                let tgt_ty: Option<JavaType> = match &**target {
4888                    Expr::Field { ty, .. } => Some(ty.erased()),
4889                    Expr::ArrayIndex { array, .. } => {
4890                        match array.type_ref().erased() {
4891                            JavaType::Array(el) => Some(el.as_ref().clone()),
4892                            _ => None,
4893                        }
4894                    }
4895                    // Local targets: the split/booleanize machinery owns
4896                    // the mixed-kind register cases; the residue that
4897                    // reached render (boolean无法转换为byte/long, ~360
4898                    // lines) gets the same value-side bridge.
4899                    Expr::Local { var, .. } => {
4900                        if (*var as usize) < vt.vars.len() {
4901                            Some(vt.var(*var).ty.erased())
4902                        } else {
4903                            None
4904                        }
4905                    }
4906                    _ => None,
4907                };
4908                if let Some(t) = tgt_ty {
4909                    let tgt_int = matches!(
4910                        t,
4911                        JavaType::Int
4912                            | JavaType::Short
4913                            | JavaType::Byte
4914                            | JavaType::Char
4915                            | JavaType::Long
4916                    );
4917                    if tgt_int && side_bool(value, vt) && !side_int(value, vt) {
4918                        let taken = std::mem::replace(
4919                            value,
4920                            Box::new(Expr::Const(ConstVal::Null)),
4921                        );
4922                        let mut wrapped = wrap(taken);
4923                        if !matches!(t, JavaType::Int | JavaType::Long) {
4924                            wrapped = Box::new(Expr::Cast {
4925                                ty: TypeRef::J(t.clone()),
4926                                e: wrapped,
4927                            });
4928                        }
4929                        *value = wrapped;
4930                    } else if matches!(t, JavaType::Boolean)
4931                        && side_int(value, vt)
4932                        && !side_bool(value, vt)
4933                    {
4934                        let taken = std::mem::replace(
4935                            value,
4936                            Box::new(Expr::Const(ConstVal::Null)),
4937                        );
4938                        **value = Expr::Bin {
4939                            op: BinOp::Ne,
4940                            l: taken,
4941                            r: Box::new(Expr::Const(ConstVal::Int(0))),
4942                            ty: Some(TypeRef::J(JavaType::Boolean)),
4943                        };
4944                    }
4945                }
4946            }
4947            // Explicit casts across the bool/num line are invalid Java
4948            // in BOTH directions: `(byte) this.r` (dex int-to-byte over
4949            // a boolean-rendered field — weixin AccInfo writeByte ×207)
4950            // becomes `(byte)(r ? 1 : 0)`, and `(boolean) x` over an
4951            // int becomes `x != 0`.
4952            if let Expr::Cast { ty, e } = x {
4953                let t = ty.erased();
4954                let t_num = matches!(
4955                    t,
4956                    JavaType::Int
4957                        | JavaType::Short
4958                        | JavaType::Byte
4959                        | JavaType::Char
4960                        | JavaType::Long
4961                        | JavaType::Float
4962                        | JavaType::Double
4963                );
4964                if t_num && side_bool(e, vt) && !side_int(e, vt) {
4965                    let taken = std::mem::replace(e, Box::new(Expr::Const(ConstVal::Null)));
4966                    **e = Expr::Cond {
4967                        c: taken,
4968                        t: Box::new(Expr::Const(ConstVal::Int(1))),
4969                        f: Box::new(Expr::Const(ConstVal::Int(0))),
4970                    };
4971                } else if matches!(t, JavaType::Boolean)
4972                    && side_int(e, vt)
4973                    && !side_bool(e, vt)
4974                {
4975                    let taken = std::mem::replace(e, Box::new(Expr::Const(ConstVal::Null)));
4976                    *x = Expr::Bin {
4977                        op: BinOp::Ne,
4978                        l: taken,
4979                        r: Box::new(Expr::Const(ConstVal::Int(0))),
4980                        ty: Some(TypeRef::J(JavaType::Boolean)),
4981                    };
4982                }
4983            }
4984            // Null-vs-int comparison: dex if-eqz/if-nez is type-agnostic
4985            // and null IS zero — when the local came out int-typed (phi
4986            // confluence residue), rewrite the Null constant to 0
4987            // (`v95_g12 != null` → `v95_g12 != 0`, faithful and legal;
4988            // yq0/g1 546-line 二元运算符 family).
4989            if let Expr::Bin { op, l, r, .. } = x {
4990                if matches!(op, BinOp::Eq | BinOp::Ne) {
4991                    if matches!(&**r, Expr::Const(ConstVal::Null))
4992                        && side_int(l, vt)
4993                        && !side_bool(l, vt)
4994                    {
4995                        **r = Expr::Const(ConstVal::Int(0));
4996                    } else if matches!(&**l, Expr::Const(ConstVal::Null))
4997                        && side_int(r, vt)
4998                        && !side_bool(r, vt)
4999                    {
5000                        **l = Expr::Const(ConstVal::Int(0));
5001                    }
5002                }
5003            }
5004            // A boolean-typed array INDEX is a reused register holding
5005            // an int (`this.L[v3x]` — boolean无法转换为int at the index
5006            // position): bridge it in reads and writes alike.
5007            if let Expr::ArrayIndex { index, .. } = x {
5008                if side_bool(index, vt) && !side_int(index, vt) {
5009                    let taken =
5010                        std::mem::replace(index, Box::new(Expr::Const(ConstVal::Null)));
5011                    *index = wrap(taken);
5012                }
5013            }
5014            // A boolean-typed array DIMENSION is the same reused-register
5015            // case at the allocation site (`new Object[v200]` where v200
5016            // is boolean-typed — weixin AppBrandRuntime, bool→int family):
5017            // dex new-array reads the size register as int and booleans
5018            // ARE 0/1 ints there, so bridge `(b ? 1 : 0)`.
5019            if let Expr::NewArray { dims, .. } = x {
5020                for d in dims.iter_mut() {
5021                    if side_bool(d, vt) && !side_int(d, vt) {
5022                        let taken =
5023                            std::mem::replace(d, Expr::Const(ConstVal::Null));
5024                        *d = *wrap(Box::new(taken));
5025                    }
5026                }
5027            }
5028            if let Expr::Bin { op, l, r, .. } = x {
5029                let int_kind = matches!(
5030                    op,
5031                    BinOp::Or
5032                        | BinOp::And
5033                        | BinOp::Xor
5034                        | BinOp::Add
5035                        | BinOp::Sub
5036                        | BinOp::Mul
5037                        | BinOp::Div
5038                        | BinOp::Rem
5039                        | BinOp::Shl
5040                        | BinOp::Shr
5041                        | BinOp::Ushr
5042                        | BinOp::Eq
5043                        | BinOp::Ne
5044                        | BinOp::Lt
5045                        | BinOp::Ge
5046                        | BinOp::Gt
5047                        | BinOp::Le
5048                );
5049                if !int_kind {
5050                    return;
5051                }
5052                // Comparisons only bridge against a NUMERIC constant or
5053                // int-typed side (a bool == bool compare is valid Java).
5054                if side_bool(l, vt) && side_int(r, vt) && !side_bool(r, vt) {
5055                    let taken = std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
5056                    *l = wrap(taken);
5057                } else if side_bool(r, vt) && side_int(l, vt) && !side_bool(l, vt) {
5058                    let taken = std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
5059                    *r = wrap(taken);
5060                }
5061            }
5062        });
5063    });
5064}
5065
5066/// Statement-level idiom restoration (readability parity with
5067/// jadx/DAD, flagged by the androguard comparison): `v = v + 1` →
5068/// `v++`, `v = v op x` → `v op= x`. Local targets only — a compound
5069/// field/array target would change how often the receiver expression
5070/// evaluates. Var must be the LEFT operand (non-commutative ops;
5071/// string-concat order). The ±1 fold requires a numeric vt type
5072/// (`s++` on a String is invalid while `s += 1` is not).
5073/// Boolean VALUE-diamond fold (DAD short_circuit value forms):
5074/// ```text
5075///   v = false; if (c) { v = true; }   →  v = c
5076///   v = true;  if (c) { v = false; }  →  v = !c
5077///   v = false; if (c) { v = E; }      →  v = c && E     (E bool-valued)
5078///   v = true;  if (c) { v = E; }      →  v = !c || E
5079///   v = D; if (c) { v = E1; } else { v = E2; }  →  v = c ? E1 : E2
5080/// ```
5081/// `v` must be Boolean-typed; the arm value bool-valued; the guard may
5082/// not touch `v`; base and arm constants must not coincide (a no-op
5083/// diamond could drop guard side effects). Kotlin default-arg bridge
5084/// ctors compute defaulted booleans in exactly this shape INSIDE the
5085/// branch ahead of the delegation (lark MmCreateAudioRequest's v21x
5086/// chain) — split_branch rejects the control flow as Dirty and the
5087/// this() call stays in the branch ("对this的调用必须是构造器中的第一
5088/// 个语句", lark ×456). Folded to a linear def (interleaved with
5089/// forward_single_use for the `[u = call; v = u]` arm shape) the
5090/// merge_at ternary fold applies. Source-faithful: the original was
5091/// `x?.m() ?: false` — one expression; the inlined call stays under
5092/// the guard arm, preserving evaluation order and short-circuiting.
5093/// General value-diamond fold (fold_bool_value_diamonds for ANY type):
5094/// `if (c) { v = a; } else { v = b; }` → `v = c ? a : b;`, plus the
5095/// split-pair shape `v = b; if (c) { v = a; }` → the same ternary. The
5096/// Kotlin default-arg ctor's mask-guarded per-arg defaults arrive as
5097/// diamonds (DocFreshInfo v10x/v14x); folding gives each carrier a
5098/// SINGLE-assignment def so the chain-aware delegation inline can hoist
5099/// the this() first ("对this的调用必须是构造器中的第一个语句"). Fold
5100/// only when both sides are exactly one plain assignment to the SAME
5101/// local and the value types are ternary-compatible (equal erased or
5102/// both numeric — mixed-category ternaries box/surprise).
5103pub fn fold_value_diamonds(body: &mut Stmt) {
5104    let Stmt::Block(stmts) = body else { return };
5105    fold_diamonds_in(stmts);
5106}
5107
5108fn diamond_types_ok(a: &Expr, b: &Expr) -> bool {
5109    fn num(t: &JavaType) -> bool {
5110        matches!(
5111            t,
5112            JavaType::Byte
5113                | JavaType::Short
5114                | JavaType::Int
5115                | JavaType::Char
5116                | JavaType::Long
5117                | JavaType::Float
5118                | JavaType::Double
5119        )
5120    }
5121    let ta = a.type_ref().erased();
5122    let tb = b.type_ref().erased();
5123    ta == tb || (num(&ta) && num(&tb))
5124}
5125
5126/// `(var, target clone, value)` when the stmt is exactly ONE plain
5127/// assign to a local (bare or block-wrapped). The unwrap must be
5128/// RECURSIVE: the Kotlin default-args ctor diamonds arrive as
5129/// `Block([Block([Assign])])` (structurer nesting), and the one-level
5130/// peel missed them — the unfolded `if ((mask&2)==0) v=p; else v=0;`
5131/// prelude then blocked fix_ctor_super_first from hoisting the
5132/// `this(...)` delegation (weibo/lark ctor-not-first ×841 family).
5133fn single_local_assign(st: &Stmt) -> Option<(u32, Expr, Expr)> {
5134    let mut inner = st;
5135    while let Stmt::Block(v) = inner {
5136        if v.len() != 1 {
5137            return None;
5138        }
5139        inner = &v[0];
5140    }
5141    match inner {
5142        Stmt::ExprStmt(Expr::Assign { target, value, op: AssignOp::Plain }) => {
5143            match &**target {
5144                Expr::Local { var, .. } => Some((*var, *target.clone(), *value.clone())),
5145                _ => None,
5146            }
5147        }
5148        _ => None,
5149    }
5150}
5151
5152fn mk_ternary_assign(tgt: Expr, c: Expr, a: Expr, b: Expr) -> Stmt {
5153    Stmt::ExprStmt(Expr::Assign {
5154        target: Box::new(tgt),
5155        op: AssignOp::Plain,
5156        value: Box::new(Expr::Cond {
5157            c: Box::new(c),
5158            t: Box::new(a),
5159            f: Box::new(b),
5160        }),
5161    })
5162}
5163
5164fn fold_diamonds_in(stmts: &mut Vec<Stmt>) {
5165    let mut i = 0usize;
5166    while i < stmts.len() {
5167        match &mut stmts[i] {
5168            Stmt::Block(v) => fold_diamonds_in(v),
5169            Stmt::If { then_stmt, else_stmt, .. } => {
5170                if let Stmt::Block(v) = &mut **then_stmt {
5171                    fold_diamonds_in(v);
5172                }
5173                if let Some(e) = else_stmt {
5174                    if let Stmt::Block(v) = &mut **e {
5175                        fold_diamonds_in(v);
5176                    }
5177                }
5178            }
5179            _ => {}
5180        }
5181        // Shape 1: standalone if/else diamond.
5182        let shape1 = if let Stmt::If { cond, then_stmt, else_stmt: Some(el), .. } = &stmts[i] {
5183            match (single_local_assign(then_stmt), single_local_assign(el)) {
5184                (Some((v1, tgt, a)), Some((v2, _, b)))
5185                    if v1 == v2 && diamond_types_ok(&a, &b) =>
5186                {
5187                    Some(mk_ternary_assign(tgt, cond.clone(), a, b))
5188                }
5189                _ => None,
5190            }
5191        } else {
5192            None
5193        };
5194        if let Some(f) = shape1 {
5195            stmts[i] = f;
5196            i += 1;
5197            continue;
5198        }
5199        // Shape 2: adjacent `v = b;` + `if (c) { v = a; }` (no else).
5200        if i + 1 < stmts.len() {
5201            let shape2 = if let Stmt::If { cond, then_stmt, else_stmt: None, .. } = &stmts[i + 1] {
5202                match (single_local_assign(&stmts[i]), single_local_assign(then_stmt)) {
5203                    (Some((v1, tgt, b)), Some((v2, _, a)))
5204                        if v1 == v2 && diamond_types_ok(&a, &b) =>
5205                    {
5206                        Some(mk_ternary_assign(tgt, cond.clone(), a, b))
5207                    }
5208                    _ => None,
5209                }
5210            } else {
5211                None
5212            };
5213            if let Some(f) = shape2 {
5214                stmts[i] = f;
5215                stmts.remove(i + 1);
5216                continue; // re-scan the folded position
5217            }
5218        }
5219        i += 1;
5220    }
5221}
5222
5223pub fn fold_bool_value_diamonds(body: &mut Stmt, vt: &VarTable) {
5224    fn is_bool_var(v: u32, vt: &VarTable) -> bool {
5225        (v as usize) < vt.vars.len()
5226            && matches!(vt.vars[v as usize].ty.erased(), JavaType::Boolean)
5227    }
5228    fn bool_const(e: &Expr) -> Option<bool> {
5229        fn cint(e: &Expr) -> Option<i32> {
5230            match e {
5231                Expr::Const(ConstVal::Int(n)) => Some(*n),
5232                _ => None,
5233            }
5234        }
5235        match e {
5236            Expr::Const(ConstVal::Int(0)) => Some(false),
5237            Expr::Const(ConstVal::Int(1)) => Some(true),
5238            // int↔bool bridge wrappings of constants: `0 != 0` is the
5239            // fallback pipeline's rendered `false` by the time the
5240            // ctor-block fold runs.
5241            Expr::Bin { op: op @ (BinOp::Eq | BinOp::Ne), l, r, .. } => {
5242                let (a, b) = (cint(l)?, cint(r)?);
5243                let eq = a == b;
5244                Some(if matches!(op, BinOp::Eq) { eq } else { !eq })
5245            }
5246            _ => None,
5247        }
5248    }
5249    fn reads_var(e: &Expr, v: u32) -> bool {
5250        let mut hit = false;
5251        visit_exprs(e, &mut |x| {
5252            if let Expr::Local { var, .. } = x {
5253                if *var == v {
5254                    hit = true;
5255                }
5256            }
5257        });
5258        hit
5259    }
5260    /// Bool-valued expression for the composite arms (&&, ||, ?:).
5261    /// Bare 0/1 constants are handled by the caller's special cases —
5262    /// inside a composite they would render as invalid `c && 1`.
5263    fn bool_valued(e: &Expr, vt: &VarTable) -> bool {
5264        fn side(e: &Expr, vt: &VarTable) -> bool {
5265            bool_const(e).is_some() || bool_valued(e, vt)
5266        }
5267        match e {
5268            Expr::Local { var, .. } => is_bool_var(*var, vt),
5269            Expr::Method { desc, .. } => desc.ret == JavaType::Boolean,
5270            Expr::InstanceOf { .. } => true,
5271            Expr::Un { op: UnOp::Not, e: x, .. } => bool_valued(x, vt) || bool_const(x).is_some(),
5272            Expr::Bin { op, l, r, .. } => match op {
5273                BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le => true,
5274                // A logical operand may itself be a 0/1 constant
5275                // (booleanize leaves mixed forms).
5276                BinOp::LogAnd | BinOp::LogOr => {
5277                    side(l, vt) && side(r, vt)
5278                }
5279                _ => false,
5280            },
5281            Expr::Cond { t, f, .. } => side(t, vt) && side(f, vt),
5282            _ => false,
5283        }
5284    }
5285    /// A branch that is exactly one plain `v = E` with E bool-valued
5286    /// (or a 0/1 constant) and not reading v — OR the forwarding PAIR
5287    /// `[u = E; v = u]` where u is single-assigned/single-read in the
5288    /// whole method: the fold consumes the pair into `v = E` (the impure
5289    /// `u = src.m()` arm shape forward_single_use cannot inline — its
5290    /// phi-ref gate rejects values reading multi-assigned prelude vars,
5291    /// and inlining here is scope-safe because E moves exactly one
5292    /// block level out with the arm's lone def consumed).
5293    fn lone_assign(
5294        b: &Stmt,
5295        v: u32,
5296        vt: &VarTable,
5297        an: &VarAnalysis,
5298        consumed: &mut HashSet<u32>,
5299    ) -> Option<Expr> {
5300        let l = flat_list(b);
5301        if l.len() == 2 {
5302            let (u, e): (u32, &Expr) = match l[0] {
5303                Stmt::LocalDef { var, init: Some(e), .. } => (*var, e),
5304                Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => {
5305                    match &**target {
5306                        Expr::Local { var, .. } => (*var, &**value),
5307                        _ => return None,
5308                    }
5309                }
5310                _ => return None,
5311            };
5312            if u == v || consumed.contains(&u) {
5313                return None;
5314            }
5315            // Whole-method single-assign/single-read: the fold drops
5316            // u's def with the arm, so ANY other read would dangle.
5317            // (Analysis counts are per-round; folds only ever REMOVE
5318            // reads elsewhere or clone E's — whose variable reads were
5319            // already counted at their def sites — so stale counts
5320            // over-approximate and the ==1 gates stay sound.)
5321            if an.assigns.get(u as usize).copied().unwrap_or(0) != 1
5322                || an.reads.get(u as usize).copied().unwrap_or(0) != 1
5323            {
5324                return None;
5325            }
5326            let (tv, rv) = match l[1] {
5327                Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => {
5328                    match (&**target, &**value) {
5329                        (Expr::Local { var: t, .. }, Expr::Local { var: r, .. }) => (*t, *r),
5330                        _ => return None,
5331                    }
5332                }
5333                Stmt::LocalDef { var, init: Some(Expr::Local { var: r, .. }), .. } => (*var, *r),
5334                _ => return None,
5335            };
5336            if tv != v || rv != u {
5337                return None;
5338            }
5339            if reads_var(e, v) || !(bool_valued(e, vt) || bool_const(e).is_some()) {
5340                return None;
5341            }
5342            consumed.insert(u);
5343            return Some(e.clone());
5344        }
5345        if l.len() != 1 {
5346            return None;
5347        }
5348        let (target, value) = match l[0] {
5349            Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => (target, value),
5350            Stmt::LocalDef { var, init: Some(value), .. } if *var == v => {
5351                if reads_var(value, v) || !(bool_valued(value, vt) || bool_const(value).is_some()) {
5352                    return None;
5353                }
5354                return Some(value.clone());
5355            }
5356            _ => return None,
5357        };
5358        let Expr::Local { var: tv, .. } = &**target else { return None };
5359        if *tv != v || reads_var(value, v) {
5360            return None;
5361        }
5362        if !(bool_valued(value, vt) || bool_const(value).is_some()) {
5363            return None;
5364        }
5365        Some((**value).clone())
5366    }
5367    fn not(e: Expr) -> Expr {
5368        Expr::Un { op: UnOp::Not, e: Box::new(e) }
5369    }
5370    /// Peephole the int↔bool bridge artifacts the fallback pipeline
5371    /// leaves in guards: `!(x != y)` → `x == y`; `(b ? 1 : 0) == 1` →
5372    /// `b`; `!(b ? 1 : 0 == 1)` chains likewise. Purely a readability
5373    /// pass — the unsimplified forms compile.
5374    fn simplify_bool(e: Expr) -> Expr {
5375        match e {
5376            Expr::Un { op: UnOp::Not, e: inner, .. } => match *inner {
5377                // Flip first, THEN re-simplify: `!((u?1:0) != 1)` must
5378                // reach the cond01 arm as `(u?1:0) == 1` → `u` (the
5379                // flip result never re-entered simplification and the
5380                // pair arm saw a Bin where it needed the bare local).
5381                Expr::Bin { op: BinOp::Ne, l, r, ty } => {
5382                    simplify_bool(Expr::Bin { op: BinOp::Eq, l, r, ty })
5383                }
5384                Expr::Bin { op: BinOp::Eq, l, r, ty } => {
5385                    simplify_bool(Expr::Bin { op: BinOp::Ne, l, r, ty })
5386                }
5387                other => Expr::Un { op: UnOp::Not, e: Box::new(simplify_bool(other)) },
5388            },
5389            Expr::Bin { op: op @ (BinOp::Eq | BinOp::Ne), l, r, ty } => {
5390                // (c ? 1 : 0) <op> 1  →  c / !c ; <op> 0 → !c / c
5391                fn cond01(e: &Expr) -> Option<Expr> {
5392                    if let Expr::Cond { c, t, f } = e {
5393                        if matches!(&**t, Expr::Const(ConstVal::Int(1)))
5394                            && matches!(&**f, Expr::Const(ConstVal::Int(0)))
5395                        {
5396                            return Some((**c).clone());
5397                        }
5398                    }
5399                    None
5400                }
5401                let (lc, rc) = (cond01(&l), cond01(&r));
5402                match (lc, rc) {
5403                    (Some(c), None) => match (&*r, op) {
5404                        (Expr::Const(ConstVal::Int(1)), BinOp::Eq) => c,
5405                        (Expr::Const(ConstVal::Int(1)), BinOp::Ne) => not(c),
5406                        (Expr::Const(ConstVal::Int(0)), BinOp::Eq) => not(c),
5407                        (Expr::Const(ConstVal::Int(0)), BinOp::Ne) => c,
5408                        _ => Expr::Bin { op, l: Box::new(c), r, ty },
5409                    },
5410                    (None, Some(c)) => match (&*l, op) {
5411                        (Expr::Const(ConstVal::Int(1)), BinOp::Eq) => c,
5412                        (Expr::Const(ConstVal::Int(1)), BinOp::Ne) => not(c),
5413                        (Expr::Const(ConstVal::Int(0)), BinOp::Eq) => not(c),
5414                        (Expr::Const(ConstVal::Int(0)), BinOp::Ne) => c,
5415                        _ => Expr::Bin { op, l, r: Box::new(c), ty },
5416                    },
5417                    _ => Expr::Bin { op, l, r, ty },
5418                }
5419            }
5420            other => other,
5421        }
5422    }
5423    fn bin(op: BinOp, l: Expr, r: Expr) -> Expr {
5424        Expr::Bin { op, l: Box::new(l), r: Box::new(r), ty: None }
5425    }
5426
5427    for _round in 0..4 {
5428        let mut changed = false;
5429        let mut analysis = VarAnalysis {
5430            assigns: Vec::new(),
5431            reads: Vec::new(),
5432            values: Vec::new(),
5433        };
5434        analyze_vars(body, &mut analysis);
5435        let mut consumed: HashSet<u32> = HashSet::default();
5436        walk_mut_deep(body, &mut |st| {
5437            let Stmt::Block(list) = st else { return };
5438            let mut i = 0usize;
5439            while i + 1 < list.len() {
5440                let folded = try_at(list, i, vt, &is_bool_var, &bool_const, &reads_var, &lone_assign, &analysis, &mut consumed);
5441                match folded {
5442                    Some(expr) => {
5443                        let is_def = matches!(&list[i], Stmt::LocalDef { .. });
5444                        let var = match &list[i] {
5445                            Stmt::LocalDef { var, .. } => *var,
5446                            Stmt::ExprStmt(Expr::Assign { target, .. }) => {
5447                                match &**target { Expr::Local { var, .. } => *var, _ => unreachable!() }
5448                            }
5449                            _ => unreachable!(),
5450                        };
5451                        let (is_final, force_type) = match &list[i] {
5452                            Stmt::LocalDef { is_final, force_type, .. } => (*is_final, *force_type),
5453                            _ => (false, false),
5454                        };
5455                        list[i] = if is_def {
5456                            Stmt::LocalDef { var, init: Some(expr), is_final, force_type }
5457                        } else {
5458                            Stmt::ExprStmt(Expr::Assign {
5459                                target: Box::new(Expr::Local {
5460                                    var,
5461                                    ty: TypeRef::J(JavaType::Boolean),
5462                                }),
5463                                op: AssignOp::Plain,
5464                                value: Box::new(expr),
5465                            })
5466                        };
5467                        list.remove(i + 1);
5468                        changed = true;
5469                        // stay at i: a preceding base may now pair with
5470                        // nothing new, but a FOLLOWING diamond may chain.
5471                    }
5472                    None => i += 1,
5473                }
5474            }
5475        });
5476        if !changed {
5477            break;
5478        }
5479    }
5480
5481    #[allow(clippy::too_many_arguments, clippy::type_complexity)]
5482    fn try_at(
5483        list: &[Stmt],
5484        i: usize,
5485        vt: &VarTable,
5486        is_bool_var: &dyn Fn(u32, &VarTable) -> bool,
5487        bool_const: &dyn Fn(&Expr) -> Option<bool>,
5488        reads_var: &dyn Fn(&Expr, u32) -> bool,
5489        lone_assign: &dyn Fn(&Stmt, u32, &VarTable, &VarAnalysis, &mut HashSet<u32>) -> Option<Expr>,
5490        an: &VarAnalysis,
5491        consumed: &mut HashSet<u32>,
5492    ) -> Option<Expr> {
5493        // s1: `v = <0|1>` (Assign or LocalDef), v Boolean-typed.
5494        let (v, base) = match &list[i] {
5495            Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => {
5496                match &**target {
5497                    Expr::Local { var, .. } => {
5498                        // The value may be a wrapped constant (`0 != 0`
5499                        // — the fallback pipeline's rendered false).
5500                        let b = bool_const(value)?;
5501                        if !is_bool_var(*var, vt) { return None; }
5502                        (*var, b)
5503                    }
5504                    _ => return None,
5505                }
5506            }
5507            Stmt::LocalDef { var, init: Some(e), .. } => {
5508                let b = bool_const(e)?;
5509                if !is_bool_var(*var, vt) { return None; }
5510                (*var, b)
5511            }
5512            _ => return None,
5513        };
5514        let Stmt::If { cond, then_stmt, else_stmt } = &list[i + 1] else {
5515            return None;
5516        };
5517        if reads_var(cond, v) {
5518            return None;
5519        }
5520        let mut touches_v = false;
5521        visit_exprs(cond, &mut |x| {
5522            if let Expr::Assign { target, .. } = x {
5523                if let Expr::Local { var, .. } = &**target {
5524                    if *var == v { touches_v = true; }
5525                }
5526            }
5527        });
5528        if touches_v {
5529            return None;
5530        }
5531        // The structurer emits inverted diamonds (`if (c) {} else { v = E }`,
5532        // empty-then) — normalize by negating the guard.
5533        let (cond_n, then_arm, else_arm): (Expr, &Stmt, Option<&Stmt>) =
5534            if flat_list(then_stmt).is_empty() && else_stmt.is_some() {
5535                (not(cond.clone()), else_stmt.as_ref().unwrap(), None)
5536            } else {
5537                (cond.clone(), then_stmt.as_ref(), else_stmt.as_deref())
5538            };
5539        let e1 = lone_assign(then_arm, v, vt, an, consumed)?;
5540        let e2 = match else_arm {
5541            None => None,
5542            Some(b) => Some(lone_assign(b, v, vt, an, consumed)?),
5543        };
5544        let e1c = bool_const(&e1);
5545        let c = simplify_bool(cond_n);
5546        Some(match e2 {
5547            None => {
5548                if !base {
5549                    match e1c {
5550                        Some(true) => c,
5551                        Some(false) => return None, // no-op diamond: guard effects would drop
5552                        None => bin(BinOp::LogAnd, c, e1),
5553                    }
5554                } else {
5555                    match e1c {
5556                        Some(false) => not(c),
5557                        Some(true) => return None,
5558                        None => bin(BinOp::LogOr, not(c), e1),
5559                    }
5560                }
5561            }
5562            Some(e2) => {
5563                // Full diamond: both arms assign — the base is dead.
5564                if e1 == e2 {
5565                    return None; // both arms equal: folding drops guard effects
5566                }
5567                Expr::Cond { c: Box::new(c), t: Box::new(e1), f: Box::new(e2) }
5568            }
5569        })
5570    }
5571}
5572
5573pub fn idiom_compounds(body: &mut Stmt, vt: &VarTable) {
5574    walk_mut_deep(body, &mut |st| {
5575        let Stmt::ExprStmt(e) = st else { return };
5576        let Expr::Assign { target, op: AssignOp::Plain, value } = &mut *e else {
5577            return;
5578        };
5579        // Target: a local, or a bare field (static / this — no complex
5580        // receiver whose re-evaluation a compound would elide).
5581        enum Tgt {
5582            Loc(u32),
5583            Fld,
5584        }
5585        let tgt = match &**target {
5586            Expr::Local { var, .. } => Tgt::Loc(*var),
5587            Expr::Field { owner, .. }
5588                if owner.is_none()
5589                    || matches!(&**owner.as_ref().unwrap(), Expr::This) =>
5590            {
5591                Tgt::Fld
5592            }
5593            _ => return,
5594        };
5595        let Expr::Bin { op, l, r, .. } = &mut **value else {
5596            return;
5597        };
5598        // The left operand must be the SAME storage as the target.
5599        match (&tgt, &**l) {
5600            (Tgt::Loc(v), Expr::Local { var, .. }) if *var == *v => {}
5601            (Tgt::Fld, Expr::Field { cls: c2, name: n2, .. }) => {
5602                let Expr::Field { cls: c1, name: n1, .. } = &**target else {
5603                    return;
5604                };
5605                if c1 != c2 || n1 != n2 {
5606                    return;
5607                }
5608            }
5609            _ => return,
5610        }
5611        // ±1 constant on a numeric var → post-inc/dec.
5612        let one = match &**r {
5613            Expr::Const(ConstVal::Int(1)) => Some(1i64),
5614            Expr::Const(ConstVal::Int(-1)) => Some(-1),
5615            Expr::Const(ConstVal::Long(1)) => Some(1),
5616            Expr::Const(ConstVal::Long(-1)) => Some(-1),
5617            _ => None,
5618        };
5619        if *op == BinOp::Add || *op == BinOp::Sub {
5620            if let Some(mut d) = one {
5621                if *op == BinOp::Sub {
5622                    d = -d;
5623                }
5624                let tgt_ty = match &tgt {
5625                    Tgt::Loc(v) => vt.var(*v).ty.erased(),
5626                    Tgt::Fld => {
5627                        let Expr::Field { ty, .. } = &**target else {
5628                            return;
5629                        };
5630                        ty.erased()
5631                    }
5632                };
5633                let numeric = matches!(
5634                    tgt_ty,
5635                    JavaType::Int
5636                        | JavaType::Long
5637                        | JavaType::Short
5638                        | JavaType::Byte
5639                        | JavaType::Char
5640                        | JavaType::Float
5641                        | JavaType::Double
5642                );
5643                if numeric {
5644                    let wide = matches!(tgt_ty, JavaType::Long);
5645                    let inner = match &tgt {
5646                        Tgt::Loc(v) => Expr::Local {
5647                            var: *v,
5648                            ty: vt.var(*v).ty.clone(),
5649                        },
5650                        Tgt::Fld => (**target).clone(),
5651                    };
5652                    *e = Expr::PostIncDec {
5653                        e: Box::new(inner),
5654                        delta: d,
5655                        wide,
5656                    };
5657                    return;
5658                }
5659            }
5660        }
5661        let cop = match op {
5662            BinOp::Add => AssignOp::Add,
5663            BinOp::Sub => AssignOp::Sub,
5664            BinOp::Mul => AssignOp::Mul,
5665            BinOp::Div => AssignOp::Div,
5666            BinOp::Rem => AssignOp::Rem,
5667            BinOp::Shl => AssignOp::Shl,
5668            BinOp::Shr => AssignOp::Shr,
5669            BinOp::Ushr => AssignOp::Ushr,
5670            BinOp::And => AssignOp::And,
5671            BinOp::Or => AssignOp::Or,
5672            BinOp::Xor => AssignOp::Xor,
5673            _ => return,
5674        };
5675        let rhs = std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
5676        let new_target = match &tgt {
5677            Tgt::Loc(v) => Box::new(Expr::Local {
5678                var: *v,
5679                ty: vt.var(*v).ty.clone(),
5680            }),
5681            Tgt::Fld => target.clone(),
5682        };
5683        *e = Expr::Assign {
5684            target: new_target,
5685            op: cop,
5686            value: rhs,
5687        };
5688    });
5689}
5690
5691pub fn fix_bool_xor(body: &mut Stmt, vt: &VarTable, ret_bool: bool) {
5692    fn is_one(e: &Expr) -> bool {
5693        matches!(e, Expr::Const(ConstVal::Int(1)))
5694    }
5695    fn bty(e: &Expr, vt: &VarTable) -> bool {
5696        matches!(
5697            match e {
5698                Expr::Local { var, .. } => vt.var(*var).ty.erased(),
5699                other => other.type_ref().erased(),
5700            },
5701            JavaType::Boolean
5702        )
5703    }
5704    // Int-typed `x ^ 1` in a BOOLEAN sink is the compiler's `!x` over a
5705    // 0/1 slot: rewrite to `x == 0` (the booleanize return-wrap misses
5706    // Xor shapes — weibo ArraysKt `return v2 ^ 1;` against a boolean
5707    // return, "int无法转换为boolean").
5708    fn inv_of_xor(x: &Expr, vt: &VarTable) -> Option<Expr> {
5709        if let Expr::Bin { op: BinOp::Xor, l, r, .. } = x {
5710            let (int_side, one_side) = if is_one(r) {
5711                (l, r)
5712            } else if is_one(l) {
5713                (r, l)
5714            } else {
5715                return None;
5716            };
5717            let _ = one_side;
5718            if !bty(int_side, vt) {
5719                return Some(Expr::Bin {
5720                    op: BinOp::Eq,
5721                    l: int_side.clone(),
5722                    r: Box::new(Expr::Const(ConstVal::Int(0))),
5723                    ty: Some(TypeRef::J(JavaType::Boolean)),
5724                });
5725            }
5726        }
5727        None
5728    }
5729    let tgt_bool = |target: &Expr, vt: &VarTable| -> bool {
5730        match target {
5731            Expr::Local { var, .. } => matches!(vt.var(*var).ty.erased(), JavaType::Boolean),
5732            Expr::Field { ty, .. } => matches!(ty.erased(), JavaType::Boolean),
5733            _ => false,
5734        }
5735    };
5736    walk_mut_deep(body, &mut |st| match st {
5737        Stmt::Return(Some(e)) if ret_bool => {
5738            if let Some(r) = inv_of_xor(e, vt) {
5739                *e = r;
5740            }
5741        }
5742        Stmt::ExprStmt(Expr::Assign { target, value, op: AssignOp::Plain, .. }) => {
5743            if tgt_bool(target, vt) {
5744                if let Some(r) = inv_of_xor(value, vt) {
5745                    **value = r;
5746                }
5747            }
5748        }
5749        Stmt::LocalDef { var, init: Some(value), .. }
5750            if matches!(vt.var(*var).ty.erased(), JavaType::Boolean) =>
5751        {
5752            if let Some(r) = inv_of_xor(value, vt) {
5753                *value = r;
5754            }
5755        }
5756        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
5757            if let Some(r) = inv_of_xor(cond, vt) {
5758                *cond = r;
5759            }
5760        }
5761        _ => {}
5762    });
5763    fn ity(e: &Expr, vt: &VarTable) -> bool {
5764        matches!(
5765            match e {
5766                Expr::Local { var, .. } => vt.var(*var).ty.erased(),
5767                other => other.type_ref().erased(),
5768            },
5769            JavaType::Int | JavaType::Short | JavaType::Byte | JavaType::Char
5770        )
5771    }
5772    /// Mixed-kind bitwise rewrite: Kotlin's non-short-circuit
5773    /// `or`/`and` over 0/1 ints mixes generations once booleanize
5774    /// converts one side (`delete | delete2` — "boolean无法转换为int" at
5775    /// the OPERAND, weixin SQLiteDatabase ×3.3k), and `b ^ 1` is `!b`.
5776    /// The int side is a 0/1 encoding: compare it, keeping the chain
5777    /// boolean end to end. ONLY legal into a BOOLEAN sink — ungated, it
5778    /// rewrote int-sink `v6 = v5x | 4` into `v5x | 4 != 0` assigned to
5779    /// an int var (boolean无法转换为int, weixin yq5/c ×2.1k family).
5780    fn mixed_rewrite(e: &mut Expr, vt: &VarTable) {
5781        deep_rewrite(e, &mut |x| {
5782            if let Expr::Bin { op, l, r, ty } = x {
5783                match op {
5784                    BinOp::Xor => {
5785                        if is_one(r) && bty(l, vt) {
5786                            let taken =
5787                                std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
5788                            *x = Expr::Un { op: UnOp::Not, e: taken };
5789                        } else if is_one(l) && bty(r, vt) {
5790                            let taken =
5791                                std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
5792                            *x = Expr::Un { op: UnOp::Not, e: taken };
5793                        }
5794                    }
5795                    BinOp::Or | BinOp::And => {
5796                        if bty(l, vt) && ity(r, vt) {
5797                            let taken =
5798                                std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
5799                            **r = Expr::Bin {
5800                                op: BinOp::Ne,
5801                                l: taken,
5802                                r: Box::new(Expr::Const(ConstVal::Int(0))),
5803                                ty: Some(TypeRef::J(JavaType::Boolean)),
5804                            };
5805                            *ty = Some(TypeRef::J(JavaType::Boolean));
5806                        } else if ity(l, vt) && bty(r, vt) {
5807                            let taken =
5808                                std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
5809                            **l = Expr::Bin {
5810                                op: BinOp::Ne,
5811                                l: taken,
5812                                r: Box::new(Expr::Const(ConstVal::Int(0))),
5813                                ty: Some(TypeRef::J(JavaType::Boolean)),
5814                            };
5815                            *ty = Some(TypeRef::J(JavaType::Boolean));
5816                        }
5817                    }
5818                    _ => {}
5819                }
5820            }
5821        });
5822    }
5823    // Boolean sinks: bool-typed assign/def targets, boolean-method
5824    // returns, and loop/branch conditions.
5825    walk_mut_deep(body, &mut |st| match st {
5826        Stmt::Return(Some(e)) if ret_bool => mixed_rewrite(e, vt),
5827        Stmt::ExprStmt(Expr::Assign { target, value, op: AssignOp::Plain, .. }) => {
5828            if tgt_bool(target, vt) {
5829                mixed_rewrite(value, vt);
5830            }
5831        }
5832        Stmt::LocalDef { var, init: Some(e), .. }
5833            if matches!(vt.var(*var).ty.erased(), JavaType::Boolean) =>
5834        {
5835            mixed_rewrite(e, vt);
5836        }
5837        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
5838            mixed_rewrite(cond, vt);
5839        }
5840        _ => {}
5841    });
5842    // A conditional's test is a boolean sink wherever it sits (including
5843    // inside int-sink statements the walk above skips).
5844    walk_stmt_exprs(body, &mut |e| {
5845        deep_rewrite(e, &mut |x| {
5846            if let Expr::Cond { c, .. } = x {
5847                mixed_rewrite(c, vt);
5848            }
5849        });
5850    });
5851}
5852
5853/// Boolean inference: vars only ever assigned 0/1/comparisons/booleans and
5854/// read in conditions become `boolean`, with `v != 0` → `v` in conditions.
5855/// Returns the number of vars converted (0 = nothing changed — the
5856/// caller skips the post-booleanize re-split, which only has work when
5857/// a conversion exposed a mixed-kind register).
5858pub fn booleanize(vt: &mut VarTable, body: &mut Stmt, ret_bool: bool) -> usize {
5859    // Booleans propagate through local chains (`v17 = v24` where v24
5860    // itself became boolean in the first round) — iterate to a fixpoint;
5861    // the common corpus converts nothing and exits after one round.
5862    // The deref disqualifier set is structural (positions, not types) —
5863    // compute ONCE per call, not per round (12 full walks on big
5864    // methods cost weibo +60% wall).
5865    let n0 = vt.vars.len();
5866    let mut derefed = vec![false; n0];
5867    mark_derefs(body, &mut derefed);
5868    let mut total = 0usize;
5869    for _ in 0..4 {
5870        let n = booleanize_round(vt, body, ret_bool, &derefed);
5871        if n == 0 {
5872            break;
5873        }
5874        total += n;
5875    }
5876    total
5877}
5878
5879/// Mark vars used in DEREF positions (field/method receiver, array
5880/// base) — allocation-free recursion (visit_all_exprs allocates a
5881/// child Vec per statement; two callers run this per method).
5882fn mark_deref_expr(e: &Expr, derefed: &mut [bool]) {
5883    match e {
5884        Expr::Field { owner: Some(o), is_static: false, .. }
5885        | Expr::Method { owner: Some(o), .. } => {
5886            if let Expr::Local { var, .. } = &**o {
5887                if (*var as usize) < derefed.len() {
5888                    derefed[*var as usize] = true;
5889                }
5890            }
5891        }
5892        Expr::ArrayIndex { array, .. } => {
5893            if let Expr::Local { var, .. } = &**array {
5894                if (*var as usize) < derefed.len() {
5895                    derefed[*var as usize] = true;
5896                }
5897            }
5898        }
5899        _ => {}
5900    }
5901    for_each_child(e, &mut |c| mark_deref_expr(c, derefed));
5902}
5903
5904fn mark_derefs(body: &Stmt, derefed: &mut [bool]) {
5905    walk_all(body, &mut |st| {
5906        match st {
5907            Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
5908                mark_deref_expr(e, derefed);
5909            }
5910            Stmt::Return(Some(e)) => mark_deref_expr(e, derefed),
5911            Stmt::LocalDef { init: Some(e), .. } => mark_deref_expr(e, derefed),
5912            Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
5913                mark_deref_expr(cond, derefed);
5914            }
5915            Stmt::For { init, .. } => {
5916                for x in init {
5917                    mark_derefs(x, derefed);
5918                }
5919            }
5920            Stmt::Switch { selector, .. } => mark_deref_expr(selector, derefed),
5921            Stmt::ForEach { iterable, .. } => mark_deref_expr(iterable, derefed),
5922            _ => {}
5923        }
5924    });
5925}
5926
5927fn booleanize_round(
5928    vt: &mut VarTable,
5929    body: &mut Stmt,
5930    ret_bool: bool,
5931    derefed: &[bool],
5932) -> usize {
5933    let n = vt.vars.len();
5934    let mut in_cond = vec![false; n];
5935
5936    // Condition uses: `v == 0` / `v != 0` if-conditions and comparison
5937    // operands inside assigned values.
5938    walk_all(body, &mut |st| {
5939        let cond: Option<&Expr> = match st {
5940            Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
5941                Some(cond)
5942            }
5943            _ => None,
5944        };
5945        if let Some(c) = cond {
5946            if let Expr::Bin {
5947                op: BinOp::Eq | BinOp::Ne,
5948                l,
5949                r,
5950                ..
5951            } = c
5952            {
5953                for (x, other) in [(l, r), (r, l)] {
5954                    if let (Expr::Local { var, .. }, Expr::Const(ConstVal::Int(0))) =
5955                        (&**x, &**other)
5956                    {
5957                        if (*var as usize) < n {
5958                            in_cond[*var as usize] = true;
5959                        }
5960                    }
5961                }
5962            }
5963        }
5964        // A local RETURNED from a boolean method is boolean-typed even
5965        // though it never appears in a condition (`int v9 = 0/1 … return
5966        // v9;` — returning an int from `boolean check()` is a compile
5967        // error; lab package Obf.check).
5968        if ret_bool {
5969            if let Stmt::Return(Some(Expr::Local { var, .. })) = st {
5970                if (*var as usize) < n {
5971                    in_cond[*var as usize] = true;
5972                }
5973            }
5974        }
5975        // A local STORED into a boolean field is boolean-typed even though
5976        // it never appears in a condition (`b = v1` where `b` is a boolean
5977        // field, `v1` an int 0/1 local — q.java static init). The emit
5978        // layer already coerces a boolean-target assign through expr_bool,
5979        // but that renders a bare int LOCAL unchanged, so the local itself
5980        // must be booleanized (its 0/1 assigns then print false/true).
5981        if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = st {
5982            if let Expr::Field { ty, .. } = &**target {
5983                if ty.erased() == JavaType::Boolean {
5984                    if let Expr::Local { var, .. } = &**value {
5985                        if (*var as usize) < n {
5986                            in_cond[*var as usize] = true;
5987                        }
5988                    }
5989                }
5990            }
5991        }
5992        let val: Option<&Expr> = match st {
5993            Stmt::ExprStmt(Expr::Assign { value, .. }) => Some(value),
5994            // Bare expression statements (e.g. `q(v117);`) so a local
5995            // passed to a boolean parameter is caught below.
5996            Stmt::ExprStmt(e) => Some(e),
5997            Stmt::LocalDef { init: Some(e), .. } => Some(e),
5998            Stmt::Return(Some(e)) => Some(e),
5999            _ => None,
6000        };
6001        if let Some(v) = val {
6002            visit_exprs(v, &mut |x| {
6003                // A local passed as a BOOLEAN parameter is boolean-typed
6004                // (`q(v117)` where q's param is boolean, v117 an int 0/1 —
6005                // f8/b.java). The DEX passes booleans as 0/1 ints, so a
6006                // boolean param slot is authoritative for the argument's
6007                // type.
6008                if let Expr::Method { desc, args, .. } = x {
6009                    for (i, a) in args.iter().enumerate() {
6010                        if desc.args.get(i) == Some(&JavaType::Boolean) {
6011                            if let Expr::Local { var, .. } = a {
6012                                if (*var as usize) < n {
6013                                    in_cond[*var as usize] = true;
6014                                }
6015                            }
6016                        }
6017                    }
6018                }
6019                if let Expr::Bin { op, l, r, .. } = x {
6020                    if matches!(
6021                        op,
6022                        BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
6023                    ) {
6024                        for side in [l, r] {
6025                            if let Expr::Local { var, .. } = &**side {
6026                                if (*var as usize) < n {
6027                                    in_cond[*var as usize] = true;
6028                                }
6029                            }
6030                        }
6031                    }
6032                    // Kotlin/d8 merge booleans through INT bitwise ops
6033                    // (`v3 | obj instanceof g`) == 0 — the other side's
6034                    // static type being boolean makes this a boolean
6035                    // context for the local (a genuine int bitwise
6036                    // expression never has a boolean operand).
6037                    if matches!(op, BinOp::Or | BinOp::And) {
6038                        let l_bool = l.type_ref().erased() == JavaType::Boolean;
6039                        let r_bool = r.type_ref().erased() == JavaType::Boolean;
6040                        if l_bool != r_bool {
6041                            let side = if l_bool { r } else { l };
6042                            if let Expr::Local { var, .. } = &**side {
6043                                if (*var as usize) < n {
6044                                    in_cond[*var as usize] = true;
6045                                }
6046                            }
6047                        }
6048                    }
6049                }
6050            });
6051        }
6052    });
6053
6054    // A var whose every assignment value is boolean-shaped AND which is
6055    // referenced in conditions becomes boolean.
6056    // (Perf: this used to re-walk the whole statement tree ONCE PER VAR —
6057    // O(vars × stmts); on R8-merged monsters (2000+ vars, 15k statements)
6058    // that was tens of millions of visits. One pass collects the same
6059    // (any, all_bool) facts for every var.)
6060    let mut assigned_any = vec![false; n];
6061    let mut all_bool = vec![true; n];
6062    let mut edges: Vec<(usize, usize)> = Vec::new();
6063    walk_all(body, &mut |st| {
6064        let (var, value) = match st {
6065            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => match &**target {
6066                Expr::Local { var, .. } => (var, &**value),
6067                _ => return,
6068            },
6069            Stmt::LocalDef {
6070                var, init: Some(e), ..
6071            } => (var, e),
6072            _ => return,
6073        };
6074        let i = *var as usize;
6075        if i < n {
6076            assigned_any[i] = true;
6077            if !is_boolean_valued(value, vt) {
6078                all_bool[i] = false;
6079            }
6080            // Chain edges for the context closure below: `v17 = v24`
6081            // links the two locals; when the TARGET is in a boolean
6082            // context, the source inherits it (its only reader is the
6083            // boolean-shaped chain). Collected as edges because the
6084            // target's context can be discovered anywhere in the tree.
6085            if let Expr::Local { var: src, .. } = value {
6086                if (*src as usize) < n {
6087                    edges.push((i, *src as usize));
6088                }
6089            }
6090        }
6091    });
6092    // Context closure along local chains: a var in a boolean context
6093    // pushes that context through every local it is assigned from
6094    // (`return v17` — v17 = v24 — v24 = 0/1) — the statement order
6095    // cannot be relied on (the return may follow the assignment).
6096    loop {
6097        let mut changed = false;
6098        for &(tgt, src) in &edges {
6099            if in_cond[tgt] && !in_cond[src] && all_bool[src] {
6100                in_cond[src] = true;
6101                changed = true;
6102            }
6103            // FORWARD through pure copies: `v21 = v17` with v17 boolean
6104            // makes v21 boolean when ALL of v21's assignments are
6105            // bool-shaped (the loop-state save pattern — weixin u2/f's
6106            // v62 → v17 → v21 chains left the copy targets int while
6107            // the accumulator booleanized).
6108            if in_cond[src] && !in_cond[tgt] && all_bool[tgt] {
6109                in_cond[tgt] = true;
6110                changed = true;
6111            }
6112        }
6113        if !changed {
6114            break;
6115        }
6116    }
6117    let reads = count_locals_stmts(std::slice::from_ref(body));
6118    let mut boolean_vars: HashSet<u32> = HashSet::default();
6119    for i in 0..n {
6120        if vt.vars[i].is_param {
6121            continue;
6122        }
6123        if derefed[i] {
6124            continue;
6125        }
6126        if !in_cond[i] && !(reads.get(&(i as u32)).copied().unwrap_or(0) == 0) {
6127            continue;
6128        }
6129        if assigned_any[i] && all_bool[i] {
6130            boolean_vars.insert(i as u32);
6131        }
6132    }
6133    if boolean_vars.is_empty() {
6134        return 0;
6135    }
6136    for v in &boolean_vars {
6137        vt.vars[*v as usize].ty = TypeRef::J(JavaType::Boolean);
6138    }
6139    let types: Vec<&TypeRef> = vt.vars.iter().map(|v| &v.ty).collect();
6140    rewrite_exprs(body, &mut |e| {
6141        deep_rewrite(e, &mut |x| {
6142            if let Expr::Local { var, ty } = x {
6143                if let Some(want) = types.get(*var as usize) {
6144                    if ty != *want {
6145                        *ty = (*want).clone();
6146                    }
6147                }
6148            }
6149        });
6150    });
6151
6152    // Condition folding: `b != 0` → `b`, `b == 0` → `!b` (boolean vars).
6153    fold_bool_conditions(body, &boolean_vars);
6154    boolean_vars.len()
6155}
6156
6157fn is_boolean_valued(e: &Expr, vt: &VarTable) -> bool {
6158    match e {
6159        Expr::Const(ConstVal::Int(0)) | Expr::Const(ConstVal::Int(1)) => true,
6160        // Kotlin's non-short-circuit boolean `or`/`and` compiles to `|`/`&`
6161        // over boolean locals (`v15 = delete | delete2 | ..`). EITHER side
6162        // boolean suffices: `int | boolean` is illegal in source — it is
6163        // always a not-yet-booleanized side of a boolean chain (the
6164        // loop-carried accumulator `v15 = v20; v20 = v15 | delete3` can
6165        // never seed its all_bool fixpoint otherwise).
6166        Expr::Bin { op: BinOp::Or | BinOp::And, l, r, .. } => {
6167            is_boolean_valued(l, vt) || is_boolean_valued(r, vt)
6168        }
6169        // Kotlin's `!x` lowers to `x ^ 1`; at booleanize time the value
6170        // is still the Xor (fix_bool_xor runs later), so an int-typed
6171        // generation receiving it never retyped (weixin v2/i's
6172        // `int v264_g160_g7 = !v200`). BOTH sides must be bool-shaped:
6173        // `bool ^ 1` qualifies (Const 0/1 is boolean-valued above),
6174        // a genuine int `flags ^ 1` does not (flags stays Int in vt).
6175        // MUST precede the generic Bin arm (which would swallow Xor).
6176        Expr::Bin {
6177            op: BinOp::Xor,
6178            l,
6179            r,
6180            ..
6181        } => is_boolean_valued(l, vt) && is_boolean_valued(r, vt),
6182        Expr::Bin { op, .. } => matches!(
6183            op,
6184            BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
6185        ),
6186        Expr::InstanceOf { .. } => true,
6187        // The VarTable is the authority: the embedded Local ty is a
6188        // lift-time snapshot and goes STALE across booleanize rounds —
6189        // a converted copy source kept its partners int (`v150 = v131`
6190        // boolean→int, weixin ConstraintLayout ×1.4k files) because the
6191        // all_bool fixpoint consulted the stale Int.
6192        Expr::Local { var, ty } => {
6193            let t = if (*var as usize) < vt.vars.len() {
6194                vt.var(*var).ty.erased()
6195            } else {
6196                ty.erased()
6197            };
6198            t == JavaType::Boolean
6199        }
6200        Expr::Method { desc, .. } => desc.ret == JavaType::Boolean,
6201        Expr::Cond { t, f, .. } => is_boolean_valued(t, vt) && is_boolean_valued(f, vt),
6202        Expr::Un { op: UnOp::Not, .. } => true,
6203        // Field ty comes from the dex descriptor — authoritative, not a
6204        // lift snapshot (`!w1x.i` arrives here still as `w1x.i ^ 1`; the
6205        // Xor arm needs this side bool-shaped).
6206        Expr::Field { ty, .. } => ty.erased() == JavaType::Boolean,
6207        _ => false,
6208    }
6209}
6210
6211fn fold_bool_conditions(s: &mut Stmt, bv: &HashSet<u32>) {
6212    let fold = |e: &mut Expr| {
6213        deep_rewrite(e, &mut |x| {
6214            if let Expr::Bin { op, l, r, .. } = x {
6215                let (var_side, const_side) = match (&**l, &**r) {
6216                    (Expr::Local { .. }, Expr::Const(ConstVal::Int(_))) => (l, 0),
6217                    (Expr::Const(ConstVal::Int(_)), Expr::Local { .. }) => (r, 1),
6218                    _ => return,
6219                };
6220                let var = match &**var_side {
6221                    Expr::Local { var, .. } => *var,
6222                    _ => return,
6223                };
6224                if !bv.contains(&var) {
6225                    return;
6226                }
6227                let _ = const_side;
6228                match op {
6229                    BinOp::Ne => {
6230                        let inner = var_side.clone();
6231                        *x = *inner;
6232                    }
6233                    BinOp::Eq => {
6234                        let inner = var_side.clone();
6235                        *x = Expr::Un {
6236                            op: UnOp::Not,
6237                            e: Box::new(*inner),
6238                        };
6239                    }
6240                    _ => {}
6241                }
6242            }
6243        });
6244    };
6245    match s {
6246        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => fold(cond),
6247        _ => {}
6248    }
6249    walk_mut_deep(s, &mut |st| {
6250        let cond = match st {
6251            Stmt::If { cond, .. } => Some(cond),
6252            Stmt::While { cond, .. } => Some(cond),
6253            Stmt::DoWhile { cond, .. } => Some(cond),
6254            _ => None,
6255        };
6256        if let Some(c) = cond {
6257            fold(c);
6258        }
6259    });
6260}
6261
6262/// Normalize `1 == v` comparisons to `v == 1` (d8 emits const-first if-eq).
6263pub fn flip_const_compares(s: &mut Stmt) {
6264    rewrite_exprs(s, &mut |e| {
6265        deep_rewrite(e, &mut |x| {
6266            if let Expr::Bin { op, l, r, .. } = x {
6267                if matches!(
6268                    op,
6269                    BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
6270                ) {
6271                    let l_const = matches!(&**l, Expr::Const(_));
6272                    let r_var = matches!(&**r, Expr::Local { .. });
6273                    if l_const && r_var {
6274                        std::mem::swap(l, r);
6275                    }
6276                }
6277            }
6278        });
6279    });
6280}
6281
6282/// Object-typed locals compared against 0 compare against `null`.
6283pub fn null_compares(vt: &VarTable, body: &mut Stmt) {
6284    let obj_vars: HashSet<u32> = vt
6285        .vars
6286        .iter()
6287        .filter(|v| v.ty.erased().is_reference())
6288        .map(|v| v.id)
6289        .collect();
6290    rewrite_exprs(body, &mut |e| {
6291        deep_rewrite(e, &mut |x| {
6292            null_side_rewrite(x, &|v| obj_vars.contains(&v));
6293        });
6294    });
6295}
6296
6297/// Drop assignments/declarations of locals that are NEVER read anywhere
6298/// in the method. Register-machine merges commit values no Java-level
6299/// code consumes (phi residue like `printStream = check;` — not just
6300/// noise: the merge variable's inferred type need not match the value,
6301/// so these lines are often type errors as well). Side-effect-free
6302/// values drop entirely; an impure value survives as a bare expression
6303/// statement. Iterated to a fixpoint: dropping `a = b` can make `b`
6304/// unread.
6305pub fn drop_dead_locals(body: &mut Stmt) {
6306    loop {
6307        let mut reads: Vec<usize> = Vec::new();
6308        count_reads(body, &mut reads);
6309        let mut dropped = 0usize;
6310        walk_mut_deep(body, &mut |st| match st {
6311            Stmt::Block(items) => prune_dead_items(items, &reads, &mut dropped),
6312            // Switch case bodies and For inits are bare `Vec<Stmt>`, NOT
6313            // wrapped in a `Stmt::Block`, so the Block arm never sees them.
6314            // Dead phi commits land directly in case bodies (`sb4 =
6315            // compareTo;` per case): pruning only Blocks dropped the
6316            // commit's reader (in a real post-switch Block) but left the
6317            // now-dead commits, stalling the fixpoint cascade and leaking
6318            // mistyped phi residue. TryWithResources.resources is
6319            // deliberately excluded — its LocalDefs carry auto-close
6320            // semantics that a bare-expression rewrite would break.
6321            Stmt::Switch { cases, .. } => {
6322                for c in cases {
6323                    prune_dead_items(&mut c.body, &reads, &mut dropped);
6324                }
6325            }
6326            Stmt::For { init, .. } => prune_dead_items(init, &reads, &mut dropped),
6327            _ => {}
6328        });
6329        if dropped == 0 {
6330            break;
6331        }
6332    }
6333}
6334
6335/// Remove dead assignments/declarations from one bare statement vector.
6336/// Shared by `drop_dead_locals` across every prunable `Vec<Stmt>`
6337/// container. Side-effect-free values drop entirely; an impure value
6338/// survives as a bare expression statement.
6339fn prune_dead_items(items: &mut Vec<Stmt>, reads: &[usize], dropped: &mut usize) {
6340    let dead = |v: u32| reads.get(v as usize).copied().unwrap_or(0) == 0;
6341    items.retain_mut(|x| match x {
6342        Stmt::ExprStmt(Expr::Assign { target, value, op, .. }) => {
6343            let dead_target = matches!(&**target, Expr::Local { var, .. } if dead(*var));
6344            if dead_target && matches!(*op, AssignOp::Plain) {
6345                *dropped += 1;
6346                if has_side_effects(value) {
6347                    let e = std::mem::replace(value, Box::new(Expr::This));
6348                    *x = Stmt::ExprStmt(*e);
6349                    true
6350                } else {
6351                    false
6352                }
6353            } else {
6354                true
6355            }
6356        }
6357        Stmt::LocalDef { var, init, .. } if dead(*var) => {
6358            *dropped += 1;
6359            if let Some(e) = init.take() {
6360                if has_side_effects(&e) {
6361                    *x = Stmt::ExprStmt(e);
6362                    return true;
6363                }
6364            }
6365            false
6366        }
6367        _ => true,
6368    });
6369}
6370
6371/// Immutable shallow child walk (mirrors `walk_mut`), for the two-pass
6372/// scope analysis below.
6373fn for_each_child_stmt<'a>(st: &'a Stmt, f: &mut impl FnMut(&'a Stmt)) {
6374    match st {
6375        Stmt::Block(v) => {
6376            for x in v {
6377                f(x);
6378            }
6379        }
6380        Stmt::If {
6381            then_stmt,
6382            else_stmt,
6383            ..
6384        } => {
6385            f(then_stmt);
6386            if let Some(e) = else_stmt {
6387                f(e);
6388            }
6389        }
6390        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => f(body),
6391        Stmt::For { init, body, .. } => {
6392            for x in init {
6393                f(x);
6394            }
6395            f(body);
6396        }
6397        Stmt::ForEach { body, .. } => f(body),
6398        Stmt::Switch { cases, default, .. } => {
6399            for c in cases {
6400                for x in &c.body {
6401                    f(x);
6402                }
6403            }
6404            if let Some(d) = default {
6405                f(d);
6406            }
6407        }
6408        Stmt::Try {
6409            body,
6410            catches,
6411            finally,
6412        } => {
6413            f(body);
6414            for c in catches {
6415                f(&c.body);
6416            }
6417            if let Some(fl) = finally {
6418                f(fl);
6419            }
6420        }
6421        Stmt::TryWithResources {
6422            resources,
6423            body,
6424            catches,
6425            finally,
6426        } => {
6427            for r in resources {
6428                f(r);
6429            }
6430            f(body);
6431            for c in catches {
6432                f(&c.body);
6433            }
6434            if let Some(fl) = finally {
6435                f(fl);
6436            }
6437        }
6438        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => f(body),
6439        _ => {}
6440    }
6441}
6442
6443/// Vars occurring in this statement's OWN expression payloads (nested
6444/// sub-statements are descended separately so every occurrence carries
6445/// its innermost block id). ForEach/Catch bound vars are declarations,
6446/// not occurrences.
6447fn stmt_shallow_vars<F: FnMut(u32)>(st: &Stmt, out: &mut F) {
6448    fn ex<F: FnMut(u32)>(e: &Expr, out: &mut F) {
6449        visit_exprs(e, &mut |x| {
6450            if let Expr::Local { var, .. } = x {
6451                out(*var);
6452            }
6453        });
6454    }
6455    match st {
6456        Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
6457            ex(e, out)
6458        }
6459        Stmt::TernaryValue { e } => ex(e, out),
6460        Stmt::Return(Some(e)) => ex(e, out),
6461        Stmt::LocalDef { var, init, .. } => {
6462            out(*var);
6463            if let Some(e) = init {
6464                ex(e, out);
6465            }
6466        }
6467        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
6468            ex(cond, out)
6469        }
6470        Stmt::For { cond, update, .. } => {
6471            if let Some(c) = cond {
6472                ex(c, out);
6473            }
6474            for u in update {
6475                ex(u, out);
6476            }
6477        }
6478        Stmt::ForEach { iterable, .. } => ex(iterable, out),
6479        Stmt::Switch { selector, cases, .. } => {
6480            ex(selector, out);
6481            for cg in cases {
6482                if let Some(g) = &cg.guard {
6483                    ex(g, out);
6484                }
6485            }
6486        }
6487        Stmt::Assert { cond, msg } => {
6488            ex(cond, out);
6489            if let Some(m) = msg {
6490                ex(m, out);
6491            }
6492        }
6493        Stmt::Synchronized { lock, .. } => ex(lock, out),
6494        _ => {}
6495    }
6496}
6497
6498/// Pass A: number every Block in pre-order (root = 0) and record each
6499/// var's defining block. `sizes[id]` = the id-range span of the subtree.
6500fn scope_pass_a(st: &Stmt, cur: u32, next: &mut u32, def_block: &mut [u32], sizes: &mut Vec<u32>) {
6501    if let Stmt::LocalDef { var, .. } = st {
6502        let i = *var as usize;
6503        if i < def_block.len() && def_block[i] == u32::MAX {
6504            def_block[i] = cur;
6505        }
6506    }
6507    for_each_child_stmt(st, &mut |c| {
6508        if matches!(c, Stmt::Block(_)) {
6509            let id = *next;
6510            *next += 1;
6511            sizes.push(0);
6512            scope_pass_a(c, id, next, def_block, sizes);
6513            sizes[id as usize] = *next - id;
6514        } else {
6515            scope_pass_a(c, cur, next, def_block, sizes);
6516        }
6517    });
6518}
6519
6520/// Pass B (same numbering walk): count each var's occurrences overall
6521/// and within its defining block's subtree.
6522fn scope_pass_b(
6523    st: &Stmt,
6524    cur: u32,
6525    next: &mut u32,
6526    def_block: &[u32],
6527    sizes: &[u32],
6528    within: &mut [u32],
6529    total: &mut [u32],
6530) {
6531    stmt_shallow_vars(st, &mut |v| {
6532        let i = v as usize;
6533        if i < total.len() {
6534            total[i] += 1;
6535            let d = def_block[i];
6536            if d != u32::MAX && d <= cur && cur < d + sizes[d as usize] {
6537                within[i] += 1;
6538            }
6539        }
6540    });
6541    for_each_child_stmt(st, &mut |c| {
6542        if matches!(c, Stmt::Block(_)) {
6543            let id = *next;
6544            *next += 1;
6545            scope_pass_b(c, id, next, def_block, sizes, within, total);
6546        } else {
6547            scope_pass_b(c, cur, next, def_block, sizes, within, total);
6548        }
6549    });
6550}
6551
6552/// Declaration hygiene: every used var that has no LocalDef gets a bare
6553/// declaration at the top; duplicate LocalDefs demote to assignments.
6554/// In an enum's `<clinit>`, every constant's assignment
6555/// (`Self.FIELD = new Self("NAME", ordinal, ...)`) is compiler-mandated
6556/// boilerplate — when the class renders as a true `enum` declaration the
6557/// constants live in the header and these assignments must go. Only
6558/// ACC_ENUM-flagged static fields of the class itself are touched; the
6559/// `$VALUES` array assignment stays (a static block in an enum is legal).
6560pub fn strip_enum_const_inits(s: &mut Stmt, class: &crate::PoolClass) {
6561    let const_fields: jdc_core::FxHashSet<&str> = class
6562        .static_fields
6563        .iter()
6564        .filter(|f| f.access & crate::access::ACC_ENUM != 0)
6565        .map(|f| f.name.as_str())
6566        .collect();
6567    if const_fields.is_empty() {
6568        return;
6569    }
6570    if let Stmt::Block(stmts) = s {
6571        stmts.retain(|st| {
6572            !matches!(
6573                st,
6574                Stmt::ExprStmt(Expr::Assign { target, value, .. })
6575                    if matches!(&**target,
6576                        Expr::Field { cls, name, is_static: true, .. }
6577                            if cls.as_ref() == class.name
6578                                && const_fields.contains(&**name))
6579                        && matches!(&**value, Expr::New { cls: ncls, .. } if ncls.as_ref() == class.name)
6580            )
6581        });
6582    }
6583}
6584
6585/// Java requires the `super(...)`/`this(...)` delegation to be the FIRST
6586/// statement of a constructor. d8/R8 order the outer-reference capture
6587/// (`this.b = p1;`) or other field writes before the invokesuper in the
6588/// bytecode, which lifts as-is into an uncompilable statement order —
6589/// hoist the bare delegation call to position 0 (jadx does the same).
6590/// Does the expression touch `this` (explicit, implicit-instance member
6591/// access, or super)? Such a def cannot be inlined into a delegation
6592/// that must run BEFORE the supertype constructor.
6593fn contains_this_access(e: &Expr) -> bool {
6594    let mut hit = false;
6595    visit_exprs(e, &mut |x| {
6596        if hit {
6597            return;
6598        }
6599        match x {
6600            Expr::This => hit = true,
6601            Expr::Field { owner, is_static: false, .. } => {
6602                if owner.is_none() || matches!(&**owner.as_ref().unwrap(), Expr::This) {
6603                    hit = true;
6604                }
6605            }
6606            Expr::Method { owner, is_static: false, .. } => {
6607                if owner.is_none() || matches!(&**owner.as_ref().unwrap(), Expr::This) {
6608                    hit = true;
6609                }
6610            }
6611            _ => {}
6612        }
6613    });
6614    hit
6615}
6616
6617/// Inline the transitive straight-line defs of a delegation call's
6618/// arg locals into the call, so hoisting it to position 0 leaves no
6619/// dangling name (lark ProtoAdapter's `super(str)` with `str` defined
6620/// BELOW the hoist point — 找不到符号 变量 str). Returns false when any
6621/// arg var has no inlineable def (param-less, missing, this-touching,
6622/// or cyclic) — the caller then leaves the call in place: a
6623/// not-first-statement error is cheaper than an unresolved name that
6624/// poisons every downstream type.
6625/// Any occurrence of `var` (read OR assign-target) in `stmts` — the
6626/// suffix-use test for dropping a consumed def: count_locals_stmts only
6627/// sees reads, and a suffix WRITE to a dropped declaration makes
6628/// ensure_declared re-add `Type v;` at the method top — BEFORE the
6629/// hoisted this()/super() ("对this的调用必须是构造器中的第一个语句",
6630/// lark +587 the read-only test caused).
6631fn var_occurs_in(stmts: &[Stmt], var: u32) -> bool {
6632    stmts.iter().any(|st| {
6633        let mut c = st.clone();
6634        let mut hit = false;
6635        walk_stmt_exprs(&mut c, &mut |e| {
6636            if !hit {
6637                deep_rewrite(e, &mut |x| {
6638                    if let Expr::Local { var: v, .. } = x {
6639                        if *v == var {
6640                            hit = true;
6641                        }
6642                    }
6643                });
6644            }
6645        });
6646        hit
6647    })
6648}
6649
6650// ---------------------------------------------------------------------------
6651// Branched-delegation ctors → static resolver helper.
6652// ---------------------------------------------------------------------------
6653
6654/// A synthetic static method extracted from a constructor whose
6655/// delegation is preceded by computation (the Kotlin default-arg /
6656/// conditional-bridge shape). Rendered by classdec after the real
6657/// methods; the ctor becomes a single first-statement delegation whose
6658/// carrier arg is the helper call.
6659pub struct CtorHelper {
6660    pub name: String,
6661    pub ret: JavaType,
6662    pub params: Vec<(JavaType, String)>,
6663    pub body: Stmt,
6664    pub vt: VarTable,
6665}
6666
6667fn ctor_helpers() -> &'static std::sync::Mutex<std::collections::HashMap<String, Vec<CtorHelper>>> {
6668    static H: std::sync::OnceLock<std::sync::Mutex<std::collections::HashMap<String, Vec<CtorHelper>>>> =
6669        std::sync::OnceLock::new();
6670    H.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::default()))
6671}
6672
6673/// Deterministic helper name: FNV-1a over (class, ctor descriptor) —
6674/// worker-thread registration order must not leak into the output.
6675fn helper_name(class: &str, desc: &str) -> String {
6676    let mut h: u64 = 0xcbf29ce484222325;
6677    for b in class.as_bytes().iter().chain(b"::").chain(desc.as_bytes()) {
6678        h ^= *b as u64;
6679        h = h.wrapping_mul(0x100000001b3);
6680    }
6681    format!("resolve${:x}", (h >> 16) & 0xffff_ffff)
6682}
6683
6684/// Drain the helpers registered for a class (render-time). Sorted by
6685/// name: registration order is worker-scheduling dependent.
6686pub fn take_ctor_helpers(class: &str) -> Vec<CtorHelper> {
6687    let mut m = ctor_helpers().lock().unwrap_or_else(|e| e.into_inner());
6688    let mut v = m.remove(class).unwrap_or_default();
6689    v.sort_by(|a, b| a.name.cmp(&b.name));
6690    v
6691}
6692
6693fn strip_delegations_deep(stmts: &mut Vec<Stmt>) {
6694    stmts.retain(|s| !is_bare_ctor_call(s));
6695    for s in stmts.iter_mut() {
6696        strip_delegation_one(s);
6697    }
6698}
6699
6700fn strip_delegation_one(st: &mut Stmt) {
6701    if is_bare_ctor_call(st) {
6702        *st = Stmt::Block(Vec::new());
6703        return;
6704    }
6705    match st {
6706        Stmt::Block(v) => strip_delegations_deep(v),
6707        Stmt::If { then_stmt, else_stmt, .. } => {
6708            strip_delegation_one(then_stmt);
6709            if let Some(e) = else_stmt {
6710                strip_delegation_one(e);
6711            }
6712        }
6713        Stmt::While { body, .. }
6714        | Stmt::DoWhile { body, .. }
6715        | Stmt::ForEach { body, .. }
6716        | Stmt::Synchronized { body, .. }
6717        | Stmt::Labeled { body, .. } => strip_delegation_one(body),
6718        Stmt::For { init, body, .. } => {
6719            strip_delegations_deep(init);
6720            strip_delegation_one(body);
6721        }
6722        Stmt::Switch { cases, default, .. } => {
6723            for c in cases.iter_mut() {
6724                strip_delegations_deep(&mut c.body);
6725            }
6726            if let Some(d) = default {
6727                strip_delegation_one(d);
6728            }
6729        }
6730        Stmt::Try { body, catches, finally } | Stmt::TryWithResources { body, catches, finally, .. } => {
6731            strip_delegation_one(body);
6732            for c in catches.iter_mut() {
6733                strip_delegation_one(&mut c.body);
6734            }
6735            if let Some(f) = finally {
6736                strip_delegation_one(f);
6737            }
6738        }
6739        _ => {}
6740    }
6741}
6742
6743#[allow(clippy::ptr_arg)]
6744fn rewrite_bare_returns(stmts: &mut Vec<Stmt>, val: &Expr) {
6745    for s in stmts.iter_mut() {
6746        rewrite_bare_returns_one(s, val);
6747    }
6748}
6749
6750fn rewrite_bare_returns_one(st: &mut Stmt, val: &Expr) {
6751    match st {
6752        Stmt::Return(None) => *st = Stmt::Return(Some(val.clone())),
6753        Stmt::Block(v) => rewrite_bare_returns(v, val),
6754        Stmt::If { then_stmt, else_stmt, .. } => {
6755            rewrite_bare_returns_one(then_stmt, val);
6756            if let Some(e) = else_stmt {
6757                rewrite_bare_returns_one(e, val);
6758            }
6759        }
6760        Stmt::While { body, .. }
6761        | Stmt::DoWhile { body, .. }
6762        | Stmt::ForEach { body, .. }
6763        | Stmt::Synchronized { body, .. }
6764        | Stmt::Labeled { body, .. } => rewrite_bare_returns_one(body, val),
6765        Stmt::For { init, body, .. } => {
6766            rewrite_bare_returns(init, val);
6767            rewrite_bare_returns_one(body, val);
6768        }
6769        Stmt::Switch { cases, default, .. } => {
6770            for c in cases.iter_mut() {
6771                rewrite_bare_returns(&mut c.body, val);
6772            }
6773            if let Some(d) = default {
6774                rewrite_bare_returns_one(d, val);
6775            }
6776        }
6777        Stmt::Try { body, catches, finally } | Stmt::TryWithResources { body, catches, finally, .. } => {
6778            rewrite_bare_returns_one(body, val);
6779            for c in catches.iter_mut() {
6780                rewrite_bare_returns_one(&mut c.body, val);
6781            }
6782            if let Some(f) = finally {
6783                rewrite_bare_returns_one(f, val);
6784            }
6785        }
6786        _ => {}
6787    }
6788}
6789
6790fn collect_def_vars(stmts: &[Stmt], out: &mut jdc_core::FxHashSet<u32>) {
6791    for st in stmts {
6792        match st {
6793            Stmt::LocalDef { var, .. } => {
6794                out.insert(*var);
6795            }
6796            Stmt::ExprStmt(Expr::Assign { target, .. }) => {
6797                if let Expr::Local { var, .. } = &**target {
6798                    out.insert(*var);
6799                }
6800            }
6801            Stmt::Block(v) => collect_def_vars(v, out),
6802            Stmt::If { then_stmt, else_stmt, .. } => {
6803                collect_def_vars(std::slice::from_ref(then_stmt.as_ref()), out);
6804                if let Some(e) = else_stmt {
6805                    collect_def_vars(std::slice::from_ref(e.as_ref()), out);
6806                }
6807            }
6808            Stmt::While { body, .. }
6809            | Stmt::DoWhile { body, .. }
6810            | Stmt::Synchronized { body, .. }
6811            | Stmt::Labeled { body, .. } => {
6812                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
6813            }
6814            Stmt::ForEach { var, body, .. } => {
6815                out.insert(*var);
6816                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
6817            }
6818            Stmt::For { init, body, .. } => {
6819                collect_def_vars(init, out);
6820                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
6821            }
6822            Stmt::Switch { cases, default, .. } => {
6823                for c in cases {
6824                    collect_def_vars(&c.body, out);
6825                }
6826                if let Some(d) = default {
6827                    collect_def_vars(std::slice::from_ref(d.as_ref()), out);
6828                }
6829            }
6830            Stmt::Try { body, catches, finally }
6831            | Stmt::TryWithResources { body, catches, finally, .. } => {
6832                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
6833                for c in catches {
6834                    out.insert(c.var);
6835                    collect_def_vars(std::slice::from_ref(c.body.as_ref()), out);
6836                }
6837                if let Some(f) = finally {
6838                    collect_def_vars(std::slice::from_ref(f.as_ref()), out);
6839                }
6840            }
6841            _ => {}
6842        }
6843    }
6844}
6845
6846/// v1 extraction is limited to Block/If/straight-line computation (the
6847/// Kotlin default-arg shape); loops/switch/try in the extraction bail.
6848fn extraction_shape_ok(stmts: &[Stmt]) -> bool {
6849    stmts.iter().all(|st| match st {
6850        Stmt::Block(v) => extraction_shape_ok(v),
6851        Stmt::LocalDef { .. }
6852        | Stmt::ExprStmt(_)
6853        | Stmt::Return(_)
6854        | Stmt::Throw(_)
6855        | Stmt::Assert { .. }
6856        | Stmt::Break(_)
6857        | Stmt::Continue(_) => true,
6858        Stmt::If { then_stmt, else_stmt, .. } => {
6859            ok_one(then_stmt)
6860                && else_stmt.as_deref().map(ok_one).unwrap_or(true)
6861        }
6862        Stmt::While { body, .. }
6863        | Stmt::DoWhile { body, .. }
6864        | Stmt::ForEach { body, .. }
6865        | Stmt::Synchronized { body, .. } => ok_one(body),
6866        Stmt::Labeled { body, .. } => ok_one(body),
6867        Stmt::For { init, body, .. } => extraction_shape_ok(init) && ok_one(body),
6868        Stmt::Switch { cases, default, .. } => {
6869            cases.iter().all(|c| extraction_shape_ok(&c.body))
6870                && default.as_deref().map(ok_one).unwrap_or(true)
6871        }
6872        Stmt::Try { body, catches, finally }
6873        | Stmt::TryWithResources { body, catches, finally, .. } => {
6874            ok_one(body)
6875                && catches.iter().all(|c| ok_one(&c.body))
6876                && finally.as_deref().map(ok_one).unwrap_or(true)
6877        }
6878        // Label/Goto (unstructured), MonitorEnter/Exit (raw), ClassDecl
6879        // (local class), TernaryValue, Raw: stay out of v2 extraction.
6880        _ => false,
6881    })
6882}
6883
6884fn ok_one(st: &Stmt) -> bool {
6885    extraction_shape_ok(std::slice::from_ref(st))
6886}
6887
6888fn static_safe(stmts: &[Stmt]) -> bool {
6889    let mut ok = true;
6890    for st in stmts {
6891        let mut c = st.clone();
6892        walk_stmt_exprs(&mut c, &mut |e| {
6893            deep_rewrite(e, &mut |x| {
6894                match x {
6895                    Expr::This => ok = false,
6896                    // implicit-this member access / call
6897                    Expr::Field { owner: None, is_static: false, .. } => ok = false,
6898                    Expr::Method { owner: None, is_static: false, .. } => ok = false,
6899                    _ => {}
6900                }
6901            });
6902        });
6903    }
6904    ok
6905}
6906
6907/// The branched-delegation ctor family ("对this的调用必须是构造器中的
6908/// 第一个语句", ~2k across corpora): R8 renders a Kotlin default-arg /
6909/// conditional-bridge ctor as computation statements (straight-line plus
6910/// mask-guarded overrides) with every control path ending in the SAME
6911/// `this(a, b, carrier)` delegation. Java cannot express statements
6912/// before the delegation, and unlike the linear-chain case the carrier's
6913/// value is computed by multi-statement branched code that cannot fold
6914/// into a ternary. Extract the computation into a synthetic
6915/// `private static T resolve$X(<ctor params>)` and make the ctor a
6916/// single first-statement `this(a, b, resolve$X(..))` — compilable AND
6917/// faithful (jadx renders the un-compilable statements-before-this form
6918/// for this family).
6919///
6920/// v1 gates (all conservative, bail keeps today's behavior): plain class
6921/// (enums strip trace params; member-inner ctors lose the outer param at
6922/// render), every delegation textually identical, exactly one non-param
6923/// carrier local read exactly once across the args, the primary (last
6924/// top-level) delegation exists and is not already first, computation is
6925/// Block/If/straight-line only, no `this`-dependence, and no local
6926/// defined in the extraction is used after the primary.
6927pub fn extract_branched_delegation_helper(
6928    body: &mut Stmt,
6929    vt: &VarTable,
6930    class: &crate::PoolClass,
6931    desc_str: &str,
6932) {
6933    if class.is_enum() || class.nesting.enclosing_class.is_some() {
6934        return;
6935    }
6936    let Stmt::Block(stmts) = body else { return };
6937    let mut dels: Vec<Expr> = Vec::new();
6938    for st in stmts.iter() {
6939        let mut c = st.clone();
6940        walk_stmt_exprs(&mut c, &mut |e| {
6941            if is_delegation_expr(e) {
6942                dels.push(e.clone());
6943            }
6944        });
6945    }
6946    if dels.is_empty() {
6947        return;
6948    }
6949    if dels.len() == 1 && stmts.first().is_some_and(is_bare_ctor_call) {
6950        return; // already first
6951    }
6952    if !dels.iter().all(|d| *d == dels[0]) {
6953        return;
6954    }
6955    let template = dels[0].clone();
6956    let Expr::Method { args: t_args, .. } = &template else {
6957        return;
6958    };
6959    // Exactly ONE non-param local (the carrier), read exactly once.
6960    let mut carrier: Option<u32> = None;
6961    let mut reads = 0usize;
6962    let mut multi = false;
6963    for a in t_args.iter() {
6964        let c = a.clone();
6965        visit_exprs(&c, &mut |x| {
6966            if let Expr::Local { var, .. } = x {
6967                if (*var as usize) < vt.vars.len() && !vt.vars[*var as usize].is_param {
6968                    reads += 1;
6969                    match carrier {
6970                        Some(v) if v == *var => {}
6971                        Some(_) => multi = true,
6972                        None => carrier = Some(*var),
6973                    }
6974                }
6975            }
6976        });
6977    }
6978    let Some(cv) = carrier else { return };
6979    if multi || reads != 1 {
6980        return;
6981    }
6982    let Some(l) = stmts.iter().rposition(is_bare_ctor_call) else {
6983        return;
6984    };
6985    if l == 0 {
6986        return;
6987    }
6988    let mut extracted: Vec<Stmt> = stmts[..l].to_vec();
6989    if !extraction_shape_ok(&extracted) {
6990        return;
6991    }
6992    strip_delegations_deep(&mut extracted);
6993    let cv_local = Expr::Local {
6994        var: cv,
6995        ty: vt.var(cv).ty.clone(),
6996    };
6997    rewrite_bare_returns(&mut extracted, &cv_local);
6998    extracted.push(Stmt::Return(Some(cv_local)));
6999    if !static_safe(&extracted) {
7000        return;
7001    }
7002    let mut defs: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
7003    collect_def_vars(&extracted, &mut defs);
7004    if defs.iter().any(|v| var_occurs_in(&stmts[l + 1..], *v)) {
7005        return;
7006    }
7007    let params: Vec<(u32, JavaType, String)> = vt
7008        .vars
7009        .iter()
7010        .filter(|v| v.is_param && v.name != "this")
7011        .map(|v| (v.id, v.ty.erased(), v.name.clone()))
7012        .collect();
7013    let ret = vt.var(cv).ty.erased();
7014    let name = helper_name(&class.name, desc_str);
7015    let call = Expr::Method {
7016        owner: None,
7017        cls: std::sync::Arc::from(class.name.as_str()),
7018        name: std::sync::Arc::from(name.as_str()),
7019        desc: std::sync::Arc::new(jdc_core::types::MethodDescriptor {
7020            ret: ret.clone(),
7021            args: params.iter().map(|p| p.1.clone()).collect(),
7022        }),
7023        args: params
7024            .iter()
7025            .map(|(id, ty, _)| Expr::Local {
7026                var: *id,
7027                ty: TypeRef::J(ty.clone()),
7028            })
7029            .collect(),
7030        is_static: true,
7031        is_interface: false,
7032        is_special: false,
7033        is_super: false,
7034        is_dynamic: false,
7035        type_args: Vec::new(),
7036    };
7037    let mut new_del = template.clone();
7038    deep_rewrite(&mut new_del, &mut |x| {
7039        if let Expr::Local { var, .. } = x {
7040            if *var == cv {
7041                *x = call.clone();
7042            }
7043        }
7044    });
7045    {
7046        let mut m = ctor_helpers().lock().unwrap_or_else(|e| e.into_inner());
7047        let v = m.entry(class.name.clone()).or_default();
7048        if !v.iter().any(|h| h.name == name) {
7049            // The extracted body skips the late pipeline polish (it left
7050            // the ctor before cleanup ran) — invert empty thens so the
7051            // helper doesn't render `if (..) {} else {..}`.
7052            let mut hbody = Stmt::Block(extracted);
7053            invert_empty_thens(&mut hbody);
7054            v.push(CtorHelper {
7055                name,
7056                ret,
7057                params: params.into_iter().map(|(_, ty, nm)| (ty, nm)).collect(),
7058                body: hbody,
7059                vt: vt.clone(),
7060            });
7061        } else {
7062            return; // duplicate decompile of the same ctor: keep the first
7063        }
7064    }
7065    let tail: Vec<Stmt> = stmts[l + 1..].to_vec();
7066    *stmts = std::iter::once(Stmt::ExprStmt(new_del)).chain(tail).collect();
7067}
7068
7069fn inline_ctor_arg_defs(
7070    call: &mut Stmt,
7071    prefix: &[&Stmt],
7072    vt: &VarTable,
7073    consumed: &mut Vec<(u32, usize)>,
7074) -> bool {
7075    let Stmt::ExprStmt(cexpr) = call else {
7076        return false;
7077    };
7078    // Replace every read of `v` in `e` with its value-so-far; a read
7079    // BEFORE any def (val None) is unresolvable → false.
7080    fn subst(e: &mut Expr, v: u32, val: &Option<Expr>) -> bool {
7081        let mut ok = true;
7082        deep_rewrite(e, &mut |x| {
7083            if let Expr::Local { var, .. } = x {
7084                if *var == v {
7085                    match val {
7086                        Some(e2) => *x = e2.clone(),
7087                        None => ok = false,
7088                    }
7089                }
7090            }
7091        });
7092        ok
7093    }
7094    let mut seen: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
7095    for _ in 0..32 {
7096        let mut need: Option<u32> = None;
7097        visit_exprs(cexpr, &mut |x| {
7098            if need.is_none() {
7099                if let Expr::Local { var, .. } = x {
7100                    if (*var as usize) < vt.vars.len() && !vt.vars[*var as usize].is_param {
7101                        need = Some(*var);
7102                    }
7103                }
7104            }
7105        });
7106        let Some(v) = need else { break };
7107        if !seen.insert(v) {
7108            return false; // cyclic reuse (`str = .. + str`) — bail
7109        }
7110        // CHAIN walk: fold EVERY prefix def of v into the value the
7111        // delegation actually receives. The old strict single-occurrence
7112        // rule rejected the convenience-ctor reassignment chain
7113        // (`f = getInstance(ctx); f = transform(f, ..); this(ctx, f)` —
7114        // androidx CameraController, weibo this-not-first ×831): the
7115        // SOURCE inlined the computation into the delegation args and
7116        // R8 decomposed it into register writes. Reads of v are allowed
7117        // only INSIDE its own def chain; any other prefix statement
7118        // touching v means the value escapes or the object mutates
7119        // (`stringBuilder.append(..)` — inlining a fresh `new
7120        // StringBuilder()` would lose the message), so bail.
7121        let mut val: Option<Expr> = None;
7122        let mut ok = true;
7123        for (si, st) in prefix.iter().enumerate() {
7124            match st {
7125                // A bare declaration (init None — the structurer hoists
7126                // `T v;` to the top) carries no value: skip it. Failing
7127                // here rejected every chain under a hoisted decl (ci1/a).
7128                Stmt::LocalDef { var, init: None, .. } if *var == v => {}
7129                Stmt::LocalDef { var, init, .. } if *var == v => {
7130                    let Some(e) = init else { ok = false; break };
7131                    let mut e2 = (*e).clone();
7132                    if !subst(&mut e2, v, &val) {
7133                        ok = false;
7134                        break;
7135                    }
7136                    val = Some(e2);
7137                    consumed.push((v, si));
7138                }
7139                Stmt::ExprStmt(Expr::Assign { target, op, .. })
7140                    if matches!(&**target, Expr::Local { var: tv, .. } if *tv == v) =>
7141                {
7142                    if !matches!(op, AssignOp::Plain) {
7143                        ok = false; // compound write: not a plain def
7144                        break;
7145                    }
7146                    let Stmt::ExprStmt(Expr::Assign { value, .. }) = st else {
7147                        unreachable!()
7148                    };
7149                    let mut e2 = value.as_ref().clone();
7150                    if !subst(&mut e2, v, &val) {
7151                        ok = false;
7152                        break;
7153                    }
7154                    val = Some(e2);
7155                    consumed.push((v, si));
7156                }
7157                _ => {
7158                    let mut touched = false;
7159                    visit_all_exprs(st, &mut |x| {
7160                        if let Expr::Local { var, .. } = x {
7161                            if *var == v {
7162                                touched = true;
7163                            }
7164                        }
7165                    });
7166                    if touched {
7167                        ok = false;
7168                        break;
7169                    }
7170                }
7171            }
7172        }
7173        if !ok {
7174            return false;
7175        }
7176        let Some(de) = val else { return false };
7177        if contains_this_access(&de) {
7178            return false;
7179        }
7180        // Huge defs multiply per read site per round — bound them.
7181        let mut de_nodes = 0usize;
7182        visit_exprs(&de, &mut |_| de_nodes += 1);
7183        if de_nodes > 256 {
7184            return false;
7185        }
7186        deep_rewrite(cexpr, &mut |x| {
7187            if let Expr::Local { var, .. } = x {
7188                if *var == v {
7189                    *x = de.clone();
7190                }
7191            }
7192        });
7193        // Exponential blowup guard: a var read twice doubles its def
7194        // per round, compounding across the transitive chain — cap the
7195        // expression size (a 2^k node tree overflowed the worker stack
7196        // process-wide).
7197        let mut nodes = 0usize;
7198        visit_exprs(cexpr, &mut |_| nodes += 1);
7199        if nodes > 2_048 {
7200            return false;
7201        }
7202    }
7203    true
7204}
7205
7206/// Fold a prelude StringBuilder mutation chain into the delegation
7207/// argument as a fluent chain: `StringBuilder sb = new SB(x);
7208/// sb.append(a); sb.append(b); super(sb.toString())` →
7209/// `super(new SB(x).append(a).append(b).toString())`. The generic def
7210/// inliner rejects mutation calls — their effect is not expressible as
7211/// a value — but `append` RETURNS its receiver, so the chain
7212/// reconstructs exactly the same mutations in one expression (androidx
7213/// emoji2/flatbuffer UnpairedSurrogateException family, the shapes the
7214/// strict-occurrence rule was written to refuse). Aborts on any `this`
7215/// touch inside the chain (illegal before super) or any other read of
7216/// the SB local (the value must not escape the fold). Returns the
7217/// delegation's new index (statements before it were removed).
7218fn fold_sb_chain(stmts: &mut Vec<Stmt>, pos: usize) -> usize {
7219    // 1. the SB local: first `v = new StringBuilder(..)` in the prefix.
7220    let mut sbv: Option<u32> = None;
7221    let mut def_i: Option<usize> = None;
7222    let mut def_init: Option<Expr> = None;
7223    for (i, st) in stmts[..pos].iter().enumerate() {
7224        let cand: Option<(u32, &Expr)> = match st {
7225            Stmt::LocalDef {
7226                var,
7227                init: Some(e),
7228                ..
7229            } => Some((*var, e)),
7230            Stmt::ExprStmt(Expr::Assign {
7231                target,
7232                value,
7233                op: AssignOp::Plain,
7234                ..
7235            }) => match &**target {
7236                Expr::Local { var, .. } => Some((*var, value.as_ref())),
7237                _ => None,
7238            },
7239            _ => None,
7240        };
7241        if let Some((v, init)) = cand {
7242            if let Expr::New { cls, .. } = init {
7243                if cls.as_ref() == "java/lang/StringBuilder" {
7244                    sbv = Some(v);
7245                    def_i = Some(i);
7246                    def_init = Some(init.clone());
7247                    break;
7248                }
7249            }
7250        }
7251    }
7252    let (Some(sbv), Some(def_i), Some(init)) = (sbv, def_i, def_init) else {
7253        return pos;
7254    };
7255    if contains_this_access(&init) {
7256        return pos;
7257    }
7258    // 2. append statements on sbv, in order.
7259    let mut appends: Vec<usize> = Vec::new();
7260    for (i, st) in stmts[..pos].iter().enumerate() {
7261        if let Stmt::ExprStmt(Expr::Method {
7262            owner: Some(o),
7263            cls,
7264            name,
7265            ..
7266        }) = st
7267        {
7268            if matches!(&**o, Expr::Local { var, .. } if *var == sbv)
7269                && cls.as_ref() == "java/lang/StringBuilder"
7270                && &**name == "append"
7271            {
7272                appends.push(i);
7273            }
7274        }
7275    }
7276    // 3. no read of sbv after the delegation.
7277    let mut post_read = false;
7278    for st in stmts.iter().skip(pos + 1) {
7279        visit_all_exprs(st, &mut |e| {
7280            if let Expr::Local { var, .. } = e {
7281                if *var == sbv {
7282                    post_read = true;
7283                }
7284            }
7285        });
7286    }
7287    if post_read {
7288        return pos;
7289    }
7290    // 4. prefix reads of sbv only in the def and the append receivers
7291    //    (arguments are checked for `this`, receivers are the chain).
7292    for (i, st) in stmts[..pos].iter().enumerate() {
7293        if i == def_i || appends.contains(&i) {
7294            // append ARGUMENTS must not touch this; receiver is fine.
7295            if let Stmt::ExprStmt(Expr::Method { args, .. }) = st {
7296                if args.iter().any(contains_this_access) {
7297                    return pos;
7298                }
7299            }
7300            continue;
7301        }
7302        let mut leak = false;
7303        visit_all_exprs(st, &mut |e| {
7304            if let Expr::Local { var, .. } = e {
7305                if *var == sbv {
7306                    leak = true;
7307                }
7308            }
7309        });
7310        if leak {
7311            return pos;
7312        }
7313    }
7314    // 5. the call reads sbv exactly once, as `sbv.toString()`.
7315    let mut local_reads = 0usize;
7316    let mut tostring_reads = 0usize;
7317    visit_all_exprs(&stmts[pos], &mut |e| match e {
7318        Expr::Local { var, .. } if *var == sbv => local_reads += 1,
7319        Expr::Method {
7320            owner: Some(o),
7321            name,
7322            args,
7323            ..
7324        } if args.is_empty()
7325            && &**name == "toString"
7326            && matches!(&**o, Expr::Local { var, .. } if *var == sbv) =>
7327        {
7328            tostring_reads += 1;
7329        }
7330        _ => {}
7331    });
7332    if local_reads != 1 || tostring_reads != 1 {
7333        return pos;
7334    }
7335    // 6. build the fluent chain and swap it into the toString receiver.
7336    let mut chain = init;
7337    for ai in &appends {
7338        if let Stmt::ExprStmt(Expr::Method {
7339            owner: _,
7340            cls,
7341            name,
7342            desc,
7343            args,
7344            is_static,
7345            is_interface,
7346            is_special,
7347            is_super,
7348            is_dynamic,
7349            type_args,
7350            ..
7351        }) = &stmts[*ai]
7352        {
7353            chain = Expr::Method {
7354                owner: Some(Box::new(chain)),
7355                cls: cls.clone(),
7356                name: name.clone(),
7357                desc: desc.clone(),
7358                args: args.clone(),
7359                is_static: *is_static,
7360                is_interface: *is_interface,
7361                is_special: *is_special,
7362                is_super: *is_super,
7363                is_dynamic: *is_dynamic,
7364                type_args: type_args.clone(),
7365            };
7366        }
7367    }
7368    let mut replaced = false;
7369    walk_stmt_exprs(&mut stmts[pos], &mut |e| {
7370        deep_rewrite(e, &mut |x| {
7371            if let Expr::Method {
7372                owner: Some(o),
7373                name,
7374                args,
7375                ..
7376            } = x
7377            {
7378                if args.is_empty()
7379                    && &**name == "toString"
7380                    && matches!(&**o, Expr::Local { var, .. } if *var == sbv)
7381                {
7382                    **o = chain.clone();
7383                    replaced = true;
7384                }
7385            }
7386        });
7387    });
7388    if !replaced {
7389        return pos; // never remove without the swap — atomicity
7390    }
7391    // 7. drop the consumed statements (descending), adjust pos.
7392    let mut removed: Vec<usize> = appends.clone();
7393    removed.push(def_i);
7394    removed.sort_unstable();
7395    removed.dedup();
7396    for i in removed.iter().rev() {
7397        stmts.remove(*i);
7398    }
7399    pos - removed.len()
7400}
7401
7402pub fn fix_ctor_super_first(body: &mut Stmt, vt: &VarTable) {
7403    let Stmt::Block(stmts) = body else { return };
7404    if stmts.is_empty() || is_bare_ctor_call(stmts.first().unwrap()) {
7405        return;
7406    }
7407    // Top-level delegation call.
7408    if let Some(pos0) = stmts.iter().position(is_bare_ctor_call) {
7409        let pos = fold_sb_chain(stmts, pos0);
7410        let mut call = stmts[pos].clone();
7411        let prefix: Vec<&Stmt> = stmts[..pos].iter().collect();
7412        let mut consumed: Vec<(u32, usize)> = Vec::new();
7413        if !inline_ctor_arg_defs(&mut call, &prefix, vt, &mut consumed) {
7414            return;
7415        }
7416        stmts.remove(pos);
7417        // Drop the chain defs the inline consumed (all indices < pos) —
7418        // leaving them would DOUBLE-evaluate their (possibly impure)
7419        // inits. But ONLY for vars with no post-delegation use: dropping
7420        // such a var's DECLARATION makes ensure_declared re-add it at
7421        // the method top, i.e. BEFORE this() — a fresh "对this的调用必须
7422        // 是构造器中的第一个语句" (lark +587 the blind drop caused).
7423        let mut drop_idx: Vec<usize> = consumed
7424            .iter()
7425            .filter(|(v, _)| !var_occurs_in(&stmts[pos..], *v))
7426            .map(|(_, k)| *k)
7427            .collect();
7428        drop_idx.sort_unstable();
7429        drop_idx.dedup();
7430        for k in drop_idx.iter().rev() {
7431            stmts.remove(*k);
7432        }
7433        stmts.insert(0, call);
7434        return;
7435    }
7436    // The structurer can wrap the delegation in a bare nested block —
7437    // with the parameter null-checks AHEAD of it (weixin's Kotlin
7438    // intrinsics shape: `o.h(parcel, "source"); super();` at inner
7439    // positions 0/1, flattened to straight-line by cleanup AFTER this
7440    // pass, which is why the miss surfaced as 1150 weixin "对super的
7441    // 调用必须是构造器中的第一个语句"). Hoist the call from any
7442    // position whose predecessors are all straight-line statements; a
7443    // delegation behind control flow is the conditional-super family
7444    // (needs restructuring, not hoisting) and stays put.
7445    let mut found: Option<(usize, usize)> = None;
7446    for (i, st) in stmts.iter().enumerate() {
7447        if let Stmt::Block(inner) = st {
7448            if let Some(p) = inner.iter().position(is_bare_ctor_call) {
7449                if inner[..p]
7450                    .iter()
7451                    .all(|s| matches!(s, Stmt::LocalDef { .. } | Stmt::ExprStmt(_)))
7452                {
7453                    found = Some((i, p));
7454                    break;
7455                }
7456            }
7457        }
7458    }
7459    if let Some((i, p)) = found {
7460        let mut call = match &stmts[i] {
7461            Stmt::Block(inner) => inner[p].clone(),
7462            _ => unreachable!(),
7463        };
7464        let mut prefix: Vec<&Stmt> = stmts[..i].iter().collect();
7465        if let Stmt::Block(inner) = &stmts[i] {
7466            prefix.extend(inner[..p].iter());
7467        }
7468        let mut consumed: Vec<(u32, usize)> = Vec::new();
7469        if !inline_ctor_arg_defs(&mut call, &prefix, vt, &mut consumed) {
7470            return;
7471        }
7472        // Prefix index k maps to stmts[k] for k < i, inner[k - i] for
7473        // k >= i (all such k are < p). Suffix = the rest of the inner
7474        // block + the following top-level statements; a consumed def may
7475        // only drop when its var has no suffix use (declaration loss →
7476        // ensure_declared re-adds it BEFORE this()).
7477        // Compute the droppable set BEFORE mutating (borrow split).
7478        let mut ks: Vec<usize> = Vec::new();
7479        {
7480            let inner_tail: &[Stmt] = match &stmts[i] {
7481                Stmt::Block(inner) if p < inner.len() => &inner[p + 1..],
7482                _ => &[],
7483            };
7484            for (v, k) in &consumed {
7485                if !var_occurs_in(inner_tail, *v) && !var_occurs_in(&stmts[i + 1..], *v) {
7486                    ks.push(*k);
7487                }
7488            }
7489        }
7490        ks.sort_unstable();
7491        ks.dedup();
7492        if let Stmt::Block(inner) = &mut stmts[i] {
7493            inner.remove(p);
7494            for k in ks.iter().rev().filter(|k| **k >= i) {
7495                inner.remove(k - i);
7496            }
7497        }
7498        for k in ks.iter().rev().filter(|k| **k < i) {
7499            stmts.remove(*k);
7500        }
7501        stmts.insert(0, call);
7502    }
7503}
7504
7505/// A true constructor DELEGATION statement: `super(..)` (owner None) or
7506/// `this(..)` (owner This). An un-folded `new X; <init>` (lost
7507/// allocation — the ctor-fold-bug family) arrives as a `<init>` Method
7508/// with a NON-this owner and prints as `new X(..)`: it is not a
7509/// delegation and must neither satisfy nor anchor the super-first fixes
7510/// (v7.a$a2: a dead `new f(0,..)` init at position 0 made both hoists
7511/// believe the delegation was already first, leaving the real `super`
7512/// last — "对super的调用必须是构造器中的第一个语句").
7513fn is_delegation_expr(e: &Expr) -> bool {
7514    matches!(
7515        e,
7516        Expr::Method { name, is_special: true, owner, .. }
7517            if &**name == "<init>"
7518                && (owner.is_none() || matches!(owner.as_deref(), Some(Expr::This)))
7519    )
7520}
7521
7522pub(crate) fn is_bare_ctor_call(s: &Stmt) -> bool {
7523    matches!(s, Stmt::ExprStmt(e) if is_delegation_expr(e))
7524}
7525
7526/// Is the local ever ASSIGNED (write position: `x = ..`, `++x`)?
7527/// Distinct from a read count: a ctor's synthetic outer param is only
7528/// ever read; a param that gets written must stay declared.
7529pub(crate) fn local_is_written(body: &Stmt, var: u32) -> bool {
7530    let mut hit = false;
7531    let mut c = body.clone();
7532    walk_stmt_exprs(&mut c, &mut |e| {
7533        if !hit {
7534            deep_rewrite(e, &mut |x| {
7535                if hit {
7536                    return;
7537                }
7538                match x {
7539                    Expr::Assign { target, .. }
7540                        if matches!(&**target, Expr::Local { var: v, .. } if *v == var) =>
7541                    {
7542                        hit = true;
7543                    }
7544                    Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. }
7545                        if matches!(&**e, Expr::Local { var: v, .. } if *v == var) =>
7546                    {
7547                        hit = true;
7548                    }
7549                    _ => {}
7550                }
7551            });
7552        }
7553    });
7554    hit
7555}
7556
7557/// Source-form normalization for a non-static member-inner constructor.
7558/// The dex descriptor carries the synthetic outer instance as args[0]
7559/// (typed as the direct enclosing class); the emitter's qualified
7560/// `outer.new Inner(..)` / `this.new Inner(..)` sites pass it
7561/// implicitly and `super(..)` delegations drop it, so the signature
7562/// loses the param — and the body must stop referencing it: plain uses
7563/// become `Outer.this` (a `this.this$0 = p` assignment becomes
7564/// `this.this$0 = Outer.this`, exactly the runtime value), and
7565/// this()-delegations to `eligible` classes drop the leading param arg.
7566pub(crate) fn rewrite_inner_ctor_outer_param(
7567    body: &mut Stmt,
7568    param0: u32,
7569    outer_display: &str,
7570    outer_ty: &TypeRef,
7571    eligible: &[String],
7572) {
7573    // this()/super() delegations (and lost-alloc construction calls):
7574    // the leading arg IS the synthetic outer exactly when it is the
7575    // param itself.
7576    walk_stmt_exprs(body, &mut |e| {
7577        if let Expr::Method { name, cls, args, .. } = e {
7578            if &**name == "<init>"
7579                && eligible.iter().any(|c| c.as_str() == cls.as_ref())
7580                && args.first().is_some_and(|a| matches!(a, Expr::Local { var: v, .. } if *v == param0))
7581            {
7582                args.remove(0);
7583            }
7584        }
7585    });
7586    // Every remaining read of the param becomes `Outer.this`.
7587    let text = format!("{outer_display}.this");
7588    let ty = outer_ty.clone();
7589    walk_stmt_exprs(body, &mut |e| {
7590        deep_rewrite_reads(e, &mut |x| {
7591            if let Expr::Local { var: v, .. } = x {
7592                if *v == param0 {
7593                    *x = Expr::RawT(text.clone(), ty.clone());
7594                }
7595            }
7596        });
7597    });
7598}
7599
7600/// Ctors whose delegation is buried in control flow or behind arg
7601/// computations — the "对super的调用必须是构造器中的第一个语句" family
7602/// that plain hoisting cannot reach (weixin 264 / weibo 116 / reqable 71
7603/// / lark 62 after round 60): R8/Kotlin compute super-args conditionally
7604/// and dex legally invokes super mid-branch.
7605///
7606/// Shape B (branch merge): `prelude; if (c) {d1; super(A1); t1} else
7607/// {d2; super(A2); t2}; post` → one leading `super(..)` where each
7608/// differing arg becomes `c ? a1i : a2i`; branch remainders stay an
7609/// if/else AFTER the call; prelude statements move after it (nothing
7610/// may precede super in Java; bytecode pre-super work is local-only by
7611/// construction — dex forbids instance-field reads before the delegate).
7612///
7613/// Shape A (linear): `defs; super(A); rest` where `A` references the
7614/// def locals — plain hoisting (fix_ctor_super_first case 1) would move
7615/// the call above its argument definitions (forward refs). Inline the
7616/// referenced defs into the args first, drop the consumed defs, hoist.
7617/// Runs BEFORE fix_ctor_super_first and only fires when args reference
7618/// preceding locals, leaving the battle-tested plain hoist untouched
7619/// otherwise.
7620///
7621/// Inlining duplicates the def per use site, so a side-effecting def is
7622/// inlined only when its var's use count is provably preserved: a
7623/// single use in a merged arg (1 copy), or a single use in the
7624/// condition with ≤1 differing arg (the cond then appears in the `if`
7625/// plus one ternary — 2 copies of e.g. one Kotlin getter call, which
7626/// the original bytecode typically made twice anyway: `p.u() == null ?
7627/// .. : p.u().i()`). Pure defs inline freely. Every other shape aborts:
7628/// broken-but-faithful stays the status quo, wrong evaluation counts
7629/// would be worse.
7630/// Linear `def; delegation(args read def)` shapes. A plain hoist of the
7631/// delegation past its prefix would place the call ABOVE a definition
7632/// it reads — a forward reference (`super(context2, ..)` with `context2
7633/// = ..` below it). javac's attribution for the whole class then
7634/// collapses: the undefined identifier cascades into "non-static
7635/// super" and even bare `Object` resolution failures, poisoning every
7636/// later diagnostic in the file (weibo AppCompatTextView — the root of
7637/// a 4.1k-file Object cascade). The original source had the def's
7638/// expression INLINE in the delegation args; d8 computed it into a
7639/// register first. Inline the def into the args (the def is read
7640/// exactly once there — evaluation count preserved even for calls),
7641/// and when the register is REASSIGNED later (a fresh generation
7642/// sharing the slot), convert that first write into the declaration so
7643/// the var stays defined for its later readers. Any shape that cannot
7644/// be proven aborts untouched.
7645/// Count WRITE positions of `var` (assign / inc-dec targets) — the
7646/// shape-C validator needs exact counts, not just presence.
7647fn stmts_count_writes(stmts: &[Stmt], var: u32) -> usize {
7648    let mut n = 0usize;
7649    for s in stmts {
7650        let mut c = s.clone();
7651        walk_stmt_exprs(&mut c, &mut |e| {
7652            deep_rewrite(e, &mut |x| match x {
7653                Expr::Assign { target, .. }
7654                    if matches!(&**target, Expr::Local { var: v, .. } if *v == var) =>
7655                {
7656                    n += 1;
7657                }
7658                Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. }
7659                    if matches!(&**e, Expr::Local { var: v, .. } if *v == var) =>
7660                {
7661                    n += 1;
7662                }
7663                _ => {}
7664            });
7665        });
7666    }
7667    n
7668}
7669
7670/// Collect every local WRITTEN in `stmts` — the complement inside a
7671/// ctor prefix is the param set (the only locals a pre-delegation arg
7672/// expression may read).
7673fn prefix_written_vars(stmts: &[Stmt]) -> jdc_core::FxHashSet<u32> {
7674    let mut set = jdc_core::FxHashSet::default();
7675    for s in stmts {
7676        let mut c = s.clone();
7677        walk_stmt_exprs(&mut c, &mut |e| {
7678            deep_rewrite(e, &mut |x| match x {
7679                Expr::Assign { target, .. } => {
7680                    if let Expr::Local { var, .. } = &**target {
7681                        set.insert(*var);
7682                    }
7683                }
7684                Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => {
7685                    if let Expr::Local { var, .. } = &**e {
7686                        set.insert(*var);
7687                    }
7688                }
7689                _ => {}
7690            });
7691        });
7692        if let Stmt::LocalDef { var, .. } = s {
7693            set.insert(*var);
7694        }
7695    }
7696    set
7697}
7698
7699/// Is `e` safe to evaluate INSIDE a ctor-delegation argument (before
7700/// this/super exists)? Params/consts/pure ops only — no instance-field
7701/// reads, no `this`, no captures.
7702fn pre_this_safe(e: &Expr, prefix_vars: &jdc_core::FxHashSet<u32>) -> bool {
7703    match e {
7704        Expr::Local { var, .. } => !prefix_vars.contains(var),
7705        Expr::Const(_) => true,
7706        Expr::Cast { e, .. } => pre_this_safe(e, prefix_vars),
7707        Expr::Bin { l, r, .. } => {
7708            pre_this_safe(l, prefix_vars) && pre_this_safe(r, prefix_vars)
7709        }
7710        Expr::Cond { c, t, f } => {
7711            pre_this_safe(c, prefix_vars)
7712                && pre_this_safe(t, prefix_vars)
7713                && pre_this_safe(f, prefix_vars)
7714        }
7715        Expr::Field { owner, is_static, .. } => {
7716            *is_static
7717                && owner
7718                    .as_ref()
7719                    .is_none_or(|o| pre_this_safe(o, prefix_vars))
7720        }
7721        Expr::Method {
7722            owner,
7723            args,
7724            is_static,
7725            ..
7726        } => {
7727            args.iter().all(|a| pre_this_safe(a, prefix_vars))
7728                && match owner {
7729                    Some(o) => pre_this_safe(o, prefix_vars),
7730                    None => *is_static,
7731                }
7732        }
7733        // Allocation in a delegation arg is legal Java before this()
7734        // (the ctor call touches no instance state of THIS class) — the
7735        // missing arm sent every `new Date()`-style Kotlin default
7736        // through `_ => false`, rejecting the whole fold (lark
7737        // Request$AppToApp this-not-first family, ~650 lark lines).
7738        // An inner-class construction needing an outer `this` carries
7739        // Expr::This in its args, which still fails below.
7740        Expr::New { args, .. } => args.iter().all(|a| pre_this_safe(a, prefix_vars)),
7741        Expr::NewArray { dims, init, .. } => {
7742            dims.iter().all(|d| pre_this_safe(d, prefix_vars))
7743                && init
7744                    .as_ref()
7745                    .map(|l| l.iter().all(|x| pre_this_safe(x, prefix_vars)))
7746                    .unwrap_or(true)
7747        }
7748        // Pure unary/instanceof/array reads.
7749        Expr::Un { e, .. } | Expr::InstanceOf { e, .. } => pre_this_safe(e, prefix_vars),
7750        Expr::ArrayIndex { array, index } => {
7751            pre_this_safe(array, prefix_vars) && pre_this_safe(index, prefix_vars)
7752        }
7753        _ => false,
7754    }
7755}
7756
7757/// `s` is exactly one plain assignment to local `v` (optionally wrapped
7758/// in a singleton Block): the branch shape of a Kotlin default-arg
7759/// override.
7760fn single_assign_to_v(s: &Stmt, v: u32) -> Option<Expr> {
7761    match s {
7762        Stmt::ExprStmt(Expr::Assign {
7763            target,
7764            op: AssignOp::Plain,
7765            value,
7766        }) if matches!(&**target, Expr::Local { var: vv, .. } if *vv == v) => {
7767            Some((**value).clone())
7768        }
7769        Stmt::Block(xs) if xs.len() == 1 => single_assign_to_v(&xs[0], v),
7770        _ => None,
7771    }
7772}
7773
7774/// Kotlin default-argument BRIDGE ctors (shape C of the delegation
7775/// family): `<init>(params.., int mask, DefaultConstructorMarker)`
7776/// computes each defaulted arg into a local — `v = p; if ((mask&bit)
7777/// != 0) { v = DEFAULT; }` — then delegates `this(.., v, ..)`. Java
7778/// allows NOTHING before the delegation, so the computations must fold
7779/// INTO the args (`this(.., (mask&bit) != 0 ? DEFAULT : p, ..)`).
7780/// Unfolded, the hoist passes lift the delegation above the raw local
7781/// reads: "找不到符号 变量 str2/v4/creationExtras2" — 2.5k root files
7782/// across weixin/lark/weibo (weixin MvvmObserverOwner$..Observer, lark
7783/// SimpleArrayMap/LongSparseArray). Per-var provability: exactly one
7784/// top-level base write, an optional single conditional override whose
7785/// branches are lone assignments, no other prefix touch of the var,
7786/// nothing after the delegation touches it, and the folded expression
7787/// is pre-this-safe. Unprovable vars stay untouched (partial folds are
7788/// strictly better — the rest was broken before too).
7789/// Flatten nested plain Blocks among the top-level statements. The
7790/// structurer hands ctor bodies as region-nested blocks (`Block([def,
7791/// Block([assign, if]), Block([delegate, return])])` — the Kotlin
7792/// bridge shape), and every delegation-repair pass scans the TOP level
7793/// only. Straight-line nesting flattens without semantics; control
7794/// flow lives inside If/While/etc. arms, never as a direct member.
7795pub(crate) fn flatten_top_blocks(stmts: &mut Vec<Stmt>) {
7796    fn rec(s: Stmt, out: &mut Vec<Stmt>) {
7797        match s {
7798            Stmt::Block(inner) => {
7799                for x in inner {
7800                    rec(x, out);
7801                }
7802            }
7803            other => out.push(other),
7804        }
7805    }
7806    if !stmts.iter().any(|s| matches!(s, Stmt::Block(_))) {
7807        return;
7808    }
7809    let mut out: Vec<Stmt> = Vec::with_capacity(stmts.len());
7810    for s in std::mem::take(stmts) {
7811        rec(s, &mut out);
7812    }
7813    *stmts = out;
7814}
7815
7816fn fold_default_arg_bridge(stmts: &mut Vec<Stmt>) -> bool {
7817    flatten_top_blocks(stmts);
7818    let Some(pos) = stmts.iter().position(is_bare_ctor_call) else {
7819        return false;
7820    };
7821    if pos == 0 {
7822        return false;
7823    }
7824    let Stmt::ExprStmt(Expr::Method { args, .. }) = &stmts[pos] else {
7825        return false;
7826    };
7827    let mut arg_vars: Vec<u32> = Vec::new();
7828    let mut reads = 0usize;
7829    for a in args {
7830        let mut c = a.clone();
7831        deep_rewrite_reads(&mut c, &mut |x| {
7832            if let Expr::Local { var, .. } = x {
7833                reads += 1;
7834                if !arg_vars.contains(var) {
7835                    arg_vars.push(*var);
7836                }
7837            }
7838        });
7839    }
7840    if arg_vars.is_empty() || reads != arg_vars.len() {
7841        return false; // a var read twice would duplicate evaluation
7842    }
7843    // The bridge ENDS at the delegation: nothing after may touch the
7844    // arg vars (reads OR writes).
7845    for &v in &arg_vars {
7846        if stmts_read_var(&stmts[pos + 1..], v) != 0
7847            || stmts_count_writes(&stmts[pos + 1..], v) != 0
7848        {
7849            return false;
7850        }
7851    }
7852    let prefix_vars = prefix_written_vars(&stmts[..pos]);
7853    let mut repl: Vec<(u32, Expr)> = Vec::new();
7854    let mut drop: Vec<usize> = Vec::new();
7855    for &v in &arg_vars {
7856        // Exact prefix shape for v.
7857        let mut base: Option<(usize, Expr)> = None;
7858        let mut decl: Option<usize> = None;
7859        let mut condf: Option<(usize, Expr, Option<Expr>, Option<Expr>)> = None;
7860        let mut ok = true;
7861        for (i, s) in stmts[..pos].iter().enumerate() {
7862            match s {
7863                Stmt::LocalDef { var, init, .. } if *var == v => match init {
7864                    None => {
7865                        if decl.is_some() {
7866                            ok = false;
7867                            break;
7868                        }
7869                        decl = Some(i);
7870                    }
7871                    Some(e) => {
7872                        if base.is_some() {
7873                            ok = false;
7874                            break;
7875                        }
7876                        base = Some((i, e.clone()));
7877                    }
7878                },
7879                Stmt::ExprStmt(Expr::Assign {
7880                    target,
7881                    op: AssignOp::Plain,
7882                    value,
7883                }) if matches!(&**target, Expr::Local { var: vv, .. } if *vv == v) => {
7884                    if base.is_some() {
7885                        ok = false;
7886                        break;
7887                    }
7888                    base = Some((i, (**value).clone()));
7889                }
7890                Stmt::If {
7891                    cond,
7892                    then_stmt,
7893                    else_stmt,
7894                } => {
7895                    let t = single_assign_to_v(then_stmt, v);
7896                    let f = else_stmt.as_ref().and_then(|b| single_assign_to_v(b, v));
7897                    let then_touches = t.is_some()
7898                        || stmts_read_var(std::slice::from_ref(&**then_stmt), v) != 0
7899                        || stmts_count_writes(std::slice::from_ref(&**then_stmt), v) != 0;
7900                    let else_touches = else_stmt.as_ref().is_some_and(|b| {
7901                        f.is_some()
7902                            || stmts_read_var(std::slice::from_ref(b), v) != 0
7903                            || stmts_count_writes(std::slice::from_ref(b), v) != 0
7904                    });
7905                    if !then_touches && !else_touches {
7906                        continue; // unrelated if
7907                    }
7908                    // Both branches must be lone assignments (a branch
7909                    // with its own delegation/return is the conditional-
7910                    // super machinery's shape, not this one), the cond
7911                    // must not read v, and only one override per var.
7912                    if condf.is_some()
7913                        || base.is_none()
7914                        || (then_touches && t.is_none())
7915                        || (else_touches && f.is_none())
7916                    {
7917                        ok = false;
7918                        break;
7919                    }
7920                    let cond_stmt = Stmt::ExprStmt(cond.clone());
7921                    if stmts_read_var(std::slice::from_ref(&cond_stmt), v) != 0 {
7922                        ok = false;
7923                        break;
7924                    }
7925                    condf = Some((i, cond.clone(), t, f));
7926                }
7927                _ => {}
7928            }
7929        }
7930        if !ok {
7931            continue;
7932        }
7933        let Some((bi, be)) = base else { continue };
7934        // Exact write/read accounting: base (+then/else) writes, zero
7935        // prefix reads.
7936        let want_writes = 1
7937            + condf
7938                .as_ref()
7939                .map(|(_, _, t, f)| t.is_some() as usize + f.is_some() as usize)
7940                .unwrap_or(0);
7941        if stmts_count_writes(&stmts[..pos], v) != want_writes
7942            || stmts_read_var(&stmts[..pos], v) != 0
7943        {
7944            continue;
7945        }
7946        let rep = match condf {
7947            Some((ci, c, t, f)) => {
7948                drop.push(ci);
7949                match (t, f) {
7950                    (Some(t), Some(f)) => Expr::Cond {
7951                        c: Box::new(c),
7952                        t: Box::new(t),
7953                        f: Box::new(f),
7954                    },
7955                    (Some(t), None) => Expr::Cond {
7956                        c: Box::new(c),
7957                        t: Box::new(t),
7958                        f: Box::new(be.clone()),
7959                    },
7960                    (None, Some(f)) => Expr::Cond {
7961                        c: Box::new(c),
7962                        t: Box::new(be.clone()),
7963                        f: Box::new(f),
7964                    },
7965                    (None, None) => unreachable!("condf requires a branch assign"),
7966                }
7967            }
7968            None => be.clone(),
7969        };
7970        if !pre_this_safe(&rep, &prefix_vars) {
7971            continue;
7972        }
7973        drop.push(bi);
7974        if let Some(d) = decl {
7975            drop.push(d);
7976        }
7977        repl.push((v, rep));
7978    }
7979    if repl.is_empty() {
7980        return false;
7981    }
7982    if let Stmt::ExprStmt(Expr::Method { args, .. }) = &mut stmts[pos] {
7983        for a in args.iter_mut() {
7984            deep_rewrite_reads(a, &mut |x| {
7985                if let Expr::Local { var, .. } = x {
7986                    if let Some((_, e)) = repl.iter().find(|(vv, _)| vv == var) {
7987                        *x = e.clone();
7988                    }
7989                }
7990            });
7991        }
7992    }
7993    drop.sort_unstable();
7994    drop.dedup();
7995    for i in drop.into_iter().rev() {
7996        stmts.remove(i);
7997    }
7998    true
7999}
8000
8001/// Rewrite static member references on Kotlin multi-file facade PARTS
8002/// (`StringsKt__StringsKt.trim(..)`) to the public facade the parts are
8003/// compiled into (`StringsKt.trim(..)`): the parts are package-private,
8004/// so every cross-package call site is "在kotlin.text中不是公共的; 无法
8005/// 从外部程序包中对其进行访问" (lark 1.4k root files; weibo's 8.4k-line
8006/// Kt cluster). Static members resolve through the facade by
8007/// inheritance (the map is gated on the extends chain).
8008pub fn rewrite_kotlin_facades(body: &mut Stmt, pool: &crate::DexPool) {
8009    let map = pool.kotlin_facade_map();
8010    if map.is_empty() {
8011        return;
8012    }
8013    walk_stmt_exprs(body, &mut |e| {
8014        match e {
8015            Expr::Method {
8016                cls,
8017                name,
8018                desc,
8019                is_static,
8020                ..
8021            } if *is_static => {
8022                if let Some((base, cover)) = map.get(cls.as_ref()) {
8023                    let hit = match cover {
8024                        None => true,
8025                        Some(c) => c.0.contains(&(name.to_string(), desc.to_string())),
8026                    };
8027                    if hit {
8028                        *cls = std::sync::Arc::from(base.as_str());
8029                    }
8030                }
8031            }
8032            Expr::Field {
8033                cls,
8034                name,
8035                is_static,
8036                ..
8037            } if *is_static => {
8038                if let Some((base, cover)) = map.get(cls.as_ref()) {
8039                    let hit = match cover {
8040                        None => true,
8041                        Some(c) => c.1.contains(name.as_ref()),
8042                    };
8043                    if hit {
8044                        *cls = std::sync::Arc::from(base.as_str());
8045                    }
8046                }
8047            }
8048            _ => {}
8049        }
8050    });
8051}
8052
8053/// Kotlin default-arg bridge fold for ENUM ctors: the synthetic
8054/// `(String, int, .., int, DefaultConstructorMarker)` ctor computes
8055/// defaulted args then chains `this(..)` mid-body — Java requires the
8056/// delegation first (对this的调用必须是构造器中的第一个语句, lark's
8057/// UserCustomStatusExtraParams$* family, 752 lines). Same fold the
8058/// class-ctor path runs; the enum branch of the pipeline used to skip
8059/// it (only strip_enum_ctor_super ran there).
8060pub fn fold_enum_default_arg_bridge(body: &mut Stmt) {
8061    let Stmt::Block(stmts) = body else { return };
8062    fold_default_arg_bridge(stmts);
8063}
8064
8065pub fn fix_ctor_delegation_arg_defs(body: &mut Stmt) {
8066    let Stmt::Block(stmts) = body else { return };
8067    // Shape C first: the Kotlin default-arg bridge (computations +
8068    // conditional overrides folded into the delegation args).
8069    fold_default_arg_bridge(stmts);
8070    // Only the top-level linear shape; the first statement needs no
8071    // repair and control flow ahead of the delegation belongs to the
8072    // conditional-super machinery.
8073    let Some(pos) = stmts.iter().position(is_bare_ctor_call) else {
8074        return;
8075    };
8076    if pos == 0 {
8077        return;
8078    }
8079    // Vars read by the delegation args, and the total read count (a var
8080    // read twice would duplicate a call evaluation on inline).
8081    let mut arg_vars: Vec<u32> = Vec::new();
8082    let mut total_reads = 0usize;
8083    {
8084        let Stmt::ExprStmt(Expr::Method { args, .. }) = &stmts[pos] else {
8085            return;
8086        };
8087        for a in args {
8088            let mut c = a.clone();
8089            deep_rewrite_reads(&mut c, &mut |x| {
8090                if let Expr::Local { var: v, .. } = x {
8091                    total_reads += 1;
8092                    if !arg_vars.contains(v) {
8093                        arg_vars.push(*v);
8094                    }
8095                }
8096            });
8097        }
8098    }
8099    if arg_vars.is_empty() || total_reads != arg_vars.len() {
8100        return;
8101    }
8102    // Vars DEFINED anywhere in the prefix (for the init-referenced
8103    // check below — an inlined init may not itself read prefix defs).
8104    let prefix_defs: Vec<u32> = stmts[..pos]
8105        .iter()
8106        .filter_map(|s| match s {
8107            Stmt::LocalDef { var, init: Some(_), .. } => Some(*var),
8108            _ => None,
8109        })
8110        .collect();
8111    // Per referenced var: exactly one prefix def, no other prefix
8112    // reader between def and delegation, the def's init reads no
8113    // prefix-def var, and the first post-delegation use is a top-level
8114    // write (becomes the declaration) or nothing at all.
8115    let mut plan: Vec<(u32, usize, Option<usize>)> = Vec::new();
8116    for v in arg_vars {
8117        let defs: Vec<usize> = stmts[..pos]
8118            .iter()
8119            .enumerate()
8120            .filter(|(i, _)| {
8121                matches!(&stmts[*i], Stmt::LocalDef { var, init: Some(_), .. } if *var == v)
8122            })
8123            .map(|(i, _)| i)
8124            .collect();
8125        if defs.len() > 1 {
8126            return; // ambiguous: multiple prefix defs
8127        }
8128        let Some(&q) = defs.first() else {
8129            continue; // a param: nothing to inline for this var
8130        };
8131        let init = match &stmts[q] {
8132            Stmt::LocalDef { init: Some(e), .. } => e.clone(),
8133            _ => return,
8134        };
8135        if stmts_read_var(&stmts[q + 1..pos], v) != 0 {
8136            return; // another reader between the def and the delegation
8137        }
8138        // The inlined init may not read a prefix-def var either — the
8139        // delegation lands ABOVE those defs after the hoist.
8140        let mut init_vars: Vec<u32> = Vec::new();
8141        deep_rewrite_reads(&mut { init.clone() }, &mut |x| {
8142            if let Expr::Local { var: vv, .. } = x {
8143                if !init_vars.contains(vv) {
8144                    init_vars.push(*vv);
8145                }
8146            }
8147        });
8148        if init_vars.iter().any(|iv| prefix_defs.contains(iv)) {
8149            return;
8150        }
8151        match stmts[pos + 1..]
8152            .iter()
8153            .position(|s| {
8154                matches!(s, Stmt::ExprStmt(Expr::Assign { target, .. })
8155                    if matches!(&**target, Expr::Local { var: vv, .. } if *vv == v))
8156            }) {
8157            Some(w) => {
8158                // Reads before the re-declaration would be dangling.
8159                if stmts_read_var(&stmts[pos + 1..pos + 1 + w], v) != 0 {
8160                    return;
8161                }
8162                plan.push((v, q, Some(pos + 1 + w)));
8163            }
8164            None => {
8165                // No later write: the def must be dead after inline.
8166                if stmts_read_var(&stmts[pos + 1..], v) != 0 {
8167                    return;
8168                }
8169                plan.push((v, q, None));
8170            }
8171        }
8172    }
8173    if plan.is_empty() {
8174        return;
8175    }
8176    // Inline the def inits into the delegation args.
8177    let values: Vec<(u32, Expr)> = plan
8178        .iter()
8179        .map(|(_v, q, _)| match &stmts[*q] {
8180            Stmt::LocalDef { var, init: Some(e), .. } => (*var, e.clone()),
8181            _ => unreachable!("plan entries carry a LocalDef with init"),
8182        })
8183        .collect();
8184    if let Stmt::ExprStmt(Expr::Method { args, .. }) = &mut stmts[pos] {
8185        for a in args {
8186            deep_rewrite_reads(a, &mut |x| {
8187                if let Expr::Local { var: v, .. } = x {
8188                    if let Some((_, e)) = values.iter().find(|(vv, _)| vv == v) {
8189                        *x = e.clone();
8190                    }
8191                }
8192            });
8193        }
8194    }
8195    // Convert the first writes into declarations BEFORE removing defs
8196    // (removals shift indices); then remove the defs in descending
8197    // order.
8198    for (v, _, w) in &plan {
8199        if let Some(w) = w {
8200            if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = &mut stmts[*w] {
8201                if matches!(&**target, Expr::Local { var: vv, .. } if vv == v) {
8202                    let taken = std::mem::replace(&mut **value, Expr::Const(ConstVal::Null));
8203                    stmts[*w] = Stmt::LocalDef {
8204                        var: *v,
8205                        init: Some(taken),
8206                        is_final: false,
8207                        force_type: false,
8208                    };
8209                }
8210            }
8211        }
8212    }
8213    let mut order: Vec<usize> = plan.iter().map(|(_, q, _)| *q).collect();
8214    order.sort_unstable();
8215    order.reverse();
8216    for q in order {
8217        stmts.remove(q);
8218    }
8219}
8220
8221/// An INT-typed method returning a boolean value: the register held the
8222/// boolean test result in one generation and the `if (v) 1 else 0` int
8223/// in another; booleanize picks the boolean generation and the plain
8224/// `return v;` breaks ("boolean无法转换为int", weixin Handle
8225/// onPausableTransaction — the source was `if (v) 1 else 0`). Wrap the
8226/// return in the recovering conditional.
8227/// Mirror of fix_int_returns: a BOOLEAN method returning a numeric-
8228/// typed value (register reuse left an int generation) wraps it
8229/// `x != 0` — the dex branch encoding, and the only legal Java form
8230/// (kh5/a `return v116_g53;` int无法转换为boolean family).
8231pub fn fix_bool_returns(vt: &VarTable, body: &mut Stmt) {
8232    walk_mut_deep(body, &mut |st| {
8233        if let Stmt::Return(Some(e)) = st {
8234            let numeric = match &*e {
8235                Expr::Local { var, ty } => {
8236                    let t = if (*var as usize) < vt.vars.len() {
8237                        vt.var(*var).ty.erased()
8238                    } else {
8239                        ty.erased()
8240                    };
8241                    matches!(
8242                        t,
8243                        JavaType::Int
8244                            | JavaType::Long
8245                            | JavaType::Short
8246                            | JavaType::Byte
8247                            | JavaType::Char
8248                    )
8249                }
8250                other => matches!(
8251                    other.type_ref().erased(),
8252                    JavaType::Int | JavaType::Long | JavaType::Short | JavaType::Byte
8253                        | JavaType::Char
8254                ),
8255            };
8256            if numeric {
8257                let taken = e.clone();
8258                *e = Expr::Bin {
8259                    op: BinOp::Ne,
8260                    l: Box::new(taken),
8261                    r: Box::new(Expr::Const(ConstVal::Int(0))),
8262                    ty: Some(TypeRef::J(JavaType::Boolean)),
8263                };
8264            }
8265        }
8266    });
8267}
8268
8269pub fn fix_int_returns(vt: &VarTable, body: &mut Stmt) {
8270    walk_mut_deep(body, &mut |st| {
8271        if let Stmt::Return(Some(e)) = st {
8272            let is_bool = match e {
8273                Expr::Local { var, .. } => {
8274                    (*var as usize) < vt.vars.len()
8275                        && vt.vars[*var as usize].ty.erased() == JavaType::Boolean
8276                }
8277                Expr::Method { desc, .. } => desc.ret == JavaType::Boolean,
8278                _ => false,
8279            };
8280            if is_bool {
8281                *e = Expr::Cond {
8282                    c: Box::new(e.clone()),
8283                    t: Box::new(Expr::Const(ConstVal::Int(1))),
8284                    f: Box::new(Expr::Const(ConstVal::Int(0))),
8285                };
8286            }
8287        }
8288    });
8289}
8290
8291/// Generation splitting: when an assignment's value type is
8292/// INCOMPATIBLE with the var's declared type, mint a FRESH var for the
8293/// new generation instead of widening (widening was measured twice as
8294/// net-negative — every use-site cast gap re-triggers javac's
8295/// error-type contagion). Reads after the split point follow the
8296/// active generation; control-flow joins intersect; loops run a
8297/// 3-round fixpoint so the back-edge carries the tail generation to
8298/// the head reads (the loop-state-tuple shape, qf5/v0's
8299/// `u0 = (qf5.u0) get;` … `u0 = looper2;`).
8300///
8301/// Split condition (reference incompatibility or a primitive/value
8302/// mix — the instanceof-corruption shape): both specific references of
8303/// different classes, or prim→ref. NOT numeric-vs-numeric (promotion
8304/// handles those) and NOT Object targets (assignable — the narrowing
8305/// cast pass covers the value side).
8306pub fn split_generations(vt: &mut VarTable, body: &mut Stmt, pool: &DexPool) {
8307    let mut counter: u32 = 0;
8308    let mut gen: std::collections::HashMap<u32, u32> = std::collections::HashMap::default();
8309    split_walk_stmt(body, vt, &mut gen, &mut counter, pool);
8310}
8311
8312/// Should the assignment `target = value` split the target?
8313fn split_needed(vt: &VarTable, target: u32, value: &Expr, pool: &DexPool) -> bool {
8314    let n = vt.vars.len();
8315    if target as usize >= n {
8316        return false;
8317    }
8318    let declared = vt.vars[target as usize].ty.erased();
8319    let vt_is_ref = |t: &JavaType| matches!(t, JavaType::Object(_) | JavaType::Array(_));
8320    // Locals consult the VarTable: the embedded ty is a lift-time
8321    // snapshot, stale after booleanize converts a copy SOURCE (the
8322    // post-booleanize re-split below relies on seeing `v131` as the
8323    // boolean it became). Bitwise Or/And/Xor over boolean operands
8324    // carry the same staleness at the EXPRESSION level (dex lowered
8325    // Kotlin's non-short-circuit `or` to int `|` — the embedded Bin ty
8326    // stays Int after booleanize converts the operands): `int v18 =
8327    // delete | delete2` must split the target generation.
8328    fn side_bool(e: &Expr, vt: &VarTable) -> bool {
8329        match e {
8330            Expr::Local { var, .. } if (*var as usize) < vt.vars.len() => {
8331                matches!(vt.var(*var).ty.erased(), JavaType::Boolean)
8332            }
8333            Expr::Const(ConstVal::Int(0 | 1)) => true,
8334            Expr::Bin { op: BinOp::Or | BinOp::And | BinOp::Xor, l, r, .. } => {
8335                side_bool(l, vt) && side_bool(r, vt)
8336            }
8337            other => matches!(other.type_ref().erased(), JavaType::Boolean),
8338        }
8339    }
8340    let value_ty = match value {
8341        Expr::Local { var, ty } if (*var as usize) < vt.vars.len() => vt.var(*var).ty.erased(),
8342        Expr::Bin {
8343            op: BinOp::Or | BinOp::And | BinOp::Xor,
8344            l,
8345            r,
8346            ..
8347        } if side_bool(l, vt) && side_bool(r, vt) => JavaType::Boolean,
8348        other => other.type_ref().erased(),
8349    };
8350    if vt_is_ref(&declared) && vt_is_ref(&value_ty) {
8351        // Both references: split only when incompatible — different
8352        // classes where neither side is Object AND the value is not a
8353        // SUBTYPE of the declared type. An assignable store needs no
8354        // fresh generation; splitting one minted an orphan at an
8355        // if-join whose sibling arm kept the base (rename_gen_back's
8356        // primitive-only gate refused the undo, drop_dead_locals then
8357        // stripped the orphan to a bare allocation): pq0/g's bridge
8358        // `map2 = new LinkedHashMap()` against the HashMap-typed phi
8359        // rendered `new LinkedHashMap();` and the arm lost its store.
8360        let obj = JavaType::Object("java/lang/Object".into());
8361        if declared == value_ty || declared == obj || value_ty == obj {
8362            return false;
8363        }
8364        // A FRESH ALLOCATION whose class relation to the declared type
8365        // is UNKNOWABLE (a framework↔framework pair — LinkedHashMap
8366        // into a HashMap-typed phi): the dex stored the instance into
8367        // this register flow, so the store was verifier-legal; the
8368        // split cannot know better and orphaned the generation (the
8369        // if-join undo is primitive-only; the dropper stripped the
8370        // orphan to a bare `new LinkedHashMap();` and pq0/g's bridge
8371        // lost the else-arm store, killing the whole branched-super
8372        // merge). Pool-known pairs KEEP the split: the generation's
8373        // precise type is what downstream subtype sinks need (weibo
8374        // `v7 = new $reportWhenComplete$1(..)` into a ContinuationImpl
8375        // phi — exempting it made `v9($1) = v7` inconvertible, +50).
8376        if let (JavaType::Object(v), JavaType::Object(d)) = (&value_ty, &declared) {
8377            if matches!(value, Expr::New { .. })
8378                && (pool.get(v).is_none() || pool.get(d).is_none())
8379            {
8380                return false;
8381            }
8382        }
8383        true
8384    } else {
8385        // Primitive/reference mix: the value generation has a kind the
8386        // target can never accept — split (the declared type stays with
8387        // the old generation's readers).
8388        if vt_is_ref(&declared) != vt_is_ref(&value_ty) {
8389            return true;
8390        }
8391        // Boolean/numeric mix: `boolean` and `int` have NO cast between
8392        // them in Java, so a register reused across the kinds can only
8393        // be expressed as two generations (weixin ConstraintLayout
8394        // `boolean v60; int v56; v60 = v56;` — 9.5k bool↔int lines once
8395        // the obscuring suppression lifted). 0/1 constants are the
8396        // boolean encoding, not a numeric generation — never split on
8397        // those (booleanize owns them).
8398        let d_bool = matches!(declared, JavaType::Boolean);
8399        let v_bool = matches!(value_ty, JavaType::Boolean);
8400        let zero_one = matches!(value, Expr::Const(ConstVal::Int(0 | 1)));
8401        d_bool != v_bool && !zero_one
8402    }
8403}
8404
8405fn rewrite_gen_reads(e: &mut Expr, gen: &std::collections::HashMap<u32, u32>, vt: &VarTable) {
8406    deep_rewrite(e, &mut |x| {
8407        if let Expr::Local { var, ty } = x {
8408            if let Some(&to) = gen.get(var) {
8409                if to != *var {
8410                    *var = to;
8411                    *ty = vt.var(to).ty.clone();
8412                }
8413            }
8414        }
8415    });
8416}
8417
8418fn do_split(
8419    vt: &mut VarTable,
8420    target: &mut Expr,
8421    old: u32,
8422    value_ty: &JavaType,
8423    gen: &mut std::collections::HashMap<u32, u32>,
8424    counter: &mut u32,
8425) {
8426    *counter += 1;
8427    let info = &vt.vars[old as usize];
8428    let id = vt.vars.len() as u32;
8429    let name = format!("{}_g{}", info.name, counter);
8430    let slot = info.slot;
8431    // A split generation is a fresh LOCAL, not a parameter — even when
8432    // the parent register is one (the param identity stays on the
8433    // original var id; the generation only carries a later write). The
8434    // inherited flag made booleanize (and every other param-skip pass)
8435    // permanently ignore param-register generations: weixin v2/i's
8436    // `int v264_g160_g7 = !v200` with `!= 0` reads (bool→int assign
8437    // family, 2.8k lines).
8438    let is_param = false;
8439    vt.vars.push(jdc_core::var::VarInfo {
8440        id,
8441        slot,
8442        name,
8443        ty: TypeRef::J(value_ty.clone()),
8444        is_param,
8445        range_start: 0,
8446        range_end: u16::MAX,
8447        synthetic_name: true,
8448    });
8449    while vt.by_slot.len() <= slot as usize {
8450        vt.by_slot.push(Vec::new());
8451    }
8452    vt.by_slot[slot as usize].push((0, u16::MAX, id));
8453    gen.insert(old, id);
8454    if let Expr::Local { var, ty, .. } = target {
8455        *var = id;
8456        *ty = TypeRef::J(value_ty.clone());
8457    }
8458}
8459
8460/// Undo a DIVERGED generation split: rewrite every occurrence of the
8461/// freshly-minted generation `from` back to the pre-if var `to` within
8462/// `s` (both read positions and assignment targets, plus LocalDef var
8463/// fields). At a control-flow join only splits BOTH branches produced
8464/// survive (the phi merges on the shared var); a gen one branch minted
8465/// alone does not. But `do_split` already rewrote that branch's write
8466/// target IN PLACE, so without this undo the branch stores into a
8467/// write-only orphan gen that no confluence reader ever reads — the
8468/// value is silently lost AND the orphan renders undeclared (kt5.b
8469/// Kotlin default-arg bridge: `v18_g1 = p6x` in then vs `v18 = false`
8470/// in else, `this(.., v18, ..)` reads only else's value).
8471fn rename_gen_back(s: &mut Stmt, from: u32, to: u32, vt: &VarTable) {
8472    if from == to || (to as usize) >= vt.vars.len() {
8473        return;
8474    }
8475    let to_ty = vt.vars[to as usize].ty.clone();
8476    walk_mut_deep(s, &mut |st| {
8477        if let Stmt::LocalDef { var, .. } = st {
8478            if *var == from {
8479                *var = to;
8480            }
8481        }
8482    });
8483    rewrite_exprs(s, &mut |e| {
8484        deep_rewrite(e, &mut |x| {
8485            if let Expr::Local { var, ty } = x {
8486                if *var == from {
8487                    *var = to;
8488                    *ty = to_ty.clone();
8489                }
8490            }
8491        });
8492    });
8493}
8494
8495/// Remove an undone generation's `by_slot` segment entry so slot-based
8496/// resolution can never hand back the now-unreferenced gen id (its
8497/// VarInfo stays — ids index `vars` — but nothing may select it).
8498fn drop_gen_slot(vt: &mut VarTable, dead: u32) {
8499    if (dead as usize) >= vt.vars.len() {
8500        return;
8501    }
8502    let slot = vt.vars[dead as usize].slot as usize;
8503    if let Some(seg) = vt.by_slot.get_mut(slot) {
8504        seg.retain(|&(_, _, id)| id != dead);
8505    }
8506}
8507
8508/// Type gate for `rename_gen_back`: merging gen `v` back onto base `k`
8509/// is only safe when the base can carry the gen's value — identical
8510/// types, or BOTH primitives (the bool/int register-reuse family that
8511/// booleanize + the cast fixers reconcile; the Kotlin bridge case,
8512/// kt5.b `int v18` receiving `boolean p6x`). A reference-vs-primitive
8513/// or cross-class merge would forge `String k = <boolean>`-style
8514/// assignments that never compile (the lark/weibo/reqable regression
8515/// of the ungated first cut: +8.4k `不兼容的类型`).
8516fn undo_type_safe(vt: &VarTable, k: u32, v: u32) -> bool {
8517    if (k as usize) >= vt.vars.len() || (v as usize) >= vt.vars.len() {
8518        return false;
8519    }
8520    let kt = vt.vars[k as usize].ty.erased();
8521    let vt_ty = vt.vars[v as usize].ty.erased();
8522    if kt == vt_ty {
8523        return true;
8524    }
8525    let is_prim = |t: &JavaType| {
8526        matches!(
8527            t,
8528            JavaType::Int
8529                | JavaType::Long
8530                | JavaType::Short
8531                | JavaType::Byte
8532                | JavaType::Char
8533                | JavaType::Float
8534                | JavaType::Double
8535                | JavaType::Boolean
8536        )
8537    };
8538    is_prim(&kt) && is_prim(&vt_ty)
8539}
8540
8541fn split_walk_stmt(
8542    st: &mut Stmt,
8543    vt: &mut VarTable,
8544    gen: &mut std::collections::HashMap<u32, u32>,
8545    counter: &mut u32,
8546    pool: &DexPool,
8547) {
8548    match st {
8549        Stmt::Block(v) => {
8550            for x in v.iter_mut() {
8551                split_walk_stmt(x, vt, gen, counter, pool);
8552            }
8553        }
8554        Stmt::ExprStmt(e) => {
8555            rewrite_gen_reads(e, gen, vt);
8556            if let Expr::Assign {
8557                target,
8558                value,
8559                op: AssignOp::Plain,
8560                ..
8561            } = e
8562            {
8563                if let Expr::Local { var, .. } = &**target {
8564                    let old = *var;
8565                    if split_needed(vt, old, value, pool) {
8566                        let value_ty = value.type_ref().erased();
8567                        do_split(vt, target, old, &value_ty, gen, counter);
8568                    }
8569                }
8570            }
8571        }
8572        Stmt::LocalDef { var, init, .. } => {
8573            if let Some(e) = init {
8574                rewrite_gen_reads(e, gen, vt);
8575                if split_needed(vt, *var, e, pool) {
8576                    // The DEF mints its own generation: retarget the def
8577                    // to a fresh var (the bare decl of the ORIGINAL stays
8578                    // for its earlier readers).
8579                    let value_ty = e.type_ref().erased();
8580                    let old = *var;
8581                    let mut fake = Expr::Local {
8582                        var: old,
8583                        ty: vt.vars[old as usize].ty.clone(),
8584                    };
8585                    do_split(vt, &mut fake, old, &value_ty, gen, counter);
8586                    if let Expr::Local { var: nv, .. } = fake {
8587                        *var = nv;
8588                    }
8589                }
8590            }
8591        }
8592        Stmt::If {
8593            cond,
8594            then_stmt,
8595            else_stmt,
8596            ..
8597        } => {
8598            rewrite_gen_reads(cond, gen, vt);
8599            let then_floor = vt.vars.len() as u32;
8600            let mut g_then = gen.clone();
8601            split_walk_stmt(then_stmt, vt, &mut g_then, counter, pool);
8602            let else_floor = vt.vars.len() as u32;
8603            let mut g_else = gen.clone();
8604            if let Some(e) = else_stmt {
8605                split_walk_stmt(e, vt, &mut g_else, counter, pool);
8606            }
8607            // Undo splits that DIVERGED at this join (see
8608            // rename_gen_back), with TWO gates learned the hard way
8609            // (first cut regressed lark +8.4k):
8610            //  1. the SIBLING KEPT THE BASE — `g_else[k]` equals the
8611            //     entry mapping, i.e. only this branch minted a fresh
8612            //     gen for k. When both branches split, each orphan is
8613            //     declared by ensure_declared at its own value type and
8614            //     compiles; renaming them back onto the base would
8615            //     force cross-kind assignments (`int k = <boolean>`).
8616            //  2. the fresh gen is WRITE-ONLY in the branch — a gen
8617            //     read inside its branch renders fine on its own.
8618            // The gated shape is the Kotlin default-arg bridge phi
8619            // loss: then `v_g = p6x` / else `v = false` / confluence
8620            // reads v — the then value was silently dropped AND v_g
8621            // rendered undeclared (kt5.b). Undo restores the clean
8622            // diamond so fold_default_arg_bridge can fire.
8623            let mut undo_then: Vec<(u32, u32)> = g_then
8624                .iter()
8625                .filter(|(&k, &v)| {
8626                    v >= then_floor
8627                        && v < else_floor
8628                        && g_else.get(&k).copied() == gen.get(&k).copied()
8629                        && undo_type_safe(vt, k, v)
8630                        && stmts_read_var(std::slice::from_ref(&**then_stmt), v) == 0
8631                })
8632                .map(|(&k, &v)| (v, k))
8633                .collect();
8634            undo_then.sort_unstable_by_key(|&(v, _)| std::cmp::Reverse(v));
8635            for &(v, k) in &undo_then {
8636                rename_gen_back(then_stmt, v, k, vt);
8637                drop_gen_slot(vt, v);
8638            }
8639            if let Some(e) = else_stmt.as_deref_mut() {
8640                let mut undo_else: Vec<(u32, u32)> = g_else
8641                    .iter()
8642                    .filter(|(&k, &v)| {
8643                        v >= else_floor
8644                            && g_then.get(&k).copied() == gen.get(&k).copied()
8645                            && undo_type_safe(vt, k, v)
8646                            && stmts_read_var(std::slice::from_ref(&*e), v) == 0
8647                    })
8648                    .map(|(&k, &v)| (v, k))
8649                    .collect();
8650                undo_else.sort_unstable_by_key(|&(v, _)| std::cmp::Reverse(v));
8651                for &(v, k) in &undo_else {
8652                    rename_gen_back(e, v, k, vt);
8653                    drop_gen_slot(vt, v);
8654                }
8655            }
8656            // Join: keep only splits BOTH paths produced (an absent else
8657            // is a fall-through path that keeps the pre-if generation).
8658            gen.retain(|k, v| g_then.get(k) == Some(v) && g_else.get(k) == Some(v));
8659        }
8660        Stmt::While { cond, body, .. } | Stmt::DoWhile { cond, body, .. } => {
8661            // 3-round fixpoint: round N walks the cond+body with the
8662            // accumulated gen — the back-edge carries the tail
8663            // generation into the head reads of the next round.
8664            let entry = gen.clone();
8665            for _ in 0..3 {
8666                rewrite_gen_reads(cond, gen, vt);
8667                split_walk_stmt(body, vt, gen, counter, pool);
8668            }
8669            // After the loop the pre-loop generation is what a
8670            // zero-iteration execution left — conservative.
8671            *gen = entry;
8672        }
8673        Stmt::For { init, body, .. } => {
8674            for x in init.iter_mut() {
8675                split_walk_stmt(x, vt, gen, counter, pool);
8676            }
8677            let entry = gen.clone();
8678            for _ in 0..3 {
8679                split_walk_stmt(body, vt, gen, counter, pool);
8680            }
8681            *gen = entry;
8682        }
8683        Stmt::ForEach { body, .. } => {
8684            let entry = gen.clone();
8685            for _ in 0..3 {
8686                split_walk_stmt(body, vt, gen, counter, pool);
8687            }
8688            *gen = entry;
8689        }
8690        Stmt::Switch { cases, default, .. } => {
8691            let entry = gen.clone();
8692            for c in cases.iter_mut() {
8693                let mut g_case = entry.clone();
8694                for x in c.body.iter_mut() {
8695                    split_walk_stmt(x, vt, &mut g_case, counter, pool);
8696                }
8697                gen.retain(|k, v| g_case.get(k) == Some(v));
8698            }
8699            if let Some(d) = default {
8700                let mut g_def = entry.clone();
8701                split_walk_stmt(d, vt, &mut g_def, counter, pool);
8702                gen.retain(|k, v| g_def.get(k) == Some(v));
8703            }
8704        }
8705        Stmt::Try {
8706            body,
8707            catches,
8708            finally,
8709        } => {
8710            let entry = gen.clone();
8711            let mut g_try = entry.clone();
8712            split_walk_stmt(body, vt, &mut g_try, counter, pool);
8713            let mut g_all = g_try.clone();
8714            for c in catches.iter_mut() {
8715                // The handler runs with the PRE-try state (the exception
8716                // may fire anywhere in the try).
8717                let mut g_c = entry.clone();
8718                split_walk_stmt(&mut c.body, vt, &mut g_c, counter, pool);
8719                g_all.retain(|k, v| g_c.get(k) == Some(v));
8720            }
8721            *gen = g_all;
8722            if let Some(f) = finally {
8723                split_walk_stmt(f, vt, gen, counter, pool);
8724            }
8725        }
8726        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => {
8727            split_walk_stmt(body, vt, gen, counter, pool);
8728        }
8729        Stmt::Return(Some(e)) | Stmt::Throw(e) => {
8730            rewrite_gen_reads(e, gen, vt);
8731        }
8732        _ => {}
8733    }
8734}
8735
8736/// Direct supertypes of framework exception classes (internal names).
8737/// Dex catch handlers legitimately list subclass+superclass pairs for
8738/// one handler (verifier: first match wins); Java multi-catch forbids
8739/// subtype-related alternatives ("multi-catch 语句中的替代无法通过子类化
8740/// 关联" ×58: FileNotFoundException|IOException ×12,
8741/// NameNotFoundException|Exception, JSONException|Exception,
8742/// IllegalArgumentException|RuntimeException, …). Dropping the
8743/// shadowed alternative preserves the caught set EXACTLY (same
8744/// handler body, same variable). Pool hierarchies resolve through
8745/// pool.is_subtype (it matches ancestor NAMES, so pool subclasses of
8746/// framework exceptions work); framework classes are absent from the
8747/// dex, so the stable public JDK/Android chains are tabled here —
8748/// standard decompiler practice. Missing entries only lose a dedupe
8749/// opportunity (safe direction); a WRONG entry would drop a live
8750/// alternative, so only unambiguous chains are listed.
8751static FW_EXC_SUPERS: &[(&str, &str)] = &[
8752    // java.lang
8753    ("java/lang/Exception", "java/lang/Throwable"),
8754    ("java/lang/RuntimeException", "java/lang/Exception"),
8755    ("java/lang/IllegalArgumentException", "java/lang/RuntimeException"),
8756    ("java/lang/IllegalStateException", "java/lang/RuntimeException"),
8757    ("java/lang/NullPointerException", "java/lang/RuntimeException"),
8758    ("java/lang/ArithmeticException", "java/lang/RuntimeException"),
8759    ("java/lang/ClassCastException", "java/lang/RuntimeException"),
8760    ("java/lang/IndexOutOfBoundsException", "java/lang/RuntimeException"),
8761    ("java/lang/ArrayIndexOutOfBoundsException", "java/lang/IndexOutOfBoundsException"),
8762    ("java/lang/StringIndexOutOfBoundsException", "java/lang/IndexOutOfBoundsException"),
8763    ("java/lang/NumberFormatException", "java/lang/IllegalArgumentException"),
8764    ("java/lang/UnsupportedOperationException", "java/lang/RuntimeException"),
8765    ("java/lang/SecurityException", "java/lang/RuntimeException"),
8766    ("java/lang/NegativeArraySizeException", "java/lang/RuntimeException"),
8767    ("java/lang/EnumConstantNotPresentException", "java/lang/RuntimeException"),
8768    ("java/lang/TypeNotPresentException", "java/lang/RuntimeException"),
8769    ("java/lang/CloneNotSupportedException", "java/lang/Exception"),
8770    ("java/lang/InterruptedException", "java/lang/Exception"),
8771    ("java/lang/ReflectiveOperationException", "java/lang/Exception"),
8772    ("java/lang/ClassNotFoundException", "java/lang/ReflectiveOperationException"),
8773    ("java/lang/NoSuchFieldException", "java/lang/ReflectiveOperationException"),
8774    ("java/lang/NoSuchMethodException", "java/lang/ReflectiveOperationException"),
8775    ("java/lang/InstantiationException", "java/lang/ReflectiveOperationException"),
8776    ("java/lang/IllegalAccessException", "java/lang/ReflectiveOperationException"),
8777    ("java/lang/reflect/InvocationTargetException", "java/lang/ReflectiveOperationException"),
8778    ("java/lang/reflect/UndeclaredThrowableException", "java/lang/RuntimeException"),
8779    ("java/lang/invoke/WrongMethodTypeException", "java/lang/RuntimeException"),
8780    ("java/lang/annotation/IncompleteAnnotationException", "java/lang/RuntimeException"),
8781    // java.lang errors
8782    ("java/lang/Error", "java/lang/Throwable"),
8783    ("java/lang/AssertionError", "java/lang/Error"),
8784    ("java/lang/VirtualMachineError", "java/lang/Error"),
8785    ("java/lang/OutOfMemoryError", "java/lang/VirtualMachineError"),
8786    ("java/lang/StackOverflowError", "java/lang/VirtualMachineError"),
8787    ("java/lang/LinkageError", "java/lang/Error"),
8788    ("java/lang/IncompatibleClassChangeError", "java/lang/LinkageError"),
8789    ("java/lang/NoSuchMethodError", "java/lang/IncompatibleClassChangeError"),
8790    ("java/lang/NoSuchFieldError", "java/lang/IncompatibleClassChangeError"),
8791    ("java/lang/NoClassDefFoundError", "java/lang/LinkageError"),
8792    ("java/lang/ExceptionInInitializerError", "java/lang/LinkageError"),
8793    ("java/lang/UnsatisfiedLinkError", "java/lang/LinkageError"),
8794    ("java/lang/VerifyError", "java/lang/LinkageError"),
8795    ("java/lang/ClassFormatError", "java/lang/LinkageError"),
8796    ("java/lang/BootstrapMethodError", "java/lang/LinkageError"),
8797    // java.util
8798    ("java/util/ConcurrentModificationException", "java/lang/RuntimeException"),
8799    ("java/util/NoSuchElementException", "java/lang/RuntimeException"),
8800    ("java/util/MissingResourceException", "java/lang/RuntimeException"),
8801    ("java/util/EmptyStackException", "java/lang/RuntimeException"),
8802    ("java/util/TooManyListenersException", "java/lang/Exception"),
8803    ("java/util/ServiceConfigurationError", "java/lang/Error"),
8804    ("java/util/concurrent/ExecutionException", "java/lang/Exception"),
8805    ("java/util/concurrent/TimeoutException", "java/lang/Exception"),
8806    ("java/util/concurrent/BrokenBarrierException", "java/lang/Exception"),
8807    ("java/util/concurrent/RejectedExecutionException", "java/lang/RuntimeException"),
8808    ("java/util/concurrent/CompletionException", "java/lang/RuntimeException"),
8809    ("java/util/concurrent/CancellationException", "java/lang/IllegalStateException"),
8810    // java.io / java.net
8811    ("java/io/IOException", "java/lang/Exception"),
8812    ("java/io/FileNotFoundException", "java/io/IOException"),
8813    ("java/io/InterruptedIOException", "java/io/IOException"),
8814    ("java/io/UnsupportedEncodingException", "java/io/IOException"),
8815    ("java/io/UTFDataFormatException", "java/io/IOException"),
8816    ("java/io/EOFException", "java/io/IOException"),
8817    ("java/io/CharConversionException", "java/io/IOException"),
8818    ("java/io/ObjectStreamException", "java/io/IOException"),
8819    ("java/io/InvalidObjectException", "java/io/ObjectStreamException"),
8820    ("java/io/NotSerializableException", "java/io/ObjectStreamException"),
8821    ("java/net/SocketException", "java/io/IOException"),
8822    ("java/net/ConnectException", "java/net/SocketException"),
8823    ("java/net/BindException", "java/net/SocketException"),
8824    ("java/net/NoRouteToHostException", "java/net/SocketException"),
8825    ("java/net/ProtocolException", "java/io/IOException"),
8826    ("java/net/UnknownHostException", "java/io/IOException"),
8827    ("java/net/MalformedURLException", "java/io/IOException"),
8828    ("java/net/HttpRetryException", "java/io/IOException"),
8829    ("java/net/SocketTimeoutException", "java/io/InterruptedIOException"),
8830    ("java/net/URISyntaxException", "java/lang/Exception"),
8831    // java.text / security / crypto / ssl / sax
8832    ("java/text/ParseException", "java/lang/Exception"),
8833    ("java/security/GeneralSecurityException", "java/lang/Exception"),
8834    ("java/security/NoSuchAlgorithmException", "java/security/GeneralSecurityException"),
8835    ("java/security/NoSuchProviderException", "java/security/GeneralSecurityException"),
8836    ("java/security/InvalidKeyException", "java/security/GeneralSecurityException"),
8837    ("java/security/InvalidAlgorithmParameterException", "java/security/GeneralSecurityException"),
8838    ("java/security/SignatureException", "java/security/GeneralSecurityException"),
8839    ("java/security/UnrecoverableKeyException", "java/security/GeneralSecurityException"),
8840    ("java/security/KeyStoreException", "java/security/GeneralSecurityException"),
8841    ("java/security/cert/CertificateException", "java/security/GeneralSecurityException"),
8842    ("java/security/spec/InvalidKeySpecException", "java/security/GeneralSecurityException"),
8843    ("javax/crypto/BadPaddingException", "java/security/GeneralSecurityException"),
8844    ("javax/crypto/IllegalBlockSizeException", "java/security/GeneralSecurityException"),
8845    ("javax/crypto/NoSuchPaddingException", "java/security/GeneralSecurityException"),
8846    ("javax/crypto/ShortBufferException", "java/security/GeneralSecurityException"),
8847    ("javax/net/ssl/SSLException", "java/io/IOException"),
8848    ("org/xml/sax/SAXException", "java/lang/Exception"),
8849    ("org/xml/sax/SAXParseException", "org/xml/sax/SAXException"),
8850    ("org/json/JSONException", "java/lang/RuntimeException"),
8851    ("org/xmlpull/v1/XmlPullParserException", "java/lang/Exception"),
8852    ("java/util/IllegalFormatException", "java/lang/IllegalArgumentException"),
8853    ("java/util/MissingFormatArgumentException", "java/util/IllegalFormatException"),
8854    ("java/util/UnknownFormatConversionException", "java/util/IllegalFormatException"),
8855    // android
8856    ("android/util/AndroidRuntimeException", "java/lang/RuntimeException"),
8857    ("android/util/AndroidException", "java/lang/Exception"),
8858    ("android/content/ActivityNotFoundException", "java/lang/RuntimeException"),
8859    ("android/content/ComponentNotFoundException", "java/lang/RuntimeException"),
8860    ("android/content/pm/PackageManager$NameNotFoundException", "java/lang/Exception"),
8861    ("android/content/res/Resources$NotFoundException", "java/lang/RuntimeException"),
8862    ("android/database/SQLException", "java/lang/RuntimeException"),
8863    ("android/database/sqlite/SQLiteException", "android/database/SQLException"),
8864    ("android/database/CursorIndexOutOfBoundsException", "java/lang/IndexOutOfBoundsException"),
8865    ("android/os/NetworkOnMainThreadException", "java/lang/RuntimeException"),
8866    ("android/system/ErrnoException", "java/lang/Exception"),
8867    ("android/provider/Settings$SettingNotFoundException", "android/util/AndroidException"),
8868    ("android/net/UriParseException", "java/lang/RuntimeException"),
8869    ("android/view/InflateException", "java/lang/RuntimeException"),
8870    ("android/accounts/AuthenticatorException", "java/lang/Exception"),
8871    ("android/accounts/OperationCanceledException", "android/accounts/AuthenticatorException"),
8872    ("android/hardware/camera2/CameraAccessException", "java/lang/Exception"),
8873    ("android/opengl/GLException", "java/lang/RuntimeException"),
8874    ("android/renderscript/RSRuntimeException", "java/lang/RuntimeException"),
8875];
8876
8877fn fw_super(c: &str) -> Option<&'static str> {
8878    FW_EXC_SUPERS
8879        .iter()
8880        .find(|(k, _)| *k == c)
8881        .map(|(_, v)| *v)
8882}
8883
8884/// `sub <: sup` for exception-class names, combining the pool
8885/// hierarchy (which also matches framework ancestor NAMES on the way
8886/// up, so pool subclasses of framework exceptions resolve) with the
8887/// tabled framework chains for pairs where both sides are outside the
8888/// dex.
8889fn exc_subtype(sub: &str, sup: &str, pool: &DexPool) -> bool {
8890    if sub == sup {
8891        return true;
8892    }
8893    if pool.is_subtype(sub, sup) {
8894        return true;
8895    }
8896    let mut cur = sub;
8897    for _ in 0..24 {
8898        let Some(next) = fw_super(cur) else { break };
8899        if next == sup {
8900            return true;
8901        }
8902        cur = next;
8903    }
8904    false
8905}
8906
8907/// Drop multi-catch alternatives shadowed by another alternative in
8908/// the same handler (JLS 14.20.2 forbids subtype-related
8909/// alternatives; the dex verifier does not). Exact duplicates collapse
8910/// to the first occurrence. The caught set is unchanged, so this is
8911/// semantics-preserving by construction.
8912pub fn dedupe_multicatch(s: &mut Stmt, pool: &DexPool) {
8913    dedupe_multicatch_walk(s, pool);
8914}
8915
8916fn dedupe_multicatch_walk(s: &mut Stmt, pool: &DexPool) {
8917    match s {
8918        Stmt::Try {
8919            body,
8920            catches,
8921            finally,
8922        }
8923        | Stmt::TryWithResources {
8924            body,
8925            catches,
8926            finally,
8927            ..
8928        } => {
8929            dedupe_multicatch_walk(body, pool);
8930            for c in catches.iter_mut() {
8931                if c.exc.len() > 1 {
8932                    let mut keep: Vec<std::sync::Arc<str>> = Vec::with_capacity(c.exc.len());
8933                    for e in c.exc.iter() {
8934                        let shadowed = c.exc.iter().any(|o| {
8935                            o.as_ref() != e.as_ref() && exc_subtype(e, o, pool)
8936                        });
8937                        if !shadowed && !keep.iter().any(|k| k.as_ref() == e.as_ref()) {
8938                            keep.push(e.clone());
8939                        }
8940                    }
8941                    c.exc = keep;
8942                }
8943                dedupe_multicatch_walk(&mut c.body, pool);
8944            }
8945            if let Some(f) = finally {
8946                dedupe_multicatch_walk(f, pool);
8947            }
8948        }
8949        Stmt::Block(v) => {
8950            for x in v.iter_mut() {
8951                dedupe_multicatch_walk(x, pool);
8952            }
8953        }
8954        Stmt::If {
8955            then_stmt,
8956            else_stmt,
8957            ..
8958        } => {
8959            dedupe_multicatch_walk(then_stmt, pool);
8960            if let Some(e) = else_stmt {
8961                dedupe_multicatch_walk(e, pool);
8962            }
8963        }
8964        Stmt::While { body, .. }
8965        | Stmt::DoWhile { body, .. }
8966        | Stmt::For { body, .. }
8967        | Stmt::ForEach { body, .. }
8968        | Stmt::Labeled { body, .. }
8969        | Stmt::Synchronized { body, .. } => dedupe_multicatch_walk(body, pool),
8970        Stmt::Switch { cases, default, .. } => {
8971            for c in cases.iter_mut() {
8972                for x in c.body.iter_mut() {
8973                    dedupe_multicatch_walk(x, pool);
8974                }
8975            }
8976            if let Some(d) = default {
8977                dedupe_multicatch_walk(d, pool);
8978            }
8979        }
8980        _ => {}
8981    }
8982}
8983
8984/// Dangling `break L<id>`: a Goto whose paired Label/Labeled-wrap was
8985/// lost to structure degradation prints `break L<id>;` against an
8986/// undeclared label ("未定义的标签", weibo 371/lark 85/weixin 535).
8987/// Inside a loop the goto-to-loop-exit shape degrades to a plain
8988/// `break` — compilable, and the dominant original semantic. Outside
8989/// any loop the jump is irreducible flow (weixin protobuf state
8990/// machines: `goto` into a lost loop middle / dispatcher block — the
8991/// target exists but not at a position Java can express); it renders
8992/// as an honest `$DDC:` marker comment instead of a guaranteed
8993/// undefined-label error, and the block's tail after it is dropped
8994/// (the original control flow never reached those statements).
8995pub fn resolve_dangling_gotos(s: &mut Stmt) {
8996    let mut labels: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
8997    let probe = s.clone();
8998    walk_all(&probe, &mut |st| {
8999        if let Stmt::Label(id) = st {
9000            labels.insert(*id);
9001        }
9002    });
9003    resolve_gotos_walk(s, &labels, 0);
9004}
9005
9006fn resolve_gotos_walk(s: &mut Stmt, labels: &jdc_core::FxHashSet<u32>, loop_depth: u32) {
9007    let depth = match s {
9008        Stmt::While { .. } | Stmt::DoWhile { .. } | Stmt::For { .. } | Stmt::ForEach { .. } => {
9009            loop_depth + 1
9010        }
9011        _ => loop_depth,
9012    };
9013    match s {
9014        Stmt::Goto(id) => {
9015            if labels.contains(id) {
9016                return; // valid pair — keep
9017            }
9018            if depth > 0 {
9019                *s = Stmt::Break(None);
9020            } else {
9021                *s = Stmt::Comment(format!(
9022                    "$DDC: unresolved jump to block {} (irreducible control flow)",
9023                    id
9024                ));
9025            }
9026        }
9027        Stmt::Block(v) => {
9028            let mut i = 0usize;
9029            while i < v.len() {
9030                // A depth-0 dangling Goto becomes a marker comment and
9031                // the original flow never reached the siblings after
9032                // it — dropping them keeps the rendered reachability
9033                // faithful (they were dead in the dex too).
9034                let dangling_here =
9035                    depth == 0 && matches!(&v[i], Stmt::Goto(id) if !labels.contains(id));
9036                resolve_gotos_walk(&mut v[i], labels, depth);
9037                if dangling_here {
9038                    v.truncate(i + 1);
9039                    break;
9040                }
9041                i += 1;
9042            }
9043        }
9044        Stmt::If { then_stmt, else_stmt, .. } => {
9045            resolve_gotos_walk(then_stmt, labels, depth);
9046            if let Some(e) = else_stmt {
9047                resolve_gotos_walk(e, labels, depth);
9048            }
9049        }
9050        Stmt::While { body, .. }
9051        | Stmt::DoWhile { body, .. }
9052        | Stmt::For { body, .. }
9053        | Stmt::ForEach { body, .. }
9054        | Stmt::Labeled { body, .. }
9055        | Stmt::Synchronized { body, .. } => resolve_gotos_walk(body, labels, depth),
9056        Stmt::Try {
9057            body,
9058            catches,
9059            finally,
9060        } => {
9061            resolve_gotos_walk(body, labels, depth);
9062            for c in catches.iter_mut() {
9063                resolve_gotos_walk(&mut c.body, labels, depth);
9064            }
9065            if let Some(f) = finally {
9066                resolve_gotos_walk(f, labels, depth);
9067            }
9068        }
9069        Stmt::Switch { cases, default, .. } => {
9070            for c in cases.iter_mut() {
9071                for x in c.body.iter_mut() {
9072                    resolve_gotos_walk(x, labels, depth);
9073                }
9074            }
9075            if let Some(d) = default {
9076                resolve_gotos_walk(d, labels, depth);
9077            }
9078        }
9079        _ => {}
9080    }
9081}
9082
9083/// READS of `v` across the statements (assignment targets excluded).
9084fn stmts_read_var(stmts: &[Stmt], v: u32) -> usize {
9085    let mut n = 0usize;
9086    for s in stmts {
9087        let mut c = s.clone();
9088        walk_stmt_exprs(&mut c, &mut |e| {
9089            deep_rewrite_reads(e, &mut |x| {
9090                if let Expr::Local { var: vv, .. } = x {
9091                    if *vv == v {
9092                        n += 1;
9093                    }
9094                }
9095            });
9096        });
9097    }
9098    n
9099}
9100
9101pub fn fix_ctor_conditional_super(body: &mut Stmt) {
9102    // A single-statement body can arrive UNWRAPPED (bare `If` — the
9103    // gb6/e throw-guard family); the shape scans need a statement list.
9104    if !matches!(body, Stmt::Block(_)) {
9105        let inner = std::mem::replace(body, Stmt::Block(Vec::new()));
9106        *body = Stmt::Block(vec![inner]);
9107    }
9108    let Stmt::Block(stmts) = body else { return };
9109    // Unwrap single-statement Block wrappers: the structurer can nest
9110    // the whole body one level deeper, hiding the top-level If from the
9111    // shape scans (cleanup would flatten it — but runs AFTER this pass).
9112    while stmts.len() == 1 {
9113        let Stmt::Block(inner) = &stmts[0] else { break };
9114        *stmts = inner.clone();
9115    }
9116    if stmts.is_empty() {
9117        return;
9118    }
9119    if is_bare_ctor_call(stmts.first().unwrap()) {
9120        dedupe_ctor_delegations(body);
9121        return;
9122    }
9123    if try_linear_super_inline(stmts) {
9124        return;
9125    }
9126    try_merge_branched_super(stmts);
9127}
9128
9129/// Shape C: the delegation is already first — strip duplicate copies
9130/// left by R8 path-duplication (Kotlin default-arg bridge ctors: ft5/j
9131/// had `super()` leading PLUS another inside a branch — "对super的调用
9132/// 必须是构造器中的第一个语句" at the copy). Every path delegates
9133/// through the leading call, so structurally-identical (or empty-args,
9134/// same-kind) copies nested in blocks/ifs are removed, never moved.
9135/// Runs both ahead of the shape-A/B attempt and AFTER the plain hoist
9136/// (fix_ctor_super_first) — the hoist is what puts the leading
9137/// delegation in place for path-duplicated ctors.
9138/// A ctor that ALREADY delegates in its first statement can hold no
9139/// other `this(..)`/`super(..)` — Baidu Titan hotpatch instrumentation
9140/// injects a conditional re-delegation inside the `if ($ic != null)`
9141/// guard of every ctor (baidusearch ctor-not-first ×61,922 across
9142/// 21,813 files: `super(context); if ($ic != null) { ..; if
9143/// ((flag&1)!=0) { super((Context) callArgs[0]); ..; return; } }`).
9144/// The guard is dead code on the unpatched runtime path (and the
9145/// decompiled output has no patch runtime at all); dropping the
9146/// secondary delegation keeps the real semantics and restores the
9147/// Java first-statement rule. Runs after dedupe_ctor_delegations
9148/// (identical copies) and before extract_branched_delegation_helper
9149/// — gated on a LEADING delegation, so branched-delegation ctors
9150/// (no leading one) still get the helper treatment. Nested class
9151/// declarations are separate ctor scopes and are not entered.
9152pub fn strip_secondary_delegations(body: &mut Stmt) {
9153    let Stmt::Block(stmts) = body else { return };
9154    if !stmts.first().is_some_and(is_bare_ctor_call) {
9155        return;
9156    }
9157    fn strip_in(s: &mut Stmt) {
9158        if is_bare_ctor_call(s) {
9159            *s = Stmt::Block(Vec::new());
9160            return;
9161        }
9162        match s {
9163            Stmt::Block(v) => v.iter_mut().for_each(strip_in),
9164            Stmt::If {
9165                then_stmt,
9166                else_stmt,
9167                ..
9168            } => {
9169                strip_in(then_stmt);
9170                if let Some(e) = else_stmt {
9171                    strip_in(e);
9172                }
9173            }
9174            Stmt::While { body, .. }
9175            | Stmt::DoWhile { body, .. }
9176            | Stmt::ForEach { body, .. }
9177            | Stmt::Synchronized { body, .. }
9178            | Stmt::Labeled { body, .. } => strip_in(body),
9179            Stmt::For { init, body, .. } => {
9180                init.iter_mut().for_each(strip_in);
9181                strip_in(body);
9182            }
9183            Stmt::Switch { cases, default, .. } => {
9184                for c in cases.iter_mut() {
9185                    c.body.iter_mut().for_each(strip_in);
9186                }
9187                if let Some(d) = default {
9188                    strip_in(d);
9189                }
9190            }
9191            Stmt::Try {
9192                body,
9193                catches,
9194                finally,
9195            }
9196            | Stmt::TryWithResources {
9197                body,
9198                catches,
9199                finally,
9200                ..
9201            } => {
9202                strip_in(body);
9203                for c in catches.iter_mut() {
9204                    strip_in(&mut c.body);
9205                }
9206                if let Some(f) = finally {
9207                    strip_in(f);
9208                }
9209            }
9210            _ => {}
9211        }
9212    }
9213    for st in stmts.iter_mut().skip(1) {
9214        strip_in(st);
9215    }
9216}
9217
9218pub fn dedupe_ctor_delegations(body: &mut Stmt) {
9219    let Stmt::Block(stmts) = body else { return };
9220    let Some(d0) = stmts.first().and_then(|s| match s {
9221        Stmt::ExprStmt(e) if is_delegation_expr(e) => Some(e.clone()),
9222        _ => None,
9223    }) else {
9224        return;
9225    };
9226    fn rec(s: &mut Stmt, d0: &Expr) {
9227        match s {
9228            Stmt::Block(v) => {
9229                v.retain(|x| {
9230                    !matches!(x, Stmt::ExprStmt(e) if is_delegation_expr(e) && same_delegation(e, d0))
9231                });
9232                for x in v.iter_mut() {
9233                    rec(x, d0);
9234                }
9235            }
9236            Stmt::If { then_stmt, else_stmt, .. } => {
9237                rec(then_stmt, d0);
9238                if let Some(e) = else_stmt {
9239                    rec(e, d0);
9240                }
9241            }
9242            _ => {}
9243        }
9244    }
9245    // Top-level copies first (rec only strips copies NESTED in
9246    // blocks/ifs — a duplicate `super()` sitting directly in the body
9247    // list fell through its `_` arm: weixin ft5/j's second super()).
9248    {
9249        let d0r = &d0;
9250        let mut idx = 0usize;
9251        stmts.retain(|s| {
9252            idx += 1;
9253            idx == 1
9254                || !(matches!(s, Stmt::ExprStmt(e)
9255                    if is_delegation_expr(e) && same_delegation(e, d0r)))
9256        });
9257    }
9258    for s in stmts.iter_mut().skip(1) {
9259        rec(s, &d0);
9260    }
9261}
9262
9263/// Structural delegation identity for dedup: same kind/target and equal
9264/// args — or both arg-less (`super()` copies across duplicated paths
9265/// render with locally-renamed args yet delegate identically).
9266fn same_delegation(a: &Expr, b: &Expr) -> bool {
9267    match (a, b) {
9268        (
9269            Expr::Method { name: n1, cls: c1, is_super: s1, args: a1, .. },
9270            Expr::Method { name: n2, cls: c2, is_super: s2, args: a2, .. },
9271        ) => {
9272            &**n1 == "<init>"
9273                && &**n2 == "<init>"
9274                && c1 == c2
9275                && s1 == s2
9276                && (a1 == a2 || (a1.is_empty() && a2.is_empty()))
9277        }
9278        _ => false,
9279    }
9280}
9281
9282/// The `Method` expr of a bare ctor-delegation statement.
9283fn ctor_call_expr(s: &Stmt) -> Option<&Expr> {
9284    match s {
9285        Stmt::ExprStmt(e) if is_delegation_expr(e) => Some(e),
9286        _ => None,
9287    }
9288}
9289
9290/// Same delegation kind ignoring args (super vs this, same target).
9291fn same_ctor_kind(a: &Expr, b: &Expr) -> bool {
9292    match (a, b) {
9293        (
9294            Expr::Method { cls: c1, is_super: s1, owner: o1, .. },
9295            Expr::Method { cls: c2, is_super: s2, owner: o2, .. },
9296        ) => c1 == c2 && s1 == s2 && o1 == o2,
9297        _ => false,
9298    }
9299}
9300
9301/// Statements of a branch body, recursively flattening pure Blocks —
9302/// the fix runs BEFORE cleanup, so structurer-emitted nesting is still
9303/// in place. Any non-Block statement (If/Try/...) stays an item and
9304/// fails the def/call classification, which is the intended abort.
9305fn flat_list(s: &Stmt) -> Vec<&Stmt> {
9306    fn rec<'a>(s: &'a Stmt, out: &mut Vec<&'a Stmt>) {
9307        match s {
9308            Stmt::Block(v) => {
9309                for x in v {
9310                    rec(x, out);
9311                }
9312            }
9313            other => out.push(other),
9314        }
9315    }
9316    let mut out = Vec::new();
9317    rec(s, &mut out);
9318    out
9319}
9320
9321/// Blank the OWNER of un-folded `<init>` calls (lost allocation): the
9322/// printer renders them as `new X(..)` and never reads the owner, so its
9323/// register reference is not a real use (v7.a$a2: the dead
9324/// `new f(0,..)`'s owner was v1's register, inflating v1's use count
9325/// and blocking the inline-hoist).
9326fn strip_lost_alloc_owners(e: &mut Expr) {
9327    deep_rewrite(e, &mut |x| {
9328        if let Expr::Method { name, is_special: true, owner: Some(o), .. } = x {
9329            if &**name == "<init>" && !matches!(**o, Expr::This) {
9330                **o = Expr::Const(jdc_core::ir::expr::ConstVal::Int(0));
9331            }
9332        }
9333    });
9334}
9335
9336/// Per-var `Local` occurrence counts over an expression (single pass;
9337/// lost-alloc `<init>` owners excluded — see strip_lost_alloc_owners).
9338fn count_locals_expr(e: &Expr) -> std::collections::HashMap<u32, usize> {
9339    let mut m = std::collections::HashMap::new();
9340    let mut probe = e.clone();
9341    strip_lost_alloc_owners(&mut probe);
9342    deep_rewrite(&mut probe, &mut |x| {
9343        if let Expr::Local { var: v, .. } = x {
9344            *m.entry(*v).or_insert(0) += 1;
9345        }
9346    });
9347    m
9348}
9349
9350pub(crate) fn count_locals_stmts(ss: &[Stmt]) -> std::collections::HashMap<u32, usize> {
9351    let mut m = std::collections::HashMap::new();
9352    for s in ss {
9353        let mut c = s.clone();
9354        walk_stmt_exprs(&mut c, &mut |e| {
9355            strip_lost_alloc_owners(e);
9356            deep_rewrite(e, &mut |x| {
9357                if let Expr::Local { var: v, .. } = x {
9358                    *m.entry(*v).or_insert(0) += 1;
9359                }
9360            });
9361        });
9362    }
9363    m
9364}
9365
9366fn cnt(m: &std::collections::HashMap<u32, usize>, v: u32) -> usize {
9367    m.get(&v).copied().unwrap_or(0)
9368}
9369
9370/// Substitute map-referenced locals, recursively expanding def chains
9371/// (depth-capped). Returns None when a referenced var has no def or the
9372/// chain is too deep.
9373fn inline_locals(
9374    e: &Expr,
9375    map: &std::collections::HashMap<u32, Expr>,
9376    depth: u32,
9377) -> Option<Expr> {
9378    if depth > 4 {
9379        return None;
9380    }
9381    let mut out = e.clone();
9382    let mut fail = false;
9383    deep_rewrite(&mut out, &mut |x| {
9384        if let Expr::Local { var, .. } = x {
9385            if let Some(def) = map.get(var) {
9386                match inline_locals(def, map, depth + 1) {
9387                    Some(r) => *x = r,
9388                    None => fail = true,
9389                }
9390            }
9391        }
9392    });
9393    if fail {
9394        None
9395    } else {
9396        Some(out)
9397    }
9398}
9399
9400/// A leading statement that defines a local: (var, init expr).
9401fn def_of(s: &Stmt) -> Option<(u32, &Expr)> {
9402    match s {
9403        Stmt::LocalDef { var, init: Some(e), .. } => Some((*var, e)),
9404        Stmt::ExprStmt(Expr::Assign {
9405            target,
9406            value,
9407            op: jdc_core::ir::expr::AssignOp::Plain,
9408            ..
9409        }) => match &**target {
9410            Expr::Local { var, .. } => Some((*var, value)),
9411            _ => None,
9412        },
9413        _ => None,
9414    }
9415}
9416
9417/// Shape A: straight-line prefix, delegation references prefix locals.
9418fn try_linear_super_inline(stmts: &mut Vec<Stmt>) -> bool {
9419    let Some(pos) = stmts.iter().position(is_bare_ctor_call) else {
9420        return false;
9421    };
9422    if pos == 0 {
9423        return false; // already first — nothing to do
9424    }
9425    // The prefix must be straight-line.
9426    if !stmts[..pos]
9427        .iter()
9428        .all(|s| matches!(s, Stmt::LocalDef { .. } | Stmt::ExprStmt(_)))
9429    {
9430        return false;
9431    }
9432    let Some(call) = ctor_call_expr(&stmts[pos]) else {
9433        return false;
9434    };
9435    let Expr::Method { args, .. } = call else {
9436        return false;
9437    };
9438    // Defs reachable from the args (last definition wins, bytecode order).
9439    let mut map: std::collections::HashMap<u32, Expr> =
9440        std::collections::HashMap::new();
9441    let mut def_idx: std::collections::HashMap<u32, usize> =
9442        std::collections::HashMap::new();
9443    for (i, s) in stmts[..pos].iter().enumerate() {
9444        if let Some((v, e)) = def_of(s) {
9445            map.insert(v, e.clone());
9446            def_idx.insert(v, i);
9447        }
9448    }
9449    // Which arg locals need inlining?
9450    let mut needed: Vec<u32> = Vec::new();
9451    for a in args {
9452        let mut probe = a.clone();
9453        strip_lost_alloc_owners(&mut probe);
9454        deep_rewrite(&mut probe, &mut |x| {
9455            if let Expr::Local { var, .. } = x {
9456                if map.contains_key(var) && !needed.contains(var) {
9457                    needed.push(*var);
9458                }
9459            }
9460        });
9461    }
9462    if needed.is_empty() {
9463        return false; // plain hoist handles it
9464    }
9465    // Guard: side-effecting defs inline only at a provably preserved
9466    // call count (exactly one use, the arg site).
9467    let mut consumed: Vec<usize> = Vec::new();
9468    let use_counts = count_locals_stmts(stmts);
9469    for &v in &needed {
9470        let def = &map[&v];
9471        let uses = cnt(&use_counts, v);
9472        if jdc_core::ir::build::has_side_effects(def) && uses != 1 {
9473            return false;
9474        }
9475        if uses == 1 {
9476            consumed.push(def_idx[&v]);
9477        }
9478    }
9479    // Build the inlined args.
9480    let mut new_args: Vec<Expr> = Vec::with_capacity(args.len());
9481    for a in args {
9482        match inline_locals(a, &map, 0) {
9483            Some(r) => new_args.push(r),
9484            None => return false,
9485        }
9486    }
9487    // Rebuild: super first (with inlined args), the unconsumed prefix
9488    // statements keep their order after it, then the original rest.
9489    let mut call_stmt = stmts.remove(pos);
9490    if let Stmt::ExprStmt(Expr::Method { args: slot, .. }) = &mut call_stmt {
9491        *slot = new_args;
9492    }
9493    let mut rest: Vec<Stmt> = Vec::with_capacity(stmts.len() + 1);
9494    rest.push(call_stmt);
9495    for (i, s) in stmts.drain(..).enumerate() {
9496        // Indices shift after `remove(pos)`: consumed holds pre-removal
9497        // indices; pos itself is already gone from `stmts`.
9498        let orig = if i >= pos { i + 1 } else { i };
9499        if !consumed.contains(&orig) {
9500            rest.push(s);
9501        }
9502    }
9503    *stmts = rest;
9504    true
9505}
9506
9507/// Shape B: if/else branches each ending in the same-kind delegation.
9508fn try_merge_branched_super(stmts: &mut Vec<Stmt>) -> bool {
9509    // Candidate If positions — the prelude before the chosen If must be
9510    // straight-line; try each If until one merges (nested/multi-if
9511    // ctors exist but the first matching one is the delegation site).
9512    let if_positions: Vec<usize> = stmts
9513        .iter()
9514        .enumerate()
9515        .filter(|(_, s)| matches!(s, Stmt::If { .. }))
9516        .map(|(i, _)| i)
9517        .collect();
9518    for if_pos in if_positions {
9519        // Cheap allocation-free gate: at least one branch must hold a
9520        // delegation (most ctor ifs are field null-checks and must not
9521        // pay the merge machinery). The OTHER side may be delegation-free
9522        // (throw-guard shape D) or absent (then-only shape E — the
9523        // rendered if/else form often only exists after later passes).
9524        let gate = match &stmts[if_pos] {
9525            Stmt::If { then_stmt, else_stmt, .. } => {
9526                contains_delegation(then_stmt)
9527                    || else_stmt.as_ref().is_some_and(|e| contains_delegation(e))
9528            }
9529            _ => false,
9530        };
9531        if gate && merge_at(stmts, if_pos) {
9532            return true;
9533        }
9534    }
9535    false
9536}
9537
9538/// Does this subtree contain a ctor delegation? (module-level: shared by
9539/// the merge gate and split_branch)
9540fn contains_delegation(s: &Stmt) -> bool {
9541    match s {
9542        Stmt::ExprStmt(e) => is_delegation_expr(e),
9543        Stmt::Block(v) => v.iter().any(contains_delegation),
9544        Stmt::If { then_stmt, else_stmt, .. } => {
9545            contains_delegation(then_stmt)
9546                || else_stmt.as_ref().is_some_and(|e| contains_delegation(e))
9547        }
9548        _ => false,
9549    }
9550}
9551
9552
9553
9554/// One merge attempt at `stmts[if_pos]`. See fix_ctor_conditional_super
9555/// for the doctrine. Extensions over the pure two-call merge:
9556/// - branch pre-call SIDE-EFFECT statements (Kotlin `o.h(param,..)`
9557///   null-checks — the zz5/j0 family) are allowed; they move into the
9558///   branch remainder (after super — the same reordering doctrine the
9559///   plain hoist already applies; Java has no pre-super statement form).
9560/// - a branch WITHOUT a delegation (throw-guard: `if (x==null) throw
9561///   .. else { super(..); .. }` — the gb6/e family) merges as pure
9562///   remainder; exactly one branch carries the call then, and no
9563///   ternary args arise (the guard throws before any path needs them).
9564/// - prelude single-assignment if/else (`if (c) {v=e1} else {v=e2}`)
9565///   folds into a Cond def for inlining (the zz5/j0 v15/v16 preludes).
9566fn merge_at(stmts: &mut Vec<Stmt>, if_pos: usize) -> bool {
9567    /// Branch classification: Call(at, defs, pre-call extra idxs) when a
9568    /// delegation sits behind only defs/side-effect statements; Clean
9569    /// when the branch has no delegation at all (pure remainder); Dirty
9570    /// otherwise (nested/conditional delegation — out of scope).
9571    enum Br {
9572        /// (call index, pre-call defs, def statement indices)
9573        Call(usize, Vec<(u32, Expr)>, Vec<usize>),
9574        Clean,
9575        Dirty,
9576    }
9577    fn split_branch(list: &[&Stmt]) -> Br {
9578        let call_pos = list.iter().position(|s| ctor_call_expr(s).is_some());
9579        let Some(c) = call_pos else {
9580            // No delegation at this level: a pure remainder branch
9581            // (throw-guard) unless one hides in nested control flow.
9582            return if list.iter().any(|s| contains_delegation(s)) {
9583                Br::Dirty
9584            } else {
9585                Br::Clean
9586            };
9587        };
9588        if list.iter().skip(c + 1).any(|s| ctor_call_expr(s).is_some()) {
9589            return Br::Dirty; // two delegations in one branch
9590        }
9591        let mut defs: Vec<(u32, Expr)> = Vec::new();
9592        let mut def_idxs: Vec<usize> = Vec::new();
9593        for (i, s) in list.iter().enumerate().take(c) {
9594            match def_of(s) {
9595                Some((v, e)) => {
9596                    defs.push((v, e.clone()));
9597                    def_idxs.push(i);
9598                }
9599                None => match s {
9600                    // Side-effect statements and bare decls ahead of the
9601                    // delegation ride along into the remainder.
9602                    Stmt::ExprStmt(_) | Stmt::LocalDef { .. } => {}
9603                    _ => return Br::Dirty, // control flow pre-call
9604                },
9605            }
9606        }
9607        Br::Call(c, defs, def_idxs)
9608    }
9609    /// Prelude single-assignment if/else → `v = c ? e1 : e2` def.
9610    fn cond_def_of(s: &Stmt, scope: &[Stmt]) -> Option<(u32, Expr)> {
9611        let Stmt::If { cond, then_stmt, else_stmt: Some(e), .. } = s else {
9612            return None;
9613        };
9614        fn one<'x>(b: &'x Stmt, scope: &[Stmt]) -> Option<(u32, &'x Expr)> {
9615            let l = flat_list(b);
9616            if l.len() == 1 {
9617                return def_of(l[0]);
9618            }
9619            // A [w = E; v = w] forwarding PAIR — the shape the
9620            // structurer leaves when E is impure (`c v5 = new c(..);
9621            // v6 = v5`) and forward_single_use's adjacency pass missed
9622            // it (a duplicated read elsewhere in its raw body broke the
9623            // single-read gate). Collapse to `v = E`, gated on w being
9624            // method-unique: the merge DROPS this branch, so any other
9625            // read/write of w would dangle (lark gp2/d Kotlin default-
9626            // arg bridge, this-not-first family).
9627            if l.len() == 2 {
9628                let (w, ex) = def_of(l[0])?;
9629                let Stmt::ExprStmt(Expr::Assign {
9630                    target,
9631                    op: AssignOp::Plain,
9632                    value,
9633                }) = l[1]
9634                else {
9635                    return None;
9636                };
9637                let Expr::Local { var: tv, .. } = &**target else {
9638                    return None;
9639                };
9640                let Expr::Local { var: rv, .. } = &**value else {
9641                    return None;
9642                };
9643                if *rv != w || *tv == w {
9644                    return None;
9645                }
9646                // stmts_count_writes only sees Expr-level assigns; the
9647                // pair's own def is often an init-bearing LocalDef, so
9648                // count those separately for the uniqueness gate.
9649                let mut ldef_writes = 0usize;
9650                for st in scope {
9651                    walk_all(st, &mut |x| {
9652                        if let Stmt::LocalDef {
9653                            var,
9654                            init: Some(_),
9655                            ..
9656                        } = x
9657                        {
9658                            if *var == w {
9659                                ldef_writes += 1;
9660                            }
9661                        }
9662                    });
9663                }
9664                if stmts_read_var(scope, w) != 1
9665                    || stmts_count_writes(scope, w) + ldef_writes != 1
9666                {
9667                    return None;
9668                }
9669                return Some((*tv, ex));
9670            }
9671            None
9672        }
9673        let (v1, e1) = one(then_stmt, scope)?;
9674        let (v2, e2) = one(e, scope)?;
9675        if v1 != v2 {
9676            return None;
9677        }
9678        Some((
9679            v1,
9680            Expr::Cond {
9681                c: Box::new(cond.clone()),
9682                t: Box::new(e1.clone()),
9683                f: Box::new(e2.clone()),
9684            },
9685        ))
9686    }
9687
9688    // Prelude: defs (flat or folded cond), bare decls, side-effect stmts.
9689    let mut pmap: std::collections::HashMap<u32, Expr> =
9690        std::collections::HashMap::new();
9691    let mut pidx: std::collections::HashMap<u32, usize> =
9692        std::collections::HashMap::new();
9693    for (i, s) in stmts[..if_pos].iter().enumerate() {
9694        match s {
9695            Stmt::LocalDef { .. } | Stmt::ExprStmt(_) => {
9696                if let Some((v, e)) = def_of(s) {
9697                    pmap.insert(v, e.clone());
9698                    pidx.insert(v, i);
9699                }
9700            }
9701            Stmt::If { .. } => match cond_def_of(s, stmts) {
9702                Some((v, e)) => {
9703                    pmap.insert(v, e);
9704                    pidx.insert(v, i);
9705                }
9706                None => return false,
9707            },
9708            _ => return false,
9709        }
9710    }
9711    let (cond, then_v, else_v, had_else) = match &stmts[if_pos] {
9712        Stmt::If { cond, then_stmt, else_stmt, .. } => (
9713            cond.clone(),
9714            then_stmt.as_ref().clone(),
9715            else_stmt.as_ref().map_or_else(|| Stmt::Block(Vec::new()), |e| e.as_ref().clone()),
9716            else_stmt.is_some(),
9717        ),
9718        _ => return false,
9719    };
9720    // Chained guards (Kotlin multi-param null-checks):
9721    // `if(a){throw..} else { if(b){throw..} else { super(..); .. } }` —
9722    // a branch that is a lone delegation-bearing If gets merged
9723    // recursively first (its delegation hoisted to the branch head), so
9724    // the outer split then sees [call, nested-guard-remainder].
9725    fn normalize_branch(b: &mut Stmt) {
9726        // MOVE-based (no deep clone): chained weixin Parcel ctors carry
9727        // huge tails; cloning per recursion level cost measurable wall
9728        // time. merge_at leaves stmts untouched on failure, so the taken
9729        // node can always go back.
9730        let slot: &mut Stmt = match b {
9731            Stmt::Block(v) if v.len() == 1 => &mut v[0],
9732            other => other,
9733        };
9734        if !matches!(slot, Stmt::If { .. }) || !contains_delegation(slot) {
9735            return;
9736        }
9737        let taken = std::mem::replace(slot, Stmt::Block(Vec::new()));
9738        let mut tmp = vec![taken];
9739        if merge_at(&mut tmp, 0) {
9740            *slot = Stmt::Block(tmp);
9741        } else {
9742            *slot = tmp.pop().unwrap();
9743        }
9744    }
9745    let mut then_v = then_v;
9746    let mut else_v = else_v;
9747    normalize_branch(&mut then_v);
9748    normalize_branch(&mut else_v);
9749    let then_list = flat_list(&then_v);
9750    let else_list = flat_list(&else_v);
9751
9752    // Exactly: two Call branches (merge with ternaries) or one Call +
9753    // one Clean (single-delegation throw-guard). Owned branch payloads.
9754    enum Side {
9755        Call {
9756            at: usize,
9757            defs: Vec<(u32, Expr)>,
9758            def_idxs: Vec<usize>,
9759        },
9760        Clean,
9761    }
9762    let (t_side, e_side, two_sided) = match (split_branch(&then_list), split_branch(&else_list)) {
9763        (Br::Call(tc, td, ti), Br::Call(ec, ed, ei)) => (
9764            Side::Call { at: tc, defs: td, def_idxs: ti },
9765            Side::Call { at: ec, defs: ed, def_idxs: ei },
9766            true,
9767        ),
9768        (Br::Call(tc, td, ti), Br::Clean) => (
9769            Side::Call { at: tc, defs: td, def_idxs: ti },
9770            Side::Clean,
9771            false,
9772        ),
9773        (Br::Clean, Br::Call(ec, ed, ei)) => (
9774            Side::Clean,
9775            Side::Call { at: ec, defs: ed, def_idxs: ei },
9776            false,
9777        ),
9778        _ => return false,
9779    };
9780    // The delegation nodes (two-sided: both; single: one).
9781    let (t_call, e_call) = match (&t_side, &e_side) {
9782        (Side::Call { at: ta, .. }, Side::Call { at: ea, .. }) => (
9783            ctor_call_expr(then_list[*ta]),
9784            ctor_call_expr(else_list[*ea]),
9785        ),
9786        (Side::Call { at: ta, .. }, Side::Clean) => {
9787            (ctor_call_expr(then_list[*ta]), None)
9788        }
9789        (Side::Clean, Side::Call { at: ea, .. }) => {
9790            (None, ctor_call_expr(else_list[*ea]))
9791        }
9792        _ => return false,
9793    };
9794    // The node that becomes the merged call: then side when present.
9795    let (call_list, call_at) = match &t_side {
9796        Side::Call { at, .. } => (&then_list, *at),
9797        Side::Clean => match &e_side {
9798            Side::Call { at, .. } => (&else_list, *at),
9799            Side::Clean => return false,
9800        },
9801    };
9802    let (base_args, other_args) = if two_sided {
9803        let (Some(tc), Some(ec)) = (t_call, e_call) else {
9804            return false;
9805        };
9806        if !same_ctor_kind(tc, ec) {
9807            return false;
9808        }
9809        let (Expr::Method { args: a1, .. }, Expr::Method { args: a2, .. }) = (tc, ec) else {
9810            return false;
9811        };
9812        if a1.len() != a2.len() {
9813            return false;
9814        }
9815        (a1, Some(a2))
9816    } else {
9817        let c = t_call.or(e_call);
9818        let Some(c) = c else { return false };
9819        let Expr::Method { args, .. } = c else {
9820            return false;
9821        };
9822        (args, None)
9823    };
9824
9825    // Def maps per side (prelude + own branch defs).
9826    let mut tmap: std::collections::HashMap<u32, Expr> = pmap.clone();
9827    let mut emap: std::collections::HashMap<u32, Expr> = pmap.clone();
9828    if let Side::Call { defs, .. } = &t_side {
9829        for (v, e) in defs {
9830            tmap.insert(*v, e.clone());
9831        }
9832    }
9833    if let Side::Call { defs, .. } = &e_side {
9834        for (v, e) in defs {
9835            emap.insert(*v, e.clone());
9836        }
9837    }
9838    let Some(cond_i) = inline_locals(&cond, &pmap, 0) else {
9839        return false;
9840    };
9841    // Inline each side's args through its own map.
9842    let mut then_args: Vec<Expr> = Vec::with_capacity(base_args.len());
9843    let mut else_args: Vec<Expr> = Vec::new();
9844    if two_sided {
9845        for a in base_args {
9846            match inline_locals(a, &tmap, 0) {
9847                Some(r) => then_args.push(r),
9848                None => return false,
9849            }
9850        }
9851        for a in other_args.unwrap() {
9852            match inline_locals(a, &emap, 0) {
9853                Some(r) => else_args.push(r),
9854                None => return false,
9855            }
9856        }
9857    } else {
9858        // Single-delegation side: base_args belong to whichever side
9859        // carries the call; pick that side's map.
9860        let from_then = matches!(&t_side, Side::Call { .. });
9861        for a in base_args {
9862            let r = if from_then {
9863                inline_locals(a, &tmap, 0)
9864            } else {
9865                inline_locals(a, &emap, 0)
9866            };
9867            match r {
9868                Some(r) => then_args.push(r),
9869                None => return false,
9870            }
9871        }
9872    }
9873    let primary_args = base_args;
9874    // Per-position merge.
9875    let mut merged: Vec<Expr> = Vec::with_capacity(then_args.len());
9876    let mut differ = 0usize;
9877    for (j, t) in then_args.iter().enumerate() {
9878        if two_sided {
9879            let e = &else_args[j];
9880            if t == e {
9881                merged.push(t.clone());
9882            } else {
9883                differ += 1;
9884                merged.push(Expr::Cond {
9885                    c: Box::new(cond_i.clone()),
9886                    t: Box::new(t.clone()),
9887                    f: Box::new(e.clone()),
9888                });
9889            }
9890        } else {
9891            merged.push(t.clone());
9892        }
9893    }
9894
9895    // ---- side-effect preservation guards (batched counts) ----
9896    let vars_in = |e: &Expr| -> Vec<u32> {
9897        let mut vs = Vec::new();
9898        let mut probe = e.clone();
9899        strip_lost_alloc_owners(&mut probe);
9900        deep_rewrite(&mut probe, &mut |x| {
9901            if let Expr::Local { var, .. } = x {
9902                if !vs.contains(var) {
9903                    vs.push(*var);
9904                }
9905            }
9906        });
9907        vs
9908    };
9909    // Remainder statements per branch (extras + post-call tail; a Clean
9910    // branch contributes all of its statements).
9911    // The consumed defs LEAVE the remainder: their expressions were
9912    // inlined into the merged delegation args, and a dead `v = E` left
9913    // after the call both reads wrong and trips the u_t1/u_t2 guards
9914    // (ContinuationImpl's branch-local `context = null / getContext()`
9915    // — the coroutine family's 对this的调用必须是第一个语句 root).
9916    let branch_remainder = |list: &[&Stmt], side: &Side| -> Vec<Stmt> {
9917        match side {
9918            Side::Clean => list.iter().map(|s| (*s).clone()).collect(),
9919            Side::Call { at, def_idxs, .. } => {
9920                list.iter()
9921                    .enumerate()
9922                    .filter(|(i, _)| *i != *at && !def_idxs.contains(i))
9923                    .map(|(_, s)| (*s).clone())
9924                    .collect()
9925            }
9926        }
9927    };
9928    let tail_then_v = branch_remainder(&then_list, &t_side);
9929    let tail_else_v = branch_remainder(&else_list, &e_side);
9930    let post = &stmts[if_pos + 1..];
9931    let mut inlined: Vec<u32> = vars_in(&cond);
9932    for a in primary_args.iter().chain(else_args.iter()) {
9933        for v in vars_in(a) {
9934            if !inlined.contains(&v) {
9935                inlined.push(v);
9936            }
9937        }
9938    }
9939    inlined.retain(|v| pmap.contains_key(v) || tmap.contains_key(v) || emap.contains_key(v));
9940    // Prelude-and-branch redefinition: ambiguous provenance — reject.
9941    for &v in &inlined {
9942        let in_branch = match (&t_side, &e_side) {
9943            (Side::Call { defs: td, .. }, Side::Call { defs: ed, .. }) => {
9944                td.iter().any(|(d, _)| *d == v) || ed.iter().any(|(d, _)| *d == v)
9945            }
9946            (Side::Call { defs, .. }, Side::Clean)
9947            | (Side::Clean, Side::Call { defs, .. }) => {
9948                defs.iter().any(|(d, _)| *d == v)
9949            }
9950            _ => false,
9951        };
9952        if pmap.contains_key(&v) && in_branch {
9953            return false;
9954        }
9955    }
9956    type Counts = std::collections::HashMap<u32, usize>;
9957    type GuardCounts = (
9958        Counts,
9959        Vec<Counts>,
9960        Vec<Counts>,
9961        Counts,
9962        Counts,
9963        Counts,
9964        Vec<(u32, Counts)>,
9965    );
9966    let (cond_counts, a1_counts, a2_counts, tail1_counts, tail2_counts, post_counts, def_counts): GuardCounts =
9967        if inlined.is_empty() {
9968        Default::default()
9969    } else {
9970        let all_defs: Vec<(u32, &Expr)> = pmap
9971            .iter()
9972            .map(|(k, v)| (*k, v))
9973            .chain(match &t_side {
9974                Side::Call { defs, .. } => defs.iter().map(|(k, v)| (*k, v)).collect(),
9975                Side::Clean => Vec::new(),
9976            })
9977            .chain(match &e_side {
9978                Side::Call { defs, .. } => defs.iter().map(|(k, v)| (*k, v)).collect(),
9979                Side::Clean => Vec::new(),
9980            })
9981            .collect();
9982        (
9983            count_locals_expr(&cond),
9984            primary_args.iter().map(count_locals_expr).collect(),
9985            else_args.iter().map(count_locals_expr).collect(),
9986            count_locals_stmts(&tail_then_v),
9987            count_locals_stmts(&tail_else_v),
9988            count_locals_stmts(post),
9989            all_defs.into_iter().map(|(v, e)| (v, count_locals_expr(e))).collect(),
9990        )
9991        };
9992    for &v in &inlined {
9993        let in_t = matches!(&t_side, Side::Call { defs, .. } if defs.iter().any(|(d, _)| *d == v));
9994        let in_e = matches!(&e_side, Side::Call { defs, .. } if defs.iter().any(|(d, _)| *d == v));
9995        let effectful = [
9996            in_t.then(|| tmap.get(&v)).flatten(),
9997            in_e.then(|| emap.get(&v)).flatten(),
9998            pmap.get(&v),
9999        ]
10000        .into_iter()
10001        .flatten()
10002        .any(jdc_core::ir::build::has_side_effects);
10003        if !effectful {
10004            continue;
10005        }
10006        let u_cond = cnt(&cond_counts, v);
10007        let u_post = cnt(&post_counts, v);
10008        let u_t1 = cnt(&tail1_counts, v);
10009        let u_t2 = cnt(&tail2_counts, v);
10010        let mut u_odef = 0usize;
10011        for (dv, dc) in &def_counts {
10012            if *dv != v && inlined.contains(dv) {
10013                u_odef += cnt(dc, v);
10014            }
10015        }
10016        let mut emit = u_cond * (1 + differ);
10017        let mut uses_args = 0usize;
10018        for j in 0..primary_args.len() {
10019            let c1 = cnt(&a1_counts[j], v);
10020            let c2 = if two_sided && j < a2_counts.len() {
10021                cnt(&a2_counts[j], v)
10022            } else {
10023                0
10024            };
10025            uses_args += c1 + c2;
10026            emit += if two_sided && then_args[j] != else_args[j] {
10027                c1 + c2
10028            } else {
10029                c1.max(c2)
10030            };
10031        }
10032        let ok = if in_t || in_e {
10033            u_cond == 0
10034                && u_post == 0
10035                && u_t1 == 0
10036                && u_t2 == 0
10037                && u_odef == 0
10038                && a1_counts.iter().map(|c| cnt(c, v)).sum::<usize>() <= 1
10039                && a2_counts.iter().map(|c| cnt(c, v)).sum::<usize>() <= 1
10040                && emit <= 2
10041                && (in_t || a1_counts.iter().all(|c| cnt(c, v) == 0))
10042                && (in_e || a2_counts.iter().all(|c| cnt(c, v) == 0))
10043        } else {
10044            u_post == 0
10045                && u_t1 == 0
10046                && u_t2 == 0
10047                && u_odef == 0
10048                && ((u_cond == 1 && uses_args == 0 && emit <= 2)
10049                    || (u_cond == 0 && emit <= 1))
10050        };
10051        if !ok {
10052            return false;
10053        }
10054    }
10055
10056    // ---- assembly ----
10057    let mut call_stmt = call_list[call_at].clone();
10058    if let Stmt::ExprStmt(Expr::Method { args: slot, .. }) = &mut call_stmt {
10059        *slot = merged;
10060    }
10061    let residual = |v: u32| -> usize {
10062        cnt(&tail1_counts, v) + cnt(&tail2_counts, v) + cnt(&post_counts, v)
10063    };
10064    let mut kept_prelude: Vec<Stmt> = Vec::new();
10065    for (i, s) in stmts[..if_pos].iter().enumerate() {
10066        let consumed = match s {
10067            Stmt::LocalDef { var, .. } => Some(*var),
10068            Stmt::ExprStmt(Expr::Assign { target, .. }) => match &**target {
10069                Expr::Local { var, .. } => Some(*var),
10070                _ => None,
10071            },
10072            Stmt::If { .. } => cond_def_of(s, stmts).map(|(v, _)| v),
10073            _ => None,
10074        };
10075        if let Some(v) = consumed {
10076            if inlined.contains(&v) && pidx.get(&v) == Some(&i) && residual(v) == 0 {
10077                continue;
10078            }
10079        }
10080        kept_prelude.push(s.clone());
10081    }
10082    let build_keep = |list: &[&Stmt], side: &Side| -> Vec<Stmt> {
10083        match side {
10084            Side::Clean => list.iter().map(|s| (*s).clone()).collect(),
10085            Side::Call { at, .. } => {
10086                let mut out = Vec::new();
10087                for (i, s) in list.iter().enumerate() {
10088                    if i == *at {
10089                        continue;
10090                    }
10091                    if i < *at {
10092                        if let Some((v, _)) = def_of(s) {
10093                            if inlined.contains(&v) && residual(v) == 0 {
10094                                continue;
10095                            }
10096                        }
10097                    }
10098                    out.push((*s).clone());
10099                }
10100                out
10101            }
10102        }
10103    };
10104    let then_keep = build_keep(&then_list, &t_side);
10105    let else_keep = build_keep(&else_list, &e_side);
10106    let else_part = if else_keep.is_empty() && !had_else {
10107        None
10108    } else {
10109        Some(Box::new(Stmt::Block(else_keep)))
10110    };
10111    let new_if = Stmt::If {
10112        cond: cond_i,
10113        then_stmt: Box::new(Stmt::Block(then_keep)),
10114        else_stmt: else_part,
10115    };
10116    let if_empty = match &new_if {
10117        Stmt::If { then_stmt, else_stmt, .. } => {
10118            let be = |s: &Stmt| matches!(s, Stmt::Block(v) if v.is_empty());
10119            be(then_stmt) && else_stmt.as_ref().is_none_or(|e| be(e))
10120        }
10121        _ => false,
10122    };
10123    let mut out: Vec<Stmt> = Vec::with_capacity(stmts.len());
10124    out.push(call_stmt);
10125    out.extend(kept_prelude);
10126    if !if_empty {
10127        out.push(new_if);
10128    }
10129    out.extend(stmts[if_pos + 1..].to_vec());
10130    *stmts = out;
10131    true
10132}
10133
10134
10135pub fn strip_enum_ctor_super(body: &mut Stmt) {
10136    let is_enum_super = |s: &Stmt| {
10137        matches!(
10138            s,
10139            Stmt::ExprStmt(Expr::Method { name, is_super: true, .. }) if &**name == "<init>"
10140        )
10141    };
10142    let Stmt::Block(stmts) = body else {
10143        return;
10144    };
10145    stmts.retain(|s| !is_enum_super(s));
10146    for st in stmts.iter_mut() {
10147        if let Stmt::Block(inner) = st {
10148            inner.retain(|s| !is_enum_super(s));
10149        }
10150    }
10151}
10152
10153pub fn ensure_declared(body: &mut Stmt, vt: &VarTable) {
10154    // (Perf: dense Vec<bool> tables instead of HashSets — var ids are
10155    // dense; the `assigned` set collected here was never read and cost a
10156    // full extra tree walk per method.)
10157    let n = vt.vars.len().max(1);
10158
10159    // Demote extra LocalDefs (same var declared more than once) FIRST —
10160    // the scope analysis below must see exactly one def per var.
10161    let mut seen: HashSet<u32> = HashSet::default();
10162    demote_dup_decls(body, &mut seen, vt);
10163
10164    // Scope hoist: a LocalDef inside a nested block whose var is also
10165    // referenced OUTSIDE that block is a Java scope violation (the
10166    // branch-local `int v8 = pi.versionCode;` read after the try —
10167    // "cannot find symbol" at every outside use). Demote those defs to
10168    // assignments and let the bare top-of-method declaration below cover
10169    // the var. Two cheap passes: pre-order block numbering + per-var
10170    // inside/outside occurrence counts (pre-order subtree = id range).
10171    let mut def_block: Vec<u32> = vec![u32::MAX; n];
10172    let mut sizes: Vec<u32> = vec![0];
10173    let mut next_id: u32 = 1;
10174    scope_pass_a(body, 0, &mut next_id, &mut def_block, &mut sizes);
10175    sizes[0] = next_id;
10176    let mut within: Vec<u32> = vec![0; n];
10177    let mut total: Vec<u32> = vec![0; n];
10178    let mut next_b: u32 = 1;
10179    scope_pass_b(body, 0, &mut next_b, &def_block, &sizes, &mut within, &mut total);
10180    let mut hoist: HashSet<u32> = HashSet::default();
10181    for v in 0..n {
10182        if def_block[v] != u32::MAX && def_block[v] != 0 && within[v] < total[v] {
10183            hoist.insert(v as u32);
10184        }
10185    }
10186    if !hoist.is_empty() {
10187        let mut seed: HashSet<u32> = hoist.iter().copied().collect();
10188        demote_dup_decls(body, &mut seed, vt);
10189    }
10190
10191    let mut declared: Vec<bool> = vec![false; n];
10192    let mut is_param: Vec<bool> = vec![false; n];
10193    for v in &vt.vars {
10194        if v.is_param && (v.id as usize) < n {
10195            is_param[v.id as usize] = true;
10196        }
10197    }
10198    // Catch parameters are declared by the `catch (Type v)` clause —
10199    // bind_catches consumed their LocalDef before this pass ran, so
10200    // without this they re-declare at the top of the method and clash
10201    // with the clause (`Throwable th2;` + `catch (Throwable th2)` — a
10202    // syntax gate cannot see this, it is a semantic "already defined").
10203    let mut is_catch: Vec<bool> = vec![false; n];
10204    walk_all(body, &mut |st| {
10205        if let Stmt::Try { catches, .. } = st {
10206            for c in catches {
10207                if (c.var as usize) < n {
10208                    is_catch[c.var as usize] = true;
10209                }
10210            }
10211        }
10212    });
10213    walk_all(body, &mut |st| {
10214        if let Stmt::LocalDef { var, .. } = st {
10215            if (*var as usize) < n {
10216                declared[*var as usize] = true;
10217            }
10218        }
10219    });
10220    let mut used: HashSet<u32> = HashSet::default();
10221    // assignments=true: a var that only ever appears as an assign
10222    // TARGET still needs a declaration (`x = 5;` alone does not
10223    // declare x in Java).
10224    stmt_collect_vars(body, &mut used, true);
10225
10226    // Params are declared by the signature; hoisted vars lost their
10227    // LocalDef to the demotion above and always need the bare decl.
10228    let mut needs_decl: Vec<u32> = used
10229        .into_iter()
10230        .filter(|v| {
10231            let i = *v as usize;
10232            i >= n
10233                || ((!declared[i] && !is_param[i] && !is_catch[i]) || hoist.contains(v))
10234        })
10235        .collect();
10236    needs_decl.sort_unstable();
10237    needs_decl.dedup();
10238
10239    if !needs_decl.is_empty() {
10240        let mut decls: Vec<Stmt> = needs_decl
10241            .into_iter()
10242            .map(|v| Stmt::LocalDef {
10243                var: v,
10244                init: None,
10245                is_final: false,
10246                force_type: true,
10247            })
10248            .collect();
10249        decls.reverse();
10250        match body {
10251            Stmt::Block(v) => {
10252                for d in decls {
10253                    v.insert(0, d);
10254                }
10255            }
10256            other => {
10257                let inner = std::mem::replace(other, Stmt::Block(vec![]));
10258                *other = Stmt::Block({
10259                    let mut vs = decls;
10260                    vs.push(inner);
10261                    vs
10262                });
10263            }
10264        }
10265    }
10266}
10267
10268fn demote_dup_decls(s: &mut Stmt, seen: &mut HashSet<u32>, vt: &VarTable) {
10269    match s {
10270        // Exactly one visit per node: the Block arm consumes its children,
10271        // the fallthrough walk would double-visit them.
10272        Stmt::Block(v) => {
10273            for x in v.iter_mut() {
10274                demote_dup_decls(x, seen, vt);
10275            }
10276        }
10277        Stmt::LocalDef { var, init, .. } => {
10278            if seen.contains(var) {
10279                let ty = vt.var(*var).ty.clone();
10280                if let Some(e) = init.take() {
10281                    *s = Stmt::ExprStmt(Expr::Assign {
10282                        target: Box::new(Expr::Local { var: *var, ty }),
10283                        op: AssignOp::Plain,
10284                        value: Box::new(e),
10285                    });
10286                } else {
10287                    *s = Stmt::Block(vec![]);
10288                }
10289            } else {
10290                seen.insert(*var);
10291            }
10292        }
10293        other => {
10294            walk_mut(other, &mut |x| demote_dup_decls(x, seen, vt));
10295        }
10296    }
10297}
10298
10299#[allow(dead_code)]
10300fn unused(_: &Vec<CaseGroup>, _: &Catch) {}
10301
10302// ---------------------------------------------------------------------------
10303// jadx-style local names (ApplyVariableNames + dexdec's port of it)
10304// ---------------------------------------------------------------------------
10305
10306/// A synthetic `v12`/`p3` never survives when a better name exists.
10307/// Priority: (1) a Kotlin `Intrinsics.checkNotNullParameter(x, "name")`
10308/// names `x` from the message string (the string IS the parameter name);
10309/// (2) the single defining call — `getFoo()` → `foo`, `isFinishing()` →
10310/// `finishing`, `new File(…)` → `file`; (3) a type alias or the
10311/// lowercased class simple name. Collisions take `2`, `3`, …; a var with
10312/// two DISAGREEING defining calls stays unnamed (dexdec's
10313/// RelationalNameInference rule). Debug-info names are never touched
10314/// (`synthetic_name` gates the whole pass).
10315pub fn apply_local_names(vt: &mut VarTable, body: &Stmt) {
10316    use std::collections::{HashMap, HashSet};
10317
10318    // ---- proposals ------------------------------------------------------
10319    // var → (name, source-priority); disagreeing call names are dropped.
10320    let mut by_call: HashMap<u32, Vec<String>> = HashMap::new();
10321    let mut by_intrinsics: HashMap<u32, String> = HashMap::new();
10322
10323    visit_all_exprs(body, &mut |e| {
10324        if let Expr::Method { cls, name, args, .. } = e {
10325            if (cls.as_ref() == "kotlin/jvm/internal/Intrinsics"
10326                || cls.as_ref() == "kotlin/jvm/internal/IntrinsicsKt")
10327                && matches!(name.as_ref(), "checkNotNullParameter" | "checkParameterIsNotNull")
10328                && args.len() == 2
10329            {
10330                if let (Expr::Local { var, .. }, Expr::Const(ConstVal::Str(s))) = (&args[0], &args[1])
10331                {
10332                    by_intrinsics
10333                        .entry(*var)
10334                        .or_insert_with(|| sanitize_name(s).unwrap_or_default());
10335                }
10336            }
10337        }
10338    });
10339
10340    walk_all(body, &mut |st| {
10341        let (var, value) = match st {
10342            Stmt::LocalDef { var, init: Some(e), .. } => (*var, e),
10343            Stmt::ExprStmt(Expr::Assign { target, value, .. })
10344                if matches!(&**target, Expr::Local { .. }) =>
10345            {
10346                let Expr::Local { var, .. } = &**target else { return };
10347                (*var, value.as_ref())
10348            }
10349            _ => return,
10350        };
10351        if let Some(n) = defining_call_name(value) {
10352            by_call.entry(var).or_default().push(n);
10353        }
10354    });
10355
10356    // ---- reservation + application --------------------------------------
10357    // Real (debug-info) names can legally REPEAT across sibling source
10358    // scopes (`int i` in two loops) but our flat declaration hoisting
10359    // puts them in one scope — de-duplicate with the same numeric
10360    // suffixes the claim path uses.
10361    // Uniqueness operates on the SANITIZED display name: the identifier
10362    // sanitizer maps non-ASCII to `_`, so distinct debug names
10363    // (`ERROR_token参数缺失` / `ERROR_channelId参数缺失`) both RENDER as
10364    // `ERROR________` and collide as declarations. Tracking raw names
10365    // here would let the collision through.
10366    let sanit = |n: &str| crate::classdec::java_ident(n).into_owned();
10367    let mut taken: HashSet<String> = HashSet::default();
10368    // Parameter names are occupied REGARDLESS of syntheticness: claim()
10369    // hands out rename candidates, and without the synthetic params in
10370    // the pool a type fallback (`l7.p0` → "p0") renamed one parameter
10371    // ONTO another's slot-name (`b(v6.l p0, Object obj, l7.p0 p0)`).
10372    for v in vt.vars.iter() {
10373        // ALL synthetic names occupy their names, not just parameters:
10374        // a type fallback (`pc5.v56` → "v56") otherwise renames a local
10375        // ONTO another local's slot-name (`int v56;` beside
10376        // `pc5.v56 v56;`).
10377        if v.synthetic_name {
10378            taken.insert(sanit(&v.name));
10379        }
10380    }
10381    for v in vt.vars.iter_mut() {
10382        if v.synthetic_name {
10383            continue;
10384        }
10385        if !taken.insert(sanit(&v.name)) {
10386            let base = sanit(&v.name);
10387            for i in 2.. {
10388                let cand = format!("{base}{i}");
10389                if taken.insert(cand.clone()) {
10390                    v.name = cand;
10391                    break;
10392                }
10393            }
10394        }
10395    }
10396    let claim = |taken: &mut HashSet<String>, want: &str| -> String {
10397        let w = crate::classdec::java_ident(want).into_owned();
10398        if taken.insert(w.clone()) {
10399            return w;
10400        }
10401        for i in 2.. {
10402            let cand = format!("{w}{i}");
10403            if taken.insert(cand.clone()) {
10404                return cand;
10405            }
10406        }
10407        w
10408    };
10409
10410    for info in vt.vars.iter_mut() {
10411        if !info.synthetic_name {
10412            continue;
10413        }
10414        // (1) Intrinsics string — the Kotlin compiler wrote the real
10415        // parameter name right into the check.
10416        if let Some(n) = by_intrinsics.get(&info.id) {
10417            if !n.is_empty() {
10418                info.name = claim(&mut taken, n);
10419                continue;
10420            }
10421        }
10422        // (2) One consistent defining call.
10423        if let Some(names) = by_call.get(&info.id) {
10424            let unique: HashSet<&String> = names.iter().collect();
10425            if unique.len() == 1 {
10426                let n = unique.into_iter().next().unwrap();
10427                if !is_java_keyword(n) {
10428                    info.name = claim(&mut taken, n);
10429                    continue;
10430                }
10431            }
10432        }
10433        // (3) Type alias, else the lowercased simple class name
10434        // (jadx names a `Looper` local `looper`).
10435        let want = type_alias(&info.ty).map(str::to_string).or_else(|| simple_type_name(&info.ty));
10436        if let Some(n) = want {
10437            info.name = claim(&mut taken, &n);
10438        }
10439    }
10440
10441    // Final uniquification across ALL vars (params, locals, catch): the
10442    // reservation, claim and debug-name domains above are checked
10443    // pairwise but a name claimed LAST can still equal a debug name
10444    // that never re-checked (`zd1.v2 v2` beside `zd1.v2[] v2`).
10445    let mut seen: HashSet<String> = HashSet::default();
10446    for v in vt.vars.iter_mut() {
10447        if !seen.insert(sanit(&v.name)) {
10448            let base = sanit(&v.name);
10449            for i in 2.. {
10450                let cand = format!("{base}{i}");
10451                if seen.insert(cand.clone()) {
10452                    v.name = cand;
10453                    break;
10454                }
10455            }
10456        }
10457    }
10458}
10459
10460/// `com/android/.../Looper` → `looper` — the alias-table fallback.
10461fn simple_type_name(ty: &TypeRef) -> Option<String> {
10462    let TypeRef::J(JavaType::Object(n)) = ty else {
10463        return None;
10464    };
10465    let simple = n.rsplit('/').next().unwrap_or(n);
10466    let simple = simple.rsplit('$').next().unwrap_or(simple);
10467    // Lowercase the first ALPHABETIC char — `$`/`_`-prefixed names
10468    // (R8's `$$$_Thread`) must not survive capitalized.
10469    let mut chars = simple.chars();
10470    let mut out = String::with_capacity(simple.len());
10471    let mut lowered = false;
10472    for c in chars.by_ref() {
10473        if c.is_ascii_alphabetic() && !lowered {
10474            out.push(c.to_ascii_lowercase());
10475            lowered = true;
10476        } else {
10477            out.push(c);
10478        }
10479        if lowered {
10480            break;
10481        }
10482    }
10483    out.extend(chars);
10484    sanitize_name(&out)
10485}
10486
10487/// `getFoo()` → `foo`, `isFinishing()` → `finishing`, `new File(…)` →
10488/// `file`. The get/is prefixes only strip when the remainder starts
10489/// uppercase (so `issues()` keeps its name).
10490fn defining_call_name(e: &Expr) -> Option<String> {
10491    match e {
10492        Expr::Method { name, .. } => {
10493            let base = name
10494                .strip_prefix("get")
10495                .or_else(|| name.strip_prefix("is"))
10496                .filter(|rest| rest.chars().next().is_some_and(|c| c.is_ascii_uppercase()))
10497                .unwrap_or(name);
10498            let first = base.chars().next()?;
10499            let mut out = String::with_capacity(base.len());
10500            out.push(first.to_ascii_lowercase());
10501            out.extend(base.chars().skip(1));
10502            sanitize_name(&out)
10503        }
10504        Expr::New { cls, .. } => {
10505            let simple = cls.rsplit('/').next().unwrap_or(cls);
10506            let simple = simple.rsplit('$').next().unwrap_or(simple);
10507            let first = simple.chars().next()?;
10508            let mut out = String::with_capacity(simple.len());
10509            out.push(first.to_ascii_lowercase());
10510            out.extend(simple.chars().skip(1));
10511            sanitize_name(&out)
10512        }
10513        _ => None,
10514    }
10515}
10516
10517/// Valid java identifier, not a keyword/restricted name, ≥2 chars.
10518fn sanitize_name(s: &str) -> Option<String> {
10519    let mut out = String::with_capacity(s.len());
10520    for c in s.chars() {
10521        if c.is_ascii_alphanumeric() || c == '_' || c == '$' {
10522            out.push(c);
10523        } else {
10524            out.push('_');
10525        }
10526    }
10527    if out.len() < 2
10528        || out.chars().next().is_some_and(|c| c.is_ascii_digit())
10529        || is_java_keyword(&out)
10530    {
10531        return None;
10532    }
10533    Some(out)
10534}
10535
10536/// jadx's alias table for the common types, with the Android globals the
10537/// corpus actually shows; anything else falls back in the caller.
10538fn type_alias(ty: &TypeRef) -> Option<&'static str> {
10539    let TypeRef::J(JavaType::Object(n)) = ty else {
10540        return None;
10541    };
10542    Some(match n.as_ref() {
10543        "java/lang/String" | "kotlin/String" => "str",
10544        "java/lang/Class" => "cls",
10545        "java/lang/Throwable" => "th",
10546        "java/lang/Object" | "kotlin/Any" => "obj",
10547        "java/util/Iterator" | "kotlin/collections/Iterator" => "it",
10548        "java/lang/Boolean" => "bool",
10549        "java/lang/Integer" => "num",
10550        "java/lang/Character" => "ch",
10551        "java/lang/Byte" => "b",
10552        "java/lang/Short" => "sh",
10553        "java/lang/Float" => "f",
10554        "java/lang/Double" => "d",
10555        "java/lang/Long" => "i",
10556        "java/lang/StringBuilder" => "sb",
10557        "java/util/ArrayList" => "list",
10558        "java/util/HashMap" => "map",
10559        "android/content/Context" => "context",
10560        "android/content/Intent" => "intent",
10561        "android/os/Bundle" => "bundle",
10562        "android/view/View" => "view",
10563        "android/graphics/Bitmap" => "bitmap",
10564        "android/view/ViewGroup" => "viewGroup",
10565        _ => return None,
10566    })
10567}
10568
10569/// Every expression in the tree, statements included (read-only).
10570pub(crate) fn visit_all_exprs<F: FnMut(&Expr)>(s: &Stmt, f: &mut F) {
10571    walk_all(s, &mut |st| {
10572        let exprs: Vec<&Expr> = match st {
10573            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
10574                let mut v: Vec<&Expr> = vec![value];
10575                if !matches!(&**target, Expr::Local { .. }) {
10576                    v.push(target);
10577                }
10578                v
10579            }
10580            Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
10581                vec![e]
10582            }
10583            Stmt::Return(Some(e)) => vec![e],
10584            Stmt::LocalDef { init: Some(e), .. } => vec![e],
10585            Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
10586                vec![cond]
10587            }
10588            _ => Vec::new(),
10589        };
10590        for e in exprs {
10591            visit_exprs(e, f);
10592        }
10593    });
10594}
10595
10596/// IntDef/LongDef exact-match rendering: a literal argument to a
10597/// platform method whose parameter carries a constant domain renders as
10598/// the named constant (`setVisibility(8)` → `android.view.View.GONE`).
10599/// Combined flag values and literals outside the domain stay numeric.
10600pub fn platform_constants(body: &mut Stmt) {
10601    if !crate::platform::installed() {
10602        return;
10603    }
10604    rewrite_exprs(body, &mut |e| {
10605        if let Expr::Method { cls, name, desc, args, .. } = e {
10606            for a in args.iter_mut() {
10607                let (value, is_long) = match a {
10608                    Expr::Const(ConstVal::Int(v)) => (*v as i64, false),
10609                    Expr::Const(ConstVal::Long(v)) => (*v, true),
10610                    _ => continue,
10611                };
10612                if let Some(named) =
10613                    crate::platform::named_constant(cls, name, &desc_to_string(desc), 0, value)
10614                {
10615                    let _ = is_long;
10616                    *a = Expr::Raw(named);
10617                }
10618            }
10619        }
10620    });
10621}
10622
10623/// MethodDescriptor → dex descriptor string (`(I)V`).
10624fn desc_to_string(d: &jdc_core::types::MethodDescriptor) -> String {
10625    let mut out = String::from("(");
10626    for a in &d.args {
10627        type_to_desc(a, &mut out);
10628    }
10629    out.push(')');
10630    type_to_desc(&d.ret, &mut out);
10631    out
10632}
10633
10634fn type_to_desc(t: &jdc_core::types::JavaType, out: &mut String) {
10635    match t {
10636        JavaType::Void => out.push('V'),
10637        JavaType::Boolean => out.push('Z'),
10638        JavaType::Byte => out.push('B'),
10639        JavaType::Char => out.push('C'),
10640        JavaType::Short => out.push('S'),
10641        JavaType::Int => out.push('I'),
10642        JavaType::Float => out.push('F'),
10643        JavaType::Long => out.push('J'),
10644        JavaType::Double => out.push('D'),
10645        JavaType::Object(n) => {
10646            out.push('L');
10647            out.push_str(n);
10648            out.push(';');
10649        }
10650        JavaType::Array(inner) => {
10651            out.push('[');
10652            type_to_desc(inner, out);
10653        }
10654    }
10655}
10656
10657/// Kotlin `Intrinsics` null-check elision (jadx's ProcessKotlinInternals):
10658/// statement-position `checkNotNullParameter(p, "name")` /
10659/// `checkParameterIsNotNull` / `checkNotNull…` calls are runtime
10660/// assertions — drop them. The naming pass harvests the parameter
10661/// strings FIRST, so run this after `apply_local_names`.
10662pub fn remove_kotlin_checks(s: &mut Stmt) {
10663    walk_mut_deep(s, &mut |st| {
10664        let Stmt::Block(v) = st else { return };
10665        v.retain(|x| !is_kotlin_check(x));
10666    });
10667}
10668
10669fn is_kotlin_check(s: &Stmt) -> bool {
10670    let Stmt::ExprStmt(e) = s else { return false };
10671    let Expr::Method { cls, name, .. } = e else { return false };
10672    (cls.as_ref() == "kotlin/jvm/internal/Intrinsics" || cls.as_ref() == "kotlin/jvm/internal/IntrinsicsKt__Jdk7Kt")
10673        && matches!(
10674            name.as_ref(),
10675            "checkNotNullParameter"
10676                | "checkParameterIsNotNull"
10677                | "checkExpressionValueIsNotNull"
10678                | "checkNotNullExpressionValue"
10679                | "checkReturnedValueIsNotNull"
10680                | "checkFieldIsNotNull"
10681                | "checkNotNull"
10682        )
10683}
10684
10685// ---------------------------------------------------------------------------
10686// Synthetic-accessor inlining (jadx's MarkMethodsForInline, the safe subset)
10687// ---------------------------------------------------------------------------
10688
10689/// Inline `access$NNN`-style synthetic static bridges at their call
10690/// sites: an identity (`return pN`), a getter (`iget pX, field;
10691/// return vR`), or a method forwarder (`invoke; return`) — with the
10692/// d8 APM trace wrappers (`MethodCollector.i/.o` const-only static
10693/// calls) tolerated around the core. Only STATIC + SYNTHETIC callees
10694/// qualify (compiler-generated bridges: no override semantics, no
10695/// side effects beyond the forwarded operation).
10696/// Can `host` (internal class name) source-level reference `target`'s
10697/// class chain and member? Cross-package inlining of a synthetic
10698/// accessor used to EXPOSE package-private targets the raw dex never
10699/// referenced cross-package (BuildersKt.launch$default forwards to
10700/// kotlinx.coroutines.k.e — the widening census cannot see post-inline
10701/// calls; lark's 1,019-line k不是公共的 family). Non-public chain link
10702/// or non-public member across packages → the inline must not happen
10703/// (the public synthetic bridge stays and compiles).
10704fn inline_ref_ok(pool: &DexPool, host: &str, target: &str, member: Option<(&str, bool)>) -> bool {
10705    let hpkg = match host.rfind('/') {
10706        Some(i) => &host[..i],
10707        None => "",
10708    };
10709    let mut cur = target;
10710    loop {
10711        let tpkg = match cur.rfind('/') {
10712            Some(i) => &cur[..i],
10713            None => "",
10714        };
10715        if tpkg != hpkg {
10716            if let Some(pc) = pool.get(cur) {
10717                if pc.access & crate::access::ACC_PUBLIC == 0 {
10718                    return false;
10719                }
10720                if cur == target {
10721                    if let Some((mname, is_method)) = member {
10722                        let ok = if is_method {
10723                            pc.all_methods().any(|m| {
10724                                &*m.name == mname
10725                                    && m.access & crate::access::ACC_PUBLIC != 0
10726                            })
10727                        } else {
10728                            pc.static_fields
10729                                .iter()
10730                                .chain(pc.instance_fields.iter())
10731                                .any(|f| {
10732                                    f.name == mname
10733                                        && f.access & crate::access::ACC_PUBLIC != 0
10734                                })
10735                        };
10736                        // A member match requires the exact member to be
10737                        // public; an absent member (renamed/collapsed)
10738                        // stays allowed — the render will show what the
10739                        // pool has.
10740                        if !ok
10741                            && (if is_method {
10742                                pc.all_methods().any(|m| &*m.name == mname)
10743                            } else {
10744                                pc.static_fields
10745                                    .iter()
10746                                    .chain(pc.instance_fields.iter())
10747                                    .any(|f| f.name == mname)
10748                            })
10749                        {
10750                            return false;
10751                        }
10752                    }
10753                }
10754            }
10755        }
10756        match cur.rfind('$') {
10757            Some(i) if i > 0 => cur = &cur[..i],
10758            _ => break,
10759        }
10760    }
10761    true
10762}
10763
10764pub fn inline_accessors(s: &mut Stmt, pool: &DexPool, host: &str) {
10765    rewrite_exprs(s, &mut |e| {
10766        let Expr::Method { cls, name, desc, args, is_static, .. } = e else { return };
10767        if !*is_static || args.is_empty() {
10768            return;
10769        }
10770        let Some(target) = pool.get(cls) else { return };
10771        let desc_s = desc_to_string(desc);
10772        let Some(m) = target.find_method(name, &desc_s) else { return };
10773        if !(m.is_static() && m.access & access::ACC_SYNTHETIC != 0) {
10774            return;
10775        }
10776        let Some(dex) = pool.dex(m.dex_idx) else { return };
10777        // Snapshot first: the accessor's owning image may already be
10778        // retired, and a live read's success would depend on worker
10779        // completion interleaving — nondeterministic inlining (whether
10780        // the accessor folds at all) between identical runs.
10781        let code = match pool.accessor_code(m.dex_idx, m.code_off) {
10782            Some(bytes) => ddc_dex::CodeItem::parse(&bytes, 0),
10783            None => dex.code_at(m.code_off),
10784        };
10785        let Some(code) = code else { return };
10786        let insns: Vec<&Insn> =
10787            code.insns.iter().filter(|i| !matches!(i.kind, InsnKind::Nop)).collect();
10788        // Strip the APM trace wrappers (const-only invoke-static).
10789        let core: Vec<&Insn> = insns
10790            .iter()
10791            .copied()
10792            .filter(|i| !is_const_only_trace(&i.kind, &insns))
10793            .collect();
10794        match accessor_shape(&dex, &core, &code) {
10795            Some(Shape::Identity(arg_i)) => {
10796                if let Some(a) = args.get(arg_i) {
10797                    *e = a.clone();
10798                }
10799            }
10800            Some(Shape::FieldRead { arg, cls: field_cls, field, field_ty }) => {
10801                if !inline_ref_ok(pool, host, &field_cls, Some((&field, false))) {
10802                    return;
10803                }
10804                if let Some(owner) = args.get(arg).cloned().map(Box::new) {
10805                    let ty = TypeRef::J(desc_type(&field_ty));
10806                    // The inlined read must consult the member rename
10807                    // registry like every lifted field ref does — the
10808                    // raw dex name desynced from renamed declarations
10809                    // (lark uu4/f$b's `a`→`ax6`: 找不到符号 变量 a ×4.4k
10810                    // when the nested-collision deshadow fired).
10811                    let name = jdc_core::rename::field_display(
10812                        &field_cls,
10813                        &field,
10814                        &field_ty,
10815                    )
10816                    .map(std::sync::Arc::from)
10817                    .unwrap_or_else(|| field.into());
10818                    *e = Expr::Field {
10819                        owner: Some(owner),
10820                        cls: field_cls.into(),
10821                        name,
10822                        ty,
10823                        is_static: false,
10824                    };
10825                }
10826            }
10827            Some(Shape::Forward { cls: tcls, name: tname, desc: tdesc, instance }) => {
10828                if !inline_ref_ok(pool, host, &tcls, Some((&tname, true))) {
10829                    return;
10830                }
10831                // Same registry duty as FieldRead: the forwarded member
10832                // must render under its renamed display, not the raw
10833                // dex name.
10834                let disp = jdc_core::rename::field_display(
10835                    &tcls,
10836                    &tname,
10837                    &tdesc.to_string(),
10838                )
10839                .map(std::sync::Arc::from)
10840                .unwrap_or_else(|| tname.into());
10841                *cls = tcls.into();
10842                *name = disp;
10843                *desc = std::sync::Arc::new(tdesc);
10844                if instance && !args.is_empty() {
10845                    // The first param (the receiver) becomes the owner.
10846                    let recv = args.remove(0);
10847                    if let Expr::Method { owner, .. } = e {
10848                        *owner = Some(Box::new(recv));
10849                    }
10850                }
10851            }
10852            None => {}
10853        }
10854    });
10855}
10856
10857/// What an accessor body reduces to.
10858enum Shape {
10859    /// `return pN` — the call becomes argument N.
10860    Identity(usize),
10861    /// `iget vR, pX, field; return vR` — becomes `argN.field`.
10862    FieldRead { arg: usize, cls: String, field: String, field_ty: String },
10863    /// `invoke {pX, args…}, method@M; (move-result;)? return` —
10864    /// becomes the forwarded call.
10865    Forward { cls: String, name: String, desc: MethodDescriptor, instance: bool },
10866}
10867
10868/// Resolve the IGet field's owner class, name and type descriptor.
10869fn field_of(dex: &ddc_dex::DexFile, field_idx: u32) -> Option<(String, String, String)> {
10870    let f = dex.field(field_idx);
10871    Some((
10872        // class_name strips the `L...;` shell; type_name would leak a
10873        // descriptor into the Field's owner class (`La3.a.e_`).
10874        dex.class_name(f.class_idx),
10875        dex.string(f.name_idx).to_string(),
10876        // The FIELD TYPE stays a descriptor: desc_type parses it.
10877        dex.type_name(f.type_idx).to_string(),
10878    ))
10879}
10880
10881/// Map a register to a parameter index (static method: params occupy
10882/// the LAST ins_size registers).
10883fn param_index(code: &ddc_dex::CodeItem, reg: u16) -> Option<usize> {
10884    let rs = code.registers_size as usize;
10885    let ins = code.ins_size as usize;
10886    let r = reg as usize;
10887    let first = rs.checked_sub(ins)?;
10888    (r >= first && r < rs).then(|| r - first)
10889}
10890
10891fn accessor_shape(
10892    dex: &ddc_dex::DexFile,
10893    core: &[&Insn],
10894    code: &ddc_dex::CodeItem,
10895) -> Option<Shape> {
10896    // Dead consts that fed the stripped trace wrappers remain in the
10897    // core — skip them.
10898    let core: &[&Insn] = match core.split_first() {
10899        Some((first, rest)) if matches!(first.kind, InsnKind::Const { .. }) => rest,
10900        _ => core,
10901    };
10902    let core: &[&Insn] = match core.split_last() {
10903        Some((last, rest)) if matches!(last.kind, InsnKind::Const { .. }) => rest,
10904        _ => core,
10905    };
10906    match core {
10907        // return pN
10908        [Insn { kind: InsnKind::Return { src }, .. }] => {
10909            let idx = param_index(code, *src)?;
10910            Some(Shape::Identity(idx))
10911        }
10912        // iget vR, pX, field; return vR
10913        [
10914            Insn { kind: InsnKind::IGet { dst, obj, field_idx }, .. },
10915            Insn { kind: InsnKind::Return { src }, .. },
10916        ] if dst == src => {
10917            let arg = param_index(code, *obj)?;
10918            let (cls, field, field_ty) = field_of(dex, *field_idx)?;
10919            Some(Shape::FieldRead { arg, cls, field, field_ty })
10920        }
10921        // invoke {…}, method@M; (move-result; return)? — forwarder.
10922        [Insn { kind: InsnKind::Invoke { method_idx, regs, kind, .. }, .. }, rest @ ..] if rest.len() <= 2 => {
10923            // Only forward when every register is a param (no locals).
10924            if !regs.iter().all(|r| param_index(code, *r).is_some()) || regs.is_empty() {
10925                return None;
10926            }
10927            // Trailing must be move-result+return or return/void —
10928            // anything else (e.g. trace calls) already filtered upstream.
10929            if rest.len() == 2 {
10930                let ok = matches!(rest[0].kind, InsnKind::MoveResult { .. })
10931                    && matches!(rest[1].kind, InsnKind::Return { .. } | InsnKind::ReturnVoid);
10932                if !ok {
10933                    return None;
10934                }
10935            }
10936            let mid = dex.method(*method_idx);
10937            // class_name strips the `L...;` shell — type_name leaks a
10938            // descriptor into the forwarded call's class (`La3.a.e_`).
10939            let cls = dex.class_name(mid.class_idx);
10940            let name = dex.string(mid.name_idx).to_string();
10941            let mut s = String::from("(");
10942            for t in dex.proto_params(mid.proto_idx) {
10943                s.push_str(dex.type_name(*t));
10944            }
10945            s.push(')');
10946            s.push_str(dex.type_name(dex.proto(mid.proto_idx).return_type_idx));
10947            let desc = parse_method_descriptor(&s)?;
10948            Some(Shape::Forward { cls, name, desc, instance: !matches!(kind, InvokeKind::Static) })
10949        }
10950        _ => None,
10951    }
10952}
10953
10954
10955/// A const-only invoke-static (the d8 APM `i(732046)` / `o(732046)`
10956/// trace wrapper): every argument register is loaded by a Const (or a
10957/// Const-into-move chain) in the SAME body. Scoped to synthetic
10958/// accessors, so a genuinely side-effecting const call is never
10959/// mistaken for a trace.
10960fn is_const_only_trace(kind: &InsnKind, insns: &[&Insn]) -> bool {
10961    let InsnKind::Invoke { kind, regs, .. } = kind else { return false };
10962    if !matches!(kind, InvokeKind::Static) || regs.is_empty() {
10963        return false;
10964    }
10965    regs.iter().all(|r| const_loaded(*r, insns, 0))
10966}
10967
10968/// Const-loaded directly, or through a Move chain from a const-loaded
10969/// register (the APM `const v0, id` → `invoke {v0}` shape).
10970fn const_loaded(r: u16, insns: &[&Insn], depth: u8) -> bool {
10971    if depth > 3 {
10972        return false;
10973    }
10974    insns.iter().any(|i| match &i.kind {
10975        InsnKind::Const { dst, .. } => *dst == r,
10976        InsnKind::Move { dst, src } => *dst == r && const_loaded(*src, insns, depth + 1),
10977        _ => false,
10978    })
10979}