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