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