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

ArkScript-lang / Ark / 13228502355

09 Feb 2025 06:30PM UTC coverage: 78.314% (+0.01%) from 78.302%
13228502355

push

github

SuperFola
feat(name resolver): fixing an import shadowing bug with symbol imports and hidden imports

12 of 13 new or added lines in 3 files covered. (92.31%)

5789 of 7392 relevant lines covered (78.31%)

15114.49 hits per line

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

97.86
/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) :
310✔
10
        Pass("NameResolution", debug),
155✔
11
        m_ast()
155✔
12
    {
310✔
13
        for (const auto& builtin : Builtins::builtins)
8,835✔
14
            m_language_symbols.emplace(builtin.first);
8,680✔
15
        for (auto ope : Language::operators)
3,875✔
16
            m_language_symbols.emplace(ope);
3,720✔
17
        for (auto inst : Language::listInstructions)
1,550✔
18
            m_language_symbols.emplace(inst);
1,395✔
19

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

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

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

32
        m_logger.traceEnd();
87✔
33

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

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

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

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

55
    void NameResolutionPass::visit(Node& node, const bool register_declarations)
90,446✔
56
    {
90,446✔
57
        switch (node.nodeType())
90,446✔
58
        {
8,260✔
59
            case NodeType::Symbol:
60
            {
61
                const std::string old_name = node.string();
8,260✔
62
                updateSymbolWithFullyQualifiedName(node);
8,260✔
63
                addSymbolNode(node, old_name);
8,257✔
64
                break;
65
            }
9,005✔
66

67
            case NodeType::Field:
68
                for (auto& child : node.list())
2,224✔
69
                {
70
                    const std::string old_name = child.string();
1,487✔
71
                    // in case of field, no need to check if we can fully qualify names
72
                    child.setString(m_scope_resolver.getFullyQualifiedNameInNearestScope(old_name));
1,487✔
73
                    addSymbolNode(child, old_name);
1,487✔
74
                }
1,487✔
75
                break;
9,923✔
76

77
            case NodeType::List:
78
                if (!node.constList().empty())
9,186✔
79
                {
80
                    if (node.constList()[0].nodeType() == NodeType::Keyword)
9,161✔
81
                        visitKeyword(node, node.constList()[0].keyword(), register_declarations);
3,289✔
82
                    else
83
                    {
84
                        // function calls
85
                        // the UpdateRef function calls kind get a special treatment, like let/mut/set,
86
                        // because we need to check for mutability errors
87
                        if (node.constList().size() > 1 && node.constList()[0].nodeType() == NodeType::Symbol &&
7,463✔
88
                            node.constList()[1].nodeType() == NodeType::Symbol && register_declarations)
4,155✔
89
                        {
90
                            const auto funcname = node.constList()[0].string();
831✔
91
                            const auto arg = node.constList()[1].string();
831✔
92

93
                            if (std::ranges::find(Language::UpdateRef, funcname) != Language::UpdateRef.end() && m_scope_resolver.isImmutable(arg).value_or(false))
831✔
94
                                throw CodeError(
10✔
95
                                    fmt::format("MutabilityError: Can not modify the constant list `{}' using `{}'", arg, funcname),
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)
826✔
103
                            {
104
                                for (std::size_t i = 2, end = node.constList().size(); i < end; ++i)
72✔
105
                                {
106
                                    if (node.constList()[i].nodeType() == NodeType::Symbol && node.constList()[i].string() == arg)
36✔
107
                                        throw CodeError(
4✔
108
                                            fmt::format("MutabilityError: Can not {} the list `{}' to itself", funcname, arg),
2✔
109
                                            node.filename(),
2✔
110
                                            node.constList()[1].line(),
2✔
111
                                            node.constList()[1].col(),
2✔
112
                                            arg);
2✔
113
                                }
34✔
114
                            }
34✔
115
                        }
831✔
116

117
                        for (auto& child : node.list())
87,528✔
118
                            visit(child, register_declarations);
81,663✔
119
                    }
120
                }
9,154✔
121
                break;
9,215✔
122

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

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

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

151
                m_scope_resolver.saveNamespaceAndRemove();
35✔
152
                break;
153
            }
72,263✔
154

155
            default:
156
                break;
72,227✔
157
        }
90,385✔
158
    }
90,396✔
159

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

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

201
                        updateSymbolWithFullyQualifiedName(node.list()[1]);
209✔
202
                    }
209✔
203
                    else if (keyword != Keyword::Set)
1,080✔
204
                    {
205
                        // update the declared variable name to use the fully qualified name
206
                        // this will prevent name conflicts, and handle scope resolution
207
                        const std::string fully_qualified_name = addDefinedSymbol(name, keyword != Keyword::Let);
1,077✔
208
                        if (register_declarations)
1,077✔
209
                            node.list()[1].setString(fully_qualified_name);
592✔
210
                    }
1,077✔
211
                }
1,295✔
212
                break;
1,291✔
213

214
            case Keyword::Import:
215
                if (!node.constList().empty())
×
216
                    m_plugin_names.push_back(node.constList()[1].constList().back().string());
×
217
                break;
103✔
218

219
            case Keyword::While:
220
                // create a new scope to track variables
221
                m_scope_resolver.createNew();
103✔
222
                for (auto& child : node.list())
411✔
223
                    visit(child, register_declarations);
308✔
224
                // remove the scope once the loop has been compiled, only we were registering declarations
225
                m_scope_resolver.removeLastScope();
103✔
226
                break;
582✔
227

228
            case Keyword::Fun:
229
                // create a new scope to track variables
230
                m_scope_resolver.createNew();
479✔
231

232
                if (node.constList()[1].nodeType() == NodeType::List)
479✔
233
                {
234
                    for (auto& child : node.list()[1].list())
1,228✔
235
                    {
236
                        if (child.nodeType() == NodeType::Capture)
750✔
237
                        {
238
                            if (!m_scope_resolver.isRegistered(child.string()) && register_declarations)
145✔
239
                                throw CodeError(
4✔
240
                                    fmt::format("Can not capture {} because it is referencing a variable defined in an unreachable scope.", child.string()),
2✔
241
                                    child.filename(),
2✔
242
                                    child.line(),
2✔
243
                                    child.col(),
2✔
244
                                    child.repr());
2✔
245

246
                            // update the declared variable name to use the fully qualified name
247
                            // this will prevent name conflicts, and handle scope resolution
248
                            std::string fqn = updateSymbolWithFullyQualifiedName(child);
143✔
249
                            addDefinedSymbol(fqn, true);
143✔
250
                        }
143✔
251
                        else if (child.nodeType() == NodeType::Symbol)
605✔
252
                            addDefinedSymbol(child.string(), /* is_mutable= */ true);
605✔
253
                    }
750✔
254
                }
476✔
255
                if (node.constList().size() > 2)
477✔
256
                    visit(node.list()[2], register_declarations);
476✔
257

258
                // remove the scope once the function has been compiled, only we were registering declarations
259
                m_scope_resolver.removeLastScope();
477✔
260
                break;
1,889✔
261

262
            default:
263
                for (auto& child : node.list())
7,904✔
264
                    visit(child, register_declarations);
6,492✔
265
                break;
1,412✔
266
        }
3,283✔
267
    }
3,289✔
268

269
    void NameResolutionPass::addSymbolNode(const Node& symbol, const std::string& old_name)
9,744✔
270
    {
9,744✔
271
        const std::string& name = symbol.string();
9,744✔
272

273
        // we don't accept builtins/operators as a user symbol
274
        if (m_language_symbols.contains(name))
9,744✔
275
            return;
4,616✔
276

277
        // remove the old name node, to avoid false positive when looking for unbound symbols
278
        if (!old_name.empty())
5,128✔
279
        {
280
            auto it = std::ranges::find_if(m_symbol_nodes, [&old_name, &symbol](const Node& sym_node) -> bool {
431,598✔
281
                return sym_node.string() == old_name &&
431,187✔
282
                    sym_node.col() == symbol.col() &&
4,717✔
283
                    sym_node.line() == symbol.line() &&
5,883✔
284
                    sym_node.filename() == symbol.filename();
2,690✔
285
            });
286
            if (it != m_symbol_nodes.end())
5,128✔
287
            {
288
                it->setString(name);
2,690✔
289
                return;
2,690✔
290
            }
291
        }
5,128✔
292

293
        const auto it = std::ranges::find_if(m_symbol_nodes, [&name](const Node& sym_node) -> bool {
197,974✔
294
            return sym_node.string() == name;
195,536✔
295
        });
296
        if (it == m_symbol_nodes.end())
2,438✔
297
            m_symbol_nodes.push_back(symbol);
307✔
298
    }
9,744✔
299

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

311
    std::string NameResolutionPass::updateSymbolWithFullyQualifiedName(Node& symbol)
8,612✔
312
    {
8,612✔
313
        auto [allowed, fqn] = m_scope_resolver.canFullyQualifyName(symbol.string());
39,066✔
314

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

337
            if (m_logger.shouldTrace())
2✔
NEW
338
                m_ast.debugPrint(std::cout) << '\n';
×
339

340
            throw CodeError(
4✔
341
                message,
342
                symbol.filename(),
2✔
343
                symbol.line(),
2✔
344
                symbol.col(),
2✔
345
                symbol.repr());
2✔
346
        }
2✔
347

348
        symbol.setString(fqn);
8,609✔
349
        return fqn;
8,609✔
350
    }
8,615✔
351

352
    void NameResolutionPass::checkForUndefinedSymbol() const
87✔
353
    {
87✔
354
        for (const auto& sym : m_symbol_nodes)
390✔
355
        {
356
            const auto& str = sym.string();
303✔
357
            const bool is_plugin = mayBeFromPlugin(str);
303✔
358

359
            if (!m_defined_symbols.contains(str) && !is_plugin)
303✔
360
            {
361
                std::string message;
2✔
362

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

377
                throw CodeError(message, sym.filename(), sym.line(), sym.col(), sym.repr());
2✔
378
            }
2✔
379
        }
303✔
380
    }
87✔
381

382
    std::string NameResolutionPass::offerSuggestion(const std::string& str) const
2✔
383
    {
2✔
384
        auto iterate = [](const std::string& word, const std::unordered_set<std::string>& dict) -> std::string {
5✔
385
            std::string suggestion;
3✔
386
            // our suggestion shouldn't require more than half the string to change
387
            std::size_t suggestion_distance = word.size() / 2;
3✔
388
            for (const std::string& symbol : dict)
98✔
389
            {
390
                const std::size_t current_distance = Utils::levenshteinDistance(word, symbol);
95✔
391
                if (current_distance <= suggestion_distance)
95✔
392
                {
393
                    suggestion_distance = current_distance;
1✔
394
                    suggestion = symbol;
1✔
395
                }
1✔
396
            }
95✔
397
            return suggestion;
3✔
398
        };
3✔
399

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

414
        return suggestion;
2✔
415
    }
2✔
416
}
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