Skip to main content

ddc_dec/
passes.rs

1//! Post-convert refinement passes for DEX-lifted statement trees.
2//!
3//! The converter hands back a correct-but-raw tree; these passes restore the
4//! source shapes a register machine loses: catch parameter binding,
5//! copy-forwarding (single-use temporaries), if/else→ternary folding,
6//! `StringBuilder` chain → `+` concatenation, `synchronized` recovery from
7//! the d8 monitor pattern, erased-type inference, boolean and null
8//! comparisons, and declaration hygiene.
9
10// The tree-walker match arms intentionally mirror the statement grammar
11// one level at a time; collapsing the nested `if let`s into outer match
12// arms would trade per-arm clarity for lint silence.
13#![allow(clippy::collapsible_match)]
14
15use jdc_core::FxHashSet as HashSet;
16
17// The tree-walker match arms intentionally mirror the statement grammar
18// one level at a time; collapsing the nested `if let`s into outer match
19// arms would trade per-arm clarity for lint silence.
20use jdc_core::ir::build::has_side_effects;
21use jdc_core::emit::is_java_keyword;
22use jdc_core::ir::expr::{AssignOp, BinOp, ConcatPart, ConstVal, Expr, TypeRef, UnOp};
23use jdc_core::ir::stmt::{CaseGroup, Catch, Stmt};
24use jdc_core::types::{JavaType, MethodDescriptor};
25use jdc_core::var::VarTable;
26
27use crate::lift::MethodEnv;
28use crate::{access, desc_type, DexPool};
29use ddc_dex::insn::{Insn, InsnKind, InvokeKind};
30use jdc_core::types::parse_method_descriptor;
31
32// ---------------------------------------------------------------------------
33// Generic tree walking
34// ---------------------------------------------------------------------------
35
36/// Deep expression rewrite over a statement tree.
37pub fn rewrite_exprs<F: FnMut(&mut Expr)>(s: &mut Stmt, f: &mut F) {
38    walk_stmt_exprs(s, f);
39}
40
41pub(crate) fn walk_stmt_exprs<F: FnMut(&mut Expr)>(s: &mut Stmt, f: &mut F) {
42    match s {
43        Stmt::Block(v) => {
44            for x in v.iter_mut() {
45                walk_stmt_exprs(x, f);
46            }
47        }
48        Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
49            f(e);
50        }
51        Stmt::Return(Some(e)) => f(e),
52        Stmt::LocalDef { init, .. } => {
53            if let Some(e) = init {
54                f(e);
55            }
56        }
57        Stmt::If {
58            cond,
59            then_stmt,
60            else_stmt,
61        } => {
62            f(cond);
63            walk_stmt_exprs(then_stmt, f);
64            if let Some(e) = else_stmt {
65                walk_stmt_exprs(e, f);
66            }
67        }
68        Stmt::While { cond, body } => {
69            f(cond);
70            walk_stmt_exprs(body, f);
71        }
72        Stmt::DoWhile { body, cond } => {
73            walk_stmt_exprs(body, f);
74            f(cond);
75        }
76        Stmt::For {
77            init,
78            cond,
79            update,
80            body,
81        } => {
82            for x in init.iter_mut() {
83                walk_stmt_exprs(x, f);
84            }
85            if let Some(c) = cond {
86                f(c);
87            }
88            for u in update.iter_mut() {
89                f(u);
90            }
91            walk_stmt_exprs(body, f);
92        }
93        Stmt::ForEach { iterable, body, .. } => {
94            f(iterable);
95            walk_stmt_exprs(body, f);
96        }
97        Stmt::Switch {
98            selector,
99            cases,
100            default,
101            ..
102        } => {
103            f(selector);
104            for c in cases {
105                for x in c.body.iter_mut() {
106                    walk_stmt_exprs(x, f);
107                }
108                if let Some(g) = &mut c.guard {
109                    f(g);
110                }
111            }
112            if let Some(d) = default {
113                walk_stmt_exprs(d, f);
114            }
115        }
116        Stmt::Try {
117            body,
118            catches,
119            finally,
120        } => {
121            walk_stmt_exprs(body, f);
122            for c in catches {
123                walk_stmt_exprs(&mut c.body, f);
124            }
125            if let Some(fl) = finally {
126                walk_stmt_exprs(fl, f);
127            }
128        }
129        Stmt::TryWithResources {
130            resources,
131            body,
132            catches,
133            finally,
134        } => {
135            for x in resources.iter_mut() {
136                walk_stmt_exprs(x, f);
137            }
138            walk_stmt_exprs(body, f);
139            for c in catches {
140                walk_stmt_exprs(&mut c.body, f);
141            }
142            if let Some(fl) = finally {
143                walk_stmt_exprs(fl, f);
144            }
145        }
146        Stmt::Assert { cond, msg } => {
147            f(cond);
148            if let Some(m) = msg {
149                f(m);
150            }
151        }
152        Stmt::Synchronized { lock, body } => {
153            f(lock);
154            walk_stmt_exprs(body, f);
155        }
156        Stmt::TernaryValue { e } => f(e),
157        Stmt::Labeled { body, .. } => walk_stmt_exprs(body, f),
158        Stmt::Goto(_) | Stmt::Label(_) | Stmt::Break(_) | Stmt::Continue(_) => {}
159        _ => {}
160    }
161}
162
163/// Immutable mirror of `walk_stmt_exprs`: read-only visitors must not
164/// have to clone a statement tree just to obtain a `&mut` handle.
165/// (count_locals_stmts cloned EVERY counted statement — 29k of 50k
166/// Stmt::clone samples in corpus profiles, ~25% of total CPU.)
167pub(crate) fn visit_stmt_exprs_ro<F: FnMut(&Expr)>(s: &Stmt, f: &mut F) {
168    match s {
169        Stmt::Block(v) => {
170            for x in v {
171                visit_stmt_exprs_ro(x, f);
172            }
173        }
174        Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
175            f(e);
176        }
177        Stmt::Return(Some(e)) => f(e),
178        Stmt::LocalDef { init: Some(e), .. } => f(e),
179        Stmt::If {
180            cond,
181            then_stmt,
182            else_stmt,
183        } => {
184            f(cond);
185            visit_stmt_exprs_ro(then_stmt, f);
186            if let Some(e) = else_stmt {
187                visit_stmt_exprs_ro(e, f);
188            }
189        }
190        Stmt::While { cond, body } => {
191            f(cond);
192            visit_stmt_exprs_ro(body, f);
193        }
194        Stmt::DoWhile { body, cond } => {
195            visit_stmt_exprs_ro(body, f);
196            f(cond);
197        }
198        Stmt::For {
199            init,
200            cond,
201            update,
202            body,
203        } => {
204            for x in init {
205                visit_stmt_exprs_ro(x, f);
206            }
207            if let Some(c) = cond {
208                f(c);
209            }
210            for u in update {
211                f(u);
212            }
213            visit_stmt_exprs_ro(body, f);
214        }
215        Stmt::ForEach { iterable, body, .. } => {
216            f(iterable);
217            visit_stmt_exprs_ro(body, f);
218        }
219        Stmt::Switch {
220            selector,
221            cases,
222            default,
223            ..
224        } => {
225            f(selector);
226            for c in cases {
227                for x in &c.body {
228                    visit_stmt_exprs_ro(x, f);
229                }
230                if let Some(g) = &c.guard {
231                    f(g);
232                }
233            }
234            if let Some(d) = default {
235                visit_stmt_exprs_ro(d, f);
236            }
237        }
238        Stmt::Try {
239            body,
240            catches,
241            finally,
242        } => {
243            visit_stmt_exprs_ro(body, f);
244            for c in catches {
245                visit_stmt_exprs_ro(&c.body, f);
246            }
247            if let Some(fl) = finally {
248                visit_stmt_exprs_ro(fl, f);
249            }
250        }
251        Stmt::TryWithResources {
252            resources,
253            body,
254            catches,
255            finally,
256            ..
257        } => {
258            for x in resources {
259                visit_stmt_exprs_ro(x, f);
260            }
261            visit_stmt_exprs_ro(body, f);
262            for c in catches {
263                visit_stmt_exprs_ro(&c.body, f);
264            }
265            if let Some(fl) = finally {
266                visit_stmt_exprs_ro(fl, f);
267            }
268        }
269        Stmt::Assert { cond, msg } => {
270            f(cond);
271            if let Some(m) = msg {
272                f(m);
273            }
274        }
275        Stmt::Synchronized { lock, body } => {
276            f(lock);
277            visit_stmt_exprs_ro(body, f);
278        }
279        Stmt::TernaryValue { e } => f(e),
280        Stmt::Labeled { body, .. } => visit_stmt_exprs_ro(body, f),
281        _ => {}
282    }
283}
284
285/// Count Local reads with the lost-alloc `<init>`-owner exclusion,
286/// zero-clone: mirrors `strip_lost_alloc_owners` + deep_rewrite count
287/// exactly (the strip replaced the owner subtree with Const(0), so the
288/// owner's locals were never counted; nested exclusions cannot double-
289/// skip because the excluded owner is never descended into).
290fn count_expr_ro(e: &Expr, m: &mut jdc_core::FxHashMap<u32, usize>) {
291    if let Expr::Local { var, .. } = e {
292        *m.entry(*var).or_insert(0) += 1;
293        return;
294    }
295    if let Expr::Method {
296        name,
297        is_special: true,
298        owner: Some(o),
299        args,
300        ..
301    } = e
302    {
303        if &**name == "<init>" && !matches!(**o, Expr::This) {
304            for a in args {
305                count_expr_ro(a, m);
306            }
307            return;
308        }
309    }
310    for_each_child(e, &mut |c| count_expr_ro(c, m));
311}
312
313/// Visit every expression (immutable).
314fn visit_exprs<F: FnMut(&Expr)>(e: &Expr, f: &mut F) {
315    f(e);
316    for_each_child(e, &mut |c| visit_exprs(c, f));
317}
318
319fn for_each_child<F: FnMut(&Expr)>(e: &Expr, f: &mut F) {
320    match e {
321        Expr::Un { e, .. } | Expr::Cast { e, .. } | Expr::InstanceOf { e, .. } => f(e),
322        Expr::Bin { l, r, .. } => {
323            f(l);
324            f(r);
325        }
326        Expr::Cond { c, t, f: fe } => {
327            f(c);
328            f(t);
329            f(fe);
330        }
331        Expr::Assign { target, value, .. } => {
332            f(target);
333            f(value);
334        }
335        Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => f(e),
336        Expr::Field { owner, .. } => {
337            if let Some(o) = owner {
338                f(o);
339            }
340        }
341        Expr::Method { owner, args, .. } => {
342            if let Some(o) = owner {
343                f(o);
344            }
345            for a in args {
346                f(a);
347            }
348        }
349        Expr::ArrayIndex { array, index } => {
350            f(array);
351            f(index);
352        }
353        Expr::New { args, .. } => {
354            for a in args {
355                f(a);
356            }
357        }
358        Expr::NewArray { dims, init, .. } => {
359            for d in dims {
360                f(d);
361            }
362            if let Some(v) = init {
363                for x in v {
364                    f(x);
365                }
366            }
367        }
368        Expr::NewMultiArray { dims, .. } => {
369            for d in dims {
370                f(d);
371            }
372        }
373        Expr::StringConcat(parts) => {
374            for p in parts {
375                if let ConcatPart::Str(x) = p {
376                    f(x);
377                }
378            }
379        }
380        Expr::Invokedynamic { args, .. } => {
381            for a in args {
382                f(a);
383            }
384        }
385        Expr::AnonNew { args, .. } => {
386            for a in args {
387                f(a);
388            }
389        }
390        _ => {}
391    }
392}
393
394/// Mutable child walk (one level).
395fn for_each_child_mut<F: FnMut(&mut Expr)>(e: &mut Expr, f: &mut F) {
396    match e {
397        Expr::Un { e, .. } | Expr::Cast { e, .. } | Expr::InstanceOf { e, .. } => f(e),
398        Expr::Bin { l, r, .. } => {
399            f(l);
400            f(r);
401        }
402        Expr::Cond { c, t, f: fe } => {
403            f(c);
404            f(t);
405            f(fe);
406        }
407        Expr::Assign { target, value, .. } => {
408            f(target);
409            f(value);
410        }
411        Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => f(e),
412        Expr::Field { owner, .. } => {
413            if let Some(o) = owner {
414                f(o);
415            }
416        }
417        Expr::Method { owner, args, .. } => {
418            if let Some(o) = owner {
419                f(o);
420            }
421            for a in args.iter_mut() {
422                f(a);
423            }
424        }
425        Expr::ArrayIndex { array, index } => {
426            f(array);
427            f(index);
428        }
429        Expr::New { args, .. } => {
430            for a in args.iter_mut() {
431                f(a);
432            }
433        }
434        Expr::NewArray { dims, init, .. } => {
435            for d in dims.iter_mut() {
436                f(d);
437            }
438            if let Some(v) = init {
439                for x in v.iter_mut() {
440                    f(x);
441                }
442            }
443        }
444        Expr::NewMultiArray { dims, .. } => {
445            for d in dims.iter_mut() {
446                f(d);
447            }
448        }
449        Expr::StringConcat(parts) => {
450            for p in parts {
451                if let ConcatPart::Str(x) = p {
452                    f(x);
453                }
454            }
455        }
456        Expr::Invokedynamic { args, .. } => {
457            for a in args.iter_mut() {
458                f(a);
459            }
460        }
461        Expr::AnonNew { args, .. } => {
462            for a in args.iter_mut() {
463                f(a);
464            }
465        }
466        _ => {}
467    }
468}
469
470/// Deep mutable expression rewrite.
471pub(crate) fn deep_rewrite<F: FnMut(&mut Expr)>(e: &mut Expr, f: &mut F) {
472    f(e);
473    for_each_child_mut(e, &mut |c| deep_rewrite(c, f));
474}
475
476/// `deep_rewrite` that respects WRITE positions: a `Local` appearing as
477/// an assignment target or a `++`/`--` operand never reaches `f`.
478/// Value-forwarding closures (`*x = value`) must use this — plain
479/// deep_rewrite turned a forwarded `vX = 25` into `25 = 25` (target
480/// replaced, and drop_defs then no longer recognized the statement, so
481/// the garbage survived into the output; reqable a4/e). Compound
482/// targets (`a[i] = …`, `o.f = …`) still recurse: owners and indices
483/// ARE reads.
484/// Read-only mirror of `deep_rewrite_reads` (same descent rules: a
485/// plain Local assignment/inc-dec target is a WRITE and never reaches
486/// `f`; compound targets still descend — owners and indices are reads).
487fn visit_exprs_reads<F: FnMut(&Expr)>(e: &Expr, f: &mut F) {
488    f(e);
489    match e {
490        Expr::Assign { target, value, .. } => {
491            if !matches!(**target, Expr::Local { .. }) {
492                visit_exprs_reads(target, f);
493            }
494            visit_exprs_reads(value, f);
495        }
496        Expr::PreIncDec { e: inner, .. } | Expr::PostIncDec { e: inner, .. } => {
497            if !matches!(**inner, Expr::Local { .. }) {
498                visit_exprs_reads(inner, f);
499            }
500        }
501        _ => for_each_child(e, &mut |c| visit_exprs_reads(c, f)),
502    }
503}
504
505fn deep_rewrite_reads<F: FnMut(&mut Expr)>(e: &mut Expr, f: &mut F) {
506    f(e);
507    match e {
508        Expr::Assign { target, value, .. } => {
509            if !matches!(**target, Expr::Local { .. }) {
510                deep_rewrite_reads(target, f);
511            }
512            deep_rewrite_reads(value, f);
513        }
514        Expr::PreIncDec { e: inner, .. } | Expr::PostIncDec { e: inner, .. } => {
515            if !matches!(**inner, Expr::Local { .. }) {
516                deep_rewrite_reads(inner, f);
517            }
518        }
519        _ => for_each_child_mut(e, &mut |c| deep_rewrite_reads(c, f)),
520    }
521}
522
523fn collect_vars(e: &Expr, out: &mut HashSet<u32>) {
524    visit_exprs(e, &mut |x| {
525        if let Expr::Local { var, .. } = x {
526            out.insert(*var);
527        }
528    });
529}
530
531/// Collect variable references (reads; `assignments` adds assignment
532/// targets). Single-pass recursion — the previous shape wrapped a
533/// self-recursive closure in `walk_all`, visiting every subtree twice over
534/// (exponential in nesting depth).
535fn stmt_collect_vars(s: &Stmt, out: &mut HashSet<u32>, assignments: bool) {
536    match s {
537        Stmt::Block(v) => {
538            for x in v {
539                stmt_collect_vars(x, out, assignments);
540            }
541        }
542        Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
543            if let Expr::Local { var, .. } = &**target {
544                if assignments {
545                    out.insert(*var);
546                }
547            } else {
548                collect_vars(target, out);
549            }
550            collect_vars(value, out);
551        }
552        Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
553            collect_vars(e, out)
554        }
555        Stmt::Return(Some(e)) => collect_vars(e, out),
556        Stmt::LocalDef { var, init, .. } => {
557            if assignments {
558                out.insert(*var);
559            }
560            if let Some(e) = init {
561                collect_vars(e, out);
562            }
563        }
564        Stmt::If {
565            cond,
566            then_stmt,
567            else_stmt,
568        } => {
569            collect_vars(cond, out);
570            stmt_collect_vars(then_stmt, out, assignments);
571            if let Some(e) = else_stmt {
572                stmt_collect_vars(e, out, assignments);
573            }
574        }
575        Stmt::While { cond, body } => {
576            collect_vars(cond, out);
577            stmt_collect_vars(body, out, assignments);
578        }
579        Stmt::DoWhile { body, cond } => {
580            stmt_collect_vars(body, out, assignments);
581            collect_vars(cond, out);
582        }
583        Stmt::For {
584            init,
585            cond,
586            update,
587            body,
588        } => {
589            for x in init {
590                stmt_collect_vars(x, out, assignments);
591            }
592            if let Some(c) = cond {
593                collect_vars(c, out);
594            }
595            for u in update {
596                collect_vars(u, out);
597            }
598            stmt_collect_vars(body, out, assignments);
599        }
600        Stmt::ForEach {
601            var,
602            iterable,
603            body,
604            ..
605        } => {
606            if assignments {
607                out.insert(*var);
608            }
609            collect_vars(iterable, out);
610            stmt_collect_vars(body, out, assignments);
611        }
612        Stmt::Switch {
613            selector,
614            cases,
615            default,
616            ..
617        } => {
618            collect_vars(selector, out);
619            for c in cases {
620                for x in &c.body {
621                    stmt_collect_vars(x, out, assignments);
622                }
623            }
624            if let Some(d) = default {
625                stmt_collect_vars(d, out, assignments);
626            }
627        }
628        Stmt::Try {
629            body,
630            catches,
631            finally,
632        } => {
633            stmt_collect_vars(body, out, assignments);
634            for c in catches {
635                stmt_collect_vars(&c.body, out, assignments);
636            }
637            if let Some(f) = finally {
638                stmt_collect_vars(f, out, assignments);
639            }
640        }
641        Stmt::TryWithResources {
642            resources,
643            body,
644            catches,
645            finally,
646        } => {
647            for x in resources {
648                stmt_collect_vars(x, out, assignments);
649            }
650            stmt_collect_vars(body, out, assignments);
651            for c in catches {
652                stmt_collect_vars(&c.body, out, assignments);
653            }
654            if let Some(f) = finally {
655                stmt_collect_vars(f, out, assignments);
656            }
657        }
658        Stmt::Synchronized { lock, body } => {
659            collect_vars(lock, out);
660            stmt_collect_vars(body, out, assignments);
661        }
662        Stmt::Assert { cond, msg } => {
663            collect_vars(cond, out);
664            if let Some(m) = msg {
665                collect_vars(m, out);
666            }
667        }
668        Stmt::Labeled { body, .. } => stmt_collect_vars(body, out, assignments),
669        _ => {}
670    }
671}
672
673pub(crate) fn walk_all<F: FnMut(&Stmt)>(s: &Stmt, f: &mut F) {
674    f(s);
675    match s {
676        Stmt::Block(v) => {
677            for x in v {
678                walk_all(x, f);
679            }
680        }
681        Stmt::If {
682            then_stmt,
683            else_stmt,
684            ..
685        } => {
686            walk_all(then_stmt, f);
687            if let Some(e) = else_stmt {
688                walk_all(e, f);
689            }
690        }
691        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => walk_all(body, f),
692        Stmt::For { init, body, .. } => {
693            for x in init {
694                walk_all(x, f);
695            }
696            walk_all(body, f);
697        }
698        Stmt::ForEach { body, .. } => walk_all(body, f),
699        Stmt::Switch { cases, default, .. } => {
700            for c in cases {
701                for x in &c.body {
702                    walk_all(x, f);
703                }
704            }
705            if let Some(d) = default {
706                walk_all(d, f);
707            }
708        }
709        Stmt::Try {
710            body,
711            catches,
712            finally,
713        } => {
714            walk_all(body, f);
715            for c in catches {
716                walk_all(&c.body, f);
717            }
718            if let Some(fl) = finally {
719                walk_all(fl, f);
720            }
721        }
722        Stmt::TryWithResources {
723            resources,
724            body,
725            catches,
726            finally,
727        } => {
728            for x in resources {
729                walk_all(x, f);
730            }
731            walk_all(body, f);
732            for c in catches {
733                walk_all(&c.body, f);
734            }
735            if let Some(fl) = finally {
736                walk_all(fl, f);
737            }
738        }
739        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => walk_all(body, f),
740        _ => {}
741    }
742}
743
744// ---------------------------------------------------------------------------
745// Passes
746// ---------------------------------------------------------------------------
747
748/// Bind catch parameters: the handler's first statement (a bare LocalDef
749/// from move-exception) becomes the catch variable.
750pub fn bind_catches(s: &mut Stmt, vt: &mut VarTable) {
751    // Vars defined anywhere in the method (params + LocalDefs +
752    // assign targets): a catch body reading a var OUTSIDE this set is
753    // the unmaterialized move-exception register (the lifter minted it
754    // without a defining statement).
755    let mut defined: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
756    for v in &vt.vars {
757        if v.is_param {
758            defined.insert(v.id);
759        }
760    }
761    collect_defined_locals(s, &mut defined);
762    let reads_all = count_locals_stmts(std::slice::from_ref(s));
763    // Vars with a real assignment (LocalDef-with-init or assign target):
764    // a var only ever READ is either the unmaterialized move-exception
765    // register or a bare declaration awaiting its hoisted assignment.
766    let mut assigned: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
767    collect_assigned_locals(s, &mut assigned);
768    let mut fallback_bound: Vec<u32> = Vec::new();
769    bind_catches_walk(s, vt, &defined, &reads_all, &assigned, &mut fallback_bound);
770    // The catch parameter IS the declaration now: drop the bare
771    // `Throwable th;` hoists for vars the fallback bound.
772    if !fallback_bound.is_empty() {
773        remove_bare_decls(s, &fallback_bound);
774    }
775}
776
777fn collect_defined_locals(s: &Stmt, out: &mut jdc_core::FxHashSet<u32>) {
778    crate::passes::walk_all(s, &mut |st| {
779        match st {
780            Stmt::LocalDef { var, .. } => {
781                out.insert(*var);
782            }
783            Stmt::ExprStmt(Expr::Assign { target, .. }) => {
784                if let Expr::Local { var, .. } = &**target {
785                    out.insert(*var);
786                }
787            }
788            _ => {}
789        }
790    });
791}
792
793/// Vars with a bare `LocalDef{init: None}` declaration in `s` (dedup,
794/// declaration order).
795fn collect_bare_decls(s: &Stmt, out: &mut Vec<u32>) {
796    walk_all(s, &mut |st| {
797        if let Stmt::LocalDef { var, init: None, .. } = st {
798            if !out.contains(var) {
799                out.push(*var);
800            }
801        }
802    });
803}
804
805
806/// First read of a local that is defined nowhere in the method (and is
807/// not a parameter) inside the catch body — the unmaterialized
808/// move-exception register.
809/// Vars carrying a real assignment (init or write target).
810fn collect_assigned_locals(s: &Stmt, out: &mut jdc_core::FxHashSet<u32>) {
811    crate::passes::walk_all(s, &mut |st| {
812        match st {
813            Stmt::LocalDef { var, init: Some(_), .. } => {
814                out.insert(*var);
815            }
816            Stmt::ExprStmt(Expr::Assign { target, .. }) => {
817                if let Expr::Local { var, .. } = &**target {
818                    out.insert(*var);
819                }
820            }
821            _ => {}
822        }
823    });
824}
825
826/// First `throw <local>` var in the catch body that is never assigned
827/// anywhere and is exception-typed — the unmaterialized move-exception.
828fn first_thrown_unassigned_local(
829    body: &Stmt,
830    vt: &VarTable,
831    assigned: &jdc_core::FxHashSet<u32>,
832) -> Option<u32> {
833    let mut found: Option<u32> = None;
834    let c = body.clone();
835    walk_all(&c, &mut |st| {
836        if let Stmt::Throw(th) = st {
837            if let Expr::Local { var, .. } = th {
838                let v = *var;
839                if !assigned.contains(&v) {
840                    if let JavaType::Object(o) = vt.var(v).ty.erased() {
841                        if o.as_ref() == "java/lang/Throwable" {
842                            found = Some(v);
843                        }
844                    }
845                }
846            }
847        }
848    });
849    found
850}
851
852/// Remove the first LEAF statement (mirror of first_leaf_stmt), pruning
853/// leading blocks that became empty — the stored-def bind consumes the
854/// move-exception decl wherever the region walk nested it.
855fn remove_first_leaf(s: &mut Stmt) {
856    if let Stmt::Block(v) = s {
857        if let Some(f) = v.first_mut() {
858            remove_first_leaf(f);
859        }
860        while matches!(v.first(), Some(Stmt::Block(b)) if b.is_empty()) {
861            v.remove(0);
862        }
863    } else {
864        *s = Stmt::Block(vec![]);
865    }
866}
867
868/// Remove bare `LocalDef{var, init: None}` declarations for the given
869/// vars (the catch parameter is their declaration now).
870fn remove_bare_decls(s: &mut Stmt, vars: &[u32]) {
871    let vars: jdc_core::FxHashSet<u32> = vars.iter().copied().collect();
872    walk_mut_deep(s, &mut |st| {
873        if let Stmt::Block(v) = st {
874            v.retain(|x| {
875                !matches!(x, Stmt::LocalDef { var, init: None, .. } if vars.contains(var))
876            });
877        }
878    });
879}
880
881fn first_undefined_local_read(
882    body: &Stmt,
883    defined: &jdc_core::FxHashSet<u32>,
884) -> Option<u32> {
885    let mut found: Option<u32> = None;
886    visit_stmt_exprs_ro(body, &mut |e| {
887        if found.is_none() {
888            visit_exprs(e, &mut |x| {
889                if found.is_none() {
890                    if let Expr::Local { var, .. } = x {
891                        if !defined.contains(var) {
892                            found = Some(*var);
893                        }
894                    }
895                }
896            });
897        }
898    });
899    found
900}
901
902#[allow(clippy::too_many_arguments)]
903fn bind_catches_walk(
904    s: &mut Stmt,
905    vt: &mut VarTable,
906    defined: &jdc_core::FxHashSet<u32>,
907    reads_all: &jdc_core::FxHashMap<u32, usize>,
908    assigned: &jdc_core::FxHashSet<u32>,
909    fallback_bound: &mut Vec<u32>,
910) {
911    match s {
912        Stmt::Try {
913            body,
914            catches,
915            finally,
916        } => {
917            bind_catches_walk(body, vt, defined, reads_all, assigned, fallback_bound);
918            for c in catches.iter_mut() {
919                if c.var == u32::MAX {
920                    // Leaf-piercing head check: the region walk can wrap
921                    // the handler body as Block[Block[…]] (weixin ri5/c
922                    // thx rethrow family — the top-level `v.first()` saw
923                    // a Block, all three bind paths missed, and the emit
924                    // fell back to `catch (Throwable ignored)` beside a
925                    // never-assigned `Throwable th;` read).
926                    let stored = match first_leaf_stmt(c.body.as_ref()) {
927                        // ONLY the bare move-exception decl (the
928                        // lifter emits LocalDef{init:None} for it).
929                        // A first stmt with a REAL init is handler
930                        // computation — consuming it as the catch
931                        // store deleted the def and hijacked its
932                        // reads to the catch param (lark ih6/w's
933                        // `v11 = Thread.currentThread()` →
934                        // `catch (InterruptedException thread2) {
935                        // thread2.interrupt(); }`; ch6/e cls4.
936                        // getName; the exception-ignoring catch is
937                        // the standard interrupt idiom).
938                        Stmt::LocalDef { var, init: None, .. } => Some(*var),
939                        // Phi-copy shape `v = exc` where exc is the
940                        // unmaterialized move-exception register
941                        // (defined nowhere in the method).
942                        Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
943                            match (&**target, &**value) {
944                                (Expr::Local { var, .. }, Expr::Local { var: src, .. })
945                                    if !defined.contains(src) =>
946                                {
947                                    Some(*var)
948                                }
949                                _ => None,
950                            }
951                        }
952                        _ => None,
953                    };
954                    if let Some(v) = stored {
955                        let exc = c
956                            .exc
957                            .first()
958                            .cloned()
959                            .unwrap_or_else(|| "java/lang/Throwable".into());
960                        let slot = vt.var(v).slot;
961                        let name = "e".to_string();
962                        let new_var =
963                            vt.add_catch_var(slot, name, TypeRef::J(JavaType::Object(exc)));
964                        remove_first_leaf(c.body.as_mut());
965                        rewrite_local_refs(c.body.as_mut(), v, new_var);
966                        c.var = new_var;
967                    } else if let Some(v) = first_undefined_local_read(c.body.as_ref(), defined)
968                        .filter(|v| {
969                            // Bind only a var read NOWHERE outside this
970                            // catch: ensure_declared would hoist an
971                            // outside-read var to a method local, and
972                            // consuming it as the catch parameter would
973                            // scope it too narrowly.
974                            let in_catch = count_locals_stmts(std::slice::from_ref(c.body.as_ref()));
975                            reads_all.get(v).copied().unwrap_or(0)
976                                == in_catch.get(v).copied().unwrap_or(0)
977                        })
978                        .filter(|v| {
979                            // And only an EXCEPTION-typed var: the
980                            // move-exception registers carry the handler
981                            // type — a plain local pending its hoisted
982                            // declaration (StringBuilder sb) must not be
983                            // consumed as the catch parameter (reqable
984                            // amazon: the declaration vanished and its
985                            // outer readers broke).
986                            let ty = vt.var(*v).ty.erased();
987                            let exc = c
988                                .exc
989                                .first()
990                                .cloned()
991                                .unwrap_or_else(|| "java/lang/Throwable".into());
992                            matches!(&ty, JavaType::Object(o)
993                                if o.as_ref() == exc.as_ref()
994                                    || o.as_ref() == "java/lang/Throwable")
995                        })
996                    {
997                        // The handler's first statement is not the
998                        // move-exception def (the d8 synchronized pattern
999                        // leads with MonitorExit): the register the lifter
1000                        // minted for move-exception is still READ in the
1001                        // body (`throw th;`) with no defining statement
1002                        // anywhere. Bind IT as the catch parameter —
1003                        // otherwise emit printed `catch (Throwable ignored)`
1004                        // over an undeclared `throw th` (definite-assignment
1005                        // failure, 6.4k weibo sites).
1006                        c.var = v;
1007                        fallback_bound.push(v);
1008                    } else if let Some(v) =
1009                        first_thrown_unassigned_local(c.body.as_ref(), vt, assigned)
1010                            .filter(|v| {
1011                                let in_catch =
1012                                    count_locals_stmts(std::slice::from_ref(c.body.as_ref()));
1013                                reads_all.get(v).copied().unwrap_or(0)
1014                                    == in_catch.get(v).copied().unwrap_or(0)
1015                            })
1016                            .filter(|v| {
1017                                // Same exception-type gate: the hoisted
1018                                // `Throwable th;` declares the var but the
1019                                // move-exception never assigned it.
1020                                let ty = vt.var(*v).ty.erased();
1021                                let exc = c
1022                                    .exc
1023                                    .first()
1024                                    .cloned()
1025                                    .unwrap_or_else(|| "java/lang/Throwable".into());
1026                                matches!(&ty, JavaType::Object(o)
1027                                    if o.as_ref() == exc.as_ref()
1028                                        || o.as_ref() == "java/lang/Throwable")
1029                            })
1030                    {
1031                        // Declared-never-assigned: `Throwable th;` hoisted
1032                        // by an earlier phase, the catch body's `throw th`
1033                        // its only use — the move-exception assignment was
1034                        // never materialized. Bind th as the catch
1035                        // parameter and drop the bare declaration.
1036                        c.var = v;
1037                        fallback_bound.push(v);
1038                    }
1039                }
1040                if c.var != u32::MAX {
1041                    // Second move-exception identity INSIDE the body: R8
1042                    // guards the handler's own close() with a nested
1043                    // try/finally and re-materializes the in-flight
1044                    // exception on its exceptional continuation as a
1045                    // fresh var (`Throwable th7; throw th7;` — weixin
1046                    // y5/b, the 327-site residue of the leaf-piercing
1047                    // fix). The twin is never ASSIGNED anywhere in the
1048                    // method (its register only ever receives
1049                    // move-exception) and the catch param IS its value —
1050                    // rewrite the twin's reads to the param and drop the
1051                    // bare decl. The never-assigned gate makes the
1052                    // rewrite flow-insensitive: every body read of the
1053                    // twin reads the exception. Twins read outside this
1054                    // catch are left alone (the param's scope ends here).
1055                    let in_catch =
1056                        count_locals_stmts(std::slice::from_ref(c.body.as_ref()));
1057                    let mut twins: Vec<u32> = Vec::new();
1058                    collect_bare_decls(c.body.as_ref(), &mut twins);
1059                    let mut swept: Vec<u32> = Vec::new();
1060                    for x in twins {
1061                        if x == c.var || assigned.contains(&x) {
1062                            continue;
1063                        }
1064                        if reads_all.get(&x).copied().unwrap_or(0)
1065                            != in_catch.get(&x).copied().unwrap_or(0)
1066                        {
1067                            continue;
1068                        }
1069                        if !matches!(vt.var(x).ty.erased(),
1070                            JavaType::Object(o) if o.as_ref() == "java/lang/Throwable")
1071                        {
1072                            continue;
1073                        }
1074                        rewrite_local_refs(c.body.as_mut(), x, c.var);
1075                        swept.push(x);
1076                    }
1077                    if !swept.is_empty() {
1078                        remove_bare_decls(c.body.as_mut(), &swept);
1079                    }
1080                }
1081                bind_catches_walk(&mut c.body, vt, defined, reads_all, assigned, fallback_bound);
1082            }
1083            if let Some(f) = finally {
1084                bind_catches_walk(f.as_mut(), vt, defined, reads_all, assigned, fallback_bound);
1085            }
1086        }
1087        Stmt::Block(v) => {
1088            for x in v.iter_mut() {
1089                bind_catches_walk(x, vt, defined, reads_all, assigned, fallback_bound);
1090            }
1091        }
1092        Stmt::If {
1093            then_stmt,
1094            else_stmt,
1095            ..
1096        } => {
1097            bind_catches_walk(then_stmt, vt, defined, reads_all, assigned, fallback_bound);
1098            if let Some(e) = else_stmt {
1099                bind_catches_walk(e, vt, defined, reads_all, assigned, fallback_bound);
1100            }
1101        }
1102        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => bind_catches_walk(body, vt, defined, reads_all, assigned, fallback_bound),
1103        Stmt::For { init, body, .. } => {
1104            for x in init.iter_mut() {
1105                bind_catches_walk(x, vt, defined, reads_all, assigned, fallback_bound);
1106            }
1107            bind_catches_walk(body, vt, defined, reads_all, assigned, fallback_bound);
1108        }
1109        Stmt::ForEach { body, .. }
1110        | Stmt::Labeled { body, .. }
1111        | Stmt::Synchronized { body, .. } => bind_catches_walk(body, vt, defined, reads_all, assigned, fallback_bound),
1112        Stmt::Switch { cases, default, .. } => {
1113            for c in cases.iter_mut() {
1114                for x in c.body.iter_mut() {
1115                    bind_catches_walk(x, vt, defined, reads_all, assigned, fallback_bound);
1116                }
1117            }
1118            if let Some(d) = default {
1119                bind_catches_walk(d, vt, defined, reads_all, assigned, fallback_bound);
1120            }
1121        }
1122        _ => {}
1123    }
1124}
1125
1126fn rewrite_local_refs(s: &mut Stmt, from: u32, to: u32) {
1127    rewrite_exprs(s, &mut |e| {
1128        deep_rewrite(e, &mut |x| {
1129            if let Expr::Local { var, .. } = x {
1130                if *var == from {
1131                    *var = to;
1132                }
1133            }
1134        });
1135    });
1136}
1137
1138/// Flatten blocks, drop empty ones and trailing no-op statements.
1139pub fn cleanup(s: &mut Stmt) {
1140    jdc_core::ir::stmt::flatten(s);
1141    prune_dead_tails(s);
1142    strip_empty(s);
1143    merge_singleton_stmts(s);
1144}
1145
1146/// JLS "cannot complete normally" pruning: drop statements after the
1147/// first hard terminator in any statement sequence — blocks, switch
1148/// case bodies (bare Vecs), for-inits, catch bodies. The 无法访问的语句
1149/// family: `continue; break;` inside switch cases and `return` after a
1150/// try{…return…}catch{…return…} (pdd h0/a, c20/d; ~5.9k sites across
1151/// pdd/cmb/jianying). Conservative: loops and switches never count as
1152/// terminators (no infinite-loop modeling), sequences still carrying
1153/// fallback Label/Goto statements stay untouched, and only
1154/// Return/Throw/Break/Continue plus If/Try/Labeled/Synchronized/Block
1155/// compositions of them terminate.
1156fn seq_terminates(s: &Stmt) -> bool {
1157    match s {
1158        Stmt::Return(_) | Stmt::Throw(_) | Stmt::Break(_) | Stmt::Continue(_) => {
1159            true
1160        }
1161        Stmt::Block(v) => v.iter().any(seq_terminates),
1162        Stmt::If {
1163            then_stmt,
1164            else_stmt,
1165            ..
1166        } => {
1167            seq_terminates(then_stmt)
1168                && else_stmt
1169                    .as_ref()
1170                    .map(|e| seq_terminates(e))
1171                    .unwrap_or(false)
1172        }
1173        Stmt::Try {
1174            body,
1175            catches,
1176            finally,
1177        }
1178        | Stmt::TryWithResources {
1179            body,
1180            catches,
1181            finally,
1182            ..
1183        } => {
1184            if let Some(f) = finally {
1185                if seq_terminates(f) {
1186                    return true;
1187                }
1188            }
1189            seq_terminates(body) && catches.iter().all(|c| seq_terminates(&c.body))
1190        }
1191        Stmt::Labeled { body, .. } | Stmt::Synchronized { body, .. } => {
1192            seq_terminates(body)
1193        }
1194        // A break-less infinite while/do-while never completes normally
1195        // either — jdc-core's label-aware analysis (the emit DEADEND
1196        // truncator uses it): statements after it are 无法访问的语句 the
1197        // same way (the bulk of the pruning residue: jianying 738).
1198        Stmt::While { .. } | Stmt::DoWhile { .. } => {
1199            jdc_core::analysis::dead_end_infinite_while(s)
1200        }
1201        _ => false,
1202    }
1203}
1204
1205fn prune_seq(v: &mut Vec<Stmt>) {
1206    if v.iter()
1207        .any(|x| matches!(x, Stmt::Label(_) | Stmt::Goto(_)))
1208    {
1209        return;
1210    }
1211    if let Some(i) = v.iter().position(seq_terminates) {
1212        v.truncate(i + 1);
1213    }
1214}
1215
1216pub fn prune_dead_tails(s: &mut Stmt) {
1217    match s {
1218        Stmt::Block(v) => {
1219            for x in v.iter_mut() {
1220                prune_dead_tails(x);
1221            }
1222            prune_seq(v);
1223        }
1224        Stmt::If {
1225            then_stmt,
1226            else_stmt,
1227            ..
1228        } => {
1229            prune_dead_tails(then_stmt);
1230            if let Some(e) = else_stmt {
1231                prune_dead_tails(e);
1232            }
1233        }
1234        Stmt::While { body, .. }
1235        | Stmt::DoWhile { body, .. }
1236        | Stmt::ForEach { body, .. }
1237        | Stmt::Labeled { body, .. }
1238        | Stmt::Synchronized { body, .. } => prune_dead_tails(body),
1239        Stmt::For { init, body, .. } => {
1240            for x in init.iter_mut() {
1241                prune_dead_tails(x);
1242            }
1243            prune_seq(init);
1244            prune_dead_tails(body);
1245        }
1246        Stmt::Switch {
1247            cases, default, ..
1248        } => {
1249            for c in cases {
1250                for x in c.body.iter_mut() {
1251                    prune_dead_tails(x);
1252                }
1253                prune_seq(&mut c.body);
1254            }
1255            if let Some(d) = default {
1256                prune_dead_tails(d);
1257            }
1258        }
1259        Stmt::Try {
1260            body,
1261            catches,
1262            finally,
1263        }
1264        | Stmt::TryWithResources {
1265            body,
1266            catches,
1267            finally,
1268            ..
1269        } => {
1270            prune_dead_tails(body);
1271            for c in catches {
1272                prune_dead_tails(&mut c.body);
1273            }
1274            if let Some(f) = finally {
1275                prune_dead_tails(f);
1276            }
1277        }
1278        _ => {}
1279    }
1280}
1281
1282/// Drop assignments whose target field is a PHANTOM: the declaring class
1283/// is pool-materialized but does not declare the field (R8 inlines R-class
1284/// constants everywhere, strips the field declarations, and leaves the
1285/// `<clinit>` husk still sput-ing to them — QQ's R$anim/kj3.a husks:
1286/// class_data with 0 fields beside a clinit of phantom sget→sput pairs,
1287/// 7,774 找不到符号 in four R.java files alone). The dex statements are
1288/// dead by construction — executing them is a guaranteed NoSuchFieldError,
1289/// and R8 only keeps them because nothing triggers the husk's clinit — so
1290/// dropping them preserves every observable behavior while the faithful
1291/// render (`x = kj3.a.x;` against declared-nowhere names) cannot compile.
1292/// Conservative gates: only plain ExprStmt assignments whose TARGET is a
1293/// phantom static/instance field of a materialized class, and whose VALUE
1294/// is side-effect-free (const / local / phantom-field read / cast chain) —
1295/// a real-field read could trigger another class's clinit, and a call
1296/// could carry side effects, so those statements stay.
1297pub fn strip_phantom_field_writes(body: &mut Stmt, pool: &DexPool) {
1298    fn phantom(pool: &DexPool, cls: &str, name: &str) -> bool {
1299        // pool.get (on-demand materialize), not get_if_materialized:
1300        // lazy getclass runs must reach the same verdicts as full runs
1301        // (the RHS holder kj3/a is not pre-materialized there).
1302        let Some(pc) = pool.get(cls) else {
1303            return false;
1304        };
1305        // The IR Field ref carries the DISPLAY name (the lifter emits
1306        // through field_display) while the pool tables carry RAW names —
1307        // match both, or an obscured-renamed field (`h` → `h17` beside
1308        // class h) reads as phantom and its const-build assignment gets
1309        // dropped, breaking enum promotion (weixin z54/h fell back to
1310        // `/* enum */ class`, taking every `h[] → Enum[]` conversion
1311        // down with it — the +193 weixin battery regression).
1312        fn declares(pc: &crate::PoolClass, cls: &str, name: &str) -> bool {
1313            pc.static_fields
1314                .iter()
1315                .chain(pc.instance_fields.iter())
1316                .any(|f| {
1317                    f.name.as_ref() == name
1318                        || jdc_core::rename::field_display(cls, &f.name, &f.desc)
1319                            .is_some_and(|d| d == name)
1320                })
1321        }
1322        if declares(pc, cls, name) {
1323            return false;
1324        }
1325        // Field resolution walks the superclass chain, so "phantom" must
1326        // be PROVEN against the whole chain — a write to an inherited
1327        // field is real code (QQ com.tencent.gdtad.statistics.d inherits
1328        // `b` from super `e`; an own-fields-only verdict dropped the
1329        // legitimate `v10x.b = gdtAd2;` — silent data loss, latent until
1330        // the walk started descending into If.then_stmt). Off-pool
1331        // ancestors are enumerated through fwdb (framework fields like
1332        // View.mID are real); an ancestor NEITHER pool nor fwdb knows
1333        // (R8-stripped class) ends the proof — keep the write.
1334        let mut cur: Option<String> = pc.super_name.clone();
1335        let mut hops = 0u32;
1336        while let Some(s) = cur {
1337            hops += 1;
1338            if hops > 64 {
1339                return false;
1340            }
1341            if let Some(sup) = pool.get(&s) {
1342                if declares(sup, &s, name) {
1343                    return false;
1344                }
1345                cur = sup.super_name.clone();
1346            } else {
1347                if !crate::fwdb::exists(&s) {
1348                    return false;
1349                }
1350                let mut found = false;
1351                crate::fwdb::for_each_field(&s, |f, _| {
1352                    if f == name {
1353                        found = true;
1354                    }
1355                });
1356                if found {
1357                    return false;
1358                }
1359                // fwdb strips the Object super edge: the chain ends here.
1360                cur = crate::fwdb::super_of(&s).map(|x| x.to_string());
1361            }
1362        }
1363        true
1364    }
1365    fn droppable_value(pool: &DexPool, e: &Expr) -> bool {
1366        match e {
1367            Expr::Const(_) | Expr::Local { .. } => true,
1368            Expr::Field { cls, name, .. } => phantom(pool, cls, name),
1369            Expr::Cast { e, .. } => droppable_value(pool, e),
1370            _ => false,
1371        }
1372    }
1373    fn is_phantom_write(pool: &DexPool, st: &Stmt) -> bool {
1374        if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = st {
1375            if let Expr::Field { cls, name, .. } = &**target {
1376                return phantom(pool, cls, name) && droppable_value(pool, value);
1377            }
1378        }
1379        false
1380    }
1381    // walk_mut_deep over every Vec-holding node — the bare-Vec positions
1382    // (switch case bodies, For init, TWR resources) are the known blind
1383    // spot of Block-only recursion.
1384    walk_mut_deep(body, &mut |st| match st {
1385        Stmt::Block(v) => v.retain(|x| !is_phantom_write(pool, x)),
1386        Stmt::Switch { cases, .. } => {
1387            for c in cases.iter_mut() {
1388                c.body.retain(|x| !is_phantom_write(pool, x));
1389            }
1390        }
1391        Stmt::For { init, .. } => init.retain(|x| !is_phantom_write(pool, x)),
1392        Stmt::TryWithResources { resources, .. } => {
1393            resources.retain(|x| !is_phantom_write(pool, x))
1394        }
1395        _ => {}
1396    });
1397    if is_phantom_write(pool, body) {
1398        *body = Stmt::Block(vec![]);
1399    }
1400}
1401
1402/// Late default-init for surviving bare declarations (`T v;`). Runs
1403/// AFTER every fold/ctor/hotfix matcher that keys on `init: None`
1404/// (bind_catches, strip_clinit_hotfix_guard, the folding prefix scans),
1405/// so nothing observes the initialized form until emission. Dex
1406/// registers are zero-initialized at method entry, so `= null/0/false`
1407/// is the FAITHFUL value for any read that reaches it before the first
1408/// write — and javac's definite assignment rejects the bare form on
1409/// partial paths (可能尚未初始化变量: pdd gi1/a obj12 assigned in two
1410/// branches and read after the merge; also turns the residual
1411/// move-exception twins into compilable — and register-faithful —
1412/// null reads instead of undefined ones).
1413pub fn init_bare_decls(body: &mut Stmt, vt: &VarTable) {
1414    walk_mut_deep(body, &mut |st| {
1415        if let Stmt::LocalDef { var, init, .. } = st {
1416            if init.is_some() {
1417                return;
1418            }
1419            let lit = match vt.var(*var).ty.erased() {
1420                JavaType::Boolean => "false",
1421                JavaType::Int
1422                | JavaType::Byte
1423                | JavaType::Short
1424                | JavaType::Char => "0",
1425                JavaType::Long => "0L",
1426                JavaType::Float => "0.0f",
1427                JavaType::Double => "0.0d",
1428                JavaType::Void => return,
1429                JavaType::Object(_) | JavaType::Array(_) => "null",
1430            };
1431            *init = Some(Expr::Raw(lit.to_string()));
1432        }
1433    });
1434}
1435
1436fn strip_empty(s: &mut Stmt) {
1437    match s {
1438        Stmt::Block(v) => {
1439            v.retain(|x| !x.is_empty_block());
1440            for x in v.iter_mut() {
1441                strip_empty(x);
1442            }
1443        }
1444        Stmt::If {
1445            then_stmt,
1446            else_stmt,
1447            ..
1448        } => {
1449            strip_empty(then_stmt);
1450            if let Some(e) = else_stmt {
1451                strip_empty(e);
1452                if e.is_empty_block() {
1453                    *else_stmt = None;
1454                }
1455            }
1456        }
1457        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => strip_empty(body),
1458        Stmt::For { init, body, .. } => {
1459            init.retain(|x| !x.is_empty_block());
1460            strip_empty(body);
1461        }
1462        Stmt::ForEach { body, .. } => strip_empty(body),
1463        Stmt::Switch { cases, default, .. } => {
1464            for c in cases {
1465                c.body.retain(|x| !x.is_empty_block());
1466            }
1467            if let Some(d) = default {
1468                strip_empty(d);
1469            }
1470        }
1471        Stmt::Try {
1472            body,
1473            catches,
1474            finally,
1475        } => {
1476            strip_empty(body);
1477            for c in catches {
1478                strip_empty(&mut c.body);
1479            }
1480            if let Some(f) = finally {
1481                strip_empty(f);
1482            }
1483        }
1484        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => strip_empty(body),
1485        _ => {}
1486    }
1487}
1488
1489fn merge_singleton_stmts(s: &mut Stmt) {
1490    if let Stmt::Block(v) = s {
1491        for x in v.iter_mut() {
1492            merge_singleton_stmts(x);
1493        }
1494    }
1495}
1496
1497/// Drop statements after a definite terminator within a block.
1498pub fn prune_unreachable(s: &mut Stmt) {
1499    if let Stmt::Block(v) = s {
1500        let mut cut: Option<usize> = None;
1501        for (i, x) in v.iter().enumerate() {
1502            if matches!(x, Stmt::Return(_) | Stmt::Throw(_)) {
1503                cut = Some(i + 1);
1504                break;
1505            }
1506        }
1507        if let Some(c) = cut {
1508            if c < v.len() {
1509                v.truncate(c);
1510            }
1511        }
1512        for x in v.iter_mut() {
1513            prune_unreachable(x);
1514        }
1515    } else {
1516        walk_mut(s, &mut prune_unreachable);
1517    }
1518}
1519
1520fn walk_mut<F: FnMut(&mut Stmt)>(s: &mut Stmt, f: &mut F) {
1521    match s {
1522        Stmt::Block(v) => {
1523            for x in v.iter_mut() {
1524                f(x);
1525            }
1526        }
1527        Stmt::If {
1528            then_stmt,
1529            else_stmt,
1530            ..
1531        } => {
1532            f(then_stmt);
1533            if let Some(e) = else_stmt {
1534                f(e);
1535            }
1536        }
1537        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => f(body),
1538        Stmt::For { init, body, .. } => {
1539            for x in init.iter_mut() {
1540                f(x);
1541            }
1542            f(body);
1543        }
1544        Stmt::ForEach { body, .. } => f(body),
1545        Stmt::Switch { cases, default, .. } => {
1546            for c in cases {
1547                for x in c.body.iter_mut() {
1548                    f(x);
1549                }
1550            }
1551            if let Some(d) = default {
1552                f(d);
1553            }
1554        }
1555        Stmt::Try {
1556            body,
1557            catches,
1558            finally,
1559        } => {
1560            f(body);
1561            for c in catches {
1562                f(&mut c.body);
1563            }
1564            if let Some(fl) = finally {
1565                f(fl);
1566            }
1567        }
1568        Stmt::TryWithResources {
1569            resources,
1570            body,
1571            catches,
1572            finally,
1573        } => {
1574            for r in resources.iter_mut() {
1575                f(r);
1576            }
1577            f(body);
1578            for c in catches {
1579                f(&mut c.body);
1580            }
1581            if let Some(fl) = finally {
1582                f(fl);
1583            }
1584        }
1585        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => f(body),
1586        _ => {}
1587    }
1588}
1589
1590/// Count LocalDef statements (pipeline diagnostics).
1591pub fn count_localdefs(s: &Stmt, out: &mut usize) {
1592    if matches!(s, Stmt::LocalDef { .. }) {
1593        *out += 1;
1594    }
1595    walk_all(s, &mut |x| {
1596        if matches!(x, Stmt::LocalDef { .. }) {
1597            *out += 1;
1598        }
1599    });
1600}
1601
1602/// 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/// Last-chance dead-assignment sweep: `v = <pure value>` where `v` is
2535/// never read ANYWHERE is semantically dead regardless of which late
2536/// pass minted it (phi remnant, ctor diamond fold, scope-leaked temp —
2537/// the regular drop_dead_locals runs mid-pipeline and misses statements
2538/// minted after it). The pure gate keeps every observable side effect.
2539pub fn final_dead_assigns(body: &mut Stmt) {
2540    // Reads per var (assignment targets excluded) over the whole tree.
2541    let mut reads: Vec<usize> = Vec::new();
2542    count_reads(body, &mut reads);
2543    let is_dead = |st: &Stmt| -> bool {
2544        let Stmt::ExprStmt(Expr::Assign {
2545            target,
2546            value,
2547            op: jdc_core::ir::expr::AssignOp::Plain,
2548            ..
2549        }) = st
2550        else {
2551            return false;
2552        };
2553        let Expr::Local { var, .. } = &**target else {
2554            return false;
2555        };
2556        reads.get(*var as usize).copied().unwrap_or(0) == 0 && !has_side_effects(value)
2557    };
2558    // Replace IN PLACE: the assignment can sit as a single-statement If
2559    // arm or a bare case body where no container retain ever reaches it
2560    // (the bare-Vec/Block blind spot, switch case body / For init).
2561    walk_mut_deep(body, &mut |st| {
2562        if is_dead(st) {
2563            *st = Stmt::Block(vec![]);
2564        }
2565    });
2566    walk_mut_deep(body, &mut |st| {
2567        if let Stmt::Block(v) = st {
2568            v.retain(|x| !is_dead(x));
2569        }
2570    });
2571}
2572
2573/// Drop orphan value pushes that render as `/* X; */` comments: expression
2574/// statements whose expression cannot throw or bind — plain locals,
2575/// constants, `this`, and field reads on the implicit `this` (owner: None;
2576/// a deref of an arbitrary receiver could NPE, so owned field reads stay).
2577pub fn drop_pure_value_stmts(body: &mut Stmt) {
2578    // Shared honest predicate (jdc-core has_side_effects): a pure read
2579    // cannot throw or bind, so the statement is dead code everywhere.
2580    fn is_pure(s: &Stmt) -> bool {
2581        let Stmt::ExprStmt(e) = s else {
2582            return false;
2583        };
2584        !has_side_effects(e)
2585    }
2586    walk_mut_deep(body, &mut |st| {
2587        // Replace in place: orphans also sit as direct If branches or a
2588        // Synchronized body where no container retain can reach them.
2589        if is_pure(st) {
2590            *st = Stmt::Block(vec![]);
2591            return;
2592        }
2593        match st {
2594            Stmt::Switch { cases, .. } => {
2595                for c in cases.iter_mut() {
2596                    c.body.retain(|x| !is_pure(x));
2597                }
2598            }
2599            Stmt::For { init, .. } => init.retain(|x| !is_pure(x)),
2600            _ => {}
2601        }
2602    });
2603}
2604
2605pub fn drop_dead_raw_news(body: &mut Stmt) {
2606    fn is_orphan(s: &Stmt) -> bool {
2607        matches!(s, Stmt::ExprStmt(Expr::New { raw: true, .. }))
2608    }
2609    walk_mut_deep(body, &mut |st| match st {
2610        Stmt::Block(v) => v.retain(|x| !is_orphan(x)),
2611        Stmt::Switch { cases, .. } => {
2612            for c in cases.iter_mut() {
2613                c.body.retain(|x| !is_orphan(x));
2614            }
2615        }
2616        Stmt::For { init, .. } => init.retain(|x| !is_orphan(x)),
2617        Stmt::TryWithResources { resources, .. } => resources.retain(|x| !is_orphan(x)),
2618        _ => {}
2619    });
2620}
2621
2622/// Recover `synchronized` from the d8 monitor pattern:
2623/// `monitorenter(e); try { body } catch (Throwable) { monitorexit(e); throw t; }`
2624/// (optionally followed by `monitorexit(e)`).
2625pub fn fold_synchronized(s: &mut Stmt) {
2626    fold_sync_walk(s);
2627}
2628
2629
2630fn fold_sync_walk(s: &mut Stmt) {
2631    walk_mut_deep(s, &mut |st| {
2632        let Stmt::Block(v) = st else { return };
2633        let mut i = 0;
2634        while i < v.len() {
2635            // A fold landing on a Synchronized re-examines the same slot:
2636            // a split-region sibling Try (Case E) only becomes visible
2637            // AFTER the [enter, try] pair folded into the block form.
2638            #[allow(unused_assignments)]
2639            let mut advance = true;
2640            if i + 1 < v.len() {
2641                if let Some((repl, consumed)) = try_sync_at(v, i) {
2642                    advance = !matches!(repl.first(), Some(Stmt::Synchronized { .. }));
2643                    let _ = v.drain(i..i + consumed);
2644                    let n = repl.len();
2645                    v.splice(i..i, repl);
2646                    if advance {
2647                        i += n;
2648                    }
2649                    continue;
2650                }
2651            }
2652            if let Some((repl, consumed)) = try_sync_in_try(v, i) {
2653                let _ = v.drain(i..i + consumed);
2654                let n = repl.len();
2655                v.splice(i..i, repl);
2656                i += n;
2657                continue;
2658            }
2659            if let Some((repl, consumed)) = try_sync_deep(v, i) {
2660                let _ = v.drain(i..i + consumed);
2661                let n = repl.len();
2662                v.splice(i..i, repl);
2663                i += n;
2664                continue;
2665            }
2666            if let Some((repl, consumed)) = try_sync_absorb_after(v, i) {
2667                let _ = v.drain(i..i + consumed);
2668                let n = repl.len();
2669                v.splice(i..i, repl);
2670                i += n;
2671                continue;
2672            }
2673            i += 1;
2674        }
2675    });
2676}
2677
2678/// Side-effect-free lock-value reads: field/local/const/this only. A call
2679/// or allocation between the monitor-enter and the try belongs to the
2680/// protected body, not to a absorbable alias head.
2681fn is_pure_read_expr(e: &Expr) -> bool {
2682    let mut pure = true;
2683    visit_exprs(e, &mut |x| match x {
2684        Expr::Const(_)
2685        | Expr::Local { .. }
2686        | Expr::This
2687        | Expr::Field { .. } => {}
2688        _ => pure = false,
2689    });
2690    pure
2691}
2692
2693/// Resolve a local through its defining expression (bounded) so lock
2694/// identity survives value-view forwarding: the enter may render as the
2695/// raw field access while the exit renders as the promoted local (or vice
2696/// versa).
2697fn resolve_lock(e: &Expr, defs: &jdc_core::FxHashMap<u32, Expr>, depth: u32) -> Expr {
2698    if depth >= 4 {
2699        return e.clone();
2700    }
2701    if let Expr::Local { var, .. } = e {
2702        if let Some(d) = defs.get(var) {
2703            return resolve_lock(d, defs, depth + 1);
2704        }
2705    }
2706    e.clone()
2707}
2708
2709fn lock_same(a: &Expr, b: &Expr) -> bool {
2710    match (a, b) {
2711        (Expr::This, Expr::This) => true,
2712        (Expr::Local { var: x, .. }, Expr::Local { var: y, .. }) => x == y,
2713        (
2714            Expr::Field {
2715                owner: oa,
2716                cls: ca,
2717                name: na,
2718                ..
2719            },
2720            Expr::Field {
2721                owner: ob,
2722                cls: cb,
2723                name: nb,
2724                ..
2725            },
2726        ) => {
2727            ca == cb
2728                && na == nb
2729                && match (oa, ob) {
2730                    (None, None) => true,
2731                    (Some(ra), Some(rb)) => lock_same(ra, rb),
2732                    _ => false,
2733                }
2734        }
2735        (Expr::Const(x), Expr::Const(y)) => x == y,
2736        _ => false,
2737    }
2738}
2739
2740fn lock_equiv(a: &Expr, b: &Expr, defs: &jdc_core::FxHashMap<u32, Expr>) -> bool {
2741    lock_same(&resolve_lock(a, defs, 0), &resolve_lock(b, defs, 0))
2742}
2743
2744fn flatten_leaves<'a>(s: &'a Stmt, out: &mut Vec<&'a Stmt>) {
2745    match s {
2746        Stmt::Block(v) => {
2747            for x in v {
2748                flatten_leaves(x, out);
2749            }
2750        }
2751        _ => out.push(s),
2752    }
2753}
2754
2755/// Every `MonitorExit(lock)` inside the folded body is a compiler artifact
2756/// (the implicit release on each exit path) — drop them so the body reads
2757/// clean and the absorbed lock alias loses its last reads.
2758fn strip_lock_exits(s: &mut Stmt, lock: &Expr, defs: &jdc_core::FxHashMap<u32, Expr>) {
2759    walk_mut_deep(s, &mut |st| {
2760        let Stmt::Block(v) = st else { return };
2761        v.retain(|x| !matches!(x, Stmt::MonitorExit(e) if lock_equiv(e, lock, defs)));
2762    });
2763}
2764
2765fn try_sync_at(v: &[Stmt], i: usize) -> Option<(Vec<Stmt>, usize)> {
2766    // Local defs in scope before the enter, for lock resolution.
2767    let mut defs: jdc_core::FxHashMap<u32, Expr> = jdc_core::FxHashMap::default();
2768    for s in &v[..i] {
2769        if let Stmt::LocalDef { var, init: Some(e), .. } = s {
2770            defs.insert(*var, e.clone());
2771        }
2772    }
2773    // Case A: the enter is a direct sibling; Case B: it ends a child Block
2774    // (the converter wraps the pre-try statements of the entry block, so
2775    // the Try sits beside the wrapper, not beside the enter).
2776    let (pre, enter_e, mut j, mut absorbed): (Vec<Stmt>, &Expr, usize, Vec<Stmt>) = match &v[i] {
2777        Stmt::MonitorEnter(e) => (Vec::new(), e, i + 1, Vec::new()),
2778        Stmt::Block(b) => {
2779            let split = b
2780                .iter()
2781                .position(|x| matches!(x, Stmt::MonitorEnter(_)))?;
2782            let e = match &b[split] {
2783                Stmt::MonitorEnter(e) => e,
2784                _ => unreachable!(),
2785            };
2786            let mut absorbed: Vec<Stmt> = Vec::new();
2787            let mut k = split + 1;
2788            while let Some(Stmt::LocalDef { var, init: Some(ex), .. }) = b.get(k) {
2789                if !is_pure_read_expr(ex) {
2790                    break;
2791                }
2792                defs.insert(*var, ex.clone());
2793                absorbed.push(b[k].clone());
2794                k += 1;
2795            }
2796            // Only a tail split: anything after the absorbable run (other
2797            // than nothing) means real code follows inside the block.
2798            if k != b.len() {
2799                return None;
2800            }
2801            let pre: Vec<Stmt> = b[..split].to_vec();
2802            for s in &pre {
2803                if let Stmt::LocalDef { var, init: Some(e2), .. } = s {
2804                    defs.insert(*var, e2.clone());
2805                }
2806            }
2807            (pre, e, i + 1, absorbed)
2808        }
2809        _ => return None,
2810    };
2811    // Absorb pure alias defs parked between the enter and the try (the
2812    // value-view promotion hoists the lock register's declaration ahead of
2813    // the try span). They become the synchronized body's head.
2814    let mut absorbed_vars: Vec<u32> = Vec::new();
2815    while let Some(Stmt::LocalDef { var, init: Some(e), .. }) = v.get(j) {
2816        if !is_pure_read_expr(e) {
2817            break;
2818        }
2819        defs.insert(*var, e.clone());
2820        absorbed_vars.push(*var);
2821        absorbed.push(v[j].clone());
2822        j += 1;
2823    }
2824    let Stmt::Try {
2825        body,
2826        catches,
2827        finally,
2828    } = v.get(j)?
2829    else {
2830        return None;
2831    };
2832    if finally.is_some() || catches.len() != 1 {
2833        return None;
2834    }
2835    let c = &catches[0];
2836    if !c.exc.is_empty() {
2837        return None;
2838    }
2839    // The catch-all body: monitorexit(lock) then rethrow (the handler walk
2840    // may nest it as Block[Block[...]]).
2841    let mut flat: Vec<&Stmt> = Vec::new();
2842    flatten_leaves(c.body.as_ref(), &mut flat);
2843    if flat.len() < 2 {
2844        return None;
2845    }
2846    let Stmt::MonitorExit(exit_e) = flat[0] else {
2847        return None;
2848    };
2849    if !lock_equiv(enter_e, exit_e, &defs) {
2850        return None;
2851    }
2852    if !matches!(flat[flat.len() - 1], Stmt::Throw(_)) {
2853        return None;
2854    }
2855    for mid in &flat[1..flat.len() - 1] {
2856        if !matches!(mid, Stmt::MonitorEnter(_) | Stmt::MonitorExit(_)) {
2857            return None;
2858        }
2859    }
2860    // An absorbed alias read anywhere after the try would escape the
2861    // synchronized scope once folded — refuse the fold instead.
2862    if !absorbed_vars.is_empty() {
2863        let outside = count_locals_stmts(&v[j + 1..]);
2864        if absorbed_vars
2865            .iter()
2866            .any(|av| outside.get(av).copied().unwrap_or(0) > 0)
2867        {
2868            return None;
2869        }
2870    }
2871    let mut body_stmt = (**body).clone();
2872    strip_lock_exits(&mut body_stmt, enter_e, &defs);
2873    absorbed.push(body_stmt);
2874    let mut consumed = j - i + 1;
2875    // A matching trailing monitor-exit after the try (the normal-path
2876    // release parked at the join) is implicit in the block form too.
2877    if let Some(Stmt::MonitorExit(e)) = v.get(j + 1) {
2878        if lock_equiv(e, enter_e, &defs) {
2879            consumed += 1;
2880        }
2881    }
2882    let mut repl = pre;
2883    repl.push(Stmt::Synchronized {
2884        lock: enter_e.clone(),
2885        body: Box::new(Stmt::Block(absorbed)),
2886    });
2887    Some((repl, consumed))
2888}
2889
2890/// Case C: the monitor-enter sits INSIDE the try body (d8 extends the
2891/// protected range over the lock evaluation: `try { v=lock; monitorenter v;
2892/// rest } catch-all { monitorexit v; throw }`). Everything up to and
2893/// including the enter folds into the synchronized body — the block form
2894/// re-protects it.
2895fn try_sync_in_try(v: &[Stmt], i: usize) -> Option<(Vec<Stmt>, usize)> {
2896    let Stmt::Try {
2897        body,
2898        catches,
2899        finally,
2900    } = &v[i]
2901    else {
2902        return None;
2903    };
2904    if finally.is_some() || catches.len() != 1 {
2905        return None;
2906    }
2907    let c = &catches[0];
2908    if !c.exc.is_empty() {
2909        return None;
2910    }
2911    let mut flat: Vec<&Stmt> = Vec::new();
2912    flatten_leaves(c.body.as_ref(), &mut flat);
2913    if flat.len() < 2 {
2914        return None;
2915    }
2916    let Stmt::MonitorExit(_) = flat[0] else {
2917        return None;
2918    };
2919    if !matches!(flat[flat.len() - 1], Stmt::Throw(_)) {
2920        return None;
2921    }
2922    for mid in &flat[1..flat.len() - 1] {
2923        if !matches!(mid, Stmt::MonitorEnter(_) | Stmt::MonitorExit(_)) {
2924            return None;
2925        }
2926    }
2927    // Locate the enter inside the body's top level: a direct MonitorEnter
2928    // or a child Block whose tail is [MonitorEnter, pure-defs*]. The
2929    // wrapped prelude statements stay ahead of the fold point.
2930    let Stmt::Block(bv) = body.as_ref() else {
2931        return None;
2932    };
2933    let mut defs: jdc_core::FxHashMap<u32, Expr> = jdc_core::FxHashMap::default();
2934    for s in &v[..i] {
2935        if let Stmt::LocalDef { var, init: Some(e), .. } = s {
2936            defs.insert(*var, e.clone());
2937        }
2938    }
2939    let mut pre: Vec<Stmt> = Vec::new();
2940    let mut enter_e: Option<Expr> = None;
2941    let mut absorbed: Vec<Stmt> = Vec::new();
2942    let mut absorbed_vars: Vec<u32> = Vec::new();
2943    let mut rest_start: Option<usize> = None;
2944    for (p, el) in bv.iter().enumerate() {
2945        match el {
2946            Stmt::MonitorEnter(e) => {
2947                enter_e = Some(e.clone());
2948                rest_start = Some(p + 1);
2949                break;
2950            }
2951            Stmt::Block(inner) => {
2952                let split = inner
2953                    .iter()
2954                    .position(|x| matches!(x, Stmt::MonitorEnter(_)));
2955                if let Some(sp) = split {
2956                    let mut k = sp + 1;
2957                    let mut ok = true;
2958                    let mut tail: Vec<Stmt> = Vec::new();
2959                    while let Some(Stmt::LocalDef { var: _, init: Some(ex), .. }) = inner.get(k) {
2960                        if !is_pure_read_expr(ex) {
2961                            ok = false;
2962                            break;
2963                        }
2964                        tail.push(inner[k].clone());
2965                        k += 1;
2966                    }
2967                    if ok && k == inner.len() {
2968                        for s in &inner[..sp] {
2969                            if let Stmt::LocalDef { var, init: Some(ex), .. } = s {
2970                                defs.insert(*var, ex.clone());
2971                            }
2972                        }
2973                        pre.extend(inner[..sp].iter().cloned());
2974                        enter_e = Some(match &inner[sp] {
2975                            Stmt::MonitorEnter(e) => e.clone(),
2976                            _ => unreachable!(),
2977                        });
2978                        absorbed = tail;
2979                        rest_start = Some(p + 1);
2980                        break;
2981                    }
2982                }
2983                // Not a fold point: a plain nested block ahead of the enter.
2984                for s in inner.iter() {
2985                    if let Stmt::LocalDef { var, init: Some(ex), .. } = s {
2986                        defs.insert(*var, ex.clone());
2987                    }
2988                }
2989                pre.push(el.clone());
2990            }
2991            other => {
2992                if let Stmt::LocalDef { var, init: Some(ex), .. } = other {
2993                    defs.insert(*var, ex.clone());
2994                }
2995                pre.push(other.clone());
2996            }
2997        }
2998    }
2999    let enter_e = enter_e?;
3000    let rs = rest_start?;
3001    // Pure defs directly after the enter also join the body head.
3002    let mut q = rs;
3003    while let Some(Stmt::LocalDef { var, init: Some(ex), .. }) = bv.get(q) {
3004        if !is_pure_read_expr(ex) {
3005            break;
3006        }
3007        defs.insert(*var, ex.clone());
3008        absorbed_vars.push(*var);
3009        absorbed.push(bv[q].clone());
3010        q += 1;
3011    }
3012    let rest: Vec<Stmt> = bv[q..].to_vec();
3013    if rest.is_empty() {
3014        return None;
3015    }
3016    // A non-def prelude would move real code past the lock acquisition —
3017    // the deep fold (try kept, synchronized in place) owns that shape.
3018    if pre
3019        .iter()
3020        .any(|s| !matches!(s, Stmt::LocalDef { init: Some(e), .. } if is_pure_read_expr(e)))
3021    {
3022        return None;
3023    }
3024    // The catch's release must name the same lock.
3025    let Stmt::MonitorExit(exit_e) = flat[0] else {
3026        return None;
3027    };
3028    if !lock_equiv(&enter_e, exit_e, &defs) {
3029        return None;
3030    }
3031    // Pre/absorbed defs must not be read outside the try.
3032    let outside_vars: Vec<u32> = pre
3033        .iter()
3034        .chain(absorbed.iter())
3035        .filter_map(|s| match s {
3036            Stmt::LocalDef { var, .. } => Some(*var),
3037            _ => None,
3038        })
3039        .collect();
3040    if !outside_vars.is_empty() {
3041        let outside = count_locals_stmts(&v[i + 1..]);
3042        if outside_vars
3043            .iter()
3044            .any(|av| outside.get(av).copied().unwrap_or(0) > 0)
3045        {
3046            return None;
3047        }
3048    }
3049    let mut body_stmts = pre;
3050    body_stmts.extend(absorbed);
3051    body_stmts.extend(rest);
3052    let mut sync_body = Stmt::Block(body_stmts);
3053    strip_lock_exits(&mut sync_body, &enter_e, &defs);
3054    let mut consumed = 1;
3055    if let Some(Stmt::MonitorExit(e)) = v.get(i + 1) {
3056        if lock_equiv(e, &enter_e, &defs) {
3057            consumed += 1;
3058        }
3059    }
3060    Some((
3061        vec![Stmt::Synchronized {
3062            lock: enter_e,
3063            body: Box::new(sync_body),
3064        }],
3065        consumed,
3066    ))
3067}
3068
3069/// Case E: the protected region was SPLIT across groups — a named try/catch
3070/// nested inside the synchronized region renders as a SIBLING Try after the
3071/// folded Synchronized, with the region's normal-path release parked after
3072/// it (d8 splits javac-style around inner returns; the inner range carries
3073/// its own named handler plus the shared catch-all, so the same-handler
3074/// group merge refuses it and the structurer emits two sibling tries).
3075/// `[Synchronized, Try, MonitorExit(lock)]` -> the Try joins the lock body
3076/// and the parked release is implicit. Without this the try escapes the
3077/// lock (racy field writes) and unexpected exceptions leak the monitor.
3078fn try_sync_absorb_after(v: &[Stmt], i: usize) -> Option<(Vec<Stmt>, usize)> {
3079    let Stmt::Synchronized { lock, body } = &v[i] else {
3080        return None;
3081    };
3082    let Stmt::Try {
3083        body: tbody,
3084        catches,
3085        finally,
3086    } = v.get(i + 1)?
3087    else {
3088        return None;
3089    };
3090    if finally.is_some() {
3091        return None;
3092    }
3093    // The parked release sits at the protected region's end: everything on
3094    // the normal path before it ran INSIDE the lock (phi commits feeding
3095    // the in-lock return), everything after it is post-release. Scan past
3096    // orderable statements (register commits, pure reads, Block wrappers)
3097    // to the matching release; absorb the run into the lock body.
3098    let defs = defs_probe(v, i, body);
3099    let mut absorbed_mid: Vec<Stmt> = Vec::new();
3100    let mut rest_after: Option<Stmt> = None;
3101    let mut found = false;
3102    let mut scan = i + 2;
3103    'scan: while scan < v.len() && scan < i + 14 {
3104        match &v[scan] {
3105            Stmt::MonitorExit(e) => {
3106                if !lock_equiv(e, lock, &defs) {
3107                    return None;
3108                }
3109                found = true;
3110                scan += 1;
3111                break;
3112            }
3113            Stmt::Block(bv) => {
3114                for (k, s) in bv.iter().enumerate() {
3115                    match s {
3116                        Stmt::MonitorExit(e) => {
3117                            if !lock_equiv(e, lock, &defs) {
3118                                return None;
3119                            }
3120                            absorbed_mid.push(Stmt::Block(bv[..k].to_vec()));
3121                            let rest = &bv[k + 1..];
3122                            rest_after = (!rest.is_empty())
3123                                .then(|| Stmt::Block(rest.to_vec()));
3124                            found = true;
3125                            scan += 1;
3126                            break 'scan;
3127                        }
3128                        Stmt::ExprStmt(e) if mid_safe(e) || is_local_commit(e) => {}
3129                        _ => break 'scan,
3130                    }
3131                }
3132                absorbed_mid.push(v[scan].clone());
3133                scan += 1;
3134            }
3135            Stmt::ExprStmt(e) if mid_safe(e) || is_local_commit(e) => {
3136                absorbed_mid.push(v[scan].clone());
3137                scan += 1;
3138            }
3139            _ => break,
3140        }
3141    }
3142    if !found {
3143        return None;
3144    }
3145    let consumed = scan - i;
3146    // The adopted try's implicit releases and the handler's own release are
3147    // owned by the synchronized once inside — strip them.
3148    let mut t = (**tbody).clone();
3149    let defs = defs_probe(v, i, body);
3150    strip_lock_exits(&mut t, lock, &defs);
3151    let mut cs = Vec::with_capacity(catches.len());
3152    for c in catches.iter() {
3153        let mut cb = (*c.body).clone();
3154        strip_lock_exits(&mut cb, lock, &defs);
3155        // A catch-all reduced to a bare rethrow of the catch parameter is
3156        // the synchronized's own handler — drop it; named catches keep
3157        // (dropping one would widen the checked-exception surface).
3158        let bare_rethrow = c.exc.is_empty() && {
3159            let mut leaves: Vec<&Stmt> = Vec::new();
3160            flatten_leaves(&cb, &mut leaves);
3161            matches!(leaves.as_slice(), [Stmt::Throw(Expr::Local { var, .. })] if *var == c.var)
3162        };
3163        if bare_rethrow {
3164            continue;
3165        }
3166        let mut c2 = c.clone();
3167        *c2.body = cb;
3168        cs.push(c2);
3169    }
3170    let mut new_body = (**body).clone();
3171    let adopted = Stmt::Try {
3172        body: Box::new(t),
3173        catches: cs,
3174        finally: None,
3175    };
3176    match &mut new_body {
3177        Stmt::Block(bv) => bv.push(adopted),
3178        other => {
3179            let inner = std::mem::replace(other, Stmt::Block(vec![]));
3180            *other = Stmt::Block(vec![inner, adopted]);
3181        }
3182    }
3183    if let Stmt::Block(bv) = &mut new_body {
3184        bv.extend(absorbed_mid);
3185    }
3186    let sync = Stmt::Synchronized {
3187        lock: lock.clone(),
3188        body: Box::new(new_body),
3189    };
3190    let mut repl = vec![sync];
3191    if let Some(r) = rest_after {
3192        repl.push(r);
3193    }
3194    Some((repl, consumed))
3195}
3196
3197/// Statements orderable into the lock body: pure reads (cannot throw or
3198/// bind) and — handled by the caller's pattern — plain local-to-local
3199/// register commits.
3200fn mid_safe(e: &Expr) -> bool {
3201    !has_side_effects(e)
3202}
3203
3204/// Plain `v = local;` register commit (phi materialization at the join).
3205fn is_local_commit(e: &Expr) -> bool {
3206    match e {
3207        Expr::Assign { target, op: AssignOp::Plain, .. } => {
3208            matches!(&**target, Expr::Local { .. })
3209        }
3210        _ => false,
3211    }
3212}
3213
3214/// Defs visible to the parked release: statements before the fold plus the
3215/// absorbed alias head at the synchronized body's top level.
3216fn defs_probe<'a>(
3217    v: &'a [Stmt],
3218    i: usize,
3219    body: &'a Stmt,
3220) -> jdc_core::FxHashMap<u32, Expr> {
3221    let mut defs: jdc_core::FxHashMap<u32, Expr> = jdc_core::FxHashMap::default();
3222    for s in &v[..i] {
3223        if let Stmt::LocalDef { var, init: Some(e), .. } = s {
3224            defs.insert(*var, e.clone());
3225        }
3226    }
3227    if let Stmt::Block(bv) = body {
3228        for s in bv {
3229            if let Stmt::LocalDef { var, init: Some(e), .. } = s {
3230                defs.insert(*var, e.clone());
3231            }
3232        }
3233    }
3234    defs
3235}
3236
3237
3238/// Case D: the enter sits mid-control-flow inside the try body (nested in
3239/// if/else branches). The spine from the body root to the enter must end
3240/// at the body end — nothing may follow the fold point at any ancestor
3241/// level. The Try STAYS; only the enter's tail becomes a synchronized
3242/// block in place. The catch's release is dropped: with the lock release
3243/// owned by the synchronized, the handler's monitorexit would double-release
3244/// (the sync's implicit release fires first, then the outer catch
3245/// re-releases an unheld monitor).
3246fn try_sync_deep(v: &[Stmt], i: usize) -> Option<(Vec<Stmt>, usize)> {
3247    let Stmt::Try {
3248        body,
3249        catches,
3250        finally,
3251    } = &v[i]
3252    else {
3253        return None;
3254    };
3255    if finally.is_some() || catches.len() != 1 {
3256        return None;
3257    }
3258    let c = &catches[0];
3259    if !c.exc.is_empty() {
3260        return None;
3261    }
3262    let mut flat: Vec<&Stmt> = Vec::new();
3263    flatten_leaves(c.body.as_ref(), &mut flat);
3264    if flat.len() < 2 {
3265        return None;
3266    }
3267    let catch_exit = match flat[0] {
3268        Stmt::MonitorExit(e) => e.clone(),
3269        _ => return None,
3270    };
3271    if !matches!(flat[flat.len() - 1], Stmt::Throw(_)) {
3272        return None;
3273    }
3274    for mid in &flat[1..flat.len() - 1] {
3275        if !matches!(mid, Stmt::MonitorEnter(_) | Stmt::MonitorExit(_)) {
3276            return None;
3277        }
3278    }
3279    let mut defs0: jdc_core::FxHashMap<u32, Expr> = jdc_core::FxHashMap::default();
3280    for s in &v[..i] {
3281        if let Stmt::LocalDef { var, init: Some(e), .. } = s {
3282            defs0.insert(*var, e.clone());
3283        }
3284    }
3285    let mut body_mut = (**body).clone();
3286    let folded = match &mut body_mut {
3287        Stmt::Block(b) => try_fold_spine(b, &catch_exit, &defs0),
3288        Stmt::If {
3289            then_stmt,
3290            else_stmt,
3291            ..
3292        } => {
3293            let mut hit = false;
3294            if let Stmt::Block(b) = &mut **then_stmt {
3295                hit |= try_fold_spine(b, &catch_exit, &defs0);
3296            }
3297            if !hit {
3298                if let Some(els) = else_stmt {
3299                    if let Stmt::Block(b) = &mut **els {
3300                        hit |= try_fold_spine(b, &catch_exit, &defs0);
3301                    }
3302                }
3303            }
3304            hit
3305        }
3306        _ => false,
3307    };
3308    if !folded {
3309        return None;
3310    }
3311    // Reduce the catch-all to a bare rethrow (see fn doc).
3312    let throw = flat[flat.len() - 1].clone();
3313    let mut catches_out = catches.clone();
3314    *catches_out[0].body = Stmt::Block(vec![throw]);
3315    let mut consumed = 1;
3316    if let Some(Stmt::MonitorExit(e)) = v.get(i + 1) {
3317        if lock_equiv(e, &catch_exit, &defs0) {
3318            consumed += 1;
3319        }
3320    }
3321    Some((
3322        vec![Stmt::Try {
3323            body: Box::new(body_mut),
3324            catches: catches_out,
3325            finally: None,
3326        }],
3327        consumed,
3328    ))
3329}
3330
3331/// Fold `[ …pre…, monitorenter(e), rest… ]` whose spine reaches the
3332/// sequence end: at the enter's own level everything after it joins the
3333/// synchronized body; at every ancestor level the spine element must be
3334/// the LAST element (branches are alternatives, not successors). Mutates
3335/// in place; returns true when a fold landed.
3336fn try_fold_spine(
3337    seq: &mut Vec<Stmt>,
3338    catch_exit: &Expr,
3339    defs_in: &jdc_core::FxHashMap<u32, Expr>,
3340) -> bool {
3341    for p in (0..seq.len()).rev() {
3342        // Direct enter at this level: everything after it joins the body.
3343        if let Stmt::MonitorEnter(e0) = &seq[p] {
3344            let mut defs = defs_in.clone();
3345            for s in &seq[..p] {
3346                if let Stmt::LocalDef { var, init: Some(ex), .. } = s {
3347                    defs.insert(*var, ex.clone());
3348                }
3349            }
3350            // Lock-relevant defs parked just after the enter (the promoted
3351            // lock local's declaration) resolve the catch's release.
3352            for s in &seq[p + 1..] {
3353                match s {
3354                    Stmt::LocalDef { var, init: Some(ex), .. } if is_pure_read_expr(ex) => {
3355                        defs.insert(*var, ex.clone());
3356                    }
3357                    _ => break,
3358                }
3359            }
3360            if !lock_equiv(e0, catch_exit, &defs) {
3361                return false;
3362            }
3363            let enter_e = e0.clone();
3364            let rest: Vec<Stmt> = seq.split_off(p + 1);
3365            let mut body = Stmt::Block(rest);
3366            strip_lock_exits(&mut body, &enter_e, &defs);
3367            seq[p] = Stmt::Synchronized {
3368                lock: enter_e,
3369                body: Box::new(body),
3370            };
3371            return true;
3372        }
3373        // Nested attempt on a clone; commit only when the spine element is
3374        // this level's last (nothing may follow the fold point).
3375        let is_last = p + 1 == seq.len();
3376        let mut trial = seq[p].clone();
3377        let hit = match &mut trial {
3378            Stmt::Block(b) => try_fold_spine(b, catch_exit, defs_in),
3379            Stmt::If {
3380                then_stmt,
3381                else_stmt,
3382                ..
3383            } => {
3384                let mut hit = false;
3385                if let Stmt::Block(b) = &mut **then_stmt {
3386                    hit |= try_fold_spine(b, catch_exit, defs_in);
3387                }
3388                if !hit {
3389                    if let Some(els) = else_stmt {
3390                        if let Stmt::Block(b) = &mut **els {
3391                            hit |= try_fold_spine(b, catch_exit, defs_in);
3392                        }
3393                    }
3394                }
3395                hit
3396            }
3397            _ => false,
3398        };
3399        if hit {
3400            if is_last {
3401                seq[p] = trial;
3402                return true;
3403            }
3404            return false;
3405        }
3406    }
3407    false
3408}
3409
3410/// Copy-forward single-use temporaries: `v = e; ... v ...` inlines `e` at
3411/// the (single) use and drops the assignment when safe. Pure values inline
3412/// anywhere; impure values (calls, array reads) only when the use is the
3413/// statement immediately after the definition.
3414pub fn forward_single_use(s: &mut Stmt, _vt: &VarTable) {
3415    // Fused single traversal: assignment counts, read counts and the
3416    // single-assignment values all come out of ONE walk (three separate
3417    // full-tree walks dominated the pass cost on large methods).
3418    let mut analysis = VarAnalysis {
3419        assigns: Vec::new(),
3420        reads: Vec::new(),
3421        values: Vec::new(),
3422    };
3423    analyze_vars(s, &mut analysis);
3424    let mut assigns = analysis.assigns;
3425    let mut reads = analysis.reads;
3426    // Var ids beyond the table (synthetic test shapes) are covered by the
3427    // counters' on-demand growth.
3428    let n_vars = assigns.len().max(_vt.vars.len()).max(1);
3429    assigns.resize(n_vars, 0);
3430    reads.resize(n_vars, 0);
3431    let mut values = analysis.values;
3432    values.resize(n_vars, None);
3433
3434    // Candidates: assigned exactly once, read exactly once. Additionally
3435    // the inlined VALUE must not reference a multi-assigned var (phi vars):
3436    // moving the read across the phi's reassignment would be a stale
3437    // capture (register rotations snapshot through temps for this reason).
3438    let mut cand = vec![false; n_vars];
3439    let mut edges: Vec<Vec<u32>> = vec![Vec::new(); n_vars];
3440    for (v, cand_v) in cand.iter_mut().enumerate() {
3441        if assigns.get(v).copied().unwrap_or(0) != 1 || reads.get(v).copied().unwrap_or(0) != 1 {
3442            continue;
3443        }
3444        if let Some(val) = values.get(v).and_then(|o| o.as_ref()) {
3445            let mut refs = HashSet::default();
3446            collect_vars(val, &mut refs);
3447            if refs.iter().any(|r| assigns[*r as usize] > 1) {
3448                continue;
3449            }
3450            // Growth-graph edge set, built in the same walk: inlining v
3451            // inserts a clone of its value, and every Local inside the
3452            // clone is replaced in turn.
3453            edges[v] = refs.into_iter().collect();
3454        }
3455        *cand_v = true;
3456    }
3457
3458    // Cycle rejection on the growth graph. A def-reference cycle
3459    // (v = f(w), w = g(v) — loop-carried register rotation reaching the
3460    // pass as two mutually-referencing single-assign/single-read locals)
3461    // re-introduces the other cycle Local at every replacement level, so
3462    // deep_rewrite grows the tree one layer per level and never
3463    // terminates: weixin's com/tencent/mm/plugin/appbrand/widget/input/b4
3464    // exhausted a 64MB worker stack at ~300k recursion frames. Edges into
3465    // non-candidates are harmless (they are never replaced, so growth
3466    // dies there — and they carry no outgoing edges).
3467    //
3468    // Three-color DFS, iterative: a legit 60k-long single-use chain must
3469    // not trade one stack overflow for another. A GRAY child closes a
3470    // cycle; `bad[v]` (v on a cycle, or v's inlined closure grows into
3471    // one) then taints the whole current path — everything on it reaches
3472    // the cycle. Black verdicts memoize: a node whose subtree was proven
3473    // clean cannot grow a cycle later.
3474    let mut color = vec![0u8; n_vars]; // 0 white, 1 gray, 2 black
3475    let mut bad = vec![false; n_vars];
3476    for root in 0..n_vars {
3477        if color[root] != 0 || edges[root].is_empty() {
3478            continue;
3479        }
3480        color[root] = 1;
3481        let mut stack: Vec<(usize, usize)> = vec![(root, 0)];
3482        while let Some(top) = stack.last_mut() {
3483            let v = top.0;
3484            let i = top.1;
3485            top.1 += 1;
3486            if i < edges[v].len() {
3487                let r = edges[v][i] as usize;
3488                if color[r] == 1 {
3489                    for &(pn, _) in &stack {
3490                        bad[pn] = true;
3491                    }
3492                } else if color[r] == 0 {
3493                    color[r] = 1;
3494                    stack.push((r, 0));
3495                } else if bad[r] {
3496                    for &(pn, _) in &stack {
3497                        bad[pn] = true;
3498                    }
3499                }
3500            } else {
3501                color[v] = 2;
3502                stack.pop();
3503            }
3504        }
3505    }
3506    let single: Vec<bool> = (0..n_vars).map(|v| cand[v] && !bad[v]).collect();
3507    if !single.iter().any(|&b| b) {
3508        return;
3509    }
3510    let mut pure: Vec<bool> = (0..n_vars)
3511        .map(|v| {
3512            single[v]
3513                && values
3514                    .get(v)
3515                    .and_then(|o| o.as_ref())
3516                    .map(|e| !has_side_effects(e))
3517                    .unwrap_or(false)
3518        })
3519        .collect();
3520    let mut impure: Vec<bool> = (0..n_vars).map(|v| single[v] && !pure[v]).collect();
3521
3522    // A Field read is pure (no side effect) but NOT position-independent:
3523    // re-rendering it at the use site reads whatever the field holds
3524    // THERE. d8's `v = !this.f; this.f = true;` lifts as iget→xor→iput;
3525    // the lifter materializes the read at its defining pc (force_mat,
3526    // lift.rs) precisely so the xor sees the OLD value — and this pass
3527    // used to re-inline that temp past the write, resurrecting the
3528    // stale read (weibo Transmitter.exchangeMessageDone rendered
3529    // `this.f = true; v = !this.f;`). Block the inline-when-anywhere
3530    // path for a value whose field is written between the def and the
3531    // single use (statement pre-order positions); such candidates fall
3532    // through to the adjacent-only impure path, which cannot cross
3533    // statements.
3534    {
3535        type FieldKey = (std::sync::Arc<str>, std::sync::Arc<str>, bool);
3536        let mut field_writes: jdc_core::FxHashMap<FieldKey, Vec<usize>> =
3537            jdc_core::FxHashMap::default();
3538        let mut def_pos: Vec<usize> = vec![usize::MAX; n_vars];
3539        let mut use_pos: Vec<usize> = vec![usize::MAX; n_vars];
3540        let mut pos = 0usize;
3541        walk_all(s, &mut |st| {
3542            let here = pos;
3543            pos += 1;
3544            for e in stmt_read_exprs(st) {
3545                visit_exprs(e, &mut |x| {
3546                    if let Expr::Local { var, .. } = x {
3547                        let v = *var as usize;
3548                        if v < use_pos.len() && use_pos[v] == usize::MAX {
3549                            use_pos[v] = here;
3550                        }
3551                    }
3552                });
3553            }
3554            match st {
3555                Stmt::LocalDef { var, .. } => {
3556                    let v = *var as usize;
3557                    if v < def_pos.len() {
3558                        def_pos[v] = here;
3559                    }
3560                }
3561                Stmt::ExprStmt(Expr::Assign { target, .. }) => match &**target {
3562                    Expr::Local { var, .. } => {
3563                        let v = *var as usize;
3564                        if v < def_pos.len() {
3565                            def_pos[v] = here;
3566                        }
3567                    }
3568                    Expr::Field { cls, name, is_static, .. } => {
3569                        field_writes
3570                            .entry((cls.clone(), name.clone(), *is_static))
3571                            .or_default()
3572                            .push(here);
3573                    }
3574                    _ => {}
3575                },
3576                Stmt::ExprStmt(Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. }) => {
3577                    if let Expr::Field { cls, name, is_static, .. } = &**e {
3578                        field_writes
3579                            .entry((cls.clone(), name.clone(), *is_static))
3580                            .or_default()
3581                            .push(here);
3582                    }
3583                }
3584                _ => {}
3585            }
3586        });
3587        let mut hazard = vec![false; n_vars];
3588        for v in 0..n_vars {
3589            if !pure[v] {
3590                continue;
3591            }
3592            let Some(Some(val)) = values.get(v).map(|o| o.as_ref()) else {
3593                continue;
3594            };
3595            let (d, u) = (def_pos[v], use_pos[v]);
3596            if d == usize::MAX || u == usize::MAX || u <= d {
3597                continue;
3598            }
3599            visit_exprs(val, &mut |x| {
3600                if let Expr::Field { cls, name, is_static, .. } = x {
3601                    if let Some(ws) = field_writes.get(&(cls.clone(), name.clone(), *is_static)) {
3602                        if ws.iter().any(|w| *w > d && *w < u) {
3603                            hazard[v] = true;
3604                        }
3605                    }
3606                }
3607            });
3608        }
3609        for v in 0..n_vars {
3610            if hazard[v] {
3611                pure[v] = false;
3612                impure[v] = single[v];
3613            }
3614        }
3615    }
3616
3617    // 1. Pure values: inline anywhere.
3618    if pure.iter().any(|&b| b) {
3619        let vals: Vec<Option<Expr>> = (0..n_vars)
3620            .map(|v| {
3621                if pure[v] {
3622                    values.get(v).cloned().flatten()
3623                } else {
3624                    None
3625                }
3626            })
3627            .collect();
3628        rewrite_exprs(s, &mut |e| {
3629            deep_rewrite_reads(e, &mut |x| {
3630                if let Expr::Local { var, .. } = x {
3631                    if let Some(Some(v)) = vals.get(*var as usize) {
3632                        *x = v.clone();
3633                    }
3634                }
3635            });
3636        });
3637        drop_defs(s, &pure);
3638    }
3639
3640    // 2. Impure values: inline only into the immediately following
3641    //    statement.
3642    if impure.iter().any(|&b| b) {
3643        forward_adjacent_impure(s, &values, &impure);
3644    }
3645}
3646
3647/// The expression payloads a statement READS — the statement's own node
3648/// only (walk_all visits nested statements separately). Assignment
3649/// targets are writes (a non-local target still reads its owner/index
3650/// subtree); shared by count_reads and the position scan in
3651/// forward_single_use so the two can never drift apart.
3652fn stmt_read_exprs(st: &Stmt) -> Vec<&Expr> {
3653    match st {
3654        Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
3655            let mut v: Vec<&Expr> = vec![value];
3656            if !matches!(&**target, Expr::Local { .. }) {
3657                v.push(target);
3658            }
3659            v
3660        }
3661        // `v++` writes v — not a read (a compound `a[i]++` still reads
3662        // its owner/index subtree).
3663        Stmt::ExprStmt(e @ (Expr::PreIncDec { .. } | Expr::PostIncDec { .. })) => {
3664            let inner = match e {
3665                Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => e,
3666                _ => unreachable!(),
3667            };
3668            if matches!(**inner, Expr::Local { .. }) {
3669                Vec::new()
3670            } else {
3671                vec![e]
3672            }
3673        }
3674        Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
3675            vec![e]
3676        }
3677        Stmt::Return(Some(e)) => vec![e],
3678        Stmt::LocalDef { init: Some(e), .. } => vec![e],
3679        Stmt::If { cond, .. } => vec![cond],
3680        Stmt::While { cond, .. } => vec![cond],
3681        Stmt::DoWhile { cond, .. } => vec![cond],
3682        // Read sites that are NOT statement children (walk_all recurses
3683        // into bodies but these payloads are expressions on the node
3684        // itself). Omitting them under-counts reads, which would let
3685        // drop_dead_locals over-prune a var whose only use is a switch
3686        // selector / loop condition / lock / iterable.
3687        Stmt::Switch { selector, .. } => vec![selector],
3688        Stmt::ForEach { iterable, .. } => vec![iterable],
3689        Stmt::Synchronized { lock, .. } => vec![lock],
3690        Stmt::For { cond, update, .. } => {
3691            let mut v: Vec<&Expr> = Vec::with_capacity(update.len() + 1);
3692            if let Some(c) = cond {
3693                v.push(c);
3694            }
3695            v.extend(update.iter());
3696            v
3697        }
3698        Stmt::Assert { cond, msg } => {
3699            let mut v: Vec<&Expr> = vec![cond];
3700            if let Some(m) = msg {
3701                v.push(m);
3702            }
3703            v
3704        }
3705        _ => Vec::new(),
3706    }
3707}
3708
3709/// Count READS of variables (assignment targets excluded).
3710fn count_reads(s: &Stmt, out: &mut Vec<usize>) {
3711    walk_all(s, &mut |st| {
3712        for e in stmt_read_exprs(st) {
3713            visit_exprs(e, &mut |x| {
3714                if let Expr::Local { var, .. } = x {
3715                    grow_to(out, *var);
3716                    out[*var as usize] += 1;
3717                }
3718            });
3719        }
3720    });
3721}
3722
3723
3724fn drop_defs(s: &mut Stmt, vars: &[bool]) {
3725    walk_mut_deep(s, &mut |st| {
3726        if let Stmt::Block(v) = st {
3727            v.retain(|x| match x {
3728                Stmt::ExprStmt(Expr::Assign { target, .. }) => match &**target {
3729                    Expr::Local { var, .. } => !vars.get(*var as usize).copied().unwrap_or(false),
3730                    _ => true,
3731                },
3732                Stmt::LocalDef { var, .. } => !vars.get(*var as usize).copied().unwrap_or(false),
3733                _ => true,
3734            });
3735        }
3736    });
3737}
3738
3739/// For each statement list: `v = IMPURE; NEXT(v)` folds NEXT's reference.
3740fn forward_adjacent_impure(s: &mut Stmt, values: &[Option<Expr>], impure: &[bool]) {
3741    if let Stmt::Block(v) = s {
3742        let mut i = 0;
3743        while i + 1 < v.len() {
3744            let def_var = match &v[i] {
3745                Stmt::ExprStmt(Expr::Assign { target, .. }) => match &**target {
3746                    Expr::Local { var, .. } => Some(*var),
3747                    _ => None,
3748                },
3749                Stmt::LocalDef { var, .. } => Some(*var),
3750                _ => None,
3751            };
3752            if let Some(var) = def_var {
3753                if impure.get(var as usize).copied().unwrap_or(false) {
3754                    // Does the NEXT statement reference var?
3755                    let mut reads_here = vec![0usize; values.len().max(1)];
3756                    count_reads(&v[i + 1], &mut reads_here);
3757                    if reads_here.get(var as usize).copied().unwrap_or(0) == 1 {
3758                        // The def statement's CURRENT value (earlier inlines
3759                        // in this walk may have rewritten it — the pre-built
3760                        // values map would be stale).
3761                        let val = match &v[i] {
3762                            Stmt::ExprStmt(Expr::Assign { value, .. }) => (**value).clone(),
3763                            Stmt::LocalDef { init: Some(e), .. } => (*e).clone(),
3764                            _ => {
3765                                i += 1;
3766                                continue;
3767                            }
3768                        };
3769                        rewrite_exprs(&mut v[i + 1], &mut |e| {
3770                            deep_rewrite_reads(e, &mut |x| {
3771                                if let Expr::Local { var: v2, .. } = x {
3772                                    if *v2 == var {
3773                                        *x = val.clone();
3774                                    }
3775                                }
3776                            });
3777                        });
3778                        v.remove(i);
3779                        // The PREVIOUS definition may now be adjacent to its
3780                        // (shifted) use — re-examine it.
3781                        i = i.saturating_sub(1);
3782                        continue;
3783                    }
3784                }
3785            }
3786            i += 1;
3787        }
3788    }
3789    walk_mut(s, &mut |x| forward_adjacent_impure(x, values, impure));
3790}
3791
3792// ---------------------------------------------------------------------------
3793// Type inference, booleanization, null comparisons, declarations
3794// ---------------------------------------------------------------------------
3795
3796/// Evidence-based type inference for synthetic (non-parameter) vars.
3797pub fn infer_types(vt: &mut VarTable, body: &mut Stmt, ret: &JavaType, env: &MethodEnv) {
3798    let n = vt.vars.len();
3799    // (type, strong) — DIRECTIONAL evidence. Strong facts (an assigned
3800    // value's type, the declared return/throw/monitor type) DEFINE the
3801    // variable; weak facts (call argument expectations, receiver
3802    // contexts, numeric literals) only constrain a use. jadx's bound
3803    // system carries the same direction; a flat pool let a weak
3804    // expectation (`v26 = p1` with boolean p1 beside `v26 = 0` sugar)
3805    // outvote the definition.
3806    let mut evidence: Vec<Vec<(JavaType, bool)>> = vec![Vec::new(); n];
3807    let ev = |evidence: &mut Vec<Vec<(JavaType, bool)>>, var: u32, t: JavaType, strong: bool| {
3808        if (var as usize) < evidence.len() {
3809            evidence[var as usize].push((t, strong));
3810        }
3811    };
3812
3813    walk_all(body, &mut |st| match st {
3814        Stmt::LocalDef { var, init, .. } => {
3815            if let Some(e) = init {
3816                expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
3817                // The initializer DEFINES the declared type: `long v = l(...)`
3818                // must not stay `int v` (silent 64-bit truncation in the
3819                // eyes of a reader; javac rejects it as lossy). Null
3820                // carries no type (it would DOWNGRADE String to Object).
3821                // Literals are weak (0/1 is boolean sugar half the time);
3822                // typed producers and parameter sources are strong;
3823                // local-to-local copies are weak (register reuse).
3824                let strong = match e {
3825                    Expr::Const(_) => false,
3826                    Expr::Local { var: src, .. } => {
3827                        let src_ty = vt.vars.get(*src as usize).map(|v| v.ty.erased());
3828                        let tgt_ty = vt.vars.get(*var as usize).map(|v| v.ty.erased());
3829                        match (src_ty, tgt_ty) {
3830                            (Some(s), Some(g)) => {
3831                                (s.is_numeric() && g.is_numeric())
3832                                    || (s.is_reference() && g.is_reference())
3833                            }
3834                            _ => false,
3835                        }
3836                    }
3837                    _ => true,
3838                };
3839                ev(&mut evidence, *var, e.type_ref().erased(), strong);
3840            }
3841        }
3842        Stmt::ExprStmt(e) => {
3843            // Assignment targets: a typed PRODUCER (method/new/field) or a
3844            // PARAMETER source is a STRONG definition (`v26 = p1` with
3845            // boolean p1). A local-to-local copy is weak — register reuse
3846            // and merge materialization emit exactly that shape with
3847            // mismatched types (`sb7 = compareTo10` in a Kotlin when).
3848            // Literals stay weak.
3849            if let Expr::Assign { target, value, .. } = e {
3850                // A field WRITE through a local (`v.f = x`, dex iput)
3851                // proves v is a reference of the field's declaring
3852                // class AT THAT POINT — an iput on a boolean register
3853                // cannot exist in valid dex. Strong: it must outrank a
3854                // strong boolean copy chain into the same reused slot
3855                // (weixin coroutine state machines).
3856                if let Expr::Field { owner: Some(o), cls, is_static: false, .. } = &**target {
3857                    if cls.is_empty() {
3858                        // array-length pseudo-field: no declaring class
3859                    } else if let Expr::Local { var, .. } = &**o {
3860                        ev(
3861                            &mut evidence,
3862                            *var,
3863                            JavaType::Object(cls.clone()),
3864                            true,
3865                        );
3866                    }
3867                }
3868                if let Expr::Local { var, .. } = &**target {
3869                    let strong = match &**value {
3870                        Expr::Const(_) => false,
3871                        Expr::Local { var: src, .. } => {
3872                            // A local copy is strong when the source and
3873                            // the target's register type are the same
3874                            // FAMILY (numeric↔numeric, reference↔
3875                            // reference) — a cross-family copy (int into
3876                            // a StringBuilder local) is merge-material
3877                            // residue and stays weak.
3878                            let src_ty = vt.vars.get(*src as usize).map(|v| v.ty.erased());
3879                            let tgt_ty = vt.vars.get(*var as usize).map(|v| v.ty.erased());
3880                            match (src_ty, tgt_ty) {
3881                                (Some(s), Some(g)) => {
3882                                    (s.is_numeric() && g.is_numeric())
3883                                        || (s.is_reference() && g.is_reference())
3884                                }
3885                                _ => false,
3886                            }
3887                        }
3888                        _ => true,
3889                    };
3890                    ev(&mut evidence, *var, value.type_ref().erased(), strong);
3891                }
3892            }
3893            expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
3894        }
3895        Stmt::Throw(e) => {
3896            expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
3897            if let Expr::Local { var, .. } = e {
3898                ev(
3899                    &mut evidence,
3900                    *var,
3901                    JavaType::Object("java/lang/Throwable".into()),
3902                    true,
3903                );
3904            }
3905        }
3906        Stmt::Return(Some(e)) => {
3907            expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
3908            if let Expr::Local { var, .. } = e {
3909                ev(&mut evidence, *var, ret.clone(), true);
3910            }
3911        }
3912        Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
3913            if let Expr::Local { var, .. } = e {
3914                ev(
3915                    &mut evidence,
3916                    *var,
3917                    JavaType::Object("java/lang/Object".into()),
3918                    true,
3919                );
3920            }
3921        }
3922        Stmt::ForEach {
3923            var,
3924            iterable,
3925            is_array,
3926            ..
3927        } => {
3928            if let Expr::Local { var: v0, .. } = iterable {
3929                if *is_array {
3930                    ev(&mut evidence, *v0, JavaType::Array(Box::new(JavaType::Int)), true);
3931                } else {
3932                    ev(
3933                        &mut evidence,
3934                        *v0,
3935                        JavaType::Object("java/lang/Object".into()),
3936                        true,
3937                    );
3938                }
3939            }
3940            let _ = var;
3941        }
3942        _ => {}
3943    });
3944    let _ = env;
3945
3946    // Return-position locals take the declared return type (also inside
3947    // nested returns — handled by the walk above).
3948
3949        // Vars dereferenced as field/method receivers or arrays: their
3950    // resolved type must be a dereferenceable class — a boxed Boolean
3951    // in the strong set (Kotlin Result registers hold `Boolean | impl`
3952    // across coroutine case arms) must lose to the real class even
3953    // when the Boolean evidence comes first (weixin ry0/h2's
3954    // `Boolean bool19` receiving `bool19.d = x`).
3955    let mut derefed: Vec<bool> = vec![false; vt.vars.len()];
3956    mark_derefs(body, &mut derefed);
3957    // Array-indexed vars: their resolved type must be an ARRAY when
3958    // the evidence has one (`num4[v68]` with Integer-typed num4 —
3959    // 需要数组但找到Integer family); member derefs keep the class
3960    // preference below.
3961    let mut arr_derefed: Vec<bool> = vec![false; vt.vars.len()];
3962    visit_all_exprs(body, &mut |x| {
3963        if let Expr::ArrayIndex { array, .. } = x {
3964            if let Expr::Local { var, .. } = &**array {
3965                if (*var as usize) < arr_derefed.len() {
3966                    arr_derefed[*var as usize] = true;
3967                }
3968            }
3969        }
3970        // `v != null` is STRONG reference evidence: the lifter only
3971        // renders the null form when the dex value view held a
3972        // reference register at that pc — an int-typed generation
3973        // compared against null is phi-confluence residue (yq0/g1's
3974        // `v95_g12 != null`, 546-line 二元运算符 family). Strong Object
3975        // makes the resolver retype and split_generations separate the
3976        // int writes into their own generation.
3977        if let Expr::Bin { op, l, r, .. } = x {
3978            if matches!(op, BinOp::Eq | BinOp::Ne)
3979                && (matches!(&**r, Expr::Const(ConstVal::Null))
3980                    || matches!(&**l, Expr::Const(ConstVal::Null)))
3981            {
3982                for side in [l, r] {
3983                    if let Expr::Local { var, .. } = &**side {
3984                        ev(
3985                            &mut evidence,
3986                            *var,
3987                            JavaType::Object("java/lang/Object".into()),
3988                            true,
3989                        );
3990                    }
3991                }
3992            }
3993        }
3994    });
3995    const FINAL_JDK: &[&str] = &[
3996        "java/lang/Boolean",
3997        "java/lang/String",
3998        "java/lang/Integer",
3999        "java/lang/Long",
4000        "java/lang/Short",
4001        "java/lang/Byte",
4002        "java/lang/Character",
4003        "java/lang/Float",
4004        "java/lang/Double",
4005    ];
4006    for (i, evs) in evidence.iter().enumerate() {
4007        let info = &vt.vars[i];
4008        if info.is_param {
4009            continue;
4010        }
4011        let strong: Vec<JavaType> = evs
4012            .iter()
4013            .filter(|(_, s)| *s)
4014            .map(|(t, _)| t.clone())
4015            .collect();
4016        let all: Vec<JavaType> = evs.iter().map(|(t, _)| t.clone()).collect();
4017        if info.ty.erased().is_reference() {
4018            // Materialization residue: no strong definition, and the weak
4019            // pool names ≥2 DIFFERENT classes (a Kotlin `when` lowered
4020            // every branch's value into one register slot) — neither
4021            // class can win, every assignment needs boxing headroom:
4022            // widen to Object (uses get receiver casts).
4023            if strong.is_empty() {
4024                let refs: std::collections::HashSet<&str> = all
4025                    .iter()
4026                    .filter_map(|t| match t {
4027                        JavaType::Object(n) if n.as_ref() != "java/lang/Object" => {
4028                            Some(n.as_ref())
4029                        }
4030                        _ => None,
4031                    })
4032                    .collect();
4033                let mixed = all.iter().any(|t| t.is_numeric());
4034                if refs.len() >= 2 || (mixed && !refs.is_empty()) || (mixed && all.iter().any(|t| matches!(t, JavaType::Array(_)))) {
4035                    vt.vars[i].ty =
4036                        TypeRef::J(JavaType::Object("java/lang/Object".into()));
4037                    continue;
4038                }
4039            }
4040                        // Array-dereferenced vars prefer array evidence (an
4041            // Integer producer cannot be indexed).
4042            if arr_derefed[i] {
4043                let arr = strong
4044                    .iter()
4045                    .chain(all.iter())
4046                    .find(|t| matches!(t, JavaType::Array(_)))
4047                    .cloned();
4048                if let Some(t) = arr {
4049                    vt.vars[i].ty = TypeRef::J(t);
4050                    continue;
4051                }
4052            }
4053            // Strong definitions outrank weak expectations. A
4054            // dereferenced var prefers a real class over final JDK
4055            // boxes (Boolean can never carry `.d = x`).
4056            let deref_pick = || {
4057                if !derefed[i] {
4058                    return None;
4059                }
4060                strong
4061                    .iter()
4062                    .find(|t| {
4063                        matches!(t, JavaType::Object(n)
4064                            if !n.is_empty()
4065                                && n.as_ref() != "java/lang/Object"
4066                                && !FINAL_JDK.contains(&n.as_ref()))
4067                    })
4068                    .cloned()
4069            };
4070            if let Some(t) = deref_pick()
4071                .or_else(|| pick_object(&strong))
4072                .or_else(|| pick_object(&all))
4073            {
4074                vt.vars[i].ty = TypeRef::J(t);
4075            }
4076        } else if let Some(t) = pick_object(&strong) {
4077            // A strong reference definition (typed producer or field
4078            // write) outranks a strong boolean copy: Boolean assigns
4079            // INTO an Object slot legally, never the reverse.
4080            vt.vars[i].ty = TypeRef::J(t);
4081        } else if strong.contains(&JavaType::Boolean) {
4082            // A boolean-typed SOURCE assigned into the variable is a
4083            // definition (`v26 = p1` with boolean p1); the 0/1 literals
4084            // that share the pool are boolean sugar as often as not.
4085            vt.vars[i].ty = TypeRef::J(JavaType::Boolean);
4086        } else if !all.is_empty() && all.iter().all(|t| t.is_numeric()) {
4087            if let Some(t) = pick_numeric(&all) {
4088                vt.vars[i].ty = TypeRef::J(t);
4089            }
4090        } else {
4091            // Mixed-family materialization on a numeric-declared slot
4092            // (int slot receiving StringBuilders — the register was the
4093            // `when` accumulator): Object, same as the reference side.
4094            let refs = all.iter().filter(|t| t.is_reference()).count();
4095            let nums = all.iter().filter(|t| t.is_numeric()).count();
4096            if refs > 0 && nums > 0 {
4097                vt.vars[i].ty =
4098                    TypeRef::J(JavaType::Object("java/lang/Object".into()));
4099            } else if let Some(t) = pick_object(&strong).or_else(|| pick_object(&all)) {
4100                vt.vars[i].ty = TypeRef::J(t);
4101            }
4102        }
4103    }
4104
4105    // Rewrite embedded Local types. Borrowed view + write-only-on-change:
4106    // the table clone was n_vars JavaType clones per method, and the
4107    // rewrite cloned a type into EVERY Local node — most already carry
4108    // the right one (JavaType::Object clones allocate).
4109    let types: Vec<&TypeRef> = vt.vars.iter().map(|v| &v.ty).collect();
4110    rewrite_exprs(body, &mut |e| {
4111        deep_rewrite(e, &mut |x| {
4112            if let Expr::Local { var, ty } = x {
4113                if let Some(want) = types.get(*var as usize) {
4114                    if ty != *want {
4115                        *ty = (*want).clone();
4116                    }
4117                }
4118            }
4119        });
4120    });
4121
4122    // Numeric literals take their variable's declared width (`double v = 0L`
4123    // prints as `0.0`; `int x = 5L` as `5`).
4124    coerce_num_consts(body, &types);
4125}
4126
4127fn coerce_num_consts(body: &mut Stmt, types: &[&TypeRef]) {
4128    walk_mut_deep(body, &mut |st| {
4129        let (var, val): (u32, &mut Expr) = match st {
4130            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => match &mut **target {
4131                Expr::Local { var, .. } => (*var, value),
4132                _ => return,
4133            },
4134            Stmt::LocalDef {
4135                var, init: Some(e), ..
4136            } => (*var, e),
4137            _ => return,
4138        };
4139        let want = match types.get(var as usize) {
4140            Some(TypeRef::J(t)) => t.clone(),
4141            _ => return,
4142        };
4143        let rewritten = match &*val {
4144            Expr::Const(ConstVal::Long(l)) => match &want {
4145                JavaType::Double => Some(Expr::Const(ConstVal::Double(*l as f64))),
4146                JavaType::Float => Some(Expr::Const(ConstVal::Float(*l as f32))),
4147                _ => None,
4148            },
4149            Expr::Const(ConstVal::Int(i)) => match &want {
4150                JavaType::Double => Some(Expr::Const(ConstVal::Double(*i as f64))),
4151                JavaType::Float => Some(Expr::Const(ConstVal::Float(*i as f32))),
4152                _ => None,
4153            },
4154            _ => None,
4155        };
4156        if let Some(e) = rewritten {
4157            *val = e;
4158        }
4159    });
4160}
4161
4162fn pick_object(evs: &[JavaType]) -> Option<JavaType> {
4163    evs.iter()
4164        .find(|t| {
4165            t.is_reference()
4166                && !matches!(t, JavaType::Object(n) if n.is_empty() || n.as_ref() == "java/lang/Object")
4167        })
4168        .cloned()
4169}
4170
4171fn pick_numeric(evs: &[JavaType]) -> Option<JavaType> {
4172    if evs.is_empty() {
4173        return None;
4174    }
4175    let mut t = evs[0].clone();
4176    for x in &evs[1..] {
4177        t = join_numeric(&t, x);
4178    }
4179    Some(t)
4180}
4181
4182fn join_numeric(a: &JavaType, b: &JavaType) -> JavaType {
4183    if a == b {
4184        return a.clone();
4185    }
4186    match (a, b) {
4187        (JavaType::Double, _) | (_, JavaType::Double) => JavaType::Double,
4188        (JavaType::Float, _) | (_, JavaType::Float) => JavaType::Float,
4189        (JavaType::Long, _) | (_, JavaType::Long) => JavaType::Long,
4190        (JavaType::Int, x) if x.is_integral() => JavaType::Int,
4191        (x, JavaType::Int) if x.is_integral() => JavaType::Int,
4192        (x, y) if x.is_integral() && y.is_integral() => JavaType::Int,
4193        _ => JavaType::Int,
4194    }
4195}
4196
4197/// Gather per-var type evidence from one expression.
4198fn expr_evidence<F: FnMut(u32, JavaType)>(e: &Expr, f: &mut F) {
4199    visit_exprs(e, &mut |x| match x {
4200        Expr::Method {
4201            owner,
4202            args,
4203            desc,
4204            is_static,
4205            cls,
4206            ..
4207        } => {
4208            if !*is_static {
4209                if let Some(o) = owner {
4210                    if let Expr::Local { var, .. } = &**o {
4211                        f(*var, JavaType::Object(cls.clone()));
4212                    }
4213                }
4214            }
4215            for (i, a) in args.iter().enumerate() {
4216                if let Expr::Local { var, .. } = a {
4217                    if let Some(t) = desc.args.get(i) {
4218                        f(*var, t.clone());
4219                    }
4220                }
4221            }
4222        }
4223        Expr::Field {
4224            owner,
4225            cls,
4226            ty,
4227            is_static,
4228            ..
4229        } => {
4230            if !*is_static && !cls.is_empty() {
4231                if let Some(o) = owner {
4232                    if let Expr::Local { var, .. } = &**o {
4233                        // The field ref's declaring class is dex-exact
4234                        // evidence — the old hardcoded Object let a
4235                        // boolean copy chain outvote it (weixin ry0/h2's
4236                        // `Boolean bool19` receiving `bool19.d = x` —
4237                        // the 339-line 找不到符号 变量 d family).
4238                        // EMPTY cls = the array-length pseudo-field
4239                        // (lift models `a.length` as Field{cls:""}) —
4240                        // Object("") evidence poisoned pick_object into
4241                        // empty-typed declarations and `() x` casts.
4242                        f(*var, JavaType::Object(cls.clone()));
4243                    }
4244                }
4245            }
4246            let _ = ty;
4247        }
4248        // `v != null` / `v == null`: the register holds a reference at
4249        // that point — weak Object evidence keeps int-typed generations
4250        // from winning the pool (yq0/g1's `v95_g12 != null` with int
4251        // v95_g12 — 二元运算符 '!=' 546-line family).
4252        Expr::Bin { op, l, r, .. }
4253            if matches!(op, BinOp::Eq | BinOp::Ne)
4254                && (matches!(&**r, Expr::Const(ConstVal::Null))
4255                    || matches!(&**l, Expr::Const(ConstVal::Null))) =>
4256        {
4257            for side in [l, r] {
4258                if let Expr::Local { var, .. } = &**side {
4259                    f(*var, JavaType::Object("java/lang/Object".into()));
4260                }
4261            }
4262        }
4263        Expr::ArrayIndex { array, .. } => {
4264            if let Expr::Local { var, ty, .. } = &**array {
4265                // Reinforce the local's KNOWN array type: the hardcoded
4266                // int[] fallback contradicted a typed array local (a
4267                // register reused for byte[], int[] then byte[] across
4268                // one clinit labeled the byte[] defs `int[]`).
4269                let t = ty.erased();
4270                if matches!(t, JavaType::Array(_)) {
4271                    f(*var, t);
4272                } else {
4273                    f(*var, JavaType::Array(Box::new(JavaType::Int)));
4274                }
4275            }
4276        }
4277        Expr::Assign { .. } => {
4278            // Covered by the typed strong-evidence walk in infer_types;
4279            // keeping a weak duplicate here let expectations outvote it.
4280        }
4281        Expr::Cast { e: inner, .. } => {
4282            // A cast is an EXPLICIT narrowing at the use site — it must
4283            // NOT feed the cast target as evidence for the variable
4284            // (`Object get2 = list.get(2); j((String) get2);` had get2
4285            // retyped to String, making the assignment incompatible).
4286            // jadx's bounds carry direction: a USE bound narrows nothing.
4287            let _ = inner;
4288        }
4289        Expr::InstanceOf { e: inner, .. } => {
4290            let _ = inner;
4291        }
4292        _ => {}
4293    });
4294}
4295
4296/// Mark locals that OWN a non-static field access and whose inferred type
4297/// is a concrete `$<digits>` class (a d8/R8-desugared lambda or a Kotlin
4298/// suspend-lambda). Emit demotes such a type to its SAM interface in
4299/// DECLARED positions (`Function2 v = new Outer$..$1(..)`) for phi-friendly
4300/// readability — but interfaces carry no instance fields, so the Kotlin
4301/// coroutine `create()` capture writes (`v.L$0 = obj`, also I$/J$) render
4302/// "找不到符号 变量 L$0" (weibo MutableScatterMap$..$iterator$1, ~1k).
4303/// Forcing the concrete declared type keeps the field access legal; the phi
4304/// case is excluded because a disagreed merge stays Object (wide_stack_vars),
4305/// never a specific concrete class. Gate mirrors emit's demotion exactly
4306/// (last `$` segment all digits) so nothing else changes.
4307pub fn mark_field_owner_concrete(vt: &mut VarTable, body: &mut Stmt) {
4308    let mut owners: HashSet<u32> = HashSet::default();
4309    rewrite_exprs(body, &mut |e| {
4310        visit_exprs(e, &mut |x| {
4311            if let Expr::Field {
4312                owner: Some(o),
4313                is_static: false,
4314                ..
4315            } = x
4316            {
4317                if let Expr::Local { var, .. } = &**o {
4318                    owners.insert(*var);
4319                }
4320            }
4321        });
4322    });
4323    for var in owners {
4324        let i = var as usize;
4325        if i >= vt.vars.len() {
4326            continue;
4327        }
4328        if let JavaType::Object(cls) = vt.vars[i].ty.erased() {
4329            if let Some(last) = cls.rsplit('$').next() {
4330                if !last.is_empty() && last.chars().all(|c| c.is_ascii_digit()) {
4331                    vt.force_concrete_vars.insert(var);
4332                }
4333            }
4334        }
4335    }
4336    // EVERY local whose resolved type is a concrete `$<digits>` class
4337    // keeps its binary name, not just field owners: when one side of an
4338    // assignment falls back to the SAM/super (`ContinuationImpl v7`) and
4339    // its sibling stays concrete (`$reportWhenComplete$1 v9`), the pair
4340    // is an uncastable "ContinuationImpl无法转换为…$1" (weibo coroutine
4341    // prologues ×282). The anon fallback still serves INTERFACE-typed
4342    // phi slots (branches minting different lambdas) — their vt type is
4343    // the join, never the $N class.
4344    let concrete: Vec<u32> = vt
4345        .vars
4346        .iter()
4347        .filter(|v| {
4348            if let JavaType::Object(cls) = v.ty.erased() {
4349                cls.rsplit('$')
4350                    .next()
4351                    .is_some_and(|l| !l.is_empty() && l.chars().all(|c| c.is_ascii_digit()))
4352            } else {
4353                false
4354            }
4355        })
4356        .map(|v| v.id)
4357        .collect();
4358    for var in concrete {
4359        vt.force_concrete_vars.insert(var);
4360    }
4361}
4362
4363/// Type inference can leave a specific-reference-typed target assigned
4364/// from a `java/lang/Object`-typed value: a phi that merged String and
4365/// Object then settled on String (`String str4; ... str4 = obj;` where
4366/// `obj` is an Object field — y5/n.java), or an Object local flowing to a
4367/// typed field. Java requires a narrowing cast there; insert `(T) value`
4368/// at the USE site (round-40 philosophy: narrow at use, never retype the
4369/// declaration). Guarded tightly: only when the value's resolved static
4370/// type is EXACTLY `java/lang/Object` (the untyped top — a known subtype
4371/// is never re-cast), the value is not a null const (assignable to any
4372/// ref) nor already a cast, and the target is a specific reference type
4373/// (Object→int/boolean is unboxing, a different problem left alone).
4374pub fn insert_object_narrowing_casts(
4375    vt: &VarTable,
4376    body: &mut Stmt,
4377    pool: &DexPool,
4378    ret: &JavaType,
4379) {
4380    // Resolve a value's static type through the VarTable for locals (the
4381    // embedded Local ty can lag infer_types), else the expr's own type.
4382    // The erased materialization happens ONCE per statement and threads
4383    // through the checks — the old helpers re-erased 3-4x per statement
4384    // (each a JavaType clone with Array chains) and this pass runs over
4385    // every Assign/LocalDef/Return/call-arg corpus-wide.
4386    let value_ty = |e: &Expr| -> JavaType {
4387        match e {
4388            Expr::Local { var, .. } => vt.var(*var).ty.erased(),
4389            other => other.type_ref().erased(),
4390        }
4391    };
4392    let is_top_object_ty = |v: &JavaType| -> bool {
4393        matches!(v, JavaType::Object(c) if c.as_ref() == "java/lang/Object")
4394    };
4395    // A specific reference target type (a class other than
4396    // java/lang/Object, or an array — `String[] v = obj` needs the
4397    // cast exactly like `String v = obj`). The J arm pattern-matches
4398    // directly (no erased clone); only the rare G form erases.
4399    let specific_ref = |ty: &TypeRef| -> Option<TypeRef> {
4400        match ty {
4401            TypeRef::J(JavaType::Object(c)) if c.as_ref() != "java/lang/Object" => {
4402                Some(ty.clone())
4403            }
4404            TypeRef::J(JavaType::Array(_)) => Some(ty.clone()),
4405            _ => match ty.erased() {
4406                JavaType::Object(c) if c.as_ref() != "java/lang/Object" => Some(ty.clone()),
4407                JavaType::Array(_) => Some(ty.clone()),
4408                _ => None,
4409            },
4410        }
4411    };
4412    let non_cast_shape = |e: &Expr| -> bool {
4413        !matches!(e, Expr::Const(_) | Expr::Cast { .. } | Expr::InstanceOf { .. })
4414    };
4415    // Downcast witness: a SPECIFIC-ref value whose static type does not
4416    // fit the specific-ref target (`v9($1) = v7` with v7:
4417    // ContinuationImpl — weibo coroutine prologue ×282). The dex move
4418    // was verifier-proven against the target type, so the cast states
4419    // the bytecode truth; an actually-unconvertible static pair (both
4420    // classes, unrelated — the value view conflated) rides the
4421    // emitter's (Object) relay and still compiles. Framework types the
4422    // pool cannot order take the cast too: a downcast renders directly,
4423    // an unrelated pair relays.
4424    let needs_downcast = |vty: &JavaType, tt: &JavaType| -> bool {
4425        let (JavaType::Object(vn), JavaType::Object(tn)) = (vty, tt) else {
4426            return false;
4427        };
4428        if vn.as_ref() == "java/lang/Object" || tn.as_ref() == "java/lang/Object" {
4429            return false;
4430        }
4431        vn != tn && !pool.is_subtype(vn, tn)
4432    };
4433    // Combined decision on a materialized value type.
4434    let wants_cast = |e: &Expr, vty: &JavaType, tt: &JavaType| -> bool {
4435        non_cast_shape(e) && (is_top_object_ty(vty) || needs_downcast(vty, tt))
4436    };
4437    walk_mut_deep(body, &mut |st| match st {
4438        Stmt::ExprStmt(Expr::Assign {
4439            target,
4440            value,
4441            op: AssignOp::Plain,
4442            ..
4443        }) => {
4444            // Decide on materialized erased types (one per side); the
4445            // old code cloned the whole target TypeRef per statement
4446            // before knowing whether a cast fires at all.
4447            let tgt_erased: JavaType;
4448            let tgt_ref: Option<&TypeRef>;
4449            match &**target {
4450                Expr::Local { var, .. } => {
4451                    let ty = &vt.var(*var).ty;
4452                    tgt_erased = ty.erased();
4453                    tgt_ref = Some(ty);
4454                }
4455                Expr::Field { ty, .. } => {
4456                    tgt_erased = ty.erased();
4457                    tgt_ref = Some(ty);
4458                }
4459                // Array-element store `arr[i] = value`: the target type is
4460                // the ELEMENT type (Object→element assignments failed
4461                // "Object无法转换为String", weixin ×~150 — see history).
4462                Expr::ArrayIndex { array, .. } => {
4463                    let aty = match &**array {
4464                        Expr::Local { var, .. } => vt.var(*var).ty.erased(),
4465                        other => other.type_ref().erased(),
4466                    };
4467                    match aty {
4468                        JavaType::Array(inner) => {
4469                            tgt_erased = *inner;
4470                            tgt_ref = None;
4471                        }
4472                        _ => return,
4473                    }
4474                }
4475                _ => return,
4476            }
4477            // Specific check: the J-arm patterns fall through without
4478            // erasing; the array-element arm (no TypeRef to borrow)
4479            // re-materializes the erased form.
4480            let specific = match tgt_ref {
4481                Some(tr) => specific_ref(tr),
4482                None => specific_ref(&TypeRef::J(tgt_erased.clone())),
4483            };
4484            let Some(t) = specific else {
4485                return;
4486            };
4487            let vty = value_ty(value);
4488            if wants_cast(value, &vty, &tgt_erased) {
4489                let v = std::mem::replace(value, Box::new(Expr::This));
4490                **value = Expr::Cast { ty: t, e: v };
4491            }
4492        }
4493        Stmt::LocalDef { var, init: Some(value), .. } => {
4494            if let Some(t) = specific_ref(&vt.var(*var).ty) {
4495                let tt = vt.var(*var).ty.erased();
4496                let vty = value_ty(value);
4497                if wants_cast(value, &vty, &tt) {
4498                    let v = std::mem::replace(value, Expr::This);
4499                    *value = Expr::Cast {
4500                        ty: t,
4501                        e: Box::new(v),
4502                    };
4503                }
4504            }
4505        }
4506        Stmt::Return(Some(value)) => {
4507            if let Some(t) = specific_ref(&TypeRef::J(ret.clone())) {
4508                let vty = value_ty(value);
4509                if wants_cast(value, &vty, ret) {
4510                    let v = std::mem::replace(value, Expr::This);
4511                    *value = Expr::Cast { ty: t, e: Box::new(v) };
4512                }
4513            }
4514        }
4515        _ => {}
4516    });
4517    // Descriptor-exact formal casts at CALL args: the dex already
4518    // resolved the target method, so casting a top-Object actual to
4519    // its declared formal cannot shift overload resolution (the r57
4520    // args lesson applies to GUESSED casts, not descriptor-exact
4521    // ones). `"source_type:".concat(obj2)`, `new my0.j(obj2, ..)` —
4522    // Object无法转换为String family.
4523    walk_stmt_exprs(body, &mut |e| {
4524        deep_rewrite(e, &mut |x| {
4525            let (args, arg_tys): (&mut Vec<Expr>, &[JavaType]) = match x {
4526                Expr::Method { desc, args, is_dynamic: false, .. } => {
4527                    (args, desc.args.as_slice())
4528                }
4529                // Ctor calls are their own variant — the dex resolved the
4530                // exact <init>, so formal casts are just as safe here
4531                // (`new r(obj)` — Object无法转换为r family).
4532                Expr::New { arg_tys, args, .. } => (args, arg_tys.as_slice()),
4533                _ => return,
4534            };
4535            for (i, a) in args.iter_mut().enumerate() {
4536                let Some(formal) = arg_tys.get(i) else {
4537                    continue;
4538                };
4539                let Some(t) = specific_ref(&TypeRef::J(formal.clone())) else {
4540                    continue;
4541                };
4542                let aty = value_ty(a);
4543                if !wants_cast(a, &aty, formal) {
4544                    continue;
4545                }
4546                let v = std::mem::replace(a, Expr::Const(ConstVal::Null));
4547                *a = Expr::Cast { ty: t, e: Box::new(v) };
4548            }
4549        });
4550    });
4551}
4552
4553/// Numeric narrowing/widening mismatches at Assign / LocalDef / Return
4554/// positions get an explicit cast: dex int-to-byte / long-to-int flows
4555/// are implicit in the register machine, Java rejects them ("从long
4556/// 转换到int可能会有损失" — okio Buffer ×321 weibo lines). Casts at
4557/// these three positions are descriptor-faithful and cannot shift
4558/// overload resolution (the r57 args-position lesson). Boolean/numeric
4559/// mixes are NOT castable in Java and stay with the generation-split
4560/// machinery.
4561pub fn fix_primitive_assign_casts(vt: &VarTable, body: &mut Stmt, ret_ty: &JavaType) {
4562    fn numlike(t: &JavaType) -> bool {
4563        matches!(
4564            t,
4565            JavaType::Byte
4566                | JavaType::Short
4567                | JavaType::Int
4568                | JavaType::Char
4569                | JavaType::Long
4570                | JavaType::Float
4571                | JavaType::Double
4572        )
4573    }
4574    fn val_ty(e: &Expr, vt: &VarTable) -> JavaType {
4575        match e {
4576            Expr::Local { var, .. } if (*var as usize) < vt.vars.len() => {
4577                vt.var(*var).ty.erased()
4578            }
4579            other => other.type_ref().erased(),
4580        }
4581    }
4582    fn wrap(ty: &JavaType, value: &mut Expr) {
4583        if matches!(value, Expr::Cast { .. }) {
4584            return;
4585        }
4586        if val_ty_static(value) == *ty {
4587            return;
4588        }
4589        let taken = std::mem::replace(value, Expr::Const(ConstVal::Null));
4590        *value = Expr::Cast {
4591            ty: TypeRef::J(ty.clone()),
4592            e: Box::new(taken),
4593        };
4594    }
4595    fn val_ty_static(e: &Expr) -> JavaType {
4596        e.type_ref().erased()
4597    }
4598    /// Target-typed coercion for one assignment value. Numeric mixes
4599    /// cast; boolean mixes CONVERT (Java has no cast between boolean
4600    /// and any numeric): `d = b` → `(double) (b ? 1 : 0)`, `b = i` →
4601    /// `i != 0` — mirrors fix_primitive_arg_bridges' formal-side arms.
4602    /// The generation split cannot own these: at an if-join the
4603    /// one-sided split is undone (rename_gen_back) and the incompatible
4604    /// assign re-exposes ("boolean无法转换为double", weixin s9/t01-f).
4605    fn coerce(tt: &JavaType, value: &mut Expr, vt: &VarTable) {
4606        // Raw const bits against a floating target are the FLOAT value,
4607        // not an int to widen (`float f = <0x41000000 bits>` is 8.0f;
4608        // the (float) cast path would silently store 1.09e9f). Mirror of
4609        // fix_primitive_arg_bridges' descriptor-formal reinterpretation.
4610        match (&*value, tt) {
4611            (Expr::Const(ConstVal::Int(b)), JavaType::Float) => {
4612                *value = Expr::Const(ConstVal::Float(f32::from_bits(*b as u32)));
4613                return;
4614            }
4615            (Expr::Const(ConstVal::Long(b)), JavaType::Double) => {
4616                *value = Expr::Const(ConstVal::Double(f64::from_bits(*b as u64)));
4617                return;
4618            }
4619            (Expr::Const(ConstVal::Float(f)), JavaType::Int) => {
4620                *value = Expr::Const(ConstVal::Int(f.to_bits() as i32));
4621                return;
4622            }
4623            (Expr::Const(ConstVal::Double(d)), JavaType::Long) => {
4624                *value = Expr::Const(ConstVal::Long(d.to_bits() as i64));
4625                return;
4626            }
4627            _ => {}
4628        }
4629        let vt_val = val_ty(value, vt);
4630        let tt_bool = matches!(tt, JavaType::Boolean);
4631        let val_bool = matches!(vt_val, JavaType::Boolean);
4632        if tt_bool && !val_bool && numlike(&vt_val) {
4633            if matches!(
4634                value,
4635                Expr::Const(_)
4636                    | Expr::Bin { op: BinOp::Eq | BinOp::Ne, .. }
4637                    | Expr::Un { op: UnOp::Not, .. }
4638            ) {
4639                // Bare 0/1 constants render as false/true through the
4640                // emitter's bool coercion — wrapping them in `!= 0`
4641                // produced `0 != 0` noise the value-diamond fold then
4642                // had to re-interpret.
4643                return;
4644            }
4645            let taken = std::mem::replace(value, Expr::Const(ConstVal::Null));
4646            *value = Expr::Bin {
4647                op: BinOp::Ne,
4648                l: Box::new(taken),
4649                r: Box::new(Expr::Const(ConstVal::Int(0))),
4650                ty: Some(TypeRef::J(JavaType::Boolean)),
4651            };
4652        } else if !tt_bool && val_bool && numlike(tt) {
4653            if matches!(value, Expr::Cast { .. }) {
4654                return;
4655            }
4656            let taken = std::mem::replace(value, Expr::Const(ConstVal::Null));
4657            *value = Expr::Cast {
4658                ty: TypeRef::J(tt.clone()),
4659                e: Box::new(Expr::Cond {
4660                    c: Box::new(taken),
4661                    t: Box::new(Expr::Const(ConstVal::Int(1))),
4662                    f: Box::new(Expr::Const(ConstVal::Int(0))),
4663                }),
4664            };
4665        } else if numlike(tt) && numlike(&vt_val) {
4666            wrap(tt, value);
4667        }
4668    }
4669    walk_mut_deep(body, &mut |st| match st {
4670        Stmt::ExprStmt(Expr::Assign {
4671            target,
4672            op: AssignOp::Plain,
4673            value,
4674        }) => {
4675            let tt = match &**target {
4676                Expr::Local { var, .. } if (*var as usize) < vt.vars.len() => {
4677                    vt.var(*var).ty.erased()
4678                }
4679                Expr::Field { ty, .. } => ty.erased(),
4680                // Array store: the element type is the target type
4681                // (`sFormatStr[i] = v7` into char[] — "从int转换到char
4682                // 可能会有损失", weibo TimeUtils).
4683                Expr::ArrayIndex { array, .. } => match array.type_ref().erased() {
4684                    JavaType::Array(inner) => *inner,
4685                    _ => return,
4686                },
4687                _ => return,
4688            };
4689            coerce(&tt, value, vt);
4690        }
4691        Stmt::LocalDef {
4692            var,
4693            init: Some(value),
4694            ..
4695        } => {
4696            if (*var as usize) >= vt.vars.len() {
4697                return;
4698            }
4699            let tt = vt.var(*var).ty.erased();
4700            coerce(&tt, value, vt);
4701        }
4702        Stmt::Return(Some(e)) => {
4703            if !numlike(ret_ty) || !numlike(&val_ty(e, vt)) {
4704                return;
4705            }
4706            wrap(ret_ty, e);
4707        }
4708        _ => {}
4709    });
4710}
4711
4712/// Shadowed-ancestor field witness (the upcast sibling of
4713/// fix_field_owner_downcasts).
4714///
4715/// Java resolves a field access on the RECEIVER'S STATIC TYPE downward:
4716/// when the dex writes an ancestor's field (`iput … Fragment.A02:I`)
4717/// but a class between the receiver's static type and the declaring
4718/// ancestor declares a field with the SAME rendered name (`ImageView
4719/// A02`), the plain `this.A02` silently binds to the subclass field —
4720/// `int无法转换为ImageView` when the types differ, and a wrong-field
4721/// read/write when they don't (WhatsApp ActivitySheetFragment writes
4722/// the layout resid into the ancestor int field; its own A02 is an
4723/// ImageView — 1,216 assignment-shape sites in the WA tree). jadx
4724/// renders `super.A02`; the IR has no super-field form, so witness
4725/// with the always-legal upcast `((Fragment) this).A02` — field lookup
4726/// on a cast expression is static, landing on the ancestor at any
4727/// chain depth. The receiver may still be the typed this-Local at this
4728/// pipeline point (the This rewrite comes later), so the compile-time
4729/// type comes from the owner expr / var table, mirroring
4730/// fix_field_owner_downcasts' owner_internal. Fires ONLY on a real
4731/// same-display shadow; unshadowed ancestor refs inherit through
4732/// `this.X` and must not churn. Same-raw-name lookups only: a shadow
4733/// whose raw name differs but display collides is beyond the pool
4734/// lookup keyed by name.
4735pub fn fix_shadowed_super_fields(
4736    body: &mut Stmt,
4737    vt: &VarTable,
4738    pool: &DexPool,
4739    class_name: &str,
4740) {
4741    /// Rendered name of `(cls, raw field name)`: the registry display
4742    /// when the class declares the field, else the raw name. None when
4743    /// the class is not pool-materialized or has no such field.
4744    fn display(pool: &DexPool, cls: &str, name: &str) -> Option<String> {
4745        let pc = pool.get(cls)?;
4746        let f = pc
4747            .static_fields
4748            .iter()
4749            .chain(pc.instance_fields.iter())
4750            .find(|f| f.name.as_ref() == name)?;
4751        Some(
4752            jdc_core::rename::field_display(cls, name, &f.desc)
4753                .map(|d| d.to_string())
4754                .unwrap_or_else(|| name.to_string()),
4755        )
4756    }
4757    fn owner_ct(
4758        o: &Option<Box<Expr>>,
4759        vt: &VarTable,
4760        class_name: &str,
4761    ) -> Option<std::sync::Arc<str>> {
4762        match o.as_deref() {
4763            None | Some(Expr::This) => Some(std::sync::Arc::from(class_name)),
4764            Some(Expr::Local { var, .. }) => match vt.var(*var).ty.erased() {
4765                JavaType::Object(n) => Some(n),
4766                _ => None,
4767            },
4768            Some(other) => match other.type_ref().erased() {
4769                JavaType::Object(n) => Some(n),
4770                _ => None,
4771            },
4772        }
4773    }
4774    walk_stmt_exprs(body, &mut |e| {
4775        deep_rewrite(e, &mut |x| {
4776            let Expr::Field {
4777                owner,
4778                cls,
4779                name,
4780                is_static: false,
4781                ..
4782            } = x
4783            else {
4784                return;
4785            };
4786            let Some(ot) = owner_ct(owner, vt, class_name) else {
4787                return;
4788            };
4789            if cls.as_ref() == ot.as_ref() {
4790                return;
4791            }
4792            let Some(ref_display) = display(pool, cls, name) else {
4793                return;
4794            };
4795            // Walk from the receiver's compile-time type up to (not
4796            // including) the declaring ancestor: any same-display
4797            // declaration on the way shadows the ref.
4798            let mut cur = ot.to_string();
4799            let mut shadowed = false;
4800            let mut found = false;
4801            for _ in 0..64 {
4802                if display(pool, &cur, name).as_deref() == Some(ref_display.as_str()) {
4803                    shadowed = true;
4804                }
4805                let Some(pc) = pool.get(&cur) else {
4806                    return;
4807                };
4808                let Some(sup) = pc.super_name.clone() else {
4809                    return;
4810                };
4811                if sup.as_str() == cls.as_ref() {
4812                    found = true;
4813                    break;
4814                }
4815                cur = sup;
4816            }
4817            if !found || !shadowed {
4818                return;
4819            }
4820            let ty = TypeRef::J(JavaType::Object(cls.clone()));
4821            match owner {
4822                Some(o) => {
4823                    if let Expr::Cast { ty: ct, .. } = &mut **o {
4824                        *ct = ty;
4825                    } else {
4826                        let taken = std::mem::replace(&mut **o, Expr::This);
4827                        **o = Expr::Cast {
4828                            ty,
4829                            e: Box::new(taken),
4830                        };
4831                    }
4832                }
4833                None => {
4834                    *owner = Some(Box::new(Expr::Cast {
4835                        ty,
4836                        e: Box::new(Expr::This),
4837                    }))
4838                }
4839            }
4840        });
4841    });
4842}
4843
4844/// Subclass-field downcast witness + assign-cast narrowing.
4845///
4846/// R8 pushes fields DOWN into leaf classes (weibo: coroutine `label`
4847/// lives on each `Foo$bar$1` continuation, NOT on the rendered
4848/// BaseContinuationImpl/ContinuationImpl supers) while a check-cast in
4849/// the same flow types the receiver as the SUPERCLASS. The dex field
4850/// ref names the leaf (`$1->label` — verifier-legal through the
4851/// instance-of branch type), but Java resolves `((ContinuationImpl) c)
4852/// .label` against ContinuationImpl — "找不到符号 变量 label" (weibo
4853/// ×420). When the field's dex owner is a strict subtype of the
4854/// receiver's static type and the receiver's hierarchy does NOT
4855/// declare the name, re-target the receiver cast to the owner (the
4856/// runtime object IS the owner type — the verifier proved it).
4857///
4858/// Second rule: a `(T) e` value assigned into an S-typed target where
4859/// S <: T strictly ("ContinuationImpl无法转换为$reportWhenComplete$1")
4860/// narrows the cast to S — same verifier guarantee, and an assignment
4861/// position has no overload resolution to perturb.
4862pub fn fix_field_owner_downcasts(body: &mut Stmt, vt: &VarTable, pool: &DexPool) {
4863    fn owner_internal(o: &Expr, vt: &VarTable) -> Option<std::sync::Arc<str>> {
4864        let t = match o {
4865            Expr::Local { var, .. } => vt.var(*var).ty.erased(),
4866            other => other.type_ref().erased(),
4867        };
4868        match t {
4869            JavaType::Object(n) => Some(n),
4870            _ => None,
4871        }
4872    }
4873    fn declares_up(pool: &DexPool, class: &str, name: &str) -> bool {
4874        let mut cur = Some(class.to_string());
4875        let mut hops = 0;
4876        while let Some(c) = cur {
4877            hops += 1;
4878            if hops > 64 {
4879                return false;
4880            }
4881            let Some(pc) = pool.get(&c) else { return false };
4882            if pc
4883                .instance_fields
4884                .iter()
4885                .chain(pc.static_fields.iter())
4886                .any(|f| f.name.as_ref() == name)
4887            {
4888                return true;
4889            }
4890            cur = pc.super_name.clone();
4891        }
4892        false
4893    }
4894    walk_stmt_exprs(body, &mut |e| {
4895        deep_rewrite(e, &mut |x| {
4896            if let Expr::Field {
4897                owner: Some(o),
4898                cls,
4899                name,
4900                is_static: false,
4901                ..
4902            } = x
4903            {
4904                let Some(ot) = owner_internal(o, vt) else {
4905                    return;
4906                };
4907                if ot.as_ref() == cls.as_ref() || !pool.is_subtype(cls, &ot) {
4908                    return;
4909                }
4910                if declares_up(pool, &ot, name) || !declares_up(pool, cls, name) {
4911                    return;
4912                }
4913                let ty = TypeRef::J(JavaType::Object(cls.clone()));
4914                if let Expr::Cast { ty: ct, .. } = &mut **o {
4915                    *ct = ty;
4916                } else {
4917                    let taken = std::mem::replace(&mut **o, Expr::This);
4918                    **o = Expr::Cast { ty, e: Box::new(taken) };
4919                }
4920            }
4921            // Rule 2: `(T) e` into a strict-subtype target narrows to S.
4922            let (tgt_ty, value) = match x {
4923                Expr::Assign { target, op: AssignOp::Plain, value } => {
4924                    let t = match &**target {
4925                        Expr::Local { var, .. } => vt.var(*var).ty.erased(),
4926                        Expr::Field { ty, .. } => ty.erased(),
4927                        _ => return,
4928                    };
4929                    (t, value)
4930                }
4931                _ => return,
4932            };
4933            let JavaType::Object(sn) = tgt_ty else { return };
4934            if let Expr::Cast { ty, .. } = &mut **value {
4935                if let TypeRef::J(JavaType::Object(tn)) = ty {
4936                    if tn.as_ref() != sn.as_ref() && pool.is_subtype(&sn, tn) {
4937                        *ty = TypeRef::J(JavaType::Object(sn.clone()));
4938                    }
4939                }
4940            }
4941        });
4942    });
4943    // LocalDef inits: same narrowing against the declared var type.
4944    walk_mut_deep(body, &mut |st| {
4945        if let Stmt::LocalDef { var, init: Some(value), .. } = st {
4946            let tt = if (*var as usize) < vt.vars.len() {
4947                vt.vars[*var as usize].ty.erased()
4948            } else {
4949                return;
4950            };
4951            let JavaType::Object(sn) = tt else { return };
4952            if let Expr::Cast { ty, .. } = value {
4953                if let TypeRef::J(JavaType::Object(tn)) = ty {
4954                    if tn.as_ref() != sn.as_ref() && pool.is_subtype(&sn, tn) {
4955                        *ty = TypeRef::J(JavaType::Object(sn.clone()));
4956                    }
4957                }
4958            }
4959        }
4960    });
4961}
4962
4963/// `==`/`!=` between UNRELATED class types is a compile error in Java
4964/// ("不可比较的类型: f0和a") while dex if-eqObj happily compares any two
4965/// references (weixin fn1/w1 ×298 files). One side takes an `(Object)`
4966/// witness cast — legal against anything. Interfaces and subtype-related
4967/// pairs are already comparable and stay untouched; java/lang/Object and
4968/// arrays-of it likewise.
4969pub fn fix_incomparable_equality(body: &mut Stmt, vt: &VarTable, pool: &crate::DexPool) {
4970    fn side_ty(e: &Expr, vt: &VarTable) -> JavaType {
4971        match e {
4972            Expr::Local { var, .. } if (*var as usize) < vt.vars.len() => {
4973                vt.var(*var).ty.erased()
4974            }
4975            other => other.type_ref().erased(),
4976        }
4977    }
4978    let incomparable = |a: &JavaType, b: &JavaType| -> bool {
4979        use JavaType::{Array, Object};
4980        match (a, b) {
4981            (Object(x), Object(y)) => {
4982                if x == y || x.as_ref() == "java/lang/Object" || y.as_ref() == "java/lang/Object"
4983                {
4984                    return false;
4985                }
4986                let xi = pool.get(x).map(|c| c.is_interface()).unwrap_or(true);
4987                let yi = pool.get(y).map(|c| c.is_interface()).unwrap_or(true);
4988                // Unknown classes (framework, not in pool): a cast is
4989                // harmless but usually unnecessary — treat as comparable
4990                // to stay conservative.
4991                if xi || yi {
4992                    return false;
4993                }
4994                !pool.is_subtype(x, y) && !pool.is_subtype(y, x)
4995            }
4996            (Array(_), Object(y)) | (Object(y), Array(_)) => {
4997                y.as_ref() != "java/lang/Object"
4998                    && y.as_ref() != "java/lang/Cloneable"
4999                    && y.as_ref() != "java/io/Serializable"
5000                    && !pool.get(y).map(|c| c.is_interface()).unwrap_or(false)
5001            }
5002            _ => false,
5003        }
5004    };
5005    let is_ref = |t: &JavaType| matches!(t, JavaType::Object(_) | JavaType::Array(_));
5006    walk_stmt_exprs(body, &mut |e| {
5007        deep_rewrite(e, &mut |x| {
5008            if let Expr::Bin {
5009                op: BinOp::Eq | BinOp::Ne,
5010                l,
5011                r,
5012                ..
5013            } = x
5014            {
5015                let lt = side_ty(l, vt);
5016                let rt = side_ty(r, vt);
5017                // `obj != 0` → `obj != null` with FINAL types: the
5018                // lift-time null_side_rewrite ran before infer_types
5019                // (its obj table saw the pre-inference int), leaving
5020                // `sQLiteClosable != 0` — "二元运算符 '!=' 的操作数
5021                // 类型错误" ×700 weixin.
5022                if is_ref(&lt) && matches!(&**r, Expr::Const(ConstVal::Int(0))) {
5023                    **r = Expr::Const(ConstVal::Null);
5024                    return;
5025                }
5026                if is_ref(&rt) && matches!(&**l, Expr::Const(ConstVal::Int(0))) {
5027                    **l = Expr::Const(ConstVal::Null);
5028                    return;
5029                }
5030                if incomparable(&lt, &rt) {
5031                    let taken = std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
5032                    **l = Expr::Cast {
5033                        ty: TypeRef::J(JavaType::Object("java/lang/Object".into())),
5034                        e: taken,
5035                    };
5036                }
5037            }
5038        });
5039    });
5040}
5041
5042/// Primitive bridges at CALL/CTOR argument positions. Dex slots are
5043/// untyped category-1 integers: the verifier happily passes a Z-slot to
5044/// a `(B)` formal (weibo's Meituan-Robust hotpatch boilerplate boxes
5045/// boolean params via `new Byte(zreg)` — 4.2k+ `对于Byte(boolean),
5046/// 找不到合适的构造器`), and obfuscated/Kotlin code passes 0/1 slots to
5047/// `(Z)` formals (`int无法转换为boolean` at call sites). Java source
5048/// cannot express either — bridge at the IR level:
5049///   Boolean actual → numeric formal:  `(byte) (b ? 1 : 0)`
5050///   numeric actual → Boolean formal:  `x != 0`
5051/// The target method is DESCRIPTOR-bound (bytecode method_ref), so a
5052/// cast/compare to the exact formal type cannot flip overload
5053/// resolution the way r57's inferred object casts did — the rendered
5054/// call selects the same signature the dex named.
5055pub fn fix_primitive_arg_bridges(body: &mut Stmt, vt: &VarTable, pool: &crate::DexPool) {
5056    fn val_ty(e: &Expr, vt: &VarTable) -> JavaType {
5057        match e {
5058            Expr::Local { var, .. } => vt.var(*var).ty.erased(),
5059            other => other.type_ref().erased(),
5060        }
5061    }
5062    #[allow(clippy::type_complexity)]
5063    let is_bool = |t: &JavaType| matches!(t, JavaType::Boolean);
5064    let is_num = |t: &JavaType| {
5065        matches!(
5066            t,
5067            JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
5068                | JavaType::Long | JavaType::Float | JavaType::Double
5069        )
5070    };
5071    // BORROWED-arg form: returns the bridged replacement without
5072    // consuming the original (a `mem::replace` + `Option` dance dropped
5073    // the argument on the None path — every Boolean→Boolean arg became
5074    // a literal `null`: 61k 引用不明确 in one battery run).
5075    fn bridge(actual: &JavaType, formal: &JavaType, arg: &Expr) -> Option<Expr> {
5076        // A dex const carries the CALLEE's BITS: `const v, #0x41000000`
5077        // feeding a float formal IS 8.0f. Rendering the raw int literal
5078        // (a) mis-selects overloads — a competing `a(Context,int):void`
5079        // beats the int→float-widened `a(Context,float):int` in javac's
5080        // phase order ("不兼容的类型: void无法转换为int", weibo ju.a
5081        // ×1,259) — and (b) corrupts the VALUE (widening turns the bits
5082        // into 1.09e9f). The descriptor formal is dex ground truth:
5083        // reinterpret. Symmetric for the float-const-into-int-formal
5084        // direction (caller's bits, callee's reading).
5085        match (arg, formal) {
5086            (Expr::Const(ConstVal::Int(b)), JavaType::Float) => {
5087                return Some(Expr::Const(ConstVal::Float(f32::from_bits(*b as u32))));
5088            }
5089            (Expr::Const(ConstVal::Long(b)), JavaType::Double) => {
5090                return Some(Expr::Const(ConstVal::Double(f64::from_bits(*b as u64))));
5091            }
5092            (Expr::Const(ConstVal::Float(f)), JavaType::Int) => {
5093                return Some(Expr::Const(ConstVal::Int(f.to_bits() as i32)));
5094            }
5095            (Expr::Const(ConstVal::Double(d)), JavaType::Long) => {
5096                return Some(Expr::Const(ConstVal::Long(d.to_bits() as i64)));
5097            }
5098            _ => {}
5099        }
5100        if matches!(actual, JavaType::Boolean)
5101            && matches!(
5102                formal,
5103                JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
5104                    | JavaType::Long | JavaType::Float | JavaType::Double
5105            )
5106        {
5107            Some(Expr::Cast {
5108                ty: TypeRef::J(formal.clone()),
5109                e: Box::new(Expr::Cond {
5110                    c: Box::new(arg.clone()),
5111                    t: Box::new(Expr::Const(ConstVal::Int(1))),
5112                    f: Box::new(Expr::Const(ConstVal::Int(0))),
5113                }),
5114            })
5115        } else if matches!(formal, JavaType::Boolean)
5116            && matches!(
5117                actual,
5118                JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
5119                    | JavaType::Long | JavaType::Float | JavaType::Double
5120            )
5121        {
5122            Some(Expr::Bin {
5123                op: BinOp::Ne,
5124                l: Box::new(arg.clone()),
5125                r: Box::new(Expr::Const(ConstVal::Int(0))),
5126                ty: Some(TypeRef::J(JavaType::Boolean)),
5127            })
5128        } else if matches!(
5129            actual,
5130            JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
5131                | JavaType::Long | JavaType::Float | JavaType::Double
5132        ) && matches!(
5133            formal,
5134            JavaType::Byte | JavaType::Short | JavaType::Int | JavaType::Char
5135                | JavaType::Long | JavaType::Float | JavaType::Double
5136        ) && !implicit_widening(actual, formal)
5137        {
5138            // Numeric NARROWING into a formal (int arg → char formal):
5139            // javac rejects the lossy conversion, or — worse when the
5140            // class has no wider overload — silently resolves a
5141            // DIFFERENT overload than the dex descriptor names
5142            // (SpannableStringBuilder.append(int) doesn't exist;
5143            // `append(char)` is "从int转换到char可能会有损失", weibo ×76).
5144            // The descriptor formal is the dex ground truth; casting to
5145            // it pins the overload the bytecode actually invoked.
5146            Some(Expr::Cast {
5147                ty: TypeRef::J(formal.clone()),
5148                e: Box::new(arg.clone()),
5149            })
5150        } else {
5151            None
5152        }
5153    }
5154    /// A bare `null` argument against a SPECIFIC reference formal takes
5155    /// the descriptor cast: `this(context, null)` matching both
5156    /// Builder(Context,Notification) and Builder(Context,String) is
5157    /// "对Builder的引用不明确" (weibo NotificationCompat; protobuf
5158    /// `new FieldSet.Builder(null)` ×54). The descriptor names the
5159    /// exact invoked ctor, so the cast is dex ground truth — the
5160    /// descriptor-exact doctrine, not a guessed overload shift. Bare
5161    /// null stays for Object formals (casting those would add noise
5162    /// without resolving anything javac could not already pick).
5163    /// Framework classes javac resolves through android.jar even when
5164    /// the dex pool lacks them — the embedded API database answers
5165    /// precisely (the hand-written prefix list it replaced missed
5166    /// everything outside the guessed roots and over-trusted within
5167    /// them; removed-API families like org.apache.http are in the DB
5168    /// too, matching what a legacy-jar classpath resolves).
5169    fn fw_resolvable(n: &str) -> bool {
5170        crate::fwdb::exists(n)
5171    }
5172    fn null_arg_cast(a: &mut Expr, f: &JavaType, pool: &DexPool) -> bool {
5173        if !matches!(&*a, Expr::Const(ConstVal::Null)) {
5174            return false;
5175        }
5176        if let JavaType::Object(n) = f {
5177            // Cast only to RESOLVABLE types: a phantom formal (R8 kept
5178            // the ref, dropped the class — androidx.window.sidecar)
5179            // would turn a compiling bare `null` into a fresh
5180            // cannot-find. Pool classes render into the output;
5181            // java/javax/android/dalvik resolve through android.jar.
5182            let resolvable = pool.get(n).is_some() || fw_resolvable(n);
5183            if n.as_ref() != "java/lang/Object" && resolvable {
5184                *a = Expr::Cast {
5185                    ty: TypeRef::J(f.clone()),
5186                    e: Box::new(Expr::Const(ConstVal::Null)),
5187                };
5188                return true;
5189            }
5190        } else if let JavaType::Array(_) = f {
5191            // Descriptor-exact ARRAY cast: the dex named this exact
5192            // overload; a bare null beside sibling overloads is javac-
5193            // ambiguous (`a(String,b2)` + `a(String,String[])` both match
5194            // — weibo org/a/c/b, 对a的引用不明确 family). Same phantom
5195            // gate on the element class: casting to a phantom-element
5196            // array would trade the ambiguity for a cannot-find.
5197            let mut e: &JavaType = f;
5198            while let JavaType::Array(inner) = e {
5199                e = &**inner;
5200            }
5201            let resolvable = match e {
5202                JavaType::Object(n) => pool.get(n).is_some() || fw_resolvable(n),
5203                _ => true, // primitive-element array
5204            };
5205            if resolvable {
5206                *a = Expr::Cast {
5207                    ty: TypeRef::J(f.clone()),
5208                    e: Box::new(Expr::Const(ConstVal::Null)),
5209                };
5210                return true;
5211            }
5212        }
5213        // Bare null deliberately stays (Object formal / phantom type):
5214        // the caller's ambiguity pin may still act on it.
5215        false
5216    }
5217    /// JLS 5.1.2 widening primitive conversion (char joins the chain
5218    /// only at int; byte/short do NOT widen to char).
5219    fn implicit_widening(a: &JavaType, f: &JavaType) -> bool {
5220        use JavaType::*;
5221        match (a, f) {
5222            (Byte, Short) | (Byte, Int) | (Byte, Long) | (Byte, Float) | (Byte, Double) => true,
5223            (Short, Int) | (Short, Long) | (Short, Float) | (Short, Double) => true,
5224            (Char, Int) | (Char, Long) | (Char, Float) | (Char, Double) => true,
5225            (Int, Long) | (Int, Float) | (Int, Double) => true,
5226            (Long, Float) | (Long, Double) => true,
5227            (Float, Double) => true,
5228            (x, y) => x == y,
5229        }
5230    }
5231    // Does the owner class declare ANOTHER method with the same name
5232    // and arity but a different signature? Then javac's most-specific
5233    // overload resolution can steal the call from the dex descriptor's
5234    // target (weibo 对a的引用不明确 ×391: `c.a(null)` against
5235    // a(ExecuteResult)/a(Exception)/a(Object) — the descriptor names
5236    // a(Object), but bare null leaves the two specific overloads
5237    // ambiguously most-specific). Framework owners are not in the
5238    // pool — conservatively report no competition.
5239    let overload_competes = |cls: &str, name: &str, args: &[JavaType]| -> bool {
5240        if let Some(c) = pool.get(cls) {
5241            return c.all_methods().any(|m| {
5242                &*m.name == name
5243                    // Bridge/synthetic siblings are compiler artifacts
5244                    // resolving to the SAME target — counting them as
5245                    // competitors cast enum compareTo args to the erased
5246                    // formal the rendered specialization no longer
5247                    // accepts (Enum无法转换为LogLevel ×2,079 reqable).
5248                    && m.access & (crate::access::ACC_BRIDGE | crate::access::ACC_SYNTHETIC) == 0
5249                    && m.parsed_desc()
5250                        .is_some_and(|d| d.args.len() == args.len() && d.args != args)
5251            });
5252        }
5253        // Framework owner: NOT enumerable for pin purposes — the
5254        // full-DB enumeration experiment (step-5) measured +532 battery
5255        // with ZERO ambiguous gain: a subtype arg against a concrete
5256        // erased formal fires the cast even when the call was never
5257        // ambiguous, and bounded-generic formals (List<T> -> List,
5258        // Property<T,Float> erasures) make the cast break inference or
5259        // miss the rendered specialization. Framework subtype pins stay
5260        // on the measured allowlist below.
5261        let _ = args;
5262        false
5263    };
5264    // Ctors: formal types via the pool (unanimous across same-arity
5265    // overloads, else skip — ambiguity must not guess).
5266    let ctor_formals = |cls: &str, n: usize| -> Option<Vec<JavaType>> {
5267        let c = pool.get(cls)?;
5268        let mut out: Option<Vec<JavaType>> = None;
5269        for m in c.all_methods() {
5270            if &*m.name != "<init>" {
5271                continue;
5272            }
5273            let Some(d) = m.parsed_desc() else { continue };
5274            if d.args.len() != n {
5275                continue;
5276            }
5277            match &out {
5278                None => out = Some(d.args.clone()),
5279                Some(prev) if *prev != d.args => return None,
5280                Some(_) => {}
5281            }
5282        }
5283        out
5284    };
5285    walk_stmt_exprs(body, &mut |e| {
5286        deep_rewrite(e, &mut |x| match x {
5287            Expr::Method {
5288                cls,
5289                name,
5290                desc,
5291                args,
5292                is_static,
5293                ..
5294            } => {
5295                if args.len() != desc.args.len() {
5296                    return;
5297                }
5298                let _ = is_static;
5299                let mut competes: Option<bool> = None;
5300                for (a, f) in args.iter_mut().zip(desc.args.iter()) {
5301                    if null_arg_cast(a, f, pool) {
5302                        continue;
5303                    }
5304                    if matches!(a, Expr::Cast { .. }) {
5305                        continue;
5306                    }
5307                    // Descriptor-exact ambiguity pin: a bare null
5308                    // against an Object formal (null_arg_cast
5309                    // deliberately leaves those bare — safe only
5310                    // WITHOUT competing specific overloads) or a
5311                    // strict-subtype arg can match several declared
5312                    // overloads; casting to the descriptor formal
5313                    // leaves exactly the dex-invoked method
5314                    // applicable (JLS 15.12.2 — the descriptor is
5315                    // ground truth, so the cast pins resolution
5316                    // instead of shifting it).
5317                    let subtype_arg = match (&val_ty(a, vt), f) {
5318                        (JavaType::Object(an), JavaType::Object(fn_)) => {
5319                            // java/lang/Object is the root: ANY reference
5320                            // arg is a strict subtype of it — pool.is_subtype
5321                            // cannot see framework chains (String -> Object),
5322                            // so the Object formal gets the direct rule.
5323                            (an != fn_ && pool.is_subtype(an, fn_))
5324                                || (fn_.as_ref() == "java/lang/Object"
5325                                    && an.as_ref() != "java/lang/Object")
5326                        }
5327                        (JavaType::Array(_), JavaType::Object(fn_))
5328                            if fn_.as_ref() == "java/lang/Object" =>
5329                        {
5330                            true
5331                        }
5332                        _ => false,
5333                    };
5334                    // Bare null against java/lang/Object itself: the
5335                    // descriptor method exists (dex resolved it), so
5336                    // `(Object) null` is always legal and excludes every
5337                    // more-specific sibling overload from applicability.
5338                    // Pool owners take the competes gate (emitted
5339                    // declarations are raw, so the cast cannot disturb
5340                    // inference). FRAMEWORK owners only via allowlist of
5341                    // the classic non-generic null-ambiguous JDK methods
5342                    // (StringBuilder.append ×66 lark): a blanket cast
5343                    // breaks GENERIC framework methods whose descriptor
5344                    // formal is an ERASED type variable —
5345                    // `ofFloat((Object) null, View.ALPHA, v)` killed the
5346                    // T=View inference bare null provided (weibo +4).
5347                    let null_obj =
5348                        matches!(a, Expr::Const(ConstVal::Null)) && matches!(f, JavaType::Object(_));
5349                    let framework_owner = pool.get(cls.as_ref()).is_none();
5350                    let fw_null_ambiguous = framework_owner
5351                        && matches!(
5352                            (cls.as_ref(), name.as_ref()),
5353                            ("java/lang/StringBuilder", "append")
5354                                | ("java/lang/StringBuffer", "append")
5355                                | ("java/io/PrintStream", "println")
5356                                | ("java/io/PrintStream", "print")
5357                                | ("java/io/PrintWriter", "println")
5358                                | ("java/io/PrintWriter", "print")
5359                                | ("java/lang/String", "valueOf")
5360                        );
5361                    // Framework owners with famously overload-heavy
5362                    // methods take the subtype pin too: the arg's
5363                    // rendered type implements SEVERAL formal types
5364                    // (weibo putExtra ×41: floatMsgData is both
5365                    // Parcelable and Serializable; schedule/submit:
5366                    // Runnable-and-Callable workers) and the competes
5367                    // gate cannot enumerate framework overloads.
5368                    // Formal == java/lang/Object stays excluded — that
5369                    // is an erased type variable and the cast would
5370                    // kill generic inference (ofFloat lesson).
5371                    let fw_subtype_pinnable = framework_owner
5372                        && subtype_arg
5373                        && !matches!(f, JavaType::Object(fn_) if fn_.as_ref() == "java/lang/Object")
5374                        && matches!(
5375                            (cls.as_ref(), name.as_ref()),
5376                            ("android/content/Intent", "putExtra")
5377                                | ("java/util/concurrent/ScheduledExecutorService", "schedule")
5378                                | ("java/util/concurrent/ExecutorService", "submit")
5379                                | ("java/util/concurrent/ExecutorService", "invokeAll")
5380                        );
5381                    if fw_subtype_pinnable
5382                        || (subtype_arg
5383                            && *competes.get_or_insert_with(|| {
5384                                overload_competes(cls, name, &desc.args)
5385                            }))
5386                        || (null_obj
5387                            && (fw_null_ambiguous
5388                                || *competes.get_or_insert_with(|| {
5389                                    overload_competes(cls, name, &desc.args)
5390                                })))
5391                    {
5392                        let old =
5393                            std::mem::replace(a, Expr::Const(ConstVal::Null));
5394                        *a = Expr::Cast {
5395                            ty: TypeRef::J(f.clone()),
5396                            e: Box::new(old),
5397                        };
5398                        continue;
5399                    }
5400                    if !is_bool(&val_ty(a, vt)) && !is_num(&val_ty(a, vt)) {
5401                        continue;
5402                    }
5403                    if !is_bool(f) && !is_num(f) {
5404                        continue;
5405                    }
5406                    if let Some(b) = bridge(&val_ty(a, vt), f, a) {
5407                        *a = b;
5408                    }
5409                }
5410            }
5411            Expr::New { cls, args, arg_tys, .. } => {
5412                let formals_owned = if arg_tys.len() == args.len() && !arg_tys.is_empty() {
5413                    Some(arg_tys.clone())
5414                } else {
5415                    ctor_formals(cls, args.len())
5416                };
5417                if let Some(formals) = formals_owned {
5418                    for (a, f) in args.iter_mut().zip(formals.iter()) {
5419                        if null_arg_cast(a, f, pool) {
5420                            continue;
5421                        }
5422                        if matches!(a, Expr::Cast { .. }) {
5423                            continue;
5424                        }
5425                        if !is_bool(f) && !is_num(f) {
5426                            continue;
5427                        }
5428                        if let Some(b) = bridge(&val_ty(a, vt), f, a) {
5429                            *a = b;
5430                        }
5431                    }
5432                }
5433            }
5434            _ => {}
5435        });
5436    });
5437}
5438
5439/// `b ^ 1` / `1 ^ b` with a BOOLEAN operand is the bytecode's boolean
5440/// negation (`!b`) — weibo kotlin-stdlib `return v2 ^ 1;` ("二元运算符
5441/// '^' 的操作数类型错误"), and `x ^ true` shapes. Java only types `^`
5442/// for same-category operands.
5443/// Rename locals whose name equals a ROOT PACKAGE first segment: a
5444/// package-qualified render (`v2.n.a` — the FQN fallback a twin-shadowed
5445/// same-package ref must take) binds its first identifier lexically, and
5446/// a local `v2` in scope captures it (weixin v2/i: 找不到符号 变量 n on a
5447/// legit `v2.n.a(this, 0)`). Method-scope renames carry no registry
5448/// surface — every use renders through this same vt — unlike class/field
5449/// renames, whose reference coverage blew up five times before.
5450pub fn deshadow_locals(vt: &mut VarTable, pool: &crate::DexPool, class_name: &str, body: &Stmt) {
5451    let segs = pool.root_pkg_segs();
5452    // Import-layer simples: the file renders `import p.h;` + bare `h.e`
5453    // for obscured refs — a local `h` captures that simple name (locals
5454    // beat imports in expression position; 无法取消引用int family).
5455    let obscured = crate::classdec::obscured_simples_snapshot();
5456    // Same-package static-ref simples: a static member ref renders its
5457    // class qualifier as the SIMPLE name (same package — no import/FQN),
5458    // and a local with that name captures it (`a2.changeQuickRedirect`
5459    // bound to a String local `a2` — "找不到符号 变量 changeQuickRedirect
5460    // 位置: 类型为String的变量 a2", weibo robust-field refs ×317). Only
5461    // STATIC refs qualify (an instance ref's qualifier IS the local).
5462    let self_pkg = class_name.rfind('/').map(|i| &class_name[..i]).unwrap_or("");
5463    let mut same_pkg_simples: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
5464    visit_all_exprs(body, &mut |x| {
5465        let cls = match x {
5466            Expr::Field { cls, is_static: true, .. } => Some(cls.as_ref()),
5467            Expr::Method { cls, is_static: true, .. } => Some(cls.as_ref()),
5468            _ => None,
5469        };
5470        if let Some(c) = cls {
5471            let pkg = c.rfind('/').map(|i| &c[..i]).unwrap_or("");
5472            if pkg == self_pkg {
5473                let simple = &c[c.rfind('/').map(|i| i + 1).unwrap_or(0)..];
5474                let head = simple.split('$').next().unwrap_or(simple);
5475                if !head.is_empty() {
5476                    same_pkg_simples.insert(head.to_string());
5477                }
5478            }
5479        }
5480    });
5481    let hit = |name: &str| {
5482        segs.contains(name) || obscured.contains(name) || same_pkg_simples.contains(name)
5483    };
5484    if (segs.is_empty() && obscured.is_empty() && same_pkg_simples.is_empty())
5485        || !vt.vars.iter().any(|v| hit(&v.name))
5486    {
5487        return;
5488    }
5489    let mut taken: jdc_core::FxHashSet<String> =
5490        vt.vars.iter().map(|v| v.name.clone()).collect();
5491    for v in vt.vars.iter_mut() {
5492        if !hit(&v.name) {
5493            continue;
5494        }
5495        let mut k = 0u32;
5496        let cand = loop {
5497            k += 1;
5498            let c = if k == 1 {
5499                format!("{}x", v.name)
5500            } else {
5501                format!("{}x{}", v.name, k)
5502            };
5503            if !taken.contains(&c) {
5504                break c;
5505            }
5506        };
5507        taken.insert(cand.clone());
5508        v.name = cand;
5509    }
5510}
5511
5512/// Receiver rescue: a Field/Method receiver whose final vt type is
5513/// PRIMITIVE can never hold the member (structurer tail-copy regions
5514/// can map the receiver register onto a boolean/int generation —
5515/// wcdb HandleOperation's `v10_g2.finalizeStatement()` where the
5516/// statement var is right there; weixin deref-int/bool families,
5517/// ~2.5k lines). The member descriptor's owner class is authoritative:
5518/// when EXACTLY ONE var in the method has that type (or a subtype),
5519/// swap the receiver to it. The pre-rescue output is a guaranteed
5520/// compile error AND runtime NPE, so the unambiguous swap cannot make
5521/// it worse; ambiguous sites stay untouched.
5522/// Anonymous/inner-class outer reads: the dex reads `this$0` off the
5523/// instance register, which copy materialization can route through a
5524/// local (`v41 = this; ... v41.this$0` — weibo HorseRaceDetector$1,
5525/// 2,326-line 找不到符号 变量 this$0 family). When the field's
5526/// declaring class IS the class being decompiled, the only instance in
5527/// scope is `this` — strip the owner so it renders the bare declared
5528/// field.
5529/// Fold single-assigned `this`-copy locals in ctors back to `this`.
5530/// R8 materializes the receiver with `move-object/from16 v1, p0` and
5531/// runs every iput through the copy; the copy renders as a local and a
5532/// final-field store through it is "无法为 final 变量 g 分配值" (Java
5533/// allows blank-final writes only via `this`/simple name — hf/n2 ×29,
5534/// weixin final-var residual). Transitively: `b = a; c = b` chains
5535/// collapse. Single-assignment gate: a var ever written from another
5536/// value is not an alias.
5537pub fn fix_ctor_this_aliases(body: &mut Stmt, vt: &VarTable) {
5538    let Some(this_var) = vt
5539        .vars
5540        .iter()
5541        .find(|v| v.is_param && v.name == "this")
5542        .map(|v| v.id)
5543    else {
5544        return;
5545    };
5546    let mut analysis = VarAnalysis {
5547        assigns: Vec::new(),
5548        reads: Vec::new(),
5549        values: Vec::new(),
5550    };
5551    analyze_vars(body, &mut analysis);
5552    // All-sources rule: a local is a this-alias only when EVERY value
5553    // assignment traces to `this_var` through pure local copies. The
5554    // single-assign constraint this replaces missed R8's multi-branch
5555    // `this` spills (uuyc androidx.camera x_2: `v160 = v150` on two
5556    // paths, both this, chain this→v131→v140→v169→v174→v183→v150→v160),
5557    // so `v160.d = executor` never became `this.d = executor` and javac
5558    // rejected the final-field init (无法为 final 变量 d/i/j 分配值 ×103).
5559    // A single non-this source (field read, `new`, param, a literal)
5560    // disqualifies the local, so a marked alias is provably always this
5561    // and rewriting every read of it to `this` is sound.
5562    enum Src {
5563        This,
5564        Loc(u32),
5565        Other,
5566    }
5567    let mut sources: jdc_core::FxHashMap<u32, Vec<Src>> = jdc_core::FxHashMap::default();
5568    walk_all(body, &mut |st| {
5569        let (tgt, val): (u32, &Expr) = match st {
5570            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => match &**target {
5571                Expr::Local { var, .. } => (*var, &**value),
5572                _ => return,
5573            },
5574            Stmt::LocalDef { var, init: Some(value), .. } => (*var, value),
5575            _ => return,
5576        };
5577        let s = match val {
5578            Expr::This => Src::This,
5579            Expr::Local { var: src, .. } if *src == this_var => Src::This,
5580            Expr::Local { var: src, .. } => Src::Loc(*src),
5581            _ => Src::Other,
5582        };
5583        sources.entry(tgt).or_default().push(s);
5584    });
5585    let n = vt
5586        .vars
5587        .len()
5588        .max(analysis.assigns.len())
5589        .max(sources.keys().map(|k| *k as usize + 1).max().unwrap_or(0));
5590    let mut alias = vec![false; n];
5591    loop {
5592        let mut changed = false;
5593        for (v, srcs) in &sources {
5594            let vi = *v as usize;
5595            if vi >= n || alias[vi] || srcs.is_empty() {
5596                continue;
5597            }
5598            let ok = srcs.iter().all(|s| match s {
5599                Src::This => true,
5600                Src::Loc(w) => (*w as usize) < n && alias[*w as usize],
5601                Src::Other => false,
5602            });
5603            if ok {
5604                alias[vi] = true;
5605                changed = true;
5606            }
5607        }
5608        if !changed {
5609            break;
5610        }
5611    }
5612    if !alias.iter().any(|&b| b) {
5613        return;
5614    }
5615    walk_stmt_exprs(body, &mut |e| {
5616        deep_rewrite_reads(e, &mut |x| {
5617            if let Expr::Local { var, .. } = x {
5618                if (*var as usize) < n && alias[*var as usize] {
5619                    *x = Expr::This;
5620                }
5621            }
5622        });
5623    });
5624}
5625
5626pub fn fix_this0_owners(body: &mut Stmt, class_name: &str) {
5627    walk_stmt_exprs(body, &mut |e| {
5628        deep_rewrite(e, &mut |x| {
5629            if let Expr::Field { owner: Some(_), cls, name, .. } = x {
5630                if name.as_ref() == "this$0" && cls.as_ref() == class_name {
5631                    if let Expr::Field { owner, .. } = x {
5632                        *owner = None;
5633                    }
5634                }
5635            }
5636        });
5637    });
5638}
5639
5640pub fn rescue_primitive_receivers(body: &mut Stmt, vt: &VarTable, pool: &crate::DexPool) {
5641    fn prim(t: &JavaType) -> bool {
5642        match t {
5643            JavaType::Int
5644            | JavaType::Long
5645            | JavaType::Short
5646            | JavaType::Byte
5647            | JavaType::Char
5648            | JavaType::Float
5649            | JavaType::Double
5650            | JavaType::Boolean => true,
5651            // Boxed JDK finals carry no app members either (`bool19.d`
5652            // where bool19 came out java/lang/Boolean — the coroutine
5653            // Result-register conflation).
5654            JavaType::Object(n) => matches!(
5655                n.as_ref(),
5656                "java/lang/Boolean"
5657                    | "java/lang/String"
5658                    | "java/lang/Integer"
5659                    | "java/lang/Long"
5660                    | "java/lang/Short"
5661                    | "java/lang/Byte"
5662                    | "java/lang/Character"
5663                    | "java/lang/Float"
5664                    | "java/lang/Double"
5665            ),
5666            _ => false,
5667        }
5668    }
5669    walk_stmt_exprs(body, &mut |e| {
5670        deep_rewrite(e, &mut |x| {
5671            // Phase 1 (read-only): primitive-typed Local receiver?
5672            let (recv_var, recv_ty) = {
5673                let o: &Expr = match &*x {
5674                    Expr::Method { owner: Some(o), .. } => o,
5675                    Expr::Field { owner: Some(o), is_static: false, .. } => o,
5676                    _ => return,
5677                };
5678                let Expr::Local { var, ty } = o else {
5679                    return;
5680                };
5681                let vt_ty = if (*var as usize) < vt.vars.len() {
5682                    vt.var(*var).ty.erased()
5683                } else {
5684                    ty.erased()
5685                };
5686                if !prim(&vt_ty) {
5687                    return;
5688                }
5689                (*var, vt_ty)
5690            };
5691            let cls: std::sync::Arc<str> = match &*x {
5692                Expr::Method { cls, .. } | Expr::Field { cls, .. } => cls.clone(),
5693                _ => return,
5694            };
5695            // Type-consistent call: the receiver's own type IS the
5696            // member owner (or a subtype) — `str.hashCode()` on a
5697            // String param needs no rescue. Without this the boxed-JDK
5698            // prim() arm fired on every String member call and the
5699            // max-id fallback below hijacked the reads to the highest
5700            // String-typed var in the method — often a case-local
5701            // (lark LynxUIBaseInput str11: 37 reads rewritten to a
5702            // switch-case-scoped def, 找不到符号 ×37; x0/c$a18 str4).
5703            if let JavaType::Object(n) = &recv_ty {
5704                if n.as_ref() == cls.as_ref() || pool.is_subtype(n.as_ref(), cls.as_ref()) {
5705                    return;
5706                }
5707            }
5708            // Phase 2: vars of the owner type (exact or subtype).
5709            let mut cands: Vec<u32> = Vec::new();
5710            for v in &vt.vars {
5711                if let JavaType::Object(n) = v.ty.erased() {
5712                    if n.as_ref() == cls.as_ref()
5713                        || pool.is_subtype(n.as_ref(), cls.as_ref())
5714                    {
5715                        cands.push(v.id);
5716                    }
5717                }
5718            }
5719            let id = if cands.len() == 1 {
5720                cands[0]
5721            } else if cands.len() > 1 {
5722                // Multiple owner-typed vars: prefer the same-SLOT one
5723                // (register lineage), latest generation wins — the
5724                // receiver position is scope-insensitive (any method-
5725                // scope var renders, the decl hoist covers it).
5726                let slot = if (recv_var as usize) < vt.vars.len() {
5727                    Some(vt.var(recv_var).slot)
5728                } else {
5729                    None
5730                };
5731                let mut hits: Vec<u32> = match slot {
5732                    Some(sl) => cands
5733                        .iter()
5734                        .copied()
5735                        .filter(|&c| {
5736                            c != recv_var
5737                                && (c as usize) < vt.vars.len()
5738                                && vt.var(c).slot == sl
5739                        })
5740                        .collect(),
5741                    None => Vec::new(),
5742                };
5743                if hits.is_empty() {
5744                    // No same-slot twin: fall back to the latest
5745                    // owner-typed generation (max id).
5746                    hits = cands
5747                        .iter()
5748                        .copied()
5749                        .filter(|&c| c != recv_var)
5750                        .collect();
5751                }
5752                hits.sort_unstable();
5753                hits.dedup();
5754                if hits.is_empty() {
5755                    return;
5756                }
5757                *hits.last().unwrap()
5758            } else {
5759                // Lineage disambiguation by SLOT: register reuse makes
5760                // the Boolean/primitive twin and the object version of
5761                // one dex register share VarInfo.slot (ry0/h2's
5762                // `bool19.d` — the v1-typed twin rides the same
5763                // register). Multiple slot matches stay untouched
5764                // (deterministic conservatism).
5765                if cands.is_empty() {
5766                    return;
5767                }
5768                let slot = if (recv_var as usize) < vt.vars.len() {
5769                    vt.var(recv_var).slot
5770                } else {
5771                    return;
5772                };
5773                let mut hits: Vec<u32> = cands
5774                    .into_iter()
5775                    .filter(|&c| {
5776                        c != recv_var
5777                            && (c as usize) < vt.vars.len()
5778                            && vt.var(c).slot == slot
5779                    })
5780                    .collect();
5781                hits.sort_unstable();
5782                hits.dedup();
5783                if hits.len() != 1 {
5784                    return;
5785                }
5786                hits[0]
5787            };
5788            // Phase 3: swap.
5789            let o: &mut Box<Expr> = match x {
5790                Expr::Method { owner: Some(o), .. } => o,
5791                Expr::Field { owner: Some(o), is_static: false, .. } => o,
5792                _ => return,
5793            };
5794            if let Expr::Local { var, ty } = &mut **o {
5795                if *var == recv_var {
5796                    *var = id;
5797                    *ty = vt.vars[id as usize].ty.clone();
5798                }
5799            }
5800        });
5801    });
5802
5803}
5804
5805/// Scope-safe arg/return swaps for mistyped generations — runs AFTER
5806/// ensure_declared hoists every declaration to the method top, so the
5807/// top_ids visibility precondition holds by construction (the r134
5808/// falsification: swapping in a block-scoped var broke lexically).
5809/// Same-slot + type-match + uniqueness gates as the receiver rescue.
5810pub fn rescue_arg_return_swaps(
5811    body: &mut Stmt,
5812    vt: &VarTable,
5813    pool: &crate::DexPool,
5814    ret_ty: &JavaType,
5815) {
5816    // SCOPE-SAFE arg/return swap: a primitive-typed actual against a
5817    // specific-reference formal (or a reference-returning `return`) is
5818    // a mistyped generation — the r134 falsification showed the swap
5819    // target MUST be lexically visible at the use site, so candidates
5820    // are restricted to params + TOP-LEVEL declared locals (a hoisted
5821    // decl is visible to javac name resolution everywhere in the
5822    // method) AND same-slot (same dex register lineage) AND unique.
5823    let mut top_ids: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
5824    for v in &vt.vars {
5825        if v.is_param {
5826            top_ids.insert(v.id);
5827        }
5828    }
5829    if let Stmt::Block(stmts) = body {
5830        for st in stmts.iter() {
5831            if let Stmt::LocalDef { var, .. } = st {
5832                top_ids.insert(*var);
5833            }
5834        }
5835    }
5836    let ret_wants_ref = match ret_ty {
5837        JavaType::Object(n) => n.as_ref() != "java/lang/Object",
5838        JavaType::Array(_) => true,
5839        _ => false,
5840    };
5841    let try_swap = |a: &mut Expr, want: &JavaType| {
5842        let Expr::Local { var, ty } = &mut *a else {
5843            return;
5844        };
5845        if (*var as usize) >= vt.vars.len() {
5846            return;
5847        }
5848        let avt = vt.var(*var).ty.erased();
5849        let avt_ref = matches!(avt, JavaType::Object(_) | JavaType::Array(_));
5850        let want_arr = matches!(want, JavaType::Array(_));
5851        // Only swap a primitive-typed actual (an Object actual gets the
5852        // descriptor-exact cast instead — never both).
5853        if avt_ref && !(avt == JavaType::Object("java/lang/Object".into())) {
5854            return;
5855        }
5856        if avt_ref && !matches!(want, JavaType::Object(_)) {
5857            return;
5858        }
5859        let slot = vt.var(*var).slot;
5860        let mut hits: Vec<u32> = Vec::new();
5861        for v in &vt.vars {
5862            if v.slot != slot || !top_ids.contains(&v.id) || v.id == *var {
5863                continue;
5864            }
5865            let t = v.ty.erased();
5866            let ok = match (&t, want) {
5867                (JavaType::Object(n), JavaType::Object(w)) => {
5868                    n.as_ref() == w.as_ref()
5869                        || pool.is_subtype(n.as_ref(), w.as_ref())
5870                }
5871                (JavaType::Array(_), JavaType::Array(_)) => want_arr,
5872                _ => false,
5873            };
5874            if ok {
5875                hits.push(v.id);
5876            }
5877        }
5878        hits.sort_unstable();
5879        hits.dedup();
5880        if hits.is_empty() {
5881            return;
5882        }
5883        // Multiple same-slot candidates: the LATEST generation (max id —
5884        // split mints in walk order ≈ program order) is the closest
5885        // approximation of the register's value at the use site. A
5886        // wrong pick still compiles (type-matched by construction);
5887        // leaving the primitive actual never does.
5888        let id = *hits.last().unwrap();
5889        *var = id;
5890        *ty = vt.vars[id as usize].ty.clone();
5891    };
5892    walk_stmt_exprs(body, &mut |e| {
5893        deep_rewrite(e, &mut |x| {
5894            match x {
5895                Expr::Method { desc, args, is_dynamic: false, .. } => {
5896                    for (i, a) in args.iter_mut().enumerate() {
5897                        if let Some(f) = desc.args.get(i) {
5898                            if matches!(f, JavaType::Object(n) if n.as_ref() != "java/lang/Object")
5899                                || matches!(f, JavaType::Array(_))
5900                            {
5901                                try_swap(a, f);
5902                            }
5903                        }
5904                    }
5905                }
5906                Expr::New { arg_tys, args, .. } => {
5907                    for (i, a) in args.iter_mut().enumerate() {
5908                        if let Some(f) = arg_tys.get(i) {
5909                            if matches!(f, JavaType::Object(n) if n.as_ref() != "java/lang/Object")
5910                                || matches!(f, JavaType::Array(_))
5911                            {
5912                                try_swap(a, f);
5913                            }
5914                        }
5915                    }
5916                }
5917                _ => {}
5918            }
5919        });
5920    });
5921    if ret_wants_ref {
5922        walk_mut_deep(body, &mut |st| {
5923            if let Stmt::Return(Some(e)) = st {
5924                try_swap(e, ret_ty);
5925            }
5926        });
5927    }
5928}
5929
5930/// Reference-array initializers take null, not 0: dex fill-array-data
5931/// over a String[]/Object[] slot carries the zero word, and the lifted
5932/// `new String[] {0}` is int无法转换为String (qs0/b family). A Const
5933/// Int(0) element of a reference-typed array init is the null ref.
5934/// `refVar = const 0` (dex `const/4 vN, 0` into a reference register) is
5935/// the NULL sentinel, not an int. By the time this runs infer_types has
5936/// typed the var a reference (its `.close()`/`.length`/method reads
5937/// proved it), so retype the const to Null BEFORE split_generations —
5938/// otherwise the ref/primitive mismatch splits off an int generation and
5939/// orphans the reference reads (u7.l try-with-resources finally
5940/// `v16_g3 = 0; if (v16_g3 != 0) v16_g3.close();` → "无法取消引用int").
5941/// `fix_incomparable_equality` already retypes the `refVar != 0`
5942/// COMPARISON side (but runs after the split); this covers the ASSIGN
5943/// side, which is what triggers the bad split.
5944pub fn fix_ref_null_assigns(vt: &VarTable, body: &mut Stmt) {
5945    let is_ref_var = |var: u32| -> bool {
5946        (var as usize) < vt.vars.len()
5947            && matches!(
5948                vt.vars[var as usize].ty.erased(),
5949                JavaType::Object(_) | JavaType::Array(_)
5950            )
5951    };
5952    walk_mut_deep(body, &mut |st| match st {
5953        Stmt::ExprStmt(Expr::Assign {
5954            target,
5955            value,
5956            op: AssignOp::Plain,
5957            ..
5958        }) => {
5959            if let Expr::Local { var, .. } = &**target {
5960                if is_ref_var(*var) && matches!(&**value, Expr::Const(ConstVal::Int(0))) {
5961                    **value = Expr::Const(ConstVal::Null);
5962                }
5963            }
5964        }
5965        Stmt::LocalDef {
5966            var,
5967            init: Some(e),
5968            ..
5969        } => {
5970            if is_ref_var(*var) && matches!(e, Expr::Const(ConstVal::Int(0))) {
5971                *e = Expr::Const(ConstVal::Null);
5972            }
5973        }
5974        _ => {}
5975    });
5976}
5977
5978pub fn fix_ref_array_null_consts(body: &mut Stmt) {
5979    walk_stmt_exprs(body, &mut |e| {
5980        deep_rewrite(e, &mut |x| {
5981            if let Expr::NewArray { elem, init: Some(list), .. } = x {
5982                if matches!(elem.erased(), JavaType::Object(_) | JavaType::Array(_)) {
5983                    for slot in list.iter_mut() {
5984                        if matches!(&*slot, Expr::Const(ConstVal::Int(0))) {
5985                            *slot = Expr::Const(ConstVal::Null);
5986                        }
5987                    }
5988                }
5989            }
5990        });
5991    });
5992}
5993
5994// ---------------------------------------------------------------------------
5995// Null-sentinel repair: const-0 registers in reference-typed slots
5996// ---------------------------------------------------------------------------
5997
5998/// A dex `const/4 vN, 0` register is ambiguous between int 0 and the
5999/// null reference; the value view types the bare constant as Int.
6000/// Kotlin coroutine state machines lean on this heavily — captured
6001/// object slots are zero-initialized ahead of the label switch, then
6002/// stored into `Object` fields and passed as `Continuation` ctor args
6003/// (weixin y01/s9 q8: `new e6(s94x, v287)` "int无法转换为Continuation";
6004/// uf5/l0 `throw 0;`; wcdb RepairKit `setOnCancelListener(0)`). The
6005/// dex verifier guarantees a const-0 register reaching a reference-
6006/// typed slot holds null, so in those slots the rendering must be the
6007/// null literal.
6008///
6009/// Two rewrites, both gated on the slot's expected type being a
6010/// reference (descriptor formal types, field type, array component,
6011/// cast target, throw, method return):
6012///   1. a literal `0` in a ref slot → `null`. SKIPPED for call-arg
6013///      slots whose formal is exactly `java/lang/Object`: `f(0)` there
6014///      compiles TODAY (boxing, or an int-overload pick like
6015///      `String.valueOf(0)`), and `f(null)` could newly fail on
6016///      overload ambiguity (`valueOf(Object)` vs `valueOf(char[])`) —
6017///      a guaranteed-error slot becomes a compile, a compiling slot
6018///      must not become an error.
6019///   2. a proven null-sentinel LOCAL: an Int-typed var whose every
6020///      write is `= 0` or `= <another sentinel>` and whose every read
6021///      sits in a ref slot (or feeds another sentinel's write — a dead
6022///      chain) → `null` at each read. The same Object-formal skip
6023///      applies at arg slots. A read in ANY int/unknown context
6024///      (binop, cond test, receiver deref, selector, anon-ctor arg,
6025///      lambda capture…) disqualifies the var — register reuse across
6026///      generations keeps its int rendering.
6027///
6028/// Runs after fix_ref_null_assigns (which converts writes of REF-typed
6029/// vars) and before split_generations (sentinel writes must not mint
6030/// int generations). Writes of confirmed sentinels stay as harmless
6031/// dead int stores for drop_dead_locals.
6032pub fn fix_null_sentinels(body: &mut Stmt, vt: &VarTable, ret: &JavaType) {
6033    #[derive(Clone, Copy, PartialEq)]
6034    enum WK {
6035        Zero,
6036        Copy(u32),
6037        Other,
6038    }
6039    fn is_ref_jt(t: &JavaType) -> bool {
6040        matches!(t, JavaType::Object(_) | JavaType::Array(_))
6041    }
6042    fn cls_write(e: &Expr) -> WK {
6043        match e {
6044            Expr::Const(ConstVal::Int(0)) => WK::Zero,
6045            Expr::Local { var, .. } => WK::Copy(*var),
6046            _ => WK::Other,
6047        }
6048    }
6049    fn expr_writes(e: &Expr, writes: &mut jdc_core::FxHashMap<u32, Vec<WK>>) {
6050        visit_exprs(e, &mut |x| match x {
6051            Expr::Assign { target, op, value } => {
6052                if let Expr::Local { var, .. } = &**target {
6053                    let k = if matches!(op, AssignOp::Plain) {
6054                        cls_write(value)
6055                    } else {
6056                        WK::Other
6057                    };
6058                    writes.entry(*var).or_default().push(k);
6059                }
6060            }
6061            Expr::PreIncDec { e: inner, .. } | Expr::PostIncDec { e: inner, .. } => {
6062                if let Expr::Local { var, .. } = &**inner {
6063                    writes.entry(*var).or_default().push(WK::Other);
6064                }
6065            }
6066            _ => {}
6067        });
6068    }
6069    fn collect_writes(
6070        s: &Stmt,
6071        writes: &mut jdc_core::FxHashMap<u32, Vec<WK>>,
6072        has_zero: &mut bool,
6073    ) {
6074        fn note(e: &Expr, has_zero: &mut bool, writes: &mut jdc_core::FxHashMap<u32, Vec<WK>>) {
6075            visit_exprs(e, &mut |x| {
6076                if let Expr::Const(ConstVal::Int(0)) = x {
6077                    *has_zero = true;
6078                }
6079            });
6080            expr_writes(e, writes);
6081        }
6082        match s {
6083            Stmt::Block(v) => {
6084                for x in v {
6085                    collect_writes(x, writes, has_zero);
6086                }
6087            }
6088            Stmt::LocalDef { var, init, .. } => {
6089                if let Some(e) = init {
6090                    if matches!(e, Expr::Const(ConstVal::Int(0))) {
6091                        *has_zero = true;
6092                    }
6093                    writes.entry(*var).or_default().push(cls_write(e));
6094                }
6095            }
6096            Stmt::ExprStmt(e) => note(e, has_zero, writes),
6097            Stmt::Return(e) => {
6098                if let Some(x) = e {
6099                    note(x, has_zero, writes);
6100                }
6101            }
6102            Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
6103                note(e, has_zero, writes);
6104            }
6105            Stmt::TernaryValue { e } => note(e, has_zero, writes),
6106            Stmt::If { cond, then_stmt, else_stmt } => {
6107                note(cond, has_zero, writes);
6108                collect_writes(then_stmt, writes, has_zero);
6109                if let Some(b) = else_stmt {
6110                    collect_writes(b, writes, has_zero);
6111                }
6112            }
6113            Stmt::While { cond, body } | Stmt::DoWhile { body, cond } => {
6114                note(cond, has_zero, writes);
6115                collect_writes(body, writes, has_zero);
6116            }
6117            Stmt::For { init, cond, update, body } => {
6118                for x in init {
6119                    collect_writes(x, writes, has_zero);
6120                }
6121                if let Some(c) = cond {
6122                    note(c, has_zero, writes);
6123                }
6124                for u in update {
6125                    note(u, has_zero, writes);
6126                }
6127                collect_writes(body, writes, has_zero);
6128            }
6129            Stmt::ForEach { var, iterable, body, .. } => {
6130                writes.entry(*var).or_default().push(WK::Other);
6131                note(iterable, has_zero, writes);
6132                collect_writes(body, writes, has_zero);
6133            }
6134            Stmt::Switch { selector, cases, default, .. } => {
6135                note(selector, has_zero, writes);
6136                for c in cases {
6137                    if let Some(g) = &c.guard {
6138                        note(g, has_zero, writes);
6139                    }
6140                    for x in &c.body {
6141                        collect_writes(x, writes, has_zero);
6142                    }
6143                }
6144                if let Some(d) = default {
6145                    collect_writes(d, writes, has_zero);
6146                }
6147            }
6148            Stmt::Try { body, catches, finally } => {
6149                collect_writes(body, writes, has_zero);
6150                for c in catches {
6151                    writes.entry(c.var).or_default().push(WK::Other);
6152                    collect_writes(&c.body, writes, has_zero);
6153                }
6154                if let Some(f) = finally {
6155                    collect_writes(f, writes, has_zero);
6156                }
6157            }
6158            Stmt::TryWithResources { resources, body, catches, finally, .. } => {
6159                for r in resources {
6160                    collect_writes(r, writes, has_zero);
6161                }
6162                collect_writes(body, writes, has_zero);
6163                for c in catches {
6164                    writes.entry(c.var).or_default().push(WK::Other);
6165                    collect_writes(&c.body, writes, has_zero);
6166                }
6167                if let Some(f) = finally {
6168                    collect_writes(f, writes, has_zero);
6169                }
6170            }
6171            Stmt::Assert { cond, msg } => {
6172                note(cond, has_zero, writes);
6173                if let Some(m) = msg {
6174                    note(m, has_zero, writes);
6175                }
6176            }
6177            Stmt::Synchronized { lock, body } => {
6178                note(lock, has_zero, writes);
6179                collect_writes(body, writes, has_zero);
6180            }
6181            Stmt::Labeled { body, .. } => collect_writes(body, writes, has_zero),
6182            _ => {}
6183        }
6184    }
6185
6186    let mut writes: jdc_core::FxHashMap<u32, Vec<WK>> =
6187        jdc_core::FxHashMap::default();
6188    let mut has_zero = false;
6189    collect_writes(body, &mut writes, &mut has_zero);
6190
6191    // Write-proven eligibility fixpoint (copy chains: `v = u`).
6192    let mut eligible: HashSet<u32> = HashSet::default();
6193    let int_typed = |v: u32| {
6194        (v as usize) < vt.vars.len() && vt.vars[v as usize].ty.erased() == JavaType::Int
6195    };
6196    loop {
6197        let mut changed = false;
6198        for (&v, ws) in writes.iter() {
6199            if eligible.contains(&v) || ws.is_empty() || !int_typed(v) {
6200                continue;
6201            }
6202            if ws.iter().all(|k| match k {
6203                WK::Zero => true,
6204                WK::Copy(u) => *u != v && eligible.contains(u),
6205                WK::Other => false,
6206            }) {
6207                eligible.insert(v);
6208                changed = true;
6209            }
6210        }
6211        if !changed {
6212            break;
6213        }
6214    }
6215    if eligible.is_empty() && !has_zero {
6216        return; // nothing this pass can do
6217    }
6218
6219    #[derive(Clone, Copy, PartialEq)]
6220    enum Mode {
6221        Scan,
6222        Rewrite,
6223    }
6224    struct St<'a> {
6225        mode: Mode,
6226        vt: &'a VarTable,
6227        eligible: &'a HashSet<u32>,
6228        ref_read: HashSet<u32>,
6229        dead: HashSet<u32>,
6230        sentinels: HashSet<u32>,
6231        n_lit: usize,
6232        n_var: usize,
6233    }
6234    // A call-arg slot whose formal is exactly java/lang/Object keeps
6235    // its current rendering (see the fn doc — boxing compiles today,
6236    // null could newly ambiguize).
6237    fn arg_ctx(t: &JavaType) -> Option<JavaType> {
6238        match t {
6239            JavaType::Object(n) if n.as_ref() == "java/lang/Object" => None,
6240            t if is_ref_jt(t) => Some(t.clone()),
6241            _ => None,
6242        }
6243    }
6244    fn walk_expr(e: &mut Expr, ctx: Option<JavaType>, st: &mut St) {
6245        walk_expr_c(e, ctx, false, st);
6246    }
6247    /// `cast_arg`: the slot is a call/ctor ARGUMENT — a rewritten null
6248    /// gets an explicit formal-type cast. Bare `f(null)` re-triggers
6249    /// overload ambiguity where `f(0)` used to pick an int overload
6250    /// (`in(null)` "引用不明确", `ContentValues.put(String,null)` vs its
6251    /// 9 overloads); `(String) null` is unambiguous and jadx-shaped.
6252    fn walk_expr_c(e: &mut Expr, ctx: Option<JavaType>, cast_arg: bool, st: &mut St) {
6253        let slot_ref = ctx.as_ref().is_some_and(is_ref_jt);
6254        fn mk_null(ctx: &Option<JavaType>, cast_arg: bool) -> Expr {
6255            match (cast_arg, ctx) {
6256                (true, Some(t)) => Expr::Cast {
6257                    ty: TypeRef::J(t.clone()),
6258                    e: Box::new(Expr::Const(ConstVal::Null)),
6259                },
6260                _ => Expr::Const(ConstVal::Null),
6261            }
6262        }
6263        match e {
6264            Expr::Const(ConstVal::Int(0)) if slot_ref => {
6265                if st.mode == Mode::Rewrite {
6266                    *e = mk_null(&ctx, cast_arg);
6267                    st.n_lit += 1;
6268                }
6269                return;
6270            }
6271            Expr::Local { var, .. } => {
6272                if st.eligible.contains(var) {
6273                    if slot_ref {
6274                        st.ref_read.insert(*var);
6275                    } else {
6276                        st.dead.insert(*var);
6277                    }
6278                }
6279                if st.mode == Mode::Rewrite && slot_ref && st.sentinels.contains(var) {
6280                    *e = mk_null(&ctx, cast_arg);
6281                    st.n_var += 1;
6282                }
6283                return;
6284            }
6285            _ => {}
6286        }
6287        match e {
6288            Expr::Method { owner, desc, args, .. } => {
6289                if let Some(o) = owner {
6290                    walk_expr(o, None, st);
6291                }
6292                for (i, a) in args.iter_mut().enumerate() {
6293                    let c = desc.args.get(i).and_then(arg_ctx);
6294                    walk_expr_c(a, c, true, st);
6295                }
6296            }
6297            Expr::Invokedynamic { desc, args, .. } => {
6298                for (i, a) in args.iter_mut().enumerate() {
6299                    let c = desc.args.get(i).and_then(arg_ctx);
6300                    walk_expr_c(a, c, true, st);
6301                }
6302            }
6303            Expr::New { args, arg_tys, .. } => {
6304                for (i, a) in args.iter_mut().enumerate() {
6305                    let c = arg_tys.get(i).and_then(arg_ctx);
6306                    walk_expr_c(a, c, true, st);
6307                }
6308            }
6309            Expr::AnonNew { args, .. } => {
6310                for a in args.iter_mut() {
6311                    walk_expr(a, None, st);
6312                }
6313            }
6314            Expr::Assign { target, value, .. } => {
6315                // Sentinel-to-sentinel copy: the whole store is dead;
6316                // its RHS is by the write gate a const-0 or a bare
6317                // local, so no nested reads can hide in it.
6318                if let Expr::Local { var: u, .. } = &**target {
6319                    if st.eligible.contains(u) {
6320                        return;
6321                    }
6322                }
6323                let vctx = match &**target {
6324                    Expr::Field { ty, .. } => {
6325                        let t = ty.erased();
6326                        is_ref_jt(&t).then_some(t)
6327                    }
6328                    Expr::ArrayIndex { array, .. } => match array.type_ref().erased() {
6329                        JavaType::Array(inner) if is_ref_jt(&inner) => Some(*inner),
6330                        _ => None,
6331                    },
6332                    _ => None,
6333                };
6334                walk_expr(target, None, st);
6335                walk_expr(value, vctx, st);
6336            }
6337            Expr::Cast { ty, e: inner } => {
6338                let t = ty.erased();
6339                let c = is_ref_jt(&t).then_some(t);
6340                walk_expr(inner, c, st);
6341            }
6342            Expr::InstanceOf { e: inner, .. } => {
6343                walk_expr(inner, Some(JavaType::Object("java/lang/Object".into())), st);
6344            }
6345            Expr::Cond { c, t, f } => {
6346                walk_expr(c, None, st);
6347                let tc = ctx.clone();
6348                walk_expr(t, tc, st);
6349                walk_expr(f, ctx, st);
6350            }
6351            Expr::NewArray { elem, dims, init, .. } => {
6352                for d in dims.iter_mut() {
6353                    walk_expr(d, None, st);
6354                }
6355                if let Some(list) = init {
6356                    let et = elem.erased();
6357                    let c = is_ref_jt(&et).then_some(et);
6358                    for slot in list.iter_mut() {
6359                        walk_expr(slot, c.clone(), st);
6360                    }
6361                }
6362            }
6363            Expr::NewMultiArray { dims, .. } => {
6364                for d in dims.iter_mut() {
6365                    walk_expr(d, None, st);
6366                }
6367            }
6368            Expr::ArrayIndex { array, index } => {
6369                walk_expr(array, None, st);
6370                walk_expr(index, None, st);
6371            }
6372            Expr::Field { owner, .. } => {
6373                if let Some(o) = owner {
6374                    walk_expr(o, None, st);
6375                }
6376            }
6377            Expr::Bin { l, r, .. } => {
6378                walk_expr(l, None, st);
6379                walk_expr(r, None, st);
6380            }
6381            Expr::Un { e: inner, .. } => walk_expr(inner, None, st),
6382            Expr::PreIncDec { e: inner, .. } | Expr::PostIncDec { e: inner, .. } => {
6383                walk_expr(inner, None, st);
6384            }
6385            Expr::StringConcat(parts) => {
6386                for p in parts.iter_mut() {
6387                    if let ConcatPart::Str(x) = p {
6388                        walk_expr(x, None, st);
6389                    }
6390                }
6391            }
6392            Expr::Lambda(l) => {
6393                for c in l.captures.iter_mut() {
6394                    walk_expr(c, None, st);
6395                }
6396            }
6397            _ => {}
6398        }
6399    }
6400    fn walk_stmt(s: &mut Stmt, st: &mut St, ret: &JavaType) {
6401        match s {
6402            Stmt::Block(v) => {
6403                for x in v.iter_mut() {
6404                    walk_stmt(x, st, ret);
6405                }
6406            }
6407            Stmt::ExprStmt(e) => walk_expr(e, None, st),
6408            Stmt::LocalDef { var, init, .. } => {
6409                if let Some(e) = init {
6410                    let ctx = if (*var as usize) < st.vt.vars.len() {
6411                        let t = st.vt.vars[*var as usize].ty.erased();
6412                        is_ref_jt(&t).then_some(t)
6413                    } else {
6414                        None
6415                    };
6416                    walk_expr(e, ctx, st);
6417                }
6418            }
6419            Stmt::Return(e) => {
6420                if let Some(x) = e {
6421                    let c = is_ref_jt(ret).then(|| ret.clone());
6422                    walk_expr(x, c, st);
6423                }
6424            }
6425            Stmt::Throw(e) => {
6426                walk_expr(e, Some(JavaType::Object("java/lang/Throwable".into())), st);
6427            }
6428            Stmt::If { cond, then_stmt, else_stmt } => {
6429                walk_expr(cond, None, st);
6430                walk_stmt(then_stmt, st, ret);
6431                if let Some(b) = else_stmt {
6432                    walk_stmt(b, st, ret);
6433                }
6434            }
6435            Stmt::While { cond, body } => {
6436                walk_expr(cond, None, st);
6437                walk_stmt(body, st, ret);
6438            }
6439            Stmt::DoWhile { body, cond } => {
6440                walk_stmt(body, st, ret);
6441                walk_expr(cond, None, st);
6442            }
6443            Stmt::For { init, cond, update, body } => {
6444                for x in init.iter_mut() {
6445                    walk_stmt(x, st, ret);
6446                }
6447                if let Some(c) = cond {
6448                    walk_expr(c, None, st);
6449                }
6450                for u in update.iter_mut() {
6451                    walk_expr(u, None, st);
6452                }
6453                walk_stmt(body, st, ret);
6454            }
6455            Stmt::ForEach { iterable, body, .. } => {
6456                walk_expr(iterable, None, st);
6457                walk_stmt(body, st, ret);
6458            }
6459            Stmt::Switch { selector, cases, default, .. } => {
6460                walk_expr(selector, None, st);
6461                for c in cases.iter_mut() {
6462                    if let Some(g) = &mut c.guard {
6463                        walk_expr(g, None, st);
6464                    }
6465                    for x in c.body.iter_mut() {
6466                        walk_stmt(x, st, ret);
6467                    }
6468                }
6469                if let Some(d) = default {
6470                    walk_stmt(d, st, ret);
6471                }
6472            }
6473            Stmt::Try { body, catches, finally } => {
6474                walk_stmt(body, st, ret);
6475                for c in catches.iter_mut() {
6476                    walk_stmt(&mut c.body, st, ret);
6477                }
6478                if let Some(f) = finally {
6479                    walk_stmt(f, st, ret);
6480                }
6481            }
6482            Stmt::TryWithResources { resources, body, catches, finally, .. } => {
6483                for r in resources.iter_mut() {
6484                    walk_stmt(r, st, ret);
6485                }
6486                walk_stmt(body, st, ret);
6487                for c in catches.iter_mut() {
6488                    walk_stmt(&mut c.body, st, ret);
6489                }
6490                if let Some(f) = finally {
6491                    walk_stmt(f, st, ret);
6492                }
6493            }
6494            Stmt::Assert { cond, msg } => {
6495                walk_expr(cond, None, st);
6496                if let Some(m) = msg {
6497                    walk_expr(m, None, st);
6498                }
6499            }
6500            Stmt::Synchronized { lock, body } => {
6501                walk_expr(lock, Some(JavaType::Object("java/lang/Object".into())), st);
6502                walk_stmt(body, st, ret);
6503            }
6504            Stmt::TernaryValue { e } => walk_expr(e, None, st),
6505            Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
6506                walk_expr(e, Some(JavaType::Object("java/lang/Object".into())), st);
6507            }
6508            Stmt::Labeled { body, .. } => walk_stmt(body, st, ret),
6509            _ => {}
6510        }
6511    }
6512
6513    let mut st = St {
6514        mode: Mode::Scan,
6515        vt,
6516        eligible: &eligible,
6517        ref_read: HashSet::default(),
6518        dead: HashSet::default(),
6519        sentinels: HashSet::default(),
6520        n_lit: 0,
6521        n_var: 0,
6522    };
6523    if !eligible.is_empty() {
6524        walk_stmt(body, &mut st, ret);
6525        st.sentinels = eligible
6526            .iter()
6527            .copied()
6528            .filter(|v| st.ref_read.contains(v) && !st.dead.contains(v))
6529            .collect();
6530    }
6531    if st.sentinels.is_empty() && !has_zero {
6532        return;
6533    }
6534    st.mode = Mode::Rewrite;
6535    walk_stmt(body, &mut st, ret);
6536    if std::env::var_os("DDC_SENT").is_some() {
6537        eprintln!(
6538            "[sent] elig={} confirmed={} lit={} var={}",
6539            eligible.len(),
6540            st.sentinels.len(),
6541            st.n_lit,
6542            st.n_var
6543        );
6544    }
6545}
6546
6547/// Int-context operand bridge: a BOOLEAN-typed side of an int-kind Bin
6548/// (bitwise, arithmetic, comparison against a numeric) becomes
6549/// `(b ? 1 : 0)` — the dex-level truth (booleans ARE 0/1 ints there;
6550/// register reuse puts a bool-typed var into an int expression:
6551/// `v5x | 4`, `b == 0` — "boolean无法转换为int" / "二元运算符操作数
6552/// 类型错误" families). Runs AFTER fix_bool_xor so boolean-SINK chains
6553/// were already converted to all-boolean form and are skipped here.
6554pub fn fix_int_operand_bridges(body: &mut Stmt, vt: &VarTable) {
6555    fn side_bool(e: &Expr, vt: &VarTable) -> bool {
6556        match e {
6557            Expr::Local { var, .. } => {
6558                matches!(vt.var(*var).ty.erased(), JavaType::Boolean)
6559            }
6560            // An all-boolean bitwise bin IS boolean even when its
6561            // embedded ty is still the frozen Int of the or-int lift
6562            // (booleanize retypes the vt; mixed_rewrite sets bin ty
6563            // only on the nodes it rewrites). The flat lookup called
6564            // `(ci13|ci14)` an int side, so the sibling `v408 != 0`
6565            // got `? 1 : 0`-wrapped while the bool bin stayed bare —
6566            // rendering int | boolean inside the wrap (news compose
6567            // CoreTextFieldKt `((v408!=0 ?1:0) | (ci13|ci14) ?1:0)|v412`
6568            // ×349 residual). Mirrors fix_bool_xor's bty recursion.
6569            Expr::Bin {
6570                op: BinOp::And | BinOp::Or | BinOp::Xor,
6571                l,
6572                r,
6573                ..
6574            } => side_bool(l, vt) && side_bool(r, vt),
6575            other => matches!(other.type_ref().erased(), JavaType::Boolean),
6576        }
6577    }
6578    fn side_int(e: &Expr, vt: &VarTable) -> bool {
6579        // A boolean-composed bin keeps its frozen Int embedded ty —
6580        // without this guard it double-answers as the "int side" of
6581        // its own bridge arm and steers the wrap onto the wrong operand.
6582        if side_bool(e, vt) {
6583            return false;
6584        }
6585        matches!(
6586            match e {
6587                Expr::Local { var, .. } => vt.var(*var).ty.erased(),
6588                other => other.type_ref().erased(),
6589            },
6590            JavaType::Int | JavaType::Short | JavaType::Byte | JavaType::Char | JavaType::Long
6591        )
6592    }
6593    fn wrap(e: Box<Expr>) -> Box<Expr> {
6594        Box::new(Expr::Cond {
6595            c: e,
6596            t: Box::new(Expr::Const(ConstVal::Int(1))),
6597            f: Box::new(Expr::Const(ConstVal::Int(0))),
6598        })
6599    }
6600    walk_stmt_exprs(body, &mut |e| {
6601        deep_rewrite(e, &mut |x| {
6602            // Field / array-store positions: the target's type is
6603            // descriptor-backed (authoritative); a boolean value into
6604            // an int slot bridges `(b ? 1 : 0)` (with a narrowing cast
6605            // for byte/short/char slots), an int value into a boolean
6606            // slot bridges `v != 0` (weixin `this.L[v3x] = max3`,
6607            // `g2x.d = p1x` — 785 field + ~700 array lines).
6608            if let Expr::Assign { target, value, op: AssignOp::Plain } = x {
6609                let tgt_ty: Option<JavaType> = match &**target {
6610                    Expr::Field { ty, .. } => Some(ty.erased()),
6611                    Expr::ArrayIndex { array, .. } => {
6612                        match array.type_ref().erased() {
6613                            JavaType::Array(el) => Some(el.as_ref().clone()),
6614                            _ => None,
6615                        }
6616                    }
6617                    // Local targets: the split/booleanize machinery owns
6618                    // the mixed-kind register cases; the residue that
6619                    // reached render (boolean无法转换为byte/long, ~360
6620                    // lines) gets the same value-side bridge.
6621                    Expr::Local { var, .. } => {
6622                        if (*var as usize) < vt.vars.len() {
6623                            Some(vt.var(*var).ty.erased())
6624                        } else {
6625                            None
6626                        }
6627                    }
6628                    _ => None,
6629                };
6630                if let Some(t) = tgt_ty {
6631                    let tgt_int = matches!(
6632                        t,
6633                        JavaType::Int
6634                            | JavaType::Short
6635                            | JavaType::Byte
6636                            | JavaType::Char
6637                            | JavaType::Long
6638                    );
6639                    if tgt_int && side_bool(value, vt) && !side_int(value, vt) {
6640                        let taken = std::mem::replace(
6641                            value,
6642                            Box::new(Expr::Const(ConstVal::Null)),
6643                        );
6644                        let mut wrapped = wrap(taken);
6645                        if !matches!(t, JavaType::Int | JavaType::Long) {
6646                            wrapped = Box::new(Expr::Cast {
6647                                ty: TypeRef::J(t.clone()),
6648                                e: wrapped,
6649                            });
6650                        }
6651                        *value = wrapped;
6652                    } else if matches!(t, JavaType::Boolean)
6653                        && side_int(value, vt)
6654                        && !side_bool(value, vt)
6655                    {
6656                        let taken = std::mem::replace(
6657                            value,
6658                            Box::new(Expr::Const(ConstVal::Null)),
6659                        );
6660                        **value = Expr::Bin {
6661                            op: BinOp::Ne,
6662                            l: taken,
6663                            r: Box::new(Expr::Const(ConstVal::Int(0))),
6664                            ty: Some(TypeRef::J(JavaType::Boolean)),
6665                        };
6666                    }
6667                }
6668            }
6669            // Explicit casts across the bool/num line are invalid Java
6670            // in BOTH directions: `(byte) this.r` (dex int-to-byte over
6671            // a boolean-rendered field — weixin AccInfo writeByte ×207)
6672            // becomes `(byte)(r ? 1 : 0)`, and `(boolean) x` over an
6673            // int becomes `x != 0`.
6674            if let Expr::Cast { ty, e } = x {
6675                let t = ty.erased();
6676                let t_num = matches!(
6677                    t,
6678                    JavaType::Int
6679                        | JavaType::Short
6680                        | JavaType::Byte
6681                        | JavaType::Char
6682                        | JavaType::Long
6683                        | JavaType::Float
6684                        | JavaType::Double
6685                );
6686                if t_num && side_bool(e, vt) && !side_int(e, vt) {
6687                    let taken = std::mem::replace(e, Box::new(Expr::Const(ConstVal::Null)));
6688                    **e = Expr::Cond {
6689                        c: taken,
6690                        t: Box::new(Expr::Const(ConstVal::Int(1))),
6691                        f: Box::new(Expr::Const(ConstVal::Int(0))),
6692                    };
6693                } else if matches!(t, JavaType::Boolean)
6694                    && side_int(e, vt)
6695                    && !side_bool(e, vt)
6696                {
6697                    let taken = std::mem::replace(e, Box::new(Expr::Const(ConstVal::Null)));
6698                    *x = Expr::Bin {
6699                        op: BinOp::Ne,
6700                        l: taken,
6701                        r: Box::new(Expr::Const(ConstVal::Int(0))),
6702                        ty: Some(TypeRef::J(JavaType::Boolean)),
6703                    };
6704                }
6705            }
6706            // Null-vs-int comparison: dex if-eqz/if-nez is type-agnostic
6707            // and null IS zero — when the local came out int-typed (phi
6708            // confluence residue), rewrite the Null constant to 0
6709            // (`v95_g12 != null` → `v95_g12 != 0`, faithful and legal;
6710            // yq0/g1 546-line 二元运算符 family). BOOLEAN side: the same
6711            // zero-register compare reads as the truth test — `b != null`
6712            // IS `b`, `b == null` IS `!b` (alipay v87_g7_g2 != null,
6713            // 二元运算符 '!=' boolean vs <空值> ×43 / news ×30).
6714            if let Expr::Bin { op, l, r, .. } = x {
6715                if matches!(op, BinOp::Eq | BinOp::Ne) {
6716                    if matches!(&**r, Expr::Const(ConstVal::Null))
6717                        && side_int(l, vt)
6718                        && !side_bool(l, vt)
6719                    {
6720                        **r = Expr::Const(ConstVal::Int(0));
6721                    } else if matches!(&**l, Expr::Const(ConstVal::Null))
6722                        && side_int(r, vt)
6723                        && !side_bool(r, vt)
6724                    {
6725                        **l = Expr::Const(ConstVal::Int(0));
6726                    } else if matches!(&**r, Expr::Const(ConstVal::Null))
6727                        && side_bool(l, vt)
6728                    {
6729                        let taken =
6730                            std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
6731                        *x = if matches!(op, BinOp::Eq) {
6732                            Expr::Un { op: UnOp::Not, e: taken }
6733                        } else {
6734                            *taken
6735                        };
6736                    } else if matches!(&**l, Expr::Const(ConstVal::Null))
6737                        && side_bool(r, vt)
6738                    {
6739                        let taken =
6740                            std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
6741                        *x = if matches!(op, BinOp::Eq) {
6742                            Expr::Un { op: UnOp::Not, e: taken }
6743                        } else {
6744                            *taken
6745                        };
6746                    }
6747                }
6748            }
6749            // A boolean-typed array INDEX is a reused register holding
6750            // an int (`this.L[v3x]` — boolean无法转换为int at the index
6751            // position): bridge it in reads and writes alike.
6752            if let Expr::ArrayIndex { index, .. } = x {
6753                if side_bool(index, vt) && !side_int(index, vt) {
6754                    let taken =
6755                        std::mem::replace(index, Box::new(Expr::Const(ConstVal::Null)));
6756                    *index = wrap(taken);
6757                }
6758            }
6759            // A boolean-typed array DIMENSION is the same reused-register
6760            // case at the allocation site (`new Object[v200]` where v200
6761            // is boolean-typed — weixin AppBrandRuntime, bool→int family):
6762            // dex new-array reads the size register as int and booleans
6763            // ARE 0/1 ints there, so bridge `(b ? 1 : 0)`.
6764            if let Expr::NewArray { dims, .. } = x {
6765                for d in dims.iter_mut() {
6766                    if side_bool(d, vt) && !side_int(d, vt) {
6767                        let taken =
6768                            std::mem::replace(d, Expr::Const(ConstVal::Null));
6769                        *d = *wrap(Box::new(taken));
6770                    }
6771                }
6772            }
6773            if let Expr::Bin { op, l, r, .. } = x {
6774                let int_kind = matches!(
6775                    op,
6776                    BinOp::Or
6777                        | BinOp::And
6778                        | BinOp::Xor
6779                        | BinOp::Add
6780                        | BinOp::Sub
6781                        | BinOp::Mul
6782                        | BinOp::Div
6783                        | BinOp::Rem
6784                        | BinOp::Shl
6785                        | BinOp::Shr
6786                        | BinOp::Ushr
6787                        | BinOp::Eq
6788                        | BinOp::Ne
6789                        | BinOp::Lt
6790                        | BinOp::Ge
6791                        | BinOp::Gt
6792                        | BinOp::Le
6793                );
6794                if !int_kind {
6795                    return;
6796                }
6797                // A Null CONSTANT operand of an int-kind bitwise/arith
6798                // bin is the dex zero register (or-int reads 0 out of a
6799                // register whose value view was null — `b | null`
6800                // renders boolean | <空值>, alipay ×43 / news ×30).
6801                // Rewriting to Int(0) is register-faithful and lets the
6802                // bool-side wraps below take over (`(b ? 1 : 0) | 0`).
6803                // Only when the SIBLING is a bool/int side: a
6804                // reference-typed sibling keeps the null shape (that is
6805                // SSA-merge residue, not a zero-register read), and a
6806                // String sibling keeps `+ null` concatenation ("null" —
6807                // valid and faithful; side_str excludes it via the
6808                // side_bool/side_int guard).
6809                if matches!(
6810                    op,
6811                    BinOp::Or
6812                        | BinOp::And
6813                        | BinOp::Xor
6814                        | BinOp::Add
6815                        | BinOp::Sub
6816                        | BinOp::Mul
6817                        | BinOp::Div
6818                        | BinOp::Rem
6819                        | BinOp::Shl
6820                        | BinOp::Shr
6821                        | BinOp::Ushr
6822                ) {
6823                    if matches!(&**r, Expr::Const(ConstVal::Null))
6824                        && (side_bool(l, vt) || side_int(l, vt))
6825                    {
6826                        **r = Expr::Const(ConstVal::Int(0));
6827                    } else if matches!(&**l, Expr::Const(ConstVal::Null))
6828                        && (side_bool(r, vt) || side_int(r, vt))
6829                    {
6830                        **l = Expr::Const(ConstVal::Int(0));
6831                    }
6832                }
6833                // PURE arithmetic/shift and ORDERING operands never take
6834                // a boolean side in ANY combination (`boolean + boolean`
6835                // is as invalid as `boolean + int` — reused 0/1 registers
6836                // in `p4x + v20 + (v21?1:0)`, uuyc u0/u02; `boolean <
6837                // boolean` likewise) — bridge each bool side on its own.
6838                // String concatenation is exempt: `"s" + b` is valid Java
6839                // and wrapping would print 1/0 instead of true/false.
6840                // Equality stays on the mixed-side rule below (a
6841                // bool == bool compare IS valid).
6842                let arith = matches!(
6843                    op,
6844                    BinOp::Add
6845                        | BinOp::Sub
6846                        | BinOp::Mul
6847                        | BinOp::Div
6848                        | BinOp::Rem
6849                        | BinOp::Shl
6850                        | BinOp::Shr
6851                        | BinOp::Ushr
6852                );
6853                let ordered = matches!(op, BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le);
6854                if arith || ordered {
6855                    let side_str = |e: &Expr| {
6856                        matches!(
6857                            match e {
6858                                Expr::Local { var, .. } => vt.var(*var).ty.erased(),
6859                                other => other.type_ref().erased(),
6860                            },
6861                            JavaType::Object(ref s) if s.as_ref() == "java/lang/String"
6862                        )
6863                    };
6864                    let concat = arith && (side_str(l) || side_str(r));
6865                    if !concat {
6866                        if side_bool(l, vt) {
6867                            let taken =
6868                                std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
6869                            *l = wrap(taken);
6870                        }
6871                        if side_bool(r, vt) {
6872                            let taken =
6873                                std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
6874                            *r = wrap(taken);
6875                        }
6876                    }
6877                    return;
6878                }
6879                // Comparisons only bridge against a NUMERIC constant or
6880                // int-typed side (a bool == bool compare is valid Java).
6881                if side_bool(l, vt) && side_int(r, vt) && !side_bool(r, vt) {
6882                    let taken = std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
6883                    *l = wrap(taken);
6884                } else if side_bool(r, vt) && side_int(l, vt) && !side_bool(l, vt) {
6885                    let taken = std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
6886                    *r = wrap(taken);
6887                }
6888            }
6889        });
6890    });
6891}
6892
6893/// Statement-level idiom restoration (readability parity with
6894/// jadx/DAD, flagged by the androguard comparison): `v = v + 1` →
6895/// `v++`, `v = v op x` → `v op= x`. Local targets only — a compound
6896/// field/array target would change how often the receiver expression
6897/// evaluates. Var must be the LEFT operand (non-commutative ops;
6898/// string-concat order). The ±1 fold requires a numeric vt type
6899/// (`s++` on a String is invalid while `s += 1` is not).
6900/// Boolean VALUE-diamond fold (DAD short_circuit value forms):
6901/// ```text
6902///   v = false; if (c) { v = true; }   →  v = c
6903///   v = true;  if (c) { v = false; }  →  v = !c
6904///   v = false; if (c) { v = E; }      →  v = c && E     (E bool-valued)
6905///   v = true;  if (c) { v = E; }      →  v = !c || E
6906///   v = D; if (c) { v = E1; } else { v = E2; }  →  v = c ? E1 : E2
6907/// ```
6908/// `v` must be Boolean-typed; the arm value bool-valued; the guard may
6909/// not touch `v`; base and arm constants must not coincide (a no-op
6910/// diamond could drop guard side effects). Kotlin default-arg bridge
6911/// ctors compute defaulted booleans in exactly this shape INSIDE the
6912/// branch ahead of the delegation (lark MmCreateAudioRequest's v21x
6913/// chain) — split_branch rejects the control flow as Dirty and the
6914/// this() call stays in the branch ("对this的调用必须是构造器中的第一
6915/// 个语句", lark ×456). Folded to a linear def (interleaved with
6916/// forward_single_use for the `[u = call; v = u]` arm shape) the
6917/// merge_at ternary fold applies. Source-faithful: the original was
6918/// `x?.m() ?: false` — one expression; the inlined call stays under
6919/// the guard arm, preserving evaluation order and short-circuiting.
6920/// General value-diamond fold (fold_bool_value_diamonds for ANY type):
6921/// `if (c) { v = a; } else { v = b; }` → `v = c ? a : b;`, plus the
6922/// split-pair shape `v = b; if (c) { v = a; }` → the same ternary. The
6923/// Kotlin default-arg ctor's mask-guarded per-arg defaults arrive as
6924/// diamonds (DocFreshInfo v10x/v14x); folding gives each carrier a
6925/// SINGLE-assignment def so the chain-aware delegation inline can hoist
6926/// the this() first ("对this的调用必须是构造器中的第一个语句"). Fold
6927/// only when both sides are exactly one plain assignment to the SAME
6928/// local and the value types are ternary-compatible (equal erased or
6929/// both numeric — mixed-category ternaries box/surprise).
6930pub fn fold_value_diamonds(body: &mut Stmt, vt: &VarTable) {
6931    let Stmt::Block(stmts) = body else { return };
6932    fold_diamonds_in(stmts, vt);
6933}
6934
6935fn diamond_types_ok(a: &Expr, b: &Expr) -> bool {
6936    fn num(t: &JavaType) -> bool {
6937        matches!(
6938            t,
6939            JavaType::Byte
6940                | JavaType::Short
6941                | JavaType::Int
6942                | JavaType::Char
6943                | JavaType::Long
6944                | JavaType::Float
6945                | JavaType::Double
6946        )
6947    }
6948    // A null branch is compatible with any REFERENCE other branch: the
6949    // ternary types as lub(T, null) = T. Kotlin default-args ctors
6950    // render null defaults (`if ((mask&2)==0) v=x; else v=null;`) —
6951    // rejecting the pair left the diamond unfolded, the defs
6952    // un-inlineable, and the this(...) delegation unhoistable (weibo
6953    // ctor-not-first robust-guard/second-delegation ×818). A primitive
6954    // sibling stays rejected (`c ? 5 : null` is not Java).
6955    if let Some(other) = if matches!(a, Expr::Const(ConstVal::Null)) {
6956        Some(b)
6957    } else if matches!(b, Expr::Const(ConstVal::Null)) {
6958        Some(a)
6959    } else {
6960        None
6961    } {
6962        return !num(&other.type_ref().erased());
6963    }
6964    let ta = a.type_ref().erased();
6965    let tb = b.type_ref().erased();
6966    ta == tb || (num(&ta) && num(&tb))
6967}
6968
6969/// `(var, target clone, value)` when the stmt is exactly ONE plain
6970/// assign to a local (bare or block-wrapped). The unwrap must be
6971/// RECURSIVE: the Kotlin default-args ctor diamonds arrive as
6972/// `Block([Block([Assign])])` (structurer nesting), and the one-level
6973/// peel missed them — the unfolded `if ((mask&2)==0) v=p; else v=0;`
6974/// prelude then blocked fix_ctor_super_first from hoisting the
6975/// `this(...)` delegation (weibo/lark ctor-not-first ×841 family).
6976fn single_local_assign(st: &Stmt) -> Option<(u32, Expr, Expr)> {
6977    let mut inner = st;
6978    while let Stmt::Block(v) = inner {
6979        if v.len() != 1 {
6980            return None;
6981        }
6982        inner = &v[0];
6983    }
6984    match inner {
6985        Stmt::ExprStmt(Expr::Assign { target, value, op: AssignOp::Plain }) => {
6986            match &**target {
6987                Expr::Local { var, .. } => Some((*var, *target.clone(), *value.clone())),
6988                _ => None,
6989            }
6990        }
6991        _ => None,
6992    }
6993}
6994
6995/// Like `single_local_assign`, but ALSO accepts an arm whose value is
6996/// produced by an arm-local def: `Block([LocalDef vX = init, v = vX])`
6997/// (possibly Block-wrapped) inlines `init` into the returned value when
6998/// `vX` is read exactly once there (single evaluation preserved). The
6999/// Kotlin mask-ctor long defaults render this shape (`else { long u =
7000/// Color.getUnspecified(); v37 = u; }` — compose TextStyle/SpanStyle);
7001/// without the inline the diamond stays unfolded and blocks the whole
7002/// delegation-inlining chain downstream.
7003fn arm_assign_inlined(st: &Stmt) -> Option<(u32, Expr, Expr)> {
7004    if let Some(r) = single_local_assign(st) {
7005        return Some(r);
7006    }
7007    let mut inner = st;
7008    while let Stmt::Block(v) = inner {
7009        if v.len() == 1 {
7010            inner = &v[0];
7011        } else {
7012            break;
7013        }
7014    }
7015    let Stmt::Block(v) = inner else { return None };
7016    if v.len() != 2 {
7017        return None;
7018    }
7019    let Stmt::LocalDef {
7020        var: x,
7021        init: Some(init),
7022        ..
7023    } = &v[0]
7024    else {
7025        return None;
7026    };
7027    let (var, tgt, val) = single_local_assign(&v[1])?;
7028    let mut count = 0usize;
7029    let mut c = val.clone();
7030    deep_rewrite(&mut c, &mut |e| {
7031        if let Expr::Local { var: lv, .. } = e {
7032            if lv == x {
7033                count += 1;
7034            }
7035        }
7036    });
7037    if count != 1 {
7038        return None;
7039    }
7040    let mut out = val.clone();
7041    deep_rewrite(&mut out, &mut |e| {
7042        if let Expr::Local { var: lv, .. } = e {
7043            if lv == x {
7044                *e = init.clone();
7045            }
7046        }
7047    });
7048    Some((var, tgt, out))
7049}
7050
7051fn mk_ternary_assign(tgt: Expr, c: Expr, a: Expr, b: Expr) -> Stmt {
7052    Stmt::ExprStmt(Expr::Assign {
7053        target: Box::new(tgt),
7054        op: AssignOp::Plain,
7055        value: Box::new(Expr::Cond {
7056            c: Box::new(c),
7057            t: Box::new(a),
7058            f: Box::new(b),
7059        }),
7060    })
7061}
7062
7063/// Two var ids that render as the SAME Java local: identical base name
7064/// (generation suffixes only appear on collisions, and same-base
7065/// generations of one dex register share the rendered name) and
7066/// identical erased type (a bool/num split pair must not merge).
7067fn same_render_var(v1: u32, v2: u32, vt: &VarTable) -> bool {
7068    let (a, b) = (vt.vars.get(v1 as usize), vt.vars.get(v2 as usize));
7069    match (a, b) {
7070        (Some(x), Some(y)) => x.name == y.name && x.ty.erased() == y.ty.erased(),
7071        _ => false,
7072    }
7073}
7074
7075/// Structural equality of two statement lists MODULO SSA generations:
7076/// each arm of a diamond writes its OWN generation of the rendered
7077/// locals, so strict `==` on the tails fails even when the rendered
7078/// code is identical. Canonicalize every local id to the first id seen
7079/// for its render key (name + erased type) under a SHARED map, then
7080/// compare structurally.
7081fn canon_stmts_eq(a: &[Stmt], b: &[Stmt], vt: &VarTable) -> bool {
7082    if a.len() != b.len() {
7083        return false;
7084    }
7085    let mut map: std::collections::HashMap<String, u32> = std::collections::HashMap::default();
7086    let canon = |s: &Stmt, map: &mut std::collections::HashMap<String, u32>| -> Stmt {
7087        let mut c = s.clone();
7088        walk_stmt_exprs(&mut c, &mut |e| {
7089            deep_rewrite(e, &mut |x| {
7090                if let Expr::Local { var, .. } = x {
7091                    let key = match vt.vars.get(*var as usize) {
7092                        Some(vi) => format!("{}#{:?}", vi.name, vi.ty.erased()),
7093                        None => format!("v{}", var),
7094                    };
7095                    let id = *map.entry(key).or_insert(*var);
7096                    *x = Expr::Local {
7097                        var: id,
7098                        ty: vt.var(id).ty.clone(),
7099                    };
7100                }
7101            });
7102        });
7103        c
7104    };
7105    a.iter()
7106        .zip(b.iter())
7107        .all(|(x, y)| canon(x, &mut map) == canon(y, &mut map))
7108}
7109
7110fn fold_diamonds_in(stmts: &mut Vec<Stmt>, vt: &VarTable) {
7111    let mut i = 0usize;
7112    while i < stmts.len() {
7113        match &mut stmts[i] {
7114            Stmt::Block(v) => fold_diamonds_in(v, vt),
7115            Stmt::If { then_stmt, else_stmt, .. } => {
7116                if let Stmt::Block(v) = &mut **then_stmt {
7117                    fold_diamonds_in(v, vt);
7118                }
7119                if let Some(e) = else_stmt {
7120                    if let Stmt::Block(v) = &mut **e {
7121                        fold_diamonds_in(v, vt);
7122                    }
7123                }
7124            }
7125            _ => {}
7126        }
7127        // Shape 1: standalone if/else diamond.
7128        let shape1 = if let Stmt::If { cond, then_stmt, else_stmt: Some(el), .. } = &stmts[i] {
7129            match (arm_assign_inlined(then_stmt), arm_assign_inlined(el)) {
7130                (Some((v1, tgt, a)), Some((v2, _, b)))
7131                    if v1 == v2 && diamond_types_ok(&a, &b) =>
7132                {
7133                    Some(mk_ternary_assign(tgt, cond.clone(), a, b))
7134                }
7135                _ => None,
7136            }
7137        } else {
7138            None
7139        };
7140        if let Some(f) = shape1 {
7141            stmts[i] = f;
7142            i += 1;
7143            continue;
7144        }
7145        // Shape 3: identical-delegation diamond — BOTH arms assign the
7146        // same rendered local, then run the same tail headed by a ctor
7147        // delegation (Kotlin mask-ctor last slot: `if ((m&M)==0) { v =
7148        // x; this(.., v, ..); return; } else { v = null; this(.., v,
7149        // ..); return; }` — alipay compose TextStyle/SpanStyle family).
7150        // Fold to `v = c ? x : null;` + the shared tail: the delegation
7151        // becomes unconditional (the hotfix-guard strip downstream needs
7152        // that) and fold_default_arg_bridge can then inline the prelude
7153        // carriers into the args. Tails compare modulo SSA generation
7154        // (each arm reads its own generation of the one rendered local).
7155        // Gates: tail headed by a bare delegation, tails canonically
7156        // identical, the tail writes NEITHER generation, types ok.
7157        let shape3 = if let Stmt::If {
7158            cond,
7159            then_stmt,
7160            else_stmt: Some(el),
7161            ..
7162        } = &stmts[i]
7163        {
7164            let arm = |st: &Stmt| -> Option<((u32, Expr, Expr), Vec<Stmt>)> {
7165                let Stmt::Block(v) = st else { return None };
7166                if v.len() < 2 {
7167                    return None;
7168                }
7169                let first = single_local_assign(&v[0])?;
7170                Some((first, v[1..].to_vec()))
7171            };
7172            match (arm(then_stmt), arm(el)) {
7173                (Some(((v1, tgt1, a), mut rest_t)), Some(((v2, _t2, b), rest_e)))
7174                    if !rest_t.is_empty()
7175                        && is_bare_ctor_call(first_leaf_stmt(&rest_t[0]))
7176                        && stmts_count_writes(&rest_t, v1) == 0
7177                        && stmts_count_writes(&rest_t, v2) == 0
7178                        && diamond_types_ok(&a, &b) =>
7179                {
7180                    if v1 == v2 || same_render_var(v1, v2, vt) {
7181                        // same rendered local: fold the assign, keep the tail
7182                        if canon_stmts_eq(&rest_t, &rest_e, vt) {
7183                            Some((mk_ternary_assign(tgt1, cond.clone(), a, b), rest_t))
7184                        } else {
7185                            None
7186                        }
7187                    } else if stmts_read_var(&rest_t, v1) == 1
7188                        && stmts_read_var(&rest_e, v2) == 1
7189                        && stmts_read_var(&rest_t, v2) == 0
7190                        && stmts_read_var(&rest_e, v1) == 0
7191                    {
7192                        // SPLIT arm-local carriers (compose Span: then
7193                        // reads v23_g3, else reads v23 — different render
7194                        // names, one read each): unify by substituting
7195                        // v2 -> v1 in the else tail and requiring EXACT
7196                        // equality, then inline `c ? A1 : A2` at the
7197                        // single v1 read of the kept tail. No ternary
7198                        // assign needed — both carriers die in the fold.
7199                        let mut unified = rest_e.clone();
7200                        for st in unified.iter_mut() {
7201                            let mut c2 = st.clone();
7202                            walk_stmt_exprs(&mut c2, &mut |e| {
7203                                deep_rewrite(e, &mut |x| {
7204                                    if let Expr::Local { var, .. } = x {
7205                                        if *var == v2 {
7206                                            *x = Expr::Local {
7207                                                var: v1,
7208                                                ty: vt.var(v1).ty.clone(),
7209                                            };
7210                                        }
7211                                    }
7212                                });
7213                            });
7214                            *st = c2;
7215                        }
7216                        if unified != rest_t {
7217                            None
7218                        } else {
7219                            let tern = Expr::Cond {
7220                                c: Box::new(cond.clone()),
7221                                t: Box::new(a.clone()),
7222                                f: Box::new(b.clone()),
7223                            };
7224                            for st in rest_t.iter_mut() {
7225                                let mut c2 = st.clone();
7226                                walk_stmt_exprs(&mut c2, &mut |e| {
7227                                    deep_rewrite(e, &mut |x| {
7228                                        if let Expr::Local { var, .. } = x {
7229                                            if *var == v1 {
7230                                                *x = tern.clone();
7231                                            }
7232                                        }
7233                                    });
7234                                });
7235                                *st = c2;
7236                            }
7237                            // the If is REPLACED by the (rewritten) tail:
7238                            // no ternary assign survives — both carriers
7239                            // die in the fold.
7240                            let first = rest_t.remove(0);
7241                            Some((first, rest_t))
7242                        }
7243                    } else {
7244                        None
7245                    }
7246                }
7247                _ => None,
7248            }
7249        } else {
7250            None
7251        };
7252        if let Some((folded, rest)) = shape3 {
7253            stmts[i] = folded;
7254            stmts.splice(i + 1..i + 1, rest);
7255            i += 1;
7256            continue;
7257        }
7258        // Shape 2: adjacent `v = b;` + `if (c) { v = a; }` (no else).
7259        // The two assigns may target DIFFERENT SSA generations of the
7260        // same rendered local (sequential conditional overwrite — the
7261        // Kotlin mask-ctor `v11x = p3x; if ((mask&4)!=0) v11x = -1L;`
7262        // prelude, lark/weixin branched-defs ctor family): same base
7263        // name + same erased type renders as one Java local, so the
7264        // fold's else arm reads generation 1 and the assign targets
7265        // generation 2 — textually `v = c ? a : v`, exactly the
7266        // observable behavior of the unfolded pair.
7267        if i + 1 < stmts.len() {
7268            // An EMPTY else block counts as no-else: the structurer
7269            // emits Some(Block([])) where the source shape had none,
7270            // and cleanup (which would drop it) runs AFTER the fold —
7271            // the strict None match missed every real-world overwrite
7272            // (zero shape-2 folds on lark mask ctors).
7273            // Shape 2: adjacent `v = b;` + conditional overwrite with
7274            // NO other effect:
7275            //   form A: `if (c) { v = a; }` (else absent/empty)  -> v = c ? a : b
7276            //   form B: `if (c) { } else { v = a; }` (then empty) -> v = c ? b : a
7277            // Form B is the FOLD-time shape of every rendered no-else
7278            // overwrite: invert_empty_thens flips then/else AFTER this
7279            // pass, so the strict `else_stmt: None, overwrite-in-then`
7280            // match never saw a real-world instance (zero shape-2 folds
7281            // on lark mask ctors). The two assigns may target DIFFERENT
7282            // SSA generations of one rendered local (same base name +
7283            // same erased type): the ternary's keep-arm then reads
7284            // generation 1 — textually `v = c ? a : v`, exactly the
7285            // observable behavior of the unfolded pair (Kotlin mask-ctor
7286            // preludes: `v11x = p3x; if ((mask&4)!=0) v11x = -1L;`).
7287            let shape2 = {
7288                let s_i = single_local_assign(&stmts[i]);
7289                if let Stmt::If {
7290                    cond,
7291                    then_stmt,
7292                    else_stmt,
7293                    ..
7294                } = &stmts[i + 1]
7295                {
7296                    let then_a = single_local_assign(then_stmt);
7297                    let else_a = else_stmt.as_deref().and_then(single_local_assign);
7298                    let then_empty =
7299                        matches!(&**then_stmt, Stmt::Block(v) if v.is_empty());
7300                    let else_empty = else_stmt
7301                        .as_deref()
7302                        .map(|e| matches!(e, Stmt::Block(v) if v.is_empty()))
7303                        .unwrap_or(true);
7304                    let try_fold = |over: Option<(u32, Expr, Expr)>,
7305                                    keep_over: bool|
7306                     -> Option<Stmt> {
7307                        let (v2, tgt2, a) = over?;
7308                        let (v1, _t1, b) = s_i.as_ref()?;
7309                        if !(*v1 == v2 || same_render_var(*v1, v2, vt)) {
7310                            return None;
7311                        }
7312                        let keep = if *v1 == v2 {
7313                            b.clone()
7314                        } else {
7315                            Expr::Local {
7316                                var: *v1,
7317                                ty: vt.var(*v1).ty.clone(),
7318                            }
7319                        };
7320                        if !diamond_types_ok(&a, &keep) {
7321                            return None;
7322                        }
7323                        if keep_over {
7324                            Some(mk_ternary_assign(tgt2, cond.clone(), keep, a))
7325                        } else {
7326                            Some(mk_ternary_assign(tgt2, cond.clone(), a, keep))
7327                        }
7328                    };
7329                    if then_a.is_some() && else_empty {
7330                        try_fold(then_a, false)
7331                    } else if else_a.is_some() && then_empty {
7332                        try_fold(else_a, true)
7333                    } else {
7334                        None
7335                    }
7336                } else {
7337                    None
7338                }
7339            };
7340            // Leading-assign variant: the branch STARTS with the
7341            // overwrite and carries a tail (Kotlin mask-ctor last slot:
7342            // `str12 = null; if ((m&32)==0) { str12 = str6; this(..,
7343            // str12); return; }` — news_article CommonData/DTO family).
7344            // Fold the assign into the ternary and leave the tail in the
7345            // branch: the tail's reads see the identical value on both
7346            // paths (the ternary assigns before the branch runs). Gate:
7347            // the tail must not WRITE the var.
7348            let mut lead_fold: Option<(Stmt, Stmt)> = None;
7349            if shape2.is_none() && i + 1 < stmts.len() {
7350                let s_i2 = single_local_assign(&stmts[i]);
7351                let leading = |st: &Stmt| -> Option<((u32, Expr, Expr), Vec<Stmt>)> {
7352                    let Stmt::Block(v) = st else { return None };
7353                    if v.len() < 2 {
7354                        return None;
7355                    }
7356                    let first = single_local_assign(&v[0])?;
7357                    Some((first, v[1..].to_vec()))
7358                };
7359                if let (Some((v1, _t1, b)), Stmt::If { cond, then_stmt, else_stmt, .. }) =
7360                    (&s_i2, &stmts[i + 1])
7361                {
7362                    let else_absent = else_stmt.is_none()
7363                        || else_stmt
7364                            .as_deref()
7365                            .is_some_and(|e| matches!(e, Stmt::Block(v) if v.is_empty()));
7366                    let then_absent = matches!(&**then_stmt, Stmt::Block(v) if v.is_empty());
7367                    let try_lead = |over: Option<((u32, Expr, Expr), Vec<Stmt>)>,
7368                                    keep_over: bool|
7369                     -> Option<(Stmt, Stmt)> {
7370                        let ((v2, tgt2, a), rest) = over?;
7371                        if !(*v1 == v2 || same_render_var(*v1, v2, vt)) {
7372                            return None;
7373                        }
7374                        if stmts_count_writes(&rest, *v1) != 0
7375                            || stmts_count_writes(&rest, v2) != 0
7376                        {
7377                            return None;
7378                        }
7379                        let keep = if *v1 == v2 {
7380                            b.clone()
7381                        } else {
7382                            Expr::Local {
7383                                var: *v1,
7384                                ty: vt.var(*v1).ty.clone(),
7385                            }
7386                        };
7387                        if !diamond_types_ok(&a, &keep) {
7388                            return None;
7389                        }
7390                        let t = if keep_over {
7391                            mk_ternary_assign(tgt2, cond.clone(), keep, a)
7392                        } else {
7393                            mk_ternary_assign(tgt2, cond.clone(), a, keep)
7394                        };
7395                        let mut new_if = stmts[i + 1].clone();
7396                        if let Stmt::If { then_stmt, else_stmt, .. } = &mut new_if {
7397                            if keep_over {
7398                                *else_stmt = Some(Box::new(Stmt::Block(rest)));
7399                            } else {
7400                                **then_stmt = Stmt::Block(rest);
7401                            }
7402                        }
7403                        Some((t, new_if))
7404                    };
7405                    if else_absent {
7406                        lead_fold = try_lead(leading(then_stmt), false);
7407                    } else if then_absent {
7408                        if let Some(e) = else_stmt {
7409                            lead_fold = try_lead(leading(e), true);
7410                        }
7411                    }
7412                }
7413            }
7414            if let Some((folded, new_if)) = lead_fold {
7415                stmts[i] = folded;
7416                stmts[i + 1] = new_if;
7417                continue;
7418            }
7419            if let Some(f) = shape2 {
7420                stmts[i] = f;
7421                stmts.remove(i + 1);
7422                continue; // re-scan the folded position
7423            }
7424        }
7425        i += 1;
7426    }
7427}
7428
7429pub fn fold_bool_value_diamonds(body: &mut Stmt, vt: &VarTable) {
7430    fn is_bool_var(v: u32, vt: &VarTable) -> bool {
7431        (v as usize) < vt.vars.len()
7432            && matches!(vt.vars[v as usize].ty.erased(), JavaType::Boolean)
7433    }
7434    fn bool_const(e: &Expr) -> Option<bool> {
7435        fn cint(e: &Expr) -> Option<i32> {
7436            match e {
7437                Expr::Const(ConstVal::Int(n)) => Some(*n),
7438                _ => None,
7439            }
7440        }
7441        match e {
7442            Expr::Const(ConstVal::Int(0)) => Some(false),
7443            Expr::Const(ConstVal::Int(1)) => Some(true),
7444            // int↔bool bridge wrappings of constants: `0 != 0` is the
7445            // fallback pipeline's rendered `false` by the time the
7446            // ctor-block fold runs.
7447            Expr::Bin { op: op @ (BinOp::Eq | BinOp::Ne), l, r, .. } => {
7448                let (a, b) = (cint(l)?, cint(r)?);
7449                let eq = a == b;
7450                Some(if matches!(op, BinOp::Eq) { eq } else { !eq })
7451            }
7452            _ => None,
7453        }
7454    }
7455    fn reads_var(e: &Expr, v: u32) -> bool {
7456        let mut hit = false;
7457        visit_exprs(e, &mut |x| {
7458            if let Expr::Local { var, .. } = x {
7459                if *var == v {
7460                    hit = true;
7461                }
7462            }
7463        });
7464        hit
7465    }
7466    /// Bool-valued expression for the composite arms (&&, ||, ?:).
7467    /// Bare 0/1 constants are handled by the caller's special cases —
7468    /// inside a composite they would render as invalid `c && 1`.
7469    fn bool_valued(e: &Expr, vt: &VarTable) -> bool {
7470        fn side(e: &Expr, vt: &VarTable) -> bool {
7471            bool_const(e).is_some() || bool_valued(e, vt)
7472        }
7473        match e {
7474            Expr::Local { var, .. } => is_bool_var(*var, vt),
7475            Expr::Method { desc, .. } => desc.ret == JavaType::Boolean,
7476            Expr::InstanceOf { .. } => true,
7477            Expr::Un { op: UnOp::Not, e: x, .. } => bool_valued(x, vt) || bool_const(x).is_some(),
7478            Expr::Bin { op, l, r, .. } => match op {
7479                BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le => true,
7480                // A logical operand may itself be a 0/1 constant
7481                // (booleanize leaves mixed forms).
7482                BinOp::LogAnd | BinOp::LogOr => {
7483                    side(l, vt) && side(r, vt)
7484                }
7485                _ => false,
7486            },
7487            Expr::Cond { t, f, .. } => side(t, vt) && side(f, vt),
7488            _ => false,
7489        }
7490    }
7491    /// A branch that is exactly one plain `v = E` with E bool-valued
7492    /// (or a 0/1 constant) and not reading v — OR the forwarding PAIR
7493    /// `[u = E; v = u]` where u is single-assigned/single-read in the
7494    /// whole method: the fold consumes the pair into `v = E` (the impure
7495    /// `u = src.m()` arm shape forward_single_use cannot inline — its
7496    /// phi-ref gate rejects values reading multi-assigned prelude vars,
7497    /// and inlining here is scope-safe because E moves exactly one
7498    /// block level out with the arm's lone def consumed).
7499    fn lone_assign(
7500        b: &Stmt,
7501        v: u32,
7502        vt: &VarTable,
7503        an: &VarAnalysis,
7504        consumed: &mut HashSet<u32>,
7505    ) -> Option<Expr> {
7506        let l = flat_list(b);
7507        if l.len() == 2 {
7508            let (u, e): (u32, &Expr) = match l[0] {
7509                Stmt::LocalDef { var, init: Some(e), .. } => (*var, e),
7510                Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => {
7511                    match &**target {
7512                        Expr::Local { var, .. } => (*var, &**value),
7513                        _ => return None,
7514                    }
7515                }
7516                _ => return None,
7517            };
7518            if u == v || consumed.contains(&u) {
7519                return None;
7520            }
7521            // Whole-method single-assign/single-read: the fold drops
7522            // u's def with the arm, so ANY other read would dangle.
7523            // (Analysis counts are per-round; folds only ever REMOVE
7524            // reads elsewhere or clone E's — whose variable reads were
7525            // already counted at their def sites — so stale counts
7526            // over-approximate and the ==1 gates stay sound.)
7527            if an.assigns.get(u as usize).copied().unwrap_or(0) != 1
7528                || an.reads.get(u as usize).copied().unwrap_or(0) != 1
7529            {
7530                return None;
7531            }
7532            let (tv, rv) = match l[1] {
7533                Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => {
7534                    match (&**target, &**value) {
7535                        (Expr::Local { var: t, .. }, Expr::Local { var: r, .. }) => (*t, *r),
7536                        _ => return None,
7537                    }
7538                }
7539                Stmt::LocalDef { var, init: Some(Expr::Local { var: r, .. }), .. } => (*var, *r),
7540                _ => return None,
7541            };
7542            if tv != v || rv != u {
7543                return None;
7544            }
7545            if reads_var(e, v) || !(bool_valued(e, vt) || bool_const(e).is_some()) {
7546                return None;
7547            }
7548            consumed.insert(u);
7549            return Some(e.clone());
7550        }
7551        if l.len() != 1 {
7552            return None;
7553        }
7554        let (target, value) = match l[0] {
7555            Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => (target, value),
7556            Stmt::LocalDef { var, init: Some(value), .. } if *var == v => {
7557                if reads_var(value, v) || !(bool_valued(value, vt) || bool_const(value).is_some()) {
7558                    return None;
7559                }
7560                return Some(value.clone());
7561            }
7562            _ => return None,
7563        };
7564        let Expr::Local { var: tv, .. } = &**target else { return None };
7565        if *tv != v || reads_var(value, v) {
7566            return None;
7567        }
7568        if !(bool_valued(value, vt) || bool_const(value).is_some()) {
7569            return None;
7570        }
7571        Some((**value).clone())
7572    }
7573    fn not(e: Expr) -> Expr {
7574        Expr::Un { op: UnOp::Not, e: Box::new(e) }
7575    }
7576    /// Peephole the int↔bool bridge artifacts the fallback pipeline
7577    /// leaves in guards: `!(x != y)` → `x == y`; `(b ? 1 : 0) == 1` →
7578    /// `b`; `!(b ? 1 : 0 == 1)` chains likewise. Purely a readability
7579    /// pass — the unsimplified forms compile.
7580    fn simplify_bool(e: Expr) -> Expr {
7581        match e {
7582            Expr::Un { op: UnOp::Not, e: inner, .. } => match *inner {
7583                // Flip first, THEN re-simplify: `!((u?1:0) != 1)` must
7584                // reach the cond01 arm as `(u?1:0) == 1` → `u` (the
7585                // flip result never re-entered simplification and the
7586                // pair arm saw a Bin where it needed the bare local).
7587                Expr::Bin { op: BinOp::Ne, l, r, ty } => {
7588                    simplify_bool(Expr::Bin { op: BinOp::Eq, l, r, ty })
7589                }
7590                Expr::Bin { op: BinOp::Eq, l, r, ty } => {
7591                    simplify_bool(Expr::Bin { op: BinOp::Ne, l, r, ty })
7592                }
7593                other => Expr::Un { op: UnOp::Not, e: Box::new(simplify_bool(other)) },
7594            },
7595            Expr::Bin { op: op @ (BinOp::Eq | BinOp::Ne), l, r, ty } => {
7596                // (c ? 1 : 0) <op> 1  →  c / !c ; <op> 0 → !c / c
7597                fn cond01(e: &Expr) -> Option<Expr> {
7598                    if let Expr::Cond { c, t, f } = e {
7599                        if matches!(&**t, Expr::Const(ConstVal::Int(1)))
7600                            && matches!(&**f, Expr::Const(ConstVal::Int(0)))
7601                        {
7602                            return Some((**c).clone());
7603                        }
7604                    }
7605                    None
7606                }
7607                let (lc, rc) = (cond01(&l), cond01(&r));
7608                match (lc, rc) {
7609                    (Some(c), None) => match (&*r, op) {
7610                        (Expr::Const(ConstVal::Int(1)), BinOp::Eq) => c,
7611                        (Expr::Const(ConstVal::Int(1)), BinOp::Ne) => not(c),
7612                        (Expr::Const(ConstVal::Int(0)), BinOp::Eq) => not(c),
7613                        (Expr::Const(ConstVal::Int(0)), BinOp::Ne) => c,
7614                        _ => Expr::Bin { op, l: Box::new(c), r, ty },
7615                    },
7616                    (None, Some(c)) => match (&*l, op) {
7617                        (Expr::Const(ConstVal::Int(1)), BinOp::Eq) => c,
7618                        (Expr::Const(ConstVal::Int(1)), BinOp::Ne) => not(c),
7619                        (Expr::Const(ConstVal::Int(0)), BinOp::Eq) => not(c),
7620                        (Expr::Const(ConstVal::Int(0)), BinOp::Ne) => c,
7621                        _ => Expr::Bin { op, l, r: Box::new(c), ty },
7622                    },
7623                    _ => Expr::Bin { op, l, r, ty },
7624                }
7625            }
7626            other => other,
7627        }
7628    }
7629    fn bin(op: BinOp, l: Expr, r: Expr) -> Expr {
7630        Expr::Bin { op, l: Box::new(l), r: Box::new(r), ty: None }
7631    }
7632
7633    for _round in 0..4 {
7634        let mut changed = false;
7635        let mut analysis = VarAnalysis {
7636            assigns: Vec::new(),
7637            reads: Vec::new(),
7638            values: Vec::new(),
7639        };
7640        analyze_vars(body, &mut analysis);
7641        let mut consumed: HashSet<u32> = HashSet::default();
7642        walk_mut_deep(body, &mut |st| {
7643            let Stmt::Block(list) = st else { return };
7644            let mut i = 0usize;
7645            while i + 1 < list.len() {
7646                let folded = try_at(list, i, vt, &is_bool_var, &bool_const, &reads_var, &lone_assign, &analysis, &mut consumed);
7647                match folded {
7648                    Some(expr) => {
7649                        let is_def = matches!(&list[i], Stmt::LocalDef { .. });
7650                        let var = match &list[i] {
7651                            Stmt::LocalDef { var, .. } => *var,
7652                            Stmt::ExprStmt(Expr::Assign { target, .. }) => {
7653                                match &**target { Expr::Local { var, .. } => *var, _ => unreachable!() }
7654                            }
7655                            _ => unreachable!(),
7656                        };
7657                        let (is_final, force_type) = match &list[i] {
7658                            Stmt::LocalDef { is_final, force_type, .. } => (*is_final, *force_type),
7659                            _ => (false, false),
7660                        };
7661                        list[i] = if is_def {
7662                            Stmt::LocalDef { var, init: Some(expr), is_final, force_type }
7663                        } else {
7664                            Stmt::ExprStmt(Expr::Assign {
7665                                target: Box::new(Expr::Local {
7666                                    var,
7667                                    ty: TypeRef::J(JavaType::Boolean),
7668                                }),
7669                                op: AssignOp::Plain,
7670                                value: Box::new(expr),
7671                            })
7672                        };
7673                        list.remove(i + 1);
7674                        changed = true;
7675                        // stay at i: a preceding base may now pair with
7676                        // nothing new, but a FOLLOWING diamond may chain.
7677                    }
7678                    None => i += 1,
7679                }
7680            }
7681        });
7682        if !changed {
7683            break;
7684        }
7685    }
7686
7687    #[allow(clippy::too_many_arguments, clippy::type_complexity)]
7688    fn try_at(
7689        list: &[Stmt],
7690        i: usize,
7691        vt: &VarTable,
7692        is_bool_var: &dyn Fn(u32, &VarTable) -> bool,
7693        bool_const: &dyn Fn(&Expr) -> Option<bool>,
7694        reads_var: &dyn Fn(&Expr, u32) -> bool,
7695        lone_assign: &dyn Fn(&Stmt, u32, &VarTable, &VarAnalysis, &mut HashSet<u32>) -> Option<Expr>,
7696        an: &VarAnalysis,
7697        consumed: &mut HashSet<u32>,
7698    ) -> Option<Expr> {
7699        // s1: `v = <0|1>` (Assign or LocalDef), v Boolean-typed.
7700        let (v, base) = match &list[i] {
7701            Stmt::ExprStmt(Expr::Assign { target, op: AssignOp::Plain, value }) => {
7702                match &**target {
7703                    Expr::Local { var, .. } => {
7704                        // The value may be a wrapped constant (`0 != 0`
7705                        // — the fallback pipeline's rendered false).
7706                        let b = bool_const(value)?;
7707                        if !is_bool_var(*var, vt) { return None; }
7708                        (*var, b)
7709                    }
7710                    _ => return None,
7711                }
7712            }
7713            Stmt::LocalDef { var, init: Some(e), .. } => {
7714                let b = bool_const(e)?;
7715                if !is_bool_var(*var, vt) { return None; }
7716                (*var, b)
7717            }
7718            _ => return None,
7719        };
7720        let Stmt::If { cond, then_stmt, else_stmt } = &list[i + 1] else {
7721            return None;
7722        };
7723        if reads_var(cond, v) {
7724            return None;
7725        }
7726        let mut touches_v = false;
7727        visit_exprs(cond, &mut |x| {
7728            if let Expr::Assign { target, .. } = x {
7729                if let Expr::Local { var, .. } = &**target {
7730                    if *var == v { touches_v = true; }
7731                }
7732            }
7733        });
7734        if touches_v {
7735            return None;
7736        }
7737        // The structurer emits inverted diamonds (`if (c) {} else { v = E }`,
7738        // empty-then) — normalize by negating the guard.
7739        let (cond_n, then_arm, else_arm): (Expr, &Stmt, Option<&Stmt>) =
7740            if flat_list(then_stmt).is_empty() && else_stmt.is_some() {
7741                (not(cond.clone()), else_stmt.as_ref().unwrap(), None)
7742            } else {
7743                (cond.clone(), then_stmt.as_ref(), else_stmt.as_deref())
7744            };
7745        let e1 = lone_assign(then_arm, v, vt, an, consumed)?;
7746        let e2 = match else_arm {
7747            None => None,
7748            Some(b) => Some(lone_assign(b, v, vt, an, consumed)?),
7749        };
7750        let e1c = bool_const(&e1);
7751        let c = simplify_bool(cond_n);
7752        Some(match e2 {
7753            None => {
7754                if !base {
7755                    match e1c {
7756                        Some(true) => c,
7757                        Some(false) => return None, // no-op diamond: guard effects would drop
7758                        None => bin(BinOp::LogAnd, c, e1),
7759                    }
7760                } else {
7761                    match e1c {
7762                        Some(false) => not(c),
7763                        Some(true) => return None,
7764                        None => bin(BinOp::LogOr, not(c), e1),
7765                    }
7766                }
7767            }
7768            Some(e2) => {
7769                // Full diamond: both arms assign — the base is dead.
7770                if e1 == e2 {
7771                    return None; // both arms equal: folding drops guard effects
7772                }
7773                Expr::Cond { c: Box::new(c), t: Box::new(e1), f: Box::new(e2) }
7774            }
7775        })
7776    }
7777}
7778
7779pub fn idiom_compounds(body: &mut Stmt, vt: &VarTable) {
7780    walk_mut_deep(body, &mut |st| {
7781        let Stmt::ExprStmt(e) = st else { return };
7782        let Expr::Assign { target, op: AssignOp::Plain, value } = &mut *e else {
7783            return;
7784        };
7785        // Target: a local, or a bare field (static / this — no complex
7786        // receiver whose re-evaluation a compound would elide).
7787        enum Tgt {
7788            Loc(u32),
7789            Fld,
7790        }
7791        let tgt = match &**target {
7792            Expr::Local { var, .. } => Tgt::Loc(*var),
7793            Expr::Field { owner, .. }
7794                if owner.is_none()
7795                    || matches!(&**owner.as_ref().unwrap(), Expr::This) =>
7796            {
7797                Tgt::Fld
7798            }
7799            _ => return,
7800        };
7801        let Expr::Bin { op, l, r, .. } = &mut **value else {
7802            return;
7803        };
7804        // The left operand must be the SAME storage as the target.
7805        match (&tgt, &**l) {
7806            (Tgt::Loc(v), Expr::Local { var, .. }) if *var == *v => {}
7807            (Tgt::Fld, Expr::Field { cls: c2, name: n2, .. }) => {
7808                let Expr::Field { cls: c1, name: n1, .. } = &**target else {
7809                    return;
7810                };
7811                if c1 != c2 || n1 != n2 {
7812                    return;
7813                }
7814            }
7815            _ => return,
7816        }
7817        // ±1 constant on a numeric var → post-inc/dec.
7818        let one = match &**r {
7819            Expr::Const(ConstVal::Int(1)) => Some(1i64),
7820            Expr::Const(ConstVal::Int(-1)) => Some(-1),
7821            Expr::Const(ConstVal::Long(1)) => Some(1),
7822            Expr::Const(ConstVal::Long(-1)) => Some(-1),
7823            _ => None,
7824        };
7825        if *op == BinOp::Add || *op == BinOp::Sub {
7826            if let Some(mut d) = one {
7827                if *op == BinOp::Sub {
7828                    d = -d;
7829                }
7830                let tgt_ty = match &tgt {
7831                    Tgt::Loc(v) => vt.var(*v).ty.erased(),
7832                    Tgt::Fld => {
7833                        let Expr::Field { ty, .. } = &**target else {
7834                            return;
7835                        };
7836                        ty.erased()
7837                    }
7838                };
7839                let numeric = matches!(
7840                    tgt_ty,
7841                    JavaType::Int
7842                        | JavaType::Long
7843                        | JavaType::Short
7844                        | JavaType::Byte
7845                        | JavaType::Char
7846                        | JavaType::Float
7847                        | JavaType::Double
7848                );
7849                if numeric {
7850                    let wide = matches!(tgt_ty, JavaType::Long);
7851                    let inner = match &tgt {
7852                        Tgt::Loc(v) => Expr::Local {
7853                            var: *v,
7854                            ty: vt.var(*v).ty.clone(),
7855                        },
7856                        Tgt::Fld => (**target).clone(),
7857                    };
7858                    *e = Expr::PostIncDec {
7859                        e: Box::new(inner),
7860                        delta: d,
7861                        wide,
7862                    };
7863                    return;
7864                }
7865            }
7866        }
7867        let cop = match op {
7868            BinOp::Add => AssignOp::Add,
7869            BinOp::Sub => AssignOp::Sub,
7870            BinOp::Mul => AssignOp::Mul,
7871            BinOp::Div => AssignOp::Div,
7872            BinOp::Rem => AssignOp::Rem,
7873            BinOp::Shl => AssignOp::Shl,
7874            BinOp::Shr => AssignOp::Shr,
7875            BinOp::Ushr => AssignOp::Ushr,
7876            BinOp::And => AssignOp::And,
7877            BinOp::Or => AssignOp::Or,
7878            BinOp::Xor => AssignOp::Xor,
7879            _ => return,
7880        };
7881        let rhs = std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
7882        let new_target = match &tgt {
7883            Tgt::Loc(v) => Box::new(Expr::Local {
7884                var: *v,
7885                ty: vt.var(*v).ty.clone(),
7886            }),
7887            Tgt::Fld => target.clone(),
7888        };
7889        *e = Expr::Assign {
7890            target: new_target,
7891            op: cop,
7892            value: rhs,
7893        };
7894    });
7895}
7896
7897pub fn fix_bool_xor(body: &mut Stmt, vt: &VarTable, ret_bool: bool) {
7898    fn is_one(e: &Expr) -> bool {
7899        matches!(e, Expr::Const(ConstVal::Int(1)))
7900    }
7901    fn bty(e: &Expr, vt: &VarTable) -> bool {
7902        match e {
7903            Expr::Local { var, .. } => {
7904                matches!(vt.var(*var).ty.erased(), JavaType::Boolean)
7905            }
7906            // A bitwise bin over all-boolean sides IS boolean: the
7907            // embedded bin ty stays frozen Int from the or-int lift
7908            // (booleanize retypes the vt, not bin nodes), so the flat
7909            // lookup saw `(b1|b2) | i3` as int-sided and skipped the
7910            // mixed rewrite — rendered `boolean | int` (uuyc i5/a_3
7911            // `(v37|v38|v39)==0` ×21; news ×995 二元运算符 '|').
7912            // Mirrors emit.rs bool_ish_vt's recursion so the rewrite
7913            // here and the logical-form render there agree.
7914            Expr::Bin {
7915                op: BinOp::And | BinOp::Or | BinOp::Xor,
7916                l,
7917                r,
7918                ..
7919            } => bty(l, vt) && bty(r, vt),
7920            other => matches!(other.type_ref().erased(), JavaType::Boolean),
7921        }
7922    }
7923    // Int-typed `x ^ 1` in a BOOLEAN sink is the compiler's `!x` over a
7924    // 0/1 slot: rewrite to `x == 0` (the booleanize return-wrap misses
7925    // Xor shapes — weibo ArraysKt `return v2 ^ 1;` against a boolean
7926    // return, "int无法转换为boolean").
7927    fn inv_of_xor(x: &Expr, vt: &VarTable) -> Option<Expr> {
7928        if let Expr::Bin { op: BinOp::Xor, l, r, .. } = x {
7929            let (int_side, one_side) = if is_one(r) {
7930                (l, r)
7931            } else if is_one(l) {
7932                (r, l)
7933            } else {
7934                return None;
7935            };
7936            let _ = one_side;
7937            if !bty(int_side, vt) {
7938                return Some(Expr::Bin {
7939                    op: BinOp::Eq,
7940                    l: int_side.clone(),
7941                    r: Box::new(Expr::Const(ConstVal::Int(0))),
7942                    ty: Some(TypeRef::J(JavaType::Boolean)),
7943                });
7944            }
7945        }
7946        None
7947    }
7948    let tgt_bool = |target: &Expr, vt: &VarTable| -> bool {
7949        match target {
7950            Expr::Local { var, .. } => matches!(vt.var(*var).ty.erased(), JavaType::Boolean),
7951            Expr::Field { ty, .. } => matches!(ty.erased(), JavaType::Boolean),
7952            _ => false,
7953        }
7954    };
7955    walk_mut_deep(body, &mut |st| match st {
7956        Stmt::Return(Some(e)) if ret_bool => {
7957            if let Some(r) = inv_of_xor(e, vt) {
7958                *e = r;
7959            }
7960        }
7961        Stmt::ExprStmt(Expr::Assign { target, value, op: AssignOp::Plain, .. }) => {
7962            if tgt_bool(target, vt) {
7963                if let Some(r) = inv_of_xor(value, vt) {
7964                    **value = r;
7965                }
7966            }
7967        }
7968        Stmt::LocalDef { var, init: Some(value), .. }
7969            if matches!(vt.var(*var).ty.erased(), JavaType::Boolean) =>
7970        {
7971            if let Some(r) = inv_of_xor(value, vt) {
7972                *value = r;
7973            }
7974        }
7975        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
7976            if let Some(r) = inv_of_xor(cond, vt) {
7977                *cond = r;
7978            }
7979        }
7980        _ => {}
7981    });
7982    fn ity(e: &Expr, vt: &VarTable) -> bool {
7983        matches!(
7984            match e {
7985                Expr::Local { var, .. } => vt.var(*var).ty.erased(),
7986                other => other.type_ref().erased(),
7987            },
7988            JavaType::Int | JavaType::Short | JavaType::Byte | JavaType::Char
7989        )
7990    }
7991    /// Mixed-kind bitwise rewrite: Kotlin's non-short-circuit
7992    /// `or`/`and` over 0/1 ints mixes generations once booleanize
7993    /// converts one side (`delete | delete2` — "boolean无法转换为int" at
7994    /// the OPERAND, weixin SQLiteDatabase ×3.3k), and `b ^ 1` is `!b`.
7995    /// The int side is a 0/1 encoding: compare it, keeping the chain
7996    /// boolean end to end. ONLY legal into a BOOLEAN sink — ungated, it
7997    /// rewrote int-sink `v6 = v5x | 4` into `v5x | 4 != 0` assigned to
7998    /// an int var (boolean无法转换为int, weixin yq5/c ×2.1k family).
7999    fn mixed_rewrite(e: &mut Expr, vt: &VarTable) {
8000        deep_rewrite(e, &mut |x| {
8001            if let Expr::Bin { op, l, r, ty } = x {
8002                match op {
8003                    BinOp::Xor => {
8004                        if is_one(r) && bty(l, vt) {
8005                            let taken =
8006                                std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
8007                            *x = Expr::Un { op: UnOp::Not, e: taken };
8008                        } else if is_one(l) && bty(r, vt) {
8009                            let taken =
8010                                std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
8011                            *x = Expr::Un { op: UnOp::Not, e: taken };
8012                        }
8013                    }
8014                    BinOp::Or | BinOp::And => {
8015                        if bty(l, vt) && ity(r, vt) {
8016                            let taken =
8017                                std::mem::replace(r, Box::new(Expr::Const(ConstVal::Null)));
8018                            **r = Expr::Bin {
8019                                op: BinOp::Ne,
8020                                l: taken,
8021                                r: Box::new(Expr::Const(ConstVal::Int(0))),
8022                                ty: Some(TypeRef::J(JavaType::Boolean)),
8023                            };
8024                            *ty = Some(TypeRef::J(JavaType::Boolean));
8025                        } else if ity(l, vt) && bty(r, vt) {
8026                            let taken =
8027                                std::mem::replace(l, Box::new(Expr::Const(ConstVal::Null)));
8028                            **l = Expr::Bin {
8029                                op: BinOp::Ne,
8030                                l: taken,
8031                                r: Box::new(Expr::Const(ConstVal::Int(0))),
8032                                ty: Some(TypeRef::J(JavaType::Boolean)),
8033                            };
8034                            *ty = Some(TypeRef::J(JavaType::Boolean));
8035                        }
8036                    }
8037                    _ => {}
8038                }
8039            }
8040        });
8041    }
8042    // Boolean sinks: bool-typed assign/def targets, boolean-method
8043    // returns, and loop/branch conditions.
8044    walk_mut_deep(body, &mut |st| match st {
8045        Stmt::Return(Some(e)) if ret_bool => mixed_rewrite(e, vt),
8046        Stmt::ExprStmt(Expr::Assign { target, value, op: AssignOp::Plain, .. }) => {
8047            if tgt_bool(target, vt) {
8048                mixed_rewrite(value, vt);
8049            }
8050        }
8051        Stmt::LocalDef { var, init: Some(e), .. }
8052            if matches!(vt.var(*var).ty.erased(), JavaType::Boolean) =>
8053        {
8054            mixed_rewrite(e, vt);
8055        }
8056        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
8057            mixed_rewrite(cond, vt);
8058        }
8059        _ => {}
8060    });
8061    // A conditional's test is a boolean sink wherever it sits (including
8062    // inside int-sink statements the walk above skips).
8063    walk_stmt_exprs(body, &mut |e| {
8064        deep_rewrite(e, &mut |x| {
8065            if let Expr::Cond { c, .. } = x {
8066                mixed_rewrite(c, vt);
8067            }
8068        });
8069    });
8070}
8071
8072/// Boolean inference: vars only ever assigned 0/1/comparisons/booleans and
8073/// read in conditions become `boolean`, with `v != 0` → `v` in conditions.
8074/// Returns the number of vars converted (0 = nothing changed — the
8075/// caller skips the post-booleanize re-split, which only has work when
8076/// a conversion exposed a mixed-kind register).
8077pub fn booleanize(vt: &mut VarTable, body: &mut Stmt, ret_bool: bool) -> usize {
8078    // Booleans propagate through local chains (`v17 = v24` where v24
8079    // itself became boolean in the first round) — iterate to a fixpoint;
8080    // the common corpus converts nothing and exits after one round.
8081    // The deref disqualifier set is structural (positions, not types) —
8082    // compute ONCE per call, not per round (12 full walks on big
8083    // methods cost weibo +60% wall).
8084    let n0 = vt.vars.len();
8085    let mut derefed = vec![false; n0];
8086    mark_derefs(body, &mut derefed);
8087    let mut total = 0usize;
8088    for _ in 0..4 {
8089        let n = booleanize_round(vt, body, ret_bool, &derefed);
8090        if n == 0 {
8091            break;
8092        }
8093        total += n;
8094    }
8095    total
8096}
8097
8098/// Mark vars used in DEREF positions (field/method receiver, array
8099/// base) — allocation-free recursion (visit_all_exprs allocates a
8100/// child Vec per statement; two callers run this per method).
8101fn mark_deref_expr(e: &Expr, derefed: &mut [bool]) {
8102    match e {
8103        Expr::Field { owner: Some(o), is_static: false, .. }
8104        | Expr::Method { owner: Some(o), .. } => {
8105            if let Expr::Local { var, .. } = &**o {
8106                if (*var as usize) < derefed.len() {
8107                    derefed[*var as usize] = true;
8108                }
8109            }
8110        }
8111        Expr::ArrayIndex { array, .. } => {
8112            if let Expr::Local { var, .. } = &**array {
8113                if (*var as usize) < derefed.len() {
8114                    derefed[*var as usize] = true;
8115                }
8116            }
8117        }
8118        _ => {}
8119    }
8120    for_each_child(e, &mut |c| mark_deref_expr(c, derefed));
8121}
8122
8123fn mark_derefs(body: &Stmt, derefed: &mut [bool]) {
8124    walk_all(body, &mut |st| {
8125        match st {
8126            Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
8127                mark_deref_expr(e, derefed);
8128            }
8129            Stmt::Return(Some(e)) => mark_deref_expr(e, derefed),
8130            Stmt::LocalDef { init: Some(e), .. } => mark_deref_expr(e, derefed),
8131            Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
8132                mark_deref_expr(cond, derefed);
8133            }
8134            Stmt::For { init, .. } => {
8135                for x in init {
8136                    mark_derefs(x, derefed);
8137                }
8138            }
8139            Stmt::Switch { selector, .. } => mark_deref_expr(selector, derefed),
8140            Stmt::ForEach { iterable, .. } => mark_deref_expr(iterable, derefed),
8141            _ => {}
8142        }
8143    });
8144}
8145
8146fn booleanize_round(
8147    vt: &mut VarTable,
8148    body: &mut Stmt,
8149    ret_bool: bool,
8150    derefed: &[bool],
8151) -> usize {
8152    let n = vt.vars.len();
8153    let mut in_cond = vec![false; n];
8154
8155    // Condition uses: `v == 0` / `v != 0` if-conditions and comparison
8156    // operands inside assigned values.
8157    walk_all(body, &mut |st| {
8158        let cond: Option<&Expr> = match st {
8159            Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
8160                Some(cond)
8161            }
8162            _ => None,
8163        };
8164        if let Some(c) = cond {
8165            if let Expr::Bin {
8166                op: BinOp::Eq | BinOp::Ne,
8167                l,
8168                r,
8169                ..
8170            } = c
8171            {
8172                for (x, other) in [(l, r), (r, l)] {
8173                    if let (Expr::Local { var, .. }, Expr::Const(ConstVal::Int(0))) =
8174                        (&**x, &**other)
8175                    {
8176                        if (*var as usize) < n {
8177                            in_cond[*var as usize] = true;
8178                        }
8179                    }
8180                }
8181            }
8182        }
8183        // A local RETURNED from a boolean method is boolean-typed even
8184        // though it never appears in a condition (`int v9 = 0/1 … return
8185        // v9;` — returning an int from `boolean check()` is a compile
8186        // error; lab package Obf.check).
8187        if ret_bool {
8188            if let Stmt::Return(Some(Expr::Local { var, .. })) = st {
8189                if (*var as usize) < n {
8190                    in_cond[*var as usize] = true;
8191                }
8192            }
8193        }
8194        // A local STORED into a boolean field is boolean-typed even though
8195        // it never appears in a condition (`b = v1` where `b` is a boolean
8196        // field, `v1` an int 0/1 local — q.java static init). The emit
8197        // layer already coerces a boolean-target assign through expr_bool,
8198        // but that renders a bare int LOCAL unchanged, so the local itself
8199        // must be booleanized (its 0/1 assigns then print false/true).
8200        if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = st {
8201            if let Expr::Field { ty, .. } = &**target {
8202                if ty.erased() == JavaType::Boolean {
8203                    if let Expr::Local { var, .. } = &**value {
8204                        if (*var as usize) < n {
8205                            in_cond[*var as usize] = true;
8206                        }
8207                    }
8208                }
8209            }
8210        }
8211        let val: Option<&Expr> = match st {
8212            Stmt::ExprStmt(Expr::Assign { value, .. }) => Some(value),
8213            // Bare expression statements (e.g. `q(v117);`) so a local
8214            // passed to a boolean parameter is caught below.
8215            Stmt::ExprStmt(e) => Some(e),
8216            Stmt::LocalDef { init: Some(e), .. } => Some(e),
8217            Stmt::Return(Some(e)) => Some(e),
8218            _ => None,
8219        };
8220        if let Some(v) = val {
8221            visit_exprs(v, &mut |x| {
8222                // A local passed as a BOOLEAN parameter is boolean-typed
8223                // (`q(v117)` where q's param is boolean, v117 an int 0/1 —
8224                // f8/b.java). The DEX passes booleans as 0/1 ints, so a
8225                // boolean param slot is authoritative for the argument's
8226                // type.
8227                if let Expr::Method { desc, args, .. } = x {
8228                    for (i, a) in args.iter().enumerate() {
8229                        if desc.args.get(i) == Some(&JavaType::Boolean) {
8230                            if let Expr::Local { var, .. } = a {
8231                                if (*var as usize) < n {
8232                                    in_cond[*var as usize] = true;
8233                                }
8234                            }
8235                        }
8236                    }
8237                }
8238                if let Expr::Bin { op, l, r, .. } = x {
8239                    if matches!(
8240                        op,
8241                        BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
8242                    ) {
8243                        for side in [l, r] {
8244                            if let Expr::Local { var, .. } = &**side {
8245                                if (*var as usize) < n {
8246                                    in_cond[*var as usize] = true;
8247                                }
8248                            }
8249                        }
8250                    }
8251                    // Kotlin/d8 merge booleans through INT bitwise ops
8252                    // (`v3 | obj instanceof g`) == 0 — the other side's
8253                    // static type being boolean makes this a boolean
8254                    // context for the local (a genuine int bitwise
8255                    // expression never has a boolean operand).
8256                    if matches!(op, BinOp::Or | BinOp::And) {
8257                        let l_bool = l.type_ref().erased() == JavaType::Boolean;
8258                        let r_bool = r.type_ref().erased() == JavaType::Boolean;
8259                        if l_bool != r_bool {
8260                            let side = if l_bool { r } else { l };
8261                            if let Expr::Local { var, .. } = &**side {
8262                                if (*var as usize) < n {
8263                                    in_cond[*var as usize] = true;
8264                                }
8265                            }
8266                        }
8267                    }
8268                }
8269            });
8270        }
8271    });
8272
8273    // A var whose every assignment value is boolean-shaped AND which is
8274    // referenced in conditions becomes boolean.
8275    // (Perf: this used to re-walk the whole statement tree ONCE PER VAR —
8276    // O(vars × stmts); on R8-merged monsters (2000+ vars, 15k statements)
8277    // that was tens of millions of visits. One pass collects the same
8278    // (any, all_bool) facts for every var.)
8279    let mut assigned_any = vec![false; n];
8280    let mut all_bool = vec![true; n];
8281    let mut edges: Vec<(usize, usize)> = Vec::new();
8282    walk_all(body, &mut |st| {
8283        let (var, value) = match st {
8284            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => match &**target {
8285                Expr::Local { var, .. } => (var, &**value),
8286                _ => return,
8287            },
8288            Stmt::LocalDef {
8289                var, init: Some(e), ..
8290            } => (var, e),
8291            _ => return,
8292        };
8293        let i = *var as usize;
8294        if i < n {
8295            assigned_any[i] = true;
8296            if !is_boolean_valued(value, vt) {
8297                all_bool[i] = false;
8298            }
8299            // Chain edges for the context closure below: `v17 = v24`
8300            // links the two locals; when the TARGET is in a boolean
8301            // context, the source inherits it (its only reader is the
8302            // boolean-shaped chain). Collected as edges because the
8303            // target's context can be discovered anywhere in the tree.
8304            if let Expr::Local { var: src, .. } = value {
8305                if (*src as usize) < n {
8306                    edges.push((i, *src as usize));
8307                }
8308            }
8309        }
8310    });
8311    // Context closure along local chains: a var in a boolean context
8312    // pushes that context through every local it is assigned from
8313    // (`return v17` — v17 = v24 — v24 = 0/1) — the statement order
8314    // cannot be relied on (the return may follow the assignment).
8315    loop {
8316        let mut changed = false;
8317        for &(tgt, src) in &edges {
8318            if in_cond[tgt] && !in_cond[src] && all_bool[src] {
8319                in_cond[src] = true;
8320                changed = true;
8321            }
8322            // FORWARD through pure copies: `v21 = v17` with v17 boolean
8323            // makes v21 boolean when ALL of v21's assignments are
8324            // bool-shaped (the loop-state save pattern — weixin u2/f's
8325            // v62 → v17 → v21 chains left the copy targets int while
8326            // the accumulator booleanized).
8327            if in_cond[src] && !in_cond[tgt] && all_bool[tgt] {
8328                in_cond[tgt] = true;
8329                changed = true;
8330            }
8331        }
8332        if !changed {
8333            break;
8334        }
8335    }
8336    let reads = count_locals_stmts(std::slice::from_ref(body));
8337    let mut boolean_vars: HashSet<u32> = HashSet::default();
8338    for i in 0..n {
8339        if vt.vars[i].is_param {
8340            continue;
8341        }
8342        if derefed[i] {
8343            continue;
8344        }
8345        if !in_cond[i] && !(reads.get(&(i as u32)).copied().unwrap_or(0) == 0) {
8346            continue;
8347        }
8348        if assigned_any[i] && all_bool[i] {
8349            boolean_vars.insert(i as u32);
8350        }
8351    }
8352    if boolean_vars.is_empty() {
8353        return 0;
8354    }
8355    for v in &boolean_vars {
8356        vt.vars[*v as usize].ty = TypeRef::J(JavaType::Boolean);
8357    }
8358    let types: Vec<&TypeRef> = vt.vars.iter().map(|v| &v.ty).collect();
8359    rewrite_exprs(body, &mut |e| {
8360        deep_rewrite(e, &mut |x| {
8361            if let Expr::Local { var, ty } = x {
8362                if let Some(want) = types.get(*var as usize) {
8363                    if ty != *want {
8364                        *ty = (*want).clone();
8365                    }
8366                }
8367            }
8368        });
8369    });
8370
8371    // Condition folding: `b != 0` → `b`, `b == 0` → `!b` (boolean vars).
8372    fold_bool_conditions(body, &boolean_vars);
8373    boolean_vars.len()
8374}
8375
8376fn is_boolean_valued(e: &Expr, vt: &VarTable) -> bool {
8377    match e {
8378        Expr::Const(ConstVal::Int(0)) | Expr::Const(ConstVal::Int(1)) => true,
8379        // Kotlin's non-short-circuit boolean `or`/`and` compiles to `|`/`&`
8380        // over boolean locals (`v15 = delete | delete2 | ..`). EITHER side
8381        // boolean suffices: `int | boolean` is illegal in source — it is
8382        // always a not-yet-booleanized side of a boolean chain (the
8383        // loop-carried accumulator `v15 = v20; v20 = v15 | delete3` can
8384        // never seed its all_bool fixpoint otherwise).
8385        Expr::Bin { op: BinOp::Or | BinOp::And, l, r, .. } => {
8386            is_boolean_valued(l, vt) || is_boolean_valued(r, vt)
8387        }
8388        // Kotlin's `!x` lowers to `x ^ 1`; at booleanize time the value
8389        // is still the Xor (fix_bool_xor runs later), so an int-typed
8390        // generation receiving it never retyped (weixin v2/i's
8391        // `int v264_g160_g7 = !v200`). BOTH sides must be bool-shaped:
8392        // `bool ^ 1` qualifies (Const 0/1 is boolean-valued above),
8393        // a genuine int `flags ^ 1` does not (flags stays Int in vt).
8394        // MUST precede the generic Bin arm (which would swallow Xor).
8395        Expr::Bin {
8396            op: BinOp::Xor,
8397            l,
8398            r,
8399            ..
8400        } => is_boolean_valued(l, vt) && is_boolean_valued(r, vt),
8401        Expr::Bin { op, .. } => matches!(
8402            op,
8403            BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
8404        ),
8405        Expr::InstanceOf { .. } => true,
8406        // The VarTable is the authority: the embedded Local ty is a
8407        // lift-time snapshot and goes STALE across booleanize rounds —
8408        // a converted copy source kept its partners int (`v150 = v131`
8409        // boolean→int, weixin ConstraintLayout ×1.4k files) because the
8410        // all_bool fixpoint consulted the stale Int.
8411        Expr::Local { var, ty } => {
8412            let t = if (*var as usize) < vt.vars.len() {
8413                vt.var(*var).ty.erased()
8414            } else {
8415                ty.erased()
8416            };
8417            t == JavaType::Boolean
8418        }
8419        Expr::Method { desc, .. } => desc.ret == JavaType::Boolean,
8420        Expr::Cond { t, f, .. } => is_boolean_valued(t, vt) && is_boolean_valued(f, vt),
8421        Expr::Un { op: UnOp::Not, .. } => true,
8422        // Field ty comes from the dex descriptor — authoritative, not a
8423        // lift snapshot (`!w1x.i` arrives here still as `w1x.i ^ 1`; the
8424        // Xor arm needs this side bool-shaped).
8425        Expr::Field { ty, .. } => ty.erased() == JavaType::Boolean,
8426        _ => false,
8427    }
8428}
8429
8430fn fold_bool_conditions(s: &mut Stmt, bv: &HashSet<u32>) {
8431    let fold = |e: &mut Expr| {
8432        deep_rewrite(e, &mut |x| {
8433            if let Expr::Bin { op, l, r, .. } = x {
8434                let (var_side, const_side) = match (&**l, &**r) {
8435                    (Expr::Local { .. }, Expr::Const(ConstVal::Int(_))) => (l, 0),
8436                    (Expr::Const(ConstVal::Int(_)), Expr::Local { .. }) => (r, 1),
8437                    _ => return,
8438                };
8439                let var = match &**var_side {
8440                    Expr::Local { var, .. } => *var,
8441                    _ => return,
8442                };
8443                if !bv.contains(&var) {
8444                    return;
8445                }
8446                let _ = const_side;
8447                match op {
8448                    BinOp::Ne => {
8449                        let inner = var_side.clone();
8450                        *x = *inner;
8451                    }
8452                    BinOp::Eq => {
8453                        let inner = var_side.clone();
8454                        *x = Expr::Un {
8455                            op: UnOp::Not,
8456                            e: Box::new(*inner),
8457                        };
8458                    }
8459                    _ => {}
8460                }
8461            }
8462        });
8463    };
8464    match s {
8465        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => fold(cond),
8466        _ => {}
8467    }
8468    walk_mut_deep(s, &mut |st| {
8469        let cond = match st {
8470            Stmt::If { cond, .. } => Some(cond),
8471            Stmt::While { cond, .. } => Some(cond),
8472            Stmt::DoWhile { cond, .. } => Some(cond),
8473            _ => None,
8474        };
8475        if let Some(c) = cond {
8476            fold(c);
8477        }
8478    });
8479}
8480
8481/// Normalize `1 == v` comparisons to `v == 1` (d8 emits const-first if-eq).
8482pub fn flip_const_compares(s: &mut Stmt) {
8483    rewrite_exprs(s, &mut |e| {
8484        deep_rewrite(e, &mut |x| {
8485            if let Expr::Bin { op, l, r, .. } = x {
8486                if matches!(
8487                    op,
8488                    BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
8489                ) {
8490                    let l_const = matches!(&**l, Expr::Const(_));
8491                    let r_var = matches!(&**r, Expr::Local { .. });
8492                    if l_const && r_var {
8493                        std::mem::swap(l, r);
8494                    }
8495                }
8496            }
8497        });
8498    });
8499}
8500
8501/// Object-typed locals compared against 0 compare against `null`.
8502pub fn null_compares(vt: &VarTable, body: &mut Stmt) {
8503    let obj_vars: HashSet<u32> = vt
8504        .vars
8505        .iter()
8506        .filter(|v| v.ty.erased().is_reference())
8507        .map(|v| v.id)
8508        .collect();
8509    rewrite_exprs(body, &mut |e| {
8510        deep_rewrite(e, &mut |x| {
8511            null_side_rewrite(x, &|v| obj_vars.contains(&v));
8512        });
8513    });
8514}
8515
8516/// Drop assignments/declarations of locals that are NEVER read anywhere
8517/// in the method. Register-machine merges commit values no Java-level
8518/// code consumes (phi residue like `printStream = check;` — not just
8519/// noise: the merge variable's inferred type need not match the value,
8520/// so these lines are often type errors as well). Side-effect-free
8521/// values drop entirely; an impure value survives as a bare expression
8522/// statement. Iterated to a fixpoint: dropping `a = b` can make `b`
8523/// unread.
8524pub fn drop_dead_locals(body: &mut Stmt) {
8525    loop {
8526        let mut reads: Vec<usize> = Vec::new();
8527        count_reads(body, &mut reads);
8528        let mut dropped = 0usize;
8529        walk_mut_deep(body, &mut |st| match st {
8530            Stmt::Block(items) => prune_dead_items(items, &reads, &mut dropped),
8531            // Switch case bodies and For inits are bare `Vec<Stmt>`, NOT
8532            // wrapped in a `Stmt::Block`, so the Block arm never sees them.
8533            // Dead phi commits land directly in case bodies (`sb4 =
8534            // compareTo;` per case): pruning only Blocks dropped the
8535            // commit's reader (in a real post-switch Block) but left the
8536            // now-dead commits, stalling the fixpoint cascade and leaking
8537            // mistyped phi residue. TryWithResources.resources is
8538            // deliberately excluded — its LocalDefs carry auto-close
8539            // semantics that a bare-expression rewrite would break.
8540            Stmt::Switch { cases, .. } => {
8541                for c in cases {
8542                    prune_dead_items(&mut c.body, &reads, &mut dropped);
8543                }
8544            }
8545            Stmt::For { init, .. } => prune_dead_items(init, &reads, &mut dropped),
8546            _ => {}
8547        });
8548        if dropped == 0 {
8549            break;
8550        }
8551    }
8552}
8553
8554/// Remove dead assignments/declarations from one bare statement vector.
8555/// Shared by `drop_dead_locals` across every prunable `Vec<Stmt>`
8556/// container. Side-effect-free values drop entirely; an impure value
8557/// survives as a bare expression statement.
8558fn prune_dead_items(items: &mut Vec<Stmt>, reads: &[usize], dropped: &mut usize) {
8559    let dead = |v: u32| reads.get(v as usize).copied().unwrap_or(0) == 0;
8560    items.retain_mut(|x| match x {
8561        Stmt::ExprStmt(Expr::Assign { target, value, op, .. }) => {
8562            let dead_target = matches!(&**target, Expr::Local { var, .. } if dead(*var));
8563            if dead_target && matches!(*op, AssignOp::Plain) {
8564                *dropped += 1;
8565                if has_side_effects(value) {
8566                    let e = std::mem::replace(value, Box::new(Expr::This));
8567                    *x = Stmt::ExprStmt(*e);
8568                    true
8569                } else {
8570                    false
8571                }
8572            } else {
8573                true
8574            }
8575        }
8576        Stmt::LocalDef { var, init, .. } if dead(*var) => {
8577            *dropped += 1;
8578            if let Some(e) = init.take() {
8579                if has_side_effects(&e) {
8580                    *x = Stmt::ExprStmt(e);
8581                    return true;
8582                }
8583            }
8584            false
8585        }
8586        _ => true,
8587    });
8588}
8589
8590
8591/// Vars occurring in this statement's OWN expression payloads (nested
8592/// sub-statements are descended separately so every occurrence carries
8593/// its innermost block id). ForEach/Catch bound vars are declarations,
8594/// not occurrences.
8595fn stmt_shallow_vars<F: FnMut(u32)>(st: &Stmt, out: &mut F) {
8596    fn ex<F: FnMut(u32)>(e: &Expr, out: &mut F) {
8597        visit_exprs(e, &mut |x| {
8598            if let Expr::Local { var, .. } = x {
8599                out(*var);
8600            }
8601        });
8602    }
8603    match st {
8604        Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
8605            ex(e, out)
8606        }
8607        Stmt::TernaryValue { e } => ex(e, out),
8608        Stmt::Return(Some(e)) => ex(e, out),
8609        Stmt::LocalDef { var, init, .. } => {
8610            out(*var);
8611            if let Some(e) = init {
8612                ex(e, out);
8613            }
8614        }
8615        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
8616            ex(cond, out)
8617        }
8618        Stmt::For { cond, update, .. } => {
8619            if let Some(c) = cond {
8620                ex(c, out);
8621            }
8622            for u in update {
8623                ex(u, out);
8624            }
8625        }
8626        Stmt::ForEach { iterable, .. } => ex(iterable, out),
8627        Stmt::Switch { selector, cases, .. } => {
8628            ex(selector, out);
8629            for cg in cases {
8630                if let Some(g) = &cg.guard {
8631                    ex(g, out);
8632                }
8633            }
8634        }
8635        Stmt::Assert { cond, msg } => {
8636            ex(cond, out);
8637            if let Some(m) = msg {
8638                ex(m, out);
8639            }
8640        }
8641        Stmt::Synchronized { lock, .. } => ex(lock, out),
8642        _ => {}
8643    }
8644}
8645
8646/// Pass A: number every Block in pre-order (root = 0) and record each
8647/// var's defining block. `sizes[id]` = the id-range span of the subtree.
8648/// Pass A: number every EMITTER-BRACED position in pre-order (root = 0)
8649/// and record each var's defining block. `sizes[id]` = the id-range span
8650/// of the subtree. A braced position is a Java scope whether or not the
8651/// IR node is a Stmt::Block — switch case bodies are bare Vecs and
8652/// single-statement if arms are bare statements, and sharing the parent's
8653/// id for them hid the scope violation (Telegram jk.java: a case-local
8654/// `notificationCenterDelegate`/`d0x` read after the switch never
8655/// hoisted — 找不到符号 ×76 in one file; the `d9x.L` reads even resolved
8656/// as a PACKAGE — 程序包不存在 ×92 tree-wide).
8657fn scope_pass_a(st: &Stmt, cur: u32, next: &mut u32, def_block: &mut [u32], sizes: &mut Vec<u32>) {
8658    if let Stmt::LocalDef { var, .. } = st {
8659        let i = *var as usize;
8660        if i < def_block.len() && def_block[i] == u32::MAX {
8661            def_block[i] = cur;
8662        }
8663    }
8664    fn scoped(c: &Stmt, next: &mut u32, def_block: &mut [u32], sizes: &mut Vec<u32>) {
8665        let id = *next;
8666        *next += 1;
8667        sizes.push(0);
8668        scope_pass_a(c, id, next, def_block, sizes);
8669        sizes[id as usize] = *next - id;
8670    }
8671    match st {
8672        Stmt::Block(v) => {
8673            for x in v {
8674                scope_pass_a(x, cur, next, def_block, sizes);
8675            }
8676        }
8677        Stmt::If {
8678            then_stmt,
8679            else_stmt,
8680            ..
8681        } => {
8682            scoped(then_stmt, next, def_block, sizes);
8683            if let Some(e) = else_stmt {
8684                scoped(e, next, def_block, sizes);
8685            }
8686        }
8687        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => {
8688            scoped(body, next, def_block, sizes)
8689        }
8690        Stmt::For { init, body, .. } => {
8691            // A for-init declaration scopes over the WHOLE for statement
8692            // (header + body): one shared id, the body a nested one.
8693            let id = *next;
8694            *next += 1;
8695            sizes.push(0);
8696            for x in init {
8697                scope_pass_a(x, id, next, def_block, sizes);
8698            }
8699            scoped(body, next, def_block, sizes);
8700            sizes[id as usize] = *next - id;
8701        }
8702        Stmt::ForEach { body, .. } => scoped(body, next, def_block, sizes),
8703        Stmt::Switch { cases, default, .. } => {
8704            for c in cases {
8705                // One scope per case group (the emitter braces each);
8706                // non-Block statements share the case id, nested Blocks
8707                // get sub-ids — siblings in one brace group must share,
8708                // or a def's later same-case reads fall "outside".
8709                let id = *next;
8710                *next += 1;
8711                sizes.push(0);
8712                for x in &c.body {
8713                    if matches!(x, Stmt::Block(_)) {
8714                        scoped(x, next, def_block, sizes);
8715                    } else {
8716                        scope_pass_a(x, id, next, def_block, sizes);
8717                    }
8718                }
8719                sizes[id as usize] = *next - id;
8720            }
8721            if let Some(d) = default {
8722                scoped(d, next, def_block, sizes);
8723            }
8724        }
8725        Stmt::Try {
8726            body,
8727            catches,
8728            finally,
8729        } => {
8730            scoped(body, next, def_block, sizes);
8731            for c in catches {
8732                scoped(&c.body, next, def_block, sizes);
8733            }
8734            if let Some(f) = finally {
8735                scoped(f, next, def_block, sizes);
8736            }
8737        }
8738        Stmt::TryWithResources {
8739            resources,
8740            body,
8741            catches,
8742            finally,
8743        } => {
8744            // Resource declarations scope over the WHOLE try statement.
8745            let id = *next;
8746            *next += 1;
8747            sizes.push(0);
8748            for r in resources {
8749                scope_pass_a(r, id, next, def_block, sizes);
8750            }
8751            scoped(body, next, def_block, sizes);
8752            for c in catches {
8753                scoped(&c.body, next, def_block, sizes);
8754            }
8755            if let Some(f) = finally {
8756                scoped(f, next, def_block, sizes);
8757            }
8758            sizes[id as usize] = *next - id;
8759        }
8760        Stmt::Synchronized { body, .. } => scoped(body, next, def_block, sizes),
8761        Stmt::Labeled { body, .. } => scope_pass_a(body, cur, next, def_block, sizes),
8762        _ => {}
8763    }
8764}
8765
8766/// Pass B (same numbering walk): count each var's occurrences overall
8767/// and within its defining block's subtree. For/TWR take dedicated arms
8768/// so their header expressions count INSIDE the statement scope (a
8769/// normal `for (int i…)` must not look like an outside read of i).
8770fn scope_pass_b(
8771    st: &Stmt,
8772    cur: u32,
8773    next: &mut u32,
8774    def_block: &[u32],
8775    sizes: &[u32],
8776    within: &mut [u32],
8777    total: &mut [u32],
8778) {
8779    fn cnt(
8780        v: u32,
8781        at: u32,
8782        def_block: &[u32],
8783        sizes: &[u32],
8784        within: &mut [u32],
8785        total: &mut [u32],
8786    ) {
8787        let i = v as usize;
8788        if i < total.len() {
8789            total[i] += 1;
8790            let d = def_block[i];
8791            if d != u32::MAX && d <= at && at < d + sizes[d as usize] {
8792                within[i] += 1;
8793            }
8794        }
8795    }
8796    fn scoped(
8797        c: &Stmt,
8798        next: &mut u32,
8799        def_block: &[u32],
8800        sizes: &[u32],
8801        within: &mut [u32],
8802        total: &mut [u32],
8803    ) {
8804        let id = *next;
8805        *next += 1;
8806        scope_pass_b(c, id, next, def_block, sizes, within, total);
8807    }
8808    fn ex_at(
8809        e: &Expr,
8810        at: u32,
8811        def_block: &[u32],
8812        sizes: &[u32],
8813        within: &mut [u32],
8814        total: &mut [u32],
8815    ) {
8816        visit_exprs(e, &mut |x| {
8817            if let Expr::Local { var, .. } = x {
8818                cnt(*var, at, def_block, sizes, within, total);
8819            }
8820        });
8821    }
8822    match st {
8823        Stmt::For {
8824            init,
8825            cond,
8826            update,
8827            body,
8828        } => {
8829            let id = *next;
8830            *next += 1;
8831            for x in init {
8832                scope_pass_b(x, id, next, def_block, sizes, within, total);
8833            }
8834            if let Some(c) = cond {
8835                ex_at(c, id, def_block, sizes, within, total);
8836            }
8837            for u in update {
8838                ex_at(u, id, def_block, sizes, within, total);
8839            }
8840            scoped(body, next, def_block, sizes, within, total);
8841        }
8842        Stmt::TryWithResources {
8843            resources,
8844            body,
8845            catches,
8846            finally,
8847        } => {
8848            let id = *next;
8849            *next += 1;
8850            for r in resources {
8851                scope_pass_b(r, id, next, def_block, sizes, within, total);
8852            }
8853            scoped(body, next, def_block, sizes, within, total);
8854            for c in catches {
8855                scoped(&c.body, next, def_block, sizes, within, total);
8856            }
8857            if let Some(f) = finally {
8858                scoped(f, next, def_block, sizes, within, total);
8859            }
8860        }
8861        _ => {
8862            stmt_shallow_vars(st, &mut |v| cnt(v, cur, def_block, sizes, within, total));
8863            match st {
8864                Stmt::Block(v) => {
8865                    for x in v {
8866                        scope_pass_b(x, cur, next, def_block, sizes, within, total);
8867                    }
8868                }
8869                Stmt::If {
8870                    then_stmt,
8871                    else_stmt,
8872                    ..
8873                } => {
8874                    scoped(then_stmt, next, def_block, sizes, within, total);
8875                    if let Some(e) = else_stmt {
8876                        scoped(e, next, def_block, sizes, within, total);
8877                    }
8878                }
8879                Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => {
8880                    scoped(body, next, def_block, sizes, within, total)
8881                }
8882                Stmt::ForEach { body, .. } => {
8883                    scoped(body, next, def_block, sizes, within, total)
8884                }
8885                Stmt::Switch { cases, default, .. } => {
8886                    for c in cases {
8887                        let id = *next;
8888                        *next += 1;
8889                        for x in &c.body {
8890                            if matches!(x, Stmt::Block(_)) {
8891                                scoped(x, next, def_block, sizes, within, total);
8892                            } else {
8893                                scope_pass_b(x, id, next, def_block, sizes, within, total);
8894                            }
8895                        }
8896                    }
8897                    if let Some(d) = default {
8898                        scoped(d, next, def_block, sizes, within, total);
8899                    }
8900                }
8901                Stmt::Try {
8902                    body,
8903                    catches,
8904                    finally,
8905                } => {
8906                    scoped(body, next, def_block, sizes, within, total);
8907                    for c in catches {
8908                        scoped(&c.body, next, def_block, sizes, within, total);
8909                    }
8910                    if let Some(f) = finally {
8911                        scoped(f, next, def_block, sizes, within, total);
8912                    }
8913                }
8914                Stmt::Synchronized { body, .. } => {
8915                    scoped(body, next, def_block, sizes, within, total)
8916                }
8917                Stmt::Labeled { body, .. } => {
8918                    scope_pass_b(body, cur, next, def_block, sizes, within, total)
8919                }
8920                _ => {}
8921            }
8922        }
8923    }
8924}
8925
8926/// Declaration hygiene: every used var that has no LocalDef gets a bare
8927/// declaration at the top; duplicate LocalDefs demote to assignments.
8928/// In an enum's `<clinit>`, every constant's assignment
8929/// (`Self.FIELD = new Self("NAME", ordinal, ...)`) is compiler-mandated
8930/// boilerplate — when the class renders as a true `enum` declaration the
8931/// constants live in the header and these assignments must go. Only
8932/// ACC_ENUM-flagged static fields of the class itself are touched; the
8933/// `$VALUES` array assignment stays (a static block in an enum is legal).
8934pub fn strip_enum_const_inits(s: &mut Stmt, class: &crate::PoolClass) {
8935    let const_fields: jdc_core::FxHashSet<&str> = class
8936        .static_fields
8937        .iter()
8938        .filter(|f| f.access & crate::access::ACC_ENUM != 0)
8939        .map(|f| f.name.as_ref())
8940        .collect();
8941    if const_fields.is_empty() {
8942        return;
8943    }
8944    if let Stmt::Block(stmts) = s {
8945        stmts.retain(|st| {
8946            !matches!(
8947                st,
8948                Stmt::ExprStmt(Expr::Assign { target, value, .. })
8949                    if matches!(&**target,
8950                        Expr::Field { cls, name, is_static: true, .. }
8951                            if cls.as_ref() == class.name
8952                                && const_fields.contains(&**name))
8953                        && matches!(&**value, Expr::New { cls: ncls, .. } if ncls.as_ref() == class.name)
8954            )
8955        });
8956    }
8957}
8958
8959/// Java requires the `super(...)`/`this(...)` delegation to be the FIRST
8960/// statement of a constructor. d8/R8 order the outer-reference capture
8961/// (`this.b = p1;`) or other field writes before the invokesuper in the
8962/// bytecode, which lifts as-is into an uncompilable statement order —
8963/// hoist the bare delegation call to position 0 (jadx does the same).
8964/// Does the expression touch `this` (explicit, implicit-instance member
8965/// access, or super)? Such a def cannot be inlined into a delegation
8966/// that must run BEFORE the supertype constructor.
8967fn contains_this_access(e: &Expr) -> bool {
8968    let mut hit = false;
8969    visit_exprs(e, &mut |x| {
8970        if hit {
8971            return;
8972        }
8973        match x {
8974            Expr::This => hit = true,
8975            Expr::Field { owner, is_static: false, .. } => {
8976                if owner.is_none() || matches!(&**owner.as_ref().unwrap(), Expr::This) {
8977                    hit = true;
8978                }
8979            }
8980            Expr::Method { owner, is_static: false, .. } => {
8981                if owner.is_none() || matches!(&**owner.as_ref().unwrap(), Expr::This) {
8982                    hit = true;
8983                }
8984            }
8985            _ => {}
8986        }
8987    });
8988    hit
8989}
8990
8991/// Inline the transitive straight-line defs of a delegation call's
8992/// arg locals into the call, so hoisting it to position 0 leaves no
8993/// dangling name (lark ProtoAdapter's `super(str)` with `str` defined
8994/// BELOW the hoist point — 找不到符号 变量 str). Returns false when any
8995/// arg var has no inlineable def (param-less, missing, this-touching,
8996/// or cyclic) — the caller then leaves the call in place: a
8997/// not-first-statement error is cheaper than an unresolved name that
8998/// poisons every downstream type.
8999/// Any occurrence of `var` (read OR assign-target) in `stmts` — the
9000/// suffix-use test for dropping a consumed def: count_locals_stmts only
9001/// sees reads, and a suffix WRITE to a dropped declaration makes
9002/// ensure_declared re-add `Type v;` at the method top — BEFORE the
9003/// hoisted this()/super() ("对this的调用必须是构造器中的第一个语句",
9004/// lark +587 the read-only test caused).
9005fn var_occurs_in(stmts: &[Stmt], var: u32) -> bool {
9006    stmts.iter().any(|st| {
9007        let mut hit = false;
9008        visit_stmt_exprs_ro(st, &mut |e| {
9009            if !hit {
9010                visit_exprs(e, &mut |x| {
9011                    if let Expr::Local { var: v, .. } = x {
9012                        if *v == var {
9013                            hit = true;
9014                        }
9015                    }
9016                });
9017            }
9018        });
9019        hit
9020    })
9021}
9022
9023// ---------------------------------------------------------------------------
9024// Branched-delegation ctors → static resolver helper.
9025// ---------------------------------------------------------------------------
9026
9027/// A synthetic static method extracted from a constructor whose
9028/// delegation is preceded by computation (the Kotlin default-arg /
9029/// conditional-bridge shape). Rendered by classdec after the real
9030/// methods; the ctor becomes a single first-statement delegation whose
9031/// carrier arg is the helper call.
9032pub struct CtorHelper {
9033    pub name: String,
9034    pub ret: JavaType,
9035    pub params: Vec<(JavaType, String)>,
9036    pub body: Stmt,
9037    pub vt: VarTable,
9038}
9039
9040fn ctor_helpers() -> &'static std::sync::Mutex<std::collections::HashMap<String, Vec<CtorHelper>>> {
9041    static H: std::sync::OnceLock<std::sync::Mutex<std::collections::HashMap<String, Vec<CtorHelper>>>> =
9042        std::sync::OnceLock::new();
9043    H.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::default()))
9044}
9045
9046/// Synthetic relay ctor for the FULL mode of
9047/// extract_branched_delegation_helper: `private C(Object[] h$relay) {
9048/// this/super((T0) h$relay[0], ..); }` — the public ctor becomes
9049/// `this(resolve$X(params))`, both delegations first-statement legal.
9050pub struct CtorRelay {
9051    /// The delegation target's formal types (the casts of the unpack).
9052    pub formals: Vec<JavaType>,
9053    pub is_super: bool,
9054}
9055
9056type RelayMap = std::collections::HashMap<String, Vec<(String, CtorRelay)>>;
9057
9058fn ctor_relays() -> &'static std::sync::Mutex<RelayMap> {
9059    static H: std::sync::OnceLock<std::sync::Mutex<RelayMap>> = std::sync::OnceLock::new();
9060    H.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::default()))
9061}
9062
9063/// Take (and clear) the relay ctors registered for `class` — sorted by
9064/// the originating ctor descriptor for deterministic emission.
9065pub fn take_ctor_relays(class: &str) -> Vec<(String, CtorRelay)> {
9066    let mut m = ctor_relays().lock().unwrap_or_else(|e| e.into_inner());
9067    let mut v = m.remove(class).unwrap_or_default();
9068    v.sort_by(|a, b| a.0.cmp(&b.0));
9069    v
9070}
9071
9072fn array_init(args: Vec<Expr>) -> Expr {
9073    Expr::NewArray {
9074        elem: TypeRef::J(JavaType::Object(std::sync::Arc::from("java/lang/Object"))),
9075        dims: Vec::new(),
9076        trailing_dims: 0,
9077        init: Some(args),
9078    }
9079}
9080
9081/// Deterministic helper name: FNV-1a over (class, ctor descriptor) —
9082/// worker-thread registration order must not leak into the output.
9083fn helper_name(class: &str, desc: &str) -> String {
9084    let mut h: u64 = 0xcbf29ce484222325;
9085    for b in class.as_bytes().iter().chain(b"::").chain(desc.as_bytes()) {
9086        h ^= *b as u64;
9087        h = h.wrapping_mul(0x100000001b3);
9088    }
9089    format!("resolve${:x}", (h >> 16) & 0xffff_ffff)
9090}
9091
9092/// Drain the helpers registered for a class (render-time). Sorted by
9093/// name: registration order is worker-scheduling dependent.
9094pub fn take_ctor_helpers(class: &str) -> Vec<CtorHelper> {
9095    let mut m = ctor_helpers().lock().unwrap_or_else(|e| e.into_inner());
9096    let mut v = m.remove(class).unwrap_or_default();
9097    v.sort_by(|a, b| a.name.cmp(&b.name));
9098    v
9099}
9100
9101fn strip_delegations_deep(stmts: &mut Vec<Stmt>) {
9102    stmts.retain(|s| !is_bare_ctor_call(s));
9103    for s in stmts.iter_mut() {
9104        strip_delegation_one(s);
9105    }
9106}
9107
9108fn strip_delegation_one(st: &mut Stmt) {
9109    if is_bare_ctor_call(st) {
9110        *st = Stmt::Block(Vec::new());
9111        return;
9112    }
9113    match st {
9114        Stmt::Block(v) => strip_delegations_deep(v),
9115        Stmt::If { then_stmt, else_stmt, .. } => {
9116            strip_delegation_one(then_stmt);
9117            if let Some(e) = else_stmt {
9118                strip_delegation_one(e);
9119            }
9120        }
9121        Stmt::While { body, .. }
9122        | Stmt::DoWhile { body, .. }
9123        | Stmt::ForEach { body, .. }
9124        | Stmt::Synchronized { body, .. }
9125        | Stmt::Labeled { body, .. } => strip_delegation_one(body),
9126        Stmt::For { init, body, .. } => {
9127            strip_delegations_deep(init);
9128            strip_delegation_one(body);
9129        }
9130        Stmt::Switch { cases, default, .. } => {
9131            for c in cases.iter_mut() {
9132                strip_delegations_deep(&mut c.body);
9133            }
9134            if let Some(d) = default {
9135                strip_delegation_one(d);
9136            }
9137        }
9138        Stmt::Try { body, catches, finally } | Stmt::TryWithResources { body, catches, finally, .. } => {
9139            strip_delegation_one(body);
9140            for c in catches.iter_mut() {
9141                strip_delegation_one(&mut c.body);
9142            }
9143            if let Some(f) = finally {
9144                strip_delegation_one(f);
9145            }
9146        }
9147        _ => {}
9148    }
9149}
9150
9151#[allow(clippy::ptr_arg)]
9152fn rewrite_bare_returns(stmts: &mut Vec<Stmt>, val: &Expr) {
9153    for s in stmts.iter_mut() {
9154        rewrite_bare_returns_one(s, val);
9155    }
9156}
9157
9158fn rewrite_bare_returns_one(st: &mut Stmt, val: &Expr) {
9159    match st {
9160        Stmt::Return(None) => *st = Stmt::Return(Some(val.clone())),
9161        Stmt::Block(v) => rewrite_bare_returns(v, val),
9162        Stmt::If { then_stmt, else_stmt, .. } => {
9163            rewrite_bare_returns_one(then_stmt, val);
9164            if let Some(e) = else_stmt {
9165                rewrite_bare_returns_one(e, val);
9166            }
9167        }
9168        Stmt::While { body, .. }
9169        | Stmt::DoWhile { body, .. }
9170        | Stmt::ForEach { body, .. }
9171        | Stmt::Synchronized { body, .. }
9172        | Stmt::Labeled { body, .. } => rewrite_bare_returns_one(body, val),
9173        Stmt::For { init, body, .. } => {
9174            rewrite_bare_returns(init, val);
9175            rewrite_bare_returns_one(body, val);
9176        }
9177        Stmt::Switch { cases, default, .. } => {
9178            for c in cases.iter_mut() {
9179                rewrite_bare_returns(&mut c.body, val);
9180            }
9181            if let Some(d) = default {
9182                rewrite_bare_returns_one(d, val);
9183            }
9184        }
9185        Stmt::Try { body, catches, finally } | Stmt::TryWithResources { body, catches, finally, .. } => {
9186            rewrite_bare_returns_one(body, val);
9187            for c in catches.iter_mut() {
9188                rewrite_bare_returns_one(&mut c.body, val);
9189            }
9190            if let Some(f) = finally {
9191                rewrite_bare_returns_one(f, val);
9192            }
9193        }
9194        _ => {}
9195    }
9196}
9197
9198fn collect_def_vars(stmts: &[Stmt], out: &mut jdc_core::FxHashSet<u32>) {
9199    for st in stmts {
9200        match st {
9201            Stmt::LocalDef { var, .. } => {
9202                out.insert(*var);
9203            }
9204            Stmt::ExprStmt(Expr::Assign { target, .. }) => {
9205                if let Expr::Local { var, .. } = &**target {
9206                    out.insert(*var);
9207                }
9208            }
9209            Stmt::Block(v) => collect_def_vars(v, out),
9210            Stmt::If { then_stmt, else_stmt, .. } => {
9211                collect_def_vars(std::slice::from_ref(then_stmt.as_ref()), out);
9212                if let Some(e) = else_stmt {
9213                    collect_def_vars(std::slice::from_ref(e.as_ref()), out);
9214                }
9215            }
9216            Stmt::While { body, .. }
9217            | Stmt::DoWhile { body, .. }
9218            | Stmt::Synchronized { body, .. }
9219            | Stmt::Labeled { body, .. } => {
9220                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
9221            }
9222            Stmt::ForEach { var, body, .. } => {
9223                out.insert(*var);
9224                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
9225            }
9226            Stmt::For { init, body, .. } => {
9227                collect_def_vars(init, out);
9228                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
9229            }
9230            Stmt::Switch { cases, default, .. } => {
9231                for c in cases {
9232                    collect_def_vars(&c.body, out);
9233                }
9234                if let Some(d) = default {
9235                    collect_def_vars(std::slice::from_ref(d.as_ref()), out);
9236                }
9237            }
9238            Stmt::Try { body, catches, finally }
9239            | Stmt::TryWithResources { body, catches, finally, .. } => {
9240                collect_def_vars(std::slice::from_ref(body.as_ref()), out);
9241                for c in catches {
9242                    out.insert(c.var);
9243                    collect_def_vars(std::slice::from_ref(c.body.as_ref()), out);
9244                }
9245                if let Some(f) = finally {
9246                    collect_def_vars(std::slice::from_ref(f.as_ref()), out);
9247                }
9248            }
9249            _ => {}
9250        }
9251    }
9252}
9253
9254/// v1 extraction is limited to Block/If/straight-line computation (the
9255/// Kotlin default-arg shape); loops/switch/try in the extraction bail.
9256fn extraction_shape_ok(stmts: &[Stmt]) -> bool {
9257    stmts.iter().all(|st| match st {
9258        Stmt::Block(v) => extraction_shape_ok(v),
9259        Stmt::LocalDef { .. }
9260        | Stmt::ExprStmt(_)
9261        | Stmt::Return(_)
9262        | Stmt::Throw(_)
9263        | Stmt::Assert { .. }
9264        | Stmt::Break(_)
9265        | Stmt::Continue(_) => true,
9266        Stmt::If { then_stmt, else_stmt, .. } => {
9267            ok_one(then_stmt)
9268                && else_stmt.as_deref().map(ok_one).unwrap_or(true)
9269        }
9270        Stmt::While { body, .. }
9271        | Stmt::DoWhile { body, .. }
9272        | Stmt::ForEach { body, .. }
9273        | Stmt::Synchronized { body, .. } => ok_one(body),
9274        Stmt::Labeled { body, .. } => ok_one(body),
9275        Stmt::For { init, body, .. } => extraction_shape_ok(init) && ok_one(body),
9276        Stmt::Switch { cases, default, .. } => {
9277            cases.iter().all(|c| extraction_shape_ok(&c.body))
9278                && default.as_deref().map(ok_one).unwrap_or(true)
9279        }
9280        Stmt::Try { body, catches, finally }
9281        | Stmt::TryWithResources { body, catches, finally, .. } => {
9282            ok_one(body)
9283                && catches.iter().all(|c| ok_one(&c.body))
9284                && finally.as_deref().map(ok_one).unwrap_or(true)
9285        }
9286        // Label/Goto (unstructured), MonitorEnter/Exit (raw), ClassDecl
9287        // (local class), TernaryValue, Raw: stay out of v2 extraction.
9288        _ => false,
9289    })
9290}
9291
9292fn ok_one(st: &Stmt) -> bool {
9293    extraction_shape_ok(std::slice::from_ref(st))
9294}
9295
9296fn static_safe(stmts: &[Stmt], this_id: Option<u32>) -> bool {
9297    let mut ok = true;
9298    for st in stmts {
9299        visit_stmt_exprs_ro(st, &mut |e| {
9300            visit_exprs(e, &mut |x| {
9301                match x {
9302                    Expr::This => ok = false,
9303                    // ddc's IR usually carries `this` as the var-0
9304                    // PARAM local, not the Expr::This variant — a
9305                    // helper body reading it dangles (the this param
9306                    // is excluded from the helper's param list) AND
9307                    // renders 无法从静态上下文中引用非静态 (weixin y9/i).
9308                    Expr::Local { var, .. } => {
9309                        if Some(*var) == this_id {
9310                            ok = false;
9311                        }
9312                    }
9313                    // implicit-this member access / call
9314                    Expr::Field { owner: None, is_static: false, .. } => ok = false,
9315                    Expr::Method { owner: None, is_static: false, .. } => ok = false,
9316                    _ => {}
9317                }
9318            });
9319        });
9320    }
9321    ok
9322}
9323
9324/// The bytedance PatchProxy hotfix guard in <clinit>:
9325/// `if (vsChange != null) { PatchProxyResult p = PatchProxy.proxy(..);
9326/// if (p.isSupported) { return; } }` — a bare `return;` inside a STATIC
9327/// INITIALIZER is illegal Java (javac: 返回外部方法; videocut ×28,029 +
9328/// news_article ×19,058 — sampled 400/400 this shape). The patch path
9329/// is dead in a fresh compile (the IVsChange field is null), so the
9330/// whole leading guard statement strips, leaving the real static inits —
9331/// the <clinit> counterpart of the Titan/Robust ctor-guard strips.
9332/// Gate: FIRST top-level stmt is an If with no else whose body holds
9333/// both a bare return and a *PatchProxy* call.
9334pub fn strip_clinit_hotfix_guard(body: &mut Stmt) {
9335    // The structurer can wrap the sequence in a nested singleton block
9336    // (the enum-clinit path flattens for the same reason); the normal
9337    // emit_method path does not.
9338    if let Stmt::Block(vs) = body {
9339        flatten_top_blocks(vs);
9340    }
9341    let Stmt::Block(stmts) = body else { return };
9342    // The guard may sit behind hoisted bare declarations (`int[] v8x;`
9343    // from ensure_declared) — find the first non-decl statement.
9344    let mut gi = 0usize;
9345    while gi < stmts.len() && matches!(&stmts[gi], Stmt::LocalDef { init: None, .. }) {
9346        gi += 1;
9347    }
9348    let Some(first) = stmts.get(gi) else { return };
9349    let Stmt::If { then_stmt, else_stmt, .. } = first else {
9350        return;
9351    };
9352    if else_stmt.is_some() {
9353        return;
9354    }
9355    let mut has_ret = false;
9356    let mut has_proxy = false;
9357    let c = then_stmt.as_ref();
9358    walk_all(c, &mut |st| {
9359        if matches!(st, Stmt::Return(None)) {
9360            has_ret = true;
9361        }
9362    });
9363    let mut cc = c.clone();
9364    walk_stmt_exprs(&mut cc, &mut |e| {
9365        deep_rewrite(e, &mut |x| {
9366            if let Expr::Method { cls, .. } = x {
9367                if cls.contains("PatchProxy") {
9368                    has_proxy = true;
9369                }
9370            }
9371        });
9372    });
9373    if has_ret && has_proxy {
9374        stmts.remove(gi);
9375    }
9376}
9377
9378/// Vendor hotfix guards in CTORS that wrap a delegation:
9379/// `if (__ != null) { Object[] v = ..; ConstructorCode proxy =
9380/// ConstructorCode.proxy(..); if (proxy != null) { this(unpack(v));
9381/// proxy.afterSuper(this); return; } }` ahead of the real top-level
9382/// delegation (alipay InstantRun — ctor-not-first ×1,436; the same
9383/// shape as the already-stripped Baidu Titan / bytedance clinit /
9384/// Meituan Robust guards). The patch path is dead in a fresh compile
9385/// (the redirect field is null), and Java cannot place the wrapped
9386/// delegation legally — strip the WHOLE guard statement.
9387///
9388/// VENDOR-GATED on purpose: a conditional delegation with a top-level
9389/// fallback (`if (c) { this(a); return; } this(b);`) is legitimate
9390/// bytecode whose guard must NOT be dropped. The gate: the guard
9391/// subtree mentions a known hotfix framework type (hotfix / instantrun
9392/// / robust / titan / PatchProxy / ChangeQuickRedirect / ConstructorCode
9393/// in any method-owner, field owner+type, cast, or new).
9394/// First LEAF statement after unwrapping nested `Stmt::Block` wrappers —
9395/// the lifted ctor body reaches the ctor chain with the delegation and
9396/// the hotfix guard each wrapped in plain Blocks (the flattening that
9397/// produces the rendered shape runs later).
9398pub(crate) fn first_leaf_stmt(s: &Stmt) -> &Stmt {
9399    match s {
9400        Stmt::Block(v) if !v.is_empty() => first_leaf_stmt(&v[0]),
9401        other => other,
9402    }
9403}
9404
9405/// Vendor hotfix guards in CTORS that wrap a delegation:
9406/// `if (__ != null) { Object[] v = ..; ConstructorCode proxy =
9407/// ConstructorCode.proxy(..); if (proxy != null) { this(unpack(v));
9408/// proxy.afterSuper(this); return; } }` ahead of the real top-level
9409/// delegation (alipay InstantRun — ctor-not-first ×1,436, nearly all in
9410/// FALLBACK ENUM ctors; the same shape as the already-stripped Baidu
9411/// Titan / bytedance clinit / Meituan Robust guards). The patch path is
9412/// dead in a fresh compile (the redirect field is null), and Java cannot
9413/// place the wrapped delegation legally — strip the WHOLE guard
9414/// statement (at this pipeline stage it arrives Block-wrapped, and its
9415/// branches are still un-inverted: `If(then=∅, else=guard-body)`).
9416///
9417/// VENDOR-GATED on purpose: a conditional delegation with a top-level
9418/// fallback (`if (c) { this(a); return; } this(b);`) is legitimate
9419/// bytecode whose guard must NOT be dropped. The gate: the guard
9420/// subtree mentions a known hotfix framework type (hotfix / instantrun
9421/// / robust / titan / PatchProxy / ChangeQuickRedirect / ConstructorCode
9422/// in any method-owner, field owner+type, cast, or new).
9423pub fn strip_ctor_hotfix_guards(body: &mut Stmt) {
9424    fn marker_hit(e: &Expr) -> bool {
9425        let has = |n: &str| {
9426            let l = n.to_ascii_lowercase();
9427            l.contains("hotfix")
9428                || l.contains("instantrun")
9429                || l.contains("robust")
9430                || l.contains("titan")
9431                || l.contains("patchproxy")
9432                || l.contains("changequickredirect")
9433                || l.contains("constructorcode")
9434        };
9435        let mut hit = false;
9436        visit_exprs(e, &mut |x| {
9437            match x {
9438                Expr::Method { cls, .. } | Expr::New { cls, .. } | Expr::Field { cls, .. } => {
9439                    if has(cls) {
9440                        hit = true;
9441                    }
9442                }
9443                _ => {}
9444            }
9445            if let Expr::Field { ty, .. } = x {
9446                if let TypeRef::J(JavaType::Object(n)) = ty {
9447                    if has(n) {
9448                        hit = true;
9449                    }
9450                }
9451            }
9452        });
9453        hit
9454    }
9455    // A top-level statement that IS a hotfix guard: unwrap single-child
9456    // Block chains down to an If whose subtree holds BOTH a delegation
9457    // and a vendor marker.
9458    fn is_guard(st: &Stmt) -> bool {
9459        let mut cur = st;
9460        loop {
9461            match cur {
9462                Stmt::Block(v) if v.len() == 1 => cur = &v[0],
9463                Stmt::If { .. } => break,
9464                _ => return false,
9465            }
9466        }
9467        let mut has_del = false;
9468        walk_all(cur, &mut |s2| {
9469            if is_bare_ctor_call(s2) {
9470                has_del = true;
9471            }
9472        });
9473        if !has_del {
9474            return false;
9475        }
9476        let mut has_marker = false;
9477        visit_stmt_exprs_ro(cur, &mut |e| {
9478            if marker_hit(e) {
9479                has_marker = true;
9480            }
9481        });
9482        has_marker
9483    }
9484    // Strip at EVERY Block level: the lifted body arrives either as
9485    // [Block(guard), Block(del, ..)] (AtomicDataCollector) or as ONE big
9486    // Block [decls, guard, prelude-computes, del] (compose TextStyle).
9487    // No outside-delegation requirement: the vendor gate already proves
9488    // the statement is dead hotfix machinery (the redirect field is null
9489    // in a fresh compile — the same assumption the Titan/PatchProxy
9490    // strips run on), and the dex's fresh path IS whatever remains after
9491    // the guard. rpc/a.java: the real super() sits inside a switch arm —
9492    // requiring an unconditional outside delegation kept the guard (and
9493    // its illegal pre-super field writes) alive.
9494    fn strip_in(stmts: &mut Vec<Stmt>) {
9495        stmts.retain(|st| !is_guard(st));
9496        for st in stmts.iter_mut() {
9497            if let Stmt::Block(v) = st {
9498                strip_in(v);
9499            }
9500        }
9501    }
9502    let Stmt::Block(stmts) = body else { return };
9503    strip_in(stmts);
9504}
9505
9506/// Repeated identical branch-site delegations with param-only args
9507/// (weixin appbrand/zc: bare `super()` at four branch ends, each
9508/// followed by field-write tails) carry no computed carrier, so the
9509/// helper extraction has nothing to return — but they need no helper:
9510/// every delegating path runs the SAME call, so hoist one copy to
9511/// position 0 and strip the sites. The per-branch tails stay in their
9512/// branches and now legally follow the first-statement delegation.
9513/// Same reordering approximation fix_ctor_super_first already ships for
9514/// the straight-line case (paths that threw BEFORE delegating now run
9515/// the delegation first — Object-super, the dominant shape, is inert).
9516/// Gate: every del deep-equal and reading params only (a computed local
9517/// would dangle at position 0 — that is the helper extractor's job).
9518pub fn hoist_branch_delegations(body: &mut Stmt, vt: &VarTable) {
9519    let Stmt::Block(stmts) = body else { return };
9520    if stmts.first().is_some_and(is_bare_ctor_call) {
9521        return; // already first
9522    }
9523    let mut dels: Vec<Expr> = Vec::new();
9524    for st in stmts.iter() {
9525        visit_stmt_exprs_ro(st, &mut |e| {
9526            if is_delegation_expr(e) {
9527                dels.push(e.clone());
9528            }
9529        });
9530    }
9531    if dels.is_empty() {
9532        return;
9533    }
9534    if !dels.iter().all(|d| *d == dels[0]) {
9535        return;
9536    }
9537    let mut bad = false;
9538    visit_exprs(&dels[0], &mut |x| {
9539        if let Expr::Local { var, .. } = x {
9540            if (*var as usize) >= vt.vars.len() || !vt.vars[*var as usize].is_param {
9541                bad = true;
9542            }
9543        }
9544    });
9545    if bad {
9546        return;
9547    }
9548    let del = dels[0].clone();
9549    strip_delegations_deep(stmts);
9550    stmts.insert(0, Stmt::ExprStmt(del));
9551}
9552
9553/// The branched-delegation ctor family ("对this的调用必须是构造器中的
9554/// 第一个语句", ~2k across corpora): R8 renders a Kotlin default-arg /
9555/// conditional-bridge ctor as computation statements (straight-line plus
9556/// mask-guarded overrides) with every control path ending in a
9557/// `this(a, b, carrier)` delegation. Java cannot express statements
9558/// before the delegation, and unlike the linear-chain case the carrier's
9559/// value is computed by multi-statement branched code that cannot fold
9560/// into a ternary. Extract the computation into a synthetic
9561/// `private static T resolve$X(<ctor params>)` and make the ctor a
9562/// single first-statement `this(a, b, resolve$X(..))` — compilable AND
9563/// faithful (jadx renders the un-compilable statements-before-this form
9564/// for this family).
9565///
9566/// Two carrier modes:
9567/// - Single (v1): every delegation textually identical and exactly one
9568///   non-param local (the carrier) read exactly once across the args.
9569/// - Position (v2): delegations differ at exactly ONE arg position k,
9570///   every site passing a bare non-param local there (per-branch
9571///   carriers, lark BaseProtocol$ReliablePushList's v22_g1/v22x). Each
9572///   stripped delegation's own carrier fills the return that follows it;
9573///   merge-point returns fall back to the primary site's carrier.
9574///
9575/// The top-level primary delegation is optional in BOTH modes — when
9576/// every path delegates inside branches (weixin ln1/b1's super() in
9577/// if/try/catch) the whole body is extracted and any del serves as the
9578/// template (all non-k parts identical).
9579///
9580/// Shared gates (all conservative, bail keeps today's behavior): not an
9581/// enum (trace-param strip shifts render params); nested only when the
9582/// class RENDERS static-nested (member-inner ctors lose the outer param
9583/// at render); computation is Block/If/straight-line only; no
9584/// `this`-dependence; no local defined in the extraction used after the
9585/// primary.
9586pub fn extract_branched_delegation_helper(
9587    body: &mut Stmt,
9588    vt: &VarTable,
9589    pool: &DexPool,
9590    class: &crate::PoolClass,
9591    desc_str: &str,
9592) {
9593    if class.is_enum() {
9594        return;
9595    }
9596    // Member-inner ctors lose the synthetic outer param at render
9597    // (qualified-new absorbs it), so the helper's param list would no
9598    // longer match the rendered ctor. STATIC nested classes render all
9599    // params like top-level ones (lark BaseProtocol$PushList family).
9600    if class.nesting.enclosing_class.is_some() && !crate::ctx::nested_is_static(pool, class) {
9601        return;
9602    }
9603    let Stmt::Block(stmts) = body else { return };
9604    let mut dels: Vec<Expr> = Vec::new();
9605    for st in stmts.iter() {
9606        visit_stmt_exprs_ro(st, &mut |e| {
9607            if is_delegation_expr(e) {
9608                dels.push(e.clone());
9609            }
9610        });
9611    }
9612    if dels.is_empty() {
9613        return;
9614    }
9615    if dels.len() == 1 && stmts.first().is_some_and(is_bare_ctor_call) {
9616        return; // already first
9617    }
9618    let template = dels[0].clone();
9619    let Expr::Method { args: t_args, .. } = &template else {
9620        return;
9621    };
9622    // Carrier discovery: Single (all dels identical, one non-param local
9623    // read once) or Position (dels differ at exactly one arg slot, every
9624    // site a bare non-param local of one erased type).
9625    enum Mode {
9626        Single(u32),
9627        Position(usize),
9628        /// Every path delegates with the SAME target signature but its
9629        /// own arg expressions: the whole computation moves to the
9630        /// helper (each delegation becomes `return new Object[]{its
9631        /// args}`), the public ctor becomes `this(resolve$X(params))`
9632        /// and a synthetic `private C(Object[] h)` relay unpacks the
9633        /// carriers into the one legal first-statement delegation.
9634        Full,
9635    }
9636    // Full-mode gate: uniform delegation target (this/super, class,
9637    // descriptor), non-empty args, no pre-existing (Object[]) ctor,
9638    // and — the relay signature is per-CLASS unique — only the
9639    // lexicographically least <init> descriptor of the class may take
9640    // the relay (deterministic across parallel workers; a second
9641    // branched ctor in one class keeps its honest error).
9642    let full_gate = (|| -> Option<()> {
9643        let Expr::Method {
9644            args: a0,
9645            desc: d0,
9646            is_super: s0,
9647            cls: c0,
9648            ..
9649        } = &dels[0]
9650        else {
9651            return None;
9652        };
9653        if a0.is_empty() {
9654            return None;
9655        }
9656        for d in &dels[1..] {
9657            match d {
9658                Expr::Method {
9659                    args,
9660                    desc,
9661                    is_super,
9662                    cls,
9663                    ..
9664                } if args.len() == a0.len()
9665                    && **desc == **d0
9666                    && is_super == s0
9667                    && cls == c0 => {}
9668                _ => return None,
9669            }
9670        }
9671        if class
9672            .all_methods()
9673            .any(|m| &*m.name == "<init>" && &*m.desc == "([Ljava/lang/Object;)V")
9674        {
9675            return None;
9676        }
9677        Some(())
9678    })()
9679    .is_some();
9680    let mode = if dels.iter().all(|d| *d == dels[0]) {
9681        let mut carrier: Option<u32> = None;
9682        let mut reads = 0usize;
9683        let mut multi = false;
9684        for a in t_args.iter() {
9685            let c = a.clone();
9686            visit_exprs(&c, &mut |x| {
9687                if let Expr::Local { var, .. } = x {
9688                    if (*var as usize) < vt.vars.len() && !vt.vars[*var as usize].is_param {
9689                        reads += 1;
9690                        match carrier {
9691                            Some(v) if v == *var => {}
9692                            Some(_) => multi = true,
9693                            None => carrier = Some(*var),
9694                        }
9695                    }
9696                }
9697            });
9698        }
9699        match carrier {
9700            Some(cv) if !multi && reads == 1 => Mode::Single(cv),
9701            _ if full_gate => Mode::Full,
9702            _ => return,
9703        }
9704    } else {
9705        let positioned: Option<Mode> = (|| {
9706        let arity = t_args.len();
9707        // Uniform arity FIRST: a bare `super()` mixed among arg-carrying
9708        // dels made the single-diff slot k index past a short del's args
9709        // (deepseek v52 / kimi c60.o panics, index-out-of-bounds).
9710        if dels
9711            .iter()
9712            .any(|d| !matches!(d, Expr::Method { args, .. } if args.len() == arity))
9713        {
9714            return None;
9715        }
9716        let mut diff: Vec<usize> = Vec::new();
9717        for j in 0..arity {
9718            if dels.iter().any(|d| match d {
9719                Expr::Method { args, .. } => args.get(j) != t_args.get(j),
9720                _ => true,
9721            }) {
9722                diff.push(j);
9723            }
9724        }
9725        if diff.len() != 1 {
9726            return None;
9727        }
9728        let k = diff[0];
9729        // Non-arg parts AND all other arg positions identical: neutralize
9730        // slot k on every del and compare whole exprs (catches mixed
9731        // this()/super() or different targets).
9732        let mut base0 = dels[0].clone();
9733        if let Expr::Method { args, .. } = &mut base0 {
9734            args[k] = Expr::Const(ConstVal::Null);
9735        }
9736        for d in dels.iter() {
9737            let mut c = d.clone();
9738            if let Expr::Method { args, .. } = &mut c {
9739                args[k] = Expr::Const(ConstVal::Null);
9740            }
9741            if c != base0 {
9742                return None;
9743            }
9744        }
9745        // Slot k: bare non-param local at every site, one erased type,
9746        // and the carrier never read at another position.
9747        let mut carrier_ty: Option<JavaType> = None;
9748        for d in dels.iter() {
9749            let Expr::Method { args, .. } = d else {
9750                return None;
9751            };
9752            let Expr::Local { var, .. } = &args[k] else {
9753                return None;
9754            };
9755            if (*var as usize) >= vt.vars.len() || vt.vars[*var as usize].is_param {
9756                return None;
9757            }
9758            let ty = vt.vars[*var as usize].ty.erased();
9759            match &carrier_ty {
9760                Some(t) if *t == ty => {}
9761                Some(_) => return None,
9762                None => carrier_ty = Some(ty),
9763            }
9764            // Non-k positions must be param/const/static-only — a
9765            // computed local there would have its def extracted into
9766            // the helper and the ctor arg list would dangle (lark
9767            // audiosave/i 找不到符号 ×5-per-site). Also covers the
9768            // carrier leaking into another position.
9769            for (j, a) in args.iter().enumerate() {
9770                if j == k {
9771                    continue;
9772                }
9773                let mut bad = false;
9774                visit_exprs(a, &mut |x| {
9775                    if let Expr::Local { var: v2, .. } = x {
9776                        if (*v2 as usize) >= vt.vars.len() || !vt.vars[*v2 as usize].is_param {
9777                            bad = true;
9778                        }
9779                    }
9780                });
9781                if bad {
9782                    return None;
9783                }
9784            }
9785        }
9786        Some(Mode::Position(k))
9787        })();
9788        match positioned {
9789            Some(m) => m,
9790            None if full_gate => Mode::Full,
9791            None => return,
9792        }
9793    };
9794    let l = stmts.iter().rposition(is_bare_ctor_call);
9795    if l == Some(0) {
9796        return;
9797    }
9798    let (mut extracted, mut tail): (Vec<Stmt>, Vec<Stmt>) = match l {
9799        Some(l) => (stmts[..l].to_vec(), stmts[l + 1..].to_vec()),
9800        None => (stmts.clone(), Vec::new()),
9801    };
9802    if !extraction_shape_ok(&extracted) {
9803        return;
9804    }
9805    // Post-delegation tails (field writes / trace stubs between each
9806    // branch delegation and its return) are CTOR work, not helper
9807    // computation — they commonly touch `this`. When every site's tail
9808    // is identical (and equals the top-level continuation when one
9809    // exists) hoist one copy after the merged delegation; otherwise
9810    // they stay in the helper and the static_safe gate below decides.
9811    let mut site_tails: Vec<Vec<Stmt>> = Vec::new();
9812    {
9813        let mut probe = extracted.clone();
9814        site_tails_walk(&mut probe, &mut site_tails, false);
9815    }
9816    let hoist = !site_tails.is_empty() && {
9817        let n0 = normalize_tail(&site_tails[0]);
9818        site_tails.iter().all(|t| normalize_tail(t) == n0)
9819            && (l.is_none() || normalize_tail(&tail) == n0)
9820    };
9821    if hoist {
9822        site_tails_walk(&mut extracted, &mut Vec::new(), true);
9823        if l.is_none() {
9824            tail = normalize_tail(&site_tails[0]);
9825        }
9826    }
9827    // The delegation the ctor keeps: the top-level primary when present,
9828    // else any del (all non-k parts are identical).
9829    let primary_del: Expr = match l {
9830        Some(l) => match &stmts[l] {
9831            Stmt::ExprStmt(e) => e.clone(),
9832            _ => return,
9833        },
9834        None => dels[0].clone(),
9835    };
9836    let ret = match mode {
9837        Mode::Single(cv) => vt.var(cv).ty.erased(),
9838        Mode::Position(k) => match &primary_del {
9839            Expr::Method { args, .. } => match args.get(k) {
9840                Some(Expr::Local { var, .. }) => vt.var(*var).ty.erased(),
9841                _ => return,
9842            },
9843            _ => return,
9844        },
9845        Mode::Full => JavaType::Array(Box::new(JavaType::Object(std::sync::Arc::from(
9846            "java/lang/Object",
9847        )))),
9848    };
9849    match mode {
9850        Mode::Single(cv) => {
9851            strip_delegations_deep(&mut extracted);
9852            let cv_local = Expr::Local {
9853                var: cv,
9854                ty: vt.var(cv).ty.clone(),
9855            };
9856            rewrite_bare_returns(&mut extracted, &cv_local);
9857            if !always_returns(&extracted) {
9858                extracted.push(Stmt::Return(Some(cv_local)));
9859            }
9860        }
9861        Mode::Position(k) => {
9862            // Each stripped delegation's own slot-k carrier fills the
9863            // next bare return in its list; merge-point returns fall
9864            // back to the primary carrier below.
9865            fill_returns_by_position(&mut extracted, &|args: &[Expr]| args.get(k).cloned());
9866            let fallback = match &primary_del {
9867                Expr::Method { args, .. } => args[k].clone(),
9868                _ => return,
9869            };
9870            rewrite_bare_returns(&mut extracted, &fallback);
9871            if !always_returns(&extracted) {
9872                extracted.push(Stmt::Return(Some(fallback)));
9873            }
9874        }
9875        Mode::Full => {
9876            // Every delegation becomes `return new Object[]{its own
9877            // args}`; merge-point bare returns and the fall-through
9878            // completion take the PRIMARY delegation's args (the
9879            // top-level last del when present — its args are
9880            // top-level-scoped by construction). With no primary,
9881            // every path must already delegate explicitly.
9882            fill_returns_by_position(&mut extracted, &|args: &[Expr]| {
9883                Some(array_init(args.to_vec()))
9884            });
9885            if l.is_none() {
9886                // No top-level primary: every path must delegate. A
9887                // dead tail can still dangle after the shortest
9888                // always-returning prefix (DataHolder: an assign after
9889                // a try whose body returns and whose catch throws) —
9890                // truncate it (unreachable by Java's flow model, so
9891                // sound), else bail.
9892                if !always_returns(&extracted) {
9893                    let cut = (1..=extracted.len())
9894                        .find(|&i| always_returns(&extracted[..i]));
9895                    match cut {
9896                        Some(i) => extracted.truncate(i),
9897                        None => return,
9898                    }
9899                }
9900            }
9901            let fallback_args = match &primary_del {
9902                Expr::Method { args, .. } => args.clone(),
9903                _ => return,
9904            };
9905            let fallback = array_init(fallback_args);
9906            rewrite_bare_returns(&mut extracted, &fallback);
9907            if !always_returns(&extracted) {
9908                extracted.push(Stmt::Return(Some(fallback)));
9909            }
9910        }
9911    }
9912    let this_id = vt
9913        .vars
9914        .iter()
9915        .find(|v| v.is_param && v.name == "this")
9916        .map(|v| v.id);
9917    if !static_safe(&extracted, this_id) {
9918        return;
9919    }
9920    let mut defs: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
9921    collect_def_vars(&extracted, &mut defs);
9922    if defs.iter().any(|v| var_occurs_in(&tail, *v)) {
9923        return;
9924    }
9925    let params: Vec<(u32, JavaType, String)> = vt
9926        .vars
9927        .iter()
9928        .filter(|v| v.is_param && v.name != "this")
9929        .map(|v| (v.id, v.ty.erased(), v.name.clone()))
9930        .collect();
9931    let name = helper_name(&class.name, desc_str);
9932    let call = Expr::Method {
9933        owner: None,
9934        cls: std::sync::Arc::from(class.name.as_str()),
9935        name: std::sync::Arc::from(name.as_str()),
9936        desc: std::sync::Arc::new(jdc_core::types::MethodDescriptor {
9937            ret: ret.clone(),
9938            args: params.iter().map(|p| p.1.clone()).collect(),
9939        }),
9940        args: params
9941            .iter()
9942            .map(|(id, ty, _)| Expr::Local {
9943                var: *id,
9944                ty: TypeRef::J(ty.clone()),
9945            })
9946            .collect(),
9947        is_static: true,
9948        is_interface: false,
9949        is_special: false,
9950        is_super: false,
9951        is_dynamic: false,
9952        type_args: Vec::new(),
9953    };
9954    let mut new_del = primary_del.clone();
9955    match mode {
9956        Mode::Single(cv) => {
9957            deep_rewrite(&mut new_del, &mut |x| {
9958                if let Expr::Local { var, .. } = x {
9959                    if *var == cv {
9960                        *x = call.clone();
9961                    }
9962                }
9963            });
9964        }
9965        Mode::Position(k) => {
9966            let Expr::Method { args, .. } = &mut new_del else {
9967                return;
9968            };
9969            if k >= args.len() {
9970                return;
9971            }
9972            args[k] = call.clone();
9973        }
9974        Mode::Full => {
9975            // The public ctor's one legal statement: `this(resolve$X(p..))`
9976            // — the synthetic relay ctor below unpacks the carriers into
9977            // the uniform target delegation.
9978            new_del = Expr::Method {
9979                owner: None,
9980                cls: std::sync::Arc::from(class.name.as_str()),
9981                name: "<init>".into(),
9982                desc: std::sync::Arc::new(jdc_core::types::MethodDescriptor {
9983                    args: vec![JavaType::Array(Box::new(JavaType::Object(
9984                        std::sync::Arc::from("java/lang/Object"),
9985                    )))],
9986                    ret: JavaType::Void,
9987                }),
9988                args: vec![call.clone()],
9989                is_static: false,
9990                is_interface: false,
9991                is_special: true,
9992                is_super: false,
9993                is_dynamic: false,
9994                type_args: Vec::new(),
9995            };
9996        }
9997    }
9998    {
9999        let mut m = ctor_helpers().lock().unwrap_or_else(|e| e.into_inner());
10000        let v = m.entry(class.name.clone()).or_default();
10001        if !v.iter().any(|h| h.name == name) {
10002            // The extracted body skips the late pipeline polish (it left
10003            // the ctor before cleanup ran) — invert empty thens so the
10004            // helper doesn't render `if (..) {} else {..}`.
10005            let mut hbody = Stmt::Block(extracted);
10006            invert_empty_thens(&mut hbody);
10007            v.push(CtorHelper {
10008                name,
10009                ret: ret.clone(),
10010                params: params.into_iter().map(|(_, ty, nm)| (ty, nm)).collect(),
10011                body: hbody,
10012                vt: vt.clone(),
10013            });
10014            if matches!(mode, Mode::Full) {
10015                let Expr::Method {
10016                    desc: d0,
10017                    is_super: s0,
10018                    ..
10019                } = &primary_del
10020                else {
10021                    return;
10022                };
10023                let mut rm = ctor_relays().lock().unwrap_or_else(|e| e.into_inner());
10024                let rv = rm.entry(class.name.clone()).or_default();
10025                if !rv.iter().any(|(d, _)| d == desc_str) {
10026                    rv.push((
10027                        desc_str.to_string(),
10028                        CtorRelay {
10029                            formals: d0.args.clone(),
10030                            is_super: *s0,
10031                        },
10032                    ));
10033                }
10034            }
10035        } else {
10036            return; // duplicate decompile of the same ctor: keep the first
10037        }
10038    }
10039    *stmts = std::iter::once(Stmt::ExprStmt(new_del)).chain(tail).collect();
10040}
10041
10042/// Position-mode return fill: walk each statement list left-to-right; a
10043/// delegation at slot k hands its carrier to the next bare `return;` in
10044/// the SAME list (the bytecode shape is always `this(..); trace..;
10045/// return;`). With no following return in the list the delegation
10046/// becomes the return itself and the rest of the list (trace stubs only
10047/// — a second delegation there is impossible) is dropped as unreachable.
10048fn fill_returns_by_position(
10049    stmts: &mut Vec<Stmt>,
10050    pick: &dyn Fn(&[Expr]) -> Option<Expr>,
10051) {
10052    let mut i = 0;
10053    while i < stmts.len() {
10054        if is_bare_ctor_call(&stmts[i]) {
10055            let karg = match &stmts[i] {
10056                Stmt::ExprStmt(Expr::Method { args, .. }) => pick(args),
10057                _ => None,
10058            };
10059            if let Some(v) = karg {
10060                let mut j = i + 1;
10061                let mut found = None;
10062                while j < stmts.len() {
10063                    if matches!(stmts[j], Stmt::Return(None)) {
10064                        found = Some(j);
10065                        break;
10066                    }
10067                    if is_bare_ctor_call(&stmts[j]) {
10068                        break; // next delegation owns the next return
10069                    }
10070                    j += 1;
10071                }
10072                match found {
10073                    Some(j) => {
10074                        stmts[j] = Stmt::Return(Some(v));
10075                        stmts.remove(i);
10076                        continue;
10077                    }
10078                    None => {
10079                        stmts[i] = Stmt::Return(Some(v));
10080                        stmts.truncate(i + 1);
10081                        break;
10082                    }
10083                }
10084            }
10085        }
10086        fill_returns_one(&mut stmts[i], pick);
10087        i += 1;
10088    }
10089}
10090
10091fn fill_returns_one(st: &mut Stmt, pick: &dyn Fn(&[Expr]) -> Option<Expr>) {
10092    match st {
10093        Stmt::Block(v) => fill_returns_by_position(v, pick),
10094        Stmt::If { then_stmt, else_stmt, .. } => {
10095            fill_returns_stmt(then_stmt, pick);
10096            if let Some(e) = else_stmt {
10097                fill_returns_stmt(e, pick);
10098            }
10099        }
10100        Stmt::While { body, .. }
10101        | Stmt::DoWhile { body, .. }
10102        | Stmt::ForEach { body, .. }
10103        | Stmt::Synchronized { body, .. }
10104        | Stmt::Labeled { body, .. } => fill_returns_stmt(body, pick),
10105        Stmt::For { init, body, .. } => {
10106            fill_returns_by_position(init, pick);
10107            fill_returns_stmt(body, pick);
10108        }
10109        Stmt::Switch { cases, default, .. } => {
10110            for c in cases.iter_mut() {
10111                fill_returns_by_position(&mut c.body, pick);
10112            }
10113            if let Some(d) = default {
10114                fill_returns_stmt(d, pick);
10115            }
10116        }
10117        Stmt::Try { body, catches, finally } | Stmt::TryWithResources { body, catches, finally, .. } => {
10118            fill_returns_stmt(body, pick);
10119            for c in catches.iter_mut() {
10120                fill_returns_stmt(&mut c.body, pick);
10121            }
10122            if let Some(f) = finally {
10123                fill_returns_stmt(f, pick);
10124            }
10125        }
10126        _ => {}
10127    }
10128}
10129
10130fn fill_returns_stmt(st: &mut Stmt, pick: &dyn Fn(&[Expr]) -> Option<Expr>) {
10131    if is_bare_ctor_call(st) {
10132        // Single-statement branch body (`if (c) this(..);`) — becomes
10133        // the return directly.
10134        if let Stmt::ExprStmt(Expr::Method { args, .. }) = st {
10135            if let Some(v) = pick(args) {
10136                *st = Stmt::Return(Some(v));
10137                return;
10138            }
10139        }
10140    }
10141    fill_returns_one(st, pick);
10142}
10143
10144fn normalize_tail(t: &[Stmt]) -> Vec<Stmt> {
10145    let mut v = t.to_vec();
10146    while matches!(v.last(), Some(Stmt::Return(None))) {
10147        v.pop();
10148    }
10149    v
10150}
10151
10152/// Record (and with `carve`, remove) the statements between each
10153/// delegation and the next bare return in its list — a delegation with
10154/// no following return takes the rest of the list. Single-statement
10155/// branch bodies record an empty tail.
10156fn site_tails_walk(stmts: &mut Vec<Stmt>, out: &mut Vec<Vec<Stmt>>, carve: bool) {
10157    let mut i = 0;
10158    while i < stmts.len() {
10159        if is_bare_ctor_call(&stmts[i]) {
10160            let mut j = i + 1;
10161            let mut end = stmts.len();
10162            while j < stmts.len() {
10163                if matches!(stmts[j], Stmt::Return(None)) {
10164                    end = j;
10165                    break;
10166                }
10167                if is_bare_ctor_call(&stmts[j]) {
10168                    end = j;
10169                    break;
10170                }
10171                j += 1;
10172            }
10173            out.push(stmts[i + 1..end].to_vec());
10174            if carve {
10175                stmts.splice(i + 1..end, []);
10176            }
10177            i += 1;
10178            continue;
10179        }
10180        site_tails_one(&mut stmts[i], out, carve);
10181        i += 1;
10182    }
10183}
10184
10185fn site_tails_one(st: &mut Stmt, out: &mut Vec<Vec<Stmt>>, carve: bool) {
10186    if is_bare_ctor_call(st) {
10187        out.push(Vec::new());
10188        return;
10189    }
10190    match st {
10191        Stmt::Block(v) => site_tails_walk(v, out, carve),
10192        Stmt::If { then_stmt, else_stmt, .. } => {
10193            site_tails_one(then_stmt, out, carve);
10194            if let Some(e) = else_stmt {
10195                site_tails_one(e, out, carve);
10196            }
10197        }
10198        Stmt::While { body, .. }
10199        | Stmt::DoWhile { body, .. }
10200        | Stmt::ForEach { body, .. }
10201        | Stmt::Synchronized { body, .. }
10202        | Stmt::Labeled { body, .. } => site_tails_one(body, out, carve),
10203        Stmt::For { init, body, .. } => {
10204            site_tails_walk(init, out, carve);
10205            site_tails_one(body, out, carve);
10206        }
10207        Stmt::Switch { cases, default, .. } => {
10208            for c in cases.iter_mut() {
10209                site_tails_walk(&mut c.body, out, carve);
10210            }
10211            if let Some(d) = default {
10212                site_tails_one(d, out, carve);
10213            }
10214        }
10215        Stmt::Try { body, catches, finally } | Stmt::TryWithResources { body, catches, finally, .. } => {
10216            site_tails_one(body, out, carve);
10217            for c in catches.iter_mut() {
10218                site_tails_one(&mut c.body, out, carve);
10219            }
10220            if let Some(f) = finally {
10221                site_tails_one(f, out, carve);
10222            }
10223        }
10224        _ => {}
10225    }
10226}
10227
10228/// Can this statement list NOT complete normally? Conservative for
10229/// loops (false); If needs BOTH branches, Switch needs every case plus
10230/// a default. Used to decide whether the extracted helper needs the
10231/// trailing `return carrier;` (appending one after exhaustive branches
10232/// is javac's "unreachable statement" error).
10233fn always_returns(stmts: &[Stmt]) -> bool {
10234    let Some(last) = stmts
10235        .iter()
10236        .rev()
10237        .find(|s| !matches!(s, Stmt::Block(v) if v.is_empty()))
10238    else {
10239        return false;
10240    };
10241    match last {
10242        Stmt::Return(Some(_)) | Stmt::Throw(_) => true,
10243        Stmt::Block(v) => always_returns(v),
10244        Stmt::If { then_stmt, else_stmt, .. } => match else_stmt {
10245            Some(e) => always_returns_one(then_stmt) && always_returns_one(e),
10246            None => false,
10247        },
10248        Stmt::Labeled { body, .. } | Stmt::Synchronized { body, .. } => {
10249            always_returns_one(body)
10250        }
10251        Stmt::Switch { cases, default, .. } => {
10252            default.as_ref().is_some_and(|d| always_returns_one(d))
10253                && cases.iter().all(|c| always_returns(&c.body))
10254        }
10255        Stmt::Try { body, catches, finally }
10256        | Stmt::TryWithResources { body, catches, finally, .. } => {
10257            finally.as_ref().is_some_and(|f| always_returns_one(f))
10258                || (always_returns_one(body)
10259                    && catches.iter().all(|c| always_returns_one(&c.body)))
10260        }
10261        _ => false,
10262    }
10263}
10264
10265fn always_returns_one(st: &Stmt) -> bool {
10266    always_returns(std::slice::from_ref(st))
10267}
10268
10269fn inline_ctor_arg_defs(
10270    call: &mut Stmt,
10271    prefix: &[&Stmt],
10272    vt: &VarTable,
10273    consumed: &mut Vec<(u32, usize)>,
10274) -> bool {
10275    let Stmt::ExprStmt(cexpr) = call else {
10276        return false;
10277    };
10278    // Replace every read of `v` in `e` with its value-so-far; a read
10279    // BEFORE any def (val None) is unresolvable → false.
10280    fn subst(e: &mut Expr, v: u32, val: &Option<Expr>) -> bool {
10281        let mut ok = true;
10282        deep_rewrite(e, &mut |x| {
10283            if let Expr::Local { var, .. } = x {
10284                if *var == v {
10285                    match val {
10286                        Some(e2) => *x = e2.clone(),
10287                        None => ok = false,
10288                    }
10289                }
10290            }
10291        });
10292        ok
10293    }
10294    let mut seen: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
10295    for _ in 0..32 {
10296        let mut need: Option<u32> = None;
10297        visit_exprs(cexpr, &mut |x| {
10298            if need.is_none() {
10299                if let Expr::Local { var, .. } = x {
10300                    if (*var as usize) < vt.vars.len() && !vt.vars[*var as usize].is_param {
10301                        need = Some(*var);
10302                    }
10303                }
10304            }
10305        });
10306        let Some(v) = need else { break };
10307        if !seen.insert(v) {
10308            return false; // cyclic reuse (`str = .. + str`) — bail
10309        }
10310        // CHAIN walk: fold EVERY prefix def of v into the value the
10311        // delegation actually receives. The old strict single-occurrence
10312        // rule rejected the convenience-ctor reassignment chain
10313        // (`f = getInstance(ctx); f = transform(f, ..); this(ctx, f)` —
10314        // androidx CameraController, weibo this-not-first ×831): the
10315        // SOURCE inlined the computation into the delegation args and
10316        // R8 decomposed it into register writes. Reads of v are allowed
10317        // only INSIDE its own def chain; any other prefix statement
10318        // touching v means the value escapes or the object mutates
10319        // (`stringBuilder.append(..)` — inlining a fresh `new
10320        // StringBuilder()` would lose the message), so bail.
10321        let mut val: Option<Expr> = None;
10322        let mut ok = true;
10323        for (si, st) in prefix.iter().enumerate() {
10324            match st {
10325                // A bare declaration (init None — the structurer hoists
10326                // `T v;` to the top) carries no value: skip it. Failing
10327                // here rejected every chain under a hoisted decl (ci1/a).
10328                Stmt::LocalDef { var, init: None, .. } if *var == v => {}
10329                Stmt::LocalDef { var, init, .. } if *var == v => {
10330                    let Some(e) = init else { ok = false; break };
10331                    let mut e2 = (*e).clone();
10332                    if !subst(&mut e2, v, &val) {
10333                        ok = false;
10334                        break;
10335                    }
10336                    val = Some(e2);
10337                    consumed.push((v, si));
10338                }
10339                Stmt::ExprStmt(Expr::Assign { target, op, .. })
10340                    if matches!(&**target, Expr::Local { var: tv, .. } if *tv == v) =>
10341                {
10342                    if !matches!(op, AssignOp::Plain) {
10343                        ok = false; // compound write: not a plain def
10344                        break;
10345                    }
10346                    let Stmt::ExprStmt(Expr::Assign { value, .. }) = st else {
10347                        unreachable!()
10348                    };
10349                    let mut e2 = value.as_ref().clone();
10350                    if !subst(&mut e2, v, &val) {
10351                        ok = false;
10352                        break;
10353                    }
10354                    val = Some(e2);
10355                    consumed.push((v, si));
10356                }
10357                _ => {
10358                    let mut touched = false;
10359                    visit_all_exprs(st, &mut |x| {
10360                        if let Expr::Local { var, .. } = x {
10361                            if *var == v {
10362                                touched = true;
10363                            }
10364                        }
10365                    });
10366                    if touched {
10367                        ok = false;
10368                        break;
10369                    }
10370                }
10371            }
10372        }
10373        if !ok {
10374            return false;
10375        }
10376        let Some(de) = val else { return false };
10377        if contains_this_access(&de) {
10378            return false;
10379        }
10380        // Huge defs multiply per read site per round — bound them.
10381        let mut de_nodes = 0usize;
10382        visit_exprs(&de, &mut |_| de_nodes += 1);
10383        if de_nodes > 256 {
10384            return false;
10385        }
10386        deep_rewrite(cexpr, &mut |x| {
10387            if let Expr::Local { var, .. } = x {
10388                if *var == v {
10389                    *x = de.clone();
10390                }
10391            }
10392        });
10393        // Exponential blowup guard: a var read twice doubles its def
10394        // per round, compounding across the transitive chain — cap the
10395        // expression size (a 2^k node tree overflowed the worker stack
10396        // process-wide).
10397        let mut nodes = 0usize;
10398        visit_exprs(cexpr, &mut |_| nodes += 1);
10399        if nodes > 2_048 {
10400            return false;
10401        }
10402    }
10403    true
10404}
10405
10406/// Fold a prelude StringBuilder mutation chain into the delegation
10407/// argument as a fluent chain: `StringBuilder sb = new SB(x);
10408/// sb.append(a); sb.append(b); super(sb.toString())` →
10409/// `super(new SB(x).append(a).append(b).toString())`. The generic def
10410/// inliner rejects mutation calls — their effect is not expressible as
10411/// a value — but `append` RETURNS its receiver, so the chain
10412/// reconstructs exactly the same mutations in one expression (androidx
10413/// emoji2/flatbuffer UnpairedSurrogateException family, the shapes the
10414/// strict-occurrence rule was written to refuse). Aborts on any `this`
10415/// touch inside the chain (illegal before super) or any other read of
10416/// the SB local (the value must not escape the fold). Returns the
10417/// delegation's new index (statements before it were removed).
10418fn fold_sb_chain(stmts: &mut Vec<Stmt>, pos: usize) -> usize {
10419    // 1. the SB local: first `v = new StringBuilder(..)` in the prefix.
10420    let mut sbv: Option<u32> = None;
10421    let mut def_i: Option<usize> = None;
10422    let mut def_init: Option<Expr> = None;
10423    for (i, st) in stmts[..pos].iter().enumerate() {
10424        let cand: Option<(u32, &Expr)> = match st {
10425            Stmt::LocalDef {
10426                var,
10427                init: Some(e),
10428                ..
10429            } => Some((*var, e)),
10430            Stmt::ExprStmt(Expr::Assign {
10431                target,
10432                value,
10433                op: AssignOp::Plain,
10434                ..
10435            }) => match &**target {
10436                Expr::Local { var, .. } => Some((*var, value.as_ref())),
10437                _ => None,
10438            },
10439            _ => None,
10440        };
10441        if let Some((v, init)) = cand {
10442            if let Expr::New { cls, .. } = init {
10443                if cls.as_ref() == "java/lang/StringBuilder" {
10444                    sbv = Some(v);
10445                    def_i = Some(i);
10446                    def_init = Some(init.clone());
10447                    break;
10448                }
10449            }
10450        }
10451    }
10452    let (Some(sbv), Some(def_i), Some(init)) = (sbv, def_i, def_init) else {
10453        return pos;
10454    };
10455    if contains_this_access(&init) {
10456        return pos;
10457    }
10458    // 2. append statements on sbv, in order.
10459    let mut appends: Vec<usize> = Vec::new();
10460    for (i, st) in stmts[..pos].iter().enumerate() {
10461        if let Stmt::ExprStmt(Expr::Method {
10462            owner: Some(o),
10463            cls,
10464            name,
10465            ..
10466        }) = st
10467        {
10468            if matches!(&**o, Expr::Local { var, .. } if *var == sbv)
10469                && cls.as_ref() == "java/lang/StringBuilder"
10470                && &**name == "append"
10471            {
10472                appends.push(i);
10473            }
10474        }
10475    }
10476    // 3. no read of sbv after the delegation.
10477    let mut post_read = false;
10478    for st in stmts.iter().skip(pos + 1) {
10479        visit_all_exprs(st, &mut |e| {
10480            if let Expr::Local { var, .. } = e {
10481                if *var == sbv {
10482                    post_read = true;
10483                }
10484            }
10485        });
10486    }
10487    if post_read {
10488        return pos;
10489    }
10490    // 4. prefix reads of sbv only in the def and the append receivers
10491    //    (arguments are checked for `this`, receivers are the chain).
10492    for (i, st) in stmts[..pos].iter().enumerate() {
10493        if i == def_i || appends.contains(&i) {
10494            // append ARGUMENTS must not touch this; receiver is fine.
10495            if let Stmt::ExprStmt(Expr::Method { args, .. }) = st {
10496                if args.iter().any(contains_this_access) {
10497                    return pos;
10498                }
10499            }
10500            continue;
10501        }
10502        let mut leak = false;
10503        visit_all_exprs(st, &mut |e| {
10504            if let Expr::Local { var, .. } = e {
10505                if *var == sbv {
10506                    leak = true;
10507                }
10508            }
10509        });
10510        if leak {
10511            return pos;
10512        }
10513    }
10514    // 5. the call reads sbv exactly once, as `sbv.toString()`.
10515    let mut local_reads = 0usize;
10516    let mut tostring_reads = 0usize;
10517    visit_all_exprs(&stmts[pos], &mut |e| match e {
10518        Expr::Local { var, .. } if *var == sbv => local_reads += 1,
10519        Expr::Method {
10520            owner: Some(o),
10521            name,
10522            args,
10523            ..
10524        } if args.is_empty()
10525            && &**name == "toString"
10526            && matches!(&**o, Expr::Local { var, .. } if *var == sbv) =>
10527        {
10528            tostring_reads += 1;
10529        }
10530        _ => {}
10531    });
10532    if local_reads != 1 || tostring_reads != 1 {
10533        return pos;
10534    }
10535    // 6. build the fluent chain and swap it into the toString receiver.
10536    let mut chain = init;
10537    for ai in &appends {
10538        if let Stmt::ExprStmt(Expr::Method {
10539            owner: _,
10540            cls,
10541            name,
10542            desc,
10543            args,
10544            is_static,
10545            is_interface,
10546            is_special,
10547            is_super,
10548            is_dynamic,
10549            type_args,
10550            ..
10551        }) = &stmts[*ai]
10552        {
10553            chain = Expr::Method {
10554                owner: Some(Box::new(chain)),
10555                cls: cls.clone(),
10556                name: name.clone(),
10557                desc: desc.clone(),
10558                args: args.clone(),
10559                is_static: *is_static,
10560                is_interface: *is_interface,
10561                is_special: *is_special,
10562                is_super: *is_super,
10563                is_dynamic: *is_dynamic,
10564                type_args: type_args.clone(),
10565            };
10566        }
10567    }
10568    let mut replaced = false;
10569    walk_stmt_exprs(&mut stmts[pos], &mut |e| {
10570        deep_rewrite(e, &mut |x| {
10571            if let Expr::Method {
10572                owner: Some(o),
10573                name,
10574                args,
10575                ..
10576            } = x
10577            {
10578                if args.is_empty()
10579                    && &**name == "toString"
10580                    && matches!(&**o, Expr::Local { var, .. } if *var == sbv)
10581                {
10582                    **o = chain.clone();
10583                    replaced = true;
10584                }
10585            }
10586        });
10587    });
10588    if !replaced {
10589        return pos; // never remove without the swap — atomicity
10590    }
10591    // 7. drop the consumed statements (descending), adjust pos.
10592    let mut removed: Vec<usize> = appends.clone();
10593    removed.push(def_i);
10594    removed.sort_unstable();
10595    removed.dedup();
10596    for i in removed.iter().rev() {
10597        stmts.remove(*i);
10598    }
10599    pos - removed.len()
10600}
10601
10602pub fn fix_ctor_super_first(body: &mut Stmt, vt: &VarTable) {
10603    let Stmt::Block(stmts) = body else { return };
10604    if stmts.is_empty() || is_bare_ctor_call(stmts.first().unwrap()) {
10605        return;
10606    }
10607    // Top-level delegation call.
10608    if let Some(pos0) = stmts.iter().position(is_bare_ctor_call) {
10609        let pos = fold_sb_chain(stmts, pos0);
10610        let mut call = stmts[pos].clone();
10611        let prefix: Vec<&Stmt> = stmts[..pos].iter().collect();
10612        let mut consumed: Vec<(u32, usize)> = Vec::new();
10613        if !inline_ctor_arg_defs(&mut call, &prefix, vt, &mut consumed) {
10614            return;
10615        }
10616        stmts.remove(pos);
10617        // Drop the chain defs the inline consumed (all indices < pos) —
10618        // leaving them would DOUBLE-evaluate their (possibly impure)
10619        // inits. But ONLY for vars with no post-delegation use: dropping
10620        // such a var's DECLARATION makes ensure_declared re-add it at
10621        // the method top, i.e. BEFORE this() — a fresh "对this的调用必须
10622        // 是构造器中的第一个语句" (lark +587 the blind drop caused).
10623        let mut drop_idx: Vec<usize> = consumed
10624            .iter()
10625            .filter(|(v, _)| !var_occurs_in(&stmts[pos..], *v))
10626            .map(|(_, k)| *k)
10627            .collect();
10628        drop_idx.sort_unstable();
10629        drop_idx.dedup();
10630        for k in drop_idx.iter().rev() {
10631            stmts.remove(*k);
10632        }
10633        stmts.insert(0, call);
10634        return;
10635    }
10636    // The structurer can wrap the delegation in a bare nested block —
10637    // with the parameter null-checks AHEAD of it (weixin's Kotlin
10638    // intrinsics shape: `o.h(parcel, "source"); super();` at inner
10639    // positions 0/1, flattened to straight-line by cleanup AFTER this
10640    // pass, which is why the miss surfaced as 1150 weixin "对super的
10641    // 调用必须是构造器中的第一个语句"). Hoist the call from any
10642    // position whose predecessors are all straight-line statements; a
10643    // delegation behind control flow is the conditional-super family
10644    // (needs restructuring, not hoisting) and stays put.
10645    let mut found: Option<(usize, usize)> = None;
10646    for (i, st) in stmts.iter().enumerate() {
10647        if let Stmt::Block(inner) = st {
10648            if let Some(p) = inner.iter().position(is_bare_ctor_call) {
10649                if inner[..p]
10650                    .iter()
10651                    .all(|s| matches!(s, Stmt::LocalDef { .. } | Stmt::ExprStmt(_)))
10652                {
10653                    found = Some((i, p));
10654                    break;
10655                }
10656            }
10657        }
10658    }
10659    if let Some((i, p)) = found {
10660        let mut call = match &stmts[i] {
10661            Stmt::Block(inner) => inner[p].clone(),
10662            _ => unreachable!(),
10663        };
10664        let mut prefix: Vec<&Stmt> = stmts[..i].iter().collect();
10665        if let Stmt::Block(inner) = &stmts[i] {
10666            prefix.extend(inner[..p].iter());
10667        }
10668        let mut consumed: Vec<(u32, usize)> = Vec::new();
10669        if !inline_ctor_arg_defs(&mut call, &prefix, vt, &mut consumed) {
10670            return;
10671        }
10672        // Prefix index k maps to stmts[k] for k < i, inner[k - i] for
10673        // k >= i (all such k are < p). Suffix = the rest of the inner
10674        // block + the following top-level statements; a consumed def may
10675        // only drop when its var has no suffix use (declaration loss →
10676        // ensure_declared re-adds it BEFORE this()).
10677        // Compute the droppable set BEFORE mutating (borrow split).
10678        let mut ks: Vec<usize> = Vec::new();
10679        {
10680            let inner_tail: &[Stmt] = match &stmts[i] {
10681                Stmt::Block(inner) if p < inner.len() => &inner[p + 1..],
10682                _ => &[],
10683            };
10684            for (v, k) in &consumed {
10685                if !var_occurs_in(inner_tail, *v) && !var_occurs_in(&stmts[i + 1..], *v) {
10686                    ks.push(*k);
10687                }
10688            }
10689        }
10690        ks.sort_unstable();
10691        ks.dedup();
10692        if let Stmt::Block(inner) = &mut stmts[i] {
10693            inner.remove(p);
10694            for k in ks.iter().rev().filter(|k| **k >= i) {
10695                inner.remove(k - i);
10696            }
10697        }
10698        for k in ks.iter().rev().filter(|k| **k < i) {
10699            stmts.remove(*k);
10700        }
10701        stmts.insert(0, call);
10702    }
10703}
10704
10705/// A true constructor DELEGATION statement: `super(..)` (owner None) or
10706/// `this(..)` (owner This). An un-folded `new X; <init>` (lost
10707/// allocation — the ctor-fold-bug family) arrives as a `<init>` Method
10708/// with a NON-this owner and prints as `new X(..)`: it is not a
10709/// delegation and must neither satisfy nor anchor the super-first fixes
10710/// (v7.a$a2: a dead `new f(0,..)` init at position 0 made both hoists
10711/// believe the delegation was already first, leaving the real `super`
10712/// last — "对super的调用必须是构造器中的第一个语句").
10713fn is_delegation_expr(e: &Expr) -> bool {
10714    matches!(
10715        e,
10716        Expr::Method { name, is_special: true, owner, .. }
10717            if &**name == "<init>"
10718                && (owner.is_none() || matches!(owner.as_deref(), Some(Expr::This)))
10719    )
10720}
10721
10722pub(crate) fn is_bare_ctor_call(s: &Stmt) -> bool {
10723    matches!(s, Stmt::ExprStmt(e) if is_delegation_expr(e))
10724}
10725
10726/// Is the local ever ASSIGNED (write position: `x = ..`, `++x`)?
10727/// Distinct from a read count: a ctor's synthetic outer param is only
10728/// ever read; a param that gets written must stay declared.
10729pub(crate) fn local_is_written(body: &Stmt, var: u32) -> bool {
10730    let mut hit = false;
10731    visit_stmt_exprs_ro(body, &mut |e| {
10732        if hit {
10733            return;
10734        }
10735        visit_exprs(e, &mut |x| {
10736            if hit {
10737                return;
10738            }
10739            match x {
10740                Expr::Assign { target, .. }
10741                    if matches!(&**target, Expr::Local { var: v, .. } if *v == var) =>
10742                {
10743                    hit = true;
10744                }
10745                Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. }
10746                    if matches!(&**e, Expr::Local { var: v, .. } if *v == var) =>
10747                {
10748                    hit = true;
10749                }
10750                _ => {}
10751            }
10752        });
10753    });
10754    hit
10755}
10756
10757/// Source-form normalization for a non-static member-inner constructor.
10758/// The dex descriptor carries the synthetic outer instance as args[0]
10759/// (typed as the direct enclosing class); the emitter's qualified
10760/// `outer.new Inner(..)` / `this.new Inner(..)` sites pass it
10761/// implicitly and `super(..)` delegations drop it, so the signature
10762/// loses the param — and the body must stop referencing it: plain uses
10763/// become `Outer.this` (a `this.this$0 = p` assignment becomes
10764/// `this.this$0 = Outer.this`, exactly the runtime value), and
10765/// this()-delegations to `eligible` classes drop the leading param arg.
10766pub(crate) fn rewrite_inner_ctor_outer_param(
10767    body: &mut Stmt,
10768    param0: u32,
10769    outer_display: &str,
10770    outer_ty: &TypeRef,
10771    eligible: &[String],
10772) {
10773    // this()/super() delegations (and lost-alloc construction calls):
10774    // the leading arg IS the synthetic outer exactly when it is the
10775    // param itself.
10776    walk_stmt_exprs(body, &mut |e| {
10777        if let Expr::Method { name, cls, args, .. } = e {
10778            if &**name == "<init>"
10779                && eligible.iter().any(|c| c.as_str() == cls.as_ref())
10780                && args.first().is_some_and(|a| matches!(a, Expr::Local { var: v, .. } if *v == param0))
10781            {
10782                args.remove(0);
10783            }
10784        }
10785    });
10786    // Every remaining read of the param becomes `Outer.this`.
10787    let text = format!("{outer_display}.this");
10788    let ty = outer_ty.clone();
10789    walk_stmt_exprs(body, &mut |e| {
10790        deep_rewrite_reads(e, &mut |x| {
10791            if let Expr::Local { var: v, .. } = x {
10792                if *v == param0 {
10793                    *x = Expr::RawT(text.clone(), ty.clone());
10794                }
10795            }
10796        });
10797    });
10798}
10799
10800/// Ctors whose delegation is buried in control flow or behind arg
10801/// computations — the "对super的调用必须是构造器中的第一个语句" family
10802/// that plain hoisting cannot reach (weixin 264 / weibo 116 / reqable 71
10803/// / lark 62 after round 60): R8/Kotlin compute super-args conditionally
10804/// and dex legally invokes super mid-branch.
10805///
10806/// Shape B (branch merge): `prelude; if (c) {d1; super(A1); t1} else
10807/// {d2; super(A2); t2}; post` → one leading `super(..)` where each
10808/// differing arg becomes `c ? a1i : a2i`; branch remainders stay an
10809/// if/else AFTER the call; prelude statements move after it (nothing
10810/// may precede super in Java; bytecode pre-super work is local-only by
10811/// construction — dex forbids instance-field reads before the delegate).
10812///
10813/// Shape A (linear): `defs; super(A); rest` where `A` references the
10814/// def locals — plain hoisting (fix_ctor_super_first case 1) would move
10815/// the call above its argument definitions (forward refs). Inline the
10816/// referenced defs into the args first, drop the consumed defs, hoist.
10817/// Runs BEFORE fix_ctor_super_first and only fires when args reference
10818/// preceding locals, leaving the battle-tested plain hoist untouched
10819/// otherwise.
10820///
10821/// Inlining duplicates the def per use site, so a side-effecting def is
10822/// inlined only when its var's use count is provably preserved: a
10823/// single use in a merged arg (1 copy), or a single use in the
10824/// condition with ≤1 differing arg (the cond then appears in the `if`
10825/// plus one ternary — 2 copies of e.g. one Kotlin getter call, which
10826/// the original bytecode typically made twice anyway: `p.u() == null ?
10827/// .. : p.u().i()`). Pure defs inline freely. Every other shape aborts:
10828/// broken-but-faithful stays the status quo, wrong evaluation counts
10829/// would be worse.
10830/// Linear `def; delegation(args read def)` shapes. A plain hoist of the
10831/// delegation past its prefix would place the call ABOVE a definition
10832/// it reads — a forward reference (`super(context2, ..)` with `context2
10833/// = ..` below it). javac's attribution for the whole class then
10834/// collapses: the undefined identifier cascades into "non-static
10835/// super" and even bare `Object` resolution failures, poisoning every
10836/// later diagnostic in the file (weibo AppCompatTextView — the root of
10837/// a 4.1k-file Object cascade). The original source had the def's
10838/// expression INLINE in the delegation args; d8 computed it into a
10839/// register first. Inline the def into the args (the def is read
10840/// exactly once there — evaluation count preserved even for calls),
10841/// and when the register is REASSIGNED later (a fresh generation
10842/// sharing the slot), convert that first write into the declaration so
10843/// the var stays defined for its later readers. Any shape that cannot
10844/// be proven aborts untouched.
10845/// Count WRITE positions of `var` (assign / inc-dec targets) — the
10846/// shape-C validator needs exact counts, not just presence.
10847fn stmts_count_writes(stmts: &[Stmt], var: u32) -> usize {
10848    let mut n = 0usize;
10849    for s in stmts {
10850        visit_stmt_exprs_ro(s, &mut |e| {
10851            visit_exprs(e, &mut |x| match x {
10852                Expr::Assign { target, .. }
10853                    if matches!(&**target, Expr::Local { var: v, .. } if *v == var) =>
10854                {
10855                    n += 1;
10856                }
10857                Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. }
10858                    if matches!(&**e, Expr::Local { var: v, .. } if *v == var) =>
10859                {
10860                    n += 1;
10861                }
10862                _ => {}
10863            });
10864        });
10865    }
10866    n
10867}
10868
10869/// Collect every local WRITTEN in `stmts` — the complement inside a
10870/// ctor prefix is the param set (the only locals a pre-delegation arg
10871/// expression may read).
10872fn prefix_written_vars(stmts: &[Stmt]) -> jdc_core::FxHashSet<u32> {
10873    let mut set = jdc_core::FxHashSet::default();
10874    for s in stmts {
10875        let mut c = s.clone();
10876        walk_stmt_exprs(&mut c, &mut |e| {
10877            deep_rewrite(e, &mut |x| match x {
10878                Expr::Assign { target, .. } => {
10879                    if let Expr::Local { var, .. } = &**target {
10880                        set.insert(*var);
10881                    }
10882                }
10883                Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => {
10884                    if let Expr::Local { var, .. } = &**e {
10885                        set.insert(*var);
10886                    }
10887                }
10888                _ => {}
10889            });
10890        });
10891        if let Stmt::LocalDef { var, .. } = s {
10892            set.insert(*var);
10893        }
10894    }
10895    set
10896}
10897
10898/// Is `e` safe to evaluate INSIDE a ctor-delegation argument (before
10899/// this/super exists)? Params/consts/pure ops only — no instance-field
10900/// reads, no `this`, no captures.
10901fn pre_this_safe(e: &Expr, prefix_vars: &jdc_core::FxHashSet<u32>) -> bool {
10902    match e {
10903        Expr::Local { var, .. } => !prefix_vars.contains(var),
10904        Expr::Const(_) => true,
10905        Expr::Cast { e, .. } => pre_this_safe(e, prefix_vars),
10906        Expr::Bin { l, r, .. } => {
10907            pre_this_safe(l, prefix_vars) && pre_this_safe(r, prefix_vars)
10908        }
10909        Expr::Cond { c, t, f } => {
10910            pre_this_safe(c, prefix_vars)
10911                && pre_this_safe(t, prefix_vars)
10912                && pre_this_safe(f, prefix_vars)
10913        }
10914        Expr::Field { owner, is_static, .. } => {
10915            *is_static
10916                && owner
10917                    .as_ref()
10918                    .is_none_or(|o| pre_this_safe(o, prefix_vars))
10919        }
10920        Expr::Method {
10921            owner,
10922            args,
10923            is_static,
10924            ..
10925        } => {
10926            args.iter().all(|a| pre_this_safe(a, prefix_vars))
10927                && match owner {
10928                    Some(o) => pre_this_safe(o, prefix_vars),
10929                    None => *is_static,
10930                }
10931        }
10932        // Allocation in a delegation arg is legal Java before this()
10933        // (the ctor call touches no instance state of THIS class) — the
10934        // missing arm sent every `new Date()`-style Kotlin default
10935        // through `_ => false`, rejecting the whole fold (lark
10936        // Request$AppToApp this-not-first family, ~650 lark lines).
10937        // An inner-class construction needing an outer `this` carries
10938        // Expr::This in its args, which still fails below.
10939        Expr::New { args, .. } => args.iter().all(|a| pre_this_safe(a, prefix_vars)),
10940        Expr::NewArray { dims, init, .. } => {
10941            dims.iter().all(|d| pre_this_safe(d, prefix_vars))
10942                && init
10943                    .as_ref()
10944                    .map(|l| l.iter().all(|x| pre_this_safe(x, prefix_vars)))
10945                    .unwrap_or(true)
10946        }
10947        // Pure unary/instanceof/array reads.
10948        Expr::Un { e, .. } | Expr::InstanceOf { e, .. } => pre_this_safe(e, prefix_vars),
10949        Expr::ArrayIndex { array, index } => {
10950            pre_this_safe(array, prefix_vars) && pre_this_safe(index, prefix_vars)
10951        }
10952        _ => false,
10953    }
10954}
10955
10956/// `s` is exactly one plain assignment to local `v` (optionally wrapped
10957/// in a singleton Block): the branch shape of a Kotlin default-arg
10958/// override.
10959fn single_assign_to_v(s: &Stmt, v: u32) -> Option<Expr> {
10960    match s {
10961        Stmt::ExprStmt(Expr::Assign {
10962            target,
10963            op: AssignOp::Plain,
10964            value,
10965        }) if matches!(&**target, Expr::Local { var: vv, .. } if *vv == v) => {
10966            Some((**value).clone())
10967        }
10968        Stmt::Block(xs) if xs.len() == 1 => single_assign_to_v(&xs[0], v),
10969        _ => None,
10970    }
10971}
10972
10973/// Kotlin default-argument BRIDGE ctors (shape C of the delegation
10974/// family): `<init>(params.., int mask, DefaultConstructorMarker)`
10975/// computes each defaulted arg into a local — `v = p; if ((mask&bit)
10976/// != 0) { v = DEFAULT; }` — then delegates `this(.., v, ..)`. Java
10977/// allows NOTHING before the delegation, so the computations must fold
10978/// INTO the args (`this(.., (mask&bit) != 0 ? DEFAULT : p, ..)`).
10979/// Unfolded, the hoist passes lift the delegation above the raw local
10980/// reads: "找不到符号 变量 str2/v4/creationExtras2" — 2.5k root files
10981/// across weixin/lark/weibo (weixin MvvmObserverOwner$..Observer, lark
10982/// SimpleArrayMap/LongSparseArray). Per-var provability: exactly one
10983/// top-level base write, an optional single conditional override whose
10984/// branches are lone assignments, no other prefix touch of the var,
10985/// nothing after the delegation touches it, and the folded expression
10986/// is pre-this-safe. Unprovable vars stay untouched (partial folds are
10987/// strictly better — the rest was broken before too).
10988/// Flatten nested plain Blocks among the top-level statements. The
10989/// structurer hands ctor bodies as region-nested blocks (`Block([def,
10990/// Block([assign, if]), Block([delegate, return])])` — the Kotlin
10991/// bridge shape), and every delegation-repair pass scans the TOP level
10992/// only. Straight-line nesting flattens without semantics; control
10993/// flow lives inside If/While/etc. arms, never as a direct member.
10994pub(crate) fn flatten_top_blocks(stmts: &mut Vec<Stmt>) {
10995    fn rec(s: Stmt, out: &mut Vec<Stmt>) {
10996        match s {
10997            Stmt::Block(inner) => {
10998                for x in inner {
10999                    rec(x, out);
11000                }
11001            }
11002            other => out.push(other),
11003        }
11004    }
11005    if !stmts.iter().any(|s| matches!(s, Stmt::Block(_))) {
11006        return;
11007    }
11008    let mut out: Vec<Stmt> = Vec::with_capacity(stmts.len());
11009    for s in std::mem::take(stmts) {
11010        rec(s, &mut out);
11011    }
11012    *stmts = out;
11013}
11014
11015fn fold_default_arg_bridge(stmts: &mut Vec<Stmt>) -> bool {
11016    flatten_top_blocks(stmts);
11017    let Some(pos) = stmts.iter().position(is_bare_ctor_call) else {
11018        return false;
11019    };
11020    if pos == 0 {
11021        return false;
11022    }
11023    let Stmt::ExprStmt(Expr::Method { args, .. }) = &stmts[pos] else {
11024        return false;
11025    };
11026    let mut arg_vars: Vec<u32> = Vec::new();
11027    let mut reads = 0usize;
11028    for a in args {
11029        let mut c = a.clone();
11030        deep_rewrite_reads(&mut c, &mut |x| {
11031            if let Expr::Local { var, .. } = x {
11032                reads += 1;
11033                if !arg_vars.contains(var) {
11034                    arg_vars.push(*var);
11035                }
11036            }
11037        });
11038    }
11039    if arg_vars.is_empty() || reads != arg_vars.len() {
11040        return false; // a var read twice would duplicate evaluation
11041    }
11042    // The bridge ENDS at the delegation: nothing after may touch the
11043    // arg vars (reads OR writes).
11044    for &v in &arg_vars {
11045        if stmts_read_var(&stmts[pos + 1..], v) != 0
11046            || stmts_count_writes(&stmts[pos + 1..], v) != 0
11047        {
11048            return false;
11049        }
11050    }
11051    let prefix_vars = prefix_written_vars(&stmts[..pos]);
11052    let mut repl: Vec<(u32, Expr)> = Vec::new();
11053    let mut drop: Vec<usize> = Vec::new();
11054    for &v in &arg_vars {
11055        // Exact prefix shape for v.
11056        let mut base: Option<(usize, Expr)> = None;
11057        let mut decl: Option<usize> = None;
11058        let mut condf: Option<(usize, Expr, Option<Expr>, Option<Expr>)> = None;
11059        let mut ok = true;
11060        for (i, s) in stmts[..pos].iter().enumerate() {
11061            match s {
11062                Stmt::LocalDef { var, init, .. } if *var == v => match init {
11063                    None => {
11064                        if decl.is_some() {
11065                            ok = false;
11066                            break;
11067                        }
11068                        decl = Some(i);
11069                    }
11070                    Some(e) => {
11071                        if base.is_some() {
11072                            ok = false;
11073                            break;
11074                        }
11075                        base = Some((i, e.clone()));
11076                    }
11077                },
11078                Stmt::ExprStmt(Expr::Assign {
11079                    target,
11080                    op: AssignOp::Plain,
11081                    value,
11082                }) if matches!(&**target, Expr::Local { var: vv, .. } if *vv == v) => {
11083                    if base.is_some() {
11084                        ok = false;
11085                        break;
11086                    }
11087                    base = Some((i, (**value).clone()));
11088                }
11089                Stmt::If {
11090                    cond,
11091                    then_stmt,
11092                    else_stmt,
11093                } => {
11094                    let t = single_assign_to_v(then_stmt, v);
11095                    let f = else_stmt.as_ref().and_then(|b| single_assign_to_v(b, v));
11096                    let then_touches = t.is_some()
11097                        || stmts_read_var(std::slice::from_ref(&**then_stmt), v) != 0
11098                        || stmts_count_writes(std::slice::from_ref(&**then_stmt), v) != 0;
11099                    let else_touches = else_stmt.as_ref().is_some_and(|b| {
11100                        f.is_some()
11101                            || stmts_read_var(std::slice::from_ref(b), v) != 0
11102                            || stmts_count_writes(std::slice::from_ref(b), v) != 0
11103                    });
11104                    if !then_touches && !else_touches {
11105                        continue; // unrelated if
11106                    }
11107                    // Both branches must be lone assignments (a branch
11108                    // with its own delegation/return is the conditional-
11109                    // super machinery's shape, not this one), the cond
11110                    // must not read v, and only one override per var.
11111                    if condf.is_some()
11112                        || base.is_none()
11113                        || (then_touches && t.is_none())
11114                        || (else_touches && f.is_none())
11115                    {
11116                        ok = false;
11117                        break;
11118                    }
11119                    let cond_stmt = Stmt::ExprStmt(cond.clone());
11120                    if stmts_read_var(std::slice::from_ref(&cond_stmt), v) != 0 {
11121                        ok = false;
11122                        break;
11123                    }
11124                    condf = Some((i, cond.clone(), t, f));
11125                }
11126                _ => {}
11127            }
11128        }
11129        if !ok {
11130            continue;
11131        }
11132        let Some((bi, be)) = base else { continue };
11133        // Exact write/read accounting: base (+then/else) writes, zero
11134        // prefix reads.
11135        let want_writes = 1
11136            + condf
11137                .as_ref()
11138                .map(|(_, _, t, f)| t.is_some() as usize + f.is_some() as usize)
11139                .unwrap_or(0);
11140        if stmts_count_writes(&stmts[..pos], v) != want_writes
11141            || stmts_read_var(&stmts[..pos], v) != 0
11142        {
11143            continue;
11144        }
11145        let rep = match condf {
11146            Some((ci, c, t, f)) => {
11147                drop.push(ci);
11148                match (t, f) {
11149                    (Some(t), Some(f)) => Expr::Cond {
11150                        c: Box::new(c),
11151                        t: Box::new(t),
11152                        f: Box::new(f),
11153                    },
11154                    (Some(t), None) => Expr::Cond {
11155                        c: Box::new(c),
11156                        t: Box::new(t),
11157                        f: Box::new(be.clone()),
11158                    },
11159                    (None, Some(f)) => Expr::Cond {
11160                        c: Box::new(c),
11161                        t: Box::new(be.clone()),
11162                        f: Box::new(f),
11163                    },
11164                    (None, None) => unreachable!("condf requires a branch assign"),
11165                }
11166            }
11167            None => be.clone(),
11168        };
11169        if !pre_this_safe(&rep, &prefix_vars) {
11170            continue;
11171        }
11172        drop.push(bi);
11173        if let Some(d) = decl {
11174            drop.push(d);
11175        }
11176        repl.push((v, rep));
11177    }
11178    if repl.is_empty() {
11179        return false;
11180    }
11181    if let Stmt::ExprStmt(Expr::Method { args, .. }) = &mut stmts[pos] {
11182        for a in args.iter_mut() {
11183            deep_rewrite_reads(a, &mut |x| {
11184                if let Expr::Local { var, .. } = x {
11185                    if let Some((_, e)) = repl.iter().find(|(vv, _)| vv == var) {
11186                        *x = e.clone();
11187                    }
11188                }
11189            });
11190        }
11191    }
11192    drop.sort_unstable();
11193    drop.dedup();
11194    for i in drop.into_iter().rev() {
11195        stmts.remove(i);
11196    }
11197    true
11198}
11199
11200/// Rewrite static member references on Kotlin multi-file facade PARTS
11201/// (`StringsKt__StringsKt.trim(..)`) to the public facade the parts are
11202/// compiled into (`StringsKt.trim(..)`): the parts are package-private,
11203/// so every cross-package call site is "在kotlin.text中不是公共的; 无法
11204/// 从外部程序包中对其进行访问" (lark 1.4k root files; weibo's 8.4k-line
11205/// Kt cluster). Static members resolve through the facade by
11206/// inheritance (the map is gated on the extends chain).
11207pub fn rewrite_kotlin_facades(body: &mut Stmt, pool: &crate::DexPool) {
11208    let map = pool.kotlin_facade_map();
11209    if map.is_empty() {
11210        return;
11211    }
11212    walk_stmt_exprs(body, &mut |e| {
11213        match e {
11214            Expr::Method {
11215                cls,
11216                name,
11217                desc,
11218                is_static,
11219                ..
11220            } if *is_static => {
11221                if let Some((base, cover)) = map.get(cls.as_ref()) {
11222                    let hit = match cover {
11223                        None => true,
11224                        Some(c) => c.0.contains(&(name.to_string(), desc.to_string())),
11225                    };
11226                    if hit {
11227                        *cls = std::sync::Arc::from(base.as_str());
11228                    }
11229                }
11230            }
11231            Expr::Field {
11232                cls,
11233                name,
11234                is_static,
11235                ..
11236            } if *is_static => {
11237                if let Some((base, cover)) = map.get(cls.as_ref()) {
11238                    let hit = match cover {
11239                        None => true,
11240                        Some(c) => c.1.contains(name.as_ref()),
11241                    };
11242                    if hit {
11243                        *cls = std::sync::Arc::from(base.as_str());
11244                    }
11245                }
11246            }
11247            _ => {}
11248        }
11249    });
11250}
11251
11252/// Kotlin default-arg bridge fold for ENUM ctors: the synthetic
11253/// `(String, int, .., int, DefaultConstructorMarker)` ctor computes
11254/// defaulted args then chains `this(..)` mid-body — Java requires the
11255/// delegation first (对this的调用必须是构造器中的第一个语句, lark's
11256/// UserCustomStatusExtraParams$* family, 752 lines). Same fold the
11257/// class-ctor path runs; the enum branch of the pipeline used to skip
11258/// it (only strip_enum_ctor_super ran there).
11259pub fn fold_enum_default_arg_bridge(body: &mut Stmt) {
11260    let Stmt::Block(stmts) = body else { return };
11261    fold_default_arg_bridge(stmts);
11262}
11263
11264pub fn fix_ctor_delegation_arg_defs(body: &mut Stmt) {
11265    let Stmt::Block(stmts) = body else { return };
11266    // Shape C first: the Kotlin default-arg bridge (computations +
11267    // conditional overrides folded into the delegation args).
11268    fold_default_arg_bridge(stmts);
11269    // Only the top-level linear shape; the first statement needs no
11270    // repair and control flow ahead of the delegation belongs to the
11271    // conditional-super machinery.
11272    let Some(pos) = stmts.iter().position(is_bare_ctor_call) else {
11273        return;
11274    };
11275    if pos == 0 {
11276        return;
11277    }
11278    // Vars read by the delegation args, and the total read count (a var
11279    // read twice would duplicate a call evaluation on inline).
11280    let mut arg_vars: Vec<u32> = Vec::new();
11281    let mut total_reads = 0usize;
11282    {
11283        let Stmt::ExprStmt(Expr::Method { args, .. }) = &stmts[pos] else {
11284            return;
11285        };
11286        for a in args {
11287            let mut c = a.clone();
11288            deep_rewrite_reads(&mut c, &mut |x| {
11289                if let Expr::Local { var: v, .. } = x {
11290                    total_reads += 1;
11291                    if !arg_vars.contains(v) {
11292                        arg_vars.push(*v);
11293                    }
11294                }
11295            });
11296        }
11297    }
11298    if arg_vars.is_empty() || total_reads != arg_vars.len() {
11299        return;
11300    }
11301    // Vars DEFINED anywhere in the prefix (for the init-referenced
11302    // check below — an inlined init may not itself read prefix defs).
11303    let prefix_defs: Vec<u32> = stmts[..pos]
11304        .iter()
11305        .filter_map(|s| match s {
11306            Stmt::LocalDef { var, init: Some(_), .. } => Some(*var),
11307            _ => None,
11308        })
11309        .collect();
11310    // Per referenced var: exactly one prefix def, no other prefix
11311    // reader between def and delegation, the def's init reads no
11312    // prefix-def var, and the first post-delegation use is a top-level
11313    // write (becomes the declaration) or nothing at all.
11314    let mut plan: Vec<(u32, usize, Option<usize>)> = Vec::new();
11315    for v in arg_vars {
11316        let defs: Vec<usize> = stmts[..pos]
11317            .iter()
11318            .enumerate()
11319            .filter(|(i, _)| {
11320                matches!(&stmts[*i], Stmt::LocalDef { var, init: Some(_), .. } if *var == v)
11321            })
11322            .map(|(i, _)| i)
11323            .collect();
11324        if defs.len() > 1 {
11325            return; // ambiguous: multiple prefix defs
11326        }
11327        let Some(&q) = defs.first() else {
11328            continue; // a param: nothing to inline for this var
11329        };
11330        let init = match &stmts[q] {
11331            Stmt::LocalDef { init: Some(e), .. } => e.clone(),
11332            _ => return,
11333        };
11334        if stmts_read_var(&stmts[q + 1..pos], v) != 0 {
11335            return; // another reader between the def and the delegation
11336        }
11337        // The inlined init may not read a prefix-def var either — the
11338        // delegation lands ABOVE those defs after the hoist.
11339        let mut init_vars: Vec<u32> = Vec::new();
11340        deep_rewrite_reads(&mut { init.clone() }, &mut |x| {
11341            if let Expr::Local { var: vv, .. } = x {
11342                if !init_vars.contains(vv) {
11343                    init_vars.push(*vv);
11344                }
11345            }
11346        });
11347        if init_vars.iter().any(|iv| prefix_defs.contains(iv)) {
11348            return;
11349        }
11350        match stmts[pos + 1..]
11351            .iter()
11352            .position(|s| {
11353                matches!(s, Stmt::ExprStmt(Expr::Assign { target, .. })
11354                    if matches!(&**target, Expr::Local { var: vv, .. } if *vv == v))
11355            }) {
11356            Some(w) => {
11357                // Reads before the re-declaration would be dangling.
11358                if stmts_read_var(&stmts[pos + 1..pos + 1 + w], v) != 0 {
11359                    return;
11360                }
11361                plan.push((v, q, Some(pos + 1 + w)));
11362            }
11363            None => {
11364                // No later write: the def must be dead after inline.
11365                if stmts_read_var(&stmts[pos + 1..], v) != 0 {
11366                    return;
11367                }
11368                plan.push((v, q, None));
11369            }
11370        }
11371    }
11372    if plan.is_empty() {
11373        return;
11374    }
11375    // Inline the def inits into the delegation args.
11376    let values: Vec<(u32, Expr)> = plan
11377        .iter()
11378        .map(|(_v, q, _)| match &stmts[*q] {
11379            Stmt::LocalDef { var, init: Some(e), .. } => (*var, e.clone()),
11380            _ => unreachable!("plan entries carry a LocalDef with init"),
11381        })
11382        .collect();
11383    if let Stmt::ExprStmt(Expr::Method { args, .. }) = &mut stmts[pos] {
11384        for a in args {
11385            deep_rewrite_reads(a, &mut |x| {
11386                if let Expr::Local { var: v, .. } = x {
11387                    if let Some((_, e)) = values.iter().find(|(vv, _)| vv == v) {
11388                        *x = e.clone();
11389                    }
11390                }
11391            });
11392        }
11393    }
11394    // Convert the first writes into declarations BEFORE removing defs
11395    // (removals shift indices); then remove the defs in descending
11396    // order.
11397    for (v, _, w) in &plan {
11398        if let Some(w) = w {
11399            if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = &mut stmts[*w] {
11400                if matches!(&**target, Expr::Local { var: vv, .. } if vv == v) {
11401                    let taken = std::mem::replace(&mut **value, Expr::Const(ConstVal::Null));
11402                    stmts[*w] = Stmt::LocalDef {
11403                        var: *v,
11404                        init: Some(taken),
11405                        is_final: false,
11406                        force_type: false,
11407                    };
11408                }
11409            }
11410        }
11411    }
11412    let mut order: Vec<usize> = plan.iter().map(|(_, q, _)| *q).collect();
11413    order.sort_unstable();
11414    order.reverse();
11415    for q in order {
11416        stmts.remove(q);
11417    }
11418}
11419
11420/// An INT-typed method returning a boolean value: the register held the
11421/// boolean test result in one generation and the `if (v) 1 else 0` int
11422/// in another; booleanize picks the boolean generation and the plain
11423/// `return v;` breaks ("boolean无法转换为int", weixin Handle
11424/// onPausableTransaction — the source was `if (v) 1 else 0`). Wrap the
11425/// return in the recovering conditional.
11426/// Mirror of fix_int_returns: a BOOLEAN method returning a numeric-
11427/// typed value (register reuse left an int generation) wraps it
11428/// `x != 0` — the dex branch encoding, and the only legal Java form
11429/// (kh5/a `return v116_g53;` int无法转换为boolean family).
11430pub fn fix_bool_returns(vt: &VarTable, body: &mut Stmt) {
11431    walk_mut_deep(body, &mut |st| {
11432        if let Stmt::Return(Some(e)) = st {
11433            let numeric = match &*e {
11434                Expr::Local { var, ty } => {
11435                    let t = if (*var as usize) < vt.vars.len() {
11436                        vt.var(*var).ty.erased()
11437                    } else {
11438                        ty.erased()
11439                    };
11440                    matches!(
11441                        t,
11442                        JavaType::Int
11443                            | JavaType::Long
11444                            | JavaType::Short
11445                            | JavaType::Byte
11446                            | JavaType::Char
11447                    )
11448                }
11449                other => matches!(
11450                    other.type_ref().erased(),
11451                    JavaType::Int | JavaType::Long | JavaType::Short | JavaType::Byte
11452                        | JavaType::Char
11453                ),
11454            };
11455            if numeric {
11456                let taken = e.clone();
11457                *e = Expr::Bin {
11458                    op: BinOp::Ne,
11459                    l: Box::new(taken),
11460                    r: Box::new(Expr::Const(ConstVal::Int(0))),
11461                    ty: Some(TypeRef::J(JavaType::Boolean)),
11462                };
11463            }
11464        }
11465    });
11466}
11467
11468pub fn fix_int_returns(vt: &VarTable, body: &mut Stmt) {
11469    walk_mut_deep(body, &mut |st| {
11470        if let Stmt::Return(Some(e)) = st {
11471            let is_bool = match e {
11472                Expr::Local { var, .. } => {
11473                    (*var as usize) < vt.vars.len()
11474                        && vt.vars[*var as usize].ty.erased() == JavaType::Boolean
11475                }
11476                Expr::Method { desc, .. } => desc.ret == JavaType::Boolean,
11477                _ => false,
11478            };
11479            if is_bool {
11480                *e = Expr::Cond {
11481                    c: Box::new(e.clone()),
11482                    t: Box::new(Expr::Const(ConstVal::Int(1))),
11483                    f: Box::new(Expr::Const(ConstVal::Int(0))),
11484                };
11485            }
11486        }
11487    });
11488}
11489
11490/// Generation splitting: when an assignment's value type is
11491/// INCOMPATIBLE with the var's declared type, mint a FRESH var for the
11492/// new generation instead of widening (widening was measured twice as
11493/// net-negative — every use-site cast gap re-triggers javac's
11494/// error-type contagion). Reads after the split point follow the
11495/// active generation; control-flow joins intersect; loops run a
11496/// 3-round fixpoint so the back-edge carries the tail generation to
11497/// the head reads (the loop-state-tuple shape, qf5/v0's
11498/// `u0 = (qf5.u0) get;` … `u0 = looper2;`).
11499///
11500/// Split condition (reference incompatibility or a primitive/value
11501/// mix — the instanceof-corruption shape): both specific references of
11502/// different classes, or prim→ref. NOT numeric-vs-numeric (promotion
11503/// handles those) and NOT Object targets (assignable — the narrowing
11504/// cast pass covers the value side).
11505static WIDEN_COUNT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
11506
11507pub fn split_generations(vt: &mut VarTable, body: &mut Stmt, pool: &DexPool) {
11508    if std::env::var("DDC_DBG_PHI").is_ok() {
11509        eprintln!("[widen] cumulative={}", WIDEN_COUNT.load(std::sync::atomic::Ordering::Relaxed));
11510    }
11511    let mut counter: u32 = 0;
11512    let mut gen: std::collections::HashMap<u32, u32> = std::collections::HashMap::default();
11513    split_walk_stmt(body, vt, &mut gen, &mut counter, pool);
11514}
11515
11516/// Should the assignment `target = value` split the target?
11517fn split_needed(vt: &VarTable, target: u32, value: &Expr, pool: &DexPool) -> bool {
11518    let n = vt.vars.len();
11519    if target as usize >= n {
11520        return false;
11521    }
11522    let declared = vt.vars[target as usize].ty.erased();
11523    let vt_is_ref = |t: &JavaType| matches!(t, JavaType::Object(_) | JavaType::Array(_));
11524    // Locals consult the VarTable: the embedded ty is a lift-time
11525    // snapshot, stale after booleanize converts a copy SOURCE (the
11526    // post-booleanize re-split below relies on seeing `v131` as the
11527    // boolean it became). Bitwise Or/And/Xor over boolean operands
11528    // carry the same staleness at the EXPRESSION level (dex lowered
11529    // Kotlin's non-short-circuit `or` to int `|` — the embedded Bin ty
11530    // stays Int after booleanize converts the operands): `int v18 =
11531    // delete | delete2` must split the target generation.
11532    fn side_bool(e: &Expr, vt: &VarTable) -> bool {
11533        match e {
11534            Expr::Local { var, .. } if (*var as usize) < vt.vars.len() => {
11535                matches!(vt.var(*var).ty.erased(), JavaType::Boolean)
11536            }
11537            Expr::Const(ConstVal::Int(0 | 1)) => true,
11538            Expr::Bin { op: BinOp::Or | BinOp::And | BinOp::Xor, l, r, .. } => {
11539                side_bool(l, vt) && side_bool(r, vt)
11540            }
11541            other => matches!(other.type_ref().erased(), JavaType::Boolean),
11542        }
11543    }
11544    let value_ty = match value {
11545        Expr::Local { var, ty } if (*var as usize) < vt.vars.len() => vt.var(*var).ty.erased(),
11546        Expr::Bin {
11547            op: BinOp::Or | BinOp::And | BinOp::Xor,
11548            l,
11549            r,
11550            ..
11551        } if side_bool(l, vt) && side_bool(r, vt) => JavaType::Boolean,
11552        other => other.type_ref().erased(),
11553    };
11554    if vt_is_ref(&declared) && vt_is_ref(&value_ty) {
11555        // Both references: split only when incompatible — different
11556        // classes where neither side is Object AND the value is not a
11557        // SUBTYPE of the declared type. An assignable store needs no
11558        // fresh generation; splitting one minted an orphan at an
11559        // if-join whose sibling arm kept the base (rename_gen_back's
11560        // primitive-only gate refused the undo, drop_dead_locals then
11561        // stripped the orphan to a bare allocation): pq0/g's bridge
11562        // `map2 = new LinkedHashMap()` against the HashMap-typed phi
11563        // rendered `new LinkedHashMap();` and the arm lost its store.
11564        let obj = JavaType::Object("java/lang/Object".into());
11565        if declared == value_ty || declared == obj || value_ty == obj {
11566            return false;
11567        }
11568        // A FRESH ALLOCATION whose class relation to the declared type
11569        // is UNKNOWABLE (a framework↔framework pair — LinkedHashMap
11570        // into a HashMap-typed phi): the dex stored the instance into
11571        // this register flow, so the store was verifier-legal; the
11572        // split cannot know better and orphaned the generation (the
11573        // if-join undo is primitive-only; the dropper stripped the
11574        // orphan to a bare `new LinkedHashMap();` and pq0/g's bridge
11575        // lost the else-arm store, killing the whole branched-super
11576        // merge). Pool-known pairs KEEP the split: the generation's
11577        // precise type is what downstream subtype sinks need (weibo
11578        // `v7 = new $reportWhenComplete$1(..)` into a ContinuationImpl
11579        // phi — exempting it made `v9($1) = v7` inconvertible, +50).
11580        if let (JavaType::Object(v), JavaType::Object(d)) = (&value_ty, &declared) {
11581            if matches!(value, Expr::New { .. })
11582                && (pool.get(v).is_none() || pool.get(d).is_none())
11583            {
11584                return false;
11585            }
11586        }
11587        true
11588    } else {
11589        // Primitive/reference mix: the value generation has a kind the
11590        // target can never accept — split (the declared type stays with
11591        // the old generation's readers).
11592        if vt_is_ref(&declared) != vt_is_ref(&value_ty) {
11593            return true;
11594        }
11595        // Boolean/numeric mix: `boolean` and `int` have NO cast between
11596        // them in Java, so a register reused across the kinds can only
11597        // be expressed as two generations (weixin ConstraintLayout
11598        // `boolean v60; int v56; v60 = v56;` — 9.5k bool↔int lines once
11599        // the obscuring suppression lifted). 0/1 constants are the
11600        // boolean encoding, not a numeric generation — never split on
11601        // those (booleanize owns them).
11602        let d_bool = matches!(declared, JavaType::Boolean);
11603        let v_bool = matches!(value_ty, JavaType::Boolean);
11604        let zero_one = matches!(value, Expr::Const(ConstVal::Int(0 | 1)));
11605        d_bool != v_bool && !zero_one
11606    }
11607}
11608
11609fn rewrite_gen_reads(e: &mut Expr, gen: &std::collections::HashMap<u32, u32>, vt: &VarTable) {
11610    deep_rewrite(e, &mut |x| {
11611        if let Expr::Local { var, ty } = x {
11612            if let Some(&to) = gen.get(var) {
11613                if to != *var {
11614                    *var = to;
11615                    *ty = vt.var(to).ty.clone();
11616                }
11617            }
11618        }
11619    });
11620}
11621
11622fn do_split(
11623    vt: &mut VarTable,
11624    target: &mut Expr,
11625    old: u32,
11626    value_ty: &JavaType,
11627    gen: &mut std::collections::HashMap<u32, u32>,
11628    counter: &mut u32,
11629) {
11630    *counter += 1;
11631    let info = &vt.vars[old as usize];
11632    let id = vt.vars.len() as u32;
11633    let name = format!("{}_g{}", info.name, counter);
11634    let slot = info.slot;
11635    // A split generation is a fresh LOCAL, not a parameter — even when
11636    // the parent register is one (the param identity stays on the
11637    // original var id; the generation only carries a later write). The
11638    // inherited flag made booleanize (and every other param-skip pass)
11639    // permanently ignore param-register generations: weixin v2/i's
11640    // `int v264_g160_g7 = !v200` with `!= 0` reads (bool→int assign
11641    // family, 2.8k lines).
11642    let is_param = false;
11643    vt.vars.push(jdc_core::var::VarInfo {
11644        id,
11645        slot,
11646        name,
11647        ty: TypeRef::J(value_ty.clone()),
11648        is_param,
11649        range_start: 0,
11650        range_end: u16::MAX,
11651        synthetic_name: true,
11652    });
11653    while vt.by_slot.len() <= slot as usize {
11654        vt.by_slot.push(Vec::new());
11655    }
11656    vt.by_slot[slot as usize].push((0, u16::MAX, id));
11657    gen.insert(old, id);
11658    if let Expr::Local { var, ty, .. } = target {
11659        *var = id;
11660        *ty = TypeRef::J(value_ty.clone());
11661    }
11662}
11663
11664/// Undo a DIVERGED generation split: rewrite every occurrence of the
11665/// freshly-minted generation `from` back to the pre-if var `to` within
11666/// `s` (both read positions and assignment targets, plus LocalDef var
11667/// fields). At a control-flow join only splits BOTH branches produced
11668/// survive (the phi merges on the shared var); a gen one branch minted
11669/// alone does not. But `do_split` already rewrote that branch's write
11670/// target IN PLACE, so without this undo the branch stores into a
11671/// write-only orphan gen that no confluence reader ever reads — the
11672/// value is silently lost AND the orphan renders undeclared (kt5.b
11673/// Kotlin default-arg bridge: `v18_g1 = p6x` in then vs `v18 = false`
11674/// in else, `this(.., v18, ..)` reads only else's value).
11675/// Batched rename_gen_back: one LocalDef pass + one expression pass with
11676/// a var→(target, ty) table, instead of two full subtree walks per undone
11677/// candidate (475k widenings on weixin paid 15µs each in walks).
11678fn rename_gen_back_batch(s: &mut Stmt, pairs: &[(u32, u32)], vt: &VarTable) {
11679    use jdc_core::FxHashMap;
11680    if pairs.is_empty() {
11681        return;
11682    }
11683    let map: FxHashMap<u32, (u32, JavaType)> = pairs
11684        .iter()
11685        .map(|&(from, to)| (from, (to, vt.vars[to as usize].ty.erased())))
11686        .collect();
11687    walk_mut_deep(s, &mut |st| {
11688        if let Stmt::LocalDef { var, .. } = st {
11689            if let Some(&(to, _)) = map.get(var) {
11690                *var = to;
11691            }
11692        }
11693    });
11694    rewrite_exprs(s, &mut |e| {
11695        deep_rewrite(e, &mut |x| {
11696            if let Expr::Local { var, ty } = x {
11697                if let Some(&(to, ref to_ty)) = map.get(var) {
11698                    *var = to;
11699                    *ty = TypeRef::J(to_ty.clone());
11700                }
11701            }
11702        });
11703    });
11704}
11705
11706/// Remove an undone generation's `by_slot` segment entry so slot-based
11707/// resolution can never hand back the now-unreferenced gen id (its
11708/// VarInfo stays — ids index `vars` — but nothing may select it).
11709fn drop_gen_slot(vt: &mut VarTable, dead: u32) {
11710    if (dead as usize) >= vt.vars.len() {
11711        return;
11712    }
11713    let slot = vt.vars[dead as usize].slot as usize;
11714    if let Some(seg) = vt.by_slot.get_mut(slot) {
11715        seg.retain(|&(_, _, id)| id != dead);
11716    }
11717}
11718
11719/// Type gate for `rename_gen_back`: merging gen `v` back onto base `k`
11720/// is only safe when the base can carry the gen's value — identical
11721/// types, or BOTH primitives (the bool/int register-reuse family that
11722/// booleanize + the cast fixers reconcile; the Kotlin bridge case,
11723/// kt5.b `int v18` receiving `boolean p6x`). A reference-vs-primitive
11724/// or cross-class merge would forge `String k = <boolean>`-style
11725/// assignments that never compile (the lark/weibo/reqable regression
11726/// of the ungated first cut: +8.4k `不兼容的类型`).
11727/// Pool/framework-aware least-upper-bound of two reference classes
11728/// (internal names): the most specific common ancestor through supers and
11729/// interfaces. `None` when nothing more specific than Object is common —
11730/// widening to Object would break member accesses on the base's readers,
11731/// so that case stays split.
11732/// Object-class name of a TypeRef without the JavaType clone `erased()`
11733/// performs (Array chains deep-clone) — the split-join candidate filter
11734/// ran two clones per candidate per diverged If and the clones dominated
11735/// the LUB change's wall cost.
11736/// Marker for a ref-pair candidate whose LUB is resolved lazily (after
11737/// the write-only read-gate); the empty string never collides with a real
11738/// internal class name (they always contain at least one '/').
11739fn lub_pending() -> Option<String> {
11740    Some(String::new())
11741}
11742
11743/// Resolution of a split-undo candidate after the read-gate.
11744enum Undo {
11745    /// Rename the gen back onto the base unchanged.
11746    Direct,
11747    /// Broaden the base var's type to this LUB, then rename.
11748    Widen(String),
11749    /// No safe merge: leave the branch's gen as-is.
11750    Refuse,
11751}
11752
11753fn obj_name(t: &TypeRef) -> Option<&std::sync::Arc<str>> {
11754    match t {
11755        TypeRef::J(JavaType::Object(o)) => Some(o),
11756        _ => None,
11757    }
11758}
11759
11760fn pool_lub(a: &str, b: &str, pool: &DexPool) -> Option<String> {
11761    if a == b {
11762        return Some(a.to_string());
11763    }
11764    // The hierarchy data is immutable for the process lifetime; the two
11765    // full walks (ancestor set of `a` + BFS of `b`) with per-node String
11766    // allocations ran per diverged-If candidate and quadrupled wall time
11767    // on the big corpora (weixin 19s -> 75s) — a thread-local memo makes
11768    // the second call a hash hit.
11769    thread_local! {
11770        static MEMO: std::cell::RefCell<jdc_core::FxHashMap<(String, String), Option<String>>> =
11771            std::cell::RefCell::new(jdc_core::FxHashMap::default());
11772    }
11773    if let Some(hit) = MEMO.with(|m| m.borrow().get(&(a.to_string(), b.to_string())).cloned()) {
11774        return hit;
11775    }
11776    let r = pool_lub_uncached(a, b, pool);
11777    MEMO.with(|m| m.borrow_mut().insert((a.to_string(), b.to_string()), r.clone()));
11778    r
11779}
11780
11781fn pool_lub_uncached(a: &str, b: &str, pool: &DexPool) -> Option<String> {
11782    let mut anc: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
11783    let mut stack: Vec<String> = vec![a.to_string()];
11784    while let Some(c) = stack.pop() {
11785        if !anc.insert(c.clone()) {
11786            continue;
11787        }
11788        if anc.len() > 1024 {
11789            return None;
11790        }
11791        if let Some(cls) = pool.get(&c) {
11792            if let Some(s) = &cls.super_name {
11793                stack.push(s.clone());
11794            }
11795            for i in &cls.interfaces {
11796                stack.push(i.clone());
11797            }
11798        } else {
11799            if let Some(s) = crate::fwdb::super_of(&c) {
11800                stack.push(s.to_string());
11801            }
11802            crate::fwdb::for_each_interface(&c, |i| stack.push(i.to_string()));
11803        }
11804    }
11805    let mut seen: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
11806    let mut queue: std::collections::VecDeque<String> = std::collections::VecDeque::new();
11807    queue.push_back(b.to_string());
11808    while let Some(c) = queue.pop_front() {
11809        if !seen.insert(c.clone()) {
11810            continue;
11811        }
11812        if c != "java/lang/Object" && anc.contains(&c) {
11813            return Some(c);
11814        }
11815        if seen.len() > 1024 {
11816            return None;
11817        }
11818        if let Some(cls) = pool.get(&c) {
11819            for i in &cls.interfaces {
11820                queue.push_back(i.clone());
11821            }
11822            if let Some(s) = &cls.super_name {
11823                queue.push_back(s.clone());
11824            }
11825        } else {
11826            crate::fwdb::for_each_interface(&c, |i| queue.push_back(i.to_string()));
11827            if let Some(s) = crate::fwdb::super_of(&c) {
11828                queue.push_back(s.to_string());
11829            }
11830        }
11831    }
11832    None
11833}
11834
11835fn undo_type_safe(vt: &VarTable, k: u32, v: u32) -> bool {
11836    if (k as usize) >= vt.vars.len() || (v as usize) >= vt.vars.len() {
11837        return false;
11838    }
11839    let kt = vt.vars[k as usize].ty.erased();
11840    let vt_ty = vt.vars[v as usize].ty.erased();
11841    if kt == vt_ty {
11842        return true;
11843    }
11844    let is_prim = |t: &JavaType| {
11845        matches!(
11846            t,
11847            JavaType::Int
11848                | JavaType::Long
11849                | JavaType::Short
11850                | JavaType::Byte
11851                | JavaType::Char
11852                | JavaType::Float
11853                | JavaType::Double
11854                | JavaType::Boolean
11855        )
11856    };
11857    is_prim(&kt) && is_prim(&vt_ty)
11858}
11859
11860fn split_walk_stmt(
11861    st: &mut Stmt,
11862    vt: &mut VarTable,
11863    gen: &mut std::collections::HashMap<u32, u32>,
11864    counter: &mut u32,
11865    pool: &DexPool,
11866) {
11867    match st {
11868        Stmt::Block(v) => {
11869            for x in v.iter_mut() {
11870                split_walk_stmt(x, vt, gen, counter, pool);
11871            }
11872        }
11873        Stmt::ExprStmt(e) => {
11874            rewrite_gen_reads(e, gen, vt);
11875            if let Expr::Assign {
11876                target,
11877                value,
11878                op: AssignOp::Plain,
11879                ..
11880            } = e
11881            {
11882                if let Expr::Local { var, .. } = &**target {
11883                    let old = *var;
11884                    if split_needed(vt, old, value, pool) {
11885                        let value_ty = value.type_ref().erased();
11886                        do_split(vt, target, old, &value_ty, gen, counter);
11887                    }
11888                }
11889            }
11890        }
11891        Stmt::LocalDef { var, init, .. } => {
11892            if let Some(e) = init {
11893                rewrite_gen_reads(e, gen, vt);
11894                if split_needed(vt, *var, e, pool) {
11895                    // The DEF mints its own generation: retarget the def
11896                    // to a fresh var (the bare decl of the ORIGINAL stays
11897                    // for its earlier readers).
11898                    let value_ty = e.type_ref().erased();
11899                    let old = *var;
11900                    let mut fake = Expr::Local {
11901                        var: old,
11902                        ty: vt.vars[old as usize].ty.clone(),
11903                    };
11904                    do_split(vt, &mut fake, old, &value_ty, gen, counter);
11905                    if let Expr::Local { var: nv, .. } = fake {
11906                        *var = nv;
11907                    }
11908                }
11909            }
11910        }
11911        Stmt::If {
11912            cond,
11913            then_stmt,
11914            else_stmt,
11915            ..
11916        } => {
11917            rewrite_gen_reads(cond, gen, vt);
11918            let then_floor = vt.vars.len() as u32;
11919            let mut g_then = gen.clone();
11920            split_walk_stmt(then_stmt, vt, &mut g_then, counter, pool);
11921            let else_floor = vt.vars.len() as u32;
11922            let mut g_else = gen.clone();
11923            if let Some(e) = else_stmt {
11924                split_walk_stmt(e, vt, &mut g_else, counter, pool);
11925            }
11926            // Undo splits that DIVERGED at this join (see
11927            // rename_gen_back), with TWO gates learned the hard way
11928            // (first cut regressed lark +8.4k):
11929            //  1. the SIBLING KEPT THE BASE — `g_else[k]` equals the
11930            //     entry mapping, i.e. only this branch minted a fresh
11931            //     gen for k. When both branches split, each orphan is
11932            //     declared by ensure_declared at its own value type and
11933            //     compiles; renaming them back onto the base would
11934            //     force cross-kind assignments (`int k = <boolean>`).
11935            //  2. the fresh gen is WRITE-ONLY in the branch — a gen
11936            //     read inside its branch renders fine on its own.
11937            // The gated shape is the Kotlin default-arg bridge phi
11938            // loss: then `v_g = p6x` / else `v = false` / confluence
11939            // reads v — the then value was silently dropped AND v_g
11940            // rendered undeclared (kt5.b). Undo restores the clean
11941            // diamond so fold_default_arg_bridge can fire.
11942            // Candidates first (the read-free conditions), then ONE
11943            // early-exit probe for the small candidate set — the old
11944            // read_counts_all rebuilt a HashMap over the WHOLE branch
11945            // subtree per diverged If (quadratic in nesting depth;
11946            // together with the split walk it was ~1/3 of worker CPU on
11947            // the QQ sample profile). The map was only ever consulted
11948            // as `reads(v) == 0` for these candidates.
11949            // Candidates carry an optional base widening: a REF pair the
11950            // strict type gate refuses can still merge when the pool
11951            // knows a common supertype (LinkedList + d96.p0 join to
11952            // java.util.List). The base widens to that LUB first, so
11953            // both branch writes and the confluence reads compile on
11954            // ONE var — the alternative (leaving the branch's gen) is
11955            // an undeclared write-only orphan and the branch value is
11956            // silently lost (weixin tp2/u2: the null fallback
11957            // `p03x = d96.p0.d` never reached addAll).
11958            // Stage the undo: cheap classification first, the
11959            // write-only read-gate second, and the pool LUB walks LAST —
11960            // only for survivors. Computing the LUB inside the filter ran
11961            // hierarchy walks for candidates the read-gate then dropped
11962            // (+40% wall on weixin).
11963            let mut undo_then: Vec<(u32, u32, Option<String>)> = g_then
11964                .iter()
11965                .filter(|(&k, &v)| {
11966                    v >= then_floor
11967                        && v < else_floor
11968                        && g_else.get(&k).copied() == gen.get(&k).copied()
11969                })
11970                .filter_map(|(&k, &v)| {
11971                    if undo_type_safe(vt, k, v) {
11972                        Some((v, k, None))
11973                    } else if let (Some(_), Some(_)) =
11974                        (obj_name(&vt.vars[k as usize].ty), obj_name(&vt.vars[v as usize].ty))
11975                    {
11976                        Some((v, k, lub_pending()))
11977                    } else {
11978                        None
11979                    }
11980                })
11981                .collect();
11982            if else_floor > then_floor && !undo_then.is_empty() {
11983                let vars: Vec<u32> = undo_then.iter().map(|&(v, _, _)| v).collect();
11984                let mut hits = vec![false; vars.len()];
11985                mark_read_vars(std::slice::from_ref(&**then_stmt), &vars, &mut hits);
11986                let mut i = 0usize;
11987                undo_then.retain(|_| {
11988                    let keep = !hits[i];
11989                    i += 1;
11990                    keep
11991                });
11992            }
11993            undo_then.sort_unstable_by_key(|&(v, _, _)| std::cmp::Reverse(v));
11994            // Resolve the widenings (only for read-gate survivors), apply
11995            // them to the base types, then ONE batched subtree rewrite.
11996            let mut pairs: Vec<(u32, u32)> = Vec::with_capacity(undo_then.len());
11997            for (v, k, pending) in undo_then {
11998                // Three-way resolution: Direct (types match, the primitive
11999                // family, or the gen value is assignable to the base)
12000                // renames as-is; Widen (a pool LUB exists) broadens the
12001                // base type first; Refused (no common supertype) SKIPS —
12002                // merging that pair would forge a cross-class assignment.
12003                let kind = match pending {
12004                    None => Undo::Direct,
12005                    Some(_) => {
12006                        let (a, b) = (
12007                            obj_name(&vt.vars[k as usize].ty),
12008                            obj_name(&vt.vars[v as usize].ty),
12009                        );
12010                        match (a, b) {
12011                            (Some(a), Some(b)) if pool.is_subtype(b, a) => Undo::Direct,
12012                            (Some(a), Some(b)) => match pool_lub(a, b, pool) {
12013                                Some(lub) => Undo::Widen(lub),
12014                                None => Undo::Refuse,
12015                            },
12016                            _ => Undo::Refuse,
12017                        }
12018                    }
12019                };
12020                match kind {
12021                    Undo::Refuse => continue,
12022                    Undo::Direct => pairs.push((v, k)),
12023                    Undo::Widen(lub) => {
12024                        WIDEN_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
12025                        vt.vars[k as usize].ty = TypeRef::J(JavaType::Object(lub.as_str().into()));
12026                        pairs.push((v, k));
12027                    }
12028                }
12029            }
12030            rename_gen_back_batch(then_stmt, &pairs, vt);
12031            for &(v, _) in &pairs {
12032                drop_gen_slot(vt, v);
12033            }
12034            if let Some(e) = else_stmt.as_deref_mut() {
12035                let mut undo_else: Vec<(u32, u32, Option<String>)> = g_else
12036                    .iter()
12037                    .filter(|(&k, &v)| {
12038                        v >= else_floor && g_then.get(&k).copied() == gen.get(&k).copied()
12039                    })
12040                    .filter_map(|(&k, &v)| {
12041                        if undo_type_safe(vt, k, v) {
12042                            Some((v, k, None))
12043                        } else if let (Some(_), Some(_)) =
12044                            (obj_name(&vt.vars[k as usize].ty), obj_name(&vt.vars[v as usize].ty))
12045                        {
12046                            Some((v, k, lub_pending()))
12047                        } else {
12048                            None
12049                        }
12050                    })
12051                    .collect();
12052                if (vt.vars.len() as u32) > else_floor && !undo_else.is_empty() {
12053                    let vars: Vec<u32> = undo_else.iter().map(|&(v, _, _)| v).collect();
12054                    let mut hits = vec![false; vars.len()];
12055                    mark_read_vars(std::slice::from_ref(&*e), &vars, &mut hits);
12056                    let mut i = 0usize;
12057                    undo_else.retain(|_| {
12058                        let keep = !hits[i];
12059                        i += 1;
12060                        keep
12061                    });
12062                }
12063                undo_else.sort_unstable_by_key(|&(v, _, _)| std::cmp::Reverse(v));
12064                let mut pairs: Vec<(u32, u32)> = Vec::with_capacity(undo_else.len());
12065                for (v, k, pending) in undo_else {
12066                    let kind = match pending {
12067                        None => Undo::Direct,
12068                        Some(_) => {
12069                            let (a, b) = (
12070                                obj_name(&vt.vars[k as usize].ty),
12071                                obj_name(&vt.vars[v as usize].ty),
12072                            );
12073                            match (a, b) {
12074                                (Some(a), Some(b)) if pool.is_subtype(b, a) => Undo::Direct,
12075                                (Some(a), Some(b)) => match pool_lub(a, b, pool) {
12076                                    Some(lub) => Undo::Widen(lub),
12077                                    None => Undo::Refuse,
12078                                },
12079                                _ => Undo::Refuse,
12080                            }
12081                        }
12082                    };
12083                    match kind {
12084                        Undo::Refuse => continue,
12085                        Undo::Direct => pairs.push((v, k)),
12086                        Undo::Widen(lub) => {
12087                            WIDEN_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
12088                            vt.vars[k as usize].ty = TypeRef::J(JavaType::Object(lub.as_str().into()));
12089                            pairs.push((v, k));
12090                        }
12091                    }
12092                }
12093                rename_gen_back_batch(e, &pairs, vt);
12094                for &(v, _) in &pairs {
12095                    drop_gen_slot(vt, v);
12096                }
12097            }
12098            // Join: keep only splits BOTH paths produced (an absent else
12099            // is a fall-through path that keeps the pre-if generation).
12100            gen.retain(|k, v| g_then.get(k) == Some(v) && g_else.get(k) == Some(v));
12101        }
12102        Stmt::While { cond, body, .. } | Stmt::DoWhile { cond, body, .. } => {
12103            // 3-round fixpoint: round N walks the cond+body with the
12104            // accumulated gen — the back-edge carries the tail
12105            // generation into the head reads of the next round.
12106            let entry = gen.clone();
12107            for _ in 0..3 {
12108                rewrite_gen_reads(cond, gen, vt);
12109                split_walk_stmt(body, vt, gen, counter, pool);
12110            }
12111            // After the loop the pre-loop generation is what a
12112            // zero-iteration execution left — conservative.
12113            *gen = entry;
12114        }
12115        Stmt::For { init, body, .. } => {
12116            for x in init.iter_mut() {
12117                split_walk_stmt(x, vt, gen, counter, pool);
12118            }
12119            let entry = gen.clone();
12120            for _ in 0..3 {
12121                split_walk_stmt(body, vt, gen, counter, pool);
12122            }
12123            *gen = entry;
12124        }
12125        Stmt::ForEach { body, .. } => {
12126            let entry = gen.clone();
12127            for _ in 0..3 {
12128                split_walk_stmt(body, vt, gen, counter, pool);
12129            }
12130            *gen = entry;
12131        }
12132        Stmt::Switch { cases, default, .. } => {
12133            let entry = gen.clone();
12134            for c in cases.iter_mut() {
12135                let mut g_case = entry.clone();
12136                for x in c.body.iter_mut() {
12137                    split_walk_stmt(x, vt, &mut g_case, counter, pool);
12138                }
12139                gen.retain(|k, v| g_case.get(k) == Some(v));
12140            }
12141            if let Some(d) = default {
12142                let mut g_def = entry.clone();
12143                split_walk_stmt(d, vt, &mut g_def, counter, pool);
12144                gen.retain(|k, v| g_def.get(k) == Some(v));
12145            }
12146        }
12147        Stmt::Try {
12148            body,
12149            catches,
12150            finally,
12151        } => {
12152            let entry = gen.clone();
12153            let mut g_try = entry.clone();
12154            split_walk_stmt(body, vt, &mut g_try, counter, pool);
12155            let mut g_all = g_try.clone();
12156            for c in catches.iter_mut() {
12157                // The handler runs with the PRE-try state (the exception
12158                // may fire anywhere in the try).
12159                let mut g_c = entry.clone();
12160                split_walk_stmt(&mut c.body, vt, &mut g_c, counter, pool);
12161                g_all.retain(|k, v| g_c.get(k) == Some(v));
12162            }
12163            *gen = g_all;
12164            if let Some(f) = finally {
12165                split_walk_stmt(f, vt, gen, counter, pool);
12166            }
12167        }
12168        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => {
12169            split_walk_stmt(body, vt, gen, counter, pool);
12170        }
12171        Stmt::Return(Some(e)) | Stmt::Throw(e) => {
12172            rewrite_gen_reads(e, gen, vt);
12173        }
12174        _ => {}
12175    }
12176}
12177
12178/// Framework exception super edge — the embedded API database
12179/// (fwdb) replaced the hand-written ~110-entry JDK/Android exception
12180/// supers table: real extends edges for EVERY framework class, so
12181/// multi-catch dedupe now covers chains the table missed (and cannot
12182/// carry a wrong entry — the table's failure mode was dropping a live
12183/// alternative).
12184fn fw_super(c: &str) -> Option<&'static str> {
12185    crate::fwdb::super_of(c)
12186}
12187
12188/// `sub <: sup` for exception-class names, combining the pool
12189/// hierarchy (which also matches framework ancestor NAMES on the way
12190/// up, so pool subclasses of framework exceptions resolve) with the
12191/// tabled framework chains for pairs where both sides are outside the
12192/// dex.
12193fn exc_subtype(sub: &str, sup: &str, pool: &DexPool) -> bool {
12194    if sub == sup {
12195        return true;
12196    }
12197    if pool.is_subtype(sub, sup) {
12198        return true;
12199    }
12200    let mut cur = sub;
12201    for _ in 0..24 {
12202        let Some(next) = fw_super(cur) else { break };
12203        if next == sup {
12204            return true;
12205        }
12206        cur = next;
12207    }
12208    false
12209}
12210
12211/// Drop multi-catch alternatives shadowed by another alternative in
12212/// the same handler (JLS 14.20.2 forbids subtype-related
12213/// alternatives; the dex verifier does not). Exact duplicates collapse
12214/// to the first occurrence. The caught set is unchanged, so this is
12215/// semantics-preserving by construction.
12216pub fn dedupe_multicatch(s: &mut Stmt, pool: &DexPool) {
12217    dedupe_multicatch_walk(s, pool);
12218}
12219
12220fn dedupe_multicatch_walk(s: &mut Stmt, pool: &DexPool) {
12221    match s {
12222        Stmt::Try {
12223            body,
12224            catches,
12225            finally,
12226        }
12227        | Stmt::TryWithResources {
12228            body,
12229            catches,
12230            finally,
12231            ..
12232        } => {
12233            dedupe_multicatch_walk(body, pool);
12234            for c in catches.iter_mut() {
12235                if c.exc.len() > 1 {
12236                    let mut keep: Vec<std::sync::Arc<str>> = Vec::with_capacity(c.exc.len());
12237                    for e in c.exc.iter() {
12238                        let shadowed = c.exc.iter().any(|o| {
12239                            o.as_ref() != e.as_ref() && exc_subtype(e, o, pool)
12240                        });
12241                        if !shadowed && !keep.iter().any(|k| k.as_ref() == e.as_ref()) {
12242                            keep.push(e.clone());
12243                        }
12244                    }
12245                    c.exc = keep;
12246                }
12247                dedupe_multicatch_walk(&mut c.body, pool);
12248            }
12249            if let Some(f) = finally {
12250                dedupe_multicatch_walk(f, pool);
12251            }
12252        }
12253        Stmt::Block(v) => {
12254            for x in v.iter_mut() {
12255                dedupe_multicatch_walk(x, pool);
12256            }
12257        }
12258        Stmt::If {
12259            then_stmt,
12260            else_stmt,
12261            ..
12262        } => {
12263            dedupe_multicatch_walk(then_stmt, pool);
12264            if let Some(e) = else_stmt {
12265                dedupe_multicatch_walk(e, pool);
12266            }
12267        }
12268        Stmt::While { body, .. }
12269        | Stmt::DoWhile { body, .. }
12270        | Stmt::For { body, .. }
12271        | Stmt::ForEach { body, .. }
12272        | Stmt::Labeled { body, .. }
12273        | Stmt::Synchronized { body, .. } => dedupe_multicatch_walk(body, pool),
12274        Stmt::Switch { cases, default, .. } => {
12275            for c in cases.iter_mut() {
12276                for x in c.body.iter_mut() {
12277                    dedupe_multicatch_walk(x, pool);
12278                }
12279            }
12280            if let Some(d) = default {
12281                dedupe_multicatch_walk(d, pool);
12282            }
12283        }
12284        _ => {}
12285    }
12286}
12287
12288/// Dangling `break L<id>`: a Goto whose paired Label/Labeled-wrap was
12289/// lost to structure degradation prints `break L<id>;` against an
12290/// undeclared label ("未定义的标签", weibo 371/lark 85/weixin 535).
12291/// Inside a loop the goto-to-loop-exit shape degrades to a plain
12292/// `break` — compilable, and the dominant original semantic. Outside
12293/// any loop the jump is irreducible flow (weixin protobuf state
12294/// machines: `goto` into a lost loop middle / dispatcher block — the
12295/// target exists but not at a position Java can express); it renders
12296/// as an honest `$DDC:` marker comment instead of a guaranteed
12297/// undefined-label error, and the block's tail after it is dropped
12298/// (the original control flow never reached those statements).
12299pub fn resolve_dangling_gotos(s: &mut Stmt) {
12300    let mut labels: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
12301    // walk_all is read-only — the whole-body clone this used to make
12302    // (4.6k profile samples of Stmt::clone) bought nothing.
12303    walk_all(s, &mut |st| {
12304        if let Stmt::Label(id) = st {
12305            labels.insert(*id);
12306        }
12307    });
12308    resolve_gotos_walk(s, &labels, 0);
12309}
12310
12311fn resolve_gotos_walk(s: &mut Stmt, labels: &jdc_core::FxHashSet<u32>, loop_depth: u32) {
12312    let depth = match s {
12313        Stmt::While { .. } | Stmt::DoWhile { .. } | Stmt::For { .. } | Stmt::ForEach { .. } => {
12314            loop_depth + 1
12315        }
12316        _ => loop_depth,
12317    };
12318    match s {
12319        Stmt::Goto(id) => {
12320            if labels.contains(id) {
12321                return; // valid pair — keep
12322            }
12323            if depth > 0 {
12324                *s = Stmt::Break(None);
12325            } else {
12326                *s = Stmt::Comment(format!(
12327                    "$DDC: unresolved jump to block {} (irreducible control flow)",
12328                    id
12329                ));
12330            }
12331        }
12332        Stmt::Block(v) => {
12333            let mut i = 0usize;
12334            while i < v.len() {
12335                // A depth-0 dangling Goto becomes a marker comment and
12336                // the original flow never reached the siblings after
12337                // it — dropping them keeps the rendered reachability
12338                // faithful (they were dead in the dex too).
12339                let dangling_here =
12340                    depth == 0 && matches!(&v[i], Stmt::Goto(id) if !labels.contains(id));
12341                resolve_gotos_walk(&mut v[i], labels, depth);
12342                if dangling_here {
12343                    v.truncate(i + 1);
12344                    break;
12345                }
12346                i += 1;
12347            }
12348        }
12349        Stmt::If { then_stmt, else_stmt, .. } => {
12350            resolve_gotos_walk(then_stmt, labels, depth);
12351            if let Some(e) = else_stmt {
12352                resolve_gotos_walk(e, labels, depth);
12353            }
12354        }
12355        Stmt::While { body, .. }
12356        | Stmt::DoWhile { body, .. }
12357        | Stmt::For { body, .. }
12358        | Stmt::ForEach { body, .. }
12359        | Stmt::Labeled { body, .. }
12360        | Stmt::Synchronized { body, .. } => resolve_gotos_walk(body, labels, depth),
12361        Stmt::Try {
12362            body,
12363            catches,
12364            finally,
12365        } => {
12366            resolve_gotos_walk(body, labels, depth);
12367            for c in catches.iter_mut() {
12368                resolve_gotos_walk(&mut c.body, labels, depth);
12369            }
12370            if let Some(f) = finally {
12371                resolve_gotos_walk(f, labels, depth);
12372            }
12373        }
12374        Stmt::Switch { cases, default, .. } => {
12375            for c in cases.iter_mut() {
12376                for x in c.body.iter_mut() {
12377                    resolve_gotos_walk(x, labels, depth);
12378                }
12379            }
12380            if let Some(d) = default {
12381                resolve_gotos_walk(d, labels, depth);
12382            }
12383        }
12384        _ => {}
12385    }
12386}
12387
12388/// READS of `v` across the statements (assignment targets excluded).
12389/// All-var read counts in ONE pass (reads semantics of stmts_read_var)
12390/// — for gates that test many candidate vars against the same subtree
12391/// (split_walk's undo filters ran a full arm walk PER candidate).
12392/// Which of `vars` appear in a READ position inside `stmts`? One walk,
12393/// a small linear membership check per read, short-circuiting the check
12394/// once every candidate is found — no HashMap, no per-node hashing.
12395/// Replaces the full-subtree `read_counts_all` maps the split undo-gates
12396/// used to build per diverged If (their only query was `reads(v) == 0`
12397/// for a handful of candidate gens; the rebuild was quadratic in If
12398/// nesting depth and dominated the split pass on deep methods).
12399fn mark_read_vars(stmts: &[Stmt], vars: &[u32], hits: &mut [bool]) {
12400    let mut remaining = vars.len();
12401    for s in stmts {
12402        if remaining == 0 {
12403            break;
12404        }
12405        visit_stmt_exprs_ro(s, &mut |e| {
12406            if remaining == 0 {
12407                return;
12408            }
12409            visit_exprs_reads(e, &mut |x| {
12410                if remaining == 0 {
12411                    return;
12412                }
12413                if let Expr::Local { var, .. } = x {
12414                    for (i, v) in vars.iter().enumerate() {
12415                        if *v == *var && !hits[i] {
12416                            hits[i] = true;
12417                            remaining -= 1;
12418                        }
12419                    }
12420                }
12421            });
12422        });
12423    }
12424}
12425
12426fn stmts_read_var(stmts: &[Stmt], v: u32) -> usize {
12427    let mut n = 0usize;
12428    for s in stmts {
12429        visit_stmt_exprs_ro(s, &mut |e| {
12430            visit_exprs_reads(e, &mut |x| {
12431                if let Expr::Local { var: vv, .. } = x {
12432                    if *vv == v {
12433                        n += 1;
12434                    }
12435                }
12436            });
12437        });
12438    }
12439    n
12440}
12441
12442pub fn fix_ctor_conditional_super(body: &mut Stmt) {
12443    // A single-statement body can arrive UNWRAPPED (bare `If` — the
12444    // gb6/e throw-guard family); the shape scans need a statement list.
12445    if !matches!(body, Stmt::Block(_)) {
12446        let inner = std::mem::replace(body, Stmt::Block(Vec::new()));
12447        *body = Stmt::Block(vec![inner]);
12448    }
12449    let Stmt::Block(stmts) = body else { return };
12450    // Unwrap single-statement Block wrappers: the structurer can nest
12451    // the whole body one level deeper, hiding the top-level If from the
12452    // shape scans (cleanup would flatten it — but runs AFTER this pass).
12453    while stmts.len() == 1 {
12454        let Stmt::Block(inner) = &stmts[0] else { break };
12455        *stmts = inner.clone();
12456    }
12457    if stmts.is_empty() {
12458        return;
12459    }
12460    if is_bare_ctor_call(stmts.first().unwrap()) {
12461        dedupe_ctor_delegations(body);
12462        return;
12463    }
12464    if try_linear_super_inline(stmts) {
12465        return;
12466    }
12467    try_merge_branched_super(stmts);
12468}
12469
12470/// Shape C: the delegation is already first — strip duplicate copies
12471/// left by R8 path-duplication (Kotlin default-arg bridge ctors: ft5/j
12472/// had `super()` leading PLUS another inside a branch — "对super的调用
12473/// 必须是构造器中的第一个语句" at the copy). Every path delegates
12474/// through the leading call, so structurally-identical (or empty-args,
12475/// same-kind) copies nested in blocks/ifs are removed, never moved.
12476/// Runs both ahead of the shape-A/B attempt and AFTER the plain hoist
12477/// (fix_ctor_super_first) — the hoist is what puts the leading
12478/// delegation in place for path-duplicated ctors.
12479/// A ctor that ALREADY delegates in its first statement can hold no
12480/// other `this(..)`/`super(..)` — Baidu Titan hotpatch instrumentation
12481/// injects a conditional re-delegation inside the `if ($ic != null)`
12482/// guard of every ctor (baidusearch ctor-not-first ×61,922 across
12483/// 21,813 files: `super(context); if ($ic != null) { ..; if
12484/// ((flag&1)!=0) { super((Context) callArgs[0]); ..; return; } }`).
12485/// The guard is dead code on the unpatched runtime path (and the
12486/// decompiled output has no patch runtime at all); dropping the
12487/// secondary delegation keeps the real semantics and restores the
12488/// Java first-statement rule. Runs after dedupe_ctor_delegations
12489/// (identical copies) and before extract_branched_delegation_helper
12490/// — gated on a LEADING delegation, so branched-delegation ctors
12491/// (no leading one) still get the helper treatment. Nested class
12492/// declarations are separate ctor scopes and are not entered.
12493pub fn strip_secondary_delegations(body: &mut Stmt) {
12494    let Stmt::Block(stmts) = body else { return };
12495    if !stmts.first().is_some_and(is_bare_ctor_call) {
12496        return;
12497    }
12498    fn strip_in(s: &mut Stmt) {
12499        if is_bare_ctor_call(s) {
12500            *s = Stmt::Block(Vec::new());
12501            return;
12502        }
12503        match s {
12504            Stmt::Block(v) => v.iter_mut().for_each(strip_in),
12505            Stmt::If {
12506                then_stmt,
12507                else_stmt,
12508                ..
12509            } => {
12510                strip_in(then_stmt);
12511                if let Some(e) = else_stmt {
12512                    strip_in(e);
12513                }
12514            }
12515            Stmt::While { body, .. }
12516            | Stmt::DoWhile { body, .. }
12517            | Stmt::ForEach { body, .. }
12518            | Stmt::Synchronized { body, .. }
12519            | Stmt::Labeled { body, .. } => strip_in(body),
12520            Stmt::For { init, body, .. } => {
12521                init.iter_mut().for_each(strip_in);
12522                strip_in(body);
12523            }
12524            Stmt::Switch { cases, default, .. } => {
12525                for c in cases.iter_mut() {
12526                    c.body.iter_mut().for_each(strip_in);
12527                }
12528                if let Some(d) = default {
12529                    strip_in(d);
12530                }
12531            }
12532            Stmt::Try {
12533                body,
12534                catches,
12535                finally,
12536            }
12537            | Stmt::TryWithResources {
12538                body,
12539                catches,
12540                finally,
12541                ..
12542            } => {
12543                strip_in(body);
12544                for c in catches.iter_mut() {
12545                    strip_in(&mut c.body);
12546                }
12547                if let Some(f) = finally {
12548                    strip_in(f);
12549                }
12550            }
12551            _ => {}
12552        }
12553    }
12554    for st in stmts.iter_mut().skip(1) {
12555        strip_in(st);
12556    }
12557}
12558
12559pub fn dedupe_ctor_delegations(body: &mut Stmt) {
12560    let Stmt::Block(stmts) = body else { return };
12561    let Some(d0) = stmts.first().and_then(|s| match s {
12562        Stmt::ExprStmt(e) if is_delegation_expr(e) => Some(e.clone()),
12563        _ => None,
12564    }) else {
12565        return;
12566    };
12567    fn rec(s: &mut Stmt, d0: &Expr) {
12568        match s {
12569            Stmt::Block(v) => {
12570                v.retain(|x| {
12571                    !matches!(x, Stmt::ExprStmt(e) if is_delegation_expr(e) && same_delegation(e, d0))
12572                });
12573                for x in v.iter_mut() {
12574                    rec(x, d0);
12575                }
12576            }
12577            Stmt::If { then_stmt, else_stmt, .. } => {
12578                rec(then_stmt, d0);
12579                if let Some(e) = else_stmt {
12580                    rec(e, d0);
12581                }
12582            }
12583            _ => {}
12584        }
12585    }
12586    // Top-level copies first (rec only strips copies NESTED in
12587    // blocks/ifs — a duplicate `super()` sitting directly in the body
12588    // list fell through its `_` arm: weixin ft5/j's second super()).
12589    {
12590        let d0r = &d0;
12591        let mut idx = 0usize;
12592        stmts.retain(|s| {
12593            idx += 1;
12594            idx == 1
12595                || !(matches!(s, Stmt::ExprStmt(e)
12596                    if is_delegation_expr(e) && same_delegation(e, d0r)))
12597        });
12598    }
12599    for s in stmts.iter_mut().skip(1) {
12600        rec(s, &d0);
12601    }
12602}
12603
12604/// Structural delegation identity for dedup: same kind/target and equal
12605/// args — or both arg-less (`super()` copies across duplicated paths
12606/// render with locally-renamed args yet delegate identically).
12607fn same_delegation(a: &Expr, b: &Expr) -> bool {
12608    match (a, b) {
12609        (
12610            Expr::Method { name: n1, cls: c1, is_super: s1, args: a1, .. },
12611            Expr::Method { name: n2, cls: c2, is_super: s2, args: a2, .. },
12612        ) => {
12613            &**n1 == "<init>"
12614                && &**n2 == "<init>"
12615                && c1 == c2
12616                && s1 == s2
12617                && (a1 == a2 || (a1.is_empty() && a2.is_empty()))
12618        }
12619        _ => false,
12620    }
12621}
12622
12623/// The `Method` expr of a bare ctor-delegation statement.
12624fn ctor_call_expr(s: &Stmt) -> Option<&Expr> {
12625    match s {
12626        Stmt::ExprStmt(e) if is_delegation_expr(e) => Some(e),
12627        _ => None,
12628    }
12629}
12630
12631/// Same delegation kind ignoring args (super vs this, same target).
12632fn same_ctor_kind(a: &Expr, b: &Expr) -> bool {
12633    match (a, b) {
12634        (
12635            Expr::Method { cls: c1, is_super: s1, owner: o1, .. },
12636            Expr::Method { cls: c2, is_super: s2, owner: o2, .. },
12637        ) => c1 == c2 && s1 == s2 && o1 == o2,
12638        _ => false,
12639    }
12640}
12641
12642/// Statements of a branch body, recursively flattening pure Blocks —
12643/// the fix runs BEFORE cleanup, so structurer-emitted nesting is still
12644/// in place. Any non-Block statement (If/Try/...) stays an item and
12645/// fails the def/call classification, which is the intended abort.
12646fn flat_list(s: &Stmt) -> Vec<&Stmt> {
12647    fn rec<'a>(s: &'a Stmt, out: &mut Vec<&'a Stmt>) {
12648        match s {
12649            Stmt::Block(v) => {
12650                for x in v {
12651                    rec(x, out);
12652                }
12653            }
12654            other => out.push(other),
12655        }
12656    }
12657    let mut out = Vec::new();
12658    rec(s, &mut out);
12659    out
12660}
12661
12662/// Blank the OWNER of un-folded `<init>` calls (lost allocation): the
12663/// printer renders them as `new X(..)` and never reads the owner, so its
12664/// register reference is not a real use (v7.a$a2: the dead
12665/// `new f(0,..)`'s owner was v1's register, inflating v1's use count
12666/// and blocking the inline-hoist).
12667fn strip_lost_alloc_owners(e: &mut Expr) {
12668    deep_rewrite(e, &mut |x| {
12669        if let Expr::Method { name, is_special: true, owner: Some(o), .. } = x {
12670            if &**name == "<init>" && !matches!(**o, Expr::This) {
12671                **o = Expr::Const(jdc_core::ir::expr::ConstVal::Int(0));
12672            }
12673        }
12674    });
12675}
12676
12677/// Per-var `Local` occurrence counts over an expression (single pass;
12678/// lost-alloc `<init>` owners excluded — see strip_lost_alloc_owners).
12679fn count_locals_expr(e: &Expr) -> jdc_core::FxHashMap<u32, usize> {
12680    let mut m = jdc_core::FxHashMap::default();
12681    count_expr_ro(e, &mut m);
12682    m
12683}
12684
12685pub(crate) fn count_locals_stmts(ss: &[Stmt]) -> jdc_core::FxHashMap<u32, usize> {
12686    let mut m = jdc_core::FxHashMap::default();
12687    for s in ss {
12688        visit_stmt_exprs_ro(s, &mut |e| count_expr_ro(e, &mut m));
12689    }
12690    m
12691}
12692
12693fn cnt(m: &jdc_core::FxHashMap<u32, usize>, v: u32) -> usize {
12694    m.get(&v).copied().unwrap_or(0)
12695}
12696
12697/// Substitute map-referenced locals, recursively expanding def chains
12698/// (depth-capped). Returns None when a referenced var has no def or the
12699/// chain is too deep.
12700fn inline_locals(
12701    e: &Expr,
12702    map: &std::collections::HashMap<u32, Expr>,
12703    depth: u32,
12704) -> Option<Expr> {
12705    if depth > 4 {
12706        return None;
12707    }
12708    let mut out = e.clone();
12709    let mut fail = false;
12710    deep_rewrite(&mut out, &mut |x| {
12711        if let Expr::Local { var, .. } = x {
12712            if let Some(def) = map.get(var) {
12713                match inline_locals(def, map, depth + 1) {
12714                    Some(r) => *x = r,
12715                    None => fail = true,
12716                }
12717            }
12718        }
12719    });
12720    if fail {
12721        None
12722    } else {
12723        Some(out)
12724    }
12725}
12726
12727/// A leading statement that defines a local: (var, init expr).
12728fn def_of(s: &Stmt) -> Option<(u32, &Expr)> {
12729    match s {
12730        Stmt::LocalDef { var, init: Some(e), .. } => Some((*var, e)),
12731        Stmt::ExprStmt(Expr::Assign {
12732            target,
12733            value,
12734            op: jdc_core::ir::expr::AssignOp::Plain,
12735            ..
12736        }) => match &**target {
12737            Expr::Local { var, .. } => Some((*var, value)),
12738            _ => None,
12739        },
12740        _ => None,
12741    }
12742}
12743
12744/// Shape A: straight-line prefix, delegation references prefix locals.
12745fn try_linear_super_inline(stmts: &mut Vec<Stmt>) -> bool {
12746    let Some(pos) = stmts.iter().position(is_bare_ctor_call) else {
12747        return false;
12748    };
12749    if pos == 0 {
12750        return false; // already first — nothing to do
12751    }
12752    // The prefix must be straight-line.
12753    if !stmts[..pos]
12754        .iter()
12755        .all(|s| matches!(s, Stmt::LocalDef { .. } | Stmt::ExprStmt(_)))
12756    {
12757        return false;
12758    }
12759    let Some(call) = ctor_call_expr(&stmts[pos]) else {
12760        return false;
12761    };
12762    let Expr::Method { args, .. } = call else {
12763        return false;
12764    };
12765    // Defs reachable from the args (last definition wins, bytecode order).
12766    let mut map: std::collections::HashMap<u32, Expr> =
12767        std::collections::HashMap::new();
12768    let mut def_idx: jdc_core::FxHashMap<u32, usize> =
12769        jdc_core::FxHashMap::default();
12770    for (i, s) in stmts[..pos].iter().enumerate() {
12771        if let Some((v, e)) = def_of(s) {
12772            map.insert(v, e.clone());
12773            def_idx.insert(v, i);
12774        }
12775    }
12776    // Which arg locals need inlining?
12777    let mut needed: Vec<u32> = Vec::new();
12778    for a in args {
12779        let mut probe = a.clone();
12780        strip_lost_alloc_owners(&mut probe);
12781        deep_rewrite(&mut probe, &mut |x| {
12782            if let Expr::Local { var, .. } = x {
12783                if map.contains_key(var) && !needed.contains(var) {
12784                    needed.push(*var);
12785                }
12786            }
12787        });
12788    }
12789    if needed.is_empty() {
12790        return false; // plain hoist handles it
12791    }
12792    // Guard: side-effecting defs inline only at a provably preserved
12793    // call count (exactly one use, the arg site).
12794    let mut consumed: Vec<usize> = Vec::new();
12795    let use_counts = count_locals_stmts(stmts);
12796    for &v in &needed {
12797        let def = &map[&v];
12798        let uses = cnt(&use_counts, v);
12799        if jdc_core::ir::build::has_side_effects(def) && uses != 1 {
12800            return false;
12801        }
12802        if uses == 1 {
12803            consumed.push(def_idx[&v]);
12804        }
12805    }
12806    // Build the inlined args.
12807    let mut new_args: Vec<Expr> = Vec::with_capacity(args.len());
12808    for a in args {
12809        match inline_locals(a, &map, 0) {
12810            Some(r) => new_args.push(r),
12811            None => return false,
12812        }
12813    }
12814    // Rebuild: super first (with inlined args), the unconsumed prefix
12815    // statements keep their order after it, then the original rest.
12816    let mut call_stmt = stmts.remove(pos);
12817    if let Stmt::ExprStmt(Expr::Method { args: slot, .. }) = &mut call_stmt {
12818        *slot = new_args;
12819    }
12820    let mut rest: Vec<Stmt> = Vec::with_capacity(stmts.len() + 1);
12821    rest.push(call_stmt);
12822    for (i, s) in stmts.drain(..).enumerate() {
12823        // Indices shift after `remove(pos)`: consumed holds pre-removal
12824        // indices; pos itself is already gone from `stmts`.
12825        let orig = if i >= pos { i + 1 } else { i };
12826        if !consumed.contains(&orig) {
12827            rest.push(s);
12828        }
12829    }
12830    *stmts = rest;
12831    true
12832}
12833
12834/// Shape B: if/else branches each ending in the same-kind delegation.
12835fn try_merge_branched_super(stmts: &mut Vec<Stmt>) -> bool {
12836    // Candidate If positions — the prelude before the chosen If must be
12837    // straight-line; try each If until one merges (nested/multi-if
12838    // ctors exist but the first matching one is the delegation site).
12839    let if_positions: Vec<usize> = stmts
12840        .iter()
12841        .enumerate()
12842        .filter(|(_, s)| matches!(s, Stmt::If { .. }))
12843        .map(|(i, _)| i)
12844        .collect();
12845    for if_pos in if_positions {
12846        // Cheap allocation-free gate: at least one branch must hold a
12847        // delegation (most ctor ifs are field null-checks and must not
12848        // pay the merge machinery). The OTHER side may be delegation-free
12849        // (throw-guard shape D) or absent (then-only shape E — the
12850        // rendered if/else form often only exists after later passes).
12851        let gate = match &stmts[if_pos] {
12852            Stmt::If { then_stmt, else_stmt, .. } => {
12853                contains_delegation(then_stmt)
12854                    || else_stmt.as_ref().is_some_and(|e| contains_delegation(e))
12855            }
12856            _ => false,
12857        };
12858        if gate && merge_at(stmts, if_pos) {
12859            return true;
12860        }
12861    }
12862    false
12863}
12864
12865/// Does this subtree contain a ctor delegation? (module-level: shared by
12866/// the merge gate and split_branch)
12867fn contains_delegation(s: &Stmt) -> bool {
12868    match s {
12869        Stmt::ExprStmt(e) => is_delegation_expr(e),
12870        Stmt::Block(v) => v.iter().any(contains_delegation),
12871        Stmt::If { then_stmt, else_stmt, .. } => {
12872            contains_delegation(then_stmt)
12873                || else_stmt.as_ref().is_some_and(|e| contains_delegation(e))
12874        }
12875        _ => false,
12876    }
12877}
12878
12879
12880
12881/// One merge attempt at `stmts[if_pos]`. See fix_ctor_conditional_super
12882/// for the doctrine. Extensions over the pure two-call merge:
12883/// - branch pre-call SIDE-EFFECT statements (Kotlin `o.h(param,..)`
12884///   null-checks — the zz5/j0 family) are allowed; they move into the
12885///   branch remainder (after super — the same reordering doctrine the
12886///   plain hoist already applies; Java has no pre-super statement form).
12887/// - a branch WITHOUT a delegation (throw-guard: `if (x==null) throw
12888///   .. else { super(..); .. }` — the gb6/e family) merges as pure
12889///   remainder; exactly one branch carries the call then, and no
12890///   ternary args arise (the guard throws before any path needs them).
12891/// - prelude single-assignment if/else (`if (c) {v=e1} else {v=e2}`)
12892///   folds into a Cond def for inlining (the zz5/j0 v15/v16 preludes).
12893fn merge_at(stmts: &mut Vec<Stmt>, if_pos: usize) -> bool {
12894    /// Branch classification: Call(at, defs, pre-call extra idxs) when a
12895    /// delegation sits behind only defs/side-effect statements; Clean
12896    /// when the branch has no delegation at all (pure remainder); Dirty
12897    /// otherwise (nested/conditional delegation — out of scope).
12898    enum Br {
12899        /// (call index, pre-call defs, def statement indices)
12900        Call(usize, Vec<(u32, Expr)>, Vec<usize>),
12901        Clean,
12902        Dirty,
12903    }
12904    fn split_branch(list: &[&Stmt]) -> Br {
12905        let call_pos = list.iter().position(|s| ctor_call_expr(s).is_some());
12906        let Some(c) = call_pos else {
12907            // No delegation at this level: a pure remainder branch
12908            // (throw-guard) unless one hides in nested control flow.
12909            return if list.iter().any(|s| contains_delegation(s)) {
12910                Br::Dirty
12911            } else {
12912                Br::Clean
12913            };
12914        };
12915        if list.iter().skip(c + 1).any(|s| ctor_call_expr(s).is_some()) {
12916            return Br::Dirty; // two delegations in one branch
12917        }
12918        let mut defs: Vec<(u32, Expr)> = Vec::new();
12919        let mut def_idxs: Vec<usize> = Vec::new();
12920        for (i, s) in list.iter().enumerate().take(c) {
12921            match def_of(s) {
12922                Some((v, e)) => {
12923                    defs.push((v, e.clone()));
12924                    def_idxs.push(i);
12925                }
12926                None => match s {
12927                    // Side-effect statements and bare decls ahead of the
12928                    // delegation ride along into the remainder.
12929                    Stmt::ExprStmt(_) | Stmt::LocalDef { .. } => {}
12930                    _ => return Br::Dirty, // control flow pre-call
12931                },
12932            }
12933        }
12934        Br::Call(c, defs, def_idxs)
12935    }
12936    /// Prelude single-assignment if/else → `v = c ? e1 : e2` def.
12937    fn cond_def_of(s: &Stmt, scope: &[Stmt]) -> Option<(u32, Expr)> {
12938        let Stmt::If { cond, then_stmt, else_stmt: Some(e), .. } = s else {
12939            return None;
12940        };
12941        fn one<'x>(b: &'x Stmt, scope: &[Stmt]) -> Option<(u32, &'x Expr)> {
12942            let l = flat_list(b);
12943            if l.len() == 1 {
12944                return def_of(l[0]);
12945            }
12946            // A [w = E; v = w] forwarding PAIR — the shape the
12947            // structurer leaves when E is impure (`c v5 = new c(..);
12948            // v6 = v5`) and forward_single_use's adjacency pass missed
12949            // it (a duplicated read elsewhere in its raw body broke the
12950            // single-read gate). Collapse to `v = E`, gated on w being
12951            // method-unique: the merge DROPS this branch, so any other
12952            // read/write of w would dangle (lark gp2/d Kotlin default-
12953            // arg bridge, this-not-first family).
12954            if l.len() == 2 {
12955                let (w, ex) = def_of(l[0])?;
12956                let Stmt::ExprStmt(Expr::Assign {
12957                    target,
12958                    op: AssignOp::Plain,
12959                    value,
12960                }) = l[1]
12961                else {
12962                    return None;
12963                };
12964                let Expr::Local { var: tv, .. } = &**target else {
12965                    return None;
12966                };
12967                let Expr::Local { var: rv, .. } = &**value else {
12968                    return None;
12969                };
12970                if *rv != w || *tv == w {
12971                    return None;
12972                }
12973                // stmts_count_writes only sees Expr-level assigns; the
12974                // pair's own def is often an init-bearing LocalDef, so
12975                // count those separately for the uniqueness gate.
12976                let mut ldef_writes = 0usize;
12977                for st in scope {
12978                    walk_all(st, &mut |x| {
12979                        if let Stmt::LocalDef {
12980                            var,
12981                            init: Some(_),
12982                            ..
12983                        } = x
12984                        {
12985                            if *var == w {
12986                                ldef_writes += 1;
12987                            }
12988                        }
12989                    });
12990                }
12991                if stmts_read_var(scope, w) != 1
12992                    || stmts_count_writes(scope, w) + ldef_writes != 1
12993                {
12994                    return None;
12995                }
12996                return Some((*tv, ex));
12997            }
12998            None
12999        }
13000        let (v1, e1) = one(then_stmt, scope)?;
13001        let (v2, e2) = one(e, scope)?;
13002        if v1 != v2 {
13003            return None;
13004        }
13005        Some((
13006            v1,
13007            Expr::Cond {
13008                c: Box::new(cond.clone()),
13009                t: Box::new(e1.clone()),
13010                f: Box::new(e2.clone()),
13011            },
13012        ))
13013    }
13014
13015    // Prelude: defs (flat or folded cond), bare decls, side-effect stmts.
13016    let mut pmap: std::collections::HashMap<u32, Expr> =
13017        std::collections::HashMap::new();
13018    let mut pidx: jdc_core::FxHashMap<u32, usize> =
13019        jdc_core::FxHashMap::default();
13020    for (i, s) in stmts[..if_pos].iter().enumerate() {
13021        match s {
13022            Stmt::LocalDef { .. } | Stmt::ExprStmt(_) => {
13023                if let Some((v, e)) = def_of(s) {
13024                    pmap.insert(v, e.clone());
13025                    pidx.insert(v, i);
13026                }
13027            }
13028            Stmt::If { .. } => match cond_def_of(s, stmts) {
13029                Some((v, e)) => {
13030                    pmap.insert(v, e);
13031                    pidx.insert(v, i);
13032                }
13033                None => return false,
13034            },
13035            _ => return false,
13036        }
13037    }
13038    let (cond, then_v, else_v, had_else) = match &stmts[if_pos] {
13039        Stmt::If { cond, then_stmt, else_stmt, .. } => (
13040            cond.clone(),
13041            then_stmt.as_ref().clone(),
13042            else_stmt.as_ref().map_or_else(|| Stmt::Block(Vec::new()), |e| e.as_ref().clone()),
13043            else_stmt.is_some(),
13044        ),
13045        _ => return false,
13046    };
13047    // Chained guards (Kotlin multi-param null-checks):
13048    // `if(a){throw..} else { if(b){throw..} else { super(..); .. } }` —
13049    // a branch that is a lone delegation-bearing If gets merged
13050    // recursively first (its delegation hoisted to the branch head), so
13051    // the outer split then sees [call, nested-guard-remainder].
13052    fn normalize_branch(b: &mut Stmt) {
13053        // MOVE-based (no deep clone): chained weixin Parcel ctors carry
13054        // huge tails; cloning per recursion level cost measurable wall
13055        // time. merge_at leaves stmts untouched on failure, so the taken
13056        // node can always go back.
13057        let slot: &mut Stmt = match b {
13058            Stmt::Block(v) if v.len() == 1 => &mut v[0],
13059            other => other,
13060        };
13061        if !matches!(slot, Stmt::If { .. }) || !contains_delegation(slot) {
13062            return;
13063        }
13064        let taken = std::mem::replace(slot, Stmt::Block(Vec::new()));
13065        let mut tmp = vec![taken];
13066        if merge_at(&mut tmp, 0) {
13067            *slot = Stmt::Block(tmp);
13068        } else {
13069            *slot = tmp.pop().unwrap();
13070        }
13071    }
13072    let mut then_v = then_v;
13073    let mut else_v = else_v;
13074    normalize_branch(&mut then_v);
13075    normalize_branch(&mut else_v);
13076    let then_list = flat_list(&then_v);
13077    let else_list = flat_list(&else_v);
13078
13079    // Exactly: two Call branches (merge with ternaries) or one Call +
13080    // one Clean (single-delegation throw-guard). Owned branch payloads.
13081    enum Side {
13082        Call {
13083            at: usize,
13084            defs: Vec<(u32, Expr)>,
13085            def_idxs: Vec<usize>,
13086        },
13087        Clean,
13088    }
13089    let (t_side, e_side, two_sided) = match (split_branch(&then_list), split_branch(&else_list)) {
13090        (Br::Call(tc, td, ti), Br::Call(ec, ed, ei)) => (
13091            Side::Call { at: tc, defs: td, def_idxs: ti },
13092            Side::Call { at: ec, defs: ed, def_idxs: ei },
13093            true,
13094        ),
13095        (Br::Call(tc, td, ti), Br::Clean) => (
13096            Side::Call { at: tc, defs: td, def_idxs: ti },
13097            Side::Clean,
13098            false,
13099        ),
13100        (Br::Clean, Br::Call(ec, ed, ei)) => (
13101            Side::Clean,
13102            Side::Call { at: ec, defs: ed, def_idxs: ei },
13103            false,
13104        ),
13105        _ => return false,
13106    };
13107    // The delegation nodes (two-sided: both; single: one).
13108    let (t_call, e_call) = match (&t_side, &e_side) {
13109        (Side::Call { at: ta, .. }, Side::Call { at: ea, .. }) => (
13110            ctor_call_expr(then_list[*ta]),
13111            ctor_call_expr(else_list[*ea]),
13112        ),
13113        (Side::Call { at: ta, .. }, Side::Clean) => {
13114            (ctor_call_expr(then_list[*ta]), None)
13115        }
13116        (Side::Clean, Side::Call { at: ea, .. }) => {
13117            (None, ctor_call_expr(else_list[*ea]))
13118        }
13119        _ => return false,
13120    };
13121    // The node that becomes the merged call: then side when present.
13122    let (call_list, call_at) = match &t_side {
13123        Side::Call { at, .. } => (&then_list, *at),
13124        Side::Clean => match &e_side {
13125            Side::Call { at, .. } => (&else_list, *at),
13126            Side::Clean => return false,
13127        },
13128    };
13129    let (base_args, other_args) = if two_sided {
13130        let (Some(tc), Some(ec)) = (t_call, e_call) else {
13131            return false;
13132        };
13133        if !same_ctor_kind(tc, ec) {
13134            return false;
13135        }
13136        let (Expr::Method { args: a1, .. }, Expr::Method { args: a2, .. }) = (tc, ec) else {
13137            return false;
13138        };
13139        if a1.len() != a2.len() {
13140            return false;
13141        }
13142        (a1, Some(a2))
13143    } else {
13144        let c = t_call.or(e_call);
13145        let Some(c) = c else { return false };
13146        let Expr::Method { args, .. } = c else {
13147            return false;
13148        };
13149        (args, None)
13150    };
13151
13152    // Def maps per side (prelude + own branch defs).
13153    let mut tmap: std::collections::HashMap<u32, Expr> = pmap.clone();
13154    let mut emap: std::collections::HashMap<u32, Expr> = pmap.clone();
13155    if let Side::Call { defs, .. } = &t_side {
13156        for (v, e) in defs {
13157            tmap.insert(*v, e.clone());
13158        }
13159    }
13160    if let Side::Call { defs, .. } = &e_side {
13161        for (v, e) in defs {
13162            emap.insert(*v, e.clone());
13163        }
13164    }
13165    let Some(cond_i) = inline_locals(&cond, &pmap, 0) else {
13166        return false;
13167    };
13168    // Inline each side's args through its own map.
13169    let mut then_args: Vec<Expr> = Vec::with_capacity(base_args.len());
13170    let mut else_args: Vec<Expr> = Vec::new();
13171    if two_sided {
13172        for a in base_args {
13173            match inline_locals(a, &tmap, 0) {
13174                Some(r) => then_args.push(r),
13175                None => return false,
13176            }
13177        }
13178        for a in other_args.unwrap() {
13179            match inline_locals(a, &emap, 0) {
13180                Some(r) => else_args.push(r),
13181                None => return false,
13182            }
13183        }
13184    } else {
13185        // Single-delegation side: base_args belong to whichever side
13186        // carries the call; pick that side's map.
13187        let from_then = matches!(&t_side, Side::Call { .. });
13188        for a in base_args {
13189            let r = if from_then {
13190                inline_locals(a, &tmap, 0)
13191            } else {
13192                inline_locals(a, &emap, 0)
13193            };
13194            match r {
13195                Some(r) => then_args.push(r),
13196                None => return false,
13197            }
13198        }
13199    }
13200    let primary_args = base_args;
13201    // Per-position merge.
13202    let mut merged: Vec<Expr> = Vec::with_capacity(then_args.len());
13203    let mut differ = 0usize;
13204    for (j, t) in then_args.iter().enumerate() {
13205        if two_sided {
13206            let e = &else_args[j];
13207            if t == e {
13208                merged.push(t.clone());
13209            } else {
13210                differ += 1;
13211                merged.push(Expr::Cond {
13212                    c: Box::new(cond_i.clone()),
13213                    t: Box::new(t.clone()),
13214                    f: Box::new(e.clone()),
13215                });
13216            }
13217        } else {
13218            merged.push(t.clone());
13219        }
13220    }
13221
13222    // ---- side-effect preservation guards (batched counts) ----
13223    let vars_in = |e: &Expr| -> Vec<u32> {
13224        let mut vs = Vec::new();
13225        let mut probe = e.clone();
13226        strip_lost_alloc_owners(&mut probe);
13227        deep_rewrite(&mut probe, &mut |x| {
13228            if let Expr::Local { var, .. } = x {
13229                if !vs.contains(var) {
13230                    vs.push(*var);
13231                }
13232            }
13233        });
13234        vs
13235    };
13236    // Remainder statements per branch (extras + post-call tail; a Clean
13237    // branch contributes all of its statements).
13238    // The consumed defs LEAVE the remainder: their expressions were
13239    // inlined into the merged delegation args, and a dead `v = E` left
13240    // after the call both reads wrong and trips the u_t1/u_t2 guards
13241    // (ContinuationImpl's branch-local `context = null / getContext()`
13242    // — the coroutine family's 对this的调用必须是第一个语句 root).
13243    let branch_remainder = |list: &[&Stmt], side: &Side| -> Vec<Stmt> {
13244        match side {
13245            Side::Clean => list.iter().map(|s| (*s).clone()).collect(),
13246            Side::Call { at, def_idxs, .. } => {
13247                list.iter()
13248                    .enumerate()
13249                    .filter(|(i, _)| *i != *at && !def_idxs.contains(i))
13250                    .map(|(_, s)| (*s).clone())
13251                    .collect()
13252            }
13253        }
13254    };
13255    let tail_then_v = branch_remainder(&then_list, &t_side);
13256    let tail_else_v = branch_remainder(&else_list, &e_side);
13257    let post = &stmts[if_pos + 1..];
13258    let mut inlined: Vec<u32> = vars_in(&cond);
13259    for a in primary_args.iter().chain(else_args.iter()) {
13260        for v in vars_in(a) {
13261            if !inlined.contains(&v) {
13262                inlined.push(v);
13263            }
13264        }
13265    }
13266    inlined.retain(|v| pmap.contains_key(v) || tmap.contains_key(v) || emap.contains_key(v));
13267    // Prelude-and-branch redefinition: ambiguous provenance — reject.
13268    for &v in &inlined {
13269        let in_branch = match (&t_side, &e_side) {
13270            (Side::Call { defs: td, .. }, Side::Call { defs: ed, .. }) => {
13271                td.iter().any(|(d, _)| *d == v) || ed.iter().any(|(d, _)| *d == v)
13272            }
13273            (Side::Call { defs, .. }, Side::Clean)
13274            | (Side::Clean, Side::Call { defs, .. }) => {
13275                defs.iter().any(|(d, _)| *d == v)
13276            }
13277            _ => false,
13278        };
13279        if pmap.contains_key(&v) && in_branch {
13280            return false;
13281        }
13282    }
13283    type Counts = jdc_core::FxHashMap<u32, usize>;
13284    type GuardCounts = (
13285        Counts,
13286        Vec<Counts>,
13287        Vec<Counts>,
13288        Counts,
13289        Counts,
13290        Counts,
13291        Vec<(u32, Counts)>,
13292    );
13293    let (cond_counts, a1_counts, a2_counts, tail1_counts, tail2_counts, post_counts, def_counts): GuardCounts =
13294        if inlined.is_empty() {
13295        Default::default()
13296    } else {
13297        let all_defs: Vec<(u32, &Expr)> = pmap
13298            .iter()
13299            .map(|(k, v)| (*k, v))
13300            .chain(match &t_side {
13301                Side::Call { defs, .. } => defs.iter().map(|(k, v)| (*k, v)).collect(),
13302                Side::Clean => Vec::new(),
13303            })
13304            .chain(match &e_side {
13305                Side::Call { defs, .. } => defs.iter().map(|(k, v)| (*k, v)).collect(),
13306                Side::Clean => Vec::new(),
13307            })
13308            .collect();
13309        (
13310            count_locals_expr(&cond),
13311            primary_args.iter().map(count_locals_expr).collect(),
13312            else_args.iter().map(count_locals_expr).collect(),
13313            count_locals_stmts(&tail_then_v),
13314            count_locals_stmts(&tail_else_v),
13315            count_locals_stmts(post),
13316            all_defs.into_iter().map(|(v, e)| (v, count_locals_expr(e))).collect(),
13317        )
13318        };
13319    for &v in &inlined {
13320        let in_t = matches!(&t_side, Side::Call { defs, .. } if defs.iter().any(|(d, _)| *d == v));
13321        let in_e = matches!(&e_side, Side::Call { defs, .. } if defs.iter().any(|(d, _)| *d == v));
13322        let effectful = [
13323            in_t.then(|| tmap.get(&v)).flatten(),
13324            in_e.then(|| emap.get(&v)).flatten(),
13325            pmap.get(&v),
13326        ]
13327        .into_iter()
13328        .flatten()
13329        .any(jdc_core::ir::build::has_side_effects);
13330        if !effectful {
13331            continue;
13332        }
13333        let u_cond = cnt(&cond_counts, v);
13334        let u_post = cnt(&post_counts, v);
13335        let u_t1 = cnt(&tail1_counts, v);
13336        let u_t2 = cnt(&tail2_counts, v);
13337        let mut u_odef = 0usize;
13338        for (dv, dc) in &def_counts {
13339            if *dv != v && inlined.contains(dv) {
13340                u_odef += cnt(dc, v);
13341            }
13342        }
13343        let mut emit = u_cond * (1 + differ);
13344        let mut uses_args = 0usize;
13345        for j in 0..primary_args.len() {
13346            let c1 = cnt(&a1_counts[j], v);
13347            let c2 = if two_sided && j < a2_counts.len() {
13348                cnt(&a2_counts[j], v)
13349            } else {
13350                0
13351            };
13352            uses_args += c1 + c2;
13353            emit += if two_sided && then_args[j] != else_args[j] {
13354                c1 + c2
13355            } else {
13356                c1.max(c2)
13357            };
13358        }
13359        let ok = if in_t || in_e {
13360            u_cond == 0
13361                && u_post == 0
13362                && u_t1 == 0
13363                && u_t2 == 0
13364                && u_odef == 0
13365                && a1_counts.iter().map(|c| cnt(c, v)).sum::<usize>() <= 1
13366                && a2_counts.iter().map(|c| cnt(c, v)).sum::<usize>() <= 1
13367                && emit <= 2
13368                && (in_t || a1_counts.iter().all(|c| cnt(c, v) == 0))
13369                && (in_e || a2_counts.iter().all(|c| cnt(c, v) == 0))
13370        } else {
13371            u_post == 0
13372                && u_t1 == 0
13373                && u_t2 == 0
13374                && u_odef == 0
13375                && ((u_cond == 1 && uses_args == 0 && emit <= 2)
13376                    || (u_cond == 0 && emit <= 1))
13377        };
13378        if !ok {
13379            return false;
13380        }
13381    }
13382
13383    // ---- assembly ----
13384    let mut call_stmt = call_list[call_at].clone();
13385    if let Stmt::ExprStmt(Expr::Method { args: slot, .. }) = &mut call_stmt {
13386        *slot = merged;
13387    }
13388    let residual = |v: u32| -> usize {
13389        cnt(&tail1_counts, v) + cnt(&tail2_counts, v) + cnt(&post_counts, v)
13390    };
13391    let mut kept_prelude: Vec<Stmt> = Vec::new();
13392    for (i, s) in stmts[..if_pos].iter().enumerate() {
13393        let consumed = match s {
13394            Stmt::LocalDef { var, .. } => Some(*var),
13395            Stmt::ExprStmt(Expr::Assign { target, .. }) => match &**target {
13396                Expr::Local { var, .. } => Some(*var),
13397                _ => None,
13398            },
13399            Stmt::If { .. } => cond_def_of(s, stmts).map(|(v, _)| v),
13400            _ => None,
13401        };
13402        if let Some(v) = consumed {
13403            if inlined.contains(&v) && pidx.get(&v) == Some(&i) && residual(v) == 0 {
13404                continue;
13405            }
13406        }
13407        kept_prelude.push(s.clone());
13408    }
13409    let build_keep = |list: &[&Stmt], side: &Side| -> Vec<Stmt> {
13410        match side {
13411            Side::Clean => list.iter().map(|s| (*s).clone()).collect(),
13412            Side::Call { at, .. } => {
13413                let mut out = Vec::new();
13414                for (i, s) in list.iter().enumerate() {
13415                    if i == *at {
13416                        continue;
13417                    }
13418                    if i < *at {
13419                        if let Some((v, _)) = def_of(s) {
13420                            if inlined.contains(&v) && residual(v) == 0 {
13421                                continue;
13422                            }
13423                        }
13424                    }
13425                    out.push((*s).clone());
13426                }
13427                out
13428            }
13429        }
13430    };
13431    let then_keep = build_keep(&then_list, &t_side);
13432    let else_keep = build_keep(&else_list, &e_side);
13433    let else_part = if else_keep.is_empty() && !had_else {
13434        None
13435    } else {
13436        Some(Box::new(Stmt::Block(else_keep)))
13437    };
13438    let new_if = Stmt::If {
13439        cond: cond_i,
13440        then_stmt: Box::new(Stmt::Block(then_keep)),
13441        else_stmt: else_part,
13442    };
13443    let if_empty = match &new_if {
13444        Stmt::If { then_stmt, else_stmt, .. } => {
13445            let be = |s: &Stmt| matches!(s, Stmt::Block(v) if v.is_empty());
13446            be(then_stmt) && else_stmt.as_ref().is_none_or(|e| be(e))
13447        }
13448        _ => false,
13449    };
13450    let mut out: Vec<Stmt> = Vec::with_capacity(stmts.len());
13451    out.push(call_stmt);
13452    out.extend(kept_prelude);
13453    if !if_empty {
13454        out.push(new_if);
13455    }
13456    out.extend(stmts[if_pos + 1..].to_vec());
13457    *stmts = out;
13458    true
13459}
13460
13461
13462pub fn strip_enum_ctor_super(body: &mut Stmt) {
13463    fn is_enum_super(s: &Stmt) -> bool {
13464        matches!(
13465            s,
13466            Stmt::ExprStmt(Expr::Method { name, is_super: true, .. }) if &**name == "<init>"
13467        )
13468    }
13469    // FULL depth: hotfix-guarded ctors hide a second super(String,int)
13470    // inside the proxy branch (alipay InstantRun: `if (proxy != null) {
13471    // super((String) v3[0], ..); proxy.afterSuper(this); return; }` at
13472    // depth 3+; the old two-level strip left it — super resolved against
13473    // Object once the fallback enum lost `extends Enum`, alipay
13474    // ctor-arity ×3,364). This path only runs for fallback enums whose
13475    // dex super is Enum/Object, so EVERY super call is a dead Enum-ctor
13476    // reference wherever it sits; constant subclasses (real supers)
13477    // take the other branch in method.rs and are untouched.
13478    fn strip_deep(stmts: &mut Vec<Stmt>) {
13479        stmts.retain(|s| !is_enum_super(s));
13480        for st in stmts.iter_mut() {
13481            strip_one(st);
13482        }
13483    }
13484    fn strip_one(st: &mut Stmt) {
13485        match st {
13486            Stmt::Block(v) => strip_deep(v),
13487            Stmt::If { then_stmt, else_stmt, .. } => {
13488                strip_one(then_stmt);
13489                if let Some(e) = else_stmt {
13490                    strip_one(e);
13491                }
13492            }
13493            Stmt::While { body, .. }
13494            | Stmt::DoWhile { body, .. }
13495            | Stmt::ForEach { body, .. }
13496            | Stmt::Synchronized { body, .. }
13497            | Stmt::Labeled { body, .. } => strip_one(body),
13498            Stmt::For { init, body, .. } => {
13499                strip_deep(init);
13500                strip_one(body);
13501            }
13502            Stmt::Switch { cases, default, .. } => {
13503                for c in cases.iter_mut() {
13504                    strip_deep(&mut c.body);
13505                }
13506                if let Some(d) = default {
13507                    strip_one(d);
13508                }
13509            }
13510            Stmt::Try { body, catches, finally }
13511            | Stmt::TryWithResources { body, catches, finally, .. } => {
13512                strip_one(body);
13513                for c in catches.iter_mut() {
13514                    strip_one(&mut c.body);
13515                }
13516                if let Some(f) = finally {
13517                    strip_one(f);
13518                }
13519            }
13520            _ => {}
13521        }
13522    }
13523    let Stmt::Block(stmts) = body else {
13524        return;
13525    };
13526    strip_deep(stmts);
13527}
13528
13529pub fn ensure_declared(body: &mut Stmt, vt: &VarTable) {
13530    // (Perf: dense Vec<bool> tables instead of HashSets — var ids are
13531    // dense; the `assigned` set collected here was never read and cost a
13532    // full extra tree walk per method.)
13533    let n = vt.vars.len().max(1);
13534
13535    // Demote extra LocalDefs (same var declared more than once) FIRST —
13536    // the scope analysis below must see exactly one def per var.
13537    let mut seen: HashSet<u32> = HashSet::default();
13538    demote_dup_decls(body, &mut seen, vt);
13539
13540    // Scope hoist: a LocalDef inside a nested block whose var is also
13541    // referenced OUTSIDE that block is a Java scope violation (the
13542    // branch-local `int v8 = pi.versionCode;` read after the try —
13543    // "cannot find symbol" at every outside use). Demote those defs to
13544    // assignments and let the bare top-of-method declaration below cover
13545    // the var. Two cheap passes: pre-order block numbering + per-var
13546    // inside/outside occurrence counts (pre-order subtree = id range).
13547    let mut def_block: Vec<u32> = vec![u32::MAX; n];
13548    let mut sizes: Vec<u32> = vec![0];
13549    let mut next_id: u32 = 1;
13550    scope_pass_a(body, 0, &mut next_id, &mut def_block, &mut sizes);
13551    sizes[0] = next_id;
13552    let mut within: Vec<u32> = vec![0; n];
13553    let mut total: Vec<u32> = vec![0; n];
13554    let mut next_b: u32 = 1;
13555    scope_pass_b(body, 0, &mut next_b, &def_block, &sizes, &mut within, &mut total);
13556    let mut hoist: HashSet<u32> = HashSet::default();
13557    for v in 0..n {
13558        if def_block[v] != u32::MAX && def_block[v] != 0 && within[v] < total[v] {
13559            hoist.insert(v as u32);
13560        }
13561    }
13562    if !hoist.is_empty() {
13563        let mut seed: HashSet<u32> = hoist.iter().copied().collect();
13564        demote_dup_decls(body, &mut seed, vt);
13565    }
13566
13567    let mut declared: Vec<bool> = vec![false; n];
13568    let mut is_param: Vec<bool> = vec![false; n];
13569    for v in &vt.vars {
13570        if v.is_param && (v.id as usize) < n {
13571            is_param[v.id as usize] = true;
13572        }
13573    }
13574    // Catch parameters are declared by the `catch (Type v)` clause —
13575    // bind_catches consumed their LocalDef before this pass ran, so
13576    // without this they re-declare at the top of the method and clash
13577    // with the clause (`Throwable th2;` + `catch (Throwable th2)` — a
13578    // syntax gate cannot see this, it is a semantic "already defined").
13579    let mut is_catch: Vec<bool> = vec![false; n];
13580    walk_all(body, &mut |st| {
13581        if let Stmt::Try { catches, .. } = st {
13582            for c in catches {
13583                if (c.var as usize) < n {
13584                    is_catch[c.var as usize] = true;
13585                }
13586            }
13587        }
13588    });
13589    walk_all(body, &mut |st| {
13590        if let Stmt::LocalDef { var, .. } = st {
13591            if (*var as usize) < n {
13592                declared[*var as usize] = true;
13593            }
13594        }
13595    });
13596    let mut used: HashSet<u32> = HashSet::default();
13597    // assignments=true: a var that only ever appears as an assign
13598    // TARGET still needs a declaration (`x = 5;` alone does not
13599    // declare x in Java).
13600    stmt_collect_vars(body, &mut used, true);
13601
13602    // Params are declared by the signature; hoisted vars lost their
13603    // LocalDef to the demotion above and always need the bare decl.
13604    let mut needs_decl: Vec<u32> = used
13605        .into_iter()
13606        .filter(|v| {
13607            let i = *v as usize;
13608            i >= n
13609                || ((!declared[i] && !is_param[i] && !is_catch[i]) || hoist.contains(v))
13610        })
13611        .collect();
13612    needs_decl.sort_unstable();
13613    needs_decl.dedup();
13614
13615    if !needs_decl.is_empty() {
13616        let mut decls: Vec<Stmt> = needs_decl
13617            .into_iter()
13618            .map(|v| Stmt::LocalDef {
13619                var: v,
13620                init: None,
13621                is_final: false,
13622                force_type: true,
13623            })
13624            .collect();
13625        decls.reverse();
13626        match body {
13627            Stmt::Block(v) => {
13628                for d in decls {
13629                    v.insert(0, d);
13630                }
13631            }
13632            other => {
13633                let inner = std::mem::replace(other, Stmt::Block(vec![]));
13634                *other = Stmt::Block({
13635                    let mut vs = decls;
13636                    vs.push(inner);
13637                    vs
13638                });
13639            }
13640        }
13641    }
13642}
13643
13644fn demote_dup_decls(s: &mut Stmt, seen: &mut HashSet<u32>, vt: &VarTable) {
13645    match s {
13646        // Exactly one visit per node: the Block arm consumes its children,
13647        // the fallthrough walk would double-visit them.
13648        Stmt::Block(v) => {
13649            for x in v.iter_mut() {
13650                demote_dup_decls(x, seen, vt);
13651            }
13652        }
13653        Stmt::LocalDef { var, init, .. } => {
13654            if seen.contains(var) {
13655                let ty = vt.var(*var).ty.clone();
13656                if let Some(e) = init.take() {
13657                    *s = Stmt::ExprStmt(Expr::Assign {
13658                        target: Box::new(Expr::Local { var: *var, ty }),
13659                        op: AssignOp::Plain,
13660                        value: Box::new(e),
13661                    });
13662                } else {
13663                    *s = Stmt::Block(vec![]);
13664                }
13665            } else {
13666                seen.insert(*var);
13667            }
13668        }
13669        other => {
13670            walk_mut(other, &mut |x| demote_dup_decls(x, seen, vt));
13671        }
13672    }
13673}
13674
13675#[allow(dead_code)]
13676fn unused(_: &Vec<CaseGroup>, _: &Catch) {}
13677
13678// ---------------------------------------------------------------------------
13679// jadx-style local names (ApplyVariableNames + dexdec's port of it)
13680// ---------------------------------------------------------------------------
13681
13682/// A synthetic `v12`/`p3` never survives when a better name exists.
13683/// Priority: (1) a Kotlin `Intrinsics.checkNotNullParameter(x, "name")`
13684/// names `x` from the message string (the string IS the parameter name);
13685/// (2) the single defining call — `getFoo()` → `foo`, `isFinishing()` →
13686/// `finishing`, `new File(…)` → `file`; (3) a type alias or the
13687/// lowercased class simple name. Collisions take `2`, `3`, …; a var with
13688/// two DISAGREEING defining calls stays unnamed (dexdec's
13689/// RelationalNameInference rule). Debug-info names are never touched
13690/// (`synthetic_name` gates the whole pass).
13691pub fn apply_local_names(vt: &mut VarTable, body: &Stmt) {
13692    use std::collections::{HashMap, HashSet};
13693
13694    // ---- proposals ------------------------------------------------------
13695    // var → (name, source-priority); disagreeing call names are dropped.
13696    let mut by_call: HashMap<u32, Vec<String>> = HashMap::new();
13697    let mut by_intrinsics: HashMap<u32, String> = HashMap::new();
13698
13699    visit_all_exprs(body, &mut |e| {
13700        if let Expr::Method { cls, name, args, .. } = e {
13701            if (cls.as_ref() == "kotlin/jvm/internal/Intrinsics"
13702                || cls.as_ref() == "kotlin/jvm/internal/IntrinsicsKt")
13703                && matches!(name.as_ref(), "checkNotNullParameter" | "checkParameterIsNotNull")
13704                && args.len() == 2
13705            {
13706                if let (Expr::Local { var, .. }, Expr::Const(ConstVal::Str(s))) = (&args[0], &args[1])
13707                {
13708                    by_intrinsics
13709                        .entry(*var)
13710                        .or_insert_with(|| sanitize_name(s).unwrap_or_default());
13711                }
13712            }
13713        }
13714    });
13715
13716    walk_all(body, &mut |st| {
13717        let (var, value) = match st {
13718            Stmt::LocalDef { var, init: Some(e), .. } => (*var, e),
13719            Stmt::ExprStmt(Expr::Assign { target, value, .. })
13720                if matches!(&**target, Expr::Local { .. }) =>
13721            {
13722                let Expr::Local { var, .. } = &**target else { return };
13723                (*var, value.as_ref())
13724            }
13725            _ => return,
13726        };
13727        if let Some(n) = defining_call_name(value) {
13728            by_call.entry(var).or_default().push(n);
13729        }
13730    });
13731
13732    // ---- reservation + application --------------------------------------
13733    // Real (debug-info) names can legally REPEAT across sibling source
13734    // scopes (`int i` in two loops) but our flat declaration hoisting
13735    // puts them in one scope — de-duplicate with the same numeric
13736    // suffixes the claim path uses.
13737    // Uniqueness operates on the SANITIZED display name: the identifier
13738    // sanitizer maps non-ASCII to `_`, so distinct debug names
13739    // (`ERROR_token参数缺失` / `ERROR_channelId参数缺失`) both RENDER as
13740    // `ERROR________` and collide as declarations. Tracking raw names
13741    // here would let the collision through.
13742    let sanit = |n: &str| crate::classdec::java_ident(n).into_owned();
13743    let mut taken: HashSet<String> = HashSet::default();
13744    // Parameter names are occupied REGARDLESS of syntheticness: claim()
13745    // hands out rename candidates, and without the synthetic params in
13746    // the pool a type fallback (`l7.p0` → "p0") renamed one parameter
13747    // ONTO another's slot-name (`b(v6.l p0, Object obj, l7.p0 p0)`).
13748    for v in vt.vars.iter() {
13749        // ALL synthetic names occupy their names, not just parameters:
13750        // a type fallback (`pc5.v56` → "v56") otherwise renames a local
13751        // ONTO another local's slot-name (`int v56;` beside
13752        // `pc5.v56 v56;`).
13753        if v.synthetic_name {
13754            taken.insert(sanit(&v.name));
13755        }
13756    }
13757    for v in vt.vars.iter_mut() {
13758        if v.synthetic_name {
13759            continue;
13760        }
13761        if !taken.insert(sanit(&v.name)) {
13762            let base = sanit(&v.name);
13763            for i in 2.. {
13764                let cand = format!("{base}{i}");
13765                if taken.insert(cand.clone()) {
13766                    v.name = cand;
13767                    break;
13768                }
13769            }
13770        }
13771    }
13772    let claim = |taken: &mut HashSet<String>, want: &str| -> String {
13773        let w = crate::classdec::java_ident(want).into_owned();
13774        if taken.insert(w.clone()) {
13775            return w;
13776        }
13777        for i in 2.. {
13778            let cand = format!("{w}{i}");
13779            if taken.insert(cand.clone()) {
13780                return cand;
13781            }
13782        }
13783        w
13784    };
13785
13786    for info in vt.vars.iter_mut() {
13787        if !info.synthetic_name {
13788            continue;
13789        }
13790        // (1) Intrinsics string — the Kotlin compiler wrote the real
13791        // parameter name right into the check.
13792        if let Some(n) = by_intrinsics.get(&info.id) {
13793            if !n.is_empty() {
13794                info.name = claim(&mut taken, n);
13795                continue;
13796            }
13797        }
13798        // (2) One consistent defining call.
13799        if let Some(names) = by_call.get(&info.id) {
13800            let unique: HashSet<&String> = names.iter().collect();
13801            if unique.len() == 1 {
13802                let n = unique.into_iter().next().unwrap();
13803                if !is_java_keyword(n) {
13804                    info.name = claim(&mut taken, n);
13805                    continue;
13806                }
13807            }
13808        }
13809        // (3) Type alias, else the lowercased simple class name
13810        // (jadx names a `Looper` local `looper`).
13811        let want = type_alias(&info.ty).map(str::to_string).or_else(|| simple_type_name(&info.ty));
13812        if let Some(n) = want {
13813            info.name = claim(&mut taken, &n);
13814        }
13815    }
13816
13817    // Final uniquification across ALL vars (params, locals, catch): the
13818    // reservation, claim and debug-name domains above are checked
13819    // pairwise but a name claimed LAST can still equal a debug name
13820    // that never re-checked (`zd1.v2 v2` beside `zd1.v2[] v2`).
13821    let mut seen: HashSet<String> = HashSet::default();
13822    for v in vt.vars.iter_mut() {
13823        if !seen.insert(sanit(&v.name)) {
13824            let base = sanit(&v.name);
13825            for i in 2.. {
13826                let cand = format!("{base}{i}");
13827                if seen.insert(cand.clone()) {
13828                    v.name = cand;
13829                    break;
13830                }
13831            }
13832        }
13833    }
13834}
13835
13836/// `com/android/.../Looper` → `looper` — the alias-table fallback.
13837fn simple_type_name(ty: &TypeRef) -> Option<String> {
13838    let TypeRef::J(JavaType::Object(n)) = ty else {
13839        return None;
13840    };
13841    let simple = n.rsplit('/').next().unwrap_or(n);
13842    let simple = simple.rsplit('$').next().unwrap_or(simple);
13843    // Lowercase the first ALPHABETIC char — `$`/`_`-prefixed names
13844    // (R8's `$$$_Thread`) must not survive capitalized.
13845    let mut chars = simple.chars();
13846    let mut out = String::with_capacity(simple.len());
13847    let mut lowered = false;
13848    for c in chars.by_ref() {
13849        if c.is_ascii_alphabetic() && !lowered {
13850            out.push(c.to_ascii_lowercase());
13851            lowered = true;
13852        } else {
13853            out.push(c);
13854        }
13855        if lowered {
13856            break;
13857        }
13858    }
13859    out.extend(chars);
13860    sanitize_name(&out)
13861}
13862
13863/// `getFoo()` → `foo`, `isFinishing()` → `finishing`, `new File(…)` →
13864/// `file`. The get/is prefixes only strip when the remainder starts
13865/// uppercase (so `issues()` keeps its name).
13866fn defining_call_name(e: &Expr) -> Option<String> {
13867    match e {
13868        Expr::Method { name, .. } => {
13869            let base = name
13870                .strip_prefix("get")
13871                .or_else(|| name.strip_prefix("is"))
13872                .filter(|rest| rest.chars().next().is_some_and(|c| c.is_ascii_uppercase()))
13873                .unwrap_or(name);
13874            let first = base.chars().next()?;
13875            let mut out = String::with_capacity(base.len());
13876            out.push(first.to_ascii_lowercase());
13877            out.extend(base.chars().skip(1));
13878            sanitize_name(&out)
13879        }
13880        Expr::New { cls, .. } => {
13881            let simple = cls.rsplit('/').next().unwrap_or(cls);
13882            let simple = simple.rsplit('$').next().unwrap_or(simple);
13883            let first = simple.chars().next()?;
13884            let mut out = String::with_capacity(simple.len());
13885            out.push(first.to_ascii_lowercase());
13886            out.extend(simple.chars().skip(1));
13887            sanitize_name(&out)
13888        }
13889        _ => None,
13890    }
13891}
13892
13893/// Valid java identifier, not a keyword/restricted name, ≥2 chars.
13894fn sanitize_name(s: &str) -> Option<String> {
13895    let mut out = String::with_capacity(s.len());
13896    for c in s.chars() {
13897        if c.is_ascii_alphanumeric() || c == '_' || c == '$' {
13898            out.push(c);
13899        } else {
13900            out.push('_');
13901        }
13902    }
13903    if out.len() < 2
13904        || out.chars().next().is_some_and(|c| c.is_ascii_digit())
13905        || is_java_keyword(&out)
13906    {
13907        return None;
13908    }
13909    Some(out)
13910}
13911
13912/// jadx's alias table for the common types, with the Android globals the
13913/// corpus actually shows; anything else falls back in the caller.
13914fn type_alias(ty: &TypeRef) -> Option<&'static str> {
13915    let TypeRef::J(JavaType::Object(n)) = ty else {
13916        return None;
13917    };
13918    Some(match n.as_ref() {
13919        "java/lang/String" | "kotlin/String" => "str",
13920        "java/lang/Class" => "cls",
13921        "java/lang/Throwable" => "th",
13922        "java/lang/Object" | "kotlin/Any" => "obj",
13923        "java/util/Iterator" | "kotlin/collections/Iterator" => "it",
13924        "java/lang/Boolean" => "bool",
13925        "java/lang/Integer" => "num",
13926        "java/lang/Character" => "ch",
13927        "java/lang/Byte" => "b",
13928        "java/lang/Short" => "sh",
13929        "java/lang/Float" => "f",
13930        "java/lang/Double" => "d",
13931        "java/lang/Long" => "i",
13932        "java/lang/StringBuilder" => "sb",
13933        "java/util/ArrayList" => "list",
13934        "java/util/HashMap" => "map",
13935        "android/content/Context" => "context",
13936        "android/content/Intent" => "intent",
13937        "android/os/Bundle" => "bundle",
13938        "android/view/View" => "view",
13939        "android/graphics/Bitmap" => "bitmap",
13940        "android/view/ViewGroup" => "viewGroup",
13941        _ => return None,
13942    })
13943}
13944
13945/// Every expression in the tree, statements included (read-only).
13946pub(crate) fn visit_all_exprs<F: FnMut(&Expr)>(s: &Stmt, f: &mut F) {
13947    walk_all(s, &mut |st| {
13948        let exprs: Vec<&Expr> = match st {
13949            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
13950                let mut v: Vec<&Expr> = vec![value];
13951                if !matches!(&**target, Expr::Local { .. }) {
13952                    v.push(target);
13953                }
13954                v
13955            }
13956            Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
13957                vec![e]
13958            }
13959            Stmt::Return(Some(e)) => vec![e],
13960            Stmt::LocalDef { init: Some(e), .. } => vec![e],
13961            Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
13962                vec![cond]
13963            }
13964            _ => Vec::new(),
13965        };
13966        for e in exprs {
13967            visit_exprs(e, f);
13968        }
13969    });
13970}
13971
13972/// IntDef/LongDef exact-match rendering: a literal argument to a
13973/// platform method whose parameter carries a constant domain renders as
13974/// the named constant (`setVisibility(8)` → `android.view.View.GONE`).
13975/// Combined flag values and literals outside the domain stay numeric.
13976pub fn platform_constants(body: &mut Stmt) {
13977    if !crate::platform::installed() {
13978        return;
13979    }
13980    rewrite_exprs(body, &mut |e| {
13981        if let Expr::Method { cls, name, desc, args, .. } = e {
13982            for a in args.iter_mut() {
13983                let (value, is_long) = match a {
13984                    Expr::Const(ConstVal::Int(v)) => (*v as i64, false),
13985                    Expr::Const(ConstVal::Long(v)) => (*v, true),
13986                    _ => continue,
13987                };
13988                if let Some(named) =
13989                    crate::platform::named_constant(cls, name, &desc_to_string(desc), 0, value)
13990                {
13991                    let _ = is_long;
13992                    *a = Expr::Raw(named);
13993                }
13994            }
13995        }
13996    });
13997}
13998
13999/// MethodDescriptor → dex descriptor string (`(I)V`).
14000fn desc_to_string(d: &jdc_core::types::MethodDescriptor) -> String {
14001    let mut out = String::from("(");
14002    for a in &d.args {
14003        type_to_desc(a, &mut out);
14004    }
14005    out.push(')');
14006    type_to_desc(&d.ret, &mut out);
14007    out
14008}
14009
14010fn type_to_desc(t: &jdc_core::types::JavaType, out: &mut String) {
14011    match t {
14012        JavaType::Void => out.push('V'),
14013        JavaType::Boolean => out.push('Z'),
14014        JavaType::Byte => out.push('B'),
14015        JavaType::Char => out.push('C'),
14016        JavaType::Short => out.push('S'),
14017        JavaType::Int => out.push('I'),
14018        JavaType::Float => out.push('F'),
14019        JavaType::Long => out.push('J'),
14020        JavaType::Double => out.push('D'),
14021        JavaType::Object(n) => {
14022            out.push('L');
14023            out.push_str(n);
14024            out.push(';');
14025        }
14026        JavaType::Array(inner) => {
14027            out.push('[');
14028            type_to_desc(inner, out);
14029        }
14030    }
14031}
14032
14033/// Kotlin `Intrinsics` null-check elision (jadx's ProcessKotlinInternals):
14034/// statement-position `checkNotNullParameter(p, "name")` /
14035/// `checkParameterIsNotNull` / `checkNotNull…` calls are runtime
14036/// assertions — drop them. The naming pass harvests the parameter
14037/// strings FIRST, so run this after `apply_local_names`.
14038pub fn remove_kotlin_checks(s: &mut Stmt) {
14039    walk_mut_deep(s, &mut |st| {
14040        let Stmt::Block(v) = st else { return };
14041        v.retain(|x| !is_kotlin_check(x));
14042    });
14043}
14044
14045fn is_kotlin_check(s: &Stmt) -> bool {
14046    let Stmt::ExprStmt(e) = s else { return false };
14047    let Expr::Method { cls, name, .. } = e else { return false };
14048    (cls.as_ref() == "kotlin/jvm/internal/Intrinsics" || cls.as_ref() == "kotlin/jvm/internal/IntrinsicsKt__Jdk7Kt")
14049        && matches!(
14050            name.as_ref(),
14051            "checkNotNullParameter"
14052                | "checkParameterIsNotNull"
14053                | "checkExpressionValueIsNotNull"
14054                | "checkNotNullExpressionValue"
14055                | "checkReturnedValueIsNotNull"
14056                | "checkFieldIsNotNull"
14057                | "checkNotNull"
14058        )
14059}
14060
14061// ---------------------------------------------------------------------------
14062// Synthetic-accessor inlining (jadx's MarkMethodsForInline, the safe subset)
14063// ---------------------------------------------------------------------------
14064
14065/// Inline `access$NNN`-style synthetic static bridges at their call
14066/// sites: an identity (`return pN`), a getter (`iget pX, field;
14067/// return vR`), or a method forwarder (`invoke; return`) — with the
14068/// d8 APM trace wrappers (`MethodCollector.i/.o` const-only static
14069/// calls) tolerated around the core. Only STATIC + SYNTHETIC callees
14070/// qualify (compiler-generated bridges: no override semantics, no
14071/// side effects beyond the forwarded operation).
14072/// Can `host` (internal class name) source-level reference `target`'s
14073/// class chain and member? Cross-package inlining of a synthetic
14074/// accessor used to EXPOSE package-private targets the raw dex never
14075/// referenced cross-package (BuildersKt.launch$default forwards to
14076/// kotlinx.coroutines.k.e — the widening census cannot see post-inline
14077/// calls; lark's 1,019-line k不是公共的 family). Non-public chain link
14078/// or non-public member across packages → the inline must not happen
14079/// (the public synthetic bridge stays and compiles).
14080fn inline_ref_ok(pool: &DexPool, host: &str, target: &str, member: Option<(&str, bool)>) -> bool {
14081    let hpkg = match host.rfind('/') {
14082        Some(i) => &host[..i],
14083        None => "",
14084    };
14085    let mut cur = target;
14086    loop {
14087        let tpkg = match cur.rfind('/') {
14088            Some(i) => &cur[..i],
14089            None => "",
14090        };
14091        if tpkg != hpkg {
14092            if let Some(pc) = pool.get(cur) {
14093                if pc.access & crate::access::ACC_PUBLIC == 0 {
14094                    return false;
14095                }
14096                if cur == target {
14097                    if let Some((mname, is_method)) = member {
14098                        let ok = if is_method {
14099                            pc.all_methods().any(|m| {
14100                                &*m.name == mname
14101                                    && m.access & crate::access::ACC_PUBLIC != 0
14102                            })
14103                        } else {
14104                            pc.static_fields
14105                                .iter()
14106                                .chain(pc.instance_fields.iter())
14107                                .any(|f| {
14108                                    f.name.as_ref() == mname
14109                                        && f.access & crate::access::ACC_PUBLIC != 0
14110                                })
14111                        };
14112                        // A member match requires the exact member to be
14113                        // public; an absent member (renamed/collapsed)
14114                        // stays allowed — the render will show what the
14115                        // pool has.
14116                        if !ok
14117                            && (if is_method {
14118                                pc.all_methods().any(|m| &*m.name == mname)
14119                            } else {
14120                                pc.static_fields
14121                                    .iter()
14122                                    .chain(pc.instance_fields.iter())
14123                                    .any(|f| f.name.as_ref() == mname)
14124                            })
14125                        {
14126                            return false;
14127                        }
14128                    }
14129                }
14130            }
14131        }
14132        match cur.rfind('$') {
14133            Some(i) if i > 0 => cur = &cur[..i],
14134            _ => break,
14135        }
14136    }
14137    true
14138}
14139
14140pub fn inline_accessors(s: &mut Stmt, pool: &DexPool, host: &str) {
14141    rewrite_exprs(s, &mut |e| {
14142        let Expr::Method { cls, name, desc, args, is_static, .. } = e else { return };
14143        if !*is_static || args.is_empty() {
14144            return;
14145        }
14146        let Some(target) = pool.get(cls) else { return };
14147        let desc_s = desc_to_string(desc);
14148        let Some(m) = target.find_method(name, &desc_s) else { return };
14149        if !(m.is_static() && m.access & access::ACC_SYNTHETIC != 0) {
14150            return;
14151        }
14152        let Some(dex) = pool.dex(m.dex_idx) else { return };
14153        // Snapshot first: the accessor's owning image may already be
14154        // retired, and a live read's success would depend on worker
14155        // completion interleaving — nondeterministic inlining (whether
14156        // the accessor folds at all) between identical runs.
14157        let code = match pool.accessor_code(m.dex_idx, m.code_off) {
14158            Some(bytes) => ddc_dex::CodeItem::parse(&bytes, 0),
14159            None => dex.code_at(m.code_off),
14160        };
14161        let Some(code) = code else { return };
14162        let insns: Vec<&Insn> =
14163            code.insns.iter().filter(|i| !matches!(i.kind, InsnKind::Nop)).collect();
14164        // Strip the APM trace wrappers (const-only invoke-static).
14165        let core: Vec<&Insn> = insns
14166            .iter()
14167            .copied()
14168            .filter(|i| !is_const_only_trace(&i.kind, &insns))
14169            .collect();
14170        match accessor_shape(&dex, &core, &code) {
14171            Some(Shape::Identity(arg_i)) => {
14172                if let Some(a) = args.get(arg_i) {
14173                    *e = a.clone();
14174                }
14175            }
14176            Some(Shape::FieldRead { arg, cls: field_cls, field, field_ty }) => {
14177                if !inline_ref_ok(pool, host, &field_cls, Some((&field, false))) {
14178                    return;
14179                }
14180                if let Some(owner) = args.get(arg).cloned().map(Box::new) {
14181                    let ty = TypeRef::J(desc_type(&field_ty));
14182                    // The inlined read must consult the member rename
14183                    // registry like every lifted field ref does — the
14184                    // raw dex name desynced from renamed declarations
14185                    // (lark uu4/f$b's `a`→`ax6`: 找不到符号 变量 a ×4.4k
14186                    // when the nested-collision deshadow fired).
14187                    let name = jdc_core::rename::field_display(
14188                        &field_cls,
14189                        &field,
14190                        &field_ty,
14191                    )
14192                    .map(std::sync::Arc::from)
14193                    .unwrap_or_else(|| field.into());
14194                    *e = Expr::Field {
14195                        owner: Some(owner),
14196                        cls: field_cls.into(),
14197                        name,
14198                        ty,
14199                        is_static: false,
14200                    };
14201                }
14202            }
14203            Some(Shape::Forward { cls: tcls, name: tname, desc: tdesc, instance }) => {
14204                if !inline_ref_ok(pool, host, &tcls, Some((&tname, true))) {
14205                    return;
14206                }
14207                // Same registry duty as FieldRead: the forwarded member
14208                // must render under its renamed display, not the raw
14209                // dex name.
14210                let disp = jdc_core::rename::field_display(
14211                    &tcls,
14212                    &tname,
14213                    &tdesc.to_string(),
14214                )
14215                .map(std::sync::Arc::from)
14216                .unwrap_or_else(|| tname.into());
14217                *cls = tcls.into();
14218                *name = disp;
14219                *desc = std::sync::Arc::new(tdesc);
14220                if instance && !args.is_empty() {
14221                    // The first param (the receiver) becomes the owner.
14222                    let recv = args.remove(0);
14223                    if let Expr::Method { owner, .. } = e {
14224                        *owner = Some(Box::new(recv));
14225                    }
14226                }
14227            }
14228            None => {}
14229        }
14230    });
14231}
14232
14233/// What an accessor body reduces to.
14234enum Shape {
14235    /// `return pN` — the call becomes argument N.
14236    Identity(usize),
14237    /// `iget vR, pX, field; return vR` — becomes `argN.field`.
14238    FieldRead { arg: usize, cls: String, field: String, field_ty: String },
14239    /// `invoke {pX, args…}, method@M; (move-result;)? return` —
14240    /// becomes the forwarded call.
14241    Forward { cls: String, name: String, desc: MethodDescriptor, instance: bool },
14242}
14243
14244/// Resolve the IGet field's owner class, name and type descriptor.
14245fn field_of(dex: &ddc_dex::DexFile, field_idx: u32) -> Option<(String, String, String)> {
14246    let f = dex.field(field_idx);
14247    Some((
14248        // class_name strips the `L...;` shell; type_name would leak a
14249        // descriptor into the Field's owner class (`La3.a.e_`).
14250        dex.class_name(f.class_idx),
14251        dex.string(f.name_idx).to_string(),
14252        // The FIELD TYPE stays a descriptor: desc_type parses it.
14253        dex.type_name(f.type_idx).to_string(),
14254    ))
14255}
14256
14257/// Map a register to a parameter index (static method: params occupy
14258/// the LAST ins_size registers).
14259fn param_index(code: &ddc_dex::CodeItem, reg: u16) -> Option<usize> {
14260    let rs = code.registers_size as usize;
14261    let ins = code.ins_size as usize;
14262    let r = reg as usize;
14263    let first = rs.checked_sub(ins)?;
14264    (r >= first && r < rs).then(|| r - first)
14265}
14266
14267fn accessor_shape(
14268    dex: &ddc_dex::DexFile,
14269    core: &[&Insn],
14270    code: &ddc_dex::CodeItem,
14271) -> Option<Shape> {
14272    // Dead consts that fed the stripped trace wrappers remain in the
14273    // core — skip them. ONLY when actually dead (dst unread by the
14274    // rest): a const-returning synthetic (`lambda$new$4(String) {
14275    // return false; }` = `const/4 v0,0; return v0`, registers=1 so v0
14276    // IS the param) lost its const and matched Identity(param0) —
14277    // inline_accessors then replaced the CALL with its ARGUMENT
14278    // (`return (String) obj` against a boolean SAM — weibo
14279    // IntentSanitizer lambdas ×116, and silently WRONG renders
14280    // wherever the types happened to agree).
14281    let reads = |rest: &[&Insn], r: u16| {
14282        rest.iter()
14283            .any(|i| crate::lift::src_regs(&i.kind).contains(&r))
14284    };
14285    let core: &[&Insn] = match core.split_first() {
14286        Some((first, rest))
14287            if matches!(first.kind, InsnKind::Const { dst, .. } if !reads(rest, dst)) =>
14288        {
14289            rest
14290        }
14291        _ => core,
14292    };
14293    let core: &[&Insn] = match core.split_last() {
14294        Some((last, rest))
14295            if matches!(last.kind, InsnKind::Const { dst, .. } if !reads(rest, dst)) =>
14296        {
14297            rest
14298        }
14299        _ => core,
14300    };
14301    match core {
14302        // return pN
14303        [Insn { kind: InsnKind::Return { src }, .. }] => {
14304            let idx = param_index(code, *src)?;
14305            Some(Shape::Identity(idx))
14306        }
14307        // iget vR, pX, field; return vR
14308        [
14309            Insn { kind: InsnKind::IGet { dst, obj, field_idx }, .. },
14310            Insn { kind: InsnKind::Return { src }, .. },
14311        ] if dst == src => {
14312            let arg = param_index(code, *obj)?;
14313            let (cls, field, field_ty) = field_of(dex, *field_idx)?;
14314            Some(Shape::FieldRead { arg, cls, field, field_ty })
14315        }
14316        // invoke {…}, method@M; (move-result; return)? — forwarder.
14317        [Insn { kind: InsnKind::Invoke { method_idx, regs, kind, .. }, .. }, rest @ ..] if rest.len() <= 2 => {
14318            // Only forward when every register is a param (no locals).
14319            if !regs.iter().all(|r| param_index(code, *r).is_some()) || regs.is_empty() {
14320                return None;
14321            }
14322            // Trailing must be move-result+return or return/void —
14323            // anything else (e.g. trace calls) already filtered upstream.
14324            if rest.len() == 2 {
14325                let ok = matches!(rest[0].kind, InsnKind::MoveResult { .. })
14326                    && matches!(rest[1].kind, InsnKind::Return { .. } | InsnKind::ReturnVoid);
14327                if !ok {
14328                    return None;
14329                }
14330            }
14331            let mid = dex.method(*method_idx);
14332            // class_name strips the `L...;` shell — type_name leaks a
14333            // descriptor into the forwarded call's class (`La3.a.e_`).
14334            let cls = dex.class_name(mid.class_idx);
14335            let name = dex.string(mid.name_idx).to_string();
14336            let mut s = String::from("(");
14337            for t in dex.proto_params(mid.proto_idx) {
14338                s.push_str(dex.type_name(*t));
14339            }
14340            s.push(')');
14341            s.push_str(dex.type_name(dex.proto(mid.proto_idx).return_type_idx));
14342            let desc = parse_method_descriptor(&s)?;
14343            Some(Shape::Forward { cls, name, desc, instance: !matches!(kind, InvokeKind::Static) })
14344        }
14345        _ => None,
14346    }
14347}
14348
14349
14350/// A const-only invoke-static (the d8 APM `i(732046)` / `o(732046)`
14351/// trace wrapper): every argument register is loaded by a Const (or a
14352/// Const-into-move chain) in the SAME body. Scoped to synthetic
14353/// accessors, so a genuinely side-effecting const call is never
14354/// mistaken for a trace.
14355fn is_const_only_trace(kind: &InsnKind, insns: &[&Insn]) -> bool {
14356    let InsnKind::Invoke { kind, regs, .. } = kind else { return false };
14357    if !matches!(kind, InvokeKind::Static) || regs.is_empty() {
14358        return false;
14359    }
14360    regs.iter().all(|r| const_loaded(*r, insns, 0))
14361}
14362
14363/// Const-loaded directly, or through a Move chain from a const-loaded
14364/// register (the APM `const v0, id` → `invoke {v0}` shape).
14365fn const_loaded(r: u16, insns: &[&Insn], depth: u8) -> bool {
14366    if depth > 3 {
14367        return false;
14368    }
14369    insns.iter().any(|i| match &i.kind {
14370        InsnKind::Const { dst, .. } => *dst == r,
14371        InsnKind::Move { dst, src } => *dst == r && const_loaded(*src, insns, depth + 1),
14372        _ => false,
14373    })
14374}