diff options
| -rw-r--r-- | boot/dependency.cc | 65 | ||||
| -rw-r--r-- | boot/symbol.cc | 4 | ||||
| -rw-r--r-- | gcc/Make-lang.in | 1 | ||||
| -rw-r--r-- | gcc/gcc/elna-module-loader.cc | 102 | ||||
| -rw-r--r-- | gcc/gcc/elna1.cc | 102 | ||||
| -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 | ||||
| -rw-r--r-- | testsuite/fail_compilation/circular_import/sut.elna | 3 |
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. |
