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

ArkScript-lang / Ark / 15453345670

04 Jun 2025 09:40PM UTC coverage: 86.735% (+0.1%) from 86.608%
15453345670

push

github

SuperFola
chore: updating fuzzing corpus

2 of 3 new or added lines in 2 files covered. (66.67%)

140 existing lines in 7 files now uncovered.

7258 of 8368 relevant lines covered (86.74%)

120482.26 hits per line

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

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

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

7
namespace Ark::internal
8
{
9
    NameResolutionPass::NameResolutionPass(const unsigned debug) :
578✔
10
        Pass("NameResolution", debug)
289✔
11
    {
578✔
12
        for (const auto& builtin : Builtins::builtins)
17,051✔
13
            m_language_symbols.emplace(builtin.first);
16,762✔
14
        for (auto ope : Language::operators)
7,225✔
15
            m_language_symbols.emplace(ope);
6,936✔
16
        for (auto inst : Language::listInstructions)
2,890✔
17
            m_language_symbols.emplace(inst);
2,601✔
18

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

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

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

31
        m_logger.traceEnd();
218✔
32

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

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

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

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

54
    void NameResolutionPass::visit(Node& node, const bool register_declarations)
219,234✔
55
    {
219,234✔
56
        switch (node.nodeType())
219,234✔
57
        {
59,740✔
58
            case NodeType::Symbol:
59
            {
60
                const std::string old_name = node.string();
59,740✔
61
                updateSymbolWithFullyQualifiedName(node);
59,740✔
62
                addSymbolNode(node, old_name);
59,738✔
63
                break;
64
            }
63,408✔
65

66
            case NodeType::Field:
67
                for (auto& child : node.list())
10,993✔
68
                {
69
                    const std::string old_name = child.string();
7,333✔
70
                    // in case of field, no need to check if we can fully qualify names
71
                    child.setString(m_scope_resolver.getFullyQualifiedNameInNearestScope(old_name));
7,333✔
72
                    addSymbolNode(child, old_name);
7,333✔
73
                }
7,333✔
74
                break;
67,345✔
75

76
            case NodeType::List:
77
                if (!node.constList().empty())
63,685✔
78
                {
79
                    if (node.constList()[0].nodeType() == NodeType::Keyword)
63,658✔
80
                        visitKeyword(node, node.constList()[0].keyword(), register_declarations);
30,640✔
81
                    else
82
                    {
83
                        // function calls
84
                        // the UpdateRef function calls kind get a special treatment, like let/mut/set,
85
                        // because we need to check for mutability errors
86
                        if (node.constList().size() > 1 && node.constList()[0].nodeType() == NodeType::Symbol &&
48,793✔
87
                            node.constList()[1].nodeType() == NodeType::Symbol && register_declarations)
28,808✔
88
                        {
89
                            const auto funcname = node.constList()[0].string();
8,078✔
90
                            const auto arg = node.constList()[1].string();
8,078✔
91

92
                            if (std::ranges::find(Language::UpdateRef, funcname) != Language::UpdateRef.end() && m_scope_resolver.isImmutable(arg).value_or(false))
8,078✔
93
                                throw CodeError(
10✔
94
                                    fmt::format("MutabilityError: Can not modify the constant list `{}' using `{}'", arg, funcname),
5✔
95
                                    CodeErrorContext(
5✔
96
                                        node.filename(),
5✔
97
                                        node.constList()[1].line(),
5✔
98
                                        node.constList()[1].col(),
5✔
99
                                        arg));
5✔
100

101
                            // check that we aren't doing a (append! a a) nor a (concat! a a)
102
                            if (funcname == Language::AppendInPlace || funcname == Language::ConcatInPlace)
8,073✔
103
                            {
104
                                for (std::size_t i = 2, end = node.constList().size(); i < end; ++i)
1,019✔
105
                                {
106
                                    if (node.constList()[i].nodeType() == NodeType::Symbol && node.constList()[i].string() == arg)
510✔
107
                                        throw CodeError(
4✔
108
                                            fmt::format("MutabilityError: Can not {} the list `{}' to itself", funcname, arg),
2✔
109
                                            CodeErrorContext(
2✔
110
                                                node.filename(),
2✔
111
                                                node.constList()[1].line(),
2✔
112
                                                node.constList()[1].col(),
2✔
113
                                                arg));
2✔
114
                                }
508✔
115
                            }
507✔
116
                        }
8,078✔
117

118
                        for (auto& child : node.list())
187,133✔
119
                            visit(child, register_declarations);
154,122✔
120
                    }
121
                }
63,651✔
122
                break;
63,834✔
123

124
            case NodeType::Namespace:
125
            {
126
                auto& namespace_ = node.arkNamespace();
156✔
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);
156✔
131
                StaticScope* scope = m_scope_resolver.currentScope();
156✔
132

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

137
                // if we had specific symbols to import, check that those exist
138
                if (!namespace_.symbols.empty())
156✔
139
                {
140
                    for (const auto& sym : namespace_.symbols)
223✔
141
                    {
142
                        if (!scope->get(sym, true).has_value())
151✔
143
                            throw CodeError(
2✔
144
                                fmt::format("ImportError: Can not import symbol {} from {}, as it isn't in the package", sym, namespace_.name),
1✔
145
                                CodeErrorContext(
1✔
146
                                    namespace_.ast->filename(),
1✔
147
                                    namespace_.ast->line(),
1✔
148
                                    namespace_.ast->col(),
1✔
149
                                    "import"));
1✔
150
                    }
151✔
151
                }
71✔
152

153
                m_scope_resolver.saveNamespaceAndRemove();
155✔
154
                break;
155
            }
92,149✔
156

157
            default:
158
                break;
91,993✔
159
        }
219,170✔
160
    }
219,180✔
161

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

187
                    if (m_scope_resolver.isInScope(name) && keyword == Keyword::Let && register_declarations)
14,617✔
188
                        throw CodeError(
2✔
189
                            fmt::format("MutabilityError: Can not use 'let' to redefine variable `{}'", name),
1✔
190
                            CodeErrorContext(
1✔
191
                                node.filename(),
1✔
192
                                node.constList()[1].line(),
1✔
193
                                node.constList()[1].col(),
1✔
194
                                name));
1✔
195
                    if (keyword == Keyword::Set && m_scope_resolver.isRegistered(name))
14,616✔
196
                    {
197
                        if (m_scope_resolver.isImmutable(name).value_or(false) && register_declarations)
2,809✔
198
                            throw CodeError(
2✔
199
                                fmt::format("MutabilityError: Can not set the constant `{}' to {}", name, node.constList()[2].repr()),
1✔
200
                                CodeErrorContext(
1✔
201
                                    node.filename(),
1✔
202
                                    node.constList()[1].line(),
1✔
203
                                    node.constList()[1].col(),
1✔
204
                                    name));
1✔
205

206
                        updateSymbolWithFullyQualifiedName(node.list()[1]);
2,808✔
207
                    }
2,808✔
208
                    else if (keyword != Keyword::Set)
11,807✔
209
                    {
210
                        // update the declared variable name to use the fully qualified name
211
                        // this will prevent name conflicts, and handle scope resolution
212
                        const std::string fully_qualified_name = addDefinedSymbol(name, keyword != Keyword::Let);
11,804✔
213
                        if (register_declarations)
11,804✔
214
                            node.list()[1].setString(fully_qualified_name);
6,048✔
215
                    }
11,804✔
216
                }
14,622✔
217
                break;
14,618✔
218

219
            case Keyword::Import:
220
                if (!node.constList().empty())
1✔
221
                    m_plugin_names.push_back(node.constList()[1].constList().back().string());
1✔
222
                break;
1,491✔
223

224
            case Keyword::While:
225
                // create a new scope to track variables
226
                m_scope_resolver.createNew();
1,490✔
227
                for (auto& child : node.list())
5,959✔
228
                    visit(child, register_declarations);
4,469✔
229
                // remove the scope once the loop has been compiled, only we were registering declarations
230
                m_scope_resolver.removeLastScope();
1,490✔
231
                break;
8,279✔
232

233
            case Keyword::Fun:
234
                // create a new scope to track variables
235
                m_scope_resolver.createNew();
6,789✔
236

237
                if (node.constList()[1].nodeType() == NodeType::List)
6,789✔
238
                {
239
                    for (auto& child : node.list()[1].list())
17,097✔
240
                    {
241
                        if (child.nodeType() == NodeType::Capture)
10,309✔
242
                        {
243
                            if (!m_scope_resolver.isRegistered(child.string()) && register_declarations)
606✔
244
                                throw CodeError(
4✔
245
                                    fmt::format("Can not capture `{}' because it is referencing a variable defined in an unreachable scope.", child.string()),
2✔
246
                                    CodeErrorContext(
2✔
247
                                        child.filename(),
2✔
248
                                        child.line(),
2✔
249
                                        child.col(),
2✔
250
                                        child.repr()));
2✔
251

252
                            // update the declared variable name to use the fully qualified name
253
                            // this will prevent name conflicts, and handle scope resolution
254
                            std::string fqn = updateSymbolWithFullyQualifiedName(child);
604✔
255
                            addDefinedSymbol(fqn, true);
604✔
256
                        }
604✔
257
                        else if (child.nodeType() == NodeType::Symbol)
9,703✔
258
                            addDefinedSymbol(child.string(), /* is_mutable= */ true);
9,703✔
259
                    }
10,309✔
260
                }
6,786✔
261
                if (node.constList().size() > 2)
6,787✔
262
                    visit(node.list()[2], register_declarations);
6,786✔
263

264
                // remove the scope once the function has been compiled, only we were registering declarations
265
                m_scope_resolver.removeLastScope();
6,787✔
266
                break;
14,525✔
267

268
            default:
269
                for (auto& child : node.list())
46,385✔
270
                    visit(child, register_declarations);
38,647✔
271
                break;
7,738✔
272
        }
30,633✔
273
    }
30,640✔
274

275
    void NameResolutionPass::addSymbolNode(const Node& symbol, const std::string& old_name)
67,071✔
276
    {
67,071✔
277
        const std::string& name = symbol.string();
67,071✔
278

279
        // we don't accept builtins/operators as a user symbol
280
        if (m_language_symbols.contains(name))
67,071✔
281
            return;
27,306✔
282

283
        // remove the old name node, to avoid false positive when looking for unbound symbols
284
        if (!old_name.empty())
39,765✔
285
        {
286
            auto it = std::ranges::find_if(m_symbol_nodes, [&old_name, &symbol](const Node& sym_node) -> bool {
3,293,258✔
287
                return sym_node.string() == old_name &&
3,290,090✔
288
                    sym_node.col() == symbol.col() &&
36,597✔
289
                    sym_node.line() == symbol.line() &&
27,134✔
290
                    sym_node.filename() == symbol.filename();
12,300✔
291
            });
292
            if (it != m_symbol_nodes.end())
39,765✔
293
            {
294
                it->setString(name);
12,300✔
295
                return;
12,300✔
296
            }
297
        }
39,765✔
298

299
        const auto it = std::ranges::find_if(m_symbol_nodes, [&name](const Node& sym_node) -> bool {
1,294,125✔
300
            return sym_node.string() == name;
1,266,660✔
301
        });
302
        if (it == m_symbol_nodes.end())
27,465✔
303
            m_symbol_nodes.push_back(symbol);
2,636✔
304
    }
67,071✔
305

306
    bool NameResolutionPass::mayBeFromPlugin(const std::string& name) const noexcept
2,632✔
307
    {
2,632✔
308
        std::string splitted = Utils::splitString(name, ':')[0];
2,632✔
309
        const auto it = std::ranges::find_if(
2,632✔
310
            m_plugin_names,
2,632✔
311
            [&splitted](const std::string& plugin) -> bool {
2,633✔
312
                return plugin == splitted;
1✔
313
            });
314
        return it != m_plugin_names.end();
2,632✔
315
    }
2,632✔
316

317
    std::string NameResolutionPass::updateSymbolWithFullyQualifiedName(Node& symbol)
63,152✔
318
    {
63,152✔
319
        auto [allowed, fqn] = m_scope_resolver.canFullyQualifyName(symbol.string());
279,916✔
320

321
        if (m_language_symbols.contains(fqn) && symbol.string() != fqn)
63,152✔
322
        {
UNCOV
323
            throw CodeError(
×
UNCOV
324
                fmt::format(
×
UNCOV
325
                    "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.",
×
UNCOV
326
                    symbol.string(), fqn),
×
UNCOV
327
                CodeErrorContext(
×
UNCOV
328
                    symbol.filename(),
×
UNCOV
329
                    symbol.line(),
×
UNCOV
330
                    symbol.col(),
×
UNCOV
331
                    symbol.repr()));
×
332
        }
333
        if (!allowed)
63,152✔
334
        {
335
            std::string message;
2✔
336
            if (fqn.ends_with("#hidden"))
2✔
337
                message = fmt::format(
2✔
338
                    R"(Unbound variable "{}". However, it exists in a namespace as "{}", did you forget to add it to the symbol list while importing?)",
1✔
339
                    symbol.string(),
1✔
340
                    fqn.substr(0, fqn.find_first_of('#')));
2✔
341
            else
342
                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✔
343

344
            if (m_logger.shouldTrace())
2✔
345
                m_ast.debugPrint(std::cout) << '\n';
×
346

347
            throw CodeError(
4✔
348
                message,
349
                CodeErrorContext(
2✔
350
                    symbol.filename(),
2✔
351
                    symbol.line(),
2✔
352
                    symbol.col(),
2✔
353
                    symbol.repr()));
2✔
354
        }
2✔
355

356
        symbol.setString(fqn);
63,150✔
357
        return fqn;
63,150✔
358
    }
63,154✔
359

360
    void NameResolutionPass::checkForUndefinedSymbol() const
218✔
361
    {
218✔
362
        for (const auto& sym : m_symbol_nodes)
2,850✔
363
        {
364
            const auto& str = sym.string();
2,632✔
365
            const bool is_plugin = mayBeFromPlugin(str);
2,632✔
366

367
            if (!m_defined_symbols.contains(str) && !is_plugin)
2,632✔
368
            {
369
                std::string message;
2✔
370

371
                const std::string suggestion = offerSuggestion(str);
2✔
372
                if (suggestion.empty())
2✔
373
                    message = fmt::format(R"(Unbound variable error "{}" (variable is used but not defined))", str);
1✔
374
                else
375
                {
376
                    const std::string prefix = suggestion.substr(0, suggestion.find_first_of(':'));
1✔
377
                    const std::string note_about_prefix = fmt::format(
2✔
378
                        " You either forgot to import it in the symbol list (eg `(import {} :{})') or need to fully qualify it by adding the namespace",
1✔
379
                        prefix,
380
                        str);
1✔
381
                    const bool add_note = suggestion.ends_with(":" + str);
1✔
382
                    message = fmt::format(R"(Unbound variable error "{}" (did you mean "{}"?{}))", str, suggestion, add_note ? note_about_prefix : "");
1✔
383
                }
1✔
384

385
                throw CodeError(message, CodeErrorContext(sym.filename(), sym.line(), sym.col(), sym.repr()));
2✔
386
            }
2✔
387
        }
2,632✔
388
    }
218✔
389

390
    std::string NameResolutionPass::offerSuggestion(const std::string& str) const
2✔
391
    {
2✔
392
        auto iterate = [](const std::string& word, const std::unordered_set<std::string>& dict) -> std::string {
5✔
393
            std::string suggestion;
3✔
394
            // our suggestion shouldn't require more than half the string to change
395
            std::size_t suggestion_distance = word.size() / 2;
3✔
396
            for (const std::string& symbol : dict)
102✔
397
            {
398
                const std::size_t current_distance = Utils::levenshteinDistance(word, symbol);
99✔
399
                if (current_distance <= suggestion_distance)
99✔
400
                {
401
                    suggestion_distance = current_distance;
1✔
402
                    suggestion = symbol;
1✔
403
                }
1✔
404
            }
99✔
405
            return suggestion;
3✔
406
        };
3✔
407

408
        std::string suggestion = iterate(str, m_defined_symbols);
2✔
409
        // look for a suggestion related to language builtins
410
        if (suggestion.empty())
2✔
411
            suggestion = iterate(str, m_language_symbols);
1✔
412
        // look for a suggestion related to a namespace change
413
        if (suggestion.empty())
2✔
414
        {
415
            if (const auto it = std::ranges::find_if(m_defined_symbols, [&str](const std::string& symbol) {
4✔
416
                    return symbol.ends_with(":" + str);
2✔
417
                });
418
                it != m_defined_symbols.end())
1✔
419
                suggestion = *it;
×
420
        }
1✔
421

422
        return suggestion;
2✔
423
    }
2✔
424
}
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