Destructors
This section describes when and how values are cleaned up in Gruel.
Drop Semantics
When a value's owning binding goes out of scope and the value has not been moved elsewhere, the value is dropped. Dropping a value runs its destructor, if it has one.
A value is dropped exactly once. Values that are moved are not dropped at their original binding site; they are dropped at their final destination.
struct Data { value: i32 }
fn consume(d: Data) -> i32 { d.value }
fn main() -> i32 {
let d = Data { value: 42 };
consume(d) // d is moved, dropped inside consume()
} // d is NOT dropped here (was moved)
Drop Order
When multiple values go out of scope at the same point, they are dropped in reverse declaration order (last declared, first dropped).
fn main() -> i32 {
let a = Data { value: 1 }; // declared first
let b = Data { value: 2 }; // declared second
0
} // b dropped first, then a
Reverse declaration order (LIFO) ensures that values declared later, which may depend on earlier values, are cleaned up first.
Trivially Droppable Types
A type is trivially droppable if dropping it requires no action. Trivially droppable types have no destructor.
The following types are trivially droppable:
- All integer types (
i8,i16,i32,i64,u8,u16,u32,u64) - The boolean type (
bool) - The unit type (
()) - The never type (
!) - Enum types
- Arrays of trivially droppable types
A struct type is trivially droppable if all of its fields are trivially droppable.
// Trivially droppable: all fields are trivially droppable
struct Point { x: i32, y: i32 }
fn main() -> i32 {
let p = Point { x: 1, y: 2 };
p.x // p is trivially dropped (no-op)
}
Types with Destructors
A type has a destructor if dropping it requires cleanup actions. When such a type is dropped, its destructor is invoked.
A struct has a destructor if any of its fields has a destructor, or if the struct has a user-defined destructor.
For a struct with a destructor, fields are dropped in declaration order (first declared, first dropped).
An array type [T; N] has a destructor if its element type T has a destructor.
When an array with a destructor is dropped, each element is dropped in index order (element 0 first, then element 1, and so on).
fn main() -> i32 {
let arr: [String; 3] = ["a", "b", "c"];
0
} // Each String in arr is dropped: arr[0], arr[1], arr[2]
The distinction between "drop order of bindings" (reverse declaration) and "drop order of fields/elements" (declaration/index order) matches C++ and Rust behavior. Bindings use LIFO for dependency correctness; fields and array elements use forward order for consistency with construction order.
Drop Placement
Drops are inserted at the following points:
- At the end of a block scope, for all live bindings declared in that scope
- Before a
returnstatement, for all live bindings in all enclosing scopes - Before a
breakstatement, for all live bindings declared inside the loop
Each branch of a conditional independently drops bindings declared within that branch.
fn example(condition: bool) -> i32 {
let a = Data { value: 1 };
if condition {
let b = Data { value: 2 };
return 42; // b dropped, then a dropped, then return
}
let c = Data { value: 3 };
0 // c dropped, then a dropped
}
Function Parameter Drops
Function parameters that are passed by value (not inout or borrow) are owned by the callee. If such a parameter has a destructor and is not moved within the function body, the parameter is dropped when the function returns.
When a by-value argument with a destructor is passed to a function call, ownership transfers to the callee. The caller does not drop the value at its original scope exit.
struct Data { value: i32 }
drop fn Data(self) {
@dbg(self.value);
}
fn sink(d: Data) -> i32 {
0
} // d is dropped here (owned by callee, not consumed)
fn main() -> i32 {
let d = Data { value: 42 };
sink(d); // ownership transferred to sink, d not dropped in main
0
}
Code Generation
When a non-trivially droppable value is dropped, the compiler generates a call to the value's destructor function.
When a trivially droppable value is dropped, no code is generated. The drop is elided as a no-op.
The distinction between trivially droppable and non-trivially droppable types allows the compiler to avoid generating unnecessary cleanup code for simple types like integers and structs containing only integers.
User-Defined Destructors
A user-defined destructor is declared as an inline method named __drop inside the type's body:
struct TypeName {
// fields...
fn __drop(self) {
// cleanup code
}
}
A user-defined destructor MUST be declared as a method named __drop inside its struct's body. It MUST take exactly one parameter named self (by-value receiver) and return nothing (implicit unit type).
Each struct type MAY have at most one user-defined destructor. A compile-time error is raised if a __drop method is declared twice in the same struct body (caught by the standard duplicate-method check).
Because destructors are declared as methods on the host struct, there is no separate "destructor for an unknown type" error category. A __drop method declared outside of any struct body is rejected by the standard item-position parser.
When a value with a user-defined destructor is dropped, the user-defined destructor runs first, followed by the automatic dropping of any fields that have destructors.
struct FileHandle {
fd: i32,
}
drop fn FileHandle(self) {
// Close the file descriptor
close(self.fd);
}
The drop fn syntax was chosen because it clearly indicates the purpose of the function while being distinct from regular functions and methods. The destructor is not part of any impl block because it has special calling semantics: it is invoked automatically by the compiler when values go out of scope.
Inline Destructor Syntax (ADR-0053)
A destructor MAY also be declared inline inside a struct body as a method named __drop with the signature fn __drop(self):
struct FileHandle {
fd: i32,
fn __drop(self) {
close(self.fd);
}
}
The inline destructor MUST take exactly one parameter (self) and return the unit type. Any extra parameters, non-self receiver, or non-unit return type produces a compile-time error.
A type declared copy or linear MUST NOT declare fn __drop. A Copy type would risk a double-free on bitwise copy; linear values are never implicitly dropped, so the destructor would be unreachable.
A method named __drop CANNOT be invoked directly via method-call syntax (x.__drop()). Destructor invocation is performed solely by drop elaboration.
Semantics for the inline form are identical to the top-level drop fn form: at most one destructor per type; the user-defined destructor runs first, followed by automatic dropping of fields in declaration order.
Destructors are permitted on enums using the same fn __drop(self) syntax. For a value of an enum type with a user destructor, drop elaboration runs the user destructor first, then dispatches on the active variant and drops each owning field of that variant in declaration order.
enum Resource {
File(i32),
Socket(i32),
Unused,
fn __drop(self) {
match self {
Resource::File(fd) => close(fd),
Resource::Socket(fd) => close_socket(fd),
Resource::Unused => {},
}
}
}