aboutsummaryrefslogtreecommitdiff
path: root/include
diff options
context:
space:
mode:
Diffstat (limited to 'include')
-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
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);
+}