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

ArkScript-lang / Ark / 16862576586

10 Aug 2025 02:21PM UTC coverage: 87.685% (+0.8%) from 86.869%
16862576586

push

github

SuperFola
fix(ci): import tests/unittests/testmodule.arkm in artifacts

7633 of 8705 relevant lines covered (87.69%)

125464.93 hits per line

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

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

3
#include <Ark/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) :
606✔
10
        Pass("NameResolution", debug)
303✔
11
    {
606✔
12
        for (const auto& builtin : Builtins::builtins)
19,998✔
13
            m_language_symbols.emplace(builtin.first);
19,695✔
14
        for (auto ope : Language::operators)
7,575✔
15
            m_language_symbols.emplace(ope);
7,272✔
16
        for (auto inst : Language::listInstructions)
3,030✔
17
            m_language_symbols.emplace(inst);
2,727✔
18

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

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

28
        m_ast = ast;
232✔
29
        visit(m_ast, /* register_declarations= */ true);
232✔
30

31
        m_logger.traceEnd();
232✔
32

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

37
        m_logger.traceStart("checkForUndefinedSymbol");
232✔
38
        checkForUndefinedSymbol();
232✔
39
        m_logger.traceEnd();
232✔
40
    }
232✔
41

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

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

54
    void NameResolutionPass::visit(Node& node, const bool register_declarations)
237,758✔
55
    {
237,758✔
56
        switch (node.nodeType())
237,758✔
57
        {
67,610✔
58
            case NodeType::Symbol:
59
            {
60
                const std::string old_name = node.string();
67,610✔
61
                updateSymbolWithFullyQualifiedName(node);
67,610✔
62
                addSymbolNode(node, old_name);
67,608✔
63
                break;
64
            }
71,574✔
65

66
            case NodeType::Field:
67
                for (std::size_t i = 0, end = node.list().size(); i < end; ++i)
11,881✔
68
                {
69
                    Node& child = node.list()[i];
7,925✔
70

71
                    if (i == 0)
7,925✔
72
                    {
73
                        const std::string old_name = child.string();
3,956✔
74
                        // in case of field, no need to check if we can fully qualify names
75
                        child.setString(m_scope_resolver.getFullyQualifiedNameInNearestScope(old_name));
3,956✔
76
                        addSymbolNode(child, old_name);
3,956✔
77
                    }
3,956✔
78
                    else
79
                        addSymbolNode(child);
3,969✔
80
                }
7,925✔
81
                break;
75,122✔
82

83
            case NodeType::List:
84
                if (!node.constList().empty())
71,166✔
85
                {
86
                    if (node.constList()[0].nodeType() == NodeType::Keyword)
71,139✔
87
                        visitKeyword(node, node.constList()[0].keyword(), register_declarations);
33,990✔
88
                    else
89
                    {
90
                        // function calls
91
                        // the UpdateRef function calls kind get a special treatment, like let/mut/set,
92
                        // because we need to check for mutability errors
93
                        if (node.constList().size() > 1 && node.constList()[0].nodeType() == NodeType::Symbol &&
54,977✔
94
                            node.constList()[1].nodeType() == NodeType::Symbol && register_declarations)
32,331✔
95
                        {
96
                            const auto funcname = node.constList()[0].string();
9,117✔
97
                            const auto arg = node.constList()[1].string();
9,117✔
98

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

108
                            // check that we aren't doing a (append! a a) nor a (concat! a a)
109
                            if (funcname == Language::AppendInPlace || funcname == Language::ConcatInPlace)
9,112✔
110
                            {
111
                                for (std::size_t i = 2, end = node.constList().size(); i < end; ++i)
1,069✔
112
                                {
113
                                    if (node.constList()[i].nodeType() == NodeType::Symbol && node.constList()[i].string() == arg)
535✔
114
                                        throw CodeError(
4✔
115
                                            fmt::format("MutabilityError: Can not {} the list `{}' to itself", funcname, arg),
2✔
116
                                            CodeErrorContext(
2✔
117
                                                node.filename(),
2✔
118
                                                node.constList()[1].line(),
2✔
119
                                                node.constList()[1].col(),
2✔
120
                                                arg));
2✔
121
                                }
533✔
122
                            }
532✔
123
                        }
9,117✔
124

125
                        for (auto& child : node.list())
202,535✔
126
                            visit(child, register_declarations);
165,393✔
127
                    }
128
                }
71,132✔
129
                break;
71,326✔
130

131
            case NodeType::Namespace:
132
            {
133
                auto& namespace_ = node.arkNamespace();
167✔
134
                // no need to guard createNewNamespace with an if (register_declarations), we want to keep the namespace node
135
                // (which will get ignored by the compiler, that only uses its AST), so that we can (re)construct the
136
                // scopes correctly
137
                m_scope_resolver.createNewNamespace(namespace_.name, namespace_.with_prefix, namespace_.is_glob, namespace_.symbols);
167✔
138
                StaticScope* scope = m_scope_resolver.currentScope();
167✔
139

140
                visit(*namespace_.ast, /* register_declarations= */ true);
167✔
141
                // dual visit so that we can handle forward references
142
                visit(*namespace_.ast, /* register_declarations= */ false);
167✔
143

144
                // if we had specific symbols to import, check that those exist
145
                if (!namespace_.symbols.empty())
167✔
146
                {
147
                    const auto it = std::ranges::find_if(
156✔
148
                        namespace_.symbols,
78✔
149
                        [&scope](const std::string& sym) -> bool {
237✔
150
                            return !scope->get(sym, true).has_value();
159✔
151
                        });
152

153
                    if (it != namespace_.symbols.end())
78✔
154
                        throw CodeError(
2✔
155
                            fmt::format("ImportError: Can not import symbol {} from {}, as it isn't in the package", *it, namespace_.name),
1✔
156
                            CodeErrorContext(
1✔
157
                                namespace_.ast->filename(),
1✔
158
                                namespace_.ast->line(),
1✔
159
                                namespace_.ast->col(),
1✔
160
                                "import"));
1✔
161
                }
78✔
162

163
                m_scope_resolver.saveNamespaceAndRemove();
166✔
164
                break;
165
            }
95,026✔
166

167
            default:
168
                break;
94,859✔
169
        }
237,694✔
170
    }
237,704✔
171

172
    void NameResolutionPass::visitKeyword(Node& node, const Keyword keyword, const bool register_declarations)
33,990✔
173
    {
33,990✔
174
        switch (keyword)
33,990✔
175
        {
16,195✔
176
            case Keyword::Set:
177
                [[fallthrough]];
178
            case Keyword::Let:
179
                [[fallthrough]];
180
            case Keyword::Mut:
181
                // first, visit the value, then register the symbol
182
                // this allows us to detect things like (let foo (fun (&foo) ()))
183
                if (node.constList().size() > 2)
16,195✔
184
                    visit(node.list()[2], register_declarations);
16,194✔
185
                if (node.constList().size() > 1 && node.constList()[1].nodeType() == NodeType::Symbol)
16,195✔
186
                {
187
                    const std::string& name = node.constList()[1].string();
16,193✔
188
                    if (m_language_symbols.contains(name) && register_declarations)
16,193✔
189
                        throw CodeError(
6✔
190
                            fmt::format("Can not use a reserved identifier ('{}') as a {} name.", name, keyword == Keyword::Let ? "constant" : "variable"),
3✔
191
                            CodeErrorContext(
3✔
192
                                node.filename(),
3✔
193
                                node.constList()[1].line(),
3✔
194
                                node.constList()[1].col(),
3✔
195
                                name));
3✔
196

197
                    if (m_scope_resolver.isInScope(name) && keyword == Keyword::Let && register_declarations)
16,190✔
198
                        throw CodeError(
2✔
199
                            fmt::format("MutabilityError: Can not use 'let' to redefine variable `{}'", name),
1✔
200
                            CodeErrorContext(
1✔
201
                                node.filename(),
1✔
202
                                node.constList()[1].line(),
1✔
203
                                node.constList()[1].col(),
1✔
204
                                name));
1✔
205
                    if (keyword == Keyword::Set && m_scope_resolver.isRegistered(name))
16,189✔
206
                    {
207
                        if (m_scope_resolver.isImmutable(name).value_or(false) && register_declarations)
3,127✔
208
                            throw CodeError(
2✔
209
                                fmt::format("MutabilityError: Can not set the constant `{}' to {}", name, node.constList()[2].repr()),
1✔
210
                                CodeErrorContext(
1✔
211
                                    node.filename(),
1✔
212
                                    node.constList()[1].line(),
1✔
213
                                    node.constList()[1].col(),
1✔
214
                                    name));
1✔
215

216
                        updateSymbolWithFullyQualifiedName(node.list()[1]);
3,126✔
217
                    }
3,126✔
218
                    else if (keyword != Keyword::Set)
13,062✔
219
                    {
220
                        // update the declared variable name to use the fully qualified name
221
                        // this will prevent name conflicts, and handle scope resolution
222
                        const std::string fully_qualified_name = addDefinedSymbol(name, keyword != Keyword::Let);
13,059✔
223
                        if (register_declarations)
13,059✔
224
                            node.list()[1].setString(fully_qualified_name);
6,682✔
225
                    }
13,059✔
226
                }
16,195✔
227
                break;
16,193✔
228

229
            case Keyword::Import:
230
                if (!node.constList().empty())
3✔
231
                    m_plugin_names.push_back(node.constList()[1].constList().back().string());
3✔
232
                break;
1,680✔
233

234
            case Keyword::While:
235
                // create a new scope to track variables
236
                m_scope_resolver.createNew();
1,677✔
237
                for (auto& child : node.list())
6,707✔
238
                    visit(child, register_declarations);
5,030✔
239
                // remove the scope once the loop has been compiled, only we were registering declarations
240
                m_scope_resolver.removeLastScope();
1,677✔
241
                break;
9,160✔
242

243
            case Keyword::Fun:
244
                // create a new scope to track variables
245
                m_scope_resolver.createNew();
7,483✔
246

247
                if (node.constList()[1].nodeType() == NodeType::List)
7,483✔
248
                {
249
                    for (auto& child : node.list()[1].list())
19,076✔
250
                    {
251
                        if (child.nodeType() == NodeType::Capture)
11,594✔
252
                        {
253
                            if (!m_scope_resolver.isRegistered(child.string()) && register_declarations)
622✔
254
                                throw CodeError(
4✔
255
                                    fmt::format("Can not capture `{}' because it is referencing a variable defined in an unreachable scope.", child.string()),
2✔
256
                                    CodeErrorContext(
2✔
257
                                        child.filename(),
2✔
258
                                        child.line(),
2✔
259
                                        child.col(),
2✔
260
                                        child.repr()));
2✔
261

262
                            // save the old unqualified name of the capture, so that we can use it in the
263
                            // ASTLowerer later one
264
                            if (!child.getUnqualifiedName())
620✔
265
                            {
266
                                child.setUnqualifiedName(child.string());
205✔
267
                                m_defined_symbols.emplace(child.string());
205✔
268
                            }
205✔
269
                            // update the declared variable name to use the fully qualified name
270
                            // this will prevent name conflicts, and handle scope resolution
271
                            std::string old_name = child.string();
620✔
272
                            updateSymbolWithFullyQualifiedName(child);
620✔
273
                            // FIXME: addDefinedSymbol(fqn, true); ?
274
                            addDefinedSymbol(old_name, true);
620✔
275
                        }
620✔
276
                        else if (child.nodeType() == NodeType::Symbol)
10,972✔
277
                            addDefinedSymbol(child.string(), /* is_mutable= */ true);
10,972✔
278
                    }
11,594✔
279
                }
7,480✔
280
                if (node.constList().size() > 2)
7,481✔
281
                    visit(node.list()[2], register_declarations);
7,480✔
282

283
                // remove the scope once the function has been compiled, only we were registering declarations
284
                m_scope_resolver.removeLastScope();
7,481✔
285
                break;
16,113✔
286

287
            default:
288
                for (auto& child : node.list())
51,668✔
289
                    visit(child, register_declarations);
43,036✔
290
                break;
8,632✔
291
        }
33,983✔
292
    }
33,990✔
293

294
    void NameResolutionPass::addSymbolNode(const Node& symbol, const std::string& old_name)
75,533✔
295
    {
75,533✔
296
        const std::string& name = symbol.string();
75,533✔
297

298
        // we don't accept builtins/operators as a user symbol
299
        if (m_language_symbols.contains(name))
75,533✔
300
            return;
30,311✔
301

302
        // remove the old name node, to avoid false positive when looking for unbound symbols
303
        if (!old_name.empty())
45,222✔
304
        {
305
            auto it = std::ranges::find_if(m_symbol_nodes, [&old_name, &symbol](const Node& sym_node) -> bool {
3,838,955✔
306
                return sym_node.string() == old_name &&
3,835,583✔
307
                    sym_node.col() == symbol.col() &&
37,881✔
308
                    sym_node.line() == symbol.line() &&
24,177✔
309
                    sym_node.filename() == symbol.filename();
10,862✔
310
            });
311
            if (it != m_symbol_nodes.end())
41,253✔
312
            {
313
                it->setString(name);
10,862✔
314
                return;
10,862✔
315
            }
316
        }
41,253✔
317

318
        const auto it = std::ranges::find_if(m_symbol_nodes, [&name](const Node& sym_node) -> bool {
1,915,084✔
319
            return sym_node.string() == name;
1,880,724✔
320
        });
321
        if (it == m_symbol_nodes.end())
34,360✔
322
            m_symbol_nodes.push_back(symbol);
2,873✔
323
    }
75,533✔
324

325
    bool NameResolutionPass::mayBeFromPlugin(const std::string& name) const noexcept
2,869✔
326
    {
2,869✔
327
        std::string splitted = Utils::splitString(name, ':')[0];
2,869✔
328
        const auto it = std::ranges::find_if(
2,869✔
329
            m_plugin_names,
2,869✔
330
            [&splitted](const std::string& plugin) -> bool {
3,295✔
331
                return plugin == splitted;
426✔
332
            });
333
        return it != m_plugin_names.end();
2,869✔
334
    }
2,869✔
335

336
    std::string NameResolutionPass::updateSymbolWithFullyQualifiedName(Node& symbol)
71,356✔
337
    {
71,356✔
338
        auto [allowed, fqn] = m_scope_resolver.canFullyQualifyName(symbol.string());
315,737✔
339

340
        if (m_language_symbols.contains(fqn) && symbol.string() != fqn)
71,356✔
341
        {
342
            throw CodeError(
×
343
                fmt::format(
×
344
                    "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.",
×
345
                    symbol.string(), fqn),
×
346
                CodeErrorContext(
×
347
                    symbol.filename(),
×
348
                    symbol.line(),
×
349
                    symbol.col(),
×
350
                    symbol.repr()));
×
351
        }
352
        if (!allowed)
71,356✔
353
        {
354
            std::string message;
2✔
355
            if (fqn.ends_with("#hidden"))
2✔
356
                message = fmt::format(
2✔
357
                    R"(Unbound variable "{}". However, it exists in a namespace as "{}", did you forget to add it to the symbol list while importing?)",
1✔
358
                    symbol.string(),
1✔
359
                    fqn.substr(0, fqn.find_first_of('#')));
2✔
360
            else
361
                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);
1✔
362

363
            if (m_logger.shouldTrace())
2✔
364
                m_ast.debugPrint(std::cout) << '\n';
×
365

366
            throw CodeError(
4✔
367
                message,
368
                CodeErrorContext(
2✔
369
                    symbol.filename(),
2✔
370
                    symbol.line(),
2✔
371
                    symbol.col(),
2✔
372
                    symbol.repr()));
2✔
373
        }
2✔
374

375
        symbol.setString(fqn);
71,354✔
376
        return fqn;
71,354✔
377
    }
71,358✔
378

379
    void NameResolutionPass::checkForUndefinedSymbol() const
232✔
380
    {
232✔
381
        for (const auto& sym : m_symbol_nodes)
3,101✔
382
        {
383
            const auto& str = sym.string();
2,869✔
384
            const bool is_plugin = mayBeFromPlugin(str);
2,869✔
385

386
            if (!m_defined_symbols.contains(str) && !is_plugin)
2,869✔
387
            {
388
                std::string message;
2✔
389

390
                const std::string suggestion = offerSuggestion(str);
2✔
391
                if (suggestion.empty())
2✔
392
                    message = fmt::format(R"(Unbound variable error "{}" (variable is used but not defined))", str);
1✔
393
                else
394
                {
395
                    const std::string prefix = suggestion.substr(0, suggestion.find_first_of(':'));
1✔
396
                    const std::string note_about_prefix = fmt::format(
2✔
397
                        " You either forgot to import it in the symbol list (eg `(import {} :{})') or need to fully qualify it by adding the namespace",
1✔
398
                        prefix,
399
                        str);
1✔
400
                    const bool add_note = suggestion.ends_with(":" + str);
1✔
401
                    message = fmt::format(R"(Unbound variable error "{}" (did you mean "{}"?{}))", str, suggestion, add_note ? note_about_prefix : "");
1✔
402
                }
1✔
403

404
                throw CodeError(message, CodeErrorContext(sym.filename(), sym.line(), sym.col(), sym.repr()));
2✔
405
            }
2✔
406
        }
2,869✔
407
    }
232✔
408

409
    std::string NameResolutionPass::offerSuggestion(const std::string& str) const
2✔
410
    {
2✔
411
        auto iterate = [](const std::string& word, const std::unordered_set<std::string>& dict) -> std::string {
5✔
412
            std::string suggestion;
3✔
413
            // our suggestion shouldn't require more than half the string to change
414
            std::size_t suggestion_distance = word.size() / 2;
3✔
415
            for (const std::string& symbol : dict)
109✔
416
            {
417
                const std::size_t current_distance = Utils::levenshteinDistance(word, symbol);
106✔
418
                if (current_distance <= suggestion_distance)
106✔
419
                {
420
                    suggestion_distance = current_distance;
1✔
421
                    suggestion = symbol;
1✔
422
                }
1✔
423
            }
106✔
424
            return suggestion;
3✔
425
        };
3✔
426

427
        std::string suggestion = iterate(str, m_defined_symbols);
2✔
428
        // look for a suggestion related to language builtins
429
        if (suggestion.empty())
2✔
430
            suggestion = iterate(str, m_language_symbols);
1✔
431
        // look for a suggestion related to a namespace change
432
        if (suggestion.empty())
2✔
433
        {
434
            if (const auto it = std::ranges::find_if(m_defined_symbols, [&str](const std::string& symbol) {
4✔
435
                    return symbol.ends_with(":" + str);
2✔
436
                });
437
                it != m_defined_symbols.end())
1✔
438
                suggestion = *it;
×
439
        }
1✔
440

441
        return suggestion;
2✔
442
    }
2✔
443
}
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