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    /// Exception-table handler start pcs: an LVT range beginning at one
43    /// (or at its astore + 1/2) is a catch parameter binding. Such
44    /// ranges never receive forward store attribution — a store just
45    /// BEFORE a handler initializes the try-body variable, not the
46    /// catch param (jdk11 HostnameChecker.matchDNS: `sni = new
47    /// SNIHostName(..)` at pc 8 landed on `iae` whose handler range
48    /// starts at 14, within the 8-byte lead-in — SNIHostName无法转换为
49    /// IllegalArgumentException).
50    pub handler_starts: Vec<u16>,
51}
52
53impl VarTable {
54    pub fn add_stack_var(&mut self, slot: u16, name: String, ty: TypeRef) -> u32 {
55        let id = self.add(slot, name, ty, false, 0, u16::MAX, true);
56        self.stack_vars.push(id);
57        id
58    }
59
60    /// Allocate a synthetic variable that is NOT a stack-merge temp (e.g. a
61    /// catch parameter); it must not be hoisted by the stack-var pass.
62    /// Add a new variable identity for slot-reuse splitting (post-build;
63    /// not registered in pc-range lookups).
64    pub fn add_split(&mut self, slot: u16, name: String, ty: TypeRef) -> u32 {
65        let id = self.vars.len() as u32;
66        self.vars.push(VarInfo {
67            id,
68            slot,
69            name,
70            ty,
71            is_param: false,
72            range_start: 0,
73            range_end: u16::MAX,
74            synthetic_name: true,
75        });
76        id
77    }
78
79    pub fn add_catch_var(&mut self, slot: u16, name: String, ty: TypeRef) -> u32 {
80        self.add(slot, name, ty, false, 0, u16::MAX, true)
81    }
82
83    #[allow(clippy::too_many_arguments)] // one builder for all var kinds
84    fn add(
85        &mut self,
86        slot: u16,
87        name: String,
88        ty: TypeRef,
89        is_param: bool,
90        rs: u16,
91        re: u16,
92        synth: bool,
93    ) -> u32 {
94        let id = self.vars.len() as u32;
95        self.vars.push(VarInfo {
96            id,
97            slot,
98            name,
99            ty,
100            is_param,
101            range_start: rs,
102            range_end: re,
103            synthetic_name: synth,
104        });
105        while self.by_slot.len() <= slot as usize {
106            self.by_slot.push(Vec::new());
107        }
108        self.by_slot[slot as usize].push((rs, re, id));
109        id
110    }
111
112    /// Resolve the variable live at (pc, slot). Falls back to the nearest
113    /// preceding range, then to any var on the slot.
114    pub fn at(&self, slot: u16, pc: u16) -> Option<u32> {
115        let segs = self.by_slot.get(slot as usize)?;
116        for (rs, re, id) in segs {
117            if pc >= *rs && pc < *re {
118                return Some(*id);
119            }
120        }
121        // javac often starts an LVT range just AFTER the storing
122        // instruction; attribute a nearby access to the next range before
123        // falling back to the previous one (keeps slot-reuse splits like
124        // `byte[] dst` / `int b` on one slot apart). Never forward-
125        // attribute into a catch-param range (starts at a handler pc or
126        // its astore+1/+2): the store before a handler initializes the
127        // try-body variable (jdk11 HostnameChecker.matchDNS `sni = new
128        // SNIHostName(..)` was typed IllegalArgumentException).
129        for (rs, _, id) in segs {
130            if *rs > pc
131                && *rs - pc <= 8
132                && !self
133                    .handler_starts
134                    .iter()
135                    .any(|h| *rs == *h || *rs == h + 1 || *rs == h + 2)
136            {
137                return Some(*id);
138            }
139        }
140        let mut best: Option<u32> = None;
141        for (rs, _, id) in segs {
142            if *rs <= pc {
143                best = Some(*id);
144            }
145        }
146        best.or_else(|| segs.first().map(|(_, _, id)| *id))
147    }
148
149    /// The single variable covering a slot (when the slot is not reused).
150    pub fn sole_on_slot(&self, slot: u16) -> Option<u32> {
151        let segs = self.by_slot.get(slot as usize)?;
152        if segs.len() == 1 {
153            Some(segs[0].2)
154        } else {
155            None
156        }
157    }
158
159    pub fn vars_on_slot(&self, slot: u16) -> &[(u16, u16, u32)] {
160        self.by_slot
161            .get(slot as usize)
162            .map(|v| v.as_slice())
163            .unwrap_or(&[])
164    }
165
166    pub fn var(&self, id: u32) -> &VarInfo {
167        self.vars.get(id as usize).unwrap_or_else(|| dummy_var(id))
168    }
169
170    pub fn var_mut(&mut self, id: u32) -> &mut VarInfo {
171        &mut self.vars[id as usize]
172    }
173}
174
175/// Out-of-range fallback for `VarTable::var`: a PER-ID leaked dummy.
176/// The previous form was a single process-wide `OnceLock<VarInfo>` —
177/// initialized by whichever worker thread first hit a dangling id, so
178/// EVERY dangling reference in the run printed THAT thread's id
179/// (`var0`/`var1` flips across identical runs; three reqable enum
180/// families flipped in lockstep). Keying by id makes the fallback
181/// deterministic; leaks are bounded by distinct dangling ids per run.
182fn dummy_var(id: u32) -> &'static VarInfo {
183    use std::collections::HashMap;
184    use std::sync::{Mutex, OnceLock};
185    static DUMMIES: OnceLock<Mutex<HashMap<u32, &'static VarInfo>>> = OnceLock::new();
186    let map = DUMMIES.get_or_init(|| Mutex::new(HashMap::new()));
187    let mut g = map.lock().unwrap_or_else(|e| e.into_inner());
188    if let Some(&v) = g.get(&id) {
189        return v;
190    }
191    let v: &'static VarInfo = Box::leak(Box::new(VarInfo {
192        id,
193        slot: u16::MAX,
194        name: format!("var{}", id),
195        ty: TypeRef::J(crate::types::JavaType::Int),
196        is_param: false,
197        range_start: 0,
198        range_end: 0,
199        synthetic_name: true,
200    }));
201    g.insert(id, v);
202    v
203}
204
205pub fn sig_type_at(
206    lvtt: &[(u16, u16, String, u16)],
207    start: u16,
208    slot: u16,
209    base: &JavaType,
210) -> Option<TypeRef> {
211    lvtt.iter()
212        .find(|(s, _, _, sl)| *s == start && *sl == slot)
213        .and_then(|(_, _, sig, _)| crate::types::parse_field_signature(sig).map(TypeRef::G))
214        // Reject signatures that cannot describe the descriptor type (a
215        // merged LVT range can accidentally align with an unrelated
216        // generic entry, e.g. a type variable E over a concrete class).
217        .filter(|tr| sig_matches_base(tr, base))
218}
219
220pub fn sig_matches_base(tr: &TypeRef, base: &JavaType) -> bool {
221    let g = match tr {
222        TypeRef::G(g) => g,
223        TypeRef::J(_) => return true,
224    };
225    match g {
226        // A type variable erases to its bound (Object or a concrete
227        // class), so any reference descriptor is compatible.
228        crate::types::GenericType::TypeVar(_) => matches!(base, JavaType::Object(_)),
229        crate::types::GenericType::Class(cs) => {
230            matches!(base, JavaType::Object(n) if n.as_ref() == cs.internal_name())
231        }
232        crate::types::GenericType::Primitive(c) => base.primitive_char() == Some(*c),
233        crate::types::GenericType::Array(_) => matches!(base, JavaType::Array(_)),
234        crate::types::GenericType::Wildcard(_) => false,
235    }
236}
237
238/// Placeholder type used when the builder cannot infer a better one.
239pub fn unknown_ref_type() -> TypeRef {
240    TypeRef::G(GenericType::Class(crate::types::ClassSig {
241        package: "java/lang".into(),
242        parts: vec![crate::types::ClassSigPart {
243            name: "Object".into(),
244            args: vec![],
245        }],
246    }))
247}