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

ArkScript-lang / Ark / 17410180849

02 Sep 2025 04:42PM UTC coverage: 90.848% (-0.005%) from 90.853%
17410180849

push

github

SuperFola
feat(vm): adding new super instruction AT_SYM_INDEX_CONST, to load a value from a container using a constant as the index

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

6 existing lines in 3 files now uncovered.

7931 of 8730 relevant lines covered (90.85%)

156927.33 hits per line

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

99.24
/src/arkreactor/Compiler/NameResolution/NameResolutionPass.cpp
1
#include <Ark/Compiler/NameResolution/NameResolutionPass.hpp>
2

3
#include <Ark/Error/Exceptions.hpp>
4
#include <Ark/Utils/Utils.hpp>
5
#include <Ark/Builtins/Builtins.hpp>
6

7
namespace Ark::internal
8
{
9
    NameResolutionPass::NameResolutionPass(const unsigned debug) :
664✔
10
        Pass("NameResolution", debug)
332✔
11
    {
664✔
12
        for (const auto& builtin : Builtins::builtins)
21,912✔
13
            m_language_symbols.emplace(builtin.first);
21,580✔
14
        for (auto ope : Language::operators)
8,300✔
15
            m_language_symbols.emplace(ope);
7,968✔
16
        for (auto inst : Language::listInstructions)
3,320✔
17
            m_language_symbols.emplace(inst);
2,988✔
18

19
        m_language_symbols.emplace(Language::And);
332✔
20
        m_language_symbols.emplace(Language::Or);
332✔
21
        m_language_symbols.emplace(Language::SysArgs);
332✔
22
        m_language_symbols.emplace(Language::SysProgramName);
332✔
23
    }
332✔
24

25
    void NameResolutionPass::process(const Node& ast)
287✔
26
    {
287✔
27
        m_logger.traceStart("process");
287✔
28

29
        m_ast = ast;
253✔
30
        visit(m_ast, /* register_declarations= */ true);
253✔
31

32
        m_logger.traceEnd();
253✔
33

34
        m_logger.trace("AST after name resolution");
253✔
35
        if (m_logger.shouldTrace())
253✔
36
            m_ast.debugPrint(std::cout) << '\n';
1✔
37

38
        m_logger.traceStart("checkForUndefinedSymbol");
253✔
39
        checkForUndefinedSymbol();
253✔
40
        m_logger.traceEnd();
253✔
41
    }
253✔
42

43
    const Node& NameResolutionPass::ast() const noexcept
250✔
44
    {
250✔
45
        return m_ast;
250✔
46
    }
47

48
    std::string NameResolutionPass::addDefinedSymbol(const std::string& sym, const bool is_mutable)
30,273✔
49
    {
30,273✔
50
        const std::string fully_qualified_name = m_scope_resolver.registerInCurrent(sym, is_mutable);
30,273✔
51
        m_defined_symbols.emplace(fully_qualified_name);
30,273✔
52
        return fully_qualified_name;
30,273✔
53
    }
30,273✔
54

55
    void NameResolutionPass::visit(Node& node, const bool register_declarations)
276,392✔
56
    {
276,392✔
57
        switch (node.nodeType())
276,392✔
58
        {
83,192✔
59
            case NodeType::Symbol:
60
            {
61
                const std::string old_name = node.string();
83,192✔
62
                updateSymbolWithFullyQualifiedName(node);
83,192✔
63
                addSymbolNode(node, old_name);
83,190✔
64
                break;
65
            }
87,850✔
66

67
            case NodeType::Field:
68
                for (std::size_t i = 0, end = node.list().size(); i < end; ++i)
13,963✔
69
                {
70
                    Node& child = node.list()[i];
9,313✔
71

72
                    if (i == 0)
9,313✔
73
                    {
74
                        const std::string old_name = child.string();
4,650✔
75
                        // in case of field, no need to check if we can fully qualify names
76
                        child.setString(m_scope_resolver.getFullyQualifiedNameInNearestScope(old_name));
4,650✔
77
                        addSymbolNode(child, old_name);
4,650✔
78
                    }
4,650✔
79
                    else
80
                        addSymbolNode(child);
4,663✔
81
                }
9,313✔
82
                break;
91,692✔
83

84
            case NodeType::List:
85
                if (!node.constList().empty())
87,042✔
86
                {
87
                    if (node.constList()[0].nodeType() == NodeType::Keyword)
87,015✔
88
                        visitKeyword(node, node.constList()[0].keyword(), register_declarations);
41,908✔
89
                    else
90
                    {
91
                        // function calls
92
                        // the UpdateRef function calls kind get a special treatment, like let/mut/set,
93
                        // because we need to check for mutability errors
94
                        if (node.constList().size() > 1 && node.constList()[0].nodeType() == NodeType::Symbol &&
67,460✔
95
                            node.constList()[1].nodeType() == NodeType::Symbol && register_declarations)
39,490✔
96
                        {
97
                            const auto funcname = node.constList()[0].string();
11,405✔
98
                            const auto arg = node.constList()[1].string();
11,405✔
99

100
                            if (std::ranges::find(Language::UpdateRef, funcname) != Language::UpdateRef.end() && m_scope_resolver.isImmutable(arg).value_or(false))
11,405✔
101
                                throw CodeError(
10✔
102
                                    fmt::format("MutabilityError: Can not modify the constant list `{}' using `{}'", arg, funcname),
5✔
103
                                    CodeErrorContext(node.filename(), node.constList()[1].position()));
5✔
104

105
                            // check that we aren't doing a (append! a a) nor a (concat! a a)
106
                            if (funcname == Language::AppendInPlace || funcname == Language::ConcatInPlace)
11,400✔
107
                            {
108
                                for (std::size_t i = 2, end = node.constList().size(); i < end; ++i)
1,281✔
109
                                {
110
                                    if (node.constList()[i].nodeType() == NodeType::Symbol && node.constList()[i].string() == arg)
641✔
111
                                        throw CodeError(
4✔
112
                                            fmt::format("MutabilityError: Can not {} the list `{}' to itself", funcname, arg),
2✔
113
                                            CodeErrorContext(node.filename(), node.constList()[1].position()));
2✔
114
                                }
639✔
115
                            }
638✔
116
                        }
11,405✔
117

118
                        for (auto& child : node.list())
232,004✔
119
                            visit(child, register_declarations);
186,904✔
120
                    }
121
                }
87,008✔
122
                break;
87,225✔
123

124
            case NodeType::Namespace:
125
            {
126
                auto& namespace_ = node.arkNamespace();
190✔
127
                // no need to guard createNewNamespace with an if (register_declarations), we want to keep the namespace node
128
                // (which will get ignored by the compiler, that only uses its AST), so that we can (re)construct the
129
                // scopes correctly
130
                m_scope_resolver.createNewNamespace(namespace_.name, namespace_.with_prefix, namespace_.is_glob, namespace_.symbols);
190✔
131
                StaticScope* scope = m_scope_resolver.currentScope();
190✔
132

133
                visit(*namespace_.ast, /* register_declarations= */ true);
190✔
134
                // dual visit so that we can handle forward references
135
                visit(*namespace_.ast, /* register_declarations= */ false);
190✔
136

137
                // if we had specific symbols to import, check that those exist
138
                if (!namespace_.symbols.empty())
190✔
139
                {
140
                    const auto it = std::ranges::find_if(
270✔
141
                        namespace_.symbols,
90✔
142
                        [&scope, &namespace_](const std::string& sym) -> bool {
287✔
143
                            return !scope->get(sym, namespace_.name, true).has_value();
197✔
144
                        });
145

146
                    if (it != namespace_.symbols.end())
90✔
147
                        throw CodeError(
2✔
148
                            fmt::format("ImportError: Can not import symbol {} from {}, as it isn't in the package", *it, namespace_.name),
1✔
149
                            CodeErrorContext(namespace_.ast->filename(), namespace_.ast->position()));
1✔
150
                }
90✔
151

152
                m_scope_resolver.saveNamespaceAndRemove();
189✔
153
                break;
154
            }
101,508✔
155

156
            default:
157
                break;
101,318✔
158
        }
276,328✔
159
    }
276,338✔
160

161
    void NameResolutionPass::visitKeyword(Node& node, const Keyword keyword, const bool register_declarations)
41,908✔
162
    {
41,908✔
163
        switch (keyword)
41,908✔
164
        {
20,289✔
165
            case Keyword::Set:
166
                [[fallthrough]];
167
            case Keyword::Let:
168
                [[fallthrough]];
169
            case Keyword::Mut:
170
                // first, visit the value, then register the symbol
171
                // this allows us to detect things like (let foo (fun (&foo) ()))
172
                if (node.constList().size() > 2)
20,289✔
173
                    visit(node.list()[2], register_declarations);
20,288✔
174
                if (node.constList().size() > 1 && node.constList()[1].nodeType() == NodeType::Symbol)
20,289✔
175
                {
176
                    const std::string& name = node.constList()[1].string();
20,287✔
177
                    if (m_language_symbols.contains(name) && register_declarations)
20,287✔
178
                        throw CodeError(
6✔
179
                            fmt::format("Can not use a reserved identifier ('{}') as a {} name.", name, keyword == Keyword::Let ? "constant" : "variable"),
3✔
180
                            CodeErrorContext(node.filename(), node.constList()[1].position()));
3✔
181

182
                    if (m_scope_resolver.isInScope(name) && keyword == Keyword::Let && register_declarations)
20,284✔
183
                        throw CodeError(
2✔
184
                            fmt::format("MutabilityError: Can not use 'let' to redefine variable `{}'", name),
1✔
185
                            CodeErrorContext(node.filename(), node.constList()[1].position()));
1✔
186
                    if (keyword == Keyword::Set && m_scope_resolver.isRegistered(name))
20,283✔
187
                    {
188
                        if (m_scope_resolver.isImmutable(name).value_or(false) && register_declarations)
4,229✔
189
                            throw CodeError(
2✔
190
                                fmt::format("MutabilityError: Can not set the constant `{}' to {}", name, node.constList()[2].repr()),
1✔
191
                                CodeErrorContext(node.filename(), node.constList()[1].position()));
1✔
192

193
                        updateSymbolWithFullyQualifiedName(node.list()[1]);
4,228✔
194
                    }
4,228✔
195
                    else if (keyword != Keyword::Set)
16,054✔
196
                    {
197
                        // update the declared variable name to use the fully qualified name
198
                        // this will prevent name conflicts, and handle scope resolution
199
                        const std::string fully_qualified_name = addDefinedSymbol(name, keyword != Keyword::Let);
16,051✔
200
                        if (register_declarations)
16,051✔
201
                            node.list()[1].setString(fully_qualified_name);
8,198✔
202
                    }
16,051✔
203
                }
20,289✔
204
                break;
20,287✔
205

206
            case Keyword::Import:
207
                if (!node.constList().empty())
3✔
208
                    m_plugin_names.push_back(node.constList()[1].constList().back().string());
3✔
209
                break;
2,254✔
210

211
            case Keyword::While:
212
                // create a new scope to track variables
213
                m_scope_resolver.createNew();
2,251✔
214
                for (auto& child : node.list())
9,003✔
215
                    visit(child, register_declarations);
6,752✔
216
                // remove the scope once the loop has been compiled, only we were registering declarations
217
                m_scope_resolver.removeLastScope();
2,251✔
218
                break;
10,897✔
219

220
            case Keyword::Fun:
221
                // create a new scope to track variables
222
                m_scope_resolver.createNew();
8,646✔
223

224
                if (node.constList()[1].nodeType() == NodeType::List)
8,646✔
225
                {
226
                    for (auto& child : node.list()[1].list())
22,257✔
227
                    {
228
                        if (child.nodeType() == NodeType::Capture)
13,612✔
229
                        {
230
                            if (!m_scope_resolver.isRegistered(child.string()) && register_declarations)
684✔
231
                                throw CodeError(
4✔
232
                                    fmt::format("Can not capture `{}' because it is referencing a variable defined in an unreachable scope.", child.string()),
2✔
233
                                    CodeErrorContext(child.filename(), child.position()));
2✔
234

235
                            // save the old unqualified name of the capture, so that we can use it in the
236
                            // ASTLowerer later one
237
                            if (!child.getUnqualifiedName())
682✔
238
                            {
239
                                child.setUnqualifiedName(child.string());
227✔
240
                                m_defined_symbols.emplace(child.string());
227✔
241
                            }
227✔
242
                            // update the declared variable name to use the fully qualified name
243
                            // this will prevent name conflicts, and handle scope resolution
244
                            std::string old_name = child.string();
682✔
245
                            updateSymbolWithFullyQualifiedName(child);
682✔
246
                            // FIXME: addDefinedSymbol(fqn, true); ?
247
                            addDefinedSymbol(old_name, true);
682✔
248
                        }
682✔
249
                        else if (child.nodeType() == NodeType::Symbol)
12,928✔
250
                            addDefinedSymbol(child.string(), /* is_mutable= */ true);
12,928✔
251
                    }
13,612✔
252
                }
8,643✔
253
                if (node.constList().size() > 2)
8,644✔
254
                    visit(node.list()[2], register_declarations);
8,643✔
255

256
                // remove the scope once the function has been compiled, only we were registering declarations
257
                m_scope_resolver.removeLastScope();
8,644✔
258
                break;
19,363✔
259

260
            default:
261
                for (auto& child : node.list())
63,851✔
262
                    visit(child, register_declarations);
53,132✔
263
                break;
10,719✔
264
        }
41,901✔
265
    }
41,908✔
266

267
    void NameResolutionPass::addSymbolNode(const Node& symbol, const std::string& old_name)
92,503✔
268
    {
92,503✔
269
        const std::string& name = symbol.string();
92,503✔
270

271
        // we don't accept builtins/operators as a user symbol
272
        if (m_language_symbols.contains(name))
92,503✔
273
            return;
36,932✔
274

275
        // remove the old name node, to avoid false positive when looking for unbound symbols
276
        if (!old_name.empty())
55,571✔
277
        {
278
            auto it = std::ranges::find_if(m_symbol_nodes, [&old_name, &symbol](const Node& sym_node) -> bool {
5,482,262✔
279
                return sym_node.string() == old_name &&
5,478,147✔
280
                    sym_node.position().start == symbol.position().start &&
59,458✔
281
                    sym_node.filename() == symbol.filename();
12,665✔
282
            });
283
            if (it != m_symbol_nodes.end())
50,908✔
284
            {
285
                it->setString(name);
12,665✔
286
                return;
12,665✔
287
            }
288
        }
50,908✔
289

290
        const auto it = std::ranges::find_if(m_symbol_nodes, [&name](const Node& sym_node) -> bool {
2,579,368✔
291
            return sym_node.string() == name;
2,536,462✔
292
        });
293
        if (it == m_symbol_nodes.end())
42,906✔
294
            m_symbol_nodes.push_back(symbol);
3,504✔
295
    }
92,503✔
296

297
    bool NameResolutionPass::mayBeFromPlugin(const std::string& name) const noexcept
3,500✔
298
    {
3,500✔
299
        std::string splitted = Utils::splitString(name, ':')[0];
3,500✔
300
        const auto it = std::ranges::find_if(
3,500✔
301
            m_plugin_names,
3,500✔
302
            [&splitted](const std::string& plugin) -> bool {
3,944✔
303
                return plugin == splitted;
444✔
304
            });
305
        return it != m_plugin_names.end();
3,500✔
306
    }
3,500✔
307

308
    std::string NameResolutionPass::updateSymbolWithFullyQualifiedName(Node& symbol)
88,102✔
309
    {
88,102✔
310
        auto [allowed, fqn] = m_scope_resolver.canFullyQualifyName(symbol.string());
389,341✔
311

312
        if (m_language_symbols.contains(fqn) && symbol.string() != fqn)
88,102✔
313
        {
314
            throw CodeError(
2✔
315
                fmt::format(
1✔
316
                    "Symbol `{}' was resolved to `{}', which is also a builtin name. Either the symbol or the package it's in needs to be renamed to avoid conflicting with the builtin.",
1✔
317
                    symbol.string(), fqn),
1✔
318
                CodeErrorContext(symbol.filename(), symbol.position()));
1✔
319
        }
320
        if (!allowed)
88,101✔
321
        {
322
            std::string message;
1✔
323
            if (fqn.ends_with("#hidden"))
1✔
324
                message = fmt::format(
2✔
325
                    R"(Unbound variable "{}". However, it exists in a namespace as "{}", did you forget to add it to the symbol list while importing?)",
1✔
326
                    symbol.string(),
1✔
327
                    fqn.substr(0, fqn.find_first_of('#')));
2✔
328
            else
UNCOV
329
                message = fmt::format(R"(Unbound variable "{}". However, it exists in a namespace as "{}", did you forget to prefix it with its namespace?)", symbol.string(), fqn);
×
330

331
            if (m_logger.shouldTrace())
1✔
332
                m_ast.debugPrint(std::cout) << '\n';
×
333

334
            throw CodeError(message, CodeErrorContext(symbol.filename(), symbol.position()));
1✔
335
        }
1✔
336

337
        symbol.setString(fqn);
88,100✔
338
        return fqn;
88,100✔
339
    }
88,104✔
340

341
    void NameResolutionPass::checkForUndefinedSymbol() const
253✔
342
    {
253✔
343
        for (const auto& sym : m_symbol_nodes)
3,753✔
344
        {
345
            const auto& str = sym.string();
3,500✔
346
            const bool is_plugin = mayBeFromPlugin(str);
3,500✔
347

348
            if (!m_defined_symbols.contains(str) && !is_plugin)
3,500✔
349
            {
350
                std::string message;
3✔
351

352
                const std::string suggestion = offerSuggestion(str);
3✔
353
                if (suggestion.empty())
3✔
354
                    message = fmt::format(R"(Unbound variable error "{}" (variable is used but not defined))", str);
1✔
355
                else
356
                {
357
                    const std::string prefix = suggestion.substr(0, suggestion.find_first_of(':'));
2✔
358
                    const std::string note_about_prefix = fmt::format(
4✔
359
                        " You either forgot to import it in the symbol list (eg `(import {} :{})') or need to fully qualify it by adding the namespace",
2✔
360
                        prefix,
361
                        str);
2✔
362
                    const bool add_note = suggestion.ends_with(":" + str);
2✔
363
                    message = fmt::format(R"(Unbound variable error "{}" (did you mean "{}"?{}))", str, suggestion, add_note ? note_about_prefix : "");
2✔
364
                }
2✔
365

366
                throw CodeError(message, CodeErrorContext(sym.filename(), sym.position()));
3✔
367
            }
3✔
368
        }
3,500✔
369
    }
253✔
370

371
    std::string NameResolutionPass::offerSuggestion(const std::string& str) const
3✔
372
    {
3✔
373
        auto iterate = [](const std::string& word, const std::unordered_set<std::string>& dict) -> std::string {
8✔
374
            std::string suggestion;
5✔
375
            // our suggestion shouldn't require more than half the string to change
376
            std::size_t suggestion_distance = word.size() / 2;
5✔
377
            for (const std::string& symbol : dict)
219✔
378
            {
379
                const std::size_t current_distance = Utils::levenshteinDistance(word, symbol);
214✔
380
                if (current_distance <= suggestion_distance)
214✔
381
                {
382
                    suggestion_distance = current_distance;
1✔
383
                    suggestion = symbol;
1✔
384
                }
1✔
385
            }
214✔
386
            return suggestion;
5✔
387
        };
5✔
388

389
        std::string suggestion = iterate(str, m_defined_symbols);
3✔
390
        // look for a suggestion related to language builtins
391
        if (suggestion.empty())
3✔
392
            suggestion = iterate(str, m_language_symbols);
2✔
393
        // look for a suggestion related to a namespace change
394
        if (suggestion.empty())
3✔
395
        {
396
            if (const auto it = std::ranges::find_if(m_defined_symbols, [&str](const std::string& symbol) {
8✔
397
                    return symbol.ends_with(":" + str);
4✔
398
                });
399
                it != m_defined_symbols.end())
3✔
400
                suggestion = *it;
1✔
401
        }
2✔
402

403
        return suggestion;
3✔
404
    }
3✔
405
}
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