Skip to main content

ddc_dec/
lift.rs

1//! Register-machine expression lifting: Dalvik instructions → jdc-core IR.
2//!
3//! The register file is modeled as per-register *views* of values:
4//! * `Live(v)` — the register equals local `v`, whose defining statement was
5//!   already emitted (SSA-ish: every materialization allocates a fresh var,
6//!   so a `Local` reference can never go stale);
7//! * `Pending(e)` — the register holds the expression `e`, whose store is
8//!   deferred so consumers can nest it (`return a + b`);
9//! * `PendingCall(e)` — the value of a just-executed invoke /
10//!   filled-new-array whose statement has not been emitted yet: consumed
11//!   exactly once (a materialization, a term, or a discarded-value
12//!   statement), never duplicated;
13//! * `WideHi` — the high half of a J/D pair.
14//!
15//! At block exit the terminator is built FIRST (terms may carry pending
16//! call expressions: `return foo();`), then unconsumed calls become
17//! statements and impure pendings materialize, so cross-block states only
18//! carry pure values, locals, and open `new`/`new-array` views.
19
20use ddc_dex::insn::{ArithOp, CmpKind, CmpOp, Insn, InsnKind, InvokeKind, Payload};
21use ddc_dex::{CodeItem, DexFile};
22use jdc_core::ir::build::{has_side_effects, BlockResult, SwitchTargets, Term};
23use jdc_core::ir::expr::{AssignOp, BinOp, ConcatPart, ConstVal, Expr, TypeRef, UnOp};
24use jdc_core::ir::stmt::Stmt;
25use jdc_core::types::{JavaType, MethodDescriptor};
26use jdc_core::var::{VarInfo, VarTable};
27
28use crate::DexPool;
29
30pub type BResult<T> = Result<T, String>;
31
32/// Cheap per-method feature flags accumulated by the lifter: which
33/// post-lift passes could possibly match anything. Most methods use none of
34/// these — gating their passes skips full-tree analysis walks.
35#[derive(Clone, Copy, Default, PartialEq)]
36pub struct MethodFlags(u8);
37
38impl MethodFlags {
39    const SB: u8 = 1;
40    const MONITOR: u8 = 2;
41    const CMP: u8 = 4;
42    pub fn none() -> MethodFlags {
43        MethodFlags(0)
44    }
45    pub fn with_sb(self) -> MethodFlags {
46        MethodFlags(self.0 | Self::SB)
47    }
48    pub fn with_monitor(self) -> MethodFlags {
49        MethodFlags(self.0 | Self::MONITOR)
50    }
51    pub fn with_cmp(self) -> MethodFlags {
52        MethodFlags(self.0 | Self::CMP)
53    }
54    pub fn has_sb(self) -> bool {
55        self.0 & Self::SB != 0
56    }
57    pub fn has_monitor(self) -> bool {
58        self.0 & Self::MONITOR != 0
59    }
60    pub fn has_cmp(self) -> bool {
61        self.0 & Self::CMP != 0
62    }
63    pub fn merge(self, o: MethodFlags) -> MethodFlags {
64        MethodFlags(self.0 | o.0)
65    }
66}
67
68/// Everything one method body needs from its DEX image.
69pub struct MethodEnv<'a> {
70    pub pool: &'a DexPool,
71    /// Image index of `dex` in the pool (keys the pool's ref caches).
72    pub di: usize,
73    pub dex: &'a DexFile,
74    pub code: &'a CodeItem,
75    pub class_name: String,
76    pub method_name: std::sync::Arc<str>,
77    pub desc: MethodDescriptor,
78    /// Debug-info local table (empty for release/stripped builds):
79    /// real source names per (register, pc-range).
80    pub debug_locals: Vec<ddc_dex::DebugLocal>,
81    pub is_static: bool,
82    /// Total code units — the CFG owns the insns stream, so the lifter reads
83    /// the extent from here instead of `code.insns` (hollowed after the
84    /// CFG build moved it).
85    pub code_units: u32,
86}
87
88impl<'a> MethodEnv<'a> {
89    pub fn type_name(&self, idx: u32) -> String {
90        self.dex.type_name(idx).to_string()
91    }
92    pub fn java_type(&self, idx: u32) -> JavaType {
93        // Shared per-image parse (was desc-reparse per instruction).
94        (*self.pool.type_java(self.di, idx)).clone()
95    }
96    pub fn type_name_arc(&self, idx: u32) -> std::sync::Arc<str> {
97        self.pool.type_name_arc(self.di, idx)
98    }
99    pub fn field_ref(
100        &self,
101        idx: u32,
102    ) -> (
103        std::sync::Arc<str>,
104        std::sync::Arc<str>,
105        JavaType,
106    ) {
107        let f = self.dex.field(idx);
108        (
109            std::sync::Arc::from(self.dex.class_name(f.class_idx).as_str()),
110            std::sync::Arc::from(self.dex.string(f.name_idx)),
111            self.java_type(f.type_idx),
112        )
113    }
114    pub fn method_ref(
115        &self,
116        idx: u32,
117    ) -> (
118        std::sync::Arc<str>,
119        std::sync::Arc<str>,
120        std::sync::Arc<MethodDescriptor>,
121    ) {
122        // Was: N param Strings + format! + descriptor RE-PARSE per invoke
123        // (6-10 allocations per call instruction). All three components
124        // are shared table entries now — refcount bumps.
125        let m = self.dex.method(idx);
126        (
127            std::sync::Arc::from(self.dex.class_name(m.class_idx).as_str()),
128            std::sync::Arc::from(self.dex.string(m.name_idx)),
129            self.pool.proto_desc_parsed(self.di, m.proto_idx),
130        )
131    }
132}
133
134/// Per-register value view (see module docs).
135#[derive(Debug, Clone, PartialEq)]
136pub enum Reg {
137    Undef,
138    Live(u32),
139    Pending(Expr),
140    PendingCall(Expr),
141    WideHi,
142}
143
144/// Block-exit register state (machine-neutral view plus origin tracking).
145#[derive(Debug, Clone)]
146pub struct OutState {
147    pub regs: Vec<Reg>,
148    /// Per register: the pc of the instruction that produced its value
149    /// (orders merge materializations by original instruction order).
150    pub write_pc: Vec<u32>,
151}
152
153/// The registers an instruction READS as operands (receivers, arguments,
154/// move sources — destination registers excluded). Drives the block
155/// lookahead that decides when an allocation view must materialize.
156fn src_regs(kind: &InsnKind) -> Vec<u16> {
157    let mut out: Vec<u16> = Vec::with_capacity(4);
158    let mut push = |r: u16| out.push(r);
159    match kind {
160        InsnKind::Nop
161        | InsnKind::ReturnVoid
162        | InsnKind::Unknown
163        | InsnKind::NewInstance { .. }
164        | InsnKind::SGet { .. } => {}
165        InsnKind::Move { src, .. }
166        | InsnKind::Un { src, .. }
167        | InsnKind::Return { src }
168        | InsnKind::ArrayLength { src, .. } => push(*src),
169        InsnKind::MonitorEnter { reg }
170        | InsnKind::MonitorExit { reg }
171        | InsnKind::Throw { reg }
172        | InsnKind::CheckCast { reg, .. }
173        | InsnKind::FillArrayData { reg, .. }
174        | InsnKind::PackedSwitch { reg, .. }
175        | InsnKind::SparseSwitch { reg, .. } => push(*reg),
176        InsnKind::MoveResult { .. } | InsnKind::MoveException { .. } => {}
177        InsnKind::Const { .. }
178        | InsnKind::ConstClass { .. }
179        | InsnKind::ConstString { .. }
180        | InsnKind::ConstMethodHandle { .. }
181        | InsnKind::ConstMethodType { .. } => {}
182        InsnKind::InstanceOf { src, .. } => {
183            push(*src);
184        }
185        InsnKind::NewArray { size, .. } => {
186            push(*size);
187        }
188        InsnKind::FilledNewArray { regs, .. } | InsnKind::InvokeCustom { regs, .. } => {
189            for r in regs {
190                push(*r);
191            }
192        }
193        InsnKind::Goto { .. } => {}
194        InsnKind::Cmp { a, b, .. } | InsnKind::Bin { a, b, .. } => {
195            push(*a);
196            push(*b);
197        }
198        InsnKind::BinLit { a, .. } => push(*a),
199        InsnKind::If { a, b, z, .. } => {
200            push(*a);
201            if !*z {
202                push(*b);
203            }
204        }
205        InsnKind::AGet { array, index, .. } => {
206            push(*array);
207            push(*index);
208        }
209        InsnKind::APut {
210            value,
211            array,
212            index,
213            ..
214        } => {
215            push(*value);
216            push(*array);
217            push(*index);
218        }
219        InsnKind::IGet { obj, .. } => {
220            push(*obj);
221        }
222        InsnKind::IPut { value, obj, .. } => {
223            push(*value);
224            push(*obj);
225        }
226        InsnKind::SPut { value, .. } => {
227            push(*value);
228        }
229        InsnKind::Invoke { regs, .. } => {
230            for r in regs {
231                push(*r);
232            }
233        }
234    }
235    out
236}
237
238/// Registers an instruction READS as operands.
239/// Registers an instruction DEFINES (new value generations).
240fn dst_regs(kind: &InsnKind) -> Vec<u16> {
241    let mut out: Vec<u16> = Vec::with_capacity(2);
242    let mut push = |r: u16| out.push(r);
243    match kind {
244        InsnKind::Move { dst, .. }
245        | InsnKind::MoveResult { dst }
246        | InsnKind::MoveException { dst }
247        | InsnKind::Const { dst, .. }
248        | InsnKind::ConstClass { dst, .. }
249        | InsnKind::ConstString { dst, .. }
250        | InsnKind::ConstMethodHandle { dst, .. }
251        | InsnKind::ConstMethodType { dst, .. }
252        | InsnKind::NewInstance { dst, .. }
253        | InsnKind::NewArray { dst, .. }
254        | InsnKind::InstanceOf { dst, .. }
255        | InsnKind::ArrayLength { dst, .. }
256        | InsnKind::Cmp { dst, .. }
257        | InsnKind::AGet { dst, .. }
258        | InsnKind::IGet { dst, .. }
259        | InsnKind::SGet { dst, .. }
260        | InsnKind::Un { dst, .. }
261        | InsnKind::Bin { dst, .. }
262        | InsnKind::BinLit { dst, .. } => push(*dst),
263        _ => {}
264    }
265    out
266}
267
268/// The (pc, reg) reads that are the final read of the register's current
269/// generation: walking the register's events in pc order, a read is
270/// final when no other read of the same register precedes its next WRITE
271/// (a write starts a new generation — the old value is dead there).
272fn compute_final_reads(ins: &[Insn]) -> jdc_core::FxHashSet<(u32, u16)> {
273    use jdc_core::FxHashMap as HashMap;
274    // reg → (pc, is_read) events, pc order (ins is already pc-sorted).
275    let mut events: HashMap<u16, Vec<(u32, bool)>> = HashMap::default();
276    for i in ins {
277        for r in src_regs(&i.kind) {
278            events.entry(r).or_default().push((i.pc, true));
279        }
280        for r in dst_regs(&i.kind) {
281            events.entry(r).or_default().push((i.pc, false));
282        }
283    }
284    let mut out = jdc_core::FxHashSet::default();
285    for (reg, evs) in events {
286        let mut dedup: Vec<(u32, bool)> = Vec::with_capacity(evs.len());
287        for (pc, is_read) in evs {
288            if dedup.last() != Some(&(pc, is_read)) {
289                dedup.push((pc, is_read));
290            }
291        }
292        for (i, &(pc, is_read)) in dedup.iter().enumerate() {
293            if !is_read {
294                continue;
295            }
296            // Final read of the current generation: no other read before
297            // the register's next write.
298            let mut final_ = true;
299            if let Some(&(_, is_read2)) = dedup.get(i + 1) {
300                if is_read2 {
301                    final_ = false;
302                }
303            }
304            if final_ {
305                out.insert((pc, reg));
306            }
307        }
308    }
309    out
310}
311
312pub struct Lifter<'a> {
313    pub env: &'a MethodEnv<'a>,
314    pub vt: &'a mut VarTable,
315    /// Owning block id (stable-var registry key).
316    block_id: usize,
317    /// (block, register) → var id, shared across REBUILDS: a re-lift of
318    /// the same block with the same input reuses the same materialized
319    /// variables, so the output state is id-stable. Without this, every
320    /// rebuild allocated fresh ids, the out-state compared unequal, and
321    /// the worklist cascaded re-queuing until the visit cap (~60
322    /// rebuilds per block, 30M lifts on weibo).
323    stable: &'a mut jdc_core::FxHashMap<(usize, u16, u64), u32>,
324    regs: Vec<Reg>,
325    write_pc: Vec<u32>,
326    stmts: Vec<Stmt>,
327    pending_call: Option<Expr>,
328    code_units: u32,
329    /// Current instruction's pc (drives the allocation materialization
330    /// lookahead below).
331    cur_pc: u32,
332    /// (pc, register) reads that are the FINAL read of the register's
333    /// current value generation: no other read before the register's
334    /// next write. An allocation view (post-fold `New` / `NewArray`)
335    /// may only be inlined at a final read; a register reused for a
336    /// call result and read again (greet → move-result v0 → println(v0))
337    /// is a new generation, not another use of the allocation.
338    final_read: jdc_core::FxHashSet<(u32, u16)>,
339    /// Method-level feature flags, merged in place as features are seen
340    /// (build_block consumes the lifter, so the flags must escape via a
341    /// shared reference rather than a field read afterwards).
342    mflags: &'a mut MethodFlags,
343}
344
345impl<'a> Lifter<'a> {
346    pub fn new(
347        env: &'a MethodEnv<'a>,
348        vt: &'a mut VarTable,
349        in_regs: Vec<Reg>,
350        block_id: usize,
351        stable: &'a mut jdc_core::FxHashMap<(usize, u16, u64), u32>,
352        mflags: &'a mut MethodFlags,
353    ) -> Lifter<'a> {
354        let n = env.code.registers_size as usize;
355        let n = n.max(in_regs.len()).max(1);
356        let mut regs = in_regs;
357        regs.resize(n, Reg::Undef);
358        Lifter {
359            env,
360            vt,
361            regs,
362            write_pc: vec![0; n],
363            stmts: Vec::new(),
364            block_id,
365            stable,
366            pending_call: None,
367            code_units: env.code_units,
368            cur_pc: 0,
369            final_read: jdc_core::FxHashSet::default(),
370            mflags,
371        }
372    }
373
374    // -- variable creation ---------------------------------------------------
375
376    /// Cheap 64-bit type fingerprint for the stable-var key: hashing the
377    /// JavaType itself (string bytes + tuple) ran on EVERY register
378    /// materialization; the fingerprint is one pass with no clones
379    /// (`erased()` deep-clones Array chains). G is unreachable in DEX
380    /// lift (no generic signatures) — a coarse tag suffices.
381    fn ty_key(ty: &TypeRef) -> u64 {
382        use std::hash::{Hash, Hasher};
383        fn jt_key(j: &JavaType, h: &mut jdc_core::fx::FxHasher) {
384            match j {
385                JavaType::Void => 1u64.hash(h),
386                JavaType::Boolean => 2u64.hash(h),
387                JavaType::Byte => 3u64.hash(h),
388                JavaType::Char => 4u64.hash(h),
389                JavaType::Short => 5u64.hash(h),
390                JavaType::Int => 6u64.hash(h),
391                JavaType::Float => 7u64.hash(h),
392                JavaType::Long => 8u64.hash(h),
393                JavaType::Double => 9u64.hash(h),
394                JavaType::Object(n) => {
395                    10u64.hash(h);
396                    n.hash(h);
397                }
398                JavaType::Array(inner) => {
399                    11u64.hash(h);
400                    jt_key(inner, h);
401                }
402            }
403        }
404        let mut h = jdc_core::fx::FxHasher::default();
405        match ty {
406            TypeRef::J(j) => jt_key(j, &mut h),
407            TypeRef::G(_) => 99u64.hash(&mut h),
408        }
409        h.finish()
410    }
411
412    fn fresh_var(&mut self, slot: u16, ty: TypeRef) -> u32 {
413        // Keyed by TYPE as well as (block, slot): d8 reuses one register
414        // for values of different types (`v1: byte[]` then `v1: String`
415        // in the same method — the lab package's Main.run). A Java local
416        // cannot change type — the old (block, slot) key reused one var
417        // and then RETYPED it in place, emitting `byte[] v1 = …; v1 =
418        // Obf.s(v1, 90);` (String into byte[]) and kin all over the
419        // corpus. Distinct types mint distinct vars; rebuild stability
420        // (the worklist-convergence fix) holds because the key is a pure
421        // function of the instruction stream.
422        let key = (self.block_id, slot, Self::ty_key(&ty));
423        if let Some(&id) = self.stable.get(&key) {
424            return id;
425        }
426        // Real source name from the debug-info local table when a range
427        // covers (pc, slot); otherwise the synthetic `vN`. apply_local_
428        // names never renames non-synthetic vars and de-duplicates the
429        // (legal-in-source, illegal-in-flat-Java) same-name scopes.
430        if let Some(name) = self.debug_name(slot, &ty) {
431            // Unify same register+type+name vars across blocks: the debug
432            // table says they are ONE source local, and per-block copies
433            // degrade to `out`/`out2`/`out3` suffix soup after de-dup.
434            // Dalvik registers are method-scoped, so one Java local shared
435            // by every write site is exactly register semantics.
436            let tkey = Self::ty_key(&ty);
437            if let Some(id) = self
438                .vt
439                .vars
440                .iter()
441                .find(|v| {
442                    v.slot == slot
443                        && !v.synthetic_name
444                        && v.name == name
445                        && Self::ty_key(&v.ty) == tkey
446                })
447                .map(|v| v.id)
448            {
449                self.stable.insert(key, id);
450                return id;
451            }
452            let id = self.vt.vars.len() as u32;
453            self.stable.insert(key, id);
454            self.push_var(id, slot, name, ty, false);
455            return id;
456        }
457        let id = self.vt.vars.len() as u32;
458        let name = format!("v{}", id);
459        self.stable.insert(key, id);
460        self.push_var(id, slot, name, ty, true);
461        id
462    }
463
464    fn push_var(&mut self, id: u32, slot: u16, name: String, ty: TypeRef, synthetic: bool) {
465        self.vt.vars.push(VarInfo {
466            id,
467            slot,
468            name,
469            ty,
470            is_param: false,
471            range_start: 0,
472            range_end: u16::MAX,
473            synthetic_name: synthetic,
474        });
475        while self.vt.by_slot.len() <= slot as usize {
476            self.vt.by_slot.push(Vec::new());
477        }
478        self.vt.by_slot[slot as usize].push((0, u16::MAX, id));
479    }
480
481    /// The source-level name covering `(cur_pc, slot)` in the debug
482    /// local table, sanitized into a legal Java identifier. Linear scan:
483    /// the table is empty in release APKs (early-out) and small in
484    /// debug builds.
485    fn debug_name(&self, slot: u16, ty: &TypeRef) -> Option<String> {
486        let locals = &self.env.debug_locals;
487        if locals.is_empty() {
488            return None;
489        }
490        let want = ty.erased();
491        let covers = |pc: u32| {
492            locals.iter().find(|l| {
493                l.reg == slot
494                    && pc >= l.start
495                    && pc < l.end
496                    // Type gate: register reuse mints vars of a DIFFERENT
497                    // type inside the same range (`new-array v0, v0` reads
498                    // the old v0 as an int size inside the new v0's [C
499                    // range) — naming that junk var stole the real name.
500                    && l.ty.as_deref().map(|d| crate::desc_type(d) == want).unwrap_or(true)
501            })
502        };
503        // Materializations at block merges run BEFORE any instruction of
504        // the block (cur_pc is stale/zero) — fall back to the pc where
505        // the register's current value was BORN (write_pc), which is
506        // what the debug range actually describes.
507        let hit = covers(self.cur_pc).or_else(|| {
508            let wpc = *self.write_pc.get(slot as usize)?;
509            if wpc == 0 {
510                None
511            } else {
512                covers(wpc)
513            }
514        })?;
515        let name = crate::classdec::java_ident(&hit.name);
516        if name.is_empty() {
517            return None;
518        }
519        Some(name.into_owned())
520    }
521
522    fn local_expr(&self, v: u32) -> Expr {
523        Expr::Local {
524            var: v,
525            ty: self.vt.var(v).ty.clone(),
526        }
527    }
528
529    // -- register access -----------------------------------------------------
530
531    /// Read for nesting into another expression (clones the value).
532    #[track_caller]
533    fn read_nest(&mut self, r: u16) -> Expr {
534        match self.regs.get(r as usize).cloned().unwrap_or(Reg::Undef) {
535            Reg::Undef | Reg::WideHi => {
536                // Reading a register never written on this path (or beyond
537                // the frame): a fresh (never-assigned) local keeps the
538                // output compilable.
539                let ty = TypeRef::J(JavaType::Int);
540                let v = self.fresh_var(r, ty);
541                if (r as usize) < self.regs.len() {
542                    self.regs[r as usize] = Reg::Live(v);
543                }
544                self.local_expr(v)
545            }
546            Reg::Live(v) => self.local_expr(v),
547            Reg::Pending(e) => {
548                // An allocation view (`NewArray`, or a post-fold `New`)
549                // may be emitted inline only when THIS read is the
550                // register's final one: any later read would re-emission
551                // a FRESH allocation (`sput v0, sparse; aput v2, v0, v1`
552                // printed three distinct `new byte[4]`; a discarded-
553                // result StringBuilder chain printed one `new
554                // StringBuilder()` per append). Materialize when a later
555                // read exists; inline the last use, keeping
556                // `foo(new Bar(...))` nesting. `New { raw: true }` is
557                // always exempt: the constructor fold consumes the raw
558                // view at the invoke-direct site.
559                let alloc = matches!(&e, Expr::NewArray { .. })
560                    || (matches!(&e, Expr::New { raw: false, .. })
561                        && !self.final_read.contains(&(self.cur_pc, r)));
562                if alloc {
563                    let v = self.materialize(r);
564                    self.local_expr(v)
565                } else {
566                    // Inline AND consume: this is the register's final
567                    // read in the block, yet leaving the Pending view in
568                    // place made the block-exit materialization emit the
569                    // SAME allocation a second time (`this.last = new
570                    // Report(..); new Report(..);` — a duplicated
571                    // observable side effect; lab package Guard.report).
572                    // `New { raw: true }` is EXEMPT: the ctor fold reads
573                    // the raw view at the invoke-direct site and must
574                    // still find it (consuming it there broke every
575                    // constructor fold).
576                    if matches!(&e, Expr::NewArray { .. })
577                        || matches!(&e, Expr::New { raw: false, .. })
578                    {
579                        if let Some(slot) = self.regs.get_mut(r as usize) {
580                            *slot = Reg::Undef;
581                        }
582                    }
583                    e
584                }
585            }
586            Reg::PendingCall(_) => {
587                let v = self.materialize(r);
588                self.local_expr(v)
589            }
590        }
591    }
592
593    /// Read for a context that evaluates exactly once (term payloads): the
594    /// value is consumed, not cloned — otherwise the block-exit
595    /// materialization would emit it a second time.
596    fn read_term(&mut self, r: u16) -> Expr {
597        match self.regs.get(r as usize).cloned().unwrap_or(Reg::Undef) {
598            Reg::PendingCall(e) | Reg::Pending(e) => {
599                if (r as usize) < self.regs.len() {
600                    self.regs[r as usize] = Reg::Undef;
601                }
602                self.pending_call = None;
603                e
604            }
605            _ => self.read_nest(r),
606        }
607    }
608
609    fn write(&mut self, r: u16, e: Expr, pc: u32, wide: bool) {
610        if r as usize >= self.regs.len() {
611            return;
612        }
613        self.write_pc[r as usize] = pc;
614        // Loop-carried Pending trees grow multiplicatively across fixpoint
615        // rounds (merge clones the whole tree per side per visit); past
616        // this size the register materializes, which caps every state at
617        // a bounded expression over stable locals.
618        if expr_size(&e) > 32 {
619            self.write_pc[r as usize] = pc;
620            let v = self.materialize_value(r, e);
621            if (r as usize) < self.regs.len() {
622                self.regs[r as usize] = Reg::Live(v);
623            }
624            if wide {
625                self.mark_wide_hi(r + 1, pc);
626            }
627            return;
628        }
629        self.regs[r as usize] = Reg::Pending(e);
630        if wide {
631            self.mark_wide_hi(r + 1, pc);
632        }
633    }
634
635    /// Materialize WITHOUT consuming the register slot's caller contract:
636    /// emit `v = e` and return the var id (used by the size cap).
637    fn materialize_value(&mut self, r: u16, e: Expr) -> u32 {
638        let e = value_of_cmp(&e);
639        let ty = e.type_ref();
640        // fresh_var is type-keyed: a retyped register gets a FRESH var,
641        // so no in-place retyping here (each var keeps its mint type and
642        // its LocalDef label always matches its init).
643        let v = self.fresh_var(r, ty);
644        self.stmts.push(Stmt::LocalDef {
645            var: v,
646            init: Some(e),
647            is_final: false,
648            force_type: true,
649        });
650        v
651    }
652
653    /// Write a value whose evaluation already happened (invoke /
654    /// filled-new-array results): consumed exactly once.
655    fn write_call_result(&mut self, r: u16, e: Expr, pc: u32) {
656        if r as usize >= self.regs.len() {
657            return;
658        }
659        self.write_pc[r as usize] = pc;
660        self.regs[r as usize] = Reg::PendingCall(e);
661    }
662
663    fn mark_wide_hi(&mut self, r: u16, pc: u32) {
664        if (r as usize) < self.regs.len() {
665            self.regs[r as usize] = Reg::WideHi;
666            self.write_pc[r as usize] = pc;
667        }
668    }
669
670    /// Emit `v = expr` and mark the register `Live(v)`.
671    fn materialize(&mut self, r: u16) -> u32 {
672        let cur = self.regs.get(r as usize).cloned().unwrap_or(Reg::Undef);
673        let e = match cur {
674            Reg::Pending(e) | Reg::PendingCall(e) => e,
675            Reg::Live(v) => return v,
676            // Undef (a value flowing in from an unanalyzed path): a fresh
677            // local, NOT Const 0 — `0.new a()` was not even valid Java.
678            _ => {
679                let v = self.fresh_var(r, TypeRef::J(JavaType::Object("java/lang/Object".into())));
680                self.stmts.push(Stmt::LocalDef {
681                    var: v,
682                    init: None,
683                    is_final: false,
684                    force_type: false,
685                });
686                if (r as usize) < self.regs.len() {
687                    self.regs[r as usize] = Reg::Live(v);
688                }
689                return v;
690            }
691        };
692        // cmp sentinels stored as values become library compare calls.
693        let e = value_of_cmp(&e);
694        let ty = e.type_ref();
695        // fresh_var is type-keyed: a retyped register gets a FRESH var,
696        // so no in-place retyping here (each var keeps its mint type and
697        // its LocalDef label always matches its init).
698        let v = self.fresh_var(r, ty);
699        self.stmts.push(Stmt::LocalDef {
700            var: v,
701            init: Some(e),
702            is_final: false,
703            force_type: true,
704        });
705        if (r as usize) < self.regs.len() {
706            self.regs[r as usize] = Reg::Live(v);
707        }
708        v
709    }
710
711    /// Discard an unconsumed call result (invoke without move-result).
712    fn drop_pending_call(&mut self) {
713        if let Some(e) = self.pending_call.take() {
714            let e = value_of_cmp(&e);
715            if has_side_effects(&e) {
716                self.stmts.push(Stmt::ExprStmt(e));
717            }
718        }
719    }
720
721    // -- helpers -------------------------------------------------------------
722
723    fn owner_expr(&self, obj: Expr, cls: &str) -> Option<Box<Expr>> {
724        if cls == self.env.class_name && matches!(obj, Expr::This) {
725            return None;
726        }
727        Some(Box::new(obj))
728    }
729
730    fn arith_op(op: ArithOp) -> BinOp {
731        match op {
732            ArithOp::Add => BinOp::Add,
733            ArithOp::Sub => BinOp::Sub,
734            ArithOp::Mul => BinOp::Mul,
735            ArithOp::Div => BinOp::Div,
736            ArithOp::Rem => BinOp::Rem,
737            ArithOp::And => BinOp::And,
738            ArithOp::Or => BinOp::Or,
739            ArithOp::Xor => BinOp::Xor,
740            ArithOp::Shl => BinOp::Shl,
741            ArithOp::Shr => BinOp::Shr,
742            ArithOp::Ushr => BinOp::Ushr,
743        }
744    }
745
746    fn cmp_op(op: CmpOp) -> BinOp {
747        match op {
748            CmpOp::Eq => BinOp::Eq,
749            CmpOp::Ne => BinOp::Ne,
750            CmpOp::Lt => BinOp::Lt,
751            CmpOp::Ge => BinOp::Ge,
752            CmpOp::Gt => BinOp::Gt,
753            CmpOp::Le => BinOp::Le,
754        }
755    }
756
757    fn cmp_sentinel(kind: CmpKind, l: Expr, r: Expr) -> Expr {
758        let name = match kind {
759            CmpKind::CmplF => "\0cmpl-float",
760            CmpKind::CmpgF => "\0cmpg-float",
761            CmpKind::CmplD => "\0cmpl-double",
762            CmpKind::CmpgD => "\0cmpg-double",
763            CmpKind::CmpJ => "\0cmp-long",
764        };
765        Expr::Invokedynamic {
766            name: name.into(),
767            desc: MethodDescriptor {
768                args: vec![],
769                ret: JavaType::Int,
770            },
771            args: vec![l, r],
772            bsm_text: String::new(),
773            bsm_static_args: vec![],
774        }
775    }
776
777    // -- the main loop ---------------------------------------------------------
778
779    /// Lift one block. `handler_types` gives catch types for ranges this
780    /// block handles (for move-exception typing).
781    pub fn build_block(
782        mut self,
783        ins: &[Insn],
784        handler_types: &[Option<std::sync::Arc<str>>],
785    ) -> BResult<(BlockResult, OutState)> {
786        let payloads = &self.env.code.payloads;
787        // Block-scoped event analysis, unconditionally: `ins` is ONE
788        // BLOCK's slice, but the register generation a read belongs to
789        // spans blocks — a post-fold `New` view created in an earlier
790        // block is READ here, so gating on "this block has no
791        // new-instance/new-array" (d9bbb2a) silently flipped every such
792        // read from inline to materialize: cross-block `foo(new Bar())`
793        // nesting lost, statement counts inflated, and the changed var
794        // structure fed forward_single_use a def-reference cycle
795        // (weixin input/b4: ~300k recursion frames, 64MB stack abort).
796        self.final_read = compute_final_reads(ins);
797        for ins in ins {
798            self.cur_pc = ins.pc;
799            match &ins.kind {
800                InsnKind::Nop => {}
801                InsnKind::Unknown => {
802                    return Err(format!(
803                        "unsupported opcode {:#04x} ({}) at pc {}",
804                        ins.op,
805                        ddc_dex::insn::op_name(ins.op),
806                        ins.pc
807                    ));
808                }
809                InsnKind::Move { dst, src } => {
810                    // Impure / call values are consumed once: materialize the
811                    // source so both registers share one evaluation.
812                    let src_state = self.regs.get(*src as usize).cloned().unwrap_or(Reg::Undef);
813                    if matches!(src_state, Reg::PendingCall(_))
814                        || matches!(&src_state, Reg::Pending(e) if has_side_effects(e))
815                    {
816                        self.materialize(*src);
817                    }
818                    let st = self.regs.get(*src as usize).cloned().unwrap_or(Reg::Undef);
819                    let wide = reg_is_wide(&st, self.vt);
820                    if (*dst as usize) < self.regs.len() {
821                        self.regs[*dst as usize] = st;
822                        self.write_pc[*dst as usize] =
823                            self.write_pc.get(*src as usize).copied().unwrap_or(0);
824                    }
825                    if wide {
826                        self.mark_wide_hi(*dst + 1, ins.pc);
827                    }
828                }
829                InsnKind::MoveResult { dst } => {
830                    if let Some(e) = self.pending_call.take() {
831                        let wide =
832                            matches!(e.type_ref().erased(), JavaType::Long | JavaType::Double);
833                        self.write_call_result(*dst, e, ins.pc);
834                        if wide {
835                            self.mark_wide_hi(*dst + 1, ins.pc);
836                        }
837                    } else {
838                        // move-result without a pending call: fresh var.
839                        let v = self.fresh_var(*dst, TypeRef::J(JavaType::Int));
840                        if (*dst as usize) < self.regs.len() {
841                            self.regs[*dst as usize] = Reg::Live(v);
842                        }
843                    }
844                }
845                InsnKind::MoveException { dst } => {
846                    let ty = handler_types
847                        .iter()
848                        .flatten()
849                        .next()
850                        .map(|t| TypeRef::J(JavaType::Object(t.clone())))
851                        .unwrap_or_else(|| {
852                            TypeRef::J(JavaType::Object("java/lang/Throwable".into()))
853                        });
854                    let v = self.fresh_var(*dst, ty);
855                    self.stmts.push(Stmt::LocalDef {
856                        var: v,
857                        init: None,
858                        is_final: false,
859                        force_type: true,
860                    });
861                    if (*dst as usize) < self.regs.len() {
862                        self.regs[*dst as usize] = Reg::Live(v);
863                    }
864                }
865                InsnKind::ReturnVoid
866                | InsnKind::Return { .. }
867                | InsnKind::Throw { .. }
868                | InsnKind::Goto { .. }
869                | InsnKind::PackedSwitch { .. }
870                | InsnKind::SparseSwitch { .. }
871                | InsnKind::If { .. } => {}
872                InsnKind::Const { dst, val, wide } => {
873                    let e = if *wide {
874                        Expr::Const(ConstVal::Long(*val))
875                    } else {
876                        Expr::Const(ConstVal::Int(*val as i32))
877                    };
878                    self.write(*dst, e, ins.pc, *wide);
879                }
880                InsnKind::ConstString { dst, str_idx } => {
881                    let s = std::sync::Arc::from(self.env.dex.string(*str_idx));
882                    self.write(*dst, Expr::Const(ConstVal::Str(s)), ins.pc, false);
883                }
884                InsnKind::ConstClass { dst, type_idx } => {
885                    let ty = TypeRef::J(self.env.java_type(*type_idx));
886                    self.write(*dst, Expr::Const(ConstVal::ClassLit(ty)), ins.pc, false);
887                }
888                InsnKind::MonitorEnter { reg } => {
889                    let e = self.read_nest(*reg);
890                    *self.mflags = self.mflags.with_monitor();
891                    self.stmts.push(Stmt::MonitorEnter(e));
892                }
893                InsnKind::MonitorExit { reg } => {
894                    let e = self.read_nest(*reg);
895                    *self.mflags = self.mflags.with_monitor();
896                    self.stmts.push(Stmt::MonitorExit(e));
897                }
898                InsnKind::CheckCast { reg, type_idx } => {
899                    let e = self.read_nest(*reg);
900                    let ty = TypeRef::J(self.env.java_type(*type_idx));
901                    self.write(*reg, Expr::Cast { ty, e: Box::new(e) }, ins.pc, false);
902                }
903                InsnKind::InstanceOf { dst, src, type_idx } => {
904                    let e = self.read_nest(*src);
905                    let ty = TypeRef::J(self.env.java_type(*type_idx));
906                    self.write(*dst, Expr::InstanceOf { e: Box::new(e), ty }, ins.pc, false);
907                }
908                InsnKind::ArrayLength { dst, src } => {
909                    let e = self.read_nest(*src);
910                    let length = Expr::Field {
911                        owner: Some(Box::new(e)),
912                        cls: std::sync::Arc::from(""),
913                        name: "length".into(),
914                        ty: TypeRef::J(JavaType::Int),
915                        is_static: false,
916                    };
917                    self.write(*dst, length, ins.pc, false);
918                }
919                InsnKind::NewInstance { dst, type_idx } => {
920                    let cls = self.env.type_name_arc(*type_idx);
921                    let ty = TypeRef::J(JavaType::Object(cls.clone()));
922                    self.write(
923                        *dst,
924                        Expr::New {
925                            cls,
926                            ty,
927                            args: vec![],
928                            raw: true,
929                        },
930                        ins.pc,
931                        false,
932                    );
933                }
934                InsnKind::NewArray {
935                    dst,
936                    size,
937                    type_idx,
938                } => {
939                    let elem_desc = self.env.type_name(*type_idx);
940                    // The type id names the ARRAY type: strip one `[`.
941                    let elem = crate::desc_type(elem_desc.trim_start_matches('['));
942                    let dims = self.read_nest(*size);
943                    self.write(
944                        *dst,
945                        Expr::NewArray {
946                            elem: TypeRef::J(elem),
947                            dims: vec![dims],
948                            trailing_dims: 0,
949                            init: None,
950                        },
951                        ins.pc,
952                        false,
953                    );
954                }
955                InsnKind::FilledNewArray { regs, type_idx } => {
956                    self.drop_pending_call();
957                    let elem_desc = self.env.type_name(*type_idx);
958                    let elem = crate::desc_type(elem_desc.trim_start_matches('['));
959                    let args: Vec<Expr> = regs.iter().map(|&r| self.read_nest(r)).collect();
960                    self.pending_call = Some(Expr::NewArray {
961                        elem: TypeRef::J(elem),
962                        dims: vec![],
963                        trailing_dims: 0,
964                        init: Some(args),
965                    });
966                }
967                InsnKind::FillArrayData { reg, payload_pc } => {
968                    if let Some(Payload::ArrayData {
969                        elem_width,
970                        size,
971                        data,
972                    }) = payloads.get(payload_pc)
973                    {
974                        let elems = payload_consts(*elem_width, *size, data);
975                        let cur = self.regs.get(*reg as usize).cloned().unwrap_or(Reg::Undef);
976                        if let Reg::Pending(Expr::NewArray { elem, init, .. }) = cur {
977                            if init.is_none() {
978                                self.write(
979                                    *reg,
980                                    Expr::NewArray {
981                                        elem,
982                                        dims: vec![Expr::Const(ConstVal::Int(*size as i32))],
983                                        trailing_dims: 0,
984                                        init: Some(elems),
985                                    },
986                                    ins.pc,
987                                    false,
988                                );
989                                continue;
990                            }
991                        }
992                        // Not the new-array+fill pattern: element stores.
993                        let arr = self.read_nest(*reg);
994                        for (i, e) in elems.into_iter().enumerate() {
995                            let idx = Expr::Const(ConstVal::Int(i as i32));
996                            let target = Expr::ArrayIndex {
997                                array: Box::new(arr.clone()),
998                                index: Box::new(idx),
999                            };
1000                            self.stmts.push(Stmt::ExprStmt(Expr::Assign {
1001                                target: Box::new(target),
1002                                op: AssignOp::Plain,
1003                                value: Box::new(e),
1004                            }));
1005                        }
1006                    }
1007                }
1008                InsnKind::Cmp { dst, a, b, kind } => {
1009                    let l = self.read_nest(*a);
1010                    let r = self.read_nest(*b);
1011                    *self.mflags = self.mflags.with_cmp();
1012                    self.write(*dst, Self::cmp_sentinel(*kind, l, r), ins.pc, false);
1013                }
1014                InsnKind::AGet {
1015                    dst, array, index, ..
1016                } => {
1017                    let a = self.read_nest(*array);
1018                    let i = self.read_nest(*index);
1019                    self.write(
1020                        *dst,
1021                        Expr::ArrayIndex {
1022                            array: Box::new(a),
1023                            index: Box::new(i),
1024                        },
1025                        ins.pc,
1026                        false,
1027                    );
1028                }
1029                InsnKind::APut {
1030                    value,
1031                    array,
1032                    index,
1033                    ..
1034                } => {
1035                    self.drop_pending_call();
1036                    let v = self.read_nest(*value);
1037                    let a = self.read_nest(*array);
1038                    let i = self.read_nest(*index);
1039                    let target = Expr::ArrayIndex {
1040                        array: Box::new(a),
1041                        index: Box::new(i),
1042                    };
1043                    self.stmts.push(Stmt::ExprStmt(Expr::Assign {
1044                        target: Box::new(target),
1045                        op: AssignOp::Plain,
1046                        value: Box::new(v),
1047                    }));
1048                }
1049                InsnKind::IGet {
1050                    dst,
1051                    obj,
1052                    field_idx,
1053                } => {
1054                    self.drop_pending_call();
1055                    let (cls, name, ty) = self.env.field_ref(*field_idx);
1056                    let owner = self.read_nest(*obj);
1057                    let owner_opt = self.owner_expr(owner, &cls);
1058                    self.write(
1059                        *dst,
1060                        Expr::Field {
1061                            owner: owner_opt,
1062                            cls,
1063                            name,
1064                            ty: TypeRef::J(ty),
1065                            is_static: false,
1066                        },
1067                        ins.pc,
1068                        false,
1069                    );
1070                }
1071                InsnKind::IPut {
1072                    value,
1073                    obj,
1074                    field_idx,
1075                } => {
1076                    self.drop_pending_call();
1077                    let (cls, name, ty) = self.env.field_ref(*field_idx);
1078                    let v = null_in_obj_ctx(self.read_nest(*value), &ty);
1079                    let owner = self.read_nest(*obj);
1080                    let owner_opt = self.owner_expr(owner, &cls);
1081                    let target = Expr::Field {
1082                        owner: owner_opt,
1083                        cls,
1084                        name,
1085                        ty: TypeRef::J(ty),
1086                        is_static: false,
1087                    };
1088                    self.stmts.push(Stmt::ExprStmt(Expr::Assign {
1089                        target: Box::new(target),
1090                        op: AssignOp::Plain,
1091                        value: Box::new(v),
1092                    }));
1093                }
1094                InsnKind::SGet { dst, field_idx } => {
1095                    self.drop_pending_call();
1096                    let (cls, name, ty) = self.env.field_ref(*field_idx);
1097                    self.write(
1098                        *dst,
1099                        Expr::Field {
1100                            owner: None,
1101                            cls,
1102                            name,
1103                            ty: TypeRef::J(ty),
1104                            is_static: true,
1105                        },
1106                        ins.pc,
1107                        false,
1108                    );
1109                }
1110                InsnKind::SPut { value, field_idx } => {
1111                    self.drop_pending_call();
1112                    let (cls, name, ty) = self.env.field_ref(*field_idx);
1113                    let v = null_in_obj_ctx(self.read_nest(*value), &ty);
1114                    let target = Expr::Field {
1115                        owner: None,
1116                        cls,
1117                        name,
1118                        ty: TypeRef::J(ty),
1119                        is_static: true,
1120                    };
1121                    self.stmts.push(Stmt::ExprStmt(Expr::Assign {
1122                        target: Box::new(target),
1123                        op: AssignOp::Plain,
1124                        value: Box::new(v),
1125                    }));
1126                }
1127                InsnKind::Invoke {
1128                    kind,
1129                    regs,
1130                    method_idx,
1131                } => {
1132                    self.drop_pending_call();
1133                    self.do_invoke(*kind, regs, *method_idx, ins.pc)?;
1134                }
1135                InsnKind::InvokeCustom {
1136                    call_site_idx,
1137                    regs,
1138                } => {
1139                    self.drop_pending_call();
1140                    let e = self.build_invoke_custom(*call_site_idx, regs, ins.pc);
1141                    self.pending_call = Some(e);
1142                }
1143                InsnKind::Un {
1144                    dst,
1145                    src,
1146                    op,
1147                    from: _,
1148                    to,
1149                } => {
1150                    let e = self.read_nest(*src);
1151                    let ne = match op {
1152                        ddc_dex::insn::UnArith::Neg => Expr::Un {
1153                            op: UnOp::Neg,
1154                            e: Box::new(e),
1155                        },
1156                        ddc_dex::insn::UnArith::Not => Expr::Un {
1157                            op: UnOp::BitNot,
1158                            e: Box::new(e),
1159                        },
1160                        ddc_dex::insn::UnArith::Conv => Expr::Cast {
1161                            ty: TypeRef::J(primitive_type(*to)),
1162                            e: Box::new(e),
1163                        },
1164                    };
1165                    let wide = matches!(*to, 'J' | 'D');
1166                    self.write(*dst, ne, ins.pc, wide);
1167                }
1168                InsnKind::Bin { op, dst, a, b, ty } => {
1169                    let l = self.read_nest(*a);
1170                    let r = self.read_nest(*b);
1171                    let jt = primitive_type(*ty);
1172                    let e = Expr::Bin {
1173                        op: Self::arith_op(*op),
1174                        l: Box::new(l),
1175                        r: Box::new(r),
1176                        ty: Some(TypeRef::J(jt)),
1177                    };
1178                    let wide = matches!(*ty, 'J' | 'D');
1179                    self.write(*dst, e, ins.pc, wide);
1180                }
1181                InsnKind::BinLit {
1182                    op,
1183                    dst,
1184                    a,
1185                    lit,
1186                    rsub,
1187                } => {
1188                    let av = self.read_nest(*a);
1189                    let e = if *rsub {
1190                        Expr::Bin {
1191                            op: BinOp::Sub,
1192                            l: Box::new(Expr::Const(ConstVal::Int(*lit))),
1193                            r: Box::new(av),
1194                            ty: Some(TypeRef::J(JavaType::Int)),
1195                        }
1196                    } else {
1197                        Expr::Bin {
1198                            op: Self::arith_op(*op),
1199                            l: Box::new(av),
1200                            r: Box::new(Expr::Const(ConstVal::Int(*lit))),
1201                            ty: Some(TypeRef::J(JavaType::Int)),
1202                        }
1203                    };
1204                    self.write(*dst, e, ins.pc, false);
1205                }
1206                InsnKind::ConstMethodHandle { dst, handle_idx } => {
1207                    self.drop_pending_call();
1208                    let e = self.method_handle_expr(*handle_idx);
1209                    self.write(*dst, e, ins.pc, false);
1210                }
1211                InsnKind::ConstMethodType { dst, proto_idx } => {
1212                    self.drop_pending_call();
1213                    let proto = self.env.dex.proto(*proto_idx);
1214                    let desc = format!(
1215                        "({}){}",
1216                        self.env
1217                            .dex
1218                            .proto_params(*proto_idx)
1219                            .iter()
1220                            .map(|&t| self.env.dex.type_name(t))
1221                            .collect::<Vec<_>>()
1222                            .join(""),
1223                        self.env.dex.type_name(proto.return_type_idx)
1224                    );
1225                    self.write(*dst, Expr::Raw(desc), ins.pc, false);
1226                }
1227            }
1228        }
1229
1230        // Terminator first: terms may consume PendingCall values directly.
1231        let term = self.block_term(ins);
1232
1233        // Exit cleanup: unconsumed call → statement; impure pendings → vars.
1234        // Open `new`/`new-array` views stay pending (their consumer may live
1235        // in a later block).
1236        self.drop_pending_call();
1237        self.materialize_impure_at_exit();
1238
1239        // Move (not clone): the lifter is consumed after build_block, and
1240        // these two clones — regs deep-copies boxed Exprs for any Pending
1241        // states — ran 3.7M times on weibo.
1242        let out = OutState {
1243            regs: std::mem::take(&mut self.regs),
1244            write_pc: std::mem::take(&mut self.write_pc),
1245        };
1246        Ok((
1247            BlockResult {
1248                stmts: std::mem::take(&mut self.stmts),
1249                out_stack: Vec::new(),
1250                term,
1251            },
1252            out,
1253        ))
1254    }
1255
1256    fn do_invoke(
1257        &mut self,
1258        kind: InvokeKind,
1259        regs: &[u16],
1260        method_idx: u32,
1261        pc: u32,
1262    ) -> BResult<()> {
1263        let (cls, name, md) = self.env.method_ref(method_idx);
1264        if cls.as_ref() == "java/lang/StringBuilder" || cls.as_ref() == "java/lang/StringBuffer" {
1265            *self.mflags = self.mflags.with_sb();
1266        }
1267        let is_static = matches!(kind, InvokeKind::Static);
1268        let receiver_reg = if is_static {
1269            None
1270        } else {
1271            regs.first().copied()
1272        };
1273        let arg_regs: Vec<u16> = if is_static {
1274            regs.to_vec()
1275        } else {
1276            regs.iter().skip(1).copied().collect()
1277        };
1278        let mut args: Vec<Expr> = Vec::with_capacity(md.args.len());
1279        for (i, at) in md.args.iter().enumerate() {
1280            let r = arg_regs.get(i).copied().unwrap_or(0);
1281            args.push(null_in_obj_ctx(self.read_nest(r), at));
1282        }
1283        let recv_expr = receiver_reg.map(|r| self.read_nest(r));
1284
1285        // Constructor call: fold `new C` receivers; this/super otherwise.
1286        if name.as_ref() == "<init>" && matches!(kind, InvokeKind::Direct) {
1287            let recv_reg = receiver_reg.unwrap_or(0);
1288            let recv_state = self
1289                .regs
1290                .get(recv_reg as usize)
1291                .cloned()
1292                .unwrap_or(Reg::Undef);
1293            if let Reg::Pending(Expr::New {
1294                raw: true, cls: nc, ..
1295            }) = &recv_state
1296            {
1297                if nc == &cls {
1298                    let ty = TypeRef::J(JavaType::Object(cls.clone()));
1299                    self.write(
1300                        recv_reg,
1301                        Expr::New {
1302                            cls: cls.clone(),
1303                            ty,
1304                            args,
1305                            raw: false,
1306                        },
1307                        pc,
1308                        false,
1309                    );
1310                    return Ok(());
1311                }
1312            }
1313            let owner_expr_v = recv_expr.unwrap_or(Expr::This);
1314            // Normalize the receiver: in an instance method the `this`
1315            // parameter is ALWAYS var 0 (entry_regs pushes it first).
1316            // read_nest hands back Local{var:0}, which the emitter's
1317            // is_this/lost_alloc checks do not recognize — a plain
1318            // `super()` arrived there as a non-this receiver and printed
1319            // `new Object();` (every default ctor in every app).
1320            let owner_expr_v = if !self.env.is_static
1321                && matches!(&owner_expr_v, Expr::Local { var: 0, .. })
1322            {
1323                Expr::This
1324            } else {
1325                owner_expr_v
1326            };
1327            let is_super = cls.as_ref() != self.env.class_name.as_str();
1328            let owner = if matches!(owner_expr_v, Expr::This) && is_super {
1329                None
1330            } else {
1331                Some(Box::new(owner_expr_v))
1332            };
1333            let call = Expr::Method {
1334                owner,
1335                cls,
1336                name: "<init>".into(),
1337                desc: md,
1338                args,
1339                is_static: false,
1340                is_interface: false,
1341                is_special: true,
1342                is_super,
1343                is_dynamic: false,
1344                type_args: vec![],
1345            };
1346            self.stmts.push(Stmt::ExprStmt(call));
1347            return Ok(());
1348        }
1349
1350        let is_interface = matches!(kind, InvokeKind::Interface);
1351        let is_super = match kind {
1352            InvokeKind::Super => true,
1353            InvokeKind::Direct if !is_static => cls.as_ref() != self.env.class_name.as_str(),
1354            _ => false,
1355        };
1356        let owner: Option<Box<Expr>> = match recv_expr {
1357            None => None,
1358            Some(o) => {
1359                if is_super && matches!(o, Expr::This) {
1360                    None
1361                } else {
1362                    self.owner_expr(o, &cls)
1363                }
1364            }
1365        };
1366        let is_dynamic = matches!(kind, InvokeKind::Polymorphic);
1367        let call = Expr::Method {
1368            owner,
1369            cls,
1370            name,
1371            desc: md,
1372            args,
1373            is_static,
1374            is_interface,
1375            is_special: matches!(kind, InvokeKind::Direct),
1376            is_super,
1377            is_dynamic,
1378            type_args: vec![],
1379        };
1380        self.pending_call = Some(call);
1381        Ok(())
1382    }
1383
1384    /// Resolve a method-handle constant into a readable expression
1385    /// (`Cls::name` for method kinds).
1386    fn method_handle_expr(&mut self, handle_idx: u32) -> Expr {
1387        let Some(h) = self.env.dex.method_handle(handle_idx) else {
1388            return Expr::Raw(format!("methodHandle@{}", handle_idx));
1389        };
1390        if h.is_field {
1391            let f = self.env.dex.field(h.target_id);
1392            let cls = self.env.dex.class_name(f.class_idx);
1393            let name = self.env.dex.string(f.name_idx).to_string();
1394            return Expr::Raw(format!("{}::{}", cls.replace('/', "."), name));
1395        }
1396        let m = self.env.dex.method(h.target_id);
1397        let cls = self.env.dex.class_name(m.class_idx);
1398        let name = self.env.dex.string(m.name_idx).to_string();
1399        Expr::Raw(format!("{}::{}", cls.replace('/', "."), name))
1400    }
1401
1402    /// Build the expression for an invoke-custom site.
1403    fn build_invoke_custom(&mut self, cs_idx: u32, regs: &[u16], pc: u32) -> Expr {
1404        let Some(cs) = self.env.dex.call_site(cs_idx).cloned() else {
1405            return Expr::Invokedynamic {
1406                name: format!("call-site#{}", cs_idx),
1407                desc: jdc_core::types::MethodDescriptor {
1408                    args: vec![],
1409                    ret: JavaType::Object("java/lang/Object".into()),
1410                },
1411                args: vec![],
1412                bsm_text: "unresolved".into(),
1413                bsm_static_args: vec![],
1414            };
1415        };
1416        let site_name = self.env.dex.string(cs.name_idx).to_string();
1417        let params: Vec<JavaType> = self
1418            .env
1419            .dex
1420            .proto_params(cs.proto_idx)
1421            .iter()
1422            .map(|&t| self.env.java_type(t))
1423            .collect();
1424        let ret = self
1425            .env
1426            .java_type(self.env.dex.proto(cs.proto_idx).return_type_idx);
1427
1428        let (bs_cls, bs_name) = match self.env.dex.method_handle(cs.bootstrap_handle) {
1429            Some(h) if !h.is_field => {
1430                let m = self.env.dex.method(h.target_id);
1431                (
1432                    self.env.dex.class_name(m.class_idx),
1433                    self.env.dex.string(m.name_idx).to_string(),
1434                )
1435            }
1436            _ => (String::new(), String::new()),
1437        };
1438
1439        // Dynamic arguments (the SAM parameters for lambdas).
1440        let mut args: Vec<Expr> = Vec::with_capacity(params.len().min(regs.len()));
1441        for (i, _) in params.iter().enumerate() {
1442            if let Some(&r) = regs.get(i) {
1443                args.push(self.read_nest(r));
1444            }
1445        }
1446
1447        // StringConcatFactory: fold the recipe into `+`.
1448        if bs_name == "makeConcatWithConstants" {
1449            let recipe = cs
1450                .linker_args
1451                .iter()
1452                .find_map(|v| match v {
1453                    ddc_dex::annotations::EncodedValue::String(s) => {
1454                        Some(self.env.dex.string(*s).to_string())
1455                    }
1456                    _ => None,
1457                })
1458                .unwrap_or_default();
1459            return concat_from_recipe(&recipe, args);
1460        }
1461
1462        // LambdaMetafactory: linker args are [samType (erased),
1463        // implMethod (handle), instantiatedMethodType]. The instantiated
1464        // type carries the source-level SAM parameters.
1465        let mut impl_m = None;
1466        let mut instantiated_proto: Option<u32> = None;
1467        if bs_name == "metafactory" || bs_name == "altMetafactory" {
1468            let mut seen_handle = false;
1469            for lv in &cs.linker_args {
1470                match lv {
1471                    ddc_dex::annotations::EncodedValue::MethodHandle(hi) => {
1472                        if !seen_handle {
1473                            seen_handle = true;
1474                            if let Some(h) = self.env.dex.method_handle(*hi) {
1475                                if !h.is_field {
1476                                    let m = self.env.dex.method(h.target_id);
1477                                    impl_m = Some((
1478                                        self.env.dex.class_name(m.class_idx).replace('/', "."),
1479                                        self.env.dex.string(m.name_idx).to_string(),
1480                                    ));
1481                                }
1482                            }
1483                        }
1484                    }
1485                    ddc_dex::annotations::EncodedValue::MethodType(p) if seen_handle => {
1486                        instantiated_proto = Some(*p);
1487                    }
1488                    _ => {}
1489                }
1490            }
1491        }
1492        let _ = pc;
1493
1494        // Remaining registers are the captured values.
1495        let mut captures = Vec::new();
1496        for &r in regs.iter().skip(params.len().min(regs.len())) {
1497            captures.push(self.read_nest(r));
1498        }
1499
1500        if let Some((impl_cls, impl_name)) = impl_m {
1501            if captures.is_empty() {
1502                // Non-capturing: render real Java syntax. A synthetic
1503                // `lambda$` impl is a lambda; anything else is a method
1504                // reference (`Cls::name`).
1505                if impl_name.starts_with("lambda$") {
1506                    // Source-level parameter count comes from the
1507                    // instantiated SAM type, not the site proto (which is
1508                    // the functional-interface factory view: `()Function`).
1509                    let n_params = match instantiated_proto {
1510                        Some(p) => self.env.dex.proto_params(p).len(),
1511                        None => params.len(),
1512                    };
1513                    let pnames: Vec<String> = (0..n_params).map(|i| format!("a{}", i)).collect();
1514                    return Expr::Raw(format!(
1515                        "({}) -> {}.{}({})",
1516                        pnames.join(", "),
1517                        impl_cls,
1518                        impl_name,
1519                        pnames.join(", ")
1520                    ));
1521                }
1522                return Expr::Raw(format!("{}::{}", impl_cls, impl_name));
1523            }
1524            // Capturing: the captures are real expressions; emit the
1525            // desugared call shape (impl applies SAM params then captures).
1526            let mut call_args = args;
1527            call_args.extend(captures);
1528            return Expr::Invokedynamic {
1529                name: format!("{} -> {}.{}", site_name, impl_cls, impl_name),
1530                desc: jdc_core::types::MethodDescriptor { args: params, ret },
1531                args: call_args,
1532                bsm_text: format!("{}::{}", bs_cls, bs_name),
1533                bsm_static_args: vec![],
1534            };
1535        }
1536
1537        Expr::Invokedynamic {
1538            name: site_name,
1539            desc: jdc_core::types::MethodDescriptor { args: params, ret },
1540            args: captures,
1541            bsm_text: format!("{}::{}", bs_cls, bs_name),
1542            bsm_static_args: vec![],
1543        }
1544    }
1545
1546    fn materialize_impure_at_exit(&mut self) {
1547        let n = self.regs.len();
1548        for r in 0..n {
1549            let st = self.regs[r].clone();
1550            let impure = match &st {
1551                Reg::PendingCall(_) => true,
1552                Reg::Pending(e) => {
1553                    has_side_effects(e)
1554                        && !matches!(e, Expr::New { raw: true, .. })
1555                        && !matches!(e, Expr::NewArray { init: None, .. })
1556                }
1557                _ => false,
1558            };
1559            if impure {
1560                self.materialize(r as u16);
1561            }
1562        }
1563    }
1564
1565    /// Terminator for the block, from its last instruction.
1566    fn block_term(&mut self, ins: &[Insn]) -> Term {
1567        let Some(last) = ins.last() else {
1568            return Term::Goto;
1569        };
1570        match &last.kind {
1571            InsnKind::Goto { .. } => Term::Goto,
1572            InsnKind::ReturnVoid => Term::Return(None),
1573            InsnKind::Return { src } => {
1574                let v = null_in_obj_ctx(self.read_term(*src), &self.env.desc.ret);
1575                Term::Return(Some(v))
1576            }
1577            InsnKind::Throw { reg } => {
1578                // throw is always an object context.
1579                let mut v = self.read_term(*reg);
1580                if let Expr::Const(ConstVal::Int(0)) = v {
1581                    v = Expr::Const(ConstVal::Null);
1582                }
1583                Term::Throw(v)
1584            }
1585            InsnKind::If { op, a, b, z, .. } => {
1586                let cond = if *z {
1587                    let v = self.read_nest(*a);
1588                    let (l, r, realop) = jdc_core::ir::build::unfold_cmp(v, Self::cmp_op(*op));
1589                    Expr::Bin {
1590                        op: realop,
1591                        l: Box::new(l),
1592                        r: Box::new(r),
1593                        ty: Some(TypeRef::J(JavaType::Boolean)),
1594                    }
1595                } else {
1596                    let l = self.read_nest(*a);
1597                    let r = self.read_nest(*b);
1598                    Expr::Bin {
1599                        op: Self::cmp_op(*op),
1600                        l: Box::new(l),
1601                        r: Box::new(r),
1602                        ty: Some(TypeRef::J(JavaType::Boolean)),
1603                    }
1604                };
1605                Term::Cond { cond }
1606            }
1607            InsnKind::PackedSwitch { reg, payload_pc }
1608            | InsnKind::SparseSwitch { reg, payload_pc } => {
1609                let selector = self.read_term(*reg);
1610                let default = Some(last.pc + last.size);
1611                let targets = match self.env.code.payloads.get(payload_pc) {
1612                    Some(Payload::Packed { first_key, targets }) => SwitchTargets::Table {
1613                        low: *first_key,
1614                        targets: targets
1615                            .iter()
1616                            .map(|t| (last.pc as i64 + *t as i64) as u32)
1617                            .collect(),
1618                    },
1619                    Some(Payload::Sparse { pairs }) => SwitchTargets::Lookup {
1620                        pairs: pairs
1621                            .iter()
1622                            .map(|(k, t)| (*k, (last.pc as i64 + *t as i64) as u32))
1623                            .collect(),
1624                    },
1625                    _ => SwitchTargets::Lookup { pairs: vec![] },
1626                };
1627                Term::Switch {
1628                    selector,
1629                    targets,
1630                    default,
1631                }
1632            }
1633            // Fell off the analysis end (payload region after): goto.
1634            _ => {
1635                let next = last.pc + last.size;
1636                if next < self.code_units {
1637                    Term::Fallthrough
1638                } else {
1639                    Term::Goto
1640                }
1641            }
1642        }
1643    }
1644}
1645
1646/// StringConcatFactory recipe → `+` parts: `\u{1}` slots consume the next
1647/// argument, `\u{2}` <len> <chars> emits literal text (rare), other bytes
1648/// are literal.
1649fn concat_from_recipe(recipe: &str, args: Vec<Expr>) -> Expr {
1650    use jdc_core::ir::expr::ConcatPart;
1651    let mut parts: Vec<ConcatPart> = Vec::new();
1652    let mut arg_i = 0usize;
1653    let mut const_buf = String::new();
1654    for c in recipe.chars() {
1655        match c {
1656            '\u{1}' => {
1657                if !const_buf.is_empty() {
1658                    parts.push(ConcatPart::Const(std::mem::take(&mut const_buf)));
1659                }
1660                if arg_i < args.len() {
1661                    parts.push(ConcatPart::Str(args[arg_i].clone()));
1662                    arg_i += 1;
1663                }
1664            }
1665            '\u{2}' => {
1666                // Length-prefixed literal: skip the length char and copy the
1667                // following chars until the next marker.
1668            }
1669            c => const_buf.push(c),
1670        }
1671    }
1672    if !const_buf.is_empty() {
1673        parts.push(ConcatPart::Const(const_buf));
1674    }
1675    Expr::StringConcat(parts)
1676}
1677
1678/// Node count of an expression tree (caps loop-carried growth: a Pending
1679/// tree that grows across fixpoint rounds would otherwise reach GBs — each
1680/// merge clones the whole tree per side per visit).
1681fn expr_size(e: &Expr) -> usize {
1682    let mut n = 0usize;
1683    count_nodes(e, &mut n);
1684    n
1685}
1686
1687fn count_nodes(e: &Expr, n: &mut usize) {
1688    *n += 1;
1689    if *n > 512 {
1690        return; // cap the walk itself
1691    }
1692    match e {
1693        Expr::Un { e, .. } | Expr::Cast { e, .. } | Expr::InstanceOf { e, .. } => count_nodes(e, n),
1694        Expr::Bin { l, r, .. } => {
1695            count_nodes(l, n);
1696            count_nodes(r, n);
1697        }
1698        Expr::Cond { c, t, f } => {
1699            count_nodes(c, n);
1700            count_nodes(t, n);
1701            count_nodes(f, n);
1702        }
1703        Expr::Assign { target, value, .. } => {
1704            count_nodes(target, n);
1705            count_nodes(value, n);
1706        }
1707        Expr::PreIncDec { e, .. } | Expr::PostIncDec { e, .. } => count_nodes(e, n),
1708        Expr::Field { owner: Some(o), .. } => count_nodes(o, n),
1709        Expr::Method {
1710            owner: Some(o),
1711            args,
1712            ..
1713        } => {
1714            count_nodes(o, n);
1715            for a in args {
1716                count_nodes(a, n);
1717            }
1718        }
1719        Expr::ArrayIndex { array, index } => {
1720            count_nodes(array, n);
1721            count_nodes(index, n);
1722        }
1723        Expr::New { args, .. } => {
1724            for a in args {
1725                count_nodes(a, n);
1726            }
1727        }
1728        Expr::NewArray { dims, init, .. } => {
1729            for d in dims {
1730                count_nodes(d, n);
1731            }
1732            if let Some(v) = init {
1733                for x in v {
1734                    count_nodes(x, n);
1735                }
1736            }
1737        }
1738        Expr::NewMultiArray { dims, .. } => {
1739            for d in dims {
1740                count_nodes(d, n);
1741            }
1742        }
1743        Expr::StringConcat(parts) => {
1744            for p in parts {
1745                if let ConcatPart::Str(x) = p {
1746                    count_nodes(x, n);
1747                }
1748            }
1749        }
1750        _ => {}
1751    }
1752}
1753
1754fn reg_is_wide(st: &Reg, vt: &VarTable) -> bool {
1755    match st {
1756        Reg::Pending(e) | Reg::PendingCall(e) => e.type_ref().erased().is_wide(),
1757        Reg::Live(v) => vt.var(*v).ty.erased().is_wide(),
1758        Reg::WideHi => true,
1759        Reg::Undef => false,
1760    }
1761}
1762
1763/// d8 encodes `null` as `const/4 vN, 0`; the instruction kind does not
1764/// distinguish object contexts (return/return-object share one variant),
1765/// so the surrounding DESCRIPTOR type decides: a zero constant flowing
1766/// into a reference context is `null`, not `0` (rendering `return 0;`
1767/// from a String method — lab-package feedback).
1768fn null_in_obj_ctx(mut v: Expr, ctx_ty: &JavaType) -> Expr {
1769    if ctx_ty.is_reference() {
1770        if let Expr::Const(ConstVal::Int(0)) = v {
1771            v = Expr::Const(ConstVal::Null);
1772        }
1773    }
1774    v
1775}
1776
1777/// Turn a stored cmp sentinel into a `Long.compare`-style call.
1778fn value_of_cmp(e: &Expr) -> Expr {
1779    if let Expr::Invokedynamic { name, args, .. } = e {
1780        if name.starts_with('\0') && args.len() == 2 {
1781            let (cls, ty) = match name.as_str() {
1782                "\0cmp-long" => ("java/lang/Long", JavaType::Long),
1783                "\0cmpl-float" | "\0cmpg-float" => ("java/lang/Float", JavaType::Float),
1784                _ => ("java/lang/Double", JavaType::Double),
1785            };
1786            let desc = MethodDescriptor {
1787                args: vec![ty.clone(), ty],
1788                ret: JavaType::Int,
1789            };
1790            return Expr::Method {
1791                owner: None,
1792                cls: cls.into(),
1793                name: "compare".into(),
1794                desc: std::sync::Arc::new(desc),
1795                args: args.clone(),
1796                is_static: true,
1797                is_interface: false,
1798                is_special: false,
1799                is_super: false,
1800                is_dynamic: false,
1801                type_args: vec![],
1802            };
1803        }
1804    }
1805    e.clone()
1806}
1807
1808pub fn primitive_type(c: char) -> JavaType {
1809    match c {
1810        'Z' => JavaType::Boolean,
1811        'B' => JavaType::Byte,
1812        'C' => JavaType::Char,
1813        'S' => JavaType::Short,
1814        'F' => JavaType::Float,
1815        'J' => JavaType::Long,
1816        'D' => JavaType::Double,
1817        _ => JavaType::Int,
1818    }
1819}
1820
1821/// Constant elements from a fill-array-data payload, typed by width (the
1822/// consuming new-array's element type refines them later).
1823fn payload_consts(elem_width: u16, size: u32, data: &[u8]) -> Vec<Expr> {
1824    let n = (size as usize).min(data.len() / elem_width.max(1) as usize);
1825    let mut out = Vec::with_capacity(n);
1826    for i in 0..n {
1827        let off = i * elem_width as usize;
1828        let e = match elem_width {
1829            1 => Expr::Const(ConstVal::Int(data[off] as i8 as i32)),
1830            2 => {
1831                let v = i16::from_le_bytes([data[off], data[off + 1]]);
1832                Expr::Const(ConstVal::Int(v as i32))
1833            }
1834            4 => {
1835                let mut b = [0u8; 4];
1836                b.copy_from_slice(&data[off..off + 4]);
1837                Expr::Const(ConstVal::Int(i32::from_le_bytes(b)))
1838            }
1839            8 => {
1840                let mut b = [0u8; 8];
1841                b.copy_from_slice(&data[off..off + 8]);
1842                Expr::Const(ConstVal::Long(i64::from_le_bytes(b)))
1843            }
1844            _ => Expr::Const(ConstVal::Int(0)),
1845        };
1846        out.push(e);
1847    }
1848    out
1849}
1850
1851/// Entry register state from parameters. Returns the register state.
1852pub fn entry_regs(vt: &mut VarTable, env: &MethodEnv, param_names: &[Option<String>]) -> Vec<Reg> {
1853    let code = env.code;
1854    let n = code.registers_size as usize;
1855    let mut regs = vec![Reg::Undef; n.max(1)];
1856    let ins = code.ins_size.min(code.registers_size) as usize;
1857    let mut reg = n.saturating_sub(ins);
1858    let mut pidx = 0;
1859    if !env.is_static && reg < n {
1860        // `this` occupies the first incoming register.
1861        let id = vt.vars.len() as u32;
1862        vt.vars.push(VarInfo {
1863            id,
1864            slot: reg as u16,
1865            name: "this".into(),
1866            ty: TypeRef::J(JavaType::Object(env.class_name.as_str().into())),
1867            is_param: true,
1868            range_start: 0,
1869            range_end: u16::MAX,
1870            synthetic_name: false,
1871        });
1872        while vt.by_slot.len() <= reg {
1873            vt.by_slot.push(Vec::new());
1874        }
1875        vt.by_slot[reg].push((0, u16::MAX, id));
1876        regs[reg] = Reg::Live(id);
1877        reg += 1;
1878        pidx += 1;
1879    }
1880    for (i, arg) in env.desc.args.iter().enumerate() {
1881        if reg >= n {
1882            break;
1883        }
1884        let dbg = param_names.get(i).and_then(|o| o.clone());
1885        let named = dbg.is_some();
1886        // Debug-info names are ARBITRARY DEX strings — obfuscated apps
1887        // ship params named `25` (reqable a4/e: rendered `25 = 25;`, a
1888        // javac parse error). Run them through the same deterministic
1889        // sanitizer as every other declared identifier.
1890        let name = dbg
1891            .map(|d| crate::classdec::java_ident(&d).into_owned())
1892            .unwrap_or_else(|| format!("p{}", pidx));
1893        let id = vt.vars.len() as u32;
1894        vt.vars.push(VarInfo {
1895            id,
1896            slot: reg as u16,
1897            name,
1898            ty: TypeRef::J(arg.clone()),
1899            is_param: true,
1900            range_start: 0,
1901            range_end: u16::MAX,
1902            synthetic_name: !named,
1903        });
1904        while vt.by_slot.len() <= reg {
1905            vt.by_slot.push(Vec::new());
1906        }
1907        vt.by_slot[reg].push((0, u16::MAX, id));
1908        regs[reg] = Reg::Live(id);
1909        reg += if arg.is_wide() { 2 } else { 1 };
1910        pidx += 1;
1911    }
1912    regs
1913}