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

ArkScript-lang / Ark / 21916619485

11 Feb 2026 05:55PM UTC coverage: 93.464% (+0.05%) from 93.413%
21916619485

push

github

SuperFola
docs: adding arkdoc docstrings for append and friends, as well as assert

9195 of 9838 relevant lines covered (93.46%)

267918.09 hits per line

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

99.25
/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) :
854✔
10
        Pass("NameResolution", debug)
427✔
11
    {
854✔
12
        for (const auto& builtin : Builtins::builtins)
38,857✔
13
            m_language_symbols.emplace(builtin.first);
38,430✔
14
        for (auto ope : Language::operators)
10,675✔
15
            m_language_symbols.emplace(ope);
10,248✔
16
        for (auto inst : Language::listInstructions)
4,270✔
17
            m_language_symbols.emplace(inst);
3,843✔
18

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

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

30
        m_ast = ast;
343✔
31
        visit(m_ast, /* register_declarations= */ true);
343✔
32

33
        m_logger.traceEnd();
343✔
34

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

39
        m_logger.traceStart("checkForUndefinedSymbol");
343✔
40
        checkForUndefinedSymbol();
343✔
41
        m_logger.traceEnd();
343✔
42
    }
343✔
43

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

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

56
    void NameResolutionPass::visit(Node& node, const bool register_declarations)
390,252✔
57
    {
390,252✔
58
        switch (node.nodeType())
390,252✔
59
        {
131,610✔
60
            case NodeType::Symbol:
61
            {
62
                const std::string old_name = node.string();
131,610✔
63
                updateSymbolWithFullyQualifiedName(node);
131,610✔
64
                addSymbolNode(node, old_name);
131,608✔
65
                break;
66
            }
136,952✔
67

68
            case NodeType::Field:
69
                for (std::size_t i = 0, end = node.list().size(); i < end; ++i)
16,011✔
70
                {
71
                    Node& child = node.list()[i];
10,677✔
72

73
                    if (i == 0)
10,677✔
74
                    {
75
                        const std::string old_name = child.string();
5,334✔
76
                        // in case of field, no need to check if we can fully qualify names
77
                        child.setString(m_scope_resolver.getFullyQualifiedNameInNearestScope(old_name));
5,334✔
78
                        addSymbolNode(child, old_name);
5,334✔
79
                    }
5,334✔
80
                    else
81
                        addSymbolNode(child);
5,343✔
82
                }
10,677✔
83
                break;
137,299✔
84

85
            case NodeType::List:
86
                if (!node.constList().empty())
131,965✔
87
                {
88
                    if (node.constList()[0].nodeType() == NodeType::Keyword)
131,938✔
89
                        visitKeyword(node, node.constList()[0].keyword(), register_declarations);
66,107✔
90
                    else
91
                    {
92
                        // function calls
93
                        // the UpdateRef function calls kind get a special treatment, like let/mut/set,
94
                        // because we need to check for mutability errors
95
                        if (node.constList().size() > 1 && node.constList()[0].nodeType() == NodeType::Symbol &&
102,892✔
96
                            node.constList()[1].nodeType() == NodeType::Symbol && register_declarations)
57,855✔
97
                        {
98
                            const auto funcname = node.constList()[0].string();
18,925✔
99
                            const auto arg = node.constList()[1].string();
18,925✔
100

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

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

119
                        for (auto& child : node.list())
313,468✔
120
                            visit(child, register_declarations);
247,644✔
121
                    }
122
                }
131,931✔
123
                break;
132,152✔
124

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

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

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

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

153
                m_scope_resolver.saveNamespaceAndRemove();
193✔
154
                break;
155
            }
121,343✔
156

157
            default:
158
                break;
121,149✔
159
        }
390,188✔
160
    }
390,198✔
161

162
    void NameResolutionPass::visitKeyword(Node& node, const Keyword keyword, const bool register_declarations)
66,107✔
163
    {
66,107✔
164
        switch (keyword)
66,107✔
165
        {
32,442✔
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)
32,442✔
174
                    visit(node.list()[2], register_declarations);
32,441✔
175
                if (node.constList().size() > 1 && node.constList()[1].nodeType() == NodeType::Symbol)
32,442✔
176
                {
177
                    const std::string& name = node.constList()[1].string();
32,440✔
178
                    if (m_language_symbols.contains(name) && register_declarations)
32,440✔
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(node.filename(), node.constList()[1].position()));
3✔
182

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

194
                        updateSymbolWithFullyQualifiedName(node.list()[1]);
9,893✔
195
                    }
9,893✔
196
                    else if (keyword != Keyword::Set)
22,542✔
197
                    {
198
                        // update the declared variable name to use the fully qualified name
199
                        // this will prevent name conflicts, and handle scope resolution
200
                        const std::string fully_qualified_name = addDefinedSymbol(name, keyword != Keyword::Let);
22,539✔
201
                        if (register_declarations)
22,539✔
202
                            node.list()[1].setString(fully_qualified_name);
11,474✔
203
                    }
22,539✔
204
                }
32,442✔
205
                break;
32,440✔
206

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

212
            case Keyword::While:
213
                // create a new scope to track variables
214
                m_scope_resolver.createNew();
3,123✔
215
                for (auto& child : node.list())
12,491✔
216
                    visit(child, register_declarations);
9,368✔
217
                // remove the scope once the loop has been compiled, only we were registering declarations
218
                m_scope_resolver.removeLastScope();
3,123✔
219
                break;
14,767✔
220

221
            case Keyword::Fun:
222
                // create a new scope to track variables
223
                m_scope_resolver.createNew();
11,644✔
224

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

236
                            // save the old unqualified name of the capture, so that we can use it in the
237
                            // ASTLowerer later one
238
                            if (!child.getUnqualifiedName())
682✔
239
                            {
240
                                child.setUnqualifiedName(child.string());
227✔
241
                                m_defined_symbols.emplace(child.string());
227✔
242
                            }
227✔
243
                            // update the declared variable name to use the fully qualified name
244
                            // this will prevent name conflicts, and handle scope resolution
245
                            std::string old_name = child.string();
682✔
246
                            updateSymbolWithFullyQualifiedName(child);
682✔
247
                            addDefinedSymbol(old_name, true);
682✔
248
                        }
682✔
249
                        else if (child.nodeType() == NodeType::Symbol || child.nodeType() == NodeType::RefArg)
17,532✔
250
                            addDefinedSymbol(child.string(), /* is_mutable= */ false);
17,282✔
251
                        else if (child.nodeType() == NodeType::MutArg)
250✔
252
                            addDefinedSymbol(child.string(), /* is_mutable= */ true);
250✔
253
                    }
18,216✔
254
                }
11,641✔
255
                if (node.constList().size() > 2)
11,642✔
256
                    visit(node.list()[2], register_declarations);
11,641✔
257

258
                // remove the scope once the function has been compiled, only we were registering declarations
259
                m_scope_resolver.removeLastScope();
11,642✔
260
                break;
30,537✔
261

262
            default:
263
                for (auto& child : node.list())
107,282✔
264
                    visit(child, register_declarations);
88,387✔
265
                break;
18,895✔
266
        }
66,100✔
267
    }
66,107✔
268

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

273
        // we don't accept builtins/operators as a user symbol
274
        if (m_language_symbols.contains(name))
142,285✔
275
            return;
54,064✔
276

277
        // remove the old name node, to avoid false positive when looking for unbound symbols
278
        if (!old_name.empty())
88,221✔
279
        {
280
            auto it = std::ranges::find_if(m_symbol_nodes, [&old_name, &symbol](const Node& sym_node) -> bool {
12,621,302✔
281
                return sym_node.string() == old_name &&
12,615,126✔
282
                    sym_node.position().start == symbol.position().start &&
97,544✔
283
                    sym_node.filename() == symbol.filename();
20,842✔
284
            });
285
            if (it != m_symbol_nodes.end())
82,878✔
286
            {
287
                it->setString(name);
20,838✔
288
                return;
20,838✔
289
            }
290
        }
82,878✔
291

292
        const auto it = std::ranges::find_if(m_symbol_nodes, [&name](const Node& sym_node) -> bool {
5,331,574✔
293
            return sym_node.string() == name;
5,264,191✔
294
        });
295
        if (it == m_symbol_nodes.end())
67,383✔
296
            m_symbol_nodes.push_back(symbol);
5,000✔
297
    }
142,285✔
298

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

310
    std::string NameResolutionPass::updateSymbolWithFullyQualifiedName(Node& symbol)
142,185✔
311
    {
142,185✔
312
        auto [allowed, fqn] = m_scope_resolver.canFullyQualifyName(symbol.string());
622,805✔
313

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

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

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

339
        symbol.setString(fqn);
142,183✔
340
        return fqn;
142,183✔
341
    }
142,187✔
342

343
    void NameResolutionPass::checkForUndefinedSymbol() const
343✔
344
    {
343✔
345
        for (const auto& sym : m_symbol_nodes)
5,339✔
346
        {
347
            const auto& str = sym.string();
4,996✔
348
            const bool is_plugin = mayBeFromPlugin(str);
4,996✔
349

350
            if (!m_defined_symbols.contains(str) && !is_plugin)
4,996✔
351
            {
352
                std::string message;
4✔
353

354
                const std::string suggestion = offerSuggestion(str);
4✔
355
                if (suggestion.empty())
4✔
356
                    message = fmt::format(R"(Unbound variable error "{}" (variable is used but not defined))", str);
2✔
357
                else
358
                {
359
                    const std::string prefix = suggestion.substr(0, suggestion.find_first_of(':'));
2✔
360
                    const std::string note_about_prefix = fmt::format(
4✔
361
                        " You either forgot to import it in the symbol list (eg `(import {} :{})') or need to fully qualify it by adding the namespace",
2✔
362
                        prefix,
363
                        str);
2✔
364
                    const bool add_note = suggestion.ends_with(":" + str);
2✔
365
                    message = fmt::format(R"(Unbound variable error "{}" (did you mean "{}"?{}))", str, suggestion, add_note ? note_about_prefix : "");
2✔
366
                }
2✔
367

368
                throw CodeError(message, CodeErrorContext(sym.filename(), sym.position()));
4✔
369
            }
4✔
370
        }
4,996✔
371
    }
343✔
372

373
    std::string NameResolutionPass::offerSuggestion(const std::string& str) const
4✔
374
    {
4✔
375
        auto iterate = [](const std::string& word, const std::unordered_set<std::string>& dict) -> std::string {
11✔
376
            std::string suggestion;
7✔
377
            // our suggestion shouldn't require more than half the string to change
378
            std::size_t suggestion_distance = word.size() / 2;
7✔
379
            for (const std::string& symbol : dict)
348✔
380
            {
381
                const std::size_t current_distance = Utils::levenshteinDistance(word, symbol);
341✔
382
                if (current_distance <= suggestion_distance)
341✔
383
                {
384
                    suggestion_distance = current_distance;
1✔
385
                    suggestion = symbol;
1✔
386
                }
1✔
387
            }
341✔
388
            return suggestion;
7✔
389
        };
7✔
390

391
        std::string suggestion = iterate(str, m_defined_symbols);
4✔
392
        // look for a suggestion related to language builtins
393
        if (suggestion.empty())
4✔
394
            suggestion = iterate(str, m_language_symbols);
3✔
395
        // look for a suggestion related to a namespace change
396
        if (suggestion.empty())
4✔
397
        {
398
            if (const auto it = std::ranges::find_if(m_defined_symbols, [&str](const std::string& symbol) {
23✔
399
                    return symbol.ends_with(":" + str);
17✔
400
                });
401
                it != m_defined_symbols.end())
4✔
402
                suggestion = *it;
1✔
403
        }
3✔
404

405
        return suggestion;
4✔
406
    }
4✔
407
}
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