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                            // ONLY the bare move-exception decl (the
725                            // lifter emits LocalDef{init:None} for it).
726                            // A first stmt with a REAL init is handler
727                            // computation — consuming it as the catch
728                            // store deleted the def and hijacked its
729                            // reads to the catch param (lark ih6/w's
730                            // `v11 = Thread.currentThread()` →
731                            // `catch (InterruptedException thread2) {
732                            // thread2.interrupt(); }`; ch6/e cls4.
733                            // getName; the exception-ignoring catch is
734                            // the standard interrupt idiom).
735                            Some(Stmt::LocalDef { var, init: None, .. }) => Some(*var),
736                            // Phi-copy shape `v = exc` where exc is the
737                            // unmaterialized move-exception register
738                            // (defined nowhere in the method).
739                            Some(Stmt::ExprStmt(Expr::Assign { target, value, .. })) => {
740                                match (&**target, &**value) {
741                                    (Expr::Local { var, .. }, Expr::Local { var: src, .. })
742                                        if !defined.contains(src) =>
743                                    {
744                                        Some(*var)
745                                    }
746                                    _ => None,
747                                }
748                            }
749                            _ => None,
750                        },
751                        _ => None,
752                    };
753                    if let Some(v) = stored {
754                        let exc = c
755                            .exc
756                            .first()
757                            .cloned()
758                            .unwrap_or_else(|| "java/lang/Throwable".into());
759                        let slot = vt.var(v).slot;
760                        let name = "e".to_string();
761                        let new_var =
762                            vt.add_catch_var(slot, name, TypeRef::J(JavaType::Object(exc)));
763                        if let Stmt::Block(vs) = c.body.as_mut() {
764                            vs.remove(0);
765                        }
766                        rewrite_local_refs(c.body.as_mut(), v, new_var);
767                        c.var = new_var;
768                    } else if let Some(v) = first_undefined_local_read(c.body.as_ref(), defined)
769                        .filter(|v| {
770                            // Bind only a var read NOWHERE outside this
771                            // catch: ensure_declared would hoist an
772                            // outside-read var to a method local, and
773                            // consuming it as the catch parameter would
774                            // scope it too narrowly.
775                            let in_catch = count_locals_stmts(std::slice::from_ref(c.body.as_ref()));
776                            reads_all.get(v).copied().unwrap_or(0)
777                                == in_catch.get(v).copied().unwrap_or(0)
778                        })
779                        .filter(|v| {
780                            // And only an EXCEPTION-typed var: the
781                            // move-exception registers carry the handler
782                            // type — a plain local pending its hoisted
783                            // declaration (StringBuilder sb) must not be
784                            // consumed as the catch parameter (reqable
785                            // amazon: the declaration vanished and its
786                            // outer readers broke).
787                            let ty = vt.var(*v).ty.erased();
788                            let exc = c
789                                .exc
790                                .first()
791                                .cloned()
792                                .unwrap_or_else(|| "java/lang/Throwable".into());
793                            matches!(&ty, JavaType::Object(o)
794                                if o.as_ref() == exc.as_ref()
795                                    || o.as_ref() == "java/lang/Throwable")
796                        })
797                    {
798                        // The handler's first statement is not the
799                        // move-exception def (the d8 synchronized pattern
800                        // leads with MonitorExit): the register the lifter
801                        // minted for move-exception is still READ in the
802                        // body (`throw th;`) with no defining statement
803                        // anywhere. Bind IT as the catch parameter —
804                        // otherwise emit printed `catch (Throwable ignored)`
805                        // over an undeclared `throw th` (definite-assignment
806                        // failure, 6.4k weibo sites).
807                        c.var = v;
808                        fallback_bound.push(v);
809                    } else if let Some(v) =
810                        first_thrown_unassigned_local(c.body.as_ref(), vt, assigned)
811                            .filter(|v| {
812                                let in_catch =
813                                    count_locals_stmts(std::slice::from_ref(c.body.as_ref()));
814                                reads_all.get(v).copied().unwrap_or(0)
815                                    == in_catch.get(v).copied().unwrap_or(0)
816                            })
817                            .filter(|v| {
818                                // Same exception-type gate: the hoisted
819                                // `Throwable th;` declares the var but the
820                                // move-exception never assigned it.
821                                let ty = vt.var(*v).ty.erased();
822                                let exc = c
823                                    .exc
824                                    .first()
825                                    .cloned()
826                                    .unwrap_or_else(|| "java/lang/Throwable".into());
827                                matches!(&ty, JavaType::Object(o)
828                                    if o.as_ref() == exc.as_ref()
829                                        || o.as_ref() == "java/lang/Throwable")
830                            })
831                    {
832                        // Declared-never-assigned: `Throwable th;` hoisted
833                        // by an earlier phase, the catch body's `throw th`
834                        // its only use — the move-exception assignment was
835                        // never materialized. Bind th as the catch
836                        // parameter and drop the bare declaration.
837                        c.var = v;
838                        fallback_bound.push(v);
839                    }
840                }
841                bind_catches_walk(&mut c.body, vt, defined, reads_all, assigned, fallback_bound);
842            }
843            if let Some(f) = finally {
844                bind_catches_walk(f.as_mut(), vt, defined, reads_all, assigned, fallback_bound);
845            }
846        }
847        Stmt::Block(v) => {
848            for x in v.iter_mut() {
849                bind_catches_walk(x, vt, defined, reads_all, assigned, fallback_bound);
850            }
851        }
852        Stmt::If {
853            then_stmt,
854            else_stmt,
855            ..
856        } => {
857            bind_catches_walk(then_stmt, vt, defined, reads_all, assigned, fallback_bound);
858            if let Some(e) = else_stmt {
859                bind_catches_walk(e, vt, defined, reads_all, assigned, fallback_bound);
860            }
861        }
862        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => bind_catches_walk(body, vt, defined, reads_all, assigned, fallback_bound),
863        Stmt::For { init, body, .. } => {
864            for x in init.iter_mut() {
865                bind_catches_walk(x, vt, defined, reads_all, assigned, fallback_bound);
866            }
867            bind_catches_walk(body, vt, defined, reads_all, assigned, fallback_bound);
868        }
869        Stmt::ForEach { body, .. }
870        | Stmt::Labeled { body, .. }
871        | Stmt::Synchronized { body, .. } => bind_catches_walk(body, vt, defined, reads_all, assigned, fallback_bound),
872        Stmt::Switch { cases, default, .. } => {
873            for c in cases.iter_mut() {
874                for x in c.body.iter_mut() {
875                    bind_catches_walk(x, vt, defined, reads_all, assigned, fallback_bound);
876                }
877            }
878            if let Some(d) = default {
879                bind_catches_walk(d, vt, defined, reads_all, assigned, fallback_bound);
880            }
881        }
882        _ => {}
883    }
884}
885
886fn rewrite_local_refs(s: &mut Stmt, from: u32, to: u32) {
887    rewrite_exprs(s, &mut |e| {
888        deep_rewrite(e, &mut |x| {
889            if let Expr::Local { var, .. } = x {
890                if *var == from {
891                    *var = to;
892                }
893            }
894        });
895    });
896}
897
898/// Flatten blocks, drop empty ones and trailing no-op statements.
899pub fn cleanup(s: &mut Stmt) {
900    jdc_core::ir::stmt::flatten(s);
901    strip_empty(s);
902    merge_singleton_stmts(s);
903}
904
905fn strip_empty(s: &mut Stmt) {
906    match s {
907        Stmt::Block(v) => {
908            v.retain(|x| !x.is_empty_block());
909            for x in v.iter_mut() {
910                strip_empty(x);
911            }
912        }
913        Stmt::If {
914            then_stmt,
915            else_stmt,
916            ..
917        } => {
918            strip_empty(then_stmt);
919            if let Some(e) = else_stmt {
920                strip_empty(e);
921                if e.is_empty_block() {
922                    *else_stmt = None;
923                }
924            }
925        }
926        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => strip_empty(body),
927        Stmt::For { init, body, .. } => {
928            init.retain(|x| !x.is_empty_block());
929            strip_empty(body);
930        }
931        Stmt::ForEach { body, .. } => strip_empty(body),
932        Stmt::Switch { cases, default, .. } => {
933            for c in cases {
934                c.body.retain(|x| !x.is_empty_block());
935            }
936            if let Some(d) = default {
937                strip_empty(d);
938            }
939        }
940        Stmt::Try {
941            body,
942            catches,
943            finally,
944        } => {
945            strip_empty(body);
946            for c in catches {
947                strip_empty(&mut c.body);
948            }
949            if let Some(f) = finally {
950                strip_empty(f);
951            }
952        }
953        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => strip_empty(body),
954        _ => {}
955    }
956}
957
958fn merge_singleton_stmts(s: &mut Stmt) {
959    if let Stmt::Block(v) = s {
960        for x in v.iter_mut() {
961            merge_singleton_stmts(x);
962        }
963    }
964}
965
966/// Drop statements after a definite terminator within a block.
967pub fn prune_unreachable(s: &mut Stmt) {
968    if let Stmt::Block(v) = s {
969        let mut cut: Option<usize> = None;
970        for (i, x) in v.iter().enumerate() {
971            if matches!(x, Stmt::Return(_) | Stmt::Throw(_)) {
972                cut = Some(i + 1);
973                break;
974            }
975        }
976        if let Some(c) = cut {
977            if c < v.len() {
978                v.truncate(c);
979            }
980        }
981        for x in v.iter_mut() {
982            prune_unreachable(x);
983        }
984    } else {
985        walk_mut(s, &mut prune_unreachable);
986    }
987}
988
989fn walk_mut<F: FnMut(&mut Stmt)>(s: &mut Stmt, f: &mut F) {
990    match s {
991        Stmt::Block(v) => {
992            for x in v.iter_mut() {
993                f(x);
994            }
995        }
996        Stmt::If {
997            then_stmt,
998            else_stmt,
999            ..
1000        } => {
1001            f(then_stmt);
1002            if let Some(e) = else_stmt {
1003                f(e);
1004            }
1005        }
1006        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => f(body),
1007        Stmt::For { init, body, .. } => {
1008            for x in init.iter_mut() {
1009                f(x);
1010            }
1011            f(body);
1012        }
1013        Stmt::ForEach { body, .. } => f(body),
1014        Stmt::Switch { cases, default, .. } => {
1015            for c in cases {
1016                for x in c.body.iter_mut() {
1017                    f(x);
1018                }
1019            }
1020            if let Some(d) = default {
1021                f(d);
1022            }
1023        }
1024        Stmt::Try {
1025            body,
1026            catches,
1027            finally,
1028        } => {
1029            f(body);
1030            for c in catches {
1031                f(&mut c.body);
1032            }
1033            if let Some(fl) = finally {
1034                f(fl);
1035            }
1036        }
1037        Stmt::TryWithResources {
1038            resources,
1039            body,
1040            catches,
1041            finally,
1042        } => {
1043            for r in resources.iter_mut() {
1044                f(r);
1045            }
1046            f(body);
1047            for c in catches {
1048                f(&mut c.body);
1049            }
1050            if let Some(fl) = finally {
1051                f(fl);
1052            }
1053        }
1054        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => f(body),
1055        _ => {}
1056    }
1057}
1058
1059/// Count LocalDef statements (pipeline diagnostics).
1060pub fn count_localdefs(s: &Stmt, out: &mut usize) {
1061    if matches!(s, Stmt::LocalDef { .. }) {
1062        *out += 1;
1063    }
1064    walk_all(s, &mut |x| {
1065        if matches!(x, Stmt::LocalDef { .. }) {
1066            *out += 1;
1067        }
1068    });
1069}
1070
1071/// Drop a trailing `return;` (d8's explicit return-void at method end).
1072pub fn strip_trailing_void_return(s: &mut Stmt) {
1073    if let Stmt::Block(v) = s {
1074        loop {
1075            match v.last_mut() {
1076                Some(Stmt::Return(None)) => {
1077                    v.pop();
1078                }
1079                // The structurer can close a region in a bare nested
1080                // block; a `return;` at its tail is still a dangling
1081                // statement (inside a static initializer it is illegal
1082                // outright — clinit's closing return).
1083                Some(Stmt::Block(_)) => {
1084                    let last = v.last_mut().unwrap();
1085                    let before = match last {
1086                        Stmt::Block(b) => b.len(),
1087                        _ => 0,
1088                    };
1089                    strip_trailing_void_return(last);
1090                    let after = match last {
1091                        Stmt::Block(b) => b.len(),
1092                        _ => 0,
1093                    };
1094                    if after == before {
1095                        break; // nothing stripped inside
1096                    }
1097                }
1098                _ => break,
1099            }
1100        }
1101    }
1102}
1103
1104/// `if (c) { } else { B }` → `if (!c) { B }` — the empty-then shape is
1105/// how the structurer lands an inverted diamond, and source never writes
1106/// it (jadx prints the positive form). negate() carries De Morgan so
1107/// composed conditions stay clean.
1108pub fn invert_empty_thens(s: &mut Stmt) {
1109    walk_mut_deep(s, &mut |st| {
1110        if let Stmt::If {
1111            cond,
1112            then_stmt,
1113            else_stmt,
1114        } = st
1115        {
1116            let then_empty = matches!(&**then_stmt, Stmt::Block(v) if v.is_empty());
1117            if then_empty {
1118                if let Some(els) = else_stmt.take() {
1119                    let c = std::mem::replace(cond, Expr::Const(ConstVal::Int(0)));
1120                    *cond = jdc_core::convert::negate(c);
1121                    *then_stmt = els;
1122                }
1123            }
1124        }
1125    });
1126}
1127
1128/// Fold if-diamonds back into short-circuit conditions (DAD's
1129/// short_circuit_struct, statement level). The structurer emits nested
1130/// ifs for `a && b` / `a || b` bytecode diamonds; javac source almost
1131/// never nests them, and jadx folds all four shapes:
1132///   if (c1) { if (c2) {T} else {F} } else {F}  →  if (c1 && c2) {T} else {F}
1133///   if (c1) { if (c2) {F} else {T} } else {F}  →  if (c1 && !c2) {T} else {F}
1134///   if (c1) {T} else { if (c2) {T} else {F} }  →  if (c1 || c2) {T} else {F}
1135///   if (c1) {T} else { if (c2) {F} else {T} }  →  if (c1 || !c2) {T} else {F}
1136/// Branch identity is deep structural equality (Stmt: PartialEq);
1137/// iterate to a fixpoint so chained diamonds `(a && b) && c` collapse.
1138pub fn fold_short_circuits(s: &mut Stmt) {
1139    for _ in 0..8 {
1140        let mut changed = false;
1141        walk_mut_deep(s, &mut |st| {
1142            if try_fold_diamond(st) {
1143                changed = true;
1144            }
1145        });
1146        if !changed {
1147            break;
1148        }
1149    }
1150}
1151
1152fn try_fold_diamond(st: &mut Stmt) -> bool {
1153    enum Shape {
1154        ThenAnd,
1155        ThenAndNot,
1156        ElseOr,
1157        ElseOrNot,
1158        BareAnd,
1159        BareAndNot,
1160    }
1161    // Probe the shape on an immutable borrow first (the fold takes the
1162    // whole If apart; matching and rebuilding inside one borrow is not
1163    // expressible).
1164    let shape = match &*st {
1165        // One-sided diamonds: the shared branch is the implicit empty
1166        // fall-through (`if (c1) { if (c2) {T} }` → `if (c1 && c2) {T}`).
1167        Stmt::If {
1168            then_stmt,
1169            else_stmt: None,
1170            ..
1171        } => match &**then_stmt {
1172            Stmt::If {
1173                then_stmt: t2,
1174                else_stmt: None,
1175                ..
1176            } if !matches!(&**t2, Stmt::Block(v) if v.is_empty()) => Some(Shape::BareAnd),
1177            Stmt::If {
1178                then_stmt: t2,
1179                else_stmt: Some(_),
1180                ..
1181            } if matches!(&**t2, Stmt::Block(v) if v.is_empty()) => Some(Shape::BareAndNot),
1182            _ => None,
1183        },
1184        Stmt::If {
1185            then_stmt,
1186            else_stmt: Some(els),
1187            ..
1188        } => match (&**then_stmt, &**els) {
1189            (Stmt::If { else_stmt: Some(f2), .. }, _) if f2.as_ref() == els.as_ref() => {
1190                Some(Shape::ThenAnd)
1191            }
1192            (Stmt::If { then_stmt: t2, else_stmt: Some(_), .. }, _)
1193                if t2.as_ref() == els.as_ref() =>
1194            {
1195                Some(Shape::ThenAndNot)
1196            }
1197            (_, Stmt::If { then_stmt: t2, else_stmt: Some(_), .. })
1198                if t2.as_ref() == then_stmt.as_ref() =>
1199            {
1200                Some(Shape::ElseOr)
1201            }
1202            (_, Stmt::If { else_stmt: Some(f2), .. })
1203                if f2.as_ref() == then_stmt.as_ref() =>
1204            {
1205                Some(Shape::ElseOrNot)
1206            }
1207            _ => None,
1208        },
1209        _ => None,
1210    };
1211    let Some(shape) = shape else {
1212        return false;
1213    };
1214
1215    let taken = std::mem::replace(st, Stmt::Block(vec![]));
1216    // Bare (no-else) diamonds fold without an else at all.
1217    if matches!(shape, Shape::BareAnd | Shape::BareAndNot) {
1218        let Stmt::If {
1219            cond: c1,
1220            then_stmt,
1221            else_stmt: None,
1222        } = taken
1223        else {
1224            unreachable!()
1225        };
1226        let Stmt::If {
1227            cond: c2,
1228            then_stmt: t2,
1229            else_stmt: f2,
1230        } = *then_stmt
1231        else {
1232            unreachable!()
1233        };
1234        let sc = |op: BinOp, l: Expr, r: Expr| Expr::Bin {
1235            op,
1236            l: Box::new(l),
1237            r: Box::new(r),
1238            ty: None,
1239        };
1240        match shape {
1241            // if (c1 && c2) {T}
1242            Shape::BareAnd => {
1243                *st = Stmt::If {
1244                    cond: sc(BinOp::LogAnd, c1, c2),
1245                    then_stmt: t2,
1246                    else_stmt: None,
1247                };
1248            }
1249            // if (c1) { if (c2) {} else {T} }  →  if (c1 && !c2) {T}
1250            _ => {
1251                let body = f2.unwrap();
1252                *st = Stmt::If {
1253                    cond: sc(
1254                        BinOp::LogAnd,
1255                        c1,
1256                        Expr::Un {
1257                            op: UnOp::Not,
1258                            e: Box::new(c2),
1259                        },
1260                    ),
1261                    then_stmt: body,
1262                    else_stmt: None,
1263                };
1264            }
1265        }
1266        return true;
1267    }
1268    let Stmt::If {
1269        cond: c1,
1270        then_stmt,
1271        else_stmt: Some(els),
1272    } = taken
1273    else {
1274        unreachable!("shape probe guaranteed an If with else")
1275    };
1276    let sc = |op: BinOp, l: Expr, r: Expr| Expr::Bin {
1277        op,
1278        l: Box::new(l),
1279        r: Box::new(r),
1280        ty: None,
1281    };
1282    let not = |e: Expr| Expr::Un {
1283        op: UnOp::Not,
1284        e: Box::new(e),
1285    };
1286    match shape {
1287        Shape::BareAnd | Shape::BareAndNot => unreachable!("handled above"),
1288        Shape::ThenAnd | Shape::ThenAndNot => {
1289            let Stmt::If {
1290                cond: c2,
1291                then_stmt: t2,
1292                else_stmt: Some(f2),
1293            } = *then_stmt
1294            else {
1295                unreachable!()
1296            };
1297            let (nc, body, alt) = match shape {
1298                // if (c1 && c2) {T} else {F}
1299                Shape::ThenAnd => (sc(BinOp::LogAnd, c1, c2), t2, f2),
1300                // if (c1 && !c2) {T} else {F}   (inner: {F} else {T})
1301                _ => (sc(BinOp::LogAnd, c1, not(c2)), f2, t2),
1302            };
1303            *st = Stmt::If {
1304                cond: nc,
1305                then_stmt: body,
1306                else_stmt: Some(alt),
1307            };
1308        }
1309        Shape::ElseOr | Shape::ElseOrNot => {
1310            let Stmt::If {
1311                cond: c2,
1312                then_stmt: t2,
1313                else_stmt: Some(f2),
1314            } = *els
1315            else {
1316                unreachable!()
1317            };
1318            // The surviving then-body is the OUTER then (identical to the
1319            // matching inner branch by the probe).
1320            let body = then_stmt;
1321            let (nc, alt) = match shape {
1322                // if (c1 || c2) {T} else {F}   (inner: {T} else {F})
1323                Shape::ElseOr => (sc(BinOp::LogOr, c1, c2), f2),
1324                // if (c1 || !c2) {T} else {F}  (inner: {F} else {T})
1325                _ => (sc(BinOp::LogOr, c1, not(c2)), t2),
1326            };
1327            *st = Stmt::If {
1328                cond: nc,
1329                then_stmt: body,
1330                else_stmt: Some(alt),
1331            };
1332        }
1333    }
1334    true
1335}
1336
1337/// A static initializer cannot contain `return` in source form — the
1338/// clinit's closing return is a method-level artifact, but the
1339/// structurer can leave it nested inside loops/branches where the
1340/// top-level strip cannot see it (x2/a: `static { while { ...; return;
1341/// } }` — "return outside method", 184 sites on reqable).
1342pub fn strip_clinit_returns(s: &mut Stmt) {
1343    walk_mut_deep(s, &mut |st| {
1344        if let Stmt::Block(v) = st {
1345            v.retain(|x| !matches!(x, Stmt::Return(None)));
1346        }
1347    });
1348}
1349
1350pub fn prepend_comment(s: &mut Stmt, text: String) {
1351    let stmt = Stmt::Comment(text);
1352    match s {
1353        Stmt::Block(v) => v.insert(0, stmt),
1354        other => {
1355            let inner = std::mem::replace(other, Stmt::Block(vec![]));
1356            *other = Stmt::Block(vec![stmt, inner]);
1357        }
1358    }
1359}
1360
1361/// Fused expression rewrites that each used to walk the whole tree:
1362/// residual cmp sentinels, object-null comparisons, const-first compares.
1363/// One traversal, three rules — the separate walks were a top profile cost.
1364/// True when `e` evaluates to an object reference — a legal null-compare
1365/// side: locals via the reference table, everything else via its own type
1366/// (fields like `pi.versionName != 0` and call results are references too;
1367/// constants never are).
1368fn is_obj_expr(e: &Expr, obj_var: &dyn Fn(u32) -> bool) -> bool {
1369    match e {
1370        Expr::Local { var, .. } => obj_var(*var),
1371        Expr::Const(_) => false,
1372        _ => e.type_ref().erased().is_reference(),
1373    }
1374}
1375
1376/// `obj == 0` / `0 == obj` → `obj == null`: replace the CONST-0 side.
1377/// The previous shape replaced the LOCAL side instead — every object
1378/// null-check in the corpus rendered as `null != 0` (4,598 hits in
1379/// reqable alone) and the real operand vanished from the condition.
1380fn null_side_rewrite(x: &mut Expr, obj_var: &dyn Fn(u32) -> bool) {
1381    if let Expr::Bin { op, l, r, .. } = x {
1382        if !matches!(op, BinOp::Eq | BinOp::Ne) {
1383            return;
1384        }
1385        let l_zero = matches!(&**l, Expr::Const(ConstVal::Int(0)));
1386        let r_zero = matches!(&**r, Expr::Const(ConstVal::Int(0)));
1387        if l_zero && !r_zero && is_obj_expr(r, obj_var) {
1388            **l = Expr::Const(ConstVal::Null);
1389        } else if r_zero && !l_zero && is_obj_expr(l, obj_var) {
1390            **r = Expr::Const(ConstVal::Null);
1391        }
1392    }
1393}
1394
1395pub fn fused_expr_rewrites(s: &mut Stmt, vt: &VarTable) {
1396    // var ids are dense: a byte table beats a HashSet lookup.
1397    let mut obj_vars: Vec<bool> = Vec::with_capacity(vt.vars.len());
1398    for v in &vt.vars {
1399        obj_vars.push(v.ty.erased().is_reference());
1400    }
1401    rewrite_exprs(s, &mut |e| {
1402        deep_rewrite(e, &mut |x| {
1403            // 1. cmp sentinels.
1404            if let Expr::Invokedynamic { name, args, .. } = x {
1405                if name.starts_with('\0') && args.len() == 2 {
1406                    let (cls, ty) = match name.as_str() {
1407                        "\0cmp-long" => ("java/lang/Long", JavaType::Long),
1408                        "\0cmpl-float" | "\0cmpg-float" => ("java/lang/Float", JavaType::Float),
1409                        _ => ("java/lang/Double", JavaType::Double),
1410                    };
1411                    let desc = MethodDescriptor {
1412                        args: vec![ty.clone(), ty],
1413                        ret: JavaType::Int,
1414                    };
1415                    *x = Expr::Method {
1416                        owner: None,
1417                        cls: cls.into(),
1418                        name: "compare".into(),
1419                        desc: std::sync::Arc::new(desc),
1420                        args: args.clone(),
1421                        is_static: true,
1422                        is_interface: false,
1423                        is_special: false,
1424                        is_super: false,
1425                        is_dynamic: false,
1426                        type_args: vec![],
1427                    };
1428                    return;
1429                }
1430            }
1431            // 2. null compares (no any_obj gate: the object side may be
1432            // a field/call whose type is not in the local table).
1433            null_side_rewrite(x, &|v| obj_vars.get(v as usize).copied().unwrap_or(false));
1434            // 3. const-first compares.
1435            if let Expr::Bin { op, l, r, .. } = x {
1436                if matches!(
1437                    op,
1438                    BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
1439                ) {
1440                    let l_const = matches!(&**l, Expr::Const(_));
1441                    let r_var = matches!(&**r, Expr::Local { .. });
1442                    if l_const && r_var {
1443                        std::mem::swap(l, r);
1444                    }
1445                }
1446            }
1447        });
1448    });
1449    // 4. null into reference-typed assignment targets. The register
1450    //    machine stores null as const-0; return/throw/field-store/invoke
1451    //    argument paths were normalized in the lifter, but a null that
1452    //    MATERIALIZES into a local was rendered as `str = 0;` — javac
1453    //    "int cannot be converted to String" (the single biggest error
1454    //    family on real corpora: d8 emits null locals this way after
1455    //    every `x = null` branch).
1456    walk_mut_deep(s, &mut |st| {
1457        if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = st {
1458            let tgt_is_obj = match &**target {
1459                Expr::Local { var, .. } => {
1460                    obj_vars.get(*var as usize).copied().unwrap_or(false)
1461                }
1462                other => other.type_ref().erased().is_reference(),
1463            };
1464            if tgt_is_obj {
1465                if let Expr::Const(ConstVal::Int(0)) = &**value {
1466                    **value = Expr::Const(ConstVal::Null);
1467                }
1468            }
1469        } else if let Stmt::LocalDef { var, init, .. } = st {
1470            if obj_vars.get(*var as usize).copied().unwrap_or(false) {
1471                if let Some(iv) = init {
1472                    if let Expr::Const(ConstVal::Int(0)) = iv {
1473                        *iv = Expr::Const(ConstVal::Null);
1474                    }
1475                }
1476            }
1477        }
1478    });
1479}
1480
1481/// Replace residual cmp sentinels (`\0cmp*` Invokedynamic markers that never
1482/// reached a condition) with library compare calls.
1483#[allow(dead_code)]
1484pub fn desugar_cmp_residuals(s: &mut Stmt) {
1485    rewrite_exprs(s, &mut |e| {
1486        deep_rewrite(e, &mut |x| {
1487            if let Expr::Invokedynamic { name, args, .. } = x {
1488                if name.starts_with('\0') && args.len() == 2 {
1489                    let (cls, ty) = match name.as_str() {
1490                        "\0cmp-long" => ("java/lang/Long", JavaType::Long),
1491                        "\0cmpl-float" | "\0cmpg-float" => ("java/lang/Float", JavaType::Float),
1492                        _ => ("java/lang/Double", JavaType::Double),
1493                    };
1494                    let desc = jdc_core::types::MethodDescriptor {
1495                        args: vec![ty.clone(), ty],
1496                        ret: JavaType::Int,
1497                    };
1498                    *x = Expr::Method {
1499                        owner: None,
1500                        cls: cls.into(),
1501                        name: "compare".into(),
1502                        desc: std::sync::Arc::new(desc),
1503                        args: args.clone(),
1504                        is_static: true,
1505                        is_interface: false,
1506                        is_special: false,
1507                        is_super: false,
1508                        is_dynamic: false,
1509                        type_args: vec![],
1510                    };
1511                }
1512            }
1513        });
1514    });
1515}
1516
1517/// `if (c) { x = A; } else { x = B; }` → `x = c ? A : B;`
1518pub fn ternary_fold(s: &mut Stmt) {
1519    let mut changed = true;
1520    let mut guard = 0;
1521    while changed && guard < 8 {
1522        changed = false;
1523        guard += 1;
1524        ternary_fold_walk(s, &mut changed);
1525        strip_empty(s);
1526    }
1527}
1528
1529fn ternary_fold_walk(s: &mut Stmt, changed: &mut bool) {
1530    if let Stmt::Block(v) = s {
1531        for x in v.iter_mut() {
1532            ternary_fold_walk(x, changed);
1533        }
1534        return;
1535    }
1536    let Stmt::If {
1537        cond,
1538        then_stmt,
1539        else_stmt,
1540    } = s
1541    else {
1542        return;
1543    };
1544    ternary_fold_walk(then_stmt, changed);
1545    if let Some(e) = else_stmt {
1546        ternary_fold_walk(e, changed);
1547    }
1548    let Some(else_b) = else_stmt else { return };
1549    let target_of = |st: &Stmt| -> Option<u32> {
1550        let Stmt::ExprStmt(Expr::Assign { target, .. }) = st else {
1551            return None;
1552        };
1553        if let Expr::Local { var, .. } = &**target {
1554            Some(*var)
1555        } else {
1556            None
1557        }
1558    };
1559    let (Stmt::Block(tv), Stmt::Block(ev)) = (then_stmt.as_mut(), else_b.as_mut()) else {
1560        return;
1561    };
1562    if tv.len() != 1 || ev.len() != 1 {
1563        return;
1564    }
1565    let (Some(vt_), Some(ve)) = (target_of(&tv[0]), target_of(&ev[0])) else {
1566        return;
1567    };
1568    if vt_ != ve {
1569        return;
1570    }
1571    // The condition and both values must not re-assign the target between
1572    // the fold — they are single statements, so a mention is a re-read at
1573    // worst; re-assignment only happens via assignments inside them.
1574    let mut touched = HashSet::default();
1575    let (Stmt::ExprStmt(a_then), Stmt::ExprStmt(a_else)) = (&tv[0], &ev[0]) else {
1576        return;
1577    };
1578    if let Expr::Assign { value, .. } = a_then {
1579        collect_vars(value, &mut touched);
1580    }
1581    if let Expr::Assign { value, .. } = a_else {
1582        collect_vars(value, &mut touched);
1583    }
1584    if touched.contains(&vt_) {
1585        return;
1586    }
1587    let value_then = match a_then {
1588        Expr::Assign { value, .. } => value.clone(),
1589        _ => return,
1590    };
1591    let value_else = match a_else {
1592        Expr::Assign { value, .. } => value.clone(),
1593        _ => return,
1594    };
1595    let cond_e = cond.clone();
1596    let folded = Stmt::ExprStmt(Expr::Assign {
1597        target: Box::new(Expr::Local {
1598            var: vt_,
1599            ty: vt_join_ty(&value_then, &value_else),
1600        }),
1601        op: AssignOp::Plain,
1602        value: Box::new(Expr::Cond {
1603            c: Box::new(cond_e),
1604            t: value_then,
1605            f: value_else,
1606        }),
1607    });
1608    *s = Stmt::Block(vec![folded]);
1609    *changed = true;
1610}
1611
1612fn vt_join_ty(a: &Expr, b: &Expr) -> TypeRef {
1613    let ta = a.type_ref();
1614    let tb = b.type_ref();
1615    if ta.erased() == tb.erased() {
1616        return ta;
1617    }
1618    TypeRef::J(JavaType::Object("java/lang/Object".into()))
1619}
1620
1621/// Fold `new StringBuilder(...).append(x)...toString()` chains into
1622/// `StringConcat`. Single-assignment temporaries carry the chain.
1623///
1624/// d8 drops append results (statement-form calls), so a builder whose chain
1625/// has STATEMENT appends attached is left alone: folding its `toString`
1626/// with only the ctor arguments would misstate the contents.
1627pub fn fold_string_builders(s: &mut Stmt, vt: &VarTable) {
1628    // var → assigned value, for vars assigned exactly once.
1629    let n_vars = vt.vars.len().max(1);
1630    let mut counts = vec![0usize; n_vars];
1631    count_assignments(s, &mut counts);
1632    let mut values: Vec<Option<Expr>> = vec![None; n_vars];
1633    record_assignments(s, &counts, &mut values);
1634
1635    // Builders with statement-form appends (their chains are incomplete).
1636    let mut appended_stmts: HashSet<u32> = HashSet::default();
1637    walk_all(s, &mut |st| {
1638        if let Stmt::ExprStmt(Expr::Method {
1639            cls,
1640            name,
1641            owner,
1642            args,
1643            ..
1644        }) = st
1645        {
1646            if name.as_ref() == "append" && is_string_builder(cls) && args.len() == 1 {
1647                if let Some(Expr::Local { var, .. }) = owner.as_deref() {
1648                    appended_stmts.insert(*var);
1649                }
1650            }
1651        }
1652    });
1653
1654    rewrite_exprs(s, &mut |e| {
1655        fold_concat_in_expr(e, &values, &appended_stmts);
1656    });
1657
1658    // Drop now-unused builder temporaries (their only consumers were
1659    // folded); the drop COUNT drives iteration — no statement counting
1660    // walks at all.
1661    for _ in 0..8 {
1662        let mut reads: HashSet<u32> = HashSet::default();
1663        stmt_collect_vars(s, &mut reads, false);
1664        if drop_unused_assigns(s, &reads) == 0 {
1665            break;
1666        }
1667    }
1668}
1669
1670/// Deep statement count (pipeline guard input).
1671pub fn count_stmts_deep(s: &Stmt, out: &mut usize) {
1672    *out += 1;
1673    walk_all(s, &mut |_| {
1674        *out += 1;
1675    });
1676}
1677
1678/// Fused per-variable analysis gathered in one statement-tree walk.
1679struct VarAnalysis {
1680    assigns: Vec<usize>,
1681    reads: Vec<usize>,
1682    /// Value of the (single) assignment, when recorded.
1683    values: Vec<Option<Expr>>,
1684}
1685
1686fn analyze_vars(s: &Stmt, a: &mut VarAnalysis) {
1687    walk_all(s, &mut |st| {
1688        let exprs: Vec<&Expr> = match st {
1689            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
1690                if let Expr::Local { var, .. } = &**target {
1691                    grow_to(&mut a.assigns, *var);
1692                    let idx = *var as usize;
1693                    a.assigns[idx] += 1;
1694                    if a.assigns[idx] == 1 {
1695                        if idx >= a.values.len() {
1696                            a.values.resize(idx + 1, None);
1697                        }
1698                        a.values[idx] = Some((**value).clone());
1699                    } else if idx < a.values.len() {
1700                        a.values[idx] = None;
1701                    }
1702                }
1703                let mut v: Vec<&Expr> = vec![value];
1704                if !matches!(&**target, Expr::Local { .. }) {
1705                    v.push(target);
1706                }
1707                v
1708            }
1709            Stmt::ExprStmt(e @ (Expr::PreIncDec { .. } | Expr::PostIncDec { .. })) => {
1710                // `v++` is a read-modify-WRITE of v: counting it as a
1711                // plain read makes v a single-assign/single-read forward
1712                // candidate, and value-forwarding then fabricates `5++`
1713                // (or drops v's def and leaves ++ on an undefined name).
1714                let inner = match e {
1715                    Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => e,
1716                    _ => unreachable!(),
1717                };
1718                if let Expr::Local { var, .. } = &**inner {
1719                    grow_to(&mut a.assigns, *var);
1720                    let idx = *var as usize;
1721                    a.assigns[idx] += 1;
1722                    if idx < a.values.len() {
1723                        a.values[idx] = None;
1724                    }
1725                    return;
1726                }
1727                vec![e]
1728            }
1729            Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
1730                vec![e]
1731            }
1732            Stmt::Return(Some(e)) => vec![e],
1733            Stmt::LocalDef {
1734                var, init: Some(e), ..
1735            } => {
1736                grow_to(&mut a.assigns, *var);
1737                let idx = *var as usize;
1738                a.assigns[idx] += 1;
1739                if a.assigns[idx] == 1 {
1740                    if idx >= a.values.len() {
1741                        a.values.resize(idx + 1, None);
1742                    }
1743                    a.values[idx] = Some(e.clone());
1744                } else if idx < a.values.len() {
1745                    a.values[idx] = None;
1746                }
1747                vec![e]
1748            }
1749            Stmt::If { cond, .. } => vec![cond],
1750            Stmt::While { cond, .. } => vec![cond],
1751            Stmt::DoWhile { cond, .. } => vec![cond],
1752            _ => Vec::new(),
1753        };
1754        for e in exprs {
1755            visit_exprs(e, &mut |x| {
1756                if let Expr::Local { var, .. } = x {
1757                    grow_to(&mut a.reads, *var);
1758                    a.reads[*var as usize] += 1;
1759                }
1760            });
1761        }
1762    });
1763    // Keep the three views length-aligned.
1764    let n = a.assigns.len().max(a.reads.len()).max(a.values.len());
1765    a.assigns.resize(n, 0);
1766    a.reads.resize(n, 0);
1767    a.values.resize(n, None);
1768}
1769
1770fn grow_to(v: &mut Vec<usize>, var: u32) {
1771    if var as usize >= v.len() {
1772        v.resize(var as usize + 1, 0);
1773    }
1774}
1775
1776fn count_assignments(s: &Stmt, counts: &mut Vec<usize>) {
1777    walk_all(s, &mut |st| match st {
1778        Stmt::ExprStmt(Expr::Assign { target, .. }) => {
1779            if let Expr::Local { var, .. } = &**target {
1780                grow_to(counts, *var);
1781                counts[*var as usize] += 1;
1782            }
1783        }
1784        Stmt::LocalDef {
1785            var, init: Some(_), ..
1786        } => {
1787            grow_to(counts, *var);
1788            counts[*var as usize] += 1;
1789        }
1790        _ => {}
1791    });
1792}
1793
1794fn record_assignments(s: &Stmt, counts: &[usize], out: &mut Vec<Option<Expr>>) {
1795    walk_all(s, &mut |st| match st {
1796        Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
1797            if let Expr::Local { var, .. } = &**target {
1798                if counts.get(*var as usize).copied().unwrap_or(0) == 1 {
1799                    if *var as usize >= out.len() {
1800                        out.resize(*var as usize + 1, None);
1801                    }
1802                    out[*var as usize] = Some((**value).clone());
1803                }
1804            }
1805        }
1806        Stmt::LocalDef {
1807            var, init: Some(e), ..
1808        } if counts.get(*var as usize).copied().unwrap_or(0) == 1 => {
1809            if *var as usize >= out.len() {
1810                out.resize(*var as usize + 1, None);
1811            }
1812            out[*var as usize] = Some(e.clone());
1813        }
1814        _ => {}
1815    });
1816}
1817
1818fn fold_concat_in_expr(e: &mut Expr, values: &[Option<Expr>], appended: &HashSet<u32>) {
1819    deep_rewrite(e, &mut |x| {
1820        if let Expr::Method {
1821            cls,
1822            name,
1823            args,
1824            owner,
1825            ..
1826        } = x
1827        {
1828            if name.as_ref() == "toString" && args.is_empty() && is_string_builder(cls) {
1829                if let Some(o) = owner {
1830                    // Statement-form appends attached to any chain var mean
1831                    // the parts are incomplete — keep the call.
1832                    let mut has_stmt_appends = false;
1833                    visit_exprs(o, &mut |y| {
1834                        if let Expr::Local { var, .. } = y {
1835                            if appended.contains(var) {
1836                                has_stmt_appends = true;
1837                            }
1838                        }
1839                    });
1840                    if has_stmt_appends {
1841                        return;
1842                    }
1843                    let resolved = resolve_local(o, values);
1844                    if let Some(parts) = collect_sb_parts(&resolved, values, 0) {
1845                        // NEVER fold an empty chain: d8 splits builder
1846                        // chains across alias registers (`v2 = v0.append
1847                        // (x); v2.append(y); v0.toString()`) — the traced
1848                        // chain sees a bare `new StringBuilder` and would
1849                        // fold toString() to "" (a wrong VALUE, silently:
1850                        // lab package Obf.label). With zero parts the
1851                        // call stays — always correct, usually folded
1852                        // elsewhere once the parts are visible.
1853                        if !parts.is_empty() {
1854                            *x = Expr::StringConcat(parts);
1855                        }
1856                    }
1857                }
1858            }
1859        }
1860    });
1861}
1862
1863fn is_string_builder(cls: &str) -> bool {
1864    cls == "java/lang/StringBuilder" || cls == "java/lang/StringBuffer"
1865}
1866
1867fn resolve_local(e: &Expr, values: &[Option<Expr>]) -> Expr {
1868    match e {
1869        Expr::Local { var, ty } => values
1870            .get(*var as usize)
1871            .and_then(|o| o.clone())
1872            .unwrap_or_else(|| Expr::Local {
1873                var: *var,
1874                ty: ty.clone(),
1875            }),
1876        other => other.clone(),
1877    }
1878}
1879
1880/// Collect concat parts from a StringBuilder chain; depth caps cycles.
1881fn collect_sb_parts(e: &Expr, values: &[Option<Expr>], depth: u32) -> Option<Vec<ConcatPart>> {
1882    if depth > 16 {
1883        return None;
1884    }
1885    match e {
1886        Expr::Method {
1887            cls,
1888            name,
1889            args,
1890            owner,
1891            ..
1892        } if name.as_ref() == "append" && is_string_builder(cls) && args.len() == 1 => {
1893            let mut parts =
1894                collect_sb_parts(&resolve_local(owner.as_deref()?, values), values, depth + 1)?;
1895            parts.push(ConcatPart::Str(args[0].clone()));
1896            Some(parts)
1897        }
1898        Expr::New {
1899            cls,
1900            args,
1901            raw: false,
1902            ..
1903        } if is_string_builder(cls) => {
1904            let mut parts = Vec::new();
1905            for a in args {
1906                if let Expr::Const(ConstVal::Str(sv)) = a {
1907                    parts.push(ConcatPart::Const(sv.to_string()));
1908                } else {
1909                    parts.push(ConcatPart::Str(a.clone()));
1910                }
1911            }
1912            Some(parts)
1913        }
1914        _ => None,
1915    }
1916}
1917
1918fn drop_unused_assigns(s: &mut Stmt, reads: &HashSet<u32>) -> usize {
1919    let mut dropped = 0usize;
1920    walk_mut_deep(s, &mut |st| {
1921        if let Stmt::Block(v) = st {
1922            let before = v.len();
1923            v.retain(|x| match x {
1924                // Drop only when unread AND a builder chain (folded
1925                // consumer; `new StringBuilder` chains cannot NPE).
1926                Stmt::ExprStmt(Expr::Assign { target, value, .. }) => match &**target {
1927                    Expr::Local { var, .. } => reads.contains(var) || !is_sbish(value),
1928                    _ => true,
1929                },
1930                Stmt::LocalDef { var, init, .. } => {
1931                    reads.contains(var) || !init.as_ref().map(is_sbish).unwrap_or(false)
1932                }
1933                _ => true,
1934            });
1935            dropped += before - v.len();
1936        }
1937    });
1938    dropped
1939}
1940
1941fn is_sbish(e: &Expr) -> bool {
1942    match e {
1943        Expr::New {
1944            cls, raw: false, ..
1945        } => is_string_builder(cls),
1946        Expr::Method { name, cls, .. } => name.as_ref() == "append" && is_string_builder(cls),
1947        _ => false,
1948    }
1949}
1950
1951fn walk_mut_deep<F: FnMut(&mut Stmt)>(s: &mut Stmt, f: &mut F) {
1952    f(s);
1953    walk_mut(s, &mut |x| walk_mut_deep(x, f));
1954}
1955
1956/// Drop orphaned raw `new-instance` statements. R8 sometimes separates
1957/// `new-instance vR` from its `<init>` invoke by a register copy
1958/// (`move-object vS, vR; invoke vS.<init>(..)` — dex-legal: the init
1959/// initializes the object both registers name). The fold pairs the init
1960/// with the COPY, and the structurer's branch duplication re-materializes
1961/// the original raw marker as a bare `new C();` statement on paths whose
1962/// uses were rewired to the folded side (weixin sns/storage/c0: four
1963/// `new s1();` beside the real `s1 s1x = new s1(..)` — s1 has no no-arg
1964/// ctor, "无法将类 C的构造器 C应用到给定类型" ×195). A raw New in
1965/// statement position binds no variable and initializes nothing — pure
1966/// verifier-artifact dead code.
1967pub fn drop_dead_raw_news(body: &mut Stmt) {
1968    fn is_orphan(s: &Stmt) -> bool {
1969        matches!(s, Stmt::ExprStmt(Expr::New { raw: true, .. }))
1970    }
1971    walk_mut_deep(body, &mut |st| match st {
1972        Stmt::Block(v) => v.retain(|x| !is_orphan(x)),
1973        Stmt::Switch { cases, .. } => {
1974            for c in cases.iter_mut() {
1975                c.body.retain(|x| !is_orphan(x));
1976            }
1977        }
1978        Stmt::For { init, .. } => init.retain(|x| !is_orphan(x)),
1979        Stmt::TryWithResources { resources, .. } => resources.retain(|x| !is_orphan(x)),
1980        _ => {}
1981    });
1982}
1983
1984/// Recover `synchronized` from the d8 monitor pattern:
1985/// `monitorenter(e); try { body } catch (Throwable) { monitorexit(e); throw t; }`
1986/// (optionally followed by `monitorexit(e)`).
1987pub fn fold_synchronized(s: &mut Stmt) {
1988    fold_sync_walk(s);
1989}
1990
1991fn fold_sync_walk(s: &mut Stmt) {
1992    walk_mut_deep(s, &mut |st| {
1993        let Stmt::Block(v) = st else { return };
1994        let mut i = 0;
1995        while i < v.len() {
1996            if i + 1 < v.len() {
1997                if let Some(sync) = try_sync_at(v, i) {
1998                    let _ = v.drain(i..i + 2);
1999                    v.insert(i, sync);
2000                    continue;
2001                }
2002            }
2003            i += 1;
2004        }
2005    });
2006}
2007
2008fn try_sync_at(v: &[Stmt], i: usize) -> Option<Stmt> {
2009    let Stmt::MonitorEnter(lock) = &v[i] else {
2010        return None;
2011    };
2012    let Stmt::Try {
2013        body,
2014        catches,
2015        finally,
2016    } = &v[i + 1]
2017    else {
2018        return None;
2019    };
2020    if finally.is_some() || catches.len() != 1 {
2021        return None;
2022    }
2023    let c = &catches[0];
2024    if !c.exc.is_empty() {
2025        return None;
2026    }
2027    // The catch-all body: monitorexit(lock) then rethrow.
2028    let Stmt::Block(cv) = c.body.as_ref() else {
2029        return None;
2030    };
2031    let exit_matches = cv.len() >= 2
2032        && matches!(&cv[0], Stmt::MonitorExit(e) if expr_local_var(e) == expr_local_var(lock));
2033    let throws = cv.len() >= 2 && matches!(&cv[1], Stmt::Throw(_));
2034    if !(exit_matches && throws) {
2035        return None;
2036    }
2037    Some(Stmt::Synchronized {
2038        lock: lock.clone(),
2039        body: body.clone(),
2040    })
2041}
2042
2043fn expr_local_var(e: &Expr) -> Option<u32> {
2044    if let Expr::Local { var, .. } = e {
2045        Some(*var)
2046    } else {
2047        None
2048    }
2049}
2050
2051/// Copy-forward single-use temporaries: `v = e; ... v ...` inlines `e` at
2052/// the (single) use and drops the assignment when safe. Pure values inline
2053/// anywhere; impure values (calls, array reads) only when the use is the
2054/// statement immediately after the definition.
2055pub fn forward_single_use(s: &mut Stmt, _vt: &VarTable) {
2056    // Fused single traversal: assignment counts, read counts and the
2057    // single-assignment values all come out of ONE walk (three separate
2058    // full-tree walks dominated the pass cost on large methods).
2059    let mut analysis = VarAnalysis {
2060        assigns: Vec::new(),
2061        reads: Vec::new(),
2062        values: Vec::new(),
2063    };
2064    analyze_vars(s, &mut analysis);
2065    let mut assigns = analysis.assigns;
2066    let mut reads = analysis.reads;
2067    // Var ids beyond the table (synthetic test shapes) are covered by the
2068    // counters' on-demand growth.
2069    let n_vars = assigns.len().max(_vt.vars.len()).max(1);
2070    assigns.resize(n_vars, 0);
2071    reads.resize(n_vars, 0);
2072    let mut values = analysis.values;
2073    values.resize(n_vars, None);
2074
2075    // Candidates: assigned exactly once, read exactly once. Additionally
2076    // the inlined VALUE must not reference a multi-assigned var (phi vars):
2077    // moving the read across the phi's reassignment would be a stale
2078    // capture (register rotations snapshot through temps for this reason).
2079    let mut cand = vec![false; n_vars];
2080    let mut edges: Vec<Vec<u32>> = vec![Vec::new(); n_vars];
2081    for (v, cand_v) in cand.iter_mut().enumerate() {
2082        if assigns.get(v).copied().unwrap_or(0) != 1 || reads.get(v).copied().unwrap_or(0) != 1 {
2083            continue;
2084        }
2085        if let Some(val) = values.get(v).and_then(|o| o.as_ref()) {
2086            let mut refs = HashSet::default();
2087            collect_vars(val, &mut refs);
2088            if refs.iter().any(|r| assigns[*r as usize] > 1) {
2089                continue;
2090            }
2091            // Growth-graph edge set, built in the same walk: inlining v
2092            // inserts a clone of its value, and every Local inside the
2093            // clone is replaced in turn.
2094            edges[v] = refs.into_iter().collect();
2095        }
2096        *cand_v = true;
2097    }
2098
2099    // Cycle rejection on the growth graph. A def-reference cycle
2100    // (v = f(w), w = g(v) — loop-carried register rotation reaching the
2101    // pass as two mutually-referencing single-assign/single-read locals)
2102    // re-introduces the other cycle Local at every replacement level, so
2103    // deep_rewrite grows the tree one layer per level and never
2104    // terminates: weixin's com/tencent/mm/plugin/appbrand/widget/input/b4
2105    // exhausted a 64MB worker stack at ~300k recursion frames. Edges into
2106    // non-candidates are harmless (they are never replaced, so growth
2107    // dies there — and they carry no outgoing edges).
2108    //
2109    // Three-color DFS, iterative: a legit 60k-long single-use chain must
2110    // not trade one stack overflow for another. A GRAY child closes a
2111    // cycle; `bad[v]` (v on a cycle, or v's inlined closure grows into
2112    // one) then taints the whole current path — everything on it reaches
2113    // the cycle. Black verdicts memoize: a node whose subtree was proven
2114    // clean cannot grow a cycle later.
2115    let mut color = vec![0u8; n_vars]; // 0 white, 1 gray, 2 black
2116    let mut bad = vec![false; n_vars];
2117    for root in 0..n_vars {
2118        if color[root] != 0 || edges[root].is_empty() {
2119            continue;
2120        }
2121        color[root] = 1;
2122        let mut stack: Vec<(usize, usize)> = vec![(root, 0)];
2123        while let Some(top) = stack.last_mut() {
2124            let v = top.0;
2125            let i = top.1;
2126            top.1 += 1;
2127            if i < edges[v].len() {
2128                let r = edges[v][i] as usize;
2129                if color[r] == 1 {
2130                    for &(pn, _) in &stack {
2131                        bad[pn] = true;
2132                    }
2133                } else if color[r] == 0 {
2134                    color[r] = 1;
2135                    stack.push((r, 0));
2136                } else if bad[r] {
2137                    for &(pn, _) in &stack {
2138                        bad[pn] = true;
2139                    }
2140                }
2141            } else {
2142                color[v] = 2;
2143                stack.pop();
2144            }
2145        }
2146    }
2147    let single: Vec<bool> = (0..n_vars).map(|v| cand[v] && !bad[v]).collect();
2148    if !single.iter().any(|&b| b) {
2149        return;
2150    }
2151    let pure: Vec<bool> = (0..n_vars)
2152        .map(|v| {
2153            single[v]
2154                && values
2155                    .get(v)
2156                    .and_then(|o| o.as_ref())
2157                    .map(|e| !has_side_effects(e))
2158                    .unwrap_or(false)
2159        })
2160        .collect();
2161    let impure: Vec<bool> = (0..n_vars).map(|v| single[v] && !pure[v]).collect();
2162
2163    // 1. Pure values: inline anywhere.
2164    if pure.iter().any(|&b| b) {
2165        let vals: Vec<Option<Expr>> = (0..n_vars)
2166            .map(|v| {
2167                if pure[v] {
2168                    values.get(v).cloned().flatten()
2169                } else {
2170                    None
2171                }
2172            })
2173            .collect();
2174        rewrite_exprs(s, &mut |e| {
2175            deep_rewrite_reads(e, &mut |x| {
2176                if let Expr::Local { var, .. } = x {
2177                    if let Some(Some(v)) = vals.get(*var as usize) {
2178                        *x = v.clone();
2179                    }
2180                }
2181            });
2182        });
2183        drop_defs(s, &pure);
2184    }
2185
2186    // 2. Impure values: inline only into the immediately following
2187    //    statement.
2188    if impure.iter().any(|&b| b) {
2189        forward_adjacent_impure(s, &values, &impure);
2190    }
2191}
2192
2193/// Count READS of variables (assignment targets excluded).
2194fn count_reads(s: &Stmt, out: &mut Vec<usize>) {
2195    walk_all(s, &mut |st| {
2196        let exprs: Vec<&Expr> = match st {
2197            // Assign must match BEFORE the generic ExprStmt arm (the
2198            // assignment target is a write, not a read).
2199            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
2200                let mut v: Vec<&Expr> = vec![value];
2201                if !matches!(&**target, Expr::Local { .. }) {
2202                    v.push(target);
2203                }
2204                v
2205            }
2206            // `v++` writes v — not a read (a compound `a[i]++` still
2207            // reads its owner/index subtree).
2208            Stmt::ExprStmt(e @ (Expr::PreIncDec { .. } | Expr::PostIncDec { .. })) => {
2209                let inner = match e {
2210                    Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => e,
2211                    _ => unreachable!(),
2212                };
2213                if matches!(**inner, Expr::Local { .. }) {
2214                    Vec::new()
2215                } else {
2216                    vec![e]
2217                }
2218            }
2219            Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
2220                vec![e]
2221            }
2222            Stmt::Return(Some(e)) => vec![e],
2223            Stmt::LocalDef { init: Some(e), .. } => vec![e],
2224            Stmt::If { cond, .. } => vec![cond],
2225            Stmt::While { cond, .. } => vec![cond],
2226            Stmt::DoWhile { cond, .. } => vec![cond],
2227            // Read sites that are NOT statement children (walk_all recurses
2228            // into bodies but these payloads are expressions on the node
2229            // itself). Omitting them under-counts reads, which would let
2230            // drop_dead_locals over-prune a var whose only use is a switch
2231            // selector / loop condition / lock / iterable.
2232            Stmt::Switch { selector, .. } => vec![selector],
2233            Stmt::ForEach { iterable, .. } => vec![iterable],
2234            Stmt::Synchronized { lock, .. } => vec![lock],
2235            Stmt::For { cond, update, .. } => {
2236                let mut v: Vec<&Expr> = Vec::with_capacity(update.len() + 1);
2237                if let Some(c) = cond {
2238                    v.push(c);
2239                }
2240                v.extend(update.iter());
2241                v
2242            }
2243            Stmt::Assert { cond, msg } => {
2244                let mut v: Vec<&Expr> = vec![cond];
2245                if let Some(m) = msg {
2246                    v.push(m);
2247                }
2248                v
2249            }
2250            _ => Vec::new(),
2251        };
2252        for e in exprs {
2253            visit_exprs(e, &mut |x| {
2254                if let Expr::Local { var, .. } = x {
2255                    grow_to(out, *var);
2256                    out[*var as usize] += 1;
2257                }
2258            });
2259        }
2260    });
2261}
2262
2263fn drop_defs(s: &mut Stmt, vars: &[bool]) {
2264    walk_mut_deep(s, &mut |st| {
2265        if let Stmt::Block(v) = st {
2266            v.retain(|x| match x {
2267                Stmt::ExprStmt(Expr::Assign { target, .. }) => match &**target {
2268                    Expr::Local { var, .. } => !vars.get(*var as usize).copied().unwrap_or(false),
2269                    _ => true,
2270                },
2271                Stmt::LocalDef { var, .. } => !vars.get(*var as usize).copied().unwrap_or(false),
2272                _ => true,
2273            });
2274        }
2275    });
2276}
2277
2278/// For each statement list: `v = IMPURE; NEXT(v)` folds NEXT's reference.
2279fn forward_adjacent_impure(s: &mut Stmt, values: &[Option<Expr>], impure: &[bool]) {
2280    if let Stmt::Block(v) = s {
2281        let mut i = 0;
2282        while i + 1 < v.len() {
2283            let def_var = match &v[i] {
2284                Stmt::ExprStmt(Expr::Assign { target, .. }) => match &**target {
2285                    Expr::Local { var, .. } => Some(*var),
2286                    _ => None,
2287                },
2288                Stmt::LocalDef { var, .. } => Some(*var),
2289                _ => None,
2290            };
2291            if let Some(var) = def_var {
2292                if impure.get(var as usize).copied().unwrap_or(false) {
2293                    // Does the NEXT statement reference var?
2294                    let mut reads_here = vec![0usize; values.len().max(1)];
2295                    count_reads(&v[i + 1], &mut reads_here);
2296                    if reads_here.get(var as usize).copied().unwrap_or(0) == 1 {
2297                        // The def statement's CURRENT value (earlier inlines
2298                        // in this walk may have rewritten it — the pre-built
2299                        // values map would be stale).
2300                        let val = match &v[i] {
2301                            Stmt::ExprStmt(Expr::Assign { value, .. }) => (**value).clone(),
2302                            Stmt::LocalDef { init: Some(e), .. } => (*e).clone(),
2303                            _ => {
2304                                i += 1;
2305                                continue;
2306                            }
2307                        };
2308                        rewrite_exprs(&mut v[i + 1], &mut |e| {
2309                            deep_rewrite_reads(e, &mut |x| {
2310                                if let Expr::Local { var: v2, .. } = x {
2311                                    if *v2 == var {
2312                                        *x = val.clone();
2313                                    }
2314                                }
2315                            });
2316                        });
2317                        v.remove(i);
2318                        // The PREVIOUS definition may now be adjacent to its
2319                        // (shifted) use — re-examine it.
2320                        i = i.saturating_sub(1);
2321                        continue;
2322                    }
2323                }
2324            }
2325            i += 1;
2326        }
2327    }
2328    walk_mut(s, &mut |x| forward_adjacent_impure(x, values, impure));
2329}
2330
2331// ---------------------------------------------------------------------------
2332// Type inference, booleanization, null comparisons, declarations
2333// ---------------------------------------------------------------------------
2334
2335/// Evidence-based type inference for synthetic (non-parameter) vars.
2336pub fn infer_types(vt: &mut VarTable, body: &mut Stmt, ret: &JavaType, env: &MethodEnv) {
2337    let n = vt.vars.len();
2338    // (type, strong) — DIRECTIONAL evidence. Strong facts (an assigned
2339    // value's type, the declared return/throw/monitor type) DEFINE the
2340    // variable; weak facts (call argument expectations, receiver
2341    // contexts, numeric literals) only constrain a use. jadx's bound
2342    // system carries the same direction; a flat pool let a weak
2343    // expectation (`v26 = p1` with boolean p1 beside `v26 = 0` sugar)
2344    // outvote the definition.
2345    let mut evidence: Vec<Vec<(JavaType, bool)>> = vec![Vec::new(); n];
2346    let ev = |evidence: &mut Vec<Vec<(JavaType, bool)>>, var: u32, t: JavaType, strong: bool| {
2347        if (var as usize) < evidence.len() {
2348            evidence[var as usize].push((t, strong));
2349        }
2350    };
2351
2352    walk_all(body, &mut |st| match st {
2353        Stmt::LocalDef { var, init, .. } => {
2354            if let Some(e) = init {
2355                expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
2356                // The initializer DEFINES the declared type: `long v = l(...)`
2357                // must not stay `int v` (silent 64-bit truncation in the
2358                // eyes of a reader; javac rejects it as lossy). Null
2359                // carries no type (it would DOWNGRADE String to Object).
2360                // Literals are weak (0/1 is boolean sugar half the time);
2361                // typed producers and parameter sources are strong;
2362                // local-to-local copies are weak (register reuse).
2363                let strong = match e {
2364                    Expr::Const(_) => false,
2365                    Expr::Local { var: src, .. } => {
2366                        let src_ty = vt.vars.get(*src as usize).map(|v| v.ty.erased());
2367                        let tgt_ty = vt.vars.get(*var as usize).map(|v| v.ty.erased());
2368                        match (src_ty, tgt_ty) {
2369                            (Some(s), Some(g)) => {
2370                                (s.is_numeric() && g.is_numeric())
2371                                    || (s.is_reference() && g.is_reference())
2372                            }
2373                            _ => false,
2374                        }
2375                    }
2376                    _ => true,
2377                };
2378                ev(&mut evidence, *var, e.type_ref().erased(), strong);
2379            }
2380        }
2381        Stmt::ExprStmt(e) => {
2382            // Assignment targets: a typed PRODUCER (method/new/field) or a
2383            // PARAMETER source is a STRONG definition (`v26 = p1` with
2384            // boolean p1). A local-to-local copy is weak — register reuse
2385            // and merge materialization emit exactly that shape with
2386            // mismatched types (`sb7 = compareTo10` in a Kotlin when).
2387            // Literals stay weak.
2388            if let Expr::Assign { target, value, .. } = e {
2389                // A field WRITE through a local (`v.f = x`, dex iput)
2390                // proves v is a reference of the field's declaring
2391                // class AT THAT POINT — an iput on a boolean register
2392                // cannot exist in valid dex. Strong: it must outrank a
2393                // strong boolean copy chain into the same reused slot
2394                // (weixin coroutine state machines).
2395                if let Expr::Field { owner: Some(o), cls, is_static: false, .. } = &**target {
2396                    if cls.is_empty() {
2397                        // array-length pseudo-field: no declaring class
2398                    } else if let Expr::Local { var, .. } = &**o {
2399                        ev(
2400                            &mut evidence,
2401                            *var,
2402                            JavaType::Object(cls.clone()),
2403                            true,
2404                        );
2405                    }
2406                }
2407                if let Expr::Local { var, .. } = &**target {
2408                    let strong = match &**value {
2409                        Expr::Const(_) => false,
2410                        Expr::Local { var: src, .. } => {
2411                            // A local copy is strong when the source and
2412                            // the target's register type are the same
2413                            // FAMILY (numeric↔numeric, reference↔
2414                            // reference) — a cross-family copy (int into
2415                            // a StringBuilder local) is merge-material
2416                            // residue and stays weak.
2417                            let src_ty = vt.vars.get(*src as usize).map(|v| v.ty.erased());
2418                            let tgt_ty = vt.vars.get(*var as usize).map(|v| v.ty.erased());
2419                            match (src_ty, tgt_ty) {
2420                                (Some(s), Some(g)) => {
2421                                    (s.is_numeric() && g.is_numeric())
2422                                        || (s.is_reference() && g.is_reference())
2423                                }
2424                                _ => false,
2425                            }
2426                        }
2427                        _ => true,
2428                    };
2429                    ev(&mut evidence, *var, value.type_ref().erased(), strong);
2430                }
2431            }
2432            expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
2433        }
2434        Stmt::Throw(e) => {
2435            expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
2436            if let Expr::Local { var, .. } = e {
2437                ev(
2438                    &mut evidence,
2439                    *var,
2440                    JavaType::Object("java/lang/Throwable".into()),
2441                    true,
2442                );
2443            }
2444        }
2445        Stmt::Return(Some(e)) => {
2446            expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
2447            if let Expr::Local { var, .. } = e {
2448                ev(&mut evidence, *var, ret.clone(), true);
2449            }
2450        }
2451        Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
2452            if let Expr::Local { var, .. } = e {
2453                ev(
2454                    &mut evidence,
2455                    *var,
2456                    JavaType::Object("java/lang/Object".into()),
2457                    true,
2458                );
2459            }
2460        }
2461        Stmt::ForEach {
2462            var,
2463            iterable,
2464            is_array,
2465            ..
2466        } => {
2467            if let Expr::Local { var: v0, .. } = iterable {
2468                if *is_array {
2469                    ev(&mut evidence, *v0, JavaType::Array(Box::new(JavaType::Int)), true);
2470                } else {
2471                    ev(
2472                        &mut evidence,
2473                        *v0,
2474                        JavaType::Object("java/lang/Object".into()),
2475                        true,
2476                    );
2477                }
2478            }
2479            let _ = var;
2480        }
2481        _ => {}
2482    });
2483    let _ = env;
2484
2485    // Return-position locals take the declared return type (also inside
2486    // nested returns — handled by the walk above).
2487
2488        // Vars dereferenced as field/method receivers or arrays: their
2489    // resolved type must be a dereferenceable class — a boxed Boolean
2490    // in the strong set (Kotlin Result registers hold `Boolean | impl`
2491    // across coroutine case arms) must lose to the real class even
2492    // when the Boolean evidence comes first (weixin ry0/h2's
2493    // `Boolean bool19` receiving `bool19.d = x`).
2494    let mut derefed: Vec<bool> = vec![false; vt.vars.len()];
2495    mark_derefs(body, &mut derefed);
2496    // Array-indexed vars: their resolved type must be an ARRAY when
2497    // the evidence has one (`num4[v68]` with Integer-typed num4 —
2498    // 需要数组但找到Integer family); member derefs keep the class
2499    // preference below.
2500    let mut arr_derefed: Vec<bool> = vec![false; vt.vars.len()];
2501    visit_all_exprs(body, &mut |x| {
2502        if let Expr::ArrayIndex { array, .. } = x {
2503            if let Expr::Local { var, .. } = &**array {
2504                if (*var as usize) < arr_derefed.len() {
2505                    arr_derefed[*var as usize] = true;
2506                }
2507            }
2508        }
2509        // `v != null` is STRONG reference evidence: the lifter only
2510        // renders the null form when the dex value view held a
2511        // reference register at that pc — an int-typed generation
2512        // compared against null is phi-confluence residue (yq0/g1's
2513        // `v95_g12 != null`, 546-line 二元运算符 family). Strong Object
2514        // makes the resolver retype and split_generations separate the
2515        // int writes into their own generation.
2516        if let Expr::Bin { op, l, r, .. } = x {
2517            if matches!(op, BinOp::Eq | BinOp::Ne)
2518                && (matches!(&**r, Expr::Const(ConstVal::Null))
2519                    || matches!(&**l, Expr::Const(ConstVal::Null)))
2520            {
2521                for side in [l, r] {
2522                    if let Expr::Local { var, .. } = &**side {
2523                        ev(
2524                            &mut evidence,
2525                            *var,
2526                            JavaType::Object("java/lang/Object".into()),
2527                            true,
2528                        );
2529                    }
2530                }
2531            }
2532        }
2533    });
2534    const FINAL_JDK: &[&str] = &[
2535        "java/lang/Boolean",
2536        "java/lang/String",
2537        "java/lang/Integer",
2538        "java/lang/Long",
2539        "java/lang/Short",
2540        "java/lang/Byte",
2541        "java/lang/Character",
2542        "java/lang/Float",
2543        "java/lang/Double",
2544    ];
2545    for (i, evs) in evidence.iter().enumerate() {
2546        let info = &vt.vars[i];
2547        if info.is_param {
2548            continue;
2549        }
2550        let strong: Vec<JavaType> = evs
2551            .iter()
2552            .filter(|(_, s)| *s)
2553            .map(|(t, _)| t.clone())
2554            .collect();
2555        let all: Vec<JavaType> = evs.iter().map(|(t, _)| t.clone()).collect();
2556        if info.ty.erased().is_reference() {
2557            // Materialization residue: no strong definition, and the weak
2558            // pool names ≥2 DIFFERENT classes (a Kotlin `when` lowered
2559            // every branch's value into one register slot) — neither
2560            // class can win, every assignment needs boxing headroom:
2561            // widen to Object (uses get receiver casts).
2562            if strong.is_empty() {
2563                let refs: std::collections::HashSet<&str> = all
2564                    .iter()
2565                    .filter_map(|t| match t {
2566                        JavaType::Object(n) if n.as_ref() != "java/lang/Object" => {
2567                            Some(n.as_ref())
2568                        }
2569                        _ => None,
2570                    })
2571                    .collect();
2572                let mixed = all.iter().any(|t| t.is_numeric());
2573                if refs.len() >= 2 || (mixed && !refs.is_empty()) || (mixed && all.iter().any(|t| matches!(t, JavaType::Array(_)))) {
2574                    vt.vars[i].ty =
2575                        TypeRef::J(JavaType::Object("java/lang/Object".into()));
2576                    continue;
2577                }
2578            }
2579                        // Array-dereferenced vars prefer array evidence (an
2580            // Integer producer cannot be indexed).
2581            if arr_derefed[i] {
2582                let arr = strong
2583                    .iter()
2584                    .chain(all.iter())
2585                    .find(|t| matches!(t, JavaType::Array(_)))
2586                    .cloned();
2587                if let Some(t) = arr {
2588                    vt.vars[i].ty = TypeRef::J(t);
2589                    continue;
2590                }
2591            }
2592            // Strong definitions outrank weak expectations. A
2593            // dereferenced var prefers a real class over final JDK
2594            // boxes (Boolean can never carry `.d = x`).
2595            let deref_pick = || {
2596                if !derefed[i] {
2597                    return None;
2598                }
2599                strong
2600                    .iter()
2601                    .find(|t| {
2602                        matches!(t, JavaType::Object(n)
2603                            if !n.is_empty()
2604                                && n.as_ref() != "java/lang/Object"
2605                                && !FINAL_JDK.contains(&n.as_ref()))
2606                    })
2607                    .cloned()
2608            };
2609            if let Some(t) = deref_pick()
2610                .or_else(|| pick_object(&strong))
2611                .or_else(|| pick_object(&all))
2612            {
2613                vt.vars[i].ty = TypeRef::J(t);
2614            }
2615        } else if let Some(t) = pick_object(&strong) {
2616            // A strong reference definition (typed producer or field
2617            // write) outranks a strong boolean copy: Boolean assigns
2618            // INTO an Object slot legally, never the reverse.
2619            vt.vars[i].ty = TypeRef::J(t);
2620        } else if strong.contains(&JavaType::Boolean) {
2621            // A boolean-typed SOURCE assigned into the variable is a
2622            // definition (`v26 = p1` with boolean p1); the 0/1 literals
2623            // that share the pool are boolean sugar as often as not.
2624            vt.vars[i].ty = TypeRef::J(JavaType::Boolean);
2625        } else if !all.is_empty() && all.iter().all(|t| t.is_numeric()) {
2626            if let Some(t) = pick_numeric(&all) {
2627                vt.vars[i].ty = TypeRef::J(t);
2628            }
2629        } else {
2630            // Mixed-family materialization on a numeric-declared slot
2631            // (int slot receiving StringBuilders — the register was the
2632            // `when` accumulator): Object, same as the reference side.
2633            let refs = all.iter().filter(|t| t.is_reference()).count();
2634            let nums = all.iter().filter(|t| t.is_numeric()).count();
2635            if refs > 0 && nums > 0 {
2636                vt.vars[i].ty =
2637                    TypeRef::J(JavaType::Object("java/lang/Object".into()));
2638            } else if let Some(t) = pick_object(&strong).or_else(|| pick_object(&all)) {
2639                vt.vars[i].ty = TypeRef::J(t);
2640            }
2641        }
2642    }
2643
2644    // Rewrite embedded Local types. Borrowed view + write-only-on-change:
2645    // the table clone was n_vars JavaType clones per method, and the
2646    // rewrite cloned a type into EVERY Local node — most already carry
2647    // the right one (JavaType::Object clones allocate).
2648    let types: Vec<&TypeRef> = vt.vars.iter().map(|v| &v.ty).collect();
2649    rewrite_exprs(body, &mut |e| {
2650        deep_rewrite(e, &mut |x| {
2651            if let Expr::Local { var, ty } = x {
2652                if let Some(want) = types.get(*var as usize) {
2653                    if ty != *want {
2654                        *ty = (*want).clone();
2655                    }
2656                }
2657            }
2658        });
2659    });
2660
2661    // Numeric literals take their variable's declared width (`double v = 0L`
2662    // prints as `0.0`; `int x = 5L` as `5`).
2663    coerce_num_consts(body, &types);
2664}
2665
2666fn coerce_num_consts(body: &mut Stmt, types: &[&TypeRef]) {
2667    walk_mut_deep(body, &mut |st| {
2668        let (var, val): (u32, &mut Expr) = match st {
2669            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => match &mut **target {
2670                Expr::Local { var, .. } => (*var, value),
2671                _ => return,
2672            },
2673            Stmt::LocalDef {
2674                var, init: Some(e), ..
2675            } => (*var, e),
2676            _ => return,
2677        };
2678        let want = match types.get(var as usize) {
2679            Some(TypeRef::J(t)) => t.clone(),
2680            _ => return,
2681        };
2682        let rewritten = match &*val {
2683            Expr::Const(ConstVal::Long(l)) => match &want {
2684                JavaType::Double => Some(Expr::Const(ConstVal::Double(*l as f64))),
2685                JavaType::Float => Some(Expr::Const(ConstVal::Float(*l as f32))),
2686                _ => None,
2687            },
2688            Expr::Const(ConstVal::Int(i)) => match &want {
2689                JavaType::Double => Some(Expr::Const(ConstVal::Double(*i as f64))),
2690                JavaType::Float => Some(Expr::Const(ConstVal::Float(*i as f32))),
2691                _ => None,
2692            },
2693            _ => None,
2694        };
2695        if let Some(e) = rewritten {
2696            *val = e;
2697        }
2698    });
2699}
2700
2701fn pick_object(evs: &[JavaType]) -> Option<JavaType> {
2702    evs.iter()
2703        .find(|t| {
2704            t.is_reference()
2705                && !matches!(t, JavaType::Object(n) if n.is_empty() || n.as_ref() == "java/lang/Object")
2706        })
2707        .cloned()
2708}
2709
2710fn pick_numeric(evs: &[JavaType]) -> Option<JavaType> {
2711    if evs.is_empty() {
2712        return None;
2713    }
2714    let mut t = evs[0].clone();
2715    for x in &evs[1..] {
2716        t = join_numeric(&t, x);
2717    }
2718    Some(t)
2719}
2720
2721fn join_numeric(a: &JavaType, b: &JavaType) -> JavaType {
2722    if a == b {
2723        return a.clone();
2724    }
2725    match (a, b) {
2726        (JavaType::Double, _) | (_, JavaType::Double) => JavaType::Double,
2727        (JavaType::Float, _) | (_, JavaType::Float) => JavaType::Float,
2728        (JavaType::Long, _) | (_, JavaType::Long) => JavaType::Long,
2729        (JavaType::Int, x) if x.is_integral() => JavaType::Int,
2730        (x, JavaType::Int) if x.is_integral() => JavaType::Int,
2731        (x, y) if x.is_integral() && y.is_integral() => JavaType::Int,
2732        _ => JavaType::Int,
2733    }
2734}
2735
2736/// Gather per-var type evidence from one expression.
2737fn expr_evidence<F: FnMut(u32, JavaType)>(e: &Expr, f: &mut F) {
2738    visit_exprs(e, &mut |x| match x {
2739        Expr::Method {
2740            owner,
2741            args,
2742            desc,
2743            is_static,
2744            cls,
2745            ..
2746        } => {
2747            if !*is_static {
2748                if let Some(o) = owner {
2749                    if let Expr::Local { var, .. } = &**o {
2750                        f(*var, JavaType::Object(cls.clone()));
2751                    }
2752                }
2753            }
2754            for (i, a) in args.iter().enumerate() {
2755                if let Expr::Local { var, .. } = a {
2756                    if let Some(t) = desc.args.get(i) {
2757                        f(*var, t.clone());
2758                    }
2759                }
2760            }
2761        }
2762        Expr::Field {
2763            owner,
2764            cls,
2765            ty,
2766            is_static,
2767            ..
2768        } => {
2769            if !*is_static && !cls.is_empty() {
2770                if let Some(o) = owner {
2771                    if let Expr::Local { var, .. } = &**o {
2772                        // The field ref's declaring class is dex-exact
2773                        // evidence — the old hardcoded Object let a
2774                        // boolean copy chain outvote it (weixin ry0/h2's
2775                        // `Boolean bool19` receiving `bool19.d = x` —
2776                        // the 339-line 找不到符号 变量 d family).
2777                        // EMPTY cls = the array-length pseudo-field
2778                        // (lift models `a.length` as Field{cls:""}) —
2779                        // Object("") evidence poisoned pick_object into
2780                        // empty-typed declarations and `() x` casts.
2781                        f(*var, JavaType::Object(cls.clone()));
2782                    }
2783                }
2784            }
2785            let _ = ty;
2786        }
2787        // `v != null` / `v == null`: the register holds a reference at
2788        // that point — weak Object evidence keeps int-typed generations
2789        // from winning the pool (yq0/g1's `v95_g12 != null` with int
2790        // v95_g12 — 二元运算符 '!=' 546-line family).
2791        Expr::Bin { op, l, r, .. }
2792            if matches!(op, BinOp::Eq | BinOp::Ne)
2793                && (matches!(&**r, Expr::Const(ConstVal::Null))
2794                    || matches!(&**l, Expr::Const(ConstVal::Null))) =>
2795        {
2796            for side in [l, r] {
2797                if let Expr::Local { var, .. } = &**side {
2798                    f(*var, JavaType::Object("java/lang/Object".into()));
2799                }
2800            }
2801        }
2802        Expr::ArrayIndex { array, .. } => {
2803            if let Expr::Local { var, ty, .. } = &**array {
2804                // Reinforce the local's KNOWN array type: the hardcoded
2805                // int[] fallback contradicted a typed array local (a
2806                // register reused for byte[], int[] then byte[] across
2807                // one clinit labeled the byte[] defs `int[]`).
2808                let t = ty.erased();
2809                if matches!(t, JavaType::Array(_)) {
2810                    f(*var, t);
2811                } else {
2812                    f(*var, JavaType::Array(Box::new(JavaType::Int)));
2813                }
2814            }
2815        }
2816        Expr::Assign { .. } => {
2817            // Covered by the typed strong-evidence walk in infer_types;
2818            // keeping a weak duplicate here let expectations outvote it.
2819        }
2820        Expr::Cast { e: inner, .. } => {
2821            // A cast is an EXPLICIT narrowing at the use site — it must
2822            // NOT feed the cast target as evidence for the variable
2823            // (`Object get2 = list.get(2); j((String) get2);` had get2
2824            // retyped to String, making the assignment incompatible).
2825            // jadx's bounds carry direction: a USE bound narrows nothing.
2826            let _ = inner;
2827        }
2828        Expr::InstanceOf { e: inner, .. } => {
2829            let _ = inner;
2830        }
2831        _ => {}
2832    });
2833}
2834
2835/// Mark locals that OWN a non-static field access and whose inferred type
2836/// is a concrete `$<digits>` class (a d8/R8-desugared lambda or a Kotlin
2837/// suspend-lambda). Emit demotes such a type to its SAM interface in
2838/// DECLARED positions (`Function2 v = new Outer$..$1(..)`) for phi-friendly
2839/// readability — but interfaces carry no instance fields, so the Kotlin
2840/// coroutine `create()` capture writes (`v.L$0 = obj`, also I$/J$) render
2841/// "找不到符号 变量 L$0" (weibo MutableScatterMap$..$iterator$1, ~1k).
2842/// Forcing the concrete declared type keeps the field access legal; the phi
2843/// case is excluded because a disagreed merge stays Object (wide_stack_vars),
2844/// never a specific concrete class. Gate mirrors emit's demotion exactly
2845/// (last `$` segment all digits) so nothing else changes.
2846pub fn mark_field_owner_concrete(vt: &mut VarTable, body: &mut Stmt) {
2847    let mut owners: HashSet<u32> = HashSet::default();
2848    rewrite_exprs(body, &mut |e| {
2849        visit_exprs(e, &mut |x| {
2850            if let Expr::Field {
2851                owner: Some(o),
2852                is_static: false,
2853                ..
2854            } = x
2855            {
2856                if let Expr::Local { var, .. } = &**o {
2857                    owners.insert(*var);
2858                }
2859            }
2860        });
2861    });
2862    for var in owners {
2863        let i = var as usize;
2864        if i >= vt.vars.len() {
2865            continue;
2866        }
2867        if let JavaType::Object(cls) = vt.vars[i].ty.erased() {
2868            if let Some(last) = cls.rsplit('$').next() {
2869                if !last.is_empty() && last.chars().all(|c| c.is_ascii_digit()) {
2870                    vt.force_concrete_vars.insert(var);
2871                }
2872            }
2873        }
2874    }
2875    // EVERY local whose resolved type is a concrete `$<digits>` class
2876    // keeps its binary name, not just field owners: when one side of an
2877    // assignment falls back to the SAM/super (`ContinuationImpl v7`) and
2878    // its sibling stays concrete (`$reportWhenComplete$1 v9`), the pair
2879    // is an uncastable "ContinuationImpl无法转换为…$1" (weibo coroutine
2880    // prologues ×282). The anon fallback still serves INTERFACE-typed
2881    // phi slots (branches minting different lambdas) — their vt type is
2882    // the join, never the $N class.
2883    let concrete: Vec<u32> = vt
2884        .vars
2885        .iter()
2886        .filter(|v| {
2887            if let JavaType::Object(cls) = v.ty.erased() {
2888                cls.rsplit('$')
2889                    .next()
2890                    .is_some_and(|l| !l.is_empty() && l.chars().all(|c| c.is_ascii_digit()))
2891            } else {
2892                false
2893            }
2894        })
2895        .map(|v| v.id)
2896        .collect();
2897    for var in concrete {
2898        vt.force_concrete_vars.insert(var);
2899    }
2900}
2901
2902/// Type inference can leave a specific-reference-typed target assigned
2903/// from a `java/lang/Object`-typed value: a phi that merged String and
2904/// Object then settled on String (`String str4; ... str4 = obj;` where
2905/// `obj` is an Object field — y5/n.java), or an Object local flowing to a
2906/// typed field. Java requires a narrowing cast there; insert `(T) value`
2907/// at the USE site (round-40 philosophy: narrow at use, never retype the
2908/// declaration). Guarded tightly: only when the value's resolved static
2909/// type is EXACTLY `java/lang/Object` (the untyped top — a known subtype
2910/// is never re-cast), the value is not a null const (assignable to any
2911/// ref) nor already a cast, and the target is a specific reference type
2912/// (Object→int/boolean is unboxing, a different problem left alone).
2913pub fn insert_object_narrowing_casts(
2914    vt: &VarTable,
2915    body: &mut Stmt,
2916    pool: &DexPool,
2917    ret: &JavaType,
2918) {
2919    // Resolve a value's static type through the VarTable for locals (the
2920    // embedded Local ty can lag infer_types), else the expr's own type.
2921    let value_ty = |e: &Expr| -> JavaType {
2922        match e {
2923            Expr::Local { var, .. } => vt.var(*var).ty.erased(),
2924            other => other.type_ref().erased(),
2925        }
2926    };
2927    let is_top_object = |e: &Expr| -> bool {
2928        value_ty(e) == JavaType::Object("java/lang/Object".into())
2929    };
2930    // A specific reference target type (a class other than
2931    // java/lang/Object, or an array — `String[] v = obj` needs the
2932    // cast exactly like `String v = obj`).
2933    let specific_ref = |ty: &TypeRef| -> Option<TypeRef> {
2934        match ty.erased() {
2935            JavaType::Object(c) if c.as_ref() != "java/lang/Object" => Some(ty.clone()),
2936            JavaType::Array(_) => Some(ty.clone()),
2937            _ => None,
2938        }
2939    };
2940    let castable = |e: &Expr| -> bool {
2941        is_top_object(e)
2942            && !matches!(e, Expr::Const(_) | Expr::Cast { .. } | Expr::InstanceOf { .. })
2943    };
2944    // Downcast witness: a SPECIFIC-ref value whose static type does not
2945    // fit the specific-ref target (`v9($1) = v7` with v7:
2946    // ContinuationImpl — weibo coroutine prologue ×282). The dex move
2947    // was verifier-proven against the target type, so the cast states
2948    // the bytecode truth; an actually-unconvertible static pair (both
2949    // classes, unrelated — the value view conflated) rides the
2950    // emitter's (Object) relay and still compiles. Framework types the
2951    // pool cannot order take the cast too: a downcast renders directly,
2952    // an unrelated pair relays.
2953    let needs_downcast = |e: &Expr, tt: &JavaType| -> bool {
2954        if matches!(e, Expr::Const(_) | Expr::Cast { .. } | Expr::InstanceOf { .. }) {
2955            return false;
2956        }
2957        let v = value_ty(e);
2958        let (JavaType::Object(vn), JavaType::Object(tn)) = (&v, tt) else {
2959            return false;
2960        };
2961        if vn.as_ref() == "java/lang/Object" || tn.as_ref() == "java/lang/Object" {
2962            return false;
2963        }
2964        vn != tn && !pool.is_subtype(vn, tn)
2965    };
2966    walk_mut_deep(body, &mut |st| match st {
2967        Stmt::ExprStmt(Expr::Assign {
2968            target,
2969            value,
2970            op: AssignOp::Plain,
2971            ..
2972        }) => {
2973            let tgt = match &**target {
2974                Expr::Local { var, .. } => vt.var(*var).ty.clone(),
2975                Expr::Field { ty, .. } => ty.clone(),
2976                // Array-element store `arr[i] = value`: the target type is
2977                // the ELEMENT type. Without this arm an Object→element
2978                // assignment (`String[] a; a[0] = pair.first` — Pair.first
2979                // erases to Object) skipped the cast and failed "Object
2980                // 无法转换为String" (weixin ×~150). Resolve the array's type
2981                // through the vt (the embedded Local ty can lag infer_types).
2982                Expr::ArrayIndex { array, .. } => {
2983                    let aty = match &**array {
2984                        Expr::Local { var, .. } => vt.var(*var).ty.erased(),
2985                        other => other.type_ref().erased(),
2986                    };
2987                    match aty {
2988                        JavaType::Array(inner) => TypeRef::J(*inner),
2989                        _ => return,
2990                    }
2991                }
2992                _ => return,
2993            };
2994            if let Some(t) = specific_ref(&tgt) {
2995                if castable(value) || needs_downcast(value, &tgt.erased()) {
2996                    let v = std::mem::replace(value, Box::new(Expr::This));
2997                    **value = Expr::Cast { ty: t, e: v };
2998                }
2999            }
3000        }
3001        Stmt::LocalDef { var, init: Some(value), .. } => {
3002            if let Some(t) = specific_ref(&vt.var(*var).ty) {
3003                let tt = vt.var(*var).ty.erased();
3004                if castable(value) || needs_downcast(value, &tt) {
3005                    let v = std::mem::replace(value, Expr::This);
3006                    *value = Expr::Cast {
3007                        ty: t,
3008                        e: Box::new(v),
3009                    };
3010                }
3011            }
3012        }
3013        Stmt::Return(Some(value)) => {
3014            if let Some(t) = specific_ref(&TypeRef::J(ret.clone())) {
3015                if castable(value) || needs_downcast(value, ret) {
3016                    let v = std::mem::replace(value, Expr::This);
3017                    *value = Expr::Cast { ty: t, e: Box::new(v) };
3018                }
3019            }
3020        }
3021        _ => {}
3022    });
3023    // Descriptor-exact formal casts at CALL args: the dex already
3024    // resolved the target method, so casting a top-Object actual to
3025    // its declared formal cannot shift overload resolution (the r57
3026    // args lesson applies to GUESSED casts, not descriptor-exact
3027    // ones). `"source_type:".concat(obj2)`, `new my0.j(obj2, ..)` —
3028    // Object无法转换为String family.
3029    walk_stmt_exprs(body, &mut |e| {
3030        deep_rewrite(e, &mut |x| {
3031            let (args, arg_tys): (&mut Vec<Expr>, &[JavaType]) = match x {
3032                Expr::Method { desc, args, is_dynamic: false, .. } => {
3033                    (args, desc.args.as_slice())
3034                }
3035                // Ctor calls are their own variant — the dex resolved the
3036                // exact <init>, so formal casts are just as safe here
3037                // (`new r(obj)` — Object无法转换为r family).
3038                Expr::New { arg_tys, args, .. } => (args, arg_tys.as_slice()),
3039                _ => return,
3040            };
3041            for (i, a) in args.iter_mut().enumerate() {
3042                let Some(formal) = arg_tys.get(i) else {
3043                    continue;
3044                };
3045                let Some(t) = specific_ref(&TypeRef::J(formal.clone())) else {
3046                    continue;
3047                };
3048                if !(castable(a) || needs_downcast(a, formal)) {
3049                    continue;
3050                }
3051                let v = std::mem::replace(a, Expr::Const(ConstVal::Null));
3052                *a = Expr::Cast { ty: t, e: Box::new(v) };
3053            }
3054        });
3055    });
3056}
3057
3058/// Numeric narrowing/widening mismatches at Assign / LocalDef / Return
3059/// positions get an explicit cast: dex int-to-byte / long-to-int flows
3060/// are implicit in the register machine, Java rejects them ("从long
3061/// 转换到int可能会有损失" — okio Buffer ×321 weibo lines). Casts at
3062/// these three positions are descriptor-faithful and cannot shift
3063/// overload resolution (the r57 args-position lesson). Boolean/numeric
3064/// mixes are NOT castable in Java and stay with the generation-split
3065/// machinery.
3066pub fn fix_primitive_assign_casts(vt: &VarTable, body: &mut Stmt, ret_ty: &JavaType) {
3067    fn numlike(t: &JavaType) -> bool {
3068        matches!(
3069            t,
3070            JavaType::Byte
3071                | JavaType::Short
3072                | JavaType::Int
3073                | JavaType::Char
3074                | JavaType::Long
3075                | JavaType::Float
3076                | JavaType::Double
3077        )
3078    }
3079    fn val_ty(e: &Expr, vt: &VarTable) -> JavaType {
3080        match e {
3081            Expr::Local { var, .. } if (*var as usize) < vt.vars.len() => {
3082                vt.var(*var).ty.erased()
3083            }
3084            other => other.type_ref().erased(),
3085        }
3086    }
3087    fn wrap(ty: &JavaType, value: &mut Expr) {
3088        if matches!(value, Expr::Cast { .. }) {
3089            return;
3090        }
3091        if val_ty_static(value) == *ty {
3092            return;
3093        }
3094        let taken = std::mem::replace(value, Expr::Const(ConstVal::Null));
3095        *value = Expr::Cast {
3096            ty: TypeRef::J(ty.clone()),
3097            e: Box::new(taken),
3098        };
3099    }
3100    fn val_ty_static(e: &Expr) -> JavaType {
3101        e.type_ref().erased()
3102    }
3103    /// Target-typed coercion for one assignment value. Numeric mixes
3104    /// cast; boolean mixes CONVERT (Java has no cast between boolean
3105    /// and any numeric): `d = b` → `(double) (b ? 1 : 0)`, `b = i` →
3106    /// `i != 0` — mirrors fix_primitive_arg_bridges' formal-side arms.
3107    /// The generation split cannot own these: at an if-join the
3108    /// one-sided split is undone (rename_gen_back) and the incompatible
3109    /// assign re-exposes ("boolean无法转换为double", weixin s9/t01-f).
3110    fn coerce(tt: &JavaType, value: &mut Expr, vt: &VarTable) {
3111        // Raw const bits against a floating target are the FLOAT value,
3112        // not an int to widen (`float f = <0x41000000 bits>` is 8.0f;
3113        // the (float) cast path would silently store 1.09e9f). Mirror of
3114        // fix_primitive_arg_bridges' descriptor-formal reinterpretation.
3115        match (&*value, tt) {
3116            (Expr::Const(ConstVal::Int(b)), JavaType::Float) => {
3117                *value = Expr::Const(ConstVal::Float(f32::from_bits(*b as u32)));
3118                return;
3119            }
3120            (Expr::Const(ConstVal::Long(b)), JavaType::Double) => {
3121                *value = Expr::Const(ConstVal::Double(f64::from_bits(*b as u64)));
3122                return;
3123            }
3124            (Expr::Const(ConstVal::Float(f)), JavaType::Int) => {
3125                *value = Expr::Const(ConstVal::Int(f.to_bits() as i32));
3126                return;
3127            }
3128            (Expr::Const(ConstVal::Double(d)), JavaType::Long) => {
3129                *value = Expr::Const(ConstVal::Long(d.to_bits() as i64));
3130                return;
3131            }
3132            _ => {}
3133        }
3134        let vt_val = val_ty(value, vt);
3135        let tt_bool = matches!(tt, JavaType::Boolean);
3136        let val_bool = matches!(vt_val, JavaType::Boolean);
3137        if tt_bool && !val_bool && numlike(&vt_val) {
3138            if matches!(
3139                value,
3140                Expr::Const(_)
3141                    | Expr::Bin { op: BinOp::Eq | BinOp::Ne, .. }
3142                    | Expr::Un { op: UnOp::Not, .. }
3143            ) {
3144                // Bare 0/1 constants render as false/true through the
3145                // emitter's bool coercion — wrapping them in `!= 0`
3146                // produced `0 != 0` noise the value-diamond fold then
3147                // had to re-interpret.
3148                return;
3149            }
3150            let taken = std::mem::replace(value, Expr::Const(ConstVal::Null));
3151            *value = Expr::Bin {
3152                op: BinOp::Ne,
3153                l: Box::new(taken),
3154                r: Box::new(Expr::Const(ConstVal::Int(0))),
3155                ty: Some(TypeRef::J(JavaType::Boolean)),
3156            };
3157        } else if !tt_bool && val_bool && numlike(tt) {
3158            if matches!(value, Expr::Cast { .. }) {
3159                return;
3160            }
3161            let taken = std::mem::replace(value, Expr::Const(ConstVal::Null));
3162            *value = Expr::Cast {
3163                ty: TypeRef::J(tt.clone()),
3164                e: Box::new(Expr::Cond {
3165                    c: Box::new(taken),
3166                    t: Box::new(Expr::Const(ConstVal::Int(1))),
3167                    f: Box::new(Expr::Const(ConstVal::Int(0))),
3168                }),
3169            };
3170        } else if numlike(tt) && numlike(&vt_val) {
3171            wrap(tt, value);
3172        }
3173    }
3174    walk_mut_deep(body, &mut |st| match st {
3175        Stmt::ExprStmt(Expr::Assign {
3176            target,
3177            op: AssignOp::Plain,
3178            value,
3179        }) => {
3180            let tt = match &**target {
3181                Expr::Local { var, .. } if (*var as usize) < vt.vars.len() => {
3182                    vt.var(*var).ty.erased()
3183                }
3184                Expr::Field { ty, .. } => ty.erased(),
3185                // Array store: the element type is the target type
3186                // (`sFormatStr[i] = v7` into char[] — "从int转换到char
3187                // 可能会有损失", weibo TimeUtils).
3188                Expr::ArrayIndex { array, .. } => match array.type_ref().erased() {
3189                    JavaType::Array(inner) => *inner,
3190                    _ => return,
3191                },
3192                _ => return,
3193            };
3194            coerce(&tt, value, vt);
3195        }
3196        Stmt::LocalDef {
3197            var,
3198            init: Some(value),
3199            ..
3200        } => {
3201            if (*var as usize) >= vt.vars.len() {
3202                return;
3203            }
3204            let tt = vt.var(*var).ty.erased();
3205            coerce(&tt, value, vt);
3206        }
3207        Stmt::Return(Some(e)) => {
3208            if !numlike(ret_ty) || !numlike(&val_ty(e, vt)) {
3209                return;
3210            }
3211            wrap(ret_ty, e);
3212        }
3213        _ => {}
3214    });
3215}
3216
3217/// Subclass-field downcast witness + assign-cast narrowing.
3218///
3219/// R8 pushes fields DOWN into leaf classes (weibo: coroutine `label`
3220/// lives on each `Foo$bar$1` continuation, NOT on the rendered
3221/// BaseContinuationImpl/ContinuationImpl supers) while a check-cast in
3222/// the same flow types the receiver as the SUPERCLASS. The dex field
3223/// ref names the leaf (`$1->label` — verifier-legal through the
3224/// instance-of branch type), but Java resolves `((ContinuationImpl) c)
3225/// .label` against ContinuationImpl — "找不到符号 变量 label" (weibo
3226/// ×420). When the field's dex owner is a strict subtype of the
3227/// receiver's static type and the receiver's hierarchy does NOT
3228/// declare the name, re-target the receiver cast to the owner (the
3229/// runtime object IS the owner type — the verifier proved it).
3230///
3231/// Second rule: a `(T) e` value assigned into an S-typed target where
3232/// S <: T strictly ("ContinuationImpl无法转换为$reportWhenComplete$1")
3233/// narrows the cast to S — same verifier guarantee, and an assignment
3234/// position has no overload resolution to perturb.
3235pub fn fix_field_owner_downcasts(body: &mut Stmt, vt: &VarTable, pool: &DexPool) {
3236    fn owner_internal(o: &Expr, vt: &VarTable) -> Option<std::sync::Arc<str>> {
3237        let t = match o {
3238            Expr::Local { var, .. } => vt.var(*var).ty.erased(),
3239            other => other.type_ref().erased(),
3240        };
3241        match t {
3242            JavaType::Object(n) => Some(n),
3243            _ => None,
3244        }
3245    }
3246    fn declares_up(pool: &DexPool, class: &str, name: &str) -> bool {
3247        let mut cur = Some(class.to_string());
3248        let mut hops = 0;
3249        while let Some(c) = cur {
3250            hops += 1;
3251            if hops > 64 {
3252                return false;
3253            }
3254            let Some(pc) = pool.get(&c) else { return false };
3255            if pc
3256                .instance_fields
3257                .iter()
3258                .chain(pc.static_fields.iter())
3259                .any(|f| f.name.as_str() == name)
3260            {
3261                return true;
3262            }
3263            cur = pc.super_name.clone();
3264        }
3265        false
3266    }
3267    walk_stmt_exprs(body, &mut |e| {
3268        deep_rewrite(e, &mut |x| {
3269            if let Expr::Field {
3270                owner: Some(o),
3271                cls,
3272                name,
3273                is_static: false,
3274                ..
3275            } = x
3276            {
3277                let Some(ot) = owner_internal(o, vt) else {
3278                    return;
3279                };
3280                if ot.as_ref() == cls.as_ref() || !pool.is_subtype(cls, &ot) {
3281                    return;
3282                }
3283                if declares_up(pool, &ot, name) || !declares_up(pool, cls, name) {
3284                    return;
3285                }
3286                let ty = TypeRef::J(JavaType::Object(cls.clone()));
3287                if let Expr::Cast { ty: ct, .. } = &mut **o {
3288                    *ct = ty;
3289                } else {
3290                    let taken = std::mem::replace(&mut **o, Expr::This);
3291                    **o = Expr::Cast { ty, e: Box::new(taken) };
3292                }
3293            }
3294            // Rule 2: `(T) e` into a strict-subtype target narrows to S.
3295            let (tgt_ty, value) = match x {
3296                Expr::Assign { target, op: AssignOp::Plain, value } => {
3297                    let t = match &**target {
3298                        Expr::Local { var, .. } => vt.var(*var).ty.erased(),
3299                        Expr::Field { ty, .. } => ty.erased(),
3300                        _ => return,
3301                    };
3302                    (t, value)
3303                }
3304                _ => return,
3305            };
3306            let JavaType::Object(sn) = tgt_ty else { return };
3307            if let Expr::Cast { ty, .. } = &mut **value {
3308                if let TypeRef::J(JavaType::Object(tn)) = ty {
3309                    if tn.as_ref() != sn.as_ref() && pool.is_subtype(&sn, tn) {
3310                        *ty = TypeRef::J(JavaType::Object(sn.clone()));
3311                    }
3312                }
3313            }
3314        });
3315    });
3316    // LocalDef inits: same narrowing against the declared var type.
3317    walk_mut_deep(body, &mut |st| {
3318        if let Stmt::LocalDef { var, init: Some(value), .. } = st {
3319            let tt = if (*var as usize) < vt.vars.len() {
3320                vt.vars[*var as usize].ty.erased()
3321            } else {
3322                return;
3323            };
3324            let JavaType::Object(sn) = tt else { return };
3325            if let Expr::Cast { ty, .. } = value {
3326                if let TypeRef::J(JavaType::Object(tn)) = ty {
3327                    if tn.as_ref() != sn.as_ref() && pool.is_subtype(&sn, tn) {
3328                        *ty = TypeRef::J(JavaType::Object(sn.clone()));
3329                    }
3330                }
3331            }
3332        }
3333    });
3334}
3335
3336/// `==`/`!=` between UNRELATED class types is a compile error in Java
3337/// ("不可比较的类型: f0和a") while dex if-eqObj happily compares any two
3338/// references (weixin fn1/w1 ×298 files). One side takes an `(Object)`
3339/// witness cast — legal against anything. Interfaces and subtype-related
3340/// pairs are already comparable and stay untouched; java/lang/Object and
3341/// arrays-of it likewise.
3342pub fn fix_incomparable_equality(body: &mut Stmt, vt: &VarTable, pool: &crate::DexPool) {
3343    fn side_ty(e: &Expr, vt: &VarTable) -> JavaType {
3344        match e {
3345            Expr::Local { var, .. } if (*var as usize) < vt.vars.len() => {
3346                vt.var(*var).ty.erased()
3347            }
3348            other => other.type_ref().erased(),
3349        }
3350    }
3351    let incomparable = |a: &JavaType, b: &JavaType| -> bool {
3352        use JavaType::{Array, Object};
3353        match (a, b) {
3354            (Object(x), Object(y)) => {
3355                if x == y || x.as_ref() == "java/lang/Object" || y.as_ref() == "java/lang/Object"
3356                {
3357                    return false;
3358                }
3359                let xi = pool.get(x).map(|c| c.is_interface()).unwrap_or(true);
3360                let yi = pool.get(y).map(|c| c.is_interface()).unwrap_or(true);
3361                // Unknown classes (framework, not in pool): a cast is
3362                // harmless but usually unnecessary — treat as comparable
3363                // to stay conservative.
3364                if xi || yi {
3365                    return false;
3366                }
3367                !pool.is_subtype(x, y) && !pool.is_subtype(y, x)
3368            }
3369            (Array(_), Object(y)) | (Object(y), Array(_)) => {
3370                y.as_ref() != "java/lang/Object"
3371                    && y.as_ref() != "java/lang/Cloneable"
3372                    && y.as_ref() != "java/io/Serializable"
3373                    && !pool.get(y).map(|c| c.is_interface()).unwrap_or(false)
3374            }
3375            _ => false,
3376        }
3377    };
3378    let is_ref = |t: &JavaType| matches!(t, JavaType::Object(_) | JavaType::Array(_));
3379    walk_stmt_exprs(body, &mut |e| {
3380        deep_rewrite(e, &mut |x| {
3381            if let Expr::Bin {
3382                op: BinOp::Eq | BinOp::Ne,
3383                l,
3384                r,
3385                ..
3386            } = x
3387            {
3388                let lt = side_ty(l, vt);
3389                let rt = side_ty(r, vt);
3390                // `obj != 0` → `obj != null` with FINAL types: the
3391                // lift-time null_side_rewrite ran before infer_types
3392                // (its obj table saw the pre-inference int), leaving
3393                // `sQLiteClosable != 0` — "二元运算符 '!=' 的操作数
3394                // 类型错误" ×700 weixin.
3395                if is_ref(&lt) && matches!(&**r, Expr::Const(ConstVal::Int(0))) {
3396                    **r = Expr::Const(ConstVal::Null);
3397                    return;
3398                }
3399                if is_ref(&rt) && matches!(&**l, Expr::Const(ConstVal::Int(0))) {
3400                    **l = Expr::Const(ConstVal::Null);
3401                    return;
3402                }
3403                if incomparable(&lt, &rt) {
3404                    let taken = std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
3405                    **l = Expr::Cast {
3406                        ty: TypeRef::J(JavaType::Object("java/lang/Object".into())),
3407                        e: taken,
3408                    };
3409                }
3410            }
3411        });
3412    });
3413}
3414
3415/// Primitive bridges at CALL/CTOR argument positions. Dex slots are
3416/// untyped category-1 integers: the verifier happily passes a Z-slot to
3417/// a `(B)` formal (weibo's Meituan-Robust hotpatch boilerplate boxes
3418/// boolean params via `new Byte(zreg)` — 4.2k+ `对于Byte(boolean),
3419/// 找不到合适的构造器`), and obfuscated/Kotlin code passes 0/1 slots to
3420/// `(Z)` formals (`int无法转换为boolean` at call sites). Java source
3421/// cannot express either — bridge at the IR level:
3422///   Boolean actual → numeric formal:  `(byte) (b ? 1 : 0)`
3423///   numeric actual → Boolean formal:  `x != 0`
3424/// The target method is DESCRIPTOR-bound (bytecode method_ref), so a
3425/// cast/compare to the exact formal type cannot flip overload
3426/// resolution the way r57's inferred object casts did — the rendered
3427/// call selects the same signature the dex named.
3428pub fn fix_primitive_arg_bridges(body: &mut Stmt, vt: &VarTable, pool: &crate::DexPool) {
3429    fn val_ty(e: &Expr, vt: &VarTable) -> JavaType {
3430        match e {
3431            Expr::Local { var, .. } => vt.var(*var).ty.erased(),
3432            other => other.type_ref().erased(),
3433        }
3434    }
3435    #[allow(clippy::type_complexity)]
3436    let is_bool = |t: &JavaType| matches!(t, JavaType::Boolean);
3437    let is_num = |t: &JavaType| {
3438        matches!(
3439            t,
3440            JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
3441                | JavaType::Long | JavaType::Float | JavaType::Double
3442        )
3443    };
3444    // BORROWED-arg form: returns the bridged replacement without
3445    // consuming the original (a `mem::replace` + `Option` dance dropped
3446    // the argument on the None path — every Boolean→Boolean arg became
3447    // a literal `null`: 61k 引用不明确 in one battery run).
3448    fn bridge(actual: &JavaType, formal: &JavaType, arg: &Expr) -> Option<Expr> {
3449        // A dex const carries the CALLEE's BITS: `const v, #0x41000000`
3450        // feeding a float formal IS 8.0f. Rendering the raw int literal
3451        // (a) mis-selects overloads — a competing `a(Context,int):void`
3452        // beats the int→float-widened `a(Context,float):int` in javac's
3453        // phase order ("不兼容的类型: void无法转换为int", weibo ju.a
3454        // ×1,259) — and (b) corrupts the VALUE (widening turns the bits
3455        // into 1.09e9f). The descriptor formal is dex ground truth:
3456        // reinterpret. Symmetric for the float-const-into-int-formal
3457        // direction (caller's bits, callee's reading).
3458        match (arg, formal) {
3459            (Expr::Const(ConstVal::Int(b)), JavaType::Float) => {
3460                return Some(Expr::Const(ConstVal::Float(f32::from_bits(*b as u32))));
3461            }
3462            (Expr::Const(ConstVal::Long(b)), JavaType::Double) => {
3463                return Some(Expr::Const(ConstVal::Double(f64::from_bits(*b as u64))));
3464            }
3465            (Expr::Const(ConstVal::Float(f)), JavaType::Int) => {
3466                return Some(Expr::Const(ConstVal::Int(f.to_bits() as i32)));
3467            }
3468            (Expr::Const(ConstVal::Double(d)), JavaType::Long) => {
3469                return Some(Expr::Const(ConstVal::Long(d.to_bits() as i64)));
3470            }
3471            _ => {}
3472        }
3473        if matches!(actual, JavaType::Boolean)
3474            && matches!(
3475                formal,
3476                JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
3477                    | JavaType::Long | JavaType::Float | JavaType::Double
3478            )
3479        {
3480            Some(Expr::Cast {
3481                ty: TypeRef::J(formal.clone()),
3482                e: Box::new(Expr::Cond {
3483                    c: Box::new(arg.clone()),
3484                    t: Box::new(Expr::Const(ConstVal::Int(1))),
3485                    f: Box::new(Expr::Const(ConstVal::Int(0))),
3486                }),
3487            })
3488        } else if matches!(formal, JavaType::Boolean)
3489            && matches!(
3490                actual,
3491                JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
3492                    | JavaType::Long | JavaType::Float | JavaType::Double
3493            )
3494        {
3495            Some(Expr::Bin {
3496                op: BinOp::Ne,
3497                l: Box::new(arg.clone()),
3498                r: Box::new(Expr::Const(ConstVal::Int(0))),
3499                ty: Some(TypeRef::J(JavaType::Boolean)),
3500            })
3501        } else if matches!(
3502            actual,
3503            JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
3504                | JavaType::Long | JavaType::Float | JavaType::Double
3505        ) && matches!(
3506            formal,
3507            JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
3508                | JavaType::Long | JavaType::Float | JavaType::Double
3509        ) && !implicit_widening(actual, formal)
3510        {
3511            // Numeric NARROWING into a formal (int arg → char formal):
3512            // javac rejects the lossy conversion, or — worse when the
3513            // class has no wider overload — silently resolves a
3514            // DIFFERENT overload than the dex descriptor names
3515            // (SpannableStringBuilder.append(int) doesn't exist;
3516            // `append(char)` is "从int转换到char可能会有损失", weibo ×76).
3517            // The descriptor formal is the dex ground truth; casting to
3518            // it pins the overload the bytecode actually invoked.
3519            Some(Expr::Cast {
3520                ty: TypeRef::J(formal.clone()),
3521                e: Box::new(arg.clone()),
3522            })
3523        } else {
3524            None
3525        }
3526    }
3527    /// A bare `null` argument against a SPECIFIC reference formal takes
3528    /// the descriptor cast: `this(context, null)` matching both
3529    /// Builder(Context,Notification) and Builder(Context,String) is
3530    /// "对Builder的引用不明确" (weibo NotificationCompat; protobuf
3531    /// `new FieldSet.Builder(null)` ×54). The descriptor names the
3532    /// exact invoked ctor, so the cast is dex ground truth — the
3533    /// descriptor-exact doctrine, not a guessed overload shift. Bare
3534    /// null stays for Object formals (casting those would add noise
3535    /// without resolving anything javac could not already pick).
3536    /// Framework classes javac resolves through android.jar even when
3537    /// the dex pool lacks them — the embedded API database answers
3538    /// precisely (the hand-written prefix list it replaced missed
3539    /// everything outside the guessed roots and over-trusted within
3540    /// them; removed-API families like org.apache.http are in the DB
3541    /// too, matching what a legacy-jar classpath resolves).
3542    fn fw_resolvable(n: &str) -> bool {
3543        crate::fwdb::exists(n)
3544    }
3545    fn null_arg_cast(a: &mut Expr, f: &JavaType, pool: &DexPool) -> bool {
3546        if !matches!(&*a, Expr::Const(ConstVal::Null)) {
3547            return false;
3548        }
3549        if let JavaType::Object(n) = f {
3550            // Cast only to RESOLVABLE types: a phantom formal (R8 kept
3551            // the ref, dropped the class — androidx.window.sidecar)
3552            // would turn a compiling bare `null` into a fresh
3553            // cannot-find. Pool classes render into the output;
3554            // java/javax/android/dalvik resolve through android.jar.
3555            let resolvable = pool.get(n).is_some() || fw_resolvable(n);
3556            if n.as_ref() != "java/lang/Object" && resolvable {
3557                *a = Expr::Cast {
3558                    ty: TypeRef::J(f.clone()),
3559                    e: Box::new(Expr::Const(ConstVal::Null)),
3560                };
3561                return true;
3562            }
3563        } else if let JavaType::Array(_) = f {
3564            // Descriptor-exact ARRAY cast: the dex named this exact
3565            // overload; a bare null beside sibling overloads is javac-
3566            // ambiguous (`a(String,b2)` + `a(String,String[])` both match
3567            // — weibo org/a/c/b, 对a的引用不明确 family). Same phantom
3568            // gate on the element class: casting to a phantom-element
3569            // array would trade the ambiguity for a cannot-find.
3570            let mut e: &JavaType = f;
3571            while let JavaType::Array(inner) = e {
3572                e = &**inner;
3573            }
3574            let resolvable = match e {
3575                JavaType::Object(n) => pool.get(n).is_some() || fw_resolvable(n),
3576                _ => true, // primitive-element array
3577            };
3578            if resolvable {
3579                *a = Expr::Cast {
3580                    ty: TypeRef::J(f.clone()),
3581                    e: Box::new(Expr::Const(ConstVal::Null)),
3582                };
3583                return true;
3584            }
3585        }
3586        // Bare null deliberately stays (Object formal / phantom type):
3587        // the caller's ambiguity pin may still act on it.
3588        false
3589    }
3590    /// JLS 5.1.2 widening primitive conversion (char joins the chain
3591    /// only at int; byte/short do NOT widen to char).
3592    fn implicit_widening(a: &JavaType, f: &JavaType) -> bool {
3593        use JavaType::*;
3594        match (a, f) {
3595            (Byte, Short) | (Byte, Int) | (Byte, Long) | (Byte, Float) | (Byte, Double) => true,
3596            (Short, Int) | (Short, Long) | (Short, Float) | (Short, Double) => true,
3597            (Char, Int) | (Char, Long) | (Char, Float) | (Char, Double) => true,
3598            (Int, Long) | (Int, Float) | (Int, Double) => true,
3599            (Long, Float) | (Long, Double) => true,
3600            (Float, Double) => true,
3601            (x, y) => x == y,
3602        }
3603    }
3604    // Does the owner class declare ANOTHER method with the same name
3605    // and arity but a different signature? Then javac's most-specific
3606    // overload resolution can steal the call from the dex descriptor's
3607    // target (weibo 对a的引用不明确 ×391: `c.a(null)` against
3608    // a(ExecuteResult)/a(Exception)/a(Object) — the descriptor names
3609    // a(Object), but bare null leaves the two specific overloads
3610    // ambiguously most-specific). Framework owners are not in the
3611    // pool — conservatively report no competition.
3612    let overload_competes = |cls: &str, name: &str, args: &[JavaType]| -> bool {
3613        if let Some(c) = pool.get(cls) {
3614            return c.all_methods().any(|m| {
3615                &*m.name == name
3616                    // Bridge/synthetic siblings are compiler artifacts
3617                    // resolving to the SAME target — counting them as
3618                    // competitors cast enum compareTo args to the erased
3619                    // formal the rendered specialization no longer
3620                    // accepts (Enum无法转换为LogLevel ×2,079 reqable).
3621                    && m.access & (crate::access::ACC_BRIDGE | crate::access::ACC_SYNTHETIC) == 0
3622                    && m.parsed_desc()
3623                        .is_some_and(|d| d.args.len() == args.len() && d.args != args)
3624            });
3625        }
3626        // Framework owner: NOT enumerable for pin purposes — the
3627        // full-DB enumeration experiment (step-5) measured +532 battery
3628        // with ZERO ambiguous gain: a subtype arg against a concrete
3629        // erased formal fires the cast even when the call was never
3630        // ambiguous, and bounded-generic formals (List<T> -> List,
3631        // Property<T,Float> erasures) make the cast break inference or
3632        // miss the rendered specialization. Framework subtype pins stay
3633        // on the measured allowlist below.
3634        let _ = args;
3635        false
3636    };
3637    // Ctors: formal types via the pool (unanimous across same-arity
3638    // overloads, else skip — ambiguity must not guess).
3639    let ctor_formals = |cls: &str, n: usize| -> Option<Vec<JavaType>> {
3640        let c = pool.get(cls)?;
3641        let mut out: Option<Vec<JavaType>> = None;
3642        for m in c.all_methods() {
3643            if &*m.name != "<init>" {
3644                continue;
3645            }
3646            let Some(d) = m.parsed_desc() else { continue };
3647            if d.args.len() != n {
3648                continue;
3649            }
3650            match &out {
3651                None => out = Some(d.args.clone()),
3652                Some(prev) if *prev != d.args => return None,
3653                Some(_) => {}
3654            }
3655        }
3656        out
3657    };
3658    walk_stmt_exprs(body, &mut |e| {
3659        deep_rewrite(e, &mut |x| match x {
3660            Expr::Method {
3661                cls,
3662                name,
3663                desc,
3664                args,
3665                is_static,
3666                ..
3667            } => {
3668                if args.len() != desc.args.len() {
3669                    return;
3670                }
3671                let _ = is_static;
3672                let mut competes: Option<bool> = None;
3673                for (a, f) in args.iter_mut().zip(desc.args.iter()) {
3674                    if null_arg_cast(a, f, pool) {
3675                        continue;
3676                    }
3677                    if matches!(a, Expr::Cast { .. }) {
3678                        continue;
3679                    }
3680                    // Descriptor-exact ambiguity pin: a bare null
3681                    // against an Object formal (null_arg_cast
3682                    // deliberately leaves those bare — safe only
3683                    // WITHOUT competing specific overloads) or a
3684                    // strict-subtype arg can match several declared
3685                    // overloads; casting to the descriptor formal
3686                    // leaves exactly the dex-invoked method
3687                    // applicable (JLS 15.12.2 — the descriptor is
3688                    // ground truth, so the cast pins resolution
3689                    // instead of shifting it).
3690                    let subtype_arg = match (&val_ty(a, vt), f) {
3691                        (JavaType::Object(an), JavaType::Object(fn_)) => {
3692                            // java/lang/Object is the root: ANY reference
3693                            // arg is a strict subtype of it — pool.is_subtype
3694                            // cannot see framework chains (String -> Object),
3695                            // so the Object formal gets the direct rule.
3696                            (an != fn_ && pool.is_subtype(an, fn_))
3697                                || (fn_.as_ref() == "java/lang/Object"
3698                                    && an.as_ref() != "java/lang/Object")
3699                        }
3700                        (JavaType::Array(_), JavaType::Object(fn_))
3701                            if fn_.as_ref() == "java/lang/Object" =>
3702                        {
3703                            true
3704                        }
3705                        _ => false,
3706                    };
3707                    // Bare null against java/lang/Object itself: the
3708                    // descriptor method exists (dex resolved it), so
3709                    // `(Object) null` is always legal and excludes every
3710                    // more-specific sibling overload from applicability.
3711                    // Pool owners take the competes gate (emitted
3712                    // declarations are raw, so the cast cannot disturb
3713                    // inference). FRAMEWORK owners only via allowlist of
3714                    // the classic non-generic null-ambiguous JDK methods
3715                    // (StringBuilder.append ×66 lark): a blanket cast
3716                    // breaks GENERIC framework methods whose descriptor
3717                    // formal is an ERASED type variable —
3718                    // `ofFloat((Object) null, View.ALPHA, v)` killed the
3719                    // T=View inference bare null provided (weibo +4).
3720                    let null_obj =
3721                        matches!(a, Expr::Const(ConstVal::Null)) && matches!(f, JavaType::Object(_));
3722                    let framework_owner = pool.get(cls.as_ref()).is_none();
3723                    let fw_null_ambiguous = framework_owner
3724                        && matches!(
3725                            (cls.as_ref(), name.as_ref()),
3726                            ("java/lang/StringBuilder", "append")
3727                                | ("java/lang/StringBuffer", "append")
3728                                | ("java/io/PrintStream", "println")
3729                                | ("java/io/PrintStream", "print")
3730                                | ("java/io/PrintWriter", "println")
3731                                | ("java/io/PrintWriter", "print")
3732                                | ("java/lang/String", "valueOf")
3733                        );
3734                    // Framework owners with famously overload-heavy
3735                    // methods take the subtype pin too: the arg's
3736                    // rendered type implements SEVERAL formal types
3737                    // (weibo putExtra ×41: floatMsgData is both
3738                    // Parcelable and Serializable; schedule/submit:
3739                    // Runnable-and-Callable workers) and the competes
3740                    // gate cannot enumerate framework overloads.
3741                    // Formal == java/lang/Object stays excluded — that
3742                    // is an erased type variable and the cast would
3743                    // kill generic inference (ofFloat lesson).
3744                    let fw_subtype_pinnable = framework_owner
3745                        && subtype_arg
3746                        && !matches!(f, JavaType::Object(fn_) if fn_.as_ref() == "java/lang/Object")
3747                        && matches!(
3748                            (cls.as_ref(), name.as_ref()),
3749                            ("android/content/Intent", "putExtra")
3750                                | ("java/util/concurrent/ScheduledExecutorService", "schedule")
3751                                | ("java/util/concurrent/ExecutorService", "submit")
3752                                | ("java/util/concurrent/ExecutorService", "invokeAll")
3753                        );
3754                    if fw_subtype_pinnable
3755                        || (subtype_arg
3756                            && *competes.get_or_insert_with(|| {
3757                                overload_competes(cls, name, &desc.args)
3758                            }))
3759                        || (null_obj
3760                            && (fw_null_ambiguous
3761                                || *competes.get_or_insert_with(|| {
3762                                    overload_competes(cls, name, &desc.args)
3763                                })))
3764                    {
3765                        let old =
3766                            std::mem::replace(a, Expr::Const(ConstVal::Null));
3767                        *a = Expr::Cast {
3768                            ty: TypeRef::J(f.clone()),
3769                            e: Box::new(old),
3770                        };
3771                        continue;
3772                    }
3773                    if !is_bool(&val_ty(a, vt)) && !is_num(&val_ty(a, vt)) {
3774                        continue;
3775                    }
3776                    if !is_bool(f) && !is_num(f) {
3777                        continue;
3778                    }
3779                    if let Some(b) = bridge(&val_ty(a, vt), f, a) {
3780                        *a = b;
3781                    }
3782                }
3783            }
3784            Expr::New { cls, args, arg_tys, .. } => {
3785                let formals_owned = if arg_tys.len() == args.len() && !arg_tys.is_empty() {
3786                    Some(arg_tys.clone())
3787                } else {
3788                    ctor_formals(cls, args.len())
3789                };
3790                if let Some(formals) = formals_owned {
3791                    for (a, f) in args.iter_mut().zip(formals.iter()) {
3792                        if null_arg_cast(a, f, pool) {
3793                            continue;
3794                        }
3795                        if matches!(a, Expr::Cast { .. }) {
3796                            continue;
3797                        }
3798                        if !is_bool(f) && !is_num(f) {
3799                            continue;
3800                        }
3801                        if let Some(b) = bridge(&val_ty(a, vt), f, a) {
3802                            *a = b;
3803                        }
3804                    }
3805                }
3806            }
3807            _ => {}
3808        });
3809    });
3810}
3811
3812/// `b ^ 1` / `1 ^ b` with a BOOLEAN operand is the bytecode's boolean
3813/// negation (`!b`) — weibo kotlin-stdlib `return v2 ^ 1;` ("二元运算符
3814/// '^' 的操作数类型错误"), and `x ^ true` shapes. Java only types `^`
3815/// for same-category operands.
3816/// Rename locals whose name equals a ROOT PACKAGE first segment: a
3817/// package-qualified render (`v2.n.a` — the FQN fallback a twin-shadowed
3818/// same-package ref must take) binds its first identifier lexically, and
3819/// a local `v2` in scope captures it (weixin v2/i: 找不到符号 变量 n on a
3820/// legit `v2.n.a(this, 0)`). Method-scope renames carry no registry
3821/// surface — every use renders through this same vt — unlike class/field
3822/// renames, whose reference coverage blew up five times before.
3823pub fn deshadow_locals(vt: &mut VarTable, pool: &crate::DexPool, class_name: &str, body: &Stmt) {
3824    let segs = pool.root_pkg_segs();
3825    // Import-layer simples: the file renders `import p.h;` + bare `h.e`
3826    // for obscured refs — a local `h` captures that simple name (locals
3827    // beat imports in expression position; 无法取消引用int family).
3828    let obscured = crate::classdec::obscured_simples_snapshot();
3829    // Same-package static-ref simples: a static member ref renders its
3830    // class qualifier as the SIMPLE name (same package — no import/FQN),
3831    // and a local with that name captures it (`a2.changeQuickRedirect`
3832    // bound to a String local `a2` — "找不到符号 变量 changeQuickRedirect
3833    // 位置: 类型为String的变量 a2", weibo robust-field refs ×317). Only
3834    // STATIC refs qualify (an instance ref's qualifier IS the local).
3835    let self_pkg = class_name.rfind('/').map(|i| &class_name[..i]).unwrap_or("");
3836    let mut same_pkg_simples: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
3837    visit_all_exprs(body, &mut |x| {
3838        let cls = match x {
3839            Expr::Field { cls, is_static: true, .. } => Some(cls.as_ref()),
3840            Expr::Method { cls, is_static: true, .. } => Some(cls.as_ref()),
3841            _ => None,
3842        };
3843        if let Some(c) = cls {
3844            let pkg = c.rfind('/').map(|i| &c[..i]).unwrap_or("");
3845            if pkg == self_pkg {
3846                let simple = &c[c.rfind('/').map(|i| i + 1).unwrap_or(0)..];
3847                let head = simple.split('$').next().unwrap_or(simple);
3848                if !head.is_empty() {
3849                    same_pkg_simples.insert(head.to_string());
3850                }
3851            }
3852        }
3853    });
3854    let hit = |name: &str| {
3855        segs.contains(name) || obscured.contains(name) || same_pkg_simples.contains(name)
3856    };
3857    if (segs.is_empty() && obscured.is_empty() && same_pkg_simples.is_empty())
3858        || !vt.vars.iter().any(|v| hit(&v.name))
3859    {
3860        return;
3861    }
3862    let mut taken: jdc_core::FxHashSet<String> =
3863        vt.vars.iter().map(|v| v.name.clone()).collect();
3864    for v in vt.vars.iter_mut() {
3865        if !hit(&v.name) {
3866            continue;
3867        }
3868        let mut k = 0u32;
3869        let cand = loop {
3870            k += 1;
3871            let c = if k == 1 {
3872                format!("{}x", v.name)
3873            } else {
3874                format!("{}x{}", v.name, k)
3875            };
3876            if !taken.contains(&c) {
3877                break c;
3878            }
3879        };
3880        taken.insert(cand.clone());
3881        v.name = cand;
3882    }
3883}
3884
3885/// Receiver rescue: a Field/Method receiver whose final vt type is
3886/// PRIMITIVE can never hold the member (structurer tail-copy regions
3887/// can map the receiver register onto a boolean/int generation —
3888/// wcdb HandleOperation's `v10_g2.finalizeStatement()` where the
3889/// statement var is right there; weixin deref-int/bool families,
3890/// ~2.5k lines). The member descriptor's owner class is authoritative:
3891/// when EXACTLY ONE var in the method has that type (or a subtype),
3892/// swap the receiver to it. The pre-rescue output is a guaranteed
3893/// compile error AND runtime NPE, so the unambiguous swap cannot make
3894/// it worse; ambiguous sites stay untouched.
3895/// Anonymous/inner-class outer reads: the dex reads `this$0` off the
3896/// instance register, which copy materialization can route through a
3897/// local (`v41 = this; ... v41.this$0` — weibo HorseRaceDetector$1,
3898/// 2,326-line 找不到符号 变量 this$0 family). When the field's
3899/// declaring class IS the class being decompiled, the only instance in
3900/// scope is `this` — strip the owner so it renders the bare declared
3901/// field.
3902/// Fold single-assigned `this`-copy locals in ctors back to `this`.
3903/// R8 materializes the receiver with `move-object/from16 v1, p0` and
3904/// runs every iput through the copy; the copy renders as a local and a
3905/// final-field store through it is "无法为 final 变量 g 分配值" (Java
3906/// allows blank-final writes only via `this`/simple name — hf/n2 ×29,
3907/// weixin final-var residual). Transitively: `b = a; c = b` chains
3908/// collapse. Single-assignment gate: a var ever written from another
3909/// value is not an alias.
3910pub fn fix_ctor_this_aliases(body: &mut Stmt, vt: &VarTable) {
3911    let Some(this_var) = vt
3912        .vars
3913        .iter()
3914        .find(|v| v.is_param && v.name == "this")
3915        .map(|v| v.id)
3916    else {
3917        return;
3918    };
3919    let mut analysis = VarAnalysis {
3920        assigns: Vec::new(),
3921        reads: Vec::new(),
3922        values: Vec::new(),
3923    };
3924    analyze_vars(body, &mut analysis);
3925    let n = vt.vars.len().max(analysis.assigns.len());
3926    let mut alias = vec![false; n];
3927    for _ in 0..4 {
3928        let mut changed = false;
3929        for v in 0..n as u32 {
3930            if alias[v as usize] {
3931                continue;
3932            }
3933            if analysis.assigns.get(v as usize).copied().unwrap_or(0) != 1 {
3934                continue;
3935            }
3936            let Some(Expr::Local { var: src, .. }) = analysis.values.get(v as usize).and_then(|o| o.as_ref()) else {
3937                continue;
3938            };
3939            if *src == this_var
3940                || (*src < n as u32 && alias[*src as usize])
3941            {
3942                alias[v as usize] = true;
3943                changed = true;
3944            }
3945        }
3946        if !changed {
3947            break;
3948        }
3949    }
3950    if !alias.iter().any(|&b| b) {
3951        return;
3952    }
3953    walk_stmt_exprs(body, &mut |e| {
3954        deep_rewrite_reads(e, &mut |x| {
3955            if let Expr::Local { var, .. } = x {
3956                if (*var as usize) < n && alias[*var as usize] {
3957                    *x = Expr::This;
3958                }
3959            }
3960        });
3961    });
3962}
3963
3964pub fn fix_this0_owners(body: &mut Stmt, class_name: &str) {
3965    walk_stmt_exprs(body, &mut |e| {
3966        deep_rewrite(e, &mut |x| {
3967            if let Expr::Field { owner: Some(_), cls, name, .. } = x {
3968                if name.as_ref() == "this$0" && cls.as_ref() == class_name {
3969                    if let Expr::Field { owner, .. } = x {
3970                        *owner = None;
3971                    }
3972                }
3973            }
3974        });
3975    });
3976}
3977
3978pub fn rescue_primitive_receivers(body: &mut Stmt, vt: &VarTable, pool: &crate::DexPool) {
3979    fn prim(t: &JavaType) -> bool {
3980        match t {
3981            JavaType::Int
3982            | JavaType::Long
3983            | JavaType::Short
3984            | JavaType::Byte
3985            | JavaType::Char
3986            | JavaType::Float
3987            | JavaType::Double
3988            | JavaType::Boolean => true,
3989            // Boxed JDK finals carry no app members either (`bool19.d`
3990            // where bool19 came out java/lang/Boolean — the coroutine
3991            // Result-register conflation).
3992            JavaType::Object(n) => matches!(
3993                n.as_ref(),
3994                "java/lang/Boolean"
3995                    | "java/lang/String"
3996                    | "java/lang/Integer"
3997                    | "java/lang/Long"
3998                    | "java/lang/Short"
3999                    | "java/lang/Byte"
4000                    | "java/lang/Character"
4001                    | "java/lang/Float"
4002                    | "java/lang/Double"
4003            ),
4004            _ => false,
4005        }
4006    }
4007    walk_stmt_exprs(body, &mut |e| {
4008        deep_rewrite(e, &mut |x| {
4009            // Phase 1 (read-only): primitive-typed Local receiver?
4010            let (recv_var, recv_ty) = {
4011                let o: &Expr = match &*x {
4012                    Expr::Method { owner: Some(o), .. } => o,
4013                    Expr::Field { owner: Some(o), is_static: false, .. } => o,
4014                    _ => return,
4015                };
4016                let Expr::Local { var, ty } = o else {
4017                    return;
4018                };
4019                let vt_ty = if (*var as usize) < vt.vars.len() {
4020                    vt.var(*var).ty.erased()
4021                } else {
4022                    ty.erased()
4023                };
4024                if !prim(&vt_ty) {
4025                    return;
4026                }
4027                (*var, vt_ty)
4028            };
4029            let cls: std::sync::Arc<str> = match &*x {
4030                Expr::Method { cls, .. } | Expr::Field { cls, .. } => cls.clone(),
4031                _ => return,
4032            };
4033            // Type-consistent call: the receiver's own type IS the
4034            // member owner (or a subtype) — `str.hashCode()` on a
4035            // String param needs no rescue. Without this the boxed-JDK
4036            // prim() arm fired on every String member call and the
4037            // max-id fallback below hijacked the reads to the highest
4038            // String-typed var in the method — often a case-local
4039            // (lark LynxUIBaseInput str11: 37 reads rewritten to a
4040            // switch-case-scoped def, 找不到符号 ×37; x0/c$a18 str4).
4041            if let JavaType::Object(n) = &recv_ty {
4042                if n.as_ref() == cls.as_ref() || pool.is_subtype(n.as_ref(), cls.as_ref()) {
4043                    return;
4044                }
4045            }
4046            // Phase 2: vars of the owner type (exact or subtype).
4047            let mut cands: Vec<u32> = Vec::new();
4048            for v in &vt.vars {
4049                if let JavaType::Object(n) = v.ty.erased() {
4050                    if n.as_ref() == cls.as_ref()
4051                        || pool.is_subtype(n.as_ref(), cls.as_ref())
4052                    {
4053                        cands.push(v.id);
4054                    }
4055                }
4056            }
4057            let id = if cands.len() == 1 {
4058                cands[0]
4059            } else if cands.len() > 1 {
4060                // Multiple owner-typed vars: prefer the same-SLOT one
4061                // (register lineage), latest generation wins — the
4062                // receiver position is scope-insensitive (any method-
4063                // scope var renders, the decl hoist covers it).
4064                let slot = if (recv_var as usize) < vt.vars.len() {
4065                    Some(vt.var(recv_var).slot)
4066                } else {
4067                    None
4068                };
4069                let mut hits: Vec<u32> = match slot {
4070                    Some(sl) => cands
4071                        .iter()
4072                        .copied()
4073                        .filter(|&c| {
4074                            c != recv_var
4075                                && (c as usize) < vt.vars.len()
4076                                && vt.var(c).slot == sl
4077                        })
4078                        .collect(),
4079                    None => Vec::new(),
4080                };
4081                if hits.is_empty() {
4082                    // No same-slot twin: fall back to the latest
4083                    // owner-typed generation (max id).
4084                    hits = cands
4085                        .iter()
4086                        .copied()
4087                        .filter(|&c| c != recv_var)
4088                        .collect();
4089                }
4090                hits.sort_unstable();
4091                hits.dedup();
4092                if hits.is_empty() {
4093                    return;
4094                }
4095                *hits.last().unwrap()
4096            } else {
4097                // Lineage disambiguation by SLOT: register reuse makes
4098                // the Boolean/primitive twin and the object version of
4099                // one dex register share VarInfo.slot (ry0/h2's
4100                // `bool19.d` — the v1-typed twin rides the same
4101                // register). Multiple slot matches stay untouched
4102                // (deterministic conservatism).
4103                if cands.is_empty() {
4104                    return;
4105                }
4106                let slot = if (recv_var as usize) < vt.vars.len() {
4107                    vt.var(recv_var).slot
4108                } else {
4109                    return;
4110                };
4111                let mut hits: Vec<u32> = cands
4112                    .into_iter()
4113                    .filter(|&c| {
4114                        c != recv_var
4115                            && (c as usize) < vt.vars.len()
4116                            && vt.var(c).slot == slot
4117                    })
4118                    .collect();
4119                hits.sort_unstable();
4120                hits.dedup();
4121                if hits.len() != 1 {
4122                    return;
4123                }
4124                hits[0]
4125            };
4126            // Phase 3: swap.
4127            let o: &mut Box<Expr> = match x {
4128                Expr::Method { owner: Some(o), .. } => o,
4129                Expr::Field { owner: Some(o), is_static: false, .. } => o,
4130                _ => return,
4131            };
4132            if let Expr::Local { var, ty } = &mut **o {
4133                if *var == recv_var {
4134                    *var = id;
4135                    *ty = vt.vars[id as usize].ty.clone();
4136                }
4137            }
4138        });
4139    });
4140
4141}
4142
4143/// Scope-safe arg/return swaps for mistyped generations — runs AFTER
4144/// ensure_declared hoists every declaration to the method top, so the
4145/// top_ids visibility precondition holds by construction (the r134
4146/// falsification: swapping in a block-scoped var broke lexically).
4147/// Same-slot + type-match + uniqueness gates as the receiver rescue.
4148pub fn rescue_arg_return_swaps(
4149    body: &mut Stmt,
4150    vt: &VarTable,
4151    pool: &crate::DexPool,
4152    ret_ty: &JavaType,
4153) {
4154    // SCOPE-SAFE arg/return swap: a primitive-typed actual against a
4155    // specific-reference formal (or a reference-returning `return`) is
4156    // a mistyped generation — the r134 falsification showed the swap
4157    // target MUST be lexically visible at the use site, so candidates
4158    // are restricted to params + TOP-LEVEL declared locals (a hoisted
4159    // decl is visible to javac name resolution everywhere in the
4160    // method) AND same-slot (same dex register lineage) AND unique.
4161    let mut top_ids: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
4162    for v in &vt.vars {
4163        if v.is_param {
4164            top_ids.insert(v.id);
4165        }
4166    }
4167    if let Stmt::Block(stmts) = body {
4168        for st in stmts.iter() {
4169            if let Stmt::LocalDef { var, .. } = st {
4170                top_ids.insert(*var);
4171            }
4172        }
4173    }
4174    let ret_wants_ref = match ret_ty {
4175        JavaType::Object(n) => n.as_ref() != "java/lang/Object",
4176        JavaType::Array(_) => true,
4177        _ => false,
4178    };
4179    let try_swap = |a: &mut Expr, want: &JavaType| {
4180        let Expr::Local { var, ty } = &mut *a else {
4181            return;
4182        };
4183        if (*var as usize) >= vt.vars.len() {
4184            return;
4185        }
4186        let avt = vt.var(*var).ty.erased();
4187        let avt_ref = matches!(avt, JavaType::Object(_) | JavaType::Array(_));
4188        let want_arr = matches!(want, JavaType::Array(_));
4189        // Only swap a primitive-typed actual (an Object actual gets the
4190        // descriptor-exact cast instead — never both).
4191        if avt_ref && !(avt == JavaType::Object("java/lang/Object".into())) {
4192            return;
4193        }
4194        if avt_ref && !matches!(want, JavaType::Object(_)) {
4195            return;
4196        }
4197        let slot = vt.var(*var).slot;
4198        let mut hits: Vec<u32> = Vec::new();
4199        for v in &vt.vars {
4200            if v.slot != slot || !top_ids.contains(&v.id) || v.id == *var {
4201                continue;
4202            }
4203            let t = v.ty.erased();
4204            let ok = match (&t, want) {
4205                (JavaType::Object(n), JavaType::Object(w)) => {
4206                    n.as_ref() == w.as_ref()
4207                        || pool.is_subtype(n.as_ref(), w.as_ref())
4208                }
4209                (JavaType::Array(_), JavaType::Array(_)) => want_arr,
4210                _ => false,
4211            };
4212            if ok {
4213                hits.push(v.id);
4214            }
4215        }
4216        hits.sort_unstable();
4217        hits.dedup();
4218        if hits.is_empty() {
4219            return;
4220        }
4221        // Multiple same-slot candidates: the LATEST generation (max id —
4222        // split mints in walk order ≈ program order) is the closest
4223        // approximation of the register's value at the use site. A
4224        // wrong pick still compiles (type-matched by construction);
4225        // leaving the primitive actual never does.
4226        let id = *hits.last().unwrap();
4227        *var = id;
4228        *ty = vt.vars[id as usize].ty.clone();
4229    };
4230    walk_stmt_exprs(body, &mut |e| {
4231        deep_rewrite(e, &mut |x| {
4232            match x {
4233                Expr::Method { desc, args, is_dynamic: false, .. } => {
4234                    for (i, a) in args.iter_mut().enumerate() {
4235                        if let Some(f) = desc.args.get(i) {
4236                            if matches!(f, JavaType::Object(n) if n.as_ref() != "java/lang/Object")
4237                                || matches!(f, JavaType::Array(_))
4238                            {
4239                                try_swap(a, f);
4240                            }
4241                        }
4242                    }
4243                }
4244                Expr::New { arg_tys, args, .. } => {
4245                    for (i, a) in args.iter_mut().enumerate() {
4246                        if let Some(f) = arg_tys.get(i) {
4247                            if matches!(f, JavaType::Object(n) if n.as_ref() != "java/lang/Object")
4248                                || matches!(f, JavaType::Array(_))
4249                            {
4250                                try_swap(a, f);
4251                            }
4252                        }
4253                    }
4254                }
4255                _ => {}
4256            }
4257        });
4258    });
4259    if ret_wants_ref {
4260        walk_mut_deep(body, &mut |st| {
4261            if let Stmt::Return(Some(e)) = st {
4262                try_swap(e, ret_ty);
4263            }
4264        });
4265    }
4266}
4267
4268/// Reference-array initializers take null, not 0: dex fill-array-data
4269/// over a String[]/Object[] slot carries the zero word, and the lifted
4270/// `new String[] {0}` is int无法转换为String (qs0/b family). A Const
4271/// Int(0) element of a reference-typed array init is the null ref.
4272/// `refVar = const 0` (dex `const/4 vN, 0` into a reference register) is
4273/// the NULL sentinel, not an int. By the time this runs infer_types has
4274/// typed the var a reference (its `.close()`/`.length`/method reads
4275/// proved it), so retype the const to Null BEFORE split_generations —
4276/// otherwise the ref/primitive mismatch splits off an int generation and
4277/// orphans the reference reads (u7.l try-with-resources finally
4278/// `v16_g3 = 0; if (v16_g3 != 0) v16_g3.close();` → "无法取消引用int").
4279/// `fix_incomparable_equality` already retypes the `refVar != 0`
4280/// COMPARISON side (but runs after the split); this covers the ASSIGN
4281/// side, which is what triggers the bad split.
4282pub fn fix_ref_null_assigns(vt: &VarTable, body: &mut Stmt) {
4283    let is_ref_var = |var: u32| -> bool {
4284        (var as usize) < vt.vars.len()
4285            && matches!(
4286                vt.vars[var as usize].ty.erased(),
4287                JavaType::Object(_) | JavaType::Array(_)
4288            )
4289    };
4290    walk_mut_deep(body, &mut |st| match st {
4291        Stmt::ExprStmt(Expr::Assign {
4292            target,
4293            value,
4294            op: AssignOp::Plain,
4295            ..
4296        }) => {
4297            if let Expr::Local { var, .. } = &**target {
4298                if is_ref_var(*var) && matches!(&**value, Expr::Const(ConstVal::Int(0))) {
4299                    **value = Expr::Const(ConstVal::Null);
4300                }
4301            }
4302        }
4303        Stmt::LocalDef {
4304            var,
4305            init: Some(e),
4306            ..
4307        } => {
4308            if is_ref_var(*var) && matches!(e, Expr::Const(ConstVal::Int(0))) {
4309                *e = Expr::Const(ConstVal::Null);
4310            }
4311        }
4312        _ => {}
4313    });
4314}
4315
4316pub fn fix_ref_array_null_consts(body: &mut Stmt) {
4317    walk_stmt_exprs(body, &mut |e| {
4318        deep_rewrite(e, &mut |x| {
4319            if let Expr::NewArray { elem, init: Some(list), .. } = x {
4320                if matches!(elem.erased(), JavaType::Object(_) | JavaType::Array(_)) {
4321                    for slot in list.iter_mut() {
4322                        if matches!(&*slot, Expr::Const(ConstVal::Int(0))) {
4323                            *slot = Expr::Const(ConstVal::Null);
4324                        }
4325                    }
4326                }
4327            }
4328        });
4329    });
4330}
4331
4332// ---------------------------------------------------------------------------
4333// Null-sentinel repair: const-0 registers in reference-typed slots
4334// ---------------------------------------------------------------------------
4335
4336/// A dex `const/4 vN, 0` register is ambiguous between int 0 and the
4337/// null reference; the value view types the bare constant as Int.
4338/// Kotlin coroutine state machines lean on this heavily — captured
4339/// object slots are zero-initialized ahead of the label switch, then
4340/// stored into `Object` fields and passed as `Continuation` ctor args
4341/// (weixin y01/s9 q8: `new e6(s94x, v287)` "int无法转换为Continuation";
4342/// uf5/l0 `throw 0;`; wcdb RepairKit `setOnCancelListener(0)`). The
4343/// dex verifier guarantees a const-0 register reaching a reference-
4344/// typed slot holds null, so in those slots the rendering must be the
4345/// null literal.
4346///
4347/// Two rewrites, both gated on the slot's expected type being a
4348/// reference (descriptor formal types, field type, array component,
4349/// cast target, throw, method return):
4350///   1. a literal `0` in a ref slot → `null`. SKIPPED for call-arg
4351///      slots whose formal is exactly `java/lang/Object`: `f(0)` there
4352///      compiles TODAY (boxing, or an int-overload pick like
4353///      `String.valueOf(0)`), and `f(null)` could newly fail on
4354///      overload ambiguity (`valueOf(Object)` vs `valueOf(char[])`) —
4355///      a guaranteed-error slot becomes a compile, a compiling slot
4356///      must not become an error.
4357///   2. a proven null-sentinel LOCAL: an Int-typed var whose every
4358///      write is `= 0` or `= <another sentinel>` and whose every read
4359///      sits in a ref slot (or feeds another sentinel's write — a dead
4360///      chain) → `null` at each read. The same Object-formal skip
4361///      applies at arg slots. A read in ANY int/unknown context
4362///      (binop, cond test, receiver deref, selector, anon-ctor arg,
4363///      lambda capture…) disqualifies the var — register reuse across
4364///      generations keeps its int rendering.
4365///
4366/// Runs after fix_ref_null_assigns (which converts writes of REF-typed
4367/// vars) and before split_generations (sentinel writes must not mint
4368/// int generations). Writes of confirmed sentinels stay as harmless
4369/// dead int stores for drop_dead_locals.
4370pub fn fix_null_sentinels(body: &mut Stmt, vt: &VarTable, ret: &JavaType) {
4371    #[derive(Clone, Copy, PartialEq)]
4372    enum WK {
4373        Zero,
4374        Copy(u32),
4375        Other,
4376    }
4377    fn is_ref_jt(t: &JavaType) -> bool {
4378        matches!(t, JavaType::Object(_) | JavaType::Array(_))
4379    }
4380    fn cls_write(e: &Expr) -> WK {
4381        match e {
4382            Expr::Const(ConstVal::Int(0)) => WK::Zero,
4383            Expr::Local { var, .. } => WK::Copy(*var),
4384            _ => WK::Other,
4385        }
4386    }
4387    fn expr_writes(e: &Expr, writes: &mut std::collections::HashMap<u32, Vec<WK>>) {
4388        visit_exprs(e, &mut |x| match x {
4389            Expr::Assign { target, op, value } => {
4390                if let Expr::Local { var, .. } = &**target {
4391                    let k = if matches!(op, AssignOp::Plain) {
4392                        cls_write(value)
4393                    } else {
4394                        WK::Other
4395                    };
4396                    writes.entry(*var).or_default().push(k);
4397                }
4398            }
4399            Expr::PreIncDec { e: inner, .. } | Expr::PostIncDec { e: inner, .. } => {
4400                if let Expr::Local { var, .. } = &**inner {
4401                    writes.entry(*var).or_default().push(WK::Other);
4402                }
4403            }
4404            _ => {}
4405        });
4406    }
4407    fn collect_writes(
4408        s: &Stmt,
4409        writes: &mut std::collections::HashMap<u32, Vec<WK>>,
4410        has_zero: &mut bool,
4411    ) {
4412        fn note(e: &Expr, has_zero: &mut bool, writes: &mut std::collections::HashMap<u32, Vec<WK>>) {
4413            visit_exprs(e, &mut |x| {
4414                if let Expr::Const(ConstVal::Int(0)) = x {
4415                    *has_zero = true;
4416                }
4417            });
4418            expr_writes(e, writes);
4419        }
4420        match s {
4421            Stmt::Block(v) => {
4422                for x in v {
4423                    collect_writes(x, writes, has_zero);
4424                }
4425            }
4426            Stmt::LocalDef { var, init, .. } => {
4427                if let Some(e) = init {
4428                    if matches!(e, Expr::Const(ConstVal::Int(0))) {
4429                        *has_zero = true;
4430                    }
4431                    writes.entry(*var).or_default().push(cls_write(e));
4432                }
4433            }
4434            Stmt::ExprStmt(e) => note(e, has_zero, writes),
4435            Stmt::Return(e) => {
4436                if let Some(x) = e {
4437                    note(x, has_zero, writes);
4438                }
4439            }
4440            Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
4441                note(e, has_zero, writes);
4442            }
4443            Stmt::TernaryValue { e } => note(e, has_zero, writes),
4444            Stmt::If { cond, then_stmt, else_stmt } => {
4445                note(cond, has_zero, writes);
4446                collect_writes(then_stmt, writes, has_zero);
4447                if let Some(b) = else_stmt {
4448                    collect_writes(b, writes, has_zero);
4449                }
4450            }
4451            Stmt::While { cond, body } | Stmt::DoWhile { body, cond } => {
4452                note(cond, has_zero, writes);
4453                collect_writes(body, writes, has_zero);
4454            }
4455            Stmt::For { init, cond, update, body } => {
4456                for x in init {
4457                    collect_writes(x, writes, has_zero);
4458                }
4459                if let Some(c) = cond {
4460                    note(c, has_zero, writes);
4461                }
4462                for u in update {
4463                    note(u, has_zero, writes);
4464                }
4465                collect_writes(body, writes, has_zero);
4466            }
4467            Stmt::ForEach { var, iterable, body, .. } => {
4468                writes.entry(*var).or_default().push(WK::Other);
4469                note(iterable, has_zero, writes);
4470                collect_writes(body, writes, has_zero);
4471            }
4472            Stmt::Switch { selector, cases, default, .. } => {
4473                note(selector, has_zero, writes);
4474                for c in cases {
4475                    if let Some(g) = &c.guard {
4476                        note(g, has_zero, writes);
4477                    }
4478                    for x in &c.body {
4479                        collect_writes(x, writes, has_zero);
4480                    }
4481                }
4482                if let Some(d) = default {
4483                    collect_writes(d, writes, has_zero);
4484                }
4485            }
4486            Stmt::Try { body, catches, finally } => {
4487                collect_writes(body, writes, has_zero);
4488                for c in catches {
4489                    writes.entry(c.var).or_default().push(WK::Other);
4490                    collect_writes(&c.body, writes, has_zero);
4491                }
4492                if let Some(f) = finally {
4493                    collect_writes(f, writes, has_zero);
4494                }
4495            }
4496            Stmt::TryWithResources { resources, body, catches, finally, .. } => {
4497                for r in resources {
4498                    collect_writes(r, writes, has_zero);
4499                }
4500                collect_writes(body, writes, has_zero);
4501                for c in catches {
4502                    writes.entry(c.var).or_default().push(WK::Other);
4503                    collect_writes(&c.body, writes, has_zero);
4504                }
4505                if let Some(f) = finally {
4506                    collect_writes(f, writes, has_zero);
4507                }
4508            }
4509            Stmt::Assert { cond, msg } => {
4510                note(cond, has_zero, writes);
4511                if let Some(m) = msg {
4512                    note(m, has_zero, writes);
4513                }
4514            }
4515            Stmt::Synchronized { lock, body } => {
4516                note(lock, has_zero, writes);
4517                collect_writes(body, writes, has_zero);
4518            }
4519            Stmt::Labeled { body, .. } => collect_writes(body, writes, has_zero),
4520            _ => {}
4521        }
4522    }
4523
4524    let mut writes: std::collections::HashMap<u32, Vec<WK>> =
4525        std::collections::HashMap::new();
4526    let mut has_zero = false;
4527    collect_writes(body, &mut writes, &mut has_zero);
4528
4529    // Write-proven eligibility fixpoint (copy chains: `v = u`).
4530    let mut eligible: HashSet<u32> = HashSet::default();
4531    let int_typed = |v: u32| {
4532        (v as usize) < vt.vars.len() && vt.vars[v as usize].ty.erased() == JavaType::Int
4533    };
4534    loop {
4535        let mut changed = false;
4536        for (&v, ws) in writes.iter() {
4537            if eligible.contains(&v) || ws.is_empty() || !int_typed(v) {
4538                continue;
4539            }
4540            if ws.iter().all(|k| match k {
4541                WK::Zero => true,
4542                WK::Copy(u) => *u != v && eligible.contains(u),
4543                WK::Other => false,
4544            }) {
4545                eligible.insert(v);
4546                changed = true;
4547            }
4548        }
4549        if !changed {
4550            break;
4551        }
4552    }
4553    if eligible.is_empty() && !has_zero {
4554        return; // nothing this pass can do
4555    }
4556
4557    #[derive(Clone, Copy, PartialEq)]
4558    enum Mode {
4559        Scan,
4560        Rewrite,
4561    }
4562    struct St<'a> {
4563        mode: Mode,
4564        vt: &'a VarTable,
4565        eligible: &'a HashSet<u32>,
4566        ref_read: HashSet<u32>,
4567        dead: HashSet<u32>,
4568        sentinels: HashSet<u32>,
4569        n_lit: usize,
4570        n_var: usize,
4571    }
4572    // A call-arg slot whose formal is exactly java/lang/Object keeps
4573    // its current rendering (see the fn doc — boxing compiles today,
4574    // null could newly ambiguize).
4575    fn arg_ctx(t: &JavaType) -> Option<JavaType> {
4576        match t {
4577            JavaType::Object(n) if n.as_ref() == "java/lang/Object" => None,
4578            t if is_ref_jt(t) => Some(t.clone()),
4579            _ => None,
4580        }
4581    }
4582    fn walk_expr(e: &mut Expr, ctx: Option<JavaType>, st: &mut St) {
4583        walk_expr_c(e, ctx, false, st);
4584    }
4585    /// `cast_arg`: the slot is a call/ctor ARGUMENT — a rewritten null
4586    /// gets an explicit formal-type cast. Bare `f(null)` re-triggers
4587    /// overload ambiguity where `f(0)` used to pick an int overload
4588    /// (`in(null)` "引用不明确", `ContentValues.put(String,null)` vs its
4589    /// 9 overloads); `(String) null` is unambiguous and jadx-shaped.
4590    fn walk_expr_c(e: &mut Expr, ctx: Option<JavaType>, cast_arg: bool, st: &mut St) {
4591        let slot_ref = ctx.as_ref().is_some_and(is_ref_jt);
4592        fn mk_null(ctx: &Option<JavaType>, cast_arg: bool) -> Expr {
4593            match (cast_arg, ctx) {
4594                (true, Some(t)) => Expr::Cast {
4595                    ty: TypeRef::J(t.clone()),
4596                    e: Box::new(Expr::Const(ConstVal::Null)),
4597                },
4598                _ => Expr::Const(ConstVal::Null),
4599            }
4600        }
4601        match e {
4602            Expr::Const(ConstVal::Int(0)) if slot_ref => {
4603                if st.mode == Mode::Rewrite {
4604                    *e = mk_null(&ctx, cast_arg);
4605                    st.n_lit += 1;
4606                }
4607                return;
4608            }
4609            Expr::Local { var, .. } => {
4610                if st.eligible.contains(var) {
4611                    if slot_ref {
4612                        st.ref_read.insert(*var);
4613                    } else {
4614                        st.dead.insert(*var);
4615                    }
4616                }
4617                if st.mode == Mode::Rewrite && slot_ref && st.sentinels.contains(var) {
4618                    *e = mk_null(&ctx, cast_arg);
4619                    st.n_var += 1;
4620                }
4621                return;
4622            }
4623            _ => {}
4624        }
4625        match e {
4626            Expr::Method { owner, desc, args, .. } => {
4627                if let Some(o) = owner {
4628                    walk_expr(o, None, st);
4629                }
4630                for (i, a) in args.iter_mut().enumerate() {
4631                    let c = desc.args.get(i).and_then(arg_ctx);
4632                    walk_expr_c(a, c, true, st);
4633                }
4634            }
4635            Expr::Invokedynamic { desc, args, .. } => {
4636                for (i, a) in args.iter_mut().enumerate() {
4637                    let c = desc.args.get(i).and_then(arg_ctx);
4638                    walk_expr_c(a, c, true, st);
4639                }
4640            }
4641            Expr::New { args, arg_tys, .. } => {
4642                for (i, a) in args.iter_mut().enumerate() {
4643                    let c = arg_tys.get(i).and_then(arg_ctx);
4644                    walk_expr_c(a, c, true, st);
4645                }
4646            }
4647            Expr::AnonNew { args, .. } => {
4648                for a in args.iter_mut() {
4649                    walk_expr(a, None, st);
4650                }
4651            }
4652            Expr::Assign { target, value, .. } => {
4653                // Sentinel-to-sentinel copy: the whole store is dead;
4654                // its RHS is by the write gate a const-0 or a bare
4655                // local, so no nested reads can hide in it.
4656                if let Expr::Local { var: u, .. } = &**target {
4657                    if st.eligible.contains(u) {
4658                        return;
4659                    }
4660                }
4661                let vctx = match &**target {
4662                    Expr::Field { ty, .. } => {
4663                        let t = ty.erased();
4664                        is_ref_jt(&t).then_some(t)
4665                    }
4666                    Expr::ArrayIndex { array, .. } => match array.type_ref().erased() {
4667                        JavaType::Array(inner) if is_ref_jt(&inner) => Some(*inner),
4668                        _ => None,
4669                    },
4670                    _ => None,
4671                };
4672                walk_expr(target, None, st);
4673                walk_expr(value, vctx, st);
4674            }
4675            Expr::Cast { ty, e: inner } => {
4676                let t = ty.erased();
4677                let c = is_ref_jt(&t).then_some(t);
4678                walk_expr(inner, c, st);
4679            }
4680            Expr::InstanceOf { e: inner, .. } => {
4681                walk_expr(inner, Some(JavaType::Object("java/lang/Object".into())), st);
4682            }
4683            Expr::Cond { c, t, f } => {
4684                walk_expr(c, None, st);
4685                let tc = ctx.clone();
4686                walk_expr(t, tc, st);
4687                walk_expr(f, ctx, st);
4688            }
4689            Expr::NewArray { elem, dims, init, .. } => {
4690                for d in dims.iter_mut() {
4691                    walk_expr(d, None, st);
4692                }
4693                if let Some(list) = init {
4694                    let et = elem.erased();
4695                    let c = is_ref_jt(&et).then_some(et);
4696                    for slot in list.iter_mut() {
4697                        walk_expr(slot, c.clone(), st);
4698                    }
4699                }
4700            }
4701            Expr::NewMultiArray { dims, .. } => {
4702                for d in dims.iter_mut() {
4703                    walk_expr(d, None, st);
4704                }
4705            }
4706            Expr::ArrayIndex { array, index } => {
4707                walk_expr(array, None, st);
4708                walk_expr(index, None, st);
4709            }
4710            Expr::Field { owner, .. } => {
4711                if let Some(o) = owner {
4712                    walk_expr(o, None, st);
4713                }
4714            }
4715            Expr::Bin { l, r, .. } => {
4716                walk_expr(l, None, st);
4717                walk_expr(r, None, st);
4718            }
4719            Expr::Un { e: inner, .. } => walk_expr(inner, None, st),
4720            Expr::PreIncDec { e: inner, .. } | Expr::PostIncDec { e: inner, .. } => {
4721                walk_expr(inner, None, st);
4722            }
4723            Expr::StringConcat(parts) => {
4724                for p in parts.iter_mut() {
4725                    if let ConcatPart::Str(x) = p {
4726                        walk_expr(x, None, st);
4727                    }
4728                }
4729            }
4730            Expr::Lambda(l) => {
4731                for c in l.captures.iter_mut() {
4732                    walk_expr(c, None, st);
4733                }
4734            }
4735            _ => {}
4736        }
4737    }
4738    fn walk_stmt(s: &mut Stmt, st: &mut St, ret: &JavaType) {
4739        match s {
4740            Stmt::Block(v) => {
4741                for x in v.iter_mut() {
4742                    walk_stmt(x, st, ret);
4743                }
4744            }
4745            Stmt::ExprStmt(e) => walk_expr(e, None, st),
4746            Stmt::LocalDef { var, init, .. } => {
4747                if let Some(e) = init {
4748                    let ctx = if (*var as usize) < st.vt.vars.len() {
4749                        let t = st.vt.vars[*var as usize].ty.erased();
4750                        is_ref_jt(&t).then_some(t)
4751                    } else {
4752                        None
4753                    };
4754                    walk_expr(e, ctx, st);
4755                }
4756            }
4757            Stmt::Return(e) => {
4758                if let Some(x) = e {
4759                    let c = is_ref_jt(ret).then(|| ret.clone());
4760                    walk_expr(x, c, st);
4761                }
4762            }
4763            Stmt::Throw(e) => {
4764                walk_expr(e, Some(JavaType::Object("java/lang/Throwable".into())), st);
4765            }
4766            Stmt::If { cond, then_stmt, else_stmt } => {
4767                walk_expr(cond, None, st);
4768                walk_stmt(then_stmt, st, ret);
4769                if let Some(b) = else_stmt {
4770                    walk_stmt(b, st, ret);
4771                }
4772            }
4773            Stmt::While { cond, body } => {
4774                walk_expr(cond, None, st);
4775                walk_stmt(body, st, ret);
4776            }
4777            Stmt::DoWhile { body, cond } => {
4778                walk_stmt(body, st, ret);
4779                walk_expr(cond, None, st);
4780            }
4781            Stmt::For { init, cond, update, body } => {
4782                for x in init.iter_mut() {
4783                    walk_stmt(x, st, ret);
4784                }
4785                if let Some(c) = cond {
4786                    walk_expr(c, None, st);
4787                }
4788                for u in update.iter_mut() {
4789                    walk_expr(u, None, st);
4790                }
4791                walk_stmt(body, st, ret);
4792            }
4793            Stmt::ForEach { iterable, body, .. } => {
4794                walk_expr(iterable, None, st);
4795                walk_stmt(body, st, ret);
4796            }
4797            Stmt::Switch { selector, cases, default, .. } => {
4798                walk_expr(selector, None, st);
4799                for c in cases.iter_mut() {
4800                    if let Some(g) = &mut c.guard {
4801                        walk_expr(g, None, st);
4802                    }
4803                    for x in c.body.iter_mut() {
4804                        walk_stmt(x, st, ret);
4805                    }
4806                }
4807                if let Some(d) = default {
4808                    walk_stmt(d, st, ret);
4809                }
4810            }
4811            Stmt::Try { body, catches, finally } => {
4812                walk_stmt(body, st, ret);
4813                for c in catches.iter_mut() {
4814                    walk_stmt(&mut c.body, st, ret);
4815                }
4816                if let Some(f) = finally {
4817                    walk_stmt(f, st, ret);
4818                }
4819            }
4820            Stmt::TryWithResources { resources, body, catches, finally, .. } => {
4821                for r in resources.iter_mut() {
4822                    walk_stmt(r, st, ret);
4823                }
4824                walk_stmt(body, st, ret);
4825                for c in catches.iter_mut() {
4826                    walk_stmt(&mut c.body, st, ret);
4827                }
4828                if let Some(f) = finally {
4829                    walk_stmt(f, st, ret);
4830                }
4831            }
4832            Stmt::Assert { cond, msg } => {
4833                walk_expr(cond, None, st);
4834                if let Some(m) = msg {
4835                    walk_expr(m, None, st);
4836                }
4837            }
4838            Stmt::Synchronized { lock, body } => {
4839                walk_expr(lock, Some(JavaType::Object("java/lang/Object".into())), st);
4840                walk_stmt(body, st, ret);
4841            }
4842            Stmt::TernaryValue { e } => walk_expr(e, None, st),
4843            Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
4844                walk_expr(e, Some(JavaType::Object("java/lang/Object".into())), st);
4845            }
4846            Stmt::Labeled { body, .. } => walk_stmt(body, st, ret),
4847            _ => {}
4848        }
4849    }
4850
4851    let mut st = St {
4852        mode: Mode::Scan,
4853        vt,
4854        eligible: &eligible,
4855        ref_read: HashSet::default(),
4856        dead: HashSet::default(),
4857        sentinels: HashSet::default(),
4858        n_lit: 0,
4859        n_var: 0,
4860    };
4861    if !eligible.is_empty() {
4862        walk_stmt(body, &mut st, ret);
4863        st.sentinels = eligible
4864            .iter()
4865            .copied()
4866            .filter(|v| st.ref_read.contains(v) && !st.dead.contains(v))
4867            .collect();
4868    }
4869    if st.sentinels.is_empty() && !has_zero {
4870        return;
4871    }
4872    st.mode = Mode::Rewrite;
4873    walk_stmt(body, &mut st, ret);
4874    if std::env::var_os("DDC_SENT").is_some() {
4875        eprintln!(
4876            "[sent] elig={} confirmed={} lit={} var={}",
4877            eligible.len(),
4878            st.sentinels.len(),
4879            st.n_lit,
4880            st.n_var
4881        );
4882    }
4883}
4884
4885/// Int-context operand bridge: a BOOLEAN-typed side of an int-kind Bin
4886/// (bitwise, arithmetic, comparison against a numeric) becomes
4887/// `(b ? 1 : 0)` — the dex-level truth (booleans ARE 0/1 ints there;
4888/// register reuse puts a bool-typed var into an int expression:
4889/// `v5x | 4`, `b == 0` — "boolean无法转换为int" / "二元运算符操作数
4890/// 类型错误" families). Runs AFTER fix_bool_xor so boolean-SINK chains
4891/// were already converted to all-boolean form and are skipped here.
4892pub fn fix_int_operand_bridges(body: &mut Stmt, vt: &VarTable) {
4893    fn side_bool(e: &Expr, vt: &VarTable) -> bool {
4894        matches!(
4895            match e {
4896                Expr::Local { var, .. } => vt.var(*var).ty.erased(),
4897                other => other.type_ref().erased(),
4898            },
4899            JavaType::Boolean
4900        )
4901    }
4902    fn side_int(e: &Expr, vt: &VarTable) -> bool {
4903        matches!(
4904            match e {
4905                Expr::Local { var, .. } => vt.var(*var).ty.erased(),
4906                other => other.type_ref().erased(),
4907            },
4908            JavaType::Int | JavaType::Short | JavaType::Byte | JavaType::Char | JavaType::Long
4909        )
4910    }
4911    fn wrap(e: Box<Expr>) -> Box<Expr> {
4912        Box::new(Expr::Cond {
4913            c: e,
4914            t: Box::new(Expr::Const(ConstVal::Int(1))),
4915            f: Box::new(Expr::Const(ConstVal::Int(0))),
4916        })
4917    }
4918    walk_stmt_exprs(body, &mut |e| {
4919        deep_rewrite(e, &mut |x| {
4920            // Field / array-store positions: the target's type is
4921            // descriptor-backed (authoritative); a boolean value into
4922            // an int slot bridges `(b ? 1 : 0)` (with a narrowing cast
4923            // for byte/short/char slots), an int value into a boolean
4924            // slot bridges `v != 0` (weixin `this.L[v3x] = max3`,
4925            // `g2x.d = p1x` — 785 field + ~700 array lines).
4926            if let Expr::Assign { target, value, op: AssignOp::Plain } = x {
4927                let tgt_ty: Option<JavaType> = match &**target {
4928                    Expr::Field { ty, .. } => Some(ty.erased()),
4929                    Expr::ArrayIndex { array, .. } => {
4930                        match array.type_ref().erased() {
4931                            JavaType::Array(el) => Some(el.as_ref().clone()),
4932                            _ => None,
4933                        }
4934                    }
4935                    // Local targets: the split/booleanize machinery owns
4936                    // the mixed-kind register cases; the residue that
4937                    // reached render (boolean无法转换为byte/long, ~360
4938                    // lines) gets the same value-side bridge.
4939                    Expr::Local { var, .. } => {
4940                        if (*var as usize) < vt.vars.len() {
4941                            Some(vt.var(*var).ty.erased())
4942                        } else {
4943                            None
4944                        }
4945                    }
4946                    _ => None,
4947                };
4948                if let Some(t) = tgt_ty {
4949                    let tgt_int = matches!(
4950                        t,
4951                        JavaType::Int
4952                            | JavaType::Short
4953                            | JavaType::Byte
4954                            | JavaType::Char
4955                            | JavaType::Long
4956                    );
4957                    if tgt_int && side_bool(value, vt) && !side_int(value, vt) {
4958                        let taken = std::mem::replace(
4959                            value,
4960                            Box::new(Expr::Const(ConstVal::Null)),
4961                        );
4962                        let mut wrapped = wrap(taken);
4963                        if !matches!(t, JavaType::Int | JavaType::Long) {
4964                            wrapped = Box::new(Expr::Cast {
4965                                ty: TypeRef::J(t.clone()),
4966                                e: wrapped,
4967                            });
4968                        }
4969                        *value = wrapped;
4970                    } else if matches!(t, JavaType::Boolean)
4971                        && side_int(value, vt)
4972                        && !side_bool(value, vt)
4973                    {
4974                        let taken = std::mem::replace(
4975                            value,
4976                            Box::new(Expr::Const(ConstVal::Null)),
4977                        );
4978                        **value = Expr::Bin {
4979                            op: BinOp::Ne,
4980                            l: taken,
4981                            r: Box::new(Expr::Const(ConstVal::Int(0))),
4982                            ty: Some(TypeRef::J(JavaType::Boolean)),
4983                        };
4984                    }
4985                }
4986            }
4987            // Explicit casts across the bool/num line are invalid Java
4988            // in BOTH directions: `(byte) this.r` (dex int-to-byte over
4989            // a boolean-rendered field — weixin AccInfo writeByte ×207)
4990            // becomes `(byte)(r ? 1 : 0)`, and `(boolean) x` over an
4991            // int becomes `x != 0`.
4992            if let Expr::Cast { ty, e } = x {
4993                let t = ty.erased();
4994                let t_num = matches!(
4995                    t,
4996                    JavaType::Int
4997                        | JavaType::Short
4998                        | JavaType::Byte
4999                        | JavaType::Char
5000                        | JavaType::Long
5001                        | JavaType::Float
5002                        | JavaType::Double
5003                );
5004                if t_num && side_bool(e, vt) && !side_int(e, vt) {
5005                    let taken = std::mem::replace(e, Box::new(Expr::Const(ConstVal::Null)));
5006                    **e = Expr::Cond {
5007                        c: taken,
5008                        t: Box::new(Expr::Const(ConstVal::Int(1))),
5009                        f: Box::new(Expr::Const(ConstVal::Int(0))),
5010                    };
5011                } else if matches!(t, JavaType::Boolean)
5012                    && side_int(e, vt)
5013                    && !side_bool(e, vt)
5014                {
5015                    let taken = std::mem::replace(e, Box::new(Expr::Const(ConstVal::Null)));
5016                    *x = Expr::Bin {
5017                        op: BinOp::Ne,
5018                        l: taken,
5019                        r: Box::new(Expr::Const(ConstVal::Int(0))),
5020                        ty: Some(TypeRef::J(JavaType::Boolean)),
5021                    };
5022                }
5023            }
5024            // Null-vs-int comparison: dex if-eqz/if-nez is type-agnostic
5025            // and null IS zero — when the local came out int-typed (phi
5026            // confluence residue), rewrite the Null constant to 0
5027            // (`v95_g12 != null` → `v95_g12 != 0`, faithful and legal;
5028            // yq0/g1 546-line 二元运算符 family).
5029            if let Expr::Bin { op, l, r, .. } = x {
5030                if matches!(op, BinOp::Eq | BinOp::Ne) {
5031                    if matches!(&**r, Expr::Const(ConstVal::Null))
5032                        && side_int(l, vt)
5033                        && !side_bool(l, vt)
5034                    {
5035                        **r = Expr::Const(ConstVal::Int(0));
5036                    } else if matches!(&**l, Expr::Const(ConstVal::Null))
5037                        && side_int(r, vt)
5038                        && !side_bool(r, vt)
5039                    {
5040                        **l = Expr::Const(ConstVal::Int(0));
5041                    }
5042                }
5043            }
5044            // A boolean-typed array INDEX is a reused register holding
5045            // an int (`this.L[v3x]` — boolean无法转换为int at the index
5046            // position): bridge it in reads and writes alike.
5047            if let Expr::ArrayIndex { index, .. } = x {
5048                if side_bool(index, vt) && !side_int(index, vt) {
5049                    let taken =
5050                        std::mem::replace(index, Box::new(Expr::Const(ConstVal::Null)));
5051                    *index = wrap(taken);
5052                }
5053            }
5054            // A boolean-typed array DIMENSION is the same reused-register
5055            // case at the allocation site (`new Object[v200]` where v200
5056            // is boolean-typed — weixin AppBrandRuntime, bool→int family):
5057            // dex new-array reads the size register as int and booleans
5058            // ARE 0/1 ints there, so bridge `(b ? 1 : 0)`.
5059            if let Expr::NewArray { dims, .. } = x {
5060                for d in dims.iter_mut() {
5061                    if side_bool(d, vt) && !side_int(d, vt) {
5062                        let taken =
5063                            std::mem::replace(d, Expr::Const(ConstVal::Null));
5064                        *d = *wrap(Box::new(taken));
5065                    }
5066                }
5067            }
5068            if let Expr::Bin { op, l, r, .. } = x {
5069                let int_kind = matches!(
5070                    op,
5071                    BinOp::Or
5072                        | BinOp::And
5073                        | BinOp::Xor
5074                        | BinOp::Add
5075                        | BinOp::Sub
5076                        | BinOp::Mul
5077                        | BinOp::Div
5078                        | BinOp::Rem
5079                        | BinOp::Shl
5080                        | BinOp::Shr
5081                        | BinOp::Ushr
5082                        | BinOp::Eq
5083                        | BinOp::Ne
5084                        | BinOp::Lt
5085                        | BinOp::Ge
5086                        | BinOp::Gt
5087                        | BinOp::Le
5088                );
5089                if !int_kind {
5090                    return;
5091                }
5092                // Comparisons only bridge against a NUMERIC constant or
5093                // int-typed side (a bool == bool compare is valid Java).
5094                if side_bool(l, vt) && side_int(r, vt) && !side_bool(r, vt) {
5095                    let taken = std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
5096                    *l = wrap(taken);
5097                } else if side_bool(r, vt) && side_int(l, vt) && !side_bool(l, vt) {
5098                    let taken = std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
5099                    *r = wrap(taken);
5100                }
5101            }
5102        });
5103    });
5104}
5105
5106/// Statement-level idiom restoration (readability parity with
5107/// jadx/DAD, flagged by the androguard comparison): `v = v + 1` →
5108/// `v++`, `v = v op x` → `v op= x`. Local targets only — a compound
5109/// field/array target would change how often the receiver expression
5110/// evaluates. Var must be the LEFT operand (non-commutative ops;
5111/// string-concat order). The ±1 fold requires a numeric vt type
5112/// (`s++` on a String is invalid while `s += 1` is not).
5113/// Boolean VALUE-diamond fold (DAD short_circuit value forms):
5114/// ```text
5115///   v = false; if (c) { v = true; }   →  v = c
5116///   v = true;  if (c) { v = false; }  →  v = !c
5117///   v = false; if (c) { v = E; }      →  v = c && E     (E bool-valued)
5118///   v = true;  if (c) { v = E; }      →  v = !c || E
5119///   v = D; if (c) { v = E1; } else { v = E2; }  →  v = c ? E1 : E2
5120/// ```
5121/// `v` must be Boolean-typed; the arm value bool-valued; the guard may
5122/// not touch `v`; base and arm constants must not coincide (a no-op
5123/// diamond could drop guard side effects). Kotlin default-arg bridge
5124/// ctors compute defaulted booleans in exactly this shape INSIDE the
5125/// branch ahead of the delegation (lark MmCreateAudioRequest's v21x
5126/// chain) — split_branch rejects the control flow as Dirty and the
5127/// this() call stays in the branch ("对this的调用必须是构造器中的第一
5128/// 个语句", lark ×456). Folded to a linear def (interleaved with
5129/// forward_single_use for the `[u = call; v = u]` arm shape) the
5130/// merge_at ternary fold applies. Source-faithful: the original was
5131/// `x?.m() ?: false` — one expression; the inlined call stays under
5132/// the guard arm, preserving evaluation order and short-circuiting.
5133/// General value-diamond fold (fold_bool_value_diamonds for ANY type):
5134/// `if (c) { v = a; } else { v = b; }` → `v = c ? a : b;`, plus the
5135/// split-pair shape `v = b; if (c) { v = a; }` → the same ternary. The
5136/// Kotlin default-arg ctor's mask-guarded per-arg defaults arrive as
5137/// diamonds (DocFreshInfo v10x/v14x); folding gives each carrier a
5138/// SINGLE-assignment def so the chain-aware delegation inline can hoist
5139/// the this() first ("对this的调用必须是构造器中的第一个语句"). Fold
5140/// only when both sides are exactly one plain assignment to the SAME
5141/// local and the value types are ternary-compatible (equal erased or
5142/// both numeric — mixed-category ternaries box/surprise).
5143pub fn fold_value_diamonds(body: &mut Stmt, vt: &VarTable) {
5144    let Stmt::Block(stmts) = body else { return };
5145    fold_diamonds_in(stmts, vt);
5146}
5147
5148fn diamond_types_ok(a: &Expr, b: &Expr) -> bool {
5149    fn num(t: &JavaType) -> bool {
5150        matches!(
5151            t,
5152            JavaType::Byte
5153                | JavaType::Short
5154                | JavaType::Int
5155                | JavaType::Char
5156                | JavaType::Long
5157                | JavaType::Float
5158                | JavaType::Double
5159        )
5160    }
5161    // A null branch is compatible with any REFERENCE other branch: the
5162    // ternary types as lub(T, null) = T. Kotlin default-args ctors
5163    // render null defaults (`if ((mask&2)==0) v=x; else v=null;`) —
5164    // rejecting the pair left the diamond unfolded, the defs
5165    // un-inlineable, and the this(...) delegation unhoistable (weibo
5166    // ctor-not-first robust-guard/second-delegation ×818). A primitive
5167    // sibling stays rejected (`c ? 5 : null` is not Java).
5168    if let Some(other) = if matches!(a, Expr::Const(ConstVal::Null)) {
5169        Some(b)
5170    } else if matches!(b, Expr::Const(ConstVal::Null)) {
5171        Some(a)
5172    } else {
5173        None
5174    } {
5175        return !num(&other.type_ref().erased());
5176    }
5177    let ta = a.type_ref().erased();
5178    let tb = b.type_ref().erased();
5179    ta == tb || (num(&ta) && num(&tb))
5180}
5181
5182/// `(var, target clone, value)` when the stmt is exactly ONE plain
5183/// assign to a local (bare or block-wrapped). The unwrap must be
5184/// RECURSIVE: the Kotlin default-args ctor diamonds arrive as
5185/// `Block([Block([Assign])])` (structurer nesting), and the one-level
5186/// peel missed them — the unfolded `if ((mask&2)==0) v=p; else v=0;`
5187/// prelude then blocked fix_ctor_super_first from hoisting the
5188/// `this(...)` delegation (weibo/lark ctor-not-first ×841 family).
5189fn single_local_assign(st: &Stmt) -> Option<(u32, Expr, Expr)> {
5190    let mut inner = st;
5191    while let Stmt::Block(v) = inner {
5192        if v.len() != 1 {
5193            return None;
5194        }
5195        inner = &v[0];
5196    }
5197    match inner {
5198        Stmt::ExprStmt(Expr::Assign { target, value, op: AssignOp::Plain }) => {
5199            match &**target {
5200                Expr::Local { var, .. } => Some((*var, *target.clone(), *value.clone())),
5201                _ => None,
5202            }
5203        }
5204        _ => None,
5205    }
5206}
5207
5208fn mk_ternary_assign(tgt: Expr, c: Expr, a: Expr, b: Expr) -> Stmt {
5209    Stmt::ExprStmt(Expr::Assign {
5210        target: Box::new(tgt),
5211        op: AssignOp::Plain,
5212        value: Box::new(Expr::Cond {
5213            c: Box::new(c),
5214            t: Box::new(a),
5215            f: Box::new(b),
5216        }),
5217    })
5218}
5219
5220/// Two var ids that render as the SAME Java local: identical base name
5221/// (generation suffixes only appear on collisions, and same-base
5222/// generations of one dex register share the rendered name) and
5223/// identical erased type (a bool/num split pair must not merge).
5224fn same_render_var(v1: u32, v2: u32, vt: &VarTable) -> bool {
5225    let (a, b) = (vt.vars.get(v1 as usize), vt.vars.get(v2 as usize));
5226    match (a, b) {
5227        (Some(x), Some(y)) => x.name == y.name && x.ty.erased() == y.ty.erased(),
5228        _ => false,
5229    }
5230}
5231
5232fn fold_diamonds_in(stmts: &mut Vec<Stmt>, vt: &VarTable) {
5233    let mut i = 0usize;
5234    while i < stmts.len() {
5235        match &mut stmts[i] {
5236            Stmt::Block(v) => fold_diamonds_in(v, vt),
5237            Stmt::If { then_stmt, else_stmt, .. } => {
5238                if let Stmt::Block(v) = &mut **then_stmt {
5239                    fold_diamonds_in(v, vt);
5240                }
5241                if let Some(e) = else_stmt {
5242                    if let Stmt::Block(v) = &mut **e {
5243                        fold_diamonds_in(v, vt);
5244                    }
5245                }
5246            }
5247            _ => {}
5248        }
5249        // Shape 1: standalone if/else diamond.
5250        let shape1 = if let Stmt::If { cond, then_stmt, else_stmt: Some(el), .. } = &stmts[i] {
5251            match (single_local_assign(then_stmt), single_local_assign(el)) {
5252                (Some((v1, tgt, a)), Some((v2, _, b)))
5253                    if v1 == v2 && diamond_types_ok(&a, &b) =>
5254                {
5255                    Some(mk_ternary_assign(tgt, cond.clone(), a, b))
5256                }
5257                _ => None,
5258            }
5259        } else {
5260            None
5261        };
5262        if let Some(f) = shape1 {
5263            stmts[i] = f;
5264            i += 1;
5265            continue;
5266        }
5267        // Shape 2: adjacent `v = b;` + `if (c) { v = a; }` (no else).
5268        // The two assigns may target DIFFERENT SSA generations of the
5269        // same rendered local (sequential conditional overwrite — the
5270        // Kotlin mask-ctor `v11x = p3x; if ((mask&4)!=0) v11x = -1L;`
5271        // prelude, lark/weixin branched-defs ctor family): same base
5272        // name + same erased type renders as one Java local, so the
5273        // fold's else arm reads generation 1 and the assign targets
5274        // generation 2 — textually `v = c ? a : v`, exactly the
5275        // observable behavior of the unfolded pair.
5276        if i + 1 < stmts.len() {
5277            // An EMPTY else block counts as no-else: the structurer
5278            // emits Some(Block([])) where the source shape had none,
5279            // and cleanup (which would drop it) runs AFTER the fold —
5280            // the strict None match missed every real-world overwrite
5281            // (zero shape-2 folds on lark mask ctors).
5282            // Shape 2: adjacent `v = b;` + conditional overwrite with
5283            // NO other effect:
5284            //   form A: `if (c) { v = a; }` (else absent/empty)  -> v = c ? a : b
5285            //   form B: `if (c) { } else { v = a; }` (then empty) -> v = c ? b : a
5286            // Form B is the FOLD-time shape of every rendered no-else
5287            // overwrite: invert_empty_thens flips then/else AFTER this
5288            // pass, so the strict `else_stmt: None, overwrite-in-then`
5289            // match never saw a real-world instance (zero shape-2 folds
5290            // on lark mask ctors). The two assigns may target DIFFERENT
5291            // SSA generations of one rendered local (same base name +
5292            // same erased type): the ternary's keep-arm then reads
5293            // generation 1 — textually `v = c ? a : v`, exactly the
5294            // observable behavior of the unfolded pair (Kotlin mask-ctor
5295            // preludes: `v11x = p3x; if ((mask&4)!=0) v11x = -1L;`).
5296            let shape2 = {
5297                let s_i = single_local_assign(&stmts[i]);
5298                if let Stmt::If {
5299                    cond,
5300                    then_stmt,
5301                    else_stmt,
5302                    ..
5303                } = &stmts[i + 1]
5304                {
5305                    let then_a = single_local_assign(then_stmt);
5306                    let else_a = else_stmt.as_deref().and_then(single_local_assign);
5307                    let then_empty =
5308                        matches!(&**then_stmt, Stmt::Block(v) if v.is_empty());
5309                    let else_empty = else_stmt
5310                        .as_deref()
5311                        .map(|e| matches!(e, Stmt::Block(v) if v.is_empty()))
5312                        .unwrap_or(true);
5313                    let try_fold = |over: Option<(u32, Expr, Expr)>,
5314                                    keep_over: bool|
5315                     -> Option<Stmt> {
5316                        let (v2, tgt2, a) = over?;
5317                        let (v1, _t1, b) = s_i.as_ref()?;
5318                        if !(*v1 == v2 || same_render_var(*v1, v2, vt)) {
5319                            return None;
5320                        }
5321                        let keep = if *v1 == v2 {
5322                            b.clone()
5323                        } else {
5324                            Expr::Local {
5325                                var: *v1,
5326                                ty: vt.var(*v1).ty.clone(),
5327                            }
5328                        };
5329                        if !diamond_types_ok(&a, &keep) {
5330                            return None;
5331                        }
5332                        if keep_over {
5333                            Some(mk_ternary_assign(tgt2, cond.clone(), keep, a))
5334                        } else {
5335                            Some(mk_ternary_assign(tgt2, cond.clone(), a, keep))
5336                        }
5337                    };
5338                    if then_a.is_some() && else_empty {
5339                        try_fold(then_a, false)
5340                    } else if else_a.is_some() && then_empty {
5341                        try_fold(else_a, true)
5342                    } else {
5343                        None
5344                    }
5345                } else {
5346                    None
5347                }
5348            };
5349            if let Some(f) = shape2 {
5350                stmts[i] = f;
5351                stmts.remove(i + 1);
5352                continue; // re-scan the folded position
5353            }
5354        }
5355        i += 1;
5356    }
5357}
5358
5359pub fn fold_bool_value_diamonds(body: &mut Stmt, vt: &VarTable) {
5360    fn is_bool_var(v: u32, vt: &VarTable) -> bool {
5361        (v as usize) < vt.vars.len()
5362            && matches!(vt.vars[v as usize].ty.erased(), JavaType::Boolean)
5363    }
5364    fn bool_const(e: &Expr) -> Option<bool> {
5365        fn cint(e: &Expr) -> Option<i32> {
5366            match e {
5367                Expr::Const(ConstVal::Int(n)) => Some(*n),
5368                _ => None,
5369            }
5370        }
5371        match e {
5372            Expr::Const(ConstVal::Int(0)) => Some(false),
5373            Expr::Const(ConstVal::Int(1)) => Some(true),
5374            // int↔bool bridge wrappings of constants: `0 != 0` is the
5375            // fallback pipeline's rendered `false` by the time the
5376            // ctor-block fold runs.
5377            Expr::Bin { op: op @ (BinOp::Eq | BinOp::Ne), l, r, .. } => {
5378                let (a, b) = (cint(l)?, cint(r)?);
5379                let eq = a == b;
5380                Some(if matches!(op, BinOp::Eq) { eq } else { !eq })
5381            }
5382            _ => None,
5383        }
5384    }
5385    fn reads_var(e: &Expr, v: u32) -> bool {
5386        let mut hit = false;
5387        visit_exprs(e, &mut |x| {
5388            if let Expr::Local { var, .. } = x {
5389                if *var == v {
5390                    hit = true;
5391                }
5392            }
5393        });
5394        hit
5395    }
5396    /// Bool-valued expression for the composite arms (&&, ||, ?:).
5397    /// Bare 0/1 constants are handled by the caller's special cases —
5398    /// inside a composite they would render as invalid `c && 1`.
5399    fn bool_valued(e: &Expr, vt: &VarTable) -> bool {
5400        fn side(e: &Expr, vt: &VarTable) -> bool {
5401            bool_const(e).is_some() || bool_valued(e, vt)
5402        }
5403        match e {
5404            Expr::Local { var, .. } => is_bool_var(*var, vt),
5405            Expr::Method { desc, .. } => desc.ret == JavaType::Boolean,
5406            Expr::InstanceOf { .. } => true,
5407            Expr::Un { op: UnOp::Not, e: x, .. } => bool_valued(x, vt) || bool_const(x).is_some(),
5408            Expr::Bin { op, l, r, .. } => match op {
5409                BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le => true,
5410                // A logical operand may itself be a 0/1 constant
5411                // (booleanize leaves mixed forms).
5412                BinOp::LogAnd | BinOp::LogOr => {
5413                    side(l, vt) && side(r, vt)
5414                }
5415                _ => false,
5416            },
5417            Expr::Cond { t, f, .. } => side(t, vt) && side(f, vt),
5418            _ => false,
5419        }
5420    }
5421    /// A branch that is exactly one plain `v = E` with E bool-valued
5422    /// (or a 0/1 constant) and not reading v — OR the forwarding PAIR
5423    /// `[u = E; v = u]` where u is single-assigned/single-read in the
5424    /// whole method: the fold consumes the pair into `v = E` (the impure
5425    /// `u = src.m()` arm shape forward_single_use cannot inline — its
5426    /// phi-ref gate rejects values reading multi-assigned prelude vars,
5427    /// and inlining here is scope-safe because E moves exactly one
5428    /// block level out with the arm's lone def consumed).
5429    fn lone_assign(
5430        b: &Stmt,
5431        v: u32,
5432        vt: &VarTable,
5433        an: &VarAnalysis,
5434        consumed: &mut HashSet<u32>,
5435    ) -> Option<Expr> {
5436        let l = flat_list(b);
5437        if l.len() == 2 {
5438            let (u, e): (u32, &Expr) = match l[0] {
5439                Stmt::LocalDef { var, init: Some(e), .. } => (*var, e),
5440                Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => {
5441                    match &**target {
5442                        Expr::Local { var, .. } => (*var, &**value),
5443                        _ => return None,
5444                    }
5445                }
5446                _ => return None,
5447            };
5448            if u == v || consumed.contains(&u) {
5449                return None;
5450            }
5451            // Whole-method single-assign/single-read: the fold drops
5452            // u's def with the arm, so ANY other read would dangle.
5453            // (Analysis counts are per-round; folds only ever REMOVE
5454            // reads elsewhere or clone E's — whose variable reads were
5455            // already counted at their def sites — so stale counts
5456            // over-approximate and the ==1 gates stay sound.)
5457            if an.assigns.get(u as usize).copied().unwrap_or(0) != 1
5458                || an.reads.get(u as usize).copied().unwrap_or(0) != 1
5459            {
5460                return None;
5461            }
5462            let (tv, rv) = match l[1] {
5463                Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => {
5464                    match (&**target, &**value) {
5465                        (Expr::Local { var: t, .. }, Expr::Local { var: r, .. }) => (*t, *r),
5466                        _ => return None,
5467                    }
5468                }
5469                Stmt::LocalDef { var, init: Some(Expr::Local { var: r, .. }), .. } => (*var, *r),
5470                _ => return None,
5471            };
5472            if tv != v || rv != u {
5473                return None;
5474            }
5475            if reads_var(e, v) || !(bool_valued(e, vt) || bool_const(e).is_some()) {
5476                return None;
5477            }
5478            consumed.insert(u);
5479            return Some(e.clone());
5480        }
5481        if l.len() != 1 {
5482            return None;
5483        }
5484        let (target, value) = match l[0] {
5485            Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => (target, value),
5486            Stmt::LocalDef { var, init: Some(value), .. } if *var == v => {
5487                if reads_var(value, v) || !(bool_valued(value, vt) || bool_const(value).is_some()) {
5488                    return None;
5489                }
5490                return Some(value.clone());
5491            }
5492            _ => return None,
5493        };
5494        let Expr::Local { var: tv, .. } = &**target else { return None };
5495        if *tv != v || reads_var(value, v) {
5496            return None;
5497        }
5498        if !(bool_valued(value, vt) || bool_const(value).is_some()) {
5499            return None;
5500        }
5501        Some((**value).clone())
5502    }
5503    fn not(e: Expr) -> Expr {
5504        Expr::Un { op: UnOp::Not, e: Box::new(e) }
5505    }
5506    /// Peephole the int↔bool bridge artifacts the fallback pipeline
5507    /// leaves in guards: `!(x != y)` → `x == y`; `(b ? 1 : 0) == 1` →
5508    /// `b`; `!(b ? 1 : 0 == 1)` chains likewise. Purely a readability
5509    /// pass — the unsimplified forms compile.
5510    fn simplify_bool(e: Expr) -> Expr {
5511        match e {
5512            Expr::Un { op: UnOp::Not, e: inner, .. } => match *inner {
5513                // Flip first, THEN re-simplify: `!((u?1:0) != 1)` must
5514                // reach the cond01 arm as `(u?1:0) == 1` → `u` (the
5515                // flip result never re-entered simplification and the
5516                // pair arm saw a Bin where it needed the bare local).
5517                Expr::Bin { op: BinOp::Ne, l, r, ty } => {
5518                    simplify_bool(Expr::Bin { op: BinOp::Eq, l, r, ty })
5519                }
5520                Expr::Bin { op: BinOp::Eq, l, r, ty } => {
5521                    simplify_bool(Expr::Bin { op: BinOp::Ne, l, r, ty })
5522                }
5523                other => Expr::Un { op: UnOp::Not, e: Box::new(simplify_bool(other)) },
5524            },
5525            Expr::Bin { op: op @ (BinOp::Eq | BinOp::Ne), l, r, ty } => {
5526                // (c ? 1 : 0) <op> 1  →  c / !c ; <op> 0 → !c / c
5527                fn cond01(e: &Expr) -> Option<Expr> {
5528                    if let Expr::Cond { c, t, f } = e {
5529                        if matches!(&**t, Expr::Const(ConstVal::Int(1)))
5530                            && matches!(&**f, Expr::Const(ConstVal::Int(0)))
5531                        {
5532                            return Some((**c).clone());
5533                        }
5534                    }
5535                    None
5536                }
5537                let (lc, rc) = (cond01(&l), cond01(&r));
5538                match (lc, rc) {
5539                    (Some(c), None) => match (&*r, op) {
5540                        (Expr::Const(ConstVal::Int(1)), BinOp::Eq) => c,
5541                        (Expr::Const(ConstVal::Int(1)), BinOp::Ne) => not(c),
5542                        (Expr::Const(ConstVal::Int(0)), BinOp::Eq) => not(c),
5543                        (Expr::Const(ConstVal::Int(0)), BinOp::Ne) => c,
5544                        _ => Expr::Bin { op, l: Box::new(c), r, ty },
5545                    },
5546                    (None, Some(c)) => match (&*l, op) {
5547                        (Expr::Const(ConstVal::Int(1)), BinOp::Eq) => c,
5548                        (Expr::Const(ConstVal::Int(1)), BinOp::Ne) => not(c),
5549                        (Expr::Const(ConstVal::Int(0)), BinOp::Eq) => not(c),
5550                        (Expr::Const(ConstVal::Int(0)), BinOp::Ne) => c,
5551                        _ => Expr::Bin { op, l, r: Box::new(c), ty },
5552                    },
5553                    _ => Expr::Bin { op, l, r, ty },
5554                }
5555            }
5556            other => other,
5557        }
5558    }
5559    fn bin(op: BinOp, l: Expr, r: Expr) -> Expr {
5560        Expr::Bin { op, l: Box::new(l), r: Box::new(r), ty: None }
5561    }
5562
5563    for _round in 0..4 {
5564        let mut changed = false;
5565        let mut analysis = VarAnalysis {
5566            assigns: Vec::new(),
5567            reads: Vec::new(),
5568            values: Vec::new(),
5569        };
5570        analyze_vars(body, &mut analysis);
5571        let mut consumed: HashSet<u32> = HashSet::default();
5572        walk_mut_deep(body, &mut |st| {
5573            let Stmt::Block(list) = st else { return };
5574            let mut i = 0usize;
5575            while i + 1 < list.len() {
5576                let folded = try_at(list, i, vt, &is_bool_var, &bool_const, &reads_var, &lone_assign, &analysis, &mut consumed);
5577                match folded {
5578                    Some(expr) => {
5579                        let is_def = matches!(&list[i], Stmt::LocalDef { .. });
5580                        let var = match &list[i] {
5581                            Stmt::LocalDef { var, .. } => *var,
5582                            Stmt::ExprStmt(Expr::Assign { target, .. }) => {
5583                                match &**target { Expr::Local { var, .. } => *var, _ => unreachable!() }
5584                            }
5585                            _ => unreachable!(),
5586                        };
5587                        let (is_final, force_type) = match &list[i] {
5588                            Stmt::LocalDef { is_final, force_type, .. } => (*is_final, *force_type),
5589                            _ => (false, false),
5590                        };
5591                        list[i] = if is_def {
5592                            Stmt::LocalDef { var, init: Some(expr), is_final, force_type }
5593                        } else {
5594                            Stmt::ExprStmt(Expr::Assign {
5595                                target: Box::new(Expr::Local {
5596                                    var,
5597                                    ty: TypeRef::J(JavaType::Boolean),
5598                                }),
5599                                op: AssignOp::Plain,
5600                                value: Box::new(expr),
5601                            })
5602                        };
5603                        list.remove(i + 1);
5604                        changed = true;
5605                        // stay at i: a preceding base may now pair with
5606                        // nothing new, but a FOLLOWING diamond may chain.
5607                    }
5608                    None => i += 1,
5609                }
5610            }
5611        });
5612        if !changed {
5613            break;
5614        }
5615    }
5616
5617    #[allow(clippy::too_many_arguments, clippy::type_complexity)]
5618    fn try_at(
5619        list: &[Stmt],
5620        i: usize,
5621        vt: &VarTable,
5622        is_bool_var: &dyn Fn(u32, &VarTable) -> bool,
5623        bool_const: &dyn Fn(&Expr) -> Option<bool>,
5624        reads_var: &dyn Fn(&Expr, u32) -> bool,
5625        lone_assign: &dyn Fn(&Stmt, u32, &VarTable, &VarAnalysis, &mut HashSet<u32>) -> Option<Expr>,
5626        an: &VarAnalysis,
5627        consumed: &mut HashSet<u32>,
5628    ) -> Option<Expr> {
5629        // s1: `v = <0|1>` (Assign or LocalDef), v Boolean-typed.
5630        let (v, base) = match &list[i] {
5631            Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => {
5632                match &**target {
5633                    Expr::Local { var, .. } => {
5634                        // The value may be a wrapped constant (`0 != 0`
5635                        // — the fallback pipeline's rendered false).
5636                        let b = bool_const(value)?;
5637                        if !is_bool_var(*var, vt) { return None; }
5638                        (*var, b)
5639                    }
5640                    _ => return None,
5641                }
5642            }
5643            Stmt::LocalDef { var, init: Some(e), .. } => {
5644                let b = bool_const(e)?;
5645                if !is_bool_var(*var, vt) { return None; }
5646                (*var, b)
5647            }
5648            _ => return None,
5649        };
5650        let Stmt::If { cond, then_stmt, else_stmt } = &list[i + 1] else {
5651            return None;
5652        };
5653        if reads_var(cond, v) {
5654            return None;
5655        }
5656        let mut touches_v = false;
5657        visit_exprs(cond, &mut |x| {
5658            if let Expr::Assign { target, .. } = x {
5659                if let Expr::Local { var, .. } = &**target {
5660                    if *var == v { touches_v = true; }
5661                }
5662            }
5663        });
5664        if touches_v {
5665            return None;
5666        }
5667        // The structurer emits inverted diamonds (`if (c) {} else { v = E }`,
5668        // empty-then) — normalize by negating the guard.
5669        let (cond_n, then_arm, else_arm): (Expr, &Stmt, Option<&Stmt>) =
5670            if flat_list(then_stmt).is_empty() && else_stmt.is_some() {
5671                (not(cond.clone()), else_stmt.as_ref().unwrap(), None)
5672            } else {
5673                (cond.clone(), then_stmt.as_ref(), else_stmt.as_deref())
5674            };
5675        let e1 = lone_assign(then_arm, v, vt, an, consumed)?;
5676        let e2 = match else_arm {
5677            None => None,
5678            Some(b) => Some(lone_assign(b, v, vt, an, consumed)?),
5679        };
5680        let e1c = bool_const(&e1);
5681        let c = simplify_bool(cond_n);
5682        Some(match e2 {
5683            None => {
5684                if !base {
5685                    match e1c {
5686                        Some(true) => c,
5687                        Some(false) => return None, // no-op diamond: guard effects would drop
5688                        None => bin(BinOp::LogAnd, c, e1),
5689                    }
5690                } else {
5691                    match e1c {
5692                        Some(false) => not(c),
5693                        Some(true) => return None,
5694                        None => bin(BinOp::LogOr, not(c), e1),
5695                    }
5696                }
5697            }
5698            Some(e2) => {
5699                // Full diamond: both arms assign — the base is dead.
5700                if e1 == e2 {
5701                    return None; // both arms equal: folding drops guard effects
5702                }
5703                Expr::Cond { c: Box::new(c), t: Box::new(e1), f: Box::new(e2) }
5704            }
5705        })
5706    }
5707}
5708
5709pub fn idiom_compounds(body: &mut Stmt, vt: &VarTable) {
5710    walk_mut_deep(body, &mut |st| {
5711        let Stmt::ExprStmt(e) = st else { return };
5712        let Expr::Assign { target, op: AssignOp::Plain, value } = &mut *e else {
5713            return;
5714        };
5715        // Target: a local, or a bare field (static / this — no complex
5716        // receiver whose re-evaluation a compound would elide).
5717        enum Tgt {
5718            Loc(u32),
5719            Fld,
5720        }
5721        let tgt = match &**target {
5722            Expr::Local { var, .. } => Tgt::Loc(*var),
5723            Expr::Field { owner, .. }
5724                if owner.is_none()
5725                    || matches!(&**owner.as_ref().unwrap(), Expr::This) =>
5726            {
5727                Tgt::Fld
5728            }
5729            _ => return,
5730        };
5731        let Expr::Bin { op, l, r, .. } = &mut **value else {
5732            return;
5733        };
5734        // The left operand must be the SAME storage as the target.
5735        match (&tgt, &**l) {
5736            (Tgt::Loc(v), Expr::Local { var, .. }) if *var == *v => {}
5737            (Tgt::Fld, Expr::Field { cls: c2, name: n2, .. }) => {
5738                let Expr::Field { cls: c1, name: n1, .. } = &**target else {
5739                    return;
5740                };
5741                if c1 != c2 || n1 != n2 {
5742                    return;
5743                }
5744            }
5745            _ => return,
5746        }
5747        // ±1 constant on a numeric var → post-inc/dec.
5748        let one = match &**r {
5749            Expr::Const(ConstVal::Int(1)) => Some(1i64),
5750            Expr::Const(ConstVal::Int(-1)) => Some(-1),
5751            Expr::Const(ConstVal::Long(1)) => Some(1),
5752            Expr::Const(ConstVal::Long(-1)) => Some(-1),
5753            _ => None,
5754        };
5755        if *op == BinOp::Add || *op == BinOp::Sub {
5756            if let Some(mut d) = one {
5757                if *op == BinOp::Sub {
5758                    d = -d;
5759                }
5760                let tgt_ty = match &tgt {
5761                    Tgt::Loc(v) => vt.var(*v).ty.erased(),
5762                    Tgt::Fld => {
5763                        let Expr::Field { ty, .. } = &**target else {
5764                            return;
5765                        };
5766                        ty.erased()
5767                    }
5768                };
5769                let numeric = matches!(
5770                    tgt_ty,
5771                    JavaType::Int
5772                        | JavaType::Long
5773                        | JavaType::Short
5774                        | JavaType::Byte
5775                        | JavaType::Char
5776                        | JavaType::Float
5777                        | JavaType::Double
5778                );
5779                if numeric {
5780                    let wide = matches!(tgt_ty, JavaType::Long);
5781                    let inner = match &tgt {
5782                        Tgt::Loc(v) => Expr::Local {
5783                            var: *v,
5784                            ty: vt.var(*v).ty.clone(),
5785                        },
5786                        Tgt::Fld => (**target).clone(),
5787                    };
5788                    *e = Expr::PostIncDec {
5789                        e: Box::new(inner),
5790                        delta: d,
5791                        wide,
5792                    };
5793                    return;
5794                }
5795            }
5796        }
5797        let cop = match op {
5798            BinOp::Add => AssignOp::Add,
5799            BinOp::Sub => AssignOp::Sub,
5800            BinOp::Mul => AssignOp::Mul,
5801            BinOp::Div => AssignOp::Div,
5802            BinOp::Rem => AssignOp::Rem,
5803            BinOp::Shl => AssignOp::Shl,
5804            BinOp::Shr => AssignOp::Shr,
5805            BinOp::Ushr => AssignOp::Ushr,
5806            BinOp::And => AssignOp::And,
5807            BinOp::Or => AssignOp::Or,
5808            BinOp::Xor => AssignOp::Xor,
5809            _ => return,
5810        };
5811        let rhs = std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
5812        let new_target = match &tgt {
5813            Tgt::Loc(v) => Box::new(Expr::Local {
5814                var: *v,
5815                ty: vt.var(*v).ty.clone(),
5816            }),
5817            Tgt::Fld => target.clone(),
5818        };
5819        *e = Expr::Assign {
5820            target: new_target,
5821            op: cop,
5822            value: rhs,
5823        };
5824    });
5825}
5826
5827pub fn fix_bool_xor(body: &mut Stmt, vt: &VarTable, ret_bool: bool) {
5828    fn is_one(e: &Expr) -> bool {
5829        matches!(e, Expr::Const(ConstVal::Int(1)))
5830    }
5831    fn bty(e: &Expr, vt: &VarTable) -> bool {
5832        matches!(
5833            match e {
5834                Expr::Local { var, .. } => vt.var(*var).ty.erased(),
5835                other => other.type_ref().erased(),
5836            },
5837            JavaType::Boolean
5838        )
5839    }
5840    // Int-typed `x ^ 1` in a BOOLEAN sink is the compiler's `!x` over a
5841    // 0/1 slot: rewrite to `x == 0` (the booleanize return-wrap misses
5842    // Xor shapes — weibo ArraysKt `return v2 ^ 1;` against a boolean
5843    // return, "int无法转换为boolean").
5844    fn inv_of_xor(x: &Expr, vt: &VarTable) -> Option<Expr> {
5845        if let Expr::Bin { op: BinOp::Xor, l, r, .. } = x {
5846            let (int_side, one_side) = if is_one(r) {
5847                (l, r)
5848            } else if is_one(l) {
5849                (r, l)
5850            } else {
5851                return None;
5852            };
5853            let _ = one_side;
5854            if !bty(int_side, vt) {
5855                return Some(Expr::Bin {
5856                    op: BinOp::Eq,
5857                    l: int_side.clone(),
5858                    r: Box::new(Expr::Const(ConstVal::Int(0))),
5859                    ty: Some(TypeRef::J(JavaType::Boolean)),
5860                });
5861            }
5862        }
5863        None
5864    }
5865    let tgt_bool = |target: &Expr, vt: &VarTable| -> bool {
5866        match target {
5867            Expr::Local { var, .. } => matches!(vt.var(*var).ty.erased(), JavaType::Boolean),
5868            Expr::Field { ty, .. } => matches!(ty.erased(), JavaType::Boolean),
5869            _ => false,
5870        }
5871    };
5872    walk_mut_deep(body, &mut |st| match st {
5873        Stmt::Return(Some(e)) if ret_bool => {
5874            if let Some(r) = inv_of_xor(e, vt) {
5875                *e = r;
5876            }
5877        }
5878        Stmt::ExprStmt(Expr::Assign { target, value, op: AssignOp::Plain, .. }) => {
5879            if tgt_bool(target, vt) {
5880                if let Some(r) = inv_of_xor(value, vt) {
5881                    **value = r;
5882                }
5883            }
5884        }
5885        Stmt::LocalDef { var, init: Some(value), .. }
5886            if matches!(vt.var(*var).ty.erased(), JavaType::Boolean) =>
5887        {
5888            if let Some(r) = inv_of_xor(value, vt) {
5889                *value = r;
5890            }
5891        }
5892        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
5893            if let Some(r) = inv_of_xor(cond, vt) {
5894                *cond = r;
5895            }
5896        }
5897        _ => {}
5898    });
5899    fn ity(e: &Expr, vt: &VarTable) -> bool {
5900        matches!(
5901            match e {
5902                Expr::Local { var, .. } => vt.var(*var).ty.erased(),
5903                other => other.type_ref().erased(),
5904            },
5905            JavaType::Int | JavaType::Short | JavaType::Byte | JavaType::Char
5906        )
5907    }
5908    /// Mixed-kind bitwise rewrite: Kotlin's non-short-circuit
5909    /// `or`/`and` over 0/1 ints mixes generations once booleanize
5910    /// converts one side (`delete | delete2` — "boolean无法转换为int" at
5911    /// the OPERAND, weixin SQLiteDatabase ×3.3k), and `b ^ 1` is `!b`.
5912    /// The int side is a 0/1 encoding: compare it, keeping the chain
5913    /// boolean end to end. ONLY legal into a BOOLEAN sink — ungated, it
5914    /// rewrote int-sink `v6 = v5x | 4` into `v5x | 4 != 0` assigned to
5915    /// an int var (boolean无法转换为int, weixin yq5/c ×2.1k family).
5916    fn mixed_rewrite(e: &mut Expr, vt: &VarTable) {
5917        deep_rewrite(e, &mut |x| {
5918            if let Expr::Bin { op, l, r, ty } = x {
5919                match op {
5920                    BinOp::Xor => {
5921                        if is_one(r) && bty(l, vt) {
5922                            let taken =
5923                                std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
5924                            *x = Expr::Un { op: UnOp::Not, e: taken };
5925                        } else if is_one(l) && bty(r, vt) {
5926                            let taken =
5927                                std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
5928                            *x = Expr::Un { op: UnOp::Not, e: taken };
5929                        }
5930                    }
5931                    BinOp::Or | BinOp::And => {
5932                        if bty(l, vt) && ity(r, vt) {
5933                            let taken =
5934                                std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
5935                            **r = Expr::Bin {
5936                                op: BinOp::Ne,
5937                                l: taken,
5938                                r: Box::new(Expr::Const(ConstVal::Int(0))),
5939                                ty: Some(TypeRef::J(JavaType::Boolean)),
5940                            };
5941                            *ty = Some(TypeRef::J(JavaType::Boolean));
5942                        } else if ity(l, vt) && bty(r, vt) {
5943                            let taken =
5944                                std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
5945                            **l = Expr::Bin {
5946                                op: BinOp::Ne,
5947                                l: taken,
5948                                r: Box::new(Expr::Const(ConstVal::Int(0))),
5949                                ty: Some(TypeRef::J(JavaType::Boolean)),
5950                            };
5951                            *ty = Some(TypeRef::J(JavaType::Boolean));
5952                        }
5953                    }
5954                    _ => {}
5955                }
5956            }
5957        });
5958    }
5959    // Boolean sinks: bool-typed assign/def targets, boolean-method
5960    // returns, and loop/branch conditions.
5961    walk_mut_deep(body, &mut |st| match st {
5962        Stmt::Return(Some(e)) if ret_bool => mixed_rewrite(e, vt),
5963        Stmt::ExprStmt(Expr::Assign { target, value, op: AssignOp::Plain, .. }) => {
5964            if tgt_bool(target, vt) {
5965                mixed_rewrite(value, vt);
5966            }
5967        }
5968        Stmt::LocalDef { var, init: Some(e), .. }
5969            if matches!(vt.var(*var).ty.erased(), JavaType::Boolean) =>
5970        {
5971            mixed_rewrite(e, vt);
5972        }
5973        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
5974            mixed_rewrite(cond, vt);
5975        }
5976        _ => {}
5977    });
5978    // A conditional's test is a boolean sink wherever it sits (including
5979    // inside int-sink statements the walk above skips).
5980    walk_stmt_exprs(body, &mut |e| {
5981        deep_rewrite(e, &mut |x| {
5982            if let Expr::Cond { c, .. } = x {
5983                mixed_rewrite(c, vt);
5984            }
5985        });
5986    });
5987}
5988
5989/// Boolean inference: vars only ever assigned 0/1/comparisons/booleans and
5990/// read in conditions become `boolean`, with `v != 0` → `v` in conditions.
5991/// Returns the number of vars converted (0 = nothing changed — the
5992/// caller skips the post-booleanize re-split, which only has work when
5993/// a conversion exposed a mixed-kind register).
5994pub fn booleanize(vt: &mut VarTable, body: &mut Stmt, ret_bool: bool) -> usize {
5995    // Booleans propagate through local chains (`v17 = v24` where v24
5996    // itself became boolean in the first round) — iterate to a fixpoint;
5997    // the common corpus converts nothing and exits after one round.
5998    // The deref disqualifier set is structural (positions, not types) —
5999    // compute ONCE per call, not per round (12 full walks on big
6000    // methods cost weibo +60% wall).
6001    let n0 = vt.vars.len();
6002    let mut derefed = vec![false; n0];
6003    mark_derefs(body, &mut derefed);
6004    let mut total = 0usize;
6005    for _ in 0..4 {
6006        let n = booleanize_round(vt, body, ret_bool, &derefed);
6007        if n == 0 {
6008            break;
6009        }
6010        total += n;
6011    }
6012    total
6013}
6014
6015/// Mark vars used in DEREF positions (field/method receiver, array
6016/// base) — allocation-free recursion (visit_all_exprs allocates a
6017/// child Vec per statement; two callers run this per method).
6018fn mark_deref_expr(e: &Expr, derefed: &mut [bool]) {
6019    match e {
6020        Expr::Field { owner: Some(o), is_static: false, .. }
6021        | Expr::Method { owner: Some(o), .. } => {
6022            if let Expr::Local { var, .. } = &**o {
6023                if (*var as usize) < derefed.len() {
6024                    derefed[*var as usize] = true;
6025                }
6026            }
6027        }
6028        Expr::ArrayIndex { array, .. } => {
6029            if let Expr::Local { var, .. } = &**array {
6030                if (*var as usize) < derefed.len() {
6031                    derefed[*var as usize] = true;
6032                }
6033            }
6034        }
6035        _ => {}
6036    }
6037    for_each_child(e, &mut |c| mark_deref_expr(c, derefed));
6038}
6039
6040fn mark_derefs(body: &Stmt, derefed: &mut [bool]) {
6041    walk_all(body, &mut |st| {
6042        match st {
6043            Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
6044                mark_deref_expr(e, derefed);
6045            }
6046            Stmt::Return(Some(e)) => mark_deref_expr(e, derefed),
6047            Stmt::LocalDef { init: Some(e), .. } => mark_deref_expr(e, derefed),
6048            Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
6049                mark_deref_expr(cond, derefed);
6050            }
6051            Stmt::For { init, .. } => {
6052                for x in init {
6053                    mark_derefs(x, derefed);
6054                }
6055            }
6056            Stmt::Switch { selector, .. } => mark_deref_expr(selector, derefed),
6057            Stmt::ForEach { iterable, .. } => mark_deref_expr(iterable, derefed),
6058            _ => {}
6059        }
6060    });
6061}
6062
6063fn booleanize_round(
6064    vt: &mut VarTable,
6065    body: &mut Stmt,
6066    ret_bool: bool,
6067    derefed: &[bool],
6068) -> usize {
6069    let n = vt.vars.len();
6070    let mut in_cond = vec![false; n];
6071
6072    // Condition uses: `v == 0` / `v != 0` if-conditions and comparison
6073    // operands inside assigned values.
6074    walk_all(body, &mut |st| {
6075        let cond: Option<&Expr> = match st {
6076            Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
6077                Some(cond)
6078            }
6079            _ => None,
6080        };
6081        if let Some(c) = cond {
6082            if let Expr::Bin {
6083                op: BinOp::Eq | BinOp::Ne,
6084                l,
6085                r,
6086                ..
6087            } = c
6088            {
6089                for (x, other) in [(l, r), (r, l)] {
6090                    if let (Expr::Local { var, .. }, Expr::Const(ConstVal::Int(0))) =
6091                        (&**x, &**other)
6092                    {
6093                        if (*var as usize) < n {
6094                            in_cond[*var as usize] = true;
6095                        }
6096                    }
6097                }
6098            }
6099        }
6100        // A local RETURNED from a boolean method is boolean-typed even
6101        // though it never appears in a condition (`int v9 = 0/1 … return
6102        // v9;` — returning an int from `boolean check()` is a compile
6103        // error; lab package Obf.check).
6104        if ret_bool {
6105            if let Stmt::Return(Some(Expr::Local { var, .. })) = st {
6106                if (*var as usize) < n {
6107                    in_cond[*var as usize] = true;
6108                }
6109            }
6110        }
6111        // A local STORED into a boolean field is boolean-typed even though
6112        // it never appears in a condition (`b = v1` where `b` is a boolean
6113        // field, `v1` an int 0/1 local — q.java static init). The emit
6114        // layer already coerces a boolean-target assign through expr_bool,
6115        // but that renders a bare int LOCAL unchanged, so the local itself
6116        // must be booleanized (its 0/1 assigns then print false/true).
6117        if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = st {
6118            if let Expr::Field { ty, .. } = &**target {
6119                if ty.erased() == JavaType::Boolean {
6120                    if let Expr::Local { var, .. } = &**value {
6121                        if (*var as usize) < n {
6122                            in_cond[*var as usize] = true;
6123                        }
6124                    }
6125                }
6126            }
6127        }
6128        let val: Option<&Expr> = match st {
6129            Stmt::ExprStmt(Expr::Assign { value, .. }) => Some(value),
6130            // Bare expression statements (e.g. `q(v117);`) so a local
6131            // passed to a boolean parameter is caught below.
6132            Stmt::ExprStmt(e) => Some(e),
6133            Stmt::LocalDef { init: Some(e), .. } => Some(e),
6134            Stmt::Return(Some(e)) => Some(e),
6135            _ => None,
6136        };
6137        if let Some(v) = val {
6138            visit_exprs(v, &mut |x| {
6139                // A local passed as a BOOLEAN parameter is boolean-typed
6140                // (`q(v117)` where q's param is boolean, v117 an int 0/1 —
6141                // f8/b.java). The DEX passes booleans as 0/1 ints, so a
6142                // boolean param slot is authoritative for the argument's
6143                // type.
6144                if let Expr::Method { desc, args, .. } = x {
6145                    for (i, a) in args.iter().enumerate() {
6146                        if desc.args.get(i) == Some(&JavaType::Boolean) {
6147                            if let Expr::Local { var, .. } = a {
6148                                if (*var as usize) < n {
6149                                    in_cond[*var as usize] = true;
6150                                }
6151                            }
6152                        }
6153                    }
6154                }
6155                if let Expr::Bin { op, l, r, .. } = x {
6156                    if matches!(
6157                        op,
6158                        BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
6159                    ) {
6160                        for side in [l, r] {
6161                            if let Expr::Local { var, .. } = &**side {
6162                                if (*var as usize) < n {
6163                                    in_cond[*var as usize] = true;
6164                                }
6165                            }
6166                        }
6167                    }
6168                    // Kotlin/d8 merge booleans through INT bitwise ops
6169                    // (`v3 | obj instanceof g`) == 0 — the other side's
6170                    // static type being boolean makes this a boolean
6171                    // context for the local (a genuine int bitwise
6172                    // expression never has a boolean operand).
6173                    if matches!(op, BinOp::Or | BinOp::And) {
6174                        let l_bool = l.type_ref().erased() == JavaType::Boolean;
6175                        let r_bool = r.type_ref().erased() == JavaType::Boolean;
6176                        if l_bool != r_bool {
6177                            let side = if l_bool { r } else { l };
6178                            if let Expr::Local { var, .. } = &**side {
6179                                if (*var as usize) < n {
6180                                    in_cond[*var as usize] = true;
6181                                }
6182                            }
6183                        }
6184                    }
6185                }
6186            });
6187        }
6188    });
6189
6190    // A var whose every assignment value is boolean-shaped AND which is
6191    // referenced in conditions becomes boolean.
6192    // (Perf: this used to re-walk the whole statement tree ONCE PER VAR —
6193    // O(vars × stmts); on R8-merged monsters (2000+ vars, 15k statements)
6194    // that was tens of millions of visits. One pass collects the same
6195    // (any, all_bool) facts for every var.)
6196    let mut assigned_any = vec![false; n];
6197    let mut all_bool = vec![true; n];
6198    let mut edges: Vec<(usize, usize)> = Vec::new();
6199    walk_all(body, &mut |st| {
6200        let (var, value) = match st {
6201            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => match &**target {
6202                Expr::Local { var, .. } => (var, &**value),
6203                _ => return,
6204            },
6205            Stmt::LocalDef {
6206                var, init: Some(e), ..
6207            } => (var, e),
6208            _ => return,
6209        };
6210        let i = *var as usize;
6211        if i < n {
6212            assigned_any[i] = true;
6213            if !is_boolean_valued(value, vt) {
6214                all_bool[i] = false;
6215            }
6216            // Chain edges for the context closure below: `v17 = v24`
6217            // links the two locals; when the TARGET is in a boolean
6218            // context, the source inherits it (its only reader is the
6219            // boolean-shaped chain). Collected as edges because the
6220            // target's context can be discovered anywhere in the tree.
6221            if let Expr::Local { var: src, .. } = value {
6222                if (*src as usize) < n {
6223                    edges.push((i, *src as usize));
6224                }
6225            }
6226        }
6227    });
6228    // Context closure along local chains: a var in a boolean context
6229    // pushes that context through every local it is assigned from
6230    // (`return v17` — v17 = v24 — v24 = 0/1) — the statement order
6231    // cannot be relied on (the return may follow the assignment).
6232    loop {
6233        let mut changed = false;
6234        for &(tgt, src) in &edges {
6235            if in_cond[tgt] && !in_cond[src] && all_bool[src] {
6236                in_cond[src] = true;
6237                changed = true;
6238            }
6239            // FORWARD through pure copies: `v21 = v17` with v17 boolean
6240            // makes v21 boolean when ALL of v21's assignments are
6241            // bool-shaped (the loop-state save pattern — weixin u2/f's
6242            // v62 → v17 → v21 chains left the copy targets int while
6243            // the accumulator booleanized).
6244            if in_cond[src] && !in_cond[tgt] && all_bool[tgt] {
6245                in_cond[tgt] = true;
6246                changed = true;
6247            }
6248        }
6249        if !changed {
6250            break;
6251        }
6252    }
6253    let reads = count_locals_stmts(std::slice::from_ref(body));
6254    let mut boolean_vars: HashSet<u32> = HashSet::default();
6255    for i in 0..n {
6256        if vt.vars[i].is_param {
6257            continue;
6258        }
6259        if derefed[i] {
6260            continue;
6261        }
6262        if !in_cond[i] && !(reads.get(&(i as u32)).copied().unwrap_or(0) == 0) {
6263            continue;
6264        }
6265        if assigned_any[i] && all_bool[i] {
6266            boolean_vars.insert(i as u32);
6267        }
6268    }
6269    if boolean_vars.is_empty() {
6270        return 0;
6271    }
6272    for v in &boolean_vars {
6273        vt.vars[*v as usize].ty = TypeRef::J(JavaType::Boolean);
6274    }
6275    let types: Vec<&TypeRef> = vt.vars.iter().map(|v| &v.ty).collect();
6276    rewrite_exprs(body, &mut |e| {
6277        deep_rewrite(e, &mut |x| {
6278            if let Expr::Local { var, ty } = x {
6279                if let Some(want) = types.get(*var as usize) {
6280                    if ty != *want {
6281                        *ty = (*want).clone();
6282                    }
6283                }
6284            }
6285        });
6286    });
6287
6288    // Condition folding: `b != 0` → `b`, `b == 0` → `!b` (boolean vars).
6289    fold_bool_conditions(body, &boolean_vars);
6290    boolean_vars.len()
6291}
6292
6293fn is_boolean_valued(e: &Expr, vt: &VarTable) -> bool {
6294    match e {
6295        Expr::Const(ConstVal::Int(0)) | Expr::Const(ConstVal::Int(1)) => true,
6296        // Kotlin's non-short-circuit boolean `or`/`and` compiles to `|`/`&`
6297        // over boolean locals (`v15 = delete | delete2 | ..`). EITHER side
6298        // boolean suffices: `int | boolean` is illegal in source — it is
6299        // always a not-yet-booleanized side of a boolean chain (the
6300        // loop-carried accumulator `v15 = v20; v20 = v15 | delete3` can
6301        // never seed its all_bool fixpoint otherwise).
6302        Expr::Bin { op: BinOp::Or | BinOp::And, l, r, .. } => {
6303            is_boolean_valued(l, vt) || is_boolean_valued(r, vt)
6304        }
6305        // Kotlin's `!x` lowers to `x ^ 1`; at booleanize time the value
6306        // is still the Xor (fix_bool_xor runs later), so an int-typed
6307        // generation receiving it never retyped (weixin v2/i's
6308        // `int v264_g160_g7 = !v200`). BOTH sides must be bool-shaped:
6309        // `bool ^ 1` qualifies (Const 0/1 is boolean-valued above),
6310        // a genuine int `flags ^ 1` does not (flags stays Int in vt).
6311        // MUST precede the generic Bin arm (which would swallow Xor).
6312        Expr::Bin {
6313            op: BinOp::Xor,
6314            l,
6315            r,
6316            ..
6317        } => is_boolean_valued(l, vt) && is_boolean_valued(r, vt),
6318        Expr::Bin { op, .. } => matches!(
6319            op,
6320            BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
6321        ),
6322        Expr::InstanceOf { .. } => true,
6323        // The VarTable is the authority: the embedded Local ty is a
6324        // lift-time snapshot and goes STALE across booleanize rounds —
6325        // a converted copy source kept its partners int (`v150 = v131`
6326        // boolean→int, weixin ConstraintLayout ×1.4k files) because the
6327        // all_bool fixpoint consulted the stale Int.
6328        Expr::Local { var, ty } => {
6329            let t = if (*var as usize) < vt.vars.len() {
6330                vt.var(*var).ty.erased()
6331            } else {
6332                ty.erased()
6333            };
6334            t == JavaType::Boolean
6335        }
6336        Expr::Method { desc, .. } => desc.ret == JavaType::Boolean,
6337        Expr::Cond { t, f, .. } => is_boolean_valued(t, vt) && is_boolean_valued(f, vt),
6338        Expr::Un { op: UnOp::Not, .. } => true,
6339        // Field ty comes from the dex descriptor — authoritative, not a
6340        // lift snapshot (`!w1x.i` arrives here still as `w1x.i ^ 1`; the
6341        // Xor arm needs this side bool-shaped).
6342        Expr::Field { ty, .. } => ty.erased() == JavaType::Boolean,
6343        _ => false,
6344    }
6345}
6346
6347fn fold_bool_conditions(s: &mut Stmt, bv: &HashSet<u32>) {
6348    let fold = |e: &mut Expr| {
6349        deep_rewrite(e, &mut |x| {
6350            if let Expr::Bin { op, l, r, .. } = x {
6351                let (var_side, const_side) = match (&**l, &**r) {
6352                    (Expr::Local { .. }, Expr::Const(ConstVal::Int(_))) => (l, 0),
6353                    (Expr::Const(ConstVal::Int(_)), Expr::Local { .. }) => (r, 1),
6354                    _ => return,
6355                };
6356                let var = match &**var_side {
6357                    Expr::Local { var, .. } => *var,
6358                    _ => return,
6359                };
6360                if !bv.contains(&var) {
6361                    return;
6362                }
6363                let _ = const_side;
6364                match op {
6365                    BinOp::Ne => {
6366                        let inner = var_side.clone();
6367                        *x = *inner;
6368                    }
6369                    BinOp::Eq => {
6370                        let inner = var_side.clone();
6371                        *x = Expr::Un {
6372                            op: UnOp::Not,
6373                            e: Box::new(*inner),
6374                        };
6375                    }
6376                    _ => {}
6377                }
6378            }
6379        });
6380    };
6381    match s {
6382        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => fold(cond),
6383        _ => {}
6384    }
6385    walk_mut_deep(s, &mut |st| {
6386        let cond = match st {
6387            Stmt::If { cond, .. } => Some(cond),
6388            Stmt::While { cond, .. } => Some(cond),
6389            Stmt::DoWhile { cond, .. } => Some(cond),
6390            _ => None,
6391        };
6392        if let Some(c) = cond {
6393            fold(c);
6394        }
6395    });
6396}
6397
6398/// Normalize `1 == v` comparisons to `v == 1` (d8 emits const-first if-eq).
6399pub fn flip_const_compares(s: &mut Stmt) {
6400    rewrite_exprs(s, &mut |e| {
6401        deep_rewrite(e, &mut |x| {
6402            if let Expr::Bin { op, l, r, .. } = x {
6403                if matches!(
6404                    op,
6405                    BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
6406                ) {
6407                    let l_const = matches!(&**l, Expr::Const(_));
6408                    let r_var = matches!(&**r, Expr::Local { .. });
6409                    if l_const && r_var {
6410                        std::mem::swap(l, r);
6411                    }
6412                }
6413            }
6414        });
6415    });
6416}
6417
6418/// Object-typed locals compared against 0 compare against `null`.
6419pub fn null_compares(vt: &VarTable, body: &mut Stmt) {
6420    let obj_vars: HashSet<u32> = vt
6421        .vars
6422        .iter()
6423        .filter(|v| v.ty.erased().is_reference())
6424        .map(|v| v.id)
6425        .collect();
6426    rewrite_exprs(body, &mut |e| {
6427        deep_rewrite(e, &mut |x| {
6428            null_side_rewrite(x, &|v| obj_vars.contains(&v));
6429        });
6430    });
6431}
6432
6433/// Drop assignments/declarations of locals that are NEVER read anywhere
6434/// in the method. Register-machine merges commit values no Java-level
6435/// code consumes (phi residue like `printStream = check;` — not just
6436/// noise: the merge variable's inferred type need not match the value,
6437/// so these lines are often type errors as well). Side-effect-free
6438/// values drop entirely; an impure value survives as a bare expression
6439/// statement. Iterated to a fixpoint: dropping `a = b` can make `b`
6440/// unread.
6441pub fn drop_dead_locals(body: &mut Stmt) {
6442    loop {
6443        let mut reads: Vec<usize> = Vec::new();
6444        count_reads(body, &mut reads);
6445        let mut dropped = 0usize;
6446        walk_mut_deep(body, &mut |st| match st {
6447            Stmt::Block(items) => prune_dead_items(items, &reads, &mut dropped),
6448            // Switch case bodies and For inits are bare `Vec<Stmt>`, NOT
6449            // wrapped in a `Stmt::Block`, so the Block arm never sees them.
6450            // Dead phi commits land directly in case bodies (`sb4 =
6451            // compareTo;` per case): pruning only Blocks dropped the
6452            // commit's reader (in a real post-switch Block) but left the
6453            // now-dead commits, stalling the fixpoint cascade and leaking
6454            // mistyped phi residue. TryWithResources.resources is
6455            // deliberately excluded — its LocalDefs carry auto-close
6456            // semantics that a bare-expression rewrite would break.
6457            Stmt::Switch { cases, .. } => {
6458                for c in cases {
6459                    prune_dead_items(&mut c.body, &reads, &mut dropped);
6460                }
6461            }
6462            Stmt::For { init, .. } => prune_dead_items(init, &reads, &mut dropped),
6463            _ => {}
6464        });
6465        if dropped == 0 {
6466            break;
6467        }
6468    }
6469}
6470
6471/// Remove dead assignments/declarations from one bare statement vector.
6472/// Shared by `drop_dead_locals` across every prunable `Vec<Stmt>`
6473/// container. Side-effect-free values drop entirely; an impure value
6474/// survives as a bare expression statement.
6475fn prune_dead_items(items: &mut Vec<Stmt>, reads: &[usize], dropped: &mut usize) {
6476    let dead = |v: u32| reads.get(v as usize).copied().unwrap_or(0) == 0;
6477    items.retain_mut(|x| match x {
6478        Stmt::ExprStmt(Expr::Assign { target, value, op, .. }) => {
6479            let dead_target = matches!(&**target, Expr::Local { var, .. } if dead(*var));
6480            if dead_target && matches!(*op, AssignOp::Plain) {
6481                *dropped += 1;
6482                if has_side_effects(value) {
6483                    let e = std::mem::replace(value, Box::new(Expr::This));
6484                    *x = Stmt::ExprStmt(*e);
6485                    true
6486                } else {
6487                    false
6488                }
6489            } else {
6490                true
6491            }
6492        }
6493        Stmt::LocalDef { var, init, .. } if dead(*var) => {
6494            *dropped += 1;
6495            if let Some(e) = init.take() {
6496                if has_side_effects(&e) {
6497                    *x = Stmt::ExprStmt(e);
6498                    return true;
6499                }
6500            }
6501            false
6502        }
6503        _ => true,
6504    });
6505}
6506
6507/// Immutable shallow child walk (mirrors `walk_mut`), for the two-pass
6508/// scope analysis below.
6509fn for_each_child_stmt<'a>(st: &'a Stmt, f: &mut impl FnMut(&'a Stmt)) {
6510    match st {
6511        Stmt::Block(v) => {
6512            for x in v {
6513                f(x);
6514            }
6515        }
6516        Stmt::If {
6517            then_stmt,
6518            else_stmt,
6519            ..
6520        } => {
6521            f(then_stmt);
6522            if let Some(e) = else_stmt {
6523                f(e);
6524            }
6525        }
6526        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => f(body),
6527        Stmt::For { init, body, .. } => {
6528            for x in init {
6529                f(x);
6530            }
6531            f(body);
6532        }
6533        Stmt::ForEach { body, .. } => f(body),
6534        Stmt::Switch { cases, default, .. } => {
6535            for c in cases {
6536                for x in &c.body {
6537                    f(x);
6538                }
6539            }
6540            if let Some(d) = default {
6541                f(d);
6542            }
6543        }
6544        Stmt::Try {
6545            body,
6546            catches,
6547            finally,
6548        } => {
6549            f(body);
6550            for c in catches {
6551                f(&c.body);
6552            }
6553            if let Some(fl) = finally {
6554                f(fl);
6555            }
6556        }
6557        Stmt::TryWithResources {
6558            resources,
6559            body,
6560            catches,
6561            finally,
6562        } => {
6563            for r in resources {
6564                f(r);
6565            }
6566            f(body);
6567            for c in catches {
6568                f(&c.body);
6569            }
6570            if let Some(fl) = finally {
6571                f(fl);
6572            }
6573        }
6574        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => f(body),
6575        _ => {}
6576    }
6577}
6578
6579/// Vars occurring in this statement's OWN expression payloads (nested
6580/// sub-statements are descended separately so every occurrence carries
6581/// its innermost block id). ForEach/Catch bound vars are declarations,
6582/// not occurrences.
6583fn stmt_shallow_vars<F: FnMut(u32)>(st: &Stmt, out: &mut F) {
6584    fn ex<F: FnMut(u32)>(e: &Expr, out: &mut F) {
6585        visit_exprs(e, &mut |x| {
6586            if let Expr::Local { var, .. } = x {
6587                out(*var);
6588            }
6589        });
6590    }
6591    match st {
6592        Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
6593            ex(e, out)
6594        }
6595        Stmt::TernaryValue { e } => ex(e, out),
6596        Stmt::Return(Some(e)) => ex(e, out),
6597        Stmt::LocalDef { var, init, .. } => {
6598            out(*var);
6599            if let Some(e) = init {
6600                ex(e, out);
6601            }
6602        }
6603        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
6604            ex(cond, out)
6605        }
6606        Stmt::For { cond, update, .. } => {
6607            if let Some(c) = cond {
6608                ex(c, out);
6609            }
6610            for u in update {
6611                ex(u, out);
6612            }
6613        }
6614        Stmt::ForEach { iterable, .. } => ex(iterable, out),
6615        Stmt::Switch { selector, cases, .. } => {
6616            ex(selector, out);
6617            for cg in cases {
6618                if let Some(g) = &cg.guard {
6619                    ex(g, out);
6620                }
6621            }
6622        }
6623        Stmt::Assert { cond, msg } => {
6624            ex(cond, out);
6625            if let Some(m) = msg {
6626                ex(m, out);
6627            }
6628        }
6629        Stmt::Synchronized { lock, .. } => ex(lock, out),
6630        _ => {}
6631    }
6632}
6633
6634/// Pass A: number every Block in pre-order (root = 0) and record each
6635/// var's defining block. `sizes[id]` = the id-range span of the subtree.
6636fn scope_pass_a(st: &Stmt, cur: u32, next: &mut u32, def_block: &mut [u32], sizes: &mut Vec<u32>) {
6637    if let Stmt::LocalDef { var, .. } = st {
6638        let i = *var as usize;
6639        if i < def_block.len() && def_block[i] == u32::MAX {
6640            def_block[i] = cur;
6641        }
6642    }
6643    for_each_child_stmt(st, &mut |c| {
6644        if matches!(c, Stmt::Block(_)) {
6645            let id = *next;
6646            *next += 1;
6647            sizes.push(0);
6648            scope_pass_a(c, id, next, def_block, sizes);
6649            sizes[id as usize] = *next - id;
6650        } else {
6651            scope_pass_a(c, cur, next, def_block, sizes);
6652        }
6653    });
6654}
6655
6656/// Pass B (same numbering walk): count each var's occurrences overall
6657/// and within its defining block's subtree.
6658fn scope_pass_b(
6659    st: &Stmt,
6660    cur: u32,
6661    next: &mut u32,
6662    def_block: &[u32],
6663    sizes: &[u32],
6664    within: &mut [u32],
6665    total: &mut [u32],
6666) {
6667    stmt_shallow_vars(st, &mut |v| {
6668        let i = v as usize;
6669        if i < total.len() {
6670            total[i] += 1;
6671            let d = def_block[i];
6672            if d != u32::MAX && d <= cur && cur < d + sizes[d as usize] {
6673                within[i] += 1;
6674            }
6675        }
6676    });
6677    for_each_child_stmt(st, &mut |c| {
6678        if matches!(c, Stmt::Block(_)) {
6679            let id = *next;
6680            *next += 1;
6681            scope_pass_b(c, id, next, def_block, sizes, within, total);
6682        } else {
6683            scope_pass_b(c, cur, next, def_block, sizes, within, total);
6684        }
6685    });
6686}
6687
6688/// Declaration hygiene: every used var that has no LocalDef gets a bare
6689/// declaration at the top; duplicate LocalDefs demote to assignments.
6690/// In an enum's `<clinit>`, every constant's assignment
6691/// (`Self.FIELD = new Self("NAME", ordinal, ...)`) is compiler-mandated
6692/// boilerplate — when the class renders as a true `enum` declaration the
6693/// constants live in the header and these assignments must go. Only
6694/// ACC_ENUM-flagged static fields of the class itself are touched; the
6695/// `$VALUES` array assignment stays (a static block in an enum is legal).
6696pub fn strip_enum_const_inits(s: &mut Stmt, class: &crate::PoolClass) {
6697    let const_fields: jdc_core::FxHashSet<&str> = class
6698        .static_fields
6699        .iter()
6700        .filter(|f| f.access & crate::access::ACC_ENUM != 0)
6701        .map(|f| f.name.as_str())
6702        .collect();
6703    if const_fields.is_empty() {
6704        return;
6705    }
6706    if let Stmt::Block(stmts) = s {
6707        stmts.retain(|st| {
6708            !matches!(
6709                st,
6710                Stmt::ExprStmt(Expr::Assign { target, value, .. })
6711                    if matches!(&**target,
6712                        Expr::Field { cls, name, is_static: true, .. }
6713                            if cls.as_ref() == class.name
6714                                && const_fields.contains(&**name))
6715                        && matches!(&**value, Expr::New { cls: ncls, .. } if ncls.as_ref() == class.name)
6716            )
6717        });
6718    }
6719}
6720
6721/// Java requires the `super(...)`/`this(...)` delegation to be the FIRST
6722/// statement of a constructor. d8/R8 order the outer-reference capture
6723/// (`this.b = p1;`) or other field writes before the invokesuper in the
6724/// bytecode, which lifts as-is into an uncompilable statement order —
6725/// hoist the bare delegation call to position 0 (jadx does the same).
6726/// Does the expression touch `this` (explicit, implicit-instance member
6727/// access, or super)? Such a def cannot be inlined into a delegation
6728/// that must run BEFORE the supertype constructor.
6729fn contains_this_access(e: &Expr) -> bool {
6730    let mut hit = false;
6731    visit_exprs(e, &mut |x| {
6732        if hit {
6733            return;
6734        }
6735        match x {
6736            Expr::This => hit = true,
6737            Expr::Field { owner, is_static: false, .. } => {
6738                if owner.is_none() || matches!(&**owner.as_ref().unwrap(), Expr::This) {
6739                    hit = true;
6740                }
6741            }
6742            Expr::Method { owner, is_static: false, .. } => {
6743                if owner.is_none() || matches!(&**owner.as_ref().unwrap(), Expr::This) {
6744                    hit = true;
6745                }
6746            }
6747            _ => {}
6748        }
6749    });
6750    hit
6751}
6752
6753/// Inline the transitive straight-line defs of a delegation call's
6754/// arg locals into the call, so hoisting it to position 0 leaves no
6755/// dangling name (lark ProtoAdapter's `super(str)` with `str` defined
6756/// BELOW the hoist point — 找不到符号 变量 str). Returns false when any
6757/// arg var has no inlineable def (param-less, missing, this-touching,
6758/// or cyclic) — the caller then leaves the call in place: a
6759/// not-first-statement error is cheaper than an unresolved name that
6760/// poisons every downstream type.
6761/// Any occurrence of `var` (read OR assign-target) in `stmts` — the
6762/// suffix-use test for dropping a consumed def: count_locals_stmts only
6763/// sees reads, and a suffix WRITE to a dropped declaration makes
6764/// ensure_declared re-add `Type v;` at the method top — BEFORE the
6765/// hoisted this()/super() ("对this的调用必须是构造器中的第一个语句",
6766/// lark +587 the read-only test caused).
6767fn var_occurs_in(stmts: &[Stmt], var: u32) -> bool {
6768    stmts.iter().any(|st| {
6769        let mut c = st.clone();
6770        let mut hit = false;
6771        walk_stmt_exprs(&mut c, &mut |e| {
6772            if !hit {
6773                deep_rewrite(e, &mut |x| {
6774                    if let Expr::Local { var: v, .. } = x {
6775                        if *v == var {
6776                            hit = true;
6777                        }
6778                    }
6779                });
6780            }
6781        });
6782        hit
6783    })
6784}
6785
6786// ---------------------------------------------------------------------------
6787// Branched-delegation ctors → static resolver helper.
6788// ---------------------------------------------------------------------------
6789
6790/// A synthetic static method extracted from a constructor whose
6791/// delegation is preceded by computation (the Kotlin default-arg /
6792/// conditional-bridge shape). Rendered by classdec after the real
6793/// methods; the ctor becomes a single first-statement delegation whose
6794/// carrier arg is the helper call.
6795pub struct CtorHelper {
6796    pub name: String,
6797    pub ret: JavaType,
6798    pub params: Vec<(JavaType, String)>,
6799    pub body: Stmt,
6800    pub vt: VarTable,
6801}
6802
6803fn ctor_helpers() -> &'static std::sync::Mutex<std::collections::HashMap<String, Vec<CtorHelper>>> {
6804    static H: std::sync::OnceLock<std::sync::Mutex<std::collections::HashMap<String, Vec<CtorHelper>>>> =
6805        std::sync::OnceLock::new();
6806    H.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::default()))
6807}
6808
6809/// Deterministic helper name: FNV-1a over (class, ctor descriptor) —
6810/// worker-thread registration order must not leak into the output.
6811fn helper_name(class: &str, desc: &str) -> String {
6812    let mut h: u64 = 0xcbf29ce484222325;
6813    for b in class.as_bytes().iter().chain(b"::").chain(desc.as_bytes()) {
6814        h ^= *b as u64;
6815        h = h.wrapping_mul(0x100000001b3);
6816    }
6817    format!("resolve${:x}", (h >> 16) & 0xffff_ffff)
6818}
6819
6820/// Drain the helpers registered for a class (render-time). Sorted by
6821/// name: registration order is worker-scheduling dependent.
6822pub fn take_ctor_helpers(class: &str) -> Vec<CtorHelper> {
6823    let mut m = ctor_helpers().lock().unwrap_or_else(|e| e.into_inner());
6824    let mut v = m.remove(class).unwrap_or_default();
6825    v.sort_by(|a, b| a.name.cmp(&b.name));
6826    v
6827}
6828
6829fn strip_delegations_deep(stmts: &mut Vec<Stmt>) {
6830    stmts.retain(|s| !is_bare_ctor_call(s));
6831    for s in stmts.iter_mut() {
6832        strip_delegation_one(s);
6833    }
6834}
6835
6836fn strip_delegation_one(st: &mut Stmt) {
6837    if is_bare_ctor_call(st) {
6838        *st = Stmt::Block(Vec::new());
6839        return;
6840    }
6841    match st {
6842        Stmt::Block(v) => strip_delegations_deep(v),
6843        Stmt::If { then_stmt, else_stmt, .. } => {
6844            strip_delegation_one(then_stmt);
6845            if let Some(e) = else_stmt {
6846                strip_delegation_one(e);
6847            }
6848        }
6849        Stmt::While { body, .. }
6850        | Stmt::DoWhile { body, .. }
6851        | Stmt::ForEach { body, .. }
6852        | Stmt::Synchronized { body, .. }
6853        | Stmt::Labeled { body, .. } => strip_delegation_one(body),
6854        Stmt::For { init, body, .. } => {
6855            strip_delegations_deep(init);
6856            strip_delegation_one(body);
6857        }
6858        Stmt::Switch { cases, default, .. } => {
6859            for c in cases.iter_mut() {
6860                strip_delegations_deep(&mut c.body);
6861            }
6862            if let Some(d) = default {
6863                strip_delegation_one(d);
6864            }
6865        }
6866        Stmt::Try { body, catches, finally } | Stmt::TryWithResources { body, catches, finally, .. } => {
6867            strip_delegation_one(body);
6868            for c in catches.iter_mut() {
6869                strip_delegation_one(&mut c.body);
6870            }
6871            if let Some(f) = finally {
6872                strip_delegation_one(f);
6873            }
6874        }
6875        _ => {}
6876    }
6877}
6878
6879#[allow(clippy::ptr_arg)]
6880fn rewrite_bare_returns(stmts: &mut Vec<Stmt>, val: &Expr) {
6881    for s in stmts.iter_mut() {
6882        rewrite_bare_returns_one(s, val);
6883    }
6884}
6885
6886fn rewrite_bare_returns_one(st: &mut Stmt, val: &Expr) {
6887    match st {
6888        Stmt::Return(None) => *st = Stmt::Return(Some(val.clone())),
6889        Stmt::Block(v) => rewrite_bare_returns(v, val),
6890        Stmt::If { then_stmt, else_stmt, .. } => {
6891            rewrite_bare_returns_one(then_stmt, val);
6892            if let Some(e) = else_stmt {
6893                rewrite_bare_returns_one(e, val);
6894            }
6895        }
6896        Stmt::While { body, .. }
6897        | Stmt::DoWhile { body, .. }
6898        | Stmt::ForEach { body, .. }
6899        | Stmt::Synchronized { body, .. }
6900        | Stmt::Labeled { body, .. } => rewrite_bare_returns_one(body, val),
6901        Stmt::For { init, body, .. } => {
6902            rewrite_bare_returns(init, val);
6903            rewrite_bare_returns_one(body, val);
6904        }
6905        Stmt::Switch { cases, default, .. } => {
6906            for c in cases.iter_mut() {
6907                rewrite_bare_returns(&mut c.body, val);
6908            }
6909            if let Some(d) = default {
6910                rewrite_bare_returns_one(d, val);
6911            }
6912        }
6913        Stmt::Try { body, catches, finally } | Stmt::TryWithResources { body, catches, finally, .. } => {
6914            rewrite_bare_returns_one(body, val);
6915            for c in catches.iter_mut() {
6916                rewrite_bare_returns_one(&mut c.body, val);
6917            }
6918            if let Some(f) = finally {
6919                rewrite_bare_returns_one(f, val);
6920            }
6921        }
6922        _ => {}
6923    }
6924}
6925
6926fn collect_def_vars(stmts: &[Stmt], out: &mut jdc_core::FxHashSet<u32>) {
6927    for st in stmts {
6928        match st {
6929            Stmt::LocalDef { var, .. } => {
6930                out.insert(*var);
6931            }
6932            Stmt::ExprStmt(Expr::Assign { target, .. }) => {
6933                if let Expr::Local { var, .. } = &**target {
6934                    out.insert(*var);
6935                }
6936            }
6937            Stmt::Block(v) => collect_def_vars(v, out),
6938            Stmt::If { then_stmt, else_stmt, .. } => {
6939                collect_def_vars(std::slice::from_ref(then_stmt.as_ref()), out);
6940                if let Some(e) = else_stmt {
6941                    collect_def_vars(std::slice::from_ref(e.as_ref()), out);
6942                }
6943            }
6944            Stmt::While { body, .. }
6945            | Stmt::DoWhile { body, .. }
6946            | Stmt::Synchronized { body, .. }
6947            | Stmt::Labeled { body, .. } => {
6948                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
6949            }
6950            Stmt::ForEach { var, body, .. } => {
6951                out.insert(*var);
6952                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
6953            }
6954            Stmt::For { init, body, .. } => {
6955                collect_def_vars(init, out);
6956                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
6957            }
6958            Stmt::Switch { cases, default, .. } => {
6959                for c in cases {
6960                    collect_def_vars(&c.body, out);
6961                }
6962                if let Some(d) = default {
6963                    collect_def_vars(std::slice::from_ref(d.as_ref()), out);
6964                }
6965            }
6966            Stmt::Try { body, catches, finally }
6967            | Stmt::TryWithResources { body, catches, finally, .. } => {
6968                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
6969                for c in catches {
6970                    out.insert(c.var);
6971                    collect_def_vars(std::slice::from_ref(c.body.as_ref()), out);
6972                }
6973                if let Some(f) = finally {
6974                    collect_def_vars(std::slice::from_ref(f.as_ref()), out);
6975                }
6976            }
6977            _ => {}
6978        }
6979    }
6980}
6981
6982/// v1 extraction is limited to Block/If/straight-line computation (the
6983/// Kotlin default-arg shape); loops/switch/try in the extraction bail.
6984fn extraction_shape_ok(stmts: &[Stmt]) -> bool {
6985    stmts.iter().all(|st| match st {
6986        Stmt::Block(v) => extraction_shape_ok(v),
6987        Stmt::LocalDef { .. }
6988        | Stmt::ExprStmt(_)
6989        | Stmt::Return(_)
6990        | Stmt::Throw(_)
6991        | Stmt::Assert { .. }
6992        | Stmt::Break(_)
6993        | Stmt::Continue(_) => true,
6994        Stmt::If { then_stmt, else_stmt, .. } => {
6995            ok_one(then_stmt)
6996                && else_stmt.as_deref().map(ok_one).unwrap_or(true)
6997        }
6998        Stmt::While { body, .. }
6999        | Stmt::DoWhile { body, .. }
7000        | Stmt::ForEach { body, .. }
7001        | Stmt::Synchronized { body, .. } => ok_one(body),
7002        Stmt::Labeled { body, .. } => ok_one(body),
7003        Stmt::For { init, body, .. } => extraction_shape_ok(init) && ok_one(body),
7004        Stmt::Switch { cases, default, .. } => {
7005            cases.iter().all(|c| extraction_shape_ok(&c.body))
7006                && default.as_deref().map(ok_one).unwrap_or(true)
7007        }
7008        Stmt::Try { body, catches, finally }
7009        | Stmt::TryWithResources { body, catches, finally, .. } => {
7010            ok_one(body)
7011                && catches.iter().all(|c| ok_one(&c.body))
7012                && finally.as_deref().map(ok_one).unwrap_or(true)
7013        }
7014        // Label/Goto (unstructured), MonitorEnter/Exit (raw), ClassDecl
7015        // (local class), TernaryValue, Raw: stay out of v2 extraction.
7016        _ => false,
7017    })
7018}
7019
7020fn ok_one(st: &Stmt) -> bool {
7021    extraction_shape_ok(std::slice::from_ref(st))
7022}
7023
7024fn static_safe(stmts: &[Stmt], this_id: Option<u32>) -> bool {
7025    let mut ok = true;
7026    for st in stmts {
7027        let mut c = st.clone();
7028        walk_stmt_exprs(&mut c, &mut |e| {
7029            deep_rewrite(e, &mut |x| {
7030                match x {
7031                    Expr::This => ok = false,
7032                    // ddc's IR usually carries `this` as the var-0
7033                    // PARAM local, not the Expr::This variant — a
7034                    // helper body reading it dangles (the this param
7035                    // is excluded from the helper's param list) AND
7036                    // renders 无法从静态上下文中引用非静态 (weixin y9/i).
7037                    Expr::Local { var, .. } => {
7038                        if Some(*var) == this_id {
7039                            ok = false;
7040                        }
7041                    }
7042                    // implicit-this member access / call
7043                    Expr::Field { owner: None, is_static: false, .. } => ok = false,
7044                    Expr::Method { owner: None, is_static: false, .. } => ok = false,
7045                    _ => {}
7046                }
7047            });
7048        });
7049    }
7050    ok
7051}
7052
7053/// Repeated identical branch-site delegations with param-only args
7054/// (weixin appbrand/zc: bare `super()` at four branch ends, each
7055/// followed by field-write tails) carry no computed carrier, so the
7056/// helper extraction has nothing to return — but they need no helper:
7057/// every delegating path runs the SAME call, so hoist one copy to
7058/// position 0 and strip the sites. The per-branch tails stay in their
7059/// branches and now legally follow the first-statement delegation.
7060/// Same reordering approximation fix_ctor_super_first already ships for
7061/// the straight-line case (paths that threw BEFORE delegating now run
7062/// the delegation first — Object-super, the dominant shape, is inert).
7063/// Gate: every del deep-equal and reading params only (a computed local
7064/// would dangle at position 0 — that is the helper extractor's job).
7065pub fn hoist_branch_delegations(body: &mut Stmt, vt: &VarTable) {
7066    let Stmt::Block(stmts) = body else { return };
7067    if stmts.first().is_some_and(is_bare_ctor_call) {
7068        return; // already first
7069    }
7070    let mut dels: Vec<Expr> = Vec::new();
7071    for st in stmts.iter() {
7072        let mut c = st.clone();
7073        walk_stmt_exprs(&mut c, &mut |e| {
7074            if is_delegation_expr(e) {
7075                dels.push(e.clone());
7076            }
7077        });
7078    }
7079    if dels.is_empty() {
7080        return;
7081    }
7082    if !dels.iter().all(|d| *d == dels[0]) {
7083        return;
7084    }
7085    let mut bad = false;
7086    visit_exprs(&dels[0], &mut |x| {
7087        if let Expr::Local { var, .. } = x {
7088            if (*var as usize) >= vt.vars.len() || !vt.vars[*var as usize].is_param {
7089                bad = true;
7090            }
7091        }
7092    });
7093    if bad {
7094        return;
7095    }
7096    let del = dels[0].clone();
7097    strip_delegations_deep(stmts);
7098    stmts.insert(0, Stmt::ExprStmt(del));
7099}
7100
7101/// The branched-delegation ctor family ("对this的调用必须是构造器中的
7102/// 第一个语句", ~2k across corpora): R8 renders a Kotlin default-arg /
7103/// conditional-bridge ctor as computation statements (straight-line plus
7104/// mask-guarded overrides) with every control path ending in a
7105/// `this(a, b, carrier)` delegation. Java cannot express statements
7106/// before the delegation, and unlike the linear-chain case the carrier's
7107/// value is computed by multi-statement branched code that cannot fold
7108/// into a ternary. Extract the computation into a synthetic
7109/// `private static T resolve$X(<ctor params>)` and make the ctor a
7110/// single first-statement `this(a, b, resolve$X(..))` — compilable AND
7111/// faithful (jadx renders the un-compilable statements-before-this form
7112/// for this family).
7113///
7114/// Two carrier modes:
7115/// - Single (v1): every delegation textually identical and exactly one
7116///   non-param local (the carrier) read exactly once across the args.
7117/// - Position (v2): delegations differ at exactly ONE arg position k,
7118///   every site passing a bare non-param local there (per-branch
7119///   carriers, lark BaseProtocol$ReliablePushList's v22_g1/v22x). Each
7120///   stripped delegation's own carrier fills the return that follows it;
7121///   merge-point returns fall back to the primary site's carrier.
7122///
7123/// The top-level primary delegation is optional in BOTH modes — when
7124/// every path delegates inside branches (weixin ln1/b1's super() in
7125/// if/try/catch) the whole body is extracted and any del serves as the
7126/// template (all non-k parts identical).
7127///
7128/// Shared gates (all conservative, bail keeps today's behavior): not an
7129/// enum (trace-param strip shifts render params); nested only when the
7130/// class RENDERS static-nested (member-inner ctors lose the outer param
7131/// at render); computation is Block/If/straight-line only; no
7132/// `this`-dependence; no local defined in the extraction used after the
7133/// primary.
7134pub fn extract_branched_delegation_helper(
7135    body: &mut Stmt,
7136    vt: &VarTable,
7137    pool: &DexPool,
7138    class: &crate::PoolClass,
7139    desc_str: &str,
7140) {
7141    if class.is_enum() {
7142        return;
7143    }
7144    // Member-inner ctors lose the synthetic outer param at render
7145    // (qualified-new absorbs it), so the helper's param list would no
7146    // longer match the rendered ctor. STATIC nested classes render all
7147    // params like top-level ones (lark BaseProtocol$PushList family).
7148    if class.nesting.enclosing_class.is_some() && !crate::ctx::nested_is_static(pool, class) {
7149        return;
7150    }
7151    let Stmt::Block(stmts) = body else { return };
7152    let mut dels: Vec<Expr> = Vec::new();
7153    for st in stmts.iter() {
7154        let mut c = st.clone();
7155        walk_stmt_exprs(&mut c, &mut |e| {
7156            if is_delegation_expr(e) {
7157                dels.push(e.clone());
7158            }
7159        });
7160    }
7161    if dels.is_empty() {
7162        return;
7163    }
7164    if dels.len() == 1 && stmts.first().is_some_and(is_bare_ctor_call) {
7165        return; // already first
7166    }
7167    let template = dels[0].clone();
7168    let Expr::Method { args: t_args, .. } = &template else {
7169        return;
7170    };
7171    // Carrier discovery: Single (all dels identical, one non-param local
7172    // read once) or Position (dels differ at exactly one arg slot, every
7173    // site a bare non-param local of one erased type).
7174    enum Mode {
7175        Single(u32),
7176        Position(usize),
7177    }
7178    let mode = if dels.iter().all(|d| *d == dels[0]) {
7179        let mut carrier: Option<u32> = None;
7180        let mut reads = 0usize;
7181        let mut multi = false;
7182        for a in t_args.iter() {
7183            let c = a.clone();
7184            visit_exprs(&c, &mut |x| {
7185                if let Expr::Local { var, .. } = x {
7186                    if (*var as usize) < vt.vars.len() && !vt.vars[*var as usize].is_param {
7187                        reads += 1;
7188                        match carrier {
7189                            Some(v) if v == *var => {}
7190                            Some(_) => multi = true,
7191                            None => carrier = Some(*var),
7192                        }
7193                    }
7194                }
7195            });
7196        }
7197        match carrier {
7198            Some(cv) if !multi && reads == 1 => Mode::Single(cv),
7199            _ => return,
7200        }
7201    } else {
7202        let arity = t_args.len();
7203        // Uniform arity FIRST: a bare `super()` mixed among arg-carrying
7204        // dels made the single-diff slot k index past a short del's args
7205        // (deepseek v52 / kimi c60.o panics, index-out-of-bounds).
7206        if dels
7207            .iter()
7208            .any(|d| !matches!(d, Expr::Method { args, .. } if args.len() == arity))
7209        {
7210            return;
7211        }
7212        let mut diff: Vec<usize> = Vec::new();
7213        for j in 0..arity {
7214            if dels.iter().any(|d| match d {
7215                Expr::Method { args, .. } => args.get(j) != t_args.get(j),
7216                _ => true,
7217            }) {
7218                diff.push(j);
7219            }
7220        }
7221        if diff.len() != 1 {
7222            return;
7223        }
7224        let k = diff[0];
7225        // Non-arg parts AND all other arg positions identical: neutralize
7226        // slot k on every del and compare whole exprs (catches mixed
7227        // this()/super() or different targets).
7228        let mut base0 = dels[0].clone();
7229        if let Expr::Method { args, .. } = &mut base0 {
7230            args[k] = Expr::Const(ConstVal::Null);
7231        }
7232        for d in dels.iter() {
7233            let mut c = d.clone();
7234            if let Expr::Method { args, .. } = &mut c {
7235                args[k] = Expr::Const(ConstVal::Null);
7236            }
7237            if c != base0 {
7238                return;
7239            }
7240        }
7241        // Slot k: bare non-param local at every site, one erased type,
7242        // and the carrier never read at another position.
7243        let mut carrier_ty: Option<JavaType> = None;
7244        for d in dels.iter() {
7245            let Expr::Method { args, .. } = d else {
7246                return;
7247            };
7248            let Expr::Local { var, .. } = &args[k] else {
7249                return;
7250            };
7251            if (*var as usize) >= vt.vars.len() || vt.vars[*var as usize].is_param {
7252                return;
7253            }
7254            let ty = vt.vars[*var as usize].ty.erased();
7255            match &carrier_ty {
7256                Some(t) if *t == ty => {}
7257                Some(_) => return,
7258                None => carrier_ty = Some(ty),
7259            }
7260            // Non-k positions must be param/const/static-only — a
7261            // computed local there would have its def extracted into
7262            // the helper and the ctor arg list would dangle (lark
7263            // audiosave/i 找不到符号 ×5-per-site). Also covers the
7264            // carrier leaking into another position.
7265            for (j, a) in args.iter().enumerate() {
7266                if j == k {
7267                    continue;
7268                }
7269                let mut bad = false;
7270                visit_exprs(a, &mut |x| {
7271                    if let Expr::Local { var: v2, .. } = x {
7272                        if (*v2 as usize) >= vt.vars.len() || !vt.vars[*v2 as usize].is_param {
7273                            bad = true;
7274                        }
7275                    }
7276                });
7277                if bad {
7278                    return;
7279                }
7280            }
7281        }
7282        Mode::Position(k)
7283    };
7284    let l = stmts.iter().rposition(is_bare_ctor_call);
7285    if l == Some(0) {
7286        return;
7287    }
7288    let (mut extracted, mut tail): (Vec<Stmt>, Vec<Stmt>) = match l {
7289        Some(l) => (stmts[..l].to_vec(), stmts[l + 1..].to_vec()),
7290        None => (stmts.clone(), Vec::new()),
7291    };
7292    if !extraction_shape_ok(&extracted) {
7293        return;
7294    }
7295    // Post-delegation tails (field writes / trace stubs between each
7296    // branch delegation and its return) are CTOR work, not helper
7297    // computation — they commonly touch `this`. When every site's tail
7298    // is identical (and equals the top-level continuation when one
7299    // exists) hoist one copy after the merged delegation; otherwise
7300    // they stay in the helper and the static_safe gate below decides.
7301    let mut site_tails: Vec<Vec<Stmt>> = Vec::new();
7302    {
7303        let mut probe = extracted.clone();
7304        site_tails_walk(&mut probe, &mut site_tails, false);
7305    }
7306    let hoist = !site_tails.is_empty() && {
7307        let n0 = normalize_tail(&site_tails[0]);
7308        site_tails.iter().all(|t| normalize_tail(t) == n0)
7309            && (l.is_none() || normalize_tail(&tail) == n0)
7310    };
7311    if hoist {
7312        site_tails_walk(&mut extracted, &mut Vec::new(), true);
7313        if l.is_none() {
7314            tail = normalize_tail(&site_tails[0]);
7315        }
7316    }
7317    // The delegation the ctor keeps: the top-level primary when present,
7318    // else any del (all non-k parts are identical).
7319    let primary_del: Expr = match l {
7320        Some(l) => match &stmts[l] {
7321            Stmt::ExprStmt(e) => e.clone(),
7322            _ => return,
7323        },
7324        None => dels[0].clone(),
7325    };
7326    let ret = match mode {
7327        Mode::Single(cv) => vt.var(cv).ty.erased(),
7328        Mode::Position(k) => match &primary_del {
7329            Expr::Method { args, .. } => match args.get(k) {
7330                Some(Expr::Local { var, .. }) => vt.var(*var).ty.erased(),
7331                _ => return,
7332            },
7333            _ => return,
7334        },
7335    };
7336    match mode {
7337        Mode::Single(cv) => {
7338            strip_delegations_deep(&mut extracted);
7339            let cv_local = Expr::Local {
7340                var: cv,
7341                ty: vt.var(cv).ty.clone(),
7342            };
7343            rewrite_bare_returns(&mut extracted, &cv_local);
7344            if !always_returns(&extracted) {
7345                extracted.push(Stmt::Return(Some(cv_local)));
7346            }
7347        }
7348        Mode::Position(k) => {
7349            // Each stripped delegation's own slot-k carrier fills the
7350            // next bare return in its list; merge-point returns fall
7351            // back to the primary carrier below.
7352            fill_returns_by_position(&mut extracted, k);
7353            let fallback = match &primary_del {
7354                Expr::Method { args, .. } => args[k].clone(),
7355                _ => return,
7356            };
7357            rewrite_bare_returns(&mut extracted, &fallback);
7358            if !always_returns(&extracted) {
7359                extracted.push(Stmt::Return(Some(fallback)));
7360            }
7361        }
7362    }
7363    let this_id = vt
7364        .vars
7365        .iter()
7366        .find(|v| v.is_param && v.name == "this")
7367        .map(|v| v.id);
7368    if !static_safe(&extracted, this_id) {
7369        return;
7370    }
7371    let mut defs: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
7372    collect_def_vars(&extracted, &mut defs);
7373    if defs.iter().any(|v| var_occurs_in(&tail, *v)) {
7374        return;
7375    }
7376    let params: Vec<(u32, JavaType, String)> = vt
7377        .vars
7378        .iter()
7379        .filter(|v| v.is_param && v.name != "this")
7380        .map(|v| (v.id, v.ty.erased(), v.name.clone()))
7381        .collect();
7382    let name = helper_name(&class.name, desc_str);
7383    let call = Expr::Method {
7384        owner: None,
7385        cls: std::sync::Arc::from(class.name.as_str()),
7386        name: std::sync::Arc::from(name.as_str()),
7387        desc: std::sync::Arc::new(jdc_core::types::MethodDescriptor {
7388            ret: ret.clone(),
7389            args: params.iter().map(|p| p.1.clone()).collect(),
7390        }),
7391        args: params
7392            .iter()
7393            .map(|(id, ty, _)| Expr::Local {
7394                var: *id,
7395                ty: TypeRef::J(ty.clone()),
7396            })
7397            .collect(),
7398        is_static: true,
7399        is_interface: false,
7400        is_special: false,
7401        is_super: false,
7402        is_dynamic: false,
7403        type_args: Vec::new(),
7404    };
7405    let mut new_del = primary_del.clone();
7406    match mode {
7407        Mode::Single(cv) => {
7408            deep_rewrite(&mut new_del, &mut |x| {
7409                if let Expr::Local { var, .. } = x {
7410                    if *var == cv {
7411                        *x = call.clone();
7412                    }
7413                }
7414            });
7415        }
7416        Mode::Position(k) => {
7417            let Expr::Method { args, .. } = &mut new_del else {
7418                return;
7419            };
7420            if k >= args.len() {
7421                return;
7422            }
7423            args[k] = call.clone();
7424        }
7425    }
7426    {
7427        let mut m = ctor_helpers().lock().unwrap_or_else(|e| e.into_inner());
7428        let v = m.entry(class.name.clone()).or_default();
7429        if !v.iter().any(|h| h.name == name) {
7430            // The extracted body skips the late pipeline polish (it left
7431            // the ctor before cleanup ran) — invert empty thens so the
7432            // helper doesn't render `if (..) {} else {..}`.
7433            let mut hbody = Stmt::Block(extracted);
7434            invert_empty_thens(&mut hbody);
7435            v.push(CtorHelper {
7436                name,
7437                ret,
7438                params: params.into_iter().map(|(_, ty, nm)| (ty, nm)).collect(),
7439                body: hbody,
7440                vt: vt.clone(),
7441            });
7442        } else {
7443            return; // duplicate decompile of the same ctor: keep the first
7444        }
7445    }
7446    *stmts = std::iter::once(Stmt::ExprStmt(new_del)).chain(tail).collect();
7447}
7448
7449/// Position-mode return fill: walk each statement list left-to-right; a
7450/// delegation at slot k hands its carrier to the next bare `return;` in
7451/// the SAME list (the bytecode shape is always `this(..); trace..;
7452/// return;`). With no following return in the list the delegation
7453/// becomes the return itself and the rest of the list (trace stubs only
7454/// — a second delegation there is impossible) is dropped as unreachable.
7455fn fill_returns_by_position(stmts: &mut Vec<Stmt>, k: usize) {
7456    let mut i = 0;
7457    while i < stmts.len() {
7458        if is_bare_ctor_call(&stmts[i]) {
7459            let karg = match &stmts[i] {
7460                Stmt::ExprStmt(Expr::Method { args, .. }) => args.get(k).cloned(),
7461                _ => None,
7462            };
7463            if let Some(v) = karg {
7464                let mut j = i + 1;
7465                let mut found = None;
7466                while j < stmts.len() {
7467                    if matches!(stmts[j], Stmt::Return(None)) {
7468                        found = Some(j);
7469                        break;
7470                    }
7471                    if is_bare_ctor_call(&stmts[j]) {
7472                        break; // next delegation owns the next return
7473                    }
7474                    j += 1;
7475                }
7476                match found {
7477                    Some(j) => {
7478                        stmts[j] = Stmt::Return(Some(v));
7479                        stmts.remove(i);
7480                        continue;
7481                    }
7482                    None => {
7483                        stmts[i] = Stmt::Return(Some(v));
7484                        stmts.truncate(i + 1);
7485                        break;
7486                    }
7487                }
7488            }
7489        }
7490        fill_returns_one(&mut stmts[i], k);
7491        i += 1;
7492    }
7493}
7494
7495fn fill_returns_one(st: &mut Stmt, k: usize) {
7496    match st {
7497        Stmt::Block(v) => fill_returns_by_position(v, k),
7498        Stmt::If { then_stmt, else_stmt, .. } => {
7499            fill_returns_stmt(then_stmt, k);
7500            if let Some(e) = else_stmt {
7501                fill_returns_stmt(e, k);
7502            }
7503        }
7504        Stmt::While { body, .. }
7505        | Stmt::DoWhile { body, .. }
7506        | Stmt::ForEach { body, .. }
7507        | Stmt::Synchronized { body, .. }
7508        | Stmt::Labeled { body, .. } => fill_returns_stmt(body, k),
7509        Stmt::For { init, body, .. } => {
7510            fill_returns_by_position(init, k);
7511            fill_returns_stmt(body, k);
7512        }
7513        Stmt::Switch { cases, default, .. } => {
7514            for c in cases.iter_mut() {
7515                fill_returns_by_position(&mut c.body, k);
7516            }
7517            if let Some(d) = default {
7518                fill_returns_stmt(d, k);
7519            }
7520        }
7521        Stmt::Try { body, catches, finally } | Stmt::TryWithResources { body, catches, finally, .. } => {
7522            fill_returns_stmt(body, k);
7523            for c in catches.iter_mut() {
7524                fill_returns_stmt(&mut c.body, k);
7525            }
7526            if let Some(f) = finally {
7527                fill_returns_stmt(f, k);
7528            }
7529        }
7530        _ => {}
7531    }
7532}
7533
7534fn fill_returns_stmt(st: &mut Stmt, k: usize) {
7535    if is_bare_ctor_call(st) {
7536        // Single-statement branch body (`if (c) this(..);`) — becomes
7537        // the return directly.
7538        if let Stmt::ExprStmt(Expr::Method { args, .. }) = st {
7539            if let Some(v) = args.get(k).cloned() {
7540                *st = Stmt::Return(Some(v));
7541                return;
7542            }
7543        }
7544    }
7545    fill_returns_one(st, k);
7546}
7547
7548fn normalize_tail(t: &[Stmt]) -> Vec<Stmt> {
7549    let mut v = t.to_vec();
7550    while matches!(v.last(), Some(Stmt::Return(None))) {
7551        v.pop();
7552    }
7553    v
7554}
7555
7556/// Record (and with `carve`, remove) the statements between each
7557/// delegation and the next bare return in its list — a delegation with
7558/// no following return takes the rest of the list. Single-statement
7559/// branch bodies record an empty tail.
7560fn site_tails_walk(stmts: &mut Vec<Stmt>, out: &mut Vec<Vec<Stmt>>, carve: bool) {
7561    let mut i = 0;
7562    while i < stmts.len() {
7563        if is_bare_ctor_call(&stmts[i]) {
7564            let mut j = i + 1;
7565            let mut end = stmts.len();
7566            while j < stmts.len() {
7567                if matches!(stmts[j], Stmt::Return(None)) {
7568                    end = j;
7569                    break;
7570                }
7571                if is_bare_ctor_call(&stmts[j]) {
7572                    end = j;
7573                    break;
7574                }
7575                j += 1;
7576            }
7577            out.push(stmts[i + 1..end].to_vec());
7578            if carve {
7579                stmts.splice(i + 1..end, []);
7580            }
7581            i += 1;
7582            continue;
7583        }
7584        site_tails_one(&mut stmts[i], out, carve);
7585        i += 1;
7586    }
7587}
7588
7589fn site_tails_one(st: &mut Stmt, out: &mut Vec<Vec<Stmt>>, carve: bool) {
7590    if is_bare_ctor_call(st) {
7591        out.push(Vec::new());
7592        return;
7593    }
7594    match st {
7595        Stmt::Block(v) => site_tails_walk(v, out, carve),
7596        Stmt::If { then_stmt, else_stmt, .. } => {
7597            site_tails_one(then_stmt, out, carve);
7598            if let Some(e) = else_stmt {
7599                site_tails_one(e, out, carve);
7600            }
7601        }
7602        Stmt::While { body, .. }
7603        | Stmt::DoWhile { body, .. }
7604        | Stmt::ForEach { body, .. }
7605        | Stmt::Synchronized { body, .. }
7606        | Stmt::Labeled { body, .. } => site_tails_one(body, out, carve),
7607        Stmt::For { init, body, .. } => {
7608            site_tails_walk(init, out, carve);
7609            site_tails_one(body, out, carve);
7610        }
7611        Stmt::Switch { cases, default, .. } => {
7612            for c in cases.iter_mut() {
7613                site_tails_walk(&mut c.body, out, carve);
7614            }
7615            if let Some(d) = default {
7616                site_tails_one(d, out, carve);
7617            }
7618        }
7619        Stmt::Try { body, catches, finally } | Stmt::TryWithResources { body, catches, finally, .. } => {
7620            site_tails_one(body, out, carve);
7621            for c in catches.iter_mut() {
7622                site_tails_one(&mut c.body, out, carve);
7623            }
7624            if let Some(f) = finally {
7625                site_tails_one(f, out, carve);
7626            }
7627        }
7628        _ => {}
7629    }
7630}
7631
7632/// Can this statement list NOT complete normally? Conservative for
7633/// loops (false); If needs BOTH branches, Switch needs every case plus
7634/// a default. Used to decide whether the extracted helper needs the
7635/// trailing `return carrier;` (appending one after exhaustive branches
7636/// is javac's "unreachable statement" error).
7637fn always_returns(stmts: &[Stmt]) -> bool {
7638    let Some(last) = stmts
7639        .iter()
7640        .rev()
7641        .find(|s| !matches!(s, Stmt::Block(v) if v.is_empty()))
7642    else {
7643        return false;
7644    };
7645    match last {
7646        Stmt::Return(Some(_)) | Stmt::Throw(_) => true,
7647        Stmt::Block(v) => always_returns(v),
7648        Stmt::If { then_stmt, else_stmt, .. } => match else_stmt {
7649            Some(e) => always_returns_one(then_stmt) && always_returns_one(e),
7650            None => false,
7651        },
7652        Stmt::Labeled { body, .. } | Stmt::Synchronized { body, .. } => {
7653            always_returns_one(body)
7654        }
7655        Stmt::Switch { cases, default, .. } => {
7656            default.as_ref().is_some_and(|d| always_returns_one(d))
7657                && cases.iter().all(|c| always_returns(&c.body))
7658        }
7659        Stmt::Try { body, catches, finally }
7660        | Stmt::TryWithResources { body, catches, finally, .. } => {
7661            finally.as_ref().is_some_and(|f| always_returns_one(f))
7662                || (always_returns_one(body)
7663                    && catches.iter().all(|c| always_returns_one(&c.body)))
7664        }
7665        _ => false,
7666    }
7667}
7668
7669fn always_returns_one(st: &Stmt) -> bool {
7670    always_returns(std::slice::from_ref(st))
7671}
7672
7673fn inline_ctor_arg_defs(
7674    call: &mut Stmt,
7675    prefix: &[&Stmt],
7676    vt: &VarTable,
7677    consumed: &mut Vec<(u32, usize)>,
7678) -> bool {
7679    let Stmt::ExprStmt(cexpr) = call else {
7680        return false;
7681    };
7682    // Replace every read of `v` in `e` with its value-so-far; a read
7683    // BEFORE any def (val None) is unresolvable → false.
7684    fn subst(e: &mut Expr, v: u32, val: &Option<Expr>) -> bool {
7685        let mut ok = true;
7686        deep_rewrite(e, &mut |x| {
7687            if let Expr::Local { var, .. } = x {
7688                if *var == v {
7689                    match val {
7690                        Some(e2) => *x = e2.clone(),
7691                        None => ok = false,
7692                    }
7693                }
7694            }
7695        });
7696        ok
7697    }
7698    let mut seen: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
7699    for _ in 0..32 {
7700        let mut need: Option<u32> = None;
7701        visit_exprs(cexpr, &mut |x| {
7702            if need.is_none() {
7703                if let Expr::Local { var, .. } = x {
7704                    if (*var as usize) < vt.vars.len() && !vt.vars[*var as usize].is_param {
7705                        need = Some(*var);
7706                    }
7707                }
7708            }
7709        });
7710        let Some(v) = need else { break };
7711        if !seen.insert(v) {
7712            return false; // cyclic reuse (`str = .. + str`) — bail
7713        }
7714        // CHAIN walk: fold EVERY prefix def of v into the value the
7715        // delegation actually receives. The old strict single-occurrence
7716        // rule rejected the convenience-ctor reassignment chain
7717        // (`f = getInstance(ctx); f = transform(f, ..); this(ctx, f)` —
7718        // androidx CameraController, weibo this-not-first ×831): the
7719        // SOURCE inlined the computation into the delegation args and
7720        // R8 decomposed it into register writes. Reads of v are allowed
7721        // only INSIDE its own def chain; any other prefix statement
7722        // touching v means the value escapes or the object mutates
7723        // (`stringBuilder.append(..)` — inlining a fresh `new
7724        // StringBuilder()` would lose the message), so bail.
7725        let mut val: Option<Expr> = None;
7726        let mut ok = true;
7727        for (si, st) in prefix.iter().enumerate() {
7728            match st {
7729                // A bare declaration (init None — the structurer hoists
7730                // `T v;` to the top) carries no value: skip it. Failing
7731                // here rejected every chain under a hoisted decl (ci1/a).
7732                Stmt::LocalDef { var, init: None, .. } if *var == v => {}
7733                Stmt::LocalDef { var, init, .. } if *var == v => {
7734                    let Some(e) = init else { ok = false; break };
7735                    let mut e2 = (*e).clone();
7736                    if !subst(&mut e2, v, &val) {
7737                        ok = false;
7738                        break;
7739                    }
7740                    val = Some(e2);
7741                    consumed.push((v, si));
7742                }
7743                Stmt::ExprStmt(Expr::Assign { target, op, .. })
7744                    if matches!(&**target, Expr::Local { var: tv, .. } if *tv == v) =>
7745                {
7746                    if !matches!(op, AssignOp::Plain) {
7747                        ok = false; // compound write: not a plain def
7748                        break;
7749                    }
7750                    let Stmt::ExprStmt(Expr::Assign { value, .. }) = st else {
7751                        unreachable!()
7752                    };
7753                    let mut e2 = value.as_ref().clone();
7754                    if !subst(&mut e2, v, &val) {
7755                        ok = false;
7756                        break;
7757                    }
7758                    val = Some(e2);
7759                    consumed.push((v, si));
7760                }
7761                _ => {
7762                    let mut touched = false;
7763                    visit_all_exprs(st, &mut |x| {
7764                        if let Expr::Local { var, .. } = x {
7765                            if *var == v {
7766                                touched = true;
7767                            }
7768                        }
7769                    });
7770                    if touched {
7771                        ok = false;
7772                        break;
7773                    }
7774                }
7775            }
7776        }
7777        if !ok {
7778            return false;
7779        }
7780        let Some(de) = val else { return false };
7781        if contains_this_access(&de) {
7782            return false;
7783        }
7784        // Huge defs multiply per read site per round — bound them.
7785        let mut de_nodes = 0usize;
7786        visit_exprs(&de, &mut |_| de_nodes += 1);
7787        if de_nodes > 256 {
7788            return false;
7789        }
7790        deep_rewrite(cexpr, &mut |x| {
7791            if let Expr::Local { var, .. } = x {
7792                if *var == v {
7793                    *x = de.clone();
7794                }
7795            }
7796        });
7797        // Exponential blowup guard: a var read twice doubles its def
7798        // per round, compounding across the transitive chain — cap the
7799        // expression size (a 2^k node tree overflowed the worker stack
7800        // process-wide).
7801        let mut nodes = 0usize;
7802        visit_exprs(cexpr, &mut |_| nodes += 1);
7803        if nodes > 2_048 {
7804            return false;
7805        }
7806    }
7807    true
7808}
7809
7810/// Fold a prelude StringBuilder mutation chain into the delegation
7811/// argument as a fluent chain: `StringBuilder sb = new SB(x);
7812/// sb.append(a); sb.append(b); super(sb.toString())` →
7813/// `super(new SB(x).append(a).append(b).toString())`. The generic def
7814/// inliner rejects mutation calls — their effect is not expressible as
7815/// a value — but `append` RETURNS its receiver, so the chain
7816/// reconstructs exactly the same mutations in one expression (androidx
7817/// emoji2/flatbuffer UnpairedSurrogateException family, the shapes the
7818/// strict-occurrence rule was written to refuse). Aborts on any `this`
7819/// touch inside the chain (illegal before super) or any other read of
7820/// the SB local (the value must not escape the fold). Returns the
7821/// delegation's new index (statements before it were removed).
7822fn fold_sb_chain(stmts: &mut Vec<Stmt>, pos: usize) -> usize {
7823    // 1. the SB local: first `v = new StringBuilder(..)` in the prefix.
7824    let mut sbv: Option<u32> = None;
7825    let mut def_i: Option<usize> = None;
7826    let mut def_init: Option<Expr> = None;
7827    for (i, st) in stmts[..pos].iter().enumerate() {
7828        let cand: Option<(u32, &Expr)> = match st {
7829            Stmt::LocalDef {
7830                var,
7831                init: Some(e),
7832                ..
7833            } => Some((*var, e)),
7834            Stmt::ExprStmt(Expr::Assign {
7835                target,
7836                value,
7837                op: AssignOp::Plain,
7838                ..
7839            }) => match &**target {
7840                Expr::Local { var, .. } => Some((*var, value.as_ref())),
7841                _ => None,
7842            },
7843            _ => None,
7844        };
7845        if let Some((v, init)) = cand {
7846            if let Expr::New { cls, .. } = init {
7847                if cls.as_ref() == "java/lang/StringBuilder" {
7848                    sbv = Some(v);
7849                    def_i = Some(i);
7850                    def_init = Some(init.clone());
7851                    break;
7852                }
7853            }
7854        }
7855    }
7856    let (Some(sbv), Some(def_i), Some(init)) = (sbv, def_i, def_init) else {
7857        return pos;
7858    };
7859    if contains_this_access(&init) {
7860        return pos;
7861    }
7862    // 2. append statements on sbv, in order.
7863    let mut appends: Vec<usize> = Vec::new();
7864    for (i, st) in stmts[..pos].iter().enumerate() {
7865        if let Stmt::ExprStmt(Expr::Method {
7866            owner: Some(o),
7867            cls,
7868            name,
7869            ..
7870        }) = st
7871        {
7872            if matches!(&**o, Expr::Local { var, .. } if *var == sbv)
7873                && cls.as_ref() == "java/lang/StringBuilder"
7874                && &**name == "append"
7875            {
7876                appends.push(i);
7877            }
7878        }
7879    }
7880    // 3. no read of sbv after the delegation.
7881    let mut post_read = false;
7882    for st in stmts.iter().skip(pos + 1) {
7883        visit_all_exprs(st, &mut |e| {
7884            if let Expr::Local { var, .. } = e {
7885                if *var == sbv {
7886                    post_read = true;
7887                }
7888            }
7889        });
7890    }
7891    if post_read {
7892        return pos;
7893    }
7894    // 4. prefix reads of sbv only in the def and the append receivers
7895    //    (arguments are checked for `this`, receivers are the chain).
7896    for (i, st) in stmts[..pos].iter().enumerate() {
7897        if i == def_i || appends.contains(&i) {
7898            // append ARGUMENTS must not touch this; receiver is fine.
7899            if let Stmt::ExprStmt(Expr::Method { args, .. }) = st {
7900                if args.iter().any(contains_this_access) {
7901                    return pos;
7902                }
7903            }
7904            continue;
7905        }
7906        let mut leak = false;
7907        visit_all_exprs(st, &mut |e| {
7908            if let Expr::Local { var, .. } = e {
7909                if *var == sbv {
7910                    leak = true;
7911                }
7912            }
7913        });
7914        if leak {
7915            return pos;
7916        }
7917    }
7918    // 5. the call reads sbv exactly once, as `sbv.toString()`.
7919    let mut local_reads = 0usize;
7920    let mut tostring_reads = 0usize;
7921    visit_all_exprs(&stmts[pos], &mut |e| match e {
7922        Expr::Local { var, .. } if *var == sbv => local_reads += 1,
7923        Expr::Method {
7924            owner: Some(o),
7925            name,
7926            args,
7927            ..
7928        } if args.is_empty()
7929            && &**name == "toString"
7930            && matches!(&**o, Expr::Local { var, .. } if *var == sbv) =>
7931        {
7932            tostring_reads += 1;
7933        }
7934        _ => {}
7935    });
7936    if local_reads != 1 || tostring_reads != 1 {
7937        return pos;
7938    }
7939    // 6. build the fluent chain and swap it into the toString receiver.
7940    let mut chain = init;
7941    for ai in &appends {
7942        if let Stmt::ExprStmt(Expr::Method {
7943            owner: _,
7944            cls,
7945            name,
7946            desc,
7947            args,
7948            is_static,
7949            is_interface,
7950            is_special,
7951            is_super,
7952            is_dynamic,
7953            type_args,
7954            ..
7955        }) = &stmts[*ai]
7956        {
7957            chain = Expr::Method {
7958                owner: Some(Box::new(chain)),
7959                cls: cls.clone(),
7960                name: name.clone(),
7961                desc: desc.clone(),
7962                args: args.clone(),
7963                is_static: *is_static,
7964                is_interface: *is_interface,
7965                is_special: *is_special,
7966                is_super: *is_super,
7967                is_dynamic: *is_dynamic,
7968                type_args: type_args.clone(),
7969            };
7970        }
7971    }
7972    let mut replaced = false;
7973    walk_stmt_exprs(&mut stmts[pos], &mut |e| {
7974        deep_rewrite(e, &mut |x| {
7975            if let Expr::Method {
7976                owner: Some(o),
7977                name,
7978                args,
7979                ..
7980            } = x
7981            {
7982                if args.is_empty()
7983                    && &**name == "toString"
7984                    && matches!(&**o, Expr::Local { var, .. } if *var == sbv)
7985                {
7986                    **o = chain.clone();
7987                    replaced = true;
7988                }
7989            }
7990        });
7991    });
7992    if !replaced {
7993        return pos; // never remove without the swap — atomicity
7994    }
7995    // 7. drop the consumed statements (descending), adjust pos.
7996    let mut removed: Vec<usize> = appends.clone();
7997    removed.push(def_i);
7998    removed.sort_unstable();
7999    removed.dedup();
8000    for i in removed.iter().rev() {
8001        stmts.remove(*i);
8002    }
8003    pos - removed.len()
8004}
8005
8006pub fn fix_ctor_super_first(body: &mut Stmt, vt: &VarTable) {
8007    let Stmt::Block(stmts) = body else { return };
8008    if stmts.is_empty() || is_bare_ctor_call(stmts.first().unwrap()) {
8009        return;
8010    }
8011    // Top-level delegation call.
8012    if let Some(pos0) = stmts.iter().position(is_bare_ctor_call) {
8013        let pos = fold_sb_chain(stmts, pos0);
8014        let mut call = stmts[pos].clone();
8015        let prefix: Vec<&Stmt> = stmts[..pos].iter().collect();
8016        let mut consumed: Vec<(u32, usize)> = Vec::new();
8017        if !inline_ctor_arg_defs(&mut call, &prefix, vt, &mut consumed) {
8018            return;
8019        }
8020        stmts.remove(pos);
8021        // Drop the chain defs the inline consumed (all indices < pos) —
8022        // leaving them would DOUBLE-evaluate their (possibly impure)
8023        // inits. But ONLY for vars with no post-delegation use: dropping
8024        // such a var's DECLARATION makes ensure_declared re-add it at
8025        // the method top, i.e. BEFORE this() — a fresh "对this的调用必须
8026        // 是构造器中的第一个语句" (lark +587 the blind drop caused).
8027        let mut drop_idx: Vec<usize> = consumed
8028            .iter()
8029            .filter(|(v, _)| !var_occurs_in(&stmts[pos..], *v))
8030            .map(|(_, k)| *k)
8031            .collect();
8032        drop_idx.sort_unstable();
8033        drop_idx.dedup();
8034        for k in drop_idx.iter().rev() {
8035            stmts.remove(*k);
8036        }
8037        stmts.insert(0, call);
8038        return;
8039    }
8040    // The structurer can wrap the delegation in a bare nested block —
8041    // with the parameter null-checks AHEAD of it (weixin's Kotlin
8042    // intrinsics shape: `o.h(parcel, "source"); super();` at inner
8043    // positions 0/1, flattened to straight-line by cleanup AFTER this
8044    // pass, which is why the miss surfaced as 1150 weixin "对super的
8045    // 调用必须是构造器中的第一个语句"). Hoist the call from any
8046    // position whose predecessors are all straight-line statements; a
8047    // delegation behind control flow is the conditional-super family
8048    // (needs restructuring, not hoisting) and stays put.
8049    let mut found: Option<(usize, usize)> = None;
8050    for (i, st) in stmts.iter().enumerate() {
8051        if let Stmt::Block(inner) = st {
8052            if let Some(p) = inner.iter().position(is_bare_ctor_call) {
8053                if inner[..p]
8054                    .iter()
8055                    .all(|s| matches!(s, Stmt::LocalDef { .. } | Stmt::ExprStmt(_)))
8056                {
8057                    found = Some((i, p));
8058                    break;
8059                }
8060            }
8061        }
8062    }
8063    if let Some((i, p)) = found {
8064        let mut call = match &stmts[i] {
8065            Stmt::Block(inner) => inner[p].clone(),
8066            _ => unreachable!(),
8067        };
8068        let mut prefix: Vec<&Stmt> = stmts[..i].iter().collect();
8069        if let Stmt::Block(inner) = &stmts[i] {
8070            prefix.extend(inner[..p].iter());
8071        }
8072        let mut consumed: Vec<(u32, usize)> = Vec::new();
8073        if !inline_ctor_arg_defs(&mut call, &prefix, vt, &mut consumed) {
8074            return;
8075        }
8076        // Prefix index k maps to stmts[k] for k < i, inner[k - i] for
8077        // k >= i (all such k are < p). Suffix = the rest of the inner
8078        // block + the following top-level statements; a consumed def may
8079        // only drop when its var has no suffix use (declaration loss →
8080        // ensure_declared re-adds it BEFORE this()).
8081        // Compute the droppable set BEFORE mutating (borrow split).
8082        let mut ks: Vec<usize> = Vec::new();
8083        {
8084            let inner_tail: &[Stmt] = match &stmts[i] {
8085                Stmt::Block(inner) if p < inner.len() => &inner[p + 1..],
8086                _ => &[],
8087            };
8088            for (v, k) in &consumed {
8089                if !var_occurs_in(inner_tail, *v) && !var_occurs_in(&stmts[i + 1..], *v) {
8090                    ks.push(*k);
8091                }
8092            }
8093        }
8094        ks.sort_unstable();
8095        ks.dedup();
8096        if let Stmt::Block(inner) = &mut stmts[i] {
8097            inner.remove(p);
8098            for k in ks.iter().rev().filter(|k| **k >= i) {
8099                inner.remove(k - i);
8100            }
8101        }
8102        for k in ks.iter().rev().filter(|k| **k < i) {
8103            stmts.remove(*k);
8104        }
8105        stmts.insert(0, call);
8106    }
8107}
8108
8109/// A true constructor DELEGATION statement: `super(..)` (owner None) or
8110/// `this(..)` (owner This). An un-folded `new X; <init>` (lost
8111/// allocation — the ctor-fold-bug family) arrives as a `<init>` Method
8112/// with a NON-this owner and prints as `new X(..)`: it is not a
8113/// delegation and must neither satisfy nor anchor the super-first fixes
8114/// (v7.a$a2: a dead `new f(0,..)` init at position 0 made both hoists
8115/// believe the delegation was already first, leaving the real `super`
8116/// last — "对super的调用必须是构造器中的第一个语句").
8117fn is_delegation_expr(e: &Expr) -> bool {
8118    matches!(
8119        e,
8120        Expr::Method { name, is_special: true, owner, .. }
8121            if &**name == "<init>"
8122                && (owner.is_none() || matches!(owner.as_deref(), Some(Expr::This)))
8123    )
8124}
8125
8126pub(crate) fn is_bare_ctor_call(s: &Stmt) -> bool {
8127    matches!(s, Stmt::ExprStmt(e) if is_delegation_expr(e))
8128}
8129
8130/// Is the local ever ASSIGNED (write position: `x = ..`, `++x`)?
8131/// Distinct from a read count: a ctor's synthetic outer param is only
8132/// ever read; a param that gets written must stay declared.
8133pub(crate) fn local_is_written(body: &Stmt, var: u32) -> bool {
8134    let mut hit = false;
8135    let mut c = body.clone();
8136    walk_stmt_exprs(&mut c, &mut |e| {
8137        if !hit {
8138            deep_rewrite(e, &mut |x| {
8139                if hit {
8140                    return;
8141                }
8142                match x {
8143                    Expr::Assign { target, .. }
8144                        if matches!(&**target, Expr::Local { var: v, .. } if *v == var) =>
8145                    {
8146                        hit = true;
8147                    }
8148                    Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. }
8149                        if matches!(&**e, Expr::Local { var: v, .. } if *v == var) =>
8150                    {
8151                        hit = true;
8152                    }
8153                    _ => {}
8154                }
8155            });
8156        }
8157    });
8158    hit
8159}
8160
8161/// Source-form normalization for a non-static member-inner constructor.
8162/// The dex descriptor carries the synthetic outer instance as args[0]
8163/// (typed as the direct enclosing class); the emitter's qualified
8164/// `outer.new Inner(..)` / `this.new Inner(..)` sites pass it
8165/// implicitly and `super(..)` delegations drop it, so the signature
8166/// loses the param — and the body must stop referencing it: plain uses
8167/// become `Outer.this` (a `this.this$0 = p` assignment becomes
8168/// `this.this$0 = Outer.this`, exactly the runtime value), and
8169/// this()-delegations to `eligible` classes drop the leading param arg.
8170pub(crate) fn rewrite_inner_ctor_outer_param(
8171    body: &mut Stmt,
8172    param0: u32,
8173    outer_display: &str,
8174    outer_ty: &TypeRef,
8175    eligible: &[String],
8176) {
8177    // this()/super() delegations (and lost-alloc construction calls):
8178    // the leading arg IS the synthetic outer exactly when it is the
8179    // param itself.
8180    walk_stmt_exprs(body, &mut |e| {
8181        if let Expr::Method { name, cls, args, .. } = e {
8182            if &**name == "<init>"
8183                && eligible.iter().any(|c| c.as_str() == cls.as_ref())
8184                && args.first().is_some_and(|a| matches!(a, Expr::Local { var: v, .. } if *v == param0))
8185            {
8186                args.remove(0);
8187            }
8188        }
8189    });
8190    // Every remaining read of the param becomes `Outer.this`.
8191    let text = format!("{outer_display}.this");
8192    let ty = outer_ty.clone();
8193    walk_stmt_exprs(body, &mut |e| {
8194        deep_rewrite_reads(e, &mut |x| {
8195            if let Expr::Local { var: v, .. } = x {
8196                if *v == param0 {
8197                    *x = Expr::RawT(text.clone(), ty.clone());
8198                }
8199            }
8200        });
8201    });
8202}
8203
8204/// Ctors whose delegation is buried in control flow or behind arg
8205/// computations — the "对super的调用必须是构造器中的第一个语句" family
8206/// that plain hoisting cannot reach (weixin 264 / weibo 116 / reqable 71
8207/// / lark 62 after round 60): R8/Kotlin compute super-args conditionally
8208/// and dex legally invokes super mid-branch.
8209///
8210/// Shape B (branch merge): `prelude; if (c) {d1; super(A1); t1} else
8211/// {d2; super(A2); t2}; post` → one leading `super(..)` where each
8212/// differing arg becomes `c ? a1i : a2i`; branch remainders stay an
8213/// if/else AFTER the call; prelude statements move after it (nothing
8214/// may precede super in Java; bytecode pre-super work is local-only by
8215/// construction — dex forbids instance-field reads before the delegate).
8216///
8217/// Shape A (linear): `defs; super(A); rest` where `A` references the
8218/// def locals — plain hoisting (fix_ctor_super_first case 1) would move
8219/// the call above its argument definitions (forward refs). Inline the
8220/// referenced defs into the args first, drop the consumed defs, hoist.
8221/// Runs BEFORE fix_ctor_super_first and only fires when args reference
8222/// preceding locals, leaving the battle-tested plain hoist untouched
8223/// otherwise.
8224///
8225/// Inlining duplicates the def per use site, so a side-effecting def is
8226/// inlined only when its var's use count is provably preserved: a
8227/// single use in a merged arg (1 copy), or a single use in the
8228/// condition with ≤1 differing arg (the cond then appears in the `if`
8229/// plus one ternary — 2 copies of e.g. one Kotlin getter call, which
8230/// the original bytecode typically made twice anyway: `p.u() == null ?
8231/// .. : p.u().i()`). Pure defs inline freely. Every other shape aborts:
8232/// broken-but-faithful stays the status quo, wrong evaluation counts
8233/// would be worse.
8234/// Linear `def; delegation(args read def)` shapes. A plain hoist of the
8235/// delegation past its prefix would place the call ABOVE a definition
8236/// it reads — a forward reference (`super(context2, ..)` with `context2
8237/// = ..` below it). javac's attribution for the whole class then
8238/// collapses: the undefined identifier cascades into "non-static
8239/// super" and even bare `Object` resolution failures, poisoning every
8240/// later diagnostic in the file (weibo AppCompatTextView — the root of
8241/// a 4.1k-file Object cascade). The original source had the def's
8242/// expression INLINE in the delegation args; d8 computed it into a
8243/// register first. Inline the def into the args (the def is read
8244/// exactly once there — evaluation count preserved even for calls),
8245/// and when the register is REASSIGNED later (a fresh generation
8246/// sharing the slot), convert that first write into the declaration so
8247/// the var stays defined for its later readers. Any shape that cannot
8248/// be proven aborts untouched.
8249/// Count WRITE positions of `var` (assign / inc-dec targets) — the
8250/// shape-C validator needs exact counts, not just presence.
8251fn stmts_count_writes(stmts: &[Stmt], var: u32) -> usize {
8252    let mut n = 0usize;
8253    for s in stmts {
8254        let mut c = s.clone();
8255        walk_stmt_exprs(&mut c, &mut |e| {
8256            deep_rewrite(e, &mut |x| match x {
8257                Expr::Assign { target, .. }
8258                    if matches!(&**target, Expr::Local { var: v, .. } if *v == var) =>
8259                {
8260                    n += 1;
8261                }
8262                Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. }
8263                    if matches!(&**e, Expr::Local { var: v, .. } if *v == var) =>
8264                {
8265                    n += 1;
8266                }
8267                _ => {}
8268            });
8269        });
8270    }
8271    n
8272}
8273
8274/// Collect every local WRITTEN in `stmts` — the complement inside a
8275/// ctor prefix is the param set (the only locals a pre-delegation arg
8276/// expression may read).
8277fn prefix_written_vars(stmts: &[Stmt]) -> jdc_core::FxHashSet<u32> {
8278    let mut set = jdc_core::FxHashSet::default();
8279    for s in stmts {
8280        let mut c = s.clone();
8281        walk_stmt_exprs(&mut c, &mut |e| {
8282            deep_rewrite(e, &mut |x| match x {
8283                Expr::Assign { target, .. } => {
8284                    if let Expr::Local { var, .. } = &**target {
8285                        set.insert(*var);
8286                    }
8287                }
8288                Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => {
8289                    if let Expr::Local { var, .. } = &**e {
8290                        set.insert(*var);
8291                    }
8292                }
8293                _ => {}
8294            });
8295        });
8296        if let Stmt::LocalDef { var, .. } = s {
8297            set.insert(*var);
8298        }
8299    }
8300    set
8301}
8302
8303/// Is `e` safe to evaluate INSIDE a ctor-delegation argument (before
8304/// this/super exists)? Params/consts/pure ops only — no instance-field
8305/// reads, no `this`, no captures.
8306fn pre_this_safe(e: &Expr, prefix_vars: &jdc_core::FxHashSet<u32>) -> bool {
8307    match e {
8308        Expr::Local { var, .. } => !prefix_vars.contains(var),
8309        Expr::Const(_) => true,
8310        Expr::Cast { e, .. } => pre_this_safe(e, prefix_vars),
8311        Expr::Bin { l, r, .. } => {
8312            pre_this_safe(l, prefix_vars) && pre_this_safe(r, prefix_vars)
8313        }
8314        Expr::Cond { c, t, f } => {
8315            pre_this_safe(c, prefix_vars)
8316                && pre_this_safe(t, prefix_vars)
8317                && pre_this_safe(f, prefix_vars)
8318        }
8319        Expr::Field { owner, is_static, .. } => {
8320            *is_static
8321                && owner
8322                    .as_ref()
8323                    .is_none_or(|o| pre_this_safe(o, prefix_vars))
8324        }
8325        Expr::Method {
8326            owner,
8327            args,
8328            is_static,
8329            ..
8330        } => {
8331            args.iter().all(|a| pre_this_safe(a, prefix_vars))
8332                && match owner {
8333                    Some(o) => pre_this_safe(o, prefix_vars),
8334                    None => *is_static,
8335                }
8336        }
8337        // Allocation in a delegation arg is legal Java before this()
8338        // (the ctor call touches no instance state of THIS class) — the
8339        // missing arm sent every `new Date()`-style Kotlin default
8340        // through `_ => false`, rejecting the whole fold (lark
8341        // Request$AppToApp this-not-first family, ~650 lark lines).
8342        // An inner-class construction needing an outer `this` carries
8343        // Expr::This in its args, which still fails below.
8344        Expr::New { args, .. } => args.iter().all(|a| pre_this_safe(a, prefix_vars)),
8345        Expr::NewArray { dims, init, .. } => {
8346            dims.iter().all(|d| pre_this_safe(d, prefix_vars))
8347                && init
8348                    .as_ref()
8349                    .map(|l| l.iter().all(|x| pre_this_safe(x, prefix_vars)))
8350                    .unwrap_or(true)
8351        }
8352        // Pure unary/instanceof/array reads.
8353        Expr::Un { e, .. } | Expr::InstanceOf { e, .. } => pre_this_safe(e, prefix_vars),
8354        Expr::ArrayIndex { array, index } => {
8355            pre_this_safe(array, prefix_vars) && pre_this_safe(index, prefix_vars)
8356        }
8357        _ => false,
8358    }
8359}
8360
8361/// `s` is exactly one plain assignment to local `v` (optionally wrapped
8362/// in a singleton Block): the branch shape of a Kotlin default-arg
8363/// override.
8364fn single_assign_to_v(s: &Stmt, v: u32) -> Option<Expr> {
8365    match s {
8366        Stmt::ExprStmt(Expr::Assign {
8367            target,
8368            op: AssignOp::Plain,
8369            value,
8370        }) if matches!(&**target, Expr::Local { var: vv, .. } if *vv == v) => {
8371            Some((**value).clone())
8372        }
8373        Stmt::Block(xs) if xs.len() == 1 => single_assign_to_v(&xs[0], v),
8374        _ => None,
8375    }
8376}
8377
8378/// Kotlin default-argument BRIDGE ctors (shape C of the delegation
8379/// family): `<init>(params.., int mask, DefaultConstructorMarker)`
8380/// computes each defaulted arg into a local — `v = p; if ((mask&bit)
8381/// != 0) { v = DEFAULT; }` — then delegates `this(.., v, ..)`. Java
8382/// allows NOTHING before the delegation, so the computations must fold
8383/// INTO the args (`this(.., (mask&bit) != 0 ? DEFAULT : p, ..)`).
8384/// Unfolded, the hoist passes lift the delegation above the raw local
8385/// reads: "找不到符号 变量 str2/v4/creationExtras2" — 2.5k root files
8386/// across weixin/lark/weibo (weixin MvvmObserverOwner$..Observer, lark
8387/// SimpleArrayMap/LongSparseArray). Per-var provability: exactly one
8388/// top-level base write, an optional single conditional override whose
8389/// branches are lone assignments, no other prefix touch of the var,
8390/// nothing after the delegation touches it, and the folded expression
8391/// is pre-this-safe. Unprovable vars stay untouched (partial folds are
8392/// strictly better — the rest was broken before too).
8393/// Flatten nested plain Blocks among the top-level statements. The
8394/// structurer hands ctor bodies as region-nested blocks (`Block([def,
8395/// Block([assign, if]), Block([delegate, return])])` — the Kotlin
8396/// bridge shape), and every delegation-repair pass scans the TOP level
8397/// only. Straight-line nesting flattens without semantics; control
8398/// flow lives inside If/While/etc. arms, never as a direct member.
8399pub(crate) fn flatten_top_blocks(stmts: &mut Vec<Stmt>) {
8400    fn rec(s: Stmt, out: &mut Vec<Stmt>) {
8401        match s {
8402            Stmt::Block(inner) => {
8403                for x in inner {
8404                    rec(x, out);
8405                }
8406            }
8407            other => out.push(other),
8408        }
8409    }
8410    if !stmts.iter().any(|s| matches!(s, Stmt::Block(_))) {
8411        return;
8412    }
8413    let mut out: Vec<Stmt> = Vec::with_capacity(stmts.len());
8414    for s in std::mem::take(stmts) {
8415        rec(s, &mut out);
8416    }
8417    *stmts = out;
8418}
8419
8420fn fold_default_arg_bridge(stmts: &mut Vec<Stmt>) -> bool {
8421    flatten_top_blocks(stmts);
8422    let Some(pos) = stmts.iter().position(is_bare_ctor_call) else {
8423        return false;
8424    };
8425    if pos == 0 {
8426        return false;
8427    }
8428    let Stmt::ExprStmt(Expr::Method { args, .. }) = &stmts[pos] else {
8429        return false;
8430    };
8431    let mut arg_vars: Vec<u32> = Vec::new();
8432    let mut reads = 0usize;
8433    for a in args {
8434        let mut c = a.clone();
8435        deep_rewrite_reads(&mut c, &mut |x| {
8436            if let Expr::Local { var, .. } = x {
8437                reads += 1;
8438                if !arg_vars.contains(var) {
8439                    arg_vars.push(*var);
8440                }
8441            }
8442        });
8443    }
8444    if arg_vars.is_empty() || reads != arg_vars.len() {
8445        return false; // a var read twice would duplicate evaluation
8446    }
8447    // The bridge ENDS at the delegation: nothing after may touch the
8448    // arg vars (reads OR writes).
8449    for &v in &arg_vars {
8450        if stmts_read_var(&stmts[pos + 1..], v) != 0
8451            || stmts_count_writes(&stmts[pos + 1..], v) != 0
8452        {
8453            return false;
8454        }
8455    }
8456    let prefix_vars = prefix_written_vars(&stmts[..pos]);
8457    let mut repl: Vec<(u32, Expr)> = Vec::new();
8458    let mut drop: Vec<usize> = Vec::new();
8459    for &v in &arg_vars {
8460        // Exact prefix shape for v.
8461        let mut base: Option<(usize, Expr)> = None;
8462        let mut decl: Option<usize> = None;
8463        let mut condf: Option<(usize, Expr, Option<Expr>, Option<Expr>)> = None;
8464        let mut ok = true;
8465        for (i, s) in stmts[..pos].iter().enumerate() {
8466            match s {
8467                Stmt::LocalDef { var, init, .. } if *var == v => match init {
8468                    None => {
8469                        if decl.is_some() {
8470                            ok = false;
8471                            break;
8472                        }
8473                        decl = Some(i);
8474                    }
8475                    Some(e) => {
8476                        if base.is_some() {
8477                            ok = false;
8478                            break;
8479                        }
8480                        base = Some((i, e.clone()));
8481                    }
8482                },
8483                Stmt::ExprStmt(Expr::Assign {
8484                    target,
8485                    op: AssignOp::Plain,
8486                    value,
8487                }) if matches!(&**target, Expr::Local { var: vv, .. } if *vv == v) => {
8488                    if base.is_some() {
8489                        ok = false;
8490                        break;
8491                    }
8492                    base = Some((i, (**value).clone()));
8493                }
8494                Stmt::If {
8495                    cond,
8496                    then_stmt,
8497                    else_stmt,
8498                } => {
8499                    let t = single_assign_to_v(then_stmt, v);
8500                    let f = else_stmt.as_ref().and_then(|b| single_assign_to_v(b, v));
8501                    let then_touches = t.is_some()
8502                        || stmts_read_var(std::slice::from_ref(&**then_stmt), v) != 0
8503                        || stmts_count_writes(std::slice::from_ref(&**then_stmt), v) != 0;
8504                    let else_touches = else_stmt.as_ref().is_some_and(|b| {
8505                        f.is_some()
8506                            || stmts_read_var(std::slice::from_ref(b), v) != 0
8507                            || stmts_count_writes(std::slice::from_ref(b), v) != 0
8508                    });
8509                    if !then_touches && !else_touches {
8510                        continue; // unrelated if
8511                    }
8512                    // Both branches must be lone assignments (a branch
8513                    // with its own delegation/return is the conditional-
8514                    // super machinery's shape, not this one), the cond
8515                    // must not read v, and only one override per var.
8516                    if condf.is_some()
8517                        || base.is_none()
8518                        || (then_touches && t.is_none())
8519                        || (else_touches && f.is_none())
8520                    {
8521                        ok = false;
8522                        break;
8523                    }
8524                    let cond_stmt = Stmt::ExprStmt(cond.clone());
8525                    if stmts_read_var(std::slice::from_ref(&cond_stmt), v) != 0 {
8526                        ok = false;
8527                        break;
8528                    }
8529                    condf = Some((i, cond.clone(), t, f));
8530                }
8531                _ => {}
8532            }
8533        }
8534        if !ok {
8535            continue;
8536        }
8537        let Some((bi, be)) = base else { continue };
8538        // Exact write/read accounting: base (+then/else) writes, zero
8539        // prefix reads.
8540        let want_writes = 1
8541            + condf
8542                .as_ref()
8543                .map(|(_, _, t, f)| t.is_some() as usize + f.is_some() as usize)
8544                .unwrap_or(0);
8545        if stmts_count_writes(&stmts[..pos], v) != want_writes
8546            || stmts_read_var(&stmts[..pos], v) != 0
8547        {
8548            continue;
8549        }
8550        let rep = match condf {
8551            Some((ci, c, t, f)) => {
8552                drop.push(ci);
8553                match (t, f) {
8554                    (Some(t), Some(f)) => Expr::Cond {
8555                        c: Box::new(c),
8556                        t: Box::new(t),
8557                        f: Box::new(f),
8558                    },
8559                    (Some(t), None) => Expr::Cond {
8560                        c: Box::new(c),
8561                        t: Box::new(t),
8562                        f: Box::new(be.clone()),
8563                    },
8564                    (None, Some(f)) => Expr::Cond {
8565                        c: Box::new(c),
8566                        t: Box::new(be.clone()),
8567                        f: Box::new(f),
8568                    },
8569                    (None, None) => unreachable!("condf requires a branch assign"),
8570                }
8571            }
8572            None => be.clone(),
8573        };
8574        if !pre_this_safe(&rep, &prefix_vars) {
8575            continue;
8576        }
8577        drop.push(bi);
8578        if let Some(d) = decl {
8579            drop.push(d);
8580        }
8581        repl.push((v, rep));
8582    }
8583    if repl.is_empty() {
8584        return false;
8585    }
8586    if let Stmt::ExprStmt(Expr::Method { args, .. }) = &mut stmts[pos] {
8587        for a in args.iter_mut() {
8588            deep_rewrite_reads(a, &mut |x| {
8589                if let Expr::Local { var, .. } = x {
8590                    if let Some((_, e)) = repl.iter().find(|(vv, _)| vv == var) {
8591                        *x = e.clone();
8592                    }
8593                }
8594            });
8595        }
8596    }
8597    drop.sort_unstable();
8598    drop.dedup();
8599    for i in drop.into_iter().rev() {
8600        stmts.remove(i);
8601    }
8602    true
8603}
8604
8605/// Rewrite static member references on Kotlin multi-file facade PARTS
8606/// (`StringsKt__StringsKt.trim(..)`) to the public facade the parts are
8607/// compiled into (`StringsKt.trim(..)`): the parts are package-private,
8608/// so every cross-package call site is "在kotlin.text中不是公共的; 无法
8609/// 从外部程序包中对其进行访问" (lark 1.4k root files; weibo's 8.4k-line
8610/// Kt cluster). Static members resolve through the facade by
8611/// inheritance (the map is gated on the extends chain).
8612pub fn rewrite_kotlin_facades(body: &mut Stmt, pool: &crate::DexPool) {
8613    let map = pool.kotlin_facade_map();
8614    if map.is_empty() {
8615        return;
8616    }
8617    walk_stmt_exprs(body, &mut |e| {
8618        match e {
8619            Expr::Method {
8620                cls,
8621                name,
8622                desc,
8623                is_static,
8624                ..
8625            } if *is_static => {
8626                if let Some((base, cover)) = map.get(cls.as_ref()) {
8627                    let hit = match cover {
8628                        None => true,
8629                        Some(c) => c.0.contains(&(name.to_string(), desc.to_string())),
8630                    };
8631                    if hit {
8632                        *cls = std::sync::Arc::from(base.as_str());
8633                    }
8634                }
8635            }
8636            Expr::Field {
8637                cls,
8638                name,
8639                is_static,
8640                ..
8641            } if *is_static => {
8642                if let Some((base, cover)) = map.get(cls.as_ref()) {
8643                    let hit = match cover {
8644                        None => true,
8645                        Some(c) => c.1.contains(name.as_ref()),
8646                    };
8647                    if hit {
8648                        *cls = std::sync::Arc::from(base.as_str());
8649                    }
8650                }
8651            }
8652            _ => {}
8653        }
8654    });
8655}
8656
8657/// Kotlin default-arg bridge fold for ENUM ctors: the synthetic
8658/// `(String, int, .., int, DefaultConstructorMarker)` ctor computes
8659/// defaulted args then chains `this(..)` mid-body — Java requires the
8660/// delegation first (对this的调用必须是构造器中的第一个语句, lark's
8661/// UserCustomStatusExtraParams$* family, 752 lines). Same fold the
8662/// class-ctor path runs; the enum branch of the pipeline used to skip
8663/// it (only strip_enum_ctor_super ran there).
8664pub fn fold_enum_default_arg_bridge(body: &mut Stmt) {
8665    let Stmt::Block(stmts) = body else { return };
8666    fold_default_arg_bridge(stmts);
8667}
8668
8669pub fn fix_ctor_delegation_arg_defs(body: &mut Stmt) {
8670    let Stmt::Block(stmts) = body else { return };
8671    // Shape C first: the Kotlin default-arg bridge (computations +
8672    // conditional overrides folded into the delegation args).
8673    fold_default_arg_bridge(stmts);
8674    // Only the top-level linear shape; the first statement needs no
8675    // repair and control flow ahead of the delegation belongs to the
8676    // conditional-super machinery.
8677    let Some(pos) = stmts.iter().position(is_bare_ctor_call) else {
8678        return;
8679    };
8680    if pos == 0 {
8681        return;
8682    }
8683    // Vars read by the delegation args, and the total read count (a var
8684    // read twice would duplicate a call evaluation on inline).
8685    let mut arg_vars: Vec<u32> = Vec::new();
8686    let mut total_reads = 0usize;
8687    {
8688        let Stmt::ExprStmt(Expr::Method { args, .. }) = &stmts[pos] else {
8689            return;
8690        };
8691        for a in args {
8692            let mut c = a.clone();
8693            deep_rewrite_reads(&mut c, &mut |x| {
8694                if let Expr::Local { var: v, .. } = x {
8695                    total_reads += 1;
8696                    if !arg_vars.contains(v) {
8697                        arg_vars.push(*v);
8698                    }
8699                }
8700            });
8701        }
8702    }
8703    if arg_vars.is_empty() || total_reads != arg_vars.len() {
8704        return;
8705    }
8706    // Vars DEFINED anywhere in the prefix (for the init-referenced
8707    // check below — an inlined init may not itself read prefix defs).
8708    let prefix_defs: Vec<u32> = stmts[..pos]
8709        .iter()
8710        .filter_map(|s| match s {
8711            Stmt::LocalDef { var, init: Some(_), .. } => Some(*var),
8712            _ => None,
8713        })
8714        .collect();
8715    // Per referenced var: exactly one prefix def, no other prefix
8716    // reader between def and delegation, the def's init reads no
8717    // prefix-def var, and the first post-delegation use is a top-level
8718    // write (becomes the declaration) or nothing at all.
8719    let mut plan: Vec<(u32, usize, Option<usize>)> = Vec::new();
8720    for v in arg_vars {
8721        let defs: Vec<usize> = stmts[..pos]
8722            .iter()
8723            .enumerate()
8724            .filter(|(i, _)| {
8725                matches!(&stmts[*i], Stmt::LocalDef { var, init: Some(_), .. } if *var == v)
8726            })
8727            .map(|(i, _)| i)
8728            .collect();
8729        if defs.len() > 1 {
8730            return; // ambiguous: multiple prefix defs
8731        }
8732        let Some(&q) = defs.first() else {
8733            continue; // a param: nothing to inline for this var
8734        };
8735        let init = match &stmts[q] {
8736            Stmt::LocalDef { init: Some(e), .. } => e.clone(),
8737            _ => return,
8738        };
8739        if stmts_read_var(&stmts[q + 1..pos], v) != 0 {
8740            return; // another reader between the def and the delegation
8741        }
8742        // The inlined init may not read a prefix-def var either — the
8743        // delegation lands ABOVE those defs after the hoist.
8744        let mut init_vars: Vec<u32> = Vec::new();
8745        deep_rewrite_reads(&mut { init.clone() }, &mut |x| {
8746            if let Expr::Local { var: vv, .. } = x {
8747                if !init_vars.contains(vv) {
8748                    init_vars.push(*vv);
8749                }
8750            }
8751        });
8752        if init_vars.iter().any(|iv| prefix_defs.contains(iv)) {
8753            return;
8754        }
8755        match stmts[pos + 1..]
8756            .iter()
8757            .position(|s| {
8758                matches!(s, Stmt::ExprStmt(Expr::Assign { target, .. })
8759                    if matches!(&**target, Expr::Local { var: vv, .. } if *vv == v))
8760            }) {
8761            Some(w) => {
8762                // Reads before the re-declaration would be dangling.
8763                if stmts_read_var(&stmts[pos + 1..pos + 1 + w], v) != 0 {
8764                    return;
8765                }
8766                plan.push((v, q, Some(pos + 1 + w)));
8767            }
8768            None => {
8769                // No later write: the def must be dead after inline.
8770                if stmts_read_var(&stmts[pos + 1..], v) != 0 {
8771                    return;
8772                }
8773                plan.push((v, q, None));
8774            }
8775        }
8776    }
8777    if plan.is_empty() {
8778        return;
8779    }
8780    // Inline the def inits into the delegation args.
8781    let values: Vec<(u32, Expr)> = plan
8782        .iter()
8783        .map(|(_v, q, _)| match &stmts[*q] {
8784            Stmt::LocalDef { var, init: Some(e), .. } => (*var, e.clone()),
8785            _ => unreachable!("plan entries carry a LocalDef with init"),
8786        })
8787        .collect();
8788    if let Stmt::ExprStmt(Expr::Method { args, .. }) = &mut stmts[pos] {
8789        for a in args {
8790            deep_rewrite_reads(a, &mut |x| {
8791                if let Expr::Local { var: v, .. } = x {
8792                    if let Some((_, e)) = values.iter().find(|(vv, _)| vv == v) {
8793                        *x = e.clone();
8794                    }
8795                }
8796            });
8797        }
8798    }
8799    // Convert the first writes into declarations BEFORE removing defs
8800    // (removals shift indices); then remove the defs in descending
8801    // order.
8802    for (v, _, w) in &plan {
8803        if let Some(w) = w {
8804            if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = &mut stmts[*w] {
8805                if matches!(&**target, Expr::Local { var: vv, .. } if vv == v) {
8806                    let taken = std::mem::replace(&mut **value, Expr::Const(ConstVal::Null));
8807                    stmts[*w] = Stmt::LocalDef {
8808                        var: *v,
8809                        init: Some(taken),
8810                        is_final: false,
8811                        force_type: false,
8812                    };
8813                }
8814            }
8815        }
8816    }
8817    let mut order: Vec<usize> = plan.iter().map(|(_, q, _)| *q).collect();
8818    order.sort_unstable();
8819    order.reverse();
8820    for q in order {
8821        stmts.remove(q);
8822    }
8823}
8824
8825/// An INT-typed method returning a boolean value: the register held the
8826/// boolean test result in one generation and the `if (v) 1 else 0` int
8827/// in another; booleanize picks the boolean generation and the plain
8828/// `return v;` breaks ("boolean无法转换为int", weixin Handle
8829/// onPausableTransaction — the source was `if (v) 1 else 0`). Wrap the
8830/// return in the recovering conditional.
8831/// Mirror of fix_int_returns: a BOOLEAN method returning a numeric-
8832/// typed value (register reuse left an int generation) wraps it
8833/// `x != 0` — the dex branch encoding, and the only legal Java form
8834/// (kh5/a `return v116_g53;` int无法转换为boolean family).
8835pub fn fix_bool_returns(vt: &VarTable, body: &mut Stmt) {
8836    walk_mut_deep(body, &mut |st| {
8837        if let Stmt::Return(Some(e)) = st {
8838            let numeric = match &*e {
8839                Expr::Local { var, ty } => {
8840                    let t = if (*var as usize) < vt.vars.len() {
8841                        vt.var(*var).ty.erased()
8842                    } else {
8843                        ty.erased()
8844                    };
8845                    matches!(
8846                        t,
8847                        JavaType::Int
8848                            | JavaType::Long
8849                            | JavaType::Short
8850                            | JavaType::Byte
8851                            | JavaType::Char
8852                    )
8853                }
8854                other => matches!(
8855                    other.type_ref().erased(),
8856                    JavaType::Int | JavaType::Long | JavaType::Short | JavaType::Byte
8857                        | JavaType::Char
8858                ),
8859            };
8860            if numeric {
8861                let taken = e.clone();
8862                *e = Expr::Bin {
8863                    op: BinOp::Ne,
8864                    l: Box::new(taken),
8865                    r: Box::new(Expr::Const(ConstVal::Int(0))),
8866                    ty: Some(TypeRef::J(JavaType::Boolean)),
8867                };
8868            }
8869        }
8870    });
8871}
8872
8873pub fn fix_int_returns(vt: &VarTable, body: &mut Stmt) {
8874    walk_mut_deep(body, &mut |st| {
8875        if let Stmt::Return(Some(e)) = st {
8876            let is_bool = match e {
8877                Expr::Local { var, .. } => {
8878                    (*var as usize) < vt.vars.len()
8879                        && vt.vars[*var as usize].ty.erased() == JavaType::Boolean
8880                }
8881                Expr::Method { desc, .. } => desc.ret == JavaType::Boolean,
8882                _ => false,
8883            };
8884            if is_bool {
8885                *e = Expr::Cond {
8886                    c: Box::new(e.clone()),
8887                    t: Box::new(Expr::Const(ConstVal::Int(1))),
8888                    f: Box::new(Expr::Const(ConstVal::Int(0))),
8889                };
8890            }
8891        }
8892    });
8893}
8894
8895/// Generation splitting: when an assignment's value type is
8896/// INCOMPATIBLE with the var's declared type, mint a FRESH var for the
8897/// new generation instead of widening (widening was measured twice as
8898/// net-negative — every use-site cast gap re-triggers javac's
8899/// error-type contagion). Reads after the split point follow the
8900/// active generation; control-flow joins intersect; loops run a
8901/// 3-round fixpoint so the back-edge carries the tail generation to
8902/// the head reads (the loop-state-tuple shape, qf5/v0's
8903/// `u0 = (qf5.u0) get;` … `u0 = looper2;`).
8904///
8905/// Split condition (reference incompatibility or a primitive/value
8906/// mix — the instanceof-corruption shape): both specific references of
8907/// different classes, or prim→ref. NOT numeric-vs-numeric (promotion
8908/// handles those) and NOT Object targets (assignable — the narrowing
8909/// cast pass covers the value side).
8910pub fn split_generations(vt: &mut VarTable, body: &mut Stmt, pool: &DexPool) {
8911    let mut counter: u32 = 0;
8912    let mut gen: std::collections::HashMap<u32, u32> = std::collections::HashMap::default();
8913    split_walk_stmt(body, vt, &mut gen, &mut counter, pool);
8914}
8915
8916/// Should the assignment `target = value` split the target?
8917fn split_needed(vt: &VarTable, target: u32, value: &Expr, pool: &DexPool) -> bool {
8918    let n = vt.vars.len();
8919    if target as usize >= n {
8920        return false;
8921    }
8922    let declared = vt.vars[target as usize].ty.erased();
8923    let vt_is_ref = |t: &JavaType| matches!(t, JavaType::Object(_) | JavaType::Array(_));
8924    // Locals consult the VarTable: the embedded ty is a lift-time
8925    // snapshot, stale after booleanize converts a copy SOURCE (the
8926    // post-booleanize re-split below relies on seeing `v131` as the
8927    // boolean it became). Bitwise Or/And/Xor over boolean operands
8928    // carry the same staleness at the EXPRESSION level (dex lowered
8929    // Kotlin's non-short-circuit `or` to int `|` — the embedded Bin ty
8930    // stays Int after booleanize converts the operands): `int v18 =
8931    // delete | delete2` must split the target generation.
8932    fn side_bool(e: &Expr, vt: &VarTable) -> bool {
8933        match e {
8934            Expr::Local { var, .. } if (*var as usize) < vt.vars.len() => {
8935                matches!(vt.var(*var).ty.erased(), JavaType::Boolean)
8936            }
8937            Expr::Const(ConstVal::Int(0 | 1)) => true,
8938            Expr::Bin { op: BinOp::Or | BinOp::And | BinOp::Xor, l, r, .. } => {
8939                side_bool(l, vt) && side_bool(r, vt)
8940            }
8941            other => matches!(other.type_ref().erased(), JavaType::Boolean),
8942        }
8943    }
8944    let value_ty = match value {
8945        Expr::Local { var, ty } if (*var as usize) < vt.vars.len() => vt.var(*var).ty.erased(),
8946        Expr::Bin {
8947            op: BinOp::Or | BinOp::And | BinOp::Xor,
8948            l,
8949            r,
8950            ..
8951        } if side_bool(l, vt) && side_bool(r, vt) => JavaType::Boolean,
8952        other => other.type_ref().erased(),
8953    };
8954    if vt_is_ref(&declared) && vt_is_ref(&value_ty) {
8955        // Both references: split only when incompatible — different
8956        // classes where neither side is Object AND the value is not a
8957        // SUBTYPE of the declared type. An assignable store needs no
8958        // fresh generation; splitting one minted an orphan at an
8959        // if-join whose sibling arm kept the base (rename_gen_back's
8960        // primitive-only gate refused the undo, drop_dead_locals then
8961        // stripped the orphan to a bare allocation): pq0/g's bridge
8962        // `map2 = new LinkedHashMap()` against the HashMap-typed phi
8963        // rendered `new LinkedHashMap();` and the arm lost its store.
8964        let obj = JavaType::Object("java/lang/Object".into());
8965        if declared == value_ty || declared == obj || value_ty == obj {
8966            return false;
8967        }
8968        // A FRESH ALLOCATION whose class relation to the declared type
8969        // is UNKNOWABLE (a framework↔framework pair — LinkedHashMap
8970        // into a HashMap-typed phi): the dex stored the instance into
8971        // this register flow, so the store was verifier-legal; the
8972        // split cannot know better and orphaned the generation (the
8973        // if-join undo is primitive-only; the dropper stripped the
8974        // orphan to a bare `new LinkedHashMap();` and pq0/g's bridge
8975        // lost the else-arm store, killing the whole branched-super
8976        // merge). Pool-known pairs KEEP the split: the generation's
8977        // precise type is what downstream subtype sinks need (weibo
8978        // `v7 = new $reportWhenComplete$1(..)` into a ContinuationImpl
8979        // phi — exempting it made `v9($1) = v7` inconvertible, +50).
8980        if let (JavaType::Object(v), JavaType::Object(d)) = (&value_ty, &declared) {
8981            if matches!(value, Expr::New { .. })
8982                && (pool.get(v).is_none() || pool.get(d).is_none())
8983            {
8984                return false;
8985            }
8986        }
8987        true
8988    } else {
8989        // Primitive/reference mix: the value generation has a kind the
8990        // target can never accept — split (the declared type stays with
8991        // the old generation's readers).
8992        if vt_is_ref(&declared) != vt_is_ref(&value_ty) {
8993            return true;
8994        }
8995        // Boolean/numeric mix: `boolean` and `int` have NO cast between
8996        // them in Java, so a register reused across the kinds can only
8997        // be expressed as two generations (weixin ConstraintLayout
8998        // `boolean v60; int v56; v60 = v56;` — 9.5k bool↔int lines once
8999        // the obscuring suppression lifted). 0/1 constants are the
9000        // boolean encoding, not a numeric generation — never split on
9001        // those (booleanize owns them).
9002        let d_bool = matches!(declared, JavaType::Boolean);
9003        let v_bool = matches!(value_ty, JavaType::Boolean);
9004        let zero_one = matches!(value, Expr::Const(ConstVal::Int(0 | 1)));
9005        d_bool != v_bool && !zero_one
9006    }
9007}
9008
9009fn rewrite_gen_reads(e: &mut Expr, gen: &std::collections::HashMap<u32, u32>, vt: &VarTable) {
9010    deep_rewrite(e, &mut |x| {
9011        if let Expr::Local { var, ty } = x {
9012            if let Some(&to) = gen.get(var) {
9013                if to != *var {
9014                    *var = to;
9015                    *ty = vt.var(to).ty.clone();
9016                }
9017            }
9018        }
9019    });
9020}
9021
9022fn do_split(
9023    vt: &mut VarTable,
9024    target: &mut Expr,
9025    old: u32,
9026    value_ty: &JavaType,
9027    gen: &mut std::collections::HashMap<u32, u32>,
9028    counter: &mut u32,
9029) {
9030    *counter += 1;
9031    let info = &vt.vars[old as usize];
9032    let id = vt.vars.len() as u32;
9033    let name = format!("{}_g{}", info.name, counter);
9034    let slot = info.slot;
9035    // A split generation is a fresh LOCAL, not a parameter — even when
9036    // the parent register is one (the param identity stays on the
9037    // original var id; the generation only carries a later write). The
9038    // inherited flag made booleanize (and every other param-skip pass)
9039    // permanently ignore param-register generations: weixin v2/i's
9040    // `int v264_g160_g7 = !v200` with `!= 0` reads (bool→int assign
9041    // family, 2.8k lines).
9042    let is_param = false;
9043    vt.vars.push(jdc_core::var::VarInfo {
9044        id,
9045        slot,
9046        name,
9047        ty: TypeRef::J(value_ty.clone()),
9048        is_param,
9049        range_start: 0,
9050        range_end: u16::MAX,
9051        synthetic_name: true,
9052    });
9053    while vt.by_slot.len() <= slot as usize {
9054        vt.by_slot.push(Vec::new());
9055    }
9056    vt.by_slot[slot as usize].push((0, u16::MAX, id));
9057    gen.insert(old, id);
9058    if let Expr::Local { var, ty, .. } = target {
9059        *var = id;
9060        *ty = TypeRef::J(value_ty.clone());
9061    }
9062}
9063
9064/// Undo a DIVERGED generation split: rewrite every occurrence of the
9065/// freshly-minted generation `from` back to the pre-if var `to` within
9066/// `s` (both read positions and assignment targets, plus LocalDef var
9067/// fields). At a control-flow join only splits BOTH branches produced
9068/// survive (the phi merges on the shared var); a gen one branch minted
9069/// alone does not. But `do_split` already rewrote that branch's write
9070/// target IN PLACE, so without this undo the branch stores into a
9071/// write-only orphan gen that no confluence reader ever reads — the
9072/// value is silently lost AND the orphan renders undeclared (kt5.b
9073/// Kotlin default-arg bridge: `v18_g1 = p6x` in then vs `v18 = false`
9074/// in else, `this(.., v18, ..)` reads only else's value).
9075fn rename_gen_back(s: &mut Stmt, from: u32, to: u32, vt: &VarTable) {
9076    if from == to || (to as usize) >= vt.vars.len() {
9077        return;
9078    }
9079    let to_ty = vt.vars[to as usize].ty.clone();
9080    walk_mut_deep(s, &mut |st| {
9081        if let Stmt::LocalDef { var, .. } = st {
9082            if *var == from {
9083                *var = to;
9084            }
9085        }
9086    });
9087    rewrite_exprs(s, &mut |e| {
9088        deep_rewrite(e, &mut |x| {
9089            if let Expr::Local { var, ty } = x {
9090                if *var == from {
9091                    *var = to;
9092                    *ty = to_ty.clone();
9093                }
9094            }
9095        });
9096    });
9097}
9098
9099/// Remove an undone generation's `by_slot` segment entry so slot-based
9100/// resolution can never hand back the now-unreferenced gen id (its
9101/// VarInfo stays — ids index `vars` — but nothing may select it).
9102fn drop_gen_slot(vt: &mut VarTable, dead: u32) {
9103    if (dead as usize) >= vt.vars.len() {
9104        return;
9105    }
9106    let slot = vt.vars[dead as usize].slot as usize;
9107    if let Some(seg) = vt.by_slot.get_mut(slot) {
9108        seg.retain(|&(_, _, id)| id != dead);
9109    }
9110}
9111
9112/// Type gate for `rename_gen_back`: merging gen `v` back onto base `k`
9113/// is only safe when the base can carry the gen's value — identical
9114/// types, or BOTH primitives (the bool/int register-reuse family that
9115/// booleanize + the cast fixers reconcile; the Kotlin bridge case,
9116/// kt5.b `int v18` receiving `boolean p6x`). A reference-vs-primitive
9117/// or cross-class merge would forge `String k = <boolean>`-style
9118/// assignments that never compile (the lark/weibo/reqable regression
9119/// of the ungated first cut: +8.4k `不兼容的类型`).
9120fn undo_type_safe(vt: &VarTable, k: u32, v: u32) -> bool {
9121    if (k as usize) >= vt.vars.len() || (v as usize) >= vt.vars.len() {
9122        return false;
9123    }
9124    let kt = vt.vars[k as usize].ty.erased();
9125    let vt_ty = vt.vars[v as usize].ty.erased();
9126    if kt == vt_ty {
9127        return true;
9128    }
9129    let is_prim = |t: &JavaType| {
9130        matches!(
9131            t,
9132            JavaType::Int
9133                | JavaType::Long
9134                | JavaType::Short
9135                | JavaType::Byte
9136                | JavaType::Char
9137                | JavaType::Float
9138                | JavaType::Double
9139                | JavaType::Boolean
9140        )
9141    };
9142    is_prim(&kt) && is_prim(&vt_ty)
9143}
9144
9145fn split_walk_stmt(
9146    st: &mut Stmt,
9147    vt: &mut VarTable,
9148    gen: &mut std::collections::HashMap<u32, u32>,
9149    counter: &mut u32,
9150    pool: &DexPool,
9151) {
9152    match st {
9153        Stmt::Block(v) => {
9154            for x in v.iter_mut() {
9155                split_walk_stmt(x, vt, gen, counter, pool);
9156            }
9157        }
9158        Stmt::ExprStmt(e) => {
9159            rewrite_gen_reads(e, gen, vt);
9160            if let Expr::Assign {
9161                target,
9162                value,
9163                op: AssignOp::Plain,
9164                ..
9165            } = e
9166            {
9167                if let Expr::Local { var, .. } = &**target {
9168                    let old = *var;
9169                    if split_needed(vt, old, value, pool) {
9170                        let value_ty = value.type_ref().erased();
9171                        do_split(vt, target, old, &value_ty, gen, counter);
9172                    }
9173                }
9174            }
9175        }
9176        Stmt::LocalDef { var, init, .. } => {
9177            if let Some(e) = init {
9178                rewrite_gen_reads(e, gen, vt);
9179                if split_needed(vt, *var, e, pool) {
9180                    // The DEF mints its own generation: retarget the def
9181                    // to a fresh var (the bare decl of the ORIGINAL stays
9182                    // for its earlier readers).
9183                    let value_ty = e.type_ref().erased();
9184                    let old = *var;
9185                    let mut fake = Expr::Local {
9186                        var: old,
9187                        ty: vt.vars[old as usize].ty.clone(),
9188                    };
9189                    do_split(vt, &mut fake, old, &value_ty, gen, counter);
9190                    if let Expr::Local { var: nv, .. } = fake {
9191                        *var = nv;
9192                    }
9193                }
9194            }
9195        }
9196        Stmt::If {
9197            cond,
9198            then_stmt,
9199            else_stmt,
9200            ..
9201        } => {
9202            rewrite_gen_reads(cond, gen, vt);
9203            let then_floor = vt.vars.len() as u32;
9204            let mut g_then = gen.clone();
9205            split_walk_stmt(then_stmt, vt, &mut g_then, counter, pool);
9206            let else_floor = vt.vars.len() as u32;
9207            let mut g_else = gen.clone();
9208            if let Some(e) = else_stmt {
9209                split_walk_stmt(e, vt, &mut g_else, counter, pool);
9210            }
9211            // Undo splits that DIVERGED at this join (see
9212            // rename_gen_back), with TWO gates learned the hard way
9213            // (first cut regressed lark +8.4k):
9214            //  1. the SIBLING KEPT THE BASE — `g_else[k]` equals the
9215            //     entry mapping, i.e. only this branch minted a fresh
9216            //     gen for k. When both branches split, each orphan is
9217            //     declared by ensure_declared at its own value type and
9218            //     compiles; renaming them back onto the base would
9219            //     force cross-kind assignments (`int k = <boolean>`).
9220            //  2. the fresh gen is WRITE-ONLY in the branch — a gen
9221            //     read inside its branch renders fine on its own.
9222            // The gated shape is the Kotlin default-arg bridge phi
9223            // loss: then `v_g = p6x` / else `v = false` / confluence
9224            // reads v — the then value was silently dropped AND v_g
9225            // rendered undeclared (kt5.b). Undo restores the clean
9226            // diamond so fold_default_arg_bridge can fire.
9227            let mut undo_then: Vec<(u32, u32)> = g_then
9228                .iter()
9229                .filter(|(&k, &v)| {
9230                    v >= then_floor
9231                        && v < else_floor
9232                        && g_else.get(&k).copied() == gen.get(&k).copied()
9233                        && undo_type_safe(vt, k, v)
9234                        && stmts_read_var(std::slice::from_ref(&**then_stmt), v) == 0
9235                })
9236                .map(|(&k, &v)| (v, k))
9237                .collect();
9238            undo_then.sort_unstable_by_key(|&(v, _)| std::cmp::Reverse(v));
9239            for &(v, k) in &undo_then {
9240                rename_gen_back(then_stmt, v, k, vt);
9241                drop_gen_slot(vt, v);
9242            }
9243            if let Some(e) = else_stmt.as_deref_mut() {
9244                let mut undo_else: Vec<(u32, u32)> = g_else
9245                    .iter()
9246                    .filter(|(&k, &v)| {
9247                        v >= else_floor
9248                            && g_then.get(&k).copied() == gen.get(&k).copied()
9249                            && undo_type_safe(vt, k, v)
9250                            && stmts_read_var(std::slice::from_ref(&*e), v) == 0
9251                    })
9252                    .map(|(&k, &v)| (v, k))
9253                    .collect();
9254                undo_else.sort_unstable_by_key(|&(v, _)| std::cmp::Reverse(v));
9255                for &(v, k) in &undo_else {
9256                    rename_gen_back(e, v, k, vt);
9257                    drop_gen_slot(vt, v);
9258                }
9259            }
9260            // Join: keep only splits BOTH paths produced (an absent else
9261            // is a fall-through path that keeps the pre-if generation).
9262            gen.retain(|k, v| g_then.get(k) == Some(v) && g_else.get(k) == Some(v));
9263        }
9264        Stmt::While { cond, body, .. } | Stmt::DoWhile { cond, body, .. } => {
9265            // 3-round fixpoint: round N walks the cond+body with the
9266            // accumulated gen — the back-edge carries the tail
9267            // generation into the head reads of the next round.
9268            let entry = gen.clone();
9269            for _ in 0..3 {
9270                rewrite_gen_reads(cond, gen, vt);
9271                split_walk_stmt(body, vt, gen, counter, pool);
9272            }
9273            // After the loop the pre-loop generation is what a
9274            // zero-iteration execution left — conservative.
9275            *gen = entry;
9276        }
9277        Stmt::For { init, body, .. } => {
9278            for x in init.iter_mut() {
9279                split_walk_stmt(x, vt, gen, counter, pool);
9280            }
9281            let entry = gen.clone();
9282            for _ in 0..3 {
9283                split_walk_stmt(body, vt, gen, counter, pool);
9284            }
9285            *gen = entry;
9286        }
9287        Stmt::ForEach { body, .. } => {
9288            let entry = gen.clone();
9289            for _ in 0..3 {
9290                split_walk_stmt(body, vt, gen, counter, pool);
9291            }
9292            *gen = entry;
9293        }
9294        Stmt::Switch { cases, default, .. } => {
9295            let entry = gen.clone();
9296            for c in cases.iter_mut() {
9297                let mut g_case = entry.clone();
9298                for x in c.body.iter_mut() {
9299                    split_walk_stmt(x, vt, &mut g_case, counter, pool);
9300                }
9301                gen.retain(|k, v| g_case.get(k) == Some(v));
9302            }
9303            if let Some(d) = default {
9304                let mut g_def = entry.clone();
9305                split_walk_stmt(d, vt, &mut g_def, counter, pool);
9306                gen.retain(|k, v| g_def.get(k) == Some(v));
9307            }
9308        }
9309        Stmt::Try {
9310            body,
9311            catches,
9312            finally,
9313        } => {
9314            let entry = gen.clone();
9315            let mut g_try = entry.clone();
9316            split_walk_stmt(body, vt, &mut g_try, counter, pool);
9317            let mut g_all = g_try.clone();
9318            for c in catches.iter_mut() {
9319                // The handler runs with the PRE-try state (the exception
9320                // may fire anywhere in the try).
9321                let mut g_c = entry.clone();
9322                split_walk_stmt(&mut c.body, vt, &mut g_c, counter, pool);
9323                g_all.retain(|k, v| g_c.get(k) == Some(v));
9324            }
9325            *gen = g_all;
9326            if let Some(f) = finally {
9327                split_walk_stmt(f, vt, gen, counter, pool);
9328            }
9329        }
9330        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => {
9331            split_walk_stmt(body, vt, gen, counter, pool);
9332        }
9333        Stmt::Return(Some(e)) | Stmt::Throw(e) => {
9334            rewrite_gen_reads(e, gen, vt);
9335        }
9336        _ => {}
9337    }
9338}
9339
9340/// Framework exception super edge — the embedded API database
9341/// (fwdb) replaced the hand-written ~110-entry JDK/Android exception
9342/// supers table: real extends edges for EVERY framework class, so
9343/// multi-catch dedupe now covers chains the table missed (and cannot
9344/// carry a wrong entry — the table's failure mode was dropping a live
9345/// alternative).
9346fn fw_super(c: &str) -> Option<&'static str> {
9347    crate::fwdb::super_of(c)
9348}
9349
9350/// `sub <: sup` for exception-class names, combining the pool
9351/// hierarchy (which also matches framework ancestor NAMES on the way
9352/// up, so pool subclasses of framework exceptions resolve) with the
9353/// tabled framework chains for pairs where both sides are outside the
9354/// dex.
9355fn exc_subtype(sub: &str, sup: &str, pool: &DexPool) -> bool {
9356    if sub == sup {
9357        return true;
9358    }
9359    if pool.is_subtype(sub, sup) {
9360        return true;
9361    }
9362    let mut cur = sub;
9363    for _ in 0..24 {
9364        let Some(next) = fw_super(cur) else { break };
9365        if next == sup {
9366            return true;
9367        }
9368        cur = next;
9369    }
9370    false
9371}
9372
9373/// Drop multi-catch alternatives shadowed by another alternative in
9374/// the same handler (JLS 14.20.2 forbids subtype-related
9375/// alternatives; the dex verifier does not). Exact duplicates collapse
9376/// to the first occurrence. The caught set is unchanged, so this is
9377/// semantics-preserving by construction.
9378pub fn dedupe_multicatch(s: &mut Stmt, pool: &DexPool) {
9379    dedupe_multicatch_walk(s, pool);
9380}
9381
9382fn dedupe_multicatch_walk(s: &mut Stmt, pool: &DexPool) {
9383    match s {
9384        Stmt::Try {
9385            body,
9386            catches,
9387            finally,
9388        }
9389        | Stmt::TryWithResources {
9390            body,
9391            catches,
9392            finally,
9393            ..
9394        } => {
9395            dedupe_multicatch_walk(body, pool);
9396            for c in catches.iter_mut() {
9397                if c.exc.len() > 1 {
9398                    let mut keep: Vec<std::sync::Arc<str>> = Vec::with_capacity(c.exc.len());
9399                    for e in c.exc.iter() {
9400                        let shadowed = c.exc.iter().any(|o| {
9401                            o.as_ref() != e.as_ref() && exc_subtype(e, o, pool)
9402                        });
9403                        if !shadowed && !keep.iter().any(|k| k.as_ref() == e.as_ref()) {
9404                            keep.push(e.clone());
9405                        }
9406                    }
9407                    c.exc = keep;
9408                }
9409                dedupe_multicatch_walk(&mut c.body, pool);
9410            }
9411            if let Some(f) = finally {
9412                dedupe_multicatch_walk(f, pool);
9413            }
9414        }
9415        Stmt::Block(v) => {
9416            for x in v.iter_mut() {
9417                dedupe_multicatch_walk(x, pool);
9418            }
9419        }
9420        Stmt::If {
9421            then_stmt,
9422            else_stmt,
9423            ..
9424        } => {
9425            dedupe_multicatch_walk(then_stmt, pool);
9426            if let Some(e) = else_stmt {
9427                dedupe_multicatch_walk(e, pool);
9428            }
9429        }
9430        Stmt::While { body, .. }
9431        | Stmt::DoWhile { body, .. }
9432        | Stmt::For { body, .. }
9433        | Stmt::ForEach { body, .. }
9434        | Stmt::Labeled { body, .. }
9435        | Stmt::Synchronized { body, .. } => dedupe_multicatch_walk(body, pool),
9436        Stmt::Switch { cases, default, .. } => {
9437            for c in cases.iter_mut() {
9438                for x in c.body.iter_mut() {
9439                    dedupe_multicatch_walk(x, pool);
9440                }
9441            }
9442            if let Some(d) = default {
9443                dedupe_multicatch_walk(d, pool);
9444            }
9445        }
9446        _ => {}
9447    }
9448}
9449
9450/// Dangling `break L<id>`: a Goto whose paired Label/Labeled-wrap was
9451/// lost to structure degradation prints `break L<id>;` against an
9452/// undeclared label ("未定义的标签", weibo 371/lark 85/weixin 535).
9453/// Inside a loop the goto-to-loop-exit shape degrades to a plain
9454/// `break` — compilable, and the dominant original semantic. Outside
9455/// any loop the jump is irreducible flow (weixin protobuf state
9456/// machines: `goto` into a lost loop middle / dispatcher block — the
9457/// target exists but not at a position Java can express); it renders
9458/// as an honest `$DDC:` marker comment instead of a guaranteed
9459/// undefined-label error, and the block's tail after it is dropped
9460/// (the original control flow never reached those statements).
9461pub fn resolve_dangling_gotos(s: &mut Stmt) {
9462    let mut labels: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
9463    let probe = s.clone();
9464    walk_all(&probe, &mut |st| {
9465        if let Stmt::Label(id) = st {
9466            labels.insert(*id);
9467        }
9468    });
9469    resolve_gotos_walk(s, &labels, 0);
9470}
9471
9472fn resolve_gotos_walk(s: &mut Stmt, labels: &jdc_core::FxHashSet<u32>, loop_depth: u32) {
9473    let depth = match s {
9474        Stmt::While { .. } | Stmt::DoWhile { .. } | Stmt::For { .. } | Stmt::ForEach { .. } => {
9475            loop_depth + 1
9476        }
9477        _ => loop_depth,
9478    };
9479    match s {
9480        Stmt::Goto(id) => {
9481            if labels.contains(id) {
9482                return; // valid pair — keep
9483            }
9484            if depth > 0 {
9485                *s = Stmt::Break(None);
9486            } else {
9487                *s = Stmt::Comment(format!(
9488                    "$DDC: unresolved jump to block {} (irreducible control flow)",
9489                    id
9490                ));
9491            }
9492        }
9493        Stmt::Block(v) => {
9494            let mut i = 0usize;
9495            while i < v.len() {
9496                // A depth-0 dangling Goto becomes a marker comment and
9497                // the original flow never reached the siblings after
9498                // it — dropping them keeps the rendered reachability
9499                // faithful (they were dead in the dex too).
9500                let dangling_here =
9501                    depth == 0 && matches!(&v[i], Stmt::Goto(id) if !labels.contains(id));
9502                resolve_gotos_walk(&mut v[i], labels, depth);
9503                if dangling_here {
9504                    v.truncate(i + 1);
9505                    break;
9506                }
9507                i += 1;
9508            }
9509        }
9510        Stmt::If { then_stmt, else_stmt, .. } => {
9511            resolve_gotos_walk(then_stmt, labels, depth);
9512            if let Some(e) = else_stmt {
9513                resolve_gotos_walk(e, labels, depth);
9514            }
9515        }
9516        Stmt::While { body, .. }
9517        | Stmt::DoWhile { body, .. }
9518        | Stmt::For { body, .. }
9519        | Stmt::ForEach { body, .. }
9520        | Stmt::Labeled { body, .. }
9521        | Stmt::Synchronized { body, .. } => resolve_gotos_walk(body, labels, depth),
9522        Stmt::Try {
9523            body,
9524            catches,
9525            finally,
9526        } => {
9527            resolve_gotos_walk(body, labels, depth);
9528            for c in catches.iter_mut() {
9529                resolve_gotos_walk(&mut c.body, labels, depth);
9530            }
9531            if let Some(f) = finally {
9532                resolve_gotos_walk(f, labels, depth);
9533            }
9534        }
9535        Stmt::Switch { cases, default, .. } => {
9536            for c in cases.iter_mut() {
9537                for x in c.body.iter_mut() {
9538                    resolve_gotos_walk(x, labels, depth);
9539                }
9540            }
9541            if let Some(d) = default {
9542                resolve_gotos_walk(d, labels, depth);
9543            }
9544        }
9545        _ => {}
9546    }
9547}
9548
9549/// READS of `v` across the statements (assignment targets excluded).
9550fn stmts_read_var(stmts: &[Stmt], v: u32) -> usize {
9551    let mut n = 0usize;
9552    for s in stmts {
9553        let mut c = s.clone();
9554        walk_stmt_exprs(&mut c, &mut |e| {
9555            deep_rewrite_reads(e, &mut |x| {
9556                if let Expr::Local { var: vv, .. } = x {
9557                    if *vv == v {
9558                        n += 1;
9559                    }
9560                }
9561            });
9562        });
9563    }
9564    n
9565}
9566
9567pub fn fix_ctor_conditional_super(body: &mut Stmt) {
9568    // A single-statement body can arrive UNWRAPPED (bare `If` — the
9569    // gb6/e throw-guard family); the shape scans need a statement list.
9570    if !matches!(body, Stmt::Block(_)) {
9571        let inner = std::mem::replace(body, Stmt::Block(Vec::new()));
9572        *body = Stmt::Block(vec![inner]);
9573    }
9574    let Stmt::Block(stmts) = body else { return };
9575    // Unwrap single-statement Block wrappers: the structurer can nest
9576    // the whole body one level deeper, hiding the top-level If from the
9577    // shape scans (cleanup would flatten it — but runs AFTER this pass).
9578    while stmts.len() == 1 {
9579        let Stmt::Block(inner) = &stmts[0] else { break };
9580        *stmts = inner.clone();
9581    }
9582    if stmts.is_empty() {
9583        return;
9584    }
9585    if is_bare_ctor_call(stmts.first().unwrap()) {
9586        dedupe_ctor_delegations(body);
9587        return;
9588    }
9589    if try_linear_super_inline(stmts) {
9590        return;
9591    }
9592    try_merge_branched_super(stmts);
9593}
9594
9595/// Shape C: the delegation is already first — strip duplicate copies
9596/// left by R8 path-duplication (Kotlin default-arg bridge ctors: ft5/j
9597/// had `super()` leading PLUS another inside a branch — "对super的调用
9598/// 必须是构造器中的第一个语句" at the copy). Every path delegates
9599/// through the leading call, so structurally-identical (or empty-args,
9600/// same-kind) copies nested in blocks/ifs are removed, never moved.
9601/// Runs both ahead of the shape-A/B attempt and AFTER the plain hoist
9602/// (fix_ctor_super_first) — the hoist is what puts the leading
9603/// delegation in place for path-duplicated ctors.
9604/// A ctor that ALREADY delegates in its first statement can hold no
9605/// other `this(..)`/`super(..)` — Baidu Titan hotpatch instrumentation
9606/// injects a conditional re-delegation inside the `if ($ic != null)`
9607/// guard of every ctor (baidusearch ctor-not-first ×61,922 across
9608/// 21,813 files: `super(context); if ($ic != null) { ..; if
9609/// ((flag&1)!=0) { super((Context) callArgs[0]); ..; return; } }`).
9610/// The guard is dead code on the unpatched runtime path (and the
9611/// decompiled output has no patch runtime at all); dropping the
9612/// secondary delegation keeps the real semantics and restores the
9613/// Java first-statement rule. Runs after dedupe_ctor_delegations
9614/// (identical copies) and before extract_branched_delegation_helper
9615/// — gated on a LEADING delegation, so branched-delegation ctors
9616/// (no leading one) still get the helper treatment. Nested class
9617/// declarations are separate ctor scopes and are not entered.
9618pub fn strip_secondary_delegations(body: &mut Stmt) {
9619    let Stmt::Block(stmts) = body else { return };
9620    if !stmts.first().is_some_and(is_bare_ctor_call) {
9621        return;
9622    }
9623    fn strip_in(s: &mut Stmt) {
9624        if is_bare_ctor_call(s) {
9625            *s = Stmt::Block(Vec::new());
9626            return;
9627        }
9628        match s {
9629            Stmt::Block(v) => v.iter_mut().for_each(strip_in),
9630            Stmt::If {
9631                then_stmt,
9632                else_stmt,
9633                ..
9634            } => {
9635                strip_in(then_stmt);
9636                if let Some(e) = else_stmt {
9637                    strip_in(e);
9638                }
9639            }
9640            Stmt::While { body, .. }
9641            | Stmt::DoWhile { body, .. }
9642            | Stmt::ForEach { body, .. }
9643            | Stmt::Synchronized { body, .. }
9644            | Stmt::Labeled { body, .. } => strip_in(body),
9645            Stmt::For { init, body, .. } => {
9646                init.iter_mut().for_each(strip_in);
9647                strip_in(body);
9648            }
9649            Stmt::Switch { cases, default, .. } => {
9650                for c in cases.iter_mut() {
9651                    c.body.iter_mut().for_each(strip_in);
9652                }
9653                if let Some(d) = default {
9654                    strip_in(d);
9655                }
9656            }
9657            Stmt::Try {
9658                body,
9659                catches,
9660                finally,
9661            }
9662            | Stmt::TryWithResources {
9663                body,
9664                catches,
9665                finally,
9666                ..
9667            } => {
9668                strip_in(body);
9669                for c in catches.iter_mut() {
9670                    strip_in(&mut c.body);
9671                }
9672                if let Some(f) = finally {
9673                    strip_in(f);
9674                }
9675            }
9676            _ => {}
9677        }
9678    }
9679    for st in stmts.iter_mut().skip(1) {
9680        strip_in(st);
9681    }
9682}
9683
9684pub fn dedupe_ctor_delegations(body: &mut Stmt) {
9685    let Stmt::Block(stmts) = body else { return };
9686    let Some(d0) = stmts.first().and_then(|s| match s {
9687        Stmt::ExprStmt(e) if is_delegation_expr(e) => Some(e.clone()),
9688        _ => None,
9689    }) else {
9690        return;
9691    };
9692    fn rec(s: &mut Stmt, d0: &Expr) {
9693        match s {
9694            Stmt::Block(v) => {
9695                v.retain(|x| {
9696                    !matches!(x, Stmt::ExprStmt(e) if is_delegation_expr(e) && same_delegation(e, d0))
9697                });
9698                for x in v.iter_mut() {
9699                    rec(x, d0);
9700                }
9701            }
9702            Stmt::If { then_stmt, else_stmt, .. } => {
9703                rec(then_stmt, d0);
9704                if let Some(e) = else_stmt {
9705                    rec(e, d0);
9706                }
9707            }
9708            _ => {}
9709        }
9710    }
9711    // Top-level copies first (rec only strips copies NESTED in
9712    // blocks/ifs — a duplicate `super()` sitting directly in the body
9713    // list fell through its `_` arm: weixin ft5/j's second super()).
9714    {
9715        let d0r = &d0;
9716        let mut idx = 0usize;
9717        stmts.retain(|s| {
9718            idx += 1;
9719            idx == 1
9720                || !(matches!(s, Stmt::ExprStmt(e)
9721                    if is_delegation_expr(e) && same_delegation(e, d0r)))
9722        });
9723    }
9724    for s in stmts.iter_mut().skip(1) {
9725        rec(s, &d0);
9726    }
9727}
9728
9729/// Structural delegation identity for dedup: same kind/target and equal
9730/// args — or both arg-less (`super()` copies across duplicated paths
9731/// render with locally-renamed args yet delegate identically).
9732fn same_delegation(a: &Expr, b: &Expr) -> bool {
9733    match (a, b) {
9734        (
9735            Expr::Method { name: n1, cls: c1, is_super: s1, args: a1, .. },
9736            Expr::Method { name: n2, cls: c2, is_super: s2, args: a2, .. },
9737        ) => {
9738            &**n1 == "<init>"
9739                && &**n2 == "<init>"
9740                && c1 == c2
9741                && s1 == s2
9742                && (a1 == a2 || (a1.is_empty() && a2.is_empty()))
9743        }
9744        _ => false,
9745    }
9746}
9747
9748/// The `Method` expr of a bare ctor-delegation statement.
9749fn ctor_call_expr(s: &Stmt) -> Option<&Expr> {
9750    match s {
9751        Stmt::ExprStmt(e) if is_delegation_expr(e) => Some(e),
9752        _ => None,
9753    }
9754}
9755
9756/// Same delegation kind ignoring args (super vs this, same target).
9757fn same_ctor_kind(a: &Expr, b: &Expr) -> bool {
9758    match (a, b) {
9759        (
9760            Expr::Method { cls: c1, is_super: s1, owner: o1, .. },
9761            Expr::Method { cls: c2, is_super: s2, owner: o2, .. },
9762        ) => c1 == c2 && s1 == s2 && o1 == o2,
9763        _ => false,
9764    }
9765}
9766
9767/// Statements of a branch body, recursively flattening pure Blocks —
9768/// the fix runs BEFORE cleanup, so structurer-emitted nesting is still
9769/// in place. Any non-Block statement (If/Try/...) stays an item and
9770/// fails the def/call classification, which is the intended abort.
9771fn flat_list(s: &Stmt) -> Vec<&Stmt> {
9772    fn rec<'a>(s: &'a Stmt, out: &mut Vec<&'a Stmt>) {
9773        match s {
9774            Stmt::Block(v) => {
9775                for x in v {
9776                    rec(x, out);
9777                }
9778            }
9779            other => out.push(other),
9780        }
9781    }
9782    let mut out = Vec::new();
9783    rec(s, &mut out);
9784    out
9785}
9786
9787/// Blank the OWNER of un-folded `<init>` calls (lost allocation): the
9788/// printer renders them as `new X(..)` and never reads the owner, so its
9789/// register reference is not a real use (v7.a$a2: the dead
9790/// `new f(0,..)`'s owner was v1's register, inflating v1's use count
9791/// and blocking the inline-hoist).
9792fn strip_lost_alloc_owners(e: &mut Expr) {
9793    deep_rewrite(e, &mut |x| {
9794        if let Expr::Method { name, is_special: true, owner: Some(o), .. } = x {
9795            if &**name == "<init>" && !matches!(**o, Expr::This) {
9796                **o = Expr::Const(jdc_core::ir::expr::ConstVal::Int(0));
9797            }
9798        }
9799    });
9800}
9801
9802/// Per-var `Local` occurrence counts over an expression (single pass;
9803/// lost-alloc `<init>` owners excluded — see strip_lost_alloc_owners).
9804fn count_locals_expr(e: &Expr) -> std::collections::HashMap<u32, usize> {
9805    let mut m = std::collections::HashMap::new();
9806    let mut probe = e.clone();
9807    strip_lost_alloc_owners(&mut probe);
9808    deep_rewrite(&mut probe, &mut |x| {
9809        if let Expr::Local { var: v, .. } = x {
9810            *m.entry(*v).or_insert(0) += 1;
9811        }
9812    });
9813    m
9814}
9815
9816pub(crate) fn count_locals_stmts(ss: &[Stmt]) -> std::collections::HashMap<u32, usize> {
9817    let mut m = std::collections::HashMap::new();
9818    for s in ss {
9819        let mut c = s.clone();
9820        walk_stmt_exprs(&mut c, &mut |e| {
9821            strip_lost_alloc_owners(e);
9822            deep_rewrite(e, &mut |x| {
9823                if let Expr::Local { var: v, .. } = x {
9824                    *m.entry(*v).or_insert(0) += 1;
9825                }
9826            });
9827        });
9828    }
9829    m
9830}
9831
9832fn cnt(m: &std::collections::HashMap<u32, usize>, v: u32) -> usize {
9833    m.get(&v).copied().unwrap_or(0)
9834}
9835
9836/// Substitute map-referenced locals, recursively expanding def chains
9837/// (depth-capped). Returns None when a referenced var has no def or the
9838/// chain is too deep.
9839fn inline_locals(
9840    e: &Expr,
9841    map: &std::collections::HashMap<u32, Expr>,
9842    depth: u32,
9843) -> Option<Expr> {
9844    if depth > 4 {
9845        return None;
9846    }
9847    let mut out = e.clone();
9848    let mut fail = false;
9849    deep_rewrite(&mut out, &mut |x| {
9850        if let Expr::Local { var, .. } = x {
9851            if let Some(def) = map.get(var) {
9852                match inline_locals(def, map, depth + 1) {
9853                    Some(r) => *x = r,
9854                    None => fail = true,
9855                }
9856            }
9857        }
9858    });
9859    if fail {
9860        None
9861    } else {
9862        Some(out)
9863    }
9864}
9865
9866/// A leading statement that defines a local: (var, init expr).
9867fn def_of(s: &Stmt) -> Option<(u32, &Expr)> {
9868    match s {
9869        Stmt::LocalDef { var, init: Some(e), .. } => Some((*var, e)),
9870        Stmt::ExprStmt(Expr::Assign {
9871            target,
9872            value,
9873            op: jdc_core::ir::expr::AssignOp::Plain,
9874            ..
9875        }) => match &**target {
9876            Expr::Local { var, .. } => Some((*var, value)),
9877            _ => None,
9878        },
9879        _ => None,
9880    }
9881}
9882
9883/// Shape A: straight-line prefix, delegation references prefix locals.
9884fn try_linear_super_inline(stmts: &mut Vec<Stmt>) -> bool {
9885    let Some(pos) = stmts.iter().position(is_bare_ctor_call) else {
9886        return false;
9887    };
9888    if pos == 0 {
9889        return false; // already first — nothing to do
9890    }
9891    // The prefix must be straight-line.
9892    if !stmts[..pos]
9893        .iter()
9894        .all(|s| matches!(s, Stmt::LocalDef { .. } | Stmt::ExprStmt(_)))
9895    {
9896        return false;
9897    }
9898    let Some(call) = ctor_call_expr(&stmts[pos]) else {
9899        return false;
9900    };
9901    let Expr::Method { args, .. } = call else {
9902        return false;
9903    };
9904    // Defs reachable from the args (last definition wins, bytecode order).
9905    let mut map: std::collections::HashMap<u32, Expr> =
9906        std::collections::HashMap::new();
9907    let mut def_idx: std::collections::HashMap<u32, usize> =
9908        std::collections::HashMap::new();
9909    for (i, s) in stmts[..pos].iter().enumerate() {
9910        if let Some((v, e)) = def_of(s) {
9911            map.insert(v, e.clone());
9912            def_idx.insert(v, i);
9913        }
9914    }
9915    // Which arg locals need inlining?
9916    let mut needed: Vec<u32> = Vec::new();
9917    for a in args {
9918        let mut probe = a.clone();
9919        strip_lost_alloc_owners(&mut probe);
9920        deep_rewrite(&mut probe, &mut |x| {
9921            if let Expr::Local { var, .. } = x {
9922                if map.contains_key(var) && !needed.contains(var) {
9923                    needed.push(*var);
9924                }
9925            }
9926        });
9927    }
9928    if needed.is_empty() {
9929        return false; // plain hoist handles it
9930    }
9931    // Guard: side-effecting defs inline only at a provably preserved
9932    // call count (exactly one use, the arg site).
9933    let mut consumed: Vec<usize> = Vec::new();
9934    let use_counts = count_locals_stmts(stmts);
9935    for &v in &needed {
9936        let def = &map[&v];
9937        let uses = cnt(&use_counts, v);
9938        if jdc_core::ir::build::has_side_effects(def) && uses != 1 {
9939            return false;
9940        }
9941        if uses == 1 {
9942            consumed.push(def_idx[&v]);
9943        }
9944    }
9945    // Build the inlined args.
9946    let mut new_args: Vec<Expr> = Vec::with_capacity(args.len());
9947    for a in args {
9948        match inline_locals(a, &map, 0) {
9949            Some(r) => new_args.push(r),
9950            None => return false,
9951        }
9952    }
9953    // Rebuild: super first (with inlined args), the unconsumed prefix
9954    // statements keep their order after it, then the original rest.
9955    let mut call_stmt = stmts.remove(pos);
9956    if let Stmt::ExprStmt(Expr::Method { args: slot, .. }) = &mut call_stmt {
9957        *slot = new_args;
9958    }
9959    let mut rest: Vec<Stmt> = Vec::with_capacity(stmts.len() + 1);
9960    rest.push(call_stmt);
9961    for (i, s) in stmts.drain(..).enumerate() {
9962        // Indices shift after `remove(pos)`: consumed holds pre-removal
9963        // indices; pos itself is already gone from `stmts`.
9964        let orig = if i >= pos { i + 1 } else { i };
9965        if !consumed.contains(&orig) {
9966            rest.push(s);
9967        }
9968    }
9969    *stmts = rest;
9970    true
9971}
9972
9973/// Shape B: if/else branches each ending in the same-kind delegation.
9974fn try_merge_branched_super(stmts: &mut Vec<Stmt>) -> bool {
9975    // Candidate If positions — the prelude before the chosen If must be
9976    // straight-line; try each If until one merges (nested/multi-if
9977    // ctors exist but the first matching one is the delegation site).
9978    let if_positions: Vec<usize> = stmts
9979        .iter()
9980        .enumerate()
9981        .filter(|(_, s)| matches!(s, Stmt::If { .. }))
9982        .map(|(i, _)| i)
9983        .collect();
9984    for if_pos in if_positions {
9985        // Cheap allocation-free gate: at least one branch must hold a
9986        // delegation (most ctor ifs are field null-checks and must not
9987        // pay the merge machinery). The OTHER side may be delegation-free
9988        // (throw-guard shape D) or absent (then-only shape E — the
9989        // rendered if/else form often only exists after later passes).
9990        let gate = match &stmts[if_pos] {
9991            Stmt::If { then_stmt, else_stmt, .. } => {
9992                contains_delegation(then_stmt)
9993                    || else_stmt.as_ref().is_some_and(|e| contains_delegation(e))
9994            }
9995            _ => false,
9996        };
9997        if gate && merge_at(stmts, if_pos) {
9998            return true;
9999        }
10000    }
10001    false
10002}
10003
10004/// Does this subtree contain a ctor delegation? (module-level: shared by
10005/// the merge gate and split_branch)
10006fn contains_delegation(s: &Stmt) -> bool {
10007    match s {
10008        Stmt::ExprStmt(e) => is_delegation_expr(e),
10009        Stmt::Block(v) => v.iter().any(contains_delegation),
10010        Stmt::If { then_stmt, else_stmt, .. } => {
10011            contains_delegation(then_stmt)
10012                || else_stmt.as_ref().is_some_and(|e| contains_delegation(e))
10013        }
10014        _ => false,
10015    }
10016}
10017
10018
10019
10020/// One merge attempt at `stmts[if_pos]`. See fix_ctor_conditional_super
10021/// for the doctrine. Extensions over the pure two-call merge:
10022/// - branch pre-call SIDE-EFFECT statements (Kotlin `o.h(param,..)`
10023///   null-checks — the zz5/j0 family) are allowed; they move into the
10024///   branch remainder (after super — the same reordering doctrine the
10025///   plain hoist already applies; Java has no pre-super statement form).
10026/// - a branch WITHOUT a delegation (throw-guard: `if (x==null) throw
10027///   .. else { super(..); .. }` — the gb6/e family) merges as pure
10028///   remainder; exactly one branch carries the call then, and no
10029///   ternary args arise (the guard throws before any path needs them).
10030/// - prelude single-assignment if/else (`if (c) {v=e1} else {v=e2}`)
10031///   folds into a Cond def for inlining (the zz5/j0 v15/v16 preludes).
10032fn merge_at(stmts: &mut Vec<Stmt>, if_pos: usize) -> bool {
10033    /// Branch classification: Call(at, defs, pre-call extra idxs) when a
10034    /// delegation sits behind only defs/side-effect statements; Clean
10035    /// when the branch has no delegation at all (pure remainder); Dirty
10036    /// otherwise (nested/conditional delegation — out of scope).
10037    enum Br {
10038        /// (call index, pre-call defs, def statement indices)
10039        Call(usize, Vec<(u32, Expr)>, Vec<usize>),
10040        Clean,
10041        Dirty,
10042    }
10043    fn split_branch(list: &[&Stmt]) -> Br {
10044        let call_pos = list.iter().position(|s| ctor_call_expr(s).is_some());
10045        let Some(c) = call_pos else {
10046            // No delegation at this level: a pure remainder branch
10047            // (throw-guard) unless one hides in nested control flow.
10048            return if list.iter().any(|s| contains_delegation(s)) {
10049                Br::Dirty
10050            } else {
10051                Br::Clean
10052            };
10053        };
10054        if list.iter().skip(c + 1).any(|s| ctor_call_expr(s).is_some()) {
10055            return Br::Dirty; // two delegations in one branch
10056        }
10057        let mut defs: Vec<(u32, Expr)> = Vec::new();
10058        let mut def_idxs: Vec<usize> = Vec::new();
10059        for (i, s) in list.iter().enumerate().take(c) {
10060            match def_of(s) {
10061                Some((v, e)) => {
10062                    defs.push((v, e.clone()));
10063                    def_idxs.push(i);
10064                }
10065                None => match s {
10066                    // Side-effect statements and bare decls ahead of the
10067                    // delegation ride along into the remainder.
10068                    Stmt::ExprStmt(_) | Stmt::LocalDef { .. } => {}
10069                    _ => return Br::Dirty, // control flow pre-call
10070                },
10071            }
10072        }
10073        Br::Call(c, defs, def_idxs)
10074    }
10075    /// Prelude single-assignment if/else → `v = c ? e1 : e2` def.
10076    fn cond_def_of(s: &Stmt, scope: &[Stmt]) -> Option<(u32, Expr)> {
10077        let Stmt::If { cond, then_stmt, else_stmt: Some(e), .. } = s else {
10078            return None;
10079        };
10080        fn one<'x>(b: &'x Stmt, scope: &[Stmt]) -> Option<(u32, &'x Expr)> {
10081            let l = flat_list(b);
10082            if l.len() == 1 {
10083                return def_of(l[0]);
10084            }
10085            // A [w = E; v = w] forwarding PAIR — the shape the
10086            // structurer leaves when E is impure (`c v5 = new c(..);
10087            // v6 = v5`) and forward_single_use's adjacency pass missed
10088            // it (a duplicated read elsewhere in its raw body broke the
10089            // single-read gate). Collapse to `v = E`, gated on w being
10090            // method-unique: the merge DROPS this branch, so any other
10091            // read/write of w would dangle (lark gp2/d Kotlin default-
10092            // arg bridge, this-not-first family).
10093            if l.len() == 2 {
10094                let (w, ex) = def_of(l[0])?;
10095                let Stmt::ExprStmt(Expr::Assign {
10096                    target,
10097                    op: AssignOp::Plain,
10098                    value,
10099                }) = l[1]
10100                else {
10101                    return None;
10102                };
10103                let Expr::Local { var: tv, .. } = &**target else {
10104                    return None;
10105                };
10106                let Expr::Local { var: rv, .. } = &**value else {
10107                    return None;
10108                };
10109                if *rv != w || *tv == w {
10110                    return None;
10111                }
10112                // stmts_count_writes only sees Expr-level assigns; the
10113                // pair's own def is often an init-bearing LocalDef, so
10114                // count those separately for the uniqueness gate.
10115                let mut ldef_writes = 0usize;
10116                for st in scope {
10117                    walk_all(st, &mut |x| {
10118                        if let Stmt::LocalDef {
10119                            var,
10120                            init: Some(_),
10121                            ..
10122                        } = x
10123                        {
10124                            if *var == w {
10125                                ldef_writes += 1;
10126                            }
10127                        }
10128                    });
10129                }
10130                if stmts_read_var(scope, w) != 1
10131                    || stmts_count_writes(scope, w) + ldef_writes != 1
10132                {
10133                    return None;
10134                }
10135                return Some((*tv, ex));
10136            }
10137            None
10138        }
10139        let (v1, e1) = one(then_stmt, scope)?;
10140        let (v2, e2) = one(e, scope)?;
10141        if v1 != v2 {
10142            return None;
10143        }
10144        Some((
10145            v1,
10146            Expr::Cond {
10147                c: Box::new(cond.clone()),
10148                t: Box::new(e1.clone()),
10149                f: Box::new(e2.clone()),
10150            },
10151        ))
10152    }
10153
10154    // Prelude: defs (flat or folded cond), bare decls, side-effect stmts.
10155    let mut pmap: std::collections::HashMap<u32, Expr> =
10156        std::collections::HashMap::new();
10157    let mut pidx: std::collections::HashMap<u32, usize> =
10158        std::collections::HashMap::new();
10159    for (i, s) in stmts[..if_pos].iter().enumerate() {
10160        match s {
10161            Stmt::LocalDef { .. } | Stmt::ExprStmt(_) => {
10162                if let Some((v, e)) = def_of(s) {
10163                    pmap.insert(v, e.clone());
10164                    pidx.insert(v, i);
10165                }
10166            }
10167            Stmt::If { .. } => match cond_def_of(s, stmts) {
10168                Some((v, e)) => {
10169                    pmap.insert(v, e);
10170                    pidx.insert(v, i);
10171                }
10172                None => return false,
10173            },
10174            _ => return false,
10175        }
10176    }
10177    let (cond, then_v, else_v, had_else) = match &stmts[if_pos] {
10178        Stmt::If { cond, then_stmt, else_stmt, .. } => (
10179            cond.clone(),
10180            then_stmt.as_ref().clone(),
10181            else_stmt.as_ref().map_or_else(|| Stmt::Block(Vec::new()), |e| e.as_ref().clone()),
10182            else_stmt.is_some(),
10183        ),
10184        _ => return false,
10185    };
10186    // Chained guards (Kotlin multi-param null-checks):
10187    // `if(a){throw..} else { if(b){throw..} else { super(..); .. } }` —
10188    // a branch that is a lone delegation-bearing If gets merged
10189    // recursively first (its delegation hoisted to the branch head), so
10190    // the outer split then sees [call, nested-guard-remainder].
10191    fn normalize_branch(b: &mut Stmt) {
10192        // MOVE-based (no deep clone): chained weixin Parcel ctors carry
10193        // huge tails; cloning per recursion level cost measurable wall
10194        // time. merge_at leaves stmts untouched on failure, so the taken
10195        // node can always go back.
10196        let slot: &mut Stmt = match b {
10197            Stmt::Block(v) if v.len() == 1 => &mut v[0],
10198            other => other,
10199        };
10200        if !matches!(slot, Stmt::If { .. }) || !contains_delegation(slot) {
10201            return;
10202        }
10203        let taken = std::mem::replace(slot, Stmt::Block(Vec::new()));
10204        let mut tmp = vec![taken];
10205        if merge_at(&mut tmp, 0) {
10206            *slot = Stmt::Block(tmp);
10207        } else {
10208            *slot = tmp.pop().unwrap();
10209        }
10210    }
10211    let mut then_v = then_v;
10212    let mut else_v = else_v;
10213    normalize_branch(&mut then_v);
10214    normalize_branch(&mut else_v);
10215    let then_list = flat_list(&then_v);
10216    let else_list = flat_list(&else_v);
10217
10218    // Exactly: two Call branches (merge with ternaries) or one Call +
10219    // one Clean (single-delegation throw-guard). Owned branch payloads.
10220    enum Side {
10221        Call {
10222            at: usize,
10223            defs: Vec<(u32, Expr)>,
10224            def_idxs: Vec<usize>,
10225        },
10226        Clean,
10227    }
10228    let (t_side, e_side, two_sided) = match (split_branch(&then_list), split_branch(&else_list)) {
10229        (Br::Call(tc, td, ti), Br::Call(ec, ed, ei)) => (
10230            Side::Call { at: tc, defs: td, def_idxs: ti },
10231            Side::Call { at: ec, defs: ed, def_idxs: ei },
10232            true,
10233        ),
10234        (Br::Call(tc, td, ti), Br::Clean) => (
10235            Side::Call { at: tc, defs: td, def_idxs: ti },
10236            Side::Clean,
10237            false,
10238        ),
10239        (Br::Clean, Br::Call(ec, ed, ei)) => (
10240            Side::Clean,
10241            Side::Call { at: ec, defs: ed, def_idxs: ei },
10242            false,
10243        ),
10244        _ => return false,
10245    };
10246    // The delegation nodes (two-sided: both; single: one).
10247    let (t_call, e_call) = match (&t_side, &e_side) {
10248        (Side::Call { at: ta, .. }, Side::Call { at: ea, .. }) => (
10249            ctor_call_expr(then_list[*ta]),
10250            ctor_call_expr(else_list[*ea]),
10251        ),
10252        (Side::Call { at: ta, .. }, Side::Clean) => {
10253            (ctor_call_expr(then_list[*ta]), None)
10254        }
10255        (Side::Clean, Side::Call { at: ea, .. }) => {
10256            (None, ctor_call_expr(else_list[*ea]))
10257        }
10258        _ => return false,
10259    };
10260    // The node that becomes the merged call: then side when present.
10261    let (call_list, call_at) = match &t_side {
10262        Side::Call { at, .. } => (&then_list, *at),
10263        Side::Clean => match &e_side {
10264            Side::Call { at, .. } => (&else_list, *at),
10265            Side::Clean => return false,
10266        },
10267    };
10268    let (base_args, other_args) = if two_sided {
10269        let (Some(tc), Some(ec)) = (t_call, e_call) else {
10270            return false;
10271        };
10272        if !same_ctor_kind(tc, ec) {
10273            return false;
10274        }
10275        let (Expr::Method { args: a1, .. }, Expr::Method { args: a2, .. }) = (tc, ec) else {
10276            return false;
10277        };
10278        if a1.len() != a2.len() {
10279            return false;
10280        }
10281        (a1, Some(a2))
10282    } else {
10283        let c = t_call.or(e_call);
10284        let Some(c) = c else { return false };
10285        let Expr::Method { args, .. } = c else {
10286            return false;
10287        };
10288        (args, None)
10289    };
10290
10291    // Def maps per side (prelude + own branch defs).
10292    let mut tmap: std::collections::HashMap<u32, Expr> = pmap.clone();
10293    let mut emap: std::collections::HashMap<u32, Expr> = pmap.clone();
10294    if let Side::Call { defs, .. } = &t_side {
10295        for (v, e) in defs {
10296            tmap.insert(*v, e.clone());
10297        }
10298    }
10299    if let Side::Call { defs, .. } = &e_side {
10300        for (v, e) in defs {
10301            emap.insert(*v, e.clone());
10302        }
10303    }
10304    let Some(cond_i) = inline_locals(&cond, &pmap, 0) else {
10305        return false;
10306    };
10307    // Inline each side's args through its own map.
10308    let mut then_args: Vec<Expr> = Vec::with_capacity(base_args.len());
10309    let mut else_args: Vec<Expr> = Vec::new();
10310    if two_sided {
10311        for a in base_args {
10312            match inline_locals(a, &tmap, 0) {
10313                Some(r) => then_args.push(r),
10314                None => return false,
10315            }
10316        }
10317        for a in other_args.unwrap() {
10318            match inline_locals(a, &emap, 0) {
10319                Some(r) => else_args.push(r),
10320                None => return false,
10321            }
10322        }
10323    } else {
10324        // Single-delegation side: base_args belong to whichever side
10325        // carries the call; pick that side's map.
10326        let from_then = matches!(&t_side, Side::Call { .. });
10327        for a in base_args {
10328            let r = if from_then {
10329                inline_locals(a, &tmap, 0)
10330            } else {
10331                inline_locals(a, &emap, 0)
10332            };
10333            match r {
10334                Some(r) => then_args.push(r),
10335                None => return false,
10336            }
10337        }
10338    }
10339    let primary_args = base_args;
10340    // Per-position merge.
10341    let mut merged: Vec<Expr> = Vec::with_capacity(then_args.len());
10342    let mut differ = 0usize;
10343    for (j, t) in then_args.iter().enumerate() {
10344        if two_sided {
10345            let e = &else_args[j];
10346            if t == e {
10347                merged.push(t.clone());
10348            } else {
10349                differ += 1;
10350                merged.push(Expr::Cond {
10351                    c: Box::new(cond_i.clone()),
10352                    t: Box::new(t.clone()),
10353                    f: Box::new(e.clone()),
10354                });
10355            }
10356        } else {
10357            merged.push(t.clone());
10358        }
10359    }
10360
10361    // ---- side-effect preservation guards (batched counts) ----
10362    let vars_in = |e: &Expr| -> Vec<u32> {
10363        let mut vs = Vec::new();
10364        let mut probe = e.clone();
10365        strip_lost_alloc_owners(&mut probe);
10366        deep_rewrite(&mut probe, &mut |x| {
10367            if let Expr::Local { var, .. } = x {
10368                if !vs.contains(var) {
10369                    vs.push(*var);
10370                }
10371            }
10372        });
10373        vs
10374    };
10375    // Remainder statements per branch (extras + post-call tail; a Clean
10376    // branch contributes all of its statements).
10377    // The consumed defs LEAVE the remainder: their expressions were
10378    // inlined into the merged delegation args, and a dead `v = E` left
10379    // after the call both reads wrong and trips the u_t1/u_t2 guards
10380    // (ContinuationImpl's branch-local `context = null / getContext()`
10381    // — the coroutine family's 对this的调用必须是第一个语句 root).
10382    let branch_remainder = |list: &[&Stmt], side: &Side| -> Vec<Stmt> {
10383        match side {
10384            Side::Clean => list.iter().map(|s| (*s).clone()).collect(),
10385            Side::Call { at, def_idxs, .. } => {
10386                list.iter()
10387                    .enumerate()
10388                    .filter(|(i, _)| *i != *at && !def_idxs.contains(i))
10389                    .map(|(_, s)| (*s).clone())
10390                    .collect()
10391            }
10392        }
10393    };
10394    let tail_then_v = branch_remainder(&then_list, &t_side);
10395    let tail_else_v = branch_remainder(&else_list, &e_side);
10396    let post = &stmts[if_pos + 1..];
10397    let mut inlined: Vec<u32> = vars_in(&cond);
10398    for a in primary_args.iter().chain(else_args.iter()) {
10399        for v in vars_in(a) {
10400            if !inlined.contains(&v) {
10401                inlined.push(v);
10402            }
10403        }
10404    }
10405    inlined.retain(|v| pmap.contains_key(v) || tmap.contains_key(v) || emap.contains_key(v));
10406    // Prelude-and-branch redefinition: ambiguous provenance — reject.
10407    for &v in &inlined {
10408        let in_branch = match (&t_side, &e_side) {
10409            (Side::Call { defs: td, .. }, Side::Call { defs: ed, .. }) => {
10410                td.iter().any(|(d, _)| *d == v) || ed.iter().any(|(d, _)| *d == v)
10411            }
10412            (Side::Call { defs, .. }, Side::Clean)
10413            | (Side::Clean, Side::Call { defs, .. }) => {
10414                defs.iter().any(|(d, _)| *d == v)
10415            }
10416            _ => false,
10417        };
10418        if pmap.contains_key(&v) && in_branch {
10419            return false;
10420        }
10421    }
10422    type Counts = std::collections::HashMap<u32, usize>;
10423    type GuardCounts = (
10424        Counts,
10425        Vec<Counts>,
10426        Vec<Counts>,
10427        Counts,
10428        Counts,
10429        Counts,
10430        Vec<(u32, Counts)>,
10431    );
10432    let (cond_counts, a1_counts, a2_counts, tail1_counts, tail2_counts, post_counts, def_counts): GuardCounts =
10433        if inlined.is_empty() {
10434        Default::default()
10435    } else {
10436        let all_defs: Vec<(u32, &Expr)> = pmap
10437            .iter()
10438            .map(|(k, v)| (*k, v))
10439            .chain(match &t_side {
10440                Side::Call { defs, .. } => defs.iter().map(|(k, v)| (*k, v)).collect(),
10441                Side::Clean => Vec::new(),
10442            })
10443            .chain(match &e_side {
10444                Side::Call { defs, .. } => defs.iter().map(|(k, v)| (*k, v)).collect(),
10445                Side::Clean => Vec::new(),
10446            })
10447            .collect();
10448        (
10449            count_locals_expr(&cond),
10450            primary_args.iter().map(count_locals_expr).collect(),
10451            else_args.iter().map(count_locals_expr).collect(),
10452            count_locals_stmts(&tail_then_v),
10453            count_locals_stmts(&tail_else_v),
10454            count_locals_stmts(post),
10455            all_defs.into_iter().map(|(v, e)| (v, count_locals_expr(e))).collect(),
10456        )
10457        };
10458    for &v in &inlined {
10459        let in_t = matches!(&t_side, Side::Call { defs, .. } if defs.iter().any(|(d, _)| *d == v));
10460        let in_e = matches!(&e_side, Side::Call { defs, .. } if defs.iter().any(|(d, _)| *d == v));
10461        let effectful = [
10462            in_t.then(|| tmap.get(&v)).flatten(),
10463            in_e.then(|| emap.get(&v)).flatten(),
10464            pmap.get(&v),
10465        ]
10466        .into_iter()
10467        .flatten()
10468        .any(jdc_core::ir::build::has_side_effects);
10469        if !effectful {
10470            continue;
10471        }
10472        let u_cond = cnt(&cond_counts, v);
10473        let u_post = cnt(&post_counts, v);
10474        let u_t1 = cnt(&tail1_counts, v);
10475        let u_t2 = cnt(&tail2_counts, v);
10476        let mut u_odef = 0usize;
10477        for (dv, dc) in &def_counts {
10478            if *dv != v && inlined.contains(dv) {
10479                u_odef += cnt(dc, v);
10480            }
10481        }
10482        let mut emit = u_cond * (1 + differ);
10483        let mut uses_args = 0usize;
10484        for j in 0..primary_args.len() {
10485            let c1 = cnt(&a1_counts[j], v);
10486            let c2 = if two_sided && j < a2_counts.len() {
10487                cnt(&a2_counts[j], v)
10488            } else {
10489                0
10490            };
10491            uses_args += c1 + c2;
10492            emit += if two_sided && then_args[j] != else_args[j] {
10493                c1 + c2
10494            } else {
10495                c1.max(c2)
10496            };
10497        }
10498        let ok = if in_t || in_e {
10499            u_cond == 0
10500                && u_post == 0
10501                && u_t1 == 0
10502                && u_t2 == 0
10503                && u_odef == 0
10504                && a1_counts.iter().map(|c| cnt(c, v)).sum::<usize>() <= 1
10505                && a2_counts.iter().map(|c| cnt(c, v)).sum::<usize>() <= 1
10506                && emit <= 2
10507                && (in_t || a1_counts.iter().all(|c| cnt(c, v) == 0))
10508                && (in_e || a2_counts.iter().all(|c| cnt(c, v) == 0))
10509        } else {
10510            u_post == 0
10511                && u_t1 == 0
10512                && u_t2 == 0
10513                && u_odef == 0
10514                && ((u_cond == 1 && uses_args == 0 && emit <= 2)
10515                    || (u_cond == 0 && emit <= 1))
10516        };
10517        if !ok {
10518            return false;
10519        }
10520    }
10521
10522    // ---- assembly ----
10523    let mut call_stmt = call_list[call_at].clone();
10524    if let Stmt::ExprStmt(Expr::Method { args: slot, .. }) = &mut call_stmt {
10525        *slot = merged;
10526    }
10527    let residual = |v: u32| -> usize {
10528        cnt(&tail1_counts, v) + cnt(&tail2_counts, v) + cnt(&post_counts, v)
10529    };
10530    let mut kept_prelude: Vec<Stmt> = Vec::new();
10531    for (i, s) in stmts[..if_pos].iter().enumerate() {
10532        let consumed = match s {
10533            Stmt::LocalDef { var, .. } => Some(*var),
10534            Stmt::ExprStmt(Expr::Assign { target, .. }) => match &**target {
10535                Expr::Local { var, .. } => Some(*var),
10536                _ => None,
10537            },
10538            Stmt::If { .. } => cond_def_of(s, stmts).map(|(v, _)| v),
10539            _ => None,
10540        };
10541        if let Some(v) = consumed {
10542            if inlined.contains(&v) && pidx.get(&v) == Some(&i) && residual(v) == 0 {
10543                continue;
10544            }
10545        }
10546        kept_prelude.push(s.clone());
10547    }
10548    let build_keep = |list: &[&Stmt], side: &Side| -> Vec<Stmt> {
10549        match side {
10550            Side::Clean => list.iter().map(|s| (*s).clone()).collect(),
10551            Side::Call { at, .. } => {
10552                let mut out = Vec::new();
10553                for (i, s) in list.iter().enumerate() {
10554                    if i == *at {
10555                        continue;
10556                    }
10557                    if i < *at {
10558                        if let Some((v, _)) = def_of(s) {
10559                            if inlined.contains(&v) && residual(v) == 0 {
10560                                continue;
10561                            }
10562                        }
10563                    }
10564                    out.push((*s).clone());
10565                }
10566                out
10567            }
10568        }
10569    };
10570    let then_keep = build_keep(&then_list, &t_side);
10571    let else_keep = build_keep(&else_list, &e_side);
10572    let else_part = if else_keep.is_empty() && !had_else {
10573        None
10574    } else {
10575        Some(Box::new(Stmt::Block(else_keep)))
10576    };
10577    let new_if = Stmt::If {
10578        cond: cond_i,
10579        then_stmt: Box::new(Stmt::Block(then_keep)),
10580        else_stmt: else_part,
10581    };
10582    let if_empty = match &new_if {
10583        Stmt::If { then_stmt, else_stmt, .. } => {
10584            let be = |s: &Stmt| matches!(s, Stmt::Block(v) if v.is_empty());
10585            be(then_stmt) && else_stmt.as_ref().is_none_or(|e| be(e))
10586        }
10587        _ => false,
10588    };
10589    let mut out: Vec<Stmt> = Vec::with_capacity(stmts.len());
10590    out.push(call_stmt);
10591    out.extend(kept_prelude);
10592    if !if_empty {
10593        out.push(new_if);
10594    }
10595    out.extend(stmts[if_pos + 1..].to_vec());
10596    *stmts = out;
10597    true
10598}
10599
10600
10601pub fn strip_enum_ctor_super(body: &mut Stmt) {
10602    let is_enum_super = |s: &Stmt| {
10603        matches!(
10604            s,
10605            Stmt::ExprStmt(Expr::Method { name, is_super: true, .. }) if &**name == "<init>"
10606        )
10607    };
10608    let Stmt::Block(stmts) = body else {
10609        return;
10610    };
10611    stmts.retain(|s| !is_enum_super(s));
10612    for st in stmts.iter_mut() {
10613        if let Stmt::Block(inner) = st {
10614            inner.retain(|s| !is_enum_super(s));
10615        }
10616    }
10617}
10618
10619pub fn ensure_declared(body: &mut Stmt, vt: &VarTable) {
10620    // (Perf: dense Vec<bool> tables instead of HashSets — var ids are
10621    // dense; the `assigned` set collected here was never read and cost a
10622    // full extra tree walk per method.)
10623    let n = vt.vars.len().max(1);
10624
10625    // Demote extra LocalDefs (same var declared more than once) FIRST —
10626    // the scope analysis below must see exactly one def per var.
10627    let mut seen: HashSet<u32> = HashSet::default();
10628    demote_dup_decls(body, &mut seen, vt);
10629
10630    // Scope hoist: a LocalDef inside a nested block whose var is also
10631    // referenced OUTSIDE that block is a Java scope violation (the
10632    // branch-local `int v8 = pi.versionCode;` read after the try —
10633    // "cannot find symbol" at every outside use). Demote those defs to
10634    // assignments and let the bare top-of-method declaration below cover
10635    // the var. Two cheap passes: pre-order block numbering + per-var
10636    // inside/outside occurrence counts (pre-order subtree = id range).
10637    let mut def_block: Vec<u32> = vec![u32::MAX; n];
10638    let mut sizes: Vec<u32> = vec![0];
10639    let mut next_id: u32 = 1;
10640    scope_pass_a(body, 0, &mut next_id, &mut def_block, &mut sizes);
10641    sizes[0] = next_id;
10642    let mut within: Vec<u32> = vec![0; n];
10643    let mut total: Vec<u32> = vec![0; n];
10644    let mut next_b: u32 = 1;
10645    scope_pass_b(body, 0, &mut next_b, &def_block, &sizes, &mut within, &mut total);
10646    let mut hoist: HashSet<u32> = HashSet::default();
10647    for v in 0..n {
10648        if def_block[v] != u32::MAX && def_block[v] != 0 && within[v] < total[v] {
10649            hoist.insert(v as u32);
10650        }
10651    }
10652    if !hoist.is_empty() {
10653        let mut seed: HashSet<u32> = hoist.iter().copied().collect();
10654        demote_dup_decls(body, &mut seed, vt);
10655    }
10656
10657    let mut declared: Vec<bool> = vec![false; n];
10658    let mut is_param: Vec<bool> = vec![false; n];
10659    for v in &vt.vars {
10660        if v.is_param && (v.id as usize) < n {
10661            is_param[v.id as usize] = true;
10662        }
10663    }
10664    // Catch parameters are declared by the `catch (Type v)` clause —
10665    // bind_catches consumed their LocalDef before this pass ran, so
10666    // without this they re-declare at the top of the method and clash
10667    // with the clause (`Throwable th2;` + `catch (Throwable th2)` — a
10668    // syntax gate cannot see this, it is a semantic "already defined").
10669    let mut is_catch: Vec<bool> = vec![false; n];
10670    walk_all(body, &mut |st| {
10671        if let Stmt::Try { catches, .. } = st {
10672            for c in catches {
10673                if (c.var as usize) < n {
10674                    is_catch[c.var as usize] = true;
10675                }
10676            }
10677        }
10678    });
10679    walk_all(body, &mut |st| {
10680        if let Stmt::LocalDef { var, .. } = st {
10681            if (*var as usize) < n {
10682                declared[*var as usize] = true;
10683            }
10684        }
10685    });
10686    let mut used: HashSet<u32> = HashSet::default();
10687    // assignments=true: a var that only ever appears as an assign
10688    // TARGET still needs a declaration (`x = 5;` alone does not
10689    // declare x in Java).
10690    stmt_collect_vars(body, &mut used, true);
10691
10692    // Params are declared by the signature; hoisted vars lost their
10693    // LocalDef to the demotion above and always need the bare decl.
10694    let mut needs_decl: Vec<u32> = used
10695        .into_iter()
10696        .filter(|v| {
10697            let i = *v as usize;
10698            i >= n
10699                || ((!declared[i] && !is_param[i] && !is_catch[i]) || hoist.contains(v))
10700        })
10701        .collect();
10702    needs_decl.sort_unstable();
10703    needs_decl.dedup();
10704
10705    if !needs_decl.is_empty() {
10706        let mut decls: Vec<Stmt> = needs_decl
10707            .into_iter()
10708            .map(|v| Stmt::LocalDef {
10709                var: v,
10710                init: None,
10711                is_final: false,
10712                force_type: true,
10713            })
10714            .collect();
10715        decls.reverse();
10716        match body {
10717            Stmt::Block(v) => {
10718                for d in decls {
10719                    v.insert(0, d);
10720                }
10721            }
10722            other => {
10723                let inner = std::mem::replace(other, Stmt::Block(vec![]));
10724                *other = Stmt::Block({
10725                    let mut vs = decls;
10726                    vs.push(inner);
10727                    vs
10728                });
10729            }
10730        }
10731    }
10732}
10733
10734fn demote_dup_decls(s: &mut Stmt, seen: &mut HashSet<u32>, vt: &VarTable) {
10735    match s {
10736        // Exactly one visit per node: the Block arm consumes its children,
10737        // the fallthrough walk would double-visit them.
10738        Stmt::Block(v) => {
10739            for x in v.iter_mut() {
10740                demote_dup_decls(x, seen, vt);
10741            }
10742        }
10743        Stmt::LocalDef { var, init, .. } => {
10744            if seen.contains(var) {
10745                let ty = vt.var(*var).ty.clone();
10746                if let Some(e) = init.take() {
10747                    *s = Stmt::ExprStmt(Expr::Assign {
10748                        target: Box::new(Expr::Local { var: *var, ty }),
10749                        op: AssignOp::Plain,
10750                        value: Box::new(e),
10751                    });
10752                } else {
10753                    *s = Stmt::Block(vec![]);
10754                }
10755            } else {
10756                seen.insert(*var);
10757            }
10758        }
10759        other => {
10760            walk_mut(other, &mut |x| demote_dup_decls(x, seen, vt));
10761        }
10762    }
10763}
10764
10765#[allow(dead_code)]
10766fn unused(_: &Vec<CaseGroup>, _: &Catch) {}
10767
10768// ---------------------------------------------------------------------------
10769// jadx-style local names (ApplyVariableNames + dexdec's port of it)
10770// ---------------------------------------------------------------------------
10771
10772/// A synthetic `v12`/`p3` never survives when a better name exists.
10773/// Priority: (1) a Kotlin `Intrinsics.checkNotNullParameter(x, "name")`
10774/// names `x` from the message string (the string IS the parameter name);
10775/// (2) the single defining call — `getFoo()` → `foo`, `isFinishing()` →
10776/// `finishing`, `new File(…)` → `file`; (3) a type alias or the
10777/// lowercased class simple name. Collisions take `2`, `3`, …; a var with
10778/// two DISAGREEING defining calls stays unnamed (dexdec's
10779/// RelationalNameInference rule). Debug-info names are never touched
10780/// (`synthetic_name` gates the whole pass).
10781pub fn apply_local_names(vt: &mut VarTable, body: &Stmt) {
10782    use std::collections::{HashMap, HashSet};
10783
10784    // ---- proposals ------------------------------------------------------
10785    // var → (name, source-priority); disagreeing call names are dropped.
10786    let mut by_call: HashMap<u32, Vec<String>> = HashMap::new();
10787    let mut by_intrinsics: HashMap<u32, String> = HashMap::new();
10788
10789    visit_all_exprs(body, &mut |e| {
10790        if let Expr::Method { cls, name, args, .. } = e {
10791            if (cls.as_ref() == "kotlin/jvm/internal/Intrinsics"
10792                || cls.as_ref() == "kotlin/jvm/internal/IntrinsicsKt")
10793                && matches!(name.as_ref(), "checkNotNullParameter" | "checkParameterIsNotNull")
10794                && args.len() == 2
10795            {
10796                if let (Expr::Local { var, .. }, Expr::Const(ConstVal::Str(s))) = (&args[0], &args[1])
10797                {
10798                    by_intrinsics
10799                        .entry(*var)
10800                        .or_insert_with(|| sanitize_name(s).unwrap_or_default());
10801                }
10802            }
10803        }
10804    });
10805
10806    walk_all(body, &mut |st| {
10807        let (var, value) = match st {
10808            Stmt::LocalDef { var, init: Some(e), .. } => (*var, e),
10809            Stmt::ExprStmt(Expr::Assign { target, value, .. })
10810                if matches!(&**target, Expr::Local { .. }) =>
10811            {
10812                let Expr::Local { var, .. } = &**target else { return };
10813                (*var, value.as_ref())
10814            }
10815            _ => return,
10816        };
10817        if let Some(n) = defining_call_name(value) {
10818            by_call.entry(var).or_default().push(n);
10819        }
10820    });
10821
10822    // ---- reservation + application --------------------------------------
10823    // Real (debug-info) names can legally REPEAT across sibling source
10824    // scopes (`int i` in two loops) but our flat declaration hoisting
10825    // puts them in one scope — de-duplicate with the same numeric
10826    // suffixes the claim path uses.
10827    // Uniqueness operates on the SANITIZED display name: the identifier
10828    // sanitizer maps non-ASCII to `_`, so distinct debug names
10829    // (`ERROR_token参数缺失` / `ERROR_channelId参数缺失`) both RENDER as
10830    // `ERROR________` and collide as declarations. Tracking raw names
10831    // here would let the collision through.
10832    let sanit = |n: &str| crate::classdec::java_ident(n).into_owned();
10833    let mut taken: HashSet<String> = HashSet::default();
10834    // Parameter names are occupied REGARDLESS of syntheticness: claim()
10835    // hands out rename candidates, and without the synthetic params in
10836    // the pool a type fallback (`l7.p0` → "p0") renamed one parameter
10837    // ONTO another's slot-name (`b(v6.l p0, Object obj, l7.p0 p0)`).
10838    for v in vt.vars.iter() {
10839        // ALL synthetic names occupy their names, not just parameters:
10840        // a type fallback (`pc5.v56` → "v56") otherwise renames a local
10841        // ONTO another local's slot-name (`int v56;` beside
10842        // `pc5.v56 v56;`).
10843        if v.synthetic_name {
10844            taken.insert(sanit(&v.name));
10845        }
10846    }
10847    for v in vt.vars.iter_mut() {
10848        if v.synthetic_name {
10849            continue;
10850        }
10851        if !taken.insert(sanit(&v.name)) {
10852            let base = sanit(&v.name);
10853            for i in 2.. {
10854                let cand = format!("{base}{i}");
10855                if taken.insert(cand.clone()) {
10856                    v.name = cand;
10857                    break;
10858                }
10859            }
10860        }
10861    }
10862    let claim = |taken: &mut HashSet<String>, want: &str| -> String {
10863        let w = crate::classdec::java_ident(want).into_owned();
10864        if taken.insert(w.clone()) {
10865            return w;
10866        }
10867        for i in 2.. {
10868            let cand = format!("{w}{i}");
10869            if taken.insert(cand.clone()) {
10870                return cand;
10871            }
10872        }
10873        w
10874    };
10875
10876    for info in vt.vars.iter_mut() {
10877        if !info.synthetic_name {
10878            continue;
10879        }
10880        // (1) Intrinsics string — the Kotlin compiler wrote the real
10881        // parameter name right into the check.
10882        if let Some(n) = by_intrinsics.get(&info.id) {
10883            if !n.is_empty() {
10884                info.name = claim(&mut taken, n);
10885                continue;
10886            }
10887        }
10888        // (2) One consistent defining call.
10889        if let Some(names) = by_call.get(&info.id) {
10890            let unique: HashSet<&String> = names.iter().collect();
10891            if unique.len() == 1 {
10892                let n = unique.into_iter().next().unwrap();
10893                if !is_java_keyword(n) {
10894                    info.name = claim(&mut taken, n);
10895                    continue;
10896                }
10897            }
10898        }
10899        // (3) Type alias, else the lowercased simple class name
10900        // (jadx names a `Looper` local `looper`).
10901        let want = type_alias(&info.ty).map(str::to_string).or_else(|| simple_type_name(&info.ty));
10902        if let Some(n) = want {
10903            info.name = claim(&mut taken, &n);
10904        }
10905    }
10906
10907    // Final uniquification across ALL vars (params, locals, catch): the
10908    // reservation, claim and debug-name domains above are checked
10909    // pairwise but a name claimed LAST can still equal a debug name
10910    // that never re-checked (`zd1.v2 v2` beside `zd1.v2[] v2`).
10911    let mut seen: HashSet<String> = HashSet::default();
10912    for v in vt.vars.iter_mut() {
10913        if !seen.insert(sanit(&v.name)) {
10914            let base = sanit(&v.name);
10915            for i in 2.. {
10916                let cand = format!("{base}{i}");
10917                if seen.insert(cand.clone()) {
10918                    v.name = cand;
10919                    break;
10920                }
10921            }
10922        }
10923    }
10924}
10925
10926/// `com/android/.../Looper` → `looper` — the alias-table fallback.
10927fn simple_type_name(ty: &TypeRef) -> Option<String> {
10928    let TypeRef::J(JavaType::Object(n)) = ty else {
10929        return None;
10930    };
10931    let simple = n.rsplit('/').next().unwrap_or(n);
10932    let simple = simple.rsplit('$').next().unwrap_or(simple);
10933    // Lowercase the first ALPHABETIC char — `$`/`_`-prefixed names
10934    // (R8's `$$$_Thread`) must not survive capitalized.
10935    let mut chars = simple.chars();
10936    let mut out = String::with_capacity(simple.len());
10937    let mut lowered = false;
10938    for c in chars.by_ref() {
10939        if c.is_ascii_alphabetic() && !lowered {
10940            out.push(c.to_ascii_lowercase());
10941            lowered = true;
10942        } else {
10943            out.push(c);
10944        }
10945        if lowered {
10946            break;
10947        }
10948    }
10949    out.extend(chars);
10950    sanitize_name(&out)
10951}
10952
10953/// `getFoo()` → `foo`, `isFinishing()` → `finishing`, `new File(…)` →
10954/// `file`. The get/is prefixes only strip when the remainder starts
10955/// uppercase (so `issues()` keeps its name).
10956fn defining_call_name(e: &Expr) -> Option<String> {
10957    match e {
10958        Expr::Method { name, .. } => {
10959            let base = name
10960                .strip_prefix("get")
10961                .or_else(|| name.strip_prefix("is"))
10962                .filter(|rest| rest.chars().next().is_some_and(|c| c.is_ascii_uppercase()))
10963                .unwrap_or(name);
10964            let first = base.chars().next()?;
10965            let mut out = String::with_capacity(base.len());
10966            out.push(first.to_ascii_lowercase());
10967            out.extend(base.chars().skip(1));
10968            sanitize_name(&out)
10969        }
10970        Expr::New { cls, .. } => {
10971            let simple = cls.rsplit('/').next().unwrap_or(cls);
10972            let simple = simple.rsplit('$').next().unwrap_or(simple);
10973            let first = simple.chars().next()?;
10974            let mut out = String::with_capacity(simple.len());
10975            out.push(first.to_ascii_lowercase());
10976            out.extend(simple.chars().skip(1));
10977            sanitize_name(&out)
10978        }
10979        _ => None,
10980    }
10981}
10982
10983/// Valid java identifier, not a keyword/restricted name, ≥2 chars.
10984fn sanitize_name(s: &str) -> Option<String> {
10985    let mut out = String::with_capacity(s.len());
10986    for c in s.chars() {
10987        if c.is_ascii_alphanumeric() || c == '_' || c == '$' {
10988            out.push(c);
10989        } else {
10990            out.push('_');
10991        }
10992    }
10993    if out.len() < 2
10994        || out.chars().next().is_some_and(|c| c.is_ascii_digit())
10995        || is_java_keyword(&out)
10996    {
10997        return None;
10998    }
10999    Some(out)
11000}
11001
11002/// jadx's alias table for the common types, with the Android globals the
11003/// corpus actually shows; anything else falls back in the caller.
11004fn type_alias(ty: &TypeRef) -> Option<&'static str> {
11005    let TypeRef::J(JavaType::Object(n)) = ty else {
11006        return None;
11007    };
11008    Some(match n.as_ref() {
11009        "java/lang/String" | "kotlin/String" => "str",
11010        "java/lang/Class" => "cls",
11011        "java/lang/Throwable" => "th",
11012        "java/lang/Object" | "kotlin/Any" => "obj",
11013        "java/util/Iterator" | "kotlin/collections/Iterator" => "it",
11014        "java/lang/Boolean" => "bool",
11015        "java/lang/Integer" => "num",
11016        "java/lang/Character" => "ch",
11017        "java/lang/Byte" => "b",
11018        "java/lang/Short" => "sh",
11019        "java/lang/Float" => "f",
11020        "java/lang/Double" => "d",
11021        "java/lang/Long" => "i",
11022        "java/lang/StringBuilder" => "sb",
11023        "java/util/ArrayList" => "list",
11024        "java/util/HashMap" => "map",
11025        "android/content/Context" => "context",
11026        "android/content/Intent" => "intent",
11027        "android/os/Bundle" => "bundle",
11028        "android/view/View" => "view",
11029        "android/graphics/Bitmap" => "bitmap",
11030        "android/view/ViewGroup" => "viewGroup",
11031        _ => return None,
11032    })
11033}
11034
11035/// Every expression in the tree, statements included (read-only).
11036pub(crate) fn visit_all_exprs<F: FnMut(&Expr)>(s: &Stmt, f: &mut F) {
11037    walk_all(s, &mut |st| {
11038        let exprs: Vec<&Expr> = match st {
11039            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
11040                let mut v: Vec<&Expr> = vec![value];
11041                if !matches!(&**target, Expr::Local { .. }) {
11042                    v.push(target);
11043                }
11044                v
11045            }
11046            Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
11047                vec![e]
11048            }
11049            Stmt::Return(Some(e)) => vec![e],
11050            Stmt::LocalDef { init: Some(e), .. } => vec![e],
11051            Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
11052                vec![cond]
11053            }
11054            _ => Vec::new(),
11055        };
11056        for e in exprs {
11057            visit_exprs(e, f);
11058        }
11059    });
11060}
11061
11062/// IntDef/LongDef exact-match rendering: a literal argument to a
11063/// platform method whose parameter carries a constant domain renders as
11064/// the named constant (`setVisibility(8)` → `android.view.View.GONE`).
11065/// Combined flag values and literals outside the domain stay numeric.
11066pub fn platform_constants(body: &mut Stmt) {
11067    if !crate::platform::installed() {
11068        return;
11069    }
11070    rewrite_exprs(body, &mut |e| {
11071        if let Expr::Method { cls, name, desc, args, .. } = e {
11072            for a in args.iter_mut() {
11073                let (value, is_long) = match a {
11074                    Expr::Const(ConstVal::Int(v)) => (*v as i64, false),
11075                    Expr::Const(ConstVal::Long(v)) => (*v, true),
11076                    _ => continue,
11077                };
11078                if let Some(named) =
11079                    crate::platform::named_constant(cls, name, &desc_to_string(desc), 0, value)
11080                {
11081                    let _ = is_long;
11082                    *a = Expr::Raw(named);
11083                }
11084            }
11085        }
11086    });
11087}
11088
11089/// MethodDescriptor → dex descriptor string (`(I)V`).
11090fn desc_to_string(d: &jdc_core::types::MethodDescriptor) -> String {
11091    let mut out = String::from("(");
11092    for a in &d.args {
11093        type_to_desc(a, &mut out);
11094    }
11095    out.push(')');
11096    type_to_desc(&d.ret, &mut out);
11097    out
11098}
11099
11100fn type_to_desc(t: &jdc_core::types::JavaType, out: &mut String) {
11101    match t {
11102        JavaType::Void => out.push('V'),
11103        JavaType::Boolean => out.push('Z'),
11104        JavaType::Byte => out.push('B'),
11105        JavaType::Char => out.push('C'),
11106        JavaType::Short => out.push('S'),
11107        JavaType::Int => out.push('I'),
11108        JavaType::Float => out.push('F'),
11109        JavaType::Long => out.push('J'),
11110        JavaType::Double => out.push('D'),
11111        JavaType::Object(n) => {
11112            out.push('L');
11113            out.push_str(n);
11114            out.push(';');
11115        }
11116        JavaType::Array(inner) => {
11117            out.push('[');
11118            type_to_desc(inner, out);
11119        }
11120    }
11121}
11122
11123/// Kotlin `Intrinsics` null-check elision (jadx's ProcessKotlinInternals):
11124/// statement-position `checkNotNullParameter(p, "name")` /
11125/// `checkParameterIsNotNull` / `checkNotNull…` calls are runtime
11126/// assertions — drop them. The naming pass harvests the parameter
11127/// strings FIRST, so run this after `apply_local_names`.
11128pub fn remove_kotlin_checks(s: &mut Stmt) {
11129    walk_mut_deep(s, &mut |st| {
11130        let Stmt::Block(v) = st else { return };
11131        v.retain(|x| !is_kotlin_check(x));
11132    });
11133}
11134
11135fn is_kotlin_check(s: &Stmt) -> bool {
11136    let Stmt::ExprStmt(e) = s else { return false };
11137    let Expr::Method { cls, name, .. } = e else { return false };
11138    (cls.as_ref() == "kotlin/jvm/internal/Intrinsics" || cls.as_ref() == "kotlin/jvm/internal/IntrinsicsKt__Jdk7Kt")
11139        && matches!(
11140            name.as_ref(),
11141            "checkNotNullParameter"
11142                | "checkParameterIsNotNull"
11143                | "checkExpressionValueIsNotNull"
11144                | "checkNotNullExpressionValue"
11145                | "checkReturnedValueIsNotNull"
11146                | "checkFieldIsNotNull"
11147                | "checkNotNull"
11148        )
11149}
11150
11151// ---------------------------------------------------------------------------
11152// Synthetic-accessor inlining (jadx's MarkMethodsForInline, the safe subset)
11153// ---------------------------------------------------------------------------
11154
11155/// Inline `access$NNN`-style synthetic static bridges at their call
11156/// sites: an identity (`return pN`), a getter (`iget pX, field;
11157/// return vR`), or a method forwarder (`invoke; return`) — with the
11158/// d8 APM trace wrappers (`MethodCollector.i/.o` const-only static
11159/// calls) tolerated around the core. Only STATIC + SYNTHETIC callees
11160/// qualify (compiler-generated bridges: no override semantics, no
11161/// side effects beyond the forwarded operation).
11162/// Can `host` (internal class name) source-level reference `target`'s
11163/// class chain and member? Cross-package inlining of a synthetic
11164/// accessor used to EXPOSE package-private targets the raw dex never
11165/// referenced cross-package (BuildersKt.launch$default forwards to
11166/// kotlinx.coroutines.k.e — the widening census cannot see post-inline
11167/// calls; lark's 1,019-line k不是公共的 family). Non-public chain link
11168/// or non-public member across packages → the inline must not happen
11169/// (the public synthetic bridge stays and compiles).
11170fn inline_ref_ok(pool: &DexPool, host: &str, target: &str, member: Option<(&str, bool)>) -> bool {
11171    let hpkg = match host.rfind('/') {
11172        Some(i) => &host[..i],
11173        None => "",
11174    };
11175    let mut cur = target;
11176    loop {
11177        let tpkg = match cur.rfind('/') {
11178            Some(i) => &cur[..i],
11179            None => "",
11180        };
11181        if tpkg != hpkg {
11182            if let Some(pc) = pool.get(cur) {
11183                if pc.access & crate::access::ACC_PUBLIC == 0 {
11184                    return false;
11185                }
11186                if cur == target {
11187                    if let Some((mname, is_method)) = member {
11188                        let ok = if is_method {
11189                            pc.all_methods().any(|m| {
11190                                &*m.name == mname
11191                                    && m.access & crate::access::ACC_PUBLIC != 0
11192                            })
11193                        } else {
11194                            pc.static_fields
11195                                .iter()
11196                                .chain(pc.instance_fields.iter())
11197                                .any(|f| {
11198                                    f.name == mname
11199                                        && f.access & crate::access::ACC_PUBLIC != 0
11200                                })
11201                        };
11202                        // A member match requires the exact member to be
11203                        // public; an absent member (renamed/collapsed)
11204                        // stays allowed — the render will show what the
11205                        // pool has.
11206                        if !ok
11207                            && (if is_method {
11208                                pc.all_methods().any(|m| &*m.name == mname)
11209                            } else {
11210                                pc.static_fields
11211                                    .iter()
11212                                    .chain(pc.instance_fields.iter())
11213                                    .any(|f| f.name == mname)
11214                            })
11215                        {
11216                            return false;
11217                        }
11218                    }
11219                }
11220            }
11221        }
11222        match cur.rfind('$') {
11223            Some(i) if i > 0 => cur = &cur[..i],
11224            _ => break,
11225        }
11226    }
11227    true
11228}
11229
11230pub fn inline_accessors(s: &mut Stmt, pool: &DexPool, host: &str) {
11231    rewrite_exprs(s, &mut |e| {
11232        let Expr::Method { cls, name, desc, args, is_static, .. } = e else { return };
11233        if !*is_static || args.is_empty() {
11234            return;
11235        }
11236        let Some(target) = pool.get(cls) else { return };
11237        let desc_s = desc_to_string(desc);
11238        let Some(m) = target.find_method(name, &desc_s) else { return };
11239        if !(m.is_static() && m.access & access::ACC_SYNTHETIC != 0) {
11240            return;
11241        }
11242        let Some(dex) = pool.dex(m.dex_idx) else { return };
11243        // Snapshot first: the accessor's owning image may already be
11244        // retired, and a live read's success would depend on worker
11245        // completion interleaving — nondeterministic inlining (whether
11246        // the accessor folds at all) between identical runs.
11247        let code = match pool.accessor_code(m.dex_idx, m.code_off) {
11248            Some(bytes) => ddc_dex::CodeItem::parse(&bytes, 0),
11249            None => dex.code_at(m.code_off),
11250        };
11251        let Some(code) = code else { return };
11252        let insns: Vec<&Insn> =
11253            code.insns.iter().filter(|i| !matches!(i.kind, InsnKind::Nop)).collect();
11254        // Strip the APM trace wrappers (const-only invoke-static).
11255        let core: Vec<&Insn> = insns
11256            .iter()
11257            .copied()
11258            .filter(|i| !is_const_only_trace(&i.kind, &insns))
11259            .collect();
11260        match accessor_shape(&dex, &core, &code) {
11261            Some(Shape::Identity(arg_i)) => {
11262                if let Some(a) = args.get(arg_i) {
11263                    *e = a.clone();
11264                }
11265            }
11266            Some(Shape::FieldRead { arg, cls: field_cls, field, field_ty }) => {
11267                if !inline_ref_ok(pool, host, &field_cls, Some((&field, false))) {
11268                    return;
11269                }
11270                if let Some(owner) = args.get(arg).cloned().map(Box::new) {
11271                    let ty = TypeRef::J(desc_type(&field_ty));
11272                    // The inlined read must consult the member rename
11273                    // registry like every lifted field ref does — the
11274                    // raw dex name desynced from renamed declarations
11275                    // (lark uu4/f$b's `a`→`ax6`: 找不到符号 变量 a ×4.4k
11276                    // when the nested-collision deshadow fired).
11277                    let name = jdc_core::rename::field_display(
11278                        &field_cls,
11279                        &field,
11280                        &field_ty,
11281                    )
11282                    .map(std::sync::Arc::from)
11283                    .unwrap_or_else(|| field.into());
11284                    *e = Expr::Field {
11285                        owner: Some(owner),
11286                        cls: field_cls.into(),
11287                        name,
11288                        ty,
11289                        is_static: false,
11290                    };
11291                }
11292            }
11293            Some(Shape::Forward { cls: tcls, name: tname, desc: tdesc, instance }) => {
11294                if !inline_ref_ok(pool, host, &tcls, Some((&tname, true))) {
11295                    return;
11296                }
11297                // Same registry duty as FieldRead: the forwarded member
11298                // must render under its renamed display, not the raw
11299                // dex name.
11300                let disp = jdc_core::rename::field_display(
11301                    &tcls,
11302                    &tname,
11303                    &tdesc.to_string(),
11304                )
11305                .map(std::sync::Arc::from)
11306                .unwrap_or_else(|| tname.into());
11307                *cls = tcls.into();
11308                *name = disp;
11309                *desc = std::sync::Arc::new(tdesc);
11310                if instance && !args.is_empty() {
11311                    // The first param (the receiver) becomes the owner.
11312                    let recv = args.remove(0);
11313                    if let Expr::Method { owner, .. } = e {
11314                        *owner = Some(Box::new(recv));
11315                    }
11316                }
11317            }
11318            None => {}
11319        }
11320    });
11321}
11322
11323/// What an accessor body reduces to.
11324enum Shape {
11325    /// `return pN` — the call becomes argument N.
11326    Identity(usize),
11327    /// `iget vR, pX, field; return vR` — becomes `argN.field`.
11328    FieldRead { arg: usize, cls: String, field: String, field_ty: String },
11329    /// `invoke {pX, args…}, method@M; (move-result;)? return` —
11330    /// becomes the forwarded call.
11331    Forward { cls: String, name: String, desc: MethodDescriptor, instance: bool },
11332}
11333
11334/// Resolve the IGet field's owner class, name and type descriptor.
11335fn field_of(dex: &ddc_dex::DexFile, field_idx: u32) -> Option<(String, String, String)> {
11336    let f = dex.field(field_idx);
11337    Some((
11338        // class_name strips the `L...;` shell; type_name would leak a
11339        // descriptor into the Field's owner class (`La3.a.e_`).
11340        dex.class_name(f.class_idx),
11341        dex.string(f.name_idx).to_string(),
11342        // The FIELD TYPE stays a descriptor: desc_type parses it.
11343        dex.type_name(f.type_idx).to_string(),
11344    ))
11345}
11346
11347/// Map a register to a parameter index (static method: params occupy
11348/// the LAST ins_size registers).
11349fn param_index(code: &ddc_dex::CodeItem, reg: u16) -> Option<usize> {
11350    let rs = code.registers_size as usize;
11351    let ins = code.ins_size as usize;
11352    let r = reg as usize;
11353    let first = rs.checked_sub(ins)?;
11354    (r >= first && r < rs).then(|| r - first)
11355}
11356
11357fn accessor_shape(
11358    dex: &ddc_dex::DexFile,
11359    core: &[&Insn],
11360    code: &ddc_dex::CodeItem,
11361) -> Option<Shape> {
11362    // Dead consts that fed the stripped trace wrappers remain in the
11363    // core — skip them.
11364    let core: &[&Insn] = match core.split_first() {
11365        Some((first, rest)) if matches!(first.kind, InsnKind::Const { .. }) => rest,
11366        _ => core,
11367    };
11368    let core: &[&Insn] = match core.split_last() {
11369        Some((last, rest)) if matches!(last.kind, InsnKind::Const { .. }) => rest,
11370        _ => core,
11371    };
11372    match core {
11373        // return pN
11374        [Insn { kind: InsnKind::Return { src }, .. }] => {
11375            let idx = param_index(code, *src)?;
11376            Some(Shape::Identity(idx))
11377        }
11378        // iget vR, pX, field; return vR
11379        [
11380            Insn { kind: InsnKind::IGet { dst, obj, field_idx }, .. },
11381            Insn { kind: InsnKind::Return { src }, .. },
11382        ] if dst == src => {
11383            let arg = param_index(code, *obj)?;
11384            let (cls, field, field_ty) = field_of(dex, *field_idx)?;
11385            Some(Shape::FieldRead { arg, cls, field, field_ty })
11386        }
11387        // invoke {…}, method@M; (move-result; return)? — forwarder.
11388        [Insn { kind: InsnKind::Invoke { method_idx, regs, kind, .. }, .. }, rest @ ..] if rest.len() <= 2 => {
11389            // Only forward when every register is a param (no locals).
11390            if !regs.iter().all(|r| param_index(code, *r).is_some()) || regs.is_empty() {
11391                return None;
11392            }
11393            // Trailing must be move-result+return or return/void —
11394            // anything else (e.g. trace calls) already filtered upstream.
11395            if rest.len() == 2 {
11396                let ok = matches!(rest[0].kind, InsnKind::MoveResult { .. })
11397                    && matches!(rest[1].kind, InsnKind::Return { .. } | InsnKind::ReturnVoid);
11398                if !ok {
11399                    return None;
11400                }
11401            }
11402            let mid = dex.method(*method_idx);
11403            // class_name strips the `L...;` shell — type_name leaks a
11404            // descriptor into the forwarded call's class (`La3.a.e_`).
11405            let cls = dex.class_name(mid.class_idx);
11406            let name = dex.string(mid.name_idx).to_string();
11407            let mut s = String::from("(");
11408            for t in dex.proto_params(mid.proto_idx) {
11409                s.push_str(dex.type_name(*t));
11410            }
11411            s.push(')');
11412            s.push_str(dex.type_name(dex.proto(mid.proto_idx).return_type_idx));
11413            let desc = parse_method_descriptor(&s)?;
11414            Some(Shape::Forward { cls, name, desc, instance: !matches!(kind, InvokeKind::Static) })
11415        }
11416        _ => None,
11417    }
11418}
11419
11420
11421/// A const-only invoke-static (the d8 APM `i(732046)` / `o(732046)`
11422/// trace wrapper): every argument register is loaded by a Const (or a
11423/// Const-into-move chain) in the SAME body. Scoped to synthetic
11424/// accessors, so a genuinely side-effecting const call is never
11425/// mistaken for a trace.
11426fn is_const_only_trace(kind: &InsnKind, insns: &[&Insn]) -> bool {
11427    let InsnKind::Invoke { kind, regs, .. } = kind else { return false };
11428    if !matches!(kind, InvokeKind::Static) || regs.is_empty() {
11429        return false;
11430    }
11431    regs.iter().all(|r| const_loaded(*r, insns, 0))
11432}
11433
11434/// Const-loaded directly, or through a Move chain from a const-loaded
11435/// register (the APM `const v0, id` → `invoke {v0}` shape).
11436fn const_loaded(r: u16, insns: &[&Insn], depth: u8) -> bool {
11437    if depth > 3 {
11438        return false;
11439    }
11440    insns.iter().any(|i| match &i.kind {
11441        InsnKind::Const { dst, .. } => *dst == r,
11442        InsnKind::Move { dst, src } => *dst == r && const_loaded(*src, insns, depth + 1),
11443        _ => false,
11444    })
11445}