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/// Immutable mirror of `walk_stmt_exprs`: read-only visitors must not
164/// have to clone a statement tree just to obtain a `&mut` handle.
165/// (count_locals_stmts cloned EVERY counted statement — 29k of 50k
166/// Stmt::clone samples in corpus profiles, ~25% of total CPU.)
167pub(crate) fn visit_stmt_exprs_ro<F: FnMut(&Expr)>(s: &Stmt, f: &mut F) {
168    match s {
169        Stmt::Block(v) => {
170            for x in v {
171                visit_stmt_exprs_ro(x, f);
172            }
173        }
174        Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
175            f(e);
176        }
177        Stmt::Return(Some(e)) => f(e),
178        Stmt::LocalDef { init: Some(e), .. } => f(e),
179        Stmt::If {
180            cond,
181            then_stmt,
182            else_stmt,
183        } => {
184            f(cond);
185            visit_stmt_exprs_ro(then_stmt, f);
186            if let Some(e) = else_stmt {
187                visit_stmt_exprs_ro(e, f);
188            }
189        }
190        Stmt::While { cond, body } => {
191            f(cond);
192            visit_stmt_exprs_ro(body, f);
193        }
194        Stmt::DoWhile { body, cond } => {
195            visit_stmt_exprs_ro(body, f);
196            f(cond);
197        }
198        Stmt::For {
199            init,
200            cond,
201            update,
202            body,
203        } => {
204            for x in init {
205                visit_stmt_exprs_ro(x, f);
206            }
207            if let Some(c) = cond {
208                f(c);
209            }
210            for u in update {
211                f(u);
212            }
213            visit_stmt_exprs_ro(body, f);
214        }
215        Stmt::ForEach { iterable, body, .. } => {
216            f(iterable);
217            visit_stmt_exprs_ro(body, f);
218        }
219        Stmt::Switch {
220            selector,
221            cases,
222            default,
223            ..
224        } => {
225            f(selector);
226            for c in cases {
227                for x in &c.body {
228                    visit_stmt_exprs_ro(x, f);
229                }
230                if let Some(g) = &c.guard {
231                    f(g);
232                }
233            }
234            if let Some(d) = default {
235                visit_stmt_exprs_ro(d, f);
236            }
237        }
238        Stmt::Try {
239            body,
240            catches,
241            finally,
242        } => {
243            visit_stmt_exprs_ro(body, f);
244            for c in catches {
245                visit_stmt_exprs_ro(&c.body, f);
246            }
247            if let Some(fl) = finally {
248                visit_stmt_exprs_ro(fl, f);
249            }
250        }
251        Stmt::TryWithResources {
252            resources,
253            body,
254            catches,
255            finally,
256            ..
257        } => {
258            for x in resources {
259                visit_stmt_exprs_ro(x, f);
260            }
261            visit_stmt_exprs_ro(body, f);
262            for c in catches {
263                visit_stmt_exprs_ro(&c.body, f);
264            }
265            if let Some(fl) = finally {
266                visit_stmt_exprs_ro(fl, f);
267            }
268        }
269        Stmt::Assert { cond, msg } => {
270            f(cond);
271            if let Some(m) = msg {
272                f(m);
273            }
274        }
275        Stmt::Synchronized { lock, body } => {
276            f(lock);
277            visit_stmt_exprs_ro(body, f);
278        }
279        Stmt::TernaryValue { e } => f(e),
280        Stmt::Labeled { body, .. } => visit_stmt_exprs_ro(body, f),
281        _ => {}
282    }
283}
284
285/// Count Local reads with the lost-alloc `<init>`-owner exclusion,
286/// zero-clone: mirrors `strip_lost_alloc_owners` + deep_rewrite count
287/// exactly (the strip replaced the owner subtree with Const(0), so the
288/// owner's locals were never counted; nested exclusions cannot double-
289/// skip because the excluded owner is never descended into).
290fn count_expr_ro(e: &Expr, m: &mut jdc_core::FxHashMap<u32, usize>) {
291    if let Expr::Local { var, .. } = e {
292        *m.entry(*var).or_insert(0) += 1;
293        return;
294    }
295    if let Expr::Method {
296        name,
297        is_special: true,
298        owner: Some(o),
299        args,
300        ..
301    } = e
302    {
303        if &**name == "<init>" && !matches!(**o, Expr::This) {
304            for a in args {
305                count_expr_ro(a, m);
306            }
307            return;
308        }
309    }
310    for_each_child(e, &mut |c| count_expr_ro(c, m));
311}
312
313/// Visit every expression (immutable).
314fn visit_exprs<F: FnMut(&Expr)>(e: &Expr, f: &mut F) {
315    f(e);
316    for_each_child(e, &mut |c| visit_exprs(c, f));
317}
318
319fn for_each_child<F: FnMut(&Expr)>(e: &Expr, f: &mut F) {
320    match e {
321        Expr::Un { e, .. } | Expr::Cast { e, .. } | Expr::InstanceOf { e, .. } => f(e),
322        Expr::Bin { l, r, .. } => {
323            f(l);
324            f(r);
325        }
326        Expr::Cond { c, t, f: fe } => {
327            f(c);
328            f(t);
329            f(fe);
330        }
331        Expr::Assign { target, value, .. } => {
332            f(target);
333            f(value);
334        }
335        Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => f(e),
336        Expr::Field { owner, .. } => {
337            if let Some(o) = owner {
338                f(o);
339            }
340        }
341        Expr::Method { owner, args, .. } => {
342            if let Some(o) = owner {
343                f(o);
344            }
345            for a in args {
346                f(a);
347            }
348        }
349        Expr::ArrayIndex { array, index } => {
350            f(array);
351            f(index);
352        }
353        Expr::New { args, .. } => {
354            for a in args {
355                f(a);
356            }
357        }
358        Expr::NewArray { dims, init, .. } => {
359            for d in dims {
360                f(d);
361            }
362            if let Some(v) = init {
363                for x in v {
364                    f(x);
365                }
366            }
367        }
368        Expr::NewMultiArray { dims, .. } => {
369            for d in dims {
370                f(d);
371            }
372        }
373        Expr::StringConcat(parts) => {
374            for p in parts {
375                if let ConcatPart::Str(x) = p {
376                    f(x);
377                }
378            }
379        }
380        Expr::Invokedynamic { args, .. } => {
381            for a in args {
382                f(a);
383            }
384        }
385        Expr::AnonNew { args, .. } => {
386            for a in args {
387                f(a);
388            }
389        }
390        _ => {}
391    }
392}
393
394/// Mutable child walk (one level).
395fn for_each_child_mut<F: FnMut(&mut Expr)>(e: &mut Expr, f: &mut F) {
396    match e {
397        Expr::Un { e, .. } | Expr::Cast { e, .. } | Expr::InstanceOf { e, .. } => f(e),
398        Expr::Bin { l, r, .. } => {
399            f(l);
400            f(r);
401        }
402        Expr::Cond { c, t, f: fe } => {
403            f(c);
404            f(t);
405            f(fe);
406        }
407        Expr::Assign { target, value, .. } => {
408            f(target);
409            f(value);
410        }
411        Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => f(e),
412        Expr::Field { owner, .. } => {
413            if let Some(o) = owner {
414                f(o);
415            }
416        }
417        Expr::Method { owner, args, .. } => {
418            if let Some(o) = owner {
419                f(o);
420            }
421            for a in args.iter_mut() {
422                f(a);
423            }
424        }
425        Expr::ArrayIndex { array, index } => {
426            f(array);
427            f(index);
428        }
429        Expr::New { args, .. } => {
430            for a in args.iter_mut() {
431                f(a);
432            }
433        }
434        Expr::NewArray { dims, init, .. } => {
435            for d in dims.iter_mut() {
436                f(d);
437            }
438            if let Some(v) = init {
439                for x in v.iter_mut() {
440                    f(x);
441                }
442            }
443        }
444        Expr::NewMultiArray { dims, .. } => {
445            for d in dims.iter_mut() {
446                f(d);
447            }
448        }
449        Expr::StringConcat(parts) => {
450            for p in parts {
451                if let ConcatPart::Str(x) = p {
452                    f(x);
453                }
454            }
455        }
456        Expr::Invokedynamic { args, .. } => {
457            for a in args.iter_mut() {
458                f(a);
459            }
460        }
461        Expr::AnonNew { args, .. } => {
462            for a in args.iter_mut() {
463                f(a);
464            }
465        }
466        _ => {}
467    }
468}
469
470/// Deep mutable expression rewrite.
471pub(crate) fn deep_rewrite<F: FnMut(&mut Expr)>(e: &mut Expr, f: &mut F) {
472    f(e);
473    for_each_child_mut(e, &mut |c| deep_rewrite(c, f));
474}
475
476/// `deep_rewrite` that respects WRITE positions: a `Local` appearing as
477/// an assignment target or a `++`/`--` operand never reaches `f`.
478/// Value-forwarding closures (`*x = value`) must use this — plain
479/// deep_rewrite turned a forwarded `vX = 25` into `25 = 25` (target
480/// replaced, and drop_defs then no longer recognized the statement, so
481/// the garbage survived into the output; reqable a4/e). Compound
482/// targets (`a[i] = …`, `o.f = …`) still recurse: owners and indices
483/// ARE reads.
484/// Read-only mirror of `deep_rewrite_reads` (same descent rules: a
485/// plain Local assignment/inc-dec target is a WRITE and never reaches
486/// `f`; compound targets still descend — owners and indices are reads).
487fn visit_exprs_reads<F: FnMut(&Expr)>(e: &Expr, f: &mut F) {
488    f(e);
489    match e {
490        Expr::Assign { target, value, .. } => {
491            if !matches!(**target, Expr::Local { .. }) {
492                visit_exprs_reads(target, f);
493            }
494            visit_exprs_reads(value, f);
495        }
496        Expr::PreIncDec { e: inner, .. } | Expr::PostIncDec { e: inner, .. } => {
497            if !matches!(**inner, Expr::Local { .. }) {
498                visit_exprs_reads(inner, f);
499            }
500        }
501        _ => for_each_child(e, &mut |c| visit_exprs_reads(c, f)),
502    }
503}
504
505fn deep_rewrite_reads<F: FnMut(&mut Expr)>(e: &mut Expr, f: &mut F) {
506    f(e);
507    match e {
508        Expr::Assign { target, value, .. } => {
509            if !matches!(**target, Expr::Local { .. }) {
510                deep_rewrite_reads(target, f);
511            }
512            deep_rewrite_reads(value, f);
513        }
514        Expr::PreIncDec { e: inner, .. } | Expr::PostIncDec { e: inner, .. } => {
515            if !matches!(**inner, Expr::Local { .. }) {
516                deep_rewrite_reads(inner, f);
517            }
518        }
519        _ => for_each_child_mut(e, &mut |c| deep_rewrite_reads(c, f)),
520    }
521}
522
523fn collect_vars(e: &Expr, out: &mut HashSet<u32>) {
524    visit_exprs(e, &mut |x| {
525        if let Expr::Local { var, .. } = x {
526            out.insert(*var);
527        }
528    });
529}
530
531/// Collect variable references (reads; `assignments` adds assignment
532/// targets). Single-pass recursion — the previous shape wrapped a
533/// self-recursive closure in `walk_all`, visiting every subtree twice over
534/// (exponential in nesting depth).
535fn stmt_collect_vars(s: &Stmt, out: &mut HashSet<u32>, assignments: bool) {
536    match s {
537        Stmt::Block(v) => {
538            for x in v {
539                stmt_collect_vars(x, out, assignments);
540            }
541        }
542        Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
543            if let Expr::Local { var, .. } = &**target {
544                if assignments {
545                    out.insert(*var);
546                }
547            } else {
548                collect_vars(target, out);
549            }
550            collect_vars(value, out);
551        }
552        Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
553            collect_vars(e, out)
554        }
555        Stmt::Return(Some(e)) => collect_vars(e, out),
556        Stmt::LocalDef { var, init, .. } => {
557            if assignments {
558                out.insert(*var);
559            }
560            if let Some(e) = init {
561                collect_vars(e, out);
562            }
563        }
564        Stmt::If {
565            cond,
566            then_stmt,
567            else_stmt,
568        } => {
569            collect_vars(cond, out);
570            stmt_collect_vars(then_stmt, out, assignments);
571            if let Some(e) = else_stmt {
572                stmt_collect_vars(e, out, assignments);
573            }
574        }
575        Stmt::While { cond, body } => {
576            collect_vars(cond, out);
577            stmt_collect_vars(body, out, assignments);
578        }
579        Stmt::DoWhile { body, cond } => {
580            stmt_collect_vars(body, out, assignments);
581            collect_vars(cond, out);
582        }
583        Stmt::For {
584            init,
585            cond,
586            update,
587            body,
588        } => {
589            for x in init {
590                stmt_collect_vars(x, out, assignments);
591            }
592            if let Some(c) = cond {
593                collect_vars(c, out);
594            }
595            for u in update {
596                collect_vars(u, out);
597            }
598            stmt_collect_vars(body, out, assignments);
599        }
600        Stmt::ForEach {
601            var,
602            iterable,
603            body,
604            ..
605        } => {
606            if assignments {
607                out.insert(*var);
608            }
609            collect_vars(iterable, out);
610            stmt_collect_vars(body, out, assignments);
611        }
612        Stmt::Switch {
613            selector,
614            cases,
615            default,
616            ..
617        } => {
618            collect_vars(selector, out);
619            for c in cases {
620                for x in &c.body {
621                    stmt_collect_vars(x, out, assignments);
622                }
623            }
624            if let Some(d) = default {
625                stmt_collect_vars(d, out, assignments);
626            }
627        }
628        Stmt::Try {
629            body,
630            catches,
631            finally,
632        } => {
633            stmt_collect_vars(body, out, assignments);
634            for c in catches {
635                stmt_collect_vars(&c.body, out, assignments);
636            }
637            if let Some(f) = finally {
638                stmt_collect_vars(f, out, assignments);
639            }
640        }
641        Stmt::TryWithResources {
642            resources,
643            body,
644            catches,
645            finally,
646        } => {
647            for x in resources {
648                stmt_collect_vars(x, out, assignments);
649            }
650            stmt_collect_vars(body, out, assignments);
651            for c in catches {
652                stmt_collect_vars(&c.body, out, assignments);
653            }
654            if let Some(f) = finally {
655                stmt_collect_vars(f, out, assignments);
656            }
657        }
658        Stmt::Synchronized { lock, body } => {
659            collect_vars(lock, out);
660            stmt_collect_vars(body, out, assignments);
661        }
662        Stmt::Assert { cond, msg } => {
663            collect_vars(cond, out);
664            if let Some(m) = msg {
665                collect_vars(m, out);
666            }
667        }
668        Stmt::Labeled { body, .. } => stmt_collect_vars(body, out, assignments),
669        _ => {}
670    }
671}
672
673fn walk_all<F: FnMut(&Stmt)>(s: &Stmt, f: &mut F) {
674    f(s);
675    match s {
676        Stmt::Block(v) => {
677            for x in v {
678                walk_all(x, f);
679            }
680        }
681        Stmt::If {
682            then_stmt,
683            else_stmt,
684            ..
685        } => {
686            walk_all(then_stmt, f);
687            if let Some(e) = else_stmt {
688                walk_all(e, f);
689            }
690        }
691        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => walk_all(body, f),
692        Stmt::For { init, body, .. } => {
693            for x in init {
694                walk_all(x, f);
695            }
696            walk_all(body, f);
697        }
698        Stmt::ForEach { body, .. } => walk_all(body, f),
699        Stmt::Switch { cases, default, .. } => {
700            for c in cases {
701                for x in &c.body {
702                    walk_all(x, f);
703                }
704            }
705            if let Some(d) = default {
706                walk_all(d, f);
707            }
708        }
709        Stmt::Try {
710            body,
711            catches,
712            finally,
713        } => {
714            walk_all(body, f);
715            for c in catches {
716                walk_all(&c.body, f);
717            }
718            if let Some(fl) = finally {
719                walk_all(fl, f);
720            }
721        }
722        Stmt::TryWithResources {
723            resources,
724            body,
725            catches,
726            finally,
727        } => {
728            for x in resources {
729                walk_all(x, f);
730            }
731            walk_all(body, f);
732            for c in catches {
733                walk_all(&c.body, f);
734            }
735            if let Some(fl) = finally {
736                walk_all(fl, f);
737            }
738        }
739        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => walk_all(body, f),
740        _ => {}
741    }
742}
743
744// ---------------------------------------------------------------------------
745// Passes
746// ---------------------------------------------------------------------------
747
748/// Bind catch parameters: the handler's first statement (a bare LocalDef
749/// from move-exception) becomes the catch variable.
750pub fn bind_catches(s: &mut Stmt, vt: &mut VarTable) {
751    // Vars defined anywhere in the method (params + LocalDefs +
752    // assign targets): a catch body reading a var OUTSIDE this set is
753    // the unmaterialized move-exception register (the lifter minted it
754    // without a defining statement).
755    let mut defined: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
756    for v in &vt.vars {
757        if v.is_param {
758            defined.insert(v.id);
759        }
760    }
761    collect_defined_locals(s, &mut defined);
762    let reads_all = count_locals_stmts(std::slice::from_ref(s));
763    // Vars with a real assignment (LocalDef-with-init or assign target):
764    // a var only ever READ is either the unmaterialized move-exception
765    // register or a bare declaration awaiting its hoisted assignment.
766    let mut assigned: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
767    collect_assigned_locals(s, &mut assigned);
768    let mut fallback_bound: Vec<u32> = Vec::new();
769    bind_catches_walk(s, vt, &defined, &reads_all, &assigned, &mut fallback_bound);
770    // The catch parameter IS the declaration now: drop the bare
771    // `Throwable th;` hoists for vars the fallback bound.
772    if !fallback_bound.is_empty() {
773        remove_bare_decls(s, &fallback_bound);
774    }
775}
776
777fn collect_defined_locals(s: &Stmt, out: &mut jdc_core::FxHashSet<u32>) {
778    crate::passes::walk_all(s, &mut |st| {
779        match st {
780            Stmt::LocalDef { var, .. } => {
781                out.insert(*var);
782            }
783            Stmt::ExprStmt(Expr::Assign { target, .. }) => {
784                if let Expr::Local { var, .. } = &**target {
785                    out.insert(*var);
786                }
787            }
788            _ => {}
789        }
790    });
791}
792
793/// Vars with a bare `LocalDef{init: None}` declaration in `s` (dedup,
794/// declaration order).
795fn collect_bare_decls(s: &Stmt, out: &mut Vec<u32>) {
796    walk_all(s, &mut |st| {
797        if let Stmt::LocalDef { var, init: None, .. } = st {
798            if !out.contains(var) {
799                out.push(*var);
800            }
801        }
802    });
803}
804
805
806/// First read of a local that is defined nowhere in the method (and is
807/// not a parameter) inside the catch body — the unmaterialized
808/// move-exception register.
809/// Vars carrying a real assignment (init or write target).
810fn collect_assigned_locals(s: &Stmt, out: &mut jdc_core::FxHashSet<u32>) {
811    crate::passes::walk_all(s, &mut |st| {
812        match st {
813            Stmt::LocalDef { var, init: Some(_), .. } => {
814                out.insert(*var);
815            }
816            Stmt::ExprStmt(Expr::Assign { target, .. }) => {
817                if let Expr::Local { var, .. } = &**target {
818                    out.insert(*var);
819                }
820            }
821            _ => {}
822        }
823    });
824}
825
826/// First `throw <local>` var in the catch body that is never assigned
827/// anywhere and is exception-typed — the unmaterialized move-exception.
828fn first_thrown_unassigned_local(
829    body: &Stmt,
830    vt: &VarTable,
831    assigned: &jdc_core::FxHashSet<u32>,
832) -> Option<u32> {
833    let mut found: Option<u32> = None;
834    let c = body.clone();
835    walk_all(&c, &mut |st| {
836        if let Stmt::Throw(th) = st {
837            if let Expr::Local { var, .. } = th {
838                let v = *var;
839                if !assigned.contains(&v) {
840                    if let JavaType::Object(o) = vt.var(v).ty.erased() {
841                        if o.as_ref() == "java/lang/Throwable" {
842                            found = Some(v);
843                        }
844                    }
845                }
846            }
847        }
848    });
849    found
850}
851
852/// Remove the first LEAF statement (mirror of first_leaf_stmt), pruning
853/// leading blocks that became empty — the stored-def bind consumes the
854/// move-exception decl wherever the region walk nested it.
855fn remove_first_leaf(s: &mut Stmt) {
856    if let Stmt::Block(v) = s {
857        if let Some(f) = v.first_mut() {
858            remove_first_leaf(f);
859        }
860        while matches!(v.first(), Some(Stmt::Block(b)) if b.is_empty()) {
861            v.remove(0);
862        }
863    } else {
864        *s = Stmt::Block(vec![]);
865    }
866}
867
868/// Remove bare `LocalDef{var, init: None}` declarations for the given
869/// vars (the catch parameter is their declaration now).
870fn remove_bare_decls(s: &mut Stmt, vars: &[u32]) {
871    let vars: jdc_core::FxHashSet<u32> = vars.iter().copied().collect();
872    walk_mut_deep(s, &mut |st| {
873        if let Stmt::Block(v) = st {
874            v.retain(|x| {
875                !matches!(x, Stmt::LocalDef { var, init: None, .. } if vars.contains(var))
876            });
877        }
878    });
879}
880
881fn first_undefined_local_read(
882    body: &Stmt,
883    defined: &jdc_core::FxHashSet<u32>,
884) -> Option<u32> {
885    let mut found: Option<u32> = None;
886    visit_stmt_exprs_ro(body, &mut |e| {
887        if found.is_none() {
888            visit_exprs(e, &mut |x| {
889                if found.is_none() {
890                    if let Expr::Local { var, .. } = x {
891                        if !defined.contains(var) {
892                            found = Some(*var);
893                        }
894                    }
895                }
896            });
897        }
898    });
899    found
900}
901
902#[allow(clippy::too_many_arguments)]
903fn bind_catches_walk(
904    s: &mut Stmt,
905    vt: &mut VarTable,
906    defined: &jdc_core::FxHashSet<u32>,
907    reads_all: &jdc_core::FxHashMap<u32, usize>,
908    assigned: &jdc_core::FxHashSet<u32>,
909    fallback_bound: &mut Vec<u32>,
910) {
911    match s {
912        Stmt::Try {
913            body,
914            catches,
915            finally,
916        } => {
917            bind_catches_walk(body, vt, defined, reads_all, assigned, fallback_bound);
918            for c in catches.iter_mut() {
919                if c.var == u32::MAX {
920                    // Leaf-piercing head check: the region walk can wrap
921                    // the handler body as Block[Block[…]] (weixin ri5/c
922                    // thx rethrow family — the top-level `v.first()` saw
923                    // a Block, all three bind paths missed, and the emit
924                    // fell back to `catch (Throwable ignored)` beside a
925                    // never-assigned `Throwable th;` read).
926                    let stored = match first_leaf_stmt(c.body.as_ref()) {
927                        // ONLY the bare move-exception decl (the
928                        // lifter emits LocalDef{init:None} for it).
929                        // A first stmt with a REAL init is handler
930                        // computation — consuming it as the catch
931                        // store deleted the def and hijacked its
932                        // reads to the catch param (lark ih6/w's
933                        // `v11 = Thread.currentThread()` →
934                        // `catch (InterruptedException thread2) {
935                        // thread2.interrupt(); }`; ch6/e cls4.
936                        // getName; the exception-ignoring catch is
937                        // the standard interrupt idiom).
938                        Stmt::LocalDef { var, init: None, .. } => Some(*var),
939                        // Phi-copy shape `v = exc` where exc is the
940                        // unmaterialized move-exception register
941                        // (defined nowhere in the method).
942                        Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
943                            match (&**target, &**value) {
944                                (Expr::Local { var, .. }, Expr::Local { var: src, .. })
945                                    if !defined.contains(src) =>
946                                {
947                                    Some(*var)
948                                }
949                                _ => None,
950                            }
951                        }
952                        _ => None,
953                    };
954                    if let Some(v) = stored {
955                        let exc = c
956                            .exc
957                            .first()
958                            .cloned()
959                            .unwrap_or_else(|| "java/lang/Throwable".into());
960                        let slot = vt.var(v).slot;
961                        let name = "e".to_string();
962                        let new_var =
963                            vt.add_catch_var(slot, name, TypeRef::J(JavaType::Object(exc)));
964                        remove_first_leaf(c.body.as_mut());
965                        rewrite_local_refs(c.body.as_mut(), v, new_var);
966                        c.var = new_var;
967                    } else if let Some(v) = first_undefined_local_read(c.body.as_ref(), defined)
968                        .filter(|v| {
969                            // Bind only a var read NOWHERE outside this
970                            // catch: ensure_declared would hoist an
971                            // outside-read var to a method local, and
972                            // consuming it as the catch parameter would
973                            // scope it too narrowly.
974                            let in_catch = count_locals_stmts(std::slice::from_ref(c.body.as_ref()));
975                            reads_all.get(v).copied().unwrap_or(0)
976                                == in_catch.get(v).copied().unwrap_or(0)
977                        })
978                        .filter(|v| {
979                            // And only an EXCEPTION-typed var: the
980                            // move-exception registers carry the handler
981                            // type — a plain local pending its hoisted
982                            // declaration (StringBuilder sb) must not be
983                            // consumed as the catch parameter (reqable
984                            // amazon: the declaration vanished and its
985                            // outer readers broke).
986                            let ty = vt.var(*v).ty.erased();
987                            let exc = c
988                                .exc
989                                .first()
990                                .cloned()
991                                .unwrap_or_else(|| "java/lang/Throwable".into());
992                            matches!(&ty, JavaType::Object(o)
993                                if o.as_ref() == exc.as_ref()
994                                    || o.as_ref() == "java/lang/Throwable")
995                        })
996                    {
997                        // The handler's first statement is not the
998                        // move-exception def (the d8 synchronized pattern
999                        // leads with MonitorExit): the register the lifter
1000                        // minted for move-exception is still READ in the
1001                        // body (`throw th;`) with no defining statement
1002                        // anywhere. Bind IT as the catch parameter —
1003                        // otherwise emit printed `catch (Throwable ignored)`
1004                        // over an undeclared `throw th` (definite-assignment
1005                        // failure, 6.4k weibo sites).
1006                        c.var = v;
1007                        fallback_bound.push(v);
1008                    } else if let Some(v) =
1009                        first_thrown_unassigned_local(c.body.as_ref(), vt, assigned)
1010                            .filter(|v| {
1011                                let in_catch =
1012                                    count_locals_stmts(std::slice::from_ref(c.body.as_ref()));
1013                                reads_all.get(v).copied().unwrap_or(0)
1014                                    == in_catch.get(v).copied().unwrap_or(0)
1015                            })
1016                            .filter(|v| {
1017                                // Same exception-type gate: the hoisted
1018                                // `Throwable th;` declares the var but the
1019                                // move-exception never assigned it.
1020                                let ty = vt.var(*v).ty.erased();
1021                                let exc = c
1022                                    .exc
1023                                    .first()
1024                                    .cloned()
1025                                    .unwrap_or_else(|| "java/lang/Throwable".into());
1026                                matches!(&ty, JavaType::Object(o)
1027                                    if o.as_ref() == exc.as_ref()
1028                                        || o.as_ref() == "java/lang/Throwable")
1029                            })
1030                    {
1031                        // Declared-never-assigned: `Throwable th;` hoisted
1032                        // by an earlier phase, the catch body's `throw th`
1033                        // its only use — the move-exception assignment was
1034                        // never materialized. Bind th as the catch
1035                        // parameter and drop the bare declaration.
1036                        c.var = v;
1037                        fallback_bound.push(v);
1038                    }
1039                }
1040                if c.var != u32::MAX {
1041                    // Second move-exception identity INSIDE the body: R8
1042                    // guards the handler's own close() with a nested
1043                    // try/finally and re-materializes the in-flight
1044                    // exception on its exceptional continuation as a
1045                    // fresh var (`Throwable th7; throw th7;` — weixin
1046                    // y5/b, the 327-site residue of the leaf-piercing
1047                    // fix). The twin is never ASSIGNED anywhere in the
1048                    // method (its register only ever receives
1049                    // move-exception) and the catch param IS its value —
1050                    // rewrite the twin's reads to the param and drop the
1051                    // bare decl. The never-assigned gate makes the
1052                    // rewrite flow-insensitive: every body read of the
1053                    // twin reads the exception. Twins read outside this
1054                    // catch are left alone (the param's scope ends here).
1055                    let in_catch =
1056                        count_locals_stmts(std::slice::from_ref(c.body.as_ref()));
1057                    let mut twins: Vec<u32> = Vec::new();
1058                    collect_bare_decls(c.body.as_ref(), &mut twins);
1059                    let mut swept: Vec<u32> = Vec::new();
1060                    for x in twins {
1061                        if x == c.var || assigned.contains(&x) {
1062                            continue;
1063                        }
1064                        if reads_all.get(&x).copied().unwrap_or(0)
1065                            != in_catch.get(&x).copied().unwrap_or(0)
1066                        {
1067                            continue;
1068                        }
1069                        if !matches!(vt.var(x).ty.erased(),
1070                            JavaType::Object(o) if o.as_ref() == "java/lang/Throwable")
1071                        {
1072                            continue;
1073                        }
1074                        rewrite_local_refs(c.body.as_mut(), x, c.var);
1075                        swept.push(x);
1076                    }
1077                    if !swept.is_empty() {
1078                        remove_bare_decls(c.body.as_mut(), &swept);
1079                    }
1080                }
1081                bind_catches_walk(&mut c.body, vt, defined, reads_all, assigned, fallback_bound);
1082            }
1083            if let Some(f) = finally {
1084                bind_catches_walk(f.as_mut(), vt, defined, reads_all, assigned, fallback_bound);
1085            }
1086        }
1087        Stmt::Block(v) => {
1088            for x in v.iter_mut() {
1089                bind_catches_walk(x, vt, defined, reads_all, assigned, fallback_bound);
1090            }
1091        }
1092        Stmt::If {
1093            then_stmt,
1094            else_stmt,
1095            ..
1096        } => {
1097            bind_catches_walk(then_stmt, vt, defined, reads_all, assigned, fallback_bound);
1098            if let Some(e) = else_stmt {
1099                bind_catches_walk(e, vt, defined, reads_all, assigned, fallback_bound);
1100            }
1101        }
1102        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => bind_catches_walk(body, vt, defined, reads_all, assigned, fallback_bound),
1103        Stmt::For { init, body, .. } => {
1104            for x in init.iter_mut() {
1105                bind_catches_walk(x, vt, defined, reads_all, assigned, fallback_bound);
1106            }
1107            bind_catches_walk(body, vt, defined, reads_all, assigned, fallback_bound);
1108        }
1109        Stmt::ForEach { body, .. }
1110        | Stmt::Labeled { body, .. }
1111        | Stmt::Synchronized { body, .. } => bind_catches_walk(body, vt, defined, reads_all, assigned, fallback_bound),
1112        Stmt::Switch { cases, default, .. } => {
1113            for c in cases.iter_mut() {
1114                for x in c.body.iter_mut() {
1115                    bind_catches_walk(x, vt, defined, reads_all, assigned, fallback_bound);
1116                }
1117            }
1118            if let Some(d) = default {
1119                bind_catches_walk(d, vt, defined, reads_all, assigned, fallback_bound);
1120            }
1121        }
1122        _ => {}
1123    }
1124}
1125
1126fn rewrite_local_refs(s: &mut Stmt, from: u32, to: u32) {
1127    rewrite_exprs(s, &mut |e| {
1128        deep_rewrite(e, &mut |x| {
1129            if let Expr::Local { var, .. } = x {
1130                if *var == from {
1131                    *var = to;
1132                }
1133            }
1134        });
1135    });
1136}
1137
1138/// Flatten blocks, drop empty ones and trailing no-op statements.
1139pub fn cleanup(s: &mut Stmt) {
1140    jdc_core::ir::stmt::flatten(s);
1141    prune_dead_tails(s);
1142    strip_empty(s);
1143    merge_singleton_stmts(s);
1144}
1145
1146/// JLS "cannot complete normally" pruning: drop statements after the
1147/// first hard terminator in any statement sequence — blocks, switch
1148/// case bodies (bare Vecs), for-inits, catch bodies. The 无法访问的语句
1149/// family: `continue; break;` inside switch cases and `return` after a
1150/// try{…return…}catch{…return…} (pdd h0/a, c20/d; ~5.9k sites across
1151/// pdd/cmb/jianying). Conservative: loops and switches never count as
1152/// terminators (no infinite-loop modeling), sequences still carrying
1153/// fallback Label/Goto statements stay untouched, and only
1154/// Return/Throw/Break/Continue plus If/Try/Labeled/Synchronized/Block
1155/// compositions of them terminate.
1156fn seq_terminates(s: &Stmt) -> bool {
1157    match s {
1158        Stmt::Return(_) | Stmt::Throw(_) | Stmt::Break(_) | Stmt::Continue(_) => {
1159            true
1160        }
1161        Stmt::Block(v) => v.iter().any(seq_terminates),
1162        Stmt::If {
1163            then_stmt,
1164            else_stmt,
1165            ..
1166        } => {
1167            seq_terminates(then_stmt)
1168                && else_stmt
1169                    .as_ref()
1170                    .map(|e| seq_terminates(e))
1171                    .unwrap_or(false)
1172        }
1173        Stmt::Try {
1174            body,
1175            catches,
1176            finally,
1177        }
1178        | Stmt::TryWithResources {
1179            body,
1180            catches,
1181            finally,
1182            ..
1183        } => {
1184            if let Some(f) = finally {
1185                if seq_terminates(f) {
1186                    return true;
1187                }
1188            }
1189            seq_terminates(body) && catches.iter().all(|c| seq_terminates(&c.body))
1190        }
1191        Stmt::Labeled { body, .. } | Stmt::Synchronized { body, .. } => {
1192            seq_terminates(body)
1193        }
1194        // A break-less infinite while/do-while never completes normally
1195        // either — jdc-core's label-aware analysis (the emit DEADEND
1196        // truncator uses it): statements after it are 无法访问的语句 the
1197        // same way (the bulk of the pruning residue: jianying 738).
1198        Stmt::While { .. } | Stmt::DoWhile { .. } => {
1199            jdc_core::analysis::dead_end_infinite_while(s)
1200        }
1201        _ => false,
1202    }
1203}
1204
1205fn prune_seq(v: &mut Vec<Stmt>) {
1206    if v.iter()
1207        .any(|x| matches!(x, Stmt::Label(_) | Stmt::Goto(_)))
1208    {
1209        return;
1210    }
1211    if let Some(i) = v.iter().position(seq_terminates) {
1212        v.truncate(i + 1);
1213    }
1214}
1215
1216pub fn prune_dead_tails(s: &mut Stmt) {
1217    match s {
1218        Stmt::Block(v) => {
1219            for x in v.iter_mut() {
1220                prune_dead_tails(x);
1221            }
1222            prune_seq(v);
1223        }
1224        Stmt::If {
1225            then_stmt,
1226            else_stmt,
1227            ..
1228        } => {
1229            prune_dead_tails(then_stmt);
1230            if let Some(e) = else_stmt {
1231                prune_dead_tails(e);
1232            }
1233        }
1234        Stmt::While { body, .. }
1235        | Stmt::DoWhile { body, .. }
1236        | Stmt::ForEach { body, .. }
1237        | Stmt::Labeled { body, .. }
1238        | Stmt::Synchronized { body, .. } => prune_dead_tails(body),
1239        Stmt::For { init, body, .. } => {
1240            for x in init.iter_mut() {
1241                prune_dead_tails(x);
1242            }
1243            prune_seq(init);
1244            prune_dead_tails(body);
1245        }
1246        Stmt::Switch {
1247            cases, default, ..
1248        } => {
1249            for c in cases {
1250                for x in c.body.iter_mut() {
1251                    prune_dead_tails(x);
1252                }
1253                prune_seq(&mut c.body);
1254            }
1255            if let Some(d) = default {
1256                prune_dead_tails(d);
1257            }
1258        }
1259        Stmt::Try {
1260            body,
1261            catches,
1262            finally,
1263        }
1264        | Stmt::TryWithResources {
1265            body,
1266            catches,
1267            finally,
1268            ..
1269        } => {
1270            prune_dead_tails(body);
1271            for c in catches {
1272                prune_dead_tails(&mut c.body);
1273            }
1274            if let Some(f) = finally {
1275                prune_dead_tails(f);
1276            }
1277        }
1278        _ => {}
1279    }
1280}
1281
1282/// Drop assignments whose target field is a PHANTOM: the declaring class
1283/// is pool-materialized but does not declare the field (R8 inlines R-class
1284/// constants everywhere, strips the field declarations, and leaves the
1285/// `<clinit>` husk still sput-ing to them — QQ's R$anim/kj3.a husks:
1286/// class_data with 0 fields beside a clinit of phantom sget→sput pairs,
1287/// 7,774 找不到符号 in four R.java files alone). The dex statements are
1288/// dead by construction — executing them is a guaranteed NoSuchFieldError,
1289/// and R8 only keeps them because nothing triggers the husk's clinit — so
1290/// dropping them preserves every observable behavior while the faithful
1291/// render (`x = kj3.a.x;` against declared-nowhere names) cannot compile.
1292/// Conservative gates: only plain ExprStmt assignments whose TARGET is a
1293/// phantom static/instance field of a materialized class, and whose VALUE
1294/// is side-effect-free (const / local / phantom-field read / cast chain) —
1295/// a real-field read could trigger another class's clinit, and a call
1296/// could carry side effects, so those statements stay.
1297pub fn strip_phantom_field_writes(body: &mut Stmt, pool: &DexPool) {
1298    fn phantom(pool: &DexPool, cls: &str, name: &str) -> bool {
1299        // pool.get (on-demand materialize), not get_if_materialized:
1300        // lazy getclass runs must reach the same verdicts as full runs
1301        // (the RHS holder kj3/a is not pre-materialized there).
1302        let Some(pc) = pool.get(cls) else {
1303            return false;
1304        };
1305        // The IR Field ref carries the DISPLAY name (the lifter emits
1306        // through field_display) while the pool tables carry RAW names —
1307        // match both, or an obscured-renamed field (`h` → `h17` beside
1308        // class h) reads as phantom and its const-build assignment gets
1309        // dropped, breaking enum promotion (weixin z54/h fell back to
1310        // `/* enum */ class`, taking every `h[] → Enum[]` conversion
1311        // down with it — the +193 weixin battery regression).
1312        fn declares(pc: &crate::PoolClass, cls: &str, name: &str) -> bool {
1313            pc.static_fields
1314                .iter()
1315                .chain(pc.instance_fields.iter())
1316                .any(|f| {
1317                    f.name.as_str() == name
1318                        || jdc_core::rename::field_display(cls, &f.name, &f.desc)
1319                            .is_some_and(|d| d == name)
1320                })
1321        }
1322        if declares(pc, cls, name) {
1323            return false;
1324        }
1325        // Field resolution walks the superclass chain, so "phantom" must
1326        // be PROVEN against the whole chain — a write to an inherited
1327        // field is real code (QQ com.tencent.gdtad.statistics.d inherits
1328        // `b` from super `e`; an own-fields-only verdict dropped the
1329        // legitimate `v10x.b = gdtAd2;` — silent data loss, latent until
1330        // the walk started descending into If.then_stmt). Off-pool
1331        // ancestors are enumerated through fwdb (framework fields like
1332        // View.mID are real); an ancestor NEITHER pool nor fwdb knows
1333        // (R8-stripped class) ends the proof — keep the write.
1334        let mut cur: Option<String> = pc.super_name.clone();
1335        let mut hops = 0u32;
1336        while let Some(s) = cur {
1337            hops += 1;
1338            if hops > 64 {
1339                return false;
1340            }
1341            if let Some(sup) = pool.get(&s) {
1342                if declares(sup, &s, name) {
1343                    return false;
1344                }
1345                cur = sup.super_name.clone();
1346            } else {
1347                if !crate::fwdb::exists(&s) {
1348                    return false;
1349                }
1350                let mut found = false;
1351                crate::fwdb::for_each_field(&s, |f, _| {
1352                    if f == name {
1353                        found = true;
1354                    }
1355                });
1356                if found {
1357                    return false;
1358                }
1359                // fwdb strips the Object super edge: the chain ends here.
1360                cur = crate::fwdb::super_of(&s).map(|x| x.to_string());
1361            }
1362        }
1363        true
1364    }
1365    fn droppable_value(pool: &DexPool, e: &Expr) -> bool {
1366        match e {
1367            Expr::Const(_) | Expr::Local { .. } => true,
1368            Expr::Field { cls, name, .. } => phantom(pool, cls, name),
1369            Expr::Cast { e, .. } => droppable_value(pool, e),
1370            _ => false,
1371        }
1372    }
1373    fn is_phantom_write(pool: &DexPool, st: &Stmt) -> bool {
1374        if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = st {
1375            if let Expr::Field { cls, name, .. } = &**target {
1376                return phantom(pool, cls, name) && droppable_value(pool, value);
1377            }
1378        }
1379        false
1380    }
1381    // walk_mut_deep over every Vec-holding node — the bare-Vec positions
1382    // (switch case bodies, For init, TWR resources) are the known blind
1383    // spot of Block-only recursion.
1384    walk_mut_deep(body, &mut |st| match st {
1385        Stmt::Block(v) => v.retain(|x| !is_phantom_write(pool, x)),
1386        Stmt::Switch { cases, .. } => {
1387            for c in cases.iter_mut() {
1388                c.body.retain(|x| !is_phantom_write(pool, x));
1389            }
1390        }
1391        Stmt::For { init, .. } => init.retain(|x| !is_phantom_write(pool, x)),
1392        Stmt::TryWithResources { resources, .. } => {
1393            resources.retain(|x| !is_phantom_write(pool, x))
1394        }
1395        _ => {}
1396    });
1397    if is_phantom_write(pool, body) {
1398        *body = Stmt::Block(vec![]);
1399    }
1400}
1401
1402/// Late default-init for surviving bare declarations (`T v;`). Runs
1403/// AFTER every fold/ctor/hotfix matcher that keys on `init: None`
1404/// (bind_catches, strip_clinit_hotfix_guard, the folding prefix scans),
1405/// so nothing observes the initialized form until emission. Dex
1406/// registers are zero-initialized at method entry, so `= null/0/false`
1407/// is the FAITHFUL value for any read that reaches it before the first
1408/// write — and javac's definite assignment rejects the bare form on
1409/// partial paths (可能尚未初始化变量: pdd gi1/a obj12 assigned in two
1410/// branches and read after the merge; also turns the residual
1411/// move-exception twins into compilable — and register-faithful —
1412/// null reads instead of undefined ones).
1413pub fn init_bare_decls(body: &mut Stmt, vt: &VarTable) {
1414    walk_mut_deep(body, &mut |st| {
1415        if let Stmt::LocalDef { var, init, .. } = st {
1416            if init.is_some() {
1417                return;
1418            }
1419            let lit = match vt.var(*var).ty.erased() {
1420                JavaType::Boolean => "false",
1421                JavaType::Int
1422                | JavaType::Byte
1423                | JavaType::Short
1424                | JavaType::Char => "0",
1425                JavaType::Long => "0L",
1426                JavaType::Float => "0.0f",
1427                JavaType::Double => "0.0d",
1428                JavaType::Void => return,
1429                JavaType::Object(_) | JavaType::Array(_) => "null",
1430            };
1431            *init = Some(Expr::Raw(lit.to_string()));
1432        }
1433    });
1434}
1435
1436fn strip_empty(s: &mut Stmt) {
1437    match s {
1438        Stmt::Block(v) => {
1439            v.retain(|x| !x.is_empty_block());
1440            for x in v.iter_mut() {
1441                strip_empty(x);
1442            }
1443        }
1444        Stmt::If {
1445            then_stmt,
1446            else_stmt,
1447            ..
1448        } => {
1449            strip_empty(then_stmt);
1450            if let Some(e) = else_stmt {
1451                strip_empty(e);
1452                if e.is_empty_block() {
1453                    *else_stmt = None;
1454                }
1455            }
1456        }
1457        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => strip_empty(body),
1458        Stmt::For { init, body, .. } => {
1459            init.retain(|x| !x.is_empty_block());
1460            strip_empty(body);
1461        }
1462        Stmt::ForEach { body, .. } => strip_empty(body),
1463        Stmt::Switch { cases, default, .. } => {
1464            for c in cases {
1465                c.body.retain(|x| !x.is_empty_block());
1466            }
1467            if let Some(d) = default {
1468                strip_empty(d);
1469            }
1470        }
1471        Stmt::Try {
1472            body,
1473            catches,
1474            finally,
1475        } => {
1476            strip_empty(body);
1477            for c in catches {
1478                strip_empty(&mut c.body);
1479            }
1480            if let Some(f) = finally {
1481                strip_empty(f);
1482            }
1483        }
1484        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => strip_empty(body),
1485        _ => {}
1486    }
1487}
1488
1489fn merge_singleton_stmts(s: &mut Stmt) {
1490    if let Stmt::Block(v) = s {
1491        for x in v.iter_mut() {
1492            merge_singleton_stmts(x);
1493        }
1494    }
1495}
1496
1497/// Drop statements after a definite terminator within a block.
1498pub fn prune_unreachable(s: &mut Stmt) {
1499    if let Stmt::Block(v) = s {
1500        let mut cut: Option<usize> = None;
1501        for (i, x) in v.iter().enumerate() {
1502            if matches!(x, Stmt::Return(_) | Stmt::Throw(_)) {
1503                cut = Some(i + 1);
1504                break;
1505            }
1506        }
1507        if let Some(c) = cut {
1508            if c < v.len() {
1509                v.truncate(c);
1510            }
1511        }
1512        for x in v.iter_mut() {
1513            prune_unreachable(x);
1514        }
1515    } else {
1516        walk_mut(s, &mut prune_unreachable);
1517    }
1518}
1519
1520fn walk_mut<F: FnMut(&mut Stmt)>(s: &mut Stmt, f: &mut F) {
1521    match s {
1522        Stmt::Block(v) => {
1523            for x in v.iter_mut() {
1524                f(x);
1525            }
1526        }
1527        Stmt::If {
1528            then_stmt,
1529            else_stmt,
1530            ..
1531        } => {
1532            f(then_stmt);
1533            if let Some(e) = else_stmt {
1534                f(e);
1535            }
1536        }
1537        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => f(body),
1538        Stmt::For { init, body, .. } => {
1539            for x in init.iter_mut() {
1540                f(x);
1541            }
1542            f(body);
1543        }
1544        Stmt::ForEach { body, .. } => f(body),
1545        Stmt::Switch { cases, default, .. } => {
1546            for c in cases {
1547                for x in c.body.iter_mut() {
1548                    f(x);
1549                }
1550            }
1551            if let Some(d) = default {
1552                f(d);
1553            }
1554        }
1555        Stmt::Try {
1556            body,
1557            catches,
1558            finally,
1559        } => {
1560            f(body);
1561            for c in catches {
1562                f(&mut c.body);
1563            }
1564            if let Some(fl) = finally {
1565                f(fl);
1566            }
1567        }
1568        Stmt::TryWithResources {
1569            resources,
1570            body,
1571            catches,
1572            finally,
1573        } => {
1574            for r in resources.iter_mut() {
1575                f(r);
1576            }
1577            f(body);
1578            for c in catches {
1579                f(&mut c.body);
1580            }
1581            if let Some(fl) = finally {
1582                f(fl);
1583            }
1584        }
1585        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => f(body),
1586        _ => {}
1587    }
1588}
1589
1590/// Count LocalDef statements (pipeline diagnostics).
1591pub fn count_localdefs(s: &Stmt, out: &mut usize) {
1592    if matches!(s, Stmt::LocalDef { .. }) {
1593        *out += 1;
1594    }
1595    walk_all(s, &mut |x| {
1596        if matches!(x, Stmt::LocalDef { .. }) {
1597            *out += 1;
1598        }
1599    });
1600}
1601
1602/// Drop a trailing `return;` (d8's explicit return-void at method end).
1603pub fn strip_trailing_void_return(s: &mut Stmt) {
1604    if let Stmt::Block(v) = s {
1605        loop {
1606            match v.last_mut() {
1607                Some(Stmt::Return(None)) => {
1608                    v.pop();
1609                }
1610                // The structurer can close a region in a bare nested
1611                // block; a `return;` at its tail is still a dangling
1612                // statement (inside a static initializer it is illegal
1613                // outright — clinit's closing return).
1614                Some(Stmt::Block(_)) => {
1615                    let last = v.last_mut().unwrap();
1616                    let before = match last {
1617                        Stmt::Block(b) => b.len(),
1618                        _ => 0,
1619                    };
1620                    strip_trailing_void_return(last);
1621                    let after = match last {
1622                        Stmt::Block(b) => b.len(),
1623                        _ => 0,
1624                    };
1625                    if after == before {
1626                        break; // nothing stripped inside
1627                    }
1628                }
1629                _ => break,
1630            }
1631        }
1632    }
1633}
1634
1635/// `if (c) { } else { B }` → `if (!c) { B }` — the empty-then shape is
1636/// how the structurer lands an inverted diamond, and source never writes
1637/// it (jadx prints the positive form). negate() carries De Morgan so
1638/// composed conditions stay clean.
1639pub fn invert_empty_thens(s: &mut Stmt) {
1640    walk_mut_deep(s, &mut |st| {
1641        if let Stmt::If {
1642            cond,
1643            then_stmt,
1644            else_stmt,
1645        } = st
1646        {
1647            let then_empty = matches!(&**then_stmt, Stmt::Block(v) if v.is_empty());
1648            if then_empty {
1649                if let Some(els) = else_stmt.take() {
1650                    let c = std::mem::replace(cond, Expr::Const(ConstVal::Int(0)));
1651                    *cond = jdc_core::convert::negate(c);
1652                    *then_stmt = els;
1653                }
1654            }
1655        }
1656    });
1657}
1658
1659/// Fold if-diamonds back into short-circuit conditions (DAD's
1660/// short_circuit_struct, statement level). The structurer emits nested
1661/// ifs for `a && b` / `a || b` bytecode diamonds; javac source almost
1662/// never nests them, and jadx folds all four shapes:
1663///   if (c1) { if (c2) {T} else {F} } else {F}  →  if (c1 && c2) {T} else {F}
1664///   if (c1) { if (c2) {F} else {T} } else {F}  →  if (c1 && !c2) {T} else {F}
1665///   if (c1) {T} else { if (c2) {T} else {F} }  →  if (c1 || c2) {T} else {F}
1666///   if (c1) {T} else { if (c2) {F} else {T} }  →  if (c1 || !c2) {T} else {F}
1667/// Branch identity is deep structural equality (Stmt: PartialEq);
1668/// iterate to a fixpoint so chained diamonds `(a && b) && c` collapse.
1669pub fn fold_short_circuits(s: &mut Stmt) {
1670    for _ in 0..8 {
1671        let mut changed = false;
1672        walk_mut_deep(s, &mut |st| {
1673            if try_fold_diamond(st) {
1674                changed = true;
1675            }
1676        });
1677        if !changed {
1678            break;
1679        }
1680    }
1681}
1682
1683fn try_fold_diamond(st: &mut Stmt) -> bool {
1684    enum Shape {
1685        ThenAnd,
1686        ThenAndNot,
1687        ElseOr,
1688        ElseOrNot,
1689        BareAnd,
1690        BareAndNot,
1691    }
1692    // Probe the shape on an immutable borrow first (the fold takes the
1693    // whole If apart; matching and rebuilding inside one borrow is not
1694    // expressible).
1695    let shape = match &*st {
1696        // One-sided diamonds: the shared branch is the implicit empty
1697        // fall-through (`if (c1) { if (c2) {T} }` → `if (c1 && c2) {T}`).
1698        Stmt::If {
1699            then_stmt,
1700            else_stmt: None,
1701            ..
1702        } => match &**then_stmt {
1703            Stmt::If {
1704                then_stmt: t2,
1705                else_stmt: None,
1706                ..
1707            } if !matches!(&**t2, Stmt::Block(v) if v.is_empty()) => Some(Shape::BareAnd),
1708            Stmt::If {
1709                then_stmt: t2,
1710                else_stmt: Some(_),
1711                ..
1712            } if matches!(&**t2, Stmt::Block(v) if v.is_empty()) => Some(Shape::BareAndNot),
1713            _ => None,
1714        },
1715        Stmt::If {
1716            then_stmt,
1717            else_stmt: Some(els),
1718            ..
1719        } => match (&**then_stmt, &**els) {
1720            (Stmt::If { else_stmt: Some(f2), .. }, _) if f2.as_ref() == els.as_ref() => {
1721                Some(Shape::ThenAnd)
1722            }
1723            (Stmt::If { then_stmt: t2, else_stmt: Some(_), .. }, _)
1724                if t2.as_ref() == els.as_ref() =>
1725            {
1726                Some(Shape::ThenAndNot)
1727            }
1728            (_, Stmt::If { then_stmt: t2, else_stmt: Some(_), .. })
1729                if t2.as_ref() == then_stmt.as_ref() =>
1730            {
1731                Some(Shape::ElseOr)
1732            }
1733            (_, Stmt::If { else_stmt: Some(f2), .. })
1734                if f2.as_ref() == then_stmt.as_ref() =>
1735            {
1736                Some(Shape::ElseOrNot)
1737            }
1738            _ => None,
1739        },
1740        _ => None,
1741    };
1742    let Some(shape) = shape else {
1743        return false;
1744    };
1745
1746    let taken = std::mem::replace(st, Stmt::Block(vec![]));
1747    // Bare (no-else) diamonds fold without an else at all.
1748    if matches!(shape, Shape::BareAnd | Shape::BareAndNot) {
1749        let Stmt::If {
1750            cond: c1,
1751            then_stmt,
1752            else_stmt: None,
1753        } = taken
1754        else {
1755            unreachable!()
1756        };
1757        let Stmt::If {
1758            cond: c2,
1759            then_stmt: t2,
1760            else_stmt: f2,
1761        } = *then_stmt
1762        else {
1763            unreachable!()
1764        };
1765        let sc = |op: BinOp, l: Expr, r: Expr| Expr::Bin {
1766            op,
1767            l: Box::new(l),
1768            r: Box::new(r),
1769            ty: None,
1770        };
1771        match shape {
1772            // if (c1 && c2) {T}
1773            Shape::BareAnd => {
1774                *st = Stmt::If {
1775                    cond: sc(BinOp::LogAnd, c1, c2),
1776                    then_stmt: t2,
1777                    else_stmt: None,
1778                };
1779            }
1780            // if (c1) { if (c2) {} else {T} }  →  if (c1 && !c2) {T}
1781            _ => {
1782                let body = f2.unwrap();
1783                *st = Stmt::If {
1784                    cond: sc(
1785                        BinOp::LogAnd,
1786                        c1,
1787                        Expr::Un {
1788                            op: UnOp::Not,
1789                            e: Box::new(c2),
1790                        },
1791                    ),
1792                    then_stmt: body,
1793                    else_stmt: None,
1794                };
1795            }
1796        }
1797        return true;
1798    }
1799    let Stmt::If {
1800        cond: c1,
1801        then_stmt,
1802        else_stmt: Some(els),
1803    } = taken
1804    else {
1805        unreachable!("shape probe guaranteed an If with else")
1806    };
1807    let sc = |op: BinOp, l: Expr, r: Expr| Expr::Bin {
1808        op,
1809        l: Box::new(l),
1810        r: Box::new(r),
1811        ty: None,
1812    };
1813    let not = |e: Expr| Expr::Un {
1814        op: UnOp::Not,
1815        e: Box::new(e),
1816    };
1817    match shape {
1818        Shape::BareAnd | Shape::BareAndNot => unreachable!("handled above"),
1819        Shape::ThenAnd | Shape::ThenAndNot => {
1820            let Stmt::If {
1821                cond: c2,
1822                then_stmt: t2,
1823                else_stmt: Some(f2),
1824            } = *then_stmt
1825            else {
1826                unreachable!()
1827            };
1828            let (nc, body, alt) = match shape {
1829                // if (c1 && c2) {T} else {F}
1830                Shape::ThenAnd => (sc(BinOp::LogAnd, c1, c2), t2, f2),
1831                // if (c1 && !c2) {T} else {F}   (inner: {F} else {T})
1832                _ => (sc(BinOp::LogAnd, c1, not(c2)), f2, t2),
1833            };
1834            *st = Stmt::If {
1835                cond: nc,
1836                then_stmt: body,
1837                else_stmt: Some(alt),
1838            };
1839        }
1840        Shape::ElseOr | Shape::ElseOrNot => {
1841            let Stmt::If {
1842                cond: c2,
1843                then_stmt: t2,
1844                else_stmt: Some(f2),
1845            } = *els
1846            else {
1847                unreachable!()
1848            };
1849            // The surviving then-body is the OUTER then (identical to the
1850            // matching inner branch by the probe).
1851            let body = then_stmt;
1852            let (nc, alt) = match shape {
1853                // if (c1 || c2) {T} else {F}   (inner: {T} else {F})
1854                Shape::ElseOr => (sc(BinOp::LogOr, c1, c2), f2),
1855                // if (c1 || !c2) {T} else {F}  (inner: {F} else {T})
1856                _ => (sc(BinOp::LogOr, c1, not(c2)), t2),
1857            };
1858            *st = Stmt::If {
1859                cond: nc,
1860                then_stmt: body,
1861                else_stmt: Some(alt),
1862            };
1863        }
1864    }
1865    true
1866}
1867
1868/// A static initializer cannot contain `return` in source form — the
1869/// clinit's closing return is a method-level artifact, but the
1870/// structurer can leave it nested inside loops/branches where the
1871/// top-level strip cannot see it (x2/a: `static { while { ...; return;
1872/// } }` — "return outside method", 184 sites on reqable).
1873pub fn strip_clinit_returns(s: &mut Stmt) {
1874    walk_mut_deep(s, &mut |st| {
1875        if let Stmt::Block(v) = st {
1876            v.retain(|x| !matches!(x, Stmt::Return(None)));
1877        }
1878    });
1879}
1880
1881pub fn prepend_comment(s: &mut Stmt, text: String) {
1882    let stmt = Stmt::Comment(text);
1883    match s {
1884        Stmt::Block(v) => v.insert(0, stmt),
1885        other => {
1886            let inner = std::mem::replace(other, Stmt::Block(vec![]));
1887            *other = Stmt::Block(vec![stmt, inner]);
1888        }
1889    }
1890}
1891
1892/// Fused expression rewrites that each used to walk the whole tree:
1893/// residual cmp sentinels, object-null comparisons, const-first compares.
1894/// One traversal, three rules — the separate walks were a top profile cost.
1895/// True when `e` evaluates to an object reference — a legal null-compare
1896/// side: locals via the reference table, everything else via its own type
1897/// (fields like `pi.versionName != 0` and call results are references too;
1898/// constants never are).
1899fn is_obj_expr(e: &Expr, obj_var: &dyn Fn(u32) -> bool) -> bool {
1900    match e {
1901        Expr::Local { var, .. } => obj_var(*var),
1902        Expr::Const(_) => false,
1903        _ => e.type_ref().erased().is_reference(),
1904    }
1905}
1906
1907/// `obj == 0` / `0 == obj` → `obj == null`: replace the CONST-0 side.
1908/// The previous shape replaced the LOCAL side instead — every object
1909/// null-check in the corpus rendered as `null != 0` (4,598 hits in
1910/// reqable alone) and the real operand vanished from the condition.
1911fn null_side_rewrite(x: &mut Expr, obj_var: &dyn Fn(u32) -> bool) {
1912    if let Expr::Bin { op, l, r, .. } = x {
1913        if !matches!(op, BinOp::Eq | BinOp::Ne) {
1914            return;
1915        }
1916        let l_zero = matches!(&**l, Expr::Const(ConstVal::Int(0)));
1917        let r_zero = matches!(&**r, Expr::Const(ConstVal::Int(0)));
1918        if l_zero && !r_zero && is_obj_expr(r, obj_var) {
1919            **l = Expr::Const(ConstVal::Null);
1920        } else if r_zero && !l_zero && is_obj_expr(l, obj_var) {
1921            **r = Expr::Const(ConstVal::Null);
1922        }
1923    }
1924}
1925
1926pub fn fused_expr_rewrites(s: &mut Stmt, vt: &VarTable) {
1927    // var ids are dense: a byte table beats a HashSet lookup.
1928    let mut obj_vars: Vec<bool> = Vec::with_capacity(vt.vars.len());
1929    for v in &vt.vars {
1930        obj_vars.push(v.ty.erased().is_reference());
1931    }
1932    rewrite_exprs(s, &mut |e| {
1933        deep_rewrite(e, &mut |x| {
1934            // 1. cmp sentinels.
1935            if let Expr::Invokedynamic { name, args, .. } = x {
1936                if name.starts_with('\0') && args.len() == 2 {
1937                    let (cls, ty) = match name.as_str() {
1938                        "\0cmp-long" => ("java/lang/Long", JavaType::Long),
1939                        "\0cmpl-float" | "\0cmpg-float" => ("java/lang/Float", JavaType::Float),
1940                        _ => ("java/lang/Double", JavaType::Double),
1941                    };
1942                    let desc = MethodDescriptor {
1943                        args: vec![ty.clone(), ty],
1944                        ret: JavaType::Int,
1945                    };
1946                    *x = Expr::Method {
1947                        owner: None,
1948                        cls: cls.into(),
1949                        name: "compare".into(),
1950                        desc: std::sync::Arc::new(desc),
1951                        args: args.clone(),
1952                        is_static: true,
1953                        is_interface: false,
1954                        is_special: false,
1955                        is_super: false,
1956                        is_dynamic: false,
1957                        type_args: vec![],
1958                    };
1959                    return;
1960                }
1961            }
1962            // 2. null compares (no any_obj gate: the object side may be
1963            // a field/call whose type is not in the local table).
1964            null_side_rewrite(x, &|v| obj_vars.get(v as usize).copied().unwrap_or(false));
1965            // 3. const-first compares.
1966            if let Expr::Bin { op, l, r, .. } = x {
1967                if matches!(
1968                    op,
1969                    BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
1970                ) {
1971                    let l_const = matches!(&**l, Expr::Const(_));
1972                    let r_var = matches!(&**r, Expr::Local { .. });
1973                    if l_const && r_var {
1974                        std::mem::swap(l, r);
1975                    }
1976                }
1977            }
1978        });
1979    });
1980    // 4. null into reference-typed assignment targets. The register
1981    //    machine stores null as const-0; return/throw/field-store/invoke
1982    //    argument paths were normalized in the lifter, but a null that
1983    //    MATERIALIZES into a local was rendered as `str = 0;` — javac
1984    //    "int cannot be converted to String" (the single biggest error
1985    //    family on real corpora: d8 emits null locals this way after
1986    //    every `x = null` branch).
1987    walk_mut_deep(s, &mut |st| {
1988        if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = st {
1989            let tgt_is_obj = match &**target {
1990                Expr::Local { var, .. } => {
1991                    obj_vars.get(*var as usize).copied().unwrap_or(false)
1992                }
1993                other => other.type_ref().erased().is_reference(),
1994            };
1995            if tgt_is_obj {
1996                if let Expr::Const(ConstVal::Int(0)) = &**value {
1997                    **value = Expr::Const(ConstVal::Null);
1998                }
1999            }
2000        } else if let Stmt::LocalDef { var, init, .. } = st {
2001            if obj_vars.get(*var as usize).copied().unwrap_or(false) {
2002                if let Some(iv) = init {
2003                    if let Expr::Const(ConstVal::Int(0)) = iv {
2004                        *iv = Expr::Const(ConstVal::Null);
2005                    }
2006                }
2007            }
2008        }
2009    });
2010}
2011
2012/// Replace residual cmp sentinels (`\0cmp*` Invokedynamic markers that never
2013/// reached a condition) with library compare calls.
2014#[allow(dead_code)]
2015pub fn desugar_cmp_residuals(s: &mut Stmt) {
2016    rewrite_exprs(s, &mut |e| {
2017        deep_rewrite(e, &mut |x| {
2018            if let Expr::Invokedynamic { name, args, .. } = x {
2019                if name.starts_with('\0') && args.len() == 2 {
2020                    let (cls, ty) = match name.as_str() {
2021                        "\0cmp-long" => ("java/lang/Long", JavaType::Long),
2022                        "\0cmpl-float" | "\0cmpg-float" => ("java/lang/Float", JavaType::Float),
2023                        _ => ("java/lang/Double", JavaType::Double),
2024                    };
2025                    let desc = jdc_core::types::MethodDescriptor {
2026                        args: vec![ty.clone(), ty],
2027                        ret: JavaType::Int,
2028                    };
2029                    *x = Expr::Method {
2030                        owner: None,
2031                        cls: cls.into(),
2032                        name: "compare".into(),
2033                        desc: std::sync::Arc::new(desc),
2034                        args: args.clone(),
2035                        is_static: true,
2036                        is_interface: false,
2037                        is_special: false,
2038                        is_super: false,
2039                        is_dynamic: false,
2040                        type_args: vec![],
2041                    };
2042                }
2043            }
2044        });
2045    });
2046}
2047
2048/// `if (c) { x = A; } else { x = B; }` → `x = c ? A : B;`
2049pub fn ternary_fold(s: &mut Stmt) {
2050    let mut changed = true;
2051    let mut guard = 0;
2052    while changed && guard < 8 {
2053        changed = false;
2054        guard += 1;
2055        ternary_fold_walk(s, &mut changed);
2056        strip_empty(s);
2057    }
2058}
2059
2060fn ternary_fold_walk(s: &mut Stmt, changed: &mut bool) {
2061    if let Stmt::Block(v) = s {
2062        for x in v.iter_mut() {
2063            ternary_fold_walk(x, changed);
2064        }
2065        return;
2066    }
2067    let Stmt::If {
2068        cond,
2069        then_stmt,
2070        else_stmt,
2071    } = s
2072    else {
2073        return;
2074    };
2075    ternary_fold_walk(then_stmt, changed);
2076    if let Some(e) = else_stmt {
2077        ternary_fold_walk(e, changed);
2078    }
2079    let Some(else_b) = else_stmt else { return };
2080    let target_of = |st: &Stmt| -> Option<u32> {
2081        let Stmt::ExprStmt(Expr::Assign { target, .. }) = st else {
2082            return None;
2083        };
2084        if let Expr::Local { var, .. } = &**target {
2085            Some(*var)
2086        } else {
2087            None
2088        }
2089    };
2090    let (Stmt::Block(tv), Stmt::Block(ev)) = (then_stmt.as_mut(), else_b.as_mut()) else {
2091        return;
2092    };
2093    if tv.len() != 1 || ev.len() != 1 {
2094        return;
2095    }
2096    let (Some(vt_), Some(ve)) = (target_of(&tv[0]), target_of(&ev[0])) else {
2097        return;
2098    };
2099    if vt_ != ve {
2100        return;
2101    }
2102    // The condition and both values must not re-assign the target between
2103    // the fold — they are single statements, so a mention is a re-read at
2104    // worst; re-assignment only happens via assignments inside them.
2105    let mut touched = HashSet::default();
2106    let (Stmt::ExprStmt(a_then), Stmt::ExprStmt(a_else)) = (&tv[0], &ev[0]) else {
2107        return;
2108    };
2109    if let Expr::Assign { value, .. } = a_then {
2110        collect_vars(value, &mut touched);
2111    }
2112    if let Expr::Assign { value, .. } = a_else {
2113        collect_vars(value, &mut touched);
2114    }
2115    if touched.contains(&vt_) {
2116        return;
2117    }
2118    let value_then = match a_then {
2119        Expr::Assign { value, .. } => value.clone(),
2120        _ => return,
2121    };
2122    let value_else = match a_else {
2123        Expr::Assign { value, .. } => value.clone(),
2124        _ => return,
2125    };
2126    let cond_e = cond.clone();
2127    let folded = Stmt::ExprStmt(Expr::Assign {
2128        target: Box::new(Expr::Local {
2129            var: vt_,
2130            ty: vt_join_ty(&value_then, &value_else),
2131        }),
2132        op: AssignOp::Plain,
2133        value: Box::new(Expr::Cond {
2134            c: Box::new(cond_e),
2135            t: value_then,
2136            f: value_else,
2137        }),
2138    });
2139    *s = Stmt::Block(vec![folded]);
2140    *changed = true;
2141}
2142
2143fn vt_join_ty(a: &Expr, b: &Expr) -> TypeRef {
2144    let ta = a.type_ref();
2145    let tb = b.type_ref();
2146    if ta.erased() == tb.erased() {
2147        return ta;
2148    }
2149    TypeRef::J(JavaType::Object("java/lang/Object".into()))
2150}
2151
2152/// Fold `new StringBuilder(...).append(x)...toString()` chains into
2153/// `StringConcat`. Single-assignment temporaries carry the chain.
2154///
2155/// d8 drops append results (statement-form calls), so a builder whose chain
2156/// has STATEMENT appends attached is left alone: folding its `toString`
2157/// with only the ctor arguments would misstate the contents.
2158pub fn fold_string_builders(s: &mut Stmt, vt: &VarTable) {
2159    // var → assigned value, for vars assigned exactly once.
2160    let n_vars = vt.vars.len().max(1);
2161    let mut counts = vec![0usize; n_vars];
2162    count_assignments(s, &mut counts);
2163    let mut values: Vec<Option<Expr>> = vec![None; n_vars];
2164    record_assignments(s, &counts, &mut values);
2165
2166    // Builders with statement-form appends (their chains are incomplete).
2167    let mut appended_stmts: HashSet<u32> = HashSet::default();
2168    walk_all(s, &mut |st| {
2169        if let Stmt::ExprStmt(Expr::Method {
2170            cls,
2171            name,
2172            owner,
2173            args,
2174            ..
2175        }) = st
2176        {
2177            if name.as_ref() == "append" && is_string_builder(cls) && args.len() == 1 {
2178                if let Some(Expr::Local { var, .. }) = owner.as_deref() {
2179                    appended_stmts.insert(*var);
2180                }
2181            }
2182        }
2183    });
2184
2185    rewrite_exprs(s, &mut |e| {
2186        fold_concat_in_expr(e, &values, &appended_stmts);
2187    });
2188
2189    // Drop now-unused builder temporaries (their only consumers were
2190    // folded); the drop COUNT drives iteration — no statement counting
2191    // walks at all.
2192    for _ in 0..8 {
2193        let mut reads: HashSet<u32> = HashSet::default();
2194        stmt_collect_vars(s, &mut reads, false);
2195        if drop_unused_assigns(s, &reads) == 0 {
2196            break;
2197        }
2198    }
2199}
2200
2201/// Deep statement count (pipeline guard input).
2202pub fn count_stmts_deep(s: &Stmt, out: &mut usize) {
2203    *out += 1;
2204    walk_all(s, &mut |_| {
2205        *out += 1;
2206    });
2207}
2208
2209/// Fused per-variable analysis gathered in one statement-tree walk.
2210struct VarAnalysis {
2211    assigns: Vec<usize>,
2212    reads: Vec<usize>,
2213    /// Value of the (single) assignment, when recorded.
2214    values: Vec<Option<Expr>>,
2215}
2216
2217fn analyze_vars(s: &Stmt, a: &mut VarAnalysis) {
2218    walk_all(s, &mut |st| {
2219        let exprs: Vec<&Expr> = match st {
2220            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
2221                if let Expr::Local { var, .. } = &**target {
2222                    grow_to(&mut a.assigns, *var);
2223                    let idx = *var as usize;
2224                    a.assigns[idx] += 1;
2225                    if a.assigns[idx] == 1 {
2226                        if idx >= a.values.len() {
2227                            a.values.resize(idx + 1, None);
2228                        }
2229                        a.values[idx] = Some((**value).clone());
2230                    } else if idx < a.values.len() {
2231                        a.values[idx] = None;
2232                    }
2233                }
2234                let mut v: Vec<&Expr> = vec![value];
2235                if !matches!(&**target, Expr::Local { .. }) {
2236                    v.push(target);
2237                }
2238                v
2239            }
2240            Stmt::ExprStmt(e @ (Expr::PreIncDec { .. } | Expr::PostIncDec { .. })) => {
2241                // `v++` is a read-modify-WRITE of v: counting it as a
2242                // plain read makes v a single-assign/single-read forward
2243                // candidate, and value-forwarding then fabricates `5++`
2244                // (or drops v's def and leaves ++ on an undefined name).
2245                let inner = match e {
2246                    Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => e,
2247                    _ => unreachable!(),
2248                };
2249                if let Expr::Local { var, .. } = &**inner {
2250                    grow_to(&mut a.assigns, *var);
2251                    let idx = *var as usize;
2252                    a.assigns[idx] += 1;
2253                    if idx < a.values.len() {
2254                        a.values[idx] = None;
2255                    }
2256                    return;
2257                }
2258                vec![e]
2259            }
2260            Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
2261                vec![e]
2262            }
2263            Stmt::Return(Some(e)) => vec![e],
2264            Stmt::LocalDef {
2265                var, init: Some(e), ..
2266            } => {
2267                grow_to(&mut a.assigns, *var);
2268                let idx = *var as usize;
2269                a.assigns[idx] += 1;
2270                if a.assigns[idx] == 1 {
2271                    if idx >= a.values.len() {
2272                        a.values.resize(idx + 1, None);
2273                    }
2274                    a.values[idx] = Some(e.clone());
2275                } else if idx < a.values.len() {
2276                    a.values[idx] = None;
2277                }
2278                vec![e]
2279            }
2280            Stmt::If { cond, .. } => vec![cond],
2281            Stmt::While { cond, .. } => vec![cond],
2282            Stmt::DoWhile { cond, .. } => vec![cond],
2283            _ => Vec::new(),
2284        };
2285        for e in exprs {
2286            visit_exprs(e, &mut |x| {
2287                if let Expr::Local { var, .. } = x {
2288                    grow_to(&mut a.reads, *var);
2289                    a.reads[*var as usize] += 1;
2290                }
2291            });
2292        }
2293    });
2294    // Keep the three views length-aligned.
2295    let n = a.assigns.len().max(a.reads.len()).max(a.values.len());
2296    a.assigns.resize(n, 0);
2297    a.reads.resize(n, 0);
2298    a.values.resize(n, None);
2299}
2300
2301fn grow_to(v: &mut Vec<usize>, var: u32) {
2302    if var as usize >= v.len() {
2303        v.resize(var as usize + 1, 0);
2304    }
2305}
2306
2307fn count_assignments(s: &Stmt, counts: &mut Vec<usize>) {
2308    walk_all(s, &mut |st| match st {
2309        Stmt::ExprStmt(Expr::Assign { target, .. }) => {
2310            if let Expr::Local { var, .. } = &**target {
2311                grow_to(counts, *var);
2312                counts[*var as usize] += 1;
2313            }
2314        }
2315        Stmt::LocalDef {
2316            var, init: Some(_), ..
2317        } => {
2318            grow_to(counts, *var);
2319            counts[*var as usize] += 1;
2320        }
2321        _ => {}
2322    });
2323}
2324
2325fn record_assignments(s: &Stmt, counts: &[usize], out: &mut Vec<Option<Expr>>) {
2326    walk_all(s, &mut |st| match st {
2327        Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
2328            if let Expr::Local { var, .. } = &**target {
2329                if counts.get(*var as usize).copied().unwrap_or(0) == 1 {
2330                    if *var as usize >= out.len() {
2331                        out.resize(*var as usize + 1, None);
2332                    }
2333                    out[*var as usize] = Some((**value).clone());
2334                }
2335            }
2336        }
2337        Stmt::LocalDef {
2338            var, init: Some(e), ..
2339        } if counts.get(*var as usize).copied().unwrap_or(0) == 1 => {
2340            if *var as usize >= out.len() {
2341                out.resize(*var as usize + 1, None);
2342            }
2343            out[*var as usize] = Some(e.clone());
2344        }
2345        _ => {}
2346    });
2347}
2348
2349fn fold_concat_in_expr(e: &mut Expr, values: &[Option<Expr>], appended: &HashSet<u32>) {
2350    deep_rewrite(e, &mut |x| {
2351        if let Expr::Method {
2352            cls,
2353            name,
2354            args,
2355            owner,
2356            ..
2357        } = x
2358        {
2359            if name.as_ref() == "toString" && args.is_empty() && is_string_builder(cls) {
2360                if let Some(o) = owner {
2361                    // Statement-form appends attached to any chain var mean
2362                    // the parts are incomplete — keep the call.
2363                    let mut has_stmt_appends = false;
2364                    visit_exprs(o, &mut |y| {
2365                        if let Expr::Local { var, .. } = y {
2366                            if appended.contains(var) {
2367                                has_stmt_appends = true;
2368                            }
2369                        }
2370                    });
2371                    if has_stmt_appends {
2372                        return;
2373                    }
2374                    let resolved = resolve_local(o, values);
2375                    if let Some(parts) = collect_sb_parts(&resolved, values, 0) {
2376                        // NEVER fold an empty chain: d8 splits builder
2377                        // chains across alias registers (`v2 = v0.append
2378                        // (x); v2.append(y); v0.toString()`) — the traced
2379                        // chain sees a bare `new StringBuilder` and would
2380                        // fold toString() to "" (a wrong VALUE, silently:
2381                        // lab package Obf.label). With zero parts the
2382                        // call stays — always correct, usually folded
2383                        // elsewhere once the parts are visible.
2384                        if !parts.is_empty() {
2385                            *x = Expr::StringConcat(parts);
2386                        }
2387                    }
2388                }
2389            }
2390        }
2391    });
2392}
2393
2394fn is_string_builder(cls: &str) -> bool {
2395    cls == "java/lang/StringBuilder" || cls == "java/lang/StringBuffer"
2396}
2397
2398fn resolve_local(e: &Expr, values: &[Option<Expr>]) -> Expr {
2399    match e {
2400        Expr::Local { var, ty } => values
2401            .get(*var as usize)
2402            .and_then(|o| o.clone())
2403            .unwrap_or_else(|| Expr::Local {
2404                var: *var,
2405                ty: ty.clone(),
2406            }),
2407        other => other.clone(),
2408    }
2409}
2410
2411/// Collect concat parts from a StringBuilder chain; depth caps cycles.
2412fn collect_sb_parts(e: &Expr, values: &[Option<Expr>], depth: u32) -> Option<Vec<ConcatPart>> {
2413    if depth > 16 {
2414        return None;
2415    }
2416    match e {
2417        Expr::Method {
2418            cls,
2419            name,
2420            args,
2421            owner,
2422            ..
2423        } if name.as_ref() == "append" && is_string_builder(cls) && args.len() == 1 => {
2424            let mut parts =
2425                collect_sb_parts(&resolve_local(owner.as_deref()?, values), values, depth + 1)?;
2426            parts.push(ConcatPart::Str(args[0].clone()));
2427            Some(parts)
2428        }
2429        Expr::New {
2430            cls,
2431            args,
2432            raw: false,
2433            ..
2434        } if is_string_builder(cls) => {
2435            let mut parts = Vec::new();
2436            for a in args {
2437                if let Expr::Const(ConstVal::Str(sv)) = a {
2438                    parts.push(ConcatPart::Const(sv.to_string()));
2439                } else {
2440                    parts.push(ConcatPart::Str(a.clone()));
2441                }
2442            }
2443            Some(parts)
2444        }
2445        _ => None,
2446    }
2447}
2448
2449fn drop_unused_assigns(s: &mut Stmt, reads: &HashSet<u32>) -> usize {
2450    let mut dropped = 0usize;
2451    walk_mut_deep(s, &mut |st| {
2452        if let Stmt::Block(v) = st {
2453            let before = v.len();
2454            v.retain(|x| match x {
2455                // Drop only when unread AND a builder chain (folded
2456                // consumer; `new StringBuilder` chains cannot NPE).
2457                Stmt::ExprStmt(Expr::Assign { target, value, .. }) => match &**target {
2458                    Expr::Local { var, .. } => reads.contains(var) || !is_sbish(value),
2459                    _ => true,
2460                },
2461                Stmt::LocalDef { var, init, .. } => {
2462                    reads.contains(var) || !init.as_ref().map(is_sbish).unwrap_or(false)
2463                }
2464                _ => true,
2465            });
2466            dropped += before - v.len();
2467        }
2468    });
2469    dropped
2470}
2471
2472fn is_sbish(e: &Expr) -> bool {
2473    match e {
2474        Expr::New {
2475            cls, raw: false, ..
2476        } => is_string_builder(cls),
2477        Expr::Method { name, cls, .. } => name.as_ref() == "append" && is_string_builder(cls),
2478        _ => false,
2479    }
2480}
2481
2482fn walk_mut_deep<F: FnMut(&mut Stmt)>(s: &mut Stmt, f: &mut F) {
2483    f(s);
2484    walk_mut(s, &mut |x| walk_mut_deep(x, f));
2485}
2486
2487/// Drop orphaned raw `new-instance` statements. R8 sometimes separates
2488/// `new-instance vR` from its `<init>` invoke by a register copy
2489/// (`move-object vS, vR; invoke vS.<init>(..)` — dex-legal: the init
2490/// initializes the object both registers name). The fold pairs the init
2491/// with the COPY, and the structurer's branch duplication re-materializes
2492/// the original raw marker as a bare `new C();` statement on paths whose
2493/// uses were rewired to the folded side (weixin sns/storage/c0: four
2494/// `new s1();` beside the real `s1 s1x = new s1(..)` — s1 has no no-arg
2495/// ctor, "无法将类 C的构造器 C应用到给定类型" ×195). A raw New in
2496/// statement position binds no variable and initializes nothing — pure
2497/// verifier-artifact dead code.
2498pub fn drop_dead_raw_news(body: &mut Stmt) {
2499    fn is_orphan(s: &Stmt) -> bool {
2500        matches!(s, Stmt::ExprStmt(Expr::New { raw: true, .. }))
2501    }
2502    walk_mut_deep(body, &mut |st| match st {
2503        Stmt::Block(v) => v.retain(|x| !is_orphan(x)),
2504        Stmt::Switch { cases, .. } => {
2505            for c in cases.iter_mut() {
2506                c.body.retain(|x| !is_orphan(x));
2507            }
2508        }
2509        Stmt::For { init, .. } => init.retain(|x| !is_orphan(x)),
2510        Stmt::TryWithResources { resources, .. } => resources.retain(|x| !is_orphan(x)),
2511        _ => {}
2512    });
2513}
2514
2515/// Recover `synchronized` from the d8 monitor pattern:
2516/// `monitorenter(e); try { body } catch (Throwable) { monitorexit(e); throw t; }`
2517/// (optionally followed by `monitorexit(e)`).
2518pub fn fold_synchronized(s: &mut Stmt) {
2519    fold_sync_walk(s);
2520}
2521
2522fn fold_sync_walk(s: &mut Stmt) {
2523    walk_mut_deep(s, &mut |st| {
2524        let Stmt::Block(v) = st else { return };
2525        let mut i = 0;
2526        while i < v.len() {
2527            if i + 1 < v.len() {
2528                if let Some(sync) = try_sync_at(v, i) {
2529                    let _ = v.drain(i..i + 2);
2530                    v.insert(i, sync);
2531                    continue;
2532                }
2533            }
2534            i += 1;
2535        }
2536    });
2537}
2538
2539fn try_sync_at(v: &[Stmt], i: usize) -> Option<Stmt> {
2540    let Stmt::MonitorEnter(lock) = &v[i] else {
2541        return None;
2542    };
2543    let Stmt::Try {
2544        body,
2545        catches,
2546        finally,
2547    } = &v[i + 1]
2548    else {
2549        return None;
2550    };
2551    if finally.is_some() || catches.len() != 1 {
2552        return None;
2553    }
2554    let c = &catches[0];
2555    if !c.exc.is_empty() {
2556        return None;
2557    }
2558    // The catch-all body: monitorexit(lock) then rethrow.
2559    let Stmt::Block(cv) = c.body.as_ref() else {
2560        return None;
2561    };
2562    let exit_matches = cv.len() >= 2
2563        && matches!(&cv[0], Stmt::MonitorExit(e) if expr_local_var(e) == expr_local_var(lock));
2564    let throws = cv.len() >= 2 && matches!(&cv[1], Stmt::Throw(_));
2565    if !(exit_matches && throws) {
2566        return None;
2567    }
2568    Some(Stmt::Synchronized {
2569        lock: lock.clone(),
2570        body: body.clone(),
2571    })
2572}
2573
2574fn expr_local_var(e: &Expr) -> Option<u32> {
2575    if let Expr::Local { var, .. } = e {
2576        Some(*var)
2577    } else {
2578        None
2579    }
2580}
2581
2582/// Copy-forward single-use temporaries: `v = e; ... v ...` inlines `e` at
2583/// the (single) use and drops the assignment when safe. Pure values inline
2584/// anywhere; impure values (calls, array reads) only when the use is the
2585/// statement immediately after the definition.
2586pub fn forward_single_use(s: &mut Stmt, _vt: &VarTable) {
2587    // Fused single traversal: assignment counts, read counts and the
2588    // single-assignment values all come out of ONE walk (three separate
2589    // full-tree walks dominated the pass cost on large methods).
2590    let mut analysis = VarAnalysis {
2591        assigns: Vec::new(),
2592        reads: Vec::new(),
2593        values: Vec::new(),
2594    };
2595    analyze_vars(s, &mut analysis);
2596    let mut assigns = analysis.assigns;
2597    let mut reads = analysis.reads;
2598    // Var ids beyond the table (synthetic test shapes) are covered by the
2599    // counters' on-demand growth.
2600    let n_vars = assigns.len().max(_vt.vars.len()).max(1);
2601    assigns.resize(n_vars, 0);
2602    reads.resize(n_vars, 0);
2603    let mut values = analysis.values;
2604    values.resize(n_vars, None);
2605
2606    // Candidates: assigned exactly once, read exactly once. Additionally
2607    // the inlined VALUE must not reference a multi-assigned var (phi vars):
2608    // moving the read across the phi's reassignment would be a stale
2609    // capture (register rotations snapshot through temps for this reason).
2610    let mut cand = vec![false; n_vars];
2611    let mut edges: Vec<Vec<u32>> = vec![Vec::new(); n_vars];
2612    for (v, cand_v) in cand.iter_mut().enumerate() {
2613        if assigns.get(v).copied().unwrap_or(0) != 1 || reads.get(v).copied().unwrap_or(0) != 1 {
2614            continue;
2615        }
2616        if let Some(val) = values.get(v).and_then(|o| o.as_ref()) {
2617            let mut refs = HashSet::default();
2618            collect_vars(val, &mut refs);
2619            if refs.iter().any(|r| assigns[*r as usize] > 1) {
2620                continue;
2621            }
2622            // Growth-graph edge set, built in the same walk: inlining v
2623            // inserts a clone of its value, and every Local inside the
2624            // clone is replaced in turn.
2625            edges[v] = refs.into_iter().collect();
2626        }
2627        *cand_v = true;
2628    }
2629
2630    // Cycle rejection on the growth graph. A def-reference cycle
2631    // (v = f(w), w = g(v) — loop-carried register rotation reaching the
2632    // pass as two mutually-referencing single-assign/single-read locals)
2633    // re-introduces the other cycle Local at every replacement level, so
2634    // deep_rewrite grows the tree one layer per level and never
2635    // terminates: weixin's com/tencent/mm/plugin/appbrand/widget/input/b4
2636    // exhausted a 64MB worker stack at ~300k recursion frames. Edges into
2637    // non-candidates are harmless (they are never replaced, so growth
2638    // dies there — and they carry no outgoing edges).
2639    //
2640    // Three-color DFS, iterative: a legit 60k-long single-use chain must
2641    // not trade one stack overflow for another. A GRAY child closes a
2642    // cycle; `bad[v]` (v on a cycle, or v's inlined closure grows into
2643    // one) then taints the whole current path — everything on it reaches
2644    // the cycle. Black verdicts memoize: a node whose subtree was proven
2645    // clean cannot grow a cycle later.
2646    let mut color = vec![0u8; n_vars]; // 0 white, 1 gray, 2 black
2647    let mut bad = vec![false; n_vars];
2648    for root in 0..n_vars {
2649        if color[root] != 0 || edges[root].is_empty() {
2650            continue;
2651        }
2652        color[root] = 1;
2653        let mut stack: Vec<(usize, usize)> = vec![(root, 0)];
2654        while let Some(top) = stack.last_mut() {
2655            let v = top.0;
2656            let i = top.1;
2657            top.1 += 1;
2658            if i < edges[v].len() {
2659                let r = edges[v][i] as usize;
2660                if color[r] == 1 {
2661                    for &(pn, _) in &stack {
2662                        bad[pn] = true;
2663                    }
2664                } else if color[r] == 0 {
2665                    color[r] = 1;
2666                    stack.push((r, 0));
2667                } else if bad[r] {
2668                    for &(pn, _) in &stack {
2669                        bad[pn] = true;
2670                    }
2671                }
2672            } else {
2673                color[v] = 2;
2674                stack.pop();
2675            }
2676        }
2677    }
2678    let single: Vec<bool> = (0..n_vars).map(|v| cand[v] && !bad[v]).collect();
2679    if !single.iter().any(|&b| b) {
2680        return;
2681    }
2682    let pure: Vec<bool> = (0..n_vars)
2683        .map(|v| {
2684            single[v]
2685                && values
2686                    .get(v)
2687                    .and_then(|o| o.as_ref())
2688                    .map(|e| !has_side_effects(e))
2689                    .unwrap_or(false)
2690        })
2691        .collect();
2692    let impure: Vec<bool> = (0..n_vars).map(|v| single[v] && !pure[v]).collect();
2693
2694    // 1. Pure values: inline anywhere.
2695    if pure.iter().any(|&b| b) {
2696        let vals: Vec<Option<Expr>> = (0..n_vars)
2697            .map(|v| {
2698                if pure[v] {
2699                    values.get(v).cloned().flatten()
2700                } else {
2701                    None
2702                }
2703            })
2704            .collect();
2705        rewrite_exprs(s, &mut |e| {
2706            deep_rewrite_reads(e, &mut |x| {
2707                if let Expr::Local { var, .. } = x {
2708                    if let Some(Some(v)) = vals.get(*var as usize) {
2709                        *x = v.clone();
2710                    }
2711                }
2712            });
2713        });
2714        drop_defs(s, &pure);
2715    }
2716
2717    // 2. Impure values: inline only into the immediately following
2718    //    statement.
2719    if impure.iter().any(|&b| b) {
2720        forward_adjacent_impure(s, &values, &impure);
2721    }
2722}
2723
2724/// Count READS of variables (assignment targets excluded).
2725fn count_reads(s: &Stmt, out: &mut Vec<usize>) {
2726    walk_all(s, &mut |st| {
2727        let exprs: Vec<&Expr> = match st {
2728            // Assign must match BEFORE the generic ExprStmt arm (the
2729            // assignment target is a write, not a read).
2730            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
2731                let mut v: Vec<&Expr> = vec![value];
2732                if !matches!(&**target, Expr::Local { .. }) {
2733                    v.push(target);
2734                }
2735                v
2736            }
2737            // `v++` writes v — not a read (a compound `a[i]++` still
2738            // reads its owner/index subtree).
2739            Stmt::ExprStmt(e @ (Expr::PreIncDec { .. } | Expr::PostIncDec { .. })) => {
2740                let inner = match e {
2741                    Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => e,
2742                    _ => unreachable!(),
2743                };
2744                if matches!(**inner, Expr::Local { .. }) {
2745                    Vec::new()
2746                } else {
2747                    vec![e]
2748                }
2749            }
2750            Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
2751                vec![e]
2752            }
2753            Stmt::Return(Some(e)) => vec![e],
2754            Stmt::LocalDef { init: Some(e), .. } => vec![e],
2755            Stmt::If { cond, .. } => vec![cond],
2756            Stmt::While { cond, .. } => vec![cond],
2757            Stmt::DoWhile { cond, .. } => vec![cond],
2758            // Read sites that are NOT statement children (walk_all recurses
2759            // into bodies but these payloads are expressions on the node
2760            // itself). Omitting them under-counts reads, which would let
2761            // drop_dead_locals over-prune a var whose only use is a switch
2762            // selector / loop condition / lock / iterable.
2763            Stmt::Switch { selector, .. } => vec![selector],
2764            Stmt::ForEach { iterable, .. } => vec![iterable],
2765            Stmt::Synchronized { lock, .. } => vec![lock],
2766            Stmt::For { cond, update, .. } => {
2767                let mut v: Vec<&Expr> = Vec::with_capacity(update.len() + 1);
2768                if let Some(c) = cond {
2769                    v.push(c);
2770                }
2771                v.extend(update.iter());
2772                v
2773            }
2774            Stmt::Assert { cond, msg } => {
2775                let mut v: Vec<&Expr> = vec![cond];
2776                if let Some(m) = msg {
2777                    v.push(m);
2778                }
2779                v
2780            }
2781            _ => Vec::new(),
2782        };
2783        for e in exprs {
2784            visit_exprs(e, &mut |x| {
2785                if let Expr::Local { var, .. } = x {
2786                    grow_to(out, *var);
2787                    out[*var as usize] += 1;
2788                }
2789            });
2790        }
2791    });
2792}
2793
2794fn drop_defs(s: &mut Stmt, vars: &[bool]) {
2795    walk_mut_deep(s, &mut |st| {
2796        if let Stmt::Block(v) = st {
2797            v.retain(|x| match x {
2798                Stmt::ExprStmt(Expr::Assign { target, .. }) => match &**target {
2799                    Expr::Local { var, .. } => !vars.get(*var as usize).copied().unwrap_or(false),
2800                    _ => true,
2801                },
2802                Stmt::LocalDef { var, .. } => !vars.get(*var as usize).copied().unwrap_or(false),
2803                _ => true,
2804            });
2805        }
2806    });
2807}
2808
2809/// For each statement list: `v = IMPURE; NEXT(v)` folds NEXT's reference.
2810fn forward_adjacent_impure(s: &mut Stmt, values: &[Option<Expr>], impure: &[bool]) {
2811    if let Stmt::Block(v) = s {
2812        let mut i = 0;
2813        while i + 1 < v.len() {
2814            let def_var = match &v[i] {
2815                Stmt::ExprStmt(Expr::Assign { target, .. }) => match &**target {
2816                    Expr::Local { var, .. } => Some(*var),
2817                    _ => None,
2818                },
2819                Stmt::LocalDef { var, .. } => Some(*var),
2820                _ => None,
2821            };
2822            if let Some(var) = def_var {
2823                if impure.get(var as usize).copied().unwrap_or(false) {
2824                    // Does the NEXT statement reference var?
2825                    let mut reads_here = vec![0usize; values.len().max(1)];
2826                    count_reads(&v[i + 1], &mut reads_here);
2827                    if reads_here.get(var as usize).copied().unwrap_or(0) == 1 {
2828                        // The def statement's CURRENT value (earlier inlines
2829                        // in this walk may have rewritten it — the pre-built
2830                        // values map would be stale).
2831                        let val = match &v[i] {
2832                            Stmt::ExprStmt(Expr::Assign { value, .. }) => (**value).clone(),
2833                            Stmt::LocalDef { init: Some(e), .. } => (*e).clone(),
2834                            _ => {
2835                                i += 1;
2836                                continue;
2837                            }
2838                        };
2839                        rewrite_exprs(&mut v[i + 1], &mut |e| {
2840                            deep_rewrite_reads(e, &mut |x| {
2841                                if let Expr::Local { var: v2, .. } = x {
2842                                    if *v2 == var {
2843                                        *x = val.clone();
2844                                    }
2845                                }
2846                            });
2847                        });
2848                        v.remove(i);
2849                        // The PREVIOUS definition may now be adjacent to its
2850                        // (shifted) use — re-examine it.
2851                        i = i.saturating_sub(1);
2852                        continue;
2853                    }
2854                }
2855            }
2856            i += 1;
2857        }
2858    }
2859    walk_mut(s, &mut |x| forward_adjacent_impure(x, values, impure));
2860}
2861
2862// ---------------------------------------------------------------------------
2863// Type inference, booleanization, null comparisons, declarations
2864// ---------------------------------------------------------------------------
2865
2866/// Evidence-based type inference for synthetic (non-parameter) vars.
2867pub fn infer_types(vt: &mut VarTable, body: &mut Stmt, ret: &JavaType, env: &MethodEnv) {
2868    let n = vt.vars.len();
2869    // (type, strong) — DIRECTIONAL evidence. Strong facts (an assigned
2870    // value's type, the declared return/throw/monitor type) DEFINE the
2871    // variable; weak facts (call argument expectations, receiver
2872    // contexts, numeric literals) only constrain a use. jadx's bound
2873    // system carries the same direction; a flat pool let a weak
2874    // expectation (`v26 = p1` with boolean p1 beside `v26 = 0` sugar)
2875    // outvote the definition.
2876    let mut evidence: Vec<Vec<(JavaType, bool)>> = vec![Vec::new(); n];
2877    let ev = |evidence: &mut Vec<Vec<(JavaType, bool)>>, var: u32, t: JavaType, strong: bool| {
2878        if (var as usize) < evidence.len() {
2879            evidence[var as usize].push((t, strong));
2880        }
2881    };
2882
2883    walk_all(body, &mut |st| match st {
2884        Stmt::LocalDef { var, init, .. } => {
2885            if let Some(e) = init {
2886                expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
2887                // The initializer DEFINES the declared type: `long v = l(...)`
2888                // must not stay `int v` (silent 64-bit truncation in the
2889                // eyes of a reader; javac rejects it as lossy). Null
2890                // carries no type (it would DOWNGRADE String to Object).
2891                // Literals are weak (0/1 is boolean sugar half the time);
2892                // typed producers and parameter sources are strong;
2893                // local-to-local copies are weak (register reuse).
2894                let strong = match e {
2895                    Expr::Const(_) => false,
2896                    Expr::Local { var: src, .. } => {
2897                        let src_ty = vt.vars.get(*src as usize).map(|v| v.ty.erased());
2898                        let tgt_ty = vt.vars.get(*var as usize).map(|v| v.ty.erased());
2899                        match (src_ty, tgt_ty) {
2900                            (Some(s), Some(g)) => {
2901                                (s.is_numeric() && g.is_numeric())
2902                                    || (s.is_reference() && g.is_reference())
2903                            }
2904                            _ => false,
2905                        }
2906                    }
2907                    _ => true,
2908                };
2909                ev(&mut evidence, *var, e.type_ref().erased(), strong);
2910            }
2911        }
2912        Stmt::ExprStmt(e) => {
2913            // Assignment targets: a typed PRODUCER (method/new/field) or a
2914            // PARAMETER source is a STRONG definition (`v26 = p1` with
2915            // boolean p1). A local-to-local copy is weak — register reuse
2916            // and merge materialization emit exactly that shape with
2917            // mismatched types (`sb7 = compareTo10` in a Kotlin when).
2918            // Literals stay weak.
2919            if let Expr::Assign { target, value, .. } = e {
2920                // A field WRITE through a local (`v.f = x`, dex iput)
2921                // proves v is a reference of the field's declaring
2922                // class AT THAT POINT — an iput on a boolean register
2923                // cannot exist in valid dex. Strong: it must outrank a
2924                // strong boolean copy chain into the same reused slot
2925                // (weixin coroutine state machines).
2926                if let Expr::Field { owner: Some(o), cls, is_static: false, .. } = &**target {
2927                    if cls.is_empty() {
2928                        // array-length pseudo-field: no declaring class
2929                    } else if let Expr::Local { var, .. } = &**o {
2930                        ev(
2931                            &mut evidence,
2932                            *var,
2933                            JavaType::Object(cls.clone()),
2934                            true,
2935                        );
2936                    }
2937                }
2938                if let Expr::Local { var, .. } = &**target {
2939                    let strong = match &**value {
2940                        Expr::Const(_) => false,
2941                        Expr::Local { var: src, .. } => {
2942                            // A local copy is strong when the source and
2943                            // the target's register type are the same
2944                            // FAMILY (numeric↔numeric, reference↔
2945                            // reference) — a cross-family copy (int into
2946                            // a StringBuilder local) is merge-material
2947                            // residue and stays weak.
2948                            let src_ty = vt.vars.get(*src as usize).map(|v| v.ty.erased());
2949                            let tgt_ty = vt.vars.get(*var as usize).map(|v| v.ty.erased());
2950                            match (src_ty, tgt_ty) {
2951                                (Some(s), Some(g)) => {
2952                                    (s.is_numeric() && g.is_numeric())
2953                                        || (s.is_reference() && g.is_reference())
2954                                }
2955                                _ => false,
2956                            }
2957                        }
2958                        _ => true,
2959                    };
2960                    ev(&mut evidence, *var, value.type_ref().erased(), strong);
2961                }
2962            }
2963            expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
2964        }
2965        Stmt::Throw(e) => {
2966            expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
2967            if let Expr::Local { var, .. } = e {
2968                ev(
2969                    &mut evidence,
2970                    *var,
2971                    JavaType::Object("java/lang/Throwable".into()),
2972                    true,
2973                );
2974            }
2975        }
2976        Stmt::Return(Some(e)) => {
2977            expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
2978            if let Expr::Local { var, .. } = e {
2979                ev(&mut evidence, *var, ret.clone(), true);
2980            }
2981        }
2982        Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
2983            if let Expr::Local { var, .. } = e {
2984                ev(
2985                    &mut evidence,
2986                    *var,
2987                    JavaType::Object("java/lang/Object".into()),
2988                    true,
2989                );
2990            }
2991        }
2992        Stmt::ForEach {
2993            var,
2994            iterable,
2995            is_array,
2996            ..
2997        } => {
2998            if let Expr::Local { var: v0, .. } = iterable {
2999                if *is_array {
3000                    ev(&mut evidence, *v0, JavaType::Array(Box::new(JavaType::Int)), true);
3001                } else {
3002                    ev(
3003                        &mut evidence,
3004                        *v0,
3005                        JavaType::Object("java/lang/Object".into()),
3006                        true,
3007                    );
3008                }
3009            }
3010            let _ = var;
3011        }
3012        _ => {}
3013    });
3014    let _ = env;
3015
3016    // Return-position locals take the declared return type (also inside
3017    // nested returns — handled by the walk above).
3018
3019        // Vars dereferenced as field/method receivers or arrays: their
3020    // resolved type must be a dereferenceable class — a boxed Boolean
3021    // in the strong set (Kotlin Result registers hold `Boolean | impl`
3022    // across coroutine case arms) must lose to the real class even
3023    // when the Boolean evidence comes first (weixin ry0/h2's
3024    // `Boolean bool19` receiving `bool19.d = x`).
3025    let mut derefed: Vec<bool> = vec![false; vt.vars.len()];
3026    mark_derefs(body, &mut derefed);
3027    // Array-indexed vars: their resolved type must be an ARRAY when
3028    // the evidence has one (`num4[v68]` with Integer-typed num4 —
3029    // 需要数组但找到Integer family); member derefs keep the class
3030    // preference below.
3031    let mut arr_derefed: Vec<bool> = vec![false; vt.vars.len()];
3032    visit_all_exprs(body, &mut |x| {
3033        if let Expr::ArrayIndex { array, .. } = x {
3034            if let Expr::Local { var, .. } = &**array {
3035                if (*var as usize) < arr_derefed.len() {
3036                    arr_derefed[*var as usize] = true;
3037                }
3038            }
3039        }
3040        // `v != null` is STRONG reference evidence: the lifter only
3041        // renders the null form when the dex value view held a
3042        // reference register at that pc — an int-typed generation
3043        // compared against null is phi-confluence residue (yq0/g1's
3044        // `v95_g12 != null`, 546-line 二元运算符 family). Strong Object
3045        // makes the resolver retype and split_generations separate the
3046        // int writes into their own generation.
3047        if let Expr::Bin { op, l, r, .. } = x {
3048            if matches!(op, BinOp::Eq | BinOp::Ne)
3049                && (matches!(&**r, Expr::Const(ConstVal::Null))
3050                    || matches!(&**l, Expr::Const(ConstVal::Null)))
3051            {
3052                for side in [l, r] {
3053                    if let Expr::Local { var, .. } = &**side {
3054                        ev(
3055                            &mut evidence,
3056                            *var,
3057                            JavaType::Object("java/lang/Object".into()),
3058                            true,
3059                        );
3060                    }
3061                }
3062            }
3063        }
3064    });
3065    const FINAL_JDK: &[&str] = &[
3066        "java/lang/Boolean",
3067        "java/lang/String",
3068        "java/lang/Integer",
3069        "java/lang/Long",
3070        "java/lang/Short",
3071        "java/lang/Byte",
3072        "java/lang/Character",
3073        "java/lang/Float",
3074        "java/lang/Double",
3075    ];
3076    for (i, evs) in evidence.iter().enumerate() {
3077        let info = &vt.vars[i];
3078        if info.is_param {
3079            continue;
3080        }
3081        let strong: Vec<JavaType> = evs
3082            .iter()
3083            .filter(|(_, s)| *s)
3084            .map(|(t, _)| t.clone())
3085            .collect();
3086        let all: Vec<JavaType> = evs.iter().map(|(t, _)| t.clone()).collect();
3087        if info.ty.erased().is_reference() {
3088            // Materialization residue: no strong definition, and the weak
3089            // pool names ≥2 DIFFERENT classes (a Kotlin `when` lowered
3090            // every branch's value into one register slot) — neither
3091            // class can win, every assignment needs boxing headroom:
3092            // widen to Object (uses get receiver casts).
3093            if strong.is_empty() {
3094                let refs: std::collections::HashSet<&str> = all
3095                    .iter()
3096                    .filter_map(|t| match t {
3097                        JavaType::Object(n) if n.as_ref() != "java/lang/Object" => {
3098                            Some(n.as_ref())
3099                        }
3100                        _ => None,
3101                    })
3102                    .collect();
3103                let mixed = all.iter().any(|t| t.is_numeric());
3104                if refs.len() >= 2 || (mixed && !refs.is_empty()) || (mixed && all.iter().any(|t| matches!(t, JavaType::Array(_)))) {
3105                    vt.vars[i].ty =
3106                        TypeRef::J(JavaType::Object("java/lang/Object".into()));
3107                    continue;
3108                }
3109            }
3110                        // Array-dereferenced vars prefer array evidence (an
3111            // Integer producer cannot be indexed).
3112            if arr_derefed[i] {
3113                let arr = strong
3114                    .iter()
3115                    .chain(all.iter())
3116                    .find(|t| matches!(t, JavaType::Array(_)))
3117                    .cloned();
3118                if let Some(t) = arr {
3119                    vt.vars[i].ty = TypeRef::J(t);
3120                    continue;
3121                }
3122            }
3123            // Strong definitions outrank weak expectations. A
3124            // dereferenced var prefers a real class over final JDK
3125            // boxes (Boolean can never carry `.d = x`).
3126            let deref_pick = || {
3127                if !derefed[i] {
3128                    return None;
3129                }
3130                strong
3131                    .iter()
3132                    .find(|t| {
3133                        matches!(t, JavaType::Object(n)
3134                            if !n.is_empty()
3135                                && n.as_ref() != "java/lang/Object"
3136                                && !FINAL_JDK.contains(&n.as_ref()))
3137                    })
3138                    .cloned()
3139            };
3140            if let Some(t) = deref_pick()
3141                .or_else(|| pick_object(&strong))
3142                .or_else(|| pick_object(&all))
3143            {
3144                vt.vars[i].ty = TypeRef::J(t);
3145            }
3146        } else if let Some(t) = pick_object(&strong) {
3147            // A strong reference definition (typed producer or field
3148            // write) outranks a strong boolean copy: Boolean assigns
3149            // INTO an Object slot legally, never the reverse.
3150            vt.vars[i].ty = TypeRef::J(t);
3151        } else if strong.contains(&JavaType::Boolean) {
3152            // A boolean-typed SOURCE assigned into the variable is a
3153            // definition (`v26 = p1` with boolean p1); the 0/1 literals
3154            // that share the pool are boolean sugar as often as not.
3155            vt.vars[i].ty = TypeRef::J(JavaType::Boolean);
3156        } else if !all.is_empty() && all.iter().all(|t| t.is_numeric()) {
3157            if let Some(t) = pick_numeric(&all) {
3158                vt.vars[i].ty = TypeRef::J(t);
3159            }
3160        } else {
3161            // Mixed-family materialization on a numeric-declared slot
3162            // (int slot receiving StringBuilders — the register was the
3163            // `when` accumulator): Object, same as the reference side.
3164            let refs = all.iter().filter(|t| t.is_reference()).count();
3165            let nums = all.iter().filter(|t| t.is_numeric()).count();
3166            if refs > 0 && nums > 0 {
3167                vt.vars[i].ty =
3168                    TypeRef::J(JavaType::Object("java/lang/Object".into()));
3169            } else if let Some(t) = pick_object(&strong).or_else(|| pick_object(&all)) {
3170                vt.vars[i].ty = TypeRef::J(t);
3171            }
3172        }
3173    }
3174
3175    // Rewrite embedded Local types. Borrowed view + write-only-on-change:
3176    // the table clone was n_vars JavaType clones per method, and the
3177    // rewrite cloned a type into EVERY Local node — most already carry
3178    // the right one (JavaType::Object clones allocate).
3179    let types: Vec<&TypeRef> = vt.vars.iter().map(|v| &v.ty).collect();
3180    rewrite_exprs(body, &mut |e| {
3181        deep_rewrite(e, &mut |x| {
3182            if let Expr::Local { var, ty } = x {
3183                if let Some(want) = types.get(*var as usize) {
3184                    if ty != *want {
3185                        *ty = (*want).clone();
3186                    }
3187                }
3188            }
3189        });
3190    });
3191
3192    // Numeric literals take their variable's declared width (`double v = 0L`
3193    // prints as `0.0`; `int x = 5L` as `5`).
3194    coerce_num_consts(body, &types);
3195}
3196
3197fn coerce_num_consts(body: &mut Stmt, types: &[&TypeRef]) {
3198    walk_mut_deep(body, &mut |st| {
3199        let (var, val): (u32, &mut Expr) = match st {
3200            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => match &mut **target {
3201                Expr::Local { var, .. } => (*var, value),
3202                _ => return,
3203            },
3204            Stmt::LocalDef {
3205                var, init: Some(e), ..
3206            } => (*var, e),
3207            _ => return,
3208        };
3209        let want = match types.get(var as usize) {
3210            Some(TypeRef::J(t)) => t.clone(),
3211            _ => return,
3212        };
3213        let rewritten = match &*val {
3214            Expr::Const(ConstVal::Long(l)) => match &want {
3215                JavaType::Double => Some(Expr::Const(ConstVal::Double(*l as f64))),
3216                JavaType::Float => Some(Expr::Const(ConstVal::Float(*l as f32))),
3217                _ => None,
3218            },
3219            Expr::Const(ConstVal::Int(i)) => match &want {
3220                JavaType::Double => Some(Expr::Const(ConstVal::Double(*i as f64))),
3221                JavaType::Float => Some(Expr::Const(ConstVal::Float(*i as f32))),
3222                _ => None,
3223            },
3224            _ => None,
3225        };
3226        if let Some(e) = rewritten {
3227            *val = e;
3228        }
3229    });
3230}
3231
3232fn pick_object(evs: &[JavaType]) -> Option<JavaType> {
3233    evs.iter()
3234        .find(|t| {
3235            t.is_reference()
3236                && !matches!(t, JavaType::Object(n) if n.is_empty() || n.as_ref() == "java/lang/Object")
3237        })
3238        .cloned()
3239}
3240
3241fn pick_numeric(evs: &[JavaType]) -> Option<JavaType> {
3242    if evs.is_empty() {
3243        return None;
3244    }
3245    let mut t = evs[0].clone();
3246    for x in &evs[1..] {
3247        t = join_numeric(&t, x);
3248    }
3249    Some(t)
3250}
3251
3252fn join_numeric(a: &JavaType, b: &JavaType) -> JavaType {
3253    if a == b {
3254        return a.clone();
3255    }
3256    match (a, b) {
3257        (JavaType::Double, _) | (_, JavaType::Double) => JavaType::Double,
3258        (JavaType::Float, _) | (_, JavaType::Float) => JavaType::Float,
3259        (JavaType::Long, _) | (_, JavaType::Long) => JavaType::Long,
3260        (JavaType::Int, x) if x.is_integral() => JavaType::Int,
3261        (x, JavaType::Int) if x.is_integral() => JavaType::Int,
3262        (x, y) if x.is_integral() && y.is_integral() => JavaType::Int,
3263        _ => JavaType::Int,
3264    }
3265}
3266
3267/// Gather per-var type evidence from one expression.
3268fn expr_evidence<F: FnMut(u32, JavaType)>(e: &Expr, f: &mut F) {
3269    visit_exprs(e, &mut |x| match x {
3270        Expr::Method {
3271            owner,
3272            args,
3273            desc,
3274            is_static,
3275            cls,
3276            ..
3277        } => {
3278            if !*is_static {
3279                if let Some(o) = owner {
3280                    if let Expr::Local { var, .. } = &**o {
3281                        f(*var, JavaType::Object(cls.clone()));
3282                    }
3283                }
3284            }
3285            for (i, a) in args.iter().enumerate() {
3286                if let Expr::Local { var, .. } = a {
3287                    if let Some(t) = desc.args.get(i) {
3288                        f(*var, t.clone());
3289                    }
3290                }
3291            }
3292        }
3293        Expr::Field {
3294            owner,
3295            cls,
3296            ty,
3297            is_static,
3298            ..
3299        } => {
3300            if !*is_static && !cls.is_empty() {
3301                if let Some(o) = owner {
3302                    if let Expr::Local { var, .. } = &**o {
3303                        // The field ref's declaring class is dex-exact
3304                        // evidence — the old hardcoded Object let a
3305                        // boolean copy chain outvote it (weixin ry0/h2's
3306                        // `Boolean bool19` receiving `bool19.d = x` —
3307                        // the 339-line 找不到符号 变量 d family).
3308                        // EMPTY cls = the array-length pseudo-field
3309                        // (lift models `a.length` as Field{cls:""}) —
3310                        // Object("") evidence poisoned pick_object into
3311                        // empty-typed declarations and `() x` casts.
3312                        f(*var, JavaType::Object(cls.clone()));
3313                    }
3314                }
3315            }
3316            let _ = ty;
3317        }
3318        // `v != null` / `v == null`: the register holds a reference at
3319        // that point — weak Object evidence keeps int-typed generations
3320        // from winning the pool (yq0/g1's `v95_g12 != null` with int
3321        // v95_g12 — 二元运算符 '!=' 546-line family).
3322        Expr::Bin { op, l, r, .. }
3323            if matches!(op, BinOp::Eq | BinOp::Ne)
3324                && (matches!(&**r, Expr::Const(ConstVal::Null))
3325                    || matches!(&**l, Expr::Const(ConstVal::Null))) =>
3326        {
3327            for side in [l, r] {
3328                if let Expr::Local { var, .. } = &**side {
3329                    f(*var, JavaType::Object("java/lang/Object".into()));
3330                }
3331            }
3332        }
3333        Expr::ArrayIndex { array, .. } => {
3334            if let Expr::Local { var, ty, .. } = &**array {
3335                // Reinforce the local's KNOWN array type: the hardcoded
3336                // int[] fallback contradicted a typed array local (a
3337                // register reused for byte[], int[] then byte[] across
3338                // one clinit labeled the byte[] defs `int[]`).
3339                let t = ty.erased();
3340                if matches!(t, JavaType::Array(_)) {
3341                    f(*var, t);
3342                } else {
3343                    f(*var, JavaType::Array(Box::new(JavaType::Int)));
3344                }
3345            }
3346        }
3347        Expr::Assign { .. } => {
3348            // Covered by the typed strong-evidence walk in infer_types;
3349            // keeping a weak duplicate here let expectations outvote it.
3350        }
3351        Expr::Cast { e: inner, .. } => {
3352            // A cast is an EXPLICIT narrowing at the use site — it must
3353            // NOT feed the cast target as evidence for the variable
3354            // (`Object get2 = list.get(2); j((String) get2);` had get2
3355            // retyped to String, making the assignment incompatible).
3356            // jadx's bounds carry direction: a USE bound narrows nothing.
3357            let _ = inner;
3358        }
3359        Expr::InstanceOf { e: inner, .. } => {
3360            let _ = inner;
3361        }
3362        _ => {}
3363    });
3364}
3365
3366/// Mark locals that OWN a non-static field access and whose inferred type
3367/// is a concrete `$<digits>` class (a d8/R8-desugared lambda or a Kotlin
3368/// suspend-lambda). Emit demotes such a type to its SAM interface in
3369/// DECLARED positions (`Function2 v = new Outer$..$1(..)`) for phi-friendly
3370/// readability — but interfaces carry no instance fields, so the Kotlin
3371/// coroutine `create()` capture writes (`v.L$0 = obj`, also I$/J$) render
3372/// "找不到符号 变量 L$0" (weibo MutableScatterMap$..$iterator$1, ~1k).
3373/// Forcing the concrete declared type keeps the field access legal; the phi
3374/// case is excluded because a disagreed merge stays Object (wide_stack_vars),
3375/// never a specific concrete class. Gate mirrors emit's demotion exactly
3376/// (last `$` segment all digits) so nothing else changes.
3377pub fn mark_field_owner_concrete(vt: &mut VarTable, body: &mut Stmt) {
3378    let mut owners: HashSet<u32> = HashSet::default();
3379    rewrite_exprs(body, &mut |e| {
3380        visit_exprs(e, &mut |x| {
3381            if let Expr::Field {
3382                owner: Some(o),
3383                is_static: false,
3384                ..
3385            } = x
3386            {
3387                if let Expr::Local { var, .. } = &**o {
3388                    owners.insert(*var);
3389                }
3390            }
3391        });
3392    });
3393    for var in owners {
3394        let i = var as usize;
3395        if i >= vt.vars.len() {
3396            continue;
3397        }
3398        if let JavaType::Object(cls) = vt.vars[i].ty.erased() {
3399            if let Some(last) = cls.rsplit('$').next() {
3400                if !last.is_empty() && last.chars().all(|c| c.is_ascii_digit()) {
3401                    vt.force_concrete_vars.insert(var);
3402                }
3403            }
3404        }
3405    }
3406    // EVERY local whose resolved type is a concrete `$<digits>` class
3407    // keeps its binary name, not just field owners: when one side of an
3408    // assignment falls back to the SAM/super (`ContinuationImpl v7`) and
3409    // its sibling stays concrete (`$reportWhenComplete$1 v9`), the pair
3410    // is an uncastable "ContinuationImpl无法转换为…$1" (weibo coroutine
3411    // prologues ×282). The anon fallback still serves INTERFACE-typed
3412    // phi slots (branches minting different lambdas) — their vt type is
3413    // the join, never the $N class.
3414    let concrete: Vec<u32> = vt
3415        .vars
3416        .iter()
3417        .filter(|v| {
3418            if let JavaType::Object(cls) = v.ty.erased() {
3419                cls.rsplit('$')
3420                    .next()
3421                    .is_some_and(|l| !l.is_empty() && l.chars().all(|c| c.is_ascii_digit()))
3422            } else {
3423                false
3424            }
3425        })
3426        .map(|v| v.id)
3427        .collect();
3428    for var in concrete {
3429        vt.force_concrete_vars.insert(var);
3430    }
3431}
3432
3433/// Type inference can leave a specific-reference-typed target assigned
3434/// from a `java/lang/Object`-typed value: a phi that merged String and
3435/// Object then settled on String (`String str4; ... str4 = obj;` where
3436/// `obj` is an Object field — y5/n.java), or an Object local flowing to a
3437/// typed field. Java requires a narrowing cast there; insert `(T) value`
3438/// at the USE site (round-40 philosophy: narrow at use, never retype the
3439/// declaration). Guarded tightly: only when the value's resolved static
3440/// type is EXACTLY `java/lang/Object` (the untyped top — a known subtype
3441/// is never re-cast), the value is not a null const (assignable to any
3442/// ref) nor already a cast, and the target is a specific reference type
3443/// (Object→int/boolean is unboxing, a different problem left alone).
3444pub fn insert_object_narrowing_casts(
3445    vt: &VarTable,
3446    body: &mut Stmt,
3447    pool: &DexPool,
3448    ret: &JavaType,
3449) {
3450    // Resolve a value's static type through the VarTable for locals (the
3451    // embedded Local ty can lag infer_types), else the expr's own type.
3452    let value_ty = |e: &Expr| -> JavaType {
3453        match e {
3454            Expr::Local { var, .. } => vt.var(*var).ty.erased(),
3455            other => other.type_ref().erased(),
3456        }
3457    };
3458    let is_top_object = |e: &Expr| -> bool {
3459        value_ty(e) == JavaType::Object("java/lang/Object".into())
3460    };
3461    // A specific reference target type (a class other than
3462    // java/lang/Object, or an array — `String[] v = obj` needs the
3463    // cast exactly like `String v = obj`).
3464    let specific_ref = |ty: &TypeRef| -> Option<TypeRef> {
3465        match ty.erased() {
3466            JavaType::Object(c) if c.as_ref() != "java/lang/Object" => Some(ty.clone()),
3467            JavaType::Array(_) => Some(ty.clone()),
3468            _ => None,
3469        }
3470    };
3471    let castable = |e: &Expr| -> bool {
3472        is_top_object(e)
3473            && !matches!(e, Expr::Const(_) | Expr::Cast { .. } | Expr::InstanceOf { .. })
3474    };
3475    // Downcast witness: a SPECIFIC-ref value whose static type does not
3476    // fit the specific-ref target (`v9($1) = v7` with v7:
3477    // ContinuationImpl — weibo coroutine prologue ×282). The dex move
3478    // was verifier-proven against the target type, so the cast states
3479    // the bytecode truth; an actually-unconvertible static pair (both
3480    // classes, unrelated — the value view conflated) rides the
3481    // emitter's (Object) relay and still compiles. Framework types the
3482    // pool cannot order take the cast too: a downcast renders directly,
3483    // an unrelated pair relays.
3484    let needs_downcast = |e: &Expr, tt: &JavaType| -> bool {
3485        if matches!(e, Expr::Const(_) | Expr::Cast { .. } | Expr::InstanceOf { .. }) {
3486            return false;
3487        }
3488        let v = value_ty(e);
3489        let (JavaType::Object(vn), JavaType::Object(tn)) = (&v, tt) else {
3490            return false;
3491        };
3492        if vn.as_ref() == "java/lang/Object" || tn.as_ref() == "java/lang/Object" {
3493            return false;
3494        }
3495        vn != tn && !pool.is_subtype(vn, tn)
3496    };
3497    walk_mut_deep(body, &mut |st| match st {
3498        Stmt::ExprStmt(Expr::Assign {
3499            target,
3500            value,
3501            op: AssignOp::Plain,
3502            ..
3503        }) => {
3504            let tgt = match &**target {
3505                Expr::Local { var, .. } => vt.var(*var).ty.clone(),
3506                Expr::Field { ty, .. } => ty.clone(),
3507                // Array-element store `arr[i] = value`: the target type is
3508                // the ELEMENT type. Without this arm an Object→element
3509                // assignment (`String[] a; a[0] = pair.first` — Pair.first
3510                // erases to Object) skipped the cast and failed "Object
3511                // 无法转换为String" (weixin ×~150). Resolve the array's type
3512                // through the vt (the embedded Local ty can lag infer_types).
3513                Expr::ArrayIndex { array, .. } => {
3514                    let aty = match &**array {
3515                        Expr::Local { var, .. } => vt.var(*var).ty.erased(),
3516                        other => other.type_ref().erased(),
3517                    };
3518                    match aty {
3519                        JavaType::Array(inner) => TypeRef::J(*inner),
3520                        _ => return,
3521                    }
3522                }
3523                _ => return,
3524            };
3525            if let Some(t) = specific_ref(&tgt) {
3526                if castable(value) || needs_downcast(value, &tgt.erased()) {
3527                    let v = std::mem::replace(value, Box::new(Expr::This));
3528                    **value = Expr::Cast { ty: t, e: v };
3529                }
3530            }
3531        }
3532        Stmt::LocalDef { var, init: Some(value), .. } => {
3533            if let Some(t) = specific_ref(&vt.var(*var).ty) {
3534                let tt = vt.var(*var).ty.erased();
3535                if castable(value) || needs_downcast(value, &tt) {
3536                    let v = std::mem::replace(value, Expr::This);
3537                    *value = Expr::Cast {
3538                        ty: t,
3539                        e: Box::new(v),
3540                    };
3541                }
3542            }
3543        }
3544        Stmt::Return(Some(value)) => {
3545            if let Some(t) = specific_ref(&TypeRef::J(ret.clone())) {
3546                if castable(value) || needs_downcast(value, ret) {
3547                    let v = std::mem::replace(value, Expr::This);
3548                    *value = Expr::Cast { ty: t, e: Box::new(v) };
3549                }
3550            }
3551        }
3552        _ => {}
3553    });
3554    // Descriptor-exact formal casts at CALL args: the dex already
3555    // resolved the target method, so casting a top-Object actual to
3556    // its declared formal cannot shift overload resolution (the r57
3557    // args lesson applies to GUESSED casts, not descriptor-exact
3558    // ones). `"source_type:".concat(obj2)`, `new my0.j(obj2, ..)` —
3559    // Object无法转换为String family.
3560    walk_stmt_exprs(body, &mut |e| {
3561        deep_rewrite(e, &mut |x| {
3562            let (args, arg_tys): (&mut Vec<Expr>, &[JavaType]) = match x {
3563                Expr::Method { desc, args, is_dynamic: false, .. } => {
3564                    (args, desc.args.as_slice())
3565                }
3566                // Ctor calls are their own variant — the dex resolved the
3567                // exact <init>, so formal casts are just as safe here
3568                // (`new r(obj)` — Object无法转换为r family).
3569                Expr::New { arg_tys, args, .. } => (args, arg_tys.as_slice()),
3570                _ => return,
3571            };
3572            for (i, a) in args.iter_mut().enumerate() {
3573                let Some(formal) = arg_tys.get(i) else {
3574                    continue;
3575                };
3576                let Some(t) = specific_ref(&TypeRef::J(formal.clone())) else {
3577                    continue;
3578                };
3579                if !(castable(a) || needs_downcast(a, formal)) {
3580                    continue;
3581                }
3582                let v = std::mem::replace(a, Expr::Const(ConstVal::Null));
3583                *a = Expr::Cast { ty: t, e: Box::new(v) };
3584            }
3585        });
3586    });
3587}
3588
3589/// Numeric narrowing/widening mismatches at Assign / LocalDef / Return
3590/// positions get an explicit cast: dex int-to-byte / long-to-int flows
3591/// are implicit in the register machine, Java rejects them ("从long
3592/// 转换到int可能会有损失" — okio Buffer ×321 weibo lines). Casts at
3593/// these three positions are descriptor-faithful and cannot shift
3594/// overload resolution (the r57 args-position lesson). Boolean/numeric
3595/// mixes are NOT castable in Java and stay with the generation-split
3596/// machinery.
3597pub fn fix_primitive_assign_casts(vt: &VarTable, body: &mut Stmt, ret_ty: &JavaType) {
3598    fn numlike(t: &JavaType) -> bool {
3599        matches!(
3600            t,
3601            JavaType::Byte
3602                | JavaType::Short
3603                | JavaType::Int
3604                | JavaType::Char
3605                | JavaType::Long
3606                | JavaType::Float
3607                | JavaType::Double
3608        )
3609    }
3610    fn val_ty(e: &Expr, vt: &VarTable) -> JavaType {
3611        match e {
3612            Expr::Local { var, .. } if (*var as usize) < vt.vars.len() => {
3613                vt.var(*var).ty.erased()
3614            }
3615            other => other.type_ref().erased(),
3616        }
3617    }
3618    fn wrap(ty: &JavaType, value: &mut Expr) {
3619        if matches!(value, Expr::Cast { .. }) {
3620            return;
3621        }
3622        if val_ty_static(value) == *ty {
3623            return;
3624        }
3625        let taken = std::mem::replace(value, Expr::Const(ConstVal::Null));
3626        *value = Expr::Cast {
3627            ty: TypeRef::J(ty.clone()),
3628            e: Box::new(taken),
3629        };
3630    }
3631    fn val_ty_static(e: &Expr) -> JavaType {
3632        e.type_ref().erased()
3633    }
3634    /// Target-typed coercion for one assignment value. Numeric mixes
3635    /// cast; boolean mixes CONVERT (Java has no cast between boolean
3636    /// and any numeric): `d = b` → `(double) (b ? 1 : 0)`, `b = i` →
3637    /// `i != 0` — mirrors fix_primitive_arg_bridges' formal-side arms.
3638    /// The generation split cannot own these: at an if-join the
3639    /// one-sided split is undone (rename_gen_back) and the incompatible
3640    /// assign re-exposes ("boolean无法转换为double", weixin s9/t01-f).
3641    fn coerce(tt: &JavaType, value: &mut Expr, vt: &VarTable) {
3642        // Raw const bits against a floating target are the FLOAT value,
3643        // not an int to widen (`float f = <0x41000000 bits>` is 8.0f;
3644        // the (float) cast path would silently store 1.09e9f). Mirror of
3645        // fix_primitive_arg_bridges' descriptor-formal reinterpretation.
3646        match (&*value, tt) {
3647            (Expr::Const(ConstVal::Int(b)), JavaType::Float) => {
3648                *value = Expr::Const(ConstVal::Float(f32::from_bits(*b as u32)));
3649                return;
3650            }
3651            (Expr::Const(ConstVal::Long(b)), JavaType::Double) => {
3652                *value = Expr::Const(ConstVal::Double(f64::from_bits(*b as u64)));
3653                return;
3654            }
3655            (Expr::Const(ConstVal::Float(f)), JavaType::Int) => {
3656                *value = Expr::Const(ConstVal::Int(f.to_bits() as i32));
3657                return;
3658            }
3659            (Expr::Const(ConstVal::Double(d)), JavaType::Long) => {
3660                *value = Expr::Const(ConstVal::Long(d.to_bits() as i64));
3661                return;
3662            }
3663            _ => {}
3664        }
3665        let vt_val = val_ty(value, vt);
3666        let tt_bool = matches!(tt, JavaType::Boolean);
3667        let val_bool = matches!(vt_val, JavaType::Boolean);
3668        if tt_bool && !val_bool && numlike(&vt_val) {
3669            if matches!(
3670                value,
3671                Expr::Const(_)
3672                    | Expr::Bin { op: BinOp::Eq | BinOp::Ne, .. }
3673                    | Expr::Un { op: UnOp::Not, .. }
3674            ) {
3675                // Bare 0/1 constants render as false/true through the
3676                // emitter's bool coercion — wrapping them in `!= 0`
3677                // produced `0 != 0` noise the value-diamond fold then
3678                // had to re-interpret.
3679                return;
3680            }
3681            let taken = std::mem::replace(value, Expr::Const(ConstVal::Null));
3682            *value = Expr::Bin {
3683                op: BinOp::Ne,
3684                l: Box::new(taken),
3685                r: Box::new(Expr::Const(ConstVal::Int(0))),
3686                ty: Some(TypeRef::J(JavaType::Boolean)),
3687            };
3688        } else if !tt_bool && val_bool && numlike(tt) {
3689            if matches!(value, Expr::Cast { .. }) {
3690                return;
3691            }
3692            let taken = std::mem::replace(value, Expr::Const(ConstVal::Null));
3693            *value = Expr::Cast {
3694                ty: TypeRef::J(tt.clone()),
3695                e: Box::new(Expr::Cond {
3696                    c: Box::new(taken),
3697                    t: Box::new(Expr::Const(ConstVal::Int(1))),
3698                    f: Box::new(Expr::Const(ConstVal::Int(0))),
3699                }),
3700            };
3701        } else if numlike(tt) && numlike(&vt_val) {
3702            wrap(tt, value);
3703        }
3704    }
3705    walk_mut_deep(body, &mut |st| match st {
3706        Stmt::ExprStmt(Expr::Assign {
3707            target,
3708            op: AssignOp::Plain,
3709            value,
3710        }) => {
3711            let tt = match &**target {
3712                Expr::Local { var, .. } if (*var as usize) < vt.vars.len() => {
3713                    vt.var(*var).ty.erased()
3714                }
3715                Expr::Field { ty, .. } => ty.erased(),
3716                // Array store: the element type is the target type
3717                // (`sFormatStr[i] = v7` into char[] — "从int转换到char
3718                // 可能会有损失", weibo TimeUtils).
3719                Expr::ArrayIndex { array, .. } => match array.type_ref().erased() {
3720                    JavaType::Array(inner) => *inner,
3721                    _ => return,
3722                },
3723                _ => return,
3724            };
3725            coerce(&tt, value, vt);
3726        }
3727        Stmt::LocalDef {
3728            var,
3729            init: Some(value),
3730            ..
3731        } => {
3732            if (*var as usize) >= vt.vars.len() {
3733                return;
3734            }
3735            let tt = vt.var(*var).ty.erased();
3736            coerce(&tt, value, vt);
3737        }
3738        Stmt::Return(Some(e)) => {
3739            if !numlike(ret_ty) || !numlike(&val_ty(e, vt)) {
3740                return;
3741            }
3742            wrap(ret_ty, e);
3743        }
3744        _ => {}
3745    });
3746}
3747
3748/// Shadowed-ancestor field witness (the upcast sibling of
3749/// fix_field_owner_downcasts).
3750///
3751/// Java resolves a field access on the RECEIVER'S STATIC TYPE downward:
3752/// when the dex writes an ancestor's field (`iput … Fragment.A02:I`)
3753/// but a class between the receiver's static type and the declaring
3754/// ancestor declares a field with the SAME rendered name (`ImageView
3755/// A02`), the plain `this.A02` silently binds to the subclass field —
3756/// `int无法转换为ImageView` when the types differ, and a wrong-field
3757/// read/write when they don't (WhatsApp ActivitySheetFragment writes
3758/// the layout resid into the ancestor int field; its own A02 is an
3759/// ImageView — 1,216 assignment-shape sites in the WA tree). jadx
3760/// renders `super.A02`; the IR has no super-field form, so witness
3761/// with the always-legal upcast `((Fragment) this).A02` — field lookup
3762/// on a cast expression is static, landing on the ancestor at any
3763/// chain depth. The receiver may still be the typed this-Local at this
3764/// pipeline point (the This rewrite comes later), so the compile-time
3765/// type comes from the owner expr / var table, mirroring
3766/// fix_field_owner_downcasts' owner_internal. Fires ONLY on a real
3767/// same-display shadow; unshadowed ancestor refs inherit through
3768/// `this.X` and must not churn. Same-raw-name lookups only: a shadow
3769/// whose raw name differs but display collides is beyond the pool
3770/// lookup keyed by name.
3771pub fn fix_shadowed_super_fields(
3772    body: &mut Stmt,
3773    vt: &VarTable,
3774    pool: &DexPool,
3775    class_name: &str,
3776) {
3777    /// Rendered name of `(cls, raw field name)`: the registry display
3778    /// when the class declares the field, else the raw name. None when
3779    /// the class is not pool-materialized or has no such field.
3780    fn display(pool: &DexPool, cls: &str, name: &str) -> Option<String> {
3781        let pc = pool.get(cls)?;
3782        let f = pc
3783            .static_fields
3784            .iter()
3785            .chain(pc.instance_fields.iter())
3786            .find(|f| f.name.as_str() == name)?;
3787        Some(
3788            jdc_core::rename::field_display(cls, name, &f.desc)
3789                .map(|d| d.to_string())
3790                .unwrap_or_else(|| name.to_string()),
3791        )
3792    }
3793    fn owner_ct(
3794        o: &Option<Box<Expr>>,
3795        vt: &VarTable,
3796        class_name: &str,
3797    ) -> Option<std::sync::Arc<str>> {
3798        match o.as_deref() {
3799            None | Some(Expr::This) => Some(std::sync::Arc::from(class_name)),
3800            Some(Expr::Local { var, .. }) => match vt.var(*var).ty.erased() {
3801                JavaType::Object(n) => Some(n),
3802                _ => None,
3803            },
3804            Some(other) => match other.type_ref().erased() {
3805                JavaType::Object(n) => Some(n),
3806                _ => None,
3807            },
3808        }
3809    }
3810    walk_stmt_exprs(body, &mut |e| {
3811        deep_rewrite(e, &mut |x| {
3812            let Expr::Field {
3813                owner,
3814                cls,
3815                name,
3816                is_static: false,
3817                ..
3818            } = x
3819            else {
3820                return;
3821            };
3822            let Some(ot) = owner_ct(owner, vt, class_name) else {
3823                return;
3824            };
3825            if cls.as_ref() == ot.as_ref() {
3826                return;
3827            }
3828            let Some(ref_display) = display(pool, cls, name) else {
3829                return;
3830            };
3831            // Walk from the receiver's compile-time type up to (not
3832            // including) the declaring ancestor: any same-display
3833            // declaration on the way shadows the ref.
3834            let mut cur = ot.to_string();
3835            let mut shadowed = false;
3836            let mut found = false;
3837            for _ in 0..64 {
3838                if display(pool, &cur, name).as_deref() == Some(ref_display.as_str()) {
3839                    shadowed = true;
3840                }
3841                let Some(pc) = pool.get(&cur) else {
3842                    return;
3843                };
3844                let Some(sup) = pc.super_name.clone() else {
3845                    return;
3846                };
3847                if sup.as_str() == cls.as_ref() {
3848                    found = true;
3849                    break;
3850                }
3851                cur = sup;
3852            }
3853            if !found || !shadowed {
3854                return;
3855            }
3856            let ty = TypeRef::J(JavaType::Object(cls.clone()));
3857            match owner {
3858                Some(o) => {
3859                    if let Expr::Cast { ty: ct, .. } = &mut **o {
3860                        *ct = ty;
3861                    } else {
3862                        let taken = std::mem::replace(&mut **o, Expr::This);
3863                        **o = Expr::Cast {
3864                            ty,
3865                            e: Box::new(taken),
3866                        };
3867                    }
3868                }
3869                None => {
3870                    *owner = Some(Box::new(Expr::Cast {
3871                        ty,
3872                        e: Box::new(Expr::This),
3873                    }))
3874                }
3875            }
3876        });
3877    });
3878}
3879
3880/// Subclass-field downcast witness + assign-cast narrowing.
3881///
3882/// R8 pushes fields DOWN into leaf classes (weibo: coroutine `label`
3883/// lives on each `Foo$bar$1` continuation, NOT on the rendered
3884/// BaseContinuationImpl/ContinuationImpl supers) while a check-cast in
3885/// the same flow types the receiver as the SUPERCLASS. The dex field
3886/// ref names the leaf (`$1->label` — verifier-legal through the
3887/// instance-of branch type), but Java resolves `((ContinuationImpl) c)
3888/// .label` against ContinuationImpl — "找不到符号 变量 label" (weibo
3889/// ×420). When the field's dex owner is a strict subtype of the
3890/// receiver's static type and the receiver's hierarchy does NOT
3891/// declare the name, re-target the receiver cast to the owner (the
3892/// runtime object IS the owner type — the verifier proved it).
3893///
3894/// Second rule: a `(T) e` value assigned into an S-typed target where
3895/// S <: T strictly ("ContinuationImpl无法转换为$reportWhenComplete$1")
3896/// narrows the cast to S — same verifier guarantee, and an assignment
3897/// position has no overload resolution to perturb.
3898pub fn fix_field_owner_downcasts(body: &mut Stmt, vt: &VarTable, pool: &DexPool) {
3899    fn owner_internal(o: &Expr, vt: &VarTable) -> Option<std::sync::Arc<str>> {
3900        let t = match o {
3901            Expr::Local { var, .. } => vt.var(*var).ty.erased(),
3902            other => other.type_ref().erased(),
3903        };
3904        match t {
3905            JavaType::Object(n) => Some(n),
3906            _ => None,
3907        }
3908    }
3909    fn declares_up(pool: &DexPool, class: &str, name: &str) -> bool {
3910        let mut cur = Some(class.to_string());
3911        let mut hops = 0;
3912        while let Some(c) = cur {
3913            hops += 1;
3914            if hops > 64 {
3915                return false;
3916            }
3917            let Some(pc) = pool.get(&c) else { return false };
3918            if pc
3919                .instance_fields
3920                .iter()
3921                .chain(pc.static_fields.iter())
3922                .any(|f| f.name.as_str() == name)
3923            {
3924                return true;
3925            }
3926            cur = pc.super_name.clone();
3927        }
3928        false
3929    }
3930    walk_stmt_exprs(body, &mut |e| {
3931        deep_rewrite(e, &mut |x| {
3932            if let Expr::Field {
3933                owner: Some(o),
3934                cls,
3935                name,
3936                is_static: false,
3937                ..
3938            } = x
3939            {
3940                let Some(ot) = owner_internal(o, vt) else {
3941                    return;
3942                };
3943                if ot.as_ref() == cls.as_ref() || !pool.is_subtype(cls, &ot) {
3944                    return;
3945                }
3946                if declares_up(pool, &ot, name) || !declares_up(pool, cls, name) {
3947                    return;
3948                }
3949                let ty = TypeRef::J(JavaType::Object(cls.clone()));
3950                if let Expr::Cast { ty: ct, .. } = &mut **o {
3951                    *ct = ty;
3952                } else {
3953                    let taken = std::mem::replace(&mut **o, Expr::This);
3954                    **o = Expr::Cast { ty, e: Box::new(taken) };
3955                }
3956            }
3957            // Rule 2: `(T) e` into a strict-subtype target narrows to S.
3958            let (tgt_ty, value) = match x {
3959                Expr::Assign { target, op: AssignOp::Plain, value } => {
3960                    let t = match &**target {
3961                        Expr::Local { var, .. } => vt.var(*var).ty.erased(),
3962                        Expr::Field { ty, .. } => ty.erased(),
3963                        _ => return,
3964                    };
3965                    (t, value)
3966                }
3967                _ => return,
3968            };
3969            let JavaType::Object(sn) = tgt_ty else { return };
3970            if let Expr::Cast { ty, .. } = &mut **value {
3971                if let TypeRef::J(JavaType::Object(tn)) = ty {
3972                    if tn.as_ref() != sn.as_ref() && pool.is_subtype(&sn, tn) {
3973                        *ty = TypeRef::J(JavaType::Object(sn.clone()));
3974                    }
3975                }
3976            }
3977        });
3978    });
3979    // LocalDef inits: same narrowing against the declared var type.
3980    walk_mut_deep(body, &mut |st| {
3981        if let Stmt::LocalDef { var, init: Some(value), .. } = st {
3982            let tt = if (*var as usize) < vt.vars.len() {
3983                vt.vars[*var as usize].ty.erased()
3984            } else {
3985                return;
3986            };
3987            let JavaType::Object(sn) = tt else { return };
3988            if let Expr::Cast { ty, .. } = value {
3989                if let TypeRef::J(JavaType::Object(tn)) = ty {
3990                    if tn.as_ref() != sn.as_ref() && pool.is_subtype(&sn, tn) {
3991                        *ty = TypeRef::J(JavaType::Object(sn.clone()));
3992                    }
3993                }
3994            }
3995        }
3996    });
3997}
3998
3999/// `==`/`!=` between UNRELATED class types is a compile error in Java
4000/// ("不可比较的类型: f0和a") while dex if-eqObj happily compares any two
4001/// references (weixin fn1/w1 ×298 files). One side takes an `(Object)`
4002/// witness cast — legal against anything. Interfaces and subtype-related
4003/// pairs are already comparable and stay untouched; java/lang/Object and
4004/// arrays-of it likewise.
4005pub fn fix_incomparable_equality(body: &mut Stmt, vt: &VarTable, pool: &crate::DexPool) {
4006    fn side_ty(e: &Expr, vt: &VarTable) -> JavaType {
4007        match e {
4008            Expr::Local { var, .. } if (*var as usize) < vt.vars.len() => {
4009                vt.var(*var).ty.erased()
4010            }
4011            other => other.type_ref().erased(),
4012        }
4013    }
4014    let incomparable = |a: &JavaType, b: &JavaType| -> bool {
4015        use JavaType::{Array, Object};
4016        match (a, b) {
4017            (Object(x), Object(y)) => {
4018                if x == y || x.as_ref() == "java/lang/Object" || y.as_ref() == "java/lang/Object"
4019                {
4020                    return false;
4021                }
4022                let xi = pool.get(x).map(|c| c.is_interface()).unwrap_or(true);
4023                let yi = pool.get(y).map(|c| c.is_interface()).unwrap_or(true);
4024                // Unknown classes (framework, not in pool): a cast is
4025                // harmless but usually unnecessary — treat as comparable
4026                // to stay conservative.
4027                if xi || yi {
4028                    return false;
4029                }
4030                !pool.is_subtype(x, y) && !pool.is_subtype(y, x)
4031            }
4032            (Array(_), Object(y)) | (Object(y), Array(_)) => {
4033                y.as_ref() != "java/lang/Object"
4034                    && y.as_ref() != "java/lang/Cloneable"
4035                    && y.as_ref() != "java/io/Serializable"
4036                    && !pool.get(y).map(|c| c.is_interface()).unwrap_or(false)
4037            }
4038            _ => false,
4039        }
4040    };
4041    let is_ref = |t: &JavaType| matches!(t, JavaType::Object(_) | JavaType::Array(_));
4042    walk_stmt_exprs(body, &mut |e| {
4043        deep_rewrite(e, &mut |x| {
4044            if let Expr::Bin {
4045                op: BinOp::Eq | BinOp::Ne,
4046                l,
4047                r,
4048                ..
4049            } = x
4050            {
4051                let lt = side_ty(l, vt);
4052                let rt = side_ty(r, vt);
4053                // `obj != 0` → `obj != null` with FINAL types: the
4054                // lift-time null_side_rewrite ran before infer_types
4055                // (its obj table saw the pre-inference int), leaving
4056                // `sQLiteClosable != 0` — "二元运算符 '!=' 的操作数
4057                // 类型错误" ×700 weixin.
4058                if is_ref(&lt) && matches!(&**r, Expr::Const(ConstVal::Int(0))) {
4059                    **r = Expr::Const(ConstVal::Null);
4060                    return;
4061                }
4062                if is_ref(&rt) && matches!(&**l, Expr::Const(ConstVal::Int(0))) {
4063                    **l = Expr::Const(ConstVal::Null);
4064                    return;
4065                }
4066                if incomparable(&lt, &rt) {
4067                    let taken = std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
4068                    **l = Expr::Cast {
4069                        ty: TypeRef::J(JavaType::Object("java/lang/Object".into())),
4070                        e: taken,
4071                    };
4072                }
4073            }
4074        });
4075    });
4076}
4077
4078/// Primitive bridges at CALL/CTOR argument positions. Dex slots are
4079/// untyped category-1 integers: the verifier happily passes a Z-slot to
4080/// a `(B)` formal (weibo's Meituan-Robust hotpatch boilerplate boxes
4081/// boolean params via `new Byte(zreg)` — 4.2k+ `对于Byte(boolean),
4082/// 找不到合适的构造器`), and obfuscated/Kotlin code passes 0/1 slots to
4083/// `(Z)` formals (`int无法转换为boolean` at call sites). Java source
4084/// cannot express either — bridge at the IR level:
4085///   Boolean actual → numeric formal:  `(byte) (b ? 1 : 0)`
4086///   numeric actual → Boolean formal:  `x != 0`
4087/// The target method is DESCRIPTOR-bound (bytecode method_ref), so a
4088/// cast/compare to the exact formal type cannot flip overload
4089/// resolution the way r57's inferred object casts did — the rendered
4090/// call selects the same signature the dex named.
4091pub fn fix_primitive_arg_bridges(body: &mut Stmt, vt: &VarTable, pool: &crate::DexPool) {
4092    fn val_ty(e: &Expr, vt: &VarTable) -> JavaType {
4093        match e {
4094            Expr::Local { var, .. } => vt.var(*var).ty.erased(),
4095            other => other.type_ref().erased(),
4096        }
4097    }
4098    #[allow(clippy::type_complexity)]
4099    let is_bool = |t: &JavaType| matches!(t, JavaType::Boolean);
4100    let is_num = |t: &JavaType| {
4101        matches!(
4102            t,
4103            JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
4104                | JavaType::Long | JavaType::Float | JavaType::Double
4105        )
4106    };
4107    // BORROWED-arg form: returns the bridged replacement without
4108    // consuming the original (a `mem::replace` + `Option` dance dropped
4109    // the argument on the None path — every Boolean→Boolean arg became
4110    // a literal `null`: 61k 引用不明确 in one battery run).
4111    fn bridge(actual: &JavaType, formal: &JavaType, arg: &Expr) -> Option<Expr> {
4112        // A dex const carries the CALLEE's BITS: `const v, #0x41000000`
4113        // feeding a float formal IS 8.0f. Rendering the raw int literal
4114        // (a) mis-selects overloads — a competing `a(Context,int):void`
4115        // beats the int→float-widened `a(Context,float):int` in javac's
4116        // phase order ("不兼容的类型: void无法转换为int", weibo ju.a
4117        // ×1,259) — and (b) corrupts the VALUE (widening turns the bits
4118        // into 1.09e9f). The descriptor formal is dex ground truth:
4119        // reinterpret. Symmetric for the float-const-into-int-formal
4120        // direction (caller's bits, callee's reading).
4121        match (arg, formal) {
4122            (Expr::Const(ConstVal::Int(b)), JavaType::Float) => {
4123                return Some(Expr::Const(ConstVal::Float(f32::from_bits(*b as u32))));
4124            }
4125            (Expr::Const(ConstVal::Long(b)), JavaType::Double) => {
4126                return Some(Expr::Const(ConstVal::Double(f64::from_bits(*b as u64))));
4127            }
4128            (Expr::Const(ConstVal::Float(f)), JavaType::Int) => {
4129                return Some(Expr::Const(ConstVal::Int(f.to_bits() as i32)));
4130            }
4131            (Expr::Const(ConstVal::Double(d)), JavaType::Long) => {
4132                return Some(Expr::Const(ConstVal::Long(d.to_bits() as i64)));
4133            }
4134            _ => {}
4135        }
4136        if matches!(actual, JavaType::Boolean)
4137            && matches!(
4138                formal,
4139                JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
4140                    | JavaType::Long | JavaType::Float | JavaType::Double
4141            )
4142        {
4143            Some(Expr::Cast {
4144                ty: TypeRef::J(formal.clone()),
4145                e: Box::new(Expr::Cond {
4146                    c: Box::new(arg.clone()),
4147                    t: Box::new(Expr::Const(ConstVal::Int(1))),
4148                    f: Box::new(Expr::Const(ConstVal::Int(0))),
4149                }),
4150            })
4151        } else if matches!(formal, JavaType::Boolean)
4152            && matches!(
4153                actual,
4154                JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
4155                    | JavaType::Long | JavaType::Float | JavaType::Double
4156            )
4157        {
4158            Some(Expr::Bin {
4159                op: BinOp::Ne,
4160                l: Box::new(arg.clone()),
4161                r: Box::new(Expr::Const(ConstVal::Int(0))),
4162                ty: Some(TypeRef::J(JavaType::Boolean)),
4163            })
4164        } else if matches!(
4165            actual,
4166            JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
4167                | JavaType::Long | JavaType::Float | JavaType::Double
4168        ) && matches!(
4169            formal,
4170            JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
4171                | JavaType::Long | JavaType::Float | JavaType::Double
4172        ) && !implicit_widening(actual, formal)
4173        {
4174            // Numeric NARROWING into a formal (int arg → char formal):
4175            // javac rejects the lossy conversion, or — worse when the
4176            // class has no wider overload — silently resolves a
4177            // DIFFERENT overload than the dex descriptor names
4178            // (SpannableStringBuilder.append(int) doesn't exist;
4179            // `append(char)` is "从int转换到char可能会有损失", weibo ×76).
4180            // The descriptor formal is the dex ground truth; casting to
4181            // it pins the overload the bytecode actually invoked.
4182            Some(Expr::Cast {
4183                ty: TypeRef::J(formal.clone()),
4184                e: Box::new(arg.clone()),
4185            })
4186        } else {
4187            None
4188        }
4189    }
4190    /// A bare `null` argument against a SPECIFIC reference formal takes
4191    /// the descriptor cast: `this(context, null)` matching both
4192    /// Builder(Context,Notification) and Builder(Context,String) is
4193    /// "对Builder的引用不明确" (weibo NotificationCompat; protobuf
4194    /// `new FieldSet.Builder(null)` ×54). The descriptor names the
4195    /// exact invoked ctor, so the cast is dex ground truth — the
4196    /// descriptor-exact doctrine, not a guessed overload shift. Bare
4197    /// null stays for Object formals (casting those would add noise
4198    /// without resolving anything javac could not already pick).
4199    /// Framework classes javac resolves through android.jar even when
4200    /// the dex pool lacks them — the embedded API database answers
4201    /// precisely (the hand-written prefix list it replaced missed
4202    /// everything outside the guessed roots and over-trusted within
4203    /// them; removed-API families like org.apache.http are in the DB
4204    /// too, matching what a legacy-jar classpath resolves).
4205    fn fw_resolvable(n: &str) -> bool {
4206        crate::fwdb::exists(n)
4207    }
4208    fn null_arg_cast(a: &mut Expr, f: &JavaType, pool: &DexPool) -> bool {
4209        if !matches!(&*a, Expr::Const(ConstVal::Null)) {
4210            return false;
4211        }
4212        if let JavaType::Object(n) = f {
4213            // Cast only to RESOLVABLE types: a phantom formal (R8 kept
4214            // the ref, dropped the class — androidx.window.sidecar)
4215            // would turn a compiling bare `null` into a fresh
4216            // cannot-find. Pool classes render into the output;
4217            // java/javax/android/dalvik resolve through android.jar.
4218            let resolvable = pool.get(n).is_some() || fw_resolvable(n);
4219            if n.as_ref() != "java/lang/Object" && resolvable {
4220                *a = Expr::Cast {
4221                    ty: TypeRef::J(f.clone()),
4222                    e: Box::new(Expr::Const(ConstVal::Null)),
4223                };
4224                return true;
4225            }
4226        } else if let JavaType::Array(_) = f {
4227            // Descriptor-exact ARRAY cast: the dex named this exact
4228            // overload; a bare null beside sibling overloads is javac-
4229            // ambiguous (`a(String,b2)` + `a(String,String[])` both match
4230            // — weibo org/a/c/b, 对a的引用不明确 family). Same phantom
4231            // gate on the element class: casting to a phantom-element
4232            // array would trade the ambiguity for a cannot-find.
4233            let mut e: &JavaType = f;
4234            while let JavaType::Array(inner) = e {
4235                e = &**inner;
4236            }
4237            let resolvable = match e {
4238                JavaType::Object(n) => pool.get(n).is_some() || fw_resolvable(n),
4239                _ => true, // primitive-element array
4240            };
4241            if resolvable {
4242                *a = Expr::Cast {
4243                    ty: TypeRef::J(f.clone()),
4244                    e: Box::new(Expr::Const(ConstVal::Null)),
4245                };
4246                return true;
4247            }
4248        }
4249        // Bare null deliberately stays (Object formal / phantom type):
4250        // the caller's ambiguity pin may still act on it.
4251        false
4252    }
4253    /// JLS 5.1.2 widening primitive conversion (char joins the chain
4254    /// only at int; byte/short do NOT widen to char).
4255    fn implicit_widening(a: &JavaType, f: &JavaType) -> bool {
4256        use JavaType::*;
4257        match (a, f) {
4258            (Byte, Short) | (Byte, Int) | (Byte, Long) | (Byte, Float) | (Byte, Double) => true,
4259            (Short, Int) | (Short, Long) | (Short, Float) | (Short, Double) => true,
4260            (Char, Int) | (Char, Long) | (Char, Float) | (Char, Double) => true,
4261            (Int, Long) | (Int, Float) | (Int, Double) => true,
4262            (Long, Float) | (Long, Double) => true,
4263            (Float, Double) => true,
4264            (x, y) => x == y,
4265        }
4266    }
4267    // Does the owner class declare ANOTHER method with the same name
4268    // and arity but a different signature? Then javac's most-specific
4269    // overload resolution can steal the call from the dex descriptor's
4270    // target (weibo 对a的引用不明确 ×391: `c.a(null)` against
4271    // a(ExecuteResult)/a(Exception)/a(Object) — the descriptor names
4272    // a(Object), but bare null leaves the two specific overloads
4273    // ambiguously most-specific). Framework owners are not in the
4274    // pool — conservatively report no competition.
4275    let overload_competes = |cls: &str, name: &str, args: &[JavaType]| -> bool {
4276        if let Some(c) = pool.get(cls) {
4277            return c.all_methods().any(|m| {
4278                &*m.name == name
4279                    // Bridge/synthetic siblings are compiler artifacts
4280                    // resolving to the SAME target — counting them as
4281                    // competitors cast enum compareTo args to the erased
4282                    // formal the rendered specialization no longer
4283                    // accepts (Enum无法转换为LogLevel ×2,079 reqable).
4284                    && m.access & (crate::access::ACC_BRIDGE | crate::access::ACC_SYNTHETIC) == 0
4285                    && m.parsed_desc()
4286                        .is_some_and(|d| d.args.len() == args.len() && d.args != args)
4287            });
4288        }
4289        // Framework owner: NOT enumerable for pin purposes — the
4290        // full-DB enumeration experiment (step-5) measured +532 battery
4291        // with ZERO ambiguous gain: a subtype arg against a concrete
4292        // erased formal fires the cast even when the call was never
4293        // ambiguous, and bounded-generic formals (List<T> -> List,
4294        // Property<T,Float> erasures) make the cast break inference or
4295        // miss the rendered specialization. Framework subtype pins stay
4296        // on the measured allowlist below.
4297        let _ = args;
4298        false
4299    };
4300    // Ctors: formal types via the pool (unanimous across same-arity
4301    // overloads, else skip — ambiguity must not guess).
4302    let ctor_formals = |cls: &str, n: usize| -> Option<Vec<JavaType>> {
4303        let c = pool.get(cls)?;
4304        let mut out: Option<Vec<JavaType>> = None;
4305        for m in c.all_methods() {
4306            if &*m.name != "<init>" {
4307                continue;
4308            }
4309            let Some(d) = m.parsed_desc() else { continue };
4310            if d.args.len() != n {
4311                continue;
4312            }
4313            match &out {
4314                None => out = Some(d.args.clone()),
4315                Some(prev) if *prev != d.args => return None,
4316                Some(_) => {}
4317            }
4318        }
4319        out
4320    };
4321    walk_stmt_exprs(body, &mut |e| {
4322        deep_rewrite(e, &mut |x| match x {
4323            Expr::Method {
4324                cls,
4325                name,
4326                desc,
4327                args,
4328                is_static,
4329                ..
4330            } => {
4331                if args.len() != desc.args.len() {
4332                    return;
4333                }
4334                let _ = is_static;
4335                let mut competes: Option<bool> = None;
4336                for (a, f) in args.iter_mut().zip(desc.args.iter()) {
4337                    if null_arg_cast(a, f, pool) {
4338                        continue;
4339                    }
4340                    if matches!(a, Expr::Cast { .. }) {
4341                        continue;
4342                    }
4343                    // Descriptor-exact ambiguity pin: a bare null
4344                    // against an Object formal (null_arg_cast
4345                    // deliberately leaves those bare — safe only
4346                    // WITHOUT competing specific overloads) or a
4347                    // strict-subtype arg can match several declared
4348                    // overloads; casting to the descriptor formal
4349                    // leaves exactly the dex-invoked method
4350                    // applicable (JLS 15.12.2 — the descriptor is
4351                    // ground truth, so the cast pins resolution
4352                    // instead of shifting it).
4353                    let subtype_arg = match (&val_ty(a, vt), f) {
4354                        (JavaType::Object(an), JavaType::Object(fn_)) => {
4355                            // java/lang/Object is the root: ANY reference
4356                            // arg is a strict subtype of it — pool.is_subtype
4357                            // cannot see framework chains (String -> Object),
4358                            // so the Object formal gets the direct rule.
4359                            (an != fn_ && pool.is_subtype(an, fn_))
4360                                || (fn_.as_ref() == "java/lang/Object"
4361                                    && an.as_ref() != "java/lang/Object")
4362                        }
4363                        (JavaType::Array(_), JavaType::Object(fn_))
4364                            if fn_.as_ref() == "java/lang/Object" =>
4365                        {
4366                            true
4367                        }
4368                        _ => false,
4369                    };
4370                    // Bare null against java/lang/Object itself: the
4371                    // descriptor method exists (dex resolved it), so
4372                    // `(Object) null` is always legal and excludes every
4373                    // more-specific sibling overload from applicability.
4374                    // Pool owners take the competes gate (emitted
4375                    // declarations are raw, so the cast cannot disturb
4376                    // inference). FRAMEWORK owners only via allowlist of
4377                    // the classic non-generic null-ambiguous JDK methods
4378                    // (StringBuilder.append ×66 lark): a blanket cast
4379                    // breaks GENERIC framework methods whose descriptor
4380                    // formal is an ERASED type variable —
4381                    // `ofFloat((Object) null, View.ALPHA, v)` killed the
4382                    // T=View inference bare null provided (weibo +4).
4383                    let null_obj =
4384                        matches!(a, Expr::Const(ConstVal::Null)) && matches!(f, JavaType::Object(_));
4385                    let framework_owner = pool.get(cls.as_ref()).is_none();
4386                    let fw_null_ambiguous = framework_owner
4387                        && matches!(
4388                            (cls.as_ref(), name.as_ref()),
4389                            ("java/lang/StringBuilder", "append")
4390                                | ("java/lang/StringBuffer", "append")
4391                                | ("java/io/PrintStream", "println")
4392                                | ("java/io/PrintStream", "print")
4393                                | ("java/io/PrintWriter", "println")
4394                                | ("java/io/PrintWriter", "print")
4395                                | ("java/lang/String", "valueOf")
4396                        );
4397                    // Framework owners with famously overload-heavy
4398                    // methods take the subtype pin too: the arg's
4399                    // rendered type implements SEVERAL formal types
4400                    // (weibo putExtra ×41: floatMsgData is both
4401                    // Parcelable and Serializable; schedule/submit:
4402                    // Runnable-and-Callable workers) and the competes
4403                    // gate cannot enumerate framework overloads.
4404                    // Formal == java/lang/Object stays excluded — that
4405                    // is an erased type variable and the cast would
4406                    // kill generic inference (ofFloat lesson).
4407                    let fw_subtype_pinnable = framework_owner
4408                        && subtype_arg
4409                        && !matches!(f, JavaType::Object(fn_) if fn_.as_ref() == "java/lang/Object")
4410                        && matches!(
4411                            (cls.as_ref(), name.as_ref()),
4412                            ("android/content/Intent", "putExtra")
4413                                | ("java/util/concurrent/ScheduledExecutorService", "schedule")
4414                                | ("java/util/concurrent/ExecutorService", "submit")
4415                                | ("java/util/concurrent/ExecutorService", "invokeAll")
4416                        );
4417                    if fw_subtype_pinnable
4418                        || (subtype_arg
4419                            && *competes.get_or_insert_with(|| {
4420                                overload_competes(cls, name, &desc.args)
4421                            }))
4422                        || (null_obj
4423                            && (fw_null_ambiguous
4424                                || *competes.get_or_insert_with(|| {
4425                                    overload_competes(cls, name, &desc.args)
4426                                })))
4427                    {
4428                        let old =
4429                            std::mem::replace(a, Expr::Const(ConstVal::Null));
4430                        *a = Expr::Cast {
4431                            ty: TypeRef::J(f.clone()),
4432                            e: Box::new(old),
4433                        };
4434                        continue;
4435                    }
4436                    if !is_bool(&val_ty(a, vt)) && !is_num(&val_ty(a, vt)) {
4437                        continue;
4438                    }
4439                    if !is_bool(f) && !is_num(f) {
4440                        continue;
4441                    }
4442                    if let Some(b) = bridge(&val_ty(a, vt), f, a) {
4443                        *a = b;
4444                    }
4445                }
4446            }
4447            Expr::New { cls, args, arg_tys, .. } => {
4448                let formals_owned = if arg_tys.len() == args.len() && !arg_tys.is_empty() {
4449                    Some(arg_tys.clone())
4450                } else {
4451                    ctor_formals(cls, args.len())
4452                };
4453                if let Some(formals) = formals_owned {
4454                    for (a, f) in args.iter_mut().zip(formals.iter()) {
4455                        if null_arg_cast(a, f, pool) {
4456                            continue;
4457                        }
4458                        if matches!(a, Expr::Cast { .. }) {
4459                            continue;
4460                        }
4461                        if !is_bool(f) && !is_num(f) {
4462                            continue;
4463                        }
4464                        if let Some(b) = bridge(&val_ty(a, vt), f, a) {
4465                            *a = b;
4466                        }
4467                    }
4468                }
4469            }
4470            _ => {}
4471        });
4472    });
4473}
4474
4475/// `b ^ 1` / `1 ^ b` with a BOOLEAN operand is the bytecode's boolean
4476/// negation (`!b`) — weibo kotlin-stdlib `return v2 ^ 1;` ("二元运算符
4477/// '^' 的操作数类型错误"), and `x ^ true` shapes. Java only types `^`
4478/// for same-category operands.
4479/// Rename locals whose name equals a ROOT PACKAGE first segment: a
4480/// package-qualified render (`v2.n.a` — the FQN fallback a twin-shadowed
4481/// same-package ref must take) binds its first identifier lexically, and
4482/// a local `v2` in scope captures it (weixin v2/i: 找不到符号 变量 n on a
4483/// legit `v2.n.a(this, 0)`). Method-scope renames carry no registry
4484/// surface — every use renders through this same vt — unlike class/field
4485/// renames, whose reference coverage blew up five times before.
4486pub fn deshadow_locals(vt: &mut VarTable, pool: &crate::DexPool, class_name: &str, body: &Stmt) {
4487    let segs = pool.root_pkg_segs();
4488    // Import-layer simples: the file renders `import p.h;` + bare `h.e`
4489    // for obscured refs — a local `h` captures that simple name (locals
4490    // beat imports in expression position; 无法取消引用int family).
4491    let obscured = crate::classdec::obscured_simples_snapshot();
4492    // Same-package static-ref simples: a static member ref renders its
4493    // class qualifier as the SIMPLE name (same package — no import/FQN),
4494    // and a local with that name captures it (`a2.changeQuickRedirect`
4495    // bound to a String local `a2` — "找不到符号 变量 changeQuickRedirect
4496    // 位置: 类型为String的变量 a2", weibo robust-field refs ×317). Only
4497    // STATIC refs qualify (an instance ref's qualifier IS the local).
4498    let self_pkg = class_name.rfind('/').map(|i| &class_name[..i]).unwrap_or("");
4499    let mut same_pkg_simples: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
4500    visit_all_exprs(body, &mut |x| {
4501        let cls = match x {
4502            Expr::Field { cls, is_static: true, .. } => Some(cls.as_ref()),
4503            Expr::Method { cls, is_static: true, .. } => Some(cls.as_ref()),
4504            _ => None,
4505        };
4506        if let Some(c) = cls {
4507            let pkg = c.rfind('/').map(|i| &c[..i]).unwrap_or("");
4508            if pkg == self_pkg {
4509                let simple = &c[c.rfind('/').map(|i| i + 1).unwrap_or(0)..];
4510                let head = simple.split('$').next().unwrap_or(simple);
4511                if !head.is_empty() {
4512                    same_pkg_simples.insert(head.to_string());
4513                }
4514            }
4515        }
4516    });
4517    let hit = |name: &str| {
4518        segs.contains(name) || obscured.contains(name) || same_pkg_simples.contains(name)
4519    };
4520    if (segs.is_empty() && obscured.is_empty() && same_pkg_simples.is_empty())
4521        || !vt.vars.iter().any(|v| hit(&v.name))
4522    {
4523        return;
4524    }
4525    let mut taken: jdc_core::FxHashSet<String> =
4526        vt.vars.iter().map(|v| v.name.clone()).collect();
4527    for v in vt.vars.iter_mut() {
4528        if !hit(&v.name) {
4529            continue;
4530        }
4531        let mut k = 0u32;
4532        let cand = loop {
4533            k += 1;
4534            let c = if k == 1 {
4535                format!("{}x", v.name)
4536            } else {
4537                format!("{}x{}", v.name, k)
4538            };
4539            if !taken.contains(&c) {
4540                break c;
4541            }
4542        };
4543        taken.insert(cand.clone());
4544        v.name = cand;
4545    }
4546}
4547
4548/// Receiver rescue: a Field/Method receiver whose final vt type is
4549/// PRIMITIVE can never hold the member (structurer tail-copy regions
4550/// can map the receiver register onto a boolean/int generation —
4551/// wcdb HandleOperation's `v10_g2.finalizeStatement()` where the
4552/// statement var is right there; weixin deref-int/bool families,
4553/// ~2.5k lines). The member descriptor's owner class is authoritative:
4554/// when EXACTLY ONE var in the method has that type (or a subtype),
4555/// swap the receiver to it. The pre-rescue output is a guaranteed
4556/// compile error AND runtime NPE, so the unambiguous swap cannot make
4557/// it worse; ambiguous sites stay untouched.
4558/// Anonymous/inner-class outer reads: the dex reads `this$0` off the
4559/// instance register, which copy materialization can route through a
4560/// local (`v41 = this; ... v41.this$0` — weibo HorseRaceDetector$1,
4561/// 2,326-line 找不到符号 变量 this$0 family). When the field's
4562/// declaring class IS the class being decompiled, the only instance in
4563/// scope is `this` — strip the owner so it renders the bare declared
4564/// field.
4565/// Fold single-assigned `this`-copy locals in ctors back to `this`.
4566/// R8 materializes the receiver with `move-object/from16 v1, p0` and
4567/// runs every iput through the copy; the copy renders as a local and a
4568/// final-field store through it is "无法为 final 变量 g 分配值" (Java
4569/// allows blank-final writes only via `this`/simple name — hf/n2 ×29,
4570/// weixin final-var residual). Transitively: `b = a; c = b` chains
4571/// collapse. Single-assignment gate: a var ever written from another
4572/// value is not an alias.
4573pub fn fix_ctor_this_aliases(body: &mut Stmt, vt: &VarTable) {
4574    let Some(this_var) = vt
4575        .vars
4576        .iter()
4577        .find(|v| v.is_param && v.name == "this")
4578        .map(|v| v.id)
4579    else {
4580        return;
4581    };
4582    let mut analysis = VarAnalysis {
4583        assigns: Vec::new(),
4584        reads: Vec::new(),
4585        values: Vec::new(),
4586    };
4587    analyze_vars(body, &mut analysis);
4588    // All-sources rule: a local is a this-alias only when EVERY value
4589    // assignment traces to `this_var` through pure local copies. The
4590    // single-assign constraint this replaces missed R8's multi-branch
4591    // `this` spills (uuyc androidx.camera x_2: `v160 = v150` on two
4592    // paths, both this, chain this→v131→v140→v169→v174→v183→v150→v160),
4593    // so `v160.d = executor` never became `this.d = executor` and javac
4594    // rejected the final-field init (无法为 final 变量 d/i/j 分配值 ×103).
4595    // A single non-this source (field read, `new`, param, a literal)
4596    // disqualifies the local, so a marked alias is provably always this
4597    // and rewriting every read of it to `this` is sound.
4598    enum Src {
4599        This,
4600        Loc(u32),
4601        Other,
4602    }
4603    let mut sources: jdc_core::FxHashMap<u32, Vec<Src>> = jdc_core::FxHashMap::default();
4604    walk_all(body, &mut |st| {
4605        let (tgt, val): (u32, &Expr) = match st {
4606            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => match &**target {
4607                Expr::Local { var, .. } => (*var, &**value),
4608                _ => return,
4609            },
4610            Stmt::LocalDef { var, init: Some(value), .. } => (*var, value),
4611            _ => return,
4612        };
4613        let s = match val {
4614            Expr::This => Src::This,
4615            Expr::Local { var: src, .. } if *src == this_var => Src::This,
4616            Expr::Local { var: src, .. } => Src::Loc(*src),
4617            _ => Src::Other,
4618        };
4619        sources.entry(tgt).or_default().push(s);
4620    });
4621    let n = vt
4622        .vars
4623        .len()
4624        .max(analysis.assigns.len())
4625        .max(sources.keys().map(|k| *k as usize + 1).max().unwrap_or(0));
4626    let mut alias = vec![false; n];
4627    loop {
4628        let mut changed = false;
4629        for (v, srcs) in &sources {
4630            let vi = *v as usize;
4631            if vi >= n || alias[vi] || srcs.is_empty() {
4632                continue;
4633            }
4634            let ok = srcs.iter().all(|s| match s {
4635                Src::This => true,
4636                Src::Loc(w) => (*w as usize) < n && alias[*w as usize],
4637                Src::Other => false,
4638            });
4639            if ok {
4640                alias[vi] = true;
4641                changed = true;
4642            }
4643        }
4644        if !changed {
4645            break;
4646        }
4647    }
4648    if !alias.iter().any(|&b| b) {
4649        return;
4650    }
4651    walk_stmt_exprs(body, &mut |e| {
4652        deep_rewrite_reads(e, &mut |x| {
4653            if let Expr::Local { var, .. } = x {
4654                if (*var as usize) < n && alias[*var as usize] {
4655                    *x = Expr::This;
4656                }
4657            }
4658        });
4659    });
4660}
4661
4662pub fn fix_this0_owners(body: &mut Stmt, class_name: &str) {
4663    walk_stmt_exprs(body, &mut |e| {
4664        deep_rewrite(e, &mut |x| {
4665            if let Expr::Field { owner: Some(_), cls, name, .. } = x {
4666                if name.as_ref() == "this$0" && cls.as_ref() == class_name {
4667                    if let Expr::Field { owner, .. } = x {
4668                        *owner = None;
4669                    }
4670                }
4671            }
4672        });
4673    });
4674}
4675
4676pub fn rescue_primitive_receivers(body: &mut Stmt, vt: &VarTable, pool: &crate::DexPool) {
4677    fn prim(t: &JavaType) -> bool {
4678        match t {
4679            JavaType::Int
4680            | JavaType::Long
4681            | JavaType::Short
4682            | JavaType::Byte
4683            | JavaType::Char
4684            | JavaType::Float
4685            | JavaType::Double
4686            | JavaType::Boolean => true,
4687            // Boxed JDK finals carry no app members either (`bool19.d`
4688            // where bool19 came out java/lang/Boolean — the coroutine
4689            // Result-register conflation).
4690            JavaType::Object(n) => matches!(
4691                n.as_ref(),
4692                "java/lang/Boolean"
4693                    | "java/lang/String"
4694                    | "java/lang/Integer"
4695                    | "java/lang/Long"
4696                    | "java/lang/Short"
4697                    | "java/lang/Byte"
4698                    | "java/lang/Character"
4699                    | "java/lang/Float"
4700                    | "java/lang/Double"
4701            ),
4702            _ => false,
4703        }
4704    }
4705    walk_stmt_exprs(body, &mut |e| {
4706        deep_rewrite(e, &mut |x| {
4707            // Phase 1 (read-only): primitive-typed Local receiver?
4708            let (recv_var, recv_ty) = {
4709                let o: &Expr = match &*x {
4710                    Expr::Method { owner: Some(o), .. } => o,
4711                    Expr::Field { owner: Some(o), is_static: false, .. } => o,
4712                    _ => return,
4713                };
4714                let Expr::Local { var, ty } = o else {
4715                    return;
4716                };
4717                let vt_ty = if (*var as usize) < vt.vars.len() {
4718                    vt.var(*var).ty.erased()
4719                } else {
4720                    ty.erased()
4721                };
4722                if !prim(&vt_ty) {
4723                    return;
4724                }
4725                (*var, vt_ty)
4726            };
4727            let cls: std::sync::Arc<str> = match &*x {
4728                Expr::Method { cls, .. } | Expr::Field { cls, .. } => cls.clone(),
4729                _ => return,
4730            };
4731            // Type-consistent call: the receiver's own type IS the
4732            // member owner (or a subtype) — `str.hashCode()` on a
4733            // String param needs no rescue. Without this the boxed-JDK
4734            // prim() arm fired on every String member call and the
4735            // max-id fallback below hijacked the reads to the highest
4736            // String-typed var in the method — often a case-local
4737            // (lark LynxUIBaseInput str11: 37 reads rewritten to a
4738            // switch-case-scoped def, 找不到符号 ×37; x0/c$a18 str4).
4739            if let JavaType::Object(n) = &recv_ty {
4740                if n.as_ref() == cls.as_ref() || pool.is_subtype(n.as_ref(), cls.as_ref()) {
4741                    return;
4742                }
4743            }
4744            // Phase 2: vars of the owner type (exact or subtype).
4745            let mut cands: Vec<u32> = Vec::new();
4746            for v in &vt.vars {
4747                if let JavaType::Object(n) = v.ty.erased() {
4748                    if n.as_ref() == cls.as_ref()
4749                        || pool.is_subtype(n.as_ref(), cls.as_ref())
4750                    {
4751                        cands.push(v.id);
4752                    }
4753                }
4754            }
4755            let id = if cands.len() == 1 {
4756                cands[0]
4757            } else if cands.len() > 1 {
4758                // Multiple owner-typed vars: prefer the same-SLOT one
4759                // (register lineage), latest generation wins — the
4760                // receiver position is scope-insensitive (any method-
4761                // scope var renders, the decl hoist covers it).
4762                let slot = if (recv_var as usize) < vt.vars.len() {
4763                    Some(vt.var(recv_var).slot)
4764                } else {
4765                    None
4766                };
4767                let mut hits: Vec<u32> = match slot {
4768                    Some(sl) => cands
4769                        .iter()
4770                        .copied()
4771                        .filter(|&c| {
4772                            c != recv_var
4773                                && (c as usize) < vt.vars.len()
4774                                && vt.var(c).slot == sl
4775                        })
4776                        .collect(),
4777                    None => Vec::new(),
4778                };
4779                if hits.is_empty() {
4780                    // No same-slot twin: fall back to the latest
4781                    // owner-typed generation (max id).
4782                    hits = cands
4783                        .iter()
4784                        .copied()
4785                        .filter(|&c| c != recv_var)
4786                        .collect();
4787                }
4788                hits.sort_unstable();
4789                hits.dedup();
4790                if hits.is_empty() {
4791                    return;
4792                }
4793                *hits.last().unwrap()
4794            } else {
4795                // Lineage disambiguation by SLOT: register reuse makes
4796                // the Boolean/primitive twin and the object version of
4797                // one dex register share VarInfo.slot (ry0/h2's
4798                // `bool19.d` — the v1-typed twin rides the same
4799                // register). Multiple slot matches stay untouched
4800                // (deterministic conservatism).
4801                if cands.is_empty() {
4802                    return;
4803                }
4804                let slot = if (recv_var as usize) < vt.vars.len() {
4805                    vt.var(recv_var).slot
4806                } else {
4807                    return;
4808                };
4809                let mut hits: Vec<u32> = cands
4810                    .into_iter()
4811                    .filter(|&c| {
4812                        c != recv_var
4813                            && (c as usize) < vt.vars.len()
4814                            && vt.var(c).slot == slot
4815                    })
4816                    .collect();
4817                hits.sort_unstable();
4818                hits.dedup();
4819                if hits.len() != 1 {
4820                    return;
4821                }
4822                hits[0]
4823            };
4824            // Phase 3: swap.
4825            let o: &mut Box<Expr> = match x {
4826                Expr::Method { owner: Some(o), .. } => o,
4827                Expr::Field { owner: Some(o), is_static: false, .. } => o,
4828                _ => return,
4829            };
4830            if let Expr::Local { var, ty } = &mut **o {
4831                if *var == recv_var {
4832                    *var = id;
4833                    *ty = vt.vars[id as usize].ty.clone();
4834                }
4835            }
4836        });
4837    });
4838
4839}
4840
4841/// Scope-safe arg/return swaps for mistyped generations — runs AFTER
4842/// ensure_declared hoists every declaration to the method top, so the
4843/// top_ids visibility precondition holds by construction (the r134
4844/// falsification: swapping in a block-scoped var broke lexically).
4845/// Same-slot + type-match + uniqueness gates as the receiver rescue.
4846pub fn rescue_arg_return_swaps(
4847    body: &mut Stmt,
4848    vt: &VarTable,
4849    pool: &crate::DexPool,
4850    ret_ty: &JavaType,
4851) {
4852    // SCOPE-SAFE arg/return swap: a primitive-typed actual against a
4853    // specific-reference formal (or a reference-returning `return`) is
4854    // a mistyped generation — the r134 falsification showed the swap
4855    // target MUST be lexically visible at the use site, so candidates
4856    // are restricted to params + TOP-LEVEL declared locals (a hoisted
4857    // decl is visible to javac name resolution everywhere in the
4858    // method) AND same-slot (same dex register lineage) AND unique.
4859    let mut top_ids: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
4860    for v in &vt.vars {
4861        if v.is_param {
4862            top_ids.insert(v.id);
4863        }
4864    }
4865    if let Stmt::Block(stmts) = body {
4866        for st in stmts.iter() {
4867            if let Stmt::LocalDef { var, .. } = st {
4868                top_ids.insert(*var);
4869            }
4870        }
4871    }
4872    let ret_wants_ref = match ret_ty {
4873        JavaType::Object(n) => n.as_ref() != "java/lang/Object",
4874        JavaType::Array(_) => true,
4875        _ => false,
4876    };
4877    let try_swap = |a: &mut Expr, want: &JavaType| {
4878        let Expr::Local { var, ty } = &mut *a else {
4879            return;
4880        };
4881        if (*var as usize) >= vt.vars.len() {
4882            return;
4883        }
4884        let avt = vt.var(*var).ty.erased();
4885        let avt_ref = matches!(avt, JavaType::Object(_) | JavaType::Array(_));
4886        let want_arr = matches!(want, JavaType::Array(_));
4887        // Only swap a primitive-typed actual (an Object actual gets the
4888        // descriptor-exact cast instead — never both).
4889        if avt_ref && !(avt == JavaType::Object("java/lang/Object".into())) {
4890            return;
4891        }
4892        if avt_ref && !matches!(want, JavaType::Object(_)) {
4893            return;
4894        }
4895        let slot = vt.var(*var).slot;
4896        let mut hits: Vec<u32> = Vec::new();
4897        for v in &vt.vars {
4898            if v.slot != slot || !top_ids.contains(&v.id) || v.id == *var {
4899                continue;
4900            }
4901            let t = v.ty.erased();
4902            let ok = match (&t, want) {
4903                (JavaType::Object(n), JavaType::Object(w)) => {
4904                    n.as_ref() == w.as_ref()
4905                        || pool.is_subtype(n.as_ref(), w.as_ref())
4906                }
4907                (JavaType::Array(_), JavaType::Array(_)) => want_arr,
4908                _ => false,
4909            };
4910            if ok {
4911                hits.push(v.id);
4912            }
4913        }
4914        hits.sort_unstable();
4915        hits.dedup();
4916        if hits.is_empty() {
4917            return;
4918        }
4919        // Multiple same-slot candidates: the LATEST generation (max id —
4920        // split mints in walk order ≈ program order) is the closest
4921        // approximation of the register's value at the use site. A
4922        // wrong pick still compiles (type-matched by construction);
4923        // leaving the primitive actual never does.
4924        let id = *hits.last().unwrap();
4925        *var = id;
4926        *ty = vt.vars[id as usize].ty.clone();
4927    };
4928    walk_stmt_exprs(body, &mut |e| {
4929        deep_rewrite(e, &mut |x| {
4930            match x {
4931                Expr::Method { desc, args, is_dynamic: false, .. } => {
4932                    for (i, a) in args.iter_mut().enumerate() {
4933                        if let Some(f) = desc.args.get(i) {
4934                            if matches!(f, JavaType::Object(n) if n.as_ref() != "java/lang/Object")
4935                                || matches!(f, JavaType::Array(_))
4936                            {
4937                                try_swap(a, f);
4938                            }
4939                        }
4940                    }
4941                }
4942                Expr::New { arg_tys, args, .. } => {
4943                    for (i, a) in args.iter_mut().enumerate() {
4944                        if let Some(f) = arg_tys.get(i) {
4945                            if matches!(f, JavaType::Object(n) if n.as_ref() != "java/lang/Object")
4946                                || matches!(f, JavaType::Array(_))
4947                            {
4948                                try_swap(a, f);
4949                            }
4950                        }
4951                    }
4952                }
4953                _ => {}
4954            }
4955        });
4956    });
4957    if ret_wants_ref {
4958        walk_mut_deep(body, &mut |st| {
4959            if let Stmt::Return(Some(e)) = st {
4960                try_swap(e, ret_ty);
4961            }
4962        });
4963    }
4964}
4965
4966/// Reference-array initializers take null, not 0: dex fill-array-data
4967/// over a String[]/Object[] slot carries the zero word, and the lifted
4968/// `new String[] {0}` is int无法转换为String (qs0/b family). A Const
4969/// Int(0) element of a reference-typed array init is the null ref.
4970/// `refVar = const 0` (dex `const/4 vN, 0` into a reference register) is
4971/// the NULL sentinel, not an int. By the time this runs infer_types has
4972/// typed the var a reference (its `.close()`/`.length`/method reads
4973/// proved it), so retype the const to Null BEFORE split_generations —
4974/// otherwise the ref/primitive mismatch splits off an int generation and
4975/// orphans the reference reads (u7.l try-with-resources finally
4976/// `v16_g3 = 0; if (v16_g3 != 0) v16_g3.close();` → "无法取消引用int").
4977/// `fix_incomparable_equality` already retypes the `refVar != 0`
4978/// COMPARISON side (but runs after the split); this covers the ASSIGN
4979/// side, which is what triggers the bad split.
4980pub fn fix_ref_null_assigns(vt: &VarTable, body: &mut Stmt) {
4981    let is_ref_var = |var: u32| -> bool {
4982        (var as usize) < vt.vars.len()
4983            && matches!(
4984                vt.vars[var as usize].ty.erased(),
4985                JavaType::Object(_) | JavaType::Array(_)
4986            )
4987    };
4988    walk_mut_deep(body, &mut |st| match st {
4989        Stmt::ExprStmt(Expr::Assign {
4990            target,
4991            value,
4992            op: AssignOp::Plain,
4993            ..
4994        }) => {
4995            if let Expr::Local { var, .. } = &**target {
4996                if is_ref_var(*var) && matches!(&**value, Expr::Const(ConstVal::Int(0))) {
4997                    **value = Expr::Const(ConstVal::Null);
4998                }
4999            }
5000        }
5001        Stmt::LocalDef {
5002            var,
5003            init: Some(e),
5004            ..
5005        } => {
5006            if is_ref_var(*var) && matches!(e, Expr::Const(ConstVal::Int(0))) {
5007                *e = Expr::Const(ConstVal::Null);
5008            }
5009        }
5010        _ => {}
5011    });
5012}
5013
5014pub fn fix_ref_array_null_consts(body: &mut Stmt) {
5015    walk_stmt_exprs(body, &mut |e| {
5016        deep_rewrite(e, &mut |x| {
5017            if let Expr::NewArray { elem, init: Some(list), .. } = x {
5018                if matches!(elem.erased(), JavaType::Object(_) | JavaType::Array(_)) {
5019                    for slot in list.iter_mut() {
5020                        if matches!(&*slot, Expr::Const(ConstVal::Int(0))) {
5021                            *slot = Expr::Const(ConstVal::Null);
5022                        }
5023                    }
5024                }
5025            }
5026        });
5027    });
5028}
5029
5030// ---------------------------------------------------------------------------
5031// Null-sentinel repair: const-0 registers in reference-typed slots
5032// ---------------------------------------------------------------------------
5033
5034/// A dex `const/4 vN, 0` register is ambiguous between int 0 and the
5035/// null reference; the value view types the bare constant as Int.
5036/// Kotlin coroutine state machines lean on this heavily — captured
5037/// object slots are zero-initialized ahead of the label switch, then
5038/// stored into `Object` fields and passed as `Continuation` ctor args
5039/// (weixin y01/s9 q8: `new e6(s94x, v287)` "int无法转换为Continuation";
5040/// uf5/l0 `throw 0;`; wcdb RepairKit `setOnCancelListener(0)`). The
5041/// dex verifier guarantees a const-0 register reaching a reference-
5042/// typed slot holds null, so in those slots the rendering must be the
5043/// null literal.
5044///
5045/// Two rewrites, both gated on the slot's expected type being a
5046/// reference (descriptor formal types, field type, array component,
5047/// cast target, throw, method return):
5048///   1. a literal `0` in a ref slot → `null`. SKIPPED for call-arg
5049///      slots whose formal is exactly `java/lang/Object`: `f(0)` there
5050///      compiles TODAY (boxing, or an int-overload pick like
5051///      `String.valueOf(0)`), and `f(null)` could newly fail on
5052///      overload ambiguity (`valueOf(Object)` vs `valueOf(char[])`) —
5053///      a guaranteed-error slot becomes a compile, a compiling slot
5054///      must not become an error.
5055///   2. a proven null-sentinel LOCAL: an Int-typed var whose every
5056///      write is `= 0` or `= <another sentinel>` and whose every read
5057///      sits in a ref slot (or feeds another sentinel's write — a dead
5058///      chain) → `null` at each read. The same Object-formal skip
5059///      applies at arg slots. A read in ANY int/unknown context
5060///      (binop, cond test, receiver deref, selector, anon-ctor arg,
5061///      lambda capture…) disqualifies the var — register reuse across
5062///      generations keeps its int rendering.
5063///
5064/// Runs after fix_ref_null_assigns (which converts writes of REF-typed
5065/// vars) and before split_generations (sentinel writes must not mint
5066/// int generations). Writes of confirmed sentinels stay as harmless
5067/// dead int stores for drop_dead_locals.
5068pub fn fix_null_sentinels(body: &mut Stmt, vt: &VarTable, ret: &JavaType) {
5069    #[derive(Clone, Copy, PartialEq)]
5070    enum WK {
5071        Zero,
5072        Copy(u32),
5073        Other,
5074    }
5075    fn is_ref_jt(t: &JavaType) -> bool {
5076        matches!(t, JavaType::Object(_) | JavaType::Array(_))
5077    }
5078    fn cls_write(e: &Expr) -> WK {
5079        match e {
5080            Expr::Const(ConstVal::Int(0)) => WK::Zero,
5081            Expr::Local { var, .. } => WK::Copy(*var),
5082            _ => WK::Other,
5083        }
5084    }
5085    fn expr_writes(e: &Expr, writes: &mut jdc_core::FxHashMap<u32, Vec<WK>>) {
5086        visit_exprs(e, &mut |x| match x {
5087            Expr::Assign { target, op, value } => {
5088                if let Expr::Local { var, .. } = &**target {
5089                    let k = if matches!(op, AssignOp::Plain) {
5090                        cls_write(value)
5091                    } else {
5092                        WK::Other
5093                    };
5094                    writes.entry(*var).or_default().push(k);
5095                }
5096            }
5097            Expr::PreIncDec { e: inner, .. } | Expr::PostIncDec { e: inner, .. } => {
5098                if let Expr::Local { var, .. } = &**inner {
5099                    writes.entry(*var).or_default().push(WK::Other);
5100                }
5101            }
5102            _ => {}
5103        });
5104    }
5105    fn collect_writes(
5106        s: &Stmt,
5107        writes: &mut jdc_core::FxHashMap<u32, Vec<WK>>,
5108        has_zero: &mut bool,
5109    ) {
5110        fn note(e: &Expr, has_zero: &mut bool, writes: &mut jdc_core::FxHashMap<u32, Vec<WK>>) {
5111            visit_exprs(e, &mut |x| {
5112                if let Expr::Const(ConstVal::Int(0)) = x {
5113                    *has_zero = true;
5114                }
5115            });
5116            expr_writes(e, writes);
5117        }
5118        match s {
5119            Stmt::Block(v) => {
5120                for x in v {
5121                    collect_writes(x, writes, has_zero);
5122                }
5123            }
5124            Stmt::LocalDef { var, init, .. } => {
5125                if let Some(e) = init {
5126                    if matches!(e, Expr::Const(ConstVal::Int(0))) {
5127                        *has_zero = true;
5128                    }
5129                    writes.entry(*var).or_default().push(cls_write(e));
5130                }
5131            }
5132            Stmt::ExprStmt(e) => note(e, has_zero, writes),
5133            Stmt::Return(e) => {
5134                if let Some(x) = e {
5135                    note(x, has_zero, writes);
5136                }
5137            }
5138            Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
5139                note(e, has_zero, writes);
5140            }
5141            Stmt::TernaryValue { e } => note(e, has_zero, writes),
5142            Stmt::If { cond, then_stmt, else_stmt } => {
5143                note(cond, has_zero, writes);
5144                collect_writes(then_stmt, writes, has_zero);
5145                if let Some(b) = else_stmt {
5146                    collect_writes(b, writes, has_zero);
5147                }
5148            }
5149            Stmt::While { cond, body } | Stmt::DoWhile { body, cond } => {
5150                note(cond, has_zero, writes);
5151                collect_writes(body, writes, has_zero);
5152            }
5153            Stmt::For { init, cond, update, body } => {
5154                for x in init {
5155                    collect_writes(x, writes, has_zero);
5156                }
5157                if let Some(c) = cond {
5158                    note(c, has_zero, writes);
5159                }
5160                for u in update {
5161                    note(u, has_zero, writes);
5162                }
5163                collect_writes(body, writes, has_zero);
5164            }
5165            Stmt::ForEach { var, iterable, body, .. } => {
5166                writes.entry(*var).or_default().push(WK::Other);
5167                note(iterable, has_zero, writes);
5168                collect_writes(body, writes, has_zero);
5169            }
5170            Stmt::Switch { selector, cases, default, .. } => {
5171                note(selector, has_zero, writes);
5172                for c in cases {
5173                    if let Some(g) = &c.guard {
5174                        note(g, has_zero, writes);
5175                    }
5176                    for x in &c.body {
5177                        collect_writes(x, writes, has_zero);
5178                    }
5179                }
5180                if let Some(d) = default {
5181                    collect_writes(d, writes, has_zero);
5182                }
5183            }
5184            Stmt::Try { body, catches, finally } => {
5185                collect_writes(body, writes, has_zero);
5186                for c in catches {
5187                    writes.entry(c.var).or_default().push(WK::Other);
5188                    collect_writes(&c.body, writes, has_zero);
5189                }
5190                if let Some(f) = finally {
5191                    collect_writes(f, writes, has_zero);
5192                }
5193            }
5194            Stmt::TryWithResources { resources, body, catches, finally, .. } => {
5195                for r in resources {
5196                    collect_writes(r, writes, has_zero);
5197                }
5198                collect_writes(body, writes, has_zero);
5199                for c in catches {
5200                    writes.entry(c.var).or_default().push(WK::Other);
5201                    collect_writes(&c.body, writes, has_zero);
5202                }
5203                if let Some(f) = finally {
5204                    collect_writes(f, writes, has_zero);
5205                }
5206            }
5207            Stmt::Assert { cond, msg } => {
5208                note(cond, has_zero, writes);
5209                if let Some(m) = msg {
5210                    note(m, has_zero, writes);
5211                }
5212            }
5213            Stmt::Synchronized { lock, body } => {
5214                note(lock, has_zero, writes);
5215                collect_writes(body, writes, has_zero);
5216            }
5217            Stmt::Labeled { body, .. } => collect_writes(body, writes, has_zero),
5218            _ => {}
5219        }
5220    }
5221
5222    let mut writes: jdc_core::FxHashMap<u32, Vec<WK>> =
5223        jdc_core::FxHashMap::default();
5224    let mut has_zero = false;
5225    collect_writes(body, &mut writes, &mut has_zero);
5226
5227    // Write-proven eligibility fixpoint (copy chains: `v = u`).
5228    let mut eligible: HashSet<u32> = HashSet::default();
5229    let int_typed = |v: u32| {
5230        (v as usize) < vt.vars.len() && vt.vars[v as usize].ty.erased() == JavaType::Int
5231    };
5232    loop {
5233        let mut changed = false;
5234        for (&v, ws) in writes.iter() {
5235            if eligible.contains(&v) || ws.is_empty() || !int_typed(v) {
5236                continue;
5237            }
5238            if ws.iter().all(|k| match k {
5239                WK::Zero => true,
5240                WK::Copy(u) => *u != v && eligible.contains(u),
5241                WK::Other => false,
5242            }) {
5243                eligible.insert(v);
5244                changed = true;
5245            }
5246        }
5247        if !changed {
5248            break;
5249        }
5250    }
5251    if eligible.is_empty() && !has_zero {
5252        return; // nothing this pass can do
5253    }
5254
5255    #[derive(Clone, Copy, PartialEq)]
5256    enum Mode {
5257        Scan,
5258        Rewrite,
5259    }
5260    struct St<'a> {
5261        mode: Mode,
5262        vt: &'a VarTable,
5263        eligible: &'a HashSet<u32>,
5264        ref_read: HashSet<u32>,
5265        dead: HashSet<u32>,
5266        sentinels: HashSet<u32>,
5267        n_lit: usize,
5268        n_var: usize,
5269    }
5270    // A call-arg slot whose formal is exactly java/lang/Object keeps
5271    // its current rendering (see the fn doc — boxing compiles today,
5272    // null could newly ambiguize).
5273    fn arg_ctx(t: &JavaType) -> Option<JavaType> {
5274        match t {
5275            JavaType::Object(n) if n.as_ref() == "java/lang/Object" => None,
5276            t if is_ref_jt(t) => Some(t.clone()),
5277            _ => None,
5278        }
5279    }
5280    fn walk_expr(e: &mut Expr, ctx: Option<JavaType>, st: &mut St) {
5281        walk_expr_c(e, ctx, false, st);
5282    }
5283    /// `cast_arg`: the slot is a call/ctor ARGUMENT — a rewritten null
5284    /// gets an explicit formal-type cast. Bare `f(null)` re-triggers
5285    /// overload ambiguity where `f(0)` used to pick an int overload
5286    /// (`in(null)` "引用不明确", `ContentValues.put(String,null)` vs its
5287    /// 9 overloads); `(String) null` is unambiguous and jadx-shaped.
5288    fn walk_expr_c(e: &mut Expr, ctx: Option<JavaType>, cast_arg: bool, st: &mut St) {
5289        let slot_ref = ctx.as_ref().is_some_and(is_ref_jt);
5290        fn mk_null(ctx: &Option<JavaType>, cast_arg: bool) -> Expr {
5291            match (cast_arg, ctx) {
5292                (true, Some(t)) => Expr::Cast {
5293                    ty: TypeRef::J(t.clone()),
5294                    e: Box::new(Expr::Const(ConstVal::Null)),
5295                },
5296                _ => Expr::Const(ConstVal::Null),
5297            }
5298        }
5299        match e {
5300            Expr::Const(ConstVal::Int(0)) if slot_ref => {
5301                if st.mode == Mode::Rewrite {
5302                    *e = mk_null(&ctx, cast_arg);
5303                    st.n_lit += 1;
5304                }
5305                return;
5306            }
5307            Expr::Local { var, .. } => {
5308                if st.eligible.contains(var) {
5309                    if slot_ref {
5310                        st.ref_read.insert(*var);
5311                    } else {
5312                        st.dead.insert(*var);
5313                    }
5314                }
5315                if st.mode == Mode::Rewrite && slot_ref && st.sentinels.contains(var) {
5316                    *e = mk_null(&ctx, cast_arg);
5317                    st.n_var += 1;
5318                }
5319                return;
5320            }
5321            _ => {}
5322        }
5323        match e {
5324            Expr::Method { owner, desc, args, .. } => {
5325                if let Some(o) = owner {
5326                    walk_expr(o, None, st);
5327                }
5328                for (i, a) in args.iter_mut().enumerate() {
5329                    let c = desc.args.get(i).and_then(arg_ctx);
5330                    walk_expr_c(a, c, true, st);
5331                }
5332            }
5333            Expr::Invokedynamic { desc, args, .. } => {
5334                for (i, a) in args.iter_mut().enumerate() {
5335                    let c = desc.args.get(i).and_then(arg_ctx);
5336                    walk_expr_c(a, c, true, st);
5337                }
5338            }
5339            Expr::New { args, arg_tys, .. } => {
5340                for (i, a) in args.iter_mut().enumerate() {
5341                    let c = arg_tys.get(i).and_then(arg_ctx);
5342                    walk_expr_c(a, c, true, st);
5343                }
5344            }
5345            Expr::AnonNew { args, .. } => {
5346                for a in args.iter_mut() {
5347                    walk_expr(a, None, st);
5348                }
5349            }
5350            Expr::Assign { target, value, .. } => {
5351                // Sentinel-to-sentinel copy: the whole store is dead;
5352                // its RHS is by the write gate a const-0 or a bare
5353                // local, so no nested reads can hide in it.
5354                if let Expr::Local { var: u, .. } = &**target {
5355                    if st.eligible.contains(u) {
5356                        return;
5357                    }
5358                }
5359                let vctx = match &**target {
5360                    Expr::Field { ty, .. } => {
5361                        let t = ty.erased();
5362                        is_ref_jt(&t).then_some(t)
5363                    }
5364                    Expr::ArrayIndex { array, .. } => match array.type_ref().erased() {
5365                        JavaType::Array(inner) if is_ref_jt(&inner) => Some(*inner),
5366                        _ => None,
5367                    },
5368                    _ => None,
5369                };
5370                walk_expr(target, None, st);
5371                walk_expr(value, vctx, st);
5372            }
5373            Expr::Cast { ty, e: inner } => {
5374                let t = ty.erased();
5375                let c = is_ref_jt(&t).then_some(t);
5376                walk_expr(inner, c, st);
5377            }
5378            Expr::InstanceOf { e: inner, .. } => {
5379                walk_expr(inner, Some(JavaType::Object("java/lang/Object".into())), st);
5380            }
5381            Expr::Cond { c, t, f } => {
5382                walk_expr(c, None, st);
5383                let tc = ctx.clone();
5384                walk_expr(t, tc, st);
5385                walk_expr(f, ctx, st);
5386            }
5387            Expr::NewArray { elem, dims, init, .. } => {
5388                for d in dims.iter_mut() {
5389                    walk_expr(d, None, st);
5390                }
5391                if let Some(list) = init {
5392                    let et = elem.erased();
5393                    let c = is_ref_jt(&et).then_some(et);
5394                    for slot in list.iter_mut() {
5395                        walk_expr(slot, c.clone(), st);
5396                    }
5397                }
5398            }
5399            Expr::NewMultiArray { dims, .. } => {
5400                for d in dims.iter_mut() {
5401                    walk_expr(d, None, st);
5402                }
5403            }
5404            Expr::ArrayIndex { array, index } => {
5405                walk_expr(array, None, st);
5406                walk_expr(index, None, st);
5407            }
5408            Expr::Field { owner, .. } => {
5409                if let Some(o) = owner {
5410                    walk_expr(o, None, st);
5411                }
5412            }
5413            Expr::Bin { l, r, .. } => {
5414                walk_expr(l, None, st);
5415                walk_expr(r, None, st);
5416            }
5417            Expr::Un { e: inner, .. } => walk_expr(inner, None, st),
5418            Expr::PreIncDec { e: inner, .. } | Expr::PostIncDec { e: inner, .. } => {
5419                walk_expr(inner, None, st);
5420            }
5421            Expr::StringConcat(parts) => {
5422                for p in parts.iter_mut() {
5423                    if let ConcatPart::Str(x) = p {
5424                        walk_expr(x, None, st);
5425                    }
5426                }
5427            }
5428            Expr::Lambda(l) => {
5429                for c in l.captures.iter_mut() {
5430                    walk_expr(c, None, st);
5431                }
5432            }
5433            _ => {}
5434        }
5435    }
5436    fn walk_stmt(s: &mut Stmt, st: &mut St, ret: &JavaType) {
5437        match s {
5438            Stmt::Block(v) => {
5439                for x in v.iter_mut() {
5440                    walk_stmt(x, st, ret);
5441                }
5442            }
5443            Stmt::ExprStmt(e) => walk_expr(e, None, st),
5444            Stmt::LocalDef { var, init, .. } => {
5445                if let Some(e) = init {
5446                    let ctx = if (*var as usize) < st.vt.vars.len() {
5447                        let t = st.vt.vars[*var as usize].ty.erased();
5448                        is_ref_jt(&t).then_some(t)
5449                    } else {
5450                        None
5451                    };
5452                    walk_expr(e, ctx, st);
5453                }
5454            }
5455            Stmt::Return(e) => {
5456                if let Some(x) = e {
5457                    let c = is_ref_jt(ret).then(|| ret.clone());
5458                    walk_expr(x, c, st);
5459                }
5460            }
5461            Stmt::Throw(e) => {
5462                walk_expr(e, Some(JavaType::Object("java/lang/Throwable".into())), st);
5463            }
5464            Stmt::If { cond, then_stmt, else_stmt } => {
5465                walk_expr(cond, None, st);
5466                walk_stmt(then_stmt, st, ret);
5467                if let Some(b) = else_stmt {
5468                    walk_stmt(b, st, ret);
5469                }
5470            }
5471            Stmt::While { cond, body } => {
5472                walk_expr(cond, None, st);
5473                walk_stmt(body, st, ret);
5474            }
5475            Stmt::DoWhile { body, cond } => {
5476                walk_stmt(body, st, ret);
5477                walk_expr(cond, None, st);
5478            }
5479            Stmt::For { init, cond, update, body } => {
5480                for x in init.iter_mut() {
5481                    walk_stmt(x, st, ret);
5482                }
5483                if let Some(c) = cond {
5484                    walk_expr(c, None, st);
5485                }
5486                for u in update.iter_mut() {
5487                    walk_expr(u, None, st);
5488                }
5489                walk_stmt(body, st, ret);
5490            }
5491            Stmt::ForEach { iterable, body, .. } => {
5492                walk_expr(iterable, None, st);
5493                walk_stmt(body, st, ret);
5494            }
5495            Stmt::Switch { selector, cases, default, .. } => {
5496                walk_expr(selector, None, st);
5497                for c in cases.iter_mut() {
5498                    if let Some(g) = &mut c.guard {
5499                        walk_expr(g, None, st);
5500                    }
5501                    for x in c.body.iter_mut() {
5502                        walk_stmt(x, st, ret);
5503                    }
5504                }
5505                if let Some(d) = default {
5506                    walk_stmt(d, st, ret);
5507                }
5508            }
5509            Stmt::Try { body, catches, finally } => {
5510                walk_stmt(body, st, ret);
5511                for c in catches.iter_mut() {
5512                    walk_stmt(&mut c.body, st, ret);
5513                }
5514                if let Some(f) = finally {
5515                    walk_stmt(f, st, ret);
5516                }
5517            }
5518            Stmt::TryWithResources { resources, body, catches, finally, .. } => {
5519                for r in resources.iter_mut() {
5520                    walk_stmt(r, st, ret);
5521                }
5522                walk_stmt(body, st, ret);
5523                for c in catches.iter_mut() {
5524                    walk_stmt(&mut c.body, st, ret);
5525                }
5526                if let Some(f) = finally {
5527                    walk_stmt(f, st, ret);
5528                }
5529            }
5530            Stmt::Assert { cond, msg } => {
5531                walk_expr(cond, None, st);
5532                if let Some(m) = msg {
5533                    walk_expr(m, None, st);
5534                }
5535            }
5536            Stmt::Synchronized { lock, body } => {
5537                walk_expr(lock, Some(JavaType::Object("java/lang/Object".into())), st);
5538                walk_stmt(body, st, ret);
5539            }
5540            Stmt::TernaryValue { e } => walk_expr(e, None, st),
5541            Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
5542                walk_expr(e, Some(JavaType::Object("java/lang/Object".into())), st);
5543            }
5544            Stmt::Labeled { body, .. } => walk_stmt(body, st, ret),
5545            _ => {}
5546        }
5547    }
5548
5549    let mut st = St {
5550        mode: Mode::Scan,
5551        vt,
5552        eligible: &eligible,
5553        ref_read: HashSet::default(),
5554        dead: HashSet::default(),
5555        sentinels: HashSet::default(),
5556        n_lit: 0,
5557        n_var: 0,
5558    };
5559    if !eligible.is_empty() {
5560        walk_stmt(body, &mut st, ret);
5561        st.sentinels = eligible
5562            .iter()
5563            .copied()
5564            .filter(|v| st.ref_read.contains(v) && !st.dead.contains(v))
5565            .collect();
5566    }
5567    if st.sentinels.is_empty() && !has_zero {
5568        return;
5569    }
5570    st.mode = Mode::Rewrite;
5571    walk_stmt(body, &mut st, ret);
5572    if std::env::var_os("DDC_SENT").is_some() {
5573        eprintln!(
5574            "[sent] elig={} confirmed={} lit={} var={}",
5575            eligible.len(),
5576            st.sentinels.len(),
5577            st.n_lit,
5578            st.n_var
5579        );
5580    }
5581}
5582
5583/// Int-context operand bridge: a BOOLEAN-typed side of an int-kind Bin
5584/// (bitwise, arithmetic, comparison against a numeric) becomes
5585/// `(b ? 1 : 0)` — the dex-level truth (booleans ARE 0/1 ints there;
5586/// register reuse puts a bool-typed var into an int expression:
5587/// `v5x | 4`, `b == 0` — "boolean无法转换为int" / "二元运算符操作数
5588/// 类型错误" families). Runs AFTER fix_bool_xor so boolean-SINK chains
5589/// were already converted to all-boolean form and are skipped here.
5590pub fn fix_int_operand_bridges(body: &mut Stmt, vt: &VarTable) {
5591    fn side_bool(e: &Expr, vt: &VarTable) -> bool {
5592        match e {
5593            Expr::Local { var, .. } => {
5594                matches!(vt.var(*var).ty.erased(), JavaType::Boolean)
5595            }
5596            // An all-boolean bitwise bin IS boolean even when its
5597            // embedded ty is still the frozen Int of the or-int lift
5598            // (booleanize retypes the vt; mixed_rewrite sets bin ty
5599            // only on the nodes it rewrites). The flat lookup called
5600            // `(ci13|ci14)` an int side, so the sibling `v408 != 0`
5601            // got `? 1 : 0`-wrapped while the bool bin stayed bare —
5602            // rendering int | boolean inside the wrap (news compose
5603            // CoreTextFieldKt `((v408!=0 ?1:0) | (ci13|ci14) ?1:0)|v412`
5604            // ×349 residual). Mirrors fix_bool_xor's bty recursion.
5605            Expr::Bin {
5606                op: BinOp::And | BinOp::Or | BinOp::Xor,
5607                l,
5608                r,
5609                ..
5610            } => side_bool(l, vt) && side_bool(r, vt),
5611            other => matches!(other.type_ref().erased(), JavaType::Boolean),
5612        }
5613    }
5614    fn side_int(e: &Expr, vt: &VarTable) -> bool {
5615        // A boolean-composed bin keeps its frozen Int embedded ty —
5616        // without this guard it double-answers as the "int side" of
5617        // its own bridge arm and steers the wrap onto the wrong operand.
5618        if side_bool(e, vt) {
5619            return false;
5620        }
5621        matches!(
5622            match e {
5623                Expr::Local { var, .. } => vt.var(*var).ty.erased(),
5624                other => other.type_ref().erased(),
5625            },
5626            JavaType::Int | JavaType::Short | JavaType::Byte | JavaType::Char | JavaType::Long
5627        )
5628    }
5629    fn wrap(e: Box<Expr>) -> Box<Expr> {
5630        Box::new(Expr::Cond {
5631            c: e,
5632            t: Box::new(Expr::Const(ConstVal::Int(1))),
5633            f: Box::new(Expr::Const(ConstVal::Int(0))),
5634        })
5635    }
5636    walk_stmt_exprs(body, &mut |e| {
5637        deep_rewrite(e, &mut |x| {
5638            // Field / array-store positions: the target's type is
5639            // descriptor-backed (authoritative); a boolean value into
5640            // an int slot bridges `(b ? 1 : 0)` (with a narrowing cast
5641            // for byte/short/char slots), an int value into a boolean
5642            // slot bridges `v != 0` (weixin `this.L[v3x] = max3`,
5643            // `g2x.d = p1x` — 785 field + ~700 array lines).
5644            if let Expr::Assign { target, value, op: AssignOp::Plain } = x {
5645                let tgt_ty: Option<JavaType> = match &**target {
5646                    Expr::Field { ty, .. } => Some(ty.erased()),
5647                    Expr::ArrayIndex { array, .. } => {
5648                        match array.type_ref().erased() {
5649                            JavaType::Array(el) => Some(el.as_ref().clone()),
5650                            _ => None,
5651                        }
5652                    }
5653                    // Local targets: the split/booleanize machinery owns
5654                    // the mixed-kind register cases; the residue that
5655                    // reached render (boolean无法转换为byte/long, ~360
5656                    // lines) gets the same value-side bridge.
5657                    Expr::Local { var, .. } => {
5658                        if (*var as usize) < vt.vars.len() {
5659                            Some(vt.var(*var).ty.erased())
5660                        } else {
5661                            None
5662                        }
5663                    }
5664                    _ => None,
5665                };
5666                if let Some(t) = tgt_ty {
5667                    let tgt_int = matches!(
5668                        t,
5669                        JavaType::Int
5670                            | JavaType::Short
5671                            | JavaType::Byte
5672                            | JavaType::Char
5673                            | JavaType::Long
5674                    );
5675                    if tgt_int && side_bool(value, vt) && !side_int(value, vt) {
5676                        let taken = std::mem::replace(
5677                            value,
5678                            Box::new(Expr::Const(ConstVal::Null)),
5679                        );
5680                        let mut wrapped = wrap(taken);
5681                        if !matches!(t, JavaType::Int | JavaType::Long) {
5682                            wrapped = Box::new(Expr::Cast {
5683                                ty: TypeRef::J(t.clone()),
5684                                e: wrapped,
5685                            });
5686                        }
5687                        *value = wrapped;
5688                    } else if matches!(t, JavaType::Boolean)
5689                        && side_int(value, vt)
5690                        && !side_bool(value, vt)
5691                    {
5692                        let taken = std::mem::replace(
5693                            value,
5694                            Box::new(Expr::Const(ConstVal::Null)),
5695                        );
5696                        **value = Expr::Bin {
5697                            op: BinOp::Ne,
5698                            l: taken,
5699                            r: Box::new(Expr::Const(ConstVal::Int(0))),
5700                            ty: Some(TypeRef::J(JavaType::Boolean)),
5701                        };
5702                    }
5703                }
5704            }
5705            // Explicit casts across the bool/num line are invalid Java
5706            // in BOTH directions: `(byte) this.r` (dex int-to-byte over
5707            // a boolean-rendered field — weixin AccInfo writeByte ×207)
5708            // becomes `(byte)(r ? 1 : 0)`, and `(boolean) x` over an
5709            // int becomes `x != 0`.
5710            if let Expr::Cast { ty, e } = x {
5711                let t = ty.erased();
5712                let t_num = matches!(
5713                    t,
5714                    JavaType::Int
5715                        | JavaType::Short
5716                        | JavaType::Byte
5717                        | JavaType::Char
5718                        | JavaType::Long
5719                        | JavaType::Float
5720                        | JavaType::Double
5721                );
5722                if t_num && side_bool(e, vt) && !side_int(e, vt) {
5723                    let taken = std::mem::replace(e, Box::new(Expr::Const(ConstVal::Null)));
5724                    **e = Expr::Cond {
5725                        c: taken,
5726                        t: Box::new(Expr::Const(ConstVal::Int(1))),
5727                        f: Box::new(Expr::Const(ConstVal::Int(0))),
5728                    };
5729                } else if matches!(t, JavaType::Boolean)
5730                    && side_int(e, vt)
5731                    && !side_bool(e, vt)
5732                {
5733                    let taken = std::mem::replace(e, Box::new(Expr::Const(ConstVal::Null)));
5734                    *x = Expr::Bin {
5735                        op: BinOp::Ne,
5736                        l: taken,
5737                        r: Box::new(Expr::Const(ConstVal::Int(0))),
5738                        ty: Some(TypeRef::J(JavaType::Boolean)),
5739                    };
5740                }
5741            }
5742            // Null-vs-int comparison: dex if-eqz/if-nez is type-agnostic
5743            // and null IS zero — when the local came out int-typed (phi
5744            // confluence residue), rewrite the Null constant to 0
5745            // (`v95_g12 != null` → `v95_g12 != 0`, faithful and legal;
5746            // yq0/g1 546-line 二元运算符 family). BOOLEAN side: the same
5747            // zero-register compare reads as the truth test — `b != null`
5748            // IS `b`, `b == null` IS `!b` (alipay v87_g7_g2 != null,
5749            // 二元运算符 '!=' boolean vs <空值> ×43 / news ×30).
5750            if let Expr::Bin { op, l, r, .. } = x {
5751                if matches!(op, BinOp::Eq | BinOp::Ne) {
5752                    if matches!(&**r, Expr::Const(ConstVal::Null))
5753                        && side_int(l, vt)
5754                        && !side_bool(l, vt)
5755                    {
5756                        **r = Expr::Const(ConstVal::Int(0));
5757                    } else if matches!(&**l, Expr::Const(ConstVal::Null))
5758                        && side_int(r, vt)
5759                        && !side_bool(r, vt)
5760                    {
5761                        **l = Expr::Const(ConstVal::Int(0));
5762                    } else if matches!(&**r, Expr::Const(ConstVal::Null))
5763                        && side_bool(l, vt)
5764                    {
5765                        let taken =
5766                            std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
5767                        *x = if matches!(op, BinOp::Eq) {
5768                            Expr::Un { op: UnOp::Not, e: taken }
5769                        } else {
5770                            *taken
5771                        };
5772                    } else if matches!(&**l, Expr::Const(ConstVal::Null))
5773                        && side_bool(r, vt)
5774                    {
5775                        let taken =
5776                            std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
5777                        *x = if matches!(op, BinOp::Eq) {
5778                            Expr::Un { op: UnOp::Not, e: taken }
5779                        } else {
5780                            *taken
5781                        };
5782                    }
5783                }
5784            }
5785            // A boolean-typed array INDEX is a reused register holding
5786            // an int (`this.L[v3x]` — boolean无法转换为int at the index
5787            // position): bridge it in reads and writes alike.
5788            if let Expr::ArrayIndex { index, .. } = x {
5789                if side_bool(index, vt) && !side_int(index, vt) {
5790                    let taken =
5791                        std::mem::replace(index, Box::new(Expr::Const(ConstVal::Null)));
5792                    *index = wrap(taken);
5793                }
5794            }
5795            // A boolean-typed array DIMENSION is the same reused-register
5796            // case at the allocation site (`new Object[v200]` where v200
5797            // is boolean-typed — weixin AppBrandRuntime, bool→int family):
5798            // dex new-array reads the size register as int and booleans
5799            // ARE 0/1 ints there, so bridge `(b ? 1 : 0)`.
5800            if let Expr::NewArray { dims, .. } = x {
5801                for d in dims.iter_mut() {
5802                    if side_bool(d, vt) && !side_int(d, vt) {
5803                        let taken =
5804                            std::mem::replace(d, Expr::Const(ConstVal::Null));
5805                        *d = *wrap(Box::new(taken));
5806                    }
5807                }
5808            }
5809            if let Expr::Bin { op, l, r, .. } = x {
5810                let int_kind = matches!(
5811                    op,
5812                    BinOp::Or
5813                        | BinOp::And
5814                        | BinOp::Xor
5815                        | BinOp::Add
5816                        | BinOp::Sub
5817                        | BinOp::Mul
5818                        | BinOp::Div
5819                        | BinOp::Rem
5820                        | BinOp::Shl
5821                        | BinOp::Shr
5822                        | BinOp::Ushr
5823                        | BinOp::Eq
5824                        | BinOp::Ne
5825                        | BinOp::Lt
5826                        | BinOp::Ge
5827                        | BinOp::Gt
5828                        | BinOp::Le
5829                );
5830                if !int_kind {
5831                    return;
5832                }
5833                // A Null CONSTANT operand of an int-kind bitwise/arith
5834                // bin is the dex zero register (or-int reads 0 out of a
5835                // register whose value view was null — `b | null`
5836                // renders boolean | <空值>, alipay ×43 / news ×30).
5837                // Rewriting to Int(0) is register-faithful and lets the
5838                // bool-side wraps below take over (`(b ? 1 : 0) | 0`).
5839                // Only when the SIBLING is a bool/int side: a
5840                // reference-typed sibling keeps the null shape (that is
5841                // SSA-merge residue, not a zero-register read), and a
5842                // String sibling keeps `+ null` concatenation ("null" —
5843                // valid and faithful; side_str excludes it via the
5844                // side_bool/side_int guard).
5845                if matches!(
5846                    op,
5847                    BinOp::Or
5848                        | BinOp::And
5849                        | BinOp::Xor
5850                        | BinOp::Add
5851                        | BinOp::Sub
5852                        | BinOp::Mul
5853                        | BinOp::Div
5854                        | BinOp::Rem
5855                        | BinOp::Shl
5856                        | BinOp::Shr
5857                        | BinOp::Ushr
5858                ) {
5859                    if matches!(&**r, Expr::Const(ConstVal::Null))
5860                        && (side_bool(l, vt) || side_int(l, vt))
5861                    {
5862                        **r = Expr::Const(ConstVal::Int(0));
5863                    } else if matches!(&**l, Expr::Const(ConstVal::Null))
5864                        && (side_bool(r, vt) || side_int(r, vt))
5865                    {
5866                        **l = Expr::Const(ConstVal::Int(0));
5867                    }
5868                }
5869                // PURE arithmetic/shift and ORDERING operands never take
5870                // a boolean side in ANY combination (`boolean + boolean`
5871                // is as invalid as `boolean + int` — reused 0/1 registers
5872                // in `p4x + v20 + (v21?1:0)`, uuyc u0/u02; `boolean <
5873                // boolean` likewise) — bridge each bool side on its own.
5874                // String concatenation is exempt: `"s" + b` is valid Java
5875                // and wrapping would print 1/0 instead of true/false.
5876                // Equality stays on the mixed-side rule below (a
5877                // bool == bool compare IS valid).
5878                let arith = matches!(
5879                    op,
5880                    BinOp::Add
5881                        | BinOp::Sub
5882                        | BinOp::Mul
5883                        | BinOp::Div
5884                        | BinOp::Rem
5885                        | BinOp::Shl
5886                        | BinOp::Shr
5887                        | BinOp::Ushr
5888                );
5889                let ordered = matches!(op, BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le);
5890                if arith || ordered {
5891                    let side_str = |e: &Expr| {
5892                        matches!(
5893                            match e {
5894                                Expr::Local { var, .. } => vt.var(*var).ty.erased(),
5895                                other => other.type_ref().erased(),
5896                            },
5897                            JavaType::Object(ref s) if s.as_ref() == "java/lang/String"
5898                        )
5899                    };
5900                    let concat = arith && (side_str(l) || side_str(r));
5901                    if !concat {
5902                        if side_bool(l, vt) {
5903                            let taken =
5904                                std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
5905                            *l = wrap(taken);
5906                        }
5907                        if side_bool(r, vt) {
5908                            let taken =
5909                                std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
5910                            *r = wrap(taken);
5911                        }
5912                    }
5913                    return;
5914                }
5915                // Comparisons only bridge against a NUMERIC constant or
5916                // int-typed side (a bool == bool compare is valid Java).
5917                if side_bool(l, vt) && side_int(r, vt) && !side_bool(r, vt) {
5918                    let taken = std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
5919                    *l = wrap(taken);
5920                } else if side_bool(r, vt) && side_int(l, vt) && !side_bool(l, vt) {
5921                    let taken = std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
5922                    *r = wrap(taken);
5923                }
5924            }
5925        });
5926    });
5927}
5928
5929/// Statement-level idiom restoration (readability parity with
5930/// jadx/DAD, flagged by the androguard comparison): `v = v + 1` →
5931/// `v++`, `v = v op x` → `v op= x`. Local targets only — a compound
5932/// field/array target would change how often the receiver expression
5933/// evaluates. Var must be the LEFT operand (non-commutative ops;
5934/// string-concat order). The ±1 fold requires a numeric vt type
5935/// (`s++` on a String is invalid while `s += 1` is not).
5936/// Boolean VALUE-diamond fold (DAD short_circuit value forms):
5937/// ```text
5938///   v = false; if (c) { v = true; }   →  v = c
5939///   v = true;  if (c) { v = false; }  →  v = !c
5940///   v = false; if (c) { v = E; }      →  v = c && E     (E bool-valued)
5941///   v = true;  if (c) { v = E; }      →  v = !c || E
5942///   v = D; if (c) { v = E1; } else { v = E2; }  →  v = c ? E1 : E2
5943/// ```
5944/// `v` must be Boolean-typed; the arm value bool-valued; the guard may
5945/// not touch `v`; base and arm constants must not coincide (a no-op
5946/// diamond could drop guard side effects). Kotlin default-arg bridge
5947/// ctors compute defaulted booleans in exactly this shape INSIDE the
5948/// branch ahead of the delegation (lark MmCreateAudioRequest's v21x
5949/// chain) — split_branch rejects the control flow as Dirty and the
5950/// this() call stays in the branch ("对this的调用必须是构造器中的第一
5951/// 个语句", lark ×456). Folded to a linear def (interleaved with
5952/// forward_single_use for the `[u = call; v = u]` arm shape) the
5953/// merge_at ternary fold applies. Source-faithful: the original was
5954/// `x?.m() ?: false` — one expression; the inlined call stays under
5955/// the guard arm, preserving evaluation order and short-circuiting.
5956/// General value-diamond fold (fold_bool_value_diamonds for ANY type):
5957/// `if (c) { v = a; } else { v = b; }` → `v = c ? a : b;`, plus the
5958/// split-pair shape `v = b; if (c) { v = a; }` → the same ternary. The
5959/// Kotlin default-arg ctor's mask-guarded per-arg defaults arrive as
5960/// diamonds (DocFreshInfo v10x/v14x); folding gives each carrier a
5961/// SINGLE-assignment def so the chain-aware delegation inline can hoist
5962/// the this() first ("对this的调用必须是构造器中的第一个语句"). Fold
5963/// only when both sides are exactly one plain assignment to the SAME
5964/// local and the value types are ternary-compatible (equal erased or
5965/// both numeric — mixed-category ternaries box/surprise).
5966pub fn fold_value_diamonds(body: &mut Stmt, vt: &VarTable) {
5967    let Stmt::Block(stmts) = body else { return };
5968    fold_diamonds_in(stmts, vt);
5969}
5970
5971fn diamond_types_ok(a: &Expr, b: &Expr) -> bool {
5972    fn num(t: &JavaType) -> bool {
5973        matches!(
5974            t,
5975            JavaType::Byte
5976                | JavaType::Short
5977                | JavaType::Int
5978                | JavaType::Char
5979                | JavaType::Long
5980                | JavaType::Float
5981                | JavaType::Double
5982        )
5983    }
5984    // A null branch is compatible with any REFERENCE other branch: the
5985    // ternary types as lub(T, null) = T. Kotlin default-args ctors
5986    // render null defaults (`if ((mask&2)==0) v=x; else v=null;`) —
5987    // rejecting the pair left the diamond unfolded, the defs
5988    // un-inlineable, and the this(...) delegation unhoistable (weibo
5989    // ctor-not-first robust-guard/second-delegation ×818). A primitive
5990    // sibling stays rejected (`c ? 5 : null` is not Java).
5991    if let Some(other) = if matches!(a, Expr::Const(ConstVal::Null)) {
5992        Some(b)
5993    } else if matches!(b, Expr::Const(ConstVal::Null)) {
5994        Some(a)
5995    } else {
5996        None
5997    } {
5998        return !num(&other.type_ref().erased());
5999    }
6000    let ta = a.type_ref().erased();
6001    let tb = b.type_ref().erased();
6002    ta == tb || (num(&ta) && num(&tb))
6003}
6004
6005/// `(var, target clone, value)` when the stmt is exactly ONE plain
6006/// assign to a local (bare or block-wrapped). The unwrap must be
6007/// RECURSIVE: the Kotlin default-args ctor diamonds arrive as
6008/// `Block([Block([Assign])])` (structurer nesting), and the one-level
6009/// peel missed them — the unfolded `if ((mask&2)==0) v=p; else v=0;`
6010/// prelude then blocked fix_ctor_super_first from hoisting the
6011/// `this(...)` delegation (weibo/lark ctor-not-first ×841 family).
6012fn single_local_assign(st: &Stmt) -> Option<(u32, Expr, Expr)> {
6013    let mut inner = st;
6014    while let Stmt::Block(v) = inner {
6015        if v.len() != 1 {
6016            return None;
6017        }
6018        inner = &v[0];
6019    }
6020    match inner {
6021        Stmt::ExprStmt(Expr::Assign { target, value, op: AssignOp::Plain }) => {
6022            match &**target {
6023                Expr::Local { var, .. } => Some((*var, *target.clone(), *value.clone())),
6024                _ => None,
6025            }
6026        }
6027        _ => None,
6028    }
6029}
6030
6031/// Like `single_local_assign`, but ALSO accepts an arm whose value is
6032/// produced by an arm-local def: `Block([LocalDef vX = init, v = vX])`
6033/// (possibly Block-wrapped) inlines `init` into the returned value when
6034/// `vX` is read exactly once there (single evaluation preserved). The
6035/// Kotlin mask-ctor long defaults render this shape (`else { long u =
6036/// Color.getUnspecified(); v37 = u; }` — compose TextStyle/SpanStyle);
6037/// without the inline the diamond stays unfolded and blocks the whole
6038/// delegation-inlining chain downstream.
6039fn arm_assign_inlined(st: &Stmt) -> Option<(u32, Expr, Expr)> {
6040    if let Some(r) = single_local_assign(st) {
6041        return Some(r);
6042    }
6043    let mut inner = st;
6044    while let Stmt::Block(v) = inner {
6045        if v.len() == 1 {
6046            inner = &v[0];
6047        } else {
6048            break;
6049        }
6050    }
6051    let Stmt::Block(v) = inner else { return None };
6052    if v.len() != 2 {
6053        return None;
6054    }
6055    let Stmt::LocalDef {
6056        var: x,
6057        init: Some(init),
6058        ..
6059    } = &v[0]
6060    else {
6061        return None;
6062    };
6063    let (var, tgt, val) = single_local_assign(&v[1])?;
6064    let mut count = 0usize;
6065    let mut c = val.clone();
6066    deep_rewrite(&mut c, &mut |e| {
6067        if let Expr::Local { var: lv, .. } = e {
6068            if lv == x {
6069                count += 1;
6070            }
6071        }
6072    });
6073    if count != 1 {
6074        return None;
6075    }
6076    let mut out = val.clone();
6077    deep_rewrite(&mut out, &mut |e| {
6078        if let Expr::Local { var: lv, .. } = e {
6079            if lv == x {
6080                *e = init.clone();
6081            }
6082        }
6083    });
6084    Some((var, tgt, out))
6085}
6086
6087fn mk_ternary_assign(tgt: Expr, c: Expr, a: Expr, b: Expr) -> Stmt {
6088    Stmt::ExprStmt(Expr::Assign {
6089        target: Box::new(tgt),
6090        op: AssignOp::Plain,
6091        value: Box::new(Expr::Cond {
6092            c: Box::new(c),
6093            t: Box::new(a),
6094            f: Box::new(b),
6095        }),
6096    })
6097}
6098
6099/// Two var ids that render as the SAME Java local: identical base name
6100/// (generation suffixes only appear on collisions, and same-base
6101/// generations of one dex register share the rendered name) and
6102/// identical erased type (a bool/num split pair must not merge).
6103fn same_render_var(v1: u32, v2: u32, vt: &VarTable) -> bool {
6104    let (a, b) = (vt.vars.get(v1 as usize), vt.vars.get(v2 as usize));
6105    match (a, b) {
6106        (Some(x), Some(y)) => x.name == y.name && x.ty.erased() == y.ty.erased(),
6107        _ => false,
6108    }
6109}
6110
6111/// Structural equality of two statement lists MODULO SSA generations:
6112/// each arm of a diamond writes its OWN generation of the rendered
6113/// locals, so strict `==` on the tails fails even when the rendered
6114/// code is identical. Canonicalize every local id to the first id seen
6115/// for its render key (name + erased type) under a SHARED map, then
6116/// compare structurally.
6117fn canon_stmts_eq(a: &[Stmt], b: &[Stmt], vt: &VarTable) -> bool {
6118    if a.len() != b.len() {
6119        return false;
6120    }
6121    let mut map: std::collections::HashMap<String, u32> = std::collections::HashMap::default();
6122    let canon = |s: &Stmt, map: &mut std::collections::HashMap<String, u32>| -> Stmt {
6123        let mut c = s.clone();
6124        walk_stmt_exprs(&mut c, &mut |e| {
6125            deep_rewrite(e, &mut |x| {
6126                if let Expr::Local { var, .. } = x {
6127                    let key = match vt.vars.get(*var as usize) {
6128                        Some(vi) => format!("{}#{:?}", vi.name, vi.ty.erased()),
6129                        None => format!("v{}", var),
6130                    };
6131                    let id = *map.entry(key).or_insert(*var);
6132                    *x = Expr::Local {
6133                        var: id,
6134                        ty: vt.var(id).ty.clone(),
6135                    };
6136                }
6137            });
6138        });
6139        c
6140    };
6141    a.iter()
6142        .zip(b.iter())
6143        .all(|(x, y)| canon(x, &mut map) == canon(y, &mut map))
6144}
6145
6146fn fold_diamonds_in(stmts: &mut Vec<Stmt>, vt: &VarTable) {
6147    let mut i = 0usize;
6148    while i < stmts.len() {
6149        match &mut stmts[i] {
6150            Stmt::Block(v) => fold_diamonds_in(v, vt),
6151            Stmt::If { then_stmt, else_stmt, .. } => {
6152                if let Stmt::Block(v) = &mut **then_stmt {
6153                    fold_diamonds_in(v, vt);
6154                }
6155                if let Some(e) = else_stmt {
6156                    if let Stmt::Block(v) = &mut **e {
6157                        fold_diamonds_in(v, vt);
6158                    }
6159                }
6160            }
6161            _ => {}
6162        }
6163        // Shape 1: standalone if/else diamond.
6164        let shape1 = if let Stmt::If { cond, then_stmt, else_stmt: Some(el), .. } = &stmts[i] {
6165            match (arm_assign_inlined(then_stmt), arm_assign_inlined(el)) {
6166                (Some((v1, tgt, a)), Some((v2, _, b)))
6167                    if v1 == v2 && diamond_types_ok(&a, &b) =>
6168                {
6169                    Some(mk_ternary_assign(tgt, cond.clone(), a, b))
6170                }
6171                _ => None,
6172            }
6173        } else {
6174            None
6175        };
6176        if let Some(f) = shape1 {
6177            stmts[i] = f;
6178            i += 1;
6179            continue;
6180        }
6181        // Shape 3: identical-delegation diamond — BOTH arms assign the
6182        // same rendered local, then run the same tail headed by a ctor
6183        // delegation (Kotlin mask-ctor last slot: `if ((m&M)==0) { v =
6184        // x; this(.., v, ..); return; } else { v = null; this(.., v,
6185        // ..); return; }` — alipay compose TextStyle/SpanStyle family).
6186        // Fold to `v = c ? x : null;` + the shared tail: the delegation
6187        // becomes unconditional (the hotfix-guard strip downstream needs
6188        // that) and fold_default_arg_bridge can then inline the prelude
6189        // carriers into the args. Tails compare modulo SSA generation
6190        // (each arm reads its own generation of the one rendered local).
6191        // Gates: tail headed by a bare delegation, tails canonically
6192        // identical, the tail writes NEITHER generation, types ok.
6193        let shape3 = if let Stmt::If {
6194            cond,
6195            then_stmt,
6196            else_stmt: Some(el),
6197            ..
6198        } = &stmts[i]
6199        {
6200            let arm = |st: &Stmt| -> Option<((u32, Expr, Expr), Vec<Stmt>)> {
6201                let Stmt::Block(v) = st else { return None };
6202                if v.len() < 2 {
6203                    return None;
6204                }
6205                let first = single_local_assign(&v[0])?;
6206                Some((first, v[1..].to_vec()))
6207            };
6208            match (arm(then_stmt), arm(el)) {
6209                (Some(((v1, tgt1, a), mut rest_t)), Some(((v2, _t2, b), rest_e)))
6210                    if !rest_t.is_empty()
6211                        && is_bare_ctor_call(first_leaf_stmt(&rest_t[0]))
6212                        && stmts_count_writes(&rest_t, v1) == 0
6213                        && stmts_count_writes(&rest_t, v2) == 0
6214                        && diamond_types_ok(&a, &b) =>
6215                {
6216                    if v1 == v2 || same_render_var(v1, v2, vt) {
6217                        // same rendered local: fold the assign, keep the tail
6218                        if canon_stmts_eq(&rest_t, &rest_e, vt) {
6219                            Some((mk_ternary_assign(tgt1, cond.clone(), a, b), rest_t))
6220                        } else {
6221                            None
6222                        }
6223                    } else if stmts_read_var(&rest_t, v1) == 1
6224                        && stmts_read_var(&rest_e, v2) == 1
6225                        && stmts_read_var(&rest_t, v2) == 0
6226                        && stmts_read_var(&rest_e, v1) == 0
6227                    {
6228                        // SPLIT arm-local carriers (compose Span: then
6229                        // reads v23_g3, else reads v23 — different render
6230                        // names, one read each): unify by substituting
6231                        // v2 -> v1 in the else tail and requiring EXACT
6232                        // equality, then inline `c ? A1 : A2` at the
6233                        // single v1 read of the kept tail. No ternary
6234                        // assign needed — both carriers die in the fold.
6235                        let mut unified = rest_e.clone();
6236                        for st in unified.iter_mut() {
6237                            let mut c2 = st.clone();
6238                            walk_stmt_exprs(&mut c2, &mut |e| {
6239                                deep_rewrite(e, &mut |x| {
6240                                    if let Expr::Local { var, .. } = x {
6241                                        if *var == v2 {
6242                                            *x = Expr::Local {
6243                                                var: v1,
6244                                                ty: vt.var(v1).ty.clone(),
6245                                            };
6246                                        }
6247                                    }
6248                                });
6249                            });
6250                            *st = c2;
6251                        }
6252                        if unified != rest_t {
6253                            None
6254                        } else {
6255                            let tern = Expr::Cond {
6256                                c: Box::new(cond.clone()),
6257                                t: Box::new(a.clone()),
6258                                f: Box::new(b.clone()),
6259                            };
6260                            for st in rest_t.iter_mut() {
6261                                let mut c2 = st.clone();
6262                                walk_stmt_exprs(&mut c2, &mut |e| {
6263                                    deep_rewrite(e, &mut |x| {
6264                                        if let Expr::Local { var, .. } = x {
6265                                            if *var == v1 {
6266                                                *x = tern.clone();
6267                                            }
6268                                        }
6269                                    });
6270                                });
6271                                *st = c2;
6272                            }
6273                            // the If is REPLACED by the (rewritten) tail:
6274                            // no ternary assign survives — both carriers
6275                            // die in the fold.
6276                            let first = rest_t.remove(0);
6277                            Some((first, rest_t))
6278                        }
6279                    } else {
6280                        None
6281                    }
6282                }
6283                _ => None,
6284            }
6285        } else {
6286            None
6287        };
6288        if let Some((folded, rest)) = shape3 {
6289            stmts[i] = folded;
6290            stmts.splice(i + 1..i + 1, rest);
6291            i += 1;
6292            continue;
6293        }
6294        // Shape 2: adjacent `v = b;` + `if (c) { v = a; }` (no else).
6295        // The two assigns may target DIFFERENT SSA generations of the
6296        // same rendered local (sequential conditional overwrite — the
6297        // Kotlin mask-ctor `v11x = p3x; if ((mask&4)!=0) v11x = -1L;`
6298        // prelude, lark/weixin branched-defs ctor family): same base
6299        // name + same erased type renders as one Java local, so the
6300        // fold's else arm reads generation 1 and the assign targets
6301        // generation 2 — textually `v = c ? a : v`, exactly the
6302        // observable behavior of the unfolded pair.
6303        if i + 1 < stmts.len() {
6304            // An EMPTY else block counts as no-else: the structurer
6305            // emits Some(Block([])) where the source shape had none,
6306            // and cleanup (which would drop it) runs AFTER the fold —
6307            // the strict None match missed every real-world overwrite
6308            // (zero shape-2 folds on lark mask ctors).
6309            // Shape 2: adjacent `v = b;` + conditional overwrite with
6310            // NO other effect:
6311            //   form A: `if (c) { v = a; }` (else absent/empty)  -> v = c ? a : b
6312            //   form B: `if (c) { } else { v = a; }` (then empty) -> v = c ? b : a
6313            // Form B is the FOLD-time shape of every rendered no-else
6314            // overwrite: invert_empty_thens flips then/else AFTER this
6315            // pass, so the strict `else_stmt: None, overwrite-in-then`
6316            // match never saw a real-world instance (zero shape-2 folds
6317            // on lark mask ctors). The two assigns may target DIFFERENT
6318            // SSA generations of one rendered local (same base name +
6319            // same erased type): the ternary's keep-arm then reads
6320            // generation 1 — textually `v = c ? a : v`, exactly the
6321            // observable behavior of the unfolded pair (Kotlin mask-ctor
6322            // preludes: `v11x = p3x; if ((mask&4)!=0) v11x = -1L;`).
6323            let shape2 = {
6324                let s_i = single_local_assign(&stmts[i]);
6325                if let Stmt::If {
6326                    cond,
6327                    then_stmt,
6328                    else_stmt,
6329                    ..
6330                } = &stmts[i + 1]
6331                {
6332                    let then_a = single_local_assign(then_stmt);
6333                    let else_a = else_stmt.as_deref().and_then(single_local_assign);
6334                    let then_empty =
6335                        matches!(&**then_stmt, Stmt::Block(v) if v.is_empty());
6336                    let else_empty = else_stmt
6337                        .as_deref()
6338                        .map(|e| matches!(e, Stmt::Block(v) if v.is_empty()))
6339                        .unwrap_or(true);
6340                    let try_fold = |over: Option<(u32, Expr, Expr)>,
6341                                    keep_over: bool|
6342                     -> Option<Stmt> {
6343                        let (v2, tgt2, a) = over?;
6344                        let (v1, _t1, b) = s_i.as_ref()?;
6345                        if !(*v1 == v2 || same_render_var(*v1, v2, vt)) {
6346                            return None;
6347                        }
6348                        let keep = if *v1 == v2 {
6349                            b.clone()
6350                        } else {
6351                            Expr::Local {
6352                                var: *v1,
6353                                ty: vt.var(*v1).ty.clone(),
6354                            }
6355                        };
6356                        if !diamond_types_ok(&a, &keep) {
6357                            return None;
6358                        }
6359                        if keep_over {
6360                            Some(mk_ternary_assign(tgt2, cond.clone(), keep, a))
6361                        } else {
6362                            Some(mk_ternary_assign(tgt2, cond.clone(), a, keep))
6363                        }
6364                    };
6365                    if then_a.is_some() && else_empty {
6366                        try_fold(then_a, false)
6367                    } else if else_a.is_some() && then_empty {
6368                        try_fold(else_a, true)
6369                    } else {
6370                        None
6371                    }
6372                } else {
6373                    None
6374                }
6375            };
6376            // Leading-assign variant: the branch STARTS with the
6377            // overwrite and carries a tail (Kotlin mask-ctor last slot:
6378            // `str12 = null; if ((m&32)==0) { str12 = str6; this(..,
6379            // str12); return; }` — news_article CommonData/DTO family).
6380            // Fold the assign into the ternary and leave the tail in the
6381            // branch: the tail's reads see the identical value on both
6382            // paths (the ternary assigns before the branch runs). Gate:
6383            // the tail must not WRITE the var.
6384            let mut lead_fold: Option<(Stmt, Stmt)> = None;
6385            if shape2.is_none() && i + 1 < stmts.len() {
6386                let s_i2 = single_local_assign(&stmts[i]);
6387                let leading = |st: &Stmt| -> Option<((u32, Expr, Expr), Vec<Stmt>)> {
6388                    let Stmt::Block(v) = st else { return None };
6389                    if v.len() < 2 {
6390                        return None;
6391                    }
6392                    let first = single_local_assign(&v[0])?;
6393                    Some((first, v[1..].to_vec()))
6394                };
6395                if let (Some((v1, _t1, b)), Stmt::If { cond, then_stmt, else_stmt, .. }) =
6396                    (&s_i2, &stmts[i + 1])
6397                {
6398                    let else_absent = else_stmt.is_none()
6399                        || else_stmt
6400                            .as_deref()
6401                            .is_some_and(|e| matches!(e, Stmt::Block(v) if v.is_empty()));
6402                    let then_absent = matches!(&**then_stmt, Stmt::Block(v) if v.is_empty());
6403                    let try_lead = |over: Option<((u32, Expr, Expr), Vec<Stmt>)>,
6404                                    keep_over: bool|
6405                     -> Option<(Stmt, Stmt)> {
6406                        let ((v2, tgt2, a), rest) = over?;
6407                        if !(*v1 == v2 || same_render_var(*v1, v2, vt)) {
6408                            return None;
6409                        }
6410                        if stmts_count_writes(&rest, *v1) != 0
6411                            || stmts_count_writes(&rest, v2) != 0
6412                        {
6413                            return None;
6414                        }
6415                        let keep = if *v1 == v2 {
6416                            b.clone()
6417                        } else {
6418                            Expr::Local {
6419                                var: *v1,
6420                                ty: vt.var(*v1).ty.clone(),
6421                            }
6422                        };
6423                        if !diamond_types_ok(&a, &keep) {
6424                            return None;
6425                        }
6426                        let t = if keep_over {
6427                            mk_ternary_assign(tgt2, cond.clone(), keep, a)
6428                        } else {
6429                            mk_ternary_assign(tgt2, cond.clone(), a, keep)
6430                        };
6431                        let mut new_if = stmts[i + 1].clone();
6432                        if let Stmt::If { then_stmt, else_stmt, .. } = &mut new_if {
6433                            if keep_over {
6434                                *else_stmt = Some(Box::new(Stmt::Block(rest)));
6435                            } else {
6436                                **then_stmt = Stmt::Block(rest);
6437                            }
6438                        }
6439                        Some((t, new_if))
6440                    };
6441                    if else_absent {
6442                        lead_fold = try_lead(leading(then_stmt), false);
6443                    } else if then_absent {
6444                        if let Some(e) = else_stmt {
6445                            lead_fold = try_lead(leading(e), true);
6446                        }
6447                    }
6448                }
6449            }
6450            if let Some((folded, new_if)) = lead_fold {
6451                stmts[i] = folded;
6452                stmts[i + 1] = new_if;
6453                continue;
6454            }
6455            if let Some(f) = shape2 {
6456                stmts[i] = f;
6457                stmts.remove(i + 1);
6458                continue; // re-scan the folded position
6459            }
6460        }
6461        i += 1;
6462    }
6463}
6464
6465pub fn fold_bool_value_diamonds(body: &mut Stmt, vt: &VarTable) {
6466    fn is_bool_var(v: u32, vt: &VarTable) -> bool {
6467        (v as usize) < vt.vars.len()
6468            && matches!(vt.vars[v as usize].ty.erased(), JavaType::Boolean)
6469    }
6470    fn bool_const(e: &Expr) -> Option<bool> {
6471        fn cint(e: &Expr) -> Option<i32> {
6472            match e {
6473                Expr::Const(ConstVal::Int(n)) => Some(*n),
6474                _ => None,
6475            }
6476        }
6477        match e {
6478            Expr::Const(ConstVal::Int(0)) => Some(false),
6479            Expr::Const(ConstVal::Int(1)) => Some(true),
6480            // int↔bool bridge wrappings of constants: `0 != 0` is the
6481            // fallback pipeline's rendered `false` by the time the
6482            // ctor-block fold runs.
6483            Expr::Bin { op: op @ (BinOp::Eq | BinOp::Ne), l, r, .. } => {
6484                let (a, b) = (cint(l)?, cint(r)?);
6485                let eq = a == b;
6486                Some(if matches!(op, BinOp::Eq) { eq } else { !eq })
6487            }
6488            _ => None,
6489        }
6490    }
6491    fn reads_var(e: &Expr, v: u32) -> bool {
6492        let mut hit = false;
6493        visit_exprs(e, &mut |x| {
6494            if let Expr::Local { var, .. } = x {
6495                if *var == v {
6496                    hit = true;
6497                }
6498            }
6499        });
6500        hit
6501    }
6502    /// Bool-valued expression for the composite arms (&&, ||, ?:).
6503    /// Bare 0/1 constants are handled by the caller's special cases —
6504    /// inside a composite they would render as invalid `c && 1`.
6505    fn bool_valued(e: &Expr, vt: &VarTable) -> bool {
6506        fn side(e: &Expr, vt: &VarTable) -> bool {
6507            bool_const(e).is_some() || bool_valued(e, vt)
6508        }
6509        match e {
6510            Expr::Local { var, .. } => is_bool_var(*var, vt),
6511            Expr::Method { desc, .. } => desc.ret == JavaType::Boolean,
6512            Expr::InstanceOf { .. } => true,
6513            Expr::Un { op: UnOp::Not, e: x, .. } => bool_valued(x, vt) || bool_const(x).is_some(),
6514            Expr::Bin { op, l, r, .. } => match op {
6515                BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le => true,
6516                // A logical operand may itself be a 0/1 constant
6517                // (booleanize leaves mixed forms).
6518                BinOp::LogAnd | BinOp::LogOr => {
6519                    side(l, vt) && side(r, vt)
6520                }
6521                _ => false,
6522            },
6523            Expr::Cond { t, f, .. } => side(t, vt) && side(f, vt),
6524            _ => false,
6525        }
6526    }
6527    /// A branch that is exactly one plain `v = E` with E bool-valued
6528    /// (or a 0/1 constant) and not reading v — OR the forwarding PAIR
6529    /// `[u = E; v = u]` where u is single-assigned/single-read in the
6530    /// whole method: the fold consumes the pair into `v = E` (the impure
6531    /// `u = src.m()` arm shape forward_single_use cannot inline — its
6532    /// phi-ref gate rejects values reading multi-assigned prelude vars,
6533    /// and inlining here is scope-safe because E moves exactly one
6534    /// block level out with the arm's lone def consumed).
6535    fn lone_assign(
6536        b: &Stmt,
6537        v: u32,
6538        vt: &VarTable,
6539        an: &VarAnalysis,
6540        consumed: &mut HashSet<u32>,
6541    ) -> Option<Expr> {
6542        let l = flat_list(b);
6543        if l.len() == 2 {
6544            let (u, e): (u32, &Expr) = match l[0] {
6545                Stmt::LocalDef { var, init: Some(e), .. } => (*var, e),
6546                Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => {
6547                    match &**target {
6548                        Expr::Local { var, .. } => (*var, &**value),
6549                        _ => return None,
6550                    }
6551                }
6552                _ => return None,
6553            };
6554            if u == v || consumed.contains(&u) {
6555                return None;
6556            }
6557            // Whole-method single-assign/single-read: the fold drops
6558            // u's def with the arm, so ANY other read would dangle.
6559            // (Analysis counts are per-round; folds only ever REMOVE
6560            // reads elsewhere or clone E's — whose variable reads were
6561            // already counted at their def sites — so stale counts
6562            // over-approximate and the ==1 gates stay sound.)
6563            if an.assigns.get(u as usize).copied().unwrap_or(0) != 1
6564                || an.reads.get(u as usize).copied().unwrap_or(0) != 1
6565            {
6566                return None;
6567            }
6568            let (tv, rv) = match l[1] {
6569                Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => {
6570                    match (&**target, &**value) {
6571                        (Expr::Local { var: t, .. }, Expr::Local { var: r, .. }) => (*t, *r),
6572                        _ => return None,
6573                    }
6574                }
6575                Stmt::LocalDef { var, init: Some(Expr::Local { var: r, .. }), .. } => (*var, *r),
6576                _ => return None,
6577            };
6578            if tv != v || rv != u {
6579                return None;
6580            }
6581            if reads_var(e, v) || !(bool_valued(e, vt) || bool_const(e).is_some()) {
6582                return None;
6583            }
6584            consumed.insert(u);
6585            return Some(e.clone());
6586        }
6587        if l.len() != 1 {
6588            return None;
6589        }
6590        let (target, value) = match l[0] {
6591            Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => (target, value),
6592            Stmt::LocalDef { var, init: Some(value), .. } if *var == v => {
6593                if reads_var(value, v) || !(bool_valued(value, vt) || bool_const(value).is_some()) {
6594                    return None;
6595                }
6596                return Some(value.clone());
6597            }
6598            _ => return None,
6599        };
6600        let Expr::Local { var: tv, .. } = &**target else { return None };
6601        if *tv != v || reads_var(value, v) {
6602            return None;
6603        }
6604        if !(bool_valued(value, vt) || bool_const(value).is_some()) {
6605            return None;
6606        }
6607        Some((**value).clone())
6608    }
6609    fn not(e: Expr) -> Expr {
6610        Expr::Un { op: UnOp::Not, e: Box::new(e) }
6611    }
6612    /// Peephole the int↔bool bridge artifacts the fallback pipeline
6613    /// leaves in guards: `!(x != y)` → `x == y`; `(b ? 1 : 0) == 1` →
6614    /// `b`; `!(b ? 1 : 0 == 1)` chains likewise. Purely a readability
6615    /// pass — the unsimplified forms compile.
6616    fn simplify_bool(e: Expr) -> Expr {
6617        match e {
6618            Expr::Un { op: UnOp::Not, e: inner, .. } => match *inner {
6619                // Flip first, THEN re-simplify: `!((u?1:0) != 1)` must
6620                // reach the cond01 arm as `(u?1:0) == 1` → `u` (the
6621                // flip result never re-entered simplification and the
6622                // pair arm saw a Bin where it needed the bare local).
6623                Expr::Bin { op: BinOp::Ne, l, r, ty } => {
6624                    simplify_bool(Expr::Bin { op: BinOp::Eq, l, r, ty })
6625                }
6626                Expr::Bin { op: BinOp::Eq, l, r, ty } => {
6627                    simplify_bool(Expr::Bin { op: BinOp::Ne, l, r, ty })
6628                }
6629                other => Expr::Un { op: UnOp::Not, e: Box::new(simplify_bool(other)) },
6630            },
6631            Expr::Bin { op: op @ (BinOp::Eq | BinOp::Ne), l, r, ty } => {
6632                // (c ? 1 : 0) <op> 1  →  c / !c ; <op> 0 → !c / c
6633                fn cond01(e: &Expr) -> Option<Expr> {
6634                    if let Expr::Cond { c, t, f } = e {
6635                        if matches!(&**t, Expr::Const(ConstVal::Int(1)))
6636                            && matches!(&**f, Expr::Const(ConstVal::Int(0)))
6637                        {
6638                            return Some((**c).clone());
6639                        }
6640                    }
6641                    None
6642                }
6643                let (lc, rc) = (cond01(&l), cond01(&r));
6644                match (lc, rc) {
6645                    (Some(c), None) => match (&*r, op) {
6646                        (Expr::Const(ConstVal::Int(1)), BinOp::Eq) => c,
6647                        (Expr::Const(ConstVal::Int(1)), BinOp::Ne) => not(c),
6648                        (Expr::Const(ConstVal::Int(0)), BinOp::Eq) => not(c),
6649                        (Expr::Const(ConstVal::Int(0)), BinOp::Ne) => c,
6650                        _ => Expr::Bin { op, l: Box::new(c), r, ty },
6651                    },
6652                    (None, Some(c)) => match (&*l, op) {
6653                        (Expr::Const(ConstVal::Int(1)), BinOp::Eq) => c,
6654                        (Expr::Const(ConstVal::Int(1)), BinOp::Ne) => not(c),
6655                        (Expr::Const(ConstVal::Int(0)), BinOp::Eq) => not(c),
6656                        (Expr::Const(ConstVal::Int(0)), BinOp::Ne) => c,
6657                        _ => Expr::Bin { op, l, r: Box::new(c), ty },
6658                    },
6659                    _ => Expr::Bin { op, l, r, ty },
6660                }
6661            }
6662            other => other,
6663        }
6664    }
6665    fn bin(op: BinOp, l: Expr, r: Expr) -> Expr {
6666        Expr::Bin { op, l: Box::new(l), r: Box::new(r), ty: None }
6667    }
6668
6669    for _round in 0..4 {
6670        let mut changed = false;
6671        let mut analysis = VarAnalysis {
6672            assigns: Vec::new(),
6673            reads: Vec::new(),
6674            values: Vec::new(),
6675        };
6676        analyze_vars(body, &mut analysis);
6677        let mut consumed: HashSet<u32> = HashSet::default();
6678        walk_mut_deep(body, &mut |st| {
6679            let Stmt::Block(list) = st else { return };
6680            let mut i = 0usize;
6681            while i + 1 < list.len() {
6682                let folded = try_at(list, i, vt, &is_bool_var, &bool_const, &reads_var, &lone_assign, &analysis, &mut consumed);
6683                match folded {
6684                    Some(expr) => {
6685                        let is_def = matches!(&list[i], Stmt::LocalDef { .. });
6686                        let var = match &list[i] {
6687                            Stmt::LocalDef { var, .. } => *var,
6688                            Stmt::ExprStmt(Expr::Assign { target, .. }) => {
6689                                match &**target { Expr::Local { var, .. } => *var, _ => unreachable!() }
6690                            }
6691                            _ => unreachable!(),
6692                        };
6693                        let (is_final, force_type) = match &list[i] {
6694                            Stmt::LocalDef { is_final, force_type, .. } => (*is_final, *force_type),
6695                            _ => (false, false),
6696                        };
6697                        list[i] = if is_def {
6698                            Stmt::LocalDef { var, init: Some(expr), is_final, force_type }
6699                        } else {
6700                            Stmt::ExprStmt(Expr::Assign {
6701                                target: Box::new(Expr::Local {
6702                                    var,
6703                                    ty: TypeRef::J(JavaType::Boolean),
6704                                }),
6705                                op: AssignOp::Plain,
6706                                value: Box::new(expr),
6707                            })
6708                        };
6709                        list.remove(i + 1);
6710                        changed = true;
6711                        // stay at i: a preceding base may now pair with
6712                        // nothing new, but a FOLLOWING diamond may chain.
6713                    }
6714                    None => i += 1,
6715                }
6716            }
6717        });
6718        if !changed {
6719            break;
6720        }
6721    }
6722
6723    #[allow(clippy::too_many_arguments, clippy::type_complexity)]
6724    fn try_at(
6725        list: &[Stmt],
6726        i: usize,
6727        vt: &VarTable,
6728        is_bool_var: &dyn Fn(u32, &VarTable) -> bool,
6729        bool_const: &dyn Fn(&Expr) -> Option<bool>,
6730        reads_var: &dyn Fn(&Expr, u32) -> bool,
6731        lone_assign: &dyn Fn(&Stmt, u32, &VarTable, &VarAnalysis, &mut HashSet<u32>) -> Option<Expr>,
6732        an: &VarAnalysis,
6733        consumed: &mut HashSet<u32>,
6734    ) -> Option<Expr> {
6735        // s1: `v = <0|1>` (Assign or LocalDef), v Boolean-typed.
6736        let (v, base) = match &list[i] {
6737            Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => {
6738                match &**target {
6739                    Expr::Local { var, .. } => {
6740                        // The value may be a wrapped constant (`0 != 0`
6741                        // — the fallback pipeline's rendered false).
6742                        let b = bool_const(value)?;
6743                        if !is_bool_var(*var, vt) { return None; }
6744                        (*var, b)
6745                    }
6746                    _ => return None,
6747                }
6748            }
6749            Stmt::LocalDef { var, init: Some(e), .. } => {
6750                let b = bool_const(e)?;
6751                if !is_bool_var(*var, vt) { return None; }
6752                (*var, b)
6753            }
6754            _ => return None,
6755        };
6756        let Stmt::If { cond, then_stmt, else_stmt } = &list[i + 1] else {
6757            return None;
6758        };
6759        if reads_var(cond, v) {
6760            return None;
6761        }
6762        let mut touches_v = false;
6763        visit_exprs(cond, &mut |x| {
6764            if let Expr::Assign { target, .. } = x {
6765                if let Expr::Local { var, .. } = &**target {
6766                    if *var == v { touches_v = true; }
6767                }
6768            }
6769        });
6770        if touches_v {
6771            return None;
6772        }
6773        // The structurer emits inverted diamonds (`if (c) {} else { v = E }`,
6774        // empty-then) — normalize by negating the guard.
6775        let (cond_n, then_arm, else_arm): (Expr, &Stmt, Option<&Stmt>) =
6776            if flat_list(then_stmt).is_empty() && else_stmt.is_some() {
6777                (not(cond.clone()), else_stmt.as_ref().unwrap(), None)
6778            } else {
6779                (cond.clone(), then_stmt.as_ref(), else_stmt.as_deref())
6780            };
6781        let e1 = lone_assign(then_arm, v, vt, an, consumed)?;
6782        let e2 = match else_arm {
6783            None => None,
6784            Some(b) => Some(lone_assign(b, v, vt, an, consumed)?),
6785        };
6786        let e1c = bool_const(&e1);
6787        let c = simplify_bool(cond_n);
6788        Some(match e2 {
6789            None => {
6790                if !base {
6791                    match e1c {
6792                        Some(true) => c,
6793                        Some(false) => return None, // no-op diamond: guard effects would drop
6794                        None => bin(BinOp::LogAnd, c, e1),
6795                    }
6796                } else {
6797                    match e1c {
6798                        Some(false) => not(c),
6799                        Some(true) => return None,
6800                        None => bin(BinOp::LogOr, not(c), e1),
6801                    }
6802                }
6803            }
6804            Some(e2) => {
6805                // Full diamond: both arms assign — the base is dead.
6806                if e1 == e2 {
6807                    return None; // both arms equal: folding drops guard effects
6808                }
6809                Expr::Cond { c: Box::new(c), t: Box::new(e1), f: Box::new(e2) }
6810            }
6811        })
6812    }
6813}
6814
6815pub fn idiom_compounds(body: &mut Stmt, vt: &VarTable) {
6816    walk_mut_deep(body, &mut |st| {
6817        let Stmt::ExprStmt(e) = st else { return };
6818        let Expr::Assign { target, op: AssignOp::Plain, value } = &mut *e else {
6819            return;
6820        };
6821        // Target: a local, or a bare field (static / this — no complex
6822        // receiver whose re-evaluation a compound would elide).
6823        enum Tgt {
6824            Loc(u32),
6825            Fld,
6826        }
6827        let tgt = match &**target {
6828            Expr::Local { var, .. } => Tgt::Loc(*var),
6829            Expr::Field { owner, .. }
6830                if owner.is_none()
6831                    || matches!(&**owner.as_ref().unwrap(), Expr::This) =>
6832            {
6833                Tgt::Fld
6834            }
6835            _ => return,
6836        };
6837        let Expr::Bin { op, l, r, .. } = &mut **value else {
6838            return;
6839        };
6840        // The left operand must be the SAME storage as the target.
6841        match (&tgt, &**l) {
6842            (Tgt::Loc(v), Expr::Local { var, .. }) if *var == *v => {}
6843            (Tgt::Fld, Expr::Field { cls: c2, name: n2, .. }) => {
6844                let Expr::Field { cls: c1, name: n1, .. } = &**target else {
6845                    return;
6846                };
6847                if c1 != c2 || n1 != n2 {
6848                    return;
6849                }
6850            }
6851            _ => return,
6852        }
6853        // ±1 constant on a numeric var → post-inc/dec.
6854        let one = match &**r {
6855            Expr::Const(ConstVal::Int(1)) => Some(1i64),
6856            Expr::Const(ConstVal::Int(-1)) => Some(-1),
6857            Expr::Const(ConstVal::Long(1)) => Some(1),
6858            Expr::Const(ConstVal::Long(-1)) => Some(-1),
6859            _ => None,
6860        };
6861        if *op == BinOp::Add || *op == BinOp::Sub {
6862            if let Some(mut d) = one {
6863                if *op == BinOp::Sub {
6864                    d = -d;
6865                }
6866                let tgt_ty = match &tgt {
6867                    Tgt::Loc(v) => vt.var(*v).ty.erased(),
6868                    Tgt::Fld => {
6869                        let Expr::Field { ty, .. } = &**target else {
6870                            return;
6871                        };
6872                        ty.erased()
6873                    }
6874                };
6875                let numeric = matches!(
6876                    tgt_ty,
6877                    JavaType::Int
6878                        | JavaType::Long
6879                        | JavaType::Short
6880                        | JavaType::Byte
6881                        | JavaType::Char
6882                        | JavaType::Float
6883                        | JavaType::Double
6884                );
6885                if numeric {
6886                    let wide = matches!(tgt_ty, JavaType::Long);
6887                    let inner = match &tgt {
6888                        Tgt::Loc(v) => Expr::Local {
6889                            var: *v,
6890                            ty: vt.var(*v).ty.clone(),
6891                        },
6892                        Tgt::Fld => (**target).clone(),
6893                    };
6894                    *e = Expr::PostIncDec {
6895                        e: Box::new(inner),
6896                        delta: d,
6897                        wide,
6898                    };
6899                    return;
6900                }
6901            }
6902        }
6903        let cop = match op {
6904            BinOp::Add => AssignOp::Add,
6905            BinOp::Sub => AssignOp::Sub,
6906            BinOp::Mul => AssignOp::Mul,
6907            BinOp::Div => AssignOp::Div,
6908            BinOp::Rem => AssignOp::Rem,
6909            BinOp::Shl => AssignOp::Shl,
6910            BinOp::Shr => AssignOp::Shr,
6911            BinOp::Ushr => AssignOp::Ushr,
6912            BinOp::And => AssignOp::And,
6913            BinOp::Or => AssignOp::Or,
6914            BinOp::Xor => AssignOp::Xor,
6915            _ => return,
6916        };
6917        let rhs = std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
6918        let new_target = match &tgt {
6919            Tgt::Loc(v) => Box::new(Expr::Local {
6920                var: *v,
6921                ty: vt.var(*v).ty.clone(),
6922            }),
6923            Tgt::Fld => target.clone(),
6924        };
6925        *e = Expr::Assign {
6926            target: new_target,
6927            op: cop,
6928            value: rhs,
6929        };
6930    });
6931}
6932
6933pub fn fix_bool_xor(body: &mut Stmt, vt: &VarTable, ret_bool: bool) {
6934    fn is_one(e: &Expr) -> bool {
6935        matches!(e, Expr::Const(ConstVal::Int(1)))
6936    }
6937    fn bty(e: &Expr, vt: &VarTable) -> bool {
6938        match e {
6939            Expr::Local { var, .. } => {
6940                matches!(vt.var(*var).ty.erased(), JavaType::Boolean)
6941            }
6942            // A bitwise bin over all-boolean sides IS boolean: the
6943            // embedded bin ty stays frozen Int from the or-int lift
6944            // (booleanize retypes the vt, not bin nodes), so the flat
6945            // lookup saw `(b1|b2) | i3` as int-sided and skipped the
6946            // mixed rewrite — rendered `boolean | int` (uuyc i5/a_3
6947            // `(v37|v38|v39)==0` ×21; news ×995 二元运算符 '|').
6948            // Mirrors emit.rs bool_ish_vt's recursion so the rewrite
6949            // here and the logical-form render there agree.
6950            Expr::Bin {
6951                op: BinOp::And | BinOp::Or | BinOp::Xor,
6952                l,
6953                r,
6954                ..
6955            } => bty(l, vt) && bty(r, vt),
6956            other => matches!(other.type_ref().erased(), JavaType::Boolean),
6957        }
6958    }
6959    // Int-typed `x ^ 1` in a BOOLEAN sink is the compiler's `!x` over a
6960    // 0/1 slot: rewrite to `x == 0` (the booleanize return-wrap misses
6961    // Xor shapes — weibo ArraysKt `return v2 ^ 1;` against a boolean
6962    // return, "int无法转换为boolean").
6963    fn inv_of_xor(x: &Expr, vt: &VarTable) -> Option<Expr> {
6964        if let Expr::Bin { op: BinOp::Xor, l, r, .. } = x {
6965            let (int_side, one_side) = if is_one(r) {
6966                (l, r)
6967            } else if is_one(l) {
6968                (r, l)
6969            } else {
6970                return None;
6971            };
6972            let _ = one_side;
6973            if !bty(int_side, vt) {
6974                return Some(Expr::Bin {
6975                    op: BinOp::Eq,
6976                    l: int_side.clone(),
6977                    r: Box::new(Expr::Const(ConstVal::Int(0))),
6978                    ty: Some(TypeRef::J(JavaType::Boolean)),
6979                });
6980            }
6981        }
6982        None
6983    }
6984    let tgt_bool = |target: &Expr, vt: &VarTable| -> bool {
6985        match target {
6986            Expr::Local { var, .. } => matches!(vt.var(*var).ty.erased(), JavaType::Boolean),
6987            Expr::Field { ty, .. } => matches!(ty.erased(), JavaType::Boolean),
6988            _ => false,
6989        }
6990    };
6991    walk_mut_deep(body, &mut |st| match st {
6992        Stmt::Return(Some(e)) if ret_bool => {
6993            if let Some(r) = inv_of_xor(e, vt) {
6994                *e = r;
6995            }
6996        }
6997        Stmt::ExprStmt(Expr::Assign { target, value, op: AssignOp::Plain, .. }) => {
6998            if tgt_bool(target, vt) {
6999                if let Some(r) = inv_of_xor(value, vt) {
7000                    **value = r;
7001                }
7002            }
7003        }
7004        Stmt::LocalDef { var, init: Some(value), .. }
7005            if matches!(vt.var(*var).ty.erased(), JavaType::Boolean) =>
7006        {
7007            if let Some(r) = inv_of_xor(value, vt) {
7008                *value = r;
7009            }
7010        }
7011        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
7012            if let Some(r) = inv_of_xor(cond, vt) {
7013                *cond = r;
7014            }
7015        }
7016        _ => {}
7017    });
7018    fn ity(e: &Expr, vt: &VarTable) -> bool {
7019        matches!(
7020            match e {
7021                Expr::Local { var, .. } => vt.var(*var).ty.erased(),
7022                other => other.type_ref().erased(),
7023            },
7024            JavaType::Int | JavaType::Short | JavaType::Byte | JavaType::Char
7025        )
7026    }
7027    /// Mixed-kind bitwise rewrite: Kotlin's non-short-circuit
7028    /// `or`/`and` over 0/1 ints mixes generations once booleanize
7029    /// converts one side (`delete | delete2` — "boolean无法转换为int" at
7030    /// the OPERAND, weixin SQLiteDatabase ×3.3k), and `b ^ 1` is `!b`.
7031    /// The int side is a 0/1 encoding: compare it, keeping the chain
7032    /// boolean end to end. ONLY legal into a BOOLEAN sink — ungated, it
7033    /// rewrote int-sink `v6 = v5x | 4` into `v5x | 4 != 0` assigned to
7034    /// an int var (boolean无法转换为int, weixin yq5/c ×2.1k family).
7035    fn mixed_rewrite(e: &mut Expr, vt: &VarTable) {
7036        deep_rewrite(e, &mut |x| {
7037            if let Expr::Bin { op, l, r, ty } = x {
7038                match op {
7039                    BinOp::Xor => {
7040                        if is_one(r) && bty(l, vt) {
7041                            let taken =
7042                                std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
7043                            *x = Expr::Un { op: UnOp::Not, e: taken };
7044                        } else if is_one(l) && bty(r, vt) {
7045                            let taken =
7046                                std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
7047                            *x = Expr::Un { op: UnOp::Not, e: taken };
7048                        }
7049                    }
7050                    BinOp::Or | BinOp::And => {
7051                        if bty(l, vt) && ity(r, vt) {
7052                            let taken =
7053                                std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
7054                            **r = Expr::Bin {
7055                                op: BinOp::Ne,
7056                                l: taken,
7057                                r: Box::new(Expr::Const(ConstVal::Int(0))),
7058                                ty: Some(TypeRef::J(JavaType::Boolean)),
7059                            };
7060                            *ty = Some(TypeRef::J(JavaType::Boolean));
7061                        } else if ity(l, vt) && bty(r, vt) {
7062                            let taken =
7063                                std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
7064                            **l = Expr::Bin {
7065                                op: BinOp::Ne,
7066                                l: taken,
7067                                r: Box::new(Expr::Const(ConstVal::Int(0))),
7068                                ty: Some(TypeRef::J(JavaType::Boolean)),
7069                            };
7070                            *ty = Some(TypeRef::J(JavaType::Boolean));
7071                        }
7072                    }
7073                    _ => {}
7074                }
7075            }
7076        });
7077    }
7078    // Boolean sinks: bool-typed assign/def targets, boolean-method
7079    // returns, and loop/branch conditions.
7080    walk_mut_deep(body, &mut |st| match st {
7081        Stmt::Return(Some(e)) if ret_bool => mixed_rewrite(e, vt),
7082        Stmt::ExprStmt(Expr::Assign { target, value, op: AssignOp::Plain, .. }) => {
7083            if tgt_bool(target, vt) {
7084                mixed_rewrite(value, vt);
7085            }
7086        }
7087        Stmt::LocalDef { var, init: Some(e), .. }
7088            if matches!(vt.var(*var).ty.erased(), JavaType::Boolean) =>
7089        {
7090            mixed_rewrite(e, vt);
7091        }
7092        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
7093            mixed_rewrite(cond, vt);
7094        }
7095        _ => {}
7096    });
7097    // A conditional's test is a boolean sink wherever it sits (including
7098    // inside int-sink statements the walk above skips).
7099    walk_stmt_exprs(body, &mut |e| {
7100        deep_rewrite(e, &mut |x| {
7101            if let Expr::Cond { c, .. } = x {
7102                mixed_rewrite(c, vt);
7103            }
7104        });
7105    });
7106}
7107
7108/// Boolean inference: vars only ever assigned 0/1/comparisons/booleans and
7109/// read in conditions become `boolean`, with `v != 0` → `v` in conditions.
7110/// Returns the number of vars converted (0 = nothing changed — the
7111/// caller skips the post-booleanize re-split, which only has work when
7112/// a conversion exposed a mixed-kind register).
7113pub fn booleanize(vt: &mut VarTable, body: &mut Stmt, ret_bool: bool) -> usize {
7114    // Booleans propagate through local chains (`v17 = v24` where v24
7115    // itself became boolean in the first round) — iterate to a fixpoint;
7116    // the common corpus converts nothing and exits after one round.
7117    // The deref disqualifier set is structural (positions, not types) —
7118    // compute ONCE per call, not per round (12 full walks on big
7119    // methods cost weibo +60% wall).
7120    let n0 = vt.vars.len();
7121    let mut derefed = vec![false; n0];
7122    mark_derefs(body, &mut derefed);
7123    let mut total = 0usize;
7124    for _ in 0..4 {
7125        let n = booleanize_round(vt, body, ret_bool, &derefed);
7126        if n == 0 {
7127            break;
7128        }
7129        total += n;
7130    }
7131    total
7132}
7133
7134/// Mark vars used in DEREF positions (field/method receiver, array
7135/// base) — allocation-free recursion (visit_all_exprs allocates a
7136/// child Vec per statement; two callers run this per method).
7137fn mark_deref_expr(e: &Expr, derefed: &mut [bool]) {
7138    match e {
7139        Expr::Field { owner: Some(o), is_static: false, .. }
7140        | Expr::Method { owner: Some(o), .. } => {
7141            if let Expr::Local { var, .. } = &**o {
7142                if (*var as usize) < derefed.len() {
7143                    derefed[*var as usize] = true;
7144                }
7145            }
7146        }
7147        Expr::ArrayIndex { array, .. } => {
7148            if let Expr::Local { var, .. } = &**array {
7149                if (*var as usize) < derefed.len() {
7150                    derefed[*var as usize] = true;
7151                }
7152            }
7153        }
7154        _ => {}
7155    }
7156    for_each_child(e, &mut |c| mark_deref_expr(c, derefed));
7157}
7158
7159fn mark_derefs(body: &Stmt, derefed: &mut [bool]) {
7160    walk_all(body, &mut |st| {
7161        match st {
7162            Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
7163                mark_deref_expr(e, derefed);
7164            }
7165            Stmt::Return(Some(e)) => mark_deref_expr(e, derefed),
7166            Stmt::LocalDef { init: Some(e), .. } => mark_deref_expr(e, derefed),
7167            Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
7168                mark_deref_expr(cond, derefed);
7169            }
7170            Stmt::For { init, .. } => {
7171                for x in init {
7172                    mark_derefs(x, derefed);
7173                }
7174            }
7175            Stmt::Switch { selector, .. } => mark_deref_expr(selector, derefed),
7176            Stmt::ForEach { iterable, .. } => mark_deref_expr(iterable, derefed),
7177            _ => {}
7178        }
7179    });
7180}
7181
7182fn booleanize_round(
7183    vt: &mut VarTable,
7184    body: &mut Stmt,
7185    ret_bool: bool,
7186    derefed: &[bool],
7187) -> usize {
7188    let n = vt.vars.len();
7189    let mut in_cond = vec![false; n];
7190
7191    // Condition uses: `v == 0` / `v != 0` if-conditions and comparison
7192    // operands inside assigned values.
7193    walk_all(body, &mut |st| {
7194        let cond: Option<&Expr> = match st {
7195            Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
7196                Some(cond)
7197            }
7198            _ => None,
7199        };
7200        if let Some(c) = cond {
7201            if let Expr::Bin {
7202                op: BinOp::Eq | BinOp::Ne,
7203                l,
7204                r,
7205                ..
7206            } = c
7207            {
7208                for (x, other) in [(l, r), (r, l)] {
7209                    if let (Expr::Local { var, .. }, Expr::Const(ConstVal::Int(0))) =
7210                        (&**x, &**other)
7211                    {
7212                        if (*var as usize) < n {
7213                            in_cond[*var as usize] = true;
7214                        }
7215                    }
7216                }
7217            }
7218        }
7219        // A local RETURNED from a boolean method is boolean-typed even
7220        // though it never appears in a condition (`int v9 = 0/1 … return
7221        // v9;` — returning an int from `boolean check()` is a compile
7222        // error; lab package Obf.check).
7223        if ret_bool {
7224            if let Stmt::Return(Some(Expr::Local { var, .. })) = st {
7225                if (*var as usize) < n {
7226                    in_cond[*var as usize] = true;
7227                }
7228            }
7229        }
7230        // A local STORED into a boolean field is boolean-typed even though
7231        // it never appears in a condition (`b = v1` where `b` is a boolean
7232        // field, `v1` an int 0/1 local — q.java static init). The emit
7233        // layer already coerces a boolean-target assign through expr_bool,
7234        // but that renders a bare int LOCAL unchanged, so the local itself
7235        // must be booleanized (its 0/1 assigns then print false/true).
7236        if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = st {
7237            if let Expr::Field { ty, .. } = &**target {
7238                if ty.erased() == JavaType::Boolean {
7239                    if let Expr::Local { var, .. } = &**value {
7240                        if (*var as usize) < n {
7241                            in_cond[*var as usize] = true;
7242                        }
7243                    }
7244                }
7245            }
7246        }
7247        let val: Option<&Expr> = match st {
7248            Stmt::ExprStmt(Expr::Assign { value, .. }) => Some(value),
7249            // Bare expression statements (e.g. `q(v117);`) so a local
7250            // passed to a boolean parameter is caught below.
7251            Stmt::ExprStmt(e) => Some(e),
7252            Stmt::LocalDef { init: Some(e), .. } => Some(e),
7253            Stmt::Return(Some(e)) => Some(e),
7254            _ => None,
7255        };
7256        if let Some(v) = val {
7257            visit_exprs(v, &mut |x| {
7258                // A local passed as a BOOLEAN parameter is boolean-typed
7259                // (`q(v117)` where q's param is boolean, v117 an int 0/1 —
7260                // f8/b.java). The DEX passes booleans as 0/1 ints, so a
7261                // boolean param slot is authoritative for the argument's
7262                // type.
7263                if let Expr::Method { desc, args, .. } = x {
7264                    for (i, a) in args.iter().enumerate() {
7265                        if desc.args.get(i) == Some(&JavaType::Boolean) {
7266                            if let Expr::Local { var, .. } = a {
7267                                if (*var as usize) < n {
7268                                    in_cond[*var as usize] = true;
7269                                }
7270                            }
7271                        }
7272                    }
7273                }
7274                if let Expr::Bin { op, l, r, .. } = x {
7275                    if matches!(
7276                        op,
7277                        BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
7278                    ) {
7279                        for side in [l, r] {
7280                            if let Expr::Local { var, .. } = &**side {
7281                                if (*var as usize) < n {
7282                                    in_cond[*var as usize] = true;
7283                                }
7284                            }
7285                        }
7286                    }
7287                    // Kotlin/d8 merge booleans through INT bitwise ops
7288                    // (`v3 | obj instanceof g`) == 0 — the other side's
7289                    // static type being boolean makes this a boolean
7290                    // context for the local (a genuine int bitwise
7291                    // expression never has a boolean operand).
7292                    if matches!(op, BinOp::Or | BinOp::And) {
7293                        let l_bool = l.type_ref().erased() == JavaType::Boolean;
7294                        let r_bool = r.type_ref().erased() == JavaType::Boolean;
7295                        if l_bool != r_bool {
7296                            let side = if l_bool { r } else { l };
7297                            if let Expr::Local { var, .. } = &**side {
7298                                if (*var as usize) < n {
7299                                    in_cond[*var as usize] = true;
7300                                }
7301                            }
7302                        }
7303                    }
7304                }
7305            });
7306        }
7307    });
7308
7309    // A var whose every assignment value is boolean-shaped AND which is
7310    // referenced in conditions becomes boolean.
7311    // (Perf: this used to re-walk the whole statement tree ONCE PER VAR —
7312    // O(vars × stmts); on R8-merged monsters (2000+ vars, 15k statements)
7313    // that was tens of millions of visits. One pass collects the same
7314    // (any, all_bool) facts for every var.)
7315    let mut assigned_any = vec![false; n];
7316    let mut all_bool = vec![true; n];
7317    let mut edges: Vec<(usize, usize)> = Vec::new();
7318    walk_all(body, &mut |st| {
7319        let (var, value) = match st {
7320            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => match &**target {
7321                Expr::Local { var, .. } => (var, &**value),
7322                _ => return,
7323            },
7324            Stmt::LocalDef {
7325                var, init: Some(e), ..
7326            } => (var, e),
7327            _ => return,
7328        };
7329        let i = *var as usize;
7330        if i < n {
7331            assigned_any[i] = true;
7332            if !is_boolean_valued(value, vt) {
7333                all_bool[i] = false;
7334            }
7335            // Chain edges for the context closure below: `v17 = v24`
7336            // links the two locals; when the TARGET is in a boolean
7337            // context, the source inherits it (its only reader is the
7338            // boolean-shaped chain). Collected as edges because the
7339            // target's context can be discovered anywhere in the tree.
7340            if let Expr::Local { var: src, .. } = value {
7341                if (*src as usize) < n {
7342                    edges.push((i, *src as usize));
7343                }
7344            }
7345        }
7346    });
7347    // Context closure along local chains: a var in a boolean context
7348    // pushes that context through every local it is assigned from
7349    // (`return v17` — v17 = v24 — v24 = 0/1) — the statement order
7350    // cannot be relied on (the return may follow the assignment).
7351    loop {
7352        let mut changed = false;
7353        for &(tgt, src) in &edges {
7354            if in_cond[tgt] && !in_cond[src] && all_bool[src] {
7355                in_cond[src] = true;
7356                changed = true;
7357            }
7358            // FORWARD through pure copies: `v21 = v17` with v17 boolean
7359            // makes v21 boolean when ALL of v21's assignments are
7360            // bool-shaped (the loop-state save pattern — weixin u2/f's
7361            // v62 → v17 → v21 chains left the copy targets int while
7362            // the accumulator booleanized).
7363            if in_cond[src] && !in_cond[tgt] && all_bool[tgt] {
7364                in_cond[tgt] = true;
7365                changed = true;
7366            }
7367        }
7368        if !changed {
7369            break;
7370        }
7371    }
7372    let reads = count_locals_stmts(std::slice::from_ref(body));
7373    let mut boolean_vars: HashSet<u32> = HashSet::default();
7374    for i in 0..n {
7375        if vt.vars[i].is_param {
7376            continue;
7377        }
7378        if derefed[i] {
7379            continue;
7380        }
7381        if !in_cond[i] && !(reads.get(&(i as u32)).copied().unwrap_or(0) == 0) {
7382            continue;
7383        }
7384        if assigned_any[i] && all_bool[i] {
7385            boolean_vars.insert(i as u32);
7386        }
7387    }
7388    if boolean_vars.is_empty() {
7389        return 0;
7390    }
7391    for v in &boolean_vars {
7392        vt.vars[*v as usize].ty = TypeRef::J(JavaType::Boolean);
7393    }
7394    let types: Vec<&TypeRef> = vt.vars.iter().map(|v| &v.ty).collect();
7395    rewrite_exprs(body, &mut |e| {
7396        deep_rewrite(e, &mut |x| {
7397            if let Expr::Local { var, ty } = x {
7398                if let Some(want) = types.get(*var as usize) {
7399                    if ty != *want {
7400                        *ty = (*want).clone();
7401                    }
7402                }
7403            }
7404        });
7405    });
7406
7407    // Condition folding: `b != 0` → `b`, `b == 0` → `!b` (boolean vars).
7408    fold_bool_conditions(body, &boolean_vars);
7409    boolean_vars.len()
7410}
7411
7412fn is_boolean_valued(e: &Expr, vt: &VarTable) -> bool {
7413    match e {
7414        Expr::Const(ConstVal::Int(0)) | Expr::Const(ConstVal::Int(1)) => true,
7415        // Kotlin's non-short-circuit boolean `or`/`and` compiles to `|`/`&`
7416        // over boolean locals (`v15 = delete | delete2 | ..`). EITHER side
7417        // boolean suffices: `int | boolean` is illegal in source — it is
7418        // always a not-yet-booleanized side of a boolean chain (the
7419        // loop-carried accumulator `v15 = v20; v20 = v15 | delete3` can
7420        // never seed its all_bool fixpoint otherwise).
7421        Expr::Bin { op: BinOp::Or | BinOp::And, l, r, .. } => {
7422            is_boolean_valued(l, vt) || is_boolean_valued(r, vt)
7423        }
7424        // Kotlin's `!x` lowers to `x ^ 1`; at booleanize time the value
7425        // is still the Xor (fix_bool_xor runs later), so an int-typed
7426        // generation receiving it never retyped (weixin v2/i's
7427        // `int v264_g160_g7 = !v200`). BOTH sides must be bool-shaped:
7428        // `bool ^ 1` qualifies (Const 0/1 is boolean-valued above),
7429        // a genuine int `flags ^ 1` does not (flags stays Int in vt).
7430        // MUST precede the generic Bin arm (which would swallow Xor).
7431        Expr::Bin {
7432            op: BinOp::Xor,
7433            l,
7434            r,
7435            ..
7436        } => is_boolean_valued(l, vt) && is_boolean_valued(r, vt),
7437        Expr::Bin { op, .. } => matches!(
7438            op,
7439            BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
7440        ),
7441        Expr::InstanceOf { .. } => true,
7442        // The VarTable is the authority: the embedded Local ty is a
7443        // lift-time snapshot and goes STALE across booleanize rounds —
7444        // a converted copy source kept its partners int (`v150 = v131`
7445        // boolean→int, weixin ConstraintLayout ×1.4k files) because the
7446        // all_bool fixpoint consulted the stale Int.
7447        Expr::Local { var, ty } => {
7448            let t = if (*var as usize) < vt.vars.len() {
7449                vt.var(*var).ty.erased()
7450            } else {
7451                ty.erased()
7452            };
7453            t == JavaType::Boolean
7454        }
7455        Expr::Method { desc, .. } => desc.ret == JavaType::Boolean,
7456        Expr::Cond { t, f, .. } => is_boolean_valued(t, vt) && is_boolean_valued(f, vt),
7457        Expr::Un { op: UnOp::Not, .. } => true,
7458        // Field ty comes from the dex descriptor — authoritative, not a
7459        // lift snapshot (`!w1x.i` arrives here still as `w1x.i ^ 1`; the
7460        // Xor arm needs this side bool-shaped).
7461        Expr::Field { ty, .. } => ty.erased() == JavaType::Boolean,
7462        _ => false,
7463    }
7464}
7465
7466fn fold_bool_conditions(s: &mut Stmt, bv: &HashSet<u32>) {
7467    let fold = |e: &mut Expr| {
7468        deep_rewrite(e, &mut |x| {
7469            if let Expr::Bin { op, l, r, .. } = x {
7470                let (var_side, const_side) = match (&**l, &**r) {
7471                    (Expr::Local { .. }, Expr::Const(ConstVal::Int(_))) => (l, 0),
7472                    (Expr::Const(ConstVal::Int(_)), Expr::Local { .. }) => (r, 1),
7473                    _ => return,
7474                };
7475                let var = match &**var_side {
7476                    Expr::Local { var, .. } => *var,
7477                    _ => return,
7478                };
7479                if !bv.contains(&var) {
7480                    return;
7481                }
7482                let _ = const_side;
7483                match op {
7484                    BinOp::Ne => {
7485                        let inner = var_side.clone();
7486                        *x = *inner;
7487                    }
7488                    BinOp::Eq => {
7489                        let inner = var_side.clone();
7490                        *x = Expr::Un {
7491                            op: UnOp::Not,
7492                            e: Box::new(*inner),
7493                        };
7494                    }
7495                    _ => {}
7496                }
7497            }
7498        });
7499    };
7500    match s {
7501        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => fold(cond),
7502        _ => {}
7503    }
7504    walk_mut_deep(s, &mut |st| {
7505        let cond = match st {
7506            Stmt::If { cond, .. } => Some(cond),
7507            Stmt::While { cond, .. } => Some(cond),
7508            Stmt::DoWhile { cond, .. } => Some(cond),
7509            _ => None,
7510        };
7511        if let Some(c) = cond {
7512            fold(c);
7513        }
7514    });
7515}
7516
7517/// Normalize `1 == v` comparisons to `v == 1` (d8 emits const-first if-eq).
7518pub fn flip_const_compares(s: &mut Stmt) {
7519    rewrite_exprs(s, &mut |e| {
7520        deep_rewrite(e, &mut |x| {
7521            if let Expr::Bin { op, l, r, .. } = x {
7522                if matches!(
7523                    op,
7524                    BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
7525                ) {
7526                    let l_const = matches!(&**l, Expr::Const(_));
7527                    let r_var = matches!(&**r, Expr::Local { .. });
7528                    if l_const && r_var {
7529                        std::mem::swap(l, r);
7530                    }
7531                }
7532            }
7533        });
7534    });
7535}
7536
7537/// Object-typed locals compared against 0 compare against `null`.
7538pub fn null_compares(vt: &VarTable, body: &mut Stmt) {
7539    let obj_vars: HashSet<u32> = vt
7540        .vars
7541        .iter()
7542        .filter(|v| v.ty.erased().is_reference())
7543        .map(|v| v.id)
7544        .collect();
7545    rewrite_exprs(body, &mut |e| {
7546        deep_rewrite(e, &mut |x| {
7547            null_side_rewrite(x, &|v| obj_vars.contains(&v));
7548        });
7549    });
7550}
7551
7552/// Drop assignments/declarations of locals that are NEVER read anywhere
7553/// in the method. Register-machine merges commit values no Java-level
7554/// code consumes (phi residue like `printStream = check;` — not just
7555/// noise: the merge variable's inferred type need not match the value,
7556/// so these lines are often type errors as well). Side-effect-free
7557/// values drop entirely; an impure value survives as a bare expression
7558/// statement. Iterated to a fixpoint: dropping `a = b` can make `b`
7559/// unread.
7560pub fn drop_dead_locals(body: &mut Stmt) {
7561    loop {
7562        let mut reads: Vec<usize> = Vec::new();
7563        count_reads(body, &mut reads);
7564        let mut dropped = 0usize;
7565        walk_mut_deep(body, &mut |st| match st {
7566            Stmt::Block(items) => prune_dead_items(items, &reads, &mut dropped),
7567            // Switch case bodies and For inits are bare `Vec<Stmt>`, NOT
7568            // wrapped in a `Stmt::Block`, so the Block arm never sees them.
7569            // Dead phi commits land directly in case bodies (`sb4 =
7570            // compareTo;` per case): pruning only Blocks dropped the
7571            // commit's reader (in a real post-switch Block) but left the
7572            // now-dead commits, stalling the fixpoint cascade and leaking
7573            // mistyped phi residue. TryWithResources.resources is
7574            // deliberately excluded — its LocalDefs carry auto-close
7575            // semantics that a bare-expression rewrite would break.
7576            Stmt::Switch { cases, .. } => {
7577                for c in cases {
7578                    prune_dead_items(&mut c.body, &reads, &mut dropped);
7579                }
7580            }
7581            Stmt::For { init, .. } => prune_dead_items(init, &reads, &mut dropped),
7582            _ => {}
7583        });
7584        if dropped == 0 {
7585            break;
7586        }
7587    }
7588}
7589
7590/// Remove dead assignments/declarations from one bare statement vector.
7591/// Shared by `drop_dead_locals` across every prunable `Vec<Stmt>`
7592/// container. Side-effect-free values drop entirely; an impure value
7593/// survives as a bare expression statement.
7594fn prune_dead_items(items: &mut Vec<Stmt>, reads: &[usize], dropped: &mut usize) {
7595    let dead = |v: u32| reads.get(v as usize).copied().unwrap_or(0) == 0;
7596    items.retain_mut(|x| match x {
7597        Stmt::ExprStmt(Expr::Assign { target, value, op, .. }) => {
7598            let dead_target = matches!(&**target, Expr::Local { var, .. } if dead(*var));
7599            if dead_target && matches!(*op, AssignOp::Plain) {
7600                *dropped += 1;
7601                if has_side_effects(value) {
7602                    let e = std::mem::replace(value, Box::new(Expr::This));
7603                    *x = Stmt::ExprStmt(*e);
7604                    true
7605                } else {
7606                    false
7607                }
7608            } else {
7609                true
7610            }
7611        }
7612        Stmt::LocalDef { var, init, .. } if dead(*var) => {
7613            *dropped += 1;
7614            if let Some(e) = init.take() {
7615                if has_side_effects(&e) {
7616                    *x = Stmt::ExprStmt(e);
7617                    return true;
7618                }
7619            }
7620            false
7621        }
7622        _ => true,
7623    });
7624}
7625
7626
7627/// Vars occurring in this statement's OWN expression payloads (nested
7628/// sub-statements are descended separately so every occurrence carries
7629/// its innermost block id). ForEach/Catch bound vars are declarations,
7630/// not occurrences.
7631fn stmt_shallow_vars<F: FnMut(u32)>(st: &Stmt, out: &mut F) {
7632    fn ex<F: FnMut(u32)>(e: &Expr, out: &mut F) {
7633        visit_exprs(e, &mut |x| {
7634            if let Expr::Local { var, .. } = x {
7635                out(*var);
7636            }
7637        });
7638    }
7639    match st {
7640        Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
7641            ex(e, out)
7642        }
7643        Stmt::TernaryValue { e } => ex(e, out),
7644        Stmt::Return(Some(e)) => ex(e, out),
7645        Stmt::LocalDef { var, init, .. } => {
7646            out(*var);
7647            if let Some(e) = init {
7648                ex(e, out);
7649            }
7650        }
7651        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
7652            ex(cond, out)
7653        }
7654        Stmt::For { cond, update, .. } => {
7655            if let Some(c) = cond {
7656                ex(c, out);
7657            }
7658            for u in update {
7659                ex(u, out);
7660            }
7661        }
7662        Stmt::ForEach { iterable, .. } => ex(iterable, out),
7663        Stmt::Switch { selector, cases, .. } => {
7664            ex(selector, out);
7665            for cg in cases {
7666                if let Some(g) = &cg.guard {
7667                    ex(g, out);
7668                }
7669            }
7670        }
7671        Stmt::Assert { cond, msg } => {
7672            ex(cond, out);
7673            if let Some(m) = msg {
7674                ex(m, out);
7675            }
7676        }
7677        Stmt::Synchronized { lock, .. } => ex(lock, out),
7678        _ => {}
7679    }
7680}
7681
7682/// Pass A: number every Block in pre-order (root = 0) and record each
7683/// var's defining block. `sizes[id]` = the id-range span of the subtree.
7684/// Pass A: number every EMITTER-BRACED position in pre-order (root = 0)
7685/// and record each var's defining block. `sizes[id]` = the id-range span
7686/// of the subtree. A braced position is a Java scope whether or not the
7687/// IR node is a Stmt::Block — switch case bodies are bare Vecs and
7688/// single-statement if arms are bare statements, and sharing the parent's
7689/// id for them hid the scope violation (Telegram jk.java: a case-local
7690/// `notificationCenterDelegate`/`d0x` read after the switch never
7691/// hoisted — 找不到符号 ×76 in one file; the `d9x.L` reads even resolved
7692/// as a PACKAGE — 程序包不存在 ×92 tree-wide).
7693fn scope_pass_a(st: &Stmt, cur: u32, next: &mut u32, def_block: &mut [u32], sizes: &mut Vec<u32>) {
7694    if let Stmt::LocalDef { var, .. } = st {
7695        let i = *var as usize;
7696        if i < def_block.len() && def_block[i] == u32::MAX {
7697            def_block[i] = cur;
7698        }
7699    }
7700    fn scoped(c: &Stmt, next: &mut u32, def_block: &mut [u32], sizes: &mut Vec<u32>) {
7701        let id = *next;
7702        *next += 1;
7703        sizes.push(0);
7704        scope_pass_a(c, id, next, def_block, sizes);
7705        sizes[id as usize] = *next - id;
7706    }
7707    match st {
7708        Stmt::Block(v) => {
7709            for x in v {
7710                scope_pass_a(x, cur, next, def_block, sizes);
7711            }
7712        }
7713        Stmt::If {
7714            then_stmt,
7715            else_stmt,
7716            ..
7717        } => {
7718            scoped(then_stmt, next, def_block, sizes);
7719            if let Some(e) = else_stmt {
7720                scoped(e, next, def_block, sizes);
7721            }
7722        }
7723        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => {
7724            scoped(body, next, def_block, sizes)
7725        }
7726        Stmt::For { init, body, .. } => {
7727            // A for-init declaration scopes over the WHOLE for statement
7728            // (header + body): one shared id, the body a nested one.
7729            let id = *next;
7730            *next += 1;
7731            sizes.push(0);
7732            for x in init {
7733                scope_pass_a(x, id, next, def_block, sizes);
7734            }
7735            scoped(body, next, def_block, sizes);
7736            sizes[id as usize] = *next - id;
7737        }
7738        Stmt::ForEach { body, .. } => scoped(body, next, def_block, sizes),
7739        Stmt::Switch { cases, default, .. } => {
7740            for c in cases {
7741                // One scope per case group (the emitter braces each);
7742                // non-Block statements share the case id, nested Blocks
7743                // get sub-ids — siblings in one brace group must share,
7744                // or a def's later same-case reads fall "outside".
7745                let id = *next;
7746                *next += 1;
7747                sizes.push(0);
7748                for x in &c.body {
7749                    if matches!(x, Stmt::Block(_)) {
7750                        scoped(x, next, def_block, sizes);
7751                    } else {
7752                        scope_pass_a(x, id, next, def_block, sizes);
7753                    }
7754                }
7755                sizes[id as usize] = *next - id;
7756            }
7757            if let Some(d) = default {
7758                scoped(d, next, def_block, sizes);
7759            }
7760        }
7761        Stmt::Try {
7762            body,
7763            catches,
7764            finally,
7765        } => {
7766            scoped(body, next, def_block, sizes);
7767            for c in catches {
7768                scoped(&c.body, next, def_block, sizes);
7769            }
7770            if let Some(f) = finally {
7771                scoped(f, next, def_block, sizes);
7772            }
7773        }
7774        Stmt::TryWithResources {
7775            resources,
7776            body,
7777            catches,
7778            finally,
7779        } => {
7780            // Resource declarations scope over the WHOLE try statement.
7781            let id = *next;
7782            *next += 1;
7783            sizes.push(0);
7784            for r in resources {
7785                scope_pass_a(r, id, next, def_block, sizes);
7786            }
7787            scoped(body, next, def_block, sizes);
7788            for c in catches {
7789                scoped(&c.body, next, def_block, sizes);
7790            }
7791            if let Some(f) = finally {
7792                scoped(f, next, def_block, sizes);
7793            }
7794            sizes[id as usize] = *next - id;
7795        }
7796        Stmt::Synchronized { body, .. } => scoped(body, next, def_block, sizes),
7797        Stmt::Labeled { body, .. } => scope_pass_a(body, cur, next, def_block, sizes),
7798        _ => {}
7799    }
7800}
7801
7802/// Pass B (same numbering walk): count each var's occurrences overall
7803/// and within its defining block's subtree. For/TWR take dedicated arms
7804/// so their header expressions count INSIDE the statement scope (a
7805/// normal `for (int i…)` must not look like an outside read of i).
7806fn scope_pass_b(
7807    st: &Stmt,
7808    cur: u32,
7809    next: &mut u32,
7810    def_block: &[u32],
7811    sizes: &[u32],
7812    within: &mut [u32],
7813    total: &mut [u32],
7814) {
7815    fn cnt(
7816        v: u32,
7817        at: u32,
7818        def_block: &[u32],
7819        sizes: &[u32],
7820        within: &mut [u32],
7821        total: &mut [u32],
7822    ) {
7823        let i = v as usize;
7824        if i < total.len() {
7825            total[i] += 1;
7826            let d = def_block[i];
7827            if d != u32::MAX && d <= at && at < d + sizes[d as usize] {
7828                within[i] += 1;
7829            }
7830        }
7831    }
7832    fn scoped(
7833        c: &Stmt,
7834        next: &mut u32,
7835        def_block: &[u32],
7836        sizes: &[u32],
7837        within: &mut [u32],
7838        total: &mut [u32],
7839    ) {
7840        let id = *next;
7841        *next += 1;
7842        scope_pass_b(c, id, next, def_block, sizes, within, total);
7843    }
7844    fn ex_at(
7845        e: &Expr,
7846        at: u32,
7847        def_block: &[u32],
7848        sizes: &[u32],
7849        within: &mut [u32],
7850        total: &mut [u32],
7851    ) {
7852        visit_exprs(e, &mut |x| {
7853            if let Expr::Local { var, .. } = x {
7854                cnt(*var, at, def_block, sizes, within, total);
7855            }
7856        });
7857    }
7858    match st {
7859        Stmt::For {
7860            init,
7861            cond,
7862            update,
7863            body,
7864        } => {
7865            let id = *next;
7866            *next += 1;
7867            for x in init {
7868                scope_pass_b(x, id, next, def_block, sizes, within, total);
7869            }
7870            if let Some(c) = cond {
7871                ex_at(c, id, def_block, sizes, within, total);
7872            }
7873            for u in update {
7874                ex_at(u, id, def_block, sizes, within, total);
7875            }
7876            scoped(body, next, def_block, sizes, within, total);
7877        }
7878        Stmt::TryWithResources {
7879            resources,
7880            body,
7881            catches,
7882            finally,
7883        } => {
7884            let id = *next;
7885            *next += 1;
7886            for r in resources {
7887                scope_pass_b(r, id, next, def_block, sizes, within, total);
7888            }
7889            scoped(body, next, def_block, sizes, within, total);
7890            for c in catches {
7891                scoped(&c.body, next, def_block, sizes, within, total);
7892            }
7893            if let Some(f) = finally {
7894                scoped(f, next, def_block, sizes, within, total);
7895            }
7896        }
7897        _ => {
7898            stmt_shallow_vars(st, &mut |v| cnt(v, cur, def_block, sizes, within, total));
7899            match st {
7900                Stmt::Block(v) => {
7901                    for x in v {
7902                        scope_pass_b(x, cur, next, def_block, sizes, within, total);
7903                    }
7904                }
7905                Stmt::If {
7906                    then_stmt,
7907                    else_stmt,
7908                    ..
7909                } => {
7910                    scoped(then_stmt, next, def_block, sizes, within, total);
7911                    if let Some(e) = else_stmt {
7912                        scoped(e, next, def_block, sizes, within, total);
7913                    }
7914                }
7915                Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => {
7916                    scoped(body, next, def_block, sizes, within, total)
7917                }
7918                Stmt::ForEach { body, .. } => {
7919                    scoped(body, next, def_block, sizes, within, total)
7920                }
7921                Stmt::Switch { cases, default, .. } => {
7922                    for c in cases {
7923                        let id = *next;
7924                        *next += 1;
7925                        for x in &c.body {
7926                            if matches!(x, Stmt::Block(_)) {
7927                                scoped(x, next, def_block, sizes, within, total);
7928                            } else {
7929                                scope_pass_b(x, id, next, def_block, sizes, within, total);
7930                            }
7931                        }
7932                    }
7933                    if let Some(d) = default {
7934                        scoped(d, next, def_block, sizes, within, total);
7935                    }
7936                }
7937                Stmt::Try {
7938                    body,
7939                    catches,
7940                    finally,
7941                } => {
7942                    scoped(body, next, def_block, sizes, within, total);
7943                    for c in catches {
7944                        scoped(&c.body, next, def_block, sizes, within, total);
7945                    }
7946                    if let Some(f) = finally {
7947                        scoped(f, next, def_block, sizes, within, total);
7948                    }
7949                }
7950                Stmt::Synchronized { body, .. } => {
7951                    scoped(body, next, def_block, sizes, within, total)
7952                }
7953                Stmt::Labeled { body, .. } => {
7954                    scope_pass_b(body, cur, next, def_block, sizes, within, total)
7955                }
7956                _ => {}
7957            }
7958        }
7959    }
7960}
7961
7962/// Declaration hygiene: every used var that has no LocalDef gets a bare
7963/// declaration at the top; duplicate LocalDefs demote to assignments.
7964/// In an enum's `<clinit>`, every constant's assignment
7965/// (`Self.FIELD = new Self("NAME", ordinal, ...)`) is compiler-mandated
7966/// boilerplate — when the class renders as a true `enum` declaration the
7967/// constants live in the header and these assignments must go. Only
7968/// ACC_ENUM-flagged static fields of the class itself are touched; the
7969/// `$VALUES` array assignment stays (a static block in an enum is legal).
7970pub fn strip_enum_const_inits(s: &mut Stmt, class: &crate::PoolClass) {
7971    let const_fields: jdc_core::FxHashSet<&str> = class
7972        .static_fields
7973        .iter()
7974        .filter(|f| f.access & crate::access::ACC_ENUM != 0)
7975        .map(|f| f.name.as_str())
7976        .collect();
7977    if const_fields.is_empty() {
7978        return;
7979    }
7980    if let Stmt::Block(stmts) = s {
7981        stmts.retain(|st| {
7982            !matches!(
7983                st,
7984                Stmt::ExprStmt(Expr::Assign { target, value, .. })
7985                    if matches!(&**target,
7986                        Expr::Field { cls, name, is_static: true, .. }
7987                            if cls.as_ref() == class.name
7988                                && const_fields.contains(&**name))
7989                        && matches!(&**value, Expr::New { cls: ncls, .. } if ncls.as_ref() == class.name)
7990            )
7991        });
7992    }
7993}
7994
7995/// Java requires the `super(...)`/`this(...)` delegation to be the FIRST
7996/// statement of a constructor. d8/R8 order the outer-reference capture
7997/// (`this.b = p1;`) or other field writes before the invokesuper in the
7998/// bytecode, which lifts as-is into an uncompilable statement order —
7999/// hoist the bare delegation call to position 0 (jadx does the same).
8000/// Does the expression touch `this` (explicit, implicit-instance member
8001/// access, or super)? Such a def cannot be inlined into a delegation
8002/// that must run BEFORE the supertype constructor.
8003fn contains_this_access(e: &Expr) -> bool {
8004    let mut hit = false;
8005    visit_exprs(e, &mut |x| {
8006        if hit {
8007            return;
8008        }
8009        match x {
8010            Expr::This => hit = true,
8011            Expr::Field { owner, is_static: false, .. } => {
8012                if owner.is_none() || matches!(&**owner.as_ref().unwrap(), Expr::This) {
8013                    hit = true;
8014                }
8015            }
8016            Expr::Method { owner, is_static: false, .. } => {
8017                if owner.is_none() || matches!(&**owner.as_ref().unwrap(), Expr::This) {
8018                    hit = true;
8019                }
8020            }
8021            _ => {}
8022        }
8023    });
8024    hit
8025}
8026
8027/// Inline the transitive straight-line defs of a delegation call's
8028/// arg locals into the call, so hoisting it to position 0 leaves no
8029/// dangling name (lark ProtoAdapter's `super(str)` with `str` defined
8030/// BELOW the hoist point — 找不到符号 变量 str). Returns false when any
8031/// arg var has no inlineable def (param-less, missing, this-touching,
8032/// or cyclic) — the caller then leaves the call in place: a
8033/// not-first-statement error is cheaper than an unresolved name that
8034/// poisons every downstream type.
8035/// Any occurrence of `var` (read OR assign-target) in `stmts` — the
8036/// suffix-use test for dropping a consumed def: count_locals_stmts only
8037/// sees reads, and a suffix WRITE to a dropped declaration makes
8038/// ensure_declared re-add `Type v;` at the method top — BEFORE the
8039/// hoisted this()/super() ("对this的调用必须是构造器中的第一个语句",
8040/// lark +587 the read-only test caused).
8041fn var_occurs_in(stmts: &[Stmt], var: u32) -> bool {
8042    stmts.iter().any(|st| {
8043        let mut hit = false;
8044        visit_stmt_exprs_ro(st, &mut |e| {
8045            if !hit {
8046                visit_exprs(e, &mut |x| {
8047                    if let Expr::Local { var: v, .. } = x {
8048                        if *v == var {
8049                            hit = true;
8050                        }
8051                    }
8052                });
8053            }
8054        });
8055        hit
8056    })
8057}
8058
8059// ---------------------------------------------------------------------------
8060// Branched-delegation ctors → static resolver helper.
8061// ---------------------------------------------------------------------------
8062
8063/// A synthetic static method extracted from a constructor whose
8064/// delegation is preceded by computation (the Kotlin default-arg /
8065/// conditional-bridge shape). Rendered by classdec after the real
8066/// methods; the ctor becomes a single first-statement delegation whose
8067/// carrier arg is the helper call.
8068pub struct CtorHelper {
8069    pub name: String,
8070    pub ret: JavaType,
8071    pub params: Vec<(JavaType, String)>,
8072    pub body: Stmt,
8073    pub vt: VarTable,
8074}
8075
8076fn ctor_helpers() -> &'static std::sync::Mutex<std::collections::HashMap<String, Vec<CtorHelper>>> {
8077    static H: std::sync::OnceLock<std::sync::Mutex<std::collections::HashMap<String, Vec<CtorHelper>>>> =
8078        std::sync::OnceLock::new();
8079    H.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::default()))
8080}
8081
8082/// Synthetic relay ctor for the FULL mode of
8083/// extract_branched_delegation_helper: `private C(Object[] h$relay) {
8084/// this/super((T0) h$relay[0], ..); }` — the public ctor becomes
8085/// `this(resolve$X(params))`, both delegations first-statement legal.
8086pub struct CtorRelay {
8087    /// The delegation target's formal types (the casts of the unpack).
8088    pub formals: Vec<JavaType>,
8089    pub is_super: bool,
8090}
8091
8092type RelayMap = std::collections::HashMap<String, Vec<(String, CtorRelay)>>;
8093
8094fn ctor_relays() -> &'static std::sync::Mutex<RelayMap> {
8095    static H: std::sync::OnceLock<std::sync::Mutex<RelayMap>> = std::sync::OnceLock::new();
8096    H.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::default()))
8097}
8098
8099/// Take (and clear) the relay ctors registered for `class` — sorted by
8100/// the originating ctor descriptor for deterministic emission.
8101pub fn take_ctor_relays(class: &str) -> Vec<(String, CtorRelay)> {
8102    let mut m = ctor_relays().lock().unwrap_or_else(|e| e.into_inner());
8103    let mut v = m.remove(class).unwrap_or_default();
8104    v.sort_by(|a, b| a.0.cmp(&b.0));
8105    v
8106}
8107
8108fn array_init(args: Vec<Expr>) -> Expr {
8109    Expr::NewArray {
8110        elem: TypeRef::J(JavaType::Object(std::sync::Arc::from("java/lang/Object"))),
8111        dims: Vec::new(),
8112        trailing_dims: 0,
8113        init: Some(args),
8114    }
8115}
8116
8117/// Deterministic helper name: FNV-1a over (class, ctor descriptor) —
8118/// worker-thread registration order must not leak into the output.
8119fn helper_name(class: &str, desc: &str) -> String {
8120    let mut h: u64 = 0xcbf29ce484222325;
8121    for b in class.as_bytes().iter().chain(b"::").chain(desc.as_bytes()) {
8122        h ^= *b as u64;
8123        h = h.wrapping_mul(0x100000001b3);
8124    }
8125    format!("resolve${:x}", (h >> 16) & 0xffff_ffff)
8126}
8127
8128/// Drain the helpers registered for a class (render-time). Sorted by
8129/// name: registration order is worker-scheduling dependent.
8130pub fn take_ctor_helpers(class: &str) -> Vec<CtorHelper> {
8131    let mut m = ctor_helpers().lock().unwrap_or_else(|e| e.into_inner());
8132    let mut v = m.remove(class).unwrap_or_default();
8133    v.sort_by(|a, b| a.name.cmp(&b.name));
8134    v
8135}
8136
8137fn strip_delegations_deep(stmts: &mut Vec<Stmt>) {
8138    stmts.retain(|s| !is_bare_ctor_call(s));
8139    for s in stmts.iter_mut() {
8140        strip_delegation_one(s);
8141    }
8142}
8143
8144fn strip_delegation_one(st: &mut Stmt) {
8145    if is_bare_ctor_call(st) {
8146        *st = Stmt::Block(Vec::new());
8147        return;
8148    }
8149    match st {
8150        Stmt::Block(v) => strip_delegations_deep(v),
8151        Stmt::If { then_stmt, else_stmt, .. } => {
8152            strip_delegation_one(then_stmt);
8153            if let Some(e) = else_stmt {
8154                strip_delegation_one(e);
8155            }
8156        }
8157        Stmt::While { body, .. }
8158        | Stmt::DoWhile { body, .. }
8159        | Stmt::ForEach { body, .. }
8160        | Stmt::Synchronized { body, .. }
8161        | Stmt::Labeled { body, .. } => strip_delegation_one(body),
8162        Stmt::For { init, body, .. } => {
8163            strip_delegations_deep(init);
8164            strip_delegation_one(body);
8165        }
8166        Stmt::Switch { cases, default, .. } => {
8167            for c in cases.iter_mut() {
8168                strip_delegations_deep(&mut c.body);
8169            }
8170            if let Some(d) = default {
8171                strip_delegation_one(d);
8172            }
8173        }
8174        Stmt::Try { body, catches, finally } | Stmt::TryWithResources { body, catches, finally, .. } => {
8175            strip_delegation_one(body);
8176            for c in catches.iter_mut() {
8177                strip_delegation_one(&mut c.body);
8178            }
8179            if let Some(f) = finally {
8180                strip_delegation_one(f);
8181            }
8182        }
8183        _ => {}
8184    }
8185}
8186
8187#[allow(clippy::ptr_arg)]
8188fn rewrite_bare_returns(stmts: &mut Vec<Stmt>, val: &Expr) {
8189    for s in stmts.iter_mut() {
8190        rewrite_bare_returns_one(s, val);
8191    }
8192}
8193
8194fn rewrite_bare_returns_one(st: &mut Stmt, val: &Expr) {
8195    match st {
8196        Stmt::Return(None) => *st = Stmt::Return(Some(val.clone())),
8197        Stmt::Block(v) => rewrite_bare_returns(v, val),
8198        Stmt::If { then_stmt, else_stmt, .. } => {
8199            rewrite_bare_returns_one(then_stmt, val);
8200            if let Some(e) = else_stmt {
8201                rewrite_bare_returns_one(e, val);
8202            }
8203        }
8204        Stmt::While { body, .. }
8205        | Stmt::DoWhile { body, .. }
8206        | Stmt::ForEach { body, .. }
8207        | Stmt::Synchronized { body, .. }
8208        | Stmt::Labeled { body, .. } => rewrite_bare_returns_one(body, val),
8209        Stmt::For { init, body, .. } => {
8210            rewrite_bare_returns(init, val);
8211            rewrite_bare_returns_one(body, val);
8212        }
8213        Stmt::Switch { cases, default, .. } => {
8214            for c in cases.iter_mut() {
8215                rewrite_bare_returns(&mut c.body, val);
8216            }
8217            if let Some(d) = default {
8218                rewrite_bare_returns_one(d, val);
8219            }
8220        }
8221        Stmt::Try { body, catches, finally } | Stmt::TryWithResources { body, catches, finally, .. } => {
8222            rewrite_bare_returns_one(body, val);
8223            for c in catches.iter_mut() {
8224                rewrite_bare_returns_one(&mut c.body, val);
8225            }
8226            if let Some(f) = finally {
8227                rewrite_bare_returns_one(f, val);
8228            }
8229        }
8230        _ => {}
8231    }
8232}
8233
8234fn collect_def_vars(stmts: &[Stmt], out: &mut jdc_core::FxHashSet<u32>) {
8235    for st in stmts {
8236        match st {
8237            Stmt::LocalDef { var, .. } => {
8238                out.insert(*var);
8239            }
8240            Stmt::ExprStmt(Expr::Assign { target, .. }) => {
8241                if let Expr::Local { var, .. } = &**target {
8242                    out.insert(*var);
8243                }
8244            }
8245            Stmt::Block(v) => collect_def_vars(v, out),
8246            Stmt::If { then_stmt, else_stmt, .. } => {
8247                collect_def_vars(std::slice::from_ref(then_stmt.as_ref()), out);
8248                if let Some(e) = else_stmt {
8249                    collect_def_vars(std::slice::from_ref(e.as_ref()), out);
8250                }
8251            }
8252            Stmt::While { body, .. }
8253            | Stmt::DoWhile { body, .. }
8254            | Stmt::Synchronized { body, .. }
8255            | Stmt::Labeled { body, .. } => {
8256                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
8257            }
8258            Stmt::ForEach { var, body, .. } => {
8259                out.insert(*var);
8260                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
8261            }
8262            Stmt::For { init, body, .. } => {
8263                collect_def_vars(init, out);
8264                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
8265            }
8266            Stmt::Switch { cases, default, .. } => {
8267                for c in cases {
8268                    collect_def_vars(&c.body, out);
8269                }
8270                if let Some(d) = default {
8271                    collect_def_vars(std::slice::from_ref(d.as_ref()), out);
8272                }
8273            }
8274            Stmt::Try { body, catches, finally }
8275            | Stmt::TryWithResources { body, catches, finally, .. } => {
8276                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
8277                for c in catches {
8278                    out.insert(c.var);
8279                    collect_def_vars(std::slice::from_ref(c.body.as_ref()), out);
8280                }
8281                if let Some(f) = finally {
8282                    collect_def_vars(std::slice::from_ref(f.as_ref()), out);
8283                }
8284            }
8285            _ => {}
8286        }
8287    }
8288}
8289
8290/// v1 extraction is limited to Block/If/straight-line computation (the
8291/// Kotlin default-arg shape); loops/switch/try in the extraction bail.
8292fn extraction_shape_ok(stmts: &[Stmt]) -> bool {
8293    stmts.iter().all(|st| match st {
8294        Stmt::Block(v) => extraction_shape_ok(v),
8295        Stmt::LocalDef { .. }
8296        | Stmt::ExprStmt(_)
8297        | Stmt::Return(_)
8298        | Stmt::Throw(_)
8299        | Stmt::Assert { .. }
8300        | Stmt::Break(_)
8301        | Stmt::Continue(_) => true,
8302        Stmt::If { then_stmt, else_stmt, .. } => {
8303            ok_one(then_stmt)
8304                && else_stmt.as_deref().map(ok_one).unwrap_or(true)
8305        }
8306        Stmt::While { body, .. }
8307        | Stmt::DoWhile { body, .. }
8308        | Stmt::ForEach { body, .. }
8309        | Stmt::Synchronized { body, .. } => ok_one(body),
8310        Stmt::Labeled { body, .. } => ok_one(body),
8311        Stmt::For { init, body, .. } => extraction_shape_ok(init) && ok_one(body),
8312        Stmt::Switch { cases, default, .. } => {
8313            cases.iter().all(|c| extraction_shape_ok(&c.body))
8314                && default.as_deref().map(ok_one).unwrap_or(true)
8315        }
8316        Stmt::Try { body, catches, finally }
8317        | Stmt::TryWithResources { body, catches, finally, .. } => {
8318            ok_one(body)
8319                && catches.iter().all(|c| ok_one(&c.body))
8320                && finally.as_deref().map(ok_one).unwrap_or(true)
8321        }
8322        // Label/Goto (unstructured), MonitorEnter/Exit (raw), ClassDecl
8323        // (local class), TernaryValue, Raw: stay out of v2 extraction.
8324        _ => false,
8325    })
8326}
8327
8328fn ok_one(st: &Stmt) -> bool {
8329    extraction_shape_ok(std::slice::from_ref(st))
8330}
8331
8332fn static_safe(stmts: &[Stmt], this_id: Option<u32>) -> bool {
8333    let mut ok = true;
8334    for st in stmts {
8335        visit_stmt_exprs_ro(st, &mut |e| {
8336            visit_exprs(e, &mut |x| {
8337                match x {
8338                    Expr::This => ok = false,
8339                    // ddc's IR usually carries `this` as the var-0
8340                    // PARAM local, not the Expr::This variant — a
8341                    // helper body reading it dangles (the this param
8342                    // is excluded from the helper's param list) AND
8343                    // renders 无法从静态上下文中引用非静态 (weixin y9/i).
8344                    Expr::Local { var, .. } => {
8345                        if Some(*var) == this_id {
8346                            ok = false;
8347                        }
8348                    }
8349                    // implicit-this member access / call
8350                    Expr::Field { owner: None, is_static: false, .. } => ok = false,
8351                    Expr::Method { owner: None, is_static: false, .. } => ok = false,
8352                    _ => {}
8353                }
8354            });
8355        });
8356    }
8357    ok
8358}
8359
8360/// The bytedance PatchProxy hotfix guard in <clinit>:
8361/// `if (vsChange != null) { PatchProxyResult p = PatchProxy.proxy(..);
8362/// if (p.isSupported) { return; } }` — a bare `return;` inside a STATIC
8363/// INITIALIZER is illegal Java (javac: 返回外部方法; videocut ×28,029 +
8364/// news_article ×19,058 — sampled 400/400 this shape). The patch path
8365/// is dead in a fresh compile (the IVsChange field is null), so the
8366/// whole leading guard statement strips, leaving the real static inits —
8367/// the <clinit> counterpart of the Titan/Robust ctor-guard strips.
8368/// Gate: FIRST top-level stmt is an If with no else whose body holds
8369/// both a bare return and a *PatchProxy* call.
8370pub fn strip_clinit_hotfix_guard(body: &mut Stmt) {
8371    // The structurer can wrap the sequence in a nested singleton block
8372    // (the enum-clinit path flattens for the same reason); the normal
8373    // emit_method path does not.
8374    if let Stmt::Block(vs) = body {
8375        flatten_top_blocks(vs);
8376    }
8377    let Stmt::Block(stmts) = body else { return };
8378    // The guard may sit behind hoisted bare declarations (`int[] v8x;`
8379    // from ensure_declared) — find the first non-decl statement.
8380    let mut gi = 0usize;
8381    while gi < stmts.len() && matches!(&stmts[gi], Stmt::LocalDef { init: None, .. }) {
8382        gi += 1;
8383    }
8384    let Some(first) = stmts.get(gi) else { return };
8385    let Stmt::If { then_stmt, else_stmt, .. } = first else {
8386        return;
8387    };
8388    if else_stmt.is_some() {
8389        return;
8390    }
8391    let mut has_ret = false;
8392    let mut has_proxy = false;
8393    let c = then_stmt.as_ref();
8394    walk_all(c, &mut |st| {
8395        if matches!(st, Stmt::Return(None)) {
8396            has_ret = true;
8397        }
8398    });
8399    let mut cc = c.clone();
8400    walk_stmt_exprs(&mut cc, &mut |e| {
8401        deep_rewrite(e, &mut |x| {
8402            if let Expr::Method { cls, .. } = x {
8403                if cls.contains("PatchProxy") {
8404                    has_proxy = true;
8405                }
8406            }
8407        });
8408    });
8409    if has_ret && has_proxy {
8410        stmts.remove(gi);
8411    }
8412}
8413
8414/// Vendor hotfix guards in CTORS that wrap a delegation:
8415/// `if (__ != null) { Object[] v = ..; ConstructorCode proxy =
8416/// ConstructorCode.proxy(..); if (proxy != null) { this(unpack(v));
8417/// proxy.afterSuper(this); return; } }` ahead of the real top-level
8418/// delegation (alipay InstantRun — ctor-not-first ×1,436; the same
8419/// shape as the already-stripped Baidu Titan / bytedance clinit /
8420/// Meituan Robust guards). The patch path is dead in a fresh compile
8421/// (the redirect field is null), and Java cannot place the wrapped
8422/// delegation legally — strip the WHOLE guard statement.
8423///
8424/// VENDOR-GATED on purpose: a conditional delegation with a top-level
8425/// fallback (`if (c) { this(a); return; } this(b);`) is legitimate
8426/// bytecode whose guard must NOT be dropped. The gate: the guard
8427/// subtree mentions a known hotfix framework type (hotfix / instantrun
8428/// / robust / titan / PatchProxy / ChangeQuickRedirect / ConstructorCode
8429/// in any method-owner, field owner+type, cast, or new).
8430/// First LEAF statement after unwrapping nested `Stmt::Block` wrappers —
8431/// the lifted ctor body reaches the ctor chain with the delegation and
8432/// the hotfix guard each wrapped in plain Blocks (the flattening that
8433/// produces the rendered shape runs later).
8434pub(crate) fn first_leaf_stmt(s: &Stmt) -> &Stmt {
8435    match s {
8436        Stmt::Block(v) if !v.is_empty() => first_leaf_stmt(&v[0]),
8437        other => other,
8438    }
8439}
8440
8441/// Vendor hotfix guards in CTORS that wrap a delegation:
8442/// `if (__ != null) { Object[] v = ..; ConstructorCode proxy =
8443/// ConstructorCode.proxy(..); if (proxy != null) { this(unpack(v));
8444/// proxy.afterSuper(this); return; } }` ahead of the real top-level
8445/// delegation (alipay InstantRun — ctor-not-first ×1,436, nearly all in
8446/// FALLBACK ENUM ctors; the same shape as the already-stripped Baidu
8447/// Titan / bytedance clinit / Meituan Robust guards). The patch path is
8448/// dead in a fresh compile (the redirect field is null), and Java cannot
8449/// place the wrapped delegation legally — strip the WHOLE guard
8450/// statement (at this pipeline stage it arrives Block-wrapped, and its
8451/// branches are still un-inverted: `If(then=∅, else=guard-body)`).
8452///
8453/// VENDOR-GATED on purpose: a conditional delegation with a top-level
8454/// fallback (`if (c) { this(a); return; } this(b);`) is legitimate
8455/// bytecode whose guard must NOT be dropped. The gate: the guard
8456/// subtree mentions a known hotfix framework type (hotfix / instantrun
8457/// / robust / titan / PatchProxy / ChangeQuickRedirect / ConstructorCode
8458/// in any method-owner, field owner+type, cast, or new).
8459pub fn strip_ctor_hotfix_guards(body: &mut Stmt) {
8460    fn marker_hit(e: &Expr) -> bool {
8461        let has = |n: &str| {
8462            let l = n.to_ascii_lowercase();
8463            l.contains("hotfix")
8464                || l.contains("instantrun")
8465                || l.contains("robust")
8466                || l.contains("titan")
8467                || l.contains("patchproxy")
8468                || l.contains("changequickredirect")
8469                || l.contains("constructorcode")
8470        };
8471        let mut hit = false;
8472        visit_exprs(e, &mut |x| {
8473            match x {
8474                Expr::Method { cls, .. } | Expr::New { cls, .. } | Expr::Field { cls, .. } => {
8475                    if has(cls) {
8476                        hit = true;
8477                    }
8478                }
8479                _ => {}
8480            }
8481            if let Expr::Field { ty, .. } = x {
8482                if let TypeRef::J(JavaType::Object(n)) = ty {
8483                    if has(n) {
8484                        hit = true;
8485                    }
8486                }
8487            }
8488        });
8489        hit
8490    }
8491    // A top-level statement that IS a hotfix guard: unwrap single-child
8492    // Block chains down to an If whose subtree holds BOTH a delegation
8493    // and a vendor marker.
8494    fn is_guard(st: &Stmt) -> bool {
8495        let mut cur = st;
8496        loop {
8497            match cur {
8498                Stmt::Block(v) if v.len() == 1 => cur = &v[0],
8499                Stmt::If { .. } => break,
8500                _ => return false,
8501            }
8502        }
8503        let mut has_del = false;
8504        walk_all(cur, &mut |s2| {
8505            if is_bare_ctor_call(s2) {
8506                has_del = true;
8507            }
8508        });
8509        if !has_del {
8510            return false;
8511        }
8512        let mut has_marker = false;
8513        visit_stmt_exprs_ro(cur, &mut |e| {
8514            if marker_hit(e) {
8515                has_marker = true;
8516            }
8517        });
8518        has_marker
8519    }
8520    // Strip at EVERY Block level: the lifted body arrives either as
8521    // [Block(guard), Block(del, ..)] (AtomicDataCollector) or as ONE big
8522    // Block [decls, guard, prelude-computes, del] (compose TextStyle).
8523    // No outside-delegation requirement: the vendor gate already proves
8524    // the statement is dead hotfix machinery (the redirect field is null
8525    // in a fresh compile — the same assumption the Titan/PatchProxy
8526    // strips run on), and the dex's fresh path IS whatever remains after
8527    // the guard. rpc/a.java: the real super() sits inside a switch arm —
8528    // requiring an unconditional outside delegation kept the guard (and
8529    // its illegal pre-super field writes) alive.
8530    fn strip_in(stmts: &mut Vec<Stmt>) {
8531        stmts.retain(|st| !is_guard(st));
8532        for st in stmts.iter_mut() {
8533            if let Stmt::Block(v) = st {
8534                strip_in(v);
8535            }
8536        }
8537    }
8538    let Stmt::Block(stmts) = body else { return };
8539    strip_in(stmts);
8540}
8541
8542/// Repeated identical branch-site delegations with param-only args
8543/// (weixin appbrand/zc: bare `super()` at four branch ends, each
8544/// followed by field-write tails) carry no computed carrier, so the
8545/// helper extraction has nothing to return — but they need no helper:
8546/// every delegating path runs the SAME call, so hoist one copy to
8547/// position 0 and strip the sites. The per-branch tails stay in their
8548/// branches and now legally follow the first-statement delegation.
8549/// Same reordering approximation fix_ctor_super_first already ships for
8550/// the straight-line case (paths that threw BEFORE delegating now run
8551/// the delegation first — Object-super, the dominant shape, is inert).
8552/// Gate: every del deep-equal and reading params only (a computed local
8553/// would dangle at position 0 — that is the helper extractor's job).
8554pub fn hoist_branch_delegations(body: &mut Stmt, vt: &VarTable) {
8555    let Stmt::Block(stmts) = body else { return };
8556    if stmts.first().is_some_and(is_bare_ctor_call) {
8557        return; // already first
8558    }
8559    let mut dels: Vec<Expr> = Vec::new();
8560    for st in stmts.iter() {
8561        visit_stmt_exprs_ro(st, &mut |e| {
8562            if is_delegation_expr(e) {
8563                dels.push(e.clone());
8564            }
8565        });
8566    }
8567    if dels.is_empty() {
8568        return;
8569    }
8570    if !dels.iter().all(|d| *d == dels[0]) {
8571        return;
8572    }
8573    let mut bad = false;
8574    visit_exprs(&dels[0], &mut |x| {
8575        if let Expr::Local { var, .. } = x {
8576            if (*var as usize) >= vt.vars.len() || !vt.vars[*var as usize].is_param {
8577                bad = true;
8578            }
8579        }
8580    });
8581    if bad {
8582        return;
8583    }
8584    let del = dels[0].clone();
8585    strip_delegations_deep(stmts);
8586    stmts.insert(0, Stmt::ExprStmt(del));
8587}
8588
8589/// The branched-delegation ctor family ("对this的调用必须是构造器中的
8590/// 第一个语句", ~2k across corpora): R8 renders a Kotlin default-arg /
8591/// conditional-bridge ctor as computation statements (straight-line plus
8592/// mask-guarded overrides) with every control path ending in a
8593/// `this(a, b, carrier)` delegation. Java cannot express statements
8594/// before the delegation, and unlike the linear-chain case the carrier's
8595/// value is computed by multi-statement branched code that cannot fold
8596/// into a ternary. Extract the computation into a synthetic
8597/// `private static T resolve$X(<ctor params>)` and make the ctor a
8598/// single first-statement `this(a, b, resolve$X(..))` — compilable AND
8599/// faithful (jadx renders the un-compilable statements-before-this form
8600/// for this family).
8601///
8602/// Two carrier modes:
8603/// - Single (v1): every delegation textually identical and exactly one
8604///   non-param local (the carrier) read exactly once across the args.
8605/// - Position (v2): delegations differ at exactly ONE arg position k,
8606///   every site passing a bare non-param local there (per-branch
8607///   carriers, lark BaseProtocol$ReliablePushList's v22_g1/v22x). Each
8608///   stripped delegation's own carrier fills the return that follows it;
8609///   merge-point returns fall back to the primary site's carrier.
8610///
8611/// The top-level primary delegation is optional in BOTH modes — when
8612/// every path delegates inside branches (weixin ln1/b1's super() in
8613/// if/try/catch) the whole body is extracted and any del serves as the
8614/// template (all non-k parts identical).
8615///
8616/// Shared gates (all conservative, bail keeps today's behavior): not an
8617/// enum (trace-param strip shifts render params); nested only when the
8618/// class RENDERS static-nested (member-inner ctors lose the outer param
8619/// at render); computation is Block/If/straight-line only; no
8620/// `this`-dependence; no local defined in the extraction used after the
8621/// primary.
8622pub fn extract_branched_delegation_helper(
8623    body: &mut Stmt,
8624    vt: &VarTable,
8625    pool: &DexPool,
8626    class: &crate::PoolClass,
8627    desc_str: &str,
8628) {
8629    if class.is_enum() {
8630        return;
8631    }
8632    // Member-inner ctors lose the synthetic outer param at render
8633    // (qualified-new absorbs it), so the helper's param list would no
8634    // longer match the rendered ctor. STATIC nested classes render all
8635    // params like top-level ones (lark BaseProtocol$PushList family).
8636    if class.nesting.enclosing_class.is_some() && !crate::ctx::nested_is_static(pool, class) {
8637        return;
8638    }
8639    let Stmt::Block(stmts) = body else { return };
8640    let mut dels: Vec<Expr> = Vec::new();
8641    for st in stmts.iter() {
8642        visit_stmt_exprs_ro(st, &mut |e| {
8643            if is_delegation_expr(e) {
8644                dels.push(e.clone());
8645            }
8646        });
8647    }
8648    if dels.is_empty() {
8649        return;
8650    }
8651    if dels.len() == 1 && stmts.first().is_some_and(is_bare_ctor_call) {
8652        return; // already first
8653    }
8654    let template = dels[0].clone();
8655    let Expr::Method { args: t_args, .. } = &template else {
8656        return;
8657    };
8658    // Carrier discovery: Single (all dels identical, one non-param local
8659    // read once) or Position (dels differ at exactly one arg slot, every
8660    // site a bare non-param local of one erased type).
8661    enum Mode {
8662        Single(u32),
8663        Position(usize),
8664        /// Every path delegates with the SAME target signature but its
8665        /// own arg expressions: the whole computation moves to the
8666        /// helper (each delegation becomes `return new Object[]{its
8667        /// args}`), the public ctor becomes `this(resolve$X(params))`
8668        /// and a synthetic `private C(Object[] h)` relay unpacks the
8669        /// carriers into the one legal first-statement delegation.
8670        Full,
8671    }
8672    // Full-mode gate: uniform delegation target (this/super, class,
8673    // descriptor), non-empty args, no pre-existing (Object[]) ctor,
8674    // and — the relay signature is per-CLASS unique — only the
8675    // lexicographically least <init> descriptor of the class may take
8676    // the relay (deterministic across parallel workers; a second
8677    // branched ctor in one class keeps its honest error).
8678    let full_gate = (|| -> Option<()> {
8679        let Expr::Method {
8680            args: a0,
8681            desc: d0,
8682            is_super: s0,
8683            cls: c0,
8684            ..
8685        } = &dels[0]
8686        else {
8687            return None;
8688        };
8689        if a0.is_empty() {
8690            return None;
8691        }
8692        for d in &dels[1..] {
8693            match d {
8694                Expr::Method {
8695                    args,
8696                    desc,
8697                    is_super,
8698                    cls,
8699                    ..
8700                } if args.len() == a0.len()
8701                    && **desc == **d0
8702                    && is_super == s0
8703                    && cls == c0 => {}
8704                _ => return None,
8705            }
8706        }
8707        if class
8708            .all_methods()
8709            .any(|m| &*m.name == "<init>" && &*m.desc == "([Ljava/lang/Object;)V")
8710        {
8711            return None;
8712        }
8713        Some(())
8714    })()
8715    .is_some();
8716    let mode = if dels.iter().all(|d| *d == dels[0]) {
8717        let mut carrier: Option<u32> = None;
8718        let mut reads = 0usize;
8719        let mut multi = false;
8720        for a in t_args.iter() {
8721            let c = a.clone();
8722            visit_exprs(&c, &mut |x| {
8723                if let Expr::Local { var, .. } = x {
8724                    if (*var as usize) < vt.vars.len() && !vt.vars[*var as usize].is_param {
8725                        reads += 1;
8726                        match carrier {
8727                            Some(v) if v == *var => {}
8728                            Some(_) => multi = true,
8729                            None => carrier = Some(*var),
8730                        }
8731                    }
8732                }
8733            });
8734        }
8735        match carrier {
8736            Some(cv) if !multi && reads == 1 => Mode::Single(cv),
8737            _ if full_gate => Mode::Full,
8738            _ => return,
8739        }
8740    } else {
8741        let positioned: Option<Mode> = (|| {
8742        let arity = t_args.len();
8743        // Uniform arity FIRST: a bare `super()` mixed among arg-carrying
8744        // dels made the single-diff slot k index past a short del's args
8745        // (deepseek v52 / kimi c60.o panics, index-out-of-bounds).
8746        if dels
8747            .iter()
8748            .any(|d| !matches!(d, Expr::Method { args, .. } if args.len() == arity))
8749        {
8750            return None;
8751        }
8752        let mut diff: Vec<usize> = Vec::new();
8753        for j in 0..arity {
8754            if dels.iter().any(|d| match d {
8755                Expr::Method { args, .. } => args.get(j) != t_args.get(j),
8756                _ => true,
8757            }) {
8758                diff.push(j);
8759            }
8760        }
8761        if diff.len() != 1 {
8762            return None;
8763        }
8764        let k = diff[0];
8765        // Non-arg parts AND all other arg positions identical: neutralize
8766        // slot k on every del and compare whole exprs (catches mixed
8767        // this()/super() or different targets).
8768        let mut base0 = dels[0].clone();
8769        if let Expr::Method { args, .. } = &mut base0 {
8770            args[k] = Expr::Const(ConstVal::Null);
8771        }
8772        for d in dels.iter() {
8773            let mut c = d.clone();
8774            if let Expr::Method { args, .. } = &mut c {
8775                args[k] = Expr::Const(ConstVal::Null);
8776            }
8777            if c != base0 {
8778                return None;
8779            }
8780        }
8781        // Slot k: bare non-param local at every site, one erased type,
8782        // and the carrier never read at another position.
8783        let mut carrier_ty: Option<JavaType> = None;
8784        for d in dels.iter() {
8785            let Expr::Method { args, .. } = d else {
8786                return None;
8787            };
8788            let Expr::Local { var, .. } = &args[k] else {
8789                return None;
8790            };
8791            if (*var as usize) >= vt.vars.len() || vt.vars[*var as usize].is_param {
8792                return None;
8793            }
8794            let ty = vt.vars[*var as usize].ty.erased();
8795            match &carrier_ty {
8796                Some(t) if *t == ty => {}
8797                Some(_) => return None,
8798                None => carrier_ty = Some(ty),
8799            }
8800            // Non-k positions must be param/const/static-only — a
8801            // computed local there would have its def extracted into
8802            // the helper and the ctor arg list would dangle (lark
8803            // audiosave/i 找不到符号 ×5-per-site). Also covers the
8804            // carrier leaking into another position.
8805            for (j, a) in args.iter().enumerate() {
8806                if j == k {
8807                    continue;
8808                }
8809                let mut bad = false;
8810                visit_exprs(a, &mut |x| {
8811                    if let Expr::Local { var: v2, .. } = x {
8812                        if (*v2 as usize) >= vt.vars.len() || !vt.vars[*v2 as usize].is_param {
8813                            bad = true;
8814                        }
8815                    }
8816                });
8817                if bad {
8818                    return None;
8819                }
8820            }
8821        }
8822        Some(Mode::Position(k))
8823        })();
8824        match positioned {
8825            Some(m) => m,
8826            None if full_gate => Mode::Full,
8827            None => return,
8828        }
8829    };
8830    let l = stmts.iter().rposition(is_bare_ctor_call);
8831    if l == Some(0) {
8832        return;
8833    }
8834    let (mut extracted, mut tail): (Vec<Stmt>, Vec<Stmt>) = match l {
8835        Some(l) => (stmts[..l].to_vec(), stmts[l + 1..].to_vec()),
8836        None => (stmts.clone(), Vec::new()),
8837    };
8838    if !extraction_shape_ok(&extracted) {
8839        return;
8840    }
8841    // Post-delegation tails (field writes / trace stubs between each
8842    // branch delegation and its return) are CTOR work, not helper
8843    // computation — they commonly touch `this`. When every site's tail
8844    // is identical (and equals the top-level continuation when one
8845    // exists) hoist one copy after the merged delegation; otherwise
8846    // they stay in the helper and the static_safe gate below decides.
8847    let mut site_tails: Vec<Vec<Stmt>> = Vec::new();
8848    {
8849        let mut probe = extracted.clone();
8850        site_tails_walk(&mut probe, &mut site_tails, false);
8851    }
8852    let hoist = !site_tails.is_empty() && {
8853        let n0 = normalize_tail(&site_tails[0]);
8854        site_tails.iter().all(|t| normalize_tail(t) == n0)
8855            && (l.is_none() || normalize_tail(&tail) == n0)
8856    };
8857    if hoist {
8858        site_tails_walk(&mut extracted, &mut Vec::new(), true);
8859        if l.is_none() {
8860            tail = normalize_tail(&site_tails[0]);
8861        }
8862    }
8863    // The delegation the ctor keeps: the top-level primary when present,
8864    // else any del (all non-k parts are identical).
8865    let primary_del: Expr = match l {
8866        Some(l) => match &stmts[l] {
8867            Stmt::ExprStmt(e) => e.clone(),
8868            _ => return,
8869        },
8870        None => dels[0].clone(),
8871    };
8872    let ret = match mode {
8873        Mode::Single(cv) => vt.var(cv).ty.erased(),
8874        Mode::Position(k) => match &primary_del {
8875            Expr::Method { args, .. } => match args.get(k) {
8876                Some(Expr::Local { var, .. }) => vt.var(*var).ty.erased(),
8877                _ => return,
8878            },
8879            _ => return,
8880        },
8881        Mode::Full => JavaType::Array(Box::new(JavaType::Object(std::sync::Arc::from(
8882            "java/lang/Object",
8883        )))),
8884    };
8885    match mode {
8886        Mode::Single(cv) => {
8887            strip_delegations_deep(&mut extracted);
8888            let cv_local = Expr::Local {
8889                var: cv,
8890                ty: vt.var(cv).ty.clone(),
8891            };
8892            rewrite_bare_returns(&mut extracted, &cv_local);
8893            if !always_returns(&extracted) {
8894                extracted.push(Stmt::Return(Some(cv_local)));
8895            }
8896        }
8897        Mode::Position(k) => {
8898            // Each stripped delegation's own slot-k carrier fills the
8899            // next bare return in its list; merge-point returns fall
8900            // back to the primary carrier below.
8901            fill_returns_by_position(&mut extracted, &|args: &[Expr]| args.get(k).cloned());
8902            let fallback = match &primary_del {
8903                Expr::Method { args, .. } => args[k].clone(),
8904                _ => return,
8905            };
8906            rewrite_bare_returns(&mut extracted, &fallback);
8907            if !always_returns(&extracted) {
8908                extracted.push(Stmt::Return(Some(fallback)));
8909            }
8910        }
8911        Mode::Full => {
8912            // Every delegation becomes `return new Object[]{its own
8913            // args}`; merge-point bare returns and the fall-through
8914            // completion take the PRIMARY delegation's args (the
8915            // top-level last del when present — its args are
8916            // top-level-scoped by construction). With no primary,
8917            // every path must already delegate explicitly.
8918            fill_returns_by_position(&mut extracted, &|args: &[Expr]| {
8919                Some(array_init(args.to_vec()))
8920            });
8921            if l.is_none() {
8922                // No top-level primary: every path must delegate. A
8923                // dead tail can still dangle after the shortest
8924                // always-returning prefix (DataHolder: an assign after
8925                // a try whose body returns and whose catch throws) —
8926                // truncate it (unreachable by Java's flow model, so
8927                // sound), else bail.
8928                if !always_returns(&extracted) {
8929                    let cut = (1..=extracted.len())
8930                        .find(|&i| always_returns(&extracted[..i]));
8931                    match cut {
8932                        Some(i) => extracted.truncate(i),
8933                        None => return,
8934                    }
8935                }
8936            }
8937            let fallback_args = match &primary_del {
8938                Expr::Method { args, .. } => args.clone(),
8939                _ => return,
8940            };
8941            let fallback = array_init(fallback_args);
8942            rewrite_bare_returns(&mut extracted, &fallback);
8943            if !always_returns(&extracted) {
8944                extracted.push(Stmt::Return(Some(fallback)));
8945            }
8946        }
8947    }
8948    let this_id = vt
8949        .vars
8950        .iter()
8951        .find(|v| v.is_param && v.name == "this")
8952        .map(|v| v.id);
8953    if !static_safe(&extracted, this_id) {
8954        return;
8955    }
8956    let mut defs: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
8957    collect_def_vars(&extracted, &mut defs);
8958    if defs.iter().any(|v| var_occurs_in(&tail, *v)) {
8959        return;
8960    }
8961    let params: Vec<(u32, JavaType, String)> = vt
8962        .vars
8963        .iter()
8964        .filter(|v| v.is_param && v.name != "this")
8965        .map(|v| (v.id, v.ty.erased(), v.name.clone()))
8966        .collect();
8967    let name = helper_name(&class.name, desc_str);
8968    let call = Expr::Method {
8969        owner: None,
8970        cls: std::sync::Arc::from(class.name.as_str()),
8971        name: std::sync::Arc::from(name.as_str()),
8972        desc: std::sync::Arc::new(jdc_core::types::MethodDescriptor {
8973            ret: ret.clone(),
8974            args: params.iter().map(|p| p.1.clone()).collect(),
8975        }),
8976        args: params
8977            .iter()
8978            .map(|(id, ty, _)| Expr::Local {
8979                var: *id,
8980                ty: TypeRef::J(ty.clone()),
8981            })
8982            .collect(),
8983        is_static: true,
8984        is_interface: false,
8985        is_special: false,
8986        is_super: false,
8987        is_dynamic: false,
8988        type_args: Vec::new(),
8989    };
8990    let mut new_del = primary_del.clone();
8991    match mode {
8992        Mode::Single(cv) => {
8993            deep_rewrite(&mut new_del, &mut |x| {
8994                if let Expr::Local { var, .. } = x {
8995                    if *var == cv {
8996                        *x = call.clone();
8997                    }
8998                }
8999            });
9000        }
9001        Mode::Position(k) => {
9002            let Expr::Method { args, .. } = &mut new_del else {
9003                return;
9004            };
9005            if k >= args.len() {
9006                return;
9007            }
9008            args[k] = call.clone();
9009        }
9010        Mode::Full => {
9011            // The public ctor's one legal statement: `this(resolve$X(p..))`
9012            // — the synthetic relay ctor below unpacks the carriers into
9013            // the uniform target delegation.
9014            new_del = Expr::Method {
9015                owner: None,
9016                cls: std::sync::Arc::from(class.name.as_str()),
9017                name: "<init>".into(),
9018                desc: std::sync::Arc::new(jdc_core::types::MethodDescriptor {
9019                    args: vec![JavaType::Array(Box::new(JavaType::Object(
9020                        std::sync::Arc::from("java/lang/Object"),
9021                    )))],
9022                    ret: JavaType::Void,
9023                }),
9024                args: vec![call.clone()],
9025                is_static: false,
9026                is_interface: false,
9027                is_special: true,
9028                is_super: false,
9029                is_dynamic: false,
9030                type_args: Vec::new(),
9031            };
9032        }
9033    }
9034    {
9035        let mut m = ctor_helpers().lock().unwrap_or_else(|e| e.into_inner());
9036        let v = m.entry(class.name.clone()).or_default();
9037        if !v.iter().any(|h| h.name == name) {
9038            // The extracted body skips the late pipeline polish (it left
9039            // the ctor before cleanup ran) — invert empty thens so the
9040            // helper doesn't render `if (..) {} else {..}`.
9041            let mut hbody = Stmt::Block(extracted);
9042            invert_empty_thens(&mut hbody);
9043            v.push(CtorHelper {
9044                name,
9045                ret: ret.clone(),
9046                params: params.into_iter().map(|(_, ty, nm)| (ty, nm)).collect(),
9047                body: hbody,
9048                vt: vt.clone(),
9049            });
9050            if matches!(mode, Mode::Full) {
9051                let Expr::Method {
9052                    desc: d0,
9053                    is_super: s0,
9054                    ..
9055                } = &primary_del
9056                else {
9057                    return;
9058                };
9059                let mut rm = ctor_relays().lock().unwrap_or_else(|e| e.into_inner());
9060                let rv = rm.entry(class.name.clone()).or_default();
9061                if !rv.iter().any(|(d, _)| d == desc_str) {
9062                    rv.push((
9063                        desc_str.to_string(),
9064                        CtorRelay {
9065                            formals: d0.args.clone(),
9066                            is_super: *s0,
9067                        },
9068                    ));
9069                }
9070            }
9071        } else {
9072            return; // duplicate decompile of the same ctor: keep the first
9073        }
9074    }
9075    *stmts = std::iter::once(Stmt::ExprStmt(new_del)).chain(tail).collect();
9076}
9077
9078/// Position-mode return fill: walk each statement list left-to-right; a
9079/// delegation at slot k hands its carrier to the next bare `return;` in
9080/// the SAME list (the bytecode shape is always `this(..); trace..;
9081/// return;`). With no following return in the list the delegation
9082/// becomes the return itself and the rest of the list (trace stubs only
9083/// — a second delegation there is impossible) is dropped as unreachable.
9084fn fill_returns_by_position(
9085    stmts: &mut Vec<Stmt>,
9086    pick: &dyn Fn(&[Expr]) -> Option<Expr>,
9087) {
9088    let mut i = 0;
9089    while i < stmts.len() {
9090        if is_bare_ctor_call(&stmts[i]) {
9091            let karg = match &stmts[i] {
9092                Stmt::ExprStmt(Expr::Method { args, .. }) => pick(args),
9093                _ => None,
9094            };
9095            if let Some(v) = karg {
9096                let mut j = i + 1;
9097                let mut found = None;
9098                while j < stmts.len() {
9099                    if matches!(stmts[j], Stmt::Return(None)) {
9100                        found = Some(j);
9101                        break;
9102                    }
9103                    if is_bare_ctor_call(&stmts[j]) {
9104                        break; // next delegation owns the next return
9105                    }
9106                    j += 1;
9107                }
9108                match found {
9109                    Some(j) => {
9110                        stmts[j] = Stmt::Return(Some(v));
9111                        stmts.remove(i);
9112                        continue;
9113                    }
9114                    None => {
9115                        stmts[i] = Stmt::Return(Some(v));
9116                        stmts.truncate(i + 1);
9117                        break;
9118                    }
9119                }
9120            }
9121        }
9122        fill_returns_one(&mut stmts[i], pick);
9123        i += 1;
9124    }
9125}
9126
9127fn fill_returns_one(st: &mut Stmt, pick: &dyn Fn(&[Expr]) -> Option<Expr>) {
9128    match st {
9129        Stmt::Block(v) => fill_returns_by_position(v, pick),
9130        Stmt::If { then_stmt, else_stmt, .. } => {
9131            fill_returns_stmt(then_stmt, pick);
9132            if let Some(e) = else_stmt {
9133                fill_returns_stmt(e, pick);
9134            }
9135        }
9136        Stmt::While { body, .. }
9137        | Stmt::DoWhile { body, .. }
9138        | Stmt::ForEach { body, .. }
9139        | Stmt::Synchronized { body, .. }
9140        | Stmt::Labeled { body, .. } => fill_returns_stmt(body, pick),
9141        Stmt::For { init, body, .. } => {
9142            fill_returns_by_position(init, pick);
9143            fill_returns_stmt(body, pick);
9144        }
9145        Stmt::Switch { cases, default, .. } => {
9146            for c in cases.iter_mut() {
9147                fill_returns_by_position(&mut c.body, pick);
9148            }
9149            if let Some(d) = default {
9150                fill_returns_stmt(d, pick);
9151            }
9152        }
9153        Stmt::Try { body, catches, finally } | Stmt::TryWithResources { body, catches, finally, .. } => {
9154            fill_returns_stmt(body, pick);
9155            for c in catches.iter_mut() {
9156                fill_returns_stmt(&mut c.body, pick);
9157            }
9158            if let Some(f) = finally {
9159                fill_returns_stmt(f, pick);
9160            }
9161        }
9162        _ => {}
9163    }
9164}
9165
9166fn fill_returns_stmt(st: &mut Stmt, pick: &dyn Fn(&[Expr]) -> Option<Expr>) {
9167    if is_bare_ctor_call(st) {
9168        // Single-statement branch body (`if (c) this(..);`) — becomes
9169        // the return directly.
9170        if let Stmt::ExprStmt(Expr::Method { args, .. }) = st {
9171            if let Some(v) = pick(args) {
9172                *st = Stmt::Return(Some(v));
9173                return;
9174            }
9175        }
9176    }
9177    fill_returns_one(st, pick);
9178}
9179
9180fn normalize_tail(t: &[Stmt]) -> Vec<Stmt> {
9181    let mut v = t.to_vec();
9182    while matches!(v.last(), Some(Stmt::Return(None))) {
9183        v.pop();
9184    }
9185    v
9186}
9187
9188/// Record (and with `carve`, remove) the statements between each
9189/// delegation and the next bare return in its list — a delegation with
9190/// no following return takes the rest of the list. Single-statement
9191/// branch bodies record an empty tail.
9192fn site_tails_walk(stmts: &mut Vec<Stmt>, out: &mut Vec<Vec<Stmt>>, carve: bool) {
9193    let mut i = 0;
9194    while i < stmts.len() {
9195        if is_bare_ctor_call(&stmts[i]) {
9196            let mut j = i + 1;
9197            let mut end = stmts.len();
9198            while j < stmts.len() {
9199                if matches!(stmts[j], Stmt::Return(None)) {
9200                    end = j;
9201                    break;
9202                }
9203                if is_bare_ctor_call(&stmts[j]) {
9204                    end = j;
9205                    break;
9206                }
9207                j += 1;
9208            }
9209            out.push(stmts[i + 1..end].to_vec());
9210            if carve {
9211                stmts.splice(i + 1..end, []);
9212            }
9213            i += 1;
9214            continue;
9215        }
9216        site_tails_one(&mut stmts[i], out, carve);
9217        i += 1;
9218    }
9219}
9220
9221fn site_tails_one(st: &mut Stmt, out: &mut Vec<Vec<Stmt>>, carve: bool) {
9222    if is_bare_ctor_call(st) {
9223        out.push(Vec::new());
9224        return;
9225    }
9226    match st {
9227        Stmt::Block(v) => site_tails_walk(v, out, carve),
9228        Stmt::If { then_stmt, else_stmt, .. } => {
9229            site_tails_one(then_stmt, out, carve);
9230            if let Some(e) = else_stmt {
9231                site_tails_one(e, out, carve);
9232            }
9233        }
9234        Stmt::While { body, .. }
9235        | Stmt::DoWhile { body, .. }
9236        | Stmt::ForEach { body, .. }
9237        | Stmt::Synchronized { body, .. }
9238        | Stmt::Labeled { body, .. } => site_tails_one(body, out, carve),
9239        Stmt::For { init, body, .. } => {
9240            site_tails_walk(init, out, carve);
9241            site_tails_one(body, out, carve);
9242        }
9243        Stmt::Switch { cases, default, .. } => {
9244            for c in cases.iter_mut() {
9245                site_tails_walk(&mut c.body, out, carve);
9246            }
9247            if let Some(d) = default {
9248                site_tails_one(d, out, carve);
9249            }
9250        }
9251        Stmt::Try { body, catches, finally } | Stmt::TryWithResources { body, catches, finally, .. } => {
9252            site_tails_one(body, out, carve);
9253            for c in catches.iter_mut() {
9254                site_tails_one(&mut c.body, out, carve);
9255            }
9256            if let Some(f) = finally {
9257                site_tails_one(f, out, carve);
9258            }
9259        }
9260        _ => {}
9261    }
9262}
9263
9264/// Can this statement list NOT complete normally? Conservative for
9265/// loops (false); If needs BOTH branches, Switch needs every case plus
9266/// a default. Used to decide whether the extracted helper needs the
9267/// trailing `return carrier;` (appending one after exhaustive branches
9268/// is javac's "unreachable statement" error).
9269fn always_returns(stmts: &[Stmt]) -> bool {
9270    let Some(last) = stmts
9271        .iter()
9272        .rev()
9273        .find(|s| !matches!(s, Stmt::Block(v) if v.is_empty()))
9274    else {
9275        return false;
9276    };
9277    match last {
9278        Stmt::Return(Some(_)) | Stmt::Throw(_) => true,
9279        Stmt::Block(v) => always_returns(v),
9280        Stmt::If { then_stmt, else_stmt, .. } => match else_stmt {
9281            Some(e) => always_returns_one(then_stmt) && always_returns_one(e),
9282            None => false,
9283        },
9284        Stmt::Labeled { body, .. } | Stmt::Synchronized { body, .. } => {
9285            always_returns_one(body)
9286        }
9287        Stmt::Switch { cases, default, .. } => {
9288            default.as_ref().is_some_and(|d| always_returns_one(d))
9289                && cases.iter().all(|c| always_returns(&c.body))
9290        }
9291        Stmt::Try { body, catches, finally }
9292        | Stmt::TryWithResources { body, catches, finally, .. } => {
9293            finally.as_ref().is_some_and(|f| always_returns_one(f))
9294                || (always_returns_one(body)
9295                    && catches.iter().all(|c| always_returns_one(&c.body)))
9296        }
9297        _ => false,
9298    }
9299}
9300
9301fn always_returns_one(st: &Stmt) -> bool {
9302    always_returns(std::slice::from_ref(st))
9303}
9304
9305fn inline_ctor_arg_defs(
9306    call: &mut Stmt,
9307    prefix: &[&Stmt],
9308    vt: &VarTable,
9309    consumed: &mut Vec<(u32, usize)>,
9310) -> bool {
9311    let Stmt::ExprStmt(cexpr) = call else {
9312        return false;
9313    };
9314    // Replace every read of `v` in `e` with its value-so-far; a read
9315    // BEFORE any def (val None) is unresolvable → false.
9316    fn subst(e: &mut Expr, v: u32, val: &Option<Expr>) -> bool {
9317        let mut ok = true;
9318        deep_rewrite(e, &mut |x| {
9319            if let Expr::Local { var, .. } = x {
9320                if *var == v {
9321                    match val {
9322                        Some(e2) => *x = e2.clone(),
9323                        None => ok = false,
9324                    }
9325                }
9326            }
9327        });
9328        ok
9329    }
9330    let mut seen: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
9331    for _ in 0..32 {
9332        let mut need: Option<u32> = None;
9333        visit_exprs(cexpr, &mut |x| {
9334            if need.is_none() {
9335                if let Expr::Local { var, .. } = x {
9336                    if (*var as usize) < vt.vars.len() && !vt.vars[*var as usize].is_param {
9337                        need = Some(*var);
9338                    }
9339                }
9340            }
9341        });
9342        let Some(v) = need else { break };
9343        if !seen.insert(v) {
9344            return false; // cyclic reuse (`str = .. + str`) — bail
9345        }
9346        // CHAIN walk: fold EVERY prefix def of v into the value the
9347        // delegation actually receives. The old strict single-occurrence
9348        // rule rejected the convenience-ctor reassignment chain
9349        // (`f = getInstance(ctx); f = transform(f, ..); this(ctx, f)` —
9350        // androidx CameraController, weibo this-not-first ×831): the
9351        // SOURCE inlined the computation into the delegation args and
9352        // R8 decomposed it into register writes. Reads of v are allowed
9353        // only INSIDE its own def chain; any other prefix statement
9354        // touching v means the value escapes or the object mutates
9355        // (`stringBuilder.append(..)` — inlining a fresh `new
9356        // StringBuilder()` would lose the message), so bail.
9357        let mut val: Option<Expr> = None;
9358        let mut ok = true;
9359        for (si, st) in prefix.iter().enumerate() {
9360            match st {
9361                // A bare declaration (init None — the structurer hoists
9362                // `T v;` to the top) carries no value: skip it. Failing
9363                // here rejected every chain under a hoisted decl (ci1/a).
9364                Stmt::LocalDef { var, init: None, .. } if *var == v => {}
9365                Stmt::LocalDef { var, init, .. } if *var == v => {
9366                    let Some(e) = init else { ok = false; break };
9367                    let mut e2 = (*e).clone();
9368                    if !subst(&mut e2, v, &val) {
9369                        ok = false;
9370                        break;
9371                    }
9372                    val = Some(e2);
9373                    consumed.push((v, si));
9374                }
9375                Stmt::ExprStmt(Expr::Assign { target, op, .. })
9376                    if matches!(&**target, Expr::Local { var: tv, .. } if *tv == v) =>
9377                {
9378                    if !matches!(op, AssignOp::Plain) {
9379                        ok = false; // compound write: not a plain def
9380                        break;
9381                    }
9382                    let Stmt::ExprStmt(Expr::Assign { value, .. }) = st else {
9383                        unreachable!()
9384                    };
9385                    let mut e2 = value.as_ref().clone();
9386                    if !subst(&mut e2, v, &val) {
9387                        ok = false;
9388                        break;
9389                    }
9390                    val = Some(e2);
9391                    consumed.push((v, si));
9392                }
9393                _ => {
9394                    let mut touched = false;
9395                    visit_all_exprs(st, &mut |x| {
9396                        if let Expr::Local { var, .. } = x {
9397                            if *var == v {
9398                                touched = true;
9399                            }
9400                        }
9401                    });
9402                    if touched {
9403                        ok = false;
9404                        break;
9405                    }
9406                }
9407            }
9408        }
9409        if !ok {
9410            return false;
9411        }
9412        let Some(de) = val else { return false };
9413        if contains_this_access(&de) {
9414            return false;
9415        }
9416        // Huge defs multiply per read site per round — bound them.
9417        let mut de_nodes = 0usize;
9418        visit_exprs(&de, &mut |_| de_nodes += 1);
9419        if de_nodes > 256 {
9420            return false;
9421        }
9422        deep_rewrite(cexpr, &mut |x| {
9423            if let Expr::Local { var, .. } = x {
9424                if *var == v {
9425                    *x = de.clone();
9426                }
9427            }
9428        });
9429        // Exponential blowup guard: a var read twice doubles its def
9430        // per round, compounding across the transitive chain — cap the
9431        // expression size (a 2^k node tree overflowed the worker stack
9432        // process-wide).
9433        let mut nodes = 0usize;
9434        visit_exprs(cexpr, &mut |_| nodes += 1);
9435        if nodes > 2_048 {
9436            return false;
9437        }
9438    }
9439    true
9440}
9441
9442/// Fold a prelude StringBuilder mutation chain into the delegation
9443/// argument as a fluent chain: `StringBuilder sb = new SB(x);
9444/// sb.append(a); sb.append(b); super(sb.toString())` →
9445/// `super(new SB(x).append(a).append(b).toString())`. The generic def
9446/// inliner rejects mutation calls — their effect is not expressible as
9447/// a value — but `append` RETURNS its receiver, so the chain
9448/// reconstructs exactly the same mutations in one expression (androidx
9449/// emoji2/flatbuffer UnpairedSurrogateException family, the shapes the
9450/// strict-occurrence rule was written to refuse). Aborts on any `this`
9451/// touch inside the chain (illegal before super) or any other read of
9452/// the SB local (the value must not escape the fold). Returns the
9453/// delegation's new index (statements before it were removed).
9454fn fold_sb_chain(stmts: &mut Vec<Stmt>, pos: usize) -> usize {
9455    // 1. the SB local: first `v = new StringBuilder(..)` in the prefix.
9456    let mut sbv: Option<u32> = None;
9457    let mut def_i: Option<usize> = None;
9458    let mut def_init: Option<Expr> = None;
9459    for (i, st) in stmts[..pos].iter().enumerate() {
9460        let cand: Option<(u32, &Expr)> = match st {
9461            Stmt::LocalDef {
9462                var,
9463                init: Some(e),
9464                ..
9465            } => Some((*var, e)),
9466            Stmt::ExprStmt(Expr::Assign {
9467                target,
9468                value,
9469                op: AssignOp::Plain,
9470                ..
9471            }) => match &**target {
9472                Expr::Local { var, .. } => Some((*var, value.as_ref())),
9473                _ => None,
9474            },
9475            _ => None,
9476        };
9477        if let Some((v, init)) = cand {
9478            if let Expr::New { cls, .. } = init {
9479                if cls.as_ref() == "java/lang/StringBuilder" {
9480                    sbv = Some(v);
9481                    def_i = Some(i);
9482                    def_init = Some(init.clone());
9483                    break;
9484                }
9485            }
9486        }
9487    }
9488    let (Some(sbv), Some(def_i), Some(init)) = (sbv, def_i, def_init) else {
9489        return pos;
9490    };
9491    if contains_this_access(&init) {
9492        return pos;
9493    }
9494    // 2. append statements on sbv, in order.
9495    let mut appends: Vec<usize> = Vec::new();
9496    for (i, st) in stmts[..pos].iter().enumerate() {
9497        if let Stmt::ExprStmt(Expr::Method {
9498            owner: Some(o),
9499            cls,
9500            name,
9501            ..
9502        }) = st
9503        {
9504            if matches!(&**o, Expr::Local { var, .. } if *var == sbv)
9505                && cls.as_ref() == "java/lang/StringBuilder"
9506                && &**name == "append"
9507            {
9508                appends.push(i);
9509            }
9510        }
9511    }
9512    // 3. no read of sbv after the delegation.
9513    let mut post_read = false;
9514    for st in stmts.iter().skip(pos + 1) {
9515        visit_all_exprs(st, &mut |e| {
9516            if let Expr::Local { var, .. } = e {
9517                if *var == sbv {
9518                    post_read = true;
9519                }
9520            }
9521        });
9522    }
9523    if post_read {
9524        return pos;
9525    }
9526    // 4. prefix reads of sbv only in the def and the append receivers
9527    //    (arguments are checked for `this`, receivers are the chain).
9528    for (i, st) in stmts[..pos].iter().enumerate() {
9529        if i == def_i || appends.contains(&i) {
9530            // append ARGUMENTS must not touch this; receiver is fine.
9531            if let Stmt::ExprStmt(Expr::Method { args, .. }) = st {
9532                if args.iter().any(contains_this_access) {
9533                    return pos;
9534                }
9535            }
9536            continue;
9537        }
9538        let mut leak = false;
9539        visit_all_exprs(st, &mut |e| {
9540            if let Expr::Local { var, .. } = e {
9541                if *var == sbv {
9542                    leak = true;
9543                }
9544            }
9545        });
9546        if leak {
9547            return pos;
9548        }
9549    }
9550    // 5. the call reads sbv exactly once, as `sbv.toString()`.
9551    let mut local_reads = 0usize;
9552    let mut tostring_reads = 0usize;
9553    visit_all_exprs(&stmts[pos], &mut |e| match e {
9554        Expr::Local { var, .. } if *var == sbv => local_reads += 1,
9555        Expr::Method {
9556            owner: Some(o),
9557            name,
9558            args,
9559            ..
9560        } if args.is_empty()
9561            && &**name == "toString"
9562            && matches!(&**o, Expr::Local { var, .. } if *var == sbv) =>
9563        {
9564            tostring_reads += 1;
9565        }
9566        _ => {}
9567    });
9568    if local_reads != 1 || tostring_reads != 1 {
9569        return pos;
9570    }
9571    // 6. build the fluent chain and swap it into the toString receiver.
9572    let mut chain = init;
9573    for ai in &appends {
9574        if let Stmt::ExprStmt(Expr::Method {
9575            owner: _,
9576            cls,
9577            name,
9578            desc,
9579            args,
9580            is_static,
9581            is_interface,
9582            is_special,
9583            is_super,
9584            is_dynamic,
9585            type_args,
9586            ..
9587        }) = &stmts[*ai]
9588        {
9589            chain = Expr::Method {
9590                owner: Some(Box::new(chain)),
9591                cls: cls.clone(),
9592                name: name.clone(),
9593                desc: desc.clone(),
9594                args: args.clone(),
9595                is_static: *is_static,
9596                is_interface: *is_interface,
9597                is_special: *is_special,
9598                is_super: *is_super,
9599                is_dynamic: *is_dynamic,
9600                type_args: type_args.clone(),
9601            };
9602        }
9603    }
9604    let mut replaced = false;
9605    walk_stmt_exprs(&mut stmts[pos], &mut |e| {
9606        deep_rewrite(e, &mut |x| {
9607            if let Expr::Method {
9608                owner: Some(o),
9609                name,
9610                args,
9611                ..
9612            } = x
9613            {
9614                if args.is_empty()
9615                    && &**name == "toString"
9616                    && matches!(&**o, Expr::Local { var, .. } if *var == sbv)
9617                {
9618                    **o = chain.clone();
9619                    replaced = true;
9620                }
9621            }
9622        });
9623    });
9624    if !replaced {
9625        return pos; // never remove without the swap — atomicity
9626    }
9627    // 7. drop the consumed statements (descending), adjust pos.
9628    let mut removed: Vec<usize> = appends.clone();
9629    removed.push(def_i);
9630    removed.sort_unstable();
9631    removed.dedup();
9632    for i in removed.iter().rev() {
9633        stmts.remove(*i);
9634    }
9635    pos - removed.len()
9636}
9637
9638pub fn fix_ctor_super_first(body: &mut Stmt, vt: &VarTable) {
9639    let Stmt::Block(stmts) = body else { return };
9640    if stmts.is_empty() || is_bare_ctor_call(stmts.first().unwrap()) {
9641        return;
9642    }
9643    // Top-level delegation call.
9644    if let Some(pos0) = stmts.iter().position(is_bare_ctor_call) {
9645        let pos = fold_sb_chain(stmts, pos0);
9646        let mut call = stmts[pos].clone();
9647        let prefix: Vec<&Stmt> = stmts[..pos].iter().collect();
9648        let mut consumed: Vec<(u32, usize)> = Vec::new();
9649        if !inline_ctor_arg_defs(&mut call, &prefix, vt, &mut consumed) {
9650            return;
9651        }
9652        stmts.remove(pos);
9653        // Drop the chain defs the inline consumed (all indices < pos) —
9654        // leaving them would DOUBLE-evaluate their (possibly impure)
9655        // inits. But ONLY for vars with no post-delegation use: dropping
9656        // such a var's DECLARATION makes ensure_declared re-add it at
9657        // the method top, i.e. BEFORE this() — a fresh "对this的调用必须
9658        // 是构造器中的第一个语句" (lark +587 the blind drop caused).
9659        let mut drop_idx: Vec<usize> = consumed
9660            .iter()
9661            .filter(|(v, _)| !var_occurs_in(&stmts[pos..], *v))
9662            .map(|(_, k)| *k)
9663            .collect();
9664        drop_idx.sort_unstable();
9665        drop_idx.dedup();
9666        for k in drop_idx.iter().rev() {
9667            stmts.remove(*k);
9668        }
9669        stmts.insert(0, call);
9670        return;
9671    }
9672    // The structurer can wrap the delegation in a bare nested block —
9673    // with the parameter null-checks AHEAD of it (weixin's Kotlin
9674    // intrinsics shape: `o.h(parcel, "source"); super();` at inner
9675    // positions 0/1, flattened to straight-line by cleanup AFTER this
9676    // pass, which is why the miss surfaced as 1150 weixin "对super的
9677    // 调用必须是构造器中的第一个语句"). Hoist the call from any
9678    // position whose predecessors are all straight-line statements; a
9679    // delegation behind control flow is the conditional-super family
9680    // (needs restructuring, not hoisting) and stays put.
9681    let mut found: Option<(usize, usize)> = None;
9682    for (i, st) in stmts.iter().enumerate() {
9683        if let Stmt::Block(inner) = st {
9684            if let Some(p) = inner.iter().position(is_bare_ctor_call) {
9685                if inner[..p]
9686                    .iter()
9687                    .all(|s| matches!(s, Stmt::LocalDef { .. } | Stmt::ExprStmt(_)))
9688                {
9689                    found = Some((i, p));
9690                    break;
9691                }
9692            }
9693        }
9694    }
9695    if let Some((i, p)) = found {
9696        let mut call = match &stmts[i] {
9697            Stmt::Block(inner) => inner[p].clone(),
9698            _ => unreachable!(),
9699        };
9700        let mut prefix: Vec<&Stmt> = stmts[..i].iter().collect();
9701        if let Stmt::Block(inner) = &stmts[i] {
9702            prefix.extend(inner[..p].iter());
9703        }
9704        let mut consumed: Vec<(u32, usize)> = Vec::new();
9705        if !inline_ctor_arg_defs(&mut call, &prefix, vt, &mut consumed) {
9706            return;
9707        }
9708        // Prefix index k maps to stmts[k] for k < i, inner[k - i] for
9709        // k >= i (all such k are < p). Suffix = the rest of the inner
9710        // block + the following top-level statements; a consumed def may
9711        // only drop when its var has no suffix use (declaration loss →
9712        // ensure_declared re-adds it BEFORE this()).
9713        // Compute the droppable set BEFORE mutating (borrow split).
9714        let mut ks: Vec<usize> = Vec::new();
9715        {
9716            let inner_tail: &[Stmt] = match &stmts[i] {
9717                Stmt::Block(inner) if p < inner.len() => &inner[p + 1..],
9718                _ => &[],
9719            };
9720            for (v, k) in &consumed {
9721                if !var_occurs_in(inner_tail, *v) && !var_occurs_in(&stmts[i + 1..], *v) {
9722                    ks.push(*k);
9723                }
9724            }
9725        }
9726        ks.sort_unstable();
9727        ks.dedup();
9728        if let Stmt::Block(inner) = &mut stmts[i] {
9729            inner.remove(p);
9730            for k in ks.iter().rev().filter(|k| **k >= i) {
9731                inner.remove(k - i);
9732            }
9733        }
9734        for k in ks.iter().rev().filter(|k| **k < i) {
9735            stmts.remove(*k);
9736        }
9737        stmts.insert(0, call);
9738    }
9739}
9740
9741/// A true constructor DELEGATION statement: `super(..)` (owner None) or
9742/// `this(..)` (owner This). An un-folded `new X; <init>` (lost
9743/// allocation — the ctor-fold-bug family) arrives as a `<init>` Method
9744/// with a NON-this owner and prints as `new X(..)`: it is not a
9745/// delegation and must neither satisfy nor anchor the super-first fixes
9746/// (v7.a$a2: a dead `new f(0,..)` init at position 0 made both hoists
9747/// believe the delegation was already first, leaving the real `super`
9748/// last — "对super的调用必须是构造器中的第一个语句").
9749fn is_delegation_expr(e: &Expr) -> bool {
9750    matches!(
9751        e,
9752        Expr::Method { name, is_special: true, owner, .. }
9753            if &**name == "<init>"
9754                && (owner.is_none() || matches!(owner.as_deref(), Some(Expr::This)))
9755    )
9756}
9757
9758pub(crate) fn is_bare_ctor_call(s: &Stmt) -> bool {
9759    matches!(s, Stmt::ExprStmt(e) if is_delegation_expr(e))
9760}
9761
9762/// Is the local ever ASSIGNED (write position: `x = ..`, `++x`)?
9763/// Distinct from a read count: a ctor's synthetic outer param is only
9764/// ever read; a param that gets written must stay declared.
9765pub(crate) fn local_is_written(body: &Stmt, var: u32) -> bool {
9766    let mut hit = false;
9767    visit_stmt_exprs_ro(body, &mut |e| {
9768        if hit {
9769            return;
9770        }
9771        visit_exprs(e, &mut |x| {
9772            if hit {
9773                return;
9774            }
9775            match x {
9776                Expr::Assign { target, .. }
9777                    if matches!(&**target, Expr::Local { var: v, .. } if *v == var) =>
9778                {
9779                    hit = true;
9780                }
9781                Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. }
9782                    if matches!(&**e, Expr::Local { var: v, .. } if *v == var) =>
9783                {
9784                    hit = true;
9785                }
9786                _ => {}
9787            }
9788        });
9789    });
9790    hit
9791}
9792
9793/// Source-form normalization for a non-static member-inner constructor.
9794/// The dex descriptor carries the synthetic outer instance as args[0]
9795/// (typed as the direct enclosing class); the emitter's qualified
9796/// `outer.new Inner(..)` / `this.new Inner(..)` sites pass it
9797/// implicitly and `super(..)` delegations drop it, so the signature
9798/// loses the param — and the body must stop referencing it: plain uses
9799/// become `Outer.this` (a `this.this$0 = p` assignment becomes
9800/// `this.this$0 = Outer.this`, exactly the runtime value), and
9801/// this()-delegations to `eligible` classes drop the leading param arg.
9802pub(crate) fn rewrite_inner_ctor_outer_param(
9803    body: &mut Stmt,
9804    param0: u32,
9805    outer_display: &str,
9806    outer_ty: &TypeRef,
9807    eligible: &[String],
9808) {
9809    // this()/super() delegations (and lost-alloc construction calls):
9810    // the leading arg IS the synthetic outer exactly when it is the
9811    // param itself.
9812    walk_stmt_exprs(body, &mut |e| {
9813        if let Expr::Method { name, cls, args, .. } = e {
9814            if &**name == "<init>"
9815                && eligible.iter().any(|c| c.as_str() == cls.as_ref())
9816                && args.first().is_some_and(|a| matches!(a, Expr::Local { var: v, .. } if *v == param0))
9817            {
9818                args.remove(0);
9819            }
9820        }
9821    });
9822    // Every remaining read of the param becomes `Outer.this`.
9823    let text = format!("{outer_display}.this");
9824    let ty = outer_ty.clone();
9825    walk_stmt_exprs(body, &mut |e| {
9826        deep_rewrite_reads(e, &mut |x| {
9827            if let Expr::Local { var: v, .. } = x {
9828                if *v == param0 {
9829                    *x = Expr::RawT(text.clone(), ty.clone());
9830                }
9831            }
9832        });
9833    });
9834}
9835
9836/// Ctors whose delegation is buried in control flow or behind arg
9837/// computations — the "对super的调用必须是构造器中的第一个语句" family
9838/// that plain hoisting cannot reach (weixin 264 / weibo 116 / reqable 71
9839/// / lark 62 after round 60): R8/Kotlin compute super-args conditionally
9840/// and dex legally invokes super mid-branch.
9841///
9842/// Shape B (branch merge): `prelude; if (c) {d1; super(A1); t1} else
9843/// {d2; super(A2); t2}; post` → one leading `super(..)` where each
9844/// differing arg becomes `c ? a1i : a2i`; branch remainders stay an
9845/// if/else AFTER the call; prelude statements move after it (nothing
9846/// may precede super in Java; bytecode pre-super work is local-only by
9847/// construction — dex forbids instance-field reads before the delegate).
9848///
9849/// Shape A (linear): `defs; super(A); rest` where `A` references the
9850/// def locals — plain hoisting (fix_ctor_super_first case 1) would move
9851/// the call above its argument definitions (forward refs). Inline the
9852/// referenced defs into the args first, drop the consumed defs, hoist.
9853/// Runs BEFORE fix_ctor_super_first and only fires when args reference
9854/// preceding locals, leaving the battle-tested plain hoist untouched
9855/// otherwise.
9856///
9857/// Inlining duplicates the def per use site, so a side-effecting def is
9858/// inlined only when its var's use count is provably preserved: a
9859/// single use in a merged arg (1 copy), or a single use in the
9860/// condition with ≤1 differing arg (the cond then appears in the `if`
9861/// plus one ternary — 2 copies of e.g. one Kotlin getter call, which
9862/// the original bytecode typically made twice anyway: `p.u() == null ?
9863/// .. : p.u().i()`). Pure defs inline freely. Every other shape aborts:
9864/// broken-but-faithful stays the status quo, wrong evaluation counts
9865/// would be worse.
9866/// Linear `def; delegation(args read def)` shapes. A plain hoist of the
9867/// delegation past its prefix would place the call ABOVE a definition
9868/// it reads — a forward reference (`super(context2, ..)` with `context2
9869/// = ..` below it). javac's attribution for the whole class then
9870/// collapses: the undefined identifier cascades into "non-static
9871/// super" and even bare `Object` resolution failures, poisoning every
9872/// later diagnostic in the file (weibo AppCompatTextView — the root of
9873/// a 4.1k-file Object cascade). The original source had the def's
9874/// expression INLINE in the delegation args; d8 computed it into a
9875/// register first. Inline the def into the args (the def is read
9876/// exactly once there — evaluation count preserved even for calls),
9877/// and when the register is REASSIGNED later (a fresh generation
9878/// sharing the slot), convert that first write into the declaration so
9879/// the var stays defined for its later readers. Any shape that cannot
9880/// be proven aborts untouched.
9881/// Count WRITE positions of `var` (assign / inc-dec targets) — the
9882/// shape-C validator needs exact counts, not just presence.
9883fn stmts_count_writes(stmts: &[Stmt], var: u32) -> usize {
9884    let mut n = 0usize;
9885    for s in stmts {
9886        visit_stmt_exprs_ro(s, &mut |e| {
9887            visit_exprs(e, &mut |x| match x {
9888                Expr::Assign { target, .. }
9889                    if matches!(&**target, Expr::Local { var: v, .. } if *v == var) =>
9890                {
9891                    n += 1;
9892                }
9893                Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. }
9894                    if matches!(&**e, Expr::Local { var: v, .. } if *v == var) =>
9895                {
9896                    n += 1;
9897                }
9898                _ => {}
9899            });
9900        });
9901    }
9902    n
9903}
9904
9905/// Collect every local WRITTEN in `stmts` — the complement inside a
9906/// ctor prefix is the param set (the only locals a pre-delegation arg
9907/// expression may read).
9908fn prefix_written_vars(stmts: &[Stmt]) -> jdc_core::FxHashSet<u32> {
9909    let mut set = jdc_core::FxHashSet::default();
9910    for s in stmts {
9911        let mut c = s.clone();
9912        walk_stmt_exprs(&mut c, &mut |e| {
9913            deep_rewrite(e, &mut |x| match x {
9914                Expr::Assign { target, .. } => {
9915                    if let Expr::Local { var, .. } = &**target {
9916                        set.insert(*var);
9917                    }
9918                }
9919                Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => {
9920                    if let Expr::Local { var, .. } = &**e {
9921                        set.insert(*var);
9922                    }
9923                }
9924                _ => {}
9925            });
9926        });
9927        if let Stmt::LocalDef { var, .. } = s {
9928            set.insert(*var);
9929        }
9930    }
9931    set
9932}
9933
9934/// Is `e` safe to evaluate INSIDE a ctor-delegation argument (before
9935/// this/super exists)? Params/consts/pure ops only — no instance-field
9936/// reads, no `this`, no captures.
9937fn pre_this_safe(e: &Expr, prefix_vars: &jdc_core::FxHashSet<u32>) -> bool {
9938    match e {
9939        Expr::Local { var, .. } => !prefix_vars.contains(var),
9940        Expr::Const(_) => true,
9941        Expr::Cast { e, .. } => pre_this_safe(e, prefix_vars),
9942        Expr::Bin { l, r, .. } => {
9943            pre_this_safe(l, prefix_vars) && pre_this_safe(r, prefix_vars)
9944        }
9945        Expr::Cond { c, t, f } => {
9946            pre_this_safe(c, prefix_vars)
9947                && pre_this_safe(t, prefix_vars)
9948                && pre_this_safe(f, prefix_vars)
9949        }
9950        Expr::Field { owner, is_static, .. } => {
9951            *is_static
9952                && owner
9953                    .as_ref()
9954                    .is_none_or(|o| pre_this_safe(o, prefix_vars))
9955        }
9956        Expr::Method {
9957            owner,
9958            args,
9959            is_static,
9960            ..
9961        } => {
9962            args.iter().all(|a| pre_this_safe(a, prefix_vars))
9963                && match owner {
9964                    Some(o) => pre_this_safe(o, prefix_vars),
9965                    None => *is_static,
9966                }
9967        }
9968        // Allocation in a delegation arg is legal Java before this()
9969        // (the ctor call touches no instance state of THIS class) — the
9970        // missing arm sent every `new Date()`-style Kotlin default
9971        // through `_ => false`, rejecting the whole fold (lark
9972        // Request$AppToApp this-not-first family, ~650 lark lines).
9973        // An inner-class construction needing an outer `this` carries
9974        // Expr::This in its args, which still fails below.
9975        Expr::New { args, .. } => args.iter().all(|a| pre_this_safe(a, prefix_vars)),
9976        Expr::NewArray { dims, init, .. } => {
9977            dims.iter().all(|d| pre_this_safe(d, prefix_vars))
9978                && init
9979                    .as_ref()
9980                    .map(|l| l.iter().all(|x| pre_this_safe(x, prefix_vars)))
9981                    .unwrap_or(true)
9982        }
9983        // Pure unary/instanceof/array reads.
9984        Expr::Un { e, .. } | Expr::InstanceOf { e, .. } => pre_this_safe(e, prefix_vars),
9985        Expr::ArrayIndex { array, index } => {
9986            pre_this_safe(array, prefix_vars) && pre_this_safe(index, prefix_vars)
9987        }
9988        _ => false,
9989    }
9990}
9991
9992/// `s` is exactly one plain assignment to local `v` (optionally wrapped
9993/// in a singleton Block): the branch shape of a Kotlin default-arg
9994/// override.
9995fn single_assign_to_v(s: &Stmt, v: u32) -> Option<Expr> {
9996    match s {
9997        Stmt::ExprStmt(Expr::Assign {
9998            target,
9999            op: AssignOp::Plain,
10000            value,
10001        }) if matches!(&**target, Expr::Local { var: vv, .. } if *vv == v) => {
10002            Some((**value).clone())
10003        }
10004        Stmt::Block(xs) if xs.len() == 1 => single_assign_to_v(&xs[0], v),
10005        _ => None,
10006    }
10007}
10008
10009/// Kotlin default-argument BRIDGE ctors (shape C of the delegation
10010/// family): `<init>(params.., int mask, DefaultConstructorMarker)`
10011/// computes each defaulted arg into a local — `v = p; if ((mask&bit)
10012/// != 0) { v = DEFAULT; }` — then delegates `this(.., v, ..)`. Java
10013/// allows NOTHING before the delegation, so the computations must fold
10014/// INTO the args (`this(.., (mask&bit) != 0 ? DEFAULT : p, ..)`).
10015/// Unfolded, the hoist passes lift the delegation above the raw local
10016/// reads: "找不到符号 变量 str2/v4/creationExtras2" — 2.5k root files
10017/// across weixin/lark/weibo (weixin MvvmObserverOwner$..Observer, lark
10018/// SimpleArrayMap/LongSparseArray). Per-var provability: exactly one
10019/// top-level base write, an optional single conditional override whose
10020/// branches are lone assignments, no other prefix touch of the var,
10021/// nothing after the delegation touches it, and the folded expression
10022/// is pre-this-safe. Unprovable vars stay untouched (partial folds are
10023/// strictly better — the rest was broken before too).
10024/// Flatten nested plain Blocks among the top-level statements. The
10025/// structurer hands ctor bodies as region-nested blocks (`Block([def,
10026/// Block([assign, if]), Block([delegate, return])])` — the Kotlin
10027/// bridge shape), and every delegation-repair pass scans the TOP level
10028/// only. Straight-line nesting flattens without semantics; control
10029/// flow lives inside If/While/etc. arms, never as a direct member.
10030pub(crate) fn flatten_top_blocks(stmts: &mut Vec<Stmt>) {
10031    fn rec(s: Stmt, out: &mut Vec<Stmt>) {
10032        match s {
10033            Stmt::Block(inner) => {
10034                for x in inner {
10035                    rec(x, out);
10036                }
10037            }
10038            other => out.push(other),
10039        }
10040    }
10041    if !stmts.iter().any(|s| matches!(s, Stmt::Block(_))) {
10042        return;
10043    }
10044    let mut out: Vec<Stmt> = Vec::with_capacity(stmts.len());
10045    for s in std::mem::take(stmts) {
10046        rec(s, &mut out);
10047    }
10048    *stmts = out;
10049}
10050
10051fn fold_default_arg_bridge(stmts: &mut Vec<Stmt>) -> bool {
10052    flatten_top_blocks(stmts);
10053    let Some(pos) = stmts.iter().position(is_bare_ctor_call) else {
10054        return false;
10055    };
10056    if pos == 0 {
10057        return false;
10058    }
10059    let Stmt::ExprStmt(Expr::Method { args, .. }) = &stmts[pos] else {
10060        return false;
10061    };
10062    let mut arg_vars: Vec<u32> = Vec::new();
10063    let mut reads = 0usize;
10064    for a in args {
10065        let mut c = a.clone();
10066        deep_rewrite_reads(&mut c, &mut |x| {
10067            if let Expr::Local { var, .. } = x {
10068                reads += 1;
10069                if !arg_vars.contains(var) {
10070                    arg_vars.push(*var);
10071                }
10072            }
10073        });
10074    }
10075    if arg_vars.is_empty() || reads != arg_vars.len() {
10076        return false; // a var read twice would duplicate evaluation
10077    }
10078    // The bridge ENDS at the delegation: nothing after may touch the
10079    // arg vars (reads OR writes).
10080    for &v in &arg_vars {
10081        if stmts_read_var(&stmts[pos + 1..], v) != 0
10082            || stmts_count_writes(&stmts[pos + 1..], v) != 0
10083        {
10084            return false;
10085        }
10086    }
10087    let prefix_vars = prefix_written_vars(&stmts[..pos]);
10088    let mut repl: Vec<(u32, Expr)> = Vec::new();
10089    let mut drop: Vec<usize> = Vec::new();
10090    for &v in &arg_vars {
10091        // Exact prefix shape for v.
10092        let mut base: Option<(usize, Expr)> = None;
10093        let mut decl: Option<usize> = None;
10094        let mut condf: Option<(usize, Expr, Option<Expr>, Option<Expr>)> = None;
10095        let mut ok = true;
10096        for (i, s) in stmts[..pos].iter().enumerate() {
10097            match s {
10098                Stmt::LocalDef { var, init, .. } if *var == v => match init {
10099                    None => {
10100                        if decl.is_some() {
10101                            ok = false;
10102                            break;
10103                        }
10104                        decl = Some(i);
10105                    }
10106                    Some(e) => {
10107                        if base.is_some() {
10108                            ok = false;
10109                            break;
10110                        }
10111                        base = Some((i, e.clone()));
10112                    }
10113                },
10114                Stmt::ExprStmt(Expr::Assign {
10115                    target,
10116                    op: AssignOp::Plain,
10117                    value,
10118                }) if matches!(&**target, Expr::Local { var: vv, .. } if *vv == v) => {
10119                    if base.is_some() {
10120                        ok = false;
10121                        break;
10122                    }
10123                    base = Some((i, (**value).clone()));
10124                }
10125                Stmt::If {
10126                    cond,
10127                    then_stmt,
10128                    else_stmt,
10129                } => {
10130                    let t = single_assign_to_v(then_stmt, v);
10131                    let f = else_stmt.as_ref().and_then(|b| single_assign_to_v(b, v));
10132                    let then_touches = t.is_some()
10133                        || stmts_read_var(std::slice::from_ref(&**then_stmt), v) != 0
10134                        || stmts_count_writes(std::slice::from_ref(&**then_stmt), v) != 0;
10135                    let else_touches = else_stmt.as_ref().is_some_and(|b| {
10136                        f.is_some()
10137                            || stmts_read_var(std::slice::from_ref(b), v) != 0
10138                            || stmts_count_writes(std::slice::from_ref(b), v) != 0
10139                    });
10140                    if !then_touches && !else_touches {
10141                        continue; // unrelated if
10142                    }
10143                    // Both branches must be lone assignments (a branch
10144                    // with its own delegation/return is the conditional-
10145                    // super machinery's shape, not this one), the cond
10146                    // must not read v, and only one override per var.
10147                    if condf.is_some()
10148                        || base.is_none()
10149                        || (then_touches && t.is_none())
10150                        || (else_touches && f.is_none())
10151                    {
10152                        ok = false;
10153                        break;
10154                    }
10155                    let cond_stmt = Stmt::ExprStmt(cond.clone());
10156                    if stmts_read_var(std::slice::from_ref(&cond_stmt), v) != 0 {
10157                        ok = false;
10158                        break;
10159                    }
10160                    condf = Some((i, cond.clone(), t, f));
10161                }
10162                _ => {}
10163            }
10164        }
10165        if !ok {
10166            continue;
10167        }
10168        let Some((bi, be)) = base else { continue };
10169        // Exact write/read accounting: base (+then/else) writes, zero
10170        // prefix reads.
10171        let want_writes = 1
10172            + condf
10173                .as_ref()
10174                .map(|(_, _, t, f)| t.is_some() as usize + f.is_some() as usize)
10175                .unwrap_or(0);
10176        if stmts_count_writes(&stmts[..pos], v) != want_writes
10177            || stmts_read_var(&stmts[..pos], v) != 0
10178        {
10179            continue;
10180        }
10181        let rep = match condf {
10182            Some((ci, c, t, f)) => {
10183                drop.push(ci);
10184                match (t, f) {
10185                    (Some(t), Some(f)) => Expr::Cond {
10186                        c: Box::new(c),
10187                        t: Box::new(t),
10188                        f: Box::new(f),
10189                    },
10190                    (Some(t), None) => Expr::Cond {
10191                        c: Box::new(c),
10192                        t: Box::new(t),
10193                        f: Box::new(be.clone()),
10194                    },
10195                    (None, Some(f)) => Expr::Cond {
10196                        c: Box::new(c),
10197                        t: Box::new(be.clone()),
10198                        f: Box::new(f),
10199                    },
10200                    (None, None) => unreachable!("condf requires a branch assign"),
10201                }
10202            }
10203            None => be.clone(),
10204        };
10205        if !pre_this_safe(&rep, &prefix_vars) {
10206            continue;
10207        }
10208        drop.push(bi);
10209        if let Some(d) = decl {
10210            drop.push(d);
10211        }
10212        repl.push((v, rep));
10213    }
10214    if repl.is_empty() {
10215        return false;
10216    }
10217    if let Stmt::ExprStmt(Expr::Method { args, .. }) = &mut stmts[pos] {
10218        for a in args.iter_mut() {
10219            deep_rewrite_reads(a, &mut |x| {
10220                if let Expr::Local { var, .. } = x {
10221                    if let Some((_, e)) = repl.iter().find(|(vv, _)| vv == var) {
10222                        *x = e.clone();
10223                    }
10224                }
10225            });
10226        }
10227    }
10228    drop.sort_unstable();
10229    drop.dedup();
10230    for i in drop.into_iter().rev() {
10231        stmts.remove(i);
10232    }
10233    true
10234}
10235
10236/// Rewrite static member references on Kotlin multi-file facade PARTS
10237/// (`StringsKt__StringsKt.trim(..)`) to the public facade the parts are
10238/// compiled into (`StringsKt.trim(..)`): the parts are package-private,
10239/// so every cross-package call site is "在kotlin.text中不是公共的; 无法
10240/// 从外部程序包中对其进行访问" (lark 1.4k root files; weibo's 8.4k-line
10241/// Kt cluster). Static members resolve through the facade by
10242/// inheritance (the map is gated on the extends chain).
10243pub fn rewrite_kotlin_facades(body: &mut Stmt, pool: &crate::DexPool) {
10244    let map = pool.kotlin_facade_map();
10245    if map.is_empty() {
10246        return;
10247    }
10248    walk_stmt_exprs(body, &mut |e| {
10249        match e {
10250            Expr::Method {
10251                cls,
10252                name,
10253                desc,
10254                is_static,
10255                ..
10256            } if *is_static => {
10257                if let Some((base, cover)) = map.get(cls.as_ref()) {
10258                    let hit = match cover {
10259                        None => true,
10260                        Some(c) => c.0.contains(&(name.to_string(), desc.to_string())),
10261                    };
10262                    if hit {
10263                        *cls = std::sync::Arc::from(base.as_str());
10264                    }
10265                }
10266            }
10267            Expr::Field {
10268                cls,
10269                name,
10270                is_static,
10271                ..
10272            } if *is_static => {
10273                if let Some((base, cover)) = map.get(cls.as_ref()) {
10274                    let hit = match cover {
10275                        None => true,
10276                        Some(c) => c.1.contains(name.as_ref()),
10277                    };
10278                    if hit {
10279                        *cls = std::sync::Arc::from(base.as_str());
10280                    }
10281                }
10282            }
10283            _ => {}
10284        }
10285    });
10286}
10287
10288/// Kotlin default-arg bridge fold for ENUM ctors: the synthetic
10289/// `(String, int, .., int, DefaultConstructorMarker)` ctor computes
10290/// defaulted args then chains `this(..)` mid-body — Java requires the
10291/// delegation first (对this的调用必须是构造器中的第一个语句, lark's
10292/// UserCustomStatusExtraParams$* family, 752 lines). Same fold the
10293/// class-ctor path runs; the enum branch of the pipeline used to skip
10294/// it (only strip_enum_ctor_super ran there).
10295pub fn fold_enum_default_arg_bridge(body: &mut Stmt) {
10296    let Stmt::Block(stmts) = body else { return };
10297    fold_default_arg_bridge(stmts);
10298}
10299
10300pub fn fix_ctor_delegation_arg_defs(body: &mut Stmt) {
10301    let Stmt::Block(stmts) = body else { return };
10302    // Shape C first: the Kotlin default-arg bridge (computations +
10303    // conditional overrides folded into the delegation args).
10304    fold_default_arg_bridge(stmts);
10305    // Only the top-level linear shape; the first statement needs no
10306    // repair and control flow ahead of the delegation belongs to the
10307    // conditional-super machinery.
10308    let Some(pos) = stmts.iter().position(is_bare_ctor_call) else {
10309        return;
10310    };
10311    if pos == 0 {
10312        return;
10313    }
10314    // Vars read by the delegation args, and the total read count (a var
10315    // read twice would duplicate a call evaluation on inline).
10316    let mut arg_vars: Vec<u32> = Vec::new();
10317    let mut total_reads = 0usize;
10318    {
10319        let Stmt::ExprStmt(Expr::Method { args, .. }) = &stmts[pos] else {
10320            return;
10321        };
10322        for a in args {
10323            let mut c = a.clone();
10324            deep_rewrite_reads(&mut c, &mut |x| {
10325                if let Expr::Local { var: v, .. } = x {
10326                    total_reads += 1;
10327                    if !arg_vars.contains(v) {
10328                        arg_vars.push(*v);
10329                    }
10330                }
10331            });
10332        }
10333    }
10334    if arg_vars.is_empty() || total_reads != arg_vars.len() {
10335        return;
10336    }
10337    // Vars DEFINED anywhere in the prefix (for the init-referenced
10338    // check below — an inlined init may not itself read prefix defs).
10339    let prefix_defs: Vec<u32> = stmts[..pos]
10340        .iter()
10341        .filter_map(|s| match s {
10342            Stmt::LocalDef { var, init: Some(_), .. } => Some(*var),
10343            _ => None,
10344        })
10345        .collect();
10346    // Per referenced var: exactly one prefix def, no other prefix
10347    // reader between def and delegation, the def's init reads no
10348    // prefix-def var, and the first post-delegation use is a top-level
10349    // write (becomes the declaration) or nothing at all.
10350    let mut plan: Vec<(u32, usize, Option<usize>)> = Vec::new();
10351    for v in arg_vars {
10352        let defs: Vec<usize> = stmts[..pos]
10353            .iter()
10354            .enumerate()
10355            .filter(|(i, _)| {
10356                matches!(&stmts[*i], Stmt::LocalDef { var, init: Some(_), .. } if *var == v)
10357            })
10358            .map(|(i, _)| i)
10359            .collect();
10360        if defs.len() > 1 {
10361            return; // ambiguous: multiple prefix defs
10362        }
10363        let Some(&q) = defs.first() else {
10364            continue; // a param: nothing to inline for this var
10365        };
10366        let init = match &stmts[q] {
10367            Stmt::LocalDef { init: Some(e), .. } => e.clone(),
10368            _ => return,
10369        };
10370        if stmts_read_var(&stmts[q + 1..pos], v) != 0 {
10371            return; // another reader between the def and the delegation
10372        }
10373        // The inlined init may not read a prefix-def var either — the
10374        // delegation lands ABOVE those defs after the hoist.
10375        let mut init_vars: Vec<u32> = Vec::new();
10376        deep_rewrite_reads(&mut { init.clone() }, &mut |x| {
10377            if let Expr::Local { var: vv, .. } = x {
10378                if !init_vars.contains(vv) {
10379                    init_vars.push(*vv);
10380                }
10381            }
10382        });
10383        if init_vars.iter().any(|iv| prefix_defs.contains(iv)) {
10384            return;
10385        }
10386        match stmts[pos + 1..]
10387            .iter()
10388            .position(|s| {
10389                matches!(s, Stmt::ExprStmt(Expr::Assign { target, .. })
10390                    if matches!(&**target, Expr::Local { var: vv, .. } if *vv == v))
10391            }) {
10392            Some(w) => {
10393                // Reads before the re-declaration would be dangling.
10394                if stmts_read_var(&stmts[pos + 1..pos + 1 + w], v) != 0 {
10395                    return;
10396                }
10397                plan.push((v, q, Some(pos + 1 + w)));
10398            }
10399            None => {
10400                // No later write: the def must be dead after inline.
10401                if stmts_read_var(&stmts[pos + 1..], v) != 0 {
10402                    return;
10403                }
10404                plan.push((v, q, None));
10405            }
10406        }
10407    }
10408    if plan.is_empty() {
10409        return;
10410    }
10411    // Inline the def inits into the delegation args.
10412    let values: Vec<(u32, Expr)> = plan
10413        .iter()
10414        .map(|(_v, q, _)| match &stmts[*q] {
10415            Stmt::LocalDef { var, init: Some(e), .. } => (*var, e.clone()),
10416            _ => unreachable!("plan entries carry a LocalDef with init"),
10417        })
10418        .collect();
10419    if let Stmt::ExprStmt(Expr::Method { args, .. }) = &mut stmts[pos] {
10420        for a in args {
10421            deep_rewrite_reads(a, &mut |x| {
10422                if let Expr::Local { var: v, .. } = x {
10423                    if let Some((_, e)) = values.iter().find(|(vv, _)| vv == v) {
10424                        *x = e.clone();
10425                    }
10426                }
10427            });
10428        }
10429    }
10430    // Convert the first writes into declarations BEFORE removing defs
10431    // (removals shift indices); then remove the defs in descending
10432    // order.
10433    for (v, _, w) in &plan {
10434        if let Some(w) = w {
10435            if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = &mut stmts[*w] {
10436                if matches!(&**target, Expr::Local { var: vv, .. } if vv == v) {
10437                    let taken = std::mem::replace(&mut **value, Expr::Const(ConstVal::Null));
10438                    stmts[*w] = Stmt::LocalDef {
10439                        var: *v,
10440                        init: Some(taken),
10441                        is_final: false,
10442                        force_type: false,
10443                    };
10444                }
10445            }
10446        }
10447    }
10448    let mut order: Vec<usize> = plan.iter().map(|(_, q, _)| *q).collect();
10449    order.sort_unstable();
10450    order.reverse();
10451    for q in order {
10452        stmts.remove(q);
10453    }
10454}
10455
10456/// An INT-typed method returning a boolean value: the register held the
10457/// boolean test result in one generation and the `if (v) 1 else 0` int
10458/// in another; booleanize picks the boolean generation and the plain
10459/// `return v;` breaks ("boolean无法转换为int", weixin Handle
10460/// onPausableTransaction — the source was `if (v) 1 else 0`). Wrap the
10461/// return in the recovering conditional.
10462/// Mirror of fix_int_returns: a BOOLEAN method returning a numeric-
10463/// typed value (register reuse left an int generation) wraps it
10464/// `x != 0` — the dex branch encoding, and the only legal Java form
10465/// (kh5/a `return v116_g53;` int无法转换为boolean family).
10466pub fn fix_bool_returns(vt: &VarTable, body: &mut Stmt) {
10467    walk_mut_deep(body, &mut |st| {
10468        if let Stmt::Return(Some(e)) = st {
10469            let numeric = match &*e {
10470                Expr::Local { var, ty } => {
10471                    let t = if (*var as usize) < vt.vars.len() {
10472                        vt.var(*var).ty.erased()
10473                    } else {
10474                        ty.erased()
10475                    };
10476                    matches!(
10477                        t,
10478                        JavaType::Int
10479                            | JavaType::Long
10480                            | JavaType::Short
10481                            | JavaType::Byte
10482                            | JavaType::Char
10483                    )
10484                }
10485                other => matches!(
10486                    other.type_ref().erased(),
10487                    JavaType::Int | JavaType::Long | JavaType::Short | JavaType::Byte
10488                        | JavaType::Char
10489                ),
10490            };
10491            if numeric {
10492                let taken = e.clone();
10493                *e = Expr::Bin {
10494                    op: BinOp::Ne,
10495                    l: Box::new(taken),
10496                    r: Box::new(Expr::Const(ConstVal::Int(0))),
10497                    ty: Some(TypeRef::J(JavaType::Boolean)),
10498                };
10499            }
10500        }
10501    });
10502}
10503
10504pub fn fix_int_returns(vt: &VarTable, body: &mut Stmt) {
10505    walk_mut_deep(body, &mut |st| {
10506        if let Stmt::Return(Some(e)) = st {
10507            let is_bool = match e {
10508                Expr::Local { var, .. } => {
10509                    (*var as usize) < vt.vars.len()
10510                        && vt.vars[*var as usize].ty.erased() == JavaType::Boolean
10511                }
10512                Expr::Method { desc, .. } => desc.ret == JavaType::Boolean,
10513                _ => false,
10514            };
10515            if is_bool {
10516                *e = Expr::Cond {
10517                    c: Box::new(e.clone()),
10518                    t: Box::new(Expr::Const(ConstVal::Int(1))),
10519                    f: Box::new(Expr::Const(ConstVal::Int(0))),
10520                };
10521            }
10522        }
10523    });
10524}
10525
10526/// Generation splitting: when an assignment's value type is
10527/// INCOMPATIBLE with the var's declared type, mint a FRESH var for the
10528/// new generation instead of widening (widening was measured twice as
10529/// net-negative — every use-site cast gap re-triggers javac's
10530/// error-type contagion). Reads after the split point follow the
10531/// active generation; control-flow joins intersect; loops run a
10532/// 3-round fixpoint so the back-edge carries the tail generation to
10533/// the head reads (the loop-state-tuple shape, qf5/v0's
10534/// `u0 = (qf5.u0) get;` … `u0 = looper2;`).
10535///
10536/// Split condition (reference incompatibility or a primitive/value
10537/// mix — the instanceof-corruption shape): both specific references of
10538/// different classes, or prim→ref. NOT numeric-vs-numeric (promotion
10539/// handles those) and NOT Object targets (assignable — the narrowing
10540/// cast pass covers the value side).
10541pub fn split_generations(vt: &mut VarTable, body: &mut Stmt, pool: &DexPool) {
10542    let mut counter: u32 = 0;
10543    let mut gen: std::collections::HashMap<u32, u32> = std::collections::HashMap::default();
10544    split_walk_stmt(body, vt, &mut gen, &mut counter, pool);
10545}
10546
10547/// Should the assignment `target = value` split the target?
10548fn split_needed(vt: &VarTable, target: u32, value: &Expr, pool: &DexPool) -> bool {
10549    let n = vt.vars.len();
10550    if target as usize >= n {
10551        return false;
10552    }
10553    let declared = vt.vars[target as usize].ty.erased();
10554    let vt_is_ref = |t: &JavaType| matches!(t, JavaType::Object(_) | JavaType::Array(_));
10555    // Locals consult the VarTable: the embedded ty is a lift-time
10556    // snapshot, stale after booleanize converts a copy SOURCE (the
10557    // post-booleanize re-split below relies on seeing `v131` as the
10558    // boolean it became). Bitwise Or/And/Xor over boolean operands
10559    // carry the same staleness at the EXPRESSION level (dex lowered
10560    // Kotlin's non-short-circuit `or` to int `|` — the embedded Bin ty
10561    // stays Int after booleanize converts the operands): `int v18 =
10562    // delete | delete2` must split the target generation.
10563    fn side_bool(e: &Expr, vt: &VarTable) -> bool {
10564        match e {
10565            Expr::Local { var, .. } if (*var as usize) < vt.vars.len() => {
10566                matches!(vt.var(*var).ty.erased(), JavaType::Boolean)
10567            }
10568            Expr::Const(ConstVal::Int(0 | 1)) => true,
10569            Expr::Bin { op: BinOp::Or | BinOp::And | BinOp::Xor, l, r, .. } => {
10570                side_bool(l, vt) && side_bool(r, vt)
10571            }
10572            other => matches!(other.type_ref().erased(), JavaType::Boolean),
10573        }
10574    }
10575    let value_ty = match value {
10576        Expr::Local { var, ty } if (*var as usize) < vt.vars.len() => vt.var(*var).ty.erased(),
10577        Expr::Bin {
10578            op: BinOp::Or | BinOp::And | BinOp::Xor,
10579            l,
10580            r,
10581            ..
10582        } if side_bool(l, vt) && side_bool(r, vt) => JavaType::Boolean,
10583        other => other.type_ref().erased(),
10584    };
10585    if vt_is_ref(&declared) && vt_is_ref(&value_ty) {
10586        // Both references: split only when incompatible — different
10587        // classes where neither side is Object AND the value is not a
10588        // SUBTYPE of the declared type. An assignable store needs no
10589        // fresh generation; splitting one minted an orphan at an
10590        // if-join whose sibling arm kept the base (rename_gen_back's
10591        // primitive-only gate refused the undo, drop_dead_locals then
10592        // stripped the orphan to a bare allocation): pq0/g's bridge
10593        // `map2 = new LinkedHashMap()` against the HashMap-typed phi
10594        // rendered `new LinkedHashMap();` and the arm lost its store.
10595        let obj = JavaType::Object("java/lang/Object".into());
10596        if declared == value_ty || declared == obj || value_ty == obj {
10597            return false;
10598        }
10599        // A FRESH ALLOCATION whose class relation to the declared type
10600        // is UNKNOWABLE (a framework↔framework pair — LinkedHashMap
10601        // into a HashMap-typed phi): the dex stored the instance into
10602        // this register flow, so the store was verifier-legal; the
10603        // split cannot know better and orphaned the generation (the
10604        // if-join undo is primitive-only; the dropper stripped the
10605        // orphan to a bare `new LinkedHashMap();` and pq0/g's bridge
10606        // lost the else-arm store, killing the whole branched-super
10607        // merge). Pool-known pairs KEEP the split: the generation's
10608        // precise type is what downstream subtype sinks need (weibo
10609        // `v7 = new $reportWhenComplete$1(..)` into a ContinuationImpl
10610        // phi — exempting it made `v9($1) = v7` inconvertible, +50).
10611        if let (JavaType::Object(v), JavaType::Object(d)) = (&value_ty, &declared) {
10612            if matches!(value, Expr::New { .. })
10613                && (pool.get(v).is_none() || pool.get(d).is_none())
10614            {
10615                return false;
10616            }
10617        }
10618        true
10619    } else {
10620        // Primitive/reference mix: the value generation has a kind the
10621        // target can never accept — split (the declared type stays with
10622        // the old generation's readers).
10623        if vt_is_ref(&declared) != vt_is_ref(&value_ty) {
10624            return true;
10625        }
10626        // Boolean/numeric mix: `boolean` and `int` have NO cast between
10627        // them in Java, so a register reused across the kinds can only
10628        // be expressed as two generations (weixin ConstraintLayout
10629        // `boolean v60; int v56; v60 = v56;` — 9.5k bool↔int lines once
10630        // the obscuring suppression lifted). 0/1 constants are the
10631        // boolean encoding, not a numeric generation — never split on
10632        // those (booleanize owns them).
10633        let d_bool = matches!(declared, JavaType::Boolean);
10634        let v_bool = matches!(value_ty, JavaType::Boolean);
10635        let zero_one = matches!(value, Expr::Const(ConstVal::Int(0 | 1)));
10636        d_bool != v_bool && !zero_one
10637    }
10638}
10639
10640fn rewrite_gen_reads(e: &mut Expr, gen: &std::collections::HashMap<u32, u32>, vt: &VarTable) {
10641    deep_rewrite(e, &mut |x| {
10642        if let Expr::Local { var, ty } = x {
10643            if let Some(&to) = gen.get(var) {
10644                if to != *var {
10645                    *var = to;
10646                    *ty = vt.var(to).ty.clone();
10647                }
10648            }
10649        }
10650    });
10651}
10652
10653fn do_split(
10654    vt: &mut VarTable,
10655    target: &mut Expr,
10656    old: u32,
10657    value_ty: &JavaType,
10658    gen: &mut std::collections::HashMap<u32, u32>,
10659    counter: &mut u32,
10660) {
10661    *counter += 1;
10662    let info = &vt.vars[old as usize];
10663    let id = vt.vars.len() as u32;
10664    let name = format!("{}_g{}", info.name, counter);
10665    let slot = info.slot;
10666    // A split generation is a fresh LOCAL, not a parameter — even when
10667    // the parent register is one (the param identity stays on the
10668    // original var id; the generation only carries a later write). The
10669    // inherited flag made booleanize (and every other param-skip pass)
10670    // permanently ignore param-register generations: weixin v2/i's
10671    // `int v264_g160_g7 = !v200` with `!= 0` reads (bool→int assign
10672    // family, 2.8k lines).
10673    let is_param = false;
10674    vt.vars.push(jdc_core::var::VarInfo {
10675        id,
10676        slot,
10677        name,
10678        ty: TypeRef::J(value_ty.clone()),
10679        is_param,
10680        range_start: 0,
10681        range_end: u16::MAX,
10682        synthetic_name: true,
10683    });
10684    while vt.by_slot.len() <= slot as usize {
10685        vt.by_slot.push(Vec::new());
10686    }
10687    vt.by_slot[slot as usize].push((0, u16::MAX, id));
10688    gen.insert(old, id);
10689    if let Expr::Local { var, ty, .. } = target {
10690        *var = id;
10691        *ty = TypeRef::J(value_ty.clone());
10692    }
10693}
10694
10695/// Undo a DIVERGED generation split: rewrite every occurrence of the
10696/// freshly-minted generation `from` back to the pre-if var `to` within
10697/// `s` (both read positions and assignment targets, plus LocalDef var
10698/// fields). At a control-flow join only splits BOTH branches produced
10699/// survive (the phi merges on the shared var); a gen one branch minted
10700/// alone does not. But `do_split` already rewrote that branch's write
10701/// target IN PLACE, so without this undo the branch stores into a
10702/// write-only orphan gen that no confluence reader ever reads — the
10703/// value is silently lost AND the orphan renders undeclared (kt5.b
10704/// Kotlin default-arg bridge: `v18_g1 = p6x` in then vs `v18 = false`
10705/// in else, `this(.., v18, ..)` reads only else's value).
10706fn rename_gen_back(s: &mut Stmt, from: u32, to: u32, vt: &VarTable) {
10707    if from == to || (to as usize) >= vt.vars.len() {
10708        return;
10709    }
10710    let to_ty = vt.vars[to as usize].ty.clone();
10711    walk_mut_deep(s, &mut |st| {
10712        if let Stmt::LocalDef { var, .. } = st {
10713            if *var == from {
10714                *var = to;
10715            }
10716        }
10717    });
10718    rewrite_exprs(s, &mut |e| {
10719        deep_rewrite(e, &mut |x| {
10720            if let Expr::Local { var, ty } = x {
10721                if *var == from {
10722                    *var = to;
10723                    *ty = to_ty.clone();
10724                }
10725            }
10726        });
10727    });
10728}
10729
10730/// Remove an undone generation's `by_slot` segment entry so slot-based
10731/// resolution can never hand back the now-unreferenced gen id (its
10732/// VarInfo stays — ids index `vars` — but nothing may select it).
10733fn drop_gen_slot(vt: &mut VarTable, dead: u32) {
10734    if (dead as usize) >= vt.vars.len() {
10735        return;
10736    }
10737    let slot = vt.vars[dead as usize].slot as usize;
10738    if let Some(seg) = vt.by_slot.get_mut(slot) {
10739        seg.retain(|&(_, _, id)| id != dead);
10740    }
10741}
10742
10743/// Type gate for `rename_gen_back`: merging gen `v` back onto base `k`
10744/// is only safe when the base can carry the gen's value — identical
10745/// types, or BOTH primitives (the bool/int register-reuse family that
10746/// booleanize + the cast fixers reconcile; the Kotlin bridge case,
10747/// kt5.b `int v18` receiving `boolean p6x`). A reference-vs-primitive
10748/// or cross-class merge would forge `String k = <boolean>`-style
10749/// assignments that never compile (the lark/weibo/reqable regression
10750/// of the ungated first cut: +8.4k `不兼容的类型`).
10751fn undo_type_safe(vt: &VarTable, k: u32, v: u32) -> bool {
10752    if (k as usize) >= vt.vars.len() || (v as usize) >= vt.vars.len() {
10753        return false;
10754    }
10755    let kt = vt.vars[k as usize].ty.erased();
10756    let vt_ty = vt.vars[v as usize].ty.erased();
10757    if kt == vt_ty {
10758        return true;
10759    }
10760    let is_prim = |t: &JavaType| {
10761        matches!(
10762            t,
10763            JavaType::Int
10764                | JavaType::Long
10765                | JavaType::Short
10766                | JavaType::Byte
10767                | JavaType::Char
10768                | JavaType::Float
10769                | JavaType::Double
10770                | JavaType::Boolean
10771        )
10772    };
10773    is_prim(&kt) && is_prim(&vt_ty)
10774}
10775
10776fn split_walk_stmt(
10777    st: &mut Stmt,
10778    vt: &mut VarTable,
10779    gen: &mut std::collections::HashMap<u32, u32>,
10780    counter: &mut u32,
10781    pool: &DexPool,
10782) {
10783    match st {
10784        Stmt::Block(v) => {
10785            for x in v.iter_mut() {
10786                split_walk_stmt(x, vt, gen, counter, pool);
10787            }
10788        }
10789        Stmt::ExprStmt(e) => {
10790            rewrite_gen_reads(e, gen, vt);
10791            if let Expr::Assign {
10792                target,
10793                value,
10794                op: AssignOp::Plain,
10795                ..
10796            } = e
10797            {
10798                if let Expr::Local { var, .. } = &**target {
10799                    let old = *var;
10800                    if split_needed(vt, old, value, pool) {
10801                        let value_ty = value.type_ref().erased();
10802                        do_split(vt, target, old, &value_ty, gen, counter);
10803                    }
10804                }
10805            }
10806        }
10807        Stmt::LocalDef { var, init, .. } => {
10808            if let Some(e) = init {
10809                rewrite_gen_reads(e, gen, vt);
10810                if split_needed(vt, *var, e, pool) {
10811                    // The DEF mints its own generation: retarget the def
10812                    // to a fresh var (the bare decl of the ORIGINAL stays
10813                    // for its earlier readers).
10814                    let value_ty = e.type_ref().erased();
10815                    let old = *var;
10816                    let mut fake = Expr::Local {
10817                        var: old,
10818                        ty: vt.vars[old as usize].ty.clone(),
10819                    };
10820                    do_split(vt, &mut fake, old, &value_ty, gen, counter);
10821                    if let Expr::Local { var: nv, .. } = fake {
10822                        *var = nv;
10823                    }
10824                }
10825            }
10826        }
10827        Stmt::If {
10828            cond,
10829            then_stmt,
10830            else_stmt,
10831            ..
10832        } => {
10833            rewrite_gen_reads(cond, gen, vt);
10834            let then_floor = vt.vars.len() as u32;
10835            let mut g_then = gen.clone();
10836            split_walk_stmt(then_stmt, vt, &mut g_then, counter, pool);
10837            let else_floor = vt.vars.len() as u32;
10838            let mut g_else = gen.clone();
10839            if let Some(e) = else_stmt {
10840                split_walk_stmt(e, vt, &mut g_else, counter, pool);
10841            }
10842            // Undo splits that DIVERGED at this join (see
10843            // rename_gen_back), with TWO gates learned the hard way
10844            // (first cut regressed lark +8.4k):
10845            //  1. the SIBLING KEPT THE BASE — `g_else[k]` equals the
10846            //     entry mapping, i.e. only this branch minted a fresh
10847            //     gen for k. When both branches split, each orphan is
10848            //     declared by ensure_declared at its own value type and
10849            //     compiles; renaming them back onto the base would
10850            //     force cross-kind assignments (`int k = <boolean>`).
10851            //  2. the fresh gen is WRITE-ONLY in the branch — a gen
10852            //     read inside its branch renders fine on its own.
10853            // The gated shape is the Kotlin default-arg bridge phi
10854            // loss: then `v_g = p6x` / else `v = false` / confluence
10855            // reads v — the then value was silently dropped AND v_g
10856            // rendered undeclared (kt5.b). Undo restores the clean
10857            // diamond so fold_default_arg_bridge can fire.
10858            // Candidates first (the read-free conditions), then ONE
10859            // early-exit probe for the small candidate set — the old
10860            // read_counts_all rebuilt a HashMap over the WHOLE branch
10861            // subtree per diverged If (quadratic in nesting depth;
10862            // together with the split walk it was ~1/3 of worker CPU on
10863            // the QQ sample profile). The map was only ever consulted
10864            // as `reads(v) == 0` for these candidates.
10865            let mut undo_then: Vec<(u32, u32)> = g_then
10866                .iter()
10867                .filter(|(&k, &v)| {
10868                    v >= then_floor
10869                        && v < else_floor
10870                        && g_else.get(&k).copied() == gen.get(&k).copied()
10871                        && undo_type_safe(vt, k, v)
10872                })
10873                .map(|(&k, &v)| (v, k))
10874                .collect();
10875            if else_floor > then_floor && !undo_then.is_empty() {
10876                let vars: Vec<u32> = undo_then.iter().map(|&(v, _)| v).collect();
10877                let mut hits = vec![false; vars.len()];
10878                mark_read_vars(std::slice::from_ref(&**then_stmt), &vars, &mut hits);
10879                let mut i = 0usize;
10880                undo_then.retain(|_| {
10881                    let keep = !hits[i];
10882                    i += 1;
10883                    keep
10884                });
10885            }
10886            undo_then.sort_unstable_by_key(|&(v, _)| std::cmp::Reverse(v));
10887            for &(v, k) in &undo_then {
10888                rename_gen_back(then_stmt, v, k, vt);
10889                drop_gen_slot(vt, v);
10890            }
10891            if let Some(e) = else_stmt.as_deref_mut() {
10892                let mut undo_else: Vec<(u32, u32)> = g_else
10893                    .iter()
10894                    .filter(|(&k, &v)| {
10895                        v >= else_floor
10896                            && g_then.get(&k).copied() == gen.get(&k).copied()
10897                            && undo_type_safe(vt, k, v)
10898                    })
10899                    .map(|(&k, &v)| (v, k))
10900                    .collect();
10901                if (vt.vars.len() as u32) > else_floor && !undo_else.is_empty() {
10902                    let vars: Vec<u32> = undo_else.iter().map(|&(v, _)| v).collect();
10903                    let mut hits = vec![false; vars.len()];
10904                    mark_read_vars(std::slice::from_ref(&*e), &vars, &mut hits);
10905                    let mut i = 0usize;
10906                    undo_else.retain(|_| {
10907                        let keep = !hits[i];
10908                        i += 1;
10909                        keep
10910                    });
10911                }
10912                undo_else.sort_unstable_by_key(|&(v, _)| std::cmp::Reverse(v));
10913                for &(v, k) in &undo_else {
10914                    rename_gen_back(e, v, k, vt);
10915                    drop_gen_slot(vt, v);
10916                }
10917            }
10918            // Join: keep only splits BOTH paths produced (an absent else
10919            // is a fall-through path that keeps the pre-if generation).
10920            gen.retain(|k, v| g_then.get(k) == Some(v) && g_else.get(k) == Some(v));
10921        }
10922        Stmt::While { cond, body, .. } | Stmt::DoWhile { cond, body, .. } => {
10923            // 3-round fixpoint: round N walks the cond+body with the
10924            // accumulated gen — the back-edge carries the tail
10925            // generation into the head reads of the next round.
10926            let entry = gen.clone();
10927            for _ in 0..3 {
10928                rewrite_gen_reads(cond, gen, vt);
10929                split_walk_stmt(body, vt, gen, counter, pool);
10930            }
10931            // After the loop the pre-loop generation is what a
10932            // zero-iteration execution left — conservative.
10933            *gen = entry;
10934        }
10935        Stmt::For { init, body, .. } => {
10936            for x in init.iter_mut() {
10937                split_walk_stmt(x, vt, gen, counter, pool);
10938            }
10939            let entry = gen.clone();
10940            for _ in 0..3 {
10941                split_walk_stmt(body, vt, gen, counter, pool);
10942            }
10943            *gen = entry;
10944        }
10945        Stmt::ForEach { body, .. } => {
10946            let entry = gen.clone();
10947            for _ in 0..3 {
10948                split_walk_stmt(body, vt, gen, counter, pool);
10949            }
10950            *gen = entry;
10951        }
10952        Stmt::Switch { cases, default, .. } => {
10953            let entry = gen.clone();
10954            for c in cases.iter_mut() {
10955                let mut g_case = entry.clone();
10956                for x in c.body.iter_mut() {
10957                    split_walk_stmt(x, vt, &mut g_case, counter, pool);
10958                }
10959                gen.retain(|k, v| g_case.get(k) == Some(v));
10960            }
10961            if let Some(d) = default {
10962                let mut g_def = entry.clone();
10963                split_walk_stmt(d, vt, &mut g_def, counter, pool);
10964                gen.retain(|k, v| g_def.get(k) == Some(v));
10965            }
10966        }
10967        Stmt::Try {
10968            body,
10969            catches,
10970            finally,
10971        } => {
10972            let entry = gen.clone();
10973            let mut g_try = entry.clone();
10974            split_walk_stmt(body, vt, &mut g_try, counter, pool);
10975            let mut g_all = g_try.clone();
10976            for c in catches.iter_mut() {
10977                // The handler runs with the PRE-try state (the exception
10978                // may fire anywhere in the try).
10979                let mut g_c = entry.clone();
10980                split_walk_stmt(&mut c.body, vt, &mut g_c, counter, pool);
10981                g_all.retain(|k, v| g_c.get(k) == Some(v));
10982            }
10983            *gen = g_all;
10984            if let Some(f) = finally {
10985                split_walk_stmt(f, vt, gen, counter, pool);
10986            }
10987        }
10988        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => {
10989            split_walk_stmt(body, vt, gen, counter, pool);
10990        }
10991        Stmt::Return(Some(e)) | Stmt::Throw(e) => {
10992            rewrite_gen_reads(e, gen, vt);
10993        }
10994        _ => {}
10995    }
10996}
10997
10998/// Framework exception super edge — the embedded API database
10999/// (fwdb) replaced the hand-written ~110-entry JDK/Android exception
11000/// supers table: real extends edges for EVERY framework class, so
11001/// multi-catch dedupe now covers chains the table missed (and cannot
11002/// carry a wrong entry — the table's failure mode was dropping a live
11003/// alternative).
11004fn fw_super(c: &str) -> Option<&'static str> {
11005    crate::fwdb::super_of(c)
11006}
11007
11008/// `sub <: sup` for exception-class names, combining the pool
11009/// hierarchy (which also matches framework ancestor NAMES on the way
11010/// up, so pool subclasses of framework exceptions resolve) with the
11011/// tabled framework chains for pairs where both sides are outside the
11012/// dex.
11013fn exc_subtype(sub: &str, sup: &str, pool: &DexPool) -> bool {
11014    if sub == sup {
11015        return true;
11016    }
11017    if pool.is_subtype(sub, sup) {
11018        return true;
11019    }
11020    let mut cur = sub;
11021    for _ in 0..24 {
11022        let Some(next) = fw_super(cur) else { break };
11023        if next == sup {
11024            return true;
11025        }
11026        cur = next;
11027    }
11028    false
11029}
11030
11031/// Drop multi-catch alternatives shadowed by another alternative in
11032/// the same handler (JLS 14.20.2 forbids subtype-related
11033/// alternatives; the dex verifier does not). Exact duplicates collapse
11034/// to the first occurrence. The caught set is unchanged, so this is
11035/// semantics-preserving by construction.
11036pub fn dedupe_multicatch(s: &mut Stmt, pool: &DexPool) {
11037    dedupe_multicatch_walk(s, pool);
11038}
11039
11040fn dedupe_multicatch_walk(s: &mut Stmt, pool: &DexPool) {
11041    match s {
11042        Stmt::Try {
11043            body,
11044            catches,
11045            finally,
11046        }
11047        | Stmt::TryWithResources {
11048            body,
11049            catches,
11050            finally,
11051            ..
11052        } => {
11053            dedupe_multicatch_walk(body, pool);
11054            for c in catches.iter_mut() {
11055                if c.exc.len() > 1 {
11056                    let mut keep: Vec<std::sync::Arc<str>> = Vec::with_capacity(c.exc.len());
11057                    for e in c.exc.iter() {
11058                        let shadowed = c.exc.iter().any(|o| {
11059                            o.as_ref() != e.as_ref() && exc_subtype(e, o, pool)
11060                        });
11061                        if !shadowed && !keep.iter().any(|k| k.as_ref() == e.as_ref()) {
11062                            keep.push(e.clone());
11063                        }
11064                    }
11065                    c.exc = keep;
11066                }
11067                dedupe_multicatch_walk(&mut c.body, pool);
11068            }
11069            if let Some(f) = finally {
11070                dedupe_multicatch_walk(f, pool);
11071            }
11072        }
11073        Stmt::Block(v) => {
11074            for x in v.iter_mut() {
11075                dedupe_multicatch_walk(x, pool);
11076            }
11077        }
11078        Stmt::If {
11079            then_stmt,
11080            else_stmt,
11081            ..
11082        } => {
11083            dedupe_multicatch_walk(then_stmt, pool);
11084            if let Some(e) = else_stmt {
11085                dedupe_multicatch_walk(e, pool);
11086            }
11087        }
11088        Stmt::While { body, .. }
11089        | Stmt::DoWhile { body, .. }
11090        | Stmt::For { body, .. }
11091        | Stmt::ForEach { body, .. }
11092        | Stmt::Labeled { body, .. }
11093        | Stmt::Synchronized { body, .. } => dedupe_multicatch_walk(body, pool),
11094        Stmt::Switch { cases, default, .. } => {
11095            for c in cases.iter_mut() {
11096                for x in c.body.iter_mut() {
11097                    dedupe_multicatch_walk(x, pool);
11098                }
11099            }
11100            if let Some(d) = default {
11101                dedupe_multicatch_walk(d, pool);
11102            }
11103        }
11104        _ => {}
11105    }
11106}
11107
11108/// Dangling `break L<id>`: a Goto whose paired Label/Labeled-wrap was
11109/// lost to structure degradation prints `break L<id>;` against an
11110/// undeclared label ("未定义的标签", weibo 371/lark 85/weixin 535).
11111/// Inside a loop the goto-to-loop-exit shape degrades to a plain
11112/// `break` — compilable, and the dominant original semantic. Outside
11113/// any loop the jump is irreducible flow (weixin protobuf state
11114/// machines: `goto` into a lost loop middle / dispatcher block — the
11115/// target exists but not at a position Java can express); it renders
11116/// as an honest `$DDC:` marker comment instead of a guaranteed
11117/// undefined-label error, and the block's tail after it is dropped
11118/// (the original control flow never reached those statements).
11119pub fn resolve_dangling_gotos(s: &mut Stmt) {
11120    let mut labels: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
11121    // walk_all is read-only — the whole-body clone this used to make
11122    // (4.6k profile samples of Stmt::clone) bought nothing.
11123    walk_all(s, &mut |st| {
11124        if let Stmt::Label(id) = st {
11125            labels.insert(*id);
11126        }
11127    });
11128    resolve_gotos_walk(s, &labels, 0);
11129}
11130
11131fn resolve_gotos_walk(s: &mut Stmt, labels: &jdc_core::FxHashSet<u32>, loop_depth: u32) {
11132    let depth = match s {
11133        Stmt::While { .. } | Stmt::DoWhile { .. } | Stmt::For { .. } | Stmt::ForEach { .. } => {
11134            loop_depth + 1
11135        }
11136        _ => loop_depth,
11137    };
11138    match s {
11139        Stmt::Goto(id) => {
11140            if labels.contains(id) {
11141                return; // valid pair — keep
11142            }
11143            if depth > 0 {
11144                *s = Stmt::Break(None);
11145            } else {
11146                *s = Stmt::Comment(format!(
11147                    "$DDC: unresolved jump to block {} (irreducible control flow)",
11148                    id
11149                ));
11150            }
11151        }
11152        Stmt::Block(v) => {
11153            let mut i = 0usize;
11154            while i < v.len() {
11155                // A depth-0 dangling Goto becomes a marker comment and
11156                // the original flow never reached the siblings after
11157                // it — dropping them keeps the rendered reachability
11158                // faithful (they were dead in the dex too).
11159                let dangling_here =
11160                    depth == 0 && matches!(&v[i], Stmt::Goto(id) if !labels.contains(id));
11161                resolve_gotos_walk(&mut v[i], labels, depth);
11162                if dangling_here {
11163                    v.truncate(i + 1);
11164                    break;
11165                }
11166                i += 1;
11167            }
11168        }
11169        Stmt::If { then_stmt, else_stmt, .. } => {
11170            resolve_gotos_walk(then_stmt, labels, depth);
11171            if let Some(e) = else_stmt {
11172                resolve_gotos_walk(e, labels, depth);
11173            }
11174        }
11175        Stmt::While { body, .. }
11176        | Stmt::DoWhile { body, .. }
11177        | Stmt::For { body, .. }
11178        | Stmt::ForEach { body, .. }
11179        | Stmt::Labeled { body, .. }
11180        | Stmt::Synchronized { body, .. } => resolve_gotos_walk(body, labels, depth),
11181        Stmt::Try {
11182            body,
11183            catches,
11184            finally,
11185        } => {
11186            resolve_gotos_walk(body, labels, depth);
11187            for c in catches.iter_mut() {
11188                resolve_gotos_walk(&mut c.body, labels, depth);
11189            }
11190            if let Some(f) = finally {
11191                resolve_gotos_walk(f, labels, depth);
11192            }
11193        }
11194        Stmt::Switch { cases, default, .. } => {
11195            for c in cases.iter_mut() {
11196                for x in c.body.iter_mut() {
11197                    resolve_gotos_walk(x, labels, depth);
11198                }
11199            }
11200            if let Some(d) = default {
11201                resolve_gotos_walk(d, labels, depth);
11202            }
11203        }
11204        _ => {}
11205    }
11206}
11207
11208/// READS of `v` across the statements (assignment targets excluded).
11209/// All-var read counts in ONE pass (reads semantics of stmts_read_var)
11210/// — for gates that test many candidate vars against the same subtree
11211/// (split_walk's undo filters ran a full arm walk PER candidate).
11212/// Which of `vars` appear in a READ position inside `stmts`? One walk,
11213/// a small linear membership check per read, short-circuiting the check
11214/// once every candidate is found — no HashMap, no per-node hashing.
11215/// Replaces the full-subtree `read_counts_all` maps the split undo-gates
11216/// used to build per diverged If (their only query was `reads(v) == 0`
11217/// for a handful of candidate gens; the rebuild was quadratic in If
11218/// nesting depth and dominated the split pass on deep methods).
11219fn mark_read_vars(stmts: &[Stmt], vars: &[u32], hits: &mut [bool]) {
11220    let mut remaining = vars.len();
11221    for s in stmts {
11222        if remaining == 0 {
11223            break;
11224        }
11225        visit_stmt_exprs_ro(s, &mut |e| {
11226            if remaining == 0 {
11227                return;
11228            }
11229            visit_exprs_reads(e, &mut |x| {
11230                if remaining == 0 {
11231                    return;
11232                }
11233                if let Expr::Local { var, .. } = x {
11234                    for (i, v) in vars.iter().enumerate() {
11235                        if *v == *var && !hits[i] {
11236                            hits[i] = true;
11237                            remaining -= 1;
11238                        }
11239                    }
11240                }
11241            });
11242        });
11243    }
11244}
11245
11246fn stmts_read_var(stmts: &[Stmt], v: u32) -> usize {
11247    let mut n = 0usize;
11248    for s in stmts {
11249        visit_stmt_exprs_ro(s, &mut |e| {
11250            visit_exprs_reads(e, &mut |x| {
11251                if let Expr::Local { var: vv, .. } = x {
11252                    if *vv == v {
11253                        n += 1;
11254                    }
11255                }
11256            });
11257        });
11258    }
11259    n
11260}
11261
11262pub fn fix_ctor_conditional_super(body: &mut Stmt) {
11263    // A single-statement body can arrive UNWRAPPED (bare `If` — the
11264    // gb6/e throw-guard family); the shape scans need a statement list.
11265    if !matches!(body, Stmt::Block(_)) {
11266        let inner = std::mem::replace(body, Stmt::Block(Vec::new()));
11267        *body = Stmt::Block(vec![inner]);
11268    }
11269    let Stmt::Block(stmts) = body else { return };
11270    // Unwrap single-statement Block wrappers: the structurer can nest
11271    // the whole body one level deeper, hiding the top-level If from the
11272    // shape scans (cleanup would flatten it — but runs AFTER this pass).
11273    while stmts.len() == 1 {
11274        let Stmt::Block(inner) = &stmts[0] else { break };
11275        *stmts = inner.clone();
11276    }
11277    if stmts.is_empty() {
11278        return;
11279    }
11280    if is_bare_ctor_call(stmts.first().unwrap()) {
11281        dedupe_ctor_delegations(body);
11282        return;
11283    }
11284    if try_linear_super_inline(stmts) {
11285        return;
11286    }
11287    try_merge_branched_super(stmts);
11288}
11289
11290/// Shape C: the delegation is already first — strip duplicate copies
11291/// left by R8 path-duplication (Kotlin default-arg bridge ctors: ft5/j
11292/// had `super()` leading PLUS another inside a branch — "对super的调用
11293/// 必须是构造器中的第一个语句" at the copy). Every path delegates
11294/// through the leading call, so structurally-identical (or empty-args,
11295/// same-kind) copies nested in blocks/ifs are removed, never moved.
11296/// Runs both ahead of the shape-A/B attempt and AFTER the plain hoist
11297/// (fix_ctor_super_first) — the hoist is what puts the leading
11298/// delegation in place for path-duplicated ctors.
11299/// A ctor that ALREADY delegates in its first statement can hold no
11300/// other `this(..)`/`super(..)` — Baidu Titan hotpatch instrumentation
11301/// injects a conditional re-delegation inside the `if ($ic != null)`
11302/// guard of every ctor (baidusearch ctor-not-first ×61,922 across
11303/// 21,813 files: `super(context); if ($ic != null) { ..; if
11304/// ((flag&1)!=0) { super((Context) callArgs[0]); ..; return; } }`).
11305/// The guard is dead code on the unpatched runtime path (and the
11306/// decompiled output has no patch runtime at all); dropping the
11307/// secondary delegation keeps the real semantics and restores the
11308/// Java first-statement rule. Runs after dedupe_ctor_delegations
11309/// (identical copies) and before extract_branched_delegation_helper
11310/// — gated on a LEADING delegation, so branched-delegation ctors
11311/// (no leading one) still get the helper treatment. Nested class
11312/// declarations are separate ctor scopes and are not entered.
11313pub fn strip_secondary_delegations(body: &mut Stmt) {
11314    let Stmt::Block(stmts) = body else { return };
11315    if !stmts.first().is_some_and(is_bare_ctor_call) {
11316        return;
11317    }
11318    fn strip_in(s: &mut Stmt) {
11319        if is_bare_ctor_call(s) {
11320            *s = Stmt::Block(Vec::new());
11321            return;
11322        }
11323        match s {
11324            Stmt::Block(v) => v.iter_mut().for_each(strip_in),
11325            Stmt::If {
11326                then_stmt,
11327                else_stmt,
11328                ..
11329            } => {
11330                strip_in(then_stmt);
11331                if let Some(e) = else_stmt {
11332                    strip_in(e);
11333                }
11334            }
11335            Stmt::While { body, .. }
11336            | Stmt::DoWhile { body, .. }
11337            | Stmt::ForEach { body, .. }
11338            | Stmt::Synchronized { body, .. }
11339            | Stmt::Labeled { body, .. } => strip_in(body),
11340            Stmt::For { init, body, .. } => {
11341                init.iter_mut().for_each(strip_in);
11342                strip_in(body);
11343            }
11344            Stmt::Switch { cases, default, .. } => {
11345                for c in cases.iter_mut() {
11346                    c.body.iter_mut().for_each(strip_in);
11347                }
11348                if let Some(d) = default {
11349                    strip_in(d);
11350                }
11351            }
11352            Stmt::Try {
11353                body,
11354                catches,
11355                finally,
11356            }
11357            | Stmt::TryWithResources {
11358                body,
11359                catches,
11360                finally,
11361                ..
11362            } => {
11363                strip_in(body);
11364                for c in catches.iter_mut() {
11365                    strip_in(&mut c.body);
11366                }
11367                if let Some(f) = finally {
11368                    strip_in(f);
11369                }
11370            }
11371            _ => {}
11372        }
11373    }
11374    for st in stmts.iter_mut().skip(1) {
11375        strip_in(st);
11376    }
11377}
11378
11379pub fn dedupe_ctor_delegations(body: &mut Stmt) {
11380    let Stmt::Block(stmts) = body else { return };
11381    let Some(d0) = stmts.first().and_then(|s| match s {
11382        Stmt::ExprStmt(e) if is_delegation_expr(e) => Some(e.clone()),
11383        _ => None,
11384    }) else {
11385        return;
11386    };
11387    fn rec(s: &mut Stmt, d0: &Expr) {
11388        match s {
11389            Stmt::Block(v) => {
11390                v.retain(|x| {
11391                    !matches!(x, Stmt::ExprStmt(e) if is_delegation_expr(e) && same_delegation(e, d0))
11392                });
11393                for x in v.iter_mut() {
11394                    rec(x, d0);
11395                }
11396            }
11397            Stmt::If { then_stmt, else_stmt, .. } => {
11398                rec(then_stmt, d0);
11399                if let Some(e) = else_stmt {
11400                    rec(e, d0);
11401                }
11402            }
11403            _ => {}
11404        }
11405    }
11406    // Top-level copies first (rec only strips copies NESTED in
11407    // blocks/ifs — a duplicate `super()` sitting directly in the body
11408    // list fell through its `_` arm: weixin ft5/j's second super()).
11409    {
11410        let d0r = &d0;
11411        let mut idx = 0usize;
11412        stmts.retain(|s| {
11413            idx += 1;
11414            idx == 1
11415                || !(matches!(s, Stmt::ExprStmt(e)
11416                    if is_delegation_expr(e) && same_delegation(e, d0r)))
11417        });
11418    }
11419    for s in stmts.iter_mut().skip(1) {
11420        rec(s, &d0);
11421    }
11422}
11423
11424/// Structural delegation identity for dedup: same kind/target and equal
11425/// args — or both arg-less (`super()` copies across duplicated paths
11426/// render with locally-renamed args yet delegate identically).
11427fn same_delegation(a: &Expr, b: &Expr) -> bool {
11428    match (a, b) {
11429        (
11430            Expr::Method { name: n1, cls: c1, is_super: s1, args: a1, .. },
11431            Expr::Method { name: n2, cls: c2, is_super: s2, args: a2, .. },
11432        ) => {
11433            &**n1 == "<init>"
11434                && &**n2 == "<init>"
11435                && c1 == c2
11436                && s1 == s2
11437                && (a1 == a2 || (a1.is_empty() && a2.is_empty()))
11438        }
11439        _ => false,
11440    }
11441}
11442
11443/// The `Method` expr of a bare ctor-delegation statement.
11444fn ctor_call_expr(s: &Stmt) -> Option<&Expr> {
11445    match s {
11446        Stmt::ExprStmt(e) if is_delegation_expr(e) => Some(e),
11447        _ => None,
11448    }
11449}
11450
11451/// Same delegation kind ignoring args (super vs this, same target).
11452fn same_ctor_kind(a: &Expr, b: &Expr) -> bool {
11453    match (a, b) {
11454        (
11455            Expr::Method { cls: c1, is_super: s1, owner: o1, .. },
11456            Expr::Method { cls: c2, is_super: s2, owner: o2, .. },
11457        ) => c1 == c2 && s1 == s2 && o1 == o2,
11458        _ => false,
11459    }
11460}
11461
11462/// Statements of a branch body, recursively flattening pure Blocks —
11463/// the fix runs BEFORE cleanup, so structurer-emitted nesting is still
11464/// in place. Any non-Block statement (If/Try/...) stays an item and
11465/// fails the def/call classification, which is the intended abort.
11466fn flat_list(s: &Stmt) -> Vec<&Stmt> {
11467    fn rec<'a>(s: &'a Stmt, out: &mut Vec<&'a Stmt>) {
11468        match s {
11469            Stmt::Block(v) => {
11470                for x in v {
11471                    rec(x, out);
11472                }
11473            }
11474            other => out.push(other),
11475        }
11476    }
11477    let mut out = Vec::new();
11478    rec(s, &mut out);
11479    out
11480}
11481
11482/// Blank the OWNER of un-folded `<init>` calls (lost allocation): the
11483/// printer renders them as `new X(..)` and never reads the owner, so its
11484/// register reference is not a real use (v7.a$a2: the dead
11485/// `new f(0,..)`'s owner was v1's register, inflating v1's use count
11486/// and blocking the inline-hoist).
11487fn strip_lost_alloc_owners(e: &mut Expr) {
11488    deep_rewrite(e, &mut |x| {
11489        if let Expr::Method { name, is_special: true, owner: Some(o), .. } = x {
11490            if &**name == "<init>" && !matches!(**o, Expr::This) {
11491                **o = Expr::Const(jdc_core::ir::expr::ConstVal::Int(0));
11492            }
11493        }
11494    });
11495}
11496
11497/// Per-var `Local` occurrence counts over an expression (single pass;
11498/// lost-alloc `<init>` owners excluded — see strip_lost_alloc_owners).
11499fn count_locals_expr(e: &Expr) -> jdc_core::FxHashMap<u32, usize> {
11500    let mut m = jdc_core::FxHashMap::default();
11501    count_expr_ro(e, &mut m);
11502    m
11503}
11504
11505pub(crate) fn count_locals_stmts(ss: &[Stmt]) -> jdc_core::FxHashMap<u32, usize> {
11506    let mut m = jdc_core::FxHashMap::default();
11507    for s in ss {
11508        visit_stmt_exprs_ro(s, &mut |e| count_expr_ro(e, &mut m));
11509    }
11510    m
11511}
11512
11513fn cnt(m: &jdc_core::FxHashMap<u32, usize>, v: u32) -> usize {
11514    m.get(&v).copied().unwrap_or(0)
11515}
11516
11517/// Substitute map-referenced locals, recursively expanding def chains
11518/// (depth-capped). Returns None when a referenced var has no def or the
11519/// chain is too deep.
11520fn inline_locals(
11521    e: &Expr,
11522    map: &std::collections::HashMap<u32, Expr>,
11523    depth: u32,
11524) -> Option<Expr> {
11525    if depth > 4 {
11526        return None;
11527    }
11528    let mut out = e.clone();
11529    let mut fail = false;
11530    deep_rewrite(&mut out, &mut |x| {
11531        if let Expr::Local { var, .. } = x {
11532            if let Some(def) = map.get(var) {
11533                match inline_locals(def, map, depth + 1) {
11534                    Some(r) => *x = r,
11535                    None => fail = true,
11536                }
11537            }
11538        }
11539    });
11540    if fail {
11541        None
11542    } else {
11543        Some(out)
11544    }
11545}
11546
11547/// A leading statement that defines a local: (var, init expr).
11548fn def_of(s: &Stmt) -> Option<(u32, &Expr)> {
11549    match s {
11550        Stmt::LocalDef { var, init: Some(e), .. } => Some((*var, e)),
11551        Stmt::ExprStmt(Expr::Assign {
11552            target,
11553            value,
11554            op: jdc_core::ir::expr::AssignOp::Plain,
11555            ..
11556        }) => match &**target {
11557            Expr::Local { var, .. } => Some((*var, value)),
11558            _ => None,
11559        },
11560        _ => None,
11561    }
11562}
11563
11564/// Shape A: straight-line prefix, delegation references prefix locals.
11565fn try_linear_super_inline(stmts: &mut Vec<Stmt>) -> bool {
11566    let Some(pos) = stmts.iter().position(is_bare_ctor_call) else {
11567        return false;
11568    };
11569    if pos == 0 {
11570        return false; // already first — nothing to do
11571    }
11572    // The prefix must be straight-line.
11573    if !stmts[..pos]
11574        .iter()
11575        .all(|s| matches!(s, Stmt::LocalDef { .. } | Stmt::ExprStmt(_)))
11576    {
11577        return false;
11578    }
11579    let Some(call) = ctor_call_expr(&stmts[pos]) else {
11580        return false;
11581    };
11582    let Expr::Method { args, .. } = call else {
11583        return false;
11584    };
11585    // Defs reachable from the args (last definition wins, bytecode order).
11586    let mut map: std::collections::HashMap<u32, Expr> =
11587        std::collections::HashMap::new();
11588    let mut def_idx: jdc_core::FxHashMap<u32, usize> =
11589        jdc_core::FxHashMap::default();
11590    for (i, s) in stmts[..pos].iter().enumerate() {
11591        if let Some((v, e)) = def_of(s) {
11592            map.insert(v, e.clone());
11593            def_idx.insert(v, i);
11594        }
11595    }
11596    // Which arg locals need inlining?
11597    let mut needed: Vec<u32> = Vec::new();
11598    for a in args {
11599        let mut probe = a.clone();
11600        strip_lost_alloc_owners(&mut probe);
11601        deep_rewrite(&mut probe, &mut |x| {
11602            if let Expr::Local { var, .. } = x {
11603                if map.contains_key(var) && !needed.contains(var) {
11604                    needed.push(*var);
11605                }
11606            }
11607        });
11608    }
11609    if needed.is_empty() {
11610        return false; // plain hoist handles it
11611    }
11612    // Guard: side-effecting defs inline only at a provably preserved
11613    // call count (exactly one use, the arg site).
11614    let mut consumed: Vec<usize> = Vec::new();
11615    let use_counts = count_locals_stmts(stmts);
11616    for &v in &needed {
11617        let def = &map[&v];
11618        let uses = cnt(&use_counts, v);
11619        if jdc_core::ir::build::has_side_effects(def) && uses != 1 {
11620            return false;
11621        }
11622        if uses == 1 {
11623            consumed.push(def_idx[&v]);
11624        }
11625    }
11626    // Build the inlined args.
11627    let mut new_args: Vec<Expr> = Vec::with_capacity(args.len());
11628    for a in args {
11629        match inline_locals(a, &map, 0) {
11630            Some(r) => new_args.push(r),
11631            None => return false,
11632        }
11633    }
11634    // Rebuild: super first (with inlined args), the unconsumed prefix
11635    // statements keep their order after it, then the original rest.
11636    let mut call_stmt = stmts.remove(pos);
11637    if let Stmt::ExprStmt(Expr::Method { args: slot, .. }) = &mut call_stmt {
11638        *slot = new_args;
11639    }
11640    let mut rest: Vec<Stmt> = Vec::with_capacity(stmts.len() + 1);
11641    rest.push(call_stmt);
11642    for (i, s) in stmts.drain(..).enumerate() {
11643        // Indices shift after `remove(pos)`: consumed holds pre-removal
11644        // indices; pos itself is already gone from `stmts`.
11645        let orig = if i >= pos { i + 1 } else { i };
11646        if !consumed.contains(&orig) {
11647            rest.push(s);
11648        }
11649    }
11650    *stmts = rest;
11651    true
11652}
11653
11654/// Shape B: if/else branches each ending in the same-kind delegation.
11655fn try_merge_branched_super(stmts: &mut Vec<Stmt>) -> bool {
11656    // Candidate If positions — the prelude before the chosen If must be
11657    // straight-line; try each If until one merges (nested/multi-if
11658    // ctors exist but the first matching one is the delegation site).
11659    let if_positions: Vec<usize> = stmts
11660        .iter()
11661        .enumerate()
11662        .filter(|(_, s)| matches!(s, Stmt::If { .. }))
11663        .map(|(i, _)| i)
11664        .collect();
11665    for if_pos in if_positions {
11666        // Cheap allocation-free gate: at least one branch must hold a
11667        // delegation (most ctor ifs are field null-checks and must not
11668        // pay the merge machinery). The OTHER side may be delegation-free
11669        // (throw-guard shape D) or absent (then-only shape E — the
11670        // rendered if/else form often only exists after later passes).
11671        let gate = match &stmts[if_pos] {
11672            Stmt::If { then_stmt, else_stmt, .. } => {
11673                contains_delegation(then_stmt)
11674                    || else_stmt.as_ref().is_some_and(|e| contains_delegation(e))
11675            }
11676            _ => false,
11677        };
11678        if gate && merge_at(stmts, if_pos) {
11679            return true;
11680        }
11681    }
11682    false
11683}
11684
11685/// Does this subtree contain a ctor delegation? (module-level: shared by
11686/// the merge gate and split_branch)
11687fn contains_delegation(s: &Stmt) -> bool {
11688    match s {
11689        Stmt::ExprStmt(e) => is_delegation_expr(e),
11690        Stmt::Block(v) => v.iter().any(contains_delegation),
11691        Stmt::If { then_stmt, else_stmt, .. } => {
11692            contains_delegation(then_stmt)
11693                || else_stmt.as_ref().is_some_and(|e| contains_delegation(e))
11694        }
11695        _ => false,
11696    }
11697}
11698
11699
11700
11701/// One merge attempt at `stmts[if_pos]`. See fix_ctor_conditional_super
11702/// for the doctrine. Extensions over the pure two-call merge:
11703/// - branch pre-call SIDE-EFFECT statements (Kotlin `o.h(param,..)`
11704///   null-checks — the zz5/j0 family) are allowed; they move into the
11705///   branch remainder (after super — the same reordering doctrine the
11706///   plain hoist already applies; Java has no pre-super statement form).
11707/// - a branch WITHOUT a delegation (throw-guard: `if (x==null) throw
11708///   .. else { super(..); .. }` — the gb6/e family) merges as pure
11709///   remainder; exactly one branch carries the call then, and no
11710///   ternary args arise (the guard throws before any path needs them).
11711/// - prelude single-assignment if/else (`if (c) {v=e1} else {v=e2}`)
11712///   folds into a Cond def for inlining (the zz5/j0 v15/v16 preludes).
11713fn merge_at(stmts: &mut Vec<Stmt>, if_pos: usize) -> bool {
11714    /// Branch classification: Call(at, defs, pre-call extra idxs) when a
11715    /// delegation sits behind only defs/side-effect statements; Clean
11716    /// when the branch has no delegation at all (pure remainder); Dirty
11717    /// otherwise (nested/conditional delegation — out of scope).
11718    enum Br {
11719        /// (call index, pre-call defs, def statement indices)
11720        Call(usize, Vec<(u32, Expr)>, Vec<usize>),
11721        Clean,
11722        Dirty,
11723    }
11724    fn split_branch(list: &[&Stmt]) -> Br {
11725        let call_pos = list.iter().position(|s| ctor_call_expr(s).is_some());
11726        let Some(c) = call_pos else {
11727            // No delegation at this level: a pure remainder branch
11728            // (throw-guard) unless one hides in nested control flow.
11729            return if list.iter().any(|s| contains_delegation(s)) {
11730                Br::Dirty
11731            } else {
11732                Br::Clean
11733            };
11734        };
11735        if list.iter().skip(c + 1).any(|s| ctor_call_expr(s).is_some()) {
11736            return Br::Dirty; // two delegations in one branch
11737        }
11738        let mut defs: Vec<(u32, Expr)> = Vec::new();
11739        let mut def_idxs: Vec<usize> = Vec::new();
11740        for (i, s) in list.iter().enumerate().take(c) {
11741            match def_of(s) {
11742                Some((v, e)) => {
11743                    defs.push((v, e.clone()));
11744                    def_idxs.push(i);
11745                }
11746                None => match s {
11747                    // Side-effect statements and bare decls ahead of the
11748                    // delegation ride along into the remainder.
11749                    Stmt::ExprStmt(_) | Stmt::LocalDef { .. } => {}
11750                    _ => return Br::Dirty, // control flow pre-call
11751                },
11752            }
11753        }
11754        Br::Call(c, defs, def_idxs)
11755    }
11756    /// Prelude single-assignment if/else → `v = c ? e1 : e2` def.
11757    fn cond_def_of(s: &Stmt, scope: &[Stmt]) -> Option<(u32, Expr)> {
11758        let Stmt::If { cond, then_stmt, else_stmt: Some(e), .. } = s else {
11759            return None;
11760        };
11761        fn one<'x>(b: &'x Stmt, scope: &[Stmt]) -> Option<(u32, &'x Expr)> {
11762            let l = flat_list(b);
11763            if l.len() == 1 {
11764                return def_of(l[0]);
11765            }
11766            // A [w = E; v = w] forwarding PAIR — the shape the
11767            // structurer leaves when E is impure (`c v5 = new c(..);
11768            // v6 = v5`) and forward_single_use's adjacency pass missed
11769            // it (a duplicated read elsewhere in its raw body broke the
11770            // single-read gate). Collapse to `v = E`, gated on w being
11771            // method-unique: the merge DROPS this branch, so any other
11772            // read/write of w would dangle (lark gp2/d Kotlin default-
11773            // arg bridge, this-not-first family).
11774            if l.len() == 2 {
11775                let (w, ex) = def_of(l[0])?;
11776                let Stmt::ExprStmt(Expr::Assign {
11777                    target,
11778                    op: AssignOp::Plain,
11779                    value,
11780                }) = l[1]
11781                else {
11782                    return None;
11783                };
11784                let Expr::Local { var: tv, .. } = &**target else {
11785                    return None;
11786                };
11787                let Expr::Local { var: rv, .. } = &**value else {
11788                    return None;
11789                };
11790                if *rv != w || *tv == w {
11791                    return None;
11792                }
11793                // stmts_count_writes only sees Expr-level assigns; the
11794                // pair's own def is often an init-bearing LocalDef, so
11795                // count those separately for the uniqueness gate.
11796                let mut ldef_writes = 0usize;
11797                for st in scope {
11798                    walk_all(st, &mut |x| {
11799                        if let Stmt::LocalDef {
11800                            var,
11801                            init: Some(_),
11802                            ..
11803                        } = x
11804                        {
11805                            if *var == w {
11806                                ldef_writes += 1;
11807                            }
11808                        }
11809                    });
11810                }
11811                if stmts_read_var(scope, w) != 1
11812                    || stmts_count_writes(scope, w) + ldef_writes != 1
11813                {
11814                    return None;
11815                }
11816                return Some((*tv, ex));
11817            }
11818            None
11819        }
11820        let (v1, e1) = one(then_stmt, scope)?;
11821        let (v2, e2) = one(e, scope)?;
11822        if v1 != v2 {
11823            return None;
11824        }
11825        Some((
11826            v1,
11827            Expr::Cond {
11828                c: Box::new(cond.clone()),
11829                t: Box::new(e1.clone()),
11830                f: Box::new(e2.clone()),
11831            },
11832        ))
11833    }
11834
11835    // Prelude: defs (flat or folded cond), bare decls, side-effect stmts.
11836    let mut pmap: std::collections::HashMap<u32, Expr> =
11837        std::collections::HashMap::new();
11838    let mut pidx: jdc_core::FxHashMap<u32, usize> =
11839        jdc_core::FxHashMap::default();
11840    for (i, s) in stmts[..if_pos].iter().enumerate() {
11841        match s {
11842            Stmt::LocalDef { .. } | Stmt::ExprStmt(_) => {
11843                if let Some((v, e)) = def_of(s) {
11844                    pmap.insert(v, e.clone());
11845                    pidx.insert(v, i);
11846                }
11847            }
11848            Stmt::If { .. } => match cond_def_of(s, stmts) {
11849                Some((v, e)) => {
11850                    pmap.insert(v, e);
11851                    pidx.insert(v, i);
11852                }
11853                None => return false,
11854            },
11855            _ => return false,
11856        }
11857    }
11858    let (cond, then_v, else_v, had_else) = match &stmts[if_pos] {
11859        Stmt::If { cond, then_stmt, else_stmt, .. } => (
11860            cond.clone(),
11861            then_stmt.as_ref().clone(),
11862            else_stmt.as_ref().map_or_else(|| Stmt::Block(Vec::new()), |e| e.as_ref().clone()),
11863            else_stmt.is_some(),
11864        ),
11865        _ => return false,
11866    };
11867    // Chained guards (Kotlin multi-param null-checks):
11868    // `if(a){throw..} else { if(b){throw..} else { super(..); .. } }` —
11869    // a branch that is a lone delegation-bearing If gets merged
11870    // recursively first (its delegation hoisted to the branch head), so
11871    // the outer split then sees [call, nested-guard-remainder].
11872    fn normalize_branch(b: &mut Stmt) {
11873        // MOVE-based (no deep clone): chained weixin Parcel ctors carry
11874        // huge tails; cloning per recursion level cost measurable wall
11875        // time. merge_at leaves stmts untouched on failure, so the taken
11876        // node can always go back.
11877        let slot: &mut Stmt = match b {
11878            Stmt::Block(v) if v.len() == 1 => &mut v[0],
11879            other => other,
11880        };
11881        if !matches!(slot, Stmt::If { .. }) || !contains_delegation(slot) {
11882            return;
11883        }
11884        let taken = std::mem::replace(slot, Stmt::Block(Vec::new()));
11885        let mut tmp = vec![taken];
11886        if merge_at(&mut tmp, 0) {
11887            *slot = Stmt::Block(tmp);
11888        } else {
11889            *slot = tmp.pop().unwrap();
11890        }
11891    }
11892    let mut then_v = then_v;
11893    let mut else_v = else_v;
11894    normalize_branch(&mut then_v);
11895    normalize_branch(&mut else_v);
11896    let then_list = flat_list(&then_v);
11897    let else_list = flat_list(&else_v);
11898
11899    // Exactly: two Call branches (merge with ternaries) or one Call +
11900    // one Clean (single-delegation throw-guard). Owned branch payloads.
11901    enum Side {
11902        Call {
11903            at: usize,
11904            defs: Vec<(u32, Expr)>,
11905            def_idxs: Vec<usize>,
11906        },
11907        Clean,
11908    }
11909    let (t_side, e_side, two_sided) = match (split_branch(&then_list), split_branch(&else_list)) {
11910        (Br::Call(tc, td, ti), Br::Call(ec, ed, ei)) => (
11911            Side::Call { at: tc, defs: td, def_idxs: ti },
11912            Side::Call { at: ec, defs: ed, def_idxs: ei },
11913            true,
11914        ),
11915        (Br::Call(tc, td, ti), Br::Clean) => (
11916            Side::Call { at: tc, defs: td, def_idxs: ti },
11917            Side::Clean,
11918            false,
11919        ),
11920        (Br::Clean, Br::Call(ec, ed, ei)) => (
11921            Side::Clean,
11922            Side::Call { at: ec, defs: ed, def_idxs: ei },
11923            false,
11924        ),
11925        _ => return false,
11926    };
11927    // The delegation nodes (two-sided: both; single: one).
11928    let (t_call, e_call) = match (&t_side, &e_side) {
11929        (Side::Call { at: ta, .. }, Side::Call { at: ea, .. }) => (
11930            ctor_call_expr(then_list[*ta]),
11931            ctor_call_expr(else_list[*ea]),
11932        ),
11933        (Side::Call { at: ta, .. }, Side::Clean) => {
11934            (ctor_call_expr(then_list[*ta]), None)
11935        }
11936        (Side::Clean, Side::Call { at: ea, .. }) => {
11937            (None, ctor_call_expr(else_list[*ea]))
11938        }
11939        _ => return false,
11940    };
11941    // The node that becomes the merged call: then side when present.
11942    let (call_list, call_at) = match &t_side {
11943        Side::Call { at, .. } => (&then_list, *at),
11944        Side::Clean => match &e_side {
11945            Side::Call { at, .. } => (&else_list, *at),
11946            Side::Clean => return false,
11947        },
11948    };
11949    let (base_args, other_args) = if two_sided {
11950        let (Some(tc), Some(ec)) = (t_call, e_call) else {
11951            return false;
11952        };
11953        if !same_ctor_kind(tc, ec) {
11954            return false;
11955        }
11956        let (Expr::Method { args: a1, .. }, Expr::Method { args: a2, .. }) = (tc, ec) else {
11957            return false;
11958        };
11959        if a1.len() != a2.len() {
11960            return false;
11961        }
11962        (a1, Some(a2))
11963    } else {
11964        let c = t_call.or(e_call);
11965        let Some(c) = c else { return false };
11966        let Expr::Method { args, .. } = c else {
11967            return false;
11968        };
11969        (args, None)
11970    };
11971
11972    // Def maps per side (prelude + own branch defs).
11973    let mut tmap: std::collections::HashMap<u32, Expr> = pmap.clone();
11974    let mut emap: std::collections::HashMap<u32, Expr> = pmap.clone();
11975    if let Side::Call { defs, .. } = &t_side {
11976        for (v, e) in defs {
11977            tmap.insert(*v, e.clone());
11978        }
11979    }
11980    if let Side::Call { defs, .. } = &e_side {
11981        for (v, e) in defs {
11982            emap.insert(*v, e.clone());
11983        }
11984    }
11985    let Some(cond_i) = inline_locals(&cond, &pmap, 0) else {
11986        return false;
11987    };
11988    // Inline each side's args through its own map.
11989    let mut then_args: Vec<Expr> = Vec::with_capacity(base_args.len());
11990    let mut else_args: Vec<Expr> = Vec::new();
11991    if two_sided {
11992        for a in base_args {
11993            match inline_locals(a, &tmap, 0) {
11994                Some(r) => then_args.push(r),
11995                None => return false,
11996            }
11997        }
11998        for a in other_args.unwrap() {
11999            match inline_locals(a, &emap, 0) {
12000                Some(r) => else_args.push(r),
12001                None => return false,
12002            }
12003        }
12004    } else {
12005        // Single-delegation side: base_args belong to whichever side
12006        // carries the call; pick that side's map.
12007        let from_then = matches!(&t_side, Side::Call { .. });
12008        for a in base_args {
12009            let r = if from_then {
12010                inline_locals(a, &tmap, 0)
12011            } else {
12012                inline_locals(a, &emap, 0)
12013            };
12014            match r {
12015                Some(r) => then_args.push(r),
12016                None => return false,
12017            }
12018        }
12019    }
12020    let primary_args = base_args;
12021    // Per-position merge.
12022    let mut merged: Vec<Expr> = Vec::with_capacity(then_args.len());
12023    let mut differ = 0usize;
12024    for (j, t) in then_args.iter().enumerate() {
12025        if two_sided {
12026            let e = &else_args[j];
12027            if t == e {
12028                merged.push(t.clone());
12029            } else {
12030                differ += 1;
12031                merged.push(Expr::Cond {
12032                    c: Box::new(cond_i.clone()),
12033                    t: Box::new(t.clone()),
12034                    f: Box::new(e.clone()),
12035                });
12036            }
12037        } else {
12038            merged.push(t.clone());
12039        }
12040    }
12041
12042    // ---- side-effect preservation guards (batched counts) ----
12043    let vars_in = |e: &Expr| -> Vec<u32> {
12044        let mut vs = Vec::new();
12045        let mut probe = e.clone();
12046        strip_lost_alloc_owners(&mut probe);
12047        deep_rewrite(&mut probe, &mut |x| {
12048            if let Expr::Local { var, .. } = x {
12049                if !vs.contains(var) {
12050                    vs.push(*var);
12051                }
12052            }
12053        });
12054        vs
12055    };
12056    // Remainder statements per branch (extras + post-call tail; a Clean
12057    // branch contributes all of its statements).
12058    // The consumed defs LEAVE the remainder: their expressions were
12059    // inlined into the merged delegation args, and a dead `v = E` left
12060    // after the call both reads wrong and trips the u_t1/u_t2 guards
12061    // (ContinuationImpl's branch-local `context = null / getContext()`
12062    // — the coroutine family's 对this的调用必须是第一个语句 root).
12063    let branch_remainder = |list: &[&Stmt], side: &Side| -> Vec<Stmt> {
12064        match side {
12065            Side::Clean => list.iter().map(|s| (*s).clone()).collect(),
12066            Side::Call { at, def_idxs, .. } => {
12067                list.iter()
12068                    .enumerate()
12069                    .filter(|(i, _)| *i != *at && !def_idxs.contains(i))
12070                    .map(|(_, s)| (*s).clone())
12071                    .collect()
12072            }
12073        }
12074    };
12075    let tail_then_v = branch_remainder(&then_list, &t_side);
12076    let tail_else_v = branch_remainder(&else_list, &e_side);
12077    let post = &stmts[if_pos + 1..];
12078    let mut inlined: Vec<u32> = vars_in(&cond);
12079    for a in primary_args.iter().chain(else_args.iter()) {
12080        for v in vars_in(a) {
12081            if !inlined.contains(&v) {
12082                inlined.push(v);
12083            }
12084        }
12085    }
12086    inlined.retain(|v| pmap.contains_key(v) || tmap.contains_key(v) || emap.contains_key(v));
12087    // Prelude-and-branch redefinition: ambiguous provenance — reject.
12088    for &v in &inlined {
12089        let in_branch = match (&t_side, &e_side) {
12090            (Side::Call { defs: td, .. }, Side::Call { defs: ed, .. }) => {
12091                td.iter().any(|(d, _)| *d == v) || ed.iter().any(|(d, _)| *d == v)
12092            }
12093            (Side::Call { defs, .. }, Side::Clean)
12094            | (Side::Clean, Side::Call { defs, .. }) => {
12095                defs.iter().any(|(d, _)| *d == v)
12096            }
12097            _ => false,
12098        };
12099        if pmap.contains_key(&v) && in_branch {
12100            return false;
12101        }
12102    }
12103    type Counts = jdc_core::FxHashMap<u32, usize>;
12104    type GuardCounts = (
12105        Counts,
12106        Vec<Counts>,
12107        Vec<Counts>,
12108        Counts,
12109        Counts,
12110        Counts,
12111        Vec<(u32, Counts)>,
12112    );
12113    let (cond_counts, a1_counts, a2_counts, tail1_counts, tail2_counts, post_counts, def_counts): GuardCounts =
12114        if inlined.is_empty() {
12115        Default::default()
12116    } else {
12117        let all_defs: Vec<(u32, &Expr)> = pmap
12118            .iter()
12119            .map(|(k, v)| (*k, v))
12120            .chain(match &t_side {
12121                Side::Call { defs, .. } => defs.iter().map(|(k, v)| (*k, v)).collect(),
12122                Side::Clean => Vec::new(),
12123            })
12124            .chain(match &e_side {
12125                Side::Call { defs, .. } => defs.iter().map(|(k, v)| (*k, v)).collect(),
12126                Side::Clean => Vec::new(),
12127            })
12128            .collect();
12129        (
12130            count_locals_expr(&cond),
12131            primary_args.iter().map(count_locals_expr).collect(),
12132            else_args.iter().map(count_locals_expr).collect(),
12133            count_locals_stmts(&tail_then_v),
12134            count_locals_stmts(&tail_else_v),
12135            count_locals_stmts(post),
12136            all_defs.into_iter().map(|(v, e)| (v, count_locals_expr(e))).collect(),
12137        )
12138        };
12139    for &v in &inlined {
12140        let in_t = matches!(&t_side, Side::Call { defs, .. } if defs.iter().any(|(d, _)| *d == v));
12141        let in_e = matches!(&e_side, Side::Call { defs, .. } if defs.iter().any(|(d, _)| *d == v));
12142        let effectful = [
12143            in_t.then(|| tmap.get(&v)).flatten(),
12144            in_e.then(|| emap.get(&v)).flatten(),
12145            pmap.get(&v),
12146        ]
12147        .into_iter()
12148        .flatten()
12149        .any(jdc_core::ir::build::has_side_effects);
12150        if !effectful {
12151            continue;
12152        }
12153        let u_cond = cnt(&cond_counts, v);
12154        let u_post = cnt(&post_counts, v);
12155        let u_t1 = cnt(&tail1_counts, v);
12156        let u_t2 = cnt(&tail2_counts, v);
12157        let mut u_odef = 0usize;
12158        for (dv, dc) in &def_counts {
12159            if *dv != v && inlined.contains(dv) {
12160                u_odef += cnt(dc, v);
12161            }
12162        }
12163        let mut emit = u_cond * (1 + differ);
12164        let mut uses_args = 0usize;
12165        for j in 0..primary_args.len() {
12166            let c1 = cnt(&a1_counts[j], v);
12167            let c2 = if two_sided && j < a2_counts.len() {
12168                cnt(&a2_counts[j], v)
12169            } else {
12170                0
12171            };
12172            uses_args += c1 + c2;
12173            emit += if two_sided && then_args[j] != else_args[j] {
12174                c1 + c2
12175            } else {
12176                c1.max(c2)
12177            };
12178        }
12179        let ok = if in_t || in_e {
12180            u_cond == 0
12181                && u_post == 0
12182                && u_t1 == 0
12183                && u_t2 == 0
12184                && u_odef == 0
12185                && a1_counts.iter().map(|c| cnt(c, v)).sum::<usize>() <= 1
12186                && a2_counts.iter().map(|c| cnt(c, v)).sum::<usize>() <= 1
12187                && emit <= 2
12188                && (in_t || a1_counts.iter().all(|c| cnt(c, v) == 0))
12189                && (in_e || a2_counts.iter().all(|c| cnt(c, v) == 0))
12190        } else {
12191            u_post == 0
12192                && u_t1 == 0
12193                && u_t2 == 0
12194                && u_odef == 0
12195                && ((u_cond == 1 && uses_args == 0 && emit <= 2)
12196                    || (u_cond == 0 && emit <= 1))
12197        };
12198        if !ok {
12199            return false;
12200        }
12201    }
12202
12203    // ---- assembly ----
12204    let mut call_stmt = call_list[call_at].clone();
12205    if let Stmt::ExprStmt(Expr::Method { args: slot, .. }) = &mut call_stmt {
12206        *slot = merged;
12207    }
12208    let residual = |v: u32| -> usize {
12209        cnt(&tail1_counts, v) + cnt(&tail2_counts, v) + cnt(&post_counts, v)
12210    };
12211    let mut kept_prelude: Vec<Stmt> = Vec::new();
12212    for (i, s) in stmts[..if_pos].iter().enumerate() {
12213        let consumed = match s {
12214            Stmt::LocalDef { var, .. } => Some(*var),
12215            Stmt::ExprStmt(Expr::Assign { target, .. }) => match &**target {
12216                Expr::Local { var, .. } => Some(*var),
12217                _ => None,
12218            },
12219            Stmt::If { .. } => cond_def_of(s, stmts).map(|(v, _)| v),
12220            _ => None,
12221        };
12222        if let Some(v) = consumed {
12223            if inlined.contains(&v) && pidx.get(&v) == Some(&i) && residual(v) == 0 {
12224                continue;
12225            }
12226        }
12227        kept_prelude.push(s.clone());
12228    }
12229    let build_keep = |list: &[&Stmt], side: &Side| -> Vec<Stmt> {
12230        match side {
12231            Side::Clean => list.iter().map(|s| (*s).clone()).collect(),
12232            Side::Call { at, .. } => {
12233                let mut out = Vec::new();
12234                for (i, s) in list.iter().enumerate() {
12235                    if i == *at {
12236                        continue;
12237                    }
12238                    if i < *at {
12239                        if let Some((v, _)) = def_of(s) {
12240                            if inlined.contains(&v) && residual(v) == 0 {
12241                                continue;
12242                            }
12243                        }
12244                    }
12245                    out.push((*s).clone());
12246                }
12247                out
12248            }
12249        }
12250    };
12251    let then_keep = build_keep(&then_list, &t_side);
12252    let else_keep = build_keep(&else_list, &e_side);
12253    let else_part = if else_keep.is_empty() && !had_else {
12254        None
12255    } else {
12256        Some(Box::new(Stmt::Block(else_keep)))
12257    };
12258    let new_if = Stmt::If {
12259        cond: cond_i,
12260        then_stmt: Box::new(Stmt::Block(then_keep)),
12261        else_stmt: else_part,
12262    };
12263    let if_empty = match &new_if {
12264        Stmt::If { then_stmt, else_stmt, .. } => {
12265            let be = |s: &Stmt| matches!(s, Stmt::Block(v) if v.is_empty());
12266            be(then_stmt) && else_stmt.as_ref().is_none_or(|e| be(e))
12267        }
12268        _ => false,
12269    };
12270    let mut out: Vec<Stmt> = Vec::with_capacity(stmts.len());
12271    out.push(call_stmt);
12272    out.extend(kept_prelude);
12273    if !if_empty {
12274        out.push(new_if);
12275    }
12276    out.extend(stmts[if_pos + 1..].to_vec());
12277    *stmts = out;
12278    true
12279}
12280
12281
12282pub fn strip_enum_ctor_super(body: &mut Stmt) {
12283    fn is_enum_super(s: &Stmt) -> bool {
12284        matches!(
12285            s,
12286            Stmt::ExprStmt(Expr::Method { name, is_super: true, .. }) if &**name == "<init>"
12287        )
12288    }
12289    // FULL depth: hotfix-guarded ctors hide a second super(String,int)
12290    // inside the proxy branch (alipay InstantRun: `if (proxy != null) {
12291    // super((String) v3[0], ..); proxy.afterSuper(this); return; }` at
12292    // depth 3+; the old two-level strip left it — super resolved against
12293    // Object once the fallback enum lost `extends Enum`, alipay
12294    // ctor-arity ×3,364). This path only runs for fallback enums whose
12295    // dex super is Enum/Object, so EVERY super call is a dead Enum-ctor
12296    // reference wherever it sits; constant subclasses (real supers)
12297    // take the other branch in method.rs and are untouched.
12298    fn strip_deep(stmts: &mut Vec<Stmt>) {
12299        stmts.retain(|s| !is_enum_super(s));
12300        for st in stmts.iter_mut() {
12301            strip_one(st);
12302        }
12303    }
12304    fn strip_one(st: &mut Stmt) {
12305        match st {
12306            Stmt::Block(v) => strip_deep(v),
12307            Stmt::If { then_stmt, else_stmt, .. } => {
12308                strip_one(then_stmt);
12309                if let Some(e) = else_stmt {
12310                    strip_one(e);
12311                }
12312            }
12313            Stmt::While { body, .. }
12314            | Stmt::DoWhile { body, .. }
12315            | Stmt::ForEach { body, .. }
12316            | Stmt::Synchronized { body, .. }
12317            | Stmt::Labeled { body, .. } => strip_one(body),
12318            Stmt::For { init, body, .. } => {
12319                strip_deep(init);
12320                strip_one(body);
12321            }
12322            Stmt::Switch { cases, default, .. } => {
12323                for c in cases.iter_mut() {
12324                    strip_deep(&mut c.body);
12325                }
12326                if let Some(d) = default {
12327                    strip_one(d);
12328                }
12329            }
12330            Stmt::Try { body, catches, finally }
12331            | Stmt::TryWithResources { body, catches, finally, .. } => {
12332                strip_one(body);
12333                for c in catches.iter_mut() {
12334                    strip_one(&mut c.body);
12335                }
12336                if let Some(f) = finally {
12337                    strip_one(f);
12338                }
12339            }
12340            _ => {}
12341        }
12342    }
12343    let Stmt::Block(stmts) = body else {
12344        return;
12345    };
12346    strip_deep(stmts);
12347}
12348
12349pub fn ensure_declared(body: &mut Stmt, vt: &VarTable) {
12350    // (Perf: dense Vec<bool> tables instead of HashSets — var ids are
12351    // dense; the `assigned` set collected here was never read and cost a
12352    // full extra tree walk per method.)
12353    let n = vt.vars.len().max(1);
12354
12355    // Demote extra LocalDefs (same var declared more than once) FIRST —
12356    // the scope analysis below must see exactly one def per var.
12357    let mut seen: HashSet<u32> = HashSet::default();
12358    demote_dup_decls(body, &mut seen, vt);
12359
12360    // Scope hoist: a LocalDef inside a nested block whose var is also
12361    // referenced OUTSIDE that block is a Java scope violation (the
12362    // branch-local `int v8 = pi.versionCode;` read after the try —
12363    // "cannot find symbol" at every outside use). Demote those defs to
12364    // assignments and let the bare top-of-method declaration below cover
12365    // the var. Two cheap passes: pre-order block numbering + per-var
12366    // inside/outside occurrence counts (pre-order subtree = id range).
12367    let mut def_block: Vec<u32> = vec![u32::MAX; n];
12368    let mut sizes: Vec<u32> = vec![0];
12369    let mut next_id: u32 = 1;
12370    scope_pass_a(body, 0, &mut next_id, &mut def_block, &mut sizes);
12371    sizes[0] = next_id;
12372    let mut within: Vec<u32> = vec![0; n];
12373    let mut total: Vec<u32> = vec![0; n];
12374    let mut next_b: u32 = 1;
12375    scope_pass_b(body, 0, &mut next_b, &def_block, &sizes, &mut within, &mut total);
12376    let mut hoist: HashSet<u32> = HashSet::default();
12377    for v in 0..n {
12378        if def_block[v] != u32::MAX && def_block[v] != 0 && within[v] < total[v] {
12379            hoist.insert(v as u32);
12380        }
12381    }
12382    if !hoist.is_empty() {
12383        let mut seed: HashSet<u32> = hoist.iter().copied().collect();
12384        demote_dup_decls(body, &mut seed, vt);
12385    }
12386
12387    let mut declared: Vec<bool> = vec![false; n];
12388    let mut is_param: Vec<bool> = vec![false; n];
12389    for v in &vt.vars {
12390        if v.is_param && (v.id as usize) < n {
12391            is_param[v.id as usize] = true;
12392        }
12393    }
12394    // Catch parameters are declared by the `catch (Type v)` clause —
12395    // bind_catches consumed their LocalDef before this pass ran, so
12396    // without this they re-declare at the top of the method and clash
12397    // with the clause (`Throwable th2;` + `catch (Throwable th2)` — a
12398    // syntax gate cannot see this, it is a semantic "already defined").
12399    let mut is_catch: Vec<bool> = vec![false; n];
12400    walk_all(body, &mut |st| {
12401        if let Stmt::Try { catches, .. } = st {
12402            for c in catches {
12403                if (c.var as usize) < n {
12404                    is_catch[c.var as usize] = true;
12405                }
12406            }
12407        }
12408    });
12409    walk_all(body, &mut |st| {
12410        if let Stmt::LocalDef { var, .. } = st {
12411            if (*var as usize) < n {
12412                declared[*var as usize] = true;
12413            }
12414        }
12415    });
12416    let mut used: HashSet<u32> = HashSet::default();
12417    // assignments=true: a var that only ever appears as an assign
12418    // TARGET still needs a declaration (`x = 5;` alone does not
12419    // declare x in Java).
12420    stmt_collect_vars(body, &mut used, true);
12421
12422    // Params are declared by the signature; hoisted vars lost their
12423    // LocalDef to the demotion above and always need the bare decl.
12424    let mut needs_decl: Vec<u32> = used
12425        .into_iter()
12426        .filter(|v| {
12427            let i = *v as usize;
12428            i >= n
12429                || ((!declared[i] && !is_param[i] && !is_catch[i]) || hoist.contains(v))
12430        })
12431        .collect();
12432    needs_decl.sort_unstable();
12433    needs_decl.dedup();
12434
12435    if !needs_decl.is_empty() {
12436        let mut decls: Vec<Stmt> = needs_decl
12437            .into_iter()
12438            .map(|v| Stmt::LocalDef {
12439                var: v,
12440                init: None,
12441                is_final: false,
12442                force_type: true,
12443            })
12444            .collect();
12445        decls.reverse();
12446        match body {
12447            Stmt::Block(v) => {
12448                for d in decls {
12449                    v.insert(0, d);
12450                }
12451            }
12452            other => {
12453                let inner = std::mem::replace(other, Stmt::Block(vec![]));
12454                *other = Stmt::Block({
12455                    let mut vs = decls;
12456                    vs.push(inner);
12457                    vs
12458                });
12459            }
12460        }
12461    }
12462}
12463
12464fn demote_dup_decls(s: &mut Stmt, seen: &mut HashSet<u32>, vt: &VarTable) {
12465    match s {
12466        // Exactly one visit per node: the Block arm consumes its children,
12467        // the fallthrough walk would double-visit them.
12468        Stmt::Block(v) => {
12469            for x in v.iter_mut() {
12470                demote_dup_decls(x, seen, vt);
12471            }
12472        }
12473        Stmt::LocalDef { var, init, .. } => {
12474            if seen.contains(var) {
12475                let ty = vt.var(*var).ty.clone();
12476                if let Some(e) = init.take() {
12477                    *s = Stmt::ExprStmt(Expr::Assign {
12478                        target: Box::new(Expr::Local { var: *var, ty }),
12479                        op: AssignOp::Plain,
12480                        value: Box::new(e),
12481                    });
12482                } else {
12483                    *s = Stmt::Block(vec![]);
12484                }
12485            } else {
12486                seen.insert(*var);
12487            }
12488        }
12489        other => {
12490            walk_mut(other, &mut |x| demote_dup_decls(x, seen, vt));
12491        }
12492    }
12493}
12494
12495#[allow(dead_code)]
12496fn unused(_: &Vec<CaseGroup>, _: &Catch) {}
12497
12498// ---------------------------------------------------------------------------
12499// jadx-style local names (ApplyVariableNames + dexdec's port of it)
12500// ---------------------------------------------------------------------------
12501
12502/// A synthetic `v12`/`p3` never survives when a better name exists.
12503/// Priority: (1) a Kotlin `Intrinsics.checkNotNullParameter(x, "name")`
12504/// names `x` from the message string (the string IS the parameter name);
12505/// (2) the single defining call — `getFoo()` → `foo`, `isFinishing()` →
12506/// `finishing`, `new File(…)` → `file`; (3) a type alias or the
12507/// lowercased class simple name. Collisions take `2`, `3`, …; a var with
12508/// two DISAGREEING defining calls stays unnamed (dexdec's
12509/// RelationalNameInference rule). Debug-info names are never touched
12510/// (`synthetic_name` gates the whole pass).
12511pub fn apply_local_names(vt: &mut VarTable, body: &Stmt) {
12512    use std::collections::{HashMap, HashSet};
12513
12514    // ---- proposals ------------------------------------------------------
12515    // var → (name, source-priority); disagreeing call names are dropped.
12516    let mut by_call: HashMap<u32, Vec<String>> = HashMap::new();
12517    let mut by_intrinsics: HashMap<u32, String> = HashMap::new();
12518
12519    visit_all_exprs(body, &mut |e| {
12520        if let Expr::Method { cls, name, args, .. } = e {
12521            if (cls.as_ref() == "kotlin/jvm/internal/Intrinsics"
12522                || cls.as_ref() == "kotlin/jvm/internal/IntrinsicsKt")
12523                && matches!(name.as_ref(), "checkNotNullParameter" | "checkParameterIsNotNull")
12524                && args.len() == 2
12525            {
12526                if let (Expr::Local { var, .. }, Expr::Const(ConstVal::Str(s))) = (&args[0], &args[1])
12527                {
12528                    by_intrinsics
12529                        .entry(*var)
12530                        .or_insert_with(|| sanitize_name(s).unwrap_or_default());
12531                }
12532            }
12533        }
12534    });
12535
12536    walk_all(body, &mut |st| {
12537        let (var, value) = match st {
12538            Stmt::LocalDef { var, init: Some(e), .. } => (*var, e),
12539            Stmt::ExprStmt(Expr::Assign { target, value, .. })
12540                if matches!(&**target, Expr::Local { .. }) =>
12541            {
12542                let Expr::Local { var, .. } = &**target else { return };
12543                (*var, value.as_ref())
12544            }
12545            _ => return,
12546        };
12547        if let Some(n) = defining_call_name(value) {
12548            by_call.entry(var).or_default().push(n);
12549        }
12550    });
12551
12552    // ---- reservation + application --------------------------------------
12553    // Real (debug-info) names can legally REPEAT across sibling source
12554    // scopes (`int i` in two loops) but our flat declaration hoisting
12555    // puts them in one scope — de-duplicate with the same numeric
12556    // suffixes the claim path uses.
12557    // Uniqueness operates on the SANITIZED display name: the identifier
12558    // sanitizer maps non-ASCII to `_`, so distinct debug names
12559    // (`ERROR_token参数缺失` / `ERROR_channelId参数缺失`) both RENDER as
12560    // `ERROR________` and collide as declarations. Tracking raw names
12561    // here would let the collision through.
12562    let sanit = |n: &str| crate::classdec::java_ident(n).into_owned();
12563    let mut taken: HashSet<String> = HashSet::default();
12564    // Parameter names are occupied REGARDLESS of syntheticness: claim()
12565    // hands out rename candidates, and without the synthetic params in
12566    // the pool a type fallback (`l7.p0` → "p0") renamed one parameter
12567    // ONTO another's slot-name (`b(v6.l p0, Object obj, l7.p0 p0)`).
12568    for v in vt.vars.iter() {
12569        // ALL synthetic names occupy their names, not just parameters:
12570        // a type fallback (`pc5.v56` → "v56") otherwise renames a local
12571        // ONTO another local's slot-name (`int v56;` beside
12572        // `pc5.v56 v56;`).
12573        if v.synthetic_name {
12574            taken.insert(sanit(&v.name));
12575        }
12576    }
12577    for v in vt.vars.iter_mut() {
12578        if v.synthetic_name {
12579            continue;
12580        }
12581        if !taken.insert(sanit(&v.name)) {
12582            let base = sanit(&v.name);
12583            for i in 2.. {
12584                let cand = format!("{base}{i}");
12585                if taken.insert(cand.clone()) {
12586                    v.name = cand;
12587                    break;
12588                }
12589            }
12590        }
12591    }
12592    let claim = |taken: &mut HashSet<String>, want: &str| -> String {
12593        let w = crate::classdec::java_ident(want).into_owned();
12594        if taken.insert(w.clone()) {
12595            return w;
12596        }
12597        for i in 2.. {
12598            let cand = format!("{w}{i}");
12599            if taken.insert(cand.clone()) {
12600                return cand;
12601            }
12602        }
12603        w
12604    };
12605
12606    for info in vt.vars.iter_mut() {
12607        if !info.synthetic_name {
12608            continue;
12609        }
12610        // (1) Intrinsics string — the Kotlin compiler wrote the real
12611        // parameter name right into the check.
12612        if let Some(n) = by_intrinsics.get(&info.id) {
12613            if !n.is_empty() {
12614                info.name = claim(&mut taken, n);
12615                continue;
12616            }
12617        }
12618        // (2) One consistent defining call.
12619        if let Some(names) = by_call.get(&info.id) {
12620            let unique: HashSet<&String> = names.iter().collect();
12621            if unique.len() == 1 {
12622                let n = unique.into_iter().next().unwrap();
12623                if !is_java_keyword(n) {
12624                    info.name = claim(&mut taken, n);
12625                    continue;
12626                }
12627            }
12628        }
12629        // (3) Type alias, else the lowercased simple class name
12630        // (jadx names a `Looper` local `looper`).
12631        let want = type_alias(&info.ty).map(str::to_string).or_else(|| simple_type_name(&info.ty));
12632        if let Some(n) = want {
12633            info.name = claim(&mut taken, &n);
12634        }
12635    }
12636
12637    // Final uniquification across ALL vars (params, locals, catch): the
12638    // reservation, claim and debug-name domains above are checked
12639    // pairwise but a name claimed LAST can still equal a debug name
12640    // that never re-checked (`zd1.v2 v2` beside `zd1.v2[] v2`).
12641    let mut seen: HashSet<String> = HashSet::default();
12642    for v in vt.vars.iter_mut() {
12643        if !seen.insert(sanit(&v.name)) {
12644            let base = sanit(&v.name);
12645            for i in 2.. {
12646                let cand = format!("{base}{i}");
12647                if seen.insert(cand.clone()) {
12648                    v.name = cand;
12649                    break;
12650                }
12651            }
12652        }
12653    }
12654}
12655
12656/// `com/android/.../Looper` → `looper` — the alias-table fallback.
12657fn simple_type_name(ty: &TypeRef) -> Option<String> {
12658    let TypeRef::J(JavaType::Object(n)) = ty else {
12659        return None;
12660    };
12661    let simple = n.rsplit('/').next().unwrap_or(n);
12662    let simple = simple.rsplit('$').next().unwrap_or(simple);
12663    // Lowercase the first ALPHABETIC char — `$`/`_`-prefixed names
12664    // (R8's `$$$_Thread`) must not survive capitalized.
12665    let mut chars = simple.chars();
12666    let mut out = String::with_capacity(simple.len());
12667    let mut lowered = false;
12668    for c in chars.by_ref() {
12669        if c.is_ascii_alphabetic() && !lowered {
12670            out.push(c.to_ascii_lowercase());
12671            lowered = true;
12672        } else {
12673            out.push(c);
12674        }
12675        if lowered {
12676            break;
12677        }
12678    }
12679    out.extend(chars);
12680    sanitize_name(&out)
12681}
12682
12683/// `getFoo()` → `foo`, `isFinishing()` → `finishing`, `new File(…)` →
12684/// `file`. The get/is prefixes only strip when the remainder starts
12685/// uppercase (so `issues()` keeps its name).
12686fn defining_call_name(e: &Expr) -> Option<String> {
12687    match e {
12688        Expr::Method { name, .. } => {
12689            let base = name
12690                .strip_prefix("get")
12691                .or_else(|| name.strip_prefix("is"))
12692                .filter(|rest| rest.chars().next().is_some_and(|c| c.is_ascii_uppercase()))
12693                .unwrap_or(name);
12694            let first = base.chars().next()?;
12695            let mut out = String::with_capacity(base.len());
12696            out.push(first.to_ascii_lowercase());
12697            out.extend(base.chars().skip(1));
12698            sanitize_name(&out)
12699        }
12700        Expr::New { cls, .. } => {
12701            let simple = cls.rsplit('/').next().unwrap_or(cls);
12702            let simple = simple.rsplit('$').next().unwrap_or(simple);
12703            let first = simple.chars().next()?;
12704            let mut out = String::with_capacity(simple.len());
12705            out.push(first.to_ascii_lowercase());
12706            out.extend(simple.chars().skip(1));
12707            sanitize_name(&out)
12708        }
12709        _ => None,
12710    }
12711}
12712
12713/// Valid java identifier, not a keyword/restricted name, ≥2 chars.
12714fn sanitize_name(s: &str) -> Option<String> {
12715    let mut out = String::with_capacity(s.len());
12716    for c in s.chars() {
12717        if c.is_ascii_alphanumeric() || c == '_' || c == '$' {
12718            out.push(c);
12719        } else {
12720            out.push('_');
12721        }
12722    }
12723    if out.len() < 2
12724        || out.chars().next().is_some_and(|c| c.is_ascii_digit())
12725        || is_java_keyword(&out)
12726    {
12727        return None;
12728    }
12729    Some(out)
12730}
12731
12732/// jadx's alias table for the common types, with the Android globals the
12733/// corpus actually shows; anything else falls back in the caller.
12734fn type_alias(ty: &TypeRef) -> Option<&'static str> {
12735    let TypeRef::J(JavaType::Object(n)) = ty else {
12736        return None;
12737    };
12738    Some(match n.as_ref() {
12739        "java/lang/String" | "kotlin/String" => "str",
12740        "java/lang/Class" => "cls",
12741        "java/lang/Throwable" => "th",
12742        "java/lang/Object" | "kotlin/Any" => "obj",
12743        "java/util/Iterator" | "kotlin/collections/Iterator" => "it",
12744        "java/lang/Boolean" => "bool",
12745        "java/lang/Integer" => "num",
12746        "java/lang/Character" => "ch",
12747        "java/lang/Byte" => "b",
12748        "java/lang/Short" => "sh",
12749        "java/lang/Float" => "f",
12750        "java/lang/Double" => "d",
12751        "java/lang/Long" => "i",
12752        "java/lang/StringBuilder" => "sb",
12753        "java/util/ArrayList" => "list",
12754        "java/util/HashMap" => "map",
12755        "android/content/Context" => "context",
12756        "android/content/Intent" => "intent",
12757        "android/os/Bundle" => "bundle",
12758        "android/view/View" => "view",
12759        "android/graphics/Bitmap" => "bitmap",
12760        "android/view/ViewGroup" => "viewGroup",
12761        _ => return None,
12762    })
12763}
12764
12765/// Every expression in the tree, statements included (read-only).
12766pub(crate) fn visit_all_exprs<F: FnMut(&Expr)>(s: &Stmt, f: &mut F) {
12767    walk_all(s, &mut |st| {
12768        let exprs: Vec<&Expr> = match st {
12769            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
12770                let mut v: Vec<&Expr> = vec![value];
12771                if !matches!(&**target, Expr::Local { .. }) {
12772                    v.push(target);
12773                }
12774                v
12775            }
12776            Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
12777                vec![e]
12778            }
12779            Stmt::Return(Some(e)) => vec![e],
12780            Stmt::LocalDef { init: Some(e), .. } => vec![e],
12781            Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
12782                vec![cond]
12783            }
12784            _ => Vec::new(),
12785        };
12786        for e in exprs {
12787            visit_exprs(e, f);
12788        }
12789    });
12790}
12791
12792/// IntDef/LongDef exact-match rendering: a literal argument to a
12793/// platform method whose parameter carries a constant domain renders as
12794/// the named constant (`setVisibility(8)` → `android.view.View.GONE`).
12795/// Combined flag values and literals outside the domain stay numeric.
12796pub fn platform_constants(body: &mut Stmt) {
12797    if !crate::platform::installed() {
12798        return;
12799    }
12800    rewrite_exprs(body, &mut |e| {
12801        if let Expr::Method { cls, name, desc, args, .. } = e {
12802            for a in args.iter_mut() {
12803                let (value, is_long) = match a {
12804                    Expr::Const(ConstVal::Int(v)) => (*v as i64, false),
12805                    Expr::Const(ConstVal::Long(v)) => (*v, true),
12806                    _ => continue,
12807                };
12808                if let Some(named) =
12809                    crate::platform::named_constant(cls, name, &desc_to_string(desc), 0, value)
12810                {
12811                    let _ = is_long;
12812                    *a = Expr::Raw(named);
12813                }
12814            }
12815        }
12816    });
12817}
12818
12819/// MethodDescriptor → dex descriptor string (`(I)V`).
12820fn desc_to_string(d: &jdc_core::types::MethodDescriptor) -> String {
12821    let mut out = String::from("(");
12822    for a in &d.args {
12823        type_to_desc(a, &mut out);
12824    }
12825    out.push(')');
12826    type_to_desc(&d.ret, &mut out);
12827    out
12828}
12829
12830fn type_to_desc(t: &jdc_core::types::JavaType, out: &mut String) {
12831    match t {
12832        JavaType::Void => out.push('V'),
12833        JavaType::Boolean => out.push('Z'),
12834        JavaType::Byte => out.push('B'),
12835        JavaType::Char => out.push('C'),
12836        JavaType::Short => out.push('S'),
12837        JavaType::Int => out.push('I'),
12838        JavaType::Float => out.push('F'),
12839        JavaType::Long => out.push('J'),
12840        JavaType::Double => out.push('D'),
12841        JavaType::Object(n) => {
12842            out.push('L');
12843            out.push_str(n);
12844            out.push(';');
12845        }
12846        JavaType::Array(inner) => {
12847            out.push('[');
12848            type_to_desc(inner, out);
12849        }
12850    }
12851}
12852
12853/// Kotlin `Intrinsics` null-check elision (jadx's ProcessKotlinInternals):
12854/// statement-position `checkNotNullParameter(p, "name")` /
12855/// `checkParameterIsNotNull` / `checkNotNull…` calls are runtime
12856/// assertions — drop them. The naming pass harvests the parameter
12857/// strings FIRST, so run this after `apply_local_names`.
12858pub fn remove_kotlin_checks(s: &mut Stmt) {
12859    walk_mut_deep(s, &mut |st| {
12860        let Stmt::Block(v) = st else { return };
12861        v.retain(|x| !is_kotlin_check(x));
12862    });
12863}
12864
12865fn is_kotlin_check(s: &Stmt) -> bool {
12866    let Stmt::ExprStmt(e) = s else { return false };
12867    let Expr::Method { cls, name, .. } = e else { return false };
12868    (cls.as_ref() == "kotlin/jvm/internal/Intrinsics" || cls.as_ref() == "kotlin/jvm/internal/IntrinsicsKt__Jdk7Kt")
12869        && matches!(
12870            name.as_ref(),
12871            "checkNotNullParameter"
12872                | "checkParameterIsNotNull"
12873                | "checkExpressionValueIsNotNull"
12874                | "checkNotNullExpressionValue"
12875                | "checkReturnedValueIsNotNull"
12876                | "checkFieldIsNotNull"
12877                | "checkNotNull"
12878        )
12879}
12880
12881// ---------------------------------------------------------------------------
12882// Synthetic-accessor inlining (jadx's MarkMethodsForInline, the safe subset)
12883// ---------------------------------------------------------------------------
12884
12885/// Inline `access$NNN`-style synthetic static bridges at their call
12886/// sites: an identity (`return pN`), a getter (`iget pX, field;
12887/// return vR`), or a method forwarder (`invoke; return`) — with the
12888/// d8 APM trace wrappers (`MethodCollector.i/.o` const-only static
12889/// calls) tolerated around the core. Only STATIC + SYNTHETIC callees
12890/// qualify (compiler-generated bridges: no override semantics, no
12891/// side effects beyond the forwarded operation).
12892/// Can `host` (internal class name) source-level reference `target`'s
12893/// class chain and member? Cross-package inlining of a synthetic
12894/// accessor used to EXPOSE package-private targets the raw dex never
12895/// referenced cross-package (BuildersKt.launch$default forwards to
12896/// kotlinx.coroutines.k.e — the widening census cannot see post-inline
12897/// calls; lark's 1,019-line k不是公共的 family). Non-public chain link
12898/// or non-public member across packages → the inline must not happen
12899/// (the public synthetic bridge stays and compiles).
12900fn inline_ref_ok(pool: &DexPool, host: &str, target: &str, member: Option<(&str, bool)>) -> bool {
12901    let hpkg = match host.rfind('/') {
12902        Some(i) => &host[..i],
12903        None => "",
12904    };
12905    let mut cur = target;
12906    loop {
12907        let tpkg = match cur.rfind('/') {
12908            Some(i) => &cur[..i],
12909            None => "",
12910        };
12911        if tpkg != hpkg {
12912            if let Some(pc) = pool.get(cur) {
12913                if pc.access & crate::access::ACC_PUBLIC == 0 {
12914                    return false;
12915                }
12916                if cur == target {
12917                    if let Some((mname, is_method)) = member {
12918                        let ok = if is_method {
12919                            pc.all_methods().any(|m| {
12920                                &*m.name == mname
12921                                    && m.access & crate::access::ACC_PUBLIC != 0
12922                            })
12923                        } else {
12924                            pc.static_fields
12925                                .iter()
12926                                .chain(pc.instance_fields.iter())
12927                                .any(|f| {
12928                                    f.name == mname
12929                                        && f.access & crate::access::ACC_PUBLIC != 0
12930                                })
12931                        };
12932                        // A member match requires the exact member to be
12933                        // public; an absent member (renamed/collapsed)
12934                        // stays allowed — the render will show what the
12935                        // pool has.
12936                        if !ok
12937                            && (if is_method {
12938                                pc.all_methods().any(|m| &*m.name == mname)
12939                            } else {
12940                                pc.static_fields
12941                                    .iter()
12942                                    .chain(pc.instance_fields.iter())
12943                                    .any(|f| f.name == mname)
12944                            })
12945                        {
12946                            return false;
12947                        }
12948                    }
12949                }
12950            }
12951        }
12952        match cur.rfind('$') {
12953            Some(i) if i > 0 => cur = &cur[..i],
12954            _ => break,
12955        }
12956    }
12957    true
12958}
12959
12960pub fn inline_accessors(s: &mut Stmt, pool: &DexPool, host: &str) {
12961    rewrite_exprs(s, &mut |e| {
12962        let Expr::Method { cls, name, desc, args, is_static, .. } = e else { return };
12963        if !*is_static || args.is_empty() {
12964            return;
12965        }
12966        let Some(target) = pool.get(cls) else { return };
12967        let desc_s = desc_to_string(desc);
12968        let Some(m) = target.find_method(name, &desc_s) else { return };
12969        if !(m.is_static() && m.access & access::ACC_SYNTHETIC != 0) {
12970            return;
12971        }
12972        let Some(dex) = pool.dex(m.dex_idx) else { return };
12973        // Snapshot first: the accessor's owning image may already be
12974        // retired, and a live read's success would depend on worker
12975        // completion interleaving — nondeterministic inlining (whether
12976        // the accessor folds at all) between identical runs.
12977        let code = match pool.accessor_code(m.dex_idx, m.code_off) {
12978            Some(bytes) => ddc_dex::CodeItem::parse(&bytes, 0),
12979            None => dex.code_at(m.code_off),
12980        };
12981        let Some(code) = code else { return };
12982        let insns: Vec<&Insn> =
12983            code.insns.iter().filter(|i| !matches!(i.kind, InsnKind::Nop)).collect();
12984        // Strip the APM trace wrappers (const-only invoke-static).
12985        let core: Vec<&Insn> = insns
12986            .iter()
12987            .copied()
12988            .filter(|i| !is_const_only_trace(&i.kind, &insns))
12989            .collect();
12990        match accessor_shape(&dex, &core, &code) {
12991            Some(Shape::Identity(arg_i)) => {
12992                if let Some(a) = args.get(arg_i) {
12993                    *e = a.clone();
12994                }
12995            }
12996            Some(Shape::FieldRead { arg, cls: field_cls, field, field_ty }) => {
12997                if !inline_ref_ok(pool, host, &field_cls, Some((&field, false))) {
12998                    return;
12999                }
13000                if let Some(owner) = args.get(arg).cloned().map(Box::new) {
13001                    let ty = TypeRef::J(desc_type(&field_ty));
13002                    // The inlined read must consult the member rename
13003                    // registry like every lifted field ref does — the
13004                    // raw dex name desynced from renamed declarations
13005                    // (lark uu4/f$b's `a`→`ax6`: 找不到符号 变量 a ×4.4k
13006                    // when the nested-collision deshadow fired).
13007                    let name = jdc_core::rename::field_display(
13008                        &field_cls,
13009                        &field,
13010                        &field_ty,
13011                    )
13012                    .map(std::sync::Arc::from)
13013                    .unwrap_or_else(|| field.into());
13014                    *e = Expr::Field {
13015                        owner: Some(owner),
13016                        cls: field_cls.into(),
13017                        name,
13018                        ty,
13019                        is_static: false,
13020                    };
13021                }
13022            }
13023            Some(Shape::Forward { cls: tcls, name: tname, desc: tdesc, instance }) => {
13024                if !inline_ref_ok(pool, host, &tcls, Some((&tname, true))) {
13025                    return;
13026                }
13027                // Same registry duty as FieldRead: the forwarded member
13028                // must render under its renamed display, not the raw
13029                // dex name.
13030                let disp = jdc_core::rename::field_display(
13031                    &tcls,
13032                    &tname,
13033                    &tdesc.to_string(),
13034                )
13035                .map(std::sync::Arc::from)
13036                .unwrap_or_else(|| tname.into());
13037                *cls = tcls.into();
13038                *name = disp;
13039                *desc = std::sync::Arc::new(tdesc);
13040                if instance && !args.is_empty() {
13041                    // The first param (the receiver) becomes the owner.
13042                    let recv = args.remove(0);
13043                    if let Expr::Method { owner, .. } = e {
13044                        *owner = Some(Box::new(recv));
13045                    }
13046                }
13047            }
13048            None => {}
13049        }
13050    });
13051}
13052
13053/// What an accessor body reduces to.
13054enum Shape {
13055    /// `return pN` — the call becomes argument N.
13056    Identity(usize),
13057    /// `iget vR, pX, field; return vR` — becomes `argN.field`.
13058    FieldRead { arg: usize, cls: String, field: String, field_ty: String },
13059    /// `invoke {pX, args…}, method@M; (move-result;)? return` —
13060    /// becomes the forwarded call.
13061    Forward { cls: String, name: String, desc: MethodDescriptor, instance: bool },
13062}
13063
13064/// Resolve the IGet field's owner class, name and type descriptor.
13065fn field_of(dex: &ddc_dex::DexFile, field_idx: u32) -> Option<(String, String, String)> {
13066    let f = dex.field(field_idx);
13067    Some((
13068        // class_name strips the `L...;` shell; type_name would leak a
13069        // descriptor into the Field's owner class (`La3.a.e_`).
13070        dex.class_name(f.class_idx),
13071        dex.string(f.name_idx).to_string(),
13072        // The FIELD TYPE stays a descriptor: desc_type parses it.
13073        dex.type_name(f.type_idx).to_string(),
13074    ))
13075}
13076
13077/// Map a register to a parameter index (static method: params occupy
13078/// the LAST ins_size registers).
13079fn param_index(code: &ddc_dex::CodeItem, reg: u16) -> Option<usize> {
13080    let rs = code.registers_size as usize;
13081    let ins = code.ins_size as usize;
13082    let r = reg as usize;
13083    let first = rs.checked_sub(ins)?;
13084    (r >= first && r < rs).then(|| r - first)
13085}
13086
13087fn accessor_shape(
13088    dex: &ddc_dex::DexFile,
13089    core: &[&Insn],
13090    code: &ddc_dex::CodeItem,
13091) -> Option<Shape> {
13092    // Dead consts that fed the stripped trace wrappers remain in the
13093    // core — skip them. ONLY when actually dead (dst unread by the
13094    // rest): a const-returning synthetic (`lambda$new$4(String) {
13095    // return false; }` = `const/4 v0,0; return v0`, registers=1 so v0
13096    // IS the param) lost its const and matched Identity(param0) —
13097    // inline_accessors then replaced the CALL with its ARGUMENT
13098    // (`return (String) obj` against a boolean SAM — weibo
13099    // IntentSanitizer lambdas ×116, and silently WRONG renders
13100    // wherever the types happened to agree).
13101    let reads = |rest: &[&Insn], r: u16| {
13102        rest.iter()
13103            .any(|i| crate::lift::src_regs(&i.kind).contains(&r))
13104    };
13105    let core: &[&Insn] = match core.split_first() {
13106        Some((first, rest))
13107            if matches!(first.kind, InsnKind::Const { dst, .. } if !reads(rest, dst)) =>
13108        {
13109            rest
13110        }
13111        _ => core,
13112    };
13113    let core: &[&Insn] = match core.split_last() {
13114        Some((last, rest))
13115            if matches!(last.kind, InsnKind::Const { dst, .. } if !reads(rest, dst)) =>
13116        {
13117            rest
13118        }
13119        _ => core,
13120    };
13121    match core {
13122        // return pN
13123        [Insn { kind: InsnKind::Return { src }, .. }] => {
13124            let idx = param_index(code, *src)?;
13125            Some(Shape::Identity(idx))
13126        }
13127        // iget vR, pX, field; return vR
13128        [
13129            Insn { kind: InsnKind::IGet { dst, obj, field_idx }, .. },
13130            Insn { kind: InsnKind::Return { src }, .. },
13131        ] if dst == src => {
13132            let arg = param_index(code, *obj)?;
13133            let (cls, field, field_ty) = field_of(dex, *field_idx)?;
13134            Some(Shape::FieldRead { arg, cls, field, field_ty })
13135        }
13136        // invoke {…}, method@M; (move-result; return)? — forwarder.
13137        [Insn { kind: InsnKind::Invoke { method_idx, regs, kind, .. }, .. }, rest @ ..] if rest.len() <= 2 => {
13138            // Only forward when every register is a param (no locals).
13139            if !regs.iter().all(|r| param_index(code, *r).is_some()) || regs.is_empty() {
13140                return None;
13141            }
13142            // Trailing must be move-result+return or return/void —
13143            // anything else (e.g. trace calls) already filtered upstream.
13144            if rest.len() == 2 {
13145                let ok = matches!(rest[0].kind, InsnKind::MoveResult { .. })
13146                    && matches!(rest[1].kind, InsnKind::Return { .. } | InsnKind::ReturnVoid);
13147                if !ok {
13148                    return None;
13149                }
13150            }
13151            let mid = dex.method(*method_idx);
13152            // class_name strips the `L...;` shell — type_name leaks a
13153            // descriptor into the forwarded call's class (`La3.a.e_`).
13154            let cls = dex.class_name(mid.class_idx);
13155            let name = dex.string(mid.name_idx).to_string();
13156            let mut s = String::from("(");
13157            for t in dex.proto_params(mid.proto_idx) {
13158                s.push_str(dex.type_name(*t));
13159            }
13160            s.push(')');
13161            s.push_str(dex.type_name(dex.proto(mid.proto_idx).return_type_idx));
13162            let desc = parse_method_descriptor(&s)?;
13163            Some(Shape::Forward { cls, name, desc, instance: !matches!(kind, InvokeKind::Static) })
13164        }
13165        _ => None,
13166    }
13167}
13168
13169
13170/// A const-only invoke-static (the d8 APM `i(732046)` / `o(732046)`
13171/// trace wrapper): every argument register is loaded by a Const (or a
13172/// Const-into-move chain) in the SAME body. Scoped to synthetic
13173/// accessors, so a genuinely side-effecting const call is never
13174/// mistaken for a trace.
13175fn is_const_only_trace(kind: &InsnKind, insns: &[&Insn]) -> bool {
13176    let InsnKind::Invoke { kind, regs, .. } = kind else { return false };
13177    if !matches!(kind, InvokeKind::Static) || regs.is_empty() {
13178        return false;
13179    }
13180    regs.iter().all(|r| const_loaded(*r, insns, 0))
13181}
13182
13183/// Const-loaded directly, or through a Move chain from a const-loaded
13184/// register (the APM `const v0, id` → `invoke {v0}` shape).
13185fn const_loaded(r: u16, insns: &[&Insn], depth: u8) -> bool {
13186    if depth > 3 {
13187        return false;
13188    }
13189    insns.iter().any(|i| match &i.kind {
13190        InsnKind::Const { dst, .. } => *dst == r,
13191        InsnKind::Move { dst, src } => *dst == r && const_loaded(*src, insns, depth + 1),
13192        _ => false,
13193    })
13194}