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/// Visit every expression (immutable).
164fn visit_exprs<F: FnMut(&Expr)>(e: &Expr, f: &mut F) {
165    f(e);
166    for_each_child(e, &mut |c| visit_exprs(c, f));
167}
168
169fn for_each_child<F: FnMut(&Expr)>(e: &Expr, f: &mut F) {
170    match e {
171        Expr::Un { e, .. } | Expr::Cast { e, .. } | Expr::InstanceOf { e, .. } => f(e),
172        Expr::Bin { l, r, .. } => {
173            f(l);
174            f(r);
175        }
176        Expr::Cond { c, t, f: fe } => {
177            f(c);
178            f(t);
179            f(fe);
180        }
181        Expr::Assign { target, value, .. } => {
182            f(target);
183            f(value);
184        }
185        Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => f(e),
186        Expr::Field { owner, .. } => {
187            if let Some(o) = owner {
188                f(o);
189            }
190        }
191        Expr::Method { owner, args, .. } => {
192            if let Some(o) = owner {
193                f(o);
194            }
195            for a in args {
196                f(a);
197            }
198        }
199        Expr::ArrayIndex { array, index } => {
200            f(array);
201            f(index);
202        }
203        Expr::New { args, .. } => {
204            for a in args {
205                f(a);
206            }
207        }
208        Expr::NewArray { dims, init, .. } => {
209            for d in dims {
210                f(d);
211            }
212            if let Some(v) = init {
213                for x in v {
214                    f(x);
215                }
216            }
217        }
218        Expr::NewMultiArray { dims, .. } => {
219            for d in dims {
220                f(d);
221            }
222        }
223        Expr::StringConcat(parts) => {
224            for p in parts {
225                if let ConcatPart::Str(x) = p {
226                    f(x);
227                }
228            }
229        }
230        Expr::Invokedynamic { args, .. } => {
231            for a in args {
232                f(a);
233            }
234        }
235        Expr::AnonNew { args, .. } => {
236            for a in args {
237                f(a);
238            }
239        }
240        _ => {}
241    }
242}
243
244/// Mutable child walk (one level).
245fn for_each_child_mut<F: FnMut(&mut Expr)>(e: &mut Expr, f: &mut F) {
246    match e {
247        Expr::Un { e, .. } | Expr::Cast { e, .. } | Expr::InstanceOf { e, .. } => f(e),
248        Expr::Bin { l, r, .. } => {
249            f(l);
250            f(r);
251        }
252        Expr::Cond { c, t, f: fe } => {
253            f(c);
254            f(t);
255            f(fe);
256        }
257        Expr::Assign { target, value, .. } => {
258            f(target);
259            f(value);
260        }
261        Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => f(e),
262        Expr::Field { owner, .. } => {
263            if let Some(o) = owner {
264                f(o);
265            }
266        }
267        Expr::Method { owner, args, .. } => {
268            if let Some(o) = owner {
269                f(o);
270            }
271            for a in args.iter_mut() {
272                f(a);
273            }
274        }
275        Expr::ArrayIndex { array, index } => {
276            f(array);
277            f(index);
278        }
279        Expr::New { args, .. } => {
280            for a in args.iter_mut() {
281                f(a);
282            }
283        }
284        Expr::NewArray { dims, init, .. } => {
285            for d in dims.iter_mut() {
286                f(d);
287            }
288            if let Some(v) = init {
289                for x in v.iter_mut() {
290                    f(x);
291                }
292            }
293        }
294        Expr::NewMultiArray { dims, .. } => {
295            for d in dims.iter_mut() {
296                f(d);
297            }
298        }
299        Expr::StringConcat(parts) => {
300            for p in parts {
301                if let ConcatPart::Str(x) = p {
302                    f(x);
303                }
304            }
305        }
306        Expr::Invokedynamic { args, .. } => {
307            for a in args.iter_mut() {
308                f(a);
309            }
310        }
311        Expr::AnonNew { args, .. } => {
312            for a in args.iter_mut() {
313                f(a);
314            }
315        }
316        _ => {}
317    }
318}
319
320/// Deep mutable expression rewrite.
321pub(crate) fn deep_rewrite<F: FnMut(&mut Expr)>(e: &mut Expr, f: &mut F) {
322    f(e);
323    for_each_child_mut(e, &mut |c| deep_rewrite(c, f));
324}
325
326/// `deep_rewrite` that respects WRITE positions: a `Local` appearing as
327/// an assignment target or a `++`/`--` operand never reaches `f`.
328/// Value-forwarding closures (`*x = value`) must use this — plain
329/// deep_rewrite turned a forwarded `vX = 25` into `25 = 25` (target
330/// replaced, and drop_defs then no longer recognized the statement, so
331/// the garbage survived into the output; reqable a4/e). Compound
332/// targets (`a[i] = …`, `o.f = …`) still recurse: owners and indices
333/// ARE reads.
334fn deep_rewrite_reads<F: FnMut(&mut Expr)>(e: &mut Expr, f: &mut F) {
335    f(e);
336    match e {
337        Expr::Assign { target, value, .. } => {
338            if !matches!(**target, Expr::Local { .. }) {
339                deep_rewrite_reads(target, f);
340            }
341            deep_rewrite_reads(value, f);
342        }
343        Expr::PreIncDec { e: inner, .. } | Expr::PostIncDec { e: inner, .. } => {
344            if !matches!(**inner, Expr::Local { .. }) {
345                deep_rewrite_reads(inner, f);
346            }
347        }
348        _ => for_each_child_mut(e, &mut |c| deep_rewrite_reads(c, f)),
349    }
350}
351
352fn collect_vars(e: &Expr, out: &mut HashSet<u32>) {
353    visit_exprs(e, &mut |x| {
354        if let Expr::Local { var, .. } = x {
355            out.insert(*var);
356        }
357    });
358}
359
360/// Collect variable references (reads; `assignments` adds assignment
361/// targets). Single-pass recursion — the previous shape wrapped a
362/// self-recursive closure in `walk_all`, visiting every subtree twice over
363/// (exponential in nesting depth).
364fn stmt_collect_vars(s: &Stmt, out: &mut HashSet<u32>, assignments: bool) {
365    match s {
366        Stmt::Block(v) => {
367            for x in v {
368                stmt_collect_vars(x, out, assignments);
369            }
370        }
371        Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
372            if let Expr::Local { var, .. } = &**target {
373                if assignments {
374                    out.insert(*var);
375                }
376            } else {
377                collect_vars(target, out);
378            }
379            collect_vars(value, out);
380        }
381        Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
382            collect_vars(e, out)
383        }
384        Stmt::Return(Some(e)) => collect_vars(e, out),
385        Stmt::LocalDef { var, init, .. } => {
386            if assignments {
387                out.insert(*var);
388            }
389            if let Some(e) = init {
390                collect_vars(e, out);
391            }
392        }
393        Stmt::If {
394            cond,
395            then_stmt,
396            else_stmt,
397        } => {
398            collect_vars(cond, out);
399            stmt_collect_vars(then_stmt, out, assignments);
400            if let Some(e) = else_stmt {
401                stmt_collect_vars(e, out, assignments);
402            }
403        }
404        Stmt::While { cond, body } => {
405            collect_vars(cond, out);
406            stmt_collect_vars(body, out, assignments);
407        }
408        Stmt::DoWhile { body, cond } => {
409            stmt_collect_vars(body, out, assignments);
410            collect_vars(cond, out);
411        }
412        Stmt::For {
413            init,
414            cond,
415            update,
416            body,
417        } => {
418            for x in init {
419                stmt_collect_vars(x, out, assignments);
420            }
421            if let Some(c) = cond {
422                collect_vars(c, out);
423            }
424            for u in update {
425                collect_vars(u, out);
426            }
427            stmt_collect_vars(body, out, assignments);
428        }
429        Stmt::ForEach {
430            var,
431            iterable,
432            body,
433            ..
434        } => {
435            if assignments {
436                out.insert(*var);
437            }
438            collect_vars(iterable, out);
439            stmt_collect_vars(body, out, assignments);
440        }
441        Stmt::Switch {
442            selector,
443            cases,
444            default,
445            ..
446        } => {
447            collect_vars(selector, out);
448            for c in cases {
449                for x in &c.body {
450                    stmt_collect_vars(x, out, assignments);
451                }
452            }
453            if let Some(d) = default {
454                stmt_collect_vars(d, out, assignments);
455            }
456        }
457        Stmt::Try {
458            body,
459            catches,
460            finally,
461        } => {
462            stmt_collect_vars(body, out, assignments);
463            for c in catches {
464                stmt_collect_vars(&c.body, out, assignments);
465            }
466            if let Some(f) = finally {
467                stmt_collect_vars(f, out, assignments);
468            }
469        }
470        Stmt::TryWithResources {
471            resources,
472            body,
473            catches,
474            finally,
475        } => {
476            for x in resources {
477                stmt_collect_vars(x, out, assignments);
478            }
479            stmt_collect_vars(body, out, assignments);
480            for c in catches {
481                stmt_collect_vars(&c.body, out, assignments);
482            }
483            if let Some(f) = finally {
484                stmt_collect_vars(f, out, assignments);
485            }
486        }
487        Stmt::Synchronized { lock, body } => {
488            collect_vars(lock, out);
489            stmt_collect_vars(body, out, assignments);
490        }
491        Stmt::Assert { cond, msg } => {
492            collect_vars(cond, out);
493            if let Some(m) = msg {
494                collect_vars(m, out);
495            }
496        }
497        Stmt::Labeled { body, .. } => stmt_collect_vars(body, out, assignments),
498        _ => {}
499    }
500}
501
502fn walk_all<F: FnMut(&Stmt)>(s: &Stmt, f: &mut F) {
503    f(s);
504    match s {
505        Stmt::Block(v) => {
506            for x in v {
507                walk_all(x, f);
508            }
509        }
510        Stmt::If {
511            then_stmt,
512            else_stmt,
513            ..
514        } => {
515            walk_all(then_stmt, f);
516            if let Some(e) = else_stmt {
517                walk_all(e, f);
518            }
519        }
520        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => walk_all(body, f),
521        Stmt::For { init, body, .. } => {
522            for x in init {
523                walk_all(x, f);
524            }
525            walk_all(body, f);
526        }
527        Stmt::ForEach { body, .. } => walk_all(body, f),
528        Stmt::Switch { cases, default, .. } => {
529            for c in cases {
530                for x in &c.body {
531                    walk_all(x, f);
532                }
533            }
534            if let Some(d) = default {
535                walk_all(d, f);
536            }
537        }
538        Stmt::Try {
539            body,
540            catches,
541            finally,
542        } => {
543            walk_all(body, f);
544            for c in catches {
545                walk_all(&c.body, f);
546            }
547            if let Some(fl) = finally {
548                walk_all(fl, f);
549            }
550        }
551        Stmt::TryWithResources {
552            resources,
553            body,
554            catches,
555            finally,
556        } => {
557            for x in resources {
558                walk_all(x, f);
559            }
560            walk_all(body, f);
561            for c in catches {
562                walk_all(&c.body, f);
563            }
564            if let Some(fl) = finally {
565                walk_all(fl, f);
566            }
567        }
568        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => walk_all(body, f),
569        _ => {}
570    }
571}
572
573// ---------------------------------------------------------------------------
574// Passes
575// ---------------------------------------------------------------------------
576
577/// Bind catch parameters: the handler's first statement (a bare LocalDef
578/// from move-exception) becomes the catch variable.
579pub fn bind_catches(s: &mut Stmt, vt: &mut VarTable) {
580    match s {
581        Stmt::Try {
582            body,
583            catches,
584            finally,
585        } => {
586            bind_catches(body, vt);
587            for c in catches.iter_mut() {
588                if c.var == u32::MAX {
589                    let stored = match c.body.as_ref() {
590                        Stmt::Block(v) => match v.first() {
591                            Some(Stmt::LocalDef { var, .. }) => Some(*var),
592                            Some(Stmt::ExprStmt(Expr::Assign { target, .. })) => match &**target {
593                                Expr::Local { var, .. } => Some(*var),
594                                _ => None,
595                            },
596                            _ => None,
597                        },
598                        _ => None,
599                    };
600                    if let Some(v) = stored {
601                        let exc = c
602                            .exc
603                            .first()
604                            .cloned()
605                            .unwrap_or_else(|| "java/lang/Throwable".into());
606                        let slot = vt.var(v).slot;
607                        let name = "e".to_string();
608                        let new_var =
609                            vt.add_catch_var(slot, name, TypeRef::J(JavaType::Object(exc)));
610                        if let Stmt::Block(vs) = c.body.as_mut() {
611                            vs.remove(0);
612                        }
613                        rewrite_local_refs(c.body.as_mut(), v, new_var);
614                        c.var = new_var;
615                    }
616                }
617                bind_catches(&mut c.body, vt);
618            }
619            if let Some(f) = finally {
620                bind_catches(f.as_mut(), vt);
621            }
622        }
623        Stmt::Block(v) => {
624            for x in v.iter_mut() {
625                bind_catches(x, vt);
626            }
627        }
628        Stmt::If {
629            then_stmt,
630            else_stmt,
631            ..
632        } => {
633            bind_catches(then_stmt, vt);
634            if let Some(e) = else_stmt {
635                bind_catches(e, vt);
636            }
637        }
638        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => bind_catches(body, vt),
639        Stmt::For { init, body, .. } => {
640            for x in init.iter_mut() {
641                bind_catches(x, vt);
642            }
643            bind_catches(body, vt);
644        }
645        Stmt::ForEach { body, .. }
646        | Stmt::Labeled { body, .. }
647        | Stmt::Synchronized { body, .. } => bind_catches(body, vt),
648        Stmt::Switch { cases, default, .. } => {
649            for c in cases.iter_mut() {
650                for x in c.body.iter_mut() {
651                    bind_catches(x, vt);
652                }
653            }
654            if let Some(d) = default {
655                bind_catches(d, vt);
656            }
657        }
658        _ => {}
659    }
660}
661
662fn rewrite_local_refs(s: &mut Stmt, from: u32, to: u32) {
663    rewrite_exprs(s, &mut |e| {
664        deep_rewrite(e, &mut |x| {
665            if let Expr::Local { var, .. } = x {
666                if *var == from {
667                    *var = to;
668                }
669            }
670        });
671    });
672}
673
674/// Flatten blocks, drop empty ones and trailing no-op statements.
675pub fn cleanup(s: &mut Stmt) {
676    jdc_core::ir::stmt::flatten(s);
677    strip_empty(s);
678    merge_singleton_stmts(s);
679}
680
681fn strip_empty(s: &mut Stmt) {
682    match s {
683        Stmt::Block(v) => {
684            v.retain(|x| !x.is_empty_block());
685            for x in v.iter_mut() {
686                strip_empty(x);
687            }
688        }
689        Stmt::If {
690            then_stmt,
691            else_stmt,
692            ..
693        } => {
694            strip_empty(then_stmt);
695            if let Some(e) = else_stmt {
696                strip_empty(e);
697                if e.is_empty_block() {
698                    *else_stmt = None;
699                }
700            }
701        }
702        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => strip_empty(body),
703        Stmt::For { init, body, .. } => {
704            init.retain(|x| !x.is_empty_block());
705            strip_empty(body);
706        }
707        Stmt::ForEach { body, .. } => strip_empty(body),
708        Stmt::Switch { cases, default, .. } => {
709            for c in cases {
710                c.body.retain(|x| !x.is_empty_block());
711            }
712            if let Some(d) = default {
713                strip_empty(d);
714            }
715        }
716        Stmt::Try {
717            body,
718            catches,
719            finally,
720        } => {
721            strip_empty(body);
722            for c in catches {
723                strip_empty(&mut c.body);
724            }
725            if let Some(f) = finally {
726                strip_empty(f);
727            }
728        }
729        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => strip_empty(body),
730        _ => {}
731    }
732}
733
734fn merge_singleton_stmts(s: &mut Stmt) {
735    if let Stmt::Block(v) = s {
736        for x in v.iter_mut() {
737            merge_singleton_stmts(x);
738        }
739    }
740}
741
742/// Drop statements after a definite terminator within a block.
743pub fn prune_unreachable(s: &mut Stmt) {
744    if let Stmt::Block(v) = s {
745        let mut cut: Option<usize> = None;
746        for (i, x) in v.iter().enumerate() {
747            if matches!(x, Stmt::Return(_) | Stmt::Throw(_)) {
748                cut = Some(i + 1);
749                break;
750            }
751        }
752        if let Some(c) = cut {
753            if c < v.len() {
754                v.truncate(c);
755            }
756        }
757        for x in v.iter_mut() {
758            prune_unreachable(x);
759        }
760    } else {
761        walk_mut(s, &mut prune_unreachable);
762    }
763}
764
765fn walk_mut<F: FnMut(&mut Stmt)>(s: &mut Stmt, f: &mut F) {
766    match s {
767        Stmt::Block(v) => {
768            for x in v.iter_mut() {
769                f(x);
770            }
771        }
772        Stmt::If {
773            then_stmt,
774            else_stmt,
775            ..
776        } => {
777            f(then_stmt);
778            if let Some(e) = else_stmt {
779                f(e);
780            }
781        }
782        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => f(body),
783        Stmt::For { init, body, .. } => {
784            for x in init.iter_mut() {
785                f(x);
786            }
787            f(body);
788        }
789        Stmt::ForEach { body, .. } => f(body),
790        Stmt::Switch { cases, default, .. } => {
791            for c in cases {
792                for x in c.body.iter_mut() {
793                    f(x);
794                }
795            }
796            if let Some(d) = default {
797                f(d);
798            }
799        }
800        Stmt::Try {
801            body,
802            catches,
803            finally,
804        } => {
805            f(body);
806            for c in catches {
807                f(&mut c.body);
808            }
809            if let Some(fl) = finally {
810                f(fl);
811            }
812        }
813        Stmt::TryWithResources {
814            resources,
815            body,
816            catches,
817            finally,
818        } => {
819            for r in resources.iter_mut() {
820                f(r);
821            }
822            f(body);
823            for c in catches {
824                f(&mut c.body);
825            }
826            if let Some(fl) = finally {
827                f(fl);
828            }
829        }
830        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => f(body),
831        _ => {}
832    }
833}
834
835/// Count LocalDef statements (pipeline diagnostics).
836pub fn count_localdefs(s: &Stmt, out: &mut usize) {
837    if matches!(s, Stmt::LocalDef { .. }) {
838        *out += 1;
839    }
840    walk_all(s, &mut |x| {
841        if matches!(x, Stmt::LocalDef { .. }) {
842            *out += 1;
843        }
844    });
845}
846
847/// Drop a trailing `return;` (d8's explicit return-void at method end).
848pub fn strip_trailing_void_return(s: &mut Stmt) {
849    if let Stmt::Block(v) = s {
850        loop {
851            match v.last_mut() {
852                Some(Stmt::Return(None)) => {
853                    v.pop();
854                }
855                // The structurer can close a region in a bare nested
856                // block; a `return;` at its tail is still a dangling
857                // statement (inside a static initializer it is illegal
858                // outright — clinit's closing return).
859                Some(Stmt::Block(_)) => {
860                    let last = v.last_mut().unwrap();
861                    let before = match last {
862                        Stmt::Block(b) => b.len(),
863                        _ => 0,
864                    };
865                    strip_trailing_void_return(last);
866                    let after = match last {
867                        Stmt::Block(b) => b.len(),
868                        _ => 0,
869                    };
870                    if after == before {
871                        break; // nothing stripped inside
872                    }
873                }
874                _ => break,
875            }
876        }
877    }
878}
879
880/// `if (c) { } else { B }` → `if (!c) { B }` — the empty-then shape is
881/// how the structurer lands an inverted diamond, and source never writes
882/// it (jadx prints the positive form). negate() carries De Morgan so
883/// composed conditions stay clean.
884pub fn invert_empty_thens(s: &mut Stmt) {
885    walk_mut_deep(s, &mut |st| {
886        if let Stmt::If {
887            cond,
888            then_stmt,
889            else_stmt,
890        } = st
891        {
892            let then_empty = matches!(&**then_stmt, Stmt::Block(v) if v.is_empty());
893            if then_empty {
894                if let Some(els) = else_stmt.take() {
895                    let c = std::mem::replace(cond, Expr::Const(ConstVal::Int(0)));
896                    *cond = jdc_core::convert::negate(c);
897                    *then_stmt = els;
898                }
899            }
900        }
901    });
902}
903
904/// Fold if-diamonds back into short-circuit conditions (DAD's
905/// short_circuit_struct, statement level). The structurer emits nested
906/// ifs for `a && b` / `a || b` bytecode diamonds; javac source almost
907/// never nests them, and jadx folds all four shapes:
908///   if (c1) { if (c2) {T} else {F} } else {F}  →  if (c1 && c2) {T} else {F}
909///   if (c1) { if (c2) {F} else {T} } else {F}  →  if (c1 && !c2) {T} else {F}
910///   if (c1) {T} else { if (c2) {T} else {F} }  →  if (c1 || c2) {T} else {F}
911///   if (c1) {T} else { if (c2) {F} else {T} }  →  if (c1 || !c2) {T} else {F}
912/// Branch identity is deep structural equality (Stmt: PartialEq);
913/// iterate to a fixpoint so chained diamonds `(a && b) && c` collapse.
914pub fn fold_short_circuits(s: &mut Stmt) {
915    for _ in 0..8 {
916        let mut changed = false;
917        walk_mut_deep(s, &mut |st| {
918            if try_fold_diamond(st) {
919                changed = true;
920            }
921        });
922        if !changed {
923            break;
924        }
925    }
926}
927
928fn try_fold_diamond(st: &mut Stmt) -> bool {
929    enum Shape {
930        ThenAnd,
931        ThenAndNot,
932        ElseOr,
933        ElseOrNot,
934        BareAnd,
935        BareAndNot,
936    }
937    // Probe the shape on an immutable borrow first (the fold takes the
938    // whole If apart; matching and rebuilding inside one borrow is not
939    // expressible).
940    let shape = match &*st {
941        // One-sided diamonds: the shared branch is the implicit empty
942        // fall-through (`if (c1) { if (c2) {T} }` → `if (c1 && c2) {T}`).
943        Stmt::If {
944            then_stmt,
945            else_stmt: None,
946            ..
947        } => match &**then_stmt {
948            Stmt::If {
949                then_stmt: t2,
950                else_stmt: None,
951                ..
952            } if !matches!(&**t2, Stmt::Block(v) if v.is_empty()) => Some(Shape::BareAnd),
953            Stmt::If {
954                then_stmt: t2,
955                else_stmt: Some(_),
956                ..
957            } if matches!(&**t2, Stmt::Block(v) if v.is_empty()) => Some(Shape::BareAndNot),
958            _ => None,
959        },
960        Stmt::If {
961            then_stmt,
962            else_stmt: Some(els),
963            ..
964        } => match (&**then_stmt, &**els) {
965            (Stmt::If { else_stmt: Some(f2), .. }, _) if f2.as_ref() == els.as_ref() => {
966                Some(Shape::ThenAnd)
967            }
968            (Stmt::If { then_stmt: t2, else_stmt: Some(_), .. }, _)
969                if t2.as_ref() == els.as_ref() =>
970            {
971                Some(Shape::ThenAndNot)
972            }
973            (_, Stmt::If { then_stmt: t2, else_stmt: Some(_), .. })
974                if t2.as_ref() == then_stmt.as_ref() =>
975            {
976                Some(Shape::ElseOr)
977            }
978            (_, Stmt::If { else_stmt: Some(f2), .. })
979                if f2.as_ref() == then_stmt.as_ref() =>
980            {
981                Some(Shape::ElseOrNot)
982            }
983            _ => None,
984        },
985        _ => None,
986    };
987    let Some(shape) = shape else {
988        return false;
989    };
990
991    let taken = std::mem::replace(st, Stmt::Block(vec![]));
992    // Bare (no-else) diamonds fold without an else at all.
993    if matches!(shape, Shape::BareAnd | Shape::BareAndNot) {
994        let Stmt::If {
995            cond: c1,
996            then_stmt,
997            else_stmt: None,
998        } = taken
999        else {
1000            unreachable!()
1001        };
1002        let Stmt::If {
1003            cond: c2,
1004            then_stmt: t2,
1005            else_stmt: f2,
1006        } = *then_stmt
1007        else {
1008            unreachable!()
1009        };
1010        let sc = |op: BinOp, l: Expr, r: Expr| Expr::Bin {
1011            op,
1012            l: Box::new(l),
1013            r: Box::new(r),
1014            ty: None,
1015        };
1016        match shape {
1017            // if (c1 && c2) {T}
1018            Shape::BareAnd => {
1019                *st = Stmt::If {
1020                    cond: sc(BinOp::LogAnd, c1, c2),
1021                    then_stmt: t2,
1022                    else_stmt: None,
1023                };
1024            }
1025            // if (c1) { if (c2) {} else {T} }  →  if (c1 && !c2) {T}
1026            _ => {
1027                let body = f2.unwrap();
1028                *st = Stmt::If {
1029                    cond: sc(
1030                        BinOp::LogAnd,
1031                        c1,
1032                        Expr::Un {
1033                            op: UnOp::Not,
1034                            e: Box::new(c2),
1035                        },
1036                    ),
1037                    then_stmt: body,
1038                    else_stmt: None,
1039                };
1040            }
1041        }
1042        return true;
1043    }
1044    let Stmt::If {
1045        cond: c1,
1046        then_stmt,
1047        else_stmt: Some(els),
1048    } = taken
1049    else {
1050        unreachable!("shape probe guaranteed an If with else")
1051    };
1052    let sc = |op: BinOp, l: Expr, r: Expr| Expr::Bin {
1053        op,
1054        l: Box::new(l),
1055        r: Box::new(r),
1056        ty: None,
1057    };
1058    let not = |e: Expr| Expr::Un {
1059        op: UnOp::Not,
1060        e: Box::new(e),
1061    };
1062    match shape {
1063        Shape::BareAnd | Shape::BareAndNot => unreachable!("handled above"),
1064        Shape::ThenAnd | Shape::ThenAndNot => {
1065            let Stmt::If {
1066                cond: c2,
1067                then_stmt: t2,
1068                else_stmt: Some(f2),
1069            } = *then_stmt
1070            else {
1071                unreachable!()
1072            };
1073            let (nc, body, alt) = match shape {
1074                // if (c1 && c2) {T} else {F}
1075                Shape::ThenAnd => (sc(BinOp::LogAnd, c1, c2), t2, f2),
1076                // if (c1 && !c2) {T} else {F}   (inner: {F} else {T})
1077                _ => (sc(BinOp::LogAnd, c1, not(c2)), f2, t2),
1078            };
1079            *st = Stmt::If {
1080                cond: nc,
1081                then_stmt: body,
1082                else_stmt: Some(alt),
1083            };
1084        }
1085        Shape::ElseOr | Shape::ElseOrNot => {
1086            let Stmt::If {
1087                cond: c2,
1088                then_stmt: t2,
1089                else_stmt: Some(f2),
1090            } = *els
1091            else {
1092                unreachable!()
1093            };
1094            // The surviving then-body is the OUTER then (identical to the
1095            // matching inner branch by the probe).
1096            let body = then_stmt;
1097            let (nc, alt) = match shape {
1098                // if (c1 || c2) {T} else {F}   (inner: {T} else {F})
1099                Shape::ElseOr => (sc(BinOp::LogOr, c1, c2), f2),
1100                // if (c1 || !c2) {T} else {F}  (inner: {F} else {T})
1101                _ => (sc(BinOp::LogOr, c1, not(c2)), t2),
1102            };
1103            *st = Stmt::If {
1104                cond: nc,
1105                then_stmt: body,
1106                else_stmt: Some(alt),
1107            };
1108        }
1109    }
1110    true
1111}
1112
1113/// A static initializer cannot contain `return` in source form — the
1114/// clinit's closing return is a method-level artifact, but the
1115/// structurer can leave it nested inside loops/branches where the
1116/// top-level strip cannot see it (x2/a: `static { while { ...; return;
1117/// } }` — "return outside method", 184 sites on reqable).
1118pub fn strip_clinit_returns(s: &mut Stmt) {
1119    walk_mut_deep(s, &mut |st| {
1120        if let Stmt::Block(v) = st {
1121            v.retain(|x| !matches!(x, Stmt::Return(None)));
1122        }
1123    });
1124}
1125
1126pub fn prepend_comment(s: &mut Stmt, text: String) {
1127    let stmt = Stmt::Comment(text);
1128    match s {
1129        Stmt::Block(v) => v.insert(0, stmt),
1130        other => {
1131            let inner = std::mem::replace(other, Stmt::Block(vec![]));
1132            *other = Stmt::Block(vec![stmt, inner]);
1133        }
1134    }
1135}
1136
1137/// Fused expression rewrites that each used to walk the whole tree:
1138/// residual cmp sentinels, object-null comparisons, const-first compares.
1139/// One traversal, three rules — the separate walks were a top profile cost.
1140/// True when `e` evaluates to an object reference — a legal null-compare
1141/// side: locals via the reference table, everything else via its own type
1142/// (fields like `pi.versionName != 0` and call results are references too;
1143/// constants never are).
1144fn is_obj_expr(e: &Expr, obj_var: &dyn Fn(u32) -> bool) -> bool {
1145    match e {
1146        Expr::Local { var, .. } => obj_var(*var),
1147        Expr::Const(_) => false,
1148        _ => e.type_ref().erased().is_reference(),
1149    }
1150}
1151
1152/// `obj == 0` / `0 == obj` → `obj == null`: replace the CONST-0 side.
1153/// The previous shape replaced the LOCAL side instead — every object
1154/// null-check in the corpus rendered as `null != 0` (4,598 hits in
1155/// reqable alone) and the real operand vanished from the condition.
1156fn null_side_rewrite(x: &mut Expr, obj_var: &dyn Fn(u32) -> bool) {
1157    if let Expr::Bin { op, l, r, .. } = x {
1158        if !matches!(op, BinOp::Eq | BinOp::Ne) {
1159            return;
1160        }
1161        let l_zero = matches!(&**l, Expr::Const(ConstVal::Int(0)));
1162        let r_zero = matches!(&**r, Expr::Const(ConstVal::Int(0)));
1163        if l_zero && !r_zero && is_obj_expr(r, obj_var) {
1164            **l = Expr::Const(ConstVal::Null);
1165        } else if r_zero && !l_zero && is_obj_expr(l, obj_var) {
1166            **r = Expr::Const(ConstVal::Null);
1167        }
1168    }
1169}
1170
1171pub fn fused_expr_rewrites(s: &mut Stmt, vt: &VarTable) {
1172    // var ids are dense: a byte table beats a HashSet lookup.
1173    let mut obj_vars: Vec<bool> = Vec::with_capacity(vt.vars.len());
1174    for v in &vt.vars {
1175        obj_vars.push(v.ty.erased().is_reference());
1176    }
1177    rewrite_exprs(s, &mut |e| {
1178        deep_rewrite(e, &mut |x| {
1179            // 1. cmp sentinels.
1180            if let Expr::Invokedynamic { name, args, .. } = x {
1181                if name.starts_with('\0') && args.len() == 2 {
1182                    let (cls, ty) = match name.as_str() {
1183                        "\0cmp-long" => ("java/lang/Long", JavaType::Long),
1184                        "\0cmpl-float" | "\0cmpg-float" => ("java/lang/Float", JavaType::Float),
1185                        _ => ("java/lang/Double", JavaType::Double),
1186                    };
1187                    let desc = MethodDescriptor {
1188                        args: vec![ty.clone(), ty],
1189                        ret: JavaType::Int,
1190                    };
1191                    *x = Expr::Method {
1192                        owner: None,
1193                        cls: cls.into(),
1194                        name: "compare".into(),
1195                        desc: std::sync::Arc::new(desc),
1196                        args: args.clone(),
1197                        is_static: true,
1198                        is_interface: false,
1199                        is_special: false,
1200                        is_super: false,
1201                        is_dynamic: false,
1202                        type_args: vec![],
1203                    };
1204                    return;
1205                }
1206            }
1207            // 2. null compares (no any_obj gate: the object side may be
1208            // a field/call whose type is not in the local table).
1209            null_side_rewrite(x, &|v| obj_vars.get(v as usize).copied().unwrap_or(false));
1210            // 3. const-first compares.
1211            if let Expr::Bin { op, l, r, .. } = x {
1212                if matches!(
1213                    op,
1214                    BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
1215                ) {
1216                    let l_const = matches!(&**l, Expr::Const(_));
1217                    let r_var = matches!(&**r, Expr::Local { .. });
1218                    if l_const && r_var {
1219                        std::mem::swap(l, r);
1220                    }
1221                }
1222            }
1223        });
1224    });
1225    // 4. null into reference-typed assignment targets. The register
1226    //    machine stores null as const-0; return/throw/field-store/invoke
1227    //    argument paths were normalized in the lifter, but a null that
1228    //    MATERIALIZES into a local was rendered as `str = 0;` — javac
1229    //    "int cannot be converted to String" (the single biggest error
1230    //    family on real corpora: d8 emits null locals this way after
1231    //    every `x = null` branch).
1232    walk_mut_deep(s, &mut |st| {
1233        if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = st {
1234            let tgt_is_obj = match &**target {
1235                Expr::Local { var, .. } => {
1236                    obj_vars.get(*var as usize).copied().unwrap_or(false)
1237                }
1238                other => other.type_ref().erased().is_reference(),
1239            };
1240            if tgt_is_obj {
1241                if let Expr::Const(ConstVal::Int(0)) = &**value {
1242                    **value = Expr::Const(ConstVal::Null);
1243                }
1244            }
1245        } else if let Stmt::LocalDef { var, init, .. } = st {
1246            if obj_vars.get(*var as usize).copied().unwrap_or(false) {
1247                if let Some(iv) = init {
1248                    if let Expr::Const(ConstVal::Int(0)) = iv {
1249                        *iv = Expr::Const(ConstVal::Null);
1250                    }
1251                }
1252            }
1253        }
1254    });
1255}
1256
1257/// Replace residual cmp sentinels (`\0cmp*` Invokedynamic markers that never
1258/// reached a condition) with library compare calls.
1259#[allow(dead_code)]
1260pub fn desugar_cmp_residuals(s: &mut Stmt) {
1261    rewrite_exprs(s, &mut |e| {
1262        deep_rewrite(e, &mut |x| {
1263            if let Expr::Invokedynamic { name, args, .. } = x {
1264                if name.starts_with('\0') && args.len() == 2 {
1265                    let (cls, ty) = match name.as_str() {
1266                        "\0cmp-long" => ("java/lang/Long", JavaType::Long),
1267                        "\0cmpl-float" | "\0cmpg-float" => ("java/lang/Float", JavaType::Float),
1268                        _ => ("java/lang/Double", JavaType::Double),
1269                    };
1270                    let desc = jdc_core::types::MethodDescriptor {
1271                        args: vec![ty.clone(), ty],
1272                        ret: JavaType::Int,
1273                    };
1274                    *x = Expr::Method {
1275                        owner: None,
1276                        cls: cls.into(),
1277                        name: "compare".into(),
1278                        desc: std::sync::Arc::new(desc),
1279                        args: args.clone(),
1280                        is_static: true,
1281                        is_interface: false,
1282                        is_special: false,
1283                        is_super: false,
1284                        is_dynamic: false,
1285                        type_args: vec![],
1286                    };
1287                }
1288            }
1289        });
1290    });
1291}
1292
1293/// `if (c) { x = A; } else { x = B; }` → `x = c ? A : B;`
1294pub fn ternary_fold(s: &mut Stmt) {
1295    let mut changed = true;
1296    let mut guard = 0;
1297    while changed && guard < 8 {
1298        changed = false;
1299        guard += 1;
1300        ternary_fold_walk(s, &mut changed);
1301        strip_empty(s);
1302    }
1303}
1304
1305fn ternary_fold_walk(s: &mut Stmt, changed: &mut bool) {
1306    if let Stmt::Block(v) = s {
1307        for x in v.iter_mut() {
1308            ternary_fold_walk(x, changed);
1309        }
1310        return;
1311    }
1312    let Stmt::If {
1313        cond,
1314        then_stmt,
1315        else_stmt,
1316    } = s
1317    else {
1318        return;
1319    };
1320    ternary_fold_walk(then_stmt, changed);
1321    if let Some(e) = else_stmt {
1322        ternary_fold_walk(e, changed);
1323    }
1324    let Some(else_b) = else_stmt else { return };
1325    let target_of = |st: &Stmt| -> Option<u32> {
1326        let Stmt::ExprStmt(Expr::Assign { target, .. }) = st else {
1327            return None;
1328        };
1329        if let Expr::Local { var, .. } = &**target {
1330            Some(*var)
1331        } else {
1332            None
1333        }
1334    };
1335    let (Stmt::Block(tv), Stmt::Block(ev)) = (then_stmt.as_mut(), else_b.as_mut()) else {
1336        return;
1337    };
1338    if tv.len() != 1 || ev.len() != 1 {
1339        return;
1340    }
1341    let (Some(vt_), Some(ve)) = (target_of(&tv[0]), target_of(&ev[0])) else {
1342        return;
1343    };
1344    if vt_ != ve {
1345        return;
1346    }
1347    // The condition and both values must not re-assign the target between
1348    // the fold — they are single statements, so a mention is a re-read at
1349    // worst; re-assignment only happens via assignments inside them.
1350    let mut touched = HashSet::default();
1351    let (Stmt::ExprStmt(a_then), Stmt::ExprStmt(a_else)) = (&tv[0], &ev[0]) else {
1352        return;
1353    };
1354    if let Expr::Assign { value, .. } = a_then {
1355        collect_vars(value, &mut touched);
1356    }
1357    if let Expr::Assign { value, .. } = a_else {
1358        collect_vars(value, &mut touched);
1359    }
1360    if touched.contains(&vt_) {
1361        return;
1362    }
1363    let value_then = match a_then {
1364        Expr::Assign { value, .. } => value.clone(),
1365        _ => return,
1366    };
1367    let value_else = match a_else {
1368        Expr::Assign { value, .. } => value.clone(),
1369        _ => return,
1370    };
1371    let cond_e = cond.clone();
1372    let folded = Stmt::ExprStmt(Expr::Assign {
1373        target: Box::new(Expr::Local {
1374            var: vt_,
1375            ty: vt_join_ty(&value_then, &value_else),
1376        }),
1377        op: AssignOp::Plain,
1378        value: Box::new(Expr::Cond {
1379            c: Box::new(cond_e),
1380            t: value_then,
1381            f: value_else,
1382        }),
1383    });
1384    *s = Stmt::Block(vec![folded]);
1385    *changed = true;
1386}
1387
1388fn vt_join_ty(a: &Expr, b: &Expr) -> TypeRef {
1389    let ta = a.type_ref();
1390    let tb = b.type_ref();
1391    if ta.erased() == tb.erased() {
1392        return ta;
1393    }
1394    TypeRef::J(JavaType::Object("java/lang/Object".into()))
1395}
1396
1397/// Fold `new StringBuilder(...).append(x)...toString()` chains into
1398/// `StringConcat`. Single-assignment temporaries carry the chain.
1399///
1400/// d8 drops append results (statement-form calls), so a builder whose chain
1401/// has STATEMENT appends attached is left alone: folding its `toString`
1402/// with only the ctor arguments would misstate the contents.
1403pub fn fold_string_builders(s: &mut Stmt, vt: &VarTable) {
1404    // var → assigned value, for vars assigned exactly once.
1405    let n_vars = vt.vars.len().max(1);
1406    let mut counts = vec![0usize; n_vars];
1407    count_assignments(s, &mut counts);
1408    let mut values: Vec<Option<Expr>> = vec![None; n_vars];
1409    record_assignments(s, &counts, &mut values);
1410
1411    // Builders with statement-form appends (their chains are incomplete).
1412    let mut appended_stmts: HashSet<u32> = HashSet::default();
1413    walk_all(s, &mut |st| {
1414        if let Stmt::ExprStmt(Expr::Method {
1415            cls,
1416            name,
1417            owner,
1418            args,
1419            ..
1420        }) = st
1421        {
1422            if name.as_ref() == "append" && is_string_builder(cls) && args.len() == 1 {
1423                if let Some(Expr::Local { var, .. }) = owner.as_deref() {
1424                    appended_stmts.insert(*var);
1425                }
1426            }
1427        }
1428    });
1429
1430    rewrite_exprs(s, &mut |e| {
1431        fold_concat_in_expr(e, &values, &appended_stmts);
1432    });
1433
1434    // Drop now-unused builder temporaries (their only consumers were
1435    // folded); the drop COUNT drives iteration — no statement counting
1436    // walks at all.
1437    for _ in 0..8 {
1438        let mut reads: HashSet<u32> = HashSet::default();
1439        stmt_collect_vars(s, &mut reads, false);
1440        if drop_unused_assigns(s, &reads) == 0 {
1441            break;
1442        }
1443    }
1444}
1445
1446/// Deep statement count (pipeline guard input).
1447pub fn count_stmts_deep(s: &Stmt, out: &mut usize) {
1448    *out += 1;
1449    walk_all(s, &mut |_| {
1450        *out += 1;
1451    });
1452}
1453
1454/// Fused per-variable analysis gathered in one statement-tree walk.
1455struct VarAnalysis {
1456    assigns: Vec<usize>,
1457    reads: Vec<usize>,
1458    /// Value of the (single) assignment, when recorded.
1459    values: Vec<Option<Expr>>,
1460}
1461
1462fn analyze_vars(s: &Stmt, a: &mut VarAnalysis) {
1463    walk_all(s, &mut |st| {
1464        let exprs: Vec<&Expr> = match st {
1465            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
1466                if let Expr::Local { var, .. } = &**target {
1467                    grow_to(&mut a.assigns, *var);
1468                    let idx = *var as usize;
1469                    a.assigns[idx] += 1;
1470                    if a.assigns[idx] == 1 {
1471                        if idx >= a.values.len() {
1472                            a.values.resize(idx + 1, None);
1473                        }
1474                        a.values[idx] = Some((**value).clone());
1475                    } else if idx < a.values.len() {
1476                        a.values[idx] = None;
1477                    }
1478                }
1479                let mut v: Vec<&Expr> = vec![value];
1480                if !matches!(&**target, Expr::Local { .. }) {
1481                    v.push(target);
1482                }
1483                v
1484            }
1485            Stmt::ExprStmt(e @ (Expr::PreIncDec { .. } | Expr::PostIncDec { .. })) => {
1486                // `v++` is a read-modify-WRITE of v: counting it as a
1487                // plain read makes v a single-assign/single-read forward
1488                // candidate, and value-forwarding then fabricates `5++`
1489                // (or drops v's def and leaves ++ on an undefined name).
1490                let inner = match e {
1491                    Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => e,
1492                    _ => unreachable!(),
1493                };
1494                if let Expr::Local { var, .. } = &**inner {
1495                    grow_to(&mut a.assigns, *var);
1496                    let idx = *var as usize;
1497                    a.assigns[idx] += 1;
1498                    if idx < a.values.len() {
1499                        a.values[idx] = None;
1500                    }
1501                    return;
1502                }
1503                vec![e]
1504            }
1505            Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
1506                vec![e]
1507            }
1508            Stmt::Return(Some(e)) => vec![e],
1509            Stmt::LocalDef {
1510                var, init: Some(e), ..
1511            } => {
1512                grow_to(&mut a.assigns, *var);
1513                let idx = *var as usize;
1514                a.assigns[idx] += 1;
1515                if a.assigns[idx] == 1 {
1516                    if idx >= a.values.len() {
1517                        a.values.resize(idx + 1, None);
1518                    }
1519                    a.values[idx] = Some(e.clone());
1520                } else if idx < a.values.len() {
1521                    a.values[idx] = None;
1522                }
1523                vec![e]
1524            }
1525            Stmt::If { cond, .. } => vec![cond],
1526            Stmt::While { cond, .. } => vec![cond],
1527            Stmt::DoWhile { cond, .. } => vec![cond],
1528            _ => Vec::new(),
1529        };
1530        for e in exprs {
1531            visit_exprs(e, &mut |x| {
1532                if let Expr::Local { var, .. } = x {
1533                    grow_to(&mut a.reads, *var);
1534                    a.reads[*var as usize] += 1;
1535                }
1536            });
1537        }
1538    });
1539    // Keep the three views length-aligned.
1540    let n = a.assigns.len().max(a.reads.len()).max(a.values.len());
1541    a.assigns.resize(n, 0);
1542    a.reads.resize(n, 0);
1543    a.values.resize(n, None);
1544}
1545
1546fn grow_to(v: &mut Vec<usize>, var: u32) {
1547    if var as usize >= v.len() {
1548        v.resize(var as usize + 1, 0);
1549    }
1550}
1551
1552fn count_assignments(s: &Stmt, counts: &mut Vec<usize>) {
1553    walk_all(s, &mut |st| match st {
1554        Stmt::ExprStmt(Expr::Assign { target, .. }) => {
1555            if let Expr::Local { var, .. } = &**target {
1556                grow_to(counts, *var);
1557                counts[*var as usize] += 1;
1558            }
1559        }
1560        Stmt::LocalDef {
1561            var, init: Some(_), ..
1562        } => {
1563            grow_to(counts, *var);
1564            counts[*var as usize] += 1;
1565        }
1566        _ => {}
1567    });
1568}
1569
1570fn record_assignments(s: &Stmt, counts: &[usize], out: &mut Vec<Option<Expr>>) {
1571    walk_all(s, &mut |st| match st {
1572        Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
1573            if let Expr::Local { var, .. } = &**target {
1574                if counts.get(*var as usize).copied().unwrap_or(0) == 1 {
1575                    if *var as usize >= out.len() {
1576                        out.resize(*var as usize + 1, None);
1577                    }
1578                    out[*var as usize] = Some((**value).clone());
1579                }
1580            }
1581        }
1582        Stmt::LocalDef {
1583            var, init: Some(e), ..
1584        } if counts.get(*var as usize).copied().unwrap_or(0) == 1 => {
1585            if *var as usize >= out.len() {
1586                out.resize(*var as usize + 1, None);
1587            }
1588            out[*var as usize] = Some(e.clone());
1589        }
1590        _ => {}
1591    });
1592}
1593
1594fn fold_concat_in_expr(e: &mut Expr, values: &[Option<Expr>], appended: &HashSet<u32>) {
1595    deep_rewrite(e, &mut |x| {
1596        if let Expr::Method {
1597            cls,
1598            name,
1599            args,
1600            owner,
1601            ..
1602        } = x
1603        {
1604            if name.as_ref() == "toString" && args.is_empty() && is_string_builder(cls) {
1605                if let Some(o) = owner {
1606                    // Statement-form appends attached to any chain var mean
1607                    // the parts are incomplete — keep the call.
1608                    let mut has_stmt_appends = false;
1609                    visit_exprs(o, &mut |y| {
1610                        if let Expr::Local { var, .. } = y {
1611                            if appended.contains(var) {
1612                                has_stmt_appends = true;
1613                            }
1614                        }
1615                    });
1616                    if has_stmt_appends {
1617                        return;
1618                    }
1619                    let resolved = resolve_local(o, values);
1620                    if let Some(parts) = collect_sb_parts(&resolved, values, 0) {
1621                        // NEVER fold an empty chain: d8 splits builder
1622                        // chains across alias registers (`v2 = v0.append
1623                        // (x); v2.append(y); v0.toString()`) — the traced
1624                        // chain sees a bare `new StringBuilder` and would
1625                        // fold toString() to "" (a wrong VALUE, silently:
1626                        // lab package Obf.label). With zero parts the
1627                        // call stays — always correct, usually folded
1628                        // elsewhere once the parts are visible.
1629                        if !parts.is_empty() {
1630                            *x = Expr::StringConcat(parts);
1631                        }
1632                    }
1633                }
1634            }
1635        }
1636    });
1637}
1638
1639fn is_string_builder(cls: &str) -> bool {
1640    cls == "java/lang/StringBuilder" || cls == "java/lang/StringBuffer"
1641}
1642
1643fn resolve_local(e: &Expr, values: &[Option<Expr>]) -> Expr {
1644    match e {
1645        Expr::Local { var, ty } => values
1646            .get(*var as usize)
1647            .and_then(|o| o.clone())
1648            .unwrap_or_else(|| Expr::Local {
1649                var: *var,
1650                ty: ty.clone(),
1651            }),
1652        other => other.clone(),
1653    }
1654}
1655
1656/// Collect concat parts from a StringBuilder chain; depth caps cycles.
1657fn collect_sb_parts(e: &Expr, values: &[Option<Expr>], depth: u32) -> Option<Vec<ConcatPart>> {
1658    if depth > 16 {
1659        return None;
1660    }
1661    match e {
1662        Expr::Method {
1663            cls,
1664            name,
1665            args,
1666            owner,
1667            ..
1668        } if name.as_ref() == "append" && is_string_builder(cls) && args.len() == 1 => {
1669            let mut parts =
1670                collect_sb_parts(&resolve_local(owner.as_deref()?, values), values, depth + 1)?;
1671            parts.push(ConcatPart::Str(args[0].clone()));
1672            Some(parts)
1673        }
1674        Expr::New {
1675            cls,
1676            args,
1677            raw: false,
1678            ..
1679        } if is_string_builder(cls) => {
1680            let mut parts = Vec::new();
1681            for a in args {
1682                if let Expr::Const(ConstVal::Str(sv)) = a {
1683                    parts.push(ConcatPart::Const(sv.to_string()));
1684                } else {
1685                    parts.push(ConcatPart::Str(a.clone()));
1686                }
1687            }
1688            Some(parts)
1689        }
1690        _ => None,
1691    }
1692}
1693
1694fn drop_unused_assigns(s: &mut Stmt, reads: &HashSet<u32>) -> usize {
1695    let mut dropped = 0usize;
1696    walk_mut_deep(s, &mut |st| {
1697        if let Stmt::Block(v) = st {
1698            let before = v.len();
1699            v.retain(|x| match x {
1700                // Drop only when unread AND a builder chain (folded
1701                // consumer; `new StringBuilder` chains cannot NPE).
1702                Stmt::ExprStmt(Expr::Assign { target, value, .. }) => match &**target {
1703                    Expr::Local { var, .. } => reads.contains(var) || !is_sbish(value),
1704                    _ => true,
1705                },
1706                Stmt::LocalDef { var, init, .. } => {
1707                    reads.contains(var) || !init.as_ref().map(is_sbish).unwrap_or(false)
1708                }
1709                _ => true,
1710            });
1711            dropped += before - v.len();
1712        }
1713    });
1714    dropped
1715}
1716
1717fn is_sbish(e: &Expr) -> bool {
1718    match e {
1719        Expr::New {
1720            cls, raw: false, ..
1721        } => is_string_builder(cls),
1722        Expr::Method { name, cls, .. } => name.as_ref() == "append" && is_string_builder(cls),
1723        _ => false,
1724    }
1725}
1726
1727fn walk_mut_deep<F: FnMut(&mut Stmt)>(s: &mut Stmt, f: &mut F) {
1728    f(s);
1729    walk_mut(s, &mut |x| walk_mut_deep(x, f));
1730}
1731
1732/// Recover `synchronized` from the d8 monitor pattern:
1733/// `monitorenter(e); try { body } catch (Throwable) { monitorexit(e); throw t; }`
1734/// (optionally followed by `monitorexit(e)`).
1735pub fn fold_synchronized(s: &mut Stmt) {
1736    fold_sync_walk(s);
1737}
1738
1739fn fold_sync_walk(s: &mut Stmt) {
1740    walk_mut_deep(s, &mut |st| {
1741        let Stmt::Block(v) = st else { return };
1742        let mut i = 0;
1743        while i < v.len() {
1744            if i + 1 < v.len() {
1745                if let Some(sync) = try_sync_at(v, i) {
1746                    let _ = v.drain(i..i + 2);
1747                    v.insert(i, sync);
1748                    continue;
1749                }
1750            }
1751            i += 1;
1752        }
1753    });
1754}
1755
1756fn try_sync_at(v: &[Stmt], i: usize) -> Option<Stmt> {
1757    let Stmt::MonitorEnter(lock) = &v[i] else {
1758        return None;
1759    };
1760    let Stmt::Try {
1761        body,
1762        catches,
1763        finally,
1764    } = &v[i + 1]
1765    else {
1766        return None;
1767    };
1768    if finally.is_some() || catches.len() != 1 {
1769        return None;
1770    }
1771    let c = &catches[0];
1772    if !c.exc.is_empty() {
1773        return None;
1774    }
1775    // The catch-all body: monitorexit(lock) then rethrow.
1776    let Stmt::Block(cv) = c.body.as_ref() else {
1777        return None;
1778    };
1779    let exit_matches = cv.len() >= 2
1780        && matches!(&cv[0], Stmt::MonitorExit(e) if expr_local_var(e) == expr_local_var(lock));
1781    let throws = cv.len() >= 2 && matches!(&cv[1], Stmt::Throw(_));
1782    if !(exit_matches && throws) {
1783        return None;
1784    }
1785    Some(Stmt::Synchronized {
1786        lock: lock.clone(),
1787        body: body.clone(),
1788    })
1789}
1790
1791fn expr_local_var(e: &Expr) -> Option<u32> {
1792    if let Expr::Local { var, .. } = e {
1793        Some(*var)
1794    } else {
1795        None
1796    }
1797}
1798
1799/// Copy-forward single-use temporaries: `v = e; ... v ...` inlines `e` at
1800/// the (single) use and drops the assignment when safe. Pure values inline
1801/// anywhere; impure values (calls, array reads) only when the use is the
1802/// statement immediately after the definition.
1803pub fn forward_single_use(s: &mut Stmt, _vt: &VarTable) {
1804    // Fused single traversal: assignment counts, read counts and the
1805    // single-assignment values all come out of ONE walk (three separate
1806    // full-tree walks dominated the pass cost on large methods).
1807    let mut analysis = VarAnalysis {
1808        assigns: Vec::new(),
1809        reads: Vec::new(),
1810        values: Vec::new(),
1811    };
1812    analyze_vars(s, &mut analysis);
1813    let mut assigns = analysis.assigns;
1814    let mut reads = analysis.reads;
1815    // Var ids beyond the table (synthetic test shapes) are covered by the
1816    // counters' on-demand growth.
1817    let n_vars = assigns.len().max(_vt.vars.len()).max(1);
1818    assigns.resize(n_vars, 0);
1819    reads.resize(n_vars, 0);
1820    let mut values = analysis.values;
1821    values.resize(n_vars, None);
1822
1823    // Candidates: assigned exactly once, read exactly once. Additionally
1824    // the inlined VALUE must not reference a multi-assigned var (phi vars):
1825    // moving the read across the phi's reassignment would be a stale
1826    // capture (register rotations snapshot through temps for this reason).
1827    let mut cand = vec![false; n_vars];
1828    let mut edges: Vec<Vec<u32>> = vec![Vec::new(); n_vars];
1829    for (v, cand_v) in cand.iter_mut().enumerate() {
1830        if assigns.get(v).copied().unwrap_or(0) != 1 || reads.get(v).copied().unwrap_or(0) != 1 {
1831            continue;
1832        }
1833        if let Some(val) = values.get(v).and_then(|o| o.as_ref()) {
1834            let mut refs = HashSet::default();
1835            collect_vars(val, &mut refs);
1836            if refs.iter().any(|r| assigns[*r as usize] > 1) {
1837                continue;
1838            }
1839            // Growth-graph edge set, built in the same walk: inlining v
1840            // inserts a clone of its value, and every Local inside the
1841            // clone is replaced in turn.
1842            edges[v] = refs.into_iter().collect();
1843        }
1844        *cand_v = true;
1845    }
1846
1847    // Cycle rejection on the growth graph. A def-reference cycle
1848    // (v = f(w), w = g(v) — loop-carried register rotation reaching the
1849    // pass as two mutually-referencing single-assign/single-read locals)
1850    // re-introduces the other cycle Local at every replacement level, so
1851    // deep_rewrite grows the tree one layer per level and never
1852    // terminates: weixin's com/tencent/mm/plugin/appbrand/widget/input/b4
1853    // exhausted a 64MB worker stack at ~300k recursion frames. Edges into
1854    // non-candidates are harmless (they are never replaced, so growth
1855    // dies there — and they carry no outgoing edges).
1856    //
1857    // Three-color DFS, iterative: a legit 60k-long single-use chain must
1858    // not trade one stack overflow for another. A GRAY child closes a
1859    // cycle; `bad[v]` (v on a cycle, or v's inlined closure grows into
1860    // one) then taints the whole current path — everything on it reaches
1861    // the cycle. Black verdicts memoize: a node whose subtree was proven
1862    // clean cannot grow a cycle later.
1863    let mut color = vec![0u8; n_vars]; // 0 white, 1 gray, 2 black
1864    let mut bad = vec![false; n_vars];
1865    for root in 0..n_vars {
1866        if color[root] != 0 || edges[root].is_empty() {
1867            continue;
1868        }
1869        color[root] = 1;
1870        let mut stack: Vec<(usize, usize)> = vec![(root, 0)];
1871        while let Some(top) = stack.last_mut() {
1872            let v = top.0;
1873            let i = top.1;
1874            top.1 += 1;
1875            if i < edges[v].len() {
1876                let r = edges[v][i] as usize;
1877                if color[r] == 1 {
1878                    for &(pn, _) in &stack {
1879                        bad[pn] = true;
1880                    }
1881                } else if color[r] == 0 {
1882                    color[r] = 1;
1883                    stack.push((r, 0));
1884                } else if bad[r] {
1885                    for &(pn, _) in &stack {
1886                        bad[pn] = true;
1887                    }
1888                }
1889            } else {
1890                color[v] = 2;
1891                stack.pop();
1892            }
1893        }
1894    }
1895    let single: Vec<bool> = (0..n_vars).map(|v| cand[v] && !bad[v]).collect();
1896    if !single.iter().any(|&b| b) {
1897        return;
1898    }
1899    let pure: Vec<bool> = (0..n_vars)
1900        .map(|v| {
1901            single[v]
1902                && values
1903                    .get(v)
1904                    .and_then(|o| o.as_ref())
1905                    .map(|e| !has_side_effects(e))
1906                    .unwrap_or(false)
1907        })
1908        .collect();
1909    let impure: Vec<bool> = (0..n_vars).map(|v| single[v] && !pure[v]).collect();
1910
1911    // 1. Pure values: inline anywhere.
1912    if pure.iter().any(|&b| b) {
1913        let vals: Vec<Option<Expr>> = (0..n_vars)
1914            .map(|v| {
1915                if pure[v] {
1916                    values.get(v).cloned().flatten()
1917                } else {
1918                    None
1919                }
1920            })
1921            .collect();
1922        rewrite_exprs(s, &mut |e| {
1923            deep_rewrite_reads(e, &mut |x| {
1924                if let Expr::Local { var, .. } = x {
1925                    if let Some(Some(v)) = vals.get(*var as usize) {
1926                        *x = v.clone();
1927                    }
1928                }
1929            });
1930        });
1931        drop_defs(s, &pure);
1932    }
1933
1934    // 2. Impure values: inline only into the immediately following
1935    //    statement.
1936    if impure.iter().any(|&b| b) {
1937        forward_adjacent_impure(s, &values, &impure);
1938    }
1939}
1940
1941/// Count READS of variables (assignment targets excluded).
1942fn count_reads(s: &Stmt, out: &mut Vec<usize>) {
1943    walk_all(s, &mut |st| {
1944        let exprs: Vec<&Expr> = match st {
1945            // Assign must match BEFORE the generic ExprStmt arm (the
1946            // assignment target is a write, not a read).
1947            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
1948                let mut v: Vec<&Expr> = vec![value];
1949                if !matches!(&**target, Expr::Local { .. }) {
1950                    v.push(target);
1951                }
1952                v
1953            }
1954            // `v++` writes v — not a read (a compound `a[i]++` still
1955            // reads its owner/index subtree).
1956            Stmt::ExprStmt(e @ (Expr::PreIncDec { .. } | Expr::PostIncDec { .. })) => {
1957                let inner = match e {
1958                    Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => e,
1959                    _ => unreachable!(),
1960                };
1961                if matches!(**inner, Expr::Local { .. }) {
1962                    Vec::new()
1963                } else {
1964                    vec![e]
1965                }
1966            }
1967            Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
1968                vec![e]
1969            }
1970            Stmt::Return(Some(e)) => vec![e],
1971            Stmt::LocalDef { init: Some(e), .. } => vec![e],
1972            Stmt::If { cond, .. } => vec![cond],
1973            Stmt::While { cond, .. } => vec![cond],
1974            Stmt::DoWhile { cond, .. } => vec![cond],
1975            // Read sites that are NOT statement children (walk_all recurses
1976            // into bodies but these payloads are expressions on the node
1977            // itself). Omitting them under-counts reads, which would let
1978            // drop_dead_locals over-prune a var whose only use is a switch
1979            // selector / loop condition / lock / iterable.
1980            Stmt::Switch { selector, .. } => vec![selector],
1981            Stmt::ForEach { iterable, .. } => vec![iterable],
1982            Stmt::Synchronized { lock, .. } => vec![lock],
1983            Stmt::For { cond, update, .. } => {
1984                let mut v: Vec<&Expr> = Vec::with_capacity(update.len() + 1);
1985                if let Some(c) = cond {
1986                    v.push(c);
1987                }
1988                v.extend(update.iter());
1989                v
1990            }
1991            Stmt::Assert { cond, msg } => {
1992                let mut v: Vec<&Expr> = vec![cond];
1993                if let Some(m) = msg {
1994                    v.push(m);
1995                }
1996                v
1997            }
1998            _ => Vec::new(),
1999        };
2000        for e in exprs {
2001            visit_exprs(e, &mut |x| {
2002                if let Expr::Local { var, .. } = x {
2003                    grow_to(out, *var);
2004                    out[*var as usize] += 1;
2005                }
2006            });
2007        }
2008    });
2009}
2010
2011fn drop_defs(s: &mut Stmt, vars: &[bool]) {
2012    walk_mut_deep(s, &mut |st| {
2013        if let Stmt::Block(v) = st {
2014            v.retain(|x| match x {
2015                Stmt::ExprStmt(Expr::Assign { target, .. }) => match &**target {
2016                    Expr::Local { var, .. } => !vars.get(*var as usize).copied().unwrap_or(false),
2017                    _ => true,
2018                },
2019                Stmt::LocalDef { var, .. } => !vars.get(*var as usize).copied().unwrap_or(false),
2020                _ => true,
2021            });
2022        }
2023    });
2024}
2025
2026/// For each statement list: `v = IMPURE; NEXT(v)` folds NEXT's reference.
2027fn forward_adjacent_impure(s: &mut Stmt, values: &[Option<Expr>], impure: &[bool]) {
2028    if let Stmt::Block(v) = s {
2029        let mut i = 0;
2030        while i + 1 < v.len() {
2031            let def_var = match &v[i] {
2032                Stmt::ExprStmt(Expr::Assign { target, .. }) => match &**target {
2033                    Expr::Local { var, .. } => Some(*var),
2034                    _ => None,
2035                },
2036                Stmt::LocalDef { var, .. } => Some(*var),
2037                _ => None,
2038            };
2039            if let Some(var) = def_var {
2040                if impure.get(var as usize).copied().unwrap_or(false) {
2041                    // Does the NEXT statement reference var?
2042                    let mut reads_here = vec![0usize; values.len().max(1)];
2043                    count_reads(&v[i + 1], &mut reads_here);
2044                    if reads_here.get(var as usize).copied().unwrap_or(0) == 1 {
2045                        // The def statement's CURRENT value (earlier inlines
2046                        // in this walk may have rewritten it — the pre-built
2047                        // values map would be stale).
2048                        let val = match &v[i] {
2049                            Stmt::ExprStmt(Expr::Assign { value, .. }) => (**value).clone(),
2050                            Stmt::LocalDef { init: Some(e), .. } => (*e).clone(),
2051                            _ => {
2052                                i += 1;
2053                                continue;
2054                            }
2055                        };
2056                        rewrite_exprs(&mut v[i + 1], &mut |e| {
2057                            deep_rewrite_reads(e, &mut |x| {
2058                                if let Expr::Local { var: v2, .. } = x {
2059                                    if *v2 == var {
2060                                        *x = val.clone();
2061                                    }
2062                                }
2063                            });
2064                        });
2065                        v.remove(i);
2066                        // The PREVIOUS definition may now be adjacent to its
2067                        // (shifted) use — re-examine it.
2068                        i = i.saturating_sub(1);
2069                        continue;
2070                    }
2071                }
2072            }
2073            i += 1;
2074        }
2075    }
2076    walk_mut(s, &mut |x| forward_adjacent_impure(x, values, impure));
2077}
2078
2079// ---------------------------------------------------------------------------
2080// Type inference, booleanization, null comparisons, declarations
2081// ---------------------------------------------------------------------------
2082
2083/// Evidence-based type inference for synthetic (non-parameter) vars.
2084pub fn infer_types(vt: &mut VarTable, body: &mut Stmt, ret: &JavaType, env: &MethodEnv) {
2085    let n = vt.vars.len();
2086    // (type, strong) — DIRECTIONAL evidence. Strong facts (an assigned
2087    // value's type, the declared return/throw/monitor type) DEFINE the
2088    // variable; weak facts (call argument expectations, receiver
2089    // contexts, numeric literals) only constrain a use. jadx's bound
2090    // system carries the same direction; a flat pool let a weak
2091    // expectation (`v26 = p1` with boolean p1 beside `v26 = 0` sugar)
2092    // outvote the definition.
2093    let mut evidence: Vec<Vec<(JavaType, bool)>> = vec![Vec::new(); n];
2094    let ev = |evidence: &mut Vec<Vec<(JavaType, bool)>>, var: u32, t: JavaType, strong: bool| {
2095        if (var as usize) < evidence.len() {
2096            evidence[var as usize].push((t, strong));
2097        }
2098    };
2099
2100    walk_all(body, &mut |st| match st {
2101        Stmt::LocalDef { var, init, .. } => {
2102            if let Some(e) = init {
2103                expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
2104                // The initializer DEFINES the declared type: `long v = l(...)`
2105                // must not stay `int v` (silent 64-bit truncation in the
2106                // eyes of a reader; javac rejects it as lossy). Null
2107                // carries no type (it would DOWNGRADE String to Object).
2108                // Literals are weak (0/1 is boolean sugar half the time);
2109                // typed producers and parameter sources are strong;
2110                // local-to-local copies are weak (register reuse).
2111                let strong = match e {
2112                    Expr::Const(_) => false,
2113                    Expr::Local { var: src, .. } => {
2114                        let src_ty = vt.vars.get(*src as usize).map(|v| v.ty.erased());
2115                        let tgt_ty = vt.vars.get(*var as usize).map(|v| v.ty.erased());
2116                        match (src_ty, tgt_ty) {
2117                            (Some(s), Some(g)) => {
2118                                (s.is_numeric() && g.is_numeric())
2119                                    || (s.is_reference() && g.is_reference())
2120                            }
2121                            _ => false,
2122                        }
2123                    }
2124                    _ => true,
2125                };
2126                ev(&mut evidence, *var, e.type_ref().erased(), strong);
2127            }
2128        }
2129        Stmt::ExprStmt(e) => {
2130            // Assignment targets: a typed PRODUCER (method/new/field) or a
2131            // PARAMETER source is a STRONG definition (`v26 = p1` with
2132            // boolean p1). A local-to-local copy is weak — register reuse
2133            // and merge materialization emit exactly that shape with
2134            // mismatched types (`sb7 = compareTo10` in a Kotlin when).
2135            // Literals stay weak.
2136            if let Expr::Assign { target, value, .. } = e {
2137                if let Expr::Local { var, .. } = &**target {
2138                    let strong = match &**value {
2139                        Expr::Const(_) => false,
2140                        Expr::Local { var: src, .. } => {
2141                            // A local copy is strong when the source and
2142                            // the target's register type are the same
2143                            // FAMILY (numeric↔numeric, reference↔
2144                            // reference) — a cross-family copy (int into
2145                            // a StringBuilder local) is merge-material
2146                            // residue and stays weak.
2147                            let src_ty = vt.vars.get(*src as usize).map(|v| v.ty.erased());
2148                            let tgt_ty = vt.vars.get(*var as usize).map(|v| v.ty.erased());
2149                            match (src_ty, tgt_ty) {
2150                                (Some(s), Some(g)) => {
2151                                    (s.is_numeric() && g.is_numeric())
2152                                        || (s.is_reference() && g.is_reference())
2153                                }
2154                                _ => false,
2155                            }
2156                        }
2157                        _ => true,
2158                    };
2159                    ev(&mut evidence, *var, value.type_ref().erased(), strong);
2160                }
2161            }
2162            expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
2163        }
2164        Stmt::Throw(e) => {
2165            expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
2166            if let Expr::Local { var, .. } = e {
2167                ev(
2168                    &mut evidence,
2169                    *var,
2170                    JavaType::Object("java/lang/Throwable".into()),
2171                    true,
2172                );
2173            }
2174        }
2175        Stmt::Return(Some(e)) => {
2176            expr_evidence(e, &mut |v, t| ev(&mut evidence, v, t, false));
2177            if let Expr::Local { var, .. } = e {
2178                ev(&mut evidence, *var, ret.clone(), true);
2179            }
2180        }
2181        Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
2182            if let Expr::Local { var, .. } = e {
2183                ev(
2184                    &mut evidence,
2185                    *var,
2186                    JavaType::Object("java/lang/Object".into()),
2187                    true,
2188                );
2189            }
2190        }
2191        Stmt::ForEach {
2192            var,
2193            iterable,
2194            is_array,
2195            ..
2196        } => {
2197            if let Expr::Local { var: v0, .. } = iterable {
2198                if *is_array {
2199                    ev(&mut evidence, *v0, JavaType::Array(Box::new(JavaType::Int)), true);
2200                } else {
2201                    ev(
2202                        &mut evidence,
2203                        *v0,
2204                        JavaType::Object("java/lang/Object".into()),
2205                        true,
2206                    );
2207                }
2208            }
2209            let _ = var;
2210        }
2211        _ => {}
2212    });
2213    let _ = env;
2214
2215    // Return-position locals take the declared return type (also inside
2216    // nested returns — handled by the walk above).
2217
2218    for (i, evs) in evidence.iter().enumerate() {
2219        let info = &vt.vars[i];
2220        if info.is_param {
2221            continue;
2222        }
2223        let strong: Vec<JavaType> = evs
2224            .iter()
2225            .filter(|(_, s)| *s)
2226            .map(|(t, _)| t.clone())
2227            .collect();
2228        let all: Vec<JavaType> = evs.iter().map(|(t, _)| t.clone()).collect();
2229        if info.ty.erased().is_reference() {
2230            // Materialization residue: no strong definition, and the weak
2231            // pool names ≥2 DIFFERENT classes (a Kotlin `when` lowered
2232            // every branch's value into one register slot) — neither
2233            // class can win, every assignment needs boxing headroom:
2234            // widen to Object (uses get receiver casts).
2235            if strong.is_empty() {
2236                let refs: std::collections::HashSet<&str> = all
2237                    .iter()
2238                    .filter_map(|t| match t {
2239                        JavaType::Object(n) if n.as_ref() != "java/lang/Object" => {
2240                            Some(n.as_ref())
2241                        }
2242                        _ => None,
2243                    })
2244                    .collect();
2245                let mixed = all.iter().any(|t| t.is_numeric());
2246                if refs.len() >= 2 || (mixed && !refs.is_empty()) || (mixed && all.iter().any(|t| matches!(t, JavaType::Array(_)))) {
2247                    vt.vars[i].ty =
2248                        TypeRef::J(JavaType::Object("java/lang/Object".into()));
2249                    continue;
2250                }
2251            }
2252            // Strong definitions outrank weak expectations.
2253            if let Some(t) = pick_object(&strong).or_else(|| pick_object(&all)) {
2254                vt.vars[i].ty = TypeRef::J(t);
2255            }
2256        } else if strong.contains(&JavaType::Boolean) {
2257            // A boolean-typed SOURCE assigned into the variable is a
2258            // definition (`v26 = p1` with boolean p1); the 0/1 literals
2259            // that share the pool are boolean sugar as often as not.
2260            vt.vars[i].ty = TypeRef::J(JavaType::Boolean);
2261        } else if !all.is_empty() && all.iter().all(|t| t.is_numeric()) {
2262            if let Some(t) = pick_numeric(&all) {
2263                vt.vars[i].ty = TypeRef::J(t);
2264            }
2265        } else {
2266            // Mixed-family materialization on a numeric-declared slot
2267            // (int slot receiving StringBuilders — the register was the
2268            // `when` accumulator): Object, same as the reference side.
2269            let refs = all.iter().filter(|t| t.is_reference()).count();
2270            let nums = all.iter().filter(|t| t.is_numeric()).count();
2271            if refs > 0 && nums > 0 {
2272                vt.vars[i].ty =
2273                    TypeRef::J(JavaType::Object("java/lang/Object".into()));
2274            } else if let Some(t) = pick_object(&strong).or_else(|| pick_object(&all)) {
2275                vt.vars[i].ty = TypeRef::J(t);
2276            }
2277        }
2278    }
2279
2280    // Rewrite embedded Local types. Borrowed view + write-only-on-change:
2281    // the table clone was n_vars JavaType clones per method, and the
2282    // rewrite cloned a type into EVERY Local node — most already carry
2283    // the right one (JavaType::Object clones allocate).
2284    let types: Vec<&TypeRef> = vt.vars.iter().map(|v| &v.ty).collect();
2285    rewrite_exprs(body, &mut |e| {
2286        deep_rewrite(e, &mut |x| {
2287            if let Expr::Local { var, ty } = x {
2288                if let Some(want) = types.get(*var as usize) {
2289                    if ty != *want {
2290                        *ty = (*want).clone();
2291                    }
2292                }
2293            }
2294        });
2295    });
2296
2297    // Numeric literals take their variable's declared width (`double v = 0L`
2298    // prints as `0.0`; `int x = 5L` as `5`).
2299    coerce_num_consts(body, &types);
2300}
2301
2302fn coerce_num_consts(body: &mut Stmt, types: &[&TypeRef]) {
2303    walk_mut_deep(body, &mut |st| {
2304        let (var, val): (u32, &mut Expr) = match st {
2305            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => match &mut **target {
2306                Expr::Local { var, .. } => (*var, value),
2307                _ => return,
2308            },
2309            Stmt::LocalDef {
2310                var, init: Some(e), ..
2311            } => (*var, e),
2312            _ => return,
2313        };
2314        let want = match types.get(var as usize) {
2315            Some(TypeRef::J(t)) => t.clone(),
2316            _ => return,
2317        };
2318        let rewritten = match &*val {
2319            Expr::Const(ConstVal::Long(l)) => match &want {
2320                JavaType::Double => Some(Expr::Const(ConstVal::Double(*l as f64))),
2321                JavaType::Float => Some(Expr::Const(ConstVal::Float(*l as f32))),
2322                _ => None,
2323            },
2324            Expr::Const(ConstVal::Int(i)) => match &want {
2325                JavaType::Double => Some(Expr::Const(ConstVal::Double(*i as f64))),
2326                JavaType::Float => Some(Expr::Const(ConstVal::Float(*i as f32))),
2327                _ => None,
2328            },
2329            _ => None,
2330        };
2331        if let Some(e) = rewritten {
2332            *val = e;
2333        }
2334    });
2335}
2336
2337fn pick_object(evs: &[JavaType]) -> Option<JavaType> {
2338    evs.iter()
2339        .find(|t| t.is_reference() && !matches!(t, JavaType::Object(n) if n.as_ref() == "java/lang/Object"))
2340        .cloned()
2341}
2342
2343fn pick_numeric(evs: &[JavaType]) -> Option<JavaType> {
2344    if evs.is_empty() {
2345        return None;
2346    }
2347    let mut t = evs[0].clone();
2348    for x in &evs[1..] {
2349        t = join_numeric(&t, x);
2350    }
2351    Some(t)
2352}
2353
2354fn join_numeric(a: &JavaType, b: &JavaType) -> JavaType {
2355    if a == b {
2356        return a.clone();
2357    }
2358    match (a, b) {
2359        (JavaType::Double, _) | (_, JavaType::Double) => JavaType::Double,
2360        (JavaType::Float, _) | (_, JavaType::Float) => JavaType::Float,
2361        (JavaType::Long, _) | (_, JavaType::Long) => JavaType::Long,
2362        (JavaType::Int, x) if x.is_integral() => JavaType::Int,
2363        (x, JavaType::Int) if x.is_integral() => JavaType::Int,
2364        (x, y) if x.is_integral() && y.is_integral() => JavaType::Int,
2365        _ => JavaType::Int,
2366    }
2367}
2368
2369/// Gather per-var type evidence from one expression.
2370fn expr_evidence<F: FnMut(u32, JavaType)>(e: &Expr, f: &mut F) {
2371    visit_exprs(e, &mut |x| match x {
2372        Expr::Method {
2373            owner,
2374            args,
2375            desc,
2376            is_static,
2377            cls,
2378            ..
2379        } => {
2380            if !*is_static {
2381                if let Some(o) = owner {
2382                    if let Expr::Local { var, .. } = &**o {
2383                        f(*var, JavaType::Object(cls.clone()));
2384                    }
2385                }
2386            }
2387            for (i, a) in args.iter().enumerate() {
2388                if let Expr::Local { var, .. } = a {
2389                    if let Some(t) = desc.args.get(i) {
2390                        f(*var, t.clone());
2391                    }
2392                }
2393            }
2394        }
2395        Expr::Field {
2396            owner,
2397            ty,
2398            is_static,
2399            ..
2400        } => {
2401            if !*is_static {
2402                if let Some(o) = owner {
2403                    if let Expr::Local { var, .. } = &**o {
2404                        f(*var, JavaType::Object("java/lang/Object".into()));
2405                    }
2406                }
2407            }
2408            let _ = ty;
2409        }
2410        Expr::ArrayIndex { array, .. } => {
2411            if let Expr::Local { var, ty, .. } = &**array {
2412                // Reinforce the local's KNOWN array type: the hardcoded
2413                // int[] fallback contradicted a typed array local (a
2414                // register reused for byte[], int[] then byte[] across
2415                // one clinit labeled the byte[] defs `int[]`).
2416                let t = ty.erased();
2417                if matches!(t, JavaType::Array(_)) {
2418                    f(*var, t);
2419                } else {
2420                    f(*var, JavaType::Array(Box::new(JavaType::Int)));
2421                }
2422            }
2423        }
2424        Expr::Assign { .. } => {
2425            // Covered by the typed strong-evidence walk in infer_types;
2426            // keeping a weak duplicate here let expectations outvote it.
2427        }
2428        Expr::Cast { e: inner, .. } => {
2429            // A cast is an EXPLICIT narrowing at the use site — it must
2430            // NOT feed the cast target as evidence for the variable
2431            // (`Object get2 = list.get(2); j((String) get2);` had get2
2432            // retyped to String, making the assignment incompatible).
2433            // jadx's bounds carry direction: a USE bound narrows nothing.
2434            let _ = inner;
2435        }
2436        Expr::InstanceOf { e: inner, .. } => {
2437            let _ = inner;
2438        }
2439        _ => {}
2440    });
2441}
2442
2443/// Type inference can leave a specific-reference-typed target assigned
2444/// from a `java/lang/Object`-typed value: a phi that merged String and
2445/// Object then settled on String (`String str4; ... str4 = obj;` where
2446/// `obj` is an Object field — y5/n.java), or an Object local flowing to a
2447/// typed field. Java requires a narrowing cast there; insert `(T) value`
2448/// at the USE site (round-40 philosophy: narrow at use, never retype the
2449/// declaration). Guarded tightly: only when the value's resolved static
2450/// type is EXACTLY `java/lang/Object` (the untyped top — a known subtype
2451/// is never re-cast), the value is not a null const (assignable to any
2452/// ref) nor already a cast, and the target is a specific reference type
2453/// (Object→int/boolean is unboxing, a different problem left alone).
2454pub fn insert_object_narrowing_casts(vt: &VarTable, body: &mut Stmt) {
2455    // Resolve a value's static type through the VarTable for locals (the
2456    // embedded Local ty can lag infer_types), else the expr's own type.
2457    let value_ty = |e: &Expr| -> JavaType {
2458        match e {
2459            Expr::Local { var, .. } => vt.var(*var).ty.erased(),
2460            other => other.type_ref().erased(),
2461        }
2462    };
2463    let is_top_object = |e: &Expr| -> bool {
2464        value_ty(e) == JavaType::Object("java/lang/Object".into())
2465    };
2466    // A specific reference target type (a class other than java/lang/Object).
2467    let specific_ref = |ty: &TypeRef| -> Option<TypeRef> {
2468        match ty.erased() {
2469            JavaType::Object(c) if c.as_ref() != "java/lang/Object" => Some(ty.clone()),
2470            _ => None,
2471        }
2472    };
2473    let castable = |e: &Expr| -> bool {
2474        is_top_object(e)
2475            && !matches!(e, Expr::Const(_) | Expr::Cast { .. } | Expr::InstanceOf { .. })
2476    };
2477    walk_mut_deep(body, &mut |st| match st {
2478        Stmt::ExprStmt(Expr::Assign {
2479            target,
2480            value,
2481            op: AssignOp::Plain,
2482            ..
2483        }) => {
2484            let tgt = match &**target {
2485                Expr::Local { var, .. } => vt.var(*var).ty.clone(),
2486                Expr::Field { ty, .. } => ty.clone(),
2487                _ => return,
2488            };
2489            if let Some(t) = specific_ref(&tgt) {
2490                if castable(value) {
2491                    let v = std::mem::replace(value, Box::new(Expr::This));
2492                    **value = Expr::Cast { ty: t, e: v };
2493                }
2494            }
2495        }
2496        Stmt::LocalDef { var, init: Some(value), .. } => {
2497            if let Some(t) = specific_ref(&vt.var(*var).ty) {
2498                if castable(value) {
2499                    let v = std::mem::replace(value, Expr::This);
2500                    *value = Expr::Cast {
2501                        ty: t,
2502                        e: Box::new(v),
2503                    };
2504                }
2505            }
2506        }
2507        _ => {}
2508    });
2509}
2510
2511/// Boolean inference: vars only ever assigned 0/1/comparisons/booleans and
2512/// read in conditions become `boolean`, with `v != 0` → `v` in conditions.
2513pub fn booleanize(vt: &mut VarTable, body: &mut Stmt, ret_bool: bool) {
2514    // Booleans propagate through local chains (`v17 = v24` where v24
2515    // itself became boolean in the first round) — iterate to a fixpoint;
2516    // the common corpus converts nothing and exits after one round.
2517    for _ in 0..4 {
2518        if booleanize_round(vt, body, ret_bool) == 0 {
2519            break;
2520        }
2521    }
2522}
2523
2524fn booleanize_round(vt: &mut VarTable, body: &mut Stmt, ret_bool: bool) -> usize {
2525    let n = vt.vars.len();
2526    let mut in_cond = vec![false; n];
2527
2528    // Condition uses: `v == 0` / `v != 0` if-conditions and comparison
2529    // operands inside assigned values.
2530    walk_all(body, &mut |st| {
2531        let cond: Option<&Expr> = match st {
2532            Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
2533                Some(cond)
2534            }
2535            _ => None,
2536        };
2537        if let Some(c) = cond {
2538            if let Expr::Bin {
2539                op: BinOp::Eq | BinOp::Ne,
2540                l,
2541                r,
2542                ..
2543            } = c
2544            {
2545                for (x, other) in [(l, r), (r, l)] {
2546                    if let (Expr::Local { var, .. }, Expr::Const(ConstVal::Int(0))) =
2547                        (&**x, &**other)
2548                    {
2549                        if (*var as usize) < n {
2550                            in_cond[*var as usize] = true;
2551                        }
2552                    }
2553                }
2554            }
2555        }
2556        // A local RETURNED from a boolean method is boolean-typed even
2557        // though it never appears in a condition (`int v9 = 0/1 … return
2558        // v9;` — returning an int from `boolean check()` is a compile
2559        // error; lab package Obf.check).
2560        if ret_bool {
2561            if let Stmt::Return(Some(Expr::Local { var, .. })) = st {
2562                if (*var as usize) < n {
2563                    in_cond[*var as usize] = true;
2564                }
2565            }
2566        }
2567        // A local STORED into a boolean field is boolean-typed even though
2568        // it never appears in a condition (`b = v1` where `b` is a boolean
2569        // field, `v1` an int 0/1 local — q.java static init). The emit
2570        // layer already coerces a boolean-target assign through expr_bool,
2571        // but that renders a bare int LOCAL unchanged, so the local itself
2572        // must be booleanized (its 0/1 assigns then print false/true).
2573        if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = st {
2574            if let Expr::Field { ty, .. } = &**target {
2575                if ty.erased() == JavaType::Boolean {
2576                    if let Expr::Local { var, .. } = &**value {
2577                        if (*var as usize) < n {
2578                            in_cond[*var as usize] = true;
2579                        }
2580                    }
2581                }
2582            }
2583        }
2584        let val: Option<&Expr> = match st {
2585            Stmt::ExprStmt(Expr::Assign { value, .. }) => Some(value),
2586            // Bare expression statements (e.g. `q(v117);`) so a local
2587            // passed to a boolean parameter is caught below.
2588            Stmt::ExprStmt(e) => Some(e),
2589            Stmt::LocalDef { init: Some(e), .. } => Some(e),
2590            Stmt::Return(Some(e)) => Some(e),
2591            _ => None,
2592        };
2593        if let Some(v) = val {
2594            visit_exprs(v, &mut |x| {
2595                // A local passed as a BOOLEAN parameter is boolean-typed
2596                // (`q(v117)` where q's param is boolean, v117 an int 0/1 —
2597                // f8/b.java). The DEX passes booleans as 0/1 ints, so a
2598                // boolean param slot is authoritative for the argument's
2599                // type.
2600                if let Expr::Method { desc, args, .. } = x {
2601                    for (i, a) in args.iter().enumerate() {
2602                        if desc.args.get(i) == Some(&JavaType::Boolean) {
2603                            if let Expr::Local { var, .. } = a {
2604                                if (*var as usize) < n {
2605                                    in_cond[*var as usize] = true;
2606                                }
2607                            }
2608                        }
2609                    }
2610                }
2611                if let Expr::Bin { op, l, r, .. } = x {
2612                    if matches!(
2613                        op,
2614                        BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
2615                    ) {
2616                        for side in [l, r] {
2617                            if let Expr::Local { var, .. } = &**side {
2618                                if (*var as usize) < n {
2619                                    in_cond[*var as usize] = true;
2620                                }
2621                            }
2622                        }
2623                    }
2624                    // Kotlin/d8 merge booleans through INT bitwise ops
2625                    // (`v3 | obj instanceof g`) == 0 — the other side's
2626                    // static type being boolean makes this a boolean
2627                    // context for the local (a genuine int bitwise
2628                    // expression never has a boolean operand).
2629                    if matches!(op, BinOp::Or | BinOp::And) {
2630                        let l_bool = l.type_ref().erased() == JavaType::Boolean;
2631                        let r_bool = r.type_ref().erased() == JavaType::Boolean;
2632                        if l_bool != r_bool {
2633                            let side = if l_bool { r } else { l };
2634                            if let Expr::Local { var, .. } = &**side {
2635                                if (*var as usize) < n {
2636                                    in_cond[*var as usize] = true;
2637                                }
2638                            }
2639                        }
2640                    }
2641                }
2642            });
2643        }
2644    });
2645
2646    // A var whose every assignment value is boolean-shaped AND which is
2647    // referenced in conditions becomes boolean.
2648    // (Perf: this used to re-walk the whole statement tree ONCE PER VAR —
2649    // O(vars × stmts); on R8-merged monsters (2000+ vars, 15k statements)
2650    // that was tens of millions of visits. One pass collects the same
2651    // (any, all_bool) facts for every var.)
2652    let mut assigned_any = vec![false; n];
2653    let mut all_bool = vec![true; n];
2654    let mut edges: Vec<(usize, usize)> = Vec::new();
2655    walk_all(body, &mut |st| {
2656        let (var, value) = match st {
2657            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => match &**target {
2658                Expr::Local { var, .. } => (var, &**value),
2659                _ => return,
2660            },
2661            Stmt::LocalDef {
2662                var, init: Some(e), ..
2663            } => (var, e),
2664            _ => return,
2665        };
2666        let i = *var as usize;
2667        if i < n {
2668            assigned_any[i] = true;
2669            if !is_boolean_valued(value) {
2670                all_bool[i] = false;
2671            }
2672            // Chain edges for the context closure below: `v17 = v24`
2673            // links the two locals; when the TARGET is in a boolean
2674            // context, the source inherits it (its only reader is the
2675            // boolean-shaped chain). Collected as edges because the
2676            // target's context can be discovered anywhere in the tree.
2677            if let Expr::Local { var: src, .. } = value {
2678                if (*src as usize) < n {
2679                    edges.push((i, *src as usize));
2680                }
2681            }
2682        }
2683    });
2684    // Context closure along local chains: a var in a boolean context
2685    // pushes that context through every local it is assigned from
2686    // (`return v17` — v17 = v24 — v24 = 0/1) — the statement order
2687    // cannot be relied on (the return may follow the assignment).
2688    loop {
2689        let mut changed = false;
2690        for &(tgt, src) in &edges {
2691            if in_cond[tgt] && !in_cond[src] && all_bool[src] {
2692                in_cond[src] = true;
2693                changed = true;
2694            }
2695        }
2696        if !changed {
2697            break;
2698        }
2699    }
2700    let mut boolean_vars: HashSet<u32> = HashSet::default();
2701    for i in 0..n {
2702        if vt.vars[i].is_param {
2703            continue;
2704        }
2705        if !in_cond[i] {
2706            continue;
2707        }
2708        if assigned_any[i] && all_bool[i] {
2709            boolean_vars.insert(i as u32);
2710        }
2711    }
2712    if boolean_vars.is_empty() {
2713        return 0;
2714    }
2715    for v in &boolean_vars {
2716        vt.vars[*v as usize].ty = TypeRef::J(JavaType::Boolean);
2717    }
2718    let types: Vec<&TypeRef> = vt.vars.iter().map(|v| &v.ty).collect();
2719    rewrite_exprs(body, &mut |e| {
2720        deep_rewrite(e, &mut |x| {
2721            if let Expr::Local { var, ty } = x {
2722                if let Some(want) = types.get(*var as usize) {
2723                    if ty != *want {
2724                        *ty = (*want).clone();
2725                    }
2726                }
2727            }
2728        });
2729    });
2730
2731    // Condition folding: `b != 0` → `b`, `b == 0` → `!b` (boolean vars).
2732    fold_bool_conditions(body, &boolean_vars);
2733    boolean_vars.len()
2734}
2735
2736fn is_boolean_valued(e: &Expr) -> bool {
2737    match e {
2738        Expr::Const(ConstVal::Int(0)) | Expr::Const(ConstVal::Int(1)) => true,
2739        Expr::Bin { op, .. } => matches!(
2740            op,
2741            BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
2742        ),
2743        Expr::InstanceOf { .. } => true,
2744        Expr::Local { ty, .. } => ty.erased() == JavaType::Boolean,
2745        Expr::Method { desc, .. } => desc.ret == JavaType::Boolean,
2746        Expr::Cond { t, f, .. } => is_boolean_valued(t) && is_boolean_valued(f),
2747        Expr::Un { op: UnOp::Not, .. } => true,
2748        _ => false,
2749    }
2750}
2751
2752fn fold_bool_conditions(s: &mut Stmt, bv: &HashSet<u32>) {
2753    let fold = |e: &mut Expr| {
2754        deep_rewrite(e, &mut |x| {
2755            if let Expr::Bin { op, l, r, .. } = x {
2756                let (var_side, const_side) = match (&**l, &**r) {
2757                    (Expr::Local { .. }, Expr::Const(ConstVal::Int(_))) => (l, 0),
2758                    (Expr::Const(ConstVal::Int(_)), Expr::Local { .. }) => (r, 1),
2759                    _ => return,
2760                };
2761                let var = match &**var_side {
2762                    Expr::Local { var, .. } => *var,
2763                    _ => return,
2764                };
2765                if !bv.contains(&var) {
2766                    return;
2767                }
2768                let _ = const_side;
2769                match op {
2770                    BinOp::Ne => {
2771                        let inner = var_side.clone();
2772                        *x = *inner;
2773                    }
2774                    BinOp::Eq => {
2775                        let inner = var_side.clone();
2776                        *x = Expr::Un {
2777                            op: UnOp::Not,
2778                            e: Box::new(*inner),
2779                        };
2780                    }
2781                    _ => {}
2782                }
2783            }
2784        });
2785    };
2786    match s {
2787        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => fold(cond),
2788        _ => {}
2789    }
2790    walk_mut_deep(s, &mut |st| {
2791        let cond = match st {
2792            Stmt::If { cond, .. } => Some(cond),
2793            Stmt::While { cond, .. } => Some(cond),
2794            Stmt::DoWhile { cond, .. } => Some(cond),
2795            _ => None,
2796        };
2797        if let Some(c) = cond {
2798            fold(c);
2799        }
2800    });
2801}
2802
2803/// Normalize `1 == v` comparisons to `v == 1` (d8 emits const-first if-eq).
2804pub fn flip_const_compares(s: &mut Stmt) {
2805    rewrite_exprs(s, &mut |e| {
2806        deep_rewrite(e, &mut |x| {
2807            if let Expr::Bin { op, l, r, .. } = x {
2808                if matches!(
2809                    op,
2810                    BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Ge | BinOp::Gt | BinOp::Le
2811                ) {
2812                    let l_const = matches!(&**l, Expr::Const(_));
2813                    let r_var = matches!(&**r, Expr::Local { .. });
2814                    if l_const && r_var {
2815                        std::mem::swap(l, r);
2816                    }
2817                }
2818            }
2819        });
2820    });
2821}
2822
2823/// Object-typed locals compared against 0 compare against `null`.
2824pub fn null_compares(vt: &VarTable, body: &mut Stmt) {
2825    let obj_vars: HashSet<u32> = vt
2826        .vars
2827        .iter()
2828        .filter(|v| v.ty.erased().is_reference())
2829        .map(|v| v.id)
2830        .collect();
2831    rewrite_exprs(body, &mut |e| {
2832        deep_rewrite(e, &mut |x| {
2833            null_side_rewrite(x, &|v| obj_vars.contains(&v));
2834        });
2835    });
2836}
2837
2838/// Drop assignments/declarations of locals that are NEVER read anywhere
2839/// in the method. Register-machine merges commit values no Java-level
2840/// code consumes (phi residue like `printStream = check;` — not just
2841/// noise: the merge variable's inferred type need not match the value,
2842/// so these lines are often type errors as well). Side-effect-free
2843/// values drop entirely; an impure value survives as a bare expression
2844/// statement. Iterated to a fixpoint: dropping `a = b` can make `b`
2845/// unread.
2846pub fn drop_dead_locals(body: &mut Stmt) {
2847    loop {
2848        let mut reads: Vec<usize> = Vec::new();
2849        count_reads(body, &mut reads);
2850        let mut dropped = 0usize;
2851        walk_mut_deep(body, &mut |st| match st {
2852            Stmt::Block(items) => prune_dead_items(items, &reads, &mut dropped),
2853            // Switch case bodies and For inits are bare `Vec<Stmt>`, NOT
2854            // wrapped in a `Stmt::Block`, so the Block arm never sees them.
2855            // Dead phi commits land directly in case bodies (`sb4 =
2856            // compareTo;` per case): pruning only Blocks dropped the
2857            // commit's reader (in a real post-switch Block) but left the
2858            // now-dead commits, stalling the fixpoint cascade and leaking
2859            // mistyped phi residue. TryWithResources.resources is
2860            // deliberately excluded — its LocalDefs carry auto-close
2861            // semantics that a bare-expression rewrite would break.
2862            Stmt::Switch { cases, .. } => {
2863                for c in cases {
2864                    prune_dead_items(&mut c.body, &reads, &mut dropped);
2865                }
2866            }
2867            Stmt::For { init, .. } => prune_dead_items(init, &reads, &mut dropped),
2868            _ => {}
2869        });
2870        if dropped == 0 {
2871            break;
2872        }
2873    }
2874}
2875
2876/// Remove dead assignments/declarations from one bare statement vector.
2877/// Shared by `drop_dead_locals` across every prunable `Vec<Stmt>`
2878/// container. Side-effect-free values drop entirely; an impure value
2879/// survives as a bare expression statement.
2880fn prune_dead_items(items: &mut Vec<Stmt>, reads: &[usize], dropped: &mut usize) {
2881    let dead = |v: u32| reads.get(v as usize).copied().unwrap_or(0) == 0;
2882    items.retain_mut(|x| match x {
2883        Stmt::ExprStmt(Expr::Assign { target, value, op, .. }) => {
2884            let dead_target = matches!(&**target, Expr::Local { var, .. } if dead(*var));
2885            if dead_target && matches!(*op, AssignOp::Plain) {
2886                *dropped += 1;
2887                if has_side_effects(value) {
2888                    let e = std::mem::replace(value, Box::new(Expr::This));
2889                    *x = Stmt::ExprStmt(*e);
2890                    true
2891                } else {
2892                    false
2893                }
2894            } else {
2895                true
2896            }
2897        }
2898        Stmt::LocalDef { var, init, .. } if dead(*var) => {
2899            *dropped += 1;
2900            if let Some(e) = init.take() {
2901                if has_side_effects(&e) {
2902                    *x = Stmt::ExprStmt(e);
2903                    return true;
2904                }
2905            }
2906            false
2907        }
2908        _ => true,
2909    });
2910}
2911
2912/// Immutable shallow child walk (mirrors `walk_mut`), for the two-pass
2913/// scope analysis below.
2914fn for_each_child_stmt<'a>(st: &'a Stmt, f: &mut impl FnMut(&'a Stmt)) {
2915    match st {
2916        Stmt::Block(v) => {
2917            for x in v {
2918                f(x);
2919            }
2920        }
2921        Stmt::If {
2922            then_stmt,
2923            else_stmt,
2924            ..
2925        } => {
2926            f(then_stmt);
2927            if let Some(e) = else_stmt {
2928                f(e);
2929            }
2930        }
2931        Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => f(body),
2932        Stmt::For { init, body, .. } => {
2933            for x in init {
2934                f(x);
2935            }
2936            f(body);
2937        }
2938        Stmt::ForEach { body, .. } => f(body),
2939        Stmt::Switch { cases, default, .. } => {
2940            for c in cases {
2941                for x in &c.body {
2942                    f(x);
2943                }
2944            }
2945            if let Some(d) = default {
2946                f(d);
2947            }
2948        }
2949        Stmt::Try {
2950            body,
2951            catches,
2952            finally,
2953        } => {
2954            f(body);
2955            for c in catches {
2956                f(&c.body);
2957            }
2958            if let Some(fl) = finally {
2959                f(fl);
2960            }
2961        }
2962        Stmt::TryWithResources {
2963            resources,
2964            body,
2965            catches,
2966            finally,
2967        } => {
2968            for r in resources {
2969                f(r);
2970            }
2971            f(body);
2972            for c in catches {
2973                f(&c.body);
2974            }
2975            if let Some(fl) = finally {
2976                f(fl);
2977            }
2978        }
2979        Stmt::Synchronized { body, .. } | Stmt::Labeled { body, .. } => f(body),
2980        _ => {}
2981    }
2982}
2983
2984/// Vars occurring in this statement's OWN expression payloads (nested
2985/// sub-statements are descended separately so every occurrence carries
2986/// its innermost block id). ForEach/Catch bound vars are declarations,
2987/// not occurrences.
2988fn stmt_shallow_vars<F: FnMut(u32)>(st: &Stmt, out: &mut F) {
2989    fn ex<F: FnMut(u32)>(e: &Expr, out: &mut F) {
2990        visit_exprs(e, &mut |x| {
2991            if let Expr::Local { var, .. } = x {
2992                out(*var);
2993            }
2994        });
2995    }
2996    match st {
2997        Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
2998            ex(e, out)
2999        }
3000        Stmt::TernaryValue { e } => ex(e, out),
3001        Stmt::Return(Some(e)) => ex(e, out),
3002        Stmt::LocalDef { var, init, .. } => {
3003            out(*var);
3004            if let Some(e) = init {
3005                ex(e, out);
3006            }
3007        }
3008        Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
3009            ex(cond, out)
3010        }
3011        Stmt::For { cond, update, .. } => {
3012            if let Some(c) = cond {
3013                ex(c, out);
3014            }
3015            for u in update {
3016                ex(u, out);
3017            }
3018        }
3019        Stmt::ForEach { iterable, .. } => ex(iterable, out),
3020        Stmt::Switch { selector, cases, .. } => {
3021            ex(selector, out);
3022            for cg in cases {
3023                if let Some(g) = &cg.guard {
3024                    ex(g, out);
3025                }
3026            }
3027        }
3028        Stmt::Assert { cond, msg } => {
3029            ex(cond, out);
3030            if let Some(m) = msg {
3031                ex(m, out);
3032            }
3033        }
3034        Stmt::Synchronized { lock, .. } => ex(lock, out),
3035        _ => {}
3036    }
3037}
3038
3039/// Pass A: number every Block in pre-order (root = 0) and record each
3040/// var's defining block. `sizes[id]` = the id-range span of the subtree.
3041fn scope_pass_a(st: &Stmt, cur: u32, next: &mut u32, def_block: &mut [u32], sizes: &mut Vec<u32>) {
3042    if let Stmt::LocalDef { var, .. } = st {
3043        let i = *var as usize;
3044        if i < def_block.len() && def_block[i] == u32::MAX {
3045            def_block[i] = cur;
3046        }
3047    }
3048    for_each_child_stmt(st, &mut |c| {
3049        if matches!(c, Stmt::Block(_)) {
3050            let id = *next;
3051            *next += 1;
3052            sizes.push(0);
3053            scope_pass_a(c, id, next, def_block, sizes);
3054            sizes[id as usize] = *next - id;
3055        } else {
3056            scope_pass_a(c, cur, next, def_block, sizes);
3057        }
3058    });
3059}
3060
3061/// Pass B (same numbering walk): count each var's occurrences overall
3062/// and within its defining block's subtree.
3063fn scope_pass_b(
3064    st: &Stmt,
3065    cur: u32,
3066    next: &mut u32,
3067    def_block: &[u32],
3068    sizes: &[u32],
3069    within: &mut [u32],
3070    total: &mut [u32],
3071) {
3072    stmt_shallow_vars(st, &mut |v| {
3073        let i = v as usize;
3074        if i < total.len() {
3075            total[i] += 1;
3076            let d = def_block[i];
3077            if d != u32::MAX && d <= cur && cur < d + sizes[d as usize] {
3078                within[i] += 1;
3079            }
3080        }
3081    });
3082    for_each_child_stmt(st, &mut |c| {
3083        if matches!(c, Stmt::Block(_)) {
3084            let id = *next;
3085            *next += 1;
3086            scope_pass_b(c, id, next, def_block, sizes, within, total);
3087        } else {
3088            scope_pass_b(c, cur, next, def_block, sizes, within, total);
3089        }
3090    });
3091}
3092
3093/// Declaration hygiene: every used var that has no LocalDef gets a bare
3094/// declaration at the top; duplicate LocalDefs demote to assignments.
3095/// In an enum's `<clinit>`, every constant's assignment
3096/// (`Self.FIELD = new Self("NAME", ordinal, ...)`) is compiler-mandated
3097/// boilerplate — when the class renders as a true `enum` declaration the
3098/// constants live in the header and these assignments must go. Only
3099/// ACC_ENUM-flagged static fields of the class itself are touched; the
3100/// `$VALUES` array assignment stays (a static block in an enum is legal).
3101pub fn strip_enum_const_inits(s: &mut Stmt, class: &crate::PoolClass) {
3102    let const_fields: jdc_core::FxHashSet<&str> = class
3103        .static_fields
3104        .iter()
3105        .filter(|f| f.access & crate::access::ACC_ENUM != 0)
3106        .map(|f| f.name.as_str())
3107        .collect();
3108    if const_fields.is_empty() {
3109        return;
3110    }
3111    if let Stmt::Block(stmts) = s {
3112        stmts.retain(|st| {
3113            !matches!(
3114                st,
3115                Stmt::ExprStmt(Expr::Assign { target, value, .. })
3116                    if matches!(&**target,
3117                        Expr::Field { cls, name, is_static: true, .. }
3118                            if cls.as_ref() == class.name
3119                                && const_fields.contains(&**name))
3120                        && matches!(&**value, Expr::New { cls: ncls, .. } if ncls.as_ref() == class.name)
3121            )
3122        });
3123    }
3124}
3125
3126/// Java requires the `super(...)`/`this(...)` delegation to be the FIRST
3127/// statement of a constructor. d8/R8 order the outer-reference capture
3128/// (`this.b = p1;`) or other field writes before the invokesuper in the
3129/// bytecode, which lifts as-is into an uncompilable statement order —
3130/// hoist the bare delegation call to position 0 (jadx does the same).
3131pub fn fix_ctor_super_first(body: &mut Stmt) {
3132    let Stmt::Block(stmts) = body else { return };
3133    if stmts.is_empty() || is_bare_ctor_call(stmts.first().unwrap()) {
3134        return;
3135    }
3136    // Top-level delegation call.
3137    if let Some(pos) = stmts.iter().position(is_bare_ctor_call) {
3138        let call = stmts.remove(pos);
3139        stmts.insert(0, call);
3140        return;
3141    }
3142    // The structurer can wrap the delegation in a bare nested block —
3143    // with the parameter null-checks AHEAD of it (weixin's Kotlin
3144    // intrinsics shape: `o.h(parcel, "source"); super();` at inner
3145    // positions 0/1, flattened to straight-line by cleanup AFTER this
3146    // pass, which is why the miss surfaced as 1150 weixin "对super的
3147    // 调用必须是构造器中的第一个语句"). Hoist the call from any
3148    // position whose predecessors are all straight-line statements; a
3149    // delegation behind control flow is the conditional-super family
3150    // (needs restructuring, not hoisting) and stays put.
3151    let mut found: Option<(usize, usize)> = None;
3152    for (i, st) in stmts.iter().enumerate() {
3153        if let Stmt::Block(inner) = st {
3154            if let Some(p) = inner.iter().position(is_bare_ctor_call) {
3155                if inner[..p]
3156                    .iter()
3157                    .all(|s| matches!(s, Stmt::LocalDef { .. } | Stmt::ExprStmt(_)))
3158                {
3159                    found = Some((i, p));
3160                    break;
3161                }
3162            }
3163        }
3164    }
3165    if let Some((i, p)) = found {
3166        let call = match &mut stmts[i] {
3167            Stmt::Block(inner) => inner.remove(p),
3168            _ => unreachable!(),
3169        };
3170        stmts.insert(0, call);
3171    }
3172}
3173
3174/// A true constructor DELEGATION statement: `super(..)` (owner None) or
3175/// `this(..)` (owner This). An un-folded `new X; <init>` (lost
3176/// allocation — the ctor-fold-bug family) arrives as a `<init>` Method
3177/// with a NON-this owner and prints as `new X(..)`: it is not a
3178/// delegation and must neither satisfy nor anchor the super-first fixes
3179/// (v7.a$a2: a dead `new f(0,..)` init at position 0 made both hoists
3180/// believe the delegation was already first, leaving the real `super`
3181/// last — "对super的调用必须是构造器中的第一个语句").
3182fn is_delegation_expr(e: &Expr) -> bool {
3183    matches!(
3184        e,
3185        Expr::Method { name, is_special: true, owner, .. }
3186            if &**name == "<init>"
3187                && (owner.is_none() || matches!(owner.as_deref(), Some(Expr::This)))
3188    )
3189}
3190
3191fn is_bare_ctor_call(s: &Stmt) -> bool {
3192    matches!(s, Stmt::ExprStmt(e) if is_delegation_expr(e))
3193}
3194
3195/// Ctors whose delegation is buried in control flow or behind arg
3196/// computations — the "对super的调用必须是构造器中的第一个语句" family
3197/// that plain hoisting cannot reach (weixin 264 / weibo 116 / reqable 71
3198/// / lark 62 after round 60): R8/Kotlin compute super-args conditionally
3199/// and dex legally invokes super mid-branch.
3200///
3201/// Shape B (branch merge): `prelude; if (c) {d1; super(A1); t1} else
3202/// {d2; super(A2); t2}; post` → one leading `super(..)` where each
3203/// differing arg becomes `c ? a1i : a2i`; branch remainders stay an
3204/// if/else AFTER the call; prelude statements move after it (nothing
3205/// may precede super in Java; bytecode pre-super work is local-only by
3206/// construction — dex forbids instance-field reads before the delegate).
3207///
3208/// Shape A (linear): `defs; super(A); rest` where `A` references the
3209/// def locals — plain hoisting (fix_ctor_super_first case 1) would move
3210/// the call above its argument definitions (forward refs). Inline the
3211/// referenced defs into the args first, drop the consumed defs, hoist.
3212/// Runs BEFORE fix_ctor_super_first and only fires when args reference
3213/// preceding locals, leaving the battle-tested plain hoist untouched
3214/// otherwise.
3215///
3216/// Inlining duplicates the def per use site, so a side-effecting def is
3217/// inlined only when its var's use count is provably preserved: a
3218/// single use in a merged arg (1 copy), or a single use in the
3219/// condition with ≤1 differing arg (the cond then appears in the `if`
3220/// plus one ternary — 2 copies of e.g. one Kotlin getter call, which
3221/// the original bytecode typically made twice anyway: `p.u() == null ?
3222/// .. : p.u().i()`). Pure defs inline freely. Every other shape aborts:
3223/// broken-but-faithful stays the status quo, wrong evaluation counts
3224/// would be worse.
3225pub fn fix_ctor_conditional_super(body: &mut Stmt) {
3226    // A single-statement body can arrive UNWRAPPED (bare `If` — the
3227    // gb6/e throw-guard family); the shape scans need a statement list.
3228    if !matches!(body, Stmt::Block(_)) {
3229        let inner = std::mem::replace(body, Stmt::Block(Vec::new()));
3230        *body = Stmt::Block(vec![inner]);
3231    }
3232    let Stmt::Block(stmts) = body else { return };
3233    // Unwrap single-statement Block wrappers: the structurer can nest
3234    // the whole body one level deeper, hiding the top-level If from the
3235    // shape scans (cleanup would flatten it — but runs AFTER this pass).
3236    while stmts.len() == 1 {
3237        let Stmt::Block(inner) = &stmts[0] else { break };
3238        *stmts = inner.clone();
3239    }
3240    if stmts.is_empty() {
3241        return;
3242    }
3243    if is_bare_ctor_call(stmts.first().unwrap()) {
3244        dedupe_ctor_delegations(body);
3245        return;
3246    }
3247    if try_linear_super_inline(stmts) {
3248        return;
3249    }
3250    try_merge_branched_super(stmts);
3251}
3252
3253/// Shape C: the delegation is already first — strip duplicate copies
3254/// left by R8 path-duplication (Kotlin default-arg bridge ctors: ft5/j
3255/// had `super()` leading PLUS another inside a branch — "对super的调用
3256/// 必须是构造器中的第一个语句" at the copy). Every path delegates
3257/// through the leading call, so structurally-identical (or empty-args,
3258/// same-kind) copies nested in blocks/ifs are removed, never moved.
3259/// Runs both ahead of the shape-A/B attempt and AFTER the plain hoist
3260/// (fix_ctor_super_first) — the hoist is what puts the leading
3261/// delegation in place for path-duplicated ctors.
3262pub fn dedupe_ctor_delegations(body: &mut Stmt) {
3263    let Stmt::Block(stmts) = body else { return };
3264    let Some(d0) = stmts.first().and_then(|s| match s {
3265        Stmt::ExprStmt(e) if is_delegation_expr(e) => Some(e.clone()),
3266        _ => None,
3267    }) else {
3268        return;
3269    };
3270    fn rec(s: &mut Stmt, d0: &Expr) {
3271        match s {
3272            Stmt::Block(v) => {
3273                v.retain(|x| {
3274                    !matches!(x, Stmt::ExprStmt(e) if is_delegation_expr(e) && same_delegation(e, d0))
3275                });
3276                for x in v.iter_mut() {
3277                    rec(x, d0);
3278                }
3279            }
3280            Stmt::If { then_stmt, else_stmt, .. } => {
3281                rec(then_stmt, d0);
3282                if let Some(e) = else_stmt {
3283                    rec(e, d0);
3284                }
3285            }
3286            _ => {}
3287        }
3288    }
3289    for s in stmts.iter_mut().skip(1) {
3290        rec(s, &d0);
3291    }
3292}
3293
3294/// Structural delegation identity for dedup: same kind/target and equal
3295/// args — or both arg-less (`super()` copies across duplicated paths
3296/// render with locally-renamed args yet delegate identically).
3297fn same_delegation(a: &Expr, b: &Expr) -> bool {
3298    match (a, b) {
3299        (
3300            Expr::Method { name: n1, cls: c1, is_super: s1, args: a1, .. },
3301            Expr::Method { name: n2, cls: c2, is_super: s2, args: a2, .. },
3302        ) => {
3303            &**n1 == "<init>"
3304                && &**n2 == "<init>"
3305                && c1 == c2
3306                && s1 == s2
3307                && (a1 == a2 || (a1.is_empty() && a2.is_empty()))
3308        }
3309        _ => false,
3310    }
3311}
3312
3313/// The `Method` expr of a bare ctor-delegation statement.
3314fn ctor_call_expr(s: &Stmt) -> Option<&Expr> {
3315    match s {
3316        Stmt::ExprStmt(e) if is_delegation_expr(e) => Some(e),
3317        _ => None,
3318    }
3319}
3320
3321/// Same delegation kind ignoring args (super vs this, same target).
3322fn same_ctor_kind(a: &Expr, b: &Expr) -> bool {
3323    match (a, b) {
3324        (
3325            Expr::Method { cls: c1, is_super: s1, owner: o1, .. },
3326            Expr::Method { cls: c2, is_super: s2, owner: o2, .. },
3327        ) => c1 == c2 && s1 == s2 && o1 == o2,
3328        _ => false,
3329    }
3330}
3331
3332/// Statements of a branch body, recursively flattening pure Blocks —
3333/// the fix runs BEFORE cleanup, so structurer-emitted nesting is still
3334/// in place. Any non-Block statement (If/Try/...) stays an item and
3335/// fails the def/call classification, which is the intended abort.
3336fn flat_list(s: &Stmt) -> Vec<&Stmt> {
3337    fn rec<'a>(s: &'a Stmt, out: &mut Vec<&'a Stmt>) {
3338        match s {
3339            Stmt::Block(v) => {
3340                for x in v {
3341                    rec(x, out);
3342                }
3343            }
3344            other => out.push(other),
3345        }
3346    }
3347    let mut out = Vec::new();
3348    rec(s, &mut out);
3349    out
3350}
3351
3352/// Blank the OWNER of un-folded `<init>` calls (lost allocation): the
3353/// printer renders them as `new X(..)` and never reads the owner, so its
3354/// register reference is not a real use (v7.a$a2: the dead
3355/// `new f(0,..)`'s owner was v1's register, inflating v1's use count
3356/// and blocking the inline-hoist).
3357fn strip_lost_alloc_owners(e: &mut Expr) {
3358    deep_rewrite(e, &mut |x| {
3359        if let Expr::Method { name, is_special: true, owner: Some(o), .. } = x {
3360            if &**name == "<init>" && !matches!(**o, Expr::This) {
3361                **o = Expr::Const(jdc_core::ir::expr::ConstVal::Int(0));
3362            }
3363        }
3364    });
3365}
3366
3367/// Per-var `Local` occurrence counts over an expression (single pass;
3368/// lost-alloc `<init>` owners excluded — see strip_lost_alloc_owners).
3369fn count_locals_expr(e: &Expr) -> std::collections::HashMap<u32, usize> {
3370    let mut m = std::collections::HashMap::new();
3371    let mut probe = e.clone();
3372    strip_lost_alloc_owners(&mut probe);
3373    deep_rewrite(&mut probe, &mut |x| {
3374        if let Expr::Local { var: v, .. } = x {
3375            *m.entry(*v).or_insert(0) += 1;
3376        }
3377    });
3378    m
3379}
3380
3381fn count_locals_stmts(ss: &[Stmt]) -> std::collections::HashMap<u32, usize> {
3382    let mut m = std::collections::HashMap::new();
3383    for s in ss {
3384        let mut c = s.clone();
3385        walk_stmt_exprs(&mut c, &mut |e| {
3386            strip_lost_alloc_owners(e);
3387            deep_rewrite(e, &mut |x| {
3388                if let Expr::Local { var: v, .. } = x {
3389                    *m.entry(*v).or_insert(0) += 1;
3390                }
3391            });
3392        });
3393    }
3394    m
3395}
3396
3397fn cnt(m: &std::collections::HashMap<u32, usize>, v: u32) -> usize {
3398    m.get(&v).copied().unwrap_or(0)
3399}
3400
3401/// Substitute map-referenced locals, recursively expanding def chains
3402/// (depth-capped). Returns None when a referenced var has no def or the
3403/// chain is too deep.
3404fn inline_locals(
3405    e: &Expr,
3406    map: &std::collections::HashMap<u32, Expr>,
3407    depth: u32,
3408) -> Option<Expr> {
3409    if depth > 4 {
3410        return None;
3411    }
3412    let mut out = e.clone();
3413    let mut fail = false;
3414    deep_rewrite(&mut out, &mut |x| {
3415        if let Expr::Local { var, .. } = x {
3416            if let Some(def) = map.get(var) {
3417                match inline_locals(def, map, depth + 1) {
3418                    Some(r) => *x = r,
3419                    None => fail = true,
3420                }
3421            }
3422        }
3423    });
3424    if fail {
3425        None
3426    } else {
3427        Some(out)
3428    }
3429}
3430
3431/// A leading statement that defines a local: (var, init expr).
3432fn def_of(s: &Stmt) -> Option<(u32, &Expr)> {
3433    match s {
3434        Stmt::LocalDef { var, init: Some(e), .. } => Some((*var, e)),
3435        Stmt::ExprStmt(Expr::Assign {
3436            target,
3437            value,
3438            op: jdc_core::ir::expr::AssignOp::Plain,
3439            ..
3440        }) => match &**target {
3441            Expr::Local { var, .. } => Some((*var, value)),
3442            _ => None,
3443        },
3444        _ => None,
3445    }
3446}
3447
3448/// Shape A: straight-line prefix, delegation references prefix locals.
3449fn try_linear_super_inline(stmts: &mut Vec<Stmt>) -> bool {
3450    let Some(pos) = stmts.iter().position(is_bare_ctor_call) else {
3451        return false;
3452    };
3453    if pos == 0 {
3454        return false; // already first — nothing to do
3455    }
3456    // The prefix must be straight-line.
3457    if !stmts[..pos]
3458        .iter()
3459        .all(|s| matches!(s, Stmt::LocalDef { .. } | Stmt::ExprStmt(_)))
3460    {
3461        return false;
3462    }
3463    let Some(call) = ctor_call_expr(&stmts[pos]) else {
3464        return false;
3465    };
3466    let Expr::Method { args, .. } = call else {
3467        return false;
3468    };
3469    // Defs reachable from the args (last definition wins, bytecode order).
3470    let mut map: std::collections::HashMap<u32, Expr> =
3471        std::collections::HashMap::new();
3472    let mut def_idx: std::collections::HashMap<u32, usize> =
3473        std::collections::HashMap::new();
3474    for (i, s) in stmts[..pos].iter().enumerate() {
3475        if let Some((v, e)) = def_of(s) {
3476            map.insert(v, e.clone());
3477            def_idx.insert(v, i);
3478        }
3479    }
3480    // Which arg locals need inlining?
3481    let mut needed: Vec<u32> = Vec::new();
3482    for a in args {
3483        let mut probe = a.clone();
3484        strip_lost_alloc_owners(&mut probe);
3485        deep_rewrite(&mut probe, &mut |x| {
3486            if let Expr::Local { var, .. } = x {
3487                if map.contains_key(var) && !needed.contains(var) {
3488                    needed.push(*var);
3489                }
3490            }
3491        });
3492    }
3493    if needed.is_empty() {
3494        return false; // plain hoist handles it
3495    }
3496    // Guard: side-effecting defs inline only at a provably preserved
3497    // call count (exactly one use, the arg site).
3498    let mut consumed: Vec<usize> = Vec::new();
3499    let use_counts = count_locals_stmts(stmts);
3500    for &v in &needed {
3501        let def = &map[&v];
3502        let uses = cnt(&use_counts, v);
3503        if jdc_core::ir::build::has_side_effects(def) && uses != 1 {
3504            return false;
3505        }
3506        if uses == 1 {
3507            consumed.push(def_idx[&v]);
3508        }
3509    }
3510    // Build the inlined args.
3511    let mut new_args: Vec<Expr> = Vec::with_capacity(args.len());
3512    for a in args {
3513        match inline_locals(a, &map, 0) {
3514            Some(r) => new_args.push(r),
3515            None => return false,
3516        }
3517    }
3518    // Rebuild: super first (with inlined args), the unconsumed prefix
3519    // statements keep their order after it, then the original rest.
3520    let mut call_stmt = stmts.remove(pos);
3521    if let Stmt::ExprStmt(Expr::Method { args: slot, .. }) = &mut call_stmt {
3522        *slot = new_args;
3523    }
3524    let mut rest: Vec<Stmt> = Vec::with_capacity(stmts.len() + 1);
3525    rest.push(call_stmt);
3526    for (i, s) in stmts.drain(..).enumerate() {
3527        // Indices shift after `remove(pos)`: consumed holds pre-removal
3528        // indices; pos itself is already gone from `stmts`.
3529        let orig = if i >= pos { i + 1 } else { i };
3530        if !consumed.contains(&orig) {
3531            rest.push(s);
3532        }
3533    }
3534    *stmts = rest;
3535    true
3536}
3537
3538/// Shape B: if/else branches each ending in the same-kind delegation.
3539fn try_merge_branched_super(stmts: &mut Vec<Stmt>) -> bool {
3540    // Candidate If positions — the prelude before the chosen If must be
3541    // straight-line; try each If until one merges (nested/multi-if
3542    // ctors exist but the first matching one is the delegation site).
3543    let if_positions: Vec<usize> = stmts
3544        .iter()
3545        .enumerate()
3546        .filter(|(_, s)| matches!(s, Stmt::If { .. }))
3547        .map(|(i, _)| i)
3548        .collect();
3549    for if_pos in if_positions {
3550        // Cheap allocation-free gate: at least one branch must hold a
3551        // delegation (most ctor ifs are field null-checks and must not
3552        // pay the merge machinery). The OTHER side may be delegation-free
3553        // (throw-guard shape D) or absent (then-only shape E — the
3554        // rendered if/else form often only exists after later passes).
3555        let gate = match &stmts[if_pos] {
3556            Stmt::If { then_stmt, else_stmt, .. } => {
3557                contains_delegation(then_stmt)
3558                    || else_stmt.as_ref().is_some_and(|e| contains_delegation(e))
3559            }
3560            _ => false,
3561        };
3562        if gate && merge_at(stmts, if_pos) {
3563            return true;
3564        }
3565    }
3566    false
3567}
3568
3569/// Does this subtree contain a ctor delegation? (module-level: shared by
3570/// the merge gate and split_branch)
3571fn contains_delegation(s: &Stmt) -> bool {
3572    match s {
3573        Stmt::ExprStmt(e) => is_delegation_expr(e),
3574        Stmt::Block(v) => v.iter().any(contains_delegation),
3575        Stmt::If { then_stmt, else_stmt, .. } => {
3576            contains_delegation(then_stmt)
3577                || else_stmt.as_ref().is_some_and(|e| contains_delegation(e))
3578        }
3579        _ => false,
3580    }
3581}
3582
3583
3584
3585/// One merge attempt at `stmts[if_pos]`. See fix_ctor_conditional_super
3586/// for the doctrine. Extensions over the pure two-call merge:
3587/// - branch pre-call SIDE-EFFECT statements (Kotlin `o.h(param,..)`
3588///   null-checks — the zz5/j0 family) are allowed; they move into the
3589///   branch remainder (after super — the same reordering doctrine the
3590///   plain hoist already applies; Java has no pre-super statement form).
3591/// - a branch WITHOUT a delegation (throw-guard: `if (x==null) throw
3592///   .. else { super(..); .. }` — the gb6/e family) merges as pure
3593///   remainder; exactly one branch carries the call then, and no
3594///   ternary args arise (the guard throws before any path needs them).
3595/// - prelude single-assignment if/else (`if (c) {v=e1} else {v=e2}`)
3596///   folds into a Cond def for inlining (the zz5/j0 v15/v16 preludes).
3597fn merge_at(stmts: &mut Vec<Stmt>, if_pos: usize) -> bool {
3598    /// Branch classification: Call(at, defs, pre-call extra idxs) when a
3599    /// delegation sits behind only defs/side-effect statements; Clean
3600    /// when the branch has no delegation at all (pure remainder); Dirty
3601    /// otherwise (nested/conditional delegation — out of scope).
3602    enum Br {
3603        Call(usize, Vec<(u32, Expr)>),
3604        Clean,
3605        Dirty,
3606    }
3607    fn split_branch(list: &[&Stmt]) -> Br {
3608        let call_pos = list.iter().position(|s| ctor_call_expr(s).is_some());
3609        let Some(c) = call_pos else {
3610            // No delegation at this level: a pure remainder branch
3611            // (throw-guard) unless one hides in nested control flow.
3612            return if list.iter().any(|s| contains_delegation(s)) {
3613                Br::Dirty
3614            } else {
3615                Br::Clean
3616            };
3617        };
3618        if list.iter().skip(c + 1).any(|s| ctor_call_expr(s).is_some()) {
3619            return Br::Dirty; // two delegations in one branch
3620        }
3621        let mut defs: Vec<(u32, Expr)> = Vec::new();
3622        for s in list.iter().take(c) {
3623            match def_of(s) {
3624                Some((v, e)) => defs.push((v, e.clone())),
3625                None => match s {
3626                    // Side-effect statements and bare decls ahead of the
3627                    // delegation ride along into the remainder.
3628                    Stmt::ExprStmt(_) | Stmt::LocalDef { .. } => {}
3629                    _ => return Br::Dirty, // control flow pre-call
3630                },
3631            }
3632        }
3633        Br::Call(c, defs)
3634    }
3635    /// Prelude single-assignment if/else → `v = c ? e1 : e2` def.
3636    fn cond_def_of(s: &Stmt) -> Option<(u32, Expr)> {
3637        let Stmt::If { cond, then_stmt, else_stmt: Some(e), .. } = s else {
3638            return None;
3639        };
3640        fn one(b: &Stmt) -> Option<(u32, &Expr)> {
3641            let l = flat_list(b);
3642            if l.len() == 1 {
3643                def_of(l[0])
3644            } else {
3645                None
3646            }
3647        }
3648        let (v1, e1) = one(then_stmt)?;
3649        let (v2, e2) = one(e)?;
3650        if v1 != v2 {
3651            return None;
3652        }
3653        Some((
3654            v1,
3655            Expr::Cond {
3656                c: Box::new(cond.clone()),
3657                t: Box::new(e1.clone()),
3658                f: Box::new(e2.clone()),
3659            },
3660        ))
3661    }
3662
3663    // Prelude: defs (flat or folded cond), bare decls, side-effect stmts.
3664    let mut pmap: std::collections::HashMap<u32, Expr> =
3665        std::collections::HashMap::new();
3666    let mut pidx: std::collections::HashMap<u32, usize> =
3667        std::collections::HashMap::new();
3668    for (i, s) in stmts[..if_pos].iter().enumerate() {
3669        match s {
3670            Stmt::LocalDef { .. } | Stmt::ExprStmt(_) => {
3671                if let Some((v, e)) = def_of(s) {
3672                    pmap.insert(v, e.clone());
3673                    pidx.insert(v, i);
3674                }
3675            }
3676            Stmt::If { .. } => match cond_def_of(s) {
3677                Some((v, e)) => {
3678                    pmap.insert(v, e);
3679                    pidx.insert(v, i);
3680                }
3681                None => return false,
3682            },
3683            _ => return false,
3684        }
3685    }
3686    let (cond, then_v, else_v, had_else) = match &stmts[if_pos] {
3687        Stmt::If { cond, then_stmt, else_stmt, .. } => (
3688            cond.clone(),
3689            then_stmt.as_ref().clone(),
3690            else_stmt.as_ref().map_or_else(|| Stmt::Block(Vec::new()), |e| e.as_ref().clone()),
3691            else_stmt.is_some(),
3692        ),
3693        _ => return false,
3694    };
3695    // Chained guards (Kotlin multi-param null-checks):
3696    // `if(a){throw..} else { if(b){throw..} else { super(..); .. } }` —
3697    // a branch that is a lone delegation-bearing If gets merged
3698    // recursively first (its delegation hoisted to the branch head), so
3699    // the outer split then sees [call, nested-guard-remainder].
3700    fn normalize_branch(b: &mut Stmt) {
3701        // MOVE-based (no deep clone): chained weixin Parcel ctors carry
3702        // huge tails; cloning per recursion level cost measurable wall
3703        // time. merge_at leaves stmts untouched on failure, so the taken
3704        // node can always go back.
3705        let slot: &mut Stmt = match b {
3706            Stmt::Block(v) if v.len() == 1 => &mut v[0],
3707            other => other,
3708        };
3709        if !matches!(slot, Stmt::If { .. }) || !contains_delegation(slot) {
3710            return;
3711        }
3712        let taken = std::mem::replace(slot, Stmt::Block(Vec::new()));
3713        let mut tmp = vec![taken];
3714        if merge_at(&mut tmp, 0) {
3715            *slot = Stmt::Block(tmp);
3716        } else {
3717            *slot = tmp.pop().unwrap();
3718        }
3719    }
3720    let mut then_v = then_v;
3721    let mut else_v = else_v;
3722    normalize_branch(&mut then_v);
3723    normalize_branch(&mut else_v);
3724    let then_list = flat_list(&then_v);
3725    let else_list = flat_list(&else_v);
3726
3727    // Exactly: two Call branches (merge with ternaries) or one Call +
3728    // one Clean (single-delegation throw-guard). Owned branch payloads.
3729    enum Side {
3730        Call { at: usize, defs: Vec<(u32, Expr)> },
3731        Clean,
3732    }
3733    let (t_side, e_side, two_sided) = match (split_branch(&then_list), split_branch(&else_list)) {
3734        (Br::Call(tc, td), Br::Call(ec, ed)) => (
3735            Side::Call { at: tc, defs: td },
3736            Side::Call { at: ec, defs: ed },
3737            true,
3738        ),
3739        (Br::Call(tc, td), Br::Clean) => {
3740            (Side::Call { at: tc, defs: td }, Side::Clean, false)
3741        }
3742        (Br::Clean, Br::Call(ec, ed)) => {
3743            (Side::Clean, Side::Call { at: ec, defs: ed }, false)
3744        }
3745        _ => return false,
3746    };
3747    // The delegation nodes (two-sided: both; single: one).
3748    let (t_call, e_call) = match (&t_side, &e_side) {
3749        (Side::Call { at: ta, .. }, Side::Call { at: ea, .. }) => (
3750            ctor_call_expr(then_list[*ta]),
3751            ctor_call_expr(else_list[*ea]),
3752        ),
3753        (Side::Call { at: ta, .. }, Side::Clean) => {
3754            (ctor_call_expr(then_list[*ta]), None)
3755        }
3756        (Side::Clean, Side::Call { at: ea, .. }) => {
3757            (None, ctor_call_expr(else_list[*ea]))
3758        }
3759        _ => return false,
3760    };
3761    // The node that becomes the merged call: then side when present.
3762    let (call_list, call_at) = match &t_side {
3763        Side::Call { at, .. } => (&then_list, *at),
3764        Side::Clean => match &e_side {
3765            Side::Call { at, .. } => (&else_list, *at),
3766            Side::Clean => return false,
3767        },
3768    };
3769    let (base_args, other_args) = if two_sided {
3770        let (Some(tc), Some(ec)) = (t_call, e_call) else {
3771            return false;
3772        };
3773        if !same_ctor_kind(tc, ec) {
3774            return false;
3775        }
3776        let (Expr::Method { args: a1, .. }, Expr::Method { args: a2, .. }) = (tc, ec) else {
3777            return false;
3778        };
3779        if a1.len() != a2.len() {
3780            return false;
3781        }
3782        (a1, Some(a2))
3783    } else {
3784        let c = t_call.or(e_call);
3785        let Some(c) = c else { return false };
3786        let Expr::Method { args, .. } = c else {
3787            return false;
3788        };
3789        (args, None)
3790    };
3791
3792    // Def maps per side (prelude + own branch defs).
3793    let mut tmap: std::collections::HashMap<u32, Expr> = pmap.clone();
3794    let mut emap: std::collections::HashMap<u32, Expr> = pmap.clone();
3795    if let Side::Call { defs, .. } = &t_side {
3796        for (v, e) in defs {
3797            tmap.insert(*v, e.clone());
3798        }
3799    }
3800    if let Side::Call { defs, .. } = &e_side {
3801        for (v, e) in defs {
3802            emap.insert(*v, e.clone());
3803        }
3804    }
3805    let Some(cond_i) = inline_locals(&cond, &pmap, 0) else {
3806        return false;
3807    };
3808    // Inline each side's args through its own map.
3809    let mut then_args: Vec<Expr> = Vec::with_capacity(base_args.len());
3810    let mut else_args: Vec<Expr> = Vec::new();
3811    if two_sided {
3812        for a in base_args {
3813            match inline_locals(a, &tmap, 0) {
3814                Some(r) => then_args.push(r),
3815                None => return false,
3816            }
3817        }
3818        for a in other_args.unwrap() {
3819            match inline_locals(a, &emap, 0) {
3820                Some(r) => else_args.push(r),
3821                None => return false,
3822            }
3823        }
3824    } else {
3825        // Single-delegation side: base_args belong to whichever side
3826        // carries the call; pick that side's map.
3827        let from_then = matches!(&t_side, Side::Call { .. });
3828        for a in base_args {
3829            let r = if from_then {
3830                inline_locals(a, &tmap, 0)
3831            } else {
3832                inline_locals(a, &emap, 0)
3833            };
3834            match r {
3835                Some(r) => then_args.push(r),
3836                None => return false,
3837            }
3838        }
3839    }
3840    let primary_args = base_args;
3841    // Per-position merge.
3842    let mut merged: Vec<Expr> = Vec::with_capacity(then_args.len());
3843    let mut differ = 0usize;
3844    for (j, t) in then_args.iter().enumerate() {
3845        if two_sided {
3846            let e = &else_args[j];
3847            if t == e {
3848                merged.push(t.clone());
3849            } else {
3850                differ += 1;
3851                merged.push(Expr::Cond {
3852                    c: Box::new(cond_i.clone()),
3853                    t: Box::new(t.clone()),
3854                    f: Box::new(e.clone()),
3855                });
3856            }
3857        } else {
3858            merged.push(t.clone());
3859        }
3860    }
3861
3862    // ---- side-effect preservation guards (batched counts) ----
3863    let vars_in = |e: &Expr| -> Vec<u32> {
3864        let mut vs = Vec::new();
3865        let mut probe = e.clone();
3866        strip_lost_alloc_owners(&mut probe);
3867        deep_rewrite(&mut probe, &mut |x| {
3868            if let Expr::Local { var, .. } = x {
3869                if !vs.contains(var) {
3870                    vs.push(*var);
3871                }
3872            }
3873        });
3874        vs
3875    };
3876    // Remainder statements per branch (extras + post-call tail; a Clean
3877    // branch contributes all of its statements).
3878    let branch_remainder = |list: &[&Stmt], side: &Side| -> Vec<Stmt> {
3879        match side {
3880            Side::Clean => list.iter().map(|s| (*s).clone()).collect(),
3881            Side::Call { at, .. } => {
3882                list.iter()
3883                    .enumerate()
3884                    .filter(|(i, _)| *i != *at)
3885                    .map(|(_, s)| (*s).clone())
3886                    .collect()
3887            }
3888        }
3889    };
3890    let tail_then_v = branch_remainder(&then_list, &t_side);
3891    let tail_else_v = branch_remainder(&else_list, &e_side);
3892    let post = &stmts[if_pos + 1..];
3893    let mut inlined: Vec<u32> = vars_in(&cond);
3894    for a in primary_args.iter().chain(else_args.iter()) {
3895        for v in vars_in(a) {
3896            if !inlined.contains(&v) {
3897                inlined.push(v);
3898            }
3899        }
3900    }
3901    inlined.retain(|v| pmap.contains_key(v) || tmap.contains_key(v) || emap.contains_key(v));
3902    // Prelude-and-branch redefinition: ambiguous provenance — reject.
3903    for &v in &inlined {
3904        let in_branch = match (&t_side, &e_side) {
3905            (Side::Call { defs: td, .. }, Side::Call { defs: ed, .. }) => {
3906                td.iter().any(|(d, _)| *d == v) || ed.iter().any(|(d, _)| *d == v)
3907            }
3908            (Side::Call { defs, .. }, Side::Clean)
3909            | (Side::Clean, Side::Call { defs, .. }) => {
3910                defs.iter().any(|(d, _)| *d == v)
3911            }
3912            _ => false,
3913        };
3914        if pmap.contains_key(&v) && in_branch {
3915            return false;
3916        }
3917    }
3918    type Counts = std::collections::HashMap<u32, usize>;
3919    type GuardCounts = (
3920        Counts,
3921        Vec<Counts>,
3922        Vec<Counts>,
3923        Counts,
3924        Counts,
3925        Counts,
3926        Vec<(u32, Counts)>,
3927    );
3928    let (cond_counts, a1_counts, a2_counts, tail1_counts, tail2_counts, post_counts, def_counts): GuardCounts =
3929        if inlined.is_empty() {
3930        Default::default()
3931    } else {
3932        let all_defs: Vec<(u32, &Expr)> = pmap
3933            .iter()
3934            .map(|(k, v)| (*k, v))
3935            .chain(match &t_side {
3936                Side::Call { defs, .. } => defs.iter().map(|(k, v)| (*k, v)).collect(),
3937                Side::Clean => Vec::new(),
3938            })
3939            .chain(match &e_side {
3940                Side::Call { defs, .. } => defs.iter().map(|(k, v)| (*k, v)).collect(),
3941                Side::Clean => Vec::new(),
3942            })
3943            .collect();
3944        (
3945            count_locals_expr(&cond),
3946            primary_args.iter().map(count_locals_expr).collect(),
3947            else_args.iter().map(count_locals_expr).collect(),
3948            count_locals_stmts(&tail_then_v),
3949            count_locals_stmts(&tail_else_v),
3950            count_locals_stmts(post),
3951            all_defs.into_iter().map(|(v, e)| (v, count_locals_expr(e))).collect(),
3952        )
3953        };
3954    for &v in &inlined {
3955        let in_t = matches!(&t_side, Side::Call { defs, .. } if defs.iter().any(|(d, _)| *d == v));
3956        let in_e = matches!(&e_side, Side::Call { defs, .. } if defs.iter().any(|(d, _)| *d == v));
3957        let effectful = [
3958            in_t.then(|| tmap.get(&v)).flatten(),
3959            in_e.then(|| emap.get(&v)).flatten(),
3960            pmap.get(&v),
3961        ]
3962        .into_iter()
3963        .flatten()
3964        .any(jdc_core::ir::build::has_side_effects);
3965        if !effectful {
3966            continue;
3967        }
3968        let u_cond = cnt(&cond_counts, v);
3969        let u_post = cnt(&post_counts, v);
3970        let u_t1 = cnt(&tail1_counts, v);
3971        let u_t2 = cnt(&tail2_counts, v);
3972        let mut u_odef = 0usize;
3973        for (dv, dc) in &def_counts {
3974            if *dv != v && inlined.contains(dv) {
3975                u_odef += cnt(dc, v);
3976            }
3977        }
3978        let mut emit = u_cond * (1 + differ);
3979        let mut uses_args = 0usize;
3980        for j in 0..primary_args.len() {
3981            let c1 = cnt(&a1_counts[j], v);
3982            let c2 = if two_sided && j < a2_counts.len() {
3983                cnt(&a2_counts[j], v)
3984            } else {
3985                0
3986            };
3987            uses_args += c1 + c2;
3988            emit += if two_sided && then_args[j] != else_args[j] {
3989                c1 + c2
3990            } else {
3991                c1.max(c2)
3992            };
3993        }
3994        let ok = if in_t || in_e {
3995            u_cond == 0
3996                && u_post == 0
3997                && u_t1 == 0
3998                && u_t2 == 0
3999                && u_odef == 0
4000                && a1_counts.iter().map(|c| cnt(c, v)).sum::<usize>() <= 1
4001                && a2_counts.iter().map(|c| cnt(c, v)).sum::<usize>() <= 1
4002                && emit <= 2
4003                && (in_t || a1_counts.iter().all(|c| cnt(c, v) == 0))
4004                && (in_e || a2_counts.iter().all(|c| cnt(c, v) == 0))
4005        } else {
4006            u_post == 0
4007                && u_t1 == 0
4008                && u_t2 == 0
4009                && u_odef == 0
4010                && ((u_cond == 1 && uses_args == 0 && emit <= 2)
4011                    || (u_cond == 0 && emit <= 1))
4012        };
4013        if !ok {
4014            return false;
4015        }
4016    }
4017
4018    // ---- assembly ----
4019    let mut call_stmt = call_list[call_at].clone();
4020    if let Stmt::ExprStmt(Expr::Method { args: slot, .. }) = &mut call_stmt {
4021        *slot = merged;
4022    }
4023    let residual = |v: u32| -> usize {
4024        cnt(&tail1_counts, v) + cnt(&tail2_counts, v) + cnt(&post_counts, v)
4025    };
4026    let mut kept_prelude: Vec<Stmt> = Vec::new();
4027    for (i, s) in stmts[..if_pos].iter().enumerate() {
4028        let consumed = match s {
4029            Stmt::LocalDef { var, .. } => Some(*var),
4030            Stmt::ExprStmt(Expr::Assign { target, .. }) => match &**target {
4031                Expr::Local { var, .. } => Some(*var),
4032                _ => None,
4033            },
4034            Stmt::If { .. } => cond_def_of(s).map(|(v, _)| v),
4035            _ => None,
4036        };
4037        if let Some(v) = consumed {
4038            if inlined.contains(&v) && pidx.get(&v) == Some(&i) && residual(v) == 0 {
4039                continue;
4040            }
4041        }
4042        kept_prelude.push(s.clone());
4043    }
4044    let build_keep = |list: &[&Stmt], side: &Side| -> Vec<Stmt> {
4045        match side {
4046            Side::Clean => list.iter().map(|s| (*s).clone()).collect(),
4047            Side::Call { at, .. } => {
4048                let mut out = Vec::new();
4049                for (i, s) in list.iter().enumerate() {
4050                    if i == *at {
4051                        continue;
4052                    }
4053                    if i < *at {
4054                        if let Some((v, _)) = def_of(s) {
4055                            if inlined.contains(&v) && residual(v) == 0 {
4056                                continue;
4057                            }
4058                        }
4059                    }
4060                    out.push((*s).clone());
4061                }
4062                out
4063            }
4064        }
4065    };
4066    let then_keep = build_keep(&then_list, &t_side);
4067    let else_keep = build_keep(&else_list, &e_side);
4068    let else_part = if else_keep.is_empty() && !had_else {
4069        None
4070    } else {
4071        Some(Box::new(Stmt::Block(else_keep)))
4072    };
4073    let new_if = Stmt::If {
4074        cond: cond_i,
4075        then_stmt: Box::new(Stmt::Block(then_keep)),
4076        else_stmt: else_part,
4077    };
4078    let if_empty = match &new_if {
4079        Stmt::If { then_stmt, else_stmt, .. } => {
4080            let be = |s: &Stmt| matches!(s, Stmt::Block(v) if v.is_empty());
4081            be(then_stmt) && else_stmt.as_ref().is_none_or(|e| be(e))
4082        }
4083        _ => false,
4084    };
4085    let mut out: Vec<Stmt> = Vec::with_capacity(stmts.len());
4086    out.push(call_stmt);
4087    out.extend(kept_prelude);
4088    if !if_empty {
4089        out.push(new_if);
4090    }
4091    out.extend(stmts[if_pos + 1..].to_vec());
4092    *stmts = out;
4093    true
4094}
4095
4096
4097pub fn strip_enum_ctor_super(body: &mut Stmt) {
4098    let is_enum_super = |s: &Stmt| {
4099        matches!(
4100            s,
4101            Stmt::ExprStmt(Expr::Method { name, is_super: true, .. }) if &**name == "<init>"
4102        )
4103    };
4104    let Stmt::Block(stmts) = body else {
4105        return;
4106    };
4107    stmts.retain(|s| !is_enum_super(s));
4108    for st in stmts.iter_mut() {
4109        if let Stmt::Block(inner) = st {
4110            inner.retain(|s| !is_enum_super(s));
4111        }
4112    }
4113}
4114
4115pub fn ensure_declared(body: &mut Stmt, vt: &VarTable) {
4116    // (Perf: dense Vec<bool> tables instead of HashSets — var ids are
4117    // dense; the `assigned` set collected here was never read and cost a
4118    // full extra tree walk per method.)
4119    let n = vt.vars.len().max(1);
4120
4121    // Demote extra LocalDefs (same var declared more than once) FIRST —
4122    // the scope analysis below must see exactly one def per var.
4123    let mut seen: HashSet<u32> = HashSet::default();
4124    demote_dup_decls(body, &mut seen, vt);
4125
4126    // Scope hoist: a LocalDef inside a nested block whose var is also
4127    // referenced OUTSIDE that block is a Java scope violation (the
4128    // branch-local `int v8 = pi.versionCode;` read after the try —
4129    // "cannot find symbol" at every outside use). Demote those defs to
4130    // assignments and let the bare top-of-method declaration below cover
4131    // the var. Two cheap passes: pre-order block numbering + per-var
4132    // inside/outside occurrence counts (pre-order subtree = id range).
4133    let mut def_block: Vec<u32> = vec![u32::MAX; n];
4134    let mut sizes: Vec<u32> = vec![0];
4135    let mut next_id: u32 = 1;
4136    scope_pass_a(body, 0, &mut next_id, &mut def_block, &mut sizes);
4137    sizes[0] = next_id;
4138    let mut within: Vec<u32> = vec![0; n];
4139    let mut total: Vec<u32> = vec![0; n];
4140    let mut next_b: u32 = 1;
4141    scope_pass_b(body, 0, &mut next_b, &def_block, &sizes, &mut within, &mut total);
4142    let mut hoist: HashSet<u32> = HashSet::default();
4143    for v in 0..n {
4144        if def_block[v] != u32::MAX && def_block[v] != 0 && within[v] < total[v] {
4145            hoist.insert(v as u32);
4146        }
4147    }
4148    if !hoist.is_empty() {
4149        let mut seed: HashSet<u32> = hoist.iter().copied().collect();
4150        demote_dup_decls(body, &mut seed, vt);
4151    }
4152
4153    let mut declared: Vec<bool> = vec![false; n];
4154    let mut is_param: Vec<bool> = vec![false; n];
4155    for v in &vt.vars {
4156        if v.is_param && (v.id as usize) < n {
4157            is_param[v.id as usize] = true;
4158        }
4159    }
4160    // Catch parameters are declared by the `catch (Type v)` clause —
4161    // bind_catches consumed their LocalDef before this pass ran, so
4162    // without this they re-declare at the top of the method and clash
4163    // with the clause (`Throwable th2;` + `catch (Throwable th2)` — a
4164    // syntax gate cannot see this, it is a semantic "already defined").
4165    let mut is_catch: Vec<bool> = vec![false; n];
4166    walk_all(body, &mut |st| {
4167        if let Stmt::Try { catches, .. } = st {
4168            for c in catches {
4169                if (c.var as usize) < n {
4170                    is_catch[c.var as usize] = true;
4171                }
4172            }
4173        }
4174    });
4175    walk_all(body, &mut |st| {
4176        if let Stmt::LocalDef { var, .. } = st {
4177            if (*var as usize) < n {
4178                declared[*var as usize] = true;
4179            }
4180        }
4181    });
4182    let mut used: HashSet<u32> = HashSet::default();
4183    // assignments=true: a var that only ever appears as an assign
4184    // TARGET still needs a declaration (`x = 5;` alone does not
4185    // declare x in Java).
4186    stmt_collect_vars(body, &mut used, true);
4187
4188    // Params are declared by the signature; hoisted vars lost their
4189    // LocalDef to the demotion above and always need the bare decl.
4190    let mut needs_decl: Vec<u32> = used
4191        .into_iter()
4192        .filter(|v| {
4193            let i = *v as usize;
4194            i >= n
4195                || ((!declared[i] && !is_param[i] && !is_catch[i]) || hoist.contains(v))
4196        })
4197        .collect();
4198    needs_decl.sort_unstable();
4199    needs_decl.dedup();
4200
4201    if !needs_decl.is_empty() {
4202        let mut decls: Vec<Stmt> = needs_decl
4203            .into_iter()
4204            .map(|v| Stmt::LocalDef {
4205                var: v,
4206                init: None,
4207                is_final: false,
4208                force_type: true,
4209            })
4210            .collect();
4211        decls.reverse();
4212        match body {
4213            Stmt::Block(v) => {
4214                for d in decls {
4215                    v.insert(0, d);
4216                }
4217            }
4218            other => {
4219                let inner = std::mem::replace(other, Stmt::Block(vec![]));
4220                *other = Stmt::Block({
4221                    let mut vs = decls;
4222                    vs.push(inner);
4223                    vs
4224                });
4225            }
4226        }
4227    }
4228}
4229
4230fn demote_dup_decls(s: &mut Stmt, seen: &mut HashSet<u32>, vt: &VarTable) {
4231    match s {
4232        // Exactly one visit per node: the Block arm consumes its children,
4233        // the fallthrough walk would double-visit them.
4234        Stmt::Block(v) => {
4235            for x in v.iter_mut() {
4236                demote_dup_decls(x, seen, vt);
4237            }
4238        }
4239        Stmt::LocalDef { var, init, .. } => {
4240            if seen.contains(var) {
4241                let ty = vt.var(*var).ty.clone();
4242                if let Some(e) = init.take() {
4243                    *s = Stmt::ExprStmt(Expr::Assign {
4244                        target: Box::new(Expr::Local { var: *var, ty }),
4245                        op: AssignOp::Plain,
4246                        value: Box::new(e),
4247                    });
4248                } else {
4249                    *s = Stmt::Block(vec![]);
4250                }
4251            } else {
4252                seen.insert(*var);
4253            }
4254        }
4255        other => {
4256            walk_mut(other, &mut |x| demote_dup_decls(x, seen, vt));
4257        }
4258    }
4259}
4260
4261#[allow(dead_code)]
4262fn unused(_: &Vec<CaseGroup>, _: &Catch) {}
4263
4264// ---------------------------------------------------------------------------
4265// jadx-style local names (ApplyVariableNames + dexdec's port of it)
4266// ---------------------------------------------------------------------------
4267
4268/// A synthetic `v12`/`p3` never survives when a better name exists.
4269/// Priority: (1) a Kotlin `Intrinsics.checkNotNullParameter(x, "name")`
4270/// names `x` from the message string (the string IS the parameter name);
4271/// (2) the single defining call — `getFoo()` → `foo`, `isFinishing()` →
4272/// `finishing`, `new File(…)` → `file`; (3) a type alias or the
4273/// lowercased class simple name. Collisions take `2`, `3`, …; a var with
4274/// two DISAGREEING defining calls stays unnamed (dexdec's
4275/// RelationalNameInference rule). Debug-info names are never touched
4276/// (`synthetic_name` gates the whole pass).
4277pub fn apply_local_names(vt: &mut VarTable, body: &Stmt) {
4278    use std::collections::{HashMap, HashSet};
4279
4280    // ---- proposals ------------------------------------------------------
4281    // var → (name, source-priority); disagreeing call names are dropped.
4282    let mut by_call: HashMap<u32, Vec<String>> = HashMap::new();
4283    let mut by_intrinsics: HashMap<u32, String> = HashMap::new();
4284
4285    visit_all_exprs(body, &mut |e| {
4286        if let Expr::Method { cls, name, args, .. } = e {
4287            if (cls.as_ref() == "kotlin/jvm/internal/Intrinsics"
4288                || cls.as_ref() == "kotlin/jvm/internal/IntrinsicsKt")
4289                && matches!(name.as_ref(), "checkNotNullParameter" | "checkParameterIsNotNull")
4290                && args.len() == 2
4291            {
4292                if let (Expr::Local { var, .. }, Expr::Const(ConstVal::Str(s))) = (&args[0], &args[1])
4293                {
4294                    by_intrinsics
4295                        .entry(*var)
4296                        .or_insert_with(|| sanitize_name(s).unwrap_or_default());
4297                }
4298            }
4299        }
4300    });
4301
4302    walk_all(body, &mut |st| {
4303        let (var, value) = match st {
4304            Stmt::LocalDef { var, init: Some(e), .. } => (*var, e),
4305            Stmt::ExprStmt(Expr::Assign { target, value, .. })
4306                if matches!(&**target, Expr::Local { .. }) =>
4307            {
4308                let Expr::Local { var, .. } = &**target else { return };
4309                (*var, value.as_ref())
4310            }
4311            _ => return,
4312        };
4313        if let Some(n) = defining_call_name(value) {
4314            by_call.entry(var).or_default().push(n);
4315        }
4316    });
4317
4318    // ---- reservation + application --------------------------------------
4319    // Real (debug-info) names can legally REPEAT across sibling source
4320    // scopes (`int i` in two loops) but our flat declaration hoisting
4321    // puts them in one scope — de-duplicate with the same numeric
4322    // suffixes the claim path uses.
4323    // Uniqueness operates on the SANITIZED display name: the identifier
4324    // sanitizer maps non-ASCII to `_`, so distinct debug names
4325    // (`ERROR_token参数缺失` / `ERROR_channelId参数缺失`) both RENDER as
4326    // `ERROR________` and collide as declarations. Tracking raw names
4327    // here would let the collision through.
4328    let sanit = |n: &str| crate::classdec::java_ident(n).into_owned();
4329    let mut taken: HashSet<String> = HashSet::default();
4330    // Parameter names are occupied REGARDLESS of syntheticness: claim()
4331    // hands out rename candidates, and without the synthetic params in
4332    // the pool a type fallback (`l7.p0` → "p0") renamed one parameter
4333    // ONTO another's slot-name (`b(v6.l p0, Object obj, l7.p0 p0)`).
4334    for v in vt.vars.iter() {
4335        // ALL synthetic names occupy their names, not just parameters:
4336        // a type fallback (`pc5.v56` → "v56") otherwise renames a local
4337        // ONTO another local's slot-name (`int v56;` beside
4338        // `pc5.v56 v56;`).
4339        if v.synthetic_name {
4340            taken.insert(sanit(&v.name));
4341        }
4342    }
4343    for v in vt.vars.iter_mut() {
4344        if v.synthetic_name {
4345            continue;
4346        }
4347        if !taken.insert(sanit(&v.name)) {
4348            let base = sanit(&v.name);
4349            for i in 2.. {
4350                let cand = format!("{base}{i}");
4351                if taken.insert(cand.clone()) {
4352                    v.name = cand;
4353                    break;
4354                }
4355            }
4356        }
4357    }
4358    let claim = |taken: &mut HashSet<String>, want: &str| -> String {
4359        let w = crate::classdec::java_ident(want).into_owned();
4360        if taken.insert(w.clone()) {
4361            return w;
4362        }
4363        for i in 2.. {
4364            let cand = format!("{w}{i}");
4365            if taken.insert(cand.clone()) {
4366                return cand;
4367            }
4368        }
4369        w
4370    };
4371
4372    for info in vt.vars.iter_mut() {
4373        if !info.synthetic_name {
4374            continue;
4375        }
4376        // (1) Intrinsics string — the Kotlin compiler wrote the real
4377        // parameter name right into the check.
4378        if let Some(n) = by_intrinsics.get(&info.id) {
4379            if !n.is_empty() {
4380                info.name = claim(&mut taken, n);
4381                continue;
4382            }
4383        }
4384        // (2) One consistent defining call.
4385        if let Some(names) = by_call.get(&info.id) {
4386            let unique: HashSet<&String> = names.iter().collect();
4387            if unique.len() == 1 {
4388                let n = unique.into_iter().next().unwrap();
4389                if !is_java_keyword(n) {
4390                    info.name = claim(&mut taken, n);
4391                    continue;
4392                }
4393            }
4394        }
4395        // (3) Type alias, else the lowercased simple class name
4396        // (jadx names a `Looper` local `looper`).
4397        let want = type_alias(&info.ty).map(str::to_string).or_else(|| simple_type_name(&info.ty));
4398        if let Some(n) = want {
4399            info.name = claim(&mut taken, &n);
4400        }
4401    }
4402
4403    // Final uniquification across ALL vars (params, locals, catch): the
4404    // reservation, claim and debug-name domains above are checked
4405    // pairwise but a name claimed LAST can still equal a debug name
4406    // that never re-checked (`zd1.v2 v2` beside `zd1.v2[] v2`).
4407    let mut seen: HashSet<String> = HashSet::default();
4408    for v in vt.vars.iter_mut() {
4409        if !seen.insert(sanit(&v.name)) {
4410            let base = sanit(&v.name);
4411            for i in 2.. {
4412                let cand = format!("{base}{i}");
4413                if seen.insert(cand.clone()) {
4414                    v.name = cand;
4415                    break;
4416                }
4417            }
4418        }
4419    }
4420}
4421
4422/// `com/android/.../Looper` → `looper` — the alias-table fallback.
4423fn simple_type_name(ty: &TypeRef) -> Option<String> {
4424    let TypeRef::J(JavaType::Object(n)) = ty else {
4425        return None;
4426    };
4427    let simple = n.rsplit('/').next().unwrap_or(n);
4428    let simple = simple.rsplit('$').next().unwrap_or(simple);
4429    // Lowercase the first ALPHABETIC char — `$`/`_`-prefixed names
4430    // (R8's `$$$_Thread`) must not survive capitalized.
4431    let mut chars = simple.chars();
4432    let mut out = String::with_capacity(simple.len());
4433    let mut lowered = false;
4434    for c in chars.by_ref() {
4435        if c.is_ascii_alphabetic() && !lowered {
4436            out.push(c.to_ascii_lowercase());
4437            lowered = true;
4438        } else {
4439            out.push(c);
4440        }
4441        if lowered {
4442            break;
4443        }
4444    }
4445    out.extend(chars);
4446    sanitize_name(&out)
4447}
4448
4449/// `getFoo()` → `foo`, `isFinishing()` → `finishing`, `new File(…)` →
4450/// `file`. The get/is prefixes only strip when the remainder starts
4451/// uppercase (so `issues()` keeps its name).
4452fn defining_call_name(e: &Expr) -> Option<String> {
4453    match e {
4454        Expr::Method { name, .. } => {
4455            let base = name
4456                .strip_prefix("get")
4457                .or_else(|| name.strip_prefix("is"))
4458                .filter(|rest| rest.chars().next().is_some_and(|c| c.is_ascii_uppercase()))
4459                .unwrap_or(name);
4460            let first = base.chars().next()?;
4461            let mut out = String::with_capacity(base.len());
4462            out.push(first.to_ascii_lowercase());
4463            out.extend(base.chars().skip(1));
4464            sanitize_name(&out)
4465        }
4466        Expr::New { cls, .. } => {
4467            let simple = cls.rsplit('/').next().unwrap_or(cls);
4468            let simple = simple.rsplit('$').next().unwrap_or(simple);
4469            let first = simple.chars().next()?;
4470            let mut out = String::with_capacity(simple.len());
4471            out.push(first.to_ascii_lowercase());
4472            out.extend(simple.chars().skip(1));
4473            sanitize_name(&out)
4474        }
4475        _ => None,
4476    }
4477}
4478
4479/// Valid java identifier, not a keyword/restricted name, ≥2 chars.
4480fn sanitize_name(s: &str) -> Option<String> {
4481    let mut out = String::with_capacity(s.len());
4482    for c in s.chars() {
4483        if c.is_ascii_alphanumeric() || c == '_' || c == '$' {
4484            out.push(c);
4485        } else {
4486            out.push('_');
4487        }
4488    }
4489    if out.len() < 2
4490        || out.chars().next().is_some_and(|c| c.is_ascii_digit())
4491        || is_java_keyword(&out)
4492    {
4493        return None;
4494    }
4495    Some(out)
4496}
4497
4498/// jadx's alias table for the common types, with the Android globals the
4499/// corpus actually shows; anything else falls back in the caller.
4500fn type_alias(ty: &TypeRef) -> Option<&'static str> {
4501    let TypeRef::J(JavaType::Object(n)) = ty else {
4502        return None;
4503    };
4504    Some(match n.as_ref() {
4505        "java/lang/String" | "kotlin/String" => "str",
4506        "java/lang/Class" => "cls",
4507        "java/lang/Throwable" => "th",
4508        "java/lang/Object" | "kotlin/Any" => "obj",
4509        "java/util/Iterator" | "kotlin/collections/Iterator" => "it",
4510        "java/lang/Boolean" => "bool",
4511        "java/lang/Integer" => "num",
4512        "java/lang/Character" => "ch",
4513        "java/lang/Byte" => "b",
4514        "java/lang/Short" => "sh",
4515        "java/lang/Float" => "f",
4516        "java/lang/Double" => "d",
4517        "java/lang/Long" => "i",
4518        "java/lang/StringBuilder" => "sb",
4519        "java/util/ArrayList" => "list",
4520        "java/util/HashMap" => "map",
4521        "android/content/Context" => "context",
4522        "android/content/Intent" => "intent",
4523        "android/os/Bundle" => "bundle",
4524        "android/view/View" => "view",
4525        "android/graphics/Bitmap" => "bitmap",
4526        "android/view/ViewGroup" => "viewGroup",
4527        _ => return None,
4528    })
4529}
4530
4531/// Every expression in the tree, statements included (read-only).
4532fn visit_all_exprs<F: FnMut(&Expr)>(s: &Stmt, f: &mut F) {
4533    walk_all(s, &mut |st| {
4534        let exprs: Vec<&Expr> = match st {
4535            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
4536                let mut v: Vec<&Expr> = vec![value];
4537                if !matches!(&**target, Expr::Local { .. }) {
4538                    v.push(target);
4539                }
4540                v
4541            }
4542            Stmt::ExprStmt(e) | Stmt::Throw(e) | Stmt::MonitorEnter(e) | Stmt::MonitorExit(e) => {
4543                vec![e]
4544            }
4545            Stmt::Return(Some(e)) => vec![e],
4546            Stmt::LocalDef { init: Some(e), .. } => vec![e],
4547            Stmt::If { cond, .. } | Stmt::While { cond, .. } | Stmt::DoWhile { cond, .. } => {
4548                vec![cond]
4549            }
4550            _ => Vec::new(),
4551        };
4552        for e in exprs {
4553            visit_exprs(e, f);
4554        }
4555    });
4556}
4557
4558/// IntDef/LongDef exact-match rendering: a literal argument to a
4559/// platform method whose parameter carries a constant domain renders as
4560/// the named constant (`setVisibility(8)` → `android.view.View.GONE`).
4561/// Combined flag values and literals outside the domain stay numeric.
4562pub fn platform_constants(body: &mut Stmt) {
4563    if !crate::platform::installed() {
4564        return;
4565    }
4566    rewrite_exprs(body, &mut |e| {
4567        if let Expr::Method { cls, name, desc, args, .. } = e {
4568            for a in args.iter_mut() {
4569                let (value, is_long) = match a {
4570                    Expr::Const(ConstVal::Int(v)) => (*v as i64, false),
4571                    Expr::Const(ConstVal::Long(v)) => (*v, true),
4572                    _ => continue,
4573                };
4574                if let Some(named) =
4575                    crate::platform::named_constant(cls, name, &desc_to_string(desc), 0, value)
4576                {
4577                    let _ = is_long;
4578                    *a = Expr::Raw(named);
4579                }
4580            }
4581        }
4582    });
4583}
4584
4585/// MethodDescriptor → dex descriptor string (`(I)V`).
4586fn desc_to_string(d: &jdc_core::types::MethodDescriptor) -> String {
4587    let mut out = String::from("(");
4588    for a in &d.args {
4589        type_to_desc(a, &mut out);
4590    }
4591    out.push(')');
4592    type_to_desc(&d.ret, &mut out);
4593    out
4594}
4595
4596fn type_to_desc(t: &jdc_core::types::JavaType, out: &mut String) {
4597    match t {
4598        JavaType::Void => out.push('V'),
4599        JavaType::Boolean => out.push('Z'),
4600        JavaType::Byte => out.push('B'),
4601        JavaType::Char => out.push('C'),
4602        JavaType::Short => out.push('S'),
4603        JavaType::Int => out.push('I'),
4604        JavaType::Float => out.push('F'),
4605        JavaType::Long => out.push('J'),
4606        JavaType::Double => out.push('D'),
4607        JavaType::Object(n) => {
4608            out.push('L');
4609            out.push_str(n);
4610            out.push(';');
4611        }
4612        JavaType::Array(inner) => {
4613            out.push('[');
4614            type_to_desc(inner, out);
4615        }
4616    }
4617}
4618
4619/// Kotlin `Intrinsics` null-check elision (jadx's ProcessKotlinInternals):
4620/// statement-position `checkNotNullParameter(p, "name")` /
4621/// `checkParameterIsNotNull` / `checkNotNull…` calls are runtime
4622/// assertions — drop them. The naming pass harvests the parameter
4623/// strings FIRST, so run this after `apply_local_names`.
4624pub fn remove_kotlin_checks(s: &mut Stmt) {
4625    walk_mut_deep(s, &mut |st| {
4626        let Stmt::Block(v) = st else { return };
4627        v.retain(|x| !is_kotlin_check(x));
4628    });
4629}
4630
4631fn is_kotlin_check(s: &Stmt) -> bool {
4632    let Stmt::ExprStmt(e) = s else { return false };
4633    let Expr::Method { cls, name, .. } = e else { return false };
4634    (cls.as_ref() == "kotlin/jvm/internal/Intrinsics" || cls.as_ref() == "kotlin/jvm/internal/IntrinsicsKt__Jdk7Kt")
4635        && matches!(
4636            name.as_ref(),
4637            "checkNotNullParameter"
4638                | "checkParameterIsNotNull"
4639                | "checkExpressionValueIsNotNull"
4640                | "checkNotNullExpressionValue"
4641                | "checkReturnedValueIsNotNull"
4642                | "checkFieldIsNotNull"
4643                | "checkNotNull"
4644        )
4645}
4646
4647// ---------------------------------------------------------------------------
4648// Synthetic-accessor inlining (jadx's MarkMethodsForInline, the safe subset)
4649// ---------------------------------------------------------------------------
4650
4651/// Inline `access$NNN`-style synthetic static bridges at their call
4652/// sites: an identity (`return pN`), a getter (`iget pX, field;
4653/// return vR`), or a method forwarder (`invoke; return`) — with the
4654/// d8 APM trace wrappers (`MethodCollector.i/.o` const-only static
4655/// calls) tolerated around the core. Only STATIC + SYNTHETIC callees
4656/// qualify (compiler-generated bridges: no override semantics, no
4657/// side effects beyond the forwarded operation).
4658pub fn inline_accessors(s: &mut Stmt, pool: &DexPool) {
4659    rewrite_exprs(s, &mut |e| {
4660        let Expr::Method { cls, name, desc, args, is_static, .. } = e else { return };
4661        if !*is_static || args.is_empty() {
4662            return;
4663        }
4664        let Some(target) = pool.get(cls) else { return };
4665        let desc_s = desc_to_string(desc);
4666        let Some(m) = target.find_method(name, &desc_s) else { return };
4667        if !(m.is_static() && m.access & access::ACC_SYNTHETIC != 0) {
4668            return;
4669        }
4670        let Some(dex) = pool.dex(m.dex_idx) else { return };
4671        // Snapshot first: the accessor's owning image may already be
4672        // retired, and a live read's success would depend on worker
4673        // completion interleaving — nondeterministic inlining (whether
4674        // the accessor folds at all) between identical runs.
4675        let code = match pool.accessor_code(m.dex_idx, m.code_off) {
4676            Some(bytes) => ddc_dex::CodeItem::parse(&bytes, 0),
4677            None => dex.code_at(m.code_off),
4678        };
4679        let Some(code) = code else { return };
4680        let insns: Vec<&Insn> =
4681            code.insns.iter().filter(|i| !matches!(i.kind, InsnKind::Nop)).collect();
4682        // Strip the APM trace wrappers (const-only invoke-static).
4683        let core: Vec<&Insn> = insns
4684            .iter()
4685            .copied()
4686            .filter(|i| !is_const_only_trace(&i.kind, &insns))
4687            .collect();
4688        match accessor_shape(&dex, &core, &code) {
4689            Some(Shape::Identity(arg_i)) => {
4690                if let Some(a) = args.get(arg_i) {
4691                    *e = a.clone();
4692                }
4693            }
4694            Some(Shape::FieldRead { arg, cls: field_cls, field, field_ty }) => {
4695                if let Some(owner) = args.get(arg).cloned().map(Box::new) {
4696                    let ty = TypeRef::J(desc_type(&field_ty));
4697                    *e = Expr::Field {
4698                        owner: Some(owner),
4699                        cls: field_cls.into(),
4700                        name: field.into(),
4701                        ty,
4702                        is_static: false,
4703                    };
4704                }
4705            }
4706            Some(Shape::Forward { cls: tcls, name: tname, desc: tdesc, instance }) => {
4707                *cls = tcls.into();
4708                *name = tname.into();
4709                *desc = std::sync::Arc::new(tdesc);
4710                if instance && !args.is_empty() {
4711                    // The first param (the receiver) becomes the owner.
4712                    let recv = args.remove(0);
4713                    if let Expr::Method { owner, .. } = e {
4714                        *owner = Some(Box::new(recv));
4715                    }
4716                }
4717            }
4718            None => {}
4719        }
4720    });
4721}
4722
4723/// What an accessor body reduces to.
4724enum Shape {
4725    /// `return pN` — the call becomes argument N.
4726    Identity(usize),
4727    /// `iget vR, pX, field; return vR` — becomes `argN.field`.
4728    FieldRead { arg: usize, cls: String, field: String, field_ty: String },
4729    /// `invoke {pX, args…}, method@M; (move-result;)? return` —
4730    /// becomes the forwarded call.
4731    Forward { cls: String, name: String, desc: MethodDescriptor, instance: bool },
4732}
4733
4734/// Resolve the IGet field's owner class, name and type descriptor.
4735fn field_of(dex: &ddc_dex::DexFile, field_idx: u32) -> Option<(String, String, String)> {
4736    let f = dex.field(field_idx);
4737    Some((
4738        // class_name strips the `L...;` shell; type_name would leak a
4739        // descriptor into the Field's owner class (`La3.a.e_`).
4740        dex.class_name(f.class_idx),
4741        dex.string(f.name_idx).to_string(),
4742        // The FIELD TYPE stays a descriptor: desc_type parses it.
4743        dex.type_name(f.type_idx).to_string(),
4744    ))
4745}
4746
4747/// Map a register to a parameter index (static method: params occupy
4748/// the LAST ins_size registers).
4749fn param_index(code: &ddc_dex::CodeItem, reg: u16) -> Option<usize> {
4750    let rs = code.registers_size as usize;
4751    let ins = code.ins_size as usize;
4752    let r = reg as usize;
4753    let first = rs.checked_sub(ins)?;
4754    (r >= first && r < rs).then(|| r - first)
4755}
4756
4757fn accessor_shape(
4758    dex: &ddc_dex::DexFile,
4759    core: &[&Insn],
4760    code: &ddc_dex::CodeItem,
4761) -> Option<Shape> {
4762    // Dead consts that fed the stripped trace wrappers remain in the
4763    // core — skip them.
4764    let core: &[&Insn] = match core.split_first() {
4765        Some((first, rest)) if matches!(first.kind, InsnKind::Const { .. }) => rest,
4766        _ => core,
4767    };
4768    let core: &[&Insn] = match core.split_last() {
4769        Some((last, rest)) if matches!(last.kind, InsnKind::Const { .. }) => rest,
4770        _ => core,
4771    };
4772    match core {
4773        // return pN
4774        [Insn { kind: InsnKind::Return { src }, .. }] => {
4775            let idx = param_index(code, *src)?;
4776            Some(Shape::Identity(idx))
4777        }
4778        // iget vR, pX, field; return vR
4779        [
4780            Insn { kind: InsnKind::IGet { dst, obj, field_idx }, .. },
4781            Insn { kind: InsnKind::Return { src }, .. },
4782        ] if dst == src => {
4783            let arg = param_index(code, *obj)?;
4784            let (cls, field, field_ty) = field_of(dex, *field_idx)?;
4785            Some(Shape::FieldRead { arg, cls, field, field_ty })
4786        }
4787        // invoke {…}, method@M; (move-result; return)? — forwarder.
4788        [Insn { kind: InsnKind::Invoke { method_idx, regs, kind, .. }, .. }, rest @ ..] if rest.len() <= 2 => {
4789            // Only forward when every register is a param (no locals).
4790            if !regs.iter().all(|r| param_index(code, *r).is_some()) || regs.is_empty() {
4791                return None;
4792            }
4793            // Trailing must be move-result+return or return/void —
4794            // anything else (e.g. trace calls) already filtered upstream.
4795            if rest.len() == 2 {
4796                let ok = matches!(rest[0].kind, InsnKind::MoveResult { .. })
4797                    && matches!(rest[1].kind, InsnKind::Return { .. } | InsnKind::ReturnVoid);
4798                if !ok {
4799                    return None;
4800                }
4801            }
4802            let mid = dex.method(*method_idx);
4803            // class_name strips the `L...;` shell — type_name leaks a
4804            // descriptor into the forwarded call's class (`La3.a.e_`).
4805            let cls = dex.class_name(mid.class_idx);
4806            let name = dex.string(mid.name_idx).to_string();
4807            let mut s = String::from("(");
4808            for t in dex.proto_params(mid.proto_idx) {
4809                s.push_str(dex.type_name(*t));
4810            }
4811            s.push(')');
4812            s.push_str(dex.type_name(dex.proto(mid.proto_idx).return_type_idx));
4813            let desc = parse_method_descriptor(&s)?;
4814            Some(Shape::Forward { cls, name, desc, instance: !matches!(kind, InvokeKind::Static) })
4815        }
4816        _ => None,
4817    }
4818}
4819
4820
4821/// A const-only invoke-static (the d8 APM `i(732046)` / `o(732046)`
4822/// trace wrapper): every argument register is loaded by a Const (or a
4823/// Const-into-move chain) in the SAME body. Scoped to synthetic
4824/// accessors, so a genuinely side-effecting const call is never
4825/// mistaken for a trace.
4826fn is_const_only_trace(kind: &InsnKind, insns: &[&Insn]) -> bool {
4827    let InsnKind::Invoke { kind, regs, .. } = kind else { return false };
4828    if !matches!(kind, InvokeKind::Static) || regs.is_empty() {
4829        return false;
4830    }
4831    regs.iter().all(|r| const_loaded(*r, insns, 0))
4832}
4833
4834/// Const-loaded directly, or through a Move chain from a const-loaded
4835/// register (the APM `const v0, id` → `invoke {v0}` shape).
4836fn const_loaded(r: u16, insns: &[&Insn], depth: u8) -> bool {
4837    if depth > 3 {
4838        return false;
4839    }
4840    insns.iter().any(|i| match &i.kind {
4841        InsnKind::Const { dst, .. } => *dst == r,
4842        InsnKind::Move { dst, src } => *dst == r && const_loaded(*src, insns, depth + 1),
4843        _ => false,
4844    })
4845}