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- The last read of
rowsis thesum: it gets the list, and lets go of it when it is done. Nothing else holds it, so it is freed there, andsendruns 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
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
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
_ 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
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
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
- 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:
dropruns 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
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
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
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
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
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
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
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| You want | Write | Why |
|---|---|---|
| Free a big value early | just stop using it | it is freed after its last use |
| Free it, then keep the name | rows = [] | 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 point | put it in an inner { … } block | a value with drop lives to the end of its block, then drop runs |
| Free many small values at once | arena { … } | everything made inside goes when the block ends; only its result is copied out |
| Manage raw memory yourself, like C | free(p) inside unsafe | nothing 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.