• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ArkScript-lang / Ark / 23719314426

29 Mar 2026 09:18PM UTC coverage: 93.76% (+0.002%) from 93.758%
23719314426

push

github

SuperFola
feat(debugger): enhance command definition to add a description

32 of 32 new or added lines in 2 files covered. (100.0%)

17 existing lines in 1 file now uncovered.

9766 of 10416 relevant lines covered (93.76%)

652758.06 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.43
/src/arkreactor/VM/Debugger.cpp
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>
13
#include <Ark/Compiler/Welder.hpp>
14
#include <Ark/Compiler/BytecodeReader.hpp>
15
#include <Ark/Error/Diagnostics.hpp>
16

17
namespace 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
        initCommands();
×
27
        saveState(context);
×
UNCOV
28
    }
×
29

30
    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) :
12✔
31
        m_libenv(libenv),
6✔
32
        m_symbols(symbols),
6✔
33
        m_constants(constants),
6✔
34
        m_os(os),
6✔
35
        m_colorize(false),
6✔
36
        m_prompt_stream(std::make_unique<std::ifstream>(path_to_prompt_file))
6✔
37
    {
6✔
38
        initCommands();
6✔
39
    }
6✔
40

41
    void Debugger::saveState(const ExecutionContext& context)
13✔
42
    {
13✔
43
        m_states.emplace_back(
26✔
44
            std::make_unique<SavedState>(
26✔
45
                context.ip,
13✔
46
                context.pp,
13✔
47
                context.sp,
13✔
48
                context.fc,
13✔
49
                context.locals,
13✔
50
                context.stacked_closure_scopes));
13✔
51
    }
13✔
52

53
    void Debugger::resetContextToSavedState(ExecutionContext& context)
13✔
54
    {
13✔
55
        const auto& [ip, pp, sp, fc, locals, closure_scopes] = *m_states.back();
39✔
56
        context.locals = locals;
13✔
57
        context.stacked_closure_scopes = closure_scopes;
13✔
58
        context.ip = ip;
13✔
59
        context.pp = pp;
13✔
60
        context.sp = sp;
13✔
61
        context.fc = fc;
13✔
62

63
        m_states.pop_back();
13✔
64
    }
13✔
65

66
    void Debugger::run(VM& vm, ExecutionContext& context, const bool from_breakpoint)
13✔
67
    {
13✔
68
        using namespace std::chrono_literals;
69

70
        if (from_breakpoint)
13✔
71
            showContext(vm, context);
12✔
72

73
        m_running = true;
13✔
74
        const bool is_vm_running = vm.m_running;
13✔
75
        const std::size_t ip_at_breakpoint = context.ip,
13✔
76
                          pp_at_breakpoint = context.pp;
13✔
77
        // create dedicated scope, so that we won't be overwriting existing variables
78
        context.locals.emplace_back(context.scopes_storage.data(), context.locals.back().storageEnd());
13✔
79
        std::size_t last_ip = 0;
13✔
80

81
        while (true)
26✔
82
        {
83
            std::optional<std::string> maybe_input = prompt(ip_at_breakpoint, pp_at_breakpoint, vm, context);
26✔
84

85
            if (maybe_input)
26✔
86
            {
87
                const std::string& line = maybe_input.value();
13✔
88

89
                if (const auto compiled = compile(m_code + line, vm.m_state.m_pages.size()); compiled.has_value())
26✔
90
                {
91
                    context.ip = last_ip;
13✔
92
                    context.pp = vm.m_state.m_pages.size();
13✔
93

94
                    vm.m_state.extendBytecode(compiled->pages, compiled->symbols, compiled->constants);
13✔
95

96
                    if (vm.safeRun(context) == 0)
13✔
97
                    {
98
                        // executing code worked
99
                        m_code += line + "\n";
13✔
100
                        // place ip to end of bytecode instruction (HALT)
101
                        last_ip = context.ip - 4;
13✔
102

103
                        const Value* maybe_value = vm.peekAndResolveAsPtr(context);
13✔
104
                        if (maybe_value != nullptr &&
13✔
105
                            maybe_value->valueType() != ValueType::Undefined &&
13✔
106
                            maybe_value->valueType() != ValueType::InstPtr &&
3✔
107
                            maybe_value->valueType() != ValueType::Garbage)
×
108
                            fmt::println(
×
109
                                m_os,
×
UNCOV
110
                                "{}",
×
UNCOV
111
                                fmt::styled(
×
UNCOV
112
                                    maybe_value->toString(vm),
×
UNCOV
113
                                    m_colorize ? fmt::fg(fmt::color::chocolate) : fmt::text_style()));
×
114
                    }
13✔
115
                }
13✔
116
                else
UNCOV
117
                    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
×
118
            }
13✔
119
            else
120
                break;
13✔
121
        }
26✔
122

123
        context.locals.pop_back();
13✔
124

125
        // we do not want to retain code from the past executions
126
        m_code.clear();
13✔
127
        m_line_count = 0;
13✔
128

129
        // we hit a HALT instruction that set 'running' to false, ignore that if we were still running!
130
        vm.m_running = is_vm_running;
13✔
131
        m_running = false;
13✔
132
    }
13✔
133

134
    void Debugger::initCommands()
6✔
135
    {
6✔
136
        m_commands = {
42✔
137
            Command(
6✔
138
                "help",
6✔
139
                "display this message",
6✔
140
                [this](const std::string&, const CommandArgs&) {
7✔
141
                    fmt::println(m_os, "Available commands:");
1✔
142
                    for (const Command& cmd : m_commands)
7✔
143
                    {
144
                        if (cmd.is_exact)
6✔
145
                            fmt::println(m_os, "  {} -- {}", fmt::join(cmd.names, ", "), cmd.description);
4✔
146
                        else
147
                            // todo: make arguments description configurable
6✔
148
                            fmt::println(m_os, "  {} <n=5> -- {}", fmt::join(cmd.names, ", "), cmd.description);
8✔
149
                    }
6✔
150
                    return false;
1✔
151
                }),
152
            Command(
6✔
153
                { "c", "continue" },
6✔
154
                "resume execution",
12✔
155
                [this](const std::string&, const CommandArgs&) {
18✔
156
                    fmt::println(m_os, "dbg: continue");
12✔
157
                    return true;
12✔
158
                }),
159
            Command(
6✔
160
                { "q", "quit" },
6✔
161
                "quit the debugger, stopping the script execution",
6✔
162
                [this](const std::string&, const CommandArgs&) {
7✔
163
                    fmt::println(m_os, "dbg: stop");
1✔
164
                    m_quit_vm = true;
1✔
165
                    return true;
1✔
166
                }),
167
            Command(
6✔
168
                StartsWith,
6✔
169
                "stack",
6✔
170
                "show the last n values on the stack",
6✔
171
                [this](const std::string& line, const CommandArgs& args) {
9✔
172
                    if (const auto arg = getArgAndParseOrError("stack", line, /* default_value= */ 5))
6✔
173
                        showStack(*args.vm_ptr, *args.ctx_ptr, arg.value());
3✔
174
                    return false;
3✔
UNCOV
175
                }),
×
176
            Command(
6✔
177
                StartsWith,
6✔
178
                "locals",
6✔
179
                "show the last n values on the locals' stack",
6✔
180
                [this](const std::string& line, const CommandArgs& args) {
12✔
181
                    if (const auto arg = getArgAndParseOrError("locals", line, /* default_value= */ 5))
12✔
182
                        showLocals(*args.vm_ptr, *args.ctx_ptr, arg.value());
6✔
183
                    return false;
6✔
UNCOV
184
                }),
×
185
            Command(
6✔
186
                "ptr",
6✔
187
                "show the values of the VM pointers",
6✔
188
                [this](const std::string&, const CommandArgs& args) {
7✔
189
                    fmt::println(
2✔
190
                        m_os,
1✔
191
                        "IP: {} - PP: {} - SP: {}",
1✔
192
                        fmt::styled(args.ip / 4, m_colorize ? fmt::fg(fmt::color::cyan) : fmt::text_style()),
1✔
193
                        fmt::styled(args.pp, m_colorize ? fmt::fg(fmt::color::green) : fmt::text_style()),
1✔
194
                        fmt::styled(args.ctx_ptr->sp, m_colorize ? fmt::fg(fmt::color::yellow) : fmt::text_style()));
1✔
195
                    return false;
1✔
196
                }),
197
        };
198
    }
6✔
199

200
    std::optional<Debugger::Command> Debugger::matchCommand(const std::string& line) const
41✔
201
    {
41✔
202
        for (const Command& c : m_commands)
219✔
203
        {
204
            if (c.is_exact)
178✔
205
            {
206
                if (std::ranges::find(c.names, line) != c.names.end())
127✔
207
                    return c;
15✔
208
            }
112✔
209
            else
210
            {
211
                if (std::ranges::find_if(c.names, [&line](const std::string& name) -> bool {
102✔
212
                        return line.starts_with(name);
51✔
213
                    }) != c.names.end())
51✔
214
                    return c;
9✔
215
            }
216
        }
178✔
217

218
        return std::nullopt;
17✔
219
    }
41✔
220

221
    void Debugger::showContext(const VM& vm, const ExecutionContext& context) const
12✔
222
    {
12✔
223
        // show the line where the breakpoint hit
224
        const auto maybe_source_loc = vm.findSourceLocation(context.ip, context.pp);
12✔
225
        if (maybe_source_loc)
12✔
226
        {
227
            const auto filename = vm.m_state.m_filenames[maybe_source_loc->filename_id];
12✔
228

229
            if (Utils::fileExists(filename))
12✔
230
            {
231
                fmt::println(m_os, "");
12✔
232
                Diagnostics::makeContext(
24✔
233
                    Diagnostics::ErrorLocation {
24✔
234
                        .filename = filename,
12✔
235
                        .start = FilePos { .line = maybe_source_loc->line, .column = 0 },
12✔
236
                        .end = std::nullopt,
12✔
237
                        .maybe_content = std::nullopt },
12✔
238
                    m_os,
12✔
239
                    /* maybe_context= */ std::nullopt,
12✔
240
                    /* colorize= */ m_colorize);
12✔
241
                fmt::println(m_os, "");
12✔
242
            }
12✔
243
        }
12✔
244
    }
12✔
245

246
    void Debugger::showStack(VM& vm, const ExecutionContext& context, const std::size_t count) const
3✔
247
    {
3✔
248
        std::size_t i = 1;
3✔
249
        do
9✔
250
        {
251
            if (context.sp < i)
9✔
252
                break;
1✔
253

254
            const auto color = m_colorize ? fmt::fg(i % 2 == 0 ? fmt::color::forest_green : fmt::color::cornflower_blue) : fmt::text_style();
8✔
255
            fmt::println(
16✔
256
                m_os,
8✔
257
                "{} -> {}",
8✔
258
                fmt::styled(context.sp - i, color),
8✔
259
                fmt::styled(context.stack[context.sp - i].toString(vm, /* show_as_code= */ true), color));
8✔
260
            ++i;
8✔
261
        } while (i < count);
8✔
262

263
        if (context.sp == 0)
3✔
264
            fmt::println(m_os, "Stack is empty");
1✔
265

266
        fmt::println(m_os, "");
3✔
267
    }
3✔
268

269
    void Debugger::showLocals(VM& vm, ExecutionContext& context, const std::size_t count) const
6✔
270
    {
6✔
271
        const std::size_t limit = context.locals[context.locals.size() - 2].size();  // -2 because we created a scope for the debugger
6✔
272
        if (limit > 0 && count > 0)
6✔
273
        {
274
            fmt::println(m_os, "scope size: {}", limit);
6✔
275
            fmt::println(m_os, "index |  id |      name      |    type   | value");
6✔
276
            std::size_t i = 0;
6✔
277

278
            do
17✔
279
            {
280
                if (limit <= i)
17✔
281
                    break;
3✔
282

283
                auto& [id, value] = context.locals[context.locals.size() - 2].atPosReverse(i);
56✔
284
                const auto color = m_colorize ? fmt::fg(i % 2 == 0 ? fmt::color::forest_green : fmt::color::cornflower_blue) : fmt::text_style();
14✔
285

286
                fmt::println(
28✔
287
                    m_os,
14✔
288
                    "{:>5} | {:3} | {:14} | {:>9} | {}",
14✔
289
                    fmt::styled(limit - i - 1, color),
14✔
290
                    fmt::styled(id, color),
28✔
291
                    fmt::styled(vm.m_state.m_symbols[id], color),
14✔
292
                    fmt::styled(std::to_string(value.valueType()), color),
28✔
293
                    fmt::styled(value.toString(vm, /* show_as_code= */ true), color));
28✔
294
                ++i;
14✔
295
            } while (i < count);
14✔
296
        }
6✔
297
        else
UNCOV
298
            fmt::println(m_os, "Current scope is empty");
×
299

300
        fmt::println(m_os, "");
6✔
301
    }
6✔
302

303
    std::optional<std::string> Debugger::getCommandArg(const std::string& command, const std::string& line)
9✔
304
    {
9✔
305
        std::string arg = line.substr(command.size());
9✔
306
        Utils::trimWhitespace(arg);
9✔
307

308
        if (arg.empty())
9✔
309
            return std::nullopt;
6✔
310
        return arg;
3✔
311
    }
9✔
312

313
    std::optional<std::size_t> Debugger::parseStringAsInt(const std::string& str)
3✔
314
    {
3✔
315
        std::size_t result = 0;
3✔
316
        auto [ptr, ec] = std::from_chars(str.data(), str.data() + str.size(), result);
3✔
317

318
        if (ec == std::errc())
3✔
319
            return result;
3✔
UNCOV
320
        return std::nullopt;
×
321
    }
3✔
322

323
    std::optional<std::size_t> Debugger::getArgAndParseOrError(const std::string& command, const std::string& line, const std::size_t default_value) const
9✔
324
    {
9✔
325
        const auto maybe_arg = getCommandArg(command, line);
9✔
326
        std::size_t count = default_value;
9✔
327
        if (maybe_arg)
9✔
328
        {
329
            if (const auto maybe_int = parseStringAsInt(maybe_arg.value()))
6✔
330
                count = maybe_int.value();
3✔
331
            else
332
            {
UNCOV
333
                fmt::println(m_os, "Couldn't parse argument as an integer");
×
UNCOV
334
                return std::nullopt;
×
335
            }
336
        }
3✔
337

338
        return count;
9✔
339
    }
9✔
340

341
    std::optional<std::string> Debugger::prompt(const std::size_t ip, const std::size_t pp, VM& vm, ExecutionContext& context)
26✔
342
    {
26✔
343
        std::string code;
26✔
344
        long open_parens = 0;
26✔
345
        long open_braces = 0;
26✔
346

347
        while (true)
41✔
348
        {
349
            const bool unfinished_block = open_parens != 0 || open_braces != 0;
41✔
350
            fmt::print(
123✔
351
                m_os,
41✔
352
                "dbg[{},{}]:{:0>3}{} ",
41✔
353
                fmt::format("pp:{}", fmt::styled(pp, m_colorize ? fmt::fg(fmt::color::green) : fmt::text_style())),
41✔
354
                fmt::format("ip:{}", fmt::styled(ip / 4, m_colorize ? fmt::fg(fmt::color::cyan) : fmt::text_style())),
41✔
355
                m_line_count,
41✔
356
                unfinished_block ? ":" : ">");
41✔
357

358
            std::string line;
41✔
359
            if (m_prompt_stream)
41✔
360
            {
361
                std::getline(*m_prompt_stream, line);
41✔
362
                fmt::println(m_os, "{}", line);  // because nothing is printed otherwise, and prompts get printed on the same line
41✔
363
            }
41✔
364
            else
UNCOV
365
                std::getline(std::cin, line);
×
366

367
            Utils::trimWhitespace(line);
41✔
368

369
            if (line.empty() && !unfinished_block)
41✔
370
            {
UNCOV
371
                fmt::println(m_os, "dbg: continue");
×
UNCOV
372
                return std::nullopt;
×
373
            }
374

375
            if (const auto& maybe_cmd = matchCommand(line))
82✔
376
            {
377
                if (maybe_cmd->action(line, CommandArgs { .vm_ptr = &vm, .ctx_ptr = &context, .ip = ip, .pp = pp }))
24✔
378
                    return std::nullopt;
13✔
379
            }
11✔
380
            else
381
            {
382
                code += line + "\n";
17✔
383

384
                open_parens += Utils::countOpenEnclosures(line, '(', ')');
17✔
385
                open_braces += Utils::countOpenEnclosures(line, '{', '}');
17✔
386

387
                ++m_line_count;
17✔
388
                if (open_braces == 0 && open_parens == 0)
17✔
389
                    break;
13✔
390
            }
391
        }
41✔
392

393
        return code;
13✔
394
    }
26✔
395

396
    std::optional<CompiledPrompt> Debugger::compile(const std::string& code, const std::size_t start_page_at_offset) const
13✔
397
    {
13✔
398
        Welder welder(0, m_libenv, DefaultFeatures);
13✔
399
        if (!welder.computeASTFromStringWithKnownSymbols(code, m_symbols))
13✔
UNCOV
400
            return std::nullopt;
×
401
        if (!welder.generateBytecodeUsingTables(m_symbols, m_constants, start_page_at_offset))
13✔
UNCOV
402
            return std::nullopt;
×
403

404
        BytecodeReader bcr;
13✔
405
        bcr.feed(welder.bytecode());
13✔
406
        const auto syms = bcr.symbols();
13✔
407
        const auto vals = bcr.values(syms);
13✔
408
        const auto files = bcr.filenames(vals);
13✔
409
        const auto inst_locs = bcr.instLocations(files);
13✔
410
        const auto [pages, _] = bcr.code(inst_locs);
13✔
411

412
        return std::optional(CompiledPrompt(pages, syms.symbols, vals.values));
26✔
413
    }
13✔
414
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc