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

ArkScript-lang / Ark / 22545853933

01 Mar 2026 02:54PM UTC coverage: 93.49% (+0.02%) from 93.475%
22545853933

Pull #651

github

web-flow
Merge 75411da38 into 946d9b0ed
Pull Request #651: feat(debugger): add 'stack' and 'locals' command to the debugger

81 of 87 new or added lines in 1 file covered. (93.1%)

4 existing lines in 1 file now uncovered.

9334 of 9984 relevant lines covered (93.49%)

274627.94 hits per line

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

92.59
/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

9
#include <Ark/State.hpp>
10
#include <Ark/VM/VM.hpp>
11
#include <Ark/Utils/Files.hpp>
12
#include <Ark/Compiler/Welder.hpp>
13
#include <Ark/Compiler/BytecodeReader.hpp>
14
#include <Ark/Error/Diagnostics.hpp>
15

16
namespace Ark::internal
17
{
UNCOV
18
    Debugger::Debugger(const ExecutionContext& context, const std::vector<std::filesystem::path>& libenv, const std::vector<std::string>& symbols, const std::vector<Value>& constants) :
×
19
        m_libenv(libenv),
×
20
        m_symbols(symbols),
×
21
        m_constants(constants),
×
22
        m_os(std::cout),
×
23
        m_colorize(true)
×
24
    {
×
25
        saveState(context);
×
26
    }
×
27

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

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

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

59
        m_states.pop_back();
13✔
60
    }
13✔
61

62
    void Debugger::run(VM& vm, ExecutionContext& context, const bool from_breakpoint)
13✔
63
    {
13✔
64
        using namespace std::chrono_literals;
65

66
        if (from_breakpoint)
13✔
67
            showContext(vm, context);
12✔
68

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

77
        while (true)
26✔
78
        {
79
            std::optional<std::string> maybe_input = prompt(ip_at_breakpoint, pp_at_breakpoint, vm, context);
26✔
80

81
            if (maybe_input)
26✔
82
            {
83
                const std::string& line = maybe_input.value();
13✔
84

85
                if (const auto compiled = compile(m_code + line, vm.m_state.m_pages.size()); compiled.has_value())
26✔
86
                {
87
                    context.ip = last_ip;
13✔
88
                    context.pp = vm.m_state.m_pages.size();
13✔
89

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

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

99
                        const Value* maybe_value = vm.peekAndResolveAsPtr(context);
13✔
100
                        if (maybe_value != nullptr && maybe_value->valueType() != ValueType::Undefined && maybe_value->valueType() != ValueType::InstPtr)
13✔
101
                            fmt::println(
20✔
102
                                m_os,
10✔
103
                                "{}",
10✔
104
                                fmt::styled(
20✔
105
                                    maybe_value->toString(vm),
10✔
106
                                    m_colorize ? fmt::fg(fmt::color::chocolate) : fmt::text_style()));
10✔
107
                    }
13✔
108
                }
13✔
109
                else
110
                    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
6✔
111
            }
19✔
112
            else
113
                break;
13✔
114
        }
26✔
115

116
        context.locals.pop_back();
13✔
117

6✔
118
        // we do not want to retain code from the past executions
119
        m_code.clear();
13✔
120
        m_line_count = 0;
13✔
121

122
        // we hit a HALT instruction that set 'running' to false, ignore that if we were still running!
123
        vm.m_running = is_vm_running;
13✔
124
        m_running = false;
13✔
125
    }
13✔
126

127
    void Debugger::showContext(const VM& vm, const ExecutionContext& context) const
12✔
128
    {
12✔
129
        // show the line where the breakpoint hit
130
        const auto maybe_source_loc = vm.findSourceLocation(context.ip, context.pp);
12✔
131
        if (maybe_source_loc)
12✔
132
        {
133
            const auto filename = vm.m_state.m_filenames[maybe_source_loc->filename_id];
12✔
134

135
            if (Utils::fileExists(filename))
12✔
136
            {
137
                fmt::println(m_os, "");
12✔
138
                Diagnostics::makeContext(
24✔
139
                    Diagnostics::ErrorLocation {
24✔
140
                        .filename = filename,
12✔
141
                        .start = FilePos { .line = maybe_source_loc->line, .column = 0 },
12✔
142
                        .end = std::nullopt,
12✔
143
                        .maybe_content = std::nullopt },
12✔
144
                    m_os,
12✔
145
                    /* maybe_context= */ std::nullopt,
12✔
146
                    /* colorize= */ m_colorize);
12✔
147
                fmt::println(m_os, "");
12✔
148
            }
12✔
149
        }
12✔
150
    }
12✔
151

152
    void Debugger::showStack(VM& vm, const ExecutionContext& context, const std::size_t count) const
3✔
153
    {
3✔
154
        std::size_t i = 1;
3✔
155
        do
9✔
156
        {
157
            if (context.sp < i)
9✔
158
                break;
1✔
159

160
            const auto color = m_colorize ? fmt::fg(i % 2 == 0 ? fmt::color::forest_green : fmt::color::cornflower_blue) : fmt::text_style();
8✔
161
            fmt::println(
16✔
162
                m_os,
8✔
163
                "{} -> {}",
8✔
164
                fmt::styled(context.sp - i, color),
8✔
165
                fmt::styled(context.stack[context.sp - i].toString(vm, /* show_as_code= */ true), color));
8✔
166
            ++i;
8✔
167
        } while (i < count);
8✔
168

169
        if (context.sp == 0)
3✔
170
            fmt::println(m_os, "Stack is empty");
1✔
171

172
        fmt::println(m_os, "");
3✔
173
    }
3✔
174

175
    void Debugger::showLocals(VM& vm, ExecutionContext& context, const std::size_t count) const
6✔
176
    {
6✔
177
        const std::size_t limit = context.locals[context.locals.size() - 2].size();  // -2 because we created a scope for the debugger
6✔
178
        if (limit > 0 && count > 0)
6✔
179
        {
180
            fmt::println(m_os, "scope size: {}", limit);
6✔
181
            fmt::println(m_os, "index |  id |    type   | value");
6✔
182
            std::size_t i = 0;
6✔
183

184
            do
17✔
185
            {
186
                if (limit <= i)
17✔
187
                    break;
3✔
188

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

192
                fmt::println(
28✔
193
                    m_os,
14✔
194
                    "{:>5} | {:3} | {:>9} | {}",
14✔
195
                    fmt::styled(limit - i - 1, color),
14✔
196
                    fmt::styled(id, color),
28✔
197
                    fmt::styled(std::to_string(value.valueType()), color),
28✔
198
                    fmt::styled(value.toString(vm, /* show_as_code= */ true), color));
28✔
199
                ++i;
14✔
200
            } while (i < count);
14✔
201
        }
6✔
202
        else
NEW
203
            fmt::println(m_os, "Current scope is empty");
×
204

205
        fmt::println(m_os, "");
6✔
206
    }
6✔
207

208
    std::optional<std::string> Debugger::getCommandArg(const std::string& command, const std::string& line)
9✔
209
    {
9✔
210
        std::string arg = line.substr(command.size());
9✔
211
        Utils::trimWhitespace(arg);
9✔
212

213
        if (arg.empty())
9✔
214
            return std::nullopt;
6✔
215
        return arg;
3✔
216
    }
9✔
217

218
    std::optional<std::size_t> Debugger::parseStringAsInt(const std::string& str)
3✔
219
    {
3✔
220
        std::size_t result = 0;
3✔
221
        auto [ptr, ec] = std::from_chars(str.data(), str.data() + str.size(), result);
3✔
222

223
        if (ec == std::errc())
3✔
224
            return result;
3✔
NEW
225
        return std::nullopt;
×
226
    }
3✔
227

228
    std::optional<std::size_t> Debugger::getArgAndParseOrError(const std::string& command, const std::string& line, const std::size_t default_value) const
9✔
229
    {
9✔
230
        const auto maybe_arg = getCommandArg(command, line);
9✔
231
        std::size_t count = default_value;
9✔
232
        if (maybe_arg)
9✔
233
        {
234
            if (const auto maybe_int = parseStringAsInt(maybe_arg.value()))
6✔
235
                count = maybe_int.value();
3✔
236
            else
237
            {
NEW
238
                fmt::println(m_os, "Couldn't parse argument as an integer");
×
NEW
239
                return std::nullopt;
×
240
            }
241
        }
3✔
242

243
        return count;
9✔
244
    }
9✔
245

246
    std::optional<std::string> Debugger::prompt(const std::size_t ip, const std::size_t pp, VM& vm, ExecutionContext& context)
26✔
247
    {
26✔
248
        std::string code;
26✔
249
        long open_parens = 0;
26✔
250
        long open_braces = 0;
26✔
251

252
        while (true)
36✔
253
        {
254
            const bool unfinished_block = open_parens != 0 || open_braces != 0;
36✔
255
            fmt::print(
108✔
256
                m_os,
36✔
257
                "dbg[{},{}]:{:0>3}{} ",
36✔
258
                fmt::format("pp:{}", fmt::styled(pp, m_colorize ? fmt::fg(fmt::color::green) : fmt::text_style())),
36✔
259
                fmt::format("ip:{}", fmt::styled(ip, m_colorize ? fmt::fg(fmt::color::cyan) : fmt::text_style())),
36✔
260
                m_line_count,
36✔
261
                unfinished_block ? ":" : ">");
36✔
262

263
            std::string line;
36✔
264
            if (m_prompt_stream)
36✔
265
            {
266
                std::getline(*m_prompt_stream, line);
36✔
267
                fmt::println(m_os, "{}", line);  // because nothing is printed otherwise, and prompts get printed on the same line
36✔
268
            }
36✔
269
            else
UNCOV
270
                std::getline(std::cin, line);
×
271

272
            Utils::trimWhitespace(line);
36✔
273

274
            if (line == "c" || line == "continue" || line.empty())
36✔
275
            {
276
                fmt::println(m_os, "dbg: continue");
12✔
277
                return std::nullopt;
12✔
278
            }
279
            else if (line == "q" || line == "quit")
24✔
280
            {
281
                fmt::println(m_os, "dbg: stop");
1✔
282
                m_quit_vm = true;
1✔
283
                return std::nullopt;
1✔
284
            }
285
            else if (line.starts_with("stack"))
23✔
286
            {
287
                if (auto arg = getArgAndParseOrError("stack", line, /* default_value= */ 5))
6✔
288
                    showStack(vm, context, arg.value());
3✔
289
                else
NEW
290
                    return std::nullopt;
×
291
            }
3✔
292
            else if (line.starts_with("locals"))
20✔
293
            {
294
                if (auto arg = getArgAndParseOrError("locals", line, /* default_value= */ 5))
12✔
295
                    showLocals(vm, context, arg.value());
6✔
296
                else
NEW
297
                    return std::nullopt;
×
298
            }
6✔
299
            else if (line == "help")
14✔
300
            {
301
                fmt::println(m_os, "Available commands:");
1✔
302
                fmt::println(m_os, "  help -- display this message");
1✔
303
                fmt::println(m_os, "  c, continue -- resume execution");
1✔
304
                fmt::println(m_os, "  q, quit -- quit the debugger, stopping the script execution");
1✔
305
                fmt::println(m_os, "  stack <n=5> -- show the last n values on the stack");
1✔
306
                fmt::println(m_os, "  locals <n=5> -- show the last n values on the locals' stack");
1✔
307
            }
1✔
308
            else
309
            {
310
                code += line;
13✔
311

312
                open_parens += Utils::countOpenEnclosures(line, '(', ')');
13✔
313
                open_braces += Utils::countOpenEnclosures(line, '{', '}');
13✔
314

315
                ++m_line_count;
13✔
316
                if (open_braces == 0 && open_parens == 0)
13✔
317
                    break;
13✔
318
            }
319
        }
36✔
320

321
        return code;
13✔
322
    }
26✔
323

324
    std::optional<CompiledPrompt> Debugger::compile(const std::string& code, const std::size_t start_page_at_offset) const
13✔
325
    {
13✔
326
        Welder welder(0, m_libenv, DefaultFeatures);
13✔
327
        if (!welder.computeASTFromStringWithKnownSymbols(code, m_symbols))
13✔
UNCOV
328
            return std::nullopt;
×
329
        if (!welder.generateBytecodeUsingTables(m_symbols, m_constants, start_page_at_offset))
13✔
UNCOV
330
            return std::nullopt;
×
331

332
        BytecodeReader bcr;
13✔
333
        bcr.feed(welder.bytecode());
13✔
334
        const auto syms = bcr.symbols();
13✔
335
        const auto vals = bcr.values(syms);
13✔
336
        const auto files = bcr.filenames(vals);
13✔
337
        const auto inst_locs = bcr.instLocations(files);
13✔
338
        const auto [pages, _] = bcr.code(inst_locs);
13✔
339

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