aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEugen Wissner <belka@caraus.de>2026-08-25 23:41:18 +0200
committerEugen Wissner <belka@caraus.de>2026-08-25 23:41:18 +0200
commit81370291c039593a9886c7b3f776ca55a2858f0d (patch)
tree47e12c5263e7784d3f0fc75fbbf6d791291e2230
parentdf51f9c4ee0ee4ac22206513bcd60e6928b7eaeb (diff)
downloadelna-81370291c039593a9886c7b3f776ca55a2858f0d.tar.gz
Reject circular imports
-rw-r--r--boot/dependency.cc65
-rw-r--r--boot/symbol.cc4
-rw-r--r--gcc/Make-lang.in1
-rw-r--r--gcc/gcc/elna-module-loader.cc102
-rw-r--r--gcc/gcc/elna1.cc102
-rw-r--r--include/elna/boot/dependency.h204
-rw-r--r--include/elna/boot/symbol.h13
-rw-r--r--include/elna/gcc/elna-module-loader.h92
-rw-r--r--testsuite/fail_compilation/circular_import/sut.elna3
9 files changed, 429 insertions, 157 deletions
diff --git a/boot/dependency.cc b/boot/dependency.cc
index 4590513..7f87e40 100644
--- a/boot/dependency.cc
+++ b/boot/dependency.cc
@@ -26,6 +26,17 @@ along with GCC; see the file COPYING3. If not see
namespace elna::boot
{
+ circular_import_error::circular_import_error(const source_position position,
+ const std::string& module_name)
+ : diagnostic(position), module_name(module_name)
+ {
+ }
+
+ std::string circular_import_error::what() const
+ {
+ return "Circular import of module '" + this->module_name + "'";
+ }
+
dependency read_source(std::istream& entry_point, const target_info& target)
{
driver parse_driver;
@@ -35,58 +46,66 @@ namespace elna::boot
dependency outcome;
if (parser() != 0)
{
- std::swap(outcome.errors(), parse_driver.errors());
+ std::swap(outcome.errors, parse_driver.errors());
return outcome;
}
else
{
- std::swap(outcome.tree, parse_driver.tree);
+ std::swap(outcome.value, parse_driver.tree);
}
materialization_visitor materialization_visitor(target);
- outcome.tree->accept(&materialization_visitor);
+ outcome.value->accept(&materialization_visitor);
if (materialization_visitor.has_errors())
{
- std::swap(outcome.errors(), materialization_visitor.errors());
- return outcome;
+ std::swap(outcome.errors, materialization_visitor.errors());
+ outcome.value.reset();
}
- declaration_visitor declaration_visitor{};
- outcome.tree->accept(&declaration_visitor);
-
- if (declaration_visitor.has_errors())
- {
- std::swap(outcome.errors(), declaration_visitor.errors());
- }
- outcome.unresolved = declaration_visitor.unresolved;
-
return outcome;
}
- diagnostic_list analyze_semantics(std::unique_ptr<unit>& tree, symbol_bag& bag,
- const target_info& target)
+ analysis_result analyze_semantics(std::unique_ptr<unit>& tree,
+ const std::vector<std::shared_ptr<symbol_table>>& imports,
+ const std::shared_ptr<symbol_table>& globals, const target_info& target)
{
- name_analysis_visitor name_analyser(bag, target);
+ declaration_visitor declarations{};
+ tree->accept(&declarations);
+ analysis_result result{ .value = { std::move(declarations.unresolved), globals }, .errors = {} };
+
+ if (declarations.has_errors())
+ {
+ std::swap(result.errors, declarations.errors());
+ return result;
+ }
+ for (const auto& import : imports)
+ {
+ result.value.add_import(import);
+ }
+ name_analysis_visitor name_analyser(result.value, target);
tree->accept(&name_analyser);
if (name_analyser.has_errors())
{
- return std::move(name_analyser.errors());
+ std::swap(result.errors, name_analyser.errors());
+ return result;
}
- type_analysis_visitor type_analyzer(bag, target);
+ type_analysis_visitor type_analyzer(result.value, target);
tree->accept(&type_analyzer);
if (type_analyzer.has_errors())
{
- return std::move(type_analyzer.errors());
+ std::swap(result.errors, type_analyzer.errors());
+ return result;
}
- validation_visitor validator(bag, target);
+ validation_visitor validator(result.value, target);
tree->accept(&validator);
if (validator.has_errors())
{
- return std::move(validator.errors());
+ std::swap(result.errors, validator.errors());
+ return result;
}
- return diagnostic_list{};
+ return result;
}
std::filesystem::path build_path(const std::vector<std::string>& segments)
diff --git a/boot/symbol.cc b/boot/symbol.cc
index b32782e..fcafae4 100644
--- a/boot/symbol.cc
+++ b/boot/symbol.cc
@@ -409,9 +409,9 @@ namespace elna::boot
return unresolved_declaration;
}
- void symbol_bag::add_import(const symbol_bag& bag)
+ void symbol_bag::add_import(const std::shared_ptr<symbol_table>& symbols)
{
- this->imports.push_front(bag.exported_symbols());
+ this->imports.push_front(symbols);
}
bool symbol_bag::is_global() const
diff --git a/gcc/Make-lang.in b/gcc/Make-lang.in
index bf1893d..b9d8b5e 100644
--- a/gcc/Make-lang.in
+++ b/gcc/Make-lang.in
@@ -47,6 +47,7 @@ elna_OBJS = \
elna/elna-diagnostic.o \
elna/elna-tree.o \
elna/elna-builtins.o \
+ elna/elna-module-loader.o \
elna/ast.o \
elna/evaluator.o \
elna/dependency.o \
diff --git a/gcc/gcc/elna-module-loader.cc b/gcc/gcc/elna-module-loader.cc
new file mode 100644
index 0000000..1dd7596
--- /dev/null
+++ b/gcc/gcc/elna-module-loader.cc
@@ -0,0 +1,102 @@
+/* Module loading glue between the boot pipeline and the GCC driver.
+ Copyright (C) 2025 Free Software Foundation, Inc.
+
+GCC is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 3, or (at your option)
+any later version.
+
+GCC is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3. If not see
+<http://www.gnu.org/licenses/>. */
+
+#include "elna/gcc/elna-module-loader.h"
+
+#include <fstream>
+
+#include "elna/gcc/elna-builtins.h"
+#include "elna/gcc/elna-diagnostic.h"
+#include "elna/gcc/elna-generic.h"
+
+namespace elna::gcc
+{
+ std::vector<std::string> elna_include_dirs;
+
+ module_loader::module_loader(const std::shared_ptr<symbol_table>& symbols)
+ : symbols(symbols)
+ {
+ }
+
+ std::filesystem::path module_loader::resolve(const std::filesystem::path& relative,
+ const boot::source_position& position)
+ {
+ std::vector<std::filesystem::path> found;
+
+ for (const auto& include_path : elna_include_dirs)
+ {
+ std::filesystem::path full_path = include_path / relative;
+
+ if (std::filesystem::exists(full_path))
+ {
+ found.push_back(std::move(full_path));
+ }
+ }
+ if (found.empty())
+ {
+ return relative;
+ }
+ if (found.size() > 1)
+ {
+ const location_t gcc_location = get_location(&position);
+ error_at(gcc_location, "Module %s was found in more than one include path",
+ relative.native().c_str());
+ }
+ return std::filesystem::weakly_canonical(found.front());
+ }
+
+ boot::dependency module_loader::read(const std::filesystem::path& key)
+ {
+ std::ifstream entry_point{ key, std::ios::in };
+
+ if (!entry_point)
+ {
+ fatal_error(UNKNOWN_LOCATION, "Cannot open filename %s: %m", key.native().c_str());
+ }
+ const linemap_guard guard(key);
+ return boot::read_source(entry_point, get_host_target());
+ }
+
+ void module_loader::finalize(const std::filesystem::path&,
+ const std::shared_ptr<boot::symbol_table>& module_scope) const
+ {
+ rewrite_symbol_table(module_scope, this->symbols);
+ }
+
+ void compile_files(const char *const *filenames, unsigned int count)
+ {
+ const boot::target_info target = get_host_target();
+ boot::dependency_state<std::shared_ptr<symbol_table>> state{ builtin_symbol_table(), target };
+ module_loader loader{ state.custom };
+
+ for (unsigned int i = 0; i < count; i++)
+ {
+ const std::filesystem::path key = std::filesystem::weakly_canonical(filenames[i]);
+ boot::dependency result = state.compile(key, loader, target);
+
+ report_errors(result.errors);
+
+ if (result.value != nullptr)
+ {
+ linemap_add(line_table, LC_ENTER, 0, key.native().c_str(), 1);
+ generic_visitor visitor{ state.custom, state.find(key)->second, target };
+ result.value->accept(&visitor);
+ linemap_add(line_table, LC_LEAVE, 0, nullptr, 0);
+ }
+ }
+ }
+}
diff --git a/gcc/gcc/elna1.cc b/gcc/gcc/elna1.cc
index 180c839..2c43178 100644
--- a/gcc/gcc/elna1.cc
+++ b/gcc/gcc/elna1.cc
@@ -15,13 +15,9 @@ You should have received a copy of the GNU General Public License
along with GCC; see the file COPYING3. If not see
<http://www.gnu.org/licenses/>. */
-#include <fstream>
-
-#include "elna/gcc/elna-diagnostic.h"
-#include "elna/boot/dependency.h"
-#include "elna/gcc/elna-tree.h"
-#include "elna/gcc/elna-generic.h"
+#include "elna/gcc/elna-module-loader.h"
#include "elna/gcc/elna-builtins.h"
+#include "elna/gcc/elna-tree.h"
#include "config.h"
#include "system.h"
@@ -38,7 +34,6 @@ along with GCC; see the file COPYING3. If not see
tree elna_global_trees[ELNA_TI_MAX];
hash_map<nofree_string_hash, tree> *elna_global_decls = nullptr;
-std::vector<std::string> elna_include_dirs;
/* The resulting tree type. */
@@ -64,98 +59,9 @@ static bool elna_langhook_init()
return true;
}
-using dependency_state = elna::boot::dependency_state<std::shared_ptr<elna::gcc::symbol_table>>;
-
-static std::vector<std::filesystem::path> find_module(const elna::boot::import_declaration* declaration)
-{
- std::filesystem::path relative_path = elna::boot::build_path(declaration->segments);
- std::vector<std::filesystem::path> found;
-
- for (const auto& include_path : elna_include_dirs)
- {
- std::filesystem::path full_path = include_path / relative_path;
-
- if (std::filesystem::exists(full_path))
- {
- found.push_back(std::move(full_path));
- }
- }
- if (found.empty())
- {
- found.push_back(std::move(relative_path));
- }
- else if (found.size() > 1)
- {
- const location_t gcc_location = elna::gcc::get_location(&declaration->position());
- error_at(gcc_location, "Module %s was found in more than one include path",
- relative_path.native().c_str());
- }
- return found;
-}
-
-static elna::boot::dependency elna_parse_file(dependency_state& state, const char *filename)
-{
- std::ifstream entry_point{ filename, std::ios::in };
-
- if (!entry_point)
- {
- fatal_error(UNKNOWN_LOCATION, "Cannot open filename %s: %m", filename);
- }
- const elna::gcc::linemap_guard guard(filename);
- elna::boot::dependency outcome = elna::boot::read_source(entry_point,
- elna::gcc::get_host_target());
-
- elna::boot::symbol_bag outcome_bag{ std::move(outcome.unresolved), state.globals };
-
- if (!outcome.has_errors())
- {
- for (const elna::boot::import_declaration* sub_tree : outcome.tree->imports)
- {
- const std::filesystem::path sub_path = find_module(sub_tree)[0];
- dependency_state::const_iterator cached_import = state.find(sub_path);
-
- if (cached_import == std::cend(state))
- {
- const char *filename_pointer = ggc_strdup(sub_path.native().c_str());
-
- elna_parse_file(state, filename_pointer);
- cached_import = state.find(sub_path);
- }
- if (cached_import != std::cend(state))
- {
- outcome_bag.add_import(cached_import->second);
- }
- }
- outcome.errors() = analyze_semantics(outcome.tree, outcome_bag, elna::gcc::get_host_target());
- }
- if (outcome.has_errors())
- {
- elna::gcc::report_errors(outcome.errors());
- }
- state.insert(filename, outcome_bag);
- elna::gcc::rewrite_symbol_table(outcome_bag.leave(), state.custom);
-
- return outcome;
-}
-
static void elna_langhook_parse_file()
{
- elna::boot::target_info target = elna::gcc::get_host_target();
- dependency_state state{ elna::gcc::builtin_symbol_table(), target };
-
- for (unsigned int i = 0; i < num_in_fnames; i++)
- {
- elna::boot::dependency outcome = elna_parse_file(state, in_fnames[i]);
-
- if (!outcome.has_errors())
- {
- linemap_add(line_table, LC_ENTER, 0, in_fnames[i], 1);
- elna::gcc::generic_visitor generic_visitor{ state.custom,
- state.find(in_fnames[i])->second, target };
- outcome.tree->accept(&generic_visitor);
- linemap_add(line_table, LC_LEAVE, 0, nullptr, 0);
- }
- }
+ elna::gcc::compile_files(in_fnames, num_in_fnames);
}
static tree elna_langhook_type_for_mode(enum machine_mode mode, int unsignedp)
@@ -259,7 +165,7 @@ static bool elna_langhook_handle_option(
switch (static_cast<opt_code>(scode))
{
case OPT_I:
- elna_include_dirs.emplace_back(arg);
+ elna::gcc::elna_include_dirs.emplace_back(arg);
return true;
default:
return true;
diff --git a/include/elna/boot/dependency.h b/include/elna/boot/dependency.h
index d534585..bcd0035 100644
--- a/include/elna/boot/dependency.h
+++ b/include/elna/boot/dependency.h
@@ -21,32 +21,127 @@ along with GCC; see the file COPYING3. If not see
#include "elna/boot/ast.h"
#include "elna/boot/symbol.h"
+#include <concepts>
#include <filesystem>
#include <fstream>
+#include <unordered_set>
namespace elna::boot
{
- class dependency : public diagnostic_container
+ /**
+ * Result of a compilation step: a possibly incomplete \c value together
+ * with the diagnostics produced so far.
+ *
+ * When \c errors is non-empty, the value may be missing or incomplete;
+ * how much of it is usable is defined per instantiation.
+ */
+ template<typename T>
+ struct outcome
+ {
+ T value;
+ diagnostic_list errors;
+ };
+
+ /**
+ * Abstract syntax tree and diagnostics of a module compilation step.
+ *
+ * The tree is non-null if and only if the module itself was processed
+ * successfully and may be used further. Depending on the step, the
+ * diagnostics may also include the diagnostics of the imported modules.
+ */
+ using dependency = outcome<std::unique_ptr<unit>>;
+
+ /**
+ * Module scope and diagnostics produced by the semantic analysis of one
+ * module. The bag is a valid partial scope even when the analysis failed.
+ */
+ using analysis_result = outcome<symbol_bag>;
+
+ /**
+ * An import that closes a cycle, e.g. a module importing itself.
+ */
+ class circular_import_error final : public diagnostic
{
- diagnostic_list m_errors;
+ const std::string module_name;
public:
- std::unique_ptr<unit> tree;
- forward_table unresolved;
+ circular_import_error(const source_position position, const std::string& module_name);
- dependency() = default;
+ std::string what() const override;
};
+ /**
+ * Reads, parses and materializes a module. Materialization completes the
+ * syntactic processing; semantic analysis is a separate step.
+ *
+ * \param entry_point Module source.
+ * \param target Target machine information.
+ *
+ * \return Parsed module.
+ */
dependency read_source(std::istream& entry_point, const target_info& target);
+
+ /**
+ * Turns the import declaration into a relative module path, appending the
+ * \c .elna extension. Pure path manipulation - no file system access.
+ *
+ * \param segments Import declaration segments.
+ *
+ * \return Relative module path.
+ */
std::filesystem::path build_path(const std::vector<std::string>& segments);
- diagnostic_list analyze_semantics(std::unique_ptr<unit>& tree, symbol_bag& bag,
- const target_info& target);
+ /**
+ * Analyzes a module semantically: collects type declarations, resolves
+ * names, checks types and validates the module.
+ *
+ * Creates the symbol bag of the module; the unresolved type declarations
+ * never leave this function, so the bag is always complete when returned.
+ *
+ * \param tree Module to analyze.
+ * \param imports Exported symbols of the imported modules.
+ * \param globals Global (builtin) symbols.
+ * \param target Target machine information.
+ *
+ * \return Module scope and diagnostics.
+ */
+ analysis_result analyze_semantics(std::unique_ptr<unit>& tree,
+ const std::vector<std::shared_ptr<symbol_table>>& imports,
+ const std::shared_ptr<symbol_table>& globals, const target_info& target);
+
+ /**
+ * Host interface for \c dependency_state::compile.
+ *
+ * The boot layer deliberately does not touch the file system; all file
+ * system access of the compilation is concentrated in the host-side
+ * loader: it resolves import paths, opens the module sources and
+ * registers the finished module scopes with the code generator.
+ */
+ template<typename Loader>
+ concept module_loader = requires(Loader& loader,
+ const std::filesystem::path& relative, const source_position& position,
+ const std::filesystem::path& key,
+ const std::shared_ptr<symbol_table>& module_scope)
+ {
+ { loader.resolve(relative, position) } -> std::same_as<std::filesystem::path>;
+ { loader.read(key) } -> std::same_as<dependency>;
+ loader.finalize(key, module_scope);
+ };
+
+ /**
+ * Caches analyzed modules and drives the module compilation: reads a
+ * module, resolves its imports recursively and analyzes the results.
+ *
+ * \tparam T Host-side symbol table type.
+ */
template<typename T>
class dependency_state
{
std::unordered_map<std::filesystem::path, symbol_bag> cache;
+ // Modules being compiled right now. Used to detect circular imports.
+ std::unordered_set<std::filesystem::path> in_progress;
+
// The builtin table stores global aliases (such as Int, Word) as weak
// pointers, so the state keeps their owners alive.
std::vector<std::shared_ptr<alias_type>> alias_owners;
@@ -55,11 +150,10 @@ namespace elna::boot
const std::shared_ptr<symbol_table> globals;
T custom;
- using iterator = std::unordered_map<std::filesystem::path, symbol_bag>::iterator;
using const_iterator = std::unordered_map<std::filesystem::path, symbol_bag>::const_iterator;
explicit dependency_state(T custom, const target_info& target)
- : globals(builtin_symbol_table(target, this->alias_owners)), custom(custom)
+ : globals(builtin_symbol_table(target, this->alias_owners)), custom(std::move(custom))
{
}
@@ -68,29 +162,77 @@ namespace elna::boot
return cache.find(key);
}
- void insert(const std::filesystem::path& key, const symbol_bag& value)
- {
- cache.insert({ key, value });
- }
-
- iterator begin()
- {
- return this->cache.begin();
- }
-
- iterator end()
- {
- return this->cache.end();
- }
-
- const_iterator begin() const
- {
- return this->cache.cbegin();
- }
-
- const_iterator end() const
+ /**
+ * Compiles the module at \p key and everything it imports.
+ *
+ * \tparam Loader Host-side loader satisfying \c module_loader.
+ *
+ * \param key Module path. The key must be the path under which the
+ * module is found by \c resolve - modules are compared by
+ * their canonical paths to detect circular imports.
+ * \param loader Host-side module loader.
+ * \param target Target machine information.
+ *
+ * \return Abstract syntax tree of the module itself and the
+ * diagnostics of this module and all its imports (imports
+ * first).
+ */
+ template<module_loader Loader>
+ dependency compile(const std::filesystem::path& key, Loader& loader,
+ const target_info& target)
{
- return this->cache.cend();
+ auto cached = this->cache.find(key);
+ if (cached != this->cache.cend())
+ {
+ return { .value = nullptr, .errors = {} };
+ }
+ this->in_progress.insert(key);
+ dependency outcome = loader.read(key);
+
+ if (!outcome.errors.empty())
+ {
+ diagnostic_list errors;
+ std::swap(errors, outcome.errors);
+ this->cache.insert({ key, symbol_bag({}, this->globals) });
+ this->in_progress.erase(key);
+ return { .value = nullptr, .errors = std::move(errors) };
+ }
+ diagnostic_list errors;
+ std::vector<std::shared_ptr<symbol_table>> imports;
+
+ for (const import_declaration* sub_tree : outcome.value->imports)
+ {
+ const std::filesystem::path module_path = loader.resolve(
+ build_path(sub_tree->segments), sub_tree->position());
+
+ if (this->in_progress.contains(module_path))
+ {
+ errors.push_back(std::make_unique<circular_import_error>(sub_tree->position(),
+ module_path.stem().string()));
+ continue;
+ }
+ dependency sub = this->compile(module_path, loader, target);
+ for (auto& error : sub.errors)
+ {
+ errors.push_back(std::move(error));
+ }
+ imports.push_back(this->cache.find(module_path)->second.exported_symbols());
+ }
+ analysis_result result = analyze_semantics(outcome.value, imports, this->globals, target);
+ const bool failed = !result.errors.empty();
+
+ for (auto& error : result.errors)
+ {
+ errors.push_back(std::move(error));
+ }
+ this->cache.insert({ key, result.value });
+ loader.finalize(key, result.value.leave());
+ this->in_progress.erase(key);
+ if (failed)
+ {
+ outcome.value.reset();
+ }
+ return { .value = std::move(outcome.value), .errors = std::move(errors) };
}
};
}
diff --git a/include/elna/boot/symbol.h b/include/elna/boot/symbol.h
index 0c1026d..e67f0f7 100644
--- a/include/elna/boot/symbol.h
+++ b/include/elna/boot/symbol.h
@@ -499,9 +499,9 @@ namespace elna::boot
/**
* Add imported symbols to the scope.
*
- * \param bag Symbol bag of another module.
+ * \param symbols Exported symbol table of another module.
*/
- void add_import(const symbol_bag& bag);
+ void add_import(const std::shared_ptr<symbol_table>& symbols);
/**
* Tells whether the current scope is the module global scope.
@@ -513,9 +513,16 @@ namespace elna::boot
*/
bool is_global() const;
- private:
+ /**
+ * The symbol table containing the exported declarations of the
+ * module, computed lazily. Must be called while the current scope is
+ * still the module scope.
+ *
+ * \return Exported symbols of the module.
+ */
std::shared_ptr<symbol_table> exported_symbols() const;
+ private:
mutable std::shared_ptr<symbol_table> m_exported;
};
diff --git a/include/elna/gcc/elna-module-loader.h b/include/elna/gcc/elna-module-loader.h
new file mode 100644
index 0000000..04303a3
--- /dev/null
+++ b/include/elna/gcc/elna-module-loader.h
@@ -0,0 +1,92 @@
+/* Module loading glue between the boot pipeline and the GCC driver.
+ Copyright (C) 2025 Free Software Foundation, Inc.
+
+GCC is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 3, or (at your option)
+any later version.
+
+GCC is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3. If not see
+<http://www.gnu.org/licenses/>. */
+
+#pragma once
+
+#include <filesystem>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "config.h"
+#include "system.h"
+#include "coretypes.h"
+
+#include "elna/boot/dependency.h"
+#include "elna/gcc/elna-tree.h"
+
+namespace elna::gcc
+{
+ // Include directories populated by the -I option handler, searched by
+ // the module loader when resolving import paths.
+ extern std::vector<std::string> elna_include_dirs;
+
+ /**
+ * Module loader tying the boot compilation pipeline to the GCC driver.
+ *
+ * All file system access of the compilation is concentrated here: the
+ * boot layer only builds and compares paths, the loader resolves import
+ * paths against the include directories, opens the files and registers
+ * each analyzed module scope with the GCC symbol table.
+ */
+ class module_loader
+ {
+ const std::shared_ptr<symbol_table> symbols;
+
+ public:
+ explicit module_loader(const std::shared_ptr<symbol_table>& symbols);
+
+ /**
+ * Searches the include directories for \p relative.
+ *
+ * \param relative Module path built from the import declaration.
+ * \param position Import declaration position for the ambiguity
+ * diagnostic.
+ *
+ * \return The resolved module path, canonical if found.
+ */
+ static std::filesystem::path resolve(const std::filesystem::path& relative,
+ const boot::source_position& position);
+
+ /**
+ * Opens the module source and parses it.
+ *
+ * \param key Resolved module path.
+ *
+ * \return Parsed module.
+ */
+ static boot::dependency read(const std::filesystem::path& key);
+
+ /**
+ * Registers the analyzed module scope with the GCC symbol table.
+ *
+ * \param key Resolved module path.
+ * \param module_scope Analyzed module scope.
+ */
+ void finalize(const std::filesystem::path& key,
+ const std::shared_ptr<boot::symbol_table>& module_scope) const;
+ };
+
+ /**
+ * Compiles the given modules and everything they import, reports the
+ * diagnostics and generates code for the modules that compiled cleanly.
+ *
+ * \param filenames Entry module names.
+ * \param count Number of entry modules.
+ */
+ void compile_files(const char *const *filenames, unsigned int count);
+}
diff --git a/testsuite/fail_compilation/circular_import/sut.elna b/testsuite/fail_compilation/circular_import/sut.elna
new file mode 100644
index 0000000..f6452fb
--- /dev/null
+++ b/testsuite/fail_compilation/circular_import/sut.elna
@@ -0,0 +1,3 @@
+import sut (* @Error Circular import of module 'sut' *)
+
+end.