From 90369815ad0032366cc98b802ec98753df211821 Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Tue, 27 Feb 2024 13:28:46 -0800 Subject: [PATCH] Support for name lookup into interfaces. (#3729) Follow the existing support for classes. None of this is especially useful until other features land: we don't yet have any use for defining methods of an interface out of line, because we don't support `default` or `final` interface methods, and we don't have impl lookup, so referring to an interface member is also not especially useful. But this is a nice piece to factor out that's a prerequisite for effectively testing other interface features. --- toolchain/check/decl_name_stack.cpp | 36 ++++++++- toolchain/check/handle_name.cpp | 17 ++++ .../fail_add_member_outside_definition.carbon | 72 +++++++++++++++++ .../interface/fail_lookup_undefined.carbon | 81 +++++++++++++++++++ .../interface/fail_todo_facet_lookup.carbon | 43 ++++++++++ .../testdata/interface/member_lookup.carbon | 43 ++++++++++ toolchain/diagnostics/diagnostic_kind.def | 2 + 7 files changed, 291 insertions(+), 3 deletions(-) create mode 100644 toolchain/check/testdata/interface/fail_add_member_outside_definition.carbon create mode 100644 toolchain/check/testdata/interface/fail_lookup_undefined.carbon create mode 100644 toolchain/check/testdata/interface/fail_todo_facet_lookup.carbon create mode 100644 toolchain/check/testdata/interface/member_lookup.carbon diff --git a/toolchain/check/decl_name_stack.cpp b/toolchain/check/decl_name_stack.cpp index 553fb82decac..96f7488ad646 100644 --- a/toolchain/check/decl_name_stack.cpp +++ b/toolchain/check/decl_name_stack.cpp @@ -195,6 +195,21 @@ auto DeclNameStack::UpdateScopeIfNeeded(NameContext& name_context, } break; } + case SemIR::InterfaceDecl::Kind: { + const auto& interface_info = context_->interfaces().Get( + resolved_inst.As().interface_id); + if (interface_info.is_defined()) { + name_context.state = NameContext::State::Resolved; + name_context.target_scope_id = interface_info.scope_id; + if (!is_unqualified) { + PushNameQualifierScope(*context_, name_context.resolved_inst_id, + interface_info.scope_id); + } + } else { + name_context.state = NameContext::State::ResolvedNonScope; + } + break; + } case SemIR::Namespace::Kind: { auto scope_id = resolved_inst.As().name_scope_id; name_context.state = NameContext::State::Resolved; @@ -234,9 +249,8 @@ auto DeclNameStack::TryResolveQualifier(NameContext& name_context, case NameContext::State::ResolvedNonScope: { // Because more qualifiers were found, we diagnose that the earlier // qualifier didn't resolve to a scoped entity. - if (auto class_decl = context_->insts() - .Get(name_context.resolved_inst_id) - .TryAs()) { + if (auto class_decl = context_->insts().TryGetAs( + name_context.resolved_inst_id)) { CARBON_DIAGNOSTIC(QualifiedDeclInIncompleteClassScope, Error, "Cannot declare a member of incomplete class `{0}`.", SemIR::TypeId); @@ -245,6 +259,22 @@ auto DeclNameStack::TryResolveQualifier(NameContext& name_context, context_->classes().Get(class_decl->class_id).self_type_id); context_->NoteIncompleteClass(class_decl->class_id, builder); builder.Emit(); + } else if (auto interface_decl = + context_->insts().TryGetAs( + name_context.resolved_inst_id)) { + CARBON_DIAGNOSTIC( + QualifiedDeclInUndefinedInterfaceScope, Error, + "Cannot declare a member of undefined interface `{0}`.", + std::string); + auto builder = context_->emitter().Build( + name_context.parse_node, QualifiedDeclInUndefinedInterfaceScope, + context_->sem_ir().StringifyTypeExpr( + context_->sem_ir() + .constant_values() + .Get(name_context.resolved_inst_id) + .inst_id())); + context_->NoteUndefinedInterface(interface_decl->interface_id, builder); + builder.Emit(); } else { CARBON_DIAGNOSTIC(QualifiedNameInNonScope, Error, "Name qualifiers are only allowed for entities that " diff --git a/toolchain/check/handle_name.cpp b/toolchain/check/handle_name.cpp index bb82d9adda68..16d0b1aaa903 100644 --- a/toolchain/check/handle_name.cpp +++ b/toolchain/check/handle_name.cpp @@ -25,6 +25,8 @@ static auto GetAsNameScope(Context& context, SemIR::InstId base_id) if (auto base_as_namespace = base.TryAs()) { return base_as_namespace->name_scope_id; } + // TODO: Consider refactoring the near-identical class and interface support + // below. if (auto base_as_class = base.TryAs()) { auto& class_info = context.classes().Get(base_as_class->class_id); if (!class_info.is_defined()) { @@ -39,6 +41,21 @@ static auto GetAsNameScope(Context& context, SemIR::InstId base_id) } return class_info.scope_id; } + if (auto base_as_interface = base.TryAs()) { + auto& interface_info = + context.interfaces().Get(base_as_interface->interface_id); + if (!interface_info.is_defined()) { + CARBON_DIAGNOSTIC(QualifiedExprInUndefinedInterfaceScope, Error, + "Member access into undefined interface `{0}`.", + std::string); + auto builder = context.emitter().Build( + base_id, QualifiedExprInUndefinedInterfaceScope, + context.sem_ir().StringifyTypeExpr(base_id)); + context.NoteUndefinedInterface(base_as_interface->interface_id, builder); + builder.Emit(); + } + return interface_info.scope_id; + } return std::nullopt; } diff --git a/toolchain/check/testdata/interface/fail_add_member_outside_definition.carbon b/toolchain/check/testdata/interface/fail_add_member_outside_definition.carbon new file mode 100644 index 000000000000..51afa962d2ec --- /dev/null +++ b/toolchain/check/testdata/interface/fail_add_member_outside_definition.carbon @@ -0,0 +1,72 @@ +// 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 +// +// AUTOUPDATE + +interface Interface { } + +// CHECK:STDERR: fail_add_member_outside_definition.carbon:[[@LINE+3]]:14: ERROR: Out-of-line declaration requires a declaration in scoped entity. +// CHECK:STDERR: fn Interface.F() {} +// CHECK:STDERR: ^ +fn Interface.F() {} + +// Nesting interfaces like this is not valid, but make sure we don't crash. +interface Outer { + interface Inner { + // CHECK:STDERR: fail_add_member_outside_definition.carbon:[[@LINE+3]]:8: ERROR: Name `Outer` not found. + // CHECK:STDERR: fn Outer.F(); + // CHECK:STDERR: ^~~~~ + fn Outer.F(); + } + // CHECK:STDERR: fail_add_member_outside_definition.carbon:[[@LINE+3]]:12: ERROR: Out-of-line declaration requires a declaration in scoped entity. + // CHECK:STDERR: fn Inner.F(); + // CHECK:STDERR: ^ + fn Inner.F(); +} + +// CHECK:STDOUT: --- fail_add_member_outside_definition.carbon +// CHECK:STDOUT: +// CHECK:STDOUT: constants { +// CHECK:STDOUT: %.1: type = interface_type @Interface [template] +// CHECK:STDOUT: %.2: type = interface_type @Outer [template] +// CHECK:STDOUT: %.3: type = interface_type @Inner [template] +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: file { +// CHECK:STDOUT: package: = namespace {.Interface = %Interface.decl, .Outer = %Outer.decl} [template] +// CHECK:STDOUT: %Interface.decl = interface_decl @Interface, () [template = constants.%.1] +// CHECK:STDOUT: %F: = fn_decl @F.1 [template] +// CHECK:STDOUT: %Outer.decl = interface_decl @Outer, () [template = constants.%.2] +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: interface @Interface { +// CHECK:STDOUT: +// CHECK:STDOUT: !members: +// CHECK:STDOUT: .F = file.%F +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: interface @Outer { +// CHECK:STDOUT: %Inner.decl = interface_decl @Inner, () [template = constants.%.3] +// CHECK:STDOUT: %F: = fn_decl @F.2 [template] +// CHECK:STDOUT: +// CHECK:STDOUT: !members: +// CHECK:STDOUT: .Inner = %Inner.decl +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: interface @Inner { +// CHECK:STDOUT: %.loc20: = fn_decl @.1 [template] +// CHECK:STDOUT: +// CHECK:STDOUT: !members: +// CHECK:STDOUT: .F = @Outer.%F +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: fn @F.1() { +// CHECK:STDOUT: !entry: +// CHECK:STDOUT: return +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: fn @.1(); +// CHECK:STDOUT: +// CHECK:STDOUT: fn @F.2(); +// CHECK:STDOUT: diff --git a/toolchain/check/testdata/interface/fail_lookup_undefined.carbon b/toolchain/check/testdata/interface/fail_lookup_undefined.carbon new file mode 100644 index 000000000000..137d4347e76f --- /dev/null +++ b/toolchain/check/testdata/interface/fail_lookup_undefined.carbon @@ -0,0 +1,81 @@ +// 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 +// +// AUTOUPDATE + +interface Undefined; + +// CHECK:STDERR: fail_lookup_undefined.carbon:[[@LINE+6]]:4: ERROR: Cannot declare a member of undefined interface `Undefined`. +// CHECK:STDERR: fn Undefined.F(); +// CHECK:STDERR: ^~~~~~~~~ +// CHECK:STDERR: fail_lookup_undefined.carbon:[[@LINE-5]]:1: Interface was forward declared here. +// CHECK:STDERR: interface Undefined; +// CHECK:STDERR: ^~~~~~~~~~~~~~~~~~~~ +fn Undefined.F(); + +fn Test() { + // CHECK:STDERR: fail_lookup_undefined.carbon:[[@LINE+6]]:3: ERROR: Member access into undefined interface `Undefined`. + // CHECK:STDERR: Undefined.G(); + // CHECK:STDERR: ^~~~~~~~~ + // CHECK:STDERR: fail_lookup_undefined.carbon:[[@LINE-14]]:1: Interface was forward declared here. + // CHECK:STDERR: interface Undefined; + // CHECK:STDERR: ^~~~~~~~~~~~~~~~~~~~ + Undefined.G(); +} + +interface BeingDefined { + // CHECK:STDERR: fail_lookup_undefined.carbon:[[@LINE+9]]:13: ERROR: Member access into undefined interface `BeingDefined`. + // CHECK:STDERR: fn H() -> BeingDefined.T; + // CHECK:STDERR: ^~~~~~~~~~~~ + // CHECK:STDERR: fail_lookup_undefined.carbon:[[@LINE-4]]:1: Interface is currently being defined. + // CHECK:STDERR: interface BeingDefined { + // CHECK:STDERR: ^~~~~~~~~~~~~~~~~~~~~~~~ + // CHECK:STDERR: fail_lookup_undefined.carbon:[[@LINE+3]]:13: ERROR: Name `T` not found. + // CHECK:STDERR: fn H() -> BeingDefined.T; + // CHECK:STDERR: ^~~~~~~~~~~~~~ + fn H() -> BeingDefined.T; + // CHECK:STDERR: fail_lookup_undefined.carbon:[[@LINE+3]]:6: ERROR: Name `BeingDefined` not found. + // CHECK:STDERR: fn BeingDefined.I(); + // CHECK:STDERR: ^~~~~~~~~~~~ + fn BeingDefined.I(); +} + +// CHECK:STDOUT: --- fail_lookup_undefined.carbon +// CHECK:STDOUT: +// CHECK:STDOUT: constants { +// CHECK:STDOUT: %.1: type = interface_type @Undefined [template] +// CHECK:STDOUT: %.2: type = interface_type @BeingDefined [template] +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: file { +// CHECK:STDOUT: package: = namespace {.Undefined = %Undefined.decl, .Test = %Test, .BeingDefined = %BeingDefined.decl} [template] +// CHECK:STDOUT: %Undefined.decl = interface_decl @Undefined, () [template = constants.%.1] +// CHECK:STDOUT: %.loc15: = fn_decl @.1 [template] +// CHECK:STDOUT: %Test: = fn_decl @Test [template] +// CHECK:STDOUT: %BeingDefined.decl = interface_decl @BeingDefined, () [template = constants.%.2] +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: interface @Undefined; +// CHECK:STDOUT: +// CHECK:STDOUT: interface @BeingDefined { +// CHECK:STDOUT: %H: = fn_decl @H [template] +// CHECK:STDOUT: %.loc41: = fn_decl @.2 [template] +// CHECK:STDOUT: +// CHECK:STDOUT: !members: +// CHECK:STDOUT: .H = %H +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: fn @.1(); +// CHECK:STDOUT: +// CHECK:STDOUT: fn @Test() { +// CHECK:STDOUT: !entry: +// CHECK:STDOUT: %Undefined.ref: type = name_ref Undefined, file.%Undefined.decl [template = constants.%.1] +// CHECK:STDOUT: %G.ref: = name_ref G, [template = ] +// CHECK:STDOUT: return +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: fn @H() -> ; +// CHECK:STDOUT: +// CHECK:STDOUT: fn @.2(); +// CHECK:STDOUT: diff --git a/toolchain/check/testdata/interface/fail_todo_facet_lookup.carbon b/toolchain/check/testdata/interface/fail_todo_facet_lookup.carbon new file mode 100644 index 000000000000..25dd4521f403 --- /dev/null +++ b/toolchain/check/testdata/interface/fail_todo_facet_lookup.carbon @@ -0,0 +1,43 @@ +// 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 +// +// AUTOUPDATE + +interface Interface { fn F(); } + +fn CallStatic(T:! Interface) { + // CHECK:STDERR: fail_todo_facet_lookup.carbon:[[@LINE+3]]:3: ERROR: Type `Interface` does not support qualified expressions. + // CHECK:STDERR: T.F(); + // CHECK:STDERR: ^~~ + T.F(); +} + +// CHECK:STDOUT: --- fail_todo_facet_lookup.carbon +// CHECK:STDOUT: +// CHECK:STDOUT: constants { +// CHECK:STDOUT: %.1: type = interface_type @Interface [template] +// CHECK:STDOUT: %.2: type = tuple_type () [template] +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: file { +// CHECK:STDOUT: package: = namespace {.Interface = %Interface.decl, .CallStatic = %CallStatic} [template] +// CHECK:STDOUT: %Interface.decl = interface_decl @Interface, () [template = constants.%.1] +// CHECK:STDOUT: %CallStatic: = fn_decl @CallStatic [template] +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: interface @Interface { +// CHECK:STDOUT: %F: = fn_decl @F [template] +// CHECK:STDOUT: +// CHECK:STDOUT: !members: +// CHECK:STDOUT: .F = %F +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: fn @F(); +// CHECK:STDOUT: +// CHECK:STDOUT: fn @CallStatic(%T: Interface) { +// CHECK:STDOUT: !entry: +// CHECK:STDOUT: %T.ref: Interface = name_ref T, %T [symbolic = %T] +// CHECK:STDOUT: return +// CHECK:STDOUT: } +// CHECK:STDOUT: diff --git a/toolchain/check/testdata/interface/member_lookup.carbon b/toolchain/check/testdata/interface/member_lookup.carbon new file mode 100644 index 000000000000..b3e960b05586 --- /dev/null +++ b/toolchain/check/testdata/interface/member_lookup.carbon @@ -0,0 +1,43 @@ +// 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 +// +// AUTOUPDATE + +interface Interface { fn F(); } + +fn F() { + // TODO: This should not be valid by itself. + Interface.F(); +} + +// CHECK:STDOUT: --- member_lookup.carbon +// CHECK:STDOUT: +// CHECK:STDOUT: constants { +// CHECK:STDOUT: %.1: type = interface_type @Interface [template] +// CHECK:STDOUT: %.2: type = tuple_type () [template] +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: file { +// CHECK:STDOUT: package: = namespace {.Interface = %Interface.decl, .F = %F} [template] +// CHECK:STDOUT: %Interface.decl = interface_decl @Interface, () [template = constants.%.1] +// CHECK:STDOUT: %F: = fn_decl @F.2 [template] +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: interface @Interface { +// CHECK:STDOUT: %F: = fn_decl @F.1 [template] +// CHECK:STDOUT: +// CHECK:STDOUT: !members: +// CHECK:STDOUT: .F = %F +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: fn @F.1(); +// CHECK:STDOUT: +// CHECK:STDOUT: fn @F.2() { +// CHECK:STDOUT: !entry: +// CHECK:STDOUT: %Interface.ref: type = name_ref Interface, file.%Interface.decl [template = constants.%.1] +// CHECK:STDOUT: %F.ref: = name_ref F, @Interface.%F [template = @Interface.%F] +// CHECK:STDOUT: %.loc11: init () = call %F.ref() +// CHECK:STDOUT: return +// CHECK:STDOUT: } +// CHECK:STDOUT: diff --git a/toolchain/diagnostics/diagnostic_kind.def b/toolchain/diagnostics/diagnostic_kind.def index 6257a6586ade..981f08424595 100644 --- a/toolchain/diagnostics/diagnostic_kind.def +++ b/toolchain/diagnostics/diagnostic_kind.def @@ -242,9 +242,11 @@ CARBON_DIAGNOSTIC_KIND(ExplicitAsConversionFailure) CARBON_DIAGNOSTIC_KIND(TypeExprEvaluationFailure) CARBON_DIAGNOSTIC_KIND(QualifiedDeclOutsideScopeEntity) CARBON_DIAGNOSTIC_KIND(QualifiedDeclInIncompleteClassScope) +CARBON_DIAGNOSTIC_KIND(QualifiedDeclInUndefinedInterfaceScope) CARBON_DIAGNOSTIC_KIND(QualifiedNameInNonScope) CARBON_DIAGNOSTIC_KIND(QualifiedNameNonScopeEntity) CARBON_DIAGNOSTIC_KIND(QualifiedExprInIncompleteClassScope) +CARBON_DIAGNOSTIC_KIND(QualifiedExprInUndefinedInterfaceScope) CARBON_DIAGNOSTIC_KIND(QualifiedExprUnsupported) CARBON_DIAGNOSTIC_KIND(QualifiedExprNameNotFound) CARBON_DIAGNOSTIC_KIND(UseOfNonExprAsValue)