ArkScript
A small, lisp-inspired, functional scripting language
ImportSolver.cpp
Go to the documentation of this file.
2
3#include <ranges>
4#include <algorithm>
5#include <fmt/core.h>
6
7#include <Ark/Utils/Files.hpp>
10
11namespace Ark::internal
12{
13 ImportSolver::ImportSolver(const unsigned debug, const std::vector<std::filesystem::path>& libenv) :
14 Pass("ImportSolver", debug), m_debug_level(debug), m_libenv(libenv), m_ast()
15 {}
16
17 ImportSolver& ImportSolver::setup(const std::filesystem::path& root, const std::vector<Import>& origin_imports)
18 {
19 // keep the given root if it's a directory, it means it comes from a code string evaluation in the state, where we don't have a filename
20 m_root = is_directory(root) ? root : root.parent_path();
21
22 for (const auto& origin_import : std::ranges::reverse_view(origin_imports))
23 m_imports.push({ root, origin_import });
24
25 return *this;
26 }
27
28 void ImportSolver::process(const Node& origin_ast)
29 {
30 m_logger.traceStart("process");
31
32 while (!m_imports.empty())
33 {
34 ImportWithSource source = m_imports.top();
35 m_logger.debug("Importing {}", source.import.toPackageString());
36
37 // Remove the top element to process the other imports
38 // It needs to be removed first because we might be adding
39 // other imports later and don't want to pop THEM
40 m_imports.pop();
41 const auto package = source.import.toPackageString();
42
43 if (m_packages.contains(package))
44 {
45 // merge the definition, so that we can generate valid Full Qualified Names in the name & scope resolver
46 m_packages[package].import.with_prefix |= source.import.with_prefix;
47 m_packages[package].import.is_glob |= source.import.is_glob;
48 for (auto&& symbol : source.import.symbols)
49 m_packages[package].import.symbols.push_back(symbol);
50 }
51 else
52 {
53 // NOTE: since the "file" (=root) argument doesn't change between all calls, we could get rid of it
54 std::vector<ImportWithSource> temp = parseImport(source.file, source.import);
55 for (auto& additional_import : std::ranges::reverse_view(temp))
56 m_imports.push(additional_import);
57 }
58 }
59
60 m_logger.traceStart("findAndReplaceImports");
61 m_ast = findAndReplaceImports(origin_ast).first;
63
65 }
66
67 std::pair<Node, bool> ImportSolver::findAndReplaceImports(const Node& ast)
68 {
69 Node x = ast;
70 if (x.nodeType() == NodeType::List)
71 {
72 if (x.constList().size() >= 2 && x.constList()[0].nodeType() == NodeType::Keyword &&
73 x.constList()[0].keyword() == Keyword::Import)
74 {
75 // compute the package string: foo.bar.egg
76 const auto import_node = x.constList()[1].constList();
77 const std::string package = std::accumulate(
78 std::next(import_node.begin()),
79 import_node.end(),
80 import_node[0].string(),
81 [](const std::string& acc, const Node& elem) -> std::string {
82 return acc + "." + elem.string();
83 });
84
85 // if it wasn't imported already, register it
86 if (std::ranges::find(m_imported, package) == m_imported.end())
87 {
88 m_imported.push_back(package);
89 // modules are already handled, we can safely replace the node
90 x = m_packages[package].ast;
91 if (!m_packages[package].has_been_processed)
92 {
93 const auto import = m_packages[package].import;
94
95 // prefix to lowercase ; usually considered unsafe (https://devblogs.microsoft.com/oldnewthing/20241007-00/?p=110345)
96 // but we are dealing with prefix from filenames, thus we can somewhat assume we are in safe zone
97 std::string prefix = import.prefix;
98 std::ranges::transform(
99 prefix, prefix.begin(),
100 [](auto c) {
101 return std::tolower(c);
102 });
103
104 x = Node(Namespace {
105 .name = prefix,
106 .is_glob = import.is_glob,
107 .with_prefix = import.with_prefix,
108 .symbols = import.symbols,
109 .ast = std::make_shared<Node>(findAndReplaceImports(x).first) });
110
111 x.arkNamespace().ast->setPositionFrom(ast);
112 }
113 // we parsed an import node, return true in the pair to notify the caller
114 return std::make_pair(x, /* is_import= */ true);
115 }
116
117 // Replace by empty node to avoid breaking the code gen
118 x = Node(NodeType::List);
120 }
121 else
122 {
123 for (std::size_t i = 0; i < x.constList().size(); ++i)
124 {
125 auto [node, is_import] = findAndReplaceImports(x.constList()[i]);
126 x.list()[i] = node;
127 }
128 }
129 }
130
131 return std::make_pair(x, /* is_import= */ false);
132 }
133
134 const Node& ImportSolver::ast() const noexcept
135 {
136 return m_ast;
137 }
138
139 std::vector<ImportSolver::ImportWithSource> ImportSolver::parseImport(const std::filesystem::path& source, const Import& import)
140 {
141 m_logger.traceStart(fmt::format("parseImport {}", source.string()));
142
143 const auto path = findFile(source, import);
144 if (path.extension() == ".arkm") // Nothing to import in case of modules
145 {
146 // Creating an import node that will stay there when visiting the AST and
147 // replacing the imports with their parsed module
148 auto module_node = Node(NodeType::List);
149 module_node.push_back(Node(Keyword::Import));
150
151 auto package_node = Node(NodeType::List);
152 std::ranges::transform(
153 import.package,
154 std::back_inserter(package_node.list()), [](const std::string& stem) {
155 return Node(NodeType::String, stem);
156 });
157 module_node.push_back(package_node);
158 // empty symbols list
159 module_node.push_back(Node(NodeType::List));
160
161 m_packages[import.toPackageString()] = Package {
162 module_node,
163 import,
164 true
165 };
166
167 return {};
168 }
169
170 Parser parser(m_debug_level);
171 const std::string code = Utils::readFile(path.generic_string());
172 parser.process(path.string(), code);
173 m_packages[import.toPackageString()] = Package {
174 parser.ast(),
175 import,
176 false
177 };
178
180
181 auto imports = parser.imports();
182 std::vector<ImportWithSource> output;
183 std::ranges::transform(
184 imports,
185 std::back_inserter(output), [&path](const Import& i) {
186 return ImportWithSource { path, i };
187 });
188 return output;
189 }
190
191 std::optional<std::filesystem::path> testExtensions(const std::filesystem::path& folder, const std::string& package_path)
192 {
193 if (auto code_path = folder / (package_path + ".ark"); std::filesystem::exists(code_path))
194 return code_path;
195 if (auto module_path = folder / (package_path + ".arkm"); std::filesystem::exists(module_path))
196 return module_path;
197 return {};
198 }
199
200 std::filesystem::path ImportSolver::findFile(const std::filesystem::path& file, const Import& import_) const
201 {
202 const std::string package_path = import_.packageToPath();
203 if (auto maybe_path = testExtensions(m_root, package_path); maybe_path.has_value())
204 return maybe_path.value();
205
206 // search in all folders in environment path
207 for (const auto& path : m_libenv)
208 {
209 if (auto maybe_path = testExtensions(path, package_path); maybe_path.has_value())
210 return maybe_path.value();
211 }
212
213 // fallback, we couldn't find the file
214 throw CodeError(
215 fmt::format("While processing file {}, couldn't import {}: file not found",
216 file.filename().string(), import_.toPackageString()),
218 file.generic_string(),
219 FileSpan { .start = FilePos { .line = import_.line, .column = import_.col }, .end = std::nullopt }));
220 }
221}
ArkScript homemade exceptions.
Lots of utilities about the filesystem.
Handle imports, resolve them with modules and everything.
Parse ArkScript code, but do not handle any import declarations.
std::stack< ImportWithSource > m_imports
std::filesystem::path m_root
Folder were the entry file is.
std::unordered_map< std::string, Package > m_packages
Package name to package AST & data mapping.
void process(const Node &origin_ast) override
Start processing the given AST.
std::filesystem::path findFile(const std::filesystem::path &file, const Import &import_) const
Search for an import file, using the root file path.
std::vector< std::filesystem::path > m_libenv
std::pair< Node, bool > findAndReplaceImports(const Node &ast)
Visits the AST, looking for import nodes to replace with their parsed module version.
ImportSolver & setup(const std::filesystem::path &root, const std::vector< Import > &origin_imports)
Configure the ImportSolver.
ImportSolver(unsigned debug, const std::vector< std::filesystem::path > &libenv)
Create a new ImportSolver.
std::vector< ImportWithSource > parseImport(const std::filesystem::path &source, const Import &import)
Parse a given file and returns a list of its imports. The AST is parsed and stored in m_modules[impor...
std::vector< std::string > m_imported
List of imports, in the order they were found and parsed.
const Node & ast() const noexcept override
Output of the compiler pass.
void debug(const Logger::MessageAndLocation &data, Args &&... args)
Write a debug level log using fmtlib.
Definition Logger.hpp:77
void traceStart(std::string &&trace_name)
Definition Logger.hpp:90
A node of an Abstract Syntax Tree for ArkScript.
Definition Node.hpp:32
NodeType nodeType() const noexcept
Return the node type.
Definition Node.cpp:78
const std::vector< Node > & constList() const noexcept
Return the list of sub-nodes held by the node.
Definition Node.cpp:73
Namespace & arkNamespace() noexcept
Return the namespace held by the value (if the node type allows it)
Definition Node.cpp:53
void push_back(const Node &node) noexcept
Every node has a list as well as a value so we can push_back on all node no matter their type.
Definition Node.cpp:63
std::vector< Node > & list() noexcept
Return the list of sub-nodes held by the node.
Definition Node.cpp:68
void process(const std::string &filename, const std::string &code)
Parse the given code.
Definition Parser.cpp:51
const Node & ast() const noexcept
Definition Parser.cpp:92
const std::vector< Import > & imports() const
Definition Parser.cpp:97
An interface to describe compiler passes.
Definition Pass.hpp:23
std::string readFile(const std::string &name)
Helper to read a file.
Definition Files.hpp:47
std::optional< std::filesystem::path > testExtensions(const std::filesystem::path &folder, const std::string &package_path)
CodeError thrown by the compiler (parser, macro processor, optimizer, and compiler itself)
Describes a span for a node/atom in a file, its start position and end position.
Definition Position.hpp:35
std::optional< FilePos > end
Definition Position.hpp:37
std::string packageToPath() const
Definition Import.hpp:69
std::string toPackageString() const
Definition Import.hpp:54
std::shared_ptr< Node > ast
Definition Namespace.hpp:18