diff options
| author | Eugen Wissner <belka@caraus.de> | 2026-08-25 23:41:18 +0200 |
|---|---|---|
| committer | Eugen Wissner <belka@caraus.de> | 2026-08-25 23:41:18 +0200 |
| commit | 81370291c039593a9886c7b3f776ca55a2858f0d (patch) | |
| tree | 47e12c5263e7784d3f0fc75fbbf6d791291e2230 /include | |
| parent | df51f9c4ee0ee4ac22206513bcd60e6928b7eaeb (diff) | |
| download | elna-81370291c039593a9886c7b3f776ca55a2858f0d.tar.gz | |
Reject circular imports
Diffstat (limited to 'include')
| -rw-r--r-- | include/elna/boot/dependency.h | 204 | ||||
| -rw-r--r-- | include/elna/boot/symbol.h | 13 | ||||
| -rw-r--r-- | include/elna/gcc/elna-module-loader.h | 92 |
3 files changed, 275 insertions, 34 deletions
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); +} |
