Kaynağa Gözat

Refactor name lookup logic out of Context (#4930)

This is a pretty straight move of name lookup functionality to
name_lookup.*
Jon Ross-Perkins 1 yıl önce
ebeveyn
işleme
afef6cd940

+ 2 - 0
toolchain/check/BUILD

@@ -36,6 +36,7 @@ cc_library(
         "merge.cpp",
         "modifiers.cpp",
         "name_component.cpp",
+        "name_lookup.cpp",
         "operator.cpp",
         "pattern_match.cpp",
         "return.cpp",
@@ -68,6 +69,7 @@ cc_library(
         "merge.h",
         "modifiers.h",
         "name_component.h",
+        "name_lookup.h",
         "operator.h",
         "param_and_arg_refs_stack.h",
         "pattern_match.h",

+ 0 - 463
toolchain/check/context.cpp

@@ -242,28 +242,6 @@ auto Context::DiagnoseNameNotFound(SemIRLoc loc, SemIR::NameId name_id)
   emitter_->Emit(loc, NameNotFound, name_id);
 }
 
-auto Context::DiagnoseMemberNameNotFound(
-    SemIRLoc loc, SemIR::NameId name_id,
-    llvm::ArrayRef<LookupScope> lookup_scopes) -> void {
-  if (lookup_scopes.size() == 1 &&
-      lookup_scopes.front().name_scope_id.has_value()) {
-    auto specific_id = lookup_scopes.front().specific_id;
-    auto scope_inst_id =
-        specific_id.has_value()
-            ? GetInstForSpecific(*this, specific_id)
-            : name_scopes().Get(lookup_scopes.front().name_scope_id).inst_id();
-    CARBON_DIAGNOSTIC(MemberNameNotFoundInScope, Error,
-                      "member name `{0}` not found in {1}", SemIR::NameId,
-                      InstIdAsType);
-    emitter_->Emit(loc, MemberNameNotFoundInScope, name_id, scope_inst_id);
-    return;
-  }
-
-  CARBON_DIAGNOSTIC(MemberNameNotFound, Error, "member name `{0}` not found",
-                    SemIR::NameId);
-  emitter_->Emit(loc, MemberNameNotFound, name_id);
-}
-
 auto Context::NoteAbstractClass(SemIR::ClassId class_id,
                                 DiagnosticBuilder& builder) -> void {
   const auto& class_info = classes().Get(class_id);
@@ -306,447 +284,6 @@ auto Context::NoteUndefinedInterface(SemIR::InterfaceId interface_id,
   }
 }
 
-auto Context::AddNameToLookup(SemIR::NameId name_id, SemIR::InstId target_id,
-                              ScopeIndex scope_index) -> void {
-  if (auto existing =
-          scope_stack().LookupOrAddName(name_id, target_id, scope_index);
-      existing.has_value()) {
-    DiagnoseDuplicateName(target_id, existing);
-  }
-}
-
-auto Context::LookupNameInDecl(SemIR::LocId loc_id, SemIR::NameId name_id,
-                               SemIR::NameScopeId scope_id,
-                               ScopeIndex scope_index)
-    -> SemIR::ScopeLookupResult {
-  if (!scope_id.has_value()) {
-    // Look for a name in the specified scope or a scope nested within it 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.
-    //
-    // There is also one case where the name would be in an inner scope:
-    //
-    //  - The name is redeclared by a parameter of the same entity:
-    //
-    //    fn F() {
-    //      class C(C:! type);
-    //    }
-    //
-    //    In this case, the class C is not a redeclaration of its parameter, but
-    //    we find the parameter in order to diagnose a redeclaration error.
-    return SemIR::ScopeLookupResult::MakeWrappedLookupResult(
-        scope_stack().LookupInLexicalScopesWithin(name_id, scope_index),
-        SemIR::AccessKind::Public);
-  } else {
-    // We do not look into `extend`ed scopes here. A qualified name in a
-    // declaration must specify the exact scope in which the name was originally
-    // introduced:
-    //
-    //    base class A { fn F(); }
-    //    class B { extend base: A; }
-    //
-    //    // Error, no `F` in `B`.
-    //    fn B.F() {}
-    return LookupNameInExactScope(loc_id, name_id, scope_id,
-                                  name_scopes().Get(scope_id),
-                                  /*is_being_declared=*/true);
-  }
-}
-
-auto Context::LookupUnqualifiedName(Parse::NodeId node_id,
-                                    SemIR::NameId name_id, bool required)
-    -> LookupResult {
-  // TODO: Check for shadowed lookup results.
-
-  // Find the results from ancestor lexical scopes. These will be combined with
-  // results from non-lexical scopes such as namespaces and classes.
-  auto [lexical_result, non_lexical_scopes] =
-      scope_stack().LookupInLexicalScopes(name_id);
-
-  // Walk the non-lexical scopes and perform lookups into each of them.
-  for (auto [index, lookup_scope_id, specific_id] :
-       llvm::reverse(non_lexical_scopes)) {
-    if (auto non_lexical_result =
-            LookupQualifiedName(node_id, name_id,
-                                LookupScope{.name_scope_id = lookup_scope_id,
-                                            .specific_id = specific_id},
-                                /*required=*/false);
-        non_lexical_result.scope_result.is_found()) {
-      return non_lexical_result;
-    }
-  }
-
-  if (lexical_result == SemIR::InstId::InitTombstone) {
-    CARBON_DIAGNOSTIC(UsedBeforeInitialization, Error,
-                      "`{0}` used before initialization", SemIR::NameId);
-    emitter_->Emit(node_id, UsedBeforeInitialization, name_id);
-    return {.specific_id = SemIR::SpecificId::None,
-            .scope_result = SemIR::ScopeLookupResult::MakeError()};
-  }
-
-  if (lexical_result.has_value()) {
-    // A lexical scope never needs an associated specific. If there's a
-    // lexically enclosing generic, then it also encloses the point of use of
-    // the name.
-    return {.specific_id = SemIR::SpecificId::None,
-            .scope_result = SemIR::ScopeLookupResult::MakeFound(
-                lexical_result, SemIR::AccessKind::Public)};
-  }
-
-  // We didn't find anything at all.
-  if (required) {
-    DiagnoseNameNotFound(node_id, name_id);
-  }
-
-  return {.specific_id = SemIR::SpecificId::None,
-          .scope_result = SemIR::ScopeLookupResult::MakeError()};
-}
-
-auto Context::LookupNameInExactScope(SemIR::LocId loc_id, SemIR::NameId name_id,
-                                     SemIR::NameScopeId scope_id,
-                                     SemIR::NameScope& scope,
-                                     bool is_being_declared)
-    -> SemIR::ScopeLookupResult {
-  if (auto entry_id = is_being_declared
-                          ? scope.Lookup(name_id)
-                          : scope.LookupOrPoison(loc_id, name_id)) {
-    auto lookup_result = scope.GetEntry(*entry_id).result;
-    if (!lookup_result.is_poisoned()) {
-      LoadImportRef(*this, lookup_result.target_inst_id());
-    }
-    return lookup_result;
-  }
-
-  if (!scope.import_ir_scopes().empty()) {
-    // TODO: Enforce other access modifiers for imports.
-    return SemIR::ScopeLookupResult::MakeWrappedLookupResult(
-        ImportNameFromOtherPackage(*this, loc_id, scope_id,
-                                   scope.import_ir_scopes(), name_id),
-        SemIR::AccessKind::Public);
-  }
-  return SemIR::ScopeLookupResult::MakeNotFound();
-}
-
-// Prints diagnostics on invalid qualified name access.
-static auto DiagnoseInvalidQualifiedNameAccess(Context& context, SemIRLoc loc,
-                                               SemIR::InstId scope_result_id,
-                                               SemIR::NameId name_id,
-                                               SemIR::AccessKind access_kind,
-                                               bool is_parent_access,
-                                               AccessInfo access_info) -> void {
-  auto class_type = context.insts().TryGetAs<SemIR::ClassType>(
-      context.constant_values().GetInstId(access_info.constant_id));
-  if (!class_type) {
-    return;
-  }
-
-  // TODO: Support scoped entities other than just classes.
-  const auto& class_info = context.classes().Get(class_type->class_id);
-
-  auto parent_type_id = class_info.self_type_id;
-
-  if (access_kind == SemIR::AccessKind::Private && is_parent_access) {
-    if (auto base_type_id =
-            class_info.GetBaseType(context.sem_ir(), class_type->specific_id);
-        base_type_id.has_value()) {
-      parent_type_id = base_type_id;
-    } else if (auto adapted_type_id = class_info.GetAdaptedType(
-                   context.sem_ir(), class_type->specific_id);
-               adapted_type_id.has_value()) {
-      parent_type_id = adapted_type_id;
-    } else {
-      CARBON_FATAL("Expected parent for parent access");
-    }
-  }
-
-  CARBON_DIAGNOSTIC(
-      ClassInvalidMemberAccess, Error,
-      "cannot access {0:private|protected} member `{1}` of type {2}",
-      BoolAsSelect, SemIR::NameId, SemIR::TypeId);
-  CARBON_DIAGNOSTIC(ClassMemberDeclaration, Note, "declared here");
-  context.emitter()
-      .Build(loc, ClassInvalidMemberAccess,
-             access_kind == SemIR::AccessKind::Private, name_id, parent_type_id)
-      .Note(scope_result_id, ClassMemberDeclaration)
-      .Emit();
-}
-
-// Returns whether the access is prohibited by the access modifiers.
-static auto IsAccessProhibited(std::optional<AccessInfo> access_info,
-                               SemIR::AccessKind access_kind,
-                               bool is_parent_access) -> bool {
-  if (!access_info) {
-    return false;
-  }
-
-  switch (access_kind) {
-    case SemIR::AccessKind::Public:
-      return false;
-    case SemIR::AccessKind::Protected:
-      return access_info->highest_allowed_access == SemIR::AccessKind::Public;
-    case SemIR::AccessKind::Private:
-      return access_info->highest_allowed_access !=
-                 SemIR::AccessKind::Private ||
-             is_parent_access;
-  }
-}
-
-// Information regarding a prohibited access.
-struct ProhibitedAccessInfo {
-  // The resulting inst of the lookup.
-  SemIR::InstId scope_result_id;
-  // The access kind of the lookup.
-  SemIR::AccessKind access_kind;
-  // If the lookup is from an extended scope. For example, if this is a base
-  // class member access from a class that extends it.
-  bool is_parent_access;
-};
-
-auto Context::AppendLookupScopesForConstant(
-    SemIR::LocId loc_id, SemIR::ConstantId base_const_id,
-    llvm::SmallVector<LookupScope>* scopes) -> bool {
-  auto base_id = constant_values().GetInstId(base_const_id);
-  auto base = insts().Get(base_id);
-  if (auto base_as_namespace = base.TryAs<SemIR::Namespace>()) {
-    scopes->push_back(
-        LookupScope{.name_scope_id = base_as_namespace->name_scope_id,
-                    .specific_id = SemIR::SpecificId::None});
-    return true;
-  }
-  if (auto base_as_class = base.TryAs<SemIR::ClassType>()) {
-    RequireDefinedType(
-        *this, GetTypeIdForTypeConstant(base_const_id), loc_id, [&] {
-          CARBON_DIAGNOSTIC(QualifiedExprInIncompleteClassScope, Error,
-                            "member access into incomplete class {0}",
-                            InstIdAsType);
-          return emitter().Build(loc_id, QualifiedExprInIncompleteClassScope,
-                                 base_id);
-        });
-    auto& class_info = classes().Get(base_as_class->class_id);
-    scopes->push_back(LookupScope{.name_scope_id = class_info.scope_id,
-                                  .specific_id = base_as_class->specific_id});
-    return true;
-  }
-  if (auto base_as_facet_type = base.TryAs<SemIR::FacetType>()) {
-    RequireDefinedType(
-        *this, GetTypeIdForTypeConstant(base_const_id), loc_id, [&] {
-          CARBON_DIAGNOSTIC(QualifiedExprInUndefinedInterfaceScope, Error,
-                            "member access into undefined interface {0}",
-                            InstIdAsType);
-          return emitter().Build(loc_id, QualifiedExprInUndefinedInterfaceScope,
-                                 base_id);
-        });
-    const auto& facet_type_info =
-        facet_types().Get(base_as_facet_type->facet_type_id);
-    for (auto interface : facet_type_info.impls_constraints) {
-      auto& interface_info = interfaces().Get(interface.interface_id);
-      scopes->push_back(LookupScope{.name_scope_id = interface_info.scope_id,
-                                    .specific_id = interface.specific_id});
-    }
-    return true;
-  }
-  if (base_const_id == SemIR::ErrorInst::SingletonConstantId) {
-    // Lookup into this scope should fail without producing an error.
-    scopes->push_back(LookupScope{.name_scope_id = SemIR::NameScopeId::None,
-                                  .specific_id = SemIR::SpecificId::None});
-    return true;
-  }
-  // TODO: Per the design, if `base_id` is any kind of type, then lookup should
-  // treat it as a name scope, even if it doesn't have members. For example,
-  // `(i32*).X` should fail because there's no name `X` in `i32*`, not because
-  // there's no name `X` in `type`.
-  return false;
-}
-
-auto Context::LookupQualifiedName(SemIR::LocId loc_id, SemIR::NameId name_id,
-                                  llvm::ArrayRef<LookupScope> lookup_scopes,
-                                  bool required,
-                                  std::optional<AccessInfo> access_info)
-    -> LookupResult {
-  llvm::SmallVector<LookupScope> scopes(lookup_scopes);
-
-  // TODO: Support reporting of multiple prohibited access.
-  llvm::SmallVector<ProhibitedAccessInfo> prohibited_accesses;
-
-  LookupResult result = {
-      .specific_id = SemIR::SpecificId::None,
-      .scope_result = SemIR::ScopeLookupResult::MakeNotFound()};
-  bool has_error = false;
-  bool is_parent_access = false;
-
-  // Walk this scope and, if nothing is found here, the scopes it extends.
-  while (!scopes.empty()) {
-    auto [scope_id, specific_id] = scopes.pop_back_val();
-    if (!scope_id.has_value()) {
-      has_error = true;
-      continue;
-    }
-    auto& name_scope = name_scopes().Get(scope_id);
-    has_error |= name_scope.has_error();
-
-    const SemIR::ScopeLookupResult scope_result =
-        LookupNameInExactScope(loc_id, name_id, scope_id, name_scope);
-    SemIR::AccessKind access_kind = scope_result.access_kind();
-
-    auto is_access_prohibited =
-        IsAccessProhibited(access_info, access_kind, is_parent_access);
-
-    // Keep track of prohibited accesses, this will be useful for reporting
-    // multiple prohibited accesses if we can't find a suitable lookup.
-    if (is_access_prohibited) {
-      prohibited_accesses.push_back({
-          .scope_result_id = scope_result.target_inst_id(),
-          .access_kind = access_kind,
-          .is_parent_access = is_parent_access,
-      });
-    }
-
-    if (!scope_result.is_found() || is_access_prohibited) {
-      // If nothing is found in this scope or if we encountered an invalid
-      // access, look in its extended scopes.
-      const auto& extended = name_scope.extended_scopes();
-      scopes.reserve(scopes.size() + extended.size());
-      for (auto extended_id : llvm::reverse(extended)) {
-        // Substitute into the constant describing the extended scope to
-        // determine its corresponding specific.
-        CARBON_CHECK(extended_id.has_value());
-        LoadImportRef(*this, extended_id);
-        SemIR::ConstantId const_id =
-            GetConstantValueInSpecific(sem_ir(), specific_id, extended_id);
-
-        DiagnosticAnnotationScope annotate_diagnostics(
-            &emitter(), [&](auto& builder) {
-              CARBON_DIAGNOSTIC(FromExtendHere, Note,
-                                "declared as an extended scope here");
-              builder.Note(extended_id, FromExtendHere);
-            });
-        if (!AppendLookupScopesForConstant(loc_id, const_id, &scopes)) {
-          // TODO: Handle case where we have a symbolic type and instead should
-          // look in its type.
-        }
-      }
-      is_parent_access |= !extended.empty();
-      continue;
-    }
-
-    // If this is our second lookup result, diagnose an ambiguity.
-    if (result.scope_result.is_found()) {
-      CARBON_DIAGNOSTIC(
-          NameAmbiguousDueToExtend, Error,
-          "ambiguous use of name `{0}` found in multiple extended scopes",
-          SemIR::NameId);
-      emitter_->Emit(loc_id, NameAmbiguousDueToExtend, name_id);
-      // TODO: Add notes pointing to the scopes.
-      return {.specific_id = SemIR::SpecificId::None,
-              .scope_result = SemIR::ScopeLookupResult::MakeError()};
-    }
-
-    result.scope_result = scope_result;
-    result.specific_id = specific_id;
-  }
-
-  if (required && !result.scope_result.is_found()) {
-    if (!has_error) {
-      if (prohibited_accesses.empty()) {
-        DiagnoseMemberNameNotFound(loc_id, name_id, lookup_scopes);
-      } else {
-        //  TODO: We should report multiple prohibited accesses in case we don't
-        //  find a valid lookup. Reporting the last one should suffice for now.
-        auto [scope_result_id, access_kind, is_parent_access] =
-            prohibited_accesses.back();
-
-        // Note, `access_info` is guaranteed to have a value here, since
-        // `prohibited_accesses` is non-empty.
-        DiagnoseInvalidQualifiedNameAccess(*this, loc_id, scope_result_id,
-                                           name_id, access_kind,
-                                           is_parent_access, *access_info);
-      }
-    }
-
-    CARBON_CHECK(!result.scope_result.is_poisoned());
-    return {.specific_id = SemIR::SpecificId::None,
-            .scope_result = SemIR::ScopeLookupResult::MakeError()};
-  }
-
-  return result;
-}
-
-// Returns the scope of the Core package, or `None` if it's not found.
-//
-// TODO: Consider tracking the Core package in SemIR so we don't need to use
-// name lookup to find it.
-static auto GetCorePackage(Context& context, SemIR::LocId loc_id,
-                           llvm::StringRef name) -> SemIR::NameScopeId {
-  auto packaging = context.parse_tree().packaging_decl();
-  if (packaging && packaging->names.package_id == PackageNameId::Core) {
-    return SemIR::NameScopeId::Package;
-  }
-  auto core_name_id = SemIR::NameId::Core;
-
-  // Look up `package.Core`.
-  auto core_scope_result = context.LookupNameInExactScope(
-      loc_id, core_name_id, SemIR::NameScopeId::Package,
-      context.name_scopes().Get(SemIR::NameScopeId::Package));
-  if (core_scope_result.is_found()) {
-    // We expect it to be a namespace.
-    if (auto namespace_inst = context.insts().TryGetAs<SemIR::Namespace>(
-            core_scope_result.target_inst_id())) {
-      // TODO: Decide whether to allow the case where `Core` is not a package.
-      return namespace_inst->name_scope_id;
-    }
-  }
-
-  CARBON_DIAGNOSTIC(
-      CoreNotFound, Error,
-      "`Core.{0}` implicitly referenced here, but package `Core` not found",
-      std::string);
-  context.emitter().Emit(loc_id, CoreNotFound, name.str());
-  return SemIR::NameScopeId::None;
-}
-
-auto Context::LookupNameInCore(SemIR::LocId loc_id, llvm::StringRef name)
-    -> SemIR::InstId {
-  auto core_package_id = GetCorePackage(*this, loc_id, name);
-  if (!core_package_id.has_value()) {
-    return SemIR::ErrorInst::SingletonInstId;
-  }
-
-  auto name_id = SemIR::NameId::ForIdentifier(identifiers().Add(name));
-  auto scope_result = LookupNameInExactScope(
-      loc_id, name_id, core_package_id, name_scopes().Get(core_package_id));
-  if (!scope_result.is_found()) {
-    CARBON_DIAGNOSTIC(
-        CoreNameNotFound, Error,
-        "name `Core.{0}` implicitly referenced here, but not found",
-        SemIR::NameId);
-    emitter_->Emit(loc_id, CoreNameNotFound, name_id);
-    return SemIR::ErrorInst::SingletonInstId;
-  }
-
-  // Look through import_refs and aliases.
-  return constant_values().GetConstantInstId(scope_result.target_inst_id());
-}
-
 auto Context::Finalize() -> void {
   // Pop information for the file-level scope.
   sem_ir().set_top_inst_block_id(inst_block_stack().Pop());

+ 0 - 87
toolchain/check/context.h

@@ -32,35 +32,6 @@
 
 namespace Carbon::Check {
 
-// Information about a scope in which we can perform name lookup.
-struct LookupScope {
-  // The name scope in which names are searched.
-  SemIR::NameScopeId name_scope_id;
-  // The specific for the name scope, or `None` if the name scope is not
-  // defined by a generic or we should perform lookup into the generic itself.
-  SemIR::SpecificId specific_id;
-};
-
-// A result produced by name lookup.
-struct LookupResult {
-  // The specific in which the lookup result was found. `None` if the result
-  // was not found in a specific.
-  SemIR::SpecificId specific_id;
-
-  // The result from the lookup in the scope.
-  SemIR::ScopeLookupResult scope_result;
-};
-
-// Information about an access.
-struct AccessInfo {
-  // The constant being accessed.
-  SemIR::ConstantId constant_id;
-
-  // The highest allowed access for a lookup. For example, `Protected` allows
-  // access to `Public` and `Protected` names, but not `Private`.
-  SemIR::AccessKind highest_allowed_access;
-};
-
 // Context and shared functionality for semantics handlers.
 class Context {
  public:
@@ -207,59 +178,6 @@ class Context {
     sem_ir().insts().SetLocId(inst_id, SemIR::LocId(node_id));
   }
 
-  // Adds a name to name lookup. Prints a diagnostic for name conflicts. If
-  // specified, `scope_index` specifies which lexical scope the name is inserted
-  // into, otherwise the name is inserted into the current scope.
-  auto AddNameToLookup(SemIR::NameId name_id, SemIR::InstId target_id,
-                       ScopeIndex scope_index = ScopeIndex::None) -> void;
-
-  // Performs name lookup in a specified scope for a name appearing in a
-  // declaration. If scope_id is `None`, performs lookup into the lexical scope
-  // specified by scope_index instead.
-  auto LookupNameInDecl(SemIR::LocId loc_id, SemIR::NameId name_id,
-                        SemIR::NameScopeId scope_id, ScopeIndex scope_index)
-      -> SemIR::ScopeLookupResult;
-
-  // Performs an unqualified name lookup, returning the referenced `InstId`.
-  auto LookupUnqualifiedName(Parse::NodeId node_id, SemIR::NameId name_id,
-                             bool required = true) -> LookupResult;
-
-  // Performs a name lookup in a specified scope, returning the referenced
-  // `InstId`. Does not look into extended scopes. Returns `InstId::None` if the
-  // name is not found.
-  //
-  // If `is_being_declared` is false, then this is a regular name lookup, and
-  // the name will be poisoned if not found so that later lookups will fail; a
-  // poisoned name will be treated as if it is not declared. Otherwise, this is
-  // a lookup for a name being declared, so the name will not be poisoned, but
-  // poison will be returned if it's already been looked up.
-  auto LookupNameInExactScope(SemIR::LocId loc_id, SemIR::NameId name_id,
-                              SemIR::NameScopeId scope_id,
-                              SemIR::NameScope& scope,
-                              bool is_being_declared = false)
-      -> SemIR::ScopeLookupResult;
-
-  // Appends the lookup scopes corresponding to `base_const_id` to `*scopes`.
-  // Returns `false` if not a scope. On invalid scopes, prints a diagnostic, but
-  // still updates `*scopes` and returns `true`.
-  auto AppendLookupScopesForConstant(SemIR::LocId loc_id,
-                                     SemIR::ConstantId base_const_id,
-                                     llvm::SmallVector<LookupScope>* scopes)
-      -> bool;
-
-  // Performs a qualified name lookup in a specified scopes and in scopes that
-  // they extend, returning the referenced `InstId`.
-  auto LookupQualifiedName(SemIR::LocId loc_id, SemIR::NameId name_id,
-                           llvm::ArrayRef<LookupScope> lookup_scopes,
-                           bool required = true,
-                           std::optional<AccessInfo> access_info = std::nullopt)
-      -> LookupResult;
-
-  // Returns the `InstId` corresponding to a name in the core package, or
-  // BuiltinErrorInst if not found.
-  auto LookupNameInCore(SemIR::LocId loc_id, llvm::StringRef name)
-      -> SemIR::InstId;
-
   // Prints a diagnostic for a duplicate name.
   auto DiagnoseDuplicateName(SemIRLoc dup_def, SemIRLoc prev_def) -> void;
 
@@ -270,11 +188,6 @@ class Context {
   // Prints a diagnostic for a missing name.
   auto DiagnoseNameNotFound(SemIRLoc loc, SemIR::NameId name_id) -> void;
 
-  // Prints a diagnostic for a missing qualified name.
-  auto DiagnoseMemberNameNotFound(SemIRLoc loc, SemIR::NameId name_id,
-                                  llvm::ArrayRef<LookupScope> lookup_scopes)
-      -> void;
-
   // Adds a note to a diagnostic explaining that a class is incomplete.
   auto NoteIncompleteClass(SemIR::ClassId class_id, DiagnosticBuilder& builder)
       -> void;

+ 6 - 5
toolchain/check/decl_name_stack.cpp

@@ -10,6 +10,7 @@
 #include "toolchain/check/generic.h"
 #include "toolchain/check/merge.h"
 #include "toolchain/check/name_component.h"
+#include "toolchain/check/name_lookup.h"
 #include "toolchain/diagnostics/diagnostic.h"
 #include "toolchain/sem_ir/ids.h"
 #include "toolchain/sem_ir/name_scope.h"
@@ -134,8 +135,8 @@ auto DeclNameStack::AddName(NameContext name_context, SemIR::InstId target_id,
 
     case NameContext::State::Unresolved:
       if (!name_context.parent_scope_id.has_value()) {
-        context_->AddNameToLookup(name_context.unresolved_name_id, target_id,
-                                  name_context.initial_scope_index);
+        AddNameToLookup(*context_, name_context.unresolved_name_id, target_id,
+                        name_context.initial_scope_index);
       } else {
         auto& name_scope =
             context_->name_scopes().Get(name_context.parent_scope_id);
@@ -264,9 +265,9 @@ auto DeclNameStack::ApplyAndLookupName(NameContext& name_context,
   }
 
   // For identifier nodes, we need to perform a lookup on the identifier.
-  auto lookup_result = context_->LookupNameInDecl(
-      name_context.loc_id, name_id, name_context.parent_scope_id,
-      name_context.initial_scope_index);
+  auto lookup_result = LookupNameInDecl(*context_, name_context.loc_id, name_id,
+                                        name_context.parent_scope_id,
+                                        name_context.initial_scope_index);
   if (lookup_result.is_poisoned()) {
     name_context.unresolved_name_id = name_id;
     name_context.poisoning_loc_id = lookup_result.poisoning_loc_id();

+ 2 - 1
toolchain/check/handle_binding_pattern.cpp

@@ -6,6 +6,7 @@
 #include "toolchain/check/convert.h"
 #include "toolchain/check/handle.h"
 #include "toolchain/check/interface.h"
+#include "toolchain/check/name_lookup.h"
 #include "toolchain/check/return.h"
 #include "toolchain/check/subpattern.h"
 #include "toolchain/check/type_completion.h"
@@ -226,7 +227,7 @@ static auto HandleAnyBindingPattern(Context& context, Parse::NodeId node_id,
           break;
       }
       if (had_error) {
-        context.AddNameToLookup(name_id, SemIR::ErrorInst::SingletonInstId);
+        AddNameToLookup(context, name_id, SemIR::ErrorInst::SingletonInstId);
         // Replace the parameter with `ErrorInst` so that we don't try
         // constructing a generic based on it.
         param_pattern_id = SemIR::ErrorInst::SingletonInstId;

+ 2 - 1
toolchain/check/handle_impl.cpp

@@ -10,6 +10,7 @@
 #include "toolchain/check/impl.h"
 #include "toolchain/check/merge.h"
 #include "toolchain/check/modifiers.h"
+#include "toolchain/check/name_lookup.h"
 #include "toolchain/check/pattern_match.h"
 #include "toolchain/parse/typed_nodes.h"
 #include "toolchain/sem_ir/generic.h"
@@ -65,7 +66,7 @@ auto HandleParseNode(Context& context, Parse::TypeImplAsId node_id) -> bool {
   // to the `NameScopeId` of the `impl`, because this happens before we enter
   // the `impl` scope or even identify which `impl` we're declaring.
   // TODO: Revisit this once #3714 is resolved.
-  context.AddNameToLookup(SemIR::NameId::SelfType, self_id);
+  AddNameToLookup(context, SemIR::NameId::SelfType, self_id);
   return true;
 }
 

+ 2 - 1
toolchain/check/handle_index.cpp

@@ -9,6 +9,7 @@
 #include "toolchain/check/convert.h"
 #include "toolchain/check/handle.h"
 #include "toolchain/check/literal.h"
+#include "toolchain/check/name_lookup.h"
 #include "toolchain/check/operator.h"
 #include "toolchain/diagnostics/diagnostic.h"
 #include "toolchain/sem_ir/inst.h"
@@ -31,7 +32,7 @@ auto HandleParseNode(Context& /*context*/, Parse::IndexExprStartId /*node_id*/)
 static auto GetIndexWithArgs(Context& context, Parse::NodeId node_id,
                              SemIR::TypeId self_id)
     -> std::optional<llvm::ArrayRef<SemIR::InstId>> {
-  auto index_with_inst_id = context.LookupNameInCore(node_id, "IndexWith");
+  auto index_with_inst_id = LookupNameInCore(context, node_id, "IndexWith");
   // If the `IndexWith` interface doesn't have generic arguments then return an
   // empty reference.
   if (context.insts().Is<SemIR::FacetType>(index_with_inst_id)) {

+ 3 - 2
toolchain/check/handle_literal.cpp

@@ -6,6 +6,7 @@
 #include "toolchain/check/context.h"
 #include "toolchain/check/handle.h"
 #include "toolchain/check/literal.h"
+#include "toolchain/check/name_lookup.h"
 #include "toolchain/diagnostics/format_providers.h"
 #include "toolchain/sem_ir/typed_insts.h"
 
@@ -92,7 +93,7 @@ auto HandleParseNode(Context& context, Parse::StringLiteralId node_id) -> bool {
 
 auto HandleParseNode(Context& context, Parse::BoolTypeLiteralId node_id)
     -> bool {
-  auto fn_inst_id = context.LookupNameInCore(node_id, "Bool");
+  auto fn_inst_id = LookupNameInCore(context, node_id, "Bool");
   auto type_inst_id = PerformCall(context, node_id, fn_inst_id, {});
   context.node_stack().Push(node_id, type_inst_id);
   return true;
@@ -143,7 +144,7 @@ auto HandleParseNode(Context& context, Parse::FloatTypeLiteralId node_id)
   auto tok_id = context.parse_tree().node_token(node_id);
   auto size_id = context.tokens().GetTypeLiteralSize(tok_id);
   auto width_id = MakeIntLiteral(context, node_id, size_id);
-  auto fn_inst_id = context.LookupNameInCore(node_id, "Float");
+  auto fn_inst_id = LookupNameInCore(context, node_id, "Float");
   auto type_inst_id = PerformCall(context, node_id, fn_inst_id, {width_id});
   context.node_stack().Push(node_id, type_inst_id);
   return true;

+ 2 - 1
toolchain/check/handle_name.cpp

@@ -7,6 +7,7 @@
 #include "toolchain/check/handle.h"
 #include "toolchain/check/member_access.h"
 #include "toolchain/check/name_component.h"
+#include "toolchain/check/name_lookup.h"
 #include "toolchain/check/pointer_dereference.h"
 #include "toolchain/lex/token_kind.h"
 #include "toolchain/sem_ir/inst.h"
@@ -102,7 +103,7 @@ static auto GetIdentifierAsName(Context& context, Parse::NodeId node_id)
 // lookup.
 static auto HandleNameAsExpr(Context& context, Parse::NodeId node_id,
                              SemIR::NameId name_id) -> SemIR::InstId {
-  auto result = context.LookupUnqualifiedName(node_id, name_id);
+  auto result = LookupUnqualifiedName(context, node_id, name_id);
   SemIR::InstId inst_id = result.scope_result.target_inst_id();
   auto value = context.insts().Get(inst_id);
   auto type_id = SemIR::GetTypeInSpecific(context.sem_ir(), result.specific_id,

+ 4 - 3
toolchain/check/impl.cpp

@@ -12,6 +12,7 @@
 #include "toolchain/check/generic.h"
 #include "toolchain/check/import_ref.h"
 #include "toolchain/check/interface.h"
+#include "toolchain/check/name_lookup.h"
 #include "toolchain/check/type_completion.h"
 #include "toolchain/diagnostics/diagnostic_emitter.h"
 #include "toolchain/sem_ir/generic.h"
@@ -377,9 +378,9 @@ auto FinishImplWitness(Context& context, SemIR::Impl& impl) -> void {
           CARBON_FATAL("Unexpected type: {0}", type_inst);
         }
         auto& fn = context.functions().Get(fn_type->function_id);
-        auto lookup_result = context.LookupNameInExactScope(
-            context.insts().GetLocId(decl_id), fn.name_id, impl.scope_id,
-            impl_scope);
+        auto lookup_result =
+            LookupNameInExactScope(context, context.insts().GetLocId(decl_id),
+                                   fn.name_id, impl.scope_id, impl_scope);
         if (lookup_result.is_found()) {
           used_decl_ids.push_back(lookup_result.target_inst_id());
           witness_block[index] = CheckAssociatedFunctionImplementation(

+ 3 - 2
toolchain/check/literal.cpp

@@ -7,6 +7,7 @@
 #include "toolchain/check/call.h"
 #include "toolchain/check/context.h"
 #include "toolchain/check/convert.h"
+#include "toolchain/check/name_lookup.h"
 
 namespace Carbon::Check {
 
@@ -22,8 +23,8 @@ auto MakeIntTypeLiteral(Context& context, Parse::NodeId node_id,
                         SemIR::IntKind int_kind, IntId size_id)
     -> SemIR::InstId {
   auto width_id = MakeIntLiteral(context, node_id, size_id);
-  auto fn_inst_id = context.LookupNameInCore(
-      node_id, int_kind == SemIR::IntKind::Signed ? "Int" : "UInt");
+  auto fn_inst_id = LookupNameInCore(
+      context, node_id, int_kind == SemIR::IntKind::Signed ? "Int" : "UInt");
   return PerformCall(context, node_id, fn_inst_id, {width_id});
 }
 

+ 9 - 9
toolchain/check/member_access.cpp

@@ -13,6 +13,7 @@
 #include "toolchain/check/impl_lookup.h"
 #include "toolchain/check/import_ref.h"
 #include "toolchain/check/interface.h"
+#include "toolchain/check/name_lookup.h"
 #include "toolchain/check/type_completion.h"
 #include "toolchain/diagnostics/diagnostic_emitter.h"
 #include "toolchain/sem_ir/function.h"
@@ -60,9 +61,8 @@ static auto GetHighestAllowedAccess(Context& context, SemIR::LocId loc_id,
                                     SemIR::ConstantId name_scope_const_id)
     -> SemIR::AccessKind {
   SemIR::ScopeLookupResult lookup_result =
-      context
-          .LookupUnqualifiedName(loc_id.node_id(), SemIR::NameId::SelfType,
-                                 /*required=*/false)
+      LookupUnqualifiedName(context, loc_id.node_id(), SemIR::NameId::SelfType,
+                            /*required=*/false)
           .scope_result;
   CARBON_CHECK(!lookup_result.is_poisoned());
   if (!lookup_result.is_found()) {
@@ -241,8 +241,8 @@ static auto LookupMemberNameInScope(Context& context, SemIR::LocId loc_id,
           GetHighestAllowedAccess(context, loc_id, name_scope_const_id),
   };
   LookupResult result =
-      context.LookupQualifiedName(loc_id, name_id, lookup_scopes,
-                                  /*required=*/true, access_info);
+      LookupQualifiedName(context, loc_id, name_id, lookup_scopes,
+                          /*required=*/true, access_info);
 
   if (!result.scope_result.is_found()) {
     return SemIR::ErrorInst::SingletonInstId;
@@ -436,8 +436,8 @@ auto PerformMemberAccess(Context& context, SemIR::LocId loc_id,
   if (auto base_const_id = context.constant_values().Get(base_id);
       base_const_id.is_constant()) {
     llvm::SmallVector<LookupScope> lookup_scopes;
-    if (context.AppendLookupScopesForConstant(loc_id, base_const_id,
-                                              &lookup_scopes)) {
+    if (AppendLookupScopesForConstant(context, loc_id, base_const_id,
+                                      &lookup_scopes)) {
       return LookupMemberNameInScope(context, loc_id, base_id, name_id,
                                      base_const_id, lookup_scopes,
                                      /*lookup_in_type_of_base=*/false);
@@ -465,8 +465,8 @@ auto PerformMemberAccess(Context& context, SemIR::LocId loc_id,
 
   // Find the scope corresponding to the base type.
   llvm::SmallVector<LookupScope> lookup_scopes;
-  if (!context.AppendLookupScopesForConstant(loc_id, base_type_const_id,
-                                             &lookup_scopes)) {
+  if (!AppendLookupScopesForConstant(context, loc_id, base_type_const_id,
+                                     &lookup_scopes)) {
     // The base type is not a name scope. Try some fallback options.
     if (auto struct_type = context.insts().TryGetAs<SemIR::StructType>(
             context.constant_values().GetInstId(base_type_const_id))) {

+ 483 - 0
toolchain/check/name_lookup.cpp

@@ -0,0 +1,483 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#include "toolchain/check/name_lookup.h"
+
+#include "toolchain/check/generic.h"
+#include "toolchain/check/import.h"
+#include "toolchain/check/import_ref.h"
+#include "toolchain/check/type_completion.h"
+#include "toolchain/diagnostics/format_providers.h"
+
+namespace Carbon::Check {
+
+auto AddNameToLookup(Context& context, SemIR::NameId name_id,
+                     SemIR::InstId target_id, ScopeIndex scope_index) -> void {
+  if (auto existing = context.scope_stack().LookupOrAddName(name_id, target_id,
+                                                            scope_index);
+      existing.has_value()) {
+    context.DiagnoseDuplicateName(target_id, existing);
+  }
+}
+
+auto LookupNameInDecl(Context& context, SemIR::LocId loc_id,
+                      SemIR::NameId name_id, SemIR::NameScopeId scope_id,
+                      ScopeIndex scope_index) -> SemIR::ScopeLookupResult {
+  if (!scope_id.has_value()) {
+    // Look for a name in the specified scope or a scope nested within it 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.
+    //
+    // There is also one case where the name would be in an inner scope:
+    //
+    //  - The name is redeclared by a parameter of the same entity:
+    //
+    //    fn F() {
+    //      class C(C:! type);
+    //    }
+    //
+    //    In this case, the class C is not a redeclaration of its parameter, but
+    //    we find the parameter in order to diagnose a redeclaration error.
+    return SemIR::ScopeLookupResult::MakeWrappedLookupResult(
+        context.scope_stack().LookupInLexicalScopesWithin(name_id, scope_index),
+        SemIR::AccessKind::Public);
+  } else {
+    // We do not look into `extend`ed scopes here. A qualified name in a
+    // declaration must specify the exact scope in which the name was originally
+    // introduced:
+    //
+    //    base class A { fn F(); }
+    //    class B { extend base: A; }
+    //
+    //    // Error, no `F` in `B`.
+    //    fn B.F() {}
+    return LookupNameInExactScope(context, loc_id, name_id, scope_id,
+                                  context.name_scopes().Get(scope_id),
+                                  /*is_being_declared=*/true);
+  }
+}
+
+auto LookupUnqualifiedName(Context& context, Parse::NodeId node_id,
+                           SemIR::NameId name_id, bool required)
+    -> LookupResult {
+  // TODO: Check for shadowed lookup results.
+
+  // Find the results from ancestor lexical scopes. These will be combined with
+  // results from non-lexical scopes such as namespaces and classes.
+  auto [lexical_result, non_lexical_scopes] =
+      context.scope_stack().LookupInLexicalScopes(name_id);
+
+  // Walk the non-lexical scopes and perform lookups into each of them.
+  for (auto [index, lookup_scope_id, specific_id] :
+       llvm::reverse(non_lexical_scopes)) {
+    if (auto non_lexical_result =
+            LookupQualifiedName(context, node_id, name_id,
+                                LookupScope{.name_scope_id = lookup_scope_id,
+                                            .specific_id = specific_id},
+                                /*required=*/false);
+        non_lexical_result.scope_result.is_found()) {
+      return non_lexical_result;
+    }
+  }
+
+  if (lexical_result == SemIR::InstId::InitTombstone) {
+    CARBON_DIAGNOSTIC(UsedBeforeInitialization, Error,
+                      "`{0}` used before initialization", SemIR::NameId);
+    context.emitter().Emit(node_id, UsedBeforeInitialization, name_id);
+    return {.specific_id = SemIR::SpecificId::None,
+            .scope_result = SemIR::ScopeLookupResult::MakeError()};
+  }
+
+  if (lexical_result.has_value()) {
+    // A lexical scope never needs an associated specific. If there's a
+    // lexically enclosing generic, then it also encloses the point of use of
+    // the name.
+    return {.specific_id = SemIR::SpecificId::None,
+            .scope_result = SemIR::ScopeLookupResult::MakeFound(
+                lexical_result, SemIR::AccessKind::Public)};
+  }
+
+  // We didn't find anything at all.
+  if (required) {
+    context.DiagnoseNameNotFound(node_id, name_id);
+  }
+
+  return {.specific_id = SemIR::SpecificId::None,
+          .scope_result = SemIR::ScopeLookupResult::MakeError()};
+}
+
+auto LookupNameInExactScope(Context& context, SemIR::LocId loc_id,
+                            SemIR::NameId name_id, SemIR::NameScopeId scope_id,
+                            SemIR::NameScope& scope, bool is_being_declared)
+    -> SemIR::ScopeLookupResult {
+  if (auto entry_id = is_being_declared
+                          ? scope.Lookup(name_id)
+                          : scope.LookupOrPoison(loc_id, name_id)) {
+    auto lookup_result = scope.GetEntry(*entry_id).result;
+    if (!lookup_result.is_poisoned()) {
+      LoadImportRef(context, lookup_result.target_inst_id());
+    }
+    return lookup_result;
+  }
+
+  if (!scope.import_ir_scopes().empty()) {
+    // TODO: Enforce other access modifiers for imports.
+    return SemIR::ScopeLookupResult::MakeWrappedLookupResult(
+        ImportNameFromOtherPackage(context, loc_id, scope_id,
+                                   scope.import_ir_scopes(), name_id),
+        SemIR::AccessKind::Public);
+  }
+  return SemIR::ScopeLookupResult::MakeNotFound();
+}
+
+// Prints diagnostics on invalid qualified name access.
+static auto DiagnoseInvalidQualifiedNameAccess(Context& context, SemIRLoc loc,
+                                               SemIR::InstId scope_result_id,
+                                               SemIR::NameId name_id,
+                                               SemIR::AccessKind access_kind,
+                                               bool is_parent_access,
+                                               AccessInfo access_info) -> void {
+  auto class_type = context.insts().TryGetAs<SemIR::ClassType>(
+      context.constant_values().GetInstId(access_info.constant_id));
+  if (!class_type) {
+    return;
+  }
+
+  // TODO: Support scoped entities other than just classes.
+  const auto& class_info = context.classes().Get(class_type->class_id);
+
+  auto parent_type_id = class_info.self_type_id;
+
+  if (access_kind == SemIR::AccessKind::Private && is_parent_access) {
+    if (auto base_type_id =
+            class_info.GetBaseType(context.sem_ir(), class_type->specific_id);
+        base_type_id.has_value()) {
+      parent_type_id = base_type_id;
+    } else if (auto adapted_type_id = class_info.GetAdaptedType(
+                   context.sem_ir(), class_type->specific_id);
+               adapted_type_id.has_value()) {
+      parent_type_id = adapted_type_id;
+    } else {
+      CARBON_FATAL("Expected parent for parent access");
+    }
+  }
+
+  CARBON_DIAGNOSTIC(
+      ClassInvalidMemberAccess, Error,
+      "cannot access {0:private|protected} member `{1}` of type {2}",
+      BoolAsSelect, SemIR::NameId, SemIR::TypeId);
+  CARBON_DIAGNOSTIC(ClassMemberDeclaration, Note, "declared here");
+  context.emitter()
+      .Build(loc, ClassInvalidMemberAccess,
+             access_kind == SemIR::AccessKind::Private, name_id, parent_type_id)
+      .Note(scope_result_id, ClassMemberDeclaration)
+      .Emit();
+}
+
+// Returns whether the access is prohibited by the access modifiers.
+static auto IsAccessProhibited(std::optional<AccessInfo> access_info,
+                               SemIR::AccessKind access_kind,
+                               bool is_parent_access) -> bool {
+  if (!access_info) {
+    return false;
+  }
+
+  switch (access_kind) {
+    case SemIR::AccessKind::Public:
+      return false;
+    case SemIR::AccessKind::Protected:
+      return access_info->highest_allowed_access == SemIR::AccessKind::Public;
+    case SemIR::AccessKind::Private:
+      return access_info->highest_allowed_access !=
+                 SemIR::AccessKind::Private ||
+             is_parent_access;
+  }
+}
+
+// Information regarding a prohibited access.
+struct ProhibitedAccessInfo {
+  // The resulting inst of the lookup.
+  SemIR::InstId scope_result_id;
+  // The access kind of the lookup.
+  SemIR::AccessKind access_kind;
+  // If the lookup is from an extended scope. For example, if this is a base
+  // class member access from a class that extends it.
+  bool is_parent_access;
+};
+
+auto AppendLookupScopesForConstant(Context& context, SemIR::LocId loc_id,
+                                   SemIR::ConstantId base_const_id,
+                                   llvm::SmallVector<LookupScope>* scopes)
+    -> bool {
+  auto base_id = context.constant_values().GetInstId(base_const_id);
+  auto base = context.insts().Get(base_id);
+  if (auto base_as_namespace = base.TryAs<SemIR::Namespace>()) {
+    scopes->push_back(
+        LookupScope{.name_scope_id = base_as_namespace->name_scope_id,
+                    .specific_id = SemIR::SpecificId::None});
+    return true;
+  }
+  if (auto base_as_class = base.TryAs<SemIR::ClassType>()) {
+    RequireDefinedType(
+        context, context.GetTypeIdForTypeConstant(base_const_id), loc_id, [&] {
+          CARBON_DIAGNOSTIC(QualifiedExprInIncompleteClassScope, Error,
+                            "member access into incomplete class {0}",
+                            InstIdAsType);
+          return context.emitter().Build(
+              loc_id, QualifiedExprInIncompleteClassScope, base_id);
+        });
+    auto& class_info = context.classes().Get(base_as_class->class_id);
+    scopes->push_back(LookupScope{.name_scope_id = class_info.scope_id,
+                                  .specific_id = base_as_class->specific_id});
+    return true;
+  }
+  if (auto base_as_facet_type = base.TryAs<SemIR::FacetType>()) {
+    RequireDefinedType(
+        context, context.GetTypeIdForTypeConstant(base_const_id), loc_id, [&] {
+          CARBON_DIAGNOSTIC(QualifiedExprInUndefinedInterfaceScope, Error,
+                            "member access into undefined interface {0}",
+                            InstIdAsType);
+          return context.emitter().Build(
+              loc_id, QualifiedExprInUndefinedInterfaceScope, base_id);
+        });
+    const auto& facet_type_info =
+        context.facet_types().Get(base_as_facet_type->facet_type_id);
+    for (auto interface : facet_type_info.impls_constraints) {
+      auto& interface_info = context.interfaces().Get(interface.interface_id);
+      scopes->push_back(LookupScope{.name_scope_id = interface_info.scope_id,
+                                    .specific_id = interface.specific_id});
+    }
+    return true;
+  }
+  if (base_const_id == SemIR::ErrorInst::SingletonConstantId) {
+    // Lookup into this scope should fail without producing an error.
+    scopes->push_back(LookupScope{.name_scope_id = SemIR::NameScopeId::None,
+                                  .specific_id = SemIR::SpecificId::None});
+    return true;
+  }
+  // TODO: Per the design, if `base_id` is any kind of type, then lookup should
+  // treat it as a name scope, even if it doesn't have members. For example,
+  // `(i32*).X` should fail because there's no name `X` in `i32*`, not because
+  // there's no name `X` in `type`.
+  return false;
+}
+
+// Prints a diagnostic for a missing qualified name.
+static auto DiagnoseMemberNameNotFound(
+    Context& context, SemIRLoc loc, SemIR::NameId name_id,
+    llvm::ArrayRef<LookupScope> lookup_scopes) -> void {
+  if (lookup_scopes.size() == 1 &&
+      lookup_scopes.front().name_scope_id.has_value()) {
+    auto specific_id = lookup_scopes.front().specific_id;
+    auto scope_inst_id = specific_id.has_value()
+                             ? GetInstForSpecific(context, specific_id)
+                             : context.name_scopes()
+                                   .Get(lookup_scopes.front().name_scope_id)
+                                   .inst_id();
+    CARBON_DIAGNOSTIC(MemberNameNotFoundInScope, Error,
+                      "member name `{0}` not found in {1}", SemIR::NameId,
+                      InstIdAsType);
+    context.emitter().Emit(loc, MemberNameNotFoundInScope, name_id,
+                           scope_inst_id);
+    return;
+  }
+
+  CARBON_DIAGNOSTIC(MemberNameNotFound, Error, "member name `{0}` not found",
+                    SemIR::NameId);
+  context.emitter().Emit(loc, MemberNameNotFound, name_id);
+}
+
+auto LookupQualifiedName(Context& context, SemIR::LocId loc_id,
+                         SemIR::NameId name_id,
+                         llvm::ArrayRef<LookupScope> lookup_scopes,
+                         bool required, std::optional<AccessInfo> access_info)
+    -> LookupResult {
+  llvm::SmallVector<LookupScope> scopes(lookup_scopes);
+
+  // TODO: Support reporting of multiple prohibited access.
+  llvm::SmallVector<ProhibitedAccessInfo> prohibited_accesses;
+
+  LookupResult result = {
+      .specific_id = SemIR::SpecificId::None,
+      .scope_result = SemIR::ScopeLookupResult::MakeNotFound()};
+  bool has_error = false;
+  bool is_parent_access = false;
+
+  // Walk this scope and, if nothing is found here, the scopes it extends.
+  while (!scopes.empty()) {
+    auto [scope_id, specific_id] = scopes.pop_back_val();
+    if (!scope_id.has_value()) {
+      has_error = true;
+      continue;
+    }
+    auto& name_scope = context.name_scopes().Get(scope_id);
+    has_error |= name_scope.has_error();
+
+    const SemIR::ScopeLookupResult scope_result =
+        LookupNameInExactScope(context, loc_id, name_id, scope_id, name_scope);
+    SemIR::AccessKind access_kind = scope_result.access_kind();
+
+    auto is_access_prohibited =
+        IsAccessProhibited(access_info, access_kind, is_parent_access);
+
+    // Keep track of prohibited accesses, this will be useful for reporting
+    // multiple prohibited accesses if we can't find a suitable lookup.
+    if (is_access_prohibited) {
+      prohibited_accesses.push_back({
+          .scope_result_id = scope_result.target_inst_id(),
+          .access_kind = access_kind,
+          .is_parent_access = is_parent_access,
+      });
+    }
+
+    if (!scope_result.is_found() || is_access_prohibited) {
+      // If nothing is found in this scope or if we encountered an invalid
+      // access, look in its extended scopes.
+      const auto& extended = name_scope.extended_scopes();
+      scopes.reserve(scopes.size() + extended.size());
+      for (auto extended_id : llvm::reverse(extended)) {
+        // Substitute into the constant describing the extended scope to
+        // determine its corresponding specific.
+        CARBON_CHECK(extended_id.has_value());
+        LoadImportRef(context, extended_id);
+        SemIR::ConstantId const_id = GetConstantValueInSpecific(
+            context.sem_ir(), specific_id, extended_id);
+
+        DiagnosticAnnotationScope annotate_diagnostics(
+            &context.emitter(), [&](auto& builder) {
+              CARBON_DIAGNOSTIC(FromExtendHere, Note,
+                                "declared as an extended scope here");
+              builder.Note(extended_id, FromExtendHere);
+            });
+        if (!AppendLookupScopesForConstant(context, loc_id, const_id,
+                                           &scopes)) {
+          // TODO: Handle case where we have a symbolic type and instead should
+          // look in its type.
+        }
+      }
+      is_parent_access |= !extended.empty();
+      continue;
+    }
+
+    // If this is our second lookup result, diagnose an ambiguity.
+    if (result.scope_result.is_found()) {
+      CARBON_DIAGNOSTIC(
+          NameAmbiguousDueToExtend, Error,
+          "ambiguous use of name `{0}` found in multiple extended scopes",
+          SemIR::NameId);
+      context.emitter().Emit(loc_id, NameAmbiguousDueToExtend, name_id);
+      // TODO: Add notes pointing to the scopes.
+      return {.specific_id = SemIR::SpecificId::None,
+              .scope_result = SemIR::ScopeLookupResult::MakeError()};
+    }
+
+    result.scope_result = scope_result;
+    result.specific_id = specific_id;
+  }
+
+  if (required && !result.scope_result.is_found()) {
+    if (!has_error) {
+      if (prohibited_accesses.empty()) {
+        DiagnoseMemberNameNotFound(context, loc_id, name_id, lookup_scopes);
+      } else {
+        //  TODO: We should report multiple prohibited accesses in case we don't
+        //  find a valid lookup. Reporting the last one should suffice for now.
+        auto [scope_result_id, access_kind, is_parent_access] =
+            prohibited_accesses.back();
+
+        // Note, `access_info` is guaranteed to have a value here, since
+        // `prohibited_accesses` is non-empty.
+        DiagnoseInvalidQualifiedNameAccess(context, loc_id, scope_result_id,
+                                           name_id, access_kind,
+                                           is_parent_access, *access_info);
+      }
+    }
+
+    CARBON_CHECK(!result.scope_result.is_poisoned());
+    return {.specific_id = SemIR::SpecificId::None,
+            .scope_result = SemIR::ScopeLookupResult::MakeError()};
+  }
+
+  return result;
+}
+
+// Returns the scope of the Core package, or `None` if it's not found.
+//
+// TODO: Consider tracking the Core package in SemIR so we don't need to use
+// name lookup to find it.
+static auto GetCorePackage(Context& context, SemIR::LocId loc_id,
+                           llvm::StringRef name) -> SemIR::NameScopeId {
+  auto packaging = context.parse_tree().packaging_decl();
+  if (packaging && packaging->names.package_id == PackageNameId::Core) {
+    return SemIR::NameScopeId::Package;
+  }
+  auto core_name_id = SemIR::NameId::Core;
+
+  // Look up `package.Core`.
+  auto core_scope_result = LookupNameInExactScope(
+      context, loc_id, core_name_id, SemIR::NameScopeId::Package,
+      context.name_scopes().Get(SemIR::NameScopeId::Package));
+  if (core_scope_result.is_found()) {
+    // We expect it to be a namespace.
+    if (auto namespace_inst = context.insts().TryGetAs<SemIR::Namespace>(
+            core_scope_result.target_inst_id())) {
+      // TODO: Decide whether to allow the case where `Core` is not a package.
+      return namespace_inst->name_scope_id;
+    }
+  }
+
+  CARBON_DIAGNOSTIC(
+      CoreNotFound, Error,
+      "`Core.{0}` implicitly referenced here, but package `Core` not found",
+      std::string);
+  context.emitter().Emit(loc_id, CoreNotFound, name.str());
+  return SemIR::NameScopeId::None;
+}
+
+auto LookupNameInCore(Context& context, SemIR::LocId loc_id,
+                      llvm::StringRef name) -> SemIR::InstId {
+  auto core_package_id = GetCorePackage(context, loc_id, name);
+  if (!core_package_id.has_value()) {
+    return SemIR::ErrorInst::SingletonInstId;
+  }
+
+  auto name_id = SemIR::NameId::ForIdentifier(context.identifiers().Add(name));
+  auto scope_result =
+      LookupNameInExactScope(context, loc_id, name_id, core_package_id,
+                             context.name_scopes().Get(core_package_id));
+  if (!scope_result.is_found()) {
+    CARBON_DIAGNOSTIC(
+        CoreNameNotFound, Error,
+        "name `Core.{0}` implicitly referenced here, but not found",
+        SemIR::NameId);
+    context.emitter().Emit(loc_id, CoreNameNotFound, name_id);
+    return SemIR::ErrorInst::SingletonInstId;
+  }
+
+  // Look through import_refs and aliases.
+  return context.constant_values().GetConstantInstId(
+      scope_result.target_inst_id());
+}
+
+}  // namespace Carbon::Check

+ 102 - 0
toolchain/check/name_lookup.h

@@ -0,0 +1,102 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#ifndef CARBON_TOOLCHAIN_CHECK_NAME_LOOKUP_H_
+#define CARBON_TOOLCHAIN_CHECK_NAME_LOOKUP_H_
+
+#include "llvm/ADT/ArrayRef.h"
+#include "llvm/ADT/StringRef.h"
+#include "toolchain/check/context.h"
+#include "toolchain/sem_ir/ids.h"
+
+namespace Carbon::Check {
+
+// Information about a scope in which we can perform name lookup.
+struct LookupScope {
+  // The name scope in which names are searched.
+  SemIR::NameScopeId name_scope_id;
+  // The specific for the name scope, or `None` if the name scope is not
+  // defined by a generic or we should perform lookup into the generic itself.
+  SemIR::SpecificId specific_id;
+};
+
+// A result produced by name lookup.
+struct LookupResult {
+  // The specific in which the lookup result was found. `None` if the result
+  // was not found in a specific.
+  SemIR::SpecificId specific_id;
+
+  // The result from the lookup in the scope.
+  SemIR::ScopeLookupResult scope_result;
+};
+
+// Information about an access.
+struct AccessInfo {
+  // The constant being accessed.
+  SemIR::ConstantId constant_id;
+
+  // The highest allowed access for a lookup. For example, `Protected` allows
+  // access to `Public` and `Protected` names, but not `Private`.
+  SemIR::AccessKind highest_allowed_access;
+};
+
+// Adds a name to name lookup. Prints a diagnostic for name conflicts. If
+// specified, `scope_index` specifies which lexical scope the name is inserted
+// into, otherwise the name is inserted into the current scope.
+auto AddNameToLookup(Context& context, SemIR::NameId name_id,
+                     SemIR::InstId target_id,
+                     ScopeIndex scope_index = ScopeIndex::None) -> void;
+
+// Performs name lookup in a specified scope for a name appearing in a
+// declaration. If scope_id is `None`, performs lookup into the lexical scope
+// specified by scope_index instead.
+auto LookupNameInDecl(Context& context, SemIR::LocId loc_id,
+                      SemIR::NameId name_id, SemIR::NameScopeId scope_id,
+                      ScopeIndex scope_index) -> SemIR::ScopeLookupResult;
+
+// Performs an unqualified name lookup, returning the referenced `InstId`.
+auto LookupUnqualifiedName(Context& context, Parse::NodeId node_id,
+                           SemIR::NameId name_id, bool required = true)
+    -> LookupResult;
+
+// Performs a name lookup in a specified scope, returning the referenced
+// `InstId`. Does not look into extended scopes. Returns `InstId::None` if the
+// name is not found.
+//
+// If `is_being_declared` is false, then this is a regular name lookup, and
+// the name will be poisoned if not found so that later lookups will fail; a
+// poisoned name will be treated as if it is not declared. Otherwise, this is
+// a lookup for a name being declared, so the name will not be poisoned, but
+// poison will be returned if it's already been looked up.
+auto LookupNameInExactScope(Context& context, SemIR::LocId loc_id,
+                            SemIR::NameId name_id, SemIR::NameScopeId scope_id,
+                            SemIR::NameScope& scope,
+                            bool is_being_declared = false)
+    -> SemIR::ScopeLookupResult;
+
+// Appends the lookup scopes corresponding to `base_const_id` to `*scopes`.
+// Returns `false` if not a scope. On invalid scopes, prints a diagnostic, but
+// still updates `*scopes` and returns `true`.
+auto AppendLookupScopesForConstant(Context& context, SemIR::LocId loc_id,
+                                   SemIR::ConstantId base_const_id,
+                                   llvm::SmallVector<LookupScope>* scopes)
+    -> bool;
+
+// Performs a qualified name lookup in a specified scopes and in scopes that
+// they extend, returning the referenced `InstId`.
+auto LookupQualifiedName(Context& context, SemIR::LocId loc_id,
+                         SemIR::NameId name_id,
+                         llvm::ArrayRef<LookupScope> lookup_scopes,
+                         bool required = true,
+                         std::optional<AccessInfo> access_info = std::nullopt)
+    -> LookupResult;
+
+// Returns the `InstId` corresponding to a name in the core package, or
+// BuiltinErrorInst if not found.
+auto LookupNameInCore(Context& context, SemIR::LocId loc_id,
+                      llvm::StringRef name) -> SemIR::InstId;
+
+}  // namespace Carbon::Check
+
+#endif  // CARBON_TOOLCHAIN_CHECK_NAME_LOOKUP_H_

+ 2 - 1
toolchain/check/operator.cpp

@@ -8,6 +8,7 @@
 #include "toolchain/check/context.h"
 #include "toolchain/check/generic.h"
 #include "toolchain/check/member_access.h"
+#include "toolchain/check/name_lookup.h"
 #include "toolchain/sem_ir/ids.h"
 #include "toolchain/sem_ir/typed_insts.h"
 
@@ -17,7 +18,7 @@ namespace Carbon::Check {
 static auto GetOperatorOpFunction(Context& context, SemIR::LocId loc_id,
                                   Operator op) -> SemIR::InstId {
   // Look up the interface, and pass it any generic arguments.
-  auto interface_id = context.LookupNameInCore(loc_id, op.interface_name);
+  auto interface_id = LookupNameInCore(context, loc_id, op.interface_name);
   if (!op.interface_args_ref.empty()) {
     interface_id =
         PerformCall(context, loc_id, interface_id, op.interface_args_ref);