Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
32134cf
feat(instructions, vm, compiler)!: adding BREAKPOINT instruction and …
SuperFola Jan 5, 2026
236239e
feat(error messages): enhance operator arity error messages by adding…
SuperFola Jan 5, 2026
1ff32ee
feat(ast lowerer): breakpoints can be placed anywhere without disturb…
SuperFola Jan 5, 2026
8cdae3e
feat(cli): add -fdebugger to toggle the debugger in the VM when an er…
SuperFola Jan 5, 2026
cf0ba80
refactor: create a dedicated Ark/VM/Value/ folder for better organiza…
SuperFola Jan 12, 2026
d43ba70
refactor: print vm pointers in VM::showBacktraceWithException instead…
SuperFola Jan 12, 2026
f4650b0
feat(vm): enhancing the arity error handling and adding a test for wh…
SuperFola Jan 12, 2026
e652ec3
feat(debugger): creating a very basic debugger, started by VM::showBa…
SuperFola Jan 12, 2026
bacd46a
refactor(macro processor): compute the arg_name of a function macro o…
SuperFola Jan 15, 2026
453e86f
feat(tests): testing the new functionalities developped for the debugger
SuperFola Jan 15, 2026
e1fd022
feat(breakpoints): allow 'breakpoint' to take 0 or 1 argument (boolea…
SuperFola Jan 18, 2026
31f78d0
feat(debugger): create a minimalistic shell for the debugger
SuperFola Jan 18, 2026
b204126
feat(debugger): adding a way to use a prompt file for the debugger, i…
SuperFola Jan 19, 2026
da2690d
chore: use const vector& when loading lambdas into Ark::State
SuperFola Jan 19, 2026
8dfa1d6
chore: addressing cppcheck recommandations: more ranges, less variabl…
SuperFola Jan 19, 2026
d599a70
feat(tests, debugger): testing the debugger triggering on errors
SuperFola Jan 19, 2026
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
Prev Previous commit
Next Next commit
feat(debugger): adding a way to use a prompt file for the debugger, i…
…nstead of requiring the user to provide an input
  • Loading branch information
SuperFola committed Jan 19, 2026
commit b204126c3f89ce3318d0e67b7dfc28d4dba9a59e
4 changes: 2 additions & 2 deletions include/Ark/Constants.hpp.in
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ namespace Ark
// Compiler options
constexpr uint16_t FeatureImportSolver = 1 << 0;
constexpr uint16_t FeatureMacroProcessor = 1 << 1;
constexpr uint16_t FeatureASTOptimizer = 1 << 2; ///< This is disabled so that embedding ArkScript does not prune nodes from the AST ; it is active in the `arkscript` executable
constexpr uint16_t FeatureASTOptimizer = 1 << 2; ///< Disabled by default because embedding ArkScript should not prune nodes from the AST ; it is active in the `arkscript` executable
constexpr uint16_t FeatureIROptimizer = 1 << 3;
constexpr uint16_t FeatureNameResolver = 1 << 4;
constexpr uint16_t FeatureVMDebugger = 1 << 5; ///< This is disabled so that embedding ArkScript does not launch the debugger on every error when running code
constexpr uint16_t FeatureVMDebugger = 1 << 5; ///< Disabled by default because embedding ArkScript should not launch the debugger on every error when running code

constexpr uint16_t FeatureDumpIR = 1 << 14;
/// This feature should only be used in tests, to disable diagnostics generation and enable exceptions to be thrown
Expand Down
37 changes: 30 additions & 7 deletions include/Ark/VM/Debugger.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ namespace Ark::internal
std::vector<std::shared_ptr<ClosureScope>> closure_scopes;
};

struct CompiledPrompt
{
std::vector<bytecode_t> pages;
std::vector<std::string> symbols;
std::vector<Value> constants;
};

class Debugger
{
public:
Expand All @@ -48,7 +55,18 @@ namespace Ark::internal
* @param symbols symbols table of the VM
* @param constants constants table of the VM
*/
explicit Debugger(const ExecutionContext& context, const std::vector<std::filesystem::path>& libenv, const std::vector<std::string>& symbols, const std::vector<Value>& constants);
Debugger(const ExecutionContext& context, const std::vector<std::filesystem::path>& libenv, const std::vector<std::string>& symbols, const std::vector<Value>& constants);

/**
* @brief Create a new Debugger object that will use lines from a file as prompts, instead of waiting for user inputs
*
* @param libenv
* @param path_to_prompt_file
* @param os output stream
* @param symbols symbols table of the VM
* @param constants constants table of the VM
*/
Debugger(const std::vector<std::filesystem::path>& libenv, const std::string& path_to_prompt_file, std::ostream& os, const std::vector<std::string>& symbols, const std::vector<Value>& constants);

/**
* @brief Save the current VM state, to get back to it once the debugger is done running
Expand Down Expand Up @@ -87,22 +105,27 @@ namespace Ark::internal
std::vector<std::filesystem::path> m_libenv;
std::vector<std::string> m_symbols;
std::vector<Value> m_constants;
bool m_running;
bool m_quit_vm;
bool m_running { false };
bool m_quit_vm { false };

std::ostream& m_os;
bool m_colorize;
std::unique_ptr<std::istream> m_prompt_stream;
std::string m_code; ///< Code added while inside the debugger
std::size_t m_line_count = 0;
std::size_t m_line_count { 0 };

void showContext(const VM& vm, const ExecutionContext& context) const;

std::optional<std::string> prompt();
std::optional<std::string> prompt(std::size_t ip, std::size_t pp);

/**
* @brief Take care of compiling new code using the existing data tables
*
* @param code
* @param start_page_at_offset offset to start the new pages at
* @return std::optional<std::vector<bytecode_t>> optional set of bytecode pages if compilation succeeded
* @return std::optional<CompiledPrompt> optional set of bytecode pages, symbols and constants if compilation succeeded
*/
std::optional<std::vector<bytecode_t>> compile(const std::string& code, std::size_t start_page_at_offset);
[[nodiscard]] std::optional<CompiledPrompt> compile(const std::string& code, std::size_t start_page_at_offset) const;
};
}

Expand Down
8 changes: 8 additions & 0 deletions include/Ark/VM/VM.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,14 @@ namespace Ark
*/
[[nodiscard]] bool forceReloadPlugins() const;

/**
* @brief Configure the debugger to use a prompt file instead of asking the user for an input
*
* @param path path to prompt file (one prompt per line)
* @param os output stream
*/
void usePromptFileForDebugger(const std::string& path, std::ostream& os = std::cout);

/**
* @brief Throw a VM error message
*
Expand Down
2 changes: 1 addition & 1 deletion src/arkreactor/Compiler/Welder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ namespace Ark
m_root_file = std::filesystem::current_path(); // No filename given, take the current working directory

for (const std::string& sym : symbols)
m_name_resolver.addDefinedSymbol(sym, /* is_mutable= */ false);
m_name_resolver.addDefinedSymbol(sym, /* is_mutable= */ true);
return computeAST(ARK_NO_NAME_FILE, code);
}

Expand Down
128 changes: 81 additions & 47 deletions src/arkreactor/VM/Debugger.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include <fmt/core.h>
#include <fmt/color.h>
#include <fmt/ostream.h>

#include <Ark/State.hpp>
#include <Ark/VM/VM.hpp>
Expand All @@ -13,11 +14,17 @@
namespace Ark::internal
{
Debugger::Debugger(const ExecutionContext& context, const std::vector<std::filesystem::path>& libenv, const std::vector<std::string>& symbols, const std::vector<Value>& constants) :
m_libenv(libenv), m_symbols(symbols), m_constants(constants), m_running(false), m_quit_vm(false)
m_libenv(libenv), m_symbols(symbols), m_constants(constants), m_os(std::cout), m_colorize(true)
{
saveState(context);
}

Debugger::Debugger(const std::vector<std::filesystem::path>& libenv, const std::string& path_to_prompt_file, std::ostream& os, const std::vector<std::string>& symbols, const std::vector<Value>& constants) :
m_libenv(libenv), m_symbols(symbols), m_constants(constants), m_os(os), m_colorize(false)
{
m_prompt_stream = std::make_unique<std::ifstream>(path_to_prompt_file);
}

void Debugger::saveState(const ExecutionContext& context)
{
m_states.emplace_back(
Expand Down Expand Up @@ -45,72 +52,89 @@ namespace Ark::internal

void Debugger::run(VM& vm, ExecutionContext& context)
{
showContext(vm, context);

m_running = true;
const bool is_vm_running = vm.m_running;

// show the line where the breakpoint hit
const auto maybe_source_loc = vm.findSourceLocation(context.ip, context.pp);
if (maybe_source_loc)
{
const auto filename = vm.m_state.m_filenames[maybe_source_loc->filename_id];

if (Utils::fileExists(filename))
{
fmt::println("");
Diagnostics::makeContext(
Diagnostics::ErrorLocation {
.filename = filename,
.start = FilePos { .line = maybe_source_loc->line, .column = 0 },
.end = std::nullopt },
std::cout,
/* maybe_context= */ std::nullopt,
/* colorize= */ true);
fmt::println("");
}
}
const std::size_t ip_at_breakpoint = context.ip,
pp_at_breakpoint = context.pp;
// create dedicated scope, so that we won't be overwriting existing variables
context.locals.emplace_back(context.scopes_storage.data(), context.locals.back().storageEnd());
std::size_t last_ip = 0;

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

if (maybe_input)
{
const std::string& line = maybe_input.value();

if (const auto pages = compile(m_code + line, vm.m_state.m_pages.size()); pages.has_value())
if (const auto compiled = compile(m_code + line, vm.m_state.m_pages.size()); compiled.has_value())
{
context.ip = 0;
context.ip = last_ip;
context.pp = vm.m_state.m_pages.size();
// create dedicated scope, so that we won't be overwriting existing variables
context.locals.emplace_back(context.scopes_storage.data(), context.locals.back().storageEnd());

vm.m_state.extendBytecode(pages.value(), m_symbols, m_constants);
vm.m_state.extendBytecode(compiled->pages, compiled->symbols, compiled->constants);

if (vm.safeRun(context) == 0)
{
// executing code worked
m_code += line;
m_code += line + "\n";
// place ip to end of bytecode instruction (HALT)
last_ip = context.ip - 4;

const Value* maybe_value = vm.peekAndResolveAsPtr(context);
if (maybe_value != nullptr && maybe_value->valueType() != ValueType::Undefined && maybe_value->valueType() != ValueType::InstPtr)
fmt::println("{}", fmt::styled(maybe_value->toString(vm), fmt::fg(fmt::color::chocolate)));
fmt::println(
m_os,
"{}",
fmt::styled(
maybe_value->toString(vm),
m_colorize ? fmt::fg(fmt::color::chocolate) : fmt::text_style()));
}

context.locals.pop_back();
}
}
else
break;
}

m_running = false;
context.locals.pop_back();

// we do not want to retain code from the past executions
m_code.clear();
m_line_count = 0;

// we hit a HALT instruction that set 'running' to false, ignore that if we were still running!
vm.m_running = is_vm_running;
m_running = false;
}

std::optional<std::string> Debugger::prompt()
void Debugger::showContext(const VM& vm, const ExecutionContext& context) const
{
// show the line where the breakpoint hit
const auto maybe_source_loc = vm.findSourceLocation(context.ip, context.pp);
if (maybe_source_loc)
{
const auto filename = vm.m_state.m_filenames[maybe_source_loc->filename_id];

if (Utils::fileExists(filename))
{
fmt::println(m_os, "");
Diagnostics::makeContext(
Diagnostics::ErrorLocation {
.filename = filename,
.start = FilePos { .line = maybe_source_loc->line, .column = 0 },
.end = std::nullopt },
m_os,
/* maybe_context= */ std::nullopt,
/* colorize= */ m_colorize);
fmt::println(m_os, "");
}
}
}

std::optional<std::string> Debugger::prompt(const std::size_t ip, const std::size_t pp)
{
std::string code;
long open_parens = 0;
Expand All @@ -119,29 +143,42 @@ namespace Ark::internal
while (true)
{
const bool unfinished_block = open_parens != 0 || open_braces != 0;
fmt::print("dbg:{:0>3}{} ", m_line_count, unfinished_block ? ":" : ">");
fmt::print(
m_os,
"dbg[{},{}]:{:0>3}{} ",
fmt::format("pp:{}", fmt::styled(pp, m_colorize ? fmt::fg(fmt::color::green) : fmt::text_style())),
fmt::format("ip:{}", fmt::styled(ip, m_colorize ? fmt::fg(fmt::color::cyan) : fmt::text_style())),
m_line_count,
unfinished_block ? ":" : ">");

std::string line;
std::getline(std::cin, line);
if (m_prompt_stream)
{
std::getline(*m_prompt_stream, line);
fmt::println(m_os, "{}", line); // because nothing is printed otherwise, and prompts get printed on the same line
}
else
std::getline(std::cin, line);

Utils::trimWhitespace(line);

if (line == "c" || line == "continue" || line.empty())
{
fmt::println("dbg: continue");
fmt::println(m_os, "dbg: continue");
return std::nullopt;
}
else if (line == "q" || line == "quit")
{
fmt::println("dbg: stop");
fmt::println(m_os, "dbg: stop");
m_quit_vm = true;
return std::nullopt;
}
else if (line == "help")
{
fmt::println("Available commands:");
fmt::println(" help -- display this message");
fmt::println(" c, continue -- resume execution");
fmt::println(" q, quit -- quit the debugger, stopping the script execution");
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");
}
else
{
Expand All @@ -159,7 +196,7 @@ namespace Ark::internal
return code;
}

std::optional<std::vector<bytecode_t>> Debugger::compile(const std::string& code, const std::size_t start_page_at_offset)
std::optional<CompiledPrompt> Debugger::compile(const std::string& code, const std::size_t start_page_at_offset) const
{
Welder welder(0, m_libenv, DefaultFeatures);
if (!welder.computeASTFromStringWithKnownSymbols(code, m_symbols))
Expand All @@ -175,9 +212,6 @@ namespace Ark::internal
const auto inst_locs = bcr.instLocations(files);
const auto [pages, _] = bcr.code(inst_locs);

m_symbols = syms.symbols;
m_constants = vals.values;

return pages;
return std::optional(CompiledPrompt(pages, syms.symbols, vals.values));
}
}
5 changes: 5 additions & 0 deletions src/arkreactor/VM/VM.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,11 @@ namespace Ark
}
}

void VM::usePromptFileForDebugger(const std::string& path, std::ostream& os)
{
m_debugger = std::make_unique<Debugger>(m_state.m_libenv, path, os, m_state.m_symbols, m_state.m_constants);
}

void VM::throwVMError(ErrorKind kind, const std::string& message)
{
throw std::runtime_error(std::string(errorKinds[static_cast<std::size_t>(kind)]) + ": " + message + "\n");
Expand Down
52 changes: 52 additions & 0 deletions tests/unittests/Suites/DebuggerSuite.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#include <boost/ut.hpp>

#include <fmt/ostream.h>

#include <Ark/Ark.hpp>
#include <TestsHelper.hpp>

using namespace boost;

ut::suite<"Debugger"> debugger_suite = [] {
using namespace ut;

constexpr uint16_t features = Ark::DefaultFeatures | Ark::FeatureVMDebugger;

iterTestFiles(
"DebuggerSuite",
[](TestData&& data) {
std::stringstream os;
Ark::State state({ lib_path });

// cppcheck-suppress constParameterReference
state.loadFunction("prn", [&os](std::vector<Ark::Value>& args, Ark::VM* vm) -> Ark::Value {
for (const auto& value : args)
fmt::print(os, "{}", value.toString(*vm));
fmt::println(os, "");
return Ark::Nil;
});

should("compile without error for " + data.stem) = [&] {
expect(mut(state).doFile(data.path, features));
};

should("launch the debugger and compute expressions for " + data.stem) = [&] {
std::filesystem::path prompt_path(data.path);
prompt_path.replace_extension("prompt");

try
{
Ark::VM vm(state);
vm.usePromptFileForDebugger(prompt_path.generic_string(), os);
vm.run(/* fail_with_exception= */ true);

const std::string output = sanitizeOutput(os.str());
expectOrDiff(data.expected, output);
}
catch (const std::exception&)
{
expect(false);
}
};
});
};
Loading
Loading