diff options
| author | Eugen Wissner <belka@caraus.de> | 2026-08-28 02:39:42 +0200 |
|---|---|---|
| committer | Eugen Wissner <belka@caraus.de> | 2026-08-28 02:39:42 +0200 |
| commit | 8e655a0786aec5e0215e09f2a9629c6f32e34793 (patch) | |
| tree | 0788634cf66fed9c1a1d264b5b8c30da49380559 | |
| parent | 4d4537866690a1ef3d882f5e9a6e01d8220a3650 (diff) | |
| download | elna-8e655a0786aec5e0215e09f2a9629c6f32e34793.tar.gz | |
Reject declarations shadowing imports
25 files changed, 409 insertions, 145 deletions
diff --git a/boot/dependency.cc b/boot/dependency.cc index 7f87e40..7e103aa 100644 --- a/boot/dependency.cc +++ b/boot/dependency.cc @@ -37,36 +37,36 @@ namespace elna::boot return "Circular import of module '" + this->module_name + "'"; } - dependency read_source(std::istream& entry_point, const target_info& target) + read_result read_source(std::istream& entry_point, const target_info& target) { driver parse_driver; lexer tokenizer(entry_point); yy::parser parser(tokenizer, parse_driver); - dependency outcome; if (parser() != 0) { - std::swap(outcome.errors, parse_driver.errors()); - return outcome; - } - else - { - std::swap(outcome.value, parse_driver.tree); + diagnostic_list errors; + std::swap(errors, parse_driver.errors()); + return read_result{ std::in_place_type<diagnostic_list>, std::move(errors) }; } + std::unique_ptr<unit> tree; + std::swap(tree, parse_driver.tree); materialization_visitor materialization_visitor(target); - outcome.value->accept(&materialization_visitor); + tree->accept(&materialization_visitor); if (materialization_visitor.has_errors()) { - std::swap(outcome.errors, materialization_visitor.errors()); - outcome.value.reset(); + diagnostic_list errors; + std::swap(errors, materialization_visitor.errors()); + return read_result{ std::in_place_type<diagnostic_list>, std::move(errors) }; } - return outcome; + return read_result{ std::in_place_type<std::unique_ptr<unit>>, std::move(tree) }; } 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) + const std::shared_ptr<symbol_table>& globals, const target_info& target, + const std::filesystem::path& module_path) { declaration_visitor declarations{}; tree->accept(&declarations); @@ -81,7 +81,7 @@ namespace elna::boot { result.value.add_import(import); } - name_analysis_visitor name_analyser(result.value, target); + name_analysis_visitor name_analyser(result.value, target, module_path); tree->accept(&name_analyser); if (name_analyser.has_errors()) diff --git a/boot/evaluator.cc b/boot/evaluator.cc index 1d10346..6a9fb3b 100644 --- a/boot/evaluator.cc +++ b/boot/evaluator.cc @@ -51,7 +51,7 @@ namespace elna::boot }, this->payload); } - std::optional<std::pair<std::string, source_position>> non_constant_expression_error::note() const + std::optional<diagnostic_note> non_constant_expression_error::note() const { if (std::holds_alternative<initializer>(this->payload)) { @@ -297,25 +297,37 @@ namespace elna::boot std::optional<constant_value> evaluator::evaluate_field_access(field_access_expression& subject) { - auto type_to_check = subject.base().type_decoration; - if (type_to_check.empty()) + // Accessing a member of an enumeration. The base is the type name, + // so the enumeration is looked up in the symbol table instead of + // reading the type decoration set by name analysis. + if (auto *base_designator = subject.base().is_designator()) { - type_to_check = subject.type_decoration; - } - auto resolved_base = resolve_underlying_type(type_to_check); - if (auto enumeration = resolved_base.get<enumeration_type>()) - { - auto enumeration_distance = std::distance(enumeration->members.begin(), - std::ranges::find(enumeration->members, subject.field().name())); - const std::size_t enumeration_position = static_cast<std::size_t>(enumeration_distance); - - if (enumeration_position >= enumeration->members.size()) + if (auto *base_name = base_designator->is_named()) { - return std::nullopt; + auto symbol = this->bag.lookup(base_name->name); + + if (symbol != nullptr) + { + if (auto type_symbol = symbol->is_type()) + { + if (auto enumeration = resolve_underlying_type(type_symbol->symbol).get<enumeration_type>()) + { + auto member_iterator = std::ranges::find(enumeration->members, subject.field().name()); + + if (member_iterator == enumeration->members.end()) + { + return std::nullopt; + } + const std::size_t enumeration_position = static_cast<std::size_t>( + std::distance(enumeration->members.begin(), member_iterator)); + + return constant_value{ integer_literal::from(enumeration_position + 1U) }; + } + } + } } - return constant_value{ integer_literal::from(enumeration_position + 1U) }; } - else if (auto base = evaluate(subject.base())) + if (auto base = evaluate(subject.base())) { if (auto *record = std::get_if<constant_aggregate<ordered_map>>(&base.value())) { @@ -732,19 +744,32 @@ namespace elna::boot { auto value = evaluate(subject.value()); - if (!value.has_value() || subject.type_decoration.empty()) + if (!value.has_value()) { return std::nullopt; } - const type resolved = resolve_underlying_type(subject.type_decoration); - - if (is_primitive_type(resolved, "Int")) - { - return cast_to_int(value.value()); - } - else if (is_primitive_type(resolved, "Word")) + // The target type is looked up in the symbol table instead of reading + // the type decoration set by name analysis. + if (auto *target = subject.target().is_named()) { - return cast_to_word(value.value()); + auto symbol = this->bag.lookup(target->name); + + if (symbol != nullptr) + { + if (auto type_symbol = symbol->is_type()) + { + const type resolved = resolve_underlying_type(type_symbol->symbol); + + if (is_primitive_type(resolved, "Int")) + { + return cast_to_int(value.value()); + } + else if (is_primitive_type(resolved, "Word")) + { + return cast_to_word(value.value()); + } + } + } } return constant_value{ value.value() }; diff --git a/boot/name_analysis.cc b/boot/name_analysis.cc index 30d3cda..10b8a8d 100644 --- a/boot/name_analysis.cc +++ b/boot/name_analysis.cc @@ -24,7 +24,7 @@ along with GCC; see the file COPYING3. If not see namespace elna::boot { declaration_error::declaration_error(const source_position position, const std::string& name, payload_type payload) - : diagnostic(position), name(name), payload(payload) + : diagnostic(position), name(name), payload(std::move(payload)) { } @@ -57,11 +57,12 @@ namespace elna::boot }, this->payload); } - std::optional<std::pair<std::string, source_position>> declaration_error::note() const + std::optional<diagnostic_note> declaration_error::note() const { if (std::holds_alternative<redefinition>(this->payload)) { - return previous_declaration_note(std::get<redefinition>(this->payload).original); + return previous_declaration_note(std::get<redefinition>(this->payload).original, + "previously declared here", std::get<redefinition>(this->payload).file); } else { @@ -99,7 +100,7 @@ namespace elna::boot }, this->payload); } - std::optional<std::pair<std::string, source_position>> const_qualifier_error::note() const + std::optional<diagnostic_note> const_qualifier_error::note() const { if (std::holds_alternative<not_initialized>(this->payload)) { @@ -175,7 +176,7 @@ namespace elna::boot }, payload); } - std::optional<std::pair<std::string, source_position>> member_error::note() const + std::optional<diagnostic_note> member_error::note() const { if (std::holds_alternative<duplicate>(this->payload)) { @@ -201,8 +202,9 @@ namespace elna::boot } } - name_analysis_visitor::name_analysis_visitor(symbol_bag bag, const target_info& target) - : bag(std::move(bag)), constant_evaluator(this->bag, target) + name_analysis_visitor::name_analysis_visitor(symbol_bag bag, const target_info& target, + std::filesystem::path module_file) + : bag(std::move(bag)), constant_evaluator(this->bag, target), module_file(std::move(module_file)) { } @@ -295,7 +297,15 @@ namespace elna::boot info->exported = declaration->identifier.exported(); info->position.emplace(declaration->position()); - this->bag.enter(declaration->identifier.name(), info); + info->file = this->module_file; + + if (!this->bag.enter(declaration->identifier.name(), info)) + { + auto original = this->bag.lookup(declaration->identifier.name()); + add_error<declaration_error>(declaration->identifier.id().position(), declaration->identifier.name(), + declaration_error::redefinition{ .original = original->position, + .file = this->redefinition_file(original) }); + } } void name_analysis_visitor::visit(pointer_type_expression *expression) @@ -542,17 +552,25 @@ namespace elna::boot this->current_type = type(result_type); } + std::filesystem::path name_analysis_visitor::redefinition_file( + const std::shared_ptr<info>& original) const + { + return original->file != this->module_file ? original->file : std::filesystem::path{}; + } + std::shared_ptr<variable_info> name_analysis_visitor::register_variable(const std::string& name, const bool is_extern, const source_position position) { auto variable_symbol = std::make_shared<variable_info>(this->current_type, is_extern); variable_symbol->position.emplace(position); + variable_symbol->file = this->module_file; if (!this->bag.enter(name, variable_symbol)) { auto original = this->bag.lookup(name); add_error<declaration_error>(position, name, - declaration_error::redefinition{ .original = original->position }); + declaration_error::redefinition{ .original = original->position, + .file = this->redefinition_file(original) }); } return variable_symbol; } @@ -631,7 +649,14 @@ namespace elna::boot } info->exported = declaration->identifier.exported(); info->position.emplace(declaration->position()); - this->bag.enter(declaration->identifier.name(), info); + info->file = this->module_file; + if (!this->bag.enter(declaration->identifier.name(), info)) + { + auto original = this->bag.lookup(declaration->identifier.name()); + add_error<declaration_error>(declaration->identifier.id().position(), declaration->identifier.name(), + declaration_error::redefinition{ .original = original->position, + .file = this->redefinition_file(original) }); + } } void name_analysis_visitor::visit(procedure_call *call) @@ -666,12 +691,16 @@ namespace elna::boot { this->bag.enter(); auto variable_type = lookup_primitive_type("Int"); - this->bag.enter("count", std::make_shared<variable_info>(variable_type, false)); + auto count_symbol = std::make_shared<variable_info>(variable_type, false); + count_symbol->file = this->module_file; + this->bag.enter("count", count_symbol); variable_type = lookup_primitive_type("Word8"); variable_type = type(std::make_shared<pointer_type>(variable_type)); variable_type = type(std::make_shared<pointer_type>(variable_type)); - this->bag.enter("parameters", std::make_shared<variable_info>(variable_type, false)); + auto parameters_symbol = std::make_shared<variable_info>(variable_type, false); + parameters_symbol->file = this->module_file; + this->bag.enter("parameters", parameters_symbol); for (statement *const statement : unit->entry_point) { @@ -971,7 +1000,7 @@ namespace elna::boot { add_error<declaration_error>(declaration->identifier.id().position(), declaration->identifier.id().name(), - declaration_error::redefinition{ .original = declaration->position() }); + declaration_error::redefinition{ .original = declaration->position(), .file = {} }); } } diff --git a/boot/result.cc b/boot/result.cc index d25b85b..635a121 100644 --- a/boot/result.cc +++ b/boot/result.cc @@ -75,6 +75,11 @@ namespace elna::boot return m_errors; } + const std::deque<std::unique_ptr<diagnostic>>& diagnostic_container::errors() const + { + return m_errors; + } + bool diagnostic_container::has_errors() const { return !m_errors.empty(); @@ -131,12 +136,13 @@ namespace elna::boot return this->m_exported; } - std::optional<std::pair<std::string, source_position>> previous_declaration_note( - const std::optional<source_position>& original, std::string_view label) + std::optional<diagnostic_note> previous_declaration_note( + const std::optional<source_position>& original, std::string_view label, + const std::filesystem::path& file) { if (original.has_value() && original.value().start().available()) { - return std::make_pair(std::string(label), original.value()); + return diagnostic_note{ .message = std::string(label), .position = original.value(), .file = file }; } else { @@ -144,13 +150,13 @@ namespace elna::boot } } - std::optional<std::pair<std::string, source_position>> identifier_list_note( + std::optional<diagnostic_note> identifier_list_note( const std::vector<identifier>& identifiers) { auto position_span = source_position(identifiers.front().position().start(), identifiers.back().position().end()); - return std::make_optional(std::make_pair(join(identifiers), position_span)); + return diagnostic_note{ .message = join(identifiers), .position = position_span, .file = {} }; } std::vector<identifier> extract_identifiers(const std::vector<identifier_definition>& identifiers) diff --git a/boot/symbol.cc b/boot/symbol.cc index 8855d52..7ef09c9 100644 --- a/boot/symbol.cc +++ b/boot/symbol.cc @@ -17,6 +17,7 @@ along with GCC; see the file COPYING3. If not see #include "elna/boot/symbol.h" +#include <ranges> #include <cassert> #include <utility> @@ -356,21 +357,28 @@ namespace elna::boot this->symbols = std::make_shared<symbol_table>(global_table); } + std::shared_ptr<info> symbol_bag::lookup_import(const std::string& name) const + { + const auto found = std::ranges::find_if(this->imports, + [&name](const std::shared_ptr<symbol_table>& import_bag) { + return import_bag->lookup(name) != nullptr; + } + ); + return found == this->imports.cend() ? nullptr : (*found)->lookup(name); + } + std::shared_ptr<info> symbol_bag::lookup(const std::string& name) { - for (const auto& import_bag : this->imports) + if (auto result = this->lookup_import(name)) { - if (auto result = import_bag->lookup(name)) - { - return result; - } + return result; } return this->symbols->lookup(name); } bool symbol_bag::enter(const std::string& name, const std::shared_ptr<info>& entry) { - return this->symbols->enter(name, entry); + return this->lookup_import(name) == nullptr && this->symbols->enter(name, entry); } std::shared_ptr<symbol_table> symbol_bag::enter() diff --git a/boot/validation.cc b/boot/validation.cc index a81282b..b046b4e 100644 --- a/boot/validation.cc +++ b/boot/validation.cc @@ -31,7 +31,7 @@ namespace elna::boot return "Duplicate case label"; } - std::optional<std::pair<std::string, source_position>> validation_error::note() const + std::optional<diagnostic_note> validation_error::note() const { return previous_declaration_note(this->first, "Previous label here"); } diff --git a/gcc/gcc/elna-diagnostic.cc b/gcc/gcc/elna-diagnostic.cc index ea1f64c..1a65eed 100644 --- a/gcc/gcc/elna-diagnostic.cc +++ b/gcc/gcc/elna-diagnostic.cc @@ -63,9 +63,11 @@ namespace elna::gcc return make_location(caret, start, end); } - void report_errors(const std::deque<std::unique_ptr<boot::diagnostic>>& errors) + void report_errors(const boot::module_diagnostics& diagnostics) { - for (const auto& error : errors) + const linemap_guard guard(diagnostics.module); + + for (const auto& error : diagnostics.errors()) { if (error->position.start().available()) { @@ -79,8 +81,23 @@ namespace elna::gcc } if (auto note = error->note()) { - const location_t note_loc = make_range(note->second); - inform(note_loc, "%s", note->first.c_str()); + if (note->file.empty()) + { + const location_t note_loc = make_range(note->position); + inform(note_loc, "%s", note->message.c_str()); + } + else + { + // Positions carry no file identity; render the note under + // a map of the module the note points into. LC_RENAME + // continues the current map under the new name without + // recording an include edge, so no "In file included + // from" chain is printed for the note. + linemap_add(line_table, LC_RENAME, 0, ggc_strdup(note->file.native().c_str()), 1); + const location_t note_loc = make_range(note->position); + inform(note_loc, "%s", note->message.c_str()); + linemap_add(line_table, LC_RENAME, 0, ggc_strdup(diagnostics.module.native().c_str()), 1); + } } } } diff --git a/gcc/gcc/elna-module-loader.cc b/gcc/gcc/elna-module-loader.cc index 1dd7596..8827a1e 100644 --- a/gcc/gcc/elna-module-loader.cc +++ b/gcc/gcc/elna-module-loader.cc @@ -33,7 +33,7 @@ namespace elna::gcc } std::filesystem::path module_loader::resolve(const std::filesystem::path& relative, - const boot::source_position& position) + const boot::source_position& position, const std::filesystem::path& module) { std::vector<std::filesystem::path> found; @@ -52,14 +52,20 @@ namespace elna::gcc } if (found.size() > 1) { + const linemap_guard guard(module); 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()); + + for (const auto& candidate : found) + { + inform(UNKNOWN_LOCATION, " %s", candidate.native().c_str()); + } } return std::filesystem::weakly_canonical(found.front()); } - boot::dependency module_loader::read(const std::filesystem::path& key) + boot::read_result module_loader::read(const std::filesystem::path& key) { std::ifstream entry_point{ key, std::ios::in }; @@ -88,7 +94,10 @@ namespace elna::gcc const std::filesystem::path key = std::filesystem::weakly_canonical(filenames[i]); boot::dependency result = state.compile(key, loader, target); - report_errors(result.errors); + for (const auto& diagnostics : result.errors) + { + report_errors(diagnostics); + } if (result.value != nullptr) { diff --git a/include/elna/boot/ast.h b/include/elna/boot/ast.h index c6a0828..f959936 100644 --- a/include/elna/boot/ast.h +++ b/include/elna/boot/ast.h @@ -201,7 +201,7 @@ namespace elna::boot * override only the node types they care about, with the guarantee that * unimplemented nodes are never silently ignored. */ - class empty_visitor : public parser_visitor + class empty_visitor : public virtual parser_visitor { public: [[noreturn]] void visit(array_type_expression *) override; @@ -249,7 +249,7 @@ namespace elna::boot /** * Abstract visitor that visits all nodes recursively. */ - class walking_visitor : public parser_visitor + class walking_visitor : public virtual parser_visitor { public: void visit(array_type_expression *) override; diff --git a/include/elna/boot/dependency.h b/include/elna/boot/dependency.h index 028dcf1..18df64c 100644 --- a/include/elna/boot/dependency.h +++ b/include/elna/boot/dependency.h @@ -25,37 +25,83 @@ along with GCC; see the file COPYING3. If not see #include <filesystem> #include <fstream> #include <unordered_set> +#include <variant> +#include <vector> namespace elna::boot { /** - * 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. + * Result of the module read step: the abstract syntax tree of the + * module, or the diagnostics that prevented it. The tree is present + * exactly when the read succeeded. */ - template<typename T> - struct outcome + using read_result = std::variant<std::unique_ptr<unit>, diagnostic_list>; + + /** + * Diagnostics produced while compiling one module, paired with the + * module path they were produced in. + */ + struct module_diagnostics : diagnostic_container { - T value; - diagnostic_list errors; + std::filesystem::path module; + + module_diagnostics(std::filesystem::path module, diagnostic_list errors) + : diagnostic_container(std::move(errors)), module(std::move(module)) + { + } }; /** - * 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. + * Result of compiling a module and its transitive imports: the AST of + * the module itself and the diagnostics of every module compiled along + * the way, grouped by the module that produced them, imports first. */ - using dependency = outcome<std::unique_ptr<unit>>; + struct dependency + { + std::unique_ptr<unit> value; + std::vector<module_diagnostics> errors; + + /** + * Appends the diagnostics of one module, merging them with the + * previous entry when it belongs to the same module. + * + * \param module Module the diagnostics were produced in. + * \param errors Diagnostics to append. An empty list is ignored. + */ + void append(std::filesystem::path module, diagnostic_list errors) + { + if (errors.empty()) + { + return; + } + if (!this->errors.empty() && this->errors.back().module == module) + { + auto& existing = this->errors.back().errors(); + for (auto& error : errors) + { + existing.push_back(std::move(error)); + } + } + else + { + this->errors.emplace_back(std::move(module), std::move(errors)); + } + } + }; /** * Module scope and diagnostics produced by the semantic analysis of one - * module. The bag is a valid partial scope even when the analysis failed. + * module. + * + * The bag is a valid partial scope even when the analysis failed: the + * symbols collected before the failure are still used by the importing + * modules. */ - using analysis_result = outcome<symbol_bag>; + struct analysis_result + { + symbol_bag value; + diagnostic_list errors; + }; /** * An import that closes a cycle, e.g. a module importing itself. @@ -77,9 +123,9 @@ namespace elna::boot * \param entry_point Module source. * \param target Target machine information. * - * \return Parsed module. + * \return Parsed module, or the diagnostics that prevented it. */ - dependency read_source(std::istream& entry_point, const target_info& target); + read_result read_source(std::istream& entry_point, const target_info& target); /** * Turns the import declaration into a relative module path, appending the @@ -102,12 +148,15 @@ namespace elna::boot * \param imports Exported symbols of the imported modules. * \param globals Global (builtin) symbols. * \param target Target machine information. + * \param module_path Path of the module being analyzed, recorded in the + * symbol infos for diagnostics. * * \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); + const std::shared_ptr<symbol_table>& globals, const target_info& target, + const std::filesystem::path& module_path); /** * Host interface for \c dependency_state::compile. @@ -123,8 +172,8 @@ namespace elna::boot 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.resolve(relative, position, key) } -> std::same_as<std::filesystem::path>; + { loader.read(key) } -> std::same_as<read_result>; loader.finalize(key, module_scope); }; @@ -170,8 +219,8 @@ namespace elna::boot * \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). + * diagnostics of this module and all its imports, grouped by + * the module that produced them, imports first. */ template<module_loader Loader> dependency compile(const std::filesystem::path& key, Loader& loader, @@ -180,55 +229,58 @@ namespace elna::boot auto cached = this->cache.find(key); if (cached != this->cache.cend()) { - return { .value = nullptr, .errors = {} }; + return {}; } this->in_progress.insert(key); - dependency outcome = loader.read(key); + read_result parsed = loader.read(key); + dependency result; - if (!outcome.errors.empty()) + if (diagnostic_list *read_errors = std::get_if<diagnostic_list>(&parsed)) { - 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) }; + result.append(key, std::move(*read_errors)); + return result; } - diagnostic_list errors; + auto& tree = std::get<std::unique_ptr<unit>>(parsed); std::vector<std::shared_ptr<symbol_table>> imports; + diagnostic_list circular; - for (const import_declaration* sub_tree : outcome.value->imports) + for (const import_declaration* sub_tree : tree->imports) { const std::filesystem::path module_path = loader.resolve( - build_path(sub_tree->segments), sub_tree->position()); + build_path(sub_tree->segments), sub_tree->position(), key); if (this->in_progress.contains(module_path)) { - errors.push_back(std::make_unique<circular_import_error>(sub_tree->position(), + circular.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) + for (auto& diagnostics : sub.errors) { - errors.push_back(std::move(error)); + result.append(std::move(diagnostics.module), std::move(diagnostics.errors())); } 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(); + result.append(key, std::move(circular)); + analysis_result analysis = analyze_semantics(tree, imports, this->globals, target, key); + const bool failed = !analysis.errors.empty(); - for (auto& error : result.errors) + if (failed) { - errors.push_back(std::move(error)); + result.append(key, std::move(analysis.errors)); } - this->cache.insert({ key, result.value }); - loader.finalize(key, result.value.leave()); + this->cache.insert({ key, analysis.value }); + loader.finalize(key, analysis.value.leave()); this->in_progress.erase(key); if (failed) { - outcome.value.reset(); + tree.reset(); } - return { .value = std::move(outcome.value), .errors = std::move(errors) }; + result.value = std::move(tree); + return result; } }; } diff --git a/include/elna/boot/evaluator.h b/include/elna/boot/evaluator.h index dd70483..35cca1a 100644 --- a/include/elna/boot/evaluator.h +++ b/include/elna/boot/evaluator.h @@ -45,7 +45,7 @@ namespace elna::boot non_constant_expression_error(const source_position position, payload_type payload); std::string what() const override; - std::optional<std::pair<std::string, source_position>> note() const override; + std::optional<diagnostic_note> note() const override; private: payload_type payload; diff --git a/include/elna/boot/name_analysis.h b/include/elna/boot/name_analysis.h index f2e2eb9..a2e9437 100644 --- a/include/elna/boot/name_analysis.h +++ b/include/elna/boot/name_analysis.h @@ -22,6 +22,7 @@ along with GCC; see the file COPYING3. If not see #include "elna/boot/symbol.h" #include "elna/boot/evaluator.h" +#include <filesystem> #include <string> #include <memory> #include <optional> @@ -46,13 +47,18 @@ namespace elna::boot struct redefinition { std::optional<source_position> original; + /** + * Module the original is declared in. Empty when it is the same + * module as the error. + */ + std::filesystem::path file; }; using payload_type = std::variant<redefinition, kind>; declaration_error(const source_position position, const std::string& name, payload_type payload); std::string what() const override; - std::optional<std::pair<std::string, source_position>> note() const override; + std::optional<diagnostic_note> note() const override; private: std::string name; @@ -78,7 +84,7 @@ namespace elna::boot using payload_type = std::variant<not_initialized, kind>; const_qualifier_error(const source_position position, payload_type payload); - std::optional<std::pair<std::string, source_position>> note() const override; + std::optional<diagnostic_note> note() const override; std::string what() const override; @@ -103,7 +109,7 @@ namespace elna::boot const type& composite, payload_type payload); std::string what() const override; - std::optional<std::pair<std::string, source_position>> note() const override; + std::optional<diagnostic_note> note() const override; private: std::string name; @@ -128,6 +134,8 @@ namespace elna::boot type current_type; symbol_bag bag; evaluator constant_evaluator; + /// Path of the module being analyzed, recorded in the symbol infos. + std::filesystem::path module_file; std::pair<procedure_type, std::vector<std::string>> build_procedure( procedure_type_expression& expression); @@ -136,13 +144,24 @@ namespace elna::boot std::shared_ptr<variable_info> register_variable(const std::string& name, const bool is_extern, const source_position position); + /** + * File of \p original for a redefinition note. + * + * \param original Symbol the error redefines. + * \return Module of the original declaration; empty when it is in the + * module being analyzed, so the note renders under the + * error's own file. + */ + std::filesystem::path redefinition_file(const std::shared_ptr<info>& original) const; + type lookup_primitive_type(const std::string& name); type lookup_field(const type& composite_type, const std::string& field_name); std::optional<type> lookup_pointer_like_field(const std::string& field_name, const type& element_type); public: - name_analysis_visitor(symbol_bag bag, const target_info& target); + name_analysis_visitor(symbol_bag bag, const target_info& target, + std::filesystem::path module_file); void visit(array_type_expression *expression) override; void visit(slice_type_expression *expression) override; diff --git a/include/elna/boot/result.h b/include/elna/boot/result.h index b34c4d4..eacc072 100644 --- a/include/elna/boot/result.h +++ b/include/elna/boot/result.h @@ -19,6 +19,7 @@ along with GCC; see the file COPYING3. If not see #include <cstddef> #include <cstdint> +#include <filesystem> #include <functional> #include <string> #include <deque> @@ -79,6 +80,21 @@ namespace elna::boot }; /** + * Supplementary context shown alongside a primary diagnostic, e.g. the + * location of a previous declaration. + */ + struct diagnostic_note + { + std::string message; + source_position position; + /** + * Names the module the \c position belongs to. When empty, the position + * is in the same module as the primary diagnostic. + */ + std::filesystem::path file; + }; + + /** * A compilation error consists of an error message and position. */ class diagnostic @@ -98,9 +114,9 @@ namespace elna::boot * Supplementary context shown alongside the primary error, * e.g.\ the location of a previous declaration. * - * \return Optional note message and position. + * \return Optional note. */ - virtual std::optional<std::pair<std::string, source_position>> note() const + virtual std::optional<diagnostic_note> note() const { return std::nullopt; } @@ -115,8 +131,25 @@ namespace elna::boot diagnostic_container() = default; + /** + * Adopts a ready-made error list. + * + * \param errors Error list to take over. + */ + explicit diagnostic_container(diagnostic_list errors) + : m_errors(std::move(errors)) + { + } + public: + diagnostic_container(const diagnostic_container&) = delete; + // deque's move constructor is not noexcept in all standard library + // implementations, so the implicitly declared move is not either, + // and derived types would fall back to the deleted copy. + diagnostic_container(diagnostic_container&&) noexcept = default; + diagnostic_list& errors(); + const diagnostic_list& errors() const; template<typename T, typename... Args> void add_error(Args&&... arguments) @@ -188,12 +221,16 @@ namespace elna::boot * * \param original Source position of the previous declaration. * \param label Description what was declared previously. + * \param file Module the previous declaration was declared in. Empty if it + * is in the same module as the primary error. * \return Error note if the position of the previous declaration is available. */ - std::optional<std::pair<std::string, source_position>> previous_declaration_note( - const std::optional<source_position>& original, std::string_view label = "previously declared here"); + std::optional<diagnostic_note> previous_declaration_note( + const std::optional<source_position>& original, + std::string_view label = "previously declared here", + const std::filesystem::path& file = {}); - std::optional<std::pair<std::string, source_position>> identifier_list_note( + std::optional<diagnostic_note> identifier_list_note( const std::vector<identifier>& identifiers); /** diff --git a/include/elna/boot/symbol.h b/include/elna/boot/symbol.h index 46fb9e2..5f0de28 100644 --- a/include/elna/boot/symbol.h +++ b/include/elna/boot/symbol.h @@ -18,6 +18,7 @@ along with GCC; see the file COPYING3. If not see #pragma once #include <cstdint> +#include <filesystem> #include <forward_list> #include <memory> #include <optional> @@ -208,6 +209,8 @@ namespace elna::boot public: bool exported{ false }; std::optional<source_position> position; + /// Module the symbol is declared in. Empty for builtin symbols. + std::filesystem::path file; virtual ~info() = 0; @@ -434,6 +437,8 @@ namespace elna::boot std::forward_list<std::shared_ptr<symbol_table>> imports; forward_table unresolved; + std::shared_ptr<info> lookup_import(const std::string& name) const; + public: /** diff --git a/include/elna/boot/validation.h b/include/elna/boot/validation.h index 2b1de95..c99ce07 100644 --- a/include/elna/boot/validation.h +++ b/include/elna/boot/validation.h @@ -33,7 +33,7 @@ namespace elna::boot validation_error(const source_position position, source_position first); std::string what() const override; - std::optional<std::pair<std::string, source_position>> note() const override; + std::optional<diagnostic_note> note() const override; private: source_position first; diff --git a/include/elna/gcc/elna-diagnostic.h b/include/elna/gcc/elna-diagnostic.h index 10d5b44..858e21e 100644 --- a/include/elna/gcc/elna-diagnostic.h +++ b/include/elna/gcc/elna-diagnostic.h @@ -26,7 +26,7 @@ along with GCC; see the file COPYING3. If not see #include "coretypes.h" #include "diagnostic.h" -#include "elna/boot/result.h" +#include "elna/boot/dependency.h" namespace elna::gcc { @@ -43,5 +43,15 @@ namespace elna::gcc location_t get_location(const boot::source_position *position); location_t make_range(const boot::source_position& position); - void report_errors(const std::deque<std::unique_ptr<boot::diagnostic>>& errors); + + /** + * Reports the diagnostics of one module. + * + * Enters the module into the line map so that every position renders + * with the module's file, and leaves it afterwards. Notes pointing into + * other modules are rendered under their own file via a line map rename. + * + * \param diagnostics Diagnostics of one module paired with its path. + */ + void report_errors(const boot::module_diagnostics& diagnostics); } diff --git a/include/elna/gcc/elna-module-loader.h b/include/elna/gcc/elna-module-loader.h index 04303a3..2b46fc5 100644 --- a/include/elna/gcc/elna-module-loader.h +++ b/include/elna/gcc/elna-module-loader.h @@ -56,20 +56,22 @@ namespace elna::gcc * \param relative Module path built from the import declaration. * \param position Import declaration position for the ambiguity * diagnostic. + * \param module Module being compiled. The import declaration belongs + * to it, so the ambiguity diagnostic renders under its file. * * \return The resolved module path, canonical if found. */ static std::filesystem::path resolve(const std::filesystem::path& relative, - const boot::source_position& position); + const boot::source_position& position, const std::filesystem::path& module); /** * Opens the module source and parses it. * * \param key Resolved module path. * - * \return Parsed module. + * \return Parsed module, or the diagnostics that prevented it. */ - static boot::dependency read(const std::filesystem::path& key); + static boot::read_result read(const std::filesystem::path& key); /** * Registers the analyzed module scope with the GCC symbol table. diff --git a/rakelib/gcc.rake b/rakelib/gcc.rake index db832e1..30175eb 100644 --- a/rakelib/gcc.rake +++ b/rakelib/gcc.rake @@ -249,13 +249,19 @@ namespace :gcc do modules.each do |extra| cp_r extra, test_target end - extra_sources = test_target.glob("**/*.elna") - .map { |extra| %("#{extra.relative_path_from test_target}") } - - convert_test main, (test_target + 'sut.elna'), category, [ - "(* { dg-additional-options \"-I#{test_target.expand_path}\" } *)", - "(* { dg-additional-sources #{extra_sources * ' '} } *)" - ] + extra_lines = ["(* { dg-additional-options \"-I#{test_target.expand_path}\" } *)"] + + # Only 'runnable' tests link the helper modules into an executable, + # so only they need the helper sources compiled as separate + # translation units via dg-additional-sources. Elna resolves imports + # on its own, so passing it as an additional source there would add a + # second positional. + if category == 'runnable' + extra_sources = test_target.glob("**/*.elna") + .map { |extra| %("#{extra.relative_path_from test_target}") } + extra_lines << "(* { dg-additional-sources #{extra_sources * ' '} } *)" + end + convert_test main, (test_target + 'sut.elna'), category, extra_lines else convert_test test_path, test_target, category end diff --git a/source/main.elna b/source/main.elna index 13d9c61..bdcaf34 100644 --- a/source/main.elna +++ b/source/main.elna @@ -29,11 +29,6 @@ type data: ^^ElnaLexerToken end -var - stdout: ^FILE - stderr: ^FILE - stdin: ^FILE - (* Standard procedures. *) diff --git a/testsuite/fail_compilation/import_local_collision/helper.elna b/testsuite/fail_compilation/import_local_collision/helper.elna new file mode 100644 index 0000000..d6e3712 --- /dev/null +++ b/testsuite/fail_compilation/import_local_collision/helper.elna @@ -0,0 +1,6 @@ +var + X*: Int + +begin + X := 1 +end. diff --git a/testsuite/fail_compilation/import_local_collision/sut.elna b/testsuite/fail_compilation/import_local_collision/sut.elna new file mode 100644 index 0000000..a9de9c9 --- /dev/null +++ b/testsuite/fail_compilation/import_local_collision/sut.elna @@ -0,0 +1,7 @@ +import helper + +var + X: Int (* @Error Symbol 'X' has been already defined *) + +begin +end. diff --git a/testsuite/fail_compilation/record_duplicate_field.elna b/testsuite/fail_compilation/record_duplicate_field.elna new file mode 100644 index 0000000..b98f990 --- /dev/null +++ b/testsuite/fail_compilation/record_duplicate_field.elna @@ -0,0 +1,8 @@ +type + R = record + x: Int; + x: Int (* @Error Record already has a field named 'x' *) + end + +begin +end. diff --git a/testsuite/fail_compilation/record_duplicate_field_import_base/helper.elna b/testsuite/fail_compilation/record_duplicate_field_import_base/helper.elna new file mode 100644 index 0000000..0edb965 --- /dev/null +++ b/testsuite/fail_compilation/record_duplicate_field_import_base/helper.elna @@ -0,0 +1,7 @@ +type + Parent* = record + x: Int; + y: Int + end + +end. diff --git a/testsuite/fail_compilation/record_duplicate_field_import_base/sut.elna b/testsuite/fail_compilation/record_duplicate_field_import_base/sut.elna new file mode 100644 index 0000000..7530017 --- /dev/null +++ b/testsuite/fail_compilation/record_duplicate_field_import_base/sut.elna @@ -0,0 +1,10 @@ +import helper + +type + Child = record(Parent) + x: Int (* @Error Record already has a field named 'x' \(defined in base type 'Parent'\) *); + z: Int + end + +begin +end. diff --git a/testsuite/fail_compilation/redefined_variable.elna b/testsuite/fail_compilation/redefined_variable.elna new file mode 100644 index 0000000..9cf8c24 --- /dev/null +++ b/testsuite/fail_compilation/redefined_variable.elna @@ -0,0 +1,6 @@ +var + x: Int + x: Int (* @Error Symbol 'x' has been already defined *) + +begin +end. |
