载入中...
搜索中...
未找到
Debugger.cpp
浏览该文件的文档.
2
4
6
7#include <simplesquirrel/simplesquirrel.hpp>
8#include <squirrel.h>
9
10#include <algorithm>
11#include <chrono>
12#include <thread>
13#include <utility>
14
15namespace eve::dev {
16namespace {
17
18std::string truthyString(bool v) { return v ? "true" : "false"; }
19
20std::string describeSqValue(HSQUIRRELVM vm, SQInteger idx) {
21 const SQObjectType t = sq_gettype(vm, idx);
22 switch (t) {
23 case OT_NULL:
24 return "null";
25 case OT_INTEGER: {
26 SQInteger v = 0;
27 sq_getinteger(vm, idx, &v);
28 return std::to_string(static_cast<long long>(v));
29 }
30 case OT_FLOAT: {
31 SQFloat v = 0;
32 sq_getfloat(vm, idx, &v);
33 return std::to_string(static_cast<double>(v));
34 }
35 case OT_BOOL: {
36 SQBool v = SQFalse;
37 sq_getbool(vm, idx, &v);
38 return v ? "true" : "false";
39 }
40 case OT_STRING: {
41 const SQChar* s = nullptr;
42 sq_getstring(vm, idx, &s);
43 return s ? std::string("\"") + s + "\"" : "\"\"";
44 }
45 case OT_TABLE:
46 return "<table>";
47 case OT_ARRAY:
48 return "<array>";
49 case OT_USERDATA:
50 return "<userdata>";
51 case OT_CLOSURE:
52 return "<closure>";
53 case OT_NATIVECLOSURE:
54 return "<native>";
55 case OT_INSTANCE:
56 return "<instance>";
57 case OT_CLASS:
58 return "<class>";
59 case OT_THREAD:
60 return "<thread>";
61 default:
62 return "<other>";
63 }
64}
65
66std::string typeName(HSQUIRRELVM vm, SQInteger idx) {
67 switch (sq_gettype(vm, idx)) {
68 case OT_NULL:
69 return "null";
70 case OT_INTEGER:
71 return "integer";
72 case OT_FLOAT:
73 return "float";
74 case OT_BOOL:
75 return "bool";
76 case OT_STRING:
77 return "string";
78 case OT_TABLE:
79 return "table";
80 case OT_ARRAY:
81 return "array";
82 case OT_USERDATA:
83 return "userdata";
84 case OT_CLOSURE:
85 return "closure";
86 case OT_NATIVECLOSURE:
87 return "native";
88 case OT_INSTANCE:
89 return "instance";
90 case OT_CLASS:
91 return "class";
92 case OT_THREAD:
93 return "thread";
94 default:
95 return "other";
96 }
97}
98
99std::string typeName(HSQUIRRELVM vm, SQInteger idx);
100
102VariableInfo describeAt(HSQUIRRELVM vm, SQInteger idx) {
103 VariableInfo info;
104 const SQObjectType t = sq_gettype(vm, idx);
105 info.type = typeName(vm, idx);
106 switch (t) {
107 case OT_NULL:
108 info.value = "null";
109 break;
110 case OT_INTEGER: {
111 SQInteger v = 0;
112 sq_getinteger(vm, idx, &v);
113 info.value = std::to_string(static_cast<long long>(v));
114 break;
115 }
116 case OT_FLOAT: {
117 SQFloat v = 0;
118 sq_getfloat(vm, idx, &v);
119 info.value = std::to_string(static_cast<double>(v));
120 break;
121 }
122 case OT_BOOL: {
123 SQBool v = SQFalse;
124 sq_getbool(vm, idx, &v);
125 info.value = v ? "true" : "false";
126 break;
127 }
128 case OT_STRING: {
129 const SQChar* s = nullptr;
130 sq_getstring(vm, idx, &s);
131 info.value = std::string("\"") + (s ? s : "") + "\"";
132 break;
133 }
134 case OT_TABLE:
135 case OT_ARRAY: {
136 const SQInteger n = sq_getsize(vm, idx);
137 info.value = std::string(t == OT_TABLE ? "<table (" : "<array (") +
138 std::to_string(static_cast<long long>(n)) + ")>";
139 info.expandable = true;
140 info.childCount = static_cast<int>(n);
141 break;
142 }
143 case OT_CLASS:
144 info.value = "<class>";
145 info.expandable = true;
146 break;
147 case OT_INSTANCE:
148 info.value = "<instance>";
149 info.expandable = true;
150 break;
151 case OT_USERDATA:
152 info.value = "<userdata>";
153 break;
154 case OT_CLOSURE:
155 info.value = "<closure>";
156 info.expandable = true; // expandable: free variables
157 break;
158 case OT_NATIVECLOSURE:
159 info.value = "<native>";
160 break;
161 case OT_THREAD:
162 info.value = "<thread>";
163 break;
164 case OT_GENERATOR:
165 info.value = "<generator>";
166 break;
167 default:
168 info.value = "<other>";
169 break;
170 }
171 return info;
172}
173
175std::vector<VariableInfo> enumerateAt(HSQUIRRELVM vm, SQInteger idx) {
176 std::vector<VariableInfo> out;
177 const SQObjectType t = sq_gettype(vm, idx);
178 const SQInteger top = sq_gettop(vm);
179 const int absIdx = idx < 0 ? top + static_cast<int>(idx) + 1 : static_cast<int>(idx);
180
181 if (t == OT_ARRAY) {
182 const SQInteger size = sq_getsize(vm, idx);
183 for (SQInteger i = 0; i < size; ++i) {
184 const SQInteger before = sq_gettop(vm);
185 sq_pushinteger(vm, i);
186 if (SQ_FAILED(sq_get(vm, absIdx))) {
187 sq_settop(vm, before);
188 continue;
189 }
190 VariableInfo info = describeAt(vm, -1);
191 info.name = std::to_string(static_cast<long long>(i));
192 sq_settop(vm, before);
193 out.push_back(std::move(info));
194 }
195 return out;
196 }
197
198 if (t == OT_TABLE || t == OT_CLASS) {
199 sq_pushinteger(vm, 0); // iterator position
200 while (SQ_SUCCEEDED(sq_next(vm, absIdx))) {
201 VariableInfo info = describeAt(vm, -1);
202 switch (sq_gettype(vm, -2)) {
203 case OT_STRING: {
204 const SQChar* s = nullptr;
205 sq_getstring(vm, -2, &s);
206 info.name = s ? s : "";
207 break;
208 }
209 case OT_INTEGER: {
210 SQInteger k = 0;
211 sq_getinteger(vm, -2, &k);
212 info.name = std::to_string(static_cast<long long>(k));
213 break;
214 }
215 case OT_FLOAT: {
216 SQFloat k = 0;
217 sq_getfloat(vm, -2, &k);
218 info.name = std::to_string(static_cast<double>(k));
219 break;
220 }
221 case OT_BOOL: {
222 SQBool k = SQFalse;
223 sq_getbool(vm, -2, &k);
224 info.name = truthyString(k != SQFalse);
225 break;
226 }
227 default:
228 info.name = "<key>";
229 break;
230 }
231 sq_pop(vm, 2);
232 out.push_back(std::move(info));
233 }
234 sq_settop(vm, top);
235 return out;
236 }
237
238 if (t == OT_INSTANCE) {
239 sq_getclass(vm, absIdx); // class at top
240 const int clsAbs = sq_gettop(vm);
241 sq_pushinteger(vm, 0);
242 while (SQ_SUCCEEDED(sq_next(vm, clsAbs))) {
243 const SQChar* s = nullptr;
244 if (sq_gettype(vm, -2) == OT_STRING) sq_getstring(vm, -2, &s);
245 const std::string name = s ? s : "";
246 const SQInteger before = sq_gettop(vm);
247 sq_push(vm, absIdx); // instance
248 sq_pushstring(vm, name.c_str(), -1);
249 VariableInfo info;
250 if (SQ_SUCCEEDED(sq_get(vm, -2))) {
251 info = describeAt(vm, -1);
252 } else {
253 info.type = "error";
254 info.value = "not found";
255 }
256 info.name = name;
257 sq_settop(vm, before);
258 sq_pop(vm, 2); // drop key + value; keep iterator position
259 out.push_back(std::move(info));
260 }
261 sq_settop(vm, top);
262 return out;
263 }
264
265 if (t == OT_CLOSURE) {
266 SQInteger nparams = 0, nfree = 0;
267 if (SQ_SUCCEEDED(sq_getclosureinfo(vm, idx, &nparams, &nfree))) {
268 for (SQUnsignedInteger i = 0; i < static_cast<SQUnsignedInteger>(nfree); ++i) {
269 const SQChar* n = sq_getfreevariable(vm, absIdx, i);
270 VariableInfo info = describeAt(vm, -1);
271 info.name = n ? n : ("upvalue_" + std::to_string(static_cast<long long>(i)));
272 sq_poptop(vm);
273 out.push_back(std::move(info));
274 }
275 }
276 return out;
277 }
278 return out;
279}
280
281bool valueTruthy(const VariableInfo& info) {
282 if (info.type == "error") return false;
283 if (info.type == "null") return false;
284 if (info.type == "bool") return info.value == "true";
285 if (info.type == "integer") {
286 try {
287 return std::stoll(info.value) != 0;
288 } catch (...) {
289 return true;
290 }
291 }
292 if (info.type == "float") {
293 try {
294 return std::stod(info.value) != 0.0;
295 } catch (...) {
296 return true;
297 }
298 }
299 return true;
300}
301
302} // namespace
303
305 static Debugger inst;
306 return inst;
307}
308
309std::string Debugger::normalizeSource(std::string source) {
310 if (source.empty()) return source;
311 // Strip common URI prefixes VS Code may send.
312 if (source.rfind("file://", 0) == 0) {
313 source = source.substr(7);
314 // file://localhost/Users/... → /Users/...
315 if (source.rfind("localhost/", 0) == 0) source = source.substr(9);
316 }
317 // Unify separators.
318 for (char& c : source) {
319 if (c == '\\') c = '/';
320 }
321 // Drop leading ./
322 while (source.size() >= 2 && source[0] == '.' && source[1] == '/') source = source.substr(2);
323 return source;
324}
325
326std::string Debugger::sourceBasename(const std::string& source) {
327 const std::string norm = normalizeSource(source);
328 const auto slash = norm.find_last_of('/');
329 return (slash == std::string::npos) ? norm : norm.substr(slash + 1);
330}
331
332bool Debugger::sourcesMatch(const std::string& a, const std::string& b) {
333 const std::string na = normalizeSource(a);
334 const std::string nb = normalizeSource(b);
335 if (na.empty() || nb.empty()) return false;
336 if (na == nb) return true;
337 const std::string ba = sourceBasename(na);
338 const std::string bb = sourceBasename(nb);
339 if (!ba.empty() && ba == bb) return true;
340 // Suffix match: ".../scripts/main.nut" vs "scripts/main.nut"
341 const std::string& longer = na.size() >= nb.size() ? na : nb;
342 const std::string& shorter = na.size() >= nb.size() ? nb : na;
343 if (longer.size() > shorter.size() &&
344 longer.compare(longer.size() - shorter.size(), shorter.size(), shorter) == 0) {
345 const auto idx = longer.size() - shorter.size();
346 return idx == 0 || longer[idx - 1] == '/';
347 }
348 return false;
349}
350
352 std::lock_guard<std::mutex> lock(mu_);
353 vm_ = vm;
354 mode_ = RunMode::Running;
355 reason_ = PauseReason::None;
356 pauseLoc_ = {};
357 stepFrameArmed_ = false;
358}
359
361 std::lock_guard<std::mutex> lock(mu_);
362 vm_ = nullptr;
363 mode_ = RunMode::Running;
364 reason_ = PauseReason::None;
365 stepFrameArmed_ = false;
366}
367
369 reason_.store(reason);
370 mode_.store(RunMode::Paused);
371 // Frame-level Pause has no script site; drop a stale hook location so
372 // smart step() does not treat this as mid-script.
373 if (reason == PauseReason::PauseKey) pauseLoc_ = {};
374}
375
377 reason_.store(PauseReason::None);
378 mode_.store(RunMode::Running);
379 stepFrameArmed_ = false;
380 stepStartDepth_ = 0;
381 stepSkipLoc_ = {};
382 pauseLoc_ = {};
383}
384
386 reason_.store(PauseReason::Step);
387 mode_.store(RunMode::StepFrame);
388 stepFrameArmed_ = true;
389}
390
392 HSQUIRRELVM vm = vm_;
393 if (!vm) return 0;
394 int depth = 0;
395 for (int level = 0;; ++level) {
396 SQStackInfos si;
397 if (SQ_FAILED(sq_stackinfos(vm, level, &si))) break;
398 ++depth;
399 }
400 return depth;
401}
402
403void Debugger::beginScriptStep(RunMode stepMode) {
404 reason_.store(PauseReason::Step);
405 stepStartDepth_ = scriptStackDepth();
406 // Squirrel can emit several _OP_LINE for one source line; skip the line we
407 // are currently paused on until the location changes.
408 stepSkipLoc_ = pauseLoc_;
409 // Not currently inside a script frame (frame-level pause): stop on the
410 // first line we see — treat like stepInto with an open depth gate.
411 if (stepStartDepth_ <= 0) {
412 mode_.store(RunMode::StepInto);
413 stepStartDepth_ = 0;
414 return;
415 }
416 mode_.store(stepMode);
417}
418
419void Debugger::stepInto() { beginScriptStep(RunMode::StepInto); }
420
421void Debugger::stepOver() { beginScriptStep(RunMode::StepOver); }
422
424 // No caller to return to → just step over the current line.
425 if (scriptStackDepth() <= 1) {
426 beginScriptStep(RunMode::StepOver);
427 return;
428 }
429 beginScriptStep(RunMode::StepOut);
430}
431
433 // Prefer script step-over when we have a script pause site; else one frame.
434 const PauseReason r = reason_.load();
435 if (!pauseLoc_.empty() &&
437 stepOver();
438 return;
439 }
440 stepFrame();
441}
442
444 const RunMode m = mode_.load();
445 if (m == RunMode::Running) return true;
446 if (m == RunMode::StepFrame) return true;
447 // Allow the game loop to enter eve_update so script steps can begin after
448 // a frame-level pause.
449 if (m == RunMode::StepInto || m == RunMode::StepOver || m == RunMode::StepOut) return true;
450 return false; // Paused
451}
452
454 if (mode_.load() == RunMode::StepFrame || stepFrameArmed_) {
455 stepFrameArmed_ = false;
456 reason_.store(PauseReason::Step);
457 mode_.store(RunMode::Paused);
458 // Frame step finished outside the line hook — next smart step is frame.
459 pauseLoc_ = {};
460 }
461}
462
463bool Debugger::matchBreakpoint(const std::string& source, int line) const {
464 if (!bpsEnabled_.load()) return false;
465 for (const auto& bp : bps_) {
466 if (!bp.enabled || bp.line != line) continue;
467 if (sourcesMatch(source, bp.source)) return true;
468 }
469 return false;
470}
471
472bool Debugger::matchBreakpointAny(const std::string& source, int line) const {
473 for (const auto& bp : bps_) {
474 if (bp.line != line) continue;
475 if (sourcesMatch(source, bp.source)) return true;
476 }
477 return false;
478}
479
480bool Debugger::conditionHolds(const Breakpoint& bp) const {
481 if (bp.condition.empty()) return true;
482 // Failed / unreadable conditions stop (safer than silently never stopping).
483 const VariableInfo r = evaluate(bp.condition, 0);
484 if (r.type == "error") return true;
485 return valueTruthy(r);
486}
487
489 const RunMode m = mode_.load();
490 if (m == RunMode::Paused) {
491 return true;
492 }
493
494 // Verification: any breakpoint whose exact line we just executed is real.
495 // Report it (once) even when stepping filters or conditions suppress the stop.
496 std::vector<int> verifiedNow;
497 {
498 std::lock_guard<std::mutex> lock(mu_);
499 if (!bps_.empty() && matchBreakpointAny(loc.source, loc.line)) {
500 for (auto& bp : bps_) {
501 if (bp.verified || bp.line != loc.line ||
502 !sourcesMatch(loc.source, bp.source))
503 continue;
504 bp.verified = true;
505 verifiedNow.push_back(bp.id);
506 }
507 }
508 }
509 for (const int id : verifiedNow) {
510 if (bpEventFn_) bpEventFn_(id, loc.source, loc.line, true);
511 }
512
513 const bool stepping =
515
516 // While leaving the paused line, ignore further events for that exact
517 // source+line (extra _OP_LINE and re-armed breakpoints on the same site).
518 if (stepping && !stepSkipLoc_.empty() && stepSkipLoc_.line == loc.line &&
519 sourcesMatch(stepSkipLoc_.source, loc.source)) {
520 return false;
521 }
522
523 // Breakpoints win over step filters (hit inside a skipped call).
524 Breakpoint matched;
525 bool hit = false;
526 {
527 std::lock_guard<std::mutex> lock(mu_);
528 if (bpsEnabled_.load()) {
529 for (const auto& bp : bps_) {
530 if (!bp.enabled || bp.line != loc.line ||
531 !sourcesMatch(loc.source, bp.source))
532 continue;
533 matched = bp;
534 hit = true;
535 break;
536 }
537 }
538 }
539 if (hit) {
540 if (!conditionHolds(matched)) return false;
541 stepSkipLoc_ = {};
542 pauseLoc_ = loc;
543 reason_.store(PauseReason::Breakpoint);
544 mode_.store(RunMode::Paused);
545 RenderVision::instance().notifyPending("breakpoint", loc.source, loc.line);
546 return true;
547 }
548
549 if (stepping) {
550 const int depth = scriptStackDepth();
551 bool stop = false;
552 if (m == RunMode::StepInto) {
553 stop = true;
554 } else if (m == RunMode::StepOver) {
555 // Same frame or outer: stop. Deeper (inside a call): keep going.
556 stop = depth <= stepStartDepth_;
557 } else { // StepOut
558 stop = depth < stepStartDepth_;
559 }
560 if (stop) {
561 stepSkipLoc_ = {};
562 pauseLoc_ = loc;
563 reason_.store(PauseReason::Step);
564 mode_.store(RunMode::Paused);
565 return true;
566 }
567 return false;
568 }
569 return false;
570}
571
572void Debugger::waitWhilePaused(const std::function<void()>& pump) {
573 while (mode_.load() == RunMode::Paused) {
574 if (pump) pump();
575 else if (pump_) pump_();
576 std::this_thread::sleep_for(std::chrono::milliseconds(5));
577 if (!vm_) break;
578 }
579}
580
581int Debugger::setBreakpoint(std::string source, int line, bool enabled,
582 std::string condition) {
583 source = normalizeSource(std::move(source));
584 if (source.empty() || line <= 0) return 0;
585 std::lock_guard<std::mutex> lock(mu_);
586 for (auto& bp : bps_) {
587 if (bp.line == line && normalizeSource(bp.source) == source) {
588 bp.enabled = enabled;
589 if (!condition.empty()) bp.condition = std::move(condition);
590 return bp.id;
591 }
592 }
593 Breakpoint bp;
594 bp.source = std::move(source);
595 bp.line = line;
596 bp.enabled = enabled;
597 bp.condition = std::move(condition);
598 bp.id = nextBpId_++;
599 bps_.push_back(bp);
600 return bp.id;
601}
602
603bool Debugger::clearBreakpoint(std::string source, int line) {
604 std::lock_guard<std::mutex> lock(mu_);
605 const auto before = bps_.size();
606 bps_.erase(std::remove_if(bps_.begin(), bps_.end(),
607 [&](const Breakpoint& bp) {
608 return bp.line == line && sourcesMatch(source, bp.source);
609 }),
610 bps_.end());
611 return bps_.size() != before;
612}
613
614void Debugger::clearBreakpoints(const std::string& source) {
615 std::lock_guard<std::mutex> lock(mu_);
616 if (source.empty()) {
617 bps_.clear();
618 return;
619 }
620 bps_.erase(std::remove_if(bps_.begin(), bps_.end(),
621 [&](const Breakpoint& bp) { return sourcesMatch(source, bp.source); }),
622 bps_.end());
623}
624
625std::vector<Breakpoint> Debugger::breakpoints() const {
626 std::lock_guard<std::mutex> lock(mu_);
627 return bps_;
628}
629
630bool Debugger::hasBreakpoint(const std::string& source, int line) const {
631 std::lock_guard<std::mutex> lock(mu_);
632 return matchBreakpoint(source, line);
633}
634
635void Debugger::addWatch(std::string expression) {
636 if (expression.empty()) return;
637 std::lock_guard<std::mutex> lock(mu_);
638 for (const auto& e : watchExprs_) {
639 if (e == expression) return;
640 }
641 watchExprs_.push_back(std::move(expression));
642}
643
644bool Debugger::removeWatch(const std::string& expression) {
645 std::lock_guard<std::mutex> lock(mu_);
646 const auto before = watchExprs_.size();
647 watchExprs_.erase(std::remove(watchExprs_.begin(), watchExprs_.end(), expression),
648 watchExprs_.end());
649 return watchExprs_.size() != before;
650}
651
653 std::lock_guard<std::mutex> lock(mu_);
654 watchExprs_.clear();
655 watchCache_.clear();
656}
657
658std::vector<WatchEntry> Debugger::watches() const {
659 std::lock_guard<std::mutex> lock(mu_);
660 return watchCache_;
661}
662
664 std::vector<std::string> exprs;
665 {
666 std::lock_guard<std::mutex> lock(mu_);
667 exprs = watchExprs_;
668 }
669 std::vector<WatchEntry> cache;
670 cache.reserve(exprs.size());
671 for (const auto& e : exprs) {
673 w.expression = e;
674 auto info = evaluate(e);
675 w.ok = !info.type.empty() && info.type != "error";
676 w.value = info.value;
677 cache.push_back(std::move(w));
678 }
679 std::lock_guard<std::mutex> lock(mu_);
680 watchCache_ = std::move(cache);
681}
682
683VariableInfo Debugger::readLocal(HSQUIRRELVM vm, unsigned level, const std::string& name) const {
684 VariableInfo info;
685 info.name = name;
686 if (!vm) {
687 info.type = "error";
688 info.value = "no vm";
689 return info;
690 }
691 for (SQUnsignedInteger n = 0;; ++n) {
692 const SQInteger top = sq_gettop(vm);
693 const SQChar* lname = sq_getlocal(vm, level, n);
694 if (!lname) {
695 sq_settop(vm, top);
696 break;
697 }
698 if (name == lname) {
699 info.value = describeSqValue(vm, -1);
700 info.type = typeName(vm, -1);
701 sq_settop(vm, top);
702 return info;
703 }
704 sq_settop(vm, top);
705 }
706 info.type = "error";
707 info.value = "not found";
708 return info;
709}
710
711VariableInfo Debugger::readRoot(HSQUIRRELVM vm, const std::string& name) const {
712 VariableInfo info;
713 info.name = name;
714 if (!vm) {
715 info.type = "error";
716 info.value = "no vm";
717 return info;
718 }
719 const SQInteger top = sq_gettop(vm);
720 sq_pushroottable(vm);
721 sq_pushstring(vm, name.c_str(), -1);
722 if (SQ_SUCCEEDED(sq_get(vm, -2))) {
723 info.value = describeSqValue(vm, -1);
724 info.type = typeName(vm, -1);
725 } else {
726 info.type = "error";
727 info.value = "not found";
728 }
729 sq_settop(vm, top);
730 return info;
731}
732
733VariableInfo Debugger::evaluateLegacy(const std::string& expression) const {
734 HSQUIRRELVM vm = vm_;
735 if (!vm || expression.empty()) {
736 VariableInfo info;
737 info.name = expression;
738 info.type = "error";
739 info.value = "unavailable";
740 return info;
741 }
742 // Prefer local, then roottable slot. Full expression eval is intentionally
743 // limited (safe for watches of variable names / dotted root paths).
744 auto local = readLocal(vm, 0, expression);
745 if (local.type != "error") return local;
746
747 // Support a.b root path (tables only).
748 if (expression.find('.') != std::string::npos) {
749 VariableInfo info;
750 info.name = expression;
751 const SQInteger top = sq_gettop(vm);
752 sq_pushroottable(vm);
753 bool ok = true;
754 size_t start = 0;
755 while (start < expression.size()) {
756 size_t dot = expression.find('.', start);
757 if (dot == std::string::npos) dot = expression.size();
758 const std::string part = expression.substr(start, dot - start);
759 sq_pushstring(vm, part.c_str(), -1);
760 if (SQ_FAILED(sq_get(vm, -2))) {
761 ok = false;
762 break;
763 }
764 // Replace parent with child: [root, parent, child] → keep child under root.
765 sq_remove(vm, -2);
766 start = dot + 1;
767 }
768 if (ok) {
769 info.value = describeSqValue(vm, -1);
770 info.type = typeName(vm, -1);
771 } else {
772 info.type = "error";
773 info.value = "not found";
774 }
775 sq_settop(vm, top);
776 return info;
777 }
778 return readRoot(vm, expression);
779}
780
781std::vector<VariableInfo> Debugger::locals(int stackLevel) const {
782 std::vector<VariableInfo> out;
783 HSQUIRRELVM vm = vm_;
784 if (!vm) return out;
785 const SQUnsignedInteger level = static_cast<SQUnsignedInteger>(stackLevel < 0 ? 0 : stackLevel);
786 for (SQUnsignedInteger n = 0;; ++n) {
787 const SQInteger top = sq_gettop(vm);
788 const SQChar* name = sq_getlocal(vm, level, n);
789 if (!name) {
790 sq_settop(vm, top);
791 break;
792 }
793 VariableInfo info = describeAt(vm, -1);
794 info.name = name;
795 sq_settop(vm, top);
796 out.push_back(std::move(info));
797 }
798 return out;
799}
800
801std::vector<VariableInfo> Debugger::globals() const {
802 std::vector<VariableInfo> out;
803 HSQUIRRELVM vm = vm_;
804 if (!vm) return out;
805 const SQInteger top = sq_gettop(vm);
806 sq_pushroottable(vm);
807 out = enumerateAt(vm, -1);
808 sq_settop(vm, top);
809 return out;
810}
811
812bool Debugger::pushLocalValue(HSQUIRRELVM vm, unsigned level, const std::string& name) const {
813 for (SQUnsignedInteger n = 0;; ++n) {
814 const SQInteger top = sq_gettop(vm);
815 const SQChar* lname = sq_getlocal(vm, level, n);
816 if (!lname) {
817 sq_settop(vm, top);
818 return false;
819 }
820 if (name == lname) return true; // local value is on the stack
821 sq_settop(vm, top);
822 }
823}
824
825bool Debugger::pushPathValue(HSQUIRRELVM vm, VarKind kind, int frame,
826 const std::vector<std::string>& path) const {
827 if (!vm) return false;
828 const SQInteger top = sq_gettop(vm);
829 if (kind == VarKind::Globals) {
830 sq_pushroottable(vm);
831 } else if (kind == VarKind::Locals) {
832 if (path.empty()) return false;
833 if (!pushLocalValue(vm, static_cast<unsigned>(frame < 0 ? 0 : frame), path[0]))
834 return false;
835 } else {
836 return false;
837 }
838
839 const size_t start = (kind == VarKind::Globals) ? 0 : 1;
840 for (size_t i = start; i < path.size(); ++i) {
841 const SQInteger before = sq_gettop(vm);
842 const int curAbs = before;
843 sq_pushstring(vm, path[i].c_str(), -1);
844 if (SQ_FAILED(sq_get(vm, curAbs))) {
845 sq_settop(vm, top);
846 return false;
847 }
848 sq_remove(vm, -2); // drop parent, keep child
849 }
850 return true;
851}
852
853std::vector<VariableInfo> Debugger::containerChildren(VarKind kind, int frame,
854 const std::vector<std::string>& path) const {
855 HSQUIRRELVM vm = vm_;
856 if (!vm) return {};
857 if (path.empty()) {
858 if (kind == VarKind::Locals) return locals(frame);
859 if (kind == VarKind::Globals) return globals();
860 return {};
861 }
862 const SQInteger top = sq_gettop(vm);
863 if (!pushPathValue(vm, kind, frame, path)) {
864 sq_settop(vm, top);
865 return {};
866 }
867 std::vector<VariableInfo> out = enumerateAt(vm, -1);
868 sq_settop(vm, top);
869 return out;
870}
871
872VariableInfo Debugger::evaluate(const std::string& expression, int frameLevel) const {
873 HSQUIRRELVM vm = vm_;
874 if (!vm || expression.empty()) {
875 VariableInfo info;
876 info.name = expression;
877 info.type = "error";
878 info.value = "unavailable";
879 return info;
880 }
881 const SQInteger top = sq_gettop(vm);
882 const int level = frameLevel < 0 ? 0 : frameLevel;
883
884 // Locals env: fresh table seeded with the frame's locals; delegate = roottable
885 // so global names also resolve (locals shadow globals, like in the frame).
886 sq_newtable(vm);
887 for (SQUnsignedInteger n = 0;; ++n) {
888 const SQInteger before = sq_gettop(vm);
889 const SQChar* lname = sq_getlocal(vm, static_cast<SQUnsignedInteger>(level), n);
890 if (!lname) {
891 sq_settop(vm, before);
892 break;
893 }
894 // Stack after sq_getlocal: [env, value]; build [env, key, value].
895 sq_pushstring(vm, lname, -1); // key
896 sq_push(vm, -2); // duplicate value
897 sq_remove(vm, -3); // drop original value
898 sq_newslot(vm, -3, SQFalse); // env[key] = value (pops key+value)
899 }
900 sq_pushroottable(vm);
901 if (SQ_FAILED(sq_setdelegate(vm, -2))) {
902 sq_settop(vm, top);
903 return evaluateLegacy(expression);
904 }
905
906 const std::string src = "return (" + expression + ");";
907 if (SQ_FAILED(sq_compilebuffer(vm, src.c_str(), static_cast<SQInteger>(src.size()),
908 _SC("eval"), SQTrue))) {
909 sq_settop(vm, top);
911 VariableInfo info;
912 info.name = expression;
913 info.type = "error";
914 info.value = ctx.empty() ? "compile error" : eve::script::formatScriptError(ctx);
915 return info;
916 }
917 // stack: [env, closure]
918 sq_push(vm, -2); // [env, closure, env]
919 sq_setclosureroot(vm, -2); // closure env = locals table (pops copy)
920 sq_pushroottable(vm); // this/arg for the call
921 VariableInfo info;
922 info.name = expression;
923 if (SQ_SUCCEEDED(sq_call(vm, 1, SQTrue, SQFalse))) {
924 info = describeAt(vm, -1);
925 } else {
927 info.type = "error";
928 info.value = ctx.empty() ? "eval error" : eve::script::formatScriptError(ctx);
929 }
930 sq_settop(vm, top);
931 return info;
932}
933
934std::vector<StackFrameInfo> Debugger::stackTrace(int maxFrames) const {
935 std::vector<StackFrameInfo> out;
936 HSQUIRRELVM vm = vm_;
937 if (!vm) return out;
938 if (maxFrames <= 0) maxFrames = 32;
939 for (int level = 0; level <= maxFrames; ++level) {
940 SQStackInfos si;
941 if (SQ_FAILED(sq_stackinfos(vm, level, &si))) break;
943 f.id = level;
944 if (si.source) f.loc.source = si.source;
945 f.loc.line = static_cast<int>(si.line);
946 if (si.funcname) {
947 f.loc.function = si.funcname;
948 f.name = si.funcname;
949 } else {
950 f.name = "?";
951 }
952 // Native-only frames (Squirrel reports source "NATIVE" / line -1, e.g.
953 // the uncaught-error hook parked above the throwing script frame) carry
954 // no script location. Skip them so frame 0 in the IDE is the throw
955 // site; ids stay at stack levels so scopes(frameId) still resolves to
956 // the right frame's locals.
957 if (f.loc.source == "NATIVE" || (f.loc.source.empty() && f.loc.line <= 0)) continue;
958 out.push_back(std::move(f));
959 }
960 return out;
961}
962
963} // namespace eve::dev
struct SQVM * HSQUIRRELVM
int line
Tok kind
HSQUIRRELVM vm
Definition ECS.cpp:20
glm::vec3 n
Definition Grass.cpp:64
int w
float depth
uint32_t a
uint32_t b
uint32_t c
int idx
float f
const char * name
Definition RockMesh.cpp:21
bool enabled
int v
float m[16]
uint32_t s
Definition Weather.cpp:28
Script + frame debugger: pause/step, breakpoints, watches.
Definition Debugger.hpp:83
bool hasBreakpoint(const std::string &source, int line) const
Definition Debugger.cpp:630
void waitWhilePaused(const std::function< void()> &pump={})
Block until resume/step/detach (processes external poll callbacks).
Definition Debugger.cpp:572
static std::string sourceBasename(const std::string &source)
Basename of a normalized path (empty-safe).
Definition Debugger.cpp:326
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...
Definition Debugger.cpp:853
void stepInto()
Enter calls: stop on the next script line at any depth.
Definition Debugger.cpp:419
void refreshWatches()
Re-evaluate all watches against current VM (paused preferred).
Definition Debugger.cpp:663
void notifyFrameDone()
After a frame when StepFrame was active → return to Paused.
Definition Debugger.cpp:453
void stepOut()
Finish current function: stop when stack depth drops.
Definition Debugger.cpp:423
HSQUIRRELVM vm() const
Definition Debugger.hpp:93
bool onScriptLine(const SourceLoc &loc)
Called from Squirrel line debug hook. Returns true if execution should block (breakpoint / step).
Definition Debugger.cpp:488
std::vector< WatchEntry > watches() const
Definition Debugger.cpp:658
static std::string normalizeSource(std::string source)
Normalize source paths for breakpoint matching (basename fallback).
Definition Debugger.cpp:309
std::vector< VariableInfo > globals() const
Root-table slots (globals).
Definition Debugger.cpp:801
bool removeWatch(const std::string &expression)
Definition Debugger.cpp:644
std::vector< VariableInfo > locals(int stackLevel=0) const
Locals of the given call-stack level (0 = current script frame).
Definition Debugger.cpp:781
void stepOver()
Skip calls: stop on the next line at ≤ current stack depth.
Definition Debugger.cpp:421
void addWatch(std::string expression)
Definition Debugger.cpp:635
int scriptStackDepth() const
Current Squirrel call depth (1 = topmost script frame). 0 if none.
Definition Debugger.cpp:391
bool shouldRunUpdate()
Frame loop: true ⇒ call eve_update this frame. Consumes StepFrame.
Definition Debugger.cpp:443
void pause(PauseReason reason=PauseReason::PauseKey)
Definition Debugger.cpp:368
bool clearBreakpoint(std::string source, int line)
Definition Debugger.cpp:603
int setBreakpoint(std::string source, int line, bool enabled=true, std::string condition={})
Definition Debugger.cpp:581
static Debugger & instance()
Definition Debugger.cpp:304
std::vector< Breakpoint > breakpoints() const
Definition Debugger.cpp:625
void step()
Convenience: script stepOver when mid-hook; otherwise one game frame. Prefer stepInto/stepOver/stepOu...
Definition Debugger.cpp:432
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, basename,...
Definition Debugger.cpp:332
void attach(HSQUIRRELVM vm)
Definition Debugger.cpp:351
std::vector< StackFrameInfo > stackTrace(int maxFrames=32) const
Definition Debugger.cpp:934
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,...
Definition Debugger.cpp:872
void clearBreakpoints(const std::string &source={})
Definition Debugger.cpp:614
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/resolvePath).
Definition Debugger.hpp:67
ScriptErrorContext captureCompileError(HSQUIRRELVM vm)
Captures the last compilation error recorded by the VM.
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.
std::string condition
Definition Debugger.hpp:41
std::string source
Definition Debugger.hpp:37
Source location in a Squirrel (or synthetic) script.
Definition CallGraph.hpp:16
bool empty() const
Definition CallGraph.hpp:21
std::string source
Definition CallGraph.hpp:17
std::string expression
Definition Debugger.hpp:46
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