diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 68d10cccd..12d20373f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,7 +22,8 @@ repos: "--inline-suppr", "--suppressions-list=cppcheck-suppressions.txt", "--suppress=unusedStructMember", - "--suppress=checkersReport" + "--suppress=checkersReport", + "--check-level=exhaustive" ] - repo: https://github.com/compilerla/conventional-pre-commit rev: 'v3.2.0' diff --git a/CHANGELOG.md b/CHANGELOG.md index 9101b221a..9b44c26a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ - new `MAKE_CLOSURE ` instruction, generated in place of a `LOAD_CONST` when a closure is made - added `-fdump-ir` to dump the IR entities to a file named `{file}.ark.ir` - added 11 super instructions and their implementation to the VM +- support for the glob import syntax and symbol import syntax ### Changed - instructions are on 4 bytes: 1 byte for the instruction, 1 byte of padding, 2 bytes for an immediate argument @@ -90,6 +91,7 @@ - the parser can detect ill-formed macros (that are seen as function macros while being value macros) - adding a `CALL_BUILTIN ` super instruction - fixed formatting of comments after the last symbol in an import node +- renamed `str:xyz` builtins to `string:xyz` for uniformity with the standard library ### Removed - removed unused `NodeType::Closure` diff --git a/examples/99bottles.ark b/examples/99bottles.ark index 6e54da720..5ddb82010 100644 --- a/examples/99bottles.ark +++ b/examples/99bottles.ark @@ -20,6 +20,6 @@ (mut n i) (while (> n 1) { - (print (str:format "{} Bottles of beer on the wall\n{} bottles of beer\nTake one down, pass it around" n n)) + (print (string:format "{} Bottles of beer on the wall\n{} bottles of beer\nTake one down, pass it around" n n)) (set n (- n 1)) - (print (str:format "{} Bottles of beer on the wall." n)) }) + (print (string:format "{} Bottles of beer on the wall." n)) }) diff --git a/examples/blockchain.ark b/examples/blockchain.ark index aa48d3f75..e06bed22f 100644 --- a/examples/blockchain.ark +++ b/examples/blockchain.ark @@ -104,9 +104,9 @@ (let new (json:fromString request)) (set nodes_transactions (append nodes_transactions new)) (print "New transaction") - (print (str:format "FROM: {}" (json:get new "from"))) - (print (str:format "TO: {}" (json:get new "to"))) - (print (str:format "AMOUNT: {}" (json:get new "amount"))) + (print (string:format "FROM: {}" (json:get new "from"))) + (print (string:format "TO: {}" (json:get new "to"))) + (print (string:format "AMOUNT: {}" (json:get new "amount"))) # return value [200 "transaction submission successful" "text/plain"] })) diff --git a/examples/sum_digits.ark b/examples/sum_digits.ark index 700a4c477..64b8c2339 100644 --- a/examples/sum_digits.ark +++ b/examples/sum_digits.ark @@ -2,7 +2,7 @@ (import std.String) (let to-base (fun (n base) { - (let o (str:ord n)) + (let o (string:ord n)) (let v (if (and (>= o 48) (<= o 57)) diff --git a/include/Ark/Builtins/Builtins.hpp b/include/Ark/Builtins/Builtins.hpp index e59bd5dd2..4bb4e4f3d 100644 --- a/include/Ark/Builtins/Builtins.hpp +++ b/include/Ark/Builtins/Builtins.hpp @@ -72,11 +72,11 @@ namespace Ark::internal::Builtins namespace String { - Value format(std::vector& n, VM* vm); // str:format, multiple arguments - Value findSubStr(std::vector& n, VM* vm); // str:find, 2 arguments - Value removeAtStr(std::vector& n, VM* vm); // str:removeAt, 2 arguments - Value ord(std::vector& n, VM* vm); // str:ord, 1 arguments - Value chr(std::vector& n, VM* vm); // str:chr, 1 arguments + Value format(std::vector& n, VM* vm); // string:format, multiple arguments + Value findSubStr(std::vector& n, VM* vm); // string:find, 2 arguments + Value removeAtStr(std::vector& n, VM* vm); // string:removeAt, 2 arguments + Value ord(std::vector& n, VM* vm); // string:ord, 1 arguments + Value chr(std::vector& n, VM* vm); // string:chr, 1 arguments } namespace Mathematics diff --git a/include/Ark/Compiler/AST/Import.hpp b/include/Ark/Compiler/AST/Import.hpp index 4ebeec9c3..f25f28992 100644 --- a/include/Ark/Compiler/AST/Import.hpp +++ b/include/Ark/Compiler/AST/Import.hpp @@ -1,5 +1,5 @@ -#ifndef COMPILER_AST_IMPORT_HPP -#define COMPILER_AST_IMPORT_HPP +#ifndef ARK_COMPILER_AST_IMPORT_HPP +#define ARK_COMPILER_AST_IMPORT_HPP #include #include @@ -31,13 +31,18 @@ namespace Ark::internal std::vector package; /** - * @brief Import with prefix (the package) or not + * @brief Import with prefix (import package) * */ bool with_prefix = true; /** - * @brief List of symbols to import, can be empty if none provided + * @brief Import as glob (import package:*) + */ + bool is_glob = false; + + /** + * @brief List of symbols to import, can be empty if none provided. (import package :a :b) * */ std::vector symbols; @@ -68,17 +73,6 @@ namespace Ark::internal }); } - /** - * @brief Check if we should import everything, given something like `(import foo.bar.egg:*)` - * - * @return true if all symbols of the file should be imported in the importer scope - * @return false otherwise - */ - [[nodiscard]] bool isGlob() const - { - return !with_prefix && symbols.empty(); - } - /** * @brief Check if we should import everything with a prefix, given a `(import foo.bar.egg)` * diff --git a/include/Ark/Compiler/AST/Namespace.hpp b/include/Ark/Compiler/AST/Namespace.hpp new file mode 100644 index 000000000..3e039f680 --- /dev/null +++ b/include/Ark/Compiler/AST/Namespace.hpp @@ -0,0 +1,33 @@ +#ifndef ARK_COMPILER_AST_NAMESPACE_HPP +#define ARK_COMPILER_AST_NAMESPACE_HPP + +#include +#include +#include + +namespace Ark::internal +{ + class Node; + + struct Namespace + { + std::string name; + bool is_glob; // (import package:*) + bool with_prefix; // (import package) + std::vector symbols; // (import package :a :b) + std::shared_ptr ast; + }; + + inline bool operator==(const Namespace& A, const Namespace& B) + { + return A.name == B.name && A.is_glob == B.is_glob && + A.with_prefix == B.with_prefix; + } + + inline bool operator<([[maybe_unused]] const Namespace&, [[maybe_unused]] const Namespace&) + { + return true; + } +} + +#endif // ARK_COMPILER_AST_NAMESPACE_HPP diff --git a/include/Ark/Compiler/AST/Node.hpp b/include/Ark/Compiler/AST/Node.hpp index c70163986..46c6e3261 100644 --- a/include/Ark/Compiler/AST/Node.hpp +++ b/include/Ark/Compiler/AST/Node.hpp @@ -17,6 +17,7 @@ #include #include +#include #include #include @@ -29,7 +30,7 @@ namespace Ark::internal class ARK_API Node { public: - using Value = std::variant>; + using Value = std::variant, Namespace>; Node() = default; @@ -39,6 +40,7 @@ namespace Ark::internal explicit Node(double value); explicit Node(long value); explicit Node(Keyword value); + explicit Node(Namespace namespace_); /** * @brief Return the string held by the value (if the node type allows it) @@ -61,6 +63,20 @@ namespace Ark::internal */ [[nodiscard]] Keyword keyword() const noexcept; + /** + * @brief Return the namespace held by the value (if the node type allows it) + * + * @return Namespace& + */ + [[nodiscard]] Namespace& arkNamespace() noexcept; + + /** + * @brief Return the namespace held by the value (if the node type allows it) + * + * @return const Namespace& + */ + [[nodiscard]] const Namespace& constArkNamespace() const noexcept; + /** * @brief Every node has a list as well as a value so we can push_back on all node no matter their type * @@ -96,6 +112,13 @@ namespace Ark::internal */ [[nodiscard]] bool isListLike() const noexcept; + /** + * @brief Check if the node is a string like node + * @return true if the node is either a symbol, a string or a spread + * @return false + */ + [[nodiscard]] bool isStringLike() const noexcept; + /** * @brief Copy a node to the current one, while keeping the filename and position in the file * diff --git a/include/Ark/Compiler/Common.hpp b/include/Ark/Compiler/Common.hpp index 7d293e224..1e4a64b02 100644 --- a/include/Ark/Compiler/Common.hpp +++ b/include/Ark/Compiler/Common.hpp @@ -2,7 +2,7 @@ * @file Common.hpp * @author Alexandre Plateau (lexplt.dev@gmail.com) * @brief Common code for the compiler - * @version 0.4 + * @version 0.5 * @date 2021-10-02 * * @copyright Copyright (c) 2021-2024 @@ -36,11 +36,12 @@ namespace Ark::internal Spread, Field, Macro, + Namespace, Unused }; /// Node types as string, in the same order as the enum NodeType - constexpr std::array nodeTypes = { + constexpr std::array nodeTypes = { "Symbol", "Capture", "Keyword", @@ -50,6 +51,7 @@ namespace Ark::internal "Spread", "Field", "Macro", + "Namespace", "Unused" }; diff --git a/include/Ark/Compiler/Macros/Processor.hpp b/include/Ark/Compiler/Macros/Processor.hpp index 2ffe3542c..0eee108c5 100644 --- a/include/Ark/Compiler/Macros/Processor.hpp +++ b/include/Ark/Compiler/Macros/Processor.hpp @@ -130,8 +130,9 @@ namespace Ark::internal * * @param node node on which to operate * @param depth + * @param is_processing_namespace */ - void processNode(Node& node, unsigned depth); + void processNode(Node& node, unsigned depth, bool is_processing_namespace = false); /** * @brief Apply a macro on a given node diff --git a/include/Ark/Compiler/NameResolutionPass.hpp b/include/Ark/Compiler/NameResolution/NameResolutionPass.hpp similarity index 50% rename from include/Ark/Compiler/NameResolutionPass.hpp rename to include/Ark/Compiler/NameResolution/NameResolutionPass.hpp index edd25c6ee..fbf4686cc 100644 --- a/include/Ark/Compiler/NameResolutionPass.hpp +++ b/include/Ark/Compiler/NameResolution/NameResolutionPass.hpp @@ -14,110 +14,13 @@ #include #include -#include #include #include +#include namespace Ark::internal { - struct Variable - { - std::string name; - bool is_mutable; - - bool operator==(const Variable& other) const = default; - }; -} - -template <> -struct std::hash -{ - inline size_t operator()(const Ark::internal::Variable& x) const noexcept - { - return std::hash {}(x.name); - } -}; - -namespace Ark::internal -{ - class ScopeResolver - { - public: - /** - * @brief Create a ScopeResolver - * @details Kickstart by create a default global scope - */ - ScopeResolver(); - - /** - * @brief Create a new scope - */ - void createNew(); - - /** - * @brief Remove the last scope - */ - void removeLocalScope(); - - /** - * @brief Register a variable in the current (last) scope - * @param name - * @param is_mutable - */ - void registerInCurrent(const std::string& name, bool is_mutable); - - /** - * @brief Checks the scopes in reverse order for 'name' and returns its mutability status - * @param name - * @return std::nullopt if the variable could not be found - * @return true if immutable - * @return false if mutable - */ - [[nodiscard]] std::optional isImmutable(const std::string& name) const; - - /** - * @brief Checks if any scope has 'name', in reverse order - * @param name - * @return - */ - [[nodiscard]] bool isRegistered(const std::string& name) const; - - /** - * @brief Checks if 'name' is in the current scope - * - * @param name - * @return - */ - [[nodiscard]] bool isInScope(const std::string& name) const; - - class Scope - { - public: - /** - * @brief Add a variable to the scope, given a mutability status - * @param name - * @param is_mutable - */ - void add(const std::string& name, bool is_mutable); - - /** - * @brief Try to return a variable from this scope with a given name. - * @param name - * @return std::optional std::nullopt if the variable isn't in scope - */ - [[nodiscard]] std::optional get(const std::string& name) const; - - [[nodiscard]] bool has(const std::string& name) const; - - private: - std::unordered_set m_vars {}; - }; - - private: - std::vector m_scopes; - }; - class NameResolutionPass final : public Pass { public: @@ -145,7 +48,7 @@ namespace Ark::internal * @param sym * @param is_mutable true if the symbol is inside mut/set, false otherwise (let) */ - void addDefinedSymbol(const std::string& sym, bool is_mutable); + std::string addDefinedSymbol(const std::string& sym, bool is_mutable); private: Node m_ast; @@ -158,22 +61,25 @@ namespace Ark::internal /** * @brief Recursively visit nodes * @param node node to visit + * @param register_declarations whether or not the visit should register declarations */ - void visit(const Node& node); + void visit(Node& node, bool register_declarations); /** * * @param node a list node whose first child is a keyword * @param keyword + * @param register_declarations whether or not the visit should register declarations */ - void visitKeyword(const Node& node, Keyword keyword); + void visitKeyword(Node& node, Keyword keyword, bool register_declarations); /** * @brief Register a given node in the symbol table * * @param symbol + * @param old_name old name for symbol, to replace it with the new one if it was renamed */ - void addSymbolNode(const Node& symbol); + void addSymbolNode(const Node& symbol, const std::string& old_name = ""); /** * @brief Checking if a symbol may be coming from a plugin @@ -184,6 +90,8 @@ namespace Ark::internal */ [[nodiscard]] bool mayBeFromPlugin(const std::string& name) const noexcept; + std::string updateSymbolWithFullyQualifiedName(Node& symbol); + /** * @brief Checks for undefined symbols, not present in the defined symbols table * diff --git a/include/Ark/Compiler/NameResolution/ScopeResolver.hpp b/include/Ark/Compiler/NameResolution/ScopeResolver.hpp new file mode 100644 index 000000000..489f933af --- /dev/null +++ b/include/Ark/Compiler/NameResolution/ScopeResolver.hpp @@ -0,0 +1,125 @@ +/** + * @file ScopeResolver.hpp + * @author Alexandre Plateau (lexplt.dev@gmail.com) + * @brief Handle scope resolution at compile time + * @version 0.1 + * @date 2024-11-30 + * + * @copyright Copyright (c) 2024 + * + */ + +#ifndef ARK_COMPILER_NAMERESOLUTION_SCOPERESOLVER_HPP +#define ARK_COMPILER_NAMERESOLUTION_SCOPERESOLVER_HPP + +#include +#include +#include +#include +#include + +#include + +namespace Ark::internal +{ + class ScopeResolver + { + public: + /** + * @brief Create a ScopeResolver + * @details Kickstart by create a default global scope + */ + ScopeResolver(); + + ScopeResolver(const ScopeResolver&) = delete; + ScopeResolver& operator=(const ScopeResolver&) = delete; + ScopeResolver(ScopeResolver&&) = default; + ScopeResolver& operator=(ScopeResolver&&) = default; + + /** + * @brief Create a new scope + */ + void createNew(); + + /** + * @brief Remove the last scope + */ + void removeLastScope(); + + /** + * @brief Create a new namespace scope + * @param name + * @param with_prefix + * @param is_glob + * @param symbols + */ + void createNewNamespace(const std::string& name, bool with_prefix, bool is_glob, const std::vector& symbols); + + /** + * @brief Register a Declaration in the current (last) scope + * @param name + * @param is_mutable + * @return std::string the fully qualified name assigned by the scope + */ + std::string registerInCurrent(const std::string& name, bool is_mutable); + + /** + * @brief Save the last scope as a namespace, by attaching it to the nearest namespace scope + * @details Also handle removing the scope from the scope pile. + */ + void saveNamespaceAndRemove(); + + /** + * @brief Checks the scopes in reverse order for 'name' and returns its mutability status + * @param name + * @return std::nullopt if the Declaration could not be found + * @return true if immutable + * @return false if mutable + */ + [[nodiscard]] std::optional isImmutable(const std::string& name) const; + + /** + * @brief Checks if any scope has 'name', in reverse order + * @param name + * @return + */ + [[nodiscard]] bool isRegistered(const std::string& name) const; + + /** + * @brief Checks if 'name' is in the current scope + * + * @param name + * @return + */ + [[nodiscard]] bool isInScope(const std::string& name) const; + + /** + * @brief Get a FQN from a variable name in the nearest scope it is declared in + * + * @param name + * @return std::string + */ + [[nodiscard]] std::string getFullyQualifiedNameInNearestScope(const std::string& name) const; + + /** + * @brief Checks if a name can be fully qualified (allows only unprefixed names to be resolved by glob namespaces or inside their own namespace) + * + * @param name + * @return std::pair if the name can be fully qualified, first element is true ; second element is the FQN + */ + [[nodiscard]] std::pair canFullyQualifyName(const std::string& name); + + /** + * @brief Return a non-owning raw pointer to the current scope + * + * @return StaticScope* non-owning pointer to the current scope + * @return nullptr if there are no scope + */ + [[nodiscard]] StaticScope* currentScope() const; + + private: + std::vector> m_scopes; + }; +} + +#endif // ARK_COMPILER_NAMERESOLUTION_SCOPERESOLVER_HPP diff --git a/include/Ark/Compiler/NameResolution/StaticScope.hpp b/include/Ark/Compiler/NameResolution/StaticScope.hpp new file mode 100644 index 000000000..996320bf4 --- /dev/null +++ b/include/Ark/Compiler/NameResolution/StaticScope.hpp @@ -0,0 +1,137 @@ +/** + * @file StaticScope.hpp + * @author Alexandre Plateau (lexplt.dev@gmail.com) + * @brief + * @version 0.1 + * @date 2024-11-30 + * + * @copyright Copyright (c) 2024 + * + */ + +#ifndef ARK_COMPILER_NAMERESOLUTION_STATICSCOPE_HPP +#define ARK_COMPILER_NAMERESOLUTION_STATICSCOPE_HPP + +#include +#include +#include +#include +#include + +namespace Ark::internal +{ + struct Declaration + { + std::string name; + bool is_mutable; + + bool operator==(const Declaration& other) const = default; + }; +} + +template <> +struct std::hash +{ + inline size_t operator()(const Ark::internal::Declaration& x) const noexcept + { + return std::hash {}(x.name); + } +}; + +namespace Ark::internal +{ + class StaticScope + { + public: + virtual ~StaticScope() = default; + + /** + * @brief Add a Declaration to the scope, given a mutability status + * @param name + * @param is_mutable + */ + virtual void add(const std::string& name, bool is_mutable); + + /** + * @brief Try to return a Declaration from this scope with a given name. + * @param name + * @param extensive_lookup unused in StaticScope + * @return std::optional std::nullopt if the Declaration isn't in scope + */ + [[nodiscard]] virtual std::optional get(const std::string& name, bool extensive_lookup) const; + + /** + * @brief Given a Declaration name, compute its fully qualified name + * @param name + * @return std::string fully qualified name in the scope + */ + [[nodiscard]] virtual std::string fullyQualifiedName(const std::string& name) const; + + /** + * @brief Save a namespace scope to help with lookup + * + * @return true if the scope was saved, on NamespaceScope + * @return false on StaticScope + */ + virtual bool saveNamespace(std::unique_ptr&); + + [[nodiscard]] virtual bool isNamespace() const; + [[nodiscard]] inline virtual bool withPrefix() const { return false; } + [[nodiscard]] inline virtual bool isGlob() const { return false; } + [[nodiscard]] inline virtual std::string prefix() const { return ""; } + + private: + std::unordered_set m_vars {}; + }; + + class NamespaceScope final : public StaticScope + { + public: + NamespaceScope(std::string name, bool with_prefix, bool is_glob, const std::vector& symbols); + + /** + * @brief Add a Declaration to the scope, given a mutability status + * @param name + * @param is_mutable + */ + void add(const std::string& name, bool is_mutable) override; + + /** + * @brief Try to return a Declaration from this scope with a given name. + * @param name + * @param extensive_lookup if true, use the additional saved namespaces + * @return std::optional std::nullopt if the Declaration isn't in scope + */ + [[nodiscard]] std::optional get(const std::string& name, bool extensive_lookup) const override; + + /** + * @brief Given a Declaration name, compute its fully qualified name + * @param name + * @return std::string fully qualified name in the namespace + */ + [[nodiscard]] std::string fullyQualifiedName(const std::string& name) const override; + + /** + * @brief Save a namespace scope to help with lookup + * + * @return true if the scope was saved, on NamespaceScope + * @return false on StaticScope + */ + bool saveNamespace(std::unique_ptr&) override; + + [[nodiscard]] bool isNamespace() const override; + [[nodiscard]] inline bool withPrefix() const override { return m_with_prefix; } + [[nodiscard]] inline bool isGlob() const override { return m_is_glob; } + [[nodiscard]] inline std::string prefix() const override { return m_namespace; } + + private: + std::string m_namespace; + bool m_with_prefix; + bool m_is_glob; + std::vector m_symbols; + std::unordered_set m_vars {}; + std::vector> m_additional_namespaces; + }; +} + +#endif // ARK_COMPILER_NAMERESOLUTION_STATICSCOPE_HPP diff --git a/include/Ark/Compiler/Package/ImportSolver.hpp b/include/Ark/Compiler/Package/ImportSolver.hpp index 3b8be5526..5f301cdc3 100644 --- a/include/Ark/Compiler/Package/ImportSolver.hpp +++ b/include/Ark/Compiler/Package/ImportSolver.hpp @@ -21,7 +21,7 @@ #include #include #include -#include +#include namespace Ark::internal { @@ -53,9 +53,8 @@ namespace Ark::internal std::filesystem::path m_root; ///< Folder were the entry file is Node m_ast; std::stack m_imports; - std::unordered_map m_modules; ///< Package to module map - // TODO is this ok? is this fine? this is sort of ugly - std::vector m_imported; ///< List of imports, in the order they were found and parsed + std::unordered_map m_packages; ///< Package name to package AST & data mapping + std::vector m_imported; ///< List of imports, in the order they were found and parsed /** * @brief Visits the AST, looking for import nodes to replace with their parsed module version diff --git a/include/Ark/Compiler/Package/Module.hpp b/include/Ark/Compiler/Package/Module.hpp deleted file mode 100644 index 4a0dac45a..000000000 --- a/include/Ark/Compiler/Package/Module.hpp +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef ARK_MODULE_HPP -#define ARK_MODULE_HPP - -#include - -namespace Ark::internal -{ - // TODO store something better than just the AST (AST+what we are importing as private/public/namespaced... vs all) - // so that we can remember the order in which we encountered imports. - struct Module - { - Node ast; - bool has_been_processed = false; // TODO document this - }; -} - -#endif // ARK_MODULE_HPP diff --git a/include/Ark/Compiler/Package/Package.hpp b/include/Ark/Compiler/Package/Package.hpp new file mode 100644 index 000000000..63e3a658b --- /dev/null +++ b/include/Ark/Compiler/Package/Package.hpp @@ -0,0 +1,17 @@ +#ifndef ARK_PACKAGE_HPP +#define ARK_PACKAGE_HPP + +#include +#include + +namespace Ark::internal +{ + struct Package + { + Node ast; + Import import; + bool has_been_processed = false; ///< Set to false for source code files, to indicate they need parsing. True for modules (.arkm), as we cannot parse those + }; +} + +#endif // ARK_PACKAGE_HPP diff --git a/include/Ark/Compiler/Welder.hpp b/include/Ark/Compiler/Welder.hpp index 2fb96bfa9..2249bc4b3 100644 --- a/include/Ark/Compiler/Welder.hpp +++ b/include/Ark/Compiler/Welder.hpp @@ -27,7 +27,7 @@ #include #include #include -#include +#include namespace Ark { diff --git a/include/Ark/VM/Value/Closure.hpp b/include/Ark/VM/Value/Closure.hpp index 994bf0d2d..e8ac77a79 100644 --- a/include/Ark/VM/Value/Closure.hpp +++ b/include/Ark/VM/Value/Closure.hpp @@ -61,6 +61,15 @@ namespace Ark::internal */ [[nodiscard]] PageAddr_t pageAddr() const { return m_page_addr; } + /** + * @brief Used when generating error messages in the VM, to see if a symbol might have been wrongly fully qualified + * + * @param end + * @param vm + * @return true if the closure has a field which is the end of 'end' + */ + [[nodiscard]] bool hasFieldEndingWith(const std::string& end, VM& vm) const; + /** * @brief Print the closure to a string * diff --git a/include/CLI/Formatter.hpp b/include/CLI/Formatter.hpp index d4ecf89ee..e51232785 100644 --- a/include/CLI/Formatter.hpp +++ b/include/CLI/Formatter.hpp @@ -110,7 +110,7 @@ class Formatter final */ static std::string prefix(const std::size_t indent) { - return std::string(indent * FormatterConfig.SpacePerIndent, ' '); + return std::string(indent * FormatterConfig::SpacePerIndent, ' '); } /** diff --git a/lib/std b/lib/std index 09bffc93c..096876a94 160000 --- a/lib/std +++ b/lib/std @@ -1 +1 @@ -Subproject commit 09bffc93c00d46f2bc5a6d268c85b15f33c45e88 +Subproject commit 096876a940ebaca3372435a7d19954517e9cc547 diff --git a/src/arkreactor/Builtins/Builtins.cpp b/src/arkreactor/Builtins/Builtins.cpp index a3b962752..f1600247a 100644 --- a/src/arkreactor/Builtins/Builtins.cpp +++ b/src/arkreactor/Builtins/Builtins.cpp @@ -55,11 +55,11 @@ namespace Ark::internal::Builtins { "sys:exit", Value(System::exit_) }, // String - { "str:format", Value(String::format) }, - { "str:find", Value(String::findSubStr) }, - { "str:removeAt", Value(String::removeAtStr) }, - { "str:ord", Value(String::ord) }, - { "str:chr", Value(String::chr) }, + { "string:format", Value(String::format) }, + { "string:find", Value(String::findSubStr) }, + { "string:removeAt", Value(String::removeAtStr) }, + { "string:ord", Value(String::ord) }, + { "string:chr", Value(String::chr) }, // Mathematics { "math:exp", Value(Mathematics::exponential) }, diff --git a/src/arkreactor/Builtins/String.cpp b/src/arkreactor/Builtins/String.cpp index 1ff0a39d9..f44b46dff 100644 --- a/src/arkreactor/Builtins/String.cpp +++ b/src/arkreactor/Builtins/String.cpp @@ -12,16 +12,16 @@ namespace Ark::internal::Builtins::String { /** - * @name str:format + * @name string:format * @brief Format a String given replacements * @details https://fmt.dev/latest/syntax.html * @param format the String to format * @param values as any argument as you need, of any valid ArkScript type * =begin - * (str:format "Hello {}, my name is {}" "world" "ArkScript") + * (string:format "Hello {}, my name is {}" "world" "ArkScript") * # Hello world, my name is ArkScript * - * (str:format "Test {} with {{}}" "1") + * (string:format "Test {} with {{}}" "1") * # Test 1 with {} * =end * @author https://github.com/SuperFola @@ -30,7 +30,7 @@ namespace Ark::internal::Builtins::String { if (n.size() < 2 || n[0].valueType() != ValueType::String) types::generateError( - "str:format", + "string:format", { { types::Contract { { types::Typedef("string", ValueType::String), types::Typedef("value", ValueType::Any, /* variadic */ true) } } } }, n); @@ -60,7 +60,7 @@ namespace Ark::internal::Builtins::String catch (fmt::format_error& e) { throw std::runtime_error( - fmt::format("str:format: can not format \"{}\" ({} argument{} provided) because of {}", + fmt::format("string:format: can not format \"{}\" ({} argument{} provided) because of {}", n[0].stringRef(), n.size() - 1, // if we have more than one argument (not counting the string to format), plural form @@ -70,14 +70,14 @@ namespace Ark::internal::Builtins::String } /** - * @name str:find + * @name string:find * @brief Search a substring in a given String * @details The original String is not modified. Return -1 when not found * @param string the String to search in * @param substr the substring to search for * =begin - * (str:find "hello world" "hello") # 0 - * (str:find "hello world" "aworld") # -1 + * (string:find "hello world" "hello") # 0 + * (string:find "hello world" "aworld") # -1 * =end * @author https://github.com/SuperFola */ @@ -85,7 +85,7 @@ namespace Ark::internal::Builtins::String { if (!types::check(n, ValueType::String, ValueType::String)) types::generateError( - "str:find", + "string:find", { { types::Contract { { types::Typedef("string", ValueType::String), types::Typedef("substr", ValueType::String) } } } }, n); @@ -96,14 +96,14 @@ namespace Ark::internal::Builtins::String } /** - * @name str:removeAt + * @name string:removeAt * @brief Remove a character from a String given an index * @details The original String is not modified * @param string the String to modify * @param index the index of the character to remove (can be negative to search from the end) * =begin - * (str:removeAt "hello world" 0) # "ello world" - * (str:removeAt "hello world" -1) # "hello worl" + * (string:removeAt "hello world" 0) # "ello world" + * (string:removeAt "hello world" -1) # "hello worl" * =end * @author https://github.com/SuperFola */ @@ -111,25 +111,25 @@ namespace Ark::internal::Builtins::String { if (!types::check(n, ValueType::String, ValueType::Number)) types::generateError( - "str:removeAt", + "string:removeAt", { { types::Contract { { types::Typedef("string", ValueType::String), types::Typedef("index", ValueType::Number) } } } }, n); long id = static_cast(n[1].number()); if (id < 0 || std::cmp_greater_equal(id, n[0].stringRef().size())) - throw std::runtime_error(fmt::format("str:removeAt: index {} out of range (length: {})", id, n[0].stringRef().size())); + throw std::runtime_error(fmt::format("string:removeAt: index {} out of range (length: {})", id, n[0].stringRef().size())); n[0].stringRef().erase(static_cast(id), 1); return n[0]; } /** - * @name str:ord + * @name string:ord * @brief Get the ordinal of a given character * @param char a String with a single UTF8 character * =begin - * (str:ord "h") # 104 - * (str:ord "Ô") # 212 + * (string:ord "h") # 104 + * (string:ord "Ô") # 212 * =end * @author https://github.com/SuperFola */ @@ -137,7 +137,7 @@ namespace Ark::internal::Builtins::String { if (!types::check(n, ValueType::String)) types::generateError( - "str:ord", + "string:ord", { { types::Contract { { types::Typedef("string", ValueType::String) } } } }, n); @@ -145,12 +145,12 @@ namespace Ark::internal::Builtins::String } /** - * @name str:chr + * @name string:chr * @brief Create a character from an UTF8 codepoint * @param codepoint an UTF8 codepoint (Number) * =begin - * (str:chr 104) # "h" - * (str:chr 212) # "Ô" + * (string:chr 104) # "h" + * (string:chr 212) # "Ô" * =end * @author https://github.com/SuperFola */ @@ -158,7 +158,7 @@ namespace Ark::internal::Builtins::String { if (!types::check(n, ValueType::Number)) types::generateError( - "str:chr", + "string:chr", { { types::Contract { { types::Typedef("codepoint", ValueType::Number) } } } }, n); diff --git a/src/arkreactor/Compiler/AST/Node.cpp b/src/arkreactor/Compiler/AST/Node.cpp index 5a4415390..d1bd88426 100644 --- a/src/arkreactor/Compiler/AST/Node.cpp +++ b/src/arkreactor/Compiler/AST/Node.cpp @@ -30,6 +30,10 @@ namespace Ark::internal m_type(NodeType::Keyword), m_value(value) {} + Node::Node(Namespace namespace_) : + m_type(NodeType::Namespace), m_value(namespace_) + {} + const std::string& Node::string() const noexcept { return std::get(m_value); @@ -45,6 +49,16 @@ namespace Ark::internal return std::get(m_value); } + Namespace& Node::arkNamespace() noexcept + { + return std::get(m_value); + } + + const Namespace& Node::constArkNamespace() const noexcept + { + return std::get(m_value); + } + void Node::push_back(const Node& node) noexcept { list().push_back(node); @@ -70,6 +84,11 @@ namespace Ark::internal return m_type == NodeType::List || m_type == NodeType::Macro; } + bool Node::isStringLike() const noexcept + { + return m_type == NodeType::Symbol || m_type == NodeType::String || m_type == NodeType::Spread; + } + void Node::updateValueAndType(const Node& source) noexcept { m_type = source.m_type; @@ -198,6 +217,13 @@ namespace Ark::internal data += "..." + string(); break; + // namespace node should not have a representation as it is purely internal, + // and it can't be exploited by macros (unless you try passing an import node + // to a macro, which should not happen?) + case NodeType::Namespace: + data += constArkNamespace().ast->repr(); + break; + case NodeType::Unused: break; } @@ -265,6 +291,14 @@ namespace Ark::internal os << "Spread:" << string(); break; + case NodeType::Namespace: + { + const auto details = constArkNamespace(); + os << "( Namespace:" << details.name << " "; + details.ast->debugPrint(os) << " )"; + break; + } + case NodeType::Unused: os << "Unused:" << string(); break; diff --git a/src/arkreactor/Compiler/AST/Parser.cpp b/src/arkreactor/Compiler/AST/Parser.cpp index 9f3f624c5..144935486 100644 --- a/src/arkreactor/Compiler/AST/Parser.cpp +++ b/src/arkreactor/Compiler/AST/Parser.cpp @@ -341,6 +341,7 @@ namespace Ark::internal // save the import data structure to know we encounter an import node, and retrieve its data more easily later on import_data.with_prefix = false; + import_data.is_glob = true; m_imports.push_back(import_data); return leaf; @@ -458,32 +459,36 @@ namespace Ark::internal while (!isEOF()) { + const auto pos = getCursor(); if (accept(IsChar('&'))) // captures { has_captures = true; std::string capture; if (!name(&capture)) break; - args->push_back(Node(NodeType::Capture, capture).attachNearestCommentBefore(comment)); - comment.clear(); - newlineOrComment(&comment); + Node node = Node(NodeType::Capture, capture).attachNearestCommentBefore(comment); + setNodePosAndFilename(node, pos); + args->push_back(node); } else { - const auto pos = getCount(); + const auto count = getCount(); std::string symbol; if (!name(&symbol)) break; if (has_captures) { - backtrack(pos); + backtrack(count); error("Captured variables should be at the end of the argument list", symbol); } - args->push_back(Node(NodeType::Symbol, symbol).attachNearestCommentBefore(comment)); - comment.clear(); - newlineOrComment(&comment); + Node node = Node(NodeType::Symbol, symbol).attachNearestCommentBefore(comment); + setNodePosAndFilename(node, pos); + args->push_back(node); } + + comment.clear(); + newlineOrComment(&comment); } if (accept(IsChar(')'))) diff --git a/src/arkreactor/Compiler/Compiler.cpp b/src/arkreactor/Compiler/Compiler.cpp index 3168b3fac..455ceda9e 100644 --- a/src/arkreactor/Compiler/Compiler.cpp +++ b/src/arkreactor/Compiler/Compiler.cpp @@ -140,6 +140,9 @@ namespace Ark::internal if (!is_result_unused) page(p).emplace_back(LOAD_CONST, i); } + // namespace nodes + else if (x.nodeType() == NodeType::Namespace) + compileExpression(*x.constArkNamespace().ast, p, is_result_unused, is_terminal, var_name); // empty code block should be nil else if (x.constList().empty()) { diff --git a/src/arkreactor/Compiler/Macros/Processor.cpp b/src/arkreactor/Compiler/Macros/Processor.cpp index 16ba637a2..f83a1074f 100644 --- a/src/arkreactor/Compiler/Macros/Processor.cpp +++ b/src/arkreactor/Compiler/Macros/Processor.cpp @@ -97,7 +97,7 @@ namespace Ark::internal } } - void MacroProcessor::processNode(Node& node, unsigned depth) + void MacroProcessor::processNode(Node& node, unsigned depth, bool is_processing_namespace) { if (depth >= MaxMacroProcessingDepth) throwMacroProcessingError( @@ -114,27 +114,30 @@ namespace Ark::internal while (i < node.list().size()) { const std::size_t pos = i; - const bool had_begin = isBeginNode(node.list()[pos]); + Node& child = node.list()[pos]; + const bool had_begin = isBeginNode(child); bool added_begin = false; - if (node.list()[pos].nodeType() == NodeType::Macro) + if (child.nodeType() == NodeType::Macro) { // create a scope only if needed - if ((!m_macros.empty() && !m_macros.back().empty() && m_macros.back().depth() < depth) || !has_created) + if ((!m_macros.empty() && !m_macros.back().empty() && m_macros.back().depth() < depth && !is_processing_namespace) || + (!has_created && !is_processing_namespace) || + (m_macros.empty() && is_processing_namespace)) { has_created = true; m_macros.emplace_back(depth); } - handleMacroNode(node.list()[pos]); - added_begin = isBeginNode(node.list()[pos]) && !had_begin; + handleMacroNode(child); + added_begin = isBeginNode(child) && !had_begin; } else // running on non-macros { - applyMacro(node.list()[pos], 0); - added_begin = isBeginNode(node.list()[pos]) && !had_begin; + applyMacro(child, 0); + added_begin = isBeginNode(child) && !had_begin; - if (node.list()[pos].nodeType() == NodeType::Unused) + if (child.nodeType() == NodeType::Unused) node.list().erase(node.constList().begin() + static_cast::difference_type>(pos)); else // Go forward only if it isn't a macro, because we delete macros @@ -147,9 +150,9 @@ namespace Ark::internal // process subnodes if any if (node.nodeType() == NodeType::List && pos < node.constList().size()) { - processNode(node.list()[pos], depth + 1); + processNode(child, depth + 1); // needed if we created a function node from a macro - registerFuncDef(node.list()[pos]); + registerFuncDef(child); } } @@ -159,15 +162,24 @@ namespace Ark::internal if (added_begin) removeBegin(node, pos); // if there is an unused node or a leftover macro need, we need to get rid of it in the final ast - else if (node.list()[pos].nodeType() == NodeType::Macro || node.list()[pos].nodeType() == NodeType::Unused) + else if (child.nodeType() == NodeType::Macro || child.nodeType() == NodeType::Unused) node.list().erase(node.constList().begin() + static_cast::difference_type>(pos)); } } // delete a scope only if needed - if (!m_macros.empty() && m_macros.back().depth() == depth) + if (!m_macros.empty() && m_macros.back().depth() == depth && !is_processing_namespace) m_macros.pop_back(); } + else if (node.nodeType() == NodeType::Namespace) + { + Node& namespace_ast = *node.arkNamespace().ast; + // We have to use depth - 1 because it was incremented previously, as a namespace node + // must be in a list node. Then depth - 1 is safe as depth is at least 1. + // Using a decreased value of depth ensures that macros are stored in the correct scope, + // and not deleted when the namespace traversal ends. + processNode(namespace_ast, depth - 1, /* is_processing_namespace= */ true); + } } bool MacroProcessor::applyMacro(Node& node, const unsigned depth) @@ -648,6 +660,7 @@ namespace Ark::internal case NodeType::Number: case NodeType::Macro: case NodeType::Spread: + case NodeType::Namespace: case NodeType::Unused: return true; } diff --git a/src/arkreactor/Compiler/NameResolutionPass.cpp b/src/arkreactor/Compiler/NameResolution/NameResolutionPass.cpp similarity index 52% rename from src/arkreactor/Compiler/NameResolutionPass.cpp rename to src/arkreactor/Compiler/NameResolution/NameResolutionPass.cpp index da1d854fb..217cf27ef 100644 --- a/src/arkreactor/Compiler/NameResolutionPass.cpp +++ b/src/arkreactor/Compiler/NameResolution/NameResolutionPass.cpp @@ -1,76 +1,11 @@ -#include +#include #include #include #include -#include -#include - namespace Ark::internal { - void ScopeResolver::Scope::add(const std::string& name, bool is_mutable) - { - m_vars.emplace(name, is_mutable); - } - - std::optional ScopeResolver::Scope::get(const std::string& name) const - { - if (const auto it = std::ranges::find(m_vars, name, &Variable::name); it != m_vars.end()) - return *it; - return std::nullopt; - } - - bool ScopeResolver::Scope::has(const std::string& name) const - { - return std::ranges::find(m_vars, name, &Variable::name) != m_vars.end(); - } - - ScopeResolver::ScopeResolver() - { - createNew(); - } - - void ScopeResolver::createNew() - { - m_scopes.emplace_back(); - } - - void ScopeResolver::removeLocalScope() - { - m_scopes.pop_back(); - } - - void ScopeResolver::registerInCurrent(const std::string& name, const bool is_mutable) - { - m_scopes.back().add(name, is_mutable); - } - - std::optional ScopeResolver::isImmutable(const std::string& name) const - { - for (const auto& m_scope : std::ranges::reverse_view(m_scopes)) - { - if (auto maybe = m_scope.get(name); maybe.has_value()) - return !maybe.value().is_mutable; - } - return std::nullopt; - } - - bool ScopeResolver::isRegistered(const std::string& name) const - { - return std::ranges::any_of( - m_scopes.rbegin(), - m_scopes.rend(), - [name](const Scope& scope) { - return scope.has(name); - }); - } - - bool ScopeResolver::isInScope(const std::string& name) const - { - return m_scopes.back().has(name); - } - NameResolutionPass::NameResolutionPass(const unsigned debug) : Pass("NameResolution", debug), m_ast() @@ -92,11 +27,16 @@ namespace Ark::internal m_logger.traceStart("process"); m_ast = ast; - visit(ast); - m_logger.traceStart("checkForUndefinedSymbol"); - checkForUndefinedSymbol(); + visit(m_ast, /* register_declarations= */ true); + m_logger.traceEnd(); + m_logger.trace("AST after name resolution"); + if (m_logger.shouldTrace()) + m_ast.debugPrint(std::cout) << '\n'; + + m_logger.traceStart("checkForUndefinedSymbol"); + checkForUndefinedSymbol(); m_logger.traceEnd(); } @@ -105,37 +45,47 @@ namespace Ark::internal return m_ast; } - void NameResolutionPass::addDefinedSymbol(const std::string& sym, const bool is_mutable) + std::string NameResolutionPass::addDefinedSymbol(const std::string& sym, const bool is_mutable) { - m_defined_symbols.emplace(sym); - m_scope_resolver.registerInCurrent(sym, is_mutable); + const std::string fully_qualified_name = m_scope_resolver.registerInCurrent(sym, is_mutable); + m_defined_symbols.emplace(fully_qualified_name); + return fully_qualified_name; } - void NameResolutionPass::visit(const Node& node) + void NameResolutionPass::visit(Node& node, const bool register_declarations) { switch (node.nodeType()) { case NodeType::Symbol: - addSymbolNode(node); + { + const std::string old_name = node.string(); + updateSymbolWithFullyQualifiedName(node); + addSymbolNode(node, old_name); break; + } case NodeType::Field: - for (const auto& child : node.constList()) - addSymbolNode(child); + for (auto& child : node.list()) + { + const std::string old_name = child.string(); + // in case of field, no need to check if we can fully qualify names + child.setString(m_scope_resolver.getFullyQualifiedNameInNearestScope(old_name)); + addSymbolNode(child, old_name); + } break; case NodeType::List: if (!node.constList().empty()) { if (node.constList()[0].nodeType() == NodeType::Keyword) - visitKeyword(node, node.constList()[0].keyword()); + visitKeyword(node, node.constList()[0].keyword(), register_declarations); else { // function calls // the UpdateRef function calls kind get a special treatment, like let/mut/set, // because we need to check for mutability errors if (node.constList().size() > 1 && node.constList()[0].nodeType() == NodeType::Symbol && - node.constList()[1].nodeType() == NodeType::Symbol) + node.constList()[1].nodeType() == NodeType::Symbol && register_declarations) { const auto funcname = node.constList()[0].string(); const auto arg = node.constList()[1].string(); @@ -164,18 +114,50 @@ namespace Ark::internal } } - for (const auto& child : node.constList()) - visit(child); + for (auto& child : node.list()) + visit(child, register_declarations); } } break; + case NodeType::Namespace: + { + auto& namespace_ = node.arkNamespace(); + // no need to guard createNewNamespace with an if (register_declarations), we want to keep the namespace node + // (which will get ignored by the compiler, that only uses its AST), so that we can (re)construct the + // scopes correctly + m_scope_resolver.createNewNamespace(namespace_.name, namespace_.with_prefix, namespace_.is_glob, namespace_.symbols); + StaticScope* scope = m_scope_resolver.currentScope(); + + visit(*namespace_.ast, /* register_declarations= */ true); + // dual visit so that we can handle forward references + visit(*namespace_.ast, /* register_declarations= */ false); + + // if we had specific symbols to import, check that those exist + if (!namespace_.symbols.empty()) + { + for (const auto& sym : namespace_.symbols) + { + if (!scope->get(sym, true).has_value()) + throw CodeError( + fmt::format("ImportError: Can not import symbol {} from {}, as it isn't in the package", sym, namespace_.name), + node.filename(), + node.constList()[1].line(), + node.constList()[1].col(), + sym); + } + } + + m_scope_resolver.saveNamespaceAndRemove(); + break; + } + default: break; } } - void NameResolutionPass::visitKeyword(const Node& node, const Keyword keyword) + void NameResolutionPass::visitKeyword(Node& node, const Keyword keyword, const bool register_declarations) { switch (keyword) { @@ -187,11 +169,11 @@ namespace Ark::internal // first, visit the value, then register the symbol // this allows us to detect things like (let foo (fun (&foo) ())) if (node.constList().size() > 2) - visit(node.constList()[2]); + visit(node.list()[2], register_declarations); if (node.constList().size() > 1 && node.constList()[1].nodeType() == NodeType::Symbol) { const std::string& name = node.constList()[1].string(); - if (m_language_symbols.contains(name)) + if (m_language_symbols.contains(name) && register_declarations) throw CodeError( fmt::format("Can not use a reserved identifier ('{}') as a {} name.", name, keyword == Keyword::Let ? "constant" : "variable"), node.filename(), @@ -199,28 +181,33 @@ namespace Ark::internal node.constList()[1].col(), name); - if (m_scope_resolver.isInScope(name) && keyword == Keyword::Let) + if (m_scope_resolver.isInScope(name) && keyword == Keyword::Let && register_declarations) throw CodeError( fmt::format("MutabilityError: Can not use 'let' to redefine variable `{}'", name), node.filename(), node.constList()[1].line(), node.constList()[1].col(), name); - else if (keyword == Keyword::Set) + if (keyword == Keyword::Set && m_scope_resolver.isRegistered(name)) { - const auto val = node.constList()[2].repr(); - - if (const auto mutability = m_scope_resolver.isImmutable(name); m_scope_resolver.isRegistered(name) && - mutability.value_or(false)) + if (m_scope_resolver.isImmutable(name).value_or(false) && register_declarations) throw CodeError( - fmt::format("MutabilityError: Can not set the constant `{}' to {}", name, val), + fmt::format("MutabilityError: Can not set the constant `{}' to {}", name, node.constList()[2].repr()), node.filename(), node.constList()[1].line(), node.constList()[1].col(), name); + + updateSymbolWithFullyQualifiedName(node.list()[1]); + } + else if (keyword != Keyword::Set) + { + // update the declared variable name to use the fully qualified name + // this will prevent name conflicts, and handle scope resolution + const std::string fully_qualified_name = addDefinedSymbol(name, keyword != Keyword::Let); + if (register_declarations) + node.list()[1].setString(fully_qualified_name); } - else - addDefinedSymbol(name, keyword != Keyword::Let); } break; @@ -232,51 +219,45 @@ namespace Ark::internal case Keyword::Fun: // create a new scope to track variables m_scope_resolver.createNew(); + if (node.constList()[1].nodeType() == NodeType::List) { - for (const auto& child : node.constList()[1].constList()) + for (auto& child : node.list()[1].list()) { if (child.nodeType() == NodeType::Capture) { - // First, check that the capture is a defined symbol - if (!m_defined_symbols.contains(child.string())) - { - // we didn't find node in the defined symbol list, thus we can't capture node - throw CodeError( - fmt::format("Can not capture {} because it is referencing an unbound variable.", child.string()), - child.filename(), - child.line(), - child.col(), - child.repr()); - } - else if (!m_scope_resolver.isRegistered(child.string())) - { + if (!m_scope_resolver.isRegistered(child.string()) && register_declarations) throw CodeError( fmt::format("Can not capture {} because it is referencing a variable defined in an unreachable scope.", child.string()), child.filename(), child.line(), child.col(), child.repr()); - } - addDefinedSymbol(child.string(), /* is_mutable= */ true); + + // update the declared variable name to use the fully qualified name + // this will prevent name conflicts, and handle scope resolution + std::string fqn = updateSymbolWithFullyQualifiedName(child); + addDefinedSymbol(fqn, true); } else if (child.nodeType() == NodeType::Symbol) addDefinedSymbol(child.string(), /* is_mutable= */ true); } } if (node.constList().size() > 2) - visit(node.constList()[2]); - m_scope_resolver.removeLocalScope(); // and remove it once the function has been compiled + visit(node.list()[2], register_declarations); + + // remove the scope once the function has been compiled, only we were registering declarations + m_scope_resolver.removeLastScope(); break; default: - for (const auto& child : node.constList()) - visit(child); + for (auto& child : node.list()) + visit(child, register_declarations); break; } } - void NameResolutionPass::addSymbolNode(const Node& symbol) + void NameResolutionPass::addSymbolNode(const Node& symbol, const std::string& old_name) { const std::string& name = symbol.string(); @@ -284,6 +265,22 @@ namespace Ark::internal if (m_language_symbols.contains(name)) return; + // remove the old name node, to avoid false positive when looking for unbound symbols + if (!old_name.empty()) + { + auto it = std::ranges::find_if(m_symbol_nodes, [&old_name, &symbol](const Node& sym_node) -> bool { + return sym_node.string() == old_name && + sym_node.col() == symbol.col() && + sym_node.line() == symbol.line() && + sym_node.filename() == symbol.filename(); + }); + if (it != m_symbol_nodes.end()) + { + it->setString(name); + return; + } + } + const auto it = std::ranges::find_if(m_symbol_nodes, [&name](const Node& sym_node) -> bool { return sym_node.string() == name; }); @@ -302,6 +299,26 @@ namespace Ark::internal return it != m_plugin_names.end(); } + std::string NameResolutionPass::updateSymbolWithFullyQualifiedName(Node& symbol) + { + auto [allowed, fqn] = m_scope_resolver.canFullyQualifyName(symbol.string()); + + if (!allowed) + { + std::string match = m_scope_resolver.getFullyQualifiedNameInNearestScope(symbol.string()); + + throw CodeError( + fmt::format(R"(Unbound variable "{}". However, it exists in a namespace as "{}", did you forget to prefix it with its namespace?)", symbol.string(), match), + symbol.filename(), + symbol.line(), + symbol.col(), + symbol.repr()); + } + + symbol.setString(fqn); + return fqn; + } + void NameResolutionPass::checkForUndefinedSymbol() const { for (const auto& sym : m_symbol_nodes) @@ -317,7 +334,11 @@ namespace Ark::internal if (suggestion.empty()) message = fmt::format(R"(Unbound variable error "{}" (variable is used but not defined))", str); else - message = fmt::format(R"(Unbound variable error "{}" (did you mean "{}"?))", str, suggestion); + { + const std::string note_about_macros = " Inside macros, names must be fully qualified with their package prefix"; + const bool add_macro_note = suggestion.ends_with(":" + str); + message = fmt::format(R"(Unbound variable error "{}" (did you mean "{}"?{}))", str, suggestion, add_macro_note ? note_about_macros : ""); + } throw CodeError(message, sym.filename(), sym.line(), sym.col(), sym.repr()); } @@ -326,18 +347,34 @@ namespace Ark::internal std::string NameResolutionPass::offerSuggestion(const std::string& str) const { - std::string suggestion; - // our suggestion shouldn't require more than half the string to change - std::size_t suggestion_distance = str.size() / 2; - - for (const std::string& symbol : m_defined_symbols) - { - const std::size_t current_distance = Utils::levenshteinDistance(str, symbol); - if (current_distance <= suggestion_distance) + auto iterate = [](const std::string& word, const std::unordered_set& dict) -> std::string { + std::string suggestion; + // our suggestion shouldn't require more than half the string to change + std::size_t suggestion_distance = word.size() / 2; + for (const std::string& symbol : dict) { - suggestion_distance = current_distance; - suggestion = symbol; + const std::size_t current_distance = Utils::levenshteinDistance(word, symbol); + if (current_distance <= suggestion_distance) + { + suggestion_distance = current_distance; + suggestion = symbol; + } } + return suggestion; + }; + + std::string suggestion = iterate(str, m_defined_symbols); + // look for a suggestion related to language builtins + if (suggestion.empty()) + suggestion = iterate(str, m_language_symbols); + // look for a suggestion related to a namespace change + if (suggestion.empty()) + { + if (const auto it = std::ranges::find_if(m_defined_symbols, [&str](const std::string& symbol) { + return symbol.ends_with(":" + str); + }); + it != m_defined_symbols.end()) + suggestion = *it; } return suggestion; diff --git a/src/arkreactor/Compiler/NameResolution/ScopeResolver.cpp b/src/arkreactor/Compiler/NameResolution/ScopeResolver.cpp new file mode 100644 index 000000000..bb4a89e50 --- /dev/null +++ b/src/arkreactor/Compiler/NameResolution/ScopeResolver.cpp @@ -0,0 +1,116 @@ +#include + +#include + +namespace Ark::internal +{ + ScopeResolver::ScopeResolver() + { + createNewNamespace("", /* with_prefix= */ false, /* is_glob= */ true, /* symbols= */ {}); + } + + void ScopeResolver::createNew() + { + m_scopes.emplace_back(std::make_unique()); + } + + void ScopeResolver::removeLastScope() + { + m_scopes.pop_back(); + } + + void ScopeResolver::createNewNamespace(const std::string& name, bool with_prefix, bool is_glob, const std::vector& symbols) + { + m_scopes.emplace_back(std::make_unique(name, with_prefix, is_glob, symbols)); + } + + std::string ScopeResolver::registerInCurrent(const std::string& name, const bool is_mutable) + { + m_scopes.back()->add(name, is_mutable); + return m_scopes.back()->fullyQualifiedName(name); + } + + void ScopeResolver::saveNamespaceAndRemove() + { + for (auto& m_scope : std::ranges::reverse_view(m_scopes) | std::ranges::views::drop(1)) + { + if (m_scope->saveNamespace(m_scopes.back())) + break; + } + + m_scopes.pop_back(); + } + + std::optional ScopeResolver::isImmutable(const std::string& name) const + { + for (const auto& m_scope : std::ranges::reverse_view(m_scopes)) + { + if (auto maybe = m_scope->get(name, true); maybe.has_value()) + return !maybe.value().is_mutable; + } + return std::nullopt; + } + + bool ScopeResolver::isRegistered(const std::string& name) const + { + for (const auto& m_scope : std::ranges::reverse_view(m_scopes)) + { + if (m_scope->get(name, true).has_value()) + return true; + } + return false; + } + + bool ScopeResolver::isInScope(const std::string& name) const + { + return m_scopes.back()->get(name, false).has_value(); + } + + std::string ScopeResolver::getFullyQualifiedNameInNearestScope(const std::string& name) const + { + for (const auto& scope : std::ranges::reverse_view(m_scopes)) + { + if (auto maybe_fqn = scope->get(name, true); maybe_fqn.has_value()) + return maybe_fqn.value().name; + } + return name; + } + + std::pair ScopeResolver::canFullyQualifyName(const std::string& name) + { + // a given name can be fully qualified if + // old == new + // old != new and new has prefix + // if the prefix namespace is glob + // if the prefix namespace is with_prefix && it is the top most scope + const std::string maybe_fqn = getFullyQualifiedNameInNearestScope(name); + + if (maybe_fqn == name) + return std::make_pair(true, maybe_fqn); + + const std::string prefix = maybe_fqn.substr(0, maybe_fqn.find_first_of(':')); + auto namespaces = + std::ranges::reverse_view(m_scopes) | std::ranges::views::filter([](const auto& e) { + return e->isNamespace(); + }); + bool top = true; + for (auto& scope : namespaces) + { + if (top && prefix == scope->prefix()) + return std::make_pair(true, maybe_fqn); + if (!top && prefix == scope->prefix() && scope->isGlob()) + return std::make_pair(true, maybe_fqn); + + top = false; + } + + return std::make_pair(false, maybe_fqn); + } + + StaticScope* ScopeResolver::currentScope() const + { + if (!m_scopes.empty()) [[likely]] + return m_scopes.back().get(); + return nullptr; + } +} diff --git a/src/arkreactor/Compiler/NameResolution/StaticScope.cpp b/src/arkreactor/Compiler/NameResolution/StaticScope.cpp new file mode 100644 index 000000000..c052ab149 --- /dev/null +++ b/src/arkreactor/Compiler/NameResolution/StaticScope.cpp @@ -0,0 +1,100 @@ +#include + +#include +#include +#include + +namespace Ark::internal +{ + void StaticScope::add(const std::string& name, bool is_mutable) + { + m_vars.emplace(name, is_mutable); + } + + std::optional StaticScope::get(const std::string& name, [[maybe_unused]] const bool extensive_lookup) const + { + if (const auto it = std::ranges::find(m_vars, name, &Declaration::name); it != m_vars.end()) + return *it; + return std::nullopt; + } + + std::string StaticScope::fullyQualifiedName(const std::string& name) const + { + return name; + } + + bool StaticScope::saveNamespace([[maybe_unused]] std::unique_ptr&) + { + // the scope can not be saved on a static scope + return false; + } + + bool StaticScope::isNamespace() const + { + return false; + } + + NamespaceScope::NamespaceScope(std::string name, const bool with_prefix, const bool is_glob, const std::vector& symbols) : + StaticScope(), + m_namespace(std::move(name)), + m_with_prefix(with_prefix), + m_is_glob(is_glob), + m_symbols(symbols) + {} + + void NamespaceScope::add(const std::string& name, bool is_mutable) + { + m_vars.emplace(fullyQualifiedName(name), is_mutable); + } + + std::optional NamespaceScope::get(const std::string& name, const bool extensive_lookup) const + { + const bool starts_with_prefix = !m_namespace.empty() && name.starts_with(m_namespace + ":"); + // If the name starts with the namespace and we imported the namespace with prefix + // search for name in the namespace + if (starts_with_prefix && m_with_prefix) + { + if (const auto it = std::ranges::find(m_vars, name, &Declaration::name); it != m_vars.end()) + return *it; + } + // If the name does not start with the prefix, and we import through either glob or symbol list + // search for the name in the namespace, while adding the namespace in front (as we use fully + // qualified names when registering declarations). + // If the name wasn't qualified, in a prefixed namespace, look up for it but by qualifying the name + else if (!starts_with_prefix && (m_is_glob || std::ranges::find(m_symbols, name) != m_symbols.end() || m_with_prefix)) + { + if (const auto it_fqn = std::ranges::find(m_vars, fullyQualifiedName(name), &Declaration::name); it_fqn != m_vars.end()) + return *it_fqn; + } + // lookup in the additional saved namespaces + if (extensive_lookup) + { + for (const auto& scope : m_additional_namespaces) + { + if (auto maybe_decl = scope->get(name, extensive_lookup); maybe_decl.has_value()) + return maybe_decl; + } + } + // otherwise we didn't find the name in the namespace + return std::nullopt; + } + + std::string NamespaceScope::fullyQualifiedName(const std::string& name) const + { + const bool starts_with_prefix = !m_namespace.empty() && name.starts_with(m_namespace + ":"); + if (!m_namespace.empty() && !starts_with_prefix) + return fmt::format("{}:{}", m_namespace, name); + return name; + } + + bool NamespaceScope::saveNamespace(std::unique_ptr& scope) + { + m_additional_namespaces.push_back(std::move(scope)); + return true; + } + + bool NamespaceScope::isNamespace() const + { + return true; + } +} diff --git a/src/arkreactor/Compiler/Package/ImportSolver.cpp b/src/arkreactor/Compiler/Package/ImportSolver.cpp index eace7b096..2f68ef8f4 100644 --- a/src/arkreactor/Compiler/Package/ImportSolver.cpp +++ b/src/arkreactor/Compiler/Package/ImportSolver.cpp @@ -2,10 +2,10 @@ #include #include -#include #include #include +#include #include namespace Ark::internal @@ -40,28 +40,22 @@ namespace Ark::internal // It needs to be removed first because we might be adding // other imports later and don't want to pop THEM m_imports.pop(); + const auto package = import.toPackageString(); - // TODO: add special handling for each type of import (prefixed, with symbols, glob pattern) - if (!m_modules.contains(import.toPackageString())) + if (m_packages.contains(package)) { - // NOTE: since the "file" (=root) argument doesn't change between all calls, we could get rid of it - std::vector additional_imports = parseImport(m_root, import); - // TODO import and store the new node as a Module node. - // Module nodes should be scoped relatively to their packages - // They should provide specific methods to resolve symbols, - // mark them as public or private. - // OR we could have a map, update the module - // accordingly, and once we are done concat all the nodes - // in a single AST. - for (auto& additional_import : std::ranges::reverse_view(additional_imports)) - m_imports.push(additional_import); + // merge the definition, so that we can generate valid Full Qualified Names in the name & scope resolver + m_packages[package].import.with_prefix |= import.with_prefix; + m_packages[package].import.is_glob |= import.is_glob; + for (auto&& symbol : import.symbols) + m_packages[package].import.symbols.push_back(symbol); } else { - // TODO: if we already imported a package we should merge their definition - // (import foo:*) > (import foo:a) -- no prefix - // (import foo) -- with prefix - // and then decide what to do with the module + // NOTE: since the "file" (=root) argument doesn't change between all calls, we could get rid of it + std::vector temp = parseImport(m_root, import); + for (auto& additional_import : std::ranges::reverse_view(temp)) + m_imports.push(additional_import); } } @@ -80,23 +74,44 @@ namespace Ark::internal if (x.constList().size() >= 2 && x.constList()[0].nodeType() == NodeType::Keyword && x.constList()[0].keyword() == Keyword::Import) { - // TODO maybe we'll have problems with :* ? - std::string package = std::accumulate( - std::next(x.constList()[1].constList().begin()), - x.constList()[1].constList().end(), - x.constList()[1].constList()[0].string(), + // compute the package string: foo.bar.egg + const auto import_node = x.constList()[1].constList(); + const std::string package = std::accumulate( + std::next(import_node.begin()), + import_node.end(), + import_node[0].string(), [](const std::string& acc, const Node& elem) -> std::string { return acc + "." + elem.string(); }); + // if it wasn't imported already, register it if (std::ranges::find(m_imported, package) == m_imported.end()) { m_imported.push_back(package); // modules are already handled, we can safely replace the node - x = m_modules[package].ast; - if (!m_modules[package].has_been_processed) - x = findAndReplaceImports(x).first; // todo: ? - return std::make_pair(x, !m_modules[package].has_been_processed); + x = m_packages[package].ast; + if (!m_packages[package].has_been_processed) + { + const auto import = m_packages[package].import; + + // prefix to lowercase ; usually considered unsafe (https://devblogs.microsoft.com/oldnewthing/20241007-00/?p=110345) + // but we are dealing with prefix from filenames, thus we can somewhat assume we are in safe zone + std::string prefix = import.prefix; + std::ranges::transform( + prefix, prefix.begin(), + [](auto c) { + return std::tolower(c); + }); + + x = Node(Namespace { + .name = prefix, + .is_glob = import.is_glob, + .with_prefix = import.with_prefix, + .symbols = import.symbols, + .ast = std::make_shared(findAndReplaceImports(x).first) }); + } + // we parsed an import node, return true in the pair to notify the caller + return std::make_pair(x, /* is_import= */ true); } // Replace by empty node to avoid breaking the code gen @@ -108,37 +123,12 @@ namespace Ark::internal for (std::size_t i = 0; i < x.constList().size(); ++i) { auto [node, is_import] = findAndReplaceImports(x.constList()[i]); - if (!is_import) - x.list()[i] = node; - else - { - if (node.constList().size() > 1) - { - x.list()[i] = node.constList()[1]; - // NOTE maybe maybe maybe - // why do we start at 2 and not 1? - for (std::size_t j = 2, end_j = node.constList().size(); j < end_j; ++j) - { - if (i + j - 1 < x.list().size()) - x.list().insert( - x.list().begin() + static_cast::difference_type>(i + j - 1), - node.constList()[j]); - else - x.list().push_back(node.constList()[j]); - } - - // -2 because we skipped the Begin node and the first node of the block isn't inserted - // but replaces an existing one - i += node.constList().size() - 2; - } - else - x.list()[i] = node; - } + x.list()[i] = node; } } } - return std::make_pair(x, false); + return std::make_pair(x, /* is_import= */ false); } const Node& ImportSolver::ast() const noexcept @@ -166,8 +156,9 @@ namespace Ark::internal // empty symbols list module_node.push_back(Node(NodeType::List)); - m_modules[import.toPackageString()] = Module { + m_packages[import.toPackageString()] = Package { module_node, + import, true }; @@ -177,8 +168,9 @@ namespace Ark::internal Parser parser(m_debug_level); const std::string code = Utils::readFile(path.generic_string()); parser.process(path.string(), code); - m_modules[import.toPackageString()] = Module { + m_packages[import.toPackageString()] = Package { parser.ast(), + import, false }; diff --git a/src/arkreactor/Compiler/Welder.cpp b/src/arkreactor/Compiler/Welder.cpp index e84efe76d..b51ed5779 100644 --- a/src/arkreactor/Compiler/Welder.cpp +++ b/src/arkreactor/Compiler/Welder.cpp @@ -4,7 +4,7 @@ #include #include #include -#include +#include #include #include @@ -143,16 +143,16 @@ namespace Ark m_computed_ast = m_macro_processor.ast(); } - if ((m_features & FeatureASTOptimizer) != 0) + if ((m_features & FeatureNameResolver) != 0) { - m_ast_optimizer.process(m_computed_ast); - m_computed_ast = m_ast_optimizer.ast(); + m_name_resolver.process(m_computed_ast); + m_computed_ast = m_name_resolver.ast(); } - if ((m_features & FeatureNameResolver) != 0) + if ((m_features & FeatureASTOptimizer) != 0) { - // NOTE: ast isn't modified by the name resolver, no need to update m_computed_ast - m_name_resolver.process(m_computed_ast); + m_ast_optimizer.process(m_computed_ast); + m_computed_ast = m_ast_optimizer.ast(); } return true; diff --git a/src/arkreactor/Exceptions.cpp b/src/arkreactor/Exceptions.cpp index 103d5ec93..77a46c2ae 100644 --- a/src/arkreactor/Exceptions.cpp +++ b/src/arkreactor/Exceptions.cpp @@ -150,8 +150,7 @@ namespace Ark::Diagnostics std::stringstream ss; std::size_t size = 3; - // todo add "can be string" attribute - if (node.nodeType() == internal::NodeType::Symbol || node.nodeType() == internal::NodeType::String || node.nodeType() == internal::NodeType::Spread) + if (node.isStringLike()) size = node.string().size(); helper( diff --git a/src/arkreactor/VM/VM.cpp b/src/arkreactor/VM/VM.cpp index baebf5726..d3d11a48e 100644 --- a/src/arkreactor/VM/VM.cpp +++ b/src/arkreactor/VM/VM.cpp @@ -619,7 +619,22 @@ namespace Ark push(field, context); } else - throwVMError(ErrorKind::Scope, fmt::format("`{}' isn't in the closure environment: {}", m_state.m_symbols[arg], var->refClosure().toString(*this))); + { + if (!var->refClosure().hasFieldEndingWith(m_state.m_symbols[arg], *this)) + throwVMError( + ErrorKind::Scope, + fmt::format( + "`{0}' isn't in the closure environment: {1}", + m_state.m_symbols[arg], + var->refClosure().toString(*this))); + throwVMError( + ErrorKind::Scope, + fmt::format( + "`{0}' isn't in the closure environment: {1}. A variable in the package might have the same name as '{0}', " + "and name resolution tried to fully qualify it. Rename either the variable or the capture to solve this", + m_state.m_symbols[arg], + var->refClosure().toString(*this))); + } DISPATCH(); } diff --git a/src/arkreactor/VM/Value/Closure.cpp b/src/arkreactor/VM/Value/Closure.cpp index c8cc8caec..0631eac8b 100644 --- a/src/arkreactor/VM/Value/Closure.cpp +++ b/src/arkreactor/VM/Value/Closure.cpp @@ -4,6 +4,8 @@ #include #include +#include + namespace Ark::internal { Closure::Closure(const Scope& scope, const PageAddr_t pa) noexcept : @@ -16,6 +18,16 @@ namespace Ark::internal m_page_addr(pa) {} + bool Closure::hasFieldEndingWith(const std::string& end, VM& vm) const + { + for (const auto id : std::ranges::views::keys(m_scope->m_data)) + { + if (end.ends_with(":" + vm.m_state.m_symbols[id])) + return true; + } + return false; + } + std::string Closure::toString(VM& vm) const noexcept { std::string out = "("; diff --git a/src/arkscript/Formatter.cpp b/src/arkscript/Formatter.cpp index 1c17dd895..8ea3f9c0b 100644 --- a/src/arkscript/Formatter.cpp +++ b/src/arkscript/Formatter.cpp @@ -11,7 +11,7 @@ using namespace Ark; using namespace Ark::internal; -Formatter::Formatter(bool dry_run) : +Formatter::Formatter(const bool dry_run) : m_dry_run(dry_run), m_parser(/* debug= */ 0, /* interpret= */ false), m_updated(false) {} @@ -134,7 +134,7 @@ bool Formatter::shouldSplitOnNewline(const Node& node) const std::string formatted = format(node, 0, false); const std::string::size_type sz = formatted.find_first_of('\n'); - const bool is_long_line = !((sz < FormatterConfig.LongLineLength || (sz == std::string::npos && formatted.size() < FormatterConfig.LongLineLength))); + const bool is_long_line = !((sz < FormatterConfig::LongLineLength || (sz == std::string::npos && formatted.size() < FormatterConfig::LongLineLength))); if (node.comment().empty() && (isBeginBlock(node) || isFuncCall(node))) return false; if (is_long_line || (node.isListLike() && node.constList().size() > 1) || !node.comment().empty()) @@ -210,6 +210,9 @@ std::string Formatter::format(const Node& node, std::size_t indent, bool after_n case NodeType::Macro: output += formatMacro(node, indent); break; + // not handling Namespace nor Unused node types as those can not be generated by the parser + case NodeType::Namespace: + [[fallthrough]]; case NodeType::Unused: break; } diff --git a/tests/fuzzing/arkscript.dict b/tests/fuzzing/arkscript.dict index 06e986d7d..29e3bc3ac 100644 --- a/tests/fuzzing/arkscript.dict +++ b/tests/fuzzing/arkscript.dict @@ -64,9 +64,9 @@ "time" "sys:exec" "sys:sleep" -"str:format" -"str:find" -"str:removeAt" +"string:format" +"string:find" +"string:removeAt" "math:exp" "math:ln" "math:ceil" diff --git a/tests/fuzzing/corpus/examples_99bottles.ark b/tests/fuzzing/corpus/examples_99bottles.ark index d96a216af..a4482659b 100644 --- a/tests/fuzzing/corpus/examples_99bottles.ark +++ b/tests/fuzzing/corpus/examples_99bottles.ark @@ -16,6 +16,6 @@ (mut n i) (while (> n 1) { - (print (str:format "{} Bottles of beer on the wall\n{} bottles of beer\nTake one down, pass it around" n n)) + (print (string:format "{} Bottles of beer on the wall\n{} bottles of beer\nTake one down, pass it around" n n)) (set n (- n 1)) - (print (str:format "{} Bottles of beer on the wall." n))}) + (print (string:format "{} Bottles of beer on the wall." n))}) diff --git a/tests/unittests/DiagnosticsSuite.cpp b/tests/unittests/DiagnosticsSuite.cpp index 53f846f88..ceba6047c 100644 --- a/tests/unittests/DiagnosticsSuite.cpp +++ b/tests/unittests/DiagnosticsSuite.cpp @@ -20,8 +20,8 @@ ut::suite<"Diagnostics"> diagnostics_suite = [] { should("generate an error message at compile time for compileTime/" + data.stem) = [&] { try { - mut(state).doFile(data.path, features); - expect(0 == 1); // we shouldn't be here, the compilation has to fail + const bool ok = mut(state).doFile(data.path, features); + expect(!ok) << fatal; // we shouldn't be here, the compilation has to fail } catch (const Ark::CodeError& e) { diff --git a/tests/unittests/LangSuite.cpp b/tests/unittests/LangSuite.cpp index 39ab5223d..552254711 100644 --- a/tests/unittests/LangSuite.cpp +++ b/tests/unittests/LangSuite.cpp @@ -13,9 +13,15 @@ ut::suite<"Lang"> lang_suite = [] { "[run arkscript unittests]"_test = [] { Ark::State state({ std::filesystem::path(ARK_TESTS_ROOT "/lib/") }); - should("compile the resource without any error") = [&] { - expect(mut(state).doFile(get_resource_path("LangSuite/unittests.ark"))); - }; + try + { + const bool ok = mut(state).doFile(get_resource_path("LangSuite/unittests.ark")); + expect(ok) << fatal << "compilation failed"; + } + catch (const Ark::CodeError&) + { + expect(false) << fatal << "encountered an exception while compiling"; + } Ark::VM vm(state); should("return exit code 0") = [&] { diff --git a/tests/unittests/NameResolutionSuite.cpp b/tests/unittests/NameResolutionSuite.cpp new file mode 100644 index 000000000..74efd9408 --- /dev/null +++ b/tests/unittests/NameResolutionSuite.cpp @@ -0,0 +1,105 @@ +#include + +#include + +#include +#include "TestsHelper.hpp" + +using namespace boost; + +ut::suite<"NameResolution"> name_resolution_suite = [] { + using namespace ut; + + "[run a (import b, c:*, d:lamp)]"_test = [] { + Ark::State state({ std::filesystem::path(ARK_TESTS_ROOT "/lib/") }); + + should("compile the resource without any error") = [&] { + expect(mut(state).doFile(get_resource_path("NameResolutionSuite/basic/a.ark"))); + }; + + Ark::VM vm(state); + should("return exit code 0") = [&] { + expect(mut(vm).run() == 0_i); + }; + + should("resolve symbols from all namespaces without mixing them up") = [&] { + const auto b_ok = mut(vm).operator[]("b_ok"); + expect(b_ok.valueType() == Ark::ValueType::True) << "b:arg == 'b:arg'\n"; + + const auto a_ok = mut(vm).operator[]("a_ok"); + expect(a_ok.valueType() == Ark::ValueType::True) << "arg == 'a:arg'\n"; + + const auto b_foo_ok = mut(vm).operator[]("b_foo_ok"); + expect(b_foo_ok.valueType() == Ark::ValueType::True) << "(b:foo \"aa\" \"aaa\") == \"aa aaa\")\n"; + + const auto c_ok = mut(vm).operator[]("c_ok"); + expect(c_ok.valueType() == Ark::ValueType::True) << "\"c:egg\" == c:egg, \"c:bacon\" == c:bacon\n"; + + const auto d_ok = mut(vm).operator[]("d_ok"); + expect(d_ok.valueType() == Ark::ValueType::True) << "d:lamp == \"d:lamp\"\n"; + }; + }; + + "[run a (import b, call closures)]"_test = [] { + Ark::State state({ std::filesystem::path(ARK_TESTS_ROOT "/lib/") }); + + should("compile the resource without any error") = [&] { + expect(mut(state).doFile(get_resource_path("NameResolutionSuite/forward_reference/a.ark"))); + }; + + Ark::VM vm(state); + should("return exit code 0") = [&] { + expect(mut(vm).run() == 0_i); + }; + + should("resolve symbols from all namespaces without mixing them up") = [&] { + const auto start = mut(vm).operator[]("start"); + expect(start.valueType() == Ark::ValueType::True) << "(b:parent.child.get) == [1 0]\n"; + + const auto end = mut(vm).operator[]("end"); + expect(end.valueType() == Ark::ValueType::True) << "(b:parent.child.get) == [5 12]\n"; + }; + }; + + "[run a (import b, c, with make defined in both)]"_test = [] { + Ark::State state({ std::filesystem::path(ARK_TESTS_ROOT "/lib/") }); + + should("compile the resource without any error") = [&] { + expect(mut(state).doFile(get_resource_path("NameResolutionSuite/shadowing/a.ark"))); + }; + + Ark::VM vm(state); + should("return exit code 0") = [&] { + expect(mut(vm).run() == 0_i); + }; + + should("resolve symbols from all namespaces without mixing them up") = [&] { + const auto b_ok = mut(vm).operator[]("b_ok"); + expect(b_ok.valueType() == Ark::ValueType::True) << "(= b:result \"b:make\")\n"; + + const auto c_ok = mut(vm).operator[]("c_ok"); + expect(c_ok.valueType() == Ark::ValueType::True) << "(= c:result \"c:make\")\n"; + }; + }; + + "[run a (import b (import c))]"_test = [] { + Ark::State state({ std::filesystem::path(ARK_TESTS_ROOT "/lib/") }); + + should("compile the resource without any error") = [&] { + expect(mut(state).doFile(get_resource_path("NameResolutionSuite/namespace_stacking/a.ark"))); + }; + + Ark::VM vm(state); + should("return exit code 0") = [&] { + expect(mut(vm).run() == 0_i); + }; + + should("resolve symbols from all namespaces without generating bad fully qualified names") = [&] { + const auto b_ok = mut(vm).operator[]("b_ok"); + expect(b_ok.valueType() == Ark::ValueType::True) << "(= b:test \"b:test\")\n"; + + const auto c_ok = mut(vm).operator[]("c_ok"); + expect(c_ok.valueType() == Ark::ValueType::True) << "(= c:suite \"c:suite\")\n"; + }; + }; +}; diff --git a/tests/unittests/ParserSuite.cpp b/tests/unittests/ParserSuite.cpp index 1b6ef3bfc..2d4ef2bfa 100644 --- a/tests/unittests/ParserSuite.cpp +++ b/tests/unittests/ParserSuite.cpp @@ -29,16 +29,12 @@ std::string astToString(Ark::internal::Parser& parser) ss << i << ") " << data.prefix; if (data.isBasic()) ss << " (basic)"; - else if (data.isGlob()) + if (data.is_glob) ss << " (glob)"; - else - { - ss << " ( "; - for (const std::string& sym : data.symbols) - ss << sym << " "; - ss << ")"; - } - ss << "\n"; + ss << " ( "; + for (const std::string& sym : data.symbols) + ss << sym << " "; + ss << ")\n"; } return ss.str(); diff --git a/tests/unittests/resources/ASTSuite/99bottles.ark b/tests/unittests/resources/ASTSuite/99bottles.ark index 03f599f99..a4482659b 100644 --- a/tests/unittests/resources/ASTSuite/99bottles.ark +++ b/tests/unittests/resources/ASTSuite/99bottles.ark @@ -1,10 +1,10 @@ # Lyrics from the song: -# +# # 99 bottles of beer on the wall # 99 bottles of beer # Take one down, pass it around # 98 bottles of beer on the wall -# +# # 98 bottles of beer on the wall # 98 bottles of beer # Take one down, pass it around @@ -16,6 +16,6 @@ (mut n i) (while (> n 1) { - (print (str:format "{} Bottles of beer on the wall\n{} bottles of beer\nTake one down, pass it around" n n)) + (print (string:format "{} Bottles of beer on the wall\n{} bottles of beer\nTake one down, pass it around" n n)) (set n (- n 1)) - (print (str:format "{} Bottles of beer on the wall." n))}) + (print (string:format "{} Bottles of beer on the wall." n))}) diff --git a/tests/unittests/resources/ASTSuite/99bottles.json b/tests/unittests/resources/ASTSuite/99bottles.json index a5bb66dbd..f5610bf53 100644 --- a/tests/unittests/resources/ASTSuite/99bottles.json +++ b/tests/unittests/resources/ASTSuite/99bottles.json @@ -1,3 +1,3 @@ -{"type": "Begin", "children": [{"type": "Let", "name": {"type": "Symbol", "name": "arg"}, "value": {"type": "If", "condition": {"type": "FunctionCall", "name": {"type": "Symbol", "name": ">="}, "args": [{"type": "FunctionCall", "name": {"type": "Symbol", "name": "len"}, "args": [{"type": "Symbol", "name": "sys:args"}]}, {"type": "Number", "value": 1}]}, "then": {"type": "FunctionCall", "name": {"type": "Symbol", "name": "toNumber"}, "args": [{"type": "FunctionCall", "name": {"type": "Symbol", "name": "@"}, "args": [{"type": "Symbol", "name": "sys:args"}, {"type": "Number", "value": 0}]}]}, "else": {"type": "Symbol", "name": "nil"}}}, {"type": "Let", "name": {"type": "Symbol", "name": "i"}, "value": {"type": "If", "condition": {"type": "FunctionCall", "name": {"type": "Symbol", "name": "nil?"}, "args": [{"type": "Symbol", "name": "arg"}]}, "then": {"type": "Number", "value": 100}, "else": {"type": "Symbol", "name": "arg"}}}, {"type": "Mut", "name": {"type": "Symbol", "name": "n"}, "value": {"type": "Symbol", "name": "i"}}, {"type": "While", "condition": {"type": "FunctionCall", "name": {"type": "Symbol", "name": ">"}, "args": [{"type": "Symbol", "name": "n"}, {"type": "Number", "value": 1}]}, "body": {"type": "Begin", "children": [{"type": "FunctionCall", "name": {"type": "Symbol", "name": "print"}, "args": [{"type": "FunctionCall", "name": {"type": "Symbol", "name": "str:format"}, "args": [{"type": "String", "value": "{} Bottles of beer on the wall +{"type": "Begin", "children": [{"type": "Let", "name": {"type": "Symbol", "name": "arg"}, "value": {"type": "If", "condition": {"type": "FunctionCall", "name": {"type": "Symbol", "name": ">="}, "args": [{"type": "FunctionCall", "name": {"type": "Symbol", "name": "len"}, "args": [{"type": "Symbol", "name": "sys:args"}]}, {"type": "Number", "value": 1}]}, "then": {"type": "FunctionCall", "name": {"type": "Symbol", "name": "toNumber"}, "args": [{"type": "FunctionCall", "name": {"type": "Symbol", "name": "@"}, "args": [{"type": "Symbol", "name": "sys:args"}, {"type": "Number", "value": 0}]}]}, "else": {"type": "Symbol", "name": "nil"}}}, {"type": "Let", "name": {"type": "Symbol", "name": "i"}, "value": {"type": "If", "condition": {"type": "FunctionCall", "name": {"type": "Symbol", "name": "nil?"}, "args": [{"type": "Symbol", "name": "arg"}]}, "then": {"type": "Number", "value": 100}, "else": {"type": "Symbol", "name": "arg"}}}, {"type": "Mut", "name": {"type": "Symbol", "name": "n"}, "value": {"type": "Symbol", "name": "i"}}, {"type": "While", "condition": {"type": "FunctionCall", "name": {"type": "Symbol", "name": ">"}, "args": [{"type": "Symbol", "name": "n"}, {"type": "Number", "value": 1}]}, "body": {"type": "Begin", "children": [{"type": "FunctionCall", "name": {"type": "Symbol", "name": "print"}, "args": [{"type": "FunctionCall", "name": {"type": "Symbol", "name": "string:format"}, "args": [{"type": "String", "value": "{} Bottles of beer on the wall {} bottles of beer -Take one down, pass it around"}, {"type": "Symbol", "name": "n"}, {"type": "Symbol", "name": "n"}]}]}, {"type": "Set", "name": {"type": "Symbol", "name": "n"}, "value": {"type": "FunctionCall", "name": {"type": "Symbol", "name": "-"}, "args": [{"type": "Symbol", "name": "n"}, {"type": "Number", "value": 1}]}}, {"type": "FunctionCall", "name": {"type": "Symbol", "name": "print"}, "args": [{"type": "FunctionCall", "name": {"type": "Symbol", "name": "str:format"}, "args": [{"type": "String", "value": "{} Bottles of beer on the wall."}, {"type": "Symbol", "name": "n"}]}]}]}}]} \ No newline at end of file +Take one down, pass it around"}, {"type": "Symbol", "name": "n"}, {"type": "Symbol", "name": "n"}]}]}, {"type": "Set", "name": {"type": "Symbol", "name": "n"}, "value": {"type": "FunctionCall", "name": {"type": "Symbol", "name": "-"}, "args": [{"type": "Symbol", "name": "n"}, {"type": "Number", "value": 1}]}}, {"type": "FunctionCall", "name": {"type": "Symbol", "name": "print"}, "args": [{"type": "FunctionCall", "name": {"type": "Symbol", "name": "string:format"}, "args": [{"type": "String", "value": "{} Bottles of beer on the wall."}, {"type": "Symbol", "name": "n"}]}]}]}}]} diff --git a/tests/unittests/resources/CompilerSuite/ir/99bottles.ark b/tests/unittests/resources/CompilerSuite/ir/99bottles.ark index d96a216af..a4482659b 100644 --- a/tests/unittests/resources/CompilerSuite/ir/99bottles.ark +++ b/tests/unittests/resources/CompilerSuite/ir/99bottles.ark @@ -16,6 +16,6 @@ (mut n i) (while (> n 1) { - (print (str:format "{} Bottles of beer on the wall\n{} bottles of beer\nTake one down, pass it around" n n)) + (print (string:format "{} Bottles of beer on the wall\n{} bottles of beer\nTake one down, pass it around" n n)) (set n (- n 1)) - (print (str:format "{} Bottles of beer on the wall." n))}) + (print (string:format "{} Bottles of beer on the wall." n))}) diff --git a/tests/unittests/resources/CompilerSuite/optimized_ir/99bottles.ark b/tests/unittests/resources/CompilerSuite/optimized_ir/99bottles.ark index d96a216af..a4482659b 100644 --- a/tests/unittests/resources/CompilerSuite/optimized_ir/99bottles.ark +++ b/tests/unittests/resources/CompilerSuite/optimized_ir/99bottles.ark @@ -16,6 +16,6 @@ (mut n i) (while (> n 1) { - (print (str:format "{} Bottles of beer on the wall\n{} bottles of beer\nTake one down, pass it around" n n)) + (print (string:format "{} Bottles of beer on the wall\n{} bottles of beer\nTake one down, pass it around" n n)) (set n (- n 1)) - (print (str:format "{} Bottles of beer on the wall." n))}) + (print (string:format "{} Bottles of beer on the wall." n))}) diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/capture_out_of_scope.expected b/tests/unittests/resources/DiagnosticsSuite/compileTime/capture_out_of_scope.expected index 75e3e7c3c..c04aa46b0 100644 --- a/tests/unittests/resources/DiagnosticsSuite/compileTime/capture_out_of_scope.expected +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/capture_out_of_scope.expected @@ -1,2 +1,7 @@ -At &cap @ 1:0 +At &cap @ 2:17 + 1 | (let bar (fun () (let cap 0))) + 2 | (let foo (fun (&cap) ())) + | ^~~~ + 3 | (print foo bar) + 4 | Can not capture cap because it is referencing a variable defined in an unreachable scope. diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/package/b.ark b/tests/unittests/resources/DiagnosticsSuite/compileTime/package/b.ark new file mode 100644 index 000000000..203daf203 --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/package/b.ark @@ -0,0 +1 @@ +(let bar 5) diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/unbound.expected b/tests/unittests/resources/DiagnosticsSuite/compileTime/unbound.expected deleted file mode 100644 index b142540b1..000000000 --- a/tests/unittests/resources/DiagnosticsSuite/compileTime/unbound.expected +++ /dev/null @@ -1,2 +0,0 @@ -At &d @ 1:0 - Can not capture d because it is referencing an unbound variable. diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/unbound_but_namespace.ark b/tests/unittests/resources/DiagnosticsSuite/compileTime/unbound_but_namespace.ark new file mode 100644 index 000000000..a36bed090 --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/unbound_but_namespace.ark @@ -0,0 +1,3 @@ +(import package.b) + +(print bar) diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/unbound_but_namespace.expected b/tests/unittests/resources/DiagnosticsSuite/compileTime/unbound_but_namespace.expected new file mode 100644 index 000000000..e088b148e --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/unbound_but_namespace.expected @@ -0,0 +1,7 @@ +At bar @ 3:12 + 1 | (import package.b) + 2 | + 3 | (print bar) + | ^~~~~~~~~~~ + 4 | + Unbound variable "bar". However, it exists in a namespace as "b:bar", did you forget to prefix it with its namespace? diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/unbound.ark b/tests/unittests/resources/DiagnosticsSuite/compileTime/unbound_capture.ark similarity index 100% rename from tests/unittests/resources/DiagnosticsSuite/compileTime/unbound.ark rename to tests/unittests/resources/DiagnosticsSuite/compileTime/unbound_capture.ark diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/unbound_capture.expected b/tests/unittests/resources/DiagnosticsSuite/compileTime/unbound_capture.expected new file mode 100644 index 000000000..cc8727029 --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/unbound_capture.expected @@ -0,0 +1,5 @@ +At &d @ 1:14 + 1 | (mut d (fun (&d) (print d))) + | ^~ + 2 | + Can not capture d because it is referencing a variable defined in an unreachable scope. diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/unbound_var.ark b/tests/unittests/resources/DiagnosticsSuite/compileTime/unbound_var.ark new file mode 100644 index 000000000..a8fb9bbf7 --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/unbound_var.ark @@ -0,0 +1 @@ +(let a b) diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/unbound_var.expected b/tests/unittests/resources/DiagnosticsSuite/compileTime/unbound_var.expected new file mode 100644 index 000000000..2e7fa250a --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/unbound_var.expected @@ -0,0 +1,5 @@ +At b @ 1:9 + 1 | (let a b) + | ^~~~~~~~~ + 2 | + Unbound variable error "b" (variable is used but not defined) diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/unbound_var_suggestion.ark b/tests/unittests/resources/DiagnosticsSuite/compileTime/unbound_var_suggestion.ark new file mode 100644 index 000000000..ca966b8d5 --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/unbound_var_suggestion.ark @@ -0,0 +1,2 @@ +(let bar 12) +(let a ber) diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/unbound_var_suggestion.expected b/tests/unittests/resources/DiagnosticsSuite/compileTime/unbound_var_suggestion.expected new file mode 100644 index 000000000..2db8cf96e --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/unbound_var_suggestion.expected @@ -0,0 +1,6 @@ +At ber @ 2:12 + 1 | (let bar 12) + 2 | (let a ber) + | ^~~~~~~~~~~ + 3 | + Unbound variable error "ber" (did you mean "bar"?) diff --git a/tests/unittests/resources/DiagnosticsSuite/runtime/closure_field_wrong_fqn.ark b/tests/unittests/resources/DiagnosticsSuite/runtime/closure_field_wrong_fqn.ark new file mode 100644 index 000000000..2de7cb2bb --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/runtime/closure_field_wrong_fqn.ark @@ -0,0 +1 @@ +(import closure_field_wrong_fqn.b) diff --git a/tests/unittests/resources/DiagnosticsSuite/runtime/closure_field_wrong_fqn.expected b/tests/unittests/resources/DiagnosticsSuite/runtime/closure_field_wrong_fqn.expected new file mode 100644 index 000000000..c4922e9fc --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/runtime/closure_field_wrong_fqn.expected @@ -0,0 +1 @@ +ScopeError: `b:b' isn't in the closure environment: (.a=hello .b=1). A variable in the package might have the same name as 'b:b', and name resolution tried to fully qualify it. Rename either the variable or the capture to solve this diff --git a/tests/unittests/resources/DiagnosticsSuite/runtime/closure_field_wrong_fqn/b.ark b/tests/unittests/resources/DiagnosticsSuite/runtime/closure_field_wrong_fqn/b.ark new file mode 100644 index 000000000..a0a1d40de --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/runtime/closure_field_wrong_fqn/b.ark @@ -0,0 +1,10 @@ +(let tests 0) +(let closure (fun (&tests) ())) + +(let a 1) +(let b 2) + +(let make (fun (a b) + (fun (&a &b) ()))) +(let foo (make "hello" 1)) +(let c [foo.a foo.b]) diff --git a/tests/unittests/resources/DiagnosticsSuite/runtime/fmt_arg_not_found.ark b/tests/unittests/resources/DiagnosticsSuite/runtime/fmt_arg_not_found.ark index 6beb8dbf6..53938ca46 100644 --- a/tests/unittests/resources/DiagnosticsSuite/runtime/fmt_arg_not_found.ark +++ b/tests/unittests/resources/DiagnosticsSuite/runtime/fmt_arg_not_found.ark @@ -1 +1 @@ -(str:format "Hello {}, I'm {}" "World") +(string:format "Hello {}, I'm {}" "World") diff --git a/tests/unittests/resources/DiagnosticsSuite/runtime/fmt_arg_not_found.expected b/tests/unittests/resources/DiagnosticsSuite/runtime/fmt_arg_not_found.expected index e736837c1..efe52bd0a 100644 --- a/tests/unittests/resources/DiagnosticsSuite/runtime/fmt_arg_not_found.expected +++ b/tests/unittests/resources/DiagnosticsSuite/runtime/fmt_arg_not_found.expected @@ -1 +1 @@ -str:format: can not format "Hello {}, I'm {}" (1 argument provided) because of argument not found +string:format: can not format "Hello {}, I'm {}" (1 argument provided) because of argument not found diff --git a/tests/unittests/resources/DiagnosticsSuite/runtime/str_remove_out_of_bound.ark b/tests/unittests/resources/DiagnosticsSuite/runtime/str_remove_out_of_bound.ark index b0f2b476a..b507a1ddb 100644 --- a/tests/unittests/resources/DiagnosticsSuite/runtime/str_remove_out_of_bound.ark +++ b/tests/unittests/resources/DiagnosticsSuite/runtime/str_remove_out_of_bound.ark @@ -1 +1 @@ -(str:removeAt "abc" 5) +(string:removeAt "abc" 5) diff --git a/tests/unittests/resources/DiagnosticsSuite/runtime/str_remove_out_of_bound.expected b/tests/unittests/resources/DiagnosticsSuite/runtime/str_remove_out_of_bound.expected index 46d2d705c..f8bfa6dde 100644 --- a/tests/unittests/resources/DiagnosticsSuite/runtime/str_remove_out_of_bound.expected +++ b/tests/unittests/resources/DiagnosticsSuite/runtime/str_remove_out_of_bound.expected @@ -1 +1 @@ -str:removeAt: index 5 out of range (length: 3) +string:removeAt: index 5 out of range (length: 3) diff --git a/tests/unittests/resources/LangSuite/builtins-tests.ark b/tests/unittests/resources/LangSuite/builtins-tests.ark index dc55d3cd7..e1554c8c9 100644 --- a/tests/unittests/resources/LangSuite/builtins-tests.ark +++ b/tests/unittests/resources/LangSuite/builtins-tests.ark @@ -47,17 +47,14 @@ (sys:sleep 1) (test:expect (< old (time))) - # no need to test str:format, we are already using it for the assertions, - # and it's also heavily tested in the C++ String repository in the ArkScript-lang organization (github) - - (test:eq (str:find "abc" "d") -1) - (test:eq (str:find "abc" "a") 0) - (test:eq (str:find "abc" "bc") 1) - (test:eq (str:find "abcdefghijkl" "defijkl") -1) - (test:eq (str:find "abcdefghijkl" "defghijkl") 3) - (test:eq (str:removeAt "abcdefghijkl" 3) "abcefghijkl") - (test:eq (str:removeAt "abcdefghijkl" 0) "bcdefghijkl") - (test:eq (str:removeAt "abcdefghijkl" 11) "abcdefghijk") + (test:eq (string:find "abc" "d") -1) + (test:eq (string:find "abc" "a") 0) + (test:eq (string:find "abc" "bc") 1) + (test:eq (string:find "abcdefghijkl" "defijkl") -1) + (test:eq (string:find "abcdefghijkl" "defghijkl") 3) + (test:eq (string:removeAt "abcdefghijkl" 3) "abcefghijkl") + (test:eq (string:removeAt "abcdefghijkl" 0) "bcdefghijkl") + (test:eq (string:removeAt "abcdefghijkl" 11) "abcdefghijk") # no need to test the math functions since they're 1:1 binding of C++ functions and were carefully checked # before writing this comment, to ensure we aren't binding math:sin to the C++ tan function diff --git a/tests/unittests/resources/LangSuite/list-tests.ark b/tests/unittests/resources/LangSuite/list-tests.ark index 0da2ea6ff..e22ff7b65 100644 --- a/tests/unittests/resources/LangSuite/list-tests.ark +++ b/tests/unittests/resources/LangSuite/list-tests.ark @@ -4,14 +4,14 @@ (let b [4 5 6]) (test:suite list { - (let make (fun (a b) - (fun (&a &b) ()))) + (let make (fun (c d) + (fun (&c &d) ()))) (let foo (make "hello" 1)) # if this is failing, this is most likely to be a compiler problem - (test:eq ["hello" 1] [foo.a foo.b]) - (test:eq 2 (len [foo.a foo.b])) - (test:eq ["hello"] (append [] foo.a)) + (test:eq ["hello" 1] [foo.c foo.d]) + (test:eq 2 (len [foo.c foo.d])) + (test:eq ["hello"] (append [] foo.c)) (test:case "append and return a new list" { (test:eq (append a 4) [1 2 3 4]) diff --git a/tests/unittests/resources/LangSuite/string-tests.ark b/tests/unittests/resources/LangSuite/string-tests.ark index 6192a7f1b..4322afa25 100644 --- a/tests/unittests/resources/LangSuite/string-tests.ark +++ b/tests/unittests/resources/LangSuite/string-tests.ark @@ -1,19 +1,18 @@ (import std.Testing) -(import std.String) (test:suite string { (test:case "remove char in string at index" { - (test:eq "hllo world" (str:removeAt "hello world" 1)) - (test:eq "ello world" (str:removeAt "hello world" 0)) - (test:eq "hello worl" (str:removeAt "hello world" 10)) }) + (test:eq "hllo world" (string:removeAt "hello world" 1)) + (test:eq "ello world" (string:removeAt "hello world" 0)) + (test:eq "hello worl" (string:removeAt "hello world" 10)) }) (test:case "find substring" { - (test:eq -1 (str:find "hello" "help")) - (test:eq 0 (str:find "hello" "hel")) - (test:eq 2 (str:find "hello" "llo")) - (test:eq -1 (str:find "" "1")) }) + (test:eq -1 (string:find "hello" "help")) + (test:eq 0 (string:find "hello" "hel")) + (test:eq 2 (string:find "hello" "llo")) + (test:eq -1 (string:find "" "1")) }) (test:case "format strings" { - (test:eq "nilfalsetrue" (str:format "{}{}{}" nil false true)) - (test:eq "CProcedure" (str:format "{}" print)) + (test:eq "nilfalsetrue" (string:format "{}{}{}" nil false true)) + (test:eq "CProcedure" (string:format "{}" print)) })}) diff --git a/tests/unittests/resources/LangSuite/utf8-tests.ark b/tests/unittests/resources/LangSuite/utf8-tests.ark index 5294d6adf..75e357724 100644 --- a/tests/unittests/resources/LangSuite/utf8-tests.ark +++ b/tests/unittests/resources/LangSuite/utf8-tests.ark @@ -26,7 +26,7 @@ (test:eq "\u1E0B" "ḋ") }) (test:case "testing emoji codepoints computing" { - (test:eq (str:ord "👺") 128122) - (test:eq (str:chr 128122) "👺") - (test:eq (str:ord "$") 36) - (test:eq (str:chr 36) "$") })}) + (test:eq (string:ord "👺") 128122) + (test:eq (string:chr 128122) "👺") + (test:eq (string:ord "$") 36) + (test:eq (string:chr 36) "$") })}) diff --git a/tests/unittests/resources/LangSuite/vm-tests.ark b/tests/unittests/resources/LangSuite/vm-tests.ark index 2f6a689f8..12af3af6c 100644 --- a/tests/unittests/resources/LangSuite/vm-tests.ark +++ b/tests/unittests/resources/LangSuite/vm-tests.ark @@ -126,11 +126,11 @@ (test:eq (type nil) "Nil") (test:eq (type true) "Bool") (test:eq (type false) "Bool") - (test:expect (hasField closure "tests")) + (test:expect (hasField closure "vm-tests:tests")) (test:expect (not (hasField closure "12"))) }) (test:case "closures" { - (test:eq (toString closure) "(.tests=0)") + (test:eq (toString closure) "(.vm-tests:tests=0)") (test:eq closure_1 closure_1_bis) (test:eq closure_1 closure_2) (test:neq closure_1 closure_4) diff --git a/tests/unittests/resources/NameResolutionSuite/basic/a.ark b/tests/unittests/resources/NameResolutionSuite/basic/a.ark new file mode 100644 index 000000000..cf2784cad --- /dev/null +++ b/tests/unittests/resources/NameResolutionSuite/basic/a.ark @@ -0,0 +1,14 @@ +(import b) +(import c:*) +(import d :lamp) + +(let b_ok (= "b:arg" b:arg)) + +(let arg "a:arg") +(let a_ok (= "a:arg" arg)) + +(let b_foo_ok (= "aa aaa" (b:foo "aa" "aaa"))) + +(let c_ok (and (= "c:egg" c:egg) (= "c:bacon" c:bacon))) + +(let d_ok (= "d:lamp" d:lamp)) diff --git a/tests/unittests/resources/NameResolutionSuite/basic/b.ark b/tests/unittests/resources/NameResolutionSuite/basic/b.ark new file mode 100644 index 000000000..37154aef8 --- /dev/null +++ b/tests/unittests/resources/NameResolutionSuite/basic/b.ark @@ -0,0 +1,7 @@ +(let foo (fun (arg arg2) + (bar arg arg2))) + +(let arg "b:arg") + +(let bar (fun (one two) + (+ one " " two))) diff --git a/tests/unittests/resources/NameResolutionSuite/basic/c.ark b/tests/unittests/resources/NameResolutionSuite/basic/c.ark new file mode 100644 index 000000000..e91bf206c --- /dev/null +++ b/tests/unittests/resources/NameResolutionSuite/basic/c.ark @@ -0,0 +1,2 @@ +(let egg "c:egg") +(let bacon "c:bacon") diff --git a/tests/unittests/resources/NameResolutionSuite/basic/d.ark b/tests/unittests/resources/NameResolutionSuite/basic/d.ark new file mode 100644 index 000000000..f71bcab13 --- /dev/null +++ b/tests/unittests/resources/NameResolutionSuite/basic/d.ark @@ -0,0 +1,2 @@ +(let lamp "d:lamp") +(let nope "d:nope") diff --git a/tests/unittests/resources/NameResolutionSuite/forward_reference/a.ark b/tests/unittests/resources/NameResolutionSuite/forward_reference/a.ark new file mode 100644 index 000000000..445c6b8d6 --- /dev/null +++ b/tests/unittests/resources/NameResolutionSuite/forward_reference/a.ark @@ -0,0 +1,5 @@ +(import b) + +(let start (= [1 0] (b:parent.child.get))) +(b:parent.child.call) +(let end (= [5 12] (b:parent.child.get))) diff --git a/tests/unittests/resources/NameResolutionSuite/forward_reference/b.ark b/tests/unittests/resources/NameResolutionSuite/forward_reference/b.ark new file mode 100644 index 000000000..8e2e0a08a --- /dev/null +++ b/tests/unittests/resources/NameResolutionSuite/forward_reference/b.ark @@ -0,0 +1,8 @@ +(mut inner 0) +(let call (fun () { + (set val 5) + (set inner 12) })) +(let get (fun () [val inner])) +(let child (fun (&inner &call &get) ())) +(mut val 1) +(let parent (fun (&val &child) ())) diff --git a/tests/unittests/resources/NameResolutionSuite/namespace_stacking/a.ark b/tests/unittests/resources/NameResolutionSuite/namespace_stacking/a.ark new file mode 100644 index 000000000..cc048c24d --- /dev/null +++ b/tests/unittests/resources/NameResolutionSuite/namespace_stacking/a.ark @@ -0,0 +1,4 @@ +(import b) + +(let b_ok (= b:test "b:test")) +(let c_ok (= c:suite "c:suite")) diff --git a/tests/unittests/resources/NameResolutionSuite/namespace_stacking/b.ark b/tests/unittests/resources/NameResolutionSuite/namespace_stacking/b.ark new file mode 100644 index 000000000..dee14732a --- /dev/null +++ b/tests/unittests/resources/NameResolutionSuite/namespace_stacking/b.ark @@ -0,0 +1,3 @@ +(import c) + +(let test (c:make_suite "b:test")) diff --git a/tests/unittests/resources/NameResolutionSuite/namespace_stacking/c.ark b/tests/unittests/resources/NameResolutionSuite/namespace_stacking/c.ark new file mode 100644 index 000000000..175770420 --- /dev/null +++ b/tests/unittests/resources/NameResolutionSuite/namespace_stacking/c.ark @@ -0,0 +1,2 @@ +(let make_suite (fun (name) name)) +(let suite (make_suite "c:suite")) diff --git a/tests/unittests/resources/NameResolutionSuite/shadowing/a.ark b/tests/unittests/resources/NameResolutionSuite/shadowing/a.ark new file mode 100644 index 000000000..5605abbcd --- /dev/null +++ b/tests/unittests/resources/NameResolutionSuite/shadowing/a.ark @@ -0,0 +1,5 @@ +(import b) +(import c) + +(let b_ok (= b:result "b:make")) +(let c_ok (= c:result "c:make")) diff --git a/tests/unittests/resources/NameResolutionSuite/shadowing/b.ark b/tests/unittests/resources/NameResolutionSuite/shadowing/b.ark new file mode 100644 index 000000000..044b3dd33 --- /dev/null +++ b/tests/unittests/resources/NameResolutionSuite/shadowing/b.ark @@ -0,0 +1,3 @@ +(let make (fun (a b c) + "b:make")) +(let result (make 1 2 3)) diff --git a/tests/unittests/resources/NameResolutionSuite/shadowing/c.ark b/tests/unittests/resources/NameResolutionSuite/shadowing/c.ark new file mode 100644 index 000000000..d45198bbd --- /dev/null +++ b/tests/unittests/resources/NameResolutionSuite/shadowing/c.ark @@ -0,0 +1,7 @@ +(mut result nil) + +((fun () { + (let make (fun (a b) + "c:make")) + (set result (make "hello" 1)) + })) diff --git a/tests/unittests/resources/ParserSuite/success/import.expected b/tests/unittests/resources/ParserSuite/success/import.expected index c304198bc..ec23b9e56 100644 --- a/tests/unittests/resources/ParserSuite/success/import.expected +++ b/tests/unittests/resources/ParserSuite/success/import.expected @@ -7,11 +7,11 @@ ( Keyword:Import ( Symbol:foo ) ( Symbol:a ) ) ( Keyword:Import ( Symbol:foo Symbol:bar ) ( Symbol:a Symbol:b ) ) -0) a (basic) -1) b (basic) -2) egg (basic) -3) foo (glob) -4) bar (glob) -5) egg (glob) +0) a (basic) ( ) +1) b (basic) ( ) +2) egg (basic) ( ) +3) foo (glob) ( ) +4) bar (glob) ( ) +5) egg (glob) ( ) 6) foo ( a ) 7) bar ( a b )