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