From cef93fba5e8f08c8113fb98b9a8a8ac0951e1cba Mon Sep 17 00:00:00 2001 From: pmqtt <51272730+pmqtt@users.noreply.github.com> Date: Fri, 4 Nov 2022 20:13:24 +0100 Subject: [PATCH] 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 Co-authored-by: Geoff Romer --- explorer/interpreter/action.cpp | 36 +++-- explorer/interpreter/action.h | 70 ++++++---- explorer/interpreter/action_stack.cpp | 7 +- explorer/interpreter/heap.cpp | 12 ++ explorer/interpreter/heap.h | 3 + .../interpreter/heap_allocation_interface.h | 5 + explorer/interpreter/interpreter.cpp | 126 ++++++++++++------ .../call_destructor_from_destructor.carbon | 8 +- .../testdata/destructor/destroy_array.carbon | 30 +++++ .../destructor/destroy_different_types.carbon | 27 ++++ .../destroy_inner_destructor.carbon | 38 ++++++ .../destructor/dont_call_in_method.carbon | 38 ++++++ 12 files changed, 311 insertions(+), 89 deletions(-) create mode 100644 explorer/testdata/destructor/destroy_array.carbon create mode 100644 explorer/testdata/destructor/destroy_different_types.carbon create mode 100644 explorer/testdata/destructor/destroy_inner_destructor.carbon create mode 100644 explorer/testdata/destructor/dont_call_in_method.carbon diff --git a/explorer/interpreter/action.cpp b/explorer/interpreter/action.cpp index b1c5952ec5d6..dc7f292dc6ba 100644 --- a/explorer/interpreter/action.cpp +++ b/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 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(Address(id))}); + CARBON_CHECK(success) << "Duplicate definition of " << value_node.base(); + } else { + auto [it, success] = locals_.insert( + {value_node, heap_->arena().New(Address(*allocation_id))}); + CARBON_CHECK(success) << "Duplicate definition of " << value_node.base(); + } +} + void RuntimeScope::Initialize(ValueNodeView value_node, Nonnull 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()) { diff --git a/explorer/interpreter/action.h b/explorer/interpreter/action.h index f9f8826b7940..8ce055a86cb6 100644 --- a/explorer/interpreter/action.h +++ b/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 heap) - : heap_(heap), destructor_scope_(State::Normal) {} + explicit RuntimeScope(Nonnull 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 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 value); // Transfers the names and allocations from `other` into *this. The two @@ -69,31 +66,16 @@ class RuntimeScope { -> std::optional>; // Returns the local values in created order - auto locals() const -> std::vector> { - std::vector> res; - for (const auto& entry : locals_) { - res.push_back(entry.second); - } - return res; + auto allocations() const -> const std::vector& { + 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, std::map> locals_; std::vector allocations_; Nonnull 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 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 lvalue, + Nonnull value) + : Action(Kind::DestroyAction), lvalue_(lvalue), value_(value) {} + + static auto classof(const Action* action) -> bool { + return action->kind() == Kind::DestroyAction; + } + + auto lvalue() const -> Nonnull { return lvalue_; } + + auto value() const -> Nonnull { return value_; } + + private: + Nonnull lvalue_; + Nonnull value_; }; // Action which does nothing except introduce a new scope into the action diff --git a/explorer/interpreter/action_stack.cpp b/explorer/interpreter/action_stack.cpp index a6fb07720ff3..d14ab09f73cf 100644 --- a/explorer/interpreter/action_stack.cpp +++ b/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(*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)); } } } diff --git a/explorer/interpreter/heap.cpp b/explorer/interpreter/heap.cpp index 88f576822a71..717f0b03e53f 100644 --- a/explorer/interpreter/heap.cpp +++ b/explorer/interpreter/heap.cpp @@ -46,6 +46,18 @@ auto Heap::Write(const Address& a, Nonnull v, return Success(); } +auto Heap::GetAllocationId(Nonnull v) const + -> std::optional { + 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 { if (states_[allocation.index_] == ValueState::Dead) { diff --git a/explorer/interpreter/heap.h b/explorer/interpreter/heap.h index cae098200377..ed0e5c3ddbe6 100644 --- a/explorer/interpreter/heap.h +++ b/explorer/interpreter/heap.h @@ -41,6 +41,9 @@ class Heap : public HeapAllocationInterface { auto Write(const Address& a, Nonnull v, SourceLocation source_loc) -> ErrorOr; + auto GetAllocationId(Nonnull v) const + -> std::optional override; + // Put the given value on the heap and mark its state. // Mark UninitializedValue as uninitialized and other values as alive. auto AllocateValue(Nonnull v) -> AllocationId override; diff --git a/explorer/interpreter/heap_allocation_interface.h b/explorer/interpreter/heap_allocation_interface.h index bb6f3811112e..60e54d1312f5 100644 --- a/explorer/interpreter/heap_allocation_interface.h +++ b/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 v) const + -> std::optional = 0; + protected: HeapAllocationInterface() = default; virtual ~HeapAllocationInterface() = default; diff --git a/explorer/interpreter/interpreter.cpp b/explorer/interpreter/interpreter.cpp index 2e2473400c5f..256ccd7706c5 100644 --- a/explorer/interpreter/interpreter.cpp +++ b/explorer/interpreter/interpreter.cpp @@ -89,6 +89,9 @@ class Interpreter { auto StepDeclaration() -> ErrorOr; // State transition for object destruction. auto StepCleanUp() -> ErrorOr; + auto StepDestroy() -> ErrorOr; + // State transition for tuple destruction. + auto StepCleanUpTuple() -> ErrorOr; auto CreateStruct(const std::vector& fields, const std::vector>& values) @@ -818,15 +821,17 @@ auto Interpreter::CallDestructor(Nonnull 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(*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(*method.body()); - method_scope.TransitState(); return todo_.Spawn(std::unique_ptr(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(*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,48 +2012,81 @@ auto Interpreter::StepDeclaration() -> ErrorOr { } } -auto Interpreter::StepCleanUp() -> ErrorOr { +auto Interpreter::StepDestroy() -> ErrorOr { + // 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(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(*value)) { + DestroyAction& destroy_act = cast(act); + if (act.pos() == 0) { + if (const auto* class_obj = + dyn_cast(destroy_act.value())) { + const auto& class_type = cast(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(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(item)) { const auto& class_type = cast(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(*value)) { - const auto& class_type = cast(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(member)) { - const auto& type = var->static_type(); - if (const auto* c_type = dyn_cast(&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(destroy_act.lvalue(), item)); } - act.scope()->TransitState(); + // 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(destroy_act.value())) { + const auto& class_type = cast(class_obj->type()); + const auto& class_dec = class_type.declaration(); + int index = class_dec.members().size() - act.pos(); + if (index >= 0 && index < static_cast(class_dec.members().size())) { + const auto& member = class_dec.members()[index]; + if (const auto* var = dyn_cast(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(destroy_act.lvalue(), *v)); + } + } + } + } + todo_.Pop(); + return Success(); +} + +auto Interpreter::StepCleanUp() -> ErrorOr { + Action& act = todo_.CurrentAction(); + CleanupAction& cleanup = cast(act); + if (act.pos() < cleanup.allocations_count()) { + auto allocation = + act.scope()->allocations()[cleanup.allocations_count() - act.pos() - 1]; + auto lvalue = arena_->New(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(lvalue, *value)); + } + } todo_.Pop(); return Success(); } @@ -2069,6 +2116,9 @@ auto Interpreter::Step() -> ErrorOr { 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: diff --git a/explorer/testdata/destructor/call_destructor_from_destructor.carbon b/explorer/testdata/destructor/call_destructor_from_destructor.carbon index 74bd72accd61..03eed7311062 100644 --- a/explorer/testdata/destructor/call_destructor_from_destructor.carbon +++ b/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; diff --git a/explorer/testdata/destructor/destroy_array.carbon b/explorer/testdata/destructor/destroy_array.carbon new file mode 100644 index 000000000000..7eece6582c07 --- /dev/null +++ b/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; +} diff --git a/explorer/testdata/destructor/destroy_different_types.carbon b/explorer/testdata/destructor/destroy_different_types.carbon new file mode 100644 index 000000000000..a50169586502 --- /dev/null +++ b/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; +} diff --git a/explorer/testdata/destructor/destroy_inner_destructor.carbon b/explorer/testdata/destructor/destroy_inner_destructor.carbon new file mode 100644 index 000000000000..138dba31a037 --- /dev/null +++ b/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; +} diff --git a/explorer/testdata/destructor/dont_call_in_method.carbon b/explorer/testdata/destructor/dont_call_in_method.carbon new file mode 100644 index 000000000000..f0d05acf03f4 --- /dev/null +++ b/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; +}