载入中...
搜索中...
未找到
Debugger.cpp
浏览该文件的文档.
2
4
6
7#include <simplesquirrel/simplesquirrel.hpp>
8#include <squirrel.h>
9
10#include <algorithm>
11#include <cctype>
12#include <chrono>
13#include <cstring>
14#include <fstream>
15#include <thread>
16#include <utility>
17
18namespace eve::dev {
19namespace {
20
21std::string truthyString(bool v) { return v ? "true" : "false"; }
22
23std::string describeSqValue(HSQUIRRELVM vm, SQInteger idx) {
24 const SQObjectType t = sq_gettype(vm, idx);
25 switch (t) {
26 case OT_NULL:
27 return "null";
28 case OT_INTEGER: {
29 SQInteger v = 0;
30 sq_getinteger(vm, idx, &v);
31 return std::to_string(static_cast<long long>(v));
32 }
33 case OT_FLOAT: {
34 SQFloat v = 0;
35 sq_getfloat(vm, idx, &v);
36 return std::to_string(static_cast<double>(v));
37 }
38 case OT_BOOL: {
39 SQBool v = SQFalse;
40 sq_getbool(vm, idx, &v);
41 return v ? "true" : "false";
42 }
43 case OT_STRING: {
44 const SQChar* s = nullptr;
45 sq_getstring(vm, idx, &s);
46 return s ? std::string("\"") + s + "\"" : "\"\"";
47 }
48 case OT_TABLE:
49 return "<table>";
50 case OT_ARRAY:
51 return "<array>";
52 case OT_USERDATA:
53 return "<userdata>";
54 case OT_CLOSURE:
55 return "<closure>";
56 case OT_NATIVECLOSURE:
57 return "<native>";
58 case OT_INSTANCE:
59 return "<instance>";
60 case OT_CLASS:
61 return "<class>";
62 case OT_THREAD:
63 return "<thread>";
64 default:
65 return "<other>";
66 }
67}
68
69std::string typeName(HSQUIRRELVM vm, SQInteger idx) {
70 switch (sq_gettype(vm, idx)) {
71 case OT_NULL:
72 return "null";
73 case OT_INTEGER:
74 return "integer";
75 case OT_FLOAT:
76 return "float";
77 case OT_BOOL:
78 return "bool";
79 case OT_STRING:
80 return "string";
81 case OT_TABLE:
82 return "table";
83 case OT_ARRAY:
84 return "array";
85 case OT_USERDATA:
86 return "userdata";
87 case OT_CLOSURE:
88 return "closure";
89 case OT_NATIVECLOSURE:
90 return "native";
91 case OT_INSTANCE:
92 return "instance";
93 case OT_CLASS:
94 return "class";
95 case OT_THREAD:
96 return "thread";
97 default:
98 return "other";
99 }
100}
101
102std::string typeName(HSQUIRRELVM vm, SQInteger idx);
103
105VariableInfo describeAt(HSQUIRRELVM vm, SQInteger idx) {
106 VariableInfo info;
107 const SQObjectType t = sq_gettype(vm, idx);
108 info.type = typeName(vm, idx);
109 switch (t) {
110 case OT_NULL:
111 info.value = "null";
112 break;
113 case OT_INTEGER: {
114 SQInteger v = 0;
115 sq_getinteger(vm, idx, &v);
116 info.value = std::to_string(static_cast<long long>(v));
117 break;
118 }
119 case OT_FLOAT: {
120 SQFloat v = 0;
121 sq_getfloat(vm, idx, &v);
122 info.value = std::to_string(static_cast<double>(v));
123 break;
124 }
125 case OT_BOOL: {
126 SQBool v = SQFalse;
127 sq_getbool(vm, idx, &v);
128 info.value = v ? "true" : "false";
129 break;
130 }
131 case OT_STRING: {
132 const SQChar* s = nullptr;
133 sq_getstring(vm, idx, &s);
134 info.value = std::string("\"") + (s ? s : "") + "\"";
135 break;
136 }
137 case OT_TABLE:
138 case OT_ARRAY: {
139 const SQInteger n = sq_getsize(vm, idx);
140 info.value = std::string(t == OT_TABLE ? "<table (" : "<array (") +
141 std::to_string(static_cast<long long>(n)) + ")>";
142 info.expandable = true;
143 info.childCount = static_cast<int>(n);
144 break;
145 }
146 case OT_CLASS:
147 info.value = "<class>";
148 info.expandable = true;
149 break;
150 case OT_INSTANCE:
151 info.value = "<instance>";
152 info.expandable = true;
153 break;
154 case OT_USERDATA:
155 info.value = "<userdata>";
156 break;
157 case OT_CLOSURE:
158 info.value = "<closure>";
159 info.expandable = true; // expandable: free variables
160 break;
161 case OT_NATIVECLOSURE:
162 info.value = "<native>";
163 break;
164 case OT_THREAD:
165 info.value = "<thread>";
166 break;
167 case OT_GENERATOR:
168 info.value = "<generator>";
169 break;
170 default:
171 info.value = "<other>";
172 break;
173 }
174 return info;
175}
176
178std::vector<VariableInfo> enumerateAt(HSQUIRRELVM vm, SQInteger idx) {
179 std::vector<VariableInfo> out;
180 const SQObjectType t = sq_gettype(vm, idx);
181 const SQInteger top = sq_gettop(vm);
182 const int absIdx = idx < 0 ? static_cast<int>(top) + static_cast<int>(idx) + 1 : static_cast<int>(idx);
183
184 if (t == OT_ARRAY) {
185 const SQInteger size = sq_getsize(vm, idx);
186 for (SQInteger i = 0; i < size; ++i) {
187 const SQInteger before = sq_gettop(vm);
188 sq_pushinteger(vm, i);
189 if (SQ_FAILED(sq_get(vm, absIdx))) {
190 sq_settop(vm, before);
191 continue;
192 }
193 VariableInfo info = describeAt(vm, -1);
194 info.name = std::to_string(static_cast<long long>(i));
195 sq_settop(vm, before);
196 out.push_back(std::move(info));
197 }
198 return out;
199 }
200
201 if (t == OT_TABLE || t == OT_CLASS) {
202 sq_pushinteger(vm, 0); // iterator position
203 while (SQ_SUCCEEDED(sq_next(vm, absIdx))) {
204 VariableInfo info = describeAt(vm, -1);
205 switch (sq_gettype(vm, -2)) {
206 case OT_STRING: {
207 const SQChar* s = nullptr;
208 sq_getstring(vm, -2, &s);
209 info.name = s ? s : "";
210 break;
211 }
212 case OT_INTEGER: {
213 SQInteger k = 0;
214 sq_getinteger(vm, -2, &k);
215 info.name = std::to_string(static_cast<long long>(k));
216 break;
217 }
218 case OT_FLOAT: {
219 SQFloat k = 0;
220 sq_getfloat(vm, -2, &k);
221 info.name = std::to_string(static_cast<double>(k));
222 break;
223 }
224 case OT_BOOL: {
225 SQBool k = SQFalse;
226 sq_getbool(vm, -2, &k);
227 info.name = truthyString(k != SQFalse);
228 break;
229 }
230 default:
231 info.name = "<key>";
232 break;
233 }
234 sq_pop(vm, 2);
235 out.push_back(std::move(info));
236 }
237 sq_settop(vm, top);
238 return out;
239 }
240
241 if (t == OT_INSTANCE) {
242 sq_getclass(vm, absIdx); // class at top
243 const int clsAbs = static_cast<int>(sq_gettop(vm));
244 sq_pushinteger(vm, 0);
245 while (SQ_SUCCEEDED(sq_next(vm, clsAbs))) {
246 const SQChar* s = nullptr;
247 if (sq_gettype(vm, -2) == OT_STRING) sq_getstring(vm, -2, &s);
248 const std::string name = s ? s : "";
249 const SQInteger before = sq_gettop(vm);
250 sq_push(vm, absIdx); // instance
251 sq_pushstring(vm, name.c_str(), -1);
252 VariableInfo info;
253 if (SQ_SUCCEEDED(sq_get(vm, -2))) {
254 info = describeAt(vm, -1);
255 } else {
256 info.type = "error";
257 info.value = "not found";
258 }
259 info.name = name;
260 sq_settop(vm, before);
261 sq_pop(vm, 2); // drop key + value; keep iterator position
262 out.push_back(std::move(info));
263 }
264 sq_settop(vm, top);
265 return out;
266 }
267
268 if (t == OT_CLOSURE) {
269 SQInteger nparams = 0, nfree = 0;
270 if (SQ_SUCCEEDED(sq_getclosureinfo(vm, idx, &nparams, &nfree))) {
271 for (SQUnsignedInteger i = 0; i < static_cast<SQUnsignedInteger>(nfree); ++i) {
272 const SQChar* n = sq_getfreevariable(vm, absIdx, i);
273 VariableInfo info = describeAt(vm, -1);
274 info.name = n ? n : ("upvalue_" + std::to_string(static_cast<long long>(i)));
275 sq_poptop(vm);
276 out.push_back(std::move(info));
277 }
278 }
279 return out;
280 }
281 return out;
282}
283
284bool valueTruthy(const VariableInfo& info) {
285 if (info.type == "error") return false;
286 if (info.type == "null") return false;
287 if (info.type == "bool") return info.value == "true";
288 if (info.type == "integer") {
289 try {
290 return std::stoll(info.value) != 0;
291 } catch (...) {
292 return true;
293 }
294 }
295 if (info.type == "float") {
296 try {
297 return std::stod(info.value) != 0.0;
298 } catch (...) {
299 return true;
300 }
301 }
302 return true;
303}
304
305bool isGenericFuncName(const char* name) {
306 if (!name || !name[0] || std::strcmp(name, "?") == 0) return true;
307 return std::strcmp(name, "unknown") == 0 || std::strcmp(name, "NATIVE") == 0 ||
308 std::strcmp(name, "anonymous") == 0;
309}
310
311bool isGenericFuncName(const std::string& name) { return isGenericFuncName(name.c_str()); }
312
313std::string nthLine(const std::string& text, int line) {
314 if (line <= 0) return {};
315 int n = 1;
316 size_t start = 0;
317 while (n < line) {
318 const size_t nl = text.find('\n', start);
319 if (nl == std::string::npos) return {};
320 start = nl + 1;
321 ++n;
322 }
323 size_t end = text.find('\n', start);
324 if (end == std::string::npos) end = text.size();
325 std::string out = text.substr(start, end - start);
326 if (!out.empty() && out.back() == '\r') out.pop_back();
327 return out;
328}
329
330std::string inferNameFromDefinitionLine(std::string line) {
331 while (!line.empty() && std::isspace(static_cast<unsigned char>(line.front()))) line.erase(line.begin());
332 if (line.rfind("//", 0) == 0) return {};
333 const auto isIdent = [](unsigned char c) { return std::isalnum(c) || c == '_' || c == '.'; };
334 if (line.rfind("function ", 0) == 0) {
335 size_t i = 9;
336 while (i < line.size() && std::isspace(static_cast<unsigned char>(line[i]))) ++i;
337 const size_t start = i;
338 while (i < line.size() && isIdent(static_cast<unsigned char>(line[i]))) ++i;
339 if (i > start) return line.substr(start, i - start);
340 return {};
341 }
342 size_t i = 0;
343 if (i < line.size() && (std::isalpha(static_cast<unsigned char>(line[i])) || line[i] == '_')) {
344 const size_t start = i;
345 while (i < line.size() && isIdent(static_cast<unsigned char>(line[i]))) ++i;
346 const std::string ident = line.substr(start, i - start);
347 while (i < line.size() && std::isspace(static_cast<unsigned char>(line[i]))) ++i;
348 bool assign = false;
349 if (i + 1 < line.size() && line[i] == '<' && line[i + 1] == '-') {
350 i += 2;
351 assign = true;
352 } else if (i < line.size() && line[i] == '=') {
353 ++i;
354 assign = true;
355 }
356 if (!assign) return {};
357 while (i < line.size() && std::isspace(static_cast<unsigned char>(line[i]))) ++i;
358 if (line.compare(i, 6, "async ") == 0) {
359 i += 6;
360 while (i < line.size() && std::isspace(static_cast<unsigned char>(line[i]))) ++i;
361 }
362 if (line.compare(i, 8, "function") == 0) return ident;
363 }
364 return {};
365}
366
367std::string readDefinitionLine(const std::string& source, int line) {
368 if (source.empty() || line <= 0) return {};
369 std::ifstream in(source);
370 if (!in) {
371 const auto slash = source.find_last_of("/\\");
372 if (slash != std::string::npos) in.open(source.substr(slash + 1));
373 }
374 if (!in) return {};
375 std::string text;
376 for (int n = 1; n <= line && std::getline(in, text); ++n) {
377 if (n == line) {
378 if (!text.empty() && text.back() == '\r') text.pop_back();
379 return text;
380 }
381 }
382 return {};
383}
384
385} // namespace
386
388 static Debugger inst;
389 return inst;
390}
391
392std::string Debugger::normalizeSource(std::string source) {
393 if (source.empty()) return source;
394 // Strip common URI prefixes VS Code may send.
395 if (source.rfind("file://", 0) == 0) {
396 source = source.substr(7);
397 // file://localhost/Users/... → /Users/...
398 if (source.rfind("localhost/", 0) == 0) source = source.substr(9);
399 }
400 // Unify separators.
401 for (char& c : source) {
402 if (c == '\\') c = '/';
403 }
404 // Drop leading ./
405 while (source.size() >= 2 && source[0] == '.' && source[1] == '/') source = source.substr(2);
406 return source;
407}
408
409std::string Debugger::sourceBasename(const std::string& source) {
410 const std::string norm = normalizeSource(source);
411 const auto slash = norm.find_last_of('/');
412 return (slash == std::string::npos) ? norm : norm.substr(slash + 1);
413}
414
415bool Debugger::sourcesMatch(const std::string& a, const std::string& b) {
416 const std::string na = normalizeSource(a);
417 const std::string nb = normalizeSource(b);
418 if (na.empty() || nb.empty()) return false;
419 if (na == nb) return true;
420
421 const std::string& longer = na.size() >= nb.size() ? na : nb;
422 const std::string& shorter = na.size() >= nb.size() ? nb : na;
423 if (longer.size() > shorter.size() &&
424 longer.compare(longer.size() - shorter.size(), shorter.size(), shorter) == 0) {
425 const auto idx = longer.size() - shorter.size();
426 if (idx == 0 || longer[idx - 1] == '/') return true;
427 }
428
429 // Bare filenames (no slash) still match any path with the same basename so
430 // Squirrel's `main.nut` can hit a VS Code absolute breakpoint. Two paths
431 // that both include a directory must not match on basename alone.
432 const bool aHasDir = na.find('/') != std::string::npos;
433 const bool bHasDir = nb.find('/') != std::string::npos;
434 if (aHasDir && bHasDir) return false;
435 const std::string ba = sourceBasename(na);
436 const std::string bb = sourceBasename(nb);
437 return !ba.empty() && ba == bb;
438}
439
441 std::lock_guard<std::mutex> lock(mu_);
442 vm_ = vm;
443 mode_ = RunMode::Running;
444 reason_ = PauseReason::None;
445 pauseLoc_ = {};
446 stepFrameArmed_ = false;
447 ownGraph_.clear();
448 virtualSources_.clear();
449}
450
452 std::lock_guard<std::mutex> lock(mu_);
453 vm_ = nullptr;
454 mode_ = RunMode::Running;
455 reason_ = PauseReason::None;
456 stepFrameArmed_ = false;
457 callGraph_ = nullptr;
458 ownGraph_.clear();
459 virtualSources_.clear();
460}
461
463 std::lock_guard<std::mutex> lock(mu_);
464 callGraph_ = graph;
465}
466
467void Debugger::setVirtualSource(std::string source, std::string text) {
468 std::lock_guard<std::mutex> lock(mu_);
469 virtualSources_[normalizeSource(std::move(source))] = std::move(text);
470}
471
472CallGraph& Debugger::activeGraph() {
473 return callGraph_ ? *callGraph_ : ownGraph_;
474}
475
476const CallGraph& Debugger::activeGraph() const {
477 return callGraph_ ? *callGraph_ : ownGraph_;
478}
479
480std::string Debugger::lineAt(const std::string& source, int line) const {
481 if (line <= 0) return {};
482 {
483 std::lock_guard<std::mutex> lock(mu_);
484 for (const auto& [name, text] : virtualSources_) {
485 if (sourcesMatch(name, source)) return nthLine(text, line);
486 }
487 }
488 return readDefinitionLine(source, line);
489}
490
491std::string Debugger::resolveCallName(const SourceLoc& loc) const {
492 if (!isGenericFuncName(loc.function)) return loc.function;
493 // Only the hook's own line: loc.line-1 is often the *caller's* definition
494 // when Squirrel reports the call site.
495 const std::string name = inferNameFromDefinitionLine(lineAt(loc.source, loc.line));
496 if (!name.empty()) return name;
497 return loc.function.empty() ? "unknown" : loc.function;
498}
499
500std::string Debugger::onCall(SourceLoc loc) {
501 const std::string name = resolveCallName(loc);
502 loc.function = name;
503 activeGraph().onCall(loc, name);
504 return name;
505}
506
508 activeGraph().onReturn(loc, loc.function);
509}
510
511std::string Debugger::nameFromPrototype(int level, const char* rawName, const SourceLoc& loc) const {
512 if (!isGenericFuncName(rawName)) return rawName;
513 if (vm_) {
514 SQFunctionInfo fi;
515 std::memset(&fi, 0, sizeof(fi));
516 if (SQ_SUCCEEDED(sq_getfunctioninfo(vm_, level, &fi))) {
517 if (!isGenericFuncName(fi.name)) return fi.name;
518 const char* src = (fi.source && fi.source[0]) ? fi.source : loc.source.c_str();
519 const int defLine = fi.line > 0 ? static_cast<int>(fi.line) : loc.line;
520 const std::string inferred = inferNameFromDefinitionLine(lineAt(src, defLine));
521 if (!inferred.empty()) return inferred;
522 }
523 }
524 const std::string fromPc = inferNameFromDefinitionLine(lineAt(loc.source, loc.line));
525 if (!fromPc.empty()) return fromPc;
526 for (int ln = loc.line - 1; ln > 0 && ln >= loc.line - 32; --ln) {
527 const std::string inferred = inferNameFromDefinitionLine(lineAt(loc.source, ln));
528 if (!inferred.empty()) return inferred;
529 }
530 return "unknown";
531}
532
533void Debugger::applyCallGraphNames(std::vector<StackFrameInfo>& frames) const {
534 std::vector<std::string> cgInner;
535 const auto stack = activeGraph().currentStack();
536 for (auto it = stack.rbegin(); it != stack.rend(); ++it) {
537 if (it->loc.source == "NATIVE") continue;
538 cgInner.push_back(it->loc.function);
539 }
540 // Align from the outer end so an extra/missing inner CallGraph frame cannot
541 // relabel the current SQ frame as its caller.
542 const int delta = static_cast<int>(frames.size()) - static_cast<int>(cgInner.size());
543 for (size_t i = 0; i < frames.size(); ++i) {
544 if (!isGenericFuncName(frames[i].name)) continue;
545 const int cgi = static_cast<int>(i) - delta;
546 if (cgi < 0 || cgi >= static_cast<int>(cgInner.size())) continue;
547 if (isGenericFuncName(cgInner[static_cast<size_t>(cgi)])) continue;
548 frames[i].name = cgInner[static_cast<size_t>(cgi)];
549 frames[i].loc.function = frames[i].name;
550 }
551}
552
554 // PauseKey is frame-level: drop any stale script site so smart step()
555 // becomes stepFrame. Other reasons keep the last script location unless
556 // the caller uses pauseAt.
557 if (reason == PauseReason::PauseKey) {
558 pauseAt(reason, {});
559 return;
560 }
561 pauseAt(reason, pauseLoc_);
562}
563
565 reason_.store(reason);
566 mode_.store(RunMode::Paused);
567 remainingHostFrames_ = 0;
568 pauseLoc_ = std::move(loc);
569}
570
572 reason_.store(PauseReason::None);
573 mode_.store(RunMode::Running);
574 stepFrameArmed_ = false;
575 remainingHostFrames_ = 0;
576 stepStartDepth_ = 0;
577 stepSkipLoc_ = {};
578 pauseLoc_ = {};
579}
580
582 reason_.store(PauseReason::Step);
583 mode_.store(RunMode::StepFrame);
584 stepFrameArmed_ = true;
585}
586
588 remainingHostFrames_ = count < 1 ? 1 : count;
589 stepFrame();
590}
591
593 HSQUIRRELVM vm = vm_;
594 if (!vm) return 0;
595 int depth = 0;
596 for (int level = 0;; ++level) {
597 SQStackInfos si;
598 if (SQ_FAILED(sq_stackinfos(vm, level, &si))) break;
599 ++depth;
600 }
601 return depth;
602}
603
604void Debugger::beginScriptStep(RunMode stepMode) {
605 reason_.store(PauseReason::Step);
606 stepStartDepth_ = scriptStackDepth();
607 // Squirrel can emit several _OP_LINE for one source line; skip the line we
608 // are currently paused on until the location changes.
609 stepSkipLoc_ = pauseLoc_;
610 // Not currently inside a script frame (frame-level pause): stop on the
611 // first line we see — treat like stepInto with an open depth gate.
612 if (stepStartDepth_ <= 0) {
613 mode_.store(RunMode::StepInto);
614 stepStartDepth_ = 0;
615 return;
616 }
617 mode_.store(stepMode);
618}
619
620void Debugger::stepInto() { beginScriptStep(RunMode::StepInto); }
621
622void Debugger::stepOver() { beginScriptStep(RunMode::StepOver); }
623
625 // No caller to return to → just step over the current line.
626 if (scriptStackDepth() <= 1) {
627 beginScriptStep(RunMode::StepOver);
628 return;
629 }
630 beginScriptStep(RunMode::StepOut);
631}
632
634 // Prefer script step-over when we have a script pause site; else one frame.
635 const PauseReason r = reason_.load();
636 if (!pauseLoc_.empty() &&
639 stepOver();
640 return;
641 }
642 stepFrame();
643}
644
646 const RunMode m = mode_.load();
647 if (m == RunMode::Running) return true;
648 if (m == RunMode::StepFrame) return true;
649 // Allow the game loop to enter eve_update so script steps can begin after
650 // a frame-level pause.
651 if (m == RunMode::StepInto || m == RunMode::StepOver || m == RunMode::StepOut) return true;
652 return false; // Paused
653}
654
656 if (mode_.load() == RunMode::StepFrame || stepFrameArmed_) {
657 stepFrameArmed_ = false;
658 if (remainingHostFrames_ > 1) {
659 --remainingHostFrames_;
660 stepFrame();
661 return;
662 }
663 remainingHostFrames_ = 0;
664 reason_.store(PauseReason::Step);
665 mode_.store(RunMode::Paused);
666 // Frame step finished outside the line hook — next smart step is frame.
667 pauseLoc_ = {};
668 }
669}
670
671bool Debugger::matchBreakpoint(const std::string& source, int line) const {
672 if (!bpsEnabled_.load()) return false;
673 for (const auto& bp : bps_) {
674 if (!bp.enabled || bp.line != line) continue;
675 if (sourcesMatch(source, bp.source)) return true;
676 }
677 return false;
678}
679
680bool Debugger::matchBreakpointAny(const std::string& source, int line) const {
681 for (const auto& bp : bps_) {
682 if (bp.line != line) continue;
683 if (sourcesMatch(source, bp.source)) return true;
684 }
685 return false;
686}
687
688bool Debugger::conditionHolds(const Breakpoint& bp) const {
689 if (bp.condition.empty()) return true;
690 // Failed / unreadable conditions stop (safer than silently never stopping).
691 const VariableInfo r = evaluate(bp.condition, 0);
692 if (r.type == "error") return true;
693 return valueTruthy(r);
694}
695
697 const RunMode m = mode_.load();
698 if (m == RunMode::Paused) {
699 return true;
700 }
701
702 // Verification: any breakpoint whose exact line we just executed is real.
703 // Report it (once) even when stepping filters or conditions suppress the stop.
704 std::vector<int> verifiedNow;
705 {
706 std::lock_guard<std::mutex> lock(mu_);
707 if (!bps_.empty() && matchBreakpointAny(loc.source, loc.line)) {
708 for (auto& bp : bps_) {
709 if (bp.verified || bp.line != loc.line ||
710 !sourcesMatch(loc.source, bp.source))
711 continue;
712 bp.verified = true;
713 verifiedNow.push_back(bp.id);
714 }
715 }
716 }
717 for (const int id : verifiedNow) {
718 if (bpEventFn_) bpEventFn_(id, loc.source, loc.line, true);
719 }
720
721 const bool stepping =
723
724 // While leaving the paused line, ignore further events for that exact
725 // source+line (extra _OP_LINE and re-armed breakpoints on the same site).
726 if (stepping && !stepSkipLoc_.empty() && stepSkipLoc_.line == loc.line &&
727 sourcesMatch(stepSkipLoc_.source, loc.source)) {
728 return false;
729 }
730
731 // Breakpoints win over step filters (hit inside a skipped call).
732 Breakpoint matched;
733 bool hit = false;
734 {
735 std::lock_guard<std::mutex> lock(mu_);
736 if (bpsEnabled_.load()) {
737 for (const auto& bp : bps_) {
738 if (!bp.enabled || bp.line != loc.line ||
739 !sourcesMatch(loc.source, bp.source))
740 continue;
741 matched = bp;
742 hit = true;
743 break;
744 }
745 }
746 }
747 if (hit) {
748 if (!conditionHolds(matched)) return false;
749 stepSkipLoc_ = {};
750 pauseLoc_ = loc;
751 reason_.store(PauseReason::Breakpoint);
752 mode_.store(RunMode::Paused);
753 RenderVision::instance().notifyPending("breakpoint", loc.source, loc.line);
754 return true;
755 }
756
757 if (stepping) {
758 const int depth = scriptStackDepth();
759 bool stop = false;
760 if (m == RunMode::StepInto) {
761 stop = true;
762 } else if (m == RunMode::StepOver) {
763 // Same frame or outer: stop. Deeper (inside a call): keep going.
764 stop = depth <= stepStartDepth_;
765 } else { // StepOut
766 stop = depth < stepStartDepth_;
767 }
768 if (stop) {
769 stepSkipLoc_ = {};
770 pauseLoc_ = loc;
771 reason_.store(PauseReason::Step);
772 mode_.store(RunMode::Paused);
773 return true;
774 }
775 return false;
776 }
777 return false;
778}
779
780void Debugger::waitWhilePaused(const std::function<void()>& pump) {
781 // Re-entrancy guard: evaluating an expression (eve_eval / watches /
782 // breakpoint conditions) while already paused runs bytecode on this VM,
783 // whose line hook reports "paused" and would otherwise block again on the
784 // same thread — deadlocking the pause pump. The nested caller only wants
785 // the eval to finish, so let it run instead of blocking a second time.
786 static thread_local int pausePumpDepth = 0;
787 if (pausePumpDepth > 0) return;
788 ++pausePumpDepth;
789 struct Guard {
790 ~Guard() { --pausePumpDepth; }
791 } guard;
792 try {
793 while (mode_.load() == RunMode::Paused) {
794 if (pump) pump();
795 else if (pump_) pump_();
796 std::this_thread::sleep_for(std::chrono::milliseconds(5));
797 if (!vm_) break;
798 }
799 } catch (...) {
800 // Keep the pump re-entrancy state consistent; the caller's hook is
801 // responsible for surfacing the failure.
802 }
803}
804
805int Debugger::setBreakpoint(std::string source, int line, bool enabled,
806 std::string condition) {
807 source = normalizeSource(std::move(source));
808 if (source.empty() || line <= 0) return 0;
809 std::lock_guard<std::mutex> lock(mu_);
810 for (auto& bp : bps_) {
811 if (bp.line == line && normalizeSource(bp.source) == source) {
812 bp.enabled = enabled;
813 bp.condition = std::move(condition);
814 return bp.id;
815 }
816 }
817 Breakpoint bp;
818 bp.source = std::move(source);
819 bp.line = line;
820 bp.enabled = enabled;
821 bp.condition = std::move(condition);
822 bp.id = nextBpId_++;
823 bps_.push_back(bp);
824 return bp.id;
825}
826
827bool Debugger::clearBreakpoint(std::string source, int line) {
828 std::lock_guard<std::mutex> lock(mu_);
829 const auto before = bps_.size();
830 bps_.erase(std::remove_if(bps_.begin(), bps_.end(),
831 [&](const Breakpoint& bp) {
832 return bp.line == line && sourcesMatch(source, bp.source);
833 }),
834 bps_.end());
835 return bps_.size() != before;
836}
837
838void Debugger::clearBreakpoints(const std::string& source) {
839 std::lock_guard<std::mutex> lock(mu_);
840 if (source.empty()) {
841 bps_.clear();
842 return;
843 }
844 bps_.erase(std::remove_if(bps_.begin(), bps_.end(),
845 [&](const Breakpoint& bp) { return sourcesMatch(source, bp.source); }),
846 bps_.end());
847}
848
850 std::lock_guard<std::mutex> lock(mu_);
851 for (auto& bp : bps_) {
852 if (bp.id != id) continue;
853 bp.enabled = enabled;
854 return true;
855 }
856 return false;
857}
858
859std::vector<Breakpoint> Debugger::breakpoints() const {
860 std::lock_guard<std::mutex> lock(mu_);
861 return bps_;
862}
863
864bool Debugger::hasBreakpoint(const std::string& source, int line) const {
865 std::lock_guard<std::mutex> lock(mu_);
866 return matchBreakpointAny(source, line);
867}
868
869void Debugger::addWatch(std::string expression) {
870 if (expression.empty()) return;
871 std::lock_guard<std::mutex> lock(mu_);
872 for (const auto& e : watchExprs_) {
873 if (e == expression) return;
874 }
875 watchExprs_.push_back(std::move(expression));
876}
877
878bool Debugger::removeWatch(const std::string& expression) {
879 std::lock_guard<std::mutex> lock(mu_);
880 const auto before = watchExprs_.size();
881 watchExprs_.erase(std::remove(watchExprs_.begin(), watchExprs_.end(), expression),
882 watchExprs_.end());
883 return watchExprs_.size() != before;
884}
885
887 std::lock_guard<std::mutex> lock(mu_);
888 watchExprs_.clear();
889 watchCache_.clear();
890}
891
892std::vector<WatchEntry> Debugger::watches() const {
893 std::lock_guard<std::mutex> lock(mu_);
894 return watchCache_;
895}
896
898 std::vector<std::string> exprs;
899 {
900 std::lock_guard<std::mutex> lock(mu_);
901 exprs = watchExprs_;
902 }
903 std::vector<WatchEntry> cache;
904 cache.reserve(exprs.size());
905 for (const auto& e : exprs) {
907 w.expression = e;
908 auto info = evaluate(e);
909 w.ok = !info.type.empty() && info.type != "error";
910 w.value = info.value;
911 cache.push_back(std::move(w));
912 }
913 std::lock_guard<std::mutex> lock(mu_);
914 watchCache_ = std::move(cache);
915}
916
917VariableInfo Debugger::readLocal(HSQUIRRELVM vm, unsigned level, const std::string& name) const {
919 info.name = name;
920 if (!vm) {
921 info.type = "error";
922 info.value = "no vm";
923 return info;
924 }
925 for (SQUnsignedInteger n = 0;; ++n) {
926 const SQInteger top = sq_gettop(vm);
927 const SQChar* lname = sq_getlocal(vm, level, n);
928 if (!lname) {
929 sq_settop(vm, top);
930 break;
931 }
932 if (name == lname) {
933 info.value = describeSqValue(vm, -1);
934 info.type = typeName(vm, -1);
935 sq_settop(vm, top);
936 return info;
937 }
938 sq_settop(vm, top);
939 }
940 info.type = "error";
941 info.value = "not found";
942 return info;
943}
944
945VariableInfo Debugger::readRoot(HSQUIRRELVM vm, const std::string& name) const {
946 VariableInfo info;
947 info.name = name;
948 if (!vm) {
949 info.type = "error";
950 info.value = "no vm";
951 return info;
952 }
953 const SQInteger top = sq_gettop(vm);
954 sq_pushroottable(vm);
955 sq_pushstring(vm, name.c_str(), -1);
956 if (SQ_SUCCEEDED(sq_get(vm, -2))) {
957 info.value = describeSqValue(vm, -1);
958 info.type = typeName(vm, -1);
959 } else {
960 info.type = "error";
961 info.value = "not found";
962 }
963 sq_settop(vm, top);
964 return info;
965}
966
967VariableInfo Debugger::evaluateLegacy(const std::string& expression) const {
968 HSQUIRRELVM vm = vm_;
969 if (!vm || expression.empty()) {
970 VariableInfo info;
971 info.name = expression;
972 info.type = "error";
973 info.value = "unavailable";
974 return info;
975 }
976 // Prefer local, then roottable slot. Full expression eval is intentionally
977 // limited (safe for watches of variable names / dotted root paths).
978 auto local = readLocal(vm, 0, expression);
979 if (local.type != "error") return local;
980
981 // Support a.b root path (tables only).
982 if (expression.find('.') != std::string::npos) {
983 VariableInfo info;
984 info.name = expression;
985 const SQInteger top = sq_gettop(vm);
986 sq_pushroottable(vm);
987 bool ok = true;
988 size_t start = 0;
989 while (start < expression.size()) {
990 size_t dot = expression.find('.', start);
991 if (dot == std::string::npos) dot = expression.size();
992 const std::string part = expression.substr(start, dot - start);
993 sq_pushstring(vm, part.c_str(), -1);
994 if (SQ_FAILED(sq_get(vm, -2))) {
995 ok = false;
996 break;
997 }
998 // Replace parent with child: [root, parent, child] → keep child under root.
999 sq_remove(vm, -2);
1000 start = dot + 1;
1001 }
1002 if (ok) {
1003 info.value = describeSqValue(vm, -1);
1004 info.type = typeName(vm, -1);
1005 } else {
1006 info.type = "error";
1007 info.value = "not found";
1008 }
1009 sq_settop(vm, top);
1010 return info;
1011 }
1012 return readRoot(vm, expression);
1013}
1014
1015std::vector<VariableInfo> Debugger::locals(int stackLevel) const {
1016 std::vector<VariableInfo> out;
1017 HSQUIRRELVM vm = vm_;
1018 if (!vm) return out;
1019 const SQUnsignedInteger level = static_cast<SQUnsignedInteger>(stackLevel < 0 ? 0 : stackLevel);
1020 for (SQUnsignedInteger n = 0;; ++n) {
1021 const SQInteger top = sq_gettop(vm);
1022 const SQChar* name = sq_getlocal(vm, level, n);
1023 if (!name) {
1024 sq_settop(vm, top);
1025 break;
1026 }
1027 VariableInfo info = describeAt(vm, -1);
1028 info.name = name;
1029 sq_settop(vm, top);
1030 out.push_back(std::move(info));
1031 }
1032 return out;
1033}
1034
1035std::vector<VariableInfo> Debugger::globals() const {
1036 std::vector<VariableInfo> out;
1037 HSQUIRRELVM vm = vm_;
1038 if (!vm) return out;
1039 const SQInteger top = sq_gettop(vm);
1040 sq_pushroottable(vm);
1041 out = enumerateAt(vm, -1);
1042 sq_settop(vm, top);
1043 return out;
1044}
1045
1046bool Debugger::pushLocalValue(HSQUIRRELVM vm, unsigned level, const std::string& name) const {
1047 for (SQUnsignedInteger n = 0;; ++n) {
1048 const SQInteger top = sq_gettop(vm);
1049 const SQChar* lname = sq_getlocal(vm, level, n);
1050 if (!lname) {
1051 sq_settop(vm, top);
1052 return false;
1053 }
1054 if (name == lname) return true; // local value is on the stack
1055 sq_settop(vm, top);
1056 }
1057}
1058
1059bool Debugger::pushPathValue(HSQUIRRELVM vm, VarKind kind, int frame,
1060 const std::vector<std::string>& path) const {
1061 if (!vm) return false;
1062 const SQInteger top = sq_gettop(vm);
1063 if (kind == VarKind::Globals) {
1064 sq_pushroottable(vm);
1065 } else if (kind == VarKind::Locals) {
1066 if (path.empty()) return false;
1067 if (!pushLocalValue(vm, static_cast<unsigned>(frame < 0 ? 0 : frame), path[0]))
1068 return false;
1069 } else {
1070 return false;
1071 }
1072
1073 const size_t start = (kind == VarKind::Globals) ? 0 : 1;
1074 for (size_t i = start; i < path.size(); ++i) {
1075 const SQInteger before = sq_gettop(vm);
1076 const int curAbs = static_cast<int>(before);
1077 sq_pushstring(vm, path[i].c_str(), -1);
1078 if (SQ_FAILED(sq_get(vm, curAbs))) {
1079 sq_settop(vm, top);
1080 return false;
1081 }
1082 sq_remove(vm, -2); // drop parent, keep child
1083 }
1084 return true;
1085}
1086
1087std::vector<VariableInfo> Debugger::containerChildren(VarKind kind, int frame,
1088 const std::vector<std::string>& path) const {
1089 HSQUIRRELVM vm = vm_;
1090 if (!vm) return {};
1091 if (path.empty()) {
1092 if (kind == VarKind::Locals) return locals(frame);
1093 if (kind == VarKind::Globals) return globals();
1094 return {};
1095 }
1096 const SQInteger top = sq_gettop(vm);
1097 if (!pushPathValue(vm, kind, frame, path)) {
1098 sq_settop(vm, top);
1099 return {};
1100 }
1101 std::vector<VariableInfo> out = enumerateAt(vm, -1);
1102 sq_settop(vm, top);
1103 return out;
1104}
1105
1106VariableInfo Debugger::evaluate(const std::string& expression, int frameLevel) const {
1107 HSQUIRRELVM vm = vm_;
1108 if (!vm || expression.empty()) {
1110 info.name = expression;
1111 info.type = "error";
1112 info.value = "unavailable";
1113 return info;
1114 }
1115 const SQInteger top = sq_gettop(vm);
1116 const int level = frameLevel < 0 ? 0 : frameLevel;
1117
1118 // Locals env: fresh table seeded with the frame's locals; delegate = roottable
1119 // so global names also resolve (locals shadow globals, like in the frame).
1120 sq_newtable(vm);
1121 for (SQUnsignedInteger n = 0;; ++n) {
1122 const SQInteger before = sq_gettop(vm);
1123 const SQChar* lname = sq_getlocal(vm, static_cast<SQUnsignedInteger>(level), n);
1124 if (!lname) {
1125 sq_settop(vm, before);
1126 break;
1127 }
1128 // Stack after sq_getlocal: [env, value]; build [env, key, value].
1129 sq_pushstring(vm, lname, -1); // key
1130 sq_push(vm, -2); // duplicate value
1131 sq_remove(vm, -3); // drop original value
1132 sq_newslot(vm, -3, SQFalse); // env[key] = value (pops key+value)
1133 }
1134 sq_pushroottable(vm);
1135 if (SQ_FAILED(sq_setdelegate(vm, -2))) {
1136 sq_settop(vm, top);
1137 return evaluateLegacy(expression);
1138 }
1139
1140 const std::string src = "return (" + expression + ");";
1141 // Debugger/MCP evaluation is an in-memory operation. Do not register the
1142 // expression as a project script or give it a filesystem-shaped identity.
1143 if (SQ_FAILED(sq_compilebuffer(vm, src.c_str(), static_cast<SQInteger>(src.size()),
1144 _SC("memory://devtools/evaluate"), SQTrue))) {
1145 sq_settop(vm, top);
1148 info.name = expression;
1149 info.type = "error";
1150 info.value = ctx.empty() ? "compile error" : eve::script::formatScriptError(ctx);
1151 return info;
1152 }
1153 // stack: [env, closure]
1154 sq_push(vm, -2); // [env, closure, env]
1155 sq_setclosureroot(vm, -2); // closure env = locals table (pops copy)
1156 sq_pushroottable(vm); // this/arg for the call
1158 info.name = expression;
1159 if (SQ_SUCCEEDED(sq_call(vm, 1, SQTrue, SQFalse))) {
1160 info = describeAt(vm, -1);
1161 } else {
1163 info.type = "error";
1164 info.value = ctx.empty() ? "eval error" : eve::script::formatScriptError(ctx);
1165 }
1166 sq_settop(vm, top);
1167 return info;
1168}
1169
1170std::vector<StackFrameInfo> Debugger::stackTrace(int maxFrames) const {
1171 std::vector<StackFrameInfo> out;
1172 HSQUIRRELVM vm = vm_;
1173 if (!vm) return out;
1174 if (maxFrames <= 0) maxFrames = 32;
1175 for (int level = 0; level <= maxFrames; ++level) {
1176 SQStackInfos si;
1177 if (SQ_FAILED(sq_stackinfos(vm, level, &si))) break;
1179 f.id = level;
1180 if (si.source) f.loc.source = si.source;
1181 f.loc.line = static_cast<int>(si.line);
1182 // Native-only frames (Squirrel reports source "NATIVE" / line -1, e.g.
1183 // the uncaught-error hook parked above the throwing script frame) carry
1184 // no script location. Skip them so frame 0 in the IDE is the throw
1185 // site; ids stay at stack levels so scopes(frameId) still resolves to
1186 // the right frame's locals.
1187 if (f.loc.source == "NATIVE" || (f.loc.source.empty() && f.loc.line <= 0)) continue;
1188 f.name = nameFromPrototype(level, si.funcname, f.loc);
1189 f.loc.function = f.name;
1190 out.push_back(std::move(f));
1191 }
1192 applyCallGraphNames(out);
1193 return out;
1194}
1195
1196} // namespace eve::dev
Duration start
SQInteger top
float w
Definition AnimClip.cpp:738
const std::string & s
struct SQVM * HSQUIRRELVM
eve::Value condition
HSQUIRRELVM vm
Definition ECS.cpp:20
glm::vec3 n
Definition Grass.cpp:63
double r
float v
std::int32_t c
std::string text
TokenKind kind
std::string local
std::string name
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
int level
std::map< std::string, std::vector< std::string > > graph
Definition Package.cpp:59
int idx
float f
std::string path
Definition PlayHost.cpp:110
bool hit
float t
std::uint32_t count
float size
Definition TreeMesh.cpp:156
const UnitySourceAsset & source
std::uint32_t depth
float m[16]
Runtime call graph + dynamic data-flow tracer with backward slicing.
Definition CallGraph.hpp:81
std::vector< CallFrame > currentStack() const
uint32_t onCall(const SourceLoc &loc, const std::string &funcName={})
uint32_t onReturn(const SourceLoc &loc, const std::string &funcName={})
Script + frame debugger: pause/step, breakpoints, watches.
Definition Debugger.hpp:86
bool hasBreakpoint(const std::string &source, int line) const
True if a breakpoint is registered at source+line. Ignores the skip-all master switch and the per-bre...
Definition Debugger.cpp:864
void setVirtualSource(std::string source, std::string text)
In-memory script text used to name anonymous name = function() bindings when the hook source is a com...
Definition Debugger.cpp:467
void waitWhilePaused(const std::function< void()> &pump={})
Block until resume/step/detach (processes external poll callbacks).
Definition Debugger.cpp:780
static std::string sourceBasename(const std::string &source)
Basename of a normalized path (empty-safe).
Definition Debugger.cpp:409
std::vector< VariableInfo > containerChildren(VarKind kind, int frame, const std::vector< std::string > &path) const
Children of a container variable. path is the key chain that locates the container from its root (loc...
void stepInto()
Enter calls: stop on the next script line at any depth.
Definition Debugger.cpp:620
void refreshWatches()
Re-evaluate all watches against current VM (paused preferred).
Definition Debugger.cpp:897
bool setBreakpointEnabled(int id, bool enabled)
Enable or disable by id.
Definition Debugger.cpp:849
void notifyFrameDone()
After a frame when StepFrame was active → return to Paused.
Definition Debugger.cpp:655
void stepOut()
Finish current function: stop when stack depth drops.
Definition Debugger.cpp:624
HSQUIRRELVM vm() const
Definition Debugger.hpp:96
bool onScriptLine(const SourceLoc &loc)
Called from Squirrel line debug hook. Returns true if execution should block (breakpoint / step).
Definition Debugger.cpp:696
std::vector< WatchEntry > watches() const
Definition Debugger.cpp:892
void stepFrames(int count)
Run count game frames then pause. Each frame is consumed by shouldRunUpdate / notifyFrameDone on the ...
Definition Debugger.cpp:587
static std::string normalizeSource(std::string source)
Normalize source paths for breakpoint matching (basename fallback).
Definition Debugger.cpp:392
std::string onCall(SourceLoc loc)
Squirrel call debug hook ('c'). Records a frame on the active CallGraph.
Definition Debugger.cpp:500
std::vector< VariableInfo > globals() const
Root-table slots (globals).
void pauseAt(PauseReason reason, SourceLoc loc)
Pause and record the script site (copied; Observed until the next pause/resume). Thread: VM / game lo...
Definition Debugger.cpp:564
bool removeWatch(const std::string &expression)
Definition Debugger.cpp:878
void setCallGraph(CallGraph *graph)
Use DevTool's CallGraph for stack names (same object the slicer sees). Pass nullptr to record calls o...
Definition Debugger.cpp:462
std::vector< VariableInfo > locals(int stackLevel=0) const
Locals of the given call-stack level (0 = current script frame).
void stepOver()
Skip calls: stop on the next line at ≤ current stack depth.
Definition Debugger.cpp:622
void addWatch(std::string expression)
Definition Debugger.cpp:869
int scriptStackDepth() const
Current Squirrel call depth (1 = topmost script frame). 0 if none.
Definition Debugger.cpp:592
bool shouldRunUpdate()
Frame loop: true ⇒ call eve_update this frame. Consumes StepFrame.
Definition Debugger.cpp:645
void pause(PauseReason reason=PauseReason::PauseKey)
Frame-level pause without a script site (PauseKey clears pauseLocation). For Exception / Snapshot,...
Definition Debugger.cpp:553
bool clearBreakpoint(std::string source, int line)
Definition Debugger.cpp:827
int setBreakpoint(std::string source, int line, bool enabled=true, std::string condition={})
Add or update a line breakpoint. A later call with the same source+line replaces enabled and conditio...
Definition Debugger.cpp:805
void onReturn(const SourceLoc &loc)
Squirrel return debug hook ('r'). Pops the active CallGraph frame.
Definition Debugger.cpp:507
static Debugger & instance()
Definition Debugger.cpp:387
std::vector< Breakpoint > breakpoints() const
Definition Debugger.cpp:859
void step()
Convenience: script stepOver when mid-hook; otherwise one game frame. Prefer stepInto/stepOver/stepOu...
Definition Debugger.cpp:633
static bool sourcesMatch(const std::string &a, const std::string &b)
True when two source paths refer to the same script file. Matches exact path, or one path as a direct...
Definition Debugger.cpp:415
void attach(HSQUIRRELVM vm)
Definition Debugger.cpp:440
std::vector< StackFrameInfo > stackTrace(int maxFrames=32) const
VariableInfo evaluate(const std::string &expression, int frameLevel=0) const
Evaluate an expression in the given frame's scope. Understands plain names, a.b paths,...
void clearBreakpoints(const std::string &source={})
Definition Debugger.cpp:838
void notifyPending(const std::string &reason, const std::string &source, int line)
Record that a breakpoint / critical site wants a vision dump.
static RenderVision & instance()
VarKind
Where a variable tree node is rooted (used by containerChildren).
Definition Debugger.hpp:69
@ Snapshot
Script state was restored while the debugger was already paused.
double dot(const Vec2 &a, const Vec2 &b)
Dot.
Definition UrbanTypes.h:38
std::string formatScriptError(const ScriptErrorContext &ctx)
Formats a context into a human-readable multi-line report.
ScriptErrorContext takeLastScriptError(HSQUIRRELVM vm)
Consumes and clears the last recorded error for a VM.
ScriptErrorContext captureCompileError(HSQUIRRELVM vm)
Captures the last compilation error recorded by the VM.
bool enabled
std::string condition
Definition Debugger.hpp:43
std::string source
Definition Debugger.hpp:39
Source location in a Squirrel (or synthetic) script.
Definition CallGraph.hpp:16
std::string function
Definition CallGraph.hpp:19
bool empty() const
Definition CallGraph.hpp:21
std::string source
Definition CallGraph.hpp:17
std::string expression
Definition Debugger.hpp:48
Structured snapshot of a script error: message, throw site and stack.
Definition ScriptError.h:25
bool empty() const noexcept
True when no error payload was captured.
Definition ScriptError.h:36
glm::uvec4 info