Skip to main content

ddc_dec/
passes.rs

1//! Post-convert refinement passes for DEX-lifted statement trees.
2//!
3//! The converter hands back a correct-but-raw tree; these passes restore the
4//! source shapes a register machine loses: catch parameter binding,
5//! copy-forwarding (single-use temporaries), if/else→ternary folding,
6//! `StringBuilder` chain → `+` concatenation, `synchronized` recovery from
7//! the d8 monitor pattern, erased-type inference, boolean and null
8//! comparisons, and declaration hygiene.
9
10// The tree-walker match arms intentionally mirror the statement grammar
11// one level at a time; collapsing the nested `if let`s into outer match
12// arms would trade per-arm clarity for lint silence.
13#![allow(clippy::collapsible_match)]
14
15use jdc_core::FxHashSet as HashSet;
16
17// The tree-walker match arms intentionally mirror the statement grammar
18// one level at a time; collapsing the nested `if let`s into outer match
19// arms would trade per-arm clarity for lint silence.
20use jdc_core::ir::build::has_side_effects;
21use jdc_core::emit::is_java_keyword;
22use jdc_core::ir::expr::{AssignOp, BinOp, ConcatPart, ConstVal, Expr, TypeRef, UnOp};
23use jdc_core::ir::stmt::{CaseGroup, Catch, Stmt};
24use jdc_core::types::{JavaType, MethodDescriptor};
25use jdc_core::var::VarTable;
26
27use crate::lift::MethodEnv;
28use crate::{access, desc_type, DexPool};
29use ddc_dex::insn::{Insn, InsnKind, InvokeKind};
30use jdc_core::types::parse_method_descriptor;
31
32// ---------------------------------------------------------------------------
33// Generic tree walking
34// ---------------------------------------------------------------------------
35
36/// Deep expression rewrite over a statement tree.
37pub fn rewrite_exprs<F: FnMut(&mut Expr)>(s: &mut Stmt, f: &mut F) {
38    walk_stmt_exprs(s, f);
39}
40
41pub(crate) fn walk_stmt_exprs<F: FnMut(&mut Expr)>(s: &mut Stmt, f: &mut F) {
42    match s {
43        Stmt::Block(v) => {
44            for x in v.iter_mut() {
45                walk_stmt_exprs(x, f);
46            }
47        }
48        Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
49            f(e);
50        }
51        Stmt::Return(Some(e)) => f(e),
52        Stmt::LocalDef { init, .. } => {
53            if let Some(e) = init {
54                f(e);
55            }
56        }
57        Stmt::If {
58            cond,
59            then_stmt,
60            else_stmt,
61        } => {
62            f(cond);
63            walk_stmt_exprs(then_stmt, f);
64            if let Some(e) = else_stmt {
65                walk_stmt_exprs(e, f);
66            }
67        }
68        Stmt::While { cond, body } => {
69            f(cond);
70            walk_stmt_exprs(body, f);
71        }
72        Stmt::DoWhile { body, cond } => {
73            walk_stmt_exprs(body, f);
74            f(cond);
75        }
76        Stmt::For {
77            init,
78            cond,
79            update,
80            body,
81        } => {
82            for x in init.iter_mut() {
83                walk_stmt_exprs(x, f);
84            }
85            if let Some(c) = cond {
86                f(c);
87            }
88            for u in update.iter_mut() {
89                f(u);
90            }
91            walk_stmt_exprs(body, f);
92        }
93        Stmt::ForEach { iterable, body, .. } => {
94            f(iterable);
95            walk_stmt_exprs(body, f);
96        }
97        Stmt::Switch {
98            selector,
99            cases,
100            default,
101            ..
102        } => {
103            f(selector);
104            for c in cases {
105                for x in c.body.iter_mut() {
106                    walk_stmt_exprs(x, f);
107                }
108                if let Some(g) = &mut c.guard {
109                    f(g);
110                }
111            }
112            if let Some(d) = default {
113                walk_stmt_exprs(d, f);
114            }
115        }
116        Stmt::Try {
117            body,
118            catches,
119            finally,
120        } => {
121            walk_stmt_exprs(body, f);
122            for c in catches {
123                walk_stmt_exprs(&mut c.body, f);
124            }
125            if let Some(fl) = finally {
126                walk_stmt_exprs(fl, f);
127            }
128        }
129        Stmt::TryWithResources {
130            resources,
131            body,
132            catches,
133            finally,
134        } => {
135            for x in resources.iter_mut() {
136                walk_stmt_exprs(x, f);
137            }
138            walk_stmt_exprs(body, f);
139            for c in catches {
140                walk_stmt_exprs(&mut c.body, f);
141            }
142            if let Some(fl) = finally {
143                walk_stmt_exprs(fl, f);
144            }
145        }
146        Stmt::Assert { cond, msg } => {
147            f(cond);
148            if let Some(m) = msg {
149                f(m);
150            }
151        }
152        Stmt::Synchronized { lock, body } => {
153            f(lock);
154            walk_stmt_exprs(body, f);
155        }
156        Stmt::TernaryValue { e } => f(e),
157        Stmt::Labeled { body, .. } => walk_stmt_exprs(body, f),
158        Stmt::Goto(_) | Stmt::Label(_) | Stmt::Break(_) | Stmt::Continue(_) => {}
159        _ => {}
160    }
161}
162
163/// Visit every expression (immutable).
164fn visit_exprs<F: FnMut(&Expr)>(e: &Expr, f: &mut F) {
165    f(e);
166    for_each_child(e, &mut |c| visit_exprs(c, f));
167}
168
169fn for_each_child<F: FnMut(&Expr)>(e: &Expr, f: &mut F) {
170    match e {
171        Expr::Un { e, .. } | Expr::Cast { e, .. } | Expr::InstanceOf { e, .. } => f(e),
172        Expr::Bin { l, r, .. } => {
173            f(l);
174            f(r);
175        }
176        Expr::Cond { c, t, f: fe } => {
177            f(c);
178            f(t);
179            f(fe);
180        }
181        Expr::Assign { target, value, .. } => {
182            f(target);
183            f(value);
184        }
185        Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => f(e),
186        Expr::Field { owner, .. } => {
187            if let Some(o) = owner {
188                f(o);
189            }
190        }
191        Expr::Method { owner, args, .. } => {
192            if let Some(o) = owner {
193                f(o);
194            }
195            for a in args {
196                f(a);
197            }
198        }
199        Expr::ArrayIndex { array, index } => {
200            f(array);
201            f(index);
202        }
203        Expr::New { args, .. } => {
204            for a in args {
205                f(a);
206            }
207        }
208        Expr::NewArray { dims, init, .. } => {
209            for d in dims {
210                f(d);
211            }
212            if let Some(v) = init {
213                for x in v {
214                    f(x);
215                }
216            }
217        }
218        Expr::NewMultiArray { dims, .. } => {
219            for d in dims {
220                f(d);
221            }
222        }
223        Expr::StringConcat(parts) => {
224            for p in parts {
225                if let ConcatPart::Str(x) = p {
226                    f(x);
227                }
228            }
229        }
230        Expr::Invokedynamic { args, .. } => {
231            for a in args {
232                f(a);
233            }
234        }
235        Expr::AnonNew { args, .. } => {
236            for a in args {
237                f(a);
238            }
239        }
240        _ => {}
241    }
242}
243
244/// Mutable child walk (one level).
245fn for_each_child_mut<F: FnMut(&mut Expr)>(e: &mut Expr, f: &mut F) {
246    match e {
247        Expr::Un { e, .. } | Expr::Cast { e, .. } | Expr::InstanceOf { e, .. } => f(e),
248        Expr::Bin { l, r, .. } => {
249            f(l);
250            f(r);
251        }
252        Expr::Cond { c, t, f: fe } => {
253            f(c);
254            f(t);
255            f(fe);
256        }
257        Expr::Assign { target, value, .. } => {
258            f(target);
259            f(value);
260        }
261        Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => f(e),
262        Expr::Field { owner, .. } => {
263            if let Some(o) = owner {
264                f(o);
265            }
266        }
267        Expr::Method { owner, args, .. } => {
268            if let Some(o) = owner {
269                f(o);
270            }
271            for a in args.iter_mut() {
272                f(a);
273            }
274        }
275        Expr::ArrayIndex { array, index } => {
276            f(array);
277            f(index);
278        }
279        Expr::New { args, .. } => {
280            for a in args.iter_mut() {
281                f(a);
282            }
283        }
284        Expr::NewArray { dims, init, .. } => {
285            for d in dims.iter_mut() {
286                f(d);
287            }
288            if let Some(v) = init {
289                for x in v.iter_mut() {
290                    f(x);
291                }
292            }
293        }
294        Expr::NewMultiArray { dims, .. } => {
295            for d in dims.iter_mut() {
296                f(d);
297            }
298        }
299        Expr::StringConcat(parts) => {
300            for p in parts {
301                if let ConcatPart::Str(x) = p {
302                    f(x);
303                }
304            }
305        }
306        Expr::Invokedynamic { args, .. } => {
307            for a in args.iter_mut() {
308                f(a);
309            }
310        }
311        Expr::AnonNew { args, .. } => {
312            for a in args.iter_mut() {
313                f(a);
314            }
315        }
316        _ => {}
317    }
318}
319
320/// Deep mutable expression rewrite.
321pub(crate) fn deep_rewrite<F: FnMut(&mut Expr)>(e: &mut Expr, f: &mut F) {
322    f(e);
323    for_each_child_mut(e, &mut |c| deep_rewrite(c, f));
324}
325
326/// `deep_rewrite` that respects WRITE positions: a `Local` appearing as
327/// an assignment target or a `++`/`--` operand never reaches `f`.
328/// Value-forwarding closures (`*x = value`) must use this — plain
329/// deep_rewrite turned a forwarded `vX = 25` into `25 = 25` (target
330/// replaced, and drop_defs then no longer recognized the statement, so
331/// the garbage survived into the output; reqable a4/e). Compound
332/// targets (`a[i] = …`, `o.f = …`) still recurse: owners and indices
333/// ARE reads.
334fn deep_rewrite_reads<F: FnMut(&mut Expr)>(e: &mut Expr, f: &mut F) {
335    f(e);
336    match e {
337        Expr::Assign { target, value, .. } => {
338            if !matches!(**target, Expr::Local { .. }) {
339                deep_rewrite_reads(target, f);
340            }
341            deep_rewrite_reads(value, f);
342        }
343        Expr::PreIncDec { e: inner, .. } | Expr::PostIncDec { e: inner, .. } => {
344            if !matches!(**inner, Expr::Local { .. }) {
345                deep_rewrite_reads(inner, f);
346            }
347        }
348        _ => for_each_child_mut(e, &mut |c| deep_rewrite_reads(c, f)),
349    }
350}
351
352fn collect_vars(e: &Expr, out: &mut HashSet<u32>) {
353    visit_exprs(e, &mut |x| {
354        if let Expr::Local { var, .. } = x {
355            out.insert(*var);
356        }
357    });
358}
359
360/// Collect variable references (reads; `assignments` adds assignment
361/// targets). Single-pass recursion — the previous shape wrapped a
362/// self-recursive closure in `walk_all`, visiting every subtree twice over
363/// (exponential in nesting depth).
364fn stmt_collect_vars(s: &Stmt, out: &mut HashSet<u32>, assignments: bool) {
365    match s {
366        Stmt::Block(v) => {
367            for x in v {
368                stmt_collect_vars(x, out, assignments);
369            }
370        }
371        Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
372            if let Expr::Local { var, .. } = &**target {
373                if assignments {
374                    out.insert(*var);
375                }
376            } else {
377                collect_vars(target, out);
378            }
379            collect_vars(value, out);
380        }
381        Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
382            collect_vars(e, out)
383        }
384        Stmt::Return(Some(e)) => collect_vars(e, out),
385        Stmt::LocalDef { var, init, .. } => {
386            if assignments {
387                out.insert(*var);
388            }
389            if let Some(e) = init {
390                collect_vars(e, out);
391            }
392        }
393        Stmt::If {
394            cond,
395            then_stmt,
396            else_stmt,
397        } => {
398            collect_vars(cond, out);
399            stmt_collect_vars(then_stmt, out, assignments);
400            if let Some(e) = else_stmt {
401                stmt_collect_vars(e, out, assignments);
402            }
403        }
404        Stmt::While { cond, body } => {
405            collect_vars(cond, out);
406            stmt_collect_vars(body, out, assignments);
407        }
408        Stmt::DoWhile { body, cond } => {
409            stmt_collect_vars(body, out, assignments);
410            collect_vars(cond, out);
411        }
412        Stmt::For {
413            init,
414            cond,
415            update,
416            body,
417        } => {
418            for x in init {
419                stmt_collect_vars(x, out, assignments);
420            }
421            if let Some(c) = cond {
422                collect_vars(c, out);
423            }
424            for u in update {
425                collect_vars(u, out);
426            }
427            stmt_collect_vars(body, out, assignments);
428        }
429        Stmt::ForEach {
430            var,
431            iterable,
432            body,
433            ..
434        } => {
435            if assignments {
436                out.insert(*var);
437            }
438            collect_vars(iterable, out);
439            stmt_collect_vars(body, out, assignments);
440        }
441        Stmt::Switch {
442            selector,
443            cases,
444            default,
445            ..
446        } => {
447            collect_vars(selector, out);
448            for c in cases {
449                for x in &c.body {
450                    stmt_collect_vars(x, out, assignments);
451                }
452            }
453            if let Some(d) = default {
454                stmt_collect_vars(d, out, assignments);
455            }
456        }
457        Stmt::Try {
458            body,
459            catches,
460            finally,
461        } => {
462            stmt_collect_vars(body, out, assignments);
463            for c in catches {
464                stmt_collect_vars(&c.body, out, assignments);
465            }
466            if let Some(f) = finally {
467                stmt_collect_vars(f, out, assignments);
468            }
469        }
470        Stmt::TryWithResources {
471            resources,
472            body,
473            catches,
474            finally,
475        } => {
476            for x in resources {
477                stmt_collect_vars(x, out, assignments);
478            }
479            stmt_collect_vars(body, out, assignments);
480            for c in catches {
481                stmt_collect_vars(&c.body, out, assignments);
482            }
483            if let Some(f) = finally {
484                stmt_collect_vars(f, out, assignments);
485            }
486        }
487        Stmt::Synchronized { lock, body } => {
488            collect_vars(lock, out);
489            stmt_collect_vars(body, out, assignments);
490        }
491        Stmt::Assert { cond, msg } => {
492            collect_vars(cond, out);
493            if let Some(m) = msg {
494                collect_vars(m, out);
495            }
496        }
497        Stmt::Labeled { body, .. } => stmt_collect_vars(body, out, assignments),
498        _ => {}
499    }
500}
501
502fn walk_all<F: FnMut(&Stmt)>(s: &Stmt, f: &mut F) {
503    f(s);
504    match s {
505        Stmt::Block(v) => {
506            for x in v {
507                walk_all(x, f);
508            }
509        }
510        Stmt::If {
511            then_stmt,
512            else_stmt,
513            ..
514        } => {
515            walk_all(then_stmt, f);
516            if let Some(e) = else_stmt {
517                walk_all(e, f);
518            }
519        }
520        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => walk_all(body, f),
521        Stmt::For { init, body, .. } => {
522            for x in init {
523                walk_all(x, f);
524            }
525            walk_all(body, f);
526        }
527        Stmt::ForEach { body, .. } => walk_all(body, f),
528        Stmt::Switch { cases, default, .. } => {
529            for c in cases {
530                for x in &c.body {
531                    walk_all(x, f);
532                }
533            }
534            if let Some(d) = default {
535                walk_all(d, f);
536            }
537        }
538        Stmt::Try {
539            body,
540            catches,
541            finally,
542        } => {
543            walk_all(body, f);
544            for c in catches {
545                walk_all(&c.body, f);
546            }
547            if let Some(fl) = finally {
548                walk_all(fl, f);
549            }
550        }
551        Stmt::TryWithResources {
552            resources,
553            body,
554            catches,
555            finally,
556        } => {
557            for x in resources {
558                walk_all(x, f);
559            }
560            walk_all(body, f);
561            for c in catches {
562                walk_all(&c.body, f);
563            }
564            if let Some(fl) = finally {
565                walk_all(fl, f);
566            }
567        }
568        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => walk_all(body, f),
569        _ => {}
570    }
571}
572
573// ---------------------------------------------------------------------------
574// Passes
575// ---------------------------------------------------------------------------
576
577/// Bind catch parameters: the handler's first statement (a bare LocalDef
578/// from move-exception) becomes the catch variable.
579pub fn bind_catches(s: &mut Stmt, vt: &mut VarTable) {
580    // Vars defined anywhere in the method (params + LocalDefs +
581    // assign targets): a catch body reading a var OUTSIDE this set is
582    // the unmaterialized move-exception register (the lifter minted it
583    // without a defining statement).
584    let mut defined: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
585    for v in &vt.vars {
586        if v.is_param {
587            defined.insert(v.id);
588        }
589    }
590    collect_defined_locals(s, &mut defined);
591    let reads_all = count_locals_stmts(std::slice::from_ref(s));
592    // Vars with a real assignment (LocalDef-with-init or assign target):
593    // a var only ever READ is either the unmaterialized move-exception
594    // register or a bare declaration awaiting its hoisted assignment.
595    let mut assigned: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
596    collect_assigned_locals(s, &mut assigned);
597    let mut fallback_bound: Vec<u32> = Vec::new();
598    bind_catches_walk(s, vt, &defined, &reads_all, &assigned, &mut fallback_bound);
599    // The catch parameter IS the declaration now: drop the bare
600    // `Throwable th;` hoists for vars the fallback bound.
601    if !fallback_bound.is_empty() {
602        remove_bare_decls(s, &fallback_bound);
603    }
604}
605
606fn collect_defined_locals(s: &Stmt, out: &mut jdc_core::FxHashSet<u32>) {
607    crate::passes::walk_all(s, &mut |st| {
608        match st {
609            Stmt::LocalDef { var, .. } => {
610                out.insert(*var);
611            }
612            Stmt::ExprStmt(Expr::Assign { target, .. }) => {
613                if let Expr::Local { var, .. } = &**target {
614                    out.insert(*var);
615                }
616            }
617            _ => {}
618        }
619    });
620}
621
622
623/// First read of a local that is defined nowhere in the method (and is
624/// not a parameter) inside the catch body — the unmaterialized
625/// move-exception register.
626/// Vars carrying a real assignment (init or write target).
627fn collect_assigned_locals(s: &Stmt, out: &mut jdc_core::FxHashSet<u32>) {
628    crate::passes::walk_all(s, &mut |st| {
629        match st {
630            Stmt::LocalDef { var, init: Some(_), .. } => {
631                out.insert(*var);
632            }
633            Stmt::ExprStmt(Expr::Assign { target, .. }) => {
634                if let Expr::Local { var, .. } = &**target {
635                    out.insert(*var);
636                }
637            }
638            _ => {}
639        }
640    });
641}
642
643/// First `throw <local>` var in the catch body that is never assigned
644/// anywhere and is exception-typed — the unmaterialized move-exception.
645fn first_thrown_unassigned_local(
646    body: &Stmt,
647    vt: &VarTable,
648    assigned: &jdc_core::FxHashSet<u32>,
649) -> Option<u32> {
650    let mut found: Option<u32> = None;
651    let c = body.clone();
652    walk_all(&c, &mut |st| {
653        if let Stmt::Throw(th) = st {
654            if let Expr::Local { var, .. } = th {
655                let v = *var;
656                if !assigned.contains(&v) {
657                    if let JavaType::Object(o) = vt.var(v).ty.erased() {
658                        if o.as_ref() == "java/lang/Throwable" {
659                            found = Some(v);
660                        }
661                    }
662                }
663            }
664        }
665    });
666    found
667}
668
669/// Remove bare `LocalDef{var, init: None}` declarations for the given
670/// vars (the catch parameter is their declaration now).
671fn remove_bare_decls(s: &mut Stmt, vars: &[u32]) {
672    let vars: jdc_core::FxHashSet<u32> = vars.iter().copied().collect();
673    walk_mut_deep(s, &mut |st| {
674        if let Stmt::Block(v) = st {
675            v.retain(|x| {
676                !matches!(x, Stmt::LocalDef { var, init: None, .. } if vars.contains(var))
677            });
678        }
679    });
680}
681
682fn first_undefined_local_read(
683    body: &Stmt,
684    defined: &jdc_core::FxHashSet<u32>,
685) -> Option<u32> {
686    let mut found: Option<u32> = None;
687    let mut c = body.clone();
688    crate::passes::walk_stmt_exprs(&mut c, &mut |e| {
689        if found.is_none() {
690            deep_rewrite(e, &mut |x| {
691                if found.is_none() {
692                    if let Expr::Local { var, .. } = x {
693                        if !defined.contains(var) {
694                            found = Some(*var);
695                        }
696                    }
697                }
698            });
699        }
700    });
701    found
702}
703
704#[allow(clippy::too_many_arguments)]
705fn bind_catches_walk(
706    s: &mut Stmt,
707    vt: &mut VarTable,
708    defined: &jdc_core::FxHashSet<u32>,
709    reads_all: &std::collections::HashMap<u32, usize>,
710    assigned: &jdc_core::FxHashSet<u32>,
711    fallback_bound: &mut Vec<u32>,
712) {
713    match s {
714        Stmt::Try {
715            body,
716            catches,
717            finally,
718        } => {
719            bind_catches_walk(body, vt, defined, reads_all, assigned, fallback_bound);
720            for c in catches.iter_mut() {
721                if c.var == u32::MAX {
722                    let stored = match c.body.as_ref() {
723                        Stmt::Block(v) => match v.first() {
724                            Some(Stmt::LocalDef { var, .. }) => Some(*var),
725                            Some(Stmt::ExprStmt(Expr::Assign { target, .. })) => match &**target {
726                                Expr::Local { var, .. } => Some(*var),
727                                _ => None,
728                            },
729                            _ => None,
730                        },
731                        _ => None,
732                    };
733                    if let Some(v) = stored {
734                        let exc = c
735                            .exc
736                            .first()
737                            .cloned()
738                            .unwrap_or_else(|| "java/lang/Throwable".into());
739                        let slot = vt.var(v).slot;
740                        let name = "e".to_string();
741                        let new_var =
742                            vt.add_catch_var(slot, name, TypeRef::J(JavaType::Object(exc)));
743                        if let Stmt::Block(vs) = c.body.as_mut() {
744                            vs.remove(0);
745                        }
746                        rewrite_local_refs(c.body.as_mut(), v, new_var);
747                        c.var = new_var;
748                    } else if let Some(v) = first_undefined_local_read(c.body.as_ref(), defined)
749                        .filter(|v| {
750                            // Bind only a var read NOWHERE outside this
751                            // catch: ensure_declared would hoist an
752                            // outside-read var to a method local, and
753                            // consuming it as the catch parameter would
754                            // scope it too narrowly.
755                            let in_catch = count_locals_stmts(std::slice::from_ref(c.body.as_ref()));
756                            reads_all.get(v).copied().unwrap_or(0)
757                                == in_catch.get(v).copied().unwrap_or(0)
758                        })
759                        .filter(|v| {
760                            // And only an EXCEPTION-typed var: the
761                            // move-exception registers carry the handler
762                            // type — a plain local pending its hoisted
763                            // declaration (StringBuilder sb) must not be
764                            // consumed as the catch parameter (reqable
765                            // amazon: the declaration vanished and its
766                            // outer readers broke).
767                            let ty = vt.var(*v).ty.erased();
768                            let exc = c
769                                .exc
770                                .first()
771                                .cloned()
772                                .unwrap_or_else(|| "java/lang/Throwable".into());
773                            matches!(&ty, JavaType::Object(o)
774                                if o.as_ref() == exc.as_ref()
775                                    || o.as_ref() == "java/lang/Throwable")
776                        })
777                    {
778                        // The handler's first statement is not the
779                        // move-exception def (the d8 synchronized pattern
780                        // leads with MonitorExit): the register the lifter
781                        // minted for move-exception is still READ in the
782                        // body (`throw th;`) with no defining statement
783                        // anywhere. Bind IT as the catch parameter —
784                        // otherwise emit printed `catch (Throwable ignored)`
785                        // over an undeclared `throw th` (definite-assignment
786                        // failure, 6.4k weibo sites).
787                        c.var = v;
788                        fallback_bound.push(v);
789                    } else if let Some(v) =
790                        first_thrown_unassigned_local(c.body.as_ref(), vt, assigned)
791                            .filter(|v| {
792                                let in_catch =
793                                    count_locals_stmts(std::slice::from_ref(c.body.as_ref()));
794                                reads_all.get(v).copied().unwrap_or(0)
795                                    == in_catch.get(v).copied().unwrap_or(0)
796                            })
797                            .filter(|v| {
798                                // Same exception-type gate: the hoisted
799                                // `Throwable th;` declares the var but the
800                                // move-exception never assigned it.
801                                let ty = vt.var(*v).ty.erased();
802                                let exc = c
803                                    .exc
804                                    .first()
805                                    .cloned()
806                                    .unwrap_or_else(|| "java/lang/Throwable".into());
807                                matches!(&ty, JavaType::Object(o)
808                                    if o.as_ref() == exc.as_ref()
809                                        || o.as_ref() == "java/lang/Throwable")
810                            })
811                    {
812                        // Declared-never-assigned: `Throwable th;` hoisted
813                        // by an earlier phase, the catch body's `throw th`
814                        // its only use — the move-exception assignment was
815                        // never materialized. Bind th as the catch
816                        // parameter and drop the bare declaration.
817                        c.var = v;
818                        fallback_bound.push(v);
819                    }
820                }
821                bind_catches_walk(&mut c.body, vt, defined, reads_all, assigned, fallback_bound);
822            }
823            if let Some(f) = finally {
824                bind_catches_walk(f.as_mut(), vt, defined, reads_all, assigned, fallback_bound);
825            }
826        }
827        Stmt::Block(v) => {
828            for x in v.iter_mut() {
829                bind_catches_walk(x, vt, defined, reads_all, assigned, fallback_bound);
830            }
831        }
832        Stmt::If {
833            then_stmt,
834            else_stmt,
835            ..
836        } => {
837            bind_catches_walk(then_stmt, vt, defined, reads_all, assigned, fallback_bound);
838            if let Some(e) = else_stmt {
839                bind_catches_walk(e, vt, defined, reads_all, assigned, fallback_bound);
840            }
841        }
842        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => bind_catches_walk(body, vt, defined, reads_all, assigned, fallback_bound),
843        Stmt::For { init, body, .. } => {
844            for x in init.iter_mut() {
845                bind_catches_walk(x, vt, defined, reads_all, assigned, fallback_bound);
846            }
847            bind_catches_walk(body, vt, defined, reads_all, assigned, fallback_bound);
848        }
849        Stmt::ForEach { body, .. }
850        | Stmt::Labeled { body, .. }
851        | Stmt::Synchronized { body, .. } => bind_catches_walk(body, vt, defined, reads_all, assigned, fallback_bound),
852        Stmt::Switch { cases, default, .. } => {
853            for c in cases.iter_mut() {
854                for x in c.body.iter_mut() {
855                    bind_catches_walk(x, vt, defined, reads_all, assigned, fallback_bound);
856                }
857            }
858            if let Some(d) = default {
859                bind_catches_walk(d, vt, defined, reads_all, assigned, fallback_bound);
860            }
861        }
862        _ => {}
863    }
864}
865
866fn rewrite_local_refs(s: &mut Stmt, from: u32, to: u32) {
867    rewrite_exprs(s, &mut |e| {
868        deep_rewrite(e, &mut |x| {
869            if let Expr::Local { var, .. } = x {
870                if *var == from {
871                    *var = to;
872                }
873            }
874        });
875    });
876}
877
878/// Flatten blocks, drop empty ones and trailing no-op statements.
879pub fn cleanup(s: &mut Stmt) {
880    jdc_core::ir::stmt::flatten(s);
881    strip_empty(s);
882    merge_singleton_stmts(s);
883}
884
885fn strip_empty(s: &mut Stmt) {
886    match s {
887        Stmt::Block(v) => {
888            v.retain(|x| !x.is_empty_block());
889            for x in v.iter_mut() {
890                strip_empty(x);
891            }
892        }
893        Stmt::If {
894            then_stmt,
895            else_stmt,
896            ..
897        } => {
898            strip_empty(then_stmt);
899            if let Some(e) = else_stmt {
900                strip_empty(e);
901                if e.is_empty_block() {
902                    *else_stmt = None;
903                }
904            }
905        }
906        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => strip_empty(body),
907        Stmt::For { init, body, .. } => {
908            init.retain(|x| !x.is_empty_block());
909            strip_empty(body);
910        }
911        Stmt::ForEach { body, .. } => strip_empty(body),
912        Stmt::Switch { cases, default, .. } => {
913            for c in cases {
914                c.body.retain(|x| !x.is_empty_block());
915            }
916            if let Some(d) = default {
917                strip_empty(d);
918            }
919        }
920        Stmt::Try {
921            body,
922            catches,
923            finally,
924        } => {
925            strip_empty(body);
926            for c in catches {
927                strip_empty(&mut c.body);
928            }
929            if let Some(f) = finally {
930                strip_empty(f);
931            }
932        }
933        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => strip_empty(body),
934        _ => {}
935    }
936}
937
938fn merge_singleton_stmts(s: &mut Stmt) {
939    if let Stmt::Block(v) = s {
940        for x in v.iter_mut() {
941            merge_singleton_stmts(x);
942        }
943    }
944}
945
946/// Drop statements after a definite terminator within a block.
947pub fn prune_unreachable(s: &mut Stmt) {
948    if let Stmt::Block(v) = s {
949        let mut cut: Option<usize> = None;
950        for (i, x) in v.iter().enumerate() {
951            if matches!(x, Stmt::Return(_) | Stmt::Throw(_)) {
952                cut = Some(i + 1);
953                break;
954            }
955        }
956        if let Some(c) = cut {
957            if c < v.len() {
958                v.truncate(c);
959            }
960        }
961        for x in v.iter_mut() {
962            prune_unreachable(x);
963        }
964    } else {
965        walk_mut(s, &mut prune_unreachable);
966    }
967}
968
969fn walk_mut<F: FnMut(&mut Stmt)>(s: &mut Stmt, f: &mut F) {
970    match s {
971        Stmt::Block(v) => {
972            for x in v.iter_mut() {
973                f(x);
974            }
975        }
976        Stmt::If {
977            then_stmt,
978            else_stmt,
979            ..
980        } => {
981            f(then_stmt);
982            if let Some(e) = else_stmt {
983                f(e);
984            }
985        }
986        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => f(body),
987        Stmt::For { init, body, .. } => {
988            for x in init.iter_mut() {
989                f(x);
990            }
991            f(body);
992        }
993        Stmt::ForEach { body, .. } => f(body),
994        Stmt::Switch { cases, default, .. } => {
995            for c in cases {
996                for x in c.body.iter_mut() {
997                    f(x);
998                }
999            }
1000            if let Some(d) = default {
1001                f(d);
1002            }
1003        }
1004        Stmt::Try {
1005            body,
1006            catches,
1007            finally,
1008        } => {
1009            f(body);
1010            for c in catches {
1011                f(&mut c.body);
1012            }
1013            if let Some(fl) = finally {
1014                f(fl);
1015            }
1016        }
1017        Stmt::TryWithResources {
1018            resources,
1019            body,
1020            catches,
1021            finally,
1022        } => {
1023            for r in resources.iter_mut() {
1024                f(r);
1025            }
1026            f(body);
1027            for c in catches {
1028                f(&mut c.body);
1029            }
1030            if let Some(fl) = finally {
1031                f(fl);
1032            }
1033        }
1034        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => f(body),
1035        _ => {}
1036    }
1037}
1038
1039/// Count LocalDef statements (pipeline diagnostics).
1040pub fn count_localdefs(s: &Stmt, out: &mut usize) {
1041    if matches!(s, Stmt::LocalDef { .. }) {
1042        *out += 1;
1043    }
1044    walk_all(s, &mut |x| {
1045        if matches!(x, Stmt::LocalDef { .. }) {
1046            *out += 1;
1047        }
1048    });
1049}
1050
1051/// Drop a trailing `return;` (d8's explicit return-void at method end).
1052pub fn strip_trailing_void_return(s: &mut Stmt) {
1053    if let Stmt::Block(v) = s {
1054        loop {
1055            match v.last_mut() {
1056                Some(Stmt::Return(None)) => {
1057                    v.pop();
1058                }
1059                // The structurer can close a region in a bare nested
1060                // block; a `return;` at its tail is still a dangling
1061                // statement (inside a static initializer it is illegal
1062                // outright — clinit's closing return).
1063                Some(Stmt::Block(_)) => {
1064                    let last = v.last_mut().unwrap();
1065                    let before = match last {
1066                        Stmt::Block(b) => b.len(),
1067                        _ => 0,
1068                    };
1069                    strip_trailing_void_return(last);
1070                    let after = match last {
1071                        Stmt::Block(b) => b.len(),
1072                        _ => 0,
1073                    };
1074                    if after == before {
1075                        break; // nothing stripped inside
1076                    }
1077                }
1078                _ => break,
1079            }
1080        }
1081    }
1082}
1083
1084/// `if (c) { } else { B }` → `if (!c) { B }` — the empty-then shape is
1085/// how the structurer lands an inverted diamond, and source never writes
1086/// it (jadx prints the positive form). negate() carries De Morgan so
1087/// composed conditions stay clean.
1088pub fn invert_empty_thens(s: &mut Stmt) {
1089    walk_mut_deep(s, &mut |st| {
1090        if let Stmt::If {
1091            cond,
1092            then_stmt,
1093            else_stmt,
1094        } = st
1095        {
1096            let then_empty = matches!(&**then_stmt, Stmt::Block(v) if v.is_empty());
1097            if then_empty {
1098                if let Some(els) = else_stmt.take() {
1099                    let c = std::mem::replace(cond, Expr::Const(ConstVal::Int(0)));
1100                    *cond = jdc_core::convert::negate(c);
1101                    *then_stmt = els;
1102                }
1103            }
1104        }
1105    });
1106}
1107
1108/// Fold if-diamonds back into short-circuit conditions (DAD's
1109/// short_circuit_struct, statement level). The structurer emits nested
1110/// ifs for `a && b` / `a || b` bytecode diamonds; javac source almost
1111/// never nests them, and jadx folds all four shapes:
1112///   if (c1) { if (c2) {T} else {F} } else {F}  →  if (c1 && c2) {T} else {F}
1113///   if (c1) { if (c2) {F} else {T} } else {F}  →  if (c1 && !c2) {T} else {F}
1114///   if (c1) {T} else { if (c2) {T} else {F} }  →  if (c1 || c2) {T} else {F}
1115///   if (c1) {T} else { if (c2) {F} else {T} }  →  if (c1 || !c2) {T} else {F}
1116/// Branch identity is deep structural equality (Stmt: PartialEq);
1117/// iterate to a fixpoint so chained diamonds `(a && b) && c` collapse.
1118pub fn fold_short_circuits(s: &mut Stmt) {
1119    for _ in 0..8 {
1120        let mut changed = false;
1121        walk_mut_deep(s, &mut |st| {
1122            if try_fold_diamond(st) {
1123                changed = true;
1124            }
1125        });
1126        if !changed {
1127            break;
1128        }
1129    }
1130}
1131
1132fn try_fold_diamond(st: &mut Stmt) -> bool {
1133    enum Shape {
1134        ThenAnd,
1135        ThenAndNot,
1136        ElseOr,
1137        ElseOrNot,
1138        BareAnd,
1139        BareAndNot,
1140    }
1141    // Probe the shape on an immutable borrow first (the fold takes the
1142    // whole If apart; matching and rebuilding inside one borrow is not
1143    // expressible).
1144    let shape = match &*st {
1145        // One-sided diamonds: the shared branch is the implicit empty
1146        // fall-through (`if (c1) { if (c2) {T} }` → `if (c1 && c2) {T}`).
1147        Stmt::If {
1148            then_stmt,
1149            else_stmt: None,
1150            ..
1151        } => match &**then_stmt {
1152            Stmt::If {
1153                then_stmt: t2,
1154                else_stmt: None,
1155                ..
1156            } if !matches!(&**t2, Stmt::Block(v) if v.is_empty()) => Some(Shape::BareAnd),
1157            Stmt::If {
1158                then_stmt: t2,
1159                else_stmt: Some(_),
1160                ..
1161            } if matches!(&**t2, Stmt::Block(v) if v.is_empty()) => Some(Shape::BareAndNot),
1162            _ => None,
1163        },
1164        Stmt::If {
1165            then_stmt,
1166            else_stmt: Some(els),
1167            ..
1168        } => match (&**then_stmt, &**els) {
1169            (Stmt::If { else_stmt: Some(f2), .. }, _) if f2.as_ref() == els.as_ref() => {
1170                Some(Shape::ThenAnd)
1171            }
1172            (Stmt::If { then_stmt: t2, else_stmt: Some(_), .. }, _)
1173                if t2.as_ref() == els.as_ref() =>
1174            {
1175                Some(Shape::ThenAndNot)
1176            }
1177            (_, Stmt::If { then_stmt: t2, else_stmt: Some(_), .. })
1178                if t2.as_ref() == then_stmt.as_ref() =>
1179            {
1180                Some(Shape::ElseOr)
1181            }
1182            (_, Stmt::If { else_stmt: Some(f2), .. })
1183                if f2.as_ref() == then_stmt.as_ref() =>
1184            {
1185                Some(Shape::ElseOrNot)
1186            }
1187            _ => None,
1188        },
1189        _ => None,
1190    };
1191    let Some(shape) = shape else {
1192        return false;
1193    };
1194
1195    let taken = std::mem::replace(st, Stmt::Block(vec![]));
1196    // Bare (no-else) diamonds fold without an else at all.
1197    if matches!(shape, Shape::BareAnd | Shape::BareAndNot) {
1198        let Stmt::If {
1199            cond: c1,
1200            then_stmt,
1201            else_stmt: None,
1202        } = taken
1203        else {
1204            unreachable!()
1205        };
1206        let Stmt::If {
1207            cond: c2,
1208            then_stmt: t2,
1209            else_stmt: f2,
1210        } = *then_stmt
1211        else {
1212            unreachable!()
1213        };
1214        let sc = |op: BinOp, l: Expr, r: Expr| Expr::Bin {
1215            op,
1216            l: Box::new(l),
1217            r: Box::new(r),
1218            ty: None,
1219        };
1220        match shape {
1221            // if (c1 && c2) {T}
1222            Shape::BareAnd => {
1223                *st = Stmt::If {
1224                    cond: sc(BinOp::LogAnd, c1, c2),
1225                    then_stmt: t2,
1226                    else_stmt: None,
1227                };
1228            }
1229            // if (c1) { if (c2) {} else {T} }  →  if (c1 && !c2) {T}
1230            _ => {
1231                let body = f2.unwrap();
1232                *st = Stmt::If {
1233                    cond: sc(
1234                        BinOp::LogAnd,
1235                        c1,
1236                        Expr::Un {
1237                            op: UnOp::Not,
1238                            e: Box::new(c2),
1239                        },
1240                    ),
1241                    then_stmt: body,
1242                    else_stmt: None,
1243                };
1244            }
1245        }
1246        return true;
1247    }
1248    let Stmt::If {
1249        cond: c1,
1250        then_stmt,
1251        else_stmt: Some(els),
1252    } = taken
1253    else {
1254        unreachable!("shape probe guaranteed an If with else")
1255    };
1256    let sc = |op: BinOp, l: Expr, r: Expr| Expr::Bin {
1257        op,
1258        l: Box::new(l),
1259        r: Box::new(r),
1260        ty: None,
1261    };
1262    let not = |e: Expr| Expr::Un {
1263        op: UnOp::Not,
1264        e: Box::new(e),
1265    };
1266    match shape {
1267        Shape::BareAnd | Shape::BareAndNot => unreachable!("handled above"),
1268        Shape::ThenAnd | Shape::ThenAndNot => {
1269            let Stmt::If {
1270                cond: c2,
1271                then_stmt: t2,
1272                else_stmt: Some(f2),
1273            } = *then_stmt
1274            else {
1275                unreachable!()
1276            };
1277            let (nc, body, alt) = match shape {
1278                // if (c1 && c2) {T} else {F}
1279                Shape::ThenAnd => (sc(BinOp::LogAnd, c1, c2), t2, f2),
1280                // if (c1 && !c2) {T} else {F}   (inner: {F} else {T})
1281                _ => (sc(BinOp::LogAnd, c1, not(c2)), f2, t2),
1282            };
1283            *st = Stmt::If {
1284                cond: nc,
1285                then_stmt: body,
1286                else_stmt: Some(alt),
1287            };
1288        }
1289        Shape::ElseOr | Shape::ElseOrNot => {
1290            let Stmt::If {
1291                cond: c2,
1292                then_stmt: t2,
1293                else_stmt: Some(f2),
1294            } = *els
1295            else {
1296                unreachable!()
1297            };
1298            // The surviving then-body is the OUTER then (identical to the
1299            // matching inner branch by the probe).
1300            let body = then_stmt;
1301            let (nc, alt) = match shape {
1302                // if (c1 || c2) {T} else {F}   (inner: {T} else {F})
1303                Shape::ElseOr => (sc(BinOp::LogOr, c1, c2), f2),
1304                // if (c1 || !c2) {T} else {F}  (inner: {F} else {T})
1305                _ => (sc(BinOp::LogOr, c1, not(c2)), t2),
1306            };
1307            *st = Stmt::If {
1308                cond: nc,
1309                then_stmt: body,
1310                else_stmt: Some(alt),
1311            };
1312        }
1313    }
1314    true
1315}
1316
1317/// A static initializer cannot contain `return` in source form — the
1318/// clinit's closing return is a method-level artifact, but the
1319/// structurer can leave it nested inside loops/branches where the
1320/// top-level strip cannot see it (x2/a: `static { while { ...; return;
1321/// } }` — "return outside method", 184 sites on reqable).
1322pub fn strip_clinit_returns(s: &mut Stmt) {
1323    walk_mut_deep(s, &mut |st| {
1324        if let Stmt::Block(v) = st {
1325            v.retain(|x| !matches!(x, Stmt::Return(None)));
1326        }
1327    });
1328}
1329
1330pub fn prepend_comment(s: &mut Stmt, text: String) {
1331    let stmt = Stmt::Comment(text);
1332    match s {
1333        Stmt::Block(v) => v.insert(0, stmt),
1334        other => {
1335            let inner = std::mem::replace(other, Stmt::Block(vec![]));
1336            *other = Stmt::Block(vec![stmt, inner]);
1337        }
1338    }
1339}
1340
1341/// Fused expression rewrites that each used to walk the whole tree:
1342/// residual cmp sentinels, object-null comparisons, const-first compares.
1343/// One traversal, three rules — the separate walks were a top profile cost.
1344/// True when `e` evaluates to an object reference — a legal null-compare
1345/// side: locals via the reference table, everything else via its own type
1346/// (fields like `pi.versionName != 0` and call results are references too;
1347/// constants never are).
1348fn is_obj_expr(e: &Expr, obj_var: &dyn Fn(u32) -> bool) -> bool {
1349    match e {
1350        Expr::Local { var, .. } => obj_var(*var),
1351        Expr::Const(_) => false,
1352        _ => e.type_ref().erased().is_reference(),
1353    }
1354}
1355
1356/// `obj == 0` / `0 == obj` → `obj == null`: replace the CONST-0 side.
1357/// The previous shape replaced the LOCAL side instead — every object
1358/// null-check in the corpus rendered as `null != 0` (4,598 hits in
1359/// reqable alone) and the real operand vanished from the condition.
1360fn null_side_rewrite(x: &mut Expr, obj_var: &dyn Fn(u32) -> bool) {
1361    if let Expr::Bin { op, l, r, .. } = x {
1362        if !matches!(op, BinOp::Eq | BinOp::Ne) {
1363            return;
1364        }
1365        let l_zero = matches!(&**l, Expr::Const(ConstVal::Int(0)));
1366        let r_zero = matches!(&**r, Expr::Const(ConstVal::Int(0)));
1367        if l_zero && !r_zero && is_obj_expr(r, obj_var) {
1368            **l = Expr::Const(ConstVal::Null);
1369        } else if r_zero && !l_zero && is_obj_expr(l, obj_var) {
1370            **r = Expr::Const(ConstVal::Null);
1371        }
1372    }
1373}
1374
1375pub fn fused_expr_rewrites(s: &mut Stmt, vt: &VarTable) {
1376    // var ids are dense: a byte table beats a HashSet lookup.
1377    let mut obj_vars: Vec<bool> = Vec::with_capacity(vt.vars.len());
1378    for v in &vt.vars {
1379        obj_vars.push(v.ty.erased().is_reference());
1380    }
1381    rewrite_exprs(s, &mut |e| {
1382        deep_rewrite(e, &mut |x| {
1383            // 1. cmp sentinels.
1384            if let Expr::Invokedynamic { name, args, .. } = x {
1385                if name.starts_with('\0') && args.len() == 2 {
1386                    let (cls, ty) = match name.as_str() {
1387                        "\0cmp-long" => ("java/lang/Long", JavaType::Long),
1388                        "\0cmpl-float" | "\0cmpg-float" => ("java/lang/Float", JavaType::Float),
1389                        _ => ("java/lang/Double", JavaType::Double),
1390                    };
1391                    let desc = MethodDescriptor {
1392                        args: vec![ty.clone(), ty],
1393                        ret: JavaType::Int,
1394                    };
1395                    *x = Expr::Method {
1396                        owner: None,
1397                        cls: cls.into(),
1398                        name: "compare".into(),
1399                        desc: std::sync::Arc::new(desc),
1400                        args: args.clone(),
1401                        is_static: true,
1402                        is_interface: false,
1403                        is_special: false,
1404                        is_super: false,
1405                        is_dynamic: false,
1406                        type_args: vec![],
1407                    };
1408                    return;
1409                }
1410            }
1411            // 2. null compares (no any_obj gate: the object side may be
1412            // a field/call whose type is not in the local table).
1413            null_side_rewrite(x, &|v| obj_vars.get(v as usize).copied().unwrap_or(false));
1414            // 3. const-first compares.
1415            if let Expr::Bin { op, l, r, .. } = x {
1416                if matches!(
1417                    op,
1418                    BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
1419                ) {
1420                    let l_const = matches!(&**l, Expr::Const(_));
1421                    let r_var = matches!(&**r, Expr::Local { .. });
1422                    if l_const && r_var {
1423                        std::mem::swap(l, r);
1424                    }
1425                }
1426            }
1427        });
1428    });
1429    // 4. null into reference-typed assignment targets. The register
1430    //    machine stores null as const-0; return/throw/field-store/invoke
1431    //    argument paths were normalized in the lifter, but a null that
1432    //    MATERIALIZES into a local was rendered as `str = 0;` — javac
1433    //    "int cannot be converted to String" (the single biggest error
1434    //    family on real corpora: d8 emits null locals this way after
1435    //    every `x = null` branch).
1436    walk_mut_deep(s, &mut |st| {
1437        if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = st {
1438            let tgt_is_obj = match &**target {
1439                Expr::Local { var, .. } => {
1440                    obj_vars.get(*var as usize).copied().unwrap_or(false)
1441                }
1442                other => other.type_ref().erased().is_reference(),
1443            };
1444            if tgt_is_obj {
1445                if let Expr::Const(ConstVal::Int(0)) = &**value {
1446                    **value = Expr::Const(ConstVal::Null);
1447                }
1448            }
1449        } else if let Stmt::LocalDef { var, init, .. } = st {
1450            if obj_vars.get(*var as usize).copied().unwrap_or(false) {
1451                if let Some(iv) = init {
1452                    if let Expr::Const(ConstVal::Int(0)) = iv {
1453                        *iv = Expr::Const(ConstVal::Null);
1454                    }
1455                }
1456            }
1457        }
1458    });
1459}
1460
1461/// Replace residual cmp sentinels (`\0cmp*` Invokedynamic markers that never
1462/// reached a condition) with library compare calls.
1463#[allow(dead_code)]
1464pub fn desugar_cmp_residuals(s: &mut Stmt) {
1465    rewrite_exprs(s, &mut |e| {
1466        deep_rewrite(e, &mut |x| {
1467            if let Expr::Invokedynamic { name, args, .. } = x {
1468                if name.starts_with('\0') && args.len() == 2 {
1469                    let (cls, ty) = match name.as_str() {
1470                        "\0cmp-long" => ("java/lang/Long", JavaType::Long),
1471                        "\0cmpl-float" | "\0cmpg-float" => ("java/lang/Float", JavaType::Float),
1472                        _ => ("java/lang/Double", JavaType::Double),
1473                    };
1474                    let desc = jdc_core::types::MethodDescriptor {
1475                        args: vec![ty.clone(), ty],
1476                        ret: JavaType::Int,
1477                    };
1478                    *x = Expr::Method {
1479                        owner: None,
1480                        cls: cls.into(),
1481                        name: "compare".into(),
1482                        desc: std::sync::Arc::new(desc),
1483                        args: args.clone(),
1484                        is_static: true,
1485                        is_interface: false,
1486                        is_special: false,
1487                        is_super: false,
1488                        is_dynamic: false,
1489                        type_args: vec![],
1490                    };
1491                }
1492            }
1493        });
1494    });
1495}
1496
1497/// `if (c) { x = A; } else { x = B; }` → `x = c ? A : B;`
1498pub fn ternary_fold(s: &mut Stmt) {
1499    let mut changed = true;
1500    let mut guard = 0;
1501    while changed && guard < 8 {
1502        changed = false;
1503        guard += 1;
1504        ternary_fold_walk(s, &mut changed);
1505        strip_empty(s);
1506    }
1507}
1508
1509fn ternary_fold_walk(s: &mut Stmt, changed: &mut bool) {
1510    if let Stmt::Block(v) = s {
1511        for x in v.iter_mut() {
1512            ternary_fold_walk(x, changed);
1513        }
1514        return;
1515    }
1516    let Stmt::If {
1517        cond,
1518        then_stmt,
1519        else_stmt,
1520    } = s
1521    else {
1522        return;
1523    };
1524    ternary_fold_walk(then_stmt, changed);
1525    if let Some(e) = else_stmt {
1526        ternary_fold_walk(e, changed);
1527    }
1528    let Some(else_b) = else_stmt else { return };
1529    let target_of = |st: &Stmt| -> Option<u32> {
1530        let Stmt::ExprStmt(Expr::Assign { target, .. }) = st else {
1531            return None;
1532        };
1533        if let Expr::Local { var, .. } = &**target {
1534            Some(*var)
1535        } else {
1536            None
1537        }
1538    };
1539    let (Stmt::Block(tv), Stmt::Block(ev)) = (then_stmt.as_mut(), else_b.as_mut()) else {
1540        return;
1541    };
1542    if tv.len() != 1 || ev.len() != 1 {
1543        return;
1544    }
1545    let (Some(vt_), Some(ve)) = (target_of(&tv[0]), target_of(&ev[0])) else {
1546        return;
1547    };
1548    if vt_ != ve {
1549        return;
1550    }
1551    // The condition and both values must not re-assign the target between
1552    // the fold — they are single statements, so a mention is a re-read at
1553    // worst; re-assignment only happens via assignments inside them.
1554    let mut touched = HashSet::default();
1555    let (Stmt::ExprStmt(a_then), Stmt::ExprStmt(a_else)) = (&tv[0], &ev[0]) else {
1556        return;
1557    };
1558    if let Expr::Assign { value, .. } = a_then {
1559        collect_vars(value, &mut touched);
1560    }
1561    if let Expr::Assign { value, .. } = a_else {
1562        collect_vars(value, &mut touched);
1563    }
1564    if touched.contains(&vt_) {
1565        return;
1566    }
1567    let value_then = match a_then {
1568        Expr::Assign { value, .. } => value.clone(),
1569        _ => return,
1570    };
1571    let value_else = match a_else {
1572        Expr::Assign { value, .. } => value.clone(),
1573        _ => return,
1574    };
1575    let cond_e = cond.clone();
1576    let folded = Stmt::ExprStmt(Expr::Assign {
1577        target: Box::new(Expr::Local {
1578            var: vt_,
1579            ty: vt_join_ty(&value_then, &value_else),
1580        }),
1581        op: AssignOp::Plain,
1582        value: Box::new(Expr::Cond {
1583            c: Box::new(cond_e),
1584            t: value_then,
1585            f: value_else,
1586        }),
1587    });
1588    *s = Stmt::Block(vec![folded]);
1589    *changed = true;
1590}
1591
1592fn vt_join_ty(a: &Expr, b: &Expr) -> TypeRef {
1593    let ta = a.type_ref();
1594    let tb = b.type_ref();
1595    if ta.erased() == tb.erased() {
1596        return ta;
1597    }
1598    TypeRef::J(JavaType::Object("java/lang/Object".into()))
1599}
1600
1601/// Fold `new StringBuilder(...).append(x)...toString()` chains into
1602/// `StringConcat`. Single-assignment temporaries carry the chain.
1603///
1604/// d8 drops append results (statement-form calls), so a builder whose chain
1605/// has STATEMENT appends attached is left alone: folding its `toString`
1606/// with only the ctor arguments would misstate the contents.
1607pub fn fold_string_builders(s: &mut Stmt, vt: &VarTable) {
1608    // var → assigned value, for vars assigned exactly once.
1609    let n_vars = vt.vars.len().max(1);
1610    let mut counts = vec![0usize; n_vars];
1611    count_assignments(s, &mut counts);
1612    let mut values: Vec<Option<Expr>> = vec![None; n_vars];
1613    record_assignments(s, &counts, &mut values);
1614
1615    // Builders with statement-form appends (their chains are incomplete).
1616    let mut appended_stmts: HashSet<u32> = HashSet::default();
1617    walk_all(s, &mut |st| {
1618        if let Stmt::ExprStmt(Expr::Method {
1619            cls,
1620            name,
1621            owner,
1622            args,
1623            ..
1624        }) = st
1625        {
1626            if name.as_ref() == "append" && is_string_builder(cls) && args.len() == 1 {
1627                if let Some(Expr::Local { var, .. }) = owner.as_deref() {
1628                    appended_stmts.insert(*var);
1629                }
1630            }
1631        }
1632    });
1633
1634    rewrite_exprs(s, &mut |e| {
1635        fold_concat_in_expr(e, &values, &appended_stmts);
1636    });
1637
1638    // Drop now-unused builder temporaries (their only consumers were
1639    // folded); the drop COUNT drives iteration — no statement counting
1640    // walks at all.
1641    for _ in 0..8 {
1642        let mut reads: HashSet<u32> = HashSet::default();
1643        stmt_collect_vars(s, &mut reads, false);
1644        if drop_unused_assigns(s, &reads) == 0 {
1645            break;
1646        }
1647    }
1648}
1649
1650/// Deep statement count (pipeline guard input).
1651pub fn count_stmts_deep(s: &Stmt, out: &mut usize) {
1652    *out += 1;
1653    walk_all(s, &mut |_| {
1654        *out += 1;
1655    });
1656}
1657
1658/// Fused per-variable analysis gathered in one statement-tree walk.
1659struct VarAnalysis {
1660    assigns: Vec<usize>,
1661    reads: Vec<usize>,
1662    /// Value of the (single) assignment, when recorded.
1663    values: Vec<Option<Expr>>,
1664}
1665
1666fn analyze_vars(s: &Stmt, a: &mut VarAnalysis) {
1667    walk_all(s, &mut |st| {
1668        let exprs: Vec<&Expr> = match st {
1669            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
1670                if let Expr::Local { var, .. } = &**target {
1671                    grow_to(&mut a.assigns, *var);
1672                    let idx = *var as usize;
1673                    a.assigns[idx] += 1;
1674                    if a.assigns[idx] == 1 {
1675                        if idx >= a.values.len() {
1676                            a.values.resize(idx + 1, None);
1677                        }
1678                        a.values[idx] = Some((**value).clone());
1679                    } else if idx < a.values.len() {
1680                        a.values[idx] = None;
1681                    }
1682                }
1683                let mut v: Vec<&Expr> = vec![value];
1684                if !matches!(&**target, Expr::Local { .. }) {
1685                    v.push(target);
1686                }
1687                v
1688            }
1689            Stmt::ExprStmt(e @ (Expr::PreIncDec { .. } | Expr::PostIncDec { .. })) => {
1690                // `v++` is a read-modify-WRITE of v: counting it as a
1691                // plain read makes v a single-assign/single-read forward
1692                // candidate, and value-forwarding then fabricates `5++`
1693                // (or drops v's def and leaves ++ on an undefined name).
1694                let inner = match e {
1695                    Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => e,
1696                    _ => unreachable!(),
1697                };
1698                if let Expr::Local { var, .. } = &**inner {
1699                    grow_to(&mut a.assigns, *var);
1700                    let idx = *var as usize;
1701                    a.assigns[idx] += 1;
1702                    if idx < a.values.len() {
1703                        a.values[idx] = None;
1704                    }
1705                    return;
1706                }
1707                vec![e]
1708            }
1709            Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
1710                vec![e]
1711            }
1712            Stmt::Return(Some(e)) => vec![e],
1713            Stmt::LocalDef {
1714                var, init: Some(e), ..
1715            } => {
1716                grow_to(&mut a.assigns, *var);
1717                let idx = *var as usize;
1718                a.assigns[idx] += 1;
1719                if a.assigns[idx] == 1 {
1720                    if idx >= a.values.len() {
1721                        a.values.resize(idx + 1, None);
1722                    }
1723                    a.values[idx] = Some(e.clone());
1724                } else if idx < a.values.len() {
1725                    a.values[idx] = None;
1726                }
1727                vec![e]
1728            }
1729            Stmt::If { cond, .. } => vec![cond],
1730            Stmt::While { cond, .. } => vec![cond],
1731            Stmt::DoWhile { cond, .. } => vec![cond],
1732            _ => Vec::new(),
1733        };
1734        for e in exprs {
1735            visit_exprs(e, &mut |x| {
1736                if let Expr::Local { var, .. } = x {
1737                    grow_to(&mut a.reads, *var);
1738                    a.reads[*var as usize] += 1;
1739                }
1740            });
1741        }
1742    });
1743    // Keep the three views length-aligned.
1744    let n = a.assigns.len().max(a.reads.len()).max(a.values.len());
1745    a.assigns.resize(n, 0);
1746    a.reads.resize(n, 0);
1747    a.values.resize(n, None);
1748}
1749
1750fn grow_to(v: &mut Vec<usize>, var: u32) {
1751    if var as usize >= v.len() {
1752        v.resize(var as usize + 1, 0);
1753    }
1754}
1755
1756fn count_assignments(s: &Stmt, counts: &mut Vec<usize>) {
1757    walk_all(s, &mut |st| match st {
1758        Stmt::ExprStmt(Expr::Assign { target, .. }) => {
1759            if let Expr::Local { var, .. } = &**target {
1760                grow_to(counts, *var);
1761                counts[*var as usize] += 1;
1762            }
1763        }
1764        Stmt::LocalDef {
1765            var, init: Some(_), ..
1766        } => {
1767            grow_to(counts, *var);
1768            counts[*var as usize] += 1;
1769        }
1770        _ => {}
1771    });
1772}
1773
1774fn record_assignments(s: &Stmt, counts: &[usize], out: &mut Vec<Option<Expr>>) {
1775    walk_all(s, &mut |st| match st {
1776        Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
1777            if let Expr::Local { var, .. } = &**target {
1778                if counts.get(*var as usize).copied().unwrap_or(0) == 1 {
1779                    if *var as usize >= out.len() {
1780                        out.resize(*var as usize + 1, None);
1781                    }
1782                    out[*var as usize] = Some((**value).clone());
1783                }
1784            }
1785        }
1786        Stmt::LocalDef {
1787            var, init: Some(e), ..
1788        } if counts.get(*var as usize).copied().unwrap_or(0) == 1 => {
1789            if *var as usize >= out.len() {
1790                out.resize(*var as usize + 1, None);
1791            }
1792            out[*var as usize] = Some(e.clone());
1793        }
1794        _ => {}
1795    });
1796}
1797
1798fn fold_concat_in_expr(e: &mut Expr, values: &[Option<Expr>], appended: &HashSet<u32>) {
1799    deep_rewrite(e, &mut |x| {
1800        if let Expr::Method {
1801            cls,
1802            name,
1803            args,
1804            owner,
1805            ..
1806        } = x
1807        {
1808            if name.as_ref() == "toString" && args.is_empty() && is_string_builder(cls) {
1809                if let Some(o) = owner {
1810                    // Statement-form appends attached to any chain var mean
1811                    // the parts are incomplete — keep the call.
1812                    let mut has_stmt_appends = false;
1813                    visit_exprs(o, &mut |y| {
1814                        if let Expr::Local { var, .. } = y {
1815                            if appended.contains(var) {
1816                                has_stmt_appends = true;
1817                            }
1818                        }
1819                    });
1820                    if has_stmt_appends {
1821                        return;
1822                    }
1823                    let resolved = resolve_local(o, values);
1824                    if let Some(parts) = collect_sb_parts(&resolved, values, 0) {
1825                        // NEVER fold an empty chain: d8 splits builder
1826                        // chains across alias registers (`v2 = v0.append
1827                        // (x); v2.append(y); v0.toString()`) — the traced
1828                        // chain sees a bare `new StringBuilder` and would
1829                        // fold toString() to "" (a wrong VALUE, silently:
1830                        // lab package Obf.label). With zero parts the
1831                        // call stays — always correct, usually folded
1832                        // elsewhere once the parts are visible.
1833                        if !parts.is_empty() {
1834                            *x = Expr::StringConcat(parts);
1835                        }
1836                    }
1837                }
1838            }
1839        }
1840    });
1841}
1842
1843fn is_string_builder(cls: &str) -> bool {
1844    cls == "java/lang/StringBuilder" || cls == "java/lang/StringBuffer"
1845}
1846
1847fn resolve_local(e: &Expr, values: &[Option<Expr>]) -> Expr {
1848    match e {
1849        Expr::Local { var, ty } => values
1850            .get(*var as usize)
1851            .and_then(|o| o.clone())
1852            .unwrap_or_else(|| Expr::Local {
1853                var: *var,
1854                ty: ty.clone(),
1855            }),
1856        other => other.clone(),
1857    }
1858}
1859
1860/// Collect concat parts from a StringBuilder chain; depth caps cycles.
1861fn collect_sb_parts(e: &Expr, values: &[Option<Expr>], depth: u32) -> Option<Vec<ConcatPart>> {
1862    if depth > 16 {
1863        return None;
1864    }
1865    match e {
1866        Expr::Method {
1867            cls,
1868            name,
1869            args,
1870            owner,
1871            ..
1872        } if name.as_ref() == "append" && is_string_builder(cls) && args.len() == 1 => {
1873            let mut parts =
1874                collect_sb_parts(&resolve_local(owner.as_deref()?, values), values, depth + 1)?;
1875            parts.push(ConcatPart::Str(args[0].clone()));
1876            Some(parts)
1877        }
1878        Expr::New {
1879            cls,
1880            args,
1881            raw: false,
1882            ..
1883        } if is_string_builder(cls) => {
1884            let mut parts = Vec::new();
1885            for a in args {
1886                if let Expr::Const(ConstVal::Str(sv)) = a {
1887                    parts.push(ConcatPart::Const(sv.to_string()));
1888                } else {
1889                    parts.push(ConcatPart::Str(a.clone()));
1890                }
1891            }
1892            Some(parts)
1893        }
1894        _ => None,
1895    }
1896}
1897
1898fn drop_unused_assigns(s: &mut Stmt, reads: &HashSet<u32>) -> usize {
1899    let mut dropped = 0usize;
1900    walk_mut_deep(s, &mut |st| {
1901        if let Stmt::Block(v) = st {
1902            let before = v.len();
1903            v.retain(|x| match x {
1904                // Drop only when unread AND a builder chain (folded
1905                // consumer; `new StringBuilder` chains cannot NPE).
1906                Stmt::ExprStmt(Expr::Assign { target, value, .. }) => match &**target {
1907                    Expr::Local { var, .. } => reads.contains(var) || !is_sbish(value),
1908                    _ => true,
1909                },
1910                Stmt::LocalDef { var, init, .. } => {
1911                    reads.contains(var) || !init.as_ref().map(is_sbish).unwrap_or(false)
1912                }
1913                _ => true,
1914            });
1915            dropped += before - v.len();
1916        }
1917    });
1918    dropped
1919}
1920
1921fn is_sbish(e: &Expr) -> bool {
1922    match e {
1923        Expr::New {
1924            cls, raw: false, ..
1925        } => is_string_builder(cls),
1926        Expr::Method { name, cls, .. } => name.as_ref() == "append" && is_string_builder(cls),
1927        _ => false,
1928    }
1929}
1930
1931fn walk_mut_deep<F: FnMut(&mut Stmt)>(s: &mut Stmt, f: &mut F) {
1932    f(s);
1933    walk_mut(s, &mut |x| walk_mut_deep(x, f));
1934}
1935
1936/// Drop orphaned raw `new-instance` statements. R8 sometimes separates
1937/// `new-instance vR` from its `<init>` invoke by a register copy
1938/// (`move-object vS, vR; invoke vS.<init>(..)` — dex-legal: the init
1939/// initializes the object both registers name). The fold pairs the init
1940/// with the COPY, and the structurer's branch duplication re-materializes
1941/// the original raw marker as a bare `new C();` statement on paths whose
1942/// uses were rewired to the folded side (weixin sns/storage/c0: four
1943/// `new s1();` beside the real `s1 s1x = new s1(..)` — s1 has no no-arg
1944/// ctor, "无法将类 C的构造器 C应用到给定类型" ×195). A raw New in
1945/// statement position binds no variable and initializes nothing — pure
1946/// verifier-artifact dead code.
1947pub fn drop_dead_raw_news(body: &mut Stmt) {
1948    fn is_orphan(s: &Stmt) -> bool {
1949        matches!(s, Stmt::ExprStmt(Expr::New { raw: true, .. }))
1950    }
1951    walk_mut_deep(body, &mut |st| match st {
1952        Stmt::Block(v) => v.retain(|x| !is_orphan(x)),
1953        Stmt::Switch { cases, .. } => {
1954            for c in cases.iter_mut() {
1955                c.body.retain(|x| !is_orphan(x));
1956            }
1957        }
1958        Stmt::For { init, .. } => init.retain(|x| !is_orphan(x)),
1959        Stmt::TryWithResources { resources, .. } => resources.retain(|x| !is_orphan(x)),
1960        _ => {}
1961    });
1962}
1963
1964/// Recover `synchronized` from the d8 monitor pattern:
1965/// `monitorenter(e); try { body } catch (Throwable) { monitorexit(e); throw t; }`
1966/// (optionally followed by `monitorexit(e)`).
1967pub fn fold_synchronized(s: &mut Stmt) {
1968    fold_sync_walk(s);
1969}
1970
1971fn fold_sync_walk(s: &mut Stmt) {
1972    walk_mut_deep(s, &mut |st| {
1973        let Stmt::Block(v) = st else { return };
1974        let mut i = 0;
1975        while i < v.len() {
1976            if i + 1 < v.len() {
1977                if let Some(sync) = try_sync_at(v, i) {
1978                    let _ = v.drain(i..i + 2);
1979                    v.insert(i, sync);
1980                    continue;
1981                }
1982            }
1983            i += 1;
1984        }
1985    });
1986}
1987
1988fn try_sync_at(v: &[Stmt], i: usize) -> Option<Stmt> {
1989    let Stmt::MonitorEnter(lock) = &v[i] else {
1990        return None;
1991    };
1992    let Stmt::Try {
1993        body,
1994        catches,
1995        finally,
1996    } = &v[i + 1]
1997    else {
1998        return None;
1999    };
2000    if finally.is_some() || catches.len() != 1 {
2001        return None;
2002    }
2003    let c = &catches[0];
2004    if !c.exc.is_empty() {
2005        return None;
2006    }
2007    // The catch-all body: monitorexit(lock) then rethrow.
2008    let Stmt::Block(cv) = c.body.as_ref() else {
2009        return None;
2010    };
2011    let exit_matches = cv.len() >= 2
2012        && matches!(&cv[0], Stmt::MonitorExit(e) if expr_local_var(e) == expr_local_var(lock));
2013    let throws = cv.len() >= 2 && matches!(&cv[1], Stmt::Throw(_));
2014    if !(exit_matches && throws) {
2015        return None;
2016    }
2017    Some(Stmt::Synchronized {
2018        lock: lock.clone(),
2019        body: body.clone(),
2020    })
2021}
2022
2023fn expr_local_var(e: &Expr) -> Option<u32> {
2024    if let Expr::Local { var, .. } = e {
2025        Some(*var)
2026    } else {
2027        None
2028    }
2029}
2030
2031/// Copy-forward single-use temporaries: `v = e; ... v ...` inlines `e` at
2032/// the (single) use and drops the assignment when safe. Pure values inline
2033/// anywhere; impure values (calls, array reads) only when the use is the
2034/// statement immediately after the definition.
2035pub fn forward_single_use(s: &mut Stmt, _vt: &VarTable) {
2036    // Fused single traversal: assignment counts, read counts and the
2037    // single-assignment values all come out of ONE walk (three separate
2038    // full-tree walks dominated the pass cost on large methods).
2039    let mut analysis = VarAnalysis {
2040        assigns: Vec::new(),
2041        reads: Vec::new(),
2042        values: Vec::new(),
2043    };
2044    analyze_vars(s, &mut analysis);
2045    let mut assigns = analysis.assigns;
2046    let mut reads = analysis.reads;
2047    // Var ids beyond the table (synthetic test shapes) are covered by the
2048    // counters' on-demand growth.
2049    let n_vars = assigns.len().max(_vt.vars.len()).max(1);
2050    assigns.resize(n_vars, 0);
2051    reads.resize(n_vars, 0);
2052    let mut values = analysis.values;
2053    values.resize(n_vars, None);
2054
2055    // Candidates: assigned exactly once, read exactly once. Additionally
2056    // the inlined VALUE must not reference a multi-assigned var (phi vars):
2057    // moving the read across the phi's reassignment would be a stale
2058    // capture (register rotations snapshot through temps for this reason).
2059    let mut cand = vec![false; n_vars];
2060    let mut edges: Vec<Vec<u32>> = vec![Vec::new(); n_vars];
2061    for (v, cand_v) in cand.iter_mut().enumerate() {
2062        if assigns.get(v).copied().unwrap_or(0) != 1 || reads.get(v).copied().unwrap_or(0) != 1 {
2063            continue;
2064        }
2065        if let Some(val) = values.get(v).and_then(|o| o.as_ref()) {
2066            let mut refs = HashSet::default();
2067            collect_vars(val, &mut refs);
2068            if refs.iter().any(|r| assigns[*r as usize] > 1) {
2069                continue;
2070            }
2071            // Growth-graph edge set, built in the same walk: inlining v
2072            // inserts a clone of its value, and every Local inside the
2073            // clone is replaced in turn.
2074            edges[v] = refs.into_iter().collect();
2075        }
2076        *cand_v = true;
2077    }
2078
2079    // Cycle rejection on the growth graph. A def-reference cycle
2080    // (v = f(w), w = g(v) — loop-carried register rotation reaching the
2081    // pass as two mutually-referencing single-assign/single-read locals)
2082    // re-introduces the other cycle Local at every replacement level, so
2083    // deep_rewrite grows the tree one layer per level and never
2084    // terminates: weixin's com/tencent/mm/plugin/appbrand/widget/input/b4
2085    // exhausted a 64MB worker stack at ~300k recursion frames. Edges into
2086    // non-candidates are harmless (they are never replaced, so growth
2087    // dies there — and they carry no outgoing edges).
2088    //
2089    // Three-color DFS, iterative: a legit 60k-long single-use chain must
2090    // not trade one stack overflow for another. A GRAY child closes a
2091    // cycle; `bad[v]` (v on a cycle, or v's inlined closure grows into
2092    // one) then taints the whole current path — everything on it reaches
2093    // the cycle. Black verdicts memoize: a node whose subtree was proven
2094    // clean cannot grow a cycle later.
2095    let mut color = vec![0u8; n_vars]; // 0 white, 1 gray, 2 black
2096    let mut bad = vec![false; n_vars];
2097    for root in 0..n_vars {
2098        if color[root] != 0 || edges[root].is_empty() {
2099            continue;
2100        }
2101        color[root] = 1;
2102        let mut stack: Vec<(usize, usize)> = vec![(root, 0)];
2103        while let Some(top) = stack.last_mut() {
2104            let v = top.0;
2105            let i = top.1;
2106            top.1 += 1;
2107            if i < edges[v].len() {
2108                let r = edges[v][i] as usize;
2109                if color[r] == 1 {
2110                    for &(pn, _) in &stack {
2111                        bad[pn] = true;
2112                    }
2113                } else if color[r] == 0 {
2114                    color[r] = 1;
2115                    stack.push((r, 0));
2116                } else if bad[r] {
2117                    for &(pn, _) in &stack {
2118                        bad[pn] = true;
2119                    }
2120                }
2121            } else {
2122                color[v] = 2;
2123                stack.pop();
2124            }
2125        }
2126    }
2127    let single: Vec<bool> = (0..n_vars).map(|v| cand[v] && !bad[v]).collect();
2128    if !single.iter().any(|&b| b) {
2129        return;
2130    }
2131    let pure: Vec<bool> = (0..n_vars)
2132        .map(|v| {
2133            single[v]
2134                && values
2135                    .get(v)
2136                    .and_then(|o| o.as_ref())
2137                    .map(|e| !has_side_effects(e))
2138                    .unwrap_or(false)
2139        })
2140        .collect();
2141    let impure: Vec<bool> = (0..n_vars).map(|v| single[v] && !pure[v]).collect();
2142
2143    // 1. Pure values: inline anywhere.
2144    if pure.iter().any(|&b| b) {
2145        let vals: Vec<Option<Expr>> = (0..n_vars)
2146            .map(|v| {
2147                if pure[v] {
2148                    values.get(v).cloned().flatten()
2149                } else {
2150                    None
2151                }
2152            })
2153            .collect();
2154        rewrite_exprs(s, &mut |e| {
2155            deep_rewrite_reads(e, &mut |x| {
2156                if let Expr::Local { var, .. } = x {
2157                    if let Some(Some(v)) = vals.get(*var as usize) {
2158                        *x = v.clone();
2159                    }
2160                }
2161            });
2162        });
2163        drop_defs(s, &pure);
2164    }
2165
2166    // 2. Impure values: inline only into the immediately following
2167    //    statement.
2168    if impure.iter().any(|&b| b) {
2169        forward_adjacent_impure(s, &values, &impure);
2170    }
2171}
2172
2173/// Count READS of variables (assignment targets excluded).
2174fn count_reads(s: &Stmt, out: &mut Vec<usize>) {
2175    walk_all(s, &mut |st| {
2176        let exprs: Vec<&Expr> = match st {
2177            // Assign must match BEFORE the generic ExprStmt arm (the
2178            // assignment target is a write, not a read).
2179            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
2180                let mut v: Vec<&Expr> = vec![value];
2181                if !matches!(&**target, Expr::Local { .. }) {
2182                    v.push(target);
2183                }
2184                v
2185            }
2186            // `v++` writes v — not a read (a compound `a[i]++` still
2187            // reads its owner/index subtree).
2188            Stmt::ExprStmt(e @ (Expr::PreIncDec { .. } | Expr::PostIncDec { .. })) => {
2189                let inner = match e {
2190                    Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => e,
2191                    _ => unreachable!(),
2192                };
2193                if matches!(**inner, Expr::Local { .. }) {
2194                    Vec::new()
2195                } else {
2196                    vec![e]
2197                }
2198            }
2199            Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
2200                vec![e]
2201            }
2202            Stmt::Return(Some(e)) => vec![e],
2203            Stmt::LocalDef { init: Some(e), .. } => vec![e],
2204            Stmt::If { cond, .. } => vec![cond],
2205            Stmt::While { cond, .. } => vec![cond],
2206            Stmt::DoWhile { cond, .. } => vec![cond],
2207            // Read sites that are NOT statement children (walk_all recurses
2208            // into bodies but these payloads are expressions on the node
2209            // itself). Omitting them under-counts reads, which would let
2210            // drop_dead_locals over-prune a var whose only use is a switch
2211            // selector / loop condition / lock / iterable.
2212            Stmt::Switch { selector, .. } => vec![selector],
2213            Stmt::ForEach { iterable, .. } => vec![iterable],
2214            Stmt::Synchronized { lock, .. } => vec![lock],
2215            Stmt::For { cond, update, .. } => {
2216                let mut v: Vec<&Expr> = Vec::with_capacity(update.len() + 1);
2217                if let Some(c) = cond {
2218                    v.push(c);
2219                }
2220                v.extend(update.iter());
2221                v
2222            }
2223            Stmt::Assert { cond, msg } => {
2224                let mut v: Vec<&Expr> = vec![cond];
2225                if let Some(m) = msg {
2226                    v.push(m);
2227                }
2228                v
2229            }
2230            _ => Vec::new(),
2231        };
2232        for e in exprs {
2233            visit_exprs(e, &mut |x| {
2234                if let Expr::Local { var, .. } = x {
2235                    grow_to(out, *var);
2236                    out[*var as usize] += 1;
2237                }
2238            });
2239        }
2240    });
2241}
2242
2243fn drop_defs(s: &mut Stmt, vars: &[bool]) {
2244    walk_mut_deep(s, &mut |st| {
2245        if let Stmt::Block(v) = st {
2246            v.retain(|x| match x {
2247                Stmt::ExprStmt(Expr::Assign { target, .. }) => match &**target {
2248                    Expr::Local { var, .. } => !vars.get(*var as usize).copied().unwrap_or(false),
2249                    _ => true,
2250                },
2251                Stmt::LocalDef { var, .. } => !vars.get(*var as usize).copied().unwrap_or(false),
2252                _ => true,
2253            });
2254        }
2255    });
2256}
2257
2258/// For each statement list: `v = IMPURE; NEXT(v)` folds NEXT's reference.
2259fn forward_adjacent_impure(s: &mut Stmt, values: &[Option<Expr>], impure: &[bool]) {
2260    if let Stmt::Block(v) = s {
2261        let mut i = 0;
2262        while i + 1 < v.len() {
2263            let def_var = match &v[i] {
2264                Stmt::ExprStmt(Expr::Assign { target, .. }) => match &**target {
2265                    Expr::Local { var, .. } => Some(*var),
2266                    _ => None,
2267                },
2268                Stmt::LocalDef { var, .. } => Some(*var),
2269                _ => None,
2270            };
2271            if let Some(var) = def_var {
2272                if impure.get(var as usize).copied().unwrap_or(false) {
2273                    // Does the NEXT statement reference var?
2274                    let mut reads_here = vec![0usize; values.len().max(1)];
2275                    count_reads(&v[i + 1], &mut reads_here);
2276                    if reads_here.get(var as usize).copied().unwrap_or(0) == 1 {
2277                        // The def statement's CURRENT value (earlier inlines
2278                        // in this walk may have rewritten it — the pre-built
2279                        // values map would be stale).
2280                        let val = match &v[i] {
2281                            Stmt::ExprStmt(Expr::Assign { value, .. }) => (**value).clone(),
2282                            Stmt::LocalDef { init: Some(e), .. } => (*e).clone(),
2283                            _ => {
2284                                i += 1;
2285                                continue;
2286                            }
2287                        };
2288                        rewrite_exprs(&mut v[i + 1], &mut |e| {
2289                            deep_rewrite_reads(e, &mut |x| {
2290                                if let Expr::Local { var: v2, .. } = x {
2291                                    if *v2 == var {
2292                                        *x = val.clone();
2293                                    }
2294                                }
2295                            });
2296                        });
2297                        v.remove(i);
2298                        // The PREVIOUS definition may now be adjacent to its
2299                        // (shifted) use — re-examine it.
2300                        i = i.saturating_sub(1);
2301                        continue;
2302                    }
2303                }
2304            }
2305            i += 1;
2306        }
2307    }
2308    walk_mut(s, &mut |x| forward_adjacent_impure(x, values, impure));
2309}
2310
2311// ---------------------------------------------------------------------------
2312// Type inference, booleanization, null comparisons, declarations
2313// ---------------------------------------------------------------------------
2314
2315/// Evidence-based type inference for synthetic (non-parameter) vars.
2316pub fn infer_types(vt: &mut VarTable, body: &mut Stmt, ret: &JavaType, env: &MethodEnv) {
2317    let n = vt.vars.len();
2318    // (type, strong) — DIRECTIONAL evidence. Strong facts (an assigned
2319    // value's type, the declared return/throw/monitor type) DEFINE the
2320    // variable; weak facts (call argument expectations, receiver
2321    // contexts, numeric literals) only constrain a use. jadx's bound
2322    // system carries the same direction; a flat pool let a weak
2323    // expectation (`v26 = p1` with boolean p1 beside `v26 = 0` sugar)
2324    // outvote the definition.
2325    let mut evidence: Vec<Vec<(JavaType, bool)>> = vec![Vec::new(); n];
2326    let ev = |evidence: &mut Vec<Vec<(JavaType, bool)>>, var: u32, t: JavaType, strong: bool| {
2327        if (var as usize) < evidence.len() {
2328            evidence[var as usize].push((t, strong));
2329        }
2330    };
2331
2332    walk_all(body, &mut |st| match st {
2333        Stmt::LocalDef { var, init, .. } => {
2334            if let Some(e) = init {
2335                expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
2336                // The initializer DEFINES the declared type: `long v = l(...)`
2337                // must not stay `int v` (silent 64-bit truncation in the
2338                // eyes of a reader; javac rejects it as lossy). Null
2339                // carries no type (it would DOWNGRADE String to Object).
2340                // Literals are weak (0/1 is boolean sugar half the time);
2341                // typed producers and parameter sources are strong;
2342                // local-to-local copies are weak (register reuse).
2343                let strong = match e {
2344                    Expr::Const(_) => false,
2345                    Expr::Local { var: src, .. } => {
2346                        let src_ty = vt.vars.get(*src as usize).map(|v| v.ty.erased());
2347                        let tgt_ty = vt.vars.get(*var as usize).map(|v| v.ty.erased());
2348                        match (src_ty, tgt_ty) {
2349                            (Some(s), Some(g)) => {
2350                                (s.is_numeric() && g.is_numeric())
2351                                    || (s.is_reference() && g.is_reference())
2352                            }
2353                            _ => false,
2354                        }
2355                    }
2356                    _ => true,
2357                };
2358                ev(&mut evidence, *var, e.type_ref().erased(), strong);
2359            }
2360        }
2361        Stmt::ExprStmt(e) => {
2362            // Assignment targets: a typed PRODUCER (method/new/field) or a
2363            // PARAMETER source is a STRONG definition (`v26 = p1` with
2364            // boolean p1). A local-to-local copy is weak — register reuse
2365            // and merge materialization emit exactly that shape with
2366            // mismatched types (`sb7 = compareTo10` in a Kotlin when).
2367            // Literals stay weak.
2368            if let Expr::Assign { target, value, .. } = e {
2369                // A field WRITE through a local (`v.f = x`, dex iput)
2370                // proves v is a reference of the field's declaring
2371                // class AT THAT POINT — an iput on a boolean register
2372                // cannot exist in valid dex. Strong: it must outrank a
2373                // strong boolean copy chain into the same reused slot
2374                // (weixin coroutine state machines).
2375                if let Expr::Field { owner: Some(o), cls, is_static: false, .. } = &**target {
2376                    if cls.is_empty() {
2377                        // array-length pseudo-field: no declaring class
2378                    } else if let Expr::Local { var, .. } = &**o {
2379                        ev(
2380                            &mut evidence,
2381                            *var,
2382                            JavaType::Object(cls.clone()),
2383                            true,
2384                        );
2385                    }
2386                }
2387                if let Expr::Local { var, .. } = &**target {
2388                    let strong = match &**value {
2389                        Expr::Const(_) => false,
2390                        Expr::Local { var: src, .. } => {
2391                            // A local copy is strong when the source and
2392                            // the target's register type are the same
2393                            // FAMILY (numeric↔numeric, reference↔
2394                            // reference) — a cross-family copy (int into
2395                            // a StringBuilder local) is merge-material
2396                            // residue and stays weak.
2397                            let src_ty = vt.vars.get(*src as usize).map(|v| v.ty.erased());
2398                            let tgt_ty = vt.vars.get(*var as usize).map(|v| v.ty.erased());
2399                            match (src_ty, tgt_ty) {
2400                                (Some(s), Some(g)) => {
2401                                    (s.is_numeric() && g.is_numeric())
2402                                        || (s.is_reference() && g.is_reference())
2403                                }
2404                                _ => false,
2405                            }
2406                        }
2407                        _ => true,
2408                    };
2409                    ev(&mut evidence, *var, value.type_ref().erased(), strong);
2410                }
2411            }
2412            expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
2413        }
2414        Stmt::Throw(e) => {
2415            expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
2416            if let Expr::Local { var, .. } = e {
2417                ev(
2418                    &mut evidence,
2419                    *var,
2420                    JavaType::Object("java/lang/Throwable".into()),
2421                    true,
2422                );
2423            }
2424        }
2425        Stmt::Return(Some(e)) => {
2426            expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
2427            if let Expr::Local { var, .. } = e {
2428                ev(&mut evidence, *var, ret.clone(), true);
2429            }
2430        }
2431        Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
2432            if let Expr::Local { var, .. } = e {
2433                ev(
2434                    &mut evidence,
2435                    *var,
2436                    JavaType::Object("java/lang/Object".into()),
2437                    true,
2438                );
2439            }
2440        }
2441        Stmt::ForEach {
2442            var,
2443            iterable,
2444            is_array,
2445            ..
2446        } => {
2447            if let Expr::Local { var: v0, .. } = iterable {
2448                if *is_array {
2449                    ev(&mut evidence, *v0, JavaType::Array(Box::new(JavaType::Int)), true);
2450                } else {
2451                    ev(
2452                        &mut evidence,
2453                        *v0,
2454                        JavaType::Object("java/lang/Object".into()),
2455                        true,
2456                    );
2457                }
2458            }
2459            let _ = var;
2460        }
2461        _ => {}
2462    });
2463    let _ = env;
2464
2465    // Return-position locals take the declared return type (also inside
2466    // nested returns — handled by the walk above).
2467
2468        // Vars dereferenced as field/method receivers or arrays: their
2469    // resolved type must be a dereferenceable class — a boxed Boolean
2470    // in the strong set (Kotlin Result registers hold `Boolean | impl`
2471    // across coroutine case arms) must lose to the real class even
2472    // when the Boolean evidence comes first (weixin ry0/h2's
2473    // `Boolean bool19` receiving `bool19.d = x`).
2474    let mut derefed: Vec<bool> = vec![false; vt.vars.len()];
2475    mark_derefs(body, &mut derefed);
2476    // Array-indexed vars: their resolved type must be an ARRAY when
2477    // the evidence has one (`num4[v68]` with Integer-typed num4 —
2478    // 需要数组但找到Integer family); member derefs keep the class
2479    // preference below.
2480    let mut arr_derefed: Vec<bool> = vec![false; vt.vars.len()];
2481    visit_all_exprs(body, &mut |x| {
2482        if let Expr::ArrayIndex { array, .. } = x {
2483            if let Expr::Local { var, .. } = &**array {
2484                if (*var as usize) < arr_derefed.len() {
2485                    arr_derefed[*var as usize] = true;
2486                }
2487            }
2488        }
2489        // `v != null` is STRONG reference evidence: the lifter only
2490        // renders the null form when the dex value view held a
2491        // reference register at that pc — an int-typed generation
2492        // compared against null is phi-confluence residue (yq0/g1's
2493        // `v95_g12 != null`, 546-line 二元运算符 family). Strong Object
2494        // makes the resolver retype and split_generations separate the
2495        // int writes into their own generation.
2496        if let Expr::Bin { op, l, r, .. } = x {
2497            if matches!(op, BinOp::Eq | BinOp::Ne)
2498                && (matches!(&**r, Expr::Const(ConstVal::Null))
2499                    || matches!(&**l, Expr::Const(ConstVal::Null)))
2500            {
2501                for side in [l, r] {
2502                    if let Expr::Local { var, .. } = &**side {
2503                        ev(
2504                            &mut evidence,
2505                            *var,
2506                            JavaType::Object("java/lang/Object".into()),
2507                            true,
2508                        );
2509                    }
2510                }
2511            }
2512        }
2513    });
2514    const FINAL_JDK: &[&str] = &[
2515        "java/lang/Boolean",
2516        "java/lang/String",
2517        "java/lang/Integer",
2518        "java/lang/Long",
2519        "java/lang/Short",
2520        "java/lang/Byte",
2521        "java/lang/Character",
2522        "java/lang/Float",
2523        "java/lang/Double",
2524    ];
2525    for (i, evs) in evidence.iter().enumerate() {
2526        let info = &vt.vars[i];
2527        if info.is_param {
2528            continue;
2529        }
2530        let strong: Vec<JavaType> = evs
2531            .iter()
2532            .filter(|(_, s)| *s)
2533            .map(|(t, _)| t.clone())
2534            .collect();
2535        let all: Vec<JavaType> = evs.iter().map(|(t, _)| t.clone()).collect();
2536        if info.ty.erased().is_reference() {
2537            // Materialization residue: no strong definition, and the weak
2538            // pool names ≥2 DIFFERENT classes (a Kotlin `when` lowered
2539            // every branch's value into one register slot) — neither
2540            // class can win, every assignment needs boxing headroom:
2541            // widen to Object (uses get receiver casts).
2542            if strong.is_empty() {
2543                let refs: std::collections::HashSet<&str> = all
2544                    .iter()
2545                    .filter_map(|t| match t {
2546                        JavaType::Object(n) if n.as_ref() != "java/lang/Object" => {
2547                            Some(n.as_ref())
2548                        }
2549                        _ => None,
2550                    })
2551                    .collect();
2552                let mixed = all.iter().any(|t| t.is_numeric());
2553                if refs.len() >= 2 || (mixed && !refs.is_empty()) || (mixed && all.iter().any(|t| matches!(t, JavaType::Array(_)))) {
2554                    vt.vars[i].ty =
2555                        TypeRef::J(JavaType::Object("java/lang/Object".into()));
2556                    continue;
2557                }
2558            }
2559                        // Array-dereferenced vars prefer array evidence (an
2560            // Integer producer cannot be indexed).
2561            if arr_derefed[i] {
2562                let arr = strong
2563                    .iter()
2564                    .chain(all.iter())
2565                    .find(|t| matches!(t, JavaType::Array(_)))
2566                    .cloned();
2567                if let Some(t) = arr {
2568                    vt.vars[i].ty = TypeRef::J(t);
2569                    continue;
2570                }
2571            }
2572            // Strong definitions outrank weak expectations. A
2573            // dereferenced var prefers a real class over final JDK
2574            // boxes (Boolean can never carry `.d = x`).
2575            let deref_pick = || {
2576                if !derefed[i] {
2577                    return None;
2578                }
2579                strong
2580                    .iter()
2581                    .find(|t| {
2582                        matches!(t, JavaType::Object(n)
2583                            if !n.is_empty()
2584                                && n.as_ref() != "java/lang/Object"
2585                                && !FINAL_JDK.contains(&n.as_ref()))
2586                    })
2587                    .cloned()
2588            };
2589            if let Some(t) = deref_pick()
2590                .or_else(|| pick_object(&strong))
2591                .or_else(|| pick_object(&all))
2592            {
2593                vt.vars[i].ty = TypeRef::J(t);
2594            }
2595        } else if let Some(t) = pick_object(&strong) {
2596            // A strong reference definition (typed producer or field
2597            // write) outranks a strong boolean copy: Boolean assigns
2598            // INTO an Object slot legally, never the reverse.
2599            vt.vars[i].ty = TypeRef::J(t);
2600        } else if strong.contains(&JavaType::Boolean) {
2601            // A boolean-typed SOURCE assigned into the variable is a
2602            // definition (`v26 = p1` with boolean p1); the 0/1 literals
2603            // that share the pool are boolean sugar as often as not.
2604            vt.vars[i].ty = TypeRef::J(JavaType::Boolean);
2605        } else if !all.is_empty() && all.iter().all(|t| t.is_numeric()) {
2606            if let Some(t) = pick_numeric(&all) {
2607                vt.vars[i].ty = TypeRef::J(t);
2608            }
2609        } else {
2610            // Mixed-family materialization on a numeric-declared slot
2611            // (int slot receiving StringBuilders — the register was the
2612            // `when` accumulator): Object, same as the reference side.
2613            let refs = all.iter().filter(|t| t.is_reference()).count();
2614            let nums = all.iter().filter(|t| t.is_numeric()).count();
2615            if refs > 0 && nums > 0 {
2616                vt.vars[i].ty =
2617                    TypeRef::J(JavaType::Object("java/lang/Object".into()));
2618            } else if let Some(t) = pick_object(&strong).or_else(|| pick_object(&all)) {
2619                vt.vars[i].ty = TypeRef::J(t);
2620            }
2621        }
2622    }
2623
2624    // Rewrite embedded Local types. Borrowed view + write-only-on-change:
2625    // the table clone was n_vars JavaType clones per method, and the
2626    // rewrite cloned a type into EVERY Local node — most already carry
2627    // the right one (JavaType::Object clones allocate).
2628    let types: Vec<&TypeRef> = vt.vars.iter().map(|v| &v.ty).collect();
2629    rewrite_exprs(body, &mut |e| {
2630        deep_rewrite(e, &mut |x| {
2631            if let Expr::Local { var, ty } = x {
2632                if let Some(want) = types.get(*var as usize) {
2633                    if ty != *want {
2634                        *ty = (*want).clone();
2635                    }
2636                }
2637            }
2638        });
2639    });
2640
2641    // Numeric literals take their variable's declared width (`double v = 0L`
2642    // prints as `0.0`; `int x = 5L` as `5`).
2643    coerce_num_consts(body, &types);
2644}
2645
2646fn coerce_num_consts(body: &mut Stmt, types: &[&TypeRef]) {
2647    walk_mut_deep(body, &mut |st| {
2648        let (var, val): (u32, &mut Expr) = match st {
2649            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => match &mut **target {
2650                Expr::Local { var, .. } => (*var, value),
2651                _ => return,
2652            },
2653            Stmt::LocalDef {
2654                var, init: Some(e), ..
2655            } => (*var, e),
2656            _ => return,
2657        };
2658        let want = match types.get(var as usize) {
2659            Some(TypeRef::J(t)) => t.clone(),
2660            _ => return,
2661        };
2662        let rewritten = match &*val {
2663            Expr::Const(ConstVal::Long(l)) => match &want {
2664                JavaType::Double => Some(Expr::Const(ConstVal::Double(*l as f64))),
2665                JavaType::Float => Some(Expr::Const(ConstVal::Float(*l as f32))),
2666                _ => None,
2667            },
2668            Expr::Const(ConstVal::Int(i)) => match &want {
2669                JavaType::Double => Some(Expr::Const(ConstVal::Double(*i as f64))),
2670                JavaType::Float => Some(Expr::Const(ConstVal::Float(*i as f32))),
2671                _ => None,
2672            },
2673            _ => None,
2674        };
2675        if let Some(e) = rewritten {
2676            *val = e;
2677        }
2678    });
2679}
2680
2681fn pick_object(evs: &[JavaType]) -> Option<JavaType> {
2682    evs.iter()
2683        .find(|t| {
2684            t.is_reference()
2685                && !matches!(t, JavaType::Object(n) if n.is_empty() || n.as_ref() == "java/lang/Object")
2686        })
2687        .cloned()
2688}
2689
2690fn pick_numeric(evs: &[JavaType]) -> Option<JavaType> {
2691    if evs.is_empty() {
2692        return None;
2693    }
2694    let mut t = evs[0].clone();
2695    for x in &evs[1..] {
2696        t = join_numeric(&t, x);
2697    }
2698    Some(t)
2699}
2700
2701fn join_numeric(a: &JavaType, b: &JavaType) -> JavaType {
2702    if a == b {
2703        return a.clone();
2704    }
2705    match (a, b) {
2706        (JavaType::Double, _) | (_, JavaType::Double) => JavaType::Double,
2707        (JavaType::Float, _) | (_, JavaType::Float) => JavaType::Float,
2708        (JavaType::Long, _) | (_, JavaType::Long) => JavaType::Long,
2709        (JavaType::Int, x) if x.is_integral() => JavaType::Int,
2710        (x, JavaType::Int) if x.is_integral() => JavaType::Int,
2711        (x, y) if x.is_integral() && y.is_integral() => JavaType::Int,
2712        _ => JavaType::Int,
2713    }
2714}
2715
2716/// Gather per-var type evidence from one expression.
2717fn expr_evidence<F: FnMut(u32, JavaType)>(e: &Expr, f: &mut F) {
2718    visit_exprs(e, &mut |x| match x {
2719        Expr::Method {
2720            owner,
2721            args,
2722            desc,
2723            is_static,
2724            cls,
2725            ..
2726        } => {
2727            if !*is_static {
2728                if let Some(o) = owner {
2729                    if let Expr::Local { var, .. } = &**o {
2730                        f(*var, JavaType::Object(cls.clone()));
2731                    }
2732                }
2733            }
2734            for (i, a) in args.iter().enumerate() {
2735                if let Expr::Local { var, .. } = a {
2736                    if let Some(t) = desc.args.get(i) {
2737                        f(*var, t.clone());
2738                    }
2739                }
2740            }
2741        }
2742        Expr::Field {
2743            owner,
2744            cls,
2745            ty,
2746            is_static,
2747            ..
2748        } => {
2749            if !*is_static && !cls.is_empty() {
2750                if let Some(o) = owner {
2751                    if let Expr::Local { var, .. } = &**o {
2752                        // The field ref's declaring class is dex-exact
2753                        // evidence — the old hardcoded Object let a
2754                        // boolean copy chain outvote it (weixin ry0/h2's
2755                        // `Boolean bool19` receiving `bool19.d = x` —
2756                        // the 339-line 找不到符号 变量 d family).
2757                        // EMPTY cls = the array-length pseudo-field
2758                        // (lift models `a.length` as Field{cls:""}) —
2759                        // Object("") evidence poisoned pick_object into
2760                        // empty-typed declarations and `() x` casts.
2761                        f(*var, JavaType::Object(cls.clone()));
2762                    }
2763                }
2764            }
2765            let _ = ty;
2766        }
2767        // `v != null` / `v == null`: the register holds a reference at
2768        // that point — weak Object evidence keeps int-typed generations
2769        // from winning the pool (yq0/g1's `v95_g12 != null` with int
2770        // v95_g12 — 二元运算符 '!=' 546-line family).
2771        Expr::Bin { op, l, r, .. }
2772            if matches!(op, BinOp::Eq | BinOp::Ne)
2773                && (matches!(&**r, Expr::Const(ConstVal::Null))
2774                    || matches!(&**l, Expr::Const(ConstVal::Null))) =>
2775        {
2776            for side in [l, r] {
2777                if let Expr::Local { var, .. } = &**side {
2778                    f(*var, JavaType::Object("java/lang/Object".into()));
2779                }
2780            }
2781        }
2782        Expr::ArrayIndex { array, .. } => {
2783            if let Expr::Local { var, ty, .. } = &**array {
2784                // Reinforce the local's KNOWN array type: the hardcoded
2785                // int[] fallback contradicted a typed array local (a
2786                // register reused for byte[], int[] then byte[] across
2787                // one clinit labeled the byte[] defs `int[]`).
2788                let t = ty.erased();
2789                if matches!(t, JavaType::Array(_)) {
2790                    f(*var, t);
2791                } else {
2792                    f(*var, JavaType::Array(Box::new(JavaType::Int)));
2793                }
2794            }
2795        }
2796        Expr::Assign { .. } => {
2797            // Covered by the typed strong-evidence walk in infer_types;
2798            // keeping a weak duplicate here let expectations outvote it.
2799        }
2800        Expr::Cast { e: inner, .. } => {
2801            // A cast is an EXPLICIT narrowing at the use site — it must
2802            // NOT feed the cast target as evidence for the variable
2803            // (`Object get2 = list.get(2); j((String) get2);` had get2
2804            // retyped to String, making the assignment incompatible).
2805            // jadx's bounds carry direction: a USE bound narrows nothing.
2806            let _ = inner;
2807        }
2808        Expr::InstanceOf { e: inner, .. } => {
2809            let _ = inner;
2810        }
2811        _ => {}
2812    });
2813}
2814
2815/// Mark locals that OWN a non-static field access and whose inferred type
2816/// is a concrete `$<digits>` class (a d8/R8-desugared lambda or a Kotlin
2817/// suspend-lambda). Emit demotes such a type to its SAM interface in
2818/// DECLARED positions (`Function2 v = new Outer$..$1(..)`) for phi-friendly
2819/// readability — but interfaces carry no instance fields, so the Kotlin
2820/// coroutine `create()` capture writes (`v.L$0 = obj`, also I$/J$) render
2821/// "找不到符号 变量 L$0" (weibo MutableScatterMap$..$iterator$1, ~1k).
2822/// Forcing the concrete declared type keeps the field access legal; the phi
2823/// case is excluded because a disagreed merge stays Object (wide_stack_vars),
2824/// never a specific concrete class. Gate mirrors emit's demotion exactly
2825/// (last `$` segment all digits) so nothing else changes.
2826pub fn mark_field_owner_concrete(vt: &mut VarTable, body: &mut Stmt) {
2827    let mut owners: HashSet<u32> = HashSet::default();
2828    rewrite_exprs(body, &mut |e| {
2829        visit_exprs(e, &mut |x| {
2830            if let Expr::Field {
2831                owner: Some(o),
2832                is_static: false,
2833                ..
2834            } = x
2835            {
2836                if let Expr::Local { var, .. } = &**o {
2837                    owners.insert(*var);
2838                }
2839            }
2840        });
2841    });
2842    for var in owners {
2843        let i = var as usize;
2844        if i >= vt.vars.len() {
2845            continue;
2846        }
2847        if let JavaType::Object(cls) = vt.vars[i].ty.erased() {
2848            if let Some(last) = cls.rsplit('$').next() {
2849                if !last.is_empty() && last.chars().all(|c| c.is_ascii_digit()) {
2850                    vt.force_concrete_vars.insert(var);
2851                }
2852            }
2853        }
2854    }
2855    // EVERY local whose resolved type is a concrete `$<digits>` class
2856    // keeps its binary name, not just field owners: when one side of an
2857    // assignment falls back to the SAM/super (`ContinuationImpl v7`) and
2858    // its sibling stays concrete (`$reportWhenComplete$1 v9`), the pair
2859    // is an uncastable "ContinuationImpl无法转换为…$1" (weibo coroutine
2860    // prologues ×282). The anon fallback still serves INTERFACE-typed
2861    // phi slots (branches minting different lambdas) — their vt type is
2862    // the join, never the $N class.
2863    let concrete: Vec<u32> = vt
2864        .vars
2865        .iter()
2866        .filter(|v| {
2867            if let JavaType::Object(cls) = v.ty.erased() {
2868                cls.rsplit('$')
2869                    .next()
2870                    .is_some_and(|l| !l.is_empty() && l.chars().all(|c| c.is_ascii_digit()))
2871            } else {
2872                false
2873            }
2874        })
2875        .map(|v| v.id)
2876        .collect();
2877    for var in concrete {
2878        vt.force_concrete_vars.insert(var);
2879    }
2880}
2881
2882/// Type inference can leave a specific-reference-typed target assigned
2883/// from a `java/lang/Object`-typed value: a phi that merged String and
2884/// Object then settled on String (`String str4; ... str4 = obj;` where
2885/// `obj` is an Object field — y5/n.java), or an Object local flowing to a
2886/// typed field. Java requires a narrowing cast there; insert `(T) value`
2887/// at the USE site (round-40 philosophy: narrow at use, never retype the
2888/// declaration). Guarded tightly: only when the value's resolved static
2889/// type is EXACTLY `java/lang/Object` (the untyped top — a known subtype
2890/// is never re-cast), the value is not a null const (assignable to any
2891/// ref) nor already a cast, and the target is a specific reference type
2892/// (Object→int/boolean is unboxing, a different problem left alone).
2893pub fn insert_object_narrowing_casts(
2894    vt: &VarTable,
2895    body: &mut Stmt,
2896    pool: &DexPool,
2897    ret: &JavaType,
2898) {
2899    // Resolve a value's static type through the VarTable for locals (the
2900    // embedded Local ty can lag infer_types), else the expr's own type.
2901    let value_ty = |e: &Expr| -> JavaType {
2902        match e {
2903            Expr::Local { var, .. } => vt.var(*var).ty.erased(),
2904            other => other.type_ref().erased(),
2905        }
2906    };
2907    let is_top_object = |e: &Expr| -> bool {
2908        value_ty(e) == JavaType::Object("java/lang/Object".into())
2909    };
2910    // A specific reference target type (a class other than
2911    // java/lang/Object, or an array — `String[] v = obj` needs the
2912    // cast exactly like `String v = obj`).
2913    let specific_ref = |ty: &TypeRef| -> Option<TypeRef> {
2914        match ty.erased() {
2915            JavaType::Object(c) if c.as_ref() != "java/lang/Object" => Some(ty.clone()),
2916            JavaType::Array(_) => Some(ty.clone()),
2917            _ => None,
2918        }
2919    };
2920    let castable = |e: &Expr| -> bool {
2921        is_top_object(e)
2922            && !matches!(e, Expr::Const(_) | Expr::Cast { .. } | Expr::InstanceOf { .. })
2923    };
2924    // Downcast witness: a SPECIFIC-ref value whose static type does not
2925    // fit the specific-ref target (`v9($1) = v7` with v7:
2926    // ContinuationImpl — weibo coroutine prologue ×282). The dex move
2927    // was verifier-proven against the target type, so the cast states
2928    // the bytecode truth; an actually-unconvertible static pair (both
2929    // classes, unrelated — the value view conflated) rides the
2930    // emitter's (Object) relay and still compiles. Framework types the
2931    // pool cannot order take the cast too: a downcast renders directly,
2932    // an unrelated pair relays.
2933    let needs_downcast = |e: &Expr, tt: &JavaType| -> bool {
2934        if matches!(e, Expr::Const(_) | Expr::Cast { .. } | Expr::InstanceOf { .. }) {
2935            return false;
2936        }
2937        let v = value_ty(e);
2938        let (JavaType::Object(vn), JavaType::Object(tn)) = (&v, tt) else {
2939            return false;
2940        };
2941        if vn.as_ref() == "java/lang/Object" || tn.as_ref() == "java/lang/Object" {
2942            return false;
2943        }
2944        vn != tn && !pool.is_subtype(vn, tn)
2945    };
2946    walk_mut_deep(body, &mut |st| match st {
2947        Stmt::ExprStmt(Expr::Assign {
2948            target,
2949            value,
2950            op: AssignOp::Plain,
2951            ..
2952        }) => {
2953            let tgt = match &**target {
2954                Expr::Local { var, .. } => vt.var(*var).ty.clone(),
2955                Expr::Field { ty, .. } => ty.clone(),
2956                // Array-element store `arr[i] = value`: the target type is
2957                // the ELEMENT type. Without this arm an Object→element
2958                // assignment (`String[] a; a[0] = pair.first` — Pair.first
2959                // erases to Object) skipped the cast and failed "Object
2960                // 无法转换为String" (weixin ×~150). Resolve the array's type
2961                // through the vt (the embedded Local ty can lag infer_types).
2962                Expr::ArrayIndex { array, .. } => {
2963                    let aty = match &**array {
2964                        Expr::Local { var, .. } => vt.var(*var).ty.erased(),
2965                        other => other.type_ref().erased(),
2966                    };
2967                    match aty {
2968                        JavaType::Array(inner) => TypeRef::J(*inner),
2969                        _ => return,
2970                    }
2971                }
2972                _ => return,
2973            };
2974            if let Some(t) = specific_ref(&tgt) {
2975                if castable(value) || needs_downcast(value, &tgt.erased()) {
2976                    let v = std::mem::replace(value, Box::new(Expr::This));
2977                    **value = Expr::Cast { ty: t, e: v };
2978                }
2979            }
2980        }
2981        Stmt::LocalDef { var, init: Some(value), .. } => {
2982            if let Some(t) = specific_ref(&vt.var(*var).ty) {
2983                let tt = vt.var(*var).ty.erased();
2984                if castable(value) || needs_downcast(value, &tt) {
2985                    let v = std::mem::replace(value, Expr::This);
2986                    *value = Expr::Cast {
2987                        ty: t,
2988                        e: Box::new(v),
2989                    };
2990                }
2991            }
2992        }
2993        Stmt::Return(Some(value)) => {
2994            if let Some(t) = specific_ref(&TypeRef::J(ret.clone())) {
2995                if castable(value) || needs_downcast(value, ret) {
2996                    let v = std::mem::replace(value, Expr::This);
2997                    *value = Expr::Cast { ty: t, e: Box::new(v) };
2998                }
2999            }
3000        }
3001        _ => {}
3002    });
3003    // Descriptor-exact formal casts at CALL args: the dex already
3004    // resolved the target method, so casting a top-Object actual to
3005    // its declared formal cannot shift overload resolution (the r57
3006    // args lesson applies to GUESSED casts, not descriptor-exact
3007    // ones). `"source_type:".concat(obj2)`, `new my0.j(obj2, ..)` —
3008    // Object无法转换为String family.
3009    walk_stmt_exprs(body, &mut |e| {
3010        deep_rewrite(e, &mut |x| {
3011            let (args, arg_tys): (&mut Vec<Expr>, &[JavaType]) = match x {
3012                Expr::Method { desc, args, is_dynamic: false, .. } => {
3013                    (args, desc.args.as_slice())
3014                }
3015                // Ctor calls are their own variant — the dex resolved the
3016                // exact <init>, so formal casts are just as safe here
3017                // (`new r(obj)` — Object无法转换为r family).
3018                Expr::New { arg_tys, args, .. } => (args, arg_tys.as_slice()),
3019                _ => return,
3020            };
3021            for (i, a) in args.iter_mut().enumerate() {
3022                let Some(formal) = arg_tys.get(i) else {
3023                    continue;
3024                };
3025                let Some(t) = specific_ref(&TypeRef::J(formal.clone())) else {
3026                    continue;
3027                };
3028                if !(castable(a) || needs_downcast(a, formal)) {
3029                    continue;
3030                }
3031                let v = std::mem::replace(a, Expr::Const(ConstVal::Null));
3032                *a = Expr::Cast { ty: t, e: Box::new(v) };
3033            }
3034        });
3035    });
3036}
3037
3038/// Numeric narrowing/widening mismatches at Assign / LocalDef / Return
3039/// positions get an explicit cast: dex int-to-byte / long-to-int flows
3040/// are implicit in the register machine, Java rejects them ("从long
3041/// 转换到int可能会有损失" — okio Buffer ×321 weibo lines). Casts at
3042/// these three positions are descriptor-faithful and cannot shift
3043/// overload resolution (the r57 args-position lesson). Boolean/numeric
3044/// mixes are NOT castable in Java and stay with the generation-split
3045/// machinery.
3046pub fn fix_primitive_assign_casts(vt: &VarTable, body: &mut Stmt, ret_ty: &JavaType) {
3047    fn numlike(t: &JavaType) -> bool {
3048        matches!(
3049            t,
3050            JavaType::Byte
3051                | JavaType::Short
3052                | JavaType::Int
3053                | JavaType::Char
3054                | JavaType::Long
3055                | JavaType::Float
3056                | JavaType::Double
3057        )
3058    }
3059    fn val_ty(e: &Expr, vt: &VarTable) -> JavaType {
3060        match e {
3061            Expr::Local { var, .. } if (*var as usize) < vt.vars.len() => {
3062                vt.var(*var).ty.erased()
3063            }
3064            other => other.type_ref().erased(),
3065        }
3066    }
3067    fn wrap(ty: &JavaType, value: &mut Expr) {
3068        if matches!(value, Expr::Cast { .. }) {
3069            return;
3070        }
3071        if val_ty_static(value) == *ty {
3072            return;
3073        }
3074        let taken = std::mem::replace(value, Expr::Const(ConstVal::Null));
3075        *value = Expr::Cast {
3076            ty: TypeRef::J(ty.clone()),
3077            e: Box::new(taken),
3078        };
3079    }
3080    fn val_ty_static(e: &Expr) -> JavaType {
3081        e.type_ref().erased()
3082    }
3083    /// Target-typed coercion for one assignment value. Numeric mixes
3084    /// cast; boolean mixes CONVERT (Java has no cast between boolean
3085    /// and any numeric): `d = b` → `(double) (b ? 1 : 0)`, `b = i` →
3086    /// `i != 0` — mirrors fix_primitive_arg_bridges' formal-side arms.
3087    /// The generation split cannot own these: at an if-join the
3088    /// one-sided split is undone (rename_gen_back) and the incompatible
3089    /// assign re-exposes ("boolean无法转换为double", weixin s9/t01-f).
3090    fn coerce(tt: &JavaType, value: &mut Expr, vt: &VarTable) {
3091        // Raw const bits against a floating target are the FLOAT value,
3092        // not an int to widen (`float f = <0x41000000 bits>` is 8.0f;
3093        // the (float) cast path would silently store 1.09e9f). Mirror of
3094        // fix_primitive_arg_bridges' descriptor-formal reinterpretation.
3095        match (&*value, tt) {
3096            (Expr::Const(ConstVal::Int(b)), JavaType::Float) => {
3097                *value = Expr::Const(ConstVal::Float(f32::from_bits(*b as u32)));
3098                return;
3099            }
3100            (Expr::Const(ConstVal::Long(b)), JavaType::Double) => {
3101                *value = Expr::Const(ConstVal::Double(f64::from_bits(*b as u64)));
3102                return;
3103            }
3104            (Expr::Const(ConstVal::Float(f)), JavaType::Int) => {
3105                *value = Expr::Const(ConstVal::Int(f.to_bits() as i32));
3106                return;
3107            }
3108            (Expr::Const(ConstVal::Double(d)), JavaType::Long) => {
3109                *value = Expr::Const(ConstVal::Long(d.to_bits() as i64));
3110                return;
3111            }
3112            _ => {}
3113        }
3114        let vt_val = val_ty(value, vt);
3115        let tt_bool = matches!(tt, JavaType::Boolean);
3116        let val_bool = matches!(vt_val, JavaType::Boolean);
3117        if tt_bool && !val_bool && numlike(&vt_val) {
3118            if matches!(
3119                value,
3120                Expr::Const(_)
3121                    | Expr::Bin { op: BinOp::Eq | BinOp::Ne, .. }
3122                    | Expr::Un { op: UnOp::Not, .. }
3123            ) {
3124                // Bare 0/1 constants render as false/true through the
3125                // emitter's bool coercion — wrapping them in `!= 0`
3126                // produced `0 != 0` noise the value-diamond fold then
3127                // had to re-interpret.
3128                return;
3129            }
3130            let taken = std::mem::replace(value, Expr::Const(ConstVal::Null));
3131            *value = Expr::Bin {
3132                op: BinOp::Ne,
3133                l: Box::new(taken),
3134                r: Box::new(Expr::Const(ConstVal::Int(0))),
3135                ty: Some(TypeRef::J(JavaType::Boolean)),
3136            };
3137        } else if !tt_bool && val_bool && numlike(tt) {
3138            if matches!(value, Expr::Cast { .. }) {
3139                return;
3140            }
3141            let taken = std::mem::replace(value, Expr::Const(ConstVal::Null));
3142            *value = Expr::Cast {
3143                ty: TypeRef::J(tt.clone()),
3144                e: Box::new(Expr::Cond {
3145                    c: Box::new(taken),
3146                    t: Box::new(Expr::Const(ConstVal::Int(1))),
3147                    f: Box::new(Expr::Const(ConstVal::Int(0))),
3148                }),
3149            };
3150        } else if numlike(tt) && numlike(&vt_val) {
3151            wrap(tt, value);
3152        }
3153    }
3154    walk_mut_deep(body, &mut |st| match st {
3155        Stmt::ExprStmt(Expr::Assign {
3156            target,
3157            op: AssignOp::Plain,
3158            value,
3159        }) => {
3160            let tt = match &**target {
3161                Expr::Local { var, .. } if (*var as usize) < vt.vars.len() => {
3162                    vt.var(*var).ty.erased()
3163                }
3164                Expr::Field { ty, .. } => ty.erased(),
3165                // Array store: the element type is the target type
3166                // (`sFormatStr[i] = v7` into char[] — "从int转换到char
3167                // 可能会有损失", weibo TimeUtils).
3168                Expr::ArrayIndex { array, .. } => match array.type_ref().erased() {
3169                    JavaType::Array(inner) => *inner,
3170                    _ => return,
3171                },
3172                _ => return,
3173            };
3174            coerce(&tt, value, vt);
3175        }
3176        Stmt::LocalDef {
3177            var,
3178            init: Some(value),
3179            ..
3180        } => {
3181            if (*var as usize) >= vt.vars.len() {
3182                return;
3183            }
3184            let tt = vt.var(*var).ty.erased();
3185            coerce(&tt, value, vt);
3186        }
3187        Stmt::Return(Some(e)) => {
3188            if !numlike(ret_ty) || !numlike(&val_ty(e, vt)) {
3189                return;
3190            }
3191            wrap(ret_ty, e);
3192        }
3193        _ => {}
3194    });
3195}
3196
3197/// Subclass-field downcast witness + assign-cast narrowing.
3198///
3199/// R8 pushes fields DOWN into leaf classes (weibo: coroutine `label`
3200/// lives on each `Foo$bar$1` continuation, NOT on the rendered
3201/// BaseContinuationImpl/ContinuationImpl supers) while a check-cast in
3202/// the same flow types the receiver as the SUPERCLASS. The dex field
3203/// ref names the leaf (`$1->label` — verifier-legal through the
3204/// instance-of branch type), but Java resolves `((ContinuationImpl) c)
3205/// .label` against ContinuationImpl — "找不到符号 变量 label" (weibo
3206/// ×420). When the field's dex owner is a strict subtype of the
3207/// receiver's static type and the receiver's hierarchy does NOT
3208/// declare the name, re-target the receiver cast to the owner (the
3209/// runtime object IS the owner type — the verifier proved it).
3210///
3211/// Second rule: a `(T) e` value assigned into an S-typed target where
3212/// S <: T strictly ("ContinuationImpl无法转换为$reportWhenComplete$1")
3213/// narrows the cast to S — same verifier guarantee, and an assignment
3214/// position has no overload resolution to perturb.
3215pub fn fix_field_owner_downcasts(body: &mut Stmt, vt: &VarTable, pool: &DexPool) {
3216    fn owner_internal(o: &Expr, vt: &VarTable) -> Option<std::sync::Arc<str>> {
3217        let t = match o {
3218            Expr::Local { var, .. } => vt.var(*var).ty.erased(),
3219            other => other.type_ref().erased(),
3220        };
3221        match t {
3222            JavaType::Object(n) => Some(n),
3223            _ => None,
3224        }
3225    }
3226    fn declares_up(pool: &DexPool, class: &str, name: &str) -> bool {
3227        let mut cur = Some(class.to_string());
3228        let mut hops = 0;
3229        while let Some(c) = cur {
3230            hops += 1;
3231            if hops > 64 {
3232                return false;
3233            }
3234            let Some(pc) = pool.get(&c) else { return false };
3235            if pc
3236                .instance_fields
3237                .iter()
3238                .chain(pc.static_fields.iter())
3239                .any(|f| f.name.as_str() == name)
3240            {
3241                return true;
3242            }
3243            cur = pc.super_name.clone();
3244        }
3245        false
3246    }
3247    walk_stmt_exprs(body, &mut |e| {
3248        deep_rewrite(e, &mut |x| {
3249            if let Expr::Field {
3250                owner: Some(o),
3251                cls,
3252                name,
3253                is_static: false,
3254                ..
3255            } = x
3256            {
3257                let Some(ot) = owner_internal(o, vt) else {
3258                    return;
3259                };
3260                if ot.as_ref() == cls.as_ref() || !pool.is_subtype(cls, &ot) {
3261                    return;
3262                }
3263                if declares_up(pool, &ot, name) || !declares_up(pool, cls, name) {
3264                    return;
3265                }
3266                let ty = TypeRef::J(JavaType::Object(cls.clone()));
3267                if let Expr::Cast { ty: ct, .. } = &mut **o {
3268                    *ct = ty;
3269                } else {
3270                    let taken = std::mem::replace(&mut **o, Expr::This);
3271                    **o = Expr::Cast { ty, e: Box::new(taken) };
3272                }
3273            }
3274            // Rule 2: `(T) e` into a strict-subtype target narrows to S.
3275            let (tgt_ty, value) = match x {
3276                Expr::Assign { target, op: AssignOp::Plain, value } => {
3277                    let t = match &**target {
3278                        Expr::Local { var, .. } => vt.var(*var).ty.erased(),
3279                        Expr::Field { ty, .. } => ty.erased(),
3280                        _ => return,
3281                    };
3282                    (t, value)
3283                }
3284                _ => return,
3285            };
3286            let JavaType::Object(sn) = tgt_ty else { return };
3287            if let Expr::Cast { ty, .. } = &mut **value {
3288                if let TypeRef::J(JavaType::Object(tn)) = ty {
3289                    if tn.as_ref() != sn.as_ref() && pool.is_subtype(&sn, tn) {
3290                        *ty = TypeRef::J(JavaType::Object(sn.clone()));
3291                    }
3292                }
3293            }
3294        });
3295    });
3296    // LocalDef inits: same narrowing against the declared var type.
3297    walk_mut_deep(body, &mut |st| {
3298        if let Stmt::LocalDef { var, init: Some(value), .. } = st {
3299            let tt = if (*var as usize) < vt.vars.len() {
3300                vt.vars[*var as usize].ty.erased()
3301            } else {
3302                return;
3303            };
3304            let JavaType::Object(sn) = tt else { return };
3305            if let Expr::Cast { ty, .. } = value {
3306                if let TypeRef::J(JavaType::Object(tn)) = ty {
3307                    if tn.as_ref() != sn.as_ref() && pool.is_subtype(&sn, tn) {
3308                        *ty = TypeRef::J(JavaType::Object(sn.clone()));
3309                    }
3310                }
3311            }
3312        }
3313    });
3314}
3315
3316/// `==`/`!=` between UNRELATED class types is a compile error in Java
3317/// ("不可比较的类型: f0和a") while dex if-eqObj happily compares any two
3318/// references (weixin fn1/w1 ×298 files). One side takes an `(Object)`
3319/// witness cast — legal against anything. Interfaces and subtype-related
3320/// pairs are already comparable and stay untouched; java/lang/Object and
3321/// arrays-of it likewise.
3322pub fn fix_incomparable_equality(body: &mut Stmt, vt: &VarTable, pool: &crate::DexPool) {
3323    fn side_ty(e: &Expr, vt: &VarTable) -> JavaType {
3324        match e {
3325            Expr::Local { var, .. } if (*var as usize) < vt.vars.len() => {
3326                vt.var(*var).ty.erased()
3327            }
3328            other => other.type_ref().erased(),
3329        }
3330    }
3331    let incomparable = |a: &JavaType, b: &JavaType| -> bool {
3332        use JavaType::{Array, Object};
3333        match (a, b) {
3334            (Object(x), Object(y)) => {
3335                if x == y || x.as_ref() == "java/lang/Object" || y.as_ref() == "java/lang/Object"
3336                {
3337                    return false;
3338                }
3339                let xi = pool.get(x).map(|c| c.is_interface()).unwrap_or(true);
3340                let yi = pool.get(y).map(|c| c.is_interface()).unwrap_or(true);
3341                // Unknown classes (framework, not in pool): a cast is
3342                // harmless but usually unnecessary — treat as comparable
3343                // to stay conservative.
3344                if xi || yi {
3345                    return false;
3346                }
3347                !pool.is_subtype(x, y) && !pool.is_subtype(y, x)
3348            }
3349            (Array(_), Object(y)) | (Object(y), Array(_)) => {
3350                y.as_ref() != "java/lang/Object"
3351                    && y.as_ref() != "java/lang/Cloneable"
3352                    && y.as_ref() != "java/io/Serializable"
3353                    && !pool.get(y).map(|c| c.is_interface()).unwrap_or(false)
3354            }
3355            _ => false,
3356        }
3357    };
3358    let is_ref = |t: &JavaType| matches!(t, JavaType::Object(_) | JavaType::Array(_));
3359    walk_stmt_exprs(body, &mut |e| {
3360        deep_rewrite(e, &mut |x| {
3361            if let Expr::Bin {
3362                op: BinOp::Eq | BinOp::Ne,
3363                l,
3364                r,
3365                ..
3366            } = x
3367            {
3368                let lt = side_ty(l, vt);
3369                let rt = side_ty(r, vt);
3370                // `obj != 0` → `obj != null` with FINAL types: the
3371                // lift-time null_side_rewrite ran before infer_types
3372                // (its obj table saw the pre-inference int), leaving
3373                // `sQLiteClosable != 0` — "二元运算符 '!=' 的操作数
3374                // 类型错误" ×700 weixin.
3375                if is_ref(&lt) && matches!(&**r, Expr::Const(ConstVal::Int(0))) {
3376                    **r = Expr::Const(ConstVal::Null);
3377                    return;
3378                }
3379                if is_ref(&rt) && matches!(&**l, Expr::Const(ConstVal::Int(0))) {
3380                    **l = Expr::Const(ConstVal::Null);
3381                    return;
3382                }
3383                if incomparable(&lt, &rt) {
3384                    let taken = std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
3385                    **l = Expr::Cast {
3386                        ty: TypeRef::J(JavaType::Object("java/lang/Object".into())),
3387                        e: taken,
3388                    };
3389                }
3390            }
3391        });
3392    });
3393}
3394
3395/// Primitive bridges at CALL/CTOR argument positions. Dex slots are
3396/// untyped category-1 integers: the verifier happily passes a Z-slot to
3397/// a `(B)` formal (weibo's Meituan-Robust hotpatch boilerplate boxes
3398/// boolean params via `new Byte(zreg)` — 4.2k+ `对于Byte(boolean),
3399/// 找不到合适的构造器`), and obfuscated/Kotlin code passes 0/1 slots to
3400/// `(Z)` formals (`int无法转换为boolean` at call sites). Java source
3401/// cannot express either — bridge at the IR level:
3402///   Boolean actual → numeric formal:  `(byte) (b ? 1 : 0)`
3403///   numeric actual → Boolean formal:  `x != 0`
3404/// The target method is DESCRIPTOR-bound (bytecode method_ref), so a
3405/// cast/compare to the exact formal type cannot flip overload
3406/// resolution the way r57's inferred object casts did — the rendered
3407/// call selects the same signature the dex named.
3408pub fn fix_primitive_arg_bridges(body: &mut Stmt, vt: &VarTable, pool: &crate::DexPool) {
3409    fn val_ty(e: &Expr, vt: &VarTable) -> JavaType {
3410        match e {
3411            Expr::Local { var, .. } => vt.var(*var).ty.erased(),
3412            other => other.type_ref().erased(),
3413        }
3414    }
3415    #[allow(clippy::type_complexity)]
3416    let is_bool = |t: &JavaType| matches!(t, JavaType::Boolean);
3417    let is_num = |t: &JavaType| {
3418        matches!(
3419            t,
3420            JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
3421                | JavaType::Long | JavaType::Float | JavaType::Double
3422        )
3423    };
3424    // BORROWED-arg form: returns the bridged replacement without
3425    // consuming the original (a `mem::replace` + `Option` dance dropped
3426    // the argument on the None path — every Boolean→Boolean arg became
3427    // a literal `null`: 61k 引用不明确 in one battery run).
3428    fn bridge(actual: &JavaType, formal: &JavaType, arg: &Expr) -> Option<Expr> {
3429        // A dex const carries the CALLEE's BITS: `const v, #0x41000000`
3430        // feeding a float formal IS 8.0f. Rendering the raw int literal
3431        // (a) mis-selects overloads — a competing `a(Context,int):void`
3432        // beats the int→float-widened `a(Context,float):int` in javac's
3433        // phase order ("不兼容的类型: void无法转换为int", weibo ju.a
3434        // ×1,259) — and (b) corrupts the VALUE (widening turns the bits
3435        // into 1.09e9f). The descriptor formal is dex ground truth:
3436        // reinterpret. Symmetric for the float-const-into-int-formal
3437        // direction (caller's bits, callee's reading).
3438        match (arg, formal) {
3439            (Expr::Const(ConstVal::Int(b)), JavaType::Float) => {
3440                return Some(Expr::Const(ConstVal::Float(f32::from_bits(*b as u32))));
3441            }
3442            (Expr::Const(ConstVal::Long(b)), JavaType::Double) => {
3443                return Some(Expr::Const(ConstVal::Double(f64::from_bits(*b as u64))));
3444            }
3445            (Expr::Const(ConstVal::Float(f)), JavaType::Int) => {
3446                return Some(Expr::Const(ConstVal::Int(f.to_bits() as i32)));
3447            }
3448            (Expr::Const(ConstVal::Double(d)), JavaType::Long) => {
3449                return Some(Expr::Const(ConstVal::Long(d.to_bits() as i64)));
3450            }
3451            _ => {}
3452        }
3453        if matches!(actual, JavaType::Boolean)
3454            && matches!(
3455                formal,
3456                JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
3457                    | JavaType::Long | JavaType::Float | JavaType::Double
3458            )
3459        {
3460            Some(Expr::Cast {
3461                ty: TypeRef::J(formal.clone()),
3462                e: Box::new(Expr::Cond {
3463                    c: Box::new(arg.clone()),
3464                    t: Box::new(Expr::Const(ConstVal::Int(1))),
3465                    f: Box::new(Expr::Const(ConstVal::Int(0))),
3466                }),
3467            })
3468        } else if matches!(formal, JavaType::Boolean)
3469            && matches!(
3470                actual,
3471                JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
3472                    | JavaType::Long | JavaType::Float | JavaType::Double
3473            )
3474        {
3475            Some(Expr::Bin {
3476                op: BinOp::Ne,
3477                l: Box::new(arg.clone()),
3478                r: Box::new(Expr::Const(ConstVal::Int(0))),
3479                ty: Some(TypeRef::J(JavaType::Boolean)),
3480            })
3481        } else if matches!(
3482            actual,
3483            JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
3484                | JavaType::Long | JavaType::Float | JavaType::Double
3485        ) && matches!(
3486            formal,
3487            JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
3488                | JavaType::Long | JavaType::Float | JavaType::Double
3489        ) && !implicit_widening(actual, formal)
3490        {
3491            // Numeric NARROWING into a formal (int arg → char formal):
3492            // javac rejects the lossy conversion, or — worse when the
3493            // class has no wider overload — silently resolves a
3494            // DIFFERENT overload than the dex descriptor names
3495            // (SpannableStringBuilder.append(int) doesn't exist;
3496            // `append(char)` is "从int转换到char可能会有损失", weibo ×76).
3497            // The descriptor formal is the dex ground truth; casting to
3498            // it pins the overload the bytecode actually invoked.
3499            Some(Expr::Cast {
3500                ty: TypeRef::J(formal.clone()),
3501                e: Box::new(arg.clone()),
3502            })
3503        } else {
3504            None
3505        }
3506    }
3507    /// A bare `null` argument against a SPECIFIC reference formal takes
3508    /// the descriptor cast: `this(context, null)` matching both
3509    /// Builder(Context,Notification) and Builder(Context,String) is
3510    /// "对Builder的引用不明确" (weibo NotificationCompat; protobuf
3511    /// `new FieldSet.Builder(null)` ×54). The descriptor names the
3512    /// exact invoked ctor, so the cast is dex ground truth — the
3513    /// descriptor-exact doctrine, not a guessed overload shift. Bare
3514    /// null stays for Object formals (casting those would add noise
3515    /// without resolving anything javac could not already pick).
3516    /// Framework classes javac resolves through android.jar even when
3517    /// the dex pool lacks them — org.json.JSONObject is provided by the
3518    /// platform, so `publishGlobalEventToTopPage(long,String,null)`
3519    /// against a JSONObject formal is cast-resolvable (weixin ambiguous
3520    /// ×14) despite failing the pool lookup.
3521    fn fw_resolvable(n: &str) -> bool {
3522        n.starts_with("java/")
3523            || n.starts_with("javax/")
3524            || n.starts_with("android/")
3525            || n.starts_with("dalvik/")
3526            || n.starts_with("junit/")
3527            || n.starts_with("org/json/")
3528            || n.starts_with("org/w3c/")
3529            || n.starts_with("org/xml/")
3530            || n.starts_with("org/xmlpull/")
3531            || n.starts_with("org/apache/http/")
3532    }
3533    fn null_arg_cast(a: &mut Expr, f: &JavaType, pool: &DexPool) -> bool {
3534        if !matches!(&*a, Expr::Const(ConstVal::Null)) {
3535            return false;
3536        }
3537        if let JavaType::Object(n) = f {
3538            // Cast only to RESOLVABLE types: a phantom formal (R8 kept
3539            // the ref, dropped the class — androidx.window.sidecar)
3540            // would turn a compiling bare `null` into a fresh
3541            // cannot-find. Pool classes render into the output;
3542            // java/javax/android/dalvik resolve through android.jar.
3543            let resolvable = pool.get(n).is_some() || fw_resolvable(n);
3544            if n.as_ref() != "java/lang/Object" && resolvable {
3545                *a = Expr::Cast {
3546                    ty: TypeRef::J(f.clone()),
3547                    e: Box::new(Expr::Const(ConstVal::Null)),
3548                };
3549                return true;
3550            }
3551        } else if let JavaType::Array(_) = f {
3552            // Descriptor-exact ARRAY cast: the dex named this exact
3553            // overload; a bare null beside sibling overloads is javac-
3554            // ambiguous (`a(String,b2)` + `a(String,String[])` both match
3555            // — weibo org/a/c/b, 对a的引用不明确 family). Same phantom
3556            // gate on the element class: casting to a phantom-element
3557            // array would trade the ambiguity for a cannot-find.
3558            let mut e: &JavaType = f;
3559            while let JavaType::Array(inner) = e {
3560                e = &**inner;
3561            }
3562            let resolvable = match e {
3563                JavaType::Object(n) => pool.get(n).is_some() || fw_resolvable(n),
3564                _ => true, // primitive-element array
3565            };
3566            if resolvable {
3567                *a = Expr::Cast {
3568                    ty: TypeRef::J(f.clone()),
3569                    e: Box::new(Expr::Const(ConstVal::Null)),
3570                };
3571                return true;
3572            }
3573        }
3574        // Bare null deliberately stays (Object formal / phantom type):
3575        // the caller's ambiguity pin may still act on it.
3576        false
3577    }
3578    /// JLS 5.1.2 widening primitive conversion (char joins the chain
3579    /// only at int; byte/short do NOT widen to char).
3580    fn implicit_widening(a: &JavaType, f: &JavaType) -> bool {
3581        use JavaType::*;
3582        match (a, f) {
3583            (Byte, Short) | (Byte, Int) | (Byte, Long) | (Byte, Float) | (Byte, Double) => true,
3584            (Short, Int) | (Short, Long) | (Short, Float) | (Short, Double) => true,
3585            (Char, Int) | (Char, Long) | (Char, Float) | (Char, Double) => true,
3586            (Int, Long) | (Int, Float) | (Int, Double) => true,
3587            (Long, Float) | (Long, Double) => true,
3588            (Float, Double) => true,
3589            (x, y) => x == y,
3590        }
3591    }
3592    // Does the owner class declare ANOTHER method with the same name
3593    // and arity but a different signature? Then javac's most-specific
3594    // overload resolution can steal the call from the dex descriptor's
3595    // target (weibo 对a的引用不明确 ×391: `c.a(null)` against
3596    // a(ExecuteResult)/a(Exception)/a(Object) — the descriptor names
3597    // a(Object), but bare null leaves the two specific overloads
3598    // ambiguously most-specific). Framework owners are not in the
3599    // pool — conservatively report no competition.
3600    let overload_competes = |cls: &str, name: &str, args: &[JavaType]| -> bool {
3601        let Some(c) = pool.get(cls) else {
3602            return false;
3603        };
3604        c.all_methods().any(|m| {
3605            &*m.name == name
3606                && m.parsed_desc()
3607                    .is_some_and(|d| d.args.len() == args.len() && d.args != args)
3608        })
3609    };
3610    // Ctors: formal types via the pool (unanimous across same-arity
3611    // overloads, else skip — ambiguity must not guess).
3612    let ctor_formals = |cls: &str, n: usize| -> Option<Vec<JavaType>> {
3613        let c = pool.get(cls)?;
3614        let mut out: Option<Vec<JavaType>> = None;
3615        for m in c.all_methods() {
3616            if &*m.name != "<init>" {
3617                continue;
3618            }
3619            let Some(d) = m.parsed_desc() else { continue };
3620            if d.args.len() != n {
3621                continue;
3622            }
3623            match &out {
3624                None => out = Some(d.args.clone()),
3625                Some(prev) if *prev != d.args => return None,
3626                Some(_) => {}
3627            }
3628        }
3629        out
3630    };
3631    walk_stmt_exprs(body, &mut |e| {
3632        deep_rewrite(e, &mut |x| match x {
3633            Expr::Method {
3634                cls,
3635                name,
3636                desc,
3637                args,
3638                is_static,
3639                ..
3640            } => {
3641                if args.len() != desc.args.len() {
3642                    return;
3643                }
3644                let _ = is_static;
3645                let mut competes: Option<bool> = None;
3646                for (a, f) in args.iter_mut().zip(desc.args.iter()) {
3647                    if null_arg_cast(a, f, pool) {
3648                        continue;
3649                    }
3650                    if matches!(a, Expr::Cast { .. }) {
3651                        continue;
3652                    }
3653                    // Descriptor-exact ambiguity pin: a bare null
3654                    // against an Object formal (null_arg_cast
3655                    // deliberately leaves those bare — safe only
3656                    // WITHOUT competing specific overloads) or a
3657                    // strict-subtype arg can match several declared
3658                    // overloads; casting to the descriptor formal
3659                    // leaves exactly the dex-invoked method
3660                    // applicable (JLS 15.12.2 — the descriptor is
3661                    // ground truth, so the cast pins resolution
3662                    // instead of shifting it).
3663                    let subtype_arg = match (&val_ty(a, vt), f) {
3664                        (JavaType::Object(an), JavaType::Object(fn_)) => {
3665                            an != fn_ && pool.is_subtype(an, fn_)
3666                        }
3667                        _ => false,
3668                    };
3669                    // Bare null against java/lang/Object itself: the
3670                    // descriptor method exists (dex resolved it), so
3671                    // `(Object) null` is always legal and excludes every
3672                    // more-specific sibling overload from applicability.
3673                    // Pool owners take the competes gate (emitted
3674                    // declarations are raw, so the cast cannot disturb
3675                    // inference). FRAMEWORK owners only via allowlist of
3676                    // the classic non-generic null-ambiguous JDK methods
3677                    // (StringBuilder.append ×66 lark): a blanket cast
3678                    // breaks GENERIC framework methods whose descriptor
3679                    // formal is an ERASED type variable —
3680                    // `ofFloat((Object) null, View.ALPHA, v)` killed the
3681                    // T=View inference bare null provided (weibo +4).
3682                    let null_obj =
3683                        matches!(a, Expr::Const(ConstVal::Null)) && matches!(f, JavaType::Object(_));
3684                    let framework_owner = pool.get(cls.as_ref()).is_none();
3685                    let fw_null_ambiguous = framework_owner
3686                        && matches!(
3687                            (cls.as_ref(), name.as_ref()),
3688                            ("java/lang/StringBuilder", "append")
3689                                | ("java/lang/StringBuffer", "append")
3690                                | ("java/io/PrintStream", "println")
3691                                | ("java/io/PrintStream", "print")
3692                                | ("java/io/PrintWriter", "println")
3693                                | ("java/io/PrintWriter", "print")
3694                                | ("java/lang/String", "valueOf")
3695                        );
3696                    // Framework owners with famously overload-heavy
3697                    // methods take the subtype pin too: the arg's
3698                    // rendered type implements SEVERAL formal types
3699                    // (weibo putExtra ×41: floatMsgData is both
3700                    // Parcelable and Serializable; schedule/submit:
3701                    // Runnable-and-Callable workers) and the competes
3702                    // gate cannot enumerate framework overloads.
3703                    // Formal == java/lang/Object stays excluded — that
3704                    // is an erased type variable and the cast would
3705                    // kill generic inference (ofFloat lesson).
3706                    let fw_subtype_pinnable = framework_owner
3707                        && subtype_arg
3708                        && !matches!(f, JavaType::Object(fn_) if fn_.as_ref() == "java/lang/Object")
3709                        && matches!(
3710                            (cls.as_ref(), name.as_ref()),
3711                            ("android/content/Intent", "putExtra")
3712                                | ("java/util/concurrent/ScheduledExecutorService", "schedule")
3713                                | ("java/util/concurrent/ExecutorService", "submit")
3714                                | ("java/util/concurrent/ExecutorService", "invokeAll")
3715                        );
3716                    if fw_subtype_pinnable
3717                        || (subtype_arg
3718                            && *competes.get_or_insert_with(|| {
3719                                overload_competes(cls, name, &desc.args)
3720                            }))
3721                        || (null_obj
3722                            && (fw_null_ambiguous
3723                                || *competes.get_or_insert_with(|| {
3724                                    overload_competes(cls, name, &desc.args)
3725                                })))
3726                    {
3727                        let old =
3728                            std::mem::replace(a, Expr::Const(ConstVal::Null));
3729                        *a = Expr::Cast {
3730                            ty: TypeRef::J(f.clone()),
3731                            e: Box::new(old),
3732                        };
3733                        continue;
3734                    }
3735                    if !is_bool(&val_ty(a, vt)) && !is_num(&val_ty(a, vt)) {
3736                        continue;
3737                    }
3738                    if !is_bool(f) && !is_num(f) {
3739                        continue;
3740                    }
3741                    if let Some(b) = bridge(&val_ty(a, vt), f, a) {
3742                        *a = b;
3743                    }
3744                }
3745            }
3746            Expr::New { cls, args, arg_tys, .. } => {
3747                let formals_owned = if arg_tys.len() == args.len() && !arg_tys.is_empty() {
3748                    Some(arg_tys.clone())
3749                } else {
3750                    ctor_formals(cls, args.len())
3751                };
3752                if let Some(formals) = formals_owned {
3753                    for (a, f) in args.iter_mut().zip(formals.iter()) {
3754                        if null_arg_cast(a, f, pool) {
3755                            continue;
3756                        }
3757                        if matches!(a, Expr::Cast { .. }) {
3758                            continue;
3759                        }
3760                        if !is_bool(f) && !is_num(f) {
3761                            continue;
3762                        }
3763                        if let Some(b) = bridge(&val_ty(a, vt), f, a) {
3764                            *a = b;
3765                        }
3766                    }
3767                }
3768            }
3769            _ => {}
3770        });
3771    });
3772}
3773
3774/// `b ^ 1` / `1 ^ b` with a BOOLEAN operand is the bytecode's boolean
3775/// negation (`!b`) — weibo kotlin-stdlib `return v2 ^ 1;` ("二元运算符
3776/// '^' 的操作数类型错误"), and `x ^ true` shapes. Java only types `^`
3777/// for same-category operands.
3778/// Rename locals whose name equals a ROOT PACKAGE first segment: a
3779/// package-qualified render (`v2.n.a` — the FQN fallback a twin-shadowed
3780/// same-package ref must take) binds its first identifier lexically, and
3781/// a local `v2` in scope captures it (weixin v2/i: 找不到符号 变量 n on a
3782/// legit `v2.n.a(this, 0)`). Method-scope renames carry no registry
3783/// surface — every use renders through this same vt — unlike class/field
3784/// renames, whose reference coverage blew up five times before.
3785pub fn deshadow_locals(vt: &mut VarTable, pool: &crate::DexPool, class_name: &str, body: &Stmt) {
3786    let segs = pool.root_pkg_segs();
3787    // Import-layer simples: the file renders `import p.h;` + bare `h.e`
3788    // for obscured refs — a local `h` captures that simple name (locals
3789    // beat imports in expression position; 无法取消引用int family).
3790    let obscured = crate::classdec::obscured_simples_snapshot();
3791    // Same-package static-ref simples: a static member ref renders its
3792    // class qualifier as the SIMPLE name (same package — no import/FQN),
3793    // and a local with that name captures it (`a2.changeQuickRedirect`
3794    // bound to a String local `a2` — "找不到符号 变量 changeQuickRedirect
3795    // 位置: 类型为String的变量 a2", weibo robust-field refs ×317). Only
3796    // STATIC refs qualify (an instance ref's qualifier IS the local).
3797    let self_pkg = class_name.rfind('/').map(|i| &class_name[..i]).unwrap_or("");
3798    let mut same_pkg_simples: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
3799    visit_all_exprs(body, &mut |x| {
3800        let cls = match x {
3801            Expr::Field { cls, is_static: true, .. } => Some(cls.as_ref()),
3802            Expr::Method { cls, is_static: true, .. } => Some(cls.as_ref()),
3803            _ => None,
3804        };
3805        if let Some(c) = cls {
3806            let pkg = c.rfind('/').map(|i| &c[..i]).unwrap_or("");
3807            if pkg == self_pkg {
3808                let simple = &c[c.rfind('/').map(|i| i + 1).unwrap_or(0)..];
3809                let head = simple.split('$').next().unwrap_or(simple);
3810                if !head.is_empty() {
3811                    same_pkg_simples.insert(head.to_string());
3812                }
3813            }
3814        }
3815    });
3816    let hit = |name: &str| {
3817        segs.contains(name) || obscured.contains(name) || same_pkg_simples.contains(name)
3818    };
3819    if (segs.is_empty() && obscured.is_empty() && same_pkg_simples.is_empty())
3820        || !vt.vars.iter().any(|v| hit(&v.name))
3821    {
3822        return;
3823    }
3824    let mut taken: jdc_core::FxHashSet<String> =
3825        vt.vars.iter().map(|v| v.name.clone()).collect();
3826    for v in vt.vars.iter_mut() {
3827        if !hit(&v.name) {
3828            continue;
3829        }
3830        let mut k = 0u32;
3831        let cand = loop {
3832            k += 1;
3833            let c = if k == 1 {
3834                format!("{}x", v.name)
3835            } else {
3836                format!("{}x{}", v.name, k)
3837            };
3838            if !taken.contains(&c) {
3839                break c;
3840            }
3841        };
3842        taken.insert(cand.clone());
3843        v.name = cand;
3844    }
3845}
3846
3847/// Receiver rescue: a Field/Method receiver whose final vt type is
3848/// PRIMITIVE can never hold the member (structurer tail-copy regions
3849/// can map the receiver register onto a boolean/int generation —
3850/// wcdb HandleOperation's `v10_g2.finalizeStatement()` where the
3851/// statement var is right there; weixin deref-int/bool families,
3852/// ~2.5k lines). The member descriptor's owner class is authoritative:
3853/// when EXACTLY ONE var in the method has that type (or a subtype),
3854/// swap the receiver to it. The pre-rescue output is a guaranteed
3855/// compile error AND runtime NPE, so the unambiguous swap cannot make
3856/// it worse; ambiguous sites stay untouched.
3857/// Anonymous/inner-class outer reads: the dex reads `this$0` off the
3858/// instance register, which copy materialization can route through a
3859/// local (`v41 = this; ... v41.this$0` — weibo HorseRaceDetector$1,
3860/// 2,326-line 找不到符号 变量 this$0 family). When the field's
3861/// declaring class IS the class being decompiled, the only instance in
3862/// scope is `this` — strip the owner so it renders the bare declared
3863/// field.
3864/// Fold single-assigned `this`-copy locals in ctors back to `this`.
3865/// R8 materializes the receiver with `move-object/from16 v1, p0` and
3866/// runs every iput through the copy; the copy renders as a local and a
3867/// final-field store through it is "无法为 final 变量 g 分配值" (Java
3868/// allows blank-final writes only via `this`/simple name — hf/n2 ×29,
3869/// weixin final-var residual). Transitively: `b = a; c = b` chains
3870/// collapse. Single-assignment gate: a var ever written from another
3871/// value is not an alias.
3872pub fn fix_ctor_this_aliases(body: &mut Stmt, vt: &VarTable) {
3873    let Some(this_var) = vt
3874        .vars
3875        .iter()
3876        .find(|v| v.is_param && v.name == "this")
3877        .map(|v| v.id)
3878    else {
3879        return;
3880    };
3881    let mut analysis = VarAnalysis {
3882        assigns: Vec::new(),
3883        reads: Vec::new(),
3884        values: Vec::new(),
3885    };
3886    analyze_vars(body, &mut analysis);
3887    let n = vt.vars.len().max(analysis.assigns.len());
3888    let mut alias = vec![false; n];
3889    for _ in 0..4 {
3890        let mut changed = false;
3891        for v in 0..n as u32 {
3892            if alias[v as usize] {
3893                continue;
3894            }
3895            if analysis.assigns.get(v as usize).copied().unwrap_or(0) != 1 {
3896                continue;
3897            }
3898            let Some(Expr::Local { var: src, .. }) = analysis.values.get(v as usize).and_then(|o| o.as_ref()) else {
3899                continue;
3900            };
3901            if *src == this_var
3902                || (*src < n as u32 && alias[*src as usize])
3903            {
3904                alias[v as usize] = true;
3905                changed = true;
3906            }
3907        }
3908        if !changed {
3909            break;
3910        }
3911    }
3912    if !alias.iter().any(|&b| b) {
3913        return;
3914    }
3915    walk_stmt_exprs(body, &mut |e| {
3916        deep_rewrite_reads(e, &mut |x| {
3917            if let Expr::Local { var, .. } = x {
3918                if (*var as usize) < n && alias[*var as usize] {
3919                    *x = Expr::This;
3920                }
3921            }
3922        });
3923    });
3924}
3925
3926pub fn fix_this0_owners(body: &mut Stmt, class_name: &str) {
3927    walk_stmt_exprs(body, &mut |e| {
3928        deep_rewrite(e, &mut |x| {
3929            if let Expr::Field { owner: Some(_), cls, name, .. } = x {
3930                if name.as_ref() == "this$0" && cls.as_ref() == class_name {
3931                    if let Expr::Field { owner, .. } = x {
3932                        *owner = None;
3933                    }
3934                }
3935            }
3936        });
3937    });
3938}
3939
3940pub fn rescue_primitive_receivers(body: &mut Stmt, vt: &VarTable, pool: &crate::DexPool) {
3941    fn prim(t: &JavaType) -> bool {
3942        match t {
3943            JavaType::Int
3944            | JavaType::Long
3945            | JavaType::Short
3946            | JavaType::Byte
3947            | JavaType::Char
3948            | JavaType::Float
3949            | JavaType::Double
3950            | JavaType::Boolean => true,
3951            // Boxed JDK finals carry no app members either (`bool19.d`
3952            // where bool19 came out java/lang/Boolean — the coroutine
3953            // Result-register conflation).
3954            JavaType::Object(n) => matches!(
3955                n.as_ref(),
3956                "java/lang/Boolean"
3957                    | "java/lang/String"
3958                    | "java/lang/Integer"
3959                    | "java/lang/Long"
3960                    | "java/lang/Short"
3961                    | "java/lang/Byte"
3962                    | "java/lang/Character"
3963                    | "java/lang/Float"
3964                    | "java/lang/Double"
3965            ),
3966            _ => false,
3967        }
3968    }
3969    walk_stmt_exprs(body, &mut |e| {
3970        deep_rewrite(e, &mut |x| {
3971            // Phase 1 (read-only): primitive-typed Local receiver?
3972            let recv_var = {
3973                let o: &Expr = match &*x {
3974                    Expr::Method { owner: Some(o), .. } => o,
3975                    Expr::Field { owner: Some(o), is_static: false, .. } => o,
3976                    _ => return,
3977                };
3978                let Expr::Local { var, ty } = o else {
3979                    return;
3980                };
3981                let vt_ty = if (*var as usize) < vt.vars.len() {
3982                    vt.var(*var).ty.erased()
3983                } else {
3984                    ty.erased()
3985                };
3986                if !prim(&vt_ty) {
3987                    return;
3988                }
3989                *var
3990            };
3991            let cls: std::sync::Arc<str> = match &*x {
3992                Expr::Method { cls, .. } | Expr::Field { cls, .. } => cls.clone(),
3993                _ => return,
3994            };
3995            // Phase 2: vars of the owner type (exact or subtype).
3996            let mut cands: Vec<u32> = Vec::new();
3997            for v in &vt.vars {
3998                if let JavaType::Object(n) = v.ty.erased() {
3999                    if n.as_ref() == cls.as_ref()
4000                        || pool.is_subtype(n.as_ref(), cls.as_ref())
4001                    {
4002                        cands.push(v.id);
4003                    }
4004                }
4005            }
4006            let id = if cands.len() == 1 {
4007                cands[0]
4008            } else if cands.len() > 1 {
4009                // Multiple owner-typed vars: prefer the same-SLOT one
4010                // (register lineage), latest generation wins — the
4011                // receiver position is scope-insensitive (any method-
4012                // scope var renders, the decl hoist covers it).
4013                let slot = if (recv_var as usize) < vt.vars.len() {
4014                    Some(vt.var(recv_var).slot)
4015                } else {
4016                    None
4017                };
4018                let mut hits: Vec<u32> = match slot {
4019                    Some(sl) => cands
4020                        .iter()
4021                        .copied()
4022                        .filter(|&c| {
4023                            c != recv_var
4024                                && (c as usize) < vt.vars.len()
4025                                && vt.var(c).slot == sl
4026                        })
4027                        .collect(),
4028                    None => Vec::new(),
4029                };
4030                if hits.is_empty() {
4031                    // No same-slot twin: fall back to the latest
4032                    // owner-typed generation (max id).
4033                    hits = cands
4034                        .iter()
4035                        .copied()
4036                        .filter(|&c| c != recv_var)
4037                        .collect();
4038                }
4039                hits.sort_unstable();
4040                hits.dedup();
4041                if hits.is_empty() {
4042                    return;
4043                }
4044                *hits.last().unwrap()
4045            } else {
4046                // Lineage disambiguation by SLOT: register reuse makes
4047                // the Boolean/primitive twin and the object version of
4048                // one dex register share VarInfo.slot (ry0/h2's
4049                // `bool19.d` — the v1-typed twin rides the same
4050                // register). Multiple slot matches stay untouched
4051                // (deterministic conservatism).
4052                if cands.is_empty() {
4053                    return;
4054                }
4055                let slot = if (recv_var as usize) < vt.vars.len() {
4056                    vt.var(recv_var).slot
4057                } else {
4058                    return;
4059                };
4060                let mut hits: Vec<u32> = cands
4061                    .into_iter()
4062                    .filter(|&c| {
4063                        c != recv_var
4064                            && (c as usize) < vt.vars.len()
4065                            && vt.var(c).slot == slot
4066                    })
4067                    .collect();
4068                hits.sort_unstable();
4069                hits.dedup();
4070                if hits.len() != 1 {
4071                    return;
4072                }
4073                hits[0]
4074            };
4075            // Phase 3: swap.
4076            let o: &mut Box<Expr> = match x {
4077                Expr::Method { owner: Some(o), .. } => o,
4078                Expr::Field { owner: Some(o), is_static: false, .. } => o,
4079                _ => return,
4080            };
4081            if let Expr::Local { var, ty } = &mut **o {
4082                if *var == recv_var {
4083                    *var = id;
4084                    *ty = vt.vars[id as usize].ty.clone();
4085                }
4086            }
4087        });
4088    });
4089
4090}
4091
4092/// Scope-safe arg/return swaps for mistyped generations — runs AFTER
4093/// ensure_declared hoists every declaration to the method top, so the
4094/// top_ids visibility precondition holds by construction (the r134
4095/// falsification: swapping in a block-scoped var broke lexically).
4096/// Same-slot + type-match + uniqueness gates as the receiver rescue.
4097pub fn rescue_arg_return_swaps(
4098    body: &mut Stmt,
4099    vt: &VarTable,
4100    pool: &crate::DexPool,
4101    ret_ty: &JavaType,
4102) {
4103    // SCOPE-SAFE arg/return swap: a primitive-typed actual against a
4104    // specific-reference formal (or a reference-returning `return`) is
4105    // a mistyped generation — the r134 falsification showed the swap
4106    // target MUST be lexically visible at the use site, so candidates
4107    // are restricted to params + TOP-LEVEL declared locals (a hoisted
4108    // decl is visible to javac name resolution everywhere in the
4109    // method) AND same-slot (same dex register lineage) AND unique.
4110    let mut top_ids: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
4111    for v in &vt.vars {
4112        if v.is_param {
4113            top_ids.insert(v.id);
4114        }
4115    }
4116    if let Stmt::Block(stmts) = body {
4117        for st in stmts.iter() {
4118            if let Stmt::LocalDef { var, .. } = st {
4119                top_ids.insert(*var);
4120            }
4121        }
4122    }
4123    let ret_wants_ref = match ret_ty {
4124        JavaType::Object(n) => n.as_ref() != "java/lang/Object",
4125        JavaType::Array(_) => true,
4126        _ => false,
4127    };
4128    let try_swap = |a: &mut Expr, want: &JavaType| {
4129        let Expr::Local { var, ty } = &mut *a else {
4130            return;
4131        };
4132        if (*var as usize) >= vt.vars.len() {
4133            return;
4134        }
4135        let avt = vt.var(*var).ty.erased();
4136        let avt_ref = matches!(avt, JavaType::Object(_) | JavaType::Array(_));
4137        let want_arr = matches!(want, JavaType::Array(_));
4138        // Only swap a primitive-typed actual (an Object actual gets the
4139        // descriptor-exact cast instead — never both).
4140        if avt_ref && !(avt == JavaType::Object("java/lang/Object".into())) {
4141            return;
4142        }
4143        if avt_ref && !matches!(want, JavaType::Object(_)) {
4144            return;
4145        }
4146        let slot = vt.var(*var).slot;
4147        let mut hits: Vec<u32> = Vec::new();
4148        for v in &vt.vars {
4149            if v.slot != slot || !top_ids.contains(&v.id) || v.id == *var {
4150                continue;
4151            }
4152            let t = v.ty.erased();
4153            let ok = match (&t, want) {
4154                (JavaType::Object(n), JavaType::Object(w)) => {
4155                    n.as_ref() == w.as_ref()
4156                        || pool.is_subtype(n.as_ref(), w.as_ref())
4157                }
4158                (JavaType::Array(_), JavaType::Array(_)) => want_arr,
4159                _ => false,
4160            };
4161            if ok {
4162                hits.push(v.id);
4163            }
4164        }
4165        hits.sort_unstable();
4166        hits.dedup();
4167        if hits.is_empty() {
4168            return;
4169        }
4170        // Multiple same-slot candidates: the LATEST generation (max id —
4171        // split mints in walk order ≈ program order) is the closest
4172        // approximation of the register's value at the use site. A
4173        // wrong pick still compiles (type-matched by construction);
4174        // leaving the primitive actual never does.
4175        let id = *hits.last().unwrap();
4176        *var = id;
4177        *ty = vt.vars[id as usize].ty.clone();
4178    };
4179    walk_stmt_exprs(body, &mut |e| {
4180        deep_rewrite(e, &mut |x| {
4181            match x {
4182                Expr::Method { desc, args, is_dynamic: false, .. } => {
4183                    for (i, a) in args.iter_mut().enumerate() {
4184                        if let Some(f) = desc.args.get(i) {
4185                            if matches!(f, JavaType::Object(n) if n.as_ref() != "java/lang/Object")
4186                                || matches!(f, JavaType::Array(_))
4187                            {
4188                                try_swap(a, f);
4189                            }
4190                        }
4191                    }
4192                }
4193                Expr::New { arg_tys, args, .. } => {
4194                    for (i, a) in args.iter_mut().enumerate() {
4195                        if let Some(f) = arg_tys.get(i) {
4196                            if matches!(f, JavaType::Object(n) if n.as_ref() != "java/lang/Object")
4197                                || matches!(f, JavaType::Array(_))
4198                            {
4199                                try_swap(a, f);
4200                            }
4201                        }
4202                    }
4203                }
4204                _ => {}
4205            }
4206        });
4207    });
4208    if ret_wants_ref {
4209        walk_mut_deep(body, &mut |st| {
4210            if let Stmt::Return(Some(e)) = st {
4211                try_swap(e, ret_ty);
4212            }
4213        });
4214    }
4215}
4216
4217/// Reference-array initializers take null, not 0: dex fill-array-data
4218/// over a String[]/Object[] slot carries the zero word, and the lifted
4219/// `new String[] {0}` is int无法转换为String (qs0/b family). A Const
4220/// Int(0) element of a reference-typed array init is the null ref.
4221/// `refVar = const 0` (dex `const/4 vN, 0` into a reference register) is
4222/// the NULL sentinel, not an int. By the time this runs infer_types has
4223/// typed the var a reference (its `.close()`/`.length`/method reads
4224/// proved it), so retype the const to Null BEFORE split_generations —
4225/// otherwise the ref/primitive mismatch splits off an int generation and
4226/// orphans the reference reads (u7.l try-with-resources finally
4227/// `v16_g3 = 0; if (v16_g3 != 0) v16_g3.close();` → "无法取消引用int").
4228/// `fix_incomparable_equality` already retypes the `refVar != 0`
4229/// COMPARISON side (but runs after the split); this covers the ASSIGN
4230/// side, which is what triggers the bad split.
4231pub fn fix_ref_null_assigns(vt: &VarTable, body: &mut Stmt) {
4232    let is_ref_var = |var: u32| -> bool {
4233        (var as usize) < vt.vars.len()
4234            && matches!(
4235                vt.vars[var as usize].ty.erased(),
4236                JavaType::Object(_) | JavaType::Array(_)
4237            )
4238    };
4239    walk_mut_deep(body, &mut |st| match st {
4240        Stmt::ExprStmt(Expr::Assign {
4241            target,
4242            value,
4243            op: AssignOp::Plain,
4244            ..
4245        }) => {
4246            if let Expr::Local { var, .. } = &**target {
4247                if is_ref_var(*var) && matches!(&**value, Expr::Const(ConstVal::Int(0))) {
4248                    **value = Expr::Const(ConstVal::Null);
4249                }
4250            }
4251        }
4252        Stmt::LocalDef {
4253            var,
4254            init: Some(e),
4255            ..
4256        } => {
4257            if is_ref_var(*var) && matches!(e, Expr::Const(ConstVal::Int(0))) {
4258                *e = Expr::Const(ConstVal::Null);
4259            }
4260        }
4261        _ => {}
4262    });
4263}
4264
4265pub fn fix_ref_array_null_consts(body: &mut Stmt) {
4266    walk_stmt_exprs(body, &mut |e| {
4267        deep_rewrite(e, &mut |x| {
4268            if let Expr::NewArray { elem, init: Some(list), .. } = x {
4269                if matches!(elem.erased(), JavaType::Object(_) | JavaType::Array(_)) {
4270                    for slot in list.iter_mut() {
4271                        if matches!(&*slot, Expr::Const(ConstVal::Int(0))) {
4272                            *slot = Expr::Const(ConstVal::Null);
4273                        }
4274                    }
4275                }
4276            }
4277        });
4278    });
4279}
4280
4281// ---------------------------------------------------------------------------
4282// Null-sentinel repair: const-0 registers in reference-typed slots
4283// ---------------------------------------------------------------------------
4284
4285/// A dex `const/4 vN, 0` register is ambiguous between int 0 and the
4286/// null reference; the value view types the bare constant as Int.
4287/// Kotlin coroutine state machines lean on this heavily — captured
4288/// object slots are zero-initialized ahead of the label switch, then
4289/// stored into `Object` fields and passed as `Continuation` ctor args
4290/// (weixin y01/s9 q8: `new e6(s94x, v287)` "int无法转换为Continuation";
4291/// uf5/l0 `throw 0;`; wcdb RepairKit `setOnCancelListener(0)`). The
4292/// dex verifier guarantees a const-0 register reaching a reference-
4293/// typed slot holds null, so in those slots the rendering must be the
4294/// null literal.
4295///
4296/// Two rewrites, both gated on the slot's expected type being a
4297/// reference (descriptor formal types, field type, array component,
4298/// cast target, throw, method return):
4299///   1. a literal `0` in a ref slot → `null`. SKIPPED for call-arg
4300///      slots whose formal is exactly `java/lang/Object`: `f(0)` there
4301///      compiles TODAY (boxing, or an int-overload pick like
4302///      `String.valueOf(0)`), and `f(null)` could newly fail on
4303///      overload ambiguity (`valueOf(Object)` vs `valueOf(char[])`) —
4304///      a guaranteed-error slot becomes a compile, a compiling slot
4305///      must not become an error.
4306///   2. a proven null-sentinel LOCAL: an Int-typed var whose every
4307///      write is `= 0` or `= <another sentinel>` and whose every read
4308///      sits in a ref slot (or feeds another sentinel's write — a dead
4309///      chain) → `null` at each read. The same Object-formal skip
4310///      applies at arg slots. A read in ANY int/unknown context
4311///      (binop, cond test, receiver deref, selector, anon-ctor arg,
4312///      lambda capture…) disqualifies the var — register reuse across
4313///      generations keeps its int rendering.
4314///
4315/// Runs after fix_ref_null_assigns (which converts writes of REF-typed
4316/// vars) and before split_generations (sentinel writes must not mint
4317/// int generations). Writes of confirmed sentinels stay as harmless
4318/// dead int stores for drop_dead_locals.
4319pub fn fix_null_sentinels(body: &mut Stmt, vt: &VarTable, ret: &JavaType) {
4320    #[derive(Clone, Copy, PartialEq)]
4321    enum WK {
4322        Zero,
4323        Copy(u32),
4324        Other,
4325    }
4326    fn is_ref_jt(t: &JavaType) -> bool {
4327        matches!(t, JavaType::Object(_) | JavaType::Array(_))
4328    }
4329    fn cls_write(e: &Expr) -> WK {
4330        match e {
4331            Expr::Const(ConstVal::Int(0)) => WK::Zero,
4332            Expr::Local { var, .. } => WK::Copy(*var),
4333            _ => WK::Other,
4334        }
4335    }
4336    fn expr_writes(e: &Expr, writes: &mut std::collections::HashMap<u32, Vec<WK>>) {
4337        visit_exprs(e, &mut |x| match x {
4338            Expr::Assign { target, op, value } => {
4339                if let Expr::Local { var, .. } = &**target {
4340                    let k = if matches!(op, AssignOp::Plain) {
4341                        cls_write(value)
4342                    } else {
4343                        WK::Other
4344                    };
4345                    writes.entry(*var).or_default().push(k);
4346                }
4347            }
4348            Expr::PreIncDec { e: inner, .. } | Expr::PostIncDec { e: inner, .. } => {
4349                if let Expr::Local { var, .. } = &**inner {
4350                    writes.entry(*var).or_default().push(WK::Other);
4351                }
4352            }
4353            _ => {}
4354        });
4355    }
4356    fn collect_writes(
4357        s: &Stmt,
4358        writes: &mut std::collections::HashMap<u32, Vec<WK>>,
4359        has_zero: &mut bool,
4360    ) {
4361        fn note(e: &Expr, has_zero: &mut bool, writes: &mut std::collections::HashMap<u32, Vec<WK>>) {
4362            visit_exprs(e, &mut |x| {
4363                if let Expr::Const(ConstVal::Int(0)) = x {
4364                    *has_zero = true;
4365                }
4366            });
4367            expr_writes(e, writes);
4368        }
4369        match s {
4370            Stmt::Block(v) => {
4371                for x in v {
4372                    collect_writes(x, writes, has_zero);
4373                }
4374            }
4375            Stmt::LocalDef { var, init, .. } => {
4376                if let Some(e) = init {
4377                    if matches!(e, Expr::Const(ConstVal::Int(0))) {
4378                        *has_zero = true;
4379                    }
4380                    writes.entry(*var).or_default().push(cls_write(e));
4381                }
4382            }
4383            Stmt::ExprStmt(e) => note(e, has_zero, writes),
4384            Stmt::Return(e) => {
4385                if let Some(x) = e {
4386                    note(x, has_zero, writes);
4387                }
4388            }
4389            Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
4390                note(e, has_zero, writes);
4391            }
4392            Stmt::TernaryValue { e } => note(e, has_zero, writes),
4393            Stmt::If { cond, then_stmt, else_stmt } => {
4394                note(cond, has_zero, writes);
4395                collect_writes(then_stmt, writes, has_zero);
4396                if let Some(b) = else_stmt {
4397                    collect_writes(b, writes, has_zero);
4398                }
4399            }
4400            Stmt::While { cond, body } | Stmt::DoWhile { body, cond } => {
4401                note(cond, has_zero, writes);
4402                collect_writes(body, writes, has_zero);
4403            }
4404            Stmt::For { init, cond, update, body } => {
4405                for x in init {
4406                    collect_writes(x, writes, has_zero);
4407                }
4408                if let Some(c) = cond {
4409                    note(c, has_zero, writes);
4410                }
4411                for u in update {
4412                    note(u, has_zero, writes);
4413                }
4414                collect_writes(body, writes, has_zero);
4415            }
4416            Stmt::ForEach { var, iterable, body, .. } => {
4417                writes.entry(*var).or_default().push(WK::Other);
4418                note(iterable, has_zero, writes);
4419                collect_writes(body, writes, has_zero);
4420            }
4421            Stmt::Switch { selector, cases, default, .. } => {
4422                note(selector, has_zero, writes);
4423                for c in cases {
4424                    if let Some(g) = &c.guard {
4425                        note(g, has_zero, writes);
4426                    }
4427                    for x in &c.body {
4428                        collect_writes(x, writes, has_zero);
4429                    }
4430                }
4431                if let Some(d) = default {
4432                    collect_writes(d, writes, has_zero);
4433                }
4434            }
4435            Stmt::Try { body, catches, finally } => {
4436                collect_writes(body, writes, has_zero);
4437                for c in catches {
4438                    writes.entry(c.var).or_default().push(WK::Other);
4439                    collect_writes(&c.body, writes, has_zero);
4440                }
4441                if let Some(f) = finally {
4442                    collect_writes(f, writes, has_zero);
4443                }
4444            }
4445            Stmt::TryWithResources { resources, body, catches, finally, .. } => {
4446                for r in resources {
4447                    collect_writes(r, writes, has_zero);
4448                }
4449                collect_writes(body, writes, has_zero);
4450                for c in catches {
4451                    writes.entry(c.var).or_default().push(WK::Other);
4452                    collect_writes(&c.body, writes, has_zero);
4453                }
4454                if let Some(f) = finally {
4455                    collect_writes(f, writes, has_zero);
4456                }
4457            }
4458            Stmt::Assert { cond, msg } => {
4459                note(cond, has_zero, writes);
4460                if let Some(m) = msg {
4461                    note(m, has_zero, writes);
4462                }
4463            }
4464            Stmt::Synchronized { lock, body } => {
4465                note(lock, has_zero, writes);
4466                collect_writes(body, writes, has_zero);
4467            }
4468            Stmt::Labeled { body, .. } => collect_writes(body, writes, has_zero),
4469            _ => {}
4470        }
4471    }
4472
4473    let mut writes: std::collections::HashMap<u32, Vec<WK>> =
4474        std::collections::HashMap::new();
4475    let mut has_zero = false;
4476    collect_writes(body, &mut writes, &mut has_zero);
4477
4478    // Write-proven eligibility fixpoint (copy chains: `v = u`).
4479    let mut eligible: HashSet<u32> = HashSet::default();
4480    let int_typed = |v: u32| {
4481        (v as usize) < vt.vars.len() && vt.vars[v as usize].ty.erased() == JavaType::Int
4482    };
4483    loop {
4484        let mut changed = false;
4485        for (&v, ws) in writes.iter() {
4486            if eligible.contains(&v) || ws.is_empty() || !int_typed(v) {
4487                continue;
4488            }
4489            if ws.iter().all(|k| match k {
4490                WK::Zero => true,
4491                WK::Copy(u) => *u != v && eligible.contains(u),
4492                WK::Other => false,
4493            }) {
4494                eligible.insert(v);
4495                changed = true;
4496            }
4497        }
4498        if !changed {
4499            break;
4500        }
4501    }
4502    if eligible.is_empty() && !has_zero {
4503        return; // nothing this pass can do
4504    }
4505
4506    #[derive(Clone, Copy, PartialEq)]
4507    enum Mode {
4508        Scan,
4509        Rewrite,
4510    }
4511    struct St<'a> {
4512        mode: Mode,
4513        vt: &'a VarTable,
4514        eligible: &'a HashSet<u32>,
4515        ref_read: HashSet<u32>,
4516        dead: HashSet<u32>,
4517        sentinels: HashSet<u32>,
4518        n_lit: usize,
4519        n_var: usize,
4520    }
4521    // A call-arg slot whose formal is exactly java/lang/Object keeps
4522    // its current rendering (see the fn doc — boxing compiles today,
4523    // null could newly ambiguize).
4524    fn arg_ctx(t: &JavaType) -> Option<JavaType> {
4525        match t {
4526            JavaType::Object(n) if n.as_ref() == "java/lang/Object" => None,
4527            t if is_ref_jt(t) => Some(t.clone()),
4528            _ => None,
4529        }
4530    }
4531    fn walk_expr(e: &mut Expr, ctx: Option<JavaType>, st: &mut St) {
4532        walk_expr_c(e, ctx, false, st);
4533    }
4534    /// `cast_arg`: the slot is a call/ctor ARGUMENT — a rewritten null
4535    /// gets an explicit formal-type cast. Bare `f(null)` re-triggers
4536    /// overload ambiguity where `f(0)` used to pick an int overload
4537    /// (`in(null)` "引用不明确", `ContentValues.put(String,null)` vs its
4538    /// 9 overloads); `(String) null` is unambiguous and jadx-shaped.
4539    fn walk_expr_c(e: &mut Expr, ctx: Option<JavaType>, cast_arg: bool, st: &mut St) {
4540        let slot_ref = ctx.as_ref().is_some_and(is_ref_jt);
4541        fn mk_null(ctx: &Option<JavaType>, cast_arg: bool) -> Expr {
4542            match (cast_arg, ctx) {
4543                (true, Some(t)) => Expr::Cast {
4544                    ty: TypeRef::J(t.clone()),
4545                    e: Box::new(Expr::Const(ConstVal::Null)),
4546                },
4547                _ => Expr::Const(ConstVal::Null),
4548            }
4549        }
4550        match e {
4551            Expr::Const(ConstVal::Int(0)) if slot_ref => {
4552                if st.mode == Mode::Rewrite {
4553                    *e = mk_null(&ctx, cast_arg);
4554                    st.n_lit += 1;
4555                }
4556                return;
4557            }
4558            Expr::Local { var, .. } => {
4559                if st.eligible.contains(var) {
4560                    if slot_ref {
4561                        st.ref_read.insert(*var);
4562                    } else {
4563                        st.dead.insert(*var);
4564                    }
4565                }
4566                if st.mode == Mode::Rewrite && slot_ref && st.sentinels.contains(var) {
4567                    *e = mk_null(&ctx, cast_arg);
4568                    st.n_var += 1;
4569                }
4570                return;
4571            }
4572            _ => {}
4573        }
4574        match e {
4575            Expr::Method { owner, desc, args, .. } => {
4576                if let Some(o) = owner {
4577                    walk_expr(o, None, st);
4578                }
4579                for (i, a) in args.iter_mut().enumerate() {
4580                    let c = desc.args.get(i).and_then(arg_ctx);
4581                    walk_expr_c(a, c, true, st);
4582                }
4583            }
4584            Expr::Invokedynamic { desc, args, .. } => {
4585                for (i, a) in args.iter_mut().enumerate() {
4586                    let c = desc.args.get(i).and_then(arg_ctx);
4587                    walk_expr_c(a, c, true, st);
4588                }
4589            }
4590            Expr::New { args, arg_tys, .. } => {
4591                for (i, a) in args.iter_mut().enumerate() {
4592                    let c = arg_tys.get(i).and_then(arg_ctx);
4593                    walk_expr_c(a, c, true, st);
4594                }
4595            }
4596            Expr::AnonNew { args, .. } => {
4597                for a in args.iter_mut() {
4598                    walk_expr(a, None, st);
4599                }
4600            }
4601            Expr::Assign { target, value, .. } => {
4602                // Sentinel-to-sentinel copy: the whole store is dead;
4603                // its RHS is by the write gate a const-0 or a bare
4604                // local, so no nested reads can hide in it.
4605                if let Expr::Local { var: u, .. } = &**target {
4606                    if st.eligible.contains(u) {
4607                        return;
4608                    }
4609                }
4610                let vctx = match &**target {
4611                    Expr::Field { ty, .. } => {
4612                        let t = ty.erased();
4613                        is_ref_jt(&t).then_some(t)
4614                    }
4615                    Expr::ArrayIndex { array, .. } => match array.type_ref().erased() {
4616                        JavaType::Array(inner) if is_ref_jt(&inner) => Some(*inner),
4617                        _ => None,
4618                    },
4619                    _ => None,
4620                };
4621                walk_expr(target, None, st);
4622                walk_expr(value, vctx, st);
4623            }
4624            Expr::Cast { ty, e: inner } => {
4625                let t = ty.erased();
4626                let c = is_ref_jt(&t).then_some(t);
4627                walk_expr(inner, c, st);
4628            }
4629            Expr::InstanceOf { e: inner, .. } => {
4630                walk_expr(inner, Some(JavaType::Object("java/lang/Object".into())), st);
4631            }
4632            Expr::Cond { c, t, f } => {
4633                walk_expr(c, None, st);
4634                let tc = ctx.clone();
4635                walk_expr(t, tc, st);
4636                walk_expr(f, ctx, st);
4637            }
4638            Expr::NewArray { elem, dims, init, .. } => {
4639                for d in dims.iter_mut() {
4640                    walk_expr(d, None, st);
4641                }
4642                if let Some(list) = init {
4643                    let et = elem.erased();
4644                    let c = is_ref_jt(&et).then_some(et);
4645                    for slot in list.iter_mut() {
4646                        walk_expr(slot, c.clone(), st);
4647                    }
4648                }
4649            }
4650            Expr::NewMultiArray { dims, .. } => {
4651                for d in dims.iter_mut() {
4652                    walk_expr(d, None, st);
4653                }
4654            }
4655            Expr::ArrayIndex { array, index } => {
4656                walk_expr(array, None, st);
4657                walk_expr(index, None, st);
4658            }
4659            Expr::Field { owner, .. } => {
4660                if let Some(o) = owner {
4661                    walk_expr(o, None, st);
4662                }
4663            }
4664            Expr::Bin { l, r, .. } => {
4665                walk_expr(l, None, st);
4666                walk_expr(r, None, st);
4667            }
4668            Expr::Un { e: inner, .. } => walk_expr(inner, None, st),
4669            Expr::PreIncDec { e: inner, .. } | Expr::PostIncDec { e: inner, .. } => {
4670                walk_expr(inner, None, st);
4671            }
4672            Expr::StringConcat(parts) => {
4673                for p in parts.iter_mut() {
4674                    if let ConcatPart::Str(x) = p {
4675                        walk_expr(x, None, st);
4676                    }
4677                }
4678            }
4679            Expr::Lambda(l) => {
4680                for c in l.captures.iter_mut() {
4681                    walk_expr(c, None, st);
4682                }
4683            }
4684            _ => {}
4685        }
4686    }
4687    fn walk_stmt(s: &mut Stmt, st: &mut St, ret: &JavaType) {
4688        match s {
4689            Stmt::Block(v) => {
4690                for x in v.iter_mut() {
4691                    walk_stmt(x, st, ret);
4692                }
4693            }
4694            Stmt::ExprStmt(e) => walk_expr(e, None, st),
4695            Stmt::LocalDef { var, init, .. } => {
4696                if let Some(e) = init {
4697                    let ctx = if (*var as usize) < st.vt.vars.len() {
4698                        let t = st.vt.vars[*var as usize].ty.erased();
4699                        is_ref_jt(&t).then_some(t)
4700                    } else {
4701                        None
4702                    };
4703                    walk_expr(e, ctx, st);
4704                }
4705            }
4706            Stmt::Return(e) => {
4707                if let Some(x) = e {
4708                    let c = is_ref_jt(ret).then(|| ret.clone());
4709                    walk_expr(x, c, st);
4710                }
4711            }
4712            Stmt::Throw(e) => {
4713                walk_expr(e, Some(JavaType::Object("java/lang/Throwable".into())), st);
4714            }
4715            Stmt::If { cond, then_stmt, else_stmt } => {
4716                walk_expr(cond, None, st);
4717                walk_stmt(then_stmt, st, ret);
4718                if let Some(b) = else_stmt {
4719                    walk_stmt(b, st, ret);
4720                }
4721            }
4722            Stmt::While { cond, body } => {
4723                walk_expr(cond, None, st);
4724                walk_stmt(body, st, ret);
4725            }
4726            Stmt::DoWhile { body, cond } => {
4727                walk_stmt(body, st, ret);
4728                walk_expr(cond, None, st);
4729            }
4730            Stmt::For { init, cond, update, body } => {
4731                for x in init.iter_mut() {
4732                    walk_stmt(x, st, ret);
4733                }
4734                if let Some(c) = cond {
4735                    walk_expr(c, None, st);
4736                }
4737                for u in update.iter_mut() {
4738                    walk_expr(u, None, st);
4739                }
4740                walk_stmt(body, st, ret);
4741            }
4742            Stmt::ForEach { iterable, body, .. } => {
4743                walk_expr(iterable, None, st);
4744                walk_stmt(body, st, ret);
4745            }
4746            Stmt::Switch { selector, cases, default, .. } => {
4747                walk_expr(selector, None, st);
4748                for c in cases.iter_mut() {
4749                    if let Some(g) = &mut c.guard {
4750                        walk_expr(g, None, st);
4751                    }
4752                    for x in c.body.iter_mut() {
4753                        walk_stmt(x, st, ret);
4754                    }
4755                }
4756                if let Some(d) = default {
4757                    walk_stmt(d, st, ret);
4758                }
4759            }
4760            Stmt::Try { body, catches, finally } => {
4761                walk_stmt(body, st, ret);
4762                for c in catches.iter_mut() {
4763                    walk_stmt(&mut c.body, st, ret);
4764                }
4765                if let Some(f) = finally {
4766                    walk_stmt(f, st, ret);
4767                }
4768            }
4769            Stmt::TryWithResources { resources, body, catches, finally, .. } => {
4770                for r in resources.iter_mut() {
4771                    walk_stmt(r, st, ret);
4772                }
4773                walk_stmt(body, st, ret);
4774                for c in catches.iter_mut() {
4775                    walk_stmt(&mut c.body, st, ret);
4776                }
4777                if let Some(f) = finally {
4778                    walk_stmt(f, st, ret);
4779                }
4780            }
4781            Stmt::Assert { cond, msg } => {
4782                walk_expr(cond, None, st);
4783                if let Some(m) = msg {
4784                    walk_expr(m, None, st);
4785                }
4786            }
4787            Stmt::Synchronized { lock, body } => {
4788                walk_expr(lock, Some(JavaType::Object("java/lang/Object".into())), st);
4789                walk_stmt(body, st, ret);
4790            }
4791            Stmt::TernaryValue { e } => walk_expr(e, None, st),
4792            Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
4793                walk_expr(e, Some(JavaType::Object("java/lang/Object".into())), st);
4794            }
4795            Stmt::Labeled { body, .. } => walk_stmt(body, st, ret),
4796            _ => {}
4797        }
4798    }
4799
4800    let mut st = St {
4801        mode: Mode::Scan,
4802        vt,
4803        eligible: &eligible,
4804        ref_read: HashSet::default(),
4805        dead: HashSet::default(),
4806        sentinels: HashSet::default(),
4807        n_lit: 0,
4808        n_var: 0,
4809    };
4810    if !eligible.is_empty() {
4811        walk_stmt(body, &mut st, ret);
4812        st.sentinels = eligible
4813            .iter()
4814            .copied()
4815            .filter(|v| st.ref_read.contains(v) && !st.dead.contains(v))
4816            .collect();
4817    }
4818    if st.sentinels.is_empty() && !has_zero {
4819        return;
4820    }
4821    st.mode = Mode::Rewrite;
4822    walk_stmt(body, &mut st, ret);
4823    if std::env::var_os("DDC_SENT").is_some() {
4824        eprintln!(
4825            "[sent] elig={} confirmed={} lit={} var={}",
4826            eligible.len(),
4827            st.sentinels.len(),
4828            st.n_lit,
4829            st.n_var
4830        );
4831    }
4832}
4833
4834/// Int-context operand bridge: a BOOLEAN-typed side of an int-kind Bin
4835/// (bitwise, arithmetic, comparison against a numeric) becomes
4836/// `(b ? 1 : 0)` — the dex-level truth (booleans ARE 0/1 ints there;
4837/// register reuse puts a bool-typed var into an int expression:
4838/// `v5x | 4`, `b == 0` — "boolean无法转换为int" / "二元运算符操作数
4839/// 类型错误" families). Runs AFTER fix_bool_xor so boolean-SINK chains
4840/// were already converted to all-boolean form and are skipped here.
4841pub fn fix_int_operand_bridges(body: &mut Stmt, vt: &VarTable) {
4842    fn side_bool(e: &Expr, vt: &VarTable) -> bool {
4843        matches!(
4844            match e {
4845                Expr::Local { var, .. } => vt.var(*var).ty.erased(),
4846                other => other.type_ref().erased(),
4847            },
4848            JavaType::Boolean
4849        )
4850    }
4851    fn side_int(e: &Expr, vt: &VarTable) -> bool {
4852        matches!(
4853            match e {
4854                Expr::Local { var, .. } => vt.var(*var).ty.erased(),
4855                other => other.type_ref().erased(),
4856            },
4857            JavaType::Int | JavaType::Short | JavaType::Byte | JavaType::Char | JavaType::Long
4858        )
4859    }
4860    fn wrap(e: Box<Expr>) -> Box<Expr> {
4861        Box::new(Expr::Cond {
4862            c: e,
4863            t: Box::new(Expr::Const(ConstVal::Int(1))),
4864            f: Box::new(Expr::Const(ConstVal::Int(0))),
4865        })
4866    }
4867    walk_stmt_exprs(body, &mut |e| {
4868        deep_rewrite(e, &mut |x| {
4869            // Field / array-store positions: the target's type is
4870            // descriptor-backed (authoritative); a boolean value into
4871            // an int slot bridges `(b ? 1 : 0)` (with a narrowing cast
4872            // for byte/short/char slots), an int value into a boolean
4873            // slot bridges `v != 0` (weixin `this.L[v3x] = max3`,
4874            // `g2x.d = p1x` — 785 field + ~700 array lines).
4875            if let Expr::Assign { target, value, op: AssignOp::Plain } = x {
4876                let tgt_ty: Option<JavaType> = match &**target {
4877                    Expr::Field { ty, .. } => Some(ty.erased()),
4878                    Expr::ArrayIndex { array, .. } => {
4879                        match array.type_ref().erased() {
4880                            JavaType::Array(el) => Some(el.as_ref().clone()),
4881                            _ => None,
4882                        }
4883                    }
4884                    // Local targets: the split/booleanize machinery owns
4885                    // the mixed-kind register cases; the residue that
4886                    // reached render (boolean无法转换为byte/long, ~360
4887                    // lines) gets the same value-side bridge.
4888                    Expr::Local { var, .. } => {
4889                        if (*var as usize) < vt.vars.len() {
4890                            Some(vt.var(*var).ty.erased())
4891                        } else {
4892                            None
4893                        }
4894                    }
4895                    _ => None,
4896                };
4897                if let Some(t) = tgt_ty {
4898                    let tgt_int = matches!(
4899                        t,
4900                        JavaType::Int
4901                            | JavaType::Short
4902                            | JavaType::Byte
4903                            | JavaType::Char
4904                            | JavaType::Long
4905                    );
4906                    if tgt_int && side_bool(value, vt) && !side_int(value, vt) {
4907                        let taken = std::mem::replace(
4908                            value,
4909                            Box::new(Expr::Const(ConstVal::Null)),
4910                        );
4911                        let mut wrapped = wrap(taken);
4912                        if !matches!(t, JavaType::Int | JavaType::Long) {
4913                            wrapped = Box::new(Expr::Cast {
4914                                ty: TypeRef::J(t.clone()),
4915                                e: wrapped,
4916                            });
4917                        }
4918                        *value = wrapped;
4919                    } else if matches!(t, JavaType::Boolean)
4920                        && side_int(value, vt)
4921                        && !side_bool(value, vt)
4922                    {
4923                        let taken = std::mem::replace(
4924                            value,
4925                            Box::new(Expr::Const(ConstVal::Null)),
4926                        );
4927                        **value = Expr::Bin {
4928                            op: BinOp::Ne,
4929                            l: taken,
4930                            r: Box::new(Expr::Const(ConstVal::Int(0))),
4931                            ty: Some(TypeRef::J(JavaType::Boolean)),
4932                        };
4933                    }
4934                }
4935            }
4936            // Explicit casts across the bool/num line are invalid Java
4937            // in BOTH directions: `(byte) this.r` (dex int-to-byte over
4938            // a boolean-rendered field — weixin AccInfo writeByte ×207)
4939            // becomes `(byte)(r ? 1 : 0)`, and `(boolean) x` over an
4940            // int becomes `x != 0`.
4941            if let Expr::Cast { ty, e } = x {
4942                let t = ty.erased();
4943                let t_num = matches!(
4944                    t,
4945                    JavaType::Int
4946                        | JavaType::Short
4947                        | JavaType::Byte
4948                        | JavaType::Char
4949                        | JavaType::Long
4950                        | JavaType::Float
4951                        | JavaType::Double
4952                );
4953                if t_num && side_bool(e, vt) && !side_int(e, vt) {
4954                    let taken = std::mem::replace(e, Box::new(Expr::Const(ConstVal::Null)));
4955                    **e = Expr::Cond {
4956                        c: taken,
4957                        t: Box::new(Expr::Const(ConstVal::Int(1))),
4958                        f: Box::new(Expr::Const(ConstVal::Int(0))),
4959                    };
4960                } else if matches!(t, JavaType::Boolean)
4961                    && side_int(e, vt)
4962                    && !side_bool(e, vt)
4963                {
4964                    let taken = std::mem::replace(e, Box::new(Expr::Const(ConstVal::Null)));
4965                    *x = Expr::Bin {
4966                        op: BinOp::Ne,
4967                        l: taken,
4968                        r: Box::new(Expr::Const(ConstVal::Int(0))),
4969                        ty: Some(TypeRef::J(JavaType::Boolean)),
4970                    };
4971                }
4972            }
4973            // Null-vs-int comparison: dex if-eqz/if-nez is type-agnostic
4974            // and null IS zero — when the local came out int-typed (phi
4975            // confluence residue), rewrite the Null constant to 0
4976            // (`v95_g12 != null` → `v95_g12 != 0`, faithful and legal;
4977            // yq0/g1 546-line 二元运算符 family).
4978            if let Expr::Bin { op, l, r, .. } = x {
4979                if matches!(op, BinOp::Eq | BinOp::Ne) {
4980                    if matches!(&**r, Expr::Const(ConstVal::Null))
4981                        && side_int(l, vt)
4982                        && !side_bool(l, vt)
4983                    {
4984                        **r = Expr::Const(ConstVal::Int(0));
4985                    } else if matches!(&**l, Expr::Const(ConstVal::Null))
4986                        && side_int(r, vt)
4987                        && !side_bool(r, vt)
4988                    {
4989                        **l = Expr::Const(ConstVal::Int(0));
4990                    }
4991                }
4992            }
4993            // A boolean-typed array INDEX is a reused register holding
4994            // an int (`this.L[v3x]` — boolean无法转换为int at the index
4995            // position): bridge it in reads and writes alike.
4996            if let Expr::ArrayIndex { index, .. } = x {
4997                if side_bool(index, vt) && !side_int(index, vt) {
4998                    let taken =
4999                        std::mem::replace(index, Box::new(Expr::Const(ConstVal::Null)));
5000                    *index = wrap(taken);
5001                }
5002            }
5003            // A boolean-typed array DIMENSION is the same reused-register
5004            // case at the allocation site (`new Object[v200]` where v200
5005            // is boolean-typed — weixin AppBrandRuntime, bool→int family):
5006            // dex new-array reads the size register as int and booleans
5007            // ARE 0/1 ints there, so bridge `(b ? 1 : 0)`.
5008            if let Expr::NewArray { dims, .. } = x {
5009                for d in dims.iter_mut() {
5010                    if side_bool(d, vt) && !side_int(d, vt) {
5011                        let taken =
5012                            std::mem::replace(d, Expr::Const(ConstVal::Null));
5013                        *d = *wrap(Box::new(taken));
5014                    }
5015                }
5016            }
5017            if let Expr::Bin { op, l, r, .. } = x {
5018                let int_kind = matches!(
5019                    op,
5020                    BinOp::Or
5021                        | BinOp::And
5022                        | BinOp::Xor
5023                        | BinOp::Add
5024                        | BinOp::Sub
5025                        | BinOp::Mul
5026                        | BinOp::Div
5027                        | BinOp::Rem
5028                        | BinOp::Shl
5029                        | BinOp::Shr
5030                        | BinOp::Ushr
5031                        | BinOp::Eq
5032                        | BinOp::Ne
5033                        | BinOp::Lt
5034                        | BinOp::Ge
5035                        | BinOp::Gt
5036                        | BinOp::Le
5037                );
5038                if !int_kind {
5039                    return;
5040                }
5041                // Comparisons only bridge against a NUMERIC constant or
5042                // int-typed side (a bool == bool compare is valid Java).
5043                if side_bool(l, vt) && side_int(r, vt) && !side_bool(r, vt) {
5044                    let taken = std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
5045                    *l = wrap(taken);
5046                } else if side_bool(r, vt) && side_int(l, vt) && !side_bool(l, vt) {
5047                    let taken = std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
5048                    *r = wrap(taken);
5049                }
5050            }
5051        });
5052    });
5053}
5054
5055/// Statement-level idiom restoration (readability parity with
5056/// jadx/DAD, flagged by the androguard comparison): `v = v + 1` →
5057/// `v++`, `v = v op x` → `v op= x`. Local targets only — a compound
5058/// field/array target would change how often the receiver expression
5059/// evaluates. Var must be the LEFT operand (non-commutative ops;
5060/// string-concat order). The ±1 fold requires a numeric vt type
5061/// (`s++` on a String is invalid while `s += 1` is not).
5062/// Boolean VALUE-diamond fold (DAD short_circuit value forms):
5063/// ```text
5064///   v = false; if (c) { v = true; }   →  v = c
5065///   v = true;  if (c) { v = false; }  →  v = !c
5066///   v = false; if (c) { v = E; }      →  v = c && E     (E bool-valued)
5067///   v = true;  if (c) { v = E; }      →  v = !c || E
5068///   v = D; if (c) { v = E1; } else { v = E2; }  →  v = c ? E1 : E2
5069/// ```
5070/// `v` must be Boolean-typed; the arm value bool-valued; the guard may
5071/// not touch `v`; base and arm constants must not coincide (a no-op
5072/// diamond could drop guard side effects). Kotlin default-arg bridge
5073/// ctors compute defaulted booleans in exactly this shape INSIDE the
5074/// branch ahead of the delegation (lark MmCreateAudioRequest's v21x
5075/// chain) — split_branch rejects the control flow as Dirty and the
5076/// this() call stays in the branch ("对this的调用必须是构造器中的第一
5077/// 个语句", lark ×456). Folded to a linear def (interleaved with
5078/// forward_single_use for the `[u = call; v = u]` arm shape) the
5079/// merge_at ternary fold applies. Source-faithful: the original was
5080/// `x?.m() ?: false` — one expression; the inlined call stays under
5081/// the guard arm, preserving evaluation order and short-circuiting.
5082/// General value-diamond fold (fold_bool_value_diamonds for ANY type):
5083/// `if (c) { v = a; } else { v = b; }` → `v = c ? a : b;`, plus the
5084/// split-pair shape `v = b; if (c) { v = a; }` → the same ternary. The
5085/// Kotlin default-arg ctor's mask-guarded per-arg defaults arrive as
5086/// diamonds (DocFreshInfo v10x/v14x); folding gives each carrier a
5087/// SINGLE-assignment def so the chain-aware delegation inline can hoist
5088/// the this() first ("对this的调用必须是构造器中的第一个语句"). Fold
5089/// only when both sides are exactly one plain assignment to the SAME
5090/// local and the value types are ternary-compatible (equal erased or
5091/// both numeric — mixed-category ternaries box/surprise).
5092pub fn fold_value_diamonds(body: &mut Stmt) {
5093    let Stmt::Block(stmts) = body else { return };
5094    fold_diamonds_in(stmts);
5095}
5096
5097fn diamond_types_ok(a: &Expr, b: &Expr) -> bool {
5098    fn num(t: &JavaType) -> bool {
5099        matches!(
5100            t,
5101            JavaType::Byte
5102                | JavaType::Short
5103                | JavaType::Int
5104                | JavaType::Char
5105                | JavaType::Long
5106                | JavaType::Float
5107                | JavaType::Double
5108        )
5109    }
5110    let ta = a.type_ref().erased();
5111    let tb = b.type_ref().erased();
5112    ta == tb || (num(&ta) && num(&tb))
5113}
5114
5115/// `(var, target clone, value)` when the stmt is exactly ONE plain
5116/// assign to a local (bare or block-wrapped). The unwrap must be
5117/// RECURSIVE: the Kotlin default-args ctor diamonds arrive as
5118/// `Block([Block([Assign])])` (structurer nesting), and the one-level
5119/// peel missed them — the unfolded `if ((mask&2)==0) v=p; else v=0;`
5120/// prelude then blocked fix_ctor_super_first from hoisting the
5121/// `this(...)` delegation (weibo/lark ctor-not-first ×841 family).
5122fn single_local_assign(st: &Stmt) -> Option<(u32, Expr, Expr)> {
5123    let mut inner = st;
5124    while let Stmt::Block(v) = inner {
5125        if v.len() != 1 {
5126            return None;
5127        }
5128        inner = &v[0];
5129    }
5130    match inner {
5131        Stmt::ExprStmt(Expr::Assign { target, value, op: AssignOp::Plain }) => {
5132            match &**target {
5133                Expr::Local { var, .. } => Some((*var, *target.clone(), *value.clone())),
5134                _ => None,
5135            }
5136        }
5137        _ => None,
5138    }
5139}
5140
5141fn mk_ternary_assign(tgt: Expr, c: Expr, a: Expr, b: Expr) -> Stmt {
5142    Stmt::ExprStmt(Expr::Assign {
5143        target: Box::new(tgt),
5144        op: AssignOp::Plain,
5145        value: Box::new(Expr::Cond {
5146            c: Box::new(c),
5147            t: Box::new(a),
5148            f: Box::new(b),
5149        }),
5150    })
5151}
5152
5153fn fold_diamonds_in(stmts: &mut Vec<Stmt>) {
5154    let mut i = 0usize;
5155    while i < stmts.len() {
5156        match &mut stmts[i] {
5157            Stmt::Block(v) => fold_diamonds_in(v),
5158            Stmt::If { then_stmt, else_stmt, .. } => {
5159                if let Stmt::Block(v) = &mut **then_stmt {
5160                    fold_diamonds_in(v);
5161                }
5162                if let Some(e) = else_stmt {
5163                    if let Stmt::Block(v) = &mut **e {
5164                        fold_diamonds_in(v);
5165                    }
5166                }
5167            }
5168            _ => {}
5169        }
5170        // Shape 1: standalone if/else diamond.
5171        let shape1 = if let Stmt::If { cond, then_stmt, else_stmt: Some(el), .. } = &stmts[i] {
5172            match (single_local_assign(then_stmt), single_local_assign(el)) {
5173                (Some((v1, tgt, a)), Some((v2, _, b)))
5174                    if v1 == v2 && diamond_types_ok(&a, &b) =>
5175                {
5176                    Some(mk_ternary_assign(tgt, cond.clone(), a, b))
5177                }
5178                _ => None,
5179            }
5180        } else {
5181            None
5182        };
5183        if let Some(f) = shape1 {
5184            stmts[i] = f;
5185            i += 1;
5186            continue;
5187        }
5188        // Shape 2: adjacent `v = b;` + `if (c) { v = a; }` (no else).
5189        if i + 1 < stmts.len() {
5190            let shape2 = if let Stmt::If { cond, then_stmt, else_stmt: None, .. } = &stmts[i + 1] {
5191                match (single_local_assign(&stmts[i]), single_local_assign(then_stmt)) {
5192                    (Some((v1, tgt, b)), Some((v2, _, a)))
5193                        if v1 == v2 && diamond_types_ok(&a, &b) =>
5194                    {
5195                        Some(mk_ternary_assign(tgt, cond.clone(), a, b))
5196                    }
5197                    _ => None,
5198                }
5199            } else {
5200                None
5201            };
5202            if let Some(f) = shape2 {
5203                stmts[i] = f;
5204                stmts.remove(i + 1);
5205                continue; // re-scan the folded position
5206            }
5207        }
5208        i += 1;
5209    }
5210}
5211
5212pub fn fold_bool_value_diamonds(body: &mut Stmt, vt: &VarTable) {
5213    fn is_bool_var(v: u32, vt: &VarTable) -> bool {
5214        (v as usize) < vt.vars.len()
5215            && matches!(vt.vars[v as usize].ty.erased(), JavaType::Boolean)
5216    }
5217    fn bool_const(e: &Expr) -> Option<bool> {
5218        fn cint(e: &Expr) -> Option<i32> {
5219            match e {
5220                Expr::Const(ConstVal::Int(n)) => Some(*n),
5221                _ => None,
5222            }
5223        }
5224        match e {
5225            Expr::Const(ConstVal::Int(0)) => Some(false),
5226            Expr::Const(ConstVal::Int(1)) => Some(true),
5227            // int↔bool bridge wrappings of constants: `0 != 0` is the
5228            // fallback pipeline's rendered `false` by the time the
5229            // ctor-block fold runs.
5230            Expr::Bin { op: op @ (BinOp::Eq | BinOp::Ne), l, r, .. } => {
5231                let (a, b) = (cint(l)?, cint(r)?);
5232                let eq = a == b;
5233                Some(if matches!(op, BinOp::Eq) { eq } else { !eq })
5234            }
5235            _ => None,
5236        }
5237    }
5238    fn reads_var(e: &Expr, v: u32) -> bool {
5239        let mut hit = false;
5240        visit_exprs(e, &mut |x| {
5241            if let Expr::Local { var, .. } = x {
5242                if *var == v {
5243                    hit = true;
5244                }
5245            }
5246        });
5247        hit
5248    }
5249    /// Bool-valued expression for the composite arms (&&, ||, ?:).
5250    /// Bare 0/1 constants are handled by the caller's special cases —
5251    /// inside a composite they would render as invalid `c && 1`.
5252    fn bool_valued(e: &Expr, vt: &VarTable) -> bool {
5253        fn side(e: &Expr, vt: &VarTable) -> bool {
5254            bool_const(e).is_some() || bool_valued(e, vt)
5255        }
5256        match e {
5257            Expr::Local { var, .. } => is_bool_var(*var, vt),
5258            Expr::Method { desc, .. } => desc.ret == JavaType::Boolean,
5259            Expr::InstanceOf { .. } => true,
5260            Expr::Un { op: UnOp::Not, e: x, .. } => bool_valued(x, vt) || bool_const(x).is_some(),
5261            Expr::Bin { op, l, r, .. } => match op {
5262                BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le => true,
5263                // A logical operand may itself be a 0/1 constant
5264                // (booleanize leaves mixed forms).
5265                BinOp::LogAnd | BinOp::LogOr => {
5266                    side(l, vt) && side(r, vt)
5267                }
5268                _ => false,
5269            },
5270            Expr::Cond { t, f, .. } => side(t, vt) && side(f, vt),
5271            _ => false,
5272        }
5273    }
5274    /// A branch that is exactly one plain `v = E` with E bool-valued
5275    /// (or a 0/1 constant) and not reading v — OR the forwarding PAIR
5276    /// `[u = E; v = u]` where u is single-assigned/single-read in the
5277    /// whole method: the fold consumes the pair into `v = E` (the impure
5278    /// `u = src.m()` arm shape forward_single_use cannot inline — its
5279    /// phi-ref gate rejects values reading multi-assigned prelude vars,
5280    /// and inlining here is scope-safe because E moves exactly one
5281    /// block level out with the arm's lone def consumed).
5282    fn lone_assign(
5283        b: &Stmt,
5284        v: u32,
5285        vt: &VarTable,
5286        an: &VarAnalysis,
5287        consumed: &mut HashSet<u32>,
5288    ) -> Option<Expr> {
5289        let l = flat_list(b);
5290        if l.len() == 2 {
5291            let (u, e): (u32, &Expr) = match l[0] {
5292                Stmt::LocalDef { var, init: Some(e), .. } => (*var, e),
5293                Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => {
5294                    match &**target {
5295                        Expr::Local { var, .. } => (*var, &**value),
5296                        _ => return None,
5297                    }
5298                }
5299                _ => return None,
5300            };
5301            if u == v || consumed.contains(&u) {
5302                return None;
5303            }
5304            // Whole-method single-assign/single-read: the fold drops
5305            // u's def with the arm, so ANY other read would dangle.
5306            // (Analysis counts are per-round; folds only ever REMOVE
5307            // reads elsewhere or clone E's — whose variable reads were
5308            // already counted at their def sites — so stale counts
5309            // over-approximate and the ==1 gates stay sound.)
5310            if an.assigns.get(u as usize).copied().unwrap_or(0) != 1
5311                || an.reads.get(u as usize).copied().unwrap_or(0) != 1
5312            {
5313                return None;
5314            }
5315            let (tv, rv) = match l[1] {
5316                Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => {
5317                    match (&**target, &**value) {
5318                        (Expr::Local { var: t, .. }, Expr::Local { var: r, .. }) => (*t, *r),
5319                        _ => return None,
5320                    }
5321                }
5322                Stmt::LocalDef { var, init: Some(Expr::Local { var: r, .. }), .. } => (*var, *r),
5323                _ => return None,
5324            };
5325            if tv != v || rv != u {
5326                return None;
5327            }
5328            if reads_var(e, v) || !(bool_valued(e, vt) || bool_const(e).is_some()) {
5329                return None;
5330            }
5331            consumed.insert(u);
5332            return Some(e.clone());
5333        }
5334        if l.len() != 1 {
5335            return None;
5336        }
5337        let (target, value) = match l[0] {
5338            Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => (target, value),
5339            Stmt::LocalDef { var, init: Some(value), .. } if *var == v => {
5340                if reads_var(value, v) || !(bool_valued(value, vt) || bool_const(value).is_some()) {
5341                    return None;
5342                }
5343                return Some(value.clone());
5344            }
5345            _ => return None,
5346        };
5347        let Expr::Local { var: tv, .. } = &**target else { return None };
5348        if *tv != v || reads_var(value, v) {
5349            return None;
5350        }
5351        if !(bool_valued(value, vt) || bool_const(value).is_some()) {
5352            return None;
5353        }
5354        Some((**value).clone())
5355    }
5356    fn not(e: Expr) -> Expr {
5357        Expr::Un { op: UnOp::Not, e: Box::new(e) }
5358    }
5359    /// Peephole the int↔bool bridge artifacts the fallback pipeline
5360    /// leaves in guards: `!(x != y)` → `x == y`; `(b ? 1 : 0) == 1` →
5361    /// `b`; `!(b ? 1 : 0 == 1)` chains likewise. Purely a readability
5362    /// pass — the unsimplified forms compile.
5363    fn simplify_bool(e: Expr) -> Expr {
5364        match e {
5365            Expr::Un { op: UnOp::Not, e: inner, .. } => match *inner {
5366                // Flip first, THEN re-simplify: `!((u?1:0) != 1)` must
5367                // reach the cond01 arm as `(u?1:0) == 1` → `u` (the
5368                // flip result never re-entered simplification and the
5369                // pair arm saw a Bin where it needed the bare local).
5370                Expr::Bin { op: BinOp::Ne, l, r, ty } => {
5371                    simplify_bool(Expr::Bin { op: BinOp::Eq, l, r, ty })
5372                }
5373                Expr::Bin { op: BinOp::Eq, l, r, ty } => {
5374                    simplify_bool(Expr::Bin { op: BinOp::Ne, l, r, ty })
5375                }
5376                other => Expr::Un { op: UnOp::Not, e: Box::new(simplify_bool(other)) },
5377            },
5378            Expr::Bin { op: op @ (BinOp::Eq | BinOp::Ne), l, r, ty } => {
5379                // (c ? 1 : 0) <op> 1  →  c / !c ; <op> 0 → !c / c
5380                fn cond01(e: &Expr) -> Option<Expr> {
5381                    if let Expr::Cond { c, t, f } = e {
5382                        if matches!(&**t, Expr::Const(ConstVal::Int(1)))
5383                            && matches!(&**f, Expr::Const(ConstVal::Int(0)))
5384                        {
5385                            return Some((**c).clone());
5386                        }
5387                    }
5388                    None
5389                }
5390                let (lc, rc) = (cond01(&l), cond01(&r));
5391                match (lc, rc) {
5392                    (Some(c), None) => match (&*r, op) {
5393                        (Expr::Const(ConstVal::Int(1)), BinOp::Eq) => c,
5394                        (Expr::Const(ConstVal::Int(1)), BinOp::Ne) => not(c),
5395                        (Expr::Const(ConstVal::Int(0)), BinOp::Eq) => not(c),
5396                        (Expr::Const(ConstVal::Int(0)), BinOp::Ne) => c,
5397                        _ => Expr::Bin { op, l: Box::new(c), r, ty },
5398                    },
5399                    (None, Some(c)) => match (&*l, op) {
5400                        (Expr::Const(ConstVal::Int(1)), BinOp::Eq) => c,
5401                        (Expr::Const(ConstVal::Int(1)), BinOp::Ne) => not(c),
5402                        (Expr::Const(ConstVal::Int(0)), BinOp::Eq) => not(c),
5403                        (Expr::Const(ConstVal::Int(0)), BinOp::Ne) => c,
5404                        _ => Expr::Bin { op, l, r: Box::new(c), ty },
5405                    },
5406                    _ => Expr::Bin { op, l, r, ty },
5407                }
5408            }
5409            other => other,
5410        }
5411    }
5412    fn bin(op: BinOp, l: Expr, r: Expr) -> Expr {
5413        Expr::Bin { op, l: Box::new(l), r: Box::new(r), ty: None }
5414    }
5415
5416    for _round in 0..4 {
5417        let mut changed = false;
5418        let mut analysis = VarAnalysis {
5419            assigns: Vec::new(),
5420            reads: Vec::new(),
5421            values: Vec::new(),
5422        };
5423        analyze_vars(body, &mut analysis);
5424        let mut consumed: HashSet<u32> = HashSet::default();
5425        walk_mut_deep(body, &mut |st| {
5426            let Stmt::Block(list) = st else { return };
5427            let mut i = 0usize;
5428            while i + 1 < list.len() {
5429                let folded = try_at(list, i, vt, &is_bool_var, &bool_const, &reads_var, &lone_assign, &analysis, &mut consumed);
5430                match folded {
5431                    Some(expr) => {
5432                        let is_def = matches!(&list[i], Stmt::LocalDef { .. });
5433                        let var = match &list[i] {
5434                            Stmt::LocalDef { var, .. } => *var,
5435                            Stmt::ExprStmt(Expr::Assign { target, .. }) => {
5436                                match &**target { Expr::Local { var, .. } => *var, _ => unreachable!() }
5437                            }
5438                            _ => unreachable!(),
5439                        };
5440                        let (is_final, force_type) = match &list[i] {
5441                            Stmt::LocalDef { is_final, force_type, .. } => (*is_final, *force_type),
5442                            _ => (false, false),
5443                        };
5444                        list[i] = if is_def {
5445                            Stmt::LocalDef { var, init: Some(expr), is_final, force_type }
5446                        } else {
5447                            Stmt::ExprStmt(Expr::Assign {
5448                                target: Box::new(Expr::Local {
5449                                    var,
5450                                    ty: TypeRef::J(JavaType::Boolean),
5451                                }),
5452                                op: AssignOp::Plain,
5453                                value: Box::new(expr),
5454                            })
5455                        };
5456                        list.remove(i + 1);
5457                        changed = true;
5458                        // stay at i: a preceding base may now pair with
5459                        // nothing new, but a FOLLOWING diamond may chain.
5460                    }
5461                    None => i += 1,
5462                }
5463            }
5464        });
5465        if !changed {
5466            break;
5467        }
5468    }
5469
5470    #[allow(clippy::too_many_arguments, clippy::type_complexity)]
5471    fn try_at(
5472        list: &[Stmt],
5473        i: usize,
5474        vt: &VarTable,
5475        is_bool_var: &dyn Fn(u32, &VarTable) -> bool,
5476        bool_const: &dyn Fn(&Expr) -> Option<bool>,
5477        reads_var: &dyn Fn(&Expr, u32) -> bool,
5478        lone_assign: &dyn Fn(&Stmt, u32, &VarTable, &VarAnalysis, &mut HashSet<u32>) -> Option<Expr>,
5479        an: &VarAnalysis,
5480        consumed: &mut HashSet<u32>,
5481    ) -> Option<Expr> {
5482        // s1: `v = <0|1>` (Assign or LocalDef), v Boolean-typed.
5483        let (v, base) = match &list[i] {
5484            Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => {
5485                match &**target {
5486                    Expr::Local { var, .. } => {
5487                        // The value may be a wrapped constant (`0 != 0`
5488                        // — the fallback pipeline's rendered false).
5489                        let b = bool_const(value)?;
5490                        if !is_bool_var(*var, vt) { return None; }
5491                        (*var, b)
5492                    }
5493                    _ => return None,
5494                }
5495            }
5496            Stmt::LocalDef { var, init: Some(e), .. } => {
5497                let b = bool_const(e)?;
5498                if !is_bool_var(*var, vt) { return None; }
5499                (*var, b)
5500            }
5501            _ => return None,
5502        };
5503        let Stmt::If { cond, then_stmt, else_stmt } = &list[i + 1] else {
5504            return None;
5505        };
5506        if reads_var(cond, v) {
5507            return None;
5508        }
5509        let mut touches_v = false;
5510        visit_exprs(cond, &mut |x| {
5511            if let Expr::Assign { target, .. } = x {
5512                if let Expr::Local { var, .. } = &**target {
5513                    if *var == v { touches_v = true; }
5514                }
5515            }
5516        });
5517        if touches_v {
5518            return None;
5519        }
5520        // The structurer emits inverted diamonds (`if (c) {} else { v = E }`,
5521        // empty-then) — normalize by negating the guard.
5522        let (cond_n, then_arm, else_arm): (Expr, &Stmt, Option<&Stmt>) =
5523            if flat_list(then_stmt).is_empty() && else_stmt.is_some() {
5524                (not(cond.clone()), else_stmt.as_ref().unwrap(), None)
5525            } else {
5526                (cond.clone(), then_stmt.as_ref(), else_stmt.as_deref())
5527            };
5528        let e1 = lone_assign(then_arm, v, vt, an, consumed)?;
5529        let e2 = match else_arm {
5530            None => None,
5531            Some(b) => Some(lone_assign(b, v, vt, an, consumed)?),
5532        };
5533        let e1c = bool_const(&e1);
5534        let c = simplify_bool(cond_n);
5535        Some(match e2 {
5536            None => {
5537                if !base {
5538                    match e1c {
5539                        Some(true) => c,
5540                        Some(false) => return None, // no-op diamond: guard effects would drop
5541                        None => bin(BinOp::LogAnd, c, e1),
5542                    }
5543                } else {
5544                    match e1c {
5545                        Some(false) => not(c),
5546                        Some(true) => return None,
5547                        None => bin(BinOp::LogOr, not(c), e1),
5548                    }
5549                }
5550            }
5551            Some(e2) => {
5552                // Full diamond: both arms assign — the base is dead.
5553                if e1 == e2 {
5554                    return None; // both arms equal: folding drops guard effects
5555                }
5556                Expr::Cond { c: Box::new(c), t: Box::new(e1), f: Box::new(e2) }
5557            }
5558        })
5559    }
5560}
5561
5562pub fn idiom_compounds(body: &mut Stmt, vt: &VarTable) {
5563    walk_mut_deep(body, &mut |st| {
5564        let Stmt::ExprStmt(e) = st else { return };
5565        let Expr::Assign { target, op: AssignOp::Plain, value } = &mut *e else {
5566            return;
5567        };
5568        // Target: a local, or a bare field (static / this — no complex
5569        // receiver whose re-evaluation a compound would elide).
5570        enum Tgt {
5571            Loc(u32),
5572            Fld,
5573        }
5574        let tgt = match &**target {
5575            Expr::Local { var, .. } => Tgt::Loc(*var),
5576            Expr::Field { owner, .. }
5577                if owner.is_none()
5578                    || matches!(&**owner.as_ref().unwrap(), Expr::This) =>
5579            {
5580                Tgt::Fld
5581            }
5582            _ => return,
5583        };
5584        let Expr::Bin { op, l, r, .. } = &mut **value else {
5585            return;
5586        };
5587        // The left operand must be the SAME storage as the target.
5588        match (&tgt, &**l) {
5589            (Tgt::Loc(v), Expr::Local { var, .. }) if *var == *v => {}
5590            (Tgt::Fld, Expr::Field { cls: c2, name: n2, .. }) => {
5591                let Expr::Field { cls: c1, name: n1, .. } = &**target else {
5592                    return;
5593                };
5594                if c1 != c2 || n1 != n2 {
5595                    return;
5596                }
5597            }
5598            _ => return,
5599        }
5600        // ±1 constant on a numeric var → post-inc/dec.
5601        let one = match &**r {
5602            Expr::Const(ConstVal::Int(1)) => Some(1i64),
5603            Expr::Const(ConstVal::Int(-1)) => Some(-1),
5604            Expr::Const(ConstVal::Long(1)) => Some(1),
5605            Expr::Const(ConstVal::Long(-1)) => Some(-1),
5606            _ => None,
5607        };
5608        if *op == BinOp::Add || *op == BinOp::Sub {
5609            if let Some(mut d) = one {
5610                if *op == BinOp::Sub {
5611                    d = -d;
5612                }
5613                let tgt_ty = match &tgt {
5614                    Tgt::Loc(v) => vt.var(*v).ty.erased(),
5615                    Tgt::Fld => {
5616                        let Expr::Field { ty, .. } = &**target else {
5617                            return;
5618                        };
5619                        ty.erased()
5620                    }
5621                };
5622                let numeric = matches!(
5623                    tgt_ty,
5624                    JavaType::Int
5625                        | JavaType::Long
5626                        | JavaType::Short
5627                        | JavaType::Byte
5628                        | JavaType::Char
5629                        | JavaType::Float
5630                        | JavaType::Double
5631                );
5632                if numeric {
5633                    let wide = matches!(tgt_ty, JavaType::Long);
5634                    let inner = match &tgt {
5635                        Tgt::Loc(v) => Expr::Local {
5636                            var: *v,
5637                            ty: vt.var(*v).ty.clone(),
5638                        },
5639                        Tgt::Fld => (**target).clone(),
5640                    };
5641                    *e = Expr::PostIncDec {
5642                        e: Box::new(inner),
5643                        delta: d,
5644                        wide,
5645                    };
5646                    return;
5647                }
5648            }
5649        }
5650        let cop = match op {
5651            BinOp::Add => AssignOp::Add,
5652            BinOp::Sub => AssignOp::Sub,
5653            BinOp::Mul => AssignOp::Mul,
5654            BinOp::Div => AssignOp::Div,
5655            BinOp::Rem => AssignOp::Rem,
5656            BinOp::Shl => AssignOp::Shl,
5657            BinOp::Shr => AssignOp::Shr,
5658            BinOp::Ushr => AssignOp::Ushr,
5659            BinOp::And => AssignOp::And,
5660            BinOp::Or => AssignOp::Or,
5661            BinOp::Xor => AssignOp::Xor,
5662            _ => return,
5663        };
5664        let rhs = std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
5665        let new_target = match &tgt {
5666            Tgt::Loc(v) => Box::new(Expr::Local {
5667                var: *v,
5668                ty: vt.var(*v).ty.clone(),
5669            }),
5670            Tgt::Fld => target.clone(),
5671        };
5672        *e = Expr::Assign {
5673            target: new_target,
5674            op: cop,
5675            value: rhs,
5676        };
5677    });
5678}
5679
5680pub fn fix_bool_xor(body: &mut Stmt, vt: &VarTable, ret_bool: bool) {
5681    fn is_one(e: &Expr) -> bool {
5682        matches!(e, Expr::Const(ConstVal::Int(1)))
5683    }
5684    fn bty(e: &Expr, vt: &VarTable) -> bool {
5685        matches!(
5686            match e {
5687                Expr::Local { var, .. } => vt.var(*var).ty.erased(),
5688                other => other.type_ref().erased(),
5689            },
5690            JavaType::Boolean
5691        )
5692    }
5693    // Int-typed `x ^ 1` in a BOOLEAN sink is the compiler's `!x` over a
5694    // 0/1 slot: rewrite to `x == 0` (the booleanize return-wrap misses
5695    // Xor shapes — weibo ArraysKt `return v2 ^ 1;` against a boolean
5696    // return, "int无法转换为boolean").
5697    fn inv_of_xor(x: &Expr, vt: &VarTable) -> Option<Expr> {
5698        if let Expr::Bin { op: BinOp::Xor, l, r, .. } = x {
5699            let (int_side, one_side) = if is_one(r) {
5700                (l, r)
5701            } else if is_one(l) {
5702                (r, l)
5703            } else {
5704                return None;
5705            };
5706            let _ = one_side;
5707            if !bty(int_side, vt) {
5708                return Some(Expr::Bin {
5709                    op: BinOp::Eq,
5710                    l: int_side.clone(),
5711                    r: Box::new(Expr::Const(ConstVal::Int(0))),
5712                    ty: Some(TypeRef::J(JavaType::Boolean)),
5713                });
5714            }
5715        }
5716        None
5717    }
5718    let tgt_bool = |target: &Expr, vt: &VarTable| -> bool {
5719        match target {
5720            Expr::Local { var, .. } => matches!(vt.var(*var).ty.erased(), JavaType::Boolean),
5721            Expr::Field { ty, .. } => matches!(ty.erased(), JavaType::Boolean),
5722            _ => false,
5723        }
5724    };
5725    walk_mut_deep(body, &mut |st| match st {
5726        Stmt::Return(Some(e)) if ret_bool => {
5727            if let Some(r) = inv_of_xor(e, vt) {
5728                *e = r;
5729            }
5730        }
5731        Stmt::ExprStmt(Expr::Assign { target, value, op: AssignOp::Plain, .. }) => {
5732            if tgt_bool(target, vt) {
5733                if let Some(r) = inv_of_xor(value, vt) {
5734                    **value = r;
5735                }
5736            }
5737        }
5738        Stmt::LocalDef { var, init: Some(value), .. }
5739            if matches!(vt.var(*var).ty.erased(), JavaType::Boolean) =>
5740        {
5741            if let Some(r) = inv_of_xor(value, vt) {
5742                *value = r;
5743            }
5744        }
5745        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
5746            if let Some(r) = inv_of_xor(cond, vt) {
5747                *cond = r;
5748            }
5749        }
5750        _ => {}
5751    });
5752    fn ity(e: &Expr, vt: &VarTable) -> bool {
5753        matches!(
5754            match e {
5755                Expr::Local { var, .. } => vt.var(*var).ty.erased(),
5756                other => other.type_ref().erased(),
5757            },
5758            JavaType::Int | JavaType::Short | JavaType::Byte | JavaType::Char
5759        )
5760    }
5761    /// Mixed-kind bitwise rewrite: Kotlin's non-short-circuit
5762    /// `or`/`and` over 0/1 ints mixes generations once booleanize
5763    /// converts one side (`delete | delete2` — "boolean无法转换为int" at
5764    /// the OPERAND, weixin SQLiteDatabase ×3.3k), and `b ^ 1` is `!b`.
5765    /// The int side is a 0/1 encoding: compare it, keeping the chain
5766    /// boolean end to end. ONLY legal into a BOOLEAN sink — ungated, it
5767    /// rewrote int-sink `v6 = v5x | 4` into `v5x | 4 != 0` assigned to
5768    /// an int var (boolean无法转换为int, weixin yq5/c ×2.1k family).
5769    fn mixed_rewrite(e: &mut Expr, vt: &VarTable) {
5770        deep_rewrite(e, &mut |x| {
5771            if let Expr::Bin { op, l, r, ty } = x {
5772                match op {
5773                    BinOp::Xor => {
5774                        if is_one(r) && bty(l, vt) {
5775                            let taken =
5776                                std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
5777                            *x = Expr::Un { op: UnOp::Not, e: taken };
5778                        } else if is_one(l) && bty(r, vt) {
5779                            let taken =
5780                                std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
5781                            *x = Expr::Un { op: UnOp::Not, e: taken };
5782                        }
5783                    }
5784                    BinOp::Or | BinOp::And => {
5785                        if bty(l, vt) && ity(r, vt) {
5786                            let taken =
5787                                std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
5788                            **r = Expr::Bin {
5789                                op: BinOp::Ne,
5790                                l: taken,
5791                                r: Box::new(Expr::Const(ConstVal::Int(0))),
5792                                ty: Some(TypeRef::J(JavaType::Boolean)),
5793                            };
5794                            *ty = Some(TypeRef::J(JavaType::Boolean));
5795                        } else if ity(l, vt) && bty(r, vt) {
5796                            let taken =
5797                                std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
5798                            **l = Expr::Bin {
5799                                op: BinOp::Ne,
5800                                l: taken,
5801                                r: Box::new(Expr::Const(ConstVal::Int(0))),
5802                                ty: Some(TypeRef::J(JavaType::Boolean)),
5803                            };
5804                            *ty = Some(TypeRef::J(JavaType::Boolean));
5805                        }
5806                    }
5807                    _ => {}
5808                }
5809            }
5810        });
5811    }
5812    // Boolean sinks: bool-typed assign/def targets, boolean-method
5813    // returns, and loop/branch conditions.
5814    walk_mut_deep(body, &mut |st| match st {
5815        Stmt::Return(Some(e)) if ret_bool => mixed_rewrite(e, vt),
5816        Stmt::ExprStmt(Expr::Assign { target, value, op: AssignOp::Plain, .. }) => {
5817            if tgt_bool(target, vt) {
5818                mixed_rewrite(value, vt);
5819            }
5820        }
5821        Stmt::LocalDef { var, init: Some(e), .. }
5822            if matches!(vt.var(*var).ty.erased(), JavaType::Boolean) =>
5823        {
5824            mixed_rewrite(e, vt);
5825        }
5826        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
5827            mixed_rewrite(cond, vt);
5828        }
5829        _ => {}
5830    });
5831    // A conditional's test is a boolean sink wherever it sits (including
5832    // inside int-sink statements the walk above skips).
5833    walk_stmt_exprs(body, &mut |e| {
5834        deep_rewrite(e, &mut |x| {
5835            if let Expr::Cond { c, .. } = x {
5836                mixed_rewrite(c, vt);
5837            }
5838        });
5839    });
5840}
5841
5842/// Boolean inference: vars only ever assigned 0/1/comparisons/booleans and
5843/// read in conditions become `boolean`, with `v != 0` → `v` in conditions.
5844/// Returns the number of vars converted (0 = nothing changed — the
5845/// caller skips the post-booleanize re-split, which only has work when
5846/// a conversion exposed a mixed-kind register).
5847pub fn booleanize(vt: &mut VarTable, body: &mut Stmt, ret_bool: bool) -> usize {
5848    // Booleans propagate through local chains (`v17 = v24` where v24
5849    // itself became boolean in the first round) — iterate to a fixpoint;
5850    // the common corpus converts nothing and exits after one round.
5851    // The deref disqualifier set is structural (positions, not types) —
5852    // compute ONCE per call, not per round (12 full walks on big
5853    // methods cost weibo +60% wall).
5854    let n0 = vt.vars.len();
5855    let mut derefed = vec![false; n0];
5856    mark_derefs(body, &mut derefed);
5857    let mut total = 0usize;
5858    for _ in 0..4 {
5859        let n = booleanize_round(vt, body, ret_bool, &derefed);
5860        if n == 0 {
5861            break;
5862        }
5863        total += n;
5864    }
5865    total
5866}
5867
5868/// Mark vars used in DEREF positions (field/method receiver, array
5869/// base) — allocation-free recursion (visit_all_exprs allocates a
5870/// child Vec per statement; two callers run this per method).
5871fn mark_deref_expr(e: &Expr, derefed: &mut [bool]) {
5872    match e {
5873        Expr::Field { owner: Some(o), is_static: false, .. }
5874        | Expr::Method { owner: Some(o), .. } => {
5875            if let Expr::Local { var, .. } = &**o {
5876                if (*var as usize) < derefed.len() {
5877                    derefed[*var as usize] = true;
5878                }
5879            }
5880        }
5881        Expr::ArrayIndex { array, .. } => {
5882            if let Expr::Local { var, .. } = &**array {
5883                if (*var as usize) < derefed.len() {
5884                    derefed[*var as usize] = true;
5885                }
5886            }
5887        }
5888        _ => {}
5889    }
5890    for_each_child(e, &mut |c| mark_deref_expr(c, derefed));
5891}
5892
5893fn mark_derefs(body: &Stmt, derefed: &mut [bool]) {
5894    walk_all(body, &mut |st| {
5895        match st {
5896            Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
5897                mark_deref_expr(e, derefed);
5898            }
5899            Stmt::Return(Some(e)) => mark_deref_expr(e, derefed),
5900            Stmt::LocalDef { init: Some(e), .. } => mark_deref_expr(e, derefed),
5901            Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
5902                mark_deref_expr(cond, derefed);
5903            }
5904            Stmt::For { init, .. } => {
5905                for x in init {
5906                    mark_derefs(x, derefed);
5907                }
5908            }
5909            Stmt::Switch { selector, .. } => mark_deref_expr(selector, derefed),
5910            Stmt::ForEach { iterable, .. } => mark_deref_expr(iterable, derefed),
5911            _ => {}
5912        }
5913    });
5914}
5915
5916fn booleanize_round(
5917    vt: &mut VarTable,
5918    body: &mut Stmt,
5919    ret_bool: bool,
5920    derefed: &[bool],
5921) -> usize {
5922    let n = vt.vars.len();
5923    let mut in_cond = vec![false; n];
5924
5925    // Condition uses: `v == 0` / `v != 0` if-conditions and comparison
5926    // operands inside assigned values.
5927    walk_all(body, &mut |st| {
5928        let cond: Option<&Expr> = match st {
5929            Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
5930                Some(cond)
5931            }
5932            _ => None,
5933        };
5934        if let Some(c) = cond {
5935            if let Expr::Bin {
5936                op: BinOp::Eq | BinOp::Ne,
5937                l,
5938                r,
5939                ..
5940            } = c
5941            {
5942                for (x, other) in [(l, r), (r, l)] {
5943                    if let (Expr::Local { var, .. }, Expr::Const(ConstVal::Int(0))) =
5944                        (&**x, &**other)
5945                    {
5946                        if (*var as usize) < n {
5947                            in_cond[*var as usize] = true;
5948                        }
5949                    }
5950                }
5951            }
5952        }
5953        // A local RETURNED from a boolean method is boolean-typed even
5954        // though it never appears in a condition (`int v9 = 0/1 … return
5955        // v9;` — returning an int from `boolean check()` is a compile
5956        // error; lab package Obf.check).
5957        if ret_bool {
5958            if let Stmt::Return(Some(Expr::Local { var, .. })) = st {
5959                if (*var as usize) < n {
5960                    in_cond[*var as usize] = true;
5961                }
5962            }
5963        }
5964        // A local STORED into a boolean field is boolean-typed even though
5965        // it never appears in a condition (`b = v1` where `b` is a boolean
5966        // field, `v1` an int 0/1 local — q.java static init). The emit
5967        // layer already coerces a boolean-target assign through expr_bool,
5968        // but that renders a bare int LOCAL unchanged, so the local itself
5969        // must be booleanized (its 0/1 assigns then print false/true).
5970        if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = st {
5971            if let Expr::Field { ty, .. } = &**target {
5972                if ty.erased() == JavaType::Boolean {
5973                    if let Expr::Local { var, .. } = &**value {
5974                        if (*var as usize) < n {
5975                            in_cond[*var as usize] = true;
5976                        }
5977                    }
5978                }
5979            }
5980        }
5981        let val: Option<&Expr> = match st {
5982            Stmt::ExprStmt(Expr::Assign { value, .. }) => Some(value),
5983            // Bare expression statements (e.g. `q(v117);`) so a local
5984            // passed to a boolean parameter is caught below.
5985            Stmt::ExprStmt(e) => Some(e),
5986            Stmt::LocalDef { init: Some(e), .. } => Some(e),
5987            Stmt::Return(Some(e)) => Some(e),
5988            _ => None,
5989        };
5990        if let Some(v) = val {
5991            visit_exprs(v, &mut |x| {
5992                // A local passed as a BOOLEAN parameter is boolean-typed
5993                // (`q(v117)` where q's param is boolean, v117 an int 0/1 —
5994                // f8/b.java). The DEX passes booleans as 0/1 ints, so a
5995                // boolean param slot is authoritative for the argument's
5996                // type.
5997                if let Expr::Method { desc, args, .. } = x {
5998                    for (i, a) in args.iter().enumerate() {
5999                        if desc.args.get(i) == Some(&JavaType::Boolean) {
6000                            if let Expr::Local { var, .. } = a {
6001                                if (*var as usize) < n {
6002                                    in_cond[*var as usize] = true;
6003                                }
6004                            }
6005                        }
6006                    }
6007                }
6008                if let Expr::Bin { op, l, r, .. } = x {
6009                    if matches!(
6010                        op,
6011                        BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
6012                    ) {
6013                        for side in [l, r] {
6014                            if let Expr::Local { var, .. } = &**side {
6015                                if (*var as usize) < n {
6016                                    in_cond[*var as usize] = true;
6017                                }
6018                            }
6019                        }
6020                    }
6021                    // Kotlin/d8 merge booleans through INT bitwise ops
6022                    // (`v3 | obj instanceof g`) == 0 — the other side's
6023                    // static type being boolean makes this a boolean
6024                    // context for the local (a genuine int bitwise
6025                    // expression never has a boolean operand).
6026                    if matches!(op, BinOp::Or | BinOp::And) {
6027                        let l_bool = l.type_ref().erased() == JavaType::Boolean;
6028                        let r_bool = r.type_ref().erased() == JavaType::Boolean;
6029                        if l_bool != r_bool {
6030                            let side = if l_bool { r } else { l };
6031                            if let Expr::Local { var, .. } = &**side {
6032                                if (*var as usize) < n {
6033                                    in_cond[*var as usize] = true;
6034                                }
6035                            }
6036                        }
6037                    }
6038                }
6039            });
6040        }
6041    });
6042
6043    // A var whose every assignment value is boolean-shaped AND which is
6044    // referenced in conditions becomes boolean.
6045    // (Perf: this used to re-walk the whole statement tree ONCE PER VAR —
6046    // O(vars × stmts); on R8-merged monsters (2000+ vars, 15k statements)
6047    // that was tens of millions of visits. One pass collects the same
6048    // (any, all_bool) facts for every var.)
6049    let mut assigned_any = vec![false; n];
6050    let mut all_bool = vec![true; n];
6051    let mut edges: Vec<(usize, usize)> = Vec::new();
6052    walk_all(body, &mut |st| {
6053        let (var, value) = match st {
6054            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => match &**target {
6055                Expr::Local { var, .. } => (var, &**value),
6056                _ => return,
6057            },
6058            Stmt::LocalDef {
6059                var, init: Some(e), ..
6060            } => (var, e),
6061            _ => return,
6062        };
6063        let i = *var as usize;
6064        if i < n {
6065            assigned_any[i] = true;
6066            if !is_boolean_valued(value, vt) {
6067                all_bool[i] = false;
6068            }
6069            // Chain edges for the context closure below: `v17 = v24`
6070            // links the two locals; when the TARGET is in a boolean
6071            // context, the source inherits it (its only reader is the
6072            // boolean-shaped chain). Collected as edges because the
6073            // target's context can be discovered anywhere in the tree.
6074            if let Expr::Local { var: src, .. } = value {
6075                if (*src as usize) < n {
6076                    edges.push((i, *src as usize));
6077                }
6078            }
6079        }
6080    });
6081    // Context closure along local chains: a var in a boolean context
6082    // pushes that context through every local it is assigned from
6083    // (`return v17` — v17 = v24 — v24 = 0/1) — the statement order
6084    // cannot be relied on (the return may follow the assignment).
6085    loop {
6086        let mut changed = false;
6087        for &(tgt, src) in &edges {
6088            if in_cond[tgt] && !in_cond[src] && all_bool[src] {
6089                in_cond[src] = true;
6090                changed = true;
6091            }
6092            // FORWARD through pure copies: `v21 = v17` with v17 boolean
6093            // makes v21 boolean when ALL of v21's assignments are
6094            // bool-shaped (the loop-state save pattern — weixin u2/f's
6095            // v62 → v17 → v21 chains left the copy targets int while
6096            // the accumulator booleanized).
6097            if in_cond[src] && !in_cond[tgt] && all_bool[tgt] {
6098                in_cond[tgt] = true;
6099                changed = true;
6100            }
6101        }
6102        if !changed {
6103            break;
6104        }
6105    }
6106    let reads = count_locals_stmts(std::slice::from_ref(body));
6107    let mut boolean_vars: HashSet<u32> = HashSet::default();
6108    for i in 0..n {
6109        if vt.vars[i].is_param {
6110            continue;
6111        }
6112        if derefed[i] {
6113            continue;
6114        }
6115        if !in_cond[i] && !(reads.get(&(i as u32)).copied().unwrap_or(0) == 0) {
6116            continue;
6117        }
6118        if assigned_any[i] && all_bool[i] {
6119            boolean_vars.insert(i as u32);
6120        }
6121    }
6122    if boolean_vars.is_empty() {
6123        return 0;
6124    }
6125    for v in &boolean_vars {
6126        vt.vars[*v as usize].ty = TypeRef::J(JavaType::Boolean);
6127    }
6128    let types: Vec<&TypeRef> = vt.vars.iter().map(|v| &v.ty).collect();
6129    rewrite_exprs(body, &mut |e| {
6130        deep_rewrite(e, &mut |x| {
6131            if let Expr::Local { var, ty } = x {
6132                if let Some(want) = types.get(*var as usize) {
6133                    if ty != *want {
6134                        *ty = (*want).clone();
6135                    }
6136                }
6137            }
6138        });
6139    });
6140
6141    // Condition folding: `b != 0` → `b`, `b == 0` → `!b` (boolean vars).
6142    fold_bool_conditions(body, &boolean_vars);
6143    boolean_vars.len()
6144}
6145
6146fn is_boolean_valued(e: &Expr, vt: &VarTable) -> bool {
6147    match e {
6148        Expr::Const(ConstVal::Int(0)) | Expr::Const(ConstVal::Int(1)) => true,
6149        // Kotlin's non-short-circuit boolean `or`/`and` compiles to `|`/`&`
6150        // over boolean locals (`v15 = delete | delete2 | ..`). EITHER side
6151        // boolean suffices: `int | boolean` is illegal in source — it is
6152        // always a not-yet-booleanized side of a boolean chain (the
6153        // loop-carried accumulator `v15 = v20; v20 = v15 | delete3` can
6154        // never seed its all_bool fixpoint otherwise).
6155        Expr::Bin { op: BinOp::Or | BinOp::And, l, r, .. } => {
6156            is_boolean_valued(l, vt) || is_boolean_valued(r, vt)
6157        }
6158        // Kotlin's `!x` lowers to `x ^ 1`; at booleanize time the value
6159        // is still the Xor (fix_bool_xor runs later), so an int-typed
6160        // generation receiving it never retyped (weixin v2/i's
6161        // `int v264_g160_g7 = !v200`). BOTH sides must be bool-shaped:
6162        // `bool ^ 1` qualifies (Const 0/1 is boolean-valued above),
6163        // a genuine int `flags ^ 1` does not (flags stays Int in vt).
6164        // MUST precede the generic Bin arm (which would swallow Xor).
6165        Expr::Bin {
6166            op: BinOp::Xor,
6167            l,
6168            r,
6169            ..
6170        } => is_boolean_valued(l, vt) && is_boolean_valued(r, vt),
6171        Expr::Bin { op, .. } => matches!(
6172            op,
6173            BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
6174        ),
6175        Expr::InstanceOf { .. } => true,
6176        // The VarTable is the authority: the embedded Local ty is a
6177        // lift-time snapshot and goes STALE across booleanize rounds —
6178        // a converted copy source kept its partners int (`v150 = v131`
6179        // boolean→int, weixin ConstraintLayout ×1.4k files) because the
6180        // all_bool fixpoint consulted the stale Int.
6181        Expr::Local { var, ty } => {
6182            let t = if (*var as usize) < vt.vars.len() {
6183                vt.var(*var).ty.erased()
6184            } else {
6185                ty.erased()
6186            };
6187            t == JavaType::Boolean
6188        }
6189        Expr::Method { desc, .. } => desc.ret == JavaType::Boolean,
6190        Expr::Cond { t, f, .. } => is_boolean_valued(t, vt) && is_boolean_valued(f, vt),
6191        Expr::Un { op: UnOp::Not, .. } => true,
6192        // Field ty comes from the dex descriptor — authoritative, not a
6193        // lift snapshot (`!w1x.i` arrives here still as `w1x.i ^ 1`; the
6194        // Xor arm needs this side bool-shaped).
6195        Expr::Field { ty, .. } => ty.erased() == JavaType::Boolean,
6196        _ => false,
6197    }
6198}
6199
6200fn fold_bool_conditions(s: &mut Stmt, bv: &HashSet<u32>) {
6201    let fold = |e: &mut Expr| {
6202        deep_rewrite(e, &mut |x| {
6203            if let Expr::Bin { op, l, r, .. } = x {
6204                let (var_side, const_side) = match (&**l, &**r) {
6205                    (Expr::Local { .. }, Expr::Const(ConstVal::Int(_))) => (l, 0),
6206                    (Expr::Const(ConstVal::Int(_)), Expr::Local { .. }) => (r, 1),
6207                    _ => return,
6208                };
6209                let var = match &**var_side {
6210                    Expr::Local { var, .. } => *var,
6211                    _ => return,
6212                };
6213                if !bv.contains(&var) {
6214                    return;
6215                }
6216                let _ = const_side;
6217                match op {
6218                    BinOp::Ne => {
6219                        let inner = var_side.clone();
6220                        *x = *inner;
6221                    }
6222                    BinOp::Eq => {
6223                        let inner = var_side.clone();
6224                        *x = Expr::Un {
6225                            op: UnOp::Not,
6226                            e: Box::new(*inner),
6227                        };
6228                    }
6229                    _ => {}
6230                }
6231            }
6232        });
6233    };
6234    match s {
6235        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => fold(cond),
6236        _ => {}
6237    }
6238    walk_mut_deep(s, &mut |st| {
6239        let cond = match st {
6240            Stmt::If { cond, .. } => Some(cond),
6241            Stmt::While { cond, .. } => Some(cond),
6242            Stmt::DoWhile { cond, .. } => Some(cond),
6243            _ => None,
6244        };
6245        if let Some(c) = cond {
6246            fold(c);
6247        }
6248    });
6249}
6250
6251/// Normalize `1 == v` comparisons to `v == 1` (d8 emits const-first if-eq).
6252pub fn flip_const_compares(s: &mut Stmt) {
6253    rewrite_exprs(s, &mut |e| {
6254        deep_rewrite(e, &mut |x| {
6255            if let Expr::Bin { op, l, r, .. } = x {
6256                if matches!(
6257                    op,
6258                    BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
6259                ) {
6260                    let l_const = matches!(&**l, Expr::Const(_));
6261                    let r_var = matches!(&**r, Expr::Local { .. });
6262                    if l_const && r_var {
6263                        std::mem::swap(l, r);
6264                    }
6265                }
6266            }
6267        });
6268    });
6269}
6270
6271/// Object-typed locals compared against 0 compare against `null`.
6272pub fn null_compares(vt: &VarTable, body: &mut Stmt) {
6273    let obj_vars: HashSet<u32> = vt
6274        .vars
6275        .iter()
6276        .filter(|v| v.ty.erased().is_reference())
6277        .map(|v| v.id)
6278        .collect();
6279    rewrite_exprs(body, &mut |e| {
6280        deep_rewrite(e, &mut |x| {
6281            null_side_rewrite(x, &|v| obj_vars.contains(&v));
6282        });
6283    });
6284}
6285
6286/// Drop assignments/declarations of locals that are NEVER read anywhere
6287/// in the method. Register-machine merges commit values no Java-level
6288/// code consumes (phi residue like `printStream = check;` — not just
6289/// noise: the merge variable's inferred type need not match the value,
6290/// so these lines are often type errors as well). Side-effect-free
6291/// values drop entirely; an impure value survives as a bare expression
6292/// statement. Iterated to a fixpoint: dropping `a = b` can make `b`
6293/// unread.
6294pub fn drop_dead_locals(body: &mut Stmt) {
6295    loop {
6296        let mut reads: Vec<usize> = Vec::new();
6297        count_reads(body, &mut reads);
6298        let mut dropped = 0usize;
6299        walk_mut_deep(body, &mut |st| match st {
6300            Stmt::Block(items) => prune_dead_items(items, &reads, &mut dropped),
6301            // Switch case bodies and For inits are bare `Vec<Stmt>`, NOT
6302            // wrapped in a `Stmt::Block`, so the Block arm never sees them.
6303            // Dead phi commits land directly in case bodies (`sb4 =
6304            // compareTo;` per case): pruning only Blocks dropped the
6305            // commit's reader (in a real post-switch Block) but left the
6306            // now-dead commits, stalling the fixpoint cascade and leaking
6307            // mistyped phi residue. TryWithResources.resources is
6308            // deliberately excluded — its LocalDefs carry auto-close
6309            // semantics that a bare-expression rewrite would break.
6310            Stmt::Switch { cases, .. } => {
6311                for c in cases {
6312                    prune_dead_items(&mut c.body, &reads, &mut dropped);
6313                }
6314            }
6315            Stmt::For { init, .. } => prune_dead_items(init, &reads, &mut dropped),
6316            _ => {}
6317        });
6318        if dropped == 0 {
6319            break;
6320        }
6321    }
6322}
6323
6324/// Remove dead assignments/declarations from one bare statement vector.
6325/// Shared by `drop_dead_locals` across every prunable `Vec<Stmt>`
6326/// container. Side-effect-free values drop entirely; an impure value
6327/// survives as a bare expression statement.
6328fn prune_dead_items(items: &mut Vec<Stmt>, reads: &[usize], dropped: &mut usize) {
6329    let dead = |v: u32| reads.get(v as usize).copied().unwrap_or(0) == 0;
6330    items.retain_mut(|x| match x {
6331        Stmt::ExprStmt(Expr::Assign { target, value, op, .. }) => {
6332            let dead_target = matches!(&**target, Expr::Local { var, .. } if dead(*var));
6333            if dead_target && matches!(*op, AssignOp::Plain) {
6334                *dropped += 1;
6335                if has_side_effects(value) {
6336                    let e = std::mem::replace(value, Box::new(Expr::This));
6337                    *x = Stmt::ExprStmt(*e);
6338                    true
6339                } else {
6340                    false
6341                }
6342            } else {
6343                true
6344            }
6345        }
6346        Stmt::LocalDef { var, init, .. } if dead(*var) => {
6347            *dropped += 1;
6348            if let Some(e) = init.take() {
6349                if has_side_effects(&e) {
6350                    *x = Stmt::ExprStmt(e);
6351                    return true;
6352                }
6353            }
6354            false
6355        }
6356        _ => true,
6357    });
6358}
6359
6360/// Immutable shallow child walk (mirrors `walk_mut`), for the two-pass
6361/// scope analysis below.
6362fn for_each_child_stmt<'a>(st: &'a Stmt, f: &mut impl FnMut(&'a Stmt)) {
6363    match st {
6364        Stmt::Block(v) => {
6365            for x in v {
6366                f(x);
6367            }
6368        }
6369        Stmt::If {
6370            then_stmt,
6371            else_stmt,
6372            ..
6373        } => {
6374            f(then_stmt);
6375            if let Some(e) = else_stmt {
6376                f(e);
6377            }
6378        }
6379        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => f(body),
6380        Stmt::For { init, body, .. } => {
6381            for x in init {
6382                f(x);
6383            }
6384            f(body);
6385        }
6386        Stmt::ForEach { body, .. } => f(body),
6387        Stmt::Switch { cases, default, .. } => {
6388            for c in cases {
6389                for x in &c.body {
6390                    f(x);
6391                }
6392            }
6393            if let Some(d) = default {
6394                f(d);
6395            }
6396        }
6397        Stmt::Try {
6398            body,
6399            catches,
6400            finally,
6401        } => {
6402            f(body);
6403            for c in catches {
6404                f(&c.body);
6405            }
6406            if let Some(fl) = finally {
6407                f(fl);
6408            }
6409        }
6410        Stmt::TryWithResources {
6411            resources,
6412            body,
6413            catches,
6414            finally,
6415        } => {
6416            for r in resources {
6417                f(r);
6418            }
6419            f(body);
6420            for c in catches {
6421                f(&c.body);
6422            }
6423            if let Some(fl) = finally {
6424                f(fl);
6425            }
6426        }
6427        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => f(body),
6428        _ => {}
6429    }
6430}
6431
6432/// Vars occurring in this statement's OWN expression payloads (nested
6433/// sub-statements are descended separately so every occurrence carries
6434/// its innermost block id). ForEach/Catch bound vars are declarations,
6435/// not occurrences.
6436fn stmt_shallow_vars<F: FnMut(u32)>(st: &Stmt, out: &mut F) {
6437    fn ex<F: FnMut(u32)>(e: &Expr, out: &mut F) {
6438        visit_exprs(e, &mut |x| {
6439            if let Expr::Local { var, .. } = x {
6440                out(*var);
6441            }
6442        });
6443    }
6444    match st {
6445        Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
6446            ex(e, out)
6447        }
6448        Stmt::TernaryValue { e } => ex(e, out),
6449        Stmt::Return(Some(e)) => ex(e, out),
6450        Stmt::LocalDef { var, init, .. } => {
6451            out(*var);
6452            if let Some(e) = init {
6453                ex(e, out);
6454            }
6455        }
6456        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
6457            ex(cond, out)
6458        }
6459        Stmt::For { cond, update, .. } => {
6460            if let Some(c) = cond {
6461                ex(c, out);
6462            }
6463            for u in update {
6464                ex(u, out);
6465            }
6466        }
6467        Stmt::ForEach { iterable, .. } => ex(iterable, out),
6468        Stmt::Switch { selector, cases, .. } => {
6469            ex(selector, out);
6470            for cg in cases {
6471                if let Some(g) = &cg.guard {
6472                    ex(g, out);
6473                }
6474            }
6475        }
6476        Stmt::Assert { cond, msg } => {
6477            ex(cond, out);
6478            if let Some(m) = msg {
6479                ex(m, out);
6480            }
6481        }
6482        Stmt::Synchronized { lock, .. } => ex(lock, out),
6483        _ => {}
6484    }
6485}
6486
6487/// Pass A: number every Block in pre-order (root = 0) and record each
6488/// var's defining block. `sizes[id]` = the id-range span of the subtree.
6489fn scope_pass_a(st: &Stmt, cur: u32, next: &mut u32, def_block: &mut [u32], sizes: &mut Vec<u32>) {
6490    if let Stmt::LocalDef { var, .. } = st {
6491        let i = *var as usize;
6492        if i < def_block.len() && def_block[i] == u32::MAX {
6493            def_block[i] = cur;
6494        }
6495    }
6496    for_each_child_stmt(st, &mut |c| {
6497        if matches!(c, Stmt::Block(_)) {
6498            let id = *next;
6499            *next += 1;
6500            sizes.push(0);
6501            scope_pass_a(c, id, next, def_block, sizes);
6502            sizes[id as usize] = *next - id;
6503        } else {
6504            scope_pass_a(c, cur, next, def_block, sizes);
6505        }
6506    });
6507}
6508
6509/// Pass B (same numbering walk): count each var's occurrences overall
6510/// and within its defining block's subtree.
6511fn scope_pass_b(
6512    st: &Stmt,
6513    cur: u32,
6514    next: &mut u32,
6515    def_block: &[u32],
6516    sizes: &[u32],
6517    within: &mut [u32],
6518    total: &mut [u32],
6519) {
6520    stmt_shallow_vars(st, &mut |v| {
6521        let i = v as usize;
6522        if i < total.len() {
6523            total[i] += 1;
6524            let d = def_block[i];
6525            if d != u32::MAX && d <= cur && cur < d + sizes[d as usize] {
6526                within[i] += 1;
6527            }
6528        }
6529    });
6530    for_each_child_stmt(st, &mut |c| {
6531        if matches!(c, Stmt::Block(_)) {
6532            let id = *next;
6533            *next += 1;
6534            scope_pass_b(c, id, next, def_block, sizes, within, total);
6535        } else {
6536            scope_pass_b(c, cur, next, def_block, sizes, within, total);
6537        }
6538    });
6539}
6540
6541/// Declaration hygiene: every used var that has no LocalDef gets a bare
6542/// declaration at the top; duplicate LocalDefs demote to assignments.
6543/// In an enum's `<clinit>`, every constant's assignment
6544/// (`Self.FIELD = new Self("NAME", ordinal, ...)`) is compiler-mandated
6545/// boilerplate — when the class renders as a true `enum` declaration the
6546/// constants live in the header and these assignments must go. Only
6547/// ACC_ENUM-flagged static fields of the class itself are touched; the
6548/// `$VALUES` array assignment stays (a static block in an enum is legal).
6549pub fn strip_enum_const_inits(s: &mut Stmt, class: &crate::PoolClass) {
6550    let const_fields: jdc_core::FxHashSet<&str> = class
6551        .static_fields
6552        .iter()
6553        .filter(|f| f.access & crate::access::ACC_ENUM != 0)
6554        .map(|f| f.name.as_str())
6555        .collect();
6556    if const_fields.is_empty() {
6557        return;
6558    }
6559    if let Stmt::Block(stmts) = s {
6560        stmts.retain(|st| {
6561            !matches!(
6562                st,
6563                Stmt::ExprStmt(Expr::Assign { target, value, .. })
6564                    if matches!(&**target,
6565                        Expr::Field { cls, name, is_static: true, .. }
6566                            if cls.as_ref() == class.name
6567                                && const_fields.contains(&**name))
6568                        && matches!(&**value, Expr::New { cls: ncls, .. } if ncls.as_ref() == class.name)
6569            )
6570        });
6571    }
6572}
6573
6574/// Java requires the `super(...)`/`this(...)` delegation to be the FIRST
6575/// statement of a constructor. d8/R8 order the outer-reference capture
6576/// (`this.b = p1;`) or other field writes before the invokesuper in the
6577/// bytecode, which lifts as-is into an uncompilable statement order —
6578/// hoist the bare delegation call to position 0 (jadx does the same).
6579/// Does the expression touch `this` (explicit, implicit-instance member
6580/// access, or super)? Such a def cannot be inlined into a delegation
6581/// that must run BEFORE the supertype constructor.
6582fn contains_this_access(e: &Expr) -> bool {
6583    let mut hit = false;
6584    visit_exprs(e, &mut |x| {
6585        if hit {
6586            return;
6587        }
6588        match x {
6589            Expr::This => hit = true,
6590            Expr::Field { owner, is_static: false, .. } => {
6591                if owner.is_none() || matches!(&**owner.as_ref().unwrap(), Expr::This) {
6592                    hit = true;
6593                }
6594            }
6595            Expr::Method { owner, is_static: false, .. } => {
6596                if owner.is_none() || matches!(&**owner.as_ref().unwrap(), Expr::This) {
6597                    hit = true;
6598                }
6599            }
6600            _ => {}
6601        }
6602    });
6603    hit
6604}
6605
6606/// Inline the transitive straight-line defs of a delegation call's
6607/// arg locals into the call, so hoisting it to position 0 leaves no
6608/// dangling name (lark ProtoAdapter's `super(str)` with `str` defined
6609/// BELOW the hoist point — 找不到符号 变量 str). Returns false when any
6610/// arg var has no inlineable def (param-less, missing, this-touching,
6611/// or cyclic) — the caller then leaves the call in place: a
6612/// not-first-statement error is cheaper than an unresolved name that
6613/// poisons every downstream type.
6614/// Any occurrence of `var` (read OR assign-target) in `stmts` — the
6615/// suffix-use test for dropping a consumed def: count_locals_stmts only
6616/// sees reads, and a suffix WRITE to a dropped declaration makes
6617/// ensure_declared re-add `Type v;` at the method top — BEFORE the
6618/// hoisted this()/super() ("对this的调用必须是构造器中的第一个语句",
6619/// lark +587 the read-only test caused).
6620fn var_occurs_in(stmts: &[Stmt], var: u32) -> bool {
6621    stmts.iter().any(|st| {
6622        let mut c = st.clone();
6623        let mut hit = false;
6624        walk_stmt_exprs(&mut c, &mut |e| {
6625            if !hit {
6626                deep_rewrite(e, &mut |x| {
6627                    if let Expr::Local { var: v, .. } = x {
6628                        if *v == var {
6629                            hit = true;
6630                        }
6631                    }
6632                });
6633            }
6634        });
6635        hit
6636    })
6637}
6638
6639// ---------------------------------------------------------------------------
6640// Branched-delegation ctors → static resolver helper.
6641// ---------------------------------------------------------------------------
6642
6643/// A synthetic static method extracted from a constructor whose
6644/// delegation is preceded by computation (the Kotlin default-arg /
6645/// conditional-bridge shape). Rendered by classdec after the real
6646/// methods; the ctor becomes a single first-statement delegation whose
6647/// carrier arg is the helper call.
6648pub struct CtorHelper {
6649    pub name: String,
6650    pub ret: JavaType,
6651    pub params: Vec<(JavaType, String)>,
6652    pub body: Stmt,
6653    pub vt: VarTable,
6654}
6655
6656fn ctor_helpers() -> &'static std::sync::Mutex<std::collections::HashMap<String, Vec<CtorHelper>>> {
6657    static H: std::sync::OnceLock<std::sync::Mutex<std::collections::HashMap<String, Vec<CtorHelper>>>> =
6658        std::sync::OnceLock::new();
6659    H.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::default()))
6660}
6661
6662/// Deterministic helper name: FNV-1a over (class, ctor descriptor) —
6663/// worker-thread registration order must not leak into the output.
6664fn helper_name(class: &str, desc: &str) -> String {
6665    let mut h: u64 = 0xcbf29ce484222325;
6666    for b in class.as_bytes().iter().chain(b"::").chain(desc.as_bytes()) {
6667        h ^= *b as u64;
6668        h = h.wrapping_mul(0x100000001b3);
6669    }
6670    format!("resolve${:x}", (h >> 16) & 0xffff_ffff)
6671}
6672
6673/// Drain the helpers registered for a class (render-time). Sorted by
6674/// name: registration order is worker-scheduling dependent.
6675pub fn take_ctor_helpers(class: &str) -> Vec<CtorHelper> {
6676    let mut m = ctor_helpers().lock().unwrap_or_else(|e| e.into_inner());
6677    let mut v = m.remove(class).unwrap_or_default();
6678    v.sort_by(|a, b| a.name.cmp(&b.name));
6679    v
6680}
6681
6682fn strip_delegations_deep(stmts: &mut Vec<Stmt>) {
6683    stmts.retain(|s| !is_bare_ctor_call(s));
6684    for s in stmts.iter_mut() {
6685        strip_delegation_one(s);
6686    }
6687}
6688
6689fn strip_delegation_one(st: &mut Stmt) {
6690    if is_bare_ctor_call(st) {
6691        *st = Stmt::Block(Vec::new());
6692        return;
6693    }
6694    match st {
6695        Stmt::Block(v) => strip_delegations_deep(v),
6696        Stmt::If { then_stmt, else_stmt, .. } => {
6697            strip_delegation_one(then_stmt);
6698            if let Some(e) = else_stmt {
6699                strip_delegation_one(e);
6700            }
6701        }
6702        Stmt::While { body, .. }
6703        | Stmt::DoWhile { body, .. }
6704        | Stmt::ForEach { body, .. }
6705        | Stmt::Synchronized { body, .. }
6706        | Stmt::Labeled { body, .. } => strip_delegation_one(body),
6707        Stmt::For { init, body, .. } => {
6708            strip_delegations_deep(init);
6709            strip_delegation_one(body);
6710        }
6711        Stmt::Switch { cases, default, .. } => {
6712            for c in cases.iter_mut() {
6713                strip_delegations_deep(&mut c.body);
6714            }
6715            if let Some(d) = default {
6716                strip_delegation_one(d);
6717            }
6718        }
6719        Stmt::Try { body, catches, finally } | Stmt::TryWithResources { body, catches, finally, .. } => {
6720            strip_delegation_one(body);
6721            for c in catches.iter_mut() {
6722                strip_delegation_one(&mut c.body);
6723            }
6724            if let Some(f) = finally {
6725                strip_delegation_one(f);
6726            }
6727        }
6728        _ => {}
6729    }
6730}
6731
6732#[allow(clippy::ptr_arg)]
6733fn rewrite_bare_returns(stmts: &mut Vec<Stmt>, val: &Expr) {
6734    for s in stmts.iter_mut() {
6735        rewrite_bare_returns_one(s, val);
6736    }
6737}
6738
6739fn rewrite_bare_returns_one(st: &mut Stmt, val: &Expr) {
6740    match st {
6741        Stmt::Return(None) => *st = Stmt::Return(Some(val.clone())),
6742        Stmt::Block(v) => rewrite_bare_returns(v, val),
6743        Stmt::If { then_stmt, else_stmt, .. } => {
6744            rewrite_bare_returns_one(then_stmt, val);
6745            if let Some(e) = else_stmt {
6746                rewrite_bare_returns_one(e, val);
6747            }
6748        }
6749        Stmt::While { body, .. }
6750        | Stmt::DoWhile { body, .. }
6751        | Stmt::ForEach { body, .. }
6752        | Stmt::Synchronized { body, .. }
6753        | Stmt::Labeled { body, .. } => rewrite_bare_returns_one(body, val),
6754        Stmt::For { init, body, .. } => {
6755            rewrite_bare_returns(init, val);
6756            rewrite_bare_returns_one(body, val);
6757        }
6758        Stmt::Switch { cases, default, .. } => {
6759            for c in cases.iter_mut() {
6760                rewrite_bare_returns(&mut c.body, val);
6761            }
6762            if let Some(d) = default {
6763                rewrite_bare_returns_one(d, val);
6764            }
6765        }
6766        Stmt::Try { body, catches, finally } | Stmt::TryWithResources { body, catches, finally, .. } => {
6767            rewrite_bare_returns_one(body, val);
6768            for c in catches.iter_mut() {
6769                rewrite_bare_returns_one(&mut c.body, val);
6770            }
6771            if let Some(f) = finally {
6772                rewrite_bare_returns_one(f, val);
6773            }
6774        }
6775        _ => {}
6776    }
6777}
6778
6779fn collect_def_vars(stmts: &[Stmt], out: &mut jdc_core::FxHashSet<u32>) {
6780    for st in stmts {
6781        match st {
6782            Stmt::LocalDef { var, .. } => {
6783                out.insert(*var);
6784            }
6785            Stmt::ExprStmt(Expr::Assign { target, .. }) => {
6786                if let Expr::Local { var, .. } = &**target {
6787                    out.insert(*var);
6788                }
6789            }
6790            Stmt::Block(v) => collect_def_vars(v, out),
6791            Stmt::If { then_stmt, else_stmt, .. } => {
6792                collect_def_vars(std::slice::from_ref(then_stmt.as_ref()), out);
6793                if let Some(e) = else_stmt {
6794                    collect_def_vars(std::slice::from_ref(e.as_ref()), out);
6795                }
6796            }
6797            Stmt::While { body, .. }
6798            | Stmt::DoWhile { body, .. }
6799            | Stmt::Synchronized { body, .. }
6800            | Stmt::Labeled { body, .. } => {
6801                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
6802            }
6803            Stmt::ForEach { var, body, .. } => {
6804                out.insert(*var);
6805                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
6806            }
6807            Stmt::For { init, body, .. } => {
6808                collect_def_vars(init, out);
6809                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
6810            }
6811            Stmt::Switch { cases, default, .. } => {
6812                for c in cases {
6813                    collect_def_vars(&c.body, out);
6814                }
6815                if let Some(d) = default {
6816                    collect_def_vars(std::slice::from_ref(d.as_ref()), out);
6817                }
6818            }
6819            Stmt::Try { body, catches, finally }
6820            | Stmt::TryWithResources { body, catches, finally, .. } => {
6821                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
6822                for c in catches {
6823                    out.insert(c.var);
6824                    collect_def_vars(std::slice::from_ref(c.body.as_ref()), out);
6825                }
6826                if let Some(f) = finally {
6827                    collect_def_vars(std::slice::from_ref(f.as_ref()), out);
6828                }
6829            }
6830            _ => {}
6831        }
6832    }
6833}
6834
6835/// v1 extraction is limited to Block/If/straight-line computation (the
6836/// Kotlin default-arg shape); loops/switch/try in the extraction bail.
6837fn extraction_shape_ok(stmts: &[Stmt]) -> bool {
6838    stmts.iter().all(|st| match st {
6839        Stmt::Block(v) => extraction_shape_ok(v),
6840        Stmt::LocalDef { .. }
6841        | Stmt::ExprStmt(_)
6842        | Stmt::Return(_)
6843        | Stmt::Throw(_)
6844        | Stmt::Assert { .. }
6845        | Stmt::Break(_)
6846        | Stmt::Continue(_) => true,
6847        Stmt::If { then_stmt, else_stmt, .. } => {
6848            ok_one(then_stmt)
6849                && else_stmt.as_deref().map(ok_one).unwrap_or(true)
6850        }
6851        Stmt::While { body, .. }
6852        | Stmt::DoWhile { body, .. }
6853        | Stmt::ForEach { body, .. }
6854        | Stmt::Synchronized { body, .. } => ok_one(body),
6855        Stmt::Labeled { body, .. } => ok_one(body),
6856        Stmt::For { init, body, .. } => extraction_shape_ok(init) && ok_one(body),
6857        Stmt::Switch { cases, default, .. } => {
6858            cases.iter().all(|c| extraction_shape_ok(&c.body))
6859                && default.as_deref().map(ok_one).unwrap_or(true)
6860        }
6861        Stmt::Try { body, catches, finally }
6862        | Stmt::TryWithResources { body, catches, finally, .. } => {
6863            ok_one(body)
6864                && catches.iter().all(|c| ok_one(&c.body))
6865                && finally.as_deref().map(ok_one).unwrap_or(true)
6866        }
6867        // Label/Goto (unstructured), MonitorEnter/Exit (raw), ClassDecl
6868        // (local class), TernaryValue, Raw: stay out of v2 extraction.
6869        _ => false,
6870    })
6871}
6872
6873fn ok_one(st: &Stmt) -> bool {
6874    extraction_shape_ok(std::slice::from_ref(st))
6875}
6876
6877fn static_safe(stmts: &[Stmt]) -> bool {
6878    let mut ok = true;
6879    for st in stmts {
6880        let mut c = st.clone();
6881        walk_stmt_exprs(&mut c, &mut |e| {
6882            deep_rewrite(e, &mut |x| {
6883                match x {
6884                    Expr::This => ok = false,
6885                    // implicit-this member access / call
6886                    Expr::Field { owner: None, is_static: false, .. } => ok = false,
6887                    Expr::Method { owner: None, is_static: false, .. } => ok = false,
6888                    _ => {}
6889                }
6890            });
6891        });
6892    }
6893    ok
6894}
6895
6896/// The branched-delegation ctor family ("对this的调用必须是构造器中的
6897/// 第一个语句", ~2k across corpora): R8 renders a Kotlin default-arg /
6898/// conditional-bridge ctor as computation statements (straight-line plus
6899/// mask-guarded overrides) with every control path ending in the SAME
6900/// `this(a, b, carrier)` delegation. Java cannot express statements
6901/// before the delegation, and unlike the linear-chain case the carrier's
6902/// value is computed by multi-statement branched code that cannot fold
6903/// into a ternary. Extract the computation into a synthetic
6904/// `private static T resolve$X(<ctor params>)` and make the ctor a
6905/// single first-statement `this(a, b, resolve$X(..))` — compilable AND
6906/// faithful (jadx renders the un-compilable statements-before-this form
6907/// for this family).
6908///
6909/// v1 gates (all conservative, bail keeps today's behavior): plain class
6910/// (enums strip trace params; member-inner ctors lose the outer param at
6911/// render), every delegation textually identical, exactly one non-param
6912/// carrier local read exactly once across the args, the primary (last
6913/// top-level) delegation exists and is not already first, computation is
6914/// Block/If/straight-line only, no `this`-dependence, and no local
6915/// defined in the extraction is used after the primary.
6916pub fn extract_branched_delegation_helper(
6917    body: &mut Stmt,
6918    vt: &VarTable,
6919    class: &crate::PoolClass,
6920    desc_str: &str,
6921) {
6922    if class.is_enum() || class.nesting.enclosing_class.is_some() {
6923        return;
6924    }
6925    let Stmt::Block(stmts) = body else { return };
6926    let mut dels: Vec<Expr> = Vec::new();
6927    for st in stmts.iter() {
6928        let mut c = st.clone();
6929        walk_stmt_exprs(&mut c, &mut |e| {
6930            if is_delegation_expr(e) {
6931                dels.push(e.clone());
6932            }
6933        });
6934    }
6935    if dels.is_empty() {
6936        return;
6937    }
6938    if dels.len() == 1 && stmts.first().is_some_and(is_bare_ctor_call) {
6939        return; // already first
6940    }
6941    if !dels.iter().all(|d| *d == dels[0]) {
6942        return;
6943    }
6944    let template = dels[0].clone();
6945    let Expr::Method { args: t_args, .. } = &template else {
6946        return;
6947    };
6948    // Exactly ONE non-param local (the carrier), read exactly once.
6949    let mut carrier: Option<u32> = None;
6950    let mut reads = 0usize;
6951    let mut multi = false;
6952    for a in t_args.iter() {
6953        let c = a.clone();
6954        visit_exprs(&c, &mut |x| {
6955            if let Expr::Local { var, .. } = x {
6956                if (*var as usize) < vt.vars.len() && !vt.vars[*var as usize].is_param {
6957                    reads += 1;
6958                    match carrier {
6959                        Some(v) if v == *var => {}
6960                        Some(_) => multi = true,
6961                        None => carrier = Some(*var),
6962                    }
6963                }
6964            }
6965        });
6966    }
6967    let Some(cv) = carrier else { return };
6968    if multi || reads != 1 {
6969        return;
6970    }
6971    let Some(l) = stmts.iter().rposition(is_bare_ctor_call) else {
6972        return;
6973    };
6974    if l == 0 {
6975        return;
6976    }
6977    let mut extracted: Vec<Stmt> = stmts[..l].to_vec();
6978    if !extraction_shape_ok(&extracted) {
6979        return;
6980    }
6981    strip_delegations_deep(&mut extracted);
6982    let cv_local = Expr::Local {
6983        var: cv,
6984        ty: vt.var(cv).ty.clone(),
6985    };
6986    rewrite_bare_returns(&mut extracted, &cv_local);
6987    extracted.push(Stmt::Return(Some(cv_local)));
6988    if !static_safe(&extracted) {
6989        return;
6990    }
6991    let mut defs: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
6992    collect_def_vars(&extracted, &mut defs);
6993    if defs.iter().any(|v| var_occurs_in(&stmts[l + 1..], *v)) {
6994        return;
6995    }
6996    let params: Vec<(u32, JavaType, String)> = vt
6997        .vars
6998        .iter()
6999        .filter(|v| v.is_param && v.name != "this")
7000        .map(|v| (v.id, v.ty.erased(), v.name.clone()))
7001        .collect();
7002    let ret = vt.var(cv).ty.erased();
7003    let name = helper_name(&class.name, desc_str);
7004    let call = Expr::Method {
7005        owner: None,
7006        cls: std::sync::Arc::from(class.name.as_str()),
7007        name: std::sync::Arc::from(name.as_str()),
7008        desc: std::sync::Arc::new(jdc_core::types::MethodDescriptor {
7009            ret: ret.clone(),
7010            args: params.iter().map(|p| p.1.clone()).collect(),
7011        }),
7012        args: params
7013            .iter()
7014            .map(|(id, ty, _)| Expr::Local {
7015                var: *id,
7016                ty: TypeRef::J(ty.clone()),
7017            })
7018            .collect(),
7019        is_static: true,
7020        is_interface: false,
7021        is_special: false,
7022        is_super: false,
7023        is_dynamic: false,
7024        type_args: Vec::new(),
7025    };
7026    let mut new_del = template.clone();
7027    deep_rewrite(&mut new_del, &mut |x| {
7028        if let Expr::Local { var, .. } = x {
7029            if *var == cv {
7030                *x = call.clone();
7031            }
7032        }
7033    });
7034    {
7035        let mut m = ctor_helpers().lock().unwrap_or_else(|e| e.into_inner());
7036        let v = m.entry(class.name.clone()).or_default();
7037        if !v.iter().any(|h| h.name == name) {
7038            // The extracted body skips the late pipeline polish (it left
7039            // the ctor before cleanup ran) — invert empty thens so the
7040            // helper doesn't render `if (..) {} else {..}`.
7041            let mut hbody = Stmt::Block(extracted);
7042            invert_empty_thens(&mut hbody);
7043            v.push(CtorHelper {
7044                name,
7045                ret,
7046                params: params.into_iter().map(|(_, ty, nm)| (ty, nm)).collect(),
7047                body: hbody,
7048                vt: vt.clone(),
7049            });
7050        } else {
7051            return; // duplicate decompile of the same ctor: keep the first
7052        }
7053    }
7054    let tail: Vec<Stmt> = stmts[l + 1..].to_vec();
7055    *stmts = std::iter::once(Stmt::ExprStmt(new_del)).chain(tail).collect();
7056}
7057
7058fn inline_ctor_arg_defs(
7059    call: &mut Stmt,
7060    prefix: &[&Stmt],
7061    vt: &VarTable,
7062    consumed: &mut Vec<(u32, usize)>,
7063) -> bool {
7064    let Stmt::ExprStmt(cexpr) = call else {
7065        return false;
7066    };
7067    // Replace every read of `v` in `e` with its value-so-far; a read
7068    // BEFORE any def (val None) is unresolvable → false.
7069    fn subst(e: &mut Expr, v: u32, val: &Option<Expr>) -> bool {
7070        let mut ok = true;
7071        deep_rewrite(e, &mut |x| {
7072            if let Expr::Local { var, .. } = x {
7073                if *var == v {
7074                    match val {
7075                        Some(e2) => *x = e2.clone(),
7076                        None => ok = false,
7077                    }
7078                }
7079            }
7080        });
7081        ok
7082    }
7083    let mut seen: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
7084    for _ in 0..32 {
7085        let mut need: Option<u32> = None;
7086        visit_exprs(cexpr, &mut |x| {
7087            if need.is_none() {
7088                if let Expr::Local { var, .. } = x {
7089                    if (*var as usize) < vt.vars.len() && !vt.vars[*var as usize].is_param {
7090                        need = Some(*var);
7091                    }
7092                }
7093            }
7094        });
7095        let Some(v) = need else { break };
7096        if !seen.insert(v) {
7097            return false; // cyclic reuse (`str = .. + str`) — bail
7098        }
7099        // CHAIN walk: fold EVERY prefix def of v into the value the
7100        // delegation actually receives. The old strict single-occurrence
7101        // rule rejected the convenience-ctor reassignment chain
7102        // (`f = getInstance(ctx); f = transform(f, ..); this(ctx, f)` —
7103        // androidx CameraController, weibo this-not-first ×831): the
7104        // SOURCE inlined the computation into the delegation args and
7105        // R8 decomposed it into register writes. Reads of v are allowed
7106        // only INSIDE its own def chain; any other prefix statement
7107        // touching v means the value escapes or the object mutates
7108        // (`stringBuilder.append(..)` — inlining a fresh `new
7109        // StringBuilder()` would lose the message), so bail.
7110        let mut val: Option<Expr> = None;
7111        let mut ok = true;
7112        for (si, st) in prefix.iter().enumerate() {
7113            match st {
7114                // A bare declaration (init None — the structurer hoists
7115                // `T v;` to the top) carries no value: skip it. Failing
7116                // here rejected every chain under a hoisted decl (ci1/a).
7117                Stmt::LocalDef { var, init: None, .. } if *var == v => {}
7118                Stmt::LocalDef { var, init, .. } if *var == v => {
7119                    let Some(e) = init else { ok = false; break };
7120                    let mut e2 = (*e).clone();
7121                    if !subst(&mut e2, v, &val) {
7122                        ok = false;
7123                        break;
7124                    }
7125                    val = Some(e2);
7126                    consumed.push((v, si));
7127                }
7128                Stmt::ExprStmt(Expr::Assign { target, op, .. })
7129                    if matches!(&**target, Expr::Local { var: tv, .. } if *tv == v) =>
7130                {
7131                    if !matches!(op, AssignOp::Plain) {
7132                        ok = false; // compound write: not a plain def
7133                        break;
7134                    }
7135                    let Stmt::ExprStmt(Expr::Assign { value, .. }) = st else {
7136                        unreachable!()
7137                    };
7138                    let mut e2 = value.as_ref().clone();
7139                    if !subst(&mut e2, v, &val) {
7140                        ok = false;
7141                        break;
7142                    }
7143                    val = Some(e2);
7144                    consumed.push((v, si));
7145                }
7146                _ => {
7147                    let mut touched = false;
7148                    visit_all_exprs(st, &mut |x| {
7149                        if let Expr::Local { var, .. } = x {
7150                            if *var == v {
7151                                touched = true;
7152                            }
7153                        }
7154                    });
7155                    if touched {
7156                        ok = false;
7157                        break;
7158                    }
7159                }
7160            }
7161        }
7162        if !ok {
7163            return false;
7164        }
7165        let Some(de) = val else { return false };
7166        if contains_this_access(&de) {
7167            return false;
7168        }
7169        // Huge defs multiply per read site per round — bound them.
7170        let mut de_nodes = 0usize;
7171        visit_exprs(&de, &mut |_| de_nodes += 1);
7172        if de_nodes > 256 {
7173            return false;
7174        }
7175        deep_rewrite(cexpr, &mut |x| {
7176            if let Expr::Local { var, .. } = x {
7177                if *var == v {
7178                    *x = de.clone();
7179                }
7180            }
7181        });
7182        // Exponential blowup guard: a var read twice doubles its def
7183        // per round, compounding across the transitive chain — cap the
7184        // expression size (a 2^k node tree overflowed the worker stack
7185        // process-wide).
7186        let mut nodes = 0usize;
7187        visit_exprs(cexpr, &mut |_| nodes += 1);
7188        if nodes > 2_048 {
7189            return false;
7190        }
7191    }
7192    true
7193}
7194
7195/// Fold a prelude StringBuilder mutation chain into the delegation
7196/// argument as a fluent chain: `StringBuilder sb = new SB(x);
7197/// sb.append(a); sb.append(b); super(sb.toString())` →
7198/// `super(new SB(x).append(a).append(b).toString())`. The generic def
7199/// inliner rejects mutation calls — their effect is not expressible as
7200/// a value — but `append` RETURNS its receiver, so the chain
7201/// reconstructs exactly the same mutations in one expression (androidx
7202/// emoji2/flatbuffer UnpairedSurrogateException family, the shapes the
7203/// strict-occurrence rule was written to refuse). Aborts on any `this`
7204/// touch inside the chain (illegal before super) or any other read of
7205/// the SB local (the value must not escape the fold). Returns the
7206/// delegation's new index (statements before it were removed).
7207fn fold_sb_chain(stmts: &mut Vec<Stmt>, pos: usize) -> usize {
7208    // 1. the SB local: first `v = new StringBuilder(..)` in the prefix.
7209    let mut sbv: Option<u32> = None;
7210    let mut def_i: Option<usize> = None;
7211    let mut def_init: Option<Expr> = None;
7212    for (i, st) in stmts[..pos].iter().enumerate() {
7213        let cand: Option<(u32, &Expr)> = match st {
7214            Stmt::LocalDef {
7215                var,
7216                init: Some(e),
7217                ..
7218            } => Some((*var, e)),
7219            Stmt::ExprStmt(Expr::Assign {
7220                target,
7221                value,
7222                op: AssignOp::Plain,
7223                ..
7224            }) => match &**target {
7225                Expr::Local { var, .. } => Some((*var, value.as_ref())),
7226                _ => None,
7227            },
7228            _ => None,
7229        };
7230        if let Some((v, init)) = cand {
7231            if let Expr::New { cls, .. } = init {
7232                if cls.as_ref() == "java/lang/StringBuilder" {
7233                    sbv = Some(v);
7234                    def_i = Some(i);
7235                    def_init = Some(init.clone());
7236                    break;
7237                }
7238            }
7239        }
7240    }
7241    let (Some(sbv), Some(def_i), Some(init)) = (sbv, def_i, def_init) else {
7242        return pos;
7243    };
7244    if contains_this_access(&init) {
7245        return pos;
7246    }
7247    // 2. append statements on sbv, in order.
7248    let mut appends: Vec<usize> = Vec::new();
7249    for (i, st) in stmts[..pos].iter().enumerate() {
7250        if let Stmt::ExprStmt(Expr::Method {
7251            owner: Some(o),
7252            cls,
7253            name,
7254            ..
7255        }) = st
7256        {
7257            if matches!(&**o, Expr::Local { var, .. } if *var == sbv)
7258                && cls.as_ref() == "java/lang/StringBuilder"
7259                && &**name == "append"
7260            {
7261                appends.push(i);
7262            }
7263        }
7264    }
7265    // 3. no read of sbv after the delegation.
7266    let mut post_read = false;
7267    for st in stmts.iter().skip(pos + 1) {
7268        visit_all_exprs(st, &mut |e| {
7269            if let Expr::Local { var, .. } = e {
7270                if *var == sbv {
7271                    post_read = true;
7272                }
7273            }
7274        });
7275    }
7276    if post_read {
7277        return pos;
7278    }
7279    // 4. prefix reads of sbv only in the def and the append receivers
7280    //    (arguments are checked for `this`, receivers are the chain).
7281    for (i, st) in stmts[..pos].iter().enumerate() {
7282        if i == def_i || appends.contains(&i) {
7283            // append ARGUMENTS must not touch this; receiver is fine.
7284            if let Stmt::ExprStmt(Expr::Method { args, .. }) = st {
7285                if args.iter().any(contains_this_access) {
7286                    return pos;
7287                }
7288            }
7289            continue;
7290        }
7291        let mut leak = false;
7292        visit_all_exprs(st, &mut |e| {
7293            if let Expr::Local { var, .. } = e {
7294                if *var == sbv {
7295                    leak = true;
7296                }
7297            }
7298        });
7299        if leak {
7300            return pos;
7301        }
7302    }
7303    // 5. the call reads sbv exactly once, as `sbv.toString()`.
7304    let mut local_reads = 0usize;
7305    let mut tostring_reads = 0usize;
7306    visit_all_exprs(&stmts[pos], &mut |e| match e {
7307        Expr::Local { var, .. } if *var == sbv => local_reads += 1,
7308        Expr::Method {
7309            owner: Some(o),
7310            name,
7311            args,
7312            ..
7313        } if args.is_empty()
7314            && &**name == "toString"
7315            && matches!(&**o, Expr::Local { var, .. } if *var == sbv) =>
7316        {
7317            tostring_reads += 1;
7318        }
7319        _ => {}
7320    });
7321    if local_reads != 1 || tostring_reads != 1 {
7322        return pos;
7323    }
7324    // 6. build the fluent chain and swap it into the toString receiver.
7325    let mut chain = init;
7326    for ai in &appends {
7327        if let Stmt::ExprStmt(Expr::Method {
7328            owner: _,
7329            cls,
7330            name,
7331            desc,
7332            args,
7333            is_static,
7334            is_interface,
7335            is_special,
7336            is_super,
7337            is_dynamic,
7338            type_args,
7339            ..
7340        }) = &stmts[*ai]
7341        {
7342            chain = Expr::Method {
7343                owner: Some(Box::new(chain)),
7344                cls: cls.clone(),
7345                name: name.clone(),
7346                desc: desc.clone(),
7347                args: args.clone(),
7348                is_static: *is_static,
7349                is_interface: *is_interface,
7350                is_special: *is_special,
7351                is_super: *is_super,
7352                is_dynamic: *is_dynamic,
7353                type_args: type_args.clone(),
7354            };
7355        }
7356    }
7357    let mut replaced = false;
7358    walk_stmt_exprs(&mut stmts[pos], &mut |e| {
7359        deep_rewrite(e, &mut |x| {
7360            if let Expr::Method {
7361                owner: Some(o),
7362                name,
7363                args,
7364                ..
7365            } = x
7366            {
7367                if args.is_empty()
7368                    && &**name == "toString"
7369                    && matches!(&**o, Expr::Local { var, .. } if *var == sbv)
7370                {
7371                    **o = chain.clone();
7372                    replaced = true;
7373                }
7374            }
7375        });
7376    });
7377    if !replaced {
7378        return pos; // never remove without the swap — atomicity
7379    }
7380    // 7. drop the consumed statements (descending), adjust pos.
7381    let mut removed: Vec<usize> = appends.clone();
7382    removed.push(def_i);
7383    removed.sort_unstable();
7384    removed.dedup();
7385    for i in removed.iter().rev() {
7386        stmts.remove(*i);
7387    }
7388    pos - removed.len()
7389}
7390
7391pub fn fix_ctor_super_first(body: &mut Stmt, vt: &VarTable) {
7392    let Stmt::Block(stmts) = body else { return };
7393    if stmts.is_empty() || is_bare_ctor_call(stmts.first().unwrap()) {
7394        return;
7395    }
7396    // Top-level delegation call.
7397    if let Some(pos0) = stmts.iter().position(is_bare_ctor_call) {
7398        let pos = fold_sb_chain(stmts, pos0);
7399        let mut call = stmts[pos].clone();
7400        let prefix: Vec<&Stmt> = stmts[..pos].iter().collect();
7401        let mut consumed: Vec<(u32, usize)> = Vec::new();
7402        if !inline_ctor_arg_defs(&mut call, &prefix, vt, &mut consumed) {
7403            return;
7404        }
7405        stmts.remove(pos);
7406        // Drop the chain defs the inline consumed (all indices < pos) —
7407        // leaving them would DOUBLE-evaluate their (possibly impure)
7408        // inits. But ONLY for vars with no post-delegation use: dropping
7409        // such a var's DECLARATION makes ensure_declared re-add it at
7410        // the method top, i.e. BEFORE this() — a fresh "对this的调用必须
7411        // 是构造器中的第一个语句" (lark +587 the blind drop caused).
7412        let mut drop_idx: Vec<usize> = consumed
7413            .iter()
7414            .filter(|(v, _)| !var_occurs_in(&stmts[pos..], *v))
7415            .map(|(_, k)| *k)
7416            .collect();
7417        drop_idx.sort_unstable();
7418        drop_idx.dedup();
7419        for k in drop_idx.iter().rev() {
7420            stmts.remove(*k);
7421        }
7422        stmts.insert(0, call);
7423        return;
7424    }
7425    // The structurer can wrap the delegation in a bare nested block —
7426    // with the parameter null-checks AHEAD of it (weixin's Kotlin
7427    // intrinsics shape: `o.h(parcel, "source"); super();` at inner
7428    // positions 0/1, flattened to straight-line by cleanup AFTER this
7429    // pass, which is why the miss surfaced as 1150 weixin "对super的
7430    // 调用必须是构造器中的第一个语句"). Hoist the call from any
7431    // position whose predecessors are all straight-line statements; a
7432    // delegation behind control flow is the conditional-super family
7433    // (needs restructuring, not hoisting) and stays put.
7434    let mut found: Option<(usize, usize)> = None;
7435    for (i, st) in stmts.iter().enumerate() {
7436        if let Stmt::Block(inner) = st {
7437            if let Some(p) = inner.iter().position(is_bare_ctor_call) {
7438                if inner[..p]
7439                    .iter()
7440                    .all(|s| matches!(s, Stmt::LocalDef { .. } | Stmt::ExprStmt(_)))
7441                {
7442                    found = Some((i, p));
7443                    break;
7444                }
7445            }
7446        }
7447    }
7448    if let Some((i, p)) = found {
7449        let mut call = match &stmts[i] {
7450            Stmt::Block(inner) => inner[p].clone(),
7451            _ => unreachable!(),
7452        };
7453        let mut prefix: Vec<&Stmt> = stmts[..i].iter().collect();
7454        if let Stmt::Block(inner) = &stmts[i] {
7455            prefix.extend(inner[..p].iter());
7456        }
7457        let mut consumed: Vec<(u32, usize)> = Vec::new();
7458        if !inline_ctor_arg_defs(&mut call, &prefix, vt, &mut consumed) {
7459            return;
7460        }
7461        // Prefix index k maps to stmts[k] for k < i, inner[k - i] for
7462        // k >= i (all such k are < p). Suffix = the rest of the inner
7463        // block + the following top-level statements; a consumed def may
7464        // only drop when its var has no suffix use (declaration loss →
7465        // ensure_declared re-adds it BEFORE this()).
7466        // Compute the droppable set BEFORE mutating (borrow split).
7467        let mut ks: Vec<usize> = Vec::new();
7468        {
7469            let inner_tail: &[Stmt] = match &stmts[i] {
7470                Stmt::Block(inner) if p < inner.len() => &inner[p + 1..],
7471                _ => &[],
7472            };
7473            for (v, k) in &consumed {
7474                if !var_occurs_in(inner_tail, *v) && !var_occurs_in(&stmts[i + 1..], *v) {
7475                    ks.push(*k);
7476                }
7477            }
7478        }
7479        ks.sort_unstable();
7480        ks.dedup();
7481        if let Stmt::Block(inner) = &mut stmts[i] {
7482            inner.remove(p);
7483            for k in ks.iter().rev().filter(|k| **k >= i) {
7484                inner.remove(k - i);
7485            }
7486        }
7487        for k in ks.iter().rev().filter(|k| **k < i) {
7488            stmts.remove(*k);
7489        }
7490        stmts.insert(0, call);
7491    }
7492}
7493
7494/// A true constructor DELEGATION statement: `super(..)` (owner None) or
7495/// `this(..)` (owner This). An un-folded `new X; <init>` (lost
7496/// allocation — the ctor-fold-bug family) arrives as a `<init>` Method
7497/// with a NON-this owner and prints as `new X(..)`: it is not a
7498/// delegation and must neither satisfy nor anchor the super-first fixes
7499/// (v7.a$a2: a dead `new f(0,..)` init at position 0 made both hoists
7500/// believe the delegation was already first, leaving the real `super`
7501/// last — "对super的调用必须是构造器中的第一个语句").
7502fn is_delegation_expr(e: &Expr) -> bool {
7503    matches!(
7504        e,
7505        Expr::Method { name, is_special: true, owner, .. }
7506            if &**name == "<init>"
7507                && (owner.is_none() || matches!(owner.as_deref(), Some(Expr::This)))
7508    )
7509}
7510
7511pub(crate) fn is_bare_ctor_call(s: &Stmt) -> bool {
7512    matches!(s, Stmt::ExprStmt(e) if is_delegation_expr(e))
7513}
7514
7515/// Is the local ever ASSIGNED (write position: `x = ..`, `++x`)?
7516/// Distinct from a read count: a ctor's synthetic outer param is only
7517/// ever read; a param that gets written must stay declared.
7518pub(crate) fn local_is_written(body: &Stmt, var: u32) -> bool {
7519    let mut hit = false;
7520    let mut c = body.clone();
7521    walk_stmt_exprs(&mut c, &mut |e| {
7522        if !hit {
7523            deep_rewrite(e, &mut |x| {
7524                if hit {
7525                    return;
7526                }
7527                match x {
7528                    Expr::Assign { target, .. }
7529                        if matches!(&**target, Expr::Local { var: v, .. } if *v == var) =>
7530                    {
7531                        hit = true;
7532                    }
7533                    Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. }
7534                        if matches!(&**e, Expr::Local { var: v, .. } if *v == var) =>
7535                    {
7536                        hit = true;
7537                    }
7538                    _ => {}
7539                }
7540            });
7541        }
7542    });
7543    hit
7544}
7545
7546/// Source-form normalization for a non-static member-inner constructor.
7547/// The dex descriptor carries the synthetic outer instance as args[0]
7548/// (typed as the direct enclosing class); the emitter's qualified
7549/// `outer.new Inner(..)` / `this.new Inner(..)` sites pass it
7550/// implicitly and `super(..)` delegations drop it, so the signature
7551/// loses the param — and the body must stop referencing it: plain uses
7552/// become `Outer.this` (a `this.this$0 = p` assignment becomes
7553/// `this.this$0 = Outer.this`, exactly the runtime value), and
7554/// this()-delegations to `eligible` classes drop the leading param arg.
7555pub(crate) fn rewrite_inner_ctor_outer_param(
7556    body: &mut Stmt,
7557    param0: u32,
7558    outer_display: &str,
7559    outer_ty: &TypeRef,
7560    eligible: &[String],
7561) {
7562    // this()/super() delegations (and lost-alloc construction calls):
7563    // the leading arg IS the synthetic outer exactly when it is the
7564    // param itself.
7565    walk_stmt_exprs(body, &mut |e| {
7566        if let Expr::Method { name, cls, args, .. } = e {
7567            if &**name == "<init>"
7568                && eligible.iter().any(|c| c.as_str() == cls.as_ref())
7569                && args.first().is_some_and(|a| matches!(a, Expr::Local { var: v, .. } if *v == param0))
7570            {
7571                args.remove(0);
7572            }
7573        }
7574    });
7575    // Every remaining read of the param becomes `Outer.this`.
7576    let text = format!("{outer_display}.this");
7577    let ty = outer_ty.clone();
7578    walk_stmt_exprs(body, &mut |e| {
7579        deep_rewrite_reads(e, &mut |x| {
7580            if let Expr::Local { var: v, .. } = x {
7581                if *v == param0 {
7582                    *x = Expr::RawT(text.clone(), ty.clone());
7583                }
7584            }
7585        });
7586    });
7587}
7588
7589/// Ctors whose delegation is buried in control flow or behind arg
7590/// computations — the "对super的调用必须是构造器中的第一个语句" family
7591/// that plain hoisting cannot reach (weixin 264 / weibo 116 / reqable 71
7592/// / lark 62 after round 60): R8/Kotlin compute super-args conditionally
7593/// and dex legally invokes super mid-branch.
7594///
7595/// Shape B (branch merge): `prelude; if (c) {d1; super(A1); t1} else
7596/// {d2; super(A2); t2}; post` → one leading `super(..)` where each
7597/// differing arg becomes `c ? a1i : a2i`; branch remainders stay an
7598/// if/else AFTER the call; prelude statements move after it (nothing
7599/// may precede super in Java; bytecode pre-super work is local-only by
7600/// construction — dex forbids instance-field reads before the delegate).
7601///
7602/// Shape A (linear): `defs; super(A); rest` where `A` references the
7603/// def locals — plain hoisting (fix_ctor_super_first case 1) would move
7604/// the call above its argument definitions (forward refs). Inline the
7605/// referenced defs into the args first, drop the consumed defs, hoist.
7606/// Runs BEFORE fix_ctor_super_first and only fires when args reference
7607/// preceding locals, leaving the battle-tested plain hoist untouched
7608/// otherwise.
7609///
7610/// Inlining duplicates the def per use site, so a side-effecting def is
7611/// inlined only when its var's use count is provably preserved: a
7612/// single use in a merged arg (1 copy), or a single use in the
7613/// condition with ≤1 differing arg (the cond then appears in the `if`
7614/// plus one ternary — 2 copies of e.g. one Kotlin getter call, which
7615/// the original bytecode typically made twice anyway: `p.u() == null ?
7616/// .. : p.u().i()`). Pure defs inline freely. Every other shape aborts:
7617/// broken-but-faithful stays the status quo, wrong evaluation counts
7618/// would be worse.
7619/// Linear `def; delegation(args read def)` shapes. A plain hoist of the
7620/// delegation past its prefix would place the call ABOVE a definition
7621/// it reads — a forward reference (`super(context2, ..)` with `context2
7622/// = ..` below it). javac's attribution for the whole class then
7623/// collapses: the undefined identifier cascades into "non-static
7624/// super" and even bare `Object` resolution failures, poisoning every
7625/// later diagnostic in the file (weibo AppCompatTextView — the root of
7626/// a 4.1k-file Object cascade). The original source had the def's
7627/// expression INLINE in the delegation args; d8 computed it into a
7628/// register first. Inline the def into the args (the def is read
7629/// exactly once there — evaluation count preserved even for calls),
7630/// and when the register is REASSIGNED later (a fresh generation
7631/// sharing the slot), convert that first write into the declaration so
7632/// the var stays defined for its later readers. Any shape that cannot
7633/// be proven aborts untouched.
7634/// Count WRITE positions of `var` (assign / inc-dec targets) — the
7635/// shape-C validator needs exact counts, not just presence.
7636fn stmts_count_writes(stmts: &[Stmt], var: u32) -> usize {
7637    let mut n = 0usize;
7638    for s in stmts {
7639        let mut c = s.clone();
7640        walk_stmt_exprs(&mut c, &mut |e| {
7641            deep_rewrite(e, &mut |x| match x {
7642                Expr::Assign { target, .. }
7643                    if matches!(&**target, Expr::Local { var: v, .. } if *v == var) =>
7644                {
7645                    n += 1;
7646                }
7647                Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. }
7648                    if matches!(&**e, Expr::Local { var: v, .. } if *v == var) =>
7649                {
7650                    n += 1;
7651                }
7652                _ => {}
7653            });
7654        });
7655    }
7656    n
7657}
7658
7659/// Collect every local WRITTEN in `stmts` — the complement inside a
7660/// ctor prefix is the param set (the only locals a pre-delegation arg
7661/// expression may read).
7662fn prefix_written_vars(stmts: &[Stmt]) -> jdc_core::FxHashSet<u32> {
7663    let mut set = jdc_core::FxHashSet::default();
7664    for s in stmts {
7665        let mut c = s.clone();
7666        walk_stmt_exprs(&mut c, &mut |e| {
7667            deep_rewrite(e, &mut |x| match x {
7668                Expr::Assign { target, .. } => {
7669                    if let Expr::Local { var, .. } = &**target {
7670                        set.insert(*var);
7671                    }
7672                }
7673                Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => {
7674                    if let Expr::Local { var, .. } = &**e {
7675                        set.insert(*var);
7676                    }
7677                }
7678                _ => {}
7679            });
7680        });
7681        if let Stmt::LocalDef { var, .. } = s {
7682            set.insert(*var);
7683        }
7684    }
7685    set
7686}
7687
7688/// Is `e` safe to evaluate INSIDE a ctor-delegation argument (before
7689/// this/super exists)? Params/consts/pure ops only — no instance-field
7690/// reads, no `this`, no captures.
7691fn pre_this_safe(e: &Expr, prefix_vars: &jdc_core::FxHashSet<u32>) -> bool {
7692    match e {
7693        Expr::Local { var, .. } => !prefix_vars.contains(var),
7694        Expr::Const(_) => true,
7695        Expr::Cast { e, .. } => pre_this_safe(e, prefix_vars),
7696        Expr::Bin { l, r, .. } => {
7697            pre_this_safe(l, prefix_vars) && pre_this_safe(r, prefix_vars)
7698        }
7699        Expr::Cond { c, t, f } => {
7700            pre_this_safe(c, prefix_vars)
7701                && pre_this_safe(t, prefix_vars)
7702                && pre_this_safe(f, prefix_vars)
7703        }
7704        Expr::Field { owner, is_static, .. } => {
7705            *is_static
7706                && owner
7707                    .as_ref()
7708                    .is_none_or(|o| pre_this_safe(o, prefix_vars))
7709        }
7710        Expr::Method {
7711            owner,
7712            args,
7713            is_static,
7714            ..
7715        } => {
7716            args.iter().all(|a| pre_this_safe(a, prefix_vars))
7717                && match owner {
7718                    Some(o) => pre_this_safe(o, prefix_vars),
7719                    None => *is_static,
7720                }
7721        }
7722        // Allocation in a delegation arg is legal Java before this()
7723        // (the ctor call touches no instance state of THIS class) — the
7724        // missing arm sent every `new Date()`-style Kotlin default
7725        // through `_ => false`, rejecting the whole fold (lark
7726        // Request$AppToApp this-not-first family, ~650 lark lines).
7727        // An inner-class construction needing an outer `this` carries
7728        // Expr::This in its args, which still fails below.
7729        Expr::New { args, .. } => args.iter().all(|a| pre_this_safe(a, prefix_vars)),
7730        Expr::NewArray { dims, init, .. } => {
7731            dims.iter().all(|d| pre_this_safe(d, prefix_vars))
7732                && init
7733                    .as_ref()
7734                    .map(|l| l.iter().all(|x| pre_this_safe(x, prefix_vars)))
7735                    .unwrap_or(true)
7736        }
7737        // Pure unary/instanceof/array reads.
7738        Expr::Un { e, .. } | Expr::InstanceOf { e, .. } => pre_this_safe(e, prefix_vars),
7739        Expr::ArrayIndex { array, index } => {
7740            pre_this_safe(array, prefix_vars) && pre_this_safe(index, prefix_vars)
7741        }
7742        _ => false,
7743    }
7744}
7745
7746/// `s` is exactly one plain assignment to local `v` (optionally wrapped
7747/// in a singleton Block): the branch shape of a Kotlin default-arg
7748/// override.
7749fn single_assign_to_v(s: &Stmt, v: u32) -> Option<Expr> {
7750    match s {
7751        Stmt::ExprStmt(Expr::Assign {
7752            target,
7753            op: AssignOp::Plain,
7754            value,
7755        }) if matches!(&**target, Expr::Local { var: vv, .. } if *vv == v) => {
7756            Some((**value).clone())
7757        }
7758        Stmt::Block(xs) if xs.len() == 1 => single_assign_to_v(&xs[0], v),
7759        _ => None,
7760    }
7761}
7762
7763/// Kotlin default-argument BRIDGE ctors (shape C of the delegation
7764/// family): `<init>(params.., int mask, DefaultConstructorMarker)`
7765/// computes each defaulted arg into a local — `v = p; if ((mask&bit)
7766/// != 0) { v = DEFAULT; }` — then delegates `this(.., v, ..)`. Java
7767/// allows NOTHING before the delegation, so the computations must fold
7768/// INTO the args (`this(.., (mask&bit) != 0 ? DEFAULT : p, ..)`).
7769/// Unfolded, the hoist passes lift the delegation above the raw local
7770/// reads: "找不到符号 变量 str2/v4/creationExtras2" — 2.5k root files
7771/// across weixin/lark/weibo (weixin MvvmObserverOwner$..Observer, lark
7772/// SimpleArrayMap/LongSparseArray). Per-var provability: exactly one
7773/// top-level base write, an optional single conditional override whose
7774/// branches are lone assignments, no other prefix touch of the var,
7775/// nothing after the delegation touches it, and the folded expression
7776/// is pre-this-safe. Unprovable vars stay untouched (partial folds are
7777/// strictly better — the rest was broken before too).
7778/// Flatten nested plain Blocks among the top-level statements. The
7779/// structurer hands ctor bodies as region-nested blocks (`Block([def,
7780/// Block([assign, if]), Block([delegate, return])])` — the Kotlin
7781/// bridge shape), and every delegation-repair pass scans the TOP level
7782/// only. Straight-line nesting flattens without semantics; control
7783/// flow lives inside If/While/etc. arms, never as a direct member.
7784pub(crate) fn flatten_top_blocks(stmts: &mut Vec<Stmt>) {
7785    fn rec(s: Stmt, out: &mut Vec<Stmt>) {
7786        match s {
7787            Stmt::Block(inner) => {
7788                for x in inner {
7789                    rec(x, out);
7790                }
7791            }
7792            other => out.push(other),
7793        }
7794    }
7795    if !stmts.iter().any(|s| matches!(s, Stmt::Block(_))) {
7796        return;
7797    }
7798    let mut out: Vec<Stmt> = Vec::with_capacity(stmts.len());
7799    for s in std::mem::take(stmts) {
7800        rec(s, &mut out);
7801    }
7802    *stmts = out;
7803}
7804
7805fn fold_default_arg_bridge(stmts: &mut Vec<Stmt>) -> bool {
7806    flatten_top_blocks(stmts);
7807    let Some(pos) = stmts.iter().position(is_bare_ctor_call) else {
7808        return false;
7809    };
7810    if pos == 0 {
7811        return false;
7812    }
7813    let Stmt::ExprStmt(Expr::Method { args, .. }) = &stmts[pos] else {
7814        return false;
7815    };
7816    let mut arg_vars: Vec<u32> = Vec::new();
7817    let mut reads = 0usize;
7818    for a in args {
7819        let mut c = a.clone();
7820        deep_rewrite_reads(&mut c, &mut |x| {
7821            if let Expr::Local { var, .. } = x {
7822                reads += 1;
7823                if !arg_vars.contains(var) {
7824                    arg_vars.push(*var);
7825                }
7826            }
7827        });
7828    }
7829    if arg_vars.is_empty() || reads != arg_vars.len() {
7830        return false; // a var read twice would duplicate evaluation
7831    }
7832    // The bridge ENDS at the delegation: nothing after may touch the
7833    // arg vars (reads OR writes).
7834    for &v in &arg_vars {
7835        if stmts_read_var(&stmts[pos + 1..], v) != 0
7836            || stmts_count_writes(&stmts[pos + 1..], v) != 0
7837        {
7838            return false;
7839        }
7840    }
7841    let prefix_vars = prefix_written_vars(&stmts[..pos]);
7842    let mut repl: Vec<(u32, Expr)> = Vec::new();
7843    let mut drop: Vec<usize> = Vec::new();
7844    for &v in &arg_vars {
7845        // Exact prefix shape for v.
7846        let mut base: Option<(usize, Expr)> = None;
7847        let mut decl: Option<usize> = None;
7848        let mut condf: Option<(usize, Expr, Option<Expr>, Option<Expr>)> = None;
7849        let mut ok = true;
7850        for (i, s) in stmts[..pos].iter().enumerate() {
7851            match s {
7852                Stmt::LocalDef { var, init, .. } if *var == v => match init {
7853                    None => {
7854                        if decl.is_some() {
7855                            ok = false;
7856                            break;
7857                        }
7858                        decl = Some(i);
7859                    }
7860                    Some(e) => {
7861                        if base.is_some() {
7862                            ok = false;
7863                            break;
7864                        }
7865                        base = Some((i, e.clone()));
7866                    }
7867                },
7868                Stmt::ExprStmt(Expr::Assign {
7869                    target,
7870                    op: AssignOp::Plain,
7871                    value,
7872                }) if matches!(&**target, Expr::Local { var: vv, .. } if *vv == v) => {
7873                    if base.is_some() {
7874                        ok = false;
7875                        break;
7876                    }
7877                    base = Some((i, (**value).clone()));
7878                }
7879                Stmt::If {
7880                    cond,
7881                    then_stmt,
7882                    else_stmt,
7883                } => {
7884                    let t = single_assign_to_v(then_stmt, v);
7885                    let f = else_stmt.as_ref().and_then(|b| single_assign_to_v(b, v));
7886                    let then_touches = t.is_some()
7887                        || stmts_read_var(std::slice::from_ref(&**then_stmt), v) != 0
7888                        || stmts_count_writes(std::slice::from_ref(&**then_stmt), v) != 0;
7889                    let else_touches = else_stmt.as_ref().is_some_and(|b| {
7890                        f.is_some()
7891                            || stmts_read_var(std::slice::from_ref(b), v) != 0
7892                            || stmts_count_writes(std::slice::from_ref(b), v) != 0
7893                    });
7894                    if !then_touches && !else_touches {
7895                        continue; // unrelated if
7896                    }
7897                    // Both branches must be lone assignments (a branch
7898                    // with its own delegation/return is the conditional-
7899                    // super machinery's shape, not this one), the cond
7900                    // must not read v, and only one override per var.
7901                    if condf.is_some()
7902                        || base.is_none()
7903                        || (then_touches && t.is_none())
7904                        || (else_touches && f.is_none())
7905                    {
7906                        ok = false;
7907                        break;
7908                    }
7909                    let cond_stmt = Stmt::ExprStmt(cond.clone());
7910                    if stmts_read_var(std::slice::from_ref(&cond_stmt), v) != 0 {
7911                        ok = false;
7912                        break;
7913                    }
7914                    condf = Some((i, cond.clone(), t, f));
7915                }
7916                _ => {}
7917            }
7918        }
7919        if !ok {
7920            continue;
7921        }
7922        let Some((bi, be)) = base else { continue };
7923        // Exact write/read accounting: base (+then/else) writes, zero
7924        // prefix reads.
7925        let want_writes = 1
7926            + condf
7927                .as_ref()
7928                .map(|(_, _, t, f)| t.is_some() as usize + f.is_some() as usize)
7929                .unwrap_or(0);
7930        if stmts_count_writes(&stmts[..pos], v) != want_writes
7931            || stmts_read_var(&stmts[..pos], v) != 0
7932        {
7933            continue;
7934        }
7935        let rep = match condf {
7936            Some((ci, c, t, f)) => {
7937                drop.push(ci);
7938                match (t, f) {
7939                    (Some(t), Some(f)) => Expr::Cond {
7940                        c: Box::new(c),
7941                        t: Box::new(t),
7942                        f: Box::new(f),
7943                    },
7944                    (Some(t), None) => Expr::Cond {
7945                        c: Box::new(c),
7946                        t: Box::new(t),
7947                        f: Box::new(be.clone()),
7948                    },
7949                    (None, Some(f)) => Expr::Cond {
7950                        c: Box::new(c),
7951                        t: Box::new(be.clone()),
7952                        f: Box::new(f),
7953                    },
7954                    (None, None) => unreachable!("condf requires a branch assign"),
7955                }
7956            }
7957            None => be.clone(),
7958        };
7959        if !pre_this_safe(&rep, &prefix_vars) {
7960            continue;
7961        }
7962        drop.push(bi);
7963        if let Some(d) = decl {
7964            drop.push(d);
7965        }
7966        repl.push((v, rep));
7967    }
7968    if repl.is_empty() {
7969        return false;
7970    }
7971    if let Stmt::ExprStmt(Expr::Method { args, .. }) = &mut stmts[pos] {
7972        for a in args.iter_mut() {
7973            deep_rewrite_reads(a, &mut |x| {
7974                if let Expr::Local { var, .. } = x {
7975                    if let Some((_, e)) = repl.iter().find(|(vv, _)| vv == var) {
7976                        *x = e.clone();
7977                    }
7978                }
7979            });
7980        }
7981    }
7982    drop.sort_unstable();
7983    drop.dedup();
7984    for i in drop.into_iter().rev() {
7985        stmts.remove(i);
7986    }
7987    true
7988}
7989
7990/// Rewrite static member references on Kotlin multi-file facade PARTS
7991/// (`StringsKt__StringsKt.trim(..)`) to the public facade the parts are
7992/// compiled into (`StringsKt.trim(..)`): the parts are package-private,
7993/// so every cross-package call site is "在kotlin.text中不是公共的; 无法
7994/// 从外部程序包中对其进行访问" (lark 1.4k root files; weibo's 8.4k-line
7995/// Kt cluster). Static members resolve through the facade by
7996/// inheritance (the map is gated on the extends chain).
7997pub fn rewrite_kotlin_facades(body: &mut Stmt, pool: &crate::DexPool) {
7998    let map = pool.kotlin_facade_map();
7999    if map.is_empty() {
8000        return;
8001    }
8002    walk_stmt_exprs(body, &mut |e| {
8003        match e {
8004            Expr::Method {
8005                cls,
8006                name,
8007                desc,
8008                is_static,
8009                ..
8010            } if *is_static => {
8011                if let Some((base, cover)) = map.get(cls.as_ref()) {
8012                    let hit = match cover {
8013                        None => true,
8014                        Some(c) => c.0.contains(&(name.to_string(), desc.to_string())),
8015                    };
8016                    if hit {
8017                        *cls = std::sync::Arc::from(base.as_str());
8018                    }
8019                }
8020            }
8021            Expr::Field {
8022                cls,
8023                name,
8024                is_static,
8025                ..
8026            } if *is_static => {
8027                if let Some((base, cover)) = map.get(cls.as_ref()) {
8028                    let hit = match cover {
8029                        None => true,
8030                        Some(c) => c.1.contains(name.as_ref()),
8031                    };
8032                    if hit {
8033                        *cls = std::sync::Arc::from(base.as_str());
8034                    }
8035                }
8036            }
8037            _ => {}
8038        }
8039    });
8040}
8041
8042/// Kotlin default-arg bridge fold for ENUM ctors: the synthetic
8043/// `(String, int, .., int, DefaultConstructorMarker)` ctor computes
8044/// defaulted args then chains `this(..)` mid-body — Java requires the
8045/// delegation first (对this的调用必须是构造器中的第一个语句, lark's
8046/// UserCustomStatusExtraParams$* family, 752 lines). Same fold the
8047/// class-ctor path runs; the enum branch of the pipeline used to skip
8048/// it (only strip_enum_ctor_super ran there).
8049pub fn fold_enum_default_arg_bridge(body: &mut Stmt) {
8050    let Stmt::Block(stmts) = body else { return };
8051    fold_default_arg_bridge(stmts);
8052}
8053
8054pub fn fix_ctor_delegation_arg_defs(body: &mut Stmt) {
8055    let Stmt::Block(stmts) = body else { return };
8056    // Shape C first: the Kotlin default-arg bridge (computations +
8057    // conditional overrides folded into the delegation args).
8058    fold_default_arg_bridge(stmts);
8059    // Only the top-level linear shape; the first statement needs no
8060    // repair and control flow ahead of the delegation belongs to the
8061    // conditional-super machinery.
8062    let Some(pos) = stmts.iter().position(is_bare_ctor_call) else {
8063        return;
8064    };
8065    if pos == 0 {
8066        return;
8067    }
8068    // Vars read by the delegation args, and the total read count (a var
8069    // read twice would duplicate a call evaluation on inline).
8070    let mut arg_vars: Vec<u32> = Vec::new();
8071    let mut total_reads = 0usize;
8072    {
8073        let Stmt::ExprStmt(Expr::Method { args, .. }) = &stmts[pos] else {
8074            return;
8075        };
8076        for a in args {
8077            let mut c = a.clone();
8078            deep_rewrite_reads(&mut c, &mut |x| {
8079                if let Expr::Local { var: v, .. } = x {
8080                    total_reads += 1;
8081                    if !arg_vars.contains(v) {
8082                        arg_vars.push(*v);
8083                    }
8084                }
8085            });
8086        }
8087    }
8088    if arg_vars.is_empty() || total_reads != arg_vars.len() {
8089        return;
8090    }
8091    // Vars DEFINED anywhere in the prefix (for the init-referenced
8092    // check below — an inlined init may not itself read prefix defs).
8093    let prefix_defs: Vec<u32> = stmts[..pos]
8094        .iter()
8095        .filter_map(|s| match s {
8096            Stmt::LocalDef { var, init: Some(_), .. } => Some(*var),
8097            _ => None,
8098        })
8099        .collect();
8100    // Per referenced var: exactly one prefix def, no other prefix
8101    // reader between def and delegation, the def's init reads no
8102    // prefix-def var, and the first post-delegation use is a top-level
8103    // write (becomes the declaration) or nothing at all.
8104    let mut plan: Vec<(u32, usize, Option<usize>)> = Vec::new();
8105    for v in arg_vars {
8106        let defs: Vec<usize> = stmts[..pos]
8107            .iter()
8108            .enumerate()
8109            .filter(|(i, _)| {
8110                matches!(&stmts[*i], Stmt::LocalDef { var, init: Some(_), .. } if *var == v)
8111            })
8112            .map(|(i, _)| i)
8113            .collect();
8114        if defs.len() > 1 {
8115            return; // ambiguous: multiple prefix defs
8116        }
8117        let Some(&q) = defs.first() else {
8118            continue; // a param: nothing to inline for this var
8119        };
8120        let init = match &stmts[q] {
8121            Stmt::LocalDef { init: Some(e), .. } => e.clone(),
8122            _ => return,
8123        };
8124        if stmts_read_var(&stmts[q + 1..pos], v) != 0 {
8125            return; // another reader between the def and the delegation
8126        }
8127        // The inlined init may not read a prefix-def var either — the
8128        // delegation lands ABOVE those defs after the hoist.
8129        let mut init_vars: Vec<u32> = Vec::new();
8130        deep_rewrite_reads(&mut { init.clone() }, &mut |x| {
8131            if let Expr::Local { var: vv, .. } = x {
8132                if !init_vars.contains(vv) {
8133                    init_vars.push(*vv);
8134                }
8135            }
8136        });
8137        if init_vars.iter().any(|iv| prefix_defs.contains(iv)) {
8138            return;
8139        }
8140        match stmts[pos + 1..]
8141            .iter()
8142            .position(|s| {
8143                matches!(s, Stmt::ExprStmt(Expr::Assign { target, .. })
8144                    if matches!(&**target, Expr::Local { var: vv, .. } if *vv == v))
8145            }) {
8146            Some(w) => {
8147                // Reads before the re-declaration would be dangling.
8148                if stmts_read_var(&stmts[pos + 1..pos + 1 + w], v) != 0 {
8149                    return;
8150                }
8151                plan.push((v, q, Some(pos + 1 + w)));
8152            }
8153            None => {
8154                // No later write: the def must be dead after inline.
8155                if stmts_read_var(&stmts[pos + 1..], v) != 0 {
8156                    return;
8157                }
8158                plan.push((v, q, None));
8159            }
8160        }
8161    }
8162    if plan.is_empty() {
8163        return;
8164    }
8165    // Inline the def inits into the delegation args.
8166    let values: Vec<(u32, Expr)> = plan
8167        .iter()
8168        .map(|(_v, q, _)| match &stmts[*q] {
8169            Stmt::LocalDef { var, init: Some(e), .. } => (*var, e.clone()),
8170            _ => unreachable!("plan entries carry a LocalDef with init"),
8171        })
8172        .collect();
8173    if let Stmt::ExprStmt(Expr::Method { args, .. }) = &mut stmts[pos] {
8174        for a in args {
8175            deep_rewrite_reads(a, &mut |x| {
8176                if let Expr::Local { var: v, .. } = x {
8177                    if let Some((_, e)) = values.iter().find(|(vv, _)| vv == v) {
8178                        *x = e.clone();
8179                    }
8180                }
8181            });
8182        }
8183    }
8184    // Convert the first writes into declarations BEFORE removing defs
8185    // (removals shift indices); then remove the defs in descending
8186    // order.
8187    for (v, _, w) in &plan {
8188        if let Some(w) = w {
8189            if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = &mut stmts[*w] {
8190                if matches!(&**target, Expr::Local { var: vv, .. } if vv == v) {
8191                    let taken = std::mem::replace(&mut **value, Expr::Const(ConstVal::Null));
8192                    stmts[*w] = Stmt::LocalDef {
8193                        var: *v,
8194                        init: Some(taken),
8195                        is_final: false,
8196                        force_type: false,
8197                    };
8198                }
8199            }
8200        }
8201    }
8202    let mut order: Vec<usize> = plan.iter().map(|(_, q, _)| *q).collect();
8203    order.sort_unstable();
8204    order.reverse();
8205    for q in order {
8206        stmts.remove(q);
8207    }
8208}
8209
8210/// An INT-typed method returning a boolean value: the register held the
8211/// boolean test result in one generation and the `if (v) 1 else 0` int
8212/// in another; booleanize picks the boolean generation and the plain
8213/// `return v;` breaks ("boolean无法转换为int", weixin Handle
8214/// onPausableTransaction — the source was `if (v) 1 else 0`). Wrap the
8215/// return in the recovering conditional.
8216/// Mirror of fix_int_returns: a BOOLEAN method returning a numeric-
8217/// typed value (register reuse left an int generation) wraps it
8218/// `x != 0` — the dex branch encoding, and the only legal Java form
8219/// (kh5/a `return v116_g53;` int无法转换为boolean family).
8220pub fn fix_bool_returns(vt: &VarTable, body: &mut Stmt) {
8221    walk_mut_deep(body, &mut |st| {
8222        if let Stmt::Return(Some(e)) = st {
8223            let numeric = match &*e {
8224                Expr::Local { var, ty } => {
8225                    let t = if (*var as usize) < vt.vars.len() {
8226                        vt.var(*var).ty.erased()
8227                    } else {
8228                        ty.erased()
8229                    };
8230                    matches!(
8231                        t,
8232                        JavaType::Int
8233                            | JavaType::Long
8234                            | JavaType::Short
8235                            | JavaType::Byte
8236                            | JavaType::Char
8237                    )
8238                }
8239                other => matches!(
8240                    other.type_ref().erased(),
8241                    JavaType::Int | JavaType::Long | JavaType::Short | JavaType::Byte
8242                        | JavaType::Char
8243                ),
8244            };
8245            if numeric {
8246                let taken = e.clone();
8247                *e = Expr::Bin {
8248                    op: BinOp::Ne,
8249                    l: Box::new(taken),
8250                    r: Box::new(Expr::Const(ConstVal::Int(0))),
8251                    ty: Some(TypeRef::J(JavaType::Boolean)),
8252                };
8253            }
8254        }
8255    });
8256}
8257
8258pub fn fix_int_returns(vt: &VarTable, body: &mut Stmt) {
8259    walk_mut_deep(body, &mut |st| {
8260        if let Stmt::Return(Some(e)) = st {
8261            let is_bool = match e {
8262                Expr::Local { var, .. } => {
8263                    (*var as usize) < vt.vars.len()
8264                        && vt.vars[*var as usize].ty.erased() == JavaType::Boolean
8265                }
8266                Expr::Method { desc, .. } => desc.ret == JavaType::Boolean,
8267                _ => false,
8268            };
8269            if is_bool {
8270                *e = Expr::Cond {
8271                    c: Box::new(e.clone()),
8272                    t: Box::new(Expr::Const(ConstVal::Int(1))),
8273                    f: Box::new(Expr::Const(ConstVal::Int(0))),
8274                };
8275            }
8276        }
8277    });
8278}
8279
8280/// Generation splitting: when an assignment's value type is
8281/// INCOMPATIBLE with the var's declared type, mint a FRESH var for the
8282/// new generation instead of widening (widening was measured twice as
8283/// net-negative — every use-site cast gap re-triggers javac's
8284/// error-type contagion). Reads after the split point follow the
8285/// active generation; control-flow joins intersect; loops run a
8286/// 3-round fixpoint so the back-edge carries the tail generation to
8287/// the head reads (the loop-state-tuple shape, qf5/v0's
8288/// `u0 = (qf5.u0) get;` … `u0 = looper2;`).
8289///
8290/// Split condition (reference incompatibility or a primitive/value
8291/// mix — the instanceof-corruption shape): both specific references of
8292/// different classes, or prim→ref. NOT numeric-vs-numeric (promotion
8293/// handles those) and NOT Object targets (assignable — the narrowing
8294/// cast pass covers the value side).
8295pub fn split_generations(vt: &mut VarTable, body: &mut Stmt, pool: &DexPool) {
8296    let mut counter: u32 = 0;
8297    let mut gen: std::collections::HashMap<u32, u32> = std::collections::HashMap::default();
8298    split_walk_stmt(body, vt, &mut gen, &mut counter, pool);
8299}
8300
8301/// Should the assignment `target = value` split the target?
8302fn split_needed(vt: &VarTable, target: u32, value: &Expr, pool: &DexPool) -> bool {
8303    let n = vt.vars.len();
8304    if target as usize >= n {
8305        return false;
8306    }
8307    let declared = vt.vars[target as usize].ty.erased();
8308    let vt_is_ref = |t: &JavaType| matches!(t, JavaType::Object(_) | JavaType::Array(_));
8309    // Locals consult the VarTable: the embedded ty is a lift-time
8310    // snapshot, stale after booleanize converts a copy SOURCE (the
8311    // post-booleanize re-split below relies on seeing `v131` as the
8312    // boolean it became). Bitwise Or/And/Xor over boolean operands
8313    // carry the same staleness at the EXPRESSION level (dex lowered
8314    // Kotlin's non-short-circuit `or` to int `|` — the embedded Bin ty
8315    // stays Int after booleanize converts the operands): `int v18 =
8316    // delete | delete2` must split the target generation.
8317    fn side_bool(e: &Expr, vt: &VarTable) -> bool {
8318        match e {
8319            Expr::Local { var, .. } if (*var as usize) < vt.vars.len() => {
8320                matches!(vt.var(*var).ty.erased(), JavaType::Boolean)
8321            }
8322            Expr::Const(ConstVal::Int(0 | 1)) => true,
8323            Expr::Bin { op: BinOp::Or | BinOp::And | BinOp::Xor, l, r, .. } => {
8324                side_bool(l, vt) && side_bool(r, vt)
8325            }
8326            other => matches!(other.type_ref().erased(), JavaType::Boolean),
8327        }
8328    }
8329    let value_ty = match value {
8330        Expr::Local { var, ty } if (*var as usize) < vt.vars.len() => vt.var(*var).ty.erased(),
8331        Expr::Bin {
8332            op: BinOp::Or | BinOp::And | BinOp::Xor,
8333            l,
8334            r,
8335            ..
8336        } if side_bool(l, vt) && side_bool(r, vt) => JavaType::Boolean,
8337        other => other.type_ref().erased(),
8338    };
8339    if vt_is_ref(&declared) && vt_is_ref(&value_ty) {
8340        // Both references: split only when incompatible — different
8341        // classes where neither side is Object AND the value is not a
8342        // SUBTYPE of the declared type. An assignable store needs no
8343        // fresh generation; splitting one minted an orphan at an
8344        // if-join whose sibling arm kept the base (rename_gen_back's
8345        // primitive-only gate refused the undo, drop_dead_locals then
8346        // stripped the orphan to a bare allocation): pq0/g's bridge
8347        // `map2 = new LinkedHashMap()` against the HashMap-typed phi
8348        // rendered `new LinkedHashMap();` and the arm lost its store.
8349        let obj = JavaType::Object("java/lang/Object".into());
8350        if declared == value_ty || declared == obj || value_ty == obj {
8351            return false;
8352        }
8353        // A FRESH ALLOCATION whose class relation to the declared type
8354        // is UNKNOWABLE (a framework↔framework pair — LinkedHashMap
8355        // into a HashMap-typed phi): the dex stored the instance into
8356        // this register flow, so the store was verifier-legal; the
8357        // split cannot know better and orphaned the generation (the
8358        // if-join undo is primitive-only; the dropper stripped the
8359        // orphan to a bare `new LinkedHashMap();` and pq0/g's bridge
8360        // lost the else-arm store, killing the whole branched-super
8361        // merge). Pool-known pairs KEEP the split: the generation's
8362        // precise type is what downstream subtype sinks need (weibo
8363        // `v7 = new $reportWhenComplete$1(..)` into a ContinuationImpl
8364        // phi — exempting it made `v9($1) = v7` inconvertible, +50).
8365        if let (JavaType::Object(v), JavaType::Object(d)) = (&value_ty, &declared) {
8366            if matches!(value, Expr::New { .. })
8367                && (pool.get(v).is_none() || pool.get(d).is_none())
8368            {
8369                return false;
8370            }
8371        }
8372        true
8373    } else {
8374        // Primitive/reference mix: the value generation has a kind the
8375        // target can never accept — split (the declared type stays with
8376        // the old generation's readers).
8377        if vt_is_ref(&declared) != vt_is_ref(&value_ty) {
8378            return true;
8379        }
8380        // Boolean/numeric mix: `boolean` and `int` have NO cast between
8381        // them in Java, so a register reused across the kinds can only
8382        // be expressed as two generations (weixin ConstraintLayout
8383        // `boolean v60; int v56; v60 = v56;` — 9.5k bool↔int lines once
8384        // the obscuring suppression lifted). 0/1 constants are the
8385        // boolean encoding, not a numeric generation — never split on
8386        // those (booleanize owns them).
8387        let d_bool = matches!(declared, JavaType::Boolean);
8388        let v_bool = matches!(value_ty, JavaType::Boolean);
8389        let zero_one = matches!(value, Expr::Const(ConstVal::Int(0 | 1)));
8390        d_bool != v_bool && !zero_one
8391    }
8392}
8393
8394fn rewrite_gen_reads(e: &mut Expr, gen: &std::collections::HashMap<u32, u32>, vt: &VarTable) {
8395    deep_rewrite(e, &mut |x| {
8396        if let Expr::Local { var, ty } = x {
8397            if let Some(&to) = gen.get(var) {
8398                if to != *var {
8399                    *var = to;
8400                    *ty = vt.var(to).ty.clone();
8401                }
8402            }
8403        }
8404    });
8405}
8406
8407fn do_split(
8408    vt: &mut VarTable,
8409    target: &mut Expr,
8410    old: u32,
8411    value_ty: &JavaType,
8412    gen: &mut std::collections::HashMap<u32, u32>,
8413    counter: &mut u32,
8414) {
8415    *counter += 1;
8416    let info = &vt.vars[old as usize];
8417    let id = vt.vars.len() as u32;
8418    let name = format!("{}_g{}", info.name, counter);
8419    let slot = info.slot;
8420    // A split generation is a fresh LOCAL, not a parameter — even when
8421    // the parent register is one (the param identity stays on the
8422    // original var id; the generation only carries a later write). The
8423    // inherited flag made booleanize (and every other param-skip pass)
8424    // permanently ignore param-register generations: weixin v2/i's
8425    // `int v264_g160_g7 = !v200` with `!= 0` reads (bool→int assign
8426    // family, 2.8k lines).
8427    let is_param = false;
8428    vt.vars.push(jdc_core::var::VarInfo {
8429        id,
8430        slot,
8431        name,
8432        ty: TypeRef::J(value_ty.clone()),
8433        is_param,
8434        range_start: 0,
8435        range_end: u16::MAX,
8436        synthetic_name: true,
8437    });
8438    while vt.by_slot.len() <= slot as usize {
8439        vt.by_slot.push(Vec::new());
8440    }
8441    vt.by_slot[slot as usize].push((0, u16::MAX, id));
8442    gen.insert(old, id);
8443    if let Expr::Local { var, ty, .. } = target {
8444        *var = id;
8445        *ty = TypeRef::J(value_ty.clone());
8446    }
8447}
8448
8449/// Undo a DIVERGED generation split: rewrite every occurrence of the
8450/// freshly-minted generation `from` back to the pre-if var `to` within
8451/// `s` (both read positions and assignment targets, plus LocalDef var
8452/// fields). At a control-flow join only splits BOTH branches produced
8453/// survive (the phi merges on the shared var); a gen one branch minted
8454/// alone does not. But `do_split` already rewrote that branch's write
8455/// target IN PLACE, so without this undo the branch stores into a
8456/// write-only orphan gen that no confluence reader ever reads — the
8457/// value is silently lost AND the orphan renders undeclared (kt5.b
8458/// Kotlin default-arg bridge: `v18_g1 = p6x` in then vs `v18 = false`
8459/// in else, `this(.., v18, ..)` reads only else's value).
8460fn rename_gen_back(s: &mut Stmt, from: u32, to: u32, vt: &VarTable) {
8461    if from == to || (to as usize) >= vt.vars.len() {
8462        return;
8463    }
8464    let to_ty = vt.vars[to as usize].ty.clone();
8465    walk_mut_deep(s, &mut |st| {
8466        if let Stmt::LocalDef { var, .. } = st {
8467            if *var == from {
8468                *var = to;
8469            }
8470        }
8471    });
8472    rewrite_exprs(s, &mut |e| {
8473        deep_rewrite(e, &mut |x| {
8474            if let Expr::Local { var, ty } = x {
8475                if *var == from {
8476                    *var = to;
8477                    *ty = to_ty.clone();
8478                }
8479            }
8480        });
8481    });
8482}
8483
8484/// Remove an undone generation's `by_slot` segment entry so slot-based
8485/// resolution can never hand back the now-unreferenced gen id (its
8486/// VarInfo stays — ids index `vars` — but nothing may select it).
8487fn drop_gen_slot(vt: &mut VarTable, dead: u32) {
8488    if (dead as usize) >= vt.vars.len() {
8489        return;
8490    }
8491    let slot = vt.vars[dead as usize].slot as usize;
8492    if let Some(seg) = vt.by_slot.get_mut(slot) {
8493        seg.retain(|&(_, _, id)| id != dead);
8494    }
8495}
8496
8497/// Type gate for `rename_gen_back`: merging gen `v` back onto base `k`
8498/// is only safe when the base can carry the gen's value — identical
8499/// types, or BOTH primitives (the bool/int register-reuse family that
8500/// booleanize + the cast fixers reconcile; the Kotlin bridge case,
8501/// kt5.b `int v18` receiving `boolean p6x`). A reference-vs-primitive
8502/// or cross-class merge would forge `String k = <boolean>`-style
8503/// assignments that never compile (the lark/weibo/reqable regression
8504/// of the ungated first cut: +8.4k `不兼容的类型`).
8505fn undo_type_safe(vt: &VarTable, k: u32, v: u32) -> bool {
8506    if (k as usize) >= vt.vars.len() || (v as usize) >= vt.vars.len() {
8507        return false;
8508    }
8509    let kt = vt.vars[k as usize].ty.erased();
8510    let vt_ty = vt.vars[v as usize].ty.erased();
8511    if kt == vt_ty {
8512        return true;
8513    }
8514    let is_prim = |t: &JavaType| {
8515        matches!(
8516            t,
8517            JavaType::Int
8518                | JavaType::Long
8519                | JavaType::Short
8520                | JavaType::Byte
8521                | JavaType::Char
8522                | JavaType::Float
8523                | JavaType::Double
8524                | JavaType::Boolean
8525        )
8526    };
8527    is_prim(&kt) && is_prim(&vt_ty)
8528}
8529
8530fn split_walk_stmt(
8531    st: &mut Stmt,
8532    vt: &mut VarTable,
8533    gen: &mut std::collections::HashMap<u32, u32>,
8534    counter: &mut u32,
8535    pool: &DexPool,
8536) {
8537    match st {
8538        Stmt::Block(v) => {
8539            for x in v.iter_mut() {
8540                split_walk_stmt(x, vt, gen, counter, pool);
8541            }
8542        }
8543        Stmt::ExprStmt(e) => {
8544            rewrite_gen_reads(e, gen, vt);
8545            if let Expr::Assign {
8546                target,
8547                value,
8548                op: AssignOp::Plain,
8549                ..
8550            } = e
8551            {
8552                if let Expr::Local { var, .. } = &**target {
8553                    let old = *var;
8554                    if split_needed(vt, old, value, pool) {
8555                        let value_ty = value.type_ref().erased();
8556                        do_split(vt, target, old, &value_ty, gen, counter);
8557                    }
8558                }
8559            }
8560        }
8561        Stmt::LocalDef { var, init, .. } => {
8562            if let Some(e) = init {
8563                rewrite_gen_reads(e, gen, vt);
8564                if split_needed(vt, *var, e, pool) {
8565                    // The DEF mints its own generation: retarget the def
8566                    // to a fresh var (the bare decl of the ORIGINAL stays
8567                    // for its earlier readers).
8568                    let value_ty = e.type_ref().erased();
8569                    let old = *var;
8570                    let mut fake = Expr::Local {
8571                        var: old,
8572                        ty: vt.vars[old as usize].ty.clone(),
8573                    };
8574                    do_split(vt, &mut fake, old, &value_ty, gen, counter);
8575                    if let Expr::Local { var: nv, .. } = fake {
8576                        *var = nv;
8577                    }
8578                }
8579            }
8580        }
8581        Stmt::If {
8582            cond,
8583            then_stmt,
8584            else_stmt,
8585            ..
8586        } => {
8587            rewrite_gen_reads(cond, gen, vt);
8588            let then_floor = vt.vars.len() as u32;
8589            let mut g_then = gen.clone();
8590            split_walk_stmt(then_stmt, vt, &mut g_then, counter, pool);
8591            let else_floor = vt.vars.len() as u32;
8592            let mut g_else = gen.clone();
8593            if let Some(e) = else_stmt {
8594                split_walk_stmt(e, vt, &mut g_else, counter, pool);
8595            }
8596            // Undo splits that DIVERGED at this join (see
8597            // rename_gen_back), with TWO gates learned the hard way
8598            // (first cut regressed lark +8.4k):
8599            //  1. the SIBLING KEPT THE BASE — `g_else[k]` equals the
8600            //     entry mapping, i.e. only this branch minted a fresh
8601            //     gen for k. When both branches split, each orphan is
8602            //     declared by ensure_declared at its own value type and
8603            //     compiles; renaming them back onto the base would
8604            //     force cross-kind assignments (`int k = <boolean>`).
8605            //  2. the fresh gen is WRITE-ONLY in the branch — a gen
8606            //     read inside its branch renders fine on its own.
8607            // The gated shape is the Kotlin default-arg bridge phi
8608            // loss: then `v_g = p6x` / else `v = false` / confluence
8609            // reads v — the then value was silently dropped AND v_g
8610            // rendered undeclared (kt5.b). Undo restores the clean
8611            // diamond so fold_default_arg_bridge can fire.
8612            let mut undo_then: Vec<(u32, u32)> = g_then
8613                .iter()
8614                .filter(|(&k, &v)| {
8615                    v >= then_floor
8616                        && v < else_floor
8617                        && g_else.get(&k).copied() == gen.get(&k).copied()
8618                        && undo_type_safe(vt, k, v)
8619                        && stmts_read_var(std::slice::from_ref(&**then_stmt), v) == 0
8620                })
8621                .map(|(&k, &v)| (v, k))
8622                .collect();
8623            undo_then.sort_unstable_by_key(|&(v, _)| std::cmp::Reverse(v));
8624            for &(v, k) in &undo_then {
8625                rename_gen_back(then_stmt, v, k, vt);
8626                drop_gen_slot(vt, v);
8627            }
8628            if let Some(e) = else_stmt.as_deref_mut() {
8629                let mut undo_else: Vec<(u32, u32)> = g_else
8630                    .iter()
8631                    .filter(|(&k, &v)| {
8632                        v >= else_floor
8633                            && g_then.get(&k).copied() == gen.get(&k).copied()
8634                            && undo_type_safe(vt, k, v)
8635                            && stmts_read_var(std::slice::from_ref(&*e), v) == 0
8636                    })
8637                    .map(|(&k, &v)| (v, k))
8638                    .collect();
8639                undo_else.sort_unstable_by_key(|&(v, _)| std::cmp::Reverse(v));
8640                for &(v, k) in &undo_else {
8641                    rename_gen_back(e, v, k, vt);
8642                    drop_gen_slot(vt, v);
8643                }
8644            }
8645            // Join: keep only splits BOTH paths produced (an absent else
8646            // is a fall-through path that keeps the pre-if generation).
8647            gen.retain(|k, v| g_then.get(k) == Some(v) && g_else.get(k) == Some(v));
8648        }
8649        Stmt::While { cond, body, .. } | Stmt::DoWhile { cond, body, .. } => {
8650            // 3-round fixpoint: round N walks the cond+body with the
8651            // accumulated gen — the back-edge carries the tail
8652            // generation into the head reads of the next round.
8653            let entry = gen.clone();
8654            for _ in 0..3 {
8655                rewrite_gen_reads(cond, gen, vt);
8656                split_walk_stmt(body, vt, gen, counter, pool);
8657            }
8658            // After the loop the pre-loop generation is what a
8659            // zero-iteration execution left — conservative.
8660            *gen = entry;
8661        }
8662        Stmt::For { init, body, .. } => {
8663            for x in init.iter_mut() {
8664                split_walk_stmt(x, vt, gen, counter, pool);
8665            }
8666            let entry = gen.clone();
8667            for _ in 0..3 {
8668                split_walk_stmt(body, vt, gen, counter, pool);
8669            }
8670            *gen = entry;
8671        }
8672        Stmt::ForEach { body, .. } => {
8673            let entry = gen.clone();
8674            for _ in 0..3 {
8675                split_walk_stmt(body, vt, gen, counter, pool);
8676            }
8677            *gen = entry;
8678        }
8679        Stmt::Switch { cases, default, .. } => {
8680            let entry = gen.clone();
8681            for c in cases.iter_mut() {
8682                let mut g_case = entry.clone();
8683                for x in c.body.iter_mut() {
8684                    split_walk_stmt(x, vt, &mut g_case, counter, pool);
8685                }
8686                gen.retain(|k, v| g_case.get(k) == Some(v));
8687            }
8688            if let Some(d) = default {
8689                let mut g_def = entry.clone();
8690                split_walk_stmt(d, vt, &mut g_def, counter, pool);
8691                gen.retain(|k, v| g_def.get(k) == Some(v));
8692            }
8693        }
8694        Stmt::Try {
8695            body,
8696            catches,
8697            finally,
8698        } => {
8699            let entry = gen.clone();
8700            let mut g_try = entry.clone();
8701            split_walk_stmt(body, vt, &mut g_try, counter, pool);
8702            let mut g_all = g_try.clone();
8703            for c in catches.iter_mut() {
8704                // The handler runs with the PRE-try state (the exception
8705                // may fire anywhere in the try).
8706                let mut g_c = entry.clone();
8707                split_walk_stmt(&mut c.body, vt, &mut g_c, counter, pool);
8708                g_all.retain(|k, v| g_c.get(k) == Some(v));
8709            }
8710            *gen = g_all;
8711            if let Some(f) = finally {
8712                split_walk_stmt(f, vt, gen, counter, pool);
8713            }
8714        }
8715        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => {
8716            split_walk_stmt(body, vt, gen, counter, pool);
8717        }
8718        Stmt::Return(Some(e)) | Stmt::Throw(e) => {
8719            rewrite_gen_reads(e, gen, vt);
8720        }
8721        _ => {}
8722    }
8723}
8724
8725/// Direct supertypes of framework exception classes (internal names).
8726/// Dex catch handlers legitimately list subclass+superclass pairs for
8727/// one handler (verifier: first match wins); Java multi-catch forbids
8728/// subtype-related alternatives ("multi-catch 语句中的替代无法通过子类化
8729/// 关联" ×58: FileNotFoundException|IOException ×12,
8730/// NameNotFoundException|Exception, JSONException|Exception,
8731/// IllegalArgumentException|RuntimeException, …). Dropping the
8732/// shadowed alternative preserves the caught set EXACTLY (same
8733/// handler body, same variable). Pool hierarchies resolve through
8734/// pool.is_subtype (it matches ancestor NAMES, so pool subclasses of
8735/// framework exceptions work); framework classes are absent from the
8736/// dex, so the stable public JDK/Android chains are tabled here —
8737/// standard decompiler practice. Missing entries only lose a dedupe
8738/// opportunity (safe direction); a WRONG entry would drop a live
8739/// alternative, so only unambiguous chains are listed.
8740static FW_EXC_SUPERS: &[(&str, &str)] = &[
8741    // java.lang
8742    ("java/lang/Exception", "java/lang/Throwable"),
8743    ("java/lang/RuntimeException", "java/lang/Exception"),
8744    ("java/lang/IllegalArgumentException", "java/lang/RuntimeException"),
8745    ("java/lang/IllegalStateException", "java/lang/RuntimeException"),
8746    ("java/lang/NullPointerException", "java/lang/RuntimeException"),
8747    ("java/lang/ArithmeticException", "java/lang/RuntimeException"),
8748    ("java/lang/ClassCastException", "java/lang/RuntimeException"),
8749    ("java/lang/IndexOutOfBoundsException", "java/lang/RuntimeException"),
8750    ("java/lang/ArrayIndexOutOfBoundsException", "java/lang/IndexOutOfBoundsException"),
8751    ("java/lang/StringIndexOutOfBoundsException", "java/lang/IndexOutOfBoundsException"),
8752    ("java/lang/NumberFormatException", "java/lang/IllegalArgumentException"),
8753    ("java/lang/UnsupportedOperationException", "java/lang/RuntimeException"),
8754    ("java/lang/SecurityException", "java/lang/RuntimeException"),
8755    ("java/lang/NegativeArraySizeException", "java/lang/RuntimeException"),
8756    ("java/lang/EnumConstantNotPresentException", "java/lang/RuntimeException"),
8757    ("java/lang/TypeNotPresentException", "java/lang/RuntimeException"),
8758    ("java/lang/CloneNotSupportedException", "java/lang/Exception"),
8759    ("java/lang/InterruptedException", "java/lang/Exception"),
8760    ("java/lang/ReflectiveOperationException", "java/lang/Exception"),
8761    ("java/lang/ClassNotFoundException", "java/lang/ReflectiveOperationException"),
8762    ("java/lang/NoSuchFieldException", "java/lang/ReflectiveOperationException"),
8763    ("java/lang/NoSuchMethodException", "java/lang/ReflectiveOperationException"),
8764    ("java/lang/InstantiationException", "java/lang/ReflectiveOperationException"),
8765    ("java/lang/IllegalAccessException", "java/lang/ReflectiveOperationException"),
8766    ("java/lang/reflect/InvocationTargetException", "java/lang/ReflectiveOperationException"),
8767    ("java/lang/reflect/UndeclaredThrowableException", "java/lang/RuntimeException"),
8768    ("java/lang/invoke/WrongMethodTypeException", "java/lang/RuntimeException"),
8769    ("java/lang/annotation/IncompleteAnnotationException", "java/lang/RuntimeException"),
8770    // java.lang errors
8771    ("java/lang/Error", "java/lang/Throwable"),
8772    ("java/lang/AssertionError", "java/lang/Error"),
8773    ("java/lang/VirtualMachineError", "java/lang/Error"),
8774    ("java/lang/OutOfMemoryError", "java/lang/VirtualMachineError"),
8775    ("java/lang/StackOverflowError", "java/lang/VirtualMachineError"),
8776    ("java/lang/LinkageError", "java/lang/Error"),
8777    ("java/lang/IncompatibleClassChangeError", "java/lang/LinkageError"),
8778    ("java/lang/NoSuchMethodError", "java/lang/IncompatibleClassChangeError"),
8779    ("java/lang/NoSuchFieldError", "java/lang/IncompatibleClassChangeError"),
8780    ("java/lang/NoClassDefFoundError", "java/lang/LinkageError"),
8781    ("java/lang/ExceptionInInitializerError", "java/lang/LinkageError"),
8782    ("java/lang/UnsatisfiedLinkError", "java/lang/LinkageError"),
8783    ("java/lang/VerifyError", "java/lang/LinkageError"),
8784    ("java/lang/ClassFormatError", "java/lang/LinkageError"),
8785    ("java/lang/BootstrapMethodError", "java/lang/LinkageError"),
8786    // java.util
8787    ("java/util/ConcurrentModificationException", "java/lang/RuntimeException"),
8788    ("java/util/NoSuchElementException", "java/lang/RuntimeException"),
8789    ("java/util/MissingResourceException", "java/lang/RuntimeException"),
8790    ("java/util/EmptyStackException", "java/lang/RuntimeException"),
8791    ("java/util/TooManyListenersException", "java/lang/Exception"),
8792    ("java/util/ServiceConfigurationError", "java/lang/Error"),
8793    ("java/util/concurrent/ExecutionException", "java/lang/Exception"),
8794    ("java/util/concurrent/TimeoutException", "java/lang/Exception"),
8795    ("java/util/concurrent/BrokenBarrierException", "java/lang/Exception"),
8796    ("java/util/concurrent/RejectedExecutionException", "java/lang/RuntimeException"),
8797    ("java/util/concurrent/CompletionException", "java/lang/RuntimeException"),
8798    ("java/util/concurrent/CancellationException", "java/lang/IllegalStateException"),
8799    // java.io / java.net
8800    ("java/io/IOException", "java/lang/Exception"),
8801    ("java/io/FileNotFoundException", "java/io/IOException"),
8802    ("java/io/InterruptedIOException", "java/io/IOException"),
8803    ("java/io/UnsupportedEncodingException", "java/io/IOException"),
8804    ("java/io/UTFDataFormatException", "java/io/IOException"),
8805    ("java/io/EOFException", "java/io/IOException"),
8806    ("java/io/CharConversionException", "java/io/IOException"),
8807    ("java/io/ObjectStreamException", "java/io/IOException"),
8808    ("java/io/InvalidObjectException", "java/io/ObjectStreamException"),
8809    ("java/io/NotSerializableException", "java/io/ObjectStreamException"),
8810    ("java/net/SocketException", "java/io/IOException"),
8811    ("java/net/ConnectException", "java/net/SocketException"),
8812    ("java/net/BindException", "java/net/SocketException"),
8813    ("java/net/NoRouteToHostException", "java/net/SocketException"),
8814    ("java/net/ProtocolException", "java/io/IOException"),
8815    ("java/net/UnknownHostException", "java/io/IOException"),
8816    ("java/net/MalformedURLException", "java/io/IOException"),
8817    ("java/net/HttpRetryException", "java/io/IOException"),
8818    ("java/net/SocketTimeoutException", "java/io/InterruptedIOException"),
8819    ("java/net/URISyntaxException", "java/lang/Exception"),
8820    // java.text / security / crypto / ssl / sax
8821    ("java/text/ParseException", "java/lang/Exception"),
8822    ("java/security/GeneralSecurityException", "java/lang/Exception"),
8823    ("java/security/NoSuchAlgorithmException", "java/security/GeneralSecurityException"),
8824    ("java/security/NoSuchProviderException", "java/security/GeneralSecurityException"),
8825    ("java/security/InvalidKeyException", "java/security/GeneralSecurityException"),
8826    ("java/security/InvalidAlgorithmParameterException", "java/security/GeneralSecurityException"),
8827    ("java/security/SignatureException", "java/security/GeneralSecurityException"),
8828    ("java/security/UnrecoverableKeyException", "java/security/GeneralSecurityException"),
8829    ("java/security/KeyStoreException", "java/security/GeneralSecurityException"),
8830    ("java/security/cert/CertificateException", "java/security/GeneralSecurityException"),
8831    ("java/security/spec/InvalidKeySpecException", "java/security/GeneralSecurityException"),
8832    ("javax/crypto/BadPaddingException", "java/security/GeneralSecurityException"),
8833    ("javax/crypto/IllegalBlockSizeException", "java/security/GeneralSecurityException"),
8834    ("javax/crypto/NoSuchPaddingException", "java/security/GeneralSecurityException"),
8835    ("javax/crypto/ShortBufferException", "java/security/GeneralSecurityException"),
8836    ("javax/net/ssl/SSLException", "java/io/IOException"),
8837    ("org/xml/sax/SAXException", "java/lang/Exception"),
8838    ("org/xml/sax/SAXParseException", "org/xml/sax/SAXException"),
8839    ("org/json/JSONException", "java/lang/RuntimeException"),
8840    ("org/xmlpull/v1/XmlPullParserException", "java/lang/Exception"),
8841    ("java/util/IllegalFormatException", "java/lang/IllegalArgumentException"),
8842    ("java/util/MissingFormatArgumentException", "java/util/IllegalFormatException"),
8843    ("java/util/UnknownFormatConversionException", "java/util/IllegalFormatException"),
8844    // android
8845    ("android/util/AndroidRuntimeException", "java/lang/RuntimeException"),
8846    ("android/util/AndroidException", "java/lang/Exception"),
8847    ("android/content/ActivityNotFoundException", "java/lang/RuntimeException"),
8848    ("android/content/ComponentNotFoundException", "java/lang/RuntimeException"),
8849    ("android/content/pm/PackageManager$NameNotFoundException", "java/lang/Exception"),
8850    ("android/content/res/Resources$NotFoundException", "java/lang/RuntimeException"),
8851    ("android/database/SQLException", "java/lang/RuntimeException"),
8852    ("android/database/sqlite/SQLiteException", "android/database/SQLException"),
8853    ("android/database/CursorIndexOutOfBoundsException", "java/lang/IndexOutOfBoundsException"),
8854    ("android/os/NetworkOnMainThreadException", "java/lang/RuntimeException"),
8855    ("android/system/ErrnoException", "java/lang/Exception"),
8856    ("android/provider/Settings$SettingNotFoundException", "android/util/AndroidException"),
8857    ("android/net/UriParseException", "java/lang/RuntimeException"),
8858    ("android/view/InflateException", "java/lang/RuntimeException"),
8859    ("android/accounts/AuthenticatorException", "java/lang/Exception"),
8860    ("android/accounts/OperationCanceledException", "android/accounts/AuthenticatorException"),
8861    ("android/hardware/camera2/CameraAccessException", "java/lang/Exception"),
8862    ("android/opengl/GLException", "java/lang/RuntimeException"),
8863    ("android/renderscript/RSRuntimeException", "java/lang/RuntimeException"),
8864];
8865
8866fn fw_super(c: &str) -> Option<&'static str> {
8867    FW_EXC_SUPERS
8868        .iter()
8869        .find(|(k, _)| *k == c)
8870        .map(|(_, v)| *v)
8871}
8872
8873/// `sub <: sup` for exception-class names, combining the pool
8874/// hierarchy (which also matches framework ancestor NAMES on the way
8875/// up, so pool subclasses of framework exceptions resolve) with the
8876/// tabled framework chains for pairs where both sides are outside the
8877/// dex.
8878fn exc_subtype(sub: &str, sup: &str, pool: &DexPool) -> bool {
8879    if sub == sup {
8880        return true;
8881    }
8882    if pool.is_subtype(sub, sup) {
8883        return true;
8884    }
8885    let mut cur = sub;
8886    for _ in 0..24 {
8887        let Some(next) = fw_super(cur) else { break };
8888        if next == sup {
8889            return true;
8890        }
8891        cur = next;
8892    }
8893    false
8894}
8895
8896/// Drop multi-catch alternatives shadowed by another alternative in
8897/// the same handler (JLS 14.20.2 forbids subtype-related
8898/// alternatives; the dex verifier does not). Exact duplicates collapse
8899/// to the first occurrence. The caught set is unchanged, so this is
8900/// semantics-preserving by construction.
8901pub fn dedupe_multicatch(s: &mut Stmt, pool: &DexPool) {
8902    dedupe_multicatch_walk(s, pool);
8903}
8904
8905fn dedupe_multicatch_walk(s: &mut Stmt, pool: &DexPool) {
8906    match s {
8907        Stmt::Try {
8908            body,
8909            catches,
8910            finally,
8911        }
8912        | Stmt::TryWithResources {
8913            body,
8914            catches,
8915            finally,
8916            ..
8917        } => {
8918            dedupe_multicatch_walk(body, pool);
8919            for c in catches.iter_mut() {
8920                if c.exc.len() > 1 {
8921                    let mut keep: Vec<std::sync::Arc<str>> = Vec::with_capacity(c.exc.len());
8922                    for e in c.exc.iter() {
8923                        let shadowed = c.exc.iter().any(|o| {
8924                            o.as_ref() != e.as_ref() && exc_subtype(e, o, pool)
8925                        });
8926                        if !shadowed && !keep.iter().any(|k| k.as_ref() == e.as_ref()) {
8927                            keep.push(e.clone());
8928                        }
8929                    }
8930                    c.exc = keep;
8931                }
8932                dedupe_multicatch_walk(&mut c.body, pool);
8933            }
8934            if let Some(f) = finally {
8935                dedupe_multicatch_walk(f, pool);
8936            }
8937        }
8938        Stmt::Block(v) => {
8939            for x in v.iter_mut() {
8940                dedupe_multicatch_walk(x, pool);
8941            }
8942        }
8943        Stmt::If {
8944            then_stmt,
8945            else_stmt,
8946            ..
8947        } => {
8948            dedupe_multicatch_walk(then_stmt, pool);
8949            if let Some(e) = else_stmt {
8950                dedupe_multicatch_walk(e, pool);
8951            }
8952        }
8953        Stmt::While { body, .. }
8954        | Stmt::DoWhile { body, .. }
8955        | Stmt::For { body, .. }
8956        | Stmt::ForEach { body, .. }
8957        | Stmt::Labeled { body, .. }
8958        | Stmt::Synchronized { body, .. } => dedupe_multicatch_walk(body, pool),
8959        Stmt::Switch { cases, default, .. } => {
8960            for c in cases.iter_mut() {
8961                for x in c.body.iter_mut() {
8962                    dedupe_multicatch_walk(x, pool);
8963                }
8964            }
8965            if let Some(d) = default {
8966                dedupe_multicatch_walk(d, pool);
8967            }
8968        }
8969        _ => {}
8970    }
8971}
8972
8973/// Dangling `break L<id>`: a Goto whose paired Label/Labeled-wrap was
8974/// lost to structure degradation prints `break L<id>;` against an
8975/// undeclared label ("未定义的标签", weibo 371/lark 85/weixin 535).
8976/// Inside a loop the goto-to-loop-exit shape degrades to a plain
8977/// `break` — compilable, and the dominant original semantic. Outside
8978/// any loop the jump is irreducible flow (weixin protobuf state
8979/// machines: `goto` into a lost loop middle / dispatcher block — the
8980/// target exists but not at a position Java can express); it renders
8981/// as an honest `$DDC:` marker comment instead of a guaranteed
8982/// undefined-label error, and the block's tail after it is dropped
8983/// (the original control flow never reached those statements).
8984pub fn resolve_dangling_gotos(s: &mut Stmt) {
8985    let mut labels: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
8986    let probe = s.clone();
8987    walk_all(&probe, &mut |st| {
8988        if let Stmt::Label(id) = st {
8989            labels.insert(*id);
8990        }
8991    });
8992    resolve_gotos_walk(s, &labels, 0);
8993}
8994
8995fn resolve_gotos_walk(s: &mut Stmt, labels: &jdc_core::FxHashSet<u32>, loop_depth: u32) {
8996    let depth = match s {
8997        Stmt::While { .. } | Stmt::DoWhile { .. } | Stmt::For { .. } | Stmt::ForEach { .. } => {
8998            loop_depth + 1
8999        }
9000        _ => loop_depth,
9001    };
9002    match s {
9003        Stmt::Goto(id) => {
9004            if labels.contains(id) {
9005                return; // valid pair — keep
9006            }
9007            if depth > 0 {
9008                *s = Stmt::Break(None);
9009            } else {
9010                *s = Stmt::Comment(format!(
9011                    "$DDC: unresolved jump to block {} (irreducible control flow)",
9012                    id
9013                ));
9014            }
9015        }
9016        Stmt::Block(v) => {
9017            let mut i = 0usize;
9018            while i < v.len() {
9019                // A depth-0 dangling Goto becomes a marker comment and
9020                // the original flow never reached the siblings after
9021                // it — dropping them keeps the rendered reachability
9022                // faithful (they were dead in the dex too).
9023                let dangling_here =
9024                    depth == 0 && matches!(&v[i], Stmt::Goto(id) if !labels.contains(id));
9025                resolve_gotos_walk(&mut v[i], labels, depth);
9026                if dangling_here {
9027                    v.truncate(i + 1);
9028                    break;
9029                }
9030                i += 1;
9031            }
9032        }
9033        Stmt::If { then_stmt, else_stmt, .. } => {
9034            resolve_gotos_walk(then_stmt, labels, depth);
9035            if let Some(e) = else_stmt {
9036                resolve_gotos_walk(e, labels, depth);
9037            }
9038        }
9039        Stmt::While { body, .. }
9040        | Stmt::DoWhile { body, .. }
9041        | Stmt::For { body, .. }
9042        | Stmt::ForEach { body, .. }
9043        | Stmt::Labeled { body, .. }
9044        | Stmt::Synchronized { body, .. } => resolve_gotos_walk(body, labels, depth),
9045        Stmt::Try {
9046            body,
9047            catches,
9048            finally,
9049        } => {
9050            resolve_gotos_walk(body, labels, depth);
9051            for c in catches.iter_mut() {
9052                resolve_gotos_walk(&mut c.body, labels, depth);
9053            }
9054            if let Some(f) = finally {
9055                resolve_gotos_walk(f, labels, depth);
9056            }
9057        }
9058        Stmt::Switch { cases, default, .. } => {
9059            for c in cases.iter_mut() {
9060                for x in c.body.iter_mut() {
9061                    resolve_gotos_walk(x, labels, depth);
9062                }
9063            }
9064            if let Some(d) = default {
9065                resolve_gotos_walk(d, labels, depth);
9066            }
9067        }
9068        _ => {}
9069    }
9070}
9071
9072/// READS of `v` across the statements (assignment targets excluded).
9073fn stmts_read_var(stmts: &[Stmt], v: u32) -> usize {
9074    let mut n = 0usize;
9075    for s in stmts {
9076        let mut c = s.clone();
9077        walk_stmt_exprs(&mut c, &mut |e| {
9078            deep_rewrite_reads(e, &mut |x| {
9079                if let Expr::Local { var: vv, .. } = x {
9080                    if *vv == v {
9081                        n += 1;
9082                    }
9083                }
9084            });
9085        });
9086    }
9087    n
9088}
9089
9090pub fn fix_ctor_conditional_super(body: &mut Stmt) {
9091    // A single-statement body can arrive UNWRAPPED (bare `If` — the
9092    // gb6/e throw-guard family); the shape scans need a statement list.
9093    if !matches!(body, Stmt::Block(_)) {
9094        let inner = std::mem::replace(body, Stmt::Block(Vec::new()));
9095        *body = Stmt::Block(vec![inner]);
9096    }
9097    let Stmt::Block(stmts) = body else { return };
9098    // Unwrap single-statement Block wrappers: the structurer can nest
9099    // the whole body one level deeper, hiding the top-level If from the
9100    // shape scans (cleanup would flatten it — but runs AFTER this pass).
9101    while stmts.len() == 1 {
9102        let Stmt::Block(inner) = &stmts[0] else { break };
9103        *stmts = inner.clone();
9104    }
9105    if stmts.is_empty() {
9106        return;
9107    }
9108    if is_bare_ctor_call(stmts.first().unwrap()) {
9109        dedupe_ctor_delegations(body);
9110        return;
9111    }
9112    if try_linear_super_inline(stmts) {
9113        return;
9114    }
9115    try_merge_branched_super(stmts);
9116}
9117
9118/// Shape C: the delegation is already first — strip duplicate copies
9119/// left by R8 path-duplication (Kotlin default-arg bridge ctors: ft5/j
9120/// had `super()` leading PLUS another inside a branch — "对super的调用
9121/// 必须是构造器中的第一个语句" at the copy). Every path delegates
9122/// through the leading call, so structurally-identical (or empty-args,
9123/// same-kind) copies nested in blocks/ifs are removed, never moved.
9124/// Runs both ahead of the shape-A/B attempt and AFTER the plain hoist
9125/// (fix_ctor_super_first) — the hoist is what puts the leading
9126/// delegation in place for path-duplicated ctors.
9127pub fn dedupe_ctor_delegations(body: &mut Stmt) {
9128    let Stmt::Block(stmts) = body else { return };
9129    let Some(d0) = stmts.first().and_then(|s| match s {
9130        Stmt::ExprStmt(e) if is_delegation_expr(e) => Some(e.clone()),
9131        _ => None,
9132    }) else {
9133        return;
9134    };
9135    fn rec(s: &mut Stmt, d0: &Expr) {
9136        match s {
9137            Stmt::Block(v) => {
9138                v.retain(|x| {
9139                    !matches!(x, Stmt::ExprStmt(e) if is_delegation_expr(e) && same_delegation(e, d0))
9140                });
9141                for x in v.iter_mut() {
9142                    rec(x, d0);
9143                }
9144            }
9145            Stmt::If { then_stmt, else_stmt, .. } => {
9146                rec(then_stmt, d0);
9147                if let Some(e) = else_stmt {
9148                    rec(e, d0);
9149                }
9150            }
9151            _ => {}
9152        }
9153    }
9154    // Top-level copies first (rec only strips copies NESTED in
9155    // blocks/ifs — a duplicate `super()` sitting directly in the body
9156    // list fell through its `_` arm: weixin ft5/j's second super()).
9157    {
9158        let d0r = &d0;
9159        let mut idx = 0usize;
9160        stmts.retain(|s| {
9161            idx += 1;
9162            idx == 1
9163                || !(matches!(s, Stmt::ExprStmt(e)
9164                    if is_delegation_expr(e) && same_delegation(e, d0r)))
9165        });
9166    }
9167    for s in stmts.iter_mut().skip(1) {
9168        rec(s, &d0);
9169    }
9170}
9171
9172/// Structural delegation identity for dedup: same kind/target and equal
9173/// args — or both arg-less (`super()` copies across duplicated paths
9174/// render with locally-renamed args yet delegate identically).
9175fn same_delegation(a: &Expr, b: &Expr) -> bool {
9176    match (a, b) {
9177        (
9178            Expr::Method { name: n1, cls: c1, is_super: s1, args: a1, .. },
9179            Expr::Method { name: n2, cls: c2, is_super: s2, args: a2, .. },
9180        ) => {
9181            &**n1 == "<init>"
9182                && &**n2 == "<init>"
9183                && c1 == c2
9184                && s1 == s2
9185                && (a1 == a2 || (a1.is_empty() && a2.is_empty()))
9186        }
9187        _ => false,
9188    }
9189}
9190
9191/// The `Method` expr of a bare ctor-delegation statement.
9192fn ctor_call_expr(s: &Stmt) -> Option<&Expr> {
9193    match s {
9194        Stmt::ExprStmt(e) if is_delegation_expr(e) => Some(e),
9195        _ => None,
9196    }
9197}
9198
9199/// Same delegation kind ignoring args (super vs this, same target).
9200fn same_ctor_kind(a: &Expr, b: &Expr) -> bool {
9201    match (a, b) {
9202        (
9203            Expr::Method { cls: c1, is_super: s1, owner: o1, .. },
9204            Expr::Method { cls: c2, is_super: s2, owner: o2, .. },
9205        ) => c1 == c2 && s1 == s2 && o1 == o2,
9206        _ => false,
9207    }
9208}
9209
9210/// Statements of a branch body, recursively flattening pure Blocks —
9211/// the fix runs BEFORE cleanup, so structurer-emitted nesting is still
9212/// in place. Any non-Block statement (If/Try/...) stays an item and
9213/// fails the def/call classification, which is the intended abort.
9214fn flat_list(s: &Stmt) -> Vec<&Stmt> {
9215    fn rec<'a>(s: &'a Stmt, out: &mut Vec<&'a Stmt>) {
9216        match s {
9217            Stmt::Block(v) => {
9218                for x in v {
9219                    rec(x, out);
9220                }
9221            }
9222            other => out.push(other),
9223        }
9224    }
9225    let mut out = Vec::new();
9226    rec(s, &mut out);
9227    out
9228}
9229
9230/// Blank the OWNER of un-folded `<init>` calls (lost allocation): the
9231/// printer renders them as `new X(..)` and never reads the owner, so its
9232/// register reference is not a real use (v7.a$a2: the dead
9233/// `new f(0,..)`'s owner was v1's register, inflating v1's use count
9234/// and blocking the inline-hoist).
9235fn strip_lost_alloc_owners(e: &mut Expr) {
9236    deep_rewrite(e, &mut |x| {
9237        if let Expr::Method { name, is_special: true, owner: Some(o), .. } = x {
9238            if &**name == "<init>" && !matches!(**o, Expr::This) {
9239                **o = Expr::Const(jdc_core::ir::expr::ConstVal::Int(0));
9240            }
9241        }
9242    });
9243}
9244
9245/// Per-var `Local` occurrence counts over an expression (single pass;
9246/// lost-alloc `<init>` owners excluded — see strip_lost_alloc_owners).
9247fn count_locals_expr(e: &Expr) -> std::collections::HashMap<u32, usize> {
9248    let mut m = std::collections::HashMap::new();
9249    let mut probe = e.clone();
9250    strip_lost_alloc_owners(&mut probe);
9251    deep_rewrite(&mut probe, &mut |x| {
9252        if let Expr::Local { var: v, .. } = x {
9253            *m.entry(*v).or_insert(0) += 1;
9254        }
9255    });
9256    m
9257}
9258
9259pub(crate) fn count_locals_stmts(ss: &[Stmt]) -> std::collections::HashMap<u32, usize> {
9260    let mut m = std::collections::HashMap::new();
9261    for s in ss {
9262        let mut c = s.clone();
9263        walk_stmt_exprs(&mut c, &mut |e| {
9264            strip_lost_alloc_owners(e);
9265            deep_rewrite(e, &mut |x| {
9266                if let Expr::Local { var: v, .. } = x {
9267                    *m.entry(*v).or_insert(0) += 1;
9268                }
9269            });
9270        });
9271    }
9272    m
9273}
9274
9275fn cnt(m: &std::collections::HashMap<u32, usize>, v: u32) -> usize {
9276    m.get(&v).copied().unwrap_or(0)
9277}
9278
9279/// Substitute map-referenced locals, recursively expanding def chains
9280/// (depth-capped). Returns None when a referenced var has no def or the
9281/// chain is too deep.
9282fn inline_locals(
9283    e: &Expr,
9284    map: &std::collections::HashMap<u32, Expr>,
9285    depth: u32,
9286) -> Option<Expr> {
9287    if depth > 4 {
9288        return None;
9289    }
9290    let mut out = e.clone();
9291    let mut fail = false;
9292    deep_rewrite(&mut out, &mut |x| {
9293        if let Expr::Local { var, .. } = x {
9294            if let Some(def) = map.get(var) {
9295                match inline_locals(def, map, depth + 1) {
9296                    Some(r) => *x = r,
9297                    None => fail = true,
9298                }
9299            }
9300        }
9301    });
9302    if fail {
9303        None
9304    } else {
9305        Some(out)
9306    }
9307}
9308
9309/// A leading statement that defines a local: (var, init expr).
9310fn def_of(s: &Stmt) -> Option<(u32, &Expr)> {
9311    match s {
9312        Stmt::LocalDef { var, init: Some(e), .. } => Some((*var, e)),
9313        Stmt::ExprStmt(Expr::Assign {
9314            target,
9315            value,
9316            op: jdc_core::ir::expr::AssignOp::Plain,
9317            ..
9318        }) => match &**target {
9319            Expr::Local { var, .. } => Some((*var, value)),
9320            _ => None,
9321        },
9322        _ => None,
9323    }
9324}
9325
9326/// Shape A: straight-line prefix, delegation references prefix locals.
9327fn try_linear_super_inline(stmts: &mut Vec<Stmt>) -> bool {
9328    let Some(pos) = stmts.iter().position(is_bare_ctor_call) else {
9329        return false;
9330    };
9331    if pos == 0 {
9332        return false; // already first — nothing to do
9333    }
9334    // The prefix must be straight-line.
9335    if !stmts[..pos]
9336        .iter()
9337        .all(|s| matches!(s, Stmt::LocalDef { .. } | Stmt::ExprStmt(_)))
9338    {
9339        return false;
9340    }
9341    let Some(call) = ctor_call_expr(&stmts[pos]) else {
9342        return false;
9343    };
9344    let Expr::Method { args, .. } = call else {
9345        return false;
9346    };
9347    // Defs reachable from the args (last definition wins, bytecode order).
9348    let mut map: std::collections::HashMap<u32, Expr> =
9349        std::collections::HashMap::new();
9350    let mut def_idx: std::collections::HashMap<u32, usize> =
9351        std::collections::HashMap::new();
9352    for (i, s) in stmts[..pos].iter().enumerate() {
9353        if let Some((v, e)) = def_of(s) {
9354            map.insert(v, e.clone());
9355            def_idx.insert(v, i);
9356        }
9357    }
9358    // Which arg locals need inlining?
9359    let mut needed: Vec<u32> = Vec::new();
9360    for a in args {
9361        let mut probe = a.clone();
9362        strip_lost_alloc_owners(&mut probe);
9363        deep_rewrite(&mut probe, &mut |x| {
9364            if let Expr::Local { var, .. } = x {
9365                if map.contains_key(var) && !needed.contains(var) {
9366                    needed.push(*var);
9367                }
9368            }
9369        });
9370    }
9371    if needed.is_empty() {
9372        return false; // plain hoist handles it
9373    }
9374    // Guard: side-effecting defs inline only at a provably preserved
9375    // call count (exactly one use, the arg site).
9376    let mut consumed: Vec<usize> = Vec::new();
9377    let use_counts = count_locals_stmts(stmts);
9378    for &v in &needed {
9379        let def = &map[&v];
9380        let uses = cnt(&use_counts, v);
9381        if jdc_core::ir::build::has_side_effects(def) && uses != 1 {
9382            return false;
9383        }
9384        if uses == 1 {
9385            consumed.push(def_idx[&v]);
9386        }
9387    }
9388    // Build the inlined args.
9389    let mut new_args: Vec<Expr> = Vec::with_capacity(args.len());
9390    for a in args {
9391        match inline_locals(a, &map, 0) {
9392            Some(r) => new_args.push(r),
9393            None => return false,
9394        }
9395    }
9396    // Rebuild: super first (with inlined args), the unconsumed prefix
9397    // statements keep their order after it, then the original rest.
9398    let mut call_stmt = stmts.remove(pos);
9399    if let Stmt::ExprStmt(Expr::Method { args: slot, .. }) = &mut call_stmt {
9400        *slot = new_args;
9401    }
9402    let mut rest: Vec<Stmt> = Vec::with_capacity(stmts.len() + 1);
9403    rest.push(call_stmt);
9404    for (i, s) in stmts.drain(..).enumerate() {
9405        // Indices shift after `remove(pos)`: consumed holds pre-removal
9406        // indices; pos itself is already gone from `stmts`.
9407        let orig = if i >= pos { i + 1 } else { i };
9408        if !consumed.contains(&orig) {
9409            rest.push(s);
9410        }
9411    }
9412    *stmts = rest;
9413    true
9414}
9415
9416/// Shape B: if/else branches each ending in the same-kind delegation.
9417fn try_merge_branched_super(stmts: &mut Vec<Stmt>) -> bool {
9418    // Candidate If positions — the prelude before the chosen If must be
9419    // straight-line; try each If until one merges (nested/multi-if
9420    // ctors exist but the first matching one is the delegation site).
9421    let if_positions: Vec<usize> = stmts
9422        .iter()
9423        .enumerate()
9424        .filter(|(_, s)| matches!(s, Stmt::If { .. }))
9425        .map(|(i, _)| i)
9426        .collect();
9427    for if_pos in if_positions {
9428        // Cheap allocation-free gate: at least one branch must hold a
9429        // delegation (most ctor ifs are field null-checks and must not
9430        // pay the merge machinery). The OTHER side may be delegation-free
9431        // (throw-guard shape D) or absent (then-only shape E — the
9432        // rendered if/else form often only exists after later passes).
9433        let gate = match &stmts[if_pos] {
9434            Stmt::If { then_stmt, else_stmt, .. } => {
9435                contains_delegation(then_stmt)
9436                    || else_stmt.as_ref().is_some_and(|e| contains_delegation(e))
9437            }
9438            _ => false,
9439        };
9440        if gate && merge_at(stmts, if_pos) {
9441            return true;
9442        }
9443    }
9444    false
9445}
9446
9447/// Does this subtree contain a ctor delegation? (module-level: shared by
9448/// the merge gate and split_branch)
9449fn contains_delegation(s: &Stmt) -> bool {
9450    match s {
9451        Stmt::ExprStmt(e) => is_delegation_expr(e),
9452        Stmt::Block(v) => v.iter().any(contains_delegation),
9453        Stmt::If { then_stmt, else_stmt, .. } => {
9454            contains_delegation(then_stmt)
9455                || else_stmt.as_ref().is_some_and(|e| contains_delegation(e))
9456        }
9457        _ => false,
9458    }
9459}
9460
9461
9462
9463/// One merge attempt at `stmts[if_pos]`. See fix_ctor_conditional_super
9464/// for the doctrine. Extensions over the pure two-call merge:
9465/// - branch pre-call SIDE-EFFECT statements (Kotlin `o.h(param,..)`
9466///   null-checks — the zz5/j0 family) are allowed; they move into the
9467///   branch remainder (after super — the same reordering doctrine the
9468///   plain hoist already applies; Java has no pre-super statement form).
9469/// - a branch WITHOUT a delegation (throw-guard: `if (x==null) throw
9470///   .. else { super(..); .. }` — the gb6/e family) merges as pure
9471///   remainder; exactly one branch carries the call then, and no
9472///   ternary args arise (the guard throws before any path needs them).
9473/// - prelude single-assignment if/else (`if (c) {v=e1} else {v=e2}`)
9474///   folds into a Cond def for inlining (the zz5/j0 v15/v16 preludes).
9475fn merge_at(stmts: &mut Vec<Stmt>, if_pos: usize) -> bool {
9476    /// Branch classification: Call(at, defs, pre-call extra idxs) when a
9477    /// delegation sits behind only defs/side-effect statements; Clean
9478    /// when the branch has no delegation at all (pure remainder); Dirty
9479    /// otherwise (nested/conditional delegation — out of scope).
9480    enum Br {
9481        /// (call index, pre-call defs, def statement indices)
9482        Call(usize, Vec<(u32, Expr)>, Vec<usize>),
9483        Clean,
9484        Dirty,
9485    }
9486    fn split_branch(list: &[&Stmt]) -> Br {
9487        let call_pos = list.iter().position(|s| ctor_call_expr(s).is_some());
9488        let Some(c) = call_pos else {
9489            // No delegation at this level: a pure remainder branch
9490            // (throw-guard) unless one hides in nested control flow.
9491            return if list.iter().any(|s| contains_delegation(s)) {
9492                Br::Dirty
9493            } else {
9494                Br::Clean
9495            };
9496        };
9497        if list.iter().skip(c + 1).any(|s| ctor_call_expr(s).is_some()) {
9498            return Br::Dirty; // two delegations in one branch
9499        }
9500        let mut defs: Vec<(u32, Expr)> = Vec::new();
9501        let mut def_idxs: Vec<usize> = Vec::new();
9502        for (i, s) in list.iter().enumerate().take(c) {
9503            match def_of(s) {
9504                Some((v, e)) => {
9505                    defs.push((v, e.clone()));
9506                    def_idxs.push(i);
9507                }
9508                None => match s {
9509                    // Side-effect statements and bare decls ahead of the
9510                    // delegation ride along into the remainder.
9511                    Stmt::ExprStmt(_) | Stmt::LocalDef { .. } => {}
9512                    _ => return Br::Dirty, // control flow pre-call
9513                },
9514            }
9515        }
9516        Br::Call(c, defs, def_idxs)
9517    }
9518    /// Prelude single-assignment if/else → `v = c ? e1 : e2` def.
9519    fn cond_def_of(s: &Stmt, scope: &[Stmt]) -> Option<(u32, Expr)> {
9520        let Stmt::If { cond, then_stmt, else_stmt: Some(e), .. } = s else {
9521            return None;
9522        };
9523        fn one<'x>(b: &'x Stmt, scope: &[Stmt]) -> Option<(u32, &'x Expr)> {
9524            let l = flat_list(b);
9525            if l.len() == 1 {
9526                return def_of(l[0]);
9527            }
9528            // A [w = E; v = w] forwarding PAIR — the shape the
9529            // structurer leaves when E is impure (`c v5 = new c(..);
9530            // v6 = v5`) and forward_single_use's adjacency pass missed
9531            // it (a duplicated read elsewhere in its raw body broke the
9532            // single-read gate). Collapse to `v = E`, gated on w being
9533            // method-unique: the merge DROPS this branch, so any other
9534            // read/write of w would dangle (lark gp2/d Kotlin default-
9535            // arg bridge, this-not-first family).
9536            if l.len() == 2 {
9537                let (w, ex) = def_of(l[0])?;
9538                let Stmt::ExprStmt(Expr::Assign {
9539                    target,
9540                    op: AssignOp::Plain,
9541                    value,
9542                }) = l[1]
9543                else {
9544                    return None;
9545                };
9546                let Expr::Local { var: tv, .. } = &**target else {
9547                    return None;
9548                };
9549                let Expr::Local { var: rv, .. } = &**value else {
9550                    return None;
9551                };
9552                if *rv != w || *tv == w {
9553                    return None;
9554                }
9555                // stmts_count_writes only sees Expr-level assigns; the
9556                // pair's own def is often an init-bearing LocalDef, so
9557                // count those separately for the uniqueness gate.
9558                let mut ldef_writes = 0usize;
9559                for st in scope {
9560                    walk_all(st, &mut |x| {
9561                        if let Stmt::LocalDef {
9562                            var,
9563                            init: Some(_),
9564                            ..
9565                        } = x
9566                        {
9567                            if *var == w {
9568                                ldef_writes += 1;
9569                            }
9570                        }
9571                    });
9572                }
9573                if stmts_read_var(scope, w) != 1
9574                    || stmts_count_writes(scope, w) + ldef_writes != 1
9575                {
9576                    return None;
9577                }
9578                return Some((*tv, ex));
9579            }
9580            None
9581        }
9582        let (v1, e1) = one(then_stmt, scope)?;
9583        let (v2, e2) = one(e, scope)?;
9584        if v1 != v2 {
9585            return None;
9586        }
9587        Some((
9588            v1,
9589            Expr::Cond {
9590                c: Box::new(cond.clone()),
9591                t: Box::new(e1.clone()),
9592                f: Box::new(e2.clone()),
9593            },
9594        ))
9595    }
9596
9597    // Prelude: defs (flat or folded cond), bare decls, side-effect stmts.
9598    let mut pmap: std::collections::HashMap<u32, Expr> =
9599        std::collections::HashMap::new();
9600    let mut pidx: std::collections::HashMap<u32, usize> =
9601        std::collections::HashMap::new();
9602    for (i, s) in stmts[..if_pos].iter().enumerate() {
9603        match s {
9604            Stmt::LocalDef { .. } | Stmt::ExprStmt(_) => {
9605                if let Some((v, e)) = def_of(s) {
9606                    pmap.insert(v, e.clone());
9607                    pidx.insert(v, i);
9608                }
9609            }
9610            Stmt::If { .. } => match cond_def_of(s, stmts) {
9611                Some((v, e)) => {
9612                    pmap.insert(v, e);
9613                    pidx.insert(v, i);
9614                }
9615                None => return false,
9616            },
9617            _ => return false,
9618        }
9619    }
9620    let (cond, then_v, else_v, had_else) = match &stmts[if_pos] {
9621        Stmt::If { cond, then_stmt, else_stmt, .. } => (
9622            cond.clone(),
9623            then_stmt.as_ref().clone(),
9624            else_stmt.as_ref().map_or_else(|| Stmt::Block(Vec::new()), |e| e.as_ref().clone()),
9625            else_stmt.is_some(),
9626        ),
9627        _ => return false,
9628    };
9629    // Chained guards (Kotlin multi-param null-checks):
9630    // `if(a){throw..} else { if(b){throw..} else { super(..); .. } }` —
9631    // a branch that is a lone delegation-bearing If gets merged
9632    // recursively first (its delegation hoisted to the branch head), so
9633    // the outer split then sees [call, nested-guard-remainder].
9634    fn normalize_branch(b: &mut Stmt) {
9635        // MOVE-based (no deep clone): chained weixin Parcel ctors carry
9636        // huge tails; cloning per recursion level cost measurable wall
9637        // time. merge_at leaves stmts untouched on failure, so the taken
9638        // node can always go back.
9639        let slot: &mut Stmt = match b {
9640            Stmt::Block(v) if v.len() == 1 => &mut v[0],
9641            other => other,
9642        };
9643        if !matches!(slot, Stmt::If { .. }) || !contains_delegation(slot) {
9644            return;
9645        }
9646        let taken = std::mem::replace(slot, Stmt::Block(Vec::new()));
9647        let mut tmp = vec![taken];
9648        if merge_at(&mut tmp, 0) {
9649            *slot = Stmt::Block(tmp);
9650        } else {
9651            *slot = tmp.pop().unwrap();
9652        }
9653    }
9654    let mut then_v = then_v;
9655    let mut else_v = else_v;
9656    normalize_branch(&mut then_v);
9657    normalize_branch(&mut else_v);
9658    let then_list = flat_list(&then_v);
9659    let else_list = flat_list(&else_v);
9660
9661    // Exactly: two Call branches (merge with ternaries) or one Call +
9662    // one Clean (single-delegation throw-guard). Owned branch payloads.
9663    enum Side {
9664        Call {
9665            at: usize,
9666            defs: Vec<(u32, Expr)>,
9667            def_idxs: Vec<usize>,
9668        },
9669        Clean,
9670    }
9671    let (t_side, e_side, two_sided) = match (split_branch(&then_list), split_branch(&else_list)) {
9672        (Br::Call(tc, td, ti), Br::Call(ec, ed, ei)) => (
9673            Side::Call { at: tc, defs: td, def_idxs: ti },
9674            Side::Call { at: ec, defs: ed, def_idxs: ei },
9675            true,
9676        ),
9677        (Br::Call(tc, td, ti), Br::Clean) => (
9678            Side::Call { at: tc, defs: td, def_idxs: ti },
9679            Side::Clean,
9680            false,
9681        ),
9682        (Br::Clean, Br::Call(ec, ed, ei)) => (
9683            Side::Clean,
9684            Side::Call { at: ec, defs: ed, def_idxs: ei },
9685            false,
9686        ),
9687        _ => return false,
9688    };
9689    // The delegation nodes (two-sided: both; single: one).
9690    let (t_call, e_call) = match (&t_side, &e_side) {
9691        (Side::Call { at: ta, .. }, Side::Call { at: ea, .. }) => (
9692            ctor_call_expr(then_list[*ta]),
9693            ctor_call_expr(else_list[*ea]),
9694        ),
9695        (Side::Call { at: ta, .. }, Side::Clean) => {
9696            (ctor_call_expr(then_list[*ta]), None)
9697        }
9698        (Side::Clean, Side::Call { at: ea, .. }) => {
9699            (None, ctor_call_expr(else_list[*ea]))
9700        }
9701        _ => return false,
9702    };
9703    // The node that becomes the merged call: then side when present.
9704    let (call_list, call_at) = match &t_side {
9705        Side::Call { at, .. } => (&then_list, *at),
9706        Side::Clean => match &e_side {
9707            Side::Call { at, .. } => (&else_list, *at),
9708            Side::Clean => return false,
9709        },
9710    };
9711    let (base_args, other_args) = if two_sided {
9712        let (Some(tc), Some(ec)) = (t_call, e_call) else {
9713            return false;
9714        };
9715        if !same_ctor_kind(tc, ec) {
9716            return false;
9717        }
9718        let (Expr::Method { args: a1, .. }, Expr::Method { args: a2, .. }) = (tc, ec) else {
9719            return false;
9720        };
9721        if a1.len() != a2.len() {
9722            return false;
9723        }
9724        (a1, Some(a2))
9725    } else {
9726        let c = t_call.or(e_call);
9727        let Some(c) = c else { return false };
9728        let Expr::Method { args, .. } = c else {
9729            return false;
9730        };
9731        (args, None)
9732    };
9733
9734    // Def maps per side (prelude + own branch defs).
9735    let mut tmap: std::collections::HashMap<u32, Expr> = pmap.clone();
9736    let mut emap: std::collections::HashMap<u32, Expr> = pmap.clone();
9737    if let Side::Call { defs, .. } = &t_side {
9738        for (v, e) in defs {
9739            tmap.insert(*v, e.clone());
9740        }
9741    }
9742    if let Side::Call { defs, .. } = &e_side {
9743        for (v, e) in defs {
9744            emap.insert(*v, e.clone());
9745        }
9746    }
9747    let Some(cond_i) = inline_locals(&cond, &pmap, 0) else {
9748        return false;
9749    };
9750    // Inline each side's args through its own map.
9751    let mut then_args: Vec<Expr> = Vec::with_capacity(base_args.len());
9752    let mut else_args: Vec<Expr> = Vec::new();
9753    if two_sided {
9754        for a in base_args {
9755            match inline_locals(a, &tmap, 0) {
9756                Some(r) => then_args.push(r),
9757                None => return false,
9758            }
9759        }
9760        for a in other_args.unwrap() {
9761            match inline_locals(a, &emap, 0) {
9762                Some(r) => else_args.push(r),
9763                None => return false,
9764            }
9765        }
9766    } else {
9767        // Single-delegation side: base_args belong to whichever side
9768        // carries the call; pick that side's map.
9769        let from_then = matches!(&t_side, Side::Call { .. });
9770        for a in base_args {
9771            let r = if from_then {
9772                inline_locals(a, &tmap, 0)
9773            } else {
9774                inline_locals(a, &emap, 0)
9775            };
9776            match r {
9777                Some(r) => then_args.push(r),
9778                None => return false,
9779            }
9780        }
9781    }
9782    let primary_args = base_args;
9783    // Per-position merge.
9784    let mut merged: Vec<Expr> = Vec::with_capacity(then_args.len());
9785    let mut differ = 0usize;
9786    for (j, t) in then_args.iter().enumerate() {
9787        if two_sided {
9788            let e = &else_args[j];
9789            if t == e {
9790                merged.push(t.clone());
9791            } else {
9792                differ += 1;
9793                merged.push(Expr::Cond {
9794                    c: Box::new(cond_i.clone()),
9795                    t: Box::new(t.clone()),
9796                    f: Box::new(e.clone()),
9797                });
9798            }
9799        } else {
9800            merged.push(t.clone());
9801        }
9802    }
9803
9804    // ---- side-effect preservation guards (batched counts) ----
9805    let vars_in = |e: &Expr| -> Vec<u32> {
9806        let mut vs = Vec::new();
9807        let mut probe = e.clone();
9808        strip_lost_alloc_owners(&mut probe);
9809        deep_rewrite(&mut probe, &mut |x| {
9810            if let Expr::Local { var, .. } = x {
9811                if !vs.contains(var) {
9812                    vs.push(*var);
9813                }
9814            }
9815        });
9816        vs
9817    };
9818    // Remainder statements per branch (extras + post-call tail; a Clean
9819    // branch contributes all of its statements).
9820    // The consumed defs LEAVE the remainder: their expressions were
9821    // inlined into the merged delegation args, and a dead `v = E` left
9822    // after the call both reads wrong and trips the u_t1/u_t2 guards
9823    // (ContinuationImpl's branch-local `context = null / getContext()`
9824    // — the coroutine family's 对this的调用必须是第一个语句 root).
9825    let branch_remainder = |list: &[&Stmt], side: &Side| -> Vec<Stmt> {
9826        match side {
9827            Side::Clean => list.iter().map(|s| (*s).clone()).collect(),
9828            Side::Call { at, def_idxs, .. } => {
9829                list.iter()
9830                    .enumerate()
9831                    .filter(|(i, _)| *i != *at && !def_idxs.contains(i))
9832                    .map(|(_, s)| (*s).clone())
9833                    .collect()
9834            }
9835        }
9836    };
9837    let tail_then_v = branch_remainder(&then_list, &t_side);
9838    let tail_else_v = branch_remainder(&else_list, &e_side);
9839    let post = &stmts[if_pos + 1..];
9840    let mut inlined: Vec<u32> = vars_in(&cond);
9841    for a in primary_args.iter().chain(else_args.iter()) {
9842        for v in vars_in(a) {
9843            if !inlined.contains(&v) {
9844                inlined.push(v);
9845            }
9846        }
9847    }
9848    inlined.retain(|v| pmap.contains_key(v) || tmap.contains_key(v) || emap.contains_key(v));
9849    // Prelude-and-branch redefinition: ambiguous provenance — reject.
9850    for &v in &inlined {
9851        let in_branch = match (&t_side, &e_side) {
9852            (Side::Call { defs: td, .. }, Side::Call { defs: ed, .. }) => {
9853                td.iter().any(|(d, _)| *d == v) || ed.iter().any(|(d, _)| *d == v)
9854            }
9855            (Side::Call { defs, .. }, Side::Clean)
9856            | (Side::Clean, Side::Call { defs, .. }) => {
9857                defs.iter().any(|(d, _)| *d == v)
9858            }
9859            _ => false,
9860        };
9861        if pmap.contains_key(&v) && in_branch {
9862            return false;
9863        }
9864    }
9865    type Counts = std::collections::HashMap<u32, usize>;
9866    type GuardCounts = (
9867        Counts,
9868        Vec<Counts>,
9869        Vec<Counts>,
9870        Counts,
9871        Counts,
9872        Counts,
9873        Vec<(u32, Counts)>,
9874    );
9875    let (cond_counts, a1_counts, a2_counts, tail1_counts, tail2_counts, post_counts, def_counts): GuardCounts =
9876        if inlined.is_empty() {
9877        Default::default()
9878    } else {
9879        let all_defs: Vec<(u32, &Expr)> = pmap
9880            .iter()
9881            .map(|(k, v)| (*k, v))
9882            .chain(match &t_side {
9883                Side::Call { defs, .. } => defs.iter().map(|(k, v)| (*k, v)).collect(),
9884                Side::Clean => Vec::new(),
9885            })
9886            .chain(match &e_side {
9887                Side::Call { defs, .. } => defs.iter().map(|(k, v)| (*k, v)).collect(),
9888                Side::Clean => Vec::new(),
9889            })
9890            .collect();
9891        (
9892            count_locals_expr(&cond),
9893            primary_args.iter().map(count_locals_expr).collect(),
9894            else_args.iter().map(count_locals_expr).collect(),
9895            count_locals_stmts(&tail_then_v),
9896            count_locals_stmts(&tail_else_v),
9897            count_locals_stmts(post),
9898            all_defs.into_iter().map(|(v, e)| (v, count_locals_expr(e))).collect(),
9899        )
9900        };
9901    for &v in &inlined {
9902        let in_t = matches!(&t_side, Side::Call { defs, .. } if defs.iter().any(|(d, _)| *d == v));
9903        let in_e = matches!(&e_side, Side::Call { defs, .. } if defs.iter().any(|(d, _)| *d == v));
9904        let effectful = [
9905            in_t.then(|| tmap.get(&v)).flatten(),
9906            in_e.then(|| emap.get(&v)).flatten(),
9907            pmap.get(&v),
9908        ]
9909        .into_iter()
9910        .flatten()
9911        .any(jdc_core::ir::build::has_side_effects);
9912        if !effectful {
9913            continue;
9914        }
9915        let u_cond = cnt(&cond_counts, v);
9916        let u_post = cnt(&post_counts, v);
9917        let u_t1 = cnt(&tail1_counts, v);
9918        let u_t2 = cnt(&tail2_counts, v);
9919        let mut u_odef = 0usize;
9920        for (dv, dc) in &def_counts {
9921            if *dv != v && inlined.contains(dv) {
9922                u_odef += cnt(dc, v);
9923            }
9924        }
9925        let mut emit = u_cond * (1 + differ);
9926        let mut uses_args = 0usize;
9927        for j in 0..primary_args.len() {
9928            let c1 = cnt(&a1_counts[j], v);
9929            let c2 = if two_sided && j < a2_counts.len() {
9930                cnt(&a2_counts[j], v)
9931            } else {
9932                0
9933            };
9934            uses_args += c1 + c2;
9935            emit += if two_sided && then_args[j] != else_args[j] {
9936                c1 + c2
9937            } else {
9938                c1.max(c2)
9939            };
9940        }
9941        let ok = if in_t || in_e {
9942            u_cond == 0
9943                && u_post == 0
9944                && u_t1 == 0
9945                && u_t2 == 0
9946                && u_odef == 0
9947                && a1_counts.iter().map(|c| cnt(c, v)).sum::<usize>() <= 1
9948                && a2_counts.iter().map(|c| cnt(c, v)).sum::<usize>() <= 1
9949                && emit <= 2
9950                && (in_t || a1_counts.iter().all(|c| cnt(c, v) == 0))
9951                && (in_e || a2_counts.iter().all(|c| cnt(c, v) == 0))
9952        } else {
9953            u_post == 0
9954                && u_t1 == 0
9955                && u_t2 == 0
9956                && u_odef == 0
9957                && ((u_cond == 1 && uses_args == 0 && emit <= 2)
9958                    || (u_cond == 0 && emit <= 1))
9959        };
9960        if !ok {
9961            return false;
9962        }
9963    }
9964
9965    // ---- assembly ----
9966    let mut call_stmt = call_list[call_at].clone();
9967    if let Stmt::ExprStmt(Expr::Method { args: slot, .. }) = &mut call_stmt {
9968        *slot = merged;
9969    }
9970    let residual = |v: u32| -> usize {
9971        cnt(&tail1_counts, v) + cnt(&tail2_counts, v) + cnt(&post_counts, v)
9972    };
9973    let mut kept_prelude: Vec<Stmt> = Vec::new();
9974    for (i, s) in stmts[..if_pos].iter().enumerate() {
9975        let consumed = match s {
9976            Stmt::LocalDef { var, .. } => Some(*var),
9977            Stmt::ExprStmt(Expr::Assign { target, .. }) => match &**target {
9978                Expr::Local { var, .. } => Some(*var),
9979                _ => None,
9980            },
9981            Stmt::If { .. } => cond_def_of(s, stmts).map(|(v, _)| v),
9982            _ => None,
9983        };
9984        if let Some(v) = consumed {
9985            if inlined.contains(&v) && pidx.get(&v) == Some(&i) && residual(v) == 0 {
9986                continue;
9987            }
9988        }
9989        kept_prelude.push(s.clone());
9990    }
9991    let build_keep = |list: &[&Stmt], side: &Side| -> Vec<Stmt> {
9992        match side {
9993            Side::Clean => list.iter().map(|s| (*s).clone()).collect(),
9994            Side::Call { at, .. } => {
9995                let mut out = Vec::new();
9996                for (i, s) in list.iter().enumerate() {
9997                    if i == *at {
9998                        continue;
9999                    }
10000                    if i < *at {
10001                        if let Some((v, _)) = def_of(s) {
10002                            if inlined.contains(&v) && residual(v) == 0 {
10003                                continue;
10004                            }
10005                        }
10006                    }
10007                    out.push((*s).clone());
10008                }
10009                out
10010            }
10011        }
10012    };
10013    let then_keep = build_keep(&then_list, &t_side);
10014    let else_keep = build_keep(&else_list, &e_side);
10015    let else_part = if else_keep.is_empty() && !had_else {
10016        None
10017    } else {
10018        Some(Box::new(Stmt::Block(else_keep)))
10019    };
10020    let new_if = Stmt::If {
10021        cond: cond_i,
10022        then_stmt: Box::new(Stmt::Block(then_keep)),
10023        else_stmt: else_part,
10024    };
10025    let if_empty = match &new_if {
10026        Stmt::If { then_stmt, else_stmt, .. } => {
10027            let be = |s: &Stmt| matches!(s, Stmt::Block(v) if v.is_empty());
10028            be(then_stmt) && else_stmt.as_ref().is_none_or(|e| be(e))
10029        }
10030        _ => false,
10031    };
10032    let mut out: Vec<Stmt> = Vec::with_capacity(stmts.len());
10033    out.push(call_stmt);
10034    out.extend(kept_prelude);
10035    if !if_empty {
10036        out.push(new_if);
10037    }
10038    out.extend(stmts[if_pos + 1..].to_vec());
10039    *stmts = out;
10040    true
10041}
10042
10043
10044pub fn strip_enum_ctor_super(body: &mut Stmt) {
10045    let is_enum_super = |s: &Stmt| {
10046        matches!(
10047            s,
10048            Stmt::ExprStmt(Expr::Method { name, is_super: true, .. }) if &**name == "<init>"
10049        )
10050    };
10051    let Stmt::Block(stmts) = body else {
10052        return;
10053    };
10054    stmts.retain(|s| !is_enum_super(s));
10055    for st in stmts.iter_mut() {
10056        if let Stmt::Block(inner) = st {
10057            inner.retain(|s| !is_enum_super(s));
10058        }
10059    }
10060}
10061
10062pub fn ensure_declared(body: &mut Stmt, vt: &VarTable) {
10063    // (Perf: dense Vec<bool> tables instead of HashSets — var ids are
10064    // dense; the `assigned` set collected here was never read and cost a
10065    // full extra tree walk per method.)
10066    let n = vt.vars.len().max(1);
10067
10068    // Demote extra LocalDefs (same var declared more than once) FIRST —
10069    // the scope analysis below must see exactly one def per var.
10070    let mut seen: HashSet<u32> = HashSet::default();
10071    demote_dup_decls(body, &mut seen, vt);
10072
10073    // Scope hoist: a LocalDef inside a nested block whose var is also
10074    // referenced OUTSIDE that block is a Java scope violation (the
10075    // branch-local `int v8 = pi.versionCode;` read after the try —
10076    // "cannot find symbol" at every outside use). Demote those defs to
10077    // assignments and let the bare top-of-method declaration below cover
10078    // the var. Two cheap passes: pre-order block numbering + per-var
10079    // inside/outside occurrence counts (pre-order subtree = id range).
10080    let mut def_block: Vec<u32> = vec![u32::MAX; n];
10081    let mut sizes: Vec<u32> = vec![0];
10082    let mut next_id: u32 = 1;
10083    scope_pass_a(body, 0, &mut next_id, &mut def_block, &mut sizes);
10084    sizes[0] = next_id;
10085    let mut within: Vec<u32> = vec![0; n];
10086    let mut total: Vec<u32> = vec![0; n];
10087    let mut next_b: u32 = 1;
10088    scope_pass_b(body, 0, &mut next_b, &def_block, &sizes, &mut within, &mut total);
10089    let mut hoist: HashSet<u32> = HashSet::default();
10090    for v in 0..n {
10091        if def_block[v] != u32::MAX && def_block[v] != 0 && within[v] < total[v] {
10092            hoist.insert(v as u32);
10093        }
10094    }
10095    if !hoist.is_empty() {
10096        let mut seed: HashSet<u32> = hoist.iter().copied().collect();
10097        demote_dup_decls(body, &mut seed, vt);
10098    }
10099
10100    let mut declared: Vec<bool> = vec![false; n];
10101    let mut is_param: Vec<bool> = vec![false; n];
10102    for v in &vt.vars {
10103        if v.is_param && (v.id as usize) < n {
10104            is_param[v.id as usize] = true;
10105        }
10106    }
10107    // Catch parameters are declared by the `catch (Type v)` clause —
10108    // bind_catches consumed their LocalDef before this pass ran, so
10109    // without this they re-declare at the top of the method and clash
10110    // with the clause (`Throwable th2;` + `catch (Throwable th2)` — a
10111    // syntax gate cannot see this, it is a semantic "already defined").
10112    let mut is_catch: Vec<bool> = vec![false; n];
10113    walk_all(body, &mut |st| {
10114        if let Stmt::Try { catches, .. } = st {
10115            for c in catches {
10116                if (c.var as usize) < n {
10117                    is_catch[c.var as usize] = true;
10118                }
10119            }
10120        }
10121    });
10122    walk_all(body, &mut |st| {
10123        if let Stmt::LocalDef { var, .. } = st {
10124            if (*var as usize) < n {
10125                declared[*var as usize] = true;
10126            }
10127        }
10128    });
10129    let mut used: HashSet<u32> = HashSet::default();
10130    // assignments=true: a var that only ever appears as an assign
10131    // TARGET still needs a declaration (`x = 5;` alone does not
10132    // declare x in Java).
10133    stmt_collect_vars(body, &mut used, true);
10134
10135    // Params are declared by the signature; hoisted vars lost their
10136    // LocalDef to the demotion above and always need the bare decl.
10137    let mut needs_decl: Vec<u32> = used
10138        .into_iter()
10139        .filter(|v| {
10140            let i = *v as usize;
10141            i >= n
10142                || ((!declared[i] && !is_param[i] && !is_catch[i]) || hoist.contains(v))
10143        })
10144        .collect();
10145    needs_decl.sort_unstable();
10146    needs_decl.dedup();
10147
10148    if !needs_decl.is_empty() {
10149        let mut decls: Vec<Stmt> = needs_decl
10150            .into_iter()
10151            .map(|v| Stmt::LocalDef {
10152                var: v,
10153                init: None,
10154                is_final: false,
10155                force_type: true,
10156            })
10157            .collect();
10158        decls.reverse();
10159        match body {
10160            Stmt::Block(v) => {
10161                for d in decls {
10162                    v.insert(0, d);
10163                }
10164            }
10165            other => {
10166                let inner = std::mem::replace(other, Stmt::Block(vec![]));
10167                *other = Stmt::Block({
10168                    let mut vs = decls;
10169                    vs.push(inner);
10170                    vs
10171                });
10172            }
10173        }
10174    }
10175}
10176
10177fn demote_dup_decls(s: &mut Stmt, seen: &mut HashSet<u32>, vt: &VarTable) {
10178    match s {
10179        // Exactly one visit per node: the Block arm consumes its children,
10180        // the fallthrough walk would double-visit them.
10181        Stmt::Block(v) => {
10182            for x in v.iter_mut() {
10183                demote_dup_decls(x, seen, vt);
10184            }
10185        }
10186        Stmt::LocalDef { var, init, .. } => {
10187            if seen.contains(var) {
10188                let ty = vt.var(*var).ty.clone();
10189                if let Some(e) = init.take() {
10190                    *s = Stmt::ExprStmt(Expr::Assign {
10191                        target: Box::new(Expr::Local { var: *var, ty }),
10192                        op: AssignOp::Plain,
10193                        value: Box::new(e),
10194                    });
10195                } else {
10196                    *s = Stmt::Block(vec![]);
10197                }
10198            } else {
10199                seen.insert(*var);
10200            }
10201        }
10202        other => {
10203            walk_mut(other, &mut |x| demote_dup_decls(x, seen, vt));
10204        }
10205    }
10206}
10207
10208#[allow(dead_code)]
10209fn unused(_: &Vec<CaseGroup>, _: &Catch) {}
10210
10211// ---------------------------------------------------------------------------
10212// jadx-style local names (ApplyVariableNames + dexdec's port of it)
10213// ---------------------------------------------------------------------------
10214
10215/// A synthetic `v12`/`p3` never survives when a better name exists.
10216/// Priority: (1) a Kotlin `Intrinsics.checkNotNullParameter(x, "name")`
10217/// names `x` from the message string (the string IS the parameter name);
10218/// (2) the single defining call — `getFoo()` → `foo`, `isFinishing()` →
10219/// `finishing`, `new File(…)` → `file`; (3) a type alias or the
10220/// lowercased class simple name. Collisions take `2`, `3`, …; a var with
10221/// two DISAGREEING defining calls stays unnamed (dexdec's
10222/// RelationalNameInference rule). Debug-info names are never touched
10223/// (`synthetic_name` gates the whole pass).
10224pub fn apply_local_names(vt: &mut VarTable, body: &Stmt) {
10225    use std::collections::{HashMap, HashSet};
10226
10227    // ---- proposals ------------------------------------------------------
10228    // var → (name, source-priority); disagreeing call names are dropped.
10229    let mut by_call: HashMap<u32, Vec<String>> = HashMap::new();
10230    let mut by_intrinsics: HashMap<u32, String> = HashMap::new();
10231
10232    visit_all_exprs(body, &mut |e| {
10233        if let Expr::Method { cls, name, args, .. } = e {
10234            if (cls.as_ref() == "kotlin/jvm/internal/Intrinsics"
10235                || cls.as_ref() == "kotlin/jvm/internal/IntrinsicsKt")
10236                && matches!(name.as_ref(), "checkNotNullParameter" | "checkParameterIsNotNull")
10237                && args.len() == 2
10238            {
10239                if let (Expr::Local { var, .. }, Expr::Const(ConstVal::Str(s))) = (&args[0], &args[1])
10240                {
10241                    by_intrinsics
10242                        .entry(*var)
10243                        .or_insert_with(|| sanitize_name(s).unwrap_or_default());
10244                }
10245            }
10246        }
10247    });
10248
10249    walk_all(body, &mut |st| {
10250        let (var, value) = match st {
10251            Stmt::LocalDef { var, init: Some(e), .. } => (*var, e),
10252            Stmt::ExprStmt(Expr::Assign { target, value, .. })
10253                if matches!(&**target, Expr::Local { .. }) =>
10254            {
10255                let Expr::Local { var, .. } = &**target else { return };
10256                (*var, value.as_ref())
10257            }
10258            _ => return,
10259        };
10260        if let Some(n) = defining_call_name(value) {
10261            by_call.entry(var).or_default().push(n);
10262        }
10263    });
10264
10265    // ---- reservation + application --------------------------------------
10266    // Real (debug-info) names can legally REPEAT across sibling source
10267    // scopes (`int i` in two loops) but our flat declaration hoisting
10268    // puts them in one scope — de-duplicate with the same numeric
10269    // suffixes the claim path uses.
10270    // Uniqueness operates on the SANITIZED display name: the identifier
10271    // sanitizer maps non-ASCII to `_`, so distinct debug names
10272    // (`ERROR_token参数缺失` / `ERROR_channelId参数缺失`) both RENDER as
10273    // `ERROR________` and collide as declarations. Tracking raw names
10274    // here would let the collision through.
10275    let sanit = |n: &str| crate::classdec::java_ident(n).into_owned();
10276    let mut taken: HashSet<String> = HashSet::default();
10277    // Parameter names are occupied REGARDLESS of syntheticness: claim()
10278    // hands out rename candidates, and without the synthetic params in
10279    // the pool a type fallback (`l7.p0` → "p0") renamed one parameter
10280    // ONTO another's slot-name (`b(v6.l p0, Object obj, l7.p0 p0)`).
10281    for v in vt.vars.iter() {
10282        // ALL synthetic names occupy their names, not just parameters:
10283        // a type fallback (`pc5.v56` → "v56") otherwise renames a local
10284        // ONTO another local's slot-name (`int v56;` beside
10285        // `pc5.v56 v56;`).
10286        if v.synthetic_name {
10287            taken.insert(sanit(&v.name));
10288        }
10289    }
10290    for v in vt.vars.iter_mut() {
10291        if v.synthetic_name {
10292            continue;
10293        }
10294        if !taken.insert(sanit(&v.name)) {
10295            let base = sanit(&v.name);
10296            for i in 2.. {
10297                let cand = format!("{base}{i}");
10298                if taken.insert(cand.clone()) {
10299                    v.name = cand;
10300                    break;
10301                }
10302            }
10303        }
10304    }
10305    let claim = |taken: &mut HashSet<String>, want: &str| -> String {
10306        let w = crate::classdec::java_ident(want).into_owned();
10307        if taken.insert(w.clone()) {
10308            return w;
10309        }
10310        for i in 2.. {
10311            let cand = format!("{w}{i}");
10312            if taken.insert(cand.clone()) {
10313                return cand;
10314            }
10315        }
10316        w
10317    };
10318
10319    for info in vt.vars.iter_mut() {
10320        if !info.synthetic_name {
10321            continue;
10322        }
10323        // (1) Intrinsics string — the Kotlin compiler wrote the real
10324        // parameter name right into the check.
10325        if let Some(n) = by_intrinsics.get(&info.id) {
10326            if !n.is_empty() {
10327                info.name = claim(&mut taken, n);
10328                continue;
10329            }
10330        }
10331        // (2) One consistent defining call.
10332        if let Some(names) = by_call.get(&info.id) {
10333            let unique: HashSet<&String> = names.iter().collect();
10334            if unique.len() == 1 {
10335                let n = unique.into_iter().next().unwrap();
10336                if !is_java_keyword(n) {
10337                    info.name = claim(&mut taken, n);
10338                    continue;
10339                }
10340            }
10341        }
10342        // (3) Type alias, else the lowercased simple class name
10343        // (jadx names a `Looper` local `looper`).
10344        let want = type_alias(&info.ty).map(str::to_string).or_else(|| simple_type_name(&info.ty));
10345        if let Some(n) = want {
10346            info.name = claim(&mut taken, &n);
10347        }
10348    }
10349
10350    // Final uniquification across ALL vars (params, locals, catch): the
10351    // reservation, claim and debug-name domains above are checked
10352    // pairwise but a name claimed LAST can still equal a debug name
10353    // that never re-checked (`zd1.v2 v2` beside `zd1.v2[] v2`).
10354    let mut seen: HashSet<String> = HashSet::default();
10355    for v in vt.vars.iter_mut() {
10356        if !seen.insert(sanit(&v.name)) {
10357            let base = sanit(&v.name);
10358            for i in 2.. {
10359                let cand = format!("{base}{i}");
10360                if seen.insert(cand.clone()) {
10361                    v.name = cand;
10362                    break;
10363                }
10364            }
10365        }
10366    }
10367}
10368
10369/// `com/android/.../Looper` → `looper` — the alias-table fallback.
10370fn simple_type_name(ty: &TypeRef) -> Option<String> {
10371    let TypeRef::J(JavaType::Object(n)) = ty else {
10372        return None;
10373    };
10374    let simple = n.rsplit('/').next().unwrap_or(n);
10375    let simple = simple.rsplit('$').next().unwrap_or(simple);
10376    // Lowercase the first ALPHABETIC char — `$`/`_`-prefixed names
10377    // (R8's `$$$_Thread`) must not survive capitalized.
10378    let mut chars = simple.chars();
10379    let mut out = String::with_capacity(simple.len());
10380    let mut lowered = false;
10381    for c in chars.by_ref() {
10382        if c.is_ascii_alphabetic() && !lowered {
10383            out.push(c.to_ascii_lowercase());
10384            lowered = true;
10385        } else {
10386            out.push(c);
10387        }
10388        if lowered {
10389            break;
10390        }
10391    }
10392    out.extend(chars);
10393    sanitize_name(&out)
10394}
10395
10396/// `getFoo()` → `foo`, `isFinishing()` → `finishing`, `new File(…)` →
10397/// `file`. The get/is prefixes only strip when the remainder starts
10398/// uppercase (so `issues()` keeps its name).
10399fn defining_call_name(e: &Expr) -> Option<String> {
10400    match e {
10401        Expr::Method { name, .. } => {
10402            let base = name
10403                .strip_prefix("get")
10404                .or_else(|| name.strip_prefix("is"))
10405                .filter(|rest| rest.chars().next().is_some_and(|c| c.is_ascii_uppercase()))
10406                .unwrap_or(name);
10407            let first = base.chars().next()?;
10408            let mut out = String::with_capacity(base.len());
10409            out.push(first.to_ascii_lowercase());
10410            out.extend(base.chars().skip(1));
10411            sanitize_name(&out)
10412        }
10413        Expr::New { cls, .. } => {
10414            let simple = cls.rsplit('/').next().unwrap_or(cls);
10415            let simple = simple.rsplit('$').next().unwrap_or(simple);
10416            let first = simple.chars().next()?;
10417            let mut out = String::with_capacity(simple.len());
10418            out.push(first.to_ascii_lowercase());
10419            out.extend(simple.chars().skip(1));
10420            sanitize_name(&out)
10421        }
10422        _ => None,
10423    }
10424}
10425
10426/// Valid java identifier, not a keyword/restricted name, ≥2 chars.
10427fn sanitize_name(s: &str) -> Option<String> {
10428    let mut out = String::with_capacity(s.len());
10429    for c in s.chars() {
10430        if c.is_ascii_alphanumeric() || c == '_' || c == '$' {
10431            out.push(c);
10432        } else {
10433            out.push('_');
10434        }
10435    }
10436    if out.len() < 2
10437        || out.chars().next().is_some_and(|c| c.is_ascii_digit())
10438        || is_java_keyword(&out)
10439    {
10440        return None;
10441    }
10442    Some(out)
10443}
10444
10445/// jadx's alias table for the common types, with the Android globals the
10446/// corpus actually shows; anything else falls back in the caller.
10447fn type_alias(ty: &TypeRef) -> Option<&'static str> {
10448    let TypeRef::J(JavaType::Object(n)) = ty else {
10449        return None;
10450    };
10451    Some(match n.as_ref() {
10452        "java/lang/String" | "kotlin/String" => "str",
10453        "java/lang/Class" => "cls",
10454        "java/lang/Throwable" => "th",
10455        "java/lang/Object" | "kotlin/Any" => "obj",
10456        "java/util/Iterator" | "kotlin/collections/Iterator" => "it",
10457        "java/lang/Boolean" => "bool",
10458        "java/lang/Integer" => "num",
10459        "java/lang/Character" => "ch",
10460        "java/lang/Byte" => "b",
10461        "java/lang/Short" => "sh",
10462        "java/lang/Float" => "f",
10463        "java/lang/Double" => "d",
10464        "java/lang/Long" => "i",
10465        "java/lang/StringBuilder" => "sb",
10466        "java/util/ArrayList" => "list",
10467        "java/util/HashMap" => "map",
10468        "android/content/Context" => "context",
10469        "android/content/Intent" => "intent",
10470        "android/os/Bundle" => "bundle",
10471        "android/view/View" => "view",
10472        "android/graphics/Bitmap" => "bitmap",
10473        "android/view/ViewGroup" => "viewGroup",
10474        _ => return None,
10475    })
10476}
10477
10478/// Every expression in the tree, statements included (read-only).
10479pub(crate) fn visit_all_exprs<F: FnMut(&Expr)>(s: &Stmt, f: &mut F) {
10480    walk_all(s, &mut |st| {
10481        let exprs: Vec<&Expr> = match st {
10482            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
10483                let mut v: Vec<&Expr> = vec![value];
10484                if !matches!(&**target, Expr::Local { .. }) {
10485                    v.push(target);
10486                }
10487                v
10488            }
10489            Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
10490                vec![e]
10491            }
10492            Stmt::Return(Some(e)) => vec![e],
10493            Stmt::LocalDef { init: Some(e), .. } => vec![e],
10494            Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
10495                vec![cond]
10496            }
10497            _ => Vec::new(),
10498        };
10499        for e in exprs {
10500            visit_exprs(e, f);
10501        }
10502    });
10503}
10504
10505/// IntDef/LongDef exact-match rendering: a literal argument to a
10506/// platform method whose parameter carries a constant domain renders as
10507/// the named constant (`setVisibility(8)` → `android.view.View.GONE`).
10508/// Combined flag values and literals outside the domain stay numeric.
10509pub fn platform_constants(body: &mut Stmt) {
10510    if !crate::platform::installed() {
10511        return;
10512    }
10513    rewrite_exprs(body, &mut |e| {
10514        if let Expr::Method { cls, name, desc, args, .. } = e {
10515            for a in args.iter_mut() {
10516                let (value, is_long) = match a {
10517                    Expr::Const(ConstVal::Int(v)) => (*v as i64, false),
10518                    Expr::Const(ConstVal::Long(v)) => (*v, true),
10519                    _ => continue,
10520                };
10521                if let Some(named) =
10522                    crate::platform::named_constant(cls, name, &desc_to_string(desc), 0, value)
10523                {
10524                    let _ = is_long;
10525                    *a = Expr::Raw(named);
10526                }
10527            }
10528        }
10529    });
10530}
10531
10532/// MethodDescriptor → dex descriptor string (`(I)V`).
10533fn desc_to_string(d: &jdc_core::types::MethodDescriptor) -> String {
10534    let mut out = String::from("(");
10535    for a in &d.args {
10536        type_to_desc(a, &mut out);
10537    }
10538    out.push(')');
10539    type_to_desc(&d.ret, &mut out);
10540    out
10541}
10542
10543fn type_to_desc(t: &jdc_core::types::JavaType, out: &mut String) {
10544    match t {
10545        JavaType::Void => out.push('V'),
10546        JavaType::Boolean => out.push('Z'),
10547        JavaType::Byte => out.push('B'),
10548        JavaType::Char => out.push('C'),
10549        JavaType::Short => out.push('S'),
10550        JavaType::Int => out.push('I'),
10551        JavaType::Float => out.push('F'),
10552        JavaType::Long => out.push('J'),
10553        JavaType::Double => out.push('D'),
10554        JavaType::Object(n) => {
10555            out.push('L');
10556            out.push_str(n);
10557            out.push(';');
10558        }
10559        JavaType::Array(inner) => {
10560            out.push('[');
10561            type_to_desc(inner, out);
10562        }
10563    }
10564}
10565
10566/// Kotlin `Intrinsics` null-check elision (jadx's ProcessKotlinInternals):
10567/// statement-position `checkNotNullParameter(p, "name")` /
10568/// `checkParameterIsNotNull` / `checkNotNull…` calls are runtime
10569/// assertions — drop them. The naming pass harvests the parameter
10570/// strings FIRST, so run this after `apply_local_names`.
10571pub fn remove_kotlin_checks(s: &mut Stmt) {
10572    walk_mut_deep(s, &mut |st| {
10573        let Stmt::Block(v) = st else { return };
10574        v.retain(|x| !is_kotlin_check(x));
10575    });
10576}
10577
10578fn is_kotlin_check(s: &Stmt) -> bool {
10579    let Stmt::ExprStmt(e) = s else { return false };
10580    let Expr::Method { cls, name, .. } = e else { return false };
10581    (cls.as_ref() == "kotlin/jvm/internal/Intrinsics" || cls.as_ref() == "kotlin/jvm/internal/IntrinsicsKt__Jdk7Kt")
10582        && matches!(
10583            name.as_ref(),
10584            "checkNotNullParameter"
10585                | "checkParameterIsNotNull"
10586                | "checkExpressionValueIsNotNull"
10587                | "checkNotNullExpressionValue"
10588                | "checkReturnedValueIsNotNull"
10589                | "checkFieldIsNotNull"
10590                | "checkNotNull"
10591        )
10592}
10593
10594// ---------------------------------------------------------------------------
10595// Synthetic-accessor inlining (jadx's MarkMethodsForInline, the safe subset)
10596// ---------------------------------------------------------------------------
10597
10598/// Inline `access$NNN`-style synthetic static bridges at their call
10599/// sites: an identity (`return pN`), a getter (`iget pX, field;
10600/// return vR`), or a method forwarder (`invoke; return`) — with the
10601/// d8 APM trace wrappers (`MethodCollector.i/.o` const-only static
10602/// calls) tolerated around the core. Only STATIC + SYNTHETIC callees
10603/// qualify (compiler-generated bridges: no override semantics, no
10604/// side effects beyond the forwarded operation).
10605/// Can `host` (internal class name) source-level reference `target`'s
10606/// class chain and member? Cross-package inlining of a synthetic
10607/// accessor used to EXPOSE package-private targets the raw dex never
10608/// referenced cross-package (BuildersKt.launch$default forwards to
10609/// kotlinx.coroutines.k.e — the widening census cannot see post-inline
10610/// calls; lark's 1,019-line k不是公共的 family). Non-public chain link
10611/// or non-public member across packages → the inline must not happen
10612/// (the public synthetic bridge stays and compiles).
10613fn inline_ref_ok(pool: &DexPool, host: &str, target: &str, member: Option<(&str, bool)>) -> bool {
10614    let hpkg = match host.rfind('/') {
10615        Some(i) => &host[..i],
10616        None => "",
10617    };
10618    let mut cur = target;
10619    loop {
10620        let tpkg = match cur.rfind('/') {
10621            Some(i) => &cur[..i],
10622            None => "",
10623        };
10624        if tpkg != hpkg {
10625            if let Some(pc) = pool.get(cur) {
10626                if pc.access & crate::access::ACC_PUBLIC == 0 {
10627                    return false;
10628                }
10629                if cur == target {
10630                    if let Some((mname, is_method)) = member {
10631                        let ok = if is_method {
10632                            pc.all_methods().any(|m| {
10633                                &*m.name == mname
10634                                    && m.access & crate::access::ACC_PUBLIC != 0
10635                            })
10636                        } else {
10637                            pc.static_fields
10638                                .iter()
10639                                .chain(pc.instance_fields.iter())
10640                                .any(|f| {
10641                                    f.name == mname
10642                                        && f.access & crate::access::ACC_PUBLIC != 0
10643                                })
10644                        };
10645                        // A member match requires the exact member to be
10646                        // public; an absent member (renamed/collapsed)
10647                        // stays allowed — the render will show what the
10648                        // pool has.
10649                        if !ok
10650                            && (if is_method {
10651                                pc.all_methods().any(|m| &*m.name == mname)
10652                            } else {
10653                                pc.static_fields
10654                                    .iter()
10655                                    .chain(pc.instance_fields.iter())
10656                                    .any(|f| f.name == mname)
10657                            })
10658                        {
10659                            return false;
10660                        }
10661                    }
10662                }
10663            }
10664        }
10665        match cur.rfind('$') {
10666            Some(i) if i > 0 => cur = &cur[..i],
10667            _ => break,
10668        }
10669    }
10670    true
10671}
10672
10673pub fn inline_accessors(s: &mut Stmt, pool: &DexPool, host: &str) {
10674    rewrite_exprs(s, &mut |e| {
10675        let Expr::Method { cls, name, desc, args, is_static, .. } = e else { return };
10676        if !*is_static || args.is_empty() {
10677            return;
10678        }
10679        let Some(target) = pool.get(cls) else { return };
10680        let desc_s = desc_to_string(desc);
10681        let Some(m) = target.find_method(name, &desc_s) else { return };
10682        if !(m.is_static() && m.access & access::ACC_SYNTHETIC != 0) {
10683            return;
10684        }
10685        let Some(dex) = pool.dex(m.dex_idx) else { return };
10686        // Snapshot first: the accessor's owning image may already be
10687        // retired, and a live read's success would depend on worker
10688        // completion interleaving — nondeterministic inlining (whether
10689        // the accessor folds at all) between identical runs.
10690        let code = match pool.accessor_code(m.dex_idx, m.code_off) {
10691            Some(bytes) => ddc_dex::CodeItem::parse(&bytes, 0),
10692            None => dex.code_at(m.code_off),
10693        };
10694        let Some(code) = code else { return };
10695        let insns: Vec<&Insn> =
10696            code.insns.iter().filter(|i| !matches!(i.kind, InsnKind::Nop)).collect();
10697        // Strip the APM trace wrappers (const-only invoke-static).
10698        let core: Vec<&Insn> = insns
10699            .iter()
10700            .copied()
10701            .filter(|i| !is_const_only_trace(&i.kind, &insns))
10702            .collect();
10703        match accessor_shape(&dex, &core, &code) {
10704            Some(Shape::Identity(arg_i)) => {
10705                if let Some(a) = args.get(arg_i) {
10706                    *e = a.clone();
10707                }
10708            }
10709            Some(Shape::FieldRead { arg, cls: field_cls, field, field_ty }) => {
10710                if !inline_ref_ok(pool, host, &field_cls, Some((&field, false))) {
10711                    return;
10712                }
10713                if let Some(owner) = args.get(arg).cloned().map(Box::new) {
10714                    let ty = TypeRef::J(desc_type(&field_ty));
10715                    // The inlined read must consult the member rename
10716                    // registry like every lifted field ref does — the
10717                    // raw dex name desynced from renamed declarations
10718                    // (lark uu4/f$b's `a`→`ax6`: 找不到符号 变量 a ×4.4k
10719                    // when the nested-collision deshadow fired).
10720                    let name = jdc_core::rename::field_display(
10721                        &field_cls,
10722                        &field,
10723                        &field_ty,
10724                    )
10725                    .map(std::sync::Arc::from)
10726                    .unwrap_or_else(|| field.into());
10727                    *e = Expr::Field {
10728                        owner: Some(owner),
10729                        cls: field_cls.into(),
10730                        name,
10731                        ty,
10732                        is_static: false,
10733                    };
10734                }
10735            }
10736            Some(Shape::Forward { cls: tcls, name: tname, desc: tdesc, instance }) => {
10737                if !inline_ref_ok(pool, host, &tcls, Some((&tname, true))) {
10738                    return;
10739                }
10740                // Same registry duty as FieldRead: the forwarded member
10741                // must render under its renamed display, not the raw
10742                // dex name.
10743                let disp = jdc_core::rename::field_display(
10744                    &tcls,
10745                    &tname,
10746                    &tdesc.to_string(),
10747                )
10748                .map(std::sync::Arc::from)
10749                .unwrap_or_else(|| tname.into());
10750                *cls = tcls.into();
10751                *name = disp;
10752                *desc = std::sync::Arc::new(tdesc);
10753                if instance && !args.is_empty() {
10754                    // The first param (the receiver) becomes the owner.
10755                    let recv = args.remove(0);
10756                    if let Expr::Method { owner, .. } = e {
10757                        *owner = Some(Box::new(recv));
10758                    }
10759                }
10760            }
10761            None => {}
10762        }
10763    });
10764}
10765
10766/// What an accessor body reduces to.
10767enum Shape {
10768    /// `return pN` — the call becomes argument N.
10769    Identity(usize),
10770    /// `iget vR, pX, field; return vR` — becomes `argN.field`.
10771    FieldRead { arg: usize, cls: String, field: String, field_ty: String },
10772    /// `invoke {pX, args…}, method@M; (move-result;)? return` —
10773    /// becomes the forwarded call.
10774    Forward { cls: String, name: String, desc: MethodDescriptor, instance: bool },
10775}
10776
10777/// Resolve the IGet field's owner class, name and type descriptor.
10778fn field_of(dex: &ddc_dex::DexFile, field_idx: u32) -> Option<(String, String, String)> {
10779    let f = dex.field(field_idx);
10780    Some((
10781        // class_name strips the `L...;` shell; type_name would leak a
10782        // descriptor into the Field's owner class (`La3.a.e_`).
10783        dex.class_name(f.class_idx),
10784        dex.string(f.name_idx).to_string(),
10785        // The FIELD TYPE stays a descriptor: desc_type parses it.
10786        dex.type_name(f.type_idx).to_string(),
10787    ))
10788}
10789
10790/// Map a register to a parameter index (static method: params occupy
10791/// the LAST ins_size registers).
10792fn param_index(code: &ddc_dex::CodeItem, reg: u16) -> Option<usize> {
10793    let rs = code.registers_size as usize;
10794    let ins = code.ins_size as usize;
10795    let r = reg as usize;
10796    let first = rs.checked_sub(ins)?;
10797    (r >= first && r < rs).then(|| r - first)
10798}
10799
10800fn accessor_shape(
10801    dex: &ddc_dex::DexFile,
10802    core: &[&Insn],
10803    code: &ddc_dex::CodeItem,
10804) -> Option<Shape> {
10805    // Dead consts that fed the stripped trace wrappers remain in the
10806    // core — skip them.
10807    let core: &[&Insn] = match core.split_first() {
10808        Some((first, rest)) if matches!(first.kind, InsnKind::Const { .. }) => rest,
10809        _ => core,
10810    };
10811    let core: &[&Insn] = match core.split_last() {
10812        Some((last, rest)) if matches!(last.kind, InsnKind::Const { .. }) => rest,
10813        _ => core,
10814    };
10815    match core {
10816        // return pN
10817        [Insn { kind: InsnKind::Return { src }, .. }] => {
10818            let idx = param_index(code, *src)?;
10819            Some(Shape::Identity(idx))
10820        }
10821        // iget vR, pX, field; return vR
10822        [
10823            Insn { kind: InsnKind::IGet { dst, obj, field_idx }, .. },
10824            Insn { kind: InsnKind::Return { src }, .. },
10825        ] if dst == src => {
10826            let arg = param_index(code, *obj)?;
10827            let (cls, field, field_ty) = field_of(dex, *field_idx)?;
10828            Some(Shape::FieldRead { arg, cls, field, field_ty })
10829        }
10830        // invoke {…}, method@M; (move-result; return)? — forwarder.
10831        [Insn { kind: InsnKind::Invoke { method_idx, regs, kind, .. }, .. }, rest @ ..] if rest.len() <= 2 => {
10832            // Only forward when every register is a param (no locals).
10833            if !regs.iter().all(|r| param_index(code, *r).is_some()) || regs.is_empty() {
10834                return None;
10835            }
10836            // Trailing must be move-result+return or return/void —
10837            // anything else (e.g. trace calls) already filtered upstream.
10838            if rest.len() == 2 {
10839                let ok = matches!(rest[0].kind, InsnKind::MoveResult { .. })
10840                    && matches!(rest[1].kind, InsnKind::Return { .. } | InsnKind::ReturnVoid);
10841                if !ok {
10842                    return None;
10843                }
10844            }
10845            let mid = dex.method(*method_idx);
10846            // class_name strips the `L...;` shell — type_name leaks a
10847            // descriptor into the forwarded call's class (`La3.a.e_`).
10848            let cls = dex.class_name(mid.class_idx);
10849            let name = dex.string(mid.name_idx).to_string();
10850            let mut s = String::from("(");
10851            for t in dex.proto_params(mid.proto_idx) {
10852                s.push_str(dex.type_name(*t));
10853            }
10854            s.push(')');
10855            s.push_str(dex.type_name(dex.proto(mid.proto_idx).return_type_idx));
10856            let desc = parse_method_descriptor(&s)?;
10857            Some(Shape::Forward { cls, name, desc, instance: !matches!(kind, InvokeKind::Static) })
10858        }
10859        _ => None,
10860    }
10861}
10862
10863
10864/// A const-only invoke-static (the d8 APM `i(732046)` / `o(732046)`
10865/// trace wrapper): every argument register is loaded by a Const (or a
10866/// Const-into-move chain) in the SAME body. Scoped to synthetic
10867/// accessors, so a genuinely side-effecting const call is never
10868/// mistaken for a trace.
10869fn is_const_only_trace(kind: &InsnKind, insns: &[&Insn]) -> bool {
10870    let InsnKind::Invoke { kind, regs, .. } = kind else { return false };
10871    if !matches!(kind, InvokeKind::Static) || regs.is_empty() {
10872        return false;
10873    }
10874    regs.iter().all(|r| const_loaded(*r, insns, 0))
10875}
10876
10877/// Const-loaded directly, or through a Move chain from a const-loaded
10878/// register (the APM `const v0, id` → `invoke {v0}` shape).
10879fn const_loaded(r: u16, insns: &[&Insn], depth: u8) -> bool {
10880    if depth > 3 {
10881        return false;
10882    }
10883    insns.iter().any(|i| match &i.kind {
10884        InsnKind::Const { dst, .. } => *dst == r,
10885        InsnKind::Move { dst, src } => *dst == r && const_loaded(*src, insns, depth + 1),
10886        _ => false,
10887    })
10888}