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/ostream.h>
6#include <chrono>
7#include <thread>
8#include <charconv>
9
10#include <Ark/State.hpp>
11#include <Ark/VM/VM.hpp>
12#include <Ark/Utils/Files.hpp>
16
17namespace Ark::internal
18{
19 Debugger::Debugger(const ExecutionContext& context, const std::vector<std::filesystem::path>& libenv, const std::vector<std::string>& symbols, const std::vector<Value>& constants) :
20 m_libenv(libenv),
21 m_symbols(symbols),
22 m_constants(constants),
23 m_os(std::cout),
24 m_colorize(true)
25 {
26 saveState(context);
27 }
28
29 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) :
30 m_libenv(libenv),
31 m_symbols(symbols),
32 m_constants(constants),
33 m_os(os),
34 m_colorize(false),
35 m_prompt_stream(std::make_unique<std::ifstream>(path_to_prompt_file))
36 {}
37
39 {
40 m_states.emplace_back(
41 std::make_unique<SavedState>(
42 context.ip,
43 context.pp,
44 context.sp,
45 context.fc,
46 context.locals,
47 context.stacked_closure_scopes));
48 }
49
51 {
52 const auto& [ip, pp, sp, fc, locals, closure_scopes] = *m_states.back();
53 context.locals = locals;
54 context.stacked_closure_scopes = closure_scopes;
55 context.ip = ip;
56 context.pp = pp;
57 context.sp = sp;
58 context.fc = fc;
59
60 m_states.pop_back();
61 }
62
63 void Debugger::run(VM& vm, ExecutionContext& context, const bool from_breakpoint)
64 {
65 using namespace std::chrono_literals;
66
67 if (from_breakpoint)
68 showContext(vm, context);
69
70 m_running = true;
71 const bool is_vm_running = vm.m_running;
72 const std::size_t ip_at_breakpoint = context.ip,
73 pp_at_breakpoint = context.pp;
74 // create dedicated scope, so that we won't be overwriting existing variables
75 context.locals.emplace_back(context.scopes_storage.data(), context.locals.back().storageEnd());
76 std::size_t last_ip = 0;
77
78 while (true)
79 {
80 std::optional<std::string> maybe_input = prompt(ip_at_breakpoint, pp_at_breakpoint, vm, context);
81
82 if (maybe_input)
83 {
84 const std::string& line = maybe_input.value();
85
86 if (const auto compiled = compile(m_code + line, vm.m_state.m_pages.size()); compiled.has_value())
87 {
88 context.ip = last_ip;
89 context.pp = vm.m_state.m_pages.size();
90
91 vm.m_state.extendBytecode(compiled->pages, compiled->symbols, compiled->constants);
92
93 if (vm.safeRun(context) == 0)
94 {
95 // executing code worked
96 m_code += line + "\n";
97 // place ip to end of bytecode instruction (HALT)
98 last_ip = context.ip - 4;
99
100 const Value* maybe_value = vm.peekAndResolveAsPtr(context);
101 if (maybe_value != nullptr &&
102 maybe_value->valueType() != ValueType::Undefined &&
103 maybe_value->valueType() != ValueType::InstPtr &&
104 maybe_value->valueType() != ValueType::Garbage)
105 fmt::println(
106 m_os,
107 "{}",
108 fmt::styled(
109 maybe_value->toString(vm),
110 m_colorize ? fmt::fg(fmt::color::chocolate) : fmt::text_style()));
111 }
112 }
113 else
114 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
115 }
116 else
117 break;
118 }
119
120 context.locals.pop_back();
121
122 // we do not want to retain code from the past executions
123 m_code.clear();
124 m_line_count = 0;
125
126 // we hit a HALT instruction that set 'running' to false, ignore that if we were still running!
127 vm.m_running = is_vm_running;
128 m_running = false;
129 }
130
131 void Debugger::showContext(const VM& vm, const ExecutionContext& context) const
132 {
133 // show the line where the breakpoint hit
134 const auto maybe_source_loc = vm.findSourceLocation(context.ip, context.pp);
135 if (maybe_source_loc)
136 {
137 const auto filename = vm.m_state.m_filenames[maybe_source_loc->filename_id];
138
139 if (Utils::fileExists(filename))
140 {
141 fmt::println(m_os, "");
144 .filename = filename,
145 .start = FilePos { .line = maybe_source_loc->line, .column = 0 },
146 .end = std::nullopt,
147 .maybe_content = std::nullopt },
148 m_os,
149 /* maybe_context= */ std::nullopt,
150 /* colorize= */ m_colorize);
151 fmt::println(m_os, "");
152 }
153 }
154 }
155
156 void Debugger::showStack(VM& vm, const ExecutionContext& context, const std::size_t count) const
157 {
158 std::size_t i = 1;
159 do
160 {
161 if (context.sp < i)
162 break;
163
164 const auto color = m_colorize ? fmt::fg(i % 2 == 0 ? fmt::color::forest_green : fmt::color::cornflower_blue) : fmt::text_style();
165 fmt::println(
166 m_os,
167 "{} -> {}",
168 fmt::styled(context.sp - i, color),
169 fmt::styled(context.stack[context.sp - i].toString(vm, /* show_as_code= */ true), color));
170 ++i;
171 } while (i < count);
172
173 if (context.sp == 0)
174 fmt::println(m_os, "Stack is empty");
175
176 fmt::println(m_os, "");
177 }
178
179 void Debugger::showLocals(VM& vm, ExecutionContext& context, const std::size_t count) const
180 {
181 const std::size_t limit = context.locals[context.locals.size() - 2].size(); // -2 because we created a scope for the debugger
182 if (limit > 0 && count > 0)
183 {
184 fmt::println(m_os, "scope size: {}", limit);
185 fmt::println(m_os, "index | id | name | type | value");
186 std::size_t i = 0;
187
188 do
189 {
190 if (limit <= i)
191 break;
192
193 auto& [id, value] = context.locals[context.locals.size() - 2].atPosReverse(i);
194 const auto color = m_colorize ? fmt::fg(i % 2 == 0 ? fmt::color::forest_green : fmt::color::cornflower_blue) : fmt::text_style();
195
196 fmt::println(
197 m_os,
198 "{:>5} | {:3} | {:14} | {:>9} | {}",
199 fmt::styled(limit - i - 1, color),
200 fmt::styled(id, color),
201 fmt::styled(vm.m_state.m_symbols[id], color),
202 fmt::styled(std::to_string(value.valueType()), color),
203 fmt::styled(value.toString(vm, /* show_as_code= */ true), color));
204 ++i;
205 } while (i < count);
206 }
207 else
208 fmt::println(m_os, "Current scope is empty");
209
210 fmt::println(m_os, "");
211 }
212
213 std::optional<std::string> Debugger::getCommandArg(const std::string& command, const std::string& line)
214 {
215 std::string arg = line.substr(command.size());
217
218 if (arg.empty())
219 return std::nullopt;
220 return arg;
221 }
222
223 std::optional<std::size_t> Debugger::parseStringAsInt(const std::string& str)
224 {
225 std::size_t result = 0;
226 auto [ptr, ec] = std::from_chars(str.data(), str.data() + str.size(), result);
227
228 if (ec == std::errc())
229 return result;
230 return std::nullopt;
231 }
232
233 std::optional<std::size_t> Debugger::getArgAndParseOrError(const std::string& command, const std::string& line, const std::size_t default_value) const
234 {
235 const auto maybe_arg = getCommandArg(command, line);
236 std::size_t count = default_value;
237 if (maybe_arg)
238 {
239 if (const auto maybe_int = parseStringAsInt(maybe_arg.value()))
240 count = maybe_int.value();
241 else
242 {
243 fmt::println(m_os, "Couldn't parse argument as an integer");
244 return std::nullopt;
245 }
246 }
247
248 return count;
249 }
250
251 std::optional<std::string> Debugger::prompt(const std::size_t ip, const std::size_t pp, VM& vm, ExecutionContext& context)
252 {
253 std::string code;
254 long open_parens = 0;
255 long open_braces = 0;
256
257 while (true)
258 {
259 const bool unfinished_block = open_parens != 0 || open_braces != 0;
260 fmt::print(
261 m_os,
262 "dbg[{},{}]:{:0>3}{} ",
263 fmt::format("pp:{}", fmt::styled(pp, m_colorize ? fmt::fg(fmt::color::green) : fmt::text_style())),
264 fmt::format("ip:{}", fmt::styled(ip / 4, m_colorize ? fmt::fg(fmt::color::cyan) : fmt::text_style())),
266 unfinished_block ? ":" : ">");
267
268 std::string line;
269 if (m_prompt_stream)
270 {
271 std::getline(*m_prompt_stream, line);
272 fmt::println(m_os, "{}", line); // because nothing is printed otherwise, and prompts get printed on the same line
273 }
274 else
275 std::getline(std::cin, line);
276
278
279 if (line == "c" || line == "continue" || line.empty())
280 {
281 fmt::println(m_os, "dbg: continue");
282 return std::nullopt;
283 }
284 else if (line == "q" || line == "quit")
285 {
286 fmt::println(m_os, "dbg: stop");
287 m_quit_vm = true;
288 return std::nullopt;
289 }
290 else if (line.starts_with("stack"))
291 {
292 if (auto arg = getArgAndParseOrError("stack", line, /* default_value= */ 5))
293 showStack(vm, context, arg.value());
294 else
295 return std::nullopt;
296 }
297 else if (line.starts_with("locals"))
298 {
299 if (auto arg = getArgAndParseOrError("locals", line, /* default_value= */ 5))
300 showLocals(vm, context, arg.value());
301 else
302 return std::nullopt;
303 }
304 else if (line == "help")
305 {
306 fmt::println(m_os, "Available commands:");
307 fmt::println(m_os, " help -- display this message");
308 fmt::println(m_os, " c, continue -- resume execution");
309 fmt::println(m_os, " q, quit -- quit the debugger, stopping the script execution");
310 fmt::println(m_os, " stack <n=5> -- show the last n values on the stack");
311 fmt::println(m_os, " locals <n=5> -- show the last n values on the locals' stack");
312 }
313 else
314 {
315 code += line;
316
317 open_parens += Utils::countOpenEnclosures(line, '(', ')');
318 open_braces += Utils::countOpenEnclosures(line, '{', '}');
319
320 ++m_line_count;
321 if (open_braces == 0 && open_parens == 0)
322 break;
323 }
324 }
325
326 return code;
327 }
328
329 std::optional<CompiledPrompt> Debugger::compile(const std::string& code, const std::size_t start_page_at_offset) const
330 {
331 Welder welder(0, m_libenv, DefaultFeatures);
333 return std::nullopt;
334 if (!welder.generateBytecodeUsingTables(m_symbols, m_constants, start_page_at_offset))
335 return std::nullopt;
336
337 BytecodeReader bcr;
338 bcr.feed(welder.bytecode());
339 const auto syms = bcr.symbols();
340 const auto vals = bcr.values(syms);
341 const auto files = bcr.filenames(vals);
342 const auto inst_locs = bcr.instLocations(files);
343 const auto [pages, _] = bcr.code(inst_locs);
344
345 return std::optional(CompiledPrompt(pages, syms.symbols, vals.values));
346 }
347}
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 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:249
The ArkScript virtual machine, executing ArkScript bytecode.
Definition VM.hpp:48
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:407
bool m_running
Definition VM.hpp:184
State & m_state
Definition VM.hpp:181
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:2275
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.
Definition VM.hpp:188
ValueType valueType() const noexcept
Definition Value.hpp:153
std::string toString(VM &vm, bool show_as_code=false) const noexcept
Definition Value.cpp:77
The welder joins all the compiler passes.
Definition Welder.hpp:39
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:93
const bytecode_t & bytecode() const noexcept
Definition Welder.cpp:162
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:53
void run(VM &vm, ExecutionContext &context, bool from_breakpoint)
Start the debugger shell.
Definition Debugger.cpp:63
std::string m_code
Code added while inside the debugger.
Definition Debugger.hpp:116
std::vector< std::unique_ptr< SavedState > > m_states
Definition Debugger.hpp:106
static std::optional< std::size_t > parseStringAsInt(const std::string &str)
Definition Debugger.cpp:223
void showStack(VM &vm, const ExecutionContext &context, std::size_t count) const
Definition Debugger.cpp:156
void showLocals(VM &vm, ExecutionContext &context, std::size_t count) const
Definition Debugger.cpp:179
std::optional< std::string > prompt(std::size_t ip, std::size_t pp, VM &vm, ExecutionContext &context)
Definition Debugger.cpp:251
void resetContextToSavedState(ExecutionContext &context)
Reset a VM context to the last state saved by the debugger.
Definition Debugger.cpp:50
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:19
std::optional< std::size_t > getArgAndParseOrError(const std::string &command, const std::string &line, std::size_t default_value) const
Definition Debugger.cpp:233
std::unique_ptr< std::istream > m_prompt_stream
Definition Debugger.hpp:115
std::ostream & m_os
Definition Debugger.hpp:113
std::vector< std::string > m_symbols
Definition Debugger.hpp:108
static std::optional< std::string > getCommandArg(const std::string &command, const std::string &line)
Definition Debugger.cpp:213
void showContext(const VM &vm, const ExecutionContext &context) const
Definition Debugger.cpp:131
void saveState(const ExecutionContext &context)
Save the current VM state, to get back to it once the debugger is done running.
Definition Debugger.cpp:38
std::vector< std::filesystem::path > m_libenv
Definition Debugger.hpp:107
std::vector< Value > m_constants
Definition Debugger.hpp:109
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:329
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
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:67
@ 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:233
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.