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

ArkScript-lang / Ark / 28841712321

07 Jul 2026 04:32AM UTC coverage: 94.381% (+0.03%) from 94.349%
28841712321

push

github

SuperFola
chore(ci): ignore leaks when copying a ClosureScope

10313 of 10927 relevant lines covered (94.38%)

991593.76 hits per line

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

92.75
/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/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>
14
#include <Ark/Compiler/Welder.hpp>
15
#include <Ark/Compiler/BytecodeReader.hpp>
16
#include <Ark/Error/Diagnostics.hpp>
17

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

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

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

64
        m_states.pop_back();
14✔
65
    }
14✔
66

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

71
        if (from_breakpoint)
14✔
72
            showContext(vm, context);
13✔
73

74
        m_running = true;
14✔
75
        const bool is_vm_running = vm.m_running;
14✔
76
        const std::size_t ip_at_breakpoint = context.ip,
14✔
77
                          pp_at_breakpoint = context.pp;
14✔
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());
14✔
80
        std::size_t last_ip = 0;
14✔
81

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

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

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

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

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

104
                        const Value* maybe_value = vm.peekAndResolveAsPtr(context);
13✔
105
                        if (maybe_value != nullptr &&
13✔
106
                            maybe_value->valueType() != ValueType::Undefined &&
13✔
107
                            maybe_value->valueType() != ValueType::InstPtr &&
3✔
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
                    }
13✔
116
                }
13✔
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
            }
13✔
120
            else
121
                break;
14✔
122
        }
27✔
123

124
        context.locals.pop_back();
14✔
125

126
        // we do not want to retain code from the past executions
127
        m_code.clear();
14✔
128
        m_line_count = 0;
14✔
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;
14✔
132
        m_running = false;
14✔
133
    }
14✔
134

135
    void Debugger::registerInstruction(const uint32_t word) noexcept
431✔
136
    {
431✔
137
        m_previous_insts.push_back(word);
431✔
138
    }
431✔
139

140
    std::optional<Debugger::Command::Args_t> Debugger::Command::getArgs(const std::string& line, std::ostream& os) const
16✔
141
    {
16✔
142
        std::vector<std::string> split = Utils::splitString(line, ' ');
16✔
143
        Args_t args_values = args;
16✔
144
        std::size_t i = 0;
16✔
145
        for (const auto& arg : std::ranges::views::drop(split, 1))
24✔
146
        {
147
            if (i < args.size())
8✔
148
                args_values[i].second = arg;
7✔
149
            else
150
            {
151
                fmt::println(os, "Too many arguments provided to {}, expected {}, got {}", split.front(), args.size(), split.size() - 1);
1✔
152
                return std::nullopt;
1✔
153
            }
154

155
            ++i;
7✔
156
        }
8✔
157

158
        return args_values;
22✔
159
    }
23✔
160

161
    std::optional<std::size_t> Debugger::Command::argAsCount(const std::string& line, const std::size_t idx, std::ostream& os) const
16✔
162
    {
16✔
163
        const std::optional<Args_t> maybe_parsed = getArgs(line, os);
16✔
164
        if (maybe_parsed && idx < maybe_parsed->size())
16✔
165
        {
166
            const std::string str = maybe_parsed.value()[idx].second;
15✔
167
            std::size_t result = 0;
22✔
168
            auto [ptr, ec] = std::from_chars(str.data(), str.data() + str.size(), result);
15✔
169

170
            if (ec == std::errc())
15✔
171
                return result;
13✔
172

173
            fmt::println(os, "Couldn't parse argument as an unsigned integer");
2✔
174
            return std::nullopt;
2✔
175
        }
15✔
176
        return std::nullopt;
1✔
177
    }
16✔
178

179
    void Debugger::initCommands()
7✔
180
    {
7✔
181
        m_commands = {
56✔
182
            Command(
7✔
183
                "help",
7✔
184
                "display this message",
7✔
185
                [this](const std::string&, const CommandArgs&) {
8✔
186
                    fmt::println(m_os, "Available commands:");
1✔
187
                    for (const Command& cmd : m_commands)
8✔
188
                    {
189
                        if (cmd.is_exact)
7✔
190
                            fmt::println(m_os, "  {} -- {}", fmt::join(cmd.names, ", "), cmd.description);
4✔
191
                        else
192
                        {
193
                            const auto v = std::views::transform(cmd.args, [](const auto& p) {
6✔
194
                                return fmt::format("{}={}", p.first, p.second);
3✔
195
                            });
196
                            fmt::println(m_os, "  {} <{}> -- {}", fmt::join(cmd.names, ", "), fmt::join(v, ", "), cmd.description);
3✔
197
                        }
3✔
198
                    }
7✔
199
                    return false;
1✔
200
                }),
201
            Command(
7✔
202
                { "c", "continue" },
7✔
203
                "resume execution",
7✔
204
                [this](const std::string&, const CommandArgs&) {
20✔
205
                    fmt::println(m_os, "dbg: continue");
13✔
206
                    return true;
13✔
207
                }),
208
            Command(
7✔
209
                { "q", "quit" },
7✔
210
                "quit the debugger, stopping the script execution",
7✔
211
                [this](const std::string&, const CommandArgs&) {
8✔
212
                    fmt::println(m_os, "dbg: stop");
1✔
213
                    m_quit_vm = true;
1✔
214
                    return true;
1✔
215
                }),
216
            Command(
7✔
217
                StartsWith("stack"),
7✔
218
                { { "n", "5" } },
7✔
219
                "show the last n values on the stack",
7✔
220
                [this](const std::string& line, const CommandArgs& args) {
10✔
221
                    if (const auto arg = args.me.argAsCount(line, 0, m_os))
6✔
222
                        showStack(*args.vm_ptr, *args.ctx_ptr, arg.value());
3✔
223
                    return false;
3✔
224
                }),
225
            Command(
7✔
226
                StartsWith("locals"),
7✔
227
                { { "n", "5" } },
7✔
228
                "show the last n values on the locals' stack",
7✔
229
                [this](const std::string& line, const CommandArgs& args) {
13✔
230
                    if (const auto arg = args.me.argAsCount(line, 0, m_os))
12✔
231
                        showLocals(*args.vm_ptr, *args.ctx_ptr, arg.value());
6✔
232
                    return false;
6✔
233
                }),
234
            Command(
7✔
235
                "ptr",
7✔
236
                "show the values of the VM pointers",
7✔
237
                [this](const std::string&, const CommandArgs& args) {
8✔
238
                    fmt::println(
2✔
239
                        m_os,
1✔
240
                        "IP: {} - PP: {} - SP: {}",
1✔
241
                        fmt::styled(args.ip / 4, m_colorize ? fmt::fg(fmt::color::cyan) : fmt::text_style()),
1✔
242
                        fmt::styled(args.pp, m_colorize ? fmt::fg(fmt::color::green) : fmt::text_style()),
1✔
243
                        fmt::styled(args.ctx_ptr->sp, m_colorize ? fmt::fg(fmt::color::yellow) : fmt::text_style()));
1✔
244
                    return false;
1✔
245
                }),
246
            Command(
7✔
247
                StartsWith("trace"),
7✔
248
                { { "n", "10" } },
7✔
249
                "show the last n executed instructions",
7✔
250
                [this](const std::string& line, const CommandArgs& args) {
14✔
251
                    if (const auto arg = args.me.argAsCount(line, 0, m_os))
11✔
252
                        showPreviousInstructions(*args.vm_ptr, arg.value());
4✔
253
                    return false;
7✔
254
                }),
255
        };
256
    }
7✔
257

258
    std::optional<Debugger::Command> Debugger::matchCommand(const std::string& line) const
49✔
259
    {
49✔
260
        for (const Command& c : m_commands)
295✔
261
        {
262
            if (c.is_exact)
246✔
263
            {
264
                if (std::ranges::find(c.names, line) != c.names.end())
157✔
265
                    return c;
16✔
266
            }
141✔
267
            else
268
            {
269
                if (std::ranges::find_if(c.names, [&line](const std::string& name) -> bool {
178✔
270
                        return line.starts_with(name);
89✔
271
                    }) != c.names.end())
89✔
272
                    return c;
16✔
273
            }
274
        }
246✔
275

276
        return std::nullopt;
17✔
277
    }
49✔
278

279
    void Debugger::showContext(const VM& vm, const ExecutionContext& context) const
13✔
280
    {
13✔
281
        // show the line where the breakpoint hit
282
        const auto maybe_source_loc = vm.findSourceLocation(context.ip, context.pp);
13✔
283
        if (maybe_source_loc)
13✔
284
        {
285
            const auto filename = vm.m_state.m_filenames[maybe_source_loc->filename_id];
13✔
286

287
            if (Utils::fileExists(filename))
13✔
288
            {
289
                fmt::println(m_os, "");
13✔
290
                Diagnostics::makeContext(
26✔
291
                    Diagnostics::ErrorLocation {
26✔
292
                        .filename = filename,
13✔
293
                        .start = FilePos { .line = maybe_source_loc->line, .column = 0 },
13✔
294
                        .end = std::nullopt,
13✔
295
                        .maybe_content = std::nullopt },
13✔
296
                    m_os,
13✔
297
                    /* maybe_context= */ std::nullopt,
13✔
298
                    /* colorize= */ m_colorize);
13✔
299
                fmt::println(m_os, "");
13✔
300
            }
13✔
301
        }
13✔
302
    }
13✔
303

304
    void Debugger::showStack(VM& vm, const ExecutionContext& context, const std::size_t count) const
3✔
305
    {
3✔
306
        std::size_t i = 1;
3✔
307
        do
9✔
308
        {
309
            if (context.sp < i)
9✔
310
                break;
1✔
311

312
            const auto color = m_colorize ? fmt::fg(i % 2 == 0 ? fmt::color::forest_green : fmt::color::cornflower_blue) : fmt::text_style();
8✔
313
            fmt::println(
16✔
314
                m_os,
8✔
315
                "{} -> {}",
8✔
316
                fmt::styled(context.sp - i, color),
8✔
317
                fmt::styled(context.stack[context.sp - i].toString(vm, /* show_as_code= */ true), color));
8✔
318
            ++i;
8✔
319
        } while (i < count);
8✔
320

321
        if (context.sp == 0)
3✔
322
            fmt::println(m_os, "Stack is empty");
1✔
323

324
        fmt::println(m_os, "");
3✔
325
    }
3✔
326

327
    void Debugger::showLocals(VM& vm, ExecutionContext& context, const std::size_t count) const
6✔
328
    {
6✔
329
        const std::size_t limit = context.locals[context.locals.size() - 2].size();  // -2 because we created a scope for the debugger
6✔
330
        if (limit > 0 && count > 0)
6✔
331
        {
332
            fmt::println(m_os, "scope size: {}", limit);
6✔
333
            fmt::println(m_os, "index |  id |      name      |    type   | value");
6✔
334
            std::size_t i = 0;
6✔
335

336
            do
17✔
337
            {
338
                if (limit <= i)
17✔
339
                    break;
3✔
340

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

344
                fmt::println(
28✔
345
                    m_os,
14✔
346
                    "{:>5} | {:3} | {:14} | {:>9} | {}",
14✔
347
                    fmt::styled(limit - i - 1, color),
14✔
348
                    fmt::styled(id, color),
28✔
349
                    fmt::styled(vm.m_state.m_symbols[id], color),
14✔
350
                    fmt::styled(std::to_string(value.valueType()), color),
28✔
351
                    fmt::styled(value.toString(vm, /* show_as_code= */ true), color));
28✔
352
                ++i;
14✔
353
            } while (i < count);
14✔
354
        }
6✔
355
        else
356
            fmt::println(m_os, "Current scope is empty");
×
357

358
        fmt::println(m_os, "");
6✔
359
    }
6✔
360

361
    void Debugger::showPreviousInstructions(const VM& vm, const std::size_t count) const
4✔
362
    {
4✔
363
        BytecodeReader bcr;
4✔
364
        bcr.feed(vm.bytecode());
4✔
365

366
        const auto syms = bcr.symbols();
4✔
367
        const auto vals = bcr.values(syms);
4✔
368

369
        for (std::size_t i = 0; i < count; ++i)
28✔
370
        {
371
            if (i >= m_previous_insts.size())
24✔
372
                break;
1✔
373

374
            const uint8_t inst = (m_previous_insts[m_previous_insts.size() - 1 - i] >> 24) & 0xff;
23✔
375
            const uint8_t padding = (m_previous_insts[m_previous_insts.size() - 1 - i] >> 16) & 0xff;
23✔
376
            const uint16_t arg = m_previous_insts[m_previous_insts.size() - 1 - i] & 0xffff;
23✔
377
            bcr.printInstruction(m_os, inst, padding, arg, syms, vals, m_colorize);
23✔
378
        }
23✔
379
    }
4✔
380

381
    std::optional<std::string> Debugger::prompt(const std::size_t ip, const std::size_t pp, VM& vm, ExecutionContext& context)
27✔
382
    {
27✔
383
        std::string code;
27✔
384
        long open_parens = 0;
27✔
385
        long open_braces = 0;
27✔
386

387
        while (true)
49✔
388
        {
389
            const bool unfinished_block = open_parens != 0 || open_braces != 0;
49✔
390
            fmt::print(
147✔
391
                m_os,
49✔
392
                "dbg[{},{}]:{:0>3}{} ",
49✔
393
                fmt::format("pp:{}", fmt::styled(pp, m_colorize ? fmt::fg(fmt::color::green) : fmt::text_style())),
49✔
394
                fmt::format("ip:{}", fmt::styled(ip / 4, m_colorize ? fmt::fg(fmt::color::cyan) : fmt::text_style())),
49✔
395
                m_line_count,
49✔
396
                unfinished_block ? ":" : ">");
49✔
397

398
            std::string line;
49✔
399
            if (m_prompt_stream)
49✔
400
            {
401
                std::getline(*m_prompt_stream, line);
49✔
402
                fmt::println(m_os, "{}", line);  // because nothing is printed otherwise, and prompts get printed on the same line
49✔
403
            }
49✔
404
            else
405
                std::getline(std::cin, line);
×
406

407
            Utils::trimWhitespace(line);
49✔
408

409
            if (line.empty() && !unfinished_block)
49✔
410
            {
411
                fmt::println(m_os, "dbg: continue");
×
412
                return std::nullopt;
×
413
            }
414

415
            if (const auto& maybe_cmd = matchCommand(line))
98✔
416
            {
417
                const Command cmd = maybe_cmd.value();
32✔
418
                if (cmd.action(line, CommandArgs { .vm_ptr = &vm, .ctx_ptr = &context, .ip = ip, .pp = pp, .me = cmd }))
32✔
419
                    return std::nullopt;
14✔
420
            }
32✔
421
            else
422
            {
423
                code += line + "\n";
17✔
424

425
                open_parens += Utils::countOpenEnclosures(line, '(', ')');
17✔
426
                open_braces += Utils::countOpenEnclosures(line, '{', '}');
17✔
427

428
                ++m_line_count;
17✔
429
                if (open_braces == 0 && open_parens == 0)
17✔
430
                    break;
13✔
431
            }
432
        }
49✔
433

434
        return code;
13✔
435
    }
27✔
436

437
    std::optional<CompiledPrompt> Debugger::compile(const std::string& code, const std::size_t start_page_at_offset) const
13✔
438
    {
13✔
439
        Welder welder(0, m_libenv, DefaultFeatures);
13✔
440
        if (!welder.computeASTFromStringWithKnownSymbols(code, m_symbols))
13✔
441
            return std::nullopt;
×
442
        if (!welder.generateBytecodeUsingTables(m_symbols, m_constants, start_page_at_offset))
13✔
443
            return std::nullopt;
×
444

445
        BytecodeReader bcr;
13✔
446
        bcr.feed(welder.bytecode());
13✔
447
        const auto syms = bcr.symbols();
13✔
448
        const auto vals = bcr.values(syms);
13✔
449
        const auto files = bcr.filenames(vals);
13✔
450
        const auto inst_locs = bcr.instLocations(files);
13✔
451
        const auto [pages, _] = bcr.code(inst_locs);
13✔
452

453
        return std::optional(CompiledPrompt(pages, syms.symbols, vals.values));
26✔
454
    }
13✔
455
}
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