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