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

ArkScript-lang / Ark / 28841712321

07 Jul 2026 04:32AM UTC coverage: 94.381% (+0.03%) from 94.349%
28841712321

push

github

SuperFola
chore(ci): ignore leaks when copying a ClosureScope

10313 of 10927 relevant lines covered (94.38%)

991593.76 hits per line

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

99.27
/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) :
1,096✔
10
        Pass("NameResolution", debug)
548✔
11
    {
1,096✔
12
        for (const auto& builtin : Builtins::builtins)
63,568✔
13
            m_language_symbols.emplace(builtin.first);
63,020✔
14
        for (auto ope : Language::operators)
13,700✔
15
            m_language_symbols.emplace(ope);
13,152✔
16
        for (auto inst : Language::listInstructions)
5,480✔
17
            m_language_symbols.emplace(inst);
4,932✔
18

19
        m_language_symbols.emplace(Language::And);
548✔
20
        m_language_symbols.emplace(Language::Or);
548✔
21
        m_language_symbols.emplace(Language::SysArgs);
548✔
22
        m_language_symbols.emplace(Language::SysProgramName);
548✔
23
        m_language_symbols.emplace(Language::SysVersion);
548✔
24
        m_language_symbols.emplace(Language::Apply);
548✔
25
    }
548✔
26

27
    void NameResolutionPass::process(const Node& ast)
458✔
28
    {
458✔
29
        m_logger.traceStart("process");
458✔
30

31
        m_ast = ast;
458✔
32
        visit(m_ast, /* register_declarations= */ true);
458✔
33

34
        m_logger.traceEnd();
458✔
35

36
        m_logger.debug("AST after name resolution");
458✔
37
        if (m_logger.shouldDebug())
458✔
38
            m_ast.debugPrint(std::cout) << '\n';
1✔
39

40
        m_logger.traceStart("checkForUndefinedSymbol");
458✔
41
        checkForUndefinedSymbol();
458✔
42
        m_logger.traceEnd();
458✔
43
    }
458✔
44

45
    const Node& NameResolutionPass::ast() const noexcept
453✔
46
    {
453✔
47
        return m_ast;
453✔
48
    }
49

50
    std::string NameResolutionPass::addDefinedSymbol(const std::string& sym, const bool is_mutable)
95,099✔
51
    {
95,099✔
52
        const std::string fully_qualified_name = m_scope_resolver.registerInCurrent(sym, is_mutable);
95,099✔
53
        m_defined_symbols.emplace(fully_qualified_name);
95,099✔
54
        return fully_qualified_name;
95,099✔
55
    }
95,099✔
56

57
    void NameResolutionPass::visit(Node& node, const bool register_declarations)
839,071✔
58
    {
839,071✔
59
        switch (node.nodeType())
839,071✔
60
        {
316,302✔
61
            case NodeType::Symbol:
62
            {
63
                const std::string old_name = node.string();
316,302✔
64
                updateSymbolWithFullyQualifiedName(node);
316,302✔
65
                addSymbolNode(node, old_name);
316,300✔
66
                break;
67
            }
324,058✔
68

69
            case NodeType::Field:
70
                for (std::size_t i = 0, end = node.list().size(); i < end; ++i)
23,265✔
71
                {
72
                    Node& child = node.list()[i];
15,517✔
73

74
                    if (i == 0)
15,517✔
75
                    {
76
                        const std::string old_name = child.string();
7,748✔
77
                        // in case of field, no need to check if we can fully qualify names
78
                        child.setString(m_scope_resolver.getFullyQualifiedNameInNearestScope(old_name));
7,748✔
79
                        addSymbolNode(child, old_name);
7,748✔
80
                    }
7,748✔
81
                    else
82
                        addSymbolNode(child);
7,769✔
83
                }
15,517✔
84
                break;
312,970✔
85

86
            case NodeType::List:
87
                if (!node.constList().empty())
305,222✔
88
                {
89
                    if (node.constList()[0].nodeType() == NodeType::Keyword)
305,195✔
90
                        visitKeyword(node, node.constList()[0].keyword(), register_declarations);
151,338✔
91
                    else
92
                    {
93
                        // function calls
94
                        // the UpdateRef function calls kind get a special treatment, like let/mut/set,
95
                        // because we need to check for mutability errors
96
                        if (node.constList().size() > 1 && node.constList()[0].nodeType() == NodeType::Symbol &&
244,252✔
97
                            node.constList()[1].nodeType() == NodeType::Symbol && register_declarations)
140,757✔
98
                        {
99
                            const auto funcname = node.constList()[0].string();
45,656✔
100
                            const auto arg = node.constList()[1].string();
45,656✔
101

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

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

120
                        for (auto& child : node.list())
667,594✔
121
                            visit(child, register_declarations);
513,744✔
122
                    }
123
                }
305,188✔
124
                break;
305,548✔
125

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

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

139
                // if we had specific symbols to import, check that those exist
140
                if (!namespace_.symbols.empty())
333✔
141
                {
142
                    const auto it = std::ranges::find_if(
210✔
143
                        namespace_.symbols,
70✔
144
                        [&scope, &namespace_](const std::string& sym) -> bool {
231✔
145
                            return !scope->get(sym, namespace_.name, true).has_value();
161✔
146
                        });
147

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

154
                m_scope_resolver.saveNamespaceAndRemove();
332✔
155
                break;
156
            }
209,799✔
157

158
            default:
159
                break;
209,466✔
160
        }
839,007✔
161
    }
839,017✔
162

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

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

195
                        updateSymbolWithFullyQualifiedName(node.list()[1]);
22,813✔
196
                    }
22,813✔
197
                    else if (keyword != Keyword::Set)
52,446✔
198
                    {
199
                        // update the declared variable name to use the fully qualified name
200
                        // this will prevent name conflicts, and handle scope resolution
201
                        const std::string fully_qualified_name = addDefinedSymbol(name, keyword != Keyword::Let);
52,443✔
202
                        if (register_declarations)
52,443✔
203
                            node.list()[1].setString(fully_qualified_name);
26,459✔
204
                    }
52,443✔
205
                }
75,266✔
206
                break;
75,264✔
207

208
            case Keyword::Import:
209
                if (!node.constList().empty())
3✔
210
                    m_plugin_names.push_back(node.constList()[1].constList().back().string());
3✔
211
                break;
8,030✔
212

213
            case Keyword::While:
214
                // create a new scope to track variables
215
                m_scope_resolver.createNew();
8,027✔
216
                for (auto& child : node.list())
32,107✔
217
                    visit(child, register_declarations);
24,080✔
218
                // remove the scope once the loop has been compiled, only we were registering declarations
219
                m_scope_resolver.removeLastScope();
8,027✔
220
                break;
33,674✔
221

222
            case Keyword::Fun:
223
                // create a new scope to track variables
224
                m_scope_resolver.createNew();
25,647✔
225

226
                if (node.constList()[1].nodeType() == NodeType::List)
25,647✔
227
                {
228
                    for (auto& child : node.list()[1].list())
67,246✔
229
                    {
230
                        if (child.nodeType() == NodeType::Capture)
41,600✔
231
                        {
232
                            if (!m_scope_resolver.isRegistered(child.string()) && register_declarations)
924✔
233
                                throw CodeError(
4✔
234
                                    fmt::format("Can not capture `{}' because it is referencing a variable defined in an unreachable scope.", child.string()),
2✔
235
                                    CodeErrorContext(child.filename(), child.position()));
2✔
236

237
                            // save the old unqualified name of the capture, so that we can use it in the
238
                            // ASTLowerer later one
239
                            if (!child.getUnqualifiedName())
922✔
240
                            {
241
                                child.setUnqualifiedName(child.string());
293✔
242
                                m_defined_symbols.emplace(child.string());
293✔
243
                            }
293✔
244
                            // update the declared variable name to use the fully qualified name
245
                            // this will prevent name conflicts, and handle scope resolution
246
                            std::string old_name = child.string();
922✔
247
                            updateSymbolWithFullyQualifiedName(child);
922✔
248
                            addDefinedSymbol(old_name, true);
922✔
249
                        }
922✔
250
                        else if (child.nodeType() == NodeType::Symbol || child.nodeType() == NodeType::RefArg)
40,676✔
251
                            addDefinedSymbol(child.string(), /* is_mutable= */ false);
38,730✔
252
                        else if (child.nodeType() == NodeType::MutArg)
1,946✔
253
                            addDefinedSymbol(child.string(), /* is_mutable= */ true);
1,946✔
254
                    }
41,600✔
255
                }
25,644✔
256
                if (node.constList().size() > 2)
25,645✔
257
                    visit(node.list()[2], register_declarations);
25,644✔
258

259
                // remove the scope once the function has been compiled, only we were registering declarations
260
                m_scope_resolver.removeLastScope();
25,645✔
261
                break;
68,040✔
262

263
            default:
264
                for (auto& child : node.list())
241,569✔
265
                    visit(child, register_declarations);
199,174✔
266
                break;
42,395✔
267
        }
151,331✔
268
    }
151,338✔
269

270
    void NameResolutionPass::addSymbolNode(const Node& symbol, const std::string& old_name)
331,817✔
271
    {
331,817✔
272
        const std::string& name = symbol.string();
331,817✔
273

274
        // we don't accept builtins/operators as a user symbol
275
        if (m_language_symbols.contains(name))
331,817✔
276
            return;
129,329✔
277

278
        // remove the old name node, to avoid false positive when looking for unbound symbols
279
        if (!old_name.empty())
202,488✔
280
        {
281
            auto it = std::ranges::find_if(m_symbol_nodes, [&old_name, &symbol](const Node& sym_node) -> bool {
36,255,731✔
282
                return sym_node.string() == old_name &&
36,244,028✔
283
                    sym_node.position().start == symbol.position().start &&
219,771✔
284
                    sym_node.filename() == symbol.filename();
36,755✔
285
            });
286
            if (it != m_symbol_nodes.end())
194,719✔
287
            {
288
                it->setString(name);
36,751✔
289
                return;
36,751✔
290
            }
291
        }
194,719✔
292

293
        const auto it = std::ranges::find_if(m_symbol_nodes, [&name](const Node& sym_node) -> bool {
14,502,024✔
294
            return sym_node.string() == name;
14,336,287✔
295
        });
296
        if (it == m_symbol_nodes.end())
165,737✔
297
            m_symbol_nodes.push_back(symbol);
8,727✔
298
    }
331,817✔
299

300
    bool NameResolutionPass::mayBeFromPlugin(const std::string& name) const noexcept
8,723✔
301
    {
8,723✔
302
        std::string splitted = Utils::splitString(name, ':')[0];
8,723✔
303
        const auto it = std::ranges::find_if(
8,723✔
304
            m_plugin_names,
8,723✔
305
            [&splitted](const std::string& plugin) -> bool {
9,419✔
306
                return plugin == splitted;
696✔
307
            });
308
        return it != m_plugin_names.end();
8,723✔
309
    }
8,723✔
310

311
    std::string NameResolutionPass::updateSymbolWithFullyQualifiedName(Node& symbol)
340,037✔
312
    {
340,037✔
313
        auto [allowed, fqn] = m_scope_resolver.canFullyQualifyName(symbol.string());
1,489,478✔
314

315
        if (m_language_symbols.contains(fqn) && symbol.string() != fqn)
340,037✔
316
        {
317
            throw CodeError(
2✔
318
                fmt::format(
1✔
319
                    "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✔
320
                    symbol.string(), fqn),
1✔
321
                CodeErrorContext(symbol.filename(), symbol.position()));
1✔
322
        }
323
        if (!allowed)
340,036✔
324
        {
325
            std::string message;
1✔
326
            if (fqn.ends_with(HiddenSymbolSuffix))
1✔
327
                message = fmt::format(
2✔
328
                    R"(Unbound variable "{}". However, it exists in a namespace as "{}", did you forget to add it to the symbol list while importing?)",
1✔
329
                    symbol.string(),
1✔
330
                    fqn.substr(0, fqn.find_first_of('#')));
2✔
331
            else
332
                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);
×
333

334
            if (m_logger.shouldTrace())
1✔
335
                m_ast.debugPrint(std::cout) << '\n';
×
336

337
            throw CodeError(message, CodeErrorContext(symbol.filename(), symbol.position()));
1✔
338
        }
1✔
339

340
        symbol.setString(fqn);
340,035✔
341
        return fqn;
340,035✔
342
    }
340,039✔
343

344
    void NameResolutionPass::checkForUndefinedSymbol() const
458✔
345
    {
458✔
346
        for (const auto& sym : m_symbol_nodes)
9,181✔
347
        {
348
            const auto& str = sym.string();
8,723✔
349
            const bool is_plugin = mayBeFromPlugin(str);
8,723✔
350

351
            if (!m_defined_symbols.contains(str) && !is_plugin)
8,723✔
352
            {
353
                std::string message;
5✔
354

355
                const std::string suggestion = offerSuggestion(str);
5✔
356
                if (suggestion.empty())
5✔
357
                    message = fmt::format(R"(Unbound variable "{}" (variable is used but not defined))", str);
2✔
358
                else if (suggestion.ends_with(HiddenSymbolSuffix))
3✔
359
                {
360
                    const std::string prefix = suggestion.substr(0, suggestion.find_first_of(':'));
1✔
361
                    const std::string suffix = suggestion.substr(suggestion.find_first_of(':') + 1, suggestion.size() - prefix.size() - 1 - HiddenSymbolSuffix.size());
1✔
362
                    message = fmt::format(R"(Unbound variable "{0}". Did you forget to add '{1}' when importing '{2}', eg `(import {2} :{1})'? (symbol is visible but not available))", str, suffix, prefix);
1✔
363
                }
1✔
364
                else
365
                {
366
                    const std::string prefix = suggestion.substr(0, suggestion.find_first_of(':'));
2✔
367
                    const std::string note_about_prefix = fmt::format(
4✔
368
                        " You either forgot to import it in the symbol list (eg `(import {} :{})') or need to fully qualify it by adding the namespace",
2✔
369
                        prefix,
370
                        str);
2✔
371
                    const bool add_note = suggestion.ends_with(":" + str);
2✔
372
                    message = fmt::format(R"(Unbound variable "{}" (did you mean "{}"?{}))", str, suggestion, add_note ? note_about_prefix : "");
2✔
373
                }
2✔
374

375
                throw CodeError(message, CodeErrorContext(sym.filename(), sym.position()));
5✔
376
            }
5✔
377
        }
8,723✔
378
    }
458✔
379

380
    std::string NameResolutionPass::offerSuggestion(const std::string& str) const
5✔
381
    {
5✔
382
        auto iterate = [](const std::string& word, const std::unordered_set<std::string>& dict) -> std::string {
13✔
383
            std::string suggestion;
8✔
384
            // our suggestion shouldn't require more than half the string to change
385
            std::size_t suggestion_distance = word.size() / 2;
8✔
386
            for (const std::string& symbol : dict)
417✔
387
            {
388
                if (symbol.starts_with(word) && symbol.ends_with(HiddenSymbolSuffix))
409✔
389
                {
390
                    suggestion = symbol;
1✔
391
                    break;
1✔
392
                }
393

394
                const std::size_t current_distance = Utils::levenshteinDistance(word, symbol);
408✔
395
                if (current_distance <= suggestion_distance)
408✔
396
                {
397
                    suggestion_distance = current_distance;
1✔
398
                    suggestion = symbol;
1✔
399
                }
1✔
400
            }
409✔
401
            return suggestion;
8✔
402
        };
8✔
403

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

418
        return suggestion;
5✔
419
    }
5✔
420
}
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