Rework name lookup to handle non-lexical scoping. (#3354)

When declaring a name such as `fn Ns.Class.F() { ... }`, enter the
scopes of `Ns` and `Ns.Class` as we form the name, and remain in those
non-lexical scopes until the end of the declaration.

When performing an unqualified lookup, look in any enclosing non-lexical
scopes in addition to looking into the lexical name table.

We now track a scope index with each lookup result in the lexical name
lookup table. This is used to determine whether a lexical or non-lexcial
result is the innermost result and whether a declared name is in the
same scope as some previous introduction of that name or in a nested
scope. For now, this could just be the index into the scope_stack, but
the intent is to also use this to detect names being declared after they
are first looked up, which requires the indexes to outlive their scopes,
so we use a persistent numbering of all scopes instead. The persistent
numbering also permits more invariant checking.
This commit is contained in:
Richard Smith
2023-11-06 19:48:41 +00:00
committed by GitHub
parent a4c0febc0f
commit 3bee8932a9
18 changed files with 766 additions and 150 deletions
+130 -24
View File
@@ -115,52 +115,144 @@ auto Context::NoteIncompleteClass(SemIR::ClassId class_id,
auto Context::AddNameToLookup(Parse::Node name_node, IdentifierId name_id,
SemIR::InstId target_id) -> void {
if (current_scope().names.insert(name_id).second) {
name_lookup_[name_id].push_back(target_id);
// TODO: Reject if we previously performed a failed lookup for this name in
// this scope or a scope nested within it.
auto& lexical_results = name_lookup_[name_id];
CARBON_CHECK(lexical_results.empty() ||
lexical_results.back().scope_index < current_scope_index())
<< "Failed to clean up after scope nested within the current scope";
lexical_results.push_back(
{.node_id = target_id, .scope_index = current_scope_index()});
} else {
DiagnoseDuplicateName(name_node, name_lookup_[name_id].back());
DiagnoseDuplicateName(name_node, name_lookup_[name_id].back().node_id);
}
}
auto Context::LookupName(Parse::Node parse_node, IdentifierId name_id,
SemIR::NameScopeId scope_id, bool print_diagnostics)
auto Context::LookupNameInDeclaration(Parse::Node parse_node,
IdentifierId name_id,
SemIR::NameScopeId scope_id)
-> SemIR::InstId {
if (scope_id == SemIR::NameScopeId::Invalid) {
auto it = name_lookup_.find(name_id);
if (it == name_lookup_.end()) {
if (print_diagnostics) {
DiagnoseNameNotFound(parse_node, name_id);
// Look for a name in the current scope only. There are two cases where the
// name would be in an outer scope:
//
// - The name is the sole component of the declared name:
//
// class A;
// fn F() {
// class A;
// }
//
// In this case, the inner A is not the same class as the outer A, so
// lookup should not find the outer A.
//
// - The name is a qualifier of some larger declared name:
//
// class A { class B; }
// fn F() {
// class A.B {}
// }
//
// In this case, we're not in the correct scope to define a member of
// class A, so we should reject, and we achieve this by not finding the
// name A from the outer scope.
if (auto name_it = name_lookup_.find(name_id);
name_it != name_lookup_.end()) {
CARBON_CHECK(!name_it->second.empty())
<< "Should have been erased: " << identifiers().Get(name_id);
auto result = name_it->second.back();
if (result.scope_index == current_scope_index()) {
return result.node_id;
}
return SemIR::InstId::BuiltinError;
}
CARBON_CHECK(!it->second.empty())
<< "Should have been erased: " << identifiers().Get(name_id);
// TODO: Check for ambiguous lookups.
return it->second.back();
return SemIR::InstId::Invalid;
} else {
const auto& scope = name_scopes().Get(scope_id);
auto it = scope.find(name_id);
if (it == scope.end()) {
if (print_diagnostics) {
DiagnoseNameNotFound(parse_node, name_id);
}
return SemIR::InstId::BuiltinError;
// TODO: Once we support `extend`, do not look into `extend`ed scopes here,
// following the same logic as above.
return LookupQualifiedName(parse_node, name_id, scope_id,
/*required=*/false);
}
}
auto Context::LookupUnqualifiedName(Parse::Node parse_node,
IdentifierId name_id) -> SemIR::InstId {
// TODO: Check for shadowed lookup results.
// Find the results from enclosing lexical scopes. These will be combined with
// results from non-lexical scopes such as namespaces and classes.
llvm::ArrayRef<LexicalLookupResult> lexical_results;
if (auto name_it = name_lookup_.find(name_id);
name_it != name_lookup_.end()) {
lexical_results = name_it->second;
CARBON_CHECK(!lexical_results.empty())
<< "Should have been erased: " << identifiers().Get(name_id);
}
// Walk the non-lexical scopes and perform lookups into each of them.
for (auto [index, name_scope_id] : llvm::reverse(non_lexical_scope_stack_)) {
// If the innermost lexical result is within this non-lexical scope, then
// it shadows all further non-lexical results and we're done.
if (!lexical_results.empty() &&
lexical_results.back().scope_index > index) {
return lexical_results.back().node_id;
}
return it->second;
auto non_lexical_result =
LookupQualifiedName(parse_node, name_id, name_scope_id,
/*required=*/false);
if (non_lexical_result.is_valid()) {
return non_lexical_result;
}
}
if (!lexical_results.empty()) {
return lexical_results.back().node_id;
}
// We didn't find anything at all.
DiagnoseNameNotFound(parse_node, name_id);
return SemIR::InstId::BuiltinError;
}
auto Context::LookupQualifiedName(Parse::Node parse_node, IdentifierId name_id,
SemIR::NameScopeId scope_id, bool required)
-> SemIR::InstId {
CARBON_CHECK(scope_id.is_valid()) << "No scope to perform lookup into";
const auto& scope = name_scopes().Get(scope_id);
auto it = scope.find(name_id);
if (it == scope.end()) {
// TODO: Also perform lookups into `extend`ed scopes.
if (required) {
DiagnoseNameNotFound(parse_node, name_id);
return SemIR::InstId::BuiltinError;
}
return SemIR::InstId::Invalid;
}
return it->second;
}
auto Context::PushScope(SemIR::InstId scope_inst_id,
SemIR::NameScopeId scope_id) -> void {
scope_stack_.push_back(
{.scope_inst_id = scope_inst_id, .scope_id = scope_id});
scope_stack_.push_back({.index = next_scope_index_,
.scope_inst_id = scope_inst_id,
.scope_id = scope_id});
if (scope_id.is_valid()) {
non_lexical_scope_stack_.push_back({next_scope_index_, scope_id});
}
// TODO: Handle this case more gracefully.
CARBON_CHECK(next_scope_index_.index != std::numeric_limits<int32_t>::max())
<< "Ran out of scopes";
++next_scope_index_.index;
}
auto Context::PopScope() -> void {
auto scope = scope_stack_.pop_back_val();
for (const auto& str_id : scope.names) {
auto it = name_lookup_.find(str_id);
CARBON_CHECK(it->second.back().scope_index == scope.index)
<< "Inconsistent scope index for name " << identifiers().Get(str_id);
if (it->second.size() == 1) {
// Erase names that no longer resolve.
name_lookup_.erase(it);
@@ -168,6 +260,20 @@ auto Context::PopScope() -> void {
it->second.pop_back();
}
}
if (scope.scope_id.is_valid()) {
CARBON_CHECK(non_lexical_scope_stack_.back().first == scope.index);
non_lexical_scope_stack_.pop_back();
}
}
auto Context::PopToScope(ScopeIndex index) -> void {
while (current_scope_index() > index) {
PopScope();
}
CARBON_CHECK(current_scope_index() == index)
<< "Scope index " << index << " does not enclose the current scope "
<< current_scope_index();
}
auto Context::FollowNameReferences(SemIR::InstId inst_id) -> SemIR::InstId {