ArkScript
A small, fast, functional and scripting language for video games
NameResolutionPass.cpp
Go to the documentation of this file.
2
3#include <Ark/Exceptions.hpp>
4#include <Ark/Utils.hpp>
6
7namespace Ark::internal
8{
10 Pass("NameResolution", debug),
11 m_ast()
12 {
13 for (const auto& builtin : Builtins::builtins)
14 m_language_symbols.emplace(builtin.first);
15 for (auto ope : Language::operators)
16 m_language_symbols.emplace(ope);
17 for (auto inst : Language::listInstructions)
18 m_language_symbols.emplace(inst);
19
23 }
24
26 {
27 m_logger.traceStart("process");
28
29 m_ast = ast;
30 visit(m_ast, /* register_declarations= */ true);
31
33
34 m_logger.trace("AST after name resolution");
36 m_ast.debugPrint(std::cout) << '\n';
37
38 m_logger.traceStart("checkForUndefinedSymbol");
41 }
42
43 const Node& NameResolutionPass::ast() const noexcept
44 {
45 return m_ast;
46 }
47
48 std::string NameResolutionPass::addDefinedSymbol(const std::string& sym, const bool is_mutable)
49 {
50 const std::string fully_qualified_name = m_scope_resolver.registerInCurrent(sym, is_mutable);
51 m_defined_symbols.emplace(fully_qualified_name);
52 return fully_qualified_name;
53 }
54
55 void NameResolutionPass::visit(Node& node, const bool register_declarations)
56 {
57 switch (node.nodeType())
58 {
60 {
61 const std::string old_name = node.string();
63 addSymbolNode(node, old_name);
64 break;
65 }
66
67 case NodeType::Field:
68 for (auto& child : node.list())
69 {
70 const std::string old_name = child.string();
71 // in case of field, no need to check if we can fully qualify names
73 addSymbolNode(child, old_name);
74 }
75 break;
76
77 case NodeType::List:
78 if (!node.constList().empty())
79 {
80 if (node.constList()[0].nodeType() == NodeType::Keyword)
81 visitKeyword(node, node.constList()[0].keyword(), register_declarations);
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 &&
88 node.constList()[1].nodeType() == NodeType::Symbol && register_declarations)
89 {
90 const auto funcname = node.constList()[0].string();
91 const auto arg = node.constList()[1].string();
92
93 if (std::ranges::find(Language::UpdateRef, funcname) != Language::UpdateRef.end() && m_scope_resolver.isImmutable(arg).value_or(false))
94 throw CodeError(
95 fmt::format("MutabilityError: Can not modify the constant list `{}' using `{}'", arg, funcname),
96 node.filename(),
97 node.constList()[1].line(),
98 node.constList()[1].col(),
99 arg);
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)
103 {
104 for (std::size_t i = 2, end = node.constList().size(); i < end; ++i)
105 {
106 if (node.constList()[i].nodeType() == NodeType::Symbol && node.constList()[i].string() == arg)
107 throw CodeError(
108 fmt::format("MutabilityError: Can not {} the list `{}' to itself", funcname, arg),
109 node.filename(),
110 node.constList()[1].line(),
111 node.constList()[1].col(),
112 arg);
113 }
114 }
115 }
116
117 for (auto& child : node.list())
118 visit(child, register_declarations);
119 }
120 }
121 break;
122
124 {
125 auto& namespace_ = node.arkNamespace();
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);
131
132 visit(*namespace_.ast, /* register_declarations= */ true);
133 // dual visit so that we can handle forward references
134 visit(*namespace_.ast, /* register_declarations= */ false);
135
136 // if we had specific symbols to import, check that those exist
137 if (!namespace_.symbols.empty())
138 {
139 for (const auto& sym : namespace_.symbols)
140 {
141 if (!scope->get(sym, true).has_value())
142 throw CodeError(
143 fmt::format("ImportError: Can not import symbol {} from {}, as it isn't in the package", sym, namespace_.name),
144 namespace_.ast->filename(),
145 namespace_.ast->line(),
146 namespace_.ast->col(),
147 "import");
148 }
149 }
150
152 break;
153 }
154
155 default:
156 break;
157 }
158 }
159
160 void NameResolutionPass::visitKeyword(Node& node, const Keyword keyword, const bool register_declarations)
161 {
162 switch (keyword)
163 {
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)
172 visit(node.list()[2], register_declarations);
173 if (node.constList().size() > 1 && node.constList()[1].nodeType() == NodeType::Symbol)
174 {
175 const std::string& name = node.constList()[1].string();
176 if (m_language_symbols.contains(name) && register_declarations)
177 throw CodeError(
178 fmt::format("Can not use a reserved identifier ('{}') as a {} name.", name, keyword == Keyword::Let ? "constant" : "variable"),
179 node.filename(),
180 node.constList()[1].line(),
181 node.constList()[1].col(),
182 name);
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 node.filename(),
188 node.constList()[1].line(),
189 node.constList()[1].col(),
190 name);
191 if (keyword == Keyword::Set && m_scope_resolver.isRegistered(name))
192 {
193 if (m_scope_resolver.isImmutable(name).value_or(false) && register_declarations)
194 throw CodeError(
195 fmt::format("MutabilityError: Can not set the constant `{}' to {}", name, node.constList()[2].repr()),
196 node.filename(),
197 node.constList()[1].line(),
198 node.constList()[1].col(),
199 name);
200
202 }
203 else if (keyword != Keyword::Set)
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);
208 if (register_declarations)
209 node.list()[1].setString(fully_qualified_name);
210 }
211 }
212 break;
213
214 case Keyword::Import:
215 if (!node.constList().empty())
216 m_plugin_names.push_back(node.constList()[1].constList().back().string());
217 break;
218
219 case Keyword::While:
220 // create a new scope to track variables
222 for (auto& child : node.list())
223 visit(child, register_declarations);
224 // remove the scope once the loop has been compiled, only we were registering declarations
226 break;
227
228 case Keyword::Fun:
229 // create a new scope to track variables
231
232 if (node.constList()[1].nodeType() == NodeType::List)
233 {
234 for (auto& child : node.list()[1].list())
235 {
236 if (child.nodeType() == NodeType::Capture)
237 {
238 if (!m_scope_resolver.isRegistered(child.string()) && register_declarations)
239 throw CodeError(
240 fmt::format("Can not capture {} because it is referencing a variable defined in an unreachable scope.", child.string()),
241 child.filename(),
242 child.line(),
243 child.col(),
244 child.repr());
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);
249 addDefinedSymbol(fqn, true);
250 }
251 else if (child.nodeType() == NodeType::Symbol)
252 addDefinedSymbol(child.string(), /* is_mutable= */ true);
253 }
254 }
255 if (node.constList().size() > 2)
256 visit(node.list()[2], register_declarations);
257
258 // remove the scope once the function has been compiled, only we were registering declarations
260 break;
261
262 default:
263 for (auto& child : node.list())
264 visit(child, register_declarations);
265 break;
266 }
267 }
268
269 void NameResolutionPass::addSymbolNode(const Node& symbol, const std::string& old_name)
270 {
271 const std::string& name = symbol.string();
272
273 // we don't accept builtins/operators as a user symbol
274 if (m_language_symbols.contains(name))
275 return;
276
277 // remove the old name node, to avoid false positive when looking for unbound symbols
278 if (!old_name.empty())
279 {
280 auto it = std::ranges::find_if(m_symbol_nodes, [&old_name, &symbol](const Node& sym_node) -> bool {
281 return sym_node.string() == old_name &&
282 sym_node.col() == symbol.col() &&
283 sym_node.line() == symbol.line() &&
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 symbol.filename(),
322 symbol.line(),
323 symbol.col(),
324 symbol.repr());
325 }
326 if (!allowed)
327 {
328 std::string message;
329 if (fqn.ends_with("#hidden"))
330 message = fmt::format(
331 R"(Unbound variable "{}". However, it exists in a namespace as "{}", did you forget to add it to the symbol list while importing?)",
332 symbol.string(),
333 fqn.substr(0, fqn.find_first_of('#')));
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);
336 throw CodeError(
337 message,
338 symbol.filename(),
339 symbol.line(),
340 symbol.col(),
341 symbol.repr());
342 }
343
344 symbol.setString(fqn);
345 return fqn;
346 }
347
349 {
350 for (const auto& sym : m_symbol_nodes)
351 {
352 const auto& str = sym.string();
353 const bool is_plugin = mayBeFromPlugin(str);
354
355 if (!m_defined_symbols.contains(str) && !is_plugin)
356 {
357 std::string message;
358
359 const std::string suggestion = offerSuggestion(str);
360 if (suggestion.empty())
361 message = fmt::format(R"(Unbound variable error "{}" (variable is used but not defined))", str);
362 else
363 {
364 const std::string prefix = suggestion.substr(0, suggestion.find_first_of(':'));
365 const std::string note_about_prefix = fmt::format(
366 " You either forgot to import it in the symbol list (eg `(import {} :{})') or need to fully qualify it by adding the namespace",
367 prefix,
368 str);
369 const bool add_note = suggestion.ends_with(":" + str);
370 message = fmt::format(R"(Unbound variable error "{}" (did you mean "{}"?{}))", str, suggestion, add_note ? note_about_prefix : "");
371 }
372
373 throw CodeError(message, sym.filename(), sym.line(), sym.col(), sym.repr());
374 }
375 }
376 }
377
378 std::string NameResolutionPass::offerSuggestion(const std::string& str) const
379 {
380 auto iterate = [](const std::string& word, const std::unordered_set<std::string>& dict) -> std::string {
381 std::string suggestion;
382 // our suggestion shouldn't require more than half the string to change
383 std::size_t suggestion_distance = word.size() / 2;
384 for (const std::string& symbol : dict)
385 {
386 const std::size_t current_distance = Utils::levenshteinDistance(word, symbol);
387 if (current_distance <= suggestion_distance)
388 {
389 suggestion_distance = current_distance;
390 suggestion = symbol;
391 }
392 }
393 return suggestion;
394 };
395
396 std::string suggestion = iterate(str, m_defined_symbols);
397 // look for a suggestion related to language builtins
398 if (suggestion.empty())
399 suggestion = iterate(str, m_language_symbols);
400 // look for a suggestion related to a namespace change
401 if (suggestion.empty())
402 {
403 if (const auto it = std::ranges::find_if(m_defined_symbols, [&str](const std::string& symbol) {
404 return symbol.ends_with(":" + str);
405 });
406 it != m_defined_symbols.end())
407 suggestion = *it;
408 }
409
410 return suggestion;
411 }
412}
Lots of utilities about string, filesystem and more.
Host the declaration of all the ArkScript builtins.
ArkScript homemade exceptions.
bool shouldTrace() const
Definition Logger.hpp:39
void trace(const char *fmt, Args &&... args)
Write a trace level log using fmtlib.
Definition Logger.hpp:98
void traceStart(std::string &&trace_name)
Definition Logger.hpp:75
std::vector< std::string > m_plugin_names
void visit(Node &node, bool register_declarations)
Recursively visit nodes.
void visitKeyword(Node &node, Keyword keyword, bool register_declarations)
const Node & ast() const noexcept override
Unused overload that return the input AST (untouched as this pass only generates errors)
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.
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.
void process(const Node &ast) override
Start visiting the given AST, checking for mutability violation and unbound variables.
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:31
NodeType nodeType() const noexcept
Return the node type.
Definition Node.cpp:77
const std::string & filename() const noexcept
Return the filename in which this node was created.
Definition Node.cpp:145
const std::string & string() const noexcept
Return the string held by the value (if the node type allows it)
Definition Node.cpp:37
const std::vector< Node > & constList() const noexcept
Return the list of sub-nodes held by the node.
Definition Node.cpp:72
Namespace & arkNamespace() noexcept
Return the namespace held by the value (if the node type allows it)
Definition Node.cpp:52
std::string repr() const noexcept
Compute a representation of the node without any comments or additional sugar, colors,...
Definition Node.cpp:160
std::ostream & debugPrint(std::ostream &os) const noexcept
Print a node to an output stream with added type annotations.
Definition Node.cpp:233
std::size_t col() const noexcept
Get the column at which this node was created.
Definition Node.cpp:140
void setString(const std::string &value) noexcept
Set the String object.
Definition Node.cpp:103
std::size_t line() const noexcept
Get the line at which this node was created.
Definition Node.cpp:135
std::vector< Node > & list() noexcept
Return the list of sub-nodes held by the node.
Definition Node.cpp:67
An interface to describe compiler passes.
Definition Pass.hpp:23
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...
virtual std::optional< Declaration > get(const std::string &name, bool extensive_lookup)
Try to return a Declaration from this scope with a given name.
std::vector< std::string > splitString(const std::string &source, const char sep)
Cut a string into pieces, given a character separator.
Definition Utils.hpp:32
ARK_API std::size_t levenshteinDistance(const std::string &str1, const std::string &str2)
Calculate the Levenshtein distance between two strings.
Definition Utils.cpp:5
ARK_API const std::vector< std::pair< std::string, Value > > builtins
constexpr std::array< std::string_view, 9 > listInstructions
Definition Common.hpp:100
constexpr std::string_view AppendInPlace
Definition Common.hpp:87
constexpr std::array< std::string_view, 24 > operators
Definition Common.hpp:135
constexpr std::string_view ConcatInPlace
Definition Common.hpp:88
constexpr std::string_view SysArgs
Definition Common.hpp:112
constexpr std::string_view And
Definition Common.hpp:115
constexpr std::array UpdateRef
All the builtins that modify in place a variable.
Definition Common.hpp:93
constexpr std::string_view Or
Definition Common.hpp:116
Keyword
The different keywords available.
Definition Common.hpp:60
CodeError thrown by the compiler (parser, macro processor, optimizer, and compiler itself)