Skip to main content

jdc_core/
var.rs

1//! Local variable allocation: maps bytecode slots (possibly reused across
2//! disjoint pc ranges) to named variables.
3
4//! Variable table: what the front-end knows about a method's locals.
5//!
6//! The *data* is machine-neutral (a name and a type per variable); how it is
7//! filled is not. A JVM front-end derives it from `LocalVariableTable`s; a DEX
8//! front-end synthesizes it from register live ranges (release APKs usually
9//! carry no local names at all).
10
11use std::collections::HashSet;
12
13use crate::ir::expr::TypeRef;
14use crate::types::{GenericType, JavaType};
15
16#[derive(Debug, Clone)]
17pub struct VarInfo {
18    pub id: u32,
19    pub slot: u16,
20    pub name: String,
21    pub ty: TypeRef,
22    pub is_param: bool,
23    /// pc range where this variable is live (from LVT; method end when open).
24    pub range_start: u16,
25    pub range_end: u16,
26    /// Set when the name was synthesized (no LVT entry).
27    pub synthetic_name: bool,
28}
29
30#[derive(Debug, Clone, Default)]
31pub struct VarTable {
32    pub vars: Vec<VarInfo>,
33    /// Per slot: (range_start, range_end, var_id), sorted by range_start.
34    pub by_slot: Vec<Vec<(u16, u16, u32)>>,
35    pub has_lvt: bool,
36    /// Synthetic stack-merge variables (need declaration hoisting).
37    pub stack_vars: Vec<u32>,
38    /// Merge variables whose branches disagreed on type (or carried a
39    /// generic/plain-Object value); they must stay Object so every branch
40    /// assignment type-checks. Evidence inference may not narrow them.
41    pub wide_stack_vars: HashSet<u32>,
42    /// Variables whose declared type must render as the CONCRETE class
43    /// (via `shorten_concrete`), NOT the anonymous-class → SAM-interface
44    /// fallback. A `$<digits>` desugared/suspend-lambda local that owns a
45    /// non-static FIELD access (`v.L$0 = obj`, the Kotlin coroutine
46    /// capture fields) needs its concrete type: interfaces carry no
47    /// instance fields, so `Function2 v; v.L$0` is "找不到符号 变量 L$0".
48    /// Set by the front-end for single-concrete-typed field owners; the
49    /// phi case is excluded because a disagreed merge stays Object (in
50    /// `wide_stack_vars`), never a specific concrete class.
51    pub force_concrete_vars: HashSet<u32>,
52    /// Exception-table handler start pcs: an LVT range beginning at one
53    /// (or at its astore + 1/2) is a catch parameter binding. Such
54    /// ranges never receive forward store attribution — a store just
55    /// BEFORE a handler initializes the try-body variable, not the
56    /// catch param (jdk11 HostnameChecker.matchDNS: `sni = new
57    /// SNIHostName(..)` at pc 8 landed on `iae` whose handler range
58    /// starts at 14, within the 8-byte lead-in — SNIHostName无法转换为
59    /// IllegalArgumentException).
60    pub handler_starts: Vec<u16>,
61}
62
63impl VarTable {
64    pub fn add_stack_var(&mut self, slot: u16, name: String, ty: TypeRef) -> u32 {
65        let id = self.add(slot, name, ty, false, 0, u16::MAX, true);
66        self.stack_vars.push(id);
67        id
68    }
69
70    /// Allocate a synthetic variable that is NOT a stack-merge temp (e.g. a
71    /// catch parameter); it must not be hoisted by the stack-var pass.
72    /// Add a new variable identity for slot-reuse splitting (post-build;
73    /// not registered in pc-range lookups).
74    pub fn add_split(&mut self, slot: u16, name: String, ty: TypeRef) -> u32 {
75        let id = self.vars.len() as u32;
76        self.vars.push(VarInfo {
77            id,
78            slot,
79            name,
80            ty,
81            is_param: false,
82            range_start: 0,
83            range_end: u16::MAX,
84            synthetic_name: true,
85        });
86        id
87    }
88
89    pub fn add_catch_var(&mut self, slot: u16, name: String, ty: TypeRef) -> u32 {
90        self.add(slot, name, ty, false, 0, u16::MAX, true)
91    }
92
93    #[allow(clippy::too_many_arguments)] // one builder for all var kinds
94    fn add(
95        &mut self,
96        slot: u16,
97        name: String,
98        ty: TypeRef,
99        is_param: bool,
100        rs: u16,
101        re: u16,
102        synth: bool,
103    ) -> u32 {
104        let id = self.vars.len() as u32;
105        self.vars.push(VarInfo {
106            id,
107            slot,
108            name,
109            ty,
110            is_param,
111            range_start: rs,
112            range_end: re,
113            synthetic_name: synth,
114        });
115        while self.by_slot.len() <= slot as usize {
116            self.by_slot.push(Vec::new());
117        }
118        self.by_slot[slot as usize].push((rs, re, id));
119        id
120    }
121
122    /// Resolve the variable live at (pc, slot). Falls back to the nearest
123    /// preceding range, then to any var on the slot.
124    pub fn at(&self, slot: u16, pc: u16) -> Option<u32> {
125        let segs = self.by_slot.get(slot as usize)?;
126        for (rs, re, id) in segs {
127            if pc >= *rs && pc < *re {
128                return Some(*id);
129            }
130        }
131        // javac often starts an LVT range just AFTER the storing
132        // instruction; attribute a nearby access to the next range before
133        // falling back to the previous one (keeps slot-reuse splits like
134        // `byte[] dst` / `int b` on one slot apart). Never forward-
135        // attribute into a catch-param range (starts at a handler pc or
136        // its astore+1/+2): the store before a handler initializes the
137        // try-body variable (jdk11 HostnameChecker.matchDNS `sni = new
138        // SNIHostName(..)` was typed IllegalArgumentException).
139        for (rs, _, id) in segs {
140            if *rs > pc
141                && *rs - pc <= 8
142                && !self
143                    .handler_starts
144                    .iter()
145                    .any(|h| *rs == *h || *rs == h + 1 || *rs == h + 2)
146            {
147                return Some(*id);
148            }
149        }
150        let mut best: Option<u32> = None;
151        for (rs, _, id) in segs {
152            if *rs <= pc {
153                best = Some(*id);
154            }
155        }
156        best.or_else(|| segs.first().map(|(_, _, id)| *id))
157    }
158
159    /// The single variable covering a slot (when the slot is not reused).
160    pub fn sole_on_slot(&self, slot: u16) -> Option<u32> {
161        let segs = self.by_slot.get(slot as usize)?;
162        if segs.len() == 1 {
163            Some(segs[0].2)
164        } else {
165            None
166        }
167    }
168
169    pub fn vars_on_slot(&self, slot: u16) -> &[(u16, u16, u32)] {
170        self.by_slot
171            .get(slot as usize)
172            .map(|v| v.as_slice())
173            .unwrap_or(&[])
174    }
175
176    pub fn var(&self, id: u32) -> &VarInfo {
177        self.vars.get(id as usize).unwrap_or_else(|| dummy_var(id))
178    }
179
180    pub fn var_mut(&mut self, id: u32) -> &mut VarInfo {
181        &mut self.vars[id as usize]
182    }
183}
184
185/// Out-of-range fallback for `VarTable::var`: a PER-ID leaked dummy.
186/// The previous form was a single process-wide `OnceLock<VarInfo>` —
187/// initialized by whichever worker thread first hit a dangling id, so
188/// EVERY dangling reference in the run printed THAT thread's id
189/// (`var0`/`var1` flips across identical runs; three reqable enum
190/// families flipped in lockstep). Keying by id makes the fallback
191/// deterministic; leaks are bounded by distinct dangling ids per run.
192fn dummy_var(id: u32) -> &'static VarInfo {
193    use std::collections::HashMap;
194    use std::sync::{Mutex, OnceLock};
195    static DUMMIES: OnceLock<Mutex<HashMap<u32, &'static VarInfo>>> = OnceLock::new();
196    let map = DUMMIES.get_or_init(|| Mutex::new(HashMap::new()));
197    let mut g = map.lock().unwrap_or_else(|e| e.into_inner());
198    if let Some(&v) = g.get(&id) {
199        return v;
200    }
201    let v: &'static VarInfo = Box::leak(Box::new(VarInfo {
202        id,
203        slot: u16::MAX,
204        name: format!("var{}", id),
205        ty: TypeRef::J(crate::types::JavaType::Int),
206        is_param: false,
207        range_start: 0,
208        range_end: 0,
209        synthetic_name: true,
210    }));
211    g.insert(id, v);
212    v
213}
214
215pub fn sig_type_at(
216    lvtt: &[(u16, u16, String, u16)],
217    start: u16,
218    slot: u16,
219    base: &JavaType,
220) -> Option<TypeRef> {
221    lvtt.iter()
222        .find(|(s, _, _, sl)| *s == start && *sl == slot)
223        .and_then(|(_, _, sig, _)| crate::types::parse_field_signature(sig).map(TypeRef::G))
224        // Reject signatures that cannot describe the descriptor type (a
225        // merged LVT range can accidentally align with an unrelated
226        // generic entry, e.g. a type variable E over a concrete class).
227        .filter(|tr| sig_matches_base(tr, base))
228}
229
230pub fn sig_matches_base(tr: &TypeRef, base: &JavaType) -> bool {
231    let g = match tr {
232        TypeRef::G(g) => g,
233        TypeRef::J(_) => return true,
234    };
235    match g {
236        // A type variable erases to its bound (Object or a concrete
237        // class), so any reference descriptor is compatible.
238        crate::types::GenericType::TypeVar(_) => matches!(base, JavaType::Object(_)),
239        crate::types::GenericType::Class(cs) => {
240            matches!(base, JavaType::Object(n) if n.as_ref() == cs.internal_name())
241        }
242        crate::types::GenericType::Primitive(c) => base.primitive_char() == Some(*c),
243        crate::types::GenericType::Array(_) => matches!(base, JavaType::Array(_)),
244        crate::types::GenericType::Wildcard(_) => false,
245    }
246}
247
248/// Placeholder type used when the builder cannot infer a better one.
249pub fn unknown_ref_type() -> TypeRef {
250    TypeRef::G(GenericType::Class(crate::types::ClassSig {
251        package: "java/lang".into(),
252        parts: vec![crate::types::ClassSigPart {
253            name: "Object".into(),
254            args: vec![],
255        }],
256    }))
257}