ArkScript
A small, lisp-inspired, functional scripting language
NameResolutionPass.cpp
Go to the documentation of this file.
2
4#include <Ark/Utils/Utils.hpp>
6
7namespace Ark::internal
8{
10 Pass("NameResolution", debug)
11 {
12 for (const auto& builtin : Builtins::builtins)
13 m_language_symbols.emplace(builtin.first);
14 for (auto ope : Language::operators)
15 m_language_symbols.emplace(ope);
16 for (auto inst : Language::listInstructions)
17 m_language_symbols.emplace(inst);
18
25 }
26
28 {
29 m_logger.traceStart("process");
30
31 m_ast = ast;
32 visit(m_ast, /* register_declarations= */ true);
33
35
36 m_logger.debug("AST after name resolution");
38 m_ast.debugPrint(std::cout) << '\n';
39
40 m_logger.traceStart("checkForUndefinedSymbol");
43 }
44
45 const Node& NameResolutionPass::ast() const noexcept
46 {
47 return m_ast;
48 }
49
50 std::string NameResolutionPass::addDefinedSymbol(const std::string& sym, const bool is_mutable)
51 {
52 const std::string fully_qualified_name = m_scope_resolver.registerInCurrent(sym, is_mutable);
53 m_defined_symbols.emplace(fully_qualified_name);
54 return fully_qualified_name;
55 }
56
57 void NameResolutionPass::visit(Node& node, const bool register_declarations)
58 {
59 switch (node.nodeType())
60 {
62 {
63 const std::string old_name = node.string();
65 addSymbolNode(node, old_name);
66 break;
67 }
68
69 case NodeType::Field:
70 for (std::size_t i = 0, end = node.list().size(); i < end; ++i)
71 {
72 Node& child = node.list()[i];
73
74 if (i == 0)
75 {
76 const std::string old_name = child.string();
77 // in case of field, no need to check if we can fully qualify names
79 addSymbolNode(child, old_name);
80 }
81 else
82 addSymbolNode(child);
83 }
84 break;
85
86 case NodeType::List:
87 if (!node.constList().empty())
88 {
89 if (node.constList()[0].nodeType() == NodeType::Keyword)
90 visitKeyword(node, node.constList()[0].keyword(), register_declarations);
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 &&
97 node.constList()[1].nodeType() == NodeType::Symbol && register_declarations)
98 {
99 const auto funcname = node.constList()[0].string();
100 const auto arg = node.constList()[1].string();
101
102 if (std::ranges::find(Language::UpdateRef, funcname) != Language::UpdateRef.end() && m_scope_resolver.isImmutable(arg).value_or(false))
103 throw CodeError(
104 fmt::format("MutabilityError: Can not modify the constant list `{}' using `{}'", arg, funcname),
105 CodeErrorContext(node.filename(), node.constList()[1].position()));
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)
109 {
110 for (std::size_t i = 2, end = node.constList().size(); i < end; ++i)
111 {
112 if (node.constList()[i].nodeType() == NodeType::Symbol && node.constList()[i].string() == arg)
113 throw CodeError(
114 fmt::format("MutabilityError: Can not {} the list `{}' to itself", funcname, arg),
115 CodeErrorContext(node.filename(), node.constList()[1].position()));
116 }
117 }
118 }
119
120 for (auto& child : node.list())
121 visit(child, register_declarations);
122 }
123 }
124 break;
125
127 {
128 auto& namespace_ = node.arkNamespace();
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);
134
135 visit(*namespace_.ast, /* register_declarations= */ true);
136 // dual visit so that we can handle forward references
137 visit(*namespace_.ast, /* register_declarations= */ false);
138
139 // if we had specific symbols to import, check that those exist
140 if (!namespace_.symbols.empty())
141 {
142 const auto it = std::ranges::find_if(
143 namespace_.symbols,
144 [&scope, &namespace_](const std::string& sym) -> bool {
145 return !scope->get(sym, namespace_.name, true).has_value();
146 });
147
148 if (it != namespace_.symbols.end())
149 throw CodeError(
150 fmt::format("ImportError: Can not import symbol {} from {}, as it isn't in the package", *it, namespace_.name),
151 CodeErrorContext(namespace_.ast->filename(), namespace_.ast->position()));
152 }
153
155 break;
156 }
157
158 default:
159 break;
160 }
161 }
162
163 void NameResolutionPass::visitKeyword(Node& node, const Keyword keyword, const bool register_declarations)
164 {
165 switch (keyword)
166 {
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)
175 visit(node.list()[2], register_declarations);
176 if (node.constList().size() > 1 && node.constList()[1].nodeType() == NodeType::Symbol)
177 {
178 const std::string& name = node.constList()[1].string();
179 if (m_language_symbols.contains(name) && register_declarations)
180 throw CodeError(
181 fmt::format("Can not use a reserved identifier ('{}') as a {} name.", name, keyword == Keyword::Let ? "constant" : "variable"),
182 CodeErrorContext(node.filename(), node.constList()[1].position()));
183
184 if (m_scope_resolver.isInScope(name) && keyword == Keyword::Let && register_declarations)
185 throw CodeError(
186 fmt::format("MutabilityError: Can not use 'let' to redefine variable `{}'", name),
187 CodeErrorContext(node.filename(), node.constList()[1].position()));
188 if (keyword == Keyword::Set && m_scope_resolver.isRegistered(name))
189 {
190 if (m_scope_resolver.isImmutable(name).value_or(false) && register_declarations)
191 throw CodeError(
192 fmt::format("MutabilityError: Can not set the constant `{}' to {}", name, node.constList()[2].repr()),
193 CodeErrorContext(node.filename(), node.constList()[1].position()));
194
196 }
197 else if (keyword != Keyword::Set)
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);
202 if (register_declarations)
203 node.list()[1].setString(fully_qualified_name);
204 }
205 }
206 break;
207
208 case Keyword::Import:
209 if (!node.constList().empty())
210 m_plugin_names.push_back(node.constList()[1].constList().back().string());
211 break;
212
213 case Keyword::While:
214 // create a new scope to track variables
216 for (auto& child : node.list())
217 visit(child, register_declarations);
218 // remove the scope once the loop has been compiled, only we were registering declarations
220 break;
221
222 case Keyword::Fun:
223 // create a new scope to track variables
225
226 if (node.constList()[1].nodeType() == NodeType::List)
227 {
228 for (auto& child : node.list()[1].list())
229 {
230 if (child.nodeType() == NodeType::Capture)
231 {
232 if (!m_scope_resolver.isRegistered(child.string()) && register_declarations)
233 throw CodeError(
234 fmt::format("Can not capture `{}' because it is referencing a variable defined in an unreachable scope.", child.string()),
235 CodeErrorContext(child.filename(), child.position()));
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())
240 {
241 child.setUnqualifiedName(child.string());
242 m_defined_symbols.emplace(child.string());
243 }
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();
248 addDefinedSymbol(old_name, true);
249 }
250 else if (child.nodeType() == NodeType::Symbol || child.nodeType() == NodeType::RefArg)
251 addDefinedSymbol(child.string(), /* is_mutable= */ false);
252 else if (child.nodeType() == NodeType::MutArg)
253 addDefinedSymbol(child.string(), /* is_mutable= */ true);
254 }
255 }
256 if (node.constList().size() > 2)
257 visit(node.list()[2], register_declarations);
258
259 // remove the scope once the function has been compiled, only we were registering declarations
261 break;
262
263 default:
264 for (auto& child : node.list())
265 visit(child, register_declarations);
266 break;
267 }
268 }
269
270 void NameResolutionPass::addSymbolNode(const Node& symbol, const std::string& old_name)
271 {
272 const std::string& name = symbol.string();
273
274 // we don't accept builtins/operators as a user symbol
275 if (m_language_symbols.contains(name))
276 return;
277
278 // remove the old name node, to avoid false positive when looking for unbound symbols
279 if (!old_name.empty())
280 {
281 auto it = std::ranges::find_if(m_symbol_nodes, [&old_name, &symbol](const Node& sym_node) -> bool {
282 return sym_node.string() == old_name &&
283 sym_node.position().start == symbol.position().start &&
284 sym_node.filename() == symbol.filename();
285 });
286 if (it != m_symbol_nodes.end())
287 {
288 it->setString(name);
289 return;
290 }
291 }
292
293 const auto it = std::ranges::find_if(m_symbol_nodes, [&name](const Node& sym_node) -> bool {
294 return sym_node.string() == name;
295 });
296 if (it == m_symbol_nodes.end())
297 m_symbol_nodes.push_back(symbol);
298 }
299
300 bool NameResolutionPass::mayBeFromPlugin(const std::string& name) const noexcept
301 {
302 std::string splitted = Utils::splitString(name, ':')[0];
303 const auto it = std::ranges::find_if(
304 m_plugin_names,
305 [&splitted](const std::string& plugin) -> bool {
306 return plugin == splitted;
307 });
308 return it != m_plugin_names.end();
309 }
310
312 {
313 auto [allowed, fqn] = m_scope_resolver.canFullyQualifyName(symbol.string());
314
315 if (m_language_symbols.contains(fqn) && symbol.string() != fqn)
316 {
317 throw CodeError(
318 fmt::format(
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.",
320 symbol.string(), fqn),
321 CodeErrorContext(symbol.filename(), symbol.position()));
322 }
323 if (!allowed)
324 {
325 std::string message;
326 if (fqn.ends_with(HiddenSymbolSuffix))
327 message = fmt::format(
328 R"(Unbound variable "{}". However, it exists in a namespace as "{}", did you forget to add it to the symbol list while importing?)",
329 symbol.string(),
330 fqn.substr(0, fqn.find_first_of('#')));
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())
335 m_ast.debugPrint(std::cout) << '\n';
336
337 throw CodeError(message, CodeErrorContext(symbol.filename(), symbol.position()));
338 }
339
340 symbol.setString(fqn);
341 return fqn;
342 }
343
345 {
346 for (const auto& sym : m_symbol_nodes)
347 {
348 const auto& str = sym.string();
349 const bool is_plugin = mayBeFromPlugin(str);
350
351 if (!m_defined_symbols.contains(str) && !is_plugin)
352 {
353 std::string message;
354
355 const std::string suggestion = offerSuggestion(str);
356 if (suggestion.empty())
357 message = fmt::format(R"(Unbound variable "{}" (variable is used but not defined))", str);
358 else if (suggestion.ends_with(HiddenSymbolSuffix))
359 {
360 const std::string prefix = suggestion.substr(0, suggestion.find_first_of(':'));
361 const std::string suffix = suggestion.substr(suggestion.find_first_of(':') + 1, suggestion.size() - prefix.size() - 1 - HiddenSymbolSuffix.size());
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);
363 }
364 else
365 {
366 const std::string prefix = suggestion.substr(0, suggestion.find_first_of(':'));
367 const std::string note_about_prefix = fmt::format(
368 " You either forgot to import it in the symbol list (eg `(import {} :{})') or need to fully qualify it by adding the namespace",
369 prefix,
370 str);
371 const bool add_note = suggestion.ends_with(":" + str);
372 message = fmt::format(R"(Unbound variable "{}" (did you mean "{}"?{}))", str, suggestion, add_note ? note_about_prefix : "");
373 }
374
375 throw CodeError(message, CodeErrorContext(sym.filename(), sym.position()));
376 }
377 }
378 }
379
380 std::string NameResolutionPass::offerSuggestion(const std::string& str) const
381 {
382 auto iterate = [](const std::string& word, const std::unordered_set<std::string>& dict) -> std::string {
383 std::string suggestion;
384 // our suggestion shouldn't require more than half the string to change
385 std::size_t suggestion_distance = word.size() / 2;
386 for (const std::string& symbol : dict)
387 {
388 if (symbol.starts_with(word) && symbol.ends_with(HiddenSymbolSuffix))
389 {
390 suggestion = symbol;
391 break;
392 }
393
394 const std::size_t current_distance = Utils::levenshteinDistance(word, symbol);
395 if (current_distance <= suggestion_distance)
396 {
397 suggestion_distance = current_distance;
398 suggestion = symbol;
399 }
400 }
401 return suggestion;
402 };
403
404 std::string suggestion = iterate(str, m_defined_symbols);
405 // look for a suggestion related to language builtins
406 if (suggestion.empty())
407 suggestion = iterate(str, m_language_symbols);
408 // look for a suggestion related to a namespace change
409 if (suggestion.empty())
410 {
411 if (const auto it = std::ranges::find_if(m_defined_symbols, [&str](const std::string& symbol) {
412 return symbol.ends_with(":" + str);
413 });
414 it != m_defined_symbols.end())
415 suggestion = *it;
416 }
417
418 return suggestion;
419 }
420}
Lots of utilities about string, filesystem and more.
Host the declaration of all the ArkScript builtins.
ArkScript homemade exceptions.
Resolves names and fully qualify them in the AST (prefixing them with the package they are from)
void debug(const Logger::MessageAndLocation &data, Args &&... args)
Write a debug level log using fmtlib.
Definition Logger.hpp:96
bool shouldTrace() const
Definition Logger.hpp:53
bool shouldDebug() const
Definition Logger.hpp:54
void traceStart(std::string &&trace_name)
Definition Logger.hpp:109
std::vector< std::string > m_plugin_names
const Node & ast() const noexcept
Unused overload that return the input AST (untouched as this pass only generates errors)
void visit(Node &node, bool register_declarations)
Recursively visit nodes.
void visitKeyword(Node &node, Keyword keyword, bool register_declarations)
void checkForUndefinedSymbol() const
Checks for undefined symbols, not present in the defined symbols table.
std::string offerSuggestion(const std::string &str) const
Suggest a symbol of what the user may have meant to input.
std::unordered_set< std::string > m_language_symbols
Precomputed set of language symbols that can't be used to define variables.
void process(const Node &ast)
Start visiting the given AST, checking for mutability violation and unbound variables.
std::unordered_set< std::string > m_defined_symbols
bool mayBeFromPlugin(const std::string &name) const noexcept
Checking if a symbol may be coming from a plugin.
std::string addDefinedSymbol(const std::string &sym, bool is_mutable)
Register a symbol as defined, so that later we can throw errors on undefined symbols.
std::string updateSymbolWithFullyQualifiedName(Node &symbol)
void addSymbolNode(const Node &symbol, const std::string &old_name="")
Register a given node in the symbol table.
NameResolutionPass(unsigned debug)
Create a NameResolutionPass.
A node of an Abstract Syntax Tree for ArkScript.
Definition Node.hpp:32
NodeType nodeType() const noexcept
Return the node type.
Definition Node.cpp:78
const std::string & filename() const noexcept
Return the filename in which this node was created.
Definition Node.cpp:174
const std::string & string() const noexcept
Return the string held by the value (if the node type allows it)
Definition Node.cpp:38
const std::vector< Node > & constList() const noexcept
Return the list of sub-nodes held by the node.
Definition Node.cpp:73
Namespace & arkNamespace() noexcept
Return the namespace held by the value (if the node type allows it)
Definition Node.cpp:53
std::ostream & debugPrint(std::ostream &os) const noexcept
Print a node to an output stream with added type annotations.
Definition Node.cpp:297
FileSpan position() const noexcept
Get the span of the node (start and end)
Definition Node.cpp:169
void setString(const std::string &value) noexcept
Set the String object.
Definition Node.cpp:117
std::vector< Node > & list() noexcept
Return the list of sub-nodes held by the node.
Definition Node.cpp:68
An interface to describe compiler passes.
Definition Pass.hpp:24
std::string registerInCurrent(const std::string &name, bool is_mutable)
Register a Declaration in the current (last) scope.
void createNewNamespace(const std::string &name, bool with_prefix, bool is_glob, const std::vector< std::string > &symbols)
Create a new namespace scope.
void saveNamespaceAndRemove()
Save the last scope as a namespace, by attaching it to the nearest namespace scope.
std::string getFullyQualifiedNameInNearestScope(const std::string &name) const
Get a FQN from a variable name in the nearest scope it is declared in.
bool isRegistered(const std::string &name) const
Checks if any scope has 'name', in reverse order.
void createNew()
Create a new scope.
StaticScope * currentScope() const
Return a non-owning raw pointer to the current scope.
bool isInScope(const std::string &name) const
Checks if 'name' is in the current scope.
void removeLastScope()
Remove the last scope.
std::optional< bool > isImmutable(const std::string &name) const
Checks the scopes in reverse order for 'name' and returns its mutability status.
std::pair< bool, std::string > canFullyQualifyName(const std::string &name)
Checks if a name can be fully qualified (allows only unprefixed names to be resolved by glob namespac...
std::vector< std::string > splitString(const std::string &source, const char sep)
Cut a string into pieces, given a character separator.
Definition Utils.hpp:31
ARK_API std::size_t levenshteinDistance(const std::string &str1, const std::string &str2)
Calculate the Levenshtein distance between two strings.
Definition Utils.cpp:7
ARK_API const std::vector< std::pair< std::string, Value > > builtins
constexpr std::array< std::string_view, 9 > listInstructions
Definition Common.hpp:121
constexpr std::string_view AppendInPlace
Definition Common.hpp:108
constexpr std::string_view Apply
Definition Common.hpp:140
constexpr std::array< std::string_view, 24 > operators
Definition Common.hpp:161
constexpr std::string_view ConcatInPlace
Definition Common.hpp:109
constexpr std::string_view SysArgs
Definition Common.hpp:134
constexpr std::string_view And
Definition Common.hpp:137
constexpr std::string_view SysVersion
Definition Common.hpp:133
constexpr std::array UpdateRef
All the builtins that modify in place a variable.
Definition Common.hpp:114
constexpr std::string_view Or
Definition Common.hpp:138
constexpr std::string_view SysProgramName
Definition Common.hpp:135
constexpr std::string_view HiddenSymbolSuffix
Definition Common.hpp:104
Keyword
The different keywords available.
Definition Common.hpp:79
CodeError thrown by the compiler (parser, macro processor, optimizer, and compiler itself)