Przeglądaj źródła

Feature call destructor for tuples and bug fixes for the destructor process (#2255)

This PR includes the following changes:
*  Added destruction process for tuples
*  Fix: In the current version only the last member of a object can be destroyed
*  Fix: In the current version, the destructor of the object is called after each method call 

I hope it is useful

Co-authored-by: m new <michael.burzan@outlook.de>
Co-authored-by: Geoff Romer <gromer@google.com>
pmqtt 3 lat temu
rodzic
commit
cef93fba5e

+ 22 - 14
explorer/interpreter/action.cpp

@@ -25,20 +25,19 @@ RuntimeScope::RuntimeScope(RuntimeScope&& other) noexcept
     : locals_(std::move(other.locals_)),
       // To transfer ownership of other.allocations_, we have to empty it out.
       allocations_(std::exchange(other.allocations_, {})),
-      heap_(other.heap_),
-      destructor_scope_(other.destructor_scope_) {}
+      heap_(other.heap_) {}
 
 auto RuntimeScope::operator=(RuntimeScope&& rhs) noexcept -> RuntimeScope& {
   locals_ = std::move(rhs.locals_);
   // To transfer ownership of rhs.allocations_, we have to empty it out.
   allocations_ = std::exchange(rhs.allocations_, {});
   heap_ = rhs.heap_;
-  destructor_scope_ = rhs.destructor_scope_;
   return *this;
 }
 
 RuntimeScope::~RuntimeScope() {
-  for (AllocationId allocation : allocations_) {
+  for (auto allocation : allocations_) {
+    // TODO: move this into StepCleanUp
     heap_->Deallocate(allocation);
   }
 }
@@ -52,6 +51,22 @@ void RuntimeScope::Print(llvm::raw_ostream& out) const {
   out << "}";
 }
 
+void RuntimeScope::Bind(ValueNodeView value_node, Nonnull<const Value*> value) {
+  CARBON_CHECK(!value_node.constant_value().has_value());
+  CARBON_CHECK(value->kind() != Value::Kind::LValue);
+  auto allocation_id = heap_->GetAllocationId(value);
+  if (!allocation_id) {
+    auto id = heap_->AllocateValue(value);
+    auto [it, success] =
+        locals_.insert({value_node, heap_->arena().New<LValue>(Address(id))});
+    CARBON_CHECK(success) << "Duplicate definition of " << value_node.base();
+  } else {
+    auto [it, success] = locals_.insert(
+        {value_node, heap_->arena().New<LValue>(Address(*allocation_id))});
+    CARBON_CHECK(success) << "Duplicate definition of " << value_node.base();
+  }
+}
+
 void RuntimeScope::Initialize(ValueNodeView value_node,
                               Nonnull<const Value*> value) {
   CARBON_CHECK(!value_node.constant_value().has_value());
@@ -98,16 +113,6 @@ auto RuntimeScope::Capture(
   return result;
 }
 
-void RuntimeScope::TransitState() {
-  if (destructor_scope_ == State::Normal) {
-    destructor_scope_ = State::Destructor;
-  } else if (destructor_scope_ == State::Destructor) {
-    destructor_scope_ = State::CleanUpped;
-  } else {
-    destructor_scope_ = State::CleanUpped;
-  }
-}
-
 void Action::Print(llvm::raw_ostream& out) const {
   switch (kind()) {
     case Action::Kind::LValAction:
@@ -138,6 +143,9 @@ void Action::Print(llvm::raw_ostream& out) const {
     case Action::Kind::CleanUpAction:
       out << "clean up";
       break;
+    case Action::Kind::DestroyAction:
+      out << "destroy";
+      break;
   }
   out << "." << pos_ << ".";
   if (!results_.empty()) {

+ 41 - 29
explorer/interpreter/action.h

@@ -27,12 +27,6 @@ namespace Carbon {
 // not compile-time constants.
 class RuntimeScope {
  public:
-  enum class State {
-    Normal = 0,
-    Destructor = 1,
-    CleanUpped = 2,
-  };
-
   // Returns a RuntimeScope whose Get() operation for a given name returns the
   // storage owned by the first entry in `scopes` that defines that name. This
   // behavior is closely analogous to a `[&]` capture in C++, hence the name.
@@ -42,8 +36,7 @@ class RuntimeScope {
       -> RuntimeScope;
 
   // Constructs a RuntimeScope that allocates storage in `heap`.
-  explicit RuntimeScope(Nonnull<HeapAllocationInterface*> heap)
-      : heap_(heap), destructor_scope_(State::Normal) {}
+  explicit RuntimeScope(Nonnull<HeapAllocationInterface*> heap) : heap_(heap) {}
 
   // Moving a RuntimeScope transfers ownership of its allocations.
   RuntimeScope(RuntimeScope&&) noexcept;
@@ -55,8 +48,12 @@ class RuntimeScope {
   void Print(llvm::raw_ostream& out) const;
   LLVM_DUMP_METHOD void Dump() const { Print(llvm::errs()); }
 
+  // Binds `value` as the value of `value_node`.
+  void Bind(ValueNodeView value_node, Nonnull<const Value*> value);
+
   // Allocates storage for `value_node` in `heap`, and initializes it with
   // `value`.
+  // TODO: Update existing callers to use Bind instead, where appropriate.
   void Initialize(ValueNodeView value_node, Nonnull<const Value*> value);
 
   // Transfers the names and allocations from `other` into *this. The two
@@ -69,31 +66,16 @@ class RuntimeScope {
       -> std::optional<Nonnull<const LValue*>>;
 
   // Returns the local values in created order
-  auto locals() const -> std::vector<Nonnull<const LValue*>> {
-    std::vector<Nonnull<const LValue*>> res;
-    for (const auto& entry : locals_) {
-      res.push_back(entry.second);
-    }
-    return res;
+  auto allocations() const -> const std::vector<AllocationId>& {
+    return allocations_;
   }
 
-  // Return scope state
-  // Normal     = Scope is not bind at the top of a destructor call
-  // Destructor = Scope is bind to a destructor call and was not cleaned up,
-  // CleanedUp  = Scope is bind to a destructor call and is cleaned up
-  auto DestructionState() const -> State { return destructor_scope_; }
-
-  // Transit the state from Normal to Destructor
-  // Transit the state from Destructor to CleanUpped
-  void TransitState();
-
  private:
   llvm::MapVector<ValueNodeView, Nonnull<const LValue*>,
                   std::map<ValueNodeView, unsigned>>
       locals_;
   std::vector<AllocationId> allocations_;
   Nonnull<HeapAllocationInterface*> heap_;
-  State destructor_scope_;
 };
 
 // An Action represents the current state of a self-contained computation,
@@ -120,6 +102,7 @@ class Action {
     ScopeAction,
     RecursiveAction,
     CleanUpAction,
+    DestroyAction
   };
 
   Action(const Value&) = delete;
@@ -291,21 +274,50 @@ class DeclarationAction : public Action {
   Nonnull<const Declaration*> declaration_;
 };
 
+// An Action which implements destroying all local allocations in a scope.
 class CleanupAction : public Action {
  public:
-  explicit CleanupAction(RuntimeScope scope) : Action(Kind::CleanUpAction) {
-    locals_count_ = scope.locals().size();
+  explicit CleanupAction(RuntimeScope scope)
+      : Action(Kind::CleanUpAction),
+        allocations_count_(scope.allocations().size()) {
     StartScope(std::move(scope));
   }
 
-  auto locals_count() const -> int { return locals_count_; }
+  auto allocations_count() const -> int { return allocations_count_; }
 
   static auto classof(const Action* action) -> bool {
     return action->kind() == Kind::CleanUpAction;
   }
 
  private:
-  int locals_count_;
+  int allocations_count_;
+};
+
+// An Action which implements destroying a single value, including all nested
+// values.
+class DestroyAction : public Action {
+ public:
+  // lvalue: Address of the object to be destroyed
+  // value:  The value to be destroyed
+  //         In most cases the lvalue address points to value
+  //         In the case that the member of a class is to be destroyed, points
+  //         the lvalue points to the address of the class object
+  //         and the value is the member of the class
+  explicit DestroyAction(Nonnull<const LValue*> lvalue,
+                         Nonnull<const Value*> value)
+      : Action(Kind::DestroyAction), lvalue_(lvalue), value_(value) {}
+
+  static auto classof(const Action* action) -> bool {
+    return action->kind() == Kind::DestroyAction;
+  }
+
+  auto lvalue() const -> Nonnull<const LValue*> { return lvalue_; }
+
+  auto value() const -> Nonnull<const Value*> { return value_; }
+
+ private:
+  Nonnull<const LValue*> lvalue_;
+  Nonnull<const Value*> value_;
 };
 
 // Action which does nothing except introduce a new scope into the action

+ 2 - 5
explorer/interpreter/action_stack.cpp

@@ -152,6 +152,7 @@ static auto FinishActionKindFor(Action::Kind kind) -> FinishActionKind {
       return FinishActionKind::NoValue;
     case Action::Kind::ScopeAction:
     case Action::Kind::CleanUpAction:
+    case Action::Kind::DestroyAction:
       return FinishActionKind::NeverCalled;
   }
 }
@@ -313,11 +314,7 @@ void ActionStack::PopScopes(
   while (!todo_.IsEmpty() && llvm::isa<ScopeAction>(*todo_.Top())) {
     auto act = todo_.Pop();
     if (act->scope()) {
-      if ((*act->scope()).DestructionState() <
-          RuntimeScope::State::CleanUpped) {
-        (*act->scope()).TransitState();
-        cleanup_stack.push(std::move(act));
-      }
+      cleanup_stack.push(std::move(act));
     }
   }
 }

+ 12 - 0
explorer/interpreter/heap.cpp

@@ -46,6 +46,18 @@ auto Heap::Write(const Address& a, Nonnull<const Value*> v,
   return Success();
 }
 
+auto Heap::GetAllocationId(Nonnull<const Value*> v) const
+    -> std::optional<AllocationId> {
+  auto iter = std::find(values_.begin(), values_.end(), v);
+  if (iter != values_.end()) {
+    auto index = iter - values_.begin();
+    if (states_[index] == ValueState::Alive) {
+      return AllocationId(index);
+    }
+  }
+  return std::nullopt;
+}
+
 auto Heap::CheckAlive(AllocationId allocation, SourceLocation source_loc) const
     -> ErrorOr<Success> {
   if (states_[allocation.index_] == ValueState::Dead) {

+ 3 - 0
explorer/interpreter/heap.h

@@ -41,6 +41,9 @@ class Heap : public HeapAllocationInterface {
   auto Write(const Address& a, Nonnull<const Value*> v,
              SourceLocation source_loc) -> ErrorOr<Success>;
 
+  auto GetAllocationId(Nonnull<const Value*> v) const
+      -> std::optional<AllocationId> override;
+
   // Put the given value on the heap and mark its state.
   // Mark UninitializedValue as uninitialized and other values as alive.
   auto AllocateValue(Nonnull<const Value*> v) -> AllocationId override;

+ 5 - 0
explorer/interpreter/heap_allocation_interface.h

@@ -30,6 +30,11 @@ class HeapAllocationInterface {
   // Returns the arena used to allocate the values in this heap.
   virtual auto arena() const -> Arena& = 0;
 
+  // Returns the ID of the first allocation that holds `v`, if one exists.
+  // TODO: Find a way to remove this.
+  virtual auto GetAllocationId(Nonnull<const Value*> v) const
+      -> std::optional<AllocationId> = 0;
+
  protected:
   HeapAllocationInterface() = default;
   virtual ~HeapAllocationInterface() = default;

+ 88 - 38
explorer/interpreter/interpreter.cpp

@@ -89,6 +89,9 @@ class Interpreter {
   auto StepDeclaration() -> ErrorOr<Success>;
   // State transition for object destruction.
   auto StepCleanUp() -> ErrorOr<Success>;
+  auto StepDestroy() -> ErrorOr<Success>;
+  // State transition for tuple destruction.
+  auto StepCleanUpTuple() -> ErrorOr<Success>;
 
   auto CreateStruct(const std::vector<FieldInitializer>& fields,
                     const std::vector<Nonnull<const Value*>>& values)
@@ -818,15 +821,17 @@ auto Interpreter::CallDestructor(Nonnull<const DestructorDeclaration*> fun,
   CARBON_CHECK(method.is_method());
   RuntimeScope method_scope(&heap_);
   BindingMap generic_args;
-  CARBON_CHECK(PatternMatch(&method.me_pattern().value(), receiver,
-                            fun->source_loc(), &method_scope, generic_args,
-                            trace_stream_, this->arena_));
 
+  // TODO: move this logic into PatternMatch, and call it here.
+  auto p = &method.me_pattern().value();
+  const auto& placeholder = cast<BindingPlaceholderValue>(*p);
+  if (placeholder.value_node().has_value()) {
+    method_scope.Bind(*placeholder.value_node(), receiver);
+  }
   CARBON_CHECK(method.body().has_value())
       << "Calling a method that's missing a body";
 
   auto act = std::make_unique<StatementAction>(*method.body());
-  method_scope.TransitState();
   return todo_.Spawn(std::unique_ptr<Action>(std::move(act)),
                      std::move(method_scope));
 }
@@ -900,9 +905,18 @@ auto Interpreter::CallFunction(const CallExpression& call,
       RuntimeScope method_scope(&heap_);
       BindingMap generic_args;
       // Bind the receiver to the `me` parameter.
-      CARBON_CHECK(PatternMatch(&method.me_pattern().value(), m.receiver(),
-                                call.source_loc(), &method_scope, generic_args,
-                                trace_stream_, this->arena_));
+      auto p = &method.me_pattern().value();
+      if (p->kind() == Value::Kind::BindingPlaceholderValue) {
+        // TODO: move this logic into PatternMatch
+        const auto& placeholder = cast<BindingPlaceholderValue>(*p);
+        if (placeholder.value_node().has_value()) {
+          method_scope.Bind(*placeholder.value_node(), m.receiver());
+        }
+      } else {
+        CARBON_CHECK(PatternMatch(&method.me_pattern().value(), m.receiver(),
+                                  call.source_loc(), &method_scope,
+                                  generic_args, trace_stream_, this->arena_));
+      }
       // Bind the arguments to the parameters.
       CARBON_CHECK(PatternMatch(&method.param_pattern().value(), converted_args,
                                 call.source_loc(), &method_scope, generic_args,
@@ -1998,45 +2012,60 @@ auto Interpreter::StepDeclaration() -> ErrorOr<Success> {
   }
 }
 
-auto Interpreter::StepCleanUp() -> ErrorOr<Success> {
+auto Interpreter::StepDestroy() -> ErrorOr<Success> {
+  // TODO: find a way to avoid dyn_cast in this code, and instead use static
+  // type information the way the compiler would.
   Action& act = todo_.CurrentAction();
-  auto& cleanup = cast<CleanupAction>(act);
-  if (act.pos() < cleanup.locals_count()) {
-    const auto* lvalue =
-        act.scope()->locals()[cleanup.locals_count() - act.pos() - 1];
-    SourceLocation source_loc("destructor", 1);
-    auto value = heap_.Read(lvalue->address(), source_loc);
-    if (value.ok()) {
-      if (act.scope()->DestructionState() < RuntimeScope::State::CleanUpped) {
-        if (const auto* class_obj = dyn_cast<NominalClassValue>(*value)) {
+  DestroyAction& destroy_act = cast<DestroyAction>(act);
+  if (act.pos() == 0) {
+    if (const auto* class_obj =
+            dyn_cast<NominalClassValue>(destroy_act.value())) {
+      const auto& class_type = cast<NominalClassType>(class_obj->type());
+      const auto& class_dec = class_type.declaration();
+      if (class_dec.destructor().has_value()) {
+        return CallDestructor(*class_dec.destructor(), class_obj);
+      }
+    }
+  }
+
+  if (const auto* tuple = dyn_cast<TupleValue>(destroy_act.value())) {
+    if (tuple->elements().size() > 0) {
+      int index = tuple->elements().size() - act.pos() - 1;
+      if (index >= 0) {
+        const auto& item = tuple->elements()[index];
+        if (const auto* class_obj = dyn_cast<NominalClassValue>(item)) {
           const auto& class_type = cast<NominalClassType>(class_obj->type());
           const auto& class_dec = class_type.declaration();
           if (class_dec.destructor().has_value()) {
             return CallDestructor(*class_dec.destructor(), class_obj);
           }
         }
-      } else {
-        if (const auto* class_obj = dyn_cast<NominalClassValue>(*value)) {
-          const auto& class_type = cast<NominalClassType>(class_obj->type());
-          const auto& class_dec = class_type.declaration();
-          const auto& class_members = class_dec.members();
-          for (const auto& member : class_members) {
-            if (const auto* var = dyn_cast<VariableDeclaration>(member)) {
-              const auto& type = var->static_type();
-              if (const auto* c_type = dyn_cast<NominalClassType>(&type)) {
-                const auto& c_dec = c_type->declaration();
-                if (c_dec.destructor().has_value()) {
-                  Address object = lvalue->address();
-                  Address mem = object.SubobjectAddress(Member(var));
-                  auto v = heap_.Read(mem, source_loc);
-                  act.scope()->TransitState();
-                  return CallDestructor(*c_dec.destructor(), *v);
-                }
-              }
-            }
-          }
+        if (item->kind() == Value::Kind::TupleValue) {
+          return todo_.Spawn(
+              std::make_unique<DestroyAction>(destroy_act.lvalue(), item));
+        }
+        // Type of tuple element is integral type e.g. i32
+        // or the type has no destructor
+      }
+    }
+  }
+
+  if (act.pos() > 0) {
+    if (const auto* class_obj =
+            dyn_cast<NominalClassValue>(destroy_act.value())) {
+      const auto& class_type = cast<NominalClassType>(class_obj->type());
+      const auto& class_dec = class_type.declaration();
+      int index = class_dec.members().size() - act.pos();
+      if (index >= 0 && index < static_cast<int>(class_dec.members().size())) {
+        const auto& member = class_dec.members()[index];
+        if (const auto* var = dyn_cast<VariableDeclaration>(member)) {
+          Address object = destroy_act.lvalue()->address();
+          Address mem = object.SubobjectAddress(Member(var));
+          SourceLocation source_loc("destructor", 1);
+          auto v = heap_.Read(mem, source_loc);
+          return todo_.Spawn(
+              std::make_unique<DestroyAction>(destroy_act.lvalue(), *v));
         }
-        act.scope()->TransitState();
       }
     }
   }
@@ -2044,6 +2073,24 @@ auto Interpreter::StepCleanUp() -> ErrorOr<Success> {
   return Success();
 }
 
+auto Interpreter::StepCleanUp() -> ErrorOr<Success> {
+  Action& act = todo_.CurrentAction();
+  CleanupAction& cleanup = cast<CleanupAction>(act);
+  if (act.pos() < cleanup.allocations_count()) {
+    auto allocation =
+        act.scope()->allocations()[cleanup.allocations_count() - act.pos() - 1];
+    auto lvalue = arena_->New<LValue>(Address(allocation));
+    SourceLocation source_loc("destructor", 1);
+    auto value = heap_.Read(lvalue->address(), source_loc);
+    // Step over uninitialized values
+    if (value.ok()) {
+      return todo_.Spawn(std::make_unique<DestroyAction>(lvalue, *value));
+    }
+  }
+  todo_.Pop();
+  return Success();
+}
+
 // State transition.
 auto Interpreter::Step() -> ErrorOr<Success> {
   Action& act = todo_.CurrentAction();
@@ -2069,6 +2116,9 @@ auto Interpreter::Step() -> ErrorOr<Success> {
     case Action::Kind::CleanUpAction:
       CARBON_RETURN_IF_ERROR(StepCleanUp());
       break;
+    case Action::Kind::DestroyAction:
+      CARBON_RETURN_IF_ERROR(StepDestroy());
+      break;
     case Action::Kind::ScopeAction:
       CARBON_FATAL() << "ScopeAction escaped ActionStack";
     case Action::Kind::RecursiveAction:

+ 5 - 3
explorer/testdata/destructor/call_destructor_from_destructor.carbon

@@ -9,6 +9,7 @@
 // CHECK:STDOUT: DESTRUCTOR B 2
 // CHECK:STDOUT: DESTRUCTOR A 3
 // CHECK:STDOUT: DESTRUCTOR A 4
+// CHECK:STDOUT: DESTRUCTOR A 5
 // CHECK:STDOUT: result: 1
 
 package ExplorerTest api;
@@ -28,15 +29,16 @@ class B{
         Print("DESTRUCTOR B {0}",me.n);
     }
     fn Create(x: i32) -> B{
-       return {.n = x, .a = A.Create(3) };
+       return {.n = x, .a1 = A.Create(4),.a2 = A.Create(3) };
     }
+    var a1: A;
     var n: i32;
-    var a: A;
+    var a2: A;
 }
 
 
 fn Main() -> i32 {
-  var a: A = A.Create(4);
+  var a: A = A.Create(5);
   var b: B = B.Create(2);
   var c: A = A.Create(1);
   return 1;

+ 30 - 0
explorer/testdata/destructor/destroy_array.carbon

@@ -0,0 +1,30 @@
+// 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: DESTRUCTOR A 1
+// CHECK:STDOUT: DESTRUCTOR A 2
+// CHECK:STDOUT: DESTRUCTOR A 3
+// CHECK:STDOUT: DESTRUCTOR A 4
+// CHECK:STDOUT: DESTRUCTOR A 5
+// CHECK:STDOUT: DESTRUCTOR A 6
+// CHECK:STDOUT: result: 1
+
+package ExplorerTest api;
+
+
+class A{
+    destructor[me: Self]{
+        Print("DESTRUCTOR A {0}",me.n);
+    }
+    var n: i32;
+}
+
+fn Main() -> i32 {
+  var a: [A; 2] = ({.n = 6},{.n = 5});
+  var b: [[A; 2]; 2] = (({.n = 4},{.n = 3}), ({.n = 2},{.n = 1}));
+  return 1;
+}

+ 27 - 0
explorer/testdata/destructor/destroy_different_types.carbon

@@ -0,0 +1,27 @@
+// 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: DESTRUCTOR A 0
+// CHECK:STDOUT: DESTRUCTOR A 1
+// CHECK:STDOUT: DESTRUCTOR A 2
+// CHECK:STDOUT: result: 1
+
+package ExplorerTest api;
+
+
+class A{
+    destructor[me: Self]{
+        Print("DESTRUCTOR A {0}",me.n);
+    }
+    var n: i32;
+}
+
+fn Main() -> i32 {
+  var a1: A = {.n = 2};
+  var a: [A; 2] = ({.n = 1},{.n = 0});
+  return 1;
+}

+ 38 - 0
explorer/testdata/destructor/destroy_inner_destructor.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: DESTRUCTOR A 1
+// CHECK:STDOUT: DESTRUCTOR B 2
+// CHECK:STDOUT: result: 1
+
+package ExplorerTest api;
+
+class A{
+    class B{
+        destructor[me: Self]{
+            Print("DESTRUCTOR B {0}",me.n);
+        }
+        fn Create(x: i32) -> B{
+           return {.n = x };
+        }
+        var n: i32;
+    }
+
+    destructor[me: Self]{
+        Print("DESTRUCTOR A {0}",me.n);
+    }
+    fn Create(x: i32) -> A{
+        return {.n = x, .b = B.Create(2)};
+    }
+    var n: i32;
+    var b : B;
+}
+
+fn Main() -> i32 {
+  var a: A = A.Create(1);
+  return 1;
+}

+ 38 - 0
explorer/testdata/destructor/dont_call_in_method.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}
+// AUTOUPDATE: %{explorer} %s
+// CHECK:STDOUT: TEST
+// CHECK:STDOUT: TEST 2
+// CHECK:STDOUT: DESTRUCTOR A 1
+// CHECK:STDOUT: result: 1
+
+package ExplorerTest api;
+
+class B{
+    var n : i32;
+}
+
+class A{
+    destructor[me: Self]{
+        Print("DESTRUCTOR A {0}",me.n);
+    }
+    fn test[me: Self](){
+        Print("TEST");
+    }
+    fn test2[me: Self](){
+        Print("TEST 2");
+    }
+    var n: i32;
+}
+
+fn Main() -> i32 {
+  var a: A = {.n = 1 };
+  a.test();
+  a.test2();
+  return 1;
+}