diff --git a/CHANGELOG.md b/CHANGELOG.md index 55cacc3b..06be61d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Change Log +## [Unreleased changes] - 2026-MM-DD +### Breaking changes + +### Added +- new debugger commands: `stack ` and `locals ` to print the values on the stack and in the current locals scope + +### Changed + +### Removed + ## [4.3.3] - 2026-03-01 ### Changed - runtime type checking errors are on stderr instead of stdout diff --git a/include/Ark/VM/Debugger.hpp b/include/Ark/VM/Debugger.hpp index 34a8c740..eb5d76f3 100644 --- a/include/Ark/VM/Debugger.hpp +++ b/include/Ark/VM/Debugger.hpp @@ -117,8 +117,14 @@ namespace Ark::internal std::size_t m_line_count { 0 }; void showContext(const VM& vm, const ExecutionContext& context) const; + void showStack(VM& vm, const ExecutionContext& context, std::size_t count) const; + void showLocals(VM& vm, ExecutionContext& context, std::size_t count) const; - std::optional prompt(std::size_t ip, std::size_t pp); + static std::optional getCommandArg(const std::string& command, const std::string& line); + static std::optional parseStringAsInt(const std::string& str); + [[nodiscard]] std::optional getArgAndParseOrError(const std::string& command, const std::string& line, std::size_t default_value) const; + + std::optional prompt(std::size_t ip, std::size_t pp, VM& vm, ExecutionContext& context); /** * @brief Take care of compiling new code using the existing data tables diff --git a/src/arkreactor/VM/Debugger.cpp b/src/arkreactor/VM/Debugger.cpp index 3b078168..b3f56140 100644 --- a/src/arkreactor/VM/Debugger.cpp +++ b/src/arkreactor/VM/Debugger.cpp @@ -3,6 +3,9 @@ #include #include #include +#include +#include +#include #include #include @@ -59,6 +62,8 @@ namespace Ark::internal void Debugger::run(VM& vm, ExecutionContext& context, const bool from_breakpoint) { + using namespace std::chrono_literals; + if (from_breakpoint) showContext(vm, context); @@ -72,7 +77,7 @@ namespace Ark::internal while (true) { - std::optional maybe_input = prompt(ip_at_breakpoint, pp_at_breakpoint); + std::optional maybe_input = prompt(ip_at_breakpoint, pp_at_breakpoint, vm, context); if (maybe_input) { @@ -102,6 +107,8 @@ namespace Ark::internal m_colorize ? fmt::fg(fmt::color::chocolate) : fmt::text_style())); } } + else + 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 } else break; @@ -143,7 +150,101 @@ namespace Ark::internal } } - std::optional Debugger::prompt(const std::size_t ip, const std::size_t pp) + void Debugger::showStack(VM& vm, const ExecutionContext& context, const std::size_t count) const + { + std::size_t i = 1; + do + { + if (context.sp < i) + break; + + const auto color = m_colorize ? fmt::fg(i % 2 == 0 ? fmt::color::forest_green : fmt::color::cornflower_blue) : fmt::text_style(); + fmt::println( + m_os, + "{} -> {}", + fmt::styled(context.sp - i, color), + fmt::styled(context.stack[context.sp - i].toString(vm, /* show_as_code= */ true), color)); + ++i; + } while (i < count); + + if (context.sp == 0) + fmt::println(m_os, "Stack is empty"); + + fmt::println(m_os, ""); + } + + void Debugger::showLocals(VM& vm, ExecutionContext& context, const std::size_t count) const + { + const std::size_t limit = context.locals[context.locals.size() - 2].size(); // -2 because we created a scope for the debugger + if (limit > 0 && count > 0) + { + fmt::println(m_os, "scope size: {}", limit); + fmt::println(m_os, "index | id | type | value"); + std::size_t i = 0; + + do + { + if (limit <= i) + break; + + auto& [id, value] = context.locals[context.locals.size() - 2].atPosReverse(i); + const auto color = m_colorize ? fmt::fg(i % 2 == 0 ? fmt::color::forest_green : fmt::color::cornflower_blue) : fmt::text_style(); + + fmt::println( + m_os, + "{:>5} | {:3} | {:>9} | {}", + fmt::styled(limit - i - 1, color), + fmt::styled(id, color), + fmt::styled(std::to_string(value.valueType()), color), + fmt::styled(value.toString(vm, /* show_as_code= */ true), color)); + ++i; + } while (i < count); + } + else + fmt::println(m_os, "Current scope is empty"); + + fmt::println(m_os, ""); + } + + std::optional Debugger::getCommandArg(const std::string& command, const std::string& line) + { + std::string arg = line.substr(command.size()); + Utils::trimWhitespace(arg); + + if (arg.empty()) + return std::nullopt; + return arg; + } + + std::optional Debugger::parseStringAsInt(const std::string& str) + { + std::size_t result = 0; + auto [ptr, ec] = std::from_chars(str.data(), str.data() + str.size(), result); + + if (ec == std::errc()) + return result; + return std::nullopt; + } + + std::optional Debugger::getArgAndParseOrError(const std::string& command, const std::string& line, const std::size_t default_value) const + { + const auto maybe_arg = getCommandArg(command, line); + std::size_t count = default_value; + if (maybe_arg) + { + if (const auto maybe_int = parseStringAsInt(maybe_arg.value())) + count = maybe_int.value(); + else + { + fmt::println(m_os, "Couldn't parse argument as an integer"); + return std::nullopt; + } + } + + return count; + } + + std::optional Debugger::prompt(const std::size_t ip, const std::size_t pp, VM& vm, ExecutionContext& context) { std::string code; long open_parens = 0; @@ -182,12 +283,28 @@ namespace Ark::internal m_quit_vm = true; return std::nullopt; } + else if (line.starts_with("stack")) + { + if (auto arg = getArgAndParseOrError("stack", line, /* default_value= */ 5)) + showStack(vm, context, arg.value()); + else + return std::nullopt; + } + else if (line.starts_with("locals")) + { + if (auto arg = getArgAndParseOrError("locals", line, /* default_value= */ 5)) + showLocals(vm, context, arg.value()); + else + return std::nullopt; + } else if (line == "help") { fmt::println(m_os, "Available commands:"); fmt::println(m_os, " help -- display this message"); fmt::println(m_os, " c, continue -- resume execution"); fmt::println(m_os, " q, quit -- quit the debugger, stopping the script execution"); + fmt::println(m_os, " stack -- show the last n values on the stack"); + fmt::println(m_os, " locals -- show the last n values on the locals' stack"); } else { diff --git a/tests/unittests/resources/DebuggerSuite/basic.expected b/tests/unittests/resources/DebuggerSuite/basic.expected index 3406a46d..90fe228b 100644 --- a/tests/unittests/resources/DebuggerSuite/basic.expected +++ b/tests/unittests/resources/DebuggerSuite/basic.expected @@ -31,6 +31,8 @@ Available commands: help -- display this message c, continue -- resume execution q, quit -- quit the debugger, stopping the script execution + stack -- show the last n values on the stack + locals -- show the last n values on the locals' stack dbg[pp:1,ip:20]:001> continue dbg: continue ark: in (foo x y z), after second breakpoint diff --git a/tests/unittests/resources/DebuggerSuite/stack_and_locals.ark b/tests/unittests/resources/DebuggerSuite/stack_and_locals.ark new file mode 100644 index 00000000..931d4f28 --- /dev/null +++ b/tests/unittests/resources/DebuggerSuite/stack_and_locals.ark @@ -0,0 +1,9 @@ +(let f (fun (a b) { + (let x a) + (breakpoint) + (if (= 1 a) + (f "correct" "wrong") + x) })) + +(breakpoint) +(prn (f 1 2)) diff --git a/tests/unittests/resources/DebuggerSuite/stack_and_locals.expected b/tests/unittests/resources/DebuggerSuite/stack_and_locals.expected new file mode 100644 index 00000000..f960fb2e --- /dev/null +++ b/tests/unittests/resources/DebuggerSuite/stack_and_locals.expected @@ -0,0 +1,87 @@ +In file tests/unittests/resources/DebuggerSuite/stack_and_locals.ark:8 + 5 | (f "correct" "wrong") + 6 | x) })) + 7 | + 8 | (breakpoint) + | ^~~~~~~~~~~ + 9 | (prn (f 1 2)) + 10 | + +dbg[pp:0,ip:8]:000> stack +Stack is empty + +dbg[pp:0,ip:8]:000> locals +scope size: 2 +index | id | type | value + 1 | 0 | Function | Function@1 + 0 | 4 | CProc | CProcedure + +dbg[pp:0,ip:8]:000> c +dbg: continue + +In file tests/unittests/resources/DebuggerSuite/stack_and_locals.ark:3 + 1 | (let f (fun (a b) { + 2 | (let x a) + 3 | (breakpoint) + | ^~~~~~~~~~~ + 4 | (if (= 1 a) + 5 | (f "correct" "wrong") + +dbg[pp:1,ip:16]:000> stack +3 -> Instruction@28 +2 -> Function@0 +1 -> Instruction@32 +0 -> Function@0 + +dbg[pp:1,ip:16]:000> locals 1 +scope size: 3 +index | id | type | value + 2 | 3 | Number | 1 + +dbg[pp:1,ip:16]:000> locals 2 +scope size: 3 +index | id | type | value + 2 | 3 | Number | 1 + 1 | 2 | Number | 2 + +dbg[pp:1,ip:16]:000> locals 3 +scope size: 3 +index | id | type | value + 2 | 3 | Number | 1 + 1 | 2 | Number | 2 + 0 | 1 | Number | 1 + +dbg[pp:1,ip:16]:000> locals +scope size: 3 +index | id | type | value + 2 | 3 | Number | 1 + 1 | 2 | Number | 2 + 0 | 1 | Number | 1 + +dbg[pp:1,ip:16]:000> c +dbg: continue + +In file tests/unittests/resources/DebuggerSuite/stack_and_locals.ark:3 + 1 | (let f (fun (a b) { + 2 | (let x a) + 3 | (breakpoint) + | ^~~~~~~~~~~ + 4 | (if (= 1 a) + 5 | (f "correct" "wrong") + +dbg[pp:1,ip:16]:000> stack +3 -> Instruction@28 +2 -> Function@0 +1 -> Instruction@32 +0 -> Function@0 + +dbg[pp:1,ip:16]:000> locals +scope size: 3 +index | id | type | value + 2 | 3 | String | "correct" + 1 | 2 | String | "wrong" + 0 | 1 | String | "correct" + +dbg[pp:1,ip:16]:000> c +dbg: continue +correct diff --git a/tests/unittests/resources/DebuggerSuite/stack_and_locals.prompt b/tests/unittests/resources/DebuggerSuite/stack_and_locals.prompt new file mode 100644 index 00000000..c25e5fe4 --- /dev/null +++ b/tests/unittests/resources/DebuggerSuite/stack_and_locals.prompt @@ -0,0 +1,12 @@ +stack +locals +c +stack +locals 1 +locals 2 +locals 3 +locals +c +stack +locals +c