Defer and destructors

defer

defer defers the execution of a statement to the exits of the current scope. This is useful for example when we need to perform some cleanup before returning. This avoids the mistake of forgetting to add necessary cleanup calls when we add a new return statement. defer is not allowed in constructors.

int main() {
    var p = allocateValue<int>(0); // allocate some resource
    defer deallocate(p); // runs before each return below

    if *p != 0 {
        return 2; // deallocate(p) runs before this return too
    }

    return 0; // ... and before this return
}

Destructors

A struct can declare a destructor, written ~ followed by the struct name. It runs automatically when an instance goes out of scope, which makes it a good place to release resources:

struct Logger {
    string prefix;

    ~Logger() {
        println(prefix, " closed");
    }
}

void main() {
    var _log = Logger(prefix = "app");
    println("working"); // prints "working", then "app closed" at scope exit
}

A temporary - a struct value that isn't stored anywhere - is destroyed at the end of the statement that creates it:

struct Logger {
    string prefix;

    ~Logger() {
        println(prefix, " closed");
    }
}

void main() {
    _ = Logger(prefix = "tmp"); // prints "tmp closed" at the end of this statement
    println("working");
}

A value moved into a variable, argument, or return value belongs to its new home instead, so its destructor still runs exactly once, when the owner dies.

A destructor can also be invoked explicitly with .deinit(). This consumes the value like a move: the destructor does not run again at scope exit, so the value cannot be used afterwards unless it is reassigned first.

Explicit destruction is for values the compiler won't destroy, such as elements in manually allocated memory.

Deinit through pointers

Destroying through a local pointer is tracked: when p points at a stack variable, (*p).deinit() consumes it like a direct deinit. Through a function parameter or struct member the compiler cannot know the target, so it stays unchecked: if the pointer reaches a stack variable that later dies at scope exit, the value is destroyed twice. Only deinit through such pointers when nothing else owns the target, as container internals do with heap memory.

This is how containers destroy their elements:

void main() {
    var buffer = allocate<List<int>>(1);

    var slot = &buffer[0];
    slot.init(List([1, 2, 3]));
    println(buffer[0]); // prints "[1, 2, 3]"

    buffer[0].deinit(); // destroys the element; nothing will destroy it again
    deallocate(buffer);
}

The dual of .deinit() is .forget(): it also consumes the value, but runs no destructor at all, neither now nor at scope exit. It is for bytes already moved elsewhere by hand, such as elements copied out of a temporary array into a container. Like .deinit(), the value cannot be used afterwards.

Manual destruction

Fields are destroyed automatically when the destructor finishes. Marking a field @manuallyDestroy opts it out: the destructor must then destroy the field explicitly with .deinit(). This is for storage that is only partially initialized, such as the inline buffer of SmallList, where destroying every slot would destroy uninitialized memory.

Unchecked manual-field destruction

A struct with a manually-destroyed owning field but no destructor is a compile error. The compiler does not check that the destructor actually destroys the field; forgetting .deinit() leaks it silently.

A local variable marked @manuallyDestroy stays alive when its scope ends, including on return.

Scratch-variable traps

Assignment destroys the previous value, so assigning over uninitialized scratch would destroy uninitialized memory. This is for storage that must use the value's alignment and must not own the bytes, such as swapping two values by copying their representation. A uint8 buffer is aligned as a byte, so it is the wrong scratch for that. The compiler does not check that the variable is destroyed; forgetting .deinit() on a value that should be destroyed leaks it.

struct Buffer {
    @manuallyDestroy
    List<int> items;

    Buffer() {
        items = List([1, 2, 3]);
    }

    ~Buffer() {
        items.deinit();
    }
}

void main() {
    var buffer = Buffer();
    println(buffer.items.size()); // prints 3
}
struct Probe {
    int id;

    ~Probe() {
        println("drop");
    }
}

void main() {
    {
        var tracked = Probe(id = 1);
        println(tracked.id); // prints 1
    } // prints "drop"

    @manuallyDestroy
    var scratch = Probe(id = 2);
    println(scratch.id); // prints 2
    println("done"); // prints "done"
}