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 std::collections::HashMap<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: &std::collections::HashMap<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    strip_empty(s);
1142    merge_singleton_stmts(s);
1143}
1144
1145fn strip_empty(s: &mut Stmt) {
1146    match s {
1147        Stmt::Block(v) => {
1148            v.retain(|x| !x.is_empty_block());
1149            for x in v.iter_mut() {
1150                strip_empty(x);
1151            }
1152        }
1153        Stmt::If {
1154            then_stmt,
1155            else_stmt,
1156            ..
1157        } => {
1158            strip_empty(then_stmt);
1159            if let Some(e) = else_stmt {
1160                strip_empty(e);
1161                if e.is_empty_block() {
1162                    *else_stmt = None;
1163                }
1164            }
1165        }
1166        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => strip_empty(body),
1167        Stmt::For { init, body, .. } => {
1168            init.retain(|x| !x.is_empty_block());
1169            strip_empty(body);
1170        }
1171        Stmt::ForEach { body, .. } => strip_empty(body),
1172        Stmt::Switch { cases, default, .. } => {
1173            for c in cases {
1174                c.body.retain(|x| !x.is_empty_block());
1175            }
1176            if let Some(d) = default {
1177                strip_empty(d);
1178            }
1179        }
1180        Stmt::Try {
1181            body,
1182            catches,
1183            finally,
1184        } => {
1185            strip_empty(body);
1186            for c in catches {
1187                strip_empty(&mut c.body);
1188            }
1189            if let Some(f) = finally {
1190                strip_empty(f);
1191            }
1192        }
1193        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => strip_empty(body),
1194        _ => {}
1195    }
1196}
1197
1198fn merge_singleton_stmts(s: &mut Stmt) {
1199    if let Stmt::Block(v) = s {
1200        for x in v.iter_mut() {
1201            merge_singleton_stmts(x);
1202        }
1203    }
1204}
1205
1206/// Drop statements after a definite terminator within a block.
1207pub fn prune_unreachable(s: &mut Stmt) {
1208    if let Stmt::Block(v) = s {
1209        let mut cut: Option<usize> = None;
1210        for (i, x) in v.iter().enumerate() {
1211            if matches!(x, Stmt::Return(_) | Stmt::Throw(_)) {
1212                cut = Some(i + 1);
1213                break;
1214            }
1215        }
1216        if let Some(c) = cut {
1217            if c < v.len() {
1218                v.truncate(c);
1219            }
1220        }
1221        for x in v.iter_mut() {
1222            prune_unreachable(x);
1223        }
1224    } else {
1225        walk_mut(s, &mut prune_unreachable);
1226    }
1227}
1228
1229fn walk_mut<F: FnMut(&mut Stmt)>(s: &mut Stmt, f: &mut F) {
1230    match s {
1231        Stmt::Block(v) => {
1232            for x in v.iter_mut() {
1233                f(x);
1234            }
1235        }
1236        Stmt::If {
1237            then_stmt,
1238            else_stmt,
1239            ..
1240        } => {
1241            f(then_stmt);
1242            if let Some(e) = else_stmt {
1243                f(e);
1244            }
1245        }
1246        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => f(body),
1247        Stmt::For { init, body, .. } => {
1248            for x in init.iter_mut() {
1249                f(x);
1250            }
1251            f(body);
1252        }
1253        Stmt::ForEach { body, .. } => f(body),
1254        Stmt::Switch { cases, default, .. } => {
1255            for c in cases {
1256                for x in c.body.iter_mut() {
1257                    f(x);
1258                }
1259            }
1260            if let Some(d) = default {
1261                f(d);
1262            }
1263        }
1264        Stmt::Try {
1265            body,
1266            catches,
1267            finally,
1268        } => {
1269            f(body);
1270            for c in catches {
1271                f(&mut c.body);
1272            }
1273            if let Some(fl) = finally {
1274                f(fl);
1275            }
1276        }
1277        Stmt::TryWithResources {
1278            resources,
1279            body,
1280            catches,
1281            finally,
1282        } => {
1283            for r in resources.iter_mut() {
1284                f(r);
1285            }
1286            f(body);
1287            for c in catches {
1288                f(&mut c.body);
1289            }
1290            if let Some(fl) = finally {
1291                f(fl);
1292            }
1293        }
1294        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => f(body),
1295        _ => {}
1296    }
1297}
1298
1299/// Count LocalDef statements (pipeline diagnostics).
1300pub fn count_localdefs(s: &Stmt, out: &mut usize) {
1301    if matches!(s, Stmt::LocalDef { .. }) {
1302        *out += 1;
1303    }
1304    walk_all(s, &mut |x| {
1305        if matches!(x, Stmt::LocalDef { .. }) {
1306            *out += 1;
1307        }
1308    });
1309}
1310
1311/// Drop a trailing `return;` (d8's explicit return-void at method end).
1312pub fn strip_trailing_void_return(s: &mut Stmt) {
1313    if let Stmt::Block(v) = s {
1314        loop {
1315            match v.last_mut() {
1316                Some(Stmt::Return(None)) => {
1317                    v.pop();
1318                }
1319                // The structurer can close a region in a bare nested
1320                // block; a `return;` at its tail is still a dangling
1321                // statement (inside a static initializer it is illegal
1322                // outright — clinit's closing return).
1323                Some(Stmt::Block(_)) => {
1324                    let last = v.last_mut().unwrap();
1325                    let before = match last {
1326                        Stmt::Block(b) => b.len(),
1327                        _ => 0,
1328                    };
1329                    strip_trailing_void_return(last);
1330                    let after = match last {
1331                        Stmt::Block(b) => b.len(),
1332                        _ => 0,
1333                    };
1334                    if after == before {
1335                        break; // nothing stripped inside
1336                    }
1337                }
1338                _ => break,
1339            }
1340        }
1341    }
1342}
1343
1344/// `if (c) { } else { B }` → `if (!c) { B }` — the empty-then shape is
1345/// how the structurer lands an inverted diamond, and source never writes
1346/// it (jadx prints the positive form). negate() carries De Morgan so
1347/// composed conditions stay clean.
1348pub fn invert_empty_thens(s: &mut Stmt) {
1349    walk_mut_deep(s, &mut |st| {
1350        if let Stmt::If {
1351            cond,
1352            then_stmt,
1353            else_stmt,
1354        } = st
1355        {
1356            let then_empty = matches!(&**then_stmt, Stmt::Block(v) if v.is_empty());
1357            if then_empty {
1358                if let Some(els) = else_stmt.take() {
1359                    let c = std::mem::replace(cond, Expr::Const(ConstVal::Int(0)));
1360                    *cond = jdc_core::convert::negate(c);
1361                    *then_stmt = els;
1362                }
1363            }
1364        }
1365    });
1366}
1367
1368/// Fold if-diamonds back into short-circuit conditions (DAD's
1369/// short_circuit_struct, statement level). The structurer emits nested
1370/// ifs for `a && b` / `a || b` bytecode diamonds; javac source almost
1371/// never nests them, and jadx folds all four shapes:
1372///   if (c1) { if (c2) {T} else {F} } else {F}  →  if (c1 && c2) {T} else {F}
1373///   if (c1) { if (c2) {F} else {T} } else {F}  →  if (c1 && !c2) {T} else {F}
1374///   if (c1) {T} else { if (c2) {T} else {F} }  →  if (c1 || c2) {T} else {F}
1375///   if (c1) {T} else { if (c2) {F} else {T} }  →  if (c1 || !c2) {T} else {F}
1376/// Branch identity is deep structural equality (Stmt: PartialEq);
1377/// iterate to a fixpoint so chained diamonds `(a && b) && c` collapse.
1378pub fn fold_short_circuits(s: &mut Stmt) {
1379    for _ in 0..8 {
1380        let mut changed = false;
1381        walk_mut_deep(s, &mut |st| {
1382            if try_fold_diamond(st) {
1383                changed = true;
1384            }
1385        });
1386        if !changed {
1387            break;
1388        }
1389    }
1390}
1391
1392fn try_fold_diamond(st: &mut Stmt) -> bool {
1393    enum Shape {
1394        ThenAnd,
1395        ThenAndNot,
1396        ElseOr,
1397        ElseOrNot,
1398        BareAnd,
1399        BareAndNot,
1400    }
1401    // Probe the shape on an immutable borrow first (the fold takes the
1402    // whole If apart; matching and rebuilding inside one borrow is not
1403    // expressible).
1404    let shape = match &*st {
1405        // One-sided diamonds: the shared branch is the implicit empty
1406        // fall-through (`if (c1) { if (c2) {T} }` → `if (c1 && c2) {T}`).
1407        Stmt::If {
1408            then_stmt,
1409            else_stmt: None,
1410            ..
1411        } => match &**then_stmt {
1412            Stmt::If {
1413                then_stmt: t2,
1414                else_stmt: None,
1415                ..
1416            } if !matches!(&**t2, Stmt::Block(v) if v.is_empty()) => Some(Shape::BareAnd),
1417            Stmt::If {
1418                then_stmt: t2,
1419                else_stmt: Some(_),
1420                ..
1421            } if matches!(&**t2, Stmt::Block(v) if v.is_empty()) => Some(Shape::BareAndNot),
1422            _ => None,
1423        },
1424        Stmt::If {
1425            then_stmt,
1426            else_stmt: Some(els),
1427            ..
1428        } => match (&**then_stmt, &**els) {
1429            (Stmt::If { else_stmt: Some(f2), .. }, _) if f2.as_ref() == els.as_ref() => {
1430                Some(Shape::ThenAnd)
1431            }
1432            (Stmt::If { then_stmt: t2, else_stmt: Some(_), .. }, _)
1433                if t2.as_ref() == els.as_ref() =>
1434            {
1435                Some(Shape::ThenAndNot)
1436            }
1437            (_, Stmt::If { then_stmt: t2, else_stmt: Some(_), .. })
1438                if t2.as_ref() == then_stmt.as_ref() =>
1439            {
1440                Some(Shape::ElseOr)
1441            }
1442            (_, Stmt::If { else_stmt: Some(f2), .. })
1443                if f2.as_ref() == then_stmt.as_ref() =>
1444            {
1445                Some(Shape::ElseOrNot)
1446            }
1447            _ => None,
1448        },
1449        _ => None,
1450    };
1451    let Some(shape) = shape else {
1452        return false;
1453    };
1454
1455    let taken = std::mem::replace(st, Stmt::Block(vec![]));
1456    // Bare (no-else) diamonds fold without an else at all.
1457    if matches!(shape, Shape::BareAnd | Shape::BareAndNot) {
1458        let Stmt::If {
1459            cond: c1,
1460            then_stmt,
1461            else_stmt: None,
1462        } = taken
1463        else {
1464            unreachable!()
1465        };
1466        let Stmt::If {
1467            cond: c2,
1468            then_stmt: t2,
1469            else_stmt: f2,
1470        } = *then_stmt
1471        else {
1472            unreachable!()
1473        };
1474        let sc = |op: BinOp, l: Expr, r: Expr| Expr::Bin {
1475            op,
1476            l: Box::new(l),
1477            r: Box::new(r),
1478            ty: None,
1479        };
1480        match shape {
1481            // if (c1 && c2) {T}
1482            Shape::BareAnd => {
1483                *st = Stmt::If {
1484                    cond: sc(BinOp::LogAnd, c1, c2),
1485                    then_stmt: t2,
1486                    else_stmt: None,
1487                };
1488            }
1489            // if (c1) { if (c2) {} else {T} }  →  if (c1 && !c2) {T}
1490            _ => {
1491                let body = f2.unwrap();
1492                *st = Stmt::If {
1493                    cond: sc(
1494                        BinOp::LogAnd,
1495                        c1,
1496                        Expr::Un {
1497                            op: UnOp::Not,
1498                            e: Box::new(c2),
1499                        },
1500                    ),
1501                    then_stmt: body,
1502                    else_stmt: None,
1503                };
1504            }
1505        }
1506        return true;
1507    }
1508    let Stmt::If {
1509        cond: c1,
1510        then_stmt,
1511        else_stmt: Some(els),
1512    } = taken
1513    else {
1514        unreachable!("shape probe guaranteed an If with else")
1515    };
1516    let sc = |op: BinOp, l: Expr, r: Expr| Expr::Bin {
1517        op,
1518        l: Box::new(l),
1519        r: Box::new(r),
1520        ty: None,
1521    };
1522    let not = |e: Expr| Expr::Un {
1523        op: UnOp::Not,
1524        e: Box::new(e),
1525    };
1526    match shape {
1527        Shape::BareAnd | Shape::BareAndNot => unreachable!("handled above"),
1528        Shape::ThenAnd | Shape::ThenAndNot => {
1529            let Stmt::If {
1530                cond: c2,
1531                then_stmt: t2,
1532                else_stmt: Some(f2),
1533            } = *then_stmt
1534            else {
1535                unreachable!()
1536            };
1537            let (nc, body, alt) = match shape {
1538                // if (c1 && c2) {T} else {F}
1539                Shape::ThenAnd => (sc(BinOp::LogAnd, c1, c2), t2, f2),
1540                // if (c1 && !c2) {T} else {F}   (inner: {F} else {T})
1541                _ => (sc(BinOp::LogAnd, c1, not(c2)), f2, t2),
1542            };
1543            *st = Stmt::If {
1544                cond: nc,
1545                then_stmt: body,
1546                else_stmt: Some(alt),
1547            };
1548        }
1549        Shape::ElseOr | Shape::ElseOrNot => {
1550            let Stmt::If {
1551                cond: c2,
1552                then_stmt: t2,
1553                else_stmt: Some(f2),
1554            } = *els
1555            else {
1556                unreachable!()
1557            };
1558            // The surviving then-body is the OUTER then (identical to the
1559            // matching inner branch by the probe).
1560            let body = then_stmt;
1561            let (nc, alt) = match shape {
1562                // if (c1 || c2) {T} else {F}   (inner: {T} else {F})
1563                Shape::ElseOr => (sc(BinOp::LogOr, c1, c2), f2),
1564                // if (c1 || !c2) {T} else {F}  (inner: {F} else {T})
1565                _ => (sc(BinOp::LogOr, c1, not(c2)), t2),
1566            };
1567            *st = Stmt::If {
1568                cond: nc,
1569                then_stmt: body,
1570                else_stmt: Some(alt),
1571            };
1572        }
1573    }
1574    true
1575}
1576
1577/// A static initializer cannot contain `return` in source form — the
1578/// clinit's closing return is a method-level artifact, but the
1579/// structurer can leave it nested inside loops/branches where the
1580/// top-level strip cannot see it (x2/a: `static { while { ...; return;
1581/// } }` — "return outside method", 184 sites on reqable).
1582pub fn strip_clinit_returns(s: &mut Stmt) {
1583    walk_mut_deep(s, &mut |st| {
1584        if let Stmt::Block(v) = st {
1585            v.retain(|x| !matches!(x, Stmt::Return(None)));
1586        }
1587    });
1588}
1589
1590pub fn prepend_comment(s: &mut Stmt, text: String) {
1591    let stmt = Stmt::Comment(text);
1592    match s {
1593        Stmt::Block(v) => v.insert(0, stmt),
1594        other => {
1595            let inner = std::mem::replace(other, Stmt::Block(vec![]));
1596            *other = Stmt::Block(vec![stmt, inner]);
1597        }
1598    }
1599}
1600
1601/// Fused expression rewrites that each used to walk the whole tree:
1602/// residual cmp sentinels, object-null comparisons, const-first compares.
1603/// One traversal, three rules — the separate walks were a top profile cost.
1604/// True when `e` evaluates to an object reference — a legal null-compare
1605/// side: locals via the reference table, everything else via its own type
1606/// (fields like `pi.versionName != 0` and call results are references too;
1607/// constants never are).
1608fn is_obj_expr(e: &Expr, obj_var: &dyn Fn(u32) -> bool) -> bool {
1609    match e {
1610        Expr::Local { var, .. } => obj_var(*var),
1611        Expr::Const(_) => false,
1612        _ => e.type_ref().erased().is_reference(),
1613    }
1614}
1615
1616/// `obj == 0` / `0 == obj` → `obj == null`: replace the CONST-0 side.
1617/// The previous shape replaced the LOCAL side instead — every object
1618/// null-check in the corpus rendered as `null != 0` (4,598 hits in
1619/// reqable alone) and the real operand vanished from the condition.
1620fn null_side_rewrite(x: &mut Expr, obj_var: &dyn Fn(u32) -> bool) {
1621    if let Expr::Bin { op, l, r, .. } = x {
1622        if !matches!(op, BinOp::Eq | BinOp::Ne) {
1623            return;
1624        }
1625        let l_zero = matches!(&**l, Expr::Const(ConstVal::Int(0)));
1626        let r_zero = matches!(&**r, Expr::Const(ConstVal::Int(0)));
1627        if l_zero && !r_zero && is_obj_expr(r, obj_var) {
1628            **l = Expr::Const(ConstVal::Null);
1629        } else if r_zero && !l_zero && is_obj_expr(l, obj_var) {
1630            **r = Expr::Const(ConstVal::Null);
1631        }
1632    }
1633}
1634
1635pub fn fused_expr_rewrites(s: &mut Stmt, vt: &VarTable) {
1636    // var ids are dense: a byte table beats a HashSet lookup.
1637    let mut obj_vars: Vec<bool> = Vec::with_capacity(vt.vars.len());
1638    for v in &vt.vars {
1639        obj_vars.push(v.ty.erased().is_reference());
1640    }
1641    rewrite_exprs(s, &mut |e| {
1642        deep_rewrite(e, &mut |x| {
1643            // 1. cmp sentinels.
1644            if let Expr::Invokedynamic { name, args, .. } = x {
1645                if name.starts_with('\0') && args.len() == 2 {
1646                    let (cls, ty) = match name.as_str() {
1647                        "\0cmp-long" => ("java/lang/Long", JavaType::Long),
1648                        "\0cmpl-float" | "\0cmpg-float" => ("java/lang/Float", JavaType::Float),
1649                        _ => ("java/lang/Double", JavaType::Double),
1650                    };
1651                    let desc = MethodDescriptor {
1652                        args: vec![ty.clone(), ty],
1653                        ret: JavaType::Int,
1654                    };
1655                    *x = Expr::Method {
1656                        owner: None,
1657                        cls: cls.into(),
1658                        name: "compare".into(),
1659                        desc: std::sync::Arc::new(desc),
1660                        args: args.clone(),
1661                        is_static: true,
1662                        is_interface: false,
1663                        is_special: false,
1664                        is_super: false,
1665                        is_dynamic: false,
1666                        type_args: vec![],
1667                    };
1668                    return;
1669                }
1670            }
1671            // 2. null compares (no any_obj gate: the object side may be
1672            // a field/call whose type is not in the local table).
1673            null_side_rewrite(x, &|v| obj_vars.get(v as usize).copied().unwrap_or(false));
1674            // 3. const-first compares.
1675            if let Expr::Bin { op, l, r, .. } = x {
1676                if matches!(
1677                    op,
1678                    BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
1679                ) {
1680                    let l_const = matches!(&**l, Expr::Const(_));
1681                    let r_var = matches!(&**r, Expr::Local { .. });
1682                    if l_const && r_var {
1683                        std::mem::swap(l, r);
1684                    }
1685                }
1686            }
1687        });
1688    });
1689    // 4. null into reference-typed assignment targets. The register
1690    //    machine stores null as const-0; return/throw/field-store/invoke
1691    //    argument paths were normalized in the lifter, but a null that
1692    //    MATERIALIZES into a local was rendered as `str = 0;` — javac
1693    //    "int cannot be converted to String" (the single biggest error
1694    //    family on real corpora: d8 emits null locals this way after
1695    //    every `x = null` branch).
1696    walk_mut_deep(s, &mut |st| {
1697        if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = st {
1698            let tgt_is_obj = match &**target {
1699                Expr::Local { var, .. } => {
1700                    obj_vars.get(*var as usize).copied().unwrap_or(false)
1701                }
1702                other => other.type_ref().erased().is_reference(),
1703            };
1704            if tgt_is_obj {
1705                if let Expr::Const(ConstVal::Int(0)) = &**value {
1706                    **value = Expr::Const(ConstVal::Null);
1707                }
1708            }
1709        } else if let Stmt::LocalDef { var, init, .. } = st {
1710            if obj_vars.get(*var as usize).copied().unwrap_or(false) {
1711                if let Some(iv) = init {
1712                    if let Expr::Const(ConstVal::Int(0)) = iv {
1713                        *iv = Expr::Const(ConstVal::Null);
1714                    }
1715                }
1716            }
1717        }
1718    });
1719}
1720
1721/// Replace residual cmp sentinels (`\0cmp*` Invokedynamic markers that never
1722/// reached a condition) with library compare calls.
1723#[allow(dead_code)]
1724pub fn desugar_cmp_residuals(s: &mut Stmt) {
1725    rewrite_exprs(s, &mut |e| {
1726        deep_rewrite(e, &mut |x| {
1727            if let Expr::Invokedynamic { name, args, .. } = x {
1728                if name.starts_with('\0') && args.len() == 2 {
1729                    let (cls, ty) = match name.as_str() {
1730                        "\0cmp-long" => ("java/lang/Long", JavaType::Long),
1731                        "\0cmpl-float" | "\0cmpg-float" => ("java/lang/Float", JavaType::Float),
1732                        _ => ("java/lang/Double", JavaType::Double),
1733                    };
1734                    let desc = jdc_core::types::MethodDescriptor {
1735                        args: vec![ty.clone(), ty],
1736                        ret: JavaType::Int,
1737                    };
1738                    *x = Expr::Method {
1739                        owner: None,
1740                        cls: cls.into(),
1741                        name: "compare".into(),
1742                        desc: std::sync::Arc::new(desc),
1743                        args: args.clone(),
1744                        is_static: true,
1745                        is_interface: false,
1746                        is_special: false,
1747                        is_super: false,
1748                        is_dynamic: false,
1749                        type_args: vec![],
1750                    };
1751                }
1752            }
1753        });
1754    });
1755}
1756
1757/// `if (c) { x = A; } else { x = B; }` → `x = c ? A : B;`
1758pub fn ternary_fold(s: &mut Stmt) {
1759    let mut changed = true;
1760    let mut guard = 0;
1761    while changed && guard < 8 {
1762        changed = false;
1763        guard += 1;
1764        ternary_fold_walk(s, &mut changed);
1765        strip_empty(s);
1766    }
1767}
1768
1769fn ternary_fold_walk(s: &mut Stmt, changed: &mut bool) {
1770    if let Stmt::Block(v) = s {
1771        for x in v.iter_mut() {
1772            ternary_fold_walk(x, changed);
1773        }
1774        return;
1775    }
1776    let Stmt::If {
1777        cond,
1778        then_stmt,
1779        else_stmt,
1780    } = s
1781    else {
1782        return;
1783    };
1784    ternary_fold_walk(then_stmt, changed);
1785    if let Some(e) = else_stmt {
1786        ternary_fold_walk(e, changed);
1787    }
1788    let Some(else_b) = else_stmt else { return };
1789    let target_of = |st: &Stmt| -> Option<u32> {
1790        let Stmt::ExprStmt(Expr::Assign { target, .. }) = st else {
1791            return None;
1792        };
1793        if let Expr::Local { var, .. } = &**target {
1794            Some(*var)
1795        } else {
1796            None
1797        }
1798    };
1799    let (Stmt::Block(tv), Stmt::Block(ev)) = (then_stmt.as_mut(), else_b.as_mut()) else {
1800        return;
1801    };
1802    if tv.len() != 1 || ev.len() != 1 {
1803        return;
1804    }
1805    let (Some(vt_), Some(ve)) = (target_of(&tv[0]), target_of(&ev[0])) else {
1806        return;
1807    };
1808    if vt_ != ve {
1809        return;
1810    }
1811    // The condition and both values must not re-assign the target between
1812    // the fold — they are single statements, so a mention is a re-read at
1813    // worst; re-assignment only happens via assignments inside them.
1814    let mut touched = HashSet::default();
1815    let (Stmt::ExprStmt(a_then), Stmt::ExprStmt(a_else)) = (&tv[0], &ev[0]) else {
1816        return;
1817    };
1818    if let Expr::Assign { value, .. } = a_then {
1819        collect_vars(value, &mut touched);
1820    }
1821    if let Expr::Assign { value, .. } = a_else {
1822        collect_vars(value, &mut touched);
1823    }
1824    if touched.contains(&vt_) {
1825        return;
1826    }
1827    let value_then = match a_then {
1828        Expr::Assign { value, .. } => value.clone(),
1829        _ => return,
1830    };
1831    let value_else = match a_else {
1832        Expr::Assign { value, .. } => value.clone(),
1833        _ => return,
1834    };
1835    let cond_e = cond.clone();
1836    let folded = Stmt::ExprStmt(Expr::Assign {
1837        target: Box::new(Expr::Local {
1838            var: vt_,
1839            ty: vt_join_ty(&value_then, &value_else),
1840        }),
1841        op: AssignOp::Plain,
1842        value: Box::new(Expr::Cond {
1843            c: Box::new(cond_e),
1844            t: value_then,
1845            f: value_else,
1846        }),
1847    });
1848    *s = Stmt::Block(vec![folded]);
1849    *changed = true;
1850}
1851
1852fn vt_join_ty(a: &Expr, b: &Expr) -> TypeRef {
1853    let ta = a.type_ref();
1854    let tb = b.type_ref();
1855    if ta.erased() == tb.erased() {
1856        return ta;
1857    }
1858    TypeRef::J(JavaType::Object("java/lang/Object".into()))
1859}
1860
1861/// Fold `new StringBuilder(...).append(x)...toString()` chains into
1862/// `StringConcat`. Single-assignment temporaries carry the chain.
1863///
1864/// d8 drops append results (statement-form calls), so a builder whose chain
1865/// has STATEMENT appends attached is left alone: folding its `toString`
1866/// with only the ctor arguments would misstate the contents.
1867pub fn fold_string_builders(s: &mut Stmt, vt: &VarTable) {
1868    // var → assigned value, for vars assigned exactly once.
1869    let n_vars = vt.vars.len().max(1);
1870    let mut counts = vec![0usize; n_vars];
1871    count_assignments(s, &mut counts);
1872    let mut values: Vec<Option<Expr>> = vec![None; n_vars];
1873    record_assignments(s, &counts, &mut values);
1874
1875    // Builders with statement-form appends (their chains are incomplete).
1876    let mut appended_stmts: HashSet<u32> = HashSet::default();
1877    walk_all(s, &mut |st| {
1878        if let Stmt::ExprStmt(Expr::Method {
1879            cls,
1880            name,
1881            owner,
1882            args,
1883            ..
1884        }) = st
1885        {
1886            if name.as_ref() == "append" && is_string_builder(cls) && args.len() == 1 {
1887                if let Some(Expr::Local { var, .. }) = owner.as_deref() {
1888                    appended_stmts.insert(*var);
1889                }
1890            }
1891        }
1892    });
1893
1894    rewrite_exprs(s, &mut |e| {
1895        fold_concat_in_expr(e, &values, &appended_stmts);
1896    });
1897
1898    // Drop now-unused builder temporaries (their only consumers were
1899    // folded); the drop COUNT drives iteration — no statement counting
1900    // walks at all.
1901    for _ in 0..8 {
1902        let mut reads: HashSet<u32> = HashSet::default();
1903        stmt_collect_vars(s, &mut reads, false);
1904        if drop_unused_assigns(s, &reads) == 0 {
1905            break;
1906        }
1907    }
1908}
1909
1910/// Deep statement count (pipeline guard input).
1911pub fn count_stmts_deep(s: &Stmt, out: &mut usize) {
1912    *out += 1;
1913    walk_all(s, &mut |_| {
1914        *out += 1;
1915    });
1916}
1917
1918/// Fused per-variable analysis gathered in one statement-tree walk.
1919struct VarAnalysis {
1920    assigns: Vec<usize>,
1921    reads: Vec<usize>,
1922    /// Value of the (single) assignment, when recorded.
1923    values: Vec<Option<Expr>>,
1924}
1925
1926fn analyze_vars(s: &Stmt, a: &mut VarAnalysis) {
1927    walk_all(s, &mut |st| {
1928        let exprs: Vec<&Expr> = match st {
1929            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
1930                if let Expr::Local { var, .. } = &**target {
1931                    grow_to(&mut a.assigns, *var);
1932                    let idx = *var as usize;
1933                    a.assigns[idx] += 1;
1934                    if a.assigns[idx] == 1 {
1935                        if idx >= a.values.len() {
1936                            a.values.resize(idx + 1, None);
1937                        }
1938                        a.values[idx] = Some((**value).clone());
1939                    } else if idx < a.values.len() {
1940                        a.values[idx] = None;
1941                    }
1942                }
1943                let mut v: Vec<&Expr> = vec![value];
1944                if !matches!(&**target, Expr::Local { .. }) {
1945                    v.push(target);
1946                }
1947                v
1948            }
1949            Stmt::ExprStmt(e @ (Expr::PreIncDec { .. } | Expr::PostIncDec { .. })) => {
1950                // `v++` is a read-modify-WRITE of v: counting it as a
1951                // plain read makes v a single-assign/single-read forward
1952                // candidate, and value-forwarding then fabricates `5++`
1953                // (or drops v's def and leaves ++ on an undefined name).
1954                let inner = match e {
1955                    Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => e,
1956                    _ => unreachable!(),
1957                };
1958                if let Expr::Local { var, .. } = &**inner {
1959                    grow_to(&mut a.assigns, *var);
1960                    let idx = *var as usize;
1961                    a.assigns[idx] += 1;
1962                    if idx < a.values.len() {
1963                        a.values[idx] = None;
1964                    }
1965                    return;
1966                }
1967                vec![e]
1968            }
1969            Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
1970                vec![e]
1971            }
1972            Stmt::Return(Some(e)) => vec![e],
1973            Stmt::LocalDef {
1974                var, init: Some(e), ..
1975            } => {
1976                grow_to(&mut a.assigns, *var);
1977                let idx = *var as usize;
1978                a.assigns[idx] += 1;
1979                if a.assigns[idx] == 1 {
1980                    if idx >= a.values.len() {
1981                        a.values.resize(idx + 1, None);
1982                    }
1983                    a.values[idx] = Some(e.clone());
1984                } else if idx < a.values.len() {
1985                    a.values[idx] = None;
1986                }
1987                vec![e]
1988            }
1989            Stmt::If { cond, .. } => vec![cond],
1990            Stmt::While { cond, .. } => vec![cond],
1991            Stmt::DoWhile { cond, .. } => vec![cond],
1992            _ => Vec::new(),
1993        };
1994        for e in exprs {
1995            visit_exprs(e, &mut |x| {
1996                if let Expr::Local { var, .. } = x {
1997                    grow_to(&mut a.reads, *var);
1998                    a.reads[*var as usize] += 1;
1999                }
2000            });
2001        }
2002    });
2003    // Keep the three views length-aligned.
2004    let n = a.assigns.len().max(a.reads.len()).max(a.values.len());
2005    a.assigns.resize(n, 0);
2006    a.reads.resize(n, 0);
2007    a.values.resize(n, None);
2008}
2009
2010fn grow_to(v: &mut Vec<usize>, var: u32) {
2011    if var as usize >= v.len() {
2012        v.resize(var as usize + 1, 0);
2013    }
2014}
2015
2016fn count_assignments(s: &Stmt, counts: &mut Vec<usize>) {
2017    walk_all(s, &mut |st| match st {
2018        Stmt::ExprStmt(Expr::Assign { target, .. }) => {
2019            if let Expr::Local { var, .. } = &**target {
2020                grow_to(counts, *var);
2021                counts[*var as usize] += 1;
2022            }
2023        }
2024        Stmt::LocalDef {
2025            var, init: Some(_), ..
2026        } => {
2027            grow_to(counts, *var);
2028            counts[*var as usize] += 1;
2029        }
2030        _ => {}
2031    });
2032}
2033
2034fn record_assignments(s: &Stmt, counts: &[usize], out: &mut Vec<Option<Expr>>) {
2035    walk_all(s, &mut |st| match st {
2036        Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
2037            if let Expr::Local { var, .. } = &**target {
2038                if counts.get(*var as usize).copied().unwrap_or(0) == 1 {
2039                    if *var as usize >= out.len() {
2040                        out.resize(*var as usize + 1, None);
2041                    }
2042                    out[*var as usize] = Some((**value).clone());
2043                }
2044            }
2045        }
2046        Stmt::LocalDef {
2047            var, init: Some(e), ..
2048        } if counts.get(*var as usize).copied().unwrap_or(0) == 1 => {
2049            if *var as usize >= out.len() {
2050                out.resize(*var as usize + 1, None);
2051            }
2052            out[*var as usize] = Some(e.clone());
2053        }
2054        _ => {}
2055    });
2056}
2057
2058fn fold_concat_in_expr(e: &mut Expr, values: &[Option<Expr>], appended: &HashSet<u32>) {
2059    deep_rewrite(e, &mut |x| {
2060        if let Expr::Method {
2061            cls,
2062            name,
2063            args,
2064            owner,
2065            ..
2066        } = x
2067        {
2068            if name.as_ref() == "toString" && args.is_empty() && is_string_builder(cls) {
2069                if let Some(o) = owner {
2070                    // Statement-form appends attached to any chain var mean
2071                    // the parts are incomplete — keep the call.
2072                    let mut has_stmt_appends = false;
2073                    visit_exprs(o, &mut |y| {
2074                        if let Expr::Local { var, .. } = y {
2075                            if appended.contains(var) {
2076                                has_stmt_appends = true;
2077                            }
2078                        }
2079                    });
2080                    if has_stmt_appends {
2081                        return;
2082                    }
2083                    let resolved = resolve_local(o, values);
2084                    if let Some(parts) = collect_sb_parts(&resolved, values, 0) {
2085                        // NEVER fold an empty chain: d8 splits builder
2086                        // chains across alias registers (`v2 = v0.append
2087                        // (x); v2.append(y); v0.toString()`) — the traced
2088                        // chain sees a bare `new StringBuilder` and would
2089                        // fold toString() to "" (a wrong VALUE, silently:
2090                        // lab package Obf.label). With zero parts the
2091                        // call stays — always correct, usually folded
2092                        // elsewhere once the parts are visible.
2093                        if !parts.is_empty() {
2094                            *x = Expr::StringConcat(parts);
2095                        }
2096                    }
2097                }
2098            }
2099        }
2100    });
2101}
2102
2103fn is_string_builder(cls: &str) -> bool {
2104    cls == "java/lang/StringBuilder" || cls == "java/lang/StringBuffer"
2105}
2106
2107fn resolve_local(e: &Expr, values: &[Option<Expr>]) -> Expr {
2108    match e {
2109        Expr::Local { var, ty } => values
2110            .get(*var as usize)
2111            .and_then(|o| o.clone())
2112            .unwrap_or_else(|| Expr::Local {
2113                var: *var,
2114                ty: ty.clone(),
2115            }),
2116        other => other.clone(),
2117    }
2118}
2119
2120/// Collect concat parts from a StringBuilder chain; depth caps cycles.
2121fn collect_sb_parts(e: &Expr, values: &[Option<Expr>], depth: u32) -> Option<Vec<ConcatPart>> {
2122    if depth > 16 {
2123        return None;
2124    }
2125    match e {
2126        Expr::Method {
2127            cls,
2128            name,
2129            args,
2130            owner,
2131            ..
2132        } if name.as_ref() == "append" && is_string_builder(cls) && args.len() == 1 => {
2133            let mut parts =
2134                collect_sb_parts(&resolve_local(owner.as_deref()?, values), values, depth + 1)?;
2135            parts.push(ConcatPart::Str(args[0].clone()));
2136            Some(parts)
2137        }
2138        Expr::New {
2139            cls,
2140            args,
2141            raw: false,
2142            ..
2143        } if is_string_builder(cls) => {
2144            let mut parts = Vec::new();
2145            for a in args {
2146                if let Expr::Const(ConstVal::Str(sv)) = a {
2147                    parts.push(ConcatPart::Const(sv.to_string()));
2148                } else {
2149                    parts.push(ConcatPart::Str(a.clone()));
2150                }
2151            }
2152            Some(parts)
2153        }
2154        _ => None,
2155    }
2156}
2157
2158fn drop_unused_assigns(s: &mut Stmt, reads: &HashSet<u32>) -> usize {
2159    let mut dropped = 0usize;
2160    walk_mut_deep(s, &mut |st| {
2161        if let Stmt::Block(v) = st {
2162            let before = v.len();
2163            v.retain(|x| match x {
2164                // Drop only when unread AND a builder chain (folded
2165                // consumer; `new StringBuilder` chains cannot NPE).
2166                Stmt::ExprStmt(Expr::Assign { target, value, .. }) => match &**target {
2167                    Expr::Local { var, .. } => reads.contains(var) || !is_sbish(value),
2168                    _ => true,
2169                },
2170                Stmt::LocalDef { var, init, .. } => {
2171                    reads.contains(var) || !init.as_ref().map(is_sbish).unwrap_or(false)
2172                }
2173                _ => true,
2174            });
2175            dropped += before - v.len();
2176        }
2177    });
2178    dropped
2179}
2180
2181fn is_sbish(e: &Expr) -> bool {
2182    match e {
2183        Expr::New {
2184            cls, raw: false, ..
2185        } => is_string_builder(cls),
2186        Expr::Method { name, cls, .. } => name.as_ref() == "append" && is_string_builder(cls),
2187        _ => false,
2188    }
2189}
2190
2191fn walk_mut_deep<F: FnMut(&mut Stmt)>(s: &mut Stmt, f: &mut F) {
2192    f(s);
2193    walk_mut(s, &mut |x| walk_mut_deep(x, f));
2194}
2195
2196/// Drop orphaned raw `new-instance` statements. R8 sometimes separates
2197/// `new-instance vR` from its `<init>` invoke by a register copy
2198/// (`move-object vS, vR; invoke vS.<init>(..)` — dex-legal: the init
2199/// initializes the object both registers name). The fold pairs the init
2200/// with the COPY, and the structurer's branch duplication re-materializes
2201/// the original raw marker as a bare `new C();` statement on paths whose
2202/// uses were rewired to the folded side (weixin sns/storage/c0: four
2203/// `new s1();` beside the real `s1 s1x = new s1(..)` — s1 has no no-arg
2204/// ctor, "无法将类 C的构造器 C应用到给定类型" ×195). A raw New in
2205/// statement position binds no variable and initializes nothing — pure
2206/// verifier-artifact dead code.
2207pub fn drop_dead_raw_news(body: &mut Stmt) {
2208    fn is_orphan(s: &Stmt) -> bool {
2209        matches!(s, Stmt::ExprStmt(Expr::New { raw: true, .. }))
2210    }
2211    walk_mut_deep(body, &mut |st| match st {
2212        Stmt::Block(v) => v.retain(|x| !is_orphan(x)),
2213        Stmt::Switch { cases, .. } => {
2214            for c in cases.iter_mut() {
2215                c.body.retain(|x| !is_orphan(x));
2216            }
2217        }
2218        Stmt::For { init, .. } => init.retain(|x| !is_orphan(x)),
2219        Stmt::TryWithResources { resources, .. } => resources.retain(|x| !is_orphan(x)),
2220        _ => {}
2221    });
2222}
2223
2224/// Recover `synchronized` from the d8 monitor pattern:
2225/// `monitorenter(e); try { body } catch (Throwable) { monitorexit(e); throw t; }`
2226/// (optionally followed by `monitorexit(e)`).
2227pub fn fold_synchronized(s: &mut Stmt) {
2228    fold_sync_walk(s);
2229}
2230
2231fn fold_sync_walk(s: &mut Stmt) {
2232    walk_mut_deep(s, &mut |st| {
2233        let Stmt::Block(v) = st else { return };
2234        let mut i = 0;
2235        while i < v.len() {
2236            if i + 1 < v.len() {
2237                if let Some(sync) = try_sync_at(v, i) {
2238                    let _ = v.drain(i..i + 2);
2239                    v.insert(i, sync);
2240                    continue;
2241                }
2242            }
2243            i += 1;
2244        }
2245    });
2246}
2247
2248fn try_sync_at(v: &[Stmt], i: usize) -> Option<Stmt> {
2249    let Stmt::MonitorEnter(lock) = &v[i] else {
2250        return None;
2251    };
2252    let Stmt::Try {
2253        body,
2254        catches,
2255        finally,
2256    } = &v[i + 1]
2257    else {
2258        return None;
2259    };
2260    if finally.is_some() || catches.len() != 1 {
2261        return None;
2262    }
2263    let c = &catches[0];
2264    if !c.exc.is_empty() {
2265        return None;
2266    }
2267    // The catch-all body: monitorexit(lock) then rethrow.
2268    let Stmt::Block(cv) = c.body.as_ref() else {
2269        return None;
2270    };
2271    let exit_matches = cv.len() >= 2
2272        && matches!(&cv[0], Stmt::MonitorExit(e) if expr_local_var(e) == expr_local_var(lock));
2273    let throws = cv.len() >= 2 && matches!(&cv[1], Stmt::Throw(_));
2274    if !(exit_matches && throws) {
2275        return None;
2276    }
2277    Some(Stmt::Synchronized {
2278        lock: lock.clone(),
2279        body: body.clone(),
2280    })
2281}
2282
2283fn expr_local_var(e: &Expr) -> Option<u32> {
2284    if let Expr::Local { var, .. } = e {
2285        Some(*var)
2286    } else {
2287        None
2288    }
2289}
2290
2291/// Copy-forward single-use temporaries: `v = e; ... v ...` inlines `e` at
2292/// the (single) use and drops the assignment when safe. Pure values inline
2293/// anywhere; impure values (calls, array reads) only when the use is the
2294/// statement immediately after the definition.
2295pub fn forward_single_use(s: &mut Stmt, _vt: &VarTable) {
2296    // Fused single traversal: assignment counts, read counts and the
2297    // single-assignment values all come out of ONE walk (three separate
2298    // full-tree walks dominated the pass cost on large methods).
2299    let mut analysis = VarAnalysis {
2300        assigns: Vec::new(),
2301        reads: Vec::new(),
2302        values: Vec::new(),
2303    };
2304    analyze_vars(s, &mut analysis);
2305    let mut assigns = analysis.assigns;
2306    let mut reads = analysis.reads;
2307    // Var ids beyond the table (synthetic test shapes) are covered by the
2308    // counters' on-demand growth.
2309    let n_vars = assigns.len().max(_vt.vars.len()).max(1);
2310    assigns.resize(n_vars, 0);
2311    reads.resize(n_vars, 0);
2312    let mut values = analysis.values;
2313    values.resize(n_vars, None);
2314
2315    // Candidates: assigned exactly once, read exactly once. Additionally
2316    // the inlined VALUE must not reference a multi-assigned var (phi vars):
2317    // moving the read across the phi's reassignment would be a stale
2318    // capture (register rotations snapshot through temps for this reason).
2319    let mut cand = vec![false; n_vars];
2320    let mut edges: Vec<Vec<u32>> = vec![Vec::new(); n_vars];
2321    for (v, cand_v) in cand.iter_mut().enumerate() {
2322        if assigns.get(v).copied().unwrap_or(0) != 1 || reads.get(v).copied().unwrap_or(0) != 1 {
2323            continue;
2324        }
2325        if let Some(val) = values.get(v).and_then(|o| o.as_ref()) {
2326            let mut refs = HashSet::default();
2327            collect_vars(val, &mut refs);
2328            if refs.iter().any(|r| assigns[*r as usize] > 1) {
2329                continue;
2330            }
2331            // Growth-graph edge set, built in the same walk: inlining v
2332            // inserts a clone of its value, and every Local inside the
2333            // clone is replaced in turn.
2334            edges[v] = refs.into_iter().collect();
2335        }
2336        *cand_v = true;
2337    }
2338
2339    // Cycle rejection on the growth graph. A def-reference cycle
2340    // (v = f(w), w = g(v) — loop-carried register rotation reaching the
2341    // pass as two mutually-referencing single-assign/single-read locals)
2342    // re-introduces the other cycle Local at every replacement level, so
2343    // deep_rewrite grows the tree one layer per level and never
2344    // terminates: weixin's com/tencent/mm/plugin/appbrand/widget/input/b4
2345    // exhausted a 64MB worker stack at ~300k recursion frames. Edges into
2346    // non-candidates are harmless (they are never replaced, so growth
2347    // dies there — and they carry no outgoing edges).
2348    //
2349    // Three-color DFS, iterative: a legit 60k-long single-use chain must
2350    // not trade one stack overflow for another. A GRAY child closes a
2351    // cycle; `bad[v]` (v on a cycle, or v's inlined closure grows into
2352    // one) then taints the whole current path — everything on it reaches
2353    // the cycle. Black verdicts memoize: a node whose subtree was proven
2354    // clean cannot grow a cycle later.
2355    let mut color = vec![0u8; n_vars]; // 0 white, 1 gray, 2 black
2356    let mut bad = vec![false; n_vars];
2357    for root in 0..n_vars {
2358        if color[root] != 0 || edges[root].is_empty() {
2359            continue;
2360        }
2361        color[root] = 1;
2362        let mut stack: Vec<(usize, usize)> = vec![(root, 0)];
2363        while let Some(top) = stack.last_mut() {
2364            let v = top.0;
2365            let i = top.1;
2366            top.1 += 1;
2367            if i < edges[v].len() {
2368                let r = edges[v][i] as usize;
2369                if color[r] == 1 {
2370                    for &(pn, _) in &stack {
2371                        bad[pn] = true;
2372                    }
2373                } else if color[r] == 0 {
2374                    color[r] = 1;
2375                    stack.push((r, 0));
2376                } else if bad[r] {
2377                    for &(pn, _) in &stack {
2378                        bad[pn] = true;
2379                    }
2380                }
2381            } else {
2382                color[v] = 2;
2383                stack.pop();
2384            }
2385        }
2386    }
2387    let single: Vec<bool> = (0..n_vars).map(|v| cand[v] && !bad[v]).collect();
2388    if !single.iter().any(|&b| b) {
2389        return;
2390    }
2391    let pure: Vec<bool> = (0..n_vars)
2392        .map(|v| {
2393            single[v]
2394                && values
2395                    .get(v)
2396                    .and_then(|o| o.as_ref())
2397                    .map(|e| !has_side_effects(e))
2398                    .unwrap_or(false)
2399        })
2400        .collect();
2401    let impure: Vec<bool> = (0..n_vars).map(|v| single[v] && !pure[v]).collect();
2402
2403    // 1. Pure values: inline anywhere.
2404    if pure.iter().any(|&b| b) {
2405        let vals: Vec<Option<Expr>> = (0..n_vars)
2406            .map(|v| {
2407                if pure[v] {
2408                    values.get(v).cloned().flatten()
2409                } else {
2410                    None
2411                }
2412            })
2413            .collect();
2414        rewrite_exprs(s, &mut |e| {
2415            deep_rewrite_reads(e, &mut |x| {
2416                if let Expr::Local { var, .. } = x {
2417                    if let Some(Some(v)) = vals.get(*var as usize) {
2418                        *x = v.clone();
2419                    }
2420                }
2421            });
2422        });
2423        drop_defs(s, &pure);
2424    }
2425
2426    // 2. Impure values: inline only into the immediately following
2427    //    statement.
2428    if impure.iter().any(|&b| b) {
2429        forward_adjacent_impure(s, &values, &impure);
2430    }
2431}
2432
2433/// Count READS of variables (assignment targets excluded).
2434fn count_reads(s: &Stmt, out: &mut Vec<usize>) {
2435    walk_all(s, &mut |st| {
2436        let exprs: Vec<&Expr> = match st {
2437            // Assign must match BEFORE the generic ExprStmt arm (the
2438            // assignment target is a write, not a read).
2439            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
2440                let mut v: Vec<&Expr> = vec![value];
2441                if !matches!(&**target, Expr::Local { .. }) {
2442                    v.push(target);
2443                }
2444                v
2445            }
2446            // `v++` writes v — not a read (a compound `a[i]++` still
2447            // reads its owner/index subtree).
2448            Stmt::ExprStmt(e @ (Expr::PreIncDec { .. } | Expr::PostIncDec { .. })) => {
2449                let inner = match e {
2450                    Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => e,
2451                    _ => unreachable!(),
2452                };
2453                if matches!(**inner, Expr::Local { .. }) {
2454                    Vec::new()
2455                } else {
2456                    vec![e]
2457                }
2458            }
2459            Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
2460                vec![e]
2461            }
2462            Stmt::Return(Some(e)) => vec![e],
2463            Stmt::LocalDef { init: Some(e), .. } => vec![e],
2464            Stmt::If { cond, .. } => vec![cond],
2465            Stmt::While { cond, .. } => vec![cond],
2466            Stmt::DoWhile { cond, .. } => vec![cond],
2467            // Read sites that are NOT statement children (walk_all recurses
2468            // into bodies but these payloads are expressions on the node
2469            // itself). Omitting them under-counts reads, which would let
2470            // drop_dead_locals over-prune a var whose only use is a switch
2471            // selector / loop condition / lock / iterable.
2472            Stmt::Switch { selector, .. } => vec![selector],
2473            Stmt::ForEach { iterable, .. } => vec![iterable],
2474            Stmt::Synchronized { lock, .. } => vec![lock],
2475            Stmt::For { cond, update, .. } => {
2476                let mut v: Vec<&Expr> = Vec::with_capacity(update.len() + 1);
2477                if let Some(c) = cond {
2478                    v.push(c);
2479                }
2480                v.extend(update.iter());
2481                v
2482            }
2483            Stmt::Assert { cond, msg } => {
2484                let mut v: Vec<&Expr> = vec![cond];
2485                if let Some(m) = msg {
2486                    v.push(m);
2487                }
2488                v
2489            }
2490            _ => Vec::new(),
2491        };
2492        for e in exprs {
2493            visit_exprs(e, &mut |x| {
2494                if let Expr::Local { var, .. } = x {
2495                    grow_to(out, *var);
2496                    out[*var as usize] += 1;
2497                }
2498            });
2499        }
2500    });
2501}
2502
2503fn drop_defs(s: &mut Stmt, vars: &[bool]) {
2504    walk_mut_deep(s, &mut |st| {
2505        if let Stmt::Block(v) = st {
2506            v.retain(|x| match x {
2507                Stmt::ExprStmt(Expr::Assign { target, .. }) => match &**target {
2508                    Expr::Local { var, .. } => !vars.get(*var as usize).copied().unwrap_or(false),
2509                    _ => true,
2510                },
2511                Stmt::LocalDef { var, .. } => !vars.get(*var as usize).copied().unwrap_or(false),
2512                _ => true,
2513            });
2514        }
2515    });
2516}
2517
2518/// For each statement list: `v = IMPURE; NEXT(v)` folds NEXT's reference.
2519fn forward_adjacent_impure(s: &mut Stmt, values: &[Option<Expr>], impure: &[bool]) {
2520    if let Stmt::Block(v) = s {
2521        let mut i = 0;
2522        while i + 1 < v.len() {
2523            let def_var = match &v[i] {
2524                Stmt::ExprStmt(Expr::Assign { target, .. }) => match &**target {
2525                    Expr::Local { var, .. } => Some(*var),
2526                    _ => None,
2527                },
2528                Stmt::LocalDef { var, .. } => Some(*var),
2529                _ => None,
2530            };
2531            if let Some(var) = def_var {
2532                if impure.get(var as usize).copied().unwrap_or(false) {
2533                    // Does the NEXT statement reference var?
2534                    let mut reads_here = vec![0usize; values.len().max(1)];
2535                    count_reads(&v[i + 1], &mut reads_here);
2536                    if reads_here.get(var as usize).copied().unwrap_or(0) == 1 {
2537                        // The def statement's CURRENT value (earlier inlines
2538                        // in this walk may have rewritten it — the pre-built
2539                        // values map would be stale).
2540                        let val = match &v[i] {
2541                            Stmt::ExprStmt(Expr::Assign { value, .. }) => (**value).clone(),
2542                            Stmt::LocalDef { init: Some(e), .. } => (*e).clone(),
2543                            _ => {
2544                                i += 1;
2545                                continue;
2546                            }
2547                        };
2548                        rewrite_exprs(&mut v[i + 1], &mut |e| {
2549                            deep_rewrite_reads(e, &mut |x| {
2550                                if let Expr::Local { var: v2, .. } = x {
2551                                    if *v2 == var {
2552                                        *x = val.clone();
2553                                    }
2554                                }
2555                            });
2556                        });
2557                        v.remove(i);
2558                        // The PREVIOUS definition may now be adjacent to its
2559                        // (shifted) use — re-examine it.
2560                        i = i.saturating_sub(1);
2561                        continue;
2562                    }
2563                }
2564            }
2565            i += 1;
2566        }
2567    }
2568    walk_mut(s, &mut |x| forward_adjacent_impure(x, values, impure));
2569}
2570
2571// ---------------------------------------------------------------------------
2572// Type inference, booleanization, null comparisons, declarations
2573// ---------------------------------------------------------------------------
2574
2575/// Evidence-based type inference for synthetic (non-parameter) vars.
2576pub fn infer_types(vt: &mut VarTable, body: &mut Stmt, ret: &JavaType, env: &MethodEnv) {
2577    let n = vt.vars.len();
2578    // (type, strong) — DIRECTIONAL evidence. Strong facts (an assigned
2579    // value's type, the declared return/throw/monitor type) DEFINE the
2580    // variable; weak facts (call argument expectations, receiver
2581    // contexts, numeric literals) only constrain a use. jadx's bound
2582    // system carries the same direction; a flat pool let a weak
2583    // expectation (`v26 = p1` with boolean p1 beside `v26 = 0` sugar)
2584    // outvote the definition.
2585    let mut evidence: Vec<Vec<(JavaType, bool)>> = vec![Vec::new(); n];
2586    let ev = |evidence: &mut Vec<Vec<(JavaType, bool)>>, var: u32, t: JavaType, strong: bool| {
2587        if (var as usize) < evidence.len() {
2588            evidence[var as usize].push((t, strong));
2589        }
2590    };
2591
2592    walk_all(body, &mut |st| match st {
2593        Stmt::LocalDef { var, init, .. } => {
2594            if let Some(e) = init {
2595                expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
2596                // The initializer DEFINES the declared type: `long v = l(...)`
2597                // must not stay `int v` (silent 64-bit truncation in the
2598                // eyes of a reader; javac rejects it as lossy). Null
2599                // carries no type (it would DOWNGRADE String to Object).
2600                // Literals are weak (0/1 is boolean sugar half the time);
2601                // typed producers and parameter sources are strong;
2602                // local-to-local copies are weak (register reuse).
2603                let strong = match e {
2604                    Expr::Const(_) => false,
2605                    Expr::Local { var: src, .. } => {
2606                        let src_ty = vt.vars.get(*src as usize).map(|v| v.ty.erased());
2607                        let tgt_ty = vt.vars.get(*var as usize).map(|v| v.ty.erased());
2608                        match (src_ty, tgt_ty) {
2609                            (Some(s), Some(g)) => {
2610                                (s.is_numeric() && g.is_numeric())
2611                                    || (s.is_reference() && g.is_reference())
2612                            }
2613                            _ => false,
2614                        }
2615                    }
2616                    _ => true,
2617                };
2618                ev(&mut evidence, *var, e.type_ref().erased(), strong);
2619            }
2620        }
2621        Stmt::ExprStmt(e) => {
2622            // Assignment targets: a typed PRODUCER (method/new/field) or a
2623            // PARAMETER source is a STRONG definition (`v26 = p1` with
2624            // boolean p1). A local-to-local copy is weak — register reuse
2625            // and merge materialization emit exactly that shape with
2626            // mismatched types (`sb7 = compareTo10` in a Kotlin when).
2627            // Literals stay weak.
2628            if let Expr::Assign { target, value, .. } = e {
2629                // A field WRITE through a local (`v.f = x`, dex iput)
2630                // proves v is a reference of the field's declaring
2631                // class AT THAT POINT — an iput on a boolean register
2632                // cannot exist in valid dex. Strong: it must outrank a
2633                // strong boolean copy chain into the same reused slot
2634                // (weixin coroutine state machines).
2635                if let Expr::Field { owner: Some(o), cls, is_static: false, .. } = &**target {
2636                    if cls.is_empty() {
2637                        // array-length pseudo-field: no declaring class
2638                    } else if let Expr::Local { var, .. } = &**o {
2639                        ev(
2640                            &mut evidence,
2641                            *var,
2642                            JavaType::Object(cls.clone()),
2643                            true,
2644                        );
2645                    }
2646                }
2647                if let Expr::Local { var, .. } = &**target {
2648                    let strong = match &**value {
2649                        Expr::Const(_) => false,
2650                        Expr::Local { var: src, .. } => {
2651                            // A local copy is strong when the source and
2652                            // the target's register type are the same
2653                            // FAMILY (numeric↔numeric, reference↔
2654                            // reference) — a cross-family copy (int into
2655                            // a StringBuilder local) is merge-material
2656                            // residue and stays weak.
2657                            let src_ty = vt.vars.get(*src as usize).map(|v| v.ty.erased());
2658                            let tgt_ty = vt.vars.get(*var as usize).map(|v| v.ty.erased());
2659                            match (src_ty, tgt_ty) {
2660                                (Some(s), Some(g)) => {
2661                                    (s.is_numeric() && g.is_numeric())
2662                                        || (s.is_reference() && g.is_reference())
2663                                }
2664                                _ => false,
2665                            }
2666                        }
2667                        _ => true,
2668                    };
2669                    ev(&mut evidence, *var, value.type_ref().erased(), strong);
2670                }
2671            }
2672            expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
2673        }
2674        Stmt::Throw(e) => {
2675            expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
2676            if let Expr::Local { var, .. } = e {
2677                ev(
2678                    &mut evidence,
2679                    *var,
2680                    JavaType::Object("java/lang/Throwable".into()),
2681                    true,
2682                );
2683            }
2684        }
2685        Stmt::Return(Some(e)) => {
2686            expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
2687            if let Expr::Local { var, .. } = e {
2688                ev(&mut evidence, *var, ret.clone(), true);
2689            }
2690        }
2691        Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
2692            if let Expr::Local { var, .. } = e {
2693                ev(
2694                    &mut evidence,
2695                    *var,
2696                    JavaType::Object("java/lang/Object".into()),
2697                    true,
2698                );
2699            }
2700        }
2701        Stmt::ForEach {
2702            var,
2703            iterable,
2704            is_array,
2705            ..
2706        } => {
2707            if let Expr::Local { var: v0, .. } = iterable {
2708                if *is_array {
2709                    ev(&mut evidence, *v0, JavaType::Array(Box::new(JavaType::Int)), true);
2710                } else {
2711                    ev(
2712                        &mut evidence,
2713                        *v0,
2714                        JavaType::Object("java/lang/Object".into()),
2715                        true,
2716                    );
2717                }
2718            }
2719            let _ = var;
2720        }
2721        _ => {}
2722    });
2723    let _ = env;
2724
2725    // Return-position locals take the declared return type (also inside
2726    // nested returns — handled by the walk above).
2727
2728        // Vars dereferenced as field/method receivers or arrays: their
2729    // resolved type must be a dereferenceable class — a boxed Boolean
2730    // in the strong set (Kotlin Result registers hold `Boolean | impl`
2731    // across coroutine case arms) must lose to the real class even
2732    // when the Boolean evidence comes first (weixin ry0/h2's
2733    // `Boolean bool19` receiving `bool19.d = x`).
2734    let mut derefed: Vec<bool> = vec![false; vt.vars.len()];
2735    mark_derefs(body, &mut derefed);
2736    // Array-indexed vars: their resolved type must be an ARRAY when
2737    // the evidence has one (`num4[v68]` with Integer-typed num4 —
2738    // 需要数组但找到Integer family); member derefs keep the class
2739    // preference below.
2740    let mut arr_derefed: Vec<bool> = vec![false; vt.vars.len()];
2741    visit_all_exprs(body, &mut |x| {
2742        if let Expr::ArrayIndex { array, .. } = x {
2743            if let Expr::Local { var, .. } = &**array {
2744                if (*var as usize) < arr_derefed.len() {
2745                    arr_derefed[*var as usize] = true;
2746                }
2747            }
2748        }
2749        // `v != null` is STRONG reference evidence: the lifter only
2750        // renders the null form when the dex value view held a
2751        // reference register at that pc — an int-typed generation
2752        // compared against null is phi-confluence residue (yq0/g1's
2753        // `v95_g12 != null`, 546-line 二元运算符 family). Strong Object
2754        // makes the resolver retype and split_generations separate the
2755        // int writes into their own generation.
2756        if let Expr::Bin { op, l, r, .. } = x {
2757            if matches!(op, BinOp::Eq | BinOp::Ne)
2758                && (matches!(&**r, Expr::Const(ConstVal::Null))
2759                    || matches!(&**l, Expr::Const(ConstVal::Null)))
2760            {
2761                for side in [l, r] {
2762                    if let Expr::Local { var, .. } = &**side {
2763                        ev(
2764                            &mut evidence,
2765                            *var,
2766                            JavaType::Object("java/lang/Object".into()),
2767                            true,
2768                        );
2769                    }
2770                }
2771            }
2772        }
2773    });
2774    const FINAL_JDK: &[&str] = &[
2775        "java/lang/Boolean",
2776        "java/lang/String",
2777        "java/lang/Integer",
2778        "java/lang/Long",
2779        "java/lang/Short",
2780        "java/lang/Byte",
2781        "java/lang/Character",
2782        "java/lang/Float",
2783        "java/lang/Double",
2784    ];
2785    for (i, evs) in evidence.iter().enumerate() {
2786        let info = &vt.vars[i];
2787        if info.is_param {
2788            continue;
2789        }
2790        let strong: Vec<JavaType> = evs
2791            .iter()
2792            .filter(|(_, s)| *s)
2793            .map(|(t, _)| t.clone())
2794            .collect();
2795        let all: Vec<JavaType> = evs.iter().map(|(t, _)| t.clone()).collect();
2796        if info.ty.erased().is_reference() {
2797            // Materialization residue: no strong definition, and the weak
2798            // pool names ≥2 DIFFERENT classes (a Kotlin `when` lowered
2799            // every branch's value into one register slot) — neither
2800            // class can win, every assignment needs boxing headroom:
2801            // widen to Object (uses get receiver casts).
2802            if strong.is_empty() {
2803                let refs: std::collections::HashSet<&str> = all
2804                    .iter()
2805                    .filter_map(|t| match t {
2806                        JavaType::Object(n) if n.as_ref() != "java/lang/Object" => {
2807                            Some(n.as_ref())
2808                        }
2809                        _ => None,
2810                    })
2811                    .collect();
2812                let mixed = all.iter().any(|t| t.is_numeric());
2813                if refs.len() >= 2 || (mixed && !refs.is_empty()) || (mixed && all.iter().any(|t| matches!(t, JavaType::Array(_)))) {
2814                    vt.vars[i].ty =
2815                        TypeRef::J(JavaType::Object("java/lang/Object".into()));
2816                    continue;
2817                }
2818            }
2819                        // Array-dereferenced vars prefer array evidence (an
2820            // Integer producer cannot be indexed).
2821            if arr_derefed[i] {
2822                let arr = strong
2823                    .iter()
2824                    .chain(all.iter())
2825                    .find(|t| matches!(t, JavaType::Array(_)))
2826                    .cloned();
2827                if let Some(t) = arr {
2828                    vt.vars[i].ty = TypeRef::J(t);
2829                    continue;
2830                }
2831            }
2832            // Strong definitions outrank weak expectations. A
2833            // dereferenced var prefers a real class over final JDK
2834            // boxes (Boolean can never carry `.d = x`).
2835            let deref_pick = || {
2836                if !derefed[i] {
2837                    return None;
2838                }
2839                strong
2840                    .iter()
2841                    .find(|t| {
2842                        matches!(t, JavaType::Object(n)
2843                            if !n.is_empty()
2844                                && n.as_ref() != "java/lang/Object"
2845                                && !FINAL_JDK.contains(&n.as_ref()))
2846                    })
2847                    .cloned()
2848            };
2849            if let Some(t) = deref_pick()
2850                .or_else(|| pick_object(&strong))
2851                .or_else(|| pick_object(&all))
2852            {
2853                vt.vars[i].ty = TypeRef::J(t);
2854            }
2855        } else if let Some(t) = pick_object(&strong) {
2856            // A strong reference definition (typed producer or field
2857            // write) outranks a strong boolean copy: Boolean assigns
2858            // INTO an Object slot legally, never the reverse.
2859            vt.vars[i].ty = TypeRef::J(t);
2860        } else if strong.contains(&JavaType::Boolean) {
2861            // A boolean-typed SOURCE assigned into the variable is a
2862            // definition (`v26 = p1` with boolean p1); the 0/1 literals
2863            // that share the pool are boolean sugar as often as not.
2864            vt.vars[i].ty = TypeRef::J(JavaType::Boolean);
2865        } else if !all.is_empty() && all.iter().all(|t| t.is_numeric()) {
2866            if let Some(t) = pick_numeric(&all) {
2867                vt.vars[i].ty = TypeRef::J(t);
2868            }
2869        } else {
2870            // Mixed-family materialization on a numeric-declared slot
2871            // (int slot receiving StringBuilders — the register was the
2872            // `when` accumulator): Object, same as the reference side.
2873            let refs = all.iter().filter(|t| t.is_reference()).count();
2874            let nums = all.iter().filter(|t| t.is_numeric()).count();
2875            if refs > 0 && nums > 0 {
2876                vt.vars[i].ty =
2877                    TypeRef::J(JavaType::Object("java/lang/Object".into()));
2878            } else if let Some(t) = pick_object(&strong).or_else(|| pick_object(&all)) {
2879                vt.vars[i].ty = TypeRef::J(t);
2880            }
2881        }
2882    }
2883
2884    // Rewrite embedded Local types. Borrowed view + write-only-on-change:
2885    // the table clone was n_vars JavaType clones per method, and the
2886    // rewrite cloned a type into EVERY Local node — most already carry
2887    // the right one (JavaType::Object clones allocate).
2888    let types: Vec<&TypeRef> = vt.vars.iter().map(|v| &v.ty).collect();
2889    rewrite_exprs(body, &mut |e| {
2890        deep_rewrite(e, &mut |x| {
2891            if let Expr::Local { var, ty } = x {
2892                if let Some(want) = types.get(*var as usize) {
2893                    if ty != *want {
2894                        *ty = (*want).clone();
2895                    }
2896                }
2897            }
2898        });
2899    });
2900
2901    // Numeric literals take their variable's declared width (`double v = 0L`
2902    // prints as `0.0`; `int x = 5L` as `5`).
2903    coerce_num_consts(body, &types);
2904}
2905
2906fn coerce_num_consts(body: &mut Stmt, types: &[&TypeRef]) {
2907    walk_mut_deep(body, &mut |st| {
2908        let (var, val): (u32, &mut Expr) = match st {
2909            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => match &mut **target {
2910                Expr::Local { var, .. } => (*var, value),
2911                _ => return,
2912            },
2913            Stmt::LocalDef {
2914                var, init: Some(e), ..
2915            } => (*var, e),
2916            _ => return,
2917        };
2918        let want = match types.get(var as usize) {
2919            Some(TypeRef::J(t)) => t.clone(),
2920            _ => return,
2921        };
2922        let rewritten = match &*val {
2923            Expr::Const(ConstVal::Long(l)) => match &want {
2924                JavaType::Double => Some(Expr::Const(ConstVal::Double(*l as f64))),
2925                JavaType::Float => Some(Expr::Const(ConstVal::Float(*l as f32))),
2926                _ => None,
2927            },
2928            Expr::Const(ConstVal::Int(i)) => match &want {
2929                JavaType::Double => Some(Expr::Const(ConstVal::Double(*i as f64))),
2930                JavaType::Float => Some(Expr::Const(ConstVal::Float(*i as f32))),
2931                _ => None,
2932            },
2933            _ => None,
2934        };
2935        if let Some(e) = rewritten {
2936            *val = e;
2937        }
2938    });
2939}
2940
2941fn pick_object(evs: &[JavaType]) -> Option<JavaType> {
2942    evs.iter()
2943        .find(|t| {
2944            t.is_reference()
2945                && !matches!(t, JavaType::Object(n) if n.is_empty() || n.as_ref() == "java/lang/Object")
2946        })
2947        .cloned()
2948}
2949
2950fn pick_numeric(evs: &[JavaType]) -> Option<JavaType> {
2951    if evs.is_empty() {
2952        return None;
2953    }
2954    let mut t = evs[0].clone();
2955    for x in &evs[1..] {
2956        t = join_numeric(&t, x);
2957    }
2958    Some(t)
2959}
2960
2961fn join_numeric(a: &JavaType, b: &JavaType) -> JavaType {
2962    if a == b {
2963        return a.clone();
2964    }
2965    match (a, b) {
2966        (JavaType::Double, _) | (_, JavaType::Double) => JavaType::Double,
2967        (JavaType::Float, _) | (_, JavaType::Float) => JavaType::Float,
2968        (JavaType::Long, _) | (_, JavaType::Long) => JavaType::Long,
2969        (JavaType::Int, x) if x.is_integral() => JavaType::Int,
2970        (x, JavaType::Int) if x.is_integral() => JavaType::Int,
2971        (x, y) if x.is_integral() && y.is_integral() => JavaType::Int,
2972        _ => JavaType::Int,
2973    }
2974}
2975
2976/// Gather per-var type evidence from one expression.
2977fn expr_evidence<F: FnMut(u32, JavaType)>(e: &Expr, f: &mut F) {
2978    visit_exprs(e, &mut |x| match x {
2979        Expr::Method {
2980            owner,
2981            args,
2982            desc,
2983            is_static,
2984            cls,
2985            ..
2986        } => {
2987            if !*is_static {
2988                if let Some(o) = owner {
2989                    if let Expr::Local { var, .. } = &**o {
2990                        f(*var, JavaType::Object(cls.clone()));
2991                    }
2992                }
2993            }
2994            for (i, a) in args.iter().enumerate() {
2995                if let Expr::Local { var, .. } = a {
2996                    if let Some(t) = desc.args.get(i) {
2997                        f(*var, t.clone());
2998                    }
2999                }
3000            }
3001        }
3002        Expr::Field {
3003            owner,
3004            cls,
3005            ty,
3006            is_static,
3007            ..
3008        } => {
3009            if !*is_static && !cls.is_empty() {
3010                if let Some(o) = owner {
3011                    if let Expr::Local { var, .. } = &**o {
3012                        // The field ref's declaring class is dex-exact
3013                        // evidence — the old hardcoded Object let a
3014                        // boolean copy chain outvote it (weixin ry0/h2's
3015                        // `Boolean bool19` receiving `bool19.d = x` —
3016                        // the 339-line 找不到符号 变量 d family).
3017                        // EMPTY cls = the array-length pseudo-field
3018                        // (lift models `a.length` as Field{cls:""}) —
3019                        // Object("") evidence poisoned pick_object into
3020                        // empty-typed declarations and `() x` casts.
3021                        f(*var, JavaType::Object(cls.clone()));
3022                    }
3023                }
3024            }
3025            let _ = ty;
3026        }
3027        // `v != null` / `v == null`: the register holds a reference at
3028        // that point — weak Object evidence keeps int-typed generations
3029        // from winning the pool (yq0/g1's `v95_g12 != null` with int
3030        // v95_g12 — 二元运算符 '!=' 546-line family).
3031        Expr::Bin { op, l, r, .. }
3032            if matches!(op, BinOp::Eq | BinOp::Ne)
3033                && (matches!(&**r, Expr::Const(ConstVal::Null))
3034                    || matches!(&**l, Expr::Const(ConstVal::Null))) =>
3035        {
3036            for side in [l, r] {
3037                if let Expr::Local { var, .. } = &**side {
3038                    f(*var, JavaType::Object("java/lang/Object".into()));
3039                }
3040            }
3041        }
3042        Expr::ArrayIndex { array, .. } => {
3043            if let Expr::Local { var, ty, .. } = &**array {
3044                // Reinforce the local's KNOWN array type: the hardcoded
3045                // int[] fallback contradicted a typed array local (a
3046                // register reused for byte[], int[] then byte[] across
3047                // one clinit labeled the byte[] defs `int[]`).
3048                let t = ty.erased();
3049                if matches!(t, JavaType::Array(_)) {
3050                    f(*var, t);
3051                } else {
3052                    f(*var, JavaType::Array(Box::new(JavaType::Int)));
3053                }
3054            }
3055        }
3056        Expr::Assign { .. } => {
3057            // Covered by the typed strong-evidence walk in infer_types;
3058            // keeping a weak duplicate here let expectations outvote it.
3059        }
3060        Expr::Cast { e: inner, .. } => {
3061            // A cast is an EXPLICIT narrowing at the use site — it must
3062            // NOT feed the cast target as evidence for the variable
3063            // (`Object get2 = list.get(2); j((String) get2);` had get2
3064            // retyped to String, making the assignment incompatible).
3065            // jadx's bounds carry direction: a USE bound narrows nothing.
3066            let _ = inner;
3067        }
3068        Expr::InstanceOf { e: inner, .. } => {
3069            let _ = inner;
3070        }
3071        _ => {}
3072    });
3073}
3074
3075/// Mark locals that OWN a non-static field access and whose inferred type
3076/// is a concrete `$<digits>` class (a d8/R8-desugared lambda or a Kotlin
3077/// suspend-lambda). Emit demotes such a type to its SAM interface in
3078/// DECLARED positions (`Function2 v = new Outer$..$1(..)`) for phi-friendly
3079/// readability — but interfaces carry no instance fields, so the Kotlin
3080/// coroutine `create()` capture writes (`v.L$0 = obj`, also I$/J$) render
3081/// "找不到符号 变量 L$0" (weibo MutableScatterMap$..$iterator$1, ~1k).
3082/// Forcing the concrete declared type keeps the field access legal; the phi
3083/// case is excluded because a disagreed merge stays Object (wide_stack_vars),
3084/// never a specific concrete class. Gate mirrors emit's demotion exactly
3085/// (last `$` segment all digits) so nothing else changes.
3086pub fn mark_field_owner_concrete(vt: &mut VarTable, body: &mut Stmt) {
3087    let mut owners: HashSet<u32> = HashSet::default();
3088    rewrite_exprs(body, &mut |e| {
3089        visit_exprs(e, &mut |x| {
3090            if let Expr::Field {
3091                owner: Some(o),
3092                is_static: false,
3093                ..
3094            } = x
3095            {
3096                if let Expr::Local { var, .. } = &**o {
3097                    owners.insert(*var);
3098                }
3099            }
3100        });
3101    });
3102    for var in owners {
3103        let i = var as usize;
3104        if i >= vt.vars.len() {
3105            continue;
3106        }
3107        if let JavaType::Object(cls) = vt.vars[i].ty.erased() {
3108            if let Some(last) = cls.rsplit('$').next() {
3109                if !last.is_empty() && last.chars().all(|c| c.is_ascii_digit()) {
3110                    vt.force_concrete_vars.insert(var);
3111                }
3112            }
3113        }
3114    }
3115    // EVERY local whose resolved type is a concrete `$<digits>` class
3116    // keeps its binary name, not just field owners: when one side of an
3117    // assignment falls back to the SAM/super (`ContinuationImpl v7`) and
3118    // its sibling stays concrete (`$reportWhenComplete$1 v9`), the pair
3119    // is an uncastable "ContinuationImpl无法转换为…$1" (weibo coroutine
3120    // prologues ×282). The anon fallback still serves INTERFACE-typed
3121    // phi slots (branches minting different lambdas) — their vt type is
3122    // the join, never the $N class.
3123    let concrete: Vec<u32> = vt
3124        .vars
3125        .iter()
3126        .filter(|v| {
3127            if let JavaType::Object(cls) = v.ty.erased() {
3128                cls.rsplit('$')
3129                    .next()
3130                    .is_some_and(|l| !l.is_empty() && l.chars().all(|c| c.is_ascii_digit()))
3131            } else {
3132                false
3133            }
3134        })
3135        .map(|v| v.id)
3136        .collect();
3137    for var in concrete {
3138        vt.force_concrete_vars.insert(var);
3139    }
3140}
3141
3142/// Type inference can leave a specific-reference-typed target assigned
3143/// from a `java/lang/Object`-typed value: a phi that merged String and
3144/// Object then settled on String (`String str4; ... str4 = obj;` where
3145/// `obj` is an Object field — y5/n.java), or an Object local flowing to a
3146/// typed field. Java requires a narrowing cast there; insert `(T) value`
3147/// at the USE site (round-40 philosophy: narrow at use, never retype the
3148/// declaration). Guarded tightly: only when the value's resolved static
3149/// type is EXACTLY `java/lang/Object` (the untyped top — a known subtype
3150/// is never re-cast), the value is not a null const (assignable to any
3151/// ref) nor already a cast, and the target is a specific reference type
3152/// (Object→int/boolean is unboxing, a different problem left alone).
3153pub fn insert_object_narrowing_casts(
3154    vt: &VarTable,
3155    body: &mut Stmt,
3156    pool: &DexPool,
3157    ret: &JavaType,
3158) {
3159    // Resolve a value's static type through the VarTable for locals (the
3160    // embedded Local ty can lag infer_types), else the expr's own type.
3161    let value_ty = |e: &Expr| -> JavaType {
3162        match e {
3163            Expr::Local { var, .. } => vt.var(*var).ty.erased(),
3164            other => other.type_ref().erased(),
3165        }
3166    };
3167    let is_top_object = |e: &Expr| -> bool {
3168        value_ty(e) == JavaType::Object("java/lang/Object".into())
3169    };
3170    // A specific reference target type (a class other than
3171    // java/lang/Object, or an array — `String[] v = obj` needs the
3172    // cast exactly like `String v = obj`).
3173    let specific_ref = |ty: &TypeRef| -> Option<TypeRef> {
3174        match ty.erased() {
3175            JavaType::Object(c) if c.as_ref() != "java/lang/Object" => Some(ty.clone()),
3176            JavaType::Array(_) => Some(ty.clone()),
3177            _ => None,
3178        }
3179    };
3180    let castable = |e: &Expr| -> bool {
3181        is_top_object(e)
3182            && !matches!(e, Expr::Const(_) | Expr::Cast { .. } | Expr::InstanceOf { .. })
3183    };
3184    // Downcast witness: a SPECIFIC-ref value whose static type does not
3185    // fit the specific-ref target (`v9($1) = v7` with v7:
3186    // ContinuationImpl — weibo coroutine prologue ×282). The dex move
3187    // was verifier-proven against the target type, so the cast states
3188    // the bytecode truth; an actually-unconvertible static pair (both
3189    // classes, unrelated — the value view conflated) rides the
3190    // emitter's (Object) relay and still compiles. Framework types the
3191    // pool cannot order take the cast too: a downcast renders directly,
3192    // an unrelated pair relays.
3193    let needs_downcast = |e: &Expr, tt: &JavaType| -> bool {
3194        if matches!(e, Expr::Const(_) | Expr::Cast { .. } | Expr::InstanceOf { .. }) {
3195            return false;
3196        }
3197        let v = value_ty(e);
3198        let (JavaType::Object(vn), JavaType::Object(tn)) = (&v, tt) else {
3199            return false;
3200        };
3201        if vn.as_ref() == "java/lang/Object" || tn.as_ref() == "java/lang/Object" {
3202            return false;
3203        }
3204        vn != tn && !pool.is_subtype(vn, tn)
3205    };
3206    walk_mut_deep(body, &mut |st| match st {
3207        Stmt::ExprStmt(Expr::Assign {
3208            target,
3209            value,
3210            op: AssignOp::Plain,
3211            ..
3212        }) => {
3213            let tgt = match &**target {
3214                Expr::Local { var, .. } => vt.var(*var).ty.clone(),
3215                Expr::Field { ty, .. } => ty.clone(),
3216                // Array-element store `arr[i] = value`: the target type is
3217                // the ELEMENT type. Without this arm an Object→element
3218                // assignment (`String[] a; a[0] = pair.first` — Pair.first
3219                // erases to Object) skipped the cast and failed "Object
3220                // 无法转换为String" (weixin ×~150). Resolve the array's type
3221                // through the vt (the embedded Local ty can lag infer_types).
3222                Expr::ArrayIndex { array, .. } => {
3223                    let aty = match &**array {
3224                        Expr::Local { var, .. } => vt.var(*var).ty.erased(),
3225                        other => other.type_ref().erased(),
3226                    };
3227                    match aty {
3228                        JavaType::Array(inner) => TypeRef::J(*inner),
3229                        _ => return,
3230                    }
3231                }
3232                _ => return,
3233            };
3234            if let Some(t) = specific_ref(&tgt) {
3235                if castable(value) || needs_downcast(value, &tgt.erased()) {
3236                    let v = std::mem::replace(value, Box::new(Expr::This));
3237                    **value = Expr::Cast { ty: t, e: v };
3238                }
3239            }
3240        }
3241        Stmt::LocalDef { var, init: Some(value), .. } => {
3242            if let Some(t) = specific_ref(&vt.var(*var).ty) {
3243                let tt = vt.var(*var).ty.erased();
3244                if castable(value) || needs_downcast(value, &tt) {
3245                    let v = std::mem::replace(value, Expr::This);
3246                    *value = Expr::Cast {
3247                        ty: t,
3248                        e: Box::new(v),
3249                    };
3250                }
3251            }
3252        }
3253        Stmt::Return(Some(value)) => {
3254            if let Some(t) = specific_ref(&TypeRef::J(ret.clone())) {
3255                if castable(value) || needs_downcast(value, ret) {
3256                    let v = std::mem::replace(value, Expr::This);
3257                    *value = Expr::Cast { ty: t, e: Box::new(v) };
3258                }
3259            }
3260        }
3261        _ => {}
3262    });
3263    // Descriptor-exact formal casts at CALL args: the dex already
3264    // resolved the target method, so casting a top-Object actual to
3265    // its declared formal cannot shift overload resolution (the r57
3266    // args lesson applies to GUESSED casts, not descriptor-exact
3267    // ones). `"source_type:".concat(obj2)`, `new my0.j(obj2, ..)` —
3268    // Object无法转换为String family.
3269    walk_stmt_exprs(body, &mut |e| {
3270        deep_rewrite(e, &mut |x| {
3271            let (args, arg_tys): (&mut Vec<Expr>, &[JavaType]) = match x {
3272                Expr::Method { desc, args, is_dynamic: false, .. } => {
3273                    (args, desc.args.as_slice())
3274                }
3275                // Ctor calls are their own variant — the dex resolved the
3276                // exact <init>, so formal casts are just as safe here
3277                // (`new r(obj)` — Object无法转换为r family).
3278                Expr::New { arg_tys, args, .. } => (args, arg_tys.as_slice()),
3279                _ => return,
3280            };
3281            for (i, a) in args.iter_mut().enumerate() {
3282                let Some(formal) = arg_tys.get(i) else {
3283                    continue;
3284                };
3285                let Some(t) = specific_ref(&TypeRef::J(formal.clone())) else {
3286                    continue;
3287                };
3288                if !(castable(a) || needs_downcast(a, formal)) {
3289                    continue;
3290                }
3291                let v = std::mem::replace(a, Expr::Const(ConstVal::Null));
3292                *a = Expr::Cast { ty: t, e: Box::new(v) };
3293            }
3294        });
3295    });
3296}
3297
3298/// Numeric narrowing/widening mismatches at Assign / LocalDef / Return
3299/// positions get an explicit cast: dex int-to-byte / long-to-int flows
3300/// are implicit in the register machine, Java rejects them ("从long
3301/// 转换到int可能会有损失" — okio Buffer ×321 weibo lines). Casts at
3302/// these three positions are descriptor-faithful and cannot shift
3303/// overload resolution (the r57 args-position lesson). Boolean/numeric
3304/// mixes are NOT castable in Java and stay with the generation-split
3305/// machinery.
3306pub fn fix_primitive_assign_casts(vt: &VarTable, body: &mut Stmt, ret_ty: &JavaType) {
3307    fn numlike(t: &JavaType) -> bool {
3308        matches!(
3309            t,
3310            JavaType::Byte
3311                | JavaType::Short
3312                | JavaType::Int
3313                | JavaType::Char
3314                | JavaType::Long
3315                | JavaType::Float
3316                | JavaType::Double
3317        )
3318    }
3319    fn val_ty(e: &Expr, vt: &VarTable) -> JavaType {
3320        match e {
3321            Expr::Local { var, .. } if (*var as usize) < vt.vars.len() => {
3322                vt.var(*var).ty.erased()
3323            }
3324            other => other.type_ref().erased(),
3325        }
3326    }
3327    fn wrap(ty: &JavaType, value: &mut Expr) {
3328        if matches!(value, Expr::Cast { .. }) {
3329            return;
3330        }
3331        if val_ty_static(value) == *ty {
3332            return;
3333        }
3334        let taken = std::mem::replace(value, Expr::Const(ConstVal::Null));
3335        *value = Expr::Cast {
3336            ty: TypeRef::J(ty.clone()),
3337            e: Box::new(taken),
3338        };
3339    }
3340    fn val_ty_static(e: &Expr) -> JavaType {
3341        e.type_ref().erased()
3342    }
3343    /// Target-typed coercion for one assignment value. Numeric mixes
3344    /// cast; boolean mixes CONVERT (Java has no cast between boolean
3345    /// and any numeric): `d = b` → `(double) (b ? 1 : 0)`, `b = i` →
3346    /// `i != 0` — mirrors fix_primitive_arg_bridges' formal-side arms.
3347    /// The generation split cannot own these: at an if-join the
3348    /// one-sided split is undone (rename_gen_back) and the incompatible
3349    /// assign re-exposes ("boolean无法转换为double", weixin s9/t01-f).
3350    fn coerce(tt: &JavaType, value: &mut Expr, vt: &VarTable) {
3351        // Raw const bits against a floating target are the FLOAT value,
3352        // not an int to widen (`float f = <0x41000000 bits>` is 8.0f;
3353        // the (float) cast path would silently store 1.09e9f). Mirror of
3354        // fix_primitive_arg_bridges' descriptor-formal reinterpretation.
3355        match (&*value, tt) {
3356            (Expr::Const(ConstVal::Int(b)), JavaType::Float) => {
3357                *value = Expr::Const(ConstVal::Float(f32::from_bits(*b as u32)));
3358                return;
3359            }
3360            (Expr::Const(ConstVal::Long(b)), JavaType::Double) => {
3361                *value = Expr::Const(ConstVal::Double(f64::from_bits(*b as u64)));
3362                return;
3363            }
3364            (Expr::Const(ConstVal::Float(f)), JavaType::Int) => {
3365                *value = Expr::Const(ConstVal::Int(f.to_bits() as i32));
3366                return;
3367            }
3368            (Expr::Const(ConstVal::Double(d)), JavaType::Long) => {
3369                *value = Expr::Const(ConstVal::Long(d.to_bits() as i64));
3370                return;
3371            }
3372            _ => {}
3373        }
3374        let vt_val = val_ty(value, vt);
3375        let tt_bool = matches!(tt, JavaType::Boolean);
3376        let val_bool = matches!(vt_val, JavaType::Boolean);
3377        if tt_bool && !val_bool && numlike(&vt_val) {
3378            if matches!(
3379                value,
3380                Expr::Const(_)
3381                    | Expr::Bin { op: BinOp::Eq | BinOp::Ne, .. }
3382                    | Expr::Un { op: UnOp::Not, .. }
3383            ) {
3384                // Bare 0/1 constants render as false/true through the
3385                // emitter's bool coercion — wrapping them in `!= 0`
3386                // produced `0 != 0` noise the value-diamond fold then
3387                // had to re-interpret.
3388                return;
3389            }
3390            let taken = std::mem::replace(value, Expr::Const(ConstVal::Null));
3391            *value = Expr::Bin {
3392                op: BinOp::Ne,
3393                l: Box::new(taken),
3394                r: Box::new(Expr::Const(ConstVal::Int(0))),
3395                ty: Some(TypeRef::J(JavaType::Boolean)),
3396            };
3397        } else if !tt_bool && val_bool && numlike(tt) {
3398            if matches!(value, Expr::Cast { .. }) {
3399                return;
3400            }
3401            let taken = std::mem::replace(value, Expr::Const(ConstVal::Null));
3402            *value = Expr::Cast {
3403                ty: TypeRef::J(tt.clone()),
3404                e: Box::new(Expr::Cond {
3405                    c: Box::new(taken),
3406                    t: Box::new(Expr::Const(ConstVal::Int(1))),
3407                    f: Box::new(Expr::Const(ConstVal::Int(0))),
3408                }),
3409            };
3410        } else if numlike(tt) && numlike(&vt_val) {
3411            wrap(tt, value);
3412        }
3413    }
3414    walk_mut_deep(body, &mut |st| match st {
3415        Stmt::ExprStmt(Expr::Assign {
3416            target,
3417            op: AssignOp::Plain,
3418            value,
3419        }) => {
3420            let tt = match &**target {
3421                Expr::Local { var, .. } if (*var as usize) < vt.vars.len() => {
3422                    vt.var(*var).ty.erased()
3423                }
3424                Expr::Field { ty, .. } => ty.erased(),
3425                // Array store: the element type is the target type
3426                // (`sFormatStr[i] = v7` into char[] — "从int转换到char
3427                // 可能会有损失", weibo TimeUtils).
3428                Expr::ArrayIndex { array, .. } => match array.type_ref().erased() {
3429                    JavaType::Array(inner) => *inner,
3430                    _ => return,
3431                },
3432                _ => return,
3433            };
3434            coerce(&tt, value, vt);
3435        }
3436        Stmt::LocalDef {
3437            var,
3438            init: Some(value),
3439            ..
3440        } => {
3441            if (*var as usize) >= vt.vars.len() {
3442                return;
3443            }
3444            let tt = vt.var(*var).ty.erased();
3445            coerce(&tt, value, vt);
3446        }
3447        Stmt::Return(Some(e)) => {
3448            if !numlike(ret_ty) || !numlike(&val_ty(e, vt)) {
3449                return;
3450            }
3451            wrap(ret_ty, e);
3452        }
3453        _ => {}
3454    });
3455}
3456
3457/// Shadowed-ancestor field witness (the upcast sibling of
3458/// fix_field_owner_downcasts).
3459///
3460/// Java resolves a field access on the RECEIVER'S STATIC TYPE downward:
3461/// when the dex writes an ancestor's field (`iput … Fragment.A02:I`)
3462/// but a class between the receiver's static type and the declaring
3463/// ancestor declares a field with the SAME rendered name (`ImageView
3464/// A02`), the plain `this.A02` silently binds to the subclass field —
3465/// `int无法转换为ImageView` when the types differ, and a wrong-field
3466/// read/write when they don't (WhatsApp ActivitySheetFragment writes
3467/// the layout resid into the ancestor int field; its own A02 is an
3468/// ImageView — 1,216 assignment-shape sites in the WA tree). jadx
3469/// renders `super.A02`; the IR has no super-field form, so witness
3470/// with the always-legal upcast `((Fragment) this).A02` — field lookup
3471/// on a cast expression is static, landing on the ancestor at any
3472/// chain depth. The receiver may still be the typed this-Local at this
3473/// pipeline point (the This rewrite comes later), so the compile-time
3474/// type comes from the owner expr / var table, mirroring
3475/// fix_field_owner_downcasts' owner_internal. Fires ONLY on a real
3476/// same-display shadow; unshadowed ancestor refs inherit through
3477/// `this.X` and must not churn. Same-raw-name lookups only: a shadow
3478/// whose raw name differs but display collides is beyond the pool
3479/// lookup keyed by name.
3480pub fn fix_shadowed_super_fields(
3481    body: &mut Stmt,
3482    vt: &VarTable,
3483    pool: &DexPool,
3484    class_name: &str,
3485) {
3486    /// Rendered name of `(cls, raw field name)`: the registry display
3487    /// when the class declares the field, else the raw name. None when
3488    /// the class is not pool-materialized or has no such field.
3489    fn display(pool: &DexPool, cls: &str, name: &str) -> Option<String> {
3490        let pc = pool.get(cls)?;
3491        let f = pc
3492            .static_fields
3493            .iter()
3494            .chain(pc.instance_fields.iter())
3495            .find(|f| f.name.as_str() == name)?;
3496        Some(
3497            jdc_core::rename::field_display(cls, name, &f.desc)
3498                .map(|d| d.to_string())
3499                .unwrap_or_else(|| name.to_string()),
3500        )
3501    }
3502    fn owner_ct(
3503        o: &Option<Box<Expr>>,
3504        vt: &VarTable,
3505        class_name: &str,
3506    ) -> Option<std::sync::Arc<str>> {
3507        match o.as_deref() {
3508            None | Some(Expr::This) => Some(std::sync::Arc::from(class_name)),
3509            Some(Expr::Local { var, .. }) => match vt.var(*var).ty.erased() {
3510                JavaType::Object(n) => Some(n),
3511                _ => None,
3512            },
3513            Some(other) => match other.type_ref().erased() {
3514                JavaType::Object(n) => Some(n),
3515                _ => None,
3516            },
3517        }
3518    }
3519    walk_stmt_exprs(body, &mut |e| {
3520        deep_rewrite(e, &mut |x| {
3521            let Expr::Field {
3522                owner,
3523                cls,
3524                name,
3525                is_static: false,
3526                ..
3527            } = x
3528            else {
3529                return;
3530            };
3531            let Some(ot) = owner_ct(owner, vt, class_name) else {
3532                return;
3533            };
3534            if cls.as_ref() == ot.as_ref() {
3535                return;
3536            }
3537            let Some(ref_display) = display(pool, cls, name) else {
3538                return;
3539            };
3540            // Walk from the receiver's compile-time type up to (not
3541            // including) the declaring ancestor: any same-display
3542            // declaration on the way shadows the ref.
3543            let mut cur = ot.to_string();
3544            let mut shadowed = false;
3545            let mut found = false;
3546            for _ in 0..64 {
3547                if display(pool, &cur, name).as_deref() == Some(ref_display.as_str()) {
3548                    shadowed = true;
3549                }
3550                let Some(pc) = pool.get(&cur) else {
3551                    return;
3552                };
3553                let Some(sup) = pc.super_name.clone() else {
3554                    return;
3555                };
3556                if sup.as_str() == cls.as_ref() {
3557                    found = true;
3558                    break;
3559                }
3560                cur = sup;
3561            }
3562            if !found || !shadowed {
3563                return;
3564            }
3565            let ty = TypeRef::J(JavaType::Object(cls.clone()));
3566            match owner {
3567                Some(o) => {
3568                    if let Expr::Cast { ty: ct, .. } = &mut **o {
3569                        *ct = ty;
3570                    } else {
3571                        let taken = std::mem::replace(&mut **o, Expr::This);
3572                        **o = Expr::Cast {
3573                            ty,
3574                            e: Box::new(taken),
3575                        };
3576                    }
3577                }
3578                None => {
3579                    *owner = Some(Box::new(Expr::Cast {
3580                        ty,
3581                        e: Box::new(Expr::This),
3582                    }))
3583                }
3584            }
3585        });
3586    });
3587}
3588
3589/// Subclass-field downcast witness + assign-cast narrowing.
3590///
3591/// R8 pushes fields DOWN into leaf classes (weibo: coroutine `label`
3592/// lives on each `Foo$bar$1` continuation, NOT on the rendered
3593/// BaseContinuationImpl/ContinuationImpl supers) while a check-cast in
3594/// the same flow types the receiver as the SUPERCLASS. The dex field
3595/// ref names the leaf (`$1->label` — verifier-legal through the
3596/// instance-of branch type), but Java resolves `((ContinuationImpl) c)
3597/// .label` against ContinuationImpl — "找不到符号 变量 label" (weibo
3598/// ×420). When the field's dex owner is a strict subtype of the
3599/// receiver's static type and the receiver's hierarchy does NOT
3600/// declare the name, re-target the receiver cast to the owner (the
3601/// runtime object IS the owner type — the verifier proved it).
3602///
3603/// Second rule: a `(T) e` value assigned into an S-typed target where
3604/// S <: T strictly ("ContinuationImpl无法转换为$reportWhenComplete$1")
3605/// narrows the cast to S — same verifier guarantee, and an assignment
3606/// position has no overload resolution to perturb.
3607pub fn fix_field_owner_downcasts(body: &mut Stmt, vt: &VarTable, pool: &DexPool) {
3608    fn owner_internal(o: &Expr, vt: &VarTable) -> Option<std::sync::Arc<str>> {
3609        let t = match o {
3610            Expr::Local { var, .. } => vt.var(*var).ty.erased(),
3611            other => other.type_ref().erased(),
3612        };
3613        match t {
3614            JavaType::Object(n) => Some(n),
3615            _ => None,
3616        }
3617    }
3618    fn declares_up(pool: &DexPool, class: &str, name: &str) -> bool {
3619        let mut cur = Some(class.to_string());
3620        let mut hops = 0;
3621        while let Some(c) = cur {
3622            hops += 1;
3623            if hops > 64 {
3624                return false;
3625            }
3626            let Some(pc) = pool.get(&c) else { return false };
3627            if pc
3628                .instance_fields
3629                .iter()
3630                .chain(pc.static_fields.iter())
3631                .any(|f| f.name.as_str() == name)
3632            {
3633                return true;
3634            }
3635            cur = pc.super_name.clone();
3636        }
3637        false
3638    }
3639    walk_stmt_exprs(body, &mut |e| {
3640        deep_rewrite(e, &mut |x| {
3641            if let Expr::Field {
3642                owner: Some(o),
3643                cls,
3644                name,
3645                is_static: false,
3646                ..
3647            } = x
3648            {
3649                let Some(ot) = owner_internal(o, vt) else {
3650                    return;
3651                };
3652                if ot.as_ref() == cls.as_ref() || !pool.is_subtype(cls, &ot) {
3653                    return;
3654                }
3655                if declares_up(pool, &ot, name) || !declares_up(pool, cls, name) {
3656                    return;
3657                }
3658                let ty = TypeRef::J(JavaType::Object(cls.clone()));
3659                if let Expr::Cast { ty: ct, .. } = &mut **o {
3660                    *ct = ty;
3661                } else {
3662                    let taken = std::mem::replace(&mut **o, Expr::This);
3663                    **o = Expr::Cast { ty, e: Box::new(taken) };
3664                }
3665            }
3666            // Rule 2: `(T) e` into a strict-subtype target narrows to S.
3667            let (tgt_ty, value) = match x {
3668                Expr::Assign { target, op: AssignOp::Plain, value } => {
3669                    let t = match &**target {
3670                        Expr::Local { var, .. } => vt.var(*var).ty.erased(),
3671                        Expr::Field { ty, .. } => ty.erased(),
3672                        _ => return,
3673                    };
3674                    (t, value)
3675                }
3676                _ => return,
3677            };
3678            let JavaType::Object(sn) = tgt_ty else { return };
3679            if let Expr::Cast { ty, .. } = &mut **value {
3680                if let TypeRef::J(JavaType::Object(tn)) = ty {
3681                    if tn.as_ref() != sn.as_ref() && pool.is_subtype(&sn, tn) {
3682                        *ty = TypeRef::J(JavaType::Object(sn.clone()));
3683                    }
3684                }
3685            }
3686        });
3687    });
3688    // LocalDef inits: same narrowing against the declared var type.
3689    walk_mut_deep(body, &mut |st| {
3690        if let Stmt::LocalDef { var, init: Some(value), .. } = st {
3691            let tt = if (*var as usize) < vt.vars.len() {
3692                vt.vars[*var as usize].ty.erased()
3693            } else {
3694                return;
3695            };
3696            let JavaType::Object(sn) = tt else { return };
3697            if let Expr::Cast { ty, .. } = value {
3698                if let TypeRef::J(JavaType::Object(tn)) = ty {
3699                    if tn.as_ref() != sn.as_ref() && pool.is_subtype(&sn, tn) {
3700                        *ty = TypeRef::J(JavaType::Object(sn.clone()));
3701                    }
3702                }
3703            }
3704        }
3705    });
3706}
3707
3708/// `==`/`!=` between UNRELATED class types is a compile error in Java
3709/// ("不可比较的类型: f0和a") while dex if-eqObj happily compares any two
3710/// references (weixin fn1/w1 ×298 files). One side takes an `(Object)`
3711/// witness cast — legal against anything. Interfaces and subtype-related
3712/// pairs are already comparable and stay untouched; java/lang/Object and
3713/// arrays-of it likewise.
3714pub fn fix_incomparable_equality(body: &mut Stmt, vt: &VarTable, pool: &crate::DexPool) {
3715    fn side_ty(e: &Expr, vt: &VarTable) -> JavaType {
3716        match e {
3717            Expr::Local { var, .. } if (*var as usize) < vt.vars.len() => {
3718                vt.var(*var).ty.erased()
3719            }
3720            other => other.type_ref().erased(),
3721        }
3722    }
3723    let incomparable = |a: &JavaType, b: &JavaType| -> bool {
3724        use JavaType::{Array, Object};
3725        match (a, b) {
3726            (Object(x), Object(y)) => {
3727                if x == y || x.as_ref() == "java/lang/Object" || y.as_ref() == "java/lang/Object"
3728                {
3729                    return false;
3730                }
3731                let xi = pool.get(x).map(|c| c.is_interface()).unwrap_or(true);
3732                let yi = pool.get(y).map(|c| c.is_interface()).unwrap_or(true);
3733                // Unknown classes (framework, not in pool): a cast is
3734                // harmless but usually unnecessary — treat as comparable
3735                // to stay conservative.
3736                if xi || yi {
3737                    return false;
3738                }
3739                !pool.is_subtype(x, y) && !pool.is_subtype(y, x)
3740            }
3741            (Array(_), Object(y)) | (Object(y), Array(_)) => {
3742                y.as_ref() != "java/lang/Object"
3743                    && y.as_ref() != "java/lang/Cloneable"
3744                    && y.as_ref() != "java/io/Serializable"
3745                    && !pool.get(y).map(|c| c.is_interface()).unwrap_or(false)
3746            }
3747            _ => false,
3748        }
3749    };
3750    let is_ref = |t: &JavaType| matches!(t, JavaType::Object(_) | JavaType::Array(_));
3751    walk_stmt_exprs(body, &mut |e| {
3752        deep_rewrite(e, &mut |x| {
3753            if let Expr::Bin {
3754                op: BinOp::Eq | BinOp::Ne,
3755                l,
3756                r,
3757                ..
3758            } = x
3759            {
3760                let lt = side_ty(l, vt);
3761                let rt = side_ty(r, vt);
3762                // `obj != 0` → `obj != null` with FINAL types: the
3763                // lift-time null_side_rewrite ran before infer_types
3764                // (its obj table saw the pre-inference int), leaving
3765                // `sQLiteClosable != 0` — "二元运算符 '!=' 的操作数
3766                // 类型错误" ×700 weixin.
3767                if is_ref(&lt) && matches!(&**r, Expr::Const(ConstVal::Int(0))) {
3768                    **r = Expr::Const(ConstVal::Null);
3769                    return;
3770                }
3771                if is_ref(&rt) && matches!(&**l, Expr::Const(ConstVal::Int(0))) {
3772                    **l = Expr::Const(ConstVal::Null);
3773                    return;
3774                }
3775                if incomparable(&lt, &rt) {
3776                    let taken = std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
3777                    **l = Expr::Cast {
3778                        ty: TypeRef::J(JavaType::Object("java/lang/Object".into())),
3779                        e: taken,
3780                    };
3781                }
3782            }
3783        });
3784    });
3785}
3786
3787/// Primitive bridges at CALL/CTOR argument positions. Dex slots are
3788/// untyped category-1 integers: the verifier happily passes a Z-slot to
3789/// a `(B)` formal (weibo's Meituan-Robust hotpatch boilerplate boxes
3790/// boolean params via `new Byte(zreg)` — 4.2k+ `对于Byte(boolean),
3791/// 找不到合适的构造器`), and obfuscated/Kotlin code passes 0/1 slots to
3792/// `(Z)` formals (`int无法转换为boolean` at call sites). Java source
3793/// cannot express either — bridge at the IR level:
3794///   Boolean actual → numeric formal:  `(byte) (b ? 1 : 0)`
3795///   numeric actual → Boolean formal:  `x != 0`
3796/// The target method is DESCRIPTOR-bound (bytecode method_ref), so a
3797/// cast/compare to the exact formal type cannot flip overload
3798/// resolution the way r57's inferred object casts did — the rendered
3799/// call selects the same signature the dex named.
3800pub fn fix_primitive_arg_bridges(body: &mut Stmt, vt: &VarTable, pool: &crate::DexPool) {
3801    fn val_ty(e: &Expr, vt: &VarTable) -> JavaType {
3802        match e {
3803            Expr::Local { var, .. } => vt.var(*var).ty.erased(),
3804            other => other.type_ref().erased(),
3805        }
3806    }
3807    #[allow(clippy::type_complexity)]
3808    let is_bool = |t: &JavaType| matches!(t, JavaType::Boolean);
3809    let is_num = |t: &JavaType| {
3810        matches!(
3811            t,
3812            JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
3813                | JavaType::Long | JavaType::Float | JavaType::Double
3814        )
3815    };
3816    // BORROWED-arg form: returns the bridged replacement without
3817    // consuming the original (a `mem::replace` + `Option` dance dropped
3818    // the argument on the None path — every Boolean→Boolean arg became
3819    // a literal `null`: 61k 引用不明确 in one battery run).
3820    fn bridge(actual: &JavaType, formal: &JavaType, arg: &Expr) -> Option<Expr> {
3821        // A dex const carries the CALLEE's BITS: `const v, #0x41000000`
3822        // feeding a float formal IS 8.0f. Rendering the raw int literal
3823        // (a) mis-selects overloads — a competing `a(Context,int):void`
3824        // beats the int→float-widened `a(Context,float):int` in javac's
3825        // phase order ("不兼容的类型: void无法转换为int", weibo ju.a
3826        // ×1,259) — and (b) corrupts the VALUE (widening turns the bits
3827        // into 1.09e9f). The descriptor formal is dex ground truth:
3828        // reinterpret. Symmetric for the float-const-into-int-formal
3829        // direction (caller's bits, callee's reading).
3830        match (arg, formal) {
3831            (Expr::Const(ConstVal::Int(b)), JavaType::Float) => {
3832                return Some(Expr::Const(ConstVal::Float(f32::from_bits(*b as u32))));
3833            }
3834            (Expr::Const(ConstVal::Long(b)), JavaType::Double) => {
3835                return Some(Expr::Const(ConstVal::Double(f64::from_bits(*b as u64))));
3836            }
3837            (Expr::Const(ConstVal::Float(f)), JavaType::Int) => {
3838                return Some(Expr::Const(ConstVal::Int(f.to_bits() as i32)));
3839            }
3840            (Expr::Const(ConstVal::Double(d)), JavaType::Long) => {
3841                return Some(Expr::Const(ConstVal::Long(d.to_bits() as i64)));
3842            }
3843            _ => {}
3844        }
3845        if matches!(actual, JavaType::Boolean)
3846            && matches!(
3847                formal,
3848                JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
3849                    | JavaType::Long | JavaType::Float | JavaType::Double
3850            )
3851        {
3852            Some(Expr::Cast {
3853                ty: TypeRef::J(formal.clone()),
3854                e: Box::new(Expr::Cond {
3855                    c: Box::new(arg.clone()),
3856                    t: Box::new(Expr::Const(ConstVal::Int(1))),
3857                    f: Box::new(Expr::Const(ConstVal::Int(0))),
3858                }),
3859            })
3860        } else if matches!(formal, JavaType::Boolean)
3861            && matches!(
3862                actual,
3863                JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
3864                    | JavaType::Long | JavaType::Float | JavaType::Double
3865            )
3866        {
3867            Some(Expr::Bin {
3868                op: BinOp::Ne,
3869                l: Box::new(arg.clone()),
3870                r: Box::new(Expr::Const(ConstVal::Int(0))),
3871                ty: Some(TypeRef::J(JavaType::Boolean)),
3872            })
3873        } else if matches!(
3874            actual,
3875            JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
3876                | JavaType::Long | JavaType::Float | JavaType::Double
3877        ) && matches!(
3878            formal,
3879            JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
3880                | JavaType::Long | JavaType::Float | JavaType::Double
3881        ) && !implicit_widening(actual, formal)
3882        {
3883            // Numeric NARROWING into a formal (int arg → char formal):
3884            // javac rejects the lossy conversion, or — worse when the
3885            // class has no wider overload — silently resolves a
3886            // DIFFERENT overload than the dex descriptor names
3887            // (SpannableStringBuilder.append(int) doesn't exist;
3888            // `append(char)` is "从int转换到char可能会有损失", weibo ×76).
3889            // The descriptor formal is the dex ground truth; casting to
3890            // it pins the overload the bytecode actually invoked.
3891            Some(Expr::Cast {
3892                ty: TypeRef::J(formal.clone()),
3893                e: Box::new(arg.clone()),
3894            })
3895        } else {
3896            None
3897        }
3898    }
3899    /// A bare `null` argument against a SPECIFIC reference formal takes
3900    /// the descriptor cast: `this(context, null)` matching both
3901    /// Builder(Context,Notification) and Builder(Context,String) is
3902    /// "对Builder的引用不明确" (weibo NotificationCompat; protobuf
3903    /// `new FieldSet.Builder(null)` ×54). The descriptor names the
3904    /// exact invoked ctor, so the cast is dex ground truth — the
3905    /// descriptor-exact doctrine, not a guessed overload shift. Bare
3906    /// null stays for Object formals (casting those would add noise
3907    /// without resolving anything javac could not already pick).
3908    /// Framework classes javac resolves through android.jar even when
3909    /// the dex pool lacks them — the embedded API database answers
3910    /// precisely (the hand-written prefix list it replaced missed
3911    /// everything outside the guessed roots and over-trusted within
3912    /// them; removed-API families like org.apache.http are in the DB
3913    /// too, matching what a legacy-jar classpath resolves).
3914    fn fw_resolvable(n: &str) -> bool {
3915        crate::fwdb::exists(n)
3916    }
3917    fn null_arg_cast(a: &mut Expr, f: &JavaType, pool: &DexPool) -> bool {
3918        if !matches!(&*a, Expr::Const(ConstVal::Null)) {
3919            return false;
3920        }
3921        if let JavaType::Object(n) = f {
3922            // Cast only to RESOLVABLE types: a phantom formal (R8 kept
3923            // the ref, dropped the class — androidx.window.sidecar)
3924            // would turn a compiling bare `null` into a fresh
3925            // cannot-find. Pool classes render into the output;
3926            // java/javax/android/dalvik resolve through android.jar.
3927            let resolvable = pool.get(n).is_some() || fw_resolvable(n);
3928            if n.as_ref() != "java/lang/Object" && resolvable {
3929                *a = Expr::Cast {
3930                    ty: TypeRef::J(f.clone()),
3931                    e: Box::new(Expr::Const(ConstVal::Null)),
3932                };
3933                return true;
3934            }
3935        } else if let JavaType::Array(_) = f {
3936            // Descriptor-exact ARRAY cast: the dex named this exact
3937            // overload; a bare null beside sibling overloads is javac-
3938            // ambiguous (`a(String,b2)` + `a(String,String[])` both match
3939            // — weibo org/a/c/b, 对a的引用不明确 family). Same phantom
3940            // gate on the element class: casting to a phantom-element
3941            // array would trade the ambiguity for a cannot-find.
3942            let mut e: &JavaType = f;
3943            while let JavaType::Array(inner) = e {
3944                e = &**inner;
3945            }
3946            let resolvable = match e {
3947                JavaType::Object(n) => pool.get(n).is_some() || fw_resolvable(n),
3948                _ => true, // primitive-element array
3949            };
3950            if resolvable {
3951                *a = Expr::Cast {
3952                    ty: TypeRef::J(f.clone()),
3953                    e: Box::new(Expr::Const(ConstVal::Null)),
3954                };
3955                return true;
3956            }
3957        }
3958        // Bare null deliberately stays (Object formal / phantom type):
3959        // the caller's ambiguity pin may still act on it.
3960        false
3961    }
3962    /// JLS 5.1.2 widening primitive conversion (char joins the chain
3963    /// only at int; byte/short do NOT widen to char).
3964    fn implicit_widening(a: &JavaType, f: &JavaType) -> bool {
3965        use JavaType::*;
3966        match (a, f) {
3967            (Byte, Short) | (Byte, Int) | (Byte, Long) | (Byte, Float) | (Byte, Double) => true,
3968            (Short, Int) | (Short, Long) | (Short, Float) | (Short, Double) => true,
3969            (Char, Int) | (Char, Long) | (Char, Float) | (Char, Double) => true,
3970            (Int, Long) | (Int, Float) | (Int, Double) => true,
3971            (Long, Float) | (Long, Double) => true,
3972            (Float, Double) => true,
3973            (x, y) => x == y,
3974        }
3975    }
3976    // Does the owner class declare ANOTHER method with the same name
3977    // and arity but a different signature? Then javac's most-specific
3978    // overload resolution can steal the call from the dex descriptor's
3979    // target (weibo 对a的引用不明确 ×391: `c.a(null)` against
3980    // a(ExecuteResult)/a(Exception)/a(Object) — the descriptor names
3981    // a(Object), but bare null leaves the two specific overloads
3982    // ambiguously most-specific). Framework owners are not in the
3983    // pool — conservatively report no competition.
3984    let overload_competes = |cls: &str, name: &str, args: &[JavaType]| -> bool {
3985        if let Some(c) = pool.get(cls) {
3986            return c.all_methods().any(|m| {
3987                &*m.name == name
3988                    // Bridge/synthetic siblings are compiler artifacts
3989                    // resolving to the SAME target — counting them as
3990                    // competitors cast enum compareTo args to the erased
3991                    // formal the rendered specialization no longer
3992                    // accepts (Enum无法转换为LogLevel ×2,079 reqable).
3993                    && m.access & (crate::access::ACC_BRIDGE | crate::access::ACC_SYNTHETIC) == 0
3994                    && m.parsed_desc()
3995                        .is_some_and(|d| d.args.len() == args.len() && d.args != args)
3996            });
3997        }
3998        // Framework owner: NOT enumerable for pin purposes — the
3999        // full-DB enumeration experiment (step-5) measured +532 battery
4000        // with ZERO ambiguous gain: a subtype arg against a concrete
4001        // erased formal fires the cast even when the call was never
4002        // ambiguous, and bounded-generic formals (List<T> -> List,
4003        // Property<T,Float> erasures) make the cast break inference or
4004        // miss the rendered specialization. Framework subtype pins stay
4005        // on the measured allowlist below.
4006        let _ = args;
4007        false
4008    };
4009    // Ctors: formal types via the pool (unanimous across same-arity
4010    // overloads, else skip — ambiguity must not guess).
4011    let ctor_formals = |cls: &str, n: usize| -> Option<Vec<JavaType>> {
4012        let c = pool.get(cls)?;
4013        let mut out: Option<Vec<JavaType>> = None;
4014        for m in c.all_methods() {
4015            if &*m.name != "<init>" {
4016                continue;
4017            }
4018            let Some(d) = m.parsed_desc() else { continue };
4019            if d.args.len() != n {
4020                continue;
4021            }
4022            match &out {
4023                None => out = Some(d.args.clone()),
4024                Some(prev) if *prev != d.args => return None,
4025                Some(_) => {}
4026            }
4027        }
4028        out
4029    };
4030    walk_stmt_exprs(body, &mut |e| {
4031        deep_rewrite(e, &mut |x| match x {
4032            Expr::Method {
4033                cls,
4034                name,
4035                desc,
4036                args,
4037                is_static,
4038                ..
4039            } => {
4040                if args.len() != desc.args.len() {
4041                    return;
4042                }
4043                let _ = is_static;
4044                let mut competes: Option<bool> = None;
4045                for (a, f) in args.iter_mut().zip(desc.args.iter()) {
4046                    if null_arg_cast(a, f, pool) {
4047                        continue;
4048                    }
4049                    if matches!(a, Expr::Cast { .. }) {
4050                        continue;
4051                    }
4052                    // Descriptor-exact ambiguity pin: a bare null
4053                    // against an Object formal (null_arg_cast
4054                    // deliberately leaves those bare — safe only
4055                    // WITHOUT competing specific overloads) or a
4056                    // strict-subtype arg can match several declared
4057                    // overloads; casting to the descriptor formal
4058                    // leaves exactly the dex-invoked method
4059                    // applicable (JLS 15.12.2 — the descriptor is
4060                    // ground truth, so the cast pins resolution
4061                    // instead of shifting it).
4062                    let subtype_arg = match (&val_ty(a, vt), f) {
4063                        (JavaType::Object(an), JavaType::Object(fn_)) => {
4064                            // java/lang/Object is the root: ANY reference
4065                            // arg is a strict subtype of it — pool.is_subtype
4066                            // cannot see framework chains (String -> Object),
4067                            // so the Object formal gets the direct rule.
4068                            (an != fn_ && pool.is_subtype(an, fn_))
4069                                || (fn_.as_ref() == "java/lang/Object"
4070                                    && an.as_ref() != "java/lang/Object")
4071                        }
4072                        (JavaType::Array(_), JavaType::Object(fn_))
4073                            if fn_.as_ref() == "java/lang/Object" =>
4074                        {
4075                            true
4076                        }
4077                        _ => false,
4078                    };
4079                    // Bare null against java/lang/Object itself: the
4080                    // descriptor method exists (dex resolved it), so
4081                    // `(Object) null` is always legal and excludes every
4082                    // more-specific sibling overload from applicability.
4083                    // Pool owners take the competes gate (emitted
4084                    // declarations are raw, so the cast cannot disturb
4085                    // inference). FRAMEWORK owners only via allowlist of
4086                    // the classic non-generic null-ambiguous JDK methods
4087                    // (StringBuilder.append ×66 lark): a blanket cast
4088                    // breaks GENERIC framework methods whose descriptor
4089                    // formal is an ERASED type variable —
4090                    // `ofFloat((Object) null, View.ALPHA, v)` killed the
4091                    // T=View inference bare null provided (weibo +4).
4092                    let null_obj =
4093                        matches!(a, Expr::Const(ConstVal::Null)) && matches!(f, JavaType::Object(_));
4094                    let framework_owner = pool.get(cls.as_ref()).is_none();
4095                    let fw_null_ambiguous = framework_owner
4096                        && matches!(
4097                            (cls.as_ref(), name.as_ref()),
4098                            ("java/lang/StringBuilder", "append")
4099                                | ("java/lang/StringBuffer", "append")
4100                                | ("java/io/PrintStream", "println")
4101                                | ("java/io/PrintStream", "print")
4102                                | ("java/io/PrintWriter", "println")
4103                                | ("java/io/PrintWriter", "print")
4104                                | ("java/lang/String", "valueOf")
4105                        );
4106                    // Framework owners with famously overload-heavy
4107                    // methods take the subtype pin too: the arg's
4108                    // rendered type implements SEVERAL formal types
4109                    // (weibo putExtra ×41: floatMsgData is both
4110                    // Parcelable and Serializable; schedule/submit:
4111                    // Runnable-and-Callable workers) and the competes
4112                    // gate cannot enumerate framework overloads.
4113                    // Formal == java/lang/Object stays excluded — that
4114                    // is an erased type variable and the cast would
4115                    // kill generic inference (ofFloat lesson).
4116                    let fw_subtype_pinnable = framework_owner
4117                        && subtype_arg
4118                        && !matches!(f, JavaType::Object(fn_) if fn_.as_ref() == "java/lang/Object")
4119                        && matches!(
4120                            (cls.as_ref(), name.as_ref()),
4121                            ("android/content/Intent", "putExtra")
4122                                | ("java/util/concurrent/ScheduledExecutorService", "schedule")
4123                                | ("java/util/concurrent/ExecutorService", "submit")
4124                                | ("java/util/concurrent/ExecutorService", "invokeAll")
4125                        );
4126                    if fw_subtype_pinnable
4127                        || (subtype_arg
4128                            && *competes.get_or_insert_with(|| {
4129                                overload_competes(cls, name, &desc.args)
4130                            }))
4131                        || (null_obj
4132                            && (fw_null_ambiguous
4133                                || *competes.get_or_insert_with(|| {
4134                                    overload_competes(cls, name, &desc.args)
4135                                })))
4136                    {
4137                        let old =
4138                            std::mem::replace(a, Expr::Const(ConstVal::Null));
4139                        *a = Expr::Cast {
4140                            ty: TypeRef::J(f.clone()),
4141                            e: Box::new(old),
4142                        };
4143                        continue;
4144                    }
4145                    if !is_bool(&val_ty(a, vt)) && !is_num(&val_ty(a, vt)) {
4146                        continue;
4147                    }
4148                    if !is_bool(f) && !is_num(f) {
4149                        continue;
4150                    }
4151                    if let Some(b) = bridge(&val_ty(a, vt), f, a) {
4152                        *a = b;
4153                    }
4154                }
4155            }
4156            Expr::New { cls, args, arg_tys, .. } => {
4157                let formals_owned = if arg_tys.len() == args.len() && !arg_tys.is_empty() {
4158                    Some(arg_tys.clone())
4159                } else {
4160                    ctor_formals(cls, args.len())
4161                };
4162                if let Some(formals) = formals_owned {
4163                    for (a, f) in args.iter_mut().zip(formals.iter()) {
4164                        if null_arg_cast(a, f, pool) {
4165                            continue;
4166                        }
4167                        if matches!(a, Expr::Cast { .. }) {
4168                            continue;
4169                        }
4170                        if !is_bool(f) && !is_num(f) {
4171                            continue;
4172                        }
4173                        if let Some(b) = bridge(&val_ty(a, vt), f, a) {
4174                            *a = b;
4175                        }
4176                    }
4177                }
4178            }
4179            _ => {}
4180        });
4181    });
4182}
4183
4184/// `b ^ 1` / `1 ^ b` with a BOOLEAN operand is the bytecode's boolean
4185/// negation (`!b`) — weibo kotlin-stdlib `return v2 ^ 1;` ("二元运算符
4186/// '^' 的操作数类型错误"), and `x ^ true` shapes. Java only types `^`
4187/// for same-category operands.
4188/// Rename locals whose name equals a ROOT PACKAGE first segment: a
4189/// package-qualified render (`v2.n.a` — the FQN fallback a twin-shadowed
4190/// same-package ref must take) binds its first identifier lexically, and
4191/// a local `v2` in scope captures it (weixin v2/i: 找不到符号 变量 n on a
4192/// legit `v2.n.a(this, 0)`). Method-scope renames carry no registry
4193/// surface — every use renders through this same vt — unlike class/field
4194/// renames, whose reference coverage blew up five times before.
4195pub fn deshadow_locals(vt: &mut VarTable, pool: &crate::DexPool, class_name: &str, body: &Stmt) {
4196    let segs = pool.root_pkg_segs();
4197    // Import-layer simples: the file renders `import p.h;` + bare `h.e`
4198    // for obscured refs — a local `h` captures that simple name (locals
4199    // beat imports in expression position; 无法取消引用int family).
4200    let obscured = crate::classdec::obscured_simples_snapshot();
4201    // Same-package static-ref simples: a static member ref renders its
4202    // class qualifier as the SIMPLE name (same package — no import/FQN),
4203    // and a local with that name captures it (`a2.changeQuickRedirect`
4204    // bound to a String local `a2` — "找不到符号 变量 changeQuickRedirect
4205    // 位置: 类型为String的变量 a2", weibo robust-field refs ×317). Only
4206    // STATIC refs qualify (an instance ref's qualifier IS the local).
4207    let self_pkg = class_name.rfind('/').map(|i| &class_name[..i]).unwrap_or("");
4208    let mut same_pkg_simples: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
4209    visit_all_exprs(body, &mut |x| {
4210        let cls = match x {
4211            Expr::Field { cls, is_static: true, .. } => Some(cls.as_ref()),
4212            Expr::Method { cls, is_static: true, .. } => Some(cls.as_ref()),
4213            _ => None,
4214        };
4215        if let Some(c) = cls {
4216            let pkg = c.rfind('/').map(|i| &c[..i]).unwrap_or("");
4217            if pkg == self_pkg {
4218                let simple = &c[c.rfind('/').map(|i| i + 1).unwrap_or(0)..];
4219                let head = simple.split('$').next().unwrap_or(simple);
4220                if !head.is_empty() {
4221                    same_pkg_simples.insert(head.to_string());
4222                }
4223            }
4224        }
4225    });
4226    let hit = |name: &str| {
4227        segs.contains(name) || obscured.contains(name) || same_pkg_simples.contains(name)
4228    };
4229    if (segs.is_empty() && obscured.is_empty() && same_pkg_simples.is_empty())
4230        || !vt.vars.iter().any(|v| hit(&v.name))
4231    {
4232        return;
4233    }
4234    let mut taken: jdc_core::FxHashSet<String> =
4235        vt.vars.iter().map(|v| v.name.clone()).collect();
4236    for v in vt.vars.iter_mut() {
4237        if !hit(&v.name) {
4238            continue;
4239        }
4240        let mut k = 0u32;
4241        let cand = loop {
4242            k += 1;
4243            let c = if k == 1 {
4244                format!("{}x", v.name)
4245            } else {
4246                format!("{}x{}", v.name, k)
4247            };
4248            if !taken.contains(&c) {
4249                break c;
4250            }
4251        };
4252        taken.insert(cand.clone());
4253        v.name = cand;
4254    }
4255}
4256
4257/// Receiver rescue: a Field/Method receiver whose final vt type is
4258/// PRIMITIVE can never hold the member (structurer tail-copy regions
4259/// can map the receiver register onto a boolean/int generation —
4260/// wcdb HandleOperation's `v10_g2.finalizeStatement()` where the
4261/// statement var is right there; weixin deref-int/bool families,
4262/// ~2.5k lines). The member descriptor's owner class is authoritative:
4263/// when EXACTLY ONE var in the method has that type (or a subtype),
4264/// swap the receiver to it. The pre-rescue output is a guaranteed
4265/// compile error AND runtime NPE, so the unambiguous swap cannot make
4266/// it worse; ambiguous sites stay untouched.
4267/// Anonymous/inner-class outer reads: the dex reads `this$0` off the
4268/// instance register, which copy materialization can route through a
4269/// local (`v41 = this; ... v41.this$0` — weibo HorseRaceDetector$1,
4270/// 2,326-line 找不到符号 变量 this$0 family). When the field's
4271/// declaring class IS the class being decompiled, the only instance in
4272/// scope is `this` — strip the owner so it renders the bare declared
4273/// field.
4274/// Fold single-assigned `this`-copy locals in ctors back to `this`.
4275/// R8 materializes the receiver with `move-object/from16 v1, p0` and
4276/// runs every iput through the copy; the copy renders as a local and a
4277/// final-field store through it is "无法为 final 变量 g 分配值" (Java
4278/// allows blank-final writes only via `this`/simple name — hf/n2 ×29,
4279/// weixin final-var residual). Transitively: `b = a; c = b` chains
4280/// collapse. Single-assignment gate: a var ever written from another
4281/// value is not an alias.
4282pub fn fix_ctor_this_aliases(body: &mut Stmt, vt: &VarTable) {
4283    let Some(this_var) = vt
4284        .vars
4285        .iter()
4286        .find(|v| v.is_param && v.name == "this")
4287        .map(|v| v.id)
4288    else {
4289        return;
4290    };
4291    let mut analysis = VarAnalysis {
4292        assigns: Vec::new(),
4293        reads: Vec::new(),
4294        values: Vec::new(),
4295    };
4296    analyze_vars(body, &mut analysis);
4297    let n = vt.vars.len().max(analysis.assigns.len());
4298    let mut alias = vec![false; n];
4299    for _ in 0..4 {
4300        let mut changed = false;
4301        for v in 0..n as u32 {
4302            if alias[v as usize] {
4303                continue;
4304            }
4305            if analysis.assigns.get(v as usize).copied().unwrap_or(0) != 1 {
4306                continue;
4307            }
4308            let Some(Expr::Local { var: src, .. }) = analysis.values.get(v as usize).and_then(|o| o.as_ref()) else {
4309                continue;
4310            };
4311            if *src == this_var
4312                || (*src < n as u32 && alias[*src as usize])
4313            {
4314                alias[v as usize] = true;
4315                changed = true;
4316            }
4317        }
4318        if !changed {
4319            break;
4320        }
4321    }
4322    if !alias.iter().any(|&b| b) {
4323        return;
4324    }
4325    walk_stmt_exprs(body, &mut |e| {
4326        deep_rewrite_reads(e, &mut |x| {
4327            if let Expr::Local { var, .. } = x {
4328                if (*var as usize) < n && alias[*var as usize] {
4329                    *x = Expr::This;
4330                }
4331            }
4332        });
4333    });
4334}
4335
4336pub fn fix_this0_owners(body: &mut Stmt, class_name: &str) {
4337    walk_stmt_exprs(body, &mut |e| {
4338        deep_rewrite(e, &mut |x| {
4339            if let Expr::Field { owner: Some(_), cls, name, .. } = x {
4340                if name.as_ref() == "this$0" && cls.as_ref() == class_name {
4341                    if let Expr::Field { owner, .. } = x {
4342                        *owner = None;
4343                    }
4344                }
4345            }
4346        });
4347    });
4348}
4349
4350pub fn rescue_primitive_receivers(body: &mut Stmt, vt: &VarTable, pool: &crate::DexPool) {
4351    fn prim(t: &JavaType) -> bool {
4352        match t {
4353            JavaType::Int
4354            | JavaType::Long
4355            | JavaType::Short
4356            | JavaType::Byte
4357            | JavaType::Char
4358            | JavaType::Float
4359            | JavaType::Double
4360            | JavaType::Boolean => true,
4361            // Boxed JDK finals carry no app members either (`bool19.d`
4362            // where bool19 came out java/lang/Boolean — the coroutine
4363            // Result-register conflation).
4364            JavaType::Object(n) => matches!(
4365                n.as_ref(),
4366                "java/lang/Boolean"
4367                    | "java/lang/String"
4368                    | "java/lang/Integer"
4369                    | "java/lang/Long"
4370                    | "java/lang/Short"
4371                    | "java/lang/Byte"
4372                    | "java/lang/Character"
4373                    | "java/lang/Float"
4374                    | "java/lang/Double"
4375            ),
4376            _ => false,
4377        }
4378    }
4379    walk_stmt_exprs(body, &mut |e| {
4380        deep_rewrite(e, &mut |x| {
4381            // Phase 1 (read-only): primitive-typed Local receiver?
4382            let (recv_var, recv_ty) = {
4383                let o: &Expr = match &*x {
4384                    Expr::Method { owner: Some(o), .. } => o,
4385                    Expr::Field { owner: Some(o), is_static: false, .. } => o,
4386                    _ => return,
4387                };
4388                let Expr::Local { var, ty } = o else {
4389                    return;
4390                };
4391                let vt_ty = if (*var as usize) < vt.vars.len() {
4392                    vt.var(*var).ty.erased()
4393                } else {
4394                    ty.erased()
4395                };
4396                if !prim(&vt_ty) {
4397                    return;
4398                }
4399                (*var, vt_ty)
4400            };
4401            let cls: std::sync::Arc<str> = match &*x {
4402                Expr::Method { cls, .. } | Expr::Field { cls, .. } => cls.clone(),
4403                _ => return,
4404            };
4405            // Type-consistent call: the receiver's own type IS the
4406            // member owner (or a subtype) — `str.hashCode()` on a
4407            // String param needs no rescue. Without this the boxed-JDK
4408            // prim() arm fired on every String member call and the
4409            // max-id fallback below hijacked the reads to the highest
4410            // String-typed var in the method — often a case-local
4411            // (lark LynxUIBaseInput str11: 37 reads rewritten to a
4412            // switch-case-scoped def, 找不到符号 ×37; x0/c$a18 str4).
4413            if let JavaType::Object(n) = &recv_ty {
4414                if n.as_ref() == cls.as_ref() || pool.is_subtype(n.as_ref(), cls.as_ref()) {
4415                    return;
4416                }
4417            }
4418            // Phase 2: vars of the owner type (exact or subtype).
4419            let mut cands: Vec<u32> = Vec::new();
4420            for v in &vt.vars {
4421                if let JavaType::Object(n) = v.ty.erased() {
4422                    if n.as_ref() == cls.as_ref()
4423                        || pool.is_subtype(n.as_ref(), cls.as_ref())
4424                    {
4425                        cands.push(v.id);
4426                    }
4427                }
4428            }
4429            let id = if cands.len() == 1 {
4430                cands[0]
4431            } else if cands.len() > 1 {
4432                // Multiple owner-typed vars: prefer the same-SLOT one
4433                // (register lineage), latest generation wins — the
4434                // receiver position is scope-insensitive (any method-
4435                // scope var renders, the decl hoist covers it).
4436                let slot = if (recv_var as usize) < vt.vars.len() {
4437                    Some(vt.var(recv_var).slot)
4438                } else {
4439                    None
4440                };
4441                let mut hits: Vec<u32> = match slot {
4442                    Some(sl) => cands
4443                        .iter()
4444                        .copied()
4445                        .filter(|&c| {
4446                            c != recv_var
4447                                && (c as usize) < vt.vars.len()
4448                                && vt.var(c).slot == sl
4449                        })
4450                        .collect(),
4451                    None => Vec::new(),
4452                };
4453                if hits.is_empty() {
4454                    // No same-slot twin: fall back to the latest
4455                    // owner-typed generation (max id).
4456                    hits = cands
4457                        .iter()
4458                        .copied()
4459                        .filter(|&c| c != recv_var)
4460                        .collect();
4461                }
4462                hits.sort_unstable();
4463                hits.dedup();
4464                if hits.is_empty() {
4465                    return;
4466                }
4467                *hits.last().unwrap()
4468            } else {
4469                // Lineage disambiguation by SLOT: register reuse makes
4470                // the Boolean/primitive twin and the object version of
4471                // one dex register share VarInfo.slot (ry0/h2's
4472                // `bool19.d` — the v1-typed twin rides the same
4473                // register). Multiple slot matches stay untouched
4474                // (deterministic conservatism).
4475                if cands.is_empty() {
4476                    return;
4477                }
4478                let slot = if (recv_var as usize) < vt.vars.len() {
4479                    vt.var(recv_var).slot
4480                } else {
4481                    return;
4482                };
4483                let mut hits: Vec<u32> = cands
4484                    .into_iter()
4485                    .filter(|&c| {
4486                        c != recv_var
4487                            && (c as usize) < vt.vars.len()
4488                            && vt.var(c).slot == slot
4489                    })
4490                    .collect();
4491                hits.sort_unstable();
4492                hits.dedup();
4493                if hits.len() != 1 {
4494                    return;
4495                }
4496                hits[0]
4497            };
4498            // Phase 3: swap.
4499            let o: &mut Box<Expr> = match x {
4500                Expr::Method { owner: Some(o), .. } => o,
4501                Expr::Field { owner: Some(o), is_static: false, .. } => o,
4502                _ => return,
4503            };
4504            if let Expr::Local { var, ty } = &mut **o {
4505                if *var == recv_var {
4506                    *var = id;
4507                    *ty = vt.vars[id as usize].ty.clone();
4508                }
4509            }
4510        });
4511    });
4512
4513}
4514
4515/// Scope-safe arg/return swaps for mistyped generations — runs AFTER
4516/// ensure_declared hoists every declaration to the method top, so the
4517/// top_ids visibility precondition holds by construction (the r134
4518/// falsification: swapping in a block-scoped var broke lexically).
4519/// Same-slot + type-match + uniqueness gates as the receiver rescue.
4520pub fn rescue_arg_return_swaps(
4521    body: &mut Stmt,
4522    vt: &VarTable,
4523    pool: &crate::DexPool,
4524    ret_ty: &JavaType,
4525) {
4526    // SCOPE-SAFE arg/return swap: a primitive-typed actual against a
4527    // specific-reference formal (or a reference-returning `return`) is
4528    // a mistyped generation — the r134 falsification showed the swap
4529    // target MUST be lexically visible at the use site, so candidates
4530    // are restricted to params + TOP-LEVEL declared locals (a hoisted
4531    // decl is visible to javac name resolution everywhere in the
4532    // method) AND same-slot (same dex register lineage) AND unique.
4533    let mut top_ids: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
4534    for v in &vt.vars {
4535        if v.is_param {
4536            top_ids.insert(v.id);
4537        }
4538    }
4539    if let Stmt::Block(stmts) = body {
4540        for st in stmts.iter() {
4541            if let Stmt::LocalDef { var, .. } = st {
4542                top_ids.insert(*var);
4543            }
4544        }
4545    }
4546    let ret_wants_ref = match ret_ty {
4547        JavaType::Object(n) => n.as_ref() != "java/lang/Object",
4548        JavaType::Array(_) => true,
4549        _ => false,
4550    };
4551    let try_swap = |a: &mut Expr, want: &JavaType| {
4552        let Expr::Local { var, ty } = &mut *a else {
4553            return;
4554        };
4555        if (*var as usize) >= vt.vars.len() {
4556            return;
4557        }
4558        let avt = vt.var(*var).ty.erased();
4559        let avt_ref = matches!(avt, JavaType::Object(_) | JavaType::Array(_));
4560        let want_arr = matches!(want, JavaType::Array(_));
4561        // Only swap a primitive-typed actual (an Object actual gets the
4562        // descriptor-exact cast instead — never both).
4563        if avt_ref && !(avt == JavaType::Object("java/lang/Object".into())) {
4564            return;
4565        }
4566        if avt_ref && !matches!(want, JavaType::Object(_)) {
4567            return;
4568        }
4569        let slot = vt.var(*var).slot;
4570        let mut hits: Vec<u32> = Vec::new();
4571        for v in &vt.vars {
4572            if v.slot != slot || !top_ids.contains(&v.id) || v.id == *var {
4573                continue;
4574            }
4575            let t = v.ty.erased();
4576            let ok = match (&t, want) {
4577                (JavaType::Object(n), JavaType::Object(w)) => {
4578                    n.as_ref() == w.as_ref()
4579                        || pool.is_subtype(n.as_ref(), w.as_ref())
4580                }
4581                (JavaType::Array(_), JavaType::Array(_)) => want_arr,
4582                _ => false,
4583            };
4584            if ok {
4585                hits.push(v.id);
4586            }
4587        }
4588        hits.sort_unstable();
4589        hits.dedup();
4590        if hits.is_empty() {
4591            return;
4592        }
4593        // Multiple same-slot candidates: the LATEST generation (max id —
4594        // split mints in walk order ≈ program order) is the closest
4595        // approximation of the register's value at the use site. A
4596        // wrong pick still compiles (type-matched by construction);
4597        // leaving the primitive actual never does.
4598        let id = *hits.last().unwrap();
4599        *var = id;
4600        *ty = vt.vars[id as usize].ty.clone();
4601    };
4602    walk_stmt_exprs(body, &mut |e| {
4603        deep_rewrite(e, &mut |x| {
4604            match x {
4605                Expr::Method { desc, args, is_dynamic: false, .. } => {
4606                    for (i, a) in args.iter_mut().enumerate() {
4607                        if let Some(f) = desc.args.get(i) {
4608                            if matches!(f, JavaType::Object(n) if n.as_ref() != "java/lang/Object")
4609                                || matches!(f, JavaType::Array(_))
4610                            {
4611                                try_swap(a, f);
4612                            }
4613                        }
4614                    }
4615                }
4616                Expr::New { arg_tys, args, .. } => {
4617                    for (i, a) in args.iter_mut().enumerate() {
4618                        if let Some(f) = arg_tys.get(i) {
4619                            if matches!(f, JavaType::Object(n) if n.as_ref() != "java/lang/Object")
4620                                || matches!(f, JavaType::Array(_))
4621                            {
4622                                try_swap(a, f);
4623                            }
4624                        }
4625                    }
4626                }
4627                _ => {}
4628            }
4629        });
4630    });
4631    if ret_wants_ref {
4632        walk_mut_deep(body, &mut |st| {
4633            if let Stmt::Return(Some(e)) = st {
4634                try_swap(e, ret_ty);
4635            }
4636        });
4637    }
4638}
4639
4640/// Reference-array initializers take null, not 0: dex fill-array-data
4641/// over a String[]/Object[] slot carries the zero word, and the lifted
4642/// `new String[] {0}` is int无法转换为String (qs0/b family). A Const
4643/// Int(0) element of a reference-typed array init is the null ref.
4644/// `refVar = const 0` (dex `const/4 vN, 0` into a reference register) is
4645/// the NULL sentinel, not an int. By the time this runs infer_types has
4646/// typed the var a reference (its `.close()`/`.length`/method reads
4647/// proved it), so retype the const to Null BEFORE split_generations —
4648/// otherwise the ref/primitive mismatch splits off an int generation and
4649/// orphans the reference reads (u7.l try-with-resources finally
4650/// `v16_g3 = 0; if (v16_g3 != 0) v16_g3.close();` → "无法取消引用int").
4651/// `fix_incomparable_equality` already retypes the `refVar != 0`
4652/// COMPARISON side (but runs after the split); this covers the ASSIGN
4653/// side, which is what triggers the bad split.
4654pub fn fix_ref_null_assigns(vt: &VarTable, body: &mut Stmt) {
4655    let is_ref_var = |var: u32| -> bool {
4656        (var as usize) < vt.vars.len()
4657            && matches!(
4658                vt.vars[var as usize].ty.erased(),
4659                JavaType::Object(_) | JavaType::Array(_)
4660            )
4661    };
4662    walk_mut_deep(body, &mut |st| match st {
4663        Stmt::ExprStmt(Expr::Assign {
4664            target,
4665            value,
4666            op: AssignOp::Plain,
4667            ..
4668        }) => {
4669            if let Expr::Local { var, .. } = &**target {
4670                if is_ref_var(*var) && matches!(&**value, Expr::Const(ConstVal::Int(0))) {
4671                    **value = Expr::Const(ConstVal::Null);
4672                }
4673            }
4674        }
4675        Stmt::LocalDef {
4676            var,
4677            init: Some(e),
4678            ..
4679        } => {
4680            if is_ref_var(*var) && matches!(e, Expr::Const(ConstVal::Int(0))) {
4681                *e = Expr::Const(ConstVal::Null);
4682            }
4683        }
4684        _ => {}
4685    });
4686}
4687
4688pub fn fix_ref_array_null_consts(body: &mut Stmt) {
4689    walk_stmt_exprs(body, &mut |e| {
4690        deep_rewrite(e, &mut |x| {
4691            if let Expr::NewArray { elem, init: Some(list), .. } = x {
4692                if matches!(elem.erased(), JavaType::Object(_) | JavaType::Array(_)) {
4693                    for slot in list.iter_mut() {
4694                        if matches!(&*slot, Expr::Const(ConstVal::Int(0))) {
4695                            *slot = Expr::Const(ConstVal::Null);
4696                        }
4697                    }
4698                }
4699            }
4700        });
4701    });
4702}
4703
4704// ---------------------------------------------------------------------------
4705// Null-sentinel repair: const-0 registers in reference-typed slots
4706// ---------------------------------------------------------------------------
4707
4708/// A dex `const/4 vN, 0` register is ambiguous between int 0 and the
4709/// null reference; the value view types the bare constant as Int.
4710/// Kotlin coroutine state machines lean on this heavily — captured
4711/// object slots are zero-initialized ahead of the label switch, then
4712/// stored into `Object` fields and passed as `Continuation` ctor args
4713/// (weixin y01/s9 q8: `new e6(s94x, v287)` "int无法转换为Continuation";
4714/// uf5/l0 `throw 0;`; wcdb RepairKit `setOnCancelListener(0)`). The
4715/// dex verifier guarantees a const-0 register reaching a reference-
4716/// typed slot holds null, so in those slots the rendering must be the
4717/// null literal.
4718///
4719/// Two rewrites, both gated on the slot's expected type being a
4720/// reference (descriptor formal types, field type, array component,
4721/// cast target, throw, method return):
4722///   1. a literal `0` in a ref slot → `null`. SKIPPED for call-arg
4723///      slots whose formal is exactly `java/lang/Object`: `f(0)` there
4724///      compiles TODAY (boxing, or an int-overload pick like
4725///      `String.valueOf(0)`), and `f(null)` could newly fail on
4726///      overload ambiguity (`valueOf(Object)` vs `valueOf(char[])`) —
4727///      a guaranteed-error slot becomes a compile, a compiling slot
4728///      must not become an error.
4729///   2. a proven null-sentinel LOCAL: an Int-typed var whose every
4730///      write is `= 0` or `= <another sentinel>` and whose every read
4731///      sits in a ref slot (or feeds another sentinel's write — a dead
4732///      chain) → `null` at each read. The same Object-formal skip
4733///      applies at arg slots. A read in ANY int/unknown context
4734///      (binop, cond test, receiver deref, selector, anon-ctor arg,
4735///      lambda capture…) disqualifies the var — register reuse across
4736///      generations keeps its int rendering.
4737///
4738/// Runs after fix_ref_null_assigns (which converts writes of REF-typed
4739/// vars) and before split_generations (sentinel writes must not mint
4740/// int generations). Writes of confirmed sentinels stay as harmless
4741/// dead int stores for drop_dead_locals.
4742pub fn fix_null_sentinels(body: &mut Stmt, vt: &VarTable, ret: &JavaType) {
4743    #[derive(Clone, Copy, PartialEq)]
4744    enum WK {
4745        Zero,
4746        Copy(u32),
4747        Other,
4748    }
4749    fn is_ref_jt(t: &JavaType) -> bool {
4750        matches!(t, JavaType::Object(_) | JavaType::Array(_))
4751    }
4752    fn cls_write(e: &Expr) -> WK {
4753        match e {
4754            Expr::Const(ConstVal::Int(0)) => WK::Zero,
4755            Expr::Local { var, .. } => WK::Copy(*var),
4756            _ => WK::Other,
4757        }
4758    }
4759    fn expr_writes(e: &Expr, writes: &mut std::collections::HashMap<u32, Vec<WK>>) {
4760        visit_exprs(e, &mut |x| match x {
4761            Expr::Assign { target, op, value } => {
4762                if let Expr::Local { var, .. } = &**target {
4763                    let k = if matches!(op, AssignOp::Plain) {
4764                        cls_write(value)
4765                    } else {
4766                        WK::Other
4767                    };
4768                    writes.entry(*var).or_default().push(k);
4769                }
4770            }
4771            Expr::PreIncDec { e: inner, .. } | Expr::PostIncDec { e: inner, .. } => {
4772                if let Expr::Local { var, .. } = &**inner {
4773                    writes.entry(*var).or_default().push(WK::Other);
4774                }
4775            }
4776            _ => {}
4777        });
4778    }
4779    fn collect_writes(
4780        s: &Stmt,
4781        writes: &mut std::collections::HashMap<u32, Vec<WK>>,
4782        has_zero: &mut bool,
4783    ) {
4784        fn note(e: &Expr, has_zero: &mut bool, writes: &mut std::collections::HashMap<u32, Vec<WK>>) {
4785            visit_exprs(e, &mut |x| {
4786                if let Expr::Const(ConstVal::Int(0)) = x {
4787                    *has_zero = true;
4788                }
4789            });
4790            expr_writes(e, writes);
4791        }
4792        match s {
4793            Stmt::Block(v) => {
4794                for x in v {
4795                    collect_writes(x, writes, has_zero);
4796                }
4797            }
4798            Stmt::LocalDef { var, init, .. } => {
4799                if let Some(e) = init {
4800                    if matches!(e, Expr::Const(ConstVal::Int(0))) {
4801                        *has_zero = true;
4802                    }
4803                    writes.entry(*var).or_default().push(cls_write(e));
4804                }
4805            }
4806            Stmt::ExprStmt(e) => note(e, has_zero, writes),
4807            Stmt::Return(e) => {
4808                if let Some(x) = e {
4809                    note(x, has_zero, writes);
4810                }
4811            }
4812            Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
4813                note(e, has_zero, writes);
4814            }
4815            Stmt::TernaryValue { e } => note(e, has_zero, writes),
4816            Stmt::If { cond, then_stmt, else_stmt } => {
4817                note(cond, has_zero, writes);
4818                collect_writes(then_stmt, writes, has_zero);
4819                if let Some(b) = else_stmt {
4820                    collect_writes(b, writes, has_zero);
4821                }
4822            }
4823            Stmt::While { cond, body } | Stmt::DoWhile { body, cond } => {
4824                note(cond, has_zero, writes);
4825                collect_writes(body, writes, has_zero);
4826            }
4827            Stmt::For { init, cond, update, body } => {
4828                for x in init {
4829                    collect_writes(x, writes, has_zero);
4830                }
4831                if let Some(c) = cond {
4832                    note(c, has_zero, writes);
4833                }
4834                for u in update {
4835                    note(u, has_zero, writes);
4836                }
4837                collect_writes(body, writes, has_zero);
4838            }
4839            Stmt::ForEach { var, iterable, body, .. } => {
4840                writes.entry(*var).or_default().push(WK::Other);
4841                note(iterable, has_zero, writes);
4842                collect_writes(body, writes, has_zero);
4843            }
4844            Stmt::Switch { selector, cases, default, .. } => {
4845                note(selector, has_zero, writes);
4846                for c in cases {
4847                    if let Some(g) = &c.guard {
4848                        note(g, has_zero, writes);
4849                    }
4850                    for x in &c.body {
4851                        collect_writes(x, writes, has_zero);
4852                    }
4853                }
4854                if let Some(d) = default {
4855                    collect_writes(d, writes, has_zero);
4856                }
4857            }
4858            Stmt::Try { body, catches, finally } => {
4859                collect_writes(body, writes, has_zero);
4860                for c in catches {
4861                    writes.entry(c.var).or_default().push(WK::Other);
4862                    collect_writes(&c.body, writes, has_zero);
4863                }
4864                if let Some(f) = finally {
4865                    collect_writes(f, writes, has_zero);
4866                }
4867            }
4868            Stmt::TryWithResources { resources, body, catches, finally, .. } => {
4869                for r in resources {
4870                    collect_writes(r, writes, has_zero);
4871                }
4872                collect_writes(body, writes, has_zero);
4873                for c in catches {
4874                    writes.entry(c.var).or_default().push(WK::Other);
4875                    collect_writes(&c.body, writes, has_zero);
4876                }
4877                if let Some(f) = finally {
4878                    collect_writes(f, writes, has_zero);
4879                }
4880            }
4881            Stmt::Assert { cond, msg } => {
4882                note(cond, has_zero, writes);
4883                if let Some(m) = msg {
4884                    note(m, has_zero, writes);
4885                }
4886            }
4887            Stmt::Synchronized { lock, body } => {
4888                note(lock, has_zero, writes);
4889                collect_writes(body, writes, has_zero);
4890            }
4891            Stmt::Labeled { body, .. } => collect_writes(body, writes, has_zero),
4892            _ => {}
4893        }
4894    }
4895
4896    let mut writes: std::collections::HashMap<u32, Vec<WK>> =
4897        std::collections::HashMap::new();
4898    let mut has_zero = false;
4899    collect_writes(body, &mut writes, &mut has_zero);
4900
4901    // Write-proven eligibility fixpoint (copy chains: `v = u`).
4902    let mut eligible: HashSet<u32> = HashSet::default();
4903    let int_typed = |v: u32| {
4904        (v as usize) < vt.vars.len() && vt.vars[v as usize].ty.erased() == JavaType::Int
4905    };
4906    loop {
4907        let mut changed = false;
4908        for (&v, ws) in writes.iter() {
4909            if eligible.contains(&v) || ws.is_empty() || !int_typed(v) {
4910                continue;
4911            }
4912            if ws.iter().all(|k| match k {
4913                WK::Zero => true,
4914                WK::Copy(u) => *u != v && eligible.contains(u),
4915                WK::Other => false,
4916            }) {
4917                eligible.insert(v);
4918                changed = true;
4919            }
4920        }
4921        if !changed {
4922            break;
4923        }
4924    }
4925    if eligible.is_empty() && !has_zero {
4926        return; // nothing this pass can do
4927    }
4928
4929    #[derive(Clone, Copy, PartialEq)]
4930    enum Mode {
4931        Scan,
4932        Rewrite,
4933    }
4934    struct St<'a> {
4935        mode: Mode,
4936        vt: &'a VarTable,
4937        eligible: &'a HashSet<u32>,
4938        ref_read: HashSet<u32>,
4939        dead: HashSet<u32>,
4940        sentinels: HashSet<u32>,
4941        n_lit: usize,
4942        n_var: usize,
4943    }
4944    // A call-arg slot whose formal is exactly java/lang/Object keeps
4945    // its current rendering (see the fn doc — boxing compiles today,
4946    // null could newly ambiguize).
4947    fn arg_ctx(t: &JavaType) -> Option<JavaType> {
4948        match t {
4949            JavaType::Object(n) if n.as_ref() == "java/lang/Object" => None,
4950            t if is_ref_jt(t) => Some(t.clone()),
4951            _ => None,
4952        }
4953    }
4954    fn walk_expr(e: &mut Expr, ctx: Option<JavaType>, st: &mut St) {
4955        walk_expr_c(e, ctx, false, st);
4956    }
4957    /// `cast_arg`: the slot is a call/ctor ARGUMENT — a rewritten null
4958    /// gets an explicit formal-type cast. Bare `f(null)` re-triggers
4959    /// overload ambiguity where `f(0)` used to pick an int overload
4960    /// (`in(null)` "引用不明确", `ContentValues.put(String,null)` vs its
4961    /// 9 overloads); `(String) null` is unambiguous and jadx-shaped.
4962    fn walk_expr_c(e: &mut Expr, ctx: Option<JavaType>, cast_arg: bool, st: &mut St) {
4963        let slot_ref = ctx.as_ref().is_some_and(is_ref_jt);
4964        fn mk_null(ctx: &Option<JavaType>, cast_arg: bool) -> Expr {
4965            match (cast_arg, ctx) {
4966                (true, Some(t)) => Expr::Cast {
4967                    ty: TypeRef::J(t.clone()),
4968                    e: Box::new(Expr::Const(ConstVal::Null)),
4969                },
4970                _ => Expr::Const(ConstVal::Null),
4971            }
4972        }
4973        match e {
4974            Expr::Const(ConstVal::Int(0)) if slot_ref => {
4975                if st.mode == Mode::Rewrite {
4976                    *e = mk_null(&ctx, cast_arg);
4977                    st.n_lit += 1;
4978                }
4979                return;
4980            }
4981            Expr::Local { var, .. } => {
4982                if st.eligible.contains(var) {
4983                    if slot_ref {
4984                        st.ref_read.insert(*var);
4985                    } else {
4986                        st.dead.insert(*var);
4987                    }
4988                }
4989                if st.mode == Mode::Rewrite && slot_ref && st.sentinels.contains(var) {
4990                    *e = mk_null(&ctx, cast_arg);
4991                    st.n_var += 1;
4992                }
4993                return;
4994            }
4995            _ => {}
4996        }
4997        match e {
4998            Expr::Method { owner, desc, args, .. } => {
4999                if let Some(o) = owner {
5000                    walk_expr(o, None, st);
5001                }
5002                for (i, a) in args.iter_mut().enumerate() {
5003                    let c = desc.args.get(i).and_then(arg_ctx);
5004                    walk_expr_c(a, c, true, st);
5005                }
5006            }
5007            Expr::Invokedynamic { desc, args, .. } => {
5008                for (i, a) in args.iter_mut().enumerate() {
5009                    let c = desc.args.get(i).and_then(arg_ctx);
5010                    walk_expr_c(a, c, true, st);
5011                }
5012            }
5013            Expr::New { args, arg_tys, .. } => {
5014                for (i, a) in args.iter_mut().enumerate() {
5015                    let c = arg_tys.get(i).and_then(arg_ctx);
5016                    walk_expr_c(a, c, true, st);
5017                }
5018            }
5019            Expr::AnonNew { args, .. } => {
5020                for a in args.iter_mut() {
5021                    walk_expr(a, None, st);
5022                }
5023            }
5024            Expr::Assign { target, value, .. } => {
5025                // Sentinel-to-sentinel copy: the whole store is dead;
5026                // its RHS is by the write gate a const-0 or a bare
5027                // local, so no nested reads can hide in it.
5028                if let Expr::Local { var: u, .. } = &**target {
5029                    if st.eligible.contains(u) {
5030                        return;
5031                    }
5032                }
5033                let vctx = match &**target {
5034                    Expr::Field { ty, .. } => {
5035                        let t = ty.erased();
5036                        is_ref_jt(&t).then_some(t)
5037                    }
5038                    Expr::ArrayIndex { array, .. } => match array.type_ref().erased() {
5039                        JavaType::Array(inner) if is_ref_jt(&inner) => Some(*inner),
5040                        _ => None,
5041                    },
5042                    _ => None,
5043                };
5044                walk_expr(target, None, st);
5045                walk_expr(value, vctx, st);
5046            }
5047            Expr::Cast { ty, e: inner } => {
5048                let t = ty.erased();
5049                let c = is_ref_jt(&t).then_some(t);
5050                walk_expr(inner, c, st);
5051            }
5052            Expr::InstanceOf { e: inner, .. } => {
5053                walk_expr(inner, Some(JavaType::Object("java/lang/Object".into())), st);
5054            }
5055            Expr::Cond { c, t, f } => {
5056                walk_expr(c, None, st);
5057                let tc = ctx.clone();
5058                walk_expr(t, tc, st);
5059                walk_expr(f, ctx, st);
5060            }
5061            Expr::NewArray { elem, dims, init, .. } => {
5062                for d in dims.iter_mut() {
5063                    walk_expr(d, None, st);
5064                }
5065                if let Some(list) = init {
5066                    let et = elem.erased();
5067                    let c = is_ref_jt(&et).then_some(et);
5068                    for slot in list.iter_mut() {
5069                        walk_expr(slot, c.clone(), st);
5070                    }
5071                }
5072            }
5073            Expr::NewMultiArray { dims, .. } => {
5074                for d in dims.iter_mut() {
5075                    walk_expr(d, None, st);
5076                }
5077            }
5078            Expr::ArrayIndex { array, index } => {
5079                walk_expr(array, None, st);
5080                walk_expr(index, None, st);
5081            }
5082            Expr::Field { owner, .. } => {
5083                if let Some(o) = owner {
5084                    walk_expr(o, None, st);
5085                }
5086            }
5087            Expr::Bin { l, r, .. } => {
5088                walk_expr(l, None, st);
5089                walk_expr(r, None, st);
5090            }
5091            Expr::Un { e: inner, .. } => walk_expr(inner, None, st),
5092            Expr::PreIncDec { e: inner, .. } | Expr::PostIncDec { e: inner, .. } => {
5093                walk_expr(inner, None, st);
5094            }
5095            Expr::StringConcat(parts) => {
5096                for p in parts.iter_mut() {
5097                    if let ConcatPart::Str(x) = p {
5098                        walk_expr(x, None, st);
5099                    }
5100                }
5101            }
5102            Expr::Lambda(l) => {
5103                for c in l.captures.iter_mut() {
5104                    walk_expr(c, None, st);
5105                }
5106            }
5107            _ => {}
5108        }
5109    }
5110    fn walk_stmt(s: &mut Stmt, st: &mut St, ret: &JavaType) {
5111        match s {
5112            Stmt::Block(v) => {
5113                for x in v.iter_mut() {
5114                    walk_stmt(x, st, ret);
5115                }
5116            }
5117            Stmt::ExprStmt(e) => walk_expr(e, None, st),
5118            Stmt::LocalDef { var, init, .. } => {
5119                if let Some(e) = init {
5120                    let ctx = if (*var as usize) < st.vt.vars.len() {
5121                        let t = st.vt.vars[*var as usize].ty.erased();
5122                        is_ref_jt(&t).then_some(t)
5123                    } else {
5124                        None
5125                    };
5126                    walk_expr(e, ctx, st);
5127                }
5128            }
5129            Stmt::Return(e) => {
5130                if let Some(x) = e {
5131                    let c = is_ref_jt(ret).then(|| ret.clone());
5132                    walk_expr(x, c, st);
5133                }
5134            }
5135            Stmt::Throw(e) => {
5136                walk_expr(e, Some(JavaType::Object("java/lang/Throwable".into())), st);
5137            }
5138            Stmt::If { cond, then_stmt, else_stmt } => {
5139                walk_expr(cond, None, st);
5140                walk_stmt(then_stmt, st, ret);
5141                if let Some(b) = else_stmt {
5142                    walk_stmt(b, st, ret);
5143                }
5144            }
5145            Stmt::While { cond, body } => {
5146                walk_expr(cond, None, st);
5147                walk_stmt(body, st, ret);
5148            }
5149            Stmt::DoWhile { body, cond } => {
5150                walk_stmt(body, st, ret);
5151                walk_expr(cond, None, st);
5152            }
5153            Stmt::For { init, cond, update, body } => {
5154                for x in init.iter_mut() {
5155                    walk_stmt(x, st, ret);
5156                }
5157                if let Some(c) = cond {
5158                    walk_expr(c, None, st);
5159                }
5160                for u in update.iter_mut() {
5161                    walk_expr(u, None, st);
5162                }
5163                walk_stmt(body, st, ret);
5164            }
5165            Stmt::ForEach { iterable, body, .. } => {
5166                walk_expr(iterable, None, st);
5167                walk_stmt(body, st, ret);
5168            }
5169            Stmt::Switch { selector, cases, default, .. } => {
5170                walk_expr(selector, None, st);
5171                for c in cases.iter_mut() {
5172                    if let Some(g) = &mut c.guard {
5173                        walk_expr(g, None, st);
5174                    }
5175                    for x in c.body.iter_mut() {
5176                        walk_stmt(x, st, ret);
5177                    }
5178                }
5179                if let Some(d) = default {
5180                    walk_stmt(d, st, ret);
5181                }
5182            }
5183            Stmt::Try { body, catches, finally } => {
5184                walk_stmt(body, st, ret);
5185                for c in catches.iter_mut() {
5186                    walk_stmt(&mut c.body, st, ret);
5187                }
5188                if let Some(f) = finally {
5189                    walk_stmt(f, st, ret);
5190                }
5191            }
5192            Stmt::TryWithResources { resources, body, catches, finally, .. } => {
5193                for r in resources.iter_mut() {
5194                    walk_stmt(r, st, ret);
5195                }
5196                walk_stmt(body, st, ret);
5197                for c in catches.iter_mut() {
5198                    walk_stmt(&mut c.body, st, ret);
5199                }
5200                if let Some(f) = finally {
5201                    walk_stmt(f, st, ret);
5202                }
5203            }
5204            Stmt::Assert { cond, msg } => {
5205                walk_expr(cond, None, st);
5206                if let Some(m) = msg {
5207                    walk_expr(m, None, st);
5208                }
5209            }
5210            Stmt::Synchronized { lock, body } => {
5211                walk_expr(lock, Some(JavaType::Object("java/lang/Object".into())), st);
5212                walk_stmt(body, st, ret);
5213            }
5214            Stmt::TernaryValue { e } => walk_expr(e, None, st),
5215            Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
5216                walk_expr(e, Some(JavaType::Object("java/lang/Object".into())), st);
5217            }
5218            Stmt::Labeled { body, .. } => walk_stmt(body, st, ret),
5219            _ => {}
5220        }
5221    }
5222
5223    let mut st = St {
5224        mode: Mode::Scan,
5225        vt,
5226        eligible: &eligible,
5227        ref_read: HashSet::default(),
5228        dead: HashSet::default(),
5229        sentinels: HashSet::default(),
5230        n_lit: 0,
5231        n_var: 0,
5232    };
5233    if !eligible.is_empty() {
5234        walk_stmt(body, &mut st, ret);
5235        st.sentinels = eligible
5236            .iter()
5237            .copied()
5238            .filter(|v| st.ref_read.contains(v) && !st.dead.contains(v))
5239            .collect();
5240    }
5241    if st.sentinels.is_empty() && !has_zero {
5242        return;
5243    }
5244    st.mode = Mode::Rewrite;
5245    walk_stmt(body, &mut st, ret);
5246    if std::env::var_os("DDC_SENT").is_some() {
5247        eprintln!(
5248            "[sent] elig={} confirmed={} lit={} var={}",
5249            eligible.len(),
5250            st.sentinels.len(),
5251            st.n_lit,
5252            st.n_var
5253        );
5254    }
5255}
5256
5257/// Int-context operand bridge: a BOOLEAN-typed side of an int-kind Bin
5258/// (bitwise, arithmetic, comparison against a numeric) becomes
5259/// `(b ? 1 : 0)` — the dex-level truth (booleans ARE 0/1 ints there;
5260/// register reuse puts a bool-typed var into an int expression:
5261/// `v5x | 4`, `b == 0` — "boolean无法转换为int" / "二元运算符操作数
5262/// 类型错误" families). Runs AFTER fix_bool_xor so boolean-SINK chains
5263/// were already converted to all-boolean form and are skipped here.
5264pub fn fix_int_operand_bridges(body: &mut Stmt, vt: &VarTable) {
5265    fn side_bool(e: &Expr, vt: &VarTable) -> bool {
5266        matches!(
5267            match e {
5268                Expr::Local { var, .. } => vt.var(*var).ty.erased(),
5269                other => other.type_ref().erased(),
5270            },
5271            JavaType::Boolean
5272        )
5273    }
5274    fn side_int(e: &Expr, vt: &VarTable) -> bool {
5275        matches!(
5276            match e {
5277                Expr::Local { var, .. } => vt.var(*var).ty.erased(),
5278                other => other.type_ref().erased(),
5279            },
5280            JavaType::Int | JavaType::Short | JavaType::Byte | JavaType::Char | JavaType::Long
5281        )
5282    }
5283    fn wrap(e: Box<Expr>) -> Box<Expr> {
5284        Box::new(Expr::Cond {
5285            c: e,
5286            t: Box::new(Expr::Const(ConstVal::Int(1))),
5287            f: Box::new(Expr::Const(ConstVal::Int(0))),
5288        })
5289    }
5290    walk_stmt_exprs(body, &mut |e| {
5291        deep_rewrite(e, &mut |x| {
5292            // Field / array-store positions: the target's type is
5293            // descriptor-backed (authoritative); a boolean value into
5294            // an int slot bridges `(b ? 1 : 0)` (with a narrowing cast
5295            // for byte/short/char slots), an int value into a boolean
5296            // slot bridges `v != 0` (weixin `this.L[v3x] = max3`,
5297            // `g2x.d = p1x` — 785 field + ~700 array lines).
5298            if let Expr::Assign { target, value, op: AssignOp::Plain } = x {
5299                let tgt_ty: Option<JavaType> = match &**target {
5300                    Expr::Field { ty, .. } => Some(ty.erased()),
5301                    Expr::ArrayIndex { array, .. } => {
5302                        match array.type_ref().erased() {
5303                            JavaType::Array(el) => Some(el.as_ref().clone()),
5304                            _ => None,
5305                        }
5306                    }
5307                    // Local targets: the split/booleanize machinery owns
5308                    // the mixed-kind register cases; the residue that
5309                    // reached render (boolean无法转换为byte/long, ~360
5310                    // lines) gets the same value-side bridge.
5311                    Expr::Local { var, .. } => {
5312                        if (*var as usize) < vt.vars.len() {
5313                            Some(vt.var(*var).ty.erased())
5314                        } else {
5315                            None
5316                        }
5317                    }
5318                    _ => None,
5319                };
5320                if let Some(t) = tgt_ty {
5321                    let tgt_int = matches!(
5322                        t,
5323                        JavaType::Int
5324                            | JavaType::Short
5325                            | JavaType::Byte
5326                            | JavaType::Char
5327                            | JavaType::Long
5328                    );
5329                    if tgt_int && side_bool(value, vt) && !side_int(value, vt) {
5330                        let taken = std::mem::replace(
5331                            value,
5332                            Box::new(Expr::Const(ConstVal::Null)),
5333                        );
5334                        let mut wrapped = wrap(taken);
5335                        if !matches!(t, JavaType::Int | JavaType::Long) {
5336                            wrapped = Box::new(Expr::Cast {
5337                                ty: TypeRef::J(t.clone()),
5338                                e: wrapped,
5339                            });
5340                        }
5341                        *value = wrapped;
5342                    } else if matches!(t, JavaType::Boolean)
5343                        && side_int(value, vt)
5344                        && !side_bool(value, vt)
5345                    {
5346                        let taken = std::mem::replace(
5347                            value,
5348                            Box::new(Expr::Const(ConstVal::Null)),
5349                        );
5350                        **value = Expr::Bin {
5351                            op: BinOp::Ne,
5352                            l: taken,
5353                            r: Box::new(Expr::Const(ConstVal::Int(0))),
5354                            ty: Some(TypeRef::J(JavaType::Boolean)),
5355                        };
5356                    }
5357                }
5358            }
5359            // Explicit casts across the bool/num line are invalid Java
5360            // in BOTH directions: `(byte) this.r` (dex int-to-byte over
5361            // a boolean-rendered field — weixin AccInfo writeByte ×207)
5362            // becomes `(byte)(r ? 1 : 0)`, and `(boolean) x` over an
5363            // int becomes `x != 0`.
5364            if let Expr::Cast { ty, e } = x {
5365                let t = ty.erased();
5366                let t_num = matches!(
5367                    t,
5368                    JavaType::Int
5369                        | JavaType::Short
5370                        | JavaType::Byte
5371                        | JavaType::Char
5372                        | JavaType::Long
5373                        | JavaType::Float
5374                        | JavaType::Double
5375                );
5376                if t_num && side_bool(e, vt) && !side_int(e, vt) {
5377                    let taken = std::mem::replace(e, Box::new(Expr::Const(ConstVal::Null)));
5378                    **e = Expr::Cond {
5379                        c: taken,
5380                        t: Box::new(Expr::Const(ConstVal::Int(1))),
5381                        f: Box::new(Expr::Const(ConstVal::Int(0))),
5382                    };
5383                } else if matches!(t, JavaType::Boolean)
5384                    && side_int(e, vt)
5385                    && !side_bool(e, vt)
5386                {
5387                    let taken = std::mem::replace(e, Box::new(Expr::Const(ConstVal::Null)));
5388                    *x = Expr::Bin {
5389                        op: BinOp::Ne,
5390                        l: taken,
5391                        r: Box::new(Expr::Const(ConstVal::Int(0))),
5392                        ty: Some(TypeRef::J(JavaType::Boolean)),
5393                    };
5394                }
5395            }
5396            // Null-vs-int comparison: dex if-eqz/if-nez is type-agnostic
5397            // and null IS zero — when the local came out int-typed (phi
5398            // confluence residue), rewrite the Null constant to 0
5399            // (`v95_g12 != null` → `v95_g12 != 0`, faithful and legal;
5400            // yq0/g1 546-line 二元运算符 family).
5401            if let Expr::Bin { op, l, r, .. } = x {
5402                if matches!(op, BinOp::Eq | BinOp::Ne) {
5403                    if matches!(&**r, Expr::Const(ConstVal::Null))
5404                        && side_int(l, vt)
5405                        && !side_bool(l, vt)
5406                    {
5407                        **r = Expr::Const(ConstVal::Int(0));
5408                    } else if matches!(&**l, Expr::Const(ConstVal::Null))
5409                        && side_int(r, vt)
5410                        && !side_bool(r, vt)
5411                    {
5412                        **l = Expr::Const(ConstVal::Int(0));
5413                    }
5414                }
5415            }
5416            // A boolean-typed array INDEX is a reused register holding
5417            // an int (`this.L[v3x]` — boolean无法转换为int at the index
5418            // position): bridge it in reads and writes alike.
5419            if let Expr::ArrayIndex { index, .. } = x {
5420                if side_bool(index, vt) && !side_int(index, vt) {
5421                    let taken =
5422                        std::mem::replace(index, Box::new(Expr::Const(ConstVal::Null)));
5423                    *index = wrap(taken);
5424                }
5425            }
5426            // A boolean-typed array DIMENSION is the same reused-register
5427            // case at the allocation site (`new Object[v200]` where v200
5428            // is boolean-typed — weixin AppBrandRuntime, bool→int family):
5429            // dex new-array reads the size register as int and booleans
5430            // ARE 0/1 ints there, so bridge `(b ? 1 : 0)`.
5431            if let Expr::NewArray { dims, .. } = x {
5432                for d in dims.iter_mut() {
5433                    if side_bool(d, vt) && !side_int(d, vt) {
5434                        let taken =
5435                            std::mem::replace(d, Expr::Const(ConstVal::Null));
5436                        *d = *wrap(Box::new(taken));
5437                    }
5438                }
5439            }
5440            if let Expr::Bin { op, l, r, .. } = x {
5441                let int_kind = matches!(
5442                    op,
5443                    BinOp::Or
5444                        | BinOp::And
5445                        | BinOp::Xor
5446                        | BinOp::Add
5447                        | BinOp::Sub
5448                        | BinOp::Mul
5449                        | BinOp::Div
5450                        | BinOp::Rem
5451                        | BinOp::Shl
5452                        | BinOp::Shr
5453                        | BinOp::Ushr
5454                        | BinOp::Eq
5455                        | BinOp::Ne
5456                        | BinOp::Lt
5457                        | BinOp::Ge
5458                        | BinOp::Gt
5459                        | BinOp::Le
5460                );
5461                if !int_kind {
5462                    return;
5463                }
5464                // Comparisons only bridge against a NUMERIC constant or
5465                // int-typed side (a bool == bool compare is valid Java).
5466                if side_bool(l, vt) && side_int(r, vt) && !side_bool(r, vt) {
5467                    let taken = std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
5468                    *l = wrap(taken);
5469                } else if side_bool(r, vt) && side_int(l, vt) && !side_bool(l, vt) {
5470                    let taken = std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
5471                    *r = wrap(taken);
5472                }
5473            }
5474        });
5475    });
5476}
5477
5478/// Statement-level idiom restoration (readability parity with
5479/// jadx/DAD, flagged by the androguard comparison): `v = v + 1` →
5480/// `v++`, `v = v op x` → `v op= x`. Local targets only — a compound
5481/// field/array target would change how often the receiver expression
5482/// evaluates. Var must be the LEFT operand (non-commutative ops;
5483/// string-concat order). The ±1 fold requires a numeric vt type
5484/// (`s++` on a String is invalid while `s += 1` is not).
5485/// Boolean VALUE-diamond fold (DAD short_circuit value forms):
5486/// ```text
5487///   v = false; if (c) { v = true; }   →  v = c
5488///   v = true;  if (c) { v = false; }  →  v = !c
5489///   v = false; if (c) { v = E; }      →  v = c && E     (E bool-valued)
5490///   v = true;  if (c) { v = E; }      →  v = !c || E
5491///   v = D; if (c) { v = E1; } else { v = E2; }  →  v = c ? E1 : E2
5492/// ```
5493/// `v` must be Boolean-typed; the arm value bool-valued; the guard may
5494/// not touch `v`; base and arm constants must not coincide (a no-op
5495/// diamond could drop guard side effects). Kotlin default-arg bridge
5496/// ctors compute defaulted booleans in exactly this shape INSIDE the
5497/// branch ahead of the delegation (lark MmCreateAudioRequest's v21x
5498/// chain) — split_branch rejects the control flow as Dirty and the
5499/// this() call stays in the branch ("对this的调用必须是构造器中的第一
5500/// 个语句", lark ×456). Folded to a linear def (interleaved with
5501/// forward_single_use for the `[u = call; v = u]` arm shape) the
5502/// merge_at ternary fold applies. Source-faithful: the original was
5503/// `x?.m() ?: false` — one expression; the inlined call stays under
5504/// the guard arm, preserving evaluation order and short-circuiting.
5505/// General value-diamond fold (fold_bool_value_diamonds for ANY type):
5506/// `if (c) { v = a; } else { v = b; }` → `v = c ? a : b;`, plus the
5507/// split-pair shape `v = b; if (c) { v = a; }` → the same ternary. The
5508/// Kotlin default-arg ctor's mask-guarded per-arg defaults arrive as
5509/// diamonds (DocFreshInfo v10x/v14x); folding gives each carrier a
5510/// SINGLE-assignment def so the chain-aware delegation inline can hoist
5511/// the this() first ("对this的调用必须是构造器中的第一个语句"). Fold
5512/// only when both sides are exactly one plain assignment to the SAME
5513/// local and the value types are ternary-compatible (equal erased or
5514/// both numeric — mixed-category ternaries box/surprise).
5515pub fn fold_value_diamonds(body: &mut Stmt, vt: &VarTable) {
5516    let Stmt::Block(stmts) = body else { return };
5517    fold_diamonds_in(stmts, vt);
5518}
5519
5520fn diamond_types_ok(a: &Expr, b: &Expr) -> bool {
5521    fn num(t: &JavaType) -> bool {
5522        matches!(
5523            t,
5524            JavaType::Byte
5525                | JavaType::Short
5526                | JavaType::Int
5527                | JavaType::Char
5528                | JavaType::Long
5529                | JavaType::Float
5530                | JavaType::Double
5531        )
5532    }
5533    // A null branch is compatible with any REFERENCE other branch: the
5534    // ternary types as lub(T, null) = T. Kotlin default-args ctors
5535    // render null defaults (`if ((mask&2)==0) v=x; else v=null;`) —
5536    // rejecting the pair left the diamond unfolded, the defs
5537    // un-inlineable, and the this(...) delegation unhoistable (weibo
5538    // ctor-not-first robust-guard/second-delegation ×818). A primitive
5539    // sibling stays rejected (`c ? 5 : null` is not Java).
5540    if let Some(other) = if matches!(a, Expr::Const(ConstVal::Null)) {
5541        Some(b)
5542    } else if matches!(b, Expr::Const(ConstVal::Null)) {
5543        Some(a)
5544    } else {
5545        None
5546    } {
5547        return !num(&other.type_ref().erased());
5548    }
5549    let ta = a.type_ref().erased();
5550    let tb = b.type_ref().erased();
5551    ta == tb || (num(&ta) && num(&tb))
5552}
5553
5554/// `(var, target clone, value)` when the stmt is exactly ONE plain
5555/// assign to a local (bare or block-wrapped). The unwrap must be
5556/// RECURSIVE: the Kotlin default-args ctor diamonds arrive as
5557/// `Block([Block([Assign])])` (structurer nesting), and the one-level
5558/// peel missed them — the unfolded `if ((mask&2)==0) v=p; else v=0;`
5559/// prelude then blocked fix_ctor_super_first from hoisting the
5560/// `this(...)` delegation (weibo/lark ctor-not-first ×841 family).
5561fn single_local_assign(st: &Stmt) -> Option<(u32, Expr, Expr)> {
5562    let mut inner = st;
5563    while let Stmt::Block(v) = inner {
5564        if v.len() != 1 {
5565            return None;
5566        }
5567        inner = &v[0];
5568    }
5569    match inner {
5570        Stmt::ExprStmt(Expr::Assign { target, value, op: AssignOp::Plain }) => {
5571            match &**target {
5572                Expr::Local { var, .. } => Some((*var, *target.clone(), *value.clone())),
5573                _ => None,
5574            }
5575        }
5576        _ => None,
5577    }
5578}
5579
5580/// Like `single_local_assign`, but ALSO accepts an arm whose value is
5581/// produced by an arm-local def: `Block([LocalDef vX = init, v = vX])`
5582/// (possibly Block-wrapped) inlines `init` into the returned value when
5583/// `vX` is read exactly once there (single evaluation preserved). The
5584/// Kotlin mask-ctor long defaults render this shape (`else { long u =
5585/// Color.getUnspecified(); v37 = u; }` — compose TextStyle/SpanStyle);
5586/// without the inline the diamond stays unfolded and blocks the whole
5587/// delegation-inlining chain downstream.
5588fn arm_assign_inlined(st: &Stmt) -> Option<(u32, Expr, Expr)> {
5589    if let Some(r) = single_local_assign(st) {
5590        return Some(r);
5591    }
5592    let mut inner = st;
5593    while let Stmt::Block(v) = inner {
5594        if v.len() == 1 {
5595            inner = &v[0];
5596        } else {
5597            break;
5598        }
5599    }
5600    let Stmt::Block(v) = inner else { return None };
5601    if v.len() != 2 {
5602        return None;
5603    }
5604    let Stmt::LocalDef {
5605        var: x,
5606        init: Some(init),
5607        ..
5608    } = &v[0]
5609    else {
5610        return None;
5611    };
5612    let (var, tgt, val) = single_local_assign(&v[1])?;
5613    let mut count = 0usize;
5614    let mut c = val.clone();
5615    deep_rewrite(&mut c, &mut |e| {
5616        if let Expr::Local { var: lv, .. } = e {
5617            if lv == x {
5618                count += 1;
5619            }
5620        }
5621    });
5622    if count != 1 {
5623        return None;
5624    }
5625    let mut out = val.clone();
5626    deep_rewrite(&mut out, &mut |e| {
5627        if let Expr::Local { var: lv, .. } = e {
5628            if lv == x {
5629                *e = init.clone();
5630            }
5631        }
5632    });
5633    Some((var, tgt, out))
5634}
5635
5636fn mk_ternary_assign(tgt: Expr, c: Expr, a: Expr, b: Expr) -> Stmt {
5637    Stmt::ExprStmt(Expr::Assign {
5638        target: Box::new(tgt),
5639        op: AssignOp::Plain,
5640        value: Box::new(Expr::Cond {
5641            c: Box::new(c),
5642            t: Box::new(a),
5643            f: Box::new(b),
5644        }),
5645    })
5646}
5647
5648/// Two var ids that render as the SAME Java local: identical base name
5649/// (generation suffixes only appear on collisions, and same-base
5650/// generations of one dex register share the rendered name) and
5651/// identical erased type (a bool/num split pair must not merge).
5652fn same_render_var(v1: u32, v2: u32, vt: &VarTable) -> bool {
5653    let (a, b) = (vt.vars.get(v1 as usize), vt.vars.get(v2 as usize));
5654    match (a, b) {
5655        (Some(x), Some(y)) => x.name == y.name && x.ty.erased() == y.ty.erased(),
5656        _ => false,
5657    }
5658}
5659
5660/// Structural equality of two statement lists MODULO SSA generations:
5661/// each arm of a diamond writes its OWN generation of the rendered
5662/// locals, so strict `==` on the tails fails even when the rendered
5663/// code is identical. Canonicalize every local id to the first id seen
5664/// for its render key (name + erased type) under a SHARED map, then
5665/// compare structurally.
5666fn canon_stmts_eq(a: &[Stmt], b: &[Stmt], vt: &VarTable) -> bool {
5667    if a.len() != b.len() {
5668        return false;
5669    }
5670    let mut map: std::collections::HashMap<String, u32> = std::collections::HashMap::default();
5671    let canon = |s: &Stmt, map: &mut std::collections::HashMap<String, u32>| -> Stmt {
5672        let mut c = s.clone();
5673        walk_stmt_exprs(&mut c, &mut |e| {
5674            deep_rewrite(e, &mut |x| {
5675                if let Expr::Local { var, .. } = x {
5676                    let key = match vt.vars.get(*var as usize) {
5677                        Some(vi) => format!("{}#{:?}", vi.name, vi.ty.erased()),
5678                        None => format!("v{}", var),
5679                    };
5680                    let id = *map.entry(key).or_insert(*var);
5681                    *x = Expr::Local {
5682                        var: id,
5683                        ty: vt.var(id).ty.clone(),
5684                    };
5685                }
5686            });
5687        });
5688        c
5689    };
5690    a.iter()
5691        .zip(b.iter())
5692        .all(|(x, y)| canon(x, &mut map) == canon(y, &mut map))
5693}
5694
5695fn fold_diamonds_in(stmts: &mut Vec<Stmt>, vt: &VarTable) {
5696    let mut i = 0usize;
5697    while i < stmts.len() {
5698        match &mut stmts[i] {
5699            Stmt::Block(v) => fold_diamonds_in(v, vt),
5700            Stmt::If { then_stmt, else_stmt, .. } => {
5701                if let Stmt::Block(v) = &mut **then_stmt {
5702                    fold_diamonds_in(v, vt);
5703                }
5704                if let Some(e) = else_stmt {
5705                    if let Stmt::Block(v) = &mut **e {
5706                        fold_diamonds_in(v, vt);
5707                    }
5708                }
5709            }
5710            _ => {}
5711        }
5712        // Shape 1: standalone if/else diamond.
5713        let shape1 = if let Stmt::If { cond, then_stmt, else_stmt: Some(el), .. } = &stmts[i] {
5714            match (arm_assign_inlined(then_stmt), arm_assign_inlined(el)) {
5715                (Some((v1, tgt, a)), Some((v2, _, b)))
5716                    if v1 == v2 && diamond_types_ok(&a, &b) =>
5717                {
5718                    Some(mk_ternary_assign(tgt, cond.clone(), a, b))
5719                }
5720                _ => None,
5721            }
5722        } else {
5723            None
5724        };
5725        if let Some(f) = shape1 {
5726            stmts[i] = f;
5727            i += 1;
5728            continue;
5729        }
5730        // Shape 3: identical-delegation diamond — BOTH arms assign the
5731        // same rendered local, then run the same tail headed by a ctor
5732        // delegation (Kotlin mask-ctor last slot: `if ((m&M)==0) { v =
5733        // x; this(.., v, ..); return; } else { v = null; this(.., v,
5734        // ..); return; }` — alipay compose TextStyle/SpanStyle family).
5735        // Fold to `v = c ? x : null;` + the shared tail: the delegation
5736        // becomes unconditional (the hotfix-guard strip downstream needs
5737        // that) and fold_default_arg_bridge can then inline the prelude
5738        // carriers into the args. Tails compare modulo SSA generation
5739        // (each arm reads its own generation of the one rendered local).
5740        // Gates: tail headed by a bare delegation, tails canonically
5741        // identical, the tail writes NEITHER generation, types ok.
5742        let shape3 = if let Stmt::If {
5743            cond,
5744            then_stmt,
5745            else_stmt: Some(el),
5746            ..
5747        } = &stmts[i]
5748        {
5749            let arm = |st: &Stmt| -> Option<((u32, Expr, Expr), Vec<Stmt>)> {
5750                let Stmt::Block(v) = st else { return None };
5751                if v.len() < 2 {
5752                    return None;
5753                }
5754                let first = single_local_assign(&v[0])?;
5755                Some((first, v[1..].to_vec()))
5756            };
5757            match (arm(then_stmt), arm(el)) {
5758                (Some(((v1, tgt1, a), mut rest_t)), Some(((v2, _t2, b), rest_e)))
5759                    if !rest_t.is_empty()
5760                        && is_bare_ctor_call(first_leaf_stmt(&rest_t[0]))
5761                        && stmts_count_writes(&rest_t, v1) == 0
5762                        && stmts_count_writes(&rest_t, v2) == 0
5763                        && diamond_types_ok(&a, &b) =>
5764                {
5765                    if v1 == v2 || same_render_var(v1, v2, vt) {
5766                        // same rendered local: fold the assign, keep the tail
5767                        if canon_stmts_eq(&rest_t, &rest_e, vt) {
5768                            Some((mk_ternary_assign(tgt1, cond.clone(), a, b), rest_t))
5769                        } else {
5770                            None
5771                        }
5772                    } else if stmts_read_var(&rest_t, v1) == 1
5773                        && stmts_read_var(&rest_e, v2) == 1
5774                        && stmts_read_var(&rest_t, v2) == 0
5775                        && stmts_read_var(&rest_e, v1) == 0
5776                    {
5777                        // SPLIT arm-local carriers (compose Span: then
5778                        // reads v23_g3, else reads v23 — different render
5779                        // names, one read each): unify by substituting
5780                        // v2 -> v1 in the else tail and requiring EXACT
5781                        // equality, then inline `c ? A1 : A2` at the
5782                        // single v1 read of the kept tail. No ternary
5783                        // assign needed — both carriers die in the fold.
5784                        let mut unified = rest_e.clone();
5785                        for st in unified.iter_mut() {
5786                            let mut c2 = st.clone();
5787                            walk_stmt_exprs(&mut c2, &mut |e| {
5788                                deep_rewrite(e, &mut |x| {
5789                                    if let Expr::Local { var, .. } = x {
5790                                        if *var == v2 {
5791                                            *x = Expr::Local {
5792                                                var: v1,
5793                                                ty: vt.var(v1).ty.clone(),
5794                                            };
5795                                        }
5796                                    }
5797                                });
5798                            });
5799                            *st = c2;
5800                        }
5801                        if unified != rest_t {
5802                            None
5803                        } else {
5804                            let tern = Expr::Cond {
5805                                c: Box::new(cond.clone()),
5806                                t: Box::new(a.clone()),
5807                                f: Box::new(b.clone()),
5808                            };
5809                            for st in rest_t.iter_mut() {
5810                                let mut c2 = st.clone();
5811                                walk_stmt_exprs(&mut c2, &mut |e| {
5812                                    deep_rewrite(e, &mut |x| {
5813                                        if let Expr::Local { var, .. } = x {
5814                                            if *var == v1 {
5815                                                *x = tern.clone();
5816                                            }
5817                                        }
5818                                    });
5819                                });
5820                                *st = c2;
5821                            }
5822                            // the If is REPLACED by the (rewritten) tail:
5823                            // no ternary assign survives — both carriers
5824                            // die in the fold.
5825                            let first = rest_t.remove(0);
5826                            Some((first, rest_t))
5827                        }
5828                    } else {
5829                        None
5830                    }
5831                }
5832                _ => None,
5833            }
5834        } else {
5835            None
5836        };
5837        if let Some((folded, rest)) = shape3 {
5838            stmts[i] = folded;
5839            stmts.splice(i + 1..i + 1, rest);
5840            i += 1;
5841            continue;
5842        }
5843        // Shape 2: adjacent `v = b;` + `if (c) { v = a; }` (no else).
5844        // The two assigns may target DIFFERENT SSA generations of the
5845        // same rendered local (sequential conditional overwrite — the
5846        // Kotlin mask-ctor `v11x = p3x; if ((mask&4)!=0) v11x = -1L;`
5847        // prelude, lark/weixin branched-defs ctor family): same base
5848        // name + same erased type renders as one Java local, so the
5849        // fold's else arm reads generation 1 and the assign targets
5850        // generation 2 — textually `v = c ? a : v`, exactly the
5851        // observable behavior of the unfolded pair.
5852        if i + 1 < stmts.len() {
5853            // An EMPTY else block counts as no-else: the structurer
5854            // emits Some(Block([])) where the source shape had none,
5855            // and cleanup (which would drop it) runs AFTER the fold —
5856            // the strict None match missed every real-world overwrite
5857            // (zero shape-2 folds on lark mask ctors).
5858            // Shape 2: adjacent `v = b;` + conditional overwrite with
5859            // NO other effect:
5860            //   form A: `if (c) { v = a; }` (else absent/empty)  -> v = c ? a : b
5861            //   form B: `if (c) { } else { v = a; }` (then empty) -> v = c ? b : a
5862            // Form B is the FOLD-time shape of every rendered no-else
5863            // overwrite: invert_empty_thens flips then/else AFTER this
5864            // pass, so the strict `else_stmt: None, overwrite-in-then`
5865            // match never saw a real-world instance (zero shape-2 folds
5866            // on lark mask ctors). The two assigns may target DIFFERENT
5867            // SSA generations of one rendered local (same base name +
5868            // same erased type): the ternary's keep-arm then reads
5869            // generation 1 — textually `v = c ? a : v`, exactly the
5870            // observable behavior of the unfolded pair (Kotlin mask-ctor
5871            // preludes: `v11x = p3x; if ((mask&4)!=0) v11x = -1L;`).
5872            let shape2 = {
5873                let s_i = single_local_assign(&stmts[i]);
5874                if let Stmt::If {
5875                    cond,
5876                    then_stmt,
5877                    else_stmt,
5878                    ..
5879                } = &stmts[i + 1]
5880                {
5881                    let then_a = single_local_assign(then_stmt);
5882                    let else_a = else_stmt.as_deref().and_then(single_local_assign);
5883                    let then_empty =
5884                        matches!(&**then_stmt, Stmt::Block(v) if v.is_empty());
5885                    let else_empty = else_stmt
5886                        .as_deref()
5887                        .map(|e| matches!(e, Stmt::Block(v) if v.is_empty()))
5888                        .unwrap_or(true);
5889                    let try_fold = |over: Option<(u32, Expr, Expr)>,
5890                                    keep_over: bool|
5891                     -> Option<Stmt> {
5892                        let (v2, tgt2, a) = over?;
5893                        let (v1, _t1, b) = s_i.as_ref()?;
5894                        if !(*v1 == v2 || same_render_var(*v1, v2, vt)) {
5895                            return None;
5896                        }
5897                        let keep = if *v1 == v2 {
5898                            b.clone()
5899                        } else {
5900                            Expr::Local {
5901                                var: *v1,
5902                                ty: vt.var(*v1).ty.clone(),
5903                            }
5904                        };
5905                        if !diamond_types_ok(&a, &keep) {
5906                            return None;
5907                        }
5908                        if keep_over {
5909                            Some(mk_ternary_assign(tgt2, cond.clone(), keep, a))
5910                        } else {
5911                            Some(mk_ternary_assign(tgt2, cond.clone(), a, keep))
5912                        }
5913                    };
5914                    if then_a.is_some() && else_empty {
5915                        try_fold(then_a, false)
5916                    } else if else_a.is_some() && then_empty {
5917                        try_fold(else_a, true)
5918                    } else {
5919                        None
5920                    }
5921                } else {
5922                    None
5923                }
5924            };
5925            // Leading-assign variant: the branch STARTS with the
5926            // overwrite and carries a tail (Kotlin mask-ctor last slot:
5927            // `str12 = null; if ((m&32)==0) { str12 = str6; this(..,
5928            // str12); return; }` — news_article CommonData/DTO family).
5929            // Fold the assign into the ternary and leave the tail in the
5930            // branch: the tail's reads see the identical value on both
5931            // paths (the ternary assigns before the branch runs). Gate:
5932            // the tail must not WRITE the var.
5933            let mut lead_fold: Option<(Stmt, Stmt)> = None;
5934            if shape2.is_none() && i + 1 < stmts.len() {
5935                let s_i2 = single_local_assign(&stmts[i]);
5936                let leading = |st: &Stmt| -> Option<((u32, Expr, Expr), Vec<Stmt>)> {
5937                    let Stmt::Block(v) = st else { return None };
5938                    if v.len() < 2 {
5939                        return None;
5940                    }
5941                    let first = single_local_assign(&v[0])?;
5942                    Some((first, v[1..].to_vec()))
5943                };
5944                if let (Some((v1, _t1, b)), Stmt::If { cond, then_stmt, else_stmt, .. }) =
5945                    (&s_i2, &stmts[i + 1])
5946                {
5947                    let else_absent = else_stmt.is_none()
5948                        || else_stmt
5949                            .as_deref()
5950                            .is_some_and(|e| matches!(e, Stmt::Block(v) if v.is_empty()));
5951                    let then_absent = matches!(&**then_stmt, Stmt::Block(v) if v.is_empty());
5952                    let try_lead = |over: Option<((u32, Expr, Expr), Vec<Stmt>)>,
5953                                    keep_over: bool|
5954                     -> Option<(Stmt, Stmt)> {
5955                        let ((v2, tgt2, a), rest) = over?;
5956                        if !(*v1 == v2 || same_render_var(*v1, v2, vt)) {
5957                            return None;
5958                        }
5959                        if stmts_count_writes(&rest, *v1) != 0
5960                            || stmts_count_writes(&rest, v2) != 0
5961                        {
5962                            return None;
5963                        }
5964                        let keep = if *v1 == v2 {
5965                            b.clone()
5966                        } else {
5967                            Expr::Local {
5968                                var: *v1,
5969                                ty: vt.var(*v1).ty.clone(),
5970                            }
5971                        };
5972                        if !diamond_types_ok(&a, &keep) {
5973                            return None;
5974                        }
5975                        let t = if keep_over {
5976                            mk_ternary_assign(tgt2, cond.clone(), keep, a)
5977                        } else {
5978                            mk_ternary_assign(tgt2, cond.clone(), a, keep)
5979                        };
5980                        let mut new_if = stmts[i + 1].clone();
5981                        if let Stmt::If { then_stmt, else_stmt, .. } = &mut new_if {
5982                            if keep_over {
5983                                *else_stmt = Some(Box::new(Stmt::Block(rest)));
5984                            } else {
5985                                **then_stmt = Stmt::Block(rest);
5986                            }
5987                        }
5988                        Some((t, new_if))
5989                    };
5990                    if else_absent {
5991                        lead_fold = try_lead(leading(then_stmt), false);
5992                    } else if then_absent {
5993                        if let Some(e) = else_stmt {
5994                            lead_fold = try_lead(leading(e), true);
5995                        }
5996                    }
5997                }
5998            }
5999            if let Some((folded, new_if)) = lead_fold {
6000                stmts[i] = folded;
6001                stmts[i + 1] = new_if;
6002                continue;
6003            }
6004            if let Some(f) = shape2 {
6005                stmts[i] = f;
6006                stmts.remove(i + 1);
6007                continue; // re-scan the folded position
6008            }
6009        }
6010        i += 1;
6011    }
6012}
6013
6014pub fn fold_bool_value_diamonds(body: &mut Stmt, vt: &VarTable) {
6015    fn is_bool_var(v: u32, vt: &VarTable) -> bool {
6016        (v as usize) < vt.vars.len()
6017            && matches!(vt.vars[v as usize].ty.erased(), JavaType::Boolean)
6018    }
6019    fn bool_const(e: &Expr) -> Option<bool> {
6020        fn cint(e: &Expr) -> Option<i32> {
6021            match e {
6022                Expr::Const(ConstVal::Int(n)) => Some(*n),
6023                _ => None,
6024            }
6025        }
6026        match e {
6027            Expr::Const(ConstVal::Int(0)) => Some(false),
6028            Expr::Const(ConstVal::Int(1)) => Some(true),
6029            // int↔bool bridge wrappings of constants: `0 != 0` is the
6030            // fallback pipeline's rendered `false` by the time the
6031            // ctor-block fold runs.
6032            Expr::Bin { op: op @ (BinOp::Eq | BinOp::Ne), l, r, .. } => {
6033                let (a, b) = (cint(l)?, cint(r)?);
6034                let eq = a == b;
6035                Some(if matches!(op, BinOp::Eq) { eq } else { !eq })
6036            }
6037            _ => None,
6038        }
6039    }
6040    fn reads_var(e: &Expr, v: u32) -> bool {
6041        let mut hit = false;
6042        visit_exprs(e, &mut |x| {
6043            if let Expr::Local { var, .. } = x {
6044                if *var == v {
6045                    hit = true;
6046                }
6047            }
6048        });
6049        hit
6050    }
6051    /// Bool-valued expression for the composite arms (&&, ||, ?:).
6052    /// Bare 0/1 constants are handled by the caller's special cases —
6053    /// inside a composite they would render as invalid `c && 1`.
6054    fn bool_valued(e: &Expr, vt: &VarTable) -> bool {
6055        fn side(e: &Expr, vt: &VarTable) -> bool {
6056            bool_const(e).is_some() || bool_valued(e, vt)
6057        }
6058        match e {
6059            Expr::Local { var, .. } => is_bool_var(*var, vt),
6060            Expr::Method { desc, .. } => desc.ret == JavaType::Boolean,
6061            Expr::InstanceOf { .. } => true,
6062            Expr::Un { op: UnOp::Not, e: x, .. } => bool_valued(x, vt) || bool_const(x).is_some(),
6063            Expr::Bin { op, l, r, .. } => match op {
6064                BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le => true,
6065                // A logical operand may itself be a 0/1 constant
6066                // (booleanize leaves mixed forms).
6067                BinOp::LogAnd | BinOp::LogOr => {
6068                    side(l, vt) && side(r, vt)
6069                }
6070                _ => false,
6071            },
6072            Expr::Cond { t, f, .. } => side(t, vt) && side(f, vt),
6073            _ => false,
6074        }
6075    }
6076    /// A branch that is exactly one plain `v = E` with E bool-valued
6077    /// (or a 0/1 constant) and not reading v — OR the forwarding PAIR
6078    /// `[u = E; v = u]` where u is single-assigned/single-read in the
6079    /// whole method: the fold consumes the pair into `v = E` (the impure
6080    /// `u = src.m()` arm shape forward_single_use cannot inline — its
6081    /// phi-ref gate rejects values reading multi-assigned prelude vars,
6082    /// and inlining here is scope-safe because E moves exactly one
6083    /// block level out with the arm's lone def consumed).
6084    fn lone_assign(
6085        b: &Stmt,
6086        v: u32,
6087        vt: &VarTable,
6088        an: &VarAnalysis,
6089        consumed: &mut HashSet<u32>,
6090    ) -> Option<Expr> {
6091        let l = flat_list(b);
6092        if l.len() == 2 {
6093            let (u, e): (u32, &Expr) = match l[0] {
6094                Stmt::LocalDef { var, init: Some(e), .. } => (*var, e),
6095                Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => {
6096                    match &**target {
6097                        Expr::Local { var, .. } => (*var, &**value),
6098                        _ => return None,
6099                    }
6100                }
6101                _ => return None,
6102            };
6103            if u == v || consumed.contains(&u) {
6104                return None;
6105            }
6106            // Whole-method single-assign/single-read: the fold drops
6107            // u's def with the arm, so ANY other read would dangle.
6108            // (Analysis counts are per-round; folds only ever REMOVE
6109            // reads elsewhere or clone E's — whose variable reads were
6110            // already counted at their def sites — so stale counts
6111            // over-approximate and the ==1 gates stay sound.)
6112            if an.assigns.get(u as usize).copied().unwrap_or(0) != 1
6113                || an.reads.get(u as usize).copied().unwrap_or(0) != 1
6114            {
6115                return None;
6116            }
6117            let (tv, rv) = match l[1] {
6118                Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => {
6119                    match (&**target, &**value) {
6120                        (Expr::Local { var: t, .. }, Expr::Local { var: r, .. }) => (*t, *r),
6121                        _ => return None,
6122                    }
6123                }
6124                Stmt::LocalDef { var, init: Some(Expr::Local { var: r, .. }), .. } => (*var, *r),
6125                _ => return None,
6126            };
6127            if tv != v || rv != u {
6128                return None;
6129            }
6130            if reads_var(e, v) || !(bool_valued(e, vt) || bool_const(e).is_some()) {
6131                return None;
6132            }
6133            consumed.insert(u);
6134            return Some(e.clone());
6135        }
6136        if l.len() != 1 {
6137            return None;
6138        }
6139        let (target, value) = match l[0] {
6140            Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => (target, value),
6141            Stmt::LocalDef { var, init: Some(value), .. } if *var == v => {
6142                if reads_var(value, v) || !(bool_valued(value, vt) || bool_const(value).is_some()) {
6143                    return None;
6144                }
6145                return Some(value.clone());
6146            }
6147            _ => return None,
6148        };
6149        let Expr::Local { var: tv, .. } = &**target else { return None };
6150        if *tv != v || reads_var(value, v) {
6151            return None;
6152        }
6153        if !(bool_valued(value, vt) || bool_const(value).is_some()) {
6154            return None;
6155        }
6156        Some((**value).clone())
6157    }
6158    fn not(e: Expr) -> Expr {
6159        Expr::Un { op: UnOp::Not, e: Box::new(e) }
6160    }
6161    /// Peephole the int↔bool bridge artifacts the fallback pipeline
6162    /// leaves in guards: `!(x != y)` → `x == y`; `(b ? 1 : 0) == 1` →
6163    /// `b`; `!(b ? 1 : 0 == 1)` chains likewise. Purely a readability
6164    /// pass — the unsimplified forms compile.
6165    fn simplify_bool(e: Expr) -> Expr {
6166        match e {
6167            Expr::Un { op: UnOp::Not, e: inner, .. } => match *inner {
6168                // Flip first, THEN re-simplify: `!((u?1:0) != 1)` must
6169                // reach the cond01 arm as `(u?1:0) == 1` → `u` (the
6170                // flip result never re-entered simplification and the
6171                // pair arm saw a Bin where it needed the bare local).
6172                Expr::Bin { op: BinOp::Ne, l, r, ty } => {
6173                    simplify_bool(Expr::Bin { op: BinOp::Eq, l, r, ty })
6174                }
6175                Expr::Bin { op: BinOp::Eq, l, r, ty } => {
6176                    simplify_bool(Expr::Bin { op: BinOp::Ne, l, r, ty })
6177                }
6178                other => Expr::Un { op: UnOp::Not, e: Box::new(simplify_bool(other)) },
6179            },
6180            Expr::Bin { op: op @ (BinOp::Eq | BinOp::Ne), l, r, ty } => {
6181                // (c ? 1 : 0) <op> 1  →  c / !c ; <op> 0 → !c / c
6182                fn cond01(e: &Expr) -> Option<Expr> {
6183                    if let Expr::Cond { c, t, f } = e {
6184                        if matches!(&**t, Expr::Const(ConstVal::Int(1)))
6185                            && matches!(&**f, Expr::Const(ConstVal::Int(0)))
6186                        {
6187                            return Some((**c).clone());
6188                        }
6189                    }
6190                    None
6191                }
6192                let (lc, rc) = (cond01(&l), cond01(&r));
6193                match (lc, rc) {
6194                    (Some(c), None) => match (&*r, op) {
6195                        (Expr::Const(ConstVal::Int(1)), BinOp::Eq) => c,
6196                        (Expr::Const(ConstVal::Int(1)), BinOp::Ne) => not(c),
6197                        (Expr::Const(ConstVal::Int(0)), BinOp::Eq) => not(c),
6198                        (Expr::Const(ConstVal::Int(0)), BinOp::Ne) => c,
6199                        _ => Expr::Bin { op, l: Box::new(c), r, ty },
6200                    },
6201                    (None, Some(c)) => match (&*l, op) {
6202                        (Expr::Const(ConstVal::Int(1)), BinOp::Eq) => c,
6203                        (Expr::Const(ConstVal::Int(1)), BinOp::Ne) => not(c),
6204                        (Expr::Const(ConstVal::Int(0)), BinOp::Eq) => not(c),
6205                        (Expr::Const(ConstVal::Int(0)), BinOp::Ne) => c,
6206                        _ => Expr::Bin { op, l, r: Box::new(c), ty },
6207                    },
6208                    _ => Expr::Bin { op, l, r, ty },
6209                }
6210            }
6211            other => other,
6212        }
6213    }
6214    fn bin(op: BinOp, l: Expr, r: Expr) -> Expr {
6215        Expr::Bin { op, l: Box::new(l), r: Box::new(r), ty: None }
6216    }
6217
6218    for _round in 0..4 {
6219        let mut changed = false;
6220        let mut analysis = VarAnalysis {
6221            assigns: Vec::new(),
6222            reads: Vec::new(),
6223            values: Vec::new(),
6224        };
6225        analyze_vars(body, &mut analysis);
6226        let mut consumed: HashSet<u32> = HashSet::default();
6227        walk_mut_deep(body, &mut |st| {
6228            let Stmt::Block(list) = st else { return };
6229            let mut i = 0usize;
6230            while i + 1 < list.len() {
6231                let folded = try_at(list, i, vt, &is_bool_var, &bool_const, &reads_var, &lone_assign, &analysis, &mut consumed);
6232                match folded {
6233                    Some(expr) => {
6234                        let is_def = matches!(&list[i], Stmt::LocalDef { .. });
6235                        let var = match &list[i] {
6236                            Stmt::LocalDef { var, .. } => *var,
6237                            Stmt::ExprStmt(Expr::Assign { target, .. }) => {
6238                                match &**target { Expr::Local { var, .. } => *var, _ => unreachable!() }
6239                            }
6240                            _ => unreachable!(),
6241                        };
6242                        let (is_final, force_type) = match &list[i] {
6243                            Stmt::LocalDef { is_final, force_type, .. } => (*is_final, *force_type),
6244                            _ => (false, false),
6245                        };
6246                        list[i] = if is_def {
6247                            Stmt::LocalDef { var, init: Some(expr), is_final, force_type }
6248                        } else {
6249                            Stmt::ExprStmt(Expr::Assign {
6250                                target: Box::new(Expr::Local {
6251                                    var,
6252                                    ty: TypeRef::J(JavaType::Boolean),
6253                                }),
6254                                op: AssignOp::Plain,
6255                                value: Box::new(expr),
6256                            })
6257                        };
6258                        list.remove(i + 1);
6259                        changed = true;
6260                        // stay at i: a preceding base may now pair with
6261                        // nothing new, but a FOLLOWING diamond may chain.
6262                    }
6263                    None => i += 1,
6264                }
6265            }
6266        });
6267        if !changed {
6268            break;
6269        }
6270    }
6271
6272    #[allow(clippy::too_many_arguments, clippy::type_complexity)]
6273    fn try_at(
6274        list: &[Stmt],
6275        i: usize,
6276        vt: &VarTable,
6277        is_bool_var: &dyn Fn(u32, &VarTable) -> bool,
6278        bool_const: &dyn Fn(&Expr) -> Option<bool>,
6279        reads_var: &dyn Fn(&Expr, u32) -> bool,
6280        lone_assign: &dyn Fn(&Stmt, u32, &VarTable, &VarAnalysis, &mut HashSet<u32>) -> Option<Expr>,
6281        an: &VarAnalysis,
6282        consumed: &mut HashSet<u32>,
6283    ) -> Option<Expr> {
6284        // s1: `v = <0|1>` (Assign or LocalDef), v Boolean-typed.
6285        let (v, base) = match &list[i] {
6286            Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => {
6287                match &**target {
6288                    Expr::Local { var, .. } => {
6289                        // The value may be a wrapped constant (`0 != 0`
6290                        // — the fallback pipeline's rendered false).
6291                        let b = bool_const(value)?;
6292                        if !is_bool_var(*var, vt) { return None; }
6293                        (*var, b)
6294                    }
6295                    _ => return None,
6296                }
6297            }
6298            Stmt::LocalDef { var, init: Some(e), .. } => {
6299                let b = bool_const(e)?;
6300                if !is_bool_var(*var, vt) { return None; }
6301                (*var, b)
6302            }
6303            _ => return None,
6304        };
6305        let Stmt::If { cond, then_stmt, else_stmt } = &list[i + 1] else {
6306            return None;
6307        };
6308        if reads_var(cond, v) {
6309            return None;
6310        }
6311        let mut touches_v = false;
6312        visit_exprs(cond, &mut |x| {
6313            if let Expr::Assign { target, .. } = x {
6314                if let Expr::Local { var, .. } = &**target {
6315                    if *var == v { touches_v = true; }
6316                }
6317            }
6318        });
6319        if touches_v {
6320            return None;
6321        }
6322        // The structurer emits inverted diamonds (`if (c) {} else { v = E }`,
6323        // empty-then) — normalize by negating the guard.
6324        let (cond_n, then_arm, else_arm): (Expr, &Stmt, Option<&Stmt>) =
6325            if flat_list(then_stmt).is_empty() && else_stmt.is_some() {
6326                (not(cond.clone()), else_stmt.as_ref().unwrap(), None)
6327            } else {
6328                (cond.clone(), then_stmt.as_ref(), else_stmt.as_deref())
6329            };
6330        let e1 = lone_assign(then_arm, v, vt, an, consumed)?;
6331        let e2 = match else_arm {
6332            None => None,
6333            Some(b) => Some(lone_assign(b, v, vt, an, consumed)?),
6334        };
6335        let e1c = bool_const(&e1);
6336        let c = simplify_bool(cond_n);
6337        Some(match e2 {
6338            None => {
6339                if !base {
6340                    match e1c {
6341                        Some(true) => c,
6342                        Some(false) => return None, // no-op diamond: guard effects would drop
6343                        None => bin(BinOp::LogAnd, c, e1),
6344                    }
6345                } else {
6346                    match e1c {
6347                        Some(false) => not(c),
6348                        Some(true) => return None,
6349                        None => bin(BinOp::LogOr, not(c), e1),
6350                    }
6351                }
6352            }
6353            Some(e2) => {
6354                // Full diamond: both arms assign — the base is dead.
6355                if e1 == e2 {
6356                    return None; // both arms equal: folding drops guard effects
6357                }
6358                Expr::Cond { c: Box::new(c), t: Box::new(e1), f: Box::new(e2) }
6359            }
6360        })
6361    }
6362}
6363
6364pub fn idiom_compounds(body: &mut Stmt, vt: &VarTable) {
6365    walk_mut_deep(body, &mut |st| {
6366        let Stmt::ExprStmt(e) = st else { return };
6367        let Expr::Assign { target, op: AssignOp::Plain, value } = &mut *e else {
6368            return;
6369        };
6370        // Target: a local, or a bare field (static / this — no complex
6371        // receiver whose re-evaluation a compound would elide).
6372        enum Tgt {
6373            Loc(u32),
6374            Fld,
6375        }
6376        let tgt = match &**target {
6377            Expr::Local { var, .. } => Tgt::Loc(*var),
6378            Expr::Field { owner, .. }
6379                if owner.is_none()
6380                    || matches!(&**owner.as_ref().unwrap(), Expr::This) =>
6381            {
6382                Tgt::Fld
6383            }
6384            _ => return,
6385        };
6386        let Expr::Bin { op, l, r, .. } = &mut **value else {
6387            return;
6388        };
6389        // The left operand must be the SAME storage as the target.
6390        match (&tgt, &**l) {
6391            (Tgt::Loc(v), Expr::Local { var, .. }) if *var == *v => {}
6392            (Tgt::Fld, Expr::Field { cls: c2, name: n2, .. }) => {
6393                let Expr::Field { cls: c1, name: n1, .. } = &**target else {
6394                    return;
6395                };
6396                if c1 != c2 || n1 != n2 {
6397                    return;
6398                }
6399            }
6400            _ => return,
6401        }
6402        // ±1 constant on a numeric var → post-inc/dec.
6403        let one = match &**r {
6404            Expr::Const(ConstVal::Int(1)) => Some(1i64),
6405            Expr::Const(ConstVal::Int(-1)) => Some(-1),
6406            Expr::Const(ConstVal::Long(1)) => Some(1),
6407            Expr::Const(ConstVal::Long(-1)) => Some(-1),
6408            _ => None,
6409        };
6410        if *op == BinOp::Add || *op == BinOp::Sub {
6411            if let Some(mut d) = one {
6412                if *op == BinOp::Sub {
6413                    d = -d;
6414                }
6415                let tgt_ty = match &tgt {
6416                    Tgt::Loc(v) => vt.var(*v).ty.erased(),
6417                    Tgt::Fld => {
6418                        let Expr::Field { ty, .. } = &**target else {
6419                            return;
6420                        };
6421                        ty.erased()
6422                    }
6423                };
6424                let numeric = matches!(
6425                    tgt_ty,
6426                    JavaType::Int
6427                        | JavaType::Long
6428                        | JavaType::Short
6429                        | JavaType::Byte
6430                        | JavaType::Char
6431                        | JavaType::Float
6432                        | JavaType::Double
6433                );
6434                if numeric {
6435                    let wide = matches!(tgt_ty, JavaType::Long);
6436                    let inner = match &tgt {
6437                        Tgt::Loc(v) => Expr::Local {
6438                            var: *v,
6439                            ty: vt.var(*v).ty.clone(),
6440                        },
6441                        Tgt::Fld => (**target).clone(),
6442                    };
6443                    *e = Expr::PostIncDec {
6444                        e: Box::new(inner),
6445                        delta: d,
6446                        wide,
6447                    };
6448                    return;
6449                }
6450            }
6451        }
6452        let cop = match op {
6453            BinOp::Add => AssignOp::Add,
6454            BinOp::Sub => AssignOp::Sub,
6455            BinOp::Mul => AssignOp::Mul,
6456            BinOp::Div => AssignOp::Div,
6457            BinOp::Rem => AssignOp::Rem,
6458            BinOp::Shl => AssignOp::Shl,
6459            BinOp::Shr => AssignOp::Shr,
6460            BinOp::Ushr => AssignOp::Ushr,
6461            BinOp::And => AssignOp::And,
6462            BinOp::Or => AssignOp::Or,
6463            BinOp::Xor => AssignOp::Xor,
6464            _ => return,
6465        };
6466        let rhs = std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
6467        let new_target = match &tgt {
6468            Tgt::Loc(v) => Box::new(Expr::Local {
6469                var: *v,
6470                ty: vt.var(*v).ty.clone(),
6471            }),
6472            Tgt::Fld => target.clone(),
6473        };
6474        *e = Expr::Assign {
6475            target: new_target,
6476            op: cop,
6477            value: rhs,
6478        };
6479    });
6480}
6481
6482pub fn fix_bool_xor(body: &mut Stmt, vt: &VarTable, ret_bool: bool) {
6483    fn is_one(e: &Expr) -> bool {
6484        matches!(e, Expr::Const(ConstVal::Int(1)))
6485    }
6486    fn bty(e: &Expr, vt: &VarTable) -> bool {
6487        matches!(
6488            match e {
6489                Expr::Local { var, .. } => vt.var(*var).ty.erased(),
6490                other => other.type_ref().erased(),
6491            },
6492            JavaType::Boolean
6493        )
6494    }
6495    // Int-typed `x ^ 1` in a BOOLEAN sink is the compiler's `!x` over a
6496    // 0/1 slot: rewrite to `x == 0` (the booleanize return-wrap misses
6497    // Xor shapes — weibo ArraysKt `return v2 ^ 1;` against a boolean
6498    // return, "int无法转换为boolean").
6499    fn inv_of_xor(x: &Expr, vt: &VarTable) -> Option<Expr> {
6500        if let Expr::Bin { op: BinOp::Xor, l, r, .. } = x {
6501            let (int_side, one_side) = if is_one(r) {
6502                (l, r)
6503            } else if is_one(l) {
6504                (r, l)
6505            } else {
6506                return None;
6507            };
6508            let _ = one_side;
6509            if !bty(int_side, vt) {
6510                return Some(Expr::Bin {
6511                    op: BinOp::Eq,
6512                    l: int_side.clone(),
6513                    r: Box::new(Expr::Const(ConstVal::Int(0))),
6514                    ty: Some(TypeRef::J(JavaType::Boolean)),
6515                });
6516            }
6517        }
6518        None
6519    }
6520    let tgt_bool = |target: &Expr, vt: &VarTable| -> bool {
6521        match target {
6522            Expr::Local { var, .. } => matches!(vt.var(*var).ty.erased(), JavaType::Boolean),
6523            Expr::Field { ty, .. } => matches!(ty.erased(), JavaType::Boolean),
6524            _ => false,
6525        }
6526    };
6527    walk_mut_deep(body, &mut |st| match st {
6528        Stmt::Return(Some(e)) if ret_bool => {
6529            if let Some(r) = inv_of_xor(e, vt) {
6530                *e = r;
6531            }
6532        }
6533        Stmt::ExprStmt(Expr::Assign { target, value, op: AssignOp::Plain, .. }) => {
6534            if tgt_bool(target, vt) {
6535                if let Some(r) = inv_of_xor(value, vt) {
6536                    **value = r;
6537                }
6538            }
6539        }
6540        Stmt::LocalDef { var, init: Some(value), .. }
6541            if matches!(vt.var(*var).ty.erased(), JavaType::Boolean) =>
6542        {
6543            if let Some(r) = inv_of_xor(value, vt) {
6544                *value = r;
6545            }
6546        }
6547        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
6548            if let Some(r) = inv_of_xor(cond, vt) {
6549                *cond = r;
6550            }
6551        }
6552        _ => {}
6553    });
6554    fn ity(e: &Expr, vt: &VarTable) -> bool {
6555        matches!(
6556            match e {
6557                Expr::Local { var, .. } => vt.var(*var).ty.erased(),
6558                other => other.type_ref().erased(),
6559            },
6560            JavaType::Int | JavaType::Short | JavaType::Byte | JavaType::Char
6561        )
6562    }
6563    /// Mixed-kind bitwise rewrite: Kotlin's non-short-circuit
6564    /// `or`/`and` over 0/1 ints mixes generations once booleanize
6565    /// converts one side (`delete | delete2` — "boolean无法转换为int" at
6566    /// the OPERAND, weixin SQLiteDatabase ×3.3k), and `b ^ 1` is `!b`.
6567    /// The int side is a 0/1 encoding: compare it, keeping the chain
6568    /// boolean end to end. ONLY legal into a BOOLEAN sink — ungated, it
6569    /// rewrote int-sink `v6 = v5x | 4` into `v5x | 4 != 0` assigned to
6570    /// an int var (boolean无法转换为int, weixin yq5/c ×2.1k family).
6571    fn mixed_rewrite(e: &mut Expr, vt: &VarTable) {
6572        deep_rewrite(e, &mut |x| {
6573            if let Expr::Bin { op, l, r, ty } = x {
6574                match op {
6575                    BinOp::Xor => {
6576                        if is_one(r) && bty(l, vt) {
6577                            let taken =
6578                                std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
6579                            *x = Expr::Un { op: UnOp::Not, e: taken };
6580                        } else if is_one(l) && bty(r, vt) {
6581                            let taken =
6582                                std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
6583                            *x = Expr::Un { op: UnOp::Not, e: taken };
6584                        }
6585                    }
6586                    BinOp::Or | BinOp::And => {
6587                        if bty(l, vt) && ity(r, vt) {
6588                            let taken =
6589                                std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
6590                            **r = Expr::Bin {
6591                                op: BinOp::Ne,
6592                                l: taken,
6593                                r: Box::new(Expr::Const(ConstVal::Int(0))),
6594                                ty: Some(TypeRef::J(JavaType::Boolean)),
6595                            };
6596                            *ty = Some(TypeRef::J(JavaType::Boolean));
6597                        } else if ity(l, vt) && bty(r, vt) {
6598                            let taken =
6599                                std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
6600                            **l = Expr::Bin {
6601                                op: BinOp::Ne,
6602                                l: taken,
6603                                r: Box::new(Expr::Const(ConstVal::Int(0))),
6604                                ty: Some(TypeRef::J(JavaType::Boolean)),
6605                            };
6606                            *ty = Some(TypeRef::J(JavaType::Boolean));
6607                        }
6608                    }
6609                    _ => {}
6610                }
6611            }
6612        });
6613    }
6614    // Boolean sinks: bool-typed assign/def targets, boolean-method
6615    // returns, and loop/branch conditions.
6616    walk_mut_deep(body, &mut |st| match st {
6617        Stmt::Return(Some(e)) if ret_bool => mixed_rewrite(e, vt),
6618        Stmt::ExprStmt(Expr::Assign { target, value, op: AssignOp::Plain, .. }) => {
6619            if tgt_bool(target, vt) {
6620                mixed_rewrite(value, vt);
6621            }
6622        }
6623        Stmt::LocalDef { var, init: Some(e), .. }
6624            if matches!(vt.var(*var).ty.erased(), JavaType::Boolean) =>
6625        {
6626            mixed_rewrite(e, vt);
6627        }
6628        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
6629            mixed_rewrite(cond, vt);
6630        }
6631        _ => {}
6632    });
6633    // A conditional's test is a boolean sink wherever it sits (including
6634    // inside int-sink statements the walk above skips).
6635    walk_stmt_exprs(body, &mut |e| {
6636        deep_rewrite(e, &mut |x| {
6637            if let Expr::Cond { c, .. } = x {
6638                mixed_rewrite(c, vt);
6639            }
6640        });
6641    });
6642}
6643
6644/// Boolean inference: vars only ever assigned 0/1/comparisons/booleans and
6645/// read in conditions become `boolean`, with `v != 0` → `v` in conditions.
6646/// Returns the number of vars converted (0 = nothing changed — the
6647/// caller skips the post-booleanize re-split, which only has work when
6648/// a conversion exposed a mixed-kind register).
6649pub fn booleanize(vt: &mut VarTable, body: &mut Stmt, ret_bool: bool) -> usize {
6650    // Booleans propagate through local chains (`v17 = v24` where v24
6651    // itself became boolean in the first round) — iterate to a fixpoint;
6652    // the common corpus converts nothing and exits after one round.
6653    // The deref disqualifier set is structural (positions, not types) —
6654    // compute ONCE per call, not per round (12 full walks on big
6655    // methods cost weibo +60% wall).
6656    let n0 = vt.vars.len();
6657    let mut derefed = vec![false; n0];
6658    mark_derefs(body, &mut derefed);
6659    let mut total = 0usize;
6660    for _ in 0..4 {
6661        let n = booleanize_round(vt, body, ret_bool, &derefed);
6662        if n == 0 {
6663            break;
6664        }
6665        total += n;
6666    }
6667    total
6668}
6669
6670/// Mark vars used in DEREF positions (field/method receiver, array
6671/// base) — allocation-free recursion (visit_all_exprs allocates a
6672/// child Vec per statement; two callers run this per method).
6673fn mark_deref_expr(e: &Expr, derefed: &mut [bool]) {
6674    match e {
6675        Expr::Field { owner: Some(o), is_static: false, .. }
6676        | Expr::Method { owner: Some(o), .. } => {
6677            if let Expr::Local { var, .. } = &**o {
6678                if (*var as usize) < derefed.len() {
6679                    derefed[*var as usize] = true;
6680                }
6681            }
6682        }
6683        Expr::ArrayIndex { array, .. } => {
6684            if let Expr::Local { var, .. } = &**array {
6685                if (*var as usize) < derefed.len() {
6686                    derefed[*var as usize] = true;
6687                }
6688            }
6689        }
6690        _ => {}
6691    }
6692    for_each_child(e, &mut |c| mark_deref_expr(c, derefed));
6693}
6694
6695fn mark_derefs(body: &Stmt, derefed: &mut [bool]) {
6696    walk_all(body, &mut |st| {
6697        match st {
6698            Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
6699                mark_deref_expr(e, derefed);
6700            }
6701            Stmt::Return(Some(e)) => mark_deref_expr(e, derefed),
6702            Stmt::LocalDef { init: Some(e), .. } => mark_deref_expr(e, derefed),
6703            Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
6704                mark_deref_expr(cond, derefed);
6705            }
6706            Stmt::For { init, .. } => {
6707                for x in init {
6708                    mark_derefs(x, derefed);
6709                }
6710            }
6711            Stmt::Switch { selector, .. } => mark_deref_expr(selector, derefed),
6712            Stmt::ForEach { iterable, .. } => mark_deref_expr(iterable, derefed),
6713            _ => {}
6714        }
6715    });
6716}
6717
6718fn booleanize_round(
6719    vt: &mut VarTable,
6720    body: &mut Stmt,
6721    ret_bool: bool,
6722    derefed: &[bool],
6723) -> usize {
6724    let n = vt.vars.len();
6725    let mut in_cond = vec![false; n];
6726
6727    // Condition uses: `v == 0` / `v != 0` if-conditions and comparison
6728    // operands inside assigned values.
6729    walk_all(body, &mut |st| {
6730        let cond: Option<&Expr> = match st {
6731            Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
6732                Some(cond)
6733            }
6734            _ => None,
6735        };
6736        if let Some(c) = cond {
6737            if let Expr::Bin {
6738                op: BinOp::Eq | BinOp::Ne,
6739                l,
6740                r,
6741                ..
6742            } = c
6743            {
6744                for (x, other) in [(l, r), (r, l)] {
6745                    if let (Expr::Local { var, .. }, Expr::Const(ConstVal::Int(0))) =
6746                        (&**x, &**other)
6747                    {
6748                        if (*var as usize) < n {
6749                            in_cond[*var as usize] = true;
6750                        }
6751                    }
6752                }
6753            }
6754        }
6755        // A local RETURNED from a boolean method is boolean-typed even
6756        // though it never appears in a condition (`int v9 = 0/1 … return
6757        // v9;` — returning an int from `boolean check()` is a compile
6758        // error; lab package Obf.check).
6759        if ret_bool {
6760            if let Stmt::Return(Some(Expr::Local { var, .. })) = st {
6761                if (*var as usize) < n {
6762                    in_cond[*var as usize] = true;
6763                }
6764            }
6765        }
6766        // A local STORED into a boolean field is boolean-typed even though
6767        // it never appears in a condition (`b = v1` where `b` is a boolean
6768        // field, `v1` an int 0/1 local — q.java static init). The emit
6769        // layer already coerces a boolean-target assign through expr_bool,
6770        // but that renders a bare int LOCAL unchanged, so the local itself
6771        // must be booleanized (its 0/1 assigns then print false/true).
6772        if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = st {
6773            if let Expr::Field { ty, .. } = &**target {
6774                if ty.erased() == JavaType::Boolean {
6775                    if let Expr::Local { var, .. } = &**value {
6776                        if (*var as usize) < n {
6777                            in_cond[*var as usize] = true;
6778                        }
6779                    }
6780                }
6781            }
6782        }
6783        let val: Option<&Expr> = match st {
6784            Stmt::ExprStmt(Expr::Assign { value, .. }) => Some(value),
6785            // Bare expression statements (e.g. `q(v117);`) so a local
6786            // passed to a boolean parameter is caught below.
6787            Stmt::ExprStmt(e) => Some(e),
6788            Stmt::LocalDef { init: Some(e), .. } => Some(e),
6789            Stmt::Return(Some(e)) => Some(e),
6790            _ => None,
6791        };
6792        if let Some(v) = val {
6793            visit_exprs(v, &mut |x| {
6794                // A local passed as a BOOLEAN parameter is boolean-typed
6795                // (`q(v117)` where q's param is boolean, v117 an int 0/1 —
6796                // f8/b.java). The DEX passes booleans as 0/1 ints, so a
6797                // boolean param slot is authoritative for the argument's
6798                // type.
6799                if let Expr::Method { desc, args, .. } = x {
6800                    for (i, a) in args.iter().enumerate() {
6801                        if desc.args.get(i) == Some(&JavaType::Boolean) {
6802                            if let Expr::Local { var, .. } = a {
6803                                if (*var as usize) < n {
6804                                    in_cond[*var as usize] = true;
6805                                }
6806                            }
6807                        }
6808                    }
6809                }
6810                if let Expr::Bin { op, l, r, .. } = x {
6811                    if matches!(
6812                        op,
6813                        BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
6814                    ) {
6815                        for side in [l, r] {
6816                            if let Expr::Local { var, .. } = &**side {
6817                                if (*var as usize) < n {
6818                                    in_cond[*var as usize] = true;
6819                                }
6820                            }
6821                        }
6822                    }
6823                    // Kotlin/d8 merge booleans through INT bitwise ops
6824                    // (`v3 | obj instanceof g`) == 0 — the other side's
6825                    // static type being boolean makes this a boolean
6826                    // context for the local (a genuine int bitwise
6827                    // expression never has a boolean operand).
6828                    if matches!(op, BinOp::Or | BinOp::And) {
6829                        let l_bool = l.type_ref().erased() == JavaType::Boolean;
6830                        let r_bool = r.type_ref().erased() == JavaType::Boolean;
6831                        if l_bool != r_bool {
6832                            let side = if l_bool { r } else { l };
6833                            if let Expr::Local { var, .. } = &**side {
6834                                if (*var as usize) < n {
6835                                    in_cond[*var as usize] = true;
6836                                }
6837                            }
6838                        }
6839                    }
6840                }
6841            });
6842        }
6843    });
6844
6845    // A var whose every assignment value is boolean-shaped AND which is
6846    // referenced in conditions becomes boolean.
6847    // (Perf: this used to re-walk the whole statement tree ONCE PER VAR —
6848    // O(vars × stmts); on R8-merged monsters (2000+ vars, 15k statements)
6849    // that was tens of millions of visits. One pass collects the same
6850    // (any, all_bool) facts for every var.)
6851    let mut assigned_any = vec![false; n];
6852    let mut all_bool = vec![true; n];
6853    let mut edges: Vec<(usize, usize)> = Vec::new();
6854    walk_all(body, &mut |st| {
6855        let (var, value) = match st {
6856            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => match &**target {
6857                Expr::Local { var, .. } => (var, &**value),
6858                _ => return,
6859            },
6860            Stmt::LocalDef {
6861                var, init: Some(e), ..
6862            } => (var, e),
6863            _ => return,
6864        };
6865        let i = *var as usize;
6866        if i < n {
6867            assigned_any[i] = true;
6868            if !is_boolean_valued(value, vt) {
6869                all_bool[i] = false;
6870            }
6871            // Chain edges for the context closure below: `v17 = v24`
6872            // links the two locals; when the TARGET is in a boolean
6873            // context, the source inherits it (its only reader is the
6874            // boolean-shaped chain). Collected as edges because the
6875            // target's context can be discovered anywhere in the tree.
6876            if let Expr::Local { var: src, .. } = value {
6877                if (*src as usize) < n {
6878                    edges.push((i, *src as usize));
6879                }
6880            }
6881        }
6882    });
6883    // Context closure along local chains: a var in a boolean context
6884    // pushes that context through every local it is assigned from
6885    // (`return v17` — v17 = v24 — v24 = 0/1) — the statement order
6886    // cannot be relied on (the return may follow the assignment).
6887    loop {
6888        let mut changed = false;
6889        for &(tgt, src) in &edges {
6890            if in_cond[tgt] && !in_cond[src] && all_bool[src] {
6891                in_cond[src] = true;
6892                changed = true;
6893            }
6894            // FORWARD through pure copies: `v21 = v17` with v17 boolean
6895            // makes v21 boolean when ALL of v21's assignments are
6896            // bool-shaped (the loop-state save pattern — weixin u2/f's
6897            // v62 → v17 → v21 chains left the copy targets int while
6898            // the accumulator booleanized).
6899            if in_cond[src] && !in_cond[tgt] && all_bool[tgt] {
6900                in_cond[tgt] = true;
6901                changed = true;
6902            }
6903        }
6904        if !changed {
6905            break;
6906        }
6907    }
6908    let reads = count_locals_stmts(std::slice::from_ref(body));
6909    let mut boolean_vars: HashSet<u32> = HashSet::default();
6910    for i in 0..n {
6911        if vt.vars[i].is_param {
6912            continue;
6913        }
6914        if derefed[i] {
6915            continue;
6916        }
6917        if !in_cond[i] && !(reads.get(&(i as u32)).copied().unwrap_or(0) == 0) {
6918            continue;
6919        }
6920        if assigned_any[i] && all_bool[i] {
6921            boolean_vars.insert(i as u32);
6922        }
6923    }
6924    if boolean_vars.is_empty() {
6925        return 0;
6926    }
6927    for v in &boolean_vars {
6928        vt.vars[*v as usize].ty = TypeRef::J(JavaType::Boolean);
6929    }
6930    let types: Vec<&TypeRef> = vt.vars.iter().map(|v| &v.ty).collect();
6931    rewrite_exprs(body, &mut |e| {
6932        deep_rewrite(e, &mut |x| {
6933            if let Expr::Local { var, ty } = x {
6934                if let Some(want) = types.get(*var as usize) {
6935                    if ty != *want {
6936                        *ty = (*want).clone();
6937                    }
6938                }
6939            }
6940        });
6941    });
6942
6943    // Condition folding: `b != 0` → `b`, `b == 0` → `!b` (boolean vars).
6944    fold_bool_conditions(body, &boolean_vars);
6945    boolean_vars.len()
6946}
6947
6948fn is_boolean_valued(e: &Expr, vt: &VarTable) -> bool {
6949    match e {
6950        Expr::Const(ConstVal::Int(0)) | Expr::Const(ConstVal::Int(1)) => true,
6951        // Kotlin's non-short-circuit boolean `or`/`and` compiles to `|`/`&`
6952        // over boolean locals (`v15 = delete | delete2 | ..`). EITHER side
6953        // boolean suffices: `int | boolean` is illegal in source — it is
6954        // always a not-yet-booleanized side of a boolean chain (the
6955        // loop-carried accumulator `v15 = v20; v20 = v15 | delete3` can
6956        // never seed its all_bool fixpoint otherwise).
6957        Expr::Bin { op: BinOp::Or | BinOp::And, l, r, .. } => {
6958            is_boolean_valued(l, vt) || is_boolean_valued(r, vt)
6959        }
6960        // Kotlin's `!x` lowers to `x ^ 1`; at booleanize time the value
6961        // is still the Xor (fix_bool_xor runs later), so an int-typed
6962        // generation receiving it never retyped (weixin v2/i's
6963        // `int v264_g160_g7 = !v200`). BOTH sides must be bool-shaped:
6964        // `bool ^ 1` qualifies (Const 0/1 is boolean-valued above),
6965        // a genuine int `flags ^ 1` does not (flags stays Int in vt).
6966        // MUST precede the generic Bin arm (which would swallow Xor).
6967        Expr::Bin {
6968            op: BinOp::Xor,
6969            l,
6970            r,
6971            ..
6972        } => is_boolean_valued(l, vt) && is_boolean_valued(r, vt),
6973        Expr::Bin { op, .. } => matches!(
6974            op,
6975            BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
6976        ),
6977        Expr::InstanceOf { .. } => true,
6978        // The VarTable is the authority: the embedded Local ty is a
6979        // lift-time snapshot and goes STALE across booleanize rounds —
6980        // a converted copy source kept its partners int (`v150 = v131`
6981        // boolean→int, weixin ConstraintLayout ×1.4k files) because the
6982        // all_bool fixpoint consulted the stale Int.
6983        Expr::Local { var, ty } => {
6984            let t = if (*var as usize) < vt.vars.len() {
6985                vt.var(*var).ty.erased()
6986            } else {
6987                ty.erased()
6988            };
6989            t == JavaType::Boolean
6990        }
6991        Expr::Method { desc, .. } => desc.ret == JavaType::Boolean,
6992        Expr::Cond { t, f, .. } => is_boolean_valued(t, vt) && is_boolean_valued(f, vt),
6993        Expr::Un { op: UnOp::Not, .. } => true,
6994        // Field ty comes from the dex descriptor — authoritative, not a
6995        // lift snapshot (`!w1x.i` arrives here still as `w1x.i ^ 1`; the
6996        // Xor arm needs this side bool-shaped).
6997        Expr::Field { ty, .. } => ty.erased() == JavaType::Boolean,
6998        _ => false,
6999    }
7000}
7001
7002fn fold_bool_conditions(s: &mut Stmt, bv: &HashSet<u32>) {
7003    let fold = |e: &mut Expr| {
7004        deep_rewrite(e, &mut |x| {
7005            if let Expr::Bin { op, l, r, .. } = x {
7006                let (var_side, const_side) = match (&**l, &**r) {
7007                    (Expr::Local { .. }, Expr::Const(ConstVal::Int(_))) => (l, 0),
7008                    (Expr::Const(ConstVal::Int(_)), Expr::Local { .. }) => (r, 1),
7009                    _ => return,
7010                };
7011                let var = match &**var_side {
7012                    Expr::Local { var, .. } => *var,
7013                    _ => return,
7014                };
7015                if !bv.contains(&var) {
7016                    return;
7017                }
7018                let _ = const_side;
7019                match op {
7020                    BinOp::Ne => {
7021                        let inner = var_side.clone();
7022                        *x = *inner;
7023                    }
7024                    BinOp::Eq => {
7025                        let inner = var_side.clone();
7026                        *x = Expr::Un {
7027                            op: UnOp::Not,
7028                            e: Box::new(*inner),
7029                        };
7030                    }
7031                    _ => {}
7032                }
7033            }
7034        });
7035    };
7036    match s {
7037        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => fold(cond),
7038        _ => {}
7039    }
7040    walk_mut_deep(s, &mut |st| {
7041        let cond = match st {
7042            Stmt::If { cond, .. } => Some(cond),
7043            Stmt::While { cond, .. } => Some(cond),
7044            Stmt::DoWhile { cond, .. } => Some(cond),
7045            _ => None,
7046        };
7047        if let Some(c) = cond {
7048            fold(c);
7049        }
7050    });
7051}
7052
7053/// Normalize `1 == v` comparisons to `v == 1` (d8 emits const-first if-eq).
7054pub fn flip_const_compares(s: &mut Stmt) {
7055    rewrite_exprs(s, &mut |e| {
7056        deep_rewrite(e, &mut |x| {
7057            if let Expr::Bin { op, l, r, .. } = x {
7058                if matches!(
7059                    op,
7060                    BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
7061                ) {
7062                    let l_const = matches!(&**l, Expr::Const(_));
7063                    let r_var = matches!(&**r, Expr::Local { .. });
7064                    if l_const && r_var {
7065                        std::mem::swap(l, r);
7066                    }
7067                }
7068            }
7069        });
7070    });
7071}
7072
7073/// Object-typed locals compared against 0 compare against `null`.
7074pub fn null_compares(vt: &VarTable, body: &mut Stmt) {
7075    let obj_vars: HashSet<u32> = vt
7076        .vars
7077        .iter()
7078        .filter(|v| v.ty.erased().is_reference())
7079        .map(|v| v.id)
7080        .collect();
7081    rewrite_exprs(body, &mut |e| {
7082        deep_rewrite(e, &mut |x| {
7083            null_side_rewrite(x, &|v| obj_vars.contains(&v));
7084        });
7085    });
7086}
7087
7088/// Drop assignments/declarations of locals that are NEVER read anywhere
7089/// in the method. Register-machine merges commit values no Java-level
7090/// code consumes (phi residue like `printStream = check;` — not just
7091/// noise: the merge variable's inferred type need not match the value,
7092/// so these lines are often type errors as well). Side-effect-free
7093/// values drop entirely; an impure value survives as a bare expression
7094/// statement. Iterated to a fixpoint: dropping `a = b` can make `b`
7095/// unread.
7096pub fn drop_dead_locals(body: &mut Stmt) {
7097    loop {
7098        let mut reads: Vec<usize> = Vec::new();
7099        count_reads(body, &mut reads);
7100        let mut dropped = 0usize;
7101        walk_mut_deep(body, &mut |st| match st {
7102            Stmt::Block(items) => prune_dead_items(items, &reads, &mut dropped),
7103            // Switch case bodies and For inits are bare `Vec<Stmt>`, NOT
7104            // wrapped in a `Stmt::Block`, so the Block arm never sees them.
7105            // Dead phi commits land directly in case bodies (`sb4 =
7106            // compareTo;` per case): pruning only Blocks dropped the
7107            // commit's reader (in a real post-switch Block) but left the
7108            // now-dead commits, stalling the fixpoint cascade and leaking
7109            // mistyped phi residue. TryWithResources.resources is
7110            // deliberately excluded — its LocalDefs carry auto-close
7111            // semantics that a bare-expression rewrite would break.
7112            Stmt::Switch { cases, .. } => {
7113                for c in cases {
7114                    prune_dead_items(&mut c.body, &reads, &mut dropped);
7115                }
7116            }
7117            Stmt::For { init, .. } => prune_dead_items(init, &reads, &mut dropped),
7118            _ => {}
7119        });
7120        if dropped == 0 {
7121            break;
7122        }
7123    }
7124}
7125
7126/// Remove dead assignments/declarations from one bare statement vector.
7127/// Shared by `drop_dead_locals` across every prunable `Vec<Stmt>`
7128/// container. Side-effect-free values drop entirely; an impure value
7129/// survives as a bare expression statement.
7130fn prune_dead_items(items: &mut Vec<Stmt>, reads: &[usize], dropped: &mut usize) {
7131    let dead = |v: u32| reads.get(v as usize).copied().unwrap_or(0) == 0;
7132    items.retain_mut(|x| match x {
7133        Stmt::ExprStmt(Expr::Assign { target, value, op, .. }) => {
7134            let dead_target = matches!(&**target, Expr::Local { var, .. } if dead(*var));
7135            if dead_target && matches!(*op, AssignOp::Plain) {
7136                *dropped += 1;
7137                if has_side_effects(value) {
7138                    let e = std::mem::replace(value, Box::new(Expr::This));
7139                    *x = Stmt::ExprStmt(*e);
7140                    true
7141                } else {
7142                    false
7143                }
7144            } else {
7145                true
7146            }
7147        }
7148        Stmt::LocalDef { var, init, .. } if dead(*var) => {
7149            *dropped += 1;
7150            if let Some(e) = init.take() {
7151                if has_side_effects(&e) {
7152                    *x = Stmt::ExprStmt(e);
7153                    return true;
7154                }
7155            }
7156            false
7157        }
7158        _ => true,
7159    });
7160}
7161
7162
7163/// Vars occurring in this statement's OWN expression payloads (nested
7164/// sub-statements are descended separately so every occurrence carries
7165/// its innermost block id). ForEach/Catch bound vars are declarations,
7166/// not occurrences.
7167fn stmt_shallow_vars<F: FnMut(u32)>(st: &Stmt, out: &mut F) {
7168    fn ex<F: FnMut(u32)>(e: &Expr, out: &mut F) {
7169        visit_exprs(e, &mut |x| {
7170            if let Expr::Local { var, .. } = x {
7171                out(*var);
7172            }
7173        });
7174    }
7175    match st {
7176        Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
7177            ex(e, out)
7178        }
7179        Stmt::TernaryValue { e } => ex(e, out),
7180        Stmt::Return(Some(e)) => ex(e, out),
7181        Stmt::LocalDef { var, init, .. } => {
7182            out(*var);
7183            if let Some(e) = init {
7184                ex(e, out);
7185            }
7186        }
7187        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
7188            ex(cond, out)
7189        }
7190        Stmt::For { cond, update, .. } => {
7191            if let Some(c) = cond {
7192                ex(c, out);
7193            }
7194            for u in update {
7195                ex(u, out);
7196            }
7197        }
7198        Stmt::ForEach { iterable, .. } => ex(iterable, out),
7199        Stmt::Switch { selector, cases, .. } => {
7200            ex(selector, out);
7201            for cg in cases {
7202                if let Some(g) = &cg.guard {
7203                    ex(g, out);
7204                }
7205            }
7206        }
7207        Stmt::Assert { cond, msg } => {
7208            ex(cond, out);
7209            if let Some(m) = msg {
7210                ex(m, out);
7211            }
7212        }
7213        Stmt::Synchronized { lock, .. } => ex(lock, out),
7214        _ => {}
7215    }
7216}
7217
7218/// Pass A: number every Block in pre-order (root = 0) and record each
7219/// var's defining block. `sizes[id]` = the id-range span of the subtree.
7220/// Pass A: number every EMITTER-BRACED position in pre-order (root = 0)
7221/// and record each var's defining block. `sizes[id]` = the id-range span
7222/// of the subtree. A braced position is a Java scope whether or not the
7223/// IR node is a Stmt::Block — switch case bodies are bare Vecs and
7224/// single-statement if arms are bare statements, and sharing the parent's
7225/// id for them hid the scope violation (Telegram jk.java: a case-local
7226/// `notificationCenterDelegate`/`d0x` read after the switch never
7227/// hoisted — 找不到符号 ×76 in one file; the `d9x.L` reads even resolved
7228/// as a PACKAGE — 程序包不存在 ×92 tree-wide).
7229fn scope_pass_a(st: &Stmt, cur: u32, next: &mut u32, def_block: &mut [u32], sizes: &mut Vec<u32>) {
7230    if let Stmt::LocalDef { var, .. } = st {
7231        let i = *var as usize;
7232        if i < def_block.len() && def_block[i] == u32::MAX {
7233            def_block[i] = cur;
7234        }
7235    }
7236    fn scoped(c: &Stmt, next: &mut u32, def_block: &mut [u32], sizes: &mut Vec<u32>) {
7237        let id = *next;
7238        *next += 1;
7239        sizes.push(0);
7240        scope_pass_a(c, id, next, def_block, sizes);
7241        sizes[id as usize] = *next - id;
7242    }
7243    match st {
7244        Stmt::Block(v) => {
7245            for x in v {
7246                scope_pass_a(x, cur, next, def_block, sizes);
7247            }
7248        }
7249        Stmt::If {
7250            then_stmt,
7251            else_stmt,
7252            ..
7253        } => {
7254            scoped(then_stmt, next, def_block, sizes);
7255            if let Some(e) = else_stmt {
7256                scoped(e, next, def_block, sizes);
7257            }
7258        }
7259        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => {
7260            scoped(body, next, def_block, sizes)
7261        }
7262        Stmt::For { init, body, .. } => {
7263            // A for-init declaration scopes over the WHOLE for statement
7264            // (header + body): one shared id, the body a nested one.
7265            let id = *next;
7266            *next += 1;
7267            sizes.push(0);
7268            for x in init {
7269                scope_pass_a(x, id, next, def_block, sizes);
7270            }
7271            scoped(body, next, def_block, sizes);
7272            sizes[id as usize] = *next - id;
7273        }
7274        Stmt::ForEach { body, .. } => scoped(body, next, def_block, sizes),
7275        Stmt::Switch { cases, default, .. } => {
7276            for c in cases {
7277                // One scope per case group (the emitter braces each);
7278                // non-Block statements share the case id, nested Blocks
7279                // get sub-ids — siblings in one brace group must share,
7280                // or a def's later same-case reads fall "outside".
7281                let id = *next;
7282                *next += 1;
7283                sizes.push(0);
7284                for x in &c.body {
7285                    if matches!(x, Stmt::Block(_)) {
7286                        scoped(x, next, def_block, sizes);
7287                    } else {
7288                        scope_pass_a(x, id, next, def_block, sizes);
7289                    }
7290                }
7291                sizes[id as usize] = *next - id;
7292            }
7293            if let Some(d) = default {
7294                scoped(d, next, def_block, sizes);
7295            }
7296        }
7297        Stmt::Try {
7298            body,
7299            catches,
7300            finally,
7301        } => {
7302            scoped(body, next, def_block, sizes);
7303            for c in catches {
7304                scoped(&c.body, next, def_block, sizes);
7305            }
7306            if let Some(f) = finally {
7307                scoped(f, next, def_block, sizes);
7308            }
7309        }
7310        Stmt::TryWithResources {
7311            resources,
7312            body,
7313            catches,
7314            finally,
7315        } => {
7316            // Resource declarations scope over the WHOLE try statement.
7317            let id = *next;
7318            *next += 1;
7319            sizes.push(0);
7320            for r in resources {
7321                scope_pass_a(r, id, next, def_block, sizes);
7322            }
7323            scoped(body, next, def_block, sizes);
7324            for c in catches {
7325                scoped(&c.body, next, def_block, sizes);
7326            }
7327            if let Some(f) = finally {
7328                scoped(f, next, def_block, sizes);
7329            }
7330            sizes[id as usize] = *next - id;
7331        }
7332        Stmt::Synchronized { body, .. } => scoped(body, next, def_block, sizes),
7333        Stmt::Labeled { body, .. } => scope_pass_a(body, cur, next, def_block, sizes),
7334        _ => {}
7335    }
7336}
7337
7338/// Pass B (same numbering walk): count each var's occurrences overall
7339/// and within its defining block's subtree. For/TWR take dedicated arms
7340/// so their header expressions count INSIDE the statement scope (a
7341/// normal `for (int i…)` must not look like an outside read of i).
7342fn scope_pass_b(
7343    st: &Stmt,
7344    cur: u32,
7345    next: &mut u32,
7346    def_block: &[u32],
7347    sizes: &[u32],
7348    within: &mut [u32],
7349    total: &mut [u32],
7350) {
7351    fn cnt(
7352        v: u32,
7353        at: u32,
7354        def_block: &[u32],
7355        sizes: &[u32],
7356        within: &mut [u32],
7357        total: &mut [u32],
7358    ) {
7359        let i = v as usize;
7360        if i < total.len() {
7361            total[i] += 1;
7362            let d = def_block[i];
7363            if d != u32::MAX && d <= at && at < d + sizes[d as usize] {
7364                within[i] += 1;
7365            }
7366        }
7367    }
7368    fn scoped(
7369        c: &Stmt,
7370        next: &mut u32,
7371        def_block: &[u32],
7372        sizes: &[u32],
7373        within: &mut [u32],
7374        total: &mut [u32],
7375    ) {
7376        let id = *next;
7377        *next += 1;
7378        scope_pass_b(c, id, next, def_block, sizes, within, total);
7379    }
7380    fn ex_at(
7381        e: &Expr,
7382        at: u32,
7383        def_block: &[u32],
7384        sizes: &[u32],
7385        within: &mut [u32],
7386        total: &mut [u32],
7387    ) {
7388        visit_exprs(e, &mut |x| {
7389            if let Expr::Local { var, .. } = x {
7390                cnt(*var, at, def_block, sizes, within, total);
7391            }
7392        });
7393    }
7394    match st {
7395        Stmt::For {
7396            init,
7397            cond,
7398            update,
7399            body,
7400        } => {
7401            let id = *next;
7402            *next += 1;
7403            for x in init {
7404                scope_pass_b(x, id, next, def_block, sizes, within, total);
7405            }
7406            if let Some(c) = cond {
7407                ex_at(c, id, def_block, sizes, within, total);
7408            }
7409            for u in update {
7410                ex_at(u, id, def_block, sizes, within, total);
7411            }
7412            scoped(body, next, def_block, sizes, within, total);
7413        }
7414        Stmt::TryWithResources {
7415            resources,
7416            body,
7417            catches,
7418            finally,
7419        } => {
7420            let id = *next;
7421            *next += 1;
7422            for r in resources {
7423                scope_pass_b(r, id, next, def_block, sizes, within, total);
7424            }
7425            scoped(body, next, def_block, sizes, within, total);
7426            for c in catches {
7427                scoped(&c.body, next, def_block, sizes, within, total);
7428            }
7429            if let Some(f) = finally {
7430                scoped(f, next, def_block, sizes, within, total);
7431            }
7432        }
7433        _ => {
7434            stmt_shallow_vars(st, &mut |v| cnt(v, cur, def_block, sizes, within, total));
7435            match st {
7436                Stmt::Block(v) => {
7437                    for x in v {
7438                        scope_pass_b(x, cur, next, def_block, sizes, within, total);
7439                    }
7440                }
7441                Stmt::If {
7442                    then_stmt,
7443                    else_stmt,
7444                    ..
7445                } => {
7446                    scoped(then_stmt, next, def_block, sizes, within, total);
7447                    if let Some(e) = else_stmt {
7448                        scoped(e, next, def_block, sizes, within, total);
7449                    }
7450                }
7451                Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => {
7452                    scoped(body, next, def_block, sizes, within, total)
7453                }
7454                Stmt::ForEach { body, .. } => {
7455                    scoped(body, next, def_block, sizes, within, total)
7456                }
7457                Stmt::Switch { cases, default, .. } => {
7458                    for c in cases {
7459                        let id = *next;
7460                        *next += 1;
7461                        for x in &c.body {
7462                            if matches!(x, Stmt::Block(_)) {
7463                                scoped(x, next, def_block, sizes, within, total);
7464                            } else {
7465                                scope_pass_b(x, id, next, def_block, sizes, within, total);
7466                            }
7467                        }
7468                    }
7469                    if let Some(d) = default {
7470                        scoped(d, next, def_block, sizes, within, total);
7471                    }
7472                }
7473                Stmt::Try {
7474                    body,
7475                    catches,
7476                    finally,
7477                } => {
7478                    scoped(body, next, def_block, sizes, within, total);
7479                    for c in catches {
7480                        scoped(&c.body, next, def_block, sizes, within, total);
7481                    }
7482                    if let Some(f) = finally {
7483                        scoped(f, next, def_block, sizes, within, total);
7484                    }
7485                }
7486                Stmt::Synchronized { body, .. } => {
7487                    scoped(body, next, def_block, sizes, within, total)
7488                }
7489                Stmt::Labeled { body, .. } => {
7490                    scope_pass_b(body, cur, next, def_block, sizes, within, total)
7491                }
7492                _ => {}
7493            }
7494        }
7495    }
7496}
7497
7498/// Declaration hygiene: every used var that has no LocalDef gets a bare
7499/// declaration at the top; duplicate LocalDefs demote to assignments.
7500/// In an enum's `<clinit>`, every constant's assignment
7501/// (`Self.FIELD = new Self("NAME", ordinal, ...)`) is compiler-mandated
7502/// boilerplate — when the class renders as a true `enum` declaration the
7503/// constants live in the header and these assignments must go. Only
7504/// ACC_ENUM-flagged static fields of the class itself are touched; the
7505/// `$VALUES` array assignment stays (a static block in an enum is legal).
7506pub fn strip_enum_const_inits(s: &mut Stmt, class: &crate::PoolClass) {
7507    let const_fields: jdc_core::FxHashSet<&str> = class
7508        .static_fields
7509        .iter()
7510        .filter(|f| f.access & crate::access::ACC_ENUM != 0)
7511        .map(|f| f.name.as_str())
7512        .collect();
7513    if const_fields.is_empty() {
7514        return;
7515    }
7516    if let Stmt::Block(stmts) = s {
7517        stmts.retain(|st| {
7518            !matches!(
7519                st,
7520                Stmt::ExprStmt(Expr::Assign { target, value, .. })
7521                    if matches!(&**target,
7522                        Expr::Field { cls, name, is_static: true, .. }
7523                            if cls.as_ref() == class.name
7524                                && const_fields.contains(&**name))
7525                        && matches!(&**value, Expr::New { cls: ncls, .. } if ncls.as_ref() == class.name)
7526            )
7527        });
7528    }
7529}
7530
7531/// Java requires the `super(...)`/`this(...)` delegation to be the FIRST
7532/// statement of a constructor. d8/R8 order the outer-reference capture
7533/// (`this.b = p1;`) or other field writes before the invokesuper in the
7534/// bytecode, which lifts as-is into an uncompilable statement order —
7535/// hoist the bare delegation call to position 0 (jadx does the same).
7536/// Does the expression touch `this` (explicit, implicit-instance member
7537/// access, or super)? Such a def cannot be inlined into a delegation
7538/// that must run BEFORE the supertype constructor.
7539fn contains_this_access(e: &Expr) -> bool {
7540    let mut hit = false;
7541    visit_exprs(e, &mut |x| {
7542        if hit {
7543            return;
7544        }
7545        match x {
7546            Expr::This => hit = true,
7547            Expr::Field { owner, is_static: false, .. } => {
7548                if owner.is_none() || matches!(&**owner.as_ref().unwrap(), Expr::This) {
7549                    hit = true;
7550                }
7551            }
7552            Expr::Method { owner, is_static: false, .. } => {
7553                if owner.is_none() || matches!(&**owner.as_ref().unwrap(), Expr::This) {
7554                    hit = true;
7555                }
7556            }
7557            _ => {}
7558        }
7559    });
7560    hit
7561}
7562
7563/// Inline the transitive straight-line defs of a delegation call's
7564/// arg locals into the call, so hoisting it to position 0 leaves no
7565/// dangling name (lark ProtoAdapter's `super(str)` with `str` defined
7566/// BELOW the hoist point — 找不到符号 变量 str). Returns false when any
7567/// arg var has no inlineable def (param-less, missing, this-touching,
7568/// or cyclic) — the caller then leaves the call in place: a
7569/// not-first-statement error is cheaper than an unresolved name that
7570/// poisons every downstream type.
7571/// Any occurrence of `var` (read OR assign-target) in `stmts` — the
7572/// suffix-use test for dropping a consumed def: count_locals_stmts only
7573/// sees reads, and a suffix WRITE to a dropped declaration makes
7574/// ensure_declared re-add `Type v;` at the method top — BEFORE the
7575/// hoisted this()/super() ("对this的调用必须是构造器中的第一个语句",
7576/// lark +587 the read-only test caused).
7577fn var_occurs_in(stmts: &[Stmt], var: u32) -> bool {
7578    stmts.iter().any(|st| {
7579        let mut hit = false;
7580        visit_stmt_exprs_ro(st, &mut |e| {
7581            if !hit {
7582                visit_exprs(e, &mut |x| {
7583                    if let Expr::Local { var: v, .. } = x {
7584                        if *v == var {
7585                            hit = true;
7586                        }
7587                    }
7588                });
7589            }
7590        });
7591        hit
7592    })
7593}
7594
7595// ---------------------------------------------------------------------------
7596// Branched-delegation ctors → static resolver helper.
7597// ---------------------------------------------------------------------------
7598
7599/// A synthetic static method extracted from a constructor whose
7600/// delegation is preceded by computation (the Kotlin default-arg /
7601/// conditional-bridge shape). Rendered by classdec after the real
7602/// methods; the ctor becomes a single first-statement delegation whose
7603/// carrier arg is the helper call.
7604pub struct CtorHelper {
7605    pub name: String,
7606    pub ret: JavaType,
7607    pub params: Vec<(JavaType, String)>,
7608    pub body: Stmt,
7609    pub vt: VarTable,
7610}
7611
7612fn ctor_helpers() -> &'static std::sync::Mutex<std::collections::HashMap<String, Vec<CtorHelper>>> {
7613    static H: std::sync::OnceLock<std::sync::Mutex<std::collections::HashMap<String, Vec<CtorHelper>>>> =
7614        std::sync::OnceLock::new();
7615    H.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::default()))
7616}
7617
7618/// Synthetic relay ctor for the FULL mode of
7619/// extract_branched_delegation_helper: `private C(Object[] h$relay) {
7620/// this/super((T0) h$relay[0], ..); }` — the public ctor becomes
7621/// `this(resolve$X(params))`, both delegations first-statement legal.
7622pub struct CtorRelay {
7623    /// The delegation target's formal types (the casts of the unpack).
7624    pub formals: Vec<JavaType>,
7625    pub is_super: bool,
7626}
7627
7628type RelayMap = std::collections::HashMap<String, Vec<(String, CtorRelay)>>;
7629
7630fn ctor_relays() -> &'static std::sync::Mutex<RelayMap> {
7631    static H: std::sync::OnceLock<std::sync::Mutex<RelayMap>> = std::sync::OnceLock::new();
7632    H.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::default()))
7633}
7634
7635/// Take (and clear) the relay ctors registered for `class` — sorted by
7636/// the originating ctor descriptor for deterministic emission.
7637pub fn take_ctor_relays(class: &str) -> Vec<(String, CtorRelay)> {
7638    let mut m = ctor_relays().lock().unwrap_or_else(|e| e.into_inner());
7639    let mut v = m.remove(class).unwrap_or_default();
7640    v.sort_by(|a, b| a.0.cmp(&b.0));
7641    v
7642}
7643
7644fn array_init(args: Vec<Expr>) -> Expr {
7645    Expr::NewArray {
7646        elem: TypeRef::J(JavaType::Object(std::sync::Arc::from("java/lang/Object"))),
7647        dims: Vec::new(),
7648        trailing_dims: 0,
7649        init: Some(args),
7650    }
7651}
7652
7653/// Deterministic helper name: FNV-1a over (class, ctor descriptor) —
7654/// worker-thread registration order must not leak into the output.
7655fn helper_name(class: &str, desc: &str) -> String {
7656    let mut h: u64 = 0xcbf29ce484222325;
7657    for b in class.as_bytes().iter().chain(b"::").chain(desc.as_bytes()) {
7658        h ^= *b as u64;
7659        h = h.wrapping_mul(0x100000001b3);
7660    }
7661    format!("resolve${:x}", (h >> 16) & 0xffff_ffff)
7662}
7663
7664/// Drain the helpers registered for a class (render-time). Sorted by
7665/// name: registration order is worker-scheduling dependent.
7666pub fn take_ctor_helpers(class: &str) -> Vec<CtorHelper> {
7667    let mut m = ctor_helpers().lock().unwrap_or_else(|e| e.into_inner());
7668    let mut v = m.remove(class).unwrap_or_default();
7669    v.sort_by(|a, b| a.name.cmp(&b.name));
7670    v
7671}
7672
7673fn strip_delegations_deep(stmts: &mut Vec<Stmt>) {
7674    stmts.retain(|s| !is_bare_ctor_call(s));
7675    for s in stmts.iter_mut() {
7676        strip_delegation_one(s);
7677    }
7678}
7679
7680fn strip_delegation_one(st: &mut Stmt) {
7681    if is_bare_ctor_call(st) {
7682        *st = Stmt::Block(Vec::new());
7683        return;
7684    }
7685    match st {
7686        Stmt::Block(v) => strip_delegations_deep(v),
7687        Stmt::If { then_stmt, else_stmt, .. } => {
7688            strip_delegation_one(then_stmt);
7689            if let Some(e) = else_stmt {
7690                strip_delegation_one(e);
7691            }
7692        }
7693        Stmt::While { body, .. }
7694        | Stmt::DoWhile { body, .. }
7695        | Stmt::ForEach { body, .. }
7696        | Stmt::Synchronized { body, .. }
7697        | Stmt::Labeled { body, .. } => strip_delegation_one(body),
7698        Stmt::For { init, body, .. } => {
7699            strip_delegations_deep(init);
7700            strip_delegation_one(body);
7701        }
7702        Stmt::Switch { cases, default, .. } => {
7703            for c in cases.iter_mut() {
7704                strip_delegations_deep(&mut c.body);
7705            }
7706            if let Some(d) = default {
7707                strip_delegation_one(d);
7708            }
7709        }
7710        Stmt::Try { body, catches, finally } | Stmt::TryWithResources { body, catches, finally, .. } => {
7711            strip_delegation_one(body);
7712            for c in catches.iter_mut() {
7713                strip_delegation_one(&mut c.body);
7714            }
7715            if let Some(f) = finally {
7716                strip_delegation_one(f);
7717            }
7718        }
7719        _ => {}
7720    }
7721}
7722
7723#[allow(clippy::ptr_arg)]
7724fn rewrite_bare_returns(stmts: &mut Vec<Stmt>, val: &Expr) {
7725    for s in stmts.iter_mut() {
7726        rewrite_bare_returns_one(s, val);
7727    }
7728}
7729
7730fn rewrite_bare_returns_one(st: &mut Stmt, val: &Expr) {
7731    match st {
7732        Stmt::Return(None) => *st = Stmt::Return(Some(val.clone())),
7733        Stmt::Block(v) => rewrite_bare_returns(v, val),
7734        Stmt::If { then_stmt, else_stmt, .. } => {
7735            rewrite_bare_returns_one(then_stmt, val);
7736            if let Some(e) = else_stmt {
7737                rewrite_bare_returns_one(e, val);
7738            }
7739        }
7740        Stmt::While { body, .. }
7741        | Stmt::DoWhile { body, .. }
7742        | Stmt::ForEach { body, .. }
7743        | Stmt::Synchronized { body, .. }
7744        | Stmt::Labeled { body, .. } => rewrite_bare_returns_one(body, val),
7745        Stmt::For { init, body, .. } => {
7746            rewrite_bare_returns(init, val);
7747            rewrite_bare_returns_one(body, val);
7748        }
7749        Stmt::Switch { cases, default, .. } => {
7750            for c in cases.iter_mut() {
7751                rewrite_bare_returns(&mut c.body, val);
7752            }
7753            if let Some(d) = default {
7754                rewrite_bare_returns_one(d, val);
7755            }
7756        }
7757        Stmt::Try { body, catches, finally } | Stmt::TryWithResources { body, catches, finally, .. } => {
7758            rewrite_bare_returns_one(body, val);
7759            for c in catches.iter_mut() {
7760                rewrite_bare_returns_one(&mut c.body, val);
7761            }
7762            if let Some(f) = finally {
7763                rewrite_bare_returns_one(f, val);
7764            }
7765        }
7766        _ => {}
7767    }
7768}
7769
7770fn collect_def_vars(stmts: &[Stmt], out: &mut jdc_core::FxHashSet<u32>) {
7771    for st in stmts {
7772        match st {
7773            Stmt::LocalDef { var, .. } => {
7774                out.insert(*var);
7775            }
7776            Stmt::ExprStmt(Expr::Assign { target, .. }) => {
7777                if let Expr::Local { var, .. } = &**target {
7778                    out.insert(*var);
7779                }
7780            }
7781            Stmt::Block(v) => collect_def_vars(v, out),
7782            Stmt::If { then_stmt, else_stmt, .. } => {
7783                collect_def_vars(std::slice::from_ref(then_stmt.as_ref()), out);
7784                if let Some(e) = else_stmt {
7785                    collect_def_vars(std::slice::from_ref(e.as_ref()), out);
7786                }
7787            }
7788            Stmt::While { body, .. }
7789            | Stmt::DoWhile { body, .. }
7790            | Stmt::Synchronized { body, .. }
7791            | Stmt::Labeled { body, .. } => {
7792                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
7793            }
7794            Stmt::ForEach { var, body, .. } => {
7795                out.insert(*var);
7796                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
7797            }
7798            Stmt::For { init, body, .. } => {
7799                collect_def_vars(init, out);
7800                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
7801            }
7802            Stmt::Switch { cases, default, .. } => {
7803                for c in cases {
7804                    collect_def_vars(&c.body, out);
7805                }
7806                if let Some(d) = default {
7807                    collect_def_vars(std::slice::from_ref(d.as_ref()), out);
7808                }
7809            }
7810            Stmt::Try { body, catches, finally }
7811            | Stmt::TryWithResources { body, catches, finally, .. } => {
7812                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
7813                for c in catches {
7814                    out.insert(c.var);
7815                    collect_def_vars(std::slice::from_ref(c.body.as_ref()), out);
7816                }
7817                if let Some(f) = finally {
7818                    collect_def_vars(std::slice::from_ref(f.as_ref()), out);
7819                }
7820            }
7821            _ => {}
7822        }
7823    }
7824}
7825
7826/// v1 extraction is limited to Block/If/straight-line computation (the
7827/// Kotlin default-arg shape); loops/switch/try in the extraction bail.
7828fn extraction_shape_ok(stmts: &[Stmt]) -> bool {
7829    stmts.iter().all(|st| match st {
7830        Stmt::Block(v) => extraction_shape_ok(v),
7831        Stmt::LocalDef { .. }
7832        | Stmt::ExprStmt(_)
7833        | Stmt::Return(_)
7834        | Stmt::Throw(_)
7835        | Stmt::Assert { .. }
7836        | Stmt::Break(_)
7837        | Stmt::Continue(_) => true,
7838        Stmt::If { then_stmt, else_stmt, .. } => {
7839            ok_one(then_stmt)
7840                && else_stmt.as_deref().map(ok_one).unwrap_or(true)
7841        }
7842        Stmt::While { body, .. }
7843        | Stmt::DoWhile { body, .. }
7844        | Stmt::ForEach { body, .. }
7845        | Stmt::Synchronized { body, .. } => ok_one(body),
7846        Stmt::Labeled { body, .. } => ok_one(body),
7847        Stmt::For { init, body, .. } => extraction_shape_ok(init) && ok_one(body),
7848        Stmt::Switch { cases, default, .. } => {
7849            cases.iter().all(|c| extraction_shape_ok(&c.body))
7850                && default.as_deref().map(ok_one).unwrap_or(true)
7851        }
7852        Stmt::Try { body, catches, finally }
7853        | Stmt::TryWithResources { body, catches, finally, .. } => {
7854            ok_one(body)
7855                && catches.iter().all(|c| ok_one(&c.body))
7856                && finally.as_deref().map(ok_one).unwrap_or(true)
7857        }
7858        // Label/Goto (unstructured), MonitorEnter/Exit (raw), ClassDecl
7859        // (local class), TernaryValue, Raw: stay out of v2 extraction.
7860        _ => false,
7861    })
7862}
7863
7864fn ok_one(st: &Stmt) -> bool {
7865    extraction_shape_ok(std::slice::from_ref(st))
7866}
7867
7868fn static_safe(stmts: &[Stmt], this_id: Option<u32>) -> bool {
7869    let mut ok = true;
7870    for st in stmts {
7871        visit_stmt_exprs_ro(st, &mut |e| {
7872            visit_exprs(e, &mut |x| {
7873                match x {
7874                    Expr::This => ok = false,
7875                    // ddc's IR usually carries `this` as the var-0
7876                    // PARAM local, not the Expr::This variant — a
7877                    // helper body reading it dangles (the this param
7878                    // is excluded from the helper's param list) AND
7879                    // renders 无法从静态上下文中引用非静态 (weixin y9/i).
7880                    Expr::Local { var, .. } => {
7881                        if Some(*var) == this_id {
7882                            ok = false;
7883                        }
7884                    }
7885                    // implicit-this member access / call
7886                    Expr::Field { owner: None, is_static: false, .. } => ok = false,
7887                    Expr::Method { owner: None, is_static: false, .. } => ok = false,
7888                    _ => {}
7889                }
7890            });
7891        });
7892    }
7893    ok
7894}
7895
7896/// The bytedance PatchProxy hotfix guard in <clinit>:
7897/// `if (vsChange != null) { PatchProxyResult p = PatchProxy.proxy(..);
7898/// if (p.isSupported) { return; } }` — a bare `return;` inside a STATIC
7899/// INITIALIZER is illegal Java (javac: 返回外部方法; videocut ×28,029 +
7900/// news_article ×19,058 — sampled 400/400 this shape). The patch path
7901/// is dead in a fresh compile (the IVsChange field is null), so the
7902/// whole leading guard statement strips, leaving the real static inits —
7903/// the <clinit> counterpart of the Titan/Robust ctor-guard strips.
7904/// Gate: FIRST top-level stmt is an If with no else whose body holds
7905/// both a bare return and a *PatchProxy* call.
7906pub fn strip_clinit_hotfix_guard(body: &mut Stmt) {
7907    // The structurer can wrap the sequence in a nested singleton block
7908    // (the enum-clinit path flattens for the same reason); the normal
7909    // emit_method path does not.
7910    if let Stmt::Block(vs) = body {
7911        flatten_top_blocks(vs);
7912    }
7913    let Stmt::Block(stmts) = body else { return };
7914    // The guard may sit behind hoisted bare declarations (`int[] v8x;`
7915    // from ensure_declared) — find the first non-decl statement.
7916    let mut gi = 0usize;
7917    while gi < stmts.len() && matches!(&stmts[gi], Stmt::LocalDef { init: None, .. }) {
7918        gi += 1;
7919    }
7920    let Some(first) = stmts.get(gi) else { return };
7921    let Stmt::If { then_stmt, else_stmt, .. } = first else {
7922        return;
7923    };
7924    if else_stmt.is_some() {
7925        return;
7926    }
7927    let mut has_ret = false;
7928    let mut has_proxy = false;
7929    let c = then_stmt.as_ref();
7930    walk_all(c, &mut |st| {
7931        if matches!(st, Stmt::Return(None)) {
7932            has_ret = true;
7933        }
7934    });
7935    let mut cc = c.clone();
7936    walk_stmt_exprs(&mut cc, &mut |e| {
7937        deep_rewrite(e, &mut |x| {
7938            if let Expr::Method { cls, .. } = x {
7939                if cls.contains("PatchProxy") {
7940                    has_proxy = true;
7941                }
7942            }
7943        });
7944    });
7945    if has_ret && has_proxy {
7946        stmts.remove(gi);
7947    }
7948}
7949
7950/// Vendor hotfix guards in CTORS that wrap a delegation:
7951/// `if (__ != null) { Object[] v = ..; ConstructorCode proxy =
7952/// ConstructorCode.proxy(..); if (proxy != null) { this(unpack(v));
7953/// proxy.afterSuper(this); return; } }` ahead of the real top-level
7954/// delegation (alipay InstantRun — ctor-not-first ×1,436; the same
7955/// shape as the already-stripped Baidu Titan / bytedance clinit /
7956/// Meituan Robust guards). The patch path is dead in a fresh compile
7957/// (the redirect field is null), and Java cannot place the wrapped
7958/// delegation legally — strip the WHOLE guard statement.
7959///
7960/// VENDOR-GATED on purpose: a conditional delegation with a top-level
7961/// fallback (`if (c) { this(a); return; } this(b);`) is legitimate
7962/// bytecode whose guard must NOT be dropped. The gate: the guard
7963/// subtree mentions a known hotfix framework type (hotfix / instantrun
7964/// / robust / titan / PatchProxy / ChangeQuickRedirect / ConstructorCode
7965/// in any method-owner, field owner+type, cast, or new).
7966/// First LEAF statement after unwrapping nested `Stmt::Block` wrappers —
7967/// the lifted ctor body reaches the ctor chain with the delegation and
7968/// the hotfix guard each wrapped in plain Blocks (the flattening that
7969/// produces the rendered shape runs later).
7970pub(crate) fn first_leaf_stmt(s: &Stmt) -> &Stmt {
7971    match s {
7972        Stmt::Block(v) if !v.is_empty() => first_leaf_stmt(&v[0]),
7973        other => other,
7974    }
7975}
7976
7977/// Vendor hotfix guards in CTORS that wrap a delegation:
7978/// `if (__ != null) { Object[] v = ..; ConstructorCode proxy =
7979/// ConstructorCode.proxy(..); if (proxy != null) { this(unpack(v));
7980/// proxy.afterSuper(this); return; } }` ahead of the real top-level
7981/// delegation (alipay InstantRun — ctor-not-first ×1,436, nearly all in
7982/// FALLBACK ENUM ctors; the same shape as the already-stripped Baidu
7983/// Titan / bytedance clinit / Meituan Robust guards). The patch path is
7984/// dead in a fresh compile (the redirect field is null), and Java cannot
7985/// place the wrapped delegation legally — strip the WHOLE guard
7986/// statement (at this pipeline stage it arrives Block-wrapped, and its
7987/// branches are still un-inverted: `If(then=∅, else=guard-body)`).
7988///
7989/// VENDOR-GATED on purpose: a conditional delegation with a top-level
7990/// fallback (`if (c) { this(a); return; } this(b);`) is legitimate
7991/// bytecode whose guard must NOT be dropped. The gate: the guard
7992/// subtree mentions a known hotfix framework type (hotfix / instantrun
7993/// / robust / titan / PatchProxy / ChangeQuickRedirect / ConstructorCode
7994/// in any method-owner, field owner+type, cast, or new).
7995pub fn strip_ctor_hotfix_guards(body: &mut Stmt) {
7996    fn marker_hit(e: &Expr) -> bool {
7997        let has = |n: &str| {
7998            let l = n.to_ascii_lowercase();
7999            l.contains("hotfix")
8000                || l.contains("instantrun")
8001                || l.contains("robust")
8002                || l.contains("titan")
8003                || l.contains("patchproxy")
8004                || l.contains("changequickredirect")
8005                || l.contains("constructorcode")
8006        };
8007        let mut hit = false;
8008        visit_exprs(e, &mut |x| {
8009            match x {
8010                Expr::Method { cls, .. } | Expr::New { cls, .. } | Expr::Field { cls, .. } => {
8011                    if has(cls) {
8012                        hit = true;
8013                    }
8014                }
8015                _ => {}
8016            }
8017            if let Expr::Field { ty, .. } = x {
8018                if let TypeRef::J(JavaType::Object(n)) = ty {
8019                    if has(n) {
8020                        hit = true;
8021                    }
8022                }
8023            }
8024        });
8025        hit
8026    }
8027    // A top-level statement that IS a hotfix guard: unwrap single-child
8028    // Block chains down to an If whose subtree holds BOTH a delegation
8029    // and a vendor marker.
8030    fn is_guard(st: &Stmt) -> bool {
8031        let mut cur = st;
8032        loop {
8033            match cur {
8034                Stmt::Block(v) if v.len() == 1 => cur = &v[0],
8035                Stmt::If { .. } => break,
8036                _ => return false,
8037            }
8038        }
8039        let mut has_del = false;
8040        walk_all(cur, &mut |s2| {
8041            if is_bare_ctor_call(s2) {
8042                has_del = true;
8043            }
8044        });
8045        if !has_del {
8046            return false;
8047        }
8048        let mut has_marker = false;
8049        visit_stmt_exprs_ro(cur, &mut |e| {
8050            if marker_hit(e) {
8051                has_marker = true;
8052            }
8053        });
8054        has_marker
8055    }
8056    // Strip at EVERY Block level: the lifted body arrives either as
8057    // [Block(guard), Block(del, ..)] (AtomicDataCollector) or as ONE big
8058    // Block [decls, guard, prelude-computes, del] (compose TextStyle).
8059    // No outside-delegation requirement: the vendor gate already proves
8060    // the statement is dead hotfix machinery (the redirect field is null
8061    // in a fresh compile — the same assumption the Titan/PatchProxy
8062    // strips run on), and the dex's fresh path IS whatever remains after
8063    // the guard. rpc/a.java: the real super() sits inside a switch arm —
8064    // requiring an unconditional outside delegation kept the guard (and
8065    // its illegal pre-super field writes) alive.
8066    fn strip_in(stmts: &mut Vec<Stmt>) {
8067        stmts.retain(|st| !is_guard(st));
8068        for st in stmts.iter_mut() {
8069            if let Stmt::Block(v) = st {
8070                strip_in(v);
8071            }
8072        }
8073    }
8074    let Stmt::Block(stmts) = body else { return };
8075    strip_in(stmts);
8076}
8077
8078/// Repeated identical branch-site delegations with param-only args
8079/// (weixin appbrand/zc: bare `super()` at four branch ends, each
8080/// followed by field-write tails) carry no computed carrier, so the
8081/// helper extraction has nothing to return — but they need no helper:
8082/// every delegating path runs the SAME call, so hoist one copy to
8083/// position 0 and strip the sites. The per-branch tails stay in their
8084/// branches and now legally follow the first-statement delegation.
8085/// Same reordering approximation fix_ctor_super_first already ships for
8086/// the straight-line case (paths that threw BEFORE delegating now run
8087/// the delegation first — Object-super, the dominant shape, is inert).
8088/// Gate: every del deep-equal and reading params only (a computed local
8089/// would dangle at position 0 — that is the helper extractor's job).
8090pub fn hoist_branch_delegations(body: &mut Stmt, vt: &VarTable) {
8091    let Stmt::Block(stmts) = body else { return };
8092    if stmts.first().is_some_and(is_bare_ctor_call) {
8093        return; // already first
8094    }
8095    let mut dels: Vec<Expr> = Vec::new();
8096    for st in stmts.iter() {
8097        visit_stmt_exprs_ro(st, &mut |e| {
8098            if is_delegation_expr(e) {
8099                dels.push(e.clone());
8100            }
8101        });
8102    }
8103    if dels.is_empty() {
8104        return;
8105    }
8106    if !dels.iter().all(|d| *d == dels[0]) {
8107        return;
8108    }
8109    let mut bad = false;
8110    visit_exprs(&dels[0], &mut |x| {
8111        if let Expr::Local { var, .. } = x {
8112            if (*var as usize) >= vt.vars.len() || !vt.vars[*var as usize].is_param {
8113                bad = true;
8114            }
8115        }
8116    });
8117    if bad {
8118        return;
8119    }
8120    let del = dels[0].clone();
8121    strip_delegations_deep(stmts);
8122    stmts.insert(0, Stmt::ExprStmt(del));
8123}
8124
8125/// The branched-delegation ctor family ("对this的调用必须是构造器中的
8126/// 第一个语句", ~2k across corpora): R8 renders a Kotlin default-arg /
8127/// conditional-bridge ctor as computation statements (straight-line plus
8128/// mask-guarded overrides) with every control path ending in a
8129/// `this(a, b, carrier)` delegation. Java cannot express statements
8130/// before the delegation, and unlike the linear-chain case the carrier's
8131/// value is computed by multi-statement branched code that cannot fold
8132/// into a ternary. Extract the computation into a synthetic
8133/// `private static T resolve$X(<ctor params>)` and make the ctor a
8134/// single first-statement `this(a, b, resolve$X(..))` — compilable AND
8135/// faithful (jadx renders the un-compilable statements-before-this form
8136/// for this family).
8137///
8138/// Two carrier modes:
8139/// - Single (v1): every delegation textually identical and exactly one
8140///   non-param local (the carrier) read exactly once across the args.
8141/// - Position (v2): delegations differ at exactly ONE arg position k,
8142///   every site passing a bare non-param local there (per-branch
8143///   carriers, lark BaseProtocol$ReliablePushList's v22_g1/v22x). Each
8144///   stripped delegation's own carrier fills the return that follows it;
8145///   merge-point returns fall back to the primary site's carrier.
8146///
8147/// The top-level primary delegation is optional in BOTH modes — when
8148/// every path delegates inside branches (weixin ln1/b1's super() in
8149/// if/try/catch) the whole body is extracted and any del serves as the
8150/// template (all non-k parts identical).
8151///
8152/// Shared gates (all conservative, bail keeps today's behavior): not an
8153/// enum (trace-param strip shifts render params); nested only when the
8154/// class RENDERS static-nested (member-inner ctors lose the outer param
8155/// at render); computation is Block/If/straight-line only; no
8156/// `this`-dependence; no local defined in the extraction used after the
8157/// primary.
8158pub fn extract_branched_delegation_helper(
8159    body: &mut Stmt,
8160    vt: &VarTable,
8161    pool: &DexPool,
8162    class: &crate::PoolClass,
8163    desc_str: &str,
8164) {
8165    if class.is_enum() {
8166        return;
8167    }
8168    // Member-inner ctors lose the synthetic outer param at render
8169    // (qualified-new absorbs it), so the helper's param list would no
8170    // longer match the rendered ctor. STATIC nested classes render all
8171    // params like top-level ones (lark BaseProtocol$PushList family).
8172    if class.nesting.enclosing_class.is_some() && !crate::ctx::nested_is_static(pool, class) {
8173        return;
8174    }
8175    let Stmt::Block(stmts) = body else { return };
8176    let mut dels: Vec<Expr> = Vec::new();
8177    for st in stmts.iter() {
8178        visit_stmt_exprs_ro(st, &mut |e| {
8179            if is_delegation_expr(e) {
8180                dels.push(e.clone());
8181            }
8182        });
8183    }
8184    if dels.is_empty() {
8185        return;
8186    }
8187    if dels.len() == 1 && stmts.first().is_some_and(is_bare_ctor_call) {
8188        return; // already first
8189    }
8190    let template = dels[0].clone();
8191    let Expr::Method { args: t_args, .. } = &template else {
8192        return;
8193    };
8194    // Carrier discovery: Single (all dels identical, one non-param local
8195    // read once) or Position (dels differ at exactly one arg slot, every
8196    // site a bare non-param local of one erased type).
8197    enum Mode {
8198        Single(u32),
8199        Position(usize),
8200        /// Every path delegates with the SAME target signature but its
8201        /// own arg expressions: the whole computation moves to the
8202        /// helper (each delegation becomes `return new Object[]{its
8203        /// args}`), the public ctor becomes `this(resolve$X(params))`
8204        /// and a synthetic `private C(Object[] h)` relay unpacks the
8205        /// carriers into the one legal first-statement delegation.
8206        Full,
8207    }
8208    // Full-mode gate: uniform delegation target (this/super, class,
8209    // descriptor), non-empty args, no pre-existing (Object[]) ctor,
8210    // and — the relay signature is per-CLASS unique — only the
8211    // lexicographically least <init> descriptor of the class may take
8212    // the relay (deterministic across parallel workers; a second
8213    // branched ctor in one class keeps its honest error).
8214    let full_gate = (|| -> Option<()> {
8215        let Expr::Method {
8216            args: a0,
8217            desc: d0,
8218            is_super: s0,
8219            cls: c0,
8220            ..
8221        } = &dels[0]
8222        else {
8223            return None;
8224        };
8225        if a0.is_empty() {
8226            return None;
8227        }
8228        for d in &dels[1..] {
8229            match d {
8230                Expr::Method {
8231                    args,
8232                    desc,
8233                    is_super,
8234                    cls,
8235                    ..
8236                } if args.len() == a0.len()
8237                    && **desc == **d0
8238                    && is_super == s0
8239                    && cls == c0 => {}
8240                _ => return None,
8241            }
8242        }
8243        if class
8244            .all_methods()
8245            .any(|m| &*m.name == "<init>" && &*m.desc == "([Ljava/lang/Object;)V")
8246        {
8247            return None;
8248        }
8249        Some(())
8250    })()
8251    .is_some();
8252    let mode = if dels.iter().all(|d| *d == dels[0]) {
8253        let mut carrier: Option<u32> = None;
8254        let mut reads = 0usize;
8255        let mut multi = false;
8256        for a in t_args.iter() {
8257            let c = a.clone();
8258            visit_exprs(&c, &mut |x| {
8259                if let Expr::Local { var, .. } = x {
8260                    if (*var as usize) < vt.vars.len() && !vt.vars[*var as usize].is_param {
8261                        reads += 1;
8262                        match carrier {
8263                            Some(v) if v == *var => {}
8264                            Some(_) => multi = true,
8265                            None => carrier = Some(*var),
8266                        }
8267                    }
8268                }
8269            });
8270        }
8271        match carrier {
8272            Some(cv) if !multi && reads == 1 => Mode::Single(cv),
8273            _ if full_gate => Mode::Full,
8274            _ => return,
8275        }
8276    } else {
8277        let positioned: Option<Mode> = (|| {
8278        let arity = t_args.len();
8279        // Uniform arity FIRST: a bare `super()` mixed among arg-carrying
8280        // dels made the single-diff slot k index past a short del's args
8281        // (deepseek v52 / kimi c60.o panics, index-out-of-bounds).
8282        if dels
8283            .iter()
8284            .any(|d| !matches!(d, Expr::Method { args, .. } if args.len() == arity))
8285        {
8286            return None;
8287        }
8288        let mut diff: Vec<usize> = Vec::new();
8289        for j in 0..arity {
8290            if dels.iter().any(|d| match d {
8291                Expr::Method { args, .. } => args.get(j) != t_args.get(j),
8292                _ => true,
8293            }) {
8294                diff.push(j);
8295            }
8296        }
8297        if diff.len() != 1 {
8298            return None;
8299        }
8300        let k = diff[0];
8301        // Non-arg parts AND all other arg positions identical: neutralize
8302        // slot k on every del and compare whole exprs (catches mixed
8303        // this()/super() or different targets).
8304        let mut base0 = dels[0].clone();
8305        if let Expr::Method { args, .. } = &mut base0 {
8306            args[k] = Expr::Const(ConstVal::Null);
8307        }
8308        for d in dels.iter() {
8309            let mut c = d.clone();
8310            if let Expr::Method { args, .. } = &mut c {
8311                args[k] = Expr::Const(ConstVal::Null);
8312            }
8313            if c != base0 {
8314                return None;
8315            }
8316        }
8317        // Slot k: bare non-param local at every site, one erased type,
8318        // and the carrier never read at another position.
8319        let mut carrier_ty: Option<JavaType> = None;
8320        for d in dels.iter() {
8321            let Expr::Method { args, .. } = d else {
8322                return None;
8323            };
8324            let Expr::Local { var, .. } = &args[k] else {
8325                return None;
8326            };
8327            if (*var as usize) >= vt.vars.len() || vt.vars[*var as usize].is_param {
8328                return None;
8329            }
8330            let ty = vt.vars[*var as usize].ty.erased();
8331            match &carrier_ty {
8332                Some(t) if *t == ty => {}
8333                Some(_) => return None,
8334                None => carrier_ty = Some(ty),
8335            }
8336            // Non-k positions must be param/const/static-only — a
8337            // computed local there would have its def extracted into
8338            // the helper and the ctor arg list would dangle (lark
8339            // audiosave/i 找不到符号 ×5-per-site). Also covers the
8340            // carrier leaking into another position.
8341            for (j, a) in args.iter().enumerate() {
8342                if j == k {
8343                    continue;
8344                }
8345                let mut bad = false;
8346                visit_exprs(a, &mut |x| {
8347                    if let Expr::Local { var: v2, .. } = x {
8348                        if (*v2 as usize) >= vt.vars.len() || !vt.vars[*v2 as usize].is_param {
8349                            bad = true;
8350                        }
8351                    }
8352                });
8353                if bad {
8354                    return None;
8355                }
8356            }
8357        }
8358        Some(Mode::Position(k))
8359        })();
8360        match positioned {
8361            Some(m) => m,
8362            None if full_gate => Mode::Full,
8363            None => return,
8364        }
8365    };
8366    let l = stmts.iter().rposition(is_bare_ctor_call);
8367    if l == Some(0) {
8368        return;
8369    }
8370    let (mut extracted, mut tail): (Vec<Stmt>, Vec<Stmt>) = match l {
8371        Some(l) => (stmts[..l].to_vec(), stmts[l + 1..].to_vec()),
8372        None => (stmts.clone(), Vec::new()),
8373    };
8374    if !extraction_shape_ok(&extracted) {
8375        return;
8376    }
8377    // Post-delegation tails (field writes / trace stubs between each
8378    // branch delegation and its return) are CTOR work, not helper
8379    // computation — they commonly touch `this`. When every site's tail
8380    // is identical (and equals the top-level continuation when one
8381    // exists) hoist one copy after the merged delegation; otherwise
8382    // they stay in the helper and the static_safe gate below decides.
8383    let mut site_tails: Vec<Vec<Stmt>> = Vec::new();
8384    {
8385        let mut probe = extracted.clone();
8386        site_tails_walk(&mut probe, &mut site_tails, false);
8387    }
8388    let hoist = !site_tails.is_empty() && {
8389        let n0 = normalize_tail(&site_tails[0]);
8390        site_tails.iter().all(|t| normalize_tail(t) == n0)
8391            && (l.is_none() || normalize_tail(&tail) == n0)
8392    };
8393    if hoist {
8394        site_tails_walk(&mut extracted, &mut Vec::new(), true);
8395        if l.is_none() {
8396            tail = normalize_tail(&site_tails[0]);
8397        }
8398    }
8399    // The delegation the ctor keeps: the top-level primary when present,
8400    // else any del (all non-k parts are identical).
8401    let primary_del: Expr = match l {
8402        Some(l) => match &stmts[l] {
8403            Stmt::ExprStmt(e) => e.clone(),
8404            _ => return,
8405        },
8406        None => dels[0].clone(),
8407    };
8408    let ret = match mode {
8409        Mode::Single(cv) => vt.var(cv).ty.erased(),
8410        Mode::Position(k) => match &primary_del {
8411            Expr::Method { args, .. } => match args.get(k) {
8412                Some(Expr::Local { var, .. }) => vt.var(*var).ty.erased(),
8413                _ => return,
8414            },
8415            _ => return,
8416        },
8417        Mode::Full => JavaType::Array(Box::new(JavaType::Object(std::sync::Arc::from(
8418            "java/lang/Object",
8419        )))),
8420    };
8421    match mode {
8422        Mode::Single(cv) => {
8423            strip_delegations_deep(&mut extracted);
8424            let cv_local = Expr::Local {
8425                var: cv,
8426                ty: vt.var(cv).ty.clone(),
8427            };
8428            rewrite_bare_returns(&mut extracted, &cv_local);
8429            if !always_returns(&extracted) {
8430                extracted.push(Stmt::Return(Some(cv_local)));
8431            }
8432        }
8433        Mode::Position(k) => {
8434            // Each stripped delegation's own slot-k carrier fills the
8435            // next bare return in its list; merge-point returns fall
8436            // back to the primary carrier below.
8437            fill_returns_by_position(&mut extracted, &|args: &[Expr]| args.get(k).cloned());
8438            let fallback = match &primary_del {
8439                Expr::Method { args, .. } => args[k].clone(),
8440                _ => return,
8441            };
8442            rewrite_bare_returns(&mut extracted, &fallback);
8443            if !always_returns(&extracted) {
8444                extracted.push(Stmt::Return(Some(fallback)));
8445            }
8446        }
8447        Mode::Full => {
8448            // Every delegation becomes `return new Object[]{its own
8449            // args}`; merge-point bare returns and the fall-through
8450            // completion take the PRIMARY delegation's args (the
8451            // top-level last del when present — its args are
8452            // top-level-scoped by construction). With no primary,
8453            // every path must already delegate explicitly.
8454            fill_returns_by_position(&mut extracted, &|args: &[Expr]| {
8455                Some(array_init(args.to_vec()))
8456            });
8457            if l.is_none() {
8458                // No top-level primary: every path must delegate. A
8459                // dead tail can still dangle after the shortest
8460                // always-returning prefix (DataHolder: an assign after
8461                // a try whose body returns and whose catch throws) —
8462                // truncate it (unreachable by Java's flow model, so
8463                // sound), else bail.
8464                if !always_returns(&extracted) {
8465                    let cut = (1..=extracted.len())
8466                        .find(|&i| always_returns(&extracted[..i]));
8467                    match cut {
8468                        Some(i) => extracted.truncate(i),
8469                        None => return,
8470                    }
8471                }
8472            }
8473            let fallback_args = match &primary_del {
8474                Expr::Method { args, .. } => args.clone(),
8475                _ => return,
8476            };
8477            let fallback = array_init(fallback_args);
8478            rewrite_bare_returns(&mut extracted, &fallback);
8479            if !always_returns(&extracted) {
8480                extracted.push(Stmt::Return(Some(fallback)));
8481            }
8482        }
8483    }
8484    let this_id = vt
8485        .vars
8486        .iter()
8487        .find(|v| v.is_param && v.name == "this")
8488        .map(|v| v.id);
8489    if !static_safe(&extracted, this_id) {
8490        return;
8491    }
8492    let mut defs: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
8493    collect_def_vars(&extracted, &mut defs);
8494    if defs.iter().any(|v| var_occurs_in(&tail, *v)) {
8495        return;
8496    }
8497    let params: Vec<(u32, JavaType, String)> = vt
8498        .vars
8499        .iter()
8500        .filter(|v| v.is_param && v.name != "this")
8501        .map(|v| (v.id, v.ty.erased(), v.name.clone()))
8502        .collect();
8503    let name = helper_name(&class.name, desc_str);
8504    let call = Expr::Method {
8505        owner: None,
8506        cls: std::sync::Arc::from(class.name.as_str()),
8507        name: std::sync::Arc::from(name.as_str()),
8508        desc: std::sync::Arc::new(jdc_core::types::MethodDescriptor {
8509            ret: ret.clone(),
8510            args: params.iter().map(|p| p.1.clone()).collect(),
8511        }),
8512        args: params
8513            .iter()
8514            .map(|(id, ty, _)| Expr::Local {
8515                var: *id,
8516                ty: TypeRef::J(ty.clone()),
8517            })
8518            .collect(),
8519        is_static: true,
8520        is_interface: false,
8521        is_special: false,
8522        is_super: false,
8523        is_dynamic: false,
8524        type_args: Vec::new(),
8525    };
8526    let mut new_del = primary_del.clone();
8527    match mode {
8528        Mode::Single(cv) => {
8529            deep_rewrite(&mut new_del, &mut |x| {
8530                if let Expr::Local { var, .. } = x {
8531                    if *var == cv {
8532                        *x = call.clone();
8533                    }
8534                }
8535            });
8536        }
8537        Mode::Position(k) => {
8538            let Expr::Method { args, .. } = &mut new_del else {
8539                return;
8540            };
8541            if k >= args.len() {
8542                return;
8543            }
8544            args[k] = call.clone();
8545        }
8546        Mode::Full => {
8547            // The public ctor's one legal statement: `this(resolve$X(p..))`
8548            // — the synthetic relay ctor below unpacks the carriers into
8549            // the uniform target delegation.
8550            new_del = Expr::Method {
8551                owner: None,
8552                cls: std::sync::Arc::from(class.name.as_str()),
8553                name: "<init>".into(),
8554                desc: std::sync::Arc::new(jdc_core::types::MethodDescriptor {
8555                    args: vec![JavaType::Array(Box::new(JavaType::Object(
8556                        std::sync::Arc::from("java/lang/Object"),
8557                    )))],
8558                    ret: JavaType::Void,
8559                }),
8560                args: vec![call.clone()],
8561                is_static: false,
8562                is_interface: false,
8563                is_special: true,
8564                is_super: false,
8565                is_dynamic: false,
8566                type_args: Vec::new(),
8567            };
8568        }
8569    }
8570    {
8571        let mut m = ctor_helpers().lock().unwrap_or_else(|e| e.into_inner());
8572        let v = m.entry(class.name.clone()).or_default();
8573        if !v.iter().any(|h| h.name == name) {
8574            // The extracted body skips the late pipeline polish (it left
8575            // the ctor before cleanup ran) — invert empty thens so the
8576            // helper doesn't render `if (..) {} else {..}`.
8577            let mut hbody = Stmt::Block(extracted);
8578            invert_empty_thens(&mut hbody);
8579            v.push(CtorHelper {
8580                name,
8581                ret: ret.clone(),
8582                params: params.into_iter().map(|(_, ty, nm)| (ty, nm)).collect(),
8583                body: hbody,
8584                vt: vt.clone(),
8585            });
8586            if matches!(mode, Mode::Full) {
8587                let Expr::Method {
8588                    desc: d0,
8589                    is_super: s0,
8590                    ..
8591                } = &primary_del
8592                else {
8593                    return;
8594                };
8595                let mut rm = ctor_relays().lock().unwrap_or_else(|e| e.into_inner());
8596                let rv = rm.entry(class.name.clone()).or_default();
8597                if !rv.iter().any(|(d, _)| d == desc_str) {
8598                    rv.push((
8599                        desc_str.to_string(),
8600                        CtorRelay {
8601                            formals: d0.args.clone(),
8602                            is_super: *s0,
8603                        },
8604                    ));
8605                }
8606            }
8607        } else {
8608            return; // duplicate decompile of the same ctor: keep the first
8609        }
8610    }
8611    *stmts = std::iter::once(Stmt::ExprStmt(new_del)).chain(tail).collect();
8612}
8613
8614/// Position-mode return fill: walk each statement list left-to-right; a
8615/// delegation at slot k hands its carrier to the next bare `return;` in
8616/// the SAME list (the bytecode shape is always `this(..); trace..;
8617/// return;`). With no following return in the list the delegation
8618/// becomes the return itself and the rest of the list (trace stubs only
8619/// — a second delegation there is impossible) is dropped as unreachable.
8620fn fill_returns_by_position(
8621    stmts: &mut Vec<Stmt>,
8622    pick: &dyn Fn(&[Expr]) -> Option<Expr>,
8623) {
8624    let mut i = 0;
8625    while i < stmts.len() {
8626        if is_bare_ctor_call(&stmts[i]) {
8627            let karg = match &stmts[i] {
8628                Stmt::ExprStmt(Expr::Method { args, .. }) => pick(args),
8629                _ => None,
8630            };
8631            if let Some(v) = karg {
8632                let mut j = i + 1;
8633                let mut found = None;
8634                while j < stmts.len() {
8635                    if matches!(stmts[j], Stmt::Return(None)) {
8636                        found = Some(j);
8637                        break;
8638                    }
8639                    if is_bare_ctor_call(&stmts[j]) {
8640                        break; // next delegation owns the next return
8641                    }
8642                    j += 1;
8643                }
8644                match found {
8645                    Some(j) => {
8646                        stmts[j] = Stmt::Return(Some(v));
8647                        stmts.remove(i);
8648                        continue;
8649                    }
8650                    None => {
8651                        stmts[i] = Stmt::Return(Some(v));
8652                        stmts.truncate(i + 1);
8653                        break;
8654                    }
8655                }
8656            }
8657        }
8658        fill_returns_one(&mut stmts[i], pick);
8659        i += 1;
8660    }
8661}
8662
8663fn fill_returns_one(st: &mut Stmt, pick: &dyn Fn(&[Expr]) -> Option<Expr>) {
8664    match st {
8665        Stmt::Block(v) => fill_returns_by_position(v, pick),
8666        Stmt::If { then_stmt, else_stmt, .. } => {
8667            fill_returns_stmt(then_stmt, pick);
8668            if let Some(e) = else_stmt {
8669                fill_returns_stmt(e, pick);
8670            }
8671        }
8672        Stmt::While { body, .. }
8673        | Stmt::DoWhile { body, .. }
8674        | Stmt::ForEach { body, .. }
8675        | Stmt::Synchronized { body, .. }
8676        | Stmt::Labeled { body, .. } => fill_returns_stmt(body, pick),
8677        Stmt::For { init, body, .. } => {
8678            fill_returns_by_position(init, pick);
8679            fill_returns_stmt(body, pick);
8680        }
8681        Stmt::Switch { cases, default, .. } => {
8682            for c in cases.iter_mut() {
8683                fill_returns_by_position(&mut c.body, pick);
8684            }
8685            if let Some(d) = default {
8686                fill_returns_stmt(d, pick);
8687            }
8688        }
8689        Stmt::Try { body, catches, finally } | Stmt::TryWithResources { body, catches, finally, .. } => {
8690            fill_returns_stmt(body, pick);
8691            for c in catches.iter_mut() {
8692                fill_returns_stmt(&mut c.body, pick);
8693            }
8694            if let Some(f) = finally {
8695                fill_returns_stmt(f, pick);
8696            }
8697        }
8698        _ => {}
8699    }
8700}
8701
8702fn fill_returns_stmt(st: &mut Stmt, pick: &dyn Fn(&[Expr]) -> Option<Expr>) {
8703    if is_bare_ctor_call(st) {
8704        // Single-statement branch body (`if (c) this(..);`) — becomes
8705        // the return directly.
8706        if let Stmt::ExprStmt(Expr::Method { args, .. }) = st {
8707            if let Some(v) = pick(args) {
8708                *st = Stmt::Return(Some(v));
8709                return;
8710            }
8711        }
8712    }
8713    fill_returns_one(st, pick);
8714}
8715
8716fn normalize_tail(t: &[Stmt]) -> Vec<Stmt> {
8717    let mut v = t.to_vec();
8718    while matches!(v.last(), Some(Stmt::Return(None))) {
8719        v.pop();
8720    }
8721    v
8722}
8723
8724/// Record (and with `carve`, remove) the statements between each
8725/// delegation and the next bare return in its list — a delegation with
8726/// no following return takes the rest of the list. Single-statement
8727/// branch bodies record an empty tail.
8728fn site_tails_walk(stmts: &mut Vec<Stmt>, out: &mut Vec<Vec<Stmt>>, carve: bool) {
8729    let mut i = 0;
8730    while i < stmts.len() {
8731        if is_bare_ctor_call(&stmts[i]) {
8732            let mut j = i + 1;
8733            let mut end = stmts.len();
8734            while j < stmts.len() {
8735                if matches!(stmts[j], Stmt::Return(None)) {
8736                    end = j;
8737                    break;
8738                }
8739                if is_bare_ctor_call(&stmts[j]) {
8740                    end = j;
8741                    break;
8742                }
8743                j += 1;
8744            }
8745            out.push(stmts[i + 1..end].to_vec());
8746            if carve {
8747                stmts.splice(i + 1..end, []);
8748            }
8749            i += 1;
8750            continue;
8751        }
8752        site_tails_one(&mut stmts[i], out, carve);
8753        i += 1;
8754    }
8755}
8756
8757fn site_tails_one(st: &mut Stmt, out: &mut Vec<Vec<Stmt>>, carve: bool) {
8758    if is_bare_ctor_call(st) {
8759        out.push(Vec::new());
8760        return;
8761    }
8762    match st {
8763        Stmt::Block(v) => site_tails_walk(v, out, carve),
8764        Stmt::If { then_stmt, else_stmt, .. } => {
8765            site_tails_one(then_stmt, out, carve);
8766            if let Some(e) = else_stmt {
8767                site_tails_one(e, out, carve);
8768            }
8769        }
8770        Stmt::While { body, .. }
8771        | Stmt::DoWhile { body, .. }
8772        | Stmt::ForEach { body, .. }
8773        | Stmt::Synchronized { body, .. }
8774        | Stmt::Labeled { body, .. } => site_tails_one(body, out, carve),
8775        Stmt::For { init, body, .. } => {
8776            site_tails_walk(init, out, carve);
8777            site_tails_one(body, out, carve);
8778        }
8779        Stmt::Switch { cases, default, .. } => {
8780            for c in cases.iter_mut() {
8781                site_tails_walk(&mut c.body, out, carve);
8782            }
8783            if let Some(d) = default {
8784                site_tails_one(d, out, carve);
8785            }
8786        }
8787        Stmt::Try { body, catches, finally } | Stmt::TryWithResources { body, catches, finally, .. } => {
8788            site_tails_one(body, out, carve);
8789            for c in catches.iter_mut() {
8790                site_tails_one(&mut c.body, out, carve);
8791            }
8792            if let Some(f) = finally {
8793                site_tails_one(f, out, carve);
8794            }
8795        }
8796        _ => {}
8797    }
8798}
8799
8800/// Can this statement list NOT complete normally? Conservative for
8801/// loops (false); If needs BOTH branches, Switch needs every case plus
8802/// a default. Used to decide whether the extracted helper needs the
8803/// trailing `return carrier;` (appending one after exhaustive branches
8804/// is javac's "unreachable statement" error).
8805fn always_returns(stmts: &[Stmt]) -> bool {
8806    let Some(last) = stmts
8807        .iter()
8808        .rev()
8809        .find(|s| !matches!(s, Stmt::Block(v) if v.is_empty()))
8810    else {
8811        return false;
8812    };
8813    match last {
8814        Stmt::Return(Some(_)) | Stmt::Throw(_) => true,
8815        Stmt::Block(v) => always_returns(v),
8816        Stmt::If { then_stmt, else_stmt, .. } => match else_stmt {
8817            Some(e) => always_returns_one(then_stmt) && always_returns_one(e),
8818            None => false,
8819        },
8820        Stmt::Labeled { body, .. } | Stmt::Synchronized { body, .. } => {
8821            always_returns_one(body)
8822        }
8823        Stmt::Switch { cases, default, .. } => {
8824            default.as_ref().is_some_and(|d| always_returns_one(d))
8825                && cases.iter().all(|c| always_returns(&c.body))
8826        }
8827        Stmt::Try { body, catches, finally }
8828        | Stmt::TryWithResources { body, catches, finally, .. } => {
8829            finally.as_ref().is_some_and(|f| always_returns_one(f))
8830                || (always_returns_one(body)
8831                    && catches.iter().all(|c| always_returns_one(&c.body)))
8832        }
8833        _ => false,
8834    }
8835}
8836
8837fn always_returns_one(st: &Stmt) -> bool {
8838    always_returns(std::slice::from_ref(st))
8839}
8840
8841fn inline_ctor_arg_defs(
8842    call: &mut Stmt,
8843    prefix: &[&Stmt],
8844    vt: &VarTable,
8845    consumed: &mut Vec<(u32, usize)>,
8846) -> bool {
8847    let Stmt::ExprStmt(cexpr) = call else {
8848        return false;
8849    };
8850    // Replace every read of `v` in `e` with its value-so-far; a read
8851    // BEFORE any def (val None) is unresolvable → false.
8852    fn subst(e: &mut Expr, v: u32, val: &Option<Expr>) -> bool {
8853        let mut ok = true;
8854        deep_rewrite(e, &mut |x| {
8855            if let Expr::Local { var, .. } = x {
8856                if *var == v {
8857                    match val {
8858                        Some(e2) => *x = e2.clone(),
8859                        None => ok = false,
8860                    }
8861                }
8862            }
8863        });
8864        ok
8865    }
8866    let mut seen: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
8867    for _ in 0..32 {
8868        let mut need: Option<u32> = None;
8869        visit_exprs(cexpr, &mut |x| {
8870            if need.is_none() {
8871                if let Expr::Local { var, .. } = x {
8872                    if (*var as usize) < vt.vars.len() && !vt.vars[*var as usize].is_param {
8873                        need = Some(*var);
8874                    }
8875                }
8876            }
8877        });
8878        let Some(v) = need else { break };
8879        if !seen.insert(v) {
8880            return false; // cyclic reuse (`str = .. + str`) — bail
8881        }
8882        // CHAIN walk: fold EVERY prefix def of v into the value the
8883        // delegation actually receives. The old strict single-occurrence
8884        // rule rejected the convenience-ctor reassignment chain
8885        // (`f = getInstance(ctx); f = transform(f, ..); this(ctx, f)` —
8886        // androidx CameraController, weibo this-not-first ×831): the
8887        // SOURCE inlined the computation into the delegation args and
8888        // R8 decomposed it into register writes. Reads of v are allowed
8889        // only INSIDE its own def chain; any other prefix statement
8890        // touching v means the value escapes or the object mutates
8891        // (`stringBuilder.append(..)` — inlining a fresh `new
8892        // StringBuilder()` would lose the message), so bail.
8893        let mut val: Option<Expr> = None;
8894        let mut ok = true;
8895        for (si, st) in prefix.iter().enumerate() {
8896            match st {
8897                // A bare declaration (init None — the structurer hoists
8898                // `T v;` to the top) carries no value: skip it. Failing
8899                // here rejected every chain under a hoisted decl (ci1/a).
8900                Stmt::LocalDef { var, init: None, .. } if *var == v => {}
8901                Stmt::LocalDef { var, init, .. } if *var == v => {
8902                    let Some(e) = init else { ok = false; break };
8903                    let mut e2 = (*e).clone();
8904                    if !subst(&mut e2, v, &val) {
8905                        ok = false;
8906                        break;
8907                    }
8908                    val = Some(e2);
8909                    consumed.push((v, si));
8910                }
8911                Stmt::ExprStmt(Expr::Assign { target, op, .. })
8912                    if matches!(&**target, Expr::Local { var: tv, .. } if *tv == v) =>
8913                {
8914                    if !matches!(op, AssignOp::Plain) {
8915                        ok = false; // compound write: not a plain def
8916                        break;
8917                    }
8918                    let Stmt::ExprStmt(Expr::Assign { value, .. }) = st else {
8919                        unreachable!()
8920                    };
8921                    let mut e2 = value.as_ref().clone();
8922                    if !subst(&mut e2, v, &val) {
8923                        ok = false;
8924                        break;
8925                    }
8926                    val = Some(e2);
8927                    consumed.push((v, si));
8928                }
8929                _ => {
8930                    let mut touched = false;
8931                    visit_all_exprs(st, &mut |x| {
8932                        if let Expr::Local { var, .. } = x {
8933                            if *var == v {
8934                                touched = true;
8935                            }
8936                        }
8937                    });
8938                    if touched {
8939                        ok = false;
8940                        break;
8941                    }
8942                }
8943            }
8944        }
8945        if !ok {
8946            return false;
8947        }
8948        let Some(de) = val else { return false };
8949        if contains_this_access(&de) {
8950            return false;
8951        }
8952        // Huge defs multiply per read site per round — bound them.
8953        let mut de_nodes = 0usize;
8954        visit_exprs(&de, &mut |_| de_nodes += 1);
8955        if de_nodes > 256 {
8956            return false;
8957        }
8958        deep_rewrite(cexpr, &mut |x| {
8959            if let Expr::Local { var, .. } = x {
8960                if *var == v {
8961                    *x = de.clone();
8962                }
8963            }
8964        });
8965        // Exponential blowup guard: a var read twice doubles its def
8966        // per round, compounding across the transitive chain — cap the
8967        // expression size (a 2^k node tree overflowed the worker stack
8968        // process-wide).
8969        let mut nodes = 0usize;
8970        visit_exprs(cexpr, &mut |_| nodes += 1);
8971        if nodes > 2_048 {
8972            return false;
8973        }
8974    }
8975    true
8976}
8977
8978/// Fold a prelude StringBuilder mutation chain into the delegation
8979/// argument as a fluent chain: `StringBuilder sb = new SB(x);
8980/// sb.append(a); sb.append(b); super(sb.toString())` →
8981/// `super(new SB(x).append(a).append(b).toString())`. The generic def
8982/// inliner rejects mutation calls — their effect is not expressible as
8983/// a value — but `append` RETURNS its receiver, so the chain
8984/// reconstructs exactly the same mutations in one expression (androidx
8985/// emoji2/flatbuffer UnpairedSurrogateException family, the shapes the
8986/// strict-occurrence rule was written to refuse). Aborts on any `this`
8987/// touch inside the chain (illegal before super) or any other read of
8988/// the SB local (the value must not escape the fold). Returns the
8989/// delegation's new index (statements before it were removed).
8990fn fold_sb_chain(stmts: &mut Vec<Stmt>, pos: usize) -> usize {
8991    // 1. the SB local: first `v = new StringBuilder(..)` in the prefix.
8992    let mut sbv: Option<u32> = None;
8993    let mut def_i: Option<usize> = None;
8994    let mut def_init: Option<Expr> = None;
8995    for (i, st) in stmts[..pos].iter().enumerate() {
8996        let cand: Option<(u32, &Expr)> = match st {
8997            Stmt::LocalDef {
8998                var,
8999                init: Some(e),
9000                ..
9001            } => Some((*var, e)),
9002            Stmt::ExprStmt(Expr::Assign {
9003                target,
9004                value,
9005                op: AssignOp::Plain,
9006                ..
9007            }) => match &**target {
9008                Expr::Local { var, .. } => Some((*var, value.as_ref())),
9009                _ => None,
9010            },
9011            _ => None,
9012        };
9013        if let Some((v, init)) = cand {
9014            if let Expr::New { cls, .. } = init {
9015                if cls.as_ref() == "java/lang/StringBuilder" {
9016                    sbv = Some(v);
9017                    def_i = Some(i);
9018                    def_init = Some(init.clone());
9019                    break;
9020                }
9021            }
9022        }
9023    }
9024    let (Some(sbv), Some(def_i), Some(init)) = (sbv, def_i, def_init) else {
9025        return pos;
9026    };
9027    if contains_this_access(&init) {
9028        return pos;
9029    }
9030    // 2. append statements on sbv, in order.
9031    let mut appends: Vec<usize> = Vec::new();
9032    for (i, st) in stmts[..pos].iter().enumerate() {
9033        if let Stmt::ExprStmt(Expr::Method {
9034            owner: Some(o),
9035            cls,
9036            name,
9037            ..
9038        }) = st
9039        {
9040            if matches!(&**o, Expr::Local { var, .. } if *var == sbv)
9041                && cls.as_ref() == "java/lang/StringBuilder"
9042                && &**name == "append"
9043            {
9044                appends.push(i);
9045            }
9046        }
9047    }
9048    // 3. no read of sbv after the delegation.
9049    let mut post_read = false;
9050    for st in stmts.iter().skip(pos + 1) {
9051        visit_all_exprs(st, &mut |e| {
9052            if let Expr::Local { var, .. } = e {
9053                if *var == sbv {
9054                    post_read = true;
9055                }
9056            }
9057        });
9058    }
9059    if post_read {
9060        return pos;
9061    }
9062    // 4. prefix reads of sbv only in the def and the append receivers
9063    //    (arguments are checked for `this`, receivers are the chain).
9064    for (i, st) in stmts[..pos].iter().enumerate() {
9065        if i == def_i || appends.contains(&i) {
9066            // append ARGUMENTS must not touch this; receiver is fine.
9067            if let Stmt::ExprStmt(Expr::Method { args, .. }) = st {
9068                if args.iter().any(contains_this_access) {
9069                    return pos;
9070                }
9071            }
9072            continue;
9073        }
9074        let mut leak = false;
9075        visit_all_exprs(st, &mut |e| {
9076            if let Expr::Local { var, .. } = e {
9077                if *var == sbv {
9078                    leak = true;
9079                }
9080            }
9081        });
9082        if leak {
9083            return pos;
9084        }
9085    }
9086    // 5. the call reads sbv exactly once, as `sbv.toString()`.
9087    let mut local_reads = 0usize;
9088    let mut tostring_reads = 0usize;
9089    visit_all_exprs(&stmts[pos], &mut |e| match e {
9090        Expr::Local { var, .. } if *var == sbv => local_reads += 1,
9091        Expr::Method {
9092            owner: Some(o),
9093            name,
9094            args,
9095            ..
9096        } if args.is_empty()
9097            && &**name == "toString"
9098            && matches!(&**o, Expr::Local { var, .. } if *var == sbv) =>
9099        {
9100            tostring_reads += 1;
9101        }
9102        _ => {}
9103    });
9104    if local_reads != 1 || tostring_reads != 1 {
9105        return pos;
9106    }
9107    // 6. build the fluent chain and swap it into the toString receiver.
9108    let mut chain = init;
9109    for ai in &appends {
9110        if let Stmt::ExprStmt(Expr::Method {
9111            owner: _,
9112            cls,
9113            name,
9114            desc,
9115            args,
9116            is_static,
9117            is_interface,
9118            is_special,
9119            is_super,
9120            is_dynamic,
9121            type_args,
9122            ..
9123        }) = &stmts[*ai]
9124        {
9125            chain = Expr::Method {
9126                owner: Some(Box::new(chain)),
9127                cls: cls.clone(),
9128                name: name.clone(),
9129                desc: desc.clone(),
9130                args: args.clone(),
9131                is_static: *is_static,
9132                is_interface: *is_interface,
9133                is_special: *is_special,
9134                is_super: *is_super,
9135                is_dynamic: *is_dynamic,
9136                type_args: type_args.clone(),
9137            };
9138        }
9139    }
9140    let mut replaced = false;
9141    walk_stmt_exprs(&mut stmts[pos], &mut |e| {
9142        deep_rewrite(e, &mut |x| {
9143            if let Expr::Method {
9144                owner: Some(o),
9145                name,
9146                args,
9147                ..
9148            } = x
9149            {
9150                if args.is_empty()
9151                    && &**name == "toString"
9152                    && matches!(&**o, Expr::Local { var, .. } if *var == sbv)
9153                {
9154                    **o = chain.clone();
9155                    replaced = true;
9156                }
9157            }
9158        });
9159    });
9160    if !replaced {
9161        return pos; // never remove without the swap — atomicity
9162    }
9163    // 7. drop the consumed statements (descending), adjust pos.
9164    let mut removed: Vec<usize> = appends.clone();
9165    removed.push(def_i);
9166    removed.sort_unstable();
9167    removed.dedup();
9168    for i in removed.iter().rev() {
9169        stmts.remove(*i);
9170    }
9171    pos - removed.len()
9172}
9173
9174pub fn fix_ctor_super_first(body: &mut Stmt, vt: &VarTable) {
9175    let Stmt::Block(stmts) = body else { return };
9176    if stmts.is_empty() || is_bare_ctor_call(stmts.first().unwrap()) {
9177        return;
9178    }
9179    // Top-level delegation call.
9180    if let Some(pos0) = stmts.iter().position(is_bare_ctor_call) {
9181        let pos = fold_sb_chain(stmts, pos0);
9182        let mut call = stmts[pos].clone();
9183        let prefix: Vec<&Stmt> = stmts[..pos].iter().collect();
9184        let mut consumed: Vec<(u32, usize)> = Vec::new();
9185        if !inline_ctor_arg_defs(&mut call, &prefix, vt, &mut consumed) {
9186            return;
9187        }
9188        stmts.remove(pos);
9189        // Drop the chain defs the inline consumed (all indices < pos) —
9190        // leaving them would DOUBLE-evaluate their (possibly impure)
9191        // inits. But ONLY for vars with no post-delegation use: dropping
9192        // such a var's DECLARATION makes ensure_declared re-add it at
9193        // the method top, i.e. BEFORE this() — a fresh "对this的调用必须
9194        // 是构造器中的第一个语句" (lark +587 the blind drop caused).
9195        let mut drop_idx: Vec<usize> = consumed
9196            .iter()
9197            .filter(|(v, _)| !var_occurs_in(&stmts[pos..], *v))
9198            .map(|(_, k)| *k)
9199            .collect();
9200        drop_idx.sort_unstable();
9201        drop_idx.dedup();
9202        for k in drop_idx.iter().rev() {
9203            stmts.remove(*k);
9204        }
9205        stmts.insert(0, call);
9206        return;
9207    }
9208    // The structurer can wrap the delegation in a bare nested block —
9209    // with the parameter null-checks AHEAD of it (weixin's Kotlin
9210    // intrinsics shape: `o.h(parcel, "source"); super();` at inner
9211    // positions 0/1, flattened to straight-line by cleanup AFTER this
9212    // pass, which is why the miss surfaced as 1150 weixin "对super的
9213    // 调用必须是构造器中的第一个语句"). Hoist the call from any
9214    // position whose predecessors are all straight-line statements; a
9215    // delegation behind control flow is the conditional-super family
9216    // (needs restructuring, not hoisting) and stays put.
9217    let mut found: Option<(usize, usize)> = None;
9218    for (i, st) in stmts.iter().enumerate() {
9219        if let Stmt::Block(inner) = st {
9220            if let Some(p) = inner.iter().position(is_bare_ctor_call) {
9221                if inner[..p]
9222                    .iter()
9223                    .all(|s| matches!(s, Stmt::LocalDef { .. } | Stmt::ExprStmt(_)))
9224                {
9225                    found = Some((i, p));
9226                    break;
9227                }
9228            }
9229        }
9230    }
9231    if let Some((i, p)) = found {
9232        let mut call = match &stmts[i] {
9233            Stmt::Block(inner) => inner[p].clone(),
9234            _ => unreachable!(),
9235        };
9236        let mut prefix: Vec<&Stmt> = stmts[..i].iter().collect();
9237        if let Stmt::Block(inner) = &stmts[i] {
9238            prefix.extend(inner[..p].iter());
9239        }
9240        let mut consumed: Vec<(u32, usize)> = Vec::new();
9241        if !inline_ctor_arg_defs(&mut call, &prefix, vt, &mut consumed) {
9242            return;
9243        }
9244        // Prefix index k maps to stmts[k] for k < i, inner[k - i] for
9245        // k >= i (all such k are < p). Suffix = the rest of the inner
9246        // block + the following top-level statements; a consumed def may
9247        // only drop when its var has no suffix use (declaration loss →
9248        // ensure_declared re-adds it BEFORE this()).
9249        // Compute the droppable set BEFORE mutating (borrow split).
9250        let mut ks: Vec<usize> = Vec::new();
9251        {
9252            let inner_tail: &[Stmt] = match &stmts[i] {
9253                Stmt::Block(inner) if p < inner.len() => &inner[p + 1..],
9254                _ => &[],
9255            };
9256            for (v, k) in &consumed {
9257                if !var_occurs_in(inner_tail, *v) && !var_occurs_in(&stmts[i + 1..], *v) {
9258                    ks.push(*k);
9259                }
9260            }
9261        }
9262        ks.sort_unstable();
9263        ks.dedup();
9264        if let Stmt::Block(inner) = &mut stmts[i] {
9265            inner.remove(p);
9266            for k in ks.iter().rev().filter(|k| **k >= i) {
9267                inner.remove(k - i);
9268            }
9269        }
9270        for k in ks.iter().rev().filter(|k| **k < i) {
9271            stmts.remove(*k);
9272        }
9273        stmts.insert(0, call);
9274    }
9275}
9276
9277/// A true constructor DELEGATION statement: `super(..)` (owner None) or
9278/// `this(..)` (owner This). An un-folded `new X; <init>` (lost
9279/// allocation — the ctor-fold-bug family) arrives as a `<init>` Method
9280/// with a NON-this owner and prints as `new X(..)`: it is not a
9281/// delegation and must neither satisfy nor anchor the super-first fixes
9282/// (v7.a$a2: a dead `new f(0,..)` init at position 0 made both hoists
9283/// believe the delegation was already first, leaving the real `super`
9284/// last — "对super的调用必须是构造器中的第一个语句").
9285fn is_delegation_expr(e: &Expr) -> bool {
9286    matches!(
9287        e,
9288        Expr::Method { name, is_special: true, owner, .. }
9289            if &**name == "<init>"
9290                && (owner.is_none() || matches!(owner.as_deref(), Some(Expr::This)))
9291    )
9292}
9293
9294pub(crate) fn is_bare_ctor_call(s: &Stmt) -> bool {
9295    matches!(s, Stmt::ExprStmt(e) if is_delegation_expr(e))
9296}
9297
9298/// Is the local ever ASSIGNED (write position: `x = ..`, `++x`)?
9299/// Distinct from a read count: a ctor's synthetic outer param is only
9300/// ever read; a param that gets written must stay declared.
9301pub(crate) fn local_is_written(body: &Stmt, var: u32) -> bool {
9302    let mut hit = false;
9303    visit_stmt_exprs_ro(body, &mut |e| {
9304        if hit {
9305            return;
9306        }
9307        visit_exprs(e, &mut |x| {
9308            if hit {
9309                return;
9310            }
9311            match x {
9312                Expr::Assign { target, .. }
9313                    if matches!(&**target, Expr::Local { var: v, .. } if *v == var) =>
9314                {
9315                    hit = true;
9316                }
9317                Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. }
9318                    if matches!(&**e, Expr::Local { var: v, .. } if *v == var) =>
9319                {
9320                    hit = true;
9321                }
9322                _ => {}
9323            }
9324        });
9325    });
9326    hit
9327}
9328
9329/// Source-form normalization for a non-static member-inner constructor.
9330/// The dex descriptor carries the synthetic outer instance as args[0]
9331/// (typed as the direct enclosing class); the emitter's qualified
9332/// `outer.new Inner(..)` / `this.new Inner(..)` sites pass it
9333/// implicitly and `super(..)` delegations drop it, so the signature
9334/// loses the param — and the body must stop referencing it: plain uses
9335/// become `Outer.this` (a `this.this$0 = p` assignment becomes
9336/// `this.this$0 = Outer.this`, exactly the runtime value), and
9337/// this()-delegations to `eligible` classes drop the leading param arg.
9338pub(crate) fn rewrite_inner_ctor_outer_param(
9339    body: &mut Stmt,
9340    param0: u32,
9341    outer_display: &str,
9342    outer_ty: &TypeRef,
9343    eligible: &[String],
9344) {
9345    // this()/super() delegations (and lost-alloc construction calls):
9346    // the leading arg IS the synthetic outer exactly when it is the
9347    // param itself.
9348    walk_stmt_exprs(body, &mut |e| {
9349        if let Expr::Method { name, cls, args, .. } = e {
9350            if &**name == "<init>"
9351                && eligible.iter().any(|c| c.as_str() == cls.as_ref())
9352                && args.first().is_some_and(|a| matches!(a, Expr::Local { var: v, .. } if *v == param0))
9353            {
9354                args.remove(0);
9355            }
9356        }
9357    });
9358    // Every remaining read of the param becomes `Outer.this`.
9359    let text = format!("{outer_display}.this");
9360    let ty = outer_ty.clone();
9361    walk_stmt_exprs(body, &mut |e| {
9362        deep_rewrite_reads(e, &mut |x| {
9363            if let Expr::Local { var: v, .. } = x {
9364                if *v == param0 {
9365                    *x = Expr::RawT(text.clone(), ty.clone());
9366                }
9367            }
9368        });
9369    });
9370}
9371
9372/// Ctors whose delegation is buried in control flow or behind arg
9373/// computations — the "对super的调用必须是构造器中的第一个语句" family
9374/// that plain hoisting cannot reach (weixin 264 / weibo 116 / reqable 71
9375/// / lark 62 after round 60): R8/Kotlin compute super-args conditionally
9376/// and dex legally invokes super mid-branch.
9377///
9378/// Shape B (branch merge): `prelude; if (c) {d1; super(A1); t1} else
9379/// {d2; super(A2); t2}; post` → one leading `super(..)` where each
9380/// differing arg becomes `c ? a1i : a2i`; branch remainders stay an
9381/// if/else AFTER the call; prelude statements move after it (nothing
9382/// may precede super in Java; bytecode pre-super work is local-only by
9383/// construction — dex forbids instance-field reads before the delegate).
9384///
9385/// Shape A (linear): `defs; super(A); rest` where `A` references the
9386/// def locals — plain hoisting (fix_ctor_super_first case 1) would move
9387/// the call above its argument definitions (forward refs). Inline the
9388/// referenced defs into the args first, drop the consumed defs, hoist.
9389/// Runs BEFORE fix_ctor_super_first and only fires when args reference
9390/// preceding locals, leaving the battle-tested plain hoist untouched
9391/// otherwise.
9392///
9393/// Inlining duplicates the def per use site, so a side-effecting def is
9394/// inlined only when its var's use count is provably preserved: a
9395/// single use in a merged arg (1 copy), or a single use in the
9396/// condition with ≤1 differing arg (the cond then appears in the `if`
9397/// plus one ternary — 2 copies of e.g. one Kotlin getter call, which
9398/// the original bytecode typically made twice anyway: `p.u() == null ?
9399/// .. : p.u().i()`). Pure defs inline freely. Every other shape aborts:
9400/// broken-but-faithful stays the status quo, wrong evaluation counts
9401/// would be worse.
9402/// Linear `def; delegation(args read def)` shapes. A plain hoist of the
9403/// delegation past its prefix would place the call ABOVE a definition
9404/// it reads — a forward reference (`super(context2, ..)` with `context2
9405/// = ..` below it). javac's attribution for the whole class then
9406/// collapses: the undefined identifier cascades into "non-static
9407/// super" and even bare `Object` resolution failures, poisoning every
9408/// later diagnostic in the file (weibo AppCompatTextView — the root of
9409/// a 4.1k-file Object cascade). The original source had the def's
9410/// expression INLINE in the delegation args; d8 computed it into a
9411/// register first. Inline the def into the args (the def is read
9412/// exactly once there — evaluation count preserved even for calls),
9413/// and when the register is REASSIGNED later (a fresh generation
9414/// sharing the slot), convert that first write into the declaration so
9415/// the var stays defined for its later readers. Any shape that cannot
9416/// be proven aborts untouched.
9417/// Count WRITE positions of `var` (assign / inc-dec targets) — the
9418/// shape-C validator needs exact counts, not just presence.
9419fn stmts_count_writes(stmts: &[Stmt], var: u32) -> usize {
9420    let mut n = 0usize;
9421    for s in stmts {
9422        visit_stmt_exprs_ro(s, &mut |e| {
9423            visit_exprs(e, &mut |x| match x {
9424                Expr::Assign { target, .. }
9425                    if matches!(&**target, Expr::Local { var: v, .. } if *v == var) =>
9426                {
9427                    n += 1;
9428                }
9429                Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. }
9430                    if matches!(&**e, Expr::Local { var: v, .. } if *v == var) =>
9431                {
9432                    n += 1;
9433                }
9434                _ => {}
9435            });
9436        });
9437    }
9438    n
9439}
9440
9441/// Collect every local WRITTEN in `stmts` — the complement inside a
9442/// ctor prefix is the param set (the only locals a pre-delegation arg
9443/// expression may read).
9444fn prefix_written_vars(stmts: &[Stmt]) -> jdc_core::FxHashSet<u32> {
9445    let mut set = jdc_core::FxHashSet::default();
9446    for s in stmts {
9447        let mut c = s.clone();
9448        walk_stmt_exprs(&mut c, &mut |e| {
9449            deep_rewrite(e, &mut |x| match x {
9450                Expr::Assign { target, .. } => {
9451                    if let Expr::Local { var, .. } = &**target {
9452                        set.insert(*var);
9453                    }
9454                }
9455                Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => {
9456                    if let Expr::Local { var, .. } = &**e {
9457                        set.insert(*var);
9458                    }
9459                }
9460                _ => {}
9461            });
9462        });
9463        if let Stmt::LocalDef { var, .. } = s {
9464            set.insert(*var);
9465        }
9466    }
9467    set
9468}
9469
9470/// Is `e` safe to evaluate INSIDE a ctor-delegation argument (before
9471/// this/super exists)? Params/consts/pure ops only — no instance-field
9472/// reads, no `this`, no captures.
9473fn pre_this_safe(e: &Expr, prefix_vars: &jdc_core::FxHashSet<u32>) -> bool {
9474    match e {
9475        Expr::Local { var, .. } => !prefix_vars.contains(var),
9476        Expr::Const(_) => true,
9477        Expr::Cast { e, .. } => pre_this_safe(e, prefix_vars),
9478        Expr::Bin { l, r, .. } => {
9479            pre_this_safe(l, prefix_vars) && pre_this_safe(r, prefix_vars)
9480        }
9481        Expr::Cond { c, t, f } => {
9482            pre_this_safe(c, prefix_vars)
9483                && pre_this_safe(t, prefix_vars)
9484                && pre_this_safe(f, prefix_vars)
9485        }
9486        Expr::Field { owner, is_static, .. } => {
9487            *is_static
9488                && owner
9489                    .as_ref()
9490                    .is_none_or(|o| pre_this_safe(o, prefix_vars))
9491        }
9492        Expr::Method {
9493            owner,
9494            args,
9495            is_static,
9496            ..
9497        } => {
9498            args.iter().all(|a| pre_this_safe(a, prefix_vars))
9499                && match owner {
9500                    Some(o) => pre_this_safe(o, prefix_vars),
9501                    None => *is_static,
9502                }
9503        }
9504        // Allocation in a delegation arg is legal Java before this()
9505        // (the ctor call touches no instance state of THIS class) — the
9506        // missing arm sent every `new Date()`-style Kotlin default
9507        // through `_ => false`, rejecting the whole fold (lark
9508        // Request$AppToApp this-not-first family, ~650 lark lines).
9509        // An inner-class construction needing an outer `this` carries
9510        // Expr::This in its args, which still fails below.
9511        Expr::New { args, .. } => args.iter().all(|a| pre_this_safe(a, prefix_vars)),
9512        Expr::NewArray { dims, init, .. } => {
9513            dims.iter().all(|d| pre_this_safe(d, prefix_vars))
9514                && init
9515                    .as_ref()
9516                    .map(|l| l.iter().all(|x| pre_this_safe(x, prefix_vars)))
9517                    .unwrap_or(true)
9518        }
9519        // Pure unary/instanceof/array reads.
9520        Expr::Un { e, .. } | Expr::InstanceOf { e, .. } => pre_this_safe(e, prefix_vars),
9521        Expr::ArrayIndex { array, index } => {
9522            pre_this_safe(array, prefix_vars) && pre_this_safe(index, prefix_vars)
9523        }
9524        _ => false,
9525    }
9526}
9527
9528/// `s` is exactly one plain assignment to local `v` (optionally wrapped
9529/// in a singleton Block): the branch shape of a Kotlin default-arg
9530/// override.
9531fn single_assign_to_v(s: &Stmt, v: u32) -> Option<Expr> {
9532    match s {
9533        Stmt::ExprStmt(Expr::Assign {
9534            target,
9535            op: AssignOp::Plain,
9536            value,
9537        }) if matches!(&**target, Expr::Local { var: vv, .. } if *vv == v) => {
9538            Some((**value).clone())
9539        }
9540        Stmt::Block(xs) if xs.len() == 1 => single_assign_to_v(&xs[0], v),
9541        _ => None,
9542    }
9543}
9544
9545/// Kotlin default-argument BRIDGE ctors (shape C of the delegation
9546/// family): `<init>(params.., int mask, DefaultConstructorMarker)`
9547/// computes each defaulted arg into a local — `v = p; if ((mask&bit)
9548/// != 0) { v = DEFAULT; }` — then delegates `this(.., v, ..)`. Java
9549/// allows NOTHING before the delegation, so the computations must fold
9550/// INTO the args (`this(.., (mask&bit) != 0 ? DEFAULT : p, ..)`).
9551/// Unfolded, the hoist passes lift the delegation above the raw local
9552/// reads: "找不到符号 变量 str2/v4/creationExtras2" — 2.5k root files
9553/// across weixin/lark/weibo (weixin MvvmObserverOwner$..Observer, lark
9554/// SimpleArrayMap/LongSparseArray). Per-var provability: exactly one
9555/// top-level base write, an optional single conditional override whose
9556/// branches are lone assignments, no other prefix touch of the var,
9557/// nothing after the delegation touches it, and the folded expression
9558/// is pre-this-safe. Unprovable vars stay untouched (partial folds are
9559/// strictly better — the rest was broken before too).
9560/// Flatten nested plain Blocks among the top-level statements. The
9561/// structurer hands ctor bodies as region-nested blocks (`Block([def,
9562/// Block([assign, if]), Block([delegate, return])])` — the Kotlin
9563/// bridge shape), and every delegation-repair pass scans the TOP level
9564/// only. Straight-line nesting flattens without semantics; control
9565/// flow lives inside If/While/etc. arms, never as a direct member.
9566pub(crate) fn flatten_top_blocks(stmts: &mut Vec<Stmt>) {
9567    fn rec(s: Stmt, out: &mut Vec<Stmt>) {
9568        match s {
9569            Stmt::Block(inner) => {
9570                for x in inner {
9571                    rec(x, out);
9572                }
9573            }
9574            other => out.push(other),
9575        }
9576    }
9577    if !stmts.iter().any(|s| matches!(s, Stmt::Block(_))) {
9578        return;
9579    }
9580    let mut out: Vec<Stmt> = Vec::with_capacity(stmts.len());
9581    for s in std::mem::take(stmts) {
9582        rec(s, &mut out);
9583    }
9584    *stmts = out;
9585}
9586
9587fn fold_default_arg_bridge(stmts: &mut Vec<Stmt>) -> bool {
9588    flatten_top_blocks(stmts);
9589    let Some(pos) = stmts.iter().position(is_bare_ctor_call) else {
9590        return false;
9591    };
9592    if pos == 0 {
9593        return false;
9594    }
9595    let Stmt::ExprStmt(Expr::Method { args, .. }) = &stmts[pos] else {
9596        return false;
9597    };
9598    let mut arg_vars: Vec<u32> = Vec::new();
9599    let mut reads = 0usize;
9600    for a in args {
9601        let mut c = a.clone();
9602        deep_rewrite_reads(&mut c, &mut |x| {
9603            if let Expr::Local { var, .. } = x {
9604                reads += 1;
9605                if !arg_vars.contains(var) {
9606                    arg_vars.push(*var);
9607                }
9608            }
9609        });
9610    }
9611    if arg_vars.is_empty() || reads != arg_vars.len() {
9612        return false; // a var read twice would duplicate evaluation
9613    }
9614    // The bridge ENDS at the delegation: nothing after may touch the
9615    // arg vars (reads OR writes).
9616    for &v in &arg_vars {
9617        if stmts_read_var(&stmts[pos + 1..], v) != 0
9618            || stmts_count_writes(&stmts[pos + 1..], v) != 0
9619        {
9620            return false;
9621        }
9622    }
9623    let prefix_vars = prefix_written_vars(&stmts[..pos]);
9624    let mut repl: Vec<(u32, Expr)> = Vec::new();
9625    let mut drop: Vec<usize> = Vec::new();
9626    for &v in &arg_vars {
9627        // Exact prefix shape for v.
9628        let mut base: Option<(usize, Expr)> = None;
9629        let mut decl: Option<usize> = None;
9630        let mut condf: Option<(usize, Expr, Option<Expr>, Option<Expr>)> = None;
9631        let mut ok = true;
9632        for (i, s) in stmts[..pos].iter().enumerate() {
9633            match s {
9634                Stmt::LocalDef { var, init, .. } if *var == v => match init {
9635                    None => {
9636                        if decl.is_some() {
9637                            ok = false;
9638                            break;
9639                        }
9640                        decl = Some(i);
9641                    }
9642                    Some(e) => {
9643                        if base.is_some() {
9644                            ok = false;
9645                            break;
9646                        }
9647                        base = Some((i, e.clone()));
9648                    }
9649                },
9650                Stmt::ExprStmt(Expr::Assign {
9651                    target,
9652                    op: AssignOp::Plain,
9653                    value,
9654                }) if matches!(&**target, Expr::Local { var: vv, .. } if *vv == v) => {
9655                    if base.is_some() {
9656                        ok = false;
9657                        break;
9658                    }
9659                    base = Some((i, (**value).clone()));
9660                }
9661                Stmt::If {
9662                    cond,
9663                    then_stmt,
9664                    else_stmt,
9665                } => {
9666                    let t = single_assign_to_v(then_stmt, v);
9667                    let f = else_stmt.as_ref().and_then(|b| single_assign_to_v(b, v));
9668                    let then_touches = t.is_some()
9669                        || stmts_read_var(std::slice::from_ref(&**then_stmt), v) != 0
9670                        || stmts_count_writes(std::slice::from_ref(&**then_stmt), v) != 0;
9671                    let else_touches = else_stmt.as_ref().is_some_and(|b| {
9672                        f.is_some()
9673                            || stmts_read_var(std::slice::from_ref(b), v) != 0
9674                            || stmts_count_writes(std::slice::from_ref(b), v) != 0
9675                    });
9676                    if !then_touches && !else_touches {
9677                        continue; // unrelated if
9678                    }
9679                    // Both branches must be lone assignments (a branch
9680                    // with its own delegation/return is the conditional-
9681                    // super machinery's shape, not this one), the cond
9682                    // must not read v, and only one override per var.
9683                    if condf.is_some()
9684                        || base.is_none()
9685                        || (then_touches && t.is_none())
9686                        || (else_touches && f.is_none())
9687                    {
9688                        ok = false;
9689                        break;
9690                    }
9691                    let cond_stmt = Stmt::ExprStmt(cond.clone());
9692                    if stmts_read_var(std::slice::from_ref(&cond_stmt), v) != 0 {
9693                        ok = false;
9694                        break;
9695                    }
9696                    condf = Some((i, cond.clone(), t, f));
9697                }
9698                _ => {}
9699            }
9700        }
9701        if !ok {
9702            continue;
9703        }
9704        let Some((bi, be)) = base else { continue };
9705        // Exact write/read accounting: base (+then/else) writes, zero
9706        // prefix reads.
9707        let want_writes = 1
9708            + condf
9709                .as_ref()
9710                .map(|(_, _, t, f)| t.is_some() as usize + f.is_some() as usize)
9711                .unwrap_or(0);
9712        if stmts_count_writes(&stmts[..pos], v) != want_writes
9713            || stmts_read_var(&stmts[..pos], v) != 0
9714        {
9715            continue;
9716        }
9717        let rep = match condf {
9718            Some((ci, c, t, f)) => {
9719                drop.push(ci);
9720                match (t, f) {
9721                    (Some(t), Some(f)) => Expr::Cond {
9722                        c: Box::new(c),
9723                        t: Box::new(t),
9724                        f: Box::new(f),
9725                    },
9726                    (Some(t), None) => Expr::Cond {
9727                        c: Box::new(c),
9728                        t: Box::new(t),
9729                        f: Box::new(be.clone()),
9730                    },
9731                    (None, Some(f)) => Expr::Cond {
9732                        c: Box::new(c),
9733                        t: Box::new(be.clone()),
9734                        f: Box::new(f),
9735                    },
9736                    (None, None) => unreachable!("condf requires a branch assign"),
9737                }
9738            }
9739            None => be.clone(),
9740        };
9741        if !pre_this_safe(&rep, &prefix_vars) {
9742            continue;
9743        }
9744        drop.push(bi);
9745        if let Some(d) = decl {
9746            drop.push(d);
9747        }
9748        repl.push((v, rep));
9749    }
9750    if repl.is_empty() {
9751        return false;
9752    }
9753    if let Stmt::ExprStmt(Expr::Method { args, .. }) = &mut stmts[pos] {
9754        for a in args.iter_mut() {
9755            deep_rewrite_reads(a, &mut |x| {
9756                if let Expr::Local { var, .. } = x {
9757                    if let Some((_, e)) = repl.iter().find(|(vv, _)| vv == var) {
9758                        *x = e.clone();
9759                    }
9760                }
9761            });
9762        }
9763    }
9764    drop.sort_unstable();
9765    drop.dedup();
9766    for i in drop.into_iter().rev() {
9767        stmts.remove(i);
9768    }
9769    true
9770}
9771
9772/// Rewrite static member references on Kotlin multi-file facade PARTS
9773/// (`StringsKt__StringsKt.trim(..)`) to the public facade the parts are
9774/// compiled into (`StringsKt.trim(..)`): the parts are package-private,
9775/// so every cross-package call site is "在kotlin.text中不是公共的; 无法
9776/// 从外部程序包中对其进行访问" (lark 1.4k root files; weibo's 8.4k-line
9777/// Kt cluster). Static members resolve through the facade by
9778/// inheritance (the map is gated on the extends chain).
9779pub fn rewrite_kotlin_facades(body: &mut Stmt, pool: &crate::DexPool) {
9780    let map = pool.kotlin_facade_map();
9781    if map.is_empty() {
9782        return;
9783    }
9784    walk_stmt_exprs(body, &mut |e| {
9785        match e {
9786            Expr::Method {
9787                cls,
9788                name,
9789                desc,
9790                is_static,
9791                ..
9792            } if *is_static => {
9793                if let Some((base, cover)) = map.get(cls.as_ref()) {
9794                    let hit = match cover {
9795                        None => true,
9796                        Some(c) => c.0.contains(&(name.to_string(), desc.to_string())),
9797                    };
9798                    if hit {
9799                        *cls = std::sync::Arc::from(base.as_str());
9800                    }
9801                }
9802            }
9803            Expr::Field {
9804                cls,
9805                name,
9806                is_static,
9807                ..
9808            } if *is_static => {
9809                if let Some((base, cover)) = map.get(cls.as_ref()) {
9810                    let hit = match cover {
9811                        None => true,
9812                        Some(c) => c.1.contains(name.as_ref()),
9813                    };
9814                    if hit {
9815                        *cls = std::sync::Arc::from(base.as_str());
9816                    }
9817                }
9818            }
9819            _ => {}
9820        }
9821    });
9822}
9823
9824/// Kotlin default-arg bridge fold for ENUM ctors: the synthetic
9825/// `(String, int, .., int, DefaultConstructorMarker)` ctor computes
9826/// defaulted args then chains `this(..)` mid-body — Java requires the
9827/// delegation first (对this的调用必须是构造器中的第一个语句, lark's
9828/// UserCustomStatusExtraParams$* family, 752 lines). Same fold the
9829/// class-ctor path runs; the enum branch of the pipeline used to skip
9830/// it (only strip_enum_ctor_super ran there).
9831pub fn fold_enum_default_arg_bridge(body: &mut Stmt) {
9832    let Stmt::Block(stmts) = body else { return };
9833    fold_default_arg_bridge(stmts);
9834}
9835
9836pub fn fix_ctor_delegation_arg_defs(body: &mut Stmt) {
9837    let Stmt::Block(stmts) = body else { return };
9838    // Shape C first: the Kotlin default-arg bridge (computations +
9839    // conditional overrides folded into the delegation args).
9840    fold_default_arg_bridge(stmts);
9841    // Only the top-level linear shape; the first statement needs no
9842    // repair and control flow ahead of the delegation belongs to the
9843    // conditional-super machinery.
9844    let Some(pos) = stmts.iter().position(is_bare_ctor_call) else {
9845        return;
9846    };
9847    if pos == 0 {
9848        return;
9849    }
9850    // Vars read by the delegation args, and the total read count (a var
9851    // read twice would duplicate a call evaluation on inline).
9852    let mut arg_vars: Vec<u32> = Vec::new();
9853    let mut total_reads = 0usize;
9854    {
9855        let Stmt::ExprStmt(Expr::Method { args, .. }) = &stmts[pos] else {
9856            return;
9857        };
9858        for a in args {
9859            let mut c = a.clone();
9860            deep_rewrite_reads(&mut c, &mut |x| {
9861                if let Expr::Local { var: v, .. } = x {
9862                    total_reads += 1;
9863                    if !arg_vars.contains(v) {
9864                        arg_vars.push(*v);
9865                    }
9866                }
9867            });
9868        }
9869    }
9870    if arg_vars.is_empty() || total_reads != arg_vars.len() {
9871        return;
9872    }
9873    // Vars DEFINED anywhere in the prefix (for the init-referenced
9874    // check below — an inlined init may not itself read prefix defs).
9875    let prefix_defs: Vec<u32> = stmts[..pos]
9876        .iter()
9877        .filter_map(|s| match s {
9878            Stmt::LocalDef { var, init: Some(_), .. } => Some(*var),
9879            _ => None,
9880        })
9881        .collect();
9882    // Per referenced var: exactly one prefix def, no other prefix
9883    // reader between def and delegation, the def's init reads no
9884    // prefix-def var, and the first post-delegation use is a top-level
9885    // write (becomes the declaration) or nothing at all.
9886    let mut plan: Vec<(u32, usize, Option<usize>)> = Vec::new();
9887    for v in arg_vars {
9888        let defs: Vec<usize> = stmts[..pos]
9889            .iter()
9890            .enumerate()
9891            .filter(|(i, _)| {
9892                matches!(&stmts[*i], Stmt::LocalDef { var, init: Some(_), .. } if *var == v)
9893            })
9894            .map(|(i, _)| i)
9895            .collect();
9896        if defs.len() > 1 {
9897            return; // ambiguous: multiple prefix defs
9898        }
9899        let Some(&q) = defs.first() else {
9900            continue; // a param: nothing to inline for this var
9901        };
9902        let init = match &stmts[q] {
9903            Stmt::LocalDef { init: Some(e), .. } => e.clone(),
9904            _ => return,
9905        };
9906        if stmts_read_var(&stmts[q + 1..pos], v) != 0 {
9907            return; // another reader between the def and the delegation
9908        }
9909        // The inlined init may not read a prefix-def var either — the
9910        // delegation lands ABOVE those defs after the hoist.
9911        let mut init_vars: Vec<u32> = Vec::new();
9912        deep_rewrite_reads(&mut { init.clone() }, &mut |x| {
9913            if let Expr::Local { var: vv, .. } = x {
9914                if !init_vars.contains(vv) {
9915                    init_vars.push(*vv);
9916                }
9917            }
9918        });
9919        if init_vars.iter().any(|iv| prefix_defs.contains(iv)) {
9920            return;
9921        }
9922        match stmts[pos + 1..]
9923            .iter()
9924            .position(|s| {
9925                matches!(s, Stmt::ExprStmt(Expr::Assign { target, .. })
9926                    if matches!(&**target, Expr::Local { var: vv, .. } if *vv == v))
9927            }) {
9928            Some(w) => {
9929                // Reads before the re-declaration would be dangling.
9930                if stmts_read_var(&stmts[pos + 1..pos + 1 + w], v) != 0 {
9931                    return;
9932                }
9933                plan.push((v, q, Some(pos + 1 + w)));
9934            }
9935            None => {
9936                // No later write: the def must be dead after inline.
9937                if stmts_read_var(&stmts[pos + 1..], v) != 0 {
9938                    return;
9939                }
9940                plan.push((v, q, None));
9941            }
9942        }
9943    }
9944    if plan.is_empty() {
9945        return;
9946    }
9947    // Inline the def inits into the delegation args.
9948    let values: Vec<(u32, Expr)> = plan
9949        .iter()
9950        .map(|(_v, q, _)| match &stmts[*q] {
9951            Stmt::LocalDef { var, init: Some(e), .. } => (*var, e.clone()),
9952            _ => unreachable!("plan entries carry a LocalDef with init"),
9953        })
9954        .collect();
9955    if let Stmt::ExprStmt(Expr::Method { args, .. }) = &mut stmts[pos] {
9956        for a in args {
9957            deep_rewrite_reads(a, &mut |x| {
9958                if let Expr::Local { var: v, .. } = x {
9959                    if let Some((_, e)) = values.iter().find(|(vv, _)| vv == v) {
9960                        *x = e.clone();
9961                    }
9962                }
9963            });
9964        }
9965    }
9966    // Convert the first writes into declarations BEFORE removing defs
9967    // (removals shift indices); then remove the defs in descending
9968    // order.
9969    for (v, _, w) in &plan {
9970        if let Some(w) = w {
9971            if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = &mut stmts[*w] {
9972                if matches!(&**target, Expr::Local { var: vv, .. } if vv == v) {
9973                    let taken = std::mem::replace(&mut **value, Expr::Const(ConstVal::Null));
9974                    stmts[*w] = Stmt::LocalDef {
9975                        var: *v,
9976                        init: Some(taken),
9977                        is_final: false,
9978                        force_type: false,
9979                    };
9980                }
9981            }
9982        }
9983    }
9984    let mut order: Vec<usize> = plan.iter().map(|(_, q, _)| *q).collect();
9985    order.sort_unstable();
9986    order.reverse();
9987    for q in order {
9988        stmts.remove(q);
9989    }
9990}
9991
9992/// An INT-typed method returning a boolean value: the register held the
9993/// boolean test result in one generation and the `if (v) 1 else 0` int
9994/// in another; booleanize picks the boolean generation and the plain
9995/// `return v;` breaks ("boolean无法转换为int", weixin Handle
9996/// onPausableTransaction — the source was `if (v) 1 else 0`). Wrap the
9997/// return in the recovering conditional.
9998/// Mirror of fix_int_returns: a BOOLEAN method returning a numeric-
9999/// typed value (register reuse left an int generation) wraps it
10000/// `x != 0` — the dex branch encoding, and the only legal Java form
10001/// (kh5/a `return v116_g53;` int无法转换为boolean family).
10002pub fn fix_bool_returns(vt: &VarTable, body: &mut Stmt) {
10003    walk_mut_deep(body, &mut |st| {
10004        if let Stmt::Return(Some(e)) = st {
10005            let numeric = match &*e {
10006                Expr::Local { var, ty } => {
10007                    let t = if (*var as usize) < vt.vars.len() {
10008                        vt.var(*var).ty.erased()
10009                    } else {
10010                        ty.erased()
10011                    };
10012                    matches!(
10013                        t,
10014                        JavaType::Int
10015                            | JavaType::Long
10016                            | JavaType::Short
10017                            | JavaType::Byte
10018                            | JavaType::Char
10019                    )
10020                }
10021                other => matches!(
10022                    other.type_ref().erased(),
10023                    JavaType::Int | JavaType::Long | JavaType::Short | JavaType::Byte
10024                        | JavaType::Char
10025                ),
10026            };
10027            if numeric {
10028                let taken = e.clone();
10029                *e = Expr::Bin {
10030                    op: BinOp::Ne,
10031                    l: Box::new(taken),
10032                    r: Box::new(Expr::Const(ConstVal::Int(0))),
10033                    ty: Some(TypeRef::J(JavaType::Boolean)),
10034                };
10035            }
10036        }
10037    });
10038}
10039
10040pub fn fix_int_returns(vt: &VarTable, body: &mut Stmt) {
10041    walk_mut_deep(body, &mut |st| {
10042        if let Stmt::Return(Some(e)) = st {
10043            let is_bool = match e {
10044                Expr::Local { var, .. } => {
10045                    (*var as usize) < vt.vars.len()
10046                        && vt.vars[*var as usize].ty.erased() == JavaType::Boolean
10047                }
10048                Expr::Method { desc, .. } => desc.ret == JavaType::Boolean,
10049                _ => false,
10050            };
10051            if is_bool {
10052                *e = Expr::Cond {
10053                    c: Box::new(e.clone()),
10054                    t: Box::new(Expr::Const(ConstVal::Int(1))),
10055                    f: Box::new(Expr::Const(ConstVal::Int(0))),
10056                };
10057            }
10058        }
10059    });
10060}
10061
10062/// Generation splitting: when an assignment's value type is
10063/// INCOMPATIBLE with the var's declared type, mint a FRESH var for the
10064/// new generation instead of widening (widening was measured twice as
10065/// net-negative — every use-site cast gap re-triggers javac's
10066/// error-type contagion). Reads after the split point follow the
10067/// active generation; control-flow joins intersect; loops run a
10068/// 3-round fixpoint so the back-edge carries the tail generation to
10069/// the head reads (the loop-state-tuple shape, qf5/v0's
10070/// `u0 = (qf5.u0) get;` … `u0 = looper2;`).
10071///
10072/// Split condition (reference incompatibility or a primitive/value
10073/// mix — the instanceof-corruption shape): both specific references of
10074/// different classes, or prim→ref. NOT numeric-vs-numeric (promotion
10075/// handles those) and NOT Object targets (assignable — the narrowing
10076/// cast pass covers the value side).
10077pub fn split_generations(vt: &mut VarTable, body: &mut Stmt, pool: &DexPool) {
10078    let mut counter: u32 = 0;
10079    let mut gen: std::collections::HashMap<u32, u32> = std::collections::HashMap::default();
10080    split_walk_stmt(body, vt, &mut gen, &mut counter, pool);
10081}
10082
10083/// Should the assignment `target = value` split the target?
10084fn split_needed(vt: &VarTable, target: u32, value: &Expr, pool: &DexPool) -> bool {
10085    let n = vt.vars.len();
10086    if target as usize >= n {
10087        return false;
10088    }
10089    let declared = vt.vars[target as usize].ty.erased();
10090    let vt_is_ref = |t: &JavaType| matches!(t, JavaType::Object(_) | JavaType::Array(_));
10091    // Locals consult the VarTable: the embedded ty is a lift-time
10092    // snapshot, stale after booleanize converts a copy SOURCE (the
10093    // post-booleanize re-split below relies on seeing `v131` as the
10094    // boolean it became). Bitwise Or/And/Xor over boolean operands
10095    // carry the same staleness at the EXPRESSION level (dex lowered
10096    // Kotlin's non-short-circuit `or` to int `|` — the embedded Bin ty
10097    // stays Int after booleanize converts the operands): `int v18 =
10098    // delete | delete2` must split the target generation.
10099    fn side_bool(e: &Expr, vt: &VarTable) -> bool {
10100        match e {
10101            Expr::Local { var, .. } if (*var as usize) < vt.vars.len() => {
10102                matches!(vt.var(*var).ty.erased(), JavaType::Boolean)
10103            }
10104            Expr::Const(ConstVal::Int(0 | 1)) => true,
10105            Expr::Bin { op: BinOp::Or | BinOp::And | BinOp::Xor, l, r, .. } => {
10106                side_bool(l, vt) && side_bool(r, vt)
10107            }
10108            other => matches!(other.type_ref().erased(), JavaType::Boolean),
10109        }
10110    }
10111    let value_ty = match value {
10112        Expr::Local { var, ty } if (*var as usize) < vt.vars.len() => vt.var(*var).ty.erased(),
10113        Expr::Bin {
10114            op: BinOp::Or | BinOp::And | BinOp::Xor,
10115            l,
10116            r,
10117            ..
10118        } if side_bool(l, vt) && side_bool(r, vt) => JavaType::Boolean,
10119        other => other.type_ref().erased(),
10120    };
10121    if vt_is_ref(&declared) && vt_is_ref(&value_ty) {
10122        // Both references: split only when incompatible — different
10123        // classes where neither side is Object AND the value is not a
10124        // SUBTYPE of the declared type. An assignable store needs no
10125        // fresh generation; splitting one minted an orphan at an
10126        // if-join whose sibling arm kept the base (rename_gen_back's
10127        // primitive-only gate refused the undo, drop_dead_locals then
10128        // stripped the orphan to a bare allocation): pq0/g's bridge
10129        // `map2 = new LinkedHashMap()` against the HashMap-typed phi
10130        // rendered `new LinkedHashMap();` and the arm lost its store.
10131        let obj = JavaType::Object("java/lang/Object".into());
10132        if declared == value_ty || declared == obj || value_ty == obj {
10133            return false;
10134        }
10135        // A FRESH ALLOCATION whose class relation to the declared type
10136        // is UNKNOWABLE (a framework↔framework pair — LinkedHashMap
10137        // into a HashMap-typed phi): the dex stored the instance into
10138        // this register flow, so the store was verifier-legal; the
10139        // split cannot know better and orphaned the generation (the
10140        // if-join undo is primitive-only; the dropper stripped the
10141        // orphan to a bare `new LinkedHashMap();` and pq0/g's bridge
10142        // lost the else-arm store, killing the whole branched-super
10143        // merge). Pool-known pairs KEEP the split: the generation's
10144        // precise type is what downstream subtype sinks need (weibo
10145        // `v7 = new $reportWhenComplete$1(..)` into a ContinuationImpl
10146        // phi — exempting it made `v9($1) = v7` inconvertible, +50).
10147        if let (JavaType::Object(v), JavaType::Object(d)) = (&value_ty, &declared) {
10148            if matches!(value, Expr::New { .. })
10149                && (pool.get(v).is_none() || pool.get(d).is_none())
10150            {
10151                return false;
10152            }
10153        }
10154        true
10155    } else {
10156        // Primitive/reference mix: the value generation has a kind the
10157        // target can never accept — split (the declared type stays with
10158        // the old generation's readers).
10159        if vt_is_ref(&declared) != vt_is_ref(&value_ty) {
10160            return true;
10161        }
10162        // Boolean/numeric mix: `boolean` and `int` have NO cast between
10163        // them in Java, so a register reused across the kinds can only
10164        // be expressed as two generations (weixin ConstraintLayout
10165        // `boolean v60; int v56; v60 = v56;` — 9.5k bool↔int lines once
10166        // the obscuring suppression lifted). 0/1 constants are the
10167        // boolean encoding, not a numeric generation — never split on
10168        // those (booleanize owns them).
10169        let d_bool = matches!(declared, JavaType::Boolean);
10170        let v_bool = matches!(value_ty, JavaType::Boolean);
10171        let zero_one = matches!(value, Expr::Const(ConstVal::Int(0 | 1)));
10172        d_bool != v_bool && !zero_one
10173    }
10174}
10175
10176fn rewrite_gen_reads(e: &mut Expr, gen: &std::collections::HashMap<u32, u32>, vt: &VarTable) {
10177    deep_rewrite(e, &mut |x| {
10178        if let Expr::Local { var, ty } = x {
10179            if let Some(&to) = gen.get(var) {
10180                if to != *var {
10181                    *var = to;
10182                    *ty = vt.var(to).ty.clone();
10183                }
10184            }
10185        }
10186    });
10187}
10188
10189fn do_split(
10190    vt: &mut VarTable,
10191    target: &mut Expr,
10192    old: u32,
10193    value_ty: &JavaType,
10194    gen: &mut std::collections::HashMap<u32, u32>,
10195    counter: &mut u32,
10196) {
10197    *counter += 1;
10198    let info = &vt.vars[old as usize];
10199    let id = vt.vars.len() as u32;
10200    let name = format!("{}_g{}", info.name, counter);
10201    let slot = info.slot;
10202    // A split generation is a fresh LOCAL, not a parameter — even when
10203    // the parent register is one (the param identity stays on the
10204    // original var id; the generation only carries a later write). The
10205    // inherited flag made booleanize (and every other param-skip pass)
10206    // permanently ignore param-register generations: weixin v2/i's
10207    // `int v264_g160_g7 = !v200` with `!= 0` reads (bool→int assign
10208    // family, 2.8k lines).
10209    let is_param = false;
10210    vt.vars.push(jdc_core::var::VarInfo {
10211        id,
10212        slot,
10213        name,
10214        ty: TypeRef::J(value_ty.clone()),
10215        is_param,
10216        range_start: 0,
10217        range_end: u16::MAX,
10218        synthetic_name: true,
10219    });
10220    while vt.by_slot.len() <= slot as usize {
10221        vt.by_slot.push(Vec::new());
10222    }
10223    vt.by_slot[slot as usize].push((0, u16::MAX, id));
10224    gen.insert(old, id);
10225    if let Expr::Local { var, ty, .. } = target {
10226        *var = id;
10227        *ty = TypeRef::J(value_ty.clone());
10228    }
10229}
10230
10231/// Undo a DIVERGED generation split: rewrite every occurrence of the
10232/// freshly-minted generation `from` back to the pre-if var `to` within
10233/// `s` (both read positions and assignment targets, plus LocalDef var
10234/// fields). At a control-flow join only splits BOTH branches produced
10235/// survive (the phi merges on the shared var); a gen one branch minted
10236/// alone does not. But `do_split` already rewrote that branch's write
10237/// target IN PLACE, so without this undo the branch stores into a
10238/// write-only orphan gen that no confluence reader ever reads — the
10239/// value is silently lost AND the orphan renders undeclared (kt5.b
10240/// Kotlin default-arg bridge: `v18_g1 = p6x` in then vs `v18 = false`
10241/// in else, `this(.., v18, ..)` reads only else's value).
10242fn rename_gen_back(s: &mut Stmt, from: u32, to: u32, vt: &VarTable) {
10243    if from == to || (to as usize) >= vt.vars.len() {
10244        return;
10245    }
10246    let to_ty = vt.vars[to as usize].ty.clone();
10247    walk_mut_deep(s, &mut |st| {
10248        if let Stmt::LocalDef { var, .. } = st {
10249            if *var == from {
10250                *var = to;
10251            }
10252        }
10253    });
10254    rewrite_exprs(s, &mut |e| {
10255        deep_rewrite(e, &mut |x| {
10256            if let Expr::Local { var, ty } = x {
10257                if *var == from {
10258                    *var = to;
10259                    *ty = to_ty.clone();
10260                }
10261            }
10262        });
10263    });
10264}
10265
10266/// Remove an undone generation's `by_slot` segment entry so slot-based
10267/// resolution can never hand back the now-unreferenced gen id (its
10268/// VarInfo stays — ids index `vars` — but nothing may select it).
10269fn drop_gen_slot(vt: &mut VarTable, dead: u32) {
10270    if (dead as usize) >= vt.vars.len() {
10271        return;
10272    }
10273    let slot = vt.vars[dead as usize].slot as usize;
10274    if let Some(seg) = vt.by_slot.get_mut(slot) {
10275        seg.retain(|&(_, _, id)| id != dead);
10276    }
10277}
10278
10279/// Type gate for `rename_gen_back`: merging gen `v` back onto base `k`
10280/// is only safe when the base can carry the gen's value — identical
10281/// types, or BOTH primitives (the bool/int register-reuse family that
10282/// booleanize + the cast fixers reconcile; the Kotlin bridge case,
10283/// kt5.b `int v18` receiving `boolean p6x`). A reference-vs-primitive
10284/// or cross-class merge would forge `String k = <boolean>`-style
10285/// assignments that never compile (the lark/weibo/reqable regression
10286/// of the ungated first cut: +8.4k `不兼容的类型`).
10287fn undo_type_safe(vt: &VarTable, k: u32, v: u32) -> bool {
10288    if (k as usize) >= vt.vars.len() || (v as usize) >= vt.vars.len() {
10289        return false;
10290    }
10291    let kt = vt.vars[k as usize].ty.erased();
10292    let vt_ty = vt.vars[v as usize].ty.erased();
10293    if kt == vt_ty {
10294        return true;
10295    }
10296    let is_prim = |t: &JavaType| {
10297        matches!(
10298            t,
10299            JavaType::Int
10300                | JavaType::Long
10301                | JavaType::Short
10302                | JavaType::Byte
10303                | JavaType::Char
10304                | JavaType::Float
10305                | JavaType::Double
10306                | JavaType::Boolean
10307        )
10308    };
10309    is_prim(&kt) && is_prim(&vt_ty)
10310}
10311
10312fn split_walk_stmt(
10313    st: &mut Stmt,
10314    vt: &mut VarTable,
10315    gen: &mut std::collections::HashMap<u32, u32>,
10316    counter: &mut u32,
10317    pool: &DexPool,
10318) {
10319    match st {
10320        Stmt::Block(v) => {
10321            for x in v.iter_mut() {
10322                split_walk_stmt(x, vt, gen, counter, pool);
10323            }
10324        }
10325        Stmt::ExprStmt(e) => {
10326            rewrite_gen_reads(e, gen, vt);
10327            if let Expr::Assign {
10328                target,
10329                value,
10330                op: AssignOp::Plain,
10331                ..
10332            } = e
10333            {
10334                if let Expr::Local { var, .. } = &**target {
10335                    let old = *var;
10336                    if split_needed(vt, old, value, pool) {
10337                        let value_ty = value.type_ref().erased();
10338                        do_split(vt, target, old, &value_ty, gen, counter);
10339                    }
10340                }
10341            }
10342        }
10343        Stmt::LocalDef { var, init, .. } => {
10344            if let Some(e) = init {
10345                rewrite_gen_reads(e, gen, vt);
10346                if split_needed(vt, *var, e, pool) {
10347                    // The DEF mints its own generation: retarget the def
10348                    // to a fresh var (the bare decl of the ORIGINAL stays
10349                    // for its earlier readers).
10350                    let value_ty = e.type_ref().erased();
10351                    let old = *var;
10352                    let mut fake = Expr::Local {
10353                        var: old,
10354                        ty: vt.vars[old as usize].ty.clone(),
10355                    };
10356                    do_split(vt, &mut fake, old, &value_ty, gen, counter);
10357                    if let Expr::Local { var: nv, .. } = fake {
10358                        *var = nv;
10359                    }
10360                }
10361            }
10362        }
10363        Stmt::If {
10364            cond,
10365            then_stmt,
10366            else_stmt,
10367            ..
10368        } => {
10369            rewrite_gen_reads(cond, gen, vt);
10370            let then_floor = vt.vars.len() as u32;
10371            let mut g_then = gen.clone();
10372            split_walk_stmt(then_stmt, vt, &mut g_then, counter, pool);
10373            let else_floor = vt.vars.len() as u32;
10374            let mut g_else = gen.clone();
10375            if let Some(e) = else_stmt {
10376                split_walk_stmt(e, vt, &mut g_else, counter, pool);
10377            }
10378            // Undo splits that DIVERGED at this join (see
10379            // rename_gen_back), with TWO gates learned the hard way
10380            // (first cut regressed lark +8.4k):
10381            //  1. the SIBLING KEPT THE BASE — `g_else[k]` equals the
10382            //     entry mapping, i.e. only this branch minted a fresh
10383            //     gen for k. When both branches split, each orphan is
10384            //     declared by ensure_declared at its own value type and
10385            //     compiles; renaming them back onto the base would
10386            //     force cross-kind assignments (`int k = <boolean>`).
10387            //  2. the fresh gen is WRITE-ONLY in the branch — a gen
10388            //     read inside its branch renders fine on its own.
10389            // The gated shape is the Kotlin default-arg bridge phi
10390            // loss: then `v_g = p6x` / else `v = false` / confluence
10391            // reads v — the then value was silently dropped AND v_g
10392            // rendered undeclared (kt5.b). Undo restores the clean
10393            // diamond so fold_default_arg_bridge can fire.
10394            let then_reads = (else_floor > then_floor)
10395                .then(|| read_counts_all(std::slice::from_ref(&**then_stmt)));
10396            let mut undo_then: Vec<(u32, u32)> = g_then
10397                .iter()
10398                .filter(|(&k, &v)| {
10399                    v >= then_floor
10400                        && v < else_floor
10401                        && g_else.get(&k).copied() == gen.get(&k).copied()
10402                        && undo_type_safe(vt, k, v)
10403                        && then_reads
10404                            .as_ref()
10405                            .map(|m| m.get(&v).copied().unwrap_or(0) == 0)
10406                            .unwrap_or(true)
10407                })
10408                .map(|(&k, &v)| (v, k))
10409                .collect();
10410            undo_then.sort_unstable_by_key(|&(v, _)| std::cmp::Reverse(v));
10411            for &(v, k) in &undo_then {
10412                rename_gen_back(then_stmt, v, k, vt);
10413                drop_gen_slot(vt, v);
10414            }
10415            if let Some(e) = else_stmt.as_deref_mut() {
10416                let else_reads = ((vt.vars.len() as u32) > else_floor)
10417                    .then(|| read_counts_all(std::slice::from_ref(&*e)));
10418                let mut undo_else: Vec<(u32, u32)> = g_else
10419                    .iter()
10420                    .filter(|(&k, &v)| {
10421                        v >= else_floor
10422                            && g_then.get(&k).copied() == gen.get(&k).copied()
10423                            && undo_type_safe(vt, k, v)
10424                            && else_reads
10425                                .as_ref()
10426                                .map(|m| m.get(&v).copied().unwrap_or(0) == 0)
10427                                .unwrap_or(true)
10428                    })
10429                    .map(|(&k, &v)| (v, k))
10430                    .collect();
10431                undo_else.sort_unstable_by_key(|&(v, _)| std::cmp::Reverse(v));
10432                for &(v, k) in &undo_else {
10433                    rename_gen_back(e, v, k, vt);
10434                    drop_gen_slot(vt, v);
10435                }
10436            }
10437            // Join: keep only splits BOTH paths produced (an absent else
10438            // is a fall-through path that keeps the pre-if generation).
10439            gen.retain(|k, v| g_then.get(k) == Some(v) && g_else.get(k) == Some(v));
10440        }
10441        Stmt::While { cond, body, .. } | Stmt::DoWhile { cond, body, .. } => {
10442            // 3-round fixpoint: round N walks the cond+body with the
10443            // accumulated gen — the back-edge carries the tail
10444            // generation into the head reads of the next round.
10445            let entry = gen.clone();
10446            for _ in 0..3 {
10447                rewrite_gen_reads(cond, gen, vt);
10448                split_walk_stmt(body, vt, gen, counter, pool);
10449            }
10450            // After the loop the pre-loop generation is what a
10451            // zero-iteration execution left — conservative.
10452            *gen = entry;
10453        }
10454        Stmt::For { init, body, .. } => {
10455            for x in init.iter_mut() {
10456                split_walk_stmt(x, vt, gen, counter, pool);
10457            }
10458            let entry = gen.clone();
10459            for _ in 0..3 {
10460                split_walk_stmt(body, vt, gen, counter, pool);
10461            }
10462            *gen = entry;
10463        }
10464        Stmt::ForEach { body, .. } => {
10465            let entry = gen.clone();
10466            for _ in 0..3 {
10467                split_walk_stmt(body, vt, gen, counter, pool);
10468            }
10469            *gen = entry;
10470        }
10471        Stmt::Switch { cases, default, .. } => {
10472            let entry = gen.clone();
10473            for c in cases.iter_mut() {
10474                let mut g_case = entry.clone();
10475                for x in c.body.iter_mut() {
10476                    split_walk_stmt(x, vt, &mut g_case, counter, pool);
10477                }
10478                gen.retain(|k, v| g_case.get(k) == Some(v));
10479            }
10480            if let Some(d) = default {
10481                let mut g_def = entry.clone();
10482                split_walk_stmt(d, vt, &mut g_def, counter, pool);
10483                gen.retain(|k, v| g_def.get(k) == Some(v));
10484            }
10485        }
10486        Stmt::Try {
10487            body,
10488            catches,
10489            finally,
10490        } => {
10491            let entry = gen.clone();
10492            let mut g_try = entry.clone();
10493            split_walk_stmt(body, vt, &mut g_try, counter, pool);
10494            let mut g_all = g_try.clone();
10495            for c in catches.iter_mut() {
10496                // The handler runs with the PRE-try state (the exception
10497                // may fire anywhere in the try).
10498                let mut g_c = entry.clone();
10499                split_walk_stmt(&mut c.body, vt, &mut g_c, counter, pool);
10500                g_all.retain(|k, v| g_c.get(k) == Some(v));
10501            }
10502            *gen = g_all;
10503            if let Some(f) = finally {
10504                split_walk_stmt(f, vt, gen, counter, pool);
10505            }
10506        }
10507        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => {
10508            split_walk_stmt(body, vt, gen, counter, pool);
10509        }
10510        Stmt::Return(Some(e)) | Stmt::Throw(e) => {
10511            rewrite_gen_reads(e, gen, vt);
10512        }
10513        _ => {}
10514    }
10515}
10516
10517/// Framework exception super edge — the embedded API database
10518/// (fwdb) replaced the hand-written ~110-entry JDK/Android exception
10519/// supers table: real extends edges for EVERY framework class, so
10520/// multi-catch dedupe now covers chains the table missed (and cannot
10521/// carry a wrong entry — the table's failure mode was dropping a live
10522/// alternative).
10523fn fw_super(c: &str) -> Option<&'static str> {
10524    crate::fwdb::super_of(c)
10525}
10526
10527/// `sub <: sup` for exception-class names, combining the pool
10528/// hierarchy (which also matches framework ancestor NAMES on the way
10529/// up, so pool subclasses of framework exceptions resolve) with the
10530/// tabled framework chains for pairs where both sides are outside the
10531/// dex.
10532fn exc_subtype(sub: &str, sup: &str, pool: &DexPool) -> bool {
10533    if sub == sup {
10534        return true;
10535    }
10536    if pool.is_subtype(sub, sup) {
10537        return true;
10538    }
10539    let mut cur = sub;
10540    for _ in 0..24 {
10541        let Some(next) = fw_super(cur) else { break };
10542        if next == sup {
10543            return true;
10544        }
10545        cur = next;
10546    }
10547    false
10548}
10549
10550/// Drop multi-catch alternatives shadowed by another alternative in
10551/// the same handler (JLS 14.20.2 forbids subtype-related
10552/// alternatives; the dex verifier does not). Exact duplicates collapse
10553/// to the first occurrence. The caught set is unchanged, so this is
10554/// semantics-preserving by construction.
10555pub fn dedupe_multicatch(s: &mut Stmt, pool: &DexPool) {
10556    dedupe_multicatch_walk(s, pool);
10557}
10558
10559fn dedupe_multicatch_walk(s: &mut Stmt, pool: &DexPool) {
10560    match s {
10561        Stmt::Try {
10562            body,
10563            catches,
10564            finally,
10565        }
10566        | Stmt::TryWithResources {
10567            body,
10568            catches,
10569            finally,
10570            ..
10571        } => {
10572            dedupe_multicatch_walk(body, pool);
10573            for c in catches.iter_mut() {
10574                if c.exc.len() > 1 {
10575                    let mut keep: Vec<std::sync::Arc<str>> = Vec::with_capacity(c.exc.len());
10576                    for e in c.exc.iter() {
10577                        let shadowed = c.exc.iter().any(|o| {
10578                            o.as_ref() != e.as_ref() && exc_subtype(e, o, pool)
10579                        });
10580                        if !shadowed && !keep.iter().any(|k| k.as_ref() == e.as_ref()) {
10581                            keep.push(e.clone());
10582                        }
10583                    }
10584                    c.exc = keep;
10585                }
10586                dedupe_multicatch_walk(&mut c.body, pool);
10587            }
10588            if let Some(f) = finally {
10589                dedupe_multicatch_walk(f, pool);
10590            }
10591        }
10592        Stmt::Block(v) => {
10593            for x in v.iter_mut() {
10594                dedupe_multicatch_walk(x, pool);
10595            }
10596        }
10597        Stmt::If {
10598            then_stmt,
10599            else_stmt,
10600            ..
10601        } => {
10602            dedupe_multicatch_walk(then_stmt, pool);
10603            if let Some(e) = else_stmt {
10604                dedupe_multicatch_walk(e, pool);
10605            }
10606        }
10607        Stmt::While { body, .. }
10608        | Stmt::DoWhile { body, .. }
10609        | Stmt::For { body, .. }
10610        | Stmt::ForEach { body, .. }
10611        | Stmt::Labeled { body, .. }
10612        | Stmt::Synchronized { body, .. } => dedupe_multicatch_walk(body, pool),
10613        Stmt::Switch { cases, default, .. } => {
10614            for c in cases.iter_mut() {
10615                for x in c.body.iter_mut() {
10616                    dedupe_multicatch_walk(x, pool);
10617                }
10618            }
10619            if let Some(d) = default {
10620                dedupe_multicatch_walk(d, pool);
10621            }
10622        }
10623        _ => {}
10624    }
10625}
10626
10627/// Dangling `break L<id>`: a Goto whose paired Label/Labeled-wrap was
10628/// lost to structure degradation prints `break L<id>;` against an
10629/// undeclared label ("未定义的标签", weibo 371/lark 85/weixin 535).
10630/// Inside a loop the goto-to-loop-exit shape degrades to a plain
10631/// `break` — compilable, and the dominant original semantic. Outside
10632/// any loop the jump is irreducible flow (weixin protobuf state
10633/// machines: `goto` into a lost loop middle / dispatcher block — the
10634/// target exists but not at a position Java can express); it renders
10635/// as an honest `$DDC:` marker comment instead of a guaranteed
10636/// undefined-label error, and the block's tail after it is dropped
10637/// (the original control flow never reached those statements).
10638pub fn resolve_dangling_gotos(s: &mut Stmt) {
10639    let mut labels: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
10640    // walk_all is read-only — the whole-body clone this used to make
10641    // (4.6k profile samples of Stmt::clone) bought nothing.
10642    walk_all(s, &mut |st| {
10643        if let Stmt::Label(id) = st {
10644            labels.insert(*id);
10645        }
10646    });
10647    resolve_gotos_walk(s, &labels, 0);
10648}
10649
10650fn resolve_gotos_walk(s: &mut Stmt, labels: &jdc_core::FxHashSet<u32>, loop_depth: u32) {
10651    let depth = match s {
10652        Stmt::While { .. } | Stmt::DoWhile { .. } | Stmt::For { .. } | Stmt::ForEach { .. } => {
10653            loop_depth + 1
10654        }
10655        _ => loop_depth,
10656    };
10657    match s {
10658        Stmt::Goto(id) => {
10659            if labels.contains(id) {
10660                return; // valid pair — keep
10661            }
10662            if depth > 0 {
10663                *s = Stmt::Break(None);
10664            } else {
10665                *s = Stmt::Comment(format!(
10666                    "$DDC: unresolved jump to block {} (irreducible control flow)",
10667                    id
10668                ));
10669            }
10670        }
10671        Stmt::Block(v) => {
10672            let mut i = 0usize;
10673            while i < v.len() {
10674                // A depth-0 dangling Goto becomes a marker comment and
10675                // the original flow never reached the siblings after
10676                // it — dropping them keeps the rendered reachability
10677                // faithful (they were dead in the dex too).
10678                let dangling_here =
10679                    depth == 0 && matches!(&v[i], Stmt::Goto(id) if !labels.contains(id));
10680                resolve_gotos_walk(&mut v[i], labels, depth);
10681                if dangling_here {
10682                    v.truncate(i + 1);
10683                    break;
10684                }
10685                i += 1;
10686            }
10687        }
10688        Stmt::If { then_stmt, else_stmt, .. } => {
10689            resolve_gotos_walk(then_stmt, labels, depth);
10690            if let Some(e) = else_stmt {
10691                resolve_gotos_walk(e, labels, depth);
10692            }
10693        }
10694        Stmt::While { body, .. }
10695        | Stmt::DoWhile { body, .. }
10696        | Stmt::For { body, .. }
10697        | Stmt::ForEach { body, .. }
10698        | Stmt::Labeled { body, .. }
10699        | Stmt::Synchronized { body, .. } => resolve_gotos_walk(body, labels, depth),
10700        Stmt::Try {
10701            body,
10702            catches,
10703            finally,
10704        } => {
10705            resolve_gotos_walk(body, labels, depth);
10706            for c in catches.iter_mut() {
10707                resolve_gotos_walk(&mut c.body, labels, depth);
10708            }
10709            if let Some(f) = finally {
10710                resolve_gotos_walk(f, labels, depth);
10711            }
10712        }
10713        Stmt::Switch { cases, default, .. } => {
10714            for c in cases.iter_mut() {
10715                for x in c.body.iter_mut() {
10716                    resolve_gotos_walk(x, labels, depth);
10717                }
10718            }
10719            if let Some(d) = default {
10720                resolve_gotos_walk(d, labels, depth);
10721            }
10722        }
10723        _ => {}
10724    }
10725}
10726
10727/// READS of `v` across the statements (assignment targets excluded).
10728/// All-var read counts in ONE pass (reads semantics of stmts_read_var)
10729/// — for gates that test many candidate vars against the same subtree
10730/// (split_walk's undo filters ran a full arm walk PER candidate).
10731fn read_counts_all(stmts: &[Stmt]) -> std::collections::HashMap<u32, usize> {
10732    let mut m = std::collections::HashMap::new();
10733    for s in stmts {
10734        visit_stmt_exprs_ro(s, &mut |e| {
10735            visit_exprs_reads(e, &mut |x| {
10736                if let Expr::Local { var, .. } = x {
10737                    *m.entry(*var).or_insert(0) += 1;
10738                }
10739            });
10740        });
10741    }
10742    m
10743}
10744
10745fn stmts_read_var(stmts: &[Stmt], v: u32) -> usize {
10746    let mut n = 0usize;
10747    for s in stmts {
10748        visit_stmt_exprs_ro(s, &mut |e| {
10749            visit_exprs_reads(e, &mut |x| {
10750                if let Expr::Local { var: vv, .. } = x {
10751                    if *vv == v {
10752                        n += 1;
10753                    }
10754                }
10755            });
10756        });
10757    }
10758    n
10759}
10760
10761pub fn fix_ctor_conditional_super(body: &mut Stmt) {
10762    // A single-statement body can arrive UNWRAPPED (bare `If` — the
10763    // gb6/e throw-guard family); the shape scans need a statement list.
10764    if !matches!(body, Stmt::Block(_)) {
10765        let inner = std::mem::replace(body, Stmt::Block(Vec::new()));
10766        *body = Stmt::Block(vec![inner]);
10767    }
10768    let Stmt::Block(stmts) = body else { return };
10769    // Unwrap single-statement Block wrappers: the structurer can nest
10770    // the whole body one level deeper, hiding the top-level If from the
10771    // shape scans (cleanup would flatten it — but runs AFTER this pass).
10772    while stmts.len() == 1 {
10773        let Stmt::Block(inner) = &stmts[0] else { break };
10774        *stmts = inner.clone();
10775    }
10776    if stmts.is_empty() {
10777        return;
10778    }
10779    if is_bare_ctor_call(stmts.first().unwrap()) {
10780        dedupe_ctor_delegations(body);
10781        return;
10782    }
10783    if try_linear_super_inline(stmts) {
10784        return;
10785    }
10786    try_merge_branched_super(stmts);
10787}
10788
10789/// Shape C: the delegation is already first — strip duplicate copies
10790/// left by R8 path-duplication (Kotlin default-arg bridge ctors: ft5/j
10791/// had `super()` leading PLUS another inside a branch — "对super的调用
10792/// 必须是构造器中的第一个语句" at the copy). Every path delegates
10793/// through the leading call, so structurally-identical (or empty-args,
10794/// same-kind) copies nested in blocks/ifs are removed, never moved.
10795/// Runs both ahead of the shape-A/B attempt and AFTER the plain hoist
10796/// (fix_ctor_super_first) — the hoist is what puts the leading
10797/// delegation in place for path-duplicated ctors.
10798/// A ctor that ALREADY delegates in its first statement can hold no
10799/// other `this(..)`/`super(..)` — Baidu Titan hotpatch instrumentation
10800/// injects a conditional re-delegation inside the `if ($ic != null)`
10801/// guard of every ctor (baidusearch ctor-not-first ×61,922 across
10802/// 21,813 files: `super(context); if ($ic != null) { ..; if
10803/// ((flag&1)!=0) { super((Context) callArgs[0]); ..; return; } }`).
10804/// The guard is dead code on the unpatched runtime path (and the
10805/// decompiled output has no patch runtime at all); dropping the
10806/// secondary delegation keeps the real semantics and restores the
10807/// Java first-statement rule. Runs after dedupe_ctor_delegations
10808/// (identical copies) and before extract_branched_delegation_helper
10809/// — gated on a LEADING delegation, so branched-delegation ctors
10810/// (no leading one) still get the helper treatment. Nested class
10811/// declarations are separate ctor scopes and are not entered.
10812pub fn strip_secondary_delegations(body: &mut Stmt) {
10813    let Stmt::Block(stmts) = body else { return };
10814    if !stmts.first().is_some_and(is_bare_ctor_call) {
10815        return;
10816    }
10817    fn strip_in(s: &mut Stmt) {
10818        if is_bare_ctor_call(s) {
10819            *s = Stmt::Block(Vec::new());
10820            return;
10821        }
10822        match s {
10823            Stmt::Block(v) => v.iter_mut().for_each(strip_in),
10824            Stmt::If {
10825                then_stmt,
10826                else_stmt,
10827                ..
10828            } => {
10829                strip_in(then_stmt);
10830                if let Some(e) = else_stmt {
10831                    strip_in(e);
10832                }
10833            }
10834            Stmt::While { body, .. }
10835            | Stmt::DoWhile { body, .. }
10836            | Stmt::ForEach { body, .. }
10837            | Stmt::Synchronized { body, .. }
10838            | Stmt::Labeled { body, .. } => strip_in(body),
10839            Stmt::For { init, body, .. } => {
10840                init.iter_mut().for_each(strip_in);
10841                strip_in(body);
10842            }
10843            Stmt::Switch { cases, default, .. } => {
10844                for c in cases.iter_mut() {
10845                    c.body.iter_mut().for_each(strip_in);
10846                }
10847                if let Some(d) = default {
10848                    strip_in(d);
10849                }
10850            }
10851            Stmt::Try {
10852                body,
10853                catches,
10854                finally,
10855            }
10856            | Stmt::TryWithResources {
10857                body,
10858                catches,
10859                finally,
10860                ..
10861            } => {
10862                strip_in(body);
10863                for c in catches.iter_mut() {
10864                    strip_in(&mut c.body);
10865                }
10866                if let Some(f) = finally {
10867                    strip_in(f);
10868                }
10869            }
10870            _ => {}
10871        }
10872    }
10873    for st in stmts.iter_mut().skip(1) {
10874        strip_in(st);
10875    }
10876}
10877
10878pub fn dedupe_ctor_delegations(body: &mut Stmt) {
10879    let Stmt::Block(stmts) = body else { return };
10880    let Some(d0) = stmts.first().and_then(|s| match s {
10881        Stmt::ExprStmt(e) if is_delegation_expr(e) => Some(e.clone()),
10882        _ => None,
10883    }) else {
10884        return;
10885    };
10886    fn rec(s: &mut Stmt, d0: &Expr) {
10887        match s {
10888            Stmt::Block(v) => {
10889                v.retain(|x| {
10890                    !matches!(x, Stmt::ExprStmt(e) if is_delegation_expr(e) && same_delegation(e, d0))
10891                });
10892                for x in v.iter_mut() {
10893                    rec(x, d0);
10894                }
10895            }
10896            Stmt::If { then_stmt, else_stmt, .. } => {
10897                rec(then_stmt, d0);
10898                if let Some(e) = else_stmt {
10899                    rec(e, d0);
10900                }
10901            }
10902            _ => {}
10903        }
10904    }
10905    // Top-level copies first (rec only strips copies NESTED in
10906    // blocks/ifs — a duplicate `super()` sitting directly in the body
10907    // list fell through its `_` arm: weixin ft5/j's second super()).
10908    {
10909        let d0r = &d0;
10910        let mut idx = 0usize;
10911        stmts.retain(|s| {
10912            idx += 1;
10913            idx == 1
10914                || !(matches!(s, Stmt::ExprStmt(e)
10915                    if is_delegation_expr(e) && same_delegation(e, d0r)))
10916        });
10917    }
10918    for s in stmts.iter_mut().skip(1) {
10919        rec(s, &d0);
10920    }
10921}
10922
10923/// Structural delegation identity for dedup: same kind/target and equal
10924/// args — or both arg-less (`super()` copies across duplicated paths
10925/// render with locally-renamed args yet delegate identically).
10926fn same_delegation(a: &Expr, b: &Expr) -> bool {
10927    match (a, b) {
10928        (
10929            Expr::Method { name: n1, cls: c1, is_super: s1, args: a1, .. },
10930            Expr::Method { name: n2, cls: c2, is_super: s2, args: a2, .. },
10931        ) => {
10932            &**n1 == "<init>"
10933                && &**n2 == "<init>"
10934                && c1 == c2
10935                && s1 == s2
10936                && (a1 == a2 || (a1.is_empty() && a2.is_empty()))
10937        }
10938        _ => false,
10939    }
10940}
10941
10942/// The `Method` expr of a bare ctor-delegation statement.
10943fn ctor_call_expr(s: &Stmt) -> Option<&Expr> {
10944    match s {
10945        Stmt::ExprStmt(e) if is_delegation_expr(e) => Some(e),
10946        _ => None,
10947    }
10948}
10949
10950/// Same delegation kind ignoring args (super vs this, same target).
10951fn same_ctor_kind(a: &Expr, b: &Expr) -> bool {
10952    match (a, b) {
10953        (
10954            Expr::Method { cls: c1, is_super: s1, owner: o1, .. },
10955            Expr::Method { cls: c2, is_super: s2, owner: o2, .. },
10956        ) => c1 == c2 && s1 == s2 && o1 == o2,
10957        _ => false,
10958    }
10959}
10960
10961/// Statements of a branch body, recursively flattening pure Blocks —
10962/// the fix runs BEFORE cleanup, so structurer-emitted nesting is still
10963/// in place. Any non-Block statement (If/Try/...) stays an item and
10964/// fails the def/call classification, which is the intended abort.
10965fn flat_list(s: &Stmt) -> Vec<&Stmt> {
10966    fn rec<'a>(s: &'a Stmt, out: &mut Vec<&'a Stmt>) {
10967        match s {
10968            Stmt::Block(v) => {
10969                for x in v {
10970                    rec(x, out);
10971                }
10972            }
10973            other => out.push(other),
10974        }
10975    }
10976    let mut out = Vec::new();
10977    rec(s, &mut out);
10978    out
10979}
10980
10981/// Blank the OWNER of un-folded `<init>` calls (lost allocation): the
10982/// printer renders them as `new X(..)` and never reads the owner, so its
10983/// register reference is not a real use (v7.a$a2: the dead
10984/// `new f(0,..)`'s owner was v1's register, inflating v1's use count
10985/// and blocking the inline-hoist).
10986fn strip_lost_alloc_owners(e: &mut Expr) {
10987    deep_rewrite(e, &mut |x| {
10988        if let Expr::Method { name, is_special: true, owner: Some(o), .. } = x {
10989            if &**name == "<init>" && !matches!(**o, Expr::This) {
10990                **o = Expr::Const(jdc_core::ir::expr::ConstVal::Int(0));
10991            }
10992        }
10993    });
10994}
10995
10996/// Per-var `Local` occurrence counts over an expression (single pass;
10997/// lost-alloc `<init>` owners excluded — see strip_lost_alloc_owners).
10998fn count_locals_expr(e: &Expr) -> std::collections::HashMap<u32, usize> {
10999    let mut m = std::collections::HashMap::new();
11000    count_expr_ro(e, &mut m);
11001    m
11002}
11003
11004pub(crate) fn count_locals_stmts(ss: &[Stmt]) -> std::collections::HashMap<u32, usize> {
11005    let mut m = std::collections::HashMap::new();
11006    for s in ss {
11007        visit_stmt_exprs_ro(s, &mut |e| count_expr_ro(e, &mut m));
11008    }
11009    m
11010}
11011
11012fn cnt(m: &std::collections::HashMap<u32, usize>, v: u32) -> usize {
11013    m.get(&v).copied().unwrap_or(0)
11014}
11015
11016/// Substitute map-referenced locals, recursively expanding def chains
11017/// (depth-capped). Returns None when a referenced var has no def or the
11018/// chain is too deep.
11019fn inline_locals(
11020    e: &Expr,
11021    map: &std::collections::HashMap<u32, Expr>,
11022    depth: u32,
11023) -> Option<Expr> {
11024    if depth > 4 {
11025        return None;
11026    }
11027    let mut out = e.clone();
11028    let mut fail = false;
11029    deep_rewrite(&mut out, &mut |x| {
11030        if let Expr::Local { var, .. } = x {
11031            if let Some(def) = map.get(var) {
11032                match inline_locals(def, map, depth + 1) {
11033                    Some(r) => *x = r,
11034                    None => fail = true,
11035                }
11036            }
11037        }
11038    });
11039    if fail {
11040        None
11041    } else {
11042        Some(out)
11043    }
11044}
11045
11046/// A leading statement that defines a local: (var, init expr).
11047fn def_of(s: &Stmt) -> Option<(u32, &Expr)> {
11048    match s {
11049        Stmt::LocalDef { var, init: Some(e), .. } => Some((*var, e)),
11050        Stmt::ExprStmt(Expr::Assign {
11051            target,
11052            value,
11053            op: jdc_core::ir::expr::AssignOp::Plain,
11054            ..
11055        }) => match &**target {
11056            Expr::Local { var, .. } => Some((*var, value)),
11057            _ => None,
11058        },
11059        _ => None,
11060    }
11061}
11062
11063/// Shape A: straight-line prefix, delegation references prefix locals.
11064fn try_linear_super_inline(stmts: &mut Vec<Stmt>) -> bool {
11065    let Some(pos) = stmts.iter().position(is_bare_ctor_call) else {
11066        return false;
11067    };
11068    if pos == 0 {
11069        return false; // already first — nothing to do
11070    }
11071    // The prefix must be straight-line.
11072    if !stmts[..pos]
11073        .iter()
11074        .all(|s| matches!(s, Stmt::LocalDef { .. } | Stmt::ExprStmt(_)))
11075    {
11076        return false;
11077    }
11078    let Some(call) = ctor_call_expr(&stmts[pos]) else {
11079        return false;
11080    };
11081    let Expr::Method { args, .. } = call else {
11082        return false;
11083    };
11084    // Defs reachable from the args (last definition wins, bytecode order).
11085    let mut map: std::collections::HashMap<u32, Expr> =
11086        std::collections::HashMap::new();
11087    let mut def_idx: std::collections::HashMap<u32, usize> =
11088        std::collections::HashMap::new();
11089    for (i, s) in stmts[..pos].iter().enumerate() {
11090        if let Some((v, e)) = def_of(s) {
11091            map.insert(v, e.clone());
11092            def_idx.insert(v, i);
11093        }
11094    }
11095    // Which arg locals need inlining?
11096    let mut needed: Vec<u32> = Vec::new();
11097    for a in args {
11098        let mut probe = a.clone();
11099        strip_lost_alloc_owners(&mut probe);
11100        deep_rewrite(&mut probe, &mut |x| {
11101            if let Expr::Local { var, .. } = x {
11102                if map.contains_key(var) && !needed.contains(var) {
11103                    needed.push(*var);
11104                }
11105            }
11106        });
11107    }
11108    if needed.is_empty() {
11109        return false; // plain hoist handles it
11110    }
11111    // Guard: side-effecting defs inline only at a provably preserved
11112    // call count (exactly one use, the arg site).
11113    let mut consumed: Vec<usize> = Vec::new();
11114    let use_counts = count_locals_stmts(stmts);
11115    for &v in &needed {
11116        let def = &map[&v];
11117        let uses = cnt(&use_counts, v);
11118        if jdc_core::ir::build::has_side_effects(def) && uses != 1 {
11119            return false;
11120        }
11121        if uses == 1 {
11122            consumed.push(def_idx[&v]);
11123        }
11124    }
11125    // Build the inlined args.
11126    let mut new_args: Vec<Expr> = Vec::with_capacity(args.len());
11127    for a in args {
11128        match inline_locals(a, &map, 0) {
11129            Some(r) => new_args.push(r),
11130            None => return false,
11131        }
11132    }
11133    // Rebuild: super first (with inlined args), the unconsumed prefix
11134    // statements keep their order after it, then the original rest.
11135    let mut call_stmt = stmts.remove(pos);
11136    if let Stmt::ExprStmt(Expr::Method { args: slot, .. }) = &mut call_stmt {
11137        *slot = new_args;
11138    }
11139    let mut rest: Vec<Stmt> = Vec::with_capacity(stmts.len() + 1);
11140    rest.push(call_stmt);
11141    for (i, s) in stmts.drain(..).enumerate() {
11142        // Indices shift after `remove(pos)`: consumed holds pre-removal
11143        // indices; pos itself is already gone from `stmts`.
11144        let orig = if i >= pos { i + 1 } else { i };
11145        if !consumed.contains(&orig) {
11146            rest.push(s);
11147        }
11148    }
11149    *stmts = rest;
11150    true
11151}
11152
11153/// Shape B: if/else branches each ending in the same-kind delegation.
11154fn try_merge_branched_super(stmts: &mut Vec<Stmt>) -> bool {
11155    // Candidate If positions — the prelude before the chosen If must be
11156    // straight-line; try each If until one merges (nested/multi-if
11157    // ctors exist but the first matching one is the delegation site).
11158    let if_positions: Vec<usize> = stmts
11159        .iter()
11160        .enumerate()
11161        .filter(|(_, s)| matches!(s, Stmt::If { .. }))
11162        .map(|(i, _)| i)
11163        .collect();
11164    for if_pos in if_positions {
11165        // Cheap allocation-free gate: at least one branch must hold a
11166        // delegation (most ctor ifs are field null-checks and must not
11167        // pay the merge machinery). The OTHER side may be delegation-free
11168        // (throw-guard shape D) or absent (then-only shape E — the
11169        // rendered if/else form often only exists after later passes).
11170        let gate = match &stmts[if_pos] {
11171            Stmt::If { then_stmt, else_stmt, .. } => {
11172                contains_delegation(then_stmt)
11173                    || else_stmt.as_ref().is_some_and(|e| contains_delegation(e))
11174            }
11175            _ => false,
11176        };
11177        if gate && merge_at(stmts, if_pos) {
11178            return true;
11179        }
11180    }
11181    false
11182}
11183
11184/// Does this subtree contain a ctor delegation? (module-level: shared by
11185/// the merge gate and split_branch)
11186fn contains_delegation(s: &Stmt) -> bool {
11187    match s {
11188        Stmt::ExprStmt(e) => is_delegation_expr(e),
11189        Stmt::Block(v) => v.iter().any(contains_delegation),
11190        Stmt::If { then_stmt, else_stmt, .. } => {
11191            contains_delegation(then_stmt)
11192                || else_stmt.as_ref().is_some_and(|e| contains_delegation(e))
11193        }
11194        _ => false,
11195    }
11196}
11197
11198
11199
11200/// One merge attempt at `stmts[if_pos]`. See fix_ctor_conditional_super
11201/// for the doctrine. Extensions over the pure two-call merge:
11202/// - branch pre-call SIDE-EFFECT statements (Kotlin `o.h(param,..)`
11203///   null-checks — the zz5/j0 family) are allowed; they move into the
11204///   branch remainder (after super — the same reordering doctrine the
11205///   plain hoist already applies; Java has no pre-super statement form).
11206/// - a branch WITHOUT a delegation (throw-guard: `if (x==null) throw
11207///   .. else { super(..); .. }` — the gb6/e family) merges as pure
11208///   remainder; exactly one branch carries the call then, and no
11209///   ternary args arise (the guard throws before any path needs them).
11210/// - prelude single-assignment if/else (`if (c) {v=e1} else {v=e2}`)
11211///   folds into a Cond def for inlining (the zz5/j0 v15/v16 preludes).
11212fn merge_at(stmts: &mut Vec<Stmt>, if_pos: usize) -> bool {
11213    /// Branch classification: Call(at, defs, pre-call extra idxs) when a
11214    /// delegation sits behind only defs/side-effect statements; Clean
11215    /// when the branch has no delegation at all (pure remainder); Dirty
11216    /// otherwise (nested/conditional delegation — out of scope).
11217    enum Br {
11218        /// (call index, pre-call defs, def statement indices)
11219        Call(usize, Vec<(u32, Expr)>, Vec<usize>),
11220        Clean,
11221        Dirty,
11222    }
11223    fn split_branch(list: &[&Stmt]) -> Br {
11224        let call_pos = list.iter().position(|s| ctor_call_expr(s).is_some());
11225        let Some(c) = call_pos else {
11226            // No delegation at this level: a pure remainder branch
11227            // (throw-guard) unless one hides in nested control flow.
11228            return if list.iter().any(|s| contains_delegation(s)) {
11229                Br::Dirty
11230            } else {
11231                Br::Clean
11232            };
11233        };
11234        if list.iter().skip(c + 1).any(|s| ctor_call_expr(s).is_some()) {
11235            return Br::Dirty; // two delegations in one branch
11236        }
11237        let mut defs: Vec<(u32, Expr)> = Vec::new();
11238        let mut def_idxs: Vec<usize> = Vec::new();
11239        for (i, s) in list.iter().enumerate().take(c) {
11240            match def_of(s) {
11241                Some((v, e)) => {
11242                    defs.push((v, e.clone()));
11243                    def_idxs.push(i);
11244                }
11245                None => match s {
11246                    // Side-effect statements and bare decls ahead of the
11247                    // delegation ride along into the remainder.
11248                    Stmt::ExprStmt(_) | Stmt::LocalDef { .. } => {}
11249                    _ => return Br::Dirty, // control flow pre-call
11250                },
11251            }
11252        }
11253        Br::Call(c, defs, def_idxs)
11254    }
11255    /// Prelude single-assignment if/else → `v = c ? e1 : e2` def.
11256    fn cond_def_of(s: &Stmt, scope: &[Stmt]) -> Option<(u32, Expr)> {
11257        let Stmt::If { cond, then_stmt, else_stmt: Some(e), .. } = s else {
11258            return None;
11259        };
11260        fn one<'x>(b: &'x Stmt, scope: &[Stmt]) -> Option<(u32, &'x Expr)> {
11261            let l = flat_list(b);
11262            if l.len() == 1 {
11263                return def_of(l[0]);
11264            }
11265            // A [w = E; v = w] forwarding PAIR — the shape the
11266            // structurer leaves when E is impure (`c v5 = new c(..);
11267            // v6 = v5`) and forward_single_use's adjacency pass missed
11268            // it (a duplicated read elsewhere in its raw body broke the
11269            // single-read gate). Collapse to `v = E`, gated on w being
11270            // method-unique: the merge DROPS this branch, so any other
11271            // read/write of w would dangle (lark gp2/d Kotlin default-
11272            // arg bridge, this-not-first family).
11273            if l.len() == 2 {
11274                let (w, ex) = def_of(l[0])?;
11275                let Stmt::ExprStmt(Expr::Assign {
11276                    target,
11277                    op: AssignOp::Plain,
11278                    value,
11279                }) = l[1]
11280                else {
11281                    return None;
11282                };
11283                let Expr::Local { var: tv, .. } = &**target else {
11284                    return None;
11285                };
11286                let Expr::Local { var: rv, .. } = &**value else {
11287                    return None;
11288                };
11289                if *rv != w || *tv == w {
11290                    return None;
11291                }
11292                // stmts_count_writes only sees Expr-level assigns; the
11293                // pair's own def is often an init-bearing LocalDef, so
11294                // count those separately for the uniqueness gate.
11295                let mut ldef_writes = 0usize;
11296                for st in scope {
11297                    walk_all(st, &mut |x| {
11298                        if let Stmt::LocalDef {
11299                            var,
11300                            init: Some(_),
11301                            ..
11302                        } = x
11303                        {
11304                            if *var == w {
11305                                ldef_writes += 1;
11306                            }
11307                        }
11308                    });
11309                }
11310                if stmts_read_var(scope, w) != 1
11311                    || stmts_count_writes(scope, w) + ldef_writes != 1
11312                {
11313                    return None;
11314                }
11315                return Some((*tv, ex));
11316            }
11317            None
11318        }
11319        let (v1, e1) = one(then_stmt, scope)?;
11320        let (v2, e2) = one(e, scope)?;
11321        if v1 != v2 {
11322            return None;
11323        }
11324        Some((
11325            v1,
11326            Expr::Cond {
11327                c: Box::new(cond.clone()),
11328                t: Box::new(e1.clone()),
11329                f: Box::new(e2.clone()),
11330            },
11331        ))
11332    }
11333
11334    // Prelude: defs (flat or folded cond), bare decls, side-effect stmts.
11335    let mut pmap: std::collections::HashMap<u32, Expr> =
11336        std::collections::HashMap::new();
11337    let mut pidx: std::collections::HashMap<u32, usize> =
11338        std::collections::HashMap::new();
11339    for (i, s) in stmts[..if_pos].iter().enumerate() {
11340        match s {
11341            Stmt::LocalDef { .. } | Stmt::ExprStmt(_) => {
11342                if let Some((v, e)) = def_of(s) {
11343                    pmap.insert(v, e.clone());
11344                    pidx.insert(v, i);
11345                }
11346            }
11347            Stmt::If { .. } => match cond_def_of(s, stmts) {
11348                Some((v, e)) => {
11349                    pmap.insert(v, e);
11350                    pidx.insert(v, i);
11351                }
11352                None => return false,
11353            },
11354            _ => return false,
11355        }
11356    }
11357    let (cond, then_v, else_v, had_else) = match &stmts[if_pos] {
11358        Stmt::If { cond, then_stmt, else_stmt, .. } => (
11359            cond.clone(),
11360            then_stmt.as_ref().clone(),
11361            else_stmt.as_ref().map_or_else(|| Stmt::Block(Vec::new()), |e| e.as_ref().clone()),
11362            else_stmt.is_some(),
11363        ),
11364        _ => return false,
11365    };
11366    // Chained guards (Kotlin multi-param null-checks):
11367    // `if(a){throw..} else { if(b){throw..} else { super(..); .. } }` —
11368    // a branch that is a lone delegation-bearing If gets merged
11369    // recursively first (its delegation hoisted to the branch head), so
11370    // the outer split then sees [call, nested-guard-remainder].
11371    fn normalize_branch(b: &mut Stmt) {
11372        // MOVE-based (no deep clone): chained weixin Parcel ctors carry
11373        // huge tails; cloning per recursion level cost measurable wall
11374        // time. merge_at leaves stmts untouched on failure, so the taken
11375        // node can always go back.
11376        let slot: &mut Stmt = match b {
11377            Stmt::Block(v) if v.len() == 1 => &mut v[0],
11378            other => other,
11379        };
11380        if !matches!(slot, Stmt::If { .. }) || !contains_delegation(slot) {
11381            return;
11382        }
11383        let taken = std::mem::replace(slot, Stmt::Block(Vec::new()));
11384        let mut tmp = vec![taken];
11385        if merge_at(&mut tmp, 0) {
11386            *slot = Stmt::Block(tmp);
11387        } else {
11388            *slot = tmp.pop().unwrap();
11389        }
11390    }
11391    let mut then_v = then_v;
11392    let mut else_v = else_v;
11393    normalize_branch(&mut then_v);
11394    normalize_branch(&mut else_v);
11395    let then_list = flat_list(&then_v);
11396    let else_list = flat_list(&else_v);
11397
11398    // Exactly: two Call branches (merge with ternaries) or one Call +
11399    // one Clean (single-delegation throw-guard). Owned branch payloads.
11400    enum Side {
11401        Call {
11402            at: usize,
11403            defs: Vec<(u32, Expr)>,
11404            def_idxs: Vec<usize>,
11405        },
11406        Clean,
11407    }
11408    let (t_side, e_side, two_sided) = match (split_branch(&then_list), split_branch(&else_list)) {
11409        (Br::Call(tc, td, ti), Br::Call(ec, ed, ei)) => (
11410            Side::Call { at: tc, defs: td, def_idxs: ti },
11411            Side::Call { at: ec, defs: ed, def_idxs: ei },
11412            true,
11413        ),
11414        (Br::Call(tc, td, ti), Br::Clean) => (
11415            Side::Call { at: tc, defs: td, def_idxs: ti },
11416            Side::Clean,
11417            false,
11418        ),
11419        (Br::Clean, Br::Call(ec, ed, ei)) => (
11420            Side::Clean,
11421            Side::Call { at: ec, defs: ed, def_idxs: ei },
11422            false,
11423        ),
11424        _ => return false,
11425    };
11426    // The delegation nodes (two-sided: both; single: one).
11427    let (t_call, e_call) = match (&t_side, &e_side) {
11428        (Side::Call { at: ta, .. }, Side::Call { at: ea, .. }) => (
11429            ctor_call_expr(then_list[*ta]),
11430            ctor_call_expr(else_list[*ea]),
11431        ),
11432        (Side::Call { at: ta, .. }, Side::Clean) => {
11433            (ctor_call_expr(then_list[*ta]), None)
11434        }
11435        (Side::Clean, Side::Call { at: ea, .. }) => {
11436            (None, ctor_call_expr(else_list[*ea]))
11437        }
11438        _ => return false,
11439    };
11440    // The node that becomes the merged call: then side when present.
11441    let (call_list, call_at) = match &t_side {
11442        Side::Call { at, .. } => (&then_list, *at),
11443        Side::Clean => match &e_side {
11444            Side::Call { at, .. } => (&else_list, *at),
11445            Side::Clean => return false,
11446        },
11447    };
11448    let (base_args, other_args) = if two_sided {
11449        let (Some(tc), Some(ec)) = (t_call, e_call) else {
11450            return false;
11451        };
11452        if !same_ctor_kind(tc, ec) {
11453            return false;
11454        }
11455        let (Expr::Method { args: a1, .. }, Expr::Method { args: a2, .. }) = (tc, ec) else {
11456            return false;
11457        };
11458        if a1.len() != a2.len() {
11459            return false;
11460        }
11461        (a1, Some(a2))
11462    } else {
11463        let c = t_call.or(e_call);
11464        let Some(c) = c else { return false };
11465        let Expr::Method { args, .. } = c else {
11466            return false;
11467        };
11468        (args, None)
11469    };
11470
11471    // Def maps per side (prelude + own branch defs).
11472    let mut tmap: std::collections::HashMap<u32, Expr> = pmap.clone();
11473    let mut emap: std::collections::HashMap<u32, Expr> = pmap.clone();
11474    if let Side::Call { defs, .. } = &t_side {
11475        for (v, e) in defs {
11476            tmap.insert(*v, e.clone());
11477        }
11478    }
11479    if let Side::Call { defs, .. } = &e_side {
11480        for (v, e) in defs {
11481            emap.insert(*v, e.clone());
11482        }
11483    }
11484    let Some(cond_i) = inline_locals(&cond, &pmap, 0) else {
11485        return false;
11486    };
11487    // Inline each side's args through its own map.
11488    let mut then_args: Vec<Expr> = Vec::with_capacity(base_args.len());
11489    let mut else_args: Vec<Expr> = Vec::new();
11490    if two_sided {
11491        for a in base_args {
11492            match inline_locals(a, &tmap, 0) {
11493                Some(r) => then_args.push(r),
11494                None => return false,
11495            }
11496        }
11497        for a in other_args.unwrap() {
11498            match inline_locals(a, &emap, 0) {
11499                Some(r) => else_args.push(r),
11500                None => return false,
11501            }
11502        }
11503    } else {
11504        // Single-delegation side: base_args belong to whichever side
11505        // carries the call; pick that side's map.
11506        let from_then = matches!(&t_side, Side::Call { .. });
11507        for a in base_args {
11508            let r = if from_then {
11509                inline_locals(a, &tmap, 0)
11510            } else {
11511                inline_locals(a, &emap, 0)
11512            };
11513            match r {
11514                Some(r) => then_args.push(r),
11515                None => return false,
11516            }
11517        }
11518    }
11519    let primary_args = base_args;
11520    // Per-position merge.
11521    let mut merged: Vec<Expr> = Vec::with_capacity(then_args.len());
11522    let mut differ = 0usize;
11523    for (j, t) in then_args.iter().enumerate() {
11524        if two_sided {
11525            let e = &else_args[j];
11526            if t == e {
11527                merged.push(t.clone());
11528            } else {
11529                differ += 1;
11530                merged.push(Expr::Cond {
11531                    c: Box::new(cond_i.clone()),
11532                    t: Box::new(t.clone()),
11533                    f: Box::new(e.clone()),
11534                });
11535            }
11536        } else {
11537            merged.push(t.clone());
11538        }
11539    }
11540
11541    // ---- side-effect preservation guards (batched counts) ----
11542    let vars_in = |e: &Expr| -> Vec<u32> {
11543        let mut vs = Vec::new();
11544        let mut probe = e.clone();
11545        strip_lost_alloc_owners(&mut probe);
11546        deep_rewrite(&mut probe, &mut |x| {
11547            if let Expr::Local { var, .. } = x {
11548                if !vs.contains(var) {
11549                    vs.push(*var);
11550                }
11551            }
11552        });
11553        vs
11554    };
11555    // Remainder statements per branch (extras + post-call tail; a Clean
11556    // branch contributes all of its statements).
11557    // The consumed defs LEAVE the remainder: their expressions were
11558    // inlined into the merged delegation args, and a dead `v = E` left
11559    // after the call both reads wrong and trips the u_t1/u_t2 guards
11560    // (ContinuationImpl's branch-local `context = null / getContext()`
11561    // — the coroutine family's 对this的调用必须是第一个语句 root).
11562    let branch_remainder = |list: &[&Stmt], side: &Side| -> Vec<Stmt> {
11563        match side {
11564            Side::Clean => list.iter().map(|s| (*s).clone()).collect(),
11565            Side::Call { at, def_idxs, .. } => {
11566                list.iter()
11567                    .enumerate()
11568                    .filter(|(i, _)| *i != *at && !def_idxs.contains(i))
11569                    .map(|(_, s)| (*s).clone())
11570                    .collect()
11571            }
11572        }
11573    };
11574    let tail_then_v = branch_remainder(&then_list, &t_side);
11575    let tail_else_v = branch_remainder(&else_list, &e_side);
11576    let post = &stmts[if_pos + 1..];
11577    let mut inlined: Vec<u32> = vars_in(&cond);
11578    for a in primary_args.iter().chain(else_args.iter()) {
11579        for v in vars_in(a) {
11580            if !inlined.contains(&v) {
11581                inlined.push(v);
11582            }
11583        }
11584    }
11585    inlined.retain(|v| pmap.contains_key(v) || tmap.contains_key(v) || emap.contains_key(v));
11586    // Prelude-and-branch redefinition: ambiguous provenance — reject.
11587    for &v in &inlined {
11588        let in_branch = match (&t_side, &e_side) {
11589            (Side::Call { defs: td, .. }, Side::Call { defs: ed, .. }) => {
11590                td.iter().any(|(d, _)| *d == v) || ed.iter().any(|(d, _)| *d == v)
11591            }
11592            (Side::Call { defs, .. }, Side::Clean)
11593            | (Side::Clean, Side::Call { defs, .. }) => {
11594                defs.iter().any(|(d, _)| *d == v)
11595            }
11596            _ => false,
11597        };
11598        if pmap.contains_key(&v) && in_branch {
11599            return false;
11600        }
11601    }
11602    type Counts = std::collections::HashMap<u32, usize>;
11603    type GuardCounts = (
11604        Counts,
11605        Vec<Counts>,
11606        Vec<Counts>,
11607        Counts,
11608        Counts,
11609        Counts,
11610        Vec<(u32, Counts)>,
11611    );
11612    let (cond_counts, a1_counts, a2_counts, tail1_counts, tail2_counts, post_counts, def_counts): GuardCounts =
11613        if inlined.is_empty() {
11614        Default::default()
11615    } else {
11616        let all_defs: Vec<(u32, &Expr)> = pmap
11617            .iter()
11618            .map(|(k, v)| (*k, v))
11619            .chain(match &t_side {
11620                Side::Call { defs, .. } => defs.iter().map(|(k, v)| (*k, v)).collect(),
11621                Side::Clean => Vec::new(),
11622            })
11623            .chain(match &e_side {
11624                Side::Call { defs, .. } => defs.iter().map(|(k, v)| (*k, v)).collect(),
11625                Side::Clean => Vec::new(),
11626            })
11627            .collect();
11628        (
11629            count_locals_expr(&cond),
11630            primary_args.iter().map(count_locals_expr).collect(),
11631            else_args.iter().map(count_locals_expr).collect(),
11632            count_locals_stmts(&tail_then_v),
11633            count_locals_stmts(&tail_else_v),
11634            count_locals_stmts(post),
11635            all_defs.into_iter().map(|(v, e)| (v, count_locals_expr(e))).collect(),
11636        )
11637        };
11638    for &v in &inlined {
11639        let in_t = matches!(&t_side, Side::Call { defs, .. } if defs.iter().any(|(d, _)| *d == v));
11640        let in_e = matches!(&e_side, Side::Call { defs, .. } if defs.iter().any(|(d, _)| *d == v));
11641        let effectful = [
11642            in_t.then(|| tmap.get(&v)).flatten(),
11643            in_e.then(|| emap.get(&v)).flatten(),
11644            pmap.get(&v),
11645        ]
11646        .into_iter()
11647        .flatten()
11648        .any(jdc_core::ir::build::has_side_effects);
11649        if !effectful {
11650            continue;
11651        }
11652        let u_cond = cnt(&cond_counts, v);
11653        let u_post = cnt(&post_counts, v);
11654        let u_t1 = cnt(&tail1_counts, v);
11655        let u_t2 = cnt(&tail2_counts, v);
11656        let mut u_odef = 0usize;
11657        for (dv, dc) in &def_counts {
11658            if *dv != v && inlined.contains(dv) {
11659                u_odef += cnt(dc, v);
11660            }
11661        }
11662        let mut emit = u_cond * (1 + differ);
11663        let mut uses_args = 0usize;
11664        for j in 0..primary_args.len() {
11665            let c1 = cnt(&a1_counts[j], v);
11666            let c2 = if two_sided && j < a2_counts.len() {
11667                cnt(&a2_counts[j], v)
11668            } else {
11669                0
11670            };
11671            uses_args += c1 + c2;
11672            emit += if two_sided && then_args[j] != else_args[j] {
11673                c1 + c2
11674            } else {
11675                c1.max(c2)
11676            };
11677        }
11678        let ok = if in_t || in_e {
11679            u_cond == 0
11680                && u_post == 0
11681                && u_t1 == 0
11682                && u_t2 == 0
11683                && u_odef == 0
11684                && a1_counts.iter().map(|c| cnt(c, v)).sum::<usize>() <= 1
11685                && a2_counts.iter().map(|c| cnt(c, v)).sum::<usize>() <= 1
11686                && emit <= 2
11687                && (in_t || a1_counts.iter().all(|c| cnt(c, v) == 0))
11688                && (in_e || a2_counts.iter().all(|c| cnt(c, v) == 0))
11689        } else {
11690            u_post == 0
11691                && u_t1 == 0
11692                && u_t2 == 0
11693                && u_odef == 0
11694                && ((u_cond == 1 && uses_args == 0 && emit <= 2)
11695                    || (u_cond == 0 && emit <= 1))
11696        };
11697        if !ok {
11698            return false;
11699        }
11700    }
11701
11702    // ---- assembly ----
11703    let mut call_stmt = call_list[call_at].clone();
11704    if let Stmt::ExprStmt(Expr::Method { args: slot, .. }) = &mut call_stmt {
11705        *slot = merged;
11706    }
11707    let residual = |v: u32| -> usize {
11708        cnt(&tail1_counts, v) + cnt(&tail2_counts, v) + cnt(&post_counts, v)
11709    };
11710    let mut kept_prelude: Vec<Stmt> = Vec::new();
11711    for (i, s) in stmts[..if_pos].iter().enumerate() {
11712        let consumed = match s {
11713            Stmt::LocalDef { var, .. } => Some(*var),
11714            Stmt::ExprStmt(Expr::Assign { target, .. }) => match &**target {
11715                Expr::Local { var, .. } => Some(*var),
11716                _ => None,
11717            },
11718            Stmt::If { .. } => cond_def_of(s, stmts).map(|(v, _)| v),
11719            _ => None,
11720        };
11721        if let Some(v) = consumed {
11722            if inlined.contains(&v) && pidx.get(&v) == Some(&i) && residual(v) == 0 {
11723                continue;
11724            }
11725        }
11726        kept_prelude.push(s.clone());
11727    }
11728    let build_keep = |list: &[&Stmt], side: &Side| -> Vec<Stmt> {
11729        match side {
11730            Side::Clean => list.iter().map(|s| (*s).clone()).collect(),
11731            Side::Call { at, .. } => {
11732                let mut out = Vec::new();
11733                for (i, s) in list.iter().enumerate() {
11734                    if i == *at {
11735                        continue;
11736                    }
11737                    if i < *at {
11738                        if let Some((v, _)) = def_of(s) {
11739                            if inlined.contains(&v) && residual(v) == 0 {
11740                                continue;
11741                            }
11742                        }
11743                    }
11744                    out.push((*s).clone());
11745                }
11746                out
11747            }
11748        }
11749    };
11750    let then_keep = build_keep(&then_list, &t_side);
11751    let else_keep = build_keep(&else_list, &e_side);
11752    let else_part = if else_keep.is_empty() && !had_else {
11753        None
11754    } else {
11755        Some(Box::new(Stmt::Block(else_keep)))
11756    };
11757    let new_if = Stmt::If {
11758        cond: cond_i,
11759        then_stmt: Box::new(Stmt::Block(then_keep)),
11760        else_stmt: else_part,
11761    };
11762    let if_empty = match &new_if {
11763        Stmt::If { then_stmt, else_stmt, .. } => {
11764            let be = |s: &Stmt| matches!(s, Stmt::Block(v) if v.is_empty());
11765            be(then_stmt) && else_stmt.as_ref().is_none_or(|e| be(e))
11766        }
11767        _ => false,
11768    };
11769    let mut out: Vec<Stmt> = Vec::with_capacity(stmts.len());
11770    out.push(call_stmt);
11771    out.extend(kept_prelude);
11772    if !if_empty {
11773        out.push(new_if);
11774    }
11775    out.extend(stmts[if_pos + 1..].to_vec());
11776    *stmts = out;
11777    true
11778}
11779
11780
11781pub fn strip_enum_ctor_super(body: &mut Stmt) {
11782    fn is_enum_super(s: &Stmt) -> bool {
11783        matches!(
11784            s,
11785            Stmt::ExprStmt(Expr::Method { name, is_super: true, .. }) if &**name == "<init>"
11786        )
11787    }
11788    // FULL depth: hotfix-guarded ctors hide a second super(String,int)
11789    // inside the proxy branch (alipay InstantRun: `if (proxy != null) {
11790    // super((String) v3[0], ..); proxy.afterSuper(this); return; }` at
11791    // depth 3+; the old two-level strip left it — super resolved against
11792    // Object once the fallback enum lost `extends Enum`, alipay
11793    // ctor-arity ×3,364). This path only runs for fallback enums whose
11794    // dex super is Enum/Object, so EVERY super call is a dead Enum-ctor
11795    // reference wherever it sits; constant subclasses (real supers)
11796    // take the other branch in method.rs and are untouched.
11797    fn strip_deep(stmts: &mut Vec<Stmt>) {
11798        stmts.retain(|s| !is_enum_super(s));
11799        for st in stmts.iter_mut() {
11800            strip_one(st);
11801        }
11802    }
11803    fn strip_one(st: &mut Stmt) {
11804        match st {
11805            Stmt::Block(v) => strip_deep(v),
11806            Stmt::If { then_stmt, else_stmt, .. } => {
11807                strip_one(then_stmt);
11808                if let Some(e) = else_stmt {
11809                    strip_one(e);
11810                }
11811            }
11812            Stmt::While { body, .. }
11813            | Stmt::DoWhile { body, .. }
11814            | Stmt::ForEach { body, .. }
11815            | Stmt::Synchronized { body, .. }
11816            | Stmt::Labeled { body, .. } => strip_one(body),
11817            Stmt::For { init, body, .. } => {
11818                strip_deep(init);
11819                strip_one(body);
11820            }
11821            Stmt::Switch { cases, default, .. } => {
11822                for c in cases.iter_mut() {
11823                    strip_deep(&mut c.body);
11824                }
11825                if let Some(d) = default {
11826                    strip_one(d);
11827                }
11828            }
11829            Stmt::Try { body, catches, finally }
11830            | Stmt::TryWithResources { body, catches, finally, .. } => {
11831                strip_one(body);
11832                for c in catches.iter_mut() {
11833                    strip_one(&mut c.body);
11834                }
11835                if let Some(f) = finally {
11836                    strip_one(f);
11837                }
11838            }
11839            _ => {}
11840        }
11841    }
11842    let Stmt::Block(stmts) = body else {
11843        return;
11844    };
11845    strip_deep(stmts);
11846}
11847
11848pub fn ensure_declared(body: &mut Stmt, vt: &VarTable) {
11849    // (Perf: dense Vec<bool> tables instead of HashSets — var ids are
11850    // dense; the `assigned` set collected here was never read and cost a
11851    // full extra tree walk per method.)
11852    let n = vt.vars.len().max(1);
11853
11854    // Demote extra LocalDefs (same var declared more than once) FIRST —
11855    // the scope analysis below must see exactly one def per var.
11856    let mut seen: HashSet<u32> = HashSet::default();
11857    demote_dup_decls(body, &mut seen, vt);
11858
11859    // Scope hoist: a LocalDef inside a nested block whose var is also
11860    // referenced OUTSIDE that block is a Java scope violation (the
11861    // branch-local `int v8 = pi.versionCode;` read after the try —
11862    // "cannot find symbol" at every outside use). Demote those defs to
11863    // assignments and let the bare top-of-method declaration below cover
11864    // the var. Two cheap passes: pre-order block numbering + per-var
11865    // inside/outside occurrence counts (pre-order subtree = id range).
11866    let mut def_block: Vec<u32> = vec![u32::MAX; n];
11867    let mut sizes: Vec<u32> = vec![0];
11868    let mut next_id: u32 = 1;
11869    scope_pass_a(body, 0, &mut next_id, &mut def_block, &mut sizes);
11870    sizes[0] = next_id;
11871    let mut within: Vec<u32> = vec![0; n];
11872    let mut total: Vec<u32> = vec![0; n];
11873    let mut next_b: u32 = 1;
11874    scope_pass_b(body, 0, &mut next_b, &def_block, &sizes, &mut within, &mut total);
11875    let mut hoist: HashSet<u32> = HashSet::default();
11876    for v in 0..n {
11877        if def_block[v] != u32::MAX && def_block[v] != 0 && within[v] < total[v] {
11878            hoist.insert(v as u32);
11879        }
11880    }
11881    if !hoist.is_empty() {
11882        let mut seed: HashSet<u32> = hoist.iter().copied().collect();
11883        demote_dup_decls(body, &mut seed, vt);
11884    }
11885
11886    let mut declared: Vec<bool> = vec![false; n];
11887    let mut is_param: Vec<bool> = vec![false; n];
11888    for v in &vt.vars {
11889        if v.is_param && (v.id as usize) < n {
11890            is_param[v.id as usize] = true;
11891        }
11892    }
11893    // Catch parameters are declared by the `catch (Type v)` clause —
11894    // bind_catches consumed their LocalDef before this pass ran, so
11895    // without this they re-declare at the top of the method and clash
11896    // with the clause (`Throwable th2;` + `catch (Throwable th2)` — a
11897    // syntax gate cannot see this, it is a semantic "already defined").
11898    let mut is_catch: Vec<bool> = vec![false; n];
11899    walk_all(body, &mut |st| {
11900        if let Stmt::Try { catches, .. } = st {
11901            for c in catches {
11902                if (c.var as usize) < n {
11903                    is_catch[c.var as usize] = true;
11904                }
11905            }
11906        }
11907    });
11908    walk_all(body, &mut |st| {
11909        if let Stmt::LocalDef { var, .. } = st {
11910            if (*var as usize) < n {
11911                declared[*var as usize] = true;
11912            }
11913        }
11914    });
11915    let mut used: HashSet<u32> = HashSet::default();
11916    // assignments=true: a var that only ever appears as an assign
11917    // TARGET still needs a declaration (`x = 5;` alone does not
11918    // declare x in Java).
11919    stmt_collect_vars(body, &mut used, true);
11920
11921    // Params are declared by the signature; hoisted vars lost their
11922    // LocalDef to the demotion above and always need the bare decl.
11923    let mut needs_decl: Vec<u32> = used
11924        .into_iter()
11925        .filter(|v| {
11926            let i = *v as usize;
11927            i >= n
11928                || ((!declared[i] && !is_param[i] && !is_catch[i]) || hoist.contains(v))
11929        })
11930        .collect();
11931    needs_decl.sort_unstable();
11932    needs_decl.dedup();
11933
11934    if !needs_decl.is_empty() {
11935        let mut decls: Vec<Stmt> = needs_decl
11936            .into_iter()
11937            .map(|v| Stmt::LocalDef {
11938                var: v,
11939                init: None,
11940                is_final: false,
11941                force_type: true,
11942            })
11943            .collect();
11944        decls.reverse();
11945        match body {
11946            Stmt::Block(v) => {
11947                for d in decls {
11948                    v.insert(0, d);
11949                }
11950            }
11951            other => {
11952                let inner = std::mem::replace(other, Stmt::Block(vec![]));
11953                *other = Stmt::Block({
11954                    let mut vs = decls;
11955                    vs.push(inner);
11956                    vs
11957                });
11958            }
11959        }
11960    }
11961}
11962
11963fn demote_dup_decls(s: &mut Stmt, seen: &mut HashSet<u32>, vt: &VarTable) {
11964    match s {
11965        // Exactly one visit per node: the Block arm consumes its children,
11966        // the fallthrough walk would double-visit them.
11967        Stmt::Block(v) => {
11968            for x in v.iter_mut() {
11969                demote_dup_decls(x, seen, vt);
11970            }
11971        }
11972        Stmt::LocalDef { var, init, .. } => {
11973            if seen.contains(var) {
11974                let ty = vt.var(*var).ty.clone();
11975                if let Some(e) = init.take() {
11976                    *s = Stmt::ExprStmt(Expr::Assign {
11977                        target: Box::new(Expr::Local { var: *var, ty }),
11978                        op: AssignOp::Plain,
11979                        value: Box::new(e),
11980                    });
11981                } else {
11982                    *s = Stmt::Block(vec![]);
11983                }
11984            } else {
11985                seen.insert(*var);
11986            }
11987        }
11988        other => {
11989            walk_mut(other, &mut |x| demote_dup_decls(x, seen, vt));
11990        }
11991    }
11992}
11993
11994#[allow(dead_code)]
11995fn unused(_: &Vec<CaseGroup>, _: &Catch) {}
11996
11997// ---------------------------------------------------------------------------
11998// jadx-style local names (ApplyVariableNames + dexdec's port of it)
11999// ---------------------------------------------------------------------------
12000
12001/// A synthetic `v12`/`p3` never survives when a better name exists.
12002/// Priority: (1) a Kotlin `Intrinsics.checkNotNullParameter(x, "name")`
12003/// names `x` from the message string (the string IS the parameter name);
12004/// (2) the single defining call — `getFoo()` → `foo`, `isFinishing()` →
12005/// `finishing`, `new File(…)` → `file`; (3) a type alias or the
12006/// lowercased class simple name. Collisions take `2`, `3`, …; a var with
12007/// two DISAGREEING defining calls stays unnamed (dexdec's
12008/// RelationalNameInference rule). Debug-info names are never touched
12009/// (`synthetic_name` gates the whole pass).
12010pub fn apply_local_names(vt: &mut VarTable, body: &Stmt) {
12011    use std::collections::{HashMap, HashSet};
12012
12013    // ---- proposals ------------------------------------------------------
12014    // var → (name, source-priority); disagreeing call names are dropped.
12015    let mut by_call: HashMap<u32, Vec<String>> = HashMap::new();
12016    let mut by_intrinsics: HashMap<u32, String> = HashMap::new();
12017
12018    visit_all_exprs(body, &mut |e| {
12019        if let Expr::Method { cls, name, args, .. } = e {
12020            if (cls.as_ref() == "kotlin/jvm/internal/Intrinsics"
12021                || cls.as_ref() == "kotlin/jvm/internal/IntrinsicsKt")
12022                && matches!(name.as_ref(), "checkNotNullParameter" | "checkParameterIsNotNull")
12023                && args.len() == 2
12024            {
12025                if let (Expr::Local { var, .. }, Expr::Const(ConstVal::Str(s))) = (&args[0], &args[1])
12026                {
12027                    by_intrinsics
12028                        .entry(*var)
12029                        .or_insert_with(|| sanitize_name(s).unwrap_or_default());
12030                }
12031            }
12032        }
12033    });
12034
12035    walk_all(body, &mut |st| {
12036        let (var, value) = match st {
12037            Stmt::LocalDef { var, init: Some(e), .. } => (*var, e),
12038            Stmt::ExprStmt(Expr::Assign { target, value, .. })
12039                if matches!(&**target, Expr::Local { .. }) =>
12040            {
12041                let Expr::Local { var, .. } = &**target else { return };
12042                (*var, value.as_ref())
12043            }
12044            _ => return,
12045        };
12046        if let Some(n) = defining_call_name(value) {
12047            by_call.entry(var).or_default().push(n);
12048        }
12049    });
12050
12051    // ---- reservation + application --------------------------------------
12052    // Real (debug-info) names can legally REPEAT across sibling source
12053    // scopes (`int i` in two loops) but our flat declaration hoisting
12054    // puts them in one scope — de-duplicate with the same numeric
12055    // suffixes the claim path uses.
12056    // Uniqueness operates on the SANITIZED display name: the identifier
12057    // sanitizer maps non-ASCII to `_`, so distinct debug names
12058    // (`ERROR_token参数缺失` / `ERROR_channelId参数缺失`) both RENDER as
12059    // `ERROR________` and collide as declarations. Tracking raw names
12060    // here would let the collision through.
12061    let sanit = |n: &str| crate::classdec::java_ident(n).into_owned();
12062    let mut taken: HashSet<String> = HashSet::default();
12063    // Parameter names are occupied REGARDLESS of syntheticness: claim()
12064    // hands out rename candidates, and without the synthetic params in
12065    // the pool a type fallback (`l7.p0` → "p0") renamed one parameter
12066    // ONTO another's slot-name (`b(v6.l p0, Object obj, l7.p0 p0)`).
12067    for v in vt.vars.iter() {
12068        // ALL synthetic names occupy their names, not just parameters:
12069        // a type fallback (`pc5.v56` → "v56") otherwise renames a local
12070        // ONTO another local's slot-name (`int v56;` beside
12071        // `pc5.v56 v56;`).
12072        if v.synthetic_name {
12073            taken.insert(sanit(&v.name));
12074        }
12075    }
12076    for v in vt.vars.iter_mut() {
12077        if v.synthetic_name {
12078            continue;
12079        }
12080        if !taken.insert(sanit(&v.name)) {
12081            let base = sanit(&v.name);
12082            for i in 2.. {
12083                let cand = format!("{base}{i}");
12084                if taken.insert(cand.clone()) {
12085                    v.name = cand;
12086                    break;
12087                }
12088            }
12089        }
12090    }
12091    let claim = |taken: &mut HashSet<String>, want: &str| -> String {
12092        let w = crate::classdec::java_ident(want).into_owned();
12093        if taken.insert(w.clone()) {
12094            return w;
12095        }
12096        for i in 2.. {
12097            let cand = format!("{w}{i}");
12098            if taken.insert(cand.clone()) {
12099                return cand;
12100            }
12101        }
12102        w
12103    };
12104
12105    for info in vt.vars.iter_mut() {
12106        if !info.synthetic_name {
12107            continue;
12108        }
12109        // (1) Intrinsics string — the Kotlin compiler wrote the real
12110        // parameter name right into the check.
12111        if let Some(n) = by_intrinsics.get(&info.id) {
12112            if !n.is_empty() {
12113                info.name = claim(&mut taken, n);
12114                continue;
12115            }
12116        }
12117        // (2) One consistent defining call.
12118        if let Some(names) = by_call.get(&info.id) {
12119            let unique: HashSet<&String> = names.iter().collect();
12120            if unique.len() == 1 {
12121                let n = unique.into_iter().next().unwrap();
12122                if !is_java_keyword(n) {
12123                    info.name = claim(&mut taken, n);
12124                    continue;
12125                }
12126            }
12127        }
12128        // (3) Type alias, else the lowercased simple class name
12129        // (jadx names a `Looper` local `looper`).
12130        let want = type_alias(&info.ty).map(str::to_string).or_else(|| simple_type_name(&info.ty));
12131        if let Some(n) = want {
12132            info.name = claim(&mut taken, &n);
12133        }
12134    }
12135
12136    // Final uniquification across ALL vars (params, locals, catch): the
12137    // reservation, claim and debug-name domains above are checked
12138    // pairwise but a name claimed LAST can still equal a debug name
12139    // that never re-checked (`zd1.v2 v2` beside `zd1.v2[] v2`).
12140    let mut seen: HashSet<String> = HashSet::default();
12141    for v in vt.vars.iter_mut() {
12142        if !seen.insert(sanit(&v.name)) {
12143            let base = sanit(&v.name);
12144            for i in 2.. {
12145                let cand = format!("{base}{i}");
12146                if seen.insert(cand.clone()) {
12147                    v.name = cand;
12148                    break;
12149                }
12150            }
12151        }
12152    }
12153}
12154
12155/// `com/android/.../Looper` → `looper` — the alias-table fallback.
12156fn simple_type_name(ty: &TypeRef) -> Option<String> {
12157    let TypeRef::J(JavaType::Object(n)) = ty else {
12158        return None;
12159    };
12160    let simple = n.rsplit('/').next().unwrap_or(n);
12161    let simple = simple.rsplit('$').next().unwrap_or(simple);
12162    // Lowercase the first ALPHABETIC char — `$`/`_`-prefixed names
12163    // (R8's `$$$_Thread`) must not survive capitalized.
12164    let mut chars = simple.chars();
12165    let mut out = String::with_capacity(simple.len());
12166    let mut lowered = false;
12167    for c in chars.by_ref() {
12168        if c.is_ascii_alphabetic() && !lowered {
12169            out.push(c.to_ascii_lowercase());
12170            lowered = true;
12171        } else {
12172            out.push(c);
12173        }
12174        if lowered {
12175            break;
12176        }
12177    }
12178    out.extend(chars);
12179    sanitize_name(&out)
12180}
12181
12182/// `getFoo()` → `foo`, `isFinishing()` → `finishing`, `new File(…)` →
12183/// `file`. The get/is prefixes only strip when the remainder starts
12184/// uppercase (so `issues()` keeps its name).
12185fn defining_call_name(e: &Expr) -> Option<String> {
12186    match e {
12187        Expr::Method { name, .. } => {
12188            let base = name
12189                .strip_prefix("get")
12190                .or_else(|| name.strip_prefix("is"))
12191                .filter(|rest| rest.chars().next().is_some_and(|c| c.is_ascii_uppercase()))
12192                .unwrap_or(name);
12193            let first = base.chars().next()?;
12194            let mut out = String::with_capacity(base.len());
12195            out.push(first.to_ascii_lowercase());
12196            out.extend(base.chars().skip(1));
12197            sanitize_name(&out)
12198        }
12199        Expr::New { cls, .. } => {
12200            let simple = cls.rsplit('/').next().unwrap_or(cls);
12201            let simple = simple.rsplit('$').next().unwrap_or(simple);
12202            let first = simple.chars().next()?;
12203            let mut out = String::with_capacity(simple.len());
12204            out.push(first.to_ascii_lowercase());
12205            out.extend(simple.chars().skip(1));
12206            sanitize_name(&out)
12207        }
12208        _ => None,
12209    }
12210}
12211
12212/// Valid java identifier, not a keyword/restricted name, ≥2 chars.
12213fn sanitize_name(s: &str) -> Option<String> {
12214    let mut out = String::with_capacity(s.len());
12215    for c in s.chars() {
12216        if c.is_ascii_alphanumeric() || c == '_' || c == '$' {
12217            out.push(c);
12218        } else {
12219            out.push('_');
12220        }
12221    }
12222    if out.len() < 2
12223        || out.chars().next().is_some_and(|c| c.is_ascii_digit())
12224        || is_java_keyword(&out)
12225    {
12226        return None;
12227    }
12228    Some(out)
12229}
12230
12231/// jadx's alias table for the common types, with the Android globals the
12232/// corpus actually shows; anything else falls back in the caller.
12233fn type_alias(ty: &TypeRef) -> Option<&'static str> {
12234    let TypeRef::J(JavaType::Object(n)) = ty else {
12235        return None;
12236    };
12237    Some(match n.as_ref() {
12238        "java/lang/String" | "kotlin/String" => "str",
12239        "java/lang/Class" => "cls",
12240        "java/lang/Throwable" => "th",
12241        "java/lang/Object" | "kotlin/Any" => "obj",
12242        "java/util/Iterator" | "kotlin/collections/Iterator" => "it",
12243        "java/lang/Boolean" => "bool",
12244        "java/lang/Integer" => "num",
12245        "java/lang/Character" => "ch",
12246        "java/lang/Byte" => "b",
12247        "java/lang/Short" => "sh",
12248        "java/lang/Float" => "f",
12249        "java/lang/Double" => "d",
12250        "java/lang/Long" => "i",
12251        "java/lang/StringBuilder" => "sb",
12252        "java/util/ArrayList" => "list",
12253        "java/util/HashMap" => "map",
12254        "android/content/Context" => "context",
12255        "android/content/Intent" => "intent",
12256        "android/os/Bundle" => "bundle",
12257        "android/view/View" => "view",
12258        "android/graphics/Bitmap" => "bitmap",
12259        "android/view/ViewGroup" => "viewGroup",
12260        _ => return None,
12261    })
12262}
12263
12264/// Every expression in the tree, statements included (read-only).
12265pub(crate) fn visit_all_exprs<F: FnMut(&Expr)>(s: &Stmt, f: &mut F) {
12266    walk_all(s, &mut |st| {
12267        let exprs: Vec<&Expr> = match st {
12268            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
12269                let mut v: Vec<&Expr> = vec![value];
12270                if !matches!(&**target, Expr::Local { .. }) {
12271                    v.push(target);
12272                }
12273                v
12274            }
12275            Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
12276                vec![e]
12277            }
12278            Stmt::Return(Some(e)) => vec![e],
12279            Stmt::LocalDef { init: Some(e), .. } => vec![e],
12280            Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
12281                vec![cond]
12282            }
12283            _ => Vec::new(),
12284        };
12285        for e in exprs {
12286            visit_exprs(e, f);
12287        }
12288    });
12289}
12290
12291/// IntDef/LongDef exact-match rendering: a literal argument to a
12292/// platform method whose parameter carries a constant domain renders as
12293/// the named constant (`setVisibility(8)` → `android.view.View.GONE`).
12294/// Combined flag values and literals outside the domain stay numeric.
12295pub fn platform_constants(body: &mut Stmt) {
12296    if !crate::platform::installed() {
12297        return;
12298    }
12299    rewrite_exprs(body, &mut |e| {
12300        if let Expr::Method { cls, name, desc, args, .. } = e {
12301            for a in args.iter_mut() {
12302                let (value, is_long) = match a {
12303                    Expr::Const(ConstVal::Int(v)) => (*v as i64, false),
12304                    Expr::Const(ConstVal::Long(v)) => (*v, true),
12305                    _ => continue,
12306                };
12307                if let Some(named) =
12308                    crate::platform::named_constant(cls, name, &desc_to_string(desc), 0, value)
12309                {
12310                    let _ = is_long;
12311                    *a = Expr::Raw(named);
12312                }
12313            }
12314        }
12315    });
12316}
12317
12318/// MethodDescriptor → dex descriptor string (`(I)V`).
12319fn desc_to_string(d: &jdc_core::types::MethodDescriptor) -> String {
12320    let mut out = String::from("(");
12321    for a in &d.args {
12322        type_to_desc(a, &mut out);
12323    }
12324    out.push(')');
12325    type_to_desc(&d.ret, &mut out);
12326    out
12327}
12328
12329fn type_to_desc(t: &jdc_core::types::JavaType, out: &mut String) {
12330    match t {
12331        JavaType::Void => out.push('V'),
12332        JavaType::Boolean => out.push('Z'),
12333        JavaType::Byte => out.push('B'),
12334        JavaType::Char => out.push('C'),
12335        JavaType::Short => out.push('S'),
12336        JavaType::Int => out.push('I'),
12337        JavaType::Float => out.push('F'),
12338        JavaType::Long => out.push('J'),
12339        JavaType::Double => out.push('D'),
12340        JavaType::Object(n) => {
12341            out.push('L');
12342            out.push_str(n);
12343            out.push(';');
12344        }
12345        JavaType::Array(inner) => {
12346            out.push('[');
12347            type_to_desc(inner, out);
12348        }
12349    }
12350}
12351
12352/// Kotlin `Intrinsics` null-check elision (jadx's ProcessKotlinInternals):
12353/// statement-position `checkNotNullParameter(p, "name")` /
12354/// `checkParameterIsNotNull` / `checkNotNull…` calls are runtime
12355/// assertions — drop them. The naming pass harvests the parameter
12356/// strings FIRST, so run this after `apply_local_names`.
12357pub fn remove_kotlin_checks(s: &mut Stmt) {
12358    walk_mut_deep(s, &mut |st| {
12359        let Stmt::Block(v) = st else { return };
12360        v.retain(|x| !is_kotlin_check(x));
12361    });
12362}
12363
12364fn is_kotlin_check(s: &Stmt) -> bool {
12365    let Stmt::ExprStmt(e) = s else { return false };
12366    let Expr::Method { cls, name, .. } = e else { return false };
12367    (cls.as_ref() == "kotlin/jvm/internal/Intrinsics" || cls.as_ref() == "kotlin/jvm/internal/IntrinsicsKt__Jdk7Kt")
12368        && matches!(
12369            name.as_ref(),
12370            "checkNotNullParameter"
12371                | "checkParameterIsNotNull"
12372                | "checkExpressionValueIsNotNull"
12373                | "checkNotNullExpressionValue"
12374                | "checkReturnedValueIsNotNull"
12375                | "checkFieldIsNotNull"
12376                | "checkNotNull"
12377        )
12378}
12379
12380// ---------------------------------------------------------------------------
12381// Synthetic-accessor inlining (jadx's MarkMethodsForInline, the safe subset)
12382// ---------------------------------------------------------------------------
12383
12384/// Inline `access$NNN`-style synthetic static bridges at their call
12385/// sites: an identity (`return pN`), a getter (`iget pX, field;
12386/// return vR`), or a method forwarder (`invoke; return`) — with the
12387/// d8 APM trace wrappers (`MethodCollector.i/.o` const-only static
12388/// calls) tolerated around the core. Only STATIC + SYNTHETIC callees
12389/// qualify (compiler-generated bridges: no override semantics, no
12390/// side effects beyond the forwarded operation).
12391/// Can `host` (internal class name) source-level reference `target`'s
12392/// class chain and member? Cross-package inlining of a synthetic
12393/// accessor used to EXPOSE package-private targets the raw dex never
12394/// referenced cross-package (BuildersKt.launch$default forwards to
12395/// kotlinx.coroutines.k.e — the widening census cannot see post-inline
12396/// calls; lark's 1,019-line k不是公共的 family). Non-public chain link
12397/// or non-public member across packages → the inline must not happen
12398/// (the public synthetic bridge stays and compiles).
12399fn inline_ref_ok(pool: &DexPool, host: &str, target: &str, member: Option<(&str, bool)>) -> bool {
12400    let hpkg = match host.rfind('/') {
12401        Some(i) => &host[..i],
12402        None => "",
12403    };
12404    let mut cur = target;
12405    loop {
12406        let tpkg = match cur.rfind('/') {
12407            Some(i) => &cur[..i],
12408            None => "",
12409        };
12410        if tpkg != hpkg {
12411            if let Some(pc) = pool.get(cur) {
12412                if pc.access & crate::access::ACC_PUBLIC == 0 {
12413                    return false;
12414                }
12415                if cur == target {
12416                    if let Some((mname, is_method)) = member {
12417                        let ok = if is_method {
12418                            pc.all_methods().any(|m| {
12419                                &*m.name == mname
12420                                    && m.access & crate::access::ACC_PUBLIC != 0
12421                            })
12422                        } else {
12423                            pc.static_fields
12424                                .iter()
12425                                .chain(pc.instance_fields.iter())
12426                                .any(|f| {
12427                                    f.name == mname
12428                                        && f.access & crate::access::ACC_PUBLIC != 0
12429                                })
12430                        };
12431                        // A member match requires the exact member to be
12432                        // public; an absent member (renamed/collapsed)
12433                        // stays allowed — the render will show what the
12434                        // pool has.
12435                        if !ok
12436                            && (if is_method {
12437                                pc.all_methods().any(|m| &*m.name == mname)
12438                            } else {
12439                                pc.static_fields
12440                                    .iter()
12441                                    .chain(pc.instance_fields.iter())
12442                                    .any(|f| f.name == mname)
12443                            })
12444                        {
12445                            return false;
12446                        }
12447                    }
12448                }
12449            }
12450        }
12451        match cur.rfind('$') {
12452            Some(i) if i > 0 => cur = &cur[..i],
12453            _ => break,
12454        }
12455    }
12456    true
12457}
12458
12459pub fn inline_accessors(s: &mut Stmt, pool: &DexPool, host: &str) {
12460    rewrite_exprs(s, &mut |e| {
12461        let Expr::Method { cls, name, desc, args, is_static, .. } = e else { return };
12462        if !*is_static || args.is_empty() {
12463            return;
12464        }
12465        let Some(target) = pool.get(cls) else { return };
12466        let desc_s = desc_to_string(desc);
12467        let Some(m) = target.find_method(name, &desc_s) else { return };
12468        if !(m.is_static() && m.access & access::ACC_SYNTHETIC != 0) {
12469            return;
12470        }
12471        let Some(dex) = pool.dex(m.dex_idx) else { return };
12472        // Snapshot first: the accessor's owning image may already be
12473        // retired, and a live read's success would depend on worker
12474        // completion interleaving — nondeterministic inlining (whether
12475        // the accessor folds at all) between identical runs.
12476        let code = match pool.accessor_code(m.dex_idx, m.code_off) {
12477            Some(bytes) => ddc_dex::CodeItem::parse(&bytes, 0),
12478            None => dex.code_at(m.code_off),
12479        };
12480        let Some(code) = code else { return };
12481        let insns: Vec<&Insn> =
12482            code.insns.iter().filter(|i| !matches!(i.kind, InsnKind::Nop)).collect();
12483        // Strip the APM trace wrappers (const-only invoke-static).
12484        let core: Vec<&Insn> = insns
12485            .iter()
12486            .copied()
12487            .filter(|i| !is_const_only_trace(&i.kind, &insns))
12488            .collect();
12489        match accessor_shape(&dex, &core, &code) {
12490            Some(Shape::Identity(arg_i)) => {
12491                if let Some(a) = args.get(arg_i) {
12492                    *e = a.clone();
12493                }
12494            }
12495            Some(Shape::FieldRead { arg, cls: field_cls, field, field_ty }) => {
12496                if !inline_ref_ok(pool, host, &field_cls, Some((&field, false))) {
12497                    return;
12498                }
12499                if let Some(owner) = args.get(arg).cloned().map(Box::new) {
12500                    let ty = TypeRef::J(desc_type(&field_ty));
12501                    // The inlined read must consult the member rename
12502                    // registry like every lifted field ref does — the
12503                    // raw dex name desynced from renamed declarations
12504                    // (lark uu4/f$b's `a`→`ax6`: 找不到符号 变量 a ×4.4k
12505                    // when the nested-collision deshadow fired).
12506                    let name = jdc_core::rename::field_display(
12507                        &field_cls,
12508                        &field,
12509                        &field_ty,
12510                    )
12511                    .map(std::sync::Arc::from)
12512                    .unwrap_or_else(|| field.into());
12513                    *e = Expr::Field {
12514                        owner: Some(owner),
12515                        cls: field_cls.into(),
12516                        name,
12517                        ty,
12518                        is_static: false,
12519                    };
12520                }
12521            }
12522            Some(Shape::Forward { cls: tcls, name: tname, desc: tdesc, instance }) => {
12523                if !inline_ref_ok(pool, host, &tcls, Some((&tname, true))) {
12524                    return;
12525                }
12526                // Same registry duty as FieldRead: the forwarded member
12527                // must render under its renamed display, not the raw
12528                // dex name.
12529                let disp = jdc_core::rename::field_display(
12530                    &tcls,
12531                    &tname,
12532                    &tdesc.to_string(),
12533                )
12534                .map(std::sync::Arc::from)
12535                .unwrap_or_else(|| tname.into());
12536                *cls = tcls.into();
12537                *name = disp;
12538                *desc = std::sync::Arc::new(tdesc);
12539                if instance && !args.is_empty() {
12540                    // The first param (the receiver) becomes the owner.
12541                    let recv = args.remove(0);
12542                    if let Expr::Method { owner, .. } = e {
12543                        *owner = Some(Box::new(recv));
12544                    }
12545                }
12546            }
12547            None => {}
12548        }
12549    });
12550}
12551
12552/// What an accessor body reduces to.
12553enum Shape {
12554    /// `return pN` — the call becomes argument N.
12555    Identity(usize),
12556    /// `iget vR, pX, field; return vR` — becomes `argN.field`.
12557    FieldRead { arg: usize, cls: String, field: String, field_ty: String },
12558    /// `invoke {pX, args…}, method@M; (move-result;)? return` —
12559    /// becomes the forwarded call.
12560    Forward { cls: String, name: String, desc: MethodDescriptor, instance: bool },
12561}
12562
12563/// Resolve the IGet field's owner class, name and type descriptor.
12564fn field_of(dex: &ddc_dex::DexFile, field_idx: u32) -> Option<(String, String, String)> {
12565    let f = dex.field(field_idx);
12566    Some((
12567        // class_name strips the `L...;` shell; type_name would leak a
12568        // descriptor into the Field's owner class (`La3.a.e_`).
12569        dex.class_name(f.class_idx),
12570        dex.string(f.name_idx).to_string(),
12571        // The FIELD TYPE stays a descriptor: desc_type parses it.
12572        dex.type_name(f.type_idx).to_string(),
12573    ))
12574}
12575
12576/// Map a register to a parameter index (static method: params occupy
12577/// the LAST ins_size registers).
12578fn param_index(code: &ddc_dex::CodeItem, reg: u16) -> Option<usize> {
12579    let rs = code.registers_size as usize;
12580    let ins = code.ins_size as usize;
12581    let r = reg as usize;
12582    let first = rs.checked_sub(ins)?;
12583    (r >= first && r < rs).then(|| r - first)
12584}
12585
12586fn accessor_shape(
12587    dex: &ddc_dex::DexFile,
12588    core: &[&Insn],
12589    code: &ddc_dex::CodeItem,
12590) -> Option<Shape> {
12591    // Dead consts that fed the stripped trace wrappers remain in the
12592    // core — skip them. ONLY when actually dead (dst unread by the
12593    // rest): a const-returning synthetic (`lambda$new$4(String) {
12594    // return false; }` = `const/4 v0,0; return v0`, registers=1 so v0
12595    // IS the param) lost its const and matched Identity(param0) —
12596    // inline_accessors then replaced the CALL with its ARGUMENT
12597    // (`return (String) obj` against a boolean SAM — weibo
12598    // IntentSanitizer lambdas ×116, and silently WRONG renders
12599    // wherever the types happened to agree).
12600    let reads = |rest: &[&Insn], r: u16| {
12601        rest.iter()
12602            .any(|i| crate::lift::src_regs(&i.kind).contains(&r))
12603    };
12604    let core: &[&Insn] = match core.split_first() {
12605        Some((first, rest))
12606            if matches!(first.kind, InsnKind::Const { dst, .. } if !reads(rest, dst)) =>
12607        {
12608            rest
12609        }
12610        _ => core,
12611    };
12612    let core: &[&Insn] = match core.split_last() {
12613        Some((last, rest))
12614            if matches!(last.kind, InsnKind::Const { dst, .. } if !reads(rest, dst)) =>
12615        {
12616            rest
12617        }
12618        _ => core,
12619    };
12620    match core {
12621        // return pN
12622        [Insn { kind: InsnKind::Return { src }, .. }] => {
12623            let idx = param_index(code, *src)?;
12624            Some(Shape::Identity(idx))
12625        }
12626        // iget vR, pX, field; return vR
12627        [
12628            Insn { kind: InsnKind::IGet { dst, obj, field_idx }, .. },
12629            Insn { kind: InsnKind::Return { src }, .. },
12630        ] if dst == src => {
12631            let arg = param_index(code, *obj)?;
12632            let (cls, field, field_ty) = field_of(dex, *field_idx)?;
12633            Some(Shape::FieldRead { arg, cls, field, field_ty })
12634        }
12635        // invoke {…}, method@M; (move-result; return)? — forwarder.
12636        [Insn { kind: InsnKind::Invoke { method_idx, regs, kind, .. }, .. }, rest @ ..] if rest.len() <= 2 => {
12637            // Only forward when every register is a param (no locals).
12638            if !regs.iter().all(|r| param_index(code, *r).is_some()) || regs.is_empty() {
12639                return None;
12640            }
12641            // Trailing must be move-result+return or return/void —
12642            // anything else (e.g. trace calls) already filtered upstream.
12643            if rest.len() == 2 {
12644                let ok = matches!(rest[0].kind, InsnKind::MoveResult { .. })
12645                    && matches!(rest[1].kind, InsnKind::Return { .. } | InsnKind::ReturnVoid);
12646                if !ok {
12647                    return None;
12648                }
12649            }
12650            let mid = dex.method(*method_idx);
12651            // class_name strips the `L...;` shell — type_name leaks a
12652            // descriptor into the forwarded call's class (`La3.a.e_`).
12653            let cls = dex.class_name(mid.class_idx);
12654            let name = dex.string(mid.name_idx).to_string();
12655            let mut s = String::from("(");
12656            for t in dex.proto_params(mid.proto_idx) {
12657                s.push_str(dex.type_name(*t));
12658            }
12659            s.push(')');
12660            s.push_str(dex.type_name(dex.proto(mid.proto_idx).return_type_idx));
12661            let desc = parse_method_descriptor(&s)?;
12662            Some(Shape::Forward { cls, name, desc, instance: !matches!(kind, InvokeKind::Static) })
12663        }
12664        _ => None,
12665    }
12666}
12667
12668
12669/// A const-only invoke-static (the d8 APM `i(732046)` / `o(732046)`
12670/// trace wrapper): every argument register is loaded by a Const (or a
12671/// Const-into-move chain) in the SAME body. Scoped to synthetic
12672/// accessors, so a genuinely side-effecting const call is never
12673/// mistaken for a trace.
12674fn is_const_only_trace(kind: &InsnKind, insns: &[&Insn]) -> bool {
12675    let InsnKind::Invoke { kind, regs, .. } = kind else { return false };
12676    if !matches!(kind, InvokeKind::Static) || regs.is_empty() {
12677        return false;
12678    }
12679    regs.iter().all(|r| const_loaded(*r, insns, 0))
12680}
12681
12682/// Const-loaded directly, or through a Move chain from a const-loaded
12683/// register (the APM `const v0, id` → `invoke {v0}` shape).
12684fn const_loaded(r: u16, insns: &[&Insn], depth: u8) -> bool {
12685    if depth > 3 {
12686        return false;
12687    }
12688    insns.iter().any(|i| match &i.kind {
12689        InsnKind::Const { dst, .. } => *dst == r,
12690        InsnKind::Move { dst, src } => *dst == r && const_loaded(*src, insns, depth + 1),
12691        _ => false,
12692    })
12693}