Destructors

This section describes when and how values are cleaned up in Rue.

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).

The distinction between "drop order of bindings" (reverse declaration) and "drop order of fields" (declaration order) matches C++ and Rust behavior. Bindings use LIFO for dependency correctness; fields use declaration 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 return statement, for all live bindings in all enclosing scopes
  • Before a break statement, 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
}

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 using the drop fn syntax:

drop fn TypeName(self) {
    // cleanup code
}

A user-defined destructor must be declared at the top level, outside of any impl block. It must take exactly one parameter named self and return nothing (implicit unit type).

Each struct type may have at most one user-defined destructor. A compile-time error is raised if multiple destructors are declared for the same type.

A user-defined destructor can only be declared for a struct type that is defined in the same compilation unit. A compile-time error is raised if the destructor references an unknown type or a non-struct type.

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.