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}