Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Change Log

## [Unreleased changes] - 2026-MM-DD
### Breaking changes

### Added
- new debugger commands: `stack <n>` and `locals <n>` 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
Expand Down
8 changes: 7 additions & 1 deletion include/Ark/VM/Debugger.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string> prompt(std::size_t ip, std::size_t pp);
static std::optional<std::string> getCommandArg(const std::string& command, const std::string& line);
static std::optional<std::size_t> parseStringAsInt(const std::string& str);
[[nodiscard]] std::optional<std::size_t> getArgAndParseOrError(const std::string& command, const std::string& line, std::size_t default_value) const;

std::optional<std::string> 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
Expand Down
121 changes: 119 additions & 2 deletions src/arkreactor/VM/Debugger.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
#include <fmt/core.h>
#include <fmt/color.h>
#include <fmt/ostream.h>
#include <chrono>
#include <thread>
#include <charconv>

#include <Ark/State.hpp>
#include <Ark/VM/VM.hpp>
Expand Down Expand Up @@ -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);

Expand All @@ -72,7 +77,7 @@ namespace Ark::internal

while (true)
{
std::optional<std::string> maybe_input = prompt(ip_at_breakpoint, pp_at_breakpoint);
std::optional<std::string> maybe_input = prompt(ip_at_breakpoint, pp_at_breakpoint, vm, context);

if (maybe_input)
{
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -143,7 +150,101 @@ namespace Ark::internal
}
}

std::optional<std::string> 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<std::string> 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<std::size_t> 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<std::size_t> 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<std::string> Debugger::prompt(const std::size_t ip, const std::size_t pp, VM& vm, ExecutionContext& context)
{
std::string code;
long open_parens = 0;
Expand Down Expand Up @@ -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 <n=5> -- show the last n values on the stack");
fmt::println(m_os, " locals <n=5> -- show the last n values on the locals' stack");
}
else
{
Expand Down
2 changes: 2 additions & 0 deletions tests/unittests/resources/DebuggerSuite/basic.expected
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ Available commands:
help -- display this message
c, continue -- resume execution
q, quit -- quit the debugger, stopping the script execution
stack <n=5> -- show the last n values on the stack
locals <n=5> -- 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
Expand Down
9 changes: 9 additions & 0 deletions tests/unittests/resources/DebuggerSuite/stack_and_locals.ark
Original file line number Diff line number Diff line change
@@ -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))
87 changes: 87 additions & 0 deletions tests/unittests/resources/DebuggerSuite/stack_and_locals.expected
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions tests/unittests/resources/DebuggerSuite/stack_and_locals.prompt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
stack
locals
c
stack
locals 1
locals 2
locals 3
locals
c
stack
locals
c
Loading