Skip to main content

luna_core/vm/
objname.rs

1//! Recover a source-level name for a register, for variable-aware error
2//! messages (PUC ldebug.c getobjname/findsetreg). Given the bytecode and the
3//! pc of the faulting instruction, trace a register back to the instruction
4//! that last wrote it and classify it as a field/global/upvalue/method/etc.
5//!
6//! Local-variable names are not yet recovered (the Proto carries no locvar
7//! debug table), so a register sourced from a plain local yields no name;
8//! callers then emit a bare message without the "(kind 'name')" suffix.
9
10use crate::runtime::Value;
11use crate::runtime::function::Proto;
12use crate::vm::isa::{Inst, Op};
13
14/// The string constant at index `c`, if it is a string.
15fn kname(proto: &Proto, c: u32) -> Option<String> {
16    match proto.consts.get(c as usize) {
17        Some(Value::Str(s)) => Some(String::from_utf8_lossy(s.as_bytes()).into_owned()),
18        _ => None,
19    }
20}
21
22fn upvalname(proto: &Proto, u: u32) -> Option<String> {
23    proto.upvals.get(u as usize).map(|d| d.name.to_string())
24}
25
26/// Does instruction `i` write to register `reg`?
27fn writes_reg(i: Inst, reg: u32) -> bool {
28    let a = i.a();
29    match i.op() {
30        Op::LoadNil => a <= reg && reg <= a + i.b(),
31        Op::SelfOp => reg == a || reg == a + 1,
32        // these write a run of registers starting at A (results / varargs)
33        Op::Call | Op::TailCall | Op::Vararg => reg >= a,
34        Op::TForCall => reg >= a + 4,
35        // control / store / no-result opcodes write no destination register
36        Op::Jmp
37        | Op::SetUpval
38        | Op::SetTabUp
39        | Op::SetTable
40        | Op::SetI
41        | Op::SetField
42        | Op::Close
43        | Op::Tbc
44        | Op::Eq
45        | Op::Lt
46        | Op::Le
47        | Op::EqK
48        | Op::Test
49        | Op::Return
50        | Op::Return0
51        | Op::Return1
52        | Op::SetList
53        | Op::ExtraArg
54        | Op::TForPrep => false,
55        _ => reg == a,
56    }
57}
58
59/// PUC findsetreg: the pc of the last instruction before `lastpc` that sets
60/// `reg`, or None if it is set inside a jump target region (ambiguous) or
61/// never set.
62fn find_setreg(proto: &Proto, lastpc: usize, reg: u32) -> Option<usize> {
63    let mut setreg: Option<usize> = None;
64    let mut jmptarget: usize = 0;
65    let code = &proto.code;
66    for pc in 0..lastpc {
67        let i = code[pc];
68        if i.op() == Op::Jmp {
69            let dest = (pc as i64 + 1 + i.sj() as i64) as usize;
70            if dest <= lastpc && dest > jmptarget {
71                jmptarget = dest;
72            }
73            continue;
74        }
75        if writes_reg(i, reg) {
76            setreg = if pc < jmptarget { None } else { Some(pc) };
77        }
78    }
79    setreg
80}
81
82/// PUC basicgetobjname: a register's name reached only through locals, plain
83/// register moves, and upvalues — never through table indexing. `gxf` needs
84/// this so a field that merely happens to be named `_ENV` (e.g. `a._ENV.x`) is
85/// not mistaken for the real global environment.
86fn basicgetobjname(proto: &Proto, lastpc: usize, reg: u32) -> Option<(&'static str, String)> {
87    if let Some(name) = getlocalname(proto, reg, lastpc) {
88        return Some(("local", name.to_string()));
89    }
90    let setpc = find_setreg(proto, lastpc, reg)?;
91    let i = proto.code[setpc];
92    match i.op() {
93        Op::Move => {
94            let b = i.b();
95            if b < i.a() {
96                basicgetobjname(proto, setpc, b)
97            } else {
98                None
99            }
100        }
101        Op::GetUpval => upvalname(proto, i.b()).map(|n| ("upvalue", n)),
102        Op::LoadK => kname(proto, i.bx()).map(|n| ("constant", n)),
103        // LoadKx is the 32-bit-index variant: the constant index lives in the
104        // immediately-following ExtraArg op's Ax field, so a name can still be
105        // recovered (5.4/5.3 big.lua's huge prog blows past LoadK's 18-bit
106        // limit and would otherwise erase the "(global 'X')" subject info).
107        Op::LoadKx => {
108            let next = proto.code.get(setpc + 1)?;
109            if next.op() != Op::ExtraArg {
110                return None;
111            }
112            kname(proto, next.ax()).map(|n| ("constant", n))
113        }
114        _ => None,
115    }
116}
117
118/// PUC rname: the name of register `c` when it holds a constant key (e.g. a
119/// global read compiled as `GETTABLE` with the key LOADK'd into a register);
120/// "?" otherwise.
121fn rname(proto: &Proto, pc: usize, c: u32) -> String {
122    match basicgetobjname(proto, pc, c) {
123        Some(("constant", n)) => n,
124        _ => "?".to_string(),
125    }
126}
127
128/// gxf (PUC isEnv): an indexed access names a "global" when the table is the
129/// real `_ENV`, else a "field". `isup` distinguishes GETTABUP (table is an
130/// upvalue) from GETFIELD. The table only counts as `_ENV` when it is reached
131/// as a local or an upvalue — not as some other field named `_ENV`.
132fn gxf(proto: &Proto, pc: usize, i: Inst, isup: bool) -> &'static str {
133    let t = i.b();
134    let tname = if isup {
135        upvalname(proto, t)
136    } else {
137        match basicgetobjname(proto, pc, t) {
138            Some((kind, n)) if kind == "local" || kind == "upvalue" => Some(n),
139            _ => None,
140        }
141    };
142    if tname.as_deref() == Some("_ENV") {
143        "global"
144    } else {
145        "field"
146    }
147}
148
149/// The name of the local variable occupying `reg` and live at `pc`, if any.
150/// On register reuse the innermost (latest-starting) live local wins.
151pub fn getlocalname(proto: &Proto, reg: u32, pc: usize) -> Option<&str> {
152    let pc = pc as u32;
153    proto
154        .locvars
155        .iter()
156        .filter(|lv| lv.reg == reg && lv.start_pc <= pc && pc < lv.end_pc)
157        .max_by_key(|lv| lv.start_pc)
158        .map(|lv| &*lv.name)
159}
160
161/// Name and kind for `reg` as of `lastpc`, e.g. ("field", "huge"). None when
162/// the register has no recoverable source-level name.
163pub fn getobjname(proto: &Proto, lastpc: usize, reg: u32) -> Option<(&'static str, String)> {
164    // PUC order: a live local takes precedence over symbolic execution
165    if let Some(name) = getlocalname(proto, reg, lastpc) {
166        return Some(("local", name.to_string()));
167    }
168    let setpc = find_setreg(proto, lastpc, reg)?;
169    let i = proto.code[setpc];
170    match i.op() {
171        Op::Move => {
172            let b = i.b();
173            // trace the source register, but only backwards to avoid cycles
174            if b < i.a() {
175                getobjname(proto, setpc, b)
176            } else {
177                None
178            }
179        }
180        Op::GetUpval => upvalname(proto, i.b()).map(|n| ("upvalue", n)),
181        Op::GetTabUp => kname(proto, i.c()).map(|n| (gxf(proto, setpc, i, true), n)),
182        Op::GetField => kname(proto, i.c()).map(|n| (gxf(proto, setpc, i, false), n)),
183        // a register-keyed read (global with a constant index past the GETFIELD
184        // C-operand limit, or an explicit `t[k]`): name from the key register.
185        Op::GetTable => Some((gxf(proto, setpc, i, false), rname(proto, setpc, i.c()))),
186        Op::GetI => Some(("field", "integer index".to_string())),
187        Op::SelfOp => {
188            // SELF's C is RK: a constant when the k-flag is set, otherwise a
189            // register holding the (constant-loaded) key. The latter path is
190            // taken when the method-name constant index doesn't fit OP_SELF's
191            // 8-bit C field (errors.lua :303 — `t:bbb()` past 1000 constants).
192            let name = if i.k() {
193                kname(proto, i.c())
194            } else {
195                Some(rname(proto, setpc, i.c()))
196            };
197            name.map(|n| ("method", n))
198        }
199        _ => None,
200    }
201}