Преглед изворни кода

Explorer: Support class extension (#1946)

Support class extension with `extends`
Relates to #1881 

Add support for:
* Class extension with `extends`
* Using base class methods and members
Adrien Leravat пре 3 година
родитељ
комит
7c19ef5be3

+ 13 - 5
explorer/ast/declaration.h

@@ -253,14 +253,14 @@ class ClassDeclaration : public Declaration {
                    Nonnull<SelfDeclaration*> self_decl,
                    ClassExtensibility extensibility,
                    std::optional<Nonnull<TuplePattern*>> type_params,
-                   std::optional<Nonnull<Expression*>> extends,
+                   std::optional<Nonnull<Expression*>> base,
                    std::vector<Nonnull<Declaration*>> members)
       : Declaration(AstNodeKind::ClassDeclaration, source_loc),
         name_(std::move(name)),
         extensibility_(extensibility),
         self_decl_(self_decl),
         type_params_(type_params),
-        extends_(extends),
+        base_expr_(base),
         members_(std::move(members)) {}
 
   static auto classof(const AstNode* node) -> bool {
@@ -275,8 +275,8 @@ class ClassDeclaration : public Declaration {
   auto type_params() -> std::optional<Nonnull<TuplePattern*>> {
     return type_params_;
   }
-  auto extends() const -> std::optional<Nonnull<Expression*>> {
-    return extends_;
+  auto base_expr() const -> std::optional<Nonnull<Expression*>> {
+    return base_expr_;
   }
   auto self() const -> Nonnull<const SelfDeclaration*> { return self_decl_; }
   auto self() -> Nonnull<SelfDeclaration*> { return self_decl_; }
@@ -295,14 +295,22 @@ class ClassDeclaration : public Declaration {
 
   auto value_category() const -> ValueCategory { return ValueCategory::Let; }
 
+  auto base() const -> std::optional<Nonnull<const ClassDeclaration*>> {
+    return base_;
+  }
+  void set_base(Nonnull<const ClassDeclaration*> base_decl) {
+    base_ = base_decl;
+  }
+
  private:
   std::string name_;
   ClassExtensibility extensibility_;
   Nonnull<SelfDeclaration*> self_decl_;
   std::optional<Nonnull<TuplePattern*>> type_params_;
-  std::optional<Nonnull<Expression*>> extends_;
+  std::optional<Nonnull<Expression*>> base_expr_;
   std::vector<Nonnull<Declaration*>> members_;
   std::optional<Nonnull<FunctionDeclaration*>> destructor_;
+  std::optional<Nonnull<const ClassDeclaration*>> base_;
 };
 
 // EXPERIMENTAL MIXIN FEATURE

+ 4 - 0
explorer/interpreter/resolve_names.cpp

@@ -577,6 +577,10 @@ static auto ResolveNames(Declaration& declaration, StaticScope& enclosing_scope,
       StaticScope class_scope;
       class_scope.AddParent(&enclosing_scope);
       enclosing_scope.MarkDeclared(class_decl.name());
+      if (class_decl.base_expr().has_value()) {
+        CARBON_RETURN_IF_ERROR(
+            ResolveNames(**class_decl.base_expr(), class_scope));
+      }
       if (class_decl.type_params().has_value()) {
         CARBON_RETURN_IF_ERROR(
             ResolveNames(**class_decl.type_params(), class_scope));

+ 89 - 30
explorer/interpreter/type_checker.cpp

@@ -7,14 +7,21 @@
 #include <algorithm>
 #include <iterator>
 #include <map>
+#include <optional>
 #include <set>
+#include <string>
+#include <string_view>
+#include <unordered_set>
 #include <vector>
 
 #include "common/error.h"
 #include "common/ostream.h"
 #include "explorer/ast/declaration.h"
+#include "explorer/ast/expression.h"
 #include "explorer/common/arena.h"
 #include "explorer/common/error_builders.h"
+#include "explorer/common/nonnull.h"
+#include "explorer/common/source_location.h"
 #include "explorer/interpreter/impl_scope.h"
 #include "explorer/interpreter/interpreter.h"
 #include "explorer/interpreter/pattern_analysis.h"
@@ -366,21 +373,35 @@ auto TypeChecker::FieldTypesImplicitlyConvertible(
   return true;
 }
 
+// Returns all class members from class and its parent classes.
+static auto GetClassHierarchy(const NominalClassType& class_type)
+    -> std::vector<Nonnull<const NominalClassType*>> {
+  Nonnull<const NominalClassType*> curr_class_type = &class_type;
+  std::vector<Nonnull<const NominalClassType*>> all_classes{curr_class_type};
+  while (curr_class_type->base().has_value()) {
+    curr_class_type = curr_class_type->base().value();
+    all_classes.push_back(curr_class_type);
+  }
+  return all_classes;
+}
+
 auto TypeChecker::FieldTypes(const NominalClassType& class_type) const
     -> std::vector<NamedValue> {
   std::vector<NamedValue> field_types;
-  for (Nonnull<Declaration*> m : class_type.declaration().members()) {
-    switch (m->kind()) {
-      case DeclarationKind::VariableDeclaration: {
-        const auto& var = cast<VariableDeclaration>(*m);
-        Nonnull<const Value*> field_type =
-            Substitute(class_type.bindings(), &var.binding().static_type());
-        field_types.push_back(
-            {.name = var.binding().name(), .value = field_type});
-        break;
+  for (const auto class_type : GetClassHierarchy(class_type)) {
+    for (Nonnull<Declaration*> m : class_type->declaration().members()) {
+      switch (m->kind()) {
+        case DeclarationKind::VariableDeclaration: {
+          const auto& var = cast<VariableDeclaration>(*m);
+          Nonnull<const Value*> field_type =
+              Substitute(class_type->bindings(), &var.binding().static_type());
+          field_types.push_back(
+              {.name = var.binding().name(), .value = field_type});
+          break;
+        }
+        default:
+          break;
       }
-      default:
-        break;
     }
   }
   return field_types;
@@ -2194,11 +2215,10 @@ auto TypeChecker::TypeCheckExp(Nonnull<Expression*> e,
         case Value::Kind::NominalClassType: {
           const auto& t_class = cast<NominalClassType>(object_type);
           CARBON_ASSIGN_OR_RETURN(
-              auto type_member, FindMixedMemberAndType(
-                                    access.source_loc(), access.member_name(),
-                                    t_class.declaration().members(), &t_class));
-          if (type_member.has_value()) {
-            auto [member_type, member] = type_member.value();
+              const auto res,
+              FindMemberWithParents(access.member_name(), &t_class));
+          if (res.has_value()) {
+            auto [member_type, member] = res.value();
             Nonnull<const Value*> field_type =
                 Substitute(t_class.bindings(), member_type);
             access.set_member(Member(member));
@@ -2413,9 +2433,9 @@ auto TypeChecker::TypeCheckExp(Nonnull<Expression*> e,
               const auto& class_type = cast<NominalClassType>(*type);
               CARBON_ASSIGN_OR_RETURN(
                   auto type_member,
-                  FindMixedMemberAndType(
-                      access.source_loc(), access.member_name(),
-                      class_type.declaration().members(), &class_type));
+                  FindMixedMemberAndType(access.member_name(),
+                                         class_type.declaration().members(),
+                                         &class_type));
               if (type_member.has_value()) {
                 auto [member_type, member] = type_member.value();
                 access.set_member(Member(member));
@@ -4067,13 +4087,37 @@ auto TypeChecker::DeclareClassDeclaration(Nonnull<ClassDeclaration*> class_decl,
   ImplScope class_scope;
   class_scope.AddParent(scope_info.innermost_scope);
 
-  if (class_decl->extensibility() != ClassExtensibility::None) {
-    return ProgramError(class_decl->source_loc())
-           << "Class prefixes `base` and `abstract` are not supported yet";
-  }
-  if (class_decl->extends()) {
+  if (class_decl->extensibility() == ClassExtensibility::Abstract) {
     return ProgramError(class_decl->source_loc())
-           << "Class extension with `extends` is not supported yet";
+           << "Class prefix `abstract` is not supported yet";
+  }
+
+  std::optional<Nonnull<const NominalClassType*>> base_class;
+  if (class_decl->base_expr().has_value()) {
+    Nonnull<Expression*> base_class_expr = *class_decl->base_expr();
+    CARBON_ASSIGN_OR_RETURN(const auto base_type,
+                            TypeCheckTypeExp(base_class_expr, class_scope));
+    switch (base_type->kind()) {
+      case Value::Kind::NominalClassType:
+        base_class = cast<NominalClassType>(base_type);
+        if (base_class.value()->declaration().extensibility() ==
+            ClassExtensibility::None) {
+          return ProgramError(class_decl->source_loc())
+                 << "Base class `" << base_class.value()->declaration().name()
+                 << "` is `final` and cannot inherited. Add the `base` or "
+                    "`abstract` class prefix to `"
+                 << base_class.value()->declaration().name()
+                 << "` to allow it to be inherited";
+        }
+        class_decl->set_base(&base_class.value()->declaration());
+        break;
+      default:
+        return ProgramError(class_decl->source_loc())
+               << "Unsupported base class type for class `"
+               << class_decl->name()
+               << "`. Only simple classes are currently supported as base "
+                  "class.";
+    }
   }
 
   std::vector<Nonnull<const GenericBinding*>> bindings = scope_info.bindings;
@@ -4090,7 +4134,7 @@ auto TypeChecker::DeclareClassDeclaration(Nonnull<ClassDeclaration*> class_decl,
   // For class declaration `class MyType(T:! Type, U:! AnInterface)`, `Self`
   // should have the value `MyType(T, U)`.
   Nonnull<NominalClassType*> self_type = arena_->New<NominalClassType>(
-      class_decl, Bindings::SymbolicIdentity(arena_, bindings));
+      class_decl, Bindings::SymbolicIdentity(arena_, bindings), base_class);
   self->set_static_type(arena_->New<TypeType>());
   self->set_constant_value(self_type);
 
@@ -5079,9 +5123,25 @@ auto TypeChecker::DeclareDeclaration(Nonnull<Declaration*> d,
   return Success();
 }
 
+auto TypeChecker::FindMemberWithParents(
+    std::string_view name, Nonnull<const NominalClassType*> class_type)
+    -> ErrorOr<std::optional<
+        std::pair<Nonnull<const Value*>, Nonnull<const Declaration*>>>> {
+  CARBON_ASSIGN_OR_RETURN(
+      const auto res,
+      FindMixedMemberAndType(name, class_type->declaration().members(),
+                             class_type));
+  if (res.has_value()) {
+    return res;
+  }
+  if (const auto base = class_type->base(); base.has_value()) {
+    return FindMemberWithParents(name, base.value());
+  }
+  return {std::nullopt};
+}
+
 auto TypeChecker::FindMixedMemberAndType(
-    SourceLocation source_loc, const std::string_view& name,
-    llvm::ArrayRef<Nonnull<Declaration*>> members,
+    const std::string_view& name, llvm::ArrayRef<Nonnull<Declaration*>> members,
     const Nonnull<const Value*> enclosing_type)
     -> ErrorOr<std::optional<
         std::pair<Nonnull<const Value*>, Nonnull<const Declaration*>>>> {
@@ -5091,8 +5151,7 @@ auto TypeChecker::FindMixedMemberAndType(
       Nonnull<const MixinPseudoType*> mixin = &mix_decl.mixin_value();
       CARBON_ASSIGN_OR_RETURN(
           const auto res,
-          FindMixedMemberAndType(source_loc, name,
-                                 mixin->declaration().members(), mixin));
+          FindMixedMemberAndType(name, mixin->declaration().members(), mixin));
       if (res.has_value()) {
         if (isa<NominalClassType>(enclosing_type)) {
           Bindings temp_map;

+ 8 - 2
explorer/interpreter/type_checker.h

@@ -60,14 +60,20 @@ class TypeChecker {
                  SourceLocation source_loc) const
       -> std::optional<Nonnull<const Witness*>>;
 
+  // Return the declaration of the member with the given name, from the class
+  // and its parents
+  auto FindMemberWithParents(std::string_view name,
+                             Nonnull<const NominalClassType*> enclosing_type)
+      -> ErrorOr<std::optional<
+          std::pair<Nonnull<const Value*>, Nonnull<const Declaration*>>>>;
+
   // Finds the direct or indirect member of a class or mixin by its name and
   // returns the member's declaration and type. Indirect members are members of
   // mixins that are mixed by member mix declarations. If the member is an
   // indirect member from a mix declaration, then the Self type variable within
   // the member's type is substituted with the type of the enclosing declaration
   // containing the mix declaration.
-  auto FindMixedMemberAndType(SourceLocation source_loc,
-                              const std::string_view& name,
+  auto FindMixedMemberAndType(const std::string_view& name,
                               llvm::ArrayRef<Nonnull<Declaration*>> members,
                               Nonnull<const Value*> enclosing_type)
       -> ErrorOr<std::optional<

+ 18 - 4
explorer/interpreter/value.cpp

@@ -7,6 +7,7 @@
 #include <algorithm>
 
 #include "common/check.h"
+#include "explorer/ast/declaration.h"
 #include "explorer/common/arena.h"
 #include "explorer/common/error_builders.h"
 #include "explorer/interpreter/action.h"
@@ -98,7 +99,7 @@ static auto GetMember(Nonnull<Arena*> arena, Nonnull<const Value*> v,
         // Look for a method in the object's class
         const auto& class_type = cast<NominalClassType>(object.type());
         std::optional<Nonnull<const FunctionValue*>> func =
-            class_type.FindFunction(f);
+            FindFunctionWithParents(f, class_type.declaration());
         if (!func) {
           return ProgramError(source_loc) << "member " << f << " not in " << *v
                                           << " or its " << class_type;
@@ -127,7 +128,7 @@ static auto GetMember(Nonnull<Arena*> arena, Nonnull<const Value*> v,
       // Access a class function.
       const auto& class_type = cast<NominalClassType>(*v);
       std::optional<Nonnull<const FunctionValue*>> fun =
-          class_type.FindFunction(f);
+          FindFunctionWithParents(f, class_type.declaration());
       if (fun == std::nullopt) {
         return ProgramError(source_loc)
                << "class function " << f << " not in " << *v;
@@ -979,9 +980,10 @@ auto ChoiceType::FindAlternative(std::string_view name) const
   return std::nullopt;
 }
 
-auto NominalClassType::FindFunction(std::string_view name) const
+auto FindFunction(std::string_view name,
+                  llvm::ArrayRef<Nonnull<Declaration*>> members)
     -> std::optional<Nonnull<const FunctionValue*>> {
-  for (const auto& member : declaration().members()) {
+  for (const auto& member : members) {
     switch (member->kind()) {
       case DeclarationKind::MixDeclaration: {
         const auto& mix_decl = cast<MixDeclaration>(*member);
@@ -1034,6 +1036,18 @@ auto MixinPseudoType::FindFunction(const std::string_view& name) const
   return std::nullopt;
 }
 
+auto FindFunctionWithParents(std::string_view name,
+                             const ClassDeclaration& class_decl)
+    -> std::optional<Nonnull<const FunctionValue*>> {
+  if (auto fun = FindFunction(name, class_decl.members()); fun.has_value()) {
+    return fun;
+  }
+  if (class_decl.base().has_value()) {
+    return FindFunctionWithParents(name, *class_decl.base().value());
+  }
+  return std::nullopt;
+}
+
 auto FindMember(std::string_view name,
                 llvm::ArrayRef<Nonnull<Declaration*>> members)
     -> std::optional<Nonnull<const Declaration*>> {

+ 23 - 8
explorer/interpreter/value.h

@@ -620,11 +620,14 @@ class NominalClassType : public Value {
 
   // Construct a fully instantiated generic class type to represent the
   // run-time type of an object.
-  explicit NominalClassType(Nonnull<const ClassDeclaration*> declaration,
-                            Nonnull<const Bindings*> bindings)
+  explicit NominalClassType(
+      Nonnull<const ClassDeclaration*> declaration,
+      Nonnull<const Bindings*> bindings,
+      std::optional<Nonnull<const NominalClassType*>> base = std::nullopt)
       : Value(Kind::NominalClassType),
         declaration_(declaration),
-        bindings_(bindings) {}
+        bindings_(bindings),
+        base_(base) {}
 
   static auto classof(const Value* value) -> bool {
     return value->kind() == Kind::NominalClassType;
@@ -634,6 +637,10 @@ class NominalClassType : public Value {
 
   auto bindings() const -> const Bindings& { return *bindings_; }
 
+  auto base() const -> std::optional<Nonnull<const NominalClassType*>> {
+    return base_;
+  }
+
   auto type_args() const -> const BindingMap& { return bindings_->args(); }
 
   // Witnesses for each of the class's impl bindings.
@@ -647,14 +654,10 @@ class NominalClassType : public Value {
     return declaration_->type_params().has_value() && type_args().empty();
   }
 
-  // Returns the value of the function named `name` in this class, or
-  // nullopt if there is no such function.
-  auto FindFunction(std::string_view name) const
-      -> std::optional<Nonnull<const FunctionValue*>>;
-
  private:
   Nonnull<const ClassDeclaration*> declaration_;
   Nonnull<const Bindings*> bindings_ = Bindings::None();
+  std::optional<Nonnull<const NominalClassType*>> base_;
 };
 
 class MixinPseudoType : public Value {
@@ -692,6 +695,18 @@ class MixinPseudoType : public Value {
   Nonnull<const Bindings*> bindings_ = Bindings::None();
 };
 
+// Returns the value of the function named `name` in this class, or
+// nullopt if there is no such function.
+auto FindFunction(std::string_view name,
+                  llvm::ArrayRef<Nonnull<Declaration*>> members)
+    -> std::optional<Nonnull<const FunctionValue*>>;
+
+// Returns the value of the function named `name` in this class and its
+// parents, or nullopt if there is no such function.
+auto FindFunctionWithParents(std::string_view name,
+                             const ClassDeclaration& class_decl)
+    -> std::optional<Nonnull<const FunctionValue*>>;
+
 // Return the declaration of the member with the given name.
 auto FindMember(std::string_view name,
                 llvm::ArrayRef<Nonnull<Declaration*>> members)

+ 40 - 0
explorer/testdata/class/class_inheritance_function_call.carbon

@@ -0,0 +1,40 @@
+// 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
+// RUN: %{explorer-run}
+// RUN: %{explorer-run-trace}
+// CHECK:STDOUT: Class C, call 0
+// CHECK:STDOUT: Class D, call 0
+// CHECK:STDOUT: Class C, call 1
+// CHECK:STDOUT: Class C, call 2
+// CHECK:STDOUT: Class D, call 2
+// CHECK:STDOUT: result: 0
+
+package ExplorerTest api;
+
+base class C {
+  fn FunctionC(var i: i32) {
+    Print("Class C, call {0}", i);
+  }
+}
+
+class D extends C {
+  fn FunctionD(var i: i32) {
+    C.FunctionC(i);
+    Print("Class D, call {0}", i);
+  }
+}
+
+fn Main() -> i32 {
+  // Calling function from class type
+  D.FunctionD(0);
+  // Note: D.FunctionC(0); is not accessible
+
+  // Calling function from class object
+  var d: D = {};
+  d.FunctionC(1);
+  d.FunctionD(2);
+  return 0;
+}

+ 38 - 0
explorer/testdata/class/class_inheritance_methods.carbon

@@ -0,0 +1,38 @@
+// 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
+// RUN: %{explorer-run}
+// RUN: %{explorer-run-trace}
+// CHECK:STDOUT: Class 1
+// CHECK:STDOUT: Class 2
+// CHECK:STDOUT: result: 0
+
+package ExplorerTest api;
+
+base class C {
+  fn BasePrint(v: i32) {
+    Print("Class {0}", v);
+  }
+  fn Method1[me:Self]() {
+    me.BasePrint(me.value_c);
+  }
+
+  var value_c: i32;
+}
+
+class D extends C {
+  fn Method2[me:Self]() {
+    me.BasePrint(me.value_d);
+  }
+
+  var value_d: i32;
+}
+
+fn Main() -> i32 {
+  var d: D = {.value_c = 1, .value_d = 2};
+  d.Method1();
+  d.Method2();
+  return 0;
+}

+ 33 - 0
explorer/testdata/class/class_inheritance_multiple.carbon

@@ -0,0 +1,33 @@
+// 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
+// RUN: %{explorer-run}
+// RUN: %{explorer-run-trace}
+// CHECK:STDOUT: result: 0
+
+package ExplorerTest api;
+
+base class A {
+  fn FunctionA() {}
+  var var_a: i32;
+}
+
+base class B extends A {
+  fn FunctionB() {}
+  var var_b: i32;
+}
+
+class C extends B {
+  fn FunctionC() {}
+  var var_c: i32;
+}
+
+fn Main() -> i32 {
+  var c: C = {.var_a=1, .var_b=2, .var_c=3};
+  c.FunctionA();
+  c.FunctionB();
+  c.FunctionC();
+  return 0;
+}

+ 40 - 0
explorer/testdata/class/class_inheritance_shadow.carbon

@@ -0,0 +1,40 @@
+// 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
+// RUN: %{explorer-run}
+// RUN: %{explorer-run-trace}
+// CHECK:STDOUT: Class D
+// CHECK:STDOUT: d.value = 2
+// CHECK:STDOUT: d.GetValue() = 2
+// CHECK:STDOUT: result: 0
+
+package ExplorerTest api;
+
+base class C {
+  fn Method1() {
+    Print("Class C");
+  }
+  fn GetValue[me: Self]() -> i32 {
+    return me.value;
+  }
+  var value: i32;
+}
+
+class D extends C {
+  fn Method1() {
+    Print("Class D");
+  }
+  var value: i32;
+}
+
+fn Main() -> i32 {
+  // Initialize derived value first, base value second
+  var d: D = {.value = 2, .value = 1};
+  d.Method1();
+
+  Print("d.value = {0}", d.value);
+  Print("d.GetValue() = {0}", d.GetValue());
+  return 0;
+}

+ 39 - 0
explorer/testdata/class/class_inheritance_variables.carbon

@@ -0,0 +1,39 @@
+// 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
+// RUN: %{explorer-run}
+// RUN: %{explorer-run-trace}
+// CHECK:STDOUT: Read var_c=1
+// CHECK:STDOUT: Read var_d=2
+// CHECK:STDOUT: Assign var_c=3
+// CHECK:STDOUT: Assign var_d=4
+// CHECK:STDOUT: result: 0
+
+package ExplorerTest api;
+
+base class C {
+  var var_c: i32;
+}
+
+class D extends C {
+  var var_d: i32;
+}
+
+fn Main() -> i32 {
+  // Initialization
+  var d: D = {.var_c= 1, .var_d= 2};
+
+  // Read
+  Print("Read var_c={0}", d.var_c);
+  Print("Read var_d={0}", d.var_d);
+
+  // Assignment
+  d.var_c = 3;
+  d.var_d = 4;
+  Print("Assign var_c={0}", d.var_c);
+  Print("Assign var_d={0}", d.var_d);
+
+  return 0;
+}

+ 1 - 1
explorer/testdata/class/fail_abstract_class.carbon

@@ -10,7 +10,7 @@ package ExplorerTest api;
 
 abstract class C {
   fn F() {}
-// CHECK:STDERR: COMPILATION ERROR: {{.*}}/explorer/testdata/class/fail_abstract_class.carbon:[[@LINE+1]]: Class prefixes `base` and `abstract` are not supported yet
+// CHECK:STDERR: COMPILATION ERROR: {{.*}}/explorer/testdata/class/fail_abstract_class.carbon:[[@LINE+1]]: Class prefix `abstract` is not supported yet
 }
 
 fn Main() -> i32 {

+ 4 - 4
explorer/testdata/class/fail_class_extends.carbon → explorer/testdata/class/fail_extends_final_class.carbon

@@ -8,13 +8,13 @@
 
 package ExplorerTest api;
 
-class A {
+class C {
   fn F() {}
 }
 
-class B extends A {
-  fn F() {}
-// CHECK:STDERR: COMPILATION ERROR: {{.*}}/explorer/testdata/class/fail_class_extends.carbon:[[@LINE+1]]: Class extension with `extends` is not supported yet
+class D extends C {
+  fn G() {}
+// CHECK:STDERR: COMPILATION ERROR: {{.*}}/explorer/testdata/class/fail_extends_final_class.carbon:[[@LINE+1]]: Base class `C` is `final` and cannot inherited. Add the `base` or `abstract` class prefix to `C` to allow it to be inherited
 }
 
 fn Main() -> i32 {

+ 4 - 3
explorer/testdata/class/fail_base_class.carbon → explorer/testdata/class/fail_extends_non_class.carbon

@@ -8,9 +8,10 @@
 
 package ExplorerTest api;
 
-base class C {
-  fn F() {}
-// CHECK:STDERR: COMPILATION ERROR: {{.*}}/explorer/testdata/class/fail_base_class.carbon:[[@LINE+1]]: Class prefixes `base` and `abstract` are not supported yet
+// CHECK:STDERR: COMPILATION ERROR: {{.*}}/explorer/testdata/class/fail_extends_non_class.carbon:[[@LINE+1]]: Expected a type, but got 3
+class C extends 3 {
+  var x: i32;
+  var y: i32;
 }
 
 fn Main() -> i32 {