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

ArkScript-lang / Ark / 24088572670

07 Apr 2026 03:04PM UTC coverage: 93.945% (+0.1%) from 93.837%
24088572670

push

github

SuperFola
refactor(vm): move the instruction eval loop to a template private VM::unsafeRun, introduce a safeRun that handles exceptions when calling unsafeRun

909 of 948 new or added lines in 3 files covered. (95.89%)

82 existing lines in 3 files now uncovered.

9868 of 10504 relevant lines covered (93.95%)

648642.57 hits per line

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

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

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

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

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

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

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

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

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

85
            if (maybe_input)
27✔
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,
×
110
                                "{}",
×
111
                                fmt::styled(
×
112
                                    maybe_value->toString(vm),
×
113
                                    m_colorize ? fmt::fg(fmt::color::chocolate) : fmt::text_style()));
×
114
                    }
13✔
115
                }
13✔
116
                else
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;
14✔
121
        }
27✔
122

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

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

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

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

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

157
        return args_values;
15✔
158
    }
23✔
159

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

433
        return code;
13✔
434
    }
27✔
435

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

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

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