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

ArkScript-lang / Ark / 15006215616

13 May 2025 08:34PM UTC coverage: 86.726% (+0.3%) from 86.474%
15006215616

push

github

SuperFola
feat(macro processor, error): adding better error messages when a macro fails, to show the macro we were expanding and what failed

60 of 60 new or added lines in 8 files covered. (100.0%)

83 existing lines in 6 files now uncovered.

7017 of 8091 relevant lines covered (86.73%)

79023.01 hits per line

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

98.97
/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) :
544✔
10
        Pass("NameResolution", debug),
272✔
11
        m_ast()
272✔
12
    {
544✔
13
        for (const auto& builtin : Builtins::builtins)
15,776✔
14
            m_language_symbols.emplace(builtin.first);
15,504✔
15
        for (auto ope : Language::operators)
6,800✔
16
            m_language_symbols.emplace(ope);
6,528✔
17
        for (auto inst : Language::listInstructions)
2,720✔
18
            m_language_symbols.emplace(inst);
2,448✔
19

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

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

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

32
        m_logger.traceEnd();
203✔
33

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

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

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

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

55
    void NameResolutionPass::visit(Node& node, const bool register_declarations)
181,490✔
56
    {
181,490✔
57
        switch (node.nodeType())
181,490✔
58
        {
44,004✔
59
            case NodeType::Symbol:
60
            {
61
                const std::string old_name = node.string();
44,004✔
62
                updateSymbolWithFullyQualifiedName(node);
44,004✔
63
                addSymbolNode(node, old_name);
44,001✔
64
                break;
65
            }
47,425✔
66

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

77
            case NodeType::List:
78
                if (!node.constList().empty())
46,603✔
79
                {
80
                    if (node.constList()[0].nodeType() == NodeType::Keyword)
46,578✔
81
                        visitKeyword(node, node.constList()[0].keyword(), register_declarations);
21,259✔
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 &&
36,321✔
88
                            node.constList()[1].nodeType() == NodeType::Symbol && register_declarations)
21,648✔
89
                        {
90
                            const auto funcname = node.constList()[0].string();
5,665✔
91
                            const auto arg = node.constList()[1].string();
5,665✔
92

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

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

119
                        for (auto& child : node.list())
159,652✔
120
                            visit(child, register_declarations);
134,340✔
121
                    }
122
                }
46,571✔
123
                break;
46,731✔
124

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

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

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

154
                m_scope_resolver.saveNamespaceAndRemove();
134✔
155
                break;
156
            }
87,470✔
157

158
            default:
159
                break;
87,335✔
160
        }
181,429✔
161
    }
181,440✔
162

163
    void NameResolutionPass::visitKeyword(Node& node, const Keyword keyword, const bool register_declarations)
21,259✔
164
    {
21,259✔
165
        switch (keyword)
21,259✔
166
        {
9,836✔
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)
9,836✔
175
                    visit(node.list()[2], register_declarations);
9,835✔
176
                if (node.constList().size() > 1 && node.constList()[1].nodeType() == NodeType::Symbol)
9,836✔
177
                {
178
                    const std::string& name = node.constList()[1].string();
9,834✔
179
                    if (m_language_symbols.contains(name) && register_declarations)
9,834✔
180
                        throw CodeError(
4✔
181
                            fmt::format("Can not use a reserved identifier ('{}') as a {} name.", name, keyword == Keyword::Let ? "constant" : "variable"),
2✔
182
                            CodeErrorContext(
2✔
183
                                node.filename(),
2✔
184
                                node.constList()[1].line(),
2✔
185
                                node.constList()[1].col(),
2✔
186
                                name));
2✔
187

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

207
                        updateSymbolWithFullyQualifiedName(node.list()[1]);
2,249✔
208
                    }
2,249✔
209
                    else if (keyword != Keyword::Set)
7,581✔
210
                    {
211
                        // update the declared variable name to use the fully qualified name
212
                        // this will prevent name conflicts, and handle scope resolution
213
                        const std::string fully_qualified_name = addDefinedSymbol(name, keyword != Keyword::Let);
7,578✔
214
                        if (register_declarations)
7,578✔
215
                            node.list()[1].setString(fully_qualified_name);
3,920✔
216
                    }
7,578✔
217
                }
9,836✔
218
                break;
9,833✔
219

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

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

234
            case Keyword::Fun:
235
                // create a new scope to track variables
236
                m_scope_resolver.createNew();
3,952✔
237

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

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

265
                // remove the scope once the function has been compiled, only we were registering declarations
266
                m_scope_resolver.removeLastScope();
3,950✔
267
                break;
10,185✔
268

269
            default:
270
                for (auto& child : node.list())
35,370✔
271
                    visit(child, register_declarations);
29,135✔
272
                break;
6,235✔
273
        }
21,253✔
274
    }
21,259✔
275

276
    void NameResolutionPass::addSymbolNode(const Node& symbol, const std::string& old_name)
50,840✔
277
    {
50,840✔
278
        const std::string& name = symbol.string();
50,840✔
279

280
        // we don't accept builtins/operators as a user symbol
281
        if (m_language_symbols.contains(name))
50,840✔
282
            return;
20,776✔
283

284
        // remove the old name node, to avoid false positive when looking for unbound symbols
285
        if (!old_name.empty())
30,064✔
286
        {
287
            auto it = std::ranges::find_if(m_symbol_nodes, [&old_name, &symbol](const Node& sym_node) -> bool {
2,070,808✔
288
                return sym_node.string() == old_name &&
2,068,560✔
289
                    sym_node.col() == symbol.col() &&
27,816✔
290
                    sym_node.line() == symbol.line() &&
24,580✔
291
                    sym_node.filename() == symbol.filename();
10,626✔
292
            });
293
            if (it != m_symbol_nodes.end())
30,064✔
294
            {
295
                it->setString(name);
10,626✔
296
                return;
10,626✔
297
            }
298
        }
30,064✔
299

300
        const auto it = std::ranges::find_if(m_symbol_nodes, [&name](const Node& sym_node) -> bool {
801,692✔
301
            return sym_node.string() == name;
782,254✔
302
        });
303
        if (it == m_symbol_nodes.end())
19,438✔
304
            m_symbol_nodes.push_back(symbol);
1,937✔
305
    }
50,840✔
306

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

318
    std::string NameResolutionPass::updateSymbolWithFullyQualifiedName(Node& symbol)
46,773✔
319
    {
46,773✔
320
        auto [allowed, fqn] = m_scope_resolver.canFullyQualifyName(symbol.string());
207,870✔
321

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

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

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

357
        symbol.setString(fqn);
46,770✔
358
        return fqn;
46,770✔
359
    }
46,776✔
360

361
    void NameResolutionPass::checkForUndefinedSymbol() const
203✔
362
    {
203✔
363
        for (const auto& sym : m_symbol_nodes)
2,136✔
364
        {
365
            const auto& str = sym.string();
1,933✔
366
            const bool is_plugin = mayBeFromPlugin(str);
1,933✔
367

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

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

386
                throw CodeError(message, CodeErrorContext(sym.filename(), sym.line(), sym.col(), sym.repr()));
2✔
387
            }
2✔
388
        }
1,933✔
389
    }
203✔
390

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

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

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