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