Skip to main content

ddc_dec/
passes.rs

1//! Post-convert refinement passes for DEX-lifted statement trees.
2//!
3//! The converter hands back a correct-but-raw tree; these passes restore the
4//! source shapes a register machine loses: catch parameter binding,
5//! copy-forwarding (single-use temporaries), if/else→ternary folding,
6//! `StringBuilder` chain → `+` concatenation, `synchronized` recovery from
7//! the d8 monitor pattern, erased-type inference, boolean and null
8//! comparisons, and declaration hygiene.
9
10// The tree-walker match arms intentionally mirror the statement grammar
11// one level at a time; collapsing the nested `if let`s into outer match
12// arms would trade per-arm clarity for lint silence.
13#![allow(clippy::collapsible_match)]
14
15use jdc_core::FxHashSet as HashSet;
16
17// The tree-walker match arms intentionally mirror the statement grammar
18// one level at a time; collapsing the nested `if let`s into outer match
19// arms would trade per-arm clarity for lint silence.
20use jdc_core::ir::build::has_side_effects;
21use jdc_core::emit::is_java_keyword;
22use jdc_core::ir::expr::{AssignOp, BinOp, ConcatPart, ConstVal, Expr, TypeRef, UnOp};
23use jdc_core::ir::stmt::{CaseGroup, Catch, Stmt};
24use jdc_core::types::{JavaType, MethodDescriptor};
25use jdc_core::var::VarTable;
26
27use crate::lift::MethodEnv;
28use crate::{access, desc_type, DexPool};
29use ddc_dex::insn::{Insn, InsnKind, InvokeKind};
30use jdc_core::types::parse_method_descriptor;
31
32// ---------------------------------------------------------------------------
33// Generic tree walking
34// ---------------------------------------------------------------------------
35
36/// Deep expression rewrite over a statement tree.
37pub fn rewrite_exprs<F: FnMut(&mut Expr)>(s: &mut Stmt, f: &mut F) {
38    walk_stmt_exprs(s, f);
39}
40
41pub(crate) fn walk_stmt_exprs<F: FnMut(&mut Expr)>(s: &mut Stmt, f: &mut F) {
42    match s {
43        Stmt::Block(v) => {
44            for x in v.iter_mut() {
45                walk_stmt_exprs(x, f);
46            }
47        }
48        Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
49            f(e);
50        }
51        Stmt::Return(Some(e)) => f(e),
52        Stmt::LocalDef { init, .. } => {
53            if let Some(e) = init {
54                f(e);
55            }
56        }
57        Stmt::If {
58            cond,
59            then_stmt,
60            else_stmt,
61        } => {
62            f(cond);
63            walk_stmt_exprs(then_stmt, f);
64            if let Some(e) = else_stmt {
65                walk_stmt_exprs(e, f);
66            }
67        }
68        Stmt::While { cond, body } => {
69            f(cond);
70            walk_stmt_exprs(body, f);
71        }
72        Stmt::DoWhile { body, cond } => {
73            walk_stmt_exprs(body, f);
74            f(cond);
75        }
76        Stmt::For {
77            init,
78            cond,
79            update,
80            body,
81        } => {
82            for x in init.iter_mut() {
83                walk_stmt_exprs(x, f);
84            }
85            if let Some(c) = cond {
86                f(c);
87            }
88            for u in update.iter_mut() {
89                f(u);
90            }
91            walk_stmt_exprs(body, f);
92        }
93        Stmt::ForEach { iterable, body, .. } => {
94            f(iterable);
95            walk_stmt_exprs(body, f);
96        }
97        Stmt::Switch {
98            selector,
99            cases,
100            default,
101            ..
102        } => {
103            f(selector);
104            for c in cases {
105                for x in c.body.iter_mut() {
106                    walk_stmt_exprs(x, f);
107                }
108                if let Some(g) = &mut c.guard {
109                    f(g);
110                }
111            }
112            if let Some(d) = default {
113                walk_stmt_exprs(d, f);
114            }
115        }
116        Stmt::Try {
117            body,
118            catches,
119            finally,
120        } => {
121            walk_stmt_exprs(body, f);
122            for c in catches {
123                walk_stmt_exprs(&mut c.body, f);
124            }
125            if let Some(fl) = finally {
126                walk_stmt_exprs(fl, f);
127            }
128        }
129        Stmt::TryWithResources {
130            resources,
131            body,
132            catches,
133            finally,
134        } => {
135            for x in resources.iter_mut() {
136                walk_stmt_exprs(x, f);
137            }
138            walk_stmt_exprs(body, f);
139            for c in catches {
140                walk_stmt_exprs(&mut c.body, f);
141            }
142            if let Some(fl) = finally {
143                walk_stmt_exprs(fl, f);
144            }
145        }
146        Stmt::Assert { cond, msg } => {
147            f(cond);
148            if let Some(m) = msg {
149                f(m);
150            }
151        }
152        Stmt::Synchronized { lock, body } => {
153            f(lock);
154            walk_stmt_exprs(body, f);
155        }
156        Stmt::TernaryValue { e } => f(e),
157        Stmt::Labeled { body, .. } => walk_stmt_exprs(body, f),
158        Stmt::Goto(_) | Stmt::Label(_) | Stmt::Break(_) | Stmt::Continue(_) => {}
159        _ => {}
160    }
161}
162
163/// Immutable mirror of `walk_stmt_exprs`: read-only visitors must not
164/// have to clone a statement tree just to obtain a `&mut` handle.
165/// (count_locals_stmts cloned EVERY counted statement — 29k of 50k
166/// Stmt::clone samples in corpus profiles, ~25% of total CPU.)
167pub(crate) fn visit_stmt_exprs_ro<F: FnMut(&Expr)>(s: &Stmt, f: &mut F) {
168    match s {
169        Stmt::Block(v) => {
170            for x in v {
171                visit_stmt_exprs_ro(x, f);
172            }
173        }
174        Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
175            f(e);
176        }
177        Stmt::Return(Some(e)) => f(e),
178        Stmt::LocalDef { init: Some(e), .. } => f(e),
179        Stmt::If {
180            cond,
181            then_stmt,
182            else_stmt,
183        } => {
184            f(cond);
185            visit_stmt_exprs_ro(then_stmt, f);
186            if let Some(e) = else_stmt {
187                visit_stmt_exprs_ro(e, f);
188            }
189        }
190        Stmt::While { cond, body } => {
191            f(cond);
192            visit_stmt_exprs_ro(body, f);
193        }
194        Stmt::DoWhile { body, cond } => {
195            visit_stmt_exprs_ro(body, f);
196            f(cond);
197        }
198        Stmt::For {
199            init,
200            cond,
201            update,
202            body,
203        } => {
204            for x in init {
205                visit_stmt_exprs_ro(x, f);
206            }
207            if let Some(c) = cond {
208                f(c);
209            }
210            for u in update {
211                f(u);
212            }
213            visit_stmt_exprs_ro(body, f);
214        }
215        Stmt::ForEach { iterable, body, .. } => {
216            f(iterable);
217            visit_stmt_exprs_ro(body, f);
218        }
219        Stmt::Switch {
220            selector,
221            cases,
222            default,
223            ..
224        } => {
225            f(selector);
226            for c in cases {
227                for x in &c.body {
228                    visit_stmt_exprs_ro(x, f);
229                }
230                if let Some(g) = &c.guard {
231                    f(g);
232                }
233            }
234            if let Some(d) = default {
235                visit_stmt_exprs_ro(d, f);
236            }
237        }
238        Stmt::Try {
239            body,
240            catches,
241            finally,
242        } => {
243            visit_stmt_exprs_ro(body, f);
244            for c in catches {
245                visit_stmt_exprs_ro(&c.body, f);
246            }
247            if let Some(fl) = finally {
248                visit_stmt_exprs_ro(fl, f);
249            }
250        }
251        Stmt::TryWithResources {
252            resources,
253            body,
254            catches,
255            finally,
256            ..
257        } => {
258            for x in resources {
259                visit_stmt_exprs_ro(x, f);
260            }
261            visit_stmt_exprs_ro(body, f);
262            for c in catches {
263                visit_stmt_exprs_ro(&c.body, f);
264            }
265            if let Some(fl) = finally {
266                visit_stmt_exprs_ro(fl, f);
267            }
268        }
269        Stmt::Assert { cond, msg } => {
270            f(cond);
271            if let Some(m) = msg {
272                f(m);
273            }
274        }
275        Stmt::Synchronized { lock, body } => {
276            f(lock);
277            visit_stmt_exprs_ro(body, f);
278        }
279        Stmt::TernaryValue { e } => f(e),
280        Stmt::Labeled { body, .. } => visit_stmt_exprs_ro(body, f),
281        _ => {}
282    }
283}
284
285/// Count Local reads with the lost-alloc `<init>`-owner exclusion,
286/// zero-clone: mirrors `strip_lost_alloc_owners` + deep_rewrite count
287/// exactly (the strip replaced the owner subtree with Const(0), so the
288/// owner's locals were never counted; nested exclusions cannot double-
289/// skip because the excluded owner is never descended into).
290fn count_expr_ro(e: &Expr, m: &mut jdc_core::FxHashMap<u32, usize>) {
291    if let Expr::Local { var, .. } = e {
292        *m.entry(*var).or_insert(0) += 1;
293        return;
294    }
295    if let Expr::Method {
296        name,
297        is_special: true,
298        owner: Some(o),
299        args,
300        ..
301    } = e
302    {
303        if &**name == "<init>" && !matches!(**o, Expr::This) {
304            for a in args {
305                count_expr_ro(a, m);
306            }
307            return;
308        }
309    }
310    for_each_child(e, &mut |c| count_expr_ro(c, m));
311}
312
313/// Visit every expression (immutable).
314fn visit_exprs<F: FnMut(&Expr)>(e: &Expr, f: &mut F) {
315    f(e);
316    for_each_child(e, &mut |c| visit_exprs(c, f));
317}
318
319fn for_each_child<F: FnMut(&Expr)>(e: &Expr, f: &mut F) {
320    match e {
321        Expr::Un { e, .. } | Expr::Cast { e, .. } | Expr::InstanceOf { e, .. } => f(e),
322        Expr::Bin { l, r, .. } => {
323            f(l);
324            f(r);
325        }
326        Expr::Cond { c, t, f: fe } => {
327            f(c);
328            f(t);
329            f(fe);
330        }
331        Expr::Assign { target, value, .. } => {
332            f(target);
333            f(value);
334        }
335        Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => f(e),
336        Expr::Field { owner, .. } => {
337            if let Some(o) = owner {
338                f(o);
339            }
340        }
341        Expr::Method { owner, args, .. } => {
342            if let Some(o) = owner {
343                f(o);
344            }
345            for a in args {
346                f(a);
347            }
348        }
349        Expr::ArrayIndex { array, index } => {
350            f(array);
351            f(index);
352        }
353        Expr::New { args, .. } => {
354            for a in args {
355                f(a);
356            }
357        }
358        Expr::NewArray { dims, init, .. } => {
359            for d in dims {
360                f(d);
361            }
362            if let Some(v) = init {
363                for x in v {
364                    f(x);
365                }
366            }
367        }
368        Expr::NewMultiArray { dims, .. } => {
369            for d in dims {
370                f(d);
371            }
372        }
373        Expr::StringConcat(parts) => {
374            for p in parts {
375                if let ConcatPart::Str(x) = p {
376                    f(x);
377                }
378            }
379        }
380        Expr::Invokedynamic { args, .. } => {
381            for a in args {
382                f(a);
383            }
384        }
385        Expr::AnonNew { args, .. } => {
386            for a in args {
387                f(a);
388            }
389        }
390        _ => {}
391    }
392}
393
394/// Mutable child walk (one level).
395fn for_each_child_mut<F: FnMut(&mut Expr)>(e: &mut Expr, f: &mut F) {
396    match e {
397        Expr::Un { e, .. } | Expr::Cast { e, .. } | Expr::InstanceOf { e, .. } => f(e),
398        Expr::Bin { l, r, .. } => {
399            f(l);
400            f(r);
401        }
402        Expr::Cond { c, t, f: fe } => {
403            f(c);
404            f(t);
405            f(fe);
406        }
407        Expr::Assign { target, value, .. } => {
408            f(target);
409            f(value);
410        }
411        Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => f(e),
412        Expr::Field { owner, .. } => {
413            if let Some(o) = owner {
414                f(o);
415            }
416        }
417        Expr::Method { owner, args, .. } => {
418            if let Some(o) = owner {
419                f(o);
420            }
421            for a in args.iter_mut() {
422                f(a);
423            }
424        }
425        Expr::ArrayIndex { array, index } => {
426            f(array);
427            f(index);
428        }
429        Expr::New { args, .. } => {
430            for a in args.iter_mut() {
431                f(a);
432            }
433        }
434        Expr::NewArray { dims, init, .. } => {
435            for d in dims.iter_mut() {
436                f(d);
437            }
438            if let Some(v) = init {
439                for x in v.iter_mut() {
440                    f(x);
441                }
442            }
443        }
444        Expr::NewMultiArray { dims, .. } => {
445            for d in dims.iter_mut() {
446                f(d);
447            }
448        }
449        Expr::StringConcat(parts) => {
450            for p in parts {
451                if let ConcatPart::Str(x) = p {
452                    f(x);
453                }
454            }
455        }
456        Expr::Invokedynamic { args, .. } => {
457            for a in args.iter_mut() {
458                f(a);
459            }
460        }
461        Expr::AnonNew { args, .. } => {
462            for a in args.iter_mut() {
463                f(a);
464            }
465        }
466        _ => {}
467    }
468}
469
470/// Deep mutable expression rewrite.
471pub(crate) fn deep_rewrite<F: FnMut(&mut Expr)>(e: &mut Expr, f: &mut F) {
472    f(e);
473    for_each_child_mut(e, &mut |c| deep_rewrite(c, f));
474}
475
476/// `deep_rewrite` that respects WRITE positions: a `Local` appearing as
477/// an assignment target or a `++`/`--` operand never reaches `f`.
478/// Value-forwarding closures (`*x = value`) must use this — plain
479/// deep_rewrite turned a forwarded `vX = 25` into `25 = 25` (target
480/// replaced, and drop_defs then no longer recognized the statement, so
481/// the garbage survived into the output; reqable a4/e). Compound
482/// targets (`a[i] = …`, `o.f = …`) still recurse: owners and indices
483/// ARE reads.
484/// Read-only mirror of `deep_rewrite_reads` (same descent rules: a
485/// plain Local assignment/inc-dec target is a WRITE and never reaches
486/// `f`; compound targets still descend — owners and indices are reads).
487fn visit_exprs_reads<F: FnMut(&Expr)>(e: &Expr, f: &mut F) {
488    f(e);
489    match e {
490        Expr::Assign { target, value, .. } => {
491            if !matches!(**target, Expr::Local { .. }) {
492                visit_exprs_reads(target, f);
493            }
494            visit_exprs_reads(value, f);
495        }
496        Expr::PreIncDec { e: inner, .. } | Expr::PostIncDec { e: inner, .. } => {
497            if !matches!(**inner, Expr::Local { .. }) {
498                visit_exprs_reads(inner, f);
499            }
500        }
501        _ => for_each_child(e, &mut |c| visit_exprs_reads(c, f)),
502    }
503}
504
505fn deep_rewrite_reads<F: FnMut(&mut Expr)>(e: &mut Expr, f: &mut F) {
506    f(e);
507    match e {
508        Expr::Assign { target, value, .. } => {
509            if !matches!(**target, Expr::Local { .. }) {
510                deep_rewrite_reads(target, f);
511            }
512            deep_rewrite_reads(value, f);
513        }
514        Expr::PreIncDec { e: inner, .. } | Expr::PostIncDec { e: inner, .. } => {
515            if !matches!(**inner, Expr::Local { .. }) {
516                deep_rewrite_reads(inner, f);
517            }
518        }
519        _ => for_each_child_mut(e, &mut |c| deep_rewrite_reads(c, f)),
520    }
521}
522
523fn collect_vars(e: &Expr, out: &mut HashSet<u32>) {
524    visit_exprs(e, &mut |x| {
525        if let Expr::Local { var, .. } = x {
526            out.insert(*var);
527        }
528    });
529}
530
531/// Collect variable references (reads; `assignments` adds assignment
532/// targets). Single-pass recursion — the previous shape wrapped a
533/// self-recursive closure in `walk_all`, visiting every subtree twice over
534/// (exponential in nesting depth).
535fn stmt_collect_vars(s: &Stmt, out: &mut HashSet<u32>, assignments: bool) {
536    match s {
537        Stmt::Block(v) => {
538            for x in v {
539                stmt_collect_vars(x, out, assignments);
540            }
541        }
542        Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
543            if let Expr::Local { var, .. } = &**target {
544                if assignments {
545                    out.insert(*var);
546                }
547            } else {
548                collect_vars(target, out);
549            }
550            collect_vars(value, out);
551        }
552        Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
553            collect_vars(e, out)
554        }
555        Stmt::Return(Some(e)) => collect_vars(e, out),
556        Stmt::LocalDef { var, init, .. } => {
557            if assignments {
558                out.insert(*var);
559            }
560            if let Some(e) = init {
561                collect_vars(e, out);
562            }
563        }
564        Stmt::If {
565            cond,
566            then_stmt,
567            else_stmt,
568        } => {
569            collect_vars(cond, out);
570            stmt_collect_vars(then_stmt, out, assignments);
571            if let Some(e) = else_stmt {
572                stmt_collect_vars(e, out, assignments);
573            }
574        }
575        Stmt::While { cond, body } => {
576            collect_vars(cond, out);
577            stmt_collect_vars(body, out, assignments);
578        }
579        Stmt::DoWhile { body, cond } => {
580            stmt_collect_vars(body, out, assignments);
581            collect_vars(cond, out);
582        }
583        Stmt::For {
584            init,
585            cond,
586            update,
587            body,
588        } => {
589            for x in init {
590                stmt_collect_vars(x, out, assignments);
591            }
592            if let Some(c) = cond {
593                collect_vars(c, out);
594            }
595            for u in update {
596                collect_vars(u, out);
597            }
598            stmt_collect_vars(body, out, assignments);
599        }
600        Stmt::ForEach {
601            var,
602            iterable,
603            body,
604            ..
605        } => {
606            if assignments {
607                out.insert(*var);
608            }
609            collect_vars(iterable, out);
610            stmt_collect_vars(body, out, assignments);
611        }
612        Stmt::Switch {
613            selector,
614            cases,
615            default,
616            ..
617        } => {
618            collect_vars(selector, out);
619            for c in cases {
620                for x in &c.body {
621                    stmt_collect_vars(x, out, assignments);
622                }
623            }
624            if let Some(d) = default {
625                stmt_collect_vars(d, out, assignments);
626            }
627        }
628        Stmt::Try {
629            body,
630            catches,
631            finally,
632        } => {
633            stmt_collect_vars(body, out, assignments);
634            for c in catches {
635                stmt_collect_vars(&c.body, out, assignments);
636            }
637            if let Some(f) = finally {
638                stmt_collect_vars(f, out, assignments);
639            }
640        }
641        Stmt::TryWithResources {
642            resources,
643            body,
644            catches,
645            finally,
646        } => {
647            for x in resources {
648                stmt_collect_vars(x, out, assignments);
649            }
650            stmt_collect_vars(body, out, assignments);
651            for c in catches {
652                stmt_collect_vars(&c.body, out, assignments);
653            }
654            if let Some(f) = finally {
655                stmt_collect_vars(f, out, assignments);
656            }
657        }
658        Stmt::Synchronized { lock, body } => {
659            collect_vars(lock, out);
660            stmt_collect_vars(body, out, assignments);
661        }
662        Stmt::Assert { cond, msg } => {
663            collect_vars(cond, out);
664            if let Some(m) = msg {
665                collect_vars(m, out);
666            }
667        }
668        Stmt::Labeled { body, .. } => stmt_collect_vars(body, out, assignments),
669        _ => {}
670    }
671}
672
673pub(crate) fn walk_all<F: FnMut(&Stmt)>(s: &Stmt, f: &mut F) {
674    f(s);
675    match s {
676        Stmt::Block(v) => {
677            for x in v {
678                walk_all(x, f);
679            }
680        }
681        Stmt::If {
682            then_stmt,
683            else_stmt,
684            ..
685        } => {
686            walk_all(then_stmt, f);
687            if let Some(e) = else_stmt {
688                walk_all(e, f);
689            }
690        }
691        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => walk_all(body, f),
692        Stmt::For { init, body, .. } => {
693            for x in init {
694                walk_all(x, f);
695            }
696            walk_all(body, f);
697        }
698        Stmt::ForEach { body, .. } => walk_all(body, f),
699        Stmt::Switch { cases, default, .. } => {
700            for c in cases {
701                for x in &c.body {
702                    walk_all(x, f);
703                }
704            }
705            if let Some(d) = default {
706                walk_all(d, f);
707            }
708        }
709        Stmt::Try {
710            body,
711            catches,
712            finally,
713        } => {
714            walk_all(body, f);
715            for c in catches {
716                walk_all(&c.body, f);
717            }
718            if let Some(fl) = finally {
719                walk_all(fl, f);
720            }
721        }
722        Stmt::TryWithResources {
723            resources,
724            body,
725            catches,
726            finally,
727        } => {
728            for x in resources {
729                walk_all(x, f);
730            }
731            walk_all(body, f);
732            for c in catches {
733                walk_all(&c.body, f);
734            }
735            if let Some(fl) = finally {
736                walk_all(fl, f);
737            }
738        }
739        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => walk_all(body, f),
740        _ => {}
741    }
742}
743
744// ---------------------------------------------------------------------------
745// Passes
746// ---------------------------------------------------------------------------
747
748/// Bind catch parameters: the handler's first statement (a bare LocalDef
749/// from move-exception) becomes the catch variable.
750pub fn bind_catches(s: &mut Stmt, vt: &mut VarTable) {
751    // Vars defined anywhere in the method (params + LocalDefs +
752    // assign targets): a catch body reading a var OUTSIDE this set is
753    // the unmaterialized move-exception register (the lifter minted it
754    // without a defining statement).
755    let mut defined: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
756    for v in &vt.vars {
757        if v.is_param {
758            defined.insert(v.id);
759        }
760    }
761    collect_defined_locals(s, &mut defined);
762    let reads_all = count_locals_stmts(std::slice::from_ref(s));
763    // Vars with a real assignment (LocalDef-with-init or assign target):
764    // a var only ever READ is either the unmaterialized move-exception
765    // register or a bare declaration awaiting its hoisted assignment.
766    let mut assigned: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
767    collect_assigned_locals(s, &mut assigned);
768    let mut fallback_bound: Vec<u32> = Vec::new();
769    bind_catches_walk(s, vt, &defined, &reads_all, &assigned, &mut fallback_bound);
770    // The catch parameter IS the declaration now: drop the bare
771    // `Throwable th;` hoists for vars the fallback bound.
772    if !fallback_bound.is_empty() {
773        remove_bare_decls(s, &fallback_bound);
774    }
775}
776
777fn collect_defined_locals(s: &Stmt, out: &mut jdc_core::FxHashSet<u32>) {
778    crate::passes::walk_all(s, &mut |st| {
779        match st {
780            Stmt::LocalDef { var, .. } => {
781                out.insert(*var);
782            }
783            Stmt::ExprStmt(Expr::Assign { target, .. }) => {
784                if let Expr::Local { var, .. } = &**target {
785                    out.insert(*var);
786                }
787            }
788            _ => {}
789        }
790    });
791}
792
793/// Vars with a bare `LocalDef{init: None}` declaration in `s` (dedup,
794/// declaration order).
795fn collect_bare_decls(s: &Stmt, out: &mut Vec<u32>) {
796    walk_all(s, &mut |st| {
797        if let Stmt::LocalDef { var, init: None, .. } = st {
798            if !out.contains(var) {
799                out.push(*var);
800            }
801        }
802    });
803}
804
805
806/// First read of a local that is defined nowhere in the method (and is
807/// not a parameter) inside the catch body — the unmaterialized
808/// move-exception register.
809/// Vars carrying a real assignment (init or write target).
810fn collect_assigned_locals(s: &Stmt, out: &mut jdc_core::FxHashSet<u32>) {
811    crate::passes::walk_all(s, &mut |st| {
812        match st {
813            Stmt::LocalDef { var, init: Some(_), .. } => {
814                out.insert(*var);
815            }
816            Stmt::ExprStmt(Expr::Assign { target, .. }) => {
817                if let Expr::Local { var, .. } = &**target {
818                    out.insert(*var);
819                }
820            }
821            _ => {}
822        }
823    });
824}
825
826/// First `throw <local>` var in the catch body that is never assigned
827/// anywhere and is exception-typed — the unmaterialized move-exception.
828fn first_thrown_unassigned_local(
829    body: &Stmt,
830    vt: &VarTable,
831    assigned: &jdc_core::FxHashSet<u32>,
832) -> Option<u32> {
833    let mut found: Option<u32> = None;
834    let c = body.clone();
835    walk_all(&c, &mut |st| {
836        if let Stmt::Throw(th) = st {
837            if let Expr::Local { var, .. } = th {
838                let v = *var;
839                if !assigned.contains(&v) {
840                    if let JavaType::Object(o) = vt.var(v).ty.erased() {
841                        if o.as_ref() == "java/lang/Throwable" {
842                            found = Some(v);
843                        }
844                    }
845                }
846            }
847        }
848    });
849    found
850}
851
852/// Remove the first LEAF statement (mirror of first_leaf_stmt), pruning
853/// leading blocks that became empty — the stored-def bind consumes the
854/// move-exception decl wherever the region walk nested it.
855fn remove_first_leaf(s: &mut Stmt) {
856    if let Stmt::Block(v) = s {
857        if let Some(f) = v.first_mut() {
858            remove_first_leaf(f);
859        }
860        while matches!(v.first(), Some(Stmt::Block(b)) if b.is_empty()) {
861            v.remove(0);
862        }
863    } else {
864        *s = Stmt::Block(vec![]);
865    }
866}
867
868/// Remove bare `LocalDef{var, init: None}` declarations for the given
869/// vars (the catch parameter is their declaration now).
870fn remove_bare_decls(s: &mut Stmt, vars: &[u32]) {
871    let vars: jdc_core::FxHashSet<u32> = vars.iter().copied().collect();
872    walk_mut_deep(s, &mut |st| {
873        if let Stmt::Block(v) = st {
874            v.retain(|x| {
875                !matches!(x, Stmt::LocalDef { var, init: None, .. } if vars.contains(var))
876            });
877        }
878    });
879}
880
881fn first_undefined_local_read(
882    body: &Stmt,
883    defined: &jdc_core::FxHashSet<u32>,
884) -> Option<u32> {
885    let mut found: Option<u32> = None;
886    visit_stmt_exprs_ro(body, &mut |e| {
887        if found.is_none() {
888            visit_exprs(e, &mut |x| {
889                if found.is_none() {
890                    if let Expr::Local { var, .. } = x {
891                        if !defined.contains(var) {
892                            found = Some(*var);
893                        }
894                    }
895                }
896            });
897        }
898    });
899    found
900}
901
902#[allow(clippy::too_many_arguments)]
903fn bind_catches_walk(
904    s: &mut Stmt,
905    vt: &mut VarTable,
906    defined: &jdc_core::FxHashSet<u32>,
907    reads_all: &jdc_core::FxHashMap<u32, usize>,
908    assigned: &jdc_core::FxHashSet<u32>,
909    fallback_bound: &mut Vec<u32>,
910) {
911    match s {
912        Stmt::Try {
913            body,
914            catches,
915            finally,
916        } => {
917            bind_catches_walk(body, vt, defined, reads_all, assigned, fallback_bound);
918            for c in catches.iter_mut() {
919                if c.var == u32::MAX {
920                    // Leaf-piercing head check: the region walk can wrap
921                    // the handler body as Block[Block[…]] (weixin ri5/c
922                    // thx rethrow family — the top-level `v.first()` saw
923                    // a Block, all three bind paths missed, and the emit
924                    // fell back to `catch (Throwable ignored)` beside a
925                    // never-assigned `Throwable th;` read).
926                    let stored = match first_leaf_stmt(c.body.as_ref()) {
927                        // ONLY the bare move-exception decl (the
928                        // lifter emits LocalDef{init:None} for it).
929                        // A first stmt with a REAL init is handler
930                        // computation — consuming it as the catch
931                        // store deleted the def and hijacked its
932                        // reads to the catch param (lark ih6/w's
933                        // `v11 = Thread.currentThread()` →
934                        // `catch (InterruptedException thread2) {
935                        // thread2.interrupt(); }`; ch6/e cls4.
936                        // getName; the exception-ignoring catch is
937                        // the standard interrupt idiom).
938                        Stmt::LocalDef { var, init: None, .. } => Some(*var),
939                        // Phi-copy shape `v = exc` where exc is the
940                        // unmaterialized move-exception register
941                        // (defined nowhere in the method).
942                        Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
943                            match (&**target, &**value) {
944                                (Expr::Local { var, .. }, Expr::Local { var: src, .. })
945                                    if !defined.contains(src) =>
946                                {
947                                    Some(*var)
948                                }
949                                _ => None,
950                            }
951                        }
952                        _ => None,
953                    };
954                    if let Some(v) = stored {
955                        let exc = c
956                            .exc
957                            .first()
958                            .cloned()
959                            .unwrap_or_else(|| "java/lang/Throwable".into());
960                        let slot = vt.var(v).slot;
961                        let name = "e".to_string();
962                        let new_var =
963                            vt.add_catch_var(slot, name, TypeRef::J(JavaType::Object(exc)));
964                        remove_first_leaf(c.body.as_mut());
965                        rewrite_local_refs(c.body.as_mut(), v, new_var);
966                        c.var = new_var;
967                    } else if let Some(v) = first_undefined_local_read(c.body.as_ref(), defined)
968                        .filter(|v| {
969                            // Bind only a var read NOWHERE outside this
970                            // catch: ensure_declared would hoist an
971                            // outside-read var to a method local, and
972                            // consuming it as the catch parameter would
973                            // scope it too narrowly.
974                            let in_catch = count_locals_stmts(std::slice::from_ref(c.body.as_ref()));
975                            reads_all.get(v).copied().unwrap_or(0)
976                                == in_catch.get(v).copied().unwrap_or(0)
977                        })
978                        .filter(|v| {
979                            // And only an EXCEPTION-typed var: the
980                            // move-exception registers carry the handler
981                            // type — a plain local pending its hoisted
982                            // declaration (StringBuilder sb) must not be
983                            // consumed as the catch parameter (reqable
984                            // amazon: the declaration vanished and its
985                            // outer readers broke).
986                            let ty = vt.var(*v).ty.erased();
987                            let exc = c
988                                .exc
989                                .first()
990                                .cloned()
991                                .unwrap_or_else(|| "java/lang/Throwable".into());
992                            matches!(&ty, JavaType::Object(o)
993                                if o.as_ref() == exc.as_ref()
994                                    || o.as_ref() == "java/lang/Throwable")
995                        })
996                    {
997                        // The handler's first statement is not the
998                        // move-exception def (the d8 synchronized pattern
999                        // leads with MonitorExit): the register the lifter
1000                        // minted for move-exception is still READ in the
1001                        // body (`throw th;`) with no defining statement
1002                        // anywhere. Bind IT as the catch parameter —
1003                        // otherwise emit printed `catch (Throwable ignored)`
1004                        // over an undeclared `throw th` (definite-assignment
1005                        // failure, 6.4k weibo sites).
1006                        c.var = v;
1007                        fallback_bound.push(v);
1008                    } else if let Some(v) =
1009                        first_thrown_unassigned_local(c.body.as_ref(), vt, assigned)
1010                            .filter(|v| {
1011                                let in_catch =
1012                                    count_locals_stmts(std::slice::from_ref(c.body.as_ref()));
1013                                reads_all.get(v).copied().unwrap_or(0)
1014                                    == in_catch.get(v).copied().unwrap_or(0)
1015                            })
1016                            .filter(|v| {
1017                                // Same exception-type gate: the hoisted
1018                                // `Throwable th;` declares the var but the
1019                                // move-exception never assigned it.
1020                                let ty = vt.var(*v).ty.erased();
1021                                let exc = c
1022                                    .exc
1023                                    .first()
1024                                    .cloned()
1025                                    .unwrap_or_else(|| "java/lang/Throwable".into());
1026                                matches!(&ty, JavaType::Object(o)
1027                                    if o.as_ref() == exc.as_ref()
1028                                        || o.as_ref() == "java/lang/Throwable")
1029                            })
1030                    {
1031                        // Declared-never-assigned: `Throwable th;` hoisted
1032                        // by an earlier phase, the catch body's `throw th`
1033                        // its only use — the move-exception assignment was
1034                        // never materialized. Bind th as the catch
1035                        // parameter and drop the bare declaration.
1036                        c.var = v;
1037                        fallback_bound.push(v);
1038                    }
1039                }
1040                if c.var != u32::MAX {
1041                    // Second move-exception identity INSIDE the body: R8
1042                    // guards the handler's own close() with a nested
1043                    // try/finally and re-materializes the in-flight
1044                    // exception on its exceptional continuation as a
1045                    // fresh var (`Throwable th7; throw th7;` — weixin
1046                    // y5/b, the 327-site residue of the leaf-piercing
1047                    // fix). The twin is never ASSIGNED anywhere in the
1048                    // method (its register only ever receives
1049                    // move-exception) and the catch param IS its value —
1050                    // rewrite the twin's reads to the param and drop the
1051                    // bare decl. The never-assigned gate makes the
1052                    // rewrite flow-insensitive: every body read of the
1053                    // twin reads the exception. Twins read outside this
1054                    // catch are left alone (the param's scope ends here).
1055                    let in_catch =
1056                        count_locals_stmts(std::slice::from_ref(c.body.as_ref()));
1057                    let mut twins: Vec<u32> = Vec::new();
1058                    collect_bare_decls(c.body.as_ref(), &mut twins);
1059                    let mut swept: Vec<u32> = Vec::new();
1060                    for x in twins {
1061                        if x == c.var || assigned.contains(&x) {
1062                            continue;
1063                        }
1064                        if reads_all.get(&x).copied().unwrap_or(0)
1065                            != in_catch.get(&x).copied().unwrap_or(0)
1066                        {
1067                            continue;
1068                        }
1069                        if !matches!(vt.var(x).ty.erased(),
1070                            JavaType::Object(o) if o.as_ref() == "java/lang/Throwable")
1071                        {
1072                            continue;
1073                        }
1074                        rewrite_local_refs(c.body.as_mut(), x, c.var);
1075                        swept.push(x);
1076                    }
1077                    if !swept.is_empty() {
1078                        remove_bare_decls(c.body.as_mut(), &swept);
1079                    }
1080                }
1081                bind_catches_walk(&mut c.body, vt, defined, reads_all, assigned, fallback_bound);
1082            }
1083            if let Some(f) = finally {
1084                bind_catches_walk(f.as_mut(), vt, defined, reads_all, assigned, fallback_bound);
1085            }
1086        }
1087        Stmt::Block(v) => {
1088            for x in v.iter_mut() {
1089                bind_catches_walk(x, vt, defined, reads_all, assigned, fallback_bound);
1090            }
1091        }
1092        Stmt::If {
1093            then_stmt,
1094            else_stmt,
1095            ..
1096        } => {
1097            bind_catches_walk(then_stmt, vt, defined, reads_all, assigned, fallback_bound);
1098            if let Some(e) = else_stmt {
1099                bind_catches_walk(e, vt, defined, reads_all, assigned, fallback_bound);
1100            }
1101        }
1102        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => bind_catches_walk(body, vt, defined, reads_all, assigned, fallback_bound),
1103        Stmt::For { init, body, .. } => {
1104            for x in init.iter_mut() {
1105                bind_catches_walk(x, vt, defined, reads_all, assigned, fallback_bound);
1106            }
1107            bind_catches_walk(body, vt, defined, reads_all, assigned, fallback_bound);
1108        }
1109        Stmt::ForEach { body, .. }
1110        | Stmt::Labeled { body, .. }
1111        | Stmt::Synchronized { body, .. } => bind_catches_walk(body, vt, defined, reads_all, assigned, fallback_bound),
1112        Stmt::Switch { cases, default, .. } => {
1113            for c in cases.iter_mut() {
1114                for x in c.body.iter_mut() {
1115                    bind_catches_walk(x, vt, defined, reads_all, assigned, fallback_bound);
1116                }
1117            }
1118            if let Some(d) = default {
1119                bind_catches_walk(d, vt, defined, reads_all, assigned, fallback_bound);
1120            }
1121        }
1122        _ => {}
1123    }
1124}
1125
1126fn rewrite_local_refs(s: &mut Stmt, from: u32, to: u32) {
1127    rewrite_exprs(s, &mut |e| {
1128        deep_rewrite(e, &mut |x| {
1129            if let Expr::Local { var, .. } = x {
1130                if *var == from {
1131                    *var = to;
1132                }
1133            }
1134        });
1135    });
1136}
1137
1138/// Flatten blocks, drop empty ones and trailing no-op statements.
1139pub fn cleanup(s: &mut Stmt) {
1140    jdc_core::ir::stmt::flatten(s);
1141    prune_dead_tails(s);
1142    strip_empty(s);
1143    merge_singleton_stmts(s);
1144}
1145
1146/// JLS "cannot complete normally" pruning: drop statements after the
1147/// first hard terminator in any statement sequence — blocks, switch
1148/// case bodies (bare Vecs), for-inits, catch bodies. The 无法访问的语句
1149/// family: `continue; break;` inside switch cases and `return` after a
1150/// try{…return…}catch{…return…} (pdd h0/a, c20/d; ~5.9k sites across
1151/// pdd/cmb/jianying). Conservative: loops and switches never count as
1152/// terminators (no infinite-loop modeling), sequences still carrying
1153/// fallback Label/Goto statements stay untouched, and only
1154/// Return/Throw/Break/Continue plus If/Try/Labeled/Synchronized/Block
1155/// compositions of them terminate.
1156fn seq_terminates(s: &Stmt) -> bool {
1157    match s {
1158        Stmt::Return(_) | Stmt::Throw(_) | Stmt::Break(_) | Stmt::Continue(_) => {
1159            true
1160        }
1161        Stmt::Block(v) => v.iter().any(seq_terminates),
1162        Stmt::If {
1163            then_stmt,
1164            else_stmt,
1165            ..
1166        } => {
1167            seq_terminates(then_stmt)
1168                && else_stmt
1169                    .as_ref()
1170                    .map(|e| seq_terminates(e))
1171                    .unwrap_or(false)
1172        }
1173        Stmt::Try {
1174            body,
1175            catches,
1176            finally,
1177        }
1178        | Stmt::TryWithResources {
1179            body,
1180            catches,
1181            finally,
1182            ..
1183        } => {
1184            if let Some(f) = finally {
1185                if seq_terminates(f) {
1186                    return true;
1187                }
1188            }
1189            seq_terminates(body) && catches.iter().all(|c| seq_terminates(&c.body))
1190        }
1191        Stmt::Labeled { body, .. } | Stmt::Synchronized { body, .. } => {
1192            seq_terminates(body)
1193        }
1194        // A break-less infinite while/do-while never completes normally
1195        // either — jdc-core's label-aware analysis (the emit DEADEND
1196        // truncator uses it): statements after it are 无法访问的语句 the
1197        // same way (the bulk of the pruning residue: jianying 738).
1198        Stmt::While { .. } | Stmt::DoWhile { .. } => {
1199            jdc_core::analysis::dead_end_infinite_while(s)
1200        }
1201        _ => false,
1202    }
1203}
1204
1205fn prune_seq(v: &mut Vec<Stmt>) {
1206    if v.iter()
1207        .any(|x| matches!(x, Stmt::Label(_) | Stmt::Goto(_)))
1208    {
1209        return;
1210    }
1211    if let Some(i) = v.iter().position(seq_terminates) {
1212        v.truncate(i + 1);
1213    }
1214}
1215
1216pub fn prune_dead_tails(s: &mut Stmt) {
1217    match s {
1218        Stmt::Block(v) => {
1219            for x in v.iter_mut() {
1220                prune_dead_tails(x);
1221            }
1222            prune_seq(v);
1223        }
1224        Stmt::If {
1225            then_stmt,
1226            else_stmt,
1227            ..
1228        } => {
1229            prune_dead_tails(then_stmt);
1230            if let Some(e) = else_stmt {
1231                prune_dead_tails(e);
1232            }
1233        }
1234        Stmt::While { body, .. }
1235        | Stmt::DoWhile { body, .. }
1236        | Stmt::ForEach { body, .. }
1237        | Stmt::Labeled { body, .. }
1238        | Stmt::Synchronized { body, .. } => prune_dead_tails(body),
1239        Stmt::For { init, body, .. } => {
1240            for x in init.iter_mut() {
1241                prune_dead_tails(x);
1242            }
1243            prune_seq(init);
1244            prune_dead_tails(body);
1245        }
1246        Stmt::Switch {
1247            cases, default, ..
1248        } => {
1249            for c in cases {
1250                for x in c.body.iter_mut() {
1251                    prune_dead_tails(x);
1252                }
1253                prune_seq(&mut c.body);
1254            }
1255            if let Some(d) = default {
1256                prune_dead_tails(d);
1257            }
1258        }
1259        Stmt::Try {
1260            body,
1261            catches,
1262            finally,
1263        }
1264        | Stmt::TryWithResources {
1265            body,
1266            catches,
1267            finally,
1268            ..
1269        } => {
1270            prune_dead_tails(body);
1271            for c in catches {
1272                prune_dead_tails(&mut c.body);
1273            }
1274            if let Some(f) = finally {
1275                prune_dead_tails(f);
1276            }
1277        }
1278        _ => {}
1279    }
1280}
1281
1282/// Drop assignments whose target field is a PHANTOM: the declaring class
1283/// is pool-materialized but does not declare the field (R8 inlines R-class
1284/// constants everywhere, strips the field declarations, and leaves the
1285/// `<clinit>` husk still sput-ing to them — QQ's R$anim/kj3.a husks:
1286/// class_data with 0 fields beside a clinit of phantom sget→sput pairs,
1287/// 7,774 找不到符号 in four R.java files alone). The dex statements are
1288/// dead by construction — executing them is a guaranteed NoSuchFieldError,
1289/// and R8 only keeps them because nothing triggers the husk's clinit — so
1290/// dropping them preserves every observable behavior while the faithful
1291/// render (`x = kj3.a.x;` against declared-nowhere names) cannot compile.
1292/// Conservative gates: only plain ExprStmt assignments whose TARGET is a
1293/// phantom static/instance field of a materialized class, and whose VALUE
1294/// is side-effect-free (const / local / phantom-field read / cast chain) —
1295/// a real-field read could trigger another class's clinit, and a call
1296/// could carry side effects, so those statements stay.
1297pub fn strip_phantom_field_writes(body: &mut Stmt, pool: &DexPool) {
1298    fn phantom(pool: &DexPool, cls: &str, name: &str) -> bool {
1299        // pool.get (on-demand materialize), not get_if_materialized:
1300        // lazy getclass runs must reach the same verdicts as full runs
1301        // (the RHS holder kj3/a is not pre-materialized there).
1302        let Some(pc) = pool.get(cls) else {
1303            return false;
1304        };
1305        // The IR Field ref carries the DISPLAY name (the lifter emits
1306        // through field_display) while the pool tables carry RAW names —
1307        // match both, or an obscured-renamed field (`h` → `h17` beside
1308        // class h) reads as phantom and its const-build assignment gets
1309        // dropped, breaking enum promotion (weixin z54/h fell back to
1310        // `/* enum */ class`, taking every `h[] → Enum[]` conversion
1311        // down with it — the +193 weixin battery regression).
1312        fn declares(pc: &crate::PoolClass, cls: &str, name: &str) -> bool {
1313            pc.static_fields
1314                .iter()
1315                .chain(pc.instance_fields.iter())
1316                .any(|f| {
1317                    f.name.as_ref() == name
1318                        || jdc_core::rename::field_display(cls, &f.name, &f.desc)
1319                            .is_some_and(|d| d == name)
1320                })
1321        }
1322        if declares(pc, cls, name) {
1323            return false;
1324        }
1325        // Field resolution walks the superclass chain, so "phantom" must
1326        // be PROVEN against the whole chain — a write to an inherited
1327        // field is real code (QQ com.tencent.gdtad.statistics.d inherits
1328        // `b` from super `e`; an own-fields-only verdict dropped the
1329        // legitimate `v10x.b = gdtAd2;` — silent data loss, latent until
1330        // the walk started descending into If.then_stmt). Off-pool
1331        // ancestors are enumerated through fwdb (framework fields like
1332        // View.mID are real); an ancestor NEITHER pool nor fwdb knows
1333        // (R8-stripped class) ends the proof — keep the write.
1334        let mut cur: Option<String> = pc.super_name.clone();
1335        let mut hops = 0u32;
1336        while let Some(s) = cur {
1337            hops += 1;
1338            if hops > 64 {
1339                return false;
1340            }
1341            if let Some(sup) = pool.get(&s) {
1342                if declares(sup, &s, name) {
1343                    return false;
1344                }
1345                cur = sup.super_name.clone();
1346            } else {
1347                if !crate::fwdb::exists(&s) {
1348                    return false;
1349                }
1350                let mut found = false;
1351                crate::fwdb::for_each_field(&s, |f, _| {
1352                    if f == name {
1353                        found = true;
1354                    }
1355                });
1356                if found {
1357                    return false;
1358                }
1359                // fwdb strips the Object super edge: the chain ends here.
1360                cur = crate::fwdb::super_of(&s).map(|x| x.to_string());
1361            }
1362        }
1363        true
1364    }
1365    fn droppable_value(pool: &DexPool, e: &Expr) -> bool {
1366        match e {
1367            Expr::Const(_) | Expr::Local { .. } => true,
1368            Expr::Field { cls, name, .. } => phantom(pool, cls, name),
1369            Expr::Cast { e, .. } => droppable_value(pool, e),
1370            _ => false,
1371        }
1372    }
1373    fn is_phantom_write(pool: &DexPool, st: &Stmt) -> bool {
1374        if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = st {
1375            if let Expr::Field { cls, name, .. } = &**target {
1376                return phantom(pool, cls, name) && droppable_value(pool, value);
1377            }
1378        }
1379        false
1380    }
1381    // walk_mut_deep over every Vec-holding node — the bare-Vec positions
1382    // (switch case bodies, For init, TWR resources) are the known blind
1383    // spot of Block-only recursion.
1384    walk_mut_deep(body, &mut |st| match st {
1385        Stmt::Block(v) => v.retain(|x| !is_phantom_write(pool, x)),
1386        Stmt::Switch { cases, .. } => {
1387            for c in cases.iter_mut() {
1388                c.body.retain(|x| !is_phantom_write(pool, x));
1389            }
1390        }
1391        Stmt::For { init, .. } => init.retain(|x| !is_phantom_write(pool, x)),
1392        Stmt::TryWithResources { resources, .. } => {
1393            resources.retain(|x| !is_phantom_write(pool, x))
1394        }
1395        _ => {}
1396    });
1397    if is_phantom_write(pool, body) {
1398        *body = Stmt::Block(vec![]);
1399    }
1400}
1401
1402/// Late default-init for surviving bare declarations (`T v;`). Runs
1403/// AFTER every fold/ctor/hotfix matcher that keys on `init: None`
1404/// (bind_catches, strip_clinit_hotfix_guard, the folding prefix scans),
1405/// so nothing observes the initialized form until emission. Dex
1406/// registers are zero-initialized at method entry, so `= null/0/false`
1407/// is the FAITHFUL value for any read that reaches it before the first
1408/// write — and javac's definite assignment rejects the bare form on
1409/// partial paths (可能尚未初始化变量: pdd gi1/a obj12 assigned in two
1410/// branches and read after the merge; also turns the residual
1411/// move-exception twins into compilable — and register-faithful —
1412/// null reads instead of undefined ones).
1413pub fn init_bare_decls(body: &mut Stmt, vt: &VarTable) {
1414    walk_mut_deep(body, &mut |st| {
1415        if let Stmt::LocalDef { var, init, .. } = st {
1416            if init.is_some() {
1417                return;
1418            }
1419            let lit = match vt.var(*var).ty.erased() {
1420                JavaType::Boolean => "false",
1421                JavaType::Int
1422                | JavaType::Byte
1423                | JavaType::Short
1424                | JavaType::Char => "0",
1425                JavaType::Long => "0L",
1426                JavaType::Float => "0.0f",
1427                JavaType::Double => "0.0d",
1428                JavaType::Void => return,
1429                JavaType::Object(_) | JavaType::Array(_) => "null",
1430            };
1431            *init = Some(Expr::Raw(lit.to_string()));
1432        }
1433    });
1434}
1435
1436fn strip_empty(s: &mut Stmt) {
1437    match s {
1438        Stmt::Block(v) => {
1439            v.retain(|x| !x.is_empty_block());
1440            for x in v.iter_mut() {
1441                strip_empty(x);
1442            }
1443        }
1444        Stmt::If {
1445            then_stmt,
1446            else_stmt,
1447            ..
1448        } => {
1449            strip_empty(then_stmt);
1450            if let Some(e) = else_stmt {
1451                strip_empty(e);
1452                if e.is_empty_block() {
1453                    *else_stmt = None;
1454                }
1455            }
1456        }
1457        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => strip_empty(body),
1458        Stmt::For { init, body, .. } => {
1459            init.retain(|x| !x.is_empty_block());
1460            strip_empty(body);
1461        }
1462        Stmt::ForEach { body, .. } => strip_empty(body),
1463        Stmt::Switch { cases, default, .. } => {
1464            for c in cases {
1465                c.body.retain(|x| !x.is_empty_block());
1466            }
1467            if let Some(d) = default {
1468                strip_empty(d);
1469            }
1470        }
1471        Stmt::Try {
1472            body,
1473            catches,
1474            finally,
1475        } => {
1476            strip_empty(body);
1477            for c in catches {
1478                strip_empty(&mut c.body);
1479            }
1480            if let Some(f) = finally {
1481                strip_empty(f);
1482            }
1483        }
1484        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => strip_empty(body),
1485        _ => {}
1486    }
1487}
1488
1489fn merge_singleton_stmts(s: &mut Stmt) {
1490    if let Stmt::Block(v) = s {
1491        for x in v.iter_mut() {
1492            merge_singleton_stmts(x);
1493        }
1494    }
1495}
1496
1497/// Drop statements after a definite terminator within a block.
1498pub fn prune_unreachable(s: &mut Stmt) {
1499    if let Stmt::Block(v) = s {
1500        let mut cut: Option<usize> = None;
1501        for (i, x) in v.iter().enumerate() {
1502            if matches!(x, Stmt::Return(_) | Stmt::Throw(_)) {
1503                cut = Some(i + 1);
1504                break;
1505            }
1506        }
1507        if let Some(c) = cut {
1508            if c < v.len() {
1509                v.truncate(c);
1510            }
1511        }
1512        for x in v.iter_mut() {
1513            prune_unreachable(x);
1514        }
1515    } else {
1516        walk_mut(s, &mut prune_unreachable);
1517    }
1518}
1519
1520fn walk_mut<F: FnMut(&mut Stmt)>(s: &mut Stmt, f: &mut F) {
1521    match s {
1522        Stmt::Block(v) => {
1523            for x in v.iter_mut() {
1524                f(x);
1525            }
1526        }
1527        Stmt::If {
1528            then_stmt,
1529            else_stmt,
1530            ..
1531        } => {
1532            f(then_stmt);
1533            if let Some(e) = else_stmt {
1534                f(e);
1535            }
1536        }
1537        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => f(body),
1538        Stmt::For { init, body, .. } => {
1539            for x in init.iter_mut() {
1540                f(x);
1541            }
1542            f(body);
1543        }
1544        Stmt::ForEach { body, .. } => f(body),
1545        Stmt::Switch { cases, default, .. } => {
1546            for c in cases {
1547                for x in c.body.iter_mut() {
1548                    f(x);
1549                }
1550            }
1551            if let Some(d) = default {
1552                f(d);
1553            }
1554        }
1555        Stmt::Try {
1556            body,
1557            catches,
1558            finally,
1559        } => {
1560            f(body);
1561            for c in catches {
1562                f(&mut c.body);
1563            }
1564            if let Some(fl) = finally {
1565                f(fl);
1566            }
1567        }
1568        Stmt::TryWithResources {
1569            resources,
1570            body,
1571            catches,
1572            finally,
1573        } => {
1574            for r in resources.iter_mut() {
1575                f(r);
1576            }
1577            f(body);
1578            for c in catches {
1579                f(&mut c.body);
1580            }
1581            if let Some(fl) = finally {
1582                f(fl);
1583            }
1584        }
1585        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => f(body),
1586        _ => {}
1587    }
1588}
1589
1590/// Count LocalDef statements (pipeline diagnostics).
1591pub fn count_localdefs(s: &Stmt, out: &mut usize) {
1592    if matches!(s, Stmt::LocalDef { .. }) {
1593        *out += 1;
1594    }
1595    walk_all(s, &mut |x| {
1596        if matches!(x, Stmt::LocalDef { .. }) {
1597            *out += 1;
1598        }
1599    });
1600}
1601
1602/// `if (c) { } else { B }` → `if (!c) { B }` — the empty-then shape is
1603/// how the structurer lands an inverted diamond, and source never writes
1604/// it (jadx prints the positive form). negate() carries De Morgan so
1605/// composed conditions stay clean.
1606pub fn invert_empty_thens(s: &mut Stmt) {
1607    walk_mut_deep(s, &mut |st| {
1608        if let Stmt::If {
1609            cond,
1610            then_stmt,
1611            else_stmt,
1612        } = st
1613        {
1614            let then_empty = matches!(&**then_stmt, Stmt::Block(v) if v.is_empty());
1615            if then_empty {
1616                if let Some(els) = else_stmt.take() {
1617                    let c = std::mem::replace(cond, Expr::Const(ConstVal::Int(0)));
1618                    *cond = jdc_core::convert::negate(c);
1619                    *then_stmt = els;
1620                }
1621            }
1622        }
1623    });
1624}
1625
1626/// Fold if-diamonds back into short-circuit conditions (DAD's
1627/// short_circuit_struct, statement level). The structurer emits nested
1628/// ifs for `a && b` / `a || b` bytecode diamonds; javac source almost
1629/// never nests them, and jadx folds all four shapes:
1630///   if (c1) { if (c2) {T} else {F} } else {F}  →  if (c1 && c2) {T} else {F}
1631///   if (c1) { if (c2) {F} else {T} } else {F}  →  if (c1 && !c2) {T} else {F}
1632///   if (c1) {T} else { if (c2) {T} else {F} }  →  if (c1 || c2) {T} else {F}
1633///   if (c1) {T} else { if (c2) {F} else {T} }  →  if (c1 || !c2) {T} else {F}
1634/// Branch identity is deep structural equality (Stmt: PartialEq);
1635/// iterate to a fixpoint so chained diamonds `(a && b) && c` collapse.
1636pub fn fold_short_circuits(s: &mut Stmt) {
1637    for _ in 0..8 {
1638        let mut changed = false;
1639        walk_mut_deep(s, &mut |st| {
1640            if try_fold_diamond(st) {
1641                changed = true;
1642            }
1643        });
1644        if !changed {
1645            break;
1646        }
1647    }
1648}
1649
1650fn try_fold_diamond(st: &mut Stmt) -> bool {
1651    enum Shape {
1652        ThenAnd,
1653        ThenAndNot,
1654        ElseOr,
1655        ElseOrNot,
1656        BareAnd,
1657        BareAndNot,
1658    }
1659    // Probe the shape on an immutable borrow first (the fold takes the
1660    // whole If apart; matching and rebuilding inside one borrow is not
1661    // expressible).
1662    let shape = match &*st {
1663        // One-sided diamonds: the shared branch is the implicit empty
1664        // fall-through (`if (c1) { if (c2) {T} }` → `if (c1 && c2) {T}`).
1665        Stmt::If {
1666            then_stmt,
1667            else_stmt: None,
1668            ..
1669        } => match &**then_stmt {
1670            Stmt::If {
1671                then_stmt: t2,
1672                else_stmt: None,
1673                ..
1674            } if !matches!(&**t2, Stmt::Block(v) if v.is_empty()) => Some(Shape::BareAnd),
1675            Stmt::If {
1676                then_stmt: t2,
1677                else_stmt: Some(_),
1678                ..
1679            } if matches!(&**t2, Stmt::Block(v) if v.is_empty()) => Some(Shape::BareAndNot),
1680            _ => None,
1681        },
1682        Stmt::If {
1683            then_stmt,
1684            else_stmt: Some(els),
1685            ..
1686        } => match (&**then_stmt, &**els) {
1687            (Stmt::If { else_stmt: Some(f2), .. }, _) if f2.as_ref() == els.as_ref() => {
1688                Some(Shape::ThenAnd)
1689            }
1690            (Stmt::If { then_stmt: t2, else_stmt: Some(_), .. }, _)
1691                if t2.as_ref() == els.as_ref() =>
1692            {
1693                Some(Shape::ThenAndNot)
1694            }
1695            (_, Stmt::If { then_stmt: t2, else_stmt: Some(_), .. })
1696                if t2.as_ref() == then_stmt.as_ref() =>
1697            {
1698                Some(Shape::ElseOr)
1699            }
1700            (_, Stmt::If { else_stmt: Some(f2), .. })
1701                if f2.as_ref() == then_stmt.as_ref() =>
1702            {
1703                Some(Shape::ElseOrNot)
1704            }
1705            _ => None,
1706        },
1707        _ => None,
1708    };
1709    let Some(shape) = shape else {
1710        return false;
1711    };
1712
1713    let taken = std::mem::replace(st, Stmt::Block(vec![]));
1714    // Bare (no-else) diamonds fold without an else at all.
1715    if matches!(shape, Shape::BareAnd | Shape::BareAndNot) {
1716        let Stmt::If {
1717            cond: c1,
1718            then_stmt,
1719            else_stmt: None,
1720        } = taken
1721        else {
1722            unreachable!()
1723        };
1724        let Stmt::If {
1725            cond: c2,
1726            then_stmt: t2,
1727            else_stmt: f2,
1728        } = *then_stmt
1729        else {
1730            unreachable!()
1731        };
1732        let sc = |op: BinOp, l: Expr, r: Expr| Expr::Bin {
1733            op,
1734            l: Box::new(l),
1735            r: Box::new(r),
1736            ty: None,
1737        };
1738        match shape {
1739            // if (c1 && c2) {T}
1740            Shape::BareAnd => {
1741                *st = Stmt::If {
1742                    cond: sc(BinOp::LogAnd, c1, c2),
1743                    then_stmt: t2,
1744                    else_stmt: None,
1745                };
1746            }
1747            // if (c1) { if (c2) {} else {T} }  →  if (c1 && !c2) {T}
1748            _ => {
1749                let body = f2.unwrap();
1750                *st = Stmt::If {
1751                    cond: sc(
1752                        BinOp::LogAnd,
1753                        c1,
1754                        Expr::Un {
1755                            op: UnOp::Not,
1756                            e: Box::new(c2),
1757                        },
1758                    ),
1759                    then_stmt: body,
1760                    else_stmt: None,
1761                };
1762            }
1763        }
1764        return true;
1765    }
1766    let Stmt::If {
1767        cond: c1,
1768        then_stmt,
1769        else_stmt: Some(els),
1770    } = taken
1771    else {
1772        unreachable!("shape probe guaranteed an If with else")
1773    };
1774    let sc = |op: BinOp, l: Expr, r: Expr| Expr::Bin {
1775        op,
1776        l: Box::new(l),
1777        r: Box::new(r),
1778        ty: None,
1779    };
1780    let not = |e: Expr| Expr::Un {
1781        op: UnOp::Not,
1782        e: Box::new(e),
1783    };
1784    match shape {
1785        Shape::BareAnd | Shape::BareAndNot => unreachable!("handled above"),
1786        Shape::ThenAnd | Shape::ThenAndNot => {
1787            let Stmt::If {
1788                cond: c2,
1789                then_stmt: t2,
1790                else_stmt: Some(f2),
1791            } = *then_stmt
1792            else {
1793                unreachable!()
1794            };
1795            let (nc, body, alt) = match shape {
1796                // if (c1 && c2) {T} else {F}
1797                Shape::ThenAnd => (sc(BinOp::LogAnd, c1, c2), t2, f2),
1798                // if (c1 && !c2) {T} else {F}   (inner: {F} else {T})
1799                _ => (sc(BinOp::LogAnd, c1, not(c2)), f2, t2),
1800            };
1801            *st = Stmt::If {
1802                cond: nc,
1803                then_stmt: body,
1804                else_stmt: Some(alt),
1805            };
1806        }
1807        Shape::ElseOr | Shape::ElseOrNot => {
1808            let Stmt::If {
1809                cond: c2,
1810                then_stmt: t2,
1811                else_stmt: Some(f2),
1812            } = *els
1813            else {
1814                unreachable!()
1815            };
1816            // The surviving then-body is the OUTER then (identical to the
1817            // matching inner branch by the probe).
1818            let body = then_stmt;
1819            let (nc, alt) = match shape {
1820                // if (c1 || c2) {T} else {F}   (inner: {T} else {F})
1821                Shape::ElseOr => (sc(BinOp::LogOr, c1, c2), f2),
1822                // if (c1 || !c2) {T} else {F}  (inner: {F} else {T})
1823                _ => (sc(BinOp::LogOr, c1, not(c2)), t2),
1824            };
1825            *st = Stmt::If {
1826                cond: nc,
1827                then_stmt: body,
1828                else_stmt: Some(alt),
1829            };
1830        }
1831    }
1832    true
1833}
1834
1835/// A static initializer cannot contain `return` in source form — the
1836/// clinit's closing return is a method-level artifact, but the
1837/// structurer can leave it nested inside loops/branches where the
1838/// top-level strip cannot see it (x2/a: `static { while { ...; return;
1839/// } }` — "return outside method", 184 sites on reqable).
1840pub fn strip_clinit_returns(s: &mut Stmt) {
1841    walk_mut_deep(s, &mut |st| {
1842        if let Stmt::Block(v) = st {
1843            v.retain(|x| !matches!(x, Stmt::Return(None)));
1844        }
1845    });
1846}
1847
1848pub fn prepend_comment(s: &mut Stmt, text: String) {
1849    let stmt = Stmt::Comment(text);
1850    match s {
1851        Stmt::Block(v) => v.insert(0, stmt),
1852        other => {
1853            let inner = std::mem::replace(other, Stmt::Block(vec![]));
1854            *other = Stmt::Block(vec![stmt, inner]);
1855        }
1856    }
1857}
1858
1859/// Fused expression rewrites that each used to walk the whole tree:
1860/// residual cmp sentinels, object-null comparisons, const-first compares.
1861/// One traversal, three rules — the separate walks were a top profile cost.
1862/// True when `e` evaluates to an object reference — a legal null-compare
1863/// side: locals via the reference table, everything else via its own type
1864/// (fields like `pi.versionName != 0` and call results are references too;
1865/// constants never are).
1866fn is_obj_expr(e: &Expr, obj_var: &dyn Fn(u32) -> bool) -> bool {
1867    match e {
1868        Expr::Local { var, .. } => obj_var(*var),
1869        Expr::Const(_) => false,
1870        _ => e.type_ref().erased().is_reference(),
1871    }
1872}
1873
1874/// `obj == 0` / `0 == obj` → `obj == null`: replace the CONST-0 side.
1875/// The previous shape replaced the LOCAL side instead — every object
1876/// null-check in the corpus rendered as `null != 0` (4,598 hits in
1877/// reqable alone) and the real operand vanished from the condition.
1878fn null_side_rewrite(x: &mut Expr, obj_var: &dyn Fn(u32) -> bool) {
1879    if let Expr::Bin { op, l, r, .. } = x {
1880        if !matches!(op, BinOp::Eq | BinOp::Ne) {
1881            return;
1882        }
1883        let l_zero = matches!(&**l, Expr::Const(ConstVal::Int(0)));
1884        let r_zero = matches!(&**r, Expr::Const(ConstVal::Int(0)));
1885        if l_zero && !r_zero && is_obj_expr(r, obj_var) {
1886            **l = Expr::Const(ConstVal::Null);
1887        } else if r_zero && !l_zero && is_obj_expr(l, obj_var) {
1888            **r = Expr::Const(ConstVal::Null);
1889        }
1890    }
1891}
1892
1893pub fn fused_expr_rewrites(s: &mut Stmt, vt: &VarTable) {
1894    // var ids are dense: a byte table beats a HashSet lookup.
1895    let mut obj_vars: Vec<bool> = Vec::with_capacity(vt.vars.len());
1896    for v in &vt.vars {
1897        obj_vars.push(v.ty.erased().is_reference());
1898    }
1899    rewrite_exprs(s, &mut |e| {
1900        deep_rewrite(e, &mut |x| {
1901            // 1. cmp sentinels.
1902            if let Expr::Invokedynamic { name, args, .. } = x {
1903                if name.starts_with('\0') && args.len() == 2 {
1904                    let (cls, ty) = match name.as_str() {
1905                        "\0cmp-long" => ("java/lang/Long", JavaType::Long),
1906                        "\0cmpl-float" | "\0cmpg-float" => ("java/lang/Float", JavaType::Float),
1907                        _ => ("java/lang/Double", JavaType::Double),
1908                    };
1909                    let desc = MethodDescriptor {
1910                        args: vec![ty.clone(), ty],
1911                        ret: JavaType::Int,
1912                    };
1913                    *x = Expr::Method {
1914                        owner: None,
1915                        cls: cls.into(),
1916                        name: "compare".into(),
1917                        desc: std::sync::Arc::new(desc),
1918                        args: args.clone(),
1919                        is_static: true,
1920                        is_interface: false,
1921                        is_special: false,
1922                        is_super: false,
1923                        is_dynamic: false,
1924                        type_args: vec![],
1925                    };
1926                    return;
1927                }
1928            }
1929            // 2. null compares (no any_obj gate: the object side may be
1930            // a field/call whose type is not in the local table).
1931            null_side_rewrite(x, &|v| obj_vars.get(v as usize).copied().unwrap_or(false));
1932            // 3. const-first compares.
1933            if let Expr::Bin { op, l, r, .. } = x {
1934                if matches!(
1935                    op,
1936                    BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
1937                ) {
1938                    let l_const = matches!(&**l, Expr::Const(_));
1939                    let r_var = matches!(&**r, Expr::Local { .. });
1940                    if l_const && r_var {
1941                        std::mem::swap(l, r);
1942                    }
1943                }
1944            }
1945        });
1946    });
1947    // 4. null into reference-typed assignment targets. The register
1948    //    machine stores null as const-0; return/throw/field-store/invoke
1949    //    argument paths were normalized in the lifter, but a null that
1950    //    MATERIALIZES into a local was rendered as `str = 0;` — javac
1951    //    "int cannot be converted to String" (the single biggest error
1952    //    family on real corpora: d8 emits null locals this way after
1953    //    every `x = null` branch).
1954    walk_mut_deep(s, &mut |st| {
1955        if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = st {
1956            let tgt_is_obj = match &**target {
1957                Expr::Local { var, .. } => {
1958                    obj_vars.get(*var as usize).copied().unwrap_or(false)
1959                }
1960                other => other.type_ref().erased().is_reference(),
1961            };
1962            if tgt_is_obj {
1963                if let Expr::Const(ConstVal::Int(0)) = &**value {
1964                    **value = Expr::Const(ConstVal::Null);
1965                }
1966            }
1967        } else if let Stmt::LocalDef { var, init, .. } = st {
1968            if obj_vars.get(*var as usize).copied().unwrap_or(false) {
1969                if let Some(iv) = init {
1970                    if let Expr::Const(ConstVal::Int(0)) = iv {
1971                        *iv = Expr::Const(ConstVal::Null);
1972                    }
1973                }
1974            }
1975        }
1976    });
1977}
1978
1979/// Replace residual cmp sentinels (`\0cmp*` Invokedynamic markers that never
1980/// reached a condition) with library compare calls.
1981#[allow(dead_code)]
1982pub fn desugar_cmp_residuals(s: &mut Stmt) {
1983    rewrite_exprs(s, &mut |e| {
1984        deep_rewrite(e, &mut |x| {
1985            if let Expr::Invokedynamic { name, args, .. } = x {
1986                if name.starts_with('\0') && args.len() == 2 {
1987                    let (cls, ty) = match name.as_str() {
1988                        "\0cmp-long" => ("java/lang/Long", JavaType::Long),
1989                        "\0cmpl-float" | "\0cmpg-float" => ("java/lang/Float", JavaType::Float),
1990                        _ => ("java/lang/Double", JavaType::Double),
1991                    };
1992                    let desc = jdc_core::types::MethodDescriptor {
1993                        args: vec![ty.clone(), ty],
1994                        ret: JavaType::Int,
1995                    };
1996                    *x = Expr::Method {
1997                        owner: None,
1998                        cls: cls.into(),
1999                        name: "compare".into(),
2000                        desc: std::sync::Arc::new(desc),
2001                        args: args.clone(),
2002                        is_static: true,
2003                        is_interface: false,
2004                        is_special: false,
2005                        is_super: false,
2006                        is_dynamic: false,
2007                        type_args: vec![],
2008                    };
2009                }
2010            }
2011        });
2012    });
2013}
2014
2015/// Fold `new StringBuilder(...).append(x)...toString()` chains into
2016/// `StringConcat`. Single-assignment temporaries carry the chain.
2017///
2018/// d8 drops append results (statement-form calls), so a builder whose chain
2019/// has STATEMENT appends attached is left alone: folding its `toString`
2020/// with only the ctor arguments would misstate the contents.
2021pub fn fold_string_builders(s: &mut Stmt, vt: &VarTable) {
2022    // var → assigned value, for vars assigned exactly once.
2023    let n_vars = vt.vars.len().max(1);
2024    let mut counts = vec![0usize; n_vars];
2025    count_assignments(s, &mut counts);
2026    let mut values: Vec<Option<Expr>> = vec![None; n_vars];
2027    record_assignments(s, &counts, &mut values);
2028
2029    // Builders with statement-form appends (their chains are incomplete).
2030    let mut appended_stmts: HashSet<u32> = HashSet::default();
2031    walk_all(s, &mut |st| {
2032        if let Stmt::ExprStmt(Expr::Method {
2033            cls,
2034            name,
2035            owner,
2036            args,
2037            ..
2038        }) = st
2039        {
2040            if name.as_ref() == "append" && is_string_builder(cls) && args.len() == 1 {
2041                if let Some(Expr::Local { var, .. }) = owner.as_deref() {
2042                    appended_stmts.insert(*var);
2043                }
2044            }
2045        }
2046    });
2047
2048    rewrite_exprs(s, &mut |e| {
2049        fold_concat_in_expr(e, &values, &appended_stmts);
2050    });
2051
2052    // Drop now-unused builder temporaries (their only consumers were
2053    // folded); the drop COUNT drives iteration — no statement counting
2054    // walks at all.
2055    for _ in 0..8 {
2056        let mut reads: HashSet<u32> = HashSet::default();
2057        stmt_collect_vars(s, &mut reads, false);
2058        if drop_unused_assigns(s, &reads) == 0 {
2059            break;
2060        }
2061    }
2062}
2063
2064/// Deep statement count (pipeline guard input).
2065pub fn count_stmts_deep(s: &Stmt, out: &mut usize) {
2066    *out += 1;
2067    walk_all(s, &mut |_| {
2068        *out += 1;
2069    });
2070}
2071
2072/// Fused per-variable analysis gathered in one statement-tree walk.
2073struct VarAnalysis {
2074    assigns: Vec<usize>,
2075    reads: Vec<usize>,
2076    /// Value of the (single) assignment, when recorded.
2077    values: Vec<Option<Expr>>,
2078}
2079
2080fn analyze_vars(s: &Stmt, a: &mut VarAnalysis) {
2081    walk_all(s, &mut |st| {
2082        let exprs: Vec<&Expr> = match st {
2083            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
2084                if let Expr::Local { var, .. } = &**target {
2085                    grow_to(&mut a.assigns, *var);
2086                    let idx = *var as usize;
2087                    a.assigns[idx] += 1;
2088                    if a.assigns[idx] == 1 {
2089                        if idx >= a.values.len() {
2090                            a.values.resize(idx + 1, None);
2091                        }
2092                        a.values[idx] = Some((**value).clone());
2093                    } else if idx < a.values.len() {
2094                        a.values[idx] = None;
2095                    }
2096                }
2097                let mut v: Vec<&Expr> = vec![value];
2098                if !matches!(&**target, Expr::Local { .. }) {
2099                    v.push(target);
2100                }
2101                v
2102            }
2103            Stmt::ExprStmt(e @ (Expr::PreIncDec { .. } | Expr::PostIncDec { .. })) => {
2104                // `v++` is a read-modify-WRITE of v: counting it as a
2105                // plain read makes v a single-assign/single-read forward
2106                // candidate, and value-forwarding then fabricates `5++`
2107                // (or drops v's def and leaves ++ on an undefined name).
2108                let inner = match e {
2109                    Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => e,
2110                    _ => unreachable!(),
2111                };
2112                if let Expr::Local { var, .. } = &**inner {
2113                    grow_to(&mut a.assigns, *var);
2114                    let idx = *var as usize;
2115                    a.assigns[idx] += 1;
2116                    if idx < a.values.len() {
2117                        a.values[idx] = None;
2118                    }
2119                    return;
2120                }
2121                vec![e]
2122            }
2123            Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
2124                vec![e]
2125            }
2126            Stmt::Return(Some(e)) => vec![e],
2127            Stmt::LocalDef {
2128                var, init: Some(e), ..
2129            } => {
2130                grow_to(&mut a.assigns, *var);
2131                let idx = *var as usize;
2132                a.assigns[idx] += 1;
2133                if a.assigns[idx] == 1 {
2134                    if idx >= a.values.len() {
2135                        a.values.resize(idx + 1, None);
2136                    }
2137                    a.values[idx] = Some(e.clone());
2138                } else if idx < a.values.len() {
2139                    a.values[idx] = None;
2140                }
2141                vec![e]
2142            }
2143            Stmt::If { cond, .. } => vec![cond],
2144            Stmt::While { cond, .. } => vec![cond],
2145            Stmt::DoWhile { cond, .. } => vec![cond],
2146            _ => Vec::new(),
2147        };
2148        for e in exprs {
2149            visit_exprs(e, &mut |x| {
2150                if let Expr::Local { var, .. } = x {
2151                    grow_to(&mut a.reads, *var);
2152                    a.reads[*var as usize] += 1;
2153                }
2154            });
2155        }
2156    });
2157    // Keep the three views length-aligned.
2158    let n = a.assigns.len().max(a.reads.len()).max(a.values.len());
2159    a.assigns.resize(n, 0);
2160    a.reads.resize(n, 0);
2161    a.values.resize(n, None);
2162}
2163
2164fn grow_to(v: &mut Vec<usize>, var: u32) {
2165    if var as usize >= v.len() {
2166        v.resize(var as usize + 1, 0);
2167    }
2168}
2169
2170fn count_assignments(s: &Stmt, counts: &mut Vec<usize>) {
2171    walk_all(s, &mut |st| match st {
2172        Stmt::ExprStmt(Expr::Assign { target, .. }) => {
2173            if let Expr::Local { var, .. } = &**target {
2174                grow_to(counts, *var);
2175                counts[*var as usize] += 1;
2176            }
2177        }
2178        Stmt::LocalDef {
2179            var, init: Some(_), ..
2180        } => {
2181            grow_to(counts, *var);
2182            counts[*var as usize] += 1;
2183        }
2184        _ => {}
2185    });
2186}
2187
2188fn record_assignments(s: &Stmt, counts: &[usize], out: &mut Vec<Option<Expr>>) {
2189    walk_all(s, &mut |st| match st {
2190        Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
2191            if let Expr::Local { var, .. } = &**target {
2192                if counts.get(*var as usize).copied().unwrap_or(0) == 1 {
2193                    if *var as usize >= out.len() {
2194                        out.resize(*var as usize + 1, None);
2195                    }
2196                    out[*var as usize] = Some((**value).clone());
2197                }
2198            }
2199        }
2200        Stmt::LocalDef {
2201            var, init: Some(e), ..
2202        } if counts.get(*var as usize).copied().unwrap_or(0) == 1 => {
2203            if *var as usize >= out.len() {
2204                out.resize(*var as usize + 1, None);
2205            }
2206            out[*var as usize] = Some(e.clone());
2207        }
2208        _ => {}
2209    });
2210}
2211
2212fn fold_concat_in_expr(e: &mut Expr, values: &[Option<Expr>], appended: &HashSet<u32>) {
2213    deep_rewrite(e, &mut |x| {
2214        if let Expr::Method {
2215            cls,
2216            name,
2217            args,
2218            owner,
2219            ..
2220        } = x
2221        {
2222            if name.as_ref() == "toString" && args.is_empty() && is_string_builder(cls) {
2223                if let Some(o) = owner {
2224                    // Statement-form appends attached to any chain var mean
2225                    // the parts are incomplete — keep the call.
2226                    let mut has_stmt_appends = false;
2227                    visit_exprs(o, &mut |y| {
2228                        if let Expr::Local { var, .. } = y {
2229                            if appended.contains(var) {
2230                                has_stmt_appends = true;
2231                            }
2232                        }
2233                    });
2234                    if has_stmt_appends {
2235                        return;
2236                    }
2237                    let resolved = resolve_local(o, values);
2238                    if let Some(parts) = collect_sb_parts(&resolved, values, 0) {
2239                        // NEVER fold an empty chain: d8 splits builder
2240                        // chains across alias registers (`v2 = v0.append
2241                        // (x); v2.append(y); v0.toString()`) — the traced
2242                        // chain sees a bare `new StringBuilder` and would
2243                        // fold toString() to "" (a wrong VALUE, silently:
2244                        // lab package Obf.label). With zero parts the
2245                        // call stays — always correct, usually folded
2246                        // elsewhere once the parts are visible.
2247                        if !parts.is_empty() {
2248                            *x = Expr::StringConcat(parts);
2249                        }
2250                    }
2251                }
2252            }
2253        }
2254    });
2255}
2256
2257fn is_string_builder(cls: &str) -> bool {
2258    cls == "java/lang/StringBuilder" || cls == "java/lang/StringBuffer"
2259}
2260
2261fn resolve_local(e: &Expr, values: &[Option<Expr>]) -> Expr {
2262    match e {
2263        Expr::Local { var, ty } => values
2264            .get(*var as usize)
2265            .and_then(|o| o.clone())
2266            .unwrap_or_else(|| Expr::Local {
2267                var: *var,
2268                ty: ty.clone(),
2269            }),
2270        other => other.clone(),
2271    }
2272}
2273
2274/// Collect concat parts from a StringBuilder chain; depth caps cycles.
2275fn collect_sb_parts(e: &Expr, values: &[Option<Expr>], depth: u32) -> Option<Vec<ConcatPart>> {
2276    if depth > 16 {
2277        return None;
2278    }
2279    match e {
2280        Expr::Method {
2281            cls,
2282            name,
2283            args,
2284            owner,
2285            ..
2286        } if name.as_ref() == "append" && is_string_builder(cls) && args.len() == 1 => {
2287            let mut parts =
2288                collect_sb_parts(&resolve_local(owner.as_deref()?, values), values, depth + 1)?;
2289            parts.push(ConcatPart::Str(args[0].clone()));
2290            Some(parts)
2291        }
2292        Expr::New {
2293            cls,
2294            args,
2295            raw: false,
2296            ..
2297        } if is_string_builder(cls) => {
2298            let mut parts = Vec::new();
2299            for a in args {
2300                if let Expr::Const(ConstVal::Str(sv)) = a {
2301                    parts.push(ConcatPart::Const(sv.to_string()));
2302                } else {
2303                    parts.push(ConcatPart::Str(a.clone()));
2304                }
2305            }
2306            Some(parts)
2307        }
2308        _ => None,
2309    }
2310}
2311
2312fn drop_unused_assigns(s: &mut Stmt, reads: &HashSet<u32>) -> usize {
2313    let mut dropped = 0usize;
2314    walk_mut_deep(s, &mut |st| {
2315        if let Stmt::Block(v) = st {
2316            let before = v.len();
2317            v.retain(|x| match x {
2318                // Drop only when unread AND a builder chain (folded
2319                // consumer; `new StringBuilder` chains cannot NPE).
2320                Stmt::ExprStmt(Expr::Assign { target, value, .. }) => match &**target {
2321                    Expr::Local { var, .. } => reads.contains(var) || !is_sbish(value),
2322                    _ => true,
2323                },
2324                Stmt::LocalDef { var, init, .. } => {
2325                    reads.contains(var) || !init.as_ref().map(is_sbish).unwrap_or(false)
2326                }
2327                _ => true,
2328            });
2329            dropped += before - v.len();
2330        }
2331    });
2332    dropped
2333}
2334
2335fn is_sbish(e: &Expr) -> bool {
2336    match e {
2337        Expr::New {
2338            cls, raw: false, ..
2339        } => is_string_builder(cls),
2340        Expr::Method { name, cls, .. } => name.as_ref() == "append" && is_string_builder(cls),
2341        _ => false,
2342    }
2343}
2344
2345fn walk_mut_deep<F: FnMut(&mut Stmt)>(s: &mut Stmt, f: &mut F) {
2346    f(s);
2347    walk_mut(s, &mut |x| walk_mut_deep(x, f));
2348}
2349
2350/// Drop orphaned raw `new-instance` statements. R8 sometimes separates
2351/// `new-instance vR` from its `<init>` invoke by a register copy
2352/// (`move-object vS, vR; invoke vS.<init>(..)` — dex-legal: the init
2353/// initializes the object both registers name). The fold pairs the init
2354/// with the COPY, and the structurer's branch duplication re-materializes
2355/// the original raw marker as a bare `new C();` statement on paths whose
2356/// uses were rewired to the folded side (weixin sns/storage/c0: four
2357/// `new s1();` beside the real `s1 s1x = new s1(..)` — s1 has no no-arg
2358/// ctor, "无法将类 C的构造器 C应用到给定类型" ×195). A raw New in
2359/// statement position binds no variable and initializes nothing — pure
2360/// verifier-artifact dead code.
2361/// `try { X } finally { }` with no catches and an empty finally is a
2362/// no-op wrapper (the close-rethrow handler recast mints them) — unwrap
2363/// to X. An empty finally changes no semantics: it completes normally on
2364/// every path.
2365pub fn drop_empty_finallies(body: &mut Stmt) {
2366    fn is_empty_block(s: &Stmt) -> bool {
2367        match s {
2368            Stmt::Block(v) => v.iter().all(is_empty_block),
2369            _ => false,
2370        }
2371    }
2372    walk_mut_deep(body, &mut |st| {
2373        let Stmt::Try {
2374            body: b,
2375            catches,
2376            finally,
2377        } = st
2378        else {
2379            return;
2380        };
2381        if !catches.is_empty() {
2382            return;
2383        }
2384        match finally {
2385            // `try { X }` with neither catches nor a finally (the renderer
2386            // would paste a compilability `finally { }` back) and the
2387            // empty-finally wrapper are both no-ops — unwrap to X.
2388            None => {}
2389            Some(f) if is_empty_block(f) => {}
2390            Some(_) => return,
2391        }
2392        let inner = std::mem::replace(&mut **b, Stmt::Block(vec![]));
2393        *st = inner;
2394    });
2395}
2396
2397/// Drop orphan value pushes that render as `/* X; */` comments: expression
2398/// statements whose expression cannot throw or bind — plain locals,
2399/// constants, `this`, and field reads on the implicit `this` (owner: None;
2400/// a deref of an arbitrary receiver could NPE, so owned field reads stay).
2401pub fn drop_pure_value_stmts(body: &mut Stmt) {
2402    // Shared honest predicate (jdc-core has_side_effects): a pure read
2403    // cannot throw or bind, so the statement is dead code everywhere.
2404    fn is_pure(s: &Stmt) -> bool {
2405        let Stmt::ExprStmt(e) = s else {
2406            return false;
2407        };
2408        !has_side_effects(e)
2409    }
2410    walk_mut_deep(body, &mut |st| {
2411        // Replace in place: orphans also sit as direct If branches or a
2412        // Synchronized body where no container retain can reach them.
2413        if is_pure(st) {
2414            *st = Stmt::Block(vec![]);
2415            return;
2416        }
2417        match st {
2418            Stmt::Switch { cases, .. } => {
2419                for c in cases.iter_mut() {
2420                    c.body.retain(|x| !is_pure(x));
2421                }
2422            }
2423            Stmt::For { init, .. } => init.retain(|x| !is_pure(x)),
2424            _ => {}
2425        }
2426    });
2427}
2428
2429pub fn drop_dead_raw_news(body: &mut Stmt) {
2430    fn is_orphan(s: &Stmt) -> bool {
2431        matches!(s, Stmt::ExprStmt(Expr::New { raw: true, .. }))
2432    }
2433    walk_mut_deep(body, &mut |st| match st {
2434        Stmt::Block(v) => v.retain(|x| !is_orphan(x)),
2435        Stmt::Switch { cases, .. } => {
2436            for c in cases.iter_mut() {
2437                c.body.retain(|x| !is_orphan(x));
2438            }
2439        }
2440        Stmt::For { init, .. } => init.retain(|x| !is_orphan(x)),
2441        Stmt::TryWithResources { resources, .. } => resources.retain(|x| !is_orphan(x)),
2442        _ => {}
2443    });
2444}
2445
2446/// Recover `synchronized` from the d8 monitor pattern:
2447/// `monitorenter(e); try { body } catch (Throwable) { monitorexit(e); throw t; }`
2448/// (optionally followed by `monitorexit(e)`).
2449pub fn fold_synchronized(s: &mut Stmt) {
2450    fold_sync_walk(s);
2451}
2452
2453
2454fn fold_sync_walk(s: &mut Stmt) {
2455    walk_mut_deep(s, &mut |st| {
2456        let Stmt::Block(v) = st else { return };
2457        let mut i = 0;
2458        while i < v.len() {
2459            // A fold landing on a Synchronized re-examines the same slot:
2460            // a split-region sibling Try (Case E) only becomes visible
2461            // AFTER the [enter, try] pair folded into the block form.
2462            #[allow(unused_assignments)]
2463            let mut advance = true;
2464            if i + 1 < v.len() {
2465                if let Some((repl, consumed)) = try_sync_at(v, i) {
2466                    advance = !matches!(repl.first(), Some(Stmt::Synchronized { .. }));
2467                    let _ = v.drain(i..i + consumed);
2468                    let n = repl.len();
2469                    v.splice(i..i, repl);
2470                    if advance {
2471                        i += n;
2472                    }
2473                    continue;
2474                }
2475            }
2476            if let Some((repl, consumed)) = try_sync_in_try(v, i) {
2477                let _ = v.drain(i..i + consumed);
2478                let n = repl.len();
2479                v.splice(i..i, repl);
2480                i += n;
2481                continue;
2482            }
2483            if let Some((repl, consumed)) = try_sync_deep(v, i) {
2484                let _ = v.drain(i..i + consumed);
2485                let n = repl.len();
2486                v.splice(i..i, repl);
2487                i += n;
2488                continue;
2489            }
2490            if let Some((repl, consumed)) = try_sync_absorb_after(v, i) {
2491                let _ = v.drain(i..i + consumed);
2492                let n = repl.len();
2493                v.splice(i..i, repl);
2494                i += n;
2495                continue;
2496            }
2497            i += 1;
2498        }
2499    });
2500}
2501
2502/// Side-effect-free lock-value reads: field/local/const/this only. A call
2503/// or allocation between the monitor-enter and the try belongs to the
2504/// protected body, not to a absorbable alias head.
2505fn is_pure_read_expr(e: &Expr) -> bool {
2506    let mut pure = true;
2507    visit_exprs(e, &mut |x| match x {
2508        Expr::Const(_)
2509        | Expr::Local { .. }
2510        | Expr::This
2511        | Expr::Field { .. } => {}
2512        _ => pure = false,
2513    });
2514    pure
2515}
2516
2517/// Resolve a local through its defining expression (bounded) so lock
2518/// identity survives value-view forwarding: the enter may render as the
2519/// raw field access while the exit renders as the promoted local (or vice
2520/// versa).
2521fn resolve_lock(e: &Expr, defs: &jdc_core::FxHashMap<u32, Expr>, depth: u32) -> Expr {
2522    if depth >= 4 {
2523        return e.clone();
2524    }
2525    if let Expr::Local { var, .. } = e {
2526        if let Some(d) = defs.get(var) {
2527            return resolve_lock(d, defs, depth + 1);
2528        }
2529    }
2530    e.clone()
2531}
2532
2533fn lock_same(a: &Expr, b: &Expr) -> bool {
2534    match (a, b) {
2535        (Expr::This, Expr::This) => true,
2536        (Expr::Local { var: x, .. }, Expr::Local { var: y, .. }) => x == y,
2537        (
2538            Expr::Field {
2539                owner: oa,
2540                cls: ca,
2541                name: na,
2542                ..
2543            },
2544            Expr::Field {
2545                owner: ob,
2546                cls: cb,
2547                name: nb,
2548                ..
2549            },
2550        ) => {
2551            ca == cb
2552                && na == nb
2553                && match (oa, ob) {
2554                    (None, None) => true,
2555                    (Some(ra), Some(rb)) => lock_same(ra, rb),
2556                    _ => false,
2557                }
2558        }
2559        (Expr::Const(x), Expr::Const(y)) => x == y,
2560        _ => false,
2561    }
2562}
2563
2564fn lock_equiv(a: &Expr, b: &Expr, defs: &jdc_core::FxHashMap<u32, Expr>) -> bool {
2565    lock_same(&resolve_lock(a, defs, 0), &resolve_lock(b, defs, 0))
2566}
2567
2568fn flatten_leaves<'a>(s: &'a Stmt, out: &mut Vec<&'a Stmt>) {
2569    match s {
2570        Stmt::Block(v) => {
2571            for x in v {
2572                flatten_leaves(x, out);
2573            }
2574        }
2575        _ => out.push(s),
2576    }
2577}
2578
2579/// Every `MonitorExit(lock)` inside the folded body is a compiler artifact
2580/// (the implicit release on each exit path) — drop them so the body reads
2581/// clean and the absorbed lock alias loses its last reads.
2582fn strip_lock_exits(s: &mut Stmt, lock: &Expr, defs: &jdc_core::FxHashMap<u32, Expr>) {
2583    walk_mut_deep(s, &mut |st| {
2584        let Stmt::Block(v) = st else { return };
2585        v.retain(|x| !matches!(x, Stmt::MonitorExit(e) if lock_equiv(e, lock, defs)));
2586    });
2587}
2588
2589fn try_sync_at(v: &[Stmt], i: usize) -> Option<(Vec<Stmt>, usize)> {
2590    // Local defs in scope before the enter, for lock resolution.
2591    let mut defs: jdc_core::FxHashMap<u32, Expr> = jdc_core::FxHashMap::default();
2592    for s in &v[..i] {
2593        if let Stmt::LocalDef { var, init: Some(e), .. } = s {
2594            defs.insert(*var, e.clone());
2595        }
2596    }
2597    // Case A: the enter is a direct sibling; Case B: it ends a child Block
2598    // (the converter wraps the pre-try statements of the entry block, so
2599    // the Try sits beside the wrapper, not beside the enter).
2600    let (pre, enter_e, mut j, mut absorbed): (Vec<Stmt>, &Expr, usize, Vec<Stmt>) = match &v[i] {
2601        Stmt::MonitorEnter(e) => (Vec::new(), e, i + 1, Vec::new()),
2602        Stmt::Block(b) => {
2603            let split = b
2604                .iter()
2605                .position(|x| matches!(x, Stmt::MonitorEnter(_)))?;
2606            let e = match &b[split] {
2607                Stmt::MonitorEnter(e) => e,
2608                _ => unreachable!(),
2609            };
2610            let mut absorbed: Vec<Stmt> = Vec::new();
2611            let mut k = split + 1;
2612            while let Some(Stmt::LocalDef { var, init: Some(ex), .. }) = b.get(k) {
2613                if !is_pure_read_expr(ex) {
2614                    break;
2615                }
2616                defs.insert(*var, ex.clone());
2617                absorbed.push(b[k].clone());
2618                k += 1;
2619            }
2620            // Only a tail split: anything after the absorbable run (other
2621            // than nothing) means real code follows inside the block.
2622            if k != b.len() {
2623                return None;
2624            }
2625            let pre: Vec<Stmt> = b[..split].to_vec();
2626            for s in &pre {
2627                if let Stmt::LocalDef { var, init: Some(e2), .. } = s {
2628                    defs.insert(*var, e2.clone());
2629                }
2630            }
2631            (pre, e, i + 1, absorbed)
2632        }
2633        _ => return None,
2634    };
2635    // Absorb pure alias defs parked between the enter and the try (the
2636    // value-view promotion hoists the lock register's declaration ahead of
2637    // the try span). They become the synchronized body's head.
2638    let mut absorbed_vars: Vec<u32> = Vec::new();
2639    while let Some(Stmt::LocalDef { var, init: Some(e), .. }) = v.get(j) {
2640        if !is_pure_read_expr(e) {
2641            break;
2642        }
2643        defs.insert(*var, e.clone());
2644        absorbed_vars.push(*var);
2645        absorbed.push(v[j].clone());
2646        j += 1;
2647    }
2648    let Stmt::Try {
2649        body,
2650        catches,
2651        finally,
2652    } = v.get(j)?
2653    else {
2654        return None;
2655    };
2656    if finally.is_some() || catches.len() != 1 {
2657        return None;
2658    }
2659    let c = &catches[0];
2660    if !c.exc.is_empty() {
2661        return None;
2662    }
2663    // The catch-all body: monitorexit(lock) then rethrow (the handler walk
2664    // may nest it as Block[Block[...]]).
2665    let mut flat: Vec<&Stmt> = Vec::new();
2666    flatten_leaves(c.body.as_ref(), &mut flat);
2667    if flat.len() < 2 {
2668        return None;
2669    }
2670    let Stmt::MonitorExit(exit_e) = flat[0] else {
2671        return None;
2672    };
2673    if !lock_equiv(enter_e, exit_e, &defs) {
2674        return None;
2675    }
2676    if !matches!(flat[flat.len() - 1], Stmt::Throw(_)) {
2677        return None;
2678    }
2679    for mid in &flat[1..flat.len() - 1] {
2680        if !matches!(mid, Stmt::MonitorEnter(_) | Stmt::MonitorExit(_)) {
2681            return None;
2682        }
2683    }
2684    // An absorbed alias read anywhere after the try would escape the
2685    // synchronized scope once folded — refuse the fold instead.
2686    if !absorbed_vars.is_empty() {
2687        let outside = count_locals_stmts(&v[j + 1..]);
2688        if absorbed_vars
2689            .iter()
2690            .any(|av| outside.get(av).copied().unwrap_or(0) > 0)
2691        {
2692            return None;
2693        }
2694    }
2695    let mut body_stmt = (**body).clone();
2696    strip_lock_exits(&mut body_stmt, enter_e, &defs);
2697    absorbed.push(body_stmt);
2698    let mut consumed = j - i + 1;
2699    // A matching trailing monitor-exit after the try (the normal-path
2700    // release parked at the join) is implicit in the block form too.
2701    if let Some(Stmt::MonitorExit(e)) = v.get(j + 1) {
2702        if lock_equiv(e, enter_e, &defs) {
2703            consumed += 1;
2704        }
2705    }
2706    let mut repl = pre;
2707    repl.push(Stmt::Synchronized {
2708        lock: enter_e.clone(),
2709        body: Box::new(Stmt::Block(absorbed)),
2710    });
2711    Some((repl, consumed))
2712}
2713
2714/// Case C: the monitor-enter sits INSIDE the try body (d8 extends the
2715/// protected range over the lock evaluation: `try { v=lock; monitorenter v;
2716/// rest } catch-all { monitorexit v; throw }`). Everything up to and
2717/// including the enter folds into the synchronized body — the block form
2718/// re-protects it.
2719fn try_sync_in_try(v: &[Stmt], i: usize) -> Option<(Vec<Stmt>, usize)> {
2720    let Stmt::Try {
2721        body,
2722        catches,
2723        finally,
2724    } = &v[i]
2725    else {
2726        return None;
2727    };
2728    if finally.is_some() || catches.len() != 1 {
2729        return None;
2730    }
2731    let c = &catches[0];
2732    if !c.exc.is_empty() {
2733        return None;
2734    }
2735    let mut flat: Vec<&Stmt> = Vec::new();
2736    flatten_leaves(c.body.as_ref(), &mut flat);
2737    if flat.len() < 2 {
2738        return None;
2739    }
2740    let Stmt::MonitorExit(_) = flat[0] else {
2741        return None;
2742    };
2743    if !matches!(flat[flat.len() - 1], Stmt::Throw(_)) {
2744        return None;
2745    }
2746    for mid in &flat[1..flat.len() - 1] {
2747        if !matches!(mid, Stmt::MonitorEnter(_) | Stmt::MonitorExit(_)) {
2748            return None;
2749        }
2750    }
2751    // Locate the enter inside the body's top level: a direct MonitorEnter
2752    // or a child Block whose tail is [MonitorEnter, pure-defs*]. The
2753    // wrapped prelude statements stay ahead of the fold point.
2754    let Stmt::Block(bv) = body.as_ref() else {
2755        return None;
2756    };
2757    let mut defs: jdc_core::FxHashMap<u32, Expr> = jdc_core::FxHashMap::default();
2758    for s in &v[..i] {
2759        if let Stmt::LocalDef { var, init: Some(e), .. } = s {
2760            defs.insert(*var, e.clone());
2761        }
2762    }
2763    let mut pre: Vec<Stmt> = Vec::new();
2764    let mut enter_e: Option<Expr> = None;
2765    let mut absorbed: Vec<Stmt> = Vec::new();
2766    let mut absorbed_vars: Vec<u32> = Vec::new();
2767    let mut rest_start: Option<usize> = None;
2768    for (p, el) in bv.iter().enumerate() {
2769        match el {
2770            Stmt::MonitorEnter(e) => {
2771                enter_e = Some(e.clone());
2772                rest_start = Some(p + 1);
2773                break;
2774            }
2775            Stmt::Block(inner) => {
2776                let split = inner
2777                    .iter()
2778                    .position(|x| matches!(x, Stmt::MonitorEnter(_)));
2779                if let Some(sp) = split {
2780                    let mut k = sp + 1;
2781                    let mut ok = true;
2782                    let mut tail: Vec<Stmt> = Vec::new();
2783                    while let Some(Stmt::LocalDef { var: _, init: Some(ex), .. }) = inner.get(k) {
2784                        if !is_pure_read_expr(ex) {
2785                            ok = false;
2786                            break;
2787                        }
2788                        tail.push(inner[k].clone());
2789                        k += 1;
2790                    }
2791                    if ok && k == inner.len() {
2792                        for s in &inner[..sp] {
2793                            if let Stmt::LocalDef { var, init: Some(ex), .. } = s {
2794                                defs.insert(*var, ex.clone());
2795                            }
2796                        }
2797                        pre.extend(inner[..sp].iter().cloned());
2798                        enter_e = Some(match &inner[sp] {
2799                            Stmt::MonitorEnter(e) => e.clone(),
2800                            _ => unreachable!(),
2801                        });
2802                        absorbed = tail;
2803                        rest_start = Some(p + 1);
2804                        break;
2805                    }
2806                }
2807                // Not a fold point: a plain nested block ahead of the enter.
2808                for s in inner.iter() {
2809                    if let Stmt::LocalDef { var, init: Some(ex), .. } = s {
2810                        defs.insert(*var, ex.clone());
2811                    }
2812                }
2813                pre.push(el.clone());
2814            }
2815            other => {
2816                if let Stmt::LocalDef { var, init: Some(ex), .. } = other {
2817                    defs.insert(*var, ex.clone());
2818                }
2819                pre.push(other.clone());
2820            }
2821        }
2822    }
2823    let enter_e = enter_e?;
2824    let rs = rest_start?;
2825    // Pure defs directly after the enter also join the body head.
2826    let mut q = rs;
2827    while let Some(Stmt::LocalDef { var, init: Some(ex), .. }) = bv.get(q) {
2828        if !is_pure_read_expr(ex) {
2829            break;
2830        }
2831        defs.insert(*var, ex.clone());
2832        absorbed_vars.push(*var);
2833        absorbed.push(bv[q].clone());
2834        q += 1;
2835    }
2836    let rest: Vec<Stmt> = bv[q..].to_vec();
2837    if rest.is_empty() {
2838        return None;
2839    }
2840    // A non-def prelude would move real code past the lock acquisition —
2841    // the deep fold (try kept, synchronized in place) owns that shape.
2842    if pre
2843        .iter()
2844        .any(|s| !matches!(s, Stmt::LocalDef { init: Some(e), .. } if is_pure_read_expr(e)))
2845    {
2846        return None;
2847    }
2848    // The catch's release must name the same lock.
2849    let Stmt::MonitorExit(exit_e) = flat[0] else {
2850        return None;
2851    };
2852    if !lock_equiv(&enter_e, exit_e, &defs) {
2853        return None;
2854    }
2855    // Pre/absorbed defs must not be read outside the try.
2856    let outside_vars: Vec<u32> = pre
2857        .iter()
2858        .chain(absorbed.iter())
2859        .filter_map(|s| match s {
2860            Stmt::LocalDef { var, .. } => Some(*var),
2861            _ => None,
2862        })
2863        .collect();
2864    if !outside_vars.is_empty() {
2865        let outside = count_locals_stmts(&v[i + 1..]);
2866        if outside_vars
2867            .iter()
2868            .any(|av| outside.get(av).copied().unwrap_or(0) > 0)
2869        {
2870            return None;
2871        }
2872    }
2873    let mut body_stmts = pre;
2874    body_stmts.extend(absorbed);
2875    body_stmts.extend(rest);
2876    let mut sync_body = Stmt::Block(body_stmts);
2877    strip_lock_exits(&mut sync_body, &enter_e, &defs);
2878    let mut consumed = 1;
2879    if let Some(Stmt::MonitorExit(e)) = v.get(i + 1) {
2880        if lock_equiv(e, &enter_e, &defs) {
2881            consumed += 1;
2882        }
2883    }
2884    Some((
2885        vec![Stmt::Synchronized {
2886            lock: enter_e,
2887            body: Box::new(sync_body),
2888        }],
2889        consumed,
2890    ))
2891}
2892
2893/// Case E: the protected region was SPLIT across groups — a named try/catch
2894/// nested inside the synchronized region renders as a SIBLING Try after the
2895/// folded Synchronized, with the region's normal-path release parked after
2896/// it (d8 splits javac-style around inner returns; the inner range carries
2897/// its own named handler plus the shared catch-all, so the same-handler
2898/// group merge refuses it and the structurer emits two sibling tries).
2899/// `[Synchronized, Try, MonitorExit(lock)]` -> the Try joins the lock body
2900/// and the parked release is implicit. Without this the try escapes the
2901/// lock (racy field writes) and unexpected exceptions leak the monitor.
2902fn try_sync_absorb_after(v: &[Stmt], i: usize) -> Option<(Vec<Stmt>, usize)> {
2903    let Stmt::Synchronized { lock, body } = &v[i] else {
2904        return None;
2905    };
2906    let Stmt::Try {
2907        body: tbody,
2908        catches,
2909        finally,
2910    } = v.get(i + 1)?
2911    else {
2912        return None;
2913    };
2914    if finally.is_some() {
2915        return None;
2916    }
2917    // The parked release sits at the protected region's end: everything on
2918    // the normal path before it ran INSIDE the lock (phi commits feeding
2919    // the in-lock return), everything after it is post-release. Scan past
2920    // orderable statements (register commits, pure reads, Block wrappers)
2921    // to the matching release; absorb the run into the lock body.
2922    let defs = defs_probe(v, i, body);
2923    let mut absorbed_mid: Vec<Stmt> = Vec::new();
2924    let mut rest_after: Option<Stmt> = None;
2925    let mut found = false;
2926    let mut scan = i + 2;
2927    'scan: while scan < v.len() && scan < i + 14 {
2928        match &v[scan] {
2929            Stmt::MonitorExit(e) => {
2930                if !lock_equiv(e, lock, &defs) {
2931                    return None;
2932                }
2933                found = true;
2934                scan += 1;
2935                break;
2936            }
2937            Stmt::Block(bv) => {
2938                for (k, s) in bv.iter().enumerate() {
2939                    match s {
2940                        Stmt::MonitorExit(e) => {
2941                            if !lock_equiv(e, lock, &defs) {
2942                                return None;
2943                            }
2944                            absorbed_mid.push(Stmt::Block(bv[..k].to_vec()));
2945                            let rest = &bv[k + 1..];
2946                            rest_after = (!rest.is_empty())
2947                                .then(|| Stmt::Block(rest.to_vec()));
2948                            found = true;
2949                            scan += 1;
2950                            break 'scan;
2951                        }
2952                        Stmt::ExprStmt(e) if mid_safe(e) || is_local_commit(e) => {}
2953                        _ => break 'scan,
2954                    }
2955                }
2956                absorbed_mid.push(v[scan].clone());
2957                scan += 1;
2958            }
2959            Stmt::ExprStmt(e) if mid_safe(e) || is_local_commit(e) => {
2960                absorbed_mid.push(v[scan].clone());
2961                scan += 1;
2962            }
2963            _ => break,
2964        }
2965    }
2966    if !found {
2967        return None;
2968    }
2969    let consumed = scan - i;
2970    // The adopted try's implicit releases and the handler's own release are
2971    // owned by the synchronized once inside — strip them.
2972    let mut t = (**tbody).clone();
2973    let defs = defs_probe(v, i, body);
2974    strip_lock_exits(&mut t, lock, &defs);
2975    let mut cs = Vec::with_capacity(catches.len());
2976    for c in catches.iter() {
2977        let mut cb = (*c.body).clone();
2978        strip_lock_exits(&mut cb, lock, &defs);
2979        // A catch-all reduced to a bare rethrow of the catch parameter is
2980        // the synchronized's own handler — drop it; named catches keep
2981        // (dropping one would widen the checked-exception surface).
2982        let bare_rethrow = c.exc.is_empty() && {
2983            let mut leaves: Vec<&Stmt> = Vec::new();
2984            flatten_leaves(&cb, &mut leaves);
2985            matches!(leaves.as_slice(), [Stmt::Throw(Expr::Local { var, .. })] if *var == c.var)
2986        };
2987        if bare_rethrow {
2988            continue;
2989        }
2990        let mut c2 = c.clone();
2991        *c2.body = cb;
2992        cs.push(c2);
2993    }
2994    let mut new_body = (**body).clone();
2995    let adopted = Stmt::Try {
2996        body: Box::new(t),
2997        catches: cs,
2998        finally: None,
2999    };
3000    match &mut new_body {
3001        Stmt::Block(bv) => bv.push(adopted),
3002        other => {
3003            let inner = std::mem::replace(other, Stmt::Block(vec![]));
3004            *other = Stmt::Block(vec![inner, adopted]);
3005        }
3006    }
3007    if let Stmt::Block(bv) = &mut new_body {
3008        bv.extend(absorbed_mid);
3009    }
3010    let sync = Stmt::Synchronized {
3011        lock: lock.clone(),
3012        body: Box::new(new_body),
3013    };
3014    let mut repl = vec![sync];
3015    if let Some(r) = rest_after {
3016        repl.push(r);
3017    }
3018    Some((repl, consumed))
3019}
3020
3021/// Statements orderable into the lock body: pure reads (cannot throw or
3022/// bind) and — handled by the caller's pattern — plain local-to-local
3023/// register commits.
3024fn mid_safe(e: &Expr) -> bool {
3025    !has_side_effects(e)
3026}
3027
3028/// Plain `v = local;` register commit (phi materialization at the join).
3029fn is_local_commit(e: &Expr) -> bool {
3030    match e {
3031        Expr::Assign { target, op: AssignOp::Plain, .. } => {
3032            matches!(&**target, Expr::Local { .. })
3033        }
3034        _ => false,
3035    }
3036}
3037
3038/// Defs visible to the parked release: statements before the fold plus the
3039/// absorbed alias head at the synchronized body's top level.
3040fn defs_probe<'a>(
3041    v: &'a [Stmt],
3042    i: usize,
3043    body: &'a Stmt,
3044) -> jdc_core::FxHashMap<u32, Expr> {
3045    let mut defs: jdc_core::FxHashMap<u32, Expr> = jdc_core::FxHashMap::default();
3046    for s in &v[..i] {
3047        if let Stmt::LocalDef { var, init: Some(e), .. } = s {
3048            defs.insert(*var, e.clone());
3049        }
3050    }
3051    if let Stmt::Block(bv) = body {
3052        for s in bv {
3053            if let Stmt::LocalDef { var, init: Some(e), .. } = s {
3054                defs.insert(*var, e.clone());
3055            }
3056        }
3057    }
3058    defs
3059}
3060
3061
3062/// Case D: the enter sits mid-control-flow inside the try body (nested in
3063/// if/else branches). The spine from the body root to the enter must end
3064/// at the body end — nothing may follow the fold point at any ancestor
3065/// level. The Try STAYS; only the enter's tail becomes a synchronized
3066/// block in place. The catch's release is dropped: with the lock release
3067/// owned by the synchronized, the handler's monitorexit would double-release
3068/// (the sync's implicit release fires first, then the outer catch
3069/// re-releases an unheld monitor).
3070fn try_sync_deep(v: &[Stmt], i: usize) -> Option<(Vec<Stmt>, usize)> {
3071    let Stmt::Try {
3072        body,
3073        catches,
3074        finally,
3075    } = &v[i]
3076    else {
3077        return None;
3078    };
3079    if finally.is_some() || catches.len() != 1 {
3080        return None;
3081    }
3082    let c = &catches[0];
3083    if !c.exc.is_empty() {
3084        return None;
3085    }
3086    let mut flat: Vec<&Stmt> = Vec::new();
3087    flatten_leaves(c.body.as_ref(), &mut flat);
3088    if flat.len() < 2 {
3089        return None;
3090    }
3091    let catch_exit = match flat[0] {
3092        Stmt::MonitorExit(e) => e.clone(),
3093        _ => return None,
3094    };
3095    if !matches!(flat[flat.len() - 1], Stmt::Throw(_)) {
3096        return None;
3097    }
3098    for mid in &flat[1..flat.len() - 1] {
3099        if !matches!(mid, Stmt::MonitorEnter(_) | Stmt::MonitorExit(_)) {
3100            return None;
3101        }
3102    }
3103    let mut defs0: jdc_core::FxHashMap<u32, Expr> = jdc_core::FxHashMap::default();
3104    for s in &v[..i] {
3105        if let Stmt::LocalDef { var, init: Some(e), .. } = s {
3106            defs0.insert(*var, e.clone());
3107        }
3108    }
3109    let mut body_mut = (**body).clone();
3110    let folded = match &mut body_mut {
3111        Stmt::Block(b) => try_fold_spine(b, &catch_exit, &defs0),
3112        Stmt::If {
3113            then_stmt,
3114            else_stmt,
3115            ..
3116        } => {
3117            let mut hit = false;
3118            if let Stmt::Block(b) = &mut **then_stmt {
3119                hit |= try_fold_spine(b, &catch_exit, &defs0);
3120            }
3121            if !hit {
3122                if let Some(els) = else_stmt {
3123                    if let Stmt::Block(b) = &mut **els {
3124                        hit |= try_fold_spine(b, &catch_exit, &defs0);
3125                    }
3126                }
3127            }
3128            hit
3129        }
3130        _ => false,
3131    };
3132    if !folded {
3133        return None;
3134    }
3135    // Reduce the catch-all to a bare rethrow (see fn doc).
3136    let throw = flat[flat.len() - 1].clone();
3137    let mut catches_out = catches.clone();
3138    *catches_out[0].body = Stmt::Block(vec![throw]);
3139    let mut consumed = 1;
3140    if let Some(Stmt::MonitorExit(e)) = v.get(i + 1) {
3141        if lock_equiv(e, &catch_exit, &defs0) {
3142            consumed += 1;
3143        }
3144    }
3145    Some((
3146        vec![Stmt::Try {
3147            body: Box::new(body_mut),
3148            catches: catches_out,
3149            finally: None,
3150        }],
3151        consumed,
3152    ))
3153}
3154
3155/// Fold `[ …pre…, monitorenter(e), rest… ]` whose spine reaches the
3156/// sequence end: at the enter's own level everything after it joins the
3157/// synchronized body; at every ancestor level the spine element must be
3158/// the LAST element (branches are alternatives, not successors). Mutates
3159/// in place; returns true when a fold landed.
3160fn try_fold_spine(
3161    seq: &mut Vec<Stmt>,
3162    catch_exit: &Expr,
3163    defs_in: &jdc_core::FxHashMap<u32, Expr>,
3164) -> bool {
3165    for p in (0..seq.len()).rev() {
3166        // Direct enter at this level: everything after it joins the body.
3167        if let Stmt::MonitorEnter(e0) = &seq[p] {
3168            let mut defs = defs_in.clone();
3169            for s in &seq[..p] {
3170                if let Stmt::LocalDef { var, init: Some(ex), .. } = s {
3171                    defs.insert(*var, ex.clone());
3172                }
3173            }
3174            // Lock-relevant defs parked just after the enter (the promoted
3175            // lock local's declaration) resolve the catch's release.
3176            for s in &seq[p + 1..] {
3177                match s {
3178                    Stmt::LocalDef { var, init: Some(ex), .. } if is_pure_read_expr(ex) => {
3179                        defs.insert(*var, ex.clone());
3180                    }
3181                    _ => break,
3182                }
3183            }
3184            if !lock_equiv(e0, catch_exit, &defs) {
3185                return false;
3186            }
3187            let enter_e = e0.clone();
3188            let rest: Vec<Stmt> = seq.split_off(p + 1);
3189            let mut body = Stmt::Block(rest);
3190            strip_lock_exits(&mut body, &enter_e, &defs);
3191            seq[p] = Stmt::Synchronized {
3192                lock: enter_e,
3193                body: Box::new(body),
3194            };
3195            return true;
3196        }
3197        // Nested attempt on a clone; commit only when the spine element is
3198        // this level's last (nothing may follow the fold point).
3199        let is_last = p + 1 == seq.len();
3200        let mut trial = seq[p].clone();
3201        let hit = match &mut trial {
3202            Stmt::Block(b) => try_fold_spine(b, catch_exit, defs_in),
3203            Stmt::If {
3204                then_stmt,
3205                else_stmt,
3206                ..
3207            } => {
3208                let mut hit = false;
3209                if let Stmt::Block(b) = &mut **then_stmt {
3210                    hit |= try_fold_spine(b, catch_exit, defs_in);
3211                }
3212                if !hit {
3213                    if let Some(els) = else_stmt {
3214                        if let Stmt::Block(b) = &mut **els {
3215                            hit |= try_fold_spine(b, catch_exit, defs_in);
3216                        }
3217                    }
3218                }
3219                hit
3220            }
3221            _ => false,
3222        };
3223        if hit {
3224            if is_last {
3225                seq[p] = trial;
3226                return true;
3227            }
3228            return false;
3229        }
3230    }
3231    false
3232}
3233
3234/// Copy-forward single-use temporaries: `v = e; ... v ...` inlines `e` at
3235/// the (single) use and drops the assignment when safe. Pure values inline
3236/// anywhere; impure values (calls, array reads) only when the use is the
3237/// statement immediately after the definition.
3238pub fn forward_single_use(s: &mut Stmt, _vt: &VarTable) {
3239    // Fused single traversal: assignment counts, read counts and the
3240    // single-assignment values all come out of ONE walk (three separate
3241    // full-tree walks dominated the pass cost on large methods).
3242    let mut analysis = VarAnalysis {
3243        assigns: Vec::new(),
3244        reads: Vec::new(),
3245        values: Vec::new(),
3246    };
3247    analyze_vars(s, &mut analysis);
3248    let mut assigns = analysis.assigns;
3249    let mut reads = analysis.reads;
3250    // Var ids beyond the table (synthetic test shapes) are covered by the
3251    // counters' on-demand growth.
3252    let n_vars = assigns.len().max(_vt.vars.len()).max(1);
3253    assigns.resize(n_vars, 0);
3254    reads.resize(n_vars, 0);
3255    let mut values = analysis.values;
3256    values.resize(n_vars, None);
3257
3258    // Candidates: assigned exactly once, read exactly once. Additionally
3259    // the inlined VALUE must not reference a multi-assigned var (phi vars):
3260    // moving the read across the phi's reassignment would be a stale
3261    // capture (register rotations snapshot through temps for this reason).
3262    let mut cand = vec![false; n_vars];
3263    let mut edges: Vec<Vec<u32>> = vec![Vec::new(); n_vars];
3264    for (v, cand_v) in cand.iter_mut().enumerate() {
3265        if assigns.get(v).copied().unwrap_or(0) != 1 || reads.get(v).copied().unwrap_or(0) != 1 {
3266            continue;
3267        }
3268        if let Some(val) = values.get(v).and_then(|o| o.as_ref()) {
3269            let mut refs = HashSet::default();
3270            collect_vars(val, &mut refs);
3271            if refs.iter().any(|r| assigns[*r as usize] > 1) {
3272                continue;
3273            }
3274            // Growth-graph edge set, built in the same walk: inlining v
3275            // inserts a clone of its value, and every Local inside the
3276            // clone is replaced in turn.
3277            edges[v] = refs.into_iter().collect();
3278        }
3279        *cand_v = true;
3280    }
3281
3282    // Cycle rejection on the growth graph. A def-reference cycle
3283    // (v = f(w), w = g(v) — loop-carried register rotation reaching the
3284    // pass as two mutually-referencing single-assign/single-read locals)
3285    // re-introduces the other cycle Local at every replacement level, so
3286    // deep_rewrite grows the tree one layer per level and never
3287    // terminates: weixin's com/tencent/mm/plugin/appbrand/widget/input/b4
3288    // exhausted a 64MB worker stack at ~300k recursion frames. Edges into
3289    // non-candidates are harmless (they are never replaced, so growth
3290    // dies there — and they carry no outgoing edges).
3291    //
3292    // Three-color DFS, iterative: a legit 60k-long single-use chain must
3293    // not trade one stack overflow for another. A GRAY child closes a
3294    // cycle; `bad[v]` (v on a cycle, or v's inlined closure grows into
3295    // one) then taints the whole current path — everything on it reaches
3296    // the cycle. Black verdicts memoize: a node whose subtree was proven
3297    // clean cannot grow a cycle later.
3298    let mut color = vec![0u8; n_vars]; // 0 white, 1 gray, 2 black
3299    let mut bad = vec![false; n_vars];
3300    for root in 0..n_vars {
3301        if color[root] != 0 || edges[root].is_empty() {
3302            continue;
3303        }
3304        color[root] = 1;
3305        let mut stack: Vec<(usize, usize)> = vec![(root, 0)];
3306        while let Some(top) = stack.last_mut() {
3307            let v = top.0;
3308            let i = top.1;
3309            top.1 += 1;
3310            if i < edges[v].len() {
3311                let r = edges[v][i] as usize;
3312                if color[r] == 1 {
3313                    for &(pn, _) in &stack {
3314                        bad[pn] = true;
3315                    }
3316                } else if color[r] == 0 {
3317                    color[r] = 1;
3318                    stack.push((r, 0));
3319                } else if bad[r] {
3320                    for &(pn, _) in &stack {
3321                        bad[pn] = true;
3322                    }
3323                }
3324            } else {
3325                color[v] = 2;
3326                stack.pop();
3327            }
3328        }
3329    }
3330    let single: Vec<bool> = (0..n_vars).map(|v| cand[v] && !bad[v]).collect();
3331    if !single.iter().any(|&b| b) {
3332        return;
3333    }
3334    let mut pure: Vec<bool> = (0..n_vars)
3335        .map(|v| {
3336            single[v]
3337                && values
3338                    .get(v)
3339                    .and_then(|o| o.as_ref())
3340                    .map(|e| !has_side_effects(e))
3341                    .unwrap_or(false)
3342        })
3343        .collect();
3344    let mut impure: Vec<bool> = (0..n_vars).map(|v| single[v] && !pure[v]).collect();
3345
3346    // A Field read is pure (no side effect) but NOT position-independent:
3347    // re-rendering it at the use site reads whatever the field holds
3348    // THERE. d8's `v = !this.f; this.f = true;` lifts as iget→xor→iput;
3349    // the lifter materializes the read at its defining pc (force_mat,
3350    // lift.rs) precisely so the xor sees the OLD value — and this pass
3351    // used to re-inline that temp past the write, resurrecting the
3352    // stale read (weibo Transmitter.exchangeMessageDone rendered
3353    // `this.f = true; v = !this.f;`). Block the inline-when-anywhere
3354    // path for a value whose field is written between the def and the
3355    // single use (statement pre-order positions); such candidates fall
3356    // through to the adjacent-only impure path, which cannot cross
3357    // statements.
3358    {
3359        type FieldKey = (std::sync::Arc<str>, std::sync::Arc<str>, bool);
3360        let mut field_writes: jdc_core::FxHashMap<FieldKey, Vec<usize>> =
3361            jdc_core::FxHashMap::default();
3362        let mut def_pos: Vec<usize> = vec![usize::MAX; n_vars];
3363        let mut use_pos: Vec<usize> = vec![usize::MAX; n_vars];
3364        let mut pos = 0usize;
3365        walk_all(s, &mut |st| {
3366            let here = pos;
3367            pos += 1;
3368            for e in stmt_read_exprs(st) {
3369                visit_exprs(e, &mut |x| {
3370                    if let Expr::Local { var, .. } = x {
3371                        let v = *var as usize;
3372                        if v < use_pos.len() && use_pos[v] == usize::MAX {
3373                            use_pos[v] = here;
3374                        }
3375                    }
3376                });
3377            }
3378            match st {
3379                Stmt::LocalDef { var, .. } => {
3380                    let v = *var as usize;
3381                    if v < def_pos.len() {
3382                        def_pos[v] = here;
3383                    }
3384                }
3385                Stmt::ExprStmt(Expr::Assign { target, .. }) => match &**target {
3386                    Expr::Local { var, .. } => {
3387                        let v = *var as usize;
3388                        if v < def_pos.len() {
3389                            def_pos[v] = here;
3390                        }
3391                    }
3392                    Expr::Field { cls, name, is_static, .. } => {
3393                        field_writes
3394                            .entry((cls.clone(), name.clone(), *is_static))
3395                            .or_default()
3396                            .push(here);
3397                    }
3398                    _ => {}
3399                },
3400                Stmt::ExprStmt(Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. }) => {
3401                    if let Expr::Field { cls, name, is_static, .. } = &**e {
3402                        field_writes
3403                            .entry((cls.clone(), name.clone(), *is_static))
3404                            .or_default()
3405                            .push(here);
3406                    }
3407                }
3408                _ => {}
3409            }
3410        });
3411        let mut hazard = vec![false; n_vars];
3412        for v in 0..n_vars {
3413            if !pure[v] {
3414                continue;
3415            }
3416            let Some(Some(val)) = values.get(v).map(|o| o.as_ref()) else {
3417                continue;
3418            };
3419            let (d, u) = (def_pos[v], use_pos[v]);
3420            if d == usize::MAX || u == usize::MAX || u <= d {
3421                continue;
3422            }
3423            visit_exprs(val, &mut |x| {
3424                if let Expr::Field { cls, name, is_static, .. } = x {
3425                    if let Some(ws) = field_writes.get(&(cls.clone(), name.clone(), *is_static)) {
3426                        if ws.iter().any(|w| *w > d && *w < u) {
3427                            hazard[v] = true;
3428                        }
3429                    }
3430                }
3431            });
3432        }
3433        for v in 0..n_vars {
3434            if hazard[v] {
3435                pure[v] = false;
3436                impure[v] = single[v];
3437            }
3438        }
3439    }
3440
3441    // 1. Pure values: inline anywhere.
3442    if pure.iter().any(|&b| b) {
3443        let vals: Vec<Option<Expr>> = (0..n_vars)
3444            .map(|v| {
3445                if pure[v] {
3446                    values.get(v).cloned().flatten()
3447                } else {
3448                    None
3449                }
3450            })
3451            .collect();
3452        rewrite_exprs(s, &mut |e| {
3453            deep_rewrite_reads(e, &mut |x| {
3454                if let Expr::Local { var, .. } = x {
3455                    if let Some(Some(v)) = vals.get(*var as usize) {
3456                        *x = v.clone();
3457                    }
3458                }
3459            });
3460        });
3461        drop_defs(s, &pure);
3462    }
3463
3464    // 2. Impure values: inline only into the immediately following
3465    //    statement.
3466    if impure.iter().any(|&b| b) {
3467        forward_adjacent_impure(s, &values, &impure);
3468    }
3469}
3470
3471/// The expression payloads a statement READS — the statement's own node
3472/// only (walk_all visits nested statements separately). Assignment
3473/// targets are writes (a non-local target still reads its owner/index
3474/// subtree); shared by count_reads and the position scan in
3475/// forward_single_use so the two can never drift apart.
3476fn stmt_read_exprs(st: &Stmt) -> Vec<&Expr> {
3477    match st {
3478        Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
3479            let mut v: Vec<&Expr> = vec![value];
3480            if !matches!(&**target, Expr::Local { .. }) {
3481                v.push(target);
3482            }
3483            v
3484        }
3485        // `v++` writes v — not a read (a compound `a[i]++` still reads
3486        // its owner/index subtree).
3487        Stmt::ExprStmt(e @ (Expr::PreIncDec { .. } | Expr::PostIncDec { .. })) => {
3488            let inner = match e {
3489                Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => e,
3490                _ => unreachable!(),
3491            };
3492            if matches!(**inner, Expr::Local { .. }) {
3493                Vec::new()
3494            } else {
3495                vec![e]
3496            }
3497        }
3498        Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
3499            vec![e]
3500        }
3501        Stmt::Return(Some(e)) => vec![e],
3502        Stmt::LocalDef { init: Some(e), .. } => vec![e],
3503        Stmt::If { cond, .. } => vec![cond],
3504        Stmt::While { cond, .. } => vec![cond],
3505        Stmt::DoWhile { cond, .. } => vec![cond],
3506        // Read sites that are NOT statement children (walk_all recurses
3507        // into bodies but these payloads are expressions on the node
3508        // itself). Omitting them under-counts reads, which would let
3509        // drop_dead_locals over-prune a var whose only use is a switch
3510        // selector / loop condition / lock / iterable.
3511        Stmt::Switch { selector, .. } => vec![selector],
3512        Stmt::ForEach { iterable, .. } => vec![iterable],
3513        Stmt::Synchronized { lock, .. } => vec![lock],
3514        Stmt::For { cond, update, .. } => {
3515            let mut v: Vec<&Expr> = Vec::with_capacity(update.len() + 1);
3516            if let Some(c) = cond {
3517                v.push(c);
3518            }
3519            v.extend(update.iter());
3520            v
3521        }
3522        Stmt::Assert { cond, msg } => {
3523            let mut v: Vec<&Expr> = vec![cond];
3524            if let Some(m) = msg {
3525                v.push(m);
3526            }
3527            v
3528        }
3529        _ => Vec::new(),
3530    }
3531}
3532
3533/// Count READS of variables (assignment targets excluded).
3534fn count_reads(s: &Stmt, out: &mut Vec<usize>) {
3535    walk_all(s, &mut |st| {
3536        for e in stmt_read_exprs(st) {
3537            visit_exprs(e, &mut |x| {
3538                if let Expr::Local { var, .. } = x {
3539                    grow_to(out, *var);
3540                    out[*var as usize] += 1;
3541                }
3542            });
3543        }
3544    });
3545}
3546
3547
3548fn drop_defs(s: &mut Stmt, vars: &[bool]) {
3549    walk_mut_deep(s, &mut |st| {
3550        if let Stmt::Block(v) = st {
3551            v.retain(|x| match x {
3552                Stmt::ExprStmt(Expr::Assign { target, .. }) => match &**target {
3553                    Expr::Local { var, .. } => !vars.get(*var as usize).copied().unwrap_or(false),
3554                    _ => true,
3555                },
3556                Stmt::LocalDef { var, .. } => !vars.get(*var as usize).copied().unwrap_or(false),
3557                _ => true,
3558            });
3559        }
3560    });
3561}
3562
3563/// For each statement list: `v = IMPURE; NEXT(v)` folds NEXT's reference.
3564fn forward_adjacent_impure(s: &mut Stmt, values: &[Option<Expr>], impure: &[bool]) {
3565    if let Stmt::Block(v) = s {
3566        let mut i = 0;
3567        while i + 1 < v.len() {
3568            let def_var = match &v[i] {
3569                Stmt::ExprStmt(Expr::Assign { target, .. }) => match &**target {
3570                    Expr::Local { var, .. } => Some(*var),
3571                    _ => None,
3572                },
3573                Stmt::LocalDef { var, .. } => Some(*var),
3574                _ => None,
3575            };
3576            if let Some(var) = def_var {
3577                if impure.get(var as usize).copied().unwrap_or(false) {
3578                    // Does the NEXT statement reference var?
3579                    let mut reads_here = vec![0usize; values.len().max(1)];
3580                    count_reads(&v[i + 1], &mut reads_here);
3581                    if reads_here.get(var as usize).copied().unwrap_or(0) == 1 {
3582                        // The def statement's CURRENT value (earlier inlines
3583                        // in this walk may have rewritten it — the pre-built
3584                        // values map would be stale).
3585                        let val = match &v[i] {
3586                            Stmt::ExprStmt(Expr::Assign { value, .. }) => (**value).clone(),
3587                            Stmt::LocalDef { init: Some(e), .. } => (*e).clone(),
3588                            _ => {
3589                                i += 1;
3590                                continue;
3591                            }
3592                        };
3593                        rewrite_exprs(&mut v[i + 1], &mut |e| {
3594                            deep_rewrite_reads(e, &mut |x| {
3595                                if let Expr::Local { var: v2, .. } = x {
3596                                    if *v2 == var {
3597                                        *x = val.clone();
3598                                    }
3599                                }
3600                            });
3601                        });
3602                        v.remove(i);
3603                        // The PREVIOUS definition may now be adjacent to its
3604                        // (shifted) use — re-examine it.
3605                        i = i.saturating_sub(1);
3606                        continue;
3607                    }
3608                }
3609            }
3610            i += 1;
3611        }
3612    }
3613    walk_mut(s, &mut |x| forward_adjacent_impure(x, values, impure));
3614}
3615
3616// ---------------------------------------------------------------------------
3617// Type inference, booleanization, null comparisons, declarations
3618// ---------------------------------------------------------------------------
3619
3620/// Evidence-based type inference for synthetic (non-parameter) vars.
3621pub fn infer_types(vt: &mut VarTable, body: &mut Stmt, ret: &JavaType, env: &MethodEnv) {
3622    let n = vt.vars.len();
3623    // (type, strong) — DIRECTIONAL evidence. Strong facts (an assigned
3624    // value's type, the declared return/throw/monitor type) DEFINE the
3625    // variable; weak facts (call argument expectations, receiver
3626    // contexts, numeric literals) only constrain a use. jadx's bound
3627    // system carries the same direction; a flat pool let a weak
3628    // expectation (`v26 = p1` with boolean p1 beside `v26 = 0` sugar)
3629    // outvote the definition.
3630    let mut evidence: Vec<Vec<(JavaType, bool)>> = vec![Vec::new(); n];
3631    let ev = |evidence: &mut Vec<Vec<(JavaType, bool)>>, var: u32, t: JavaType, strong: bool| {
3632        if (var as usize) < evidence.len() {
3633            evidence[var as usize].push((t, strong));
3634        }
3635    };
3636
3637    walk_all(body, &mut |st| match st {
3638        Stmt::LocalDef { var, init, .. } => {
3639            if let Some(e) = init {
3640                expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
3641                // The initializer DEFINES the declared type: `long v = l(...)`
3642                // must not stay `int v` (silent 64-bit truncation in the
3643                // eyes of a reader; javac rejects it as lossy). Null
3644                // carries no type (it would DOWNGRADE String to Object).
3645                // Literals are weak (0/1 is boolean sugar half the time);
3646                // typed producers and parameter sources are strong;
3647                // local-to-local copies are weak (register reuse).
3648                let strong = match e {
3649                    Expr::Const(_) => false,
3650                    Expr::Local { var: src, .. } => {
3651                        let src_ty = vt.vars.get(*src as usize).map(|v| v.ty.erased());
3652                        let tgt_ty = vt.vars.get(*var as usize).map(|v| v.ty.erased());
3653                        match (src_ty, tgt_ty) {
3654                            (Some(s), Some(g)) => {
3655                                (s.is_numeric() && g.is_numeric())
3656                                    || (s.is_reference() && g.is_reference())
3657                            }
3658                            _ => false,
3659                        }
3660                    }
3661                    _ => true,
3662                };
3663                ev(&mut evidence, *var, e.type_ref().erased(), strong);
3664            }
3665        }
3666        Stmt::ExprStmt(e) => {
3667            // Assignment targets: a typed PRODUCER (method/new/field) or a
3668            // PARAMETER source is a STRONG definition (`v26 = p1` with
3669            // boolean p1). A local-to-local copy is weak — register reuse
3670            // and merge materialization emit exactly that shape with
3671            // mismatched types (`sb7 = compareTo10` in a Kotlin when).
3672            // Literals stay weak.
3673            if let Expr::Assign { target, value, .. } = e {
3674                // A field WRITE through a local (`v.f = x`, dex iput)
3675                // proves v is a reference of the field's declaring
3676                // class AT THAT POINT — an iput on a boolean register
3677                // cannot exist in valid dex. Strong: it must outrank a
3678                // strong boolean copy chain into the same reused slot
3679                // (weixin coroutine state machines).
3680                if let Expr::Field { owner: Some(o), cls, is_static: false, .. } = &**target {
3681                    if cls.is_empty() {
3682                        // array-length pseudo-field: no declaring class
3683                    } else if let Expr::Local { var, .. } = &**o {
3684                        ev(
3685                            &mut evidence,
3686                            *var,
3687                            JavaType::Object(cls.clone()),
3688                            true,
3689                        );
3690                    }
3691                }
3692                if let Expr::Local { var, .. } = &**target {
3693                    let strong = match &**value {
3694                        Expr::Const(_) => false,
3695                        Expr::Local { var: src, .. } => {
3696                            // A local copy is strong when the source and
3697                            // the target's register type are the same
3698                            // FAMILY (numeric↔numeric, reference↔
3699                            // reference) — a cross-family copy (int into
3700                            // a StringBuilder local) is merge-material
3701                            // residue and stays weak.
3702                            let src_ty = vt.vars.get(*src as usize).map(|v| v.ty.erased());
3703                            let tgt_ty = vt.vars.get(*var as usize).map(|v| v.ty.erased());
3704                            match (src_ty, tgt_ty) {
3705                                (Some(s), Some(g)) => {
3706                                    (s.is_numeric() && g.is_numeric())
3707                                        || (s.is_reference() && g.is_reference())
3708                                }
3709                                _ => false,
3710                            }
3711                        }
3712                        _ => true,
3713                    };
3714                    ev(&mut evidence, *var, value.type_ref().erased(), strong);
3715                }
3716            }
3717            expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
3718        }
3719        Stmt::Throw(e) => {
3720            expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
3721            if let Expr::Local { var, .. } = e {
3722                ev(
3723                    &mut evidence,
3724                    *var,
3725                    JavaType::Object("java/lang/Throwable".into()),
3726                    true,
3727                );
3728            }
3729        }
3730        Stmt::Return(Some(e)) => {
3731            expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
3732            if let Expr::Local { var, .. } = e {
3733                ev(&mut evidence, *var, ret.clone(), true);
3734            }
3735        }
3736        Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
3737            if let Expr::Local { var, .. } = e {
3738                ev(
3739                    &mut evidence,
3740                    *var,
3741                    JavaType::Object("java/lang/Object".into()),
3742                    true,
3743                );
3744            }
3745        }
3746        Stmt::ForEach {
3747            var,
3748            iterable,
3749            is_array,
3750            ..
3751        } => {
3752            if let Expr::Local { var: v0, .. } = iterable {
3753                if *is_array {
3754                    ev(&mut evidence, *v0, JavaType::Array(Box::new(JavaType::Int)), true);
3755                } else {
3756                    ev(
3757                        &mut evidence,
3758                        *v0,
3759                        JavaType::Object("java/lang/Object".into()),
3760                        true,
3761                    );
3762                }
3763            }
3764            let _ = var;
3765        }
3766        _ => {}
3767    });
3768    let _ = env;
3769
3770    // Return-position locals take the declared return type (also inside
3771    // nested returns — handled by the walk above).
3772
3773        // Vars dereferenced as field/method receivers or arrays: their
3774    // resolved type must be a dereferenceable class — a boxed Boolean
3775    // in the strong set (Kotlin Result registers hold `Boolean | impl`
3776    // across coroutine case arms) must lose to the real class even
3777    // when the Boolean evidence comes first (weixin ry0/h2's
3778    // `Boolean bool19` receiving `bool19.d = x`).
3779    let mut derefed: Vec<bool> = vec![false; vt.vars.len()];
3780    mark_derefs(body, &mut derefed);
3781    // Array-indexed vars: their resolved type must be an ARRAY when
3782    // the evidence has one (`num4[v68]` with Integer-typed num4 —
3783    // 需要数组但找到Integer family); member derefs keep the class
3784    // preference below.
3785    let mut arr_derefed: Vec<bool> = vec![false; vt.vars.len()];
3786    visit_all_exprs(body, &mut |x| {
3787        if let Expr::ArrayIndex { array, .. } = x {
3788            if let Expr::Local { var, .. } = &**array {
3789                if (*var as usize) < arr_derefed.len() {
3790                    arr_derefed[*var as usize] = true;
3791                }
3792            }
3793        }
3794        // `v != null` is STRONG reference evidence: the lifter only
3795        // renders the null form when the dex value view held a
3796        // reference register at that pc — an int-typed generation
3797        // compared against null is phi-confluence residue (yq0/g1's
3798        // `v95_g12 != null`, 546-line 二元运算符 family). Strong Object
3799        // makes the resolver retype and split_generations separate the
3800        // int writes into their own generation.
3801        if let Expr::Bin { op, l, r, .. } = x {
3802            if matches!(op, BinOp::Eq | BinOp::Ne)
3803                && (matches!(&**r, Expr::Const(ConstVal::Null))
3804                    || matches!(&**l, Expr::Const(ConstVal::Null)))
3805            {
3806                for side in [l, r] {
3807                    if let Expr::Local { var, .. } = &**side {
3808                        ev(
3809                            &mut evidence,
3810                            *var,
3811                            JavaType::Object("java/lang/Object".into()),
3812                            true,
3813                        );
3814                    }
3815                }
3816            }
3817        }
3818    });
3819    const FINAL_JDK: &[&str] = &[
3820        "java/lang/Boolean",
3821        "java/lang/String",
3822        "java/lang/Integer",
3823        "java/lang/Long",
3824        "java/lang/Short",
3825        "java/lang/Byte",
3826        "java/lang/Character",
3827        "java/lang/Float",
3828        "java/lang/Double",
3829    ];
3830    for (i, evs) in evidence.iter().enumerate() {
3831        let info = &vt.vars[i];
3832        if info.is_param {
3833            continue;
3834        }
3835        let strong: Vec<JavaType> = evs
3836            .iter()
3837            .filter(|(_, s)| *s)
3838            .map(|(t, _)| t.clone())
3839            .collect();
3840        let all: Vec<JavaType> = evs.iter().map(|(t, _)| t.clone()).collect();
3841        if info.ty.erased().is_reference() {
3842            // Materialization residue: no strong definition, and the weak
3843            // pool names ≥2 DIFFERENT classes (a Kotlin `when` lowered
3844            // every branch's value into one register slot) — neither
3845            // class can win, every assignment needs boxing headroom:
3846            // widen to Object (uses get receiver casts).
3847            if strong.is_empty() {
3848                let refs: std::collections::HashSet<&str> = all
3849                    .iter()
3850                    .filter_map(|t| match t {
3851                        JavaType::Object(n) if n.as_ref() != "java/lang/Object" => {
3852                            Some(n.as_ref())
3853                        }
3854                        _ => None,
3855                    })
3856                    .collect();
3857                let mixed = all.iter().any(|t| t.is_numeric());
3858                if refs.len() >= 2 || (mixed && !refs.is_empty()) || (mixed && all.iter().any(|t| matches!(t, JavaType::Array(_)))) {
3859                    vt.vars[i].ty =
3860                        TypeRef::J(JavaType::Object("java/lang/Object".into()));
3861                    continue;
3862                }
3863            }
3864                        // Array-dereferenced vars prefer array evidence (an
3865            // Integer producer cannot be indexed).
3866            if arr_derefed[i] {
3867                let arr = strong
3868                    .iter()
3869                    .chain(all.iter())
3870                    .find(|t| matches!(t, JavaType::Array(_)))
3871                    .cloned();
3872                if let Some(t) = arr {
3873                    vt.vars[i].ty = TypeRef::J(t);
3874                    continue;
3875                }
3876            }
3877            // Strong definitions outrank weak expectations. A
3878            // dereferenced var prefers a real class over final JDK
3879            // boxes (Boolean can never carry `.d = x`).
3880            let deref_pick = || {
3881                if !derefed[i] {
3882                    return None;
3883                }
3884                strong
3885                    .iter()
3886                    .find(|t| {
3887                        matches!(t, JavaType::Object(n)
3888                            if !n.is_empty()
3889                                && n.as_ref() != "java/lang/Object"
3890                                && !FINAL_JDK.contains(&n.as_ref()))
3891                    })
3892                    .cloned()
3893            };
3894            if let Some(t) = deref_pick()
3895                .or_else(|| pick_object(&strong))
3896                .or_else(|| pick_object(&all))
3897            {
3898                vt.vars[i].ty = TypeRef::J(t);
3899            }
3900        } else if let Some(t) = pick_object(&strong) {
3901            // A strong reference definition (typed producer or field
3902            // write) outranks a strong boolean copy: Boolean assigns
3903            // INTO an Object slot legally, never the reverse.
3904            vt.vars[i].ty = TypeRef::J(t);
3905        } else if strong.contains(&JavaType::Boolean) {
3906            // A boolean-typed SOURCE assigned into the variable is a
3907            // definition (`v26 = p1` with boolean p1); the 0/1 literals
3908            // that share the pool are boolean sugar as often as not.
3909            vt.vars[i].ty = TypeRef::J(JavaType::Boolean);
3910        } else if !all.is_empty() && all.iter().all(|t| t.is_numeric()) {
3911            if let Some(t) = pick_numeric(&all) {
3912                vt.vars[i].ty = TypeRef::J(t);
3913            }
3914        } else {
3915            // Mixed-family materialization on a numeric-declared slot
3916            // (int slot receiving StringBuilders — the register was the
3917            // `when` accumulator): Object, same as the reference side.
3918            let refs = all.iter().filter(|t| t.is_reference()).count();
3919            let nums = all.iter().filter(|t| t.is_numeric()).count();
3920            if refs > 0 && nums > 0 {
3921                vt.vars[i].ty =
3922                    TypeRef::J(JavaType::Object("java/lang/Object".into()));
3923            } else if let Some(t) = pick_object(&strong).or_else(|| pick_object(&all)) {
3924                vt.vars[i].ty = TypeRef::J(t);
3925            }
3926        }
3927    }
3928
3929    // Rewrite embedded Local types. Borrowed view + write-only-on-change:
3930    // the table clone was n_vars JavaType clones per method, and the
3931    // rewrite cloned a type into EVERY Local node — most already carry
3932    // the right one (JavaType::Object clones allocate).
3933    let types: Vec<&TypeRef> = vt.vars.iter().map(|v| &v.ty).collect();
3934    rewrite_exprs(body, &mut |e| {
3935        deep_rewrite(e, &mut |x| {
3936            if let Expr::Local { var, ty } = x {
3937                if let Some(want) = types.get(*var as usize) {
3938                    if ty != *want {
3939                        *ty = (*want).clone();
3940                    }
3941                }
3942            }
3943        });
3944    });
3945
3946    // Numeric literals take their variable's declared width (`double v = 0L`
3947    // prints as `0.0`; `int x = 5L` as `5`).
3948    coerce_num_consts(body, &types);
3949}
3950
3951fn coerce_num_consts(body: &mut Stmt, types: &[&TypeRef]) {
3952    walk_mut_deep(body, &mut |st| {
3953        let (var, val): (u32, &mut Expr) = match st {
3954            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => match &mut **target {
3955                Expr::Local { var, .. } => (*var, value),
3956                _ => return,
3957            },
3958            Stmt::LocalDef {
3959                var, init: Some(e), ..
3960            } => (*var, e),
3961            _ => return,
3962        };
3963        let want = match types.get(var as usize) {
3964            Some(TypeRef::J(t)) => t.clone(),
3965            _ => return,
3966        };
3967        let rewritten = match &*val {
3968            // dex 的 const/const-wide 字面量是**无类型位模式**:Float/
3969            // Double 寄存器上的整型常量就是浮点位(编译器把字面量按位
3970            // 发射),必须位重解释(from_bits)——数值转换(as f32)会把
3971            // 1073741824 变成 1.07e9f 而非 2.0f,重编译后数值全错且
3972            // javac 门不可见(telegram sf/f0:measuredWidth/1073741824
3973            // 应为 /2.0f ×2.7 万行;与 4436/4911 两处孪生位对齐——它们
3974            // 用的就是 from_bits)。
3975            Expr::Const(ConstVal::Long(l)) => match &want {
3976                JavaType::Double => Some(Expr::Const(ConstVal::Double(f64::from_bits(*l as u64)))),
3977                JavaType::Float => Some(Expr::Const(ConstVal::Float(f32::from_bits(*l as u32)))),
3978                _ => None,
3979            },
3980            Expr::Const(ConstVal::Int(i)) => match &want {
3981                JavaType::Double => Some(Expr::Const(ConstVal::Double(*i as f64))),
3982                JavaType::Float => Some(Expr::Const(ConstVal::Float(f32::from_bits(*i as u32)))),
3983                _ => None,
3984            },
3985            _ => None,
3986        };
3987        if let Some(e) = rewritten {
3988            *val = e;
3989        }
3990    });
3991}
3992
3993fn pick_object(evs: &[JavaType]) -> Option<JavaType> {
3994    evs.iter()
3995        .find(|t| {
3996            t.is_reference()
3997                && !matches!(t, JavaType::Object(n) if n.is_empty() || n.as_ref() == "java/lang/Object")
3998        })
3999        .cloned()
4000}
4001
4002fn pick_numeric(evs: &[JavaType]) -> Option<JavaType> {
4003    if evs.is_empty() {
4004        return None;
4005    }
4006    let mut t = evs[0].clone();
4007    for x in &evs[1..] {
4008        t = join_numeric(&t, x);
4009    }
4010    Some(t)
4011}
4012
4013fn join_numeric(a: &JavaType, b: &JavaType) -> JavaType {
4014    if a == b {
4015        return a.clone();
4016    }
4017    match (a, b) {
4018        (JavaType::Double, _) | (_, JavaType::Double) => JavaType::Double,
4019        (JavaType::Float, _) | (_, JavaType::Float) => JavaType::Float,
4020        (JavaType::Long, _) | (_, JavaType::Long) => JavaType::Long,
4021        (JavaType::Int, x) if x.is_integral() => JavaType::Int,
4022        (x, JavaType::Int) if x.is_integral() => JavaType::Int,
4023        (x, y) if x.is_integral() && y.is_integral() => JavaType::Int,
4024        _ => JavaType::Int,
4025    }
4026}
4027
4028/// Gather per-var type evidence from one expression.
4029fn expr_evidence<F: FnMut(u32, JavaType)>(e: &Expr, f: &mut F) {
4030    visit_exprs(e, &mut |x| match x {
4031        Expr::Method {
4032            owner,
4033            args,
4034            desc,
4035            is_static,
4036            cls,
4037            ..
4038        } => {
4039            if !*is_static {
4040                if let Some(o) = owner {
4041                    if let Expr::Local { var, .. } = &**o {
4042                        f(*var, JavaType::Object(cls.clone()));
4043                    }
4044                }
4045            }
4046            for (i, a) in args.iter().enumerate() {
4047                if let Expr::Local { var, .. } = a {
4048                    if let Some(t) = desc.args.get(i) {
4049                        f(*var, t.clone());
4050                    }
4051                }
4052            }
4053        }
4054        Expr::Field {
4055            owner,
4056            cls,
4057            ty,
4058            is_static,
4059            ..
4060        } => {
4061            if !*is_static && !cls.is_empty() {
4062                if let Some(o) = owner {
4063                    if let Expr::Local { var, .. } = &**o {
4064                        // The field ref's declaring class is dex-exact
4065                        // evidence — the old hardcoded Object let a
4066                        // boolean copy chain outvote it (weixin ry0/h2's
4067                        // `Boolean bool19` receiving `bool19.d = x` —
4068                        // the 339-line 找不到符号 变量 d family).
4069                        // EMPTY cls = the array-length pseudo-field
4070                        // (lift models `a.length` as Field{cls:""}) —
4071                        // Object("") evidence poisoned pick_object into
4072                        // empty-typed declarations and `() x` casts.
4073                        f(*var, JavaType::Object(cls.clone()));
4074                    }
4075                }
4076            }
4077            let _ = ty;
4078        }
4079        // `v != null` / `v == null`: the register holds a reference at
4080        // that point — weak Object evidence keeps int-typed generations
4081        // from winning the pool (yq0/g1's `v95_g12 != null` with int
4082        // v95_g12 — 二元运算符 '!=' 546-line family).
4083        Expr::Bin { op, l, r, .. }
4084            if matches!(op, BinOp::Eq | BinOp::Ne)
4085                && (matches!(&**r, Expr::Const(ConstVal::Null))
4086                    || matches!(&**l, Expr::Const(ConstVal::Null))) =>
4087        {
4088            for side in [l, r] {
4089                if let Expr::Local { var, .. } = &**side {
4090                    f(*var, JavaType::Object("java/lang/Object".into()));
4091                }
4092            }
4093        }
4094        Expr::ArrayIndex { array, .. } => {
4095            if let Expr::Local { var, ty, .. } = &**array {
4096                // Reinforce the local's KNOWN array type: the hardcoded
4097                // int[] fallback contradicted a typed array local (a
4098                // register reused for byte[], int[] then byte[] across
4099                // one clinit labeled the byte[] defs `int[]`).
4100                let t = ty.erased();
4101                if matches!(t, JavaType::Array(_)) {
4102                    f(*var, t);
4103                } else {
4104                    f(*var, JavaType::Array(Box::new(JavaType::Int)));
4105                }
4106            }
4107        }
4108        Expr::Assign { .. } => {
4109            // Covered by the typed strong-evidence walk in infer_types;
4110            // keeping a weak duplicate here let expectations outvote it.
4111        }
4112        Expr::Cast { e: inner, .. } => {
4113            // A cast is an EXPLICIT narrowing at the use site — it must
4114            // NOT feed the cast target as evidence for the variable
4115            // (`Object get2 = list.get(2); j((String) get2);` had get2
4116            // retyped to String, making the assignment incompatible).
4117            // jadx's bounds carry direction: a USE bound narrows nothing.
4118            let _ = inner;
4119        }
4120        Expr::InstanceOf { e: inner, .. } => {
4121            let _ = inner;
4122        }
4123        _ => {}
4124    });
4125}
4126
4127/// Mark locals that OWN a non-static field access and whose inferred type
4128/// is a concrete `$<digits>` class (a d8/R8-desugared lambda or a Kotlin
4129/// suspend-lambda). Emit demotes such a type to its SAM interface in
4130/// DECLARED positions (`Function2 v = new Outer$..$1(..)`) for phi-friendly
4131/// readability — but interfaces carry no instance fields, so the Kotlin
4132/// coroutine `create()` capture writes (`v.L$0 = obj`, also I$/J$) render
4133/// "找不到符号 变量 L$0" (weibo MutableScatterMap$..$iterator$1, ~1k).
4134/// Forcing the concrete declared type keeps the field access legal; the phi
4135/// case is excluded because a disagreed merge stays Object (wide_stack_vars),
4136/// never a specific concrete class. Gate mirrors emit's demotion exactly
4137/// (last `$` segment all digits) so nothing else changes.
4138pub fn mark_field_owner_concrete(vt: &mut VarTable, body: &mut Stmt) {
4139    let mut owners: HashSet<u32> = HashSet::default();
4140    rewrite_exprs(body, &mut |e| {
4141        visit_exprs(e, &mut |x| {
4142            if let Expr::Field {
4143                owner: Some(o),
4144                is_static: false,
4145                ..
4146            } = x
4147            {
4148                if let Expr::Local { var, .. } = &**o {
4149                    owners.insert(*var);
4150                }
4151            }
4152        });
4153    });
4154    for var in owners {
4155        let i = var as usize;
4156        if i >= vt.vars.len() {
4157            continue;
4158        }
4159        if let JavaType::Object(cls) = vt.vars[i].ty.erased() {
4160            if let Some(last) = cls.rsplit('$').next() {
4161                if !last.is_empty() && last.chars().all(|c| c.is_ascii_digit()) {
4162                    vt.force_concrete_vars.insert(var);
4163                }
4164            }
4165        }
4166    }
4167    // EVERY local whose resolved type is a concrete `$<digits>` class
4168    // keeps its binary name, not just field owners: when one side of an
4169    // assignment falls back to the SAM/super (`ContinuationImpl v7`) and
4170    // its sibling stays concrete (`$reportWhenComplete$1 v9`), the pair
4171    // is an uncastable "ContinuationImpl无法转换为…$1" (weibo coroutine
4172    // prologues ×282). The anon fallback still serves INTERFACE-typed
4173    // phi slots (branches minting different lambdas) — their vt type is
4174    // the join, never the $N class.
4175    let concrete: Vec<u32> = vt
4176        .vars
4177        .iter()
4178        .filter(|v| {
4179            if let JavaType::Object(cls) = v.ty.erased() {
4180                cls.rsplit('$')
4181                    .next()
4182                    .is_some_and(|l| !l.is_empty() && l.chars().all(|c| c.is_ascii_digit()))
4183            } else {
4184                false
4185            }
4186        })
4187        .map(|v| v.id)
4188        .collect();
4189    for var in concrete {
4190        vt.force_concrete_vars.insert(var);
4191    }
4192}
4193
4194/// Type inference can leave a specific-reference-typed target assigned
4195/// from a `java/lang/Object`-typed value: a phi that merged String and
4196/// Object then settled on String (`String str4; ... str4 = obj;` where
4197/// `obj` is an Object field — y5/n.java), or an Object local flowing to a
4198/// typed field. Java requires a narrowing cast there; insert `(T) value`
4199/// at the USE site (round-40 philosophy: narrow at use, never retype the
4200/// declaration). Guarded tightly: only when the value's resolved static
4201/// type is EXACTLY `java/lang/Object` (the untyped top — a known subtype
4202/// is never re-cast), the value is not a null const (assignable to any
4203/// ref) nor already a cast, and the target is a specific reference type
4204/// (Object→int/boolean is unboxing, a different problem left alone).
4205pub fn insert_object_narrowing_casts(
4206    vt: &VarTable,
4207    body: &mut Stmt,
4208    pool: &DexPool,
4209    ret: &JavaType,
4210) {
4211    // Resolve a value's static type through the VarTable for locals (the
4212    // embedded Local ty can lag infer_types), else the expr's own type.
4213    // The erased materialization happens ONCE per statement and threads
4214    // through the checks — the old helpers re-erased 3-4x per statement
4215    // (each a JavaType clone with Array chains) and this pass runs over
4216    // every Assign/LocalDef/Return/call-arg corpus-wide.
4217    let value_ty = |e: &Expr| -> JavaType {
4218        match e {
4219            Expr::Local { var, .. } => vt.var(*var).ty.erased(),
4220            other => other.type_ref().erased(),
4221        }
4222    };
4223    let is_top_object_ty = |v: &JavaType| -> bool {
4224        matches!(v, JavaType::Object(c) if c.as_ref() == "java/lang/Object")
4225    };
4226    // A specific reference target type (a class other than
4227    // java/lang/Object, or an array — `String[] v = obj` needs the
4228    // cast exactly like `String v = obj`). The J arm pattern-matches
4229    // directly (no erased clone); only the rare G form erases.
4230    let specific_ref = |ty: &TypeRef| -> Option<TypeRef> {
4231        match ty {
4232            TypeRef::J(JavaType::Object(c)) if c.as_ref() != "java/lang/Object" => {
4233                Some(ty.clone())
4234            }
4235            TypeRef::J(JavaType::Array(_)) => Some(ty.clone()),
4236            _ => match ty.erased() {
4237                JavaType::Object(c) if c.as_ref() != "java/lang/Object" => Some(ty.clone()),
4238                JavaType::Array(_) => Some(ty.clone()),
4239                _ => None,
4240            },
4241        }
4242    };
4243    let non_cast_shape = |e: &Expr| -> bool {
4244        !matches!(e, Expr::Const(_) | Expr::Cast { .. } | Expr::InstanceOf { .. })
4245    };
4246    // Downcast witness: a SPECIFIC-ref value whose static type does not
4247    // fit the specific-ref target (`v9($1) = v7` with v7:
4248    // ContinuationImpl — weibo coroutine prologue ×282). The dex move
4249    // was verifier-proven against the target type, so the cast states
4250    // the bytecode truth; an actually-unconvertible static pair (both
4251    // classes, unrelated — the value view conflated) rides the
4252    // emitter's (Object) relay and still compiles. Framework types the
4253    // pool cannot order take the cast too: a downcast renders directly,
4254    // an unrelated pair relays.
4255    let needs_downcast = |vty: &JavaType, tt: &JavaType| -> bool {
4256        let (JavaType::Object(vn), JavaType::Object(tn)) = (vty, tt) else {
4257            return false;
4258        };
4259        if vn.as_ref() == "java/lang/Object" || tn.as_ref() == "java/lang/Object" {
4260            return false;
4261        }
4262        vn != tn && !pool.is_subtype(vn, tn)
4263    };
4264    // Combined decision on a materialized value type.
4265    let wants_cast = |e: &Expr, vty: &JavaType, tt: &JavaType| -> bool {
4266        non_cast_shape(e) && (is_top_object_ty(vty) || needs_downcast(vty, tt))
4267    };
4268    walk_mut_deep(body, &mut |st| match st {
4269        Stmt::ExprStmt(Expr::Assign {
4270            target,
4271            value,
4272            op: AssignOp::Plain,
4273            ..
4274        }) => {
4275            // Decide on materialized erased types (one per side); the
4276            // old code cloned the whole target TypeRef per statement
4277            // before knowing whether a cast fires at all.
4278            let tgt_erased: JavaType;
4279            let tgt_ref: Option<&TypeRef>;
4280            match &**target {
4281                Expr::Local { var, .. } => {
4282                    let ty = &vt.var(*var).ty;
4283                    tgt_erased = ty.erased();
4284                    tgt_ref = Some(ty);
4285                }
4286                Expr::Field { ty, .. } => {
4287                    tgt_erased = ty.erased();
4288                    tgt_ref = Some(ty);
4289                }
4290                // Array-element store `arr[i] = value`: the target type is
4291                // the ELEMENT type (Object→element assignments failed
4292                // "Object无法转换为String", weixin ×~150 — see history).
4293                Expr::ArrayIndex { array, .. } => {
4294                    let aty = match &**array {
4295                        Expr::Local { var, .. } => vt.var(*var).ty.erased(),
4296                        other => other.type_ref().erased(),
4297                    };
4298                    match aty {
4299                        JavaType::Array(inner) => {
4300                            tgt_erased = *inner;
4301                            tgt_ref = None;
4302                        }
4303                        _ => return,
4304                    }
4305                }
4306                _ => return,
4307            }
4308            // Specific check: the J-arm patterns fall through without
4309            // erasing; the array-element arm (no TypeRef to borrow)
4310            // re-materializes the erased form.
4311            let specific = match tgt_ref {
4312                Some(tr) => specific_ref(tr),
4313                None => specific_ref(&TypeRef::J(tgt_erased.clone())),
4314            };
4315            let Some(t) = specific else {
4316                return;
4317            };
4318            let vty = value_ty(value);
4319            if wants_cast(value, &vty, &tgt_erased) {
4320                let v = std::mem::replace(value, Box::new(Expr::This));
4321                **value = Expr::Cast { ty: t, e: v };
4322            }
4323        }
4324        Stmt::LocalDef { var, init: Some(value), .. } => {
4325            if let Some(t) = specific_ref(&vt.var(*var).ty) {
4326                let tt = vt.var(*var).ty.erased();
4327                let vty = value_ty(value);
4328                if wants_cast(value, &vty, &tt) {
4329                    let v = std::mem::replace(value, Expr::This);
4330                    *value = Expr::Cast {
4331                        ty: t,
4332                        e: Box::new(v),
4333                    };
4334                }
4335            }
4336        }
4337        Stmt::Return(Some(value)) => {
4338            if let Some(t) = specific_ref(&TypeRef::J(ret.clone())) {
4339                let vty = value_ty(value);
4340                if wants_cast(value, &vty, ret) {
4341                    let v = std::mem::replace(value, Expr::This);
4342                    *value = Expr::Cast { ty: t, e: Box::new(v) };
4343                }
4344            }
4345        }
4346        _ => {}
4347    });
4348    // Descriptor-exact formal casts at CALL args: the dex already
4349    // resolved the target method, so casting a top-Object actual to
4350    // its declared formal cannot shift overload resolution (the r57
4351    // args lesson applies to GUESSED casts, not descriptor-exact
4352    // ones). `"source_type:".concat(obj2)`, `new my0.j(obj2, ..)` —
4353    // Object无法转换为String family.
4354    walk_stmt_exprs(body, &mut |e| {
4355        deep_rewrite(e, &mut |x| {
4356            let (args, arg_tys): (&mut Vec<Expr>, &[JavaType]) = match x {
4357                Expr::Method { desc, args, is_dynamic: false, .. } => {
4358                    (args, desc.args.as_slice())
4359                }
4360                // Ctor calls are their own variant — the dex resolved the
4361                // exact <init>, so formal casts are just as safe here
4362                // (`new r(obj)` — Object无法转换为r family).
4363                Expr::New { arg_tys, args, .. } => (args, arg_tys.as_slice()),
4364                _ => return,
4365            };
4366            for (i, a) in args.iter_mut().enumerate() {
4367                let Some(formal) = arg_tys.get(i) else {
4368                    continue;
4369                };
4370                let Some(t) = specific_ref(&TypeRef::J(formal.clone())) else {
4371                    continue;
4372                };
4373                let aty = value_ty(a);
4374                if !wants_cast(a, &aty, formal) {
4375                    continue;
4376                }
4377                let v = std::mem::replace(a, Expr::Const(ConstVal::Null));
4378                *a = Expr::Cast { ty: t, e: Box::new(v) };
4379            }
4380        });
4381    });
4382}
4383
4384/// Numeric narrowing/widening mismatches at Assign / LocalDef / Return
4385/// positions get an explicit cast: dex int-to-byte / long-to-int flows
4386/// are implicit in the register machine, Java rejects them ("从long
4387/// 转换到int可能会有损失" — okio Buffer ×321 weibo lines). Casts at
4388/// these three positions are descriptor-faithful and cannot shift
4389/// overload resolution (the r57 args-position lesson). Boolean/numeric
4390/// mixes are NOT castable in Java and stay with the generation-split
4391/// machinery.
4392pub fn fix_primitive_assign_casts(vt: &VarTable, body: &mut Stmt, ret_ty: &JavaType) {
4393    fn numlike(t: &JavaType) -> bool {
4394        matches!(
4395            t,
4396            JavaType::Byte
4397                | JavaType::Short
4398                | JavaType::Int
4399                | JavaType::Char
4400                | JavaType::Long
4401                | JavaType::Float
4402                | JavaType::Double
4403        )
4404    }
4405    fn val_ty(e: &Expr, vt: &VarTable) -> JavaType {
4406        match e {
4407            Expr::Local { var, .. } if (*var as usize) < vt.vars.len() => {
4408                vt.var(*var).ty.erased()
4409            }
4410            other => other.type_ref().erased(),
4411        }
4412    }
4413    fn wrap(ty: &JavaType, value: &mut Expr) {
4414        if matches!(value, Expr::Cast { .. }) {
4415            return;
4416        }
4417        if val_ty_static(value) == *ty {
4418            return;
4419        }
4420        let taken = std::mem::replace(value, Expr::Const(ConstVal::Null));
4421        *value = Expr::Cast {
4422            ty: TypeRef::J(ty.clone()),
4423            e: Box::new(taken),
4424        };
4425    }
4426    fn val_ty_static(e: &Expr) -> JavaType {
4427        e.type_ref().erased()
4428    }
4429    /// Target-typed coercion for one assignment value. Numeric mixes
4430    /// cast; boolean mixes CONVERT (Java has no cast between boolean
4431    /// and any numeric): `d = b` → `(double) (b ? 1 : 0)`, `b = i` →
4432    /// `i != 0` — mirrors fix_primitive_arg_bridges' formal-side arms.
4433    /// The generation split cannot own these: at an if-join the
4434    /// one-sided split is undone (rename_gen_back) and the incompatible
4435    /// assign re-exposes ("boolean无法转换为double", weixin s9/t01-f).
4436    fn coerce(tt: &JavaType, value: &mut Expr, vt: &VarTable) {
4437        // Raw const bits against a floating target are the FLOAT value,
4438        // not an int to widen (`float f = <0x41000000 bits>` is 8.0f;
4439        // the (float) cast path would silently store 1.09e9f). Mirror of
4440        // fix_primitive_arg_bridges' descriptor-formal reinterpretation.
4441        match (&*value, tt) {
4442            (Expr::Const(ConstVal::Int(b)), JavaType::Float) => {
4443                *value = Expr::Const(ConstVal::Float(f32::from_bits(*b as u32)));
4444                return;
4445            }
4446            (Expr::Const(ConstVal::Long(b)), JavaType::Double) => {
4447                *value = Expr::Const(ConstVal::Double(f64::from_bits(*b as u64)));
4448                return;
4449            }
4450            (Expr::Const(ConstVal::Float(f)), JavaType::Int) => {
4451                *value = Expr::Const(ConstVal::Int(f.to_bits() as i32));
4452                return;
4453            }
4454            (Expr::Const(ConstVal::Double(d)), JavaType::Long) => {
4455                *value = Expr::Const(ConstVal::Long(d.to_bits() as i64));
4456                return;
4457            }
4458            _ => {}
4459        }
4460        let vt_val = val_ty(value, vt);
4461        let tt_bool = matches!(tt, JavaType::Boolean);
4462        let val_bool = matches!(vt_val, JavaType::Boolean);
4463        if tt_bool && !val_bool && numlike(&vt_val) {
4464            if matches!(
4465                value,
4466                Expr::Const(_)
4467                    | Expr::Bin { op: BinOp::Eq | BinOp::Ne, .. }
4468                    | Expr::Un { op: UnOp::Not, .. }
4469            ) {
4470                // Bare 0/1 constants render as false/true through the
4471                // emitter's bool coercion — wrapping them in `!= 0`
4472                // produced `0 != 0` noise the value-diamond fold then
4473                // had to re-interpret.
4474                return;
4475            }
4476            let taken = std::mem::replace(value, Expr::Const(ConstVal::Null));
4477            *value = Expr::Bin {
4478                op: BinOp::Ne,
4479                l: Box::new(taken),
4480                r: Box::new(Expr::Const(ConstVal::Int(0))),
4481                ty: Some(TypeRef::J(JavaType::Boolean)),
4482            };
4483        } else if !tt_bool && val_bool && numlike(tt) {
4484            if matches!(value, Expr::Cast { .. }) {
4485                return;
4486            }
4487            let taken = std::mem::replace(value, Expr::Const(ConstVal::Null));
4488            *value = Expr::Cast {
4489                ty: TypeRef::J(tt.clone()),
4490                e: Box::new(Expr::Cond {
4491                    c: Box::new(taken),
4492                    t: Box::new(Expr::Const(ConstVal::Int(1))),
4493                    f: Box::new(Expr::Const(ConstVal::Int(0))),
4494                }),
4495            };
4496        } else if numlike(tt) && numlike(&vt_val) {
4497            wrap(tt, value);
4498        }
4499    }
4500    walk_mut_deep(body, &mut |st| match st {
4501        Stmt::ExprStmt(Expr::Assign {
4502            target,
4503            op: AssignOp::Plain,
4504            value,
4505        }) => {
4506            let tt = match &**target {
4507                Expr::Local { var, .. } if (*var as usize) < vt.vars.len() => {
4508                    vt.var(*var).ty.erased()
4509                }
4510                Expr::Field { ty, .. } => ty.erased(),
4511                // Array store: the element type is the target type
4512                // (`sFormatStr[i] = v7` into char[] — "从int转换到char
4513                // 可能会有损失", weibo TimeUtils).
4514                Expr::ArrayIndex { array, .. } => match array.type_ref().erased() {
4515                    JavaType::Array(inner) => *inner,
4516                    _ => return,
4517                },
4518                _ => return,
4519            };
4520            coerce(&tt, value, vt);
4521        }
4522        Stmt::LocalDef {
4523            var,
4524            init: Some(value),
4525            ..
4526        } => {
4527            if (*var as usize) >= vt.vars.len() {
4528                return;
4529            }
4530            let tt = vt.var(*var).ty.erased();
4531            coerce(&tt, value, vt);
4532        }
4533        Stmt::Return(Some(e)) => {
4534            if !numlike(ret_ty) || !numlike(&val_ty(e, vt)) {
4535                return;
4536            }
4537            wrap(ret_ty, e);
4538        }
4539        _ => {}
4540    });
4541}
4542
4543/// Shadowed-ancestor field witness (the upcast sibling of
4544/// fix_field_owner_downcasts).
4545///
4546/// Java resolves a field access on the RECEIVER'S STATIC TYPE downward:
4547/// when the dex writes an ancestor's field (`iput … Fragment.A02:I`)
4548/// but a class between the receiver's static type and the declaring
4549/// ancestor declares a field with the SAME rendered name (`ImageView
4550/// A02`), the plain `this.A02` silently binds to the subclass field —
4551/// `int无法转换为ImageView` when the types differ, and a wrong-field
4552/// read/write when they don't (WhatsApp ActivitySheetFragment writes
4553/// the layout resid into the ancestor int field; its own A02 is an
4554/// ImageView — 1,216 assignment-shape sites in the WA tree). jadx
4555/// renders `super.A02`; the IR has no super-field form, so witness
4556/// with the always-legal upcast `((Fragment) this).A02` — field lookup
4557/// on a cast expression is static, landing on the ancestor at any
4558/// chain depth. The receiver may still be the typed this-Local at this
4559/// pipeline point (the This rewrite comes later), so the compile-time
4560/// type comes from the owner expr / var table, mirroring
4561/// fix_field_owner_downcasts' owner_internal. Fires ONLY on a real
4562/// same-display shadow; unshadowed ancestor refs inherit through
4563/// `this.X` and must not churn. Same-raw-name lookups only: a shadow
4564/// whose raw name differs but display collides is beyond the pool
4565/// lookup keyed by name.
4566pub fn fix_shadowed_super_fields(
4567    body: &mut Stmt,
4568    vt: &VarTable,
4569    pool: &DexPool,
4570    class_name: &str,
4571) {
4572    /// Rendered name of `(cls, raw field name)`: the registry display
4573    /// when the class declares the field, else the raw name. None when
4574    /// the class is not pool-materialized or has no such field.
4575    fn display(pool: &DexPool, cls: &str, name: &str) -> Option<String> {
4576        let pc = pool.get(cls)?;
4577        let f = pc
4578            .static_fields
4579            .iter()
4580            .chain(pc.instance_fields.iter())
4581            .find(|f| f.name.as_ref() == name)?;
4582        Some(
4583            jdc_core::rename::field_display(cls, name, &f.desc)
4584                .map(|d| d.to_string())
4585                .unwrap_or_else(|| name.to_string()),
4586        )
4587    }
4588    fn owner_ct(
4589        o: &Option<Box<Expr>>,
4590        vt: &VarTable,
4591        class_name: &str,
4592    ) -> Option<std::sync::Arc<str>> {
4593        match o.as_deref() {
4594            None | Some(Expr::This) => Some(std::sync::Arc::from(class_name)),
4595            Some(Expr::Local { var, .. }) => match vt.var(*var).ty.erased() {
4596                JavaType::Object(n) => Some(n),
4597                _ => None,
4598            },
4599            Some(other) => match other.type_ref().erased() {
4600                JavaType::Object(n) => Some(n),
4601                _ => None,
4602            },
4603        }
4604    }
4605    walk_stmt_exprs(body, &mut |e| {
4606        deep_rewrite(e, &mut |x| {
4607            let Expr::Field {
4608                owner,
4609                cls,
4610                name,
4611                is_static: false,
4612                ..
4613            } = x
4614            else {
4615                return;
4616            };
4617            let Some(ot) = owner_ct(owner, vt, class_name) else {
4618                return;
4619            };
4620            if cls.as_ref() == ot.as_ref() {
4621                return;
4622            }
4623            let Some(ref_display) = display(pool, cls, name) else {
4624                return;
4625            };
4626            // Walk from the receiver's compile-time type up to (not
4627            // including) the declaring ancestor: any same-display
4628            // declaration on the way shadows the ref.
4629            let mut cur = ot.to_string();
4630            let mut shadowed = false;
4631            let mut found = false;
4632            for _ in 0..64 {
4633                if display(pool, &cur, name).as_deref() == Some(ref_display.as_str()) {
4634                    shadowed = true;
4635                }
4636                let Some(pc) = pool.get(&cur) else {
4637                    return;
4638                };
4639                let Some(sup) = pc.super_name.clone() else {
4640                    return;
4641                };
4642                if sup.as_str() == cls.as_ref() {
4643                    found = true;
4644                    break;
4645                }
4646                cur = sup;
4647            }
4648            if !found || !shadowed {
4649                return;
4650            }
4651            let ty = TypeRef::J(JavaType::Object(cls.clone()));
4652            match owner {
4653                Some(o) => {
4654                    if let Expr::Cast { ty: ct, .. } = &mut **o {
4655                        *ct = ty;
4656                    } else {
4657                        let taken = std::mem::replace(&mut **o, Expr::This);
4658                        **o = Expr::Cast {
4659                            ty,
4660                            e: Box::new(taken),
4661                        };
4662                    }
4663                }
4664                None => {
4665                    *owner = Some(Box::new(Expr::Cast {
4666                        ty,
4667                        e: Box::new(Expr::This),
4668                    }))
4669                }
4670            }
4671        });
4672    });
4673}
4674
4675/// Subclass-field downcast witness + assign-cast narrowing.
4676///
4677/// R8 pushes fields DOWN into leaf classes (weibo: coroutine `label`
4678/// lives on each `Foo$bar$1` continuation, NOT on the rendered
4679/// BaseContinuationImpl/ContinuationImpl supers) while a check-cast in
4680/// the same flow types the receiver as the SUPERCLASS. The dex field
4681/// ref names the leaf (`$1->label` — verifier-legal through the
4682/// instance-of branch type), but Java resolves `((ContinuationImpl) c)
4683/// .label` against ContinuationImpl — "找不到符号 变量 label" (weibo
4684/// ×420). When the field's dex owner is a strict subtype of the
4685/// receiver's static type and the receiver's hierarchy does NOT
4686/// declare the name, re-target the receiver cast to the owner (the
4687/// runtime object IS the owner type — the verifier proved it).
4688///
4689/// Second rule: a `(T) e` value assigned into an S-typed target where
4690/// S <: T strictly ("ContinuationImpl无法转换为$reportWhenComplete$1")
4691/// narrows the cast to S — same verifier guarantee, and an assignment
4692/// position has no overload resolution to perturb.
4693pub fn fix_field_owner_downcasts(body: &mut Stmt, vt: &VarTable, pool: &DexPool) {
4694    fn owner_internal(o: &Expr, vt: &VarTable) -> Option<std::sync::Arc<str>> {
4695        let t = match o {
4696            Expr::Local { var, .. } => vt.var(*var).ty.erased(),
4697            other => other.type_ref().erased(),
4698        };
4699        match t {
4700            JavaType::Object(n) => Some(n),
4701            _ => None,
4702        }
4703    }
4704    fn declares_up(pool: &DexPool, class: &str, name: &str) -> bool {
4705        let mut cur = Some(class.to_string());
4706        let mut hops = 0;
4707        while let Some(c) = cur {
4708            hops += 1;
4709            if hops > 64 {
4710                return false;
4711            }
4712            let Some(pc) = pool.get(&c) else { return false };
4713            if pc
4714                .instance_fields
4715                .iter()
4716                .chain(pc.static_fields.iter())
4717                .any(|f| f.name.as_ref() == name)
4718            {
4719                return true;
4720            }
4721            cur = pc.super_name.clone();
4722        }
4723        false
4724    }
4725    walk_stmt_exprs(body, &mut |e| {
4726        deep_rewrite(e, &mut |x| {
4727            if let Expr::Field {
4728                owner: Some(o),
4729                cls,
4730                name,
4731                is_static: false,
4732                ..
4733            } = x
4734            {
4735                let Some(ot) = owner_internal(o, vt) else {
4736                    return;
4737                };
4738                if ot.as_ref() == cls.as_ref() || !pool.is_subtype(cls, &ot) {
4739                    return;
4740                }
4741                if declares_up(pool, &ot, name) || !declares_up(pool, cls, name) {
4742                    return;
4743                }
4744                let ty = TypeRef::J(JavaType::Object(cls.clone()));
4745                if let Expr::Cast { ty: ct, .. } = &mut **o {
4746                    *ct = ty;
4747                } else {
4748                    let taken = std::mem::replace(&mut **o, Expr::This);
4749                    **o = Expr::Cast { ty, e: Box::new(taken) };
4750                }
4751            }
4752            // Rule 2: `(T) e` into a strict-subtype target narrows to S.
4753            let (tgt_ty, value) = match x {
4754                Expr::Assign { target, op: AssignOp::Plain, value } => {
4755                    let t = match &**target {
4756                        Expr::Local { var, .. } => vt.var(*var).ty.erased(),
4757                        Expr::Field { ty, .. } => ty.erased(),
4758                        _ => return,
4759                    };
4760                    (t, value)
4761                }
4762                _ => return,
4763            };
4764            let JavaType::Object(sn) = tgt_ty else { return };
4765            if let Expr::Cast { ty, .. } = &mut **value {
4766                if let TypeRef::J(JavaType::Object(tn)) = ty {
4767                    if tn.as_ref() != sn.as_ref() && pool.is_subtype(&sn, tn) {
4768                        *ty = TypeRef::J(JavaType::Object(sn.clone()));
4769                    }
4770                }
4771            }
4772        });
4773    });
4774    // LocalDef inits: same narrowing against the declared var type.
4775    walk_mut_deep(body, &mut |st| {
4776        if let Stmt::LocalDef { var, init: Some(value), .. } = st {
4777            let tt = if (*var as usize) < vt.vars.len() {
4778                vt.vars[*var as usize].ty.erased()
4779            } else {
4780                return;
4781            };
4782            let JavaType::Object(sn) = tt else { return };
4783            if let Expr::Cast { ty, .. } = value {
4784                if let TypeRef::J(JavaType::Object(tn)) = ty {
4785                    if tn.as_ref() != sn.as_ref() && pool.is_subtype(&sn, tn) {
4786                        *ty = TypeRef::J(JavaType::Object(sn.clone()));
4787                    }
4788                }
4789            }
4790        }
4791    });
4792}
4793
4794/// `==`/`!=` between UNRELATED class types is a compile error in Java
4795/// ("不可比较的类型: f0和a") while dex if-eqObj happily compares any two
4796/// references (weixin fn1/w1 ×298 files). One side takes an `(Object)`
4797/// witness cast — legal against anything. Interfaces and subtype-related
4798/// pairs are already comparable and stay untouched; java/lang/Object and
4799/// arrays-of it likewise.
4800pub fn fix_incomparable_equality(body: &mut Stmt, vt: &VarTable, pool: &crate::DexPool) {
4801    fn side_ty(e: &Expr, vt: &VarTable) -> JavaType {
4802        match e {
4803            Expr::Local { var, .. } if (*var as usize) < vt.vars.len() => {
4804                vt.var(*var).ty.erased()
4805            }
4806            other => other.type_ref().erased(),
4807        }
4808    }
4809    let incomparable = |a: &JavaType, b: &JavaType| -> bool {
4810        use JavaType::{Array, Object};
4811        match (a, b) {
4812            (Object(x), Object(y)) => {
4813                if x == y || x.as_ref() == "java/lang/Object" || y.as_ref() == "java/lang/Object"
4814                {
4815                    return false;
4816                }
4817                let xi = pool.get(x).map(|c| c.is_interface()).unwrap_or(true);
4818                let yi = pool.get(y).map(|c| c.is_interface()).unwrap_or(true);
4819                // Unknown classes (framework, not in pool): a cast is
4820                // harmless but usually unnecessary — treat as comparable
4821                // to stay conservative.
4822                if xi || yi {
4823                    return false;
4824                }
4825                !pool.is_subtype(x, y) && !pool.is_subtype(y, x)
4826            }
4827            (Array(_), Object(y)) | (Object(y), Array(_)) => {
4828                y.as_ref() != "java/lang/Object"
4829                    && y.as_ref() != "java/lang/Cloneable"
4830                    && y.as_ref() != "java/io/Serializable"
4831                    && !pool.get(y).map(|c| c.is_interface()).unwrap_or(false)
4832            }
4833            _ => false,
4834        }
4835    };
4836    let is_ref = |t: &JavaType| matches!(t, JavaType::Object(_) | JavaType::Array(_));
4837    walk_stmt_exprs(body, &mut |e| {
4838        deep_rewrite(e, &mut |x| {
4839            if let Expr::Bin {
4840                op: BinOp::Eq | BinOp::Ne,
4841                l,
4842                r,
4843                ..
4844            } = x
4845            {
4846                let lt = side_ty(l, vt);
4847                let rt = side_ty(r, vt);
4848                // `obj != 0` → `obj != null` with FINAL types: the
4849                // lift-time null_side_rewrite ran before infer_types
4850                // (its obj table saw the pre-inference int), leaving
4851                // `sQLiteClosable != 0` — "二元运算符 '!=' 的操作数
4852                // 类型错误" ×700 weixin.
4853                if is_ref(&lt) && matches!(&**r, Expr::Const(ConstVal::Int(0))) {
4854                    **r = Expr::Const(ConstVal::Null);
4855                    return;
4856                }
4857                if is_ref(&rt) && matches!(&**l, Expr::Const(ConstVal::Int(0))) {
4858                    **l = Expr::Const(ConstVal::Null);
4859                    return;
4860                }
4861                if incomparable(&lt, &rt) {
4862                    let taken = std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
4863                    **l = Expr::Cast {
4864                        ty: TypeRef::J(JavaType::Object("java/lang/Object".into())),
4865                        e: taken,
4866                    };
4867                }
4868            }
4869        });
4870    });
4871}
4872
4873/// Primitive bridges at CALL/CTOR argument positions. Dex slots are
4874/// untyped category-1 integers: the verifier happily passes a Z-slot to
4875/// a `(B)` formal (weibo's Meituan-Robust hotpatch boilerplate boxes
4876/// boolean params via `new Byte(zreg)` — 4.2k+ `对于Byte(boolean),
4877/// 找不到合适的构造器`), and obfuscated/Kotlin code passes 0/1 slots to
4878/// `(Z)` formals (`int无法转换为boolean` at call sites). Java source
4879/// cannot express either — bridge at the IR level:
4880///   Boolean actual → numeric formal:  `(byte) (b ? 1 : 0)`
4881///   numeric actual → Boolean formal:  `x != 0`
4882/// The target method is DESCRIPTOR-bound (bytecode method_ref), so a
4883/// cast/compare to the exact formal type cannot flip overload
4884/// resolution the way r57's inferred object casts did — the rendered
4885/// call selects the same signature the dex named.
4886pub fn fix_primitive_arg_bridges(body: &mut Stmt, vt: &VarTable, pool: &crate::DexPool) {
4887    fn val_ty(e: &Expr, vt: &VarTable) -> JavaType {
4888        match e {
4889            Expr::Local { var, .. } => vt.var(*var).ty.erased(),
4890            other => other.type_ref().erased(),
4891        }
4892    }
4893    #[allow(clippy::type_complexity)]
4894    let is_bool = |t: &JavaType| matches!(t, JavaType::Boolean);
4895    let is_num = |t: &JavaType| {
4896        matches!(
4897            t,
4898            JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
4899                | JavaType::Long | JavaType::Float | JavaType::Double
4900        )
4901    };
4902    // BORROWED-arg form: returns the bridged replacement without
4903    // consuming the original (a `mem::replace` + `Option` dance dropped
4904    // the argument on the None path — every Boolean→Boolean arg became
4905    // a literal `null`: 61k 引用不明确 in one battery run).
4906    fn bridge(actual: &JavaType, formal: &JavaType, arg: &Expr) -> Option<Expr> {
4907        // A dex const carries the CALLEE's BITS: `const v, #0x41000000`
4908        // feeding a float formal IS 8.0f. Rendering the raw int literal
4909        // (a) mis-selects overloads — a competing `a(Context,int):void`
4910        // beats the int→float-widened `a(Context,float):int` in javac's
4911        // phase order ("不兼容的类型: void无法转换为int", weibo ju.a
4912        // ×1,259) — and (b) corrupts the VALUE (widening turns the bits
4913        // into 1.09e9f). The descriptor formal is dex ground truth:
4914        // reinterpret. Symmetric for the float-const-into-int-formal
4915        // direction (caller's bits, callee's reading).
4916        match (arg, formal) {
4917            (Expr::Const(ConstVal::Int(b)), JavaType::Float) => {
4918                return Some(Expr::Const(ConstVal::Float(f32::from_bits(*b as u32))));
4919            }
4920            (Expr::Const(ConstVal::Long(b)), JavaType::Double) => {
4921                return Some(Expr::Const(ConstVal::Double(f64::from_bits(*b as u64))));
4922            }
4923            (Expr::Const(ConstVal::Float(f)), JavaType::Int) => {
4924                return Some(Expr::Const(ConstVal::Int(f.to_bits() as i32)));
4925            }
4926            (Expr::Const(ConstVal::Double(d)), JavaType::Long) => {
4927                return Some(Expr::Const(ConstVal::Long(d.to_bits() as i64)));
4928            }
4929            _ => {}
4930        }
4931        if matches!(actual, JavaType::Boolean)
4932            && matches!(
4933                formal,
4934                JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
4935                    | JavaType::Long | JavaType::Float | JavaType::Double
4936            )
4937        {
4938            Some(Expr::Cast {
4939                ty: TypeRef::J(formal.clone()),
4940                e: Box::new(Expr::Cond {
4941                    c: Box::new(arg.clone()),
4942                    t: Box::new(Expr::Const(ConstVal::Int(1))),
4943                    f: Box::new(Expr::Const(ConstVal::Int(0))),
4944                }),
4945            })
4946        } else if matches!(formal, JavaType::Boolean)
4947            && matches!(
4948                actual,
4949                JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
4950                    | JavaType::Long | JavaType::Float | JavaType::Double
4951            )
4952        {
4953            Some(Expr::Bin {
4954                op: BinOp::Ne,
4955                l: Box::new(arg.clone()),
4956                r: Box::new(Expr::Const(ConstVal::Int(0))),
4957                ty: Some(TypeRef::J(JavaType::Boolean)),
4958            })
4959        } else if matches!(
4960            actual,
4961            JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
4962                | JavaType::Long | JavaType::Float | JavaType::Double
4963        ) && matches!(
4964            formal,
4965            JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
4966                | JavaType::Long | JavaType::Float | JavaType::Double
4967        ) && !implicit_widening(actual, formal)
4968        {
4969            // Numeric NARROWING into a formal (int arg → char formal):
4970            // javac rejects the lossy conversion, or — worse when the
4971            // class has no wider overload — silently resolves a
4972            // DIFFERENT overload than the dex descriptor names
4973            // (SpannableStringBuilder.append(int) doesn't exist;
4974            // `append(char)` is "从int转换到char可能会有损失", weibo ×76).
4975            // The descriptor formal is the dex ground truth; casting to
4976            // it pins the overload the bytecode actually invoked.
4977            Some(Expr::Cast {
4978                ty: TypeRef::J(formal.clone()),
4979                e: Box::new(arg.clone()),
4980            })
4981        } else {
4982            None
4983        }
4984    }
4985    /// A bare `null` argument against a SPECIFIC reference formal takes
4986    /// the descriptor cast: `this(context, null)` matching both
4987    /// Builder(Context,Notification) and Builder(Context,String) is
4988    /// "对Builder的引用不明确" (weibo NotificationCompat; protobuf
4989    /// `new FieldSet.Builder(null)` ×54). The descriptor names the
4990    /// exact invoked ctor, so the cast is dex ground truth — the
4991    /// descriptor-exact doctrine, not a guessed overload shift. Bare
4992    /// null stays for Object formals (casting those would add noise
4993    /// without resolving anything javac could not already pick).
4994    /// Framework classes javac resolves through android.jar even when
4995    /// the dex pool lacks them — the embedded API database answers
4996    /// precisely (the hand-written prefix list it replaced missed
4997    /// everything outside the guessed roots and over-trusted within
4998    /// them; removed-API families like org.apache.http are in the DB
4999    /// too, matching what a legacy-jar classpath resolves).
5000    fn fw_resolvable(n: &str) -> bool {
5001        crate::fwdb::exists(n)
5002    }
5003    fn null_arg_cast(a: &mut Expr, f: &JavaType, pool: &DexPool) -> bool {
5004        if !matches!(&*a, Expr::Const(ConstVal::Null)) {
5005            return false;
5006        }
5007        if let JavaType::Object(n) = f {
5008            // Cast only to RESOLVABLE types: a phantom formal (R8 kept
5009            // the ref, dropped the class — androidx.window.sidecar)
5010            // would turn a compiling bare `null` into a fresh
5011            // cannot-find. Pool classes render into the output;
5012            // java/javax/android/dalvik resolve through android.jar.
5013            let resolvable = pool.get(n).is_some() || fw_resolvable(n);
5014            if n.as_ref() != "java/lang/Object" && resolvable {
5015                *a = Expr::Cast {
5016                    ty: TypeRef::J(f.clone()),
5017                    e: Box::new(Expr::Const(ConstVal::Null)),
5018                };
5019                return true;
5020            }
5021        } else if let JavaType::Array(_) = f {
5022            // Descriptor-exact ARRAY cast: the dex named this exact
5023            // overload; a bare null beside sibling overloads is javac-
5024            // ambiguous (`a(String,b2)` + `a(String,String[])` both match
5025            // — weibo org/a/c/b, 对a的引用不明确 family). Same phantom
5026            // gate on the element class: casting to a phantom-element
5027            // array would trade the ambiguity for a cannot-find.
5028            let mut e: &JavaType = f;
5029            while let JavaType::Array(inner) = e {
5030                e = &**inner;
5031            }
5032            let resolvable = match e {
5033                JavaType::Object(n) => pool.get(n).is_some() || fw_resolvable(n),
5034                _ => true, // primitive-element array
5035            };
5036            if resolvable {
5037                *a = Expr::Cast {
5038                    ty: TypeRef::J(f.clone()),
5039                    e: Box::new(Expr::Const(ConstVal::Null)),
5040                };
5041                return true;
5042            }
5043        }
5044        // Bare null deliberately stays (Object formal / phantom type):
5045        // the caller's ambiguity pin may still act on it.
5046        false
5047    }
5048    /// JLS 5.1.2 widening primitive conversion (char joins the chain
5049    /// only at int; byte/short do NOT widen to char).
5050    fn implicit_widening(a: &JavaType, f: &JavaType) -> bool {
5051        use JavaType::*;
5052        match (a, f) {
5053            (Byte, Short) | (Byte, Int) | (Byte, Long) | (Byte, Float) | (Byte, Double) => true,
5054            (Short, Int) | (Short, Long) | (Short, Float) | (Short, Double) => true,
5055            (Char, Int) | (Char, Long) | (Char, Float) | (Char, Double) => true,
5056            (Int, Long) | (Int, Float) | (Int, Double) => true,
5057            (Long, Float) | (Long, Double) => true,
5058            (Float, Double) => true,
5059            (x, y) => x == y,
5060        }
5061    }
5062    // Does the owner class declare ANOTHER method with the same name
5063    // and arity but a different signature? Then javac's most-specific
5064    // overload resolution can steal the call from the dex descriptor's
5065    // target (weibo 对a的引用不明确 ×391: `c.a(null)` against
5066    // a(ExecuteResult)/a(Exception)/a(Object) — the descriptor names
5067    // a(Object), but bare null leaves the two specific overloads
5068    // ambiguously most-specific). Framework owners are not in the
5069    // pool — conservatively report no competition.
5070    let overload_competes = |cls: &str, name: &str, args: &[JavaType]| -> bool {
5071        if let Some(c) = pool.get(cls) {
5072            return c.all_methods().any(|m| {
5073                &*m.name == name
5074                    // Bridge/synthetic siblings are compiler artifacts
5075                    // resolving to the SAME target — counting them as
5076                    // competitors cast enum compareTo args to the erased
5077                    // formal the rendered specialization no longer
5078                    // accepts (Enum无法转换为LogLevel ×2,079 reqable).
5079                    && m.access & (crate::access::ACC_BRIDGE | crate::access::ACC_SYNTHETIC) == 0
5080                    && m.parsed_desc()
5081                        .is_some_and(|d| d.args.len() == args.len() && d.args != args)
5082            });
5083        }
5084        // Framework owner: NOT enumerable for pin purposes — the
5085        // full-DB enumeration experiment (step-5) measured +532 battery
5086        // with ZERO ambiguous gain: a subtype arg against a concrete
5087        // erased formal fires the cast even when the call was never
5088        // ambiguous, and bounded-generic formals (List<T> -> List,
5089        // Property<T,Float> erasures) make the cast break inference or
5090        // miss the rendered specialization. Framework subtype pins stay
5091        // on the measured allowlist below.
5092        let _ = args;
5093        false
5094    };
5095    // Ctors: formal types via the pool (unanimous across same-arity
5096    // overloads, else skip — ambiguity must not guess).
5097    let ctor_formals = |cls: &str, n: usize| -> Option<Vec<JavaType>> {
5098        let c = pool.get(cls)?;
5099        let mut out: Option<Vec<JavaType>> = None;
5100        for m in c.all_methods() {
5101            if &*m.name != "<init>" {
5102                continue;
5103            }
5104            let Some(d) = m.parsed_desc() else { continue };
5105            if d.args.len() != n {
5106                continue;
5107            }
5108            match &out {
5109                None => out = Some(d.args.clone()),
5110                Some(prev) if *prev != d.args => return None,
5111                Some(_) => {}
5112            }
5113        }
5114        out
5115    };
5116    walk_stmt_exprs(body, &mut |e| {
5117        deep_rewrite(e, &mut |x| match x {
5118            Expr::Method {
5119                cls,
5120                name,
5121                desc,
5122                args,
5123                is_static,
5124                ..
5125            } => {
5126                if args.len() != desc.args.len() {
5127                    return;
5128                }
5129                let _ = is_static;
5130                let mut competes: Option<bool> = None;
5131                for (a, f) in args.iter_mut().zip(desc.args.iter()) {
5132                    if null_arg_cast(a, f, pool) {
5133                        continue;
5134                    }
5135                    if matches!(a, Expr::Cast { .. }) {
5136                        continue;
5137                    }
5138                    // Descriptor-exact ambiguity pin: a bare null
5139                    // against an Object formal (null_arg_cast
5140                    // deliberately leaves those bare — safe only
5141                    // WITHOUT competing specific overloads) or a
5142                    // strict-subtype arg can match several declared
5143                    // overloads; casting to the descriptor formal
5144                    // leaves exactly the dex-invoked method
5145                    // applicable (JLS 15.12.2 — the descriptor is
5146                    // ground truth, so the cast pins resolution
5147                    // instead of shifting it).
5148                    let subtype_arg = match (&val_ty(a, vt), f) {
5149                        (JavaType::Object(an), JavaType::Object(fn_)) => {
5150                            // java/lang/Object is the root: ANY reference
5151                            // arg is a strict subtype of it — pool.is_subtype
5152                            // cannot see framework chains (String -> Object),
5153                            // so the Object formal gets the direct rule.
5154                            (an != fn_ && pool.is_subtype(an, fn_))
5155                                || (fn_.as_ref() == "java/lang/Object"
5156                                    && an.as_ref() != "java/lang/Object")
5157                        }
5158                        (JavaType::Array(_), JavaType::Object(fn_))
5159                            if fn_.as_ref() == "java/lang/Object" =>
5160                        {
5161                            true
5162                        }
5163                        _ => false,
5164                    };
5165                    // Bare null against java/lang/Object itself: the
5166                    // descriptor method exists (dex resolved it), so
5167                    // `(Object) null` is always legal and excludes every
5168                    // more-specific sibling overload from applicability.
5169                    // Pool owners take the competes gate (emitted
5170                    // declarations are raw, so the cast cannot disturb
5171                    // inference). FRAMEWORK owners only via allowlist of
5172                    // the classic non-generic null-ambiguous JDK methods
5173                    // (StringBuilder.append ×66 lark): a blanket cast
5174                    // breaks GENERIC framework methods whose descriptor
5175                    // formal is an ERASED type variable —
5176                    // `ofFloat((Object) null, View.ALPHA, v)` killed the
5177                    // T=View inference bare null provided (weibo +4).
5178                    let null_obj =
5179                        matches!(a, Expr::Const(ConstVal::Null)) && matches!(f, JavaType::Object(_));
5180                    let framework_owner = pool.get(cls.as_ref()).is_none();
5181                    let fw_null_ambiguous = framework_owner
5182                        && matches!(
5183                            (cls.as_ref(), name.as_ref()),
5184                            ("java/lang/StringBuilder", "append")
5185                                | ("java/lang/StringBuffer", "append")
5186                                | ("java/io/PrintStream", "println")
5187                                | ("java/io/PrintStream", "print")
5188                                | ("java/io/PrintWriter", "println")
5189                                | ("java/io/PrintWriter", "print")
5190                                | ("java/lang/String", "valueOf")
5191                        );
5192                    // Framework owners with famously overload-heavy
5193                    // methods take the subtype pin too: the arg's
5194                    // rendered type implements SEVERAL formal types
5195                    // (weibo putExtra ×41: floatMsgData is both
5196                    // Parcelable and Serializable; schedule/submit:
5197                    // Runnable-and-Callable workers) and the competes
5198                    // gate cannot enumerate framework overloads.
5199                    // Formal == java/lang/Object stays excluded — that
5200                    // is an erased type variable and the cast would
5201                    // kill generic inference (ofFloat lesson).
5202                    let fw_subtype_pinnable = framework_owner
5203                        && subtype_arg
5204                        && !matches!(f, JavaType::Object(fn_) if fn_.as_ref() == "java/lang/Object")
5205                        && matches!(
5206                            (cls.as_ref(), name.as_ref()),
5207                            ("android/content/Intent", "putExtra")
5208                                | ("java/util/concurrent/ScheduledExecutorService", "schedule")
5209                                | ("java/util/concurrent/ExecutorService", "submit")
5210                                | ("java/util/concurrent/ExecutorService", "invokeAll")
5211                        );
5212                    if fw_subtype_pinnable
5213                        || (subtype_arg
5214                            && *competes.get_or_insert_with(|| {
5215                                overload_competes(cls, name, &desc.args)
5216                            }))
5217                        || (null_obj
5218                            && (fw_null_ambiguous
5219                                || *competes.get_or_insert_with(|| {
5220                                    overload_competes(cls, name, &desc.args)
5221                                })))
5222                    {
5223                        let old =
5224                            std::mem::replace(a, Expr::Const(ConstVal::Null));
5225                        *a = Expr::Cast {
5226                            ty: TypeRef::J(f.clone()),
5227                            e: Box::new(old),
5228                        };
5229                        continue;
5230                    }
5231                    if !is_bool(&val_ty(a, vt)) && !is_num(&val_ty(a, vt)) {
5232                        continue;
5233                    }
5234                    if !is_bool(f) && !is_num(f) {
5235                        continue;
5236                    }
5237                    if let Some(b) = bridge(&val_ty(a, vt), f, a) {
5238                        *a = b;
5239                    }
5240                }
5241            }
5242            Expr::New { cls, args, arg_tys, .. } => {
5243                let formals_owned = if arg_tys.len() == args.len() && !arg_tys.is_empty() {
5244                    Some(arg_tys.clone())
5245                } else {
5246                    ctor_formals(cls, args.len())
5247                };
5248                if let Some(formals) = formals_owned {
5249                    for (a, f) in args.iter_mut().zip(formals.iter()) {
5250                        if null_arg_cast(a, f, pool) {
5251                            continue;
5252                        }
5253                        if matches!(a, Expr::Cast { .. }) {
5254                            continue;
5255                        }
5256                        if !is_bool(f) && !is_num(f) {
5257                            continue;
5258                        }
5259                        if let Some(b) = bridge(&val_ty(a, vt), f, a) {
5260                            *a = b;
5261                        }
5262                    }
5263                }
5264            }
5265            _ => {}
5266        });
5267    });
5268}
5269
5270/// `b ^ 1` / `1 ^ b` with a BOOLEAN operand is the bytecode's boolean
5271/// negation (`!b`) — weibo kotlin-stdlib `return v2 ^ 1;` ("二元运算符
5272/// '^' 的操作数类型错误"), and `x ^ true` shapes. Java only types `^`
5273/// for same-category operands.
5274/// Rename locals whose name equals a ROOT PACKAGE first segment: a
5275/// package-qualified render (`v2.n.a` — the FQN fallback a twin-shadowed
5276/// same-package ref must take) binds its first identifier lexically, and
5277/// a local `v2` in scope captures it (weixin v2/i: 找不到符号 变量 n on a
5278/// legit `v2.n.a(this, 0)`). Method-scope renames carry no registry
5279/// surface — every use renders through this same vt — unlike class/field
5280/// renames, whose reference coverage blew up five times before.
5281pub fn deshadow_locals(vt: &mut VarTable, pool: &crate::DexPool, class_name: &str, body: &Stmt) {
5282    let segs = pool.root_pkg_segs();
5283    // Import-layer simples: the file renders `import p.h;` + bare `h.e`
5284    // for obscured refs — a local `h` captures that simple name (locals
5285    // beat imports in expression position; 无法取消引用int family).
5286    let obscured = crate::classdec::obscured_simples_snapshot();
5287    // Same-package static-ref simples: a static member ref renders its
5288    // class qualifier as the SIMPLE name (same package — no import/FQN),
5289    // and a local with that name captures it (`a2.changeQuickRedirect`
5290    // bound to a String local `a2` — "找不到符号 变量 changeQuickRedirect
5291    // 位置: 类型为String的变量 a2", weibo robust-field refs ×317). Only
5292    // STATIC refs qualify (an instance ref's qualifier IS the local).
5293    let self_pkg = class_name.rfind('/').map(|i| &class_name[..i]).unwrap_or("");
5294    let mut same_pkg_simples: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
5295    visit_all_exprs(body, &mut |x| {
5296        let cls = match x {
5297            Expr::Field { cls, is_static: true, .. } => Some(cls.as_ref()),
5298            Expr::Method { cls, is_static: true, .. } => Some(cls.as_ref()),
5299            _ => None,
5300        };
5301        if let Some(c) = cls {
5302            let pkg = c.rfind('/').map(|i| &c[..i]).unwrap_or("");
5303            if pkg == self_pkg {
5304                let simple = &c[c.rfind('/').map(|i| i + 1).unwrap_or(0)..];
5305                let head = simple.split('$').next().unwrap_or(simple);
5306                if !head.is_empty() {
5307                    same_pkg_simples.insert(head.to_string());
5308                }
5309            }
5310        }
5311    });
5312    let hit = |name: &str| {
5313        segs.contains(name) || obscured.contains(name) || same_pkg_simples.contains(name)
5314    };
5315    if (segs.is_empty() && obscured.is_empty() && same_pkg_simples.is_empty())
5316        || !vt.vars.iter().any(|v| hit(&v.name))
5317    {
5318        return;
5319    }
5320    let mut taken: jdc_core::FxHashSet<String> =
5321        vt.vars.iter().map(|v| v.name.clone()).collect();
5322    for v in vt.vars.iter_mut() {
5323        if !hit(&v.name) {
5324            continue;
5325        }
5326        let mut k = 0u32;
5327        let cand = loop {
5328            k += 1;
5329            let c = if k == 1 {
5330                format!("{}x", v.name)
5331            } else {
5332                format!("{}x{}", v.name, k)
5333            };
5334            if !taken.contains(&c) {
5335                break c;
5336            }
5337        };
5338        taken.insert(cand.clone());
5339        v.name = cand;
5340    }
5341}
5342
5343/// Receiver rescue: a Field/Method receiver whose final vt type is
5344/// PRIMITIVE can never hold the member (structurer tail-copy regions
5345/// can map the receiver register onto a boolean/int generation —
5346/// wcdb HandleOperation's `v10_g2.finalizeStatement()` where the
5347/// statement var is right there; weixin deref-int/bool families,
5348/// ~2.5k lines). The member descriptor's owner class is authoritative:
5349/// when EXACTLY ONE var in the method has that type (or a subtype),
5350/// swap the receiver to it. The pre-rescue output is a guaranteed
5351/// compile error AND runtime NPE, so the unambiguous swap cannot make
5352/// it worse; ambiguous sites stay untouched.
5353/// Anonymous/inner-class outer reads: the dex reads `this$0` off the
5354/// instance register, which copy materialization can route through a
5355/// local (`v41 = this; ... v41.this$0` — weibo HorseRaceDetector$1,
5356/// 2,326-line 找不到符号 变量 this$0 family). When the field's
5357/// declaring class IS the class being decompiled, the only instance in
5358/// scope is `this` — strip the owner so it renders the bare declared
5359/// field.
5360/// Fold single-assigned `this`-copy locals in ctors back to `this`.
5361/// R8 materializes the receiver with `move-object/from16 v1, p0` and
5362/// runs every iput through the copy; the copy renders as a local and a
5363/// final-field store through it is "无法为 final 变量 g 分配值" (Java
5364/// allows blank-final writes only via `this`/simple name — hf/n2 ×29,
5365/// weixin final-var residual). Transitively: `b = a; c = b` chains
5366/// collapse. Single-assignment gate: a var ever written from another
5367/// value is not an alias.
5368pub fn fix_ctor_this_aliases(body: &mut Stmt, vt: &VarTable) {
5369    let Some(this_var) = vt
5370        .vars
5371        .iter()
5372        .find(|v| v.is_param && v.name == "this")
5373        .map(|v| v.id)
5374    else {
5375        return;
5376    };
5377    let mut analysis = VarAnalysis {
5378        assigns: Vec::new(),
5379        reads: Vec::new(),
5380        values: Vec::new(),
5381    };
5382    analyze_vars(body, &mut analysis);
5383    // All-sources rule: a local is a this-alias only when EVERY value
5384    // assignment traces to `this_var` through pure local copies. The
5385    // single-assign constraint this replaces missed R8's multi-branch
5386    // `this` spills (uuyc androidx.camera x_2: `v160 = v150` on two
5387    // paths, both this, chain this→v131→v140→v169→v174→v183→v150→v160),
5388    // so `v160.d = executor` never became `this.d = executor` and javac
5389    // rejected the final-field init (无法为 final 变量 d/i/j 分配值 ×103).
5390    // A single non-this source (field read, `new`, param, a literal)
5391    // disqualifies the local, so a marked alias is provably always this
5392    // and rewriting every read of it to `this` is sound.
5393    enum Src {
5394        This,
5395        Loc(u32),
5396        Other,
5397    }
5398    let mut sources: jdc_core::FxHashMap<u32, Vec<Src>> = jdc_core::FxHashMap::default();
5399    walk_all(body, &mut |st| {
5400        let (tgt, val): (u32, &Expr) = match st {
5401            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => match &**target {
5402                Expr::Local { var, .. } => (*var, &**value),
5403                _ => return,
5404            },
5405            Stmt::LocalDef { var, init: Some(value), .. } => (*var, value),
5406            _ => return,
5407        };
5408        let s = match val {
5409            Expr::This => Src::This,
5410            Expr::Local { var: src, .. } if *src == this_var => Src::This,
5411            Expr::Local { var: src, .. } => Src::Loc(*src),
5412            _ => Src::Other,
5413        };
5414        sources.entry(tgt).or_default().push(s);
5415    });
5416    let n = vt
5417        .vars
5418        .len()
5419        .max(analysis.assigns.len())
5420        .max(sources.keys().map(|k| *k as usize + 1).max().unwrap_or(0));
5421    let mut alias = vec![false; n];
5422    loop {
5423        let mut changed = false;
5424        for (v, srcs) in &sources {
5425            let vi = *v as usize;
5426            if vi >= n || alias[vi] || srcs.is_empty() {
5427                continue;
5428            }
5429            let ok = srcs.iter().all(|s| match s {
5430                Src::This => true,
5431                Src::Loc(w) => (*w as usize) < n && alias[*w as usize],
5432                Src::Other => false,
5433            });
5434            if ok {
5435                alias[vi] = true;
5436                changed = true;
5437            }
5438        }
5439        if !changed {
5440            break;
5441        }
5442    }
5443    if !alias.iter().any(|&b| b) {
5444        return;
5445    }
5446    walk_stmt_exprs(body, &mut |e| {
5447        deep_rewrite_reads(e, &mut |x| {
5448            if let Expr::Local { var, .. } = x {
5449                if (*var as usize) < n && alias[*var as usize] {
5450                    *x = Expr::This;
5451                }
5452            }
5453        });
5454    });
5455}
5456
5457pub fn fix_this0_owners(body: &mut Stmt, class_name: &str) {
5458    walk_stmt_exprs(body, &mut |e| {
5459        deep_rewrite(e, &mut |x| {
5460            if let Expr::Field { owner: Some(_), cls, name, .. } = x {
5461                if name.as_ref() == "this$0" && cls.as_ref() == class_name {
5462                    if let Expr::Field { owner, .. } = x {
5463                        *owner = None;
5464                    }
5465                }
5466            }
5467        });
5468    });
5469}
5470
5471pub fn rescue_primitive_receivers(body: &mut Stmt, vt: &VarTable, pool: &crate::DexPool) {
5472    fn prim(t: &JavaType) -> bool {
5473        match t {
5474            JavaType::Int
5475            | JavaType::Long
5476            | JavaType::Short
5477            | JavaType::Byte
5478            | JavaType::Char
5479            | JavaType::Float
5480            | JavaType::Double
5481            | JavaType::Boolean => true,
5482            // Boxed JDK finals carry no app members either (`bool19.d`
5483            // where bool19 came out java/lang/Boolean — the coroutine
5484            // Result-register conflation).
5485            JavaType::Object(n) => matches!(
5486                n.as_ref(),
5487                "java/lang/Boolean"
5488                    | "java/lang/String"
5489                    | "java/lang/Integer"
5490                    | "java/lang/Long"
5491                    | "java/lang/Short"
5492                    | "java/lang/Byte"
5493                    | "java/lang/Character"
5494                    | "java/lang/Float"
5495                    | "java/lang/Double"
5496            ),
5497            _ => false,
5498        }
5499    }
5500    walk_stmt_exprs(body, &mut |e| {
5501        deep_rewrite(e, &mut |x| {
5502            // Phase 1 (read-only): primitive-typed Local receiver?
5503            let (recv_var, recv_ty) = {
5504                let o: &Expr = match &*x {
5505                    Expr::Method { owner: Some(o), .. } => o,
5506                    Expr::Field { owner: Some(o), is_static: false, .. } => o,
5507                    _ => return,
5508                };
5509                let Expr::Local { var, ty } = o else {
5510                    return;
5511                };
5512                let vt_ty = if (*var as usize) < vt.vars.len() {
5513                    vt.var(*var).ty.erased()
5514                } else {
5515                    ty.erased()
5516                };
5517                if !prim(&vt_ty) {
5518                    return;
5519                }
5520                (*var, vt_ty)
5521            };
5522            let cls: std::sync::Arc<str> = match &*x {
5523                Expr::Method { cls, .. } | Expr::Field { cls, .. } => cls.clone(),
5524                _ => return,
5525            };
5526            // Type-consistent call: the receiver's own type IS the
5527            // member owner (or a subtype) — `str.hashCode()` on a
5528            // String param needs no rescue. Without this the boxed-JDK
5529            // prim() arm fired on every String member call and the
5530            // max-id fallback below hijacked the reads to the highest
5531            // String-typed var in the method — often a case-local
5532            // (lark LynxUIBaseInput str11: 37 reads rewritten to a
5533            // switch-case-scoped def, 找不到符号 ×37; x0/c$a18 str4).
5534            if let JavaType::Object(n) = &recv_ty {
5535                if n.as_ref() == cls.as_ref() || pool.is_subtype(n.as_ref(), cls.as_ref()) {
5536                    return;
5537                }
5538            }
5539            // Phase 2: vars of the owner type (exact or subtype).
5540            let mut cands: Vec<u32> = Vec::new();
5541            for v in &vt.vars {
5542                if let JavaType::Object(n) = v.ty.erased() {
5543                    if n.as_ref() == cls.as_ref()
5544                        || pool.is_subtype(n.as_ref(), cls.as_ref())
5545                    {
5546                        cands.push(v.id);
5547                    }
5548                }
5549            }
5550            let id = if cands.len() == 1 {
5551                cands[0]
5552            } else if cands.len() > 1 {
5553                // Multiple owner-typed vars: prefer the same-SLOT one
5554                // (register lineage), latest generation wins — the
5555                // receiver position is scope-insensitive (any method-
5556                // scope var renders, the decl hoist covers it).
5557                let slot = if (recv_var as usize) < vt.vars.len() {
5558                    Some(vt.var(recv_var).slot)
5559                } else {
5560                    None
5561                };
5562                let mut hits: Vec<u32> = match slot {
5563                    Some(sl) => cands
5564                        .iter()
5565                        .copied()
5566                        .filter(|&c| {
5567                            c != recv_var
5568                                && (c as usize) < vt.vars.len()
5569                                && vt.var(c).slot == sl
5570                        })
5571                        .collect(),
5572                    None => Vec::new(),
5573                };
5574                if hits.is_empty() {
5575                    // No same-slot twin: fall back to the latest
5576                    // owner-typed generation (max id).
5577                    hits = cands
5578                        .iter()
5579                        .copied()
5580                        .filter(|&c| c != recv_var)
5581                        .collect();
5582                }
5583                hits.sort_unstable();
5584                hits.dedup();
5585                if hits.is_empty() {
5586                    return;
5587                }
5588                *hits.last().unwrap()
5589            } else {
5590                // Lineage disambiguation by SLOT: register reuse makes
5591                // the Boolean/primitive twin and the object version of
5592                // one dex register share VarInfo.slot (ry0/h2's
5593                // `bool19.d` — the v1-typed twin rides the same
5594                // register). Multiple slot matches stay untouched
5595                // (deterministic conservatism).
5596                if cands.is_empty() {
5597                    return;
5598                }
5599                let slot = if (recv_var as usize) < vt.vars.len() {
5600                    vt.var(recv_var).slot
5601                } else {
5602                    return;
5603                };
5604                let mut hits: Vec<u32> = cands
5605                    .into_iter()
5606                    .filter(|&c| {
5607                        c != recv_var
5608                            && (c as usize) < vt.vars.len()
5609                            && vt.var(c).slot == slot
5610                    })
5611                    .collect();
5612                hits.sort_unstable();
5613                hits.dedup();
5614                if hits.len() != 1 {
5615                    return;
5616                }
5617                hits[0]
5618            };
5619            // Phase 3: swap.
5620            let o: &mut Box<Expr> = match x {
5621                Expr::Method { owner: Some(o), .. } => o,
5622                Expr::Field { owner: Some(o), is_static: false, .. } => o,
5623                _ => return,
5624            };
5625            if let Expr::Local { var, ty } = &mut **o {
5626                if *var == recv_var {
5627                    *var = id;
5628                    *ty = vt.vars[id as usize].ty.clone();
5629                }
5630            }
5631        });
5632    });
5633
5634}
5635
5636/// Scope-safe arg/return swaps for mistyped generations — runs AFTER
5637/// ensure_declared hoists every declaration to the method top, so the
5638/// top_ids visibility precondition holds by construction (the r134
5639/// falsification: swapping in a block-scoped var broke lexically).
5640/// Same-slot + type-match + uniqueness gates as the receiver rescue.
5641pub fn rescue_arg_return_swaps(
5642    body: &mut Stmt,
5643    vt: &VarTable,
5644    pool: &crate::DexPool,
5645    ret_ty: &JavaType,
5646) {
5647    // SCOPE-SAFE arg/return swap: a primitive-typed actual against a
5648    // specific-reference formal (or a reference-returning `return`) is
5649    // a mistyped generation — the r134 falsification showed the swap
5650    // target MUST be lexically visible at the use site, so candidates
5651    // are restricted to params + TOP-LEVEL declared locals (a hoisted
5652    // decl is visible to javac name resolution everywhere in the
5653    // method) AND same-slot (same dex register lineage) AND unique.
5654    let mut top_ids: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
5655    for v in &vt.vars {
5656        if v.is_param {
5657            top_ids.insert(v.id);
5658        }
5659    }
5660    if let Stmt::Block(stmts) = body {
5661        for st in stmts.iter() {
5662            if let Stmt::LocalDef { var, .. } = st {
5663                top_ids.insert(*var);
5664            }
5665        }
5666    }
5667    let ret_wants_ref = match ret_ty {
5668        JavaType::Object(n) => n.as_ref() != "java/lang/Object",
5669        JavaType::Array(_) => true,
5670        _ => false,
5671    };
5672    let try_swap = |a: &mut Expr, want: &JavaType| {
5673        let Expr::Local { var, ty } = &mut *a else {
5674            return;
5675        };
5676        if (*var as usize) >= vt.vars.len() {
5677            return;
5678        }
5679        let avt = vt.var(*var).ty.erased();
5680        let avt_ref = matches!(avt, JavaType::Object(_) | JavaType::Array(_));
5681        let want_arr = matches!(want, JavaType::Array(_));
5682        // Only swap a primitive-typed actual (an Object actual gets the
5683        // descriptor-exact cast instead — never both).
5684        if avt_ref && !(avt == JavaType::Object("java/lang/Object".into())) {
5685            return;
5686        }
5687        if avt_ref && !matches!(want, JavaType::Object(_)) {
5688            return;
5689        }
5690        let slot = vt.var(*var).slot;
5691        let mut hits: Vec<u32> = Vec::new();
5692        for v in &vt.vars {
5693            if v.slot != slot || !top_ids.contains(&v.id) || v.id == *var {
5694                continue;
5695            }
5696            let t = v.ty.erased();
5697            let ok = match (&t, want) {
5698                (JavaType::Object(n), JavaType::Object(w)) => {
5699                    n.as_ref() == w.as_ref()
5700                        || pool.is_subtype(n.as_ref(), w.as_ref())
5701                }
5702                (JavaType::Array(_), JavaType::Array(_)) => want_arr,
5703                _ => false,
5704            };
5705            if ok {
5706                hits.push(v.id);
5707            }
5708        }
5709        hits.sort_unstable();
5710        hits.dedup();
5711        if hits.is_empty() {
5712            return;
5713        }
5714        // Multiple same-slot candidates: the LATEST generation (max id —
5715        // split mints in walk order ≈ program order) is the closest
5716        // approximation of the register's value at the use site. A
5717        // wrong pick still compiles (type-matched by construction);
5718        // leaving the primitive actual never does.
5719        let id = *hits.last().unwrap();
5720        *var = id;
5721        *ty = vt.vars[id as usize].ty.clone();
5722    };
5723    walk_stmt_exprs(body, &mut |e| {
5724        deep_rewrite(e, &mut |x| {
5725            match x {
5726                Expr::Method { desc, args, is_dynamic: false, .. } => {
5727                    for (i, a) in args.iter_mut().enumerate() {
5728                        if let Some(f) = desc.args.get(i) {
5729                            if matches!(f, JavaType::Object(n) if n.as_ref() != "java/lang/Object")
5730                                || matches!(f, JavaType::Array(_))
5731                            {
5732                                try_swap(a, f);
5733                            }
5734                        }
5735                    }
5736                }
5737                Expr::New { arg_tys, args, .. } => {
5738                    for (i, a) in args.iter_mut().enumerate() {
5739                        if let Some(f) = arg_tys.get(i) {
5740                            if matches!(f, JavaType::Object(n) if n.as_ref() != "java/lang/Object")
5741                                || matches!(f, JavaType::Array(_))
5742                            {
5743                                try_swap(a, f);
5744                            }
5745                        }
5746                    }
5747                }
5748                _ => {}
5749            }
5750        });
5751    });
5752    if ret_wants_ref {
5753        walk_mut_deep(body, &mut |st| {
5754            if let Stmt::Return(Some(e)) = st {
5755                try_swap(e, ret_ty);
5756            }
5757        });
5758    }
5759}
5760
5761/// Reference-array initializers take null, not 0: dex fill-array-data
5762/// over a String[]/Object[] slot carries the zero word, and the lifted
5763/// `new String[] {0}` is int无法转换为String (qs0/b family). A Const
5764/// Int(0) element of a reference-typed array init is the null ref.
5765/// `refVar = const 0` (dex `const/4 vN, 0` into a reference register) is
5766/// the NULL sentinel, not an int. By the time this runs infer_types has
5767/// typed the var a reference (its `.close()`/`.length`/method reads
5768/// proved it), so retype the const to Null BEFORE split_generations —
5769/// otherwise the ref/primitive mismatch splits off an int generation and
5770/// orphans the reference reads (u7.l try-with-resources finally
5771/// `v16_g3 = 0; if (v16_g3 != 0) v16_g3.close();` → "无法取消引用int").
5772/// `fix_incomparable_equality` already retypes the `refVar != 0`
5773/// COMPARISON side (but runs after the split); this covers the ASSIGN
5774/// side, which is what triggers the bad split.
5775pub fn fix_ref_null_assigns(vt: &VarTable, body: &mut Stmt) {
5776    let is_ref_var = |var: u32| -> bool {
5777        (var as usize) < vt.vars.len()
5778            && matches!(
5779                vt.vars[var as usize].ty.erased(),
5780                JavaType::Object(_) | JavaType::Array(_)
5781            )
5782    };
5783    walk_mut_deep(body, &mut |st| match st {
5784        Stmt::ExprStmt(Expr::Assign {
5785            target,
5786            value,
5787            op: AssignOp::Plain,
5788            ..
5789        }) => {
5790            if let Expr::Local { var, .. } = &**target {
5791                if is_ref_var(*var) && matches!(&**value, Expr::Const(ConstVal::Int(0))) {
5792                    **value = Expr::Const(ConstVal::Null);
5793                }
5794            }
5795        }
5796        Stmt::LocalDef {
5797            var,
5798            init: Some(e),
5799            ..
5800        } => {
5801            if is_ref_var(*var) && matches!(e, Expr::Const(ConstVal::Int(0))) {
5802                *e = Expr::Const(ConstVal::Null);
5803            }
5804        }
5805        _ => {}
5806    });
5807}
5808
5809pub fn fix_ref_array_null_consts(body: &mut Stmt) {
5810    walk_stmt_exprs(body, &mut |e| {
5811        deep_rewrite(e, &mut |x| {
5812            if let Expr::NewArray { elem, init: Some(list), .. } = x {
5813                if matches!(elem.erased(), JavaType::Object(_) | JavaType::Array(_)) {
5814                    for slot in list.iter_mut() {
5815                        if matches!(&*slot, Expr::Const(ConstVal::Int(0))) {
5816                            *slot = Expr::Const(ConstVal::Null);
5817                        }
5818                    }
5819                }
5820            }
5821        });
5822    });
5823}
5824
5825// ---------------------------------------------------------------------------
5826// Null-sentinel repair: const-0 registers in reference-typed slots
5827// ---------------------------------------------------------------------------
5828
5829/// A dex `const/4 vN, 0` register is ambiguous between int 0 and the
5830/// null reference; the value view types the bare constant as Int.
5831/// Kotlin coroutine state machines lean on this heavily — captured
5832/// object slots are zero-initialized ahead of the label switch, then
5833/// stored into `Object` fields and passed as `Continuation` ctor args
5834/// (weixin y01/s9 q8: `new e6(s94x, v287)` "int无法转换为Continuation";
5835/// uf5/l0 `throw 0;`; wcdb RepairKit `setOnCancelListener(0)`). The
5836/// dex verifier guarantees a const-0 register reaching a reference-
5837/// typed slot holds null, so in those slots the rendering must be the
5838/// null literal.
5839///
5840/// Two rewrites, both gated on the slot's expected type being a
5841/// reference (descriptor formal types, field type, array component,
5842/// cast target, throw, method return):
5843///   1. a literal `0` in a ref slot → `null`. SKIPPED for call-arg
5844///      slots whose formal is exactly `java/lang/Object`: `f(0)` there
5845///      compiles TODAY (boxing, or an int-overload pick like
5846///      `String.valueOf(0)`), and `f(null)` could newly fail on
5847///      overload ambiguity (`valueOf(Object)` vs `valueOf(char[])`) —
5848///      a guaranteed-error slot becomes a compile, a compiling slot
5849///      must not become an error.
5850///   2. a proven null-sentinel LOCAL: an Int-typed var whose every
5851///      write is `= 0` or `= <another sentinel>` and whose every read
5852///      sits in a ref slot (or feeds another sentinel's write — a dead
5853///      chain) → `null` at each read. The same Object-formal skip
5854///      applies at arg slots. A read in ANY int/unknown context
5855///      (binop, cond test, receiver deref, selector, anon-ctor arg,
5856///      lambda capture…) disqualifies the var — register reuse across
5857///      generations keeps its int rendering.
5858///
5859/// Runs after fix_ref_null_assigns (which converts writes of REF-typed
5860/// vars) and before split_generations (sentinel writes must not mint
5861/// int generations). Writes of confirmed sentinels stay as harmless
5862/// dead int stores for drop_dead_locals.
5863pub fn fix_null_sentinels(body: &mut Stmt, vt: &VarTable, ret: &JavaType) {
5864    #[derive(Clone, Copy, PartialEq)]
5865    enum WK {
5866        Zero,
5867        Copy(u32),
5868        Other,
5869    }
5870    fn is_ref_jt(t: &JavaType) -> bool {
5871        matches!(t, JavaType::Object(_) | JavaType::Array(_))
5872    }
5873    fn cls_write(e: &Expr) -> WK {
5874        match e {
5875            Expr::Const(ConstVal::Int(0)) => WK::Zero,
5876            Expr::Local { var, .. } => WK::Copy(*var),
5877            _ => WK::Other,
5878        }
5879    }
5880    fn expr_writes(e: &Expr, writes: &mut jdc_core::FxHashMap<u32, Vec<WK>>) {
5881        visit_exprs(e, &mut |x| match x {
5882            Expr::Assign { target, op, value } => {
5883                if let Expr::Local { var, .. } = &**target {
5884                    let k = if matches!(op, AssignOp::Plain) {
5885                        cls_write(value)
5886                    } else {
5887                        WK::Other
5888                    };
5889                    writes.entry(*var).or_default().push(k);
5890                }
5891            }
5892            Expr::PreIncDec { e: inner, .. } | Expr::PostIncDec { e: inner, .. } => {
5893                if let Expr::Local { var, .. } = &**inner {
5894                    writes.entry(*var).or_default().push(WK::Other);
5895                }
5896            }
5897            _ => {}
5898        });
5899    }
5900    fn collect_writes(
5901        s: &Stmt,
5902        writes: &mut jdc_core::FxHashMap<u32, Vec<WK>>,
5903        has_zero: &mut bool,
5904    ) {
5905        fn note(e: &Expr, has_zero: &mut bool, writes: &mut jdc_core::FxHashMap<u32, Vec<WK>>) {
5906            visit_exprs(e, &mut |x| {
5907                if let Expr::Const(ConstVal::Int(0)) = x {
5908                    *has_zero = true;
5909                }
5910            });
5911            expr_writes(e, writes);
5912        }
5913        match s {
5914            Stmt::Block(v) => {
5915                for x in v {
5916                    collect_writes(x, writes, has_zero);
5917                }
5918            }
5919            Stmt::LocalDef { var, init, .. } => {
5920                if let Some(e) = init {
5921                    if matches!(e, Expr::Const(ConstVal::Int(0))) {
5922                        *has_zero = true;
5923                    }
5924                    writes.entry(*var).or_default().push(cls_write(e));
5925                }
5926            }
5927            Stmt::ExprStmt(e) => note(e, has_zero, writes),
5928            Stmt::Return(e) => {
5929                if let Some(x) = e {
5930                    note(x, has_zero, writes);
5931                }
5932            }
5933            Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
5934                note(e, has_zero, writes);
5935            }
5936            Stmt::TernaryValue { e } => note(e, has_zero, writes),
5937            Stmt::If { cond, then_stmt, else_stmt } => {
5938                note(cond, has_zero, writes);
5939                collect_writes(then_stmt, writes, has_zero);
5940                if let Some(b) = else_stmt {
5941                    collect_writes(b, writes, has_zero);
5942                }
5943            }
5944            Stmt::While { cond, body } | Stmt::DoWhile { body, cond } => {
5945                note(cond, has_zero, writes);
5946                collect_writes(body, writes, has_zero);
5947            }
5948            Stmt::For { init, cond, update, body } => {
5949                for x in init {
5950                    collect_writes(x, writes, has_zero);
5951                }
5952                if let Some(c) = cond {
5953                    note(c, has_zero, writes);
5954                }
5955                for u in update {
5956                    note(u, has_zero, writes);
5957                }
5958                collect_writes(body, writes, has_zero);
5959            }
5960            Stmt::ForEach { var, iterable, body, .. } => {
5961                writes.entry(*var).or_default().push(WK::Other);
5962                note(iterable, has_zero, writes);
5963                collect_writes(body, writes, has_zero);
5964            }
5965            Stmt::Switch { selector, cases, default, .. } => {
5966                note(selector, has_zero, writes);
5967                for c in cases {
5968                    if let Some(g) = &c.guard {
5969                        note(g, has_zero, writes);
5970                    }
5971                    for x in &c.body {
5972                        collect_writes(x, writes, has_zero);
5973                    }
5974                }
5975                if let Some(d) = default {
5976                    collect_writes(d, writes, has_zero);
5977                }
5978            }
5979            Stmt::Try { body, catches, finally } => {
5980                collect_writes(body, writes, has_zero);
5981                for c in catches {
5982                    writes.entry(c.var).or_default().push(WK::Other);
5983                    collect_writes(&c.body, writes, has_zero);
5984                }
5985                if let Some(f) = finally {
5986                    collect_writes(f, writes, has_zero);
5987                }
5988            }
5989            Stmt::TryWithResources { resources, body, catches, finally, .. } => {
5990                for r in resources {
5991                    collect_writes(r, writes, has_zero);
5992                }
5993                collect_writes(body, writes, has_zero);
5994                for c in catches {
5995                    writes.entry(c.var).or_default().push(WK::Other);
5996                    collect_writes(&c.body, writes, has_zero);
5997                }
5998                if let Some(f) = finally {
5999                    collect_writes(f, writes, has_zero);
6000                }
6001            }
6002            Stmt::Assert { cond, msg } => {
6003                note(cond, has_zero, writes);
6004                if let Some(m) = msg {
6005                    note(m, has_zero, writes);
6006                }
6007            }
6008            Stmt::Synchronized { lock, body } => {
6009                note(lock, has_zero, writes);
6010                collect_writes(body, writes, has_zero);
6011            }
6012            Stmt::Labeled { body, .. } => collect_writes(body, writes, has_zero),
6013            _ => {}
6014        }
6015    }
6016
6017    let mut writes: jdc_core::FxHashMap<u32, Vec<WK>> =
6018        jdc_core::FxHashMap::default();
6019    let mut has_zero = false;
6020    collect_writes(body, &mut writes, &mut has_zero);
6021
6022    // Write-proven eligibility fixpoint (copy chains: `v = u`).
6023    let mut eligible: HashSet<u32> = HashSet::default();
6024    let int_typed = |v: u32| {
6025        (v as usize) < vt.vars.len() && vt.vars[v as usize].ty.erased() == JavaType::Int
6026    };
6027    loop {
6028        let mut changed = false;
6029        for (&v, ws) in writes.iter() {
6030            if eligible.contains(&v) || ws.is_empty() || !int_typed(v) {
6031                continue;
6032            }
6033            if ws.iter().all(|k| match k {
6034                WK::Zero => true,
6035                WK::Copy(u) => *u != v && eligible.contains(u),
6036                WK::Other => false,
6037            }) {
6038                eligible.insert(v);
6039                changed = true;
6040            }
6041        }
6042        if !changed {
6043            break;
6044        }
6045    }
6046    if eligible.is_empty() && !has_zero {
6047        return; // nothing this pass can do
6048    }
6049
6050    #[derive(Clone, Copy, PartialEq)]
6051    enum Mode {
6052        Scan,
6053        Rewrite,
6054    }
6055    struct St<'a> {
6056        mode: Mode,
6057        vt: &'a VarTable,
6058        eligible: &'a HashSet<u32>,
6059        ref_read: HashSet<u32>,
6060        dead: HashSet<u32>,
6061        sentinels: HashSet<u32>,
6062        n_lit: usize,
6063        n_var: usize,
6064    }
6065    // A call-arg slot whose formal is exactly java/lang/Object keeps
6066    // its current rendering (see the fn doc — boxing compiles today,
6067    // null could newly ambiguize).
6068    fn arg_ctx(t: &JavaType) -> Option<JavaType> {
6069        match t {
6070            JavaType::Object(n) if n.as_ref() == "java/lang/Object" => None,
6071            t if is_ref_jt(t) => Some(t.clone()),
6072            _ => None,
6073        }
6074    }
6075    fn walk_expr(e: &mut Expr, ctx: Option<JavaType>, st: &mut St) {
6076        walk_expr_c(e, ctx, false, st);
6077    }
6078    /// `cast_arg`: the slot is a call/ctor ARGUMENT — a rewritten null
6079    /// gets an explicit formal-type cast. Bare `f(null)` re-triggers
6080    /// overload ambiguity where `f(0)` used to pick an int overload
6081    /// (`in(null)` "引用不明确", `ContentValues.put(String,null)` vs its
6082    /// 9 overloads); `(String) null` is unambiguous and jadx-shaped.
6083    fn walk_expr_c(e: &mut Expr, ctx: Option<JavaType>, cast_arg: bool, st: &mut St) {
6084        let slot_ref = ctx.as_ref().is_some_and(is_ref_jt);
6085        fn mk_null(ctx: &Option<JavaType>, cast_arg: bool) -> Expr {
6086            match (cast_arg, ctx) {
6087                (true, Some(t)) => Expr::Cast {
6088                    ty: TypeRef::J(t.clone()),
6089                    e: Box::new(Expr::Const(ConstVal::Null)),
6090                },
6091                _ => Expr::Const(ConstVal::Null),
6092            }
6093        }
6094        match e {
6095            Expr::Const(ConstVal::Int(0)) if slot_ref => {
6096                if st.mode == Mode::Rewrite {
6097                    *e = mk_null(&ctx, cast_arg);
6098                    st.n_lit += 1;
6099                }
6100                return;
6101            }
6102            Expr::Local { var, .. } => {
6103                if st.eligible.contains(var) {
6104                    if slot_ref {
6105                        st.ref_read.insert(*var);
6106                    } else {
6107                        st.dead.insert(*var);
6108                    }
6109                }
6110                if st.mode == Mode::Rewrite && slot_ref && st.sentinels.contains(var) {
6111                    *e = mk_null(&ctx, cast_arg);
6112                    st.n_var += 1;
6113                }
6114                return;
6115            }
6116            _ => {}
6117        }
6118        match e {
6119            Expr::Method { owner, desc, args, .. } => {
6120                if let Some(o) = owner {
6121                    walk_expr(o, None, st);
6122                }
6123                for (i, a) in args.iter_mut().enumerate() {
6124                    let c = desc.args.get(i).and_then(arg_ctx);
6125                    walk_expr_c(a, c, true, st);
6126                }
6127            }
6128            Expr::Invokedynamic { desc, args, .. } => {
6129                for (i, a) in args.iter_mut().enumerate() {
6130                    let c = desc.args.get(i).and_then(arg_ctx);
6131                    walk_expr_c(a, c, true, st);
6132                }
6133            }
6134            Expr::New { args, arg_tys, .. } => {
6135                for (i, a) in args.iter_mut().enumerate() {
6136                    let c = arg_tys.get(i).and_then(arg_ctx);
6137                    walk_expr_c(a, c, true, st);
6138                }
6139            }
6140            Expr::AnonNew { args, .. } => {
6141                for a in args.iter_mut() {
6142                    walk_expr(a, None, st);
6143                }
6144            }
6145            Expr::Assign { target, value, .. } => {
6146                // Sentinel-to-sentinel copy: the whole store is dead;
6147                // its RHS is by the write gate a const-0 or a bare
6148                // local, so no nested reads can hide in it.
6149                if let Expr::Local { var: u, .. } = &**target {
6150                    if st.eligible.contains(u) {
6151                        return;
6152                    }
6153                }
6154                let vctx = match &**target {
6155                    Expr::Field { ty, .. } => {
6156                        let t = ty.erased();
6157                        is_ref_jt(&t).then_some(t)
6158                    }
6159                    Expr::ArrayIndex { array, .. } => match array.type_ref().erased() {
6160                        JavaType::Array(inner) if is_ref_jt(&inner) => Some(*inner),
6161                        _ => None,
6162                    },
6163                    _ => None,
6164                };
6165                walk_expr(target, None, st);
6166                walk_expr(value, vctx, st);
6167            }
6168            Expr::Cast { ty, e: inner } => {
6169                let t = ty.erased();
6170                let c = is_ref_jt(&t).then_some(t);
6171                walk_expr(inner, c, st);
6172            }
6173            Expr::InstanceOf { e: inner, .. } => {
6174                walk_expr(inner, Some(JavaType::Object("java/lang/Object".into())), st);
6175            }
6176            Expr::Cond { c, t, f } => {
6177                walk_expr(c, None, st);
6178                let tc = ctx.clone();
6179                walk_expr(t, tc, st);
6180                walk_expr(f, ctx, st);
6181            }
6182            Expr::NewArray { elem, dims, init, .. } => {
6183                for d in dims.iter_mut() {
6184                    walk_expr(d, None, st);
6185                }
6186                if let Some(list) = init {
6187                    let et = elem.erased();
6188                    let c = is_ref_jt(&et).then_some(et);
6189                    for slot in list.iter_mut() {
6190                        walk_expr(slot, c.clone(), st);
6191                    }
6192                }
6193            }
6194            Expr::NewMultiArray { dims, .. } => {
6195                for d in dims.iter_mut() {
6196                    walk_expr(d, None, st);
6197                }
6198            }
6199            Expr::ArrayIndex { array, index } => {
6200                walk_expr(array, None, st);
6201                walk_expr(index, None, st);
6202            }
6203            Expr::Field { owner, .. } => {
6204                if let Some(o) = owner {
6205                    walk_expr(o, None, st);
6206                }
6207            }
6208            Expr::Bin { l, r, .. } => {
6209                walk_expr(l, None, st);
6210                walk_expr(r, None, st);
6211            }
6212            Expr::Un { e: inner, .. } => walk_expr(inner, None, st),
6213            Expr::PreIncDec { e: inner, .. } | Expr::PostIncDec { e: inner, .. } => {
6214                walk_expr(inner, None, st);
6215            }
6216            Expr::StringConcat(parts) => {
6217                for p in parts.iter_mut() {
6218                    if let ConcatPart::Str(x) = p {
6219                        walk_expr(x, None, st);
6220                    }
6221                }
6222            }
6223            Expr::Lambda(l) => {
6224                for c in l.captures.iter_mut() {
6225                    walk_expr(c, None, st);
6226                }
6227            }
6228            _ => {}
6229        }
6230    }
6231    fn walk_stmt(s: &mut Stmt, st: &mut St, ret: &JavaType) {
6232        match s {
6233            Stmt::Block(v) => {
6234                for x in v.iter_mut() {
6235                    walk_stmt(x, st, ret);
6236                }
6237            }
6238            Stmt::ExprStmt(e) => walk_expr(e, None, st),
6239            Stmt::LocalDef { var, init, .. } => {
6240                if let Some(e) = init {
6241                    let ctx = if (*var as usize) < st.vt.vars.len() {
6242                        let t = st.vt.vars[*var as usize].ty.erased();
6243                        is_ref_jt(&t).then_some(t)
6244                    } else {
6245                        None
6246                    };
6247                    walk_expr(e, ctx, st);
6248                }
6249            }
6250            Stmt::Return(e) => {
6251                if let Some(x) = e {
6252                    let c = is_ref_jt(ret).then(|| ret.clone());
6253                    walk_expr(x, c, st);
6254                }
6255            }
6256            Stmt::Throw(e) => {
6257                walk_expr(e, Some(JavaType::Object("java/lang/Throwable".into())), st);
6258            }
6259            Stmt::If { cond, then_stmt, else_stmt } => {
6260                walk_expr(cond, None, st);
6261                walk_stmt(then_stmt, st, ret);
6262                if let Some(b) = else_stmt {
6263                    walk_stmt(b, st, ret);
6264                }
6265            }
6266            Stmt::While { cond, body } => {
6267                walk_expr(cond, None, st);
6268                walk_stmt(body, st, ret);
6269            }
6270            Stmt::DoWhile { body, cond } => {
6271                walk_stmt(body, st, ret);
6272                walk_expr(cond, None, st);
6273            }
6274            Stmt::For { init, cond, update, body } => {
6275                for x in init.iter_mut() {
6276                    walk_stmt(x, st, ret);
6277                }
6278                if let Some(c) = cond {
6279                    walk_expr(c, None, st);
6280                }
6281                for u in update.iter_mut() {
6282                    walk_expr(u, None, st);
6283                }
6284                walk_stmt(body, st, ret);
6285            }
6286            Stmt::ForEach { iterable, body, .. } => {
6287                walk_expr(iterable, None, st);
6288                walk_stmt(body, st, ret);
6289            }
6290            Stmt::Switch { selector, cases, default, .. } => {
6291                walk_expr(selector, None, st);
6292                for c in cases.iter_mut() {
6293                    if let Some(g) = &mut c.guard {
6294                        walk_expr(g, None, st);
6295                    }
6296                    for x in c.body.iter_mut() {
6297                        walk_stmt(x, st, ret);
6298                    }
6299                }
6300                if let Some(d) = default {
6301                    walk_stmt(d, st, ret);
6302                }
6303            }
6304            Stmt::Try { body, catches, finally } => {
6305                walk_stmt(body, st, ret);
6306                for c in catches.iter_mut() {
6307                    walk_stmt(&mut c.body, st, ret);
6308                }
6309                if let Some(f) = finally {
6310                    walk_stmt(f, st, ret);
6311                }
6312            }
6313            Stmt::TryWithResources { resources, body, catches, finally, .. } => {
6314                for r in resources.iter_mut() {
6315                    walk_stmt(r, st, ret);
6316                }
6317                walk_stmt(body, st, ret);
6318                for c in catches.iter_mut() {
6319                    walk_stmt(&mut c.body, st, ret);
6320                }
6321                if let Some(f) = finally {
6322                    walk_stmt(f, st, ret);
6323                }
6324            }
6325            Stmt::Assert { cond, msg } => {
6326                walk_expr(cond, None, st);
6327                if let Some(m) = msg {
6328                    walk_expr(m, None, st);
6329                }
6330            }
6331            Stmt::Synchronized { lock, body } => {
6332                walk_expr(lock, Some(JavaType::Object("java/lang/Object".into())), st);
6333                walk_stmt(body, st, ret);
6334            }
6335            Stmt::TernaryValue { e } => walk_expr(e, None, st),
6336            Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
6337                walk_expr(e, Some(JavaType::Object("java/lang/Object".into())), st);
6338            }
6339            Stmt::Labeled { body, .. } => walk_stmt(body, st, ret),
6340            _ => {}
6341        }
6342    }
6343
6344    let mut st = St {
6345        mode: Mode::Scan,
6346        vt,
6347        eligible: &eligible,
6348        ref_read: HashSet::default(),
6349        dead: HashSet::default(),
6350        sentinels: HashSet::default(),
6351        n_lit: 0,
6352        n_var: 0,
6353    };
6354    if !eligible.is_empty() {
6355        walk_stmt(body, &mut st, ret);
6356        st.sentinels = eligible
6357            .iter()
6358            .copied()
6359            .filter(|v| st.ref_read.contains(v) && !st.dead.contains(v))
6360            .collect();
6361    }
6362    if st.sentinels.is_empty() && !has_zero {
6363        return;
6364    }
6365    st.mode = Mode::Rewrite;
6366    walk_stmt(body, &mut st, ret);
6367    if std::env::var_os("DDC_SENT").is_some() {
6368        eprintln!(
6369            "[sent] elig={} confirmed={} lit={} var={}",
6370            eligible.len(),
6371            st.sentinels.len(),
6372            st.n_lit,
6373            st.n_var
6374        );
6375    }
6376}
6377
6378/// Int-context operand bridge: a BOOLEAN-typed side of an int-kind Bin
6379/// (bitwise, arithmetic, comparison against a numeric) becomes
6380/// `(b ? 1 : 0)` — the dex-level truth (booleans ARE 0/1 ints there;
6381/// register reuse puts a bool-typed var into an int expression:
6382/// `v5x | 4`, `b == 0` — "boolean无法转换为int" / "二元运算符操作数
6383/// 类型错误" families). The XOR shape conversion this used to depend on
6384/// (fix_bool_xor) is retired — cure's xor/bool family handles it at the
6385/// source level; boolean-SINK chains are all-boolean here already.
6386pub fn fix_int_operand_bridges(body: &mut Stmt, vt: &VarTable) {
6387    fn side_bool(e: &Expr, vt: &VarTable) -> bool {
6388        match e {
6389            Expr::Local { var, .. } => {
6390                matches!(vt.var(*var).ty.erased(), JavaType::Boolean)
6391            }
6392            // An all-boolean bitwise bin IS boolean even when its
6393            // embedded ty is still the frozen Int of the or-int lift
6394            // (booleanize retypes the vt; mixed_rewrite sets bin ty
6395            // only on the nodes it rewrites). The flat lookup called
6396            // `(ci13|ci14)` an int side, so the sibling `v408 != 0`
6397            // got `? 1 : 0`-wrapped while the bool bin stayed bare —
6398            // rendering int | boolean inside the wrap (news compose
6399            // CoreTextFieldKt `((v408!=0 ?1:0) | (ci13|ci14) ?1:0)|v412`
6400            // ×349 residual). Full bool-bin recursion (the retired
6401            // fix_bool_xor's bty shape, kept here).
6402            Expr::Bin {
6403                op: BinOp::And | BinOp::Or | BinOp::Xor,
6404                l,
6405                r,
6406                ..
6407            } => side_bool(l, vt) && side_bool(r, vt),
6408            other => matches!(other.type_ref().erased(), JavaType::Boolean),
6409        }
6410    }
6411    fn side_int(e: &Expr, vt: &VarTable) -> bool {
6412        // A boolean-composed bin keeps its frozen Int embedded ty —
6413        // without this guard it double-answers as the "int side" of
6414        // its own bridge arm and steers the wrap onto the wrong operand.
6415        if side_bool(e, vt) {
6416            return false;
6417        }
6418        matches!(
6419            match e {
6420                Expr::Local { var, .. } => vt.var(*var).ty.erased(),
6421                other => other.type_ref().erased(),
6422            },
6423            JavaType::Int | JavaType::Short | JavaType::Byte | JavaType::Char | JavaType::Long
6424        )
6425    }
6426    fn wrap(e: Box<Expr>) -> Box<Expr> {
6427        Box::new(Expr::Cond {
6428            c: e,
6429            t: Box::new(Expr::Const(ConstVal::Int(1))),
6430            f: Box::new(Expr::Const(ConstVal::Int(0))),
6431        })
6432    }
6433    walk_stmt_exprs(body, &mut |e| {
6434        deep_rewrite(e, &mut |x| {
6435            // Field / array-store positions: the target's type is
6436            // descriptor-backed (authoritative); a boolean value into
6437            // an int slot bridges `(b ? 1 : 0)` (with a narrowing cast
6438            // for byte/short/char slots), an int value into a boolean
6439            // slot bridges `v != 0` (weixin `this.L[v3x] = max3`,
6440            // `g2x.d = p1x` — 785 field + ~700 array lines).
6441            if let Expr::Assign { target, value, op: AssignOp::Plain } = x {
6442                let tgt_ty: Option<JavaType> = match &**target {
6443                    Expr::Field { ty, .. } => Some(ty.erased()),
6444                    Expr::ArrayIndex { array, .. } => {
6445                        match array.type_ref().erased() {
6446                            JavaType::Array(el) => Some(el.as_ref().clone()),
6447                            _ => None,
6448                        }
6449                    }
6450                    // Local targets: the split/booleanize machinery owns
6451                    // the mixed-kind register cases; the residue that
6452                    // reached render (boolean无法转换为byte/long, ~360
6453                    // lines) gets the same value-side bridge.
6454                    Expr::Local { var, .. } => {
6455                        if (*var as usize) < vt.vars.len() {
6456                            Some(vt.var(*var).ty.erased())
6457                        } else {
6458                            None
6459                        }
6460                    }
6461                    _ => None,
6462                };
6463                if let Some(t) = tgt_ty {
6464                    let tgt_int = matches!(
6465                        t,
6466                        JavaType::Int
6467                            | JavaType::Short
6468                            | JavaType::Byte
6469                            | JavaType::Char
6470                            | JavaType::Long
6471                    );
6472                    if tgt_int && side_bool(value, vt) && !side_int(value, vt) {
6473                        let taken = std::mem::replace(
6474                            value,
6475                            Box::new(Expr::Const(ConstVal::Null)),
6476                        );
6477                        let mut wrapped = wrap(taken);
6478                        if !matches!(t, JavaType::Int | JavaType::Long) {
6479                            wrapped = Box::new(Expr::Cast {
6480                                ty: TypeRef::J(t.clone()),
6481                                e: wrapped,
6482                            });
6483                        }
6484                        *value = wrapped;
6485                    } else if matches!(t, JavaType::Boolean)
6486                        && side_int(value, vt)
6487                        && !side_bool(value, vt)
6488                    {
6489                        let taken = std::mem::replace(
6490                            value,
6491                            Box::new(Expr::Const(ConstVal::Null)),
6492                        );
6493                        **value = Expr::Bin {
6494                            op: BinOp::Ne,
6495                            l: taken,
6496                            r: Box::new(Expr::Const(ConstVal::Int(0))),
6497                            ty: Some(TypeRef::J(JavaType::Boolean)),
6498                        };
6499                    }
6500                }
6501            }
6502            // Explicit casts across the bool/num line are invalid Java
6503            // in BOTH directions: `(byte) this.r` (dex int-to-byte over
6504            // a boolean-rendered field — weixin AccInfo writeByte ×207)
6505            // becomes `(byte)(r ? 1 : 0)`, and `(boolean) x` over an
6506            // int becomes `x != 0`.
6507            if let Expr::Cast { ty, e } = x {
6508                let t = ty.erased();
6509                let t_num = matches!(
6510                    t,
6511                    JavaType::Int
6512                        | JavaType::Short
6513                        | JavaType::Byte
6514                        | JavaType::Char
6515                        | JavaType::Long
6516                        | JavaType::Float
6517                        | JavaType::Double
6518                );
6519                if t_num && side_bool(e, vt) && !side_int(e, vt) {
6520                    let taken = std::mem::replace(e, Box::new(Expr::Const(ConstVal::Null)));
6521                    **e = Expr::Cond {
6522                        c: taken,
6523                        t: Box::new(Expr::Const(ConstVal::Int(1))),
6524                        f: Box::new(Expr::Const(ConstVal::Int(0))),
6525                    };
6526                } else if matches!(t, JavaType::Boolean)
6527                    && side_int(e, vt)
6528                    && !side_bool(e, vt)
6529                {
6530                    let taken = std::mem::replace(e, Box::new(Expr::Const(ConstVal::Null)));
6531                    *x = Expr::Bin {
6532                        op: BinOp::Ne,
6533                        l: taken,
6534                        r: Box::new(Expr::Const(ConstVal::Int(0))),
6535                        ty: Some(TypeRef::J(JavaType::Boolean)),
6536                    };
6537                }
6538            }
6539            // Null-vs-int comparison: dex if-eqz/if-nez is type-agnostic
6540            // and null IS zero — when the local came out int-typed (phi
6541            // confluence residue), rewrite the Null constant to 0
6542            // (`v95_g12 != null` → `v95_g12 != 0`, faithful and legal;
6543            // yq0/g1 546-line 二元运算符 family). BOOLEAN side: the same
6544            // zero-register compare reads as the truth test — `b != null`
6545            // IS `b`, `b == null` IS `!b` (alipay v87_g7_g2 != null,
6546            // 二元运算符 '!=' boolean vs <空值> ×43 / news ×30).
6547            if let Expr::Bin { op, l, r, .. } = x {
6548                if matches!(op, BinOp::Eq | BinOp::Ne) {
6549                    if matches!(&**r, Expr::Const(ConstVal::Null))
6550                        && side_int(l, vt)
6551                        && !side_bool(l, vt)
6552                    {
6553                        **r = Expr::Const(ConstVal::Int(0));
6554                    } else if matches!(&**l, Expr::Const(ConstVal::Null))
6555                        && side_int(r, vt)
6556                        && !side_bool(r, vt)
6557                    {
6558                        **l = Expr::Const(ConstVal::Int(0));
6559                    } else if matches!(&**r, Expr::Const(ConstVal::Null))
6560                        && side_bool(l, vt)
6561                    {
6562                        let taken =
6563                            std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
6564                        *x = if matches!(op, BinOp::Eq) {
6565                            Expr::Un { op: UnOp::Not, e: taken }
6566                        } else {
6567                            *taken
6568                        };
6569                    } else if matches!(&**l, Expr::Const(ConstVal::Null))
6570                        && side_bool(r, vt)
6571                    {
6572                        let taken =
6573                            std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
6574                        *x = if matches!(op, BinOp::Eq) {
6575                            Expr::Un { op: UnOp::Not, e: taken }
6576                        } else {
6577                            *taken
6578                        };
6579                    }
6580                }
6581            }
6582            // A boolean-typed array INDEX is a reused register holding
6583            // an int (`this.L[v3x]` — boolean无法转换为int at the index
6584            // position): bridge it in reads and writes alike.
6585            if let Expr::ArrayIndex { index, .. } = x {
6586                if side_bool(index, vt) && !side_int(index, vt) {
6587                    let taken =
6588                        std::mem::replace(index, Box::new(Expr::Const(ConstVal::Null)));
6589                    *index = wrap(taken);
6590                }
6591            }
6592            // A boolean-typed array DIMENSION is the same reused-register
6593            // case at the allocation site (`new Object[v200]` where v200
6594            // is boolean-typed — weixin AppBrandRuntime, bool→int family):
6595            // dex new-array reads the size register as int and booleans
6596            // ARE 0/1 ints there, so bridge `(b ? 1 : 0)`.
6597            if let Expr::NewArray { dims, .. } = x {
6598                for d in dims.iter_mut() {
6599                    if side_bool(d, vt) && !side_int(d, vt) {
6600                        let taken =
6601                            std::mem::replace(d, Expr::Const(ConstVal::Null));
6602                        *d = *wrap(Box::new(taken));
6603                    }
6604                }
6605            }
6606            if let Expr::Bin { op, l, r, .. } = x {
6607                let int_kind = matches!(
6608                    op,
6609                    BinOp::Or
6610                        | BinOp::And
6611                        | BinOp::Xor
6612                        | BinOp::Add
6613                        | BinOp::Sub
6614                        | BinOp::Mul
6615                        | BinOp::Div
6616                        | BinOp::Rem
6617                        | BinOp::Shl
6618                        | BinOp::Shr
6619                        | BinOp::Ushr
6620                        | BinOp::Eq
6621                        | BinOp::Ne
6622                        | BinOp::Lt
6623                        | BinOp::Ge
6624                        | BinOp::Gt
6625                        | BinOp::Le
6626                );
6627                if !int_kind {
6628                    return;
6629                }
6630                // A Null CONSTANT operand of an int-kind bitwise/arith
6631                // bin is the dex zero register (or-int reads 0 out of a
6632                // register whose value view was null — `b | null`
6633                // renders boolean | <空值>, alipay ×43 / news ×30).
6634                // Rewriting to Int(0) is register-faithful and lets the
6635                // bool-side wraps below take over (`(b ? 1 : 0) | 0`).
6636                // Only when the SIBLING is a bool/int side: a
6637                // reference-typed sibling keeps the null shape (that is
6638                // SSA-merge residue, not a zero-register read), and a
6639                // String sibling keeps `+ null` concatenation ("null" —
6640                // valid and faithful; side_str excludes it via the
6641                // side_bool/side_int guard).
6642                if matches!(
6643                    op,
6644                    BinOp::Or
6645                        | BinOp::And
6646                        | BinOp::Xor
6647                        | BinOp::Add
6648                        | BinOp::Sub
6649                        | BinOp::Mul
6650                        | BinOp::Div
6651                        | BinOp::Rem
6652                        | BinOp::Shl
6653                        | BinOp::Shr
6654                        | BinOp::Ushr
6655                ) {
6656                    if matches!(&**r, Expr::Const(ConstVal::Null))
6657                        && (side_bool(l, vt) || side_int(l, vt))
6658                    {
6659                        **r = Expr::Const(ConstVal::Int(0));
6660                    } else if matches!(&**l, Expr::Const(ConstVal::Null))
6661                        && (side_bool(r, vt) || side_int(r, vt))
6662                    {
6663                        **l = Expr::Const(ConstVal::Int(0));
6664                    }
6665                }
6666                // PURE arithmetic/shift and ORDERING operands never take
6667                // a boolean side in ANY combination (`boolean + boolean`
6668                // is as invalid as `boolean + int` — reused 0/1 registers
6669                // in `p4x + v20 + (v21?1:0)`, uuyc u0/u02; `boolean <
6670                // boolean` likewise) — bridge each bool side on its own.
6671                // String concatenation is exempt: `"s" + b` is valid Java
6672                // and wrapping would print 1/0 instead of true/false.
6673                // Equality stays on the mixed-side rule below (a
6674                // bool == bool compare IS valid).
6675                let arith = matches!(
6676                    op,
6677                    BinOp::Add
6678                        | BinOp::Sub
6679                        | BinOp::Mul
6680                        | BinOp::Div
6681                        | BinOp::Rem
6682                        | BinOp::Shl
6683                        | BinOp::Shr
6684                        | BinOp::Ushr
6685                );
6686                let ordered = matches!(op, BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le);
6687                if arith || ordered {
6688                    let side_str = |e: &Expr| {
6689                        matches!(
6690                            match e {
6691                                Expr::Local { var, .. } => vt.var(*var).ty.erased(),
6692                                other => other.type_ref().erased(),
6693                            },
6694                            JavaType::Object(ref s) if s.as_ref() == "java/lang/String"
6695                        )
6696                    };
6697                    let concat = arith && (side_str(l) || side_str(r));
6698                    if !concat {
6699                        if side_bool(l, vt) {
6700                            let taken =
6701                                std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
6702                            *l = wrap(taken);
6703                        }
6704                        if side_bool(r, vt) {
6705                            let taken =
6706                                std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
6707                            *r = wrap(taken);
6708                        }
6709                    }
6710                    return;
6711                }
6712                // Comparisons only bridge against a NUMERIC constant or
6713                // int-typed side (a bool == bool compare is valid Java).
6714                if side_bool(l, vt) && side_int(r, vt) && !side_bool(r, vt) {
6715                    let taken = std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
6716                    *l = wrap(taken);
6717                } else if side_bool(r, vt) && side_int(l, vt) && !side_bool(l, vt) {
6718                    let taken = std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
6719                    *r = wrap(taken);
6720                }
6721            }
6722        });
6723    });
6724}
6725
6726/// Statement-level idiom restoration (readability parity with
6727/// jadx/DAD, flagged by the androguard comparison): `v = v + 1` →
6728/// `v++`, `v = v op x` → `v op= x`. Local targets only — a compound
6729/// field/array target would change how often the receiver expression
6730/// evaluates. Var must be the LEFT operand (non-commutative ops;
6731/// string-concat order). The ±1 fold requires a numeric vt type
6732/// (`s++` on a String is invalid while `s += 1` is not).
6733/// Boolean VALUE-diamond fold (DAD short_circuit value forms):
6734/// ```text
6735///   v = false; if (c) { v = true; }   →  v = c
6736///   v = true;  if (c) { v = false; }  →  v = !c
6737///   v = false; if (c) { v = E; }      →  v = c && E     (E bool-valued)
6738///   v = true;  if (c) { v = E; }      →  v = !c || E
6739///   v = D; if (c) { v = E1; } else { v = E2; }  →  v = c ? E1 : E2
6740/// ```
6741/// `v` must be Boolean-typed; the arm value bool-valued; the guard may
6742/// not touch `v`; base and arm constants must not coincide (a no-op
6743/// diamond could drop guard side effects). Kotlin default-arg bridge
6744/// ctors compute defaulted booleans in exactly this shape INSIDE the
6745/// branch ahead of the delegation (lark MmCreateAudioRequest's v21x
6746/// chain) — split_branch rejects the control flow as Dirty and the
6747/// this() call stays in the branch ("对this的调用必须是构造器中的第一
6748/// 个语句", lark ×456). Folded to a linear def (interleaved with
6749/// forward_single_use for the `[u = call; v = u]` arm shape) the
6750/// merge_at ternary fold applies. Source-faithful: the original was
6751/// `x?.m() ?: false` — one expression; the inlined call stays under
6752/// the guard arm, preserving evaluation order and short-circuiting.
6753/// General value-diamond fold (fold_bool_value_diamonds for ANY type):
6754/// `if (c) { v = a; } else { v = b; }` → `v = c ? a : b;`, plus the
6755/// split-pair shape `v = b; if (c) { v = a; }` → the same ternary. The
6756/// Kotlin default-arg ctor's mask-guarded per-arg defaults arrive as
6757/// diamonds (DocFreshInfo v10x/v14x); folding gives each carrier a
6758/// SINGLE-assignment def so the chain-aware delegation inline can hoist
6759/// the this() first ("对this的调用必须是构造器中的第一个语句"). Fold
6760/// only when both sides are exactly one plain assignment to the SAME
6761/// local and the value types are ternary-compatible (equal erased or
6762/// both numeric — mixed-category ternaries box/surprise).
6763pub fn fold_value_diamonds(body: &mut Stmt, vt: &VarTable) {
6764    let Stmt::Block(stmts) = body else { return };
6765    fold_diamonds_in(stmts, vt);
6766}
6767
6768fn diamond_types_ok(a: &Expr, b: &Expr) -> bool {
6769    fn num(t: &JavaType) -> bool {
6770        matches!(
6771            t,
6772            JavaType::Byte
6773                | JavaType::Short
6774                | JavaType::Int
6775                | JavaType::Char
6776                | JavaType::Long
6777                | JavaType::Float
6778                | JavaType::Double
6779        )
6780    }
6781    // A null branch is compatible with any REFERENCE other branch: the
6782    // ternary types as lub(T, null) = T. Kotlin default-args ctors
6783    // render null defaults (`if ((mask&2)==0) v=x; else v=null;`) —
6784    // rejecting the pair left the diamond unfolded, the defs
6785    // un-inlineable, and the this(...) delegation unhoistable (weibo
6786    // ctor-not-first robust-guard/second-delegation ×818). A primitive
6787    // sibling stays rejected (`c ? 5 : null` is not Java).
6788    if let Some(other) = if matches!(a, Expr::Const(ConstVal::Null)) {
6789        Some(b)
6790    } else if matches!(b, Expr::Const(ConstVal::Null)) {
6791        Some(a)
6792    } else {
6793        None
6794    } {
6795        return !num(&other.type_ref().erased());
6796    }
6797    let ta = a.type_ref().erased();
6798    let tb = b.type_ref().erased();
6799    ta == tb || (num(&ta) && num(&tb))
6800}
6801
6802/// `(var, target clone, value)` when the stmt is exactly ONE plain
6803/// assign to a local (bare or block-wrapped). The unwrap must be
6804/// RECURSIVE: the Kotlin default-args ctor diamonds arrive as
6805/// `Block([Block([Assign])])` (structurer nesting), and the one-level
6806/// peel missed them — the unfolded `if ((mask&2)==0) v=p; else v=0;`
6807/// prelude then blocked fix_ctor_super_first from hoisting the
6808/// `this(...)` delegation (weibo/lark ctor-not-first ×841 family).
6809fn single_local_assign(st: &Stmt) -> Option<(u32, Expr, Expr)> {
6810    let mut inner = st;
6811    while let Stmt::Block(v) = inner {
6812        if v.len() != 1 {
6813            return None;
6814        }
6815        inner = &v[0];
6816    }
6817    match inner {
6818        Stmt::ExprStmt(Expr::Assign { target, value, op: AssignOp::Plain }) => {
6819            match &**target {
6820                Expr::Local { var, .. } => Some((*var, *target.clone(), *value.clone())),
6821                _ => None,
6822            }
6823        }
6824        _ => None,
6825    }
6826}
6827
6828/// Like `single_local_assign`, but ALSO accepts an arm whose value is
6829/// produced by an arm-local def: `Block([LocalDef vX = init, v = vX])`
6830/// (possibly Block-wrapped) inlines `init` into the returned value when
6831/// `vX` is read exactly once there (single evaluation preserved). The
6832/// Kotlin mask-ctor long defaults render this shape (`else { long u =
6833/// Color.getUnspecified(); v37 = u; }` — compose TextStyle/SpanStyle);
6834/// without the inline the diamond stays unfolded and blocks the whole
6835/// delegation-inlining chain downstream.
6836fn arm_assign_inlined(st: &Stmt) -> Option<(u32, Expr, Expr)> {
6837    if let Some(r) = single_local_assign(st) {
6838        return Some(r);
6839    }
6840    let mut inner = st;
6841    while let Stmt::Block(v) = inner {
6842        if v.len() == 1 {
6843            inner = &v[0];
6844        } else {
6845            break;
6846        }
6847    }
6848    let Stmt::Block(v) = inner else { return None };
6849    if v.len() != 2 {
6850        return None;
6851    }
6852    let Stmt::LocalDef {
6853        var: x,
6854        init: Some(init),
6855        ..
6856    } = &v[0]
6857    else {
6858        return None;
6859    };
6860    let (var, tgt, val) = single_local_assign(&v[1])?;
6861    let mut count = 0usize;
6862    let mut c = val.clone();
6863    deep_rewrite(&mut c, &mut |e| {
6864        if let Expr::Local { var: lv, .. } = e {
6865            if lv == x {
6866                count += 1;
6867            }
6868        }
6869    });
6870    if count != 1 {
6871        return None;
6872    }
6873    let mut out = val.clone();
6874    deep_rewrite(&mut out, &mut |e| {
6875        if let Expr::Local { var: lv, .. } = e {
6876            if lv == x {
6877                *e = init.clone();
6878            }
6879        }
6880    });
6881    Some((var, tgt, out))
6882}
6883
6884fn mk_ternary_assign(tgt: Expr, c: Expr, a: Expr, b: Expr) -> Stmt {
6885    Stmt::ExprStmt(Expr::Assign {
6886        target: Box::new(tgt),
6887        op: AssignOp::Plain,
6888        value: Box::new(Expr::Cond {
6889            c: Box::new(c),
6890            t: Box::new(a),
6891            f: Box::new(b),
6892        }),
6893    })
6894}
6895
6896/// Two var ids that render as the SAME Java local: identical base name
6897/// (generation suffixes only appear on collisions, and same-base
6898/// generations of one dex register share the rendered name) and
6899/// identical erased type (a bool/num split pair must not merge).
6900fn same_render_var(v1: u32, v2: u32, vt: &VarTable) -> bool {
6901    let (a, b) = (vt.vars.get(v1 as usize), vt.vars.get(v2 as usize));
6902    match (a, b) {
6903        (Some(x), Some(y)) => x.name == y.name && x.ty.erased() == y.ty.erased(),
6904        _ => false,
6905    }
6906}
6907
6908/// Structural equality of two statement lists MODULO SSA generations:
6909/// each arm of a diamond writes its OWN generation of the rendered
6910/// locals, so strict `==` on the tails fails even when the rendered
6911/// code is identical. Canonicalize every local id to the first id seen
6912/// for its render key (name + erased type) under a SHARED map, then
6913/// compare structurally.
6914fn canon_stmts_eq(a: &[Stmt], b: &[Stmt], vt: &VarTable) -> bool {
6915    if a.len() != b.len() {
6916        return false;
6917    }
6918    let mut map: std::collections::HashMap<String, u32> = std::collections::HashMap::default();
6919    let canon = |s: &Stmt, map: &mut std::collections::HashMap<String, u32>| -> Stmt {
6920        let mut c = s.clone();
6921        walk_stmt_exprs(&mut c, &mut |e| {
6922            deep_rewrite(e, &mut |x| {
6923                if let Expr::Local { var, .. } = x {
6924                    let key = match vt.vars.get(*var as usize) {
6925                        Some(vi) => format!("{}#{:?}", vi.name, vi.ty.erased()),
6926                        None => format!("v{}", var),
6927                    };
6928                    let id = *map.entry(key).or_insert(*var);
6929                    *x = Expr::Local {
6930                        var: id,
6931                        ty: vt.var(id).ty.clone(),
6932                    };
6933                }
6934            });
6935        });
6936        c
6937    };
6938    a.iter()
6939        .zip(b.iter())
6940        .all(|(x, y)| canon(x, &mut map) == canon(y, &mut map))
6941}
6942
6943fn fold_diamonds_in(stmts: &mut Vec<Stmt>, vt: &VarTable) {
6944    let mut i = 0usize;
6945    while i < stmts.len() {
6946        match &mut stmts[i] {
6947            Stmt::Block(v) => fold_diamonds_in(v, vt),
6948            Stmt::If { then_stmt, else_stmt, .. } => {
6949                if let Stmt::Block(v) = &mut **then_stmt {
6950                    fold_diamonds_in(v, vt);
6951                }
6952                if let Some(e) = else_stmt {
6953                    if let Stmt::Block(v) = &mut **e {
6954                        fold_diamonds_in(v, vt);
6955                    }
6956                }
6957            }
6958            _ => {}
6959        }
6960        // Shape 1: standalone if/else diamond.
6961        let shape1 = if let Stmt::If { cond, then_stmt, else_stmt: Some(el), .. } = &stmts[i] {
6962            match (arm_assign_inlined(then_stmt), arm_assign_inlined(el)) {
6963                (Some((v1, tgt, a)), Some((v2, _, b)))
6964                    if v1 == v2 && diamond_types_ok(&a, &b) =>
6965                {
6966                    Some(mk_ternary_assign(tgt, cond.clone(), a, b))
6967                }
6968                _ => None,
6969            }
6970        } else {
6971            None
6972        };
6973        if let Some(f) = shape1 {
6974            stmts[i] = f;
6975            i += 1;
6976            continue;
6977        }
6978        // Shape 3: identical-delegation diamond — BOTH arms assign the
6979        // same rendered local, then run the same tail headed by a ctor
6980        // delegation (Kotlin mask-ctor last slot: `if ((m&M)==0) { v =
6981        // x; this(.., v, ..); return; } else { v = null; this(.., v,
6982        // ..); return; }` — alipay compose TextStyle/SpanStyle family).
6983        // Fold to `v = c ? x : null;` + the shared tail: the delegation
6984        // becomes unconditional (the hotfix-guard strip downstream needs
6985        // that) and fold_default_arg_bridge can then inline the prelude
6986        // carriers into the args. Tails compare modulo SSA generation
6987        // (each arm reads its own generation of the one rendered local).
6988        // Gates: tail headed by a bare delegation, tails canonically
6989        // identical, the tail writes NEITHER generation, types ok.
6990        let shape3 = if let Stmt::If {
6991            cond,
6992            then_stmt,
6993            else_stmt: Some(el),
6994            ..
6995        } = &stmts[i]
6996        {
6997            let arm = |st: &Stmt| -> Option<((u32, Expr, Expr), Vec<Stmt>)> {
6998                let Stmt::Block(v) = st else { return None };
6999                if v.len() < 2 {
7000                    return None;
7001                }
7002                let first = single_local_assign(&v[0])?;
7003                Some((first, v[1..].to_vec()))
7004            };
7005            match (arm(then_stmt), arm(el)) {
7006                (Some(((v1, tgt1, a), mut rest_t)), Some(((v2, _t2, b), rest_e)))
7007                    if !rest_t.is_empty()
7008                        && is_bare_ctor_call(first_leaf_stmt(&rest_t[0]))
7009                        && stmts_count_writes(&rest_t, v1) == 0
7010                        && stmts_count_writes(&rest_t, v2) == 0
7011                        && diamond_types_ok(&a, &b) =>
7012                {
7013                    if v1 == v2 || same_render_var(v1, v2, vt) {
7014                        // same rendered local: fold the assign, keep the tail
7015                        if canon_stmts_eq(&rest_t, &rest_e, vt) {
7016                            Some((mk_ternary_assign(tgt1, cond.clone(), a, b), rest_t))
7017                        } else {
7018                            None
7019                        }
7020                    } else if stmts_read_var(&rest_t, v1) == 1
7021                        && stmts_read_var(&rest_e, v2) == 1
7022                        && stmts_read_var(&rest_t, v2) == 0
7023                        && stmts_read_var(&rest_e, v1) == 0
7024                    {
7025                        // SPLIT arm-local carriers (compose Span: then
7026                        // reads v23_g3, else reads v23 — different render
7027                        // names, one read each): unify by substituting
7028                        // v2 -> v1 in the else tail and requiring EXACT
7029                        // equality, then inline `c ? A1 : A2` at the
7030                        // single v1 read of the kept tail. No ternary
7031                        // assign needed — both carriers die in the fold.
7032                        let mut unified = rest_e.clone();
7033                        for st in unified.iter_mut() {
7034                            let mut c2 = st.clone();
7035                            walk_stmt_exprs(&mut c2, &mut |e| {
7036                                deep_rewrite(e, &mut |x| {
7037                                    if let Expr::Local { var, .. } = x {
7038                                        if *var == v2 {
7039                                            *x = Expr::Local {
7040                                                var: v1,
7041                                                ty: vt.var(v1).ty.clone(),
7042                                            };
7043                                        }
7044                                    }
7045                                });
7046                            });
7047                            *st = c2;
7048                        }
7049                        if unified != rest_t {
7050                            None
7051                        } else {
7052                            let tern = Expr::Cond {
7053                                c: Box::new(cond.clone()),
7054                                t: Box::new(a.clone()),
7055                                f: Box::new(b.clone()),
7056                            };
7057                            for st in rest_t.iter_mut() {
7058                                let mut c2 = st.clone();
7059                                walk_stmt_exprs(&mut c2, &mut |e| {
7060                                    deep_rewrite(e, &mut |x| {
7061                                        if let Expr::Local { var, .. } = x {
7062                                            if *var == v1 {
7063                                                *x = tern.clone();
7064                                            }
7065                                        }
7066                                    });
7067                                });
7068                                *st = c2;
7069                            }
7070                            // the If is REPLACED by the (rewritten) tail:
7071                            // no ternary assign survives — both carriers
7072                            // die in the fold.
7073                            let first = rest_t.remove(0);
7074                            Some((first, rest_t))
7075                        }
7076                    } else {
7077                        None
7078                    }
7079                }
7080                _ => None,
7081            }
7082        } else {
7083            None
7084        };
7085        if let Some((folded, rest)) = shape3 {
7086            stmts[i] = folded;
7087            stmts.splice(i + 1..i + 1, rest);
7088            i += 1;
7089            continue;
7090        }
7091        // Shape 2: adjacent `v = b;` + `if (c) { v = a; }` (no else).
7092        // The two assigns may target DIFFERENT SSA generations of the
7093        // same rendered local (sequential conditional overwrite — the
7094        // Kotlin mask-ctor `v11x = p3x; if ((mask&4)!=0) v11x = -1L;`
7095        // prelude, lark/weixin branched-defs ctor family): same base
7096        // name + same erased type renders as one Java local, so the
7097        // fold's else arm reads generation 1 and the assign targets
7098        // generation 2 — textually `v = c ? a : v`, exactly the
7099        // observable behavior of the unfolded pair.
7100        if i + 1 < stmts.len() {
7101            // An EMPTY else block counts as no-else: the structurer
7102            // emits Some(Block([])) where the source shape had none,
7103            // and cleanup (which would drop it) runs AFTER the fold —
7104            // the strict None match missed every real-world overwrite
7105            // (zero shape-2 folds on lark mask ctors).
7106            // Shape 2: adjacent `v = b;` + conditional overwrite with
7107            // NO other effect:
7108            //   form A: `if (c) { v = a; }` (else absent/empty)  -> v = c ? a : b
7109            //   form B: `if (c) { } else { v = a; }` (then empty) -> v = c ? b : a
7110            // Form B is the FOLD-time shape of every rendered no-else
7111            // overwrite: invert_empty_thens flips then/else AFTER this
7112            // pass, so the strict `else_stmt: None, overwrite-in-then`
7113            // match never saw a real-world instance (zero shape-2 folds
7114            // on lark mask ctors). The two assigns may target DIFFERENT
7115            // SSA generations of one rendered local (same base name +
7116            // same erased type): the ternary's keep-arm then reads
7117            // generation 1 — textually `v = c ? a : v`, exactly the
7118            // observable behavior of the unfolded pair (Kotlin mask-ctor
7119            // preludes: `v11x = p3x; if ((mask&4)!=0) v11x = -1L;`).
7120            let shape2 = {
7121                let s_i = single_local_assign(&stmts[i]);
7122                if let Stmt::If {
7123                    cond,
7124                    then_stmt,
7125                    else_stmt,
7126                    ..
7127                } = &stmts[i + 1]
7128                {
7129                    let then_a = single_local_assign(then_stmt);
7130                    let else_a = else_stmt.as_deref().and_then(single_local_assign);
7131                    let then_empty =
7132                        matches!(&**then_stmt, Stmt::Block(v) if v.is_empty());
7133                    let else_empty = else_stmt
7134                        .as_deref()
7135                        .map(|e| matches!(e, Stmt::Block(v) if v.is_empty()))
7136                        .unwrap_or(true);
7137                    let try_fold = |over: Option<(u32, Expr, Expr)>,
7138                                    keep_over: bool|
7139                     -> Option<Stmt> {
7140                        let (v2, tgt2, a) = over?;
7141                        let (v1, _t1, b) = s_i.as_ref()?;
7142                        if !(*v1 == v2 || same_render_var(*v1, v2, vt)) {
7143                            return None;
7144                        }
7145                        let keep = if *v1 == v2 {
7146                            b.clone()
7147                        } else {
7148                            Expr::Local {
7149                                var: *v1,
7150                                ty: vt.var(*v1).ty.clone(),
7151                            }
7152                        };
7153                        if !diamond_types_ok(&a, &keep) {
7154                            return None;
7155                        }
7156                        if keep_over {
7157                            Some(mk_ternary_assign(tgt2, cond.clone(), keep, a))
7158                        } else {
7159                            Some(mk_ternary_assign(tgt2, cond.clone(), a, keep))
7160                        }
7161                    };
7162                    if then_a.is_some() && else_empty {
7163                        try_fold(then_a, false)
7164                    } else if else_a.is_some() && then_empty {
7165                        try_fold(else_a, true)
7166                    } else {
7167                        None
7168                    }
7169                } else {
7170                    None
7171                }
7172            };
7173            // Leading-assign variant: the branch STARTS with the
7174            // overwrite and carries a tail (Kotlin mask-ctor last slot:
7175            // `str12 = null; if ((m&32)==0) { str12 = str6; this(..,
7176            // str12); return; }` — news_article CommonData/DTO family).
7177            // Fold the assign into the ternary and leave the tail in the
7178            // branch: the tail's reads see the identical value on both
7179            // paths (the ternary assigns before the branch runs). Gate:
7180            // the tail must not WRITE the var.
7181            let mut lead_fold: Option<(Stmt, Stmt)> = None;
7182            if shape2.is_none() && i + 1 < stmts.len() {
7183                let s_i2 = single_local_assign(&stmts[i]);
7184                let leading = |st: &Stmt| -> Option<((u32, Expr, Expr), Vec<Stmt>)> {
7185                    let Stmt::Block(v) = st else { return None };
7186                    if v.len() < 2 {
7187                        return None;
7188                    }
7189                    let first = single_local_assign(&v[0])?;
7190                    Some((first, v[1..].to_vec()))
7191                };
7192                if let (Some((v1, _t1, b)), Stmt::If { cond, then_stmt, else_stmt, .. }) =
7193                    (&s_i2, &stmts[i + 1])
7194                {
7195                    let else_absent = else_stmt.is_none()
7196                        || else_stmt
7197                            .as_deref()
7198                            .is_some_and(|e| matches!(e, Stmt::Block(v) if v.is_empty()));
7199                    let then_absent = matches!(&**then_stmt, Stmt::Block(v) if v.is_empty());
7200                    let try_lead = |over: Option<((u32, Expr, Expr), Vec<Stmt>)>,
7201                                    keep_over: bool|
7202                     -> Option<(Stmt, Stmt)> {
7203                        let ((v2, tgt2, a), rest) = over?;
7204                        if !(*v1 == v2 || same_render_var(*v1, v2, vt)) {
7205                            return None;
7206                        }
7207                        if stmts_count_writes(&rest, *v1) != 0
7208                            || stmts_count_writes(&rest, v2) != 0
7209                        {
7210                            return None;
7211                        }
7212                        let keep = if *v1 == v2 {
7213                            b.clone()
7214                        } else {
7215                            Expr::Local {
7216                                var: *v1,
7217                                ty: vt.var(*v1).ty.clone(),
7218                            }
7219                        };
7220                        if !diamond_types_ok(&a, &keep) {
7221                            return None;
7222                        }
7223                        let t = if keep_over {
7224                            mk_ternary_assign(tgt2, cond.clone(), keep, a)
7225                        } else {
7226                            mk_ternary_assign(tgt2, cond.clone(), a, keep)
7227                        };
7228                        let mut new_if = stmts[i + 1].clone();
7229                        if let Stmt::If { then_stmt, else_stmt, .. } = &mut new_if {
7230                            if keep_over {
7231                                *else_stmt = Some(Box::new(Stmt::Block(rest)));
7232                            } else {
7233                                **then_stmt = Stmt::Block(rest);
7234                            }
7235                        }
7236                        Some((t, new_if))
7237                    };
7238                    if else_absent {
7239                        lead_fold = try_lead(leading(then_stmt), false);
7240                    } else if then_absent {
7241                        if let Some(e) = else_stmt {
7242                            lead_fold = try_lead(leading(e), true);
7243                        }
7244                    }
7245                }
7246            }
7247            if let Some((folded, new_if)) = lead_fold {
7248                stmts[i] = folded;
7249                stmts[i + 1] = new_if;
7250                continue;
7251            }
7252            if let Some(f) = shape2 {
7253                stmts[i] = f;
7254                stmts.remove(i + 1);
7255                continue; // re-scan the folded position
7256            }
7257        }
7258        i += 1;
7259    }
7260}
7261
7262pub fn fold_bool_value_diamonds(body: &mut Stmt, vt: &VarTable) {
7263    fn is_bool_var(v: u32, vt: &VarTable) -> bool {
7264        (v as usize) < vt.vars.len()
7265            && matches!(vt.vars[v as usize].ty.erased(), JavaType::Boolean)
7266    }
7267    fn bool_const(e: &Expr) -> Option<bool> {
7268        fn cint(e: &Expr) -> Option<i32> {
7269            match e {
7270                Expr::Const(ConstVal::Int(n)) => Some(*n),
7271                _ => None,
7272            }
7273        }
7274        match e {
7275            Expr::Const(ConstVal::Int(0)) => Some(false),
7276            Expr::Const(ConstVal::Int(1)) => Some(true),
7277            // int↔bool bridge wrappings of constants: `0 != 0` is the
7278            // fallback pipeline's rendered `false` by the time the
7279            // ctor-block fold runs.
7280            Expr::Bin { op: op @ (BinOp::Eq | BinOp::Ne), l, r, .. } => {
7281                let (a, b) = (cint(l)?, cint(r)?);
7282                let eq = a == b;
7283                Some(if matches!(op, BinOp::Eq) { eq } else { !eq })
7284            }
7285            _ => None,
7286        }
7287    }
7288    fn reads_var(e: &Expr, v: u32) -> bool {
7289        let mut hit = false;
7290        visit_exprs(e, &mut |x| {
7291            if let Expr::Local { var, .. } = x {
7292                if *var == v {
7293                    hit = true;
7294                }
7295            }
7296        });
7297        hit
7298    }
7299    /// Bool-valued expression for the composite arms (&&, ||, ?:).
7300    /// Bare 0/1 constants are handled by the caller's special cases —
7301    /// inside a composite they would render as invalid `c && 1`.
7302    fn bool_valued(e: &Expr, vt: &VarTable) -> bool {
7303        fn side(e: &Expr, vt: &VarTable) -> bool {
7304            bool_const(e).is_some() || bool_valued(e, vt)
7305        }
7306        match e {
7307            Expr::Local { var, .. } => is_bool_var(*var, vt),
7308            Expr::Method { desc, .. } => desc.ret == JavaType::Boolean,
7309            Expr::InstanceOf { .. } => true,
7310            Expr::Un { op: UnOp::Not, e: x, .. } => bool_valued(x, vt) || bool_const(x).is_some(),
7311            Expr::Bin { op, l, r, .. } => match op {
7312                BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le => true,
7313                // A logical operand may itself be a 0/1 constant
7314                // (booleanize leaves mixed forms).
7315                BinOp::LogAnd | BinOp::LogOr => {
7316                    side(l, vt) && side(r, vt)
7317                }
7318                _ => false,
7319            },
7320            Expr::Cond { t, f, .. } => side(t, vt) && side(f, vt),
7321            _ => false,
7322        }
7323    }
7324    /// A branch that is exactly one plain `v = E` with E bool-valued
7325    /// (or a 0/1 constant) and not reading v — OR the forwarding PAIR
7326    /// `[u = E; v = u]` where u is single-assigned/single-read in the
7327    /// whole method: the fold consumes the pair into `v = E` (the impure
7328    /// `u = src.m()` arm shape forward_single_use cannot inline — its
7329    /// phi-ref gate rejects values reading multi-assigned prelude vars,
7330    /// and inlining here is scope-safe because E moves exactly one
7331    /// block level out with the arm's lone def consumed).
7332    fn lone_assign(
7333        b: &Stmt,
7334        v: u32,
7335        vt: &VarTable,
7336        an: &VarAnalysis,
7337        consumed: &mut HashSet<u32>,
7338    ) -> Option<Expr> {
7339        let l = flat_list(b);
7340        if l.len() == 2 {
7341            let (u, e): (u32, &Expr) = match l[0] {
7342                Stmt::LocalDef { var, init: Some(e), .. } => (*var, e),
7343                Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => {
7344                    match &**target {
7345                        Expr::Local { var, .. } => (*var, &**value),
7346                        _ => return None,
7347                    }
7348                }
7349                _ => return None,
7350            };
7351            if u == v || consumed.contains(&u) {
7352                return None;
7353            }
7354            // Whole-method single-assign/single-read: the fold drops
7355            // u's def with the arm, so ANY other read would dangle.
7356            // (Analysis counts are per-round; folds only ever REMOVE
7357            // reads elsewhere or clone E's — whose variable reads were
7358            // already counted at their def sites — so stale counts
7359            // over-approximate and the ==1 gates stay sound.)
7360            if an.assigns.get(u as usize).copied().unwrap_or(0) != 1
7361                || an.reads.get(u as usize).copied().unwrap_or(0) != 1
7362            {
7363                return None;
7364            }
7365            let (tv, rv) = match l[1] {
7366                Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => {
7367                    match (&**target, &**value) {
7368                        (Expr::Local { var: t, .. }, Expr::Local { var: r, .. }) => (*t, *r),
7369                        _ => return None,
7370                    }
7371                }
7372                Stmt::LocalDef { var, init: Some(Expr::Local { var: r, .. }), .. } => (*var, *r),
7373                _ => return None,
7374            };
7375            if tv != v || rv != u {
7376                return None;
7377            }
7378            if reads_var(e, v) || !(bool_valued(e, vt) || bool_const(e).is_some()) {
7379                return None;
7380            }
7381            consumed.insert(u);
7382            return Some(e.clone());
7383        }
7384        if l.len() != 1 {
7385            return None;
7386        }
7387        let (target, value) = match l[0] {
7388            Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => (target, value),
7389            Stmt::LocalDef { var, init: Some(value), .. } if *var == v => {
7390                if reads_var(value, v) || !(bool_valued(value, vt) || bool_const(value).is_some()) {
7391                    return None;
7392                }
7393                return Some(value.clone());
7394            }
7395            _ => return None,
7396        };
7397        let Expr::Local { var: tv, .. } = &**target else { return None };
7398        if *tv != v || reads_var(value, v) {
7399            return None;
7400        }
7401        if !(bool_valued(value, vt) || bool_const(value).is_some()) {
7402            return None;
7403        }
7404        Some((**value).clone())
7405    }
7406    fn not(e: Expr) -> Expr {
7407        Expr::Un { op: UnOp::Not, e: Box::new(e) }
7408    }
7409    /// Peephole the int↔bool bridge artifacts the fallback pipeline
7410    /// leaves in guards: `!(x != y)` → `x == y`; `(b ? 1 : 0) == 1` →
7411    /// `b`; `!(b ? 1 : 0 == 1)` chains likewise. Purely a readability
7412    /// pass — the unsimplified forms compile.
7413    fn simplify_bool(e: Expr) -> Expr {
7414        match e {
7415            Expr::Un { op: UnOp::Not, e: inner, .. } => match *inner {
7416                // Flip first, THEN re-simplify: `!((u?1:0) != 1)` must
7417                // reach the cond01 arm as `(u?1:0) == 1` → `u` (the
7418                // flip result never re-entered simplification and the
7419                // pair arm saw a Bin where it needed the bare local).
7420                Expr::Bin { op: BinOp::Ne, l, r, ty } => {
7421                    simplify_bool(Expr::Bin { op: BinOp::Eq, l, r, ty })
7422                }
7423                Expr::Bin { op: BinOp::Eq, l, r, ty } => {
7424                    simplify_bool(Expr::Bin { op: BinOp::Ne, l, r, ty })
7425                }
7426                other => Expr::Un { op: UnOp::Not, e: Box::new(simplify_bool(other)) },
7427            },
7428            Expr::Bin { op: op @ (BinOp::Eq | BinOp::Ne), l, r, ty } => {
7429                // (c ? 1 : 0) <op> 1  →  c / !c ; <op> 0 → !c / c
7430                fn cond01(e: &Expr) -> Option<Expr> {
7431                    if let Expr::Cond { c, t, f } = e {
7432                        if matches!(&**t, Expr::Const(ConstVal::Int(1)))
7433                            && matches!(&**f, Expr::Const(ConstVal::Int(0)))
7434                        {
7435                            return Some((**c).clone());
7436                        }
7437                    }
7438                    None
7439                }
7440                let (lc, rc) = (cond01(&l), cond01(&r));
7441                match (lc, rc) {
7442                    (Some(c), None) => match (&*r, op) {
7443                        (Expr::Const(ConstVal::Int(1)), BinOp::Eq) => c,
7444                        (Expr::Const(ConstVal::Int(1)), BinOp::Ne) => not(c),
7445                        (Expr::Const(ConstVal::Int(0)), BinOp::Eq) => not(c),
7446                        (Expr::Const(ConstVal::Int(0)), BinOp::Ne) => c,
7447                        _ => Expr::Bin { op, l: Box::new(c), r, ty },
7448                    },
7449                    (None, Some(c)) => match (&*l, op) {
7450                        (Expr::Const(ConstVal::Int(1)), BinOp::Eq) => c,
7451                        (Expr::Const(ConstVal::Int(1)), BinOp::Ne) => not(c),
7452                        (Expr::Const(ConstVal::Int(0)), BinOp::Eq) => not(c),
7453                        (Expr::Const(ConstVal::Int(0)), BinOp::Ne) => c,
7454                        _ => Expr::Bin { op, l, r: Box::new(c), ty },
7455                    },
7456                    _ => Expr::Bin { op, l, r, ty },
7457                }
7458            }
7459            other => other,
7460        }
7461    }
7462    fn bin(op: BinOp, l: Expr, r: Expr) -> Expr {
7463        Expr::Bin { op, l: Box::new(l), r: Box::new(r), ty: None }
7464    }
7465
7466    for _round in 0..4 {
7467        let mut changed = false;
7468        let mut analysis = VarAnalysis {
7469            assigns: Vec::new(),
7470            reads: Vec::new(),
7471            values: Vec::new(),
7472        };
7473        analyze_vars(body, &mut analysis);
7474        let mut consumed: HashSet<u32> = HashSet::default();
7475        walk_mut_deep(body, &mut |st| {
7476            let Stmt::Block(list) = st else { return };
7477            let mut i = 0usize;
7478            while i + 1 < list.len() {
7479                let folded = try_at(list, i, vt, &is_bool_var, &bool_const, &reads_var, &lone_assign, &analysis, &mut consumed);
7480                match folded {
7481                    Some(expr) => {
7482                        let is_def = matches!(&list[i], Stmt::LocalDef { .. });
7483                        let var = match &list[i] {
7484                            Stmt::LocalDef { var, .. } => *var,
7485                            Stmt::ExprStmt(Expr::Assign { target, .. }) => {
7486                                match &**target { Expr::Local { var, .. } => *var, _ => unreachable!() }
7487                            }
7488                            _ => unreachable!(),
7489                        };
7490                        let (is_final, force_type) = match &list[i] {
7491                            Stmt::LocalDef { is_final, force_type, .. } => (*is_final, *force_type),
7492                            _ => (false, false),
7493                        };
7494                        list[i] = if is_def {
7495                            Stmt::LocalDef { var, init: Some(expr), is_final, force_type }
7496                        } else {
7497                            Stmt::ExprStmt(Expr::Assign {
7498                                target: Box::new(Expr::Local {
7499                                    var,
7500                                    ty: TypeRef::J(JavaType::Boolean),
7501                                }),
7502                                op: AssignOp::Plain,
7503                                value: Box::new(expr),
7504                            })
7505                        };
7506                        list.remove(i + 1);
7507                        changed = true;
7508                        // stay at i: a preceding base may now pair with
7509                        // nothing new, but a FOLLOWING diamond may chain.
7510                    }
7511                    None => i += 1,
7512                }
7513            }
7514        });
7515        if !changed {
7516            break;
7517        }
7518    }
7519
7520    #[allow(clippy::too_many_arguments, clippy::type_complexity)]
7521    fn try_at(
7522        list: &[Stmt],
7523        i: usize,
7524        vt: &VarTable,
7525        is_bool_var: &dyn Fn(u32, &VarTable) -> bool,
7526        bool_const: &dyn Fn(&Expr) -> Option<bool>,
7527        reads_var: &dyn Fn(&Expr, u32) -> bool,
7528        lone_assign: &dyn Fn(&Stmt, u32, &VarTable, &VarAnalysis, &mut HashSet<u32>) -> Option<Expr>,
7529        an: &VarAnalysis,
7530        consumed: &mut HashSet<u32>,
7531    ) -> Option<Expr> {
7532        // s1: `v = <0|1>` (Assign or LocalDef), v Boolean-typed.
7533        let (v, base) = match &list[i] {
7534            Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => {
7535                match &**target {
7536                    Expr::Local { var, .. } => {
7537                        // The value may be a wrapped constant (`0 != 0`
7538                        // — the fallback pipeline's rendered false).
7539                        let b = bool_const(value)?;
7540                        if !is_bool_var(*var, vt) { return None; }
7541                        (*var, b)
7542                    }
7543                    _ => return None,
7544                }
7545            }
7546            Stmt::LocalDef { var, init: Some(e), .. } => {
7547                let b = bool_const(e)?;
7548                if !is_bool_var(*var, vt) { return None; }
7549                (*var, b)
7550            }
7551            _ => return None,
7552        };
7553        let Stmt::If { cond, then_stmt, else_stmt } = &list[i + 1] else {
7554            return None;
7555        };
7556        if reads_var(cond, v) {
7557            return None;
7558        }
7559        let mut touches_v = false;
7560        visit_exprs(cond, &mut |x| {
7561            if let Expr::Assign { target, .. } = x {
7562                if let Expr::Local { var, .. } = &**target {
7563                    if *var == v { touches_v = true; }
7564                }
7565            }
7566        });
7567        if touches_v {
7568            return None;
7569        }
7570        // The structurer emits inverted diamonds (`if (c) {} else { v = E }`,
7571        // empty-then) — normalize by negating the guard.
7572        let (cond_n, then_arm, else_arm): (Expr, &Stmt, Option<&Stmt>) =
7573            if flat_list(then_stmt).is_empty() && else_stmt.is_some() {
7574                (not(cond.clone()), else_stmt.as_ref().unwrap(), None)
7575            } else {
7576                (cond.clone(), then_stmt.as_ref(), else_stmt.as_deref())
7577            };
7578        let e1 = lone_assign(then_arm, v, vt, an, consumed)?;
7579        let e2 = match else_arm {
7580            None => None,
7581            Some(b) => Some(lone_assign(b, v, vt, an, consumed)?),
7582        };
7583        let e1c = bool_const(&e1);
7584        let c = simplify_bool(cond_n);
7585        Some(match e2 {
7586            None => {
7587                if !base {
7588                    match e1c {
7589                        Some(true) => c,
7590                        Some(false) => return None, // no-op diamond: guard effects would drop
7591                        None => bin(BinOp::LogAnd, c, e1),
7592                    }
7593                } else {
7594                    match e1c {
7595                        Some(false) => not(c),
7596                        Some(true) => return None,
7597                        None => bin(BinOp::LogOr, not(c), e1),
7598                    }
7599                }
7600            }
7601            Some(e2) => {
7602                // Full diamond: both arms assign — the base is dead.
7603                if e1 == e2 {
7604                    return None; // both arms equal: folding drops guard effects
7605                }
7606                Expr::Cond { c: Box::new(c), t: Box::new(e1), f: Box::new(e2) }
7607            }
7608        })
7609    }
7610}
7611
7612pub fn idiom_compounds(body: &mut Stmt, vt: &VarTable) {
7613    walk_mut_deep(body, &mut |st| {
7614        let Stmt::ExprStmt(e) = st else { return };
7615        let Expr::Assign { target, op: AssignOp::Plain, value } = &mut *e else {
7616            return;
7617        };
7618        // Target: a local, or a bare field (static / this — no complex
7619        // receiver whose re-evaluation a compound would elide).
7620        enum Tgt {
7621            Loc(u32),
7622            Fld,
7623        }
7624        let tgt = match &**target {
7625            Expr::Local { var, .. } => Tgt::Loc(*var),
7626            Expr::Field { owner, .. }
7627                if owner.is_none()
7628                    || matches!(&**owner.as_ref().unwrap(), Expr::This) =>
7629            {
7630                Tgt::Fld
7631            }
7632            _ => return,
7633        };
7634        let Expr::Bin { op, l, r, .. } = &mut **value else {
7635            return;
7636        };
7637        // The left operand must be the SAME storage as the target.
7638        match (&tgt, &**l) {
7639            (Tgt::Loc(v), Expr::Local { var, .. }) if *var == *v => {}
7640            (Tgt::Fld, Expr::Field { cls: c2, name: n2, .. }) => {
7641                let Expr::Field { cls: c1, name: n1, .. } = &**target else {
7642                    return;
7643                };
7644                if c1 != c2 || n1 != n2 {
7645                    return;
7646                }
7647            }
7648            _ => return,
7649        }
7650        // ±1 constant on a numeric var → post-inc/dec.
7651        let one = match &**r {
7652            Expr::Const(ConstVal::Int(1)) => Some(1i64),
7653            Expr::Const(ConstVal::Int(-1)) => Some(-1),
7654            Expr::Const(ConstVal::Long(1)) => Some(1),
7655            Expr::Const(ConstVal::Long(-1)) => Some(-1),
7656            _ => None,
7657        };
7658        if *op == BinOp::Add || *op == BinOp::Sub {
7659            if let Some(mut d) = one {
7660                if *op == BinOp::Sub {
7661                    d = -d;
7662                }
7663                let tgt_ty = match &tgt {
7664                    Tgt::Loc(v) => vt.var(*v).ty.erased(),
7665                    Tgt::Fld => {
7666                        let Expr::Field { ty, .. } = &**target else {
7667                            return;
7668                        };
7669                        ty.erased()
7670                    }
7671                };
7672                let numeric = matches!(
7673                    tgt_ty,
7674                    JavaType::Int
7675                        | JavaType::Long
7676                        | JavaType::Short
7677                        | JavaType::Byte
7678                        | JavaType::Char
7679                        | JavaType::Float
7680                        | JavaType::Double
7681                );
7682                if numeric {
7683                    let wide = matches!(tgt_ty, JavaType::Long);
7684                    let inner = match &tgt {
7685                        Tgt::Loc(v) => Expr::Local {
7686                            var: *v,
7687                            ty: vt.var(*v).ty.clone(),
7688                        },
7689                        Tgt::Fld => (**target).clone(),
7690                    };
7691                    *e = Expr::PostIncDec {
7692                        e: Box::new(inner),
7693                        delta: d,
7694                        wide,
7695                    };
7696                    return;
7697                }
7698            }
7699        }
7700        let cop = match op {
7701            BinOp::Add => AssignOp::Add,
7702            BinOp::Sub => AssignOp::Sub,
7703            BinOp::Mul => AssignOp::Mul,
7704            BinOp::Div => AssignOp::Div,
7705            BinOp::Rem => AssignOp::Rem,
7706            BinOp::Shl => AssignOp::Shl,
7707            BinOp::Shr => AssignOp::Shr,
7708            BinOp::Ushr => AssignOp::Ushr,
7709            BinOp::And => AssignOp::And,
7710            BinOp::Or => AssignOp::Or,
7711            BinOp::Xor => AssignOp::Xor,
7712            _ => return,
7713        };
7714        let rhs = std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
7715        let new_target = match &tgt {
7716            Tgt::Loc(v) => Box::new(Expr::Local {
7717                var: *v,
7718                ty: vt.var(*v).ty.clone(),
7719            }),
7720            Tgt::Fld => target.clone(),
7721        };
7722        *e = Expr::Assign {
7723            target: new_target,
7724            op: cop,
7725            value: rhs,
7726        };
7727    });
7728}
7729
7730/// Boolean inference: vars only ever assigned 0/1/comparisons/booleans and
7731/// read in conditions become `boolean`, with `v != 0` → `v` in conditions.
7732/// Returns the number of vars converted (0 = nothing changed — the
7733/// caller skips the post-booleanize re-split, which only has work when
7734/// a conversion exposed a mixed-kind register).
7735pub fn booleanize(vt: &mut VarTable, body: &mut Stmt, ret_bool: bool) -> usize {
7736    // Booleans propagate through local chains (`v17 = v24` where v24
7737    // itself became boolean in the first round) — iterate to a fixpoint;
7738    // the common corpus converts nothing and exits after one round.
7739    // The deref disqualifier set is structural (positions, not types) —
7740    // compute ONCE per call, not per round (12 full walks on big
7741    // methods cost weibo +60% wall).
7742    let n0 = vt.vars.len();
7743    let mut derefed = vec![false; n0];
7744    mark_derefs(body, &mut derefed);
7745    let mut total = 0usize;
7746    for _ in 0..4 {
7747        let n = booleanize_round(vt, body, ret_bool, &derefed);
7748        if n == 0 {
7749            break;
7750        }
7751        total += n;
7752    }
7753    total
7754}
7755
7756/// Mark vars used in DEREF positions (field/method receiver, array
7757/// base) — allocation-free recursion (visit_all_exprs allocates a
7758/// child Vec per statement; two callers run this per method).
7759fn mark_deref_expr(e: &Expr, derefed: &mut [bool]) {
7760    match e {
7761        Expr::Field { owner: Some(o), is_static: false, .. }
7762        | Expr::Method { owner: Some(o), .. } => {
7763            if let Expr::Local { var, .. } = &**o {
7764                if (*var as usize) < derefed.len() {
7765                    derefed[*var as usize] = true;
7766                }
7767            }
7768        }
7769        Expr::ArrayIndex { array, .. } => {
7770            if let Expr::Local { var, .. } = &**array {
7771                if (*var as usize) < derefed.len() {
7772                    derefed[*var as usize] = true;
7773                }
7774            }
7775        }
7776        _ => {}
7777    }
7778    for_each_child(e, &mut |c| mark_deref_expr(c, derefed));
7779}
7780
7781fn mark_derefs(body: &Stmt, derefed: &mut [bool]) {
7782    walk_all(body, &mut |st| {
7783        match st {
7784            Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
7785                mark_deref_expr(e, derefed);
7786            }
7787            Stmt::Return(Some(e)) => mark_deref_expr(e, derefed),
7788            Stmt::LocalDef { init: Some(e), .. } => mark_deref_expr(e, derefed),
7789            Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
7790                mark_deref_expr(cond, derefed);
7791            }
7792            Stmt::For { init, .. } => {
7793                for x in init {
7794                    mark_derefs(x, derefed);
7795                }
7796            }
7797            Stmt::Switch { selector, .. } => mark_deref_expr(selector, derefed),
7798            Stmt::ForEach { iterable, .. } => mark_deref_expr(iterable, derefed),
7799            _ => {}
7800        }
7801    });
7802}
7803
7804fn booleanize_round(
7805    vt: &mut VarTable,
7806    body: &mut Stmt,
7807    ret_bool: bool,
7808    derefed: &[bool],
7809) -> usize {
7810    let n = vt.vars.len();
7811    let mut in_cond = vec![false; n];
7812
7813    // Condition uses: `v == 0` / `v != 0` if-conditions and comparison
7814    // operands inside assigned values.
7815    walk_all(body, &mut |st| {
7816        let cond: Option<&Expr> = match st {
7817            Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
7818                Some(cond)
7819            }
7820            _ => None,
7821        };
7822        if let Some(c) = cond {
7823            if let Expr::Bin {
7824                op: BinOp::Eq | BinOp::Ne,
7825                l,
7826                r,
7827                ..
7828            } = c
7829            {
7830                for (x, other) in [(l, r), (r, l)] {
7831                    if let (Expr::Local { var, .. }, Expr::Const(ConstVal::Int(0))) =
7832                        (&**x, &**other)
7833                    {
7834                        if (*var as usize) < n {
7835                            in_cond[*var as usize] = true;
7836                        }
7837                    }
7838                }
7839            }
7840        }
7841        // A local RETURNED from a boolean method is boolean-typed even
7842        // though it never appears in a condition (`int v9 = 0/1 … return
7843        // v9;` — returning an int from `boolean check()` is a compile
7844        // error; lab package Obf.check).
7845        if ret_bool {
7846            if let Stmt::Return(Some(Expr::Local { var, .. })) = st {
7847                if (*var as usize) < n {
7848                    in_cond[*var as usize] = true;
7849                }
7850            }
7851        }
7852        // A local STORED into a boolean field is boolean-typed even though
7853        // it never appears in a condition (`b = v1` where `b` is a boolean
7854        // field, `v1` an int 0/1 local — q.java static init). The emit
7855        // layer already coerces a boolean-target assign through expr_bool,
7856        // but that renders a bare int LOCAL unchanged, so the local itself
7857        // must be booleanized (its 0/1 assigns then print false/true).
7858        if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = st {
7859            if let Expr::Field { ty, .. } = &**target {
7860                if ty.erased() == JavaType::Boolean {
7861                    if let Expr::Local { var, .. } = &**value {
7862                        if (*var as usize) < n {
7863                            in_cond[*var as usize] = true;
7864                        }
7865                    }
7866                }
7867            }
7868        }
7869        let val: Option<&Expr> = match st {
7870            Stmt::ExprStmt(Expr::Assign { value, .. }) => Some(value),
7871            // Bare expression statements (e.g. `q(v117);`) so a local
7872            // passed to a boolean parameter is caught below.
7873            Stmt::ExprStmt(e) => Some(e),
7874            Stmt::LocalDef { init: Some(e), .. } => Some(e),
7875            Stmt::Return(Some(e)) => Some(e),
7876            _ => None,
7877        };
7878        if let Some(v) = val {
7879            visit_exprs(v, &mut |x| {
7880                // A local passed as a BOOLEAN parameter is boolean-typed
7881                // (`q(v117)` where q's param is boolean, v117 an int 0/1 —
7882                // f8/b.java). The DEX passes booleans as 0/1 ints, so a
7883                // boolean param slot is authoritative for the argument's
7884                // type.
7885                if let Expr::Method { desc, args, .. } = x {
7886                    for (i, a) in args.iter().enumerate() {
7887                        if desc.args.get(i) == Some(&JavaType::Boolean) {
7888                            if let Expr::Local { var, .. } = a {
7889                                if (*var as usize) < n {
7890                                    in_cond[*var as usize] = true;
7891                                }
7892                            }
7893                        }
7894                    }
7895                }
7896                if let Expr::Bin { op, l, r, .. } = x {
7897                    if matches!(
7898                        op,
7899                        BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
7900                    ) {
7901                        for side in [l, r] {
7902                            if let Expr::Local { var, .. } = &**side {
7903                                if (*var as usize) < n {
7904                                    in_cond[*var as usize] = true;
7905                                }
7906                            }
7907                        }
7908                    }
7909                    // Kotlin/d8 merge booleans through INT bitwise ops
7910                    // (`v3 | obj instanceof g`) == 0 — the other side's
7911                    // static type being boolean makes this a boolean
7912                    // context for the local (a genuine int bitwise
7913                    // expression never has a boolean operand).
7914                    if matches!(op, BinOp::Or | BinOp::And) {
7915                        let l_bool = l.type_ref().erased() == JavaType::Boolean;
7916                        let r_bool = r.type_ref().erased() == JavaType::Boolean;
7917                        if l_bool != r_bool {
7918                            let side = if l_bool { r } else { l };
7919                            if let Expr::Local { var, .. } = &**side {
7920                                if (*var as usize) < n {
7921                                    in_cond[*var as usize] = true;
7922                                }
7923                            }
7924                        }
7925                    }
7926                }
7927            });
7928        }
7929    });
7930
7931    // A var whose every assignment value is boolean-shaped AND which is
7932    // referenced in conditions becomes boolean.
7933    // (Perf: this used to re-walk the whole statement tree ONCE PER VAR —
7934    // O(vars × stmts); on R8-merged monsters (2000+ vars, 15k statements)
7935    // that was tens of millions of visits. One pass collects the same
7936    // (any, all_bool) facts for every var.)
7937    let mut assigned_any = vec![false; n];
7938    let mut all_bool = vec![true; n];
7939    let mut edges: Vec<(usize, usize)> = Vec::new();
7940    walk_all(body, &mut |st| {
7941        let (var, value) = match st {
7942            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => match &**target {
7943                Expr::Local { var, .. } => (var, &**value),
7944                _ => return,
7945            },
7946            Stmt::LocalDef {
7947                var, init: Some(e), ..
7948            } => (var, e),
7949            _ => return,
7950        };
7951        let i = *var as usize;
7952        if i < n {
7953            assigned_any[i] = true;
7954            if !is_boolean_valued(value, vt) {
7955                all_bool[i] = false;
7956            }
7957            // Chain edges for the context closure below: `v17 = v24`
7958            // links the two locals; when the TARGET is in a boolean
7959            // context, the source inherits it (its only reader is the
7960            // boolean-shaped chain). Collected as edges because the
7961            // target's context can be discovered anywhere in the tree.
7962            if let Expr::Local { var: src, .. } = value {
7963                if (*src as usize) < n {
7964                    edges.push((i, *src as usize));
7965                }
7966            }
7967        }
7968    });
7969    // Context closure along local chains: a var in a boolean context
7970    // pushes that context through every local it is assigned from
7971    // (`return v17` — v17 = v24 — v24 = 0/1) — the statement order
7972    // cannot be relied on (the return may follow the assignment).
7973    loop {
7974        let mut changed = false;
7975        for &(tgt, src) in &edges {
7976            if in_cond[tgt] && !in_cond[src] && all_bool[src] {
7977                in_cond[src] = true;
7978                changed = true;
7979            }
7980            // FORWARD through pure copies: `v21 = v17` with v17 boolean
7981            // makes v21 boolean when ALL of v21's assignments are
7982            // bool-shaped (the loop-state save pattern — weixin u2/f's
7983            // v62 → v17 → v21 chains left the copy targets int while
7984            // the accumulator booleanized).
7985            if in_cond[src] && !in_cond[tgt] && all_bool[tgt] {
7986                in_cond[tgt] = true;
7987                changed = true;
7988            }
7989        }
7990        if !changed {
7991            break;
7992        }
7993    }
7994    let reads = count_locals_stmts(std::slice::from_ref(body));
7995    let mut boolean_vars: HashSet<u32> = HashSet::default();
7996    for i in 0..n {
7997        if vt.vars[i].is_param {
7998            continue;
7999        }
8000        if derefed[i] {
8001            continue;
8002        }
8003        if !in_cond[i] && !(reads.get(&(i as u32)).copied().unwrap_or(0) == 0) {
8004            continue;
8005        }
8006        if assigned_any[i] && all_bool[i] {
8007            boolean_vars.insert(i as u32);
8008        }
8009    }
8010    if boolean_vars.is_empty() {
8011        return 0;
8012    }
8013    for v in &boolean_vars {
8014        vt.vars[*v as usize].ty = TypeRef::J(JavaType::Boolean);
8015    }
8016    let types: Vec<&TypeRef> = vt.vars.iter().map(|v| &v.ty).collect();
8017    rewrite_exprs(body, &mut |e| {
8018        deep_rewrite(e, &mut |x| {
8019            if let Expr::Local { var, ty } = x {
8020                if let Some(want) = types.get(*var as usize) {
8021                    if ty != *want {
8022                        *ty = (*want).clone();
8023                    }
8024                }
8025            }
8026        });
8027    });
8028
8029    // Condition folding: `b != 0` → `b`, `b == 0` → `!b` (boolean vars).
8030    fold_bool_conditions(body, &boolean_vars);
8031    boolean_vars.len()
8032}
8033
8034fn is_boolean_valued(e: &Expr, vt: &VarTable) -> bool {
8035    match e {
8036        Expr::Const(ConstVal::Int(0)) | Expr::Const(ConstVal::Int(1)) => true,
8037        // Kotlin's non-short-circuit boolean `or`/`and` compiles to `|`/`&`
8038        // over boolean locals (`v15 = delete | delete2 | ..`). EITHER side
8039        // boolean suffices: `int | boolean` is illegal in source — it is
8040        // always a not-yet-booleanized side of a boolean chain (the
8041        // loop-carried accumulator `v15 = v20; v20 = v15 | delete3` can
8042        // never seed its all_bool fixpoint otherwise).
8043        Expr::Bin { op: BinOp::Or | BinOp::And, l, r, .. } => {
8044            is_boolean_valued(l, vt) || is_boolean_valued(r, vt)
8045        }
8046        // Kotlin's `!x` lowers to `x ^ 1`; at booleanize time the value
8047        // is still the Xor (converted to ! form later — by cure's source
8048        // rules now), so an int-typed generation receiving it never
8049        // retyped (weixin v2/i's `int v264_g160_g7 = !v200`). BOTH sides
8050        // must be bool-shaped:
8051        // `bool ^ 1` qualifies (Const 0/1 is boolean-valued above),
8052        // a genuine int `flags ^ 1` does not (flags stays Int in vt).
8053        // MUST precede the generic Bin arm (which would swallow Xor).
8054        Expr::Bin {
8055            op: BinOp::Xor,
8056            l,
8057            r,
8058            ..
8059        } => is_boolean_valued(l, vt) && is_boolean_valued(r, vt),
8060        Expr::Bin { op, .. } => matches!(
8061            op,
8062            BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
8063        ),
8064        Expr::InstanceOf { .. } => true,
8065        // The VarTable is the authority: the embedded Local ty is a
8066        // lift-time snapshot and goes STALE across booleanize rounds —
8067        // a converted copy source kept its partners int (`v150 = v131`
8068        // boolean→int, weixin ConstraintLayout ×1.4k files) because the
8069        // all_bool fixpoint consulted the stale Int.
8070        Expr::Local { var, ty } => {
8071            let t = if (*var as usize) < vt.vars.len() {
8072                vt.var(*var).ty.erased()
8073            } else {
8074                ty.erased()
8075            };
8076            t == JavaType::Boolean
8077        }
8078        Expr::Method { desc, .. } => desc.ret == JavaType::Boolean,
8079        Expr::Cond { t, f, .. } => is_boolean_valued(t, vt) && is_boolean_valued(f, vt),
8080        Expr::Un { op: UnOp::Not, .. } => true,
8081        // Field ty comes from the dex descriptor — authoritative, not a
8082        // lift snapshot (`!w1x.i` arrives here still as `w1x.i ^ 1`; the
8083        // Xor arm needs this side bool-shaped).
8084        Expr::Field { ty, .. } => ty.erased() == JavaType::Boolean,
8085        _ => false,
8086    }
8087}
8088
8089fn fold_bool_conditions(s: &mut Stmt, bv: &HashSet<u32>) {
8090    let fold = |e: &mut Expr| {
8091        deep_rewrite(e, &mut |x| {
8092            if let Expr::Bin { op, l, r, .. } = x {
8093                let (var_side, const_side) = match (&**l, &**r) {
8094                    (Expr::Local { .. }, Expr::Const(ConstVal::Int(_))) => (l, 0),
8095                    (Expr::Const(ConstVal::Int(_)), Expr::Local { .. }) => (r, 1),
8096                    _ => return,
8097                };
8098                let var = match &**var_side {
8099                    Expr::Local { var, .. } => *var,
8100                    _ => return,
8101                };
8102                if !bv.contains(&var) {
8103                    return;
8104                }
8105                let _ = const_side;
8106                match op {
8107                    BinOp::Ne => {
8108                        let inner = var_side.clone();
8109                        *x = *inner;
8110                    }
8111                    BinOp::Eq => {
8112                        let inner = var_side.clone();
8113                        *x = Expr::Un {
8114                            op: UnOp::Not,
8115                            e: Box::new(*inner),
8116                        };
8117                    }
8118                    _ => {}
8119                }
8120            }
8121        });
8122    };
8123    match s {
8124        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => fold(cond),
8125        _ => {}
8126    }
8127    walk_mut_deep(s, &mut |st| {
8128        let cond = match st {
8129            Stmt::If { cond, .. } => Some(cond),
8130            Stmt::While { cond, .. } => Some(cond),
8131            Stmt::DoWhile { cond, .. } => Some(cond),
8132            _ => None,
8133        };
8134        if let Some(c) = cond {
8135            fold(c);
8136        }
8137    });
8138}
8139
8140/// Normalize `1 == v` comparisons to `v == 1` (d8 emits const-first if-eq).
8141pub fn flip_const_compares(s: &mut Stmt) {
8142    rewrite_exprs(s, &mut |e| {
8143        deep_rewrite(e, &mut |x| {
8144            if let Expr::Bin { op, l, r, .. } = x {
8145                if matches!(
8146                    op,
8147                    BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
8148                ) {
8149                    let l_const = matches!(&**l, Expr::Const(_));
8150                    let r_var = matches!(&**r, Expr::Local { .. });
8151                    if l_const && r_var {
8152                        std::mem::swap(l, r);
8153                    }
8154                }
8155            }
8156        });
8157    });
8158}
8159
8160/// Object-typed locals compared against 0 compare against `null`.
8161pub fn null_compares(vt: &VarTable, body: &mut Stmt) {
8162    let obj_vars: HashSet<u32> = vt
8163        .vars
8164        .iter()
8165        .filter(|v| v.ty.erased().is_reference())
8166        .map(|v| v.id)
8167        .collect();
8168    rewrite_exprs(body, &mut |e| {
8169        deep_rewrite(e, &mut |x| {
8170            null_side_rewrite(x, &|v| obj_vars.contains(&v));
8171        });
8172    });
8173}
8174
8175/// Drop assignments/declarations of locals that are NEVER read anywhere
8176/// in the method. Register-machine merges commit values no Java-level
8177/// code consumes (phi residue like `printStream = check;` — not just
8178/// noise: the merge variable's inferred type need not match the value,
8179/// so these lines are often type errors as well). Side-effect-free
8180/// values drop entirely; an impure value survives as a bare expression
8181/// statement. Iterated to a fixpoint: dropping `a = b` can make `b`
8182/// unread.
8183pub fn drop_dead_locals(body: &mut Stmt) {
8184    loop {
8185        let mut reads: Vec<usize> = Vec::new();
8186        count_reads(body, &mut reads);
8187        let mut dropped = 0usize;
8188        walk_mut_deep(body, &mut |st| match st {
8189            Stmt::Block(items) => prune_dead_items(items, &reads, &mut dropped),
8190            // Switch case bodies and For inits are bare `Vec<Stmt>`, NOT
8191            // wrapped in a `Stmt::Block`, so the Block arm never sees them.
8192            // Dead phi commits land directly in case bodies (`sb4 =
8193            // compareTo;` per case): pruning only Blocks dropped the
8194            // commit's reader (in a real post-switch Block) but left the
8195            // now-dead commits, stalling the fixpoint cascade and leaking
8196            // mistyped phi residue. TryWithResources.resources is
8197            // deliberately excluded — its LocalDefs carry auto-close
8198            // semantics that a bare-expression rewrite would break.
8199            Stmt::Switch { cases, .. } => {
8200                for c in cases {
8201                    prune_dead_items(&mut c.body, &reads, &mut dropped);
8202                }
8203            }
8204            Stmt::For { init, .. } => prune_dead_items(init, &reads, &mut dropped),
8205            _ => {}
8206        });
8207        if dropped == 0 {
8208            break;
8209        }
8210    }
8211    // Post-fixpoint in-place neutralization: dead assignments in BARE
8212    // positions (single-statement If arms, bare case bodies) that no
8213    // container retain ever reached. Runs AFTER the loop so the clean
8214    // container prune stays primary — the replacement (empty Block) is
8215    // residue the prune cannot match, so doing it inside the loop would
8216    // cannibalize clean removals (batch2 experiment: +5% lines, +534
8217    // errors). Absorbed from the deleted final_dead_assigns: without it
8218    // the decl gets pruned while the bare write survives → 找不到符号.
8219    {
8220        let mut reads: Vec<usize> = Vec::new();
8221        count_reads(body, &mut reads);
8222        let dead_inplace = |st: &Stmt| -> bool {
8223            let Stmt::ExprStmt(Expr::Assign {
8224                target,
8225                value,
8226                op: jdc_core::ir::expr::AssignOp::Plain,
8227                ..
8228            }) = st
8229            else {
8230                return false;
8231            };
8232            let Expr::Local { var, .. } = &**target else {
8233                return false;
8234            };
8235            reads.get(*var as usize).copied().unwrap_or(0) == 0 && !has_side_effects(value)
8236        };
8237        walk_mut_deep(body, &mut |st| {
8238            if dead_inplace(st) {
8239                *st = Stmt::Block(vec![]);
8240            }
8241        });
8242    }
8243}
8244
8245/// Remove dead assignments/declarations from one bare statement vector.
8246/// Shared by `drop_dead_locals` across every prunable `Vec<Stmt>`
8247/// container. Side-effect-free values drop entirely; an impure value
8248/// survives as a bare expression statement.
8249fn prune_dead_items(items: &mut Vec<Stmt>, reads: &[usize], dropped: &mut usize) {
8250    let dead = |v: u32| reads.get(v as usize).copied().unwrap_or(0) == 0;
8251    items.retain_mut(|x| match x {
8252        Stmt::ExprStmt(Expr::Assign { target, value, op, .. }) => {
8253            let dead_target = matches!(&**target, Expr::Local { var, .. } if dead(*var));
8254            if dead_target && matches!(*op, AssignOp::Plain) {
8255                *dropped += 1;
8256                if has_side_effects(value) {
8257                    let e = std::mem::replace(value, Box::new(Expr::This));
8258                    *x = Stmt::ExprStmt(*e);
8259                    true
8260                } else {
8261                    false
8262                }
8263            } else {
8264                true
8265            }
8266        }
8267        Stmt::LocalDef { var, init, .. } if dead(*var) => {
8268            *dropped += 1;
8269            if let Some(e) = init.take() {
8270                if has_side_effects(&e) {
8271                    *x = Stmt::ExprStmt(e);
8272                    return true;
8273                }
8274            }
8275            false
8276        }
8277        _ => true,
8278    });
8279}
8280
8281
8282/// Vars occurring in this statement's OWN expression payloads (nested
8283/// sub-statements are descended separately so every occurrence carries
8284/// its innermost block id). ForEach/Catch bound vars are declarations,
8285/// not occurrences.
8286fn stmt_shallow_vars<F: FnMut(u32)>(st: &Stmt, out: &mut F) {
8287    fn ex<F: FnMut(u32)>(e: &Expr, out: &mut F) {
8288        visit_exprs(e, &mut |x| {
8289            if let Expr::Local { var, .. } = x {
8290                out(*var);
8291            }
8292        });
8293    }
8294    match st {
8295        Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
8296            ex(e, out)
8297        }
8298        Stmt::TernaryValue { e } => ex(e, out),
8299        Stmt::Return(Some(e)) => ex(e, out),
8300        Stmt::LocalDef { var, init, .. } => {
8301            out(*var);
8302            if let Some(e) = init {
8303                ex(e, out);
8304            }
8305        }
8306        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
8307            ex(cond, out)
8308        }
8309        Stmt::For { cond, update, .. } => {
8310            if let Some(c) = cond {
8311                ex(c, out);
8312            }
8313            for u in update {
8314                ex(u, out);
8315            }
8316        }
8317        Stmt::ForEach { iterable, .. } => ex(iterable, out),
8318        Stmt::Switch { selector, cases, .. } => {
8319            ex(selector, out);
8320            for cg in cases {
8321                if let Some(g) = &cg.guard {
8322                    ex(g, out);
8323                }
8324            }
8325        }
8326        Stmt::Assert { cond, msg } => {
8327            ex(cond, out);
8328            if let Some(m) = msg {
8329                ex(m, out);
8330            }
8331        }
8332        Stmt::Synchronized { lock, .. } => ex(lock, out),
8333        _ => {}
8334    }
8335}
8336
8337/// Pass A: number every Block in pre-order (root = 0) and record each
8338/// var's defining block. `sizes[id]` = the id-range span of the subtree.
8339/// Pass A: number every EMITTER-BRACED position in pre-order (root = 0)
8340/// and record each var's defining block. `sizes[id]` = the id-range span
8341/// of the subtree. A braced position is a Java scope whether or not the
8342/// IR node is a Stmt::Block — switch case bodies are bare Vecs and
8343/// single-statement if arms are bare statements, and sharing the parent's
8344/// id for them hid the scope violation (Telegram jk.java: a case-local
8345/// `notificationCenterDelegate`/`d0x` read after the switch never
8346/// hoisted — 找不到符号 ×76 in one file; the `d9x.L` reads even resolved
8347/// as a PACKAGE — 程序包不存在 ×92 tree-wide).
8348fn scope_pass_a(st: &Stmt, cur: u32, next: &mut u32, def_block: &mut [u32], sizes: &mut Vec<u32>) {
8349    if let Stmt::LocalDef { var, .. } = st {
8350        let i = *var as usize;
8351        if i < def_block.len() && def_block[i] == u32::MAX {
8352            def_block[i] = cur;
8353        }
8354    }
8355    fn scoped(c: &Stmt, next: &mut u32, def_block: &mut [u32], sizes: &mut Vec<u32>) {
8356        let id = *next;
8357        *next += 1;
8358        sizes.push(0);
8359        scope_pass_a(c, id, next, def_block, sizes);
8360        sizes[id as usize] = *next - id;
8361    }
8362    match st {
8363        Stmt::Block(v) => {
8364            for x in v {
8365                scope_pass_a(x, cur, next, def_block, sizes);
8366            }
8367        }
8368        Stmt::If {
8369            then_stmt,
8370            else_stmt,
8371            ..
8372        } => {
8373            scoped(then_stmt, next, def_block, sizes);
8374            if let Some(e) = else_stmt {
8375                scoped(e, next, def_block, sizes);
8376            }
8377        }
8378        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => {
8379            scoped(body, next, def_block, sizes)
8380        }
8381        Stmt::For { init, body, .. } => {
8382            // A for-init declaration scopes over the WHOLE for statement
8383            // (header + body): one shared id, the body a nested one.
8384            let id = *next;
8385            *next += 1;
8386            sizes.push(0);
8387            for x in init {
8388                scope_pass_a(x, id, next, def_block, sizes);
8389            }
8390            scoped(body, next, def_block, sizes);
8391            sizes[id as usize] = *next - id;
8392        }
8393        Stmt::ForEach { body, .. } => scoped(body, next, def_block, sizes),
8394        Stmt::Switch { cases, default, .. } => {
8395            for c in cases {
8396                // One scope per case group (the emitter braces each);
8397                // non-Block statements share the case id, nested Blocks
8398                // get sub-ids — siblings in one brace group must share,
8399                // or a def's later same-case reads fall "outside".
8400                let id = *next;
8401                *next += 1;
8402                sizes.push(0);
8403                for x in &c.body {
8404                    if matches!(x, Stmt::Block(_)) {
8405                        scoped(x, next, def_block, sizes);
8406                    } else {
8407                        scope_pass_a(x, id, next, def_block, sizes);
8408                    }
8409                }
8410                sizes[id as usize] = *next - id;
8411            }
8412            if let Some(d) = default {
8413                scoped(d, next, def_block, sizes);
8414            }
8415        }
8416        Stmt::Try {
8417            body,
8418            catches,
8419            finally,
8420        } => {
8421            scoped(body, next, def_block, sizes);
8422            for c in catches {
8423                scoped(&c.body, next, def_block, sizes);
8424            }
8425            if let Some(f) = finally {
8426                scoped(f, next, def_block, sizes);
8427            }
8428        }
8429        Stmt::TryWithResources {
8430            resources,
8431            body,
8432            catches,
8433            finally,
8434        } => {
8435            // Resource declarations scope over the WHOLE try statement.
8436            let id = *next;
8437            *next += 1;
8438            sizes.push(0);
8439            for r in resources {
8440                scope_pass_a(r, id, next, def_block, sizes);
8441            }
8442            scoped(body, next, def_block, sizes);
8443            for c in catches {
8444                scoped(&c.body, next, def_block, sizes);
8445            }
8446            if let Some(f) = finally {
8447                scoped(f, next, def_block, sizes);
8448            }
8449            sizes[id as usize] = *next - id;
8450        }
8451        Stmt::Synchronized { body, .. } => scoped(body, next, def_block, sizes),
8452        Stmt::Labeled { body, .. } => scope_pass_a(body, cur, next, def_block, sizes),
8453        _ => {}
8454    }
8455}
8456
8457/// Pass B (same numbering walk): count each var's occurrences overall
8458/// and within its defining block's subtree. For/TWR take dedicated arms
8459/// so their header expressions count INSIDE the statement scope (a
8460/// normal `for (int i…)` must not look like an outside read of i).
8461fn scope_pass_b(
8462    st: &Stmt,
8463    cur: u32,
8464    next: &mut u32,
8465    def_block: &[u32],
8466    sizes: &[u32],
8467    within: &mut [u32],
8468    total: &mut [u32],
8469) {
8470    fn cnt(
8471        v: u32,
8472        at: u32,
8473        def_block: &[u32],
8474        sizes: &[u32],
8475        within: &mut [u32],
8476        total: &mut [u32],
8477    ) {
8478        let i = v as usize;
8479        if i < total.len() {
8480            total[i] += 1;
8481            let d = def_block[i];
8482            if d != u32::MAX && d <= at && at < d + sizes[d as usize] {
8483                within[i] += 1;
8484            }
8485        }
8486    }
8487    fn scoped(
8488        c: &Stmt,
8489        next: &mut u32,
8490        def_block: &[u32],
8491        sizes: &[u32],
8492        within: &mut [u32],
8493        total: &mut [u32],
8494    ) {
8495        let id = *next;
8496        *next += 1;
8497        scope_pass_b(c, id, next, def_block, sizes, within, total);
8498    }
8499    fn ex_at(
8500        e: &Expr,
8501        at: u32,
8502        def_block: &[u32],
8503        sizes: &[u32],
8504        within: &mut [u32],
8505        total: &mut [u32],
8506    ) {
8507        visit_exprs(e, &mut |x| {
8508            if let Expr::Local { var, .. } = x {
8509                cnt(*var, at, def_block, sizes, within, total);
8510            }
8511        });
8512    }
8513    match st {
8514        Stmt::For {
8515            init,
8516            cond,
8517            update,
8518            body,
8519        } => {
8520            let id = *next;
8521            *next += 1;
8522            for x in init {
8523                scope_pass_b(x, id, next, def_block, sizes, within, total);
8524            }
8525            if let Some(c) = cond {
8526                ex_at(c, id, def_block, sizes, within, total);
8527            }
8528            for u in update {
8529                ex_at(u, id, def_block, sizes, within, total);
8530            }
8531            scoped(body, next, def_block, sizes, within, total);
8532        }
8533        Stmt::TryWithResources {
8534            resources,
8535            body,
8536            catches,
8537            finally,
8538        } => {
8539            let id = *next;
8540            *next += 1;
8541            for r in resources {
8542                scope_pass_b(r, id, next, def_block, sizes, within, total);
8543            }
8544            scoped(body, next, def_block, sizes, within, total);
8545            for c in catches {
8546                scoped(&c.body, next, def_block, sizes, within, total);
8547            }
8548            if let Some(f) = finally {
8549                scoped(f, next, def_block, sizes, within, total);
8550            }
8551        }
8552        _ => {
8553            stmt_shallow_vars(st, &mut |v| cnt(v, cur, def_block, sizes, within, total));
8554            match st {
8555                Stmt::Block(v) => {
8556                    for x in v {
8557                        scope_pass_b(x, cur, next, def_block, sizes, within, total);
8558                    }
8559                }
8560                Stmt::If {
8561                    then_stmt,
8562                    else_stmt,
8563                    ..
8564                } => {
8565                    scoped(then_stmt, next, def_block, sizes, within, total);
8566                    if let Some(e) = else_stmt {
8567                        scoped(e, next, def_block, sizes, within, total);
8568                    }
8569                }
8570                Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => {
8571                    scoped(body, next, def_block, sizes, within, total)
8572                }
8573                Stmt::ForEach { body, .. } => {
8574                    scoped(body, next, def_block, sizes, within, total)
8575                }
8576                Stmt::Switch { cases, default, .. } => {
8577                    for c in cases {
8578                        let id = *next;
8579                        *next += 1;
8580                        for x in &c.body {
8581                            if matches!(x, Stmt::Block(_)) {
8582                                scoped(x, next, def_block, sizes, within, total);
8583                            } else {
8584                                scope_pass_b(x, id, next, def_block, sizes, within, total);
8585                            }
8586                        }
8587                    }
8588                    if let Some(d) = default {
8589                        scoped(d, next, def_block, sizes, within, total);
8590                    }
8591                }
8592                Stmt::Try {
8593                    body,
8594                    catches,
8595                    finally,
8596                } => {
8597                    scoped(body, next, def_block, sizes, within, total);
8598                    for c in catches {
8599                        scoped(&c.body, next, def_block, sizes, within, total);
8600                    }
8601                    if let Some(f) = finally {
8602                        scoped(f, next, def_block, sizes, within, total);
8603                    }
8604                }
8605                Stmt::Synchronized { body, .. } => {
8606                    scoped(body, next, def_block, sizes, within, total)
8607                }
8608                Stmt::Labeled { body, .. } => {
8609                    scope_pass_b(body, cur, next, def_block, sizes, within, total)
8610                }
8611                _ => {}
8612            }
8613        }
8614    }
8615}
8616
8617/// Declaration hygiene: every used var that has no LocalDef gets a bare
8618/// declaration at the top; duplicate LocalDefs demote to assignments.
8619/// In an enum's `<clinit>`, every constant's assignment
8620/// (`Self.FIELD = new Self("NAME", ordinal, ...)`) is compiler-mandated
8621/// boilerplate — when the class renders as a true `enum` declaration the
8622/// constants live in the header and these assignments must go. Only
8623/// ACC_ENUM-flagged static fields of the class itself are touched; the
8624/// `$VALUES` array assignment stays (a static block in an enum is legal).
8625pub fn strip_enum_const_inits(s: &mut Stmt, class: &crate::PoolClass) {
8626    let const_fields: jdc_core::FxHashSet<&str> = class
8627        .static_fields
8628        .iter()
8629        .filter(|f| f.access & crate::access::ACC_ENUM != 0)
8630        .map(|f| f.name.as_ref())
8631        .collect();
8632    if const_fields.is_empty() {
8633        return;
8634    }
8635    if let Stmt::Block(stmts) = s {
8636        stmts.retain(|st| {
8637            !matches!(
8638                st,
8639                Stmt::ExprStmt(Expr::Assign { target, value, .. })
8640                    if matches!(&**target,
8641                        Expr::Field { cls, name, is_static: true, .. }
8642                            if cls.as_ref() == class.name
8643                                && const_fields.contains(&**name))
8644                        && matches!(&**value, Expr::New { cls: ncls, .. } if ncls.as_ref() == class.name)
8645            )
8646        });
8647    }
8648}
8649
8650/// Java requires the `super(...)`/`this(...)` delegation to be the FIRST
8651/// statement of a constructor. d8/R8 order the outer-reference capture
8652/// (`this.b = p1;`) or other field writes before the invokesuper in the
8653/// bytecode, which lifts as-is into an uncompilable statement order —
8654/// hoist the bare delegation call to position 0 (jadx does the same).
8655/// Does the expression touch `this` (explicit, implicit-instance member
8656/// access, or super)? Such a def cannot be inlined into a delegation
8657/// that must run BEFORE the supertype constructor.
8658fn contains_this_access(e: &Expr) -> bool {
8659    let mut hit = false;
8660    visit_exprs(e, &mut |x| {
8661        if hit {
8662            return;
8663        }
8664        match x {
8665            Expr::This => hit = true,
8666            Expr::Field { owner, is_static: false, .. } => {
8667                if owner.is_none() || matches!(&**owner.as_ref().unwrap(), Expr::This) {
8668                    hit = true;
8669                }
8670            }
8671            Expr::Method { owner, is_static: false, .. } => {
8672                if owner.is_none() || matches!(&**owner.as_ref().unwrap(), Expr::This) {
8673                    hit = true;
8674                }
8675            }
8676            _ => {}
8677        }
8678    });
8679    hit
8680}
8681
8682/// Inline the transitive straight-line defs of a delegation call's
8683/// arg locals into the call, so hoisting it to position 0 leaves no
8684/// dangling name (lark ProtoAdapter's `super(str)` with `str` defined
8685/// BELOW the hoist point — 找不到符号 变量 str). Returns false when any
8686/// arg var has no inlineable def (param-less, missing, this-touching,
8687/// or cyclic) — the caller then leaves the call in place: a
8688/// not-first-statement error is cheaper than an unresolved name that
8689/// poisons every downstream type.
8690/// Any occurrence of `var` (read OR assign-target) in `stmts` — the
8691/// suffix-use test for dropping a consumed def: count_locals_stmts only
8692/// sees reads, and a suffix WRITE to a dropped declaration makes
8693/// ensure_declared re-add `Type v;` at the method top — BEFORE the
8694/// hoisted this()/super() ("对this的调用必须是构造器中的第一个语句",
8695/// lark +587 the read-only test caused).
8696fn var_occurs_in(stmts: &[Stmt], var: u32) -> bool {
8697    stmts.iter().any(|st| {
8698        let mut hit = false;
8699        visit_stmt_exprs_ro(st, &mut |e| {
8700            if !hit {
8701                visit_exprs(e, &mut |x| {
8702                    if let Expr::Local { var: v, .. } = x {
8703                        if *v == var {
8704                            hit = true;
8705                        }
8706                    }
8707                });
8708            }
8709        });
8710        hit
8711    })
8712}
8713
8714// ---------------------------------------------------------------------------
8715// Branched-delegation ctors → static resolver helper.
8716// ---------------------------------------------------------------------------
8717
8718/// A synthetic static method extracted from a constructor whose
8719/// delegation is preceded by computation (the Kotlin default-arg /
8720/// conditional-bridge shape). Rendered by classdec after the real
8721/// methods; the ctor becomes a single first-statement delegation whose
8722/// carrier arg is the helper call.
8723pub struct CtorHelper {
8724    pub name: String,
8725    pub ret: JavaType,
8726    pub params: Vec<(JavaType, String)>,
8727    pub body: Stmt,
8728    pub vt: VarTable,
8729}
8730
8731fn ctor_helpers() -> &'static std::sync::Mutex<std::collections::HashMap<String, Vec<CtorHelper>>> {
8732    static H: std::sync::OnceLock<std::sync::Mutex<std::collections::HashMap<String, Vec<CtorHelper>>>> =
8733        std::sync::OnceLock::new();
8734    H.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::default()))
8735}
8736
8737/// Synthetic relay ctor for the FULL mode of
8738/// extract_branched_delegation_helper: `private C(Object[] h$relay) {
8739/// this/super((T0) h$relay[0], ..); }` — the public ctor becomes
8740/// `this(resolve$X(params))`, both delegations first-statement legal.
8741pub struct CtorRelay {
8742    /// The delegation target's formal types (the casts of the unpack).
8743    pub formals: Vec<JavaType>,
8744    pub is_super: bool,
8745}
8746
8747type RelayMap = std::collections::HashMap<String, Vec<(String, CtorRelay)>>;
8748
8749fn ctor_relays() -> &'static std::sync::Mutex<RelayMap> {
8750    static H: std::sync::OnceLock<std::sync::Mutex<RelayMap>> = std::sync::OnceLock::new();
8751    H.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::default()))
8752}
8753
8754/// Take (and clear) the relay ctors registered for `class` — sorted by
8755/// the originating ctor descriptor for deterministic emission.
8756pub fn take_ctor_relays(class: &str) -> Vec<(String, CtorRelay)> {
8757    let mut m = ctor_relays().lock().unwrap_or_else(|e| e.into_inner());
8758    let mut v = m.remove(class).unwrap_or_default();
8759    v.sort_by(|a, b| a.0.cmp(&b.0));
8760    v
8761}
8762
8763fn array_init(args: Vec<Expr>) -> Expr {
8764    Expr::NewArray {
8765        elem: TypeRef::J(JavaType::Object(std::sync::Arc::from("java/lang/Object"))),
8766        dims: Vec::new(),
8767        trailing_dims: 0,
8768        init: Some(args),
8769    }
8770}
8771
8772/// Deterministic helper name: FNV-1a over (class, ctor descriptor) —
8773/// worker-thread registration order must not leak into the output.
8774fn helper_name(class: &str, desc: &str) -> String {
8775    let mut h: u64 = 0xcbf29ce484222325;
8776    for b in class.as_bytes().iter().chain(b"::").chain(desc.as_bytes()) {
8777        h ^= *b as u64;
8778        h = h.wrapping_mul(0x100000001b3);
8779    }
8780    format!("resolve${:x}", (h >> 16) & 0xffff_ffff)
8781}
8782
8783/// Drain the helpers registered for a class (render-time). Sorted by
8784/// name: registration order is worker-scheduling dependent.
8785pub fn take_ctor_helpers(class: &str) -> Vec<CtorHelper> {
8786    let mut m = ctor_helpers().lock().unwrap_or_else(|e| e.into_inner());
8787    let mut v = m.remove(class).unwrap_or_default();
8788    v.sort_by(|a, b| a.name.cmp(&b.name));
8789    v
8790}
8791
8792fn strip_delegations_deep(stmts: &mut Vec<Stmt>) {
8793    stmts.retain(|s| !is_bare_ctor_call(s));
8794    for s in stmts.iter_mut() {
8795        strip_delegation_one(s);
8796    }
8797}
8798
8799fn strip_delegation_one(st: &mut Stmt) {
8800    if is_bare_ctor_call(st) {
8801        *st = Stmt::Block(Vec::new());
8802        return;
8803    }
8804    match st {
8805        Stmt::Block(v) => strip_delegations_deep(v),
8806        Stmt::If { then_stmt, else_stmt, .. } => {
8807            strip_delegation_one(then_stmt);
8808            if let Some(e) = else_stmt {
8809                strip_delegation_one(e);
8810            }
8811        }
8812        Stmt::While { body, .. }
8813        | Stmt::DoWhile { body, .. }
8814        | Stmt::ForEach { body, .. }
8815        | Stmt::Synchronized { body, .. }
8816        | Stmt::Labeled { body, .. } => strip_delegation_one(body),
8817        Stmt::For { init, body, .. } => {
8818            strip_delegations_deep(init);
8819            strip_delegation_one(body);
8820        }
8821        Stmt::Switch { cases, default, .. } => {
8822            for c in cases.iter_mut() {
8823                strip_delegations_deep(&mut c.body);
8824            }
8825            if let Some(d) = default {
8826                strip_delegation_one(d);
8827            }
8828        }
8829        Stmt::Try { body, catches, finally } | Stmt::TryWithResources { body, catches, finally, .. } => {
8830            strip_delegation_one(body);
8831            for c in catches.iter_mut() {
8832                strip_delegation_one(&mut c.body);
8833            }
8834            if let Some(f) = finally {
8835                strip_delegation_one(f);
8836            }
8837        }
8838        _ => {}
8839    }
8840}
8841
8842#[allow(clippy::ptr_arg)]
8843fn rewrite_bare_returns(stmts: &mut Vec<Stmt>, val: &Expr) {
8844    for s in stmts.iter_mut() {
8845        rewrite_bare_returns_one(s, val);
8846    }
8847}
8848
8849fn rewrite_bare_returns_one(st: &mut Stmt, val: &Expr) {
8850    match st {
8851        Stmt::Return(None) => *st = Stmt::Return(Some(val.clone())),
8852        Stmt::Block(v) => rewrite_bare_returns(v, val),
8853        Stmt::If { then_stmt, else_stmt, .. } => {
8854            rewrite_bare_returns_one(then_stmt, val);
8855            if let Some(e) = else_stmt {
8856                rewrite_bare_returns_one(e, val);
8857            }
8858        }
8859        Stmt::While { body, .. }
8860        | Stmt::DoWhile { body, .. }
8861        | Stmt::ForEach { body, .. }
8862        | Stmt::Synchronized { body, .. }
8863        | Stmt::Labeled { body, .. } => rewrite_bare_returns_one(body, val),
8864        Stmt::For { init, body, .. } => {
8865            rewrite_bare_returns(init, val);
8866            rewrite_bare_returns_one(body, val);
8867        }
8868        Stmt::Switch { cases, default, .. } => {
8869            for c in cases.iter_mut() {
8870                rewrite_bare_returns(&mut c.body, val);
8871            }
8872            if let Some(d) = default {
8873                rewrite_bare_returns_one(d, val);
8874            }
8875        }
8876        Stmt::Try { body, catches, finally } | Stmt::TryWithResources { body, catches, finally, .. } => {
8877            rewrite_bare_returns_one(body, val);
8878            for c in catches.iter_mut() {
8879                rewrite_bare_returns_one(&mut c.body, val);
8880            }
8881            if let Some(f) = finally {
8882                rewrite_bare_returns_one(f, val);
8883            }
8884        }
8885        _ => {}
8886    }
8887}
8888
8889fn collect_def_vars(stmts: &[Stmt], out: &mut jdc_core::FxHashSet<u32>) {
8890    for st in stmts {
8891        match st {
8892            Stmt::LocalDef { var, .. } => {
8893                out.insert(*var);
8894            }
8895            Stmt::ExprStmt(Expr::Assign { target, .. }) => {
8896                if let Expr::Local { var, .. } = &**target {
8897                    out.insert(*var);
8898                }
8899            }
8900            Stmt::Block(v) => collect_def_vars(v, out),
8901            Stmt::If { then_stmt, else_stmt, .. } => {
8902                collect_def_vars(std::slice::from_ref(then_stmt.as_ref()), out);
8903                if let Some(e) = else_stmt {
8904                    collect_def_vars(std::slice::from_ref(e.as_ref()), out);
8905                }
8906            }
8907            Stmt::While { body, .. }
8908            | Stmt::DoWhile { body, .. }
8909            | Stmt::Synchronized { body, .. }
8910            | Stmt::Labeled { body, .. } => {
8911                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
8912            }
8913            Stmt::ForEach { var, body, .. } => {
8914                out.insert(*var);
8915                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
8916            }
8917            Stmt::For { init, body, .. } => {
8918                collect_def_vars(init, out);
8919                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
8920            }
8921            Stmt::Switch { cases, default, .. } => {
8922                for c in cases {
8923                    collect_def_vars(&c.body, out);
8924                }
8925                if let Some(d) = default {
8926                    collect_def_vars(std::slice::from_ref(d.as_ref()), out);
8927                }
8928            }
8929            Stmt::Try { body, catches, finally }
8930            | Stmt::TryWithResources { body, catches, finally, .. } => {
8931                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
8932                for c in catches {
8933                    out.insert(c.var);
8934                    collect_def_vars(std::slice::from_ref(c.body.as_ref()), out);
8935                }
8936                if let Some(f) = finally {
8937                    collect_def_vars(std::slice::from_ref(f.as_ref()), out);
8938                }
8939            }
8940            _ => {}
8941        }
8942    }
8943}
8944
8945/// v1 extraction is limited to Block/If/straight-line computation (the
8946/// Kotlin default-arg shape); loops/switch/try in the extraction bail.
8947fn extraction_shape_ok(stmts: &[Stmt]) -> bool {
8948    stmts.iter().all(|st| match st {
8949        Stmt::Block(v) => extraction_shape_ok(v),
8950        Stmt::LocalDef { .. }
8951        | Stmt::ExprStmt(_)
8952        | Stmt::Return(_)
8953        | Stmt::Throw(_)
8954        | Stmt::Assert { .. }
8955        | Stmt::Break(_)
8956        | Stmt::Continue(_) => true,
8957        Stmt::If { then_stmt, else_stmt, .. } => {
8958            ok_one(then_stmt)
8959                && else_stmt.as_deref().map(ok_one).unwrap_or(true)
8960        }
8961        Stmt::While { body, .. }
8962        | Stmt::DoWhile { body, .. }
8963        | Stmt::ForEach { body, .. }
8964        | Stmt::Synchronized { body, .. } => ok_one(body),
8965        Stmt::Labeled { body, .. } => ok_one(body),
8966        Stmt::For { init, body, .. } => extraction_shape_ok(init) && ok_one(body),
8967        Stmt::Switch { cases, default, .. } => {
8968            cases.iter().all(|c| extraction_shape_ok(&c.body))
8969                && default.as_deref().map(ok_one).unwrap_or(true)
8970        }
8971        Stmt::Try { body, catches, finally }
8972        | Stmt::TryWithResources { body, catches, finally, .. } => {
8973            ok_one(body)
8974                && catches.iter().all(|c| ok_one(&c.body))
8975                && finally.as_deref().map(ok_one).unwrap_or(true)
8976        }
8977        // Label/Goto (unstructured), MonitorEnter/Exit (raw), ClassDecl
8978        // (local class), TernaryValue, Raw: stay out of v2 extraction.
8979        _ => false,
8980    })
8981}
8982
8983fn ok_one(st: &Stmt) -> bool {
8984    extraction_shape_ok(std::slice::from_ref(st))
8985}
8986
8987fn static_safe(stmts: &[Stmt], this_id: Option<u32>) -> bool {
8988    let mut ok = true;
8989    for st in stmts {
8990        visit_stmt_exprs_ro(st, &mut |e| {
8991            visit_exprs(e, &mut |x| {
8992                match x {
8993                    Expr::This => ok = false,
8994                    // ddc's IR usually carries `this` as the var-0
8995                    // PARAM local, not the Expr::This variant — a
8996                    // helper body reading it dangles (the this param
8997                    // is excluded from the helper's param list) AND
8998                    // renders 无法从静态上下文中引用非静态 (weixin y9/i).
8999                    Expr::Local { var, .. } => {
9000                        if Some(*var) == this_id {
9001                            ok = false;
9002                        }
9003                    }
9004                    // implicit-this member access / call
9005                    Expr::Field { owner: None, is_static: false, .. } => ok = false,
9006                    Expr::Method { owner: None, is_static: false, .. } => ok = false,
9007                    _ => {}
9008                }
9009            });
9010        });
9011    }
9012    ok
9013}
9014
9015/// The bytedance PatchProxy hotfix guard in <clinit>:
9016/// `if (vsChange != null) { PatchProxyResult p = PatchProxy.proxy(..);
9017/// if (p.isSupported) { return; } }` — a bare `return;` inside a STATIC
9018/// INITIALIZER is illegal Java (javac: 返回外部方法; videocut ×28,029 +
9019/// news_article ×19,058 — sampled 400/400 this shape). The patch path
9020/// is dead in a fresh compile (the IVsChange field is null), so the
9021/// whole leading guard statement strips, leaving the real static inits —
9022/// the <clinit> counterpart of the Titan/Robust ctor-guard strips.
9023/// Gate: FIRST top-level stmt is an If with no else whose body holds
9024/// both a bare return and a *PatchProxy* call.
9025pub fn strip_clinit_hotfix_guard(body: &mut Stmt) {
9026    // The structurer can wrap the sequence in a nested singleton block
9027    // (the enum-clinit path flattens for the same reason); the normal
9028    // emit_method path does not.
9029    if let Stmt::Block(vs) = body {
9030        flatten_top_blocks(vs);
9031    }
9032    let Stmt::Block(stmts) = body else { return };
9033    // The guard may sit behind hoisted bare declarations (`int[] v8x;`
9034    // from ensure_declared) — find the first non-decl statement.
9035    let mut gi = 0usize;
9036    while gi < stmts.len() && matches!(&stmts[gi], Stmt::LocalDef { init: None, .. }) {
9037        gi += 1;
9038    }
9039    let Some(first) = stmts.get(gi) else { return };
9040    let Stmt::If { then_stmt, else_stmt, .. } = first else {
9041        return;
9042    };
9043    if else_stmt.is_some() {
9044        return;
9045    }
9046    let mut has_ret = false;
9047    let mut has_proxy = false;
9048    let c = then_stmt.as_ref();
9049    walk_all(c, &mut |st| {
9050        if matches!(st, Stmt::Return(None)) {
9051            has_ret = true;
9052        }
9053    });
9054    let mut cc = c.clone();
9055    walk_stmt_exprs(&mut cc, &mut |e| {
9056        deep_rewrite(e, &mut |x| {
9057            if let Expr::Method { cls, .. } = x {
9058                if cls.contains("PatchProxy") {
9059                    has_proxy = true;
9060                }
9061            }
9062        });
9063    });
9064    if has_ret && has_proxy {
9065        stmts.remove(gi);
9066    }
9067}
9068
9069/// Vendor hotfix guards in CTORS that wrap a delegation:
9070/// `if (__ != null) { Object[] v = ..; ConstructorCode proxy =
9071/// ConstructorCode.proxy(..); if (proxy != null) { this(unpack(v));
9072/// proxy.afterSuper(this); return; } }` ahead of the real top-level
9073/// delegation (alipay InstantRun — ctor-not-first ×1,436; the same
9074/// shape as the already-stripped Baidu Titan / bytedance clinit /
9075/// Meituan Robust guards). The patch path is dead in a fresh compile
9076/// (the redirect field is null), and Java cannot place the wrapped
9077/// delegation legally — strip the WHOLE guard statement.
9078///
9079/// VENDOR-GATED on purpose: a conditional delegation with a top-level
9080/// fallback (`if (c) { this(a); return; } this(b);`) is legitimate
9081/// bytecode whose guard must NOT be dropped. The gate: the guard
9082/// subtree mentions a known hotfix framework type (hotfix / instantrun
9083/// / robust / titan / PatchProxy / ChangeQuickRedirect / ConstructorCode
9084/// in any method-owner, field owner+type, cast, or new).
9085/// First LEAF statement after unwrapping nested `Stmt::Block` wrappers —
9086/// the lifted ctor body reaches the ctor chain with the delegation and
9087/// the hotfix guard each wrapped in plain Blocks (the flattening that
9088/// produces the rendered shape runs later).
9089pub(crate) fn first_leaf_stmt(s: &Stmt) -> &Stmt {
9090    match s {
9091        Stmt::Block(v) if !v.is_empty() => first_leaf_stmt(&v[0]),
9092        other => other,
9093    }
9094}
9095
9096/// Vendor hotfix guards in CTORS that wrap a delegation:
9097/// `if (__ != null) { Object[] v = ..; ConstructorCode proxy =
9098/// ConstructorCode.proxy(..); if (proxy != null) { this(unpack(v));
9099/// proxy.afterSuper(this); return; } }` ahead of the real top-level
9100/// delegation (alipay InstantRun — ctor-not-first ×1,436, nearly all in
9101/// FALLBACK ENUM ctors; the same shape as the already-stripped Baidu
9102/// Titan / bytedance clinit / Meituan Robust guards). The patch path is
9103/// dead in a fresh compile (the redirect field is null), and Java cannot
9104/// place the wrapped delegation legally — strip the WHOLE guard
9105/// statement (at this pipeline stage it arrives Block-wrapped, and its
9106/// branches are still un-inverted: `If(then=∅, else=guard-body)`).
9107///
9108/// VENDOR-GATED on purpose: a conditional delegation with a top-level
9109/// fallback (`if (c) { this(a); return; } this(b);`) is legitimate
9110/// bytecode whose guard must NOT be dropped. The gate: the guard
9111/// subtree mentions a known hotfix framework type (hotfix / instantrun
9112/// / robust / titan / PatchProxy / ChangeQuickRedirect / ConstructorCode
9113/// in any method-owner, field owner+type, cast, or new).
9114pub fn strip_ctor_hotfix_guards(body: &mut Stmt) {
9115    fn marker_hit(e: &Expr) -> bool {
9116        let has = |n: &str| {
9117            let l = n.to_ascii_lowercase();
9118            l.contains("hotfix")
9119                || l.contains("instantrun")
9120                || l.contains("robust")
9121                || l.contains("titan")
9122                || l.contains("patchproxy")
9123                || l.contains("changequickredirect")
9124                || l.contains("constructorcode")
9125        };
9126        let mut hit = false;
9127        visit_exprs(e, &mut |x| {
9128            match x {
9129                Expr::Method { cls, .. } | Expr::New { cls, .. } | Expr::Field { cls, .. } => {
9130                    if has(cls) {
9131                        hit = true;
9132                    }
9133                }
9134                _ => {}
9135            }
9136            if let Expr::Field { ty, .. } = x {
9137                if let TypeRef::J(JavaType::Object(n)) = ty {
9138                    if has(n) {
9139                        hit = true;
9140                    }
9141                }
9142            }
9143        });
9144        hit
9145    }
9146    // A top-level statement that IS a hotfix guard: unwrap single-child
9147    // Block chains down to an If whose subtree holds BOTH a delegation
9148    // and a vendor marker.
9149    fn is_guard(st: &Stmt) -> bool {
9150        let mut cur = st;
9151        loop {
9152            match cur {
9153                Stmt::Block(v) if v.len() == 1 => cur = &v[0],
9154                Stmt::If { .. } => break,
9155                _ => return false,
9156            }
9157        }
9158        let mut has_del = false;
9159        walk_all(cur, &mut |s2| {
9160            if is_bare_ctor_call(s2) {
9161                has_del = true;
9162            }
9163        });
9164        if !has_del {
9165            return false;
9166        }
9167        let mut has_marker = false;
9168        visit_stmt_exprs_ro(cur, &mut |e| {
9169            if marker_hit(e) {
9170                has_marker = true;
9171            }
9172        });
9173        has_marker
9174    }
9175    // Strip at EVERY Block level: the lifted body arrives either as
9176    // [Block(guard), Block(del, ..)] (AtomicDataCollector) or as ONE big
9177    // Block [decls, guard, prelude-computes, del] (compose TextStyle).
9178    // No outside-delegation requirement: the vendor gate already proves
9179    // the statement is dead hotfix machinery (the redirect field is null
9180    // in a fresh compile — the same assumption the Titan/PatchProxy
9181    // strips run on), and the dex's fresh path IS whatever remains after
9182    // the guard. rpc/a.java: the real super() sits inside a switch arm —
9183    // requiring an unconditional outside delegation kept the guard (and
9184    // its illegal pre-super field writes) alive.
9185    fn strip_in(stmts: &mut Vec<Stmt>) {
9186        stmts.retain(|st| !is_guard(st));
9187        for st in stmts.iter_mut() {
9188            if let Stmt::Block(v) = st {
9189                strip_in(v);
9190            }
9191        }
9192    }
9193    let Stmt::Block(stmts) = body else { return };
9194    strip_in(stmts);
9195}
9196
9197/// Repeated identical branch-site delegations with param-only args
9198/// (weixin appbrand/zc: bare `super()` at four branch ends, each
9199/// followed by field-write tails) carry no computed carrier, so the
9200/// helper extraction has nothing to return — but they need no helper:
9201/// every delegating path runs the SAME call, so hoist one copy to
9202/// position 0 and strip the sites. The per-branch tails stay in their
9203/// branches and now legally follow the first-statement delegation.
9204/// Same reordering approximation fix_ctor_super_first already ships for
9205/// the straight-line case (paths that threw BEFORE delegating now run
9206/// the delegation first — Object-super, the dominant shape, is inert).
9207/// Gate: every del deep-equal and reading params only (a computed local
9208/// would dangle at position 0 — that is the helper extractor's job).
9209pub fn hoist_branch_delegations(body: &mut Stmt, vt: &VarTable) {
9210    let Stmt::Block(stmts) = body else { return };
9211    if stmts.first().is_some_and(is_bare_ctor_call) {
9212        return; // already first
9213    }
9214    let mut dels: Vec<Expr> = Vec::new();
9215    for st in stmts.iter() {
9216        visit_stmt_exprs_ro(st, &mut |e| {
9217            if is_delegation_expr(e) {
9218                dels.push(e.clone());
9219            }
9220        });
9221    }
9222    if dels.is_empty() {
9223        return;
9224    }
9225    if !dels.iter().all(|d| *d == dels[0]) {
9226        return;
9227    }
9228    let mut bad = false;
9229    visit_exprs(&dels[0], &mut |x| {
9230        if let Expr::Local { var, .. } = x {
9231            if (*var as usize) >= vt.vars.len() || !vt.vars[*var as usize].is_param {
9232                bad = true;
9233            }
9234        }
9235    });
9236    if bad {
9237        return;
9238    }
9239    let del = dels[0].clone();
9240    strip_delegations_deep(stmts);
9241    stmts.insert(0, Stmt::ExprStmt(del));
9242}
9243
9244/// The branched-delegation ctor family ("对this的调用必须是构造器中的
9245/// 第一个语句", ~2k across corpora): R8 renders a Kotlin default-arg /
9246/// conditional-bridge ctor as computation statements (straight-line plus
9247/// mask-guarded overrides) with every control path ending in a
9248/// `this(a, b, carrier)` delegation. Java cannot express statements
9249/// before the delegation, and unlike the linear-chain case the carrier's
9250/// value is computed by multi-statement branched code that cannot fold
9251/// into a ternary. Extract the computation into a synthetic
9252/// `private static T resolve$X(<ctor params>)` and make the ctor a
9253/// single first-statement `this(a, b, resolve$X(..))` — compilable AND
9254/// faithful (jadx renders the un-compilable statements-before-this form
9255/// for this family).
9256///
9257/// Two carrier modes:
9258/// - Single (v1): every delegation textually identical and exactly one
9259///   non-param local (the carrier) read exactly once across the args.
9260/// - Position (v2): delegations differ at exactly ONE arg position k,
9261///   every site passing a bare non-param local there (per-branch
9262///   carriers, lark BaseProtocol$ReliablePushList's v22_g1/v22x). Each
9263///   stripped delegation's own carrier fills the return that follows it;
9264///   merge-point returns fall back to the primary site's carrier.
9265///
9266/// The top-level primary delegation is optional in BOTH modes — when
9267/// every path delegates inside branches (weixin ln1/b1's super() in
9268/// if/try/catch) the whole body is extracted and any del serves as the
9269/// template (all non-k parts identical).
9270///
9271/// Shared gates (all conservative, bail keeps today's behavior): not an
9272/// enum (trace-param strip shifts render params); nested only when the
9273/// class RENDERS static-nested (member-inner ctors lose the outer param
9274/// at render); computation is Block/If/straight-line only; no
9275/// `this`-dependence; no local defined in the extraction used after the
9276/// primary.
9277pub fn extract_branched_delegation_helper(
9278    body: &mut Stmt,
9279    vt: &VarTable,
9280    pool: &DexPool,
9281    class: &crate::PoolClass,
9282    desc_str: &str,
9283) {
9284    if class.is_enum() {
9285        return;
9286    }
9287    // Member-inner ctors lose the synthetic outer param at render
9288    // (qualified-new absorbs it), so the helper's param list would no
9289    // longer match the rendered ctor. STATIC nested classes render all
9290    // params like top-level ones (lark BaseProtocol$PushList family).
9291    if class.nesting.enclosing_class.is_some() && !crate::ctx::nested_is_static(pool, class) {
9292        return;
9293    }
9294    let Stmt::Block(stmts) = body else { return };
9295    let mut dels: Vec<Expr> = Vec::new();
9296    for st in stmts.iter() {
9297        visit_stmt_exprs_ro(st, &mut |e| {
9298            if is_delegation_expr(e) {
9299                dels.push(e.clone());
9300            }
9301        });
9302    }
9303    if dels.is_empty() {
9304        return;
9305    }
9306    if dels.len() == 1 && stmts.first().is_some_and(is_bare_ctor_call) {
9307        return; // already first
9308    }
9309    let template = dels[0].clone();
9310    let Expr::Method { args: t_args, .. } = &template else {
9311        return;
9312    };
9313    // Carrier discovery: Single (all dels identical, one non-param local
9314    // read once) or Position (dels differ at exactly one arg slot, every
9315    // site a bare non-param local of one erased type).
9316    enum Mode {
9317        Single(u32),
9318        Position(usize),
9319        /// Every path delegates with the SAME target signature but its
9320        /// own arg expressions: the whole computation moves to the
9321        /// helper (each delegation becomes `return new Object[]{its
9322        /// args}`), the public ctor becomes `this(resolve$X(params))`
9323        /// and a synthetic `private C(Object[] h)` relay unpacks the
9324        /// carriers into the one legal first-statement delegation.
9325        Full,
9326    }
9327    // Full-mode gate: uniform delegation target (this/super, class,
9328    // descriptor), non-empty args, no pre-existing (Object[]) ctor,
9329    // and — the relay signature is per-CLASS unique — only the
9330    // lexicographically least <init> descriptor of the class may take
9331    // the relay (deterministic across parallel workers; a second
9332    // branched ctor in one class keeps its honest error).
9333    let full_gate = (|| -> Option<()> {
9334        let Expr::Method {
9335            args: a0,
9336            desc: d0,
9337            is_super: s0,
9338            cls: c0,
9339            ..
9340        } = &dels[0]
9341        else {
9342            return None;
9343        };
9344        if a0.is_empty() {
9345            return None;
9346        }
9347        for d in &dels[1..] {
9348            match d {
9349                Expr::Method {
9350                    args,
9351                    desc,
9352                    is_super,
9353                    cls,
9354                    ..
9355                } if args.len() == a0.len()
9356                    && **desc == **d0
9357                    && is_super == s0
9358                    && cls == c0 => {}
9359                _ => return None,
9360            }
9361        }
9362        if class
9363            .all_methods()
9364            .any(|m| &*m.name == "<init>" && &*m.desc == "([Ljava/lang/Object;)V")
9365        {
9366            return None;
9367        }
9368        Some(())
9369    })()
9370    .is_some();
9371    let mode = if dels.iter().all(|d| *d == dels[0]) {
9372        let mut carrier: Option<u32> = None;
9373        let mut reads = 0usize;
9374        let mut multi = false;
9375        for a in t_args.iter() {
9376            let c = a.clone();
9377            visit_exprs(&c, &mut |x| {
9378                if let Expr::Local { var, .. } = x {
9379                    if (*var as usize) < vt.vars.len() && !vt.vars[*var as usize].is_param {
9380                        reads += 1;
9381                        match carrier {
9382                            Some(v) if v == *var => {}
9383                            Some(_) => multi = true,
9384                            None => carrier = Some(*var),
9385                        }
9386                    }
9387                }
9388            });
9389        }
9390        match carrier {
9391            Some(cv) if !multi && reads == 1 => Mode::Single(cv),
9392            _ if full_gate => Mode::Full,
9393            _ => return,
9394        }
9395    } else {
9396        let positioned: Option<Mode> = (|| {
9397        let arity = t_args.len();
9398        // Uniform arity FIRST: a bare `super()` mixed among arg-carrying
9399        // dels made the single-diff slot k index past a short del's args
9400        // (deepseek v52 / kimi c60.o panics, index-out-of-bounds).
9401        if dels
9402            .iter()
9403            .any(|d| !matches!(d, Expr::Method { args, .. } if args.len() == arity))
9404        {
9405            return None;
9406        }
9407        let mut diff: Vec<usize> = Vec::new();
9408        for j in 0..arity {
9409            if dels.iter().any(|d| match d {
9410                Expr::Method { args, .. } => args.get(j) != t_args.get(j),
9411                _ => true,
9412            }) {
9413                diff.push(j);
9414            }
9415        }
9416        if diff.len() != 1 {
9417            return None;
9418        }
9419        let k = diff[0];
9420        // Non-arg parts AND all other arg positions identical: neutralize
9421        // slot k on every del and compare whole exprs (catches mixed
9422        // this()/super() or different targets).
9423        let mut base0 = dels[0].clone();
9424        if let Expr::Method { args, .. } = &mut base0 {
9425            args[k] = Expr::Const(ConstVal::Null);
9426        }
9427        for d in dels.iter() {
9428            let mut c = d.clone();
9429            if let Expr::Method { args, .. } = &mut c {
9430                args[k] = Expr::Const(ConstVal::Null);
9431            }
9432            if c != base0 {
9433                return None;
9434            }
9435        }
9436        // Slot k: bare non-param local at every site, one erased type,
9437        // and the carrier never read at another position.
9438        let mut carrier_ty: Option<JavaType> = None;
9439        for d in dels.iter() {
9440            let Expr::Method { args, .. } = d else {
9441                return None;
9442            };
9443            let Expr::Local { var, .. } = &args[k] else {
9444                return None;
9445            };
9446            if (*var as usize) >= vt.vars.len() || vt.vars[*var as usize].is_param {
9447                return None;
9448            }
9449            let ty = vt.vars[*var as usize].ty.erased();
9450            match &carrier_ty {
9451                Some(t) if *t == ty => {}
9452                Some(_) => return None,
9453                None => carrier_ty = Some(ty),
9454            }
9455            // Non-k positions must be param/const/static-only — a
9456            // computed local there would have its def extracted into
9457            // the helper and the ctor arg list would dangle (lark
9458            // audiosave/i 找不到符号 ×5-per-site). Also covers the
9459            // carrier leaking into another position.
9460            for (j, a) in args.iter().enumerate() {
9461                if j == k {
9462                    continue;
9463                }
9464                let mut bad = false;
9465                visit_exprs(a, &mut |x| {
9466                    if let Expr::Local { var: v2, .. } = x {
9467                        if (*v2 as usize) >= vt.vars.len() || !vt.vars[*v2 as usize].is_param {
9468                            bad = true;
9469                        }
9470                    }
9471                });
9472                if bad {
9473                    return None;
9474                }
9475            }
9476        }
9477        Some(Mode::Position(k))
9478        })();
9479        match positioned {
9480            Some(m) => m,
9481            None if full_gate => Mode::Full,
9482            None => return,
9483        }
9484    };
9485    let l = stmts.iter().rposition(is_bare_ctor_call);
9486    if l == Some(0) {
9487        return;
9488    }
9489    let (mut extracted, mut tail): (Vec<Stmt>, Vec<Stmt>) = match l {
9490        Some(l) => (stmts[..l].to_vec(), stmts[l + 1..].to_vec()),
9491        None => (stmts.clone(), Vec::new()),
9492    };
9493    if !extraction_shape_ok(&extracted) {
9494        return;
9495    }
9496    // Post-delegation tails (field writes / trace stubs between each
9497    // branch delegation and its return) are CTOR work, not helper
9498    // computation — they commonly touch `this`. When every site's tail
9499    // is identical (and equals the top-level continuation when one
9500    // exists) hoist one copy after the merged delegation; otherwise
9501    // they stay in the helper and the static_safe gate below decides.
9502    let mut site_tails: Vec<Vec<Stmt>> = Vec::new();
9503    {
9504        let mut probe = extracted.clone();
9505        site_tails_walk(&mut probe, &mut site_tails, false);
9506    }
9507    let hoist = !site_tails.is_empty() && {
9508        let n0 = normalize_tail(&site_tails[0]);
9509        site_tails.iter().all(|t| normalize_tail(t) == n0)
9510            && (l.is_none() || normalize_tail(&tail) == n0)
9511    };
9512    if hoist {
9513        site_tails_walk(&mut extracted, &mut Vec::new(), true);
9514        if l.is_none() {
9515            tail = normalize_tail(&site_tails[0]);
9516        }
9517    }
9518    // The delegation the ctor keeps: the top-level primary when present,
9519    // else any del (all non-k parts are identical).
9520    let primary_del: Expr = match l {
9521        Some(l) => match &stmts[l] {
9522            Stmt::ExprStmt(e) => e.clone(),
9523            _ => return,
9524        },
9525        None => dels[0].clone(),
9526    };
9527    let ret = match mode {
9528        Mode::Single(cv) => vt.var(cv).ty.erased(),
9529        Mode::Position(k) => match &primary_del {
9530            Expr::Method { args, .. } => match args.get(k) {
9531                Some(Expr::Local { var, .. }) => vt.var(*var).ty.erased(),
9532                _ => return,
9533            },
9534            _ => return,
9535        },
9536        Mode::Full => JavaType::Array(Box::new(JavaType::Object(std::sync::Arc::from(
9537            "java/lang/Object",
9538        )))),
9539    };
9540    match mode {
9541        Mode::Single(cv) => {
9542            strip_delegations_deep(&mut extracted);
9543            let cv_local = Expr::Local {
9544                var: cv,
9545                ty: vt.var(cv).ty.clone(),
9546            };
9547            rewrite_bare_returns(&mut extracted, &cv_local);
9548            if !always_returns(&extracted) {
9549                extracted.push(Stmt::Return(Some(cv_local)));
9550            }
9551        }
9552        Mode::Position(k) => {
9553            // Each stripped delegation's own slot-k carrier fills the
9554            // next bare return in its list; merge-point returns fall
9555            // back to the primary carrier below.
9556            fill_returns_by_position(&mut extracted, &|args: &[Expr]| args.get(k).cloned());
9557            let fallback = match &primary_del {
9558                Expr::Method { args, .. } => args[k].clone(),
9559                _ => return,
9560            };
9561            rewrite_bare_returns(&mut extracted, &fallback);
9562            if !always_returns(&extracted) {
9563                extracted.push(Stmt::Return(Some(fallback)));
9564            }
9565        }
9566        Mode::Full => {
9567            // Every delegation becomes `return new Object[]{its own
9568            // args}`; merge-point bare returns and the fall-through
9569            // completion take the PRIMARY delegation's args (the
9570            // top-level last del when present — its args are
9571            // top-level-scoped by construction). With no primary,
9572            // every path must already delegate explicitly.
9573            fill_returns_by_position(&mut extracted, &|args: &[Expr]| {
9574                Some(array_init(args.to_vec()))
9575            });
9576            if l.is_none() {
9577                // No top-level primary: every path must delegate. A
9578                // dead tail can still dangle after the shortest
9579                // always-returning prefix (DataHolder: an assign after
9580                // a try whose body returns and whose catch throws) —
9581                // truncate it (unreachable by Java's flow model, so
9582                // sound), else bail.
9583                if !always_returns(&extracted) {
9584                    let cut = (1..=extracted.len())
9585                        .find(|&i| always_returns(&extracted[..i]));
9586                    match cut {
9587                        Some(i) => extracted.truncate(i),
9588                        None => return,
9589                    }
9590                }
9591            }
9592            let fallback_args = match &primary_del {
9593                Expr::Method { args, .. } => args.clone(),
9594                _ => return,
9595            };
9596            let fallback = array_init(fallback_args);
9597            rewrite_bare_returns(&mut extracted, &fallback);
9598            if !always_returns(&extracted) {
9599                extracted.push(Stmt::Return(Some(fallback)));
9600            }
9601        }
9602    }
9603    let this_id = vt
9604        .vars
9605        .iter()
9606        .find(|v| v.is_param && v.name == "this")
9607        .map(|v| v.id);
9608    if !static_safe(&extracted, this_id) {
9609        return;
9610    }
9611    let mut defs: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
9612    collect_def_vars(&extracted, &mut defs);
9613    if defs.iter().any(|v| var_occurs_in(&tail, *v)) {
9614        return;
9615    }
9616    let params: Vec<(u32, JavaType, String)> = vt
9617        .vars
9618        .iter()
9619        .filter(|v| v.is_param && v.name != "this")
9620        .map(|v| (v.id, v.ty.erased(), v.name.clone()))
9621        .collect();
9622    let name = helper_name(&class.name, desc_str);
9623    let call = Expr::Method {
9624        owner: None,
9625        cls: std::sync::Arc::from(class.name.as_str()),
9626        name: std::sync::Arc::from(name.as_str()),
9627        desc: std::sync::Arc::new(jdc_core::types::MethodDescriptor {
9628            ret: ret.clone(),
9629            args: params.iter().map(|p| p.1.clone()).collect(),
9630        }),
9631        args: params
9632            .iter()
9633            .map(|(id, ty, _)| Expr::Local {
9634                var: *id,
9635                ty: TypeRef::J(ty.clone()),
9636            })
9637            .collect(),
9638        is_static: true,
9639        is_interface: false,
9640        is_special: false,
9641        is_super: false,
9642        is_dynamic: false,
9643        type_args: Vec::new(),
9644    };
9645    let mut new_del = primary_del.clone();
9646    match mode {
9647        Mode::Single(cv) => {
9648            deep_rewrite(&mut new_del, &mut |x| {
9649                if let Expr::Local { var, .. } = x {
9650                    if *var == cv {
9651                        *x = call.clone();
9652                    }
9653                }
9654            });
9655        }
9656        Mode::Position(k) => {
9657            let Expr::Method { args, .. } = &mut new_del else {
9658                return;
9659            };
9660            if k >= args.len() {
9661                return;
9662            }
9663            args[k] = call.clone();
9664        }
9665        Mode::Full => {
9666            // The public ctor's one legal statement: `this(resolve$X(p..))`
9667            // — the synthetic relay ctor below unpacks the carriers into
9668            // the uniform target delegation.
9669            new_del = Expr::Method {
9670                owner: None,
9671                cls: std::sync::Arc::from(class.name.as_str()),
9672                name: "<init>".into(),
9673                desc: std::sync::Arc::new(jdc_core::types::MethodDescriptor {
9674                    args: vec![JavaType::Array(Box::new(JavaType::Object(
9675                        std::sync::Arc::from("java/lang/Object"),
9676                    )))],
9677                    ret: JavaType::Void,
9678                }),
9679                args: vec![call.clone()],
9680                is_static: false,
9681                is_interface: false,
9682                is_special: true,
9683                is_super: false,
9684                is_dynamic: false,
9685                type_args: Vec::new(),
9686            };
9687        }
9688    }
9689    {
9690        let mut m = ctor_helpers().lock().unwrap_or_else(|e| e.into_inner());
9691        let v = m.entry(class.name.clone()).or_default();
9692        if !v.iter().any(|h| h.name == name) {
9693            // The extracted body skips the late pipeline polish (it left
9694            // the ctor before cleanup ran) — invert empty thens so the
9695            // helper doesn't render `if (..) {} else {..}`.
9696            let mut hbody = Stmt::Block(extracted);
9697            invert_empty_thens(&mut hbody);
9698            v.push(CtorHelper {
9699                name,
9700                ret: ret.clone(),
9701                params: params.into_iter().map(|(_, ty, nm)| (ty, nm)).collect(),
9702                body: hbody,
9703                vt: vt.clone(),
9704            });
9705            if matches!(mode, Mode::Full) {
9706                let Expr::Method {
9707                    desc: d0,
9708                    is_super: s0,
9709                    ..
9710                } = &primary_del
9711                else {
9712                    return;
9713                };
9714                let mut rm = ctor_relays().lock().unwrap_or_else(|e| e.into_inner());
9715                let rv = rm.entry(class.name.clone()).or_default();
9716                if !rv.iter().any(|(d, _)| d == desc_str) {
9717                    rv.push((
9718                        desc_str.to_string(),
9719                        CtorRelay {
9720                            formals: d0.args.clone(),
9721                            is_super: *s0,
9722                        },
9723                    ));
9724                }
9725            }
9726        } else {
9727            return; // duplicate decompile of the same ctor: keep the first
9728        }
9729    }
9730    *stmts = std::iter::once(Stmt::ExprStmt(new_del)).chain(tail).collect();
9731}
9732
9733/// Position-mode return fill: walk each statement list left-to-right; a
9734/// delegation at slot k hands its carrier to the next bare `return;` in
9735/// the SAME list (the bytecode shape is always `this(..); trace..;
9736/// return;`). With no following return in the list the delegation
9737/// becomes the return itself and the rest of the list (trace stubs only
9738/// — a second delegation there is impossible) is dropped as unreachable.
9739fn fill_returns_by_position(
9740    stmts: &mut Vec<Stmt>,
9741    pick: &dyn Fn(&[Expr]) -> Option<Expr>,
9742) {
9743    let mut i = 0;
9744    while i < stmts.len() {
9745        if is_bare_ctor_call(&stmts[i]) {
9746            let karg = match &stmts[i] {
9747                Stmt::ExprStmt(Expr::Method { args, .. }) => pick(args),
9748                _ => None,
9749            };
9750            if let Some(v) = karg {
9751                let mut j = i + 1;
9752                let mut found = None;
9753                while j < stmts.len() {
9754                    if matches!(stmts[j], Stmt::Return(None)) {
9755                        found = Some(j);
9756                        break;
9757                    }
9758                    if is_bare_ctor_call(&stmts[j]) {
9759                        break; // next delegation owns the next return
9760                    }
9761                    j += 1;
9762                }
9763                match found {
9764                    Some(j) => {
9765                        stmts[j] = Stmt::Return(Some(v));
9766                        stmts.remove(i);
9767                        continue;
9768                    }
9769                    None => {
9770                        stmts[i] = Stmt::Return(Some(v));
9771                        stmts.truncate(i + 1);
9772                        break;
9773                    }
9774                }
9775            }
9776        }
9777        fill_returns_one(&mut stmts[i], pick);
9778        i += 1;
9779    }
9780}
9781
9782fn fill_returns_one(st: &mut Stmt, pick: &dyn Fn(&[Expr]) -> Option<Expr>) {
9783    match st {
9784        Stmt::Block(v) => fill_returns_by_position(v, pick),
9785        Stmt::If { then_stmt, else_stmt, .. } => {
9786            fill_returns_stmt(then_stmt, pick);
9787            if let Some(e) = else_stmt {
9788                fill_returns_stmt(e, pick);
9789            }
9790        }
9791        Stmt::While { body, .. }
9792        | Stmt::DoWhile { body, .. }
9793        | Stmt::ForEach { body, .. }
9794        | Stmt::Synchronized { body, .. }
9795        | Stmt::Labeled { body, .. } => fill_returns_stmt(body, pick),
9796        Stmt::For { init, body, .. } => {
9797            fill_returns_by_position(init, pick);
9798            fill_returns_stmt(body, pick);
9799        }
9800        Stmt::Switch { cases, default, .. } => {
9801            for c in cases.iter_mut() {
9802                fill_returns_by_position(&mut c.body, pick);
9803            }
9804            if let Some(d) = default {
9805                fill_returns_stmt(d, pick);
9806            }
9807        }
9808        Stmt::Try { body, catches, finally } | Stmt::TryWithResources { body, catches, finally, .. } => {
9809            fill_returns_stmt(body, pick);
9810            for c in catches.iter_mut() {
9811                fill_returns_stmt(&mut c.body, pick);
9812            }
9813            if let Some(f) = finally {
9814                fill_returns_stmt(f, pick);
9815            }
9816        }
9817        _ => {}
9818    }
9819}
9820
9821fn fill_returns_stmt(st: &mut Stmt, pick: &dyn Fn(&[Expr]) -> Option<Expr>) {
9822    if is_bare_ctor_call(st) {
9823        // Single-statement branch body (`if (c) this(..);`) — becomes
9824        // the return directly.
9825        if let Stmt::ExprStmt(Expr::Method { args, .. }) = st {
9826            if let Some(v) = pick(args) {
9827                *st = Stmt::Return(Some(v));
9828                return;
9829            }
9830        }
9831    }
9832    fill_returns_one(st, pick);
9833}
9834
9835fn normalize_tail(t: &[Stmt]) -> Vec<Stmt> {
9836    let mut v = t.to_vec();
9837    while matches!(v.last(), Some(Stmt::Return(None))) {
9838        v.pop();
9839    }
9840    v
9841}
9842
9843/// Record (and with `carve`, remove) the statements between each
9844/// delegation and the next bare return in its list — a delegation with
9845/// no following return takes the rest of the list. Single-statement
9846/// branch bodies record an empty tail.
9847fn site_tails_walk(stmts: &mut Vec<Stmt>, out: &mut Vec<Vec<Stmt>>, carve: bool) {
9848    let mut i = 0;
9849    while i < stmts.len() {
9850        if is_bare_ctor_call(&stmts[i]) {
9851            let mut j = i + 1;
9852            let mut end = stmts.len();
9853            while j < stmts.len() {
9854                if matches!(stmts[j], Stmt::Return(None)) {
9855                    end = j;
9856                    break;
9857                }
9858                if is_bare_ctor_call(&stmts[j]) {
9859                    end = j;
9860                    break;
9861                }
9862                j += 1;
9863            }
9864            out.push(stmts[i + 1..end].to_vec());
9865            if carve {
9866                stmts.splice(i + 1..end, []);
9867            }
9868            i += 1;
9869            continue;
9870        }
9871        site_tails_one(&mut stmts[i], out, carve);
9872        i += 1;
9873    }
9874}
9875
9876fn site_tails_one(st: &mut Stmt, out: &mut Vec<Vec<Stmt>>, carve: bool) {
9877    if is_bare_ctor_call(st) {
9878        out.push(Vec::new());
9879        return;
9880    }
9881    match st {
9882        Stmt::Block(v) => site_tails_walk(v, out, carve),
9883        Stmt::If { then_stmt, else_stmt, .. } => {
9884            site_tails_one(then_stmt, out, carve);
9885            if let Some(e) = else_stmt {
9886                site_tails_one(e, out, carve);
9887            }
9888        }
9889        Stmt::While { body, .. }
9890        | Stmt::DoWhile { body, .. }
9891        | Stmt::ForEach { body, .. }
9892        | Stmt::Synchronized { body, .. }
9893        | Stmt::Labeled { body, .. } => site_tails_one(body, out, carve),
9894        Stmt::For { init, body, .. } => {
9895            site_tails_walk(init, out, carve);
9896            site_tails_one(body, out, carve);
9897        }
9898        Stmt::Switch { cases, default, .. } => {
9899            for c in cases.iter_mut() {
9900                site_tails_walk(&mut c.body, out, carve);
9901            }
9902            if let Some(d) = default {
9903                site_tails_one(d, out, carve);
9904            }
9905        }
9906        Stmt::Try { body, catches, finally } | Stmt::TryWithResources { body, catches, finally, .. } => {
9907            site_tails_one(body, out, carve);
9908            for c in catches.iter_mut() {
9909                site_tails_one(&mut c.body, out, carve);
9910            }
9911            if let Some(f) = finally {
9912                site_tails_one(f, out, carve);
9913            }
9914        }
9915        _ => {}
9916    }
9917}
9918
9919/// Can this statement list NOT complete normally? Conservative for
9920/// loops (false); If needs BOTH branches, Switch needs every case plus
9921/// a default. Used to decide whether the extracted helper needs the
9922/// trailing `return carrier;` (appending one after exhaustive branches
9923/// is javac's "unreachable statement" error).
9924fn always_returns(stmts: &[Stmt]) -> bool {
9925    let Some(last) = stmts
9926        .iter()
9927        .rev()
9928        .find(|s| !matches!(s, Stmt::Block(v) if v.is_empty()))
9929    else {
9930        return false;
9931    };
9932    match last {
9933        Stmt::Return(Some(_)) | Stmt::Throw(_) => true,
9934        Stmt::Block(v) => always_returns(v),
9935        Stmt::If { then_stmt, else_stmt, .. } => match else_stmt {
9936            Some(e) => always_returns_one(then_stmt) && always_returns_one(e),
9937            None => false,
9938        },
9939        Stmt::Labeled { body, .. } | Stmt::Synchronized { body, .. } => {
9940            always_returns_one(body)
9941        }
9942        Stmt::Switch { cases, default, .. } => {
9943            default.as_ref().is_some_and(|d| always_returns_one(d))
9944                && cases.iter().all(|c| always_returns(&c.body))
9945        }
9946        Stmt::Try { body, catches, finally }
9947        | Stmt::TryWithResources { body, catches, finally, .. } => {
9948            finally.as_ref().is_some_and(|f| always_returns_one(f))
9949                || (always_returns_one(body)
9950                    && catches.iter().all(|c| always_returns_one(&c.body)))
9951        }
9952        _ => false,
9953    }
9954}
9955
9956fn always_returns_one(st: &Stmt) -> bool {
9957    always_returns(std::slice::from_ref(st))
9958}
9959
9960fn inline_ctor_arg_defs(
9961    call: &mut Stmt,
9962    prefix: &[&Stmt],
9963    vt: &VarTable,
9964    consumed: &mut Vec<(u32, usize)>,
9965) -> bool {
9966    let Stmt::ExprStmt(cexpr) = call else {
9967        return false;
9968    };
9969    // Replace every read of `v` in `e` with its value-so-far; a read
9970    // BEFORE any def (val None) is unresolvable → false.
9971    fn subst(e: &mut Expr, v: u32, val: &Option<Expr>) -> bool {
9972        let mut ok = true;
9973        deep_rewrite(e, &mut |x| {
9974            if let Expr::Local { var, .. } = x {
9975                if *var == v {
9976                    match val {
9977                        Some(e2) => *x = e2.clone(),
9978                        None => ok = false,
9979                    }
9980                }
9981            }
9982        });
9983        ok
9984    }
9985    let mut seen: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
9986    for _ in 0..32 {
9987        let mut need: Option<u32> = None;
9988        visit_exprs(cexpr, &mut |x| {
9989            if need.is_none() {
9990                if let Expr::Local { var, .. } = x {
9991                    if (*var as usize) < vt.vars.len() && !vt.vars[*var as usize].is_param {
9992                        need = Some(*var);
9993                    }
9994                }
9995            }
9996        });
9997        let Some(v) = need else { break };
9998        if !seen.insert(v) {
9999            return false; // cyclic reuse (`str = .. + str`) — bail
10000        }
10001        // CHAIN walk: fold EVERY prefix def of v into the value the
10002        // delegation actually receives. The old strict single-occurrence
10003        // rule rejected the convenience-ctor reassignment chain
10004        // (`f = getInstance(ctx); f = transform(f, ..); this(ctx, f)` —
10005        // androidx CameraController, weibo this-not-first ×831): the
10006        // SOURCE inlined the computation into the delegation args and
10007        // R8 decomposed it into register writes. Reads of v are allowed
10008        // only INSIDE its own def chain; any other prefix statement
10009        // touching v means the value escapes or the object mutates
10010        // (`stringBuilder.append(..)` — inlining a fresh `new
10011        // StringBuilder()` would lose the message), so bail.
10012        let mut val: Option<Expr> = None;
10013        let mut ok = true;
10014        for (si, st) in prefix.iter().enumerate() {
10015            match st {
10016                // A bare declaration (init None — the structurer hoists
10017                // `T v;` to the top) carries no value: skip it. Failing
10018                // here rejected every chain under a hoisted decl (ci1/a).
10019                Stmt::LocalDef { var, init: None, .. } if *var == v => {}
10020                Stmt::LocalDef { var, init, .. } if *var == v => {
10021                    let Some(e) = init else { ok = false; break };
10022                    let mut e2 = (*e).clone();
10023                    if !subst(&mut e2, v, &val) {
10024                        ok = false;
10025                        break;
10026                    }
10027                    val = Some(e2);
10028                    consumed.push((v, si));
10029                }
10030                Stmt::ExprStmt(Expr::Assign { target, op, .. })
10031                    if matches!(&**target, Expr::Local { var: tv, .. } if *tv == v) =>
10032                {
10033                    if !matches!(op, AssignOp::Plain) {
10034                        ok = false; // compound write: not a plain def
10035                        break;
10036                    }
10037                    let Stmt::ExprStmt(Expr::Assign { value, .. }) = st else {
10038                        unreachable!()
10039                    };
10040                    let mut e2 = value.as_ref().clone();
10041                    if !subst(&mut e2, v, &val) {
10042                        ok = false;
10043                        break;
10044                    }
10045                    val = Some(e2);
10046                    consumed.push((v, si));
10047                }
10048                _ => {
10049                    let mut touched = false;
10050                    visit_all_exprs(st, &mut |x| {
10051                        if let Expr::Local { var, .. } = x {
10052                            if *var == v {
10053                                touched = true;
10054                            }
10055                        }
10056                    });
10057                    if touched {
10058                        ok = false;
10059                        break;
10060                    }
10061                }
10062            }
10063        }
10064        if !ok {
10065            return false;
10066        }
10067        let Some(de) = val else { return false };
10068        if contains_this_access(&de) {
10069            return false;
10070        }
10071        // Huge defs multiply per read site per round — bound them.
10072        let mut de_nodes = 0usize;
10073        visit_exprs(&de, &mut |_| de_nodes += 1);
10074        if de_nodes > 256 {
10075            return false;
10076        }
10077        deep_rewrite(cexpr, &mut |x| {
10078            if let Expr::Local { var, .. } = x {
10079                if *var == v {
10080                    *x = de.clone();
10081                }
10082            }
10083        });
10084        // Exponential blowup guard: a var read twice doubles its def
10085        // per round, compounding across the transitive chain — cap the
10086        // expression size (a 2^k node tree overflowed the worker stack
10087        // process-wide).
10088        let mut nodes = 0usize;
10089        visit_exprs(cexpr, &mut |_| nodes += 1);
10090        if nodes > 2_048 {
10091            return false;
10092        }
10093    }
10094    true
10095}
10096
10097/// Fold a prelude StringBuilder mutation chain into the delegation
10098/// argument as a fluent chain: `StringBuilder sb = new SB(x);
10099/// sb.append(a); sb.append(b); super(sb.toString())` →
10100/// `super(new SB(x).append(a).append(b).toString())`. The generic def
10101/// inliner rejects mutation calls — their effect is not expressible as
10102/// a value — but `append` RETURNS its receiver, so the chain
10103/// reconstructs exactly the same mutations in one expression (androidx
10104/// emoji2/flatbuffer UnpairedSurrogateException family, the shapes the
10105/// strict-occurrence rule was written to refuse). Aborts on any `this`
10106/// touch inside the chain (illegal before super) or any other read of
10107/// the SB local (the value must not escape the fold). Returns the
10108/// delegation's new index (statements before it were removed).
10109fn fold_sb_chain(stmts: &mut Vec<Stmt>, pos: usize) -> usize {
10110    // 1. the SB local: first `v = new StringBuilder(..)` in the prefix.
10111    let mut sbv: Option<u32> = None;
10112    let mut def_i: Option<usize> = None;
10113    let mut def_init: Option<Expr> = None;
10114    for (i, st) in stmts[..pos].iter().enumerate() {
10115        let cand: Option<(u32, &Expr)> = match st {
10116            Stmt::LocalDef {
10117                var,
10118                init: Some(e),
10119                ..
10120            } => Some((*var, e)),
10121            Stmt::ExprStmt(Expr::Assign {
10122                target,
10123                value,
10124                op: AssignOp::Plain,
10125                ..
10126            }) => match &**target {
10127                Expr::Local { var, .. } => Some((*var, value.as_ref())),
10128                _ => None,
10129            },
10130            _ => None,
10131        };
10132        if let Some((v, init)) = cand {
10133            if let Expr::New { cls, .. } = init {
10134                if cls.as_ref() == "java/lang/StringBuilder" {
10135                    sbv = Some(v);
10136                    def_i = Some(i);
10137                    def_init = Some(init.clone());
10138                    break;
10139                }
10140            }
10141        }
10142    }
10143    let (Some(sbv), Some(def_i), Some(init)) = (sbv, def_i, def_init) else {
10144        return pos;
10145    };
10146    if contains_this_access(&init) {
10147        return pos;
10148    }
10149    // 2. append statements on sbv, in order.
10150    let mut appends: Vec<usize> = Vec::new();
10151    for (i, st) in stmts[..pos].iter().enumerate() {
10152        if let Stmt::ExprStmt(Expr::Method {
10153            owner: Some(o),
10154            cls,
10155            name,
10156            ..
10157        }) = st
10158        {
10159            if matches!(&**o, Expr::Local { var, .. } if *var == sbv)
10160                && cls.as_ref() == "java/lang/StringBuilder"
10161                && &**name == "append"
10162            {
10163                appends.push(i);
10164            }
10165        }
10166    }
10167    // 3. no read of sbv after the delegation.
10168    let mut post_read = false;
10169    for st in stmts.iter().skip(pos + 1) {
10170        visit_all_exprs(st, &mut |e| {
10171            if let Expr::Local { var, .. } = e {
10172                if *var == sbv {
10173                    post_read = true;
10174                }
10175            }
10176        });
10177    }
10178    if post_read {
10179        return pos;
10180    }
10181    // 4. prefix reads of sbv only in the def and the append receivers
10182    //    (arguments are checked for `this`, receivers are the chain).
10183    for (i, st) in stmts[..pos].iter().enumerate() {
10184        if i == def_i || appends.contains(&i) {
10185            // append ARGUMENTS must not touch this; receiver is fine.
10186            if let Stmt::ExprStmt(Expr::Method { args, .. }) = st {
10187                if args.iter().any(contains_this_access) {
10188                    return pos;
10189                }
10190            }
10191            continue;
10192        }
10193        let mut leak = false;
10194        visit_all_exprs(st, &mut |e| {
10195            if let Expr::Local { var, .. } = e {
10196                if *var == sbv {
10197                    leak = true;
10198                }
10199            }
10200        });
10201        if leak {
10202            return pos;
10203        }
10204    }
10205    // 5. the call reads sbv exactly once, as `sbv.toString()`.
10206    let mut local_reads = 0usize;
10207    let mut tostring_reads = 0usize;
10208    visit_all_exprs(&stmts[pos], &mut |e| match e {
10209        Expr::Local { var, .. } if *var == sbv => local_reads += 1,
10210        Expr::Method {
10211            owner: Some(o),
10212            name,
10213            args,
10214            ..
10215        } if args.is_empty()
10216            && &**name == "toString"
10217            && matches!(&**o, Expr::Local { var, .. } if *var == sbv) =>
10218        {
10219            tostring_reads += 1;
10220        }
10221        _ => {}
10222    });
10223    if local_reads != 1 || tostring_reads != 1 {
10224        return pos;
10225    }
10226    // 6. build the fluent chain and swap it into the toString receiver.
10227    let mut chain = init;
10228    for ai in &appends {
10229        if let Stmt::ExprStmt(Expr::Method {
10230            owner: _,
10231            cls,
10232            name,
10233            desc,
10234            args,
10235            is_static,
10236            is_interface,
10237            is_special,
10238            is_super,
10239            is_dynamic,
10240            type_args,
10241            ..
10242        }) = &stmts[*ai]
10243        {
10244            chain = Expr::Method {
10245                owner: Some(Box::new(chain)),
10246                cls: cls.clone(),
10247                name: name.clone(),
10248                desc: desc.clone(),
10249                args: args.clone(),
10250                is_static: *is_static,
10251                is_interface: *is_interface,
10252                is_special: *is_special,
10253                is_super: *is_super,
10254                is_dynamic: *is_dynamic,
10255                type_args: type_args.clone(),
10256            };
10257        }
10258    }
10259    let mut replaced = false;
10260    walk_stmt_exprs(&mut stmts[pos], &mut |e| {
10261        deep_rewrite(e, &mut |x| {
10262            if let Expr::Method {
10263                owner: Some(o),
10264                name,
10265                args,
10266                ..
10267            } = x
10268            {
10269                if args.is_empty()
10270                    && &**name == "toString"
10271                    && matches!(&**o, Expr::Local { var, .. } if *var == sbv)
10272                {
10273                    **o = chain.clone();
10274                    replaced = true;
10275                }
10276            }
10277        });
10278    });
10279    if !replaced {
10280        return pos; // never remove without the swap — atomicity
10281    }
10282    // 7. drop the consumed statements (descending), adjust pos.
10283    let mut removed: Vec<usize> = appends.clone();
10284    removed.push(def_i);
10285    removed.sort_unstable();
10286    removed.dedup();
10287    for i in removed.iter().rev() {
10288        stmts.remove(*i);
10289    }
10290    pos - removed.len()
10291}
10292
10293pub fn fix_ctor_super_first(body: &mut Stmt, vt: &VarTable) {
10294    let Stmt::Block(stmts) = body else { return };
10295    if stmts.is_empty() || is_bare_ctor_call(stmts.first().unwrap()) {
10296        return;
10297    }
10298    // Top-level delegation call.
10299    if let Some(pos0) = stmts.iter().position(is_bare_ctor_call) {
10300        let pos = fold_sb_chain(stmts, pos0);
10301        let mut call = stmts[pos].clone();
10302        let prefix: Vec<&Stmt> = stmts[..pos].iter().collect();
10303        let mut consumed: Vec<(u32, usize)> = Vec::new();
10304        if !inline_ctor_arg_defs(&mut call, &prefix, vt, &mut consumed) {
10305            return;
10306        }
10307        stmts.remove(pos);
10308        // Drop the chain defs the inline consumed (all indices < pos) —
10309        // leaving them would DOUBLE-evaluate their (possibly impure)
10310        // inits. But ONLY for vars with no post-delegation use: dropping
10311        // such a var's DECLARATION makes ensure_declared re-add it at
10312        // the method top, i.e. BEFORE this() — a fresh "对this的调用必须
10313        // 是构造器中的第一个语句" (lark +587 the blind drop caused).
10314        let mut drop_idx: Vec<usize> = consumed
10315            .iter()
10316            .filter(|(v, _)| !var_occurs_in(&stmts[pos..], *v))
10317            .map(|(_, k)| *k)
10318            .collect();
10319        drop_idx.sort_unstable();
10320        drop_idx.dedup();
10321        for k in drop_idx.iter().rev() {
10322            stmts.remove(*k);
10323        }
10324        stmts.insert(0, call);
10325        return;
10326    }
10327    // The structurer can wrap the delegation in a bare nested block —
10328    // with the parameter null-checks AHEAD of it (weixin's Kotlin
10329    // intrinsics shape: `o.h(parcel, "source"); super();` at inner
10330    // positions 0/1, flattened to straight-line by cleanup AFTER this
10331    // pass, which is why the miss surfaced as 1150 weixin "对super的
10332    // 调用必须是构造器中的第一个语句"). Hoist the call from any
10333    // position whose predecessors are all straight-line statements; a
10334    // delegation behind control flow is the conditional-super family
10335    // (needs restructuring, not hoisting) and stays put.
10336    let mut found: Option<(usize, usize)> = None;
10337    for (i, st) in stmts.iter().enumerate() {
10338        if let Stmt::Block(inner) = st {
10339            if let Some(p) = inner.iter().position(is_bare_ctor_call) {
10340                if inner[..p]
10341                    .iter()
10342                    .all(|s| matches!(s, Stmt::LocalDef { .. } | Stmt::ExprStmt(_)))
10343                {
10344                    found = Some((i, p));
10345                    break;
10346                }
10347            }
10348        }
10349    }
10350    if let Some((i, p)) = found {
10351        let mut call = match &stmts[i] {
10352            Stmt::Block(inner) => inner[p].clone(),
10353            _ => unreachable!(),
10354        };
10355        let mut prefix: Vec<&Stmt> = stmts[..i].iter().collect();
10356        if let Stmt::Block(inner) = &stmts[i] {
10357            prefix.extend(inner[..p].iter());
10358        }
10359        let mut consumed: Vec<(u32, usize)> = Vec::new();
10360        if !inline_ctor_arg_defs(&mut call, &prefix, vt, &mut consumed) {
10361            return;
10362        }
10363        // Prefix index k maps to stmts[k] for k < i, inner[k - i] for
10364        // k >= i (all such k are < p). Suffix = the rest of the inner
10365        // block + the following top-level statements; a consumed def may
10366        // only drop when its var has no suffix use (declaration loss →
10367        // ensure_declared re-adds it BEFORE this()).
10368        // Compute the droppable set BEFORE mutating (borrow split).
10369        let mut ks: Vec<usize> = Vec::new();
10370        {
10371            let inner_tail: &[Stmt] = match &stmts[i] {
10372                Stmt::Block(inner) if p < inner.len() => &inner[p + 1..],
10373                _ => &[],
10374            };
10375            for (v, k) in &consumed {
10376                if !var_occurs_in(inner_tail, *v) && !var_occurs_in(&stmts[i + 1..], *v) {
10377                    ks.push(*k);
10378                }
10379            }
10380        }
10381        ks.sort_unstable();
10382        ks.dedup();
10383        if let Stmt::Block(inner) = &mut stmts[i] {
10384            inner.remove(p);
10385            for k in ks.iter().rev().filter(|k| **k >= i) {
10386                inner.remove(k - i);
10387            }
10388        }
10389        for k in ks.iter().rev().filter(|k| **k < i) {
10390            stmts.remove(*k);
10391        }
10392        stmts.insert(0, call);
10393    }
10394}
10395
10396/// A true constructor DELEGATION statement: `super(..)` (owner None) or
10397/// `this(..)` (owner This). An un-folded `new X; <init>` (lost
10398/// allocation — the ctor-fold-bug family) arrives as a `<init>` Method
10399/// with a NON-this owner and prints as `new X(..)`: it is not a
10400/// delegation and must neither satisfy nor anchor the super-first fixes
10401/// (v7.a$a2: a dead `new f(0,..)` init at position 0 made both hoists
10402/// believe the delegation was already first, leaving the real `super`
10403/// last — "对super的调用必须是构造器中的第一个语句").
10404fn is_delegation_expr(e: &Expr) -> bool {
10405    matches!(
10406        e,
10407        Expr::Method { name, is_special: true, owner, .. }
10408            if &**name == "<init>"
10409                && (owner.is_none() || matches!(owner.as_deref(), Some(Expr::This)))
10410    )
10411}
10412
10413pub(crate) fn is_bare_ctor_call(s: &Stmt) -> bool {
10414    matches!(s, Stmt::ExprStmt(e) if is_delegation_expr(e))
10415}
10416
10417/// Is the local ever ASSIGNED (write position: `x = ..`, `++x`)?
10418/// Distinct from a read count: a ctor's synthetic outer param is only
10419/// ever read; a param that gets written must stay declared.
10420pub(crate) fn local_is_written(body: &Stmt, var: u32) -> bool {
10421    let mut hit = false;
10422    visit_stmt_exprs_ro(body, &mut |e| {
10423        if hit {
10424            return;
10425        }
10426        visit_exprs(e, &mut |x| {
10427            if hit {
10428                return;
10429            }
10430            match x {
10431                Expr::Assign { target, .. }
10432                    if matches!(&**target, Expr::Local { var: v, .. } if *v == var) =>
10433                {
10434                    hit = true;
10435                }
10436                Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. }
10437                    if matches!(&**e, Expr::Local { var: v, .. } if *v == var) =>
10438                {
10439                    hit = true;
10440                }
10441                _ => {}
10442            }
10443        });
10444    });
10445    hit
10446}
10447
10448/// Source-form normalization for a non-static member-inner constructor.
10449/// The dex descriptor carries the synthetic outer instance as args[0]
10450/// (typed as the direct enclosing class); the emitter's qualified
10451/// `outer.new Inner(..)` / `this.new Inner(..)` sites pass it
10452/// implicitly and `super(..)` delegations drop it, so the signature
10453/// loses the param — and the body must stop referencing it: plain uses
10454/// become `Outer.this` (a `this.this$0 = p` assignment becomes
10455/// `this.this$0 = Outer.this`, exactly the runtime value), and
10456/// this()-delegations to `eligible` classes drop the leading param arg.
10457pub(crate) fn rewrite_inner_ctor_outer_param(
10458    body: &mut Stmt,
10459    param0: u32,
10460    outer_display: &str,
10461    outer_ty: &TypeRef,
10462    eligible: &[String],
10463) {
10464    // this()/super() delegations (and lost-alloc construction calls):
10465    // the leading arg IS the synthetic outer exactly when it is the
10466    // param itself.
10467    walk_stmt_exprs(body, &mut |e| {
10468        if let Expr::Method { name, cls, args, .. } = e {
10469            if &**name == "<init>"
10470                && eligible.iter().any(|c| c.as_str() == cls.as_ref())
10471                && args.first().is_some_and(|a| matches!(a, Expr::Local { var: v, .. } if *v == param0))
10472            {
10473                args.remove(0);
10474            }
10475        }
10476    });
10477    // Every remaining read of the param becomes `Outer.this`.
10478    let text = format!("{outer_display}.this");
10479    let ty = outer_ty.clone();
10480    walk_stmt_exprs(body, &mut |e| {
10481        deep_rewrite_reads(e, &mut |x| {
10482            if let Expr::Local { var: v, .. } = x {
10483                if *v == param0 {
10484                    *x = Expr::RawT(text.clone(), ty.clone());
10485                }
10486            }
10487        });
10488    });
10489}
10490
10491/// Ctors whose delegation is buried in control flow or behind arg
10492/// computations — the "对super的调用必须是构造器中的第一个语句" family
10493/// that plain hoisting cannot reach (weixin 264 / weibo 116 / reqable 71
10494/// / lark 62 after round 60): R8/Kotlin compute super-args conditionally
10495/// and dex legally invokes super mid-branch.
10496///
10497/// Shape B (branch merge): `prelude; if (c) {d1; super(A1); t1} else
10498/// {d2; super(A2); t2}; post` → one leading `super(..)` where each
10499/// differing arg becomes `c ? a1i : a2i`; branch remainders stay an
10500/// if/else AFTER the call; prelude statements move after it (nothing
10501/// may precede super in Java; bytecode pre-super work is local-only by
10502/// construction — dex forbids instance-field reads before the delegate).
10503///
10504/// Shape A (linear): `defs; super(A); rest` where `A` references the
10505/// def locals — plain hoisting (fix_ctor_super_first case 1) would move
10506/// the call above its argument definitions (forward refs). Inline the
10507/// referenced defs into the args first, drop the consumed defs, hoist.
10508/// Runs BEFORE fix_ctor_super_first and only fires when args reference
10509/// preceding locals, leaving the battle-tested plain hoist untouched
10510/// otherwise.
10511///
10512/// Inlining duplicates the def per use site, so a side-effecting def is
10513/// inlined only when its var's use count is provably preserved: a
10514/// single use in a merged arg (1 copy), or a single use in the
10515/// condition with ≤1 differing arg (the cond then appears in the `if`
10516/// plus one ternary — 2 copies of e.g. one Kotlin getter call, which
10517/// the original bytecode typically made twice anyway: `p.u() == null ?
10518/// .. : p.u().i()`). Pure defs inline freely. Every other shape aborts:
10519/// broken-but-faithful stays the status quo, wrong evaluation counts
10520/// would be worse.
10521/// Linear `def; delegation(args read def)` shapes. A plain hoist of the
10522/// delegation past its prefix would place the call ABOVE a definition
10523/// it reads — a forward reference (`super(context2, ..)` with `context2
10524/// = ..` below it). javac's attribution for the whole class then
10525/// collapses: the undefined identifier cascades into "non-static
10526/// super" and even bare `Object` resolution failures, poisoning every
10527/// later diagnostic in the file (weibo AppCompatTextView — the root of
10528/// a 4.1k-file Object cascade). The original source had the def's
10529/// expression INLINE in the delegation args; d8 computed it into a
10530/// register first. Inline the def into the args (the def is read
10531/// exactly once there — evaluation count preserved even for calls),
10532/// and when the register is REASSIGNED later (a fresh generation
10533/// sharing the slot), convert that first write into the declaration so
10534/// the var stays defined for its later readers. Any shape that cannot
10535/// be proven aborts untouched.
10536/// Count WRITE positions of `var` (assign / inc-dec targets) — the
10537/// shape-C validator needs exact counts, not just presence.
10538fn stmts_count_writes(stmts: &[Stmt], var: u32) -> usize {
10539    let mut n = 0usize;
10540    for s in stmts {
10541        visit_stmt_exprs_ro(s, &mut |e| {
10542            visit_exprs(e, &mut |x| match x {
10543                Expr::Assign { target, .. }
10544                    if matches!(&**target, Expr::Local { var: v, .. } if *v == var) =>
10545                {
10546                    n += 1;
10547                }
10548                Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. }
10549                    if matches!(&**e, Expr::Local { var: v, .. } if *v == var) =>
10550                {
10551                    n += 1;
10552                }
10553                _ => {}
10554            });
10555        });
10556    }
10557    n
10558}
10559
10560/// Collect every local WRITTEN in `stmts` — the complement inside a
10561/// ctor prefix is the param set (the only locals a pre-delegation arg
10562/// expression may read).
10563fn prefix_written_vars(stmts: &[Stmt]) -> jdc_core::FxHashSet<u32> {
10564    let mut set = jdc_core::FxHashSet::default();
10565    for s in stmts {
10566        let mut c = s.clone();
10567        walk_stmt_exprs(&mut c, &mut |e| {
10568            deep_rewrite(e, &mut |x| match x {
10569                Expr::Assign { target, .. } => {
10570                    if let Expr::Local { var, .. } = &**target {
10571                        set.insert(*var);
10572                    }
10573                }
10574                Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => {
10575                    if let Expr::Local { var, .. } = &**e {
10576                        set.insert(*var);
10577                    }
10578                }
10579                _ => {}
10580            });
10581        });
10582        if let Stmt::LocalDef { var, .. } = s {
10583            set.insert(*var);
10584        }
10585    }
10586    set
10587}
10588
10589/// Is `e` safe to evaluate INSIDE a ctor-delegation argument (before
10590/// this/super exists)? Params/consts/pure ops only — no instance-field
10591/// reads, no `this`, no captures.
10592fn pre_this_safe(e: &Expr, prefix_vars: &jdc_core::FxHashSet<u32>) -> bool {
10593    match e {
10594        Expr::Local { var, .. } => !prefix_vars.contains(var),
10595        Expr::Const(_) => true,
10596        Expr::Cast { e, .. } => pre_this_safe(e, prefix_vars),
10597        Expr::Bin { l, r, .. } => {
10598            pre_this_safe(l, prefix_vars) && pre_this_safe(r, prefix_vars)
10599        }
10600        Expr::Cond { c, t, f } => {
10601            pre_this_safe(c, prefix_vars)
10602                && pre_this_safe(t, prefix_vars)
10603                && pre_this_safe(f, prefix_vars)
10604        }
10605        Expr::Field { owner, is_static, .. } => {
10606            *is_static
10607                && owner
10608                    .as_ref()
10609                    .is_none_or(|o| pre_this_safe(o, prefix_vars))
10610        }
10611        Expr::Method {
10612            owner,
10613            args,
10614            is_static,
10615            ..
10616        } => {
10617            args.iter().all(|a| pre_this_safe(a, prefix_vars))
10618                && match owner {
10619                    Some(o) => pre_this_safe(o, prefix_vars),
10620                    None => *is_static,
10621                }
10622        }
10623        // Allocation in a delegation arg is legal Java before this()
10624        // (the ctor call touches no instance state of THIS class) — the
10625        // missing arm sent every `new Date()`-style Kotlin default
10626        // through `_ => false`, rejecting the whole fold (lark
10627        // Request$AppToApp this-not-first family, ~650 lark lines).
10628        // An inner-class construction needing an outer `this` carries
10629        // Expr::This in its args, which still fails below.
10630        Expr::New { args, .. } => args.iter().all(|a| pre_this_safe(a, prefix_vars)),
10631        Expr::NewArray { dims, init, .. } => {
10632            dims.iter().all(|d| pre_this_safe(d, prefix_vars))
10633                && init
10634                    .as_ref()
10635                    .map(|l| l.iter().all(|x| pre_this_safe(x, prefix_vars)))
10636                    .unwrap_or(true)
10637        }
10638        // Pure unary/instanceof/array reads.
10639        Expr::Un { e, .. } | Expr::InstanceOf { e, .. } => pre_this_safe(e, prefix_vars),
10640        Expr::ArrayIndex { array, index } => {
10641            pre_this_safe(array, prefix_vars) && pre_this_safe(index, prefix_vars)
10642        }
10643        _ => false,
10644    }
10645}
10646
10647/// `s` is exactly one plain assignment to local `v` (optionally wrapped
10648/// in a singleton Block): the branch shape of a Kotlin default-arg
10649/// override.
10650fn single_assign_to_v(s: &Stmt, v: u32) -> Option<Expr> {
10651    match s {
10652        Stmt::ExprStmt(Expr::Assign {
10653            target,
10654            op: AssignOp::Plain,
10655            value,
10656        }) if matches!(&**target, Expr::Local { var: vv, .. } if *vv == v) => {
10657            Some((**value).clone())
10658        }
10659        Stmt::Block(xs) if xs.len() == 1 => single_assign_to_v(&xs[0], v),
10660        _ => None,
10661    }
10662}
10663
10664/// Kotlin default-argument BRIDGE ctors (shape C of the delegation
10665/// family): `<init>(params.., int mask, DefaultConstructorMarker)`
10666/// computes each defaulted arg into a local — `v = p; if ((mask&bit)
10667/// != 0) { v = DEFAULT; }` — then delegates `this(.., v, ..)`. Java
10668/// allows NOTHING before the delegation, so the computations must fold
10669/// INTO the args (`this(.., (mask&bit) != 0 ? DEFAULT : p, ..)`).
10670/// Unfolded, the hoist passes lift the delegation above the raw local
10671/// reads: "找不到符号 变量 str2/v4/creationExtras2" — 2.5k root files
10672/// across weixin/lark/weibo (weixin MvvmObserverOwner$..Observer, lark
10673/// SimpleArrayMap/LongSparseArray). Per-var provability: exactly one
10674/// top-level base write, an optional single conditional override whose
10675/// branches are lone assignments, no other prefix touch of the var,
10676/// nothing after the delegation touches it, and the folded expression
10677/// is pre-this-safe. Unprovable vars stay untouched (partial folds are
10678/// strictly better — the rest was broken before too).
10679/// Flatten nested plain Blocks among the top-level statements. The
10680/// structurer hands ctor bodies as region-nested blocks (`Block([def,
10681/// Block([assign, if]), Block([delegate, return])])` — the Kotlin
10682/// bridge shape), and every delegation-repair pass scans the TOP level
10683/// only. Straight-line nesting flattens without semantics; control
10684/// flow lives inside If/While/etc. arms, never as a direct member.
10685pub(crate) fn flatten_top_blocks(stmts: &mut Vec<Stmt>) {
10686    fn rec(s: Stmt, out: &mut Vec<Stmt>) {
10687        match s {
10688            Stmt::Block(inner) => {
10689                for x in inner {
10690                    rec(x, out);
10691                }
10692            }
10693            other => out.push(other),
10694        }
10695    }
10696    if !stmts.iter().any(|s| matches!(s, Stmt::Block(_))) {
10697        return;
10698    }
10699    let mut out: Vec<Stmt> = Vec::with_capacity(stmts.len());
10700    for s in std::mem::take(stmts) {
10701        rec(s, &mut out);
10702    }
10703    *stmts = out;
10704}
10705
10706fn fold_default_arg_bridge(stmts: &mut Vec<Stmt>) -> bool {
10707    flatten_top_blocks(stmts);
10708    let Some(pos) = stmts.iter().position(is_bare_ctor_call) else {
10709        return false;
10710    };
10711    if pos == 0 {
10712        return false;
10713    }
10714    let Stmt::ExprStmt(Expr::Method { args, .. }) = &stmts[pos] else {
10715        return false;
10716    };
10717    let mut arg_vars: Vec<u32> = Vec::new();
10718    let mut reads = 0usize;
10719    for a in args {
10720        let mut c = a.clone();
10721        deep_rewrite_reads(&mut c, &mut |x| {
10722            if let Expr::Local { var, .. } = x {
10723                reads += 1;
10724                if !arg_vars.contains(var) {
10725                    arg_vars.push(*var);
10726                }
10727            }
10728        });
10729    }
10730    if arg_vars.is_empty() || reads != arg_vars.len() {
10731        return false; // a var read twice would duplicate evaluation
10732    }
10733    // The bridge ENDS at the delegation: nothing after may touch the
10734    // arg vars (reads OR writes).
10735    for &v in &arg_vars {
10736        if stmts_read_var(&stmts[pos + 1..], v) != 0
10737            || stmts_count_writes(&stmts[pos + 1..], v) != 0
10738        {
10739            return false;
10740        }
10741    }
10742    let prefix_vars = prefix_written_vars(&stmts[..pos]);
10743    let mut repl: Vec<(u32, Expr)> = Vec::new();
10744    let mut drop: Vec<usize> = Vec::new();
10745    for &v in &arg_vars {
10746        // Exact prefix shape for v.
10747        let mut base: Option<(usize, Expr)> = None;
10748        let mut decl: Option<usize> = None;
10749        let mut condf: Option<(usize, Expr, Option<Expr>, Option<Expr>)> = None;
10750        let mut ok = true;
10751        for (i, s) in stmts[..pos].iter().enumerate() {
10752            match s {
10753                Stmt::LocalDef { var, init, .. } if *var == v => match init {
10754                    None => {
10755                        if decl.is_some() {
10756                            ok = false;
10757                            break;
10758                        }
10759                        decl = Some(i);
10760                    }
10761                    Some(e) => {
10762                        if base.is_some() {
10763                            ok = false;
10764                            break;
10765                        }
10766                        base = Some((i, e.clone()));
10767                    }
10768                },
10769                Stmt::ExprStmt(Expr::Assign {
10770                    target,
10771                    op: AssignOp::Plain,
10772                    value,
10773                }) if matches!(&**target, Expr::Local { var: vv, .. } if *vv == v) => {
10774                    if base.is_some() {
10775                        ok = false;
10776                        break;
10777                    }
10778                    base = Some((i, (**value).clone()));
10779                }
10780                Stmt::If {
10781                    cond,
10782                    then_stmt,
10783                    else_stmt,
10784                } => {
10785                    let t = single_assign_to_v(then_stmt, v);
10786                    let f = else_stmt.as_ref().and_then(|b| single_assign_to_v(b, v));
10787                    let then_touches = t.is_some()
10788                        || stmts_read_var(std::slice::from_ref(&**then_stmt), v) != 0
10789                        || stmts_count_writes(std::slice::from_ref(&**then_stmt), v) != 0;
10790                    let else_touches = else_stmt.as_ref().is_some_and(|b| {
10791                        f.is_some()
10792                            || stmts_read_var(std::slice::from_ref(b), v) != 0
10793                            || stmts_count_writes(std::slice::from_ref(b), v) != 0
10794                    });
10795                    if !then_touches && !else_touches {
10796                        continue; // unrelated if
10797                    }
10798                    // Both branches must be lone assignments (a branch
10799                    // with its own delegation/return is the conditional-
10800                    // super machinery's shape, not this one), the cond
10801                    // must not read v, and only one override per var.
10802                    if condf.is_some()
10803                        || base.is_none()
10804                        || (then_touches && t.is_none())
10805                        || (else_touches && f.is_none())
10806                    {
10807                        ok = false;
10808                        break;
10809                    }
10810                    let cond_stmt = Stmt::ExprStmt(cond.clone());
10811                    if stmts_read_var(std::slice::from_ref(&cond_stmt), v) != 0 {
10812                        ok = false;
10813                        break;
10814                    }
10815                    condf = Some((i, cond.clone(), t, f));
10816                }
10817                _ => {}
10818            }
10819        }
10820        if !ok {
10821            continue;
10822        }
10823        let Some((bi, be)) = base else { continue };
10824        // Exact write/read accounting: base (+then/else) writes, zero
10825        // prefix reads.
10826        let want_writes = 1
10827            + condf
10828                .as_ref()
10829                .map(|(_, _, t, f)| t.is_some() as usize + f.is_some() as usize)
10830                .unwrap_or(0);
10831        if stmts_count_writes(&stmts[..pos], v) != want_writes
10832            || stmts_read_var(&stmts[..pos], v) != 0
10833        {
10834            continue;
10835        }
10836        let rep = match condf {
10837            Some((ci, c, t, f)) => {
10838                drop.push(ci);
10839                match (t, f) {
10840                    (Some(t), Some(f)) => Expr::Cond {
10841                        c: Box::new(c),
10842                        t: Box::new(t),
10843                        f: Box::new(f),
10844                    },
10845                    (Some(t), None) => Expr::Cond {
10846                        c: Box::new(c),
10847                        t: Box::new(t),
10848                        f: Box::new(be.clone()),
10849                    },
10850                    (None, Some(f)) => Expr::Cond {
10851                        c: Box::new(c),
10852                        t: Box::new(be.clone()),
10853                        f: Box::new(f),
10854                    },
10855                    (None, None) => unreachable!("condf requires a branch assign"),
10856                }
10857            }
10858            None => be.clone(),
10859        };
10860        if !pre_this_safe(&rep, &prefix_vars) {
10861            continue;
10862        }
10863        drop.push(bi);
10864        if let Some(d) = decl {
10865            drop.push(d);
10866        }
10867        repl.push((v, rep));
10868    }
10869    if repl.is_empty() {
10870        return false;
10871    }
10872    if let Stmt::ExprStmt(Expr::Method { args, .. }) = &mut stmts[pos] {
10873        for a in args.iter_mut() {
10874            deep_rewrite_reads(a, &mut |x| {
10875                if let Expr::Local { var, .. } = x {
10876                    if let Some((_, e)) = repl.iter().find(|(vv, _)| vv == var) {
10877                        *x = e.clone();
10878                    }
10879                }
10880            });
10881        }
10882    }
10883    drop.sort_unstable();
10884    drop.dedup();
10885    for i in drop.into_iter().rev() {
10886        stmts.remove(i);
10887    }
10888    true
10889}
10890
10891/// Rewrite static member references on Kotlin multi-file facade PARTS
10892/// (`StringsKt__StringsKt.trim(..)`) to the public facade the parts are
10893/// compiled into (`StringsKt.trim(..)`): the parts are package-private,
10894/// so every cross-package call site is "在kotlin.text中不是公共的; 无法
10895/// 从外部程序包中对其进行访问" (lark 1.4k root files; weibo's 8.4k-line
10896/// Kt cluster). Static members resolve through the facade by
10897/// inheritance (the map is gated on the extends chain).
10898pub fn rewrite_kotlin_facades(body: &mut Stmt, pool: &crate::DexPool) {
10899    let map = pool.kotlin_facade_map();
10900    if map.is_empty() {
10901        return;
10902    }
10903    walk_stmt_exprs(body, &mut |e| {
10904        match e {
10905            Expr::Method {
10906                cls,
10907                name,
10908                desc,
10909                is_static,
10910                ..
10911            } if *is_static => {
10912                if let Some((base, cover)) = map.get(cls.as_ref()) {
10913                    let hit = match cover {
10914                        None => true,
10915                        Some(c) => c.0.contains(&(name.to_string(), desc.to_string())),
10916                    };
10917                    if hit {
10918                        *cls = std::sync::Arc::from(base.as_str());
10919                    }
10920                }
10921            }
10922            Expr::Field {
10923                cls,
10924                name,
10925                is_static,
10926                ..
10927            } if *is_static => {
10928                if let Some((base, cover)) = map.get(cls.as_ref()) {
10929                    let hit = match cover {
10930                        None => true,
10931                        Some(c) => c.1.contains(name.as_ref()),
10932                    };
10933                    if hit {
10934                        *cls = std::sync::Arc::from(base.as_str());
10935                    }
10936                }
10937            }
10938            _ => {}
10939        }
10940    });
10941}
10942
10943/// Kotlin default-arg bridge fold for ENUM ctors: the synthetic
10944/// `(String, int, .., int, DefaultConstructorMarker)` ctor computes
10945/// defaulted args then chains `this(..)` mid-body — Java requires the
10946/// delegation first (对this的调用必须是构造器中的第一个语句, lark's
10947/// UserCustomStatusExtraParams$* family, 752 lines). Same fold the
10948/// class-ctor path runs; the enum branch of the pipeline used to skip
10949/// it (only strip_enum_ctor_super ran there).
10950pub fn fold_enum_default_arg_bridge(body: &mut Stmt) {
10951    let Stmt::Block(stmts) = body else { return };
10952    fold_default_arg_bridge(stmts);
10953}
10954
10955pub fn fix_ctor_delegation_arg_defs(body: &mut Stmt) {
10956    let Stmt::Block(stmts) = body else { return };
10957    // Shape C first: the Kotlin default-arg bridge (computations +
10958    // conditional overrides folded into the delegation args).
10959    fold_default_arg_bridge(stmts);
10960    // Only the top-level linear shape; the first statement needs no
10961    // repair and control flow ahead of the delegation belongs to the
10962    // conditional-super machinery.
10963    let Some(pos) = stmts.iter().position(is_bare_ctor_call) else {
10964        return;
10965    };
10966    if pos == 0 {
10967        return;
10968    }
10969    // Vars read by the delegation args, and the total read count (a var
10970    // read twice would duplicate a call evaluation on inline).
10971    let mut arg_vars: Vec<u32> = Vec::new();
10972    let mut total_reads = 0usize;
10973    {
10974        let Stmt::ExprStmt(Expr::Method { args, .. }) = &stmts[pos] else {
10975            return;
10976        };
10977        for a in args {
10978            let mut c = a.clone();
10979            deep_rewrite_reads(&mut c, &mut |x| {
10980                if let Expr::Local { var: v, .. } = x {
10981                    total_reads += 1;
10982                    if !arg_vars.contains(v) {
10983                        arg_vars.push(*v);
10984                    }
10985                }
10986            });
10987        }
10988    }
10989    if arg_vars.is_empty() || total_reads != arg_vars.len() {
10990        return;
10991    }
10992    // Vars DEFINED anywhere in the prefix (for the init-referenced
10993    // check below — an inlined init may not itself read prefix defs).
10994    let prefix_defs: Vec<u32> = stmts[..pos]
10995        .iter()
10996        .filter_map(|s| match s {
10997            Stmt::LocalDef { var, init: Some(_), .. } => Some(*var),
10998            _ => None,
10999        })
11000        .collect();
11001    // Per referenced var: exactly one prefix def, no other prefix
11002    // reader between def and delegation, the def's init reads no
11003    // prefix-def var, and the first post-delegation use is a top-level
11004    // write (becomes the declaration) or nothing at all.
11005    let mut plan: Vec<(u32, usize, Option<usize>)> = Vec::new();
11006    for v in arg_vars {
11007        let defs: Vec<usize> = stmts[..pos]
11008            .iter()
11009            .enumerate()
11010            .filter(|(i, _)| {
11011                matches!(&stmts[*i], Stmt::LocalDef { var, init: Some(_), .. } if *var == v)
11012            })
11013            .map(|(i, _)| i)
11014            .collect();
11015        if defs.len() > 1 {
11016            return; // ambiguous: multiple prefix defs
11017        }
11018        let Some(&q) = defs.first() else {
11019            continue; // a param: nothing to inline for this var
11020        };
11021        let init = match &stmts[q] {
11022            Stmt::LocalDef { init: Some(e), .. } => e.clone(),
11023            _ => return,
11024        };
11025        if stmts_read_var(&stmts[q + 1..pos], v) != 0 {
11026            return; // another reader between the def and the delegation
11027        }
11028        // The inlined init may not read a prefix-def var either — the
11029        // delegation lands ABOVE those defs after the hoist.
11030        let mut init_vars: Vec<u32> = Vec::new();
11031        deep_rewrite_reads(&mut { init.clone() }, &mut |x| {
11032            if let Expr::Local { var: vv, .. } = x {
11033                if !init_vars.contains(vv) {
11034                    init_vars.push(*vv);
11035                }
11036            }
11037        });
11038        if init_vars.iter().any(|iv| prefix_defs.contains(iv)) {
11039            return;
11040        }
11041        match stmts[pos + 1..]
11042            .iter()
11043            .position(|s| {
11044                matches!(s, Stmt::ExprStmt(Expr::Assign { target, .. })
11045                    if matches!(&**target, Expr::Local { var: vv, .. } if *vv == v))
11046            }) {
11047            Some(w) => {
11048                // Reads before the re-declaration would be dangling.
11049                if stmts_read_var(&stmts[pos + 1..pos + 1 + w], v) != 0 {
11050                    return;
11051                }
11052                plan.push((v, q, Some(pos + 1 + w)));
11053            }
11054            None => {
11055                // No later write: the def must be dead after inline.
11056                if stmts_read_var(&stmts[pos + 1..], v) != 0 {
11057                    return;
11058                }
11059                plan.push((v, q, None));
11060            }
11061        }
11062    }
11063    if plan.is_empty() {
11064        return;
11065    }
11066    // Inline the def inits into the delegation args.
11067    let values: Vec<(u32, Expr)> = plan
11068        .iter()
11069        .map(|(_v, q, _)| match &stmts[*q] {
11070            Stmt::LocalDef { var, init: Some(e), .. } => (*var, e.clone()),
11071            _ => unreachable!("plan entries carry a LocalDef with init"),
11072        })
11073        .collect();
11074    if let Stmt::ExprStmt(Expr::Method { args, .. }) = &mut stmts[pos] {
11075        for a in args {
11076            deep_rewrite_reads(a, &mut |x| {
11077                if let Expr::Local { var: v, .. } = x {
11078                    if let Some((_, e)) = values.iter().find(|(vv, _)| vv == v) {
11079                        *x = e.clone();
11080                    }
11081                }
11082            });
11083        }
11084    }
11085    // Convert the first writes into declarations BEFORE removing defs
11086    // (removals shift indices); then remove the defs in descending
11087    // order.
11088    for (v, _, w) in &plan {
11089        if let Some(w) = w {
11090            if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = &mut stmts[*w] {
11091                if matches!(&**target, Expr::Local { var: vv, .. } if vv == v) {
11092                    let taken = std::mem::replace(&mut **value, Expr::Const(ConstVal::Null));
11093                    stmts[*w] = Stmt::LocalDef {
11094                        var: *v,
11095                        init: Some(taken),
11096                        is_final: false,
11097                        force_type: false,
11098                    };
11099                }
11100            }
11101        }
11102    }
11103    let mut order: Vec<usize> = plan.iter().map(|(_, q, _)| *q).collect();
11104    order.sort_unstable();
11105    order.reverse();
11106    for q in order {
11107        stmts.remove(q);
11108    }
11109}
11110
11111/// An INT-typed method returning a boolean value: the register held the
11112/// boolean test result in one generation and the `if (v) 1 else 0` int
11113/// in another; booleanize picks the boolean generation and the plain
11114/// `return v;` breaks ("boolean无法转换为int", weixin Handle
11115/// onPausableTransaction — the source was `if (v) 1 else 0`). Wrap the
11116/// return in the recovering conditional.
11117/// Mirror of fix_int_returns: a BOOLEAN method returning a numeric-
11118/// typed value (register reuse left an int generation) wraps it
11119/// `x != 0` — the dex branch encoding, and the only legal Java form
11120/// (kh5/a `return v116_g53;` int无法转换为boolean family).
11121pub fn fix_bool_returns(vt: &VarTable, body: &mut Stmt) {
11122    walk_mut_deep(body, &mut |st| {
11123        if let Stmt::Return(Some(e)) = st {
11124            let numeric = match &*e {
11125                Expr::Local { var, ty } => {
11126                    let t = if (*var as usize) < vt.vars.len() {
11127                        vt.var(*var).ty.erased()
11128                    } else {
11129                        ty.erased()
11130                    };
11131                    matches!(
11132                        t,
11133                        JavaType::Int
11134                            | JavaType::Long
11135                            | JavaType::Short
11136                            | JavaType::Byte
11137                            | JavaType::Char
11138                    )
11139                }
11140                other => matches!(
11141                    other.type_ref().erased(),
11142                    JavaType::Int | JavaType::Long | JavaType::Short | JavaType::Byte
11143                        | JavaType::Char
11144                ),
11145            };
11146            if numeric {
11147                let taken = e.clone();
11148                *e = Expr::Bin {
11149                    op: BinOp::Ne,
11150                    l: Box::new(taken),
11151                    r: Box::new(Expr::Const(ConstVal::Int(0))),
11152                    ty: Some(TypeRef::J(JavaType::Boolean)),
11153                };
11154            }
11155        }
11156    });
11157}
11158
11159pub fn fix_int_returns(vt: &VarTable, body: &mut Stmt) {
11160    walk_mut_deep(body, &mut |st| {
11161        if let Stmt::Return(Some(e)) = st {
11162            let is_bool = match e {
11163                Expr::Local { var, .. } => {
11164                    (*var as usize) < vt.vars.len()
11165                        && vt.vars[*var as usize].ty.erased() == JavaType::Boolean
11166                }
11167                Expr::Method { desc, .. } => desc.ret == JavaType::Boolean,
11168                _ => false,
11169            };
11170            if is_bool {
11171                *e = Expr::Cond {
11172                    c: Box::new(e.clone()),
11173                    t: Box::new(Expr::Const(ConstVal::Int(1))),
11174                    f: Box::new(Expr::Const(ConstVal::Int(0))),
11175                };
11176            }
11177        }
11178    });
11179}
11180
11181/// Generation splitting: when an assignment's value type is
11182/// INCOMPATIBLE with the var's declared type, mint a FRESH var for the
11183/// new generation instead of widening (widening was measured twice as
11184/// net-negative — every use-site cast gap re-triggers javac's
11185/// error-type contagion). Reads after the split point follow the
11186/// active generation; control-flow joins intersect; loops run a
11187/// 3-round fixpoint so the back-edge carries the tail generation to
11188/// the head reads (the loop-state-tuple shape, qf5/v0's
11189/// `u0 = (qf5.u0) get;` … `u0 = looper2;`).
11190///
11191/// Split condition (reference incompatibility or a primitive/value
11192/// mix — the instanceof-corruption shape): both specific references of
11193/// different classes, or prim→ref. NOT numeric-vs-numeric (promotion
11194/// handles those) and NOT Object targets (assignable — the narrowing
11195/// cast pass covers the value side).
11196static WIDEN_COUNT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
11197
11198pub fn split_generations(vt: &mut VarTable, body: &mut Stmt, pool: &DexPool) {
11199    if std::env::var("DDC_DBG_PHI").is_ok() {
11200        eprintln!("[widen] cumulative={}", WIDEN_COUNT.load(std::sync::atomic::Ordering::Relaxed));
11201    }
11202    let mut counter: u32 = 0;
11203    let mut gen: std::collections::HashMap<u32, u32> = std::collections::HashMap::default();
11204    split_walk_stmt(body, vt, &mut gen, &mut counter, pool);
11205}
11206
11207/// Should the assignment `target = value` split the target?
11208fn split_needed(vt: &VarTable, target: u32, value: &Expr, pool: &DexPool) -> bool {
11209    let n = vt.vars.len();
11210    if target as usize >= n {
11211        return false;
11212    }
11213    let declared = vt.vars[target as usize].ty.erased();
11214    let vt_is_ref = |t: &JavaType| matches!(t, JavaType::Object(_) | JavaType::Array(_));
11215    // Locals consult the VarTable: the embedded ty is a lift-time
11216    // snapshot, stale after booleanize converts a copy SOURCE (the
11217    // post-booleanize re-split below relies on seeing `v131` as the
11218    // boolean it became). Bitwise Or/And/Xor over boolean operands
11219    // carry the same staleness at the EXPRESSION level (dex lowered
11220    // Kotlin's non-short-circuit `or` to int `|` — the embedded Bin ty
11221    // stays Int after booleanize converts the operands): `int v18 =
11222    // delete | delete2` must split the target generation.
11223    fn side_bool(e: &Expr, vt: &VarTable) -> bool {
11224        match e {
11225            Expr::Local { var, .. } if (*var as usize) < vt.vars.len() => {
11226                matches!(vt.var(*var).ty.erased(), JavaType::Boolean)
11227            }
11228            Expr::Const(ConstVal::Int(0 | 1)) => true,
11229            Expr::Bin { op: BinOp::Or | BinOp::And | BinOp::Xor, l, r, .. } => {
11230                side_bool(l, vt) && side_bool(r, vt)
11231            }
11232            other => matches!(other.type_ref().erased(), JavaType::Boolean),
11233        }
11234    }
11235    let value_ty = match value {
11236        Expr::Local { var, ty } if (*var as usize) < vt.vars.len() => vt.var(*var).ty.erased(),
11237        Expr::Bin {
11238            op: BinOp::Or | BinOp::And | BinOp::Xor,
11239            l,
11240            r,
11241            ..
11242        } if side_bool(l, vt) && side_bool(r, vt) => JavaType::Boolean,
11243        other => other.type_ref().erased(),
11244    };
11245    if vt_is_ref(&declared) && vt_is_ref(&value_ty) {
11246        // Both references: split only when incompatible — different
11247        // classes where neither side is Object AND the value is not a
11248        // SUBTYPE of the declared type. An assignable store needs no
11249        // fresh generation; splitting one minted an orphan at an
11250        // if-join whose sibling arm kept the base (rename_gen_back's
11251        // primitive-only gate refused the undo, drop_dead_locals then
11252        // stripped the orphan to a bare allocation): pq0/g's bridge
11253        // `map2 = new LinkedHashMap()` against the HashMap-typed phi
11254        // rendered `new LinkedHashMap();` and the arm lost its store.
11255        let obj = JavaType::Object("java/lang/Object".into());
11256        if declared == value_ty || declared == obj || value_ty == obj {
11257            return false;
11258        }
11259        // A FRESH ALLOCATION whose class relation to the declared type
11260        // is UNKNOWABLE (a framework↔framework pair — LinkedHashMap
11261        // into a HashMap-typed phi): the dex stored the instance into
11262        // this register flow, so the store was verifier-legal; the
11263        // split cannot know better and orphaned the generation (the
11264        // if-join undo is primitive-only; the dropper stripped the
11265        // orphan to a bare `new LinkedHashMap();` and pq0/g's bridge
11266        // lost the else-arm store, killing the whole branched-super
11267        // merge). Pool-known pairs KEEP the split: the generation's
11268        // precise type is what downstream subtype sinks need (weibo
11269        // `v7 = new $reportWhenComplete$1(..)` into a ContinuationImpl
11270        // phi — exempting it made `v9($1) = v7` inconvertible, +50).
11271        if let (JavaType::Object(v), JavaType::Object(d)) = (&value_ty, &declared) {
11272            if matches!(value, Expr::New { .. })
11273                && (pool.get(v).is_none() || pool.get(d).is_none())
11274            {
11275                return false;
11276            }
11277        }
11278        true
11279    } else {
11280        // Primitive/reference mix: the value generation has a kind the
11281        // target can never accept — split (the declared type stays with
11282        // the old generation's readers).
11283        if vt_is_ref(&declared) != vt_is_ref(&value_ty) {
11284            return true;
11285        }
11286        // Boolean/numeric mix: `boolean` and `int` have NO cast between
11287        // them in Java, so a register reused across the kinds can only
11288        // be expressed as two generations (weixin ConstraintLayout
11289        // `boolean v60; int v56; v60 = v56;` — 9.5k bool↔int lines once
11290        // the obscuring suppression lifted). 0/1 constants are the
11291        // boolean encoding, not a numeric generation — never split on
11292        // those (booleanize owns them).
11293        let d_bool = matches!(declared, JavaType::Boolean);
11294        let v_bool = matches!(value_ty, JavaType::Boolean);
11295        let zero_one = matches!(value, Expr::Const(ConstVal::Int(0 | 1)));
11296        d_bool != v_bool && !zero_one
11297    }
11298}
11299
11300fn rewrite_gen_reads(e: &mut Expr, gen: &std::collections::HashMap<u32, u32>, vt: &VarTable) {
11301    deep_rewrite(e, &mut |x| {
11302        if let Expr::Local { var, ty } = x {
11303            if let Some(&to) = gen.get(var) {
11304                if to != *var {
11305                    *var = to;
11306                    *ty = vt.var(to).ty.clone();
11307                }
11308            }
11309        }
11310    });
11311}
11312
11313fn do_split(
11314    vt: &mut VarTable,
11315    target: &mut Expr,
11316    old: u32,
11317    value_ty: &JavaType,
11318    gen: &mut std::collections::HashMap<u32, u32>,
11319    counter: &mut u32,
11320) {
11321    *counter += 1;
11322    let info = &vt.vars[old as usize];
11323    let id = vt.vars.len() as u32;
11324    let name = format!("{}_g{}", info.name, counter);
11325    let slot = info.slot;
11326    // A split generation is a fresh LOCAL, not a parameter — even when
11327    // the parent register is one (the param identity stays on the
11328    // original var id; the generation only carries a later write). The
11329    // inherited flag made booleanize (and every other param-skip pass)
11330    // permanently ignore param-register generations: weixin v2/i's
11331    // `int v264_g160_g7 = !v200` with `!= 0` reads (bool→int assign
11332    // family, 2.8k lines).
11333    let is_param = false;
11334    vt.vars.push(jdc_core::var::VarInfo {
11335        id,
11336        slot,
11337        name,
11338        ty: TypeRef::J(value_ty.clone()),
11339        is_param,
11340        range_start: 0,
11341        range_end: u16::MAX,
11342        synthetic_name: true,
11343    });
11344    while vt.by_slot.len() <= slot as usize {
11345        vt.by_slot.push(Vec::new());
11346    }
11347    vt.by_slot[slot as usize].push((0, u16::MAX, id));
11348    gen.insert(old, id);
11349    if let Expr::Local { var, ty, .. } = target {
11350        *var = id;
11351        *ty = TypeRef::J(value_ty.clone());
11352    }
11353}
11354
11355/// Undo a DIVERGED generation split: rewrite every occurrence of the
11356/// freshly-minted generation `from` back to the pre-if var `to` within
11357/// `s` (both read positions and assignment targets, plus LocalDef var
11358/// fields). At a control-flow join only splits BOTH branches produced
11359/// survive (the phi merges on the shared var); a gen one branch minted
11360/// alone does not. But `do_split` already rewrote that branch's write
11361/// target IN PLACE, so without this undo the branch stores into a
11362/// write-only orphan gen that no confluence reader ever reads — the
11363/// value is silently lost AND the orphan renders undeclared (kt5.b
11364/// Kotlin default-arg bridge: `v18_g1 = p6x` in then vs `v18 = false`
11365/// in else, `this(.., v18, ..)` reads only else's value).
11366/// Batched rename_gen_back: one LocalDef pass + one expression pass with
11367/// a var→(target, ty) table, instead of two full subtree walks per undone
11368/// candidate (475k widenings on weixin paid 15µs each in walks).
11369fn rename_gen_back_batch(s: &mut Stmt, pairs: &[(u32, u32)], vt: &VarTable) {
11370    use jdc_core::FxHashMap;
11371    if pairs.is_empty() {
11372        return;
11373    }
11374    let map: FxHashMap<u32, (u32, JavaType)> = pairs
11375        .iter()
11376        .map(|&(from, to)| (from, (to, vt.vars[to as usize].ty.erased())))
11377        .collect();
11378    walk_mut_deep(s, &mut |st| {
11379        if let Stmt::LocalDef { var, .. } = st {
11380            if let Some(&(to, _)) = map.get(var) {
11381                *var = to;
11382            }
11383        }
11384    });
11385    rewrite_exprs(s, &mut |e| {
11386        deep_rewrite(e, &mut |x| {
11387            if let Expr::Local { var, ty } = x {
11388                if let Some(&(to, ref to_ty)) = map.get(var) {
11389                    *var = to;
11390                    *ty = TypeRef::J(to_ty.clone());
11391                }
11392            }
11393        });
11394    });
11395}
11396
11397/// Remove an undone generation's `by_slot` segment entry so slot-based
11398/// resolution can never hand back the now-unreferenced gen id (its
11399/// VarInfo stays — ids index `vars` — but nothing may select it).
11400fn drop_gen_slot(vt: &mut VarTable, dead: u32) {
11401    if (dead as usize) >= vt.vars.len() {
11402        return;
11403    }
11404    let slot = vt.vars[dead as usize].slot as usize;
11405    if let Some(seg) = vt.by_slot.get_mut(slot) {
11406        seg.retain(|&(_, _, id)| id != dead);
11407    }
11408}
11409
11410/// Type gate for `rename_gen_back`: merging gen `v` back onto base `k`
11411/// is only safe when the base can carry the gen's value — identical
11412/// types, or BOTH primitives (the bool/int register-reuse family that
11413/// booleanize + the cast fixers reconcile; the Kotlin bridge case,
11414/// kt5.b `int v18` receiving `boolean p6x`). A reference-vs-primitive
11415/// or cross-class merge would forge `String k = <boolean>`-style
11416/// assignments that never compile (the lark/weibo/reqable regression
11417/// of the ungated first cut: +8.4k `不兼容的类型`).
11418/// Pool/framework-aware least-upper-bound of two reference classes
11419/// (internal names): the most specific common ancestor through supers and
11420/// interfaces. `None` when nothing more specific than Object is common —
11421/// widening to Object would break member accesses on the base's readers,
11422/// so that case stays split.
11423/// Object-class name of a TypeRef without the JavaType clone `erased()`
11424/// performs (Array chains deep-clone) — the split-join candidate filter
11425/// ran two clones per candidate per diverged If and the clones dominated
11426/// the LUB change's wall cost.
11427/// Marker for a ref-pair candidate whose LUB is resolved lazily (after
11428/// the write-only read-gate); the empty string never collides with a real
11429/// internal class name (they always contain at least one '/').
11430fn lub_pending() -> Option<String> {
11431    Some(String::new())
11432}
11433
11434/// Resolution of a split-undo candidate after the read-gate.
11435enum Undo {
11436    /// Rename the gen back onto the base unchanged.
11437    Direct,
11438    /// Broaden the base var's type to this LUB, then rename.
11439    Widen(String),
11440    /// No safe merge: leave the branch's gen as-is.
11441    Refuse,
11442}
11443
11444fn obj_name(t: &TypeRef) -> Option<&std::sync::Arc<str>> {
11445    match t {
11446        TypeRef::J(JavaType::Object(o)) => Some(o),
11447        _ => None,
11448    }
11449}
11450
11451fn pool_lub(a: &str, b: &str, pool: &DexPool) -> Option<String> {
11452    if a == b {
11453        return Some(a.to_string());
11454    }
11455    // The hierarchy data is immutable for the process lifetime; the two
11456    // full walks (ancestor set of `a` + BFS of `b`) with per-node String
11457    // allocations ran per diverged-If candidate and quadrupled wall time
11458    // on the big corpora (weixin 19s -> 75s) — a thread-local memo makes
11459    // the second call a hash hit.
11460    thread_local! {
11461        static MEMO: std::cell::RefCell<jdc_core::FxHashMap<(String, String), Option<String>>> =
11462            std::cell::RefCell::new(jdc_core::FxHashMap::default());
11463    }
11464    if let Some(hit) = MEMO.with(|m| m.borrow().get(&(a.to_string(), b.to_string())).cloned()) {
11465        return hit;
11466    }
11467    let r = pool_lub_uncached(a, b, pool);
11468    MEMO.with(|m| m.borrow_mut().insert((a.to_string(), b.to_string()), r.clone()));
11469    r
11470}
11471
11472fn pool_lub_uncached(a: &str, b: &str, pool: &DexPool) -> Option<String> {
11473    let mut anc: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
11474    let mut stack: Vec<String> = vec![a.to_string()];
11475    while let Some(c) = stack.pop() {
11476        if !anc.insert(c.clone()) {
11477            continue;
11478        }
11479        if anc.len() > 1024 {
11480            return None;
11481        }
11482        if let Some(cls) = pool.get(&c) {
11483            if let Some(s) = &cls.super_name {
11484                stack.push(s.clone());
11485            }
11486            for i in &cls.interfaces {
11487                stack.push(i.clone());
11488            }
11489        } else {
11490            if let Some(s) = crate::fwdb::super_of(&c) {
11491                stack.push(s.to_string());
11492            }
11493            crate::fwdb::for_each_interface(&c, |i| stack.push(i.to_string()));
11494        }
11495    }
11496    let mut seen: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
11497    let mut queue: std::collections::VecDeque<String> = std::collections::VecDeque::new();
11498    queue.push_back(b.to_string());
11499    while let Some(c) = queue.pop_front() {
11500        if !seen.insert(c.clone()) {
11501            continue;
11502        }
11503        if c != "java/lang/Object" && anc.contains(&c) {
11504            return Some(c);
11505        }
11506        if seen.len() > 1024 {
11507            return None;
11508        }
11509        if let Some(cls) = pool.get(&c) {
11510            for i in &cls.interfaces {
11511                queue.push_back(i.clone());
11512            }
11513            if let Some(s) = &cls.super_name {
11514                queue.push_back(s.clone());
11515            }
11516        } else {
11517            crate::fwdb::for_each_interface(&c, |i| queue.push_back(i.to_string()));
11518            if let Some(s) = crate::fwdb::super_of(&c) {
11519                queue.push_back(s.to_string());
11520            }
11521        }
11522    }
11523    None
11524}
11525
11526fn undo_type_safe(vt: &VarTable, k: u32, v: u32) -> bool {
11527    if (k as usize) >= vt.vars.len() || (v as usize) >= vt.vars.len() {
11528        return false;
11529    }
11530    let kt = vt.vars[k as usize].ty.erased();
11531    let vt_ty = vt.vars[v as usize].ty.erased();
11532    if kt == vt_ty {
11533        return true;
11534    }
11535    let is_prim = |t: &JavaType| {
11536        matches!(
11537            t,
11538            JavaType::Int
11539                | JavaType::Long
11540                | JavaType::Short
11541                | JavaType::Byte
11542                | JavaType::Char
11543                | JavaType::Float
11544                | JavaType::Double
11545                | JavaType::Boolean
11546        )
11547    };
11548    is_prim(&kt) && is_prim(&vt_ty)
11549}
11550
11551fn split_walk_stmt(
11552    st: &mut Stmt,
11553    vt: &mut VarTable,
11554    gen: &mut std::collections::HashMap<u32, u32>,
11555    counter: &mut u32,
11556    pool: &DexPool,
11557) {
11558    match st {
11559        Stmt::Block(v) => {
11560            for x in v.iter_mut() {
11561                split_walk_stmt(x, vt, gen, counter, pool);
11562            }
11563        }
11564        Stmt::ExprStmt(e) => {
11565            rewrite_gen_reads(e, gen, vt);
11566            if let Expr::Assign {
11567                target,
11568                value,
11569                op: AssignOp::Plain,
11570                ..
11571            } = e
11572            {
11573                if let Expr::Local { var, .. } = &**target {
11574                    let old = *var;
11575                    if split_needed(vt, old, value, pool) {
11576                        let value_ty = value.type_ref().erased();
11577                        do_split(vt, target, old, &value_ty, gen, counter);
11578                    }
11579                }
11580            }
11581        }
11582        Stmt::LocalDef { var, init, .. } => {
11583            if let Some(e) = init {
11584                rewrite_gen_reads(e, gen, vt);
11585                if split_needed(vt, *var, e, pool) {
11586                    // The DEF mints its own generation: retarget the def
11587                    // to a fresh var (the bare decl of the ORIGINAL stays
11588                    // for its earlier readers).
11589                    let value_ty = e.type_ref().erased();
11590                    let old = *var;
11591                    let mut fake = Expr::Local {
11592                        var: old,
11593                        ty: vt.vars[old as usize].ty.clone(),
11594                    };
11595                    do_split(vt, &mut fake, old, &value_ty, gen, counter);
11596                    if let Expr::Local { var: nv, .. } = fake {
11597                        *var = nv;
11598                    }
11599                }
11600            }
11601        }
11602        Stmt::If {
11603            cond,
11604            then_stmt,
11605            else_stmt,
11606            ..
11607        } => {
11608            rewrite_gen_reads(cond, gen, vt);
11609            let then_floor = vt.vars.len() as u32;
11610            let mut g_then = gen.clone();
11611            split_walk_stmt(then_stmt, vt, &mut g_then, counter, pool);
11612            let else_floor = vt.vars.len() as u32;
11613            let mut g_else = gen.clone();
11614            if let Some(e) = else_stmt {
11615                split_walk_stmt(e, vt, &mut g_else, counter, pool);
11616            }
11617            // Undo splits that DIVERGED at this join (see
11618            // rename_gen_back), with TWO gates learned the hard way
11619            // (first cut regressed lark +8.4k):
11620            //  1. the SIBLING KEPT THE BASE — `g_else[k]` equals the
11621            //     entry mapping, i.e. only this branch minted a fresh
11622            //     gen for k. When both branches split, each orphan is
11623            //     declared by ensure_declared at its own value type and
11624            //     compiles; renaming them back onto the base would
11625            //     force cross-kind assignments (`int k = <boolean>`).
11626            //  2. the fresh gen is WRITE-ONLY in the branch — a gen
11627            //     read inside its branch renders fine on its own.
11628            // The gated shape is the Kotlin default-arg bridge phi
11629            // loss: then `v_g = p6x` / else `v = false` / confluence
11630            // reads v — the then value was silently dropped AND v_g
11631            // rendered undeclared (kt5.b). Undo restores the clean
11632            // diamond so fold_default_arg_bridge can fire.
11633            // Candidates first (the read-free conditions), then ONE
11634            // early-exit probe for the small candidate set — the old
11635            // read_counts_all rebuilt a HashMap over the WHOLE branch
11636            // subtree per diverged If (quadratic in nesting depth;
11637            // together with the split walk it was ~1/3 of worker CPU on
11638            // the QQ sample profile). The map was only ever consulted
11639            // as `reads(v) == 0` for these candidates.
11640            // Candidates carry an optional base widening: a REF pair the
11641            // strict type gate refuses can still merge when the pool
11642            // knows a common supertype (LinkedList + d96.p0 join to
11643            // java.util.List). The base widens to that LUB first, so
11644            // both branch writes and the confluence reads compile on
11645            // ONE var — the alternative (leaving the branch's gen) is
11646            // an undeclared write-only orphan and the branch value is
11647            // silently lost (weixin tp2/u2: the null fallback
11648            // `p03x = d96.p0.d` never reached addAll).
11649            // Stage the undo: cheap classification first, the
11650            // write-only read-gate second, and the pool LUB walks LAST —
11651            // only for survivors. Computing the LUB inside the filter ran
11652            // hierarchy walks for candidates the read-gate then dropped
11653            // (+40% wall on weixin).
11654            let mut undo_then: Vec<(u32, u32, Option<String>)> = g_then
11655                .iter()
11656                .filter(|(&k, &v)| {
11657                    v >= then_floor
11658                        && v < else_floor
11659                        && g_else.get(&k).copied() == gen.get(&k).copied()
11660                })
11661                .filter_map(|(&k, &v)| {
11662                    if undo_type_safe(vt, k, v) {
11663                        Some((v, k, None))
11664                    } else if let (Some(_), Some(_)) =
11665                        (obj_name(&vt.vars[k as usize].ty), obj_name(&vt.vars[v as usize].ty))
11666                    {
11667                        Some((v, k, lub_pending()))
11668                    } else {
11669                        None
11670                    }
11671                })
11672                .collect();
11673            if else_floor > then_floor && !undo_then.is_empty() {
11674                let vars: Vec<u32> = undo_then.iter().map(|&(v, _, _)| v).collect();
11675                let mut hits = vec![false; vars.len()];
11676                mark_read_vars(std::slice::from_ref(&**then_stmt), &vars, &mut hits);
11677                let mut i = 0usize;
11678                undo_then.retain(|_| {
11679                    let keep = !hits[i];
11680                    i += 1;
11681                    keep
11682                });
11683            }
11684            undo_then.sort_unstable_by_key(|&(v, _, _)| std::cmp::Reverse(v));
11685            // Resolve the widenings (only for read-gate survivors), apply
11686            // them to the base types, then ONE batched subtree rewrite.
11687            let mut pairs: Vec<(u32, u32)> = Vec::with_capacity(undo_then.len());
11688            for (v, k, pending) in undo_then {
11689                // Three-way resolution: Direct (types match, the primitive
11690                // family, or the gen value is assignable to the base)
11691                // renames as-is; Widen (a pool LUB exists) broadens the
11692                // base type first; Refused (no common supertype) SKIPS —
11693                // merging that pair would forge a cross-class assignment.
11694                let kind = match pending {
11695                    None => Undo::Direct,
11696                    Some(_) => {
11697                        let (a, b) = (
11698                            obj_name(&vt.vars[k as usize].ty),
11699                            obj_name(&vt.vars[v as usize].ty),
11700                        );
11701                        match (a, b) {
11702                            (Some(a), Some(b)) if pool.is_subtype(b, a) => Undo::Direct,
11703                            (Some(a), Some(b)) => match pool_lub(a, b, pool) {
11704                                Some(lub) => Undo::Widen(lub),
11705                                None => Undo::Refuse,
11706                            },
11707                            _ => Undo::Refuse,
11708                        }
11709                    }
11710                };
11711                match kind {
11712                    Undo::Refuse => continue,
11713                    Undo::Direct => pairs.push((v, k)),
11714                    Undo::Widen(lub) => {
11715                        WIDEN_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
11716                        vt.vars[k as usize].ty = TypeRef::J(JavaType::Object(lub.as_str().into()));
11717                        pairs.push((v, k));
11718                    }
11719                }
11720            }
11721            rename_gen_back_batch(then_stmt, &pairs, vt);
11722            for &(v, _) in &pairs {
11723                drop_gen_slot(vt, v);
11724            }
11725            if let Some(e) = else_stmt.as_deref_mut() {
11726                let mut undo_else: Vec<(u32, u32, Option<String>)> = g_else
11727                    .iter()
11728                    .filter(|(&k, &v)| {
11729                        v >= else_floor && g_then.get(&k).copied() == gen.get(&k).copied()
11730                    })
11731                    .filter_map(|(&k, &v)| {
11732                        if undo_type_safe(vt, k, v) {
11733                            Some((v, k, None))
11734                        } else if let (Some(_), Some(_)) =
11735                            (obj_name(&vt.vars[k as usize].ty), obj_name(&vt.vars[v as usize].ty))
11736                        {
11737                            Some((v, k, lub_pending()))
11738                        } else {
11739                            None
11740                        }
11741                    })
11742                    .collect();
11743                if (vt.vars.len() as u32) > else_floor && !undo_else.is_empty() {
11744                    let vars: Vec<u32> = undo_else.iter().map(|&(v, _, _)| v).collect();
11745                    let mut hits = vec![false; vars.len()];
11746                    mark_read_vars(std::slice::from_ref(&*e), &vars, &mut hits);
11747                    let mut i = 0usize;
11748                    undo_else.retain(|_| {
11749                        let keep = !hits[i];
11750                        i += 1;
11751                        keep
11752                    });
11753                }
11754                undo_else.sort_unstable_by_key(|&(v, _, _)| std::cmp::Reverse(v));
11755                let mut pairs: Vec<(u32, u32)> = Vec::with_capacity(undo_else.len());
11756                for (v, k, pending) in undo_else {
11757                    let kind = match pending {
11758                        None => Undo::Direct,
11759                        Some(_) => {
11760                            let (a, b) = (
11761                                obj_name(&vt.vars[k as usize].ty),
11762                                obj_name(&vt.vars[v as usize].ty),
11763                            );
11764                            match (a, b) {
11765                                (Some(a), Some(b)) if pool.is_subtype(b, a) => Undo::Direct,
11766                                (Some(a), Some(b)) => match pool_lub(a, b, pool) {
11767                                    Some(lub) => Undo::Widen(lub),
11768                                    None => Undo::Refuse,
11769                                },
11770                                _ => Undo::Refuse,
11771                            }
11772                        }
11773                    };
11774                    match kind {
11775                        Undo::Refuse => continue,
11776                        Undo::Direct => pairs.push((v, k)),
11777                        Undo::Widen(lub) => {
11778                            WIDEN_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
11779                            vt.vars[k as usize].ty = TypeRef::J(JavaType::Object(lub.as_str().into()));
11780                            pairs.push((v, k));
11781                        }
11782                    }
11783                }
11784                rename_gen_back_batch(e, &pairs, vt);
11785                for &(v, _) in &pairs {
11786                    drop_gen_slot(vt, v);
11787                }
11788            }
11789            // Join: keep only splits BOTH paths produced (an absent else
11790            // is a fall-through path that keeps the pre-if generation).
11791            gen.retain(|k, v| g_then.get(k) == Some(v) && g_else.get(k) == Some(v));
11792        }
11793        Stmt::While { cond, body, .. } | Stmt::DoWhile { cond, body, .. } => {
11794            // 3-round fixpoint: round N walks the cond+body with the
11795            // accumulated gen — the back-edge carries the tail
11796            // generation into the head reads of the next round.
11797            let entry = gen.clone();
11798            for _ in 0..3 {
11799                rewrite_gen_reads(cond, gen, vt);
11800                split_walk_stmt(body, vt, gen, counter, pool);
11801            }
11802            // After the loop the pre-loop generation is what a
11803            // zero-iteration execution left — conservative.
11804            *gen = entry;
11805        }
11806        Stmt::For { init, body, .. } => {
11807            for x in init.iter_mut() {
11808                split_walk_stmt(x, vt, gen, counter, pool);
11809            }
11810            let entry = gen.clone();
11811            for _ in 0..3 {
11812                split_walk_stmt(body, vt, gen, counter, pool);
11813            }
11814            *gen = entry;
11815        }
11816        Stmt::ForEach { body, .. } => {
11817            let entry = gen.clone();
11818            for _ in 0..3 {
11819                split_walk_stmt(body, vt, gen, counter, pool);
11820            }
11821            *gen = entry;
11822        }
11823        Stmt::Switch { cases, default, .. } => {
11824            let entry = gen.clone();
11825            for c in cases.iter_mut() {
11826                let mut g_case = entry.clone();
11827                for x in c.body.iter_mut() {
11828                    split_walk_stmt(x, vt, &mut g_case, counter, pool);
11829                }
11830                gen.retain(|k, v| g_case.get(k) == Some(v));
11831            }
11832            if let Some(d) = default {
11833                let mut g_def = entry.clone();
11834                split_walk_stmt(d, vt, &mut g_def, counter, pool);
11835                gen.retain(|k, v| g_def.get(k) == Some(v));
11836            }
11837        }
11838        Stmt::Try {
11839            body,
11840            catches,
11841            finally,
11842        } => {
11843            let entry = gen.clone();
11844            let mut g_try = entry.clone();
11845            split_walk_stmt(body, vt, &mut g_try, counter, pool);
11846            let mut g_all = g_try.clone();
11847            for c in catches.iter_mut() {
11848                // The handler runs with the PRE-try state (the exception
11849                // may fire anywhere in the try).
11850                let mut g_c = entry.clone();
11851                split_walk_stmt(&mut c.body, vt, &mut g_c, counter, pool);
11852                g_all.retain(|k, v| g_c.get(k) == Some(v));
11853            }
11854            *gen = g_all;
11855            if let Some(f) = finally {
11856                split_walk_stmt(f, vt, gen, counter, pool);
11857            }
11858        }
11859        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => {
11860            split_walk_stmt(body, vt, gen, counter, pool);
11861        }
11862        Stmt::Return(Some(e)) | Stmt::Throw(e) => {
11863            rewrite_gen_reads(e, gen, vt);
11864        }
11865        _ => {}
11866    }
11867}
11868
11869/// Framework exception super edge — the embedded API database
11870/// (fwdb) replaced the hand-written ~110-entry JDK/Android exception
11871/// supers table: real extends edges for EVERY framework class, so
11872/// multi-catch dedupe now covers chains the table missed (and cannot
11873/// carry a wrong entry — the table's failure mode was dropping a live
11874/// alternative).
11875fn fw_super(c: &str) -> Option<&'static str> {
11876    crate::fwdb::super_of(c)
11877}
11878
11879/// `sub <: sup` for exception-class names, combining the pool
11880/// hierarchy (which also matches framework ancestor NAMES on the way
11881/// up, so pool subclasses of framework exceptions resolve) with the
11882/// tabled framework chains for pairs where both sides are outside the
11883/// dex.
11884fn exc_subtype(sub: &str, sup: &str, pool: &DexPool) -> bool {
11885    if sub == sup {
11886        return true;
11887    }
11888    if pool.is_subtype(sub, sup) {
11889        return true;
11890    }
11891    let mut cur = sub;
11892    for _ in 0..24 {
11893        let Some(next) = fw_super(cur) else { break };
11894        if next == sup {
11895            return true;
11896        }
11897        cur = next;
11898    }
11899    false
11900}
11901
11902/// Drop multi-catch alternatives shadowed by another alternative in
11903/// the same handler (JLS 14.20.2 forbids subtype-related
11904/// alternatives; the dex verifier does not). Exact duplicates collapse
11905/// to the first occurrence. The caught set is unchanged, so this is
11906/// semantics-preserving by construction.
11907pub fn dedupe_multicatch(s: &mut Stmt, pool: &DexPool) {
11908    dedupe_multicatch_walk(s, pool);
11909}
11910
11911fn dedupe_multicatch_walk(s: &mut Stmt, pool: &DexPool) {
11912    match s {
11913        Stmt::Try {
11914            body,
11915            catches,
11916            finally,
11917        }
11918        | Stmt::TryWithResources {
11919            body,
11920            catches,
11921            finally,
11922            ..
11923        } => {
11924            dedupe_multicatch_walk(body, pool);
11925            for c in catches.iter_mut() {
11926                if c.exc.len() > 1 {
11927                    let mut keep: Vec<std::sync::Arc<str>> = Vec::with_capacity(c.exc.len());
11928                    for e in c.exc.iter() {
11929                        let shadowed = c.exc.iter().any(|o| {
11930                            o.as_ref() != e.as_ref() && exc_subtype(e, o, pool)
11931                        });
11932                        if !shadowed && !keep.iter().any(|k| k.as_ref() == e.as_ref()) {
11933                            keep.push(e.clone());
11934                        }
11935                    }
11936                    c.exc = keep;
11937                }
11938                dedupe_multicatch_walk(&mut c.body, pool);
11939            }
11940            if let Some(f) = finally {
11941                dedupe_multicatch_walk(f, pool);
11942            }
11943        }
11944        Stmt::Block(v) => {
11945            for x in v.iter_mut() {
11946                dedupe_multicatch_walk(x, pool);
11947            }
11948        }
11949        Stmt::If {
11950            then_stmt,
11951            else_stmt,
11952            ..
11953        } => {
11954            dedupe_multicatch_walk(then_stmt, pool);
11955            if let Some(e) = else_stmt {
11956                dedupe_multicatch_walk(e, pool);
11957            }
11958        }
11959        Stmt::While { body, .. }
11960        | Stmt::DoWhile { body, .. }
11961        | Stmt::For { body, .. }
11962        | Stmt::ForEach { body, .. }
11963        | Stmt::Labeled { body, .. }
11964        | Stmt::Synchronized { body, .. } => dedupe_multicatch_walk(body, pool),
11965        Stmt::Switch { cases, default, .. } => {
11966            for c in cases.iter_mut() {
11967                for x in c.body.iter_mut() {
11968                    dedupe_multicatch_walk(x, pool);
11969                }
11970            }
11971            if let Some(d) = default {
11972                dedupe_multicatch_walk(d, pool);
11973            }
11974        }
11975        _ => {}
11976    }
11977}
11978
11979/// Dangling `break L<id>`: a Goto whose paired Label/Labeled-wrap was
11980/// lost to structure degradation prints `break L<id>;` against an
11981/// undeclared label ("未定义的标签", weibo 371/lark 85/weixin 535).
11982/// Inside a loop the goto-to-loop-exit shape degrades to a plain
11983/// `break` — compilable, and the dominant original semantic. Outside
11984/// any loop the jump is irreducible flow (weixin protobuf state
11985/// machines: `goto` into a lost loop middle / dispatcher block — the
11986/// target exists but not at a position Java can express); it renders
11987/// as an honest `$DDC:` marker comment instead of a guaranteed
11988/// undefined-label error, and the block's tail after it is dropped
11989/// (the original control flow never reached those statements).
11990pub fn resolve_dangling_gotos(s: &mut Stmt) {
11991    let mut labels: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
11992    // walk_all is read-only — the whole-body clone this used to make
11993    // (4.6k profile samples of Stmt::clone) bought nothing.
11994    walk_all(s, &mut |st| {
11995        if let Stmt::Label(id) = st {
11996            labels.insert(*id);
11997        }
11998    });
11999    resolve_gotos_walk(s, &labels, 0);
12000}
12001
12002fn resolve_gotos_walk(s: &mut Stmt, labels: &jdc_core::FxHashSet<u32>, loop_depth: u32) {
12003    let depth = match s {
12004        Stmt::While { .. } | Stmt::DoWhile { .. } | Stmt::For { .. } | Stmt::ForEach { .. } => {
12005            loop_depth + 1
12006        }
12007        _ => loop_depth,
12008    };
12009    match s {
12010        Stmt::Goto(id) => {
12011            if labels.contains(id) {
12012                return; // valid pair — keep
12013            }
12014            if depth > 0 {
12015                *s = Stmt::Break(None);
12016            } else {
12017                *s = Stmt::Comment(format!(
12018                    "$DDC: unresolved jump to block {} (irreducible control flow)",
12019                    id
12020                ));
12021            }
12022        }
12023        Stmt::Block(v) => {
12024            let mut i = 0usize;
12025            while i < v.len() {
12026                // A depth-0 dangling Goto becomes a marker comment and
12027                // the original flow never reached the siblings after
12028                // it — dropping them keeps the rendered reachability
12029                // faithful (they were dead in the dex too).
12030                let dangling_here =
12031                    depth == 0 && matches!(&v[i], Stmt::Goto(id) if !labels.contains(id));
12032                resolve_gotos_walk(&mut v[i], labels, depth);
12033                if dangling_here {
12034                    v.truncate(i + 1);
12035                    break;
12036                }
12037                i += 1;
12038            }
12039        }
12040        Stmt::If { then_stmt, else_stmt, .. } => {
12041            resolve_gotos_walk(then_stmt, labels, depth);
12042            if let Some(e) = else_stmt {
12043                resolve_gotos_walk(e, labels, depth);
12044            }
12045        }
12046        Stmt::While { body, .. }
12047        | Stmt::DoWhile { body, .. }
12048        | Stmt::For { body, .. }
12049        | Stmt::ForEach { body, .. }
12050        | Stmt::Labeled { body, .. }
12051        | Stmt::Synchronized { body, .. } => resolve_gotos_walk(body, labels, depth),
12052        Stmt::Try {
12053            body,
12054            catches,
12055            finally,
12056        } => {
12057            resolve_gotos_walk(body, labels, depth);
12058            for c in catches.iter_mut() {
12059                resolve_gotos_walk(&mut c.body, labels, depth);
12060            }
12061            if let Some(f) = finally {
12062                resolve_gotos_walk(f, labels, depth);
12063            }
12064        }
12065        Stmt::Switch { cases, default, .. } => {
12066            for c in cases.iter_mut() {
12067                for x in c.body.iter_mut() {
12068                    resolve_gotos_walk(x, labels, depth);
12069                }
12070            }
12071            if let Some(d) = default {
12072                resolve_gotos_walk(d, labels, depth);
12073            }
12074        }
12075        _ => {}
12076    }
12077}
12078
12079/// READS of `v` across the statements (assignment targets excluded).
12080/// All-var read counts in ONE pass (reads semantics of stmts_read_var)
12081/// — for gates that test many candidate vars against the same subtree
12082/// (split_walk's undo filters ran a full arm walk PER candidate).
12083/// Which of `vars` appear in a READ position inside `stmts`? One walk,
12084/// a small linear membership check per read, short-circuiting the check
12085/// once every candidate is found — no HashMap, no per-node hashing.
12086/// Replaces the full-subtree `read_counts_all` maps the split undo-gates
12087/// used to build per diverged If (their only query was `reads(v) == 0`
12088/// for a handful of candidate gens; the rebuild was quadratic in If
12089/// nesting depth and dominated the split pass on deep methods).
12090fn mark_read_vars(stmts: &[Stmt], vars: &[u32], hits: &mut [bool]) {
12091    let mut remaining = vars.len();
12092    for s in stmts {
12093        if remaining == 0 {
12094            break;
12095        }
12096        visit_stmt_exprs_ro(s, &mut |e| {
12097            if remaining == 0 {
12098                return;
12099            }
12100            visit_exprs_reads(e, &mut |x| {
12101                if remaining == 0 {
12102                    return;
12103                }
12104                if let Expr::Local { var, .. } = x {
12105                    for (i, v) in vars.iter().enumerate() {
12106                        if *v == *var && !hits[i] {
12107                            hits[i] = true;
12108                            remaining -= 1;
12109                        }
12110                    }
12111                }
12112            });
12113        });
12114    }
12115}
12116
12117fn stmts_read_var(stmts: &[Stmt], v: u32) -> usize {
12118    let mut n = 0usize;
12119    for s in stmts {
12120        visit_stmt_exprs_ro(s, &mut |e| {
12121            visit_exprs_reads(e, &mut |x| {
12122                if let Expr::Local { var: vv, .. } = x {
12123                    if *vv == v {
12124                        n += 1;
12125                    }
12126                }
12127            });
12128        });
12129    }
12130    n
12131}
12132
12133pub fn fix_ctor_conditional_super(body: &mut Stmt) {
12134    // A single-statement body can arrive UNWRAPPED (bare `If` — the
12135    // gb6/e throw-guard family); the shape scans need a statement list.
12136    if !matches!(body, Stmt::Block(_)) {
12137        let inner = std::mem::replace(body, Stmt::Block(Vec::new()));
12138        *body = Stmt::Block(vec![inner]);
12139    }
12140    let Stmt::Block(stmts) = body else { return };
12141    // Unwrap single-statement Block wrappers: the structurer can nest
12142    // the whole body one level deeper, hiding the top-level If from the
12143    // shape scans (cleanup would flatten it — but runs AFTER this pass).
12144    while stmts.len() == 1 {
12145        let Stmt::Block(inner) = &stmts[0] else { break };
12146        *stmts = inner.clone();
12147    }
12148    if stmts.is_empty() {
12149        return;
12150    }
12151    if is_bare_ctor_call(stmts.first().unwrap()) {
12152        dedupe_ctor_delegations(body);
12153        return;
12154    }
12155    if try_linear_super_inline(stmts) {
12156        return;
12157    }
12158    try_merge_branched_super(stmts);
12159}
12160
12161/// Shape C: the delegation is already first — strip duplicate copies
12162/// left by R8 path-duplication (Kotlin default-arg bridge ctors: ft5/j
12163/// had `super()` leading PLUS another inside a branch — "对super的调用
12164/// 必须是构造器中的第一个语句" at the copy). Every path delegates
12165/// through the leading call, so structurally-identical (or empty-args,
12166/// same-kind) copies nested in blocks/ifs are removed, never moved.
12167/// Runs both ahead of the shape-A/B attempt and AFTER the plain hoist
12168/// (fix_ctor_super_first) — the hoist is what puts the leading
12169/// delegation in place for path-duplicated ctors.
12170/// A ctor that ALREADY delegates in its first statement can hold no
12171/// other `this(..)`/`super(..)` — Baidu Titan hotpatch instrumentation
12172/// injects a conditional re-delegation inside the `if ($ic != null)`
12173/// guard of every ctor (baidusearch ctor-not-first ×61,922 across
12174/// 21,813 files: `super(context); if ($ic != null) { ..; if
12175/// ((flag&1)!=0) { super((Context) callArgs[0]); ..; return; } }`).
12176/// The guard is dead code on the unpatched runtime path (and the
12177/// decompiled output has no patch runtime at all); dropping the
12178/// secondary delegation keeps the real semantics and restores the
12179/// Java first-statement rule. Runs after dedupe_ctor_delegations
12180/// (identical copies) and before extract_branched_delegation_helper
12181/// — gated on a LEADING delegation, so branched-delegation ctors
12182/// (no leading one) still get the helper treatment. Nested class
12183/// declarations are separate ctor scopes and are not entered.
12184pub fn strip_secondary_delegations(body: &mut Stmt) {
12185    let Stmt::Block(stmts) = body else { return };
12186    if !stmts.first().is_some_and(is_bare_ctor_call) {
12187        return;
12188    }
12189    fn strip_in(s: &mut Stmt) {
12190        if is_bare_ctor_call(s) {
12191            *s = Stmt::Block(Vec::new());
12192            return;
12193        }
12194        match s {
12195            Stmt::Block(v) => v.iter_mut().for_each(strip_in),
12196            Stmt::If {
12197                then_stmt,
12198                else_stmt,
12199                ..
12200            } => {
12201                strip_in(then_stmt);
12202                if let Some(e) = else_stmt {
12203                    strip_in(e);
12204                }
12205            }
12206            Stmt::While { body, .. }
12207            | Stmt::DoWhile { body, .. }
12208            | Stmt::ForEach { body, .. }
12209            | Stmt::Synchronized { body, .. }
12210            | Stmt::Labeled { body, .. } => strip_in(body),
12211            Stmt::For { init, body, .. } => {
12212                init.iter_mut().for_each(strip_in);
12213                strip_in(body);
12214            }
12215            Stmt::Switch { cases, default, .. } => {
12216                for c in cases.iter_mut() {
12217                    c.body.iter_mut().for_each(strip_in);
12218                }
12219                if let Some(d) = default {
12220                    strip_in(d);
12221                }
12222            }
12223            Stmt::Try {
12224                body,
12225                catches,
12226                finally,
12227            }
12228            | Stmt::TryWithResources {
12229                body,
12230                catches,
12231                finally,
12232                ..
12233            } => {
12234                strip_in(body);
12235                for c in catches.iter_mut() {
12236                    strip_in(&mut c.body);
12237                }
12238                if let Some(f) = finally {
12239                    strip_in(f);
12240                }
12241            }
12242            _ => {}
12243        }
12244    }
12245    for st in stmts.iter_mut().skip(1) {
12246        strip_in(st);
12247    }
12248}
12249
12250pub fn dedupe_ctor_delegations(body: &mut Stmt) {
12251    let Stmt::Block(stmts) = body else { return };
12252    let Some(d0) = stmts.first().and_then(|s| match s {
12253        Stmt::ExprStmt(e) if is_delegation_expr(e) => Some(e.clone()),
12254        _ => None,
12255    }) else {
12256        return;
12257    };
12258    fn rec(s: &mut Stmt, d0: &Expr) {
12259        match s {
12260            Stmt::Block(v) => {
12261                v.retain(|x| {
12262                    !matches!(x, Stmt::ExprStmt(e) if is_delegation_expr(e) && same_delegation(e, d0))
12263                });
12264                for x in v.iter_mut() {
12265                    rec(x, d0);
12266                }
12267            }
12268            Stmt::If { then_stmt, else_stmt, .. } => {
12269                rec(then_stmt, d0);
12270                if let Some(e) = else_stmt {
12271                    rec(e, d0);
12272                }
12273            }
12274            _ => {}
12275        }
12276    }
12277    // Top-level copies first (rec only strips copies NESTED in
12278    // blocks/ifs — a duplicate `super()` sitting directly in the body
12279    // list fell through its `_` arm: weixin ft5/j's second super()).
12280    {
12281        let d0r = &d0;
12282        let mut idx = 0usize;
12283        stmts.retain(|s| {
12284            idx += 1;
12285            idx == 1
12286                || !(matches!(s, Stmt::ExprStmt(e)
12287                    if is_delegation_expr(e) && same_delegation(e, d0r)))
12288        });
12289    }
12290    for s in stmts.iter_mut().skip(1) {
12291        rec(s, &d0);
12292    }
12293}
12294
12295/// Structural delegation identity for dedup: same kind/target and equal
12296/// args — or both arg-less (`super()` copies across duplicated paths
12297/// render with locally-renamed args yet delegate identically).
12298fn same_delegation(a: &Expr, b: &Expr) -> bool {
12299    match (a, b) {
12300        (
12301            Expr::Method { name: n1, cls: c1, is_super: s1, args: a1, .. },
12302            Expr::Method { name: n2, cls: c2, is_super: s2, args: a2, .. },
12303        ) => {
12304            &**n1 == "<init>"
12305                && &**n2 == "<init>"
12306                && c1 == c2
12307                && s1 == s2
12308                && (a1 == a2 || (a1.is_empty() && a2.is_empty()))
12309        }
12310        _ => false,
12311    }
12312}
12313
12314/// The `Method` expr of a bare ctor-delegation statement.
12315fn ctor_call_expr(s: &Stmt) -> Option<&Expr> {
12316    match s {
12317        Stmt::ExprStmt(e) if is_delegation_expr(e) => Some(e),
12318        _ => None,
12319    }
12320}
12321
12322/// Same delegation kind ignoring args (super vs this, same target).
12323fn same_ctor_kind(a: &Expr, b: &Expr) -> bool {
12324    match (a, b) {
12325        (
12326            Expr::Method { cls: c1, is_super: s1, owner: o1, .. },
12327            Expr::Method { cls: c2, is_super: s2, owner: o2, .. },
12328        ) => c1 == c2 && s1 == s2 && o1 == o2,
12329        _ => false,
12330    }
12331}
12332
12333/// Statements of a branch body, recursively flattening pure Blocks —
12334/// the fix runs BEFORE cleanup, so structurer-emitted nesting is still
12335/// in place. Any non-Block statement (If/Try/...) stays an item and
12336/// fails the def/call classification, which is the intended abort.
12337fn flat_list(s: &Stmt) -> Vec<&Stmt> {
12338    fn rec<'a>(s: &'a Stmt, out: &mut Vec<&'a Stmt>) {
12339        match s {
12340            Stmt::Block(v) => {
12341                for x in v {
12342                    rec(x, out);
12343                }
12344            }
12345            other => out.push(other),
12346        }
12347    }
12348    let mut out = Vec::new();
12349    rec(s, &mut out);
12350    out
12351}
12352
12353/// Blank the OWNER of un-folded `<init>` calls (lost allocation): the
12354/// printer renders them as `new X(..)` and never reads the owner, so its
12355/// register reference is not a real use (v7.a$a2: the dead
12356/// `new f(0,..)`'s owner was v1's register, inflating v1's use count
12357/// and blocking the inline-hoist).
12358fn strip_lost_alloc_owners(e: &mut Expr) {
12359    deep_rewrite(e, &mut |x| {
12360        if let Expr::Method { name, is_special: true, owner: Some(o), .. } = x {
12361            if &**name == "<init>" && !matches!(**o, Expr::This) {
12362                **o = Expr::Const(jdc_core::ir::expr::ConstVal::Int(0));
12363            }
12364        }
12365    });
12366}
12367
12368/// Per-var `Local` occurrence counts over an expression (single pass;
12369/// lost-alloc `<init>` owners excluded — see strip_lost_alloc_owners).
12370fn count_locals_expr(e: &Expr) -> jdc_core::FxHashMap<u32, usize> {
12371    let mut m = jdc_core::FxHashMap::default();
12372    count_expr_ro(e, &mut m);
12373    m
12374}
12375
12376pub(crate) fn count_locals_stmts(ss: &[Stmt]) -> jdc_core::FxHashMap<u32, usize> {
12377    let mut m = jdc_core::FxHashMap::default();
12378    for s in ss {
12379        visit_stmt_exprs_ro(s, &mut |e| count_expr_ro(e, &mut m));
12380    }
12381    m
12382}
12383
12384fn cnt(m: &jdc_core::FxHashMap<u32, usize>, v: u32) -> usize {
12385    m.get(&v).copied().unwrap_or(0)
12386}
12387
12388/// Substitute map-referenced locals, recursively expanding def chains
12389/// (depth-capped). Returns None when a referenced var has no def or the
12390/// chain is too deep.
12391fn inline_locals(
12392    e: &Expr,
12393    map: &std::collections::HashMap<u32, Expr>,
12394    depth: u32,
12395) -> Option<Expr> {
12396    if depth > 4 {
12397        return None;
12398    }
12399    let mut out = e.clone();
12400    let mut fail = false;
12401    deep_rewrite(&mut out, &mut |x| {
12402        if let Expr::Local { var, .. } = x {
12403            if let Some(def) = map.get(var) {
12404                match inline_locals(def, map, depth + 1) {
12405                    Some(r) => *x = r,
12406                    None => fail = true,
12407                }
12408            }
12409        }
12410    });
12411    if fail {
12412        None
12413    } else {
12414        Some(out)
12415    }
12416}
12417
12418/// A leading statement that defines a local: (var, init expr).
12419fn def_of(s: &Stmt) -> Option<(u32, &Expr)> {
12420    match s {
12421        Stmt::LocalDef { var, init: Some(e), .. } => Some((*var, e)),
12422        Stmt::ExprStmt(Expr::Assign {
12423            target,
12424            value,
12425            op: jdc_core::ir::expr::AssignOp::Plain,
12426            ..
12427        }) => match &**target {
12428            Expr::Local { var, .. } => Some((*var, value)),
12429            _ => None,
12430        },
12431        _ => None,
12432    }
12433}
12434
12435/// Shape A: straight-line prefix, delegation references prefix locals.
12436fn try_linear_super_inline(stmts: &mut Vec<Stmt>) -> bool {
12437    let Some(pos) = stmts.iter().position(is_bare_ctor_call) else {
12438        return false;
12439    };
12440    if pos == 0 {
12441        return false; // already first — nothing to do
12442    }
12443    // The prefix must be straight-line.
12444    if !stmts[..pos]
12445        .iter()
12446        .all(|s| matches!(s, Stmt::LocalDef { .. } | Stmt::ExprStmt(_)))
12447    {
12448        return false;
12449    }
12450    let Some(call) = ctor_call_expr(&stmts[pos]) else {
12451        return false;
12452    };
12453    let Expr::Method { args, .. } = call else {
12454        return false;
12455    };
12456    // Defs reachable from the args (last definition wins, bytecode order).
12457    let mut map: std::collections::HashMap<u32, Expr> =
12458        std::collections::HashMap::new();
12459    let mut def_idx: jdc_core::FxHashMap<u32, usize> =
12460        jdc_core::FxHashMap::default();
12461    for (i, s) in stmts[..pos].iter().enumerate() {
12462        if let Some((v, e)) = def_of(s) {
12463            map.insert(v, e.clone());
12464            def_idx.insert(v, i);
12465        }
12466    }
12467    // Which arg locals need inlining?
12468    let mut needed: Vec<u32> = Vec::new();
12469    for a in args {
12470        let mut probe = a.clone();
12471        strip_lost_alloc_owners(&mut probe);
12472        deep_rewrite(&mut probe, &mut |x| {
12473            if let Expr::Local { var, .. } = x {
12474                if map.contains_key(var) && !needed.contains(var) {
12475                    needed.push(*var);
12476                }
12477            }
12478        });
12479    }
12480    if needed.is_empty() {
12481        return false; // plain hoist handles it
12482    }
12483    // Guard: side-effecting defs inline only at a provably preserved
12484    // call count (exactly one use, the arg site).
12485    let mut consumed: Vec<usize> = Vec::new();
12486    let use_counts = count_locals_stmts(stmts);
12487    for &v in &needed {
12488        let def = &map[&v];
12489        let uses = cnt(&use_counts, v);
12490        if jdc_core::ir::build::has_side_effects(def) && uses != 1 {
12491            return false;
12492        }
12493        if uses == 1 {
12494            consumed.push(def_idx[&v]);
12495        }
12496    }
12497    // Build the inlined args.
12498    let mut new_args: Vec<Expr> = Vec::with_capacity(args.len());
12499    for a in args {
12500        match inline_locals(a, &map, 0) {
12501            Some(r) => new_args.push(r),
12502            None => return false,
12503        }
12504    }
12505    // Rebuild: super first (with inlined args), the unconsumed prefix
12506    // statements keep their order after it, then the original rest.
12507    let mut call_stmt = stmts.remove(pos);
12508    if let Stmt::ExprStmt(Expr::Method { args: slot, .. }) = &mut call_stmt {
12509        *slot = new_args;
12510    }
12511    let mut rest: Vec<Stmt> = Vec::with_capacity(stmts.len() + 1);
12512    rest.push(call_stmt);
12513    for (i, s) in stmts.drain(..).enumerate() {
12514        // Indices shift after `remove(pos)`: consumed holds pre-removal
12515        // indices; pos itself is already gone from `stmts`.
12516        let orig = if i >= pos { i + 1 } else { i };
12517        if !consumed.contains(&orig) {
12518            rest.push(s);
12519        }
12520    }
12521    *stmts = rest;
12522    true
12523}
12524
12525/// Shape B: if/else branches each ending in the same-kind delegation.
12526fn try_merge_branched_super(stmts: &mut Vec<Stmt>) -> bool {
12527    // Candidate If positions — the prelude before the chosen If must be
12528    // straight-line; try each If until one merges (nested/multi-if
12529    // ctors exist but the first matching one is the delegation site).
12530    let if_positions: Vec<usize> = stmts
12531        .iter()
12532        .enumerate()
12533        .filter(|(_, s)| matches!(s, Stmt::If { .. }))
12534        .map(|(i, _)| i)
12535        .collect();
12536    for if_pos in if_positions {
12537        // Cheap allocation-free gate: at least one branch must hold a
12538        // delegation (most ctor ifs are field null-checks and must not
12539        // pay the merge machinery). The OTHER side may be delegation-free
12540        // (throw-guard shape D) or absent (then-only shape E — the
12541        // rendered if/else form often only exists after later passes).
12542        let gate = match &stmts[if_pos] {
12543            Stmt::If { then_stmt, else_stmt, .. } => {
12544                contains_delegation(then_stmt)
12545                    || else_stmt.as_ref().is_some_and(|e| contains_delegation(e))
12546            }
12547            _ => false,
12548        };
12549        if gate && merge_at(stmts, if_pos) {
12550            return true;
12551        }
12552    }
12553    false
12554}
12555
12556/// Does this subtree contain a ctor delegation? (module-level: shared by
12557/// the merge gate and split_branch)
12558fn contains_delegation(s: &Stmt) -> bool {
12559    match s {
12560        Stmt::ExprStmt(e) => is_delegation_expr(e),
12561        Stmt::Block(v) => v.iter().any(contains_delegation),
12562        Stmt::If { then_stmt, else_stmt, .. } => {
12563            contains_delegation(then_stmt)
12564                || else_stmt.as_ref().is_some_and(|e| contains_delegation(e))
12565        }
12566        _ => false,
12567    }
12568}
12569
12570
12571
12572/// One merge attempt at `stmts[if_pos]`. See fix_ctor_conditional_super
12573/// for the doctrine. Extensions over the pure two-call merge:
12574/// - branch pre-call SIDE-EFFECT statements (Kotlin `o.h(param,..)`
12575///   null-checks — the zz5/j0 family) are allowed; they move into the
12576///   branch remainder (after super — the same reordering doctrine the
12577///   plain hoist already applies; Java has no pre-super statement form).
12578/// - a branch WITHOUT a delegation (throw-guard: `if (x==null) throw
12579///   .. else { super(..); .. }` — the gb6/e family) merges as pure
12580///   remainder; exactly one branch carries the call then, and no
12581///   ternary args arise (the guard throws before any path needs them).
12582/// - prelude single-assignment if/else (`if (c) {v=e1} else {v=e2}`)
12583///   folds into a Cond def for inlining (the zz5/j0 v15/v16 preludes).
12584fn merge_at(stmts: &mut Vec<Stmt>, if_pos: usize) -> bool {
12585    /// Branch classification: Call(at, defs, pre-call extra idxs) when a
12586    /// delegation sits behind only defs/side-effect statements; Clean
12587    /// when the branch has no delegation at all (pure remainder); Dirty
12588    /// otherwise (nested/conditional delegation — out of scope).
12589    enum Br {
12590        /// (call index, pre-call defs, def statement indices)
12591        Call(usize, Vec<(u32, Expr)>, Vec<usize>),
12592        Clean,
12593        Dirty,
12594    }
12595    fn split_branch(list: &[&Stmt]) -> Br {
12596        let call_pos = list.iter().position(|s| ctor_call_expr(s).is_some());
12597        let Some(c) = call_pos else {
12598            // No delegation at this level: a pure remainder branch
12599            // (throw-guard) unless one hides in nested control flow.
12600            return if list.iter().any(|s| contains_delegation(s)) {
12601                Br::Dirty
12602            } else {
12603                Br::Clean
12604            };
12605        };
12606        if list.iter().skip(c + 1).any(|s| ctor_call_expr(s).is_some()) {
12607            return Br::Dirty; // two delegations in one branch
12608        }
12609        let mut defs: Vec<(u32, Expr)> = Vec::new();
12610        let mut def_idxs: Vec<usize> = Vec::new();
12611        for (i, s) in list.iter().enumerate().take(c) {
12612            match def_of(s) {
12613                Some((v, e)) => {
12614                    defs.push((v, e.clone()));
12615                    def_idxs.push(i);
12616                }
12617                None => match s {
12618                    // Side-effect statements and bare decls ahead of the
12619                    // delegation ride along into the remainder.
12620                    Stmt::ExprStmt(_) | Stmt::LocalDef { .. } => {}
12621                    _ => return Br::Dirty, // control flow pre-call
12622                },
12623            }
12624        }
12625        Br::Call(c, defs, def_idxs)
12626    }
12627    /// Prelude single-assignment if/else → `v = c ? e1 : e2` def.
12628    fn cond_def_of(s: &Stmt, scope: &[Stmt]) -> Option<(u32, Expr)> {
12629        let Stmt::If { cond, then_stmt, else_stmt: Some(e), .. } = s else {
12630            return None;
12631        };
12632        fn one<'x>(b: &'x Stmt, scope: &[Stmt]) -> Option<(u32, &'x Expr)> {
12633            let l = flat_list(b);
12634            if l.len() == 1 {
12635                return def_of(l[0]);
12636            }
12637            // A [w = E; v = w] forwarding PAIR — the shape the
12638            // structurer leaves when E is impure (`c v5 = new c(..);
12639            // v6 = v5`) and forward_single_use's adjacency pass missed
12640            // it (a duplicated read elsewhere in its raw body broke the
12641            // single-read gate). Collapse to `v = E`, gated on w being
12642            // method-unique: the merge DROPS this branch, so any other
12643            // read/write of w would dangle (lark gp2/d Kotlin default-
12644            // arg bridge, this-not-first family).
12645            if l.len() == 2 {
12646                let (w, ex) = def_of(l[0])?;
12647                let Stmt::ExprStmt(Expr::Assign {
12648                    target,
12649                    op: AssignOp::Plain,
12650                    value,
12651                }) = l[1]
12652                else {
12653                    return None;
12654                };
12655                let Expr::Local { var: tv, .. } = &**target else {
12656                    return None;
12657                };
12658                let Expr::Local { var: rv, .. } = &**value else {
12659                    return None;
12660                };
12661                if *rv != w || *tv == w {
12662                    return None;
12663                }
12664                // stmts_count_writes only sees Expr-level assigns; the
12665                // pair's own def is often an init-bearing LocalDef, so
12666                // count those separately for the uniqueness gate.
12667                let mut ldef_writes = 0usize;
12668                for st in scope {
12669                    walk_all(st, &mut |x| {
12670                        if let Stmt::LocalDef {
12671                            var,
12672                            init: Some(_),
12673                            ..
12674                        } = x
12675                        {
12676                            if *var == w {
12677                                ldef_writes += 1;
12678                            }
12679                        }
12680                    });
12681                }
12682                if stmts_read_var(scope, w) != 1
12683                    || stmts_count_writes(scope, w) + ldef_writes != 1
12684                {
12685                    return None;
12686                }
12687                return Some((*tv, ex));
12688            }
12689            None
12690        }
12691        let (v1, e1) = one(then_stmt, scope)?;
12692        let (v2, e2) = one(e, scope)?;
12693        if v1 != v2 {
12694            return None;
12695        }
12696        Some((
12697            v1,
12698            Expr::Cond {
12699                c: Box::new(cond.clone()),
12700                t: Box::new(e1.clone()),
12701                f: Box::new(e2.clone()),
12702            },
12703        ))
12704    }
12705
12706    // Prelude: defs (flat or folded cond), bare decls, side-effect stmts.
12707    let mut pmap: std::collections::HashMap<u32, Expr> =
12708        std::collections::HashMap::new();
12709    let mut pidx: jdc_core::FxHashMap<u32, usize> =
12710        jdc_core::FxHashMap::default();
12711    for (i, s) in stmts[..if_pos].iter().enumerate() {
12712        match s {
12713            Stmt::LocalDef { .. } | Stmt::ExprStmt(_) => {
12714                if let Some((v, e)) = def_of(s) {
12715                    pmap.insert(v, e.clone());
12716                    pidx.insert(v, i);
12717                }
12718            }
12719            Stmt::If { .. } => match cond_def_of(s, stmts) {
12720                Some((v, e)) => {
12721                    pmap.insert(v, e);
12722                    pidx.insert(v, i);
12723                }
12724                None => return false,
12725            },
12726            _ => return false,
12727        }
12728    }
12729    let (cond, then_v, else_v, had_else) = match &stmts[if_pos] {
12730        Stmt::If { cond, then_stmt, else_stmt, .. } => (
12731            cond.clone(),
12732            then_stmt.as_ref().clone(),
12733            else_stmt.as_ref().map_or_else(|| Stmt::Block(Vec::new()), |e| e.as_ref().clone()),
12734            else_stmt.is_some(),
12735        ),
12736        _ => return false,
12737    };
12738    // Chained guards (Kotlin multi-param null-checks):
12739    // `if(a){throw..} else { if(b){throw..} else { super(..); .. } }` —
12740    // a branch that is a lone delegation-bearing If gets merged
12741    // recursively first (its delegation hoisted to the branch head), so
12742    // the outer split then sees [call, nested-guard-remainder].
12743    fn normalize_branch(b: &mut Stmt) {
12744        // MOVE-based (no deep clone): chained weixin Parcel ctors carry
12745        // huge tails; cloning per recursion level cost measurable wall
12746        // time. merge_at leaves stmts untouched on failure, so the taken
12747        // node can always go back.
12748        let slot: &mut Stmt = match b {
12749            Stmt::Block(v) if v.len() == 1 => &mut v[0],
12750            other => other,
12751        };
12752        if !matches!(slot, Stmt::If { .. }) || !contains_delegation(slot) {
12753            return;
12754        }
12755        let taken = std::mem::replace(slot, Stmt::Block(Vec::new()));
12756        let mut tmp = vec![taken];
12757        if merge_at(&mut tmp, 0) {
12758            *slot = Stmt::Block(tmp);
12759        } else {
12760            *slot = tmp.pop().unwrap();
12761        }
12762    }
12763    let mut then_v = then_v;
12764    let mut else_v = else_v;
12765    normalize_branch(&mut then_v);
12766    normalize_branch(&mut else_v);
12767    let then_list = flat_list(&then_v);
12768    let else_list = flat_list(&else_v);
12769
12770    // Exactly: two Call branches (merge with ternaries) or one Call +
12771    // one Clean (single-delegation throw-guard). Owned branch payloads.
12772    enum Side {
12773        Call {
12774            at: usize,
12775            defs: Vec<(u32, Expr)>,
12776            def_idxs: Vec<usize>,
12777        },
12778        Clean,
12779    }
12780    let (t_side, e_side, two_sided) = match (split_branch(&then_list), split_branch(&else_list)) {
12781        (Br::Call(tc, td, ti), Br::Call(ec, ed, ei)) => (
12782            Side::Call { at: tc, defs: td, def_idxs: ti },
12783            Side::Call { at: ec, defs: ed, def_idxs: ei },
12784            true,
12785        ),
12786        (Br::Call(tc, td, ti), Br::Clean) => (
12787            Side::Call { at: tc, defs: td, def_idxs: ti },
12788            Side::Clean,
12789            false,
12790        ),
12791        (Br::Clean, Br::Call(ec, ed, ei)) => (
12792            Side::Clean,
12793            Side::Call { at: ec, defs: ed, def_idxs: ei },
12794            false,
12795        ),
12796        _ => return false,
12797    };
12798    // The delegation nodes (two-sided: both; single: one).
12799    let (t_call, e_call) = match (&t_side, &e_side) {
12800        (Side::Call { at: ta, .. }, Side::Call { at: ea, .. }) => (
12801            ctor_call_expr(then_list[*ta]),
12802            ctor_call_expr(else_list[*ea]),
12803        ),
12804        (Side::Call { at: ta, .. }, Side::Clean) => {
12805            (ctor_call_expr(then_list[*ta]), None)
12806        }
12807        (Side::Clean, Side::Call { at: ea, .. }) => {
12808            (None, ctor_call_expr(else_list[*ea]))
12809        }
12810        _ => return false,
12811    };
12812    // The node that becomes the merged call: then side when present.
12813    let (call_list, call_at) = match &t_side {
12814        Side::Call { at, .. } => (&then_list, *at),
12815        Side::Clean => match &e_side {
12816            Side::Call { at, .. } => (&else_list, *at),
12817            Side::Clean => return false,
12818        },
12819    };
12820    let (base_args, other_args) = if two_sided {
12821        let (Some(tc), Some(ec)) = (t_call, e_call) else {
12822            return false;
12823        };
12824        if !same_ctor_kind(tc, ec) {
12825            return false;
12826        }
12827        let (Expr::Method { args: a1, .. }, Expr::Method { args: a2, .. }) = (tc, ec) else {
12828            return false;
12829        };
12830        if a1.len() != a2.len() {
12831            return false;
12832        }
12833        (a1, Some(a2))
12834    } else {
12835        let c = t_call.or(e_call);
12836        let Some(c) = c else { return false };
12837        let Expr::Method { args, .. } = c else {
12838            return false;
12839        };
12840        (args, None)
12841    };
12842
12843    // Def maps per side (prelude + own branch defs).
12844    let mut tmap: std::collections::HashMap<u32, Expr> = pmap.clone();
12845    let mut emap: std::collections::HashMap<u32, Expr> = pmap.clone();
12846    if let Side::Call { defs, .. } = &t_side {
12847        for (v, e) in defs {
12848            tmap.insert(*v, e.clone());
12849        }
12850    }
12851    if let Side::Call { defs, .. } = &e_side {
12852        for (v, e) in defs {
12853            emap.insert(*v, e.clone());
12854        }
12855    }
12856    let Some(cond_i) = inline_locals(&cond, &pmap, 0) else {
12857        return false;
12858    };
12859    // Inline each side's args through its own map.
12860    let mut then_args: Vec<Expr> = Vec::with_capacity(base_args.len());
12861    let mut else_args: Vec<Expr> = Vec::new();
12862    if two_sided {
12863        for a in base_args {
12864            match inline_locals(a, &tmap, 0) {
12865                Some(r) => then_args.push(r),
12866                None => return false,
12867            }
12868        }
12869        for a in other_args.unwrap() {
12870            match inline_locals(a, &emap, 0) {
12871                Some(r) => else_args.push(r),
12872                None => return false,
12873            }
12874        }
12875    } else {
12876        // Single-delegation side: base_args belong to whichever side
12877        // carries the call; pick that side's map.
12878        let from_then = matches!(&t_side, Side::Call { .. });
12879        for a in base_args {
12880            let r = if from_then {
12881                inline_locals(a, &tmap, 0)
12882            } else {
12883                inline_locals(a, &emap, 0)
12884            };
12885            match r {
12886                Some(r) => then_args.push(r),
12887                None => return false,
12888            }
12889        }
12890    }
12891    let primary_args = base_args;
12892    // Per-position merge.
12893    let mut merged: Vec<Expr> = Vec::with_capacity(then_args.len());
12894    let mut differ = 0usize;
12895    for (j, t) in then_args.iter().enumerate() {
12896        if two_sided {
12897            let e = &else_args[j];
12898            if t == e {
12899                merged.push(t.clone());
12900            } else {
12901                differ += 1;
12902                merged.push(Expr::Cond {
12903                    c: Box::new(cond_i.clone()),
12904                    t: Box::new(t.clone()),
12905                    f: Box::new(e.clone()),
12906                });
12907            }
12908        } else {
12909            merged.push(t.clone());
12910        }
12911    }
12912
12913    // ---- side-effect preservation guards (batched counts) ----
12914    let vars_in = |e: &Expr| -> Vec<u32> {
12915        let mut vs = Vec::new();
12916        let mut probe = e.clone();
12917        strip_lost_alloc_owners(&mut probe);
12918        deep_rewrite(&mut probe, &mut |x| {
12919            if let Expr::Local { var, .. } = x {
12920                if !vs.contains(var) {
12921                    vs.push(*var);
12922                }
12923            }
12924        });
12925        vs
12926    };
12927    // Remainder statements per branch (extras + post-call tail; a Clean
12928    // branch contributes all of its statements).
12929    // The consumed defs LEAVE the remainder: their expressions were
12930    // inlined into the merged delegation args, and a dead `v = E` left
12931    // after the call both reads wrong and trips the u_t1/u_t2 guards
12932    // (ContinuationImpl's branch-local `context = null / getContext()`
12933    // — the coroutine family's 对this的调用必须是第一个语句 root).
12934    let branch_remainder = |list: &[&Stmt], side: &Side| -> Vec<Stmt> {
12935        match side {
12936            Side::Clean => list.iter().map(|s| (*s).clone()).collect(),
12937            Side::Call { at, def_idxs, .. } => {
12938                list.iter()
12939                    .enumerate()
12940                    .filter(|(i, _)| *i != *at && !def_idxs.contains(i))
12941                    .map(|(_, s)| (*s).clone())
12942                    .collect()
12943            }
12944        }
12945    };
12946    let tail_then_v = branch_remainder(&then_list, &t_side);
12947    let tail_else_v = branch_remainder(&else_list, &e_side);
12948    let post = &stmts[if_pos + 1..];
12949    let mut inlined: Vec<u32> = vars_in(&cond);
12950    for a in primary_args.iter().chain(else_args.iter()) {
12951        for v in vars_in(a) {
12952            if !inlined.contains(&v) {
12953                inlined.push(v);
12954            }
12955        }
12956    }
12957    inlined.retain(|v| pmap.contains_key(v) || tmap.contains_key(v) || emap.contains_key(v));
12958    // Prelude-and-branch redefinition: ambiguous provenance — reject.
12959    for &v in &inlined {
12960        let in_branch = match (&t_side, &e_side) {
12961            (Side::Call { defs: td, .. }, Side::Call { defs: ed, .. }) => {
12962                td.iter().any(|(d, _)| *d == v) || ed.iter().any(|(d, _)| *d == v)
12963            }
12964            (Side::Call { defs, .. }, Side::Clean)
12965            | (Side::Clean, Side::Call { defs, .. }) => {
12966                defs.iter().any(|(d, _)| *d == v)
12967            }
12968            _ => false,
12969        };
12970        if pmap.contains_key(&v) && in_branch {
12971            return false;
12972        }
12973    }
12974    type Counts = jdc_core::FxHashMap<u32, usize>;
12975    type GuardCounts = (
12976        Counts,
12977        Vec<Counts>,
12978        Vec<Counts>,
12979        Counts,
12980        Counts,
12981        Counts,
12982        Vec<(u32, Counts)>,
12983    );
12984    let (cond_counts, a1_counts, a2_counts, tail1_counts, tail2_counts, post_counts, def_counts): GuardCounts =
12985        if inlined.is_empty() {
12986        Default::default()
12987    } else {
12988        let all_defs: Vec<(u32, &Expr)> = pmap
12989            .iter()
12990            .map(|(k, v)| (*k, v))
12991            .chain(match &t_side {
12992                Side::Call { defs, .. } => defs.iter().map(|(k, v)| (*k, v)).collect(),
12993                Side::Clean => Vec::new(),
12994            })
12995            .chain(match &e_side {
12996                Side::Call { defs, .. } => defs.iter().map(|(k, v)| (*k, v)).collect(),
12997                Side::Clean => Vec::new(),
12998            })
12999            .collect();
13000        (
13001            count_locals_expr(&cond),
13002            primary_args.iter().map(count_locals_expr).collect(),
13003            else_args.iter().map(count_locals_expr).collect(),
13004            count_locals_stmts(&tail_then_v),
13005            count_locals_stmts(&tail_else_v),
13006            count_locals_stmts(post),
13007            all_defs.into_iter().map(|(v, e)| (v, count_locals_expr(e))).collect(),
13008        )
13009        };
13010    for &v in &inlined {
13011        let in_t = matches!(&t_side, Side::Call { defs, .. } if defs.iter().any(|(d, _)| *d == v));
13012        let in_e = matches!(&e_side, Side::Call { defs, .. } if defs.iter().any(|(d, _)| *d == v));
13013        let effectful = [
13014            in_t.then(|| tmap.get(&v)).flatten(),
13015            in_e.then(|| emap.get(&v)).flatten(),
13016            pmap.get(&v),
13017        ]
13018        .into_iter()
13019        .flatten()
13020        .any(jdc_core::ir::build::has_side_effects);
13021        if !effectful {
13022            continue;
13023        }
13024        let u_cond = cnt(&cond_counts, v);
13025        let u_post = cnt(&post_counts, v);
13026        let u_t1 = cnt(&tail1_counts, v);
13027        let u_t2 = cnt(&tail2_counts, v);
13028        let mut u_odef = 0usize;
13029        for (dv, dc) in &def_counts {
13030            if *dv != v && inlined.contains(dv) {
13031                u_odef += cnt(dc, v);
13032            }
13033        }
13034        let mut emit = u_cond * (1 + differ);
13035        let mut uses_args = 0usize;
13036        for j in 0..primary_args.len() {
13037            let c1 = cnt(&a1_counts[j], v);
13038            let c2 = if two_sided && j < a2_counts.len() {
13039                cnt(&a2_counts[j], v)
13040            } else {
13041                0
13042            };
13043            uses_args += c1 + c2;
13044            emit += if two_sided && then_args[j] != else_args[j] {
13045                c1 + c2
13046            } else {
13047                c1.max(c2)
13048            };
13049        }
13050        let ok = if in_t || in_e {
13051            u_cond == 0
13052                && u_post == 0
13053                && u_t1 == 0
13054                && u_t2 == 0
13055                && u_odef == 0
13056                && a1_counts.iter().map(|c| cnt(c, v)).sum::<usize>() <= 1
13057                && a2_counts.iter().map(|c| cnt(c, v)).sum::<usize>() <= 1
13058                && emit <= 2
13059                && (in_t || a1_counts.iter().all(|c| cnt(c, v) == 0))
13060                && (in_e || a2_counts.iter().all(|c| cnt(c, v) == 0))
13061        } else {
13062            u_post == 0
13063                && u_t1 == 0
13064                && u_t2 == 0
13065                && u_odef == 0
13066                && ((u_cond == 1 && uses_args == 0 && emit <= 2)
13067                    || (u_cond == 0 && emit <= 1))
13068        };
13069        if !ok {
13070            return false;
13071        }
13072    }
13073
13074    // ---- assembly ----
13075    let mut call_stmt = call_list[call_at].clone();
13076    if let Stmt::ExprStmt(Expr::Method { args: slot, .. }) = &mut call_stmt {
13077        *slot = merged;
13078    }
13079    let residual = |v: u32| -> usize {
13080        cnt(&tail1_counts, v) + cnt(&tail2_counts, v) + cnt(&post_counts, v)
13081    };
13082    let mut kept_prelude: Vec<Stmt> = Vec::new();
13083    for (i, s) in stmts[..if_pos].iter().enumerate() {
13084        let consumed = match s {
13085            Stmt::LocalDef { var, .. } => Some(*var),
13086            Stmt::ExprStmt(Expr::Assign { target, .. }) => match &**target {
13087                Expr::Local { var, .. } => Some(*var),
13088                _ => None,
13089            },
13090            Stmt::If { .. } => cond_def_of(s, stmts).map(|(v, _)| v),
13091            _ => None,
13092        };
13093        if let Some(v) = consumed {
13094            if inlined.contains(&v) && pidx.get(&v) == Some(&i) && residual(v) == 0 {
13095                continue;
13096            }
13097        }
13098        kept_prelude.push(s.clone());
13099    }
13100    let build_keep = |list: &[&Stmt], side: &Side| -> Vec<Stmt> {
13101        match side {
13102            Side::Clean => list.iter().map(|s| (*s).clone()).collect(),
13103            Side::Call { at, .. } => {
13104                let mut out = Vec::new();
13105                for (i, s) in list.iter().enumerate() {
13106                    if i == *at {
13107                        continue;
13108                    }
13109                    if i < *at {
13110                        if let Some((v, _)) = def_of(s) {
13111                            if inlined.contains(&v) && residual(v) == 0 {
13112                                continue;
13113                            }
13114                        }
13115                    }
13116                    out.push((*s).clone());
13117                }
13118                out
13119            }
13120        }
13121    };
13122    let then_keep = build_keep(&then_list, &t_side);
13123    let else_keep = build_keep(&else_list, &e_side);
13124    let else_part = if else_keep.is_empty() && !had_else {
13125        None
13126    } else {
13127        Some(Box::new(Stmt::Block(else_keep)))
13128    };
13129    let new_if = Stmt::If {
13130        cond: cond_i,
13131        then_stmt: Box::new(Stmt::Block(then_keep)),
13132        else_stmt: else_part,
13133    };
13134    let if_empty = match &new_if {
13135        Stmt::If { then_stmt, else_stmt, .. } => {
13136            let be = |s: &Stmt| matches!(s, Stmt::Block(v) if v.is_empty());
13137            be(then_stmt) && else_stmt.as_ref().is_none_or(|e| be(e))
13138        }
13139        _ => false,
13140    };
13141    let mut out: Vec<Stmt> = Vec::with_capacity(stmts.len());
13142    out.push(call_stmt);
13143    out.extend(kept_prelude);
13144    if !if_empty {
13145        out.push(new_if);
13146    }
13147    out.extend(stmts[if_pos + 1..].to_vec());
13148    *stmts = out;
13149    true
13150}
13151
13152
13153pub fn strip_enum_ctor_super(body: &mut Stmt) {
13154    fn is_enum_super(s: &Stmt) -> bool {
13155        matches!(
13156            s,
13157            Stmt::ExprStmt(Expr::Method { name, is_super: true, .. }) if &**name == "<init>"
13158        )
13159    }
13160    // FULL depth: hotfix-guarded ctors hide a second super(String,int)
13161    // inside the proxy branch (alipay InstantRun: `if (proxy != null) {
13162    // super((String) v3[0], ..); proxy.afterSuper(this); return; }` at
13163    // depth 3+; the old two-level strip left it — super resolved against
13164    // Object once the fallback enum lost `extends Enum`, alipay
13165    // ctor-arity ×3,364). This path only runs for fallback enums whose
13166    // dex super is Enum/Object, so EVERY super call is a dead Enum-ctor
13167    // reference wherever it sits; constant subclasses (real supers)
13168    // take the other branch in method.rs and are untouched.
13169    fn strip_deep(stmts: &mut Vec<Stmt>) {
13170        stmts.retain(|s| !is_enum_super(s));
13171        for st in stmts.iter_mut() {
13172            strip_one(st);
13173        }
13174    }
13175    fn strip_one(st: &mut Stmt) {
13176        match st {
13177            Stmt::Block(v) => strip_deep(v),
13178            Stmt::If { then_stmt, else_stmt, .. } => {
13179                strip_one(then_stmt);
13180                if let Some(e) = else_stmt {
13181                    strip_one(e);
13182                }
13183            }
13184            Stmt::While { body, .. }
13185            | Stmt::DoWhile { body, .. }
13186            | Stmt::ForEach { body, .. }
13187            | Stmt::Synchronized { body, .. }
13188            | Stmt::Labeled { body, .. } => strip_one(body),
13189            Stmt::For { init, body, .. } => {
13190                strip_deep(init);
13191                strip_one(body);
13192            }
13193            Stmt::Switch { cases, default, .. } => {
13194                for c in cases.iter_mut() {
13195                    strip_deep(&mut c.body);
13196                }
13197                if let Some(d) = default {
13198                    strip_one(d);
13199                }
13200            }
13201            Stmt::Try { body, catches, finally }
13202            | Stmt::TryWithResources { body, catches, finally, .. } => {
13203                strip_one(body);
13204                for c in catches.iter_mut() {
13205                    strip_one(&mut c.body);
13206                }
13207                if let Some(f) = finally {
13208                    strip_one(f);
13209                }
13210            }
13211            _ => {}
13212        }
13213    }
13214    let Stmt::Block(stmts) = body else {
13215        return;
13216    };
13217    strip_deep(stmts);
13218}
13219
13220pub fn ensure_declared(body: &mut Stmt, vt: &VarTable) {
13221    // (Perf: dense Vec<bool> tables instead of HashSets — var ids are
13222    // dense; the `assigned` set collected here was never read and cost a
13223    // full extra tree walk per method.)
13224    let n = vt.vars.len().max(1);
13225
13226    // Demote extra LocalDefs (same var declared more than once) FIRST —
13227    // the scope analysis below must see exactly one def per var.
13228    let mut seen: HashSet<u32> = HashSet::default();
13229    demote_dup_decls(body, &mut seen, vt);
13230
13231    // Scope hoist: a LocalDef inside a nested block whose var is also
13232    // referenced OUTSIDE that block is a Java scope violation (the
13233    // branch-local `int v8 = pi.versionCode;` read after the try —
13234    // "cannot find symbol" at every outside use). Demote those defs to
13235    // assignments and let the bare top-of-method declaration below cover
13236    // the var. Two cheap passes: pre-order block numbering + per-var
13237    // inside/outside occurrence counts (pre-order subtree = id range).
13238    let mut def_block: Vec<u32> = vec![u32::MAX; n];
13239    let mut sizes: Vec<u32> = vec![0];
13240    let mut next_id: u32 = 1;
13241    scope_pass_a(body, 0, &mut next_id, &mut def_block, &mut sizes);
13242    sizes[0] = next_id;
13243    let mut within: Vec<u32> = vec![0; n];
13244    let mut total: Vec<u32> = vec![0; n];
13245    let mut next_b: u32 = 1;
13246    scope_pass_b(body, 0, &mut next_b, &def_block, &sizes, &mut within, &mut total);
13247    let mut hoist: HashSet<u32> = HashSet::default();
13248    for v in 0..n {
13249        if def_block[v] != u32::MAX && def_block[v] != 0 && within[v] < total[v] {
13250            hoist.insert(v as u32);
13251        }
13252    }
13253    if !hoist.is_empty() {
13254        let mut seed: HashSet<u32> = hoist.iter().copied().collect();
13255        demote_dup_decls(body, &mut seed, vt);
13256    }
13257
13258    let mut declared: Vec<bool> = vec![false; n];
13259    let mut is_param: Vec<bool> = vec![false; n];
13260    for v in &vt.vars {
13261        if v.is_param && (v.id as usize) < n {
13262            is_param[v.id as usize] = true;
13263        }
13264    }
13265    // Catch parameters are declared by the `catch (Type v)` clause —
13266    // bind_catches consumed their LocalDef before this pass ran, so
13267    // without this they re-declare at the top of the method and clash
13268    // with the clause (`Throwable th2;` + `catch (Throwable th2)` — a
13269    // syntax gate cannot see this, it is a semantic "already defined").
13270    let mut is_catch: Vec<bool> = vec![false; n];
13271    walk_all(body, &mut |st| {
13272        if let Stmt::Try { catches, .. } = st {
13273            for c in catches {
13274                if (c.var as usize) < n {
13275                    is_catch[c.var as usize] = true;
13276                }
13277            }
13278        }
13279    });
13280    walk_all(body, &mut |st| {
13281        if let Stmt::LocalDef { var, .. } = st {
13282            if (*var as usize) < n {
13283                declared[*var as usize] = true;
13284            }
13285        }
13286    });
13287    let mut used: HashSet<u32> = HashSet::default();
13288    // assignments=true: a var that only ever appears as an assign
13289    // TARGET still needs a declaration (`x = 5;` alone does not
13290    // declare x in Java).
13291    stmt_collect_vars(body, &mut used, true);
13292
13293    // Params are declared by the signature; hoisted vars lost their
13294    // LocalDef to the demotion above and always need the bare decl.
13295    let mut needs_decl: Vec<u32> = used
13296        .into_iter()
13297        .filter(|v| {
13298            let i = *v as usize;
13299            i >= n
13300                || ((!declared[i] && !is_param[i] && !is_catch[i]) || hoist.contains(v))
13301        })
13302        .collect();
13303    needs_decl.sort_unstable();
13304    needs_decl.dedup();
13305
13306    if !needs_decl.is_empty() {
13307        let mut decls: Vec<Stmt> = needs_decl
13308            .into_iter()
13309            .map(|v| Stmt::LocalDef {
13310                var: v,
13311                init: None,
13312                is_final: false,
13313                force_type: true,
13314            })
13315            .collect();
13316        decls.reverse();
13317        match body {
13318            Stmt::Block(v) => {
13319                for d in decls {
13320                    v.insert(0, d);
13321                }
13322            }
13323            other => {
13324                let inner = std::mem::replace(other, Stmt::Block(vec![]));
13325                *other = Stmt::Block({
13326                    let mut vs = decls;
13327                    vs.push(inner);
13328                    vs
13329                });
13330            }
13331        }
13332    }
13333}
13334
13335fn demote_dup_decls(s: &mut Stmt, seen: &mut HashSet<u32>, vt: &VarTable) {
13336    match s {
13337        // Exactly one visit per node: the Block arm consumes its children,
13338        // the fallthrough walk would double-visit them.
13339        Stmt::Block(v) => {
13340            for x in v.iter_mut() {
13341                demote_dup_decls(x, seen, vt);
13342            }
13343        }
13344        Stmt::LocalDef { var, init, .. } => {
13345            if seen.contains(var) {
13346                let ty = vt.var(*var).ty.clone();
13347                if let Some(e) = init.take() {
13348                    *s = Stmt::ExprStmt(Expr::Assign {
13349                        target: Box::new(Expr::Local { var: *var, ty }),
13350                        op: AssignOp::Plain,
13351                        value: Box::new(e),
13352                    });
13353                } else {
13354                    *s = Stmt::Block(vec![]);
13355                }
13356            } else {
13357                seen.insert(*var);
13358            }
13359        }
13360        other => {
13361            walk_mut(other, &mut |x| demote_dup_decls(x, seen, vt));
13362        }
13363    }
13364}
13365
13366#[allow(dead_code)]
13367fn unused(_: &Vec<CaseGroup>, _: &Catch) {}
13368
13369// ---------------------------------------------------------------------------
13370// jadx-style local names (ApplyVariableNames + dexdec's port of it)
13371// ---------------------------------------------------------------------------
13372
13373/// A synthetic `v12`/`p3` never survives when a better name exists.
13374/// Priority: (1) a Kotlin `Intrinsics.checkNotNullParameter(x, "name")`
13375/// names `x` from the message string (the string IS the parameter name);
13376/// (2) the single defining call — `getFoo()` → `foo`, `isFinishing()` →
13377/// `finishing`, `new File(…)` → `file`; (3) a type alias or the
13378/// lowercased class simple name. Collisions take `2`, `3`, …; a var with
13379/// two DISAGREEING defining calls stays unnamed (dexdec's
13380/// RelationalNameInference rule). Debug-info names are never touched
13381/// (`synthetic_name` gates the whole pass).
13382pub fn apply_local_names(vt: &mut VarTable, body: &Stmt) {
13383    use std::collections::{HashMap, HashSet};
13384
13385    // ---- proposals ------------------------------------------------------
13386    // var → (name, source-priority); disagreeing call names are dropped.
13387    let mut by_call: HashMap<u32, Vec<String>> = HashMap::new();
13388    let mut by_intrinsics: HashMap<u32, String> = HashMap::new();
13389
13390    visit_all_exprs(body, &mut |e| {
13391        if let Expr::Method { cls, name, args, .. } = e {
13392            if (cls.as_ref() == "kotlin/jvm/internal/Intrinsics"
13393                || cls.as_ref() == "kotlin/jvm/internal/IntrinsicsKt")
13394                && matches!(name.as_ref(), "checkNotNullParameter" | "checkParameterIsNotNull")
13395                && args.len() == 2
13396            {
13397                if let (Expr::Local { var, .. }, Expr::Const(ConstVal::Str(s))) = (&args[0], &args[1])
13398                {
13399                    by_intrinsics
13400                        .entry(*var)
13401                        .or_insert_with(|| sanitize_name(s).unwrap_or_default());
13402                }
13403            }
13404        }
13405    });
13406
13407    walk_all(body, &mut |st| {
13408        let (var, value) = match st {
13409            Stmt::LocalDef { var, init: Some(e), .. } => (*var, e),
13410            Stmt::ExprStmt(Expr::Assign { target, value, .. })
13411                if matches!(&**target, Expr::Local { .. }) =>
13412            {
13413                let Expr::Local { var, .. } = &**target else { return };
13414                (*var, value.as_ref())
13415            }
13416            _ => return,
13417        };
13418        if let Some(n) = defining_call_name(value) {
13419            by_call.entry(var).or_default().push(n);
13420        }
13421    });
13422
13423    // ---- reservation + application --------------------------------------
13424    // Real (debug-info) names can legally REPEAT across sibling source
13425    // scopes (`int i` in two loops) but our flat declaration hoisting
13426    // puts them in one scope — de-duplicate with the same numeric
13427    // suffixes the claim path uses.
13428    // Uniqueness operates on the SANITIZED display name: the identifier
13429    // sanitizer maps non-ASCII to `_`, so distinct debug names
13430    // (`ERROR_token参数缺失` / `ERROR_channelId参数缺失`) both RENDER as
13431    // `ERROR________` and collide as declarations. Tracking raw names
13432    // here would let the collision through.
13433    let sanit = |n: &str| crate::classdec::java_ident(n).into_owned();
13434    let mut taken: HashSet<String> = HashSet::default();
13435    // Parameter names are occupied REGARDLESS of syntheticness: claim()
13436    // hands out rename candidates, and without the synthetic params in
13437    // the pool a type fallback (`l7.p0` → "p0") renamed one parameter
13438    // ONTO another's slot-name (`b(v6.l p0, Object obj, l7.p0 p0)`).
13439    for v in vt.vars.iter() {
13440        // ALL synthetic names occupy their names, not just parameters:
13441        // a type fallback (`pc5.v56` → "v56") otherwise renames a local
13442        // ONTO another local's slot-name (`int v56;` beside
13443        // `pc5.v56 v56;`).
13444        if v.synthetic_name {
13445            taken.insert(sanit(&v.name));
13446        }
13447    }
13448    for v in vt.vars.iter_mut() {
13449        if v.synthetic_name {
13450            continue;
13451        }
13452        if !taken.insert(sanit(&v.name)) {
13453            let base = sanit(&v.name);
13454            for i in 2.. {
13455                let cand = format!("{base}{i}");
13456                if taken.insert(cand.clone()) {
13457                    v.name = cand;
13458                    break;
13459                }
13460            }
13461        }
13462    }
13463    let claim = |taken: &mut HashSet<String>, want: &str| -> String {
13464        let w = crate::classdec::java_ident(want).into_owned();
13465        if taken.insert(w.clone()) {
13466            return w;
13467        }
13468        for i in 2.. {
13469            let cand = format!("{w}{i}");
13470            if taken.insert(cand.clone()) {
13471                return cand;
13472            }
13473        }
13474        w
13475    };
13476
13477    for info in vt.vars.iter_mut() {
13478        if !info.synthetic_name {
13479            continue;
13480        }
13481        // (1) Intrinsics string — the Kotlin compiler wrote the real
13482        // parameter name right into the check.
13483        if let Some(n) = by_intrinsics.get(&info.id) {
13484            if !n.is_empty() {
13485                info.name = claim(&mut taken, n);
13486                continue;
13487            }
13488        }
13489        // (2) One consistent defining call.
13490        if let Some(names) = by_call.get(&info.id) {
13491            let unique: HashSet<&String> = names.iter().collect();
13492            if unique.len() == 1 {
13493                let n = unique.into_iter().next().unwrap();
13494                if !is_java_keyword(n) {
13495                    info.name = claim(&mut taken, n);
13496                    continue;
13497                }
13498            }
13499        }
13500        // (3) Type alias, else the lowercased simple class name
13501        // (jadx names a `Looper` local `looper`).
13502        let want = type_alias(&info.ty).map(str::to_string).or_else(|| simple_type_name(&info.ty));
13503        if let Some(n) = want {
13504            info.name = claim(&mut taken, &n);
13505        }
13506    }
13507
13508    // Final uniquification across ALL vars (params, locals, catch): the
13509    // reservation, claim and debug-name domains above are checked
13510    // pairwise but a name claimed LAST can still equal a debug name
13511    // that never re-checked (`zd1.v2 v2` beside `zd1.v2[] v2`).
13512    let mut seen: HashSet<String> = HashSet::default();
13513    for v in vt.vars.iter_mut() {
13514        if !seen.insert(sanit(&v.name)) {
13515            let base = sanit(&v.name);
13516            for i in 2.. {
13517                let cand = format!("{base}{i}");
13518                if seen.insert(cand.clone()) {
13519                    v.name = cand;
13520                    break;
13521                }
13522            }
13523        }
13524    }
13525}
13526
13527/// `com/android/.../Looper` → `looper` — the alias-table fallback.
13528fn simple_type_name(ty: &TypeRef) -> Option<String> {
13529    let TypeRef::J(JavaType::Object(n)) = ty else {
13530        return None;
13531    };
13532    let simple = n.rsplit('/').next().unwrap_or(n);
13533    let simple = simple.rsplit('$').next().unwrap_or(simple);
13534    // Lowercase the first ALPHABETIC char — `$`/`_`-prefixed names
13535    // (R8's `$$$_Thread`) must not survive capitalized.
13536    let mut chars = simple.chars();
13537    let mut out = String::with_capacity(simple.len());
13538    let mut lowered = false;
13539    for c in chars.by_ref() {
13540        if c.is_ascii_alphabetic() && !lowered {
13541            out.push(c.to_ascii_lowercase());
13542            lowered = true;
13543        } else {
13544            out.push(c);
13545        }
13546        if lowered {
13547            break;
13548        }
13549    }
13550    out.extend(chars);
13551    sanitize_name(&out)
13552}
13553
13554/// `getFoo()` → `foo`, `isFinishing()` → `finishing`, `new File(…)` →
13555/// `file`. The get/is prefixes only strip when the remainder starts
13556/// uppercase (so `issues()` keeps its name).
13557fn defining_call_name(e: &Expr) -> Option<String> {
13558    match e {
13559        Expr::Method { name, .. } => {
13560            let base = name
13561                .strip_prefix("get")
13562                .or_else(|| name.strip_prefix("is"))
13563                .filter(|rest| rest.chars().next().is_some_and(|c| c.is_ascii_uppercase()))
13564                .unwrap_or(name);
13565            let first = base.chars().next()?;
13566            let mut out = String::with_capacity(base.len());
13567            out.push(first.to_ascii_lowercase());
13568            out.extend(base.chars().skip(1));
13569            sanitize_name(&out)
13570        }
13571        Expr::New { cls, .. } => {
13572            let simple = cls.rsplit('/').next().unwrap_or(cls);
13573            let simple = simple.rsplit('$').next().unwrap_or(simple);
13574            let first = simple.chars().next()?;
13575            let mut out = String::with_capacity(simple.len());
13576            out.push(first.to_ascii_lowercase());
13577            out.extend(simple.chars().skip(1));
13578            sanitize_name(&out)
13579        }
13580        _ => None,
13581    }
13582}
13583
13584/// Valid java identifier, not a keyword/restricted name, ≥2 chars.
13585fn sanitize_name(s: &str) -> Option<String> {
13586    let mut out = String::with_capacity(s.len());
13587    for c in s.chars() {
13588        if c.is_ascii_alphanumeric() || c == '_' || c == '$' {
13589            out.push(c);
13590        } else {
13591            out.push('_');
13592        }
13593    }
13594    if out.len() < 2
13595        || out.chars().next().is_some_and(|c| c.is_ascii_digit())
13596        || is_java_keyword(&out)
13597    {
13598        return None;
13599    }
13600    Some(out)
13601}
13602
13603/// jadx's alias table for the common types, with the Android globals the
13604/// corpus actually shows; anything else falls back in the caller.
13605fn type_alias(ty: &TypeRef) -> Option<&'static str> {
13606    let TypeRef::J(JavaType::Object(n)) = ty else {
13607        return None;
13608    };
13609    Some(match n.as_ref() {
13610        "java/lang/String" | "kotlin/String" => "str",
13611        "java/lang/Class" => "cls",
13612        "java/lang/Throwable" => "th",
13613        "java/lang/Object" | "kotlin/Any" => "obj",
13614        "java/util/Iterator" | "kotlin/collections/Iterator" => "it",
13615        "java/lang/Boolean" => "bool",
13616        "java/lang/Integer" => "num",
13617        "java/lang/Character" => "ch",
13618        "java/lang/Byte" => "b",
13619        "java/lang/Short" => "sh",
13620        "java/lang/Float" => "f",
13621        "java/lang/Double" => "d",
13622        "java/lang/Long" => "i",
13623        "java/lang/StringBuilder" => "sb",
13624        "java/util/ArrayList" => "list",
13625        "java/util/HashMap" => "map",
13626        "android/content/Context" => "context",
13627        "android/content/Intent" => "intent",
13628        "android/os/Bundle" => "bundle",
13629        "android/view/View" => "view",
13630        "android/graphics/Bitmap" => "bitmap",
13631        "android/view/ViewGroup" => "viewGroup",
13632        _ => return None,
13633    })
13634}
13635
13636/// Every expression in the tree, statements included (read-only).
13637pub(crate) fn visit_all_exprs<F: FnMut(&Expr)>(s: &Stmt, f: &mut F) {
13638    walk_all(s, &mut |st| {
13639        let exprs: Vec<&Expr> = match st {
13640            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
13641                let mut v: Vec<&Expr> = vec![value];
13642                if !matches!(&**target, Expr::Local { .. }) {
13643                    v.push(target);
13644                }
13645                v
13646            }
13647            Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
13648                vec![e]
13649            }
13650            Stmt::Return(Some(e)) => vec![e],
13651            Stmt::LocalDef { init: Some(e), .. } => vec![e],
13652            Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
13653                vec![cond]
13654            }
13655            _ => Vec::new(),
13656        };
13657        for e in exprs {
13658            visit_exprs(e, f);
13659        }
13660    });
13661}
13662
13663/// IntDef/LongDef exact-match rendering: a literal argument to a
13664/// platform method whose parameter carries a constant domain renders as
13665/// the named constant (`setVisibility(8)` → `android.view.View.GONE`).
13666/// Combined flag values and literals outside the domain stay numeric.
13667pub fn platform_constants(body: &mut Stmt) {
13668    if !crate::platform::installed() {
13669        return;
13670    }
13671    rewrite_exprs(body, &mut |e| {
13672        if let Expr::Method { cls, name, desc, args, .. } = e {
13673            for a in args.iter_mut() {
13674                let (value, is_long) = match a {
13675                    Expr::Const(ConstVal::Int(v)) => (*v as i64, false),
13676                    Expr::Const(ConstVal::Long(v)) => (*v, true),
13677                    _ => continue,
13678                };
13679                if let Some(named) =
13680                    crate::platform::named_constant(cls, name, &desc_to_string(desc), 0, value)
13681                {
13682                    let _ = is_long;
13683                    *a = Expr::Raw(named);
13684                }
13685            }
13686        }
13687    });
13688}
13689
13690/// MethodDescriptor → dex descriptor string (`(I)V`).
13691fn desc_to_string(d: &jdc_core::types::MethodDescriptor) -> String {
13692    let mut out = String::from("(");
13693    for a in &d.args {
13694        type_to_desc(a, &mut out);
13695    }
13696    out.push(')');
13697    type_to_desc(&d.ret, &mut out);
13698    out
13699}
13700
13701fn type_to_desc(t: &jdc_core::types::JavaType, out: &mut String) {
13702    match t {
13703        JavaType::Void => out.push('V'),
13704        JavaType::Boolean => out.push('Z'),
13705        JavaType::Byte => out.push('B'),
13706        JavaType::Char => out.push('C'),
13707        JavaType::Short => out.push('S'),
13708        JavaType::Int => out.push('I'),
13709        JavaType::Float => out.push('F'),
13710        JavaType::Long => out.push('J'),
13711        JavaType::Double => out.push('D'),
13712        JavaType::Object(n) => {
13713            out.push('L');
13714            out.push_str(n);
13715            out.push(';');
13716        }
13717        JavaType::Array(inner) => {
13718            out.push('[');
13719            type_to_desc(inner, out);
13720        }
13721    }
13722}
13723
13724/// Kotlin `Intrinsics` null-check elision (jadx's ProcessKotlinInternals):
13725/// statement-position `checkNotNullParameter(p, "name")` /
13726/// `checkParameterIsNotNull` / `checkNotNull…` calls are runtime
13727/// assertions — drop them. The naming pass harvests the parameter
13728/// strings FIRST, so run this after `apply_local_names`.
13729pub fn remove_kotlin_checks(s: &mut Stmt) {
13730    walk_mut_deep(s, &mut |st| {
13731        let Stmt::Block(v) = st else { return };
13732        v.retain(|x| !is_kotlin_check(x));
13733    });
13734}
13735
13736fn is_kotlin_check(s: &Stmt) -> bool {
13737    let Stmt::ExprStmt(e) = s else { return false };
13738    let Expr::Method { cls, name, .. } = e else { return false };
13739    (cls.as_ref() == "kotlin/jvm/internal/Intrinsics" || cls.as_ref() == "kotlin/jvm/internal/IntrinsicsKt__Jdk7Kt")
13740        && matches!(
13741            name.as_ref(),
13742            "checkNotNullParameter"
13743                | "checkParameterIsNotNull"
13744                | "checkExpressionValueIsNotNull"
13745                | "checkNotNullExpressionValue"
13746                | "checkReturnedValueIsNotNull"
13747                | "checkFieldIsNotNull"
13748                | "checkNotNull"
13749        )
13750}
13751
13752// ---------------------------------------------------------------------------
13753// Synthetic-accessor inlining (jadx's MarkMethodsForInline, the safe subset)
13754// ---------------------------------------------------------------------------
13755
13756/// Inline `access$NNN`-style synthetic static bridges at their call
13757/// sites: an identity (`return pN`), a getter (`iget pX, field;
13758/// return vR`), or a method forwarder (`invoke; return`) — with the
13759/// d8 APM trace wrappers (`MethodCollector.i/.o` const-only static
13760/// calls) tolerated around the core. Only STATIC + SYNTHETIC callees
13761/// qualify (compiler-generated bridges: no override semantics, no
13762/// side effects beyond the forwarded operation).
13763/// Can `host` (internal class name) source-level reference `target`'s
13764/// class chain and member? Cross-package inlining of a synthetic
13765/// accessor used to EXPOSE package-private targets the raw dex never
13766/// referenced cross-package (BuildersKt.launch$default forwards to
13767/// kotlinx.coroutines.k.e — the widening census cannot see post-inline
13768/// calls; lark's 1,019-line k不是公共的 family). Non-public chain link
13769/// or non-public member across packages → the inline must not happen
13770/// (the public synthetic bridge stays and compiles).
13771fn inline_ref_ok(pool: &DexPool, host: &str, target: &str, member: Option<(&str, bool)>) -> bool {
13772    let hpkg = match host.rfind('/') {
13773        Some(i) => &host[..i],
13774        None => "",
13775    };
13776    let mut cur = target;
13777    loop {
13778        let tpkg = match cur.rfind('/') {
13779            Some(i) => &cur[..i],
13780            None => "",
13781        };
13782        if tpkg != hpkg {
13783            if let Some(pc) = pool.get(cur) {
13784                if pc.access & crate::access::ACC_PUBLIC == 0 {
13785                    return false;
13786                }
13787                if cur == target {
13788                    if let Some((mname, is_method)) = member {
13789                        let ok = if is_method {
13790                            pc.all_methods().any(|m| {
13791                                &*m.name == mname
13792                                    && m.access & crate::access::ACC_PUBLIC != 0
13793                            })
13794                        } else {
13795                            pc.static_fields
13796                                .iter()
13797                                .chain(pc.instance_fields.iter())
13798                                .any(|f| {
13799                                    f.name.as_ref() == mname
13800                                        && f.access & crate::access::ACC_PUBLIC != 0
13801                                })
13802                        };
13803                        // A member match requires the exact member to be
13804                        // public; an absent member (renamed/collapsed)
13805                        // stays allowed — the render will show what the
13806                        // pool has.
13807                        if !ok
13808                            && (if is_method {
13809                                pc.all_methods().any(|m| &*m.name == mname)
13810                            } else {
13811                                pc.static_fields
13812                                    .iter()
13813                                    .chain(pc.instance_fields.iter())
13814                                    .any(|f| f.name.as_ref() == mname)
13815                            })
13816                        {
13817                            return false;
13818                        }
13819                    }
13820                }
13821            }
13822        }
13823        match cur.rfind('$') {
13824            Some(i) if i > 0 => cur = &cur[..i],
13825            _ => break,
13826        }
13827    }
13828    true
13829}
13830
13831pub fn inline_accessors(s: &mut Stmt, pool: &DexPool, host: &str) {
13832    rewrite_exprs(s, &mut |e| {
13833        let Expr::Method { cls, name, desc, args, is_static, .. } = e else { return };
13834        if !*is_static || args.is_empty() {
13835            return;
13836        }
13837        let Some(target) = pool.get(cls) else { return };
13838        let desc_s = desc_to_string(desc);
13839        let Some(m) = target.find_method(name, &desc_s) else { return };
13840        if !(m.is_static() && m.access & access::ACC_SYNTHETIC != 0) {
13841            return;
13842        }
13843        let Some(dex) = pool.dex(m.dex_idx) else { return };
13844        // Snapshot first: the accessor's owning image may already be
13845        // retired, and a live read's success would depend on worker
13846        // completion interleaving — nondeterministic inlining (whether
13847        // the accessor folds at all) between identical runs.
13848        let code = match pool.accessor_code(m.dex_idx, m.code_off) {
13849            Some(bytes) => ddc_dex::CodeItem::parse(&bytes, 0),
13850            None => dex.code_at(m.code_off),
13851        };
13852        let Some(code) = code else { return };
13853        let insns: Vec<&Insn> =
13854            code.insns.iter().filter(|i| !matches!(i.kind, InsnKind::Nop)).collect();
13855        // Strip the APM trace wrappers (const-only invoke-static).
13856        let core: Vec<&Insn> = insns
13857            .iter()
13858            .copied()
13859            .filter(|i| !is_const_only_trace(&i.kind, &insns))
13860            .collect();
13861        match accessor_shape(&dex, &core, &code) {
13862            Some(Shape::Identity(arg_i)) => {
13863                if let Some(a) = args.get(arg_i) {
13864                    *e = a.clone();
13865                }
13866            }
13867            Some(Shape::FieldRead { arg, cls: field_cls, field, field_ty }) => {
13868                if !inline_ref_ok(pool, host, &field_cls, Some((&field, false))) {
13869                    return;
13870                }
13871                if let Some(owner) = args.get(arg).cloned().map(Box::new) {
13872                    let ty = TypeRef::J(desc_type(&field_ty));
13873                    // The inlined read must consult the member rename
13874                    // registry like every lifted field ref does — the
13875                    // raw dex name desynced from renamed declarations
13876                    // (lark uu4/f$b's `a`→`ax6`: 找不到符号 变量 a ×4.4k
13877                    // when the nested-collision deshadow fired).
13878                    let name = jdc_core::rename::field_display(
13879                        &field_cls,
13880                        &field,
13881                        &field_ty,
13882                    )
13883                    .map(std::sync::Arc::from)
13884                    .unwrap_or_else(|| field.into());
13885                    *e = Expr::Field {
13886                        owner: Some(owner),
13887                        cls: field_cls.into(),
13888                        name,
13889                        ty,
13890                        is_static: false,
13891                    };
13892                }
13893            }
13894            Some(Shape::Forward { cls: tcls, name: tname, desc: tdesc, instance }) => {
13895                if !inline_ref_ok(pool, host, &tcls, Some((&tname, true))) {
13896                    return;
13897                }
13898                // Same registry duty as FieldRead: the forwarded member
13899                // must render under its renamed display, not the raw
13900                // dex name.
13901                let disp = jdc_core::rename::field_display(
13902                    &tcls,
13903                    &tname,
13904                    &tdesc.to_string(),
13905                )
13906                .map(std::sync::Arc::from)
13907                .unwrap_or_else(|| tname.into());
13908                *cls = tcls.into();
13909                *name = disp;
13910                *desc = std::sync::Arc::new(tdesc);
13911                if instance && !args.is_empty() {
13912                    // The first param (the receiver) becomes the owner.
13913                    let recv = args.remove(0);
13914                    if let Expr::Method { owner, .. } = e {
13915                        *owner = Some(Box::new(recv));
13916                    }
13917                }
13918            }
13919            None => {}
13920        }
13921    });
13922}
13923
13924/// What an accessor body reduces to.
13925enum Shape {
13926    /// `return pN` — the call becomes argument N.
13927    Identity(usize),
13928    /// `iget vR, pX, field; return vR` — becomes `argN.field`.
13929    FieldRead { arg: usize, cls: String, field: String, field_ty: String },
13930    /// `invoke {pX, args…}, method@M; (move-result;)? return` —
13931    /// becomes the forwarded call.
13932    Forward { cls: String, name: String, desc: MethodDescriptor, instance: bool },
13933}
13934
13935/// Resolve the IGet field's owner class, name and type descriptor.
13936fn field_of(dex: &ddc_dex::DexFile, field_idx: u32) -> Option<(String, String, String)> {
13937    let f = dex.field(field_idx);
13938    Some((
13939        // class_name strips the `L...;` shell; type_name would leak a
13940        // descriptor into the Field's owner class (`La3.a.e_`).
13941        dex.class_name(f.class_idx),
13942        dex.string(f.name_idx).to_string(),
13943        // The FIELD TYPE stays a descriptor: desc_type parses it.
13944        dex.type_name(f.type_idx).to_string(),
13945    ))
13946}
13947
13948/// Map a register to a parameter index (static method: params occupy
13949/// the LAST ins_size registers).
13950fn param_index(code: &ddc_dex::CodeItem, reg: u16) -> Option<usize> {
13951    let rs = code.registers_size as usize;
13952    let ins = code.ins_size as usize;
13953    let r = reg as usize;
13954    let first = rs.checked_sub(ins)?;
13955    (r >= first && r < rs).then(|| r - first)
13956}
13957
13958fn accessor_shape(
13959    dex: &ddc_dex::DexFile,
13960    core: &[&Insn],
13961    code: &ddc_dex::CodeItem,
13962) -> Option<Shape> {
13963    // Dead consts that fed the stripped trace wrappers remain in the
13964    // core — skip them. ONLY when actually dead (dst unread by the
13965    // rest): a const-returning synthetic (`lambda$new$4(String) {
13966    // return false; }` = `const/4 v0,0; return v0`, registers=1 so v0
13967    // IS the param) lost its const and matched Identity(param0) —
13968    // inline_accessors then replaced the CALL with its ARGUMENT
13969    // (`return (String) obj` against a boolean SAM — weibo
13970    // IntentSanitizer lambdas ×116, and silently WRONG renders
13971    // wherever the types happened to agree).
13972    let reads = |rest: &[&Insn], r: u16| {
13973        rest.iter()
13974            .any(|i| crate::lift::src_regs(&i.kind).contains(&r))
13975    };
13976    let core: &[&Insn] = match core.split_first() {
13977        Some((first, rest))
13978            if matches!(first.kind, InsnKind::Const { dst, .. } if !reads(rest, dst)) =>
13979        {
13980            rest
13981        }
13982        _ => core,
13983    };
13984    let core: &[&Insn] = match core.split_last() {
13985        Some((last, rest))
13986            if matches!(last.kind, InsnKind::Const { dst, .. } if !reads(rest, dst)) =>
13987        {
13988            rest
13989        }
13990        _ => core,
13991    };
13992    match core {
13993        // return pN
13994        [Insn { kind: InsnKind::Return { src }, .. }] => {
13995            let idx = param_index(code, *src)?;
13996            Some(Shape::Identity(idx))
13997        }
13998        // iget vR, pX, field; return vR
13999        [
14000            Insn { kind: InsnKind::IGet { dst, obj, field_idx }, .. },
14001            Insn { kind: InsnKind::Return { src }, .. },
14002        ] if dst == src => {
14003            let arg = param_index(code, *obj)?;
14004            let (cls, field, field_ty) = field_of(dex, *field_idx)?;
14005            Some(Shape::FieldRead { arg, cls, field, field_ty })
14006        }
14007        // invoke {…}, method@M; (move-result; return)? — forwarder.
14008        [Insn { kind: InsnKind::Invoke { method_idx, regs, kind, .. }, .. }, rest @ ..] if rest.len() <= 2 => {
14009            // Only forward when every register is a param (no locals).
14010            if !regs.iter().all(|r| param_index(code, *r).is_some()) || regs.is_empty() {
14011                return None;
14012            }
14013            // Trailing must be move-result+return or return/void —
14014            // anything else (e.g. trace calls) already filtered upstream.
14015            if rest.len() == 2 {
14016                let ok = matches!(rest[0].kind, InsnKind::MoveResult { .. })
14017                    && matches!(rest[1].kind, InsnKind::Return { .. } | InsnKind::ReturnVoid);
14018                if !ok {
14019                    return None;
14020                }
14021            }
14022            let mid = dex.method(*method_idx);
14023            // class_name strips the `L...;` shell — type_name leaks a
14024            // descriptor into the forwarded call's class (`La3.a.e_`).
14025            let cls = dex.class_name(mid.class_idx);
14026            let name = dex.string(mid.name_idx).to_string();
14027            let mut s = String::from("(");
14028            for t in dex.proto_params(mid.proto_idx) {
14029                s.push_str(dex.type_name(*t));
14030            }
14031            s.push(')');
14032            s.push_str(dex.type_name(dex.proto(mid.proto_idx).return_type_idx));
14033            let desc = parse_method_descriptor(&s)?;
14034            Some(Shape::Forward { cls, name, desc, instance: !matches!(kind, InvokeKind::Static) })
14035        }
14036        _ => None,
14037    }
14038}
14039
14040
14041/// A const-only invoke-static (the d8 APM `i(732046)` / `o(732046)`
14042/// trace wrapper): every argument register is loaded by a Const (or a
14043/// Const-into-move chain) in the SAME body. Scoped to synthetic
14044/// accessors, so a genuinely side-effecting const call is never
14045/// mistaken for a trace.
14046fn is_const_only_trace(kind: &InsnKind, insns: &[&Insn]) -> bool {
14047    let InsnKind::Invoke { kind, regs, .. } = kind else { return false };
14048    if !matches!(kind, InvokeKind::Static) || regs.is_empty() {
14049        return false;
14050    }
14051    regs.iter().all(|r| const_loaded(*r, insns, 0))
14052}
14053
14054/// Const-loaded directly, or through a Move chain from a const-loaded
14055/// register (the APM `const v0, id` → `invoke {v0}` shape).
14056fn const_loaded(r: u16, insns: &[&Insn], depth: u8) -> bool {
14057    if depth > 3 {
14058        return false;
14059    }
14060    insns.iter().any(|i| match &i.kind {
14061        InsnKind::Const { dst, .. } => *dst == r,
14062        InsnKind::Move { dst, src } => *dst == r && const_loaded(*src, insns, depth + 1),
14063        _ => false,
14064    })
14065}