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_str() == name
1318                        || jdc_core::rename::field_display(cls, &f.name, &f.desc)
1319                            .is_some_and(|d| d == name)
1320                })
1321        }
1322        if declares(pc, cls, name) {
1323            return false;
1324        }
1325        // Field resolution walks the superclass chain, so "phantom" must
1326        // be PROVEN against the whole chain — a write to an inherited
1327        // field is real code (QQ com.tencent.gdtad.statistics.d inherits
1328        // `b` from super `e`; an own-fields-only verdict dropped the
1329        // legitimate `v10x.b = gdtAd2;` — silent data loss, latent until
1330        // the walk started descending into If.then_stmt). Off-pool
1331        // ancestors are enumerated through fwdb (framework fields like
1332        // View.mID are real); an ancestor NEITHER pool nor fwdb knows
1333        // (R8-stripped class) ends the proof — keep the write.
1334        let mut cur: Option<String> = pc.super_name.clone();
1335        let mut hops = 0u32;
1336        while let Some(s) = cur {
1337            hops += 1;
1338            if hops > 64 {
1339                return false;
1340            }
1341            if let Some(sup) = pool.get(&s) {
1342                if declares(sup, &s, name) {
1343                    return false;
1344                }
1345                cur = sup.super_name.clone();
1346            } else {
1347                if !crate::fwdb::exists(&s) {
1348                    return false;
1349                }
1350                let mut found = false;
1351                crate::fwdb::for_each_field(&s, |f, _| {
1352                    if f == name {
1353                        found = true;
1354                    }
1355                });
1356                if found {
1357                    return false;
1358                }
1359                // fwdb strips the Object super edge: the chain ends here.
1360                cur = crate::fwdb::super_of(&s).map(|x| x.to_string());
1361            }
1362        }
1363        true
1364    }
1365    fn droppable_value(pool: &DexPool, e: &Expr) -> bool {
1366        match e {
1367            Expr::Const(_) | Expr::Local { .. } => true,
1368            Expr::Field { cls, name, .. } => phantom(pool, cls, name),
1369            Expr::Cast { e, .. } => droppable_value(pool, e),
1370            _ => false,
1371        }
1372    }
1373    fn is_phantom_write(pool: &DexPool, st: &Stmt) -> bool {
1374        if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = st {
1375            if let Expr::Field { cls, name, .. } = &**target {
1376                return phantom(pool, cls, name) && droppable_value(pool, value);
1377            }
1378        }
1379        false
1380    }
1381    // walk_mut_deep over every Vec-holding node — the bare-Vec positions
1382    // (switch case bodies, For init, TWR resources) are the known blind
1383    // spot of Block-only recursion.
1384    walk_mut_deep(body, &mut |st| match st {
1385        Stmt::Block(v) => v.retain(|x| !is_phantom_write(pool, x)),
1386        Stmt::Switch { cases, .. } => {
1387            for c in cases.iter_mut() {
1388                c.body.retain(|x| !is_phantom_write(pool, x));
1389            }
1390        }
1391        Stmt::For { init, .. } => init.retain(|x| !is_phantom_write(pool, x)),
1392        Stmt::TryWithResources { resources, .. } => {
1393            resources.retain(|x| !is_phantom_write(pool, x))
1394        }
1395        _ => {}
1396    });
1397    if is_phantom_write(pool, body) {
1398        *body = Stmt::Block(vec![]);
1399    }
1400}
1401
1402/// Late default-init for surviving bare declarations (`T v;`). Runs
1403/// AFTER every fold/ctor/hotfix matcher that keys on `init: None`
1404/// (bind_catches, strip_clinit_hotfix_guard, the folding prefix scans),
1405/// so nothing observes the initialized form until emission. Dex
1406/// registers are zero-initialized at method entry, so `= null/0/false`
1407/// is the FAITHFUL value for any read that reaches it before the first
1408/// write — and javac's definite assignment rejects the bare form on
1409/// partial paths (可能尚未初始化变量: pdd gi1/a obj12 assigned in two
1410/// branches and read after the merge; also turns the residual
1411/// move-exception twins into compilable — and register-faithful —
1412/// null reads instead of undefined ones).
1413pub fn init_bare_decls(body: &mut Stmt, vt: &VarTable) {
1414    walk_mut_deep(body, &mut |st| {
1415        if let Stmt::LocalDef { var, init, .. } = st {
1416            if init.is_some() {
1417                return;
1418            }
1419            let lit = match vt.var(*var).ty.erased() {
1420                JavaType::Boolean => "false",
1421                JavaType::Int
1422                | JavaType::Byte
1423                | JavaType::Short
1424                | JavaType::Char => "0",
1425                JavaType::Long => "0L",
1426                JavaType::Float => "0.0f",
1427                JavaType::Double => "0.0d",
1428                JavaType::Void => return,
1429                JavaType::Object(_) | JavaType::Array(_) => "null",
1430            };
1431            *init = Some(Expr::Raw(lit.to_string()));
1432        }
1433    });
1434}
1435
1436fn strip_empty(s: &mut Stmt) {
1437    match s {
1438        Stmt::Block(v) => {
1439            v.retain(|x| !x.is_empty_block());
1440            for x in v.iter_mut() {
1441                strip_empty(x);
1442            }
1443        }
1444        Stmt::If {
1445            then_stmt,
1446            else_stmt,
1447            ..
1448        } => {
1449            strip_empty(then_stmt);
1450            if let Some(e) = else_stmt {
1451                strip_empty(e);
1452                if e.is_empty_block() {
1453                    *else_stmt = None;
1454                }
1455            }
1456        }
1457        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => strip_empty(body),
1458        Stmt::For { init, body, .. } => {
1459            init.retain(|x| !x.is_empty_block());
1460            strip_empty(body);
1461        }
1462        Stmt::ForEach { body, .. } => strip_empty(body),
1463        Stmt::Switch { cases, default, .. } => {
1464            for c in cases {
1465                c.body.retain(|x| !x.is_empty_block());
1466            }
1467            if let Some(d) = default {
1468                strip_empty(d);
1469            }
1470        }
1471        Stmt::Try {
1472            body,
1473            catches,
1474            finally,
1475        } => {
1476            strip_empty(body);
1477            for c in catches {
1478                strip_empty(&mut c.body);
1479            }
1480            if let Some(f) = finally {
1481                strip_empty(f);
1482            }
1483        }
1484        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => strip_empty(body),
1485        _ => {}
1486    }
1487}
1488
1489fn merge_singleton_stmts(s: &mut Stmt) {
1490    if let Stmt::Block(v) = s {
1491        for x in v.iter_mut() {
1492            merge_singleton_stmts(x);
1493        }
1494    }
1495}
1496
1497/// Drop statements after a definite terminator within a block.
1498pub fn prune_unreachable(s: &mut Stmt) {
1499    if let Stmt::Block(v) = s {
1500        let mut cut: Option<usize> = None;
1501        for (i, x) in v.iter().enumerate() {
1502            if matches!(x, Stmt::Return(_) | Stmt::Throw(_)) {
1503                cut = Some(i + 1);
1504                break;
1505            }
1506        }
1507        if let Some(c) = cut {
1508            if c < v.len() {
1509                v.truncate(c);
1510            }
1511        }
1512        for x in v.iter_mut() {
1513            prune_unreachable(x);
1514        }
1515    } else {
1516        walk_mut(s, &mut prune_unreachable);
1517    }
1518}
1519
1520fn walk_mut<F: FnMut(&mut Stmt)>(s: &mut Stmt, f: &mut F) {
1521    match s {
1522        Stmt::Block(v) => {
1523            for x in v.iter_mut() {
1524                f(x);
1525            }
1526        }
1527        Stmt::If {
1528            then_stmt,
1529            else_stmt,
1530            ..
1531        } => {
1532            f(then_stmt);
1533            if let Some(e) = else_stmt {
1534                f(e);
1535            }
1536        }
1537        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => f(body),
1538        Stmt::For { init, body, .. } => {
1539            for x in init.iter_mut() {
1540                f(x);
1541            }
1542            f(body);
1543        }
1544        Stmt::ForEach { body, .. } => f(body),
1545        Stmt::Switch { cases, default, .. } => {
1546            for c in cases {
1547                for x in c.body.iter_mut() {
1548                    f(x);
1549                }
1550            }
1551            if let Some(d) = default {
1552                f(d);
1553            }
1554        }
1555        Stmt::Try {
1556            body,
1557            catches,
1558            finally,
1559        } => {
1560            f(body);
1561            for c in catches {
1562                f(&mut c.body);
1563            }
1564            if let Some(fl) = finally {
1565                f(fl);
1566            }
1567        }
1568        Stmt::TryWithResources {
1569            resources,
1570            body,
1571            catches,
1572            finally,
1573        } => {
1574            for r in resources.iter_mut() {
1575                f(r);
1576            }
1577            f(body);
1578            for c in catches {
1579                f(&mut c.body);
1580            }
1581            if let Some(fl) = finally {
1582                f(fl);
1583            }
1584        }
1585        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => f(body),
1586        _ => {}
1587    }
1588}
1589
1590/// Count LocalDef statements (pipeline diagnostics).
1591pub fn count_localdefs(s: &Stmt, out: &mut usize) {
1592    if matches!(s, Stmt::LocalDef { .. }) {
1593        *out += 1;
1594    }
1595    walk_all(s, &mut |x| {
1596        if matches!(x, Stmt::LocalDef { .. }) {
1597            *out += 1;
1598        }
1599    });
1600}
1601
1602/// Drop a trailing `return;` (d8's explicit return-void at method end).
1603pub fn strip_trailing_void_return(s: &mut Stmt) {
1604    if let Stmt::Block(v) = s {
1605        loop {
1606            match v.last_mut() {
1607                Some(Stmt::Return(None)) => {
1608                    v.pop();
1609                }
1610                // The structurer can close a region in a bare nested
1611                // block; a `return;` at its tail is still a dangling
1612                // statement (inside a static initializer it is illegal
1613                // outright — clinit's closing return).
1614                Some(Stmt::Block(_)) => {
1615                    let last = v.last_mut().unwrap();
1616                    let before = match last {
1617                        Stmt::Block(b) => b.len(),
1618                        _ => 0,
1619                    };
1620                    strip_trailing_void_return(last);
1621                    let after = match last {
1622                        Stmt::Block(b) => b.len(),
1623                        _ => 0,
1624                    };
1625                    if after == before {
1626                        break; // nothing stripped inside
1627                    }
1628                }
1629                _ => break,
1630            }
1631        }
1632    }
1633}
1634
1635/// `if (c) { } else { B }` → `if (!c) { B }` — the empty-then shape is
1636/// how the structurer lands an inverted diamond, and source never writes
1637/// it (jadx prints the positive form). negate() carries De Morgan so
1638/// composed conditions stay clean.
1639pub fn invert_empty_thens(s: &mut Stmt) {
1640    walk_mut_deep(s, &mut |st| {
1641        if let Stmt::If {
1642            cond,
1643            then_stmt,
1644            else_stmt,
1645        } = st
1646        {
1647            let then_empty = matches!(&**then_stmt, Stmt::Block(v) if v.is_empty());
1648            if then_empty {
1649                if let Some(els) = else_stmt.take() {
1650                    let c = std::mem::replace(cond, Expr::Const(ConstVal::Int(0)));
1651                    *cond = jdc_core::convert::negate(c);
1652                    *then_stmt = els;
1653                }
1654            }
1655        }
1656    });
1657}
1658
1659/// Fold if-diamonds back into short-circuit conditions (DAD's
1660/// short_circuit_struct, statement level). The structurer emits nested
1661/// ifs for `a && b` / `a || b` bytecode diamonds; javac source almost
1662/// never nests them, and jadx folds all four shapes:
1663///   if (c1) { if (c2) {T} else {F} } else {F}  →  if (c1 && c2) {T} else {F}
1664///   if (c1) { if (c2) {F} else {T} } else {F}  →  if (c1 && !c2) {T} else {F}
1665///   if (c1) {T} else { if (c2) {T} else {F} }  →  if (c1 || c2) {T} else {F}
1666///   if (c1) {T} else { if (c2) {F} else {T} }  →  if (c1 || !c2) {T} else {F}
1667/// Branch identity is deep structural equality (Stmt: PartialEq);
1668/// iterate to a fixpoint so chained diamonds `(a && b) && c` collapse.
1669pub fn fold_short_circuits(s: &mut Stmt) {
1670    for _ in 0..8 {
1671        let mut changed = false;
1672        walk_mut_deep(s, &mut |st| {
1673            if try_fold_diamond(st) {
1674                changed = true;
1675            }
1676        });
1677        if !changed {
1678            break;
1679        }
1680    }
1681}
1682
1683fn try_fold_diamond(st: &mut Stmt) -> bool {
1684    enum Shape {
1685        ThenAnd,
1686        ThenAndNot,
1687        ElseOr,
1688        ElseOrNot,
1689        BareAnd,
1690        BareAndNot,
1691    }
1692    // Probe the shape on an immutable borrow first (the fold takes the
1693    // whole If apart; matching and rebuilding inside one borrow is not
1694    // expressible).
1695    let shape = match &*st {
1696        // One-sided diamonds: the shared branch is the implicit empty
1697        // fall-through (`if (c1) { if (c2) {T} }` → `if (c1 && c2) {T}`).
1698        Stmt::If {
1699            then_stmt,
1700            else_stmt: None,
1701            ..
1702        } => match &**then_stmt {
1703            Stmt::If {
1704                then_stmt: t2,
1705                else_stmt: None,
1706                ..
1707            } if !matches!(&**t2, Stmt::Block(v) if v.is_empty()) => Some(Shape::BareAnd),
1708            Stmt::If {
1709                then_stmt: t2,
1710                else_stmt: Some(_),
1711                ..
1712            } if matches!(&**t2, Stmt::Block(v) if v.is_empty()) => Some(Shape::BareAndNot),
1713            _ => None,
1714        },
1715        Stmt::If {
1716            then_stmt,
1717            else_stmt: Some(els),
1718            ..
1719        } => match (&**then_stmt, &**els) {
1720            (Stmt::If { else_stmt: Some(f2), .. }, _) if f2.as_ref() == els.as_ref() => {
1721                Some(Shape::ThenAnd)
1722            }
1723            (Stmt::If { then_stmt: t2, else_stmt: Some(_), .. }, _)
1724                if t2.as_ref() == els.as_ref() =>
1725            {
1726                Some(Shape::ThenAndNot)
1727            }
1728            (_, Stmt::If { then_stmt: t2, else_stmt: Some(_), .. })
1729                if t2.as_ref() == then_stmt.as_ref() =>
1730            {
1731                Some(Shape::ElseOr)
1732            }
1733            (_, Stmt::If { else_stmt: Some(f2), .. })
1734                if f2.as_ref() == then_stmt.as_ref() =>
1735            {
1736                Some(Shape::ElseOrNot)
1737            }
1738            _ => None,
1739        },
1740        _ => None,
1741    };
1742    let Some(shape) = shape else {
1743        return false;
1744    };
1745
1746    let taken = std::mem::replace(st, Stmt::Block(vec![]));
1747    // Bare (no-else) diamonds fold without an else at all.
1748    if matches!(shape, Shape::BareAnd | Shape::BareAndNot) {
1749        let Stmt::If {
1750            cond: c1,
1751            then_stmt,
1752            else_stmt: None,
1753        } = taken
1754        else {
1755            unreachable!()
1756        };
1757        let Stmt::If {
1758            cond: c2,
1759            then_stmt: t2,
1760            else_stmt: f2,
1761        } = *then_stmt
1762        else {
1763            unreachable!()
1764        };
1765        let sc = |op: BinOp, l: Expr, r: Expr| Expr::Bin {
1766            op,
1767            l: Box::new(l),
1768            r: Box::new(r),
1769            ty: None,
1770        };
1771        match shape {
1772            // if (c1 && c2) {T}
1773            Shape::BareAnd => {
1774                *st = Stmt::If {
1775                    cond: sc(BinOp::LogAnd, c1, c2),
1776                    then_stmt: t2,
1777                    else_stmt: None,
1778                };
1779            }
1780            // if (c1) { if (c2) {} else {T} }  →  if (c1 && !c2) {T}
1781            _ => {
1782                let body = f2.unwrap();
1783                *st = Stmt::If {
1784                    cond: sc(
1785                        BinOp::LogAnd,
1786                        c1,
1787                        Expr::Un {
1788                            op: UnOp::Not,
1789                            e: Box::new(c2),
1790                        },
1791                    ),
1792                    then_stmt: body,
1793                    else_stmt: None,
1794                };
1795            }
1796        }
1797        return true;
1798    }
1799    let Stmt::If {
1800        cond: c1,
1801        then_stmt,
1802        else_stmt: Some(els),
1803    } = taken
1804    else {
1805        unreachable!("shape probe guaranteed an If with else")
1806    };
1807    let sc = |op: BinOp, l: Expr, r: Expr| Expr::Bin {
1808        op,
1809        l: Box::new(l),
1810        r: Box::new(r),
1811        ty: None,
1812    };
1813    let not = |e: Expr| Expr::Un {
1814        op: UnOp::Not,
1815        e: Box::new(e),
1816    };
1817    match shape {
1818        Shape::BareAnd | Shape::BareAndNot => unreachable!("handled above"),
1819        Shape::ThenAnd | Shape::ThenAndNot => {
1820            let Stmt::If {
1821                cond: c2,
1822                then_stmt: t2,
1823                else_stmt: Some(f2),
1824            } = *then_stmt
1825            else {
1826                unreachable!()
1827            };
1828            let (nc, body, alt) = match shape {
1829                // if (c1 && c2) {T} else {F}
1830                Shape::ThenAnd => (sc(BinOp::LogAnd, c1, c2), t2, f2),
1831                // if (c1 && !c2) {T} else {F}   (inner: {F} else {T})
1832                _ => (sc(BinOp::LogAnd, c1, not(c2)), f2, t2),
1833            };
1834            *st = Stmt::If {
1835                cond: nc,
1836                then_stmt: body,
1837                else_stmt: Some(alt),
1838            };
1839        }
1840        Shape::ElseOr | Shape::ElseOrNot => {
1841            let Stmt::If {
1842                cond: c2,
1843                then_stmt: t2,
1844                else_stmt: Some(f2),
1845            } = *els
1846            else {
1847                unreachable!()
1848            };
1849            // The surviving then-body is the OUTER then (identical to the
1850            // matching inner branch by the probe).
1851            let body = then_stmt;
1852            let (nc, alt) = match shape {
1853                // if (c1 || c2) {T} else {F}   (inner: {T} else {F})
1854                Shape::ElseOr => (sc(BinOp::LogOr, c1, c2), f2),
1855                // if (c1 || !c2) {T} else {F}  (inner: {F} else {T})
1856                _ => (sc(BinOp::LogOr, c1, not(c2)), t2),
1857            };
1858            *st = Stmt::If {
1859                cond: nc,
1860                then_stmt: body,
1861                else_stmt: Some(alt),
1862            };
1863        }
1864    }
1865    true
1866}
1867
1868/// A static initializer cannot contain `return` in source form — the
1869/// clinit's closing return is a method-level artifact, but the
1870/// structurer can leave it nested inside loops/branches where the
1871/// top-level strip cannot see it (x2/a: `static { while { ...; return;
1872/// } }` — "return outside method", 184 sites on reqable).
1873pub fn strip_clinit_returns(s: &mut Stmt) {
1874    walk_mut_deep(s, &mut |st| {
1875        if let Stmt::Block(v) = st {
1876            v.retain(|x| !matches!(x, Stmt::Return(None)));
1877        }
1878    });
1879}
1880
1881pub fn prepend_comment(s: &mut Stmt, text: String) {
1882    let stmt = Stmt::Comment(text);
1883    match s {
1884        Stmt::Block(v) => v.insert(0, stmt),
1885        other => {
1886            let inner = std::mem::replace(other, Stmt::Block(vec![]));
1887            *other = Stmt::Block(vec![stmt, inner]);
1888        }
1889    }
1890}
1891
1892/// Fused expression rewrites that each used to walk the whole tree:
1893/// residual cmp sentinels, object-null comparisons, const-first compares.
1894/// One traversal, three rules — the separate walks were a top profile cost.
1895/// True when `e` evaluates to an object reference — a legal null-compare
1896/// side: locals via the reference table, everything else via its own type
1897/// (fields like `pi.versionName != 0` and call results are references too;
1898/// constants never are).
1899fn is_obj_expr(e: &Expr, obj_var: &dyn Fn(u32) -> bool) -> bool {
1900    match e {
1901        Expr::Local { var, .. } => obj_var(*var),
1902        Expr::Const(_) => false,
1903        _ => e.type_ref().erased().is_reference(),
1904    }
1905}
1906
1907/// `obj == 0` / `0 == obj` → `obj == null`: replace the CONST-0 side.
1908/// The previous shape replaced the LOCAL side instead — every object
1909/// null-check in the corpus rendered as `null != 0` (4,598 hits in
1910/// reqable alone) and the real operand vanished from the condition.
1911fn null_side_rewrite(x: &mut Expr, obj_var: &dyn Fn(u32) -> bool) {
1912    if let Expr::Bin { op, l, r, .. } = x {
1913        if !matches!(op, BinOp::Eq | BinOp::Ne) {
1914            return;
1915        }
1916        let l_zero = matches!(&**l, Expr::Const(ConstVal::Int(0)));
1917        let r_zero = matches!(&**r, Expr::Const(ConstVal::Int(0)));
1918        if l_zero && !r_zero && is_obj_expr(r, obj_var) {
1919            **l = Expr::Const(ConstVal::Null);
1920        } else if r_zero && !l_zero && is_obj_expr(l, obj_var) {
1921            **r = Expr::Const(ConstVal::Null);
1922        }
1923    }
1924}
1925
1926pub fn fused_expr_rewrites(s: &mut Stmt, vt: &VarTable) {
1927    // var ids are dense: a byte table beats a HashSet lookup.
1928    let mut obj_vars: Vec<bool> = Vec::with_capacity(vt.vars.len());
1929    for v in &vt.vars {
1930        obj_vars.push(v.ty.erased().is_reference());
1931    }
1932    rewrite_exprs(s, &mut |e| {
1933        deep_rewrite(e, &mut |x| {
1934            // 1. cmp sentinels.
1935            if let Expr::Invokedynamic { name, args, .. } = x {
1936                if name.starts_with('\0') && args.len() == 2 {
1937                    let (cls, ty) = match name.as_str() {
1938                        "\0cmp-long" => ("java/lang/Long", JavaType::Long),
1939                        "\0cmpl-float" | "\0cmpg-float" => ("java/lang/Float", JavaType::Float),
1940                        _ => ("java/lang/Double", JavaType::Double),
1941                    };
1942                    let desc = MethodDescriptor {
1943                        args: vec![ty.clone(), ty],
1944                        ret: JavaType::Int,
1945                    };
1946                    *x = Expr::Method {
1947                        owner: None,
1948                        cls: cls.into(),
1949                        name: "compare".into(),
1950                        desc: std::sync::Arc::new(desc),
1951                        args: args.clone(),
1952                        is_static: true,
1953                        is_interface: false,
1954                        is_special: false,
1955                        is_super: false,
1956                        is_dynamic: false,
1957                        type_args: vec![],
1958                    };
1959                    return;
1960                }
1961            }
1962            // 2. null compares (no any_obj gate: the object side may be
1963            // a field/call whose type is not in the local table).
1964            null_side_rewrite(x, &|v| obj_vars.get(v as usize).copied().unwrap_or(false));
1965            // 3. const-first compares.
1966            if let Expr::Bin { op, l, r, .. } = x {
1967                if matches!(
1968                    op,
1969                    BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
1970                ) {
1971                    let l_const = matches!(&**l, Expr::Const(_));
1972                    let r_var = matches!(&**r, Expr::Local { .. });
1973                    if l_const && r_var {
1974                        std::mem::swap(l, r);
1975                    }
1976                }
1977            }
1978        });
1979    });
1980    // 4. null into reference-typed assignment targets. The register
1981    //    machine stores null as const-0; return/throw/field-store/invoke
1982    //    argument paths were normalized in the lifter, but a null that
1983    //    MATERIALIZES into a local was rendered as `str = 0;` — javac
1984    //    "int cannot be converted to String" (the single biggest error
1985    //    family on real corpora: d8 emits null locals this way after
1986    //    every `x = null` branch).
1987    walk_mut_deep(s, &mut |st| {
1988        if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = st {
1989            let tgt_is_obj = match &**target {
1990                Expr::Local { var, .. } => {
1991                    obj_vars.get(*var as usize).copied().unwrap_or(false)
1992                }
1993                other => other.type_ref().erased().is_reference(),
1994            };
1995            if tgt_is_obj {
1996                if let Expr::Const(ConstVal::Int(0)) = &**value {
1997                    **value = Expr::Const(ConstVal::Null);
1998                }
1999            }
2000        } else if let Stmt::LocalDef { var, init, .. } = st {
2001            if obj_vars.get(*var as usize).copied().unwrap_or(false) {
2002                if let Some(iv) = init {
2003                    if let Expr::Const(ConstVal::Int(0)) = iv {
2004                        *iv = Expr::Const(ConstVal::Null);
2005                    }
2006                }
2007            }
2008        }
2009    });
2010}
2011
2012/// Replace residual cmp sentinels (`\0cmp*` Invokedynamic markers that never
2013/// reached a condition) with library compare calls.
2014#[allow(dead_code)]
2015pub fn desugar_cmp_residuals(s: &mut Stmt) {
2016    rewrite_exprs(s, &mut |e| {
2017        deep_rewrite(e, &mut |x| {
2018            if let Expr::Invokedynamic { name, args, .. } = x {
2019                if name.starts_with('\0') && args.len() == 2 {
2020                    let (cls, ty) = match name.as_str() {
2021                        "\0cmp-long" => ("java/lang/Long", JavaType::Long),
2022                        "\0cmpl-float" | "\0cmpg-float" => ("java/lang/Float", JavaType::Float),
2023                        _ => ("java/lang/Double", JavaType::Double),
2024                    };
2025                    let desc = jdc_core::types::MethodDescriptor {
2026                        args: vec![ty.clone(), ty],
2027                        ret: JavaType::Int,
2028                    };
2029                    *x = Expr::Method {
2030                        owner: None,
2031                        cls: cls.into(),
2032                        name: "compare".into(),
2033                        desc: std::sync::Arc::new(desc),
2034                        args: args.clone(),
2035                        is_static: true,
2036                        is_interface: false,
2037                        is_special: false,
2038                        is_super: false,
2039                        is_dynamic: false,
2040                        type_args: vec![],
2041                    };
2042                }
2043            }
2044        });
2045    });
2046}
2047
2048/// `if (c) { x = A; } else { x = B; }` → `x = c ? A : B;`
2049pub fn ternary_fold(s: &mut Stmt) {
2050    let mut changed = true;
2051    let mut guard = 0;
2052    while changed && guard < 8 {
2053        changed = false;
2054        guard += 1;
2055        ternary_fold_walk(s, &mut changed);
2056        strip_empty(s);
2057    }
2058}
2059
2060fn ternary_fold_walk(s: &mut Stmt, changed: &mut bool) {
2061    if let Stmt::Block(v) = s {
2062        for x in v.iter_mut() {
2063            ternary_fold_walk(x, changed);
2064        }
2065        return;
2066    }
2067    let Stmt::If {
2068        cond,
2069        then_stmt,
2070        else_stmt,
2071    } = s
2072    else {
2073        return;
2074    };
2075    ternary_fold_walk(then_stmt, changed);
2076    if let Some(e) = else_stmt {
2077        ternary_fold_walk(e, changed);
2078    }
2079    let Some(else_b) = else_stmt else { return };
2080    let target_of = |st: &Stmt| -> Option<u32> {
2081        let Stmt::ExprStmt(Expr::Assign { target, .. }) = st else {
2082            return None;
2083        };
2084        if let Expr::Local { var, .. } = &**target {
2085            Some(*var)
2086        } else {
2087            None
2088        }
2089    };
2090    let (Stmt::Block(tv), Stmt::Block(ev)) = (then_stmt.as_mut(), else_b.as_mut()) else {
2091        return;
2092    };
2093    if tv.len() != 1 || ev.len() != 1 {
2094        return;
2095    }
2096    let (Some(vt_), Some(ve)) = (target_of(&tv[0]), target_of(&ev[0])) else {
2097        return;
2098    };
2099    if vt_ != ve {
2100        return;
2101    }
2102    // The condition and both values must not re-assign the target between
2103    // the fold — they are single statements, so a mention is a re-read at
2104    // worst; re-assignment only happens via assignments inside them.
2105    let mut touched = HashSet::default();
2106    let (Stmt::ExprStmt(a_then), Stmt::ExprStmt(a_else)) = (&tv[0], &ev[0]) else {
2107        return;
2108    };
2109    if let Expr::Assign { value, .. } = a_then {
2110        collect_vars(value, &mut touched);
2111    }
2112    if let Expr::Assign { value, .. } = a_else {
2113        collect_vars(value, &mut touched);
2114    }
2115    if touched.contains(&vt_) {
2116        return;
2117    }
2118    let value_then = match a_then {
2119        Expr::Assign { value, .. } => value.clone(),
2120        _ => return,
2121    };
2122    let value_else = match a_else {
2123        Expr::Assign { value, .. } => value.clone(),
2124        _ => return,
2125    };
2126    let cond_e = cond.clone();
2127    let folded = Stmt::ExprStmt(Expr::Assign {
2128        target: Box::new(Expr::Local {
2129            var: vt_,
2130            ty: vt_join_ty(&value_then, &value_else),
2131        }),
2132        op: AssignOp::Plain,
2133        value: Box::new(Expr::Cond {
2134            c: Box::new(cond_e),
2135            t: value_then,
2136            f: value_else,
2137        }),
2138    });
2139    *s = Stmt::Block(vec![folded]);
2140    *changed = true;
2141}
2142
2143fn vt_join_ty(a: &Expr, b: &Expr) -> TypeRef {
2144    let ta = a.type_ref();
2145    let tb = b.type_ref();
2146    if ta.erased() == tb.erased() {
2147        return ta;
2148    }
2149    TypeRef::J(JavaType::Object("java/lang/Object".into()))
2150}
2151
2152/// Fold `new StringBuilder(...).append(x)...toString()` chains into
2153/// `StringConcat`. Single-assignment temporaries carry the chain.
2154///
2155/// d8 drops append results (statement-form calls), so a builder whose chain
2156/// has STATEMENT appends attached is left alone: folding its `toString`
2157/// with only the ctor arguments would misstate the contents.
2158pub fn fold_string_builders(s: &mut Stmt, vt: &VarTable) {
2159    // var → assigned value, for vars assigned exactly once.
2160    let n_vars = vt.vars.len().max(1);
2161    let mut counts = vec![0usize; n_vars];
2162    count_assignments(s, &mut counts);
2163    let mut values: Vec<Option<Expr>> = vec![None; n_vars];
2164    record_assignments(s, &counts, &mut values);
2165
2166    // Builders with statement-form appends (their chains are incomplete).
2167    let mut appended_stmts: HashSet<u32> = HashSet::default();
2168    walk_all(s, &mut |st| {
2169        if let Stmt::ExprStmt(Expr::Method {
2170            cls,
2171            name,
2172            owner,
2173            args,
2174            ..
2175        }) = st
2176        {
2177            if name.as_ref() == "append" && is_string_builder(cls) && args.len() == 1 {
2178                if let Some(Expr::Local { var, .. }) = owner.as_deref() {
2179                    appended_stmts.insert(*var);
2180                }
2181            }
2182        }
2183    });
2184
2185    rewrite_exprs(s, &mut |e| {
2186        fold_concat_in_expr(e, &values, &appended_stmts);
2187    });
2188
2189    // Drop now-unused builder temporaries (their only consumers were
2190    // folded); the drop COUNT drives iteration — no statement counting
2191    // walks at all.
2192    for _ in 0..8 {
2193        let mut reads: HashSet<u32> = HashSet::default();
2194        stmt_collect_vars(s, &mut reads, false);
2195        if drop_unused_assigns(s, &reads) == 0 {
2196            break;
2197        }
2198    }
2199}
2200
2201/// Deep statement count (pipeline guard input).
2202pub fn count_stmts_deep(s: &Stmt, out: &mut usize) {
2203    *out += 1;
2204    walk_all(s, &mut |_| {
2205        *out += 1;
2206    });
2207}
2208
2209/// Fused per-variable analysis gathered in one statement-tree walk.
2210struct VarAnalysis {
2211    assigns: Vec<usize>,
2212    reads: Vec<usize>,
2213    /// Value of the (single) assignment, when recorded.
2214    values: Vec<Option<Expr>>,
2215}
2216
2217fn analyze_vars(s: &Stmt, a: &mut VarAnalysis) {
2218    walk_all(s, &mut |st| {
2219        let exprs: Vec<&Expr> = match st {
2220            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
2221                if let Expr::Local { var, .. } = &**target {
2222                    grow_to(&mut a.assigns, *var);
2223                    let idx = *var as usize;
2224                    a.assigns[idx] += 1;
2225                    if a.assigns[idx] == 1 {
2226                        if idx >= a.values.len() {
2227                            a.values.resize(idx + 1, None);
2228                        }
2229                        a.values[idx] = Some((**value).clone());
2230                    } else if idx < a.values.len() {
2231                        a.values[idx] = None;
2232                    }
2233                }
2234                let mut v: Vec<&Expr> = vec![value];
2235                if !matches!(&**target, Expr::Local { .. }) {
2236                    v.push(target);
2237                }
2238                v
2239            }
2240            Stmt::ExprStmt(e @ (Expr::PreIncDec { .. } | Expr::PostIncDec { .. })) => {
2241                // `v++` is a read-modify-WRITE of v: counting it as a
2242                // plain read makes v a single-assign/single-read forward
2243                // candidate, and value-forwarding then fabricates `5++`
2244                // (or drops v's def and leaves ++ on an undefined name).
2245                let inner = match e {
2246                    Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => e,
2247                    _ => unreachable!(),
2248                };
2249                if let Expr::Local { var, .. } = &**inner {
2250                    grow_to(&mut a.assigns, *var);
2251                    let idx = *var as usize;
2252                    a.assigns[idx] += 1;
2253                    if idx < a.values.len() {
2254                        a.values[idx] = None;
2255                    }
2256                    return;
2257                }
2258                vec![e]
2259            }
2260            Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
2261                vec![e]
2262            }
2263            Stmt::Return(Some(e)) => vec![e],
2264            Stmt::LocalDef {
2265                var, init: Some(e), ..
2266            } => {
2267                grow_to(&mut a.assigns, *var);
2268                let idx = *var as usize;
2269                a.assigns[idx] += 1;
2270                if a.assigns[idx] == 1 {
2271                    if idx >= a.values.len() {
2272                        a.values.resize(idx + 1, None);
2273                    }
2274                    a.values[idx] = Some(e.clone());
2275                } else if idx < a.values.len() {
2276                    a.values[idx] = None;
2277                }
2278                vec![e]
2279            }
2280            Stmt::If { cond, .. } => vec![cond],
2281            Stmt::While { cond, .. } => vec![cond],
2282            Stmt::DoWhile { cond, .. } => vec![cond],
2283            _ => Vec::new(),
2284        };
2285        for e in exprs {
2286            visit_exprs(e, &mut |x| {
2287                if let Expr::Local { var, .. } = x {
2288                    grow_to(&mut a.reads, *var);
2289                    a.reads[*var as usize] += 1;
2290                }
2291            });
2292        }
2293    });
2294    // Keep the three views length-aligned.
2295    let n = a.assigns.len().max(a.reads.len()).max(a.values.len());
2296    a.assigns.resize(n, 0);
2297    a.reads.resize(n, 0);
2298    a.values.resize(n, None);
2299}
2300
2301fn grow_to(v: &mut Vec<usize>, var: u32) {
2302    if var as usize >= v.len() {
2303        v.resize(var as usize + 1, 0);
2304    }
2305}
2306
2307fn count_assignments(s: &Stmt, counts: &mut Vec<usize>) {
2308    walk_all(s, &mut |st| match st {
2309        Stmt::ExprStmt(Expr::Assign { target, .. }) => {
2310            if let Expr::Local { var, .. } = &**target {
2311                grow_to(counts, *var);
2312                counts[*var as usize] += 1;
2313            }
2314        }
2315        Stmt::LocalDef {
2316            var, init: Some(_), ..
2317        } => {
2318            grow_to(counts, *var);
2319            counts[*var as usize] += 1;
2320        }
2321        _ => {}
2322    });
2323}
2324
2325fn record_assignments(s: &Stmt, counts: &[usize], out: &mut Vec<Option<Expr>>) {
2326    walk_all(s, &mut |st| match st {
2327        Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
2328            if let Expr::Local { var, .. } = &**target {
2329                if counts.get(*var as usize).copied().unwrap_or(0) == 1 {
2330                    if *var as usize >= out.len() {
2331                        out.resize(*var as usize + 1, None);
2332                    }
2333                    out[*var as usize] = Some((**value).clone());
2334                }
2335            }
2336        }
2337        Stmt::LocalDef {
2338            var, init: Some(e), ..
2339        } if counts.get(*var as usize).copied().unwrap_or(0) == 1 => {
2340            if *var as usize >= out.len() {
2341                out.resize(*var as usize + 1, None);
2342            }
2343            out[*var as usize] = Some(e.clone());
2344        }
2345        _ => {}
2346    });
2347}
2348
2349fn fold_concat_in_expr(e: &mut Expr, values: &[Option<Expr>], appended: &HashSet<u32>) {
2350    deep_rewrite(e, &mut |x| {
2351        if let Expr::Method {
2352            cls,
2353            name,
2354            args,
2355            owner,
2356            ..
2357        } = x
2358        {
2359            if name.as_ref() == "toString" && args.is_empty() && is_string_builder(cls) {
2360                if let Some(o) = owner {
2361                    // Statement-form appends attached to any chain var mean
2362                    // the parts are incomplete — keep the call.
2363                    let mut has_stmt_appends = false;
2364                    visit_exprs(o, &mut |y| {
2365                        if let Expr::Local { var, .. } = y {
2366                            if appended.contains(var) {
2367                                has_stmt_appends = true;
2368                            }
2369                        }
2370                    });
2371                    if has_stmt_appends {
2372                        return;
2373                    }
2374                    let resolved = resolve_local(o, values);
2375                    if let Some(parts) = collect_sb_parts(&resolved, values, 0) {
2376                        // NEVER fold an empty chain: d8 splits builder
2377                        // chains across alias registers (`v2 = v0.append
2378                        // (x); v2.append(y); v0.toString()`) — the traced
2379                        // chain sees a bare `new StringBuilder` and would
2380                        // fold toString() to "" (a wrong VALUE, silently:
2381                        // lab package Obf.label). With zero parts the
2382                        // call stays — always correct, usually folded
2383                        // elsewhere once the parts are visible.
2384                        if !parts.is_empty() {
2385                            *x = Expr::StringConcat(parts);
2386                        }
2387                    }
2388                }
2389            }
2390        }
2391    });
2392}
2393
2394fn is_string_builder(cls: &str) -> bool {
2395    cls == "java/lang/StringBuilder" || cls == "java/lang/StringBuffer"
2396}
2397
2398fn resolve_local(e: &Expr, values: &[Option<Expr>]) -> Expr {
2399    match e {
2400        Expr::Local { var, ty } => values
2401            .get(*var as usize)
2402            .and_then(|o| o.clone())
2403            .unwrap_or_else(|| Expr::Local {
2404                var: *var,
2405                ty: ty.clone(),
2406            }),
2407        other => other.clone(),
2408    }
2409}
2410
2411/// Collect concat parts from a StringBuilder chain; depth caps cycles.
2412fn collect_sb_parts(e: &Expr, values: &[Option<Expr>], depth: u32) -> Option<Vec<ConcatPart>> {
2413    if depth > 16 {
2414        return None;
2415    }
2416    match e {
2417        Expr::Method {
2418            cls,
2419            name,
2420            args,
2421            owner,
2422            ..
2423        } if name.as_ref() == "append" && is_string_builder(cls) && args.len() == 1 => {
2424            let mut parts =
2425                collect_sb_parts(&resolve_local(owner.as_deref()?, values), values, depth + 1)?;
2426            parts.push(ConcatPart::Str(args[0].clone()));
2427            Some(parts)
2428        }
2429        Expr::New {
2430            cls,
2431            args,
2432            raw: false,
2433            ..
2434        } if is_string_builder(cls) => {
2435            let mut parts = Vec::new();
2436            for a in args {
2437                if let Expr::Const(ConstVal::Str(sv)) = a {
2438                    parts.push(ConcatPart::Const(sv.to_string()));
2439                } else {
2440                    parts.push(ConcatPart::Str(a.clone()));
2441                }
2442            }
2443            Some(parts)
2444        }
2445        _ => None,
2446    }
2447}
2448
2449fn drop_unused_assigns(s: &mut Stmt, reads: &HashSet<u32>) -> usize {
2450    let mut dropped = 0usize;
2451    walk_mut_deep(s, &mut |st| {
2452        if let Stmt::Block(v) = st {
2453            let before = v.len();
2454            v.retain(|x| match x {
2455                // Drop only when unread AND a builder chain (folded
2456                // consumer; `new StringBuilder` chains cannot NPE).
2457                Stmt::ExprStmt(Expr::Assign { target, value, .. }) => match &**target {
2458                    Expr::Local { var, .. } => reads.contains(var) || !is_sbish(value),
2459                    _ => true,
2460                },
2461                Stmt::LocalDef { var, init, .. } => {
2462                    reads.contains(var) || !init.as_ref().map(is_sbish).unwrap_or(false)
2463                }
2464                _ => true,
2465            });
2466            dropped += before - v.len();
2467        }
2468    });
2469    dropped
2470}
2471
2472fn is_sbish(e: &Expr) -> bool {
2473    match e {
2474        Expr::New {
2475            cls, raw: false, ..
2476        } => is_string_builder(cls),
2477        Expr::Method { name, cls, .. } => name.as_ref() == "append" && is_string_builder(cls),
2478        _ => false,
2479    }
2480}
2481
2482fn walk_mut_deep<F: FnMut(&mut Stmt)>(s: &mut Stmt, f: &mut F) {
2483    f(s);
2484    walk_mut(s, &mut |x| walk_mut_deep(x, f));
2485}
2486
2487/// Drop orphaned raw `new-instance` statements. R8 sometimes separates
2488/// `new-instance vR` from its `<init>` invoke by a register copy
2489/// (`move-object vS, vR; invoke vS.<init>(..)` — dex-legal: the init
2490/// initializes the object both registers name). The fold pairs the init
2491/// with the COPY, and the structurer's branch duplication re-materializes
2492/// the original raw marker as a bare `new C();` statement on paths whose
2493/// uses were rewired to the folded side (weixin sns/storage/c0: four
2494/// `new s1();` beside the real `s1 s1x = new s1(..)` — s1 has no no-arg
2495/// ctor, "无法将类 C的构造器 C应用到给定类型" ×195). A raw New in
2496/// statement position binds no variable and initializes nothing — pure
2497/// verifier-artifact dead code.
2498/// `try { X } finally { }` with no catches and an empty finally is a
2499/// no-op wrapper (the close-rethrow handler recast mints them) — unwrap
2500/// to X. An empty finally changes no semantics: it completes normally on
2501/// every path.
2502pub fn drop_empty_finallies(body: &mut Stmt) {
2503    fn is_empty_block(s: &Stmt) -> bool {
2504        match s {
2505            Stmt::Block(v) => v.iter().all(is_empty_block),
2506            _ => false,
2507        }
2508    }
2509    walk_mut_deep(body, &mut |st| {
2510        let Stmt::Try {
2511            body: b,
2512            catches,
2513            finally,
2514        } = st
2515        else {
2516            return;
2517        };
2518        if !catches.is_empty() {
2519            return;
2520        }
2521        match finally {
2522            // `try { X }` with neither catches nor a finally (the renderer
2523            // would paste a compilability `finally { }` back) and the
2524            // empty-finally wrapper are both no-ops — unwrap to X.
2525            None => {}
2526            Some(f) if is_empty_block(f) => {}
2527            Some(_) => return,
2528        }
2529        let inner = std::mem::replace(&mut **b, Stmt::Block(vec![]));
2530        *st = inner;
2531    });
2532}
2533
2534/// Drop orphan value pushes that render as `/* X; */` comments: expression
2535/// statements whose expression cannot throw or bind — plain locals,
2536/// constants, `this`, and field reads on the implicit `this` (owner: None;
2537/// a deref of an arbitrary receiver could NPE, so owned field reads stay).
2538pub fn drop_pure_value_stmts(body: &mut Stmt) {
2539    // Shared honest predicate (jdc-core has_side_effects): a pure read
2540    // cannot throw or bind, so the statement is dead code everywhere.
2541    fn is_pure(s: &Stmt) -> bool {
2542        let Stmt::ExprStmt(e) = s else {
2543            return false;
2544        };
2545        !has_side_effects(e)
2546    }
2547    walk_mut_deep(body, &mut |st| {
2548        // Replace in place: orphans also sit as direct If branches or a
2549        // Synchronized body where no container retain can reach them.
2550        if is_pure(st) {
2551            *st = Stmt::Block(vec![]);
2552            return;
2553        }
2554        match st {
2555            Stmt::Switch { cases, .. } => {
2556                for c in cases.iter_mut() {
2557                    c.body.retain(|x| !is_pure(x));
2558                }
2559            }
2560            Stmt::For { init, .. } => init.retain(|x| !is_pure(x)),
2561            _ => {}
2562        }
2563    });
2564}
2565
2566pub fn drop_dead_raw_news(body: &mut Stmt) {
2567    fn is_orphan(s: &Stmt) -> bool {
2568        matches!(s, Stmt::ExprStmt(Expr::New { raw: true, .. }))
2569    }
2570    walk_mut_deep(body, &mut |st| match st {
2571        Stmt::Block(v) => v.retain(|x| !is_orphan(x)),
2572        Stmt::Switch { cases, .. } => {
2573            for c in cases.iter_mut() {
2574                c.body.retain(|x| !is_orphan(x));
2575            }
2576        }
2577        Stmt::For { init, .. } => init.retain(|x| !is_orphan(x)),
2578        Stmt::TryWithResources { resources, .. } => resources.retain(|x| !is_orphan(x)),
2579        _ => {}
2580    });
2581}
2582
2583/// Recover `synchronized` from the d8 monitor pattern:
2584/// `monitorenter(e); try { body } catch (Throwable) { monitorexit(e); throw t; }`
2585/// (optionally followed by `monitorexit(e)`).
2586pub fn fold_synchronized(s: &mut Stmt) {
2587    if std::env::var("DDC_DBG_SYNC").is_ok() {
2588        walk_all(s, &mut |st| {
2589            if let Stmt::Try { catches, .. } = st {
2590                for c in catches.iter() {
2591                    eprintln!("[sync] catch var={} body={:.600}", c.var, format!("{:?}", c.body));
2592                }
2593            }
2594        });
2595    }
2596    fold_sync_walk(s);
2597}
2598
2599
2600fn fold_sync_walk(s: &mut Stmt) {
2601    walk_mut_deep(s, &mut |st| {
2602        let Stmt::Block(v) = st else { return };
2603        let mut i = 0;
2604        while i < v.len() {
2605            // A fold landing on a Synchronized re-examines the same slot:
2606            // a split-region sibling Try (Case E) only becomes visible
2607            // AFTER the [enter, try] pair folded into the block form.
2608            #[allow(unused_assignments)]
2609            let mut advance = true;
2610            if i + 1 < v.len() {
2611                if let Some((repl, consumed)) = try_sync_at(v, i) {
2612                    advance = !matches!(repl.first(), Some(Stmt::Synchronized { .. }));
2613                    let _ = v.drain(i..i + consumed);
2614                    let n = repl.len();
2615                    v.splice(i..i, repl);
2616                    if advance {
2617                        i += n;
2618                    }
2619                    continue;
2620                }
2621            }
2622            if let Some((repl, consumed)) = try_sync_in_try(v, i) {
2623                let _ = v.drain(i..i + consumed);
2624                let n = repl.len();
2625                v.splice(i..i, repl);
2626                i += n;
2627                continue;
2628            }
2629            if let Some((repl, consumed)) = try_sync_deep(v, i) {
2630                let _ = v.drain(i..i + consumed);
2631                let n = repl.len();
2632                v.splice(i..i, repl);
2633                i += n;
2634                continue;
2635            }
2636            if let Some((repl, consumed)) = try_sync_absorb_after(v, i) {
2637                let _ = v.drain(i..i + consumed);
2638                let n = repl.len();
2639                v.splice(i..i, repl);
2640                i += n;
2641                continue;
2642            }
2643            i += 1;
2644        }
2645    });
2646}
2647
2648/// Side-effect-free lock-value reads: field/local/const/this only. A call
2649/// or allocation between the monitor-enter and the try belongs to the
2650/// protected body, not to a absorbable alias head.
2651fn is_pure_read_expr(e: &Expr) -> bool {
2652    let mut pure = true;
2653    visit_exprs(e, &mut |x| match x {
2654        Expr::Const(_)
2655        | Expr::Local { .. }
2656        | Expr::This
2657        | Expr::Field { .. } => {}
2658        _ => pure = false,
2659    });
2660    pure
2661}
2662
2663/// Resolve a local through its defining expression (bounded) so lock
2664/// identity survives value-view forwarding: the enter may render as the
2665/// raw field access while the exit renders as the promoted local (or vice
2666/// versa).
2667fn resolve_lock(e: &Expr, defs: &jdc_core::FxHashMap<u32, Expr>, depth: u32) -> Expr {
2668    if depth >= 4 {
2669        return e.clone();
2670    }
2671    if let Expr::Local { var, .. } = e {
2672        if let Some(d) = defs.get(var) {
2673            return resolve_lock(d, defs, depth + 1);
2674        }
2675    }
2676    e.clone()
2677}
2678
2679fn lock_same(a: &Expr, b: &Expr) -> bool {
2680    match (a, b) {
2681        (Expr::This, Expr::This) => true,
2682        (Expr::Local { var: x, .. }, Expr::Local { var: y, .. }) => x == y,
2683        (
2684            Expr::Field {
2685                owner: oa,
2686                cls: ca,
2687                name: na,
2688                ..
2689            },
2690            Expr::Field {
2691                owner: ob,
2692                cls: cb,
2693                name: nb,
2694                ..
2695            },
2696        ) => {
2697            ca == cb
2698                && na == nb
2699                && match (oa, ob) {
2700                    (None, None) => true,
2701                    (Some(ra), Some(rb)) => lock_same(ra, rb),
2702                    _ => false,
2703                }
2704        }
2705        (Expr::Const(x), Expr::Const(y)) => x == y,
2706        _ => false,
2707    }
2708}
2709
2710fn lock_equiv(a: &Expr, b: &Expr, defs: &jdc_core::FxHashMap<u32, Expr>) -> bool {
2711    lock_same(&resolve_lock(a, defs, 0), &resolve_lock(b, defs, 0))
2712}
2713
2714fn flatten_leaves<'a>(s: &'a Stmt, out: &mut Vec<&'a Stmt>) {
2715    match s {
2716        Stmt::Block(v) => {
2717            for x in v {
2718                flatten_leaves(x, out);
2719            }
2720        }
2721        _ => out.push(s),
2722    }
2723}
2724
2725/// Every `MonitorExit(lock)` inside the folded body is a compiler artifact
2726/// (the implicit release on each exit path) — drop them so the body reads
2727/// clean and the absorbed lock alias loses its last reads.
2728fn strip_lock_exits(s: &mut Stmt, lock: &Expr, defs: &jdc_core::FxHashMap<u32, Expr>) {
2729    walk_mut_deep(s, &mut |st| {
2730        let Stmt::Block(v) = st else { return };
2731        v.retain(|x| !matches!(x, Stmt::MonitorExit(e) if lock_equiv(e, lock, defs)));
2732    });
2733}
2734
2735fn try_sync_at(v: &[Stmt], i: usize) -> Option<(Vec<Stmt>, usize)> {
2736    // Local defs in scope before the enter, for lock resolution.
2737    let mut defs: jdc_core::FxHashMap<u32, Expr> = jdc_core::FxHashMap::default();
2738    for s in &v[..i] {
2739        if let Stmt::LocalDef { var, init: Some(e), .. } = s {
2740            defs.insert(*var, e.clone());
2741        }
2742    }
2743    // Case A: the enter is a direct sibling; Case B: it ends a child Block
2744    // (the converter wraps the pre-try statements of the entry block, so
2745    // the Try sits beside the wrapper, not beside the enter).
2746    let (pre, enter_e, mut j, mut absorbed): (Vec<Stmt>, &Expr, usize, Vec<Stmt>) = match &v[i] {
2747        Stmt::MonitorEnter(e) => (Vec::new(), e, i + 1, Vec::new()),
2748        Stmt::Block(b) => {
2749            let split = b
2750                .iter()
2751                .position(|x| matches!(x, Stmt::MonitorEnter(_)))?;
2752            let e = match &b[split] {
2753                Stmt::MonitorEnter(e) => e,
2754                _ => unreachable!(),
2755            };
2756            let mut absorbed: Vec<Stmt> = Vec::new();
2757            let mut k = split + 1;
2758            while let Some(Stmt::LocalDef { var, init: Some(ex), .. }) = b.get(k) {
2759                if !is_pure_read_expr(ex) {
2760                    break;
2761                }
2762                defs.insert(*var, ex.clone());
2763                absorbed.push(b[k].clone());
2764                k += 1;
2765            }
2766            // Only a tail split: anything after the absorbable run (other
2767            // than nothing) means real code follows inside the block.
2768            if k != b.len() {
2769                return None;
2770            }
2771            let pre: Vec<Stmt> = b[..split].to_vec();
2772            for s in &pre {
2773                if let Stmt::LocalDef { var, init: Some(e2), .. } = s {
2774                    defs.insert(*var, e2.clone());
2775                }
2776            }
2777            (pre, e, i + 1, absorbed)
2778        }
2779        _ => return None,
2780    };
2781    // Absorb pure alias defs parked between the enter and the try (the
2782    // value-view promotion hoists the lock register's declaration ahead of
2783    // the try span). They become the synchronized body's head.
2784    let mut absorbed_vars: Vec<u32> = Vec::new();
2785    while let Some(Stmt::LocalDef { var, init: Some(e), .. }) = v.get(j) {
2786        if !is_pure_read_expr(e) {
2787            break;
2788        }
2789        defs.insert(*var, e.clone());
2790        absorbed_vars.push(*var);
2791        absorbed.push(v[j].clone());
2792        j += 1;
2793    }
2794    let Stmt::Try {
2795        body,
2796        catches,
2797        finally,
2798    } = v.get(j)?
2799    else {
2800        return None;
2801    };
2802    if finally.is_some() || catches.len() != 1 {
2803        return None;
2804    }
2805    let c = &catches[0];
2806    if !c.exc.is_empty() {
2807        return None;
2808    }
2809    // The catch-all body: monitorexit(lock) then rethrow (the handler walk
2810    // may nest it as Block[Block[...]]).
2811    let mut flat: Vec<&Stmt> = Vec::new();
2812    flatten_leaves(c.body.as_ref(), &mut flat);
2813    if flat.len() < 2 {
2814        return None;
2815    }
2816    let Stmt::MonitorExit(exit_e) = flat[0] else {
2817        return None;
2818    };
2819    if !lock_equiv(enter_e, exit_e, &defs) {
2820        return None;
2821    }
2822    if !matches!(flat[flat.len() - 1], Stmt::Throw(_)) {
2823        return None;
2824    }
2825    for mid in &flat[1..flat.len() - 1] {
2826        if !matches!(mid, Stmt::MonitorEnter(_) | Stmt::MonitorExit(_)) {
2827            return None;
2828        }
2829    }
2830    // An absorbed alias read anywhere after the try would escape the
2831    // synchronized scope once folded — refuse the fold instead.
2832    if !absorbed_vars.is_empty() {
2833        let outside = count_locals_stmts(&v[j + 1..]);
2834        if absorbed_vars
2835            .iter()
2836            .any(|av| outside.get(av).copied().unwrap_or(0) > 0)
2837        {
2838            return None;
2839        }
2840    }
2841    let mut body_stmt = (**body).clone();
2842    strip_lock_exits(&mut body_stmt, enter_e, &defs);
2843    absorbed.push(body_stmt);
2844    let mut consumed = j - i + 1;
2845    // A matching trailing monitor-exit after the try (the normal-path
2846    // release parked at the join) is implicit in the block form too.
2847    if let Some(Stmt::MonitorExit(e)) = v.get(j + 1) {
2848        if lock_equiv(e, enter_e, &defs) {
2849            consumed += 1;
2850        }
2851    }
2852    let mut repl = pre;
2853    repl.push(Stmt::Synchronized {
2854        lock: enter_e.clone(),
2855        body: Box::new(Stmt::Block(absorbed)),
2856    });
2857    Some((repl, consumed))
2858}
2859
2860/// Case C: the monitor-enter sits INSIDE the try body (d8 extends the
2861/// protected range over the lock evaluation: `try { v=lock; monitorenter v;
2862/// rest } catch-all { monitorexit v; throw }`). Everything up to and
2863/// including the enter folds into the synchronized body — the block form
2864/// re-protects it.
2865fn try_sync_in_try(v: &[Stmt], i: usize) -> Option<(Vec<Stmt>, usize)> {
2866    let Stmt::Try {
2867        body,
2868        catches,
2869        finally,
2870    } = &v[i]
2871    else {
2872        return None;
2873    };
2874    if finally.is_some() || catches.len() != 1 {
2875        return None;
2876    }
2877    let c = &catches[0];
2878    if !c.exc.is_empty() {
2879        return None;
2880    }
2881    let mut flat: Vec<&Stmt> = Vec::new();
2882    flatten_leaves(c.body.as_ref(), &mut flat);
2883    if flat.len() < 2 {
2884        return None;
2885    }
2886    let Stmt::MonitorExit(_) = flat[0] else {
2887        return None;
2888    };
2889    if !matches!(flat[flat.len() - 1], Stmt::Throw(_)) {
2890        return None;
2891    }
2892    for mid in &flat[1..flat.len() - 1] {
2893        if !matches!(mid, Stmt::MonitorEnter(_) | Stmt::MonitorExit(_)) {
2894            return None;
2895        }
2896    }
2897    // Locate the enter inside the body's top level: a direct MonitorEnter
2898    // or a child Block whose tail is [MonitorEnter, pure-defs*]. The
2899    // wrapped prelude statements stay ahead of the fold point.
2900    let Stmt::Block(bv) = body.as_ref() else {
2901        return None;
2902    };
2903    let mut defs: jdc_core::FxHashMap<u32, Expr> = jdc_core::FxHashMap::default();
2904    for s in &v[..i] {
2905        if let Stmt::LocalDef { var, init: Some(e), .. } = s {
2906            defs.insert(*var, e.clone());
2907        }
2908    }
2909    let mut pre: Vec<Stmt> = Vec::new();
2910    let mut enter_e: Option<Expr> = None;
2911    let mut absorbed: Vec<Stmt> = Vec::new();
2912    let mut absorbed_vars: Vec<u32> = Vec::new();
2913    let mut rest_start: Option<usize> = None;
2914    for (p, el) in bv.iter().enumerate() {
2915        match el {
2916            Stmt::MonitorEnter(e) => {
2917                enter_e = Some(e.clone());
2918                rest_start = Some(p + 1);
2919                break;
2920            }
2921            Stmt::Block(inner) => {
2922                let split = inner
2923                    .iter()
2924                    .position(|x| matches!(x, Stmt::MonitorEnter(_)));
2925                if let Some(sp) = split {
2926                    let mut k = sp + 1;
2927                    let mut ok = true;
2928                    let mut tail: Vec<Stmt> = Vec::new();
2929                    while let Some(Stmt::LocalDef { var: _, init: Some(ex), .. }) = inner.get(k) {
2930                        if !is_pure_read_expr(ex) {
2931                            ok = false;
2932                            break;
2933                        }
2934                        tail.push(inner[k].clone());
2935                        k += 1;
2936                    }
2937                    if ok && k == inner.len() {
2938                        for s in &inner[..sp] {
2939                            if let Stmt::LocalDef { var, init: Some(ex), .. } = s {
2940                                defs.insert(*var, ex.clone());
2941                            }
2942                        }
2943                        pre.extend(inner[..sp].iter().cloned());
2944                        enter_e = Some(match &inner[sp] {
2945                            Stmt::MonitorEnter(e) => e.clone(),
2946                            _ => unreachable!(),
2947                        });
2948                        absorbed = tail;
2949                        rest_start = Some(p + 1);
2950                        break;
2951                    }
2952                }
2953                // Not a fold point: a plain nested block ahead of the enter.
2954                for s in inner.iter() {
2955                    if let Stmt::LocalDef { var, init: Some(ex), .. } = s {
2956                        defs.insert(*var, ex.clone());
2957                    }
2958                }
2959                pre.push(el.clone());
2960            }
2961            other => {
2962                if let Stmt::LocalDef { var, init: Some(ex), .. } = other {
2963                    defs.insert(*var, ex.clone());
2964                }
2965                pre.push(other.clone());
2966            }
2967        }
2968    }
2969    let enter_e = enter_e?;
2970    let rs = rest_start?;
2971    // Pure defs directly after the enter also join the body head.
2972    let mut q = rs;
2973    while let Some(Stmt::LocalDef { var, init: Some(ex), .. }) = bv.get(q) {
2974        if !is_pure_read_expr(ex) {
2975            break;
2976        }
2977        defs.insert(*var, ex.clone());
2978        absorbed_vars.push(*var);
2979        absorbed.push(bv[q].clone());
2980        q += 1;
2981    }
2982    let rest: Vec<Stmt> = bv[q..].to_vec();
2983    if rest.is_empty() {
2984        return None;
2985    }
2986    // A non-def prelude would move real code past the lock acquisition —
2987    // the deep fold (try kept, synchronized in place) owns that shape.
2988    if pre
2989        .iter()
2990        .any(|s| !matches!(s, Stmt::LocalDef { init: Some(e), .. } if is_pure_read_expr(e)))
2991    {
2992        return None;
2993    }
2994    // The catch's release must name the same lock.
2995    let Stmt::MonitorExit(exit_e) = flat[0] else {
2996        return None;
2997    };
2998    if !lock_equiv(&enter_e, exit_e, &defs) {
2999        return None;
3000    }
3001    // Pre/absorbed defs must not be read outside the try.
3002    let outside_vars: Vec<u32> = pre
3003        .iter()
3004        .chain(absorbed.iter())
3005        .filter_map(|s| match s {
3006            Stmt::LocalDef { var, .. } => Some(*var),
3007            _ => None,
3008        })
3009        .collect();
3010    if !outside_vars.is_empty() {
3011        let outside = count_locals_stmts(&v[i + 1..]);
3012        if outside_vars
3013            .iter()
3014            .any(|av| outside.get(av).copied().unwrap_or(0) > 0)
3015        {
3016            return None;
3017        }
3018    }
3019    let mut body_stmts = pre;
3020    body_stmts.extend(absorbed);
3021    body_stmts.extend(rest);
3022    let mut sync_body = Stmt::Block(body_stmts);
3023    strip_lock_exits(&mut sync_body, &enter_e, &defs);
3024    let mut consumed = 1;
3025    if let Some(Stmt::MonitorExit(e)) = v.get(i + 1) {
3026        if lock_equiv(e, &enter_e, &defs) {
3027            consumed += 1;
3028        }
3029    }
3030    Some((
3031        vec![Stmt::Synchronized {
3032            lock: enter_e,
3033            body: Box::new(sync_body),
3034        }],
3035        consumed,
3036    ))
3037}
3038
3039/// Case E: the protected region was SPLIT across groups — a named try/catch
3040/// nested inside the synchronized region renders as a SIBLING Try after the
3041/// folded Synchronized, with the region's normal-path release parked after
3042/// it (d8 splits javac-style around inner returns; the inner range carries
3043/// its own named handler plus the shared catch-all, so the same-handler
3044/// group merge refuses it and the structurer emits two sibling tries).
3045/// `[Synchronized, Try, MonitorExit(lock)]` -> the Try joins the lock body
3046/// and the parked release is implicit. Without this the try escapes the
3047/// lock (racy field writes) and unexpected exceptions leak the monitor.
3048fn try_sync_absorb_after(v: &[Stmt], i: usize) -> Option<(Vec<Stmt>, usize)> {
3049    let Stmt::Synchronized { lock, body } = &v[i] else {
3050        return None;
3051    };
3052    let Stmt::Try {
3053        body: tbody,
3054        catches,
3055        finally,
3056    } = v.get(i + 1)?
3057    else {
3058        return None;
3059    };
3060    if finally.is_some() {
3061        return None;
3062    }
3063    // The parked release sits at the protected region's end: everything on
3064    // the normal path before it ran INSIDE the lock (phi commits feeding
3065    // the in-lock return), everything after it is post-release. Scan past
3066    // orderable statements (register commits, pure reads, Block wrappers)
3067    // to the matching release; absorb the run into the lock body.
3068    let defs = defs_probe(v, i, body);
3069    let mut absorbed_mid: Vec<Stmt> = Vec::new();
3070    let mut rest_after: Option<Stmt> = None;
3071    let mut found = false;
3072    let mut scan = i + 2;
3073    'scan: while scan < v.len() && scan < i + 14 {
3074        match &v[scan] {
3075            Stmt::MonitorExit(e) => {
3076                if !lock_equiv(e, lock, &defs) {
3077                    return None;
3078                }
3079                found = true;
3080                scan += 1;
3081                break;
3082            }
3083            Stmt::Block(bv) => {
3084                for (k, s) in bv.iter().enumerate() {
3085                    match s {
3086                        Stmt::MonitorExit(e) => {
3087                            if !lock_equiv(e, lock, &defs) {
3088                                return None;
3089                            }
3090                            absorbed_mid.push(Stmt::Block(bv[..k].to_vec()));
3091                            let rest = &bv[k + 1..];
3092                            rest_after = (!rest.is_empty())
3093                                .then(|| Stmt::Block(rest.to_vec()));
3094                            found = true;
3095                            scan += 1;
3096                            break 'scan;
3097                        }
3098                        Stmt::ExprStmt(e) if mid_safe(e) || is_local_commit(e) => {}
3099                        _ => break 'scan,
3100                    }
3101                }
3102                absorbed_mid.push(v[scan].clone());
3103                scan += 1;
3104            }
3105            Stmt::ExprStmt(e) if mid_safe(e) || is_local_commit(e) => {
3106                absorbed_mid.push(v[scan].clone());
3107                scan += 1;
3108            }
3109            _ => break,
3110        }
3111    }
3112    if !found {
3113        return None;
3114    }
3115    let consumed = scan - i;
3116    // The adopted try's implicit releases and the handler's own release are
3117    // owned by the synchronized once inside — strip them.
3118    let mut t = (**tbody).clone();
3119    let defs = defs_probe(v, i, body);
3120    strip_lock_exits(&mut t, lock, &defs);
3121    let mut cs = Vec::with_capacity(catches.len());
3122    for c in catches.iter() {
3123        let mut cb = (*c.body).clone();
3124        strip_lock_exits(&mut cb, lock, &defs);
3125        // A catch-all reduced to a bare rethrow of the catch parameter is
3126        // the synchronized's own handler — drop it; named catches keep
3127        // (dropping one would widen the checked-exception surface).
3128        let bare_rethrow = c.exc.is_empty() && {
3129            let mut leaves: Vec<&Stmt> = Vec::new();
3130            flatten_leaves(&cb, &mut leaves);
3131            matches!(leaves.as_slice(), [Stmt::Throw(Expr::Local { var, .. })] if *var == c.var)
3132        };
3133        if bare_rethrow {
3134            continue;
3135        }
3136        let mut c2 = c.clone();
3137        *c2.body = cb;
3138        cs.push(c2);
3139    }
3140    let mut new_body = (**body).clone();
3141    let adopted = Stmt::Try {
3142        body: Box::new(t),
3143        catches: cs,
3144        finally: None,
3145    };
3146    match &mut new_body {
3147        Stmt::Block(bv) => bv.push(adopted),
3148        other => {
3149            let inner = std::mem::replace(other, Stmt::Block(vec![]));
3150            *other = Stmt::Block(vec![inner, adopted]);
3151        }
3152    }
3153    if let Stmt::Block(bv) = &mut new_body {
3154        bv.extend(absorbed_mid);
3155    }
3156    let sync = Stmt::Synchronized {
3157        lock: lock.clone(),
3158        body: Box::new(new_body),
3159    };
3160    let mut repl = vec![sync];
3161    if let Some(r) = rest_after {
3162        repl.push(r);
3163    }
3164    Some((repl, consumed))
3165}
3166
3167/// Statements orderable into the lock body: pure reads (cannot throw or
3168/// bind) and — handled by the caller's pattern — plain local-to-local
3169/// register commits.
3170fn mid_safe(e: &Expr) -> bool {
3171    !has_side_effects(e)
3172}
3173
3174/// Plain `v = local;` register commit (phi materialization at the join).
3175fn is_local_commit(e: &Expr) -> bool {
3176    match e {
3177        Expr::Assign { target, op: AssignOp::Plain, .. } => {
3178            matches!(&**target, Expr::Local { .. })
3179        }
3180        _ => false,
3181    }
3182}
3183
3184/// Defs visible to the parked release: statements before the fold plus the
3185/// absorbed alias head at the synchronized body's top level.
3186fn defs_probe<'a>(
3187    v: &'a [Stmt],
3188    i: usize,
3189    body: &'a Stmt,
3190) -> jdc_core::FxHashMap<u32, Expr> {
3191    let mut defs: jdc_core::FxHashMap<u32, Expr> = jdc_core::FxHashMap::default();
3192    for s in &v[..i] {
3193        if let Stmt::LocalDef { var, init: Some(e), .. } = s {
3194            defs.insert(*var, e.clone());
3195        }
3196    }
3197    if let Stmt::Block(bv) = body {
3198        for s in bv {
3199            if let Stmt::LocalDef { var, init: Some(e), .. } = s {
3200                defs.insert(*var, e.clone());
3201            }
3202        }
3203    }
3204    defs
3205}
3206
3207
3208/// Case D: the enter sits mid-control-flow inside the try body (nested in
3209/// if/else branches). The spine from the body root to the enter must end
3210/// at the body end — nothing may follow the fold point at any ancestor
3211/// level. The Try STAYS; only the enter's tail becomes a synchronized
3212/// block in place. The catch's release is dropped: with the lock release
3213/// owned by the synchronized, the handler's monitorexit would double-release
3214/// (the sync's implicit release fires first, then the outer catch
3215/// re-releases an unheld monitor).
3216fn try_sync_deep(v: &[Stmt], i: usize) -> Option<(Vec<Stmt>, usize)> {
3217    let Stmt::Try {
3218        body,
3219        catches,
3220        finally,
3221    } = &v[i]
3222    else {
3223        return None;
3224    };
3225    if finally.is_some() || catches.len() != 1 {
3226        return None;
3227    }
3228    let c = &catches[0];
3229    if !c.exc.is_empty() {
3230        return None;
3231    }
3232    let mut flat: Vec<&Stmt> = Vec::new();
3233    flatten_leaves(c.body.as_ref(), &mut flat);
3234    if flat.len() < 2 {
3235        return None;
3236    }
3237    let catch_exit = match flat[0] {
3238        Stmt::MonitorExit(e) => e.clone(),
3239        _ => return None,
3240    };
3241    if !matches!(flat[flat.len() - 1], Stmt::Throw(_)) {
3242        return None;
3243    }
3244    for mid in &flat[1..flat.len() - 1] {
3245        if !matches!(mid, Stmt::MonitorEnter(_) | Stmt::MonitorExit(_)) {
3246            return None;
3247        }
3248    }
3249    let mut defs0: jdc_core::FxHashMap<u32, Expr> = jdc_core::FxHashMap::default();
3250    for s in &v[..i] {
3251        if let Stmt::LocalDef { var, init: Some(e), .. } = s {
3252            defs0.insert(*var, e.clone());
3253        }
3254    }
3255    let mut body_mut = (**body).clone();
3256    let folded = match &mut body_mut {
3257        Stmt::Block(b) => try_fold_spine(b, &catch_exit, &defs0),
3258        Stmt::If {
3259            then_stmt,
3260            else_stmt,
3261            ..
3262        } => {
3263            let mut hit = false;
3264            if let Stmt::Block(b) = &mut **then_stmt {
3265                hit |= try_fold_spine(b, &catch_exit, &defs0);
3266            }
3267            if !hit {
3268                if let Some(els) = else_stmt {
3269                    if let Stmt::Block(b) = &mut **els {
3270                        hit |= try_fold_spine(b, &catch_exit, &defs0);
3271                    }
3272                }
3273            }
3274            hit
3275        }
3276        _ => false,
3277    };
3278    if !folded {
3279        return None;
3280    }
3281    // Reduce the catch-all to a bare rethrow (see fn doc).
3282    let throw = flat[flat.len() - 1].clone();
3283    let mut catches_out = catches.clone();
3284    *catches_out[0].body = Stmt::Block(vec![throw]);
3285    let mut consumed = 1;
3286    if let Some(Stmt::MonitorExit(e)) = v.get(i + 1) {
3287        if lock_equiv(e, &catch_exit, &defs0) {
3288            consumed += 1;
3289        }
3290    }
3291    Some((
3292        vec![Stmt::Try {
3293            body: Box::new(body_mut),
3294            catches: catches_out,
3295            finally: None,
3296        }],
3297        consumed,
3298    ))
3299}
3300
3301/// Fold `[ …pre…, monitorenter(e), rest… ]` whose spine reaches the
3302/// sequence end: at the enter's own level everything after it joins the
3303/// synchronized body; at every ancestor level the spine element must be
3304/// the LAST element (branches are alternatives, not successors). Mutates
3305/// in place; returns true when a fold landed.
3306fn try_fold_spine(
3307    seq: &mut Vec<Stmt>,
3308    catch_exit: &Expr,
3309    defs_in: &jdc_core::FxHashMap<u32, Expr>,
3310) -> bool {
3311    for p in (0..seq.len()).rev() {
3312        // Direct enter at this level: everything after it joins the body.
3313        if let Stmt::MonitorEnter(e0) = &seq[p] {
3314            let mut defs = defs_in.clone();
3315            for s in &seq[..p] {
3316                if let Stmt::LocalDef { var, init: Some(ex), .. } = s {
3317                    defs.insert(*var, ex.clone());
3318                }
3319            }
3320            // Lock-relevant defs parked just after the enter (the promoted
3321            // lock local's declaration) resolve the catch's release.
3322            for s in &seq[p + 1..] {
3323                match s {
3324                    Stmt::LocalDef { var, init: Some(ex), .. } if is_pure_read_expr(ex) => {
3325                        defs.insert(*var, ex.clone());
3326                    }
3327                    _ => break,
3328                }
3329            }
3330            if !lock_equiv(e0, catch_exit, &defs) {
3331                return false;
3332            }
3333            let enter_e = e0.clone();
3334            let rest: Vec<Stmt> = seq.split_off(p + 1);
3335            let mut body = Stmt::Block(rest);
3336            strip_lock_exits(&mut body, &enter_e, &defs);
3337            seq[p] = Stmt::Synchronized {
3338                lock: enter_e,
3339                body: Box::new(body),
3340            };
3341            return true;
3342        }
3343        // Nested attempt on a clone; commit only when the spine element is
3344        // this level's last (nothing may follow the fold point).
3345        let is_last = p + 1 == seq.len();
3346        let mut trial = seq[p].clone();
3347        let hit = match &mut trial {
3348            Stmt::Block(b) => try_fold_spine(b, catch_exit, defs_in),
3349            Stmt::If {
3350                then_stmt,
3351                else_stmt,
3352                ..
3353            } => {
3354                let mut hit = false;
3355                if let Stmt::Block(b) = &mut **then_stmt {
3356                    hit |= try_fold_spine(b, catch_exit, defs_in);
3357                }
3358                if !hit {
3359                    if let Some(els) = else_stmt {
3360                        if let Stmt::Block(b) = &mut **els {
3361                            hit |= try_fold_spine(b, catch_exit, defs_in);
3362                        }
3363                    }
3364                }
3365                hit
3366            }
3367            _ => false,
3368        };
3369        if hit {
3370            if is_last {
3371                seq[p] = trial;
3372                return true;
3373            }
3374            return false;
3375        }
3376    }
3377    false
3378}
3379
3380/// Copy-forward single-use temporaries: `v = e; ... v ...` inlines `e` at
3381/// the (single) use and drops the assignment when safe. Pure values inline
3382/// anywhere; impure values (calls, array reads) only when the use is the
3383/// statement immediately after the definition.
3384pub fn forward_single_use(s: &mut Stmt, _vt: &VarTable) {
3385    // Fused single traversal: assignment counts, read counts and the
3386    // single-assignment values all come out of ONE walk (three separate
3387    // full-tree walks dominated the pass cost on large methods).
3388    let mut analysis = VarAnalysis {
3389        assigns: Vec::new(),
3390        reads: Vec::new(),
3391        values: Vec::new(),
3392    };
3393    analyze_vars(s, &mut analysis);
3394    let mut assigns = analysis.assigns;
3395    let mut reads = analysis.reads;
3396    // Var ids beyond the table (synthetic test shapes) are covered by the
3397    // counters' on-demand growth.
3398    let n_vars = assigns.len().max(_vt.vars.len()).max(1);
3399    assigns.resize(n_vars, 0);
3400    reads.resize(n_vars, 0);
3401    let mut values = analysis.values;
3402    values.resize(n_vars, None);
3403
3404    // Candidates: assigned exactly once, read exactly once. Additionally
3405    // the inlined VALUE must not reference a multi-assigned var (phi vars):
3406    // moving the read across the phi's reassignment would be a stale
3407    // capture (register rotations snapshot through temps for this reason).
3408    let mut cand = vec![false; n_vars];
3409    let mut edges: Vec<Vec<u32>> = vec![Vec::new(); n_vars];
3410    for (v, cand_v) in cand.iter_mut().enumerate() {
3411        if assigns.get(v).copied().unwrap_or(0) != 1 || reads.get(v).copied().unwrap_or(0) != 1 {
3412            continue;
3413        }
3414        if let Some(val) = values.get(v).and_then(|o| o.as_ref()) {
3415            let mut refs = HashSet::default();
3416            collect_vars(val, &mut refs);
3417            if refs.iter().any(|r| assigns[*r as usize] > 1) {
3418                continue;
3419            }
3420            // Growth-graph edge set, built in the same walk: inlining v
3421            // inserts a clone of its value, and every Local inside the
3422            // clone is replaced in turn.
3423            edges[v] = refs.into_iter().collect();
3424        }
3425        *cand_v = true;
3426    }
3427
3428    // Cycle rejection on the growth graph. A def-reference cycle
3429    // (v = f(w), w = g(v) — loop-carried register rotation reaching the
3430    // pass as two mutually-referencing single-assign/single-read locals)
3431    // re-introduces the other cycle Local at every replacement level, so
3432    // deep_rewrite grows the tree one layer per level and never
3433    // terminates: weixin's com/tencent/mm/plugin/appbrand/widget/input/b4
3434    // exhausted a 64MB worker stack at ~300k recursion frames. Edges into
3435    // non-candidates are harmless (they are never replaced, so growth
3436    // dies there — and they carry no outgoing edges).
3437    //
3438    // Three-color DFS, iterative: a legit 60k-long single-use chain must
3439    // not trade one stack overflow for another. A GRAY child closes a
3440    // cycle; `bad[v]` (v on a cycle, or v's inlined closure grows into
3441    // one) then taints the whole current path — everything on it reaches
3442    // the cycle. Black verdicts memoize: a node whose subtree was proven
3443    // clean cannot grow a cycle later.
3444    let mut color = vec![0u8; n_vars]; // 0 white, 1 gray, 2 black
3445    let mut bad = vec![false; n_vars];
3446    for root in 0..n_vars {
3447        if color[root] != 0 || edges[root].is_empty() {
3448            continue;
3449        }
3450        color[root] = 1;
3451        let mut stack: Vec<(usize, usize)> = vec![(root, 0)];
3452        while let Some(top) = stack.last_mut() {
3453            let v = top.0;
3454            let i = top.1;
3455            top.1 += 1;
3456            if i < edges[v].len() {
3457                let r = edges[v][i] as usize;
3458                if color[r] == 1 {
3459                    for &(pn, _) in &stack {
3460                        bad[pn] = true;
3461                    }
3462                } else if color[r] == 0 {
3463                    color[r] = 1;
3464                    stack.push((r, 0));
3465                } else if bad[r] {
3466                    for &(pn, _) in &stack {
3467                        bad[pn] = true;
3468                    }
3469                }
3470            } else {
3471                color[v] = 2;
3472                stack.pop();
3473            }
3474        }
3475    }
3476    let single: Vec<bool> = (0..n_vars).map(|v| cand[v] && !bad[v]).collect();
3477    if !single.iter().any(|&b| b) {
3478        return;
3479    }
3480    let pure: Vec<bool> = (0..n_vars)
3481        .map(|v| {
3482            single[v]
3483                && values
3484                    .get(v)
3485                    .and_then(|o| o.as_ref())
3486                    .map(|e| !has_side_effects(e))
3487                    .unwrap_or(false)
3488        })
3489        .collect();
3490    let impure: Vec<bool> = (0..n_vars).map(|v| single[v] && !pure[v]).collect();
3491
3492    // 1. Pure values: inline anywhere.
3493    if pure.iter().any(|&b| b) {
3494        let vals: Vec<Option<Expr>> = (0..n_vars)
3495            .map(|v| {
3496                if pure[v] {
3497                    values.get(v).cloned().flatten()
3498                } else {
3499                    None
3500                }
3501            })
3502            .collect();
3503        rewrite_exprs(s, &mut |e| {
3504            deep_rewrite_reads(e, &mut |x| {
3505                if let Expr::Local { var, .. } = x {
3506                    if let Some(Some(v)) = vals.get(*var as usize) {
3507                        *x = v.clone();
3508                    }
3509                }
3510            });
3511        });
3512        drop_defs(s, &pure);
3513    }
3514
3515    // 2. Impure values: inline only into the immediately following
3516    //    statement.
3517    if impure.iter().any(|&b| b) {
3518        forward_adjacent_impure(s, &values, &impure);
3519    }
3520}
3521
3522/// Count READS of variables (assignment targets excluded).
3523fn count_reads(s: &Stmt, out: &mut Vec<usize>) {
3524    walk_all(s, &mut |st| {
3525        let exprs: Vec<&Expr> = match st {
3526            // Assign must match BEFORE the generic ExprStmt arm (the
3527            // assignment target is a write, not a read).
3528            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
3529                let mut v: Vec<&Expr> = vec![value];
3530                if !matches!(&**target, Expr::Local { .. }) {
3531                    v.push(target);
3532                }
3533                v
3534            }
3535            // `v++` writes v — not a read (a compound `a[i]++` still
3536            // reads its owner/index subtree).
3537            Stmt::ExprStmt(e @ (Expr::PreIncDec { .. } | Expr::PostIncDec { .. })) => {
3538                let inner = match e {
3539                    Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => e,
3540                    _ => unreachable!(),
3541                };
3542                if matches!(**inner, Expr::Local { .. }) {
3543                    Vec::new()
3544                } else {
3545                    vec![e]
3546                }
3547            }
3548            Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
3549                vec![e]
3550            }
3551            Stmt::Return(Some(e)) => vec![e],
3552            Stmt::LocalDef { init: Some(e), .. } => vec![e],
3553            Stmt::If { cond, .. } => vec![cond],
3554            Stmt::While { cond, .. } => vec![cond],
3555            Stmt::DoWhile { cond, .. } => vec![cond],
3556            // Read sites that are NOT statement children (walk_all recurses
3557            // into bodies but these payloads are expressions on the node
3558            // itself). Omitting them under-counts reads, which would let
3559            // drop_dead_locals over-prune a var whose only use is a switch
3560            // selector / loop condition / lock / iterable.
3561            Stmt::Switch { selector, .. } => vec![selector],
3562            Stmt::ForEach { iterable, .. } => vec![iterable],
3563            Stmt::Synchronized { lock, .. } => vec![lock],
3564            Stmt::For { cond, update, .. } => {
3565                let mut v: Vec<&Expr> = Vec::with_capacity(update.len() + 1);
3566                if let Some(c) = cond {
3567                    v.push(c);
3568                }
3569                v.extend(update.iter());
3570                v
3571            }
3572            Stmt::Assert { cond, msg } => {
3573                let mut v: Vec<&Expr> = vec![cond];
3574                if let Some(m) = msg {
3575                    v.push(m);
3576                }
3577                v
3578            }
3579            _ => Vec::new(),
3580        };
3581        for e in exprs {
3582            visit_exprs(e, &mut |x| {
3583                if let Expr::Local { var, .. } = x {
3584                    grow_to(out, *var);
3585                    out[*var as usize] += 1;
3586                }
3587            });
3588        }
3589    });
3590}
3591
3592fn drop_defs(s: &mut Stmt, vars: &[bool]) {
3593    walk_mut_deep(s, &mut |st| {
3594        if let Stmt::Block(v) = st {
3595            v.retain(|x| match x {
3596                Stmt::ExprStmt(Expr::Assign { target, .. }) => match &**target {
3597                    Expr::Local { var, .. } => !vars.get(*var as usize).copied().unwrap_or(false),
3598                    _ => true,
3599                },
3600                Stmt::LocalDef { var, .. } => !vars.get(*var as usize).copied().unwrap_or(false),
3601                _ => true,
3602            });
3603        }
3604    });
3605}
3606
3607/// For each statement list: `v = IMPURE; NEXT(v)` folds NEXT's reference.
3608fn forward_adjacent_impure(s: &mut Stmt, values: &[Option<Expr>], impure: &[bool]) {
3609    if let Stmt::Block(v) = s {
3610        let mut i = 0;
3611        while i + 1 < v.len() {
3612            let def_var = match &v[i] {
3613                Stmt::ExprStmt(Expr::Assign { target, .. }) => match &**target {
3614                    Expr::Local { var, .. } => Some(*var),
3615                    _ => None,
3616                },
3617                Stmt::LocalDef { var, .. } => Some(*var),
3618                _ => None,
3619            };
3620            if let Some(var) = def_var {
3621                if impure.get(var as usize).copied().unwrap_or(false) {
3622                    // Does the NEXT statement reference var?
3623                    let mut reads_here = vec![0usize; values.len().max(1)];
3624                    count_reads(&v[i + 1], &mut reads_here);
3625                    if reads_here.get(var as usize).copied().unwrap_or(0) == 1 {
3626                        // The def statement's CURRENT value (earlier inlines
3627                        // in this walk may have rewritten it — the pre-built
3628                        // values map would be stale).
3629                        let val = match &v[i] {
3630                            Stmt::ExprStmt(Expr::Assign { value, .. }) => (**value).clone(),
3631                            Stmt::LocalDef { init: Some(e), .. } => (*e).clone(),
3632                            _ => {
3633                                i += 1;
3634                                continue;
3635                            }
3636                        };
3637                        rewrite_exprs(&mut v[i + 1], &mut |e| {
3638                            deep_rewrite_reads(e, &mut |x| {
3639                                if let Expr::Local { var: v2, .. } = x {
3640                                    if *v2 == var {
3641                                        *x = val.clone();
3642                                    }
3643                                }
3644                            });
3645                        });
3646                        v.remove(i);
3647                        // The PREVIOUS definition may now be adjacent to its
3648                        // (shifted) use — re-examine it.
3649                        i = i.saturating_sub(1);
3650                        continue;
3651                    }
3652                }
3653            }
3654            i += 1;
3655        }
3656    }
3657    walk_mut(s, &mut |x| forward_adjacent_impure(x, values, impure));
3658}
3659
3660// ---------------------------------------------------------------------------
3661// Type inference, booleanization, null comparisons, declarations
3662// ---------------------------------------------------------------------------
3663
3664/// Evidence-based type inference for synthetic (non-parameter) vars.
3665pub fn infer_types(vt: &mut VarTable, body: &mut Stmt, ret: &JavaType, env: &MethodEnv) {
3666    let n = vt.vars.len();
3667    // (type, strong) — DIRECTIONAL evidence. Strong facts (an assigned
3668    // value's type, the declared return/throw/monitor type) DEFINE the
3669    // variable; weak facts (call argument expectations, receiver
3670    // contexts, numeric literals) only constrain a use. jadx's bound
3671    // system carries the same direction; a flat pool let a weak
3672    // expectation (`v26 = p1` with boolean p1 beside `v26 = 0` sugar)
3673    // outvote the definition.
3674    let mut evidence: Vec<Vec<(JavaType, bool)>> = vec![Vec::new(); n];
3675    let ev = |evidence: &mut Vec<Vec<(JavaType, bool)>>, var: u32, t: JavaType, strong: bool| {
3676        if (var as usize) < evidence.len() {
3677            evidence[var as usize].push((t, strong));
3678        }
3679    };
3680
3681    walk_all(body, &mut |st| match st {
3682        Stmt::LocalDef { var, init, .. } => {
3683            if let Some(e) = init {
3684                expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
3685                // The initializer DEFINES the declared type: `long v = l(...)`
3686                // must not stay `int v` (silent 64-bit truncation in the
3687                // eyes of a reader; javac rejects it as lossy). Null
3688                // carries no type (it would DOWNGRADE String to Object).
3689                // Literals are weak (0/1 is boolean sugar half the time);
3690                // typed producers and parameter sources are strong;
3691                // local-to-local copies are weak (register reuse).
3692                let strong = match e {
3693                    Expr::Const(_) => false,
3694                    Expr::Local { var: src, .. } => {
3695                        let src_ty = vt.vars.get(*src as usize).map(|v| v.ty.erased());
3696                        let tgt_ty = vt.vars.get(*var as usize).map(|v| v.ty.erased());
3697                        match (src_ty, tgt_ty) {
3698                            (Some(s), Some(g)) => {
3699                                (s.is_numeric() && g.is_numeric())
3700                                    || (s.is_reference() && g.is_reference())
3701                            }
3702                            _ => false,
3703                        }
3704                    }
3705                    _ => true,
3706                };
3707                ev(&mut evidence, *var, e.type_ref().erased(), strong);
3708            }
3709        }
3710        Stmt::ExprStmt(e) => {
3711            // Assignment targets: a typed PRODUCER (method/new/field) or a
3712            // PARAMETER source is a STRONG definition (`v26 = p1` with
3713            // boolean p1). A local-to-local copy is weak — register reuse
3714            // and merge materialization emit exactly that shape with
3715            // mismatched types (`sb7 = compareTo10` in a Kotlin when).
3716            // Literals stay weak.
3717            if let Expr::Assign { target, value, .. } = e {
3718                // A field WRITE through a local (`v.f = x`, dex iput)
3719                // proves v is a reference of the field's declaring
3720                // class AT THAT POINT — an iput on a boolean register
3721                // cannot exist in valid dex. Strong: it must outrank a
3722                // strong boolean copy chain into the same reused slot
3723                // (weixin coroutine state machines).
3724                if let Expr::Field { owner: Some(o), cls, is_static: false, .. } = &**target {
3725                    if cls.is_empty() {
3726                        // array-length pseudo-field: no declaring class
3727                    } else if let Expr::Local { var, .. } = &**o {
3728                        ev(
3729                            &mut evidence,
3730                            *var,
3731                            JavaType::Object(cls.clone()),
3732                            true,
3733                        );
3734                    }
3735                }
3736                if let Expr::Local { var, .. } = &**target {
3737                    let strong = match &**value {
3738                        Expr::Const(_) => false,
3739                        Expr::Local { var: src, .. } => {
3740                            // A local copy is strong when the source and
3741                            // the target's register type are the same
3742                            // FAMILY (numeric↔numeric, reference↔
3743                            // reference) — a cross-family copy (int into
3744                            // a StringBuilder local) is merge-material
3745                            // residue and stays weak.
3746                            let src_ty = vt.vars.get(*src as usize).map(|v| v.ty.erased());
3747                            let tgt_ty = vt.vars.get(*var as usize).map(|v| v.ty.erased());
3748                            match (src_ty, tgt_ty) {
3749                                (Some(s), Some(g)) => {
3750                                    (s.is_numeric() && g.is_numeric())
3751                                        || (s.is_reference() && g.is_reference())
3752                                }
3753                                _ => false,
3754                            }
3755                        }
3756                        _ => true,
3757                    };
3758                    ev(&mut evidence, *var, value.type_ref().erased(), strong);
3759                }
3760            }
3761            expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
3762        }
3763        Stmt::Throw(e) => {
3764            expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
3765            if let Expr::Local { var, .. } = e {
3766                ev(
3767                    &mut evidence,
3768                    *var,
3769                    JavaType::Object("java/lang/Throwable".into()),
3770                    true,
3771                );
3772            }
3773        }
3774        Stmt::Return(Some(e)) => {
3775            expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
3776            if let Expr::Local { var, .. } = e {
3777                ev(&mut evidence, *var, ret.clone(), true);
3778            }
3779        }
3780        Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
3781            if let Expr::Local { var, .. } = e {
3782                ev(
3783                    &mut evidence,
3784                    *var,
3785                    JavaType::Object("java/lang/Object".into()),
3786                    true,
3787                );
3788            }
3789        }
3790        Stmt::ForEach {
3791            var,
3792            iterable,
3793            is_array,
3794            ..
3795        } => {
3796            if let Expr::Local { var: v0, .. } = iterable {
3797                if *is_array {
3798                    ev(&mut evidence, *v0, JavaType::Array(Box::new(JavaType::Int)), true);
3799                } else {
3800                    ev(
3801                        &mut evidence,
3802                        *v0,
3803                        JavaType::Object("java/lang/Object".into()),
3804                        true,
3805                    );
3806                }
3807            }
3808            let _ = var;
3809        }
3810        _ => {}
3811    });
3812    let _ = env;
3813
3814    // Return-position locals take the declared return type (also inside
3815    // nested returns — handled by the walk above).
3816
3817        // Vars dereferenced as field/method receivers or arrays: their
3818    // resolved type must be a dereferenceable class — a boxed Boolean
3819    // in the strong set (Kotlin Result registers hold `Boolean | impl`
3820    // across coroutine case arms) must lose to the real class even
3821    // when the Boolean evidence comes first (weixin ry0/h2's
3822    // `Boolean bool19` receiving `bool19.d = x`).
3823    let mut derefed: Vec<bool> = vec![false; vt.vars.len()];
3824    mark_derefs(body, &mut derefed);
3825    // Array-indexed vars: their resolved type must be an ARRAY when
3826    // the evidence has one (`num4[v68]` with Integer-typed num4 —
3827    // 需要数组但找到Integer family); member derefs keep the class
3828    // preference below.
3829    let mut arr_derefed: Vec<bool> = vec![false; vt.vars.len()];
3830    visit_all_exprs(body, &mut |x| {
3831        if let Expr::ArrayIndex { array, .. } = x {
3832            if let Expr::Local { var, .. } = &**array {
3833                if (*var as usize) < arr_derefed.len() {
3834                    arr_derefed[*var as usize] = true;
3835                }
3836            }
3837        }
3838        // `v != null` is STRONG reference evidence: the lifter only
3839        // renders the null form when the dex value view held a
3840        // reference register at that pc — an int-typed generation
3841        // compared against null is phi-confluence residue (yq0/g1's
3842        // `v95_g12 != null`, 546-line 二元运算符 family). Strong Object
3843        // makes the resolver retype and split_generations separate the
3844        // int writes into their own generation.
3845        if let Expr::Bin { op, l, r, .. } = x {
3846            if matches!(op, BinOp::Eq | BinOp::Ne)
3847                && (matches!(&**r, Expr::Const(ConstVal::Null))
3848                    || matches!(&**l, Expr::Const(ConstVal::Null)))
3849            {
3850                for side in [l, r] {
3851                    if let Expr::Local { var, .. } = &**side {
3852                        ev(
3853                            &mut evidence,
3854                            *var,
3855                            JavaType::Object("java/lang/Object".into()),
3856                            true,
3857                        );
3858                    }
3859                }
3860            }
3861        }
3862    });
3863    const FINAL_JDK: &[&str] = &[
3864        "java/lang/Boolean",
3865        "java/lang/String",
3866        "java/lang/Integer",
3867        "java/lang/Long",
3868        "java/lang/Short",
3869        "java/lang/Byte",
3870        "java/lang/Character",
3871        "java/lang/Float",
3872        "java/lang/Double",
3873    ];
3874    for (i, evs) in evidence.iter().enumerate() {
3875        let info = &vt.vars[i];
3876        if info.is_param {
3877            continue;
3878        }
3879        let strong: Vec<JavaType> = evs
3880            .iter()
3881            .filter(|(_, s)| *s)
3882            .map(|(t, _)| t.clone())
3883            .collect();
3884        let all: Vec<JavaType> = evs.iter().map(|(t, _)| t.clone()).collect();
3885        if info.ty.erased().is_reference() {
3886            // Materialization residue: no strong definition, and the weak
3887            // pool names ≥2 DIFFERENT classes (a Kotlin `when` lowered
3888            // every branch's value into one register slot) — neither
3889            // class can win, every assignment needs boxing headroom:
3890            // widen to Object (uses get receiver casts).
3891            if strong.is_empty() {
3892                let refs: std::collections::HashSet<&str> = all
3893                    .iter()
3894                    .filter_map(|t| match t {
3895                        JavaType::Object(n) if n.as_ref() != "java/lang/Object" => {
3896                            Some(n.as_ref())
3897                        }
3898                        _ => None,
3899                    })
3900                    .collect();
3901                let mixed = all.iter().any(|t| t.is_numeric());
3902                if refs.len() >= 2 || (mixed && !refs.is_empty()) || (mixed && all.iter().any(|t| matches!(t, JavaType::Array(_)))) {
3903                    vt.vars[i].ty =
3904                        TypeRef::J(JavaType::Object("java/lang/Object".into()));
3905                    continue;
3906                }
3907            }
3908                        // Array-dereferenced vars prefer array evidence (an
3909            // Integer producer cannot be indexed).
3910            if arr_derefed[i] {
3911                let arr = strong
3912                    .iter()
3913                    .chain(all.iter())
3914                    .find(|t| matches!(t, JavaType::Array(_)))
3915                    .cloned();
3916                if let Some(t) = arr {
3917                    vt.vars[i].ty = TypeRef::J(t);
3918                    continue;
3919                }
3920            }
3921            // Strong definitions outrank weak expectations. A
3922            // dereferenced var prefers a real class over final JDK
3923            // boxes (Boolean can never carry `.d = x`).
3924            let deref_pick = || {
3925                if !derefed[i] {
3926                    return None;
3927                }
3928                strong
3929                    .iter()
3930                    .find(|t| {
3931                        matches!(t, JavaType::Object(n)
3932                            if !n.is_empty()
3933                                && n.as_ref() != "java/lang/Object"
3934                                && !FINAL_JDK.contains(&n.as_ref()))
3935                    })
3936                    .cloned()
3937            };
3938            if let Some(t) = deref_pick()
3939                .or_else(|| pick_object(&strong))
3940                .or_else(|| pick_object(&all))
3941            {
3942                vt.vars[i].ty = TypeRef::J(t);
3943            }
3944        } else if let Some(t) = pick_object(&strong) {
3945            // A strong reference definition (typed producer or field
3946            // write) outranks a strong boolean copy: Boolean assigns
3947            // INTO an Object slot legally, never the reverse.
3948            vt.vars[i].ty = TypeRef::J(t);
3949        } else if strong.contains(&JavaType::Boolean) {
3950            // A boolean-typed SOURCE assigned into the variable is a
3951            // definition (`v26 = p1` with boolean p1); the 0/1 literals
3952            // that share the pool are boolean sugar as often as not.
3953            vt.vars[i].ty = TypeRef::J(JavaType::Boolean);
3954        } else if !all.is_empty() && all.iter().all(|t| t.is_numeric()) {
3955            if let Some(t) = pick_numeric(&all) {
3956                vt.vars[i].ty = TypeRef::J(t);
3957            }
3958        } else {
3959            // Mixed-family materialization on a numeric-declared slot
3960            // (int slot receiving StringBuilders — the register was the
3961            // `when` accumulator): Object, same as the reference side.
3962            let refs = all.iter().filter(|t| t.is_reference()).count();
3963            let nums = all.iter().filter(|t| t.is_numeric()).count();
3964            if refs > 0 && nums > 0 {
3965                vt.vars[i].ty =
3966                    TypeRef::J(JavaType::Object("java/lang/Object".into()));
3967            } else if let Some(t) = pick_object(&strong).or_else(|| pick_object(&all)) {
3968                vt.vars[i].ty = TypeRef::J(t);
3969            }
3970        }
3971    }
3972
3973    // Rewrite embedded Local types. Borrowed view + write-only-on-change:
3974    // the table clone was n_vars JavaType clones per method, and the
3975    // rewrite cloned a type into EVERY Local node — most already carry
3976    // the right one (JavaType::Object clones allocate).
3977    let types: Vec<&TypeRef> = vt.vars.iter().map(|v| &v.ty).collect();
3978    rewrite_exprs(body, &mut |e| {
3979        deep_rewrite(e, &mut |x| {
3980            if let Expr::Local { var, ty } = x {
3981                if let Some(want) = types.get(*var as usize) {
3982                    if ty != *want {
3983                        *ty = (*want).clone();
3984                    }
3985                }
3986            }
3987        });
3988    });
3989
3990    // Numeric literals take their variable's declared width (`double v = 0L`
3991    // prints as `0.0`; `int x = 5L` as `5`).
3992    coerce_num_consts(body, &types);
3993}
3994
3995fn coerce_num_consts(body: &mut Stmt, types: &[&TypeRef]) {
3996    walk_mut_deep(body, &mut |st| {
3997        let (var, val): (u32, &mut Expr) = match st {
3998            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => match &mut **target {
3999                Expr::Local { var, .. } => (*var, value),
4000                _ => return,
4001            },
4002            Stmt::LocalDef {
4003                var, init: Some(e), ..
4004            } => (*var, e),
4005            _ => return,
4006        };
4007        let want = match types.get(var as usize) {
4008            Some(TypeRef::J(t)) => t.clone(),
4009            _ => return,
4010        };
4011        let rewritten = match &*val {
4012            Expr::Const(ConstVal::Long(l)) => match &want {
4013                JavaType::Double => Some(Expr::Const(ConstVal::Double(*l as f64))),
4014                JavaType::Float => Some(Expr::Const(ConstVal::Float(*l as f32))),
4015                _ => None,
4016            },
4017            Expr::Const(ConstVal::Int(i)) => match &want {
4018                JavaType::Double => Some(Expr::Const(ConstVal::Double(*i as f64))),
4019                JavaType::Float => Some(Expr::Const(ConstVal::Float(*i as f32))),
4020                _ => None,
4021            },
4022            _ => None,
4023        };
4024        if let Some(e) = rewritten {
4025            *val = e;
4026        }
4027    });
4028}
4029
4030fn pick_object(evs: &[JavaType]) -> Option<JavaType> {
4031    evs.iter()
4032        .find(|t| {
4033            t.is_reference()
4034                && !matches!(t, JavaType::Object(n) if n.is_empty() || n.as_ref() == "java/lang/Object")
4035        })
4036        .cloned()
4037}
4038
4039fn pick_numeric(evs: &[JavaType]) -> Option<JavaType> {
4040    if evs.is_empty() {
4041        return None;
4042    }
4043    let mut t = evs[0].clone();
4044    for x in &evs[1..] {
4045        t = join_numeric(&t, x);
4046    }
4047    Some(t)
4048}
4049
4050fn join_numeric(a: &JavaType, b: &JavaType) -> JavaType {
4051    if a == b {
4052        return a.clone();
4053    }
4054    match (a, b) {
4055        (JavaType::Double, _) | (_, JavaType::Double) => JavaType::Double,
4056        (JavaType::Float, _) | (_, JavaType::Float) => JavaType::Float,
4057        (JavaType::Long, _) | (_, JavaType::Long) => JavaType::Long,
4058        (JavaType::Int, x) if x.is_integral() => JavaType::Int,
4059        (x, JavaType::Int) if x.is_integral() => JavaType::Int,
4060        (x, y) if x.is_integral() && y.is_integral() => JavaType::Int,
4061        _ => JavaType::Int,
4062    }
4063}
4064
4065/// Gather per-var type evidence from one expression.
4066fn expr_evidence<F: FnMut(u32, JavaType)>(e: &Expr, f: &mut F) {
4067    visit_exprs(e, &mut |x| match x {
4068        Expr::Method {
4069            owner,
4070            args,
4071            desc,
4072            is_static,
4073            cls,
4074            ..
4075        } => {
4076            if !*is_static {
4077                if let Some(o) = owner {
4078                    if let Expr::Local { var, .. } = &**o {
4079                        f(*var, JavaType::Object(cls.clone()));
4080                    }
4081                }
4082            }
4083            for (i, a) in args.iter().enumerate() {
4084                if let Expr::Local { var, .. } = a {
4085                    if let Some(t) = desc.args.get(i) {
4086                        f(*var, t.clone());
4087                    }
4088                }
4089            }
4090        }
4091        Expr::Field {
4092            owner,
4093            cls,
4094            ty,
4095            is_static,
4096            ..
4097        } => {
4098            if !*is_static && !cls.is_empty() {
4099                if let Some(o) = owner {
4100                    if let Expr::Local { var, .. } = &**o {
4101                        // The field ref's declaring class is dex-exact
4102                        // evidence — the old hardcoded Object let a
4103                        // boolean copy chain outvote it (weixin ry0/h2's
4104                        // `Boolean bool19` receiving `bool19.d = x` —
4105                        // the 339-line 找不到符号 变量 d family).
4106                        // EMPTY cls = the array-length pseudo-field
4107                        // (lift models `a.length` as Field{cls:""}) —
4108                        // Object("") evidence poisoned pick_object into
4109                        // empty-typed declarations and `() x` casts.
4110                        f(*var, JavaType::Object(cls.clone()));
4111                    }
4112                }
4113            }
4114            let _ = ty;
4115        }
4116        // `v != null` / `v == null`: the register holds a reference at
4117        // that point — weak Object evidence keeps int-typed generations
4118        // from winning the pool (yq0/g1's `v95_g12 != null` with int
4119        // v95_g12 — 二元运算符 '!=' 546-line family).
4120        Expr::Bin { op, l, r, .. }
4121            if matches!(op, BinOp::Eq | BinOp::Ne)
4122                && (matches!(&**r, Expr::Const(ConstVal::Null))
4123                    || matches!(&**l, Expr::Const(ConstVal::Null))) =>
4124        {
4125            for side in [l, r] {
4126                if let Expr::Local { var, .. } = &**side {
4127                    f(*var, JavaType::Object("java/lang/Object".into()));
4128                }
4129            }
4130        }
4131        Expr::ArrayIndex { array, .. } => {
4132            if let Expr::Local { var, ty, .. } = &**array {
4133                // Reinforce the local's KNOWN array type: the hardcoded
4134                // int[] fallback contradicted a typed array local (a
4135                // register reused for byte[], int[] then byte[] across
4136                // one clinit labeled the byte[] defs `int[]`).
4137                let t = ty.erased();
4138                if matches!(t, JavaType::Array(_)) {
4139                    f(*var, t);
4140                } else {
4141                    f(*var, JavaType::Array(Box::new(JavaType::Int)));
4142                }
4143            }
4144        }
4145        Expr::Assign { .. } => {
4146            // Covered by the typed strong-evidence walk in infer_types;
4147            // keeping a weak duplicate here let expectations outvote it.
4148        }
4149        Expr::Cast { e: inner, .. } => {
4150            // A cast is an EXPLICIT narrowing at the use site — it must
4151            // NOT feed the cast target as evidence for the variable
4152            // (`Object get2 = list.get(2); j((String) get2);` had get2
4153            // retyped to String, making the assignment incompatible).
4154            // jadx's bounds carry direction: a USE bound narrows nothing.
4155            let _ = inner;
4156        }
4157        Expr::InstanceOf { e: inner, .. } => {
4158            let _ = inner;
4159        }
4160        _ => {}
4161    });
4162}
4163
4164/// Mark locals that OWN a non-static field access and whose inferred type
4165/// is a concrete `$<digits>` class (a d8/R8-desugared lambda or a Kotlin
4166/// suspend-lambda). Emit demotes such a type to its SAM interface in
4167/// DECLARED positions (`Function2 v = new Outer$..$1(..)`) for phi-friendly
4168/// readability — but interfaces carry no instance fields, so the Kotlin
4169/// coroutine `create()` capture writes (`v.L$0 = obj`, also I$/J$) render
4170/// "找不到符号 变量 L$0" (weibo MutableScatterMap$..$iterator$1, ~1k).
4171/// Forcing the concrete declared type keeps the field access legal; the phi
4172/// case is excluded because a disagreed merge stays Object (wide_stack_vars),
4173/// never a specific concrete class. Gate mirrors emit's demotion exactly
4174/// (last `$` segment all digits) so nothing else changes.
4175pub fn mark_field_owner_concrete(vt: &mut VarTable, body: &mut Stmt) {
4176    let mut owners: HashSet<u32> = HashSet::default();
4177    rewrite_exprs(body, &mut |e| {
4178        visit_exprs(e, &mut |x| {
4179            if let Expr::Field {
4180                owner: Some(o),
4181                is_static: false,
4182                ..
4183            } = x
4184            {
4185                if let Expr::Local { var, .. } = &**o {
4186                    owners.insert(*var);
4187                }
4188            }
4189        });
4190    });
4191    for var in owners {
4192        let i = var as usize;
4193        if i >= vt.vars.len() {
4194            continue;
4195        }
4196        if let JavaType::Object(cls) = vt.vars[i].ty.erased() {
4197            if let Some(last) = cls.rsplit('$').next() {
4198                if !last.is_empty() && last.chars().all(|c| c.is_ascii_digit()) {
4199                    vt.force_concrete_vars.insert(var);
4200                }
4201            }
4202        }
4203    }
4204    // EVERY local whose resolved type is a concrete `$<digits>` class
4205    // keeps its binary name, not just field owners: when one side of an
4206    // assignment falls back to the SAM/super (`ContinuationImpl v7`) and
4207    // its sibling stays concrete (`$reportWhenComplete$1 v9`), the pair
4208    // is an uncastable "ContinuationImpl无法转换为…$1" (weibo coroutine
4209    // prologues ×282). The anon fallback still serves INTERFACE-typed
4210    // phi slots (branches minting different lambdas) — their vt type is
4211    // the join, never the $N class.
4212    let concrete: Vec<u32> = vt
4213        .vars
4214        .iter()
4215        .filter(|v| {
4216            if let JavaType::Object(cls) = v.ty.erased() {
4217                cls.rsplit('$')
4218                    .next()
4219                    .is_some_and(|l| !l.is_empty() && l.chars().all(|c| c.is_ascii_digit()))
4220            } else {
4221                false
4222            }
4223        })
4224        .map(|v| v.id)
4225        .collect();
4226    for var in concrete {
4227        vt.force_concrete_vars.insert(var);
4228    }
4229}
4230
4231/// Type inference can leave a specific-reference-typed target assigned
4232/// from a `java/lang/Object`-typed value: a phi that merged String and
4233/// Object then settled on String (`String str4; ... str4 = obj;` where
4234/// `obj` is an Object field — y5/n.java), or an Object local flowing to a
4235/// typed field. Java requires a narrowing cast there; insert `(T) value`
4236/// at the USE site (round-40 philosophy: narrow at use, never retype the
4237/// declaration). Guarded tightly: only when the value's resolved static
4238/// type is EXACTLY `java/lang/Object` (the untyped top — a known subtype
4239/// is never re-cast), the value is not a null const (assignable to any
4240/// ref) nor already a cast, and the target is a specific reference type
4241/// (Object→int/boolean is unboxing, a different problem left alone).
4242pub fn insert_object_narrowing_casts(
4243    vt: &VarTable,
4244    body: &mut Stmt,
4245    pool: &DexPool,
4246    ret: &JavaType,
4247) {
4248    // Resolve a value's static type through the VarTable for locals (the
4249    // embedded Local ty can lag infer_types), else the expr's own type.
4250    let value_ty = |e: &Expr| -> JavaType {
4251        match e {
4252            Expr::Local { var, .. } => vt.var(*var).ty.erased(),
4253            other => other.type_ref().erased(),
4254        }
4255    };
4256    let is_top_object = |e: &Expr| -> bool {
4257        value_ty(e) == JavaType::Object("java/lang/Object".into())
4258    };
4259    // A specific reference target type (a class other than
4260    // java/lang/Object, or an array — `String[] v = obj` needs the
4261    // cast exactly like `String v = obj`).
4262    let specific_ref = |ty: &TypeRef| -> Option<TypeRef> {
4263        match ty.erased() {
4264            JavaType::Object(c) if c.as_ref() != "java/lang/Object" => Some(ty.clone()),
4265            JavaType::Array(_) => Some(ty.clone()),
4266            _ => None,
4267        }
4268    };
4269    let castable = |e: &Expr| -> bool {
4270        is_top_object(e)
4271            && !matches!(e, Expr::Const(_) | Expr::Cast { .. } | Expr::InstanceOf { .. })
4272    };
4273    // Downcast witness: a SPECIFIC-ref value whose static type does not
4274    // fit the specific-ref target (`v9($1) = v7` with v7:
4275    // ContinuationImpl — weibo coroutine prologue ×282). The dex move
4276    // was verifier-proven against the target type, so the cast states
4277    // the bytecode truth; an actually-unconvertible static pair (both
4278    // classes, unrelated — the value view conflated) rides the
4279    // emitter's (Object) relay and still compiles. Framework types the
4280    // pool cannot order take the cast too: a downcast renders directly,
4281    // an unrelated pair relays.
4282    let needs_downcast = |e: &Expr, tt: &JavaType| -> bool {
4283        if matches!(e, Expr::Const(_) | Expr::Cast { .. } | Expr::InstanceOf { .. }) {
4284            return false;
4285        }
4286        let v = value_ty(e);
4287        let (JavaType::Object(vn), JavaType::Object(tn)) = (&v, tt) else {
4288            return false;
4289        };
4290        if vn.as_ref() == "java/lang/Object" || tn.as_ref() == "java/lang/Object" {
4291            return false;
4292        }
4293        vn != tn && !pool.is_subtype(vn, tn)
4294    };
4295    walk_mut_deep(body, &mut |st| match st {
4296        Stmt::ExprStmt(Expr::Assign {
4297            target,
4298            value,
4299            op: AssignOp::Plain,
4300            ..
4301        }) => {
4302            let tgt = match &**target {
4303                Expr::Local { var, .. } => vt.var(*var).ty.clone(),
4304                Expr::Field { ty, .. } => ty.clone(),
4305                // Array-element store `arr[i] = value`: the target type is
4306                // the ELEMENT type. Without this arm an Object→element
4307                // assignment (`String[] a; a[0] = pair.first` — Pair.first
4308                // erases to Object) skipped the cast and failed "Object
4309                // 无法转换为String" (weixin ×~150). Resolve the array's type
4310                // through the vt (the embedded Local ty can lag infer_types).
4311                Expr::ArrayIndex { array, .. } => {
4312                    let aty = match &**array {
4313                        Expr::Local { var, .. } => vt.var(*var).ty.erased(),
4314                        other => other.type_ref().erased(),
4315                    };
4316                    match aty {
4317                        JavaType::Array(inner) => TypeRef::J(*inner),
4318                        _ => return,
4319                    }
4320                }
4321                _ => return,
4322            };
4323            if let Some(t) = specific_ref(&tgt) {
4324                if castable(value) || needs_downcast(value, &tgt.erased()) {
4325                    let v = std::mem::replace(value, Box::new(Expr::This));
4326                    **value = Expr::Cast { ty: t, e: v };
4327                }
4328            }
4329        }
4330        Stmt::LocalDef { var, init: Some(value), .. } => {
4331            if let Some(t) = specific_ref(&vt.var(*var).ty) {
4332                let tt = vt.var(*var).ty.erased();
4333                if castable(value) || needs_downcast(value, &tt) {
4334                    let v = std::mem::replace(value, Expr::This);
4335                    *value = Expr::Cast {
4336                        ty: t,
4337                        e: Box::new(v),
4338                    };
4339                }
4340            }
4341        }
4342        Stmt::Return(Some(value)) => {
4343            if let Some(t) = specific_ref(&TypeRef::J(ret.clone())) {
4344                if castable(value) || needs_downcast(value, ret) {
4345                    let v = std::mem::replace(value, Expr::This);
4346                    *value = Expr::Cast { ty: t, e: Box::new(v) };
4347                }
4348            }
4349        }
4350        _ => {}
4351    });
4352    // Descriptor-exact formal casts at CALL args: the dex already
4353    // resolved the target method, so casting a top-Object actual to
4354    // its declared formal cannot shift overload resolution (the r57
4355    // args lesson applies to GUESSED casts, not descriptor-exact
4356    // ones). `"source_type:".concat(obj2)`, `new my0.j(obj2, ..)` —
4357    // Object无法转换为String family.
4358    walk_stmt_exprs(body, &mut |e| {
4359        deep_rewrite(e, &mut |x| {
4360            let (args, arg_tys): (&mut Vec<Expr>, &[JavaType]) = match x {
4361                Expr::Method { desc, args, is_dynamic: false, .. } => {
4362                    (args, desc.args.as_slice())
4363                }
4364                // Ctor calls are their own variant — the dex resolved the
4365                // exact <init>, so formal casts are just as safe here
4366                // (`new r(obj)` — Object无法转换为r family).
4367                Expr::New { arg_tys, args, .. } => (args, arg_tys.as_slice()),
4368                _ => return,
4369            };
4370            for (i, a) in args.iter_mut().enumerate() {
4371                let Some(formal) = arg_tys.get(i) else {
4372                    continue;
4373                };
4374                let Some(t) = specific_ref(&TypeRef::J(formal.clone())) else {
4375                    continue;
4376                };
4377                if !(castable(a) || needs_downcast(a, formal)) {
4378                    continue;
4379                }
4380                let v = std::mem::replace(a, Expr::Const(ConstVal::Null));
4381                *a = Expr::Cast { ty: t, e: Box::new(v) };
4382            }
4383        });
4384    });
4385}
4386
4387/// Numeric narrowing/widening mismatches at Assign / LocalDef / Return
4388/// positions get an explicit cast: dex int-to-byte / long-to-int flows
4389/// are implicit in the register machine, Java rejects them ("从long
4390/// 转换到int可能会有损失" — okio Buffer ×321 weibo lines). Casts at
4391/// these three positions are descriptor-faithful and cannot shift
4392/// overload resolution (the r57 args-position lesson). Boolean/numeric
4393/// mixes are NOT castable in Java and stay with the generation-split
4394/// machinery.
4395pub fn fix_primitive_assign_casts(vt: &VarTable, body: &mut Stmt, ret_ty: &JavaType) {
4396    fn numlike(t: &JavaType) -> bool {
4397        matches!(
4398            t,
4399            JavaType::Byte
4400                | JavaType::Short
4401                | JavaType::Int
4402                | JavaType::Char
4403                | JavaType::Long
4404                | JavaType::Float
4405                | JavaType::Double
4406        )
4407    }
4408    fn val_ty(e: &Expr, vt: &VarTable) -> JavaType {
4409        match e {
4410            Expr::Local { var, .. } if (*var as usize) < vt.vars.len() => {
4411                vt.var(*var).ty.erased()
4412            }
4413            other => other.type_ref().erased(),
4414        }
4415    }
4416    fn wrap(ty: &JavaType, value: &mut Expr) {
4417        if matches!(value, Expr::Cast { .. }) {
4418            return;
4419        }
4420        if val_ty_static(value) == *ty {
4421            return;
4422        }
4423        let taken = std::mem::replace(value, Expr::Const(ConstVal::Null));
4424        *value = Expr::Cast {
4425            ty: TypeRef::J(ty.clone()),
4426            e: Box::new(taken),
4427        };
4428    }
4429    fn val_ty_static(e: &Expr) -> JavaType {
4430        e.type_ref().erased()
4431    }
4432    /// Target-typed coercion for one assignment value. Numeric mixes
4433    /// cast; boolean mixes CONVERT (Java has no cast between boolean
4434    /// and any numeric): `d = b` → `(double) (b ? 1 : 0)`, `b = i` →
4435    /// `i != 0` — mirrors fix_primitive_arg_bridges' formal-side arms.
4436    /// The generation split cannot own these: at an if-join the
4437    /// one-sided split is undone (rename_gen_back) and the incompatible
4438    /// assign re-exposes ("boolean无法转换为double", weixin s9/t01-f).
4439    fn coerce(tt: &JavaType, value: &mut Expr, vt: &VarTable) {
4440        // Raw const bits against a floating target are the FLOAT value,
4441        // not an int to widen (`float f = <0x41000000 bits>` is 8.0f;
4442        // the (float) cast path would silently store 1.09e9f). Mirror of
4443        // fix_primitive_arg_bridges' descriptor-formal reinterpretation.
4444        match (&*value, tt) {
4445            (Expr::Const(ConstVal::Int(b)), JavaType::Float) => {
4446                *value = Expr::Const(ConstVal::Float(f32::from_bits(*b as u32)));
4447                return;
4448            }
4449            (Expr::Const(ConstVal::Long(b)), JavaType::Double) => {
4450                *value = Expr::Const(ConstVal::Double(f64::from_bits(*b as u64)));
4451                return;
4452            }
4453            (Expr::Const(ConstVal::Float(f)), JavaType::Int) => {
4454                *value = Expr::Const(ConstVal::Int(f.to_bits() as i32));
4455                return;
4456            }
4457            (Expr::Const(ConstVal::Double(d)), JavaType::Long) => {
4458                *value = Expr::Const(ConstVal::Long(d.to_bits() as i64));
4459                return;
4460            }
4461            _ => {}
4462        }
4463        let vt_val = val_ty(value, vt);
4464        let tt_bool = matches!(tt, JavaType::Boolean);
4465        let val_bool = matches!(vt_val, JavaType::Boolean);
4466        if tt_bool && !val_bool && numlike(&vt_val) {
4467            if matches!(
4468                value,
4469                Expr::Const(_)
4470                    | Expr::Bin { op: BinOp::Eq | BinOp::Ne, .. }
4471                    | Expr::Un { op: UnOp::Not, .. }
4472            ) {
4473                // Bare 0/1 constants render as false/true through the
4474                // emitter's bool coercion — wrapping them in `!= 0`
4475                // produced `0 != 0` noise the value-diamond fold then
4476                // had to re-interpret.
4477                return;
4478            }
4479            let taken = std::mem::replace(value, Expr::Const(ConstVal::Null));
4480            *value = Expr::Bin {
4481                op: BinOp::Ne,
4482                l: Box::new(taken),
4483                r: Box::new(Expr::Const(ConstVal::Int(0))),
4484                ty: Some(TypeRef::J(JavaType::Boolean)),
4485            };
4486        } else if !tt_bool && val_bool && numlike(tt) {
4487            if matches!(value, Expr::Cast { .. }) {
4488                return;
4489            }
4490            let taken = std::mem::replace(value, Expr::Const(ConstVal::Null));
4491            *value = Expr::Cast {
4492                ty: TypeRef::J(tt.clone()),
4493                e: Box::new(Expr::Cond {
4494                    c: Box::new(taken),
4495                    t: Box::new(Expr::Const(ConstVal::Int(1))),
4496                    f: Box::new(Expr::Const(ConstVal::Int(0))),
4497                }),
4498            };
4499        } else if numlike(tt) && numlike(&vt_val) {
4500            wrap(tt, value);
4501        }
4502    }
4503    walk_mut_deep(body, &mut |st| match st {
4504        Stmt::ExprStmt(Expr::Assign {
4505            target,
4506            op: AssignOp::Plain,
4507            value,
4508        }) => {
4509            let tt = match &**target {
4510                Expr::Local { var, .. } if (*var as usize) < vt.vars.len() => {
4511                    vt.var(*var).ty.erased()
4512                }
4513                Expr::Field { ty, .. } => ty.erased(),
4514                // Array store: the element type is the target type
4515                // (`sFormatStr[i] = v7` into char[] — "从int转换到char
4516                // 可能会有损失", weibo TimeUtils).
4517                Expr::ArrayIndex { array, .. } => match array.type_ref().erased() {
4518                    JavaType::Array(inner) => *inner,
4519                    _ => return,
4520                },
4521                _ => return,
4522            };
4523            coerce(&tt, value, vt);
4524        }
4525        Stmt::LocalDef {
4526            var,
4527            init: Some(value),
4528            ..
4529        } => {
4530            if (*var as usize) >= vt.vars.len() {
4531                return;
4532            }
4533            let tt = vt.var(*var).ty.erased();
4534            coerce(&tt, value, vt);
4535        }
4536        Stmt::Return(Some(e)) => {
4537            if !numlike(ret_ty) || !numlike(&val_ty(e, vt)) {
4538                return;
4539            }
4540            wrap(ret_ty, e);
4541        }
4542        _ => {}
4543    });
4544}
4545
4546/// Shadowed-ancestor field witness (the upcast sibling of
4547/// fix_field_owner_downcasts).
4548///
4549/// Java resolves a field access on the RECEIVER'S STATIC TYPE downward:
4550/// when the dex writes an ancestor's field (`iput … Fragment.A02:I`)
4551/// but a class between the receiver's static type and the declaring
4552/// ancestor declares a field with the SAME rendered name (`ImageView
4553/// A02`), the plain `this.A02` silently binds to the subclass field —
4554/// `int无法转换为ImageView` when the types differ, and a wrong-field
4555/// read/write when they don't (WhatsApp ActivitySheetFragment writes
4556/// the layout resid into the ancestor int field; its own A02 is an
4557/// ImageView — 1,216 assignment-shape sites in the WA tree). jadx
4558/// renders `super.A02`; the IR has no super-field form, so witness
4559/// with the always-legal upcast `((Fragment) this).A02` — field lookup
4560/// on a cast expression is static, landing on the ancestor at any
4561/// chain depth. The receiver may still be the typed this-Local at this
4562/// pipeline point (the This rewrite comes later), so the compile-time
4563/// type comes from the owner expr / var table, mirroring
4564/// fix_field_owner_downcasts' owner_internal. Fires ONLY on a real
4565/// same-display shadow; unshadowed ancestor refs inherit through
4566/// `this.X` and must not churn. Same-raw-name lookups only: a shadow
4567/// whose raw name differs but display collides is beyond the pool
4568/// lookup keyed by name.
4569pub fn fix_shadowed_super_fields(
4570    body: &mut Stmt,
4571    vt: &VarTable,
4572    pool: &DexPool,
4573    class_name: &str,
4574) {
4575    /// Rendered name of `(cls, raw field name)`: the registry display
4576    /// when the class declares the field, else the raw name. None when
4577    /// the class is not pool-materialized or has no such field.
4578    fn display(pool: &DexPool, cls: &str, name: &str) -> Option<String> {
4579        let pc = pool.get(cls)?;
4580        let f = pc
4581            .static_fields
4582            .iter()
4583            .chain(pc.instance_fields.iter())
4584            .find(|f| f.name.as_str() == name)?;
4585        Some(
4586            jdc_core::rename::field_display(cls, name, &f.desc)
4587                .map(|d| d.to_string())
4588                .unwrap_or_else(|| name.to_string()),
4589        )
4590    }
4591    fn owner_ct(
4592        o: &Option<Box<Expr>>,
4593        vt: &VarTable,
4594        class_name: &str,
4595    ) -> Option<std::sync::Arc<str>> {
4596        match o.as_deref() {
4597            None | Some(Expr::This) => Some(std::sync::Arc::from(class_name)),
4598            Some(Expr::Local { var, .. }) => match vt.var(*var).ty.erased() {
4599                JavaType::Object(n) => Some(n),
4600                _ => None,
4601            },
4602            Some(other) => match other.type_ref().erased() {
4603                JavaType::Object(n) => Some(n),
4604                _ => None,
4605            },
4606        }
4607    }
4608    walk_stmt_exprs(body, &mut |e| {
4609        deep_rewrite(e, &mut |x| {
4610            let Expr::Field {
4611                owner,
4612                cls,
4613                name,
4614                is_static: false,
4615                ..
4616            } = x
4617            else {
4618                return;
4619            };
4620            let Some(ot) = owner_ct(owner, vt, class_name) else {
4621                return;
4622            };
4623            if cls.as_ref() == ot.as_ref() {
4624                return;
4625            }
4626            let Some(ref_display) = display(pool, cls, name) else {
4627                return;
4628            };
4629            // Walk from the receiver's compile-time type up to (not
4630            // including) the declaring ancestor: any same-display
4631            // declaration on the way shadows the ref.
4632            let mut cur = ot.to_string();
4633            let mut shadowed = false;
4634            let mut found = false;
4635            for _ in 0..64 {
4636                if display(pool, &cur, name).as_deref() == Some(ref_display.as_str()) {
4637                    shadowed = true;
4638                }
4639                let Some(pc) = pool.get(&cur) else {
4640                    return;
4641                };
4642                let Some(sup) = pc.super_name.clone() else {
4643                    return;
4644                };
4645                if sup.as_str() == cls.as_ref() {
4646                    found = true;
4647                    break;
4648                }
4649                cur = sup;
4650            }
4651            if !found || !shadowed {
4652                return;
4653            }
4654            let ty = TypeRef::J(JavaType::Object(cls.clone()));
4655            match owner {
4656                Some(o) => {
4657                    if let Expr::Cast { ty: ct, .. } = &mut **o {
4658                        *ct = ty;
4659                    } else {
4660                        let taken = std::mem::replace(&mut **o, Expr::This);
4661                        **o = Expr::Cast {
4662                            ty,
4663                            e: Box::new(taken),
4664                        };
4665                    }
4666                }
4667                None => {
4668                    *owner = Some(Box::new(Expr::Cast {
4669                        ty,
4670                        e: Box::new(Expr::This),
4671                    }))
4672                }
4673            }
4674        });
4675    });
4676}
4677
4678/// Subclass-field downcast witness + assign-cast narrowing.
4679///
4680/// R8 pushes fields DOWN into leaf classes (weibo: coroutine `label`
4681/// lives on each `Foo$bar$1` continuation, NOT on the rendered
4682/// BaseContinuationImpl/ContinuationImpl supers) while a check-cast in
4683/// the same flow types the receiver as the SUPERCLASS. The dex field
4684/// ref names the leaf (`$1->label` — verifier-legal through the
4685/// instance-of branch type), but Java resolves `((ContinuationImpl) c)
4686/// .label` against ContinuationImpl — "找不到符号 变量 label" (weibo
4687/// ×420). When the field's dex owner is a strict subtype of the
4688/// receiver's static type and the receiver's hierarchy does NOT
4689/// declare the name, re-target the receiver cast to the owner (the
4690/// runtime object IS the owner type — the verifier proved it).
4691///
4692/// Second rule: a `(T) e` value assigned into an S-typed target where
4693/// S <: T strictly ("ContinuationImpl无法转换为$reportWhenComplete$1")
4694/// narrows the cast to S — same verifier guarantee, and an assignment
4695/// position has no overload resolution to perturb.
4696pub fn fix_field_owner_downcasts(body: &mut Stmt, vt: &VarTable, pool: &DexPool) {
4697    fn owner_internal(o: &Expr, vt: &VarTable) -> Option<std::sync::Arc<str>> {
4698        let t = match o {
4699            Expr::Local { var, .. } => vt.var(*var).ty.erased(),
4700            other => other.type_ref().erased(),
4701        };
4702        match t {
4703            JavaType::Object(n) => Some(n),
4704            _ => None,
4705        }
4706    }
4707    fn declares_up(pool: &DexPool, class: &str, name: &str) -> bool {
4708        let mut cur = Some(class.to_string());
4709        let mut hops = 0;
4710        while let Some(c) = cur {
4711            hops += 1;
4712            if hops > 64 {
4713                return false;
4714            }
4715            let Some(pc) = pool.get(&c) else { return false };
4716            if pc
4717                .instance_fields
4718                .iter()
4719                .chain(pc.static_fields.iter())
4720                .any(|f| f.name.as_str() == name)
4721            {
4722                return true;
4723            }
4724            cur = pc.super_name.clone();
4725        }
4726        false
4727    }
4728    walk_stmt_exprs(body, &mut |e| {
4729        deep_rewrite(e, &mut |x| {
4730            if let Expr::Field {
4731                owner: Some(o),
4732                cls,
4733                name,
4734                is_static: false,
4735                ..
4736            } = x
4737            {
4738                let Some(ot) = owner_internal(o, vt) else {
4739                    return;
4740                };
4741                if ot.as_ref() == cls.as_ref() || !pool.is_subtype(cls, &ot) {
4742                    return;
4743                }
4744                if declares_up(pool, &ot, name) || !declares_up(pool, cls, name) {
4745                    return;
4746                }
4747                let ty = TypeRef::J(JavaType::Object(cls.clone()));
4748                if let Expr::Cast { ty: ct, .. } = &mut **o {
4749                    *ct = ty;
4750                } else {
4751                    let taken = std::mem::replace(&mut **o, Expr::This);
4752                    **o = Expr::Cast { ty, e: Box::new(taken) };
4753                }
4754            }
4755            // Rule 2: `(T) e` into a strict-subtype target narrows to S.
4756            let (tgt_ty, value) = match x {
4757                Expr::Assign { target, op: AssignOp::Plain, value } => {
4758                    let t = match &**target {
4759                        Expr::Local { var, .. } => vt.var(*var).ty.erased(),
4760                        Expr::Field { ty, .. } => ty.erased(),
4761                        _ => return,
4762                    };
4763                    (t, value)
4764                }
4765                _ => return,
4766            };
4767            let JavaType::Object(sn) = tgt_ty else { return };
4768            if let Expr::Cast { ty, .. } = &mut **value {
4769                if let TypeRef::J(JavaType::Object(tn)) = ty {
4770                    if tn.as_ref() != sn.as_ref() && pool.is_subtype(&sn, tn) {
4771                        *ty = TypeRef::J(JavaType::Object(sn.clone()));
4772                    }
4773                }
4774            }
4775        });
4776    });
4777    // LocalDef inits: same narrowing against the declared var type.
4778    walk_mut_deep(body, &mut |st| {
4779        if let Stmt::LocalDef { var, init: Some(value), .. } = st {
4780            let tt = if (*var as usize) < vt.vars.len() {
4781                vt.vars[*var as usize].ty.erased()
4782            } else {
4783                return;
4784            };
4785            let JavaType::Object(sn) = tt else { return };
4786            if let Expr::Cast { ty, .. } = value {
4787                if let TypeRef::J(JavaType::Object(tn)) = ty {
4788                    if tn.as_ref() != sn.as_ref() && pool.is_subtype(&sn, tn) {
4789                        *ty = TypeRef::J(JavaType::Object(sn.clone()));
4790                    }
4791                }
4792            }
4793        }
4794    });
4795}
4796
4797/// `==`/`!=` between UNRELATED class types is a compile error in Java
4798/// ("不可比较的类型: f0和a") while dex if-eqObj happily compares any two
4799/// references (weixin fn1/w1 ×298 files). One side takes an `(Object)`
4800/// witness cast — legal against anything. Interfaces and subtype-related
4801/// pairs are already comparable and stay untouched; java/lang/Object and
4802/// arrays-of it likewise.
4803pub fn fix_incomparable_equality(body: &mut Stmt, vt: &VarTable, pool: &crate::DexPool) {
4804    fn side_ty(e: &Expr, vt: &VarTable) -> JavaType {
4805        match e {
4806            Expr::Local { var, .. } if (*var as usize) < vt.vars.len() => {
4807                vt.var(*var).ty.erased()
4808            }
4809            other => other.type_ref().erased(),
4810        }
4811    }
4812    let incomparable = |a: &JavaType, b: &JavaType| -> bool {
4813        use JavaType::{Array, Object};
4814        match (a, b) {
4815            (Object(x), Object(y)) => {
4816                if x == y || x.as_ref() == "java/lang/Object" || y.as_ref() == "java/lang/Object"
4817                {
4818                    return false;
4819                }
4820                let xi = pool.get(x).map(|c| c.is_interface()).unwrap_or(true);
4821                let yi = pool.get(y).map(|c| c.is_interface()).unwrap_or(true);
4822                // Unknown classes (framework, not in pool): a cast is
4823                // harmless but usually unnecessary — treat as comparable
4824                // to stay conservative.
4825                if xi || yi {
4826                    return false;
4827                }
4828                !pool.is_subtype(x, y) && !pool.is_subtype(y, x)
4829            }
4830            (Array(_), Object(y)) | (Object(y), Array(_)) => {
4831                y.as_ref() != "java/lang/Object"
4832                    && y.as_ref() != "java/lang/Cloneable"
4833                    && y.as_ref() != "java/io/Serializable"
4834                    && !pool.get(y).map(|c| c.is_interface()).unwrap_or(false)
4835            }
4836            _ => false,
4837        }
4838    };
4839    let is_ref = |t: &JavaType| matches!(t, JavaType::Object(_) | JavaType::Array(_));
4840    walk_stmt_exprs(body, &mut |e| {
4841        deep_rewrite(e, &mut |x| {
4842            if let Expr::Bin {
4843                op: BinOp::Eq | BinOp::Ne,
4844                l,
4845                r,
4846                ..
4847            } = x
4848            {
4849                let lt = side_ty(l, vt);
4850                let rt = side_ty(r, vt);
4851                // `obj != 0` → `obj != null` with FINAL types: the
4852                // lift-time null_side_rewrite ran before infer_types
4853                // (its obj table saw the pre-inference int), leaving
4854                // `sQLiteClosable != 0` — "二元运算符 '!=' 的操作数
4855                // 类型错误" ×700 weixin.
4856                if is_ref(&lt) && matches!(&**r, Expr::Const(ConstVal::Int(0))) {
4857                    **r = Expr::Const(ConstVal::Null);
4858                    return;
4859                }
4860                if is_ref(&rt) && matches!(&**l, Expr::Const(ConstVal::Int(0))) {
4861                    **l = Expr::Const(ConstVal::Null);
4862                    return;
4863                }
4864                if incomparable(&lt, &rt) {
4865                    let taken = std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
4866                    **l = Expr::Cast {
4867                        ty: TypeRef::J(JavaType::Object("java/lang/Object".into())),
4868                        e: taken,
4869                    };
4870                }
4871            }
4872        });
4873    });
4874}
4875
4876/// Primitive bridges at CALL/CTOR argument positions. Dex slots are
4877/// untyped category-1 integers: the verifier happily passes a Z-slot to
4878/// a `(B)` formal (weibo's Meituan-Robust hotpatch boilerplate boxes
4879/// boolean params via `new Byte(zreg)` — 4.2k+ `对于Byte(boolean),
4880/// 找不到合适的构造器`), and obfuscated/Kotlin code passes 0/1 slots to
4881/// `(Z)` formals (`int无法转换为boolean` at call sites). Java source
4882/// cannot express either — bridge at the IR level:
4883///   Boolean actual → numeric formal:  `(byte) (b ? 1 : 0)`
4884///   numeric actual → Boolean formal:  `x != 0`
4885/// The target method is DESCRIPTOR-bound (bytecode method_ref), so a
4886/// cast/compare to the exact formal type cannot flip overload
4887/// resolution the way r57's inferred object casts did — the rendered
4888/// call selects the same signature the dex named.
4889pub fn fix_primitive_arg_bridges(body: &mut Stmt, vt: &VarTable, pool: &crate::DexPool) {
4890    fn val_ty(e: &Expr, vt: &VarTable) -> JavaType {
4891        match e {
4892            Expr::Local { var, .. } => vt.var(*var).ty.erased(),
4893            other => other.type_ref().erased(),
4894        }
4895    }
4896    #[allow(clippy::type_complexity)]
4897    let is_bool = |t: &JavaType| matches!(t, JavaType::Boolean);
4898    let is_num = |t: &JavaType| {
4899        matches!(
4900            t,
4901            JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
4902                | JavaType::Long | JavaType::Float | JavaType::Double
4903        )
4904    };
4905    // BORROWED-arg form: returns the bridged replacement without
4906    // consuming the original (a `mem::replace` + `Option` dance dropped
4907    // the argument on the None path — every Boolean→Boolean arg became
4908    // a literal `null`: 61k 引用不明确 in one battery run).
4909    fn bridge(actual: &JavaType, formal: &JavaType, arg: &Expr) -> Option<Expr> {
4910        // A dex const carries the CALLEE's BITS: `const v, #0x41000000`
4911        // feeding a float formal IS 8.0f. Rendering the raw int literal
4912        // (a) mis-selects overloads — a competing `a(Context,int):void`
4913        // beats the int→float-widened `a(Context,float):int` in javac's
4914        // phase order ("不兼容的类型: void无法转换为int", weibo ju.a
4915        // ×1,259) — and (b) corrupts the VALUE (widening turns the bits
4916        // into 1.09e9f). The descriptor formal is dex ground truth:
4917        // reinterpret. Symmetric for the float-const-into-int-formal
4918        // direction (caller's bits, callee's reading).
4919        match (arg, formal) {
4920            (Expr::Const(ConstVal::Int(b)), JavaType::Float) => {
4921                return Some(Expr::Const(ConstVal::Float(f32::from_bits(*b as u32))));
4922            }
4923            (Expr::Const(ConstVal::Long(b)), JavaType::Double) => {
4924                return Some(Expr::Const(ConstVal::Double(f64::from_bits(*b as u64))));
4925            }
4926            (Expr::Const(ConstVal::Float(f)), JavaType::Int) => {
4927                return Some(Expr::Const(ConstVal::Int(f.to_bits() as i32)));
4928            }
4929            (Expr::Const(ConstVal::Double(d)), JavaType::Long) => {
4930                return Some(Expr::Const(ConstVal::Long(d.to_bits() as i64)));
4931            }
4932            _ => {}
4933        }
4934        if matches!(actual, JavaType::Boolean)
4935            && matches!(
4936                formal,
4937                JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
4938                    | JavaType::Long | JavaType::Float | JavaType::Double
4939            )
4940        {
4941            Some(Expr::Cast {
4942                ty: TypeRef::J(formal.clone()),
4943                e: Box::new(Expr::Cond {
4944                    c: Box::new(arg.clone()),
4945                    t: Box::new(Expr::Const(ConstVal::Int(1))),
4946                    f: Box::new(Expr::Const(ConstVal::Int(0))),
4947                }),
4948            })
4949        } else if matches!(formal, JavaType::Boolean)
4950            && matches!(
4951                actual,
4952                JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
4953                    | JavaType::Long | JavaType::Float | JavaType::Double
4954            )
4955        {
4956            Some(Expr::Bin {
4957                op: BinOp::Ne,
4958                l: Box::new(arg.clone()),
4959                r: Box::new(Expr::Const(ConstVal::Int(0))),
4960                ty: Some(TypeRef::J(JavaType::Boolean)),
4961            })
4962        } else if matches!(
4963            actual,
4964            JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
4965                | JavaType::Long | JavaType::Float | JavaType::Double
4966        ) && matches!(
4967            formal,
4968            JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
4969                | JavaType::Long | JavaType::Float | JavaType::Double
4970        ) && !implicit_widening(actual, formal)
4971        {
4972            // Numeric NARROWING into a formal (int arg → char formal):
4973            // javac rejects the lossy conversion, or — worse when the
4974            // class has no wider overload — silently resolves a
4975            // DIFFERENT overload than the dex descriptor names
4976            // (SpannableStringBuilder.append(int) doesn't exist;
4977            // `append(char)` is "从int转换到char可能会有损失", weibo ×76).
4978            // The descriptor formal is the dex ground truth; casting to
4979            // it pins the overload the bytecode actually invoked.
4980            Some(Expr::Cast {
4981                ty: TypeRef::J(formal.clone()),
4982                e: Box::new(arg.clone()),
4983            })
4984        } else {
4985            None
4986        }
4987    }
4988    /// A bare `null` argument against a SPECIFIC reference formal takes
4989    /// the descriptor cast: `this(context, null)` matching both
4990    /// Builder(Context,Notification) and Builder(Context,String) is
4991    /// "对Builder的引用不明确" (weibo NotificationCompat; protobuf
4992    /// `new FieldSet.Builder(null)` ×54). The descriptor names the
4993    /// exact invoked ctor, so the cast is dex ground truth — the
4994    /// descriptor-exact doctrine, not a guessed overload shift. Bare
4995    /// null stays for Object formals (casting those would add noise
4996    /// without resolving anything javac could not already pick).
4997    /// Framework classes javac resolves through android.jar even when
4998    /// the dex pool lacks them — the embedded API database answers
4999    /// precisely (the hand-written prefix list it replaced missed
5000    /// everything outside the guessed roots and over-trusted within
5001    /// them; removed-API families like org.apache.http are in the DB
5002    /// too, matching what a legacy-jar classpath resolves).
5003    fn fw_resolvable(n: &str) -> bool {
5004        crate::fwdb::exists(n)
5005    }
5006    fn null_arg_cast(a: &mut Expr, f: &JavaType, pool: &DexPool) -> bool {
5007        if !matches!(&*a, Expr::Const(ConstVal::Null)) {
5008            return false;
5009        }
5010        if let JavaType::Object(n) = f {
5011            // Cast only to RESOLVABLE types: a phantom formal (R8 kept
5012            // the ref, dropped the class — androidx.window.sidecar)
5013            // would turn a compiling bare `null` into a fresh
5014            // cannot-find. Pool classes render into the output;
5015            // java/javax/android/dalvik resolve through android.jar.
5016            let resolvable = pool.get(n).is_some() || fw_resolvable(n);
5017            if n.as_ref() != "java/lang/Object" && resolvable {
5018                *a = Expr::Cast {
5019                    ty: TypeRef::J(f.clone()),
5020                    e: Box::new(Expr::Const(ConstVal::Null)),
5021                };
5022                return true;
5023            }
5024        } else if let JavaType::Array(_) = f {
5025            // Descriptor-exact ARRAY cast: the dex named this exact
5026            // overload; a bare null beside sibling overloads is javac-
5027            // ambiguous (`a(String,b2)` + `a(String,String[])` both match
5028            // — weibo org/a/c/b, 对a的引用不明确 family). Same phantom
5029            // gate on the element class: casting to a phantom-element
5030            // array would trade the ambiguity for a cannot-find.
5031            let mut e: &JavaType = f;
5032            while let JavaType::Array(inner) = e {
5033                e = &**inner;
5034            }
5035            let resolvable = match e {
5036                JavaType::Object(n) => pool.get(n).is_some() || fw_resolvable(n),
5037                _ => true, // primitive-element array
5038            };
5039            if resolvable {
5040                *a = Expr::Cast {
5041                    ty: TypeRef::J(f.clone()),
5042                    e: Box::new(Expr::Const(ConstVal::Null)),
5043                };
5044                return true;
5045            }
5046        }
5047        // Bare null deliberately stays (Object formal / phantom type):
5048        // the caller's ambiguity pin may still act on it.
5049        false
5050    }
5051    /// JLS 5.1.2 widening primitive conversion (char joins the chain
5052    /// only at int; byte/short do NOT widen to char).
5053    fn implicit_widening(a: &JavaType, f: &JavaType) -> bool {
5054        use JavaType::*;
5055        match (a, f) {
5056            (Byte, Short) | (Byte, Int) | (Byte, Long) | (Byte, Float) | (Byte, Double) => true,
5057            (Short, Int) | (Short, Long) | (Short, Float) | (Short, Double) => true,
5058            (Char, Int) | (Char, Long) | (Char, Float) | (Char, Double) => true,
5059            (Int, Long) | (Int, Float) | (Int, Double) => true,
5060            (Long, Float) | (Long, Double) => true,
5061            (Float, Double) => true,
5062            (x, y) => x == y,
5063        }
5064    }
5065    // Does the owner class declare ANOTHER method with the same name
5066    // and arity but a different signature? Then javac's most-specific
5067    // overload resolution can steal the call from the dex descriptor's
5068    // target (weibo 对a的引用不明确 ×391: `c.a(null)` against
5069    // a(ExecuteResult)/a(Exception)/a(Object) — the descriptor names
5070    // a(Object), but bare null leaves the two specific overloads
5071    // ambiguously most-specific). Framework owners are not in the
5072    // pool — conservatively report no competition.
5073    let overload_competes = |cls: &str, name: &str, args: &[JavaType]| -> bool {
5074        if let Some(c) = pool.get(cls) {
5075            return c.all_methods().any(|m| {
5076                &*m.name == name
5077                    // Bridge/synthetic siblings are compiler artifacts
5078                    // resolving to the SAME target — counting them as
5079                    // competitors cast enum compareTo args to the erased
5080                    // formal the rendered specialization no longer
5081                    // accepts (Enum无法转换为LogLevel ×2,079 reqable).
5082                    && m.access & (crate::access::ACC_BRIDGE | crate::access::ACC_SYNTHETIC) == 0
5083                    && m.parsed_desc()
5084                        .is_some_and(|d| d.args.len() == args.len() && d.args != args)
5085            });
5086        }
5087        // Framework owner: NOT enumerable for pin purposes — the
5088        // full-DB enumeration experiment (step-5) measured +532 battery
5089        // with ZERO ambiguous gain: a subtype arg against a concrete
5090        // erased formal fires the cast even when the call was never
5091        // ambiguous, and bounded-generic formals (List<T> -> List,
5092        // Property<T,Float> erasures) make the cast break inference or
5093        // miss the rendered specialization. Framework subtype pins stay
5094        // on the measured allowlist below.
5095        let _ = args;
5096        false
5097    };
5098    // Ctors: formal types via the pool (unanimous across same-arity
5099    // overloads, else skip — ambiguity must not guess).
5100    let ctor_formals = |cls: &str, n: usize| -> Option<Vec<JavaType>> {
5101        let c = pool.get(cls)?;
5102        let mut out: Option<Vec<JavaType>> = None;
5103        for m in c.all_methods() {
5104            if &*m.name != "<init>" {
5105                continue;
5106            }
5107            let Some(d) = m.parsed_desc() else { continue };
5108            if d.args.len() != n {
5109                continue;
5110            }
5111            match &out {
5112                None => out = Some(d.args.clone()),
5113                Some(prev) if *prev != d.args => return None,
5114                Some(_) => {}
5115            }
5116        }
5117        out
5118    };
5119    walk_stmt_exprs(body, &mut |e| {
5120        deep_rewrite(e, &mut |x| match x {
5121            Expr::Method {
5122                cls,
5123                name,
5124                desc,
5125                args,
5126                is_static,
5127                ..
5128            } => {
5129                if args.len() != desc.args.len() {
5130                    return;
5131                }
5132                let _ = is_static;
5133                let mut competes: Option<bool> = None;
5134                for (a, f) in args.iter_mut().zip(desc.args.iter()) {
5135                    if null_arg_cast(a, f, pool) {
5136                        continue;
5137                    }
5138                    if matches!(a, Expr::Cast { .. }) {
5139                        continue;
5140                    }
5141                    // Descriptor-exact ambiguity pin: a bare null
5142                    // against an Object formal (null_arg_cast
5143                    // deliberately leaves those bare — safe only
5144                    // WITHOUT competing specific overloads) or a
5145                    // strict-subtype arg can match several declared
5146                    // overloads; casting to the descriptor formal
5147                    // leaves exactly the dex-invoked method
5148                    // applicable (JLS 15.12.2 — the descriptor is
5149                    // ground truth, so the cast pins resolution
5150                    // instead of shifting it).
5151                    let subtype_arg = match (&val_ty(a, vt), f) {
5152                        (JavaType::Object(an), JavaType::Object(fn_)) => {
5153                            // java/lang/Object is the root: ANY reference
5154                            // arg is a strict subtype of it — pool.is_subtype
5155                            // cannot see framework chains (String -> Object),
5156                            // so the Object formal gets the direct rule.
5157                            (an != fn_ && pool.is_subtype(an, fn_))
5158                                || (fn_.as_ref() == "java/lang/Object"
5159                                    && an.as_ref() != "java/lang/Object")
5160                        }
5161                        (JavaType::Array(_), JavaType::Object(fn_))
5162                            if fn_.as_ref() == "java/lang/Object" =>
5163                        {
5164                            true
5165                        }
5166                        _ => false,
5167                    };
5168                    // Bare null against java/lang/Object itself: the
5169                    // descriptor method exists (dex resolved it), so
5170                    // `(Object) null` is always legal and excludes every
5171                    // more-specific sibling overload from applicability.
5172                    // Pool owners take the competes gate (emitted
5173                    // declarations are raw, so the cast cannot disturb
5174                    // inference). FRAMEWORK owners only via allowlist of
5175                    // the classic non-generic null-ambiguous JDK methods
5176                    // (StringBuilder.append ×66 lark): a blanket cast
5177                    // breaks GENERIC framework methods whose descriptor
5178                    // formal is an ERASED type variable —
5179                    // `ofFloat((Object) null, View.ALPHA, v)` killed the
5180                    // T=View inference bare null provided (weibo +4).
5181                    let null_obj =
5182                        matches!(a, Expr::Const(ConstVal::Null)) && matches!(f, JavaType::Object(_));
5183                    let framework_owner = pool.get(cls.as_ref()).is_none();
5184                    let fw_null_ambiguous = framework_owner
5185                        && matches!(
5186                            (cls.as_ref(), name.as_ref()),
5187                            ("java/lang/StringBuilder", "append")
5188                                | ("java/lang/StringBuffer", "append")
5189                                | ("java/io/PrintStream", "println")
5190                                | ("java/io/PrintStream", "print")
5191                                | ("java/io/PrintWriter", "println")
5192                                | ("java/io/PrintWriter", "print")
5193                                | ("java/lang/String", "valueOf")
5194                        );
5195                    // Framework owners with famously overload-heavy
5196                    // methods take the subtype pin too: the arg's
5197                    // rendered type implements SEVERAL formal types
5198                    // (weibo putExtra ×41: floatMsgData is both
5199                    // Parcelable and Serializable; schedule/submit:
5200                    // Runnable-and-Callable workers) and the competes
5201                    // gate cannot enumerate framework overloads.
5202                    // Formal == java/lang/Object stays excluded — that
5203                    // is an erased type variable and the cast would
5204                    // kill generic inference (ofFloat lesson).
5205                    let fw_subtype_pinnable = framework_owner
5206                        && subtype_arg
5207                        && !matches!(f, JavaType::Object(fn_) if fn_.as_ref() == "java/lang/Object")
5208                        && matches!(
5209                            (cls.as_ref(), name.as_ref()),
5210                            ("android/content/Intent", "putExtra")
5211                                | ("java/util/concurrent/ScheduledExecutorService", "schedule")
5212                                | ("java/util/concurrent/ExecutorService", "submit")
5213                                | ("java/util/concurrent/ExecutorService", "invokeAll")
5214                        );
5215                    if fw_subtype_pinnable
5216                        || (subtype_arg
5217                            && *competes.get_or_insert_with(|| {
5218                                overload_competes(cls, name, &desc.args)
5219                            }))
5220                        || (null_obj
5221                            && (fw_null_ambiguous
5222                                || *competes.get_or_insert_with(|| {
5223                                    overload_competes(cls, name, &desc.args)
5224                                })))
5225                    {
5226                        let old =
5227                            std::mem::replace(a, Expr::Const(ConstVal::Null));
5228                        *a = Expr::Cast {
5229                            ty: TypeRef::J(f.clone()),
5230                            e: Box::new(old),
5231                        };
5232                        continue;
5233                    }
5234                    if !is_bool(&val_ty(a, vt)) && !is_num(&val_ty(a, vt)) {
5235                        continue;
5236                    }
5237                    if !is_bool(f) && !is_num(f) {
5238                        continue;
5239                    }
5240                    if let Some(b) = bridge(&val_ty(a, vt), f, a) {
5241                        *a = b;
5242                    }
5243                }
5244            }
5245            Expr::New { cls, args, arg_tys, .. } => {
5246                let formals_owned = if arg_tys.len() == args.len() && !arg_tys.is_empty() {
5247                    Some(arg_tys.clone())
5248                } else {
5249                    ctor_formals(cls, args.len())
5250                };
5251                if let Some(formals) = formals_owned {
5252                    for (a, f) in args.iter_mut().zip(formals.iter()) {
5253                        if null_arg_cast(a, f, pool) {
5254                            continue;
5255                        }
5256                        if matches!(a, Expr::Cast { .. }) {
5257                            continue;
5258                        }
5259                        if !is_bool(f) && !is_num(f) {
5260                            continue;
5261                        }
5262                        if let Some(b) = bridge(&val_ty(a, vt), f, a) {
5263                            *a = b;
5264                        }
5265                    }
5266                }
5267            }
5268            _ => {}
5269        });
5270    });
5271}
5272
5273/// `b ^ 1` / `1 ^ b` with a BOOLEAN operand is the bytecode's boolean
5274/// negation (`!b`) — weibo kotlin-stdlib `return v2 ^ 1;` ("二元运算符
5275/// '^' 的操作数类型错误"), and `x ^ true` shapes. Java only types `^`
5276/// for same-category operands.
5277/// Rename locals whose name equals a ROOT PACKAGE first segment: a
5278/// package-qualified render (`v2.n.a` — the FQN fallback a twin-shadowed
5279/// same-package ref must take) binds its first identifier lexically, and
5280/// a local `v2` in scope captures it (weixin v2/i: 找不到符号 变量 n on a
5281/// legit `v2.n.a(this, 0)`). Method-scope renames carry no registry
5282/// surface — every use renders through this same vt — unlike class/field
5283/// renames, whose reference coverage blew up five times before.
5284pub fn deshadow_locals(vt: &mut VarTable, pool: &crate::DexPool, class_name: &str, body: &Stmt) {
5285    let segs = pool.root_pkg_segs();
5286    // Import-layer simples: the file renders `import p.h;` + bare `h.e`
5287    // for obscured refs — a local `h` captures that simple name (locals
5288    // beat imports in expression position; 无法取消引用int family).
5289    let obscured = crate::classdec::obscured_simples_snapshot();
5290    // Same-package static-ref simples: a static member ref renders its
5291    // class qualifier as the SIMPLE name (same package — no import/FQN),
5292    // and a local with that name captures it (`a2.changeQuickRedirect`
5293    // bound to a String local `a2` — "找不到符号 变量 changeQuickRedirect
5294    // 位置: 类型为String的变量 a2", weibo robust-field refs ×317). Only
5295    // STATIC refs qualify (an instance ref's qualifier IS the local).
5296    let self_pkg = class_name.rfind('/').map(|i| &class_name[..i]).unwrap_or("");
5297    let mut same_pkg_simples: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
5298    visit_all_exprs(body, &mut |x| {
5299        let cls = match x {
5300            Expr::Field { cls, is_static: true, .. } => Some(cls.as_ref()),
5301            Expr::Method { cls, is_static: true, .. } => Some(cls.as_ref()),
5302            _ => None,
5303        };
5304        if let Some(c) = cls {
5305            let pkg = c.rfind('/').map(|i| &c[..i]).unwrap_or("");
5306            if pkg == self_pkg {
5307                let simple = &c[c.rfind('/').map(|i| i + 1).unwrap_or(0)..];
5308                let head = simple.split('$').next().unwrap_or(simple);
5309                if !head.is_empty() {
5310                    same_pkg_simples.insert(head.to_string());
5311                }
5312            }
5313        }
5314    });
5315    let hit = |name: &str| {
5316        segs.contains(name) || obscured.contains(name) || same_pkg_simples.contains(name)
5317    };
5318    if (segs.is_empty() && obscured.is_empty() && same_pkg_simples.is_empty())
5319        || !vt.vars.iter().any(|v| hit(&v.name))
5320    {
5321        return;
5322    }
5323    let mut taken: jdc_core::FxHashSet<String> =
5324        vt.vars.iter().map(|v| v.name.clone()).collect();
5325    for v in vt.vars.iter_mut() {
5326        if !hit(&v.name) {
5327            continue;
5328        }
5329        let mut k = 0u32;
5330        let cand = loop {
5331            k += 1;
5332            let c = if k == 1 {
5333                format!("{}x", v.name)
5334            } else {
5335                format!("{}x{}", v.name, k)
5336            };
5337            if !taken.contains(&c) {
5338                break c;
5339            }
5340        };
5341        taken.insert(cand.clone());
5342        v.name = cand;
5343    }
5344}
5345
5346/// Receiver rescue: a Field/Method receiver whose final vt type is
5347/// PRIMITIVE can never hold the member (structurer tail-copy regions
5348/// can map the receiver register onto a boolean/int generation —
5349/// wcdb HandleOperation's `v10_g2.finalizeStatement()` where the
5350/// statement var is right there; weixin deref-int/bool families,
5351/// ~2.5k lines). The member descriptor's owner class is authoritative:
5352/// when EXACTLY ONE var in the method has that type (or a subtype),
5353/// swap the receiver to it. The pre-rescue output is a guaranteed
5354/// compile error AND runtime NPE, so the unambiguous swap cannot make
5355/// it worse; ambiguous sites stay untouched.
5356/// Anonymous/inner-class outer reads: the dex reads `this$0` off the
5357/// instance register, which copy materialization can route through a
5358/// local (`v41 = this; ... v41.this$0` — weibo HorseRaceDetector$1,
5359/// 2,326-line 找不到符号 变量 this$0 family). When the field's
5360/// declaring class IS the class being decompiled, the only instance in
5361/// scope is `this` — strip the owner so it renders the bare declared
5362/// field.
5363/// Fold single-assigned `this`-copy locals in ctors back to `this`.
5364/// R8 materializes the receiver with `move-object/from16 v1, p0` and
5365/// runs every iput through the copy; the copy renders as a local and a
5366/// final-field store through it is "无法为 final 变量 g 分配值" (Java
5367/// allows blank-final writes only via `this`/simple name — hf/n2 ×29,
5368/// weixin final-var residual). Transitively: `b = a; c = b` chains
5369/// collapse. Single-assignment gate: a var ever written from another
5370/// value is not an alias.
5371pub fn fix_ctor_this_aliases(body: &mut Stmt, vt: &VarTable) {
5372    let Some(this_var) = vt
5373        .vars
5374        .iter()
5375        .find(|v| v.is_param && v.name == "this")
5376        .map(|v| v.id)
5377    else {
5378        return;
5379    };
5380    let mut analysis = VarAnalysis {
5381        assigns: Vec::new(),
5382        reads: Vec::new(),
5383        values: Vec::new(),
5384    };
5385    analyze_vars(body, &mut analysis);
5386    // All-sources rule: a local is a this-alias only when EVERY value
5387    // assignment traces to `this_var` through pure local copies. The
5388    // single-assign constraint this replaces missed R8's multi-branch
5389    // `this` spills (uuyc androidx.camera x_2: `v160 = v150` on two
5390    // paths, both this, chain this→v131→v140→v169→v174→v183→v150→v160),
5391    // so `v160.d = executor` never became `this.d = executor` and javac
5392    // rejected the final-field init (无法为 final 变量 d/i/j 分配值 ×103).
5393    // A single non-this source (field read, `new`, param, a literal)
5394    // disqualifies the local, so a marked alias is provably always this
5395    // and rewriting every read of it to `this` is sound.
5396    enum Src {
5397        This,
5398        Loc(u32),
5399        Other,
5400    }
5401    let mut sources: jdc_core::FxHashMap<u32, Vec<Src>> = jdc_core::FxHashMap::default();
5402    walk_all(body, &mut |st| {
5403        let (tgt, val): (u32, &Expr) = match st {
5404            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => match &**target {
5405                Expr::Local { var, .. } => (*var, &**value),
5406                _ => return,
5407            },
5408            Stmt::LocalDef { var, init: Some(value), .. } => (*var, value),
5409            _ => return,
5410        };
5411        let s = match val {
5412            Expr::This => Src::This,
5413            Expr::Local { var: src, .. } if *src == this_var => Src::This,
5414            Expr::Local { var: src, .. } => Src::Loc(*src),
5415            _ => Src::Other,
5416        };
5417        sources.entry(tgt).or_default().push(s);
5418    });
5419    let n = vt
5420        .vars
5421        .len()
5422        .max(analysis.assigns.len())
5423        .max(sources.keys().map(|k| *k as usize + 1).max().unwrap_or(0));
5424    let mut alias = vec![false; n];
5425    loop {
5426        let mut changed = false;
5427        for (v, srcs) in &sources {
5428            let vi = *v as usize;
5429            if vi >= n || alias[vi] || srcs.is_empty() {
5430                continue;
5431            }
5432            let ok = srcs.iter().all(|s| match s {
5433                Src::This => true,
5434                Src::Loc(w) => (*w as usize) < n && alias[*w as usize],
5435                Src::Other => false,
5436            });
5437            if ok {
5438                alias[vi] = true;
5439                changed = true;
5440            }
5441        }
5442        if !changed {
5443            break;
5444        }
5445    }
5446    if !alias.iter().any(|&b| b) {
5447        return;
5448    }
5449    walk_stmt_exprs(body, &mut |e| {
5450        deep_rewrite_reads(e, &mut |x| {
5451            if let Expr::Local { var, .. } = x {
5452                if (*var as usize) < n && alias[*var as usize] {
5453                    *x = Expr::This;
5454                }
5455            }
5456        });
5457    });
5458}
5459
5460pub fn fix_this0_owners(body: &mut Stmt, class_name: &str) {
5461    walk_stmt_exprs(body, &mut |e| {
5462        deep_rewrite(e, &mut |x| {
5463            if let Expr::Field { owner: Some(_), cls, name, .. } = x {
5464                if name.as_ref() == "this$0" && cls.as_ref() == class_name {
5465                    if let Expr::Field { owner, .. } = x {
5466                        *owner = None;
5467                    }
5468                }
5469            }
5470        });
5471    });
5472}
5473
5474pub fn rescue_primitive_receivers(body: &mut Stmt, vt: &VarTable, pool: &crate::DexPool) {
5475    fn prim(t: &JavaType) -> bool {
5476        match t {
5477            JavaType::Int
5478            | JavaType::Long
5479            | JavaType::Short
5480            | JavaType::Byte
5481            | JavaType::Char
5482            | JavaType::Float
5483            | JavaType::Double
5484            | JavaType::Boolean => true,
5485            // Boxed JDK finals carry no app members either (`bool19.d`
5486            // where bool19 came out java/lang/Boolean — the coroutine
5487            // Result-register conflation).
5488            JavaType::Object(n) => matches!(
5489                n.as_ref(),
5490                "java/lang/Boolean"
5491                    | "java/lang/String"
5492                    | "java/lang/Integer"
5493                    | "java/lang/Long"
5494                    | "java/lang/Short"
5495                    | "java/lang/Byte"
5496                    | "java/lang/Character"
5497                    | "java/lang/Float"
5498                    | "java/lang/Double"
5499            ),
5500            _ => false,
5501        }
5502    }
5503    walk_stmt_exprs(body, &mut |e| {
5504        deep_rewrite(e, &mut |x| {
5505            // Phase 1 (read-only): primitive-typed Local receiver?
5506            let (recv_var, recv_ty) = {
5507                let o: &Expr = match &*x {
5508                    Expr::Method { owner: Some(o), .. } => o,
5509                    Expr::Field { owner: Some(o), is_static: false, .. } => o,
5510                    _ => return,
5511                };
5512                let Expr::Local { var, ty } = o else {
5513                    return;
5514                };
5515                let vt_ty = if (*var as usize) < vt.vars.len() {
5516                    vt.var(*var).ty.erased()
5517                } else {
5518                    ty.erased()
5519                };
5520                if !prim(&vt_ty) {
5521                    return;
5522                }
5523                (*var, vt_ty)
5524            };
5525            let cls: std::sync::Arc<str> = match &*x {
5526                Expr::Method { cls, .. } | Expr::Field { cls, .. } => cls.clone(),
5527                _ => return,
5528            };
5529            // Type-consistent call: the receiver's own type IS the
5530            // member owner (or a subtype) — `str.hashCode()` on a
5531            // String param needs no rescue. Without this the boxed-JDK
5532            // prim() arm fired on every String member call and the
5533            // max-id fallback below hijacked the reads to the highest
5534            // String-typed var in the method — often a case-local
5535            // (lark LynxUIBaseInput str11: 37 reads rewritten to a
5536            // switch-case-scoped def, 找不到符号 ×37; x0/c$a18 str4).
5537            if let JavaType::Object(n) = &recv_ty {
5538                if n.as_ref() == cls.as_ref() || pool.is_subtype(n.as_ref(), cls.as_ref()) {
5539                    return;
5540                }
5541            }
5542            // Phase 2: vars of the owner type (exact or subtype).
5543            let mut cands: Vec<u32> = Vec::new();
5544            for v in &vt.vars {
5545                if let JavaType::Object(n) = v.ty.erased() {
5546                    if n.as_ref() == cls.as_ref()
5547                        || pool.is_subtype(n.as_ref(), cls.as_ref())
5548                    {
5549                        cands.push(v.id);
5550                    }
5551                }
5552            }
5553            let id = if cands.len() == 1 {
5554                cands[0]
5555            } else if cands.len() > 1 {
5556                // Multiple owner-typed vars: prefer the same-SLOT one
5557                // (register lineage), latest generation wins — the
5558                // receiver position is scope-insensitive (any method-
5559                // scope var renders, the decl hoist covers it).
5560                let slot = if (recv_var as usize) < vt.vars.len() {
5561                    Some(vt.var(recv_var).slot)
5562                } else {
5563                    None
5564                };
5565                let mut hits: Vec<u32> = match slot {
5566                    Some(sl) => cands
5567                        .iter()
5568                        .copied()
5569                        .filter(|&c| {
5570                            c != recv_var
5571                                && (c as usize) < vt.vars.len()
5572                                && vt.var(c).slot == sl
5573                        })
5574                        .collect(),
5575                    None => Vec::new(),
5576                };
5577                if hits.is_empty() {
5578                    // No same-slot twin: fall back to the latest
5579                    // owner-typed generation (max id).
5580                    hits = cands
5581                        .iter()
5582                        .copied()
5583                        .filter(|&c| c != recv_var)
5584                        .collect();
5585                }
5586                hits.sort_unstable();
5587                hits.dedup();
5588                if hits.is_empty() {
5589                    return;
5590                }
5591                *hits.last().unwrap()
5592            } else {
5593                // Lineage disambiguation by SLOT: register reuse makes
5594                // the Boolean/primitive twin and the object version of
5595                // one dex register share VarInfo.slot (ry0/h2's
5596                // `bool19.d` — the v1-typed twin rides the same
5597                // register). Multiple slot matches stay untouched
5598                // (deterministic conservatism).
5599                if cands.is_empty() {
5600                    return;
5601                }
5602                let slot = if (recv_var as usize) < vt.vars.len() {
5603                    vt.var(recv_var).slot
5604                } else {
5605                    return;
5606                };
5607                let mut hits: Vec<u32> = cands
5608                    .into_iter()
5609                    .filter(|&c| {
5610                        c != recv_var
5611                            && (c as usize) < vt.vars.len()
5612                            && vt.var(c).slot == slot
5613                    })
5614                    .collect();
5615                hits.sort_unstable();
5616                hits.dedup();
5617                if hits.len() != 1 {
5618                    return;
5619                }
5620                hits[0]
5621            };
5622            // Phase 3: swap.
5623            let o: &mut Box<Expr> = match x {
5624                Expr::Method { owner: Some(o), .. } => o,
5625                Expr::Field { owner: Some(o), is_static: false, .. } => o,
5626                _ => return,
5627            };
5628            if let Expr::Local { var, ty } = &mut **o {
5629                if *var == recv_var {
5630                    *var = id;
5631                    *ty = vt.vars[id as usize].ty.clone();
5632                }
5633            }
5634        });
5635    });
5636
5637}
5638
5639/// Scope-safe arg/return swaps for mistyped generations — runs AFTER
5640/// ensure_declared hoists every declaration to the method top, so the
5641/// top_ids visibility precondition holds by construction (the r134
5642/// falsification: swapping in a block-scoped var broke lexically).
5643/// Same-slot + type-match + uniqueness gates as the receiver rescue.
5644pub fn rescue_arg_return_swaps(
5645    body: &mut Stmt,
5646    vt: &VarTable,
5647    pool: &crate::DexPool,
5648    ret_ty: &JavaType,
5649) {
5650    // SCOPE-SAFE arg/return swap: a primitive-typed actual against a
5651    // specific-reference formal (or a reference-returning `return`) is
5652    // a mistyped generation — the r134 falsification showed the swap
5653    // target MUST be lexically visible at the use site, so candidates
5654    // are restricted to params + TOP-LEVEL declared locals (a hoisted
5655    // decl is visible to javac name resolution everywhere in the
5656    // method) AND same-slot (same dex register lineage) AND unique.
5657    let mut top_ids: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
5658    for v in &vt.vars {
5659        if v.is_param {
5660            top_ids.insert(v.id);
5661        }
5662    }
5663    if let Stmt::Block(stmts) = body {
5664        for st in stmts.iter() {
5665            if let Stmt::LocalDef { var, .. } = st {
5666                top_ids.insert(*var);
5667            }
5668        }
5669    }
5670    let ret_wants_ref = match ret_ty {
5671        JavaType::Object(n) => n.as_ref() != "java/lang/Object",
5672        JavaType::Array(_) => true,
5673        _ => false,
5674    };
5675    let try_swap = |a: &mut Expr, want: &JavaType| {
5676        let Expr::Local { var, ty } = &mut *a else {
5677            return;
5678        };
5679        if (*var as usize) >= vt.vars.len() {
5680            return;
5681        }
5682        let avt = vt.var(*var).ty.erased();
5683        let avt_ref = matches!(avt, JavaType::Object(_) | JavaType::Array(_));
5684        let want_arr = matches!(want, JavaType::Array(_));
5685        // Only swap a primitive-typed actual (an Object actual gets the
5686        // descriptor-exact cast instead — never both).
5687        if avt_ref && !(avt == JavaType::Object("java/lang/Object".into())) {
5688            return;
5689        }
5690        if avt_ref && !matches!(want, JavaType::Object(_)) {
5691            return;
5692        }
5693        let slot = vt.var(*var).slot;
5694        let mut hits: Vec<u32> = Vec::new();
5695        for v in &vt.vars {
5696            if v.slot != slot || !top_ids.contains(&v.id) || v.id == *var {
5697                continue;
5698            }
5699            let t = v.ty.erased();
5700            let ok = match (&t, want) {
5701                (JavaType::Object(n), JavaType::Object(w)) => {
5702                    n.as_ref() == w.as_ref()
5703                        || pool.is_subtype(n.as_ref(), w.as_ref())
5704                }
5705                (JavaType::Array(_), JavaType::Array(_)) => want_arr,
5706                _ => false,
5707            };
5708            if ok {
5709                hits.push(v.id);
5710            }
5711        }
5712        hits.sort_unstable();
5713        hits.dedup();
5714        if hits.is_empty() {
5715            return;
5716        }
5717        // Multiple same-slot candidates: the LATEST generation (max id —
5718        // split mints in walk order ≈ program order) is the closest
5719        // approximation of the register's value at the use site. A
5720        // wrong pick still compiles (type-matched by construction);
5721        // leaving the primitive actual never does.
5722        let id = *hits.last().unwrap();
5723        *var = id;
5724        *ty = vt.vars[id as usize].ty.clone();
5725    };
5726    walk_stmt_exprs(body, &mut |e| {
5727        deep_rewrite(e, &mut |x| {
5728            match x {
5729                Expr::Method { desc, args, is_dynamic: false, .. } => {
5730                    for (i, a) in args.iter_mut().enumerate() {
5731                        if let Some(f) = desc.args.get(i) {
5732                            if matches!(f, JavaType::Object(n) if n.as_ref() != "java/lang/Object")
5733                                || matches!(f, JavaType::Array(_))
5734                            {
5735                                try_swap(a, f);
5736                            }
5737                        }
5738                    }
5739                }
5740                Expr::New { arg_tys, args, .. } => {
5741                    for (i, a) in args.iter_mut().enumerate() {
5742                        if let Some(f) = arg_tys.get(i) {
5743                            if matches!(f, JavaType::Object(n) if n.as_ref() != "java/lang/Object")
5744                                || matches!(f, JavaType::Array(_))
5745                            {
5746                                try_swap(a, f);
5747                            }
5748                        }
5749                    }
5750                }
5751                _ => {}
5752            }
5753        });
5754    });
5755    if ret_wants_ref {
5756        walk_mut_deep(body, &mut |st| {
5757            if let Stmt::Return(Some(e)) = st {
5758                try_swap(e, ret_ty);
5759            }
5760        });
5761    }
5762}
5763
5764/// Reference-array initializers take null, not 0: dex fill-array-data
5765/// over a String[]/Object[] slot carries the zero word, and the lifted
5766/// `new String[] {0}` is int无法转换为String (qs0/b family). A Const
5767/// Int(0) element of a reference-typed array init is the null ref.
5768/// `refVar = const 0` (dex `const/4 vN, 0` into a reference register) is
5769/// the NULL sentinel, not an int. By the time this runs infer_types has
5770/// typed the var a reference (its `.close()`/`.length`/method reads
5771/// proved it), so retype the const to Null BEFORE split_generations —
5772/// otherwise the ref/primitive mismatch splits off an int generation and
5773/// orphans the reference reads (u7.l try-with-resources finally
5774/// `v16_g3 = 0; if (v16_g3 != 0) v16_g3.close();` → "无法取消引用int").
5775/// `fix_incomparable_equality` already retypes the `refVar != 0`
5776/// COMPARISON side (but runs after the split); this covers the ASSIGN
5777/// side, which is what triggers the bad split.
5778pub fn fix_ref_null_assigns(vt: &VarTable, body: &mut Stmt) {
5779    let is_ref_var = |var: u32| -> bool {
5780        (var as usize) < vt.vars.len()
5781            && matches!(
5782                vt.vars[var as usize].ty.erased(),
5783                JavaType::Object(_) | JavaType::Array(_)
5784            )
5785    };
5786    walk_mut_deep(body, &mut |st| match st {
5787        Stmt::ExprStmt(Expr::Assign {
5788            target,
5789            value,
5790            op: AssignOp::Plain,
5791            ..
5792        }) => {
5793            if let Expr::Local { var, .. } = &**target {
5794                if is_ref_var(*var) && matches!(&**value, Expr::Const(ConstVal::Int(0))) {
5795                    **value = Expr::Const(ConstVal::Null);
5796                }
5797            }
5798        }
5799        Stmt::LocalDef {
5800            var,
5801            init: Some(e),
5802            ..
5803        } => {
5804            if is_ref_var(*var) && matches!(e, Expr::Const(ConstVal::Int(0))) {
5805                *e = Expr::Const(ConstVal::Null);
5806            }
5807        }
5808        _ => {}
5809    });
5810}
5811
5812pub fn fix_ref_array_null_consts(body: &mut Stmt) {
5813    walk_stmt_exprs(body, &mut |e| {
5814        deep_rewrite(e, &mut |x| {
5815            if let Expr::NewArray { elem, init: Some(list), .. } = x {
5816                if matches!(elem.erased(), JavaType::Object(_) | JavaType::Array(_)) {
5817                    for slot in list.iter_mut() {
5818                        if matches!(&*slot, Expr::Const(ConstVal::Int(0))) {
5819                            *slot = Expr::Const(ConstVal::Null);
5820                        }
5821                    }
5822                }
5823            }
5824        });
5825    });
5826}
5827
5828// ---------------------------------------------------------------------------
5829// Null-sentinel repair: const-0 registers in reference-typed slots
5830// ---------------------------------------------------------------------------
5831
5832/// A dex `const/4 vN, 0` register is ambiguous between int 0 and the
5833/// null reference; the value view types the bare constant as Int.
5834/// Kotlin coroutine state machines lean on this heavily — captured
5835/// object slots are zero-initialized ahead of the label switch, then
5836/// stored into `Object` fields and passed as `Continuation` ctor args
5837/// (weixin y01/s9 q8: `new e6(s94x, v287)` "int无法转换为Continuation";
5838/// uf5/l0 `throw 0;`; wcdb RepairKit `setOnCancelListener(0)`). The
5839/// dex verifier guarantees a const-0 register reaching a reference-
5840/// typed slot holds null, so in those slots the rendering must be the
5841/// null literal.
5842///
5843/// Two rewrites, both gated on the slot's expected type being a
5844/// reference (descriptor formal types, field type, array component,
5845/// cast target, throw, method return):
5846///   1. a literal `0` in a ref slot → `null`. SKIPPED for call-arg
5847///      slots whose formal is exactly `java/lang/Object`: `f(0)` there
5848///      compiles TODAY (boxing, or an int-overload pick like
5849///      `String.valueOf(0)`), and `f(null)` could newly fail on
5850///      overload ambiguity (`valueOf(Object)` vs `valueOf(char[])`) —
5851///      a guaranteed-error slot becomes a compile, a compiling slot
5852///      must not become an error.
5853///   2. a proven null-sentinel LOCAL: an Int-typed var whose every
5854///      write is `= 0` or `= <another sentinel>` and whose every read
5855///      sits in a ref slot (or feeds another sentinel's write — a dead
5856///      chain) → `null` at each read. The same Object-formal skip
5857///      applies at arg slots. A read in ANY int/unknown context
5858///      (binop, cond test, receiver deref, selector, anon-ctor arg,
5859///      lambda capture…) disqualifies the var — register reuse across
5860///      generations keeps its int rendering.
5861///
5862/// Runs after fix_ref_null_assigns (which converts writes of REF-typed
5863/// vars) and before split_generations (sentinel writes must not mint
5864/// int generations). Writes of confirmed sentinels stay as harmless
5865/// dead int stores for drop_dead_locals.
5866pub fn fix_null_sentinels(body: &mut Stmt, vt: &VarTable, ret: &JavaType) {
5867    #[derive(Clone, Copy, PartialEq)]
5868    enum WK {
5869        Zero,
5870        Copy(u32),
5871        Other,
5872    }
5873    fn is_ref_jt(t: &JavaType) -> bool {
5874        matches!(t, JavaType::Object(_) | JavaType::Array(_))
5875    }
5876    fn cls_write(e: &Expr) -> WK {
5877        match e {
5878            Expr::Const(ConstVal::Int(0)) => WK::Zero,
5879            Expr::Local { var, .. } => WK::Copy(*var),
5880            _ => WK::Other,
5881        }
5882    }
5883    fn expr_writes(e: &Expr, writes: &mut jdc_core::FxHashMap<u32, Vec<WK>>) {
5884        visit_exprs(e, &mut |x| match x {
5885            Expr::Assign { target, op, value } => {
5886                if let Expr::Local { var, .. } = &**target {
5887                    let k = if matches!(op, AssignOp::Plain) {
5888                        cls_write(value)
5889                    } else {
5890                        WK::Other
5891                    };
5892                    writes.entry(*var).or_default().push(k);
5893                }
5894            }
5895            Expr::PreIncDec { e: inner, .. } | Expr::PostIncDec { e: inner, .. } => {
5896                if let Expr::Local { var, .. } = &**inner {
5897                    writes.entry(*var).or_default().push(WK::Other);
5898                }
5899            }
5900            _ => {}
5901        });
5902    }
5903    fn collect_writes(
5904        s: &Stmt,
5905        writes: &mut jdc_core::FxHashMap<u32, Vec<WK>>,
5906        has_zero: &mut bool,
5907    ) {
5908        fn note(e: &Expr, has_zero: &mut bool, writes: &mut jdc_core::FxHashMap<u32, Vec<WK>>) {
5909            visit_exprs(e, &mut |x| {
5910                if let Expr::Const(ConstVal::Int(0)) = x {
5911                    *has_zero = true;
5912                }
5913            });
5914            expr_writes(e, writes);
5915        }
5916        match s {
5917            Stmt::Block(v) => {
5918                for x in v {
5919                    collect_writes(x, writes, has_zero);
5920                }
5921            }
5922            Stmt::LocalDef { var, init, .. } => {
5923                if let Some(e) = init {
5924                    if matches!(e, Expr::Const(ConstVal::Int(0))) {
5925                        *has_zero = true;
5926                    }
5927                    writes.entry(*var).or_default().push(cls_write(e));
5928                }
5929            }
5930            Stmt::ExprStmt(e) => note(e, has_zero, writes),
5931            Stmt::Return(e) => {
5932                if let Some(x) = e {
5933                    note(x, has_zero, writes);
5934                }
5935            }
5936            Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
5937                note(e, has_zero, writes);
5938            }
5939            Stmt::TernaryValue { e } => note(e, has_zero, writes),
5940            Stmt::If { cond, then_stmt, else_stmt } => {
5941                note(cond, has_zero, writes);
5942                collect_writes(then_stmt, writes, has_zero);
5943                if let Some(b) = else_stmt {
5944                    collect_writes(b, writes, has_zero);
5945                }
5946            }
5947            Stmt::While { cond, body } | Stmt::DoWhile { body, cond } => {
5948                note(cond, has_zero, writes);
5949                collect_writes(body, writes, has_zero);
5950            }
5951            Stmt::For { init, cond, update, body } => {
5952                for x in init {
5953                    collect_writes(x, writes, has_zero);
5954                }
5955                if let Some(c) = cond {
5956                    note(c, has_zero, writes);
5957                }
5958                for u in update {
5959                    note(u, has_zero, writes);
5960                }
5961                collect_writes(body, writes, has_zero);
5962            }
5963            Stmt::ForEach { var, iterable, body, .. } => {
5964                writes.entry(*var).or_default().push(WK::Other);
5965                note(iterable, has_zero, writes);
5966                collect_writes(body, writes, has_zero);
5967            }
5968            Stmt::Switch { selector, cases, default, .. } => {
5969                note(selector, has_zero, writes);
5970                for c in cases {
5971                    if let Some(g) = &c.guard {
5972                        note(g, has_zero, writes);
5973                    }
5974                    for x in &c.body {
5975                        collect_writes(x, writes, has_zero);
5976                    }
5977                }
5978                if let Some(d) = default {
5979                    collect_writes(d, writes, has_zero);
5980                }
5981            }
5982            Stmt::Try { body, catches, finally } => {
5983                collect_writes(body, writes, has_zero);
5984                for c in catches {
5985                    writes.entry(c.var).or_default().push(WK::Other);
5986                    collect_writes(&c.body, writes, has_zero);
5987                }
5988                if let Some(f) = finally {
5989                    collect_writes(f, writes, has_zero);
5990                }
5991            }
5992            Stmt::TryWithResources { resources, body, catches, finally, .. } => {
5993                for r in resources {
5994                    collect_writes(r, writes, has_zero);
5995                }
5996                collect_writes(body, writes, has_zero);
5997                for c in catches {
5998                    writes.entry(c.var).or_default().push(WK::Other);
5999                    collect_writes(&c.body, writes, has_zero);
6000                }
6001                if let Some(f) = finally {
6002                    collect_writes(f, writes, has_zero);
6003                }
6004            }
6005            Stmt::Assert { cond, msg } => {
6006                note(cond, has_zero, writes);
6007                if let Some(m) = msg {
6008                    note(m, has_zero, writes);
6009                }
6010            }
6011            Stmt::Synchronized { lock, body } => {
6012                note(lock, has_zero, writes);
6013                collect_writes(body, writes, has_zero);
6014            }
6015            Stmt::Labeled { body, .. } => collect_writes(body, writes, has_zero),
6016            _ => {}
6017        }
6018    }
6019
6020    let mut writes: jdc_core::FxHashMap<u32, Vec<WK>> =
6021        jdc_core::FxHashMap::default();
6022    let mut has_zero = false;
6023    collect_writes(body, &mut writes, &mut has_zero);
6024
6025    // Write-proven eligibility fixpoint (copy chains: `v = u`).
6026    let mut eligible: HashSet<u32> = HashSet::default();
6027    let int_typed = |v: u32| {
6028        (v as usize) < vt.vars.len() && vt.vars[v as usize].ty.erased() == JavaType::Int
6029    };
6030    loop {
6031        let mut changed = false;
6032        for (&v, ws) in writes.iter() {
6033            if eligible.contains(&v) || ws.is_empty() || !int_typed(v) {
6034                continue;
6035            }
6036            if ws.iter().all(|k| match k {
6037                WK::Zero => true,
6038                WK::Copy(u) => *u != v && eligible.contains(u),
6039                WK::Other => false,
6040            }) {
6041                eligible.insert(v);
6042                changed = true;
6043            }
6044        }
6045        if !changed {
6046            break;
6047        }
6048    }
6049    if eligible.is_empty() && !has_zero {
6050        return; // nothing this pass can do
6051    }
6052
6053    #[derive(Clone, Copy, PartialEq)]
6054    enum Mode {
6055        Scan,
6056        Rewrite,
6057    }
6058    struct St<'a> {
6059        mode: Mode,
6060        vt: &'a VarTable,
6061        eligible: &'a HashSet<u32>,
6062        ref_read: HashSet<u32>,
6063        dead: HashSet<u32>,
6064        sentinels: HashSet<u32>,
6065        n_lit: usize,
6066        n_var: usize,
6067    }
6068    // A call-arg slot whose formal is exactly java/lang/Object keeps
6069    // its current rendering (see the fn doc — boxing compiles today,
6070    // null could newly ambiguize).
6071    fn arg_ctx(t: &JavaType) -> Option<JavaType> {
6072        match t {
6073            JavaType::Object(n) if n.as_ref() == "java/lang/Object" => None,
6074            t if is_ref_jt(t) => Some(t.clone()),
6075            _ => None,
6076        }
6077    }
6078    fn walk_expr(e: &mut Expr, ctx: Option<JavaType>, st: &mut St) {
6079        walk_expr_c(e, ctx, false, st);
6080    }
6081    /// `cast_arg`: the slot is a call/ctor ARGUMENT — a rewritten null
6082    /// gets an explicit formal-type cast. Bare `f(null)` re-triggers
6083    /// overload ambiguity where `f(0)` used to pick an int overload
6084    /// (`in(null)` "引用不明确", `ContentValues.put(String,null)` vs its
6085    /// 9 overloads); `(String) null` is unambiguous and jadx-shaped.
6086    fn walk_expr_c(e: &mut Expr, ctx: Option<JavaType>, cast_arg: bool, st: &mut St) {
6087        let slot_ref = ctx.as_ref().is_some_and(is_ref_jt);
6088        fn mk_null(ctx: &Option<JavaType>, cast_arg: bool) -> Expr {
6089            match (cast_arg, ctx) {
6090                (true, Some(t)) => Expr::Cast {
6091                    ty: TypeRef::J(t.clone()),
6092                    e: Box::new(Expr::Const(ConstVal::Null)),
6093                },
6094                _ => Expr::Const(ConstVal::Null),
6095            }
6096        }
6097        match e {
6098            Expr::Const(ConstVal::Int(0)) if slot_ref => {
6099                if st.mode == Mode::Rewrite {
6100                    *e = mk_null(&ctx, cast_arg);
6101                    st.n_lit += 1;
6102                }
6103                return;
6104            }
6105            Expr::Local { var, .. } => {
6106                if st.eligible.contains(var) {
6107                    if slot_ref {
6108                        st.ref_read.insert(*var);
6109                    } else {
6110                        st.dead.insert(*var);
6111                    }
6112                }
6113                if st.mode == Mode::Rewrite && slot_ref && st.sentinels.contains(var) {
6114                    *e = mk_null(&ctx, cast_arg);
6115                    st.n_var += 1;
6116                }
6117                return;
6118            }
6119            _ => {}
6120        }
6121        match e {
6122            Expr::Method { owner, desc, args, .. } => {
6123                if let Some(o) = owner {
6124                    walk_expr(o, None, st);
6125                }
6126                for (i, a) in args.iter_mut().enumerate() {
6127                    let c = desc.args.get(i).and_then(arg_ctx);
6128                    walk_expr_c(a, c, true, st);
6129                }
6130            }
6131            Expr::Invokedynamic { desc, args, .. } => {
6132                for (i, a) in args.iter_mut().enumerate() {
6133                    let c = desc.args.get(i).and_then(arg_ctx);
6134                    walk_expr_c(a, c, true, st);
6135                }
6136            }
6137            Expr::New { args, arg_tys, .. } => {
6138                for (i, a) in args.iter_mut().enumerate() {
6139                    let c = arg_tys.get(i).and_then(arg_ctx);
6140                    walk_expr_c(a, c, true, st);
6141                }
6142            }
6143            Expr::AnonNew { args, .. } => {
6144                for a in args.iter_mut() {
6145                    walk_expr(a, None, st);
6146                }
6147            }
6148            Expr::Assign { target, value, .. } => {
6149                // Sentinel-to-sentinel copy: the whole store is dead;
6150                // its RHS is by the write gate a const-0 or a bare
6151                // local, so no nested reads can hide in it.
6152                if let Expr::Local { var: u, .. } = &**target {
6153                    if st.eligible.contains(u) {
6154                        return;
6155                    }
6156                }
6157                let vctx = match &**target {
6158                    Expr::Field { ty, .. } => {
6159                        let t = ty.erased();
6160                        is_ref_jt(&t).then_some(t)
6161                    }
6162                    Expr::ArrayIndex { array, .. } => match array.type_ref().erased() {
6163                        JavaType::Array(inner) if is_ref_jt(&inner) => Some(*inner),
6164                        _ => None,
6165                    },
6166                    _ => None,
6167                };
6168                walk_expr(target, None, st);
6169                walk_expr(value, vctx, st);
6170            }
6171            Expr::Cast { ty, e: inner } => {
6172                let t = ty.erased();
6173                let c = is_ref_jt(&t).then_some(t);
6174                walk_expr(inner, c, st);
6175            }
6176            Expr::InstanceOf { e: inner, .. } => {
6177                walk_expr(inner, Some(JavaType::Object("java/lang/Object".into())), st);
6178            }
6179            Expr::Cond { c, t, f } => {
6180                walk_expr(c, None, st);
6181                let tc = ctx.clone();
6182                walk_expr(t, tc, st);
6183                walk_expr(f, ctx, st);
6184            }
6185            Expr::NewArray { elem, dims, init, .. } => {
6186                for d in dims.iter_mut() {
6187                    walk_expr(d, None, st);
6188                }
6189                if let Some(list) = init {
6190                    let et = elem.erased();
6191                    let c = is_ref_jt(&et).then_some(et);
6192                    for slot in list.iter_mut() {
6193                        walk_expr(slot, c.clone(), st);
6194                    }
6195                }
6196            }
6197            Expr::NewMultiArray { dims, .. } => {
6198                for d in dims.iter_mut() {
6199                    walk_expr(d, None, st);
6200                }
6201            }
6202            Expr::ArrayIndex { array, index } => {
6203                walk_expr(array, None, st);
6204                walk_expr(index, None, st);
6205            }
6206            Expr::Field { owner, .. } => {
6207                if let Some(o) = owner {
6208                    walk_expr(o, None, st);
6209                }
6210            }
6211            Expr::Bin { l, r, .. } => {
6212                walk_expr(l, None, st);
6213                walk_expr(r, None, st);
6214            }
6215            Expr::Un { e: inner, .. } => walk_expr(inner, None, st),
6216            Expr::PreIncDec { e: inner, .. } | Expr::PostIncDec { e: inner, .. } => {
6217                walk_expr(inner, None, st);
6218            }
6219            Expr::StringConcat(parts) => {
6220                for p in parts.iter_mut() {
6221                    if let ConcatPart::Str(x) = p {
6222                        walk_expr(x, None, st);
6223                    }
6224                }
6225            }
6226            Expr::Lambda(l) => {
6227                for c in l.captures.iter_mut() {
6228                    walk_expr(c, None, st);
6229                }
6230            }
6231            _ => {}
6232        }
6233    }
6234    fn walk_stmt(s: &mut Stmt, st: &mut St, ret: &JavaType) {
6235        match s {
6236            Stmt::Block(v) => {
6237                for x in v.iter_mut() {
6238                    walk_stmt(x, st, ret);
6239                }
6240            }
6241            Stmt::ExprStmt(e) => walk_expr(e, None, st),
6242            Stmt::LocalDef { var, init, .. } => {
6243                if let Some(e) = init {
6244                    let ctx = if (*var as usize) < st.vt.vars.len() {
6245                        let t = st.vt.vars[*var as usize].ty.erased();
6246                        is_ref_jt(&t).then_some(t)
6247                    } else {
6248                        None
6249                    };
6250                    walk_expr(e, ctx, st);
6251                }
6252            }
6253            Stmt::Return(e) => {
6254                if let Some(x) = e {
6255                    let c = is_ref_jt(ret).then(|| ret.clone());
6256                    walk_expr(x, c, st);
6257                }
6258            }
6259            Stmt::Throw(e) => {
6260                walk_expr(e, Some(JavaType::Object("java/lang/Throwable".into())), st);
6261            }
6262            Stmt::If { cond, then_stmt, else_stmt } => {
6263                walk_expr(cond, None, st);
6264                walk_stmt(then_stmt, st, ret);
6265                if let Some(b) = else_stmt {
6266                    walk_stmt(b, st, ret);
6267                }
6268            }
6269            Stmt::While { cond, body } => {
6270                walk_expr(cond, None, st);
6271                walk_stmt(body, st, ret);
6272            }
6273            Stmt::DoWhile { body, cond } => {
6274                walk_stmt(body, st, ret);
6275                walk_expr(cond, None, st);
6276            }
6277            Stmt::For { init, cond, update, body } => {
6278                for x in init.iter_mut() {
6279                    walk_stmt(x, st, ret);
6280                }
6281                if let Some(c) = cond {
6282                    walk_expr(c, None, st);
6283                }
6284                for u in update.iter_mut() {
6285                    walk_expr(u, None, st);
6286                }
6287                walk_stmt(body, st, ret);
6288            }
6289            Stmt::ForEach { iterable, body, .. } => {
6290                walk_expr(iterable, None, st);
6291                walk_stmt(body, st, ret);
6292            }
6293            Stmt::Switch { selector, cases, default, .. } => {
6294                walk_expr(selector, None, st);
6295                for c in cases.iter_mut() {
6296                    if let Some(g) = &mut c.guard {
6297                        walk_expr(g, None, st);
6298                    }
6299                    for x in c.body.iter_mut() {
6300                        walk_stmt(x, st, ret);
6301                    }
6302                }
6303                if let Some(d) = default {
6304                    walk_stmt(d, st, ret);
6305                }
6306            }
6307            Stmt::Try { body, catches, finally } => {
6308                walk_stmt(body, st, ret);
6309                for c in catches.iter_mut() {
6310                    walk_stmt(&mut c.body, st, ret);
6311                }
6312                if let Some(f) = finally {
6313                    walk_stmt(f, st, ret);
6314                }
6315            }
6316            Stmt::TryWithResources { resources, body, catches, finally, .. } => {
6317                for r in resources.iter_mut() {
6318                    walk_stmt(r, st, ret);
6319                }
6320                walk_stmt(body, st, ret);
6321                for c in catches.iter_mut() {
6322                    walk_stmt(&mut c.body, st, ret);
6323                }
6324                if let Some(f) = finally {
6325                    walk_stmt(f, st, ret);
6326                }
6327            }
6328            Stmt::Assert { cond, msg } => {
6329                walk_expr(cond, None, st);
6330                if let Some(m) = msg {
6331                    walk_expr(m, None, st);
6332                }
6333            }
6334            Stmt::Synchronized { lock, body } => {
6335                walk_expr(lock, Some(JavaType::Object("java/lang/Object".into())), st);
6336                walk_stmt(body, st, ret);
6337            }
6338            Stmt::TernaryValue { e } => walk_expr(e, None, st),
6339            Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
6340                walk_expr(e, Some(JavaType::Object("java/lang/Object".into())), st);
6341            }
6342            Stmt::Labeled { body, .. } => walk_stmt(body, st, ret),
6343            _ => {}
6344        }
6345    }
6346
6347    let mut st = St {
6348        mode: Mode::Scan,
6349        vt,
6350        eligible: &eligible,
6351        ref_read: HashSet::default(),
6352        dead: HashSet::default(),
6353        sentinels: HashSet::default(),
6354        n_lit: 0,
6355        n_var: 0,
6356    };
6357    if !eligible.is_empty() {
6358        walk_stmt(body, &mut st, ret);
6359        st.sentinels = eligible
6360            .iter()
6361            .copied()
6362            .filter(|v| st.ref_read.contains(v) && !st.dead.contains(v))
6363            .collect();
6364    }
6365    if st.sentinels.is_empty() && !has_zero {
6366        return;
6367    }
6368    st.mode = Mode::Rewrite;
6369    walk_stmt(body, &mut st, ret);
6370    if std::env::var_os("DDC_SENT").is_some() {
6371        eprintln!(
6372            "[sent] elig={} confirmed={} lit={} var={}",
6373            eligible.len(),
6374            st.sentinels.len(),
6375            st.n_lit,
6376            st.n_var
6377        );
6378    }
6379}
6380
6381/// Int-context operand bridge: a BOOLEAN-typed side of an int-kind Bin
6382/// (bitwise, arithmetic, comparison against a numeric) becomes
6383/// `(b ? 1 : 0)` — the dex-level truth (booleans ARE 0/1 ints there;
6384/// register reuse puts a bool-typed var into an int expression:
6385/// `v5x | 4`, `b == 0` — "boolean无法转换为int" / "二元运算符操作数
6386/// 类型错误" families). Runs AFTER fix_bool_xor so boolean-SINK chains
6387/// were already converted to all-boolean form and are skipped here.
6388pub fn fix_int_operand_bridges(body: &mut Stmt, vt: &VarTable) {
6389    fn side_bool(e: &Expr, vt: &VarTable) -> bool {
6390        match e {
6391            Expr::Local { var, .. } => {
6392                matches!(vt.var(*var).ty.erased(), JavaType::Boolean)
6393            }
6394            // An all-boolean bitwise bin IS boolean even when its
6395            // embedded ty is still the frozen Int of the or-int lift
6396            // (booleanize retypes the vt; mixed_rewrite sets bin ty
6397            // only on the nodes it rewrites). The flat lookup called
6398            // `(ci13|ci14)` an int side, so the sibling `v408 != 0`
6399            // got `? 1 : 0`-wrapped while the bool bin stayed bare —
6400            // rendering int | boolean inside the wrap (news compose
6401            // CoreTextFieldKt `((v408!=0 ?1:0) | (ci13|ci14) ?1:0)|v412`
6402            // ×349 residual). Mirrors fix_bool_xor's bty recursion.
6403            Expr::Bin {
6404                op: BinOp::And | BinOp::Or | BinOp::Xor,
6405                l,
6406                r,
6407                ..
6408            } => side_bool(l, vt) && side_bool(r, vt),
6409            other => matches!(other.type_ref().erased(), JavaType::Boolean),
6410        }
6411    }
6412    fn side_int(e: &Expr, vt: &VarTable) -> bool {
6413        // A boolean-composed bin keeps its frozen Int embedded ty —
6414        // without this guard it double-answers as the "int side" of
6415        // its own bridge arm and steers the wrap onto the wrong operand.
6416        if side_bool(e, vt) {
6417            return false;
6418        }
6419        matches!(
6420            match e {
6421                Expr::Local { var, .. } => vt.var(*var).ty.erased(),
6422                other => other.type_ref().erased(),
6423            },
6424            JavaType::Int | JavaType::Short | JavaType::Byte | JavaType::Char | JavaType::Long
6425        )
6426    }
6427    fn wrap(e: Box<Expr>) -> Box<Expr> {
6428        Box::new(Expr::Cond {
6429            c: e,
6430            t: Box::new(Expr::Const(ConstVal::Int(1))),
6431            f: Box::new(Expr::Const(ConstVal::Int(0))),
6432        })
6433    }
6434    walk_stmt_exprs(body, &mut |e| {
6435        deep_rewrite(e, &mut |x| {
6436            // Field / array-store positions: the target's type is
6437            // descriptor-backed (authoritative); a boolean value into
6438            // an int slot bridges `(b ? 1 : 0)` (with a narrowing cast
6439            // for byte/short/char slots), an int value into a boolean
6440            // slot bridges `v != 0` (weixin `this.L[v3x] = max3`,
6441            // `g2x.d = p1x` — 785 field + ~700 array lines).
6442            if let Expr::Assign { target, value, op: AssignOp::Plain } = x {
6443                let tgt_ty: Option<JavaType> = match &**target {
6444                    Expr::Field { ty, .. } => Some(ty.erased()),
6445                    Expr::ArrayIndex { array, .. } => {
6446                        match array.type_ref().erased() {
6447                            JavaType::Array(el) => Some(el.as_ref().clone()),
6448                            _ => None,
6449                        }
6450                    }
6451                    // Local targets: the split/booleanize machinery owns
6452                    // the mixed-kind register cases; the residue that
6453                    // reached render (boolean无法转换为byte/long, ~360
6454                    // lines) gets the same value-side bridge.
6455                    Expr::Local { var, .. } => {
6456                        if (*var as usize) < vt.vars.len() {
6457                            Some(vt.var(*var).ty.erased())
6458                        } else {
6459                            None
6460                        }
6461                    }
6462                    _ => None,
6463                };
6464                if let Some(t) = tgt_ty {
6465                    let tgt_int = matches!(
6466                        t,
6467                        JavaType::Int
6468                            | JavaType::Short
6469                            | JavaType::Byte
6470                            | JavaType::Char
6471                            | JavaType::Long
6472                    );
6473                    if tgt_int && side_bool(value, vt) && !side_int(value, vt) {
6474                        let taken = std::mem::replace(
6475                            value,
6476                            Box::new(Expr::Const(ConstVal::Null)),
6477                        );
6478                        let mut wrapped = wrap(taken);
6479                        if !matches!(t, JavaType::Int | JavaType::Long) {
6480                            wrapped = Box::new(Expr::Cast {
6481                                ty: TypeRef::J(t.clone()),
6482                                e: wrapped,
6483                            });
6484                        }
6485                        *value = wrapped;
6486                    } else if matches!(t, JavaType::Boolean)
6487                        && side_int(value, vt)
6488                        && !side_bool(value, vt)
6489                    {
6490                        let taken = std::mem::replace(
6491                            value,
6492                            Box::new(Expr::Const(ConstVal::Null)),
6493                        );
6494                        **value = Expr::Bin {
6495                            op: BinOp::Ne,
6496                            l: taken,
6497                            r: Box::new(Expr::Const(ConstVal::Int(0))),
6498                            ty: Some(TypeRef::J(JavaType::Boolean)),
6499                        };
6500                    }
6501                }
6502            }
6503            // Explicit casts across the bool/num line are invalid Java
6504            // in BOTH directions: `(byte) this.r` (dex int-to-byte over
6505            // a boolean-rendered field — weixin AccInfo writeByte ×207)
6506            // becomes `(byte)(r ? 1 : 0)`, and `(boolean) x` over an
6507            // int becomes `x != 0`.
6508            if let Expr::Cast { ty, e } = x {
6509                let t = ty.erased();
6510                let t_num = matches!(
6511                    t,
6512                    JavaType::Int
6513                        | JavaType::Short
6514                        | JavaType::Byte
6515                        | JavaType::Char
6516                        | JavaType::Long
6517                        | JavaType::Float
6518                        | JavaType::Double
6519                );
6520                if t_num && side_bool(e, vt) && !side_int(e, vt) {
6521                    let taken = std::mem::replace(e, Box::new(Expr::Const(ConstVal::Null)));
6522                    **e = Expr::Cond {
6523                        c: taken,
6524                        t: Box::new(Expr::Const(ConstVal::Int(1))),
6525                        f: Box::new(Expr::Const(ConstVal::Int(0))),
6526                    };
6527                } else if matches!(t, JavaType::Boolean)
6528                    && side_int(e, vt)
6529                    && !side_bool(e, vt)
6530                {
6531                    let taken = std::mem::replace(e, Box::new(Expr::Const(ConstVal::Null)));
6532                    *x = Expr::Bin {
6533                        op: BinOp::Ne,
6534                        l: taken,
6535                        r: Box::new(Expr::Const(ConstVal::Int(0))),
6536                        ty: Some(TypeRef::J(JavaType::Boolean)),
6537                    };
6538                }
6539            }
6540            // Null-vs-int comparison: dex if-eqz/if-nez is type-agnostic
6541            // and null IS zero — when the local came out int-typed (phi
6542            // confluence residue), rewrite the Null constant to 0
6543            // (`v95_g12 != null` → `v95_g12 != 0`, faithful and legal;
6544            // yq0/g1 546-line 二元运算符 family). BOOLEAN side: the same
6545            // zero-register compare reads as the truth test — `b != null`
6546            // IS `b`, `b == null` IS `!b` (alipay v87_g7_g2 != null,
6547            // 二元运算符 '!=' boolean vs <空值> ×43 / news ×30).
6548            if let Expr::Bin { op, l, r, .. } = x {
6549                if matches!(op, BinOp::Eq | BinOp::Ne) {
6550                    if matches!(&**r, Expr::Const(ConstVal::Null))
6551                        && side_int(l, vt)
6552                        && !side_bool(l, vt)
6553                    {
6554                        **r = Expr::Const(ConstVal::Int(0));
6555                    } else if matches!(&**l, Expr::Const(ConstVal::Null))
6556                        && side_int(r, vt)
6557                        && !side_bool(r, vt)
6558                    {
6559                        **l = Expr::Const(ConstVal::Int(0));
6560                    } else if matches!(&**r, Expr::Const(ConstVal::Null))
6561                        && side_bool(l, vt)
6562                    {
6563                        let taken =
6564                            std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
6565                        *x = if matches!(op, BinOp::Eq) {
6566                            Expr::Un { op: UnOp::Not, e: taken }
6567                        } else {
6568                            *taken
6569                        };
6570                    } else if matches!(&**l, Expr::Const(ConstVal::Null))
6571                        && side_bool(r, vt)
6572                    {
6573                        let taken =
6574                            std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
6575                        *x = if matches!(op, BinOp::Eq) {
6576                            Expr::Un { op: UnOp::Not, e: taken }
6577                        } else {
6578                            *taken
6579                        };
6580                    }
6581                }
6582            }
6583            // A boolean-typed array INDEX is a reused register holding
6584            // an int (`this.L[v3x]` — boolean无法转换为int at the index
6585            // position): bridge it in reads and writes alike.
6586            if let Expr::ArrayIndex { index, .. } = x {
6587                if side_bool(index, vt) && !side_int(index, vt) {
6588                    let taken =
6589                        std::mem::replace(index, Box::new(Expr::Const(ConstVal::Null)));
6590                    *index = wrap(taken);
6591                }
6592            }
6593            // A boolean-typed array DIMENSION is the same reused-register
6594            // case at the allocation site (`new Object[v200]` where v200
6595            // is boolean-typed — weixin AppBrandRuntime, bool→int family):
6596            // dex new-array reads the size register as int and booleans
6597            // ARE 0/1 ints there, so bridge `(b ? 1 : 0)`.
6598            if let Expr::NewArray { dims, .. } = x {
6599                for d in dims.iter_mut() {
6600                    if side_bool(d, vt) && !side_int(d, vt) {
6601                        let taken =
6602                            std::mem::replace(d, Expr::Const(ConstVal::Null));
6603                        *d = *wrap(Box::new(taken));
6604                    }
6605                }
6606            }
6607            if let Expr::Bin { op, l, r, .. } = x {
6608                let int_kind = matches!(
6609                    op,
6610                    BinOp::Or
6611                        | BinOp::And
6612                        | BinOp::Xor
6613                        | BinOp::Add
6614                        | BinOp::Sub
6615                        | BinOp::Mul
6616                        | BinOp::Div
6617                        | BinOp::Rem
6618                        | BinOp::Shl
6619                        | BinOp::Shr
6620                        | BinOp::Ushr
6621                        | BinOp::Eq
6622                        | BinOp::Ne
6623                        | BinOp::Lt
6624                        | BinOp::Ge
6625                        | BinOp::Gt
6626                        | BinOp::Le
6627                );
6628                if !int_kind {
6629                    return;
6630                }
6631                // A Null CONSTANT operand of an int-kind bitwise/arith
6632                // bin is the dex zero register (or-int reads 0 out of a
6633                // register whose value view was null — `b | null`
6634                // renders boolean | <空值>, alipay ×43 / news ×30).
6635                // Rewriting to Int(0) is register-faithful and lets the
6636                // bool-side wraps below take over (`(b ? 1 : 0) | 0`).
6637                // Only when the SIBLING is a bool/int side: a
6638                // reference-typed sibling keeps the null shape (that is
6639                // SSA-merge residue, not a zero-register read), and a
6640                // String sibling keeps `+ null` concatenation ("null" —
6641                // valid and faithful; side_str excludes it via the
6642                // side_bool/side_int guard).
6643                if matches!(
6644                    op,
6645                    BinOp::Or
6646                        | BinOp::And
6647                        | BinOp::Xor
6648                        | BinOp::Add
6649                        | BinOp::Sub
6650                        | BinOp::Mul
6651                        | BinOp::Div
6652                        | BinOp::Rem
6653                        | BinOp::Shl
6654                        | BinOp::Shr
6655                        | BinOp::Ushr
6656                ) {
6657                    if matches!(&**r, Expr::Const(ConstVal::Null))
6658                        && (side_bool(l, vt) || side_int(l, vt))
6659                    {
6660                        **r = Expr::Const(ConstVal::Int(0));
6661                    } else if matches!(&**l, Expr::Const(ConstVal::Null))
6662                        && (side_bool(r, vt) || side_int(r, vt))
6663                    {
6664                        **l = Expr::Const(ConstVal::Int(0));
6665                    }
6666                }
6667                // PURE arithmetic/shift and ORDERING operands never take
6668                // a boolean side in ANY combination (`boolean + boolean`
6669                // is as invalid as `boolean + int` — reused 0/1 registers
6670                // in `p4x + v20 + (v21?1:0)`, uuyc u0/u02; `boolean <
6671                // boolean` likewise) — bridge each bool side on its own.
6672                // String concatenation is exempt: `"s" + b` is valid Java
6673                // and wrapping would print 1/0 instead of true/false.
6674                // Equality stays on the mixed-side rule below (a
6675                // bool == bool compare IS valid).
6676                let arith = matches!(
6677                    op,
6678                    BinOp::Add
6679                        | BinOp::Sub
6680                        | BinOp::Mul
6681                        | BinOp::Div
6682                        | BinOp::Rem
6683                        | BinOp::Shl
6684                        | BinOp::Shr
6685                        | BinOp::Ushr
6686                );
6687                let ordered = matches!(op, BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le);
6688                if arith || ordered {
6689                    let side_str = |e: &Expr| {
6690                        matches!(
6691                            match e {
6692                                Expr::Local { var, .. } => vt.var(*var).ty.erased(),
6693                                other => other.type_ref().erased(),
6694                            },
6695                            JavaType::Object(ref s) if s.as_ref() == "java/lang/String"
6696                        )
6697                    };
6698                    let concat = arith && (side_str(l) || side_str(r));
6699                    if !concat {
6700                        if side_bool(l, vt) {
6701                            let taken =
6702                                std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
6703                            *l = wrap(taken);
6704                        }
6705                        if side_bool(r, vt) {
6706                            let taken =
6707                                std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
6708                            *r = wrap(taken);
6709                        }
6710                    }
6711                    return;
6712                }
6713                // Comparisons only bridge against a NUMERIC constant or
6714                // int-typed side (a bool == bool compare is valid Java).
6715                if side_bool(l, vt) && side_int(r, vt) && !side_bool(r, vt) {
6716                    let taken = std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
6717                    *l = wrap(taken);
6718                } else if side_bool(r, vt) && side_int(l, vt) && !side_bool(l, vt) {
6719                    let taken = std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
6720                    *r = wrap(taken);
6721                }
6722            }
6723        });
6724    });
6725}
6726
6727/// Statement-level idiom restoration (readability parity with
6728/// jadx/DAD, flagged by the androguard comparison): `v = v + 1` →
6729/// `v++`, `v = v op x` → `v op= x`. Local targets only — a compound
6730/// field/array target would change how often the receiver expression
6731/// evaluates. Var must be the LEFT operand (non-commutative ops;
6732/// string-concat order). The ±1 fold requires a numeric vt type
6733/// (`s++` on a String is invalid while `s += 1` is not).
6734/// Boolean VALUE-diamond fold (DAD short_circuit value forms):
6735/// ```text
6736///   v = false; if (c) { v = true; }   →  v = c
6737///   v = true;  if (c) { v = false; }  →  v = !c
6738///   v = false; if (c) { v = E; }      →  v = c && E     (E bool-valued)
6739///   v = true;  if (c) { v = E; }      →  v = !c || E
6740///   v = D; if (c) { v = E1; } else { v = E2; }  →  v = c ? E1 : E2
6741/// ```
6742/// `v` must be Boolean-typed; the arm value bool-valued; the guard may
6743/// not touch `v`; base and arm constants must not coincide (a no-op
6744/// diamond could drop guard side effects). Kotlin default-arg bridge
6745/// ctors compute defaulted booleans in exactly this shape INSIDE the
6746/// branch ahead of the delegation (lark MmCreateAudioRequest's v21x
6747/// chain) — split_branch rejects the control flow as Dirty and the
6748/// this() call stays in the branch ("对this的调用必须是构造器中的第一
6749/// 个语句", lark ×456). Folded to a linear def (interleaved with
6750/// forward_single_use for the `[u = call; v = u]` arm shape) the
6751/// merge_at ternary fold applies. Source-faithful: the original was
6752/// `x?.m() ?: false` — one expression; the inlined call stays under
6753/// the guard arm, preserving evaluation order and short-circuiting.
6754/// General value-diamond fold (fold_bool_value_diamonds for ANY type):
6755/// `if (c) { v = a; } else { v = b; }` → `v = c ? a : b;`, plus the
6756/// split-pair shape `v = b; if (c) { v = a; }` → the same ternary. The
6757/// Kotlin default-arg ctor's mask-guarded per-arg defaults arrive as
6758/// diamonds (DocFreshInfo v10x/v14x); folding gives each carrier a
6759/// SINGLE-assignment def so the chain-aware delegation inline can hoist
6760/// the this() first ("对this的调用必须是构造器中的第一个语句"). Fold
6761/// only when both sides are exactly one plain assignment to the SAME
6762/// local and the value types are ternary-compatible (equal erased or
6763/// both numeric — mixed-category ternaries box/surprise).
6764pub fn fold_value_diamonds(body: &mut Stmt, vt: &VarTable) {
6765    let Stmt::Block(stmts) = body else { return };
6766    fold_diamonds_in(stmts, vt);
6767}
6768
6769fn diamond_types_ok(a: &Expr, b: &Expr) -> bool {
6770    fn num(t: &JavaType) -> bool {
6771        matches!(
6772            t,
6773            JavaType::Byte
6774                | JavaType::Short
6775                | JavaType::Int
6776                | JavaType::Char
6777                | JavaType::Long
6778                | JavaType::Float
6779                | JavaType::Double
6780        )
6781    }
6782    // A null branch is compatible with any REFERENCE other branch: the
6783    // ternary types as lub(T, null) = T. Kotlin default-args ctors
6784    // render null defaults (`if ((mask&2)==0) v=x; else v=null;`) —
6785    // rejecting the pair left the diamond unfolded, the defs
6786    // un-inlineable, and the this(...) delegation unhoistable (weibo
6787    // ctor-not-first robust-guard/second-delegation ×818). A primitive
6788    // sibling stays rejected (`c ? 5 : null` is not Java).
6789    if let Some(other) = if matches!(a, Expr::Const(ConstVal::Null)) {
6790        Some(b)
6791    } else if matches!(b, Expr::Const(ConstVal::Null)) {
6792        Some(a)
6793    } else {
6794        None
6795    } {
6796        return !num(&other.type_ref().erased());
6797    }
6798    let ta = a.type_ref().erased();
6799    let tb = b.type_ref().erased();
6800    ta == tb || (num(&ta) && num(&tb))
6801}
6802
6803/// `(var, target clone, value)` when the stmt is exactly ONE plain
6804/// assign to a local (bare or block-wrapped). The unwrap must be
6805/// RECURSIVE: the Kotlin default-args ctor diamonds arrive as
6806/// `Block([Block([Assign])])` (structurer nesting), and the one-level
6807/// peel missed them — the unfolded `if ((mask&2)==0) v=p; else v=0;`
6808/// prelude then blocked fix_ctor_super_first from hoisting the
6809/// `this(...)` delegation (weibo/lark ctor-not-first ×841 family).
6810fn single_local_assign(st: &Stmt) -> Option<(u32, Expr, Expr)> {
6811    let mut inner = st;
6812    while let Stmt::Block(v) = inner {
6813        if v.len() != 1 {
6814            return None;
6815        }
6816        inner = &v[0];
6817    }
6818    match inner {
6819        Stmt::ExprStmt(Expr::Assign { target, value, op: AssignOp::Plain }) => {
6820            match &**target {
6821                Expr::Local { var, .. } => Some((*var, *target.clone(), *value.clone())),
6822                _ => None,
6823            }
6824        }
6825        _ => None,
6826    }
6827}
6828
6829/// Like `single_local_assign`, but ALSO accepts an arm whose value is
6830/// produced by an arm-local def: `Block([LocalDef vX = init, v = vX])`
6831/// (possibly Block-wrapped) inlines `init` into the returned value when
6832/// `vX` is read exactly once there (single evaluation preserved). The
6833/// Kotlin mask-ctor long defaults render this shape (`else { long u =
6834/// Color.getUnspecified(); v37 = u; }` — compose TextStyle/SpanStyle);
6835/// without the inline the diamond stays unfolded and blocks the whole
6836/// delegation-inlining chain downstream.
6837fn arm_assign_inlined(st: &Stmt) -> Option<(u32, Expr, Expr)> {
6838    if let Some(r) = single_local_assign(st) {
6839        return Some(r);
6840    }
6841    let mut inner = st;
6842    while let Stmt::Block(v) = inner {
6843        if v.len() == 1 {
6844            inner = &v[0];
6845        } else {
6846            break;
6847        }
6848    }
6849    let Stmt::Block(v) = inner else { return None };
6850    if v.len() != 2 {
6851        return None;
6852    }
6853    let Stmt::LocalDef {
6854        var: x,
6855        init: Some(init),
6856        ..
6857    } = &v[0]
6858    else {
6859        return None;
6860    };
6861    let (var, tgt, val) = single_local_assign(&v[1])?;
6862    let mut count = 0usize;
6863    let mut c = val.clone();
6864    deep_rewrite(&mut c, &mut |e| {
6865        if let Expr::Local { var: lv, .. } = e {
6866            if lv == x {
6867                count += 1;
6868            }
6869        }
6870    });
6871    if count != 1 {
6872        return None;
6873    }
6874    let mut out = val.clone();
6875    deep_rewrite(&mut out, &mut |e| {
6876        if let Expr::Local { var: lv, .. } = e {
6877            if lv == x {
6878                *e = init.clone();
6879            }
6880        }
6881    });
6882    Some((var, tgt, out))
6883}
6884
6885fn mk_ternary_assign(tgt: Expr, c: Expr, a: Expr, b: Expr) -> Stmt {
6886    Stmt::ExprStmt(Expr::Assign {
6887        target: Box::new(tgt),
6888        op: AssignOp::Plain,
6889        value: Box::new(Expr::Cond {
6890            c: Box::new(c),
6891            t: Box::new(a),
6892            f: Box::new(b),
6893        }),
6894    })
6895}
6896
6897/// Two var ids that render as the SAME Java local: identical base name
6898/// (generation suffixes only appear on collisions, and same-base
6899/// generations of one dex register share the rendered name) and
6900/// identical erased type (a bool/num split pair must not merge).
6901fn same_render_var(v1: u32, v2: u32, vt: &VarTable) -> bool {
6902    let (a, b) = (vt.vars.get(v1 as usize), vt.vars.get(v2 as usize));
6903    match (a, b) {
6904        (Some(x), Some(y)) => x.name == y.name && x.ty.erased() == y.ty.erased(),
6905        _ => false,
6906    }
6907}
6908
6909/// Structural equality of two statement lists MODULO SSA generations:
6910/// each arm of a diamond writes its OWN generation of the rendered
6911/// locals, so strict `==` on the tails fails even when the rendered
6912/// code is identical. Canonicalize every local id to the first id seen
6913/// for its render key (name + erased type) under a SHARED map, then
6914/// compare structurally.
6915fn canon_stmts_eq(a: &[Stmt], b: &[Stmt], vt: &VarTable) -> bool {
6916    if a.len() != b.len() {
6917        return false;
6918    }
6919    let mut map: std::collections::HashMap<String, u32> = std::collections::HashMap::default();
6920    let canon = |s: &Stmt, map: &mut std::collections::HashMap<String, u32>| -> Stmt {
6921        let mut c = s.clone();
6922        walk_stmt_exprs(&mut c, &mut |e| {
6923            deep_rewrite(e, &mut |x| {
6924                if let Expr::Local { var, .. } = x {
6925                    let key = match vt.vars.get(*var as usize) {
6926                        Some(vi) => format!("{}#{:?}", vi.name, vi.ty.erased()),
6927                        None => format!("v{}", var),
6928                    };
6929                    let id = *map.entry(key).or_insert(*var);
6930                    *x = Expr::Local {
6931                        var: id,
6932                        ty: vt.var(id).ty.clone(),
6933                    };
6934                }
6935            });
6936        });
6937        c
6938    };
6939    a.iter()
6940        .zip(b.iter())
6941        .all(|(x, y)| canon(x, &mut map) == canon(y, &mut map))
6942}
6943
6944fn fold_diamonds_in(stmts: &mut Vec<Stmt>, vt: &VarTable) {
6945    let mut i = 0usize;
6946    while i < stmts.len() {
6947        match &mut stmts[i] {
6948            Stmt::Block(v) => fold_diamonds_in(v, vt),
6949            Stmt::If { then_stmt, else_stmt, .. } => {
6950                if let Stmt::Block(v) = &mut **then_stmt {
6951                    fold_diamonds_in(v, vt);
6952                }
6953                if let Some(e) = else_stmt {
6954                    if let Stmt::Block(v) = &mut **e {
6955                        fold_diamonds_in(v, vt);
6956                    }
6957                }
6958            }
6959            _ => {}
6960        }
6961        // Shape 1: standalone if/else diamond.
6962        let shape1 = if let Stmt::If { cond, then_stmt, else_stmt: Some(el), .. } = &stmts[i] {
6963            match (arm_assign_inlined(then_stmt), arm_assign_inlined(el)) {
6964                (Some((v1, tgt, a)), Some((v2, _, b)))
6965                    if v1 == v2 && diamond_types_ok(&a, &b) =>
6966                {
6967                    Some(mk_ternary_assign(tgt, cond.clone(), a, b))
6968                }
6969                _ => None,
6970            }
6971        } else {
6972            None
6973        };
6974        if let Some(f) = shape1 {
6975            stmts[i] = f;
6976            i += 1;
6977            continue;
6978        }
6979        // Shape 3: identical-delegation diamond — BOTH arms assign the
6980        // same rendered local, then run the same tail headed by a ctor
6981        // delegation (Kotlin mask-ctor last slot: `if ((m&M)==0) { v =
6982        // x; this(.., v, ..); return; } else { v = null; this(.., v,
6983        // ..); return; }` — alipay compose TextStyle/SpanStyle family).
6984        // Fold to `v = c ? x : null;` + the shared tail: the delegation
6985        // becomes unconditional (the hotfix-guard strip downstream needs
6986        // that) and fold_default_arg_bridge can then inline the prelude
6987        // carriers into the args. Tails compare modulo SSA generation
6988        // (each arm reads its own generation of the one rendered local).
6989        // Gates: tail headed by a bare delegation, tails canonically
6990        // identical, the tail writes NEITHER generation, types ok.
6991        let shape3 = if let Stmt::If {
6992            cond,
6993            then_stmt,
6994            else_stmt: Some(el),
6995            ..
6996        } = &stmts[i]
6997        {
6998            let arm = |st: &Stmt| -> Option<((u32, Expr, Expr), Vec<Stmt>)> {
6999                let Stmt::Block(v) = st else { return None };
7000                if v.len() < 2 {
7001                    return None;
7002                }
7003                let first = single_local_assign(&v[0])?;
7004                Some((first, v[1..].to_vec()))
7005            };
7006            match (arm(then_stmt), arm(el)) {
7007                (Some(((v1, tgt1, a), mut rest_t)), Some(((v2, _t2, b), rest_e)))
7008                    if !rest_t.is_empty()
7009                        && is_bare_ctor_call(first_leaf_stmt(&rest_t[0]))
7010                        && stmts_count_writes(&rest_t, v1) == 0
7011                        && stmts_count_writes(&rest_t, v2) == 0
7012                        && diamond_types_ok(&a, &b) =>
7013                {
7014                    if v1 == v2 || same_render_var(v1, v2, vt) {
7015                        // same rendered local: fold the assign, keep the tail
7016                        if canon_stmts_eq(&rest_t, &rest_e, vt) {
7017                            Some((mk_ternary_assign(tgt1, cond.clone(), a, b), rest_t))
7018                        } else {
7019                            None
7020                        }
7021                    } else if stmts_read_var(&rest_t, v1) == 1
7022                        && stmts_read_var(&rest_e, v2) == 1
7023                        && stmts_read_var(&rest_t, v2) == 0
7024                        && stmts_read_var(&rest_e, v1) == 0
7025                    {
7026                        // SPLIT arm-local carriers (compose Span: then
7027                        // reads v23_g3, else reads v23 — different render
7028                        // names, one read each): unify by substituting
7029                        // v2 -> v1 in the else tail and requiring EXACT
7030                        // equality, then inline `c ? A1 : A2` at the
7031                        // single v1 read of the kept tail. No ternary
7032                        // assign needed — both carriers die in the fold.
7033                        let mut unified = rest_e.clone();
7034                        for st in unified.iter_mut() {
7035                            let mut c2 = st.clone();
7036                            walk_stmt_exprs(&mut c2, &mut |e| {
7037                                deep_rewrite(e, &mut |x| {
7038                                    if let Expr::Local { var, .. } = x {
7039                                        if *var == v2 {
7040                                            *x = Expr::Local {
7041                                                var: v1,
7042                                                ty: vt.var(v1).ty.clone(),
7043                                            };
7044                                        }
7045                                    }
7046                                });
7047                            });
7048                            *st = c2;
7049                        }
7050                        if unified != rest_t {
7051                            None
7052                        } else {
7053                            let tern = Expr::Cond {
7054                                c: Box::new(cond.clone()),
7055                                t: Box::new(a.clone()),
7056                                f: Box::new(b.clone()),
7057                            };
7058                            for st in rest_t.iter_mut() {
7059                                let mut c2 = st.clone();
7060                                walk_stmt_exprs(&mut c2, &mut |e| {
7061                                    deep_rewrite(e, &mut |x| {
7062                                        if let Expr::Local { var, .. } = x {
7063                                            if *var == v1 {
7064                                                *x = tern.clone();
7065                                            }
7066                                        }
7067                                    });
7068                                });
7069                                *st = c2;
7070                            }
7071                            // the If is REPLACED by the (rewritten) tail:
7072                            // no ternary assign survives — both carriers
7073                            // die in the fold.
7074                            let first = rest_t.remove(0);
7075                            Some((first, rest_t))
7076                        }
7077                    } else {
7078                        None
7079                    }
7080                }
7081                _ => None,
7082            }
7083        } else {
7084            None
7085        };
7086        if let Some((folded, rest)) = shape3 {
7087            stmts[i] = folded;
7088            stmts.splice(i + 1..i + 1, rest);
7089            i += 1;
7090            continue;
7091        }
7092        // Shape 2: adjacent `v = b;` + `if (c) { v = a; }` (no else).
7093        // The two assigns may target DIFFERENT SSA generations of the
7094        // same rendered local (sequential conditional overwrite — the
7095        // Kotlin mask-ctor `v11x = p3x; if ((mask&4)!=0) v11x = -1L;`
7096        // prelude, lark/weixin branched-defs ctor family): same base
7097        // name + same erased type renders as one Java local, so the
7098        // fold's else arm reads generation 1 and the assign targets
7099        // generation 2 — textually `v = c ? a : v`, exactly the
7100        // observable behavior of the unfolded pair.
7101        if i + 1 < stmts.len() {
7102            // An EMPTY else block counts as no-else: the structurer
7103            // emits Some(Block([])) where the source shape had none,
7104            // and cleanup (which would drop it) runs AFTER the fold —
7105            // the strict None match missed every real-world overwrite
7106            // (zero shape-2 folds on lark mask ctors).
7107            // Shape 2: adjacent `v = b;` + conditional overwrite with
7108            // NO other effect:
7109            //   form A: `if (c) { v = a; }` (else absent/empty)  -> v = c ? a : b
7110            //   form B: `if (c) { } else { v = a; }` (then empty) -> v = c ? b : a
7111            // Form B is the FOLD-time shape of every rendered no-else
7112            // overwrite: invert_empty_thens flips then/else AFTER this
7113            // pass, so the strict `else_stmt: None, overwrite-in-then`
7114            // match never saw a real-world instance (zero shape-2 folds
7115            // on lark mask ctors). The two assigns may target DIFFERENT
7116            // SSA generations of one rendered local (same base name +
7117            // same erased type): the ternary's keep-arm then reads
7118            // generation 1 — textually `v = c ? a : v`, exactly the
7119            // observable behavior of the unfolded pair (Kotlin mask-ctor
7120            // preludes: `v11x = p3x; if ((mask&4)!=0) v11x = -1L;`).
7121            let shape2 = {
7122                let s_i = single_local_assign(&stmts[i]);
7123                if let Stmt::If {
7124                    cond,
7125                    then_stmt,
7126                    else_stmt,
7127                    ..
7128                } = &stmts[i + 1]
7129                {
7130                    let then_a = single_local_assign(then_stmt);
7131                    let else_a = else_stmt.as_deref().and_then(single_local_assign);
7132                    let then_empty =
7133                        matches!(&**then_stmt, Stmt::Block(v) if v.is_empty());
7134                    let else_empty = else_stmt
7135                        .as_deref()
7136                        .map(|e| matches!(e, Stmt::Block(v) if v.is_empty()))
7137                        .unwrap_or(true);
7138                    let try_fold = |over: Option<(u32, Expr, Expr)>,
7139                                    keep_over: bool|
7140                     -> Option<Stmt> {
7141                        let (v2, tgt2, a) = over?;
7142                        let (v1, _t1, b) = s_i.as_ref()?;
7143                        if !(*v1 == v2 || same_render_var(*v1, v2, vt)) {
7144                            return None;
7145                        }
7146                        let keep = if *v1 == v2 {
7147                            b.clone()
7148                        } else {
7149                            Expr::Local {
7150                                var: *v1,
7151                                ty: vt.var(*v1).ty.clone(),
7152                            }
7153                        };
7154                        if !diamond_types_ok(&a, &keep) {
7155                            return None;
7156                        }
7157                        if keep_over {
7158                            Some(mk_ternary_assign(tgt2, cond.clone(), keep, a))
7159                        } else {
7160                            Some(mk_ternary_assign(tgt2, cond.clone(), a, keep))
7161                        }
7162                    };
7163                    if then_a.is_some() && else_empty {
7164                        try_fold(then_a, false)
7165                    } else if else_a.is_some() && then_empty {
7166                        try_fold(else_a, true)
7167                    } else {
7168                        None
7169                    }
7170                } else {
7171                    None
7172                }
7173            };
7174            // Leading-assign variant: the branch STARTS with the
7175            // overwrite and carries a tail (Kotlin mask-ctor last slot:
7176            // `str12 = null; if ((m&32)==0) { str12 = str6; this(..,
7177            // str12); return; }` — news_article CommonData/DTO family).
7178            // Fold the assign into the ternary and leave the tail in the
7179            // branch: the tail's reads see the identical value on both
7180            // paths (the ternary assigns before the branch runs). Gate:
7181            // the tail must not WRITE the var.
7182            let mut lead_fold: Option<(Stmt, Stmt)> = None;
7183            if shape2.is_none() && i + 1 < stmts.len() {
7184                let s_i2 = single_local_assign(&stmts[i]);
7185                let leading = |st: &Stmt| -> Option<((u32, Expr, Expr), Vec<Stmt>)> {
7186                    let Stmt::Block(v) = st else { return None };
7187                    if v.len() < 2 {
7188                        return None;
7189                    }
7190                    let first = single_local_assign(&v[0])?;
7191                    Some((first, v[1..].to_vec()))
7192                };
7193                if let (Some((v1, _t1, b)), Stmt::If { cond, then_stmt, else_stmt, .. }) =
7194                    (&s_i2, &stmts[i + 1])
7195                {
7196                    let else_absent = else_stmt.is_none()
7197                        || else_stmt
7198                            .as_deref()
7199                            .is_some_and(|e| matches!(e, Stmt::Block(v) if v.is_empty()));
7200                    let then_absent = matches!(&**then_stmt, Stmt::Block(v) if v.is_empty());
7201                    let try_lead = |over: Option<((u32, Expr, Expr), Vec<Stmt>)>,
7202                                    keep_over: bool|
7203                     -> Option<(Stmt, Stmt)> {
7204                        let ((v2, tgt2, a), rest) = over?;
7205                        if !(*v1 == v2 || same_render_var(*v1, v2, vt)) {
7206                            return None;
7207                        }
7208                        if stmts_count_writes(&rest, *v1) != 0
7209                            || stmts_count_writes(&rest, v2) != 0
7210                        {
7211                            return None;
7212                        }
7213                        let keep = if *v1 == v2 {
7214                            b.clone()
7215                        } else {
7216                            Expr::Local {
7217                                var: *v1,
7218                                ty: vt.var(*v1).ty.clone(),
7219                            }
7220                        };
7221                        if !diamond_types_ok(&a, &keep) {
7222                            return None;
7223                        }
7224                        let t = if keep_over {
7225                            mk_ternary_assign(tgt2, cond.clone(), keep, a)
7226                        } else {
7227                            mk_ternary_assign(tgt2, cond.clone(), a, keep)
7228                        };
7229                        let mut new_if = stmts[i + 1].clone();
7230                        if let Stmt::If { then_stmt, else_stmt, .. } = &mut new_if {
7231                            if keep_over {
7232                                *else_stmt = Some(Box::new(Stmt::Block(rest)));
7233                            } else {
7234                                **then_stmt = Stmt::Block(rest);
7235                            }
7236                        }
7237                        Some((t, new_if))
7238                    };
7239                    if else_absent {
7240                        lead_fold = try_lead(leading(then_stmt), false);
7241                    } else if then_absent {
7242                        if let Some(e) = else_stmt {
7243                            lead_fold = try_lead(leading(e), true);
7244                        }
7245                    }
7246                }
7247            }
7248            if let Some((folded, new_if)) = lead_fold {
7249                stmts[i] = folded;
7250                stmts[i + 1] = new_if;
7251                continue;
7252            }
7253            if let Some(f) = shape2 {
7254                stmts[i] = f;
7255                stmts.remove(i + 1);
7256                continue; // re-scan the folded position
7257            }
7258        }
7259        i += 1;
7260    }
7261}
7262
7263pub fn fold_bool_value_diamonds(body: &mut Stmt, vt: &VarTable) {
7264    fn is_bool_var(v: u32, vt: &VarTable) -> bool {
7265        (v as usize) < vt.vars.len()
7266            && matches!(vt.vars[v as usize].ty.erased(), JavaType::Boolean)
7267    }
7268    fn bool_const(e: &Expr) -> Option<bool> {
7269        fn cint(e: &Expr) -> Option<i32> {
7270            match e {
7271                Expr::Const(ConstVal::Int(n)) => Some(*n),
7272                _ => None,
7273            }
7274        }
7275        match e {
7276            Expr::Const(ConstVal::Int(0)) => Some(false),
7277            Expr::Const(ConstVal::Int(1)) => Some(true),
7278            // int↔bool bridge wrappings of constants: `0 != 0` is the
7279            // fallback pipeline's rendered `false` by the time the
7280            // ctor-block fold runs.
7281            Expr::Bin { op: op @ (BinOp::Eq | BinOp::Ne), l, r, .. } => {
7282                let (a, b) = (cint(l)?, cint(r)?);
7283                let eq = a == b;
7284                Some(if matches!(op, BinOp::Eq) { eq } else { !eq })
7285            }
7286            _ => None,
7287        }
7288    }
7289    fn reads_var(e: &Expr, v: u32) -> bool {
7290        let mut hit = false;
7291        visit_exprs(e, &mut |x| {
7292            if let Expr::Local { var, .. } = x {
7293                if *var == v {
7294                    hit = true;
7295                }
7296            }
7297        });
7298        hit
7299    }
7300    /// Bool-valued expression for the composite arms (&&, ||, ?:).
7301    /// Bare 0/1 constants are handled by the caller's special cases —
7302    /// inside a composite they would render as invalid `c && 1`.
7303    fn bool_valued(e: &Expr, vt: &VarTable) -> bool {
7304        fn side(e: &Expr, vt: &VarTable) -> bool {
7305            bool_const(e).is_some() || bool_valued(e, vt)
7306        }
7307        match e {
7308            Expr::Local { var, .. } => is_bool_var(*var, vt),
7309            Expr::Method { desc, .. } => desc.ret == JavaType::Boolean,
7310            Expr::InstanceOf { .. } => true,
7311            Expr::Un { op: UnOp::Not, e: x, .. } => bool_valued(x, vt) || bool_const(x).is_some(),
7312            Expr::Bin { op, l, r, .. } => match op {
7313                BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le => true,
7314                // A logical operand may itself be a 0/1 constant
7315                // (booleanize leaves mixed forms).
7316                BinOp::LogAnd | BinOp::LogOr => {
7317                    side(l, vt) && side(r, vt)
7318                }
7319                _ => false,
7320            },
7321            Expr::Cond { t, f, .. } => side(t, vt) && side(f, vt),
7322            _ => false,
7323        }
7324    }
7325    /// A branch that is exactly one plain `v = E` with E bool-valued
7326    /// (or a 0/1 constant) and not reading v — OR the forwarding PAIR
7327    /// `[u = E; v = u]` where u is single-assigned/single-read in the
7328    /// whole method: the fold consumes the pair into `v = E` (the impure
7329    /// `u = src.m()` arm shape forward_single_use cannot inline — its
7330    /// phi-ref gate rejects values reading multi-assigned prelude vars,
7331    /// and inlining here is scope-safe because E moves exactly one
7332    /// block level out with the arm's lone def consumed).
7333    fn lone_assign(
7334        b: &Stmt,
7335        v: u32,
7336        vt: &VarTable,
7337        an: &VarAnalysis,
7338        consumed: &mut HashSet<u32>,
7339    ) -> Option<Expr> {
7340        let l = flat_list(b);
7341        if l.len() == 2 {
7342            let (u, e): (u32, &Expr) = match l[0] {
7343                Stmt::LocalDef { var, init: Some(e), .. } => (*var, e),
7344                Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => {
7345                    match &**target {
7346                        Expr::Local { var, .. } => (*var, &**value),
7347                        _ => return None,
7348                    }
7349                }
7350                _ => return None,
7351            };
7352            if u == v || consumed.contains(&u) {
7353                return None;
7354            }
7355            // Whole-method single-assign/single-read: the fold drops
7356            // u's def with the arm, so ANY other read would dangle.
7357            // (Analysis counts are per-round; folds only ever REMOVE
7358            // reads elsewhere or clone E's — whose variable reads were
7359            // already counted at their def sites — so stale counts
7360            // over-approximate and the ==1 gates stay sound.)
7361            if an.assigns.get(u as usize).copied().unwrap_or(0) != 1
7362                || an.reads.get(u as usize).copied().unwrap_or(0) != 1
7363            {
7364                return None;
7365            }
7366            let (tv, rv) = match l[1] {
7367                Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => {
7368                    match (&**target, &**value) {
7369                        (Expr::Local { var: t, .. }, Expr::Local { var: r, .. }) => (*t, *r),
7370                        _ => return None,
7371                    }
7372                }
7373                Stmt::LocalDef { var, init: Some(Expr::Local { var: r, .. }), .. } => (*var, *r),
7374                _ => return None,
7375            };
7376            if tv != v || rv != u {
7377                return None;
7378            }
7379            if reads_var(e, v) || !(bool_valued(e, vt) || bool_const(e).is_some()) {
7380                return None;
7381            }
7382            consumed.insert(u);
7383            return Some(e.clone());
7384        }
7385        if l.len() != 1 {
7386            return None;
7387        }
7388        let (target, value) = match l[0] {
7389            Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => (target, value),
7390            Stmt::LocalDef { var, init: Some(value), .. } if *var == v => {
7391                if reads_var(value, v) || !(bool_valued(value, vt) || bool_const(value).is_some()) {
7392                    return None;
7393                }
7394                return Some(value.clone());
7395            }
7396            _ => return None,
7397        };
7398        let Expr::Local { var: tv, .. } = &**target else { return None };
7399        if *tv != v || reads_var(value, v) {
7400            return None;
7401        }
7402        if !(bool_valued(value, vt) || bool_const(value).is_some()) {
7403            return None;
7404        }
7405        Some((**value).clone())
7406    }
7407    fn not(e: Expr) -> Expr {
7408        Expr::Un { op: UnOp::Not, e: Box::new(e) }
7409    }
7410    /// Peephole the int↔bool bridge artifacts the fallback pipeline
7411    /// leaves in guards: `!(x != y)` → `x == y`; `(b ? 1 : 0) == 1` →
7412    /// `b`; `!(b ? 1 : 0 == 1)` chains likewise. Purely a readability
7413    /// pass — the unsimplified forms compile.
7414    fn simplify_bool(e: Expr) -> Expr {
7415        match e {
7416            Expr::Un { op: UnOp::Not, e: inner, .. } => match *inner {
7417                // Flip first, THEN re-simplify: `!((u?1:0) != 1)` must
7418                // reach the cond01 arm as `(u?1:0) == 1` → `u` (the
7419                // flip result never re-entered simplification and the
7420                // pair arm saw a Bin where it needed the bare local).
7421                Expr::Bin { op: BinOp::Ne, l, r, ty } => {
7422                    simplify_bool(Expr::Bin { op: BinOp::Eq, l, r, ty })
7423                }
7424                Expr::Bin { op: BinOp::Eq, l, r, ty } => {
7425                    simplify_bool(Expr::Bin { op: BinOp::Ne, l, r, ty })
7426                }
7427                other => Expr::Un { op: UnOp::Not, e: Box::new(simplify_bool(other)) },
7428            },
7429            Expr::Bin { op: op @ (BinOp::Eq | BinOp::Ne), l, r, ty } => {
7430                // (c ? 1 : 0) <op> 1  →  c / !c ; <op> 0 → !c / c
7431                fn cond01(e: &Expr) -> Option<Expr> {
7432                    if let Expr::Cond { c, t, f } = e {
7433                        if matches!(&**t, Expr::Const(ConstVal::Int(1)))
7434                            && matches!(&**f, Expr::Const(ConstVal::Int(0)))
7435                        {
7436                            return Some((**c).clone());
7437                        }
7438                    }
7439                    None
7440                }
7441                let (lc, rc) = (cond01(&l), cond01(&r));
7442                match (lc, rc) {
7443                    (Some(c), None) => match (&*r, op) {
7444                        (Expr::Const(ConstVal::Int(1)), BinOp::Eq) => c,
7445                        (Expr::Const(ConstVal::Int(1)), BinOp::Ne) => not(c),
7446                        (Expr::Const(ConstVal::Int(0)), BinOp::Eq) => not(c),
7447                        (Expr::Const(ConstVal::Int(0)), BinOp::Ne) => c,
7448                        _ => Expr::Bin { op, l: Box::new(c), r, ty },
7449                    },
7450                    (None, Some(c)) => match (&*l, op) {
7451                        (Expr::Const(ConstVal::Int(1)), BinOp::Eq) => c,
7452                        (Expr::Const(ConstVal::Int(1)), BinOp::Ne) => not(c),
7453                        (Expr::Const(ConstVal::Int(0)), BinOp::Eq) => not(c),
7454                        (Expr::Const(ConstVal::Int(0)), BinOp::Ne) => c,
7455                        _ => Expr::Bin { op, l, r: Box::new(c), ty },
7456                    },
7457                    _ => Expr::Bin { op, l, r, ty },
7458                }
7459            }
7460            other => other,
7461        }
7462    }
7463    fn bin(op: BinOp, l: Expr, r: Expr) -> Expr {
7464        Expr::Bin { op, l: Box::new(l), r: Box::new(r), ty: None }
7465    }
7466
7467    for _round in 0..4 {
7468        let mut changed = false;
7469        let mut analysis = VarAnalysis {
7470            assigns: Vec::new(),
7471            reads: Vec::new(),
7472            values: Vec::new(),
7473        };
7474        analyze_vars(body, &mut analysis);
7475        let mut consumed: HashSet<u32> = HashSet::default();
7476        walk_mut_deep(body, &mut |st| {
7477            let Stmt::Block(list) = st else { return };
7478            let mut i = 0usize;
7479            while i + 1 < list.len() {
7480                let folded = try_at(list, i, vt, &is_bool_var, &bool_const, &reads_var, &lone_assign, &analysis, &mut consumed);
7481                match folded {
7482                    Some(expr) => {
7483                        let is_def = matches!(&list[i], Stmt::LocalDef { .. });
7484                        let var = match &list[i] {
7485                            Stmt::LocalDef { var, .. } => *var,
7486                            Stmt::ExprStmt(Expr::Assign { target, .. }) => {
7487                                match &**target { Expr::Local { var, .. } => *var, _ => unreachable!() }
7488                            }
7489                            _ => unreachable!(),
7490                        };
7491                        let (is_final, force_type) = match &list[i] {
7492                            Stmt::LocalDef { is_final, force_type, .. } => (*is_final, *force_type),
7493                            _ => (false, false),
7494                        };
7495                        list[i] = if is_def {
7496                            Stmt::LocalDef { var, init: Some(expr), is_final, force_type }
7497                        } else {
7498                            Stmt::ExprStmt(Expr::Assign {
7499                                target: Box::new(Expr::Local {
7500                                    var,
7501                                    ty: TypeRef::J(JavaType::Boolean),
7502                                }),
7503                                op: AssignOp::Plain,
7504                                value: Box::new(expr),
7505                            })
7506                        };
7507                        list.remove(i + 1);
7508                        changed = true;
7509                        // stay at i: a preceding base may now pair with
7510                        // nothing new, but a FOLLOWING diamond may chain.
7511                    }
7512                    None => i += 1,
7513                }
7514            }
7515        });
7516        if !changed {
7517            break;
7518        }
7519    }
7520
7521    #[allow(clippy::too_many_arguments, clippy::type_complexity)]
7522    fn try_at(
7523        list: &[Stmt],
7524        i: usize,
7525        vt: &VarTable,
7526        is_bool_var: &dyn Fn(u32, &VarTable) -> bool,
7527        bool_const: &dyn Fn(&Expr) -> Option<bool>,
7528        reads_var: &dyn Fn(&Expr, u32) -> bool,
7529        lone_assign: &dyn Fn(&Stmt, u32, &VarTable, &VarAnalysis, &mut HashSet<u32>) -> Option<Expr>,
7530        an: &VarAnalysis,
7531        consumed: &mut HashSet<u32>,
7532    ) -> Option<Expr> {
7533        // s1: `v = <0|1>` (Assign or LocalDef), v Boolean-typed.
7534        let (v, base) = match &list[i] {
7535            Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => {
7536                match &**target {
7537                    Expr::Local { var, .. } => {
7538                        // The value may be a wrapped constant (`0 != 0`
7539                        // — the fallback pipeline's rendered false).
7540                        let b = bool_const(value)?;
7541                        if !is_bool_var(*var, vt) { return None; }
7542                        (*var, b)
7543                    }
7544                    _ => return None,
7545                }
7546            }
7547            Stmt::LocalDef { var, init: Some(e), .. } => {
7548                let b = bool_const(e)?;
7549                if !is_bool_var(*var, vt) { return None; }
7550                (*var, b)
7551            }
7552            _ => return None,
7553        };
7554        let Stmt::If { cond, then_stmt, else_stmt } = &list[i + 1] else {
7555            return None;
7556        };
7557        if reads_var(cond, v) {
7558            return None;
7559        }
7560        let mut touches_v = false;
7561        visit_exprs(cond, &mut |x| {
7562            if let Expr::Assign { target, .. } = x {
7563                if let Expr::Local { var, .. } = &**target {
7564                    if *var == v { touches_v = true; }
7565                }
7566            }
7567        });
7568        if touches_v {
7569            return None;
7570        }
7571        // The structurer emits inverted diamonds (`if (c) {} else { v = E }`,
7572        // empty-then) — normalize by negating the guard.
7573        let (cond_n, then_arm, else_arm): (Expr, &Stmt, Option<&Stmt>) =
7574            if flat_list(then_stmt).is_empty() && else_stmt.is_some() {
7575                (not(cond.clone()), else_stmt.as_ref().unwrap(), None)
7576            } else {
7577                (cond.clone(), then_stmt.as_ref(), else_stmt.as_deref())
7578            };
7579        let e1 = lone_assign(then_arm, v, vt, an, consumed)?;
7580        let e2 = match else_arm {
7581            None => None,
7582            Some(b) => Some(lone_assign(b, v, vt, an, consumed)?),
7583        };
7584        let e1c = bool_const(&e1);
7585        let c = simplify_bool(cond_n);
7586        Some(match e2 {
7587            None => {
7588                if !base {
7589                    match e1c {
7590                        Some(true) => c,
7591                        Some(false) => return None, // no-op diamond: guard effects would drop
7592                        None => bin(BinOp::LogAnd, c, e1),
7593                    }
7594                } else {
7595                    match e1c {
7596                        Some(false) => not(c),
7597                        Some(true) => return None,
7598                        None => bin(BinOp::LogOr, not(c), e1),
7599                    }
7600                }
7601            }
7602            Some(e2) => {
7603                // Full diamond: both arms assign — the base is dead.
7604                if e1 == e2 {
7605                    return None; // both arms equal: folding drops guard effects
7606                }
7607                Expr::Cond { c: Box::new(c), t: Box::new(e1), f: Box::new(e2) }
7608            }
7609        })
7610    }
7611}
7612
7613pub fn idiom_compounds(body: &mut Stmt, vt: &VarTable) {
7614    walk_mut_deep(body, &mut |st| {
7615        let Stmt::ExprStmt(e) = st else { return };
7616        let Expr::Assign { target, op: AssignOp::Plain, value } = &mut *e else {
7617            return;
7618        };
7619        // Target: a local, or a bare field (static / this — no complex
7620        // receiver whose re-evaluation a compound would elide).
7621        enum Tgt {
7622            Loc(u32),
7623            Fld,
7624        }
7625        let tgt = match &**target {
7626            Expr::Local { var, .. } => Tgt::Loc(*var),
7627            Expr::Field { owner, .. }
7628                if owner.is_none()
7629                    || matches!(&**owner.as_ref().unwrap(), Expr::This) =>
7630            {
7631                Tgt::Fld
7632            }
7633            _ => return,
7634        };
7635        let Expr::Bin { op, l, r, .. } = &mut **value else {
7636            return;
7637        };
7638        // The left operand must be the SAME storage as the target.
7639        match (&tgt, &**l) {
7640            (Tgt::Loc(v), Expr::Local { var, .. }) if *var == *v => {}
7641            (Tgt::Fld, Expr::Field { cls: c2, name: n2, .. }) => {
7642                let Expr::Field { cls: c1, name: n1, .. } = &**target else {
7643                    return;
7644                };
7645                if c1 != c2 || n1 != n2 {
7646                    return;
7647                }
7648            }
7649            _ => return,
7650        }
7651        // ±1 constant on a numeric var → post-inc/dec.
7652        let one = match &**r {
7653            Expr::Const(ConstVal::Int(1)) => Some(1i64),
7654            Expr::Const(ConstVal::Int(-1)) => Some(-1),
7655            Expr::Const(ConstVal::Long(1)) => Some(1),
7656            Expr::Const(ConstVal::Long(-1)) => Some(-1),
7657            _ => None,
7658        };
7659        if *op == BinOp::Add || *op == BinOp::Sub {
7660            if let Some(mut d) = one {
7661                if *op == BinOp::Sub {
7662                    d = -d;
7663                }
7664                let tgt_ty = match &tgt {
7665                    Tgt::Loc(v) => vt.var(*v).ty.erased(),
7666                    Tgt::Fld => {
7667                        let Expr::Field { ty, .. } = &**target else {
7668                            return;
7669                        };
7670                        ty.erased()
7671                    }
7672                };
7673                let numeric = matches!(
7674                    tgt_ty,
7675                    JavaType::Int
7676                        | JavaType::Long
7677                        | JavaType::Short
7678                        | JavaType::Byte
7679                        | JavaType::Char
7680                        | JavaType::Float
7681                        | JavaType::Double
7682                );
7683                if numeric {
7684                    let wide = matches!(tgt_ty, JavaType::Long);
7685                    let inner = match &tgt {
7686                        Tgt::Loc(v) => Expr::Local {
7687                            var: *v,
7688                            ty: vt.var(*v).ty.clone(),
7689                        },
7690                        Tgt::Fld => (**target).clone(),
7691                    };
7692                    *e = Expr::PostIncDec {
7693                        e: Box::new(inner),
7694                        delta: d,
7695                        wide,
7696                    };
7697                    return;
7698                }
7699            }
7700        }
7701        let cop = match op {
7702            BinOp::Add => AssignOp::Add,
7703            BinOp::Sub => AssignOp::Sub,
7704            BinOp::Mul => AssignOp::Mul,
7705            BinOp::Div => AssignOp::Div,
7706            BinOp::Rem => AssignOp::Rem,
7707            BinOp::Shl => AssignOp::Shl,
7708            BinOp::Shr => AssignOp::Shr,
7709            BinOp::Ushr => AssignOp::Ushr,
7710            BinOp::And => AssignOp::And,
7711            BinOp::Or => AssignOp::Or,
7712            BinOp::Xor => AssignOp::Xor,
7713            _ => return,
7714        };
7715        let rhs = std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
7716        let new_target = match &tgt {
7717            Tgt::Loc(v) => Box::new(Expr::Local {
7718                var: *v,
7719                ty: vt.var(*v).ty.clone(),
7720            }),
7721            Tgt::Fld => target.clone(),
7722        };
7723        *e = Expr::Assign {
7724            target: new_target,
7725            op: cop,
7726            value: rhs,
7727        };
7728    });
7729}
7730
7731pub fn fix_bool_xor(body: &mut Stmt, vt: &VarTable, ret_bool: bool) {
7732    fn is_one(e: &Expr) -> bool {
7733        matches!(e, Expr::Const(ConstVal::Int(1)))
7734    }
7735    fn bty(e: &Expr, vt: &VarTable) -> bool {
7736        match e {
7737            Expr::Local { var, .. } => {
7738                matches!(vt.var(*var).ty.erased(), JavaType::Boolean)
7739            }
7740            // A bitwise bin over all-boolean sides IS boolean: the
7741            // embedded bin ty stays frozen Int from the or-int lift
7742            // (booleanize retypes the vt, not bin nodes), so the flat
7743            // lookup saw `(b1|b2) | i3` as int-sided and skipped the
7744            // mixed rewrite — rendered `boolean | int` (uuyc i5/a_3
7745            // `(v37|v38|v39)==0` ×21; news ×995 二元运算符 '|').
7746            // Mirrors emit.rs bool_ish_vt's recursion so the rewrite
7747            // here and the logical-form render there agree.
7748            Expr::Bin {
7749                op: BinOp::And | BinOp::Or | BinOp::Xor,
7750                l,
7751                r,
7752                ..
7753            } => bty(l, vt) && bty(r, vt),
7754            other => matches!(other.type_ref().erased(), JavaType::Boolean),
7755        }
7756    }
7757    // Int-typed `x ^ 1` in a BOOLEAN sink is the compiler's `!x` over a
7758    // 0/1 slot: rewrite to `x == 0` (the booleanize return-wrap misses
7759    // Xor shapes — weibo ArraysKt `return v2 ^ 1;` against a boolean
7760    // return, "int无法转换为boolean").
7761    fn inv_of_xor(x: &Expr, vt: &VarTable) -> Option<Expr> {
7762        if let Expr::Bin { op: BinOp::Xor, l, r, .. } = x {
7763            let (int_side, one_side) = if is_one(r) {
7764                (l, r)
7765            } else if is_one(l) {
7766                (r, l)
7767            } else {
7768                return None;
7769            };
7770            let _ = one_side;
7771            if !bty(int_side, vt) {
7772                return Some(Expr::Bin {
7773                    op: BinOp::Eq,
7774                    l: int_side.clone(),
7775                    r: Box::new(Expr::Const(ConstVal::Int(0))),
7776                    ty: Some(TypeRef::J(JavaType::Boolean)),
7777                });
7778            }
7779        }
7780        None
7781    }
7782    let tgt_bool = |target: &Expr, vt: &VarTable| -> bool {
7783        match target {
7784            Expr::Local { var, .. } => matches!(vt.var(*var).ty.erased(), JavaType::Boolean),
7785            Expr::Field { ty, .. } => matches!(ty.erased(), JavaType::Boolean),
7786            _ => false,
7787        }
7788    };
7789    walk_mut_deep(body, &mut |st| match st {
7790        Stmt::Return(Some(e)) if ret_bool => {
7791            if let Some(r) = inv_of_xor(e, vt) {
7792                *e = r;
7793            }
7794        }
7795        Stmt::ExprStmt(Expr::Assign { target, value, op: AssignOp::Plain, .. }) => {
7796            if tgt_bool(target, vt) {
7797                if let Some(r) = inv_of_xor(value, vt) {
7798                    **value = r;
7799                }
7800            }
7801        }
7802        Stmt::LocalDef { var, init: Some(value), .. }
7803            if matches!(vt.var(*var).ty.erased(), JavaType::Boolean) =>
7804        {
7805            if let Some(r) = inv_of_xor(value, vt) {
7806                *value = r;
7807            }
7808        }
7809        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
7810            if let Some(r) = inv_of_xor(cond, vt) {
7811                *cond = r;
7812            }
7813        }
7814        _ => {}
7815    });
7816    fn ity(e: &Expr, vt: &VarTable) -> bool {
7817        matches!(
7818            match e {
7819                Expr::Local { var, .. } => vt.var(*var).ty.erased(),
7820                other => other.type_ref().erased(),
7821            },
7822            JavaType::Int | JavaType::Short | JavaType::Byte | JavaType::Char
7823        )
7824    }
7825    /// Mixed-kind bitwise rewrite: Kotlin's non-short-circuit
7826    /// `or`/`and` over 0/1 ints mixes generations once booleanize
7827    /// converts one side (`delete | delete2` — "boolean无法转换为int" at
7828    /// the OPERAND, weixin SQLiteDatabase ×3.3k), and `b ^ 1` is `!b`.
7829    /// The int side is a 0/1 encoding: compare it, keeping the chain
7830    /// boolean end to end. ONLY legal into a BOOLEAN sink — ungated, it
7831    /// rewrote int-sink `v6 = v5x | 4` into `v5x | 4 != 0` assigned to
7832    /// an int var (boolean无法转换为int, weixin yq5/c ×2.1k family).
7833    fn mixed_rewrite(e: &mut Expr, vt: &VarTable) {
7834        deep_rewrite(e, &mut |x| {
7835            if let Expr::Bin { op, l, r, ty } = x {
7836                match op {
7837                    BinOp::Xor => {
7838                        if is_one(r) && bty(l, vt) {
7839                            let taken =
7840                                std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
7841                            *x = Expr::Un { op: UnOp::Not, e: taken };
7842                        } else if is_one(l) && bty(r, vt) {
7843                            let taken =
7844                                std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
7845                            *x = Expr::Un { op: UnOp::Not, e: taken };
7846                        }
7847                    }
7848                    BinOp::Or | BinOp::And => {
7849                        if bty(l, vt) && ity(r, vt) {
7850                            let taken =
7851                                std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
7852                            **r = Expr::Bin {
7853                                op: BinOp::Ne,
7854                                l: taken,
7855                                r: Box::new(Expr::Const(ConstVal::Int(0))),
7856                                ty: Some(TypeRef::J(JavaType::Boolean)),
7857                            };
7858                            *ty = Some(TypeRef::J(JavaType::Boolean));
7859                        } else if ity(l, vt) && bty(r, vt) {
7860                            let taken =
7861                                std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
7862                            **l = Expr::Bin {
7863                                op: BinOp::Ne,
7864                                l: taken,
7865                                r: Box::new(Expr::Const(ConstVal::Int(0))),
7866                                ty: Some(TypeRef::J(JavaType::Boolean)),
7867                            };
7868                            *ty = Some(TypeRef::J(JavaType::Boolean));
7869                        }
7870                    }
7871                    _ => {}
7872                }
7873            }
7874        });
7875    }
7876    // Boolean sinks: bool-typed assign/def targets, boolean-method
7877    // returns, and loop/branch conditions.
7878    walk_mut_deep(body, &mut |st| match st {
7879        Stmt::Return(Some(e)) if ret_bool => mixed_rewrite(e, vt),
7880        Stmt::ExprStmt(Expr::Assign { target, value, op: AssignOp::Plain, .. }) => {
7881            if tgt_bool(target, vt) {
7882                mixed_rewrite(value, vt);
7883            }
7884        }
7885        Stmt::LocalDef { var, init: Some(e), .. }
7886            if matches!(vt.var(*var).ty.erased(), JavaType::Boolean) =>
7887        {
7888            mixed_rewrite(e, vt);
7889        }
7890        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
7891            mixed_rewrite(cond, vt);
7892        }
7893        _ => {}
7894    });
7895    // A conditional's test is a boolean sink wherever it sits (including
7896    // inside int-sink statements the walk above skips).
7897    walk_stmt_exprs(body, &mut |e| {
7898        deep_rewrite(e, &mut |x| {
7899            if let Expr::Cond { c, .. } = x {
7900                mixed_rewrite(c, vt);
7901            }
7902        });
7903    });
7904}
7905
7906/// Boolean inference: vars only ever assigned 0/1/comparisons/booleans and
7907/// read in conditions become `boolean`, with `v != 0` → `v` in conditions.
7908/// Returns the number of vars converted (0 = nothing changed — the
7909/// caller skips the post-booleanize re-split, which only has work when
7910/// a conversion exposed a mixed-kind register).
7911pub fn booleanize(vt: &mut VarTable, body: &mut Stmt, ret_bool: bool) -> usize {
7912    // Booleans propagate through local chains (`v17 = v24` where v24
7913    // itself became boolean in the first round) — iterate to a fixpoint;
7914    // the common corpus converts nothing and exits after one round.
7915    // The deref disqualifier set is structural (positions, not types) —
7916    // compute ONCE per call, not per round (12 full walks on big
7917    // methods cost weibo +60% wall).
7918    let n0 = vt.vars.len();
7919    let mut derefed = vec![false; n0];
7920    mark_derefs(body, &mut derefed);
7921    let mut total = 0usize;
7922    for _ in 0..4 {
7923        let n = booleanize_round(vt, body, ret_bool, &derefed);
7924        if n == 0 {
7925            break;
7926        }
7927        total += n;
7928    }
7929    total
7930}
7931
7932/// Mark vars used in DEREF positions (field/method receiver, array
7933/// base) — allocation-free recursion (visit_all_exprs allocates a
7934/// child Vec per statement; two callers run this per method).
7935fn mark_deref_expr(e: &Expr, derefed: &mut [bool]) {
7936    match e {
7937        Expr::Field { owner: Some(o), is_static: false, .. }
7938        | Expr::Method { owner: Some(o), .. } => {
7939            if let Expr::Local { var, .. } = &**o {
7940                if (*var as usize) < derefed.len() {
7941                    derefed[*var as usize] = true;
7942                }
7943            }
7944        }
7945        Expr::ArrayIndex { array, .. } => {
7946            if let Expr::Local { var, .. } = &**array {
7947                if (*var as usize) < derefed.len() {
7948                    derefed[*var as usize] = true;
7949                }
7950            }
7951        }
7952        _ => {}
7953    }
7954    for_each_child(e, &mut |c| mark_deref_expr(c, derefed));
7955}
7956
7957fn mark_derefs(body: &Stmt, derefed: &mut [bool]) {
7958    walk_all(body, &mut |st| {
7959        match st {
7960            Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
7961                mark_deref_expr(e, derefed);
7962            }
7963            Stmt::Return(Some(e)) => mark_deref_expr(e, derefed),
7964            Stmt::LocalDef { init: Some(e), .. } => mark_deref_expr(e, derefed),
7965            Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
7966                mark_deref_expr(cond, derefed);
7967            }
7968            Stmt::For { init, .. } => {
7969                for x in init {
7970                    mark_derefs(x, derefed);
7971                }
7972            }
7973            Stmt::Switch { selector, .. } => mark_deref_expr(selector, derefed),
7974            Stmt::ForEach { iterable, .. } => mark_deref_expr(iterable, derefed),
7975            _ => {}
7976        }
7977    });
7978}
7979
7980fn booleanize_round(
7981    vt: &mut VarTable,
7982    body: &mut Stmt,
7983    ret_bool: bool,
7984    derefed: &[bool],
7985) -> usize {
7986    let n = vt.vars.len();
7987    let mut in_cond = vec![false; n];
7988
7989    // Condition uses: `v == 0` / `v != 0` if-conditions and comparison
7990    // operands inside assigned values.
7991    walk_all(body, &mut |st| {
7992        let cond: Option<&Expr> = match st {
7993            Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
7994                Some(cond)
7995            }
7996            _ => None,
7997        };
7998        if let Some(c) = cond {
7999            if let Expr::Bin {
8000                op: BinOp::Eq | BinOp::Ne,
8001                l,
8002                r,
8003                ..
8004            } = c
8005            {
8006                for (x, other) in [(l, r), (r, l)] {
8007                    if let (Expr::Local { var, .. }, Expr::Const(ConstVal::Int(0))) =
8008                        (&**x, &**other)
8009                    {
8010                        if (*var as usize) < n {
8011                            in_cond[*var as usize] = true;
8012                        }
8013                    }
8014                }
8015            }
8016        }
8017        // A local RETURNED from a boolean method is boolean-typed even
8018        // though it never appears in a condition (`int v9 = 0/1 … return
8019        // v9;` — returning an int from `boolean check()` is a compile
8020        // error; lab package Obf.check).
8021        if ret_bool {
8022            if let Stmt::Return(Some(Expr::Local { var, .. })) = st {
8023                if (*var as usize) < n {
8024                    in_cond[*var as usize] = true;
8025                }
8026            }
8027        }
8028        // A local STORED into a boolean field is boolean-typed even though
8029        // it never appears in a condition (`b = v1` where `b` is a boolean
8030        // field, `v1` an int 0/1 local — q.java static init). The emit
8031        // layer already coerces a boolean-target assign through expr_bool,
8032        // but that renders a bare int LOCAL unchanged, so the local itself
8033        // must be booleanized (its 0/1 assigns then print false/true).
8034        if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = st {
8035            if let Expr::Field { ty, .. } = &**target {
8036                if ty.erased() == JavaType::Boolean {
8037                    if let Expr::Local { var, .. } = &**value {
8038                        if (*var as usize) < n {
8039                            in_cond[*var as usize] = true;
8040                        }
8041                    }
8042                }
8043            }
8044        }
8045        let val: Option<&Expr> = match st {
8046            Stmt::ExprStmt(Expr::Assign { value, .. }) => Some(value),
8047            // Bare expression statements (e.g. `q(v117);`) so a local
8048            // passed to a boolean parameter is caught below.
8049            Stmt::ExprStmt(e) => Some(e),
8050            Stmt::LocalDef { init: Some(e), .. } => Some(e),
8051            Stmt::Return(Some(e)) => Some(e),
8052            _ => None,
8053        };
8054        if let Some(v) = val {
8055            visit_exprs(v, &mut |x| {
8056                // A local passed as a BOOLEAN parameter is boolean-typed
8057                // (`q(v117)` where q's param is boolean, v117 an int 0/1 —
8058                // f8/b.java). The DEX passes booleans as 0/1 ints, so a
8059                // boolean param slot is authoritative for the argument's
8060                // type.
8061                if let Expr::Method { desc, args, .. } = x {
8062                    for (i, a) in args.iter().enumerate() {
8063                        if desc.args.get(i) == Some(&JavaType::Boolean) {
8064                            if let Expr::Local { var, .. } = a {
8065                                if (*var as usize) < n {
8066                                    in_cond[*var as usize] = true;
8067                                }
8068                            }
8069                        }
8070                    }
8071                }
8072                if let Expr::Bin { op, l, r, .. } = x {
8073                    if matches!(
8074                        op,
8075                        BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
8076                    ) {
8077                        for side in [l, r] {
8078                            if let Expr::Local { var, .. } = &**side {
8079                                if (*var as usize) < n {
8080                                    in_cond[*var as usize] = true;
8081                                }
8082                            }
8083                        }
8084                    }
8085                    // Kotlin/d8 merge booleans through INT bitwise ops
8086                    // (`v3 | obj instanceof g`) == 0 — the other side's
8087                    // static type being boolean makes this a boolean
8088                    // context for the local (a genuine int bitwise
8089                    // expression never has a boolean operand).
8090                    if matches!(op, BinOp::Or | BinOp::And) {
8091                        let l_bool = l.type_ref().erased() == JavaType::Boolean;
8092                        let r_bool = r.type_ref().erased() == JavaType::Boolean;
8093                        if l_bool != r_bool {
8094                            let side = if l_bool { r } else { l };
8095                            if let Expr::Local { var, .. } = &**side {
8096                                if (*var as usize) < n {
8097                                    in_cond[*var as usize] = true;
8098                                }
8099                            }
8100                        }
8101                    }
8102                }
8103            });
8104        }
8105    });
8106
8107    // A var whose every assignment value is boolean-shaped AND which is
8108    // referenced in conditions becomes boolean.
8109    // (Perf: this used to re-walk the whole statement tree ONCE PER VAR —
8110    // O(vars × stmts); on R8-merged monsters (2000+ vars, 15k statements)
8111    // that was tens of millions of visits. One pass collects the same
8112    // (any, all_bool) facts for every var.)
8113    let mut assigned_any = vec![false; n];
8114    let mut all_bool = vec![true; n];
8115    let mut edges: Vec<(usize, usize)> = Vec::new();
8116    walk_all(body, &mut |st| {
8117        let (var, value) = match st {
8118            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => match &**target {
8119                Expr::Local { var, .. } => (var, &**value),
8120                _ => return,
8121            },
8122            Stmt::LocalDef {
8123                var, init: Some(e), ..
8124            } => (var, e),
8125            _ => return,
8126        };
8127        let i = *var as usize;
8128        if i < n {
8129            assigned_any[i] = true;
8130            if !is_boolean_valued(value, vt) {
8131                all_bool[i] = false;
8132            }
8133            // Chain edges for the context closure below: `v17 = v24`
8134            // links the two locals; when the TARGET is in a boolean
8135            // context, the source inherits it (its only reader is the
8136            // boolean-shaped chain). Collected as edges because the
8137            // target's context can be discovered anywhere in the tree.
8138            if let Expr::Local { var: src, .. } = value {
8139                if (*src as usize) < n {
8140                    edges.push((i, *src as usize));
8141                }
8142            }
8143        }
8144    });
8145    // Context closure along local chains: a var in a boolean context
8146    // pushes that context through every local it is assigned from
8147    // (`return v17` — v17 = v24 — v24 = 0/1) — the statement order
8148    // cannot be relied on (the return may follow the assignment).
8149    loop {
8150        let mut changed = false;
8151        for &(tgt, src) in &edges {
8152            if in_cond[tgt] && !in_cond[src] && all_bool[src] {
8153                in_cond[src] = true;
8154                changed = true;
8155            }
8156            // FORWARD through pure copies: `v21 = v17` with v17 boolean
8157            // makes v21 boolean when ALL of v21's assignments are
8158            // bool-shaped (the loop-state save pattern — weixin u2/f's
8159            // v62 → v17 → v21 chains left the copy targets int while
8160            // the accumulator booleanized).
8161            if in_cond[src] && !in_cond[tgt] && all_bool[tgt] {
8162                in_cond[tgt] = true;
8163                changed = true;
8164            }
8165        }
8166        if !changed {
8167            break;
8168        }
8169    }
8170    let reads = count_locals_stmts(std::slice::from_ref(body));
8171    let mut boolean_vars: HashSet<u32> = HashSet::default();
8172    for i in 0..n {
8173        if vt.vars[i].is_param {
8174            continue;
8175        }
8176        if derefed[i] {
8177            continue;
8178        }
8179        if !in_cond[i] && !(reads.get(&(i as u32)).copied().unwrap_or(0) == 0) {
8180            continue;
8181        }
8182        if assigned_any[i] && all_bool[i] {
8183            boolean_vars.insert(i as u32);
8184        }
8185    }
8186    if boolean_vars.is_empty() {
8187        return 0;
8188    }
8189    for v in &boolean_vars {
8190        vt.vars[*v as usize].ty = TypeRef::J(JavaType::Boolean);
8191    }
8192    let types: Vec<&TypeRef> = vt.vars.iter().map(|v| &v.ty).collect();
8193    rewrite_exprs(body, &mut |e| {
8194        deep_rewrite(e, &mut |x| {
8195            if let Expr::Local { var, ty } = x {
8196                if let Some(want) = types.get(*var as usize) {
8197                    if ty != *want {
8198                        *ty = (*want).clone();
8199                    }
8200                }
8201            }
8202        });
8203    });
8204
8205    // Condition folding: `b != 0` → `b`, `b == 0` → `!b` (boolean vars).
8206    fold_bool_conditions(body, &boolean_vars);
8207    boolean_vars.len()
8208}
8209
8210fn is_boolean_valued(e: &Expr, vt: &VarTable) -> bool {
8211    match e {
8212        Expr::Const(ConstVal::Int(0)) | Expr::Const(ConstVal::Int(1)) => true,
8213        // Kotlin's non-short-circuit boolean `or`/`and` compiles to `|`/`&`
8214        // over boolean locals (`v15 = delete | delete2 | ..`). EITHER side
8215        // boolean suffices: `int | boolean` is illegal in source — it is
8216        // always a not-yet-booleanized side of a boolean chain (the
8217        // loop-carried accumulator `v15 = v20; v20 = v15 | delete3` can
8218        // never seed its all_bool fixpoint otherwise).
8219        Expr::Bin { op: BinOp::Or | BinOp::And, l, r, .. } => {
8220            is_boolean_valued(l, vt) || is_boolean_valued(r, vt)
8221        }
8222        // Kotlin's `!x` lowers to `x ^ 1`; at booleanize time the value
8223        // is still the Xor (fix_bool_xor runs later), so an int-typed
8224        // generation receiving it never retyped (weixin v2/i's
8225        // `int v264_g160_g7 = !v200`). BOTH sides must be bool-shaped:
8226        // `bool ^ 1` qualifies (Const 0/1 is boolean-valued above),
8227        // a genuine int `flags ^ 1` does not (flags stays Int in vt).
8228        // MUST precede the generic Bin arm (which would swallow Xor).
8229        Expr::Bin {
8230            op: BinOp::Xor,
8231            l,
8232            r,
8233            ..
8234        } => is_boolean_valued(l, vt) && is_boolean_valued(r, vt),
8235        Expr::Bin { op, .. } => matches!(
8236            op,
8237            BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
8238        ),
8239        Expr::InstanceOf { .. } => true,
8240        // The VarTable is the authority: the embedded Local ty is a
8241        // lift-time snapshot and goes STALE across booleanize rounds —
8242        // a converted copy source kept its partners int (`v150 = v131`
8243        // boolean→int, weixin ConstraintLayout ×1.4k files) because the
8244        // all_bool fixpoint consulted the stale Int.
8245        Expr::Local { var, ty } => {
8246            let t = if (*var as usize) < vt.vars.len() {
8247                vt.var(*var).ty.erased()
8248            } else {
8249                ty.erased()
8250            };
8251            t == JavaType::Boolean
8252        }
8253        Expr::Method { desc, .. } => desc.ret == JavaType::Boolean,
8254        Expr::Cond { t, f, .. } => is_boolean_valued(t, vt) && is_boolean_valued(f, vt),
8255        Expr::Un { op: UnOp::Not, .. } => true,
8256        // Field ty comes from the dex descriptor — authoritative, not a
8257        // lift snapshot (`!w1x.i` arrives here still as `w1x.i ^ 1`; the
8258        // Xor arm needs this side bool-shaped).
8259        Expr::Field { ty, .. } => ty.erased() == JavaType::Boolean,
8260        _ => false,
8261    }
8262}
8263
8264fn fold_bool_conditions(s: &mut Stmt, bv: &HashSet<u32>) {
8265    let fold = |e: &mut Expr| {
8266        deep_rewrite(e, &mut |x| {
8267            if let Expr::Bin { op, l, r, .. } = x {
8268                let (var_side, const_side) = match (&**l, &**r) {
8269                    (Expr::Local { .. }, Expr::Const(ConstVal::Int(_))) => (l, 0),
8270                    (Expr::Const(ConstVal::Int(_)), Expr::Local { .. }) => (r, 1),
8271                    _ => return,
8272                };
8273                let var = match &**var_side {
8274                    Expr::Local { var, .. } => *var,
8275                    _ => return,
8276                };
8277                if !bv.contains(&var) {
8278                    return;
8279                }
8280                let _ = const_side;
8281                match op {
8282                    BinOp::Ne => {
8283                        let inner = var_side.clone();
8284                        *x = *inner;
8285                    }
8286                    BinOp::Eq => {
8287                        let inner = var_side.clone();
8288                        *x = Expr::Un {
8289                            op: UnOp::Not,
8290                            e: Box::new(*inner),
8291                        };
8292                    }
8293                    _ => {}
8294                }
8295            }
8296        });
8297    };
8298    match s {
8299        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => fold(cond),
8300        _ => {}
8301    }
8302    walk_mut_deep(s, &mut |st| {
8303        let cond = match st {
8304            Stmt::If { cond, .. } => Some(cond),
8305            Stmt::While { cond, .. } => Some(cond),
8306            Stmt::DoWhile { cond, .. } => Some(cond),
8307            _ => None,
8308        };
8309        if let Some(c) = cond {
8310            fold(c);
8311        }
8312    });
8313}
8314
8315/// Normalize `1 == v` comparisons to `v == 1` (d8 emits const-first if-eq).
8316pub fn flip_const_compares(s: &mut Stmt) {
8317    rewrite_exprs(s, &mut |e| {
8318        deep_rewrite(e, &mut |x| {
8319            if let Expr::Bin { op, l, r, .. } = x {
8320                if matches!(
8321                    op,
8322                    BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
8323                ) {
8324                    let l_const = matches!(&**l, Expr::Const(_));
8325                    let r_var = matches!(&**r, Expr::Local { .. });
8326                    if l_const && r_var {
8327                        std::mem::swap(l, r);
8328                    }
8329                }
8330            }
8331        });
8332    });
8333}
8334
8335/// Object-typed locals compared against 0 compare against `null`.
8336pub fn null_compares(vt: &VarTable, body: &mut Stmt) {
8337    let obj_vars: HashSet<u32> = vt
8338        .vars
8339        .iter()
8340        .filter(|v| v.ty.erased().is_reference())
8341        .map(|v| v.id)
8342        .collect();
8343    rewrite_exprs(body, &mut |e| {
8344        deep_rewrite(e, &mut |x| {
8345            null_side_rewrite(x, &|v| obj_vars.contains(&v));
8346        });
8347    });
8348}
8349
8350/// Drop assignments/declarations of locals that are NEVER read anywhere
8351/// in the method. Register-machine merges commit values no Java-level
8352/// code consumes (phi residue like `printStream = check;` — not just
8353/// noise: the merge variable's inferred type need not match the value,
8354/// so these lines are often type errors as well). Side-effect-free
8355/// values drop entirely; an impure value survives as a bare expression
8356/// statement. Iterated to a fixpoint: dropping `a = b` can make `b`
8357/// unread.
8358pub fn drop_dead_locals(body: &mut Stmt) {
8359    loop {
8360        let mut reads: Vec<usize> = Vec::new();
8361        count_reads(body, &mut reads);
8362        let mut dropped = 0usize;
8363        walk_mut_deep(body, &mut |st| match st {
8364            Stmt::Block(items) => prune_dead_items(items, &reads, &mut dropped),
8365            // Switch case bodies and For inits are bare `Vec<Stmt>`, NOT
8366            // wrapped in a `Stmt::Block`, so the Block arm never sees them.
8367            // Dead phi commits land directly in case bodies (`sb4 =
8368            // compareTo;` per case): pruning only Blocks dropped the
8369            // commit's reader (in a real post-switch Block) but left the
8370            // now-dead commits, stalling the fixpoint cascade and leaking
8371            // mistyped phi residue. TryWithResources.resources is
8372            // deliberately excluded — its LocalDefs carry auto-close
8373            // semantics that a bare-expression rewrite would break.
8374            Stmt::Switch { cases, .. } => {
8375                for c in cases {
8376                    prune_dead_items(&mut c.body, &reads, &mut dropped);
8377                }
8378            }
8379            Stmt::For { init, .. } => prune_dead_items(init, &reads, &mut dropped),
8380            _ => {}
8381        });
8382        if dropped == 0 {
8383            break;
8384        }
8385    }
8386}
8387
8388/// Remove dead assignments/declarations from one bare statement vector.
8389/// Shared by `drop_dead_locals` across every prunable `Vec<Stmt>`
8390/// container. Side-effect-free values drop entirely; an impure value
8391/// survives as a bare expression statement.
8392fn prune_dead_items(items: &mut Vec<Stmt>, reads: &[usize], dropped: &mut usize) {
8393    let dead = |v: u32| reads.get(v as usize).copied().unwrap_or(0) == 0;
8394    items.retain_mut(|x| match x {
8395        Stmt::ExprStmt(Expr::Assign { target, value, op, .. }) => {
8396            let dead_target = matches!(&**target, Expr::Local { var, .. } if dead(*var));
8397            if dead_target && matches!(*op, AssignOp::Plain) {
8398                *dropped += 1;
8399                if has_side_effects(value) {
8400                    let e = std::mem::replace(value, Box::new(Expr::This));
8401                    *x = Stmt::ExprStmt(*e);
8402                    true
8403                } else {
8404                    false
8405                }
8406            } else {
8407                true
8408            }
8409        }
8410        Stmt::LocalDef { var, init, .. } if dead(*var) => {
8411            *dropped += 1;
8412            if let Some(e) = init.take() {
8413                if has_side_effects(&e) {
8414                    *x = Stmt::ExprStmt(e);
8415                    return true;
8416                }
8417            }
8418            false
8419        }
8420        _ => true,
8421    });
8422}
8423
8424
8425/// Vars occurring in this statement's OWN expression payloads (nested
8426/// sub-statements are descended separately so every occurrence carries
8427/// its innermost block id). ForEach/Catch bound vars are declarations,
8428/// not occurrences.
8429fn stmt_shallow_vars<F: FnMut(u32)>(st: &Stmt, out: &mut F) {
8430    fn ex<F: FnMut(u32)>(e: &Expr, out: &mut F) {
8431        visit_exprs(e, &mut |x| {
8432            if let Expr::Local { var, .. } = x {
8433                out(*var);
8434            }
8435        });
8436    }
8437    match st {
8438        Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
8439            ex(e, out)
8440        }
8441        Stmt::TernaryValue { e } => ex(e, out),
8442        Stmt::Return(Some(e)) => ex(e, out),
8443        Stmt::LocalDef { var, init, .. } => {
8444            out(*var);
8445            if let Some(e) = init {
8446                ex(e, out);
8447            }
8448        }
8449        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
8450            ex(cond, out)
8451        }
8452        Stmt::For { cond, update, .. } => {
8453            if let Some(c) = cond {
8454                ex(c, out);
8455            }
8456            for u in update {
8457                ex(u, out);
8458            }
8459        }
8460        Stmt::ForEach { iterable, .. } => ex(iterable, out),
8461        Stmt::Switch { selector, cases, .. } => {
8462            ex(selector, out);
8463            for cg in cases {
8464                if let Some(g) = &cg.guard {
8465                    ex(g, out);
8466                }
8467            }
8468        }
8469        Stmt::Assert { cond, msg } => {
8470            ex(cond, out);
8471            if let Some(m) = msg {
8472                ex(m, out);
8473            }
8474        }
8475        Stmt::Synchronized { lock, .. } => ex(lock, out),
8476        _ => {}
8477    }
8478}
8479
8480/// Pass A: number every Block in pre-order (root = 0) and record each
8481/// var's defining block. `sizes[id]` = the id-range span of the subtree.
8482/// Pass A: number every EMITTER-BRACED position in pre-order (root = 0)
8483/// and record each var's defining block. `sizes[id]` = the id-range span
8484/// of the subtree. A braced position is a Java scope whether or not the
8485/// IR node is a Stmt::Block — switch case bodies are bare Vecs and
8486/// single-statement if arms are bare statements, and sharing the parent's
8487/// id for them hid the scope violation (Telegram jk.java: a case-local
8488/// `notificationCenterDelegate`/`d0x` read after the switch never
8489/// hoisted — 找不到符号 ×76 in one file; the `d9x.L` reads even resolved
8490/// as a PACKAGE — 程序包不存在 ×92 tree-wide).
8491fn scope_pass_a(st: &Stmt, cur: u32, next: &mut u32, def_block: &mut [u32], sizes: &mut Vec<u32>) {
8492    if let Stmt::LocalDef { var, .. } = st {
8493        let i = *var as usize;
8494        if i < def_block.len() && def_block[i] == u32::MAX {
8495            def_block[i] = cur;
8496        }
8497    }
8498    fn scoped(c: &Stmt, next: &mut u32, def_block: &mut [u32], sizes: &mut Vec<u32>) {
8499        let id = *next;
8500        *next += 1;
8501        sizes.push(0);
8502        scope_pass_a(c, id, next, def_block, sizes);
8503        sizes[id as usize] = *next - id;
8504    }
8505    match st {
8506        Stmt::Block(v) => {
8507            for x in v {
8508                scope_pass_a(x, cur, next, def_block, sizes);
8509            }
8510        }
8511        Stmt::If {
8512            then_stmt,
8513            else_stmt,
8514            ..
8515        } => {
8516            scoped(then_stmt, next, def_block, sizes);
8517            if let Some(e) = else_stmt {
8518                scoped(e, next, def_block, sizes);
8519            }
8520        }
8521        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => {
8522            scoped(body, next, def_block, sizes)
8523        }
8524        Stmt::For { init, body, .. } => {
8525            // A for-init declaration scopes over the WHOLE for statement
8526            // (header + body): one shared id, the body a nested one.
8527            let id = *next;
8528            *next += 1;
8529            sizes.push(0);
8530            for x in init {
8531                scope_pass_a(x, id, next, def_block, sizes);
8532            }
8533            scoped(body, next, def_block, sizes);
8534            sizes[id as usize] = *next - id;
8535        }
8536        Stmt::ForEach { body, .. } => scoped(body, next, def_block, sizes),
8537        Stmt::Switch { cases, default, .. } => {
8538            for c in cases {
8539                // One scope per case group (the emitter braces each);
8540                // non-Block statements share the case id, nested Blocks
8541                // get sub-ids — siblings in one brace group must share,
8542                // or a def's later same-case reads fall "outside".
8543                let id = *next;
8544                *next += 1;
8545                sizes.push(0);
8546                for x in &c.body {
8547                    if matches!(x, Stmt::Block(_)) {
8548                        scoped(x, next, def_block, sizes);
8549                    } else {
8550                        scope_pass_a(x, id, next, def_block, sizes);
8551                    }
8552                }
8553                sizes[id as usize] = *next - id;
8554            }
8555            if let Some(d) = default {
8556                scoped(d, next, def_block, sizes);
8557            }
8558        }
8559        Stmt::Try {
8560            body,
8561            catches,
8562            finally,
8563        } => {
8564            scoped(body, next, def_block, sizes);
8565            for c in catches {
8566                scoped(&c.body, next, def_block, sizes);
8567            }
8568            if let Some(f) = finally {
8569                scoped(f, next, def_block, sizes);
8570            }
8571        }
8572        Stmt::TryWithResources {
8573            resources,
8574            body,
8575            catches,
8576            finally,
8577        } => {
8578            // Resource declarations scope over the WHOLE try statement.
8579            let id = *next;
8580            *next += 1;
8581            sizes.push(0);
8582            for r in resources {
8583                scope_pass_a(r, id, next, def_block, sizes);
8584            }
8585            scoped(body, next, def_block, sizes);
8586            for c in catches {
8587                scoped(&c.body, next, def_block, sizes);
8588            }
8589            if let Some(f) = finally {
8590                scoped(f, next, def_block, sizes);
8591            }
8592            sizes[id as usize] = *next - id;
8593        }
8594        Stmt::Synchronized { body, .. } => scoped(body, next, def_block, sizes),
8595        Stmt::Labeled { body, .. } => scope_pass_a(body, cur, next, def_block, sizes),
8596        _ => {}
8597    }
8598}
8599
8600/// Pass B (same numbering walk): count each var's occurrences overall
8601/// and within its defining block's subtree. For/TWR take dedicated arms
8602/// so their header expressions count INSIDE the statement scope (a
8603/// normal `for (int i…)` must not look like an outside read of i).
8604fn scope_pass_b(
8605    st: &Stmt,
8606    cur: u32,
8607    next: &mut u32,
8608    def_block: &[u32],
8609    sizes: &[u32],
8610    within: &mut [u32],
8611    total: &mut [u32],
8612) {
8613    fn cnt(
8614        v: u32,
8615        at: u32,
8616        def_block: &[u32],
8617        sizes: &[u32],
8618        within: &mut [u32],
8619        total: &mut [u32],
8620    ) {
8621        let i = v as usize;
8622        if i < total.len() {
8623            total[i] += 1;
8624            let d = def_block[i];
8625            if d != u32::MAX && d <= at && at < d + sizes[d as usize] {
8626                within[i] += 1;
8627            }
8628        }
8629    }
8630    fn scoped(
8631        c: &Stmt,
8632        next: &mut u32,
8633        def_block: &[u32],
8634        sizes: &[u32],
8635        within: &mut [u32],
8636        total: &mut [u32],
8637    ) {
8638        let id = *next;
8639        *next += 1;
8640        scope_pass_b(c, id, next, def_block, sizes, within, total);
8641    }
8642    fn ex_at(
8643        e: &Expr,
8644        at: u32,
8645        def_block: &[u32],
8646        sizes: &[u32],
8647        within: &mut [u32],
8648        total: &mut [u32],
8649    ) {
8650        visit_exprs(e, &mut |x| {
8651            if let Expr::Local { var, .. } = x {
8652                cnt(*var, at, def_block, sizes, within, total);
8653            }
8654        });
8655    }
8656    match st {
8657        Stmt::For {
8658            init,
8659            cond,
8660            update,
8661            body,
8662        } => {
8663            let id = *next;
8664            *next += 1;
8665            for x in init {
8666                scope_pass_b(x, id, next, def_block, sizes, within, total);
8667            }
8668            if let Some(c) = cond {
8669                ex_at(c, id, def_block, sizes, within, total);
8670            }
8671            for u in update {
8672                ex_at(u, id, def_block, sizes, within, total);
8673            }
8674            scoped(body, next, def_block, sizes, within, total);
8675        }
8676        Stmt::TryWithResources {
8677            resources,
8678            body,
8679            catches,
8680            finally,
8681        } => {
8682            let id = *next;
8683            *next += 1;
8684            for r in resources {
8685                scope_pass_b(r, id, next, def_block, sizes, within, total);
8686            }
8687            scoped(body, next, def_block, sizes, within, total);
8688            for c in catches {
8689                scoped(&c.body, next, def_block, sizes, within, total);
8690            }
8691            if let Some(f) = finally {
8692                scoped(f, next, def_block, sizes, within, total);
8693            }
8694        }
8695        _ => {
8696            stmt_shallow_vars(st, &mut |v| cnt(v, cur, def_block, sizes, within, total));
8697            match st {
8698                Stmt::Block(v) => {
8699                    for x in v {
8700                        scope_pass_b(x, cur, next, def_block, sizes, within, total);
8701                    }
8702                }
8703                Stmt::If {
8704                    then_stmt,
8705                    else_stmt,
8706                    ..
8707                } => {
8708                    scoped(then_stmt, next, def_block, sizes, within, total);
8709                    if let Some(e) = else_stmt {
8710                        scoped(e, next, def_block, sizes, within, total);
8711                    }
8712                }
8713                Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => {
8714                    scoped(body, next, def_block, sizes, within, total)
8715                }
8716                Stmt::ForEach { body, .. } => {
8717                    scoped(body, next, def_block, sizes, within, total)
8718                }
8719                Stmt::Switch { cases, default, .. } => {
8720                    for c in cases {
8721                        let id = *next;
8722                        *next += 1;
8723                        for x in &c.body {
8724                            if matches!(x, Stmt::Block(_)) {
8725                                scoped(x, next, def_block, sizes, within, total);
8726                            } else {
8727                                scope_pass_b(x, id, next, def_block, sizes, within, total);
8728                            }
8729                        }
8730                    }
8731                    if let Some(d) = default {
8732                        scoped(d, next, def_block, sizes, within, total);
8733                    }
8734                }
8735                Stmt::Try {
8736                    body,
8737                    catches,
8738                    finally,
8739                } => {
8740                    scoped(body, next, def_block, sizes, within, total);
8741                    for c in catches {
8742                        scoped(&c.body, next, def_block, sizes, within, total);
8743                    }
8744                    if let Some(f) = finally {
8745                        scoped(f, next, def_block, sizes, within, total);
8746                    }
8747                }
8748                Stmt::Synchronized { body, .. } => {
8749                    scoped(body, next, def_block, sizes, within, total)
8750                }
8751                Stmt::Labeled { body, .. } => {
8752                    scope_pass_b(body, cur, next, def_block, sizes, within, total)
8753                }
8754                _ => {}
8755            }
8756        }
8757    }
8758}
8759
8760/// Declaration hygiene: every used var that has no LocalDef gets a bare
8761/// declaration at the top; duplicate LocalDefs demote to assignments.
8762/// In an enum's `<clinit>`, every constant's assignment
8763/// (`Self.FIELD = new Self("NAME", ordinal, ...)`) is compiler-mandated
8764/// boilerplate — when the class renders as a true `enum` declaration the
8765/// constants live in the header and these assignments must go. Only
8766/// ACC_ENUM-flagged static fields of the class itself are touched; the
8767/// `$VALUES` array assignment stays (a static block in an enum is legal).
8768pub fn strip_enum_const_inits(s: &mut Stmt, class: &crate::PoolClass) {
8769    let const_fields: jdc_core::FxHashSet<&str> = class
8770        .static_fields
8771        .iter()
8772        .filter(|f| f.access & crate::access::ACC_ENUM != 0)
8773        .map(|f| f.name.as_str())
8774        .collect();
8775    if const_fields.is_empty() {
8776        return;
8777    }
8778    if let Stmt::Block(stmts) = s {
8779        stmts.retain(|st| {
8780            !matches!(
8781                st,
8782                Stmt::ExprStmt(Expr::Assign { target, value, .. })
8783                    if matches!(&**target,
8784                        Expr::Field { cls, name, is_static: true, .. }
8785                            if cls.as_ref() == class.name
8786                                && const_fields.contains(&**name))
8787                        && matches!(&**value, Expr::New { cls: ncls, .. } if ncls.as_ref() == class.name)
8788            )
8789        });
8790    }
8791}
8792
8793/// Java requires the `super(...)`/`this(...)` delegation to be the FIRST
8794/// statement of a constructor. d8/R8 order the outer-reference capture
8795/// (`this.b = p1;`) or other field writes before the invokesuper in the
8796/// bytecode, which lifts as-is into an uncompilable statement order —
8797/// hoist the bare delegation call to position 0 (jadx does the same).
8798/// Does the expression touch `this` (explicit, implicit-instance member
8799/// access, or super)? Such a def cannot be inlined into a delegation
8800/// that must run BEFORE the supertype constructor.
8801fn contains_this_access(e: &Expr) -> bool {
8802    let mut hit = false;
8803    visit_exprs(e, &mut |x| {
8804        if hit {
8805            return;
8806        }
8807        match x {
8808            Expr::This => hit = true,
8809            Expr::Field { owner, is_static: false, .. } => {
8810                if owner.is_none() || matches!(&**owner.as_ref().unwrap(), Expr::This) {
8811                    hit = true;
8812                }
8813            }
8814            Expr::Method { owner, is_static: false, .. } => {
8815                if owner.is_none() || matches!(&**owner.as_ref().unwrap(), Expr::This) {
8816                    hit = true;
8817                }
8818            }
8819            _ => {}
8820        }
8821    });
8822    hit
8823}
8824
8825/// Inline the transitive straight-line defs of a delegation call's
8826/// arg locals into the call, so hoisting it to position 0 leaves no
8827/// dangling name (lark ProtoAdapter's `super(str)` with `str` defined
8828/// BELOW the hoist point — 找不到符号 变量 str). Returns false when any
8829/// arg var has no inlineable def (param-less, missing, this-touching,
8830/// or cyclic) — the caller then leaves the call in place: a
8831/// not-first-statement error is cheaper than an unresolved name that
8832/// poisons every downstream type.
8833/// Any occurrence of `var` (read OR assign-target) in `stmts` — the
8834/// suffix-use test for dropping a consumed def: count_locals_stmts only
8835/// sees reads, and a suffix WRITE to a dropped declaration makes
8836/// ensure_declared re-add `Type v;` at the method top — BEFORE the
8837/// hoisted this()/super() ("对this的调用必须是构造器中的第一个语句",
8838/// lark +587 the read-only test caused).
8839fn var_occurs_in(stmts: &[Stmt], var: u32) -> bool {
8840    stmts.iter().any(|st| {
8841        let mut hit = false;
8842        visit_stmt_exprs_ro(st, &mut |e| {
8843            if !hit {
8844                visit_exprs(e, &mut |x| {
8845                    if let Expr::Local { var: v, .. } = x {
8846                        if *v == var {
8847                            hit = true;
8848                        }
8849                    }
8850                });
8851            }
8852        });
8853        hit
8854    })
8855}
8856
8857// ---------------------------------------------------------------------------
8858// Branched-delegation ctors → static resolver helper.
8859// ---------------------------------------------------------------------------
8860
8861/// A synthetic static method extracted from a constructor whose
8862/// delegation is preceded by computation (the Kotlin default-arg /
8863/// conditional-bridge shape). Rendered by classdec after the real
8864/// methods; the ctor becomes a single first-statement delegation whose
8865/// carrier arg is the helper call.
8866pub struct CtorHelper {
8867    pub name: String,
8868    pub ret: JavaType,
8869    pub params: Vec<(JavaType, String)>,
8870    pub body: Stmt,
8871    pub vt: VarTable,
8872}
8873
8874fn ctor_helpers() -> &'static std::sync::Mutex<std::collections::HashMap<String, Vec<CtorHelper>>> {
8875    static H: std::sync::OnceLock<std::sync::Mutex<std::collections::HashMap<String, Vec<CtorHelper>>>> =
8876        std::sync::OnceLock::new();
8877    H.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::default()))
8878}
8879
8880/// Synthetic relay ctor for the FULL mode of
8881/// extract_branched_delegation_helper: `private C(Object[] h$relay) {
8882/// this/super((T0) h$relay[0], ..); }` — the public ctor becomes
8883/// `this(resolve$X(params))`, both delegations first-statement legal.
8884pub struct CtorRelay {
8885    /// The delegation target's formal types (the casts of the unpack).
8886    pub formals: Vec<JavaType>,
8887    pub is_super: bool,
8888}
8889
8890type RelayMap = std::collections::HashMap<String, Vec<(String, CtorRelay)>>;
8891
8892fn ctor_relays() -> &'static std::sync::Mutex<RelayMap> {
8893    static H: std::sync::OnceLock<std::sync::Mutex<RelayMap>> = std::sync::OnceLock::new();
8894    H.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::default()))
8895}
8896
8897/// Take (and clear) the relay ctors registered for `class` — sorted by
8898/// the originating ctor descriptor for deterministic emission.
8899pub fn take_ctor_relays(class: &str) -> Vec<(String, CtorRelay)> {
8900    let mut m = ctor_relays().lock().unwrap_or_else(|e| e.into_inner());
8901    let mut v = m.remove(class).unwrap_or_default();
8902    v.sort_by(|a, b| a.0.cmp(&b.0));
8903    v
8904}
8905
8906fn array_init(args: Vec<Expr>) -> Expr {
8907    Expr::NewArray {
8908        elem: TypeRef::J(JavaType::Object(std::sync::Arc::from("java/lang/Object"))),
8909        dims: Vec::new(),
8910        trailing_dims: 0,
8911        init: Some(args),
8912    }
8913}
8914
8915/// Deterministic helper name: FNV-1a over (class, ctor descriptor) —
8916/// worker-thread registration order must not leak into the output.
8917fn helper_name(class: &str, desc: &str) -> String {
8918    let mut h: u64 = 0xcbf29ce484222325;
8919    for b in class.as_bytes().iter().chain(b"::").chain(desc.as_bytes()) {
8920        h ^= *b as u64;
8921        h = h.wrapping_mul(0x100000001b3);
8922    }
8923    format!("resolve${:x}", (h >> 16) & 0xffff_ffff)
8924}
8925
8926/// Drain the helpers registered for a class (render-time). Sorted by
8927/// name: registration order is worker-scheduling dependent.
8928pub fn take_ctor_helpers(class: &str) -> Vec<CtorHelper> {
8929    let mut m = ctor_helpers().lock().unwrap_or_else(|e| e.into_inner());
8930    let mut v = m.remove(class).unwrap_or_default();
8931    v.sort_by(|a, b| a.name.cmp(&b.name));
8932    v
8933}
8934
8935fn strip_delegations_deep(stmts: &mut Vec<Stmt>) {
8936    stmts.retain(|s| !is_bare_ctor_call(s));
8937    for s in stmts.iter_mut() {
8938        strip_delegation_one(s);
8939    }
8940}
8941
8942fn strip_delegation_one(st: &mut Stmt) {
8943    if is_bare_ctor_call(st) {
8944        *st = Stmt::Block(Vec::new());
8945        return;
8946    }
8947    match st {
8948        Stmt::Block(v) => strip_delegations_deep(v),
8949        Stmt::If { then_stmt, else_stmt, .. } => {
8950            strip_delegation_one(then_stmt);
8951            if let Some(e) = else_stmt {
8952                strip_delegation_one(e);
8953            }
8954        }
8955        Stmt::While { body, .. }
8956        | Stmt::DoWhile { body, .. }
8957        | Stmt::ForEach { body, .. }
8958        | Stmt::Synchronized { body, .. }
8959        | Stmt::Labeled { body, .. } => strip_delegation_one(body),
8960        Stmt::For { init, body, .. } => {
8961            strip_delegations_deep(init);
8962            strip_delegation_one(body);
8963        }
8964        Stmt::Switch { cases, default, .. } => {
8965            for c in cases.iter_mut() {
8966                strip_delegations_deep(&mut c.body);
8967            }
8968            if let Some(d) = default {
8969                strip_delegation_one(d);
8970            }
8971        }
8972        Stmt::Try { body, catches, finally } | Stmt::TryWithResources { body, catches, finally, .. } => {
8973            strip_delegation_one(body);
8974            for c in catches.iter_mut() {
8975                strip_delegation_one(&mut c.body);
8976            }
8977            if let Some(f) = finally {
8978                strip_delegation_one(f);
8979            }
8980        }
8981        _ => {}
8982    }
8983}
8984
8985#[allow(clippy::ptr_arg)]
8986fn rewrite_bare_returns(stmts: &mut Vec<Stmt>, val: &Expr) {
8987    for s in stmts.iter_mut() {
8988        rewrite_bare_returns_one(s, val);
8989    }
8990}
8991
8992fn rewrite_bare_returns_one(st: &mut Stmt, val: &Expr) {
8993    match st {
8994        Stmt::Return(None) => *st = Stmt::Return(Some(val.clone())),
8995        Stmt::Block(v) => rewrite_bare_returns(v, val),
8996        Stmt::If { then_stmt, else_stmt, .. } => {
8997            rewrite_bare_returns_one(then_stmt, val);
8998            if let Some(e) = else_stmt {
8999                rewrite_bare_returns_one(e, val);
9000            }
9001        }
9002        Stmt::While { body, .. }
9003        | Stmt::DoWhile { body, .. }
9004        | Stmt::ForEach { body, .. }
9005        | Stmt::Synchronized { body, .. }
9006        | Stmt::Labeled { body, .. } => rewrite_bare_returns_one(body, val),
9007        Stmt::For { init, body, .. } => {
9008            rewrite_bare_returns(init, val);
9009            rewrite_bare_returns_one(body, val);
9010        }
9011        Stmt::Switch { cases, default, .. } => {
9012            for c in cases.iter_mut() {
9013                rewrite_bare_returns(&mut c.body, val);
9014            }
9015            if let Some(d) = default {
9016                rewrite_bare_returns_one(d, val);
9017            }
9018        }
9019        Stmt::Try { body, catches, finally } | Stmt::TryWithResources { body, catches, finally, .. } => {
9020            rewrite_bare_returns_one(body, val);
9021            for c in catches.iter_mut() {
9022                rewrite_bare_returns_one(&mut c.body, val);
9023            }
9024            if let Some(f) = finally {
9025                rewrite_bare_returns_one(f, val);
9026            }
9027        }
9028        _ => {}
9029    }
9030}
9031
9032fn collect_def_vars(stmts: &[Stmt], out: &mut jdc_core::FxHashSet<u32>) {
9033    for st in stmts {
9034        match st {
9035            Stmt::LocalDef { var, .. } => {
9036                out.insert(*var);
9037            }
9038            Stmt::ExprStmt(Expr::Assign { target, .. }) => {
9039                if let Expr::Local { var, .. } = &**target {
9040                    out.insert(*var);
9041                }
9042            }
9043            Stmt::Block(v) => collect_def_vars(v, out),
9044            Stmt::If { then_stmt, else_stmt, .. } => {
9045                collect_def_vars(std::slice::from_ref(then_stmt.as_ref()), out);
9046                if let Some(e) = else_stmt {
9047                    collect_def_vars(std::slice::from_ref(e.as_ref()), out);
9048                }
9049            }
9050            Stmt::While { body, .. }
9051            | Stmt::DoWhile { body, .. }
9052            | Stmt::Synchronized { body, .. }
9053            | Stmt::Labeled { body, .. } => {
9054                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
9055            }
9056            Stmt::ForEach { var, body, .. } => {
9057                out.insert(*var);
9058                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
9059            }
9060            Stmt::For { init, body, .. } => {
9061                collect_def_vars(init, out);
9062                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
9063            }
9064            Stmt::Switch { cases, default, .. } => {
9065                for c in cases {
9066                    collect_def_vars(&c.body, out);
9067                }
9068                if let Some(d) = default {
9069                    collect_def_vars(std::slice::from_ref(d.as_ref()), out);
9070                }
9071            }
9072            Stmt::Try { body, catches, finally }
9073            | Stmt::TryWithResources { body, catches, finally, .. } => {
9074                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
9075                for c in catches {
9076                    out.insert(c.var);
9077                    collect_def_vars(std::slice::from_ref(c.body.as_ref()), out);
9078                }
9079                if let Some(f) = finally {
9080                    collect_def_vars(std::slice::from_ref(f.as_ref()), out);
9081                }
9082            }
9083            _ => {}
9084        }
9085    }
9086}
9087
9088/// v1 extraction is limited to Block/If/straight-line computation (the
9089/// Kotlin default-arg shape); loops/switch/try in the extraction bail.
9090fn extraction_shape_ok(stmts: &[Stmt]) -> bool {
9091    stmts.iter().all(|st| match st {
9092        Stmt::Block(v) => extraction_shape_ok(v),
9093        Stmt::LocalDef { .. }
9094        | Stmt::ExprStmt(_)
9095        | Stmt::Return(_)
9096        | Stmt::Throw(_)
9097        | Stmt::Assert { .. }
9098        | Stmt::Break(_)
9099        | Stmt::Continue(_) => true,
9100        Stmt::If { then_stmt, else_stmt, .. } => {
9101            ok_one(then_stmt)
9102                && else_stmt.as_deref().map(ok_one).unwrap_or(true)
9103        }
9104        Stmt::While { body, .. }
9105        | Stmt::DoWhile { body, .. }
9106        | Stmt::ForEach { body, .. }
9107        | Stmt::Synchronized { body, .. } => ok_one(body),
9108        Stmt::Labeled { body, .. } => ok_one(body),
9109        Stmt::For { init, body, .. } => extraction_shape_ok(init) && ok_one(body),
9110        Stmt::Switch { cases, default, .. } => {
9111            cases.iter().all(|c| extraction_shape_ok(&c.body))
9112                && default.as_deref().map(ok_one).unwrap_or(true)
9113        }
9114        Stmt::Try { body, catches, finally }
9115        | Stmt::TryWithResources { body, catches, finally, .. } => {
9116            ok_one(body)
9117                && catches.iter().all(|c| ok_one(&c.body))
9118                && finally.as_deref().map(ok_one).unwrap_or(true)
9119        }
9120        // Label/Goto (unstructured), MonitorEnter/Exit (raw), ClassDecl
9121        // (local class), TernaryValue, Raw: stay out of v2 extraction.
9122        _ => false,
9123    })
9124}
9125
9126fn ok_one(st: &Stmt) -> bool {
9127    extraction_shape_ok(std::slice::from_ref(st))
9128}
9129
9130fn static_safe(stmts: &[Stmt], this_id: Option<u32>) -> bool {
9131    let mut ok = true;
9132    for st in stmts {
9133        visit_stmt_exprs_ro(st, &mut |e| {
9134            visit_exprs(e, &mut |x| {
9135                match x {
9136                    Expr::This => ok = false,
9137                    // ddc's IR usually carries `this` as the var-0
9138                    // PARAM local, not the Expr::This variant — a
9139                    // helper body reading it dangles (the this param
9140                    // is excluded from the helper's param list) AND
9141                    // renders 无法从静态上下文中引用非静态 (weixin y9/i).
9142                    Expr::Local { var, .. } => {
9143                        if Some(*var) == this_id {
9144                            ok = false;
9145                        }
9146                    }
9147                    // implicit-this member access / call
9148                    Expr::Field { owner: None, is_static: false, .. } => ok = false,
9149                    Expr::Method { owner: None, is_static: false, .. } => ok = false,
9150                    _ => {}
9151                }
9152            });
9153        });
9154    }
9155    ok
9156}
9157
9158/// The bytedance PatchProxy hotfix guard in <clinit>:
9159/// `if (vsChange != null) { PatchProxyResult p = PatchProxy.proxy(..);
9160/// if (p.isSupported) { return; } }` — a bare `return;` inside a STATIC
9161/// INITIALIZER is illegal Java (javac: 返回外部方法; videocut ×28,029 +
9162/// news_article ×19,058 — sampled 400/400 this shape). The patch path
9163/// is dead in a fresh compile (the IVsChange field is null), so the
9164/// whole leading guard statement strips, leaving the real static inits —
9165/// the <clinit> counterpart of the Titan/Robust ctor-guard strips.
9166/// Gate: FIRST top-level stmt is an If with no else whose body holds
9167/// both a bare return and a *PatchProxy* call.
9168pub fn strip_clinit_hotfix_guard(body: &mut Stmt) {
9169    // The structurer can wrap the sequence in a nested singleton block
9170    // (the enum-clinit path flattens for the same reason); the normal
9171    // emit_method path does not.
9172    if let Stmt::Block(vs) = body {
9173        flatten_top_blocks(vs);
9174    }
9175    let Stmt::Block(stmts) = body else { return };
9176    // The guard may sit behind hoisted bare declarations (`int[] v8x;`
9177    // from ensure_declared) — find the first non-decl statement.
9178    let mut gi = 0usize;
9179    while gi < stmts.len() && matches!(&stmts[gi], Stmt::LocalDef { init: None, .. }) {
9180        gi += 1;
9181    }
9182    let Some(first) = stmts.get(gi) else { return };
9183    let Stmt::If { then_stmt, else_stmt, .. } = first else {
9184        return;
9185    };
9186    if else_stmt.is_some() {
9187        return;
9188    }
9189    let mut has_ret = false;
9190    let mut has_proxy = false;
9191    let c = then_stmt.as_ref();
9192    walk_all(c, &mut |st| {
9193        if matches!(st, Stmt::Return(None)) {
9194            has_ret = true;
9195        }
9196    });
9197    let mut cc = c.clone();
9198    walk_stmt_exprs(&mut cc, &mut |e| {
9199        deep_rewrite(e, &mut |x| {
9200            if let Expr::Method { cls, .. } = x {
9201                if cls.contains("PatchProxy") {
9202                    has_proxy = true;
9203                }
9204            }
9205        });
9206    });
9207    if has_ret && has_proxy {
9208        stmts.remove(gi);
9209    }
9210}
9211
9212/// Vendor hotfix guards in CTORS that wrap a delegation:
9213/// `if (__ != null) { Object[] v = ..; ConstructorCode proxy =
9214/// ConstructorCode.proxy(..); if (proxy != null) { this(unpack(v));
9215/// proxy.afterSuper(this); return; } }` ahead of the real top-level
9216/// delegation (alipay InstantRun — ctor-not-first ×1,436; the same
9217/// shape as the already-stripped Baidu Titan / bytedance clinit /
9218/// Meituan Robust guards). The patch path is dead in a fresh compile
9219/// (the redirect field is null), and Java cannot place the wrapped
9220/// delegation legally — strip the WHOLE guard statement.
9221///
9222/// VENDOR-GATED on purpose: a conditional delegation with a top-level
9223/// fallback (`if (c) { this(a); return; } this(b);`) is legitimate
9224/// bytecode whose guard must NOT be dropped. The gate: the guard
9225/// subtree mentions a known hotfix framework type (hotfix / instantrun
9226/// / robust / titan / PatchProxy / ChangeQuickRedirect / ConstructorCode
9227/// in any method-owner, field owner+type, cast, or new).
9228/// First LEAF statement after unwrapping nested `Stmt::Block` wrappers —
9229/// the lifted ctor body reaches the ctor chain with the delegation and
9230/// the hotfix guard each wrapped in plain Blocks (the flattening that
9231/// produces the rendered shape runs later).
9232pub(crate) fn first_leaf_stmt(s: &Stmt) -> &Stmt {
9233    match s {
9234        Stmt::Block(v) if !v.is_empty() => first_leaf_stmt(&v[0]),
9235        other => other,
9236    }
9237}
9238
9239/// Vendor hotfix guards in CTORS that wrap a delegation:
9240/// `if (__ != null) { Object[] v = ..; ConstructorCode proxy =
9241/// ConstructorCode.proxy(..); if (proxy != null) { this(unpack(v));
9242/// proxy.afterSuper(this); return; } }` ahead of the real top-level
9243/// delegation (alipay InstantRun — ctor-not-first ×1,436, nearly all in
9244/// FALLBACK ENUM ctors; the same shape as the already-stripped Baidu
9245/// Titan / bytedance clinit / Meituan Robust guards). The patch path is
9246/// dead in a fresh compile (the redirect field is null), and Java cannot
9247/// place the wrapped delegation legally — strip the WHOLE guard
9248/// statement (at this pipeline stage it arrives Block-wrapped, and its
9249/// branches are still un-inverted: `If(then=∅, else=guard-body)`).
9250///
9251/// VENDOR-GATED on purpose: a conditional delegation with a top-level
9252/// fallback (`if (c) { this(a); return; } this(b);`) is legitimate
9253/// bytecode whose guard must NOT be dropped. The gate: the guard
9254/// subtree mentions a known hotfix framework type (hotfix / instantrun
9255/// / robust / titan / PatchProxy / ChangeQuickRedirect / ConstructorCode
9256/// in any method-owner, field owner+type, cast, or new).
9257pub fn strip_ctor_hotfix_guards(body: &mut Stmt) {
9258    fn marker_hit(e: &Expr) -> bool {
9259        let has = |n: &str| {
9260            let l = n.to_ascii_lowercase();
9261            l.contains("hotfix")
9262                || l.contains("instantrun")
9263                || l.contains("robust")
9264                || l.contains("titan")
9265                || l.contains("patchproxy")
9266                || l.contains("changequickredirect")
9267                || l.contains("constructorcode")
9268        };
9269        let mut hit = false;
9270        visit_exprs(e, &mut |x| {
9271            match x {
9272                Expr::Method { cls, .. } | Expr::New { cls, .. } | Expr::Field { cls, .. } => {
9273                    if has(cls) {
9274                        hit = true;
9275                    }
9276                }
9277                _ => {}
9278            }
9279            if let Expr::Field { ty, .. } = x {
9280                if let TypeRef::J(JavaType::Object(n)) = ty {
9281                    if has(n) {
9282                        hit = true;
9283                    }
9284                }
9285            }
9286        });
9287        hit
9288    }
9289    // A top-level statement that IS a hotfix guard: unwrap single-child
9290    // Block chains down to an If whose subtree holds BOTH a delegation
9291    // and a vendor marker.
9292    fn is_guard(st: &Stmt) -> bool {
9293        let mut cur = st;
9294        loop {
9295            match cur {
9296                Stmt::Block(v) if v.len() == 1 => cur = &v[0],
9297                Stmt::If { .. } => break,
9298                _ => return false,
9299            }
9300        }
9301        let mut has_del = false;
9302        walk_all(cur, &mut |s2| {
9303            if is_bare_ctor_call(s2) {
9304                has_del = true;
9305            }
9306        });
9307        if !has_del {
9308            return false;
9309        }
9310        let mut has_marker = false;
9311        visit_stmt_exprs_ro(cur, &mut |e| {
9312            if marker_hit(e) {
9313                has_marker = true;
9314            }
9315        });
9316        has_marker
9317    }
9318    // Strip at EVERY Block level: the lifted body arrives either as
9319    // [Block(guard), Block(del, ..)] (AtomicDataCollector) or as ONE big
9320    // Block [decls, guard, prelude-computes, del] (compose TextStyle).
9321    // No outside-delegation requirement: the vendor gate already proves
9322    // the statement is dead hotfix machinery (the redirect field is null
9323    // in a fresh compile — the same assumption the Titan/PatchProxy
9324    // strips run on), and the dex's fresh path IS whatever remains after
9325    // the guard. rpc/a.java: the real super() sits inside a switch arm —
9326    // requiring an unconditional outside delegation kept the guard (and
9327    // its illegal pre-super field writes) alive.
9328    fn strip_in(stmts: &mut Vec<Stmt>) {
9329        stmts.retain(|st| !is_guard(st));
9330        for st in stmts.iter_mut() {
9331            if let Stmt::Block(v) = st {
9332                strip_in(v);
9333            }
9334        }
9335    }
9336    let Stmt::Block(stmts) = body else { return };
9337    strip_in(stmts);
9338}
9339
9340/// Repeated identical branch-site delegations with param-only args
9341/// (weixin appbrand/zc: bare `super()` at four branch ends, each
9342/// followed by field-write tails) carry no computed carrier, so the
9343/// helper extraction has nothing to return — but they need no helper:
9344/// every delegating path runs the SAME call, so hoist one copy to
9345/// position 0 and strip the sites. The per-branch tails stay in their
9346/// branches and now legally follow the first-statement delegation.
9347/// Same reordering approximation fix_ctor_super_first already ships for
9348/// the straight-line case (paths that threw BEFORE delegating now run
9349/// the delegation first — Object-super, the dominant shape, is inert).
9350/// Gate: every del deep-equal and reading params only (a computed local
9351/// would dangle at position 0 — that is the helper extractor's job).
9352pub fn hoist_branch_delegations(body: &mut Stmt, vt: &VarTable) {
9353    let Stmt::Block(stmts) = body else { return };
9354    if stmts.first().is_some_and(is_bare_ctor_call) {
9355        return; // already first
9356    }
9357    let mut dels: Vec<Expr> = Vec::new();
9358    for st in stmts.iter() {
9359        visit_stmt_exprs_ro(st, &mut |e| {
9360            if is_delegation_expr(e) {
9361                dels.push(e.clone());
9362            }
9363        });
9364    }
9365    if dels.is_empty() {
9366        return;
9367    }
9368    if !dels.iter().all(|d| *d == dels[0]) {
9369        return;
9370    }
9371    let mut bad = false;
9372    visit_exprs(&dels[0], &mut |x| {
9373        if let Expr::Local { var, .. } = x {
9374            if (*var as usize) >= vt.vars.len() || !vt.vars[*var as usize].is_param {
9375                bad = true;
9376            }
9377        }
9378    });
9379    if bad {
9380        return;
9381    }
9382    let del = dels[0].clone();
9383    strip_delegations_deep(stmts);
9384    stmts.insert(0, Stmt::ExprStmt(del));
9385}
9386
9387/// The branched-delegation ctor family ("对this的调用必须是构造器中的
9388/// 第一个语句", ~2k across corpora): R8 renders a Kotlin default-arg /
9389/// conditional-bridge ctor as computation statements (straight-line plus
9390/// mask-guarded overrides) with every control path ending in a
9391/// `this(a, b, carrier)` delegation. Java cannot express statements
9392/// before the delegation, and unlike the linear-chain case the carrier's
9393/// value is computed by multi-statement branched code that cannot fold
9394/// into a ternary. Extract the computation into a synthetic
9395/// `private static T resolve$X(<ctor params>)` and make the ctor a
9396/// single first-statement `this(a, b, resolve$X(..))` — compilable AND
9397/// faithful (jadx renders the un-compilable statements-before-this form
9398/// for this family).
9399///
9400/// Two carrier modes:
9401/// - Single (v1): every delegation textually identical and exactly one
9402///   non-param local (the carrier) read exactly once across the args.
9403/// - Position (v2): delegations differ at exactly ONE arg position k,
9404///   every site passing a bare non-param local there (per-branch
9405///   carriers, lark BaseProtocol$ReliablePushList's v22_g1/v22x). Each
9406///   stripped delegation's own carrier fills the return that follows it;
9407///   merge-point returns fall back to the primary site's carrier.
9408///
9409/// The top-level primary delegation is optional in BOTH modes — when
9410/// every path delegates inside branches (weixin ln1/b1's super() in
9411/// if/try/catch) the whole body is extracted and any del serves as the
9412/// template (all non-k parts identical).
9413///
9414/// Shared gates (all conservative, bail keeps today's behavior): not an
9415/// enum (trace-param strip shifts render params); nested only when the
9416/// class RENDERS static-nested (member-inner ctors lose the outer param
9417/// at render); computation is Block/If/straight-line only; no
9418/// `this`-dependence; no local defined in the extraction used after the
9419/// primary.
9420pub fn extract_branched_delegation_helper(
9421    body: &mut Stmt,
9422    vt: &VarTable,
9423    pool: &DexPool,
9424    class: &crate::PoolClass,
9425    desc_str: &str,
9426) {
9427    if class.is_enum() {
9428        return;
9429    }
9430    // Member-inner ctors lose the synthetic outer param at render
9431    // (qualified-new absorbs it), so the helper's param list would no
9432    // longer match the rendered ctor. STATIC nested classes render all
9433    // params like top-level ones (lark BaseProtocol$PushList family).
9434    if class.nesting.enclosing_class.is_some() && !crate::ctx::nested_is_static(pool, class) {
9435        return;
9436    }
9437    let Stmt::Block(stmts) = body else { return };
9438    let mut dels: Vec<Expr> = Vec::new();
9439    for st in stmts.iter() {
9440        visit_stmt_exprs_ro(st, &mut |e| {
9441            if is_delegation_expr(e) {
9442                dels.push(e.clone());
9443            }
9444        });
9445    }
9446    if dels.is_empty() {
9447        return;
9448    }
9449    if dels.len() == 1 && stmts.first().is_some_and(is_bare_ctor_call) {
9450        return; // already first
9451    }
9452    let template = dels[0].clone();
9453    let Expr::Method { args: t_args, .. } = &template else {
9454        return;
9455    };
9456    // Carrier discovery: Single (all dels identical, one non-param local
9457    // read once) or Position (dels differ at exactly one arg slot, every
9458    // site a bare non-param local of one erased type).
9459    enum Mode {
9460        Single(u32),
9461        Position(usize),
9462        /// Every path delegates with the SAME target signature but its
9463        /// own arg expressions: the whole computation moves to the
9464        /// helper (each delegation becomes `return new Object[]{its
9465        /// args}`), the public ctor becomes `this(resolve$X(params))`
9466        /// and a synthetic `private C(Object[] h)` relay unpacks the
9467        /// carriers into the one legal first-statement delegation.
9468        Full,
9469    }
9470    // Full-mode gate: uniform delegation target (this/super, class,
9471    // descriptor), non-empty args, no pre-existing (Object[]) ctor,
9472    // and — the relay signature is per-CLASS unique — only the
9473    // lexicographically least <init> descriptor of the class may take
9474    // the relay (deterministic across parallel workers; a second
9475    // branched ctor in one class keeps its honest error).
9476    let full_gate = (|| -> Option<()> {
9477        let Expr::Method {
9478            args: a0,
9479            desc: d0,
9480            is_super: s0,
9481            cls: c0,
9482            ..
9483        } = &dels[0]
9484        else {
9485            return None;
9486        };
9487        if a0.is_empty() {
9488            return None;
9489        }
9490        for d in &dels[1..] {
9491            match d {
9492                Expr::Method {
9493                    args,
9494                    desc,
9495                    is_super,
9496                    cls,
9497                    ..
9498                } if args.len() == a0.len()
9499                    && **desc == **d0
9500                    && is_super == s0
9501                    && cls == c0 => {}
9502                _ => return None,
9503            }
9504        }
9505        if class
9506            .all_methods()
9507            .any(|m| &*m.name == "<init>" && &*m.desc == "([Ljava/lang/Object;)V")
9508        {
9509            return None;
9510        }
9511        Some(())
9512    })()
9513    .is_some();
9514    let mode = if dels.iter().all(|d| *d == dels[0]) {
9515        let mut carrier: Option<u32> = None;
9516        let mut reads = 0usize;
9517        let mut multi = false;
9518        for a in t_args.iter() {
9519            let c = a.clone();
9520            visit_exprs(&c, &mut |x| {
9521                if let Expr::Local { var, .. } = x {
9522                    if (*var as usize) < vt.vars.len() && !vt.vars[*var as usize].is_param {
9523                        reads += 1;
9524                        match carrier {
9525                            Some(v) if v == *var => {}
9526                            Some(_) => multi = true,
9527                            None => carrier = Some(*var),
9528                        }
9529                    }
9530                }
9531            });
9532        }
9533        match carrier {
9534            Some(cv) if !multi && reads == 1 => Mode::Single(cv),
9535            _ if full_gate => Mode::Full,
9536            _ => return,
9537        }
9538    } else {
9539        let positioned: Option<Mode> = (|| {
9540        let arity = t_args.len();
9541        // Uniform arity FIRST: a bare `super()` mixed among arg-carrying
9542        // dels made the single-diff slot k index past a short del's args
9543        // (deepseek v52 / kimi c60.o panics, index-out-of-bounds).
9544        if dels
9545            .iter()
9546            .any(|d| !matches!(d, Expr::Method { args, .. } if args.len() == arity))
9547        {
9548            return None;
9549        }
9550        let mut diff: Vec<usize> = Vec::new();
9551        for j in 0..arity {
9552            if dels.iter().any(|d| match d {
9553                Expr::Method { args, .. } => args.get(j) != t_args.get(j),
9554                _ => true,
9555            }) {
9556                diff.push(j);
9557            }
9558        }
9559        if diff.len() != 1 {
9560            return None;
9561        }
9562        let k = diff[0];
9563        // Non-arg parts AND all other arg positions identical: neutralize
9564        // slot k on every del and compare whole exprs (catches mixed
9565        // this()/super() or different targets).
9566        let mut base0 = dels[0].clone();
9567        if let Expr::Method { args, .. } = &mut base0 {
9568            args[k] = Expr::Const(ConstVal::Null);
9569        }
9570        for d in dels.iter() {
9571            let mut c = d.clone();
9572            if let Expr::Method { args, .. } = &mut c {
9573                args[k] = Expr::Const(ConstVal::Null);
9574            }
9575            if c != base0 {
9576                return None;
9577            }
9578        }
9579        // Slot k: bare non-param local at every site, one erased type,
9580        // and the carrier never read at another position.
9581        let mut carrier_ty: Option<JavaType> = None;
9582        for d in dels.iter() {
9583            let Expr::Method { args, .. } = d else {
9584                return None;
9585            };
9586            let Expr::Local { var, .. } = &args[k] else {
9587                return None;
9588            };
9589            if (*var as usize) >= vt.vars.len() || vt.vars[*var as usize].is_param {
9590                return None;
9591            }
9592            let ty = vt.vars[*var as usize].ty.erased();
9593            match &carrier_ty {
9594                Some(t) if *t == ty => {}
9595                Some(_) => return None,
9596                None => carrier_ty = Some(ty),
9597            }
9598            // Non-k positions must be param/const/static-only — a
9599            // computed local there would have its def extracted into
9600            // the helper and the ctor arg list would dangle (lark
9601            // audiosave/i 找不到符号 ×5-per-site). Also covers the
9602            // carrier leaking into another position.
9603            for (j, a) in args.iter().enumerate() {
9604                if j == k {
9605                    continue;
9606                }
9607                let mut bad = false;
9608                visit_exprs(a, &mut |x| {
9609                    if let Expr::Local { var: v2, .. } = x {
9610                        if (*v2 as usize) >= vt.vars.len() || !vt.vars[*v2 as usize].is_param {
9611                            bad = true;
9612                        }
9613                    }
9614                });
9615                if bad {
9616                    return None;
9617                }
9618            }
9619        }
9620        Some(Mode::Position(k))
9621        })();
9622        match positioned {
9623            Some(m) => m,
9624            None if full_gate => Mode::Full,
9625            None => return,
9626        }
9627    };
9628    let l = stmts.iter().rposition(is_bare_ctor_call);
9629    if l == Some(0) {
9630        return;
9631    }
9632    let (mut extracted, mut tail): (Vec<Stmt>, Vec<Stmt>) = match l {
9633        Some(l) => (stmts[..l].to_vec(), stmts[l + 1..].to_vec()),
9634        None => (stmts.clone(), Vec::new()),
9635    };
9636    if !extraction_shape_ok(&extracted) {
9637        return;
9638    }
9639    // Post-delegation tails (field writes / trace stubs between each
9640    // branch delegation and its return) are CTOR work, not helper
9641    // computation — they commonly touch `this`. When every site's tail
9642    // is identical (and equals the top-level continuation when one
9643    // exists) hoist one copy after the merged delegation; otherwise
9644    // they stay in the helper and the static_safe gate below decides.
9645    let mut site_tails: Vec<Vec<Stmt>> = Vec::new();
9646    {
9647        let mut probe = extracted.clone();
9648        site_tails_walk(&mut probe, &mut site_tails, false);
9649    }
9650    let hoist = !site_tails.is_empty() && {
9651        let n0 = normalize_tail(&site_tails[0]);
9652        site_tails.iter().all(|t| normalize_tail(t) == n0)
9653            && (l.is_none() || normalize_tail(&tail) == n0)
9654    };
9655    if hoist {
9656        site_tails_walk(&mut extracted, &mut Vec::new(), true);
9657        if l.is_none() {
9658            tail = normalize_tail(&site_tails[0]);
9659        }
9660    }
9661    // The delegation the ctor keeps: the top-level primary when present,
9662    // else any del (all non-k parts are identical).
9663    let primary_del: Expr = match l {
9664        Some(l) => match &stmts[l] {
9665            Stmt::ExprStmt(e) => e.clone(),
9666            _ => return,
9667        },
9668        None => dels[0].clone(),
9669    };
9670    let ret = match mode {
9671        Mode::Single(cv) => vt.var(cv).ty.erased(),
9672        Mode::Position(k) => match &primary_del {
9673            Expr::Method { args, .. } => match args.get(k) {
9674                Some(Expr::Local { var, .. }) => vt.var(*var).ty.erased(),
9675                _ => return,
9676            },
9677            _ => return,
9678        },
9679        Mode::Full => JavaType::Array(Box::new(JavaType::Object(std::sync::Arc::from(
9680            "java/lang/Object",
9681        )))),
9682    };
9683    match mode {
9684        Mode::Single(cv) => {
9685            strip_delegations_deep(&mut extracted);
9686            let cv_local = Expr::Local {
9687                var: cv,
9688                ty: vt.var(cv).ty.clone(),
9689            };
9690            rewrite_bare_returns(&mut extracted, &cv_local);
9691            if !always_returns(&extracted) {
9692                extracted.push(Stmt::Return(Some(cv_local)));
9693            }
9694        }
9695        Mode::Position(k) => {
9696            // Each stripped delegation's own slot-k carrier fills the
9697            // next bare return in its list; merge-point returns fall
9698            // back to the primary carrier below.
9699            fill_returns_by_position(&mut extracted, &|args: &[Expr]| args.get(k).cloned());
9700            let fallback = match &primary_del {
9701                Expr::Method { args, .. } => args[k].clone(),
9702                _ => return,
9703            };
9704            rewrite_bare_returns(&mut extracted, &fallback);
9705            if !always_returns(&extracted) {
9706                extracted.push(Stmt::Return(Some(fallback)));
9707            }
9708        }
9709        Mode::Full => {
9710            // Every delegation becomes `return new Object[]{its own
9711            // args}`; merge-point bare returns and the fall-through
9712            // completion take the PRIMARY delegation's args (the
9713            // top-level last del when present — its args are
9714            // top-level-scoped by construction). With no primary,
9715            // every path must already delegate explicitly.
9716            fill_returns_by_position(&mut extracted, &|args: &[Expr]| {
9717                Some(array_init(args.to_vec()))
9718            });
9719            if l.is_none() {
9720                // No top-level primary: every path must delegate. A
9721                // dead tail can still dangle after the shortest
9722                // always-returning prefix (DataHolder: an assign after
9723                // a try whose body returns and whose catch throws) —
9724                // truncate it (unreachable by Java's flow model, so
9725                // sound), else bail.
9726                if !always_returns(&extracted) {
9727                    let cut = (1..=extracted.len())
9728                        .find(|&i| always_returns(&extracted[..i]));
9729                    match cut {
9730                        Some(i) => extracted.truncate(i),
9731                        None => return,
9732                    }
9733                }
9734            }
9735            let fallback_args = match &primary_del {
9736                Expr::Method { args, .. } => args.clone(),
9737                _ => return,
9738            };
9739            let fallback = array_init(fallback_args);
9740            rewrite_bare_returns(&mut extracted, &fallback);
9741            if !always_returns(&extracted) {
9742                extracted.push(Stmt::Return(Some(fallback)));
9743            }
9744        }
9745    }
9746    let this_id = vt
9747        .vars
9748        .iter()
9749        .find(|v| v.is_param && v.name == "this")
9750        .map(|v| v.id);
9751    if !static_safe(&extracted, this_id) {
9752        return;
9753    }
9754    let mut defs: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
9755    collect_def_vars(&extracted, &mut defs);
9756    if defs.iter().any(|v| var_occurs_in(&tail, *v)) {
9757        return;
9758    }
9759    let params: Vec<(u32, JavaType, String)> = vt
9760        .vars
9761        .iter()
9762        .filter(|v| v.is_param && v.name != "this")
9763        .map(|v| (v.id, v.ty.erased(), v.name.clone()))
9764        .collect();
9765    let name = helper_name(&class.name, desc_str);
9766    let call = Expr::Method {
9767        owner: None,
9768        cls: std::sync::Arc::from(class.name.as_str()),
9769        name: std::sync::Arc::from(name.as_str()),
9770        desc: std::sync::Arc::new(jdc_core::types::MethodDescriptor {
9771            ret: ret.clone(),
9772            args: params.iter().map(|p| p.1.clone()).collect(),
9773        }),
9774        args: params
9775            .iter()
9776            .map(|(id, ty, _)| Expr::Local {
9777                var: *id,
9778                ty: TypeRef::J(ty.clone()),
9779            })
9780            .collect(),
9781        is_static: true,
9782        is_interface: false,
9783        is_special: false,
9784        is_super: false,
9785        is_dynamic: false,
9786        type_args: Vec::new(),
9787    };
9788    let mut new_del = primary_del.clone();
9789    match mode {
9790        Mode::Single(cv) => {
9791            deep_rewrite(&mut new_del, &mut |x| {
9792                if let Expr::Local { var, .. } = x {
9793                    if *var == cv {
9794                        *x = call.clone();
9795                    }
9796                }
9797            });
9798        }
9799        Mode::Position(k) => {
9800            let Expr::Method { args, .. } = &mut new_del else {
9801                return;
9802            };
9803            if k >= args.len() {
9804                return;
9805            }
9806            args[k] = call.clone();
9807        }
9808        Mode::Full => {
9809            // The public ctor's one legal statement: `this(resolve$X(p..))`
9810            // — the synthetic relay ctor below unpacks the carriers into
9811            // the uniform target delegation.
9812            new_del = Expr::Method {
9813                owner: None,
9814                cls: std::sync::Arc::from(class.name.as_str()),
9815                name: "<init>".into(),
9816                desc: std::sync::Arc::new(jdc_core::types::MethodDescriptor {
9817                    args: vec![JavaType::Array(Box::new(JavaType::Object(
9818                        std::sync::Arc::from("java/lang/Object"),
9819                    )))],
9820                    ret: JavaType::Void,
9821                }),
9822                args: vec![call.clone()],
9823                is_static: false,
9824                is_interface: false,
9825                is_special: true,
9826                is_super: false,
9827                is_dynamic: false,
9828                type_args: Vec::new(),
9829            };
9830        }
9831    }
9832    {
9833        let mut m = ctor_helpers().lock().unwrap_or_else(|e| e.into_inner());
9834        let v = m.entry(class.name.clone()).or_default();
9835        if !v.iter().any(|h| h.name == name) {
9836            // The extracted body skips the late pipeline polish (it left
9837            // the ctor before cleanup ran) — invert empty thens so the
9838            // helper doesn't render `if (..) {} else {..}`.
9839            let mut hbody = Stmt::Block(extracted);
9840            invert_empty_thens(&mut hbody);
9841            v.push(CtorHelper {
9842                name,
9843                ret: ret.clone(),
9844                params: params.into_iter().map(|(_, ty, nm)| (ty, nm)).collect(),
9845                body: hbody,
9846                vt: vt.clone(),
9847            });
9848            if matches!(mode, Mode::Full) {
9849                let Expr::Method {
9850                    desc: d0,
9851                    is_super: s0,
9852                    ..
9853                } = &primary_del
9854                else {
9855                    return;
9856                };
9857                let mut rm = ctor_relays().lock().unwrap_or_else(|e| e.into_inner());
9858                let rv = rm.entry(class.name.clone()).or_default();
9859                if !rv.iter().any(|(d, _)| d == desc_str) {
9860                    rv.push((
9861                        desc_str.to_string(),
9862                        CtorRelay {
9863                            formals: d0.args.clone(),
9864                            is_super: *s0,
9865                        },
9866                    ));
9867                }
9868            }
9869        } else {
9870            return; // duplicate decompile of the same ctor: keep the first
9871        }
9872    }
9873    *stmts = std::iter::once(Stmt::ExprStmt(new_del)).chain(tail).collect();
9874}
9875
9876/// Position-mode return fill: walk each statement list left-to-right; a
9877/// delegation at slot k hands its carrier to the next bare `return;` in
9878/// the SAME list (the bytecode shape is always `this(..); trace..;
9879/// return;`). With no following return in the list the delegation
9880/// becomes the return itself and the rest of the list (trace stubs only
9881/// — a second delegation there is impossible) is dropped as unreachable.
9882fn fill_returns_by_position(
9883    stmts: &mut Vec<Stmt>,
9884    pick: &dyn Fn(&[Expr]) -> Option<Expr>,
9885) {
9886    let mut i = 0;
9887    while i < stmts.len() {
9888        if is_bare_ctor_call(&stmts[i]) {
9889            let karg = match &stmts[i] {
9890                Stmt::ExprStmt(Expr::Method { args, .. }) => pick(args),
9891                _ => None,
9892            };
9893            if let Some(v) = karg {
9894                let mut j = i + 1;
9895                let mut found = None;
9896                while j < stmts.len() {
9897                    if matches!(stmts[j], Stmt::Return(None)) {
9898                        found = Some(j);
9899                        break;
9900                    }
9901                    if is_bare_ctor_call(&stmts[j]) {
9902                        break; // next delegation owns the next return
9903                    }
9904                    j += 1;
9905                }
9906                match found {
9907                    Some(j) => {
9908                        stmts[j] = Stmt::Return(Some(v));
9909                        stmts.remove(i);
9910                        continue;
9911                    }
9912                    None => {
9913                        stmts[i] = Stmt::Return(Some(v));
9914                        stmts.truncate(i + 1);
9915                        break;
9916                    }
9917                }
9918            }
9919        }
9920        fill_returns_one(&mut stmts[i], pick);
9921        i += 1;
9922    }
9923}
9924
9925fn fill_returns_one(st: &mut Stmt, pick: &dyn Fn(&[Expr]) -> Option<Expr>) {
9926    match st {
9927        Stmt::Block(v) => fill_returns_by_position(v, pick),
9928        Stmt::If { then_stmt, else_stmt, .. } => {
9929            fill_returns_stmt(then_stmt, pick);
9930            if let Some(e) = else_stmt {
9931                fill_returns_stmt(e, pick);
9932            }
9933        }
9934        Stmt::While { body, .. }
9935        | Stmt::DoWhile { body, .. }
9936        | Stmt::ForEach { body, .. }
9937        | Stmt::Synchronized { body, .. }
9938        | Stmt::Labeled { body, .. } => fill_returns_stmt(body, pick),
9939        Stmt::For { init, body, .. } => {
9940            fill_returns_by_position(init, pick);
9941            fill_returns_stmt(body, pick);
9942        }
9943        Stmt::Switch { cases, default, .. } => {
9944            for c in cases.iter_mut() {
9945                fill_returns_by_position(&mut c.body, pick);
9946            }
9947            if let Some(d) = default {
9948                fill_returns_stmt(d, pick);
9949            }
9950        }
9951        Stmt::Try { body, catches, finally } | Stmt::TryWithResources { body, catches, finally, .. } => {
9952            fill_returns_stmt(body, pick);
9953            for c in catches.iter_mut() {
9954                fill_returns_stmt(&mut c.body, pick);
9955            }
9956            if let Some(f) = finally {
9957                fill_returns_stmt(f, pick);
9958            }
9959        }
9960        _ => {}
9961    }
9962}
9963
9964fn fill_returns_stmt(st: &mut Stmt, pick: &dyn Fn(&[Expr]) -> Option<Expr>) {
9965    if is_bare_ctor_call(st) {
9966        // Single-statement branch body (`if (c) this(..);`) — becomes
9967        // the return directly.
9968        if let Stmt::ExprStmt(Expr::Method { args, .. }) = st {
9969            if let Some(v) = pick(args) {
9970                *st = Stmt::Return(Some(v));
9971                return;
9972            }
9973        }
9974    }
9975    fill_returns_one(st, pick);
9976}
9977
9978fn normalize_tail(t: &[Stmt]) -> Vec<Stmt> {
9979    let mut v = t.to_vec();
9980    while matches!(v.last(), Some(Stmt::Return(None))) {
9981        v.pop();
9982    }
9983    v
9984}
9985
9986/// Record (and with `carve`, remove) the statements between each
9987/// delegation and the next bare return in its list — a delegation with
9988/// no following return takes the rest of the list. Single-statement
9989/// branch bodies record an empty tail.
9990fn site_tails_walk(stmts: &mut Vec<Stmt>, out: &mut Vec<Vec<Stmt>>, carve: bool) {
9991    let mut i = 0;
9992    while i < stmts.len() {
9993        if is_bare_ctor_call(&stmts[i]) {
9994            let mut j = i + 1;
9995            let mut end = stmts.len();
9996            while j < stmts.len() {
9997                if matches!(stmts[j], Stmt::Return(None)) {
9998                    end = j;
9999                    break;
10000                }
10001                if is_bare_ctor_call(&stmts[j]) {
10002                    end = j;
10003                    break;
10004                }
10005                j += 1;
10006            }
10007            out.push(stmts[i + 1..end].to_vec());
10008            if carve {
10009                stmts.splice(i + 1..end, []);
10010            }
10011            i += 1;
10012            continue;
10013        }
10014        site_tails_one(&mut stmts[i], out, carve);
10015        i += 1;
10016    }
10017}
10018
10019fn site_tails_one(st: &mut Stmt, out: &mut Vec<Vec<Stmt>>, carve: bool) {
10020    if is_bare_ctor_call(st) {
10021        out.push(Vec::new());
10022        return;
10023    }
10024    match st {
10025        Stmt::Block(v) => site_tails_walk(v, out, carve),
10026        Stmt::If { then_stmt, else_stmt, .. } => {
10027            site_tails_one(then_stmt, out, carve);
10028            if let Some(e) = else_stmt {
10029                site_tails_one(e, out, carve);
10030            }
10031        }
10032        Stmt::While { body, .. }
10033        | Stmt::DoWhile { body, .. }
10034        | Stmt::ForEach { body, .. }
10035        | Stmt::Synchronized { body, .. }
10036        | Stmt::Labeled { body, .. } => site_tails_one(body, out, carve),
10037        Stmt::For { init, body, .. } => {
10038            site_tails_walk(init, out, carve);
10039            site_tails_one(body, out, carve);
10040        }
10041        Stmt::Switch { cases, default, .. } => {
10042            for c in cases.iter_mut() {
10043                site_tails_walk(&mut c.body, out, carve);
10044            }
10045            if let Some(d) = default {
10046                site_tails_one(d, out, carve);
10047            }
10048        }
10049        Stmt::Try { body, catches, finally } | Stmt::TryWithResources { body, catches, finally, .. } => {
10050            site_tails_one(body, out, carve);
10051            for c in catches.iter_mut() {
10052                site_tails_one(&mut c.body, out, carve);
10053            }
10054            if let Some(f) = finally {
10055                site_tails_one(f, out, carve);
10056            }
10057        }
10058        _ => {}
10059    }
10060}
10061
10062/// Can this statement list NOT complete normally? Conservative for
10063/// loops (false); If needs BOTH branches, Switch needs every case plus
10064/// a default. Used to decide whether the extracted helper needs the
10065/// trailing `return carrier;` (appending one after exhaustive branches
10066/// is javac's "unreachable statement" error).
10067fn always_returns(stmts: &[Stmt]) -> bool {
10068    let Some(last) = stmts
10069        .iter()
10070        .rev()
10071        .find(|s| !matches!(s, Stmt::Block(v) if v.is_empty()))
10072    else {
10073        return false;
10074    };
10075    match last {
10076        Stmt::Return(Some(_)) | Stmt::Throw(_) => true,
10077        Stmt::Block(v) => always_returns(v),
10078        Stmt::If { then_stmt, else_stmt, .. } => match else_stmt {
10079            Some(e) => always_returns_one(then_stmt) && always_returns_one(e),
10080            None => false,
10081        },
10082        Stmt::Labeled { body, .. } | Stmt::Synchronized { body, .. } => {
10083            always_returns_one(body)
10084        }
10085        Stmt::Switch { cases, default, .. } => {
10086            default.as_ref().is_some_and(|d| always_returns_one(d))
10087                && cases.iter().all(|c| always_returns(&c.body))
10088        }
10089        Stmt::Try { body, catches, finally }
10090        | Stmt::TryWithResources { body, catches, finally, .. } => {
10091            finally.as_ref().is_some_and(|f| always_returns_one(f))
10092                || (always_returns_one(body)
10093                    && catches.iter().all(|c| always_returns_one(&c.body)))
10094        }
10095        _ => false,
10096    }
10097}
10098
10099fn always_returns_one(st: &Stmt) -> bool {
10100    always_returns(std::slice::from_ref(st))
10101}
10102
10103fn inline_ctor_arg_defs(
10104    call: &mut Stmt,
10105    prefix: &[&Stmt],
10106    vt: &VarTable,
10107    consumed: &mut Vec<(u32, usize)>,
10108) -> bool {
10109    let Stmt::ExprStmt(cexpr) = call else {
10110        return false;
10111    };
10112    // Replace every read of `v` in `e` with its value-so-far; a read
10113    // BEFORE any def (val None) is unresolvable → false.
10114    fn subst(e: &mut Expr, v: u32, val: &Option<Expr>) -> bool {
10115        let mut ok = true;
10116        deep_rewrite(e, &mut |x| {
10117            if let Expr::Local { var, .. } = x {
10118                if *var == v {
10119                    match val {
10120                        Some(e2) => *x = e2.clone(),
10121                        None => ok = false,
10122                    }
10123                }
10124            }
10125        });
10126        ok
10127    }
10128    let mut seen: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
10129    for _ in 0..32 {
10130        let mut need: Option<u32> = None;
10131        visit_exprs(cexpr, &mut |x| {
10132            if need.is_none() {
10133                if let Expr::Local { var, .. } = x {
10134                    if (*var as usize) < vt.vars.len() && !vt.vars[*var as usize].is_param {
10135                        need = Some(*var);
10136                    }
10137                }
10138            }
10139        });
10140        let Some(v) = need else { break };
10141        if !seen.insert(v) {
10142            return false; // cyclic reuse (`str = .. + str`) — bail
10143        }
10144        // CHAIN walk: fold EVERY prefix def of v into the value the
10145        // delegation actually receives. The old strict single-occurrence
10146        // rule rejected the convenience-ctor reassignment chain
10147        // (`f = getInstance(ctx); f = transform(f, ..); this(ctx, f)` —
10148        // androidx CameraController, weibo this-not-first ×831): the
10149        // SOURCE inlined the computation into the delegation args and
10150        // R8 decomposed it into register writes. Reads of v are allowed
10151        // only INSIDE its own def chain; any other prefix statement
10152        // touching v means the value escapes or the object mutates
10153        // (`stringBuilder.append(..)` — inlining a fresh `new
10154        // StringBuilder()` would lose the message), so bail.
10155        let mut val: Option<Expr> = None;
10156        let mut ok = true;
10157        for (si, st) in prefix.iter().enumerate() {
10158            match st {
10159                // A bare declaration (init None — the structurer hoists
10160                // `T v;` to the top) carries no value: skip it. Failing
10161                // here rejected every chain under a hoisted decl (ci1/a).
10162                Stmt::LocalDef { var, init: None, .. } if *var == v => {}
10163                Stmt::LocalDef { var, init, .. } if *var == v => {
10164                    let Some(e) = init else { ok = false; break };
10165                    let mut e2 = (*e).clone();
10166                    if !subst(&mut e2, v, &val) {
10167                        ok = false;
10168                        break;
10169                    }
10170                    val = Some(e2);
10171                    consumed.push((v, si));
10172                }
10173                Stmt::ExprStmt(Expr::Assign { target, op, .. })
10174                    if matches!(&**target, Expr::Local { var: tv, .. } if *tv == v) =>
10175                {
10176                    if !matches!(op, AssignOp::Plain) {
10177                        ok = false; // compound write: not a plain def
10178                        break;
10179                    }
10180                    let Stmt::ExprStmt(Expr::Assign { value, .. }) = st else {
10181                        unreachable!()
10182                    };
10183                    let mut e2 = value.as_ref().clone();
10184                    if !subst(&mut e2, v, &val) {
10185                        ok = false;
10186                        break;
10187                    }
10188                    val = Some(e2);
10189                    consumed.push((v, si));
10190                }
10191                _ => {
10192                    let mut touched = false;
10193                    visit_all_exprs(st, &mut |x| {
10194                        if let Expr::Local { var, .. } = x {
10195                            if *var == v {
10196                                touched = true;
10197                            }
10198                        }
10199                    });
10200                    if touched {
10201                        ok = false;
10202                        break;
10203                    }
10204                }
10205            }
10206        }
10207        if !ok {
10208            return false;
10209        }
10210        let Some(de) = val else { return false };
10211        if contains_this_access(&de) {
10212            return false;
10213        }
10214        // Huge defs multiply per read site per round — bound them.
10215        let mut de_nodes = 0usize;
10216        visit_exprs(&de, &mut |_| de_nodes += 1);
10217        if de_nodes > 256 {
10218            return false;
10219        }
10220        deep_rewrite(cexpr, &mut |x| {
10221            if let Expr::Local { var, .. } = x {
10222                if *var == v {
10223                    *x = de.clone();
10224                }
10225            }
10226        });
10227        // Exponential blowup guard: a var read twice doubles its def
10228        // per round, compounding across the transitive chain — cap the
10229        // expression size (a 2^k node tree overflowed the worker stack
10230        // process-wide).
10231        let mut nodes = 0usize;
10232        visit_exprs(cexpr, &mut |_| nodes += 1);
10233        if nodes > 2_048 {
10234            return false;
10235        }
10236    }
10237    true
10238}
10239
10240/// Fold a prelude StringBuilder mutation chain into the delegation
10241/// argument as a fluent chain: `StringBuilder sb = new SB(x);
10242/// sb.append(a); sb.append(b); super(sb.toString())` →
10243/// `super(new SB(x).append(a).append(b).toString())`. The generic def
10244/// inliner rejects mutation calls — their effect is not expressible as
10245/// a value — but `append` RETURNS its receiver, so the chain
10246/// reconstructs exactly the same mutations in one expression (androidx
10247/// emoji2/flatbuffer UnpairedSurrogateException family, the shapes the
10248/// strict-occurrence rule was written to refuse). Aborts on any `this`
10249/// touch inside the chain (illegal before super) or any other read of
10250/// the SB local (the value must not escape the fold). Returns the
10251/// delegation's new index (statements before it were removed).
10252fn fold_sb_chain(stmts: &mut Vec<Stmt>, pos: usize) -> usize {
10253    // 1. the SB local: first `v = new StringBuilder(..)` in the prefix.
10254    let mut sbv: Option<u32> = None;
10255    let mut def_i: Option<usize> = None;
10256    let mut def_init: Option<Expr> = None;
10257    for (i, st) in stmts[..pos].iter().enumerate() {
10258        let cand: Option<(u32, &Expr)> = match st {
10259            Stmt::LocalDef {
10260                var,
10261                init: Some(e),
10262                ..
10263            } => Some((*var, e)),
10264            Stmt::ExprStmt(Expr::Assign {
10265                target,
10266                value,
10267                op: AssignOp::Plain,
10268                ..
10269            }) => match &**target {
10270                Expr::Local { var, .. } => Some((*var, value.as_ref())),
10271                _ => None,
10272            },
10273            _ => None,
10274        };
10275        if let Some((v, init)) = cand {
10276            if let Expr::New { cls, .. } = init {
10277                if cls.as_ref() == "java/lang/StringBuilder" {
10278                    sbv = Some(v);
10279                    def_i = Some(i);
10280                    def_init = Some(init.clone());
10281                    break;
10282                }
10283            }
10284        }
10285    }
10286    let (Some(sbv), Some(def_i), Some(init)) = (sbv, def_i, def_init) else {
10287        return pos;
10288    };
10289    if contains_this_access(&init) {
10290        return pos;
10291    }
10292    // 2. append statements on sbv, in order.
10293    let mut appends: Vec<usize> = Vec::new();
10294    for (i, st) in stmts[..pos].iter().enumerate() {
10295        if let Stmt::ExprStmt(Expr::Method {
10296            owner: Some(o),
10297            cls,
10298            name,
10299            ..
10300        }) = st
10301        {
10302            if matches!(&**o, Expr::Local { var, .. } if *var == sbv)
10303                && cls.as_ref() == "java/lang/StringBuilder"
10304                && &**name == "append"
10305            {
10306                appends.push(i);
10307            }
10308        }
10309    }
10310    // 3. no read of sbv after the delegation.
10311    let mut post_read = false;
10312    for st in stmts.iter().skip(pos + 1) {
10313        visit_all_exprs(st, &mut |e| {
10314            if let Expr::Local { var, .. } = e {
10315                if *var == sbv {
10316                    post_read = true;
10317                }
10318            }
10319        });
10320    }
10321    if post_read {
10322        return pos;
10323    }
10324    // 4. prefix reads of sbv only in the def and the append receivers
10325    //    (arguments are checked for `this`, receivers are the chain).
10326    for (i, st) in stmts[..pos].iter().enumerate() {
10327        if i == def_i || appends.contains(&i) {
10328            // append ARGUMENTS must not touch this; receiver is fine.
10329            if let Stmt::ExprStmt(Expr::Method { args, .. }) = st {
10330                if args.iter().any(contains_this_access) {
10331                    return pos;
10332                }
10333            }
10334            continue;
10335        }
10336        let mut leak = false;
10337        visit_all_exprs(st, &mut |e| {
10338            if let Expr::Local { var, .. } = e {
10339                if *var == sbv {
10340                    leak = true;
10341                }
10342            }
10343        });
10344        if leak {
10345            return pos;
10346        }
10347    }
10348    // 5. the call reads sbv exactly once, as `sbv.toString()`.
10349    let mut local_reads = 0usize;
10350    let mut tostring_reads = 0usize;
10351    visit_all_exprs(&stmts[pos], &mut |e| match e {
10352        Expr::Local { var, .. } if *var == sbv => local_reads += 1,
10353        Expr::Method {
10354            owner: Some(o),
10355            name,
10356            args,
10357            ..
10358        } if args.is_empty()
10359            && &**name == "toString"
10360            && matches!(&**o, Expr::Local { var, .. } if *var == sbv) =>
10361        {
10362            tostring_reads += 1;
10363        }
10364        _ => {}
10365    });
10366    if local_reads != 1 || tostring_reads != 1 {
10367        return pos;
10368    }
10369    // 6. build the fluent chain and swap it into the toString receiver.
10370    let mut chain = init;
10371    for ai in &appends {
10372        if let Stmt::ExprStmt(Expr::Method {
10373            owner: _,
10374            cls,
10375            name,
10376            desc,
10377            args,
10378            is_static,
10379            is_interface,
10380            is_special,
10381            is_super,
10382            is_dynamic,
10383            type_args,
10384            ..
10385        }) = &stmts[*ai]
10386        {
10387            chain = Expr::Method {
10388                owner: Some(Box::new(chain)),
10389                cls: cls.clone(),
10390                name: name.clone(),
10391                desc: desc.clone(),
10392                args: args.clone(),
10393                is_static: *is_static,
10394                is_interface: *is_interface,
10395                is_special: *is_special,
10396                is_super: *is_super,
10397                is_dynamic: *is_dynamic,
10398                type_args: type_args.clone(),
10399            };
10400        }
10401    }
10402    let mut replaced = false;
10403    walk_stmt_exprs(&mut stmts[pos], &mut |e| {
10404        deep_rewrite(e, &mut |x| {
10405            if let Expr::Method {
10406                owner: Some(o),
10407                name,
10408                args,
10409                ..
10410            } = x
10411            {
10412                if args.is_empty()
10413                    && &**name == "toString"
10414                    && matches!(&**o, Expr::Local { var, .. } if *var == sbv)
10415                {
10416                    **o = chain.clone();
10417                    replaced = true;
10418                }
10419            }
10420        });
10421    });
10422    if !replaced {
10423        return pos; // never remove without the swap — atomicity
10424    }
10425    // 7. drop the consumed statements (descending), adjust pos.
10426    let mut removed: Vec<usize> = appends.clone();
10427    removed.push(def_i);
10428    removed.sort_unstable();
10429    removed.dedup();
10430    for i in removed.iter().rev() {
10431        stmts.remove(*i);
10432    }
10433    pos - removed.len()
10434}
10435
10436pub fn fix_ctor_super_first(body: &mut Stmt, vt: &VarTable) {
10437    let Stmt::Block(stmts) = body else { return };
10438    if stmts.is_empty() || is_bare_ctor_call(stmts.first().unwrap()) {
10439        return;
10440    }
10441    // Top-level delegation call.
10442    if let Some(pos0) = stmts.iter().position(is_bare_ctor_call) {
10443        let pos = fold_sb_chain(stmts, pos0);
10444        let mut call = stmts[pos].clone();
10445        let prefix: Vec<&Stmt> = stmts[..pos].iter().collect();
10446        let mut consumed: Vec<(u32, usize)> = Vec::new();
10447        if !inline_ctor_arg_defs(&mut call, &prefix, vt, &mut consumed) {
10448            return;
10449        }
10450        stmts.remove(pos);
10451        // Drop the chain defs the inline consumed (all indices < pos) —
10452        // leaving them would DOUBLE-evaluate their (possibly impure)
10453        // inits. But ONLY for vars with no post-delegation use: dropping
10454        // such a var's DECLARATION makes ensure_declared re-add it at
10455        // the method top, i.e. BEFORE this() — a fresh "对this的调用必须
10456        // 是构造器中的第一个语句" (lark +587 the blind drop caused).
10457        let mut drop_idx: Vec<usize> = consumed
10458            .iter()
10459            .filter(|(v, _)| !var_occurs_in(&stmts[pos..], *v))
10460            .map(|(_, k)| *k)
10461            .collect();
10462        drop_idx.sort_unstable();
10463        drop_idx.dedup();
10464        for k in drop_idx.iter().rev() {
10465            stmts.remove(*k);
10466        }
10467        stmts.insert(0, call);
10468        return;
10469    }
10470    // The structurer can wrap the delegation in a bare nested block —
10471    // with the parameter null-checks AHEAD of it (weixin's Kotlin
10472    // intrinsics shape: `o.h(parcel, "source"); super();` at inner
10473    // positions 0/1, flattened to straight-line by cleanup AFTER this
10474    // pass, which is why the miss surfaced as 1150 weixin "对super的
10475    // 调用必须是构造器中的第一个语句"). Hoist the call from any
10476    // position whose predecessors are all straight-line statements; a
10477    // delegation behind control flow is the conditional-super family
10478    // (needs restructuring, not hoisting) and stays put.
10479    let mut found: Option<(usize, usize)> = None;
10480    for (i, st) in stmts.iter().enumerate() {
10481        if let Stmt::Block(inner) = st {
10482            if let Some(p) = inner.iter().position(is_bare_ctor_call) {
10483                if inner[..p]
10484                    .iter()
10485                    .all(|s| matches!(s, Stmt::LocalDef { .. } | Stmt::ExprStmt(_)))
10486                {
10487                    found = Some((i, p));
10488                    break;
10489                }
10490            }
10491        }
10492    }
10493    if let Some((i, p)) = found {
10494        let mut call = match &stmts[i] {
10495            Stmt::Block(inner) => inner[p].clone(),
10496            _ => unreachable!(),
10497        };
10498        let mut prefix: Vec<&Stmt> = stmts[..i].iter().collect();
10499        if let Stmt::Block(inner) = &stmts[i] {
10500            prefix.extend(inner[..p].iter());
10501        }
10502        let mut consumed: Vec<(u32, usize)> = Vec::new();
10503        if !inline_ctor_arg_defs(&mut call, &prefix, vt, &mut consumed) {
10504            return;
10505        }
10506        // Prefix index k maps to stmts[k] for k < i, inner[k - i] for
10507        // k >= i (all such k are < p). Suffix = the rest of the inner
10508        // block + the following top-level statements; a consumed def may
10509        // only drop when its var has no suffix use (declaration loss →
10510        // ensure_declared re-adds it BEFORE this()).
10511        // Compute the droppable set BEFORE mutating (borrow split).
10512        let mut ks: Vec<usize> = Vec::new();
10513        {
10514            let inner_tail: &[Stmt] = match &stmts[i] {
10515                Stmt::Block(inner) if p < inner.len() => &inner[p + 1..],
10516                _ => &[],
10517            };
10518            for (v, k) in &consumed {
10519                if !var_occurs_in(inner_tail, *v) && !var_occurs_in(&stmts[i + 1..], *v) {
10520                    ks.push(*k);
10521                }
10522            }
10523        }
10524        ks.sort_unstable();
10525        ks.dedup();
10526        if let Stmt::Block(inner) = &mut stmts[i] {
10527            inner.remove(p);
10528            for k in ks.iter().rev().filter(|k| **k >= i) {
10529                inner.remove(k - i);
10530            }
10531        }
10532        for k in ks.iter().rev().filter(|k| **k < i) {
10533            stmts.remove(*k);
10534        }
10535        stmts.insert(0, call);
10536    }
10537}
10538
10539/// A true constructor DELEGATION statement: `super(..)` (owner None) or
10540/// `this(..)` (owner This). An un-folded `new X; <init>` (lost
10541/// allocation — the ctor-fold-bug family) arrives as a `<init>` Method
10542/// with a NON-this owner and prints as `new X(..)`: it is not a
10543/// delegation and must neither satisfy nor anchor the super-first fixes
10544/// (v7.a$a2: a dead `new f(0,..)` init at position 0 made both hoists
10545/// believe the delegation was already first, leaving the real `super`
10546/// last — "对super的调用必须是构造器中的第一个语句").
10547fn is_delegation_expr(e: &Expr) -> bool {
10548    matches!(
10549        e,
10550        Expr::Method { name, is_special: true, owner, .. }
10551            if &**name == "<init>"
10552                && (owner.is_none() || matches!(owner.as_deref(), Some(Expr::This)))
10553    )
10554}
10555
10556pub(crate) fn is_bare_ctor_call(s: &Stmt) -> bool {
10557    matches!(s, Stmt::ExprStmt(e) if is_delegation_expr(e))
10558}
10559
10560/// Is the local ever ASSIGNED (write position: `x = ..`, `++x`)?
10561/// Distinct from a read count: a ctor's synthetic outer param is only
10562/// ever read; a param that gets written must stay declared.
10563pub(crate) fn local_is_written(body: &Stmt, var: u32) -> bool {
10564    let mut hit = false;
10565    visit_stmt_exprs_ro(body, &mut |e| {
10566        if hit {
10567            return;
10568        }
10569        visit_exprs(e, &mut |x| {
10570            if hit {
10571                return;
10572            }
10573            match x {
10574                Expr::Assign { target, .. }
10575                    if matches!(&**target, Expr::Local { var: v, .. } if *v == var) =>
10576                {
10577                    hit = true;
10578                }
10579                Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. }
10580                    if matches!(&**e, Expr::Local { var: v, .. } if *v == var) =>
10581                {
10582                    hit = true;
10583                }
10584                _ => {}
10585            }
10586        });
10587    });
10588    hit
10589}
10590
10591/// Source-form normalization for a non-static member-inner constructor.
10592/// The dex descriptor carries the synthetic outer instance as args[0]
10593/// (typed as the direct enclosing class); the emitter's qualified
10594/// `outer.new Inner(..)` / `this.new Inner(..)` sites pass it
10595/// implicitly and `super(..)` delegations drop it, so the signature
10596/// loses the param — and the body must stop referencing it: plain uses
10597/// become `Outer.this` (a `this.this$0 = p` assignment becomes
10598/// `this.this$0 = Outer.this`, exactly the runtime value), and
10599/// this()-delegations to `eligible` classes drop the leading param arg.
10600pub(crate) fn rewrite_inner_ctor_outer_param(
10601    body: &mut Stmt,
10602    param0: u32,
10603    outer_display: &str,
10604    outer_ty: &TypeRef,
10605    eligible: &[String],
10606) {
10607    // this()/super() delegations (and lost-alloc construction calls):
10608    // the leading arg IS the synthetic outer exactly when it is the
10609    // param itself.
10610    walk_stmt_exprs(body, &mut |e| {
10611        if let Expr::Method { name, cls, args, .. } = e {
10612            if &**name == "<init>"
10613                && eligible.iter().any(|c| c.as_str() == cls.as_ref())
10614                && args.first().is_some_and(|a| matches!(a, Expr::Local { var: v, .. } if *v == param0))
10615            {
10616                args.remove(0);
10617            }
10618        }
10619    });
10620    // Every remaining read of the param becomes `Outer.this`.
10621    let text = format!("{outer_display}.this");
10622    let ty = outer_ty.clone();
10623    walk_stmt_exprs(body, &mut |e| {
10624        deep_rewrite_reads(e, &mut |x| {
10625            if let Expr::Local { var: v, .. } = x {
10626                if *v == param0 {
10627                    *x = Expr::RawT(text.clone(), ty.clone());
10628                }
10629            }
10630        });
10631    });
10632}
10633
10634/// Ctors whose delegation is buried in control flow or behind arg
10635/// computations — the "对super的调用必须是构造器中的第一个语句" family
10636/// that plain hoisting cannot reach (weixin 264 / weibo 116 / reqable 71
10637/// / lark 62 after round 60): R8/Kotlin compute super-args conditionally
10638/// and dex legally invokes super mid-branch.
10639///
10640/// Shape B (branch merge): `prelude; if (c) {d1; super(A1); t1} else
10641/// {d2; super(A2); t2}; post` → one leading `super(..)` where each
10642/// differing arg becomes `c ? a1i : a2i`; branch remainders stay an
10643/// if/else AFTER the call; prelude statements move after it (nothing
10644/// may precede super in Java; bytecode pre-super work is local-only by
10645/// construction — dex forbids instance-field reads before the delegate).
10646///
10647/// Shape A (linear): `defs; super(A); rest` where `A` references the
10648/// def locals — plain hoisting (fix_ctor_super_first case 1) would move
10649/// the call above its argument definitions (forward refs). Inline the
10650/// referenced defs into the args first, drop the consumed defs, hoist.
10651/// Runs BEFORE fix_ctor_super_first and only fires when args reference
10652/// preceding locals, leaving the battle-tested plain hoist untouched
10653/// otherwise.
10654///
10655/// Inlining duplicates the def per use site, so a side-effecting def is
10656/// inlined only when its var's use count is provably preserved: a
10657/// single use in a merged arg (1 copy), or a single use in the
10658/// condition with ≤1 differing arg (the cond then appears in the `if`
10659/// plus one ternary — 2 copies of e.g. one Kotlin getter call, which
10660/// the original bytecode typically made twice anyway: `p.u() == null ?
10661/// .. : p.u().i()`). Pure defs inline freely. Every other shape aborts:
10662/// broken-but-faithful stays the status quo, wrong evaluation counts
10663/// would be worse.
10664/// Linear `def; delegation(args read def)` shapes. A plain hoist of the
10665/// delegation past its prefix would place the call ABOVE a definition
10666/// it reads — a forward reference (`super(context2, ..)` with `context2
10667/// = ..` below it). javac's attribution for the whole class then
10668/// collapses: the undefined identifier cascades into "non-static
10669/// super" and even bare `Object` resolution failures, poisoning every
10670/// later diagnostic in the file (weibo AppCompatTextView — the root of
10671/// a 4.1k-file Object cascade). The original source had the def's
10672/// expression INLINE in the delegation args; d8 computed it into a
10673/// register first. Inline the def into the args (the def is read
10674/// exactly once there — evaluation count preserved even for calls),
10675/// and when the register is REASSIGNED later (a fresh generation
10676/// sharing the slot), convert that first write into the declaration so
10677/// the var stays defined for its later readers. Any shape that cannot
10678/// be proven aborts untouched.
10679/// Count WRITE positions of `var` (assign / inc-dec targets) — the
10680/// shape-C validator needs exact counts, not just presence.
10681fn stmts_count_writes(stmts: &[Stmt], var: u32) -> usize {
10682    let mut n = 0usize;
10683    for s in stmts {
10684        visit_stmt_exprs_ro(s, &mut |e| {
10685            visit_exprs(e, &mut |x| match x {
10686                Expr::Assign { target, .. }
10687                    if matches!(&**target, Expr::Local { var: v, .. } if *v == var) =>
10688                {
10689                    n += 1;
10690                }
10691                Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. }
10692                    if matches!(&**e, Expr::Local { var: v, .. } if *v == var) =>
10693                {
10694                    n += 1;
10695                }
10696                _ => {}
10697            });
10698        });
10699    }
10700    n
10701}
10702
10703/// Collect every local WRITTEN in `stmts` — the complement inside a
10704/// ctor prefix is the param set (the only locals a pre-delegation arg
10705/// expression may read).
10706fn prefix_written_vars(stmts: &[Stmt]) -> jdc_core::FxHashSet<u32> {
10707    let mut set = jdc_core::FxHashSet::default();
10708    for s in stmts {
10709        let mut c = s.clone();
10710        walk_stmt_exprs(&mut c, &mut |e| {
10711            deep_rewrite(e, &mut |x| match x {
10712                Expr::Assign { target, .. } => {
10713                    if let Expr::Local { var, .. } = &**target {
10714                        set.insert(*var);
10715                    }
10716                }
10717                Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => {
10718                    if let Expr::Local { var, .. } = &**e {
10719                        set.insert(*var);
10720                    }
10721                }
10722                _ => {}
10723            });
10724        });
10725        if let Stmt::LocalDef { var, .. } = s {
10726            set.insert(*var);
10727        }
10728    }
10729    set
10730}
10731
10732/// Is `e` safe to evaluate INSIDE a ctor-delegation argument (before
10733/// this/super exists)? Params/consts/pure ops only — no instance-field
10734/// reads, no `this`, no captures.
10735fn pre_this_safe(e: &Expr, prefix_vars: &jdc_core::FxHashSet<u32>) -> bool {
10736    match e {
10737        Expr::Local { var, .. } => !prefix_vars.contains(var),
10738        Expr::Const(_) => true,
10739        Expr::Cast { e, .. } => pre_this_safe(e, prefix_vars),
10740        Expr::Bin { l, r, .. } => {
10741            pre_this_safe(l, prefix_vars) && pre_this_safe(r, prefix_vars)
10742        }
10743        Expr::Cond { c, t, f } => {
10744            pre_this_safe(c, prefix_vars)
10745                && pre_this_safe(t, prefix_vars)
10746                && pre_this_safe(f, prefix_vars)
10747        }
10748        Expr::Field { owner, is_static, .. } => {
10749            *is_static
10750                && owner
10751                    .as_ref()
10752                    .is_none_or(|o| pre_this_safe(o, prefix_vars))
10753        }
10754        Expr::Method {
10755            owner,
10756            args,
10757            is_static,
10758            ..
10759        } => {
10760            args.iter().all(|a| pre_this_safe(a, prefix_vars))
10761                && match owner {
10762                    Some(o) => pre_this_safe(o, prefix_vars),
10763                    None => *is_static,
10764                }
10765        }
10766        // Allocation in a delegation arg is legal Java before this()
10767        // (the ctor call touches no instance state of THIS class) — the
10768        // missing arm sent every `new Date()`-style Kotlin default
10769        // through `_ => false`, rejecting the whole fold (lark
10770        // Request$AppToApp this-not-first family, ~650 lark lines).
10771        // An inner-class construction needing an outer `this` carries
10772        // Expr::This in its args, which still fails below.
10773        Expr::New { args, .. } => args.iter().all(|a| pre_this_safe(a, prefix_vars)),
10774        Expr::NewArray { dims, init, .. } => {
10775            dims.iter().all(|d| pre_this_safe(d, prefix_vars))
10776                && init
10777                    .as_ref()
10778                    .map(|l| l.iter().all(|x| pre_this_safe(x, prefix_vars)))
10779                    .unwrap_or(true)
10780        }
10781        // Pure unary/instanceof/array reads.
10782        Expr::Un { e, .. } | Expr::InstanceOf { e, .. } => pre_this_safe(e, prefix_vars),
10783        Expr::ArrayIndex { array, index } => {
10784            pre_this_safe(array, prefix_vars) && pre_this_safe(index, prefix_vars)
10785        }
10786        _ => false,
10787    }
10788}
10789
10790/// `s` is exactly one plain assignment to local `v` (optionally wrapped
10791/// in a singleton Block): the branch shape of a Kotlin default-arg
10792/// override.
10793fn single_assign_to_v(s: &Stmt, v: u32) -> Option<Expr> {
10794    match s {
10795        Stmt::ExprStmt(Expr::Assign {
10796            target,
10797            op: AssignOp::Plain,
10798            value,
10799        }) if matches!(&**target, Expr::Local { var: vv, .. } if *vv == v) => {
10800            Some((**value).clone())
10801        }
10802        Stmt::Block(xs) if xs.len() == 1 => single_assign_to_v(&xs[0], v),
10803        _ => None,
10804    }
10805}
10806
10807/// Kotlin default-argument BRIDGE ctors (shape C of the delegation
10808/// family): `<init>(params.., int mask, DefaultConstructorMarker)`
10809/// computes each defaulted arg into a local — `v = p; if ((mask&bit)
10810/// != 0) { v = DEFAULT; }` — then delegates `this(.., v, ..)`. Java
10811/// allows NOTHING before the delegation, so the computations must fold
10812/// INTO the args (`this(.., (mask&bit) != 0 ? DEFAULT : p, ..)`).
10813/// Unfolded, the hoist passes lift the delegation above the raw local
10814/// reads: "找不到符号 变量 str2/v4/creationExtras2" — 2.5k root files
10815/// across weixin/lark/weibo (weixin MvvmObserverOwner$..Observer, lark
10816/// SimpleArrayMap/LongSparseArray). Per-var provability: exactly one
10817/// top-level base write, an optional single conditional override whose
10818/// branches are lone assignments, no other prefix touch of the var,
10819/// nothing after the delegation touches it, and the folded expression
10820/// is pre-this-safe. Unprovable vars stay untouched (partial folds are
10821/// strictly better — the rest was broken before too).
10822/// Flatten nested plain Blocks among the top-level statements. The
10823/// structurer hands ctor bodies as region-nested blocks (`Block([def,
10824/// Block([assign, if]), Block([delegate, return])])` — the Kotlin
10825/// bridge shape), and every delegation-repair pass scans the TOP level
10826/// only. Straight-line nesting flattens without semantics; control
10827/// flow lives inside If/While/etc. arms, never as a direct member.
10828pub(crate) fn flatten_top_blocks(stmts: &mut Vec<Stmt>) {
10829    fn rec(s: Stmt, out: &mut Vec<Stmt>) {
10830        match s {
10831            Stmt::Block(inner) => {
10832                for x in inner {
10833                    rec(x, out);
10834                }
10835            }
10836            other => out.push(other),
10837        }
10838    }
10839    if !stmts.iter().any(|s| matches!(s, Stmt::Block(_))) {
10840        return;
10841    }
10842    let mut out: Vec<Stmt> = Vec::with_capacity(stmts.len());
10843    for s in std::mem::take(stmts) {
10844        rec(s, &mut out);
10845    }
10846    *stmts = out;
10847}
10848
10849fn fold_default_arg_bridge(stmts: &mut Vec<Stmt>) -> bool {
10850    flatten_top_blocks(stmts);
10851    let Some(pos) = stmts.iter().position(is_bare_ctor_call) else {
10852        return false;
10853    };
10854    if pos == 0 {
10855        return false;
10856    }
10857    let Stmt::ExprStmt(Expr::Method { args, .. }) = &stmts[pos] else {
10858        return false;
10859    };
10860    let mut arg_vars: Vec<u32> = Vec::new();
10861    let mut reads = 0usize;
10862    for a in args {
10863        let mut c = a.clone();
10864        deep_rewrite_reads(&mut c, &mut |x| {
10865            if let Expr::Local { var, .. } = x {
10866                reads += 1;
10867                if !arg_vars.contains(var) {
10868                    arg_vars.push(*var);
10869                }
10870            }
10871        });
10872    }
10873    if arg_vars.is_empty() || reads != arg_vars.len() {
10874        return false; // a var read twice would duplicate evaluation
10875    }
10876    // The bridge ENDS at the delegation: nothing after may touch the
10877    // arg vars (reads OR writes).
10878    for &v in &arg_vars {
10879        if stmts_read_var(&stmts[pos + 1..], v) != 0
10880            || stmts_count_writes(&stmts[pos + 1..], v) != 0
10881        {
10882            return false;
10883        }
10884    }
10885    let prefix_vars = prefix_written_vars(&stmts[..pos]);
10886    let mut repl: Vec<(u32, Expr)> = Vec::new();
10887    let mut drop: Vec<usize> = Vec::new();
10888    for &v in &arg_vars {
10889        // Exact prefix shape for v.
10890        let mut base: Option<(usize, Expr)> = None;
10891        let mut decl: Option<usize> = None;
10892        let mut condf: Option<(usize, Expr, Option<Expr>, Option<Expr>)> = None;
10893        let mut ok = true;
10894        for (i, s) in stmts[..pos].iter().enumerate() {
10895            match s {
10896                Stmt::LocalDef { var, init, .. } if *var == v => match init {
10897                    None => {
10898                        if decl.is_some() {
10899                            ok = false;
10900                            break;
10901                        }
10902                        decl = Some(i);
10903                    }
10904                    Some(e) => {
10905                        if base.is_some() {
10906                            ok = false;
10907                            break;
10908                        }
10909                        base = Some((i, e.clone()));
10910                    }
10911                },
10912                Stmt::ExprStmt(Expr::Assign {
10913                    target,
10914                    op: AssignOp::Plain,
10915                    value,
10916                }) if matches!(&**target, Expr::Local { var: vv, .. } if *vv == v) => {
10917                    if base.is_some() {
10918                        ok = false;
10919                        break;
10920                    }
10921                    base = Some((i, (**value).clone()));
10922                }
10923                Stmt::If {
10924                    cond,
10925                    then_stmt,
10926                    else_stmt,
10927                } => {
10928                    let t = single_assign_to_v(then_stmt, v);
10929                    let f = else_stmt.as_ref().and_then(|b| single_assign_to_v(b, v));
10930                    let then_touches = t.is_some()
10931                        || stmts_read_var(std::slice::from_ref(&**then_stmt), v) != 0
10932                        || stmts_count_writes(std::slice::from_ref(&**then_stmt), v) != 0;
10933                    let else_touches = else_stmt.as_ref().is_some_and(|b| {
10934                        f.is_some()
10935                            || stmts_read_var(std::slice::from_ref(b), v) != 0
10936                            || stmts_count_writes(std::slice::from_ref(b), v) != 0
10937                    });
10938                    if !then_touches && !else_touches {
10939                        continue; // unrelated if
10940                    }
10941                    // Both branches must be lone assignments (a branch
10942                    // with its own delegation/return is the conditional-
10943                    // super machinery's shape, not this one), the cond
10944                    // must not read v, and only one override per var.
10945                    if condf.is_some()
10946                        || base.is_none()
10947                        || (then_touches && t.is_none())
10948                        || (else_touches && f.is_none())
10949                    {
10950                        ok = false;
10951                        break;
10952                    }
10953                    let cond_stmt = Stmt::ExprStmt(cond.clone());
10954                    if stmts_read_var(std::slice::from_ref(&cond_stmt), v) != 0 {
10955                        ok = false;
10956                        break;
10957                    }
10958                    condf = Some((i, cond.clone(), t, f));
10959                }
10960                _ => {}
10961            }
10962        }
10963        if !ok {
10964            continue;
10965        }
10966        let Some((bi, be)) = base else { continue };
10967        // Exact write/read accounting: base (+then/else) writes, zero
10968        // prefix reads.
10969        let want_writes = 1
10970            + condf
10971                .as_ref()
10972                .map(|(_, _, t, f)| t.is_some() as usize + f.is_some() as usize)
10973                .unwrap_or(0);
10974        if stmts_count_writes(&stmts[..pos], v) != want_writes
10975            || stmts_read_var(&stmts[..pos], v) != 0
10976        {
10977            continue;
10978        }
10979        let rep = match condf {
10980            Some((ci, c, t, f)) => {
10981                drop.push(ci);
10982                match (t, f) {
10983                    (Some(t), Some(f)) => Expr::Cond {
10984                        c: Box::new(c),
10985                        t: Box::new(t),
10986                        f: Box::new(f),
10987                    },
10988                    (Some(t), None) => Expr::Cond {
10989                        c: Box::new(c),
10990                        t: Box::new(t),
10991                        f: Box::new(be.clone()),
10992                    },
10993                    (None, Some(f)) => Expr::Cond {
10994                        c: Box::new(c),
10995                        t: Box::new(be.clone()),
10996                        f: Box::new(f),
10997                    },
10998                    (None, None) => unreachable!("condf requires a branch assign"),
10999                }
11000            }
11001            None => be.clone(),
11002        };
11003        if !pre_this_safe(&rep, &prefix_vars) {
11004            continue;
11005        }
11006        drop.push(bi);
11007        if let Some(d) = decl {
11008            drop.push(d);
11009        }
11010        repl.push((v, rep));
11011    }
11012    if repl.is_empty() {
11013        return false;
11014    }
11015    if let Stmt::ExprStmt(Expr::Method { args, .. }) = &mut stmts[pos] {
11016        for a in args.iter_mut() {
11017            deep_rewrite_reads(a, &mut |x| {
11018                if let Expr::Local { var, .. } = x {
11019                    if let Some((_, e)) = repl.iter().find(|(vv, _)| vv == var) {
11020                        *x = e.clone();
11021                    }
11022                }
11023            });
11024        }
11025    }
11026    drop.sort_unstable();
11027    drop.dedup();
11028    for i in drop.into_iter().rev() {
11029        stmts.remove(i);
11030    }
11031    true
11032}
11033
11034/// Rewrite static member references on Kotlin multi-file facade PARTS
11035/// (`StringsKt__StringsKt.trim(..)`) to the public facade the parts are
11036/// compiled into (`StringsKt.trim(..)`): the parts are package-private,
11037/// so every cross-package call site is "在kotlin.text中不是公共的; 无法
11038/// 从外部程序包中对其进行访问" (lark 1.4k root files; weibo's 8.4k-line
11039/// Kt cluster). Static members resolve through the facade by
11040/// inheritance (the map is gated on the extends chain).
11041pub fn rewrite_kotlin_facades(body: &mut Stmt, pool: &crate::DexPool) {
11042    let map = pool.kotlin_facade_map();
11043    if map.is_empty() {
11044        return;
11045    }
11046    walk_stmt_exprs(body, &mut |e| {
11047        match e {
11048            Expr::Method {
11049                cls,
11050                name,
11051                desc,
11052                is_static,
11053                ..
11054            } if *is_static => {
11055                if let Some((base, cover)) = map.get(cls.as_ref()) {
11056                    let hit = match cover {
11057                        None => true,
11058                        Some(c) => c.0.contains(&(name.to_string(), desc.to_string())),
11059                    };
11060                    if hit {
11061                        *cls = std::sync::Arc::from(base.as_str());
11062                    }
11063                }
11064            }
11065            Expr::Field {
11066                cls,
11067                name,
11068                is_static,
11069                ..
11070            } if *is_static => {
11071                if let Some((base, cover)) = map.get(cls.as_ref()) {
11072                    let hit = match cover {
11073                        None => true,
11074                        Some(c) => c.1.contains(name.as_ref()),
11075                    };
11076                    if hit {
11077                        *cls = std::sync::Arc::from(base.as_str());
11078                    }
11079                }
11080            }
11081            _ => {}
11082        }
11083    });
11084}
11085
11086/// Kotlin default-arg bridge fold for ENUM ctors: the synthetic
11087/// `(String, int, .., int, DefaultConstructorMarker)` ctor computes
11088/// defaulted args then chains `this(..)` mid-body — Java requires the
11089/// delegation first (对this的调用必须是构造器中的第一个语句, lark's
11090/// UserCustomStatusExtraParams$* family, 752 lines). Same fold the
11091/// class-ctor path runs; the enum branch of the pipeline used to skip
11092/// it (only strip_enum_ctor_super ran there).
11093pub fn fold_enum_default_arg_bridge(body: &mut Stmt) {
11094    let Stmt::Block(stmts) = body else { return };
11095    fold_default_arg_bridge(stmts);
11096}
11097
11098pub fn fix_ctor_delegation_arg_defs(body: &mut Stmt) {
11099    let Stmt::Block(stmts) = body else { return };
11100    // Shape C first: the Kotlin default-arg bridge (computations +
11101    // conditional overrides folded into the delegation args).
11102    fold_default_arg_bridge(stmts);
11103    // Only the top-level linear shape; the first statement needs no
11104    // repair and control flow ahead of the delegation belongs to the
11105    // conditional-super machinery.
11106    let Some(pos) = stmts.iter().position(is_bare_ctor_call) else {
11107        return;
11108    };
11109    if pos == 0 {
11110        return;
11111    }
11112    // Vars read by the delegation args, and the total read count (a var
11113    // read twice would duplicate a call evaluation on inline).
11114    let mut arg_vars: Vec<u32> = Vec::new();
11115    let mut total_reads = 0usize;
11116    {
11117        let Stmt::ExprStmt(Expr::Method { args, .. }) = &stmts[pos] else {
11118            return;
11119        };
11120        for a in args {
11121            let mut c = a.clone();
11122            deep_rewrite_reads(&mut c, &mut |x| {
11123                if let Expr::Local { var: v, .. } = x {
11124                    total_reads += 1;
11125                    if !arg_vars.contains(v) {
11126                        arg_vars.push(*v);
11127                    }
11128                }
11129            });
11130        }
11131    }
11132    if arg_vars.is_empty() || total_reads != arg_vars.len() {
11133        return;
11134    }
11135    // Vars DEFINED anywhere in the prefix (for the init-referenced
11136    // check below — an inlined init may not itself read prefix defs).
11137    let prefix_defs: Vec<u32> = stmts[..pos]
11138        .iter()
11139        .filter_map(|s| match s {
11140            Stmt::LocalDef { var, init: Some(_), .. } => Some(*var),
11141            _ => None,
11142        })
11143        .collect();
11144    // Per referenced var: exactly one prefix def, no other prefix
11145    // reader between def and delegation, the def's init reads no
11146    // prefix-def var, and the first post-delegation use is a top-level
11147    // write (becomes the declaration) or nothing at all.
11148    let mut plan: Vec<(u32, usize, Option<usize>)> = Vec::new();
11149    for v in arg_vars {
11150        let defs: Vec<usize> = stmts[..pos]
11151            .iter()
11152            .enumerate()
11153            .filter(|(i, _)| {
11154                matches!(&stmts[*i], Stmt::LocalDef { var, init: Some(_), .. } if *var == v)
11155            })
11156            .map(|(i, _)| i)
11157            .collect();
11158        if defs.len() > 1 {
11159            return; // ambiguous: multiple prefix defs
11160        }
11161        let Some(&q) = defs.first() else {
11162            continue; // a param: nothing to inline for this var
11163        };
11164        let init = match &stmts[q] {
11165            Stmt::LocalDef { init: Some(e), .. } => e.clone(),
11166            _ => return,
11167        };
11168        if stmts_read_var(&stmts[q + 1..pos], v) != 0 {
11169            return; // another reader between the def and the delegation
11170        }
11171        // The inlined init may not read a prefix-def var either — the
11172        // delegation lands ABOVE those defs after the hoist.
11173        let mut init_vars: Vec<u32> = Vec::new();
11174        deep_rewrite_reads(&mut { init.clone() }, &mut |x| {
11175            if let Expr::Local { var: vv, .. } = x {
11176                if !init_vars.contains(vv) {
11177                    init_vars.push(*vv);
11178                }
11179            }
11180        });
11181        if init_vars.iter().any(|iv| prefix_defs.contains(iv)) {
11182            return;
11183        }
11184        match stmts[pos + 1..]
11185            .iter()
11186            .position(|s| {
11187                matches!(s, Stmt::ExprStmt(Expr::Assign { target, .. })
11188                    if matches!(&**target, Expr::Local { var: vv, .. } if *vv == v))
11189            }) {
11190            Some(w) => {
11191                // Reads before the re-declaration would be dangling.
11192                if stmts_read_var(&stmts[pos + 1..pos + 1 + w], v) != 0 {
11193                    return;
11194                }
11195                plan.push((v, q, Some(pos + 1 + w)));
11196            }
11197            None => {
11198                // No later write: the def must be dead after inline.
11199                if stmts_read_var(&stmts[pos + 1..], v) != 0 {
11200                    return;
11201                }
11202                plan.push((v, q, None));
11203            }
11204        }
11205    }
11206    if plan.is_empty() {
11207        return;
11208    }
11209    // Inline the def inits into the delegation args.
11210    let values: Vec<(u32, Expr)> = plan
11211        .iter()
11212        .map(|(_v, q, _)| match &stmts[*q] {
11213            Stmt::LocalDef { var, init: Some(e), .. } => (*var, e.clone()),
11214            _ => unreachable!("plan entries carry a LocalDef with init"),
11215        })
11216        .collect();
11217    if let Stmt::ExprStmt(Expr::Method { args, .. }) = &mut stmts[pos] {
11218        for a in args {
11219            deep_rewrite_reads(a, &mut |x| {
11220                if let Expr::Local { var: v, .. } = x {
11221                    if let Some((_, e)) = values.iter().find(|(vv, _)| vv == v) {
11222                        *x = e.clone();
11223                    }
11224                }
11225            });
11226        }
11227    }
11228    // Convert the first writes into declarations BEFORE removing defs
11229    // (removals shift indices); then remove the defs in descending
11230    // order.
11231    for (v, _, w) in &plan {
11232        if let Some(w) = w {
11233            if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = &mut stmts[*w] {
11234                if matches!(&**target, Expr::Local { var: vv, .. } if vv == v) {
11235                    let taken = std::mem::replace(&mut **value, Expr::Const(ConstVal::Null));
11236                    stmts[*w] = Stmt::LocalDef {
11237                        var: *v,
11238                        init: Some(taken),
11239                        is_final: false,
11240                        force_type: false,
11241                    };
11242                }
11243            }
11244        }
11245    }
11246    let mut order: Vec<usize> = plan.iter().map(|(_, q, _)| *q).collect();
11247    order.sort_unstable();
11248    order.reverse();
11249    for q in order {
11250        stmts.remove(q);
11251    }
11252}
11253
11254/// An INT-typed method returning a boolean value: the register held the
11255/// boolean test result in one generation and the `if (v) 1 else 0` int
11256/// in another; booleanize picks the boolean generation and the plain
11257/// `return v;` breaks ("boolean无法转换为int", weixin Handle
11258/// onPausableTransaction — the source was `if (v) 1 else 0`). Wrap the
11259/// return in the recovering conditional.
11260/// Mirror of fix_int_returns: a BOOLEAN method returning a numeric-
11261/// typed value (register reuse left an int generation) wraps it
11262/// `x != 0` — the dex branch encoding, and the only legal Java form
11263/// (kh5/a `return v116_g53;` int无法转换为boolean family).
11264pub fn fix_bool_returns(vt: &VarTable, body: &mut Stmt) {
11265    walk_mut_deep(body, &mut |st| {
11266        if let Stmt::Return(Some(e)) = st {
11267            let numeric = match &*e {
11268                Expr::Local { var, ty } => {
11269                    let t = if (*var as usize) < vt.vars.len() {
11270                        vt.var(*var).ty.erased()
11271                    } else {
11272                        ty.erased()
11273                    };
11274                    matches!(
11275                        t,
11276                        JavaType::Int
11277                            | JavaType::Long
11278                            | JavaType::Short
11279                            | JavaType::Byte
11280                            | JavaType::Char
11281                    )
11282                }
11283                other => matches!(
11284                    other.type_ref().erased(),
11285                    JavaType::Int | JavaType::Long | JavaType::Short | JavaType::Byte
11286                        | JavaType::Char
11287                ),
11288            };
11289            if numeric {
11290                let taken = e.clone();
11291                *e = Expr::Bin {
11292                    op: BinOp::Ne,
11293                    l: Box::new(taken),
11294                    r: Box::new(Expr::Const(ConstVal::Int(0))),
11295                    ty: Some(TypeRef::J(JavaType::Boolean)),
11296                };
11297            }
11298        }
11299    });
11300}
11301
11302pub fn fix_int_returns(vt: &VarTable, body: &mut Stmt) {
11303    walk_mut_deep(body, &mut |st| {
11304        if let Stmt::Return(Some(e)) = st {
11305            let is_bool = match e {
11306                Expr::Local { var, .. } => {
11307                    (*var as usize) < vt.vars.len()
11308                        && vt.vars[*var as usize].ty.erased() == JavaType::Boolean
11309                }
11310                Expr::Method { desc, .. } => desc.ret == JavaType::Boolean,
11311                _ => false,
11312            };
11313            if is_bool {
11314                *e = Expr::Cond {
11315                    c: Box::new(e.clone()),
11316                    t: Box::new(Expr::Const(ConstVal::Int(1))),
11317                    f: Box::new(Expr::Const(ConstVal::Int(0))),
11318                };
11319            }
11320        }
11321    });
11322}
11323
11324/// Generation splitting: when an assignment's value type is
11325/// INCOMPATIBLE with the var's declared type, mint a FRESH var for the
11326/// new generation instead of widening (widening was measured twice as
11327/// net-negative — every use-site cast gap re-triggers javac's
11328/// error-type contagion). Reads after the split point follow the
11329/// active generation; control-flow joins intersect; loops run a
11330/// 3-round fixpoint so the back-edge carries the tail generation to
11331/// the head reads (the loop-state-tuple shape, qf5/v0's
11332/// `u0 = (qf5.u0) get;` … `u0 = looper2;`).
11333///
11334/// Split condition (reference incompatibility or a primitive/value
11335/// mix — the instanceof-corruption shape): both specific references of
11336/// different classes, or prim→ref. NOT numeric-vs-numeric (promotion
11337/// handles those) and NOT Object targets (assignable — the narrowing
11338/// cast pass covers the value side).
11339pub fn split_generations(vt: &mut VarTable, body: &mut Stmt, pool: &DexPool) {
11340    let mut counter: u32 = 0;
11341    let mut gen: std::collections::HashMap<u32, u32> = std::collections::HashMap::default();
11342    split_walk_stmt(body, vt, &mut gen, &mut counter, pool);
11343}
11344
11345/// Should the assignment `target = value` split the target?
11346fn split_needed(vt: &VarTable, target: u32, value: &Expr, pool: &DexPool) -> bool {
11347    let n = vt.vars.len();
11348    if target as usize >= n {
11349        return false;
11350    }
11351    let declared = vt.vars[target as usize].ty.erased();
11352    let vt_is_ref = |t: &JavaType| matches!(t, JavaType::Object(_) | JavaType::Array(_));
11353    // Locals consult the VarTable: the embedded ty is a lift-time
11354    // snapshot, stale after booleanize converts a copy SOURCE (the
11355    // post-booleanize re-split below relies on seeing `v131` as the
11356    // boolean it became). Bitwise Or/And/Xor over boolean operands
11357    // carry the same staleness at the EXPRESSION level (dex lowered
11358    // Kotlin's non-short-circuit `or` to int `|` — the embedded Bin ty
11359    // stays Int after booleanize converts the operands): `int v18 =
11360    // delete | delete2` must split the target generation.
11361    fn side_bool(e: &Expr, vt: &VarTable) -> bool {
11362        match e {
11363            Expr::Local { var, .. } if (*var as usize) < vt.vars.len() => {
11364                matches!(vt.var(*var).ty.erased(), JavaType::Boolean)
11365            }
11366            Expr::Const(ConstVal::Int(0 | 1)) => true,
11367            Expr::Bin { op: BinOp::Or | BinOp::And | BinOp::Xor, l, r, .. } => {
11368                side_bool(l, vt) && side_bool(r, vt)
11369            }
11370            other => matches!(other.type_ref().erased(), JavaType::Boolean),
11371        }
11372    }
11373    let value_ty = match value {
11374        Expr::Local { var, ty } if (*var as usize) < vt.vars.len() => vt.var(*var).ty.erased(),
11375        Expr::Bin {
11376            op: BinOp::Or | BinOp::And | BinOp::Xor,
11377            l,
11378            r,
11379            ..
11380        } if side_bool(l, vt) && side_bool(r, vt) => JavaType::Boolean,
11381        other => other.type_ref().erased(),
11382    };
11383    if vt_is_ref(&declared) && vt_is_ref(&value_ty) {
11384        // Both references: split only when incompatible — different
11385        // classes where neither side is Object AND the value is not a
11386        // SUBTYPE of the declared type. An assignable store needs no
11387        // fresh generation; splitting one minted an orphan at an
11388        // if-join whose sibling arm kept the base (rename_gen_back's
11389        // primitive-only gate refused the undo, drop_dead_locals then
11390        // stripped the orphan to a bare allocation): pq0/g's bridge
11391        // `map2 = new LinkedHashMap()` against the HashMap-typed phi
11392        // rendered `new LinkedHashMap();` and the arm lost its store.
11393        let obj = JavaType::Object("java/lang/Object".into());
11394        if declared == value_ty || declared == obj || value_ty == obj {
11395            return false;
11396        }
11397        // A FRESH ALLOCATION whose class relation to the declared type
11398        // is UNKNOWABLE (a framework↔framework pair — LinkedHashMap
11399        // into a HashMap-typed phi): the dex stored the instance into
11400        // this register flow, so the store was verifier-legal; the
11401        // split cannot know better and orphaned the generation (the
11402        // if-join undo is primitive-only; the dropper stripped the
11403        // orphan to a bare `new LinkedHashMap();` and pq0/g's bridge
11404        // lost the else-arm store, killing the whole branched-super
11405        // merge). Pool-known pairs KEEP the split: the generation's
11406        // precise type is what downstream subtype sinks need (weibo
11407        // `v7 = new $reportWhenComplete$1(..)` into a ContinuationImpl
11408        // phi — exempting it made `v9($1) = v7` inconvertible, +50).
11409        if let (JavaType::Object(v), JavaType::Object(d)) = (&value_ty, &declared) {
11410            if matches!(value, Expr::New { .. })
11411                && (pool.get(v).is_none() || pool.get(d).is_none())
11412            {
11413                return false;
11414            }
11415        }
11416        true
11417    } else {
11418        // Primitive/reference mix: the value generation has a kind the
11419        // target can never accept — split (the declared type stays with
11420        // the old generation's readers).
11421        if vt_is_ref(&declared) != vt_is_ref(&value_ty) {
11422            return true;
11423        }
11424        // Boolean/numeric mix: `boolean` and `int` have NO cast between
11425        // them in Java, so a register reused across the kinds can only
11426        // be expressed as two generations (weixin ConstraintLayout
11427        // `boolean v60; int v56; v60 = v56;` — 9.5k bool↔int lines once
11428        // the obscuring suppression lifted). 0/1 constants are the
11429        // boolean encoding, not a numeric generation — never split on
11430        // those (booleanize owns them).
11431        let d_bool = matches!(declared, JavaType::Boolean);
11432        let v_bool = matches!(value_ty, JavaType::Boolean);
11433        let zero_one = matches!(value, Expr::Const(ConstVal::Int(0 | 1)));
11434        d_bool != v_bool && !zero_one
11435    }
11436}
11437
11438fn rewrite_gen_reads(e: &mut Expr, gen: &std::collections::HashMap<u32, u32>, vt: &VarTable) {
11439    deep_rewrite(e, &mut |x| {
11440        if let Expr::Local { var, ty } = x {
11441            if let Some(&to) = gen.get(var) {
11442                if to != *var {
11443                    *var = to;
11444                    *ty = vt.var(to).ty.clone();
11445                }
11446            }
11447        }
11448    });
11449}
11450
11451fn do_split(
11452    vt: &mut VarTable,
11453    target: &mut Expr,
11454    old: u32,
11455    value_ty: &JavaType,
11456    gen: &mut std::collections::HashMap<u32, u32>,
11457    counter: &mut u32,
11458) {
11459    *counter += 1;
11460    let info = &vt.vars[old as usize];
11461    let id = vt.vars.len() as u32;
11462    let name = format!("{}_g{}", info.name, counter);
11463    let slot = info.slot;
11464    // A split generation is a fresh LOCAL, not a parameter — even when
11465    // the parent register is one (the param identity stays on the
11466    // original var id; the generation only carries a later write). The
11467    // inherited flag made booleanize (and every other param-skip pass)
11468    // permanently ignore param-register generations: weixin v2/i's
11469    // `int v264_g160_g7 = !v200` with `!= 0` reads (bool→int assign
11470    // family, 2.8k lines).
11471    let is_param = false;
11472    vt.vars.push(jdc_core::var::VarInfo {
11473        id,
11474        slot,
11475        name,
11476        ty: TypeRef::J(value_ty.clone()),
11477        is_param,
11478        range_start: 0,
11479        range_end: u16::MAX,
11480        synthetic_name: true,
11481    });
11482    while vt.by_slot.len() <= slot as usize {
11483        vt.by_slot.push(Vec::new());
11484    }
11485    vt.by_slot[slot as usize].push((0, u16::MAX, id));
11486    gen.insert(old, id);
11487    if let Expr::Local { var, ty, .. } = target {
11488        *var = id;
11489        *ty = TypeRef::J(value_ty.clone());
11490    }
11491}
11492
11493/// Undo a DIVERGED generation split: rewrite every occurrence of the
11494/// freshly-minted generation `from` back to the pre-if var `to` within
11495/// `s` (both read positions and assignment targets, plus LocalDef var
11496/// fields). At a control-flow join only splits BOTH branches produced
11497/// survive (the phi merges on the shared var); a gen one branch minted
11498/// alone does not. But `do_split` already rewrote that branch's write
11499/// target IN PLACE, so without this undo the branch stores into a
11500/// write-only orphan gen that no confluence reader ever reads — the
11501/// value is silently lost AND the orphan renders undeclared (kt5.b
11502/// Kotlin default-arg bridge: `v18_g1 = p6x` in then vs `v18 = false`
11503/// in else, `this(.., v18, ..)` reads only else's value).
11504fn rename_gen_back(s: &mut Stmt, from: u32, to: u32, vt: &VarTable) {
11505    if from == to || (to as usize) >= vt.vars.len() {
11506        return;
11507    }
11508    let to_ty = vt.vars[to as usize].ty.clone();
11509    walk_mut_deep(s, &mut |st| {
11510        if let Stmt::LocalDef { var, .. } = st {
11511            if *var == from {
11512                *var = to;
11513            }
11514        }
11515    });
11516    rewrite_exprs(s, &mut |e| {
11517        deep_rewrite(e, &mut |x| {
11518            if let Expr::Local { var, ty } = x {
11519                if *var == from {
11520                    *var = to;
11521                    *ty = to_ty.clone();
11522                }
11523            }
11524        });
11525    });
11526}
11527
11528/// Remove an undone generation's `by_slot` segment entry so slot-based
11529/// resolution can never hand back the now-unreferenced gen id (its
11530/// VarInfo stays — ids index `vars` — but nothing may select it).
11531fn drop_gen_slot(vt: &mut VarTable, dead: u32) {
11532    if (dead as usize) >= vt.vars.len() {
11533        return;
11534    }
11535    let slot = vt.vars[dead as usize].slot as usize;
11536    if let Some(seg) = vt.by_slot.get_mut(slot) {
11537        seg.retain(|&(_, _, id)| id != dead);
11538    }
11539}
11540
11541/// Type gate for `rename_gen_back`: merging gen `v` back onto base `k`
11542/// is only safe when the base can carry the gen's value — identical
11543/// types, or BOTH primitives (the bool/int register-reuse family that
11544/// booleanize + the cast fixers reconcile; the Kotlin bridge case,
11545/// kt5.b `int v18` receiving `boolean p6x`). A reference-vs-primitive
11546/// or cross-class merge would forge `String k = <boolean>`-style
11547/// assignments that never compile (the lark/weibo/reqable regression
11548/// of the ungated first cut: +8.4k `不兼容的类型`).
11549fn undo_type_safe(vt: &VarTable, k: u32, v: u32) -> bool {
11550    if (k as usize) >= vt.vars.len() || (v as usize) >= vt.vars.len() {
11551        return false;
11552    }
11553    let kt = vt.vars[k as usize].ty.erased();
11554    let vt_ty = vt.vars[v as usize].ty.erased();
11555    if kt == vt_ty {
11556        return true;
11557    }
11558    let is_prim = |t: &JavaType| {
11559        matches!(
11560            t,
11561            JavaType::Int
11562                | JavaType::Long
11563                | JavaType::Short
11564                | JavaType::Byte
11565                | JavaType::Char
11566                | JavaType::Float
11567                | JavaType::Double
11568                | JavaType::Boolean
11569        )
11570    };
11571    is_prim(&kt) && is_prim(&vt_ty)
11572}
11573
11574fn split_walk_stmt(
11575    st: &mut Stmt,
11576    vt: &mut VarTable,
11577    gen: &mut std::collections::HashMap<u32, u32>,
11578    counter: &mut u32,
11579    pool: &DexPool,
11580) {
11581    match st {
11582        Stmt::Block(v) => {
11583            for x in v.iter_mut() {
11584                split_walk_stmt(x, vt, gen, counter, pool);
11585            }
11586        }
11587        Stmt::ExprStmt(e) => {
11588            rewrite_gen_reads(e, gen, vt);
11589            if let Expr::Assign {
11590                target,
11591                value,
11592                op: AssignOp::Plain,
11593                ..
11594            } = e
11595            {
11596                if let Expr::Local { var, .. } = &**target {
11597                    let old = *var;
11598                    if split_needed(vt, old, value, pool) {
11599                        let value_ty = value.type_ref().erased();
11600                        do_split(vt, target, old, &value_ty, gen, counter);
11601                    }
11602                }
11603            }
11604        }
11605        Stmt::LocalDef { var, init, .. } => {
11606            if let Some(e) = init {
11607                rewrite_gen_reads(e, gen, vt);
11608                if split_needed(vt, *var, e, pool) {
11609                    // The DEF mints its own generation: retarget the def
11610                    // to a fresh var (the bare decl of the ORIGINAL stays
11611                    // for its earlier readers).
11612                    let value_ty = e.type_ref().erased();
11613                    let old = *var;
11614                    let mut fake = Expr::Local {
11615                        var: old,
11616                        ty: vt.vars[old as usize].ty.clone(),
11617                    };
11618                    do_split(vt, &mut fake, old, &value_ty, gen, counter);
11619                    if let Expr::Local { var: nv, .. } = fake {
11620                        *var = nv;
11621                    }
11622                }
11623            }
11624        }
11625        Stmt::If {
11626            cond,
11627            then_stmt,
11628            else_stmt,
11629            ..
11630        } => {
11631            rewrite_gen_reads(cond, gen, vt);
11632            let then_floor = vt.vars.len() as u32;
11633            let mut g_then = gen.clone();
11634            split_walk_stmt(then_stmt, vt, &mut g_then, counter, pool);
11635            let else_floor = vt.vars.len() as u32;
11636            let mut g_else = gen.clone();
11637            if let Some(e) = else_stmt {
11638                split_walk_stmt(e, vt, &mut g_else, counter, pool);
11639            }
11640            // Undo splits that DIVERGED at this join (see
11641            // rename_gen_back), with TWO gates learned the hard way
11642            // (first cut regressed lark +8.4k):
11643            //  1. the SIBLING KEPT THE BASE — `g_else[k]` equals the
11644            //     entry mapping, i.e. only this branch minted a fresh
11645            //     gen for k. When both branches split, each orphan is
11646            //     declared by ensure_declared at its own value type and
11647            //     compiles; renaming them back onto the base would
11648            //     force cross-kind assignments (`int k = <boolean>`).
11649            //  2. the fresh gen is WRITE-ONLY in the branch — a gen
11650            //     read inside its branch renders fine on its own.
11651            // The gated shape is the Kotlin default-arg bridge phi
11652            // loss: then `v_g = p6x` / else `v = false` / confluence
11653            // reads v — the then value was silently dropped AND v_g
11654            // rendered undeclared (kt5.b). Undo restores the clean
11655            // diamond so fold_default_arg_bridge can fire.
11656            // Candidates first (the read-free conditions), then ONE
11657            // early-exit probe for the small candidate set — the old
11658            // read_counts_all rebuilt a HashMap over the WHOLE branch
11659            // subtree per diverged If (quadratic in nesting depth;
11660            // together with the split walk it was ~1/3 of worker CPU on
11661            // the QQ sample profile). The map was only ever consulted
11662            // as `reads(v) == 0` for these candidates.
11663            let mut undo_then: Vec<(u32, u32)> = g_then
11664                .iter()
11665                .filter(|(&k, &v)| {
11666                    v >= then_floor
11667                        && v < else_floor
11668                        && g_else.get(&k).copied() == gen.get(&k).copied()
11669                        && undo_type_safe(vt, k, v)
11670                })
11671                .map(|(&k, &v)| (v, k))
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            for &(v, k) in &undo_then {
11686                rename_gen_back(then_stmt, v, k, vt);
11687                drop_gen_slot(vt, v);
11688            }
11689            if let Some(e) = else_stmt.as_deref_mut() {
11690                let mut undo_else: Vec<(u32, u32)> = g_else
11691                    .iter()
11692                    .filter(|(&k, &v)| {
11693                        v >= else_floor
11694                            && g_then.get(&k).copied() == gen.get(&k).copied()
11695                            && undo_type_safe(vt, k, v)
11696                    })
11697                    .map(|(&k, &v)| (v, k))
11698                    .collect();
11699                if (vt.vars.len() as u32) > else_floor && !undo_else.is_empty() {
11700                    let vars: Vec<u32> = undo_else.iter().map(|&(v, _)| v).collect();
11701                    let mut hits = vec![false; vars.len()];
11702                    mark_read_vars(std::slice::from_ref(&*e), &vars, &mut hits);
11703                    let mut i = 0usize;
11704                    undo_else.retain(|_| {
11705                        let keep = !hits[i];
11706                        i += 1;
11707                        keep
11708                    });
11709                }
11710                undo_else.sort_unstable_by_key(|&(v, _)| std::cmp::Reverse(v));
11711                for &(v, k) in &undo_else {
11712                    rename_gen_back(e, v, k, vt);
11713                    drop_gen_slot(vt, v);
11714                }
11715            }
11716            // Join: keep only splits BOTH paths produced (an absent else
11717            // is a fall-through path that keeps the pre-if generation).
11718            gen.retain(|k, v| g_then.get(k) == Some(v) && g_else.get(k) == Some(v));
11719        }
11720        Stmt::While { cond, body, .. } | Stmt::DoWhile { cond, body, .. } => {
11721            // 3-round fixpoint: round N walks the cond+body with the
11722            // accumulated gen — the back-edge carries the tail
11723            // generation into the head reads of the next round.
11724            let entry = gen.clone();
11725            for _ in 0..3 {
11726                rewrite_gen_reads(cond, gen, vt);
11727                split_walk_stmt(body, vt, gen, counter, pool);
11728            }
11729            // After the loop the pre-loop generation is what a
11730            // zero-iteration execution left — conservative.
11731            *gen = entry;
11732        }
11733        Stmt::For { init, body, .. } => {
11734            for x in init.iter_mut() {
11735                split_walk_stmt(x, vt, gen, counter, pool);
11736            }
11737            let entry = gen.clone();
11738            for _ in 0..3 {
11739                split_walk_stmt(body, vt, gen, counter, pool);
11740            }
11741            *gen = entry;
11742        }
11743        Stmt::ForEach { body, .. } => {
11744            let entry = gen.clone();
11745            for _ in 0..3 {
11746                split_walk_stmt(body, vt, gen, counter, pool);
11747            }
11748            *gen = entry;
11749        }
11750        Stmt::Switch { cases, default, .. } => {
11751            let entry = gen.clone();
11752            for c in cases.iter_mut() {
11753                let mut g_case = entry.clone();
11754                for x in c.body.iter_mut() {
11755                    split_walk_stmt(x, vt, &mut g_case, counter, pool);
11756                }
11757                gen.retain(|k, v| g_case.get(k) == Some(v));
11758            }
11759            if let Some(d) = default {
11760                let mut g_def = entry.clone();
11761                split_walk_stmt(d, vt, &mut g_def, counter, pool);
11762                gen.retain(|k, v| g_def.get(k) == Some(v));
11763            }
11764        }
11765        Stmt::Try {
11766            body,
11767            catches,
11768            finally,
11769        } => {
11770            let entry = gen.clone();
11771            let mut g_try = entry.clone();
11772            split_walk_stmt(body, vt, &mut g_try, counter, pool);
11773            let mut g_all = g_try.clone();
11774            for c in catches.iter_mut() {
11775                // The handler runs with the PRE-try state (the exception
11776                // may fire anywhere in the try).
11777                let mut g_c = entry.clone();
11778                split_walk_stmt(&mut c.body, vt, &mut g_c, counter, pool);
11779                g_all.retain(|k, v| g_c.get(k) == Some(v));
11780            }
11781            *gen = g_all;
11782            if let Some(f) = finally {
11783                split_walk_stmt(f, vt, gen, counter, pool);
11784            }
11785        }
11786        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => {
11787            split_walk_stmt(body, vt, gen, counter, pool);
11788        }
11789        Stmt::Return(Some(e)) | Stmt::Throw(e) => {
11790            rewrite_gen_reads(e, gen, vt);
11791        }
11792        _ => {}
11793    }
11794}
11795
11796/// Framework exception super edge — the embedded API database
11797/// (fwdb) replaced the hand-written ~110-entry JDK/Android exception
11798/// supers table: real extends edges for EVERY framework class, so
11799/// multi-catch dedupe now covers chains the table missed (and cannot
11800/// carry a wrong entry — the table's failure mode was dropping a live
11801/// alternative).
11802fn fw_super(c: &str) -> Option<&'static str> {
11803    crate::fwdb::super_of(c)
11804}
11805
11806/// `sub <: sup` for exception-class names, combining the pool
11807/// hierarchy (which also matches framework ancestor NAMES on the way
11808/// up, so pool subclasses of framework exceptions resolve) with the
11809/// tabled framework chains for pairs where both sides are outside the
11810/// dex.
11811fn exc_subtype(sub: &str, sup: &str, pool: &DexPool) -> bool {
11812    if sub == sup {
11813        return true;
11814    }
11815    if pool.is_subtype(sub, sup) {
11816        return true;
11817    }
11818    let mut cur = sub;
11819    for _ in 0..24 {
11820        let Some(next) = fw_super(cur) else { break };
11821        if next == sup {
11822            return true;
11823        }
11824        cur = next;
11825    }
11826    false
11827}
11828
11829/// Drop multi-catch alternatives shadowed by another alternative in
11830/// the same handler (JLS 14.20.2 forbids subtype-related
11831/// alternatives; the dex verifier does not). Exact duplicates collapse
11832/// to the first occurrence. The caught set is unchanged, so this is
11833/// semantics-preserving by construction.
11834pub fn dedupe_multicatch(s: &mut Stmt, pool: &DexPool) {
11835    dedupe_multicatch_walk(s, pool);
11836}
11837
11838fn dedupe_multicatch_walk(s: &mut Stmt, pool: &DexPool) {
11839    match s {
11840        Stmt::Try {
11841            body,
11842            catches,
11843            finally,
11844        }
11845        | Stmt::TryWithResources {
11846            body,
11847            catches,
11848            finally,
11849            ..
11850        } => {
11851            dedupe_multicatch_walk(body, pool);
11852            for c in catches.iter_mut() {
11853                if c.exc.len() > 1 {
11854                    let mut keep: Vec<std::sync::Arc<str>> = Vec::with_capacity(c.exc.len());
11855                    for e in c.exc.iter() {
11856                        let shadowed = c.exc.iter().any(|o| {
11857                            o.as_ref() != e.as_ref() && exc_subtype(e, o, pool)
11858                        });
11859                        if !shadowed && !keep.iter().any(|k| k.as_ref() == e.as_ref()) {
11860                            keep.push(e.clone());
11861                        }
11862                    }
11863                    c.exc = keep;
11864                }
11865                dedupe_multicatch_walk(&mut c.body, pool);
11866            }
11867            if let Some(f) = finally {
11868                dedupe_multicatch_walk(f, pool);
11869            }
11870        }
11871        Stmt::Block(v) => {
11872            for x in v.iter_mut() {
11873                dedupe_multicatch_walk(x, pool);
11874            }
11875        }
11876        Stmt::If {
11877            then_stmt,
11878            else_stmt,
11879            ..
11880        } => {
11881            dedupe_multicatch_walk(then_stmt, pool);
11882            if let Some(e) = else_stmt {
11883                dedupe_multicatch_walk(e, pool);
11884            }
11885        }
11886        Stmt::While { body, .. }
11887        | Stmt::DoWhile { body, .. }
11888        | Stmt::For { body, .. }
11889        | Stmt::ForEach { body, .. }
11890        | Stmt::Labeled { body, .. }
11891        | Stmt::Synchronized { body, .. } => dedupe_multicatch_walk(body, pool),
11892        Stmt::Switch { cases, default, .. } => {
11893            for c in cases.iter_mut() {
11894                for x in c.body.iter_mut() {
11895                    dedupe_multicatch_walk(x, pool);
11896                }
11897            }
11898            if let Some(d) = default {
11899                dedupe_multicatch_walk(d, pool);
11900            }
11901        }
11902        _ => {}
11903    }
11904}
11905
11906/// Dangling `break L<id>`: a Goto whose paired Label/Labeled-wrap was
11907/// lost to structure degradation prints `break L<id>;` against an
11908/// undeclared label ("未定义的标签", weibo 371/lark 85/weixin 535).
11909/// Inside a loop the goto-to-loop-exit shape degrades to a plain
11910/// `break` — compilable, and the dominant original semantic. Outside
11911/// any loop the jump is irreducible flow (weixin protobuf state
11912/// machines: `goto` into a lost loop middle / dispatcher block — the
11913/// target exists but not at a position Java can express); it renders
11914/// as an honest `$DDC:` marker comment instead of a guaranteed
11915/// undefined-label error, and the block's tail after it is dropped
11916/// (the original control flow never reached those statements).
11917pub fn resolve_dangling_gotos(s: &mut Stmt) {
11918    let mut labels: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
11919    // walk_all is read-only — the whole-body clone this used to make
11920    // (4.6k profile samples of Stmt::clone) bought nothing.
11921    walk_all(s, &mut |st| {
11922        if let Stmt::Label(id) = st {
11923            labels.insert(*id);
11924        }
11925    });
11926    resolve_gotos_walk(s, &labels, 0);
11927}
11928
11929fn resolve_gotos_walk(s: &mut Stmt, labels: &jdc_core::FxHashSet<u32>, loop_depth: u32) {
11930    let depth = match s {
11931        Stmt::While { .. } | Stmt::DoWhile { .. } | Stmt::For { .. } | Stmt::ForEach { .. } => {
11932            loop_depth + 1
11933        }
11934        _ => loop_depth,
11935    };
11936    match s {
11937        Stmt::Goto(id) => {
11938            if labels.contains(id) {
11939                return; // valid pair — keep
11940            }
11941            if depth > 0 {
11942                *s = Stmt::Break(None);
11943            } else {
11944                *s = Stmt::Comment(format!(
11945                    "$DDC: unresolved jump to block {} (irreducible control flow)",
11946                    id
11947                ));
11948            }
11949        }
11950        Stmt::Block(v) => {
11951            let mut i = 0usize;
11952            while i < v.len() {
11953                // A depth-0 dangling Goto becomes a marker comment and
11954                // the original flow never reached the siblings after
11955                // it — dropping them keeps the rendered reachability
11956                // faithful (they were dead in the dex too).
11957                let dangling_here =
11958                    depth == 0 && matches!(&v[i], Stmt::Goto(id) if !labels.contains(id));
11959                resolve_gotos_walk(&mut v[i], labels, depth);
11960                if dangling_here {
11961                    v.truncate(i + 1);
11962                    break;
11963                }
11964                i += 1;
11965            }
11966        }
11967        Stmt::If { then_stmt, else_stmt, .. } => {
11968            resolve_gotos_walk(then_stmt, labels, depth);
11969            if let Some(e) = else_stmt {
11970                resolve_gotos_walk(e, labels, depth);
11971            }
11972        }
11973        Stmt::While { body, .. }
11974        | Stmt::DoWhile { body, .. }
11975        | Stmt::For { body, .. }
11976        | Stmt::ForEach { body, .. }
11977        | Stmt::Labeled { body, .. }
11978        | Stmt::Synchronized { body, .. } => resolve_gotos_walk(body, labels, depth),
11979        Stmt::Try {
11980            body,
11981            catches,
11982            finally,
11983        } => {
11984            resolve_gotos_walk(body, labels, depth);
11985            for c in catches.iter_mut() {
11986                resolve_gotos_walk(&mut c.body, labels, depth);
11987            }
11988            if let Some(f) = finally {
11989                resolve_gotos_walk(f, labels, depth);
11990            }
11991        }
11992        Stmt::Switch { cases, default, .. } => {
11993            for c in cases.iter_mut() {
11994                for x in c.body.iter_mut() {
11995                    resolve_gotos_walk(x, labels, depth);
11996                }
11997            }
11998            if let Some(d) = default {
11999                resolve_gotos_walk(d, labels, depth);
12000            }
12001        }
12002        _ => {}
12003    }
12004}
12005
12006/// READS of `v` across the statements (assignment targets excluded).
12007/// All-var read counts in ONE pass (reads semantics of stmts_read_var)
12008/// — for gates that test many candidate vars against the same subtree
12009/// (split_walk's undo filters ran a full arm walk PER candidate).
12010/// Which of `vars` appear in a READ position inside `stmts`? One walk,
12011/// a small linear membership check per read, short-circuiting the check
12012/// once every candidate is found — no HashMap, no per-node hashing.
12013/// Replaces the full-subtree `read_counts_all` maps the split undo-gates
12014/// used to build per diverged If (their only query was `reads(v) == 0`
12015/// for a handful of candidate gens; the rebuild was quadratic in If
12016/// nesting depth and dominated the split pass on deep methods).
12017fn mark_read_vars(stmts: &[Stmt], vars: &[u32], hits: &mut [bool]) {
12018    let mut remaining = vars.len();
12019    for s in stmts {
12020        if remaining == 0 {
12021            break;
12022        }
12023        visit_stmt_exprs_ro(s, &mut |e| {
12024            if remaining == 0 {
12025                return;
12026            }
12027            visit_exprs_reads(e, &mut |x| {
12028                if remaining == 0 {
12029                    return;
12030                }
12031                if let Expr::Local { var, .. } = x {
12032                    for (i, v) in vars.iter().enumerate() {
12033                        if *v == *var && !hits[i] {
12034                            hits[i] = true;
12035                            remaining -= 1;
12036                        }
12037                    }
12038                }
12039            });
12040        });
12041    }
12042}
12043
12044fn stmts_read_var(stmts: &[Stmt], v: u32) -> usize {
12045    let mut n = 0usize;
12046    for s in stmts {
12047        visit_stmt_exprs_ro(s, &mut |e| {
12048            visit_exprs_reads(e, &mut |x| {
12049                if let Expr::Local { var: vv, .. } = x {
12050                    if *vv == v {
12051                        n += 1;
12052                    }
12053                }
12054            });
12055        });
12056    }
12057    n
12058}
12059
12060pub fn fix_ctor_conditional_super(body: &mut Stmt) {
12061    // A single-statement body can arrive UNWRAPPED (bare `If` — the
12062    // gb6/e throw-guard family); the shape scans need a statement list.
12063    if !matches!(body, Stmt::Block(_)) {
12064        let inner = std::mem::replace(body, Stmt::Block(Vec::new()));
12065        *body = Stmt::Block(vec![inner]);
12066    }
12067    let Stmt::Block(stmts) = body else { return };
12068    // Unwrap single-statement Block wrappers: the structurer can nest
12069    // the whole body one level deeper, hiding the top-level If from the
12070    // shape scans (cleanup would flatten it — but runs AFTER this pass).
12071    while stmts.len() == 1 {
12072        let Stmt::Block(inner) = &stmts[0] else { break };
12073        *stmts = inner.clone();
12074    }
12075    if stmts.is_empty() {
12076        return;
12077    }
12078    if is_bare_ctor_call(stmts.first().unwrap()) {
12079        dedupe_ctor_delegations(body);
12080        return;
12081    }
12082    if try_linear_super_inline(stmts) {
12083        return;
12084    }
12085    try_merge_branched_super(stmts);
12086}
12087
12088/// Shape C: the delegation is already first — strip duplicate copies
12089/// left by R8 path-duplication (Kotlin default-arg bridge ctors: ft5/j
12090/// had `super()` leading PLUS another inside a branch — "对super的调用
12091/// 必须是构造器中的第一个语句" at the copy). Every path delegates
12092/// through the leading call, so structurally-identical (or empty-args,
12093/// same-kind) copies nested in blocks/ifs are removed, never moved.
12094/// Runs both ahead of the shape-A/B attempt and AFTER the plain hoist
12095/// (fix_ctor_super_first) — the hoist is what puts the leading
12096/// delegation in place for path-duplicated ctors.
12097/// A ctor that ALREADY delegates in its first statement can hold no
12098/// other `this(..)`/`super(..)` — Baidu Titan hotpatch instrumentation
12099/// injects a conditional re-delegation inside the `if ($ic != null)`
12100/// guard of every ctor (baidusearch ctor-not-first ×61,922 across
12101/// 21,813 files: `super(context); if ($ic != null) { ..; if
12102/// ((flag&1)!=0) { super((Context) callArgs[0]); ..; return; } }`).
12103/// The guard is dead code on the unpatched runtime path (and the
12104/// decompiled output has no patch runtime at all); dropping the
12105/// secondary delegation keeps the real semantics and restores the
12106/// Java first-statement rule. Runs after dedupe_ctor_delegations
12107/// (identical copies) and before extract_branched_delegation_helper
12108/// — gated on a LEADING delegation, so branched-delegation ctors
12109/// (no leading one) still get the helper treatment. Nested class
12110/// declarations are separate ctor scopes and are not entered.
12111pub fn strip_secondary_delegations(body: &mut Stmt) {
12112    let Stmt::Block(stmts) = body else { return };
12113    if !stmts.first().is_some_and(is_bare_ctor_call) {
12114        return;
12115    }
12116    fn strip_in(s: &mut Stmt) {
12117        if is_bare_ctor_call(s) {
12118            *s = Stmt::Block(Vec::new());
12119            return;
12120        }
12121        match s {
12122            Stmt::Block(v) => v.iter_mut().for_each(strip_in),
12123            Stmt::If {
12124                then_stmt,
12125                else_stmt,
12126                ..
12127            } => {
12128                strip_in(then_stmt);
12129                if let Some(e) = else_stmt {
12130                    strip_in(e);
12131                }
12132            }
12133            Stmt::While { body, .. }
12134            | Stmt::DoWhile { body, .. }
12135            | Stmt::ForEach { body, .. }
12136            | Stmt::Synchronized { body, .. }
12137            | Stmt::Labeled { body, .. } => strip_in(body),
12138            Stmt::For { init, body, .. } => {
12139                init.iter_mut().for_each(strip_in);
12140                strip_in(body);
12141            }
12142            Stmt::Switch { cases, default, .. } => {
12143                for c in cases.iter_mut() {
12144                    c.body.iter_mut().for_each(strip_in);
12145                }
12146                if let Some(d) = default {
12147                    strip_in(d);
12148                }
12149            }
12150            Stmt::Try {
12151                body,
12152                catches,
12153                finally,
12154            }
12155            | Stmt::TryWithResources {
12156                body,
12157                catches,
12158                finally,
12159                ..
12160            } => {
12161                strip_in(body);
12162                for c in catches.iter_mut() {
12163                    strip_in(&mut c.body);
12164                }
12165                if let Some(f) = finally {
12166                    strip_in(f);
12167                }
12168            }
12169            _ => {}
12170        }
12171    }
12172    for st in stmts.iter_mut().skip(1) {
12173        strip_in(st);
12174    }
12175}
12176
12177pub fn dedupe_ctor_delegations(body: &mut Stmt) {
12178    let Stmt::Block(stmts) = body else { return };
12179    let Some(d0) = stmts.first().and_then(|s| match s {
12180        Stmt::ExprStmt(e) if is_delegation_expr(e) => Some(e.clone()),
12181        _ => None,
12182    }) else {
12183        return;
12184    };
12185    fn rec(s: &mut Stmt, d0: &Expr) {
12186        match s {
12187            Stmt::Block(v) => {
12188                v.retain(|x| {
12189                    !matches!(x, Stmt::ExprStmt(e) if is_delegation_expr(e) && same_delegation(e, d0))
12190                });
12191                for x in v.iter_mut() {
12192                    rec(x, d0);
12193                }
12194            }
12195            Stmt::If { then_stmt, else_stmt, .. } => {
12196                rec(then_stmt, d0);
12197                if let Some(e) = else_stmt {
12198                    rec(e, d0);
12199                }
12200            }
12201            _ => {}
12202        }
12203    }
12204    // Top-level copies first (rec only strips copies NESTED in
12205    // blocks/ifs — a duplicate `super()` sitting directly in the body
12206    // list fell through its `_` arm: weixin ft5/j's second super()).
12207    {
12208        let d0r = &d0;
12209        let mut idx = 0usize;
12210        stmts.retain(|s| {
12211            idx += 1;
12212            idx == 1
12213                || !(matches!(s, Stmt::ExprStmt(e)
12214                    if is_delegation_expr(e) && same_delegation(e, d0r)))
12215        });
12216    }
12217    for s in stmts.iter_mut().skip(1) {
12218        rec(s, &d0);
12219    }
12220}
12221
12222/// Structural delegation identity for dedup: same kind/target and equal
12223/// args — or both arg-less (`super()` copies across duplicated paths
12224/// render with locally-renamed args yet delegate identically).
12225fn same_delegation(a: &Expr, b: &Expr) -> bool {
12226    match (a, b) {
12227        (
12228            Expr::Method { name: n1, cls: c1, is_super: s1, args: a1, .. },
12229            Expr::Method { name: n2, cls: c2, is_super: s2, args: a2, .. },
12230        ) => {
12231            &**n1 == "<init>"
12232                && &**n2 == "<init>"
12233                && c1 == c2
12234                && s1 == s2
12235                && (a1 == a2 || (a1.is_empty() && a2.is_empty()))
12236        }
12237        _ => false,
12238    }
12239}
12240
12241/// The `Method` expr of a bare ctor-delegation statement.
12242fn ctor_call_expr(s: &Stmt) -> Option<&Expr> {
12243    match s {
12244        Stmt::ExprStmt(e) if is_delegation_expr(e) => Some(e),
12245        _ => None,
12246    }
12247}
12248
12249/// Same delegation kind ignoring args (super vs this, same target).
12250fn same_ctor_kind(a: &Expr, b: &Expr) -> bool {
12251    match (a, b) {
12252        (
12253            Expr::Method { cls: c1, is_super: s1, owner: o1, .. },
12254            Expr::Method { cls: c2, is_super: s2, owner: o2, .. },
12255        ) => c1 == c2 && s1 == s2 && o1 == o2,
12256        _ => false,
12257    }
12258}
12259
12260/// Statements of a branch body, recursively flattening pure Blocks —
12261/// the fix runs BEFORE cleanup, so structurer-emitted nesting is still
12262/// in place. Any non-Block statement (If/Try/...) stays an item and
12263/// fails the def/call classification, which is the intended abort.
12264fn flat_list(s: &Stmt) -> Vec<&Stmt> {
12265    fn rec<'a>(s: &'a Stmt, out: &mut Vec<&'a Stmt>) {
12266        match s {
12267            Stmt::Block(v) => {
12268                for x in v {
12269                    rec(x, out);
12270                }
12271            }
12272            other => out.push(other),
12273        }
12274    }
12275    let mut out = Vec::new();
12276    rec(s, &mut out);
12277    out
12278}
12279
12280/// Blank the OWNER of un-folded `<init>` calls (lost allocation): the
12281/// printer renders them as `new X(..)` and never reads the owner, so its
12282/// register reference is not a real use (v7.a$a2: the dead
12283/// `new f(0,..)`'s owner was v1's register, inflating v1's use count
12284/// and blocking the inline-hoist).
12285fn strip_lost_alloc_owners(e: &mut Expr) {
12286    deep_rewrite(e, &mut |x| {
12287        if let Expr::Method { name, is_special: true, owner: Some(o), .. } = x {
12288            if &**name == "<init>" && !matches!(**o, Expr::This) {
12289                **o = Expr::Const(jdc_core::ir::expr::ConstVal::Int(0));
12290            }
12291        }
12292    });
12293}
12294
12295/// Per-var `Local` occurrence counts over an expression (single pass;
12296/// lost-alloc `<init>` owners excluded — see strip_lost_alloc_owners).
12297fn count_locals_expr(e: &Expr) -> jdc_core::FxHashMap<u32, usize> {
12298    let mut m = jdc_core::FxHashMap::default();
12299    count_expr_ro(e, &mut m);
12300    m
12301}
12302
12303pub(crate) fn count_locals_stmts(ss: &[Stmt]) -> jdc_core::FxHashMap<u32, usize> {
12304    let mut m = jdc_core::FxHashMap::default();
12305    for s in ss {
12306        visit_stmt_exprs_ro(s, &mut |e| count_expr_ro(e, &mut m));
12307    }
12308    m
12309}
12310
12311fn cnt(m: &jdc_core::FxHashMap<u32, usize>, v: u32) -> usize {
12312    m.get(&v).copied().unwrap_or(0)
12313}
12314
12315/// Substitute map-referenced locals, recursively expanding def chains
12316/// (depth-capped). Returns None when a referenced var has no def or the
12317/// chain is too deep.
12318fn inline_locals(
12319    e: &Expr,
12320    map: &std::collections::HashMap<u32, Expr>,
12321    depth: u32,
12322) -> Option<Expr> {
12323    if depth > 4 {
12324        return None;
12325    }
12326    let mut out = e.clone();
12327    let mut fail = false;
12328    deep_rewrite(&mut out, &mut |x| {
12329        if let Expr::Local { var, .. } = x {
12330            if let Some(def) = map.get(var) {
12331                match inline_locals(def, map, depth + 1) {
12332                    Some(r) => *x = r,
12333                    None => fail = true,
12334                }
12335            }
12336        }
12337    });
12338    if fail {
12339        None
12340    } else {
12341        Some(out)
12342    }
12343}
12344
12345/// A leading statement that defines a local: (var, init expr).
12346fn def_of(s: &Stmt) -> Option<(u32, &Expr)> {
12347    match s {
12348        Stmt::LocalDef { var, init: Some(e), .. } => Some((*var, e)),
12349        Stmt::ExprStmt(Expr::Assign {
12350            target,
12351            value,
12352            op: jdc_core::ir::expr::AssignOp::Plain,
12353            ..
12354        }) => match &**target {
12355            Expr::Local { var, .. } => Some((*var, value)),
12356            _ => None,
12357        },
12358        _ => None,
12359    }
12360}
12361
12362/// Shape A: straight-line prefix, delegation references prefix locals.
12363fn try_linear_super_inline(stmts: &mut Vec<Stmt>) -> bool {
12364    let Some(pos) = stmts.iter().position(is_bare_ctor_call) else {
12365        return false;
12366    };
12367    if pos == 0 {
12368        return false; // already first — nothing to do
12369    }
12370    // The prefix must be straight-line.
12371    if !stmts[..pos]
12372        .iter()
12373        .all(|s| matches!(s, Stmt::LocalDef { .. } | Stmt::ExprStmt(_)))
12374    {
12375        return false;
12376    }
12377    let Some(call) = ctor_call_expr(&stmts[pos]) else {
12378        return false;
12379    };
12380    let Expr::Method { args, .. } = call else {
12381        return false;
12382    };
12383    // Defs reachable from the args (last definition wins, bytecode order).
12384    let mut map: std::collections::HashMap<u32, Expr> =
12385        std::collections::HashMap::new();
12386    let mut def_idx: jdc_core::FxHashMap<u32, usize> =
12387        jdc_core::FxHashMap::default();
12388    for (i, s) in stmts[..pos].iter().enumerate() {
12389        if let Some((v, e)) = def_of(s) {
12390            map.insert(v, e.clone());
12391            def_idx.insert(v, i);
12392        }
12393    }
12394    // Which arg locals need inlining?
12395    let mut needed: Vec<u32> = Vec::new();
12396    for a in args {
12397        let mut probe = a.clone();
12398        strip_lost_alloc_owners(&mut probe);
12399        deep_rewrite(&mut probe, &mut |x| {
12400            if let Expr::Local { var, .. } = x {
12401                if map.contains_key(var) && !needed.contains(var) {
12402                    needed.push(*var);
12403                }
12404            }
12405        });
12406    }
12407    if needed.is_empty() {
12408        return false; // plain hoist handles it
12409    }
12410    // Guard: side-effecting defs inline only at a provably preserved
12411    // call count (exactly one use, the arg site).
12412    let mut consumed: Vec<usize> = Vec::new();
12413    let use_counts = count_locals_stmts(stmts);
12414    for &v in &needed {
12415        let def = &map[&v];
12416        let uses = cnt(&use_counts, v);
12417        if jdc_core::ir::build::has_side_effects(def) && uses != 1 {
12418            return false;
12419        }
12420        if uses == 1 {
12421            consumed.push(def_idx[&v]);
12422        }
12423    }
12424    // Build the inlined args.
12425    let mut new_args: Vec<Expr> = Vec::with_capacity(args.len());
12426    for a in args {
12427        match inline_locals(a, &map, 0) {
12428            Some(r) => new_args.push(r),
12429            None => return false,
12430        }
12431    }
12432    // Rebuild: super first (with inlined args), the unconsumed prefix
12433    // statements keep their order after it, then the original rest.
12434    let mut call_stmt = stmts.remove(pos);
12435    if let Stmt::ExprStmt(Expr::Method { args: slot, .. }) = &mut call_stmt {
12436        *slot = new_args;
12437    }
12438    let mut rest: Vec<Stmt> = Vec::with_capacity(stmts.len() + 1);
12439    rest.push(call_stmt);
12440    for (i, s) in stmts.drain(..).enumerate() {
12441        // Indices shift after `remove(pos)`: consumed holds pre-removal
12442        // indices; pos itself is already gone from `stmts`.
12443        let orig = if i >= pos { i + 1 } else { i };
12444        if !consumed.contains(&orig) {
12445            rest.push(s);
12446        }
12447    }
12448    *stmts = rest;
12449    true
12450}
12451
12452/// Shape B: if/else branches each ending in the same-kind delegation.
12453fn try_merge_branched_super(stmts: &mut Vec<Stmt>) -> bool {
12454    // Candidate If positions — the prelude before the chosen If must be
12455    // straight-line; try each If until one merges (nested/multi-if
12456    // ctors exist but the first matching one is the delegation site).
12457    let if_positions: Vec<usize> = stmts
12458        .iter()
12459        .enumerate()
12460        .filter(|(_, s)| matches!(s, Stmt::If { .. }))
12461        .map(|(i, _)| i)
12462        .collect();
12463    for if_pos in if_positions {
12464        // Cheap allocation-free gate: at least one branch must hold a
12465        // delegation (most ctor ifs are field null-checks and must not
12466        // pay the merge machinery). The OTHER side may be delegation-free
12467        // (throw-guard shape D) or absent (then-only shape E — the
12468        // rendered if/else form often only exists after later passes).
12469        let gate = match &stmts[if_pos] {
12470            Stmt::If { then_stmt, else_stmt, .. } => {
12471                contains_delegation(then_stmt)
12472                    || else_stmt.as_ref().is_some_and(|e| contains_delegation(e))
12473            }
12474            _ => false,
12475        };
12476        if gate && merge_at(stmts, if_pos) {
12477            return true;
12478        }
12479    }
12480    false
12481}
12482
12483/// Does this subtree contain a ctor delegation? (module-level: shared by
12484/// the merge gate and split_branch)
12485fn contains_delegation(s: &Stmt) -> bool {
12486    match s {
12487        Stmt::ExprStmt(e) => is_delegation_expr(e),
12488        Stmt::Block(v) => v.iter().any(contains_delegation),
12489        Stmt::If { then_stmt, else_stmt, .. } => {
12490            contains_delegation(then_stmt)
12491                || else_stmt.as_ref().is_some_and(|e| contains_delegation(e))
12492        }
12493        _ => false,
12494    }
12495}
12496
12497
12498
12499/// One merge attempt at `stmts[if_pos]`. See fix_ctor_conditional_super
12500/// for the doctrine. Extensions over the pure two-call merge:
12501/// - branch pre-call SIDE-EFFECT statements (Kotlin `o.h(param,..)`
12502///   null-checks — the zz5/j0 family) are allowed; they move into the
12503///   branch remainder (after super — the same reordering doctrine the
12504///   plain hoist already applies; Java has no pre-super statement form).
12505/// - a branch WITHOUT a delegation (throw-guard: `if (x==null) throw
12506///   .. else { super(..); .. }` — the gb6/e family) merges as pure
12507///   remainder; exactly one branch carries the call then, and no
12508///   ternary args arise (the guard throws before any path needs them).
12509/// - prelude single-assignment if/else (`if (c) {v=e1} else {v=e2}`)
12510///   folds into a Cond def for inlining (the zz5/j0 v15/v16 preludes).
12511fn merge_at(stmts: &mut Vec<Stmt>, if_pos: usize) -> bool {
12512    /// Branch classification: Call(at, defs, pre-call extra idxs) when a
12513    /// delegation sits behind only defs/side-effect statements; Clean
12514    /// when the branch has no delegation at all (pure remainder); Dirty
12515    /// otherwise (nested/conditional delegation — out of scope).
12516    enum Br {
12517        /// (call index, pre-call defs, def statement indices)
12518        Call(usize, Vec<(u32, Expr)>, Vec<usize>),
12519        Clean,
12520        Dirty,
12521    }
12522    fn split_branch(list: &[&Stmt]) -> Br {
12523        let call_pos = list.iter().position(|s| ctor_call_expr(s).is_some());
12524        let Some(c) = call_pos else {
12525            // No delegation at this level: a pure remainder branch
12526            // (throw-guard) unless one hides in nested control flow.
12527            return if list.iter().any(|s| contains_delegation(s)) {
12528                Br::Dirty
12529            } else {
12530                Br::Clean
12531            };
12532        };
12533        if list.iter().skip(c + 1).any(|s| ctor_call_expr(s).is_some()) {
12534            return Br::Dirty; // two delegations in one branch
12535        }
12536        let mut defs: Vec<(u32, Expr)> = Vec::new();
12537        let mut def_idxs: Vec<usize> = Vec::new();
12538        for (i, s) in list.iter().enumerate().take(c) {
12539            match def_of(s) {
12540                Some((v, e)) => {
12541                    defs.push((v, e.clone()));
12542                    def_idxs.push(i);
12543                }
12544                None => match s {
12545                    // Side-effect statements and bare decls ahead of the
12546                    // delegation ride along into the remainder.
12547                    Stmt::ExprStmt(_) | Stmt::LocalDef { .. } => {}
12548                    _ => return Br::Dirty, // control flow pre-call
12549                },
12550            }
12551        }
12552        Br::Call(c, defs, def_idxs)
12553    }
12554    /// Prelude single-assignment if/else → `v = c ? e1 : e2` def.
12555    fn cond_def_of(s: &Stmt, scope: &[Stmt]) -> Option<(u32, Expr)> {
12556        let Stmt::If { cond, then_stmt, else_stmt: Some(e), .. } = s else {
12557            return None;
12558        };
12559        fn one<'x>(b: &'x Stmt, scope: &[Stmt]) -> Option<(u32, &'x Expr)> {
12560            let l = flat_list(b);
12561            if l.len() == 1 {
12562                return def_of(l[0]);
12563            }
12564            // A [w = E; v = w] forwarding PAIR — the shape the
12565            // structurer leaves when E is impure (`c v5 = new c(..);
12566            // v6 = v5`) and forward_single_use's adjacency pass missed
12567            // it (a duplicated read elsewhere in its raw body broke the
12568            // single-read gate). Collapse to `v = E`, gated on w being
12569            // method-unique: the merge DROPS this branch, so any other
12570            // read/write of w would dangle (lark gp2/d Kotlin default-
12571            // arg bridge, this-not-first family).
12572            if l.len() == 2 {
12573                let (w, ex) = def_of(l[0])?;
12574                let Stmt::ExprStmt(Expr::Assign {
12575                    target,
12576                    op: AssignOp::Plain,
12577                    value,
12578                }) = l[1]
12579                else {
12580                    return None;
12581                };
12582                let Expr::Local { var: tv, .. } = &**target else {
12583                    return None;
12584                };
12585                let Expr::Local { var: rv, .. } = &**value else {
12586                    return None;
12587                };
12588                if *rv != w || *tv == w {
12589                    return None;
12590                }
12591                // stmts_count_writes only sees Expr-level assigns; the
12592                // pair's own def is often an init-bearing LocalDef, so
12593                // count those separately for the uniqueness gate.
12594                let mut ldef_writes = 0usize;
12595                for st in scope {
12596                    walk_all(st, &mut |x| {
12597                        if let Stmt::LocalDef {
12598                            var,
12599                            init: Some(_),
12600                            ..
12601                        } = x
12602                        {
12603                            if *var == w {
12604                                ldef_writes += 1;
12605                            }
12606                        }
12607                    });
12608                }
12609                if stmts_read_var(scope, w) != 1
12610                    || stmts_count_writes(scope, w) + ldef_writes != 1
12611                {
12612                    return None;
12613                }
12614                return Some((*tv, ex));
12615            }
12616            None
12617        }
12618        let (v1, e1) = one(then_stmt, scope)?;
12619        let (v2, e2) = one(e, scope)?;
12620        if v1 != v2 {
12621            return None;
12622        }
12623        Some((
12624            v1,
12625            Expr::Cond {
12626                c: Box::new(cond.clone()),
12627                t: Box::new(e1.clone()),
12628                f: Box::new(e2.clone()),
12629            },
12630        ))
12631    }
12632
12633    // Prelude: defs (flat or folded cond), bare decls, side-effect stmts.
12634    let mut pmap: std::collections::HashMap<u32, Expr> =
12635        std::collections::HashMap::new();
12636    let mut pidx: jdc_core::FxHashMap<u32, usize> =
12637        jdc_core::FxHashMap::default();
12638    for (i, s) in stmts[..if_pos].iter().enumerate() {
12639        match s {
12640            Stmt::LocalDef { .. } | Stmt::ExprStmt(_) => {
12641                if let Some((v, e)) = def_of(s) {
12642                    pmap.insert(v, e.clone());
12643                    pidx.insert(v, i);
12644                }
12645            }
12646            Stmt::If { .. } => match cond_def_of(s, stmts) {
12647                Some((v, e)) => {
12648                    pmap.insert(v, e);
12649                    pidx.insert(v, i);
12650                }
12651                None => return false,
12652            },
12653            _ => return false,
12654        }
12655    }
12656    let (cond, then_v, else_v, had_else) = match &stmts[if_pos] {
12657        Stmt::If { cond, then_stmt, else_stmt, .. } => (
12658            cond.clone(),
12659            then_stmt.as_ref().clone(),
12660            else_stmt.as_ref().map_or_else(|| Stmt::Block(Vec::new()), |e| e.as_ref().clone()),
12661            else_stmt.is_some(),
12662        ),
12663        _ => return false,
12664    };
12665    // Chained guards (Kotlin multi-param null-checks):
12666    // `if(a){throw..} else { if(b){throw..} else { super(..); .. } }` —
12667    // a branch that is a lone delegation-bearing If gets merged
12668    // recursively first (its delegation hoisted to the branch head), so
12669    // the outer split then sees [call, nested-guard-remainder].
12670    fn normalize_branch(b: &mut Stmt) {
12671        // MOVE-based (no deep clone): chained weixin Parcel ctors carry
12672        // huge tails; cloning per recursion level cost measurable wall
12673        // time. merge_at leaves stmts untouched on failure, so the taken
12674        // node can always go back.
12675        let slot: &mut Stmt = match b {
12676            Stmt::Block(v) if v.len() == 1 => &mut v[0],
12677            other => other,
12678        };
12679        if !matches!(slot, Stmt::If { .. }) || !contains_delegation(slot) {
12680            return;
12681        }
12682        let taken = std::mem::replace(slot, Stmt::Block(Vec::new()));
12683        let mut tmp = vec![taken];
12684        if merge_at(&mut tmp, 0) {
12685            *slot = Stmt::Block(tmp);
12686        } else {
12687            *slot = tmp.pop().unwrap();
12688        }
12689    }
12690    let mut then_v = then_v;
12691    let mut else_v = else_v;
12692    normalize_branch(&mut then_v);
12693    normalize_branch(&mut else_v);
12694    let then_list = flat_list(&then_v);
12695    let else_list = flat_list(&else_v);
12696
12697    // Exactly: two Call branches (merge with ternaries) or one Call +
12698    // one Clean (single-delegation throw-guard). Owned branch payloads.
12699    enum Side {
12700        Call {
12701            at: usize,
12702            defs: Vec<(u32, Expr)>,
12703            def_idxs: Vec<usize>,
12704        },
12705        Clean,
12706    }
12707    let (t_side, e_side, two_sided) = match (split_branch(&then_list), split_branch(&else_list)) {
12708        (Br::Call(tc, td, ti), Br::Call(ec, ed, ei)) => (
12709            Side::Call { at: tc, defs: td, def_idxs: ti },
12710            Side::Call { at: ec, defs: ed, def_idxs: ei },
12711            true,
12712        ),
12713        (Br::Call(tc, td, ti), Br::Clean) => (
12714            Side::Call { at: tc, defs: td, def_idxs: ti },
12715            Side::Clean,
12716            false,
12717        ),
12718        (Br::Clean, Br::Call(ec, ed, ei)) => (
12719            Side::Clean,
12720            Side::Call { at: ec, defs: ed, def_idxs: ei },
12721            false,
12722        ),
12723        _ => return false,
12724    };
12725    // The delegation nodes (two-sided: both; single: one).
12726    let (t_call, e_call) = match (&t_side, &e_side) {
12727        (Side::Call { at: ta, .. }, Side::Call { at: ea, .. }) => (
12728            ctor_call_expr(then_list[*ta]),
12729            ctor_call_expr(else_list[*ea]),
12730        ),
12731        (Side::Call { at: ta, .. }, Side::Clean) => {
12732            (ctor_call_expr(then_list[*ta]), None)
12733        }
12734        (Side::Clean, Side::Call { at: ea, .. }) => {
12735            (None, ctor_call_expr(else_list[*ea]))
12736        }
12737        _ => return false,
12738    };
12739    // The node that becomes the merged call: then side when present.
12740    let (call_list, call_at) = match &t_side {
12741        Side::Call { at, .. } => (&then_list, *at),
12742        Side::Clean => match &e_side {
12743            Side::Call { at, .. } => (&else_list, *at),
12744            Side::Clean => return false,
12745        },
12746    };
12747    let (base_args, other_args) = if two_sided {
12748        let (Some(tc), Some(ec)) = (t_call, e_call) else {
12749            return false;
12750        };
12751        if !same_ctor_kind(tc, ec) {
12752            return false;
12753        }
12754        let (Expr::Method { args: a1, .. }, Expr::Method { args: a2, .. }) = (tc, ec) else {
12755            return false;
12756        };
12757        if a1.len() != a2.len() {
12758            return false;
12759        }
12760        (a1, Some(a2))
12761    } else {
12762        let c = t_call.or(e_call);
12763        let Some(c) = c else { return false };
12764        let Expr::Method { args, .. } = c else {
12765            return false;
12766        };
12767        (args, None)
12768    };
12769
12770    // Def maps per side (prelude + own branch defs).
12771    let mut tmap: std::collections::HashMap<u32, Expr> = pmap.clone();
12772    let mut emap: std::collections::HashMap<u32, Expr> = pmap.clone();
12773    if let Side::Call { defs, .. } = &t_side {
12774        for (v, e) in defs {
12775            tmap.insert(*v, e.clone());
12776        }
12777    }
12778    if let Side::Call { defs, .. } = &e_side {
12779        for (v, e) in defs {
12780            emap.insert(*v, e.clone());
12781        }
12782    }
12783    let Some(cond_i) = inline_locals(&cond, &pmap, 0) else {
12784        return false;
12785    };
12786    // Inline each side's args through its own map.
12787    let mut then_args: Vec<Expr> = Vec::with_capacity(base_args.len());
12788    let mut else_args: Vec<Expr> = Vec::new();
12789    if two_sided {
12790        for a in base_args {
12791            match inline_locals(a, &tmap, 0) {
12792                Some(r) => then_args.push(r),
12793                None => return false,
12794            }
12795        }
12796        for a in other_args.unwrap() {
12797            match inline_locals(a, &emap, 0) {
12798                Some(r) => else_args.push(r),
12799                None => return false,
12800            }
12801        }
12802    } else {
12803        // Single-delegation side: base_args belong to whichever side
12804        // carries the call; pick that side's map.
12805        let from_then = matches!(&t_side, Side::Call { .. });
12806        for a in base_args {
12807            let r = if from_then {
12808                inline_locals(a, &tmap, 0)
12809            } else {
12810                inline_locals(a, &emap, 0)
12811            };
12812            match r {
12813                Some(r) => then_args.push(r),
12814                None => return false,
12815            }
12816        }
12817    }
12818    let primary_args = base_args;
12819    // Per-position merge.
12820    let mut merged: Vec<Expr> = Vec::with_capacity(then_args.len());
12821    let mut differ = 0usize;
12822    for (j, t) in then_args.iter().enumerate() {
12823        if two_sided {
12824            let e = &else_args[j];
12825            if t == e {
12826                merged.push(t.clone());
12827            } else {
12828                differ += 1;
12829                merged.push(Expr::Cond {
12830                    c: Box::new(cond_i.clone()),
12831                    t: Box::new(t.clone()),
12832                    f: Box::new(e.clone()),
12833                });
12834            }
12835        } else {
12836            merged.push(t.clone());
12837        }
12838    }
12839
12840    // ---- side-effect preservation guards (batched counts) ----
12841    let vars_in = |e: &Expr| -> Vec<u32> {
12842        let mut vs = Vec::new();
12843        let mut probe = e.clone();
12844        strip_lost_alloc_owners(&mut probe);
12845        deep_rewrite(&mut probe, &mut |x| {
12846            if let Expr::Local { var, .. } = x {
12847                if !vs.contains(var) {
12848                    vs.push(*var);
12849                }
12850            }
12851        });
12852        vs
12853    };
12854    // Remainder statements per branch (extras + post-call tail; a Clean
12855    // branch contributes all of its statements).
12856    // The consumed defs LEAVE the remainder: their expressions were
12857    // inlined into the merged delegation args, and a dead `v = E` left
12858    // after the call both reads wrong and trips the u_t1/u_t2 guards
12859    // (ContinuationImpl's branch-local `context = null / getContext()`
12860    // — the coroutine family's 对this的调用必须是第一个语句 root).
12861    let branch_remainder = |list: &[&Stmt], side: &Side| -> Vec<Stmt> {
12862        match side {
12863            Side::Clean => list.iter().map(|s| (*s).clone()).collect(),
12864            Side::Call { at, def_idxs, .. } => {
12865                list.iter()
12866                    .enumerate()
12867                    .filter(|(i, _)| *i != *at && !def_idxs.contains(i))
12868                    .map(|(_, s)| (*s).clone())
12869                    .collect()
12870            }
12871        }
12872    };
12873    let tail_then_v = branch_remainder(&then_list, &t_side);
12874    let tail_else_v = branch_remainder(&else_list, &e_side);
12875    let post = &stmts[if_pos + 1..];
12876    let mut inlined: Vec<u32> = vars_in(&cond);
12877    for a in primary_args.iter().chain(else_args.iter()) {
12878        for v in vars_in(a) {
12879            if !inlined.contains(&v) {
12880                inlined.push(v);
12881            }
12882        }
12883    }
12884    inlined.retain(|v| pmap.contains_key(v) || tmap.contains_key(v) || emap.contains_key(v));
12885    // Prelude-and-branch redefinition: ambiguous provenance — reject.
12886    for &v in &inlined {
12887        let in_branch = match (&t_side, &e_side) {
12888            (Side::Call { defs: td, .. }, Side::Call { defs: ed, .. }) => {
12889                td.iter().any(|(d, _)| *d == v) || ed.iter().any(|(d, _)| *d == v)
12890            }
12891            (Side::Call { defs, .. }, Side::Clean)
12892            | (Side::Clean, Side::Call { defs, .. }) => {
12893                defs.iter().any(|(d, _)| *d == v)
12894            }
12895            _ => false,
12896        };
12897        if pmap.contains_key(&v) && in_branch {
12898            return false;
12899        }
12900    }
12901    type Counts = jdc_core::FxHashMap<u32, usize>;
12902    type GuardCounts = (
12903        Counts,
12904        Vec<Counts>,
12905        Vec<Counts>,
12906        Counts,
12907        Counts,
12908        Counts,
12909        Vec<(u32, Counts)>,
12910    );
12911    let (cond_counts, a1_counts, a2_counts, tail1_counts, tail2_counts, post_counts, def_counts): GuardCounts =
12912        if inlined.is_empty() {
12913        Default::default()
12914    } else {
12915        let all_defs: Vec<(u32, &Expr)> = pmap
12916            .iter()
12917            .map(|(k, v)| (*k, v))
12918            .chain(match &t_side {
12919                Side::Call { defs, .. } => defs.iter().map(|(k, v)| (*k, v)).collect(),
12920                Side::Clean => Vec::new(),
12921            })
12922            .chain(match &e_side {
12923                Side::Call { defs, .. } => defs.iter().map(|(k, v)| (*k, v)).collect(),
12924                Side::Clean => Vec::new(),
12925            })
12926            .collect();
12927        (
12928            count_locals_expr(&cond),
12929            primary_args.iter().map(count_locals_expr).collect(),
12930            else_args.iter().map(count_locals_expr).collect(),
12931            count_locals_stmts(&tail_then_v),
12932            count_locals_stmts(&tail_else_v),
12933            count_locals_stmts(post),
12934            all_defs.into_iter().map(|(v, e)| (v, count_locals_expr(e))).collect(),
12935        )
12936        };
12937    for &v in &inlined {
12938        let in_t = matches!(&t_side, Side::Call { defs, .. } if defs.iter().any(|(d, _)| *d == v));
12939        let in_e = matches!(&e_side, Side::Call { defs, .. } if defs.iter().any(|(d, _)| *d == v));
12940        let effectful = [
12941            in_t.then(|| tmap.get(&v)).flatten(),
12942            in_e.then(|| emap.get(&v)).flatten(),
12943            pmap.get(&v),
12944        ]
12945        .into_iter()
12946        .flatten()
12947        .any(jdc_core::ir::build::has_side_effects);
12948        if !effectful {
12949            continue;
12950        }
12951        let u_cond = cnt(&cond_counts, v);
12952        let u_post = cnt(&post_counts, v);
12953        let u_t1 = cnt(&tail1_counts, v);
12954        let u_t2 = cnt(&tail2_counts, v);
12955        let mut u_odef = 0usize;
12956        for (dv, dc) in &def_counts {
12957            if *dv != v && inlined.contains(dv) {
12958                u_odef += cnt(dc, v);
12959            }
12960        }
12961        let mut emit = u_cond * (1 + differ);
12962        let mut uses_args = 0usize;
12963        for j in 0..primary_args.len() {
12964            let c1 = cnt(&a1_counts[j], v);
12965            let c2 = if two_sided && j < a2_counts.len() {
12966                cnt(&a2_counts[j], v)
12967            } else {
12968                0
12969            };
12970            uses_args += c1 + c2;
12971            emit += if two_sided && then_args[j] != else_args[j] {
12972                c1 + c2
12973            } else {
12974                c1.max(c2)
12975            };
12976        }
12977        let ok = if in_t || in_e {
12978            u_cond == 0
12979                && u_post == 0
12980                && u_t1 == 0
12981                && u_t2 == 0
12982                && u_odef == 0
12983                && a1_counts.iter().map(|c| cnt(c, v)).sum::<usize>() <= 1
12984                && a2_counts.iter().map(|c| cnt(c, v)).sum::<usize>() <= 1
12985                && emit <= 2
12986                && (in_t || a1_counts.iter().all(|c| cnt(c, v) == 0))
12987                && (in_e || a2_counts.iter().all(|c| cnt(c, v) == 0))
12988        } else {
12989            u_post == 0
12990                && u_t1 == 0
12991                && u_t2 == 0
12992                && u_odef == 0
12993                && ((u_cond == 1 && uses_args == 0 && emit <= 2)
12994                    || (u_cond == 0 && emit <= 1))
12995        };
12996        if !ok {
12997            return false;
12998        }
12999    }
13000
13001    // ---- assembly ----
13002    let mut call_stmt = call_list[call_at].clone();
13003    if let Stmt::ExprStmt(Expr::Method { args: slot, .. }) = &mut call_stmt {
13004        *slot = merged;
13005    }
13006    let residual = |v: u32| -> usize {
13007        cnt(&tail1_counts, v) + cnt(&tail2_counts, v) + cnt(&post_counts, v)
13008    };
13009    let mut kept_prelude: Vec<Stmt> = Vec::new();
13010    for (i, s) in stmts[..if_pos].iter().enumerate() {
13011        let consumed = match s {
13012            Stmt::LocalDef { var, .. } => Some(*var),
13013            Stmt::ExprStmt(Expr::Assign { target, .. }) => match &**target {
13014                Expr::Local { var, .. } => Some(*var),
13015                _ => None,
13016            },
13017            Stmt::If { .. } => cond_def_of(s, stmts).map(|(v, _)| v),
13018            _ => None,
13019        };
13020        if let Some(v) = consumed {
13021            if inlined.contains(&v) && pidx.get(&v) == Some(&i) && residual(v) == 0 {
13022                continue;
13023            }
13024        }
13025        kept_prelude.push(s.clone());
13026    }
13027    let build_keep = |list: &[&Stmt], side: &Side| -> Vec<Stmt> {
13028        match side {
13029            Side::Clean => list.iter().map(|s| (*s).clone()).collect(),
13030            Side::Call { at, .. } => {
13031                let mut out = Vec::new();
13032                for (i, s) in list.iter().enumerate() {
13033                    if i == *at {
13034                        continue;
13035                    }
13036                    if i < *at {
13037                        if let Some((v, _)) = def_of(s) {
13038                            if inlined.contains(&v) && residual(v) == 0 {
13039                                continue;
13040                            }
13041                        }
13042                    }
13043                    out.push((*s).clone());
13044                }
13045                out
13046            }
13047        }
13048    };
13049    let then_keep = build_keep(&then_list, &t_side);
13050    let else_keep = build_keep(&else_list, &e_side);
13051    let else_part = if else_keep.is_empty() && !had_else {
13052        None
13053    } else {
13054        Some(Box::new(Stmt::Block(else_keep)))
13055    };
13056    let new_if = Stmt::If {
13057        cond: cond_i,
13058        then_stmt: Box::new(Stmt::Block(then_keep)),
13059        else_stmt: else_part,
13060    };
13061    let if_empty = match &new_if {
13062        Stmt::If { then_stmt, else_stmt, .. } => {
13063            let be = |s: &Stmt| matches!(s, Stmt::Block(v) if v.is_empty());
13064            be(then_stmt) && else_stmt.as_ref().is_none_or(|e| be(e))
13065        }
13066        _ => false,
13067    };
13068    let mut out: Vec<Stmt> = Vec::with_capacity(stmts.len());
13069    out.push(call_stmt);
13070    out.extend(kept_prelude);
13071    if !if_empty {
13072        out.push(new_if);
13073    }
13074    out.extend(stmts[if_pos + 1..].to_vec());
13075    *stmts = out;
13076    true
13077}
13078
13079
13080pub fn strip_enum_ctor_super(body: &mut Stmt) {
13081    fn is_enum_super(s: &Stmt) -> bool {
13082        matches!(
13083            s,
13084            Stmt::ExprStmt(Expr::Method { name, is_super: true, .. }) if &**name == "<init>"
13085        )
13086    }
13087    // FULL depth: hotfix-guarded ctors hide a second super(String,int)
13088    // inside the proxy branch (alipay InstantRun: `if (proxy != null) {
13089    // super((String) v3[0], ..); proxy.afterSuper(this); return; }` at
13090    // depth 3+; the old two-level strip left it — super resolved against
13091    // Object once the fallback enum lost `extends Enum`, alipay
13092    // ctor-arity ×3,364). This path only runs for fallback enums whose
13093    // dex super is Enum/Object, so EVERY super call is a dead Enum-ctor
13094    // reference wherever it sits; constant subclasses (real supers)
13095    // take the other branch in method.rs and are untouched.
13096    fn strip_deep(stmts: &mut Vec<Stmt>) {
13097        stmts.retain(|s| !is_enum_super(s));
13098        for st in stmts.iter_mut() {
13099            strip_one(st);
13100        }
13101    }
13102    fn strip_one(st: &mut Stmt) {
13103        match st {
13104            Stmt::Block(v) => strip_deep(v),
13105            Stmt::If { then_stmt, else_stmt, .. } => {
13106                strip_one(then_stmt);
13107                if let Some(e) = else_stmt {
13108                    strip_one(e);
13109                }
13110            }
13111            Stmt::While { body, .. }
13112            | Stmt::DoWhile { body, .. }
13113            | Stmt::ForEach { body, .. }
13114            | Stmt::Synchronized { body, .. }
13115            | Stmt::Labeled { body, .. } => strip_one(body),
13116            Stmt::For { init, body, .. } => {
13117                strip_deep(init);
13118                strip_one(body);
13119            }
13120            Stmt::Switch { cases, default, .. } => {
13121                for c in cases.iter_mut() {
13122                    strip_deep(&mut c.body);
13123                }
13124                if let Some(d) = default {
13125                    strip_one(d);
13126                }
13127            }
13128            Stmt::Try { body, catches, finally }
13129            | Stmt::TryWithResources { body, catches, finally, .. } => {
13130                strip_one(body);
13131                for c in catches.iter_mut() {
13132                    strip_one(&mut c.body);
13133                }
13134                if let Some(f) = finally {
13135                    strip_one(f);
13136                }
13137            }
13138            _ => {}
13139        }
13140    }
13141    let Stmt::Block(stmts) = body else {
13142        return;
13143    };
13144    strip_deep(stmts);
13145}
13146
13147pub fn ensure_declared(body: &mut Stmt, vt: &VarTable) {
13148    // (Perf: dense Vec<bool> tables instead of HashSets — var ids are
13149    // dense; the `assigned` set collected here was never read and cost a
13150    // full extra tree walk per method.)
13151    let n = vt.vars.len().max(1);
13152
13153    // Demote extra LocalDefs (same var declared more than once) FIRST —
13154    // the scope analysis below must see exactly one def per var.
13155    let mut seen: HashSet<u32> = HashSet::default();
13156    demote_dup_decls(body, &mut seen, vt);
13157
13158    // Scope hoist: a LocalDef inside a nested block whose var is also
13159    // referenced OUTSIDE that block is a Java scope violation (the
13160    // branch-local `int v8 = pi.versionCode;` read after the try —
13161    // "cannot find symbol" at every outside use). Demote those defs to
13162    // assignments and let the bare top-of-method declaration below cover
13163    // the var. Two cheap passes: pre-order block numbering + per-var
13164    // inside/outside occurrence counts (pre-order subtree = id range).
13165    let mut def_block: Vec<u32> = vec![u32::MAX; n];
13166    let mut sizes: Vec<u32> = vec![0];
13167    let mut next_id: u32 = 1;
13168    scope_pass_a(body, 0, &mut next_id, &mut def_block, &mut sizes);
13169    sizes[0] = next_id;
13170    let mut within: Vec<u32> = vec![0; n];
13171    let mut total: Vec<u32> = vec![0; n];
13172    let mut next_b: u32 = 1;
13173    scope_pass_b(body, 0, &mut next_b, &def_block, &sizes, &mut within, &mut total);
13174    let mut hoist: HashSet<u32> = HashSet::default();
13175    for v in 0..n {
13176        if def_block[v] != u32::MAX && def_block[v] != 0 && within[v] < total[v] {
13177            hoist.insert(v as u32);
13178        }
13179    }
13180    if !hoist.is_empty() {
13181        let mut seed: HashSet<u32> = hoist.iter().copied().collect();
13182        demote_dup_decls(body, &mut seed, vt);
13183    }
13184
13185    let mut declared: Vec<bool> = vec![false; n];
13186    let mut is_param: Vec<bool> = vec![false; n];
13187    for v in &vt.vars {
13188        if v.is_param && (v.id as usize) < n {
13189            is_param[v.id as usize] = true;
13190        }
13191    }
13192    // Catch parameters are declared by the `catch (Type v)` clause —
13193    // bind_catches consumed their LocalDef before this pass ran, so
13194    // without this they re-declare at the top of the method and clash
13195    // with the clause (`Throwable th2;` + `catch (Throwable th2)` — a
13196    // syntax gate cannot see this, it is a semantic "already defined").
13197    let mut is_catch: Vec<bool> = vec![false; n];
13198    walk_all(body, &mut |st| {
13199        if let Stmt::Try { catches, .. } = st {
13200            for c in catches {
13201                if (c.var as usize) < n {
13202                    is_catch[c.var as usize] = true;
13203                }
13204            }
13205        }
13206    });
13207    walk_all(body, &mut |st| {
13208        if let Stmt::LocalDef { var, .. } = st {
13209            if (*var as usize) < n {
13210                declared[*var as usize] = true;
13211            }
13212        }
13213    });
13214    let mut used: HashSet<u32> = HashSet::default();
13215    // assignments=true: a var that only ever appears as an assign
13216    // TARGET still needs a declaration (`x = 5;` alone does not
13217    // declare x in Java).
13218    stmt_collect_vars(body, &mut used, true);
13219
13220    // Params are declared by the signature; hoisted vars lost their
13221    // LocalDef to the demotion above and always need the bare decl.
13222    let mut needs_decl: Vec<u32> = used
13223        .into_iter()
13224        .filter(|v| {
13225            let i = *v as usize;
13226            i >= n
13227                || ((!declared[i] && !is_param[i] && !is_catch[i]) || hoist.contains(v))
13228        })
13229        .collect();
13230    needs_decl.sort_unstable();
13231    needs_decl.dedup();
13232
13233    if !needs_decl.is_empty() {
13234        let mut decls: Vec<Stmt> = needs_decl
13235            .into_iter()
13236            .map(|v| Stmt::LocalDef {
13237                var: v,
13238                init: None,
13239                is_final: false,
13240                force_type: true,
13241            })
13242            .collect();
13243        decls.reverse();
13244        match body {
13245            Stmt::Block(v) => {
13246                for d in decls {
13247                    v.insert(0, d);
13248                }
13249            }
13250            other => {
13251                let inner = std::mem::replace(other, Stmt::Block(vec![]));
13252                *other = Stmt::Block({
13253                    let mut vs = decls;
13254                    vs.push(inner);
13255                    vs
13256                });
13257            }
13258        }
13259    }
13260}
13261
13262fn demote_dup_decls(s: &mut Stmt, seen: &mut HashSet<u32>, vt: &VarTable) {
13263    match s {
13264        // Exactly one visit per node: the Block arm consumes its children,
13265        // the fallthrough walk would double-visit them.
13266        Stmt::Block(v) => {
13267            for x in v.iter_mut() {
13268                demote_dup_decls(x, seen, vt);
13269            }
13270        }
13271        Stmt::LocalDef { var, init, .. } => {
13272            if seen.contains(var) {
13273                let ty = vt.var(*var).ty.clone();
13274                if let Some(e) = init.take() {
13275                    *s = Stmt::ExprStmt(Expr::Assign {
13276                        target: Box::new(Expr::Local { var: *var, ty }),
13277                        op: AssignOp::Plain,
13278                        value: Box::new(e),
13279                    });
13280                } else {
13281                    *s = Stmt::Block(vec![]);
13282                }
13283            } else {
13284                seen.insert(*var);
13285            }
13286        }
13287        other => {
13288            walk_mut(other, &mut |x| demote_dup_decls(x, seen, vt));
13289        }
13290    }
13291}
13292
13293#[allow(dead_code)]
13294fn unused(_: &Vec<CaseGroup>, _: &Catch) {}
13295
13296// ---------------------------------------------------------------------------
13297// jadx-style local names (ApplyVariableNames + dexdec's port of it)
13298// ---------------------------------------------------------------------------
13299
13300/// A synthetic `v12`/`p3` never survives when a better name exists.
13301/// Priority: (1) a Kotlin `Intrinsics.checkNotNullParameter(x, "name")`
13302/// names `x` from the message string (the string IS the parameter name);
13303/// (2) the single defining call — `getFoo()` → `foo`, `isFinishing()` →
13304/// `finishing`, `new File(…)` → `file`; (3) a type alias or the
13305/// lowercased class simple name. Collisions take `2`, `3`, …; a var with
13306/// two DISAGREEING defining calls stays unnamed (dexdec's
13307/// RelationalNameInference rule). Debug-info names are never touched
13308/// (`synthetic_name` gates the whole pass).
13309pub fn apply_local_names(vt: &mut VarTable, body: &Stmt) {
13310    use std::collections::{HashMap, HashSet};
13311
13312    // ---- proposals ------------------------------------------------------
13313    // var → (name, source-priority); disagreeing call names are dropped.
13314    let mut by_call: HashMap<u32, Vec<String>> = HashMap::new();
13315    let mut by_intrinsics: HashMap<u32, String> = HashMap::new();
13316
13317    visit_all_exprs(body, &mut |e| {
13318        if let Expr::Method { cls, name, args, .. } = e {
13319            if (cls.as_ref() == "kotlin/jvm/internal/Intrinsics"
13320                || cls.as_ref() == "kotlin/jvm/internal/IntrinsicsKt")
13321                && matches!(name.as_ref(), "checkNotNullParameter" | "checkParameterIsNotNull")
13322                && args.len() == 2
13323            {
13324                if let (Expr::Local { var, .. }, Expr::Const(ConstVal::Str(s))) = (&args[0], &args[1])
13325                {
13326                    by_intrinsics
13327                        .entry(*var)
13328                        .or_insert_with(|| sanitize_name(s).unwrap_or_default());
13329                }
13330            }
13331        }
13332    });
13333
13334    walk_all(body, &mut |st| {
13335        let (var, value) = match st {
13336            Stmt::LocalDef { var, init: Some(e), .. } => (*var, e),
13337            Stmt::ExprStmt(Expr::Assign { target, value, .. })
13338                if matches!(&**target, Expr::Local { .. }) =>
13339            {
13340                let Expr::Local { var, .. } = &**target else { return };
13341                (*var, value.as_ref())
13342            }
13343            _ => return,
13344        };
13345        if let Some(n) = defining_call_name(value) {
13346            by_call.entry(var).or_default().push(n);
13347        }
13348    });
13349
13350    // ---- reservation + application --------------------------------------
13351    // Real (debug-info) names can legally REPEAT across sibling source
13352    // scopes (`int i` in two loops) but our flat declaration hoisting
13353    // puts them in one scope — de-duplicate with the same numeric
13354    // suffixes the claim path uses.
13355    // Uniqueness operates on the SANITIZED display name: the identifier
13356    // sanitizer maps non-ASCII to `_`, so distinct debug names
13357    // (`ERROR_token参数缺失` / `ERROR_channelId参数缺失`) both RENDER as
13358    // `ERROR________` and collide as declarations. Tracking raw names
13359    // here would let the collision through.
13360    let sanit = |n: &str| crate::classdec::java_ident(n).into_owned();
13361    let mut taken: HashSet<String> = HashSet::default();
13362    // Parameter names are occupied REGARDLESS of syntheticness: claim()
13363    // hands out rename candidates, and without the synthetic params in
13364    // the pool a type fallback (`l7.p0` → "p0") renamed one parameter
13365    // ONTO another's slot-name (`b(v6.l p0, Object obj, l7.p0 p0)`).
13366    for v in vt.vars.iter() {
13367        // ALL synthetic names occupy their names, not just parameters:
13368        // a type fallback (`pc5.v56` → "v56") otherwise renames a local
13369        // ONTO another local's slot-name (`int v56;` beside
13370        // `pc5.v56 v56;`).
13371        if v.synthetic_name {
13372            taken.insert(sanit(&v.name));
13373        }
13374    }
13375    for v in vt.vars.iter_mut() {
13376        if v.synthetic_name {
13377            continue;
13378        }
13379        if !taken.insert(sanit(&v.name)) {
13380            let base = sanit(&v.name);
13381            for i in 2.. {
13382                let cand = format!("{base}{i}");
13383                if taken.insert(cand.clone()) {
13384                    v.name = cand;
13385                    break;
13386                }
13387            }
13388        }
13389    }
13390    let claim = |taken: &mut HashSet<String>, want: &str| -> String {
13391        let w = crate::classdec::java_ident(want).into_owned();
13392        if taken.insert(w.clone()) {
13393            return w;
13394        }
13395        for i in 2.. {
13396            let cand = format!("{w}{i}");
13397            if taken.insert(cand.clone()) {
13398                return cand;
13399            }
13400        }
13401        w
13402    };
13403
13404    for info in vt.vars.iter_mut() {
13405        if !info.synthetic_name {
13406            continue;
13407        }
13408        // (1) Intrinsics string — the Kotlin compiler wrote the real
13409        // parameter name right into the check.
13410        if let Some(n) = by_intrinsics.get(&info.id) {
13411            if !n.is_empty() {
13412                info.name = claim(&mut taken, n);
13413                continue;
13414            }
13415        }
13416        // (2) One consistent defining call.
13417        if let Some(names) = by_call.get(&info.id) {
13418            let unique: HashSet<&String> = names.iter().collect();
13419            if unique.len() == 1 {
13420                let n = unique.into_iter().next().unwrap();
13421                if !is_java_keyword(n) {
13422                    info.name = claim(&mut taken, n);
13423                    continue;
13424                }
13425            }
13426        }
13427        // (3) Type alias, else the lowercased simple class name
13428        // (jadx names a `Looper` local `looper`).
13429        let want = type_alias(&info.ty).map(str::to_string).or_else(|| simple_type_name(&info.ty));
13430        if let Some(n) = want {
13431            info.name = claim(&mut taken, &n);
13432        }
13433    }
13434
13435    // Final uniquification across ALL vars (params, locals, catch): the
13436    // reservation, claim and debug-name domains above are checked
13437    // pairwise but a name claimed LAST can still equal a debug name
13438    // that never re-checked (`zd1.v2 v2` beside `zd1.v2[] v2`).
13439    let mut seen: HashSet<String> = HashSet::default();
13440    for v in vt.vars.iter_mut() {
13441        if !seen.insert(sanit(&v.name)) {
13442            let base = sanit(&v.name);
13443            for i in 2.. {
13444                let cand = format!("{base}{i}");
13445                if seen.insert(cand.clone()) {
13446                    v.name = cand;
13447                    break;
13448                }
13449            }
13450        }
13451    }
13452}
13453
13454/// `com/android/.../Looper` → `looper` — the alias-table fallback.
13455fn simple_type_name(ty: &TypeRef) -> Option<String> {
13456    let TypeRef::J(JavaType::Object(n)) = ty else {
13457        return None;
13458    };
13459    let simple = n.rsplit('/').next().unwrap_or(n);
13460    let simple = simple.rsplit('$').next().unwrap_or(simple);
13461    // Lowercase the first ALPHABETIC char — `$`/`_`-prefixed names
13462    // (R8's `$$$_Thread`) must not survive capitalized.
13463    let mut chars = simple.chars();
13464    let mut out = String::with_capacity(simple.len());
13465    let mut lowered = false;
13466    for c in chars.by_ref() {
13467        if c.is_ascii_alphabetic() && !lowered {
13468            out.push(c.to_ascii_lowercase());
13469            lowered = true;
13470        } else {
13471            out.push(c);
13472        }
13473        if lowered {
13474            break;
13475        }
13476    }
13477    out.extend(chars);
13478    sanitize_name(&out)
13479}
13480
13481/// `getFoo()` → `foo`, `isFinishing()` → `finishing`, `new File(…)` →
13482/// `file`. The get/is prefixes only strip when the remainder starts
13483/// uppercase (so `issues()` keeps its name).
13484fn defining_call_name(e: &Expr) -> Option<String> {
13485    match e {
13486        Expr::Method { name, .. } => {
13487            let base = name
13488                .strip_prefix("get")
13489                .or_else(|| name.strip_prefix("is"))
13490                .filter(|rest| rest.chars().next().is_some_and(|c| c.is_ascii_uppercase()))
13491                .unwrap_or(name);
13492            let first = base.chars().next()?;
13493            let mut out = String::with_capacity(base.len());
13494            out.push(first.to_ascii_lowercase());
13495            out.extend(base.chars().skip(1));
13496            sanitize_name(&out)
13497        }
13498        Expr::New { cls, .. } => {
13499            let simple = cls.rsplit('/').next().unwrap_or(cls);
13500            let simple = simple.rsplit('$').next().unwrap_or(simple);
13501            let first = simple.chars().next()?;
13502            let mut out = String::with_capacity(simple.len());
13503            out.push(first.to_ascii_lowercase());
13504            out.extend(simple.chars().skip(1));
13505            sanitize_name(&out)
13506        }
13507        _ => None,
13508    }
13509}
13510
13511/// Valid java identifier, not a keyword/restricted name, ≥2 chars.
13512fn sanitize_name(s: &str) -> Option<String> {
13513    let mut out = String::with_capacity(s.len());
13514    for c in s.chars() {
13515        if c.is_ascii_alphanumeric() || c == '_' || c == '$' {
13516            out.push(c);
13517        } else {
13518            out.push('_');
13519        }
13520    }
13521    if out.len() < 2
13522        || out.chars().next().is_some_and(|c| c.is_ascii_digit())
13523        || is_java_keyword(&out)
13524    {
13525        return None;
13526    }
13527    Some(out)
13528}
13529
13530/// jadx's alias table for the common types, with the Android globals the
13531/// corpus actually shows; anything else falls back in the caller.
13532fn type_alias(ty: &TypeRef) -> Option<&'static str> {
13533    let TypeRef::J(JavaType::Object(n)) = ty else {
13534        return None;
13535    };
13536    Some(match n.as_ref() {
13537        "java/lang/String" | "kotlin/String" => "str",
13538        "java/lang/Class" => "cls",
13539        "java/lang/Throwable" => "th",
13540        "java/lang/Object" | "kotlin/Any" => "obj",
13541        "java/util/Iterator" | "kotlin/collections/Iterator" => "it",
13542        "java/lang/Boolean" => "bool",
13543        "java/lang/Integer" => "num",
13544        "java/lang/Character" => "ch",
13545        "java/lang/Byte" => "b",
13546        "java/lang/Short" => "sh",
13547        "java/lang/Float" => "f",
13548        "java/lang/Double" => "d",
13549        "java/lang/Long" => "i",
13550        "java/lang/StringBuilder" => "sb",
13551        "java/util/ArrayList" => "list",
13552        "java/util/HashMap" => "map",
13553        "android/content/Context" => "context",
13554        "android/content/Intent" => "intent",
13555        "android/os/Bundle" => "bundle",
13556        "android/view/View" => "view",
13557        "android/graphics/Bitmap" => "bitmap",
13558        "android/view/ViewGroup" => "viewGroup",
13559        _ => return None,
13560    })
13561}
13562
13563/// Every expression in the tree, statements included (read-only).
13564pub(crate) fn visit_all_exprs<F: FnMut(&Expr)>(s: &Stmt, f: &mut F) {
13565    walk_all(s, &mut |st| {
13566        let exprs: Vec<&Expr> = match st {
13567            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
13568                let mut v: Vec<&Expr> = vec![value];
13569                if !matches!(&**target, Expr::Local { .. }) {
13570                    v.push(target);
13571                }
13572                v
13573            }
13574            Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
13575                vec![e]
13576            }
13577            Stmt::Return(Some(e)) => vec![e],
13578            Stmt::LocalDef { init: Some(e), .. } => vec![e],
13579            Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
13580                vec![cond]
13581            }
13582            _ => Vec::new(),
13583        };
13584        for e in exprs {
13585            visit_exprs(e, f);
13586        }
13587    });
13588}
13589
13590/// IntDef/LongDef exact-match rendering: a literal argument to a
13591/// platform method whose parameter carries a constant domain renders as
13592/// the named constant (`setVisibility(8)` → `android.view.View.GONE`).
13593/// Combined flag values and literals outside the domain stay numeric.
13594pub fn platform_constants(body: &mut Stmt) {
13595    if !crate::platform::installed() {
13596        return;
13597    }
13598    rewrite_exprs(body, &mut |e| {
13599        if let Expr::Method { cls, name, desc, args, .. } = e {
13600            for a in args.iter_mut() {
13601                let (value, is_long) = match a {
13602                    Expr::Const(ConstVal::Int(v)) => (*v as i64, false),
13603                    Expr::Const(ConstVal::Long(v)) => (*v, true),
13604                    _ => continue,
13605                };
13606                if let Some(named) =
13607                    crate::platform::named_constant(cls, name, &desc_to_string(desc), 0, value)
13608                {
13609                    let _ = is_long;
13610                    *a = Expr::Raw(named);
13611                }
13612            }
13613        }
13614    });
13615}
13616
13617/// MethodDescriptor → dex descriptor string (`(I)V`).
13618fn desc_to_string(d: &jdc_core::types::MethodDescriptor) -> String {
13619    let mut out = String::from("(");
13620    for a in &d.args {
13621        type_to_desc(a, &mut out);
13622    }
13623    out.push(')');
13624    type_to_desc(&d.ret, &mut out);
13625    out
13626}
13627
13628fn type_to_desc(t: &jdc_core::types::JavaType, out: &mut String) {
13629    match t {
13630        JavaType::Void => out.push('V'),
13631        JavaType::Boolean => out.push('Z'),
13632        JavaType::Byte => out.push('B'),
13633        JavaType::Char => out.push('C'),
13634        JavaType::Short => out.push('S'),
13635        JavaType::Int => out.push('I'),
13636        JavaType::Float => out.push('F'),
13637        JavaType::Long => out.push('J'),
13638        JavaType::Double => out.push('D'),
13639        JavaType::Object(n) => {
13640            out.push('L');
13641            out.push_str(n);
13642            out.push(';');
13643        }
13644        JavaType::Array(inner) => {
13645            out.push('[');
13646            type_to_desc(inner, out);
13647        }
13648    }
13649}
13650
13651/// Kotlin `Intrinsics` null-check elision (jadx's ProcessKotlinInternals):
13652/// statement-position `checkNotNullParameter(p, "name")` /
13653/// `checkParameterIsNotNull` / `checkNotNull…` calls are runtime
13654/// assertions — drop them. The naming pass harvests the parameter
13655/// strings FIRST, so run this after `apply_local_names`.
13656pub fn remove_kotlin_checks(s: &mut Stmt) {
13657    walk_mut_deep(s, &mut |st| {
13658        let Stmt::Block(v) = st else { return };
13659        v.retain(|x| !is_kotlin_check(x));
13660    });
13661}
13662
13663fn is_kotlin_check(s: &Stmt) -> bool {
13664    let Stmt::ExprStmt(e) = s else { return false };
13665    let Expr::Method { cls, name, .. } = e else { return false };
13666    (cls.as_ref() == "kotlin/jvm/internal/Intrinsics" || cls.as_ref() == "kotlin/jvm/internal/IntrinsicsKt__Jdk7Kt")
13667        && matches!(
13668            name.as_ref(),
13669            "checkNotNullParameter"
13670                | "checkParameterIsNotNull"
13671                | "checkExpressionValueIsNotNull"
13672                | "checkNotNullExpressionValue"
13673                | "checkReturnedValueIsNotNull"
13674                | "checkFieldIsNotNull"
13675                | "checkNotNull"
13676        )
13677}
13678
13679// ---------------------------------------------------------------------------
13680// Synthetic-accessor inlining (jadx's MarkMethodsForInline, the safe subset)
13681// ---------------------------------------------------------------------------
13682
13683/// Inline `access$NNN`-style synthetic static bridges at their call
13684/// sites: an identity (`return pN`), a getter (`iget pX, field;
13685/// return vR`), or a method forwarder (`invoke; return`) — with the
13686/// d8 APM trace wrappers (`MethodCollector.i/.o` const-only static
13687/// calls) tolerated around the core. Only STATIC + SYNTHETIC callees
13688/// qualify (compiler-generated bridges: no override semantics, no
13689/// side effects beyond the forwarded operation).
13690/// Can `host` (internal class name) source-level reference `target`'s
13691/// class chain and member? Cross-package inlining of a synthetic
13692/// accessor used to EXPOSE package-private targets the raw dex never
13693/// referenced cross-package (BuildersKt.launch$default forwards to
13694/// kotlinx.coroutines.k.e — the widening census cannot see post-inline
13695/// calls; lark's 1,019-line k不是公共的 family). Non-public chain link
13696/// or non-public member across packages → the inline must not happen
13697/// (the public synthetic bridge stays and compiles).
13698fn inline_ref_ok(pool: &DexPool, host: &str, target: &str, member: Option<(&str, bool)>) -> bool {
13699    let hpkg = match host.rfind('/') {
13700        Some(i) => &host[..i],
13701        None => "",
13702    };
13703    let mut cur = target;
13704    loop {
13705        let tpkg = match cur.rfind('/') {
13706            Some(i) => &cur[..i],
13707            None => "",
13708        };
13709        if tpkg != hpkg {
13710            if let Some(pc) = pool.get(cur) {
13711                if pc.access & crate::access::ACC_PUBLIC == 0 {
13712                    return false;
13713                }
13714                if cur == target {
13715                    if let Some((mname, is_method)) = member {
13716                        let ok = if is_method {
13717                            pc.all_methods().any(|m| {
13718                                &*m.name == mname
13719                                    && m.access & crate::access::ACC_PUBLIC != 0
13720                            })
13721                        } else {
13722                            pc.static_fields
13723                                .iter()
13724                                .chain(pc.instance_fields.iter())
13725                                .any(|f| {
13726                                    f.name == mname
13727                                        && f.access & crate::access::ACC_PUBLIC != 0
13728                                })
13729                        };
13730                        // A member match requires the exact member to be
13731                        // public; an absent member (renamed/collapsed)
13732                        // stays allowed — the render will show what the
13733                        // pool has.
13734                        if !ok
13735                            && (if is_method {
13736                                pc.all_methods().any(|m| &*m.name == mname)
13737                            } else {
13738                                pc.static_fields
13739                                    .iter()
13740                                    .chain(pc.instance_fields.iter())
13741                                    .any(|f| f.name == mname)
13742                            })
13743                        {
13744                            return false;
13745                        }
13746                    }
13747                }
13748            }
13749        }
13750        match cur.rfind('$') {
13751            Some(i) if i > 0 => cur = &cur[..i],
13752            _ => break,
13753        }
13754    }
13755    true
13756}
13757
13758pub fn inline_accessors(s: &mut Stmt, pool: &DexPool, host: &str) {
13759    rewrite_exprs(s, &mut |e| {
13760        let Expr::Method { cls, name, desc, args, is_static, .. } = e else { return };
13761        if !*is_static || args.is_empty() {
13762            return;
13763        }
13764        let Some(target) = pool.get(cls) else { return };
13765        let desc_s = desc_to_string(desc);
13766        let Some(m) = target.find_method(name, &desc_s) else { return };
13767        if !(m.is_static() && m.access & access::ACC_SYNTHETIC != 0) {
13768            return;
13769        }
13770        let Some(dex) = pool.dex(m.dex_idx) else { return };
13771        // Snapshot first: the accessor's owning image may already be
13772        // retired, and a live read's success would depend on worker
13773        // completion interleaving — nondeterministic inlining (whether
13774        // the accessor folds at all) between identical runs.
13775        let code = match pool.accessor_code(m.dex_idx, m.code_off) {
13776            Some(bytes) => ddc_dex::CodeItem::parse(&bytes, 0),
13777            None => dex.code_at(m.code_off),
13778        };
13779        let Some(code) = code else { return };
13780        let insns: Vec<&Insn> =
13781            code.insns.iter().filter(|i| !matches!(i.kind, InsnKind::Nop)).collect();
13782        // Strip the APM trace wrappers (const-only invoke-static).
13783        let core: Vec<&Insn> = insns
13784            .iter()
13785            .copied()
13786            .filter(|i| !is_const_only_trace(&i.kind, &insns))
13787            .collect();
13788        match accessor_shape(&dex, &core, &code) {
13789            Some(Shape::Identity(arg_i)) => {
13790                if let Some(a) = args.get(arg_i) {
13791                    *e = a.clone();
13792                }
13793            }
13794            Some(Shape::FieldRead { arg, cls: field_cls, field, field_ty }) => {
13795                if !inline_ref_ok(pool, host, &field_cls, Some((&field, false))) {
13796                    return;
13797                }
13798                if let Some(owner) = args.get(arg).cloned().map(Box::new) {
13799                    let ty = TypeRef::J(desc_type(&field_ty));
13800                    // The inlined read must consult the member rename
13801                    // registry like every lifted field ref does — the
13802                    // raw dex name desynced from renamed declarations
13803                    // (lark uu4/f$b's `a`→`ax6`: 找不到符号 变量 a ×4.4k
13804                    // when the nested-collision deshadow fired).
13805                    let name = jdc_core::rename::field_display(
13806                        &field_cls,
13807                        &field,
13808                        &field_ty,
13809                    )
13810                    .map(std::sync::Arc::from)
13811                    .unwrap_or_else(|| field.into());
13812                    *e = Expr::Field {
13813                        owner: Some(owner),
13814                        cls: field_cls.into(),
13815                        name,
13816                        ty,
13817                        is_static: false,
13818                    };
13819                }
13820            }
13821            Some(Shape::Forward { cls: tcls, name: tname, desc: tdesc, instance }) => {
13822                if !inline_ref_ok(pool, host, &tcls, Some((&tname, true))) {
13823                    return;
13824                }
13825                // Same registry duty as FieldRead: the forwarded member
13826                // must render under its renamed display, not the raw
13827                // dex name.
13828                let disp = jdc_core::rename::field_display(
13829                    &tcls,
13830                    &tname,
13831                    &tdesc.to_string(),
13832                )
13833                .map(std::sync::Arc::from)
13834                .unwrap_or_else(|| tname.into());
13835                *cls = tcls.into();
13836                *name = disp;
13837                *desc = std::sync::Arc::new(tdesc);
13838                if instance && !args.is_empty() {
13839                    // The first param (the receiver) becomes the owner.
13840                    let recv = args.remove(0);
13841                    if let Expr::Method { owner, .. } = e {
13842                        *owner = Some(Box::new(recv));
13843                    }
13844                }
13845            }
13846            None => {}
13847        }
13848    });
13849}
13850
13851/// What an accessor body reduces to.
13852enum Shape {
13853    /// `return pN` — the call becomes argument N.
13854    Identity(usize),
13855    /// `iget vR, pX, field; return vR` — becomes `argN.field`.
13856    FieldRead { arg: usize, cls: String, field: String, field_ty: String },
13857    /// `invoke {pX, args…}, method@M; (move-result;)? return` —
13858    /// becomes the forwarded call.
13859    Forward { cls: String, name: String, desc: MethodDescriptor, instance: bool },
13860}
13861
13862/// Resolve the IGet field's owner class, name and type descriptor.
13863fn field_of(dex: &ddc_dex::DexFile, field_idx: u32) -> Option<(String, String, String)> {
13864    let f = dex.field(field_idx);
13865    Some((
13866        // class_name strips the `L...;` shell; type_name would leak a
13867        // descriptor into the Field's owner class (`La3.a.e_`).
13868        dex.class_name(f.class_idx),
13869        dex.string(f.name_idx).to_string(),
13870        // The FIELD TYPE stays a descriptor: desc_type parses it.
13871        dex.type_name(f.type_idx).to_string(),
13872    ))
13873}
13874
13875/// Map a register to a parameter index (static method: params occupy
13876/// the LAST ins_size registers).
13877fn param_index(code: &ddc_dex::CodeItem, reg: u16) -> Option<usize> {
13878    let rs = code.registers_size as usize;
13879    let ins = code.ins_size as usize;
13880    let r = reg as usize;
13881    let first = rs.checked_sub(ins)?;
13882    (r >= first && r < rs).then(|| r - first)
13883}
13884
13885fn accessor_shape(
13886    dex: &ddc_dex::DexFile,
13887    core: &[&Insn],
13888    code: &ddc_dex::CodeItem,
13889) -> Option<Shape> {
13890    // Dead consts that fed the stripped trace wrappers remain in the
13891    // core — skip them. ONLY when actually dead (dst unread by the
13892    // rest): a const-returning synthetic (`lambda$new$4(String) {
13893    // return false; }` = `const/4 v0,0; return v0`, registers=1 so v0
13894    // IS the param) lost its const and matched Identity(param0) —
13895    // inline_accessors then replaced the CALL with its ARGUMENT
13896    // (`return (String) obj` against a boolean SAM — weibo
13897    // IntentSanitizer lambdas ×116, and silently WRONG renders
13898    // wherever the types happened to agree).
13899    let reads = |rest: &[&Insn], r: u16| {
13900        rest.iter()
13901            .any(|i| crate::lift::src_regs(&i.kind).contains(&r))
13902    };
13903    let core: &[&Insn] = match core.split_first() {
13904        Some((first, rest))
13905            if matches!(first.kind, InsnKind::Const { dst, .. } if !reads(rest, dst)) =>
13906        {
13907            rest
13908        }
13909        _ => core,
13910    };
13911    let core: &[&Insn] = match core.split_last() {
13912        Some((last, rest))
13913            if matches!(last.kind, InsnKind::Const { dst, .. } if !reads(rest, dst)) =>
13914        {
13915            rest
13916        }
13917        _ => core,
13918    };
13919    match core {
13920        // return pN
13921        [Insn { kind: InsnKind::Return { src }, .. }] => {
13922            let idx = param_index(code, *src)?;
13923            Some(Shape::Identity(idx))
13924        }
13925        // iget vR, pX, field; return vR
13926        [
13927            Insn { kind: InsnKind::IGet { dst, obj, field_idx }, .. },
13928            Insn { kind: InsnKind::Return { src }, .. },
13929        ] if dst == src => {
13930            let arg = param_index(code, *obj)?;
13931            let (cls, field, field_ty) = field_of(dex, *field_idx)?;
13932            Some(Shape::FieldRead { arg, cls, field, field_ty })
13933        }
13934        // invoke {…}, method@M; (move-result; return)? — forwarder.
13935        [Insn { kind: InsnKind::Invoke { method_idx, regs, kind, .. }, .. }, rest @ ..] if rest.len() <= 2 => {
13936            // Only forward when every register is a param (no locals).
13937            if !regs.iter().all(|r| param_index(code, *r).is_some()) || regs.is_empty() {
13938                return None;
13939            }
13940            // Trailing must be move-result+return or return/void —
13941            // anything else (e.g. trace calls) already filtered upstream.
13942            if rest.len() == 2 {
13943                let ok = matches!(rest[0].kind, InsnKind::MoveResult { .. })
13944                    && matches!(rest[1].kind, InsnKind::Return { .. } | InsnKind::ReturnVoid);
13945                if !ok {
13946                    return None;
13947                }
13948            }
13949            let mid = dex.method(*method_idx);
13950            // class_name strips the `L...;` shell — type_name leaks a
13951            // descriptor into the forwarded call's class (`La3.a.e_`).
13952            let cls = dex.class_name(mid.class_idx);
13953            let name = dex.string(mid.name_idx).to_string();
13954            let mut s = String::from("(");
13955            for t in dex.proto_params(mid.proto_idx) {
13956                s.push_str(dex.type_name(*t));
13957            }
13958            s.push(')');
13959            s.push_str(dex.type_name(dex.proto(mid.proto_idx).return_type_idx));
13960            let desc = parse_method_descriptor(&s)?;
13961            Some(Shape::Forward { cls, name, desc, instance: !matches!(kind, InvokeKind::Static) })
13962        }
13963        _ => None,
13964    }
13965}
13966
13967
13968/// A const-only invoke-static (the d8 APM `i(732046)` / `o(732046)`
13969/// trace wrapper): every argument register is loaded by a Const (or a
13970/// Const-into-move chain) in the SAME body. Scoped to synthetic
13971/// accessors, so a genuinely side-effecting const call is never
13972/// mistaken for a trace.
13973fn is_const_only_trace(kind: &InsnKind, insns: &[&Insn]) -> bool {
13974    let InsnKind::Invoke { kind, regs, .. } = kind else { return false };
13975    if !matches!(kind, InvokeKind::Static) || regs.is_empty() {
13976        return false;
13977    }
13978    regs.iter().all(|r| const_loaded(*r, insns, 0))
13979}
13980
13981/// Const-loaded directly, or through a Move chain from a const-loaded
13982/// register (the APM `const v0, id` → `invoke {v0}` shape).
13983fn const_loaded(r: u16, insns: &[&Insn], depth: u8) -> bool {
13984    if depth > 3 {
13985        return false;
13986    }
13987    insns.iter().any(|i| match &i.kind {
13988        InsnKind::Const { dst, .. } => *dst == r,
13989        InsnKind::Move { dst, src } => *dst == r && const_loaded(*src, insns, depth + 1),
13990        _ => false,
13991    })
13992}