From 491e62664394a8689a3f8904fbe8dbc3dea05524 Mon Sep 17 00:00:00 2001 From: Eugen Wissner Date: Sun, 30 Aug 2026 02:08:45 +0200 Subject: Fix ICEs with recursive type declarations --- boot/name_analysis.cc | 142 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 141 insertions(+), 1 deletion(-) (limited to 'boot/name_analysis.cc') diff --git a/boot/name_analysis.cc b/boot/name_analysis.cc index cf90e3d..620a32f 100644 --- a/boot/name_analysis.cc +++ b/boot/name_analysis.cc @@ -70,6 +70,25 @@ namespace elna::boot } } + cyclic_declaration_error::cyclic_declaration_error(const source_position position, + const std::vector& cycle) + : diagnostic(position), cycle(cycle) + { + } + + std::string cyclic_declaration_error::what() const + { + auto segment = std::cbegin(this->cycle); + std::string message = "Type declaration forms a cycle: " + *segment; + + ++segment; + for (; segment != std::cend(this->cycle); ++segment) + { + message += " -> " + *segment; + } + return message + " -> " + this->cycle.front(); + } + const_qualifier_error::const_qualifier_error(const source_position position, payload_type payload) : diagnostic(position), payload(std::move(payload)) { @@ -436,12 +455,21 @@ namespace elna::boot const_qualifier_error::kind::array_position); } expression->dimensions().accept(this); + if (expression->dimensions().type_decoration.empty()) + { + // The dimension expression failed to resolve and reported its own + // error already. + this->current_type = type(); + return; + } const auto size_constant = this->constant_evaluator.evaluate_index(expression->dimensions()); if (!size_constant.has_value()) { add_error(expression->position(), non_constant_expression_error::array_dimensions{ array_base }); + this->current_type = type(); + return; } this->current_type = type(std::make_shared(array_base, size_constant.value())); } @@ -977,6 +1005,107 @@ namespace elna::boot { } + std::optional> declaration_visitor::find_alias_cycle( + const std::shared_ptr& being_resolved, const type& referent) + { + std::vector alias_path; + + return find_alias_cycle(being_resolved, referent, alias_path, walk_state{}) + ? std::nullopt : std::make_optional(alias_path); + } + + bool declaration_visitor::find_alias_cycle(const std::shared_ptr& being_resolved, + const type& referent, std::vector& alias_path, walk_state state) + { + /* + * A recursive type is legal only if the cycle passes through a record, + * which materializes the recursion, and the occurrence closing the + * cycle is behind a pointer or a slice, so that the record layout + * stays finite. All other cycles are rejected. + */ + if (auto link = referent.get()) + { + if (std::ranges::find(alias_path, link->name) != std::cend(alias_path)) + { + // A legal cycle already checked on this path. + return true; + } + alias_path.push_back(link->name); + if (being_resolved == link) + { + return state.record_seen && state.guarded; + } + const bool acyclic = find_alias_cycle(being_resolved, link->referent, alias_path, state); + if (acyclic) + { + alias_path.pop_back(); + } + return acyclic; + } + else if (auto link = referent.get()) + { + return find_alias_cycle(being_resolved, link->unqualified, alias_path, state); + } + else if (auto link = referent.get()) + { + walk_state guarded_state = state; + guarded_state.guarded = true; + return find_alias_cycle(being_resolved, link->base, alias_path, guarded_state); + } + else if (auto link = referent.get()) + { + walk_state guarded_state = state; + guarded_state.guarded = true; + return find_alias_cycle(being_resolved, link->base, alias_path, guarded_state); + } + else if (auto link = referent.get()) + { + return find_alias_cycle(being_resolved, link->base, alias_path, state); + } + else if (auto link = referent.get()) + { + const std::size_t saved_path = alias_path.size(); + + for (const type& parameter : link->parameters) + { + alias_path.resize(saved_path); + if (!find_alias_cycle(being_resolved, parameter, alias_path, state)) + { + return false; + } + } + if (!link->return_type.proper_type.empty()) + { + alias_path.resize(saved_path); + return find_alias_cycle(being_resolved, link->return_type.proper_type, alias_path, state); + } + return true; + } + else if (auto link = referent.get()) + { + const std::size_t saved_path = alias_path.size(); + walk_state in_record = state; + in_record.record_seen = true; + in_record.guarded = false; + + if (!link->base.empty() + && !find_alias_cycle(being_resolved, link->base, alias_path, in_record)) + { + return false; + } + for (const auto& [field_name, field_type] : link->fields) + { + alias_path.resize(saved_path); + if (!find_alias_cycle(being_resolved, field_type, alias_path, in_record)) + { + return false; + } + } + return true; + } + return true; + } + void declaration_visitor::visit(unit *unit) { for (type_declaration *const type : unit->types) @@ -996,7 +1125,18 @@ namespace elna::boot void declaration_visitor::visit(type_declaration *declaration) { declaration->underlying_type().accept(this); - auto resolved = this->bag.resolve(declaration->identifier.name(), this->current_type); + type underlying = this->current_type; + + // Reject the cycle and wire an empty referent: the declaration still + // resolves and is entered, so its uses degrade silently. + if (auto cycle = find_alias_cycle( + this->bag.declared(declaration->identifier.name()), underlying)) + { + add_error(declaration->position(), *cycle); + underlying = type(); + } + const std::shared_ptr resolved = + this->bag.resolve(declaration->identifier.name(), underlying); auto info = std::make_shared(type(resolved)); info->exported = declaration->identifier.exported(); -- cgit v1.2.3