aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEugen Wissner <belka@caraus.de>2026-08-28 02:39:42 +0200
committerEugen Wissner <belka@caraus.de>2026-08-28 02:39:42 +0200
commit8e655a0786aec5e0215e09f2a9629c6f32e34793 (patch)
tree0788634cf66fed9c1a1d264b5b8c30da49380559
parent4d4537866690a1ef3d882f5e9a6e01d8220a3650 (diff)
downloadelna-8e655a0786aec5e0215e09f2a9629c6f32e34793.tar.gz
Reject declarations shadowing imports
-rw-r--r--boot/dependency.cc28
-rw-r--r--boot/evaluator.cc75
-rw-r--r--boot/name_analysis.cc55
-rw-r--r--boot/result.cc16
-rw-r--r--boot/symbol.cc20
-rw-r--r--boot/validation.cc2
-rw-r--r--gcc/gcc/elna-diagnostic.cc25
-rw-r--r--gcc/gcc/elna-module-loader.cc15
-rw-r--r--include/elna/boot/ast.h4
-rw-r--r--include/elna/boot/dependency.h140
-rw-r--r--include/elna/boot/evaluator.h2
-rw-r--r--include/elna/boot/name_analysis.h27
-rw-r--r--include/elna/boot/result.h47
-rw-r--r--include/elna/boot/symbol.h5
-rw-r--r--include/elna/boot/validation.h2
-rw-r--r--include/elna/gcc/elna-diagnostic.h14
-rw-r--r--include/elna/gcc/elna-module-loader.h8
-rw-r--r--rakelib/gcc.rake20
-rw-r--r--source/main.elna5
-rw-r--r--testsuite/fail_compilation/import_local_collision/helper.elna6
-rw-r--r--testsuite/fail_compilation/import_local_collision/sut.elna7
-rw-r--r--testsuite/fail_compilation/record_duplicate_field.elna8
-rw-r--r--testsuite/fail_compilation/record_duplicate_field_import_base/helper.elna7
-rw-r--r--testsuite/fail_compilation/record_duplicate_field_import_base/sut.elna10
-rw-r--r--testsuite/fail_compilation/redefined_variable.elna6
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.