ArkScript
A small, lisp-inspired, functional scripting language
Debugger.cpp
Go to the documentation of this file.
1#include <Ark/VM/Debugger.hpp>
2
3#include <fmt/core.h>
4#include <fmt/color.h>
5#include <fmt/ranges.h>
6#include <fmt/ostream.h>
7#include <chrono>
8#include <thread>
9#include <charconv>
10
11#include <Ark/State.hpp>
12#include <Ark/VM/VM.hpp>
13#include <Ark/Utils/Files.hpp>
17
18namespace Ark::internal
19{
20 Debugger::Debugger(const ExecutionContext& context, const std::vector<std::filesystem::path>& libenv, const std::vector<std::string>& symbols, const std::vector<Value>& constants) :
21 m_libenv(libenv),
22 m_symbols(symbols),
23 m_constants(constants),
24 m_os(std::cout),
25 m_colorize(true)
26 {
28 saveState(context);
29 }
30
31 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) :
32 m_libenv(libenv),
33 m_symbols(symbols),
34 m_constants(constants),
35 m_os(os),
36 m_colorize(false),
37 m_prompt_stream(std::make_unique<std::ifstream>(path_to_prompt_file))
38 {
40 }
41
43 {
44 m_states.emplace_back(
45 std::make_unique<SavedState>(
46 context.ip,
47 context.pp,
48 context.sp,
49 context.fc,
50 context.locals,
51 context.stacked_closure_scopes));
52 }
53
55 {
56 const auto& [ip, pp, sp, fc, locals, closure_scopes] = *m_states.back();
57 context.locals = locals;
58 context.stacked_closure_scopes = closure_scopes;
59 context.ip = ip;
60 context.pp = pp;
61 context.sp = sp;
62 context.fc = fc;
63
64 m_states.pop_back();
65 }
66
67 void Debugger::run(VM& vm, ExecutionContext& context, const bool from_breakpoint)
68 {
69 using namespace std::chrono_literals;
70
71 if (from_breakpoint)
72 showContext(vm, context);
73
74 m_running = true;
75 const bool is_vm_running = vm.m_running;
76 const std::size_t ip_at_breakpoint = context.ip,
77 pp_at_breakpoint = context.pp;
78 // create dedicated scope, so that we won't be overwriting existing variables
79 context.locals.emplace_back(context.scopes_storage.data(), context.locals.back().storageEnd());
80 std::size_t last_ip = 0;
81
82 while (true)
83 {
84 std::optional<std::string> maybe_input = prompt(ip_at_breakpoint, pp_at_breakpoint, vm, context);
85
86 if (maybe_input)
87 {
88 const std::string& line = maybe_input.value();
89
90 if (const auto compiled = compile(m_code + line, vm.m_state.m_pages.size()); compiled.has_value())
91 {
92 context.ip = last_ip;
93 context.pp = vm.m_state.m_pages.size();
94
95 vm.m_state.extendBytecode(compiled->pages, compiled->symbols, compiled->constants);
96
97 if (vm.safeRun(context) == 0)
98 {
99 // executing code worked
100 m_code += line + "\n";
101 // place ip to end of bytecode instruction (HALT)
102 last_ip = context.ip - 4;
103
104 const Value* maybe_value = vm.peekAndResolveAsPtr(context);
105 if (maybe_value != nullptr &&
106 maybe_value->valueType() != ValueType::Undefined &&
107 maybe_value->valueType() != ValueType::InstPtr &&
108 maybe_value->valueType() != ValueType::Garbage)
109 fmt::println(
110 m_os,
111 "{}",
112 fmt::styled(
113 maybe_value->toString(vm),
114 m_colorize ? fmt::fg(fmt::color::chocolate) : fmt::text_style()));
115 }
116 }
117 else
118 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
119 }
120 else
121 break;
122 }
123
124 context.locals.pop_back();
125
126 // we do not want to retain code from the past executions
127 m_code.clear();
128 m_line_count = 0;
129
130 // we hit a HALT instruction that set 'running' to false, ignore that if we were still running!
131 vm.m_running = is_vm_running;
132 m_running = false;
133 }
134
135 void Debugger::registerInstruction(const uint8_t inst, const uint8_t padding, const uint16_t arg, const std::size_t ip, const std::size_t pp) noexcept
136 {
137 // We don't want to register instructions from code entered in the debugger!
138 if (!m_running)
139 {
140 m_previous_insts.emplace_back(TracedInstruction { .inst = inst, .padding = padding, .arg = arg, .ip = ip, .pp = pp });
141 if (m_previous_insts.size() > 4096)
142 m_previous_insts.pop_front();
143 }
144 }
145
146 std::optional<Debugger::Command::Args_t> Debugger::Command::getArgs(const std::string& line, std::ostream& os) const
147 {
148 std::vector<std::string> split = Utils::splitString(line, ' ');
149 Args_t args_values = args;
150 std::size_t i = 0;
151 for (const auto& arg : std::ranges::views::drop(split, 1))
152 {
153 if (i < args.size())
154 args_values[i].second = arg;
155 else
156 {
157 fmt::println(os, "Too many arguments provided to {}, expected {}, got {}", split.front(), args.size(), split.size() - 1);
158 return std::nullopt;
159 }
160
161 ++i;
162 }
163
164 return args_values;
165 }
166
167 std::optional<std::size_t> Debugger::Command::argAsCount(const std::string& line, const std::size_t idx, std::ostream& os) const
168 {
169 const std::optional<Args_t> maybe_parsed = getArgs(line, os);
170 if (maybe_parsed && idx < maybe_parsed->size())
171 {
172 const std::string str = maybe_parsed.value()[idx].second;
173 std::size_t result = 0;
174 auto [ptr, ec] = std::from_chars(str.data(), str.data() + str.size(), result);
175
176 if (ec == std::errc())
177 return result;
178
179 fmt::println(os, "Couldn't parse argument as an unsigned integer");
180 return std::nullopt;
181 }
182 return std::nullopt;
183 }
184
186 {
187 m_commands = {
188 Command(
189 "help",
190 "display this message",
191 [this](const std::string&, const CommandArgs&) {
192 fmt::println(m_os, "Available commands:");
193 for (const Command& cmd : m_commands)
194 {
195 if (cmd.is_exact)
196 fmt::println(m_os, " {} -- {}", fmt::join(cmd.names, ", "), cmd.description);
197 else
198 {
199 const auto v = std::views::transform(cmd.args, [](const auto& p) {
200 return fmt::format("{}={}", p.first, p.second);
201 });
202 fmt::println(m_os, " {} <{}> -- {}", fmt::join(cmd.names, ", "), fmt::join(v, ", "), cmd.description);
203 }
204 }
205 return false;
206 }),
207 Command(
208 { "c", "continue" },
209 "resume execution",
210 [this](const std::string&, const CommandArgs&) {
211 fmt::println(m_os, "dbg: continue");
212 return true;
213 }),
214 Command(
215 { "q", "quit" },
216 "quit the debugger, stopping the script execution",
217 [this](const std::string&, const CommandArgs&) {
218 fmt::println(m_os, "dbg: stop");
219 m_quit_vm = true;
220 return true;
221 }),
222 Command(
223 StartsWith("stack"),
224 { { "n", "5" } },
225 "show the last n values on the stack",
226 [this](const std::string& line, const CommandArgs& args) {
227 if (const auto arg = args.me.argAsCount(line, 0, m_os))
228 showStack(*args.vm_ptr, *args.ctx_ptr, arg.value());
229 return false;
230 }),
231 Command(
232 StartsWith("locals"),
233 { { "n", "5" } },
234 "show the last n values on the locals' stack",
235 [this](const std::string& line, const CommandArgs& args) {
236 if (const auto arg = args.me.argAsCount(line, 0, m_os))
237 showLocals(*args.vm_ptr, *args.ctx_ptr, arg.value());
238 return false;
239 }),
240 Command(
241 StartsWith("scopes"),
242 { { "n", "5" } },
243 "show the last n scopes",
244 [this](const std::string& line, const CommandArgs& args) {
245 if (const auto arg = args.me.argAsCount(line, 0, m_os))
246 showScopes(*args.vm_ptr, *args.ctx_ptr, arg.value());
247 return false;
248 }),
249 Command(
250 "ptr",
251 "show the values of the VM pointers",
252 [this](const std::string&, const CommandArgs& args) {
253 fmt::println(
254 m_os,
255 "IP: {} - PP: {} - SP: {}",
256 fmt::styled(args.ip / 4, m_colorize ? fmt::fg(fmt::color::cyan) : fmt::text_style()),
257 fmt::styled(args.pp, m_colorize ? fmt::fg(fmt::color::green) : fmt::text_style()),
258 fmt::styled(args.ctx_ptr->sp, m_colorize ? fmt::fg(fmt::color::yellow) : fmt::text_style()));
259 return false;
260 }),
261 Command(
262 StartsWith("trace"),
263 { { "n", "10" } },
264 "show the last n executed instructions",
265 [this](const std::string& line, const CommandArgs& args) {
266 if (const auto arg = args.me.argAsCount(line, 0, m_os))
267 showPreviousInstructions(*args.vm_ptr, arg.value());
268 return false;
269 }),
270 };
271 }
272
273 std::optional<Debugger::Command> Debugger::matchCommand(const std::string& line) const
274 {
275 for (const Command& c : m_commands)
276 {
277 if (c.is_exact)
278 {
279 if (std::ranges::find(c.names, line) != c.names.end())
280 return c;
281 }
282 else
283 {
284 if (std::ranges::find_if(c.names, [&line](const std::string& name) -> bool {
285 return line.starts_with(name);
286 }) != c.names.end())
287 return c;
288 }
289 }
290
291 return std::nullopt;
292 }
293
294 void Debugger::showContext(const VM& vm, const ExecutionContext& context) const
295 {
296 // show the line where the breakpoint hit
297 const auto maybe_source_loc = vm.findSourceLocation(context.ip, context.pp);
298 if (maybe_source_loc)
299 {
300 const auto filename = vm.m_state.m_filenames[maybe_source_loc->filename_id];
301
302 if (Utils::fileExists(filename))
303 {
304 fmt::println(m_os, "");
307 .filename = filename,
308 .start = FilePos { .line = maybe_source_loc->line, .column = 0 },
309 .end = std::nullopt,
310 .maybe_content = std::nullopt },
311 m_os,
312 /* maybe_context= */ std::nullopt,
313 /* colorize= */ m_colorize);
314 fmt::println(m_os, "");
315 }
316 }
317 }
318
319 void Debugger::showStack(VM& vm, const ExecutionContext& context, const std::size_t count) const
320 {
321 std::size_t i = 1;
322 do
323 {
324 if (context.sp < i)
325 break;
326
327 const auto color = m_colorize ? fmt::fg(i % 2 == 0 ? fmt::color::forest_green : fmt::color::cornflower_blue) : fmt::text_style();
328 fmt::println(
329 m_os,
330 "{} -> {}",
331 fmt::styled(context.sp - i, color),
332 fmt::styled(context.stack[context.sp - i].toString(vm, /* show_as_code= */ true), color));
333 ++i;
334 } while (i < count);
335
336 if (context.sp == 0)
337 fmt::println(m_os, "Stack is empty");
338
339 fmt::println(m_os, "");
340 }
341
342 void Debugger::showLocals(VM& vm, ExecutionContext& context, const std::size_t count) const
343 {
344 const std::size_t limit = context.locals[context.locals.size() - 2].size(); // -2 because we created a scope for the debugger
345 if (limit > 0 && count > 0)
346 {
347 fmt::println(m_os, "scope size: {}", limit);
348 showLocals(context.locals[context.locals.size() - 2], vm, count);
349 }
350 else
351 fmt::println(m_os, "Current scope is empty");
352
353 fmt::println(m_os, "");
354 }
355
356 // cppcheck-suppress constParameterReference
357 void Debugger::showScopes(VM& vm, ExecutionContext& context, const std::size_t count) const
358 {
359 if (count == 0)
360 fmt::println(m_os, "Nothing to show, count must be > 0");
361 else
362 {
363 const std::size_t scopes_count = context.locals.size() - 1;
364 fmt::println(m_os, "There are {} scope{}\n", scopes_count, scopes_count == 1 ? "" : "s");
365
366 std::size_t i = 0;
367
368 do
369 {
370 if (scopes_count <= i)
371 break;
372
373 // `i` is in [0, scopes_count[, so scopes_count - max(i) == 0, we can safely subtract 1
374 const std::size_t idx = scopes_count - i - 1;
375 const auto& scope = context.locals[idx];
376
377 fmt::println(m_os, "Scope {}, size: {}", idx, scope.size());
378 if (scope.size() > 0)
379 showLocals(scope, vm);
380 fmt::println("");
381
382 ++i;
383 } while (i < count);
384 }
385
386 fmt::println(m_os, "");
387 }
388
389 void Debugger::showLocals(const ScopeView& scope, VM& vm, std::optional<std::size_t> limit) const
390 {
391 fmt::println(m_os, "index | id | name | type | value");
392 std::size_t i = 0;
393
394 do
395 {
396 if (scope.size() <= i)
397 break;
398
399 auto& [id, value] = scope.atPosReverse(i);
400 const auto color = m_colorize ? fmt::fg(i % 2 == 0 ? fmt::color::forest_green : fmt::color::cornflower_blue) : fmt::text_style();
401
402 fmt::println(
403 m_os,
404 "{:>5} | {:3} | {:14} | {:>9} | {}",
405 fmt::styled(scope.size() - i - 1, color),
406 fmt::styled(id, color),
407 fmt::styled(vm.m_state.m_symbols[id], color),
408 fmt::styled(std::to_string(value.valueType()), color),
409 fmt::styled(value.toString(vm, /* show_as_code= */ true), color));
410 ++i;
411 } while (!limit.has_value() || i < limit.value());
412 }
413
414 void Debugger::showPreviousInstructions(const VM& vm, const std::size_t count) const
415 {
416 BytecodeReader bcr;
417 bcr.feed(vm.bytecode());
418
419 const auto syms = bcr.symbols();
420 const auto vals = bcr.values(syms);
421
422 if (count > 0 && !m_previous_insts.empty())
423 fmt::println(m_os, " PP, IP");
424
425 for (std::size_t i = 0; i < count; ++i)
426 {
427 if (i >= m_previous_insts.size())
428 break;
429
430 const auto& [inst, padding, arg, ip, pp] = m_previous_insts[m_previous_insts.size() - 1 - i];
431 fmt::print(m_os, "{:>3},{:>3} ", pp, ip);
432 bcr.printInstruction(m_os, inst, padding, arg, syms, vals, m_colorize);
433 }
434 }
435
436 std::optional<std::string> Debugger::prompt(const std::size_t ip, const std::size_t pp, VM& vm, ExecutionContext& context)
437 {
438 std::string code;
439 long open_parens = 0;
440 long open_braces = 0;
441
442 while (true)
443 {
444 const bool unfinished_block = open_parens != 0 || open_braces != 0;
445 fmt::print(
446 m_os,
447 "dbg[{},{}]:{:0>3}{} ",
448 fmt::format("pp:{}", fmt::styled(pp, m_colorize ? fmt::fg(fmt::color::green) : fmt::text_style())),
449 fmt::format("ip:{}", fmt::styled(ip / 4, m_colorize ? fmt::fg(fmt::color::cyan) : fmt::text_style())),
451 unfinished_block ? ":" : ">");
452
453 std::string line;
454 if (m_prompt_stream)
455 {
456 std::getline(*m_prompt_stream, line);
457 fmt::println(m_os, "{}", line); // because nothing is printed otherwise, and prompts get printed on the same line
458 }
459 else
460 std::getline(std::cin, line);
461
463
464 if (line.empty() && !unfinished_block)
465 {
466 fmt::println(m_os, "dbg: continue");
467 return std::nullopt;
468 }
469
470 if (const auto& maybe_cmd = matchCommand(line))
471 {
472 const Command cmd = maybe_cmd.value();
473 if (cmd.action(line, CommandArgs { .vm_ptr = &vm, .ctx_ptr = &context, .ip = ip, .pp = pp, .me = cmd }))
474 return std::nullopt;
475 }
476 else
477 {
478 code += line + "\n";
479
480 open_parens += Utils::countOpenEnclosures(line, '(', ')');
481 open_braces += Utils::countOpenEnclosures(line, '{', '}');
482
483 ++m_line_count;
484 if (open_braces == 0 && open_parens == 0)
485 break;
486 }
487 }
488
489 return code;
490 }
491
492 std::optional<CompiledPrompt> Debugger::compile(const std::string& code, const std::size_t start_page_at_offset) const
493 {
494 Welder welder(0, m_libenv, DefaultFeatures);
496 return std::nullopt;
497 if (!welder.generateBytecodeUsingTables(m_symbols, m_constants, start_page_at_offset))
498 return std::nullopt;
499
500 BytecodeReader bcr;
501 bcr.feed(welder.bytecode());
502 const auto syms = bcr.symbols();
503 const auto vals = bcr.values(syms);
504 const auto files = bcr.filenames(vals);
505 const auto inst_locs = bcr.instLocations(files);
506 const auto [pages, _] = bcr.code(inst_locs);
507
508 return std::optional(CompiledPrompt(pages, syms.symbols, vals.values));
509 }
510}
A bytecode disassembler for ArkScript.
Debugger used by the VM when an error or a breakpoint is reached.
Tools to report code errors nicely to the user.
Lots of utilities about the filesystem.
State used by the virtual machine: it loads the bytecode, can compile it if needed,...
The ArkScript virtual machine.
In charge of welding everything needed to compile code.
This class is just a helper to.
Symbols symbols() const
Filenames filenames(const Values &values) const
InstLocations instLocations(const Filenames &filenames) const
Code code(const InstLocations &instLocations) const
Values values(const Symbols &symbols) const
void printInstruction(std::ostream &os, uint8_t inst, uint8_t padding, uint16_t imm_arg, const Symbols &syms, const Values &vals, bool colorize=true) const
void feed(const std::string &file)
Construct needed data before displaying information about a given file.
std::vector< std::string > m_filenames
Definition State.hpp:172
std::vector< std::string > m_symbols
Definition State.hpp:170
std::vector< bytecode_t > m_pages
Definition State.hpp:174
void extendBytecode(const std::vector< bytecode_t > &pages, const std::vector< std::string > &symbols, const std::vector< Value > &constants)
Used by the debugger to add code to the VM at runtime.
Definition State.cpp:247
The ArkScript virtual machine, executing ArkScript bytecode.
Definition VM.hpp:40
ARK_ALWAYS_INLINE Value * peekAndResolveAsPtr(internal::ExecutionContext &context, std::size_t offset=0)
Return a pointer to the top of the stack without consuming it, and resolve it if possible.
int safeRun(internal::ExecutionContext &context, std::size_t untilFrameCount=0, bool fail_with_exception=false)
Run ArkScript bytecode inside a try catch to retrieve all the exceptions and display a stack trace if...
Definition VM.cpp:401
bool m_running
Definition VM.hpp:176
const bytecode_t & bytecode() const
Definition VM.hpp:162
State & m_state
Definition VM.hpp:173
std::optional< internal::InstLoc > findSourceLocation(std::size_t ip, std::size_t pp) const
Find the nearest source location information given instruction and page pointers.
Definition VM.cpp:2190
ValueType valueType() const noexcept
Definition Value.hpp:154
std::string toString(VM &vm, bool show_as_code=false) const noexcept
Definition Value.cpp:81
The welder joins all the compiler passes.
Definition Welder.hpp:40
bool generateBytecodeUsingTables(const std::vector< std::string > &symbols, const std::vector< Value > &constants, std::size_t start_page_at_offset)
Compile the AST processed by computeASTFromFile / computeASTFromString, with prefilled symbols and co...
Definition Welder.cpp:100
const bytecode_t & bytecode() const noexcept
Definition Welder.cpp:170
bool computeASTFromStringWithKnownSymbols(const std::string &code, const std::vector< std::string > &symbols)
Compile code from a string, with a set of known symbols (useful for the debugger)
Definition Welder.cpp:54
void run(VM &vm, ExecutionContext &context, bool from_breakpoint)
Start the debugger shell.
Definition Debugger.cpp:67
std::string m_code
Code added while inside the debugger.
Definition Debugger.hpp:176
std::vector< std::unique_ptr< SavedState > > m_states
Definition Debugger.hpp:164
void showStack(VM &vm, const ExecutionContext &context, std::size_t count) const
Definition Debugger.cpp:319
std::optional< Command > matchCommand(const std::string &line) const
Definition Debugger.cpp:273
void showLocals(VM &vm, ExecutionContext &context, std::size_t count) const
Definition Debugger.cpp:342
std::deque< TracedInstruction > m_previous_insts
Definition Debugger.hpp:171
std::optional< std::string > prompt(std::size_t ip, std::size_t pp, VM &vm, ExecutionContext &context)
Definition Debugger.cpp:436
void resetContextToSavedState(ExecutionContext &context)
Reset a VM context to the last state saved by the debugger.
Definition Debugger.cpp:54
Debugger(const ExecutionContext &context, const std::vector< std::filesystem::path > &libenv, const std::vector< std::string > &symbols, const std::vector< Value > &constants)
Create a new Debugger object.
Definition Debugger.cpp:20
void registerInstruction(uint8_t inst, uint8_t padding, uint16_t arg, std::size_t ip, std::size_t pp) noexcept
Definition Debugger.cpp:135
std::unique_ptr< std::istream > m_prompt_stream
Definition Debugger.hpp:175
std::ostream & m_os
Definition Debugger.hpp:173
std::vector< std::string > m_symbols
Definition Debugger.hpp:166
void showScopes(VM &vm, ExecutionContext &context, std::size_t count) const
Definition Debugger.cpp:357
std::vector< Command > m_commands
Definition Debugger.hpp:162
void showContext(const VM &vm, const ExecutionContext &context) const
Definition Debugger.cpp:294
void saveState(const ExecutionContext &context)
Save the current VM state, to get back to it once the debugger is done running.
Definition Debugger.cpp:42
std::vector< std::filesystem::path > m_libenv
Definition Debugger.hpp:165
std::vector< Value > m_constants
Definition Debugger.hpp:167
void showPreviousInstructions(const VM &vm, std::size_t count) const
Definition Debugger.cpp:414
std::optional< CompiledPrompt > compile(const std::string &code, std::size_t start_page_at_offset) const
Take care of compiling new code using the existing data tables.
Definition Debugger.cpp:492
A class to handle the VM scope more efficiently.
Definition ScopeView.hpp:27
ARK_ALWAYS_INLINE pair_t & atPosReverse(const std::size_t i) const noexcept
Return the element at index, starting from the end.
ARK_ALWAYS_INLINE std::size_t size() const noexcept
Return the size of the scope.
ARK_API void makeContext(const ErrorLocation &loc, std::ostream &os, const std::optional< CodeErrorContext > &maybe_context, bool colorize)
Helper to create a colorized context to report errors to the user.
bool fileExists(const std::string &name) noexcept
Checks if a file exists.
Definition Files.hpp:28
ARK_API long countOpenEnclosures(const std::string &line, char open, char close)
Count the open enclosure and its counterpart: (), {}, [].
Definition Utils.cpp:35
std::vector< std::string > splitString(const std::string &source, const char sep)
Cut a string into pieces, given a character separator.
Definition Utils.hpp:31
ARK_API void trimWhitespace(std::string &line)
Remove whitespaces at the start and end of a string.
Definition Utils.cpp:40
constexpr uint16_t DefaultFeatures
Definition Constants.hpp:74
@ Garbage
Used to signal a value was used and can/should be collected and removed from the stack.
STL namespace.
std::string to_string(const Ark::ValueType type) noexcept
Definition Value.hpp:235
std::vector< std::pair< std::string, std::string > > Args_t
Definition Debugger.hpp:137
std::optional< std::size_t > argAsCount(const std::string &line, std::size_t idx, std::ostream &os) const
Definition Debugger.cpp:167
std::optional< Args_t > getArgs(const std::string &line, std::ostream &os) const
Definition Debugger.cpp:146
std::array< ScopeView::pair_t, ScopeStackSize > scopes_storage
All the ScopeView use this array to store id->value.
std::vector< std::shared_ptr< ClosureScope > > stacked_closure_scopes
Stack the closure scopes to keep the closure alive as long as we are calling them.
std::vector< ScopeView > locals
std::array< Value, VMStackSizeWithOverflowBuffer > stack
std::size_t ip
Instruction pointer.