Skip to content
Kavachकवच

Automatic Memory: Freed immediately

Also known as: delete del free a variable null undefined dispose destructor drop raii release memory early unset

● Advanced7 min

A value is freed when its last use ends: no garbage collector, no pauses. A type can run its own cleanup with Drop.

Because nothing waits for a collector, files, sockets and locks close at a known point, not "eventually". Long-running servers keep steady response times, and games keep steady frame times.

How it knows when

Nothing runs in the background to look for memory that is no longer used. While it compiles, the compiler finds the last place each variable is read, and makes that read hand the value over instead of sharing it. Each value counts who holds it. When the last holder is done with it, the count reaches zero and the memory is freed at that moment. So a value nothing else holds is freed right after its last use:

type Row {
    amount: Int
}

fn load_rows() -> [Row] {
    [Row(amount: 120), Row(amount: 80), Row(amount: 300)]
}

fn send(total: Int) {
    print("sent {total}")
}

fn report() {
    const rows = load_rows()        // say this is 500 MB
    const total = rows.sum(r => r.amount)
    // `rows` is freed here: nothing reads it after this line
    send(total)                     // runs without the 500 MB
}

fn main() {
    report()
}

The program ran and printed:

kavach run last-use.kav

sent 500
last-use.kav: Kavach code, then what running it prints
  • The last read of rows is the sum: it gets the list, and lets go of it when it is done. Nothing else holds it, so it is freed there, and send runs without it.
  • A value that something else still holds, such as a copy kept in a list, is freed when its last holder is done with it.
  • Anything a function still holds when it returns is let go there.
  • The compiler works out where values are handed over; the count, which decides the moment memory is freed, is kept while the program runs. That is why a copy costs nothing until one of them is changed.

Cleanup of your own: Drop

A type that holds something to give back, such as a file, a connection or a lock, implements Drop. Its drop method runs when the block holding the value ends:

type Door: Drop {
    name: String

    fn drop(self) uses Console {
        print("closing {self.name}")
    }
}

fn visit() {
    const d = Door(name: "kitchen")
    print("inside {d.name}")
}

fn main() {
    visit()
    print("back in main")
}

The program ran and printed:

kavach run drop.kav

inside kitchenclosing kitchenback in main
drop.kav: Kavach code, then what running it prints

When a block holds several, the last one made goes first, as with stacked plates:

type Door: Drop {
    name: String

    fn drop(self) uses Console {
        print("closing {self.name}")
    }
}

fn main() {
    const front = Door(name: "front")
    const back = Door(name: "back")
    print("both open")
}

The program ran and printed:

kavach run drop-order.kav

both openclosing backclosing front
drop-order.kav: Kavach code, then what running it prints

A value with drop lives until the end of its block even when its name is never read again, so a guard protects everything after it:

type Guard: Drop {
    name: String

    fn drop(self) uses Console {
        print("released {self.name}")
    }
}

fn work() {
    const g = Guard(name: "data")
    print("working")
    print("still guarded")
}

fn main() {
    work()
    print("done")
}

The program ran and printed:

kavach run drop-guard.kav

workingstill guardedreleased datadone
drop-guard.kav: Kavach code, then what running it prints

_ names nothing, so a value given to it drops at the end of its statement, like a value made without a name. That is almost never what was meant, so a warning says so. A name that starts with _, such as _guard, keeps the value to the end of the block:

type Lock: Drop {
    name: String

    fn drop(self) uses Console {
        print("unlocked {self.name}")
    }
}

fn main() {
    const _ = Lock(name: "front door")
    const _guard = Lock(name: "back door")
    print("working")
}

The compiler warned, then the program ran:

kavach run drop-underscore.kav

warning: `_` names nothing, so this value drops here, at the end of the statement: name it `_guard` to keep it to the end of the block (spec 6.1)unlocked front doorworkingunlocked back door
drop-underscore.kav: Kavach code, then what running it prints

Leaving a block early, with return, break, continue or fail, drops its values right then, the last made first, as at its end:

type Lock: Drop {
    name: String

    fn drop(self) uses Console {
        print("unlocked {self.name}")
    }
}

type StockError = Empty

fn take(stock: Int) -> Int ! StockError {
    const _shelf = Lock(name: "shelf")
    if stock == 0 {
        fail StockError.Empty
    }
    print("taking one")
    stock - 1
}

fn main() {
    print(take(3) catch _ => -1)
    print(take(0) catch _ => -1)
}

The program ran and printed:

kavach run drop-early.kav

taking oneunlocked shelf2unlocked shelf-1
drop-early.kav: Kavach code, then what running it prints

A union can implement Drop too, with drop in an extend: it runs for every variant. So can a generic type, type Box<T>: Drop, where every instance runs the same drop:

type Seat: Drop = Booked(name: String) | Empty

extend Seat {
    fn drop(self) uses Console {
        match self {
            Seat.Booked(name) => print("released the seat of {name}"),
            Seat.Empty => print("nothing to release"),
        }
    }
}

fn main() {
    const _first = Seat.Booked(name: "Asha")
    const _second = Seat.Empty
    print("the show starts")
}

The program ran and printed:

kavach run drop-union.kav

the show startsnothing to releasereleased the seat of Asha
drop-union.kav: Kavach code, then what running it prints
  • A value made without a name drops at the end of its statement.
  • Giving a variable a new value drops the old one there.
  • Copies are one value: drop runs once, when the last holder's block ends.
  • A panic runs no drop. In the main task it ends the program; in another task, or in an agent's handler, it stops just that, and what it held is never dropped.

A value bound by if let drops at the end of the if let's block, before the next statement:

type Note: Drop {
    name: String

    fn drop(self) uses Console {
        print("freed {self.name}")
    }
}

fn find(name: String) -> Note? {
    if name.len() > 0 {
        return Note(name: name)
    }
    none
}

fn main() {
    if let n = find("receipt") {
        print("found {n.name}")
    }
    print("after the if let")
}

The program ran and printed:

kavach run drop-if-let.kav

found receiptfreed receiptafter the if let
drop-if-let.kav: Kavach code, then what running it prints

Who holds a value

Freeing can't be seen in what a program prints, but drop can. Each sample here uses a Note whose drop prints freed …; a value with drop lives to the end of the block of its last holder.

An inout parameter is a copy while the call runs, so the caller's variable still holds the old value. When the function gives it a new value, the old one goes when the call returns:

type Note: Drop {
    name: String

    fn drop(self) uses Console {
        print("freed {self.name}")
    }
}

fn renew(inout n: Note) {
    n = Note(name: "second")
    print("renew: new value given")
}

fn main() {
    let n = Note(name: "first")
    renew(n)
    print("back in main: {n.name}")
}

The program ran and printed:

kavach run drop-inout.kav

renew: new value givenfreed firstback in main: secondfreed second
drop-inout.kav: Kavach code, then what running it prints

A lambda that uses a value holds a copy of it, so the value lives as long as the lambda's block, not only to its last call. An interface value made from one holds it the same way:

type Note: Drop {
    name: String

    fn drop(self) uses Console {
        print("freed {self.name}")
    }
}

fn later(n: Note) -> fn() -> String {
    () => n.name
}

fn main() {
    const get = later(Note(name: "captured"))
    print("lambda says {get()}")
    print("end of main")
}

The program ran and printed:

kavach run drop-lambda.kav

lambda says capturedend of mainfreed captured
drop-lambda.kav: Kavach code, then what running it prints
interface Named {
    fn name(self) -> String
}

type Note: Drop, Named {
    text: String

    fn name(self) -> String {
        self.text
    }

    fn drop(self) uses Console {
        print("freed {self.text}")
    }
}

fn main() {
    const n: Named = Note(text: "boxed")
    print("named {n.name()}")
    print("end of main")
}

The program ran and printed:

kavach run drop-interface.kav

named boxedend of mainfreed boxed
drop-interface.kav: Kavach code, then what running it prints

Nested lambdas are one more holder each. The inner lambda takes its copy from the outer one, and the value goes when the last of them is done:

type Note: Drop {
    name: String

    fn drop(self) uses Console {
        print("freed {self.name}")
    }
}

fn outer(n: Note) -> fn() -> fn() -> String {
    () => () => n.name          // the inner lambda holds a copy too
}

fn main() {
    const make = outer(Note(name: "deep"))
    const inner = make()
    print("inner says {inner()}")
    print("end of main")
}

The program ran and printed:

kavach run drop-nested.kav

inner says deepend of mainfreed deep
drop-nested.kav: Kavach code, then what running it prints

A list holds each of its items. An item lives as long as the list holds it, and goes with the list, in the order of the list:

type Note: Drop {
    name: String

    fn drop(self) uses Console {
        print("freed {self.name}")
    }
}

fn main() {
    let notes = [Note(name: "first")]
    notes.push(Note(name: "second"))     // lives as long as the list holds it
    print("{notes.len()} notes")
    print("end of main")
}

The program ran and printed:

kavach run drop-list.kav

2 notesend of mainfreed firstfreed second
drop-list.kav: Kavach code, then what running it prints

Shared is one cell, and every copy of it, in a variable, a lambda, a list or a task, is a holder. The cell and its value go when the last holder lets go. Like a value with drop, a Shared lives until the end of the block that holds it. A cycle through Shared is never freed, and is reported: see Shared State.

type Note: Drop {
    name: String

    fn drop(self) uses Console {
        print("freed {self.name}")
    }
}

fn show(s: Shared<Note>) {
    const t = s                 // one more holder of the same cell
    print("show sees {t.get().name}")
}

fn main() {
    const s = Shared(Note(name: "scoreboard"))
    show(s)
    print("show ended; s still holds it")
    print("end of main")
}

The program ran and printed:

kavach run drop-shared.kav

show sees scoreboardshow ended; s still holds itend of mainfreed scoreboard
drop-shared.kav: Kavach code, then what running it prints

Deleting a variable

There is no delete (C++), del (Python) or = null (JavaScript). The compiler already frees a value at the earliest safe moment, so a delete x would only add a way to make mistakes, such as using x after deleting it, with nothing gained. To free memory sooner, stop using the value, give the variable a new value, or end the block that holds it:

fn load_rows() -> [Int] {
    [4, 8, 15, 16, 23, 42]
}

fn main() {
    let rows = load_rows()          // a large list
    const total = rows.len()
    rows = []                       // the old list is freed here
    print(total)
}

The program ran and printed:

kavach run no-delete.kav

6
no-delete.kav: Kavach code, then what running it prints
You wantWriteWhy
Free a big value earlyjust stop using itit is freed after its last use
Free it, then keep the namerows = []a new value drops the old one at once, unless something else still holds it
Clean up a file, socket or lock at a known pointput it in an inner { … } blocka value with drop lives to the end of its block, then drop runs
Free many small values at oncearena { … }everything made inside goes when the block ends; only its result is copied out
Manage raw memory yourself, like Cfree(p) inside unsafenothing is checked, as in C

A value with drop is the one exception to "freed after its last use": it lives until its block ends, even after its last use, so a guard such as a lock stays held while the code below it runs.