Skip to main content

ddc_dec/
lift.rs

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