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/// Subclass-field downcast witness + assign-cast narrowing.
3458///
3459/// R8 pushes fields DOWN into leaf classes (weibo: coroutine `label`
3460/// lives on each `Foo$bar$1` continuation, NOT on the rendered
3461/// BaseContinuationImpl/ContinuationImpl supers) while a check-cast in
3462/// the same flow types the receiver as the SUPERCLASS. The dex field
3463/// ref names the leaf (`$1->label` — verifier-legal through the
3464/// instance-of branch type), but Java resolves `((ContinuationImpl) c)
3465/// .label` against ContinuationImpl — "找不到符号 变量 label" (weibo
3466/// ×420). When the field's dex owner is a strict subtype of the
3467/// receiver's static type and the receiver's hierarchy does NOT
3468/// declare the name, re-target the receiver cast to the owner (the
3469/// runtime object IS the owner type — the verifier proved it).
3470///
3471/// Second rule: a `(T) e` value assigned into an S-typed target where
3472/// S <: T strictly ("ContinuationImpl无法转换为$reportWhenComplete$1")
3473/// narrows the cast to S — same verifier guarantee, and an assignment
3474/// position has no overload resolution to perturb.
3475pub fn fix_field_owner_downcasts(body: &mut Stmt, vt: &VarTable, pool: &DexPool) {
3476    fn owner_internal(o: &Expr, vt: &VarTable) -> Option<std::sync::Arc<str>> {
3477        let t = match o {
3478            Expr::Local { var, .. } => vt.var(*var).ty.erased(),
3479            other => other.type_ref().erased(),
3480        };
3481        match t {
3482            JavaType::Object(n) => Some(n),
3483            _ => None,
3484        }
3485    }
3486    fn declares_up(pool: &DexPool, class: &str, name: &str) -> bool {
3487        let mut cur = Some(class.to_string());
3488        let mut hops = 0;
3489        while let Some(c) = cur {
3490            hops += 1;
3491            if hops > 64 {
3492                return false;
3493            }
3494            let Some(pc) = pool.get(&c) else { return false };
3495            if pc
3496                .instance_fields
3497                .iter()
3498                .chain(pc.static_fields.iter())
3499                .any(|f| f.name.as_str() == name)
3500            {
3501                return true;
3502            }
3503            cur = pc.super_name.clone();
3504        }
3505        false
3506    }
3507    walk_stmt_exprs(body, &mut |e| {
3508        deep_rewrite(e, &mut |x| {
3509            if let Expr::Field {
3510                owner: Some(o),
3511                cls,
3512                name,
3513                is_static: false,
3514                ..
3515            } = x
3516            {
3517                let Some(ot) = owner_internal(o, vt) else {
3518                    return;
3519                };
3520                if ot.as_ref() == cls.as_ref() || !pool.is_subtype(cls, &ot) {
3521                    return;
3522                }
3523                if declares_up(pool, &ot, name) || !declares_up(pool, cls, name) {
3524                    return;
3525                }
3526                let ty = TypeRef::J(JavaType::Object(cls.clone()));
3527                if let Expr::Cast { ty: ct, .. } = &mut **o {
3528                    *ct = ty;
3529                } else {
3530                    let taken = std::mem::replace(&mut **o, Expr::This);
3531                    **o = Expr::Cast { ty, e: Box::new(taken) };
3532                }
3533            }
3534            // Rule 2: `(T) e` into a strict-subtype target narrows to S.
3535            let (tgt_ty, value) = match x {
3536                Expr::Assign { target, op: AssignOp::Plain, value } => {
3537                    let t = match &**target {
3538                        Expr::Local { var, .. } => vt.var(*var).ty.erased(),
3539                        Expr::Field { ty, .. } => ty.erased(),
3540                        _ => return,
3541                    };
3542                    (t, value)
3543                }
3544                _ => return,
3545            };
3546            let JavaType::Object(sn) = tgt_ty else { return };
3547            if let Expr::Cast { ty, .. } = &mut **value {
3548                if let TypeRef::J(JavaType::Object(tn)) = ty {
3549                    if tn.as_ref() != sn.as_ref() && pool.is_subtype(&sn, tn) {
3550                        *ty = TypeRef::J(JavaType::Object(sn.clone()));
3551                    }
3552                }
3553            }
3554        });
3555    });
3556    // LocalDef inits: same narrowing against the declared var type.
3557    walk_mut_deep(body, &mut |st| {
3558        if let Stmt::LocalDef { var, init: Some(value), .. } = st {
3559            let tt = if (*var as usize) < vt.vars.len() {
3560                vt.vars[*var as usize].ty.erased()
3561            } else {
3562                return;
3563            };
3564            let JavaType::Object(sn) = tt else { return };
3565            if let Expr::Cast { ty, .. } = value {
3566                if let TypeRef::J(JavaType::Object(tn)) = ty {
3567                    if tn.as_ref() != sn.as_ref() && pool.is_subtype(&sn, tn) {
3568                        *ty = TypeRef::J(JavaType::Object(sn.clone()));
3569                    }
3570                }
3571            }
3572        }
3573    });
3574}
3575
3576/// `==`/`!=` between UNRELATED class types is a compile error in Java
3577/// ("不可比较的类型: f0和a") while dex if-eqObj happily compares any two
3578/// references (weixin fn1/w1 ×298 files). One side takes an `(Object)`
3579/// witness cast — legal against anything. Interfaces and subtype-related
3580/// pairs are already comparable and stay untouched; java/lang/Object and
3581/// arrays-of it likewise.
3582pub fn fix_incomparable_equality(body: &mut Stmt, vt: &VarTable, pool: &crate::DexPool) {
3583    fn side_ty(e: &Expr, vt: &VarTable) -> JavaType {
3584        match e {
3585            Expr::Local { var, .. } if (*var as usize) < vt.vars.len() => {
3586                vt.var(*var).ty.erased()
3587            }
3588            other => other.type_ref().erased(),
3589        }
3590    }
3591    let incomparable = |a: &JavaType, b: &JavaType| -> bool {
3592        use JavaType::{Array, Object};
3593        match (a, b) {
3594            (Object(x), Object(y)) => {
3595                if x == y || x.as_ref() == "java/lang/Object" || y.as_ref() == "java/lang/Object"
3596                {
3597                    return false;
3598                }
3599                let xi = pool.get(x).map(|c| c.is_interface()).unwrap_or(true);
3600                let yi = pool.get(y).map(|c| c.is_interface()).unwrap_or(true);
3601                // Unknown classes (framework, not in pool): a cast is
3602                // harmless but usually unnecessary — treat as comparable
3603                // to stay conservative.
3604                if xi || yi {
3605                    return false;
3606                }
3607                !pool.is_subtype(x, y) && !pool.is_subtype(y, x)
3608            }
3609            (Array(_), Object(y)) | (Object(y), Array(_)) => {
3610                y.as_ref() != "java/lang/Object"
3611                    && y.as_ref() != "java/lang/Cloneable"
3612                    && y.as_ref() != "java/io/Serializable"
3613                    && !pool.get(y).map(|c| c.is_interface()).unwrap_or(false)
3614            }
3615            _ => false,
3616        }
3617    };
3618    let is_ref = |t: &JavaType| matches!(t, JavaType::Object(_) | JavaType::Array(_));
3619    walk_stmt_exprs(body, &mut |e| {
3620        deep_rewrite(e, &mut |x| {
3621            if let Expr::Bin {
3622                op: BinOp::Eq | BinOp::Ne,
3623                l,
3624                r,
3625                ..
3626            } = x
3627            {
3628                let lt = side_ty(l, vt);
3629                let rt = side_ty(r, vt);
3630                // `obj != 0` → `obj != null` with FINAL types: the
3631                // lift-time null_side_rewrite ran before infer_types
3632                // (its obj table saw the pre-inference int), leaving
3633                // `sQLiteClosable != 0` — "二元运算符 '!=' 的操作数
3634                // 类型错误" ×700 weixin.
3635                if is_ref(&lt) && matches!(&**r, Expr::Const(ConstVal::Int(0))) {
3636                    **r = Expr::Const(ConstVal::Null);
3637                    return;
3638                }
3639                if is_ref(&rt) && matches!(&**l, Expr::Const(ConstVal::Int(0))) {
3640                    **l = Expr::Const(ConstVal::Null);
3641                    return;
3642                }
3643                if incomparable(&lt, &rt) {
3644                    let taken = std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
3645                    **l = Expr::Cast {
3646                        ty: TypeRef::J(JavaType::Object("java/lang/Object".into())),
3647                        e: taken,
3648                    };
3649                }
3650            }
3651        });
3652    });
3653}
3654
3655/// Primitive bridges at CALL/CTOR argument positions. Dex slots are
3656/// untyped category-1 integers: the verifier happily passes a Z-slot to
3657/// a `(B)` formal (weibo's Meituan-Robust hotpatch boilerplate boxes
3658/// boolean params via `new Byte(zreg)` — 4.2k+ `对于Byte(boolean),
3659/// 找不到合适的构造器`), and obfuscated/Kotlin code passes 0/1 slots to
3660/// `(Z)` formals (`int无法转换为boolean` at call sites). Java source
3661/// cannot express either — bridge at the IR level:
3662///   Boolean actual → numeric formal:  `(byte) (b ? 1 : 0)`
3663///   numeric actual → Boolean formal:  `x != 0`
3664/// The target method is DESCRIPTOR-bound (bytecode method_ref), so a
3665/// cast/compare to the exact formal type cannot flip overload
3666/// resolution the way r57's inferred object casts did — the rendered
3667/// call selects the same signature the dex named.
3668pub fn fix_primitive_arg_bridges(body: &mut Stmt, vt: &VarTable, pool: &crate::DexPool) {
3669    fn val_ty(e: &Expr, vt: &VarTable) -> JavaType {
3670        match e {
3671            Expr::Local { var, .. } => vt.var(*var).ty.erased(),
3672            other => other.type_ref().erased(),
3673        }
3674    }
3675    #[allow(clippy::type_complexity)]
3676    let is_bool = |t: &JavaType| matches!(t, JavaType::Boolean);
3677    let is_num = |t: &JavaType| {
3678        matches!(
3679            t,
3680            JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
3681                | JavaType::Long | JavaType::Float | JavaType::Double
3682        )
3683    };
3684    // BORROWED-arg form: returns the bridged replacement without
3685    // consuming the original (a `mem::replace` + `Option` dance dropped
3686    // the argument on the None path — every Boolean→Boolean arg became
3687    // a literal `null`: 61k 引用不明确 in one battery run).
3688    fn bridge(actual: &JavaType, formal: &JavaType, arg: &Expr) -> Option<Expr> {
3689        // A dex const carries the CALLEE's BITS: `const v, #0x41000000`
3690        // feeding a float formal IS 8.0f. Rendering the raw int literal
3691        // (a) mis-selects overloads — a competing `a(Context,int):void`
3692        // beats the int→float-widened `a(Context,float):int` in javac's
3693        // phase order ("不兼容的类型: void无法转换为int", weibo ju.a
3694        // ×1,259) — and (b) corrupts the VALUE (widening turns the bits
3695        // into 1.09e9f). The descriptor formal is dex ground truth:
3696        // reinterpret. Symmetric for the float-const-into-int-formal
3697        // direction (caller's bits, callee's reading).
3698        match (arg, formal) {
3699            (Expr::Const(ConstVal::Int(b)), JavaType::Float) => {
3700                return Some(Expr::Const(ConstVal::Float(f32::from_bits(*b as u32))));
3701            }
3702            (Expr::Const(ConstVal::Long(b)), JavaType::Double) => {
3703                return Some(Expr::Const(ConstVal::Double(f64::from_bits(*b as u64))));
3704            }
3705            (Expr::Const(ConstVal::Float(f)), JavaType::Int) => {
3706                return Some(Expr::Const(ConstVal::Int(f.to_bits() as i32)));
3707            }
3708            (Expr::Const(ConstVal::Double(d)), JavaType::Long) => {
3709                return Some(Expr::Const(ConstVal::Long(d.to_bits() as i64)));
3710            }
3711            _ => {}
3712        }
3713        if matches!(actual, JavaType::Boolean)
3714            && matches!(
3715                formal,
3716                JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
3717                    | JavaType::Long | JavaType::Float | JavaType::Double
3718            )
3719        {
3720            Some(Expr::Cast {
3721                ty: TypeRef::J(formal.clone()),
3722                e: Box::new(Expr::Cond {
3723                    c: Box::new(arg.clone()),
3724                    t: Box::new(Expr::Const(ConstVal::Int(1))),
3725                    f: Box::new(Expr::Const(ConstVal::Int(0))),
3726                }),
3727            })
3728        } else if matches!(formal, JavaType::Boolean)
3729            && matches!(
3730                actual,
3731                JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
3732                    | JavaType::Long | JavaType::Float | JavaType::Double
3733            )
3734        {
3735            Some(Expr::Bin {
3736                op: BinOp::Ne,
3737                l: Box::new(arg.clone()),
3738                r: Box::new(Expr::Const(ConstVal::Int(0))),
3739                ty: Some(TypeRef::J(JavaType::Boolean)),
3740            })
3741        } else if matches!(
3742            actual,
3743            JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
3744                | JavaType::Long | JavaType::Float | JavaType::Double
3745        ) && matches!(
3746            formal,
3747            JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
3748                | JavaType::Long | JavaType::Float | JavaType::Double
3749        ) && !implicit_widening(actual, formal)
3750        {
3751            // Numeric NARROWING into a formal (int arg → char formal):
3752            // javac rejects the lossy conversion, or — worse when the
3753            // class has no wider overload — silently resolves a
3754            // DIFFERENT overload than the dex descriptor names
3755            // (SpannableStringBuilder.append(int) doesn't exist;
3756            // `append(char)` is "从int转换到char可能会有损失", weibo ×76).
3757            // The descriptor formal is the dex ground truth; casting to
3758            // it pins the overload the bytecode actually invoked.
3759            Some(Expr::Cast {
3760                ty: TypeRef::J(formal.clone()),
3761                e: Box::new(arg.clone()),
3762            })
3763        } else {
3764            None
3765        }
3766    }
3767    /// A bare `null` argument against a SPECIFIC reference formal takes
3768    /// the descriptor cast: `this(context, null)` matching both
3769    /// Builder(Context,Notification) and Builder(Context,String) is
3770    /// "对Builder的引用不明确" (weibo NotificationCompat; protobuf
3771    /// `new FieldSet.Builder(null)` ×54). The descriptor names the
3772    /// exact invoked ctor, so the cast is dex ground truth — the
3773    /// descriptor-exact doctrine, not a guessed overload shift. Bare
3774    /// null stays for Object formals (casting those would add noise
3775    /// without resolving anything javac could not already pick).
3776    /// Framework classes javac resolves through android.jar even when
3777    /// the dex pool lacks them — the embedded API database answers
3778    /// precisely (the hand-written prefix list it replaced missed
3779    /// everything outside the guessed roots and over-trusted within
3780    /// them; removed-API families like org.apache.http are in the DB
3781    /// too, matching what a legacy-jar classpath resolves).
3782    fn fw_resolvable(n: &str) -> bool {
3783        crate::fwdb::exists(n)
3784    }
3785    fn null_arg_cast(a: &mut Expr, f: &JavaType, pool: &DexPool) -> bool {
3786        if !matches!(&*a, Expr::Const(ConstVal::Null)) {
3787            return false;
3788        }
3789        if let JavaType::Object(n) = f {
3790            // Cast only to RESOLVABLE types: a phantom formal (R8 kept
3791            // the ref, dropped the class — androidx.window.sidecar)
3792            // would turn a compiling bare `null` into a fresh
3793            // cannot-find. Pool classes render into the output;
3794            // java/javax/android/dalvik resolve through android.jar.
3795            let resolvable = pool.get(n).is_some() || fw_resolvable(n);
3796            if n.as_ref() != "java/lang/Object" && resolvable {
3797                *a = Expr::Cast {
3798                    ty: TypeRef::J(f.clone()),
3799                    e: Box::new(Expr::Const(ConstVal::Null)),
3800                };
3801                return true;
3802            }
3803        } else if let JavaType::Array(_) = f {
3804            // Descriptor-exact ARRAY cast: the dex named this exact
3805            // overload; a bare null beside sibling overloads is javac-
3806            // ambiguous (`a(String,b2)` + `a(String,String[])` both match
3807            // — weibo org/a/c/b, 对a的引用不明确 family). Same phantom
3808            // gate on the element class: casting to a phantom-element
3809            // array would trade the ambiguity for a cannot-find.
3810            let mut e: &JavaType = f;
3811            while let JavaType::Array(inner) = e {
3812                e = &**inner;
3813            }
3814            let resolvable = match e {
3815                JavaType::Object(n) => pool.get(n).is_some() || fw_resolvable(n),
3816                _ => true, // primitive-element array
3817            };
3818            if resolvable {
3819                *a = Expr::Cast {
3820                    ty: TypeRef::J(f.clone()),
3821                    e: Box::new(Expr::Const(ConstVal::Null)),
3822                };
3823                return true;
3824            }
3825        }
3826        // Bare null deliberately stays (Object formal / phantom type):
3827        // the caller's ambiguity pin may still act on it.
3828        false
3829    }
3830    /// JLS 5.1.2 widening primitive conversion (char joins the chain
3831    /// only at int; byte/short do NOT widen to char).
3832    fn implicit_widening(a: &JavaType, f: &JavaType) -> bool {
3833        use JavaType::*;
3834        match (a, f) {
3835            (Byte, Short) | (Byte, Int) | (Byte, Long) | (Byte, Float) | (Byte, Double) => true,
3836            (Short, Int) | (Short, Long) | (Short, Float) | (Short, Double) => true,
3837            (Char, Int) | (Char, Long) | (Char, Float) | (Char, Double) => true,
3838            (Int, Long) | (Int, Float) | (Int, Double) => true,
3839            (Long, Float) | (Long, Double) => true,
3840            (Float, Double) => true,
3841            (x, y) => x == y,
3842        }
3843    }
3844    // Does the owner class declare ANOTHER method with the same name
3845    // and arity but a different signature? Then javac's most-specific
3846    // overload resolution can steal the call from the dex descriptor's
3847    // target (weibo 对a的引用不明确 ×391: `c.a(null)` against
3848    // a(ExecuteResult)/a(Exception)/a(Object) — the descriptor names
3849    // a(Object), but bare null leaves the two specific overloads
3850    // ambiguously most-specific). Framework owners are not in the
3851    // pool — conservatively report no competition.
3852    let overload_competes = |cls: &str, name: &str, args: &[JavaType]| -> bool {
3853        if let Some(c) = pool.get(cls) {
3854            return c.all_methods().any(|m| {
3855                &*m.name == name
3856                    // Bridge/synthetic siblings are compiler artifacts
3857                    // resolving to the SAME target — counting them as
3858                    // competitors cast enum compareTo args to the erased
3859                    // formal the rendered specialization no longer
3860                    // accepts (Enum无法转换为LogLevel ×2,079 reqable).
3861                    && m.access & (crate::access::ACC_BRIDGE | crate::access::ACC_SYNTHETIC) == 0
3862                    && m.parsed_desc()
3863                        .is_some_and(|d| d.args.len() == args.len() && d.args != args)
3864            });
3865        }
3866        // Framework owner: NOT enumerable for pin purposes — the
3867        // full-DB enumeration experiment (step-5) measured +532 battery
3868        // with ZERO ambiguous gain: a subtype arg against a concrete
3869        // erased formal fires the cast even when the call was never
3870        // ambiguous, and bounded-generic formals (List<T> -> List,
3871        // Property<T,Float> erasures) make the cast break inference or
3872        // miss the rendered specialization. Framework subtype pins stay
3873        // on the measured allowlist below.
3874        let _ = args;
3875        false
3876    };
3877    // Ctors: formal types via the pool (unanimous across same-arity
3878    // overloads, else skip — ambiguity must not guess).
3879    let ctor_formals = |cls: &str, n: usize| -> Option<Vec<JavaType>> {
3880        let c = pool.get(cls)?;
3881        let mut out: Option<Vec<JavaType>> = None;
3882        for m in c.all_methods() {
3883            if &*m.name != "<init>" {
3884                continue;
3885            }
3886            let Some(d) = m.parsed_desc() else { continue };
3887            if d.args.len() != n {
3888                continue;
3889            }
3890            match &out {
3891                None => out = Some(d.args.clone()),
3892                Some(prev) if *prev != d.args => return None,
3893                Some(_) => {}
3894            }
3895        }
3896        out
3897    };
3898    walk_stmt_exprs(body, &mut |e| {
3899        deep_rewrite(e, &mut |x| match x {
3900            Expr::Method {
3901                cls,
3902                name,
3903                desc,
3904                args,
3905                is_static,
3906                ..
3907            } => {
3908                if args.len() != desc.args.len() {
3909                    return;
3910                }
3911                let _ = is_static;
3912                let mut competes: Option<bool> = None;
3913                for (a, f) in args.iter_mut().zip(desc.args.iter()) {
3914                    if null_arg_cast(a, f, pool) {
3915                        continue;
3916                    }
3917                    if matches!(a, Expr::Cast { .. }) {
3918                        continue;
3919                    }
3920                    // Descriptor-exact ambiguity pin: a bare null
3921                    // against an Object formal (null_arg_cast
3922                    // deliberately leaves those bare — safe only
3923                    // WITHOUT competing specific overloads) or a
3924                    // strict-subtype arg can match several declared
3925                    // overloads; casting to the descriptor formal
3926                    // leaves exactly the dex-invoked method
3927                    // applicable (JLS 15.12.2 — the descriptor is
3928                    // ground truth, so the cast pins resolution
3929                    // instead of shifting it).
3930                    let subtype_arg = match (&val_ty(a, vt), f) {
3931                        (JavaType::Object(an), JavaType::Object(fn_)) => {
3932                            // java/lang/Object is the root: ANY reference
3933                            // arg is a strict subtype of it — pool.is_subtype
3934                            // cannot see framework chains (String -> Object),
3935                            // so the Object formal gets the direct rule.
3936                            (an != fn_ && pool.is_subtype(an, fn_))
3937                                || (fn_.as_ref() == "java/lang/Object"
3938                                    && an.as_ref() != "java/lang/Object")
3939                        }
3940                        (JavaType::Array(_), JavaType::Object(fn_))
3941                            if fn_.as_ref() == "java/lang/Object" =>
3942                        {
3943                            true
3944                        }
3945                        _ => false,
3946                    };
3947                    // Bare null against java/lang/Object itself: the
3948                    // descriptor method exists (dex resolved it), so
3949                    // `(Object) null` is always legal and excludes every
3950                    // more-specific sibling overload from applicability.
3951                    // Pool owners take the competes gate (emitted
3952                    // declarations are raw, so the cast cannot disturb
3953                    // inference). FRAMEWORK owners only via allowlist of
3954                    // the classic non-generic null-ambiguous JDK methods
3955                    // (StringBuilder.append ×66 lark): a blanket cast
3956                    // breaks GENERIC framework methods whose descriptor
3957                    // formal is an ERASED type variable —
3958                    // `ofFloat((Object) null, View.ALPHA, v)` killed the
3959                    // T=View inference bare null provided (weibo +4).
3960                    let null_obj =
3961                        matches!(a, Expr::Const(ConstVal::Null)) && matches!(f, JavaType::Object(_));
3962                    let framework_owner = pool.get(cls.as_ref()).is_none();
3963                    let fw_null_ambiguous = framework_owner
3964                        && matches!(
3965                            (cls.as_ref(), name.as_ref()),
3966                            ("java/lang/StringBuilder", "append")
3967                                | ("java/lang/StringBuffer", "append")
3968                                | ("java/io/PrintStream", "println")
3969                                | ("java/io/PrintStream", "print")
3970                                | ("java/io/PrintWriter", "println")
3971                                | ("java/io/PrintWriter", "print")
3972                                | ("java/lang/String", "valueOf")
3973                        );
3974                    // Framework owners with famously overload-heavy
3975                    // methods take the subtype pin too: the arg's
3976                    // rendered type implements SEVERAL formal types
3977                    // (weibo putExtra ×41: floatMsgData is both
3978                    // Parcelable and Serializable; schedule/submit:
3979                    // Runnable-and-Callable workers) and the competes
3980                    // gate cannot enumerate framework overloads.
3981                    // Formal == java/lang/Object stays excluded — that
3982                    // is an erased type variable and the cast would
3983                    // kill generic inference (ofFloat lesson).
3984                    let fw_subtype_pinnable = framework_owner
3985                        && subtype_arg
3986                        && !matches!(f, JavaType::Object(fn_) if fn_.as_ref() == "java/lang/Object")
3987                        && matches!(
3988                            (cls.as_ref(), name.as_ref()),
3989                            ("android/content/Intent", "putExtra")
3990                                | ("java/util/concurrent/ScheduledExecutorService", "schedule")
3991                                | ("java/util/concurrent/ExecutorService", "submit")
3992                                | ("java/util/concurrent/ExecutorService", "invokeAll")
3993                        );
3994                    if fw_subtype_pinnable
3995                        || (subtype_arg
3996                            && *competes.get_or_insert_with(|| {
3997                                overload_competes(cls, name, &desc.args)
3998                            }))
3999                        || (null_obj
4000                            && (fw_null_ambiguous
4001                                || *competes.get_or_insert_with(|| {
4002                                    overload_competes(cls, name, &desc.args)
4003                                })))
4004                    {
4005                        let old =
4006                            std::mem::replace(a, Expr::Const(ConstVal::Null));
4007                        *a = Expr::Cast {
4008                            ty: TypeRef::J(f.clone()),
4009                            e: Box::new(old),
4010                        };
4011                        continue;
4012                    }
4013                    if !is_bool(&val_ty(a, vt)) && !is_num(&val_ty(a, vt)) {
4014                        continue;
4015                    }
4016                    if !is_bool(f) && !is_num(f) {
4017                        continue;
4018                    }
4019                    if let Some(b) = bridge(&val_ty(a, vt), f, a) {
4020                        *a = b;
4021                    }
4022                }
4023            }
4024            Expr::New { cls, args, arg_tys, .. } => {
4025                let formals_owned = if arg_tys.len() == args.len() && !arg_tys.is_empty() {
4026                    Some(arg_tys.clone())
4027                } else {
4028                    ctor_formals(cls, args.len())
4029                };
4030                if let Some(formals) = formals_owned {
4031                    for (a, f) in args.iter_mut().zip(formals.iter()) {
4032                        if null_arg_cast(a, f, pool) {
4033                            continue;
4034                        }
4035                        if matches!(a, Expr::Cast { .. }) {
4036                            continue;
4037                        }
4038                        if !is_bool(f) && !is_num(f) {
4039                            continue;
4040                        }
4041                        if let Some(b) = bridge(&val_ty(a, vt), f, a) {
4042                            *a = b;
4043                        }
4044                    }
4045                }
4046            }
4047            _ => {}
4048        });
4049    });
4050}
4051
4052/// `b ^ 1` / `1 ^ b` with a BOOLEAN operand is the bytecode's boolean
4053/// negation (`!b`) — weibo kotlin-stdlib `return v2 ^ 1;` ("二元运算符
4054/// '^' 的操作数类型错误"), and `x ^ true` shapes. Java only types `^`
4055/// for same-category operands.
4056/// Rename locals whose name equals a ROOT PACKAGE first segment: a
4057/// package-qualified render (`v2.n.a` — the FQN fallback a twin-shadowed
4058/// same-package ref must take) binds its first identifier lexically, and
4059/// a local `v2` in scope captures it (weixin v2/i: 找不到符号 变量 n on a
4060/// legit `v2.n.a(this, 0)`). Method-scope renames carry no registry
4061/// surface — every use renders through this same vt — unlike class/field
4062/// renames, whose reference coverage blew up five times before.
4063pub fn deshadow_locals(vt: &mut VarTable, pool: &crate::DexPool, class_name: &str, body: &Stmt) {
4064    let segs = pool.root_pkg_segs();
4065    // Import-layer simples: the file renders `import p.h;` + bare `h.e`
4066    // for obscured refs — a local `h` captures that simple name (locals
4067    // beat imports in expression position; 无法取消引用int family).
4068    let obscured = crate::classdec::obscured_simples_snapshot();
4069    // Same-package static-ref simples: a static member ref renders its
4070    // class qualifier as the SIMPLE name (same package — no import/FQN),
4071    // and a local with that name captures it (`a2.changeQuickRedirect`
4072    // bound to a String local `a2` — "找不到符号 变量 changeQuickRedirect
4073    // 位置: 类型为String的变量 a2", weibo robust-field refs ×317). Only
4074    // STATIC refs qualify (an instance ref's qualifier IS the local).
4075    let self_pkg = class_name.rfind('/').map(|i| &class_name[..i]).unwrap_or("");
4076    let mut same_pkg_simples: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
4077    visit_all_exprs(body, &mut |x| {
4078        let cls = match x {
4079            Expr::Field { cls, is_static: true, .. } => Some(cls.as_ref()),
4080            Expr::Method { cls, is_static: true, .. } => Some(cls.as_ref()),
4081            _ => None,
4082        };
4083        if let Some(c) = cls {
4084            let pkg = c.rfind('/').map(|i| &c[..i]).unwrap_or("");
4085            if pkg == self_pkg {
4086                let simple = &c[c.rfind('/').map(|i| i + 1).unwrap_or(0)..];
4087                let head = simple.split('$').next().unwrap_or(simple);
4088                if !head.is_empty() {
4089                    same_pkg_simples.insert(head.to_string());
4090                }
4091            }
4092        }
4093    });
4094    let hit = |name: &str| {
4095        segs.contains(name) || obscured.contains(name) || same_pkg_simples.contains(name)
4096    };
4097    if (segs.is_empty() && obscured.is_empty() && same_pkg_simples.is_empty())
4098        || !vt.vars.iter().any(|v| hit(&v.name))
4099    {
4100        return;
4101    }
4102    let mut taken: jdc_core::FxHashSet<String> =
4103        vt.vars.iter().map(|v| v.name.clone()).collect();
4104    for v in vt.vars.iter_mut() {
4105        if !hit(&v.name) {
4106            continue;
4107        }
4108        let mut k = 0u32;
4109        let cand = loop {
4110            k += 1;
4111            let c = if k == 1 {
4112                format!("{}x", v.name)
4113            } else {
4114                format!("{}x{}", v.name, k)
4115            };
4116            if !taken.contains(&c) {
4117                break c;
4118            }
4119        };
4120        taken.insert(cand.clone());
4121        v.name = cand;
4122    }
4123}
4124
4125/// Receiver rescue: a Field/Method receiver whose final vt type is
4126/// PRIMITIVE can never hold the member (structurer tail-copy regions
4127/// can map the receiver register onto a boolean/int generation —
4128/// wcdb HandleOperation's `v10_g2.finalizeStatement()` where the
4129/// statement var is right there; weixin deref-int/bool families,
4130/// ~2.5k lines). The member descriptor's owner class is authoritative:
4131/// when EXACTLY ONE var in the method has that type (or a subtype),
4132/// swap the receiver to it. The pre-rescue output is a guaranteed
4133/// compile error AND runtime NPE, so the unambiguous swap cannot make
4134/// it worse; ambiguous sites stay untouched.
4135/// Anonymous/inner-class outer reads: the dex reads `this$0` off the
4136/// instance register, which copy materialization can route through a
4137/// local (`v41 = this; ... v41.this$0` — weibo HorseRaceDetector$1,
4138/// 2,326-line 找不到符号 变量 this$0 family). When the field's
4139/// declaring class IS the class being decompiled, the only instance in
4140/// scope is `this` — strip the owner so it renders the bare declared
4141/// field.
4142/// Fold single-assigned `this`-copy locals in ctors back to `this`.
4143/// R8 materializes the receiver with `move-object/from16 v1, p0` and
4144/// runs every iput through the copy; the copy renders as a local and a
4145/// final-field store through it is "无法为 final 变量 g 分配值" (Java
4146/// allows blank-final writes only via `this`/simple name — hf/n2 ×29,
4147/// weixin final-var residual). Transitively: `b = a; c = b` chains
4148/// collapse. Single-assignment gate: a var ever written from another
4149/// value is not an alias.
4150pub fn fix_ctor_this_aliases(body: &mut Stmt, vt: &VarTable) {
4151    let Some(this_var) = vt
4152        .vars
4153        .iter()
4154        .find(|v| v.is_param && v.name == "this")
4155        .map(|v| v.id)
4156    else {
4157        return;
4158    };
4159    let mut analysis = VarAnalysis {
4160        assigns: Vec::new(),
4161        reads: Vec::new(),
4162        values: Vec::new(),
4163    };
4164    analyze_vars(body, &mut analysis);
4165    let n = vt.vars.len().max(analysis.assigns.len());
4166    let mut alias = vec![false; n];
4167    for _ in 0..4 {
4168        let mut changed = false;
4169        for v in 0..n as u32 {
4170            if alias[v as usize] {
4171                continue;
4172            }
4173            if analysis.assigns.get(v as usize).copied().unwrap_or(0) != 1 {
4174                continue;
4175            }
4176            let Some(Expr::Local { var: src, .. }) = analysis.values.get(v as usize).and_then(|o| o.as_ref()) else {
4177                continue;
4178            };
4179            if *src == this_var
4180                || (*src < n as u32 && alias[*src as usize])
4181            {
4182                alias[v as usize] = true;
4183                changed = true;
4184            }
4185        }
4186        if !changed {
4187            break;
4188        }
4189    }
4190    if !alias.iter().any(|&b| b) {
4191        return;
4192    }
4193    walk_stmt_exprs(body, &mut |e| {
4194        deep_rewrite_reads(e, &mut |x| {
4195            if let Expr::Local { var, .. } = x {
4196                if (*var as usize) < n && alias[*var as usize] {
4197                    *x = Expr::This;
4198                }
4199            }
4200        });
4201    });
4202}
4203
4204pub fn fix_this0_owners(body: &mut Stmt, class_name: &str) {
4205    walk_stmt_exprs(body, &mut |e| {
4206        deep_rewrite(e, &mut |x| {
4207            if let Expr::Field { owner: Some(_), cls, name, .. } = x {
4208                if name.as_ref() == "this$0" && cls.as_ref() == class_name {
4209                    if let Expr::Field { owner, .. } = x {
4210                        *owner = None;
4211                    }
4212                }
4213            }
4214        });
4215    });
4216}
4217
4218pub fn rescue_primitive_receivers(body: &mut Stmt, vt: &VarTable, pool: &crate::DexPool) {
4219    fn prim(t: &JavaType) -> bool {
4220        match t {
4221            JavaType::Int
4222            | JavaType::Long
4223            | JavaType::Short
4224            | JavaType::Byte
4225            | JavaType::Char
4226            | JavaType::Float
4227            | JavaType::Double
4228            | JavaType::Boolean => true,
4229            // Boxed JDK finals carry no app members either (`bool19.d`
4230            // where bool19 came out java/lang/Boolean — the coroutine
4231            // Result-register conflation).
4232            JavaType::Object(n) => matches!(
4233                n.as_ref(),
4234                "java/lang/Boolean"
4235                    | "java/lang/String"
4236                    | "java/lang/Integer"
4237                    | "java/lang/Long"
4238                    | "java/lang/Short"
4239                    | "java/lang/Byte"
4240                    | "java/lang/Character"
4241                    | "java/lang/Float"
4242                    | "java/lang/Double"
4243            ),
4244            _ => false,
4245        }
4246    }
4247    walk_stmt_exprs(body, &mut |e| {
4248        deep_rewrite(e, &mut |x| {
4249            // Phase 1 (read-only): primitive-typed Local receiver?
4250            let (recv_var, recv_ty) = {
4251                let o: &Expr = match &*x {
4252                    Expr::Method { owner: Some(o), .. } => o,
4253                    Expr::Field { owner: Some(o), is_static: false, .. } => o,
4254                    _ => return,
4255                };
4256                let Expr::Local { var, ty } = o else {
4257                    return;
4258                };
4259                let vt_ty = if (*var as usize) < vt.vars.len() {
4260                    vt.var(*var).ty.erased()
4261                } else {
4262                    ty.erased()
4263                };
4264                if !prim(&vt_ty) {
4265                    return;
4266                }
4267                (*var, vt_ty)
4268            };
4269            let cls: std::sync::Arc<str> = match &*x {
4270                Expr::Method { cls, .. } | Expr::Field { cls, .. } => cls.clone(),
4271                _ => return,
4272            };
4273            // Type-consistent call: the receiver's own type IS the
4274            // member owner (or a subtype) — `str.hashCode()` on a
4275            // String param needs no rescue. Without this the boxed-JDK
4276            // prim() arm fired on every String member call and the
4277            // max-id fallback below hijacked the reads to the highest
4278            // String-typed var in the method — often a case-local
4279            // (lark LynxUIBaseInput str11: 37 reads rewritten to a
4280            // switch-case-scoped def, 找不到符号 ×37; x0/c$a18 str4).
4281            if let JavaType::Object(n) = &recv_ty {
4282                if n.as_ref() == cls.as_ref() || pool.is_subtype(n.as_ref(), cls.as_ref()) {
4283                    return;
4284                }
4285            }
4286            // Phase 2: vars of the owner type (exact or subtype).
4287            let mut cands: Vec<u32> = Vec::new();
4288            for v in &vt.vars {
4289                if let JavaType::Object(n) = v.ty.erased() {
4290                    if n.as_ref() == cls.as_ref()
4291                        || pool.is_subtype(n.as_ref(), cls.as_ref())
4292                    {
4293                        cands.push(v.id);
4294                    }
4295                }
4296            }
4297            let id = if cands.len() == 1 {
4298                cands[0]
4299            } else if cands.len() > 1 {
4300                // Multiple owner-typed vars: prefer the same-SLOT one
4301                // (register lineage), latest generation wins — the
4302                // receiver position is scope-insensitive (any method-
4303                // scope var renders, the decl hoist covers it).
4304                let slot = if (recv_var as usize) < vt.vars.len() {
4305                    Some(vt.var(recv_var).slot)
4306                } else {
4307                    None
4308                };
4309                let mut hits: Vec<u32> = match slot {
4310                    Some(sl) => cands
4311                        .iter()
4312                        .copied()
4313                        .filter(|&c| {
4314                            c != recv_var
4315                                && (c as usize) < vt.vars.len()
4316                                && vt.var(c).slot == sl
4317                        })
4318                        .collect(),
4319                    None => Vec::new(),
4320                };
4321                if hits.is_empty() {
4322                    // No same-slot twin: fall back to the latest
4323                    // owner-typed generation (max id).
4324                    hits = cands
4325                        .iter()
4326                        .copied()
4327                        .filter(|&c| c != recv_var)
4328                        .collect();
4329                }
4330                hits.sort_unstable();
4331                hits.dedup();
4332                if hits.is_empty() {
4333                    return;
4334                }
4335                *hits.last().unwrap()
4336            } else {
4337                // Lineage disambiguation by SLOT: register reuse makes
4338                // the Boolean/primitive twin and the object version of
4339                // one dex register share VarInfo.slot (ry0/h2's
4340                // `bool19.d` — the v1-typed twin rides the same
4341                // register). Multiple slot matches stay untouched
4342                // (deterministic conservatism).
4343                if cands.is_empty() {
4344                    return;
4345                }
4346                let slot = if (recv_var as usize) < vt.vars.len() {
4347                    vt.var(recv_var).slot
4348                } else {
4349                    return;
4350                };
4351                let mut hits: Vec<u32> = cands
4352                    .into_iter()
4353                    .filter(|&c| {
4354                        c != recv_var
4355                            && (c as usize) < vt.vars.len()
4356                            && vt.var(c).slot == slot
4357                    })
4358                    .collect();
4359                hits.sort_unstable();
4360                hits.dedup();
4361                if hits.len() != 1 {
4362                    return;
4363                }
4364                hits[0]
4365            };
4366            // Phase 3: swap.
4367            let o: &mut Box<Expr> = match x {
4368                Expr::Method { owner: Some(o), .. } => o,
4369                Expr::Field { owner: Some(o), is_static: false, .. } => o,
4370                _ => return,
4371            };
4372            if let Expr::Local { var, ty } = &mut **o {
4373                if *var == recv_var {
4374                    *var = id;
4375                    *ty = vt.vars[id as usize].ty.clone();
4376                }
4377            }
4378        });
4379    });
4380
4381}
4382
4383/// Scope-safe arg/return swaps for mistyped generations — runs AFTER
4384/// ensure_declared hoists every declaration to the method top, so the
4385/// top_ids visibility precondition holds by construction (the r134
4386/// falsification: swapping in a block-scoped var broke lexically).
4387/// Same-slot + type-match + uniqueness gates as the receiver rescue.
4388pub fn rescue_arg_return_swaps(
4389    body: &mut Stmt,
4390    vt: &VarTable,
4391    pool: &crate::DexPool,
4392    ret_ty: &JavaType,
4393) {
4394    // SCOPE-SAFE arg/return swap: a primitive-typed actual against a
4395    // specific-reference formal (or a reference-returning `return`) is
4396    // a mistyped generation — the r134 falsification showed the swap
4397    // target MUST be lexically visible at the use site, so candidates
4398    // are restricted to params + TOP-LEVEL declared locals (a hoisted
4399    // decl is visible to javac name resolution everywhere in the
4400    // method) AND same-slot (same dex register lineage) AND unique.
4401    let mut top_ids: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
4402    for v in &vt.vars {
4403        if v.is_param {
4404            top_ids.insert(v.id);
4405        }
4406    }
4407    if let Stmt::Block(stmts) = body {
4408        for st in stmts.iter() {
4409            if let Stmt::LocalDef { var, .. } = st {
4410                top_ids.insert(*var);
4411            }
4412        }
4413    }
4414    let ret_wants_ref = match ret_ty {
4415        JavaType::Object(n) => n.as_ref() != "java/lang/Object",
4416        JavaType::Array(_) => true,
4417        _ => false,
4418    };
4419    let try_swap = |a: &mut Expr, want: &JavaType| {
4420        let Expr::Local { var, ty } = &mut *a else {
4421            return;
4422        };
4423        if (*var as usize) >= vt.vars.len() {
4424            return;
4425        }
4426        let avt = vt.var(*var).ty.erased();
4427        let avt_ref = matches!(avt, JavaType::Object(_) | JavaType::Array(_));
4428        let want_arr = matches!(want, JavaType::Array(_));
4429        // Only swap a primitive-typed actual (an Object actual gets the
4430        // descriptor-exact cast instead — never both).
4431        if avt_ref && !(avt == JavaType::Object("java/lang/Object".into())) {
4432            return;
4433        }
4434        if avt_ref && !matches!(want, JavaType::Object(_)) {
4435            return;
4436        }
4437        let slot = vt.var(*var).slot;
4438        let mut hits: Vec<u32> = Vec::new();
4439        for v in &vt.vars {
4440            if v.slot != slot || !top_ids.contains(&v.id) || v.id == *var {
4441                continue;
4442            }
4443            let t = v.ty.erased();
4444            let ok = match (&t, want) {
4445                (JavaType::Object(n), JavaType::Object(w)) => {
4446                    n.as_ref() == w.as_ref()
4447                        || pool.is_subtype(n.as_ref(), w.as_ref())
4448                }
4449                (JavaType::Array(_), JavaType::Array(_)) => want_arr,
4450                _ => false,
4451            };
4452            if ok {
4453                hits.push(v.id);
4454            }
4455        }
4456        hits.sort_unstable();
4457        hits.dedup();
4458        if hits.is_empty() {
4459            return;
4460        }
4461        // Multiple same-slot candidates: the LATEST generation (max id —
4462        // split mints in walk order ≈ program order) is the closest
4463        // approximation of the register's value at the use site. A
4464        // wrong pick still compiles (type-matched by construction);
4465        // leaving the primitive actual never does.
4466        let id = *hits.last().unwrap();
4467        *var = id;
4468        *ty = vt.vars[id as usize].ty.clone();
4469    };
4470    walk_stmt_exprs(body, &mut |e| {
4471        deep_rewrite(e, &mut |x| {
4472            match x {
4473                Expr::Method { desc, args, is_dynamic: false, .. } => {
4474                    for (i, a) in args.iter_mut().enumerate() {
4475                        if let Some(f) = desc.args.get(i) {
4476                            if matches!(f, JavaType::Object(n) if n.as_ref() != "java/lang/Object")
4477                                || matches!(f, JavaType::Array(_))
4478                            {
4479                                try_swap(a, f);
4480                            }
4481                        }
4482                    }
4483                }
4484                Expr::New { arg_tys, args, .. } => {
4485                    for (i, a) in args.iter_mut().enumerate() {
4486                        if let Some(f) = arg_tys.get(i) {
4487                            if matches!(f, JavaType::Object(n) if n.as_ref() != "java/lang/Object")
4488                                || matches!(f, JavaType::Array(_))
4489                            {
4490                                try_swap(a, f);
4491                            }
4492                        }
4493                    }
4494                }
4495                _ => {}
4496            }
4497        });
4498    });
4499    if ret_wants_ref {
4500        walk_mut_deep(body, &mut |st| {
4501            if let Stmt::Return(Some(e)) = st {
4502                try_swap(e, ret_ty);
4503            }
4504        });
4505    }
4506}
4507
4508/// Reference-array initializers take null, not 0: dex fill-array-data
4509/// over a String[]/Object[] slot carries the zero word, and the lifted
4510/// `new String[] {0}` is int无法转换为String (qs0/b family). A Const
4511/// Int(0) element of a reference-typed array init is the null ref.
4512/// `refVar = const 0` (dex `const/4 vN, 0` into a reference register) is
4513/// the NULL sentinel, not an int. By the time this runs infer_types has
4514/// typed the var a reference (its `.close()`/`.length`/method reads
4515/// proved it), so retype the const to Null BEFORE split_generations —
4516/// otherwise the ref/primitive mismatch splits off an int generation and
4517/// orphans the reference reads (u7.l try-with-resources finally
4518/// `v16_g3 = 0; if (v16_g3 != 0) v16_g3.close();` → "无法取消引用int").
4519/// `fix_incomparable_equality` already retypes the `refVar != 0`
4520/// COMPARISON side (but runs after the split); this covers the ASSIGN
4521/// side, which is what triggers the bad split.
4522pub fn fix_ref_null_assigns(vt: &VarTable, body: &mut Stmt) {
4523    let is_ref_var = |var: u32| -> bool {
4524        (var as usize) < vt.vars.len()
4525            && matches!(
4526                vt.vars[var as usize].ty.erased(),
4527                JavaType::Object(_) | JavaType::Array(_)
4528            )
4529    };
4530    walk_mut_deep(body, &mut |st| match st {
4531        Stmt::ExprStmt(Expr::Assign {
4532            target,
4533            value,
4534            op: AssignOp::Plain,
4535            ..
4536        }) => {
4537            if let Expr::Local { var, .. } = &**target {
4538                if is_ref_var(*var) && matches!(&**value, Expr::Const(ConstVal::Int(0))) {
4539                    **value = Expr::Const(ConstVal::Null);
4540                }
4541            }
4542        }
4543        Stmt::LocalDef {
4544            var,
4545            init: Some(e),
4546            ..
4547        } => {
4548            if is_ref_var(*var) && matches!(e, Expr::Const(ConstVal::Int(0))) {
4549                *e = Expr::Const(ConstVal::Null);
4550            }
4551        }
4552        _ => {}
4553    });
4554}
4555
4556pub fn fix_ref_array_null_consts(body: &mut Stmt) {
4557    walk_stmt_exprs(body, &mut |e| {
4558        deep_rewrite(e, &mut |x| {
4559            if let Expr::NewArray { elem, init: Some(list), .. } = x {
4560                if matches!(elem.erased(), JavaType::Object(_) | JavaType::Array(_)) {
4561                    for slot in list.iter_mut() {
4562                        if matches!(&*slot, Expr::Const(ConstVal::Int(0))) {
4563                            *slot = Expr::Const(ConstVal::Null);
4564                        }
4565                    }
4566                }
4567            }
4568        });
4569    });
4570}
4571
4572// ---------------------------------------------------------------------------
4573// Null-sentinel repair: const-0 registers in reference-typed slots
4574// ---------------------------------------------------------------------------
4575
4576/// A dex `const/4 vN, 0` register is ambiguous between int 0 and the
4577/// null reference; the value view types the bare constant as Int.
4578/// Kotlin coroutine state machines lean on this heavily — captured
4579/// object slots are zero-initialized ahead of the label switch, then
4580/// stored into `Object` fields and passed as `Continuation` ctor args
4581/// (weixin y01/s9 q8: `new e6(s94x, v287)` "int无法转换为Continuation";
4582/// uf5/l0 `throw 0;`; wcdb RepairKit `setOnCancelListener(0)`). The
4583/// dex verifier guarantees a const-0 register reaching a reference-
4584/// typed slot holds null, so in those slots the rendering must be the
4585/// null literal.
4586///
4587/// Two rewrites, both gated on the slot's expected type being a
4588/// reference (descriptor formal types, field type, array component,
4589/// cast target, throw, method return):
4590///   1. a literal `0` in a ref slot → `null`. SKIPPED for call-arg
4591///      slots whose formal is exactly `java/lang/Object`: `f(0)` there
4592///      compiles TODAY (boxing, or an int-overload pick like
4593///      `String.valueOf(0)`), and `f(null)` could newly fail on
4594///      overload ambiguity (`valueOf(Object)` vs `valueOf(char[])`) —
4595///      a guaranteed-error slot becomes a compile, a compiling slot
4596///      must not become an error.
4597///   2. a proven null-sentinel LOCAL: an Int-typed var whose every
4598///      write is `= 0` or `= <another sentinel>` and whose every read
4599///      sits in a ref slot (or feeds another sentinel's write — a dead
4600///      chain) → `null` at each read. The same Object-formal skip
4601///      applies at arg slots. A read in ANY int/unknown context
4602///      (binop, cond test, receiver deref, selector, anon-ctor arg,
4603///      lambda capture…) disqualifies the var — register reuse across
4604///      generations keeps its int rendering.
4605///
4606/// Runs after fix_ref_null_assigns (which converts writes of REF-typed
4607/// vars) and before split_generations (sentinel writes must not mint
4608/// int generations). Writes of confirmed sentinels stay as harmless
4609/// dead int stores for drop_dead_locals.
4610pub fn fix_null_sentinels(body: &mut Stmt, vt: &VarTable, ret: &JavaType) {
4611    #[derive(Clone, Copy, PartialEq)]
4612    enum WK {
4613        Zero,
4614        Copy(u32),
4615        Other,
4616    }
4617    fn is_ref_jt(t: &JavaType) -> bool {
4618        matches!(t, JavaType::Object(_) | JavaType::Array(_))
4619    }
4620    fn cls_write(e: &Expr) -> WK {
4621        match e {
4622            Expr::Const(ConstVal::Int(0)) => WK::Zero,
4623            Expr::Local { var, .. } => WK::Copy(*var),
4624            _ => WK::Other,
4625        }
4626    }
4627    fn expr_writes(e: &Expr, writes: &mut std::collections::HashMap<u32, Vec<WK>>) {
4628        visit_exprs(e, &mut |x| match x {
4629            Expr::Assign { target, op, value } => {
4630                if let Expr::Local { var, .. } = &**target {
4631                    let k = if matches!(op, AssignOp::Plain) {
4632                        cls_write(value)
4633                    } else {
4634                        WK::Other
4635                    };
4636                    writes.entry(*var).or_default().push(k);
4637                }
4638            }
4639            Expr::PreIncDec { e: inner, .. } | Expr::PostIncDec { e: inner, .. } => {
4640                if let Expr::Local { var, .. } = &**inner {
4641                    writes.entry(*var).or_default().push(WK::Other);
4642                }
4643            }
4644            _ => {}
4645        });
4646    }
4647    fn collect_writes(
4648        s: &Stmt,
4649        writes: &mut std::collections::HashMap<u32, Vec<WK>>,
4650        has_zero: &mut bool,
4651    ) {
4652        fn note(e: &Expr, has_zero: &mut bool, writes: &mut std::collections::HashMap<u32, Vec<WK>>) {
4653            visit_exprs(e, &mut |x| {
4654                if let Expr::Const(ConstVal::Int(0)) = x {
4655                    *has_zero = true;
4656                }
4657            });
4658            expr_writes(e, writes);
4659        }
4660        match s {
4661            Stmt::Block(v) => {
4662                for x in v {
4663                    collect_writes(x, writes, has_zero);
4664                }
4665            }
4666            Stmt::LocalDef { var, init, .. } => {
4667                if let Some(e) = init {
4668                    if matches!(e, Expr::Const(ConstVal::Int(0))) {
4669                        *has_zero = true;
4670                    }
4671                    writes.entry(*var).or_default().push(cls_write(e));
4672                }
4673            }
4674            Stmt::ExprStmt(e) => note(e, has_zero, writes),
4675            Stmt::Return(e) => {
4676                if let Some(x) = e {
4677                    note(x, has_zero, writes);
4678                }
4679            }
4680            Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
4681                note(e, has_zero, writes);
4682            }
4683            Stmt::TernaryValue { e } => note(e, has_zero, writes),
4684            Stmt::If { cond, then_stmt, else_stmt } => {
4685                note(cond, has_zero, writes);
4686                collect_writes(then_stmt, writes, has_zero);
4687                if let Some(b) = else_stmt {
4688                    collect_writes(b, writes, has_zero);
4689                }
4690            }
4691            Stmt::While { cond, body } | Stmt::DoWhile { body, cond } => {
4692                note(cond, has_zero, writes);
4693                collect_writes(body, writes, has_zero);
4694            }
4695            Stmt::For { init, cond, update, body } => {
4696                for x in init {
4697                    collect_writes(x, writes, has_zero);
4698                }
4699                if let Some(c) = cond {
4700                    note(c, has_zero, writes);
4701                }
4702                for u in update {
4703                    note(u, has_zero, writes);
4704                }
4705                collect_writes(body, writes, has_zero);
4706            }
4707            Stmt::ForEach { var, iterable, body, .. } => {
4708                writes.entry(*var).or_default().push(WK::Other);
4709                note(iterable, has_zero, writes);
4710                collect_writes(body, writes, has_zero);
4711            }
4712            Stmt::Switch { selector, cases, default, .. } => {
4713                note(selector, has_zero, writes);
4714                for c in cases {
4715                    if let Some(g) = &c.guard {
4716                        note(g, has_zero, writes);
4717                    }
4718                    for x in &c.body {
4719                        collect_writes(x, writes, has_zero);
4720                    }
4721                }
4722                if let Some(d) = default {
4723                    collect_writes(d, writes, has_zero);
4724                }
4725            }
4726            Stmt::Try { body, catches, finally } => {
4727                collect_writes(body, writes, has_zero);
4728                for c in catches {
4729                    writes.entry(c.var).or_default().push(WK::Other);
4730                    collect_writes(&c.body, writes, has_zero);
4731                }
4732                if let Some(f) = finally {
4733                    collect_writes(f, writes, has_zero);
4734                }
4735            }
4736            Stmt::TryWithResources { resources, body, catches, finally, .. } => {
4737                for r in resources {
4738                    collect_writes(r, writes, has_zero);
4739                }
4740                collect_writes(body, writes, has_zero);
4741                for c in catches {
4742                    writes.entry(c.var).or_default().push(WK::Other);
4743                    collect_writes(&c.body, writes, has_zero);
4744                }
4745                if let Some(f) = finally {
4746                    collect_writes(f, writes, has_zero);
4747                }
4748            }
4749            Stmt::Assert { cond, msg } => {
4750                note(cond, has_zero, writes);
4751                if let Some(m) = msg {
4752                    note(m, has_zero, writes);
4753                }
4754            }
4755            Stmt::Synchronized { lock, body } => {
4756                note(lock, has_zero, writes);
4757                collect_writes(body, writes, has_zero);
4758            }
4759            Stmt::Labeled { body, .. } => collect_writes(body, writes, has_zero),
4760            _ => {}
4761        }
4762    }
4763
4764    let mut writes: std::collections::HashMap<u32, Vec<WK>> =
4765        std::collections::HashMap::new();
4766    let mut has_zero = false;
4767    collect_writes(body, &mut writes, &mut has_zero);
4768
4769    // Write-proven eligibility fixpoint (copy chains: `v = u`).
4770    let mut eligible: HashSet<u32> = HashSet::default();
4771    let int_typed = |v: u32| {
4772        (v as usize) < vt.vars.len() && vt.vars[v as usize].ty.erased() == JavaType::Int
4773    };
4774    loop {
4775        let mut changed = false;
4776        for (&v, ws) in writes.iter() {
4777            if eligible.contains(&v) || ws.is_empty() || !int_typed(v) {
4778                continue;
4779            }
4780            if ws.iter().all(|k| match k {
4781                WK::Zero => true,
4782                WK::Copy(u) => *u != v && eligible.contains(u),
4783                WK::Other => false,
4784            }) {
4785                eligible.insert(v);
4786                changed = true;
4787            }
4788        }
4789        if !changed {
4790            break;
4791        }
4792    }
4793    if eligible.is_empty() && !has_zero {
4794        return; // nothing this pass can do
4795    }
4796
4797    #[derive(Clone, Copy, PartialEq)]
4798    enum Mode {
4799        Scan,
4800        Rewrite,
4801    }
4802    struct St<'a> {
4803        mode: Mode,
4804        vt: &'a VarTable,
4805        eligible: &'a HashSet<u32>,
4806        ref_read: HashSet<u32>,
4807        dead: HashSet<u32>,
4808        sentinels: HashSet<u32>,
4809        n_lit: usize,
4810        n_var: usize,
4811    }
4812    // A call-arg slot whose formal is exactly java/lang/Object keeps
4813    // its current rendering (see the fn doc — boxing compiles today,
4814    // null could newly ambiguize).
4815    fn arg_ctx(t: &JavaType) -> Option<JavaType> {
4816        match t {
4817            JavaType::Object(n) if n.as_ref() == "java/lang/Object" => None,
4818            t if is_ref_jt(t) => Some(t.clone()),
4819            _ => None,
4820        }
4821    }
4822    fn walk_expr(e: &mut Expr, ctx: Option<JavaType>, st: &mut St) {
4823        walk_expr_c(e, ctx, false, st);
4824    }
4825    /// `cast_arg`: the slot is a call/ctor ARGUMENT — a rewritten null
4826    /// gets an explicit formal-type cast. Bare `f(null)` re-triggers
4827    /// overload ambiguity where `f(0)` used to pick an int overload
4828    /// (`in(null)` "引用不明确", `ContentValues.put(String,null)` vs its
4829    /// 9 overloads); `(String) null` is unambiguous and jadx-shaped.
4830    fn walk_expr_c(e: &mut Expr, ctx: Option<JavaType>, cast_arg: bool, st: &mut St) {
4831        let slot_ref = ctx.as_ref().is_some_and(is_ref_jt);
4832        fn mk_null(ctx: &Option<JavaType>, cast_arg: bool) -> Expr {
4833            match (cast_arg, ctx) {
4834                (true, Some(t)) => Expr::Cast {
4835                    ty: TypeRef::J(t.clone()),
4836                    e: Box::new(Expr::Const(ConstVal::Null)),
4837                },
4838                _ => Expr::Const(ConstVal::Null),
4839            }
4840        }
4841        match e {
4842            Expr::Const(ConstVal::Int(0)) if slot_ref => {
4843                if st.mode == Mode::Rewrite {
4844                    *e = mk_null(&ctx, cast_arg);
4845                    st.n_lit += 1;
4846                }
4847                return;
4848            }
4849            Expr::Local { var, .. } => {
4850                if st.eligible.contains(var) {
4851                    if slot_ref {
4852                        st.ref_read.insert(*var);
4853                    } else {
4854                        st.dead.insert(*var);
4855                    }
4856                }
4857                if st.mode == Mode::Rewrite && slot_ref && st.sentinels.contains(var) {
4858                    *e = mk_null(&ctx, cast_arg);
4859                    st.n_var += 1;
4860                }
4861                return;
4862            }
4863            _ => {}
4864        }
4865        match e {
4866            Expr::Method { owner, desc, args, .. } => {
4867                if let Some(o) = owner {
4868                    walk_expr(o, None, st);
4869                }
4870                for (i, a) in args.iter_mut().enumerate() {
4871                    let c = desc.args.get(i).and_then(arg_ctx);
4872                    walk_expr_c(a, c, true, st);
4873                }
4874            }
4875            Expr::Invokedynamic { desc, args, .. } => {
4876                for (i, a) in args.iter_mut().enumerate() {
4877                    let c = desc.args.get(i).and_then(arg_ctx);
4878                    walk_expr_c(a, c, true, st);
4879                }
4880            }
4881            Expr::New { args, arg_tys, .. } => {
4882                for (i, a) in args.iter_mut().enumerate() {
4883                    let c = arg_tys.get(i).and_then(arg_ctx);
4884                    walk_expr_c(a, c, true, st);
4885                }
4886            }
4887            Expr::AnonNew { args, .. } => {
4888                for a in args.iter_mut() {
4889                    walk_expr(a, None, st);
4890                }
4891            }
4892            Expr::Assign { target, value, .. } => {
4893                // Sentinel-to-sentinel copy: the whole store is dead;
4894                // its RHS is by the write gate a const-0 or a bare
4895                // local, so no nested reads can hide in it.
4896                if let Expr::Local { var: u, .. } = &**target {
4897                    if st.eligible.contains(u) {
4898                        return;
4899                    }
4900                }
4901                let vctx = match &**target {
4902                    Expr::Field { ty, .. } => {
4903                        let t = ty.erased();
4904                        is_ref_jt(&t).then_some(t)
4905                    }
4906                    Expr::ArrayIndex { array, .. } => match array.type_ref().erased() {
4907                        JavaType::Array(inner) if is_ref_jt(&inner) => Some(*inner),
4908                        _ => None,
4909                    },
4910                    _ => None,
4911                };
4912                walk_expr(target, None, st);
4913                walk_expr(value, vctx, st);
4914            }
4915            Expr::Cast { ty, e: inner } => {
4916                let t = ty.erased();
4917                let c = is_ref_jt(&t).then_some(t);
4918                walk_expr(inner, c, st);
4919            }
4920            Expr::InstanceOf { e: inner, .. } => {
4921                walk_expr(inner, Some(JavaType::Object("java/lang/Object".into())), st);
4922            }
4923            Expr::Cond { c, t, f } => {
4924                walk_expr(c, None, st);
4925                let tc = ctx.clone();
4926                walk_expr(t, tc, st);
4927                walk_expr(f, ctx, st);
4928            }
4929            Expr::NewArray { elem, dims, init, .. } => {
4930                for d in dims.iter_mut() {
4931                    walk_expr(d, None, st);
4932                }
4933                if let Some(list) = init {
4934                    let et = elem.erased();
4935                    let c = is_ref_jt(&et).then_some(et);
4936                    for slot in list.iter_mut() {
4937                        walk_expr(slot, c.clone(), st);
4938                    }
4939                }
4940            }
4941            Expr::NewMultiArray { dims, .. } => {
4942                for d in dims.iter_mut() {
4943                    walk_expr(d, None, st);
4944                }
4945            }
4946            Expr::ArrayIndex { array, index } => {
4947                walk_expr(array, None, st);
4948                walk_expr(index, None, st);
4949            }
4950            Expr::Field { owner, .. } => {
4951                if let Some(o) = owner {
4952                    walk_expr(o, None, st);
4953                }
4954            }
4955            Expr::Bin { l, r, .. } => {
4956                walk_expr(l, None, st);
4957                walk_expr(r, None, st);
4958            }
4959            Expr::Un { e: inner, .. } => walk_expr(inner, None, st),
4960            Expr::PreIncDec { e: inner, .. } | Expr::PostIncDec { e: inner, .. } => {
4961                walk_expr(inner, None, st);
4962            }
4963            Expr::StringConcat(parts) => {
4964                for p in parts.iter_mut() {
4965                    if let ConcatPart::Str(x) = p {
4966                        walk_expr(x, None, st);
4967                    }
4968                }
4969            }
4970            Expr::Lambda(l) => {
4971                for c in l.captures.iter_mut() {
4972                    walk_expr(c, None, st);
4973                }
4974            }
4975            _ => {}
4976        }
4977    }
4978    fn walk_stmt(s: &mut Stmt, st: &mut St, ret: &JavaType) {
4979        match s {
4980            Stmt::Block(v) => {
4981                for x in v.iter_mut() {
4982                    walk_stmt(x, st, ret);
4983                }
4984            }
4985            Stmt::ExprStmt(e) => walk_expr(e, None, st),
4986            Stmt::LocalDef { var, init, .. } => {
4987                if let Some(e) = init {
4988                    let ctx = if (*var as usize) < st.vt.vars.len() {
4989                        let t = st.vt.vars[*var as usize].ty.erased();
4990                        is_ref_jt(&t).then_some(t)
4991                    } else {
4992                        None
4993                    };
4994                    walk_expr(e, ctx, st);
4995                }
4996            }
4997            Stmt::Return(e) => {
4998                if let Some(x) = e {
4999                    let c = is_ref_jt(ret).then(|| ret.clone());
5000                    walk_expr(x, c, st);
5001                }
5002            }
5003            Stmt::Throw(e) => {
5004                walk_expr(e, Some(JavaType::Object("java/lang/Throwable".into())), st);
5005            }
5006            Stmt::If { cond, then_stmt, else_stmt } => {
5007                walk_expr(cond, None, st);
5008                walk_stmt(then_stmt, st, ret);
5009                if let Some(b) = else_stmt {
5010                    walk_stmt(b, st, ret);
5011                }
5012            }
5013            Stmt::While { cond, body } => {
5014                walk_expr(cond, None, st);
5015                walk_stmt(body, st, ret);
5016            }
5017            Stmt::DoWhile { body, cond } => {
5018                walk_stmt(body, st, ret);
5019                walk_expr(cond, None, st);
5020            }
5021            Stmt::For { init, cond, update, body } => {
5022                for x in init.iter_mut() {
5023                    walk_stmt(x, st, ret);
5024                }
5025                if let Some(c) = cond {
5026                    walk_expr(c, None, st);
5027                }
5028                for u in update.iter_mut() {
5029                    walk_expr(u, None, st);
5030                }
5031                walk_stmt(body, st, ret);
5032            }
5033            Stmt::ForEach { iterable, body, .. } => {
5034                walk_expr(iterable, None, st);
5035                walk_stmt(body, st, ret);
5036            }
5037            Stmt::Switch { selector, cases, default, .. } => {
5038                walk_expr(selector, None, st);
5039                for c in cases.iter_mut() {
5040                    if let Some(g) = &mut c.guard {
5041                        walk_expr(g, None, st);
5042                    }
5043                    for x in c.body.iter_mut() {
5044                        walk_stmt(x, st, ret);
5045                    }
5046                }
5047                if let Some(d) = default {
5048                    walk_stmt(d, st, ret);
5049                }
5050            }
5051            Stmt::Try { body, catches, finally } => {
5052                walk_stmt(body, st, ret);
5053                for c in catches.iter_mut() {
5054                    walk_stmt(&mut c.body, st, ret);
5055                }
5056                if let Some(f) = finally {
5057                    walk_stmt(f, st, ret);
5058                }
5059            }
5060            Stmt::TryWithResources { resources, body, catches, finally, .. } => {
5061                for r in resources.iter_mut() {
5062                    walk_stmt(r, st, ret);
5063                }
5064                walk_stmt(body, st, ret);
5065                for c in catches.iter_mut() {
5066                    walk_stmt(&mut c.body, st, ret);
5067                }
5068                if let Some(f) = finally {
5069                    walk_stmt(f, st, ret);
5070                }
5071            }
5072            Stmt::Assert { cond, msg } => {
5073                walk_expr(cond, None, st);
5074                if let Some(m) = msg {
5075                    walk_expr(m, None, st);
5076                }
5077            }
5078            Stmt::Synchronized { lock, body } => {
5079                walk_expr(lock, Some(JavaType::Object("java/lang/Object".into())), st);
5080                walk_stmt(body, st, ret);
5081            }
5082            Stmt::TernaryValue { e } => walk_expr(e, None, st),
5083            Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
5084                walk_expr(e, Some(JavaType::Object("java/lang/Object".into())), st);
5085            }
5086            Stmt::Labeled { body, .. } => walk_stmt(body, st, ret),
5087            _ => {}
5088        }
5089    }
5090
5091    let mut st = St {
5092        mode: Mode::Scan,
5093        vt,
5094        eligible: &eligible,
5095        ref_read: HashSet::default(),
5096        dead: HashSet::default(),
5097        sentinels: HashSet::default(),
5098        n_lit: 0,
5099        n_var: 0,
5100    };
5101    if !eligible.is_empty() {
5102        walk_stmt(body, &mut st, ret);
5103        st.sentinels = eligible
5104            .iter()
5105            .copied()
5106            .filter(|v| st.ref_read.contains(v) && !st.dead.contains(v))
5107            .collect();
5108    }
5109    if st.sentinels.is_empty() && !has_zero {
5110        return;
5111    }
5112    st.mode = Mode::Rewrite;
5113    walk_stmt(body, &mut st, ret);
5114    if std::env::var_os("DDC_SENT").is_some() {
5115        eprintln!(
5116            "[sent] elig={} confirmed={} lit={} var={}",
5117            eligible.len(),
5118            st.sentinels.len(),
5119            st.n_lit,
5120            st.n_var
5121        );
5122    }
5123}
5124
5125/// Int-context operand bridge: a BOOLEAN-typed side of an int-kind Bin
5126/// (bitwise, arithmetic, comparison against a numeric) becomes
5127/// `(b ? 1 : 0)` — the dex-level truth (booleans ARE 0/1 ints there;
5128/// register reuse puts a bool-typed var into an int expression:
5129/// `v5x | 4`, `b == 0` — "boolean无法转换为int" / "二元运算符操作数
5130/// 类型错误" families). Runs AFTER fix_bool_xor so boolean-SINK chains
5131/// were already converted to all-boolean form and are skipped here.
5132pub fn fix_int_operand_bridges(body: &mut Stmt, vt: &VarTable) {
5133    fn side_bool(e: &Expr, vt: &VarTable) -> bool {
5134        matches!(
5135            match e {
5136                Expr::Local { var, .. } => vt.var(*var).ty.erased(),
5137                other => other.type_ref().erased(),
5138            },
5139            JavaType::Boolean
5140        )
5141    }
5142    fn side_int(e: &Expr, vt: &VarTable) -> bool {
5143        matches!(
5144            match e {
5145                Expr::Local { var, .. } => vt.var(*var).ty.erased(),
5146                other => other.type_ref().erased(),
5147            },
5148            JavaType::Int | JavaType::Short | JavaType::Byte | JavaType::Char | JavaType::Long
5149        )
5150    }
5151    fn wrap(e: Box<Expr>) -> Box<Expr> {
5152        Box::new(Expr::Cond {
5153            c: e,
5154            t: Box::new(Expr::Const(ConstVal::Int(1))),
5155            f: Box::new(Expr::Const(ConstVal::Int(0))),
5156        })
5157    }
5158    walk_stmt_exprs(body, &mut |e| {
5159        deep_rewrite(e, &mut |x| {
5160            // Field / array-store positions: the target's type is
5161            // descriptor-backed (authoritative); a boolean value into
5162            // an int slot bridges `(b ? 1 : 0)` (with a narrowing cast
5163            // for byte/short/char slots), an int value into a boolean
5164            // slot bridges `v != 0` (weixin `this.L[v3x] = max3`,
5165            // `g2x.d = p1x` — 785 field + ~700 array lines).
5166            if let Expr::Assign { target, value, op: AssignOp::Plain } = x {
5167                let tgt_ty: Option<JavaType> = match &**target {
5168                    Expr::Field { ty, .. } => Some(ty.erased()),
5169                    Expr::ArrayIndex { array, .. } => {
5170                        match array.type_ref().erased() {
5171                            JavaType::Array(el) => Some(el.as_ref().clone()),
5172                            _ => None,
5173                        }
5174                    }
5175                    // Local targets: the split/booleanize machinery owns
5176                    // the mixed-kind register cases; the residue that
5177                    // reached render (boolean无法转换为byte/long, ~360
5178                    // lines) gets the same value-side bridge.
5179                    Expr::Local { var, .. } => {
5180                        if (*var as usize) < vt.vars.len() {
5181                            Some(vt.var(*var).ty.erased())
5182                        } else {
5183                            None
5184                        }
5185                    }
5186                    _ => None,
5187                };
5188                if let Some(t) = tgt_ty {
5189                    let tgt_int = matches!(
5190                        t,
5191                        JavaType::Int
5192                            | JavaType::Short
5193                            | JavaType::Byte
5194                            | JavaType::Char
5195                            | JavaType::Long
5196                    );
5197                    if tgt_int && side_bool(value, vt) && !side_int(value, vt) {
5198                        let taken = std::mem::replace(
5199                            value,
5200                            Box::new(Expr::Const(ConstVal::Null)),
5201                        );
5202                        let mut wrapped = wrap(taken);
5203                        if !matches!(t, JavaType::Int | JavaType::Long) {
5204                            wrapped = Box::new(Expr::Cast {
5205                                ty: TypeRef::J(t.clone()),
5206                                e: wrapped,
5207                            });
5208                        }
5209                        *value = wrapped;
5210                    } else if matches!(t, JavaType::Boolean)
5211                        && side_int(value, vt)
5212                        && !side_bool(value, vt)
5213                    {
5214                        let taken = std::mem::replace(
5215                            value,
5216                            Box::new(Expr::Const(ConstVal::Null)),
5217                        );
5218                        **value = Expr::Bin {
5219                            op: BinOp::Ne,
5220                            l: taken,
5221                            r: Box::new(Expr::Const(ConstVal::Int(0))),
5222                            ty: Some(TypeRef::J(JavaType::Boolean)),
5223                        };
5224                    }
5225                }
5226            }
5227            // Explicit casts across the bool/num line are invalid Java
5228            // in BOTH directions: `(byte) this.r` (dex int-to-byte over
5229            // a boolean-rendered field — weixin AccInfo writeByte ×207)
5230            // becomes `(byte)(r ? 1 : 0)`, and `(boolean) x` over an
5231            // int becomes `x != 0`.
5232            if let Expr::Cast { ty, e } = x {
5233                let t = ty.erased();
5234                let t_num = matches!(
5235                    t,
5236                    JavaType::Int
5237                        | JavaType::Short
5238                        | JavaType::Byte
5239                        | JavaType::Char
5240                        | JavaType::Long
5241                        | JavaType::Float
5242                        | JavaType::Double
5243                );
5244                if t_num && side_bool(e, vt) && !side_int(e, vt) {
5245                    let taken = std::mem::replace(e, Box::new(Expr::Const(ConstVal::Null)));
5246                    **e = Expr::Cond {
5247                        c: taken,
5248                        t: Box::new(Expr::Const(ConstVal::Int(1))),
5249                        f: Box::new(Expr::Const(ConstVal::Int(0))),
5250                    };
5251                } else if matches!(t, JavaType::Boolean)
5252                    && side_int(e, vt)
5253                    && !side_bool(e, vt)
5254                {
5255                    let taken = std::mem::replace(e, Box::new(Expr::Const(ConstVal::Null)));
5256                    *x = Expr::Bin {
5257                        op: BinOp::Ne,
5258                        l: taken,
5259                        r: Box::new(Expr::Const(ConstVal::Int(0))),
5260                        ty: Some(TypeRef::J(JavaType::Boolean)),
5261                    };
5262                }
5263            }
5264            // Null-vs-int comparison: dex if-eqz/if-nez is type-agnostic
5265            // and null IS zero — when the local came out int-typed (phi
5266            // confluence residue), rewrite the Null constant to 0
5267            // (`v95_g12 != null` → `v95_g12 != 0`, faithful and legal;
5268            // yq0/g1 546-line 二元运算符 family).
5269            if let Expr::Bin { op, l, r, .. } = x {
5270                if matches!(op, BinOp::Eq | BinOp::Ne) {
5271                    if matches!(&**r, Expr::Const(ConstVal::Null))
5272                        && side_int(l, vt)
5273                        && !side_bool(l, vt)
5274                    {
5275                        **r = Expr::Const(ConstVal::Int(0));
5276                    } else if matches!(&**l, Expr::Const(ConstVal::Null))
5277                        && side_int(r, vt)
5278                        && !side_bool(r, vt)
5279                    {
5280                        **l = Expr::Const(ConstVal::Int(0));
5281                    }
5282                }
5283            }
5284            // A boolean-typed array INDEX is a reused register holding
5285            // an int (`this.L[v3x]` — boolean无法转换为int at the index
5286            // position): bridge it in reads and writes alike.
5287            if let Expr::ArrayIndex { index, .. } = x {
5288                if side_bool(index, vt) && !side_int(index, vt) {
5289                    let taken =
5290                        std::mem::replace(index, Box::new(Expr::Const(ConstVal::Null)));
5291                    *index = wrap(taken);
5292                }
5293            }
5294            // A boolean-typed array DIMENSION is the same reused-register
5295            // case at the allocation site (`new Object[v200]` where v200
5296            // is boolean-typed — weixin AppBrandRuntime, bool→int family):
5297            // dex new-array reads the size register as int and booleans
5298            // ARE 0/1 ints there, so bridge `(b ? 1 : 0)`.
5299            if let Expr::NewArray { dims, .. } = x {
5300                for d in dims.iter_mut() {
5301                    if side_bool(d, vt) && !side_int(d, vt) {
5302                        let taken =
5303                            std::mem::replace(d, Expr::Const(ConstVal::Null));
5304                        *d = *wrap(Box::new(taken));
5305                    }
5306                }
5307            }
5308            if let Expr::Bin { op, l, r, .. } = x {
5309                let int_kind = matches!(
5310                    op,
5311                    BinOp::Or
5312                        | BinOp::And
5313                        | BinOp::Xor
5314                        | BinOp::Add
5315                        | BinOp::Sub
5316                        | BinOp::Mul
5317                        | BinOp::Div
5318                        | BinOp::Rem
5319                        | BinOp::Shl
5320                        | BinOp::Shr
5321                        | BinOp::Ushr
5322                        | BinOp::Eq
5323                        | BinOp::Ne
5324                        | BinOp::Lt
5325                        | BinOp::Ge
5326                        | BinOp::Gt
5327                        | BinOp::Le
5328                );
5329                if !int_kind {
5330                    return;
5331                }
5332                // Comparisons only bridge against a NUMERIC constant or
5333                // int-typed side (a bool == bool compare is valid Java).
5334                if side_bool(l, vt) && side_int(r, vt) && !side_bool(r, vt) {
5335                    let taken = std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
5336                    *l = wrap(taken);
5337                } else if side_bool(r, vt) && side_int(l, vt) && !side_bool(l, vt) {
5338                    let taken = std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
5339                    *r = wrap(taken);
5340                }
5341            }
5342        });
5343    });
5344}
5345
5346/// Statement-level idiom restoration (readability parity with
5347/// jadx/DAD, flagged by the androguard comparison): `v = v + 1` →
5348/// `v++`, `v = v op x` → `v op= x`. Local targets only — a compound
5349/// field/array target would change how often the receiver expression
5350/// evaluates. Var must be the LEFT operand (non-commutative ops;
5351/// string-concat order). The ±1 fold requires a numeric vt type
5352/// (`s++` on a String is invalid while `s += 1` is not).
5353/// Boolean VALUE-diamond fold (DAD short_circuit value forms):
5354/// ```text
5355///   v = false; if (c) { v = true; }   →  v = c
5356///   v = true;  if (c) { v = false; }  →  v = !c
5357///   v = false; if (c) { v = E; }      →  v = c && E     (E bool-valued)
5358///   v = true;  if (c) { v = E; }      →  v = !c || E
5359///   v = D; if (c) { v = E1; } else { v = E2; }  →  v = c ? E1 : E2
5360/// ```
5361/// `v` must be Boolean-typed; the arm value bool-valued; the guard may
5362/// not touch `v`; base and arm constants must not coincide (a no-op
5363/// diamond could drop guard side effects). Kotlin default-arg bridge
5364/// ctors compute defaulted booleans in exactly this shape INSIDE the
5365/// branch ahead of the delegation (lark MmCreateAudioRequest's v21x
5366/// chain) — split_branch rejects the control flow as Dirty and the
5367/// this() call stays in the branch ("对this的调用必须是构造器中的第一
5368/// 个语句", lark ×456). Folded to a linear def (interleaved with
5369/// forward_single_use for the `[u = call; v = u]` arm shape) the
5370/// merge_at ternary fold applies. Source-faithful: the original was
5371/// `x?.m() ?: false` — one expression; the inlined call stays under
5372/// the guard arm, preserving evaluation order and short-circuiting.
5373/// General value-diamond fold (fold_bool_value_diamonds for ANY type):
5374/// `if (c) { v = a; } else { v = b; }` → `v = c ? a : b;`, plus the
5375/// split-pair shape `v = b; if (c) { v = a; }` → the same ternary. The
5376/// Kotlin default-arg ctor's mask-guarded per-arg defaults arrive as
5377/// diamonds (DocFreshInfo v10x/v14x); folding gives each carrier a
5378/// SINGLE-assignment def so the chain-aware delegation inline can hoist
5379/// the this() first ("对this的调用必须是构造器中的第一个语句"). Fold
5380/// only when both sides are exactly one plain assignment to the SAME
5381/// local and the value types are ternary-compatible (equal erased or
5382/// both numeric — mixed-category ternaries box/surprise).
5383pub fn fold_value_diamonds(body: &mut Stmt, vt: &VarTable) {
5384    let Stmt::Block(stmts) = body else { return };
5385    fold_diamonds_in(stmts, vt);
5386}
5387
5388fn diamond_types_ok(a: &Expr, b: &Expr) -> bool {
5389    fn num(t: &JavaType) -> bool {
5390        matches!(
5391            t,
5392            JavaType::Byte
5393                | JavaType::Short
5394                | JavaType::Int
5395                | JavaType::Char
5396                | JavaType::Long
5397                | JavaType::Float
5398                | JavaType::Double
5399        )
5400    }
5401    // A null branch is compatible with any REFERENCE other branch: the
5402    // ternary types as lub(T, null) = T. Kotlin default-args ctors
5403    // render null defaults (`if ((mask&2)==0) v=x; else v=null;`) —
5404    // rejecting the pair left the diamond unfolded, the defs
5405    // un-inlineable, and the this(...) delegation unhoistable (weibo
5406    // ctor-not-first robust-guard/second-delegation ×818). A primitive
5407    // sibling stays rejected (`c ? 5 : null` is not Java).
5408    if let Some(other) = if matches!(a, Expr::Const(ConstVal::Null)) {
5409        Some(b)
5410    } else if matches!(b, Expr::Const(ConstVal::Null)) {
5411        Some(a)
5412    } else {
5413        None
5414    } {
5415        return !num(&other.type_ref().erased());
5416    }
5417    let ta = a.type_ref().erased();
5418    let tb = b.type_ref().erased();
5419    ta == tb || (num(&ta) && num(&tb))
5420}
5421
5422/// `(var, target clone, value)` when the stmt is exactly ONE plain
5423/// assign to a local (bare or block-wrapped). The unwrap must be
5424/// RECURSIVE: the Kotlin default-args ctor diamonds arrive as
5425/// `Block([Block([Assign])])` (structurer nesting), and the one-level
5426/// peel missed them — the unfolded `if ((mask&2)==0) v=p; else v=0;`
5427/// prelude then blocked fix_ctor_super_first from hoisting the
5428/// `this(...)` delegation (weibo/lark ctor-not-first ×841 family).
5429fn single_local_assign(st: &Stmt) -> Option<(u32, Expr, Expr)> {
5430    let mut inner = st;
5431    while let Stmt::Block(v) = inner {
5432        if v.len() != 1 {
5433            return None;
5434        }
5435        inner = &v[0];
5436    }
5437    match inner {
5438        Stmt::ExprStmt(Expr::Assign { target, value, op: AssignOp::Plain }) => {
5439            match &**target {
5440                Expr::Local { var, .. } => Some((*var, *target.clone(), *value.clone())),
5441                _ => None,
5442            }
5443        }
5444        _ => None,
5445    }
5446}
5447
5448/// Like `single_local_assign`, but ALSO accepts an arm whose value is
5449/// produced by an arm-local def: `Block([LocalDef vX = init, v = vX])`
5450/// (possibly Block-wrapped) inlines `init` into the returned value when
5451/// `vX` is read exactly once there (single evaluation preserved). The
5452/// Kotlin mask-ctor long defaults render this shape (`else { long u =
5453/// Color.getUnspecified(); v37 = u; }` — compose TextStyle/SpanStyle);
5454/// without the inline the diamond stays unfolded and blocks the whole
5455/// delegation-inlining chain downstream.
5456fn arm_assign_inlined(st: &Stmt) -> Option<(u32, Expr, Expr)> {
5457    if let Some(r) = single_local_assign(st) {
5458        return Some(r);
5459    }
5460    let mut inner = st;
5461    while let Stmt::Block(v) = inner {
5462        if v.len() == 1 {
5463            inner = &v[0];
5464        } else {
5465            break;
5466        }
5467    }
5468    let Stmt::Block(v) = inner else { return None };
5469    if v.len() != 2 {
5470        return None;
5471    }
5472    let Stmt::LocalDef {
5473        var: x,
5474        init: Some(init),
5475        ..
5476    } = &v[0]
5477    else {
5478        return None;
5479    };
5480    let (var, tgt, val) = single_local_assign(&v[1])?;
5481    let mut count = 0usize;
5482    let mut c = val.clone();
5483    deep_rewrite(&mut c, &mut |e| {
5484        if let Expr::Local { var: lv, .. } = e {
5485            if lv == x {
5486                count += 1;
5487            }
5488        }
5489    });
5490    if count != 1 {
5491        return None;
5492    }
5493    let mut out = val.clone();
5494    deep_rewrite(&mut out, &mut |e| {
5495        if let Expr::Local { var: lv, .. } = e {
5496            if lv == x {
5497                *e = init.clone();
5498            }
5499        }
5500    });
5501    Some((var, tgt, out))
5502}
5503
5504fn mk_ternary_assign(tgt: Expr, c: Expr, a: Expr, b: Expr) -> Stmt {
5505    Stmt::ExprStmt(Expr::Assign {
5506        target: Box::new(tgt),
5507        op: AssignOp::Plain,
5508        value: Box::new(Expr::Cond {
5509            c: Box::new(c),
5510            t: Box::new(a),
5511            f: Box::new(b),
5512        }),
5513    })
5514}
5515
5516/// Two var ids that render as the SAME Java local: identical base name
5517/// (generation suffixes only appear on collisions, and same-base
5518/// generations of one dex register share the rendered name) and
5519/// identical erased type (a bool/num split pair must not merge).
5520fn same_render_var(v1: u32, v2: u32, vt: &VarTable) -> bool {
5521    let (a, b) = (vt.vars.get(v1 as usize), vt.vars.get(v2 as usize));
5522    match (a, b) {
5523        (Some(x), Some(y)) => x.name == y.name && x.ty.erased() == y.ty.erased(),
5524        _ => false,
5525    }
5526}
5527
5528/// Structural equality of two statement lists MODULO SSA generations:
5529/// each arm of a diamond writes its OWN generation of the rendered
5530/// locals, so strict `==` on the tails fails even when the rendered
5531/// code is identical. Canonicalize every local id to the first id seen
5532/// for its render key (name + erased type) under a SHARED map, then
5533/// compare structurally.
5534fn canon_stmts_eq(a: &[Stmt], b: &[Stmt], vt: &VarTable) -> bool {
5535    if a.len() != b.len() {
5536        return false;
5537    }
5538    let mut map: std::collections::HashMap<String, u32> = std::collections::HashMap::default();
5539    let canon = |s: &Stmt, map: &mut std::collections::HashMap<String, u32>| -> Stmt {
5540        let mut c = s.clone();
5541        walk_stmt_exprs(&mut c, &mut |e| {
5542            deep_rewrite(e, &mut |x| {
5543                if let Expr::Local { var, .. } = x {
5544                    let key = match vt.vars.get(*var as usize) {
5545                        Some(vi) => format!("{}#{:?}", vi.name, vi.ty.erased()),
5546                        None => format!("v{}", var),
5547                    };
5548                    let id = *map.entry(key).or_insert(*var);
5549                    *x = Expr::Local {
5550                        var: id,
5551                        ty: vt.var(id).ty.clone(),
5552                    };
5553                }
5554            });
5555        });
5556        c
5557    };
5558    a.iter()
5559        .zip(b.iter())
5560        .all(|(x, y)| canon(x, &mut map) == canon(y, &mut map))
5561}
5562
5563fn fold_diamonds_in(stmts: &mut Vec<Stmt>, vt: &VarTable) {
5564    let mut i = 0usize;
5565    while i < stmts.len() {
5566        match &mut stmts[i] {
5567            Stmt::Block(v) => fold_diamonds_in(v, vt),
5568            Stmt::If { then_stmt, else_stmt, .. } => {
5569                if let Stmt::Block(v) = &mut **then_stmt {
5570                    fold_diamonds_in(v, vt);
5571                }
5572                if let Some(e) = else_stmt {
5573                    if let Stmt::Block(v) = &mut **e {
5574                        fold_diamonds_in(v, vt);
5575                    }
5576                }
5577            }
5578            _ => {}
5579        }
5580        // Shape 1: standalone if/else diamond.
5581        let shape1 = if let Stmt::If { cond, then_stmt, else_stmt: Some(el), .. } = &stmts[i] {
5582            match (arm_assign_inlined(then_stmt), arm_assign_inlined(el)) {
5583                (Some((v1, tgt, a)), Some((v2, _, b)))
5584                    if v1 == v2 && diamond_types_ok(&a, &b) =>
5585                {
5586                    Some(mk_ternary_assign(tgt, cond.clone(), a, b))
5587                }
5588                _ => None,
5589            }
5590        } else {
5591            None
5592        };
5593        if let Some(f) = shape1 {
5594            stmts[i] = f;
5595            i += 1;
5596            continue;
5597        }
5598        // Shape 3: identical-delegation diamond — BOTH arms assign the
5599        // same rendered local, then run the same tail headed by a ctor
5600        // delegation (Kotlin mask-ctor last slot: `if ((m&M)==0) { v =
5601        // x; this(.., v, ..); return; } else { v = null; this(.., v,
5602        // ..); return; }` — alipay compose TextStyle/SpanStyle family).
5603        // Fold to `v = c ? x : null;` + the shared tail: the delegation
5604        // becomes unconditional (the hotfix-guard strip downstream needs
5605        // that) and fold_default_arg_bridge can then inline the prelude
5606        // carriers into the args. Tails compare modulo SSA generation
5607        // (each arm reads its own generation of the one rendered local).
5608        // Gates: tail headed by a bare delegation, tails canonically
5609        // identical, the tail writes NEITHER generation, types ok.
5610        let shape3 = if let Stmt::If {
5611            cond,
5612            then_stmt,
5613            else_stmt: Some(el),
5614            ..
5615        } = &stmts[i]
5616        {
5617            let arm = |st: &Stmt| -> Option<((u32, Expr, Expr), Vec<Stmt>)> {
5618                let Stmt::Block(v) = st else { return None };
5619                if v.len() < 2 {
5620                    return None;
5621                }
5622                let first = single_local_assign(&v[0])?;
5623                Some((first, v[1..].to_vec()))
5624            };
5625            match (arm(then_stmt), arm(el)) {
5626                (Some(((v1, tgt1, a), mut rest_t)), Some(((v2, _t2, b), rest_e)))
5627                    if !rest_t.is_empty()
5628                        && is_bare_ctor_call(first_leaf_stmt(&rest_t[0]))
5629                        && stmts_count_writes(&rest_t, v1) == 0
5630                        && stmts_count_writes(&rest_t, v2) == 0
5631                        && diamond_types_ok(&a, &b) =>
5632                {
5633                    if v1 == v2 || same_render_var(v1, v2, vt) {
5634                        // same rendered local: fold the assign, keep the tail
5635                        if canon_stmts_eq(&rest_t, &rest_e, vt) {
5636                            Some((mk_ternary_assign(tgt1, cond.clone(), a, b), rest_t))
5637                        } else {
5638                            None
5639                        }
5640                    } else if stmts_read_var(&rest_t, v1) == 1
5641                        && stmts_read_var(&rest_e, v2) == 1
5642                        && stmts_read_var(&rest_t, v2) == 0
5643                        && stmts_read_var(&rest_e, v1) == 0
5644                    {
5645                        // SPLIT arm-local carriers (compose Span: then
5646                        // reads v23_g3, else reads v23 — different render
5647                        // names, one read each): unify by substituting
5648                        // v2 -> v1 in the else tail and requiring EXACT
5649                        // equality, then inline `c ? A1 : A2` at the
5650                        // single v1 read of the kept tail. No ternary
5651                        // assign needed — both carriers die in the fold.
5652                        let mut unified = rest_e.clone();
5653                        for st in unified.iter_mut() {
5654                            let mut c2 = st.clone();
5655                            walk_stmt_exprs(&mut c2, &mut |e| {
5656                                deep_rewrite(e, &mut |x| {
5657                                    if let Expr::Local { var, .. } = x {
5658                                        if *var == v2 {
5659                                            *x = Expr::Local {
5660                                                var: v1,
5661                                                ty: vt.var(v1).ty.clone(),
5662                                            };
5663                                        }
5664                                    }
5665                                });
5666                            });
5667                            *st = c2;
5668                        }
5669                        if unified != rest_t {
5670                            None
5671                        } else {
5672                            let tern = Expr::Cond {
5673                                c: Box::new(cond.clone()),
5674                                t: Box::new(a.clone()),
5675                                f: Box::new(b.clone()),
5676                            };
5677                            for st in rest_t.iter_mut() {
5678                                let mut c2 = st.clone();
5679                                walk_stmt_exprs(&mut c2, &mut |e| {
5680                                    deep_rewrite(e, &mut |x| {
5681                                        if let Expr::Local { var, .. } = x {
5682                                            if *var == v1 {
5683                                                *x = tern.clone();
5684                                            }
5685                                        }
5686                                    });
5687                                });
5688                                *st = c2;
5689                            }
5690                            // the If is REPLACED by the (rewritten) tail:
5691                            // no ternary assign survives — both carriers
5692                            // die in the fold.
5693                            let first = rest_t.remove(0);
5694                            Some((first, rest_t))
5695                        }
5696                    } else {
5697                        None
5698                    }
5699                }
5700                _ => None,
5701            }
5702        } else {
5703            None
5704        };
5705        if let Some((folded, rest)) = shape3 {
5706            stmts[i] = folded;
5707            stmts.splice(i + 1..i + 1, rest);
5708            i += 1;
5709            continue;
5710        }
5711        // Shape 2: adjacent `v = b;` + `if (c) { v = a; }` (no else).
5712        // The two assigns may target DIFFERENT SSA generations of the
5713        // same rendered local (sequential conditional overwrite — the
5714        // Kotlin mask-ctor `v11x = p3x; if ((mask&4)!=0) v11x = -1L;`
5715        // prelude, lark/weixin branched-defs ctor family): same base
5716        // name + same erased type renders as one Java local, so the
5717        // fold's else arm reads generation 1 and the assign targets
5718        // generation 2 — textually `v = c ? a : v`, exactly the
5719        // observable behavior of the unfolded pair.
5720        if i + 1 < stmts.len() {
5721            // An EMPTY else block counts as no-else: the structurer
5722            // emits Some(Block([])) where the source shape had none,
5723            // and cleanup (which would drop it) runs AFTER the fold —
5724            // the strict None match missed every real-world overwrite
5725            // (zero shape-2 folds on lark mask ctors).
5726            // Shape 2: adjacent `v = b;` + conditional overwrite with
5727            // NO other effect:
5728            //   form A: `if (c) { v = a; }` (else absent/empty)  -> v = c ? a : b
5729            //   form B: `if (c) { } else { v = a; }` (then empty) -> v = c ? b : a
5730            // Form B is the FOLD-time shape of every rendered no-else
5731            // overwrite: invert_empty_thens flips then/else AFTER this
5732            // pass, so the strict `else_stmt: None, overwrite-in-then`
5733            // match never saw a real-world instance (zero shape-2 folds
5734            // on lark mask ctors). The two assigns may target DIFFERENT
5735            // SSA generations of one rendered local (same base name +
5736            // same erased type): the ternary's keep-arm then reads
5737            // generation 1 — textually `v = c ? a : v`, exactly the
5738            // observable behavior of the unfolded pair (Kotlin mask-ctor
5739            // preludes: `v11x = p3x; if ((mask&4)!=0) v11x = -1L;`).
5740            let shape2 = {
5741                let s_i = single_local_assign(&stmts[i]);
5742                if let Stmt::If {
5743                    cond,
5744                    then_stmt,
5745                    else_stmt,
5746                    ..
5747                } = &stmts[i + 1]
5748                {
5749                    let then_a = single_local_assign(then_stmt);
5750                    let else_a = else_stmt.as_deref().and_then(single_local_assign);
5751                    let then_empty =
5752                        matches!(&**then_stmt, Stmt::Block(v) if v.is_empty());
5753                    let else_empty = else_stmt
5754                        .as_deref()
5755                        .map(|e| matches!(e, Stmt::Block(v) if v.is_empty()))
5756                        .unwrap_or(true);
5757                    let try_fold = |over: Option<(u32, Expr, Expr)>,
5758                                    keep_over: bool|
5759                     -> Option<Stmt> {
5760                        let (v2, tgt2, a) = over?;
5761                        let (v1, _t1, b) = s_i.as_ref()?;
5762                        if !(*v1 == v2 || same_render_var(*v1, v2, vt)) {
5763                            return None;
5764                        }
5765                        let keep = if *v1 == v2 {
5766                            b.clone()
5767                        } else {
5768                            Expr::Local {
5769                                var: *v1,
5770                                ty: vt.var(*v1).ty.clone(),
5771                            }
5772                        };
5773                        if !diamond_types_ok(&a, &keep) {
5774                            return None;
5775                        }
5776                        if keep_over {
5777                            Some(mk_ternary_assign(tgt2, cond.clone(), keep, a))
5778                        } else {
5779                            Some(mk_ternary_assign(tgt2, cond.clone(), a, keep))
5780                        }
5781                    };
5782                    if then_a.is_some() && else_empty {
5783                        try_fold(then_a, false)
5784                    } else if else_a.is_some() && then_empty {
5785                        try_fold(else_a, true)
5786                    } else {
5787                        None
5788                    }
5789                } else {
5790                    None
5791                }
5792            };
5793            // Leading-assign variant: the branch STARTS with the
5794            // overwrite and carries a tail (Kotlin mask-ctor last slot:
5795            // `str12 = null; if ((m&32)==0) { str12 = str6; this(..,
5796            // str12); return; }` — news_article CommonData/DTO family).
5797            // Fold the assign into the ternary and leave the tail in the
5798            // branch: the tail's reads see the identical value on both
5799            // paths (the ternary assigns before the branch runs). Gate:
5800            // the tail must not WRITE the var.
5801            let mut lead_fold: Option<(Stmt, Stmt)> = None;
5802            if shape2.is_none() && i + 1 < stmts.len() {
5803                let s_i2 = single_local_assign(&stmts[i]);
5804                let leading = |st: &Stmt| -> Option<((u32, Expr, Expr), Vec<Stmt>)> {
5805                    let Stmt::Block(v) = st else { return None };
5806                    if v.len() < 2 {
5807                        return None;
5808                    }
5809                    let first = single_local_assign(&v[0])?;
5810                    Some((first, v[1..].to_vec()))
5811                };
5812                if let (Some((v1, _t1, b)), Stmt::If { cond, then_stmt, else_stmt, .. }) =
5813                    (&s_i2, &stmts[i + 1])
5814                {
5815                    let else_absent = else_stmt.is_none()
5816                        || else_stmt
5817                            .as_deref()
5818                            .is_some_and(|e| matches!(e, Stmt::Block(v) if v.is_empty()));
5819                    let then_absent = matches!(&**then_stmt, Stmt::Block(v) if v.is_empty());
5820                    let try_lead = |over: Option<((u32, Expr, Expr), Vec<Stmt>)>,
5821                                    keep_over: bool|
5822                     -> Option<(Stmt, Stmt)> {
5823                        let ((v2, tgt2, a), rest) = over?;
5824                        if !(*v1 == v2 || same_render_var(*v1, v2, vt)) {
5825                            return None;
5826                        }
5827                        if stmts_count_writes(&rest, *v1) != 0
5828                            || stmts_count_writes(&rest, v2) != 0
5829                        {
5830                            return None;
5831                        }
5832                        let keep = if *v1 == v2 {
5833                            b.clone()
5834                        } else {
5835                            Expr::Local {
5836                                var: *v1,
5837                                ty: vt.var(*v1).ty.clone(),
5838                            }
5839                        };
5840                        if !diamond_types_ok(&a, &keep) {
5841                            return None;
5842                        }
5843                        let t = if keep_over {
5844                            mk_ternary_assign(tgt2, cond.clone(), keep, a)
5845                        } else {
5846                            mk_ternary_assign(tgt2, cond.clone(), a, keep)
5847                        };
5848                        let mut new_if = stmts[i + 1].clone();
5849                        if let Stmt::If { then_stmt, else_stmt, .. } = &mut new_if {
5850                            if keep_over {
5851                                *else_stmt = Some(Box::new(Stmt::Block(rest)));
5852                            } else {
5853                                **then_stmt = Stmt::Block(rest);
5854                            }
5855                        }
5856                        Some((t, new_if))
5857                    };
5858                    if else_absent {
5859                        lead_fold = try_lead(leading(then_stmt), false);
5860                    } else if then_absent {
5861                        if let Some(e) = else_stmt {
5862                            lead_fold = try_lead(leading(e), true);
5863                        }
5864                    }
5865                }
5866            }
5867            if let Some((folded, new_if)) = lead_fold {
5868                stmts[i] = folded;
5869                stmts[i + 1] = new_if;
5870                continue;
5871            }
5872            if let Some(f) = shape2 {
5873                stmts[i] = f;
5874                stmts.remove(i + 1);
5875                continue; // re-scan the folded position
5876            }
5877        }
5878        i += 1;
5879    }
5880}
5881
5882pub fn fold_bool_value_diamonds(body: &mut Stmt, vt: &VarTable) {
5883    fn is_bool_var(v: u32, vt: &VarTable) -> bool {
5884        (v as usize) < vt.vars.len()
5885            && matches!(vt.vars[v as usize].ty.erased(), JavaType::Boolean)
5886    }
5887    fn bool_const(e: &Expr) -> Option<bool> {
5888        fn cint(e: &Expr) -> Option<i32> {
5889            match e {
5890                Expr::Const(ConstVal::Int(n)) => Some(*n),
5891                _ => None,
5892            }
5893        }
5894        match e {
5895            Expr::Const(ConstVal::Int(0)) => Some(false),
5896            Expr::Const(ConstVal::Int(1)) => Some(true),
5897            // int↔bool bridge wrappings of constants: `0 != 0` is the
5898            // fallback pipeline's rendered `false` by the time the
5899            // ctor-block fold runs.
5900            Expr::Bin { op: op @ (BinOp::Eq | BinOp::Ne), l, r, .. } => {
5901                let (a, b) = (cint(l)?, cint(r)?);
5902                let eq = a == b;
5903                Some(if matches!(op, BinOp::Eq) { eq } else { !eq })
5904            }
5905            _ => None,
5906        }
5907    }
5908    fn reads_var(e: &Expr, v: u32) -> bool {
5909        let mut hit = false;
5910        visit_exprs(e, &mut |x| {
5911            if let Expr::Local { var, .. } = x {
5912                if *var == v {
5913                    hit = true;
5914                }
5915            }
5916        });
5917        hit
5918    }
5919    /// Bool-valued expression for the composite arms (&&, ||, ?:).
5920    /// Bare 0/1 constants are handled by the caller's special cases —
5921    /// inside a composite they would render as invalid `c && 1`.
5922    fn bool_valued(e: &Expr, vt: &VarTable) -> bool {
5923        fn side(e: &Expr, vt: &VarTable) -> bool {
5924            bool_const(e).is_some() || bool_valued(e, vt)
5925        }
5926        match e {
5927            Expr::Local { var, .. } => is_bool_var(*var, vt),
5928            Expr::Method { desc, .. } => desc.ret == JavaType::Boolean,
5929            Expr::InstanceOf { .. } => true,
5930            Expr::Un { op: UnOp::Not, e: x, .. } => bool_valued(x, vt) || bool_const(x).is_some(),
5931            Expr::Bin { op, l, r, .. } => match op {
5932                BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le => true,
5933                // A logical operand may itself be a 0/1 constant
5934                // (booleanize leaves mixed forms).
5935                BinOp::LogAnd | BinOp::LogOr => {
5936                    side(l, vt) && side(r, vt)
5937                }
5938                _ => false,
5939            },
5940            Expr::Cond { t, f, .. } => side(t, vt) && side(f, vt),
5941            _ => false,
5942        }
5943    }
5944    /// A branch that is exactly one plain `v = E` with E bool-valued
5945    /// (or a 0/1 constant) and not reading v — OR the forwarding PAIR
5946    /// `[u = E; v = u]` where u is single-assigned/single-read in the
5947    /// whole method: the fold consumes the pair into `v = E` (the impure
5948    /// `u = src.m()` arm shape forward_single_use cannot inline — its
5949    /// phi-ref gate rejects values reading multi-assigned prelude vars,
5950    /// and inlining here is scope-safe because E moves exactly one
5951    /// block level out with the arm's lone def consumed).
5952    fn lone_assign(
5953        b: &Stmt,
5954        v: u32,
5955        vt: &VarTable,
5956        an: &VarAnalysis,
5957        consumed: &mut HashSet<u32>,
5958    ) -> Option<Expr> {
5959        let l = flat_list(b);
5960        if l.len() == 2 {
5961            let (u, e): (u32, &Expr) = match l[0] {
5962                Stmt::LocalDef { var, init: Some(e), .. } => (*var, e),
5963                Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => {
5964                    match &**target {
5965                        Expr::Local { var, .. } => (*var, &**value),
5966                        _ => return None,
5967                    }
5968                }
5969                _ => return None,
5970            };
5971            if u == v || consumed.contains(&u) {
5972                return None;
5973            }
5974            // Whole-method single-assign/single-read: the fold drops
5975            // u's def with the arm, so ANY other read would dangle.
5976            // (Analysis counts are per-round; folds only ever REMOVE
5977            // reads elsewhere or clone E's — whose variable reads were
5978            // already counted at their def sites — so stale counts
5979            // over-approximate and the ==1 gates stay sound.)
5980            if an.assigns.get(u as usize).copied().unwrap_or(0) != 1
5981                || an.reads.get(u as usize).copied().unwrap_or(0) != 1
5982            {
5983                return None;
5984            }
5985            let (tv, rv) = match l[1] {
5986                Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => {
5987                    match (&**target, &**value) {
5988                        (Expr::Local { var: t, .. }, Expr::Local { var: r, .. }) => (*t, *r),
5989                        _ => return None,
5990                    }
5991                }
5992                Stmt::LocalDef { var, init: Some(Expr::Local { var: r, .. }), .. } => (*var, *r),
5993                _ => return None,
5994            };
5995            if tv != v || rv != u {
5996                return None;
5997            }
5998            if reads_var(e, v) || !(bool_valued(e, vt) || bool_const(e).is_some()) {
5999                return None;
6000            }
6001            consumed.insert(u);
6002            return Some(e.clone());
6003        }
6004        if l.len() != 1 {
6005            return None;
6006        }
6007        let (target, value) = match l[0] {
6008            Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => (target, value),
6009            Stmt::LocalDef { var, init: Some(value), .. } if *var == v => {
6010                if reads_var(value, v) || !(bool_valued(value, vt) || bool_const(value).is_some()) {
6011                    return None;
6012                }
6013                return Some(value.clone());
6014            }
6015            _ => return None,
6016        };
6017        let Expr::Local { var: tv, .. } = &**target else { return None };
6018        if *tv != v || reads_var(value, v) {
6019            return None;
6020        }
6021        if !(bool_valued(value, vt) || bool_const(value).is_some()) {
6022            return None;
6023        }
6024        Some((**value).clone())
6025    }
6026    fn not(e: Expr) -> Expr {
6027        Expr::Un { op: UnOp::Not, e: Box::new(e) }
6028    }
6029    /// Peephole the int↔bool bridge artifacts the fallback pipeline
6030    /// leaves in guards: `!(x != y)` → `x == y`; `(b ? 1 : 0) == 1` →
6031    /// `b`; `!(b ? 1 : 0 == 1)` chains likewise. Purely a readability
6032    /// pass — the unsimplified forms compile.
6033    fn simplify_bool(e: Expr) -> Expr {
6034        match e {
6035            Expr::Un { op: UnOp::Not, e: inner, .. } => match *inner {
6036                // Flip first, THEN re-simplify: `!((u?1:0) != 1)` must
6037                // reach the cond01 arm as `(u?1:0) == 1` → `u` (the
6038                // flip result never re-entered simplification and the
6039                // pair arm saw a Bin where it needed the bare local).
6040                Expr::Bin { op: BinOp::Ne, l, r, ty } => {
6041                    simplify_bool(Expr::Bin { op: BinOp::Eq, l, r, ty })
6042                }
6043                Expr::Bin { op: BinOp::Eq, l, r, ty } => {
6044                    simplify_bool(Expr::Bin { op: BinOp::Ne, l, r, ty })
6045                }
6046                other => Expr::Un { op: UnOp::Not, e: Box::new(simplify_bool(other)) },
6047            },
6048            Expr::Bin { op: op @ (BinOp::Eq | BinOp::Ne), l, r, ty } => {
6049                // (c ? 1 : 0) <op> 1  →  c / !c ; <op> 0 → !c / c
6050                fn cond01(e: &Expr) -> Option<Expr> {
6051                    if let Expr::Cond { c, t, f } = e {
6052                        if matches!(&**t, Expr::Const(ConstVal::Int(1)))
6053                            && matches!(&**f, Expr::Const(ConstVal::Int(0)))
6054                        {
6055                            return Some((**c).clone());
6056                        }
6057                    }
6058                    None
6059                }
6060                let (lc, rc) = (cond01(&l), cond01(&r));
6061                match (lc, rc) {
6062                    (Some(c), None) => match (&*r, op) {
6063                        (Expr::Const(ConstVal::Int(1)), BinOp::Eq) => c,
6064                        (Expr::Const(ConstVal::Int(1)), BinOp::Ne) => not(c),
6065                        (Expr::Const(ConstVal::Int(0)), BinOp::Eq) => not(c),
6066                        (Expr::Const(ConstVal::Int(0)), BinOp::Ne) => c,
6067                        _ => Expr::Bin { op, l: Box::new(c), r, ty },
6068                    },
6069                    (None, Some(c)) => match (&*l, op) {
6070                        (Expr::Const(ConstVal::Int(1)), BinOp::Eq) => c,
6071                        (Expr::Const(ConstVal::Int(1)), BinOp::Ne) => not(c),
6072                        (Expr::Const(ConstVal::Int(0)), BinOp::Eq) => not(c),
6073                        (Expr::Const(ConstVal::Int(0)), BinOp::Ne) => c,
6074                        _ => Expr::Bin { op, l, r: Box::new(c), ty },
6075                    },
6076                    _ => Expr::Bin { op, l, r, ty },
6077                }
6078            }
6079            other => other,
6080        }
6081    }
6082    fn bin(op: BinOp, l: Expr, r: Expr) -> Expr {
6083        Expr::Bin { op, l: Box::new(l), r: Box::new(r), ty: None }
6084    }
6085
6086    for _round in 0..4 {
6087        let mut changed = false;
6088        let mut analysis = VarAnalysis {
6089            assigns: Vec::new(),
6090            reads: Vec::new(),
6091            values: Vec::new(),
6092        };
6093        analyze_vars(body, &mut analysis);
6094        let mut consumed: HashSet<u32> = HashSet::default();
6095        walk_mut_deep(body, &mut |st| {
6096            let Stmt::Block(list) = st else { return };
6097            let mut i = 0usize;
6098            while i + 1 < list.len() {
6099                let folded = try_at(list, i, vt, &is_bool_var, &bool_const, &reads_var, &lone_assign, &analysis, &mut consumed);
6100                match folded {
6101                    Some(expr) => {
6102                        let is_def = matches!(&list[i], Stmt::LocalDef { .. });
6103                        let var = match &list[i] {
6104                            Stmt::LocalDef { var, .. } => *var,
6105                            Stmt::ExprStmt(Expr::Assign { target, .. }) => {
6106                                match &**target { Expr::Local { var, .. } => *var, _ => unreachable!() }
6107                            }
6108                            _ => unreachable!(),
6109                        };
6110                        let (is_final, force_type) = match &list[i] {
6111                            Stmt::LocalDef { is_final, force_type, .. } => (*is_final, *force_type),
6112                            _ => (false, false),
6113                        };
6114                        list[i] = if is_def {
6115                            Stmt::LocalDef { var, init: Some(expr), is_final, force_type }
6116                        } else {
6117                            Stmt::ExprStmt(Expr::Assign {
6118                                target: Box::new(Expr::Local {
6119                                    var,
6120                                    ty: TypeRef::J(JavaType::Boolean),
6121                                }),
6122                                op: AssignOp::Plain,
6123                                value: Box::new(expr),
6124                            })
6125                        };
6126                        list.remove(i + 1);
6127                        changed = true;
6128                        // stay at i: a preceding base may now pair with
6129                        // nothing new, but a FOLLOWING diamond may chain.
6130                    }
6131                    None => i += 1,
6132                }
6133            }
6134        });
6135        if !changed {
6136            break;
6137        }
6138    }
6139
6140    #[allow(clippy::too_many_arguments, clippy::type_complexity)]
6141    fn try_at(
6142        list: &[Stmt],
6143        i: usize,
6144        vt: &VarTable,
6145        is_bool_var: &dyn Fn(u32, &VarTable) -> bool,
6146        bool_const: &dyn Fn(&Expr) -> Option<bool>,
6147        reads_var: &dyn Fn(&Expr, u32) -> bool,
6148        lone_assign: &dyn Fn(&Stmt, u32, &VarTable, &VarAnalysis, &mut HashSet<u32>) -> Option<Expr>,
6149        an: &VarAnalysis,
6150        consumed: &mut HashSet<u32>,
6151    ) -> Option<Expr> {
6152        // s1: `v = <0|1>` (Assign or LocalDef), v Boolean-typed.
6153        let (v, base) = match &list[i] {
6154            Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => {
6155                match &**target {
6156                    Expr::Local { var, .. } => {
6157                        // The value may be a wrapped constant (`0 != 0`
6158                        // — the fallback pipeline's rendered false).
6159                        let b = bool_const(value)?;
6160                        if !is_bool_var(*var, vt) { return None; }
6161                        (*var, b)
6162                    }
6163                    _ => return None,
6164                }
6165            }
6166            Stmt::LocalDef { var, init: Some(e), .. } => {
6167                let b = bool_const(e)?;
6168                if !is_bool_var(*var, vt) { return None; }
6169                (*var, b)
6170            }
6171            _ => return None,
6172        };
6173        let Stmt::If { cond, then_stmt, else_stmt } = &list[i + 1] else {
6174            return None;
6175        };
6176        if reads_var(cond, v) {
6177            return None;
6178        }
6179        let mut touches_v = false;
6180        visit_exprs(cond, &mut |x| {
6181            if let Expr::Assign { target, .. } = x {
6182                if let Expr::Local { var, .. } = &**target {
6183                    if *var == v { touches_v = true; }
6184                }
6185            }
6186        });
6187        if touches_v {
6188            return None;
6189        }
6190        // The structurer emits inverted diamonds (`if (c) {} else { v = E }`,
6191        // empty-then) — normalize by negating the guard.
6192        let (cond_n, then_arm, else_arm): (Expr, &Stmt, Option<&Stmt>) =
6193            if flat_list(then_stmt).is_empty() && else_stmt.is_some() {
6194                (not(cond.clone()), else_stmt.as_ref().unwrap(), None)
6195            } else {
6196                (cond.clone(), then_stmt.as_ref(), else_stmt.as_deref())
6197            };
6198        let e1 = lone_assign(then_arm, v, vt, an, consumed)?;
6199        let e2 = match else_arm {
6200            None => None,
6201            Some(b) => Some(lone_assign(b, v, vt, an, consumed)?),
6202        };
6203        let e1c = bool_const(&e1);
6204        let c = simplify_bool(cond_n);
6205        Some(match e2 {
6206            None => {
6207                if !base {
6208                    match e1c {
6209                        Some(true) => c,
6210                        Some(false) => return None, // no-op diamond: guard effects would drop
6211                        None => bin(BinOp::LogAnd, c, e1),
6212                    }
6213                } else {
6214                    match e1c {
6215                        Some(false) => not(c),
6216                        Some(true) => return None,
6217                        None => bin(BinOp::LogOr, not(c), e1),
6218                    }
6219                }
6220            }
6221            Some(e2) => {
6222                // Full diamond: both arms assign — the base is dead.
6223                if e1 == e2 {
6224                    return None; // both arms equal: folding drops guard effects
6225                }
6226                Expr::Cond { c: Box::new(c), t: Box::new(e1), f: Box::new(e2) }
6227            }
6228        })
6229    }
6230}
6231
6232pub fn idiom_compounds(body: &mut Stmt, vt: &VarTable) {
6233    walk_mut_deep(body, &mut |st| {
6234        let Stmt::ExprStmt(e) = st else { return };
6235        let Expr::Assign { target, op: AssignOp::Plain, value } = &mut *e else {
6236            return;
6237        };
6238        // Target: a local, or a bare field (static / this — no complex
6239        // receiver whose re-evaluation a compound would elide).
6240        enum Tgt {
6241            Loc(u32),
6242            Fld,
6243        }
6244        let tgt = match &**target {
6245            Expr::Local { var, .. } => Tgt::Loc(*var),
6246            Expr::Field { owner, .. }
6247                if owner.is_none()
6248                    || matches!(&**owner.as_ref().unwrap(), Expr::This) =>
6249            {
6250                Tgt::Fld
6251            }
6252            _ => return,
6253        };
6254        let Expr::Bin { op, l, r, .. } = &mut **value else {
6255            return;
6256        };
6257        // The left operand must be the SAME storage as the target.
6258        match (&tgt, &**l) {
6259            (Tgt::Loc(v), Expr::Local { var, .. }) if *var == *v => {}
6260            (Tgt::Fld, Expr::Field { cls: c2, name: n2, .. }) => {
6261                let Expr::Field { cls: c1, name: n1, .. } = &**target else {
6262                    return;
6263                };
6264                if c1 != c2 || n1 != n2 {
6265                    return;
6266                }
6267            }
6268            _ => return,
6269        }
6270        // ±1 constant on a numeric var → post-inc/dec.
6271        let one = match &**r {
6272            Expr::Const(ConstVal::Int(1)) => Some(1i64),
6273            Expr::Const(ConstVal::Int(-1)) => Some(-1),
6274            Expr::Const(ConstVal::Long(1)) => Some(1),
6275            Expr::Const(ConstVal::Long(-1)) => Some(-1),
6276            _ => None,
6277        };
6278        if *op == BinOp::Add || *op == BinOp::Sub {
6279            if let Some(mut d) = one {
6280                if *op == BinOp::Sub {
6281                    d = -d;
6282                }
6283                let tgt_ty = match &tgt {
6284                    Tgt::Loc(v) => vt.var(*v).ty.erased(),
6285                    Tgt::Fld => {
6286                        let Expr::Field { ty, .. } = &**target else {
6287                            return;
6288                        };
6289                        ty.erased()
6290                    }
6291                };
6292                let numeric = matches!(
6293                    tgt_ty,
6294                    JavaType::Int
6295                        | JavaType::Long
6296                        | JavaType::Short
6297                        | JavaType::Byte
6298                        | JavaType::Char
6299                        | JavaType::Float
6300                        | JavaType::Double
6301                );
6302                if numeric {
6303                    let wide = matches!(tgt_ty, JavaType::Long);
6304                    let inner = match &tgt {
6305                        Tgt::Loc(v) => Expr::Local {
6306                            var: *v,
6307                            ty: vt.var(*v).ty.clone(),
6308                        },
6309                        Tgt::Fld => (**target).clone(),
6310                    };
6311                    *e = Expr::PostIncDec {
6312                        e: Box::new(inner),
6313                        delta: d,
6314                        wide,
6315                    };
6316                    return;
6317                }
6318            }
6319        }
6320        let cop = match op {
6321            BinOp::Add => AssignOp::Add,
6322            BinOp::Sub => AssignOp::Sub,
6323            BinOp::Mul => AssignOp::Mul,
6324            BinOp::Div => AssignOp::Div,
6325            BinOp::Rem => AssignOp::Rem,
6326            BinOp::Shl => AssignOp::Shl,
6327            BinOp::Shr => AssignOp::Shr,
6328            BinOp::Ushr => AssignOp::Ushr,
6329            BinOp::And => AssignOp::And,
6330            BinOp::Or => AssignOp::Or,
6331            BinOp::Xor => AssignOp::Xor,
6332            _ => return,
6333        };
6334        let rhs = std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
6335        let new_target = match &tgt {
6336            Tgt::Loc(v) => Box::new(Expr::Local {
6337                var: *v,
6338                ty: vt.var(*v).ty.clone(),
6339            }),
6340            Tgt::Fld => target.clone(),
6341        };
6342        *e = Expr::Assign {
6343            target: new_target,
6344            op: cop,
6345            value: rhs,
6346        };
6347    });
6348}
6349
6350pub fn fix_bool_xor(body: &mut Stmt, vt: &VarTable, ret_bool: bool) {
6351    fn is_one(e: &Expr) -> bool {
6352        matches!(e, Expr::Const(ConstVal::Int(1)))
6353    }
6354    fn bty(e: &Expr, vt: &VarTable) -> bool {
6355        matches!(
6356            match e {
6357                Expr::Local { var, .. } => vt.var(*var).ty.erased(),
6358                other => other.type_ref().erased(),
6359            },
6360            JavaType::Boolean
6361        )
6362    }
6363    // Int-typed `x ^ 1` in a BOOLEAN sink is the compiler's `!x` over a
6364    // 0/1 slot: rewrite to `x == 0` (the booleanize return-wrap misses
6365    // Xor shapes — weibo ArraysKt `return v2 ^ 1;` against a boolean
6366    // return, "int无法转换为boolean").
6367    fn inv_of_xor(x: &Expr, vt: &VarTable) -> Option<Expr> {
6368        if let Expr::Bin { op: BinOp::Xor, l, r, .. } = x {
6369            let (int_side, one_side) = if is_one(r) {
6370                (l, r)
6371            } else if is_one(l) {
6372                (r, l)
6373            } else {
6374                return None;
6375            };
6376            let _ = one_side;
6377            if !bty(int_side, vt) {
6378                return Some(Expr::Bin {
6379                    op: BinOp::Eq,
6380                    l: int_side.clone(),
6381                    r: Box::new(Expr::Const(ConstVal::Int(0))),
6382                    ty: Some(TypeRef::J(JavaType::Boolean)),
6383                });
6384            }
6385        }
6386        None
6387    }
6388    let tgt_bool = |target: &Expr, vt: &VarTable| -> bool {
6389        match target {
6390            Expr::Local { var, .. } => matches!(vt.var(*var).ty.erased(), JavaType::Boolean),
6391            Expr::Field { ty, .. } => matches!(ty.erased(), JavaType::Boolean),
6392            _ => false,
6393        }
6394    };
6395    walk_mut_deep(body, &mut |st| match st {
6396        Stmt::Return(Some(e)) if ret_bool => {
6397            if let Some(r) = inv_of_xor(e, vt) {
6398                *e = r;
6399            }
6400        }
6401        Stmt::ExprStmt(Expr::Assign { target, value, op: AssignOp::Plain, .. }) => {
6402            if tgt_bool(target, vt) {
6403                if let Some(r) = inv_of_xor(value, vt) {
6404                    **value = r;
6405                }
6406            }
6407        }
6408        Stmt::LocalDef { var, init: Some(value), .. }
6409            if matches!(vt.var(*var).ty.erased(), JavaType::Boolean) =>
6410        {
6411            if let Some(r) = inv_of_xor(value, vt) {
6412                *value = r;
6413            }
6414        }
6415        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
6416            if let Some(r) = inv_of_xor(cond, vt) {
6417                *cond = r;
6418            }
6419        }
6420        _ => {}
6421    });
6422    fn ity(e: &Expr, vt: &VarTable) -> bool {
6423        matches!(
6424            match e {
6425                Expr::Local { var, .. } => vt.var(*var).ty.erased(),
6426                other => other.type_ref().erased(),
6427            },
6428            JavaType::Int | JavaType::Short | JavaType::Byte | JavaType::Char
6429        )
6430    }
6431    /// Mixed-kind bitwise rewrite: Kotlin's non-short-circuit
6432    /// `or`/`and` over 0/1 ints mixes generations once booleanize
6433    /// converts one side (`delete | delete2` — "boolean无法转换为int" at
6434    /// the OPERAND, weixin SQLiteDatabase ×3.3k), and `b ^ 1` is `!b`.
6435    /// The int side is a 0/1 encoding: compare it, keeping the chain
6436    /// boolean end to end. ONLY legal into a BOOLEAN sink — ungated, it
6437    /// rewrote int-sink `v6 = v5x | 4` into `v5x | 4 != 0` assigned to
6438    /// an int var (boolean无法转换为int, weixin yq5/c ×2.1k family).
6439    fn mixed_rewrite(e: &mut Expr, vt: &VarTable) {
6440        deep_rewrite(e, &mut |x| {
6441            if let Expr::Bin { op, l, r, ty } = x {
6442                match op {
6443                    BinOp::Xor => {
6444                        if is_one(r) && bty(l, vt) {
6445                            let taken =
6446                                std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
6447                            *x = Expr::Un { op: UnOp::Not, e: taken };
6448                        } else if is_one(l) && bty(r, vt) {
6449                            let taken =
6450                                std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
6451                            *x = Expr::Un { op: UnOp::Not, e: taken };
6452                        }
6453                    }
6454                    BinOp::Or | BinOp::And => {
6455                        if bty(l, vt) && ity(r, vt) {
6456                            let taken =
6457                                std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
6458                            **r = Expr::Bin {
6459                                op: BinOp::Ne,
6460                                l: taken,
6461                                r: Box::new(Expr::Const(ConstVal::Int(0))),
6462                                ty: Some(TypeRef::J(JavaType::Boolean)),
6463                            };
6464                            *ty = Some(TypeRef::J(JavaType::Boolean));
6465                        } else if ity(l, vt) && bty(r, vt) {
6466                            let taken =
6467                                std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
6468                            **l = Expr::Bin {
6469                                op: BinOp::Ne,
6470                                l: taken,
6471                                r: Box::new(Expr::Const(ConstVal::Int(0))),
6472                                ty: Some(TypeRef::J(JavaType::Boolean)),
6473                            };
6474                            *ty = Some(TypeRef::J(JavaType::Boolean));
6475                        }
6476                    }
6477                    _ => {}
6478                }
6479            }
6480        });
6481    }
6482    // Boolean sinks: bool-typed assign/def targets, boolean-method
6483    // returns, and loop/branch conditions.
6484    walk_mut_deep(body, &mut |st| match st {
6485        Stmt::Return(Some(e)) if ret_bool => mixed_rewrite(e, vt),
6486        Stmt::ExprStmt(Expr::Assign { target, value, op: AssignOp::Plain, .. }) => {
6487            if tgt_bool(target, vt) {
6488                mixed_rewrite(value, vt);
6489            }
6490        }
6491        Stmt::LocalDef { var, init: Some(e), .. }
6492            if matches!(vt.var(*var).ty.erased(), JavaType::Boolean) =>
6493        {
6494            mixed_rewrite(e, vt);
6495        }
6496        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
6497            mixed_rewrite(cond, vt);
6498        }
6499        _ => {}
6500    });
6501    // A conditional's test is a boolean sink wherever it sits (including
6502    // inside int-sink statements the walk above skips).
6503    walk_stmt_exprs(body, &mut |e| {
6504        deep_rewrite(e, &mut |x| {
6505            if let Expr::Cond { c, .. } = x {
6506                mixed_rewrite(c, vt);
6507            }
6508        });
6509    });
6510}
6511
6512/// Boolean inference: vars only ever assigned 0/1/comparisons/booleans and
6513/// read in conditions become `boolean`, with `v != 0` → `v` in conditions.
6514/// Returns the number of vars converted (0 = nothing changed — the
6515/// caller skips the post-booleanize re-split, which only has work when
6516/// a conversion exposed a mixed-kind register).
6517pub fn booleanize(vt: &mut VarTable, body: &mut Stmt, ret_bool: bool) -> usize {
6518    // Booleans propagate through local chains (`v17 = v24` where v24
6519    // itself became boolean in the first round) — iterate to a fixpoint;
6520    // the common corpus converts nothing and exits after one round.
6521    // The deref disqualifier set is structural (positions, not types) —
6522    // compute ONCE per call, not per round (12 full walks on big
6523    // methods cost weibo +60% wall).
6524    let n0 = vt.vars.len();
6525    let mut derefed = vec![false; n0];
6526    mark_derefs(body, &mut derefed);
6527    let mut total = 0usize;
6528    for _ in 0..4 {
6529        let n = booleanize_round(vt, body, ret_bool, &derefed);
6530        if n == 0 {
6531            break;
6532        }
6533        total += n;
6534    }
6535    total
6536}
6537
6538/// Mark vars used in DEREF positions (field/method receiver, array
6539/// base) — allocation-free recursion (visit_all_exprs allocates a
6540/// child Vec per statement; two callers run this per method).
6541fn mark_deref_expr(e: &Expr, derefed: &mut [bool]) {
6542    match e {
6543        Expr::Field { owner: Some(o), is_static: false, .. }
6544        | Expr::Method { owner: Some(o), .. } => {
6545            if let Expr::Local { var, .. } = &**o {
6546                if (*var as usize) < derefed.len() {
6547                    derefed[*var as usize] = true;
6548                }
6549            }
6550        }
6551        Expr::ArrayIndex { array, .. } => {
6552            if let Expr::Local { var, .. } = &**array {
6553                if (*var as usize) < derefed.len() {
6554                    derefed[*var as usize] = true;
6555                }
6556            }
6557        }
6558        _ => {}
6559    }
6560    for_each_child(e, &mut |c| mark_deref_expr(c, derefed));
6561}
6562
6563fn mark_derefs(body: &Stmt, derefed: &mut [bool]) {
6564    walk_all(body, &mut |st| {
6565        match st {
6566            Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
6567                mark_deref_expr(e, derefed);
6568            }
6569            Stmt::Return(Some(e)) => mark_deref_expr(e, derefed),
6570            Stmt::LocalDef { init: Some(e), .. } => mark_deref_expr(e, derefed),
6571            Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
6572                mark_deref_expr(cond, derefed);
6573            }
6574            Stmt::For { init, .. } => {
6575                for x in init {
6576                    mark_derefs(x, derefed);
6577                }
6578            }
6579            Stmt::Switch { selector, .. } => mark_deref_expr(selector, derefed),
6580            Stmt::ForEach { iterable, .. } => mark_deref_expr(iterable, derefed),
6581            _ => {}
6582        }
6583    });
6584}
6585
6586fn booleanize_round(
6587    vt: &mut VarTable,
6588    body: &mut Stmt,
6589    ret_bool: bool,
6590    derefed: &[bool],
6591) -> usize {
6592    let n = vt.vars.len();
6593    let mut in_cond = vec![false; n];
6594
6595    // Condition uses: `v == 0` / `v != 0` if-conditions and comparison
6596    // operands inside assigned values.
6597    walk_all(body, &mut |st| {
6598        let cond: Option<&Expr> = match st {
6599            Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
6600                Some(cond)
6601            }
6602            _ => None,
6603        };
6604        if let Some(c) = cond {
6605            if let Expr::Bin {
6606                op: BinOp::Eq | BinOp::Ne,
6607                l,
6608                r,
6609                ..
6610            } = c
6611            {
6612                for (x, other) in [(l, r), (r, l)] {
6613                    if let (Expr::Local { var, .. }, Expr::Const(ConstVal::Int(0))) =
6614                        (&**x, &**other)
6615                    {
6616                        if (*var as usize) < n {
6617                            in_cond[*var as usize] = true;
6618                        }
6619                    }
6620                }
6621            }
6622        }
6623        // A local RETURNED from a boolean method is boolean-typed even
6624        // though it never appears in a condition (`int v9 = 0/1 … return
6625        // v9;` — returning an int from `boolean check()` is a compile
6626        // error; lab package Obf.check).
6627        if ret_bool {
6628            if let Stmt::Return(Some(Expr::Local { var, .. })) = st {
6629                if (*var as usize) < n {
6630                    in_cond[*var as usize] = true;
6631                }
6632            }
6633        }
6634        // A local STORED into a boolean field is boolean-typed even though
6635        // it never appears in a condition (`b = v1` where `b` is a boolean
6636        // field, `v1` an int 0/1 local — q.java static init). The emit
6637        // layer already coerces a boolean-target assign through expr_bool,
6638        // but that renders a bare int LOCAL unchanged, so the local itself
6639        // must be booleanized (its 0/1 assigns then print false/true).
6640        if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = st {
6641            if let Expr::Field { ty, .. } = &**target {
6642                if ty.erased() == JavaType::Boolean {
6643                    if let Expr::Local { var, .. } = &**value {
6644                        if (*var as usize) < n {
6645                            in_cond[*var as usize] = true;
6646                        }
6647                    }
6648                }
6649            }
6650        }
6651        let val: Option<&Expr> = match st {
6652            Stmt::ExprStmt(Expr::Assign { value, .. }) => Some(value),
6653            // Bare expression statements (e.g. `q(v117);`) so a local
6654            // passed to a boolean parameter is caught below.
6655            Stmt::ExprStmt(e) => Some(e),
6656            Stmt::LocalDef { init: Some(e), .. } => Some(e),
6657            Stmt::Return(Some(e)) => Some(e),
6658            _ => None,
6659        };
6660        if let Some(v) = val {
6661            visit_exprs(v, &mut |x| {
6662                // A local passed as a BOOLEAN parameter is boolean-typed
6663                // (`q(v117)` where q's param is boolean, v117 an int 0/1 —
6664                // f8/b.java). The DEX passes booleans as 0/1 ints, so a
6665                // boolean param slot is authoritative for the argument's
6666                // type.
6667                if let Expr::Method { desc, args, .. } = x {
6668                    for (i, a) in args.iter().enumerate() {
6669                        if desc.args.get(i) == Some(&JavaType::Boolean) {
6670                            if let Expr::Local { var, .. } = a {
6671                                if (*var as usize) < n {
6672                                    in_cond[*var as usize] = true;
6673                                }
6674                            }
6675                        }
6676                    }
6677                }
6678                if let Expr::Bin { op, l, r, .. } = x {
6679                    if matches!(
6680                        op,
6681                        BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
6682                    ) {
6683                        for side in [l, r] {
6684                            if let Expr::Local { var, .. } = &**side {
6685                                if (*var as usize) < n {
6686                                    in_cond[*var as usize] = true;
6687                                }
6688                            }
6689                        }
6690                    }
6691                    // Kotlin/d8 merge booleans through INT bitwise ops
6692                    // (`v3 | obj instanceof g`) == 0 — the other side's
6693                    // static type being boolean makes this a boolean
6694                    // context for the local (a genuine int bitwise
6695                    // expression never has a boolean operand).
6696                    if matches!(op, BinOp::Or | BinOp::And) {
6697                        let l_bool = l.type_ref().erased() == JavaType::Boolean;
6698                        let r_bool = r.type_ref().erased() == JavaType::Boolean;
6699                        if l_bool != r_bool {
6700                            let side = if l_bool { r } else { l };
6701                            if let Expr::Local { var, .. } = &**side {
6702                                if (*var as usize) < n {
6703                                    in_cond[*var as usize] = true;
6704                                }
6705                            }
6706                        }
6707                    }
6708                }
6709            });
6710        }
6711    });
6712
6713    // A var whose every assignment value is boolean-shaped AND which is
6714    // referenced in conditions becomes boolean.
6715    // (Perf: this used to re-walk the whole statement tree ONCE PER VAR —
6716    // O(vars × stmts); on R8-merged monsters (2000+ vars, 15k statements)
6717    // that was tens of millions of visits. One pass collects the same
6718    // (any, all_bool) facts for every var.)
6719    let mut assigned_any = vec![false; n];
6720    let mut all_bool = vec![true; n];
6721    let mut edges: Vec<(usize, usize)> = Vec::new();
6722    walk_all(body, &mut |st| {
6723        let (var, value) = match st {
6724            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => match &**target {
6725                Expr::Local { var, .. } => (var, &**value),
6726                _ => return,
6727            },
6728            Stmt::LocalDef {
6729                var, init: Some(e), ..
6730            } => (var, e),
6731            _ => return,
6732        };
6733        let i = *var as usize;
6734        if i < n {
6735            assigned_any[i] = true;
6736            if !is_boolean_valued(value, vt) {
6737                all_bool[i] = false;
6738            }
6739            // Chain edges for the context closure below: `v17 = v24`
6740            // links the two locals; when the TARGET is in a boolean
6741            // context, the source inherits it (its only reader is the
6742            // boolean-shaped chain). Collected as edges because the
6743            // target's context can be discovered anywhere in the tree.
6744            if let Expr::Local { var: src, .. } = value {
6745                if (*src as usize) < n {
6746                    edges.push((i, *src as usize));
6747                }
6748            }
6749        }
6750    });
6751    // Context closure along local chains: a var in a boolean context
6752    // pushes that context through every local it is assigned from
6753    // (`return v17` — v17 = v24 — v24 = 0/1) — the statement order
6754    // cannot be relied on (the return may follow the assignment).
6755    loop {
6756        let mut changed = false;
6757        for &(tgt, src) in &edges {
6758            if in_cond[tgt] && !in_cond[src] && all_bool[src] {
6759                in_cond[src] = true;
6760                changed = true;
6761            }
6762            // FORWARD through pure copies: `v21 = v17` with v17 boolean
6763            // makes v21 boolean when ALL of v21's assignments are
6764            // bool-shaped (the loop-state save pattern — weixin u2/f's
6765            // v62 → v17 → v21 chains left the copy targets int while
6766            // the accumulator booleanized).
6767            if in_cond[src] && !in_cond[tgt] && all_bool[tgt] {
6768                in_cond[tgt] = true;
6769                changed = true;
6770            }
6771        }
6772        if !changed {
6773            break;
6774        }
6775    }
6776    let reads = count_locals_stmts(std::slice::from_ref(body));
6777    let mut boolean_vars: HashSet<u32> = HashSet::default();
6778    for i in 0..n {
6779        if vt.vars[i].is_param {
6780            continue;
6781        }
6782        if derefed[i] {
6783            continue;
6784        }
6785        if !in_cond[i] && !(reads.get(&(i as u32)).copied().unwrap_or(0) == 0) {
6786            continue;
6787        }
6788        if assigned_any[i] && all_bool[i] {
6789            boolean_vars.insert(i as u32);
6790        }
6791    }
6792    if boolean_vars.is_empty() {
6793        return 0;
6794    }
6795    for v in &boolean_vars {
6796        vt.vars[*v as usize].ty = TypeRef::J(JavaType::Boolean);
6797    }
6798    let types: Vec<&TypeRef> = vt.vars.iter().map(|v| &v.ty).collect();
6799    rewrite_exprs(body, &mut |e| {
6800        deep_rewrite(e, &mut |x| {
6801            if let Expr::Local { var, ty } = x {
6802                if let Some(want) = types.get(*var as usize) {
6803                    if ty != *want {
6804                        *ty = (*want).clone();
6805                    }
6806                }
6807            }
6808        });
6809    });
6810
6811    // Condition folding: `b != 0` → `b`, `b == 0` → `!b` (boolean vars).
6812    fold_bool_conditions(body, &boolean_vars);
6813    boolean_vars.len()
6814}
6815
6816fn is_boolean_valued(e: &Expr, vt: &VarTable) -> bool {
6817    match e {
6818        Expr::Const(ConstVal::Int(0)) | Expr::Const(ConstVal::Int(1)) => true,
6819        // Kotlin's non-short-circuit boolean `or`/`and` compiles to `|`/`&`
6820        // over boolean locals (`v15 = delete | delete2 | ..`). EITHER side
6821        // boolean suffices: `int | boolean` is illegal in source — it is
6822        // always a not-yet-booleanized side of a boolean chain (the
6823        // loop-carried accumulator `v15 = v20; v20 = v15 | delete3` can
6824        // never seed its all_bool fixpoint otherwise).
6825        Expr::Bin { op: BinOp::Or | BinOp::And, l, r, .. } => {
6826            is_boolean_valued(l, vt) || is_boolean_valued(r, vt)
6827        }
6828        // Kotlin's `!x` lowers to `x ^ 1`; at booleanize time the value
6829        // is still the Xor (fix_bool_xor runs later), so an int-typed
6830        // generation receiving it never retyped (weixin v2/i's
6831        // `int v264_g160_g7 = !v200`). BOTH sides must be bool-shaped:
6832        // `bool ^ 1` qualifies (Const 0/1 is boolean-valued above),
6833        // a genuine int `flags ^ 1` does not (flags stays Int in vt).
6834        // MUST precede the generic Bin arm (which would swallow Xor).
6835        Expr::Bin {
6836            op: BinOp::Xor,
6837            l,
6838            r,
6839            ..
6840        } => is_boolean_valued(l, vt) && is_boolean_valued(r, vt),
6841        Expr::Bin { op, .. } => matches!(
6842            op,
6843            BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
6844        ),
6845        Expr::InstanceOf { .. } => true,
6846        // The VarTable is the authority: the embedded Local ty is a
6847        // lift-time snapshot and goes STALE across booleanize rounds —
6848        // a converted copy source kept its partners int (`v150 = v131`
6849        // boolean→int, weixin ConstraintLayout ×1.4k files) because the
6850        // all_bool fixpoint consulted the stale Int.
6851        Expr::Local { var, ty } => {
6852            let t = if (*var as usize) < vt.vars.len() {
6853                vt.var(*var).ty.erased()
6854            } else {
6855                ty.erased()
6856            };
6857            t == JavaType::Boolean
6858        }
6859        Expr::Method { desc, .. } => desc.ret == JavaType::Boolean,
6860        Expr::Cond { t, f, .. } => is_boolean_valued(t, vt) && is_boolean_valued(f, vt),
6861        Expr::Un { op: UnOp::Not, .. } => true,
6862        // Field ty comes from the dex descriptor — authoritative, not a
6863        // lift snapshot (`!w1x.i` arrives here still as `w1x.i ^ 1`; the
6864        // Xor arm needs this side bool-shaped).
6865        Expr::Field { ty, .. } => ty.erased() == JavaType::Boolean,
6866        _ => false,
6867    }
6868}
6869
6870fn fold_bool_conditions(s: &mut Stmt, bv: &HashSet<u32>) {
6871    let fold = |e: &mut Expr| {
6872        deep_rewrite(e, &mut |x| {
6873            if let Expr::Bin { op, l, r, .. } = x {
6874                let (var_side, const_side) = match (&**l, &**r) {
6875                    (Expr::Local { .. }, Expr::Const(ConstVal::Int(_))) => (l, 0),
6876                    (Expr::Const(ConstVal::Int(_)), Expr::Local { .. }) => (r, 1),
6877                    _ => return,
6878                };
6879                let var = match &**var_side {
6880                    Expr::Local { var, .. } => *var,
6881                    _ => return,
6882                };
6883                if !bv.contains(&var) {
6884                    return;
6885                }
6886                let _ = const_side;
6887                match op {
6888                    BinOp::Ne => {
6889                        let inner = var_side.clone();
6890                        *x = *inner;
6891                    }
6892                    BinOp::Eq => {
6893                        let inner = var_side.clone();
6894                        *x = Expr::Un {
6895                            op: UnOp::Not,
6896                            e: Box::new(*inner),
6897                        };
6898                    }
6899                    _ => {}
6900                }
6901            }
6902        });
6903    };
6904    match s {
6905        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => fold(cond),
6906        _ => {}
6907    }
6908    walk_mut_deep(s, &mut |st| {
6909        let cond = match st {
6910            Stmt::If { cond, .. } => Some(cond),
6911            Stmt::While { cond, .. } => Some(cond),
6912            Stmt::DoWhile { cond, .. } => Some(cond),
6913            _ => None,
6914        };
6915        if let Some(c) = cond {
6916            fold(c);
6917        }
6918    });
6919}
6920
6921/// Normalize `1 == v` comparisons to `v == 1` (d8 emits const-first if-eq).
6922pub fn flip_const_compares(s: &mut Stmt) {
6923    rewrite_exprs(s, &mut |e| {
6924        deep_rewrite(e, &mut |x| {
6925            if let Expr::Bin { op, l, r, .. } = x {
6926                if matches!(
6927                    op,
6928                    BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
6929                ) {
6930                    let l_const = matches!(&**l, Expr::Const(_));
6931                    let r_var = matches!(&**r, Expr::Local { .. });
6932                    if l_const && r_var {
6933                        std::mem::swap(l, r);
6934                    }
6935                }
6936            }
6937        });
6938    });
6939}
6940
6941/// Object-typed locals compared against 0 compare against `null`.
6942pub fn null_compares(vt: &VarTable, body: &mut Stmt) {
6943    let obj_vars: HashSet<u32> = vt
6944        .vars
6945        .iter()
6946        .filter(|v| v.ty.erased().is_reference())
6947        .map(|v| v.id)
6948        .collect();
6949    rewrite_exprs(body, &mut |e| {
6950        deep_rewrite(e, &mut |x| {
6951            null_side_rewrite(x, &|v| obj_vars.contains(&v));
6952        });
6953    });
6954}
6955
6956/// Drop assignments/declarations of locals that are NEVER read anywhere
6957/// in the method. Register-machine merges commit values no Java-level
6958/// code consumes (phi residue like `printStream = check;` — not just
6959/// noise: the merge variable's inferred type need not match the value,
6960/// so these lines are often type errors as well). Side-effect-free
6961/// values drop entirely; an impure value survives as a bare expression
6962/// statement. Iterated to a fixpoint: dropping `a = b` can make `b`
6963/// unread.
6964pub fn drop_dead_locals(body: &mut Stmt) {
6965    loop {
6966        let mut reads: Vec<usize> = Vec::new();
6967        count_reads(body, &mut reads);
6968        let mut dropped = 0usize;
6969        walk_mut_deep(body, &mut |st| match st {
6970            Stmt::Block(items) => prune_dead_items(items, &reads, &mut dropped),
6971            // Switch case bodies and For inits are bare `Vec<Stmt>`, NOT
6972            // wrapped in a `Stmt::Block`, so the Block arm never sees them.
6973            // Dead phi commits land directly in case bodies (`sb4 =
6974            // compareTo;` per case): pruning only Blocks dropped the
6975            // commit's reader (in a real post-switch Block) but left the
6976            // now-dead commits, stalling the fixpoint cascade and leaking
6977            // mistyped phi residue. TryWithResources.resources is
6978            // deliberately excluded — its LocalDefs carry auto-close
6979            // semantics that a bare-expression rewrite would break.
6980            Stmt::Switch { cases, .. } => {
6981                for c in cases {
6982                    prune_dead_items(&mut c.body, &reads, &mut dropped);
6983                }
6984            }
6985            Stmt::For { init, .. } => prune_dead_items(init, &reads, &mut dropped),
6986            _ => {}
6987        });
6988        if dropped == 0 {
6989            break;
6990        }
6991    }
6992}
6993
6994/// Remove dead assignments/declarations from one bare statement vector.
6995/// Shared by `drop_dead_locals` across every prunable `Vec<Stmt>`
6996/// container. Side-effect-free values drop entirely; an impure value
6997/// survives as a bare expression statement.
6998fn prune_dead_items(items: &mut Vec<Stmt>, reads: &[usize], dropped: &mut usize) {
6999    let dead = |v: u32| reads.get(v as usize).copied().unwrap_or(0) == 0;
7000    items.retain_mut(|x| match x {
7001        Stmt::ExprStmt(Expr::Assign { target, value, op, .. }) => {
7002            let dead_target = matches!(&**target, Expr::Local { var, .. } if dead(*var));
7003            if dead_target && matches!(*op, AssignOp::Plain) {
7004                *dropped += 1;
7005                if has_side_effects(value) {
7006                    let e = std::mem::replace(value, Box::new(Expr::This));
7007                    *x = Stmt::ExprStmt(*e);
7008                    true
7009                } else {
7010                    false
7011                }
7012            } else {
7013                true
7014            }
7015        }
7016        Stmt::LocalDef { var, init, .. } if dead(*var) => {
7017            *dropped += 1;
7018            if let Some(e) = init.take() {
7019                if has_side_effects(&e) {
7020                    *x = Stmt::ExprStmt(e);
7021                    return true;
7022                }
7023            }
7024            false
7025        }
7026        _ => true,
7027    });
7028}
7029
7030
7031/// Vars occurring in this statement's OWN expression payloads (nested
7032/// sub-statements are descended separately so every occurrence carries
7033/// its innermost block id). ForEach/Catch bound vars are declarations,
7034/// not occurrences.
7035fn stmt_shallow_vars<F: FnMut(u32)>(st: &Stmt, out: &mut F) {
7036    fn ex<F: FnMut(u32)>(e: &Expr, out: &mut F) {
7037        visit_exprs(e, &mut |x| {
7038            if let Expr::Local { var, .. } = x {
7039                out(*var);
7040            }
7041        });
7042    }
7043    match st {
7044        Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
7045            ex(e, out)
7046        }
7047        Stmt::TernaryValue { e } => ex(e, out),
7048        Stmt::Return(Some(e)) => ex(e, out),
7049        Stmt::LocalDef { var, init, .. } => {
7050            out(*var);
7051            if let Some(e) = init {
7052                ex(e, out);
7053            }
7054        }
7055        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
7056            ex(cond, out)
7057        }
7058        Stmt::For { cond, update, .. } => {
7059            if let Some(c) = cond {
7060                ex(c, out);
7061            }
7062            for u in update {
7063                ex(u, out);
7064            }
7065        }
7066        Stmt::ForEach { iterable, .. } => ex(iterable, out),
7067        Stmt::Switch { selector, cases, .. } => {
7068            ex(selector, out);
7069            for cg in cases {
7070                if let Some(g) = &cg.guard {
7071                    ex(g, out);
7072                }
7073            }
7074        }
7075        Stmt::Assert { cond, msg } => {
7076            ex(cond, out);
7077            if let Some(m) = msg {
7078                ex(m, out);
7079            }
7080        }
7081        Stmt::Synchronized { lock, .. } => ex(lock, out),
7082        _ => {}
7083    }
7084}
7085
7086/// Pass A: number every Block in pre-order (root = 0) and record each
7087/// var's defining block. `sizes[id]` = the id-range span of the subtree.
7088/// Pass A: number every EMITTER-BRACED position in pre-order (root = 0)
7089/// and record each var's defining block. `sizes[id]` = the id-range span
7090/// of the subtree. A braced position is a Java scope whether or not the
7091/// IR node is a Stmt::Block — switch case bodies are bare Vecs and
7092/// single-statement if arms are bare statements, and sharing the parent's
7093/// id for them hid the scope violation (Telegram jk.java: a case-local
7094/// `notificationCenterDelegate`/`d0x` read after the switch never
7095/// hoisted — 找不到符号 ×76 in one file; the `d9x.L` reads even resolved
7096/// as a PACKAGE — 程序包不存在 ×92 tree-wide).
7097fn scope_pass_a(st: &Stmt, cur: u32, next: &mut u32, def_block: &mut [u32], sizes: &mut Vec<u32>) {
7098    if let Stmt::LocalDef { var, .. } = st {
7099        let i = *var as usize;
7100        if i < def_block.len() && def_block[i] == u32::MAX {
7101            def_block[i] = cur;
7102        }
7103    }
7104    fn scoped(c: &Stmt, next: &mut u32, def_block: &mut [u32], sizes: &mut Vec<u32>) {
7105        let id = *next;
7106        *next += 1;
7107        sizes.push(0);
7108        scope_pass_a(c, id, next, def_block, sizes);
7109        sizes[id as usize] = *next - id;
7110    }
7111    match st {
7112        Stmt::Block(v) => {
7113            for x in v {
7114                scope_pass_a(x, cur, next, def_block, sizes);
7115            }
7116        }
7117        Stmt::If {
7118            then_stmt,
7119            else_stmt,
7120            ..
7121        } => {
7122            scoped(then_stmt, next, def_block, sizes);
7123            if let Some(e) = else_stmt {
7124                scoped(e, next, def_block, sizes);
7125            }
7126        }
7127        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => {
7128            scoped(body, next, def_block, sizes)
7129        }
7130        Stmt::For { init, body, .. } => {
7131            // A for-init declaration scopes over the WHOLE for statement
7132            // (header + body): one shared id, the body a nested one.
7133            let id = *next;
7134            *next += 1;
7135            sizes.push(0);
7136            for x in init {
7137                scope_pass_a(x, id, next, def_block, sizes);
7138            }
7139            scoped(body, next, def_block, sizes);
7140            sizes[id as usize] = *next - id;
7141        }
7142        Stmt::ForEach { body, .. } => scoped(body, next, def_block, sizes),
7143        Stmt::Switch { cases, default, .. } => {
7144            for c in cases {
7145                // One scope per case group (the emitter braces each);
7146                // non-Block statements share the case id, nested Blocks
7147                // get sub-ids — siblings in one brace group must share,
7148                // or a def's later same-case reads fall "outside".
7149                let id = *next;
7150                *next += 1;
7151                sizes.push(0);
7152                for x in &c.body {
7153                    if matches!(x, Stmt::Block(_)) {
7154                        scoped(x, next, def_block, sizes);
7155                    } else {
7156                        scope_pass_a(x, id, next, def_block, sizes);
7157                    }
7158                }
7159                sizes[id as usize] = *next - id;
7160            }
7161            if let Some(d) = default {
7162                scoped(d, next, def_block, sizes);
7163            }
7164        }
7165        Stmt::Try {
7166            body,
7167            catches,
7168            finally,
7169        } => {
7170            scoped(body, next, def_block, sizes);
7171            for c in catches {
7172                scoped(&c.body, next, def_block, sizes);
7173            }
7174            if let Some(f) = finally {
7175                scoped(f, next, def_block, sizes);
7176            }
7177        }
7178        Stmt::TryWithResources {
7179            resources,
7180            body,
7181            catches,
7182            finally,
7183        } => {
7184            // Resource declarations scope over the WHOLE try statement.
7185            let id = *next;
7186            *next += 1;
7187            sizes.push(0);
7188            for r in resources {
7189                scope_pass_a(r, id, next, def_block, sizes);
7190            }
7191            scoped(body, next, def_block, sizes);
7192            for c in catches {
7193                scoped(&c.body, next, def_block, sizes);
7194            }
7195            if let Some(f) = finally {
7196                scoped(f, next, def_block, sizes);
7197            }
7198            sizes[id as usize] = *next - id;
7199        }
7200        Stmt::Synchronized { body, .. } => scoped(body, next, def_block, sizes),
7201        Stmt::Labeled { body, .. } => scope_pass_a(body, cur, next, def_block, sizes),
7202        _ => {}
7203    }
7204}
7205
7206/// Pass B (same numbering walk): count each var's occurrences overall
7207/// and within its defining block's subtree. For/TWR take dedicated arms
7208/// so their header expressions count INSIDE the statement scope (a
7209/// normal `for (int i…)` must not look like an outside read of i).
7210fn scope_pass_b(
7211    st: &Stmt,
7212    cur: u32,
7213    next: &mut u32,
7214    def_block: &[u32],
7215    sizes: &[u32],
7216    within: &mut [u32],
7217    total: &mut [u32],
7218) {
7219    fn cnt(
7220        v: u32,
7221        at: u32,
7222        def_block: &[u32],
7223        sizes: &[u32],
7224        within: &mut [u32],
7225        total: &mut [u32],
7226    ) {
7227        let i = v as usize;
7228        if i < total.len() {
7229            total[i] += 1;
7230            let d = def_block[i];
7231            if d != u32::MAX && d <= at && at < d + sizes[d as usize] {
7232                within[i] += 1;
7233            }
7234        }
7235    }
7236    fn scoped(
7237        c: &Stmt,
7238        next: &mut u32,
7239        def_block: &[u32],
7240        sizes: &[u32],
7241        within: &mut [u32],
7242        total: &mut [u32],
7243    ) {
7244        let id = *next;
7245        *next += 1;
7246        scope_pass_b(c, id, next, def_block, sizes, within, total);
7247    }
7248    fn ex_at(
7249        e: &Expr,
7250        at: u32,
7251        def_block: &[u32],
7252        sizes: &[u32],
7253        within: &mut [u32],
7254        total: &mut [u32],
7255    ) {
7256        visit_exprs(e, &mut |x| {
7257            if let Expr::Local { var, .. } = x {
7258                cnt(*var, at, def_block, sizes, within, total);
7259            }
7260        });
7261    }
7262    match st {
7263        Stmt::For {
7264            init,
7265            cond,
7266            update,
7267            body,
7268        } => {
7269            let id = *next;
7270            *next += 1;
7271            for x in init {
7272                scope_pass_b(x, id, next, def_block, sizes, within, total);
7273            }
7274            if let Some(c) = cond {
7275                ex_at(c, id, def_block, sizes, within, total);
7276            }
7277            for u in update {
7278                ex_at(u, id, def_block, sizes, within, total);
7279            }
7280            scoped(body, next, def_block, sizes, within, total);
7281        }
7282        Stmt::TryWithResources {
7283            resources,
7284            body,
7285            catches,
7286            finally,
7287        } => {
7288            let id = *next;
7289            *next += 1;
7290            for r in resources {
7291                scope_pass_b(r, id, next, def_block, sizes, within, total);
7292            }
7293            scoped(body, next, def_block, sizes, within, total);
7294            for c in catches {
7295                scoped(&c.body, next, def_block, sizes, within, total);
7296            }
7297            if let Some(f) = finally {
7298                scoped(f, next, def_block, sizes, within, total);
7299            }
7300        }
7301        _ => {
7302            stmt_shallow_vars(st, &mut |v| cnt(v, cur, def_block, sizes, within, total));
7303            match st {
7304                Stmt::Block(v) => {
7305                    for x in v {
7306                        scope_pass_b(x, cur, next, def_block, sizes, within, total);
7307                    }
7308                }
7309                Stmt::If {
7310                    then_stmt,
7311                    else_stmt,
7312                    ..
7313                } => {
7314                    scoped(then_stmt, next, def_block, sizes, within, total);
7315                    if let Some(e) = else_stmt {
7316                        scoped(e, next, def_block, sizes, within, total);
7317                    }
7318                }
7319                Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => {
7320                    scoped(body, next, def_block, sizes, within, total)
7321                }
7322                Stmt::ForEach { body, .. } => {
7323                    scoped(body, next, def_block, sizes, within, total)
7324                }
7325                Stmt::Switch { cases, default, .. } => {
7326                    for c in cases {
7327                        let id = *next;
7328                        *next += 1;
7329                        for x in &c.body {
7330                            if matches!(x, Stmt::Block(_)) {
7331                                scoped(x, next, def_block, sizes, within, total);
7332                            } else {
7333                                scope_pass_b(x, id, next, def_block, sizes, within, total);
7334                            }
7335                        }
7336                    }
7337                    if let Some(d) = default {
7338                        scoped(d, next, def_block, sizes, within, total);
7339                    }
7340                }
7341                Stmt::Try {
7342                    body,
7343                    catches,
7344                    finally,
7345                } => {
7346                    scoped(body, next, def_block, sizes, within, total);
7347                    for c in catches {
7348                        scoped(&c.body, next, def_block, sizes, within, total);
7349                    }
7350                    if let Some(f) = finally {
7351                        scoped(f, next, def_block, sizes, within, total);
7352                    }
7353                }
7354                Stmt::Synchronized { body, .. } => {
7355                    scoped(body, next, def_block, sizes, within, total)
7356                }
7357                Stmt::Labeled { body, .. } => {
7358                    scope_pass_b(body, cur, next, def_block, sizes, within, total)
7359                }
7360                _ => {}
7361            }
7362        }
7363    }
7364}
7365
7366/// Declaration hygiene: every used var that has no LocalDef gets a bare
7367/// declaration at the top; duplicate LocalDefs demote to assignments.
7368/// In an enum's `<clinit>`, every constant's assignment
7369/// (`Self.FIELD = new Self("NAME", ordinal, ...)`) is compiler-mandated
7370/// boilerplate — when the class renders as a true `enum` declaration the
7371/// constants live in the header and these assignments must go. Only
7372/// ACC_ENUM-flagged static fields of the class itself are touched; the
7373/// `$VALUES` array assignment stays (a static block in an enum is legal).
7374pub fn strip_enum_const_inits(s: &mut Stmt, class: &crate::PoolClass) {
7375    let const_fields: jdc_core::FxHashSet<&str> = class
7376        .static_fields
7377        .iter()
7378        .filter(|f| f.access & crate::access::ACC_ENUM != 0)
7379        .map(|f| f.name.as_str())
7380        .collect();
7381    if const_fields.is_empty() {
7382        return;
7383    }
7384    if let Stmt::Block(stmts) = s {
7385        stmts.retain(|st| {
7386            !matches!(
7387                st,
7388                Stmt::ExprStmt(Expr::Assign { target, value, .. })
7389                    if matches!(&**target,
7390                        Expr::Field { cls, name, is_static: true, .. }
7391                            if cls.as_ref() == class.name
7392                                && const_fields.contains(&**name))
7393                        && matches!(&**value, Expr::New { cls: ncls, .. } if ncls.as_ref() == class.name)
7394            )
7395        });
7396    }
7397}
7398
7399/// Java requires the `super(...)`/`this(...)` delegation to be the FIRST
7400/// statement of a constructor. d8/R8 order the outer-reference capture
7401/// (`this.b = p1;`) or other field writes before the invokesuper in the
7402/// bytecode, which lifts as-is into an uncompilable statement order —
7403/// hoist the bare delegation call to position 0 (jadx does the same).
7404/// Does the expression touch `this` (explicit, implicit-instance member
7405/// access, or super)? Such a def cannot be inlined into a delegation
7406/// that must run BEFORE the supertype constructor.
7407fn contains_this_access(e: &Expr) -> bool {
7408    let mut hit = false;
7409    visit_exprs(e, &mut |x| {
7410        if hit {
7411            return;
7412        }
7413        match x {
7414            Expr::This => hit = true,
7415            Expr::Field { owner, is_static: false, .. } => {
7416                if owner.is_none() || matches!(&**owner.as_ref().unwrap(), Expr::This) {
7417                    hit = true;
7418                }
7419            }
7420            Expr::Method { owner, is_static: false, .. } => {
7421                if owner.is_none() || matches!(&**owner.as_ref().unwrap(), Expr::This) {
7422                    hit = true;
7423                }
7424            }
7425            _ => {}
7426        }
7427    });
7428    hit
7429}
7430
7431/// Inline the transitive straight-line defs of a delegation call's
7432/// arg locals into the call, so hoisting it to position 0 leaves no
7433/// dangling name (lark ProtoAdapter's `super(str)` with `str` defined
7434/// BELOW the hoist point — 找不到符号 变量 str). Returns false when any
7435/// arg var has no inlineable def (param-less, missing, this-touching,
7436/// or cyclic) — the caller then leaves the call in place: a
7437/// not-first-statement error is cheaper than an unresolved name that
7438/// poisons every downstream type.
7439/// Any occurrence of `var` (read OR assign-target) in `stmts` — the
7440/// suffix-use test for dropping a consumed def: count_locals_stmts only
7441/// sees reads, and a suffix WRITE to a dropped declaration makes
7442/// ensure_declared re-add `Type v;` at the method top — BEFORE the
7443/// hoisted this()/super() ("对this的调用必须是构造器中的第一个语句",
7444/// lark +587 the read-only test caused).
7445fn var_occurs_in(stmts: &[Stmt], var: u32) -> bool {
7446    stmts.iter().any(|st| {
7447        let mut hit = false;
7448        visit_stmt_exprs_ro(st, &mut |e| {
7449            if !hit {
7450                visit_exprs(e, &mut |x| {
7451                    if let Expr::Local { var: v, .. } = x {
7452                        if *v == var {
7453                            hit = true;
7454                        }
7455                    }
7456                });
7457            }
7458        });
7459        hit
7460    })
7461}
7462
7463// ---------------------------------------------------------------------------
7464// Branched-delegation ctors → static resolver helper.
7465// ---------------------------------------------------------------------------
7466
7467/// A synthetic static method extracted from a constructor whose
7468/// delegation is preceded by computation (the Kotlin default-arg /
7469/// conditional-bridge shape). Rendered by classdec after the real
7470/// methods; the ctor becomes a single first-statement delegation whose
7471/// carrier arg is the helper call.
7472pub struct CtorHelper {
7473    pub name: String,
7474    pub ret: JavaType,
7475    pub params: Vec<(JavaType, String)>,
7476    pub body: Stmt,
7477    pub vt: VarTable,
7478}
7479
7480fn ctor_helpers() -> &'static std::sync::Mutex<std::collections::HashMap<String, Vec<CtorHelper>>> {
7481    static H: std::sync::OnceLock<std::sync::Mutex<std::collections::HashMap<String, Vec<CtorHelper>>>> =
7482        std::sync::OnceLock::new();
7483    H.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::default()))
7484}
7485
7486/// Synthetic relay ctor for the FULL mode of
7487/// extract_branched_delegation_helper: `private C(Object[] h$relay) {
7488/// this/super((T0) h$relay[0], ..); }` — the public ctor becomes
7489/// `this(resolve$X(params))`, both delegations first-statement legal.
7490pub struct CtorRelay {
7491    /// The delegation target's formal types (the casts of the unpack).
7492    pub formals: Vec<JavaType>,
7493    pub is_super: bool,
7494}
7495
7496type RelayMap = std::collections::HashMap<String, Vec<(String, CtorRelay)>>;
7497
7498fn ctor_relays() -> &'static std::sync::Mutex<RelayMap> {
7499    static H: std::sync::OnceLock<std::sync::Mutex<RelayMap>> = std::sync::OnceLock::new();
7500    H.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::default()))
7501}
7502
7503/// Take (and clear) the relay ctors registered for `class` — sorted by
7504/// the originating ctor descriptor for deterministic emission.
7505pub fn take_ctor_relays(class: &str) -> Vec<(String, CtorRelay)> {
7506    let mut m = ctor_relays().lock().unwrap_or_else(|e| e.into_inner());
7507    let mut v = m.remove(class).unwrap_or_default();
7508    v.sort_by(|a, b| a.0.cmp(&b.0));
7509    v
7510}
7511
7512fn array_init(args: Vec<Expr>) -> Expr {
7513    Expr::NewArray {
7514        elem: TypeRef::J(JavaType::Object(std::sync::Arc::from("java/lang/Object"))),
7515        dims: Vec::new(),
7516        trailing_dims: 0,
7517        init: Some(args),
7518    }
7519}
7520
7521/// Deterministic helper name: FNV-1a over (class, ctor descriptor) —
7522/// worker-thread registration order must not leak into the output.
7523fn helper_name(class: &str, desc: &str) -> String {
7524    let mut h: u64 = 0xcbf29ce484222325;
7525    for b in class.as_bytes().iter().chain(b"::").chain(desc.as_bytes()) {
7526        h ^= *b as u64;
7527        h = h.wrapping_mul(0x100000001b3);
7528    }
7529    format!("resolve${:x}", (h >> 16) & 0xffff_ffff)
7530}
7531
7532/// Drain the helpers registered for a class (render-time). Sorted by
7533/// name: registration order is worker-scheduling dependent.
7534pub fn take_ctor_helpers(class: &str) -> Vec<CtorHelper> {
7535    let mut m = ctor_helpers().lock().unwrap_or_else(|e| e.into_inner());
7536    let mut v = m.remove(class).unwrap_or_default();
7537    v.sort_by(|a, b| a.name.cmp(&b.name));
7538    v
7539}
7540
7541fn strip_delegations_deep(stmts: &mut Vec<Stmt>) {
7542    stmts.retain(|s| !is_bare_ctor_call(s));
7543    for s in stmts.iter_mut() {
7544        strip_delegation_one(s);
7545    }
7546}
7547
7548fn strip_delegation_one(st: &mut Stmt) {
7549    if is_bare_ctor_call(st) {
7550        *st = Stmt::Block(Vec::new());
7551        return;
7552    }
7553    match st {
7554        Stmt::Block(v) => strip_delegations_deep(v),
7555        Stmt::If { then_stmt, else_stmt, .. } => {
7556            strip_delegation_one(then_stmt);
7557            if let Some(e) = else_stmt {
7558                strip_delegation_one(e);
7559            }
7560        }
7561        Stmt::While { body, .. }
7562        | Stmt::DoWhile { body, .. }
7563        | Stmt::ForEach { body, .. }
7564        | Stmt::Synchronized { body, .. }
7565        | Stmt::Labeled { body, .. } => strip_delegation_one(body),
7566        Stmt::For { init, body, .. } => {
7567            strip_delegations_deep(init);
7568            strip_delegation_one(body);
7569        }
7570        Stmt::Switch { cases, default, .. } => {
7571            for c in cases.iter_mut() {
7572                strip_delegations_deep(&mut c.body);
7573            }
7574            if let Some(d) = default {
7575                strip_delegation_one(d);
7576            }
7577        }
7578        Stmt::Try { body, catches, finally } | Stmt::TryWithResources { body, catches, finally, .. } => {
7579            strip_delegation_one(body);
7580            for c in catches.iter_mut() {
7581                strip_delegation_one(&mut c.body);
7582            }
7583            if let Some(f) = finally {
7584                strip_delegation_one(f);
7585            }
7586        }
7587        _ => {}
7588    }
7589}
7590
7591#[allow(clippy::ptr_arg)]
7592fn rewrite_bare_returns(stmts: &mut Vec<Stmt>, val: &Expr) {
7593    for s in stmts.iter_mut() {
7594        rewrite_bare_returns_one(s, val);
7595    }
7596}
7597
7598fn rewrite_bare_returns_one(st: &mut Stmt, val: &Expr) {
7599    match st {
7600        Stmt::Return(None) => *st = Stmt::Return(Some(val.clone())),
7601        Stmt::Block(v) => rewrite_bare_returns(v, val),
7602        Stmt::If { then_stmt, else_stmt, .. } => {
7603            rewrite_bare_returns_one(then_stmt, val);
7604            if let Some(e) = else_stmt {
7605                rewrite_bare_returns_one(e, val);
7606            }
7607        }
7608        Stmt::While { body, .. }
7609        | Stmt::DoWhile { body, .. }
7610        | Stmt::ForEach { body, .. }
7611        | Stmt::Synchronized { body, .. }
7612        | Stmt::Labeled { body, .. } => rewrite_bare_returns_one(body, val),
7613        Stmt::For { init, body, .. } => {
7614            rewrite_bare_returns(init, val);
7615            rewrite_bare_returns_one(body, val);
7616        }
7617        Stmt::Switch { cases, default, .. } => {
7618            for c in cases.iter_mut() {
7619                rewrite_bare_returns(&mut c.body, val);
7620            }
7621            if let Some(d) = default {
7622                rewrite_bare_returns_one(d, val);
7623            }
7624        }
7625        Stmt::Try { body, catches, finally } | Stmt::TryWithResources { body, catches, finally, .. } => {
7626            rewrite_bare_returns_one(body, val);
7627            for c in catches.iter_mut() {
7628                rewrite_bare_returns_one(&mut c.body, val);
7629            }
7630            if let Some(f) = finally {
7631                rewrite_bare_returns_one(f, val);
7632            }
7633        }
7634        _ => {}
7635    }
7636}
7637
7638fn collect_def_vars(stmts: &[Stmt], out: &mut jdc_core::FxHashSet<u32>) {
7639    for st in stmts {
7640        match st {
7641            Stmt::LocalDef { var, .. } => {
7642                out.insert(*var);
7643            }
7644            Stmt::ExprStmt(Expr::Assign { target, .. }) => {
7645                if let Expr::Local { var, .. } = &**target {
7646                    out.insert(*var);
7647                }
7648            }
7649            Stmt::Block(v) => collect_def_vars(v, out),
7650            Stmt::If { then_stmt, else_stmt, .. } => {
7651                collect_def_vars(std::slice::from_ref(then_stmt.as_ref()), out);
7652                if let Some(e) = else_stmt {
7653                    collect_def_vars(std::slice::from_ref(e.as_ref()), out);
7654                }
7655            }
7656            Stmt::While { body, .. }
7657            | Stmt::DoWhile { body, .. }
7658            | Stmt::Synchronized { body, .. }
7659            | Stmt::Labeled { body, .. } => {
7660                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
7661            }
7662            Stmt::ForEach { var, body, .. } => {
7663                out.insert(*var);
7664                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
7665            }
7666            Stmt::For { init, body, .. } => {
7667                collect_def_vars(init, out);
7668                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
7669            }
7670            Stmt::Switch { cases, default, .. } => {
7671                for c in cases {
7672                    collect_def_vars(&c.body, out);
7673                }
7674                if let Some(d) = default {
7675                    collect_def_vars(std::slice::from_ref(d.as_ref()), out);
7676                }
7677            }
7678            Stmt::Try { body, catches, finally }
7679            | Stmt::TryWithResources { body, catches, finally, .. } => {
7680                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
7681                for c in catches {
7682                    out.insert(c.var);
7683                    collect_def_vars(std::slice::from_ref(c.body.as_ref()), out);
7684                }
7685                if let Some(f) = finally {
7686                    collect_def_vars(std::slice::from_ref(f.as_ref()), out);
7687                }
7688            }
7689            _ => {}
7690        }
7691    }
7692}
7693
7694/// v1 extraction is limited to Block/If/straight-line computation (the
7695/// Kotlin default-arg shape); loops/switch/try in the extraction bail.
7696fn extraction_shape_ok(stmts: &[Stmt]) -> bool {
7697    stmts.iter().all(|st| match st {
7698        Stmt::Block(v) => extraction_shape_ok(v),
7699        Stmt::LocalDef { .. }
7700        | Stmt::ExprStmt(_)
7701        | Stmt::Return(_)
7702        | Stmt::Throw(_)
7703        | Stmt::Assert { .. }
7704        | Stmt::Break(_)
7705        | Stmt::Continue(_) => true,
7706        Stmt::If { then_stmt, else_stmt, .. } => {
7707            ok_one(then_stmt)
7708                && else_stmt.as_deref().map(ok_one).unwrap_or(true)
7709        }
7710        Stmt::While { body, .. }
7711        | Stmt::DoWhile { body, .. }
7712        | Stmt::ForEach { body, .. }
7713        | Stmt::Synchronized { body, .. } => ok_one(body),
7714        Stmt::Labeled { body, .. } => ok_one(body),
7715        Stmt::For { init, body, .. } => extraction_shape_ok(init) && ok_one(body),
7716        Stmt::Switch { cases, default, .. } => {
7717            cases.iter().all(|c| extraction_shape_ok(&c.body))
7718                && default.as_deref().map(ok_one).unwrap_or(true)
7719        }
7720        Stmt::Try { body, catches, finally }
7721        | Stmt::TryWithResources { body, catches, finally, .. } => {
7722            ok_one(body)
7723                && catches.iter().all(|c| ok_one(&c.body))
7724                && finally.as_deref().map(ok_one).unwrap_or(true)
7725        }
7726        // Label/Goto (unstructured), MonitorEnter/Exit (raw), ClassDecl
7727        // (local class), TernaryValue, Raw: stay out of v2 extraction.
7728        _ => false,
7729    })
7730}
7731
7732fn ok_one(st: &Stmt) -> bool {
7733    extraction_shape_ok(std::slice::from_ref(st))
7734}
7735
7736fn static_safe(stmts: &[Stmt], this_id: Option<u32>) -> bool {
7737    let mut ok = true;
7738    for st in stmts {
7739        visit_stmt_exprs_ro(st, &mut |e| {
7740            visit_exprs(e, &mut |x| {
7741                match x {
7742                    Expr::This => ok = false,
7743                    // ddc's IR usually carries `this` as the var-0
7744                    // PARAM local, not the Expr::This variant — a
7745                    // helper body reading it dangles (the this param
7746                    // is excluded from the helper's param list) AND
7747                    // renders 无法从静态上下文中引用非静态 (weixin y9/i).
7748                    Expr::Local { var, .. } => {
7749                        if Some(*var) == this_id {
7750                            ok = false;
7751                        }
7752                    }
7753                    // implicit-this member access / call
7754                    Expr::Field { owner: None, is_static: false, .. } => ok = false,
7755                    Expr::Method { owner: None, is_static: false, .. } => ok = false,
7756                    _ => {}
7757                }
7758            });
7759        });
7760    }
7761    ok
7762}
7763
7764/// The bytedance PatchProxy hotfix guard in <clinit>:
7765/// `if (vsChange != null) { PatchProxyResult p = PatchProxy.proxy(..);
7766/// if (p.isSupported) { return; } }` — a bare `return;` inside a STATIC
7767/// INITIALIZER is illegal Java (javac: 返回外部方法; videocut ×28,029 +
7768/// news_article ×19,058 — sampled 400/400 this shape). The patch path
7769/// is dead in a fresh compile (the IVsChange field is null), so the
7770/// whole leading guard statement strips, leaving the real static inits —
7771/// the <clinit> counterpart of the Titan/Robust ctor-guard strips.
7772/// Gate: FIRST top-level stmt is an If with no else whose body holds
7773/// both a bare return and a *PatchProxy* call.
7774pub fn strip_clinit_hotfix_guard(body: &mut Stmt) {
7775    // The structurer can wrap the sequence in a nested singleton block
7776    // (the enum-clinit path flattens for the same reason); the normal
7777    // emit_method path does not.
7778    if let Stmt::Block(vs) = body {
7779        flatten_top_blocks(vs);
7780    }
7781    let Stmt::Block(stmts) = body else { return };
7782    // The guard may sit behind hoisted bare declarations (`int[] v8x;`
7783    // from ensure_declared) — find the first non-decl statement.
7784    let mut gi = 0usize;
7785    while gi < stmts.len() && matches!(&stmts[gi], Stmt::LocalDef { init: None, .. }) {
7786        gi += 1;
7787    }
7788    let Some(first) = stmts.get(gi) else { return };
7789    let Stmt::If { then_stmt, else_stmt, .. } = first else {
7790        return;
7791    };
7792    if else_stmt.is_some() {
7793        return;
7794    }
7795    let mut has_ret = false;
7796    let mut has_proxy = false;
7797    let c = then_stmt.as_ref();
7798    walk_all(c, &mut |st| {
7799        if matches!(st, Stmt::Return(None)) {
7800            has_ret = true;
7801        }
7802    });
7803    let mut cc = c.clone();
7804    walk_stmt_exprs(&mut cc, &mut |e| {
7805        deep_rewrite(e, &mut |x| {
7806            if let Expr::Method { cls, .. } = x {
7807                if cls.contains("PatchProxy") {
7808                    has_proxy = true;
7809                }
7810            }
7811        });
7812    });
7813    if has_ret && has_proxy {
7814        stmts.remove(gi);
7815    }
7816}
7817
7818/// Vendor hotfix guards in CTORS that wrap a delegation:
7819/// `if (__ != null) { Object[] v = ..; ConstructorCode proxy =
7820/// ConstructorCode.proxy(..); if (proxy != null) { this(unpack(v));
7821/// proxy.afterSuper(this); return; } }` ahead of the real top-level
7822/// delegation (alipay InstantRun — ctor-not-first ×1,436; the same
7823/// shape as the already-stripped Baidu Titan / bytedance clinit /
7824/// Meituan Robust guards). The patch path is dead in a fresh compile
7825/// (the redirect field is null), and Java cannot place the wrapped
7826/// delegation legally — strip the WHOLE guard statement.
7827///
7828/// VENDOR-GATED on purpose: a conditional delegation with a top-level
7829/// fallback (`if (c) { this(a); return; } this(b);`) is legitimate
7830/// bytecode whose guard must NOT be dropped. The gate: the guard
7831/// subtree mentions a known hotfix framework type (hotfix / instantrun
7832/// / robust / titan / PatchProxy / ChangeQuickRedirect / ConstructorCode
7833/// in any method-owner, field owner+type, cast, or new).
7834/// First LEAF statement after unwrapping nested `Stmt::Block` wrappers —
7835/// the lifted ctor body reaches the ctor chain with the delegation and
7836/// the hotfix guard each wrapped in plain Blocks (the flattening that
7837/// produces the rendered shape runs later).
7838pub(crate) fn first_leaf_stmt(s: &Stmt) -> &Stmt {
7839    match s {
7840        Stmt::Block(v) if !v.is_empty() => first_leaf_stmt(&v[0]),
7841        other => other,
7842    }
7843}
7844
7845/// Vendor hotfix guards in CTORS that wrap a delegation:
7846/// `if (__ != null) { Object[] v = ..; ConstructorCode proxy =
7847/// ConstructorCode.proxy(..); if (proxy != null) { this(unpack(v));
7848/// proxy.afterSuper(this); return; } }` ahead of the real top-level
7849/// delegation (alipay InstantRun — ctor-not-first ×1,436, nearly all in
7850/// FALLBACK ENUM ctors; the same shape as the already-stripped Baidu
7851/// Titan / bytedance clinit / Meituan Robust guards). The patch path is
7852/// dead in a fresh compile (the redirect field is null), and Java cannot
7853/// place the wrapped delegation legally — strip the WHOLE guard
7854/// statement (at this pipeline stage it arrives Block-wrapped, and its
7855/// branches are still un-inverted: `If(then=∅, else=guard-body)`).
7856///
7857/// VENDOR-GATED on purpose: a conditional delegation with a top-level
7858/// fallback (`if (c) { this(a); return; } this(b);`) is legitimate
7859/// bytecode whose guard must NOT be dropped. The gate: the guard
7860/// subtree mentions a known hotfix framework type (hotfix / instantrun
7861/// / robust / titan / PatchProxy / ChangeQuickRedirect / ConstructorCode
7862/// in any method-owner, field owner+type, cast, or new).
7863pub fn strip_ctor_hotfix_guards(body: &mut Stmt) {
7864    fn marker_hit(e: &Expr) -> bool {
7865        let has = |n: &str| {
7866            let l = n.to_ascii_lowercase();
7867            l.contains("hotfix")
7868                || l.contains("instantrun")
7869                || l.contains("robust")
7870                || l.contains("titan")
7871                || l.contains("patchproxy")
7872                || l.contains("changequickredirect")
7873                || l.contains("constructorcode")
7874        };
7875        let mut hit = false;
7876        visit_exprs(e, &mut |x| {
7877            match x {
7878                Expr::Method { cls, .. } | Expr::New { cls, .. } | Expr::Field { cls, .. } => {
7879                    if has(cls) {
7880                        hit = true;
7881                    }
7882                }
7883                _ => {}
7884            }
7885            if let Expr::Field { ty, .. } = x {
7886                if let TypeRef::J(JavaType::Object(n)) = ty {
7887                    if has(n) {
7888                        hit = true;
7889                    }
7890                }
7891            }
7892        });
7893        hit
7894    }
7895    // A top-level statement that IS a hotfix guard: unwrap single-child
7896    // Block chains down to an If whose subtree holds BOTH a delegation
7897    // and a vendor marker.
7898    fn is_guard(st: &Stmt) -> bool {
7899        let mut cur = st;
7900        loop {
7901            match cur {
7902                Stmt::Block(v) if v.len() == 1 => cur = &v[0],
7903                Stmt::If { .. } => break,
7904                _ => return false,
7905            }
7906        }
7907        let mut has_del = false;
7908        walk_all(cur, &mut |s2| {
7909            if is_bare_ctor_call(s2) {
7910                has_del = true;
7911            }
7912        });
7913        if !has_del {
7914            return false;
7915        }
7916        let mut has_marker = false;
7917        visit_stmt_exprs_ro(cur, &mut |e| {
7918            if marker_hit(e) {
7919                has_marker = true;
7920            }
7921        });
7922        has_marker
7923    }
7924    // Strip at EVERY Block level: the lifted body arrives either as
7925    // [Block(guard), Block(del, ..)] (AtomicDataCollector) or as ONE big
7926    // Block [decls, guard, prelude-computes, del] (compose TextStyle).
7927    // No outside-delegation requirement: the vendor gate already proves
7928    // the statement is dead hotfix machinery (the redirect field is null
7929    // in a fresh compile — the same assumption the Titan/PatchProxy
7930    // strips run on), and the dex's fresh path IS whatever remains after
7931    // the guard. rpc/a.java: the real super() sits inside a switch arm —
7932    // requiring an unconditional outside delegation kept the guard (and
7933    // its illegal pre-super field writes) alive.
7934    fn strip_in(stmts: &mut Vec<Stmt>) {
7935        stmts.retain(|st| !is_guard(st));
7936        for st in stmts.iter_mut() {
7937            if let Stmt::Block(v) = st {
7938                strip_in(v);
7939            }
7940        }
7941    }
7942    let Stmt::Block(stmts) = body else { return };
7943    strip_in(stmts);
7944}
7945
7946/// Repeated identical branch-site delegations with param-only args
7947/// (weixin appbrand/zc: bare `super()` at four branch ends, each
7948/// followed by field-write tails) carry no computed carrier, so the
7949/// helper extraction has nothing to return — but they need no helper:
7950/// every delegating path runs the SAME call, so hoist one copy to
7951/// position 0 and strip the sites. The per-branch tails stay in their
7952/// branches and now legally follow the first-statement delegation.
7953/// Same reordering approximation fix_ctor_super_first already ships for
7954/// the straight-line case (paths that threw BEFORE delegating now run
7955/// the delegation first — Object-super, the dominant shape, is inert).
7956/// Gate: every del deep-equal and reading params only (a computed local
7957/// would dangle at position 0 — that is the helper extractor's job).
7958pub fn hoist_branch_delegations(body: &mut Stmt, vt: &VarTable) {
7959    let Stmt::Block(stmts) = body else { return };
7960    if stmts.first().is_some_and(is_bare_ctor_call) {
7961        return; // already first
7962    }
7963    let mut dels: Vec<Expr> = Vec::new();
7964    for st in stmts.iter() {
7965        visit_stmt_exprs_ro(st, &mut |e| {
7966            if is_delegation_expr(e) {
7967                dels.push(e.clone());
7968            }
7969        });
7970    }
7971    if dels.is_empty() {
7972        return;
7973    }
7974    if !dels.iter().all(|d| *d == dels[0]) {
7975        return;
7976    }
7977    let mut bad = false;
7978    visit_exprs(&dels[0], &mut |x| {
7979        if let Expr::Local { var, .. } = x {
7980            if (*var as usize) >= vt.vars.len() || !vt.vars[*var as usize].is_param {
7981                bad = true;
7982            }
7983        }
7984    });
7985    if bad {
7986        return;
7987    }
7988    let del = dels[0].clone();
7989    strip_delegations_deep(stmts);
7990    stmts.insert(0, Stmt::ExprStmt(del));
7991}
7992
7993/// The branched-delegation ctor family ("对this的调用必须是构造器中的
7994/// 第一个语句", ~2k across corpora): R8 renders a Kotlin default-arg /
7995/// conditional-bridge ctor as computation statements (straight-line plus
7996/// mask-guarded overrides) with every control path ending in a
7997/// `this(a, b, carrier)` delegation. Java cannot express statements
7998/// before the delegation, and unlike the linear-chain case the carrier's
7999/// value is computed by multi-statement branched code that cannot fold
8000/// into a ternary. Extract the computation into a synthetic
8001/// `private static T resolve$X(<ctor params>)` and make the ctor a
8002/// single first-statement `this(a, b, resolve$X(..))` — compilable AND
8003/// faithful (jadx renders the un-compilable statements-before-this form
8004/// for this family).
8005///
8006/// Two carrier modes:
8007/// - Single (v1): every delegation textually identical and exactly one
8008///   non-param local (the carrier) read exactly once across the args.
8009/// - Position (v2): delegations differ at exactly ONE arg position k,
8010///   every site passing a bare non-param local there (per-branch
8011///   carriers, lark BaseProtocol$ReliablePushList's v22_g1/v22x). Each
8012///   stripped delegation's own carrier fills the return that follows it;
8013///   merge-point returns fall back to the primary site's carrier.
8014///
8015/// The top-level primary delegation is optional in BOTH modes — when
8016/// every path delegates inside branches (weixin ln1/b1's super() in
8017/// if/try/catch) the whole body is extracted and any del serves as the
8018/// template (all non-k parts identical).
8019///
8020/// Shared gates (all conservative, bail keeps today's behavior): not an
8021/// enum (trace-param strip shifts render params); nested only when the
8022/// class RENDERS static-nested (member-inner ctors lose the outer param
8023/// at render); computation is Block/If/straight-line only; no
8024/// `this`-dependence; no local defined in the extraction used after the
8025/// primary.
8026pub fn extract_branched_delegation_helper(
8027    body: &mut Stmt,
8028    vt: &VarTable,
8029    pool: &DexPool,
8030    class: &crate::PoolClass,
8031    desc_str: &str,
8032) {
8033    if class.is_enum() {
8034        return;
8035    }
8036    // Member-inner ctors lose the synthetic outer param at render
8037    // (qualified-new absorbs it), so the helper's param list would no
8038    // longer match the rendered ctor. STATIC nested classes render all
8039    // params like top-level ones (lark BaseProtocol$PushList family).
8040    if class.nesting.enclosing_class.is_some() && !crate::ctx::nested_is_static(pool, class) {
8041        return;
8042    }
8043    let Stmt::Block(stmts) = body else { return };
8044    let mut dels: Vec<Expr> = Vec::new();
8045    for st in stmts.iter() {
8046        visit_stmt_exprs_ro(st, &mut |e| {
8047            if is_delegation_expr(e) {
8048                dels.push(e.clone());
8049            }
8050        });
8051    }
8052    if dels.is_empty() {
8053        return;
8054    }
8055    if dels.len() == 1 && stmts.first().is_some_and(is_bare_ctor_call) {
8056        return; // already first
8057    }
8058    let template = dels[0].clone();
8059    let Expr::Method { args: t_args, .. } = &template else {
8060        return;
8061    };
8062    // Carrier discovery: Single (all dels identical, one non-param local
8063    // read once) or Position (dels differ at exactly one arg slot, every
8064    // site a bare non-param local of one erased type).
8065    enum Mode {
8066        Single(u32),
8067        Position(usize),
8068        /// Every path delegates with the SAME target signature but its
8069        /// own arg expressions: the whole computation moves to the
8070        /// helper (each delegation becomes `return new Object[]{its
8071        /// args}`), the public ctor becomes `this(resolve$X(params))`
8072        /// and a synthetic `private C(Object[] h)` relay unpacks the
8073        /// carriers into the one legal first-statement delegation.
8074        Full,
8075    }
8076    // Full-mode gate: uniform delegation target (this/super, class,
8077    // descriptor), non-empty args, no pre-existing (Object[]) ctor,
8078    // and — the relay signature is per-CLASS unique — only the
8079    // lexicographically least <init> descriptor of the class may take
8080    // the relay (deterministic across parallel workers; a second
8081    // branched ctor in one class keeps its honest error).
8082    let full_gate = (|| -> Option<()> {
8083        let Expr::Method {
8084            args: a0,
8085            desc: d0,
8086            is_super: s0,
8087            cls: c0,
8088            ..
8089        } = &dels[0]
8090        else {
8091            return None;
8092        };
8093        if a0.is_empty() {
8094            return None;
8095        }
8096        for d in &dels[1..] {
8097            match d {
8098                Expr::Method {
8099                    args,
8100                    desc,
8101                    is_super,
8102                    cls,
8103                    ..
8104                } if args.len() == a0.len()
8105                    && **desc == **d0
8106                    && is_super == s0
8107                    && cls == c0 => {}
8108                _ => return None,
8109            }
8110        }
8111        if class
8112            .all_methods()
8113            .any(|m| &*m.name == "<init>" && &*m.desc == "([Ljava/lang/Object;)V")
8114        {
8115            return None;
8116        }
8117        Some(())
8118    })()
8119    .is_some();
8120    let mode = if dels.iter().all(|d| *d == dels[0]) {
8121        let mut carrier: Option<u32> = None;
8122        let mut reads = 0usize;
8123        let mut multi = false;
8124        for a in t_args.iter() {
8125            let c = a.clone();
8126            visit_exprs(&c, &mut |x| {
8127                if let Expr::Local { var, .. } = x {
8128                    if (*var as usize) < vt.vars.len() && !vt.vars[*var as usize].is_param {
8129                        reads += 1;
8130                        match carrier {
8131                            Some(v) if v == *var => {}
8132                            Some(_) => multi = true,
8133                            None => carrier = Some(*var),
8134                        }
8135                    }
8136                }
8137            });
8138        }
8139        match carrier {
8140            Some(cv) if !multi && reads == 1 => Mode::Single(cv),
8141            _ if full_gate => Mode::Full,
8142            _ => return,
8143        }
8144    } else {
8145        let positioned: Option<Mode> = (|| {
8146        let arity = t_args.len();
8147        // Uniform arity FIRST: a bare `super()` mixed among arg-carrying
8148        // dels made the single-diff slot k index past a short del's args
8149        // (deepseek v52 / kimi c60.o panics, index-out-of-bounds).
8150        if dels
8151            .iter()
8152            .any(|d| !matches!(d, Expr::Method { args, .. } if args.len() == arity))
8153        {
8154            return None;
8155        }
8156        let mut diff: Vec<usize> = Vec::new();
8157        for j in 0..arity {
8158            if dels.iter().any(|d| match d {
8159                Expr::Method { args, .. } => args.get(j) != t_args.get(j),
8160                _ => true,
8161            }) {
8162                diff.push(j);
8163            }
8164        }
8165        if diff.len() != 1 {
8166            return None;
8167        }
8168        let k = diff[0];
8169        // Non-arg parts AND all other arg positions identical: neutralize
8170        // slot k on every del and compare whole exprs (catches mixed
8171        // this()/super() or different targets).
8172        let mut base0 = dels[0].clone();
8173        if let Expr::Method { args, .. } = &mut base0 {
8174            args[k] = Expr::Const(ConstVal::Null);
8175        }
8176        for d in dels.iter() {
8177            let mut c = d.clone();
8178            if let Expr::Method { args, .. } = &mut c {
8179                args[k] = Expr::Const(ConstVal::Null);
8180            }
8181            if c != base0 {
8182                return None;
8183            }
8184        }
8185        // Slot k: bare non-param local at every site, one erased type,
8186        // and the carrier never read at another position.
8187        let mut carrier_ty: Option<JavaType> = None;
8188        for d in dels.iter() {
8189            let Expr::Method { args, .. } = d else {
8190                return None;
8191            };
8192            let Expr::Local { var, .. } = &args[k] else {
8193                return None;
8194            };
8195            if (*var as usize) >= vt.vars.len() || vt.vars[*var as usize].is_param {
8196                return None;
8197            }
8198            let ty = vt.vars[*var as usize].ty.erased();
8199            match &carrier_ty {
8200                Some(t) if *t == ty => {}
8201                Some(_) => return None,
8202                None => carrier_ty = Some(ty),
8203            }
8204            // Non-k positions must be param/const/static-only — a
8205            // computed local there would have its def extracted into
8206            // the helper and the ctor arg list would dangle (lark
8207            // audiosave/i 找不到符号 ×5-per-site). Also covers the
8208            // carrier leaking into another position.
8209            for (j, a) in args.iter().enumerate() {
8210                if j == k {
8211                    continue;
8212                }
8213                let mut bad = false;
8214                visit_exprs(a, &mut |x| {
8215                    if let Expr::Local { var: v2, .. } = x {
8216                        if (*v2 as usize) >= vt.vars.len() || !vt.vars[*v2 as usize].is_param {
8217                            bad = true;
8218                        }
8219                    }
8220                });
8221                if bad {
8222                    return None;
8223                }
8224            }
8225        }
8226        Some(Mode::Position(k))
8227        })();
8228        match positioned {
8229            Some(m) => m,
8230            None if full_gate => Mode::Full,
8231            None => return,
8232        }
8233    };
8234    let l = stmts.iter().rposition(is_bare_ctor_call);
8235    if l == Some(0) {
8236        return;
8237    }
8238    let (mut extracted, mut tail): (Vec<Stmt>, Vec<Stmt>) = match l {
8239        Some(l) => (stmts[..l].to_vec(), stmts[l + 1..].to_vec()),
8240        None => (stmts.clone(), Vec::new()),
8241    };
8242    if !extraction_shape_ok(&extracted) {
8243        return;
8244    }
8245    // Post-delegation tails (field writes / trace stubs between each
8246    // branch delegation and its return) are CTOR work, not helper
8247    // computation — they commonly touch `this`. When every site's tail
8248    // is identical (and equals the top-level continuation when one
8249    // exists) hoist one copy after the merged delegation; otherwise
8250    // they stay in the helper and the static_safe gate below decides.
8251    let mut site_tails: Vec<Vec<Stmt>> = Vec::new();
8252    {
8253        let mut probe = extracted.clone();
8254        site_tails_walk(&mut probe, &mut site_tails, false);
8255    }
8256    let hoist = !site_tails.is_empty() && {
8257        let n0 = normalize_tail(&site_tails[0]);
8258        site_tails.iter().all(|t| normalize_tail(t) == n0)
8259            && (l.is_none() || normalize_tail(&tail) == n0)
8260    };
8261    if hoist {
8262        site_tails_walk(&mut extracted, &mut Vec::new(), true);
8263        if l.is_none() {
8264            tail = normalize_tail(&site_tails[0]);
8265        }
8266    }
8267    // The delegation the ctor keeps: the top-level primary when present,
8268    // else any del (all non-k parts are identical).
8269    let primary_del: Expr = match l {
8270        Some(l) => match &stmts[l] {
8271            Stmt::ExprStmt(e) => e.clone(),
8272            _ => return,
8273        },
8274        None => dels[0].clone(),
8275    };
8276    let ret = match mode {
8277        Mode::Single(cv) => vt.var(cv).ty.erased(),
8278        Mode::Position(k) => match &primary_del {
8279            Expr::Method { args, .. } => match args.get(k) {
8280                Some(Expr::Local { var, .. }) => vt.var(*var).ty.erased(),
8281                _ => return,
8282            },
8283            _ => return,
8284        },
8285        Mode::Full => JavaType::Array(Box::new(JavaType::Object(std::sync::Arc::from(
8286            "java/lang/Object",
8287        )))),
8288    };
8289    match mode {
8290        Mode::Single(cv) => {
8291            strip_delegations_deep(&mut extracted);
8292            let cv_local = Expr::Local {
8293                var: cv,
8294                ty: vt.var(cv).ty.clone(),
8295            };
8296            rewrite_bare_returns(&mut extracted, &cv_local);
8297            if !always_returns(&extracted) {
8298                extracted.push(Stmt::Return(Some(cv_local)));
8299            }
8300        }
8301        Mode::Position(k) => {
8302            // Each stripped delegation's own slot-k carrier fills the
8303            // next bare return in its list; merge-point returns fall
8304            // back to the primary carrier below.
8305            fill_returns_by_position(&mut extracted, &|args: &[Expr]| args.get(k).cloned());
8306            let fallback = match &primary_del {
8307                Expr::Method { args, .. } => args[k].clone(),
8308                _ => return,
8309            };
8310            rewrite_bare_returns(&mut extracted, &fallback);
8311            if !always_returns(&extracted) {
8312                extracted.push(Stmt::Return(Some(fallback)));
8313            }
8314        }
8315        Mode::Full => {
8316            // Every delegation becomes `return new Object[]{its own
8317            // args}`; merge-point bare returns and the fall-through
8318            // completion take the PRIMARY delegation's args (the
8319            // top-level last del when present — its args are
8320            // top-level-scoped by construction). With no primary,
8321            // every path must already delegate explicitly.
8322            fill_returns_by_position(&mut extracted, &|args: &[Expr]| {
8323                Some(array_init(args.to_vec()))
8324            });
8325            if l.is_none() {
8326                // No top-level primary: every path must delegate. A
8327                // dead tail can still dangle after the shortest
8328                // always-returning prefix (DataHolder: an assign after
8329                // a try whose body returns and whose catch throws) —
8330                // truncate it (unreachable by Java's flow model, so
8331                // sound), else bail.
8332                if !always_returns(&extracted) {
8333                    let cut = (1..=extracted.len())
8334                        .find(|&i| always_returns(&extracted[..i]));
8335                    match cut {
8336                        Some(i) => extracted.truncate(i),
8337                        None => return,
8338                    }
8339                }
8340            }
8341            let fallback_args = match &primary_del {
8342                Expr::Method { args, .. } => args.clone(),
8343                _ => return,
8344            };
8345            let fallback = array_init(fallback_args);
8346            rewrite_bare_returns(&mut extracted, &fallback);
8347            if !always_returns(&extracted) {
8348                extracted.push(Stmt::Return(Some(fallback)));
8349            }
8350        }
8351    }
8352    let this_id = vt
8353        .vars
8354        .iter()
8355        .find(|v| v.is_param && v.name == "this")
8356        .map(|v| v.id);
8357    if !static_safe(&extracted, this_id) {
8358        return;
8359    }
8360    let mut defs: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
8361    collect_def_vars(&extracted, &mut defs);
8362    if defs.iter().any(|v| var_occurs_in(&tail, *v)) {
8363        return;
8364    }
8365    let params: Vec<(u32, JavaType, String)> = vt
8366        .vars
8367        .iter()
8368        .filter(|v| v.is_param && v.name != "this")
8369        .map(|v| (v.id, v.ty.erased(), v.name.clone()))
8370        .collect();
8371    let name = helper_name(&class.name, desc_str);
8372    let call = Expr::Method {
8373        owner: None,
8374        cls: std::sync::Arc::from(class.name.as_str()),
8375        name: std::sync::Arc::from(name.as_str()),
8376        desc: std::sync::Arc::new(jdc_core::types::MethodDescriptor {
8377            ret: ret.clone(),
8378            args: params.iter().map(|p| p.1.clone()).collect(),
8379        }),
8380        args: params
8381            .iter()
8382            .map(|(id, ty, _)| Expr::Local {
8383                var: *id,
8384                ty: TypeRef::J(ty.clone()),
8385            })
8386            .collect(),
8387        is_static: true,
8388        is_interface: false,
8389        is_special: false,
8390        is_super: false,
8391        is_dynamic: false,
8392        type_args: Vec::new(),
8393    };
8394    let mut new_del = primary_del.clone();
8395    match mode {
8396        Mode::Single(cv) => {
8397            deep_rewrite(&mut new_del, &mut |x| {
8398                if let Expr::Local { var, .. } = x {
8399                    if *var == cv {
8400                        *x = call.clone();
8401                    }
8402                }
8403            });
8404        }
8405        Mode::Position(k) => {
8406            let Expr::Method { args, .. } = &mut new_del else {
8407                return;
8408            };
8409            if k >= args.len() {
8410                return;
8411            }
8412            args[k] = call.clone();
8413        }
8414        Mode::Full => {
8415            // The public ctor's one legal statement: `this(resolve$X(p..))`
8416            // — the synthetic relay ctor below unpacks the carriers into
8417            // the uniform target delegation.
8418            new_del = Expr::Method {
8419                owner: None,
8420                cls: std::sync::Arc::from(class.name.as_str()),
8421                name: "<init>".into(),
8422                desc: std::sync::Arc::new(jdc_core::types::MethodDescriptor {
8423                    args: vec![JavaType::Array(Box::new(JavaType::Object(
8424                        std::sync::Arc::from("java/lang/Object"),
8425                    )))],
8426                    ret: JavaType::Void,
8427                }),
8428                args: vec![call.clone()],
8429                is_static: false,
8430                is_interface: false,
8431                is_special: true,
8432                is_super: false,
8433                is_dynamic: false,
8434                type_args: Vec::new(),
8435            };
8436        }
8437    }
8438    {
8439        let mut m = ctor_helpers().lock().unwrap_or_else(|e| e.into_inner());
8440        let v = m.entry(class.name.clone()).or_default();
8441        if !v.iter().any(|h| h.name == name) {
8442            // The extracted body skips the late pipeline polish (it left
8443            // the ctor before cleanup ran) — invert empty thens so the
8444            // helper doesn't render `if (..) {} else {..}`.
8445            let mut hbody = Stmt::Block(extracted);
8446            invert_empty_thens(&mut hbody);
8447            v.push(CtorHelper {
8448                name,
8449                ret: ret.clone(),
8450                params: params.into_iter().map(|(_, ty, nm)| (ty, nm)).collect(),
8451                body: hbody,
8452                vt: vt.clone(),
8453            });
8454            if matches!(mode, Mode::Full) {
8455                let Expr::Method {
8456                    desc: d0,
8457                    is_super: s0,
8458                    ..
8459                } = &primary_del
8460                else {
8461                    return;
8462                };
8463                let mut rm = ctor_relays().lock().unwrap_or_else(|e| e.into_inner());
8464                let rv = rm.entry(class.name.clone()).or_default();
8465                if !rv.iter().any(|(d, _)| d == desc_str) {
8466                    rv.push((
8467                        desc_str.to_string(),
8468                        CtorRelay {
8469                            formals: d0.args.clone(),
8470                            is_super: *s0,
8471                        },
8472                    ));
8473                }
8474            }
8475        } else {
8476            return; // duplicate decompile of the same ctor: keep the first
8477        }
8478    }
8479    *stmts = std::iter::once(Stmt::ExprStmt(new_del)).chain(tail).collect();
8480}
8481
8482/// Position-mode return fill: walk each statement list left-to-right; a
8483/// delegation at slot k hands its carrier to the next bare `return;` in
8484/// the SAME list (the bytecode shape is always `this(..); trace..;
8485/// return;`). With no following return in the list the delegation
8486/// becomes the return itself and the rest of the list (trace stubs only
8487/// — a second delegation there is impossible) is dropped as unreachable.
8488fn fill_returns_by_position(
8489    stmts: &mut Vec<Stmt>,
8490    pick: &dyn Fn(&[Expr]) -> Option<Expr>,
8491) {
8492    let mut i = 0;
8493    while i < stmts.len() {
8494        if is_bare_ctor_call(&stmts[i]) {
8495            let karg = match &stmts[i] {
8496                Stmt::ExprStmt(Expr::Method { args, .. }) => pick(args),
8497                _ => None,
8498            };
8499            if let Some(v) = karg {
8500                let mut j = i + 1;
8501                let mut found = None;
8502                while j < stmts.len() {
8503                    if matches!(stmts[j], Stmt::Return(None)) {
8504                        found = Some(j);
8505                        break;
8506                    }
8507                    if is_bare_ctor_call(&stmts[j]) {
8508                        break; // next delegation owns the next return
8509                    }
8510                    j += 1;
8511                }
8512                match found {
8513                    Some(j) => {
8514                        stmts[j] = Stmt::Return(Some(v));
8515                        stmts.remove(i);
8516                        continue;
8517                    }
8518                    None => {
8519                        stmts[i] = Stmt::Return(Some(v));
8520                        stmts.truncate(i + 1);
8521                        break;
8522                    }
8523                }
8524            }
8525        }
8526        fill_returns_one(&mut stmts[i], pick);
8527        i += 1;
8528    }
8529}
8530
8531fn fill_returns_one(st: &mut Stmt, pick: &dyn Fn(&[Expr]) -> Option<Expr>) {
8532    match st {
8533        Stmt::Block(v) => fill_returns_by_position(v, pick),
8534        Stmt::If { then_stmt, else_stmt, .. } => {
8535            fill_returns_stmt(then_stmt, pick);
8536            if let Some(e) = else_stmt {
8537                fill_returns_stmt(e, pick);
8538            }
8539        }
8540        Stmt::While { body, .. }
8541        | Stmt::DoWhile { body, .. }
8542        | Stmt::ForEach { body, .. }
8543        | Stmt::Synchronized { body, .. }
8544        | Stmt::Labeled { body, .. } => fill_returns_stmt(body, pick),
8545        Stmt::For { init, body, .. } => {
8546            fill_returns_by_position(init, pick);
8547            fill_returns_stmt(body, pick);
8548        }
8549        Stmt::Switch { cases, default, .. } => {
8550            for c in cases.iter_mut() {
8551                fill_returns_by_position(&mut c.body, pick);
8552            }
8553            if let Some(d) = default {
8554                fill_returns_stmt(d, pick);
8555            }
8556        }
8557        Stmt::Try { body, catches, finally } | Stmt::TryWithResources { body, catches, finally, .. } => {
8558            fill_returns_stmt(body, pick);
8559            for c in catches.iter_mut() {
8560                fill_returns_stmt(&mut c.body, pick);
8561            }
8562            if let Some(f) = finally {
8563                fill_returns_stmt(f, pick);
8564            }
8565        }
8566        _ => {}
8567    }
8568}
8569
8570fn fill_returns_stmt(st: &mut Stmt, pick: &dyn Fn(&[Expr]) -> Option<Expr>) {
8571    if is_bare_ctor_call(st) {
8572        // Single-statement branch body (`if (c) this(..);`) — becomes
8573        // the return directly.
8574        if let Stmt::ExprStmt(Expr::Method { args, .. }) = st {
8575            if let Some(v) = pick(args) {
8576                *st = Stmt::Return(Some(v));
8577                return;
8578            }
8579        }
8580    }
8581    fill_returns_one(st, pick);
8582}
8583
8584fn normalize_tail(t: &[Stmt]) -> Vec<Stmt> {
8585    let mut v = t.to_vec();
8586    while matches!(v.last(), Some(Stmt::Return(None))) {
8587        v.pop();
8588    }
8589    v
8590}
8591
8592/// Record (and with `carve`, remove) the statements between each
8593/// delegation and the next bare return in its list — a delegation with
8594/// no following return takes the rest of the list. Single-statement
8595/// branch bodies record an empty tail.
8596fn site_tails_walk(stmts: &mut Vec<Stmt>, out: &mut Vec<Vec<Stmt>>, carve: bool) {
8597    let mut i = 0;
8598    while i < stmts.len() {
8599        if is_bare_ctor_call(&stmts[i]) {
8600            let mut j = i + 1;
8601            let mut end = stmts.len();
8602            while j < stmts.len() {
8603                if matches!(stmts[j], Stmt::Return(None)) {
8604                    end = j;
8605                    break;
8606                }
8607                if is_bare_ctor_call(&stmts[j]) {
8608                    end = j;
8609                    break;
8610                }
8611                j += 1;
8612            }
8613            out.push(stmts[i + 1..end].to_vec());
8614            if carve {
8615                stmts.splice(i + 1..end, []);
8616            }
8617            i += 1;
8618            continue;
8619        }
8620        site_tails_one(&mut stmts[i], out, carve);
8621        i += 1;
8622    }
8623}
8624
8625fn site_tails_one(st: &mut Stmt, out: &mut Vec<Vec<Stmt>>, carve: bool) {
8626    if is_bare_ctor_call(st) {
8627        out.push(Vec::new());
8628        return;
8629    }
8630    match st {
8631        Stmt::Block(v) => site_tails_walk(v, out, carve),
8632        Stmt::If { then_stmt, else_stmt, .. } => {
8633            site_tails_one(then_stmt, out, carve);
8634            if let Some(e) = else_stmt {
8635                site_tails_one(e, out, carve);
8636            }
8637        }
8638        Stmt::While { body, .. }
8639        | Stmt::DoWhile { body, .. }
8640        | Stmt::ForEach { body, .. }
8641        | Stmt::Synchronized { body, .. }
8642        | Stmt::Labeled { body, .. } => site_tails_one(body, out, carve),
8643        Stmt::For { init, body, .. } => {
8644            site_tails_walk(init, out, carve);
8645            site_tails_one(body, out, carve);
8646        }
8647        Stmt::Switch { cases, default, .. } => {
8648            for c in cases.iter_mut() {
8649                site_tails_walk(&mut c.body, out, carve);
8650            }
8651            if let Some(d) = default {
8652                site_tails_one(d, out, carve);
8653            }
8654        }
8655        Stmt::Try { body, catches, finally } | Stmt::TryWithResources { body, catches, finally, .. } => {
8656            site_tails_one(body, out, carve);
8657            for c in catches.iter_mut() {
8658                site_tails_one(&mut c.body, out, carve);
8659            }
8660            if let Some(f) = finally {
8661                site_tails_one(f, out, carve);
8662            }
8663        }
8664        _ => {}
8665    }
8666}
8667
8668/// Can this statement list NOT complete normally? Conservative for
8669/// loops (false); If needs BOTH branches, Switch needs every case plus
8670/// a default. Used to decide whether the extracted helper needs the
8671/// trailing `return carrier;` (appending one after exhaustive branches
8672/// is javac's "unreachable statement" error).
8673fn always_returns(stmts: &[Stmt]) -> bool {
8674    let Some(last) = stmts
8675        .iter()
8676        .rev()
8677        .find(|s| !matches!(s, Stmt::Block(v) if v.is_empty()))
8678    else {
8679        return false;
8680    };
8681    match last {
8682        Stmt::Return(Some(_)) | Stmt::Throw(_) => true,
8683        Stmt::Block(v) => always_returns(v),
8684        Stmt::If { then_stmt, else_stmt, .. } => match else_stmt {
8685            Some(e) => always_returns_one(then_stmt) && always_returns_one(e),
8686            None => false,
8687        },
8688        Stmt::Labeled { body, .. } | Stmt::Synchronized { body, .. } => {
8689            always_returns_one(body)
8690        }
8691        Stmt::Switch { cases, default, .. } => {
8692            default.as_ref().is_some_and(|d| always_returns_one(d))
8693                && cases.iter().all(|c| always_returns(&c.body))
8694        }
8695        Stmt::Try { body, catches, finally }
8696        | Stmt::TryWithResources { body, catches, finally, .. } => {
8697            finally.as_ref().is_some_and(|f| always_returns_one(f))
8698                || (always_returns_one(body)
8699                    && catches.iter().all(|c| always_returns_one(&c.body)))
8700        }
8701        _ => false,
8702    }
8703}
8704
8705fn always_returns_one(st: &Stmt) -> bool {
8706    always_returns(std::slice::from_ref(st))
8707}
8708
8709fn inline_ctor_arg_defs(
8710    call: &mut Stmt,
8711    prefix: &[&Stmt],
8712    vt: &VarTable,
8713    consumed: &mut Vec<(u32, usize)>,
8714) -> bool {
8715    let Stmt::ExprStmt(cexpr) = call else {
8716        return false;
8717    };
8718    // Replace every read of `v` in `e` with its value-so-far; a read
8719    // BEFORE any def (val None) is unresolvable → false.
8720    fn subst(e: &mut Expr, v: u32, val: &Option<Expr>) -> bool {
8721        let mut ok = true;
8722        deep_rewrite(e, &mut |x| {
8723            if let Expr::Local { var, .. } = x {
8724                if *var == v {
8725                    match val {
8726                        Some(e2) => *x = e2.clone(),
8727                        None => ok = false,
8728                    }
8729                }
8730            }
8731        });
8732        ok
8733    }
8734    let mut seen: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
8735    for _ in 0..32 {
8736        let mut need: Option<u32> = None;
8737        visit_exprs(cexpr, &mut |x| {
8738            if need.is_none() {
8739                if let Expr::Local { var, .. } = x {
8740                    if (*var as usize) < vt.vars.len() && !vt.vars[*var as usize].is_param {
8741                        need = Some(*var);
8742                    }
8743                }
8744            }
8745        });
8746        let Some(v) = need else { break };
8747        if !seen.insert(v) {
8748            return false; // cyclic reuse (`str = .. + str`) — bail
8749        }
8750        // CHAIN walk: fold EVERY prefix def of v into the value the
8751        // delegation actually receives. The old strict single-occurrence
8752        // rule rejected the convenience-ctor reassignment chain
8753        // (`f = getInstance(ctx); f = transform(f, ..); this(ctx, f)` —
8754        // androidx CameraController, weibo this-not-first ×831): the
8755        // SOURCE inlined the computation into the delegation args and
8756        // R8 decomposed it into register writes. Reads of v are allowed
8757        // only INSIDE its own def chain; any other prefix statement
8758        // touching v means the value escapes or the object mutates
8759        // (`stringBuilder.append(..)` — inlining a fresh `new
8760        // StringBuilder()` would lose the message), so bail.
8761        let mut val: Option<Expr> = None;
8762        let mut ok = true;
8763        for (si, st) in prefix.iter().enumerate() {
8764            match st {
8765                // A bare declaration (init None — the structurer hoists
8766                // `T v;` to the top) carries no value: skip it. Failing
8767                // here rejected every chain under a hoisted decl (ci1/a).
8768                Stmt::LocalDef { var, init: None, .. } if *var == v => {}
8769                Stmt::LocalDef { var, init, .. } if *var == v => {
8770                    let Some(e) = init else { ok = false; break };
8771                    let mut e2 = (*e).clone();
8772                    if !subst(&mut e2, v, &val) {
8773                        ok = false;
8774                        break;
8775                    }
8776                    val = Some(e2);
8777                    consumed.push((v, si));
8778                }
8779                Stmt::ExprStmt(Expr::Assign { target, op, .. })
8780                    if matches!(&**target, Expr::Local { var: tv, .. } if *tv == v) =>
8781                {
8782                    if !matches!(op, AssignOp::Plain) {
8783                        ok = false; // compound write: not a plain def
8784                        break;
8785                    }
8786                    let Stmt::ExprStmt(Expr::Assign { value, .. }) = st else {
8787                        unreachable!()
8788                    };
8789                    let mut e2 = value.as_ref().clone();
8790                    if !subst(&mut e2, v, &val) {
8791                        ok = false;
8792                        break;
8793                    }
8794                    val = Some(e2);
8795                    consumed.push((v, si));
8796                }
8797                _ => {
8798                    let mut touched = false;
8799                    visit_all_exprs(st, &mut |x| {
8800                        if let Expr::Local { var, .. } = x {
8801                            if *var == v {
8802                                touched = true;
8803                            }
8804                        }
8805                    });
8806                    if touched {
8807                        ok = false;
8808                        break;
8809                    }
8810                }
8811            }
8812        }
8813        if !ok {
8814            return false;
8815        }
8816        let Some(de) = val else { return false };
8817        if contains_this_access(&de) {
8818            return false;
8819        }
8820        // Huge defs multiply per read site per round — bound them.
8821        let mut de_nodes = 0usize;
8822        visit_exprs(&de, &mut |_| de_nodes += 1);
8823        if de_nodes > 256 {
8824            return false;
8825        }
8826        deep_rewrite(cexpr, &mut |x| {
8827            if let Expr::Local { var, .. } = x {
8828                if *var == v {
8829                    *x = de.clone();
8830                }
8831            }
8832        });
8833        // Exponential blowup guard: a var read twice doubles its def
8834        // per round, compounding across the transitive chain — cap the
8835        // expression size (a 2^k node tree overflowed the worker stack
8836        // process-wide).
8837        let mut nodes = 0usize;
8838        visit_exprs(cexpr, &mut |_| nodes += 1);
8839        if nodes > 2_048 {
8840            return false;
8841        }
8842    }
8843    true
8844}
8845
8846/// Fold a prelude StringBuilder mutation chain into the delegation
8847/// argument as a fluent chain: `StringBuilder sb = new SB(x);
8848/// sb.append(a); sb.append(b); super(sb.toString())` →
8849/// `super(new SB(x).append(a).append(b).toString())`. The generic def
8850/// inliner rejects mutation calls — their effect is not expressible as
8851/// a value — but `append` RETURNS its receiver, so the chain
8852/// reconstructs exactly the same mutations in one expression (androidx
8853/// emoji2/flatbuffer UnpairedSurrogateException family, the shapes the
8854/// strict-occurrence rule was written to refuse). Aborts on any `this`
8855/// touch inside the chain (illegal before super) or any other read of
8856/// the SB local (the value must not escape the fold). Returns the
8857/// delegation's new index (statements before it were removed).
8858fn fold_sb_chain(stmts: &mut Vec<Stmt>, pos: usize) -> usize {
8859    // 1. the SB local: first `v = new StringBuilder(..)` in the prefix.
8860    let mut sbv: Option<u32> = None;
8861    let mut def_i: Option<usize> = None;
8862    let mut def_init: Option<Expr> = None;
8863    for (i, st) in stmts[..pos].iter().enumerate() {
8864        let cand: Option<(u32, &Expr)> = match st {
8865            Stmt::LocalDef {
8866                var,
8867                init: Some(e),
8868                ..
8869            } => Some((*var, e)),
8870            Stmt::ExprStmt(Expr::Assign {
8871                target,
8872                value,
8873                op: AssignOp::Plain,
8874                ..
8875            }) => match &**target {
8876                Expr::Local { var, .. } => Some((*var, value.as_ref())),
8877                _ => None,
8878            },
8879            _ => None,
8880        };
8881        if let Some((v, init)) = cand {
8882            if let Expr::New { cls, .. } = init {
8883                if cls.as_ref() == "java/lang/StringBuilder" {
8884                    sbv = Some(v);
8885                    def_i = Some(i);
8886                    def_init = Some(init.clone());
8887                    break;
8888                }
8889            }
8890        }
8891    }
8892    let (Some(sbv), Some(def_i), Some(init)) = (sbv, def_i, def_init) else {
8893        return pos;
8894    };
8895    if contains_this_access(&init) {
8896        return pos;
8897    }
8898    // 2. append statements on sbv, in order.
8899    let mut appends: Vec<usize> = Vec::new();
8900    for (i, st) in stmts[..pos].iter().enumerate() {
8901        if let Stmt::ExprStmt(Expr::Method {
8902            owner: Some(o),
8903            cls,
8904            name,
8905            ..
8906        }) = st
8907        {
8908            if matches!(&**o, Expr::Local { var, .. } if *var == sbv)
8909                && cls.as_ref() == "java/lang/StringBuilder"
8910                && &**name == "append"
8911            {
8912                appends.push(i);
8913            }
8914        }
8915    }
8916    // 3. no read of sbv after the delegation.
8917    let mut post_read = false;
8918    for st in stmts.iter().skip(pos + 1) {
8919        visit_all_exprs(st, &mut |e| {
8920            if let Expr::Local { var, .. } = e {
8921                if *var == sbv {
8922                    post_read = true;
8923                }
8924            }
8925        });
8926    }
8927    if post_read {
8928        return pos;
8929    }
8930    // 4. prefix reads of sbv only in the def and the append receivers
8931    //    (arguments are checked for `this`, receivers are the chain).
8932    for (i, st) in stmts[..pos].iter().enumerate() {
8933        if i == def_i || appends.contains(&i) {
8934            // append ARGUMENTS must not touch this; receiver is fine.
8935            if let Stmt::ExprStmt(Expr::Method { args, .. }) = st {
8936                if args.iter().any(contains_this_access) {
8937                    return pos;
8938                }
8939            }
8940            continue;
8941        }
8942        let mut leak = false;
8943        visit_all_exprs(st, &mut |e| {
8944            if let Expr::Local { var, .. } = e {
8945                if *var == sbv {
8946                    leak = true;
8947                }
8948            }
8949        });
8950        if leak {
8951            return pos;
8952        }
8953    }
8954    // 5. the call reads sbv exactly once, as `sbv.toString()`.
8955    let mut local_reads = 0usize;
8956    let mut tostring_reads = 0usize;
8957    visit_all_exprs(&stmts[pos], &mut |e| match e {
8958        Expr::Local { var, .. } if *var == sbv => local_reads += 1,
8959        Expr::Method {
8960            owner: Some(o),
8961            name,
8962            args,
8963            ..
8964        } if args.is_empty()
8965            && &**name == "toString"
8966            && matches!(&**o, Expr::Local { var, .. } if *var == sbv) =>
8967        {
8968            tostring_reads += 1;
8969        }
8970        _ => {}
8971    });
8972    if local_reads != 1 || tostring_reads != 1 {
8973        return pos;
8974    }
8975    // 6. build the fluent chain and swap it into the toString receiver.
8976    let mut chain = init;
8977    for ai in &appends {
8978        if let Stmt::ExprStmt(Expr::Method {
8979            owner: _,
8980            cls,
8981            name,
8982            desc,
8983            args,
8984            is_static,
8985            is_interface,
8986            is_special,
8987            is_super,
8988            is_dynamic,
8989            type_args,
8990            ..
8991        }) = &stmts[*ai]
8992        {
8993            chain = Expr::Method {
8994                owner: Some(Box::new(chain)),
8995                cls: cls.clone(),
8996                name: name.clone(),
8997                desc: desc.clone(),
8998                args: args.clone(),
8999                is_static: *is_static,
9000                is_interface: *is_interface,
9001                is_special: *is_special,
9002                is_super: *is_super,
9003                is_dynamic: *is_dynamic,
9004                type_args: type_args.clone(),
9005            };
9006        }
9007    }
9008    let mut replaced = false;
9009    walk_stmt_exprs(&mut stmts[pos], &mut |e| {
9010        deep_rewrite(e, &mut |x| {
9011            if let Expr::Method {
9012                owner: Some(o),
9013                name,
9014                args,
9015                ..
9016            } = x
9017            {
9018                if args.is_empty()
9019                    && &**name == "toString"
9020                    && matches!(&**o, Expr::Local { var, .. } if *var == sbv)
9021                {
9022                    **o = chain.clone();
9023                    replaced = true;
9024                }
9025            }
9026        });
9027    });
9028    if !replaced {
9029        return pos; // never remove without the swap — atomicity
9030    }
9031    // 7. drop the consumed statements (descending), adjust pos.
9032    let mut removed: Vec<usize> = appends.clone();
9033    removed.push(def_i);
9034    removed.sort_unstable();
9035    removed.dedup();
9036    for i in removed.iter().rev() {
9037        stmts.remove(*i);
9038    }
9039    pos - removed.len()
9040}
9041
9042pub fn fix_ctor_super_first(body: &mut Stmt, vt: &VarTable) {
9043    let Stmt::Block(stmts) = body else { return };
9044    if stmts.is_empty() || is_bare_ctor_call(stmts.first().unwrap()) {
9045        return;
9046    }
9047    // Top-level delegation call.
9048    if let Some(pos0) = stmts.iter().position(is_bare_ctor_call) {
9049        let pos = fold_sb_chain(stmts, pos0);
9050        let mut call = stmts[pos].clone();
9051        let prefix: Vec<&Stmt> = stmts[..pos].iter().collect();
9052        let mut consumed: Vec<(u32, usize)> = Vec::new();
9053        if !inline_ctor_arg_defs(&mut call, &prefix, vt, &mut consumed) {
9054            return;
9055        }
9056        stmts.remove(pos);
9057        // Drop the chain defs the inline consumed (all indices < pos) —
9058        // leaving them would DOUBLE-evaluate their (possibly impure)
9059        // inits. But ONLY for vars with no post-delegation use: dropping
9060        // such a var's DECLARATION makes ensure_declared re-add it at
9061        // the method top, i.e. BEFORE this() — a fresh "对this的调用必须
9062        // 是构造器中的第一个语句" (lark +587 the blind drop caused).
9063        let mut drop_idx: Vec<usize> = consumed
9064            .iter()
9065            .filter(|(v, _)| !var_occurs_in(&stmts[pos..], *v))
9066            .map(|(_, k)| *k)
9067            .collect();
9068        drop_idx.sort_unstable();
9069        drop_idx.dedup();
9070        for k in drop_idx.iter().rev() {
9071            stmts.remove(*k);
9072        }
9073        stmts.insert(0, call);
9074        return;
9075    }
9076    // The structurer can wrap the delegation in a bare nested block —
9077    // with the parameter null-checks AHEAD of it (weixin's Kotlin
9078    // intrinsics shape: `o.h(parcel, "source"); super();` at inner
9079    // positions 0/1, flattened to straight-line by cleanup AFTER this
9080    // pass, which is why the miss surfaced as 1150 weixin "对super的
9081    // 调用必须是构造器中的第一个语句"). Hoist the call from any
9082    // position whose predecessors are all straight-line statements; a
9083    // delegation behind control flow is the conditional-super family
9084    // (needs restructuring, not hoisting) and stays put.
9085    let mut found: Option<(usize, usize)> = None;
9086    for (i, st) in stmts.iter().enumerate() {
9087        if let Stmt::Block(inner) = st {
9088            if let Some(p) = inner.iter().position(is_bare_ctor_call) {
9089                if inner[..p]
9090                    .iter()
9091                    .all(|s| matches!(s, Stmt::LocalDef { .. } | Stmt::ExprStmt(_)))
9092                {
9093                    found = Some((i, p));
9094                    break;
9095                }
9096            }
9097        }
9098    }
9099    if let Some((i, p)) = found {
9100        let mut call = match &stmts[i] {
9101            Stmt::Block(inner) => inner[p].clone(),
9102            _ => unreachable!(),
9103        };
9104        let mut prefix: Vec<&Stmt> = stmts[..i].iter().collect();
9105        if let Stmt::Block(inner) = &stmts[i] {
9106            prefix.extend(inner[..p].iter());
9107        }
9108        let mut consumed: Vec<(u32, usize)> = Vec::new();
9109        if !inline_ctor_arg_defs(&mut call, &prefix, vt, &mut consumed) {
9110            return;
9111        }
9112        // Prefix index k maps to stmts[k] for k < i, inner[k - i] for
9113        // k >= i (all such k are < p). Suffix = the rest of the inner
9114        // block + the following top-level statements; a consumed def may
9115        // only drop when its var has no suffix use (declaration loss →
9116        // ensure_declared re-adds it BEFORE this()).
9117        // Compute the droppable set BEFORE mutating (borrow split).
9118        let mut ks: Vec<usize> = Vec::new();
9119        {
9120            let inner_tail: &[Stmt] = match &stmts[i] {
9121                Stmt::Block(inner) if p < inner.len() => &inner[p + 1..],
9122                _ => &[],
9123            };
9124            for (v, k) in &consumed {
9125                if !var_occurs_in(inner_tail, *v) && !var_occurs_in(&stmts[i + 1..], *v) {
9126                    ks.push(*k);
9127                }
9128            }
9129        }
9130        ks.sort_unstable();
9131        ks.dedup();
9132        if let Stmt::Block(inner) = &mut stmts[i] {
9133            inner.remove(p);
9134            for k in ks.iter().rev().filter(|k| **k >= i) {
9135                inner.remove(k - i);
9136            }
9137        }
9138        for k in ks.iter().rev().filter(|k| **k < i) {
9139            stmts.remove(*k);
9140        }
9141        stmts.insert(0, call);
9142    }
9143}
9144
9145/// A true constructor DELEGATION statement: `super(..)` (owner None) or
9146/// `this(..)` (owner This). An un-folded `new X; <init>` (lost
9147/// allocation — the ctor-fold-bug family) arrives as a `<init>` Method
9148/// with a NON-this owner and prints as `new X(..)`: it is not a
9149/// delegation and must neither satisfy nor anchor the super-first fixes
9150/// (v7.a$a2: a dead `new f(0,..)` init at position 0 made both hoists
9151/// believe the delegation was already first, leaving the real `super`
9152/// last — "对super的调用必须是构造器中的第一个语句").
9153fn is_delegation_expr(e: &Expr) -> bool {
9154    matches!(
9155        e,
9156        Expr::Method { name, is_special: true, owner, .. }
9157            if &**name == "<init>"
9158                && (owner.is_none() || matches!(owner.as_deref(), Some(Expr::This)))
9159    )
9160}
9161
9162pub(crate) fn is_bare_ctor_call(s: &Stmt) -> bool {
9163    matches!(s, Stmt::ExprStmt(e) if is_delegation_expr(e))
9164}
9165
9166/// Is the local ever ASSIGNED (write position: `x = ..`, `++x`)?
9167/// Distinct from a read count: a ctor's synthetic outer param is only
9168/// ever read; a param that gets written must stay declared.
9169pub(crate) fn local_is_written(body: &Stmt, var: u32) -> bool {
9170    let mut hit = false;
9171    visit_stmt_exprs_ro(body, &mut |e| {
9172        if hit {
9173            return;
9174        }
9175        visit_exprs(e, &mut |x| {
9176            if hit {
9177                return;
9178            }
9179            match x {
9180                Expr::Assign { target, .. }
9181                    if matches!(&**target, Expr::Local { var: v, .. } if *v == var) =>
9182                {
9183                    hit = true;
9184                }
9185                Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. }
9186                    if matches!(&**e, Expr::Local { var: v, .. } if *v == var) =>
9187                {
9188                    hit = true;
9189                }
9190                _ => {}
9191            }
9192        });
9193    });
9194    hit
9195}
9196
9197/// Source-form normalization for a non-static member-inner constructor.
9198/// The dex descriptor carries the synthetic outer instance as args[0]
9199/// (typed as the direct enclosing class); the emitter's qualified
9200/// `outer.new Inner(..)` / `this.new Inner(..)` sites pass it
9201/// implicitly and `super(..)` delegations drop it, so the signature
9202/// loses the param — and the body must stop referencing it: plain uses
9203/// become `Outer.this` (a `this.this$0 = p` assignment becomes
9204/// `this.this$0 = Outer.this`, exactly the runtime value), and
9205/// this()-delegations to `eligible` classes drop the leading param arg.
9206pub(crate) fn rewrite_inner_ctor_outer_param(
9207    body: &mut Stmt,
9208    param0: u32,
9209    outer_display: &str,
9210    outer_ty: &TypeRef,
9211    eligible: &[String],
9212) {
9213    // this()/super() delegations (and lost-alloc construction calls):
9214    // the leading arg IS the synthetic outer exactly when it is the
9215    // param itself.
9216    walk_stmt_exprs(body, &mut |e| {
9217        if let Expr::Method { name, cls, args, .. } = e {
9218            if &**name == "<init>"
9219                && eligible.iter().any(|c| c.as_str() == cls.as_ref())
9220                && args.first().is_some_and(|a| matches!(a, Expr::Local { var: v, .. } if *v == param0))
9221            {
9222                args.remove(0);
9223            }
9224        }
9225    });
9226    // Every remaining read of the param becomes `Outer.this`.
9227    let text = format!("{outer_display}.this");
9228    let ty = outer_ty.clone();
9229    walk_stmt_exprs(body, &mut |e| {
9230        deep_rewrite_reads(e, &mut |x| {
9231            if let Expr::Local { var: v, .. } = x {
9232                if *v == param0 {
9233                    *x = Expr::RawT(text.clone(), ty.clone());
9234                }
9235            }
9236        });
9237    });
9238}
9239
9240/// Ctors whose delegation is buried in control flow or behind arg
9241/// computations — the "对super的调用必须是构造器中的第一个语句" family
9242/// that plain hoisting cannot reach (weixin 264 / weibo 116 / reqable 71
9243/// / lark 62 after round 60): R8/Kotlin compute super-args conditionally
9244/// and dex legally invokes super mid-branch.
9245///
9246/// Shape B (branch merge): `prelude; if (c) {d1; super(A1); t1} else
9247/// {d2; super(A2); t2}; post` → one leading `super(..)` where each
9248/// differing arg becomes `c ? a1i : a2i`; branch remainders stay an
9249/// if/else AFTER the call; prelude statements move after it (nothing
9250/// may precede super in Java; bytecode pre-super work is local-only by
9251/// construction — dex forbids instance-field reads before the delegate).
9252///
9253/// Shape A (linear): `defs; super(A); rest` where `A` references the
9254/// def locals — plain hoisting (fix_ctor_super_first case 1) would move
9255/// the call above its argument definitions (forward refs). Inline the
9256/// referenced defs into the args first, drop the consumed defs, hoist.
9257/// Runs BEFORE fix_ctor_super_first and only fires when args reference
9258/// preceding locals, leaving the battle-tested plain hoist untouched
9259/// otherwise.
9260///
9261/// Inlining duplicates the def per use site, so a side-effecting def is
9262/// inlined only when its var's use count is provably preserved: a
9263/// single use in a merged arg (1 copy), or a single use in the
9264/// condition with ≤1 differing arg (the cond then appears in the `if`
9265/// plus one ternary — 2 copies of e.g. one Kotlin getter call, which
9266/// the original bytecode typically made twice anyway: `p.u() == null ?
9267/// .. : p.u().i()`). Pure defs inline freely. Every other shape aborts:
9268/// broken-but-faithful stays the status quo, wrong evaluation counts
9269/// would be worse.
9270/// Linear `def; delegation(args read def)` shapes. A plain hoist of the
9271/// delegation past its prefix would place the call ABOVE a definition
9272/// it reads — a forward reference (`super(context2, ..)` with `context2
9273/// = ..` below it). javac's attribution for the whole class then
9274/// collapses: the undefined identifier cascades into "non-static
9275/// super" and even bare `Object` resolution failures, poisoning every
9276/// later diagnostic in the file (weibo AppCompatTextView — the root of
9277/// a 4.1k-file Object cascade). The original source had the def's
9278/// expression INLINE in the delegation args; d8 computed it into a
9279/// register first. Inline the def into the args (the def is read
9280/// exactly once there — evaluation count preserved even for calls),
9281/// and when the register is REASSIGNED later (a fresh generation
9282/// sharing the slot), convert that first write into the declaration so
9283/// the var stays defined for its later readers. Any shape that cannot
9284/// be proven aborts untouched.
9285/// Count WRITE positions of `var` (assign / inc-dec targets) — the
9286/// shape-C validator needs exact counts, not just presence.
9287fn stmts_count_writes(stmts: &[Stmt], var: u32) -> usize {
9288    let mut n = 0usize;
9289    for s in stmts {
9290        visit_stmt_exprs_ro(s, &mut |e| {
9291            visit_exprs(e, &mut |x| match x {
9292                Expr::Assign { target, .. }
9293                    if matches!(&**target, Expr::Local { var: v, .. } if *v == var) =>
9294                {
9295                    n += 1;
9296                }
9297                Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. }
9298                    if matches!(&**e, Expr::Local { var: v, .. } if *v == var) =>
9299                {
9300                    n += 1;
9301                }
9302                _ => {}
9303            });
9304        });
9305    }
9306    n
9307}
9308
9309/// Collect every local WRITTEN in `stmts` — the complement inside a
9310/// ctor prefix is the param set (the only locals a pre-delegation arg
9311/// expression may read).
9312fn prefix_written_vars(stmts: &[Stmt]) -> jdc_core::FxHashSet<u32> {
9313    let mut set = jdc_core::FxHashSet::default();
9314    for s in stmts {
9315        let mut c = s.clone();
9316        walk_stmt_exprs(&mut c, &mut |e| {
9317            deep_rewrite(e, &mut |x| match x {
9318                Expr::Assign { target, .. } => {
9319                    if let Expr::Local { var, .. } = &**target {
9320                        set.insert(*var);
9321                    }
9322                }
9323                Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => {
9324                    if let Expr::Local { var, .. } = &**e {
9325                        set.insert(*var);
9326                    }
9327                }
9328                _ => {}
9329            });
9330        });
9331        if let Stmt::LocalDef { var, .. } = s {
9332            set.insert(*var);
9333        }
9334    }
9335    set
9336}
9337
9338/// Is `e` safe to evaluate INSIDE a ctor-delegation argument (before
9339/// this/super exists)? Params/consts/pure ops only — no instance-field
9340/// reads, no `this`, no captures.
9341fn pre_this_safe(e: &Expr, prefix_vars: &jdc_core::FxHashSet<u32>) -> bool {
9342    match e {
9343        Expr::Local { var, .. } => !prefix_vars.contains(var),
9344        Expr::Const(_) => true,
9345        Expr::Cast { e, .. } => pre_this_safe(e, prefix_vars),
9346        Expr::Bin { l, r, .. } => {
9347            pre_this_safe(l, prefix_vars) && pre_this_safe(r, prefix_vars)
9348        }
9349        Expr::Cond { c, t, f } => {
9350            pre_this_safe(c, prefix_vars)
9351                && pre_this_safe(t, prefix_vars)
9352                && pre_this_safe(f, prefix_vars)
9353        }
9354        Expr::Field { owner, is_static, .. } => {
9355            *is_static
9356                && owner
9357                    .as_ref()
9358                    .is_none_or(|o| pre_this_safe(o, prefix_vars))
9359        }
9360        Expr::Method {
9361            owner,
9362            args,
9363            is_static,
9364            ..
9365        } => {
9366            args.iter().all(|a| pre_this_safe(a, prefix_vars))
9367                && match owner {
9368                    Some(o) => pre_this_safe(o, prefix_vars),
9369                    None => *is_static,
9370                }
9371        }
9372        // Allocation in a delegation arg is legal Java before this()
9373        // (the ctor call touches no instance state of THIS class) — the
9374        // missing arm sent every `new Date()`-style Kotlin default
9375        // through `_ => false`, rejecting the whole fold (lark
9376        // Request$AppToApp this-not-first family, ~650 lark lines).
9377        // An inner-class construction needing an outer `this` carries
9378        // Expr::This in its args, which still fails below.
9379        Expr::New { args, .. } => args.iter().all(|a| pre_this_safe(a, prefix_vars)),
9380        Expr::NewArray { dims, init, .. } => {
9381            dims.iter().all(|d| pre_this_safe(d, prefix_vars))
9382                && init
9383                    .as_ref()
9384                    .map(|l| l.iter().all(|x| pre_this_safe(x, prefix_vars)))
9385                    .unwrap_or(true)
9386        }
9387        // Pure unary/instanceof/array reads.
9388        Expr::Un { e, .. } | Expr::InstanceOf { e, .. } => pre_this_safe(e, prefix_vars),
9389        Expr::ArrayIndex { array, index } => {
9390            pre_this_safe(array, prefix_vars) && pre_this_safe(index, prefix_vars)
9391        }
9392        _ => false,
9393    }
9394}
9395
9396/// `s` is exactly one plain assignment to local `v` (optionally wrapped
9397/// in a singleton Block): the branch shape of a Kotlin default-arg
9398/// override.
9399fn single_assign_to_v(s: &Stmt, v: u32) -> Option<Expr> {
9400    match s {
9401        Stmt::ExprStmt(Expr::Assign {
9402            target,
9403            op: AssignOp::Plain,
9404            value,
9405        }) if matches!(&**target, Expr::Local { var: vv, .. } if *vv == v) => {
9406            Some((**value).clone())
9407        }
9408        Stmt::Block(xs) if xs.len() == 1 => single_assign_to_v(&xs[0], v),
9409        _ => None,
9410    }
9411}
9412
9413/// Kotlin default-argument BRIDGE ctors (shape C of the delegation
9414/// family): `<init>(params.., int mask, DefaultConstructorMarker)`
9415/// computes each defaulted arg into a local — `v = p; if ((mask&bit)
9416/// != 0) { v = DEFAULT; }` — then delegates `this(.., v, ..)`. Java
9417/// allows NOTHING before the delegation, so the computations must fold
9418/// INTO the args (`this(.., (mask&bit) != 0 ? DEFAULT : p, ..)`).
9419/// Unfolded, the hoist passes lift the delegation above the raw local
9420/// reads: "找不到符号 变量 str2/v4/creationExtras2" — 2.5k root files
9421/// across weixin/lark/weibo (weixin MvvmObserverOwner$..Observer, lark
9422/// SimpleArrayMap/LongSparseArray). Per-var provability: exactly one
9423/// top-level base write, an optional single conditional override whose
9424/// branches are lone assignments, no other prefix touch of the var,
9425/// nothing after the delegation touches it, and the folded expression
9426/// is pre-this-safe. Unprovable vars stay untouched (partial folds are
9427/// strictly better — the rest was broken before too).
9428/// Flatten nested plain Blocks among the top-level statements. The
9429/// structurer hands ctor bodies as region-nested blocks (`Block([def,
9430/// Block([assign, if]), Block([delegate, return])])` — the Kotlin
9431/// bridge shape), and every delegation-repair pass scans the TOP level
9432/// only. Straight-line nesting flattens without semantics; control
9433/// flow lives inside If/While/etc. arms, never as a direct member.
9434pub(crate) fn flatten_top_blocks(stmts: &mut Vec<Stmt>) {
9435    fn rec(s: Stmt, out: &mut Vec<Stmt>) {
9436        match s {
9437            Stmt::Block(inner) => {
9438                for x in inner {
9439                    rec(x, out);
9440                }
9441            }
9442            other => out.push(other),
9443        }
9444    }
9445    if !stmts.iter().any(|s| matches!(s, Stmt::Block(_))) {
9446        return;
9447    }
9448    let mut out: Vec<Stmt> = Vec::with_capacity(stmts.len());
9449    for s in std::mem::take(stmts) {
9450        rec(s, &mut out);
9451    }
9452    *stmts = out;
9453}
9454
9455fn fold_default_arg_bridge(stmts: &mut Vec<Stmt>) -> bool {
9456    flatten_top_blocks(stmts);
9457    let Some(pos) = stmts.iter().position(is_bare_ctor_call) else {
9458        return false;
9459    };
9460    if pos == 0 {
9461        return false;
9462    }
9463    let Stmt::ExprStmt(Expr::Method { args, .. }) = &stmts[pos] else {
9464        return false;
9465    };
9466    let mut arg_vars: Vec<u32> = Vec::new();
9467    let mut reads = 0usize;
9468    for a in args {
9469        let mut c = a.clone();
9470        deep_rewrite_reads(&mut c, &mut |x| {
9471            if let Expr::Local { var, .. } = x {
9472                reads += 1;
9473                if !arg_vars.contains(var) {
9474                    arg_vars.push(*var);
9475                }
9476            }
9477        });
9478    }
9479    if arg_vars.is_empty() || reads != arg_vars.len() {
9480        return false; // a var read twice would duplicate evaluation
9481    }
9482    // The bridge ENDS at the delegation: nothing after may touch the
9483    // arg vars (reads OR writes).
9484    for &v in &arg_vars {
9485        if stmts_read_var(&stmts[pos + 1..], v) != 0
9486            || stmts_count_writes(&stmts[pos + 1..], v) != 0
9487        {
9488            return false;
9489        }
9490    }
9491    let prefix_vars = prefix_written_vars(&stmts[..pos]);
9492    let mut repl: Vec<(u32, Expr)> = Vec::new();
9493    let mut drop: Vec<usize> = Vec::new();
9494    for &v in &arg_vars {
9495        // Exact prefix shape for v.
9496        let mut base: Option<(usize, Expr)> = None;
9497        let mut decl: Option<usize> = None;
9498        let mut condf: Option<(usize, Expr, Option<Expr>, Option<Expr>)> = None;
9499        let mut ok = true;
9500        for (i, s) in stmts[..pos].iter().enumerate() {
9501            match s {
9502                Stmt::LocalDef { var, init, .. } if *var == v => match init {
9503                    None => {
9504                        if decl.is_some() {
9505                            ok = false;
9506                            break;
9507                        }
9508                        decl = Some(i);
9509                    }
9510                    Some(e) => {
9511                        if base.is_some() {
9512                            ok = false;
9513                            break;
9514                        }
9515                        base = Some((i, e.clone()));
9516                    }
9517                },
9518                Stmt::ExprStmt(Expr::Assign {
9519                    target,
9520                    op: AssignOp::Plain,
9521                    value,
9522                }) if matches!(&**target, Expr::Local { var: vv, .. } if *vv == v) => {
9523                    if base.is_some() {
9524                        ok = false;
9525                        break;
9526                    }
9527                    base = Some((i, (**value).clone()));
9528                }
9529                Stmt::If {
9530                    cond,
9531                    then_stmt,
9532                    else_stmt,
9533                } => {
9534                    let t = single_assign_to_v(then_stmt, v);
9535                    let f = else_stmt.as_ref().and_then(|b| single_assign_to_v(b, v));
9536                    let then_touches = t.is_some()
9537                        || stmts_read_var(std::slice::from_ref(&**then_stmt), v) != 0
9538                        || stmts_count_writes(std::slice::from_ref(&**then_stmt), v) != 0;
9539                    let else_touches = else_stmt.as_ref().is_some_and(|b| {
9540                        f.is_some()
9541                            || stmts_read_var(std::slice::from_ref(b), v) != 0
9542                            || stmts_count_writes(std::slice::from_ref(b), v) != 0
9543                    });
9544                    if !then_touches && !else_touches {
9545                        continue; // unrelated if
9546                    }
9547                    // Both branches must be lone assignments (a branch
9548                    // with its own delegation/return is the conditional-
9549                    // super machinery's shape, not this one), the cond
9550                    // must not read v, and only one override per var.
9551                    if condf.is_some()
9552                        || base.is_none()
9553                        || (then_touches && t.is_none())
9554                        || (else_touches && f.is_none())
9555                    {
9556                        ok = false;
9557                        break;
9558                    }
9559                    let cond_stmt = Stmt::ExprStmt(cond.clone());
9560                    if stmts_read_var(std::slice::from_ref(&cond_stmt), v) != 0 {
9561                        ok = false;
9562                        break;
9563                    }
9564                    condf = Some((i, cond.clone(), t, f));
9565                }
9566                _ => {}
9567            }
9568        }
9569        if !ok {
9570            continue;
9571        }
9572        let Some((bi, be)) = base else { continue };
9573        // Exact write/read accounting: base (+then/else) writes, zero
9574        // prefix reads.
9575        let want_writes = 1
9576            + condf
9577                .as_ref()
9578                .map(|(_, _, t, f)| t.is_some() as usize + f.is_some() as usize)
9579                .unwrap_or(0);
9580        if stmts_count_writes(&stmts[..pos], v) != want_writes
9581            || stmts_read_var(&stmts[..pos], v) != 0
9582        {
9583            continue;
9584        }
9585        let rep = match condf {
9586            Some((ci, c, t, f)) => {
9587                drop.push(ci);
9588                match (t, f) {
9589                    (Some(t), Some(f)) => Expr::Cond {
9590                        c: Box::new(c),
9591                        t: Box::new(t),
9592                        f: Box::new(f),
9593                    },
9594                    (Some(t), None) => Expr::Cond {
9595                        c: Box::new(c),
9596                        t: Box::new(t),
9597                        f: Box::new(be.clone()),
9598                    },
9599                    (None, Some(f)) => Expr::Cond {
9600                        c: Box::new(c),
9601                        t: Box::new(be.clone()),
9602                        f: Box::new(f),
9603                    },
9604                    (None, None) => unreachable!("condf requires a branch assign"),
9605                }
9606            }
9607            None => be.clone(),
9608        };
9609        if !pre_this_safe(&rep, &prefix_vars) {
9610            continue;
9611        }
9612        drop.push(bi);
9613        if let Some(d) = decl {
9614            drop.push(d);
9615        }
9616        repl.push((v, rep));
9617    }
9618    if repl.is_empty() {
9619        return false;
9620    }
9621    if let Stmt::ExprStmt(Expr::Method { args, .. }) = &mut stmts[pos] {
9622        for a in args.iter_mut() {
9623            deep_rewrite_reads(a, &mut |x| {
9624                if let Expr::Local { var, .. } = x {
9625                    if let Some((_, e)) = repl.iter().find(|(vv, _)| vv == var) {
9626                        *x = e.clone();
9627                    }
9628                }
9629            });
9630        }
9631    }
9632    drop.sort_unstable();
9633    drop.dedup();
9634    for i in drop.into_iter().rev() {
9635        stmts.remove(i);
9636    }
9637    true
9638}
9639
9640/// Rewrite static member references on Kotlin multi-file facade PARTS
9641/// (`StringsKt__StringsKt.trim(..)`) to the public facade the parts are
9642/// compiled into (`StringsKt.trim(..)`): the parts are package-private,
9643/// so every cross-package call site is "在kotlin.text中不是公共的; 无法
9644/// 从外部程序包中对其进行访问" (lark 1.4k root files; weibo's 8.4k-line
9645/// Kt cluster). Static members resolve through the facade by
9646/// inheritance (the map is gated on the extends chain).
9647pub fn rewrite_kotlin_facades(body: &mut Stmt, pool: &crate::DexPool) {
9648    let map = pool.kotlin_facade_map();
9649    if map.is_empty() {
9650        return;
9651    }
9652    walk_stmt_exprs(body, &mut |e| {
9653        match e {
9654            Expr::Method {
9655                cls,
9656                name,
9657                desc,
9658                is_static,
9659                ..
9660            } if *is_static => {
9661                if let Some((base, cover)) = map.get(cls.as_ref()) {
9662                    let hit = match cover {
9663                        None => true,
9664                        Some(c) => c.0.contains(&(name.to_string(), desc.to_string())),
9665                    };
9666                    if hit {
9667                        *cls = std::sync::Arc::from(base.as_str());
9668                    }
9669                }
9670            }
9671            Expr::Field {
9672                cls,
9673                name,
9674                is_static,
9675                ..
9676            } if *is_static => {
9677                if let Some((base, cover)) = map.get(cls.as_ref()) {
9678                    let hit = match cover {
9679                        None => true,
9680                        Some(c) => c.1.contains(name.as_ref()),
9681                    };
9682                    if hit {
9683                        *cls = std::sync::Arc::from(base.as_str());
9684                    }
9685                }
9686            }
9687            _ => {}
9688        }
9689    });
9690}
9691
9692/// Kotlin default-arg bridge fold for ENUM ctors: the synthetic
9693/// `(String, int, .., int, DefaultConstructorMarker)` ctor computes
9694/// defaulted args then chains `this(..)` mid-body — Java requires the
9695/// delegation first (对this的调用必须是构造器中的第一个语句, lark's
9696/// UserCustomStatusExtraParams$* family, 752 lines). Same fold the
9697/// class-ctor path runs; the enum branch of the pipeline used to skip
9698/// it (only strip_enum_ctor_super ran there).
9699pub fn fold_enum_default_arg_bridge(body: &mut Stmt) {
9700    let Stmt::Block(stmts) = body else { return };
9701    fold_default_arg_bridge(stmts);
9702}
9703
9704pub fn fix_ctor_delegation_arg_defs(body: &mut Stmt) {
9705    let Stmt::Block(stmts) = body else { return };
9706    // Shape C first: the Kotlin default-arg bridge (computations +
9707    // conditional overrides folded into the delegation args).
9708    fold_default_arg_bridge(stmts);
9709    // Only the top-level linear shape; the first statement needs no
9710    // repair and control flow ahead of the delegation belongs to the
9711    // conditional-super machinery.
9712    let Some(pos) = stmts.iter().position(is_bare_ctor_call) else {
9713        return;
9714    };
9715    if pos == 0 {
9716        return;
9717    }
9718    // Vars read by the delegation args, and the total read count (a var
9719    // read twice would duplicate a call evaluation on inline).
9720    let mut arg_vars: Vec<u32> = Vec::new();
9721    let mut total_reads = 0usize;
9722    {
9723        let Stmt::ExprStmt(Expr::Method { args, .. }) = &stmts[pos] else {
9724            return;
9725        };
9726        for a in args {
9727            let mut c = a.clone();
9728            deep_rewrite_reads(&mut c, &mut |x| {
9729                if let Expr::Local { var: v, .. } = x {
9730                    total_reads += 1;
9731                    if !arg_vars.contains(v) {
9732                        arg_vars.push(*v);
9733                    }
9734                }
9735            });
9736        }
9737    }
9738    if arg_vars.is_empty() || total_reads != arg_vars.len() {
9739        return;
9740    }
9741    // Vars DEFINED anywhere in the prefix (for the init-referenced
9742    // check below — an inlined init may not itself read prefix defs).
9743    let prefix_defs: Vec<u32> = stmts[..pos]
9744        .iter()
9745        .filter_map(|s| match s {
9746            Stmt::LocalDef { var, init: Some(_), .. } => Some(*var),
9747            _ => None,
9748        })
9749        .collect();
9750    // Per referenced var: exactly one prefix def, no other prefix
9751    // reader between def and delegation, the def's init reads no
9752    // prefix-def var, and the first post-delegation use is a top-level
9753    // write (becomes the declaration) or nothing at all.
9754    let mut plan: Vec<(u32, usize, Option<usize>)> = Vec::new();
9755    for v in arg_vars {
9756        let defs: Vec<usize> = stmts[..pos]
9757            .iter()
9758            .enumerate()
9759            .filter(|(i, _)| {
9760                matches!(&stmts[*i], Stmt::LocalDef { var, init: Some(_), .. } if *var == v)
9761            })
9762            .map(|(i, _)| i)
9763            .collect();
9764        if defs.len() > 1 {
9765            return; // ambiguous: multiple prefix defs
9766        }
9767        let Some(&q) = defs.first() else {
9768            continue; // a param: nothing to inline for this var
9769        };
9770        let init = match &stmts[q] {
9771            Stmt::LocalDef { init: Some(e), .. } => e.clone(),
9772            _ => return,
9773        };
9774        if stmts_read_var(&stmts[q + 1..pos], v) != 0 {
9775            return; // another reader between the def and the delegation
9776        }
9777        // The inlined init may not read a prefix-def var either — the
9778        // delegation lands ABOVE those defs after the hoist.
9779        let mut init_vars: Vec<u32> = Vec::new();
9780        deep_rewrite_reads(&mut { init.clone() }, &mut |x| {
9781            if let Expr::Local { var: vv, .. } = x {
9782                if !init_vars.contains(vv) {
9783                    init_vars.push(*vv);
9784                }
9785            }
9786        });
9787        if init_vars.iter().any(|iv| prefix_defs.contains(iv)) {
9788            return;
9789        }
9790        match stmts[pos + 1..]
9791            .iter()
9792            .position(|s| {
9793                matches!(s, Stmt::ExprStmt(Expr::Assign { target, .. })
9794                    if matches!(&**target, Expr::Local { var: vv, .. } if *vv == v))
9795            }) {
9796            Some(w) => {
9797                // Reads before the re-declaration would be dangling.
9798                if stmts_read_var(&stmts[pos + 1..pos + 1 + w], v) != 0 {
9799                    return;
9800                }
9801                plan.push((v, q, Some(pos + 1 + w)));
9802            }
9803            None => {
9804                // No later write: the def must be dead after inline.
9805                if stmts_read_var(&stmts[pos + 1..], v) != 0 {
9806                    return;
9807                }
9808                plan.push((v, q, None));
9809            }
9810        }
9811    }
9812    if plan.is_empty() {
9813        return;
9814    }
9815    // Inline the def inits into the delegation args.
9816    let values: Vec<(u32, Expr)> = plan
9817        .iter()
9818        .map(|(_v, q, _)| match &stmts[*q] {
9819            Stmt::LocalDef { var, init: Some(e), .. } => (*var, e.clone()),
9820            _ => unreachable!("plan entries carry a LocalDef with init"),
9821        })
9822        .collect();
9823    if let Stmt::ExprStmt(Expr::Method { args, .. }) = &mut stmts[pos] {
9824        for a in args {
9825            deep_rewrite_reads(a, &mut |x| {
9826                if let Expr::Local { var: v, .. } = x {
9827                    if let Some((_, e)) = values.iter().find(|(vv, _)| vv == v) {
9828                        *x = e.clone();
9829                    }
9830                }
9831            });
9832        }
9833    }
9834    // Convert the first writes into declarations BEFORE removing defs
9835    // (removals shift indices); then remove the defs in descending
9836    // order.
9837    for (v, _, w) in &plan {
9838        if let Some(w) = w {
9839            if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = &mut stmts[*w] {
9840                if matches!(&**target, Expr::Local { var: vv, .. } if vv == v) {
9841                    let taken = std::mem::replace(&mut **value, Expr::Const(ConstVal::Null));
9842                    stmts[*w] = Stmt::LocalDef {
9843                        var: *v,
9844                        init: Some(taken),
9845                        is_final: false,
9846                        force_type: false,
9847                    };
9848                }
9849            }
9850        }
9851    }
9852    let mut order: Vec<usize> = plan.iter().map(|(_, q, _)| *q).collect();
9853    order.sort_unstable();
9854    order.reverse();
9855    for q in order {
9856        stmts.remove(q);
9857    }
9858}
9859
9860/// An INT-typed method returning a boolean value: the register held the
9861/// boolean test result in one generation and the `if (v) 1 else 0` int
9862/// in another; booleanize picks the boolean generation and the plain
9863/// `return v;` breaks ("boolean无法转换为int", weixin Handle
9864/// onPausableTransaction — the source was `if (v) 1 else 0`). Wrap the
9865/// return in the recovering conditional.
9866/// Mirror of fix_int_returns: a BOOLEAN method returning a numeric-
9867/// typed value (register reuse left an int generation) wraps it
9868/// `x != 0` — the dex branch encoding, and the only legal Java form
9869/// (kh5/a `return v116_g53;` int无法转换为boolean family).
9870pub fn fix_bool_returns(vt: &VarTable, body: &mut Stmt) {
9871    walk_mut_deep(body, &mut |st| {
9872        if let Stmt::Return(Some(e)) = st {
9873            let numeric = match &*e {
9874                Expr::Local { var, ty } => {
9875                    let t = if (*var as usize) < vt.vars.len() {
9876                        vt.var(*var).ty.erased()
9877                    } else {
9878                        ty.erased()
9879                    };
9880                    matches!(
9881                        t,
9882                        JavaType::Int
9883                            | JavaType::Long
9884                            | JavaType::Short
9885                            | JavaType::Byte
9886                            | JavaType::Char
9887                    )
9888                }
9889                other => matches!(
9890                    other.type_ref().erased(),
9891                    JavaType::Int | JavaType::Long | JavaType::Short | JavaType::Byte
9892                        | JavaType::Char
9893                ),
9894            };
9895            if numeric {
9896                let taken = e.clone();
9897                *e = Expr::Bin {
9898                    op: BinOp::Ne,
9899                    l: Box::new(taken),
9900                    r: Box::new(Expr::Const(ConstVal::Int(0))),
9901                    ty: Some(TypeRef::J(JavaType::Boolean)),
9902                };
9903            }
9904        }
9905    });
9906}
9907
9908pub fn fix_int_returns(vt: &VarTable, body: &mut Stmt) {
9909    walk_mut_deep(body, &mut |st| {
9910        if let Stmt::Return(Some(e)) = st {
9911            let is_bool = match e {
9912                Expr::Local { var, .. } => {
9913                    (*var as usize) < vt.vars.len()
9914                        && vt.vars[*var as usize].ty.erased() == JavaType::Boolean
9915                }
9916                Expr::Method { desc, .. } => desc.ret == JavaType::Boolean,
9917                _ => false,
9918            };
9919            if is_bool {
9920                *e = Expr::Cond {
9921                    c: Box::new(e.clone()),
9922                    t: Box::new(Expr::Const(ConstVal::Int(1))),
9923                    f: Box::new(Expr::Const(ConstVal::Int(0))),
9924                };
9925            }
9926        }
9927    });
9928}
9929
9930/// Generation splitting: when an assignment's value type is
9931/// INCOMPATIBLE with the var's declared type, mint a FRESH var for the
9932/// new generation instead of widening (widening was measured twice as
9933/// net-negative — every use-site cast gap re-triggers javac's
9934/// error-type contagion). Reads after the split point follow the
9935/// active generation; control-flow joins intersect; loops run a
9936/// 3-round fixpoint so the back-edge carries the tail generation to
9937/// the head reads (the loop-state-tuple shape, qf5/v0's
9938/// `u0 = (qf5.u0) get;` … `u0 = looper2;`).
9939///
9940/// Split condition (reference incompatibility or a primitive/value
9941/// mix — the instanceof-corruption shape): both specific references of
9942/// different classes, or prim→ref. NOT numeric-vs-numeric (promotion
9943/// handles those) and NOT Object targets (assignable — the narrowing
9944/// cast pass covers the value side).
9945pub fn split_generations(vt: &mut VarTable, body: &mut Stmt, pool: &DexPool) {
9946    let mut counter: u32 = 0;
9947    let mut gen: std::collections::HashMap<u32, u32> = std::collections::HashMap::default();
9948    split_walk_stmt(body, vt, &mut gen, &mut counter, pool);
9949}
9950
9951/// Should the assignment `target = value` split the target?
9952fn split_needed(vt: &VarTable, target: u32, value: &Expr, pool: &DexPool) -> bool {
9953    let n = vt.vars.len();
9954    if target as usize >= n {
9955        return false;
9956    }
9957    let declared = vt.vars[target as usize].ty.erased();
9958    let vt_is_ref = |t: &JavaType| matches!(t, JavaType::Object(_) | JavaType::Array(_));
9959    // Locals consult the VarTable: the embedded ty is a lift-time
9960    // snapshot, stale after booleanize converts a copy SOURCE (the
9961    // post-booleanize re-split below relies on seeing `v131` as the
9962    // boolean it became). Bitwise Or/And/Xor over boolean operands
9963    // carry the same staleness at the EXPRESSION level (dex lowered
9964    // Kotlin's non-short-circuit `or` to int `|` — the embedded Bin ty
9965    // stays Int after booleanize converts the operands): `int v18 =
9966    // delete | delete2` must split the target generation.
9967    fn side_bool(e: &Expr, vt: &VarTable) -> bool {
9968        match e {
9969            Expr::Local { var, .. } if (*var as usize) < vt.vars.len() => {
9970                matches!(vt.var(*var).ty.erased(), JavaType::Boolean)
9971            }
9972            Expr::Const(ConstVal::Int(0 | 1)) => true,
9973            Expr::Bin { op: BinOp::Or | BinOp::And | BinOp::Xor, l, r, .. } => {
9974                side_bool(l, vt) && side_bool(r, vt)
9975            }
9976            other => matches!(other.type_ref().erased(), JavaType::Boolean),
9977        }
9978    }
9979    let value_ty = match value {
9980        Expr::Local { var, ty } if (*var as usize) < vt.vars.len() => vt.var(*var).ty.erased(),
9981        Expr::Bin {
9982            op: BinOp::Or | BinOp::And | BinOp::Xor,
9983            l,
9984            r,
9985            ..
9986        } if side_bool(l, vt) && side_bool(r, vt) => JavaType::Boolean,
9987        other => other.type_ref().erased(),
9988    };
9989    if vt_is_ref(&declared) && vt_is_ref(&value_ty) {
9990        // Both references: split only when incompatible — different
9991        // classes where neither side is Object AND the value is not a
9992        // SUBTYPE of the declared type. An assignable store needs no
9993        // fresh generation; splitting one minted an orphan at an
9994        // if-join whose sibling arm kept the base (rename_gen_back's
9995        // primitive-only gate refused the undo, drop_dead_locals then
9996        // stripped the orphan to a bare allocation): pq0/g's bridge
9997        // `map2 = new LinkedHashMap()` against the HashMap-typed phi
9998        // rendered `new LinkedHashMap();` and the arm lost its store.
9999        let obj = JavaType::Object("java/lang/Object".into());
10000        if declared == value_ty || declared == obj || value_ty == obj {
10001            return false;
10002        }
10003        // A FRESH ALLOCATION whose class relation to the declared type
10004        // is UNKNOWABLE (a framework↔framework pair — LinkedHashMap
10005        // into a HashMap-typed phi): the dex stored the instance into
10006        // this register flow, so the store was verifier-legal; the
10007        // split cannot know better and orphaned the generation (the
10008        // if-join undo is primitive-only; the dropper stripped the
10009        // orphan to a bare `new LinkedHashMap();` and pq0/g's bridge
10010        // lost the else-arm store, killing the whole branched-super
10011        // merge). Pool-known pairs KEEP the split: the generation's
10012        // precise type is what downstream subtype sinks need (weibo
10013        // `v7 = new $reportWhenComplete$1(..)` into a ContinuationImpl
10014        // phi — exempting it made `v9($1) = v7` inconvertible, +50).
10015        if let (JavaType::Object(v), JavaType::Object(d)) = (&value_ty, &declared) {
10016            if matches!(value, Expr::New { .. })
10017                && (pool.get(v).is_none() || pool.get(d).is_none())
10018            {
10019                return false;
10020            }
10021        }
10022        true
10023    } else {
10024        // Primitive/reference mix: the value generation has a kind the
10025        // target can never accept — split (the declared type stays with
10026        // the old generation's readers).
10027        if vt_is_ref(&declared) != vt_is_ref(&value_ty) {
10028            return true;
10029        }
10030        // Boolean/numeric mix: `boolean` and `int` have NO cast between
10031        // them in Java, so a register reused across the kinds can only
10032        // be expressed as two generations (weixin ConstraintLayout
10033        // `boolean v60; int v56; v60 = v56;` — 9.5k bool↔int lines once
10034        // the obscuring suppression lifted). 0/1 constants are the
10035        // boolean encoding, not a numeric generation — never split on
10036        // those (booleanize owns them).
10037        let d_bool = matches!(declared, JavaType::Boolean);
10038        let v_bool = matches!(value_ty, JavaType::Boolean);
10039        let zero_one = matches!(value, Expr::Const(ConstVal::Int(0 | 1)));
10040        d_bool != v_bool && !zero_one
10041    }
10042}
10043
10044fn rewrite_gen_reads(e: &mut Expr, gen: &std::collections::HashMap<u32, u32>, vt: &VarTable) {
10045    deep_rewrite(e, &mut |x| {
10046        if let Expr::Local { var, ty } = x {
10047            if let Some(&to) = gen.get(var) {
10048                if to != *var {
10049                    *var = to;
10050                    *ty = vt.var(to).ty.clone();
10051                }
10052            }
10053        }
10054    });
10055}
10056
10057fn do_split(
10058    vt: &mut VarTable,
10059    target: &mut Expr,
10060    old: u32,
10061    value_ty: &JavaType,
10062    gen: &mut std::collections::HashMap<u32, u32>,
10063    counter: &mut u32,
10064) {
10065    *counter += 1;
10066    let info = &vt.vars[old as usize];
10067    let id = vt.vars.len() as u32;
10068    let name = format!("{}_g{}", info.name, counter);
10069    let slot = info.slot;
10070    // A split generation is a fresh LOCAL, not a parameter — even when
10071    // the parent register is one (the param identity stays on the
10072    // original var id; the generation only carries a later write). The
10073    // inherited flag made booleanize (and every other param-skip pass)
10074    // permanently ignore param-register generations: weixin v2/i's
10075    // `int v264_g160_g7 = !v200` with `!= 0` reads (bool→int assign
10076    // family, 2.8k lines).
10077    let is_param = false;
10078    vt.vars.push(jdc_core::var::VarInfo {
10079        id,
10080        slot,
10081        name,
10082        ty: TypeRef::J(value_ty.clone()),
10083        is_param,
10084        range_start: 0,
10085        range_end: u16::MAX,
10086        synthetic_name: true,
10087    });
10088    while vt.by_slot.len() <= slot as usize {
10089        vt.by_slot.push(Vec::new());
10090    }
10091    vt.by_slot[slot as usize].push((0, u16::MAX, id));
10092    gen.insert(old, id);
10093    if let Expr::Local { var, ty, .. } = target {
10094        *var = id;
10095        *ty = TypeRef::J(value_ty.clone());
10096    }
10097}
10098
10099/// Undo a DIVERGED generation split: rewrite every occurrence of the
10100/// freshly-minted generation `from` back to the pre-if var `to` within
10101/// `s` (both read positions and assignment targets, plus LocalDef var
10102/// fields). At a control-flow join only splits BOTH branches produced
10103/// survive (the phi merges on the shared var); a gen one branch minted
10104/// alone does not. But `do_split` already rewrote that branch's write
10105/// target IN PLACE, so without this undo the branch stores into a
10106/// write-only orphan gen that no confluence reader ever reads — the
10107/// value is silently lost AND the orphan renders undeclared (kt5.b
10108/// Kotlin default-arg bridge: `v18_g1 = p6x` in then vs `v18 = false`
10109/// in else, `this(.., v18, ..)` reads only else's value).
10110fn rename_gen_back(s: &mut Stmt, from: u32, to: u32, vt: &VarTable) {
10111    if from == to || (to as usize) >= vt.vars.len() {
10112        return;
10113    }
10114    let to_ty = vt.vars[to as usize].ty.clone();
10115    walk_mut_deep(s, &mut |st| {
10116        if let Stmt::LocalDef { var, .. } = st {
10117            if *var == from {
10118                *var = to;
10119            }
10120        }
10121    });
10122    rewrite_exprs(s, &mut |e| {
10123        deep_rewrite(e, &mut |x| {
10124            if let Expr::Local { var, ty } = x {
10125                if *var == from {
10126                    *var = to;
10127                    *ty = to_ty.clone();
10128                }
10129            }
10130        });
10131    });
10132}
10133
10134/// Remove an undone generation's `by_slot` segment entry so slot-based
10135/// resolution can never hand back the now-unreferenced gen id (its
10136/// VarInfo stays — ids index `vars` — but nothing may select it).
10137fn drop_gen_slot(vt: &mut VarTable, dead: u32) {
10138    if (dead as usize) >= vt.vars.len() {
10139        return;
10140    }
10141    let slot = vt.vars[dead as usize].slot as usize;
10142    if let Some(seg) = vt.by_slot.get_mut(slot) {
10143        seg.retain(|&(_, _, id)| id != dead);
10144    }
10145}
10146
10147/// Type gate for `rename_gen_back`: merging gen `v` back onto base `k`
10148/// is only safe when the base can carry the gen's value — identical
10149/// types, or BOTH primitives (the bool/int register-reuse family that
10150/// booleanize + the cast fixers reconcile; the Kotlin bridge case,
10151/// kt5.b `int v18` receiving `boolean p6x`). A reference-vs-primitive
10152/// or cross-class merge would forge `String k = <boolean>`-style
10153/// assignments that never compile (the lark/weibo/reqable regression
10154/// of the ungated first cut: +8.4k `不兼容的类型`).
10155fn undo_type_safe(vt: &VarTable, k: u32, v: u32) -> bool {
10156    if (k as usize) >= vt.vars.len() || (v as usize) >= vt.vars.len() {
10157        return false;
10158    }
10159    let kt = vt.vars[k as usize].ty.erased();
10160    let vt_ty = vt.vars[v as usize].ty.erased();
10161    if kt == vt_ty {
10162        return true;
10163    }
10164    let is_prim = |t: &JavaType| {
10165        matches!(
10166            t,
10167            JavaType::Int
10168                | JavaType::Long
10169                | JavaType::Short
10170                | JavaType::Byte
10171                | JavaType::Char
10172                | JavaType::Float
10173                | JavaType::Double
10174                | JavaType::Boolean
10175        )
10176    };
10177    is_prim(&kt) && is_prim(&vt_ty)
10178}
10179
10180fn split_walk_stmt(
10181    st: &mut Stmt,
10182    vt: &mut VarTable,
10183    gen: &mut std::collections::HashMap<u32, u32>,
10184    counter: &mut u32,
10185    pool: &DexPool,
10186) {
10187    match st {
10188        Stmt::Block(v) => {
10189            for x in v.iter_mut() {
10190                split_walk_stmt(x, vt, gen, counter, pool);
10191            }
10192        }
10193        Stmt::ExprStmt(e) => {
10194            rewrite_gen_reads(e, gen, vt);
10195            if let Expr::Assign {
10196                target,
10197                value,
10198                op: AssignOp::Plain,
10199                ..
10200            } = e
10201            {
10202                if let Expr::Local { var, .. } = &**target {
10203                    let old = *var;
10204                    if split_needed(vt, old, value, pool) {
10205                        let value_ty = value.type_ref().erased();
10206                        do_split(vt, target, old, &value_ty, gen, counter);
10207                    }
10208                }
10209            }
10210        }
10211        Stmt::LocalDef { var, init, .. } => {
10212            if let Some(e) = init {
10213                rewrite_gen_reads(e, gen, vt);
10214                if split_needed(vt, *var, e, pool) {
10215                    // The DEF mints its own generation: retarget the def
10216                    // to a fresh var (the bare decl of the ORIGINAL stays
10217                    // for its earlier readers).
10218                    let value_ty = e.type_ref().erased();
10219                    let old = *var;
10220                    let mut fake = Expr::Local {
10221                        var: old,
10222                        ty: vt.vars[old as usize].ty.clone(),
10223                    };
10224                    do_split(vt, &mut fake, old, &value_ty, gen, counter);
10225                    if let Expr::Local { var: nv, .. } = fake {
10226                        *var = nv;
10227                    }
10228                }
10229            }
10230        }
10231        Stmt::If {
10232            cond,
10233            then_stmt,
10234            else_stmt,
10235            ..
10236        } => {
10237            rewrite_gen_reads(cond, gen, vt);
10238            let then_floor = vt.vars.len() as u32;
10239            let mut g_then = gen.clone();
10240            split_walk_stmt(then_stmt, vt, &mut g_then, counter, pool);
10241            let else_floor = vt.vars.len() as u32;
10242            let mut g_else = gen.clone();
10243            if let Some(e) = else_stmt {
10244                split_walk_stmt(e, vt, &mut g_else, counter, pool);
10245            }
10246            // Undo splits that DIVERGED at this join (see
10247            // rename_gen_back), with TWO gates learned the hard way
10248            // (first cut regressed lark +8.4k):
10249            //  1. the SIBLING KEPT THE BASE — `g_else[k]` equals the
10250            //     entry mapping, i.e. only this branch minted a fresh
10251            //     gen for k. When both branches split, each orphan is
10252            //     declared by ensure_declared at its own value type and
10253            //     compiles; renaming them back onto the base would
10254            //     force cross-kind assignments (`int k = <boolean>`).
10255            //  2. the fresh gen is WRITE-ONLY in the branch — a gen
10256            //     read inside its branch renders fine on its own.
10257            // The gated shape is the Kotlin default-arg bridge phi
10258            // loss: then `v_g = p6x` / else `v = false` / confluence
10259            // reads v — the then value was silently dropped AND v_g
10260            // rendered undeclared (kt5.b). Undo restores the clean
10261            // diamond so fold_default_arg_bridge can fire.
10262            let then_reads = (else_floor > then_floor)
10263                .then(|| read_counts_all(std::slice::from_ref(&**then_stmt)));
10264            let mut undo_then: Vec<(u32, u32)> = g_then
10265                .iter()
10266                .filter(|(&k, &v)| {
10267                    v >= then_floor
10268                        && v < else_floor
10269                        && g_else.get(&k).copied() == gen.get(&k).copied()
10270                        && undo_type_safe(vt, k, v)
10271                        && then_reads
10272                            .as_ref()
10273                            .map(|m| m.get(&v).copied().unwrap_or(0) == 0)
10274                            .unwrap_or(true)
10275                })
10276                .map(|(&k, &v)| (v, k))
10277                .collect();
10278            undo_then.sort_unstable_by_key(|&(v, _)| std::cmp::Reverse(v));
10279            for &(v, k) in &undo_then {
10280                rename_gen_back(then_stmt, v, k, vt);
10281                drop_gen_slot(vt, v);
10282            }
10283            if let Some(e) = else_stmt.as_deref_mut() {
10284                let else_reads = ((vt.vars.len() as u32) > else_floor)
10285                    .then(|| read_counts_all(std::slice::from_ref(&*e)));
10286                let mut undo_else: Vec<(u32, u32)> = g_else
10287                    .iter()
10288                    .filter(|(&k, &v)| {
10289                        v >= else_floor
10290                            && g_then.get(&k).copied() == gen.get(&k).copied()
10291                            && undo_type_safe(vt, k, v)
10292                            && else_reads
10293                                .as_ref()
10294                                .map(|m| m.get(&v).copied().unwrap_or(0) == 0)
10295                                .unwrap_or(true)
10296                    })
10297                    .map(|(&k, &v)| (v, k))
10298                    .collect();
10299                undo_else.sort_unstable_by_key(|&(v, _)| std::cmp::Reverse(v));
10300                for &(v, k) in &undo_else {
10301                    rename_gen_back(e, v, k, vt);
10302                    drop_gen_slot(vt, v);
10303                }
10304            }
10305            // Join: keep only splits BOTH paths produced (an absent else
10306            // is a fall-through path that keeps the pre-if generation).
10307            gen.retain(|k, v| g_then.get(k) == Some(v) && g_else.get(k) == Some(v));
10308        }
10309        Stmt::While { cond, body, .. } | Stmt::DoWhile { cond, body, .. } => {
10310            // 3-round fixpoint: round N walks the cond+body with the
10311            // accumulated gen — the back-edge carries the tail
10312            // generation into the head reads of the next round.
10313            let entry = gen.clone();
10314            for _ in 0..3 {
10315                rewrite_gen_reads(cond, gen, vt);
10316                split_walk_stmt(body, vt, gen, counter, pool);
10317            }
10318            // After the loop the pre-loop generation is what a
10319            // zero-iteration execution left — conservative.
10320            *gen = entry;
10321        }
10322        Stmt::For { init, body, .. } => {
10323            for x in init.iter_mut() {
10324                split_walk_stmt(x, vt, gen, counter, pool);
10325            }
10326            let entry = gen.clone();
10327            for _ in 0..3 {
10328                split_walk_stmt(body, vt, gen, counter, pool);
10329            }
10330            *gen = entry;
10331        }
10332        Stmt::ForEach { body, .. } => {
10333            let entry = gen.clone();
10334            for _ in 0..3 {
10335                split_walk_stmt(body, vt, gen, counter, pool);
10336            }
10337            *gen = entry;
10338        }
10339        Stmt::Switch { cases, default, .. } => {
10340            let entry = gen.clone();
10341            for c in cases.iter_mut() {
10342                let mut g_case = entry.clone();
10343                for x in c.body.iter_mut() {
10344                    split_walk_stmt(x, vt, &mut g_case, counter, pool);
10345                }
10346                gen.retain(|k, v| g_case.get(k) == Some(v));
10347            }
10348            if let Some(d) = default {
10349                let mut g_def = entry.clone();
10350                split_walk_stmt(d, vt, &mut g_def, counter, pool);
10351                gen.retain(|k, v| g_def.get(k) == Some(v));
10352            }
10353        }
10354        Stmt::Try {
10355            body,
10356            catches,
10357            finally,
10358        } => {
10359            let entry = gen.clone();
10360            let mut g_try = entry.clone();
10361            split_walk_stmt(body, vt, &mut g_try, counter, pool);
10362            let mut g_all = g_try.clone();
10363            for c in catches.iter_mut() {
10364                // The handler runs with the PRE-try state (the exception
10365                // may fire anywhere in the try).
10366                let mut g_c = entry.clone();
10367                split_walk_stmt(&mut c.body, vt, &mut g_c, counter, pool);
10368                g_all.retain(|k, v| g_c.get(k) == Some(v));
10369            }
10370            *gen = g_all;
10371            if let Some(f) = finally {
10372                split_walk_stmt(f, vt, gen, counter, pool);
10373            }
10374        }
10375        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => {
10376            split_walk_stmt(body, vt, gen, counter, pool);
10377        }
10378        Stmt::Return(Some(e)) | Stmt::Throw(e) => {
10379            rewrite_gen_reads(e, gen, vt);
10380        }
10381        _ => {}
10382    }
10383}
10384
10385/// Framework exception super edge — the embedded API database
10386/// (fwdb) replaced the hand-written ~110-entry JDK/Android exception
10387/// supers table: real extends edges for EVERY framework class, so
10388/// multi-catch dedupe now covers chains the table missed (and cannot
10389/// carry a wrong entry — the table's failure mode was dropping a live
10390/// alternative).
10391fn fw_super(c: &str) -> Option<&'static str> {
10392    crate::fwdb::super_of(c)
10393}
10394
10395/// `sub <: sup` for exception-class names, combining the pool
10396/// hierarchy (which also matches framework ancestor NAMES on the way
10397/// up, so pool subclasses of framework exceptions resolve) with the
10398/// tabled framework chains for pairs where both sides are outside the
10399/// dex.
10400fn exc_subtype(sub: &str, sup: &str, pool: &DexPool) -> bool {
10401    if sub == sup {
10402        return true;
10403    }
10404    if pool.is_subtype(sub, sup) {
10405        return true;
10406    }
10407    let mut cur = sub;
10408    for _ in 0..24 {
10409        let Some(next) = fw_super(cur) else { break };
10410        if next == sup {
10411            return true;
10412        }
10413        cur = next;
10414    }
10415    false
10416}
10417
10418/// Drop multi-catch alternatives shadowed by another alternative in
10419/// the same handler (JLS 14.20.2 forbids subtype-related
10420/// alternatives; the dex verifier does not). Exact duplicates collapse
10421/// to the first occurrence. The caught set is unchanged, so this is
10422/// semantics-preserving by construction.
10423pub fn dedupe_multicatch(s: &mut Stmt, pool: &DexPool) {
10424    dedupe_multicatch_walk(s, pool);
10425}
10426
10427fn dedupe_multicatch_walk(s: &mut Stmt, pool: &DexPool) {
10428    match s {
10429        Stmt::Try {
10430            body,
10431            catches,
10432            finally,
10433        }
10434        | Stmt::TryWithResources {
10435            body,
10436            catches,
10437            finally,
10438            ..
10439        } => {
10440            dedupe_multicatch_walk(body, pool);
10441            for c in catches.iter_mut() {
10442                if c.exc.len() > 1 {
10443                    let mut keep: Vec<std::sync::Arc<str>> = Vec::with_capacity(c.exc.len());
10444                    for e in c.exc.iter() {
10445                        let shadowed = c.exc.iter().any(|o| {
10446                            o.as_ref() != e.as_ref() && exc_subtype(e, o, pool)
10447                        });
10448                        if !shadowed && !keep.iter().any(|k| k.as_ref() == e.as_ref()) {
10449                            keep.push(e.clone());
10450                        }
10451                    }
10452                    c.exc = keep;
10453                }
10454                dedupe_multicatch_walk(&mut c.body, pool);
10455            }
10456            if let Some(f) = finally {
10457                dedupe_multicatch_walk(f, pool);
10458            }
10459        }
10460        Stmt::Block(v) => {
10461            for x in v.iter_mut() {
10462                dedupe_multicatch_walk(x, pool);
10463            }
10464        }
10465        Stmt::If {
10466            then_stmt,
10467            else_stmt,
10468            ..
10469        } => {
10470            dedupe_multicatch_walk(then_stmt, pool);
10471            if let Some(e) = else_stmt {
10472                dedupe_multicatch_walk(e, pool);
10473            }
10474        }
10475        Stmt::While { body, .. }
10476        | Stmt::DoWhile { body, .. }
10477        | Stmt::For { body, .. }
10478        | Stmt::ForEach { body, .. }
10479        | Stmt::Labeled { body, .. }
10480        | Stmt::Synchronized { body, .. } => dedupe_multicatch_walk(body, pool),
10481        Stmt::Switch { cases, default, .. } => {
10482            for c in cases.iter_mut() {
10483                for x in c.body.iter_mut() {
10484                    dedupe_multicatch_walk(x, pool);
10485                }
10486            }
10487            if let Some(d) = default {
10488                dedupe_multicatch_walk(d, pool);
10489            }
10490        }
10491        _ => {}
10492    }
10493}
10494
10495/// Dangling `break L<id>`: a Goto whose paired Label/Labeled-wrap was
10496/// lost to structure degradation prints `break L<id>;` against an
10497/// undeclared label ("未定义的标签", weibo 371/lark 85/weixin 535).
10498/// Inside a loop the goto-to-loop-exit shape degrades to a plain
10499/// `break` — compilable, and the dominant original semantic. Outside
10500/// any loop the jump is irreducible flow (weixin protobuf state
10501/// machines: `goto` into a lost loop middle / dispatcher block — the
10502/// target exists but not at a position Java can express); it renders
10503/// as an honest `$DDC:` marker comment instead of a guaranteed
10504/// undefined-label error, and the block's tail after it is dropped
10505/// (the original control flow never reached those statements).
10506pub fn resolve_dangling_gotos(s: &mut Stmt) {
10507    let mut labels: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
10508    // walk_all is read-only — the whole-body clone this used to make
10509    // (4.6k profile samples of Stmt::clone) bought nothing.
10510    walk_all(s, &mut |st| {
10511        if let Stmt::Label(id) = st {
10512            labels.insert(*id);
10513        }
10514    });
10515    resolve_gotos_walk(s, &labels, 0);
10516}
10517
10518fn resolve_gotos_walk(s: &mut Stmt, labels: &jdc_core::FxHashSet<u32>, loop_depth: u32) {
10519    let depth = match s {
10520        Stmt::While { .. } | Stmt::DoWhile { .. } | Stmt::For { .. } | Stmt::ForEach { .. } => {
10521            loop_depth + 1
10522        }
10523        _ => loop_depth,
10524    };
10525    match s {
10526        Stmt::Goto(id) => {
10527            if labels.contains(id) {
10528                return; // valid pair — keep
10529            }
10530            if depth > 0 {
10531                *s = Stmt::Break(None);
10532            } else {
10533                *s = Stmt::Comment(format!(
10534                    "$DDC: unresolved jump to block {} (irreducible control flow)",
10535                    id
10536                ));
10537            }
10538        }
10539        Stmt::Block(v) => {
10540            let mut i = 0usize;
10541            while i < v.len() {
10542                // A depth-0 dangling Goto becomes a marker comment and
10543                // the original flow never reached the siblings after
10544                // it — dropping them keeps the rendered reachability
10545                // faithful (they were dead in the dex too).
10546                let dangling_here =
10547                    depth == 0 && matches!(&v[i], Stmt::Goto(id) if !labels.contains(id));
10548                resolve_gotos_walk(&mut v[i], labels, depth);
10549                if dangling_here {
10550                    v.truncate(i + 1);
10551                    break;
10552                }
10553                i += 1;
10554            }
10555        }
10556        Stmt::If { then_stmt, else_stmt, .. } => {
10557            resolve_gotos_walk(then_stmt, labels, depth);
10558            if let Some(e) = else_stmt {
10559                resolve_gotos_walk(e, labels, depth);
10560            }
10561        }
10562        Stmt::While { body, .. }
10563        | Stmt::DoWhile { body, .. }
10564        | Stmt::For { body, .. }
10565        | Stmt::ForEach { body, .. }
10566        | Stmt::Labeled { body, .. }
10567        | Stmt::Synchronized { body, .. } => resolve_gotos_walk(body, labels, depth),
10568        Stmt::Try {
10569            body,
10570            catches,
10571            finally,
10572        } => {
10573            resolve_gotos_walk(body, labels, depth);
10574            for c in catches.iter_mut() {
10575                resolve_gotos_walk(&mut c.body, labels, depth);
10576            }
10577            if let Some(f) = finally {
10578                resolve_gotos_walk(f, labels, depth);
10579            }
10580        }
10581        Stmt::Switch { cases, default, .. } => {
10582            for c in cases.iter_mut() {
10583                for x in c.body.iter_mut() {
10584                    resolve_gotos_walk(x, labels, depth);
10585                }
10586            }
10587            if let Some(d) = default {
10588                resolve_gotos_walk(d, labels, depth);
10589            }
10590        }
10591        _ => {}
10592    }
10593}
10594
10595/// READS of `v` across the statements (assignment targets excluded).
10596/// All-var read counts in ONE pass (reads semantics of stmts_read_var)
10597/// — for gates that test many candidate vars against the same subtree
10598/// (split_walk's undo filters ran a full arm walk PER candidate).
10599fn read_counts_all(stmts: &[Stmt]) -> std::collections::HashMap<u32, usize> {
10600    let mut m = std::collections::HashMap::new();
10601    for s in stmts {
10602        visit_stmt_exprs_ro(s, &mut |e| {
10603            visit_exprs_reads(e, &mut |x| {
10604                if let Expr::Local { var, .. } = x {
10605                    *m.entry(*var).or_insert(0) += 1;
10606                }
10607            });
10608        });
10609    }
10610    m
10611}
10612
10613fn stmts_read_var(stmts: &[Stmt], v: u32) -> usize {
10614    let mut n = 0usize;
10615    for s in stmts {
10616        visit_stmt_exprs_ro(s, &mut |e| {
10617            visit_exprs_reads(e, &mut |x| {
10618                if let Expr::Local { var: vv, .. } = x {
10619                    if *vv == v {
10620                        n += 1;
10621                    }
10622                }
10623            });
10624        });
10625    }
10626    n
10627}
10628
10629pub fn fix_ctor_conditional_super(body: &mut Stmt) {
10630    // A single-statement body can arrive UNWRAPPED (bare `If` — the
10631    // gb6/e throw-guard family); the shape scans need a statement list.
10632    if !matches!(body, Stmt::Block(_)) {
10633        let inner = std::mem::replace(body, Stmt::Block(Vec::new()));
10634        *body = Stmt::Block(vec![inner]);
10635    }
10636    let Stmt::Block(stmts) = body else { return };
10637    // Unwrap single-statement Block wrappers: the structurer can nest
10638    // the whole body one level deeper, hiding the top-level If from the
10639    // shape scans (cleanup would flatten it — but runs AFTER this pass).
10640    while stmts.len() == 1 {
10641        let Stmt::Block(inner) = &stmts[0] else { break };
10642        *stmts = inner.clone();
10643    }
10644    if stmts.is_empty() {
10645        return;
10646    }
10647    if is_bare_ctor_call(stmts.first().unwrap()) {
10648        dedupe_ctor_delegations(body);
10649        return;
10650    }
10651    if try_linear_super_inline(stmts) {
10652        return;
10653    }
10654    try_merge_branched_super(stmts);
10655}
10656
10657/// Shape C: the delegation is already first — strip duplicate copies
10658/// left by R8 path-duplication (Kotlin default-arg bridge ctors: ft5/j
10659/// had `super()` leading PLUS another inside a branch — "对super的调用
10660/// 必须是构造器中的第一个语句" at the copy). Every path delegates
10661/// through the leading call, so structurally-identical (or empty-args,
10662/// same-kind) copies nested in blocks/ifs are removed, never moved.
10663/// Runs both ahead of the shape-A/B attempt and AFTER the plain hoist
10664/// (fix_ctor_super_first) — the hoist is what puts the leading
10665/// delegation in place for path-duplicated ctors.
10666/// A ctor that ALREADY delegates in its first statement can hold no
10667/// other `this(..)`/`super(..)` — Baidu Titan hotpatch instrumentation
10668/// injects a conditional re-delegation inside the `if ($ic != null)`
10669/// guard of every ctor (baidusearch ctor-not-first ×61,922 across
10670/// 21,813 files: `super(context); if ($ic != null) { ..; if
10671/// ((flag&1)!=0) { super((Context) callArgs[0]); ..; return; } }`).
10672/// The guard is dead code on the unpatched runtime path (and the
10673/// decompiled output has no patch runtime at all); dropping the
10674/// secondary delegation keeps the real semantics and restores the
10675/// Java first-statement rule. Runs after dedupe_ctor_delegations
10676/// (identical copies) and before extract_branched_delegation_helper
10677/// — gated on a LEADING delegation, so branched-delegation ctors
10678/// (no leading one) still get the helper treatment. Nested class
10679/// declarations are separate ctor scopes and are not entered.
10680pub fn strip_secondary_delegations(body: &mut Stmt) {
10681    let Stmt::Block(stmts) = body else { return };
10682    if !stmts.first().is_some_and(is_bare_ctor_call) {
10683        return;
10684    }
10685    fn strip_in(s: &mut Stmt) {
10686        if is_bare_ctor_call(s) {
10687            *s = Stmt::Block(Vec::new());
10688            return;
10689        }
10690        match s {
10691            Stmt::Block(v) => v.iter_mut().for_each(strip_in),
10692            Stmt::If {
10693                then_stmt,
10694                else_stmt,
10695                ..
10696            } => {
10697                strip_in(then_stmt);
10698                if let Some(e) = else_stmt {
10699                    strip_in(e);
10700                }
10701            }
10702            Stmt::While { body, .. }
10703            | Stmt::DoWhile { body, .. }
10704            | Stmt::ForEach { body, .. }
10705            | Stmt::Synchronized { body, .. }
10706            | Stmt::Labeled { body, .. } => strip_in(body),
10707            Stmt::For { init, body, .. } => {
10708                init.iter_mut().for_each(strip_in);
10709                strip_in(body);
10710            }
10711            Stmt::Switch { cases, default, .. } => {
10712                for c in cases.iter_mut() {
10713                    c.body.iter_mut().for_each(strip_in);
10714                }
10715                if let Some(d) = default {
10716                    strip_in(d);
10717                }
10718            }
10719            Stmt::Try {
10720                body,
10721                catches,
10722                finally,
10723            }
10724            | Stmt::TryWithResources {
10725                body,
10726                catches,
10727                finally,
10728                ..
10729            } => {
10730                strip_in(body);
10731                for c in catches.iter_mut() {
10732                    strip_in(&mut c.body);
10733                }
10734                if let Some(f) = finally {
10735                    strip_in(f);
10736                }
10737            }
10738            _ => {}
10739        }
10740    }
10741    for st in stmts.iter_mut().skip(1) {
10742        strip_in(st);
10743    }
10744}
10745
10746pub fn dedupe_ctor_delegations(body: &mut Stmt) {
10747    let Stmt::Block(stmts) = body else { return };
10748    let Some(d0) = stmts.first().and_then(|s| match s {
10749        Stmt::ExprStmt(e) if is_delegation_expr(e) => Some(e.clone()),
10750        _ => None,
10751    }) else {
10752        return;
10753    };
10754    fn rec(s: &mut Stmt, d0: &Expr) {
10755        match s {
10756            Stmt::Block(v) => {
10757                v.retain(|x| {
10758                    !matches!(x, Stmt::ExprStmt(e) if is_delegation_expr(e) && same_delegation(e, d0))
10759                });
10760                for x in v.iter_mut() {
10761                    rec(x, d0);
10762                }
10763            }
10764            Stmt::If { then_stmt, else_stmt, .. } => {
10765                rec(then_stmt, d0);
10766                if let Some(e) = else_stmt {
10767                    rec(e, d0);
10768                }
10769            }
10770            _ => {}
10771        }
10772    }
10773    // Top-level copies first (rec only strips copies NESTED in
10774    // blocks/ifs — a duplicate `super()` sitting directly in the body
10775    // list fell through its `_` arm: weixin ft5/j's second super()).
10776    {
10777        let d0r = &d0;
10778        let mut idx = 0usize;
10779        stmts.retain(|s| {
10780            idx += 1;
10781            idx == 1
10782                || !(matches!(s, Stmt::ExprStmt(e)
10783                    if is_delegation_expr(e) && same_delegation(e, d0r)))
10784        });
10785    }
10786    for s in stmts.iter_mut().skip(1) {
10787        rec(s, &d0);
10788    }
10789}
10790
10791/// Structural delegation identity for dedup: same kind/target and equal
10792/// args — or both arg-less (`super()` copies across duplicated paths
10793/// render with locally-renamed args yet delegate identically).
10794fn same_delegation(a: &Expr, b: &Expr) -> bool {
10795    match (a, b) {
10796        (
10797            Expr::Method { name: n1, cls: c1, is_super: s1, args: a1, .. },
10798            Expr::Method { name: n2, cls: c2, is_super: s2, args: a2, .. },
10799        ) => {
10800            &**n1 == "<init>"
10801                && &**n2 == "<init>"
10802                && c1 == c2
10803                && s1 == s2
10804                && (a1 == a2 || (a1.is_empty() && a2.is_empty()))
10805        }
10806        _ => false,
10807    }
10808}
10809
10810/// The `Method` expr of a bare ctor-delegation statement.
10811fn ctor_call_expr(s: &Stmt) -> Option<&Expr> {
10812    match s {
10813        Stmt::ExprStmt(e) if is_delegation_expr(e) => Some(e),
10814        _ => None,
10815    }
10816}
10817
10818/// Same delegation kind ignoring args (super vs this, same target).
10819fn same_ctor_kind(a: &Expr, b: &Expr) -> bool {
10820    match (a, b) {
10821        (
10822            Expr::Method { cls: c1, is_super: s1, owner: o1, .. },
10823            Expr::Method { cls: c2, is_super: s2, owner: o2, .. },
10824        ) => c1 == c2 && s1 == s2 && o1 == o2,
10825        _ => false,
10826    }
10827}
10828
10829/// Statements of a branch body, recursively flattening pure Blocks —
10830/// the fix runs BEFORE cleanup, so structurer-emitted nesting is still
10831/// in place. Any non-Block statement (If/Try/...) stays an item and
10832/// fails the def/call classification, which is the intended abort.
10833fn flat_list(s: &Stmt) -> Vec<&Stmt> {
10834    fn rec<'a>(s: &'a Stmt, out: &mut Vec<&'a Stmt>) {
10835        match s {
10836            Stmt::Block(v) => {
10837                for x in v {
10838                    rec(x, out);
10839                }
10840            }
10841            other => out.push(other),
10842        }
10843    }
10844    let mut out = Vec::new();
10845    rec(s, &mut out);
10846    out
10847}
10848
10849/// Blank the OWNER of un-folded `<init>` calls (lost allocation): the
10850/// printer renders them as `new X(..)` and never reads the owner, so its
10851/// register reference is not a real use (v7.a$a2: the dead
10852/// `new f(0,..)`'s owner was v1's register, inflating v1's use count
10853/// and blocking the inline-hoist).
10854fn strip_lost_alloc_owners(e: &mut Expr) {
10855    deep_rewrite(e, &mut |x| {
10856        if let Expr::Method { name, is_special: true, owner: Some(o), .. } = x {
10857            if &**name == "<init>" && !matches!(**o, Expr::This) {
10858                **o = Expr::Const(jdc_core::ir::expr::ConstVal::Int(0));
10859            }
10860        }
10861    });
10862}
10863
10864/// Per-var `Local` occurrence counts over an expression (single pass;
10865/// lost-alloc `<init>` owners excluded — see strip_lost_alloc_owners).
10866fn count_locals_expr(e: &Expr) -> std::collections::HashMap<u32, usize> {
10867    let mut m = std::collections::HashMap::new();
10868    count_expr_ro(e, &mut m);
10869    m
10870}
10871
10872pub(crate) fn count_locals_stmts(ss: &[Stmt]) -> std::collections::HashMap<u32, usize> {
10873    let mut m = std::collections::HashMap::new();
10874    for s in ss {
10875        visit_stmt_exprs_ro(s, &mut |e| count_expr_ro(e, &mut m));
10876    }
10877    m
10878}
10879
10880fn cnt(m: &std::collections::HashMap<u32, usize>, v: u32) -> usize {
10881    m.get(&v).copied().unwrap_or(0)
10882}
10883
10884/// Substitute map-referenced locals, recursively expanding def chains
10885/// (depth-capped). Returns None when a referenced var has no def or the
10886/// chain is too deep.
10887fn inline_locals(
10888    e: &Expr,
10889    map: &std::collections::HashMap<u32, Expr>,
10890    depth: u32,
10891) -> Option<Expr> {
10892    if depth > 4 {
10893        return None;
10894    }
10895    let mut out = e.clone();
10896    let mut fail = false;
10897    deep_rewrite(&mut out, &mut |x| {
10898        if let Expr::Local { var, .. } = x {
10899            if let Some(def) = map.get(var) {
10900                match inline_locals(def, map, depth + 1) {
10901                    Some(r) => *x = r,
10902                    None => fail = true,
10903                }
10904            }
10905        }
10906    });
10907    if fail {
10908        None
10909    } else {
10910        Some(out)
10911    }
10912}
10913
10914/// A leading statement that defines a local: (var, init expr).
10915fn def_of(s: &Stmt) -> Option<(u32, &Expr)> {
10916    match s {
10917        Stmt::LocalDef { var, init: Some(e), .. } => Some((*var, e)),
10918        Stmt::ExprStmt(Expr::Assign {
10919            target,
10920            value,
10921            op: jdc_core::ir::expr::AssignOp::Plain,
10922            ..
10923        }) => match &**target {
10924            Expr::Local { var, .. } => Some((*var, value)),
10925            _ => None,
10926        },
10927        _ => None,
10928    }
10929}
10930
10931/// Shape A: straight-line prefix, delegation references prefix locals.
10932fn try_linear_super_inline(stmts: &mut Vec<Stmt>) -> bool {
10933    let Some(pos) = stmts.iter().position(is_bare_ctor_call) else {
10934        return false;
10935    };
10936    if pos == 0 {
10937        return false; // already first — nothing to do
10938    }
10939    // The prefix must be straight-line.
10940    if !stmts[..pos]
10941        .iter()
10942        .all(|s| matches!(s, Stmt::LocalDef { .. } | Stmt::ExprStmt(_)))
10943    {
10944        return false;
10945    }
10946    let Some(call) = ctor_call_expr(&stmts[pos]) else {
10947        return false;
10948    };
10949    let Expr::Method { args, .. } = call else {
10950        return false;
10951    };
10952    // Defs reachable from the args (last definition wins, bytecode order).
10953    let mut map: std::collections::HashMap<u32, Expr> =
10954        std::collections::HashMap::new();
10955    let mut def_idx: std::collections::HashMap<u32, usize> =
10956        std::collections::HashMap::new();
10957    for (i, s) in stmts[..pos].iter().enumerate() {
10958        if let Some((v, e)) = def_of(s) {
10959            map.insert(v, e.clone());
10960            def_idx.insert(v, i);
10961        }
10962    }
10963    // Which arg locals need inlining?
10964    let mut needed: Vec<u32> = Vec::new();
10965    for a in args {
10966        let mut probe = a.clone();
10967        strip_lost_alloc_owners(&mut probe);
10968        deep_rewrite(&mut probe, &mut |x| {
10969            if let Expr::Local { var, .. } = x {
10970                if map.contains_key(var) && !needed.contains(var) {
10971                    needed.push(*var);
10972                }
10973            }
10974        });
10975    }
10976    if needed.is_empty() {
10977        return false; // plain hoist handles it
10978    }
10979    // Guard: side-effecting defs inline only at a provably preserved
10980    // call count (exactly one use, the arg site).
10981    let mut consumed: Vec<usize> = Vec::new();
10982    let use_counts = count_locals_stmts(stmts);
10983    for &v in &needed {
10984        let def = &map[&v];
10985        let uses = cnt(&use_counts, v);
10986        if jdc_core::ir::build::has_side_effects(def) && uses != 1 {
10987            return false;
10988        }
10989        if uses == 1 {
10990            consumed.push(def_idx[&v]);
10991        }
10992    }
10993    // Build the inlined args.
10994    let mut new_args: Vec<Expr> = Vec::with_capacity(args.len());
10995    for a in args {
10996        match inline_locals(a, &map, 0) {
10997            Some(r) => new_args.push(r),
10998            None => return false,
10999        }
11000    }
11001    // Rebuild: super first (with inlined args), the unconsumed prefix
11002    // statements keep their order after it, then the original rest.
11003    let mut call_stmt = stmts.remove(pos);
11004    if let Stmt::ExprStmt(Expr::Method { args: slot, .. }) = &mut call_stmt {
11005        *slot = new_args;
11006    }
11007    let mut rest: Vec<Stmt> = Vec::with_capacity(stmts.len() + 1);
11008    rest.push(call_stmt);
11009    for (i, s) in stmts.drain(..).enumerate() {
11010        // Indices shift after `remove(pos)`: consumed holds pre-removal
11011        // indices; pos itself is already gone from `stmts`.
11012        let orig = if i >= pos { i + 1 } else { i };
11013        if !consumed.contains(&orig) {
11014            rest.push(s);
11015        }
11016    }
11017    *stmts = rest;
11018    true
11019}
11020
11021/// Shape B: if/else branches each ending in the same-kind delegation.
11022fn try_merge_branched_super(stmts: &mut Vec<Stmt>) -> bool {
11023    // Candidate If positions — the prelude before the chosen If must be
11024    // straight-line; try each If until one merges (nested/multi-if
11025    // ctors exist but the first matching one is the delegation site).
11026    let if_positions: Vec<usize> = stmts
11027        .iter()
11028        .enumerate()
11029        .filter(|(_, s)| matches!(s, Stmt::If { .. }))
11030        .map(|(i, _)| i)
11031        .collect();
11032    for if_pos in if_positions {
11033        // Cheap allocation-free gate: at least one branch must hold a
11034        // delegation (most ctor ifs are field null-checks and must not
11035        // pay the merge machinery). The OTHER side may be delegation-free
11036        // (throw-guard shape D) or absent (then-only shape E — the
11037        // rendered if/else form often only exists after later passes).
11038        let gate = match &stmts[if_pos] {
11039            Stmt::If { then_stmt, else_stmt, .. } => {
11040                contains_delegation(then_stmt)
11041                    || else_stmt.as_ref().is_some_and(|e| contains_delegation(e))
11042            }
11043            _ => false,
11044        };
11045        if gate && merge_at(stmts, if_pos) {
11046            return true;
11047        }
11048    }
11049    false
11050}
11051
11052/// Does this subtree contain a ctor delegation? (module-level: shared by
11053/// the merge gate and split_branch)
11054fn contains_delegation(s: &Stmt) -> bool {
11055    match s {
11056        Stmt::ExprStmt(e) => is_delegation_expr(e),
11057        Stmt::Block(v) => v.iter().any(contains_delegation),
11058        Stmt::If { then_stmt, else_stmt, .. } => {
11059            contains_delegation(then_stmt)
11060                || else_stmt.as_ref().is_some_and(|e| contains_delegation(e))
11061        }
11062        _ => false,
11063    }
11064}
11065
11066
11067
11068/// One merge attempt at `stmts[if_pos]`. See fix_ctor_conditional_super
11069/// for the doctrine. Extensions over the pure two-call merge:
11070/// - branch pre-call SIDE-EFFECT statements (Kotlin `o.h(param,..)`
11071///   null-checks — the zz5/j0 family) are allowed; they move into the
11072///   branch remainder (after super — the same reordering doctrine the
11073///   plain hoist already applies; Java has no pre-super statement form).
11074/// - a branch WITHOUT a delegation (throw-guard: `if (x==null) throw
11075///   .. else { super(..); .. }` — the gb6/e family) merges as pure
11076///   remainder; exactly one branch carries the call then, and no
11077///   ternary args arise (the guard throws before any path needs them).
11078/// - prelude single-assignment if/else (`if (c) {v=e1} else {v=e2}`)
11079///   folds into a Cond def for inlining (the zz5/j0 v15/v16 preludes).
11080fn merge_at(stmts: &mut Vec<Stmt>, if_pos: usize) -> bool {
11081    /// Branch classification: Call(at, defs, pre-call extra idxs) when a
11082    /// delegation sits behind only defs/side-effect statements; Clean
11083    /// when the branch has no delegation at all (pure remainder); Dirty
11084    /// otherwise (nested/conditional delegation — out of scope).
11085    enum Br {
11086        /// (call index, pre-call defs, def statement indices)
11087        Call(usize, Vec<(u32, Expr)>, Vec<usize>),
11088        Clean,
11089        Dirty,
11090    }
11091    fn split_branch(list: &[&Stmt]) -> Br {
11092        let call_pos = list.iter().position(|s| ctor_call_expr(s).is_some());
11093        let Some(c) = call_pos else {
11094            // No delegation at this level: a pure remainder branch
11095            // (throw-guard) unless one hides in nested control flow.
11096            return if list.iter().any(|s| contains_delegation(s)) {
11097                Br::Dirty
11098            } else {
11099                Br::Clean
11100            };
11101        };
11102        if list.iter().skip(c + 1).any(|s| ctor_call_expr(s).is_some()) {
11103            return Br::Dirty; // two delegations in one branch
11104        }
11105        let mut defs: Vec<(u32, Expr)> = Vec::new();
11106        let mut def_idxs: Vec<usize> = Vec::new();
11107        for (i, s) in list.iter().enumerate().take(c) {
11108            match def_of(s) {
11109                Some((v, e)) => {
11110                    defs.push((v, e.clone()));
11111                    def_idxs.push(i);
11112                }
11113                None => match s {
11114                    // Side-effect statements and bare decls ahead of the
11115                    // delegation ride along into the remainder.
11116                    Stmt::ExprStmt(_) | Stmt::LocalDef { .. } => {}
11117                    _ => return Br::Dirty, // control flow pre-call
11118                },
11119            }
11120        }
11121        Br::Call(c, defs, def_idxs)
11122    }
11123    /// Prelude single-assignment if/else → `v = c ? e1 : e2` def.
11124    fn cond_def_of(s: &Stmt, scope: &[Stmt]) -> Option<(u32, Expr)> {
11125        let Stmt::If { cond, then_stmt, else_stmt: Some(e), .. } = s else {
11126            return None;
11127        };
11128        fn one<'x>(b: &'x Stmt, scope: &[Stmt]) -> Option<(u32, &'x Expr)> {
11129            let l = flat_list(b);
11130            if l.len() == 1 {
11131                return def_of(l[0]);
11132            }
11133            // A [w = E; v = w] forwarding PAIR — the shape the
11134            // structurer leaves when E is impure (`c v5 = new c(..);
11135            // v6 = v5`) and forward_single_use's adjacency pass missed
11136            // it (a duplicated read elsewhere in its raw body broke the
11137            // single-read gate). Collapse to `v = E`, gated on w being
11138            // method-unique: the merge DROPS this branch, so any other
11139            // read/write of w would dangle (lark gp2/d Kotlin default-
11140            // arg bridge, this-not-first family).
11141            if l.len() == 2 {
11142                let (w, ex) = def_of(l[0])?;
11143                let Stmt::ExprStmt(Expr::Assign {
11144                    target,
11145                    op: AssignOp::Plain,
11146                    value,
11147                }) = l[1]
11148                else {
11149                    return None;
11150                };
11151                let Expr::Local { var: tv, .. } = &**target else {
11152                    return None;
11153                };
11154                let Expr::Local { var: rv, .. } = &**value else {
11155                    return None;
11156                };
11157                if *rv != w || *tv == w {
11158                    return None;
11159                }
11160                // stmts_count_writes only sees Expr-level assigns; the
11161                // pair's own def is often an init-bearing LocalDef, so
11162                // count those separately for the uniqueness gate.
11163                let mut ldef_writes = 0usize;
11164                for st in scope {
11165                    walk_all(st, &mut |x| {
11166                        if let Stmt::LocalDef {
11167                            var,
11168                            init: Some(_),
11169                            ..
11170                        } = x
11171                        {
11172                            if *var == w {
11173                                ldef_writes += 1;
11174                            }
11175                        }
11176                    });
11177                }
11178                if stmts_read_var(scope, w) != 1
11179                    || stmts_count_writes(scope, w) + ldef_writes != 1
11180                {
11181                    return None;
11182                }
11183                return Some((*tv, ex));
11184            }
11185            None
11186        }
11187        let (v1, e1) = one(then_stmt, scope)?;
11188        let (v2, e2) = one(e, scope)?;
11189        if v1 != v2 {
11190            return None;
11191        }
11192        Some((
11193            v1,
11194            Expr::Cond {
11195                c: Box::new(cond.clone()),
11196                t: Box::new(e1.clone()),
11197                f: Box::new(e2.clone()),
11198            },
11199        ))
11200    }
11201
11202    // Prelude: defs (flat or folded cond), bare decls, side-effect stmts.
11203    let mut pmap: std::collections::HashMap<u32, Expr> =
11204        std::collections::HashMap::new();
11205    let mut pidx: std::collections::HashMap<u32, usize> =
11206        std::collections::HashMap::new();
11207    for (i, s) in stmts[..if_pos].iter().enumerate() {
11208        match s {
11209            Stmt::LocalDef { .. } | Stmt::ExprStmt(_) => {
11210                if let Some((v, e)) = def_of(s) {
11211                    pmap.insert(v, e.clone());
11212                    pidx.insert(v, i);
11213                }
11214            }
11215            Stmt::If { .. } => match cond_def_of(s, stmts) {
11216                Some((v, e)) => {
11217                    pmap.insert(v, e);
11218                    pidx.insert(v, i);
11219                }
11220                None => return false,
11221            },
11222            _ => return false,
11223        }
11224    }
11225    let (cond, then_v, else_v, had_else) = match &stmts[if_pos] {
11226        Stmt::If { cond, then_stmt, else_stmt, .. } => (
11227            cond.clone(),
11228            then_stmt.as_ref().clone(),
11229            else_stmt.as_ref().map_or_else(|| Stmt::Block(Vec::new()), |e| e.as_ref().clone()),
11230            else_stmt.is_some(),
11231        ),
11232        _ => return false,
11233    };
11234    // Chained guards (Kotlin multi-param null-checks):
11235    // `if(a){throw..} else { if(b){throw..} else { super(..); .. } }` —
11236    // a branch that is a lone delegation-bearing If gets merged
11237    // recursively first (its delegation hoisted to the branch head), so
11238    // the outer split then sees [call, nested-guard-remainder].
11239    fn normalize_branch(b: &mut Stmt) {
11240        // MOVE-based (no deep clone): chained weixin Parcel ctors carry
11241        // huge tails; cloning per recursion level cost measurable wall
11242        // time. merge_at leaves stmts untouched on failure, so the taken
11243        // node can always go back.
11244        let slot: &mut Stmt = match b {
11245            Stmt::Block(v) if v.len() == 1 => &mut v[0],
11246            other => other,
11247        };
11248        if !matches!(slot, Stmt::If { .. }) || !contains_delegation(slot) {
11249            return;
11250        }
11251        let taken = std::mem::replace(slot, Stmt::Block(Vec::new()));
11252        let mut tmp = vec![taken];
11253        if merge_at(&mut tmp, 0) {
11254            *slot = Stmt::Block(tmp);
11255        } else {
11256            *slot = tmp.pop().unwrap();
11257        }
11258    }
11259    let mut then_v = then_v;
11260    let mut else_v = else_v;
11261    normalize_branch(&mut then_v);
11262    normalize_branch(&mut else_v);
11263    let then_list = flat_list(&then_v);
11264    let else_list = flat_list(&else_v);
11265
11266    // Exactly: two Call branches (merge with ternaries) or one Call +
11267    // one Clean (single-delegation throw-guard). Owned branch payloads.
11268    enum Side {
11269        Call {
11270            at: usize,
11271            defs: Vec<(u32, Expr)>,
11272            def_idxs: Vec<usize>,
11273        },
11274        Clean,
11275    }
11276    let (t_side, e_side, two_sided) = match (split_branch(&then_list), split_branch(&else_list)) {
11277        (Br::Call(tc, td, ti), Br::Call(ec, ed, ei)) => (
11278            Side::Call { at: tc, defs: td, def_idxs: ti },
11279            Side::Call { at: ec, defs: ed, def_idxs: ei },
11280            true,
11281        ),
11282        (Br::Call(tc, td, ti), Br::Clean) => (
11283            Side::Call { at: tc, defs: td, def_idxs: ti },
11284            Side::Clean,
11285            false,
11286        ),
11287        (Br::Clean, Br::Call(ec, ed, ei)) => (
11288            Side::Clean,
11289            Side::Call { at: ec, defs: ed, def_idxs: ei },
11290            false,
11291        ),
11292        _ => return false,
11293    };
11294    // The delegation nodes (two-sided: both; single: one).
11295    let (t_call, e_call) = match (&t_side, &e_side) {
11296        (Side::Call { at: ta, .. }, Side::Call { at: ea, .. }) => (
11297            ctor_call_expr(then_list[*ta]),
11298            ctor_call_expr(else_list[*ea]),
11299        ),
11300        (Side::Call { at: ta, .. }, Side::Clean) => {
11301            (ctor_call_expr(then_list[*ta]), None)
11302        }
11303        (Side::Clean, Side::Call { at: ea, .. }) => {
11304            (None, ctor_call_expr(else_list[*ea]))
11305        }
11306        _ => return false,
11307    };
11308    // The node that becomes the merged call: then side when present.
11309    let (call_list, call_at) = match &t_side {
11310        Side::Call { at, .. } => (&then_list, *at),
11311        Side::Clean => match &e_side {
11312            Side::Call { at, .. } => (&else_list, *at),
11313            Side::Clean => return false,
11314        },
11315    };
11316    let (base_args, other_args) = if two_sided {
11317        let (Some(tc), Some(ec)) = (t_call, e_call) else {
11318            return false;
11319        };
11320        if !same_ctor_kind(tc, ec) {
11321            return false;
11322        }
11323        let (Expr::Method { args: a1, .. }, Expr::Method { args: a2, .. }) = (tc, ec) else {
11324            return false;
11325        };
11326        if a1.len() != a2.len() {
11327            return false;
11328        }
11329        (a1, Some(a2))
11330    } else {
11331        let c = t_call.or(e_call);
11332        let Some(c) = c else { return false };
11333        let Expr::Method { args, .. } = c else {
11334            return false;
11335        };
11336        (args, None)
11337    };
11338
11339    // Def maps per side (prelude + own branch defs).
11340    let mut tmap: std::collections::HashMap<u32, Expr> = pmap.clone();
11341    let mut emap: std::collections::HashMap<u32, Expr> = pmap.clone();
11342    if let Side::Call { defs, .. } = &t_side {
11343        for (v, e) in defs {
11344            tmap.insert(*v, e.clone());
11345        }
11346    }
11347    if let Side::Call { defs, .. } = &e_side {
11348        for (v, e) in defs {
11349            emap.insert(*v, e.clone());
11350        }
11351    }
11352    let Some(cond_i) = inline_locals(&cond, &pmap, 0) else {
11353        return false;
11354    };
11355    // Inline each side's args through its own map.
11356    let mut then_args: Vec<Expr> = Vec::with_capacity(base_args.len());
11357    let mut else_args: Vec<Expr> = Vec::new();
11358    if two_sided {
11359        for a in base_args {
11360            match inline_locals(a, &tmap, 0) {
11361                Some(r) => then_args.push(r),
11362                None => return false,
11363            }
11364        }
11365        for a in other_args.unwrap() {
11366            match inline_locals(a, &emap, 0) {
11367                Some(r) => else_args.push(r),
11368                None => return false,
11369            }
11370        }
11371    } else {
11372        // Single-delegation side: base_args belong to whichever side
11373        // carries the call; pick that side's map.
11374        let from_then = matches!(&t_side, Side::Call { .. });
11375        for a in base_args {
11376            let r = if from_then {
11377                inline_locals(a, &tmap, 0)
11378            } else {
11379                inline_locals(a, &emap, 0)
11380            };
11381            match r {
11382                Some(r) => then_args.push(r),
11383                None => return false,
11384            }
11385        }
11386    }
11387    let primary_args = base_args;
11388    // Per-position merge.
11389    let mut merged: Vec<Expr> = Vec::with_capacity(then_args.len());
11390    let mut differ = 0usize;
11391    for (j, t) in then_args.iter().enumerate() {
11392        if two_sided {
11393            let e = &else_args[j];
11394            if t == e {
11395                merged.push(t.clone());
11396            } else {
11397                differ += 1;
11398                merged.push(Expr::Cond {
11399                    c: Box::new(cond_i.clone()),
11400                    t: Box::new(t.clone()),
11401                    f: Box::new(e.clone()),
11402                });
11403            }
11404        } else {
11405            merged.push(t.clone());
11406        }
11407    }
11408
11409    // ---- side-effect preservation guards (batched counts) ----
11410    let vars_in = |e: &Expr| -> Vec<u32> {
11411        let mut vs = Vec::new();
11412        let mut probe = e.clone();
11413        strip_lost_alloc_owners(&mut probe);
11414        deep_rewrite(&mut probe, &mut |x| {
11415            if let Expr::Local { var, .. } = x {
11416                if !vs.contains(var) {
11417                    vs.push(*var);
11418                }
11419            }
11420        });
11421        vs
11422    };
11423    // Remainder statements per branch (extras + post-call tail; a Clean
11424    // branch contributes all of its statements).
11425    // The consumed defs LEAVE the remainder: their expressions were
11426    // inlined into the merged delegation args, and a dead `v = E` left
11427    // after the call both reads wrong and trips the u_t1/u_t2 guards
11428    // (ContinuationImpl's branch-local `context = null / getContext()`
11429    // — the coroutine family's 对this的调用必须是第一个语句 root).
11430    let branch_remainder = |list: &[&Stmt], side: &Side| -> Vec<Stmt> {
11431        match side {
11432            Side::Clean => list.iter().map(|s| (*s).clone()).collect(),
11433            Side::Call { at, def_idxs, .. } => {
11434                list.iter()
11435                    .enumerate()
11436                    .filter(|(i, _)| *i != *at && !def_idxs.contains(i))
11437                    .map(|(_, s)| (*s).clone())
11438                    .collect()
11439            }
11440        }
11441    };
11442    let tail_then_v = branch_remainder(&then_list, &t_side);
11443    let tail_else_v = branch_remainder(&else_list, &e_side);
11444    let post = &stmts[if_pos + 1..];
11445    let mut inlined: Vec<u32> = vars_in(&cond);
11446    for a in primary_args.iter().chain(else_args.iter()) {
11447        for v in vars_in(a) {
11448            if !inlined.contains(&v) {
11449                inlined.push(v);
11450            }
11451        }
11452    }
11453    inlined.retain(|v| pmap.contains_key(v) || tmap.contains_key(v) || emap.contains_key(v));
11454    // Prelude-and-branch redefinition: ambiguous provenance — reject.
11455    for &v in &inlined {
11456        let in_branch = match (&t_side, &e_side) {
11457            (Side::Call { defs: td, .. }, Side::Call { defs: ed, .. }) => {
11458                td.iter().any(|(d, _)| *d == v) || ed.iter().any(|(d, _)| *d == v)
11459            }
11460            (Side::Call { defs, .. }, Side::Clean)
11461            | (Side::Clean, Side::Call { defs, .. }) => {
11462                defs.iter().any(|(d, _)| *d == v)
11463            }
11464            _ => false,
11465        };
11466        if pmap.contains_key(&v) && in_branch {
11467            return false;
11468        }
11469    }
11470    type Counts = std::collections::HashMap<u32, usize>;
11471    type GuardCounts = (
11472        Counts,
11473        Vec<Counts>,
11474        Vec<Counts>,
11475        Counts,
11476        Counts,
11477        Counts,
11478        Vec<(u32, Counts)>,
11479    );
11480    let (cond_counts, a1_counts, a2_counts, tail1_counts, tail2_counts, post_counts, def_counts): GuardCounts =
11481        if inlined.is_empty() {
11482        Default::default()
11483    } else {
11484        let all_defs: Vec<(u32, &Expr)> = pmap
11485            .iter()
11486            .map(|(k, v)| (*k, v))
11487            .chain(match &t_side {
11488                Side::Call { defs, .. } => defs.iter().map(|(k, v)| (*k, v)).collect(),
11489                Side::Clean => Vec::new(),
11490            })
11491            .chain(match &e_side {
11492                Side::Call { defs, .. } => defs.iter().map(|(k, v)| (*k, v)).collect(),
11493                Side::Clean => Vec::new(),
11494            })
11495            .collect();
11496        (
11497            count_locals_expr(&cond),
11498            primary_args.iter().map(count_locals_expr).collect(),
11499            else_args.iter().map(count_locals_expr).collect(),
11500            count_locals_stmts(&tail_then_v),
11501            count_locals_stmts(&tail_else_v),
11502            count_locals_stmts(post),
11503            all_defs.into_iter().map(|(v, e)| (v, count_locals_expr(e))).collect(),
11504        )
11505        };
11506    for &v in &inlined {
11507        let in_t = matches!(&t_side, Side::Call { defs, .. } if defs.iter().any(|(d, _)| *d == v));
11508        let in_e = matches!(&e_side, Side::Call { defs, .. } if defs.iter().any(|(d, _)| *d == v));
11509        let effectful = [
11510            in_t.then(|| tmap.get(&v)).flatten(),
11511            in_e.then(|| emap.get(&v)).flatten(),
11512            pmap.get(&v),
11513        ]
11514        .into_iter()
11515        .flatten()
11516        .any(jdc_core::ir::build::has_side_effects);
11517        if !effectful {
11518            continue;
11519        }
11520        let u_cond = cnt(&cond_counts, v);
11521        let u_post = cnt(&post_counts, v);
11522        let u_t1 = cnt(&tail1_counts, v);
11523        let u_t2 = cnt(&tail2_counts, v);
11524        let mut u_odef = 0usize;
11525        for (dv, dc) in &def_counts {
11526            if *dv != v && inlined.contains(dv) {
11527                u_odef += cnt(dc, v);
11528            }
11529        }
11530        let mut emit = u_cond * (1 + differ);
11531        let mut uses_args = 0usize;
11532        for j in 0..primary_args.len() {
11533            let c1 = cnt(&a1_counts[j], v);
11534            let c2 = if two_sided && j < a2_counts.len() {
11535                cnt(&a2_counts[j], v)
11536            } else {
11537                0
11538            };
11539            uses_args += c1 + c2;
11540            emit += if two_sided && then_args[j] != else_args[j] {
11541                c1 + c2
11542            } else {
11543                c1.max(c2)
11544            };
11545        }
11546        let ok = if in_t || in_e {
11547            u_cond == 0
11548                && u_post == 0
11549                && u_t1 == 0
11550                && u_t2 == 0
11551                && u_odef == 0
11552                && a1_counts.iter().map(|c| cnt(c, v)).sum::<usize>() <= 1
11553                && a2_counts.iter().map(|c| cnt(c, v)).sum::<usize>() <= 1
11554                && emit <= 2
11555                && (in_t || a1_counts.iter().all(|c| cnt(c, v) == 0))
11556                && (in_e || a2_counts.iter().all(|c| cnt(c, v) == 0))
11557        } else {
11558            u_post == 0
11559                && u_t1 == 0
11560                && u_t2 == 0
11561                && u_odef == 0
11562                && ((u_cond == 1 && uses_args == 0 && emit <= 2)
11563                    || (u_cond == 0 && emit <= 1))
11564        };
11565        if !ok {
11566            return false;
11567        }
11568    }
11569
11570    // ---- assembly ----
11571    let mut call_stmt = call_list[call_at].clone();
11572    if let Stmt::ExprStmt(Expr::Method { args: slot, .. }) = &mut call_stmt {
11573        *slot = merged;
11574    }
11575    let residual = |v: u32| -> usize {
11576        cnt(&tail1_counts, v) + cnt(&tail2_counts, v) + cnt(&post_counts, v)
11577    };
11578    let mut kept_prelude: Vec<Stmt> = Vec::new();
11579    for (i, s) in stmts[..if_pos].iter().enumerate() {
11580        let consumed = match s {
11581            Stmt::LocalDef { var, .. } => Some(*var),
11582            Stmt::ExprStmt(Expr::Assign { target, .. }) => match &**target {
11583                Expr::Local { var, .. } => Some(*var),
11584                _ => None,
11585            },
11586            Stmt::If { .. } => cond_def_of(s, stmts).map(|(v, _)| v),
11587            _ => None,
11588        };
11589        if let Some(v) = consumed {
11590            if inlined.contains(&v) && pidx.get(&v) == Some(&i) && residual(v) == 0 {
11591                continue;
11592            }
11593        }
11594        kept_prelude.push(s.clone());
11595    }
11596    let build_keep = |list: &[&Stmt], side: &Side| -> Vec<Stmt> {
11597        match side {
11598            Side::Clean => list.iter().map(|s| (*s).clone()).collect(),
11599            Side::Call { at, .. } => {
11600                let mut out = Vec::new();
11601                for (i, s) in list.iter().enumerate() {
11602                    if i == *at {
11603                        continue;
11604                    }
11605                    if i < *at {
11606                        if let Some((v, _)) = def_of(s) {
11607                            if inlined.contains(&v) && residual(v) == 0 {
11608                                continue;
11609                            }
11610                        }
11611                    }
11612                    out.push((*s).clone());
11613                }
11614                out
11615            }
11616        }
11617    };
11618    let then_keep = build_keep(&then_list, &t_side);
11619    let else_keep = build_keep(&else_list, &e_side);
11620    let else_part = if else_keep.is_empty() && !had_else {
11621        None
11622    } else {
11623        Some(Box::new(Stmt::Block(else_keep)))
11624    };
11625    let new_if = Stmt::If {
11626        cond: cond_i,
11627        then_stmt: Box::new(Stmt::Block(then_keep)),
11628        else_stmt: else_part,
11629    };
11630    let if_empty = match &new_if {
11631        Stmt::If { then_stmt, else_stmt, .. } => {
11632            let be = |s: &Stmt| matches!(s, Stmt::Block(v) if v.is_empty());
11633            be(then_stmt) && else_stmt.as_ref().is_none_or(|e| be(e))
11634        }
11635        _ => false,
11636    };
11637    let mut out: Vec<Stmt> = Vec::with_capacity(stmts.len());
11638    out.push(call_stmt);
11639    out.extend(kept_prelude);
11640    if !if_empty {
11641        out.push(new_if);
11642    }
11643    out.extend(stmts[if_pos + 1..].to_vec());
11644    *stmts = out;
11645    true
11646}
11647
11648
11649pub fn strip_enum_ctor_super(body: &mut Stmt) {
11650    fn is_enum_super(s: &Stmt) -> bool {
11651        matches!(
11652            s,
11653            Stmt::ExprStmt(Expr::Method { name, is_super: true, .. }) if &**name == "<init>"
11654        )
11655    }
11656    // FULL depth: hotfix-guarded ctors hide a second super(String,int)
11657    // inside the proxy branch (alipay InstantRun: `if (proxy != null) {
11658    // super((String) v3[0], ..); proxy.afterSuper(this); return; }` at
11659    // depth 3+; the old two-level strip left it — super resolved against
11660    // Object once the fallback enum lost `extends Enum`, alipay
11661    // ctor-arity ×3,364). This path only runs for fallback enums whose
11662    // dex super is Enum/Object, so EVERY super call is a dead Enum-ctor
11663    // reference wherever it sits; constant subclasses (real supers)
11664    // take the other branch in method.rs and are untouched.
11665    fn strip_deep(stmts: &mut Vec<Stmt>) {
11666        stmts.retain(|s| !is_enum_super(s));
11667        for st in stmts.iter_mut() {
11668            strip_one(st);
11669        }
11670    }
11671    fn strip_one(st: &mut Stmt) {
11672        match st {
11673            Stmt::Block(v) => strip_deep(v),
11674            Stmt::If { then_stmt, else_stmt, .. } => {
11675                strip_one(then_stmt);
11676                if let Some(e) = else_stmt {
11677                    strip_one(e);
11678                }
11679            }
11680            Stmt::While { body, .. }
11681            | Stmt::DoWhile { body, .. }
11682            | Stmt::ForEach { body, .. }
11683            | Stmt::Synchronized { body, .. }
11684            | Stmt::Labeled { body, .. } => strip_one(body),
11685            Stmt::For { init, body, .. } => {
11686                strip_deep(init);
11687                strip_one(body);
11688            }
11689            Stmt::Switch { cases, default, .. } => {
11690                for c in cases.iter_mut() {
11691                    strip_deep(&mut c.body);
11692                }
11693                if let Some(d) = default {
11694                    strip_one(d);
11695                }
11696            }
11697            Stmt::Try { body, catches, finally }
11698            | Stmt::TryWithResources { body, catches, finally, .. } => {
11699                strip_one(body);
11700                for c in catches.iter_mut() {
11701                    strip_one(&mut c.body);
11702                }
11703                if let Some(f) = finally {
11704                    strip_one(f);
11705                }
11706            }
11707            _ => {}
11708        }
11709    }
11710    let Stmt::Block(stmts) = body else {
11711        return;
11712    };
11713    strip_deep(stmts);
11714}
11715
11716pub fn ensure_declared(body: &mut Stmt, vt: &VarTable) {
11717    // (Perf: dense Vec<bool> tables instead of HashSets — var ids are
11718    // dense; the `assigned` set collected here was never read and cost a
11719    // full extra tree walk per method.)
11720    let n = vt.vars.len().max(1);
11721
11722    // Demote extra LocalDefs (same var declared more than once) FIRST —
11723    // the scope analysis below must see exactly one def per var.
11724    let mut seen: HashSet<u32> = HashSet::default();
11725    demote_dup_decls(body, &mut seen, vt);
11726
11727    // Scope hoist: a LocalDef inside a nested block whose var is also
11728    // referenced OUTSIDE that block is a Java scope violation (the
11729    // branch-local `int v8 = pi.versionCode;` read after the try —
11730    // "cannot find symbol" at every outside use). Demote those defs to
11731    // assignments and let the bare top-of-method declaration below cover
11732    // the var. Two cheap passes: pre-order block numbering + per-var
11733    // inside/outside occurrence counts (pre-order subtree = id range).
11734    let mut def_block: Vec<u32> = vec![u32::MAX; n];
11735    let mut sizes: Vec<u32> = vec![0];
11736    let mut next_id: u32 = 1;
11737    scope_pass_a(body, 0, &mut next_id, &mut def_block, &mut sizes);
11738    sizes[0] = next_id;
11739    let mut within: Vec<u32> = vec![0; n];
11740    let mut total: Vec<u32> = vec![0; n];
11741    let mut next_b: u32 = 1;
11742    scope_pass_b(body, 0, &mut next_b, &def_block, &sizes, &mut within, &mut total);
11743    let mut hoist: HashSet<u32> = HashSet::default();
11744    for v in 0..n {
11745        if def_block[v] != u32::MAX && def_block[v] != 0 && within[v] < total[v] {
11746            hoist.insert(v as u32);
11747        }
11748    }
11749    if !hoist.is_empty() {
11750        let mut seed: HashSet<u32> = hoist.iter().copied().collect();
11751        demote_dup_decls(body, &mut seed, vt);
11752    }
11753
11754    let mut declared: Vec<bool> = vec![false; n];
11755    let mut is_param: Vec<bool> = vec![false; n];
11756    for v in &vt.vars {
11757        if v.is_param && (v.id as usize) < n {
11758            is_param[v.id as usize] = true;
11759        }
11760    }
11761    // Catch parameters are declared by the `catch (Type v)` clause —
11762    // bind_catches consumed their LocalDef before this pass ran, so
11763    // without this they re-declare at the top of the method and clash
11764    // with the clause (`Throwable th2;` + `catch (Throwable th2)` — a
11765    // syntax gate cannot see this, it is a semantic "already defined").
11766    let mut is_catch: Vec<bool> = vec![false; n];
11767    walk_all(body, &mut |st| {
11768        if let Stmt::Try { catches, .. } = st {
11769            for c in catches {
11770                if (c.var as usize) < n {
11771                    is_catch[c.var as usize] = true;
11772                }
11773            }
11774        }
11775    });
11776    walk_all(body, &mut |st| {
11777        if let Stmt::LocalDef { var, .. } = st {
11778            if (*var as usize) < n {
11779                declared[*var as usize] = true;
11780            }
11781        }
11782    });
11783    let mut used: HashSet<u32> = HashSet::default();
11784    // assignments=true: a var that only ever appears as an assign
11785    // TARGET still needs a declaration (`x = 5;` alone does not
11786    // declare x in Java).
11787    stmt_collect_vars(body, &mut used, true);
11788
11789    // Params are declared by the signature; hoisted vars lost their
11790    // LocalDef to the demotion above and always need the bare decl.
11791    let mut needs_decl: Vec<u32> = used
11792        .into_iter()
11793        .filter(|v| {
11794            let i = *v as usize;
11795            i >= n
11796                || ((!declared[i] && !is_param[i] && !is_catch[i]) || hoist.contains(v))
11797        })
11798        .collect();
11799    needs_decl.sort_unstable();
11800    needs_decl.dedup();
11801
11802    if !needs_decl.is_empty() {
11803        let mut decls: Vec<Stmt> = needs_decl
11804            .into_iter()
11805            .map(|v| Stmt::LocalDef {
11806                var: v,
11807                init: None,
11808                is_final: false,
11809                force_type: true,
11810            })
11811            .collect();
11812        decls.reverse();
11813        match body {
11814            Stmt::Block(v) => {
11815                for d in decls {
11816                    v.insert(0, d);
11817                }
11818            }
11819            other => {
11820                let inner = std::mem::replace(other, Stmt::Block(vec![]));
11821                *other = Stmt::Block({
11822                    let mut vs = decls;
11823                    vs.push(inner);
11824                    vs
11825                });
11826            }
11827        }
11828    }
11829}
11830
11831fn demote_dup_decls(s: &mut Stmt, seen: &mut HashSet<u32>, vt: &VarTable) {
11832    match s {
11833        // Exactly one visit per node: the Block arm consumes its children,
11834        // the fallthrough walk would double-visit them.
11835        Stmt::Block(v) => {
11836            for x in v.iter_mut() {
11837                demote_dup_decls(x, seen, vt);
11838            }
11839        }
11840        Stmt::LocalDef { var, init, .. } => {
11841            if seen.contains(var) {
11842                let ty = vt.var(*var).ty.clone();
11843                if let Some(e) = init.take() {
11844                    *s = Stmt::ExprStmt(Expr::Assign {
11845                        target: Box::new(Expr::Local { var: *var, ty }),
11846                        op: AssignOp::Plain,
11847                        value: Box::new(e),
11848                    });
11849                } else {
11850                    *s = Stmt::Block(vec![]);
11851                }
11852            } else {
11853                seen.insert(*var);
11854            }
11855        }
11856        other => {
11857            walk_mut(other, &mut |x| demote_dup_decls(x, seen, vt));
11858        }
11859    }
11860}
11861
11862#[allow(dead_code)]
11863fn unused(_: &Vec<CaseGroup>, _: &Catch) {}
11864
11865// ---------------------------------------------------------------------------
11866// jadx-style local names (ApplyVariableNames + dexdec's port of it)
11867// ---------------------------------------------------------------------------
11868
11869/// A synthetic `v12`/`p3` never survives when a better name exists.
11870/// Priority: (1) a Kotlin `Intrinsics.checkNotNullParameter(x, "name")`
11871/// names `x` from the message string (the string IS the parameter name);
11872/// (2) the single defining call — `getFoo()` → `foo`, `isFinishing()` →
11873/// `finishing`, `new File(…)` → `file`; (3) a type alias or the
11874/// lowercased class simple name. Collisions take `2`, `3`, …; a var with
11875/// two DISAGREEING defining calls stays unnamed (dexdec's
11876/// RelationalNameInference rule). Debug-info names are never touched
11877/// (`synthetic_name` gates the whole pass).
11878pub fn apply_local_names(vt: &mut VarTable, body: &Stmt) {
11879    use std::collections::{HashMap, HashSet};
11880
11881    // ---- proposals ------------------------------------------------------
11882    // var → (name, source-priority); disagreeing call names are dropped.
11883    let mut by_call: HashMap<u32, Vec<String>> = HashMap::new();
11884    let mut by_intrinsics: HashMap<u32, String> = HashMap::new();
11885
11886    visit_all_exprs(body, &mut |e| {
11887        if let Expr::Method { cls, name, args, .. } = e {
11888            if (cls.as_ref() == "kotlin/jvm/internal/Intrinsics"
11889                || cls.as_ref() == "kotlin/jvm/internal/IntrinsicsKt")
11890                && matches!(name.as_ref(), "checkNotNullParameter" | "checkParameterIsNotNull")
11891                && args.len() == 2
11892            {
11893                if let (Expr::Local { var, .. }, Expr::Const(ConstVal::Str(s))) = (&args[0], &args[1])
11894                {
11895                    by_intrinsics
11896                        .entry(*var)
11897                        .or_insert_with(|| sanitize_name(s).unwrap_or_default());
11898                }
11899            }
11900        }
11901    });
11902
11903    walk_all(body, &mut |st| {
11904        let (var, value) = match st {
11905            Stmt::LocalDef { var, init: Some(e), .. } => (*var, e),
11906            Stmt::ExprStmt(Expr::Assign { target, value, .. })
11907                if matches!(&**target, Expr::Local { .. }) =>
11908            {
11909                let Expr::Local { var, .. } = &**target else { return };
11910                (*var, value.as_ref())
11911            }
11912            _ => return,
11913        };
11914        if let Some(n) = defining_call_name(value) {
11915            by_call.entry(var).or_default().push(n);
11916        }
11917    });
11918
11919    // ---- reservation + application --------------------------------------
11920    // Real (debug-info) names can legally REPEAT across sibling source
11921    // scopes (`int i` in two loops) but our flat declaration hoisting
11922    // puts them in one scope — de-duplicate with the same numeric
11923    // suffixes the claim path uses.
11924    // Uniqueness operates on the SANITIZED display name: the identifier
11925    // sanitizer maps non-ASCII to `_`, so distinct debug names
11926    // (`ERROR_token参数缺失` / `ERROR_channelId参数缺失`) both RENDER as
11927    // `ERROR________` and collide as declarations. Tracking raw names
11928    // here would let the collision through.
11929    let sanit = |n: &str| crate::classdec::java_ident(n).into_owned();
11930    let mut taken: HashSet<String> = HashSet::default();
11931    // Parameter names are occupied REGARDLESS of syntheticness: claim()
11932    // hands out rename candidates, and without the synthetic params in
11933    // the pool a type fallback (`l7.p0` → "p0") renamed one parameter
11934    // ONTO another's slot-name (`b(v6.l p0, Object obj, l7.p0 p0)`).
11935    for v in vt.vars.iter() {
11936        // ALL synthetic names occupy their names, not just parameters:
11937        // a type fallback (`pc5.v56` → "v56") otherwise renames a local
11938        // ONTO another local's slot-name (`int v56;` beside
11939        // `pc5.v56 v56;`).
11940        if v.synthetic_name {
11941            taken.insert(sanit(&v.name));
11942        }
11943    }
11944    for v in vt.vars.iter_mut() {
11945        if v.synthetic_name {
11946            continue;
11947        }
11948        if !taken.insert(sanit(&v.name)) {
11949            let base = sanit(&v.name);
11950            for i in 2.. {
11951                let cand = format!("{base}{i}");
11952                if taken.insert(cand.clone()) {
11953                    v.name = cand;
11954                    break;
11955                }
11956            }
11957        }
11958    }
11959    let claim = |taken: &mut HashSet<String>, want: &str| -> String {
11960        let w = crate::classdec::java_ident(want).into_owned();
11961        if taken.insert(w.clone()) {
11962            return w;
11963        }
11964        for i in 2.. {
11965            let cand = format!("{w}{i}");
11966            if taken.insert(cand.clone()) {
11967                return cand;
11968            }
11969        }
11970        w
11971    };
11972
11973    for info in vt.vars.iter_mut() {
11974        if !info.synthetic_name {
11975            continue;
11976        }
11977        // (1) Intrinsics string — the Kotlin compiler wrote the real
11978        // parameter name right into the check.
11979        if let Some(n) = by_intrinsics.get(&info.id) {
11980            if !n.is_empty() {
11981                info.name = claim(&mut taken, n);
11982                continue;
11983            }
11984        }
11985        // (2) One consistent defining call.
11986        if let Some(names) = by_call.get(&info.id) {
11987            let unique: HashSet<&String> = names.iter().collect();
11988            if unique.len() == 1 {
11989                let n = unique.into_iter().next().unwrap();
11990                if !is_java_keyword(n) {
11991                    info.name = claim(&mut taken, n);
11992                    continue;
11993                }
11994            }
11995        }
11996        // (3) Type alias, else the lowercased simple class name
11997        // (jadx names a `Looper` local `looper`).
11998        let want = type_alias(&info.ty).map(str::to_string).or_else(|| simple_type_name(&info.ty));
11999        if let Some(n) = want {
12000            info.name = claim(&mut taken, &n);
12001        }
12002    }
12003
12004    // Final uniquification across ALL vars (params, locals, catch): the
12005    // reservation, claim and debug-name domains above are checked
12006    // pairwise but a name claimed LAST can still equal a debug name
12007    // that never re-checked (`zd1.v2 v2` beside `zd1.v2[] v2`).
12008    let mut seen: HashSet<String> = HashSet::default();
12009    for v in vt.vars.iter_mut() {
12010        if !seen.insert(sanit(&v.name)) {
12011            let base = sanit(&v.name);
12012            for i in 2.. {
12013                let cand = format!("{base}{i}");
12014                if seen.insert(cand.clone()) {
12015                    v.name = cand;
12016                    break;
12017                }
12018            }
12019        }
12020    }
12021}
12022
12023/// `com/android/.../Looper` → `looper` — the alias-table fallback.
12024fn simple_type_name(ty: &TypeRef) -> Option<String> {
12025    let TypeRef::J(JavaType::Object(n)) = ty else {
12026        return None;
12027    };
12028    let simple = n.rsplit('/').next().unwrap_or(n);
12029    let simple = simple.rsplit('$').next().unwrap_or(simple);
12030    // Lowercase the first ALPHABETIC char — `$`/`_`-prefixed names
12031    // (R8's `$$$_Thread`) must not survive capitalized.
12032    let mut chars = simple.chars();
12033    let mut out = String::with_capacity(simple.len());
12034    let mut lowered = false;
12035    for c in chars.by_ref() {
12036        if c.is_ascii_alphabetic() && !lowered {
12037            out.push(c.to_ascii_lowercase());
12038            lowered = true;
12039        } else {
12040            out.push(c);
12041        }
12042        if lowered {
12043            break;
12044        }
12045    }
12046    out.extend(chars);
12047    sanitize_name(&out)
12048}
12049
12050/// `getFoo()` → `foo`, `isFinishing()` → `finishing`, `new File(…)` →
12051/// `file`. The get/is prefixes only strip when the remainder starts
12052/// uppercase (so `issues()` keeps its name).
12053fn defining_call_name(e: &Expr) -> Option<String> {
12054    match e {
12055        Expr::Method { name, .. } => {
12056            let base = name
12057                .strip_prefix("get")
12058                .or_else(|| name.strip_prefix("is"))
12059                .filter(|rest| rest.chars().next().is_some_and(|c| c.is_ascii_uppercase()))
12060                .unwrap_or(name);
12061            let first = base.chars().next()?;
12062            let mut out = String::with_capacity(base.len());
12063            out.push(first.to_ascii_lowercase());
12064            out.extend(base.chars().skip(1));
12065            sanitize_name(&out)
12066        }
12067        Expr::New { cls, .. } => {
12068            let simple = cls.rsplit('/').next().unwrap_or(cls);
12069            let simple = simple.rsplit('$').next().unwrap_or(simple);
12070            let first = simple.chars().next()?;
12071            let mut out = String::with_capacity(simple.len());
12072            out.push(first.to_ascii_lowercase());
12073            out.extend(simple.chars().skip(1));
12074            sanitize_name(&out)
12075        }
12076        _ => None,
12077    }
12078}
12079
12080/// Valid java identifier, not a keyword/restricted name, ≥2 chars.
12081fn sanitize_name(s: &str) -> Option<String> {
12082    let mut out = String::with_capacity(s.len());
12083    for c in s.chars() {
12084        if c.is_ascii_alphanumeric() || c == '_' || c == '$' {
12085            out.push(c);
12086        } else {
12087            out.push('_');
12088        }
12089    }
12090    if out.len() < 2
12091        || out.chars().next().is_some_and(|c| c.is_ascii_digit())
12092        || is_java_keyword(&out)
12093    {
12094        return None;
12095    }
12096    Some(out)
12097}
12098
12099/// jadx's alias table for the common types, with the Android globals the
12100/// corpus actually shows; anything else falls back in the caller.
12101fn type_alias(ty: &TypeRef) -> Option<&'static str> {
12102    let TypeRef::J(JavaType::Object(n)) = ty else {
12103        return None;
12104    };
12105    Some(match n.as_ref() {
12106        "java/lang/String" | "kotlin/String" => "str",
12107        "java/lang/Class" => "cls",
12108        "java/lang/Throwable" => "th",
12109        "java/lang/Object" | "kotlin/Any" => "obj",
12110        "java/util/Iterator" | "kotlin/collections/Iterator" => "it",
12111        "java/lang/Boolean" => "bool",
12112        "java/lang/Integer" => "num",
12113        "java/lang/Character" => "ch",
12114        "java/lang/Byte" => "b",
12115        "java/lang/Short" => "sh",
12116        "java/lang/Float" => "f",
12117        "java/lang/Double" => "d",
12118        "java/lang/Long" => "i",
12119        "java/lang/StringBuilder" => "sb",
12120        "java/util/ArrayList" => "list",
12121        "java/util/HashMap" => "map",
12122        "android/content/Context" => "context",
12123        "android/content/Intent" => "intent",
12124        "android/os/Bundle" => "bundle",
12125        "android/view/View" => "view",
12126        "android/graphics/Bitmap" => "bitmap",
12127        "android/view/ViewGroup" => "viewGroup",
12128        _ => return None,
12129    })
12130}
12131
12132/// Every expression in the tree, statements included (read-only).
12133pub(crate) fn visit_all_exprs<F: FnMut(&Expr)>(s: &Stmt, f: &mut F) {
12134    walk_all(s, &mut |st| {
12135        let exprs: Vec<&Expr> = match st {
12136            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
12137                let mut v: Vec<&Expr> = vec![value];
12138                if !matches!(&**target, Expr::Local { .. }) {
12139                    v.push(target);
12140                }
12141                v
12142            }
12143            Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
12144                vec![e]
12145            }
12146            Stmt::Return(Some(e)) => vec![e],
12147            Stmt::LocalDef { init: Some(e), .. } => vec![e],
12148            Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
12149                vec![cond]
12150            }
12151            _ => Vec::new(),
12152        };
12153        for e in exprs {
12154            visit_exprs(e, f);
12155        }
12156    });
12157}
12158
12159/// IntDef/LongDef exact-match rendering: a literal argument to a
12160/// platform method whose parameter carries a constant domain renders as
12161/// the named constant (`setVisibility(8)` → `android.view.View.GONE`).
12162/// Combined flag values and literals outside the domain stay numeric.
12163pub fn platform_constants(body: &mut Stmt) {
12164    if !crate::platform::installed() {
12165        return;
12166    }
12167    rewrite_exprs(body, &mut |e| {
12168        if let Expr::Method { cls, name, desc, args, .. } = e {
12169            for a in args.iter_mut() {
12170                let (value, is_long) = match a {
12171                    Expr::Const(ConstVal::Int(v)) => (*v as i64, false),
12172                    Expr::Const(ConstVal::Long(v)) => (*v, true),
12173                    _ => continue,
12174                };
12175                if let Some(named) =
12176                    crate::platform::named_constant(cls, name, &desc_to_string(desc), 0, value)
12177                {
12178                    let _ = is_long;
12179                    *a = Expr::Raw(named);
12180                }
12181            }
12182        }
12183    });
12184}
12185
12186/// MethodDescriptor → dex descriptor string (`(I)V`).
12187fn desc_to_string(d: &jdc_core::types::MethodDescriptor) -> String {
12188    let mut out = String::from("(");
12189    for a in &d.args {
12190        type_to_desc(a, &mut out);
12191    }
12192    out.push(')');
12193    type_to_desc(&d.ret, &mut out);
12194    out
12195}
12196
12197fn type_to_desc(t: &jdc_core::types::JavaType, out: &mut String) {
12198    match t {
12199        JavaType::Void => out.push('V'),
12200        JavaType::Boolean => out.push('Z'),
12201        JavaType::Byte => out.push('B'),
12202        JavaType::Char => out.push('C'),
12203        JavaType::Short => out.push('S'),
12204        JavaType::Int => out.push('I'),
12205        JavaType::Float => out.push('F'),
12206        JavaType::Long => out.push('J'),
12207        JavaType::Double => out.push('D'),
12208        JavaType::Object(n) => {
12209            out.push('L');
12210            out.push_str(n);
12211            out.push(';');
12212        }
12213        JavaType::Array(inner) => {
12214            out.push('[');
12215            type_to_desc(inner, out);
12216        }
12217    }
12218}
12219
12220/// Kotlin `Intrinsics` null-check elision (jadx's ProcessKotlinInternals):
12221/// statement-position `checkNotNullParameter(p, "name")` /
12222/// `checkParameterIsNotNull` / `checkNotNull…` calls are runtime
12223/// assertions — drop them. The naming pass harvests the parameter
12224/// strings FIRST, so run this after `apply_local_names`.
12225pub fn remove_kotlin_checks(s: &mut Stmt) {
12226    walk_mut_deep(s, &mut |st| {
12227        let Stmt::Block(v) = st else { return };
12228        v.retain(|x| !is_kotlin_check(x));
12229    });
12230}
12231
12232fn is_kotlin_check(s: &Stmt) -> bool {
12233    let Stmt::ExprStmt(e) = s else { return false };
12234    let Expr::Method { cls, name, .. } = e else { return false };
12235    (cls.as_ref() == "kotlin/jvm/internal/Intrinsics" || cls.as_ref() == "kotlin/jvm/internal/IntrinsicsKt__Jdk7Kt")
12236        && matches!(
12237            name.as_ref(),
12238            "checkNotNullParameter"
12239                | "checkParameterIsNotNull"
12240                | "checkExpressionValueIsNotNull"
12241                | "checkNotNullExpressionValue"
12242                | "checkReturnedValueIsNotNull"
12243                | "checkFieldIsNotNull"
12244                | "checkNotNull"
12245        )
12246}
12247
12248// ---------------------------------------------------------------------------
12249// Synthetic-accessor inlining (jadx's MarkMethodsForInline, the safe subset)
12250// ---------------------------------------------------------------------------
12251
12252/// Inline `access$NNN`-style synthetic static bridges at their call
12253/// sites: an identity (`return pN`), a getter (`iget pX, field;
12254/// return vR`), or a method forwarder (`invoke; return`) — with the
12255/// d8 APM trace wrappers (`MethodCollector.i/.o` const-only static
12256/// calls) tolerated around the core. Only STATIC + SYNTHETIC callees
12257/// qualify (compiler-generated bridges: no override semantics, no
12258/// side effects beyond the forwarded operation).
12259/// Can `host` (internal class name) source-level reference `target`'s
12260/// class chain and member? Cross-package inlining of a synthetic
12261/// accessor used to EXPOSE package-private targets the raw dex never
12262/// referenced cross-package (BuildersKt.launch$default forwards to
12263/// kotlinx.coroutines.k.e — the widening census cannot see post-inline
12264/// calls; lark's 1,019-line k不是公共的 family). Non-public chain link
12265/// or non-public member across packages → the inline must not happen
12266/// (the public synthetic bridge stays and compiles).
12267fn inline_ref_ok(pool: &DexPool, host: &str, target: &str, member: Option<(&str, bool)>) -> bool {
12268    let hpkg = match host.rfind('/') {
12269        Some(i) => &host[..i],
12270        None => "",
12271    };
12272    let mut cur = target;
12273    loop {
12274        let tpkg = match cur.rfind('/') {
12275            Some(i) => &cur[..i],
12276            None => "",
12277        };
12278        if tpkg != hpkg {
12279            if let Some(pc) = pool.get(cur) {
12280                if pc.access & crate::access::ACC_PUBLIC == 0 {
12281                    return false;
12282                }
12283                if cur == target {
12284                    if let Some((mname, is_method)) = member {
12285                        let ok = if is_method {
12286                            pc.all_methods().any(|m| {
12287                                &*m.name == mname
12288                                    && m.access & crate::access::ACC_PUBLIC != 0
12289                            })
12290                        } else {
12291                            pc.static_fields
12292                                .iter()
12293                                .chain(pc.instance_fields.iter())
12294                                .any(|f| {
12295                                    f.name == mname
12296                                        && f.access & crate::access::ACC_PUBLIC != 0
12297                                })
12298                        };
12299                        // A member match requires the exact member to be
12300                        // public; an absent member (renamed/collapsed)
12301                        // stays allowed — the render will show what the
12302                        // pool has.
12303                        if !ok
12304                            && (if is_method {
12305                                pc.all_methods().any(|m| &*m.name == mname)
12306                            } else {
12307                                pc.static_fields
12308                                    .iter()
12309                                    .chain(pc.instance_fields.iter())
12310                                    .any(|f| f.name == mname)
12311                            })
12312                        {
12313                            return false;
12314                        }
12315                    }
12316                }
12317            }
12318        }
12319        match cur.rfind('$') {
12320            Some(i) if i > 0 => cur = &cur[..i],
12321            _ => break,
12322        }
12323    }
12324    true
12325}
12326
12327pub fn inline_accessors(s: &mut Stmt, pool: &DexPool, host: &str) {
12328    rewrite_exprs(s, &mut |e| {
12329        let Expr::Method { cls, name, desc, args, is_static, .. } = e else { return };
12330        if !*is_static || args.is_empty() {
12331            return;
12332        }
12333        let Some(target) = pool.get(cls) else { return };
12334        let desc_s = desc_to_string(desc);
12335        let Some(m) = target.find_method(name, &desc_s) else { return };
12336        if !(m.is_static() && m.access & access::ACC_SYNTHETIC != 0) {
12337            return;
12338        }
12339        let Some(dex) = pool.dex(m.dex_idx) else { return };
12340        // Snapshot first: the accessor's owning image may already be
12341        // retired, and a live read's success would depend on worker
12342        // completion interleaving — nondeterministic inlining (whether
12343        // the accessor folds at all) between identical runs.
12344        let code = match pool.accessor_code(m.dex_idx, m.code_off) {
12345            Some(bytes) => ddc_dex::CodeItem::parse(&bytes, 0),
12346            None => dex.code_at(m.code_off),
12347        };
12348        let Some(code) = code else { return };
12349        let insns: Vec<&Insn> =
12350            code.insns.iter().filter(|i| !matches!(i.kind, InsnKind::Nop)).collect();
12351        // Strip the APM trace wrappers (const-only invoke-static).
12352        let core: Vec<&Insn> = insns
12353            .iter()
12354            .copied()
12355            .filter(|i| !is_const_only_trace(&i.kind, &insns))
12356            .collect();
12357        match accessor_shape(&dex, &core, &code) {
12358            Some(Shape::Identity(arg_i)) => {
12359                if let Some(a) = args.get(arg_i) {
12360                    *e = a.clone();
12361                }
12362            }
12363            Some(Shape::FieldRead { arg, cls: field_cls, field, field_ty }) => {
12364                if !inline_ref_ok(pool, host, &field_cls, Some((&field, false))) {
12365                    return;
12366                }
12367                if let Some(owner) = args.get(arg).cloned().map(Box::new) {
12368                    let ty = TypeRef::J(desc_type(&field_ty));
12369                    // The inlined read must consult the member rename
12370                    // registry like every lifted field ref does — the
12371                    // raw dex name desynced from renamed declarations
12372                    // (lark uu4/f$b's `a`→`ax6`: 找不到符号 变量 a ×4.4k
12373                    // when the nested-collision deshadow fired).
12374                    let name = jdc_core::rename::field_display(
12375                        &field_cls,
12376                        &field,
12377                        &field_ty,
12378                    )
12379                    .map(std::sync::Arc::from)
12380                    .unwrap_or_else(|| field.into());
12381                    *e = Expr::Field {
12382                        owner: Some(owner),
12383                        cls: field_cls.into(),
12384                        name,
12385                        ty,
12386                        is_static: false,
12387                    };
12388                }
12389            }
12390            Some(Shape::Forward { cls: tcls, name: tname, desc: tdesc, instance }) => {
12391                if !inline_ref_ok(pool, host, &tcls, Some((&tname, true))) {
12392                    return;
12393                }
12394                // Same registry duty as FieldRead: the forwarded member
12395                // must render under its renamed display, not the raw
12396                // dex name.
12397                let disp = jdc_core::rename::field_display(
12398                    &tcls,
12399                    &tname,
12400                    &tdesc.to_string(),
12401                )
12402                .map(std::sync::Arc::from)
12403                .unwrap_or_else(|| tname.into());
12404                *cls = tcls.into();
12405                *name = disp;
12406                *desc = std::sync::Arc::new(tdesc);
12407                if instance && !args.is_empty() {
12408                    // The first param (the receiver) becomes the owner.
12409                    let recv = args.remove(0);
12410                    if let Expr::Method { owner, .. } = e {
12411                        *owner = Some(Box::new(recv));
12412                    }
12413                }
12414            }
12415            None => {}
12416        }
12417    });
12418}
12419
12420/// What an accessor body reduces to.
12421enum Shape {
12422    /// `return pN` — the call becomes argument N.
12423    Identity(usize),
12424    /// `iget vR, pX, field; return vR` — becomes `argN.field`.
12425    FieldRead { arg: usize, cls: String, field: String, field_ty: String },
12426    /// `invoke {pX, args…}, method@M; (move-result;)? return` —
12427    /// becomes the forwarded call.
12428    Forward { cls: String, name: String, desc: MethodDescriptor, instance: bool },
12429}
12430
12431/// Resolve the IGet field's owner class, name and type descriptor.
12432fn field_of(dex: &ddc_dex::DexFile, field_idx: u32) -> Option<(String, String, String)> {
12433    let f = dex.field(field_idx);
12434    Some((
12435        // class_name strips the `L...;` shell; type_name would leak a
12436        // descriptor into the Field's owner class (`La3.a.e_`).
12437        dex.class_name(f.class_idx),
12438        dex.string(f.name_idx).to_string(),
12439        // The FIELD TYPE stays a descriptor: desc_type parses it.
12440        dex.type_name(f.type_idx).to_string(),
12441    ))
12442}
12443
12444/// Map a register to a parameter index (static method: params occupy
12445/// the LAST ins_size registers).
12446fn param_index(code: &ddc_dex::CodeItem, reg: u16) -> Option<usize> {
12447    let rs = code.registers_size as usize;
12448    let ins = code.ins_size as usize;
12449    let r = reg as usize;
12450    let first = rs.checked_sub(ins)?;
12451    (r >= first && r < rs).then(|| r - first)
12452}
12453
12454fn accessor_shape(
12455    dex: &ddc_dex::DexFile,
12456    core: &[&Insn],
12457    code: &ddc_dex::CodeItem,
12458) -> Option<Shape> {
12459    // Dead consts that fed the stripped trace wrappers remain in the
12460    // core — skip them. ONLY when actually dead (dst unread by the
12461    // rest): a const-returning synthetic (`lambda$new$4(String) {
12462    // return false; }` = `const/4 v0,0; return v0`, registers=1 so v0
12463    // IS the param) lost its const and matched Identity(param0) —
12464    // inline_accessors then replaced the CALL with its ARGUMENT
12465    // (`return (String) obj` against a boolean SAM — weibo
12466    // IntentSanitizer lambdas ×116, and silently WRONG renders
12467    // wherever the types happened to agree).
12468    let reads = |rest: &[&Insn], r: u16| {
12469        rest.iter()
12470            .any(|i| crate::lift::src_regs(&i.kind).contains(&r))
12471    };
12472    let core: &[&Insn] = match core.split_first() {
12473        Some((first, rest))
12474            if matches!(first.kind, InsnKind::Const { dst, .. } if !reads(rest, dst)) =>
12475        {
12476            rest
12477        }
12478        _ => core,
12479    };
12480    let core: &[&Insn] = match core.split_last() {
12481        Some((last, rest))
12482            if matches!(last.kind, InsnKind::Const { dst, .. } if !reads(rest, dst)) =>
12483        {
12484            rest
12485        }
12486        _ => core,
12487    };
12488    match core {
12489        // return pN
12490        [Insn { kind: InsnKind::Return { src }, .. }] => {
12491            let idx = param_index(code, *src)?;
12492            Some(Shape::Identity(idx))
12493        }
12494        // iget vR, pX, field; return vR
12495        [
12496            Insn { kind: InsnKind::IGet { dst, obj, field_idx }, .. },
12497            Insn { kind: InsnKind::Return { src }, .. },
12498        ] if dst == src => {
12499            let arg = param_index(code, *obj)?;
12500            let (cls, field, field_ty) = field_of(dex, *field_idx)?;
12501            Some(Shape::FieldRead { arg, cls, field, field_ty })
12502        }
12503        // invoke {…}, method@M; (move-result; return)? — forwarder.
12504        [Insn { kind: InsnKind::Invoke { method_idx, regs, kind, .. }, .. }, rest @ ..] if rest.len() <= 2 => {
12505            // Only forward when every register is a param (no locals).
12506            if !regs.iter().all(|r| param_index(code, *r).is_some()) || regs.is_empty() {
12507                return None;
12508            }
12509            // Trailing must be move-result+return or return/void —
12510            // anything else (e.g. trace calls) already filtered upstream.
12511            if rest.len() == 2 {
12512                let ok = matches!(rest[0].kind, InsnKind::MoveResult { .. })
12513                    && matches!(rest[1].kind, InsnKind::Return { .. } | InsnKind::ReturnVoid);
12514                if !ok {
12515                    return None;
12516                }
12517            }
12518            let mid = dex.method(*method_idx);
12519            // class_name strips the `L...;` shell — type_name leaks a
12520            // descriptor into the forwarded call's class (`La3.a.e_`).
12521            let cls = dex.class_name(mid.class_idx);
12522            let name = dex.string(mid.name_idx).to_string();
12523            let mut s = String::from("(");
12524            for t in dex.proto_params(mid.proto_idx) {
12525                s.push_str(dex.type_name(*t));
12526            }
12527            s.push(')');
12528            s.push_str(dex.type_name(dex.proto(mid.proto_idx).return_type_idx));
12529            let desc = parse_method_descriptor(&s)?;
12530            Some(Shape::Forward { cls, name, desc, instance: !matches!(kind, InvokeKind::Static) })
12531        }
12532        _ => None,
12533    }
12534}
12535
12536
12537/// A const-only invoke-static (the d8 APM `i(732046)` / `o(732046)`
12538/// trace wrapper): every argument register is loaded by a Const (or a
12539/// Const-into-move chain) in the SAME body. Scoped to synthetic
12540/// accessors, so a genuinely side-effecting const call is never
12541/// mistaken for a trace.
12542fn is_const_only_trace(kind: &InsnKind, insns: &[&Insn]) -> bool {
12543    let InsnKind::Invoke { kind, regs, .. } = kind else { return false };
12544    if !matches!(kind, InvokeKind::Static) || regs.is_empty() {
12545        return false;
12546    }
12547    regs.iter().all(|r| const_loaded(*r, insns, 0))
12548}
12549
12550/// Const-loaded directly, or through a Move chain from a const-loaded
12551/// register (the APM `const v0, id` → `invoke {v0}` shape).
12552fn const_loaded(r: u16, insns: &[&Insn], depth: u8) -> bool {
12553    if depth > 3 {
12554        return false;
12555    }
12556    insns.iter().any(|i| match &i.kind {
12557        InsnKind::Const { dst, .. } => *dst == r,
12558        InsnKind::Move { dst, src } => *dst == r && const_loaded(*src, insns, depth + 1),
12559        _ => false,
12560    })
12561}