Skip to content
Kavachकवच

Automatic Memory: How it stays fast

● Advanced6 min

Values look copied, but the compiler moves them, shares them and copies only when it must.

  1. Moved, not copied. When a value is used for the last time, it is handed over instead of copied.
  2. Shared and counted. A large value is shared inside, with a count of who holds it. The compiler removes most count updates while compiling.
  3. Copied only on change. A value is copied only when it is changed while someone else still holds it. If nobody else does, it is changed in place. So text += part or items.push(x) in a loop copies only what is added, not the whole value each time.

Step through what happens in memory, line by line:

▶ let a = [1, 2, 3] let b = a b.push(4) print(a) // a's last use b.push(5)
held by a · count 1
123
step 1 of 5

The list is made once in memory. One variable holds it, so its count is 1.

Its first lines as a program you can run:

fn main() {
    let a = [1, 2, 3]
    let b = a
    b.push(4)
    print(a)
    print(b)
}

The program ran and printed:

kavach run values.kav

[1, 2, 3][1, 2, 3, 4]
values.kav: Kavach code, then what running it prints

Here b.push(4) is the moment the list is really copied: a still holds the old one.

Growing in a loop

Text built up in a loop has one holder, so each += adds to it in place. A copy taken before keeps its old value:

fn main() uses Console {
    let report = ""
    for i in 0..5000 {
        report += "line {i}\n"
    }
    print(report.len())

    let s = "ab"
    const before = s
    s += "c"
    print(s)
    print(before)
}

The program ran and printed:

kavach run grows-in-place.kav

48890abcab
grows-in-place.kav: Kavach code, then what running it prints
tested

Numbers, text and the rest

Not every value is counted. Numbers stay where they are; text, lists and the rest are one object each:

ValuesWhere they live
Numbers, Bool, CharIn place: in a variable, or inside the value that holds them. Never counted, never freed on their own
Text, Bytes, Decimal, BigInt, lists, maps, sets, records, tuples, unions, optionals of these, functions, interface values, Shared, tasks, channels, errorsOne object each, which counts its holders and is freed when the count reaches zero

A holder is anything that keeps a value: a variable, a list or record it is inside, a lambda that uses it, a task. A record a function never shares may never become an object at all: its fields stay in place, like numbers. A program can't tell the difference.

Nested lists and maps

The same holds at every depth. A list of lists, a list three deep, or a map of lists: change the copy anywhere inside, and only the copy changes:

fn main() {
    let a = [[2, 3], [4, 5], [5]]
    let b = a                     // shared: nothing copied yet
    b[1].push(9)                  // b changes; a is still as it was
    print(a)
    print(b)

    let deep = [[[2, 1, 4]], [[5]]]
    let other = deep
    other[0][0][1] = 99
    print(deep)
    print(other)

    let shop: [String: [String]] = [:]
    shop["fruit"] = ["apple", "pear"]
    shop["veg"] = ["leek"]
    let copy = shop
    copy["veg"] = ["leek", "kale"]
    print(shop)
    print(copy)
}

The program ran and printed:

kavach run nested.kav

[[2, 3], [4, 5], [5]][[2, 3], [4, 5, 9], [5]][[[2, 1, 4]], [[5]]][[[2, 99, 4]], [[5]]]["fruit": ["apple", "pear"], "veg": ["leek"]]["fruit": ["apple", "pear"], "veg": ["leek", "kale"]]
nested.kav: Kavach code, then what running it prints

Only what changes is copied. When b[1].push(9) runs, the inner list [4, 5] is copied first, and 9 goes onto the copy. Then the outer list is copied, but only as a list of pointers to the same inner lists. [2, 3] and [5] stay shared by a and b. If nothing else held b, nothing would be copied at all: it would change in place.

A list holds one type, so [2, 3, [4, 5, [2, 1, 4]], 5], numbers and lists mixed, is refused:

fn main() {
    const xs = [2, 3, [4, 5, [2, 1, 4]], 5]
    print(xs)
}

The compiler refused the program:

kavach run mixed.kav

error: expected `Int`, found `[Int]`: a list holds one type; to mix numbers and lists, use a union (2.6) or `Json` (2.12)
mixed.kav: Kavach code, then what running it prints

To nest numbers and lists freely, name the two kinds of item in a union that holds a list of itself. Data from outside, such as JSON, comes as a Json value, which works the same way:

type Item = Num(n: Int) | Many(items: [Item])

fn total(item: Item) -> Int {
    match item {
        Item.Num(n) => n,
        Item.Many(items) => items.sum(i => total(i)),
    }
}

fn main() {
    // [2, 3, [4, 5, [2, 1, 4]], 5]
    const inner = Item.Many(items: [Item.Num(n: 2), Item.Num(n: 1), Item.Num(n: 4)])
    const middle = Item.Many(items: [Item.Num(n: 4), Item.Num(n: 5), inner])
    const xs = Item.Many(items: [Item.Num(n: 2), Item.Num(n: 3), middle, Item.Num(n: 5)])
    print(total(xs))
}

The program ran and printed:

kavach run nested-mixed.kav

26
nested-mixed.kav: Kavach code, then what running it prints