Skip to content

Commit 617c765

Browse files
committed
feat(debugger): adding a way to use a prompt file for the debugger, instead of requiring the user to provide an input
1 parent ef353a2 commit 617c765

21 files changed

+399
-60
lines changed

include/Ark/Constants.hpp.in

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ namespace Ark
5353
// Compiler options
5454
constexpr uint16_t FeatureImportSolver = 1 << 0;
5555
constexpr uint16_t FeatureMacroProcessor = 1 << 1;
56-
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
56+
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
5757
constexpr uint16_t FeatureIROptimizer = 1 << 3;
5858
constexpr uint16_t FeatureNameResolver = 1 << 4;
59-
constexpr uint16_t FeatureVMDebugger = 1 << 5; ///< This is disabled so that embedding ArkScript does not launch the debugger on every error when running code
59+
constexpr uint16_t FeatureVMDebugger = 1 << 5; ///< Disabled by default because embedding ArkScript should not launch the debugger on every error when running code
6060

6161
constexpr uint16_t FeatureDumpIR = 1 << 14;
6262
/// This feature should only be used in tests, to disable diagnostics generation and enable exceptions to be thrown

include/Ark/VM/Debugger.hpp

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ namespace Ark::internal
3737
std::vector<std::shared_ptr<ClosureScope>> closure_scopes;
3838
};
3939

40+
struct CompiledPrompt
41+
{
42+
std::vector<bytecode_t> pages;
43+
std::vector<std::string> symbols;
44+
std::vector<Value> constants;
45+
};
46+
4047
class Debugger
4148
{
4249
public:
@@ -48,7 +55,18 @@ namespace Ark::internal
4855
* @param symbols symbols table of the VM
4956
* @param constants constants table of the VM
5057
*/
51-
explicit Debugger(const ExecutionContext& context, const std::vector<std::filesystem::path>& libenv, const std::vector<std::string>& symbols, const std::vector<Value>& constants);
58+
Debugger(const ExecutionContext& context, const std::vector<std::filesystem::path>& libenv, const std::vector<std::string>& symbols, const std::vector<Value>& constants);
59+
60+
/**
61+
* @brief Create a new Debugger object that will use lines from a file as prompts, instead of waiting for user inputs
62+
*
63+
* @param libenv
64+
* @param path_to_prompt_file
65+
* @param os output stream
66+
* @param symbols symbols table of the VM
67+
* @param constants constants table of the VM
68+
*/
69+
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);
5270

5371
/**
5472
* @brief Save the current VM state, to get back to it once the debugger is done running
@@ -87,22 +105,27 @@ namespace Ark::internal
87105
std::vector<std::filesystem::path> m_libenv;
88106
std::vector<std::string> m_symbols;
89107
std::vector<Value> m_constants;
90-
bool m_running;
91-
bool m_quit_vm;
108+
bool m_running { false };
109+
bool m_quit_vm { false };
92110

111+
std::ostream& m_os;
112+
bool m_colorize;
113+
std::unique_ptr<std::istream> m_prompt_stream;
93114
std::string m_code; ///< Code added while inside the debugger
94-
std::size_t m_line_count = 0;
115+
std::size_t m_line_count { 0 };
116+
117+
void showContext(const VM& vm, const ExecutionContext& context) const;
95118

96-
std::optional<std::string> prompt();
119+
std::optional<std::string> prompt(std::size_t ip, std::size_t pp);
97120

98121
/**
99122
* @brief Take care of compiling new code using the existing data tables
100123
*
101124
* @param code
102125
* @param start_page_at_offset offset to start the new pages at
103-
* @return std::optional<std::vector<bytecode_t>> optional set of bytecode pages if compilation succeeded
126+
* @return std::optional<CompiledPrompt> optional set of bytecode pages, symbols and constants if compilation succeeded
104127
*/
105-
std::optional<std::vector<bytecode_t>> compile(const std::string& code, std::size_t start_page_at_offset);
128+
[[nodiscard]] std::optional<CompiledPrompt> compile(const std::string& code, std::size_t start_page_at_offset) const;
106129
};
107130
}
108131

include/Ark/VM/VM.hpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,14 @@ namespace Ark
151151
*/
152152
[[nodiscard]] bool forceReloadPlugins() const;
153153

154+
/**
155+
* @brief Configure the debugger to use a prompt file instead of asking the user for an input
156+
*
157+
* @param path path to prompt file (one prompt per line)
158+
* @param os output stream
159+
*/
160+
void usePromptFileForDebugger(const std::string& path, std::ostream& os = std::cout);
161+
154162
/**
155163
* @brief Throw a VM error message
156164
*

src/arkreactor/Compiler/Welder.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ namespace Ark
5555
m_root_file = std::filesystem::current_path(); // No filename given, take the current working directory
5656

5757
for (const std::string& sym : symbols)
58-
m_name_resolver.addDefinedSymbol(sym, /* is_mutable= */ false);
58+
m_name_resolver.addDefinedSymbol(sym, /* is_mutable= */ true);
5959
return computeAST(ARK_NO_NAME_FILE, code);
6060
}
6161

src/arkreactor/VM/Debugger.cpp

Lines changed: 81 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#include <fmt/core.h>
44
#include <fmt/color.h>
5+
#include <fmt/ostream.h>
56

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

22+
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) :
23+
m_libenv(libenv), m_symbols(symbols), m_constants(constants), m_os(os), m_colorize(false)
24+
{
25+
m_prompt_stream = std::make_unique<std::ifstream>(path_to_prompt_file);
26+
}
27+
2128
void Debugger::saveState(const ExecutionContext& context)
2229
{
2330
m_states.emplace_back(
@@ -45,72 +52,89 @@ namespace Ark::internal
4552

4653
void Debugger::run(VM& vm, ExecutionContext& context)
4754
{
55+
showContext(vm, context);
56+
4857
m_running = true;
4958
const bool is_vm_running = vm.m_running;
50-
51-
// show the line where the breakpoint hit
52-
const auto maybe_source_loc = vm.findSourceLocation(context.ip, context.pp);
53-
if (maybe_source_loc)
54-
{
55-
const auto filename = vm.m_state.m_filenames[maybe_source_loc->filename_id];
56-
57-
if (Utils::fileExists(filename))
58-
{
59-
fmt::println("");
60-
Diagnostics::makeContext(
61-
Diagnostics::ErrorLocation {
62-
.filename = filename,
63-
.start = FilePos { .line = maybe_source_loc->line, .column = 0 },
64-
.end = std::nullopt },
65-
std::cout,
66-
/* maybe_context= */ std::nullopt,
67-
/* colorize= */ true);
68-
fmt::println("");
69-
}
70-
}
59+
const std::size_t ip_at_breakpoint = context.ip,
60+
pp_at_breakpoint = context.pp;
61+
// create dedicated scope, so that we won't be overwriting existing variables
62+
context.locals.emplace_back(context.scopes_storage.data(), context.locals.back().storageEnd());
63+
std::size_t last_ip = 0;
7164

7265
while (true)
7366
{
74-
std::optional<std::string> maybe_input = prompt();
67+
std::optional<std::string> maybe_input = prompt(ip_at_breakpoint, pp_at_breakpoint);
7568

7669
if (maybe_input)
7770
{
7871
const std::string& line = maybe_input.value();
7972

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

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

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

9487
const Value* maybe_value = vm.peekAndResolveAsPtr(context);
9588
if (maybe_value != nullptr && maybe_value->valueType() != ValueType::Undefined && maybe_value->valueType() != ValueType::InstPtr)
96-
fmt::println("{}", fmt::styled(maybe_value->toString(vm), fmt::fg(fmt::color::chocolate)));
89+
fmt::println(
90+
m_os,
91+
"{}",
92+
fmt::styled(
93+
maybe_value->toString(vm),
94+
m_colorize ? fmt::fg(fmt::color::chocolate) : fmt::text_style()));
9795
}
98-
99-
context.locals.pop_back();
10096
}
10197
}
10298
else
10399
break;
104100
}
105101

106-
m_running = false;
102+
context.locals.pop_back();
103+
107104
// we do not want to retain code from the past executions
108105
m_code.clear();
106+
m_line_count = 0;
107+
109108
// we hit a HALT instruction that set 'running' to false, ignore that if we were still running!
110109
vm.m_running = is_vm_running;
110+
m_running = false;
111111
}
112112

113-
std::optional<std::string> Debugger::prompt()
113+
void Debugger::showContext(const VM& vm, const ExecutionContext& context) const
114+
{
115+
// show the line where the breakpoint hit
116+
const auto maybe_source_loc = vm.findSourceLocation(context.ip, context.pp);
117+
if (maybe_source_loc)
118+
{
119+
const auto filename = vm.m_state.m_filenames[maybe_source_loc->filename_id];
120+
121+
if (Utils::fileExists(filename))
122+
{
123+
fmt::println(m_os, "");
124+
Diagnostics::makeContext(
125+
Diagnostics::ErrorLocation {
126+
.filename = filename,
127+
.start = FilePos { .line = maybe_source_loc->line, .column = 0 },
128+
.end = std::nullopt },
129+
m_os,
130+
/* maybe_context= */ std::nullopt,
131+
/* colorize= */ m_colorize);
132+
fmt::println(m_os, "");
133+
}
134+
}
135+
}
136+
137+
std::optional<std::string> Debugger::prompt(const std::size_t ip, const std::size_t pp)
114138
{
115139
std::string code;
116140
long open_parens = 0;
@@ -119,29 +143,42 @@ namespace Ark::internal
119143
while (true)
120144
{
121145
const bool unfinished_block = open_parens != 0 || open_braces != 0;
122-
fmt::print("dbg:{:0>3}{} ", m_line_count, unfinished_block ? ":" : ">");
146+
fmt::print(
147+
m_os,
148+
"dbg[{},{}]:{:0>3}{} ",
149+
fmt::format("pp:{}", fmt::styled(pp, m_colorize ? fmt::fg(fmt::color::green) : fmt::text_style())),
150+
fmt::format("ip:{}", fmt::styled(ip, m_colorize ? fmt::fg(fmt::color::cyan) : fmt::text_style())),
151+
m_line_count,
152+
unfinished_block ? ":" : ">");
153+
123154
std::string line;
124-
std::getline(std::cin, line);
155+
if (m_prompt_stream)
156+
{
157+
std::getline(*m_prompt_stream, line);
158+
fmt::println(m_os, "{}", line); // because nothing is printed otherwise, and prompts get printed on the same line
159+
}
160+
else
161+
std::getline(std::cin, line);
125162

126163
Utils::trimWhitespace(line);
127164

128165
if (line == "c" || line == "continue" || line.empty())
129166
{
130-
fmt::println("dbg: continue");
167+
fmt::println(m_os, "dbg: continue");
131168
return std::nullopt;
132169
}
133170
else if (line == "q" || line == "quit")
134171
{
135-
fmt::println("dbg: stop");
172+
fmt::println(m_os, "dbg: stop");
136173
m_quit_vm = true;
137174
return std::nullopt;
138175
}
139176
else if (line == "help")
140177
{
141-
fmt::println("Available commands:");
142-
fmt::println(" help -- display this message");
143-
fmt::println(" c, continue -- resume execution");
144-
fmt::println(" q, quit -- quit the debugger, stopping the script execution");
178+
fmt::println(m_os, "Available commands:");
179+
fmt::println(m_os, " help -- display this message");
180+
fmt::println(m_os, " c, continue -- resume execution");
181+
fmt::println(m_os, " q, quit -- quit the debugger, stopping the script execution");
145182
}
146183
else
147184
{
@@ -159,7 +196,7 @@ namespace Ark::internal
159196
return code;
160197
}
161198

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

178-
m_symbols = syms.symbols;
179-
m_constants = vals.values;
180-
181-
return pages;
215+
return std::optional(CompiledPrompt(pages, syms.symbols, vals.values));
182216
}
183217
}

src/arkreactor/VM/VM.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,11 @@ namespace Ark
510510
}
511511
}
512512

513+
void VM::usePromptFileForDebugger(const std::string& path, std::ostream& os)
514+
{
515+
m_debugger = std::make_unique<Debugger>(m_state.m_libenv, path, os, m_state.m_symbols, m_state.m_constants);
516+
}
517+
513518
void VM::throwVMError(ErrorKind kind, const std::string& message)
514519
{
515520
throw std::runtime_error(std::string(errorKinds[static_cast<std::size_t>(kind)]) + ": " + message + "\n");
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#include <boost/ut.hpp>
2+
3+
#include <fmt/ostream.h>
4+
5+
#include <Ark/Ark.hpp>
6+
#include <TestsHelper.hpp>
7+
8+
using namespace boost;
9+
10+
ut::suite<"Debugger"> debugger_suite = [] {
11+
using namespace ut;
12+
13+
constexpr uint16_t features = Ark::DefaultFeatures | Ark::FeatureVMDebugger;
14+
15+
iterTestFiles(
16+
"DebuggerSuite",
17+
[](TestData&& data) {
18+
std::stringstream os;
19+
Ark::State state({ lib_path });
20+
21+
// cppcheck-suppress constParameterReference
22+
state.loadFunction("prn", [&os](std::vector<Ark::Value>& args, Ark::VM* vm) -> Ark::Value {
23+
for (const auto& value : args)
24+
fmt::print(os, "{}", value.toString(*vm));
25+
fmt::println(os, "");
26+
return Ark::Nil;
27+
});
28+
29+
should("compile without error for " + data.stem) = [&] {
30+
expect(mut(state).doFile(data.path, features));
31+
};
32+
33+
should("launch the debugger and compute expressions for " + data.stem) = [&] {
34+
std::filesystem::path prompt_path(data.path);
35+
prompt_path.replace_extension("prompt");
36+
37+
try
38+
{
39+
Ark::VM vm(state);
40+
vm.usePromptFileForDebugger(prompt_path.generic_string(), os);
41+
vm.run(/* fail_with_exception= */ true);
42+
43+
const std::string output = sanitizeOutput(os.str());
44+
expectOrDiff(data.expected, output);
45+
}
46+
catch (const std::exception&)
47+
{
48+
expect(false);
49+
}
50+
};
51+
});
52+
};

0 commit comments

Comments
 (0)