Skip to content

Commit b9dcdb6

Browse files
committed
feat(debugger): ARK-340, add 'stack' and 'locals' command to the debugger
1 parent 946d9b0 commit b9dcdb6

File tree

7 files changed

+246
-3
lines changed

7 files changed

+246
-3
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Change Log
22

3+
## [Unreleased changes] - 2026-MM-DD
4+
### Breaking changes
5+
6+
### Added
7+
- new debugger commands: `stack <n>` and `locals <n>` to print the values on the stack and in the current locals scope
8+
9+
### Changed
10+
11+
### Removed
12+
313
## [4.3.3] - 2026-03-01
414
### Changed
515
- runtime type checking errors are on stderr instead of stdout

include/Ark/VM/Debugger.hpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,14 @@ namespace Ark::internal
117117
std::size_t m_line_count { 0 };
118118

119119
void showContext(const VM& vm, const ExecutionContext& context) const;
120+
void showStack(VM& vm, const ExecutionContext& context, std::size_t count) const;
121+
void showLocals(VM& vm, ExecutionContext& context, std::size_t count) const;
120122

121-
std::optional<std::string> prompt(std::size_t ip, std::size_t pp);
123+
static std::optional<std::string> getCommandArg(const std::string& command, const std::string& line);
124+
static std::optional<std::size_t> parseStringAsInt(const std::string& str);
125+
[[nodiscard]] std::optional<std::size_t> getArgAndParseOrError(const std::string& command, const std::string& line, std::size_t default_value) const;
126+
127+
std::optional<std::string> prompt(std::size_t ip, std::size_t pp, VM& vm, ExecutionContext& context);
122128

123129
/**
124130
* @brief Take care of compiling new code using the existing data tables

src/arkreactor/VM/Debugger.cpp

Lines changed: 119 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
#include <fmt/core.h>
44
#include <fmt/color.h>
55
#include <fmt/ostream.h>
6+
#include <chrono>
7+
#include <thread>
8+
#include <charconv>
69

710
#include <Ark/State.hpp>
811
#include <Ark/VM/VM.hpp>
@@ -59,6 +62,8 @@ namespace Ark::internal
5962

6063
void Debugger::run(VM& vm, ExecutionContext& context, const bool from_breakpoint)
6164
{
65+
using namespace std::chrono_literals;
66+
6267
if (from_breakpoint)
6368
showContext(vm, context);
6469

@@ -72,7 +77,7 @@ namespace Ark::internal
7277

7378
while (true)
7479
{
75-
std::optional<std::string> maybe_input = prompt(ip_at_breakpoint, pp_at_breakpoint);
80+
std::optional<std::string> maybe_input = prompt(ip_at_breakpoint, pp_at_breakpoint, vm, context);
7681

7782
if (maybe_input)
7883
{
@@ -102,6 +107,8 @@ namespace Ark::internal
102107
m_colorize ? fmt::fg(fmt::color::chocolate) : fmt::text_style()));
103108
}
104109
}
110+
else
111+
std::this_thread::sleep_for(50ms); // hack to wait for the diagnostics to be output to stderr, since we write to stdout and it's faster than stderr
105112
}
106113
else
107114
break;
@@ -143,7 +150,101 @@ namespace Ark::internal
143150
}
144151
}
145152

146-
std::optional<std::string> Debugger::prompt(const std::size_t ip, const std::size_t pp)
153+
void Debugger::showStack(VM& vm, const ExecutionContext& context, const std::size_t count) const
154+
{
155+
std::size_t i = 1;
156+
do
157+
{
158+
if (context.sp < i)
159+
break;
160+
161+
const auto color = m_colorize ? fmt::fg(i % 2 == 0 ? fmt::color::forest_green : fmt::color::cornflower_blue) : fmt::text_style();
162+
fmt::println(
163+
m_os,
164+
"{} -> {}",
165+
fmt::styled(context.sp - i, color),
166+
fmt::styled(context.stack[context.sp - i].toString(vm, /* show_as_code= */ true), color));
167+
++i;
168+
} while (i < count);
169+
170+
if (context.sp == 0)
171+
fmt::println(m_os, "Stack is empty");
172+
173+
fmt::println(m_os, "");
174+
}
175+
176+
void Debugger::showLocals(VM& vm, ExecutionContext& context, const std::size_t count) const
177+
{
178+
const std::size_t limit = context.locals[context.locals.size() - 2].size(); // -2 because we created a scope for the debugger
179+
if (limit > 0 && count > 0)
180+
{
181+
fmt::println(m_os, "scope size: {}", limit);
182+
fmt::println(m_os, "index | id | type | value");
183+
std::size_t i = 0;
184+
185+
do
186+
{
187+
if (limit <= i)
188+
break;
189+
190+
auto& [id, value] = context.locals[context.locals.size() - 2].atPosReverse(i);
191+
const auto color = m_colorize ? fmt::fg(i % 2 == 0 ? fmt::color::forest_green : fmt::color::cornflower_blue) : fmt::text_style();
192+
193+
fmt::println(
194+
m_os,
195+
"{:>5} | {:3} | {:>9} | {}",
196+
fmt::styled(limit - i - 1, color),
197+
fmt::styled(id, color),
198+
fmt::styled(std::to_string(value.valueType()), color),
199+
fmt::styled(value.toString(vm, /* show_as_code= */ true), color));
200+
++i;
201+
} while (i < count);
202+
}
203+
else
204+
fmt::println(m_os, "Current scope is empty");
205+
206+
fmt::println(m_os, "");
207+
}
208+
209+
std::optional<std::string> Debugger::getCommandArg(const std::string& command, const std::string& line)
210+
{
211+
std::string arg = line.substr(command.size());
212+
Utils::trimWhitespace(arg);
213+
214+
if (arg.empty())
215+
return std::nullopt;
216+
return arg;
217+
}
218+
219+
std::optional<std::size_t> Debugger::parseStringAsInt(const std::string& str)
220+
{
221+
std::size_t result = 0;
222+
auto [ptr, ec] = std::from_chars(str.data(), str.data() + str.size(), result);
223+
224+
if (ec == std::errc())
225+
return result;
226+
return std::nullopt;
227+
}
228+
229+
std::optional<std::size_t> Debugger::getArgAndParseOrError(const std::string& command, const std::string& line, const std::size_t default_value) const
230+
{
231+
const auto maybe_arg = getCommandArg(command, line);
232+
std::size_t count = default_value;
233+
if (maybe_arg)
234+
{
235+
if (const auto maybe_int = parseStringAsInt(maybe_arg.value()))
236+
count = maybe_int.value();
237+
else
238+
{
239+
fmt::println(m_os, "Couldn't parse argument as an integer");
240+
return std::nullopt;
241+
}
242+
}
243+
244+
return count;
245+
}
246+
247+
std::optional<std::string> Debugger::prompt(const std::size_t ip, const std::size_t pp, VM& vm, ExecutionContext& context)
147248
{
148249
std::string code;
149250
long open_parens = 0;
@@ -182,12 +283,28 @@ namespace Ark::internal
182283
m_quit_vm = true;
183284
return std::nullopt;
184285
}
286+
else if (line.starts_with("stack"))
287+
{
288+
if (auto arg = getArgAndParseOrError("stack", line, /* default_value= */ 5))
289+
showStack(vm, context, arg.value());
290+
else
291+
return std::nullopt;
292+
}
293+
else if (line.starts_with("locals"))
294+
{
295+
if (auto arg = getArgAndParseOrError("locals", line, /* default_value= */ 5))
296+
showLocals(vm, context, arg.value());
297+
else
298+
return std::nullopt;
299+
}
185300
else if (line == "help")
186301
{
187302
fmt::println(m_os, "Available commands:");
188303
fmt::println(m_os, " help -- display this message");
189304
fmt::println(m_os, " c, continue -- resume execution");
190305
fmt::println(m_os, " q, quit -- quit the debugger, stopping the script execution");
306+
fmt::println(m_os, " stack <n=5> -- show the last n values on the stack");
307+
fmt::println(m_os, " locals <n=5> -- show the last n values on the locals' stack");
191308
}
192309
else
193310
{

tests/unittests/resources/DebuggerSuite/basic.expected

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ Available commands:
3131
help -- display this message
3232
c, continue -- resume execution
3333
q, quit -- quit the debugger, stopping the script execution
34+
stack <n=5> -- show the last n values on the stack
35+
locals <n=5> -- show the last n values on the locals' stack
3436
dbg[pp:1,ip:20]:001> continue
3537
dbg: continue
3638
ark: in (foo x y z), after second breakpoint
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
(let f (fun (a b) {
2+
(let x a)
3+
(breakpoint)
4+
(if (= 1 a)
5+
(f "correct" "wrong")
6+
x) }))
7+
8+
(breakpoint)
9+
(prn (f 1 2))
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
In file tests/unittests/resources/DebuggerSuite/stack_and_locals.ark:8
2+
5 | (f "correct" "wrong")
3+
6 | x) }))
4+
7 |
5+
8 | (breakpoint)
6+
| ^~~~~~~~~~~
7+
9 | (prn (f 1 2))
8+
10 |
9+
10+
dbg[pp:0,ip:8]:000> stack
11+
Stack is empty
12+
13+
dbg[pp:0,ip:8]:000> locals
14+
scope size: 2
15+
index | id | type | value
16+
1 | 0 | Function | Function@1
17+
0 | 4 | CProc | CProcedure
18+
19+
dbg[pp:0,ip:8]:000> c
20+
dbg: continue
21+
22+
In file tests/unittests/resources/DebuggerSuite/stack_and_locals.ark:3
23+
1 | (let f (fun (a b) {
24+
2 | (let x a)
25+
3 | (breakpoint)
26+
| ^~~~~~~~~~~
27+
4 | (if (= 1 a)
28+
5 | (f "correct" "wrong")
29+
30+
dbg[pp:1,ip:16]:000> stack
31+
3 -> Instruction@28
32+
2 -> Function@0
33+
1 -> Instruction@32
34+
0 -> Function@0
35+
36+
dbg[pp:1,ip:16]:000> locals 1
37+
scope size: 3
38+
index | id | type | value
39+
2 | 3 | Number | 1
40+
41+
dbg[pp:1,ip:16]:000> locals 2
42+
scope size: 3
43+
index | id | type | value
44+
2 | 3 | Number | 1
45+
1 | 2 | Number | 2
46+
47+
dbg[pp:1,ip:16]:000> locals 3
48+
scope size: 3
49+
index | id | type | value
50+
2 | 3 | Number | 1
51+
1 | 2 | Number | 2
52+
0 | 1 | Number | 1
53+
54+
dbg[pp:1,ip:16]:000> locals
55+
scope size: 3
56+
index | id | type | value
57+
2 | 3 | Number | 1
58+
1 | 2 | Number | 2
59+
0 | 1 | Number | 1
60+
61+
dbg[pp:1,ip:16]:000> c
62+
dbg: continue
63+
64+
In file tests/unittests/resources/DebuggerSuite/stack_and_locals.ark:3
65+
1 | (let f (fun (a b) {
66+
2 | (let x a)
67+
3 | (breakpoint)
68+
| ^~~~~~~~~~~
69+
4 | (if (= 1 a)
70+
5 | (f "correct" "wrong")
71+
72+
dbg[pp:1,ip:16]:000> stack
73+
3 -> Instruction@28
74+
2 -> Function@0
75+
1 -> Instruction@32
76+
0 -> Function@0
77+
78+
dbg[pp:1,ip:16]:000> locals
79+
scope size: 3
80+
index | id | type | value
81+
2 | 3 | String | "correct"
82+
1 | 2 | String | "wrong"
83+
0 | 1 | String | "correct"
84+
85+
dbg[pp:1,ip:16]:000> c
86+
dbg: continue
87+
correct
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
stack
2+
locals
3+
c
4+
stack
5+
locals 1
6+
locals 2
7+
locals 3
8+
locals
9+
c
10+
stack
11+
locals
12+
c

0 commit comments

Comments
 (0)