ktm v0.28.3

defer and scoped

defer runs a statement when its block is left, however it is left: the end of the block, a return, a break, or an error passing through. scoped in front of a declaration frees the variable the same way. Both keep the clean-up next to the code that needs it, so it can't be forgotten on one of the ways out. This chapter covers defer, the order defers run in, what they see, and scoped.

defer

defer statement; registers the statement to run when the enclosing { } block is left:

i32 main() {
    defer printf("last\n");
    printf("first\n");
    printf("second\n");
    return 0;
}
first
second
last

A block after defer defers all of it:

import mem;

void work() {
    u8 *buf = mem.alloc(64);
    defer {
        mem.free(buf);
        printf("freed\n");
    }
    printf("working\n");
}

i32 main() {
    work();
    return 0;
}
working
freed

The usual pattern is to defer the release right after acquiring, so the two sit together:

import mem;

i64 sumSquares(i64 n) {
    i64 *squares = mem.alloc(n * sizeof(i64));
    defer mem.free(squares);
    for (i64 i = 0; i < n; i++) {
        squares[i] = i * i;
    }
    i64 total = 0;
    for (i64 i = 0; i < n; i++) {
        total += squares[i];
    }
    return total;
}

i32 main() {
    printf("%d\n", sumSquares(5));
    return 0;
}
30

Every way out

A deferred statement runs at the end of its block, on a return, on break and continue, on a goto out of the block, and when a thrown error passes through:

enum Err { negative };

i64 check(i64 n) {
    defer printf("  check(%d) done\n", n);
    if (n < 0) {
        throw(Err.negative);
    }
    if (n == 0) {
        return 0;
    }
    return n * 2;
}

i32 main() {
    printf("%d\n", check(5));
    printf("%d\n", check(0));
    try {
        check(-1);
    } catch {
        printf("caught\n");
    }
    return 0;
}
  check(5) done
10
  check(0) done
0
  check(-1) done
caught

With several early returns, one defer replaces a clean-up before each of them:

import mem;

i64 firstEven(i64[] input) {
    i64 *copy = mem.alloc(input.len * sizeof(i64));
    defer {
        mem.free(copy);
        printf("(copy freed)\n");
    }
    for (i64 i = 0; i < input.len; i++) {
        copy[i] = input[i];
        if (copy[i] % 2 == 0) {
            return copy[i];
        }
    }
    return -1;
}

i32 main() {
    i64 a[4] = {3, 5, 8, 9};
    i64 b[2] = {1, 3};
    printf("%d\n", firstEven(a));
    printf("%d\n", firstEven(b));
    return 0;
}
(copy freed)
8
(copy freed)
-1

The block is the scope

A defer belongs to the { } block it is written in, not to the function. In a loop body it runs at the end of every iteration, continue included:

i32 main() {
    for (i64 i = 0; i < 3; i++) {
        defer printf("end of %d\n", i);
        if (i == 1) {
            continue;
        }
        printf("body %d\n", i);
    }
    return 0;
}
body 0
end of 0
end of 1
body 2
end of 2

A plain block limits it too:

i32 main() {
    printf("before\n");
    {
        defer printf("inner block left\n");
        printf("inside\n");
    }
    printf("after\n");
    return 0;
}
before
inside
inner block left
after

break leaves the loop's body, so its defers run first:

i32 main() {
    for (i64 i = 0; i < 10; i++) {
        defer printf("done with %d\n", i);
        if (i == 2) {
            break;
        }
    }
    printf("loop over\n");
    return 0;
}
done with 0
done with 1
done with 2
loop over

Order

Several defers in a block run in reverse: the last one registered runs first. Inner blocks finish before outer ones.

i32 main() {
    defer printf("outer\n");
    {
        defer printf("1st registered, runs last\n");
        defer printf("2nd registered, runs first\n");
    }
    printf("between\n");
    return 0;
}
2nd registered, runs first
1st registered, runs last
between
outer

Reverse order is what nested resources need: what was acquired last is released first.

void open(u8 *what) {
    printf("open %s\n", what);
}

void close(u8 *what) {
    printf("close %s\n", what);
}

i32 main() {
    open("database");
    defer close("database");
    open("transaction");
    defer close("transaction");
    printf("work\n");
    return 0;
}
open database
open transaction
work
close transaction
close database

What a defer sees

The deferred statement runs later, as written: it sees the values variables have when the block ends, not when the defer was written.

i32 main() {
    i64 x = 1;
    {
        defer printf("x is %d\n", x);
        x = 2;
    }
    return 0;
}
x is 2

return evaluates its value before the defers run, so a function can return something its defer then frees, as long as it isn't a pointer into what is freed:

import mem;

i64 first(i64 n) {
    i64 *buf = mem.alloc(n * sizeof(i64));
    defer mem.free(buf);
    buf[0] = 42;
    return buf[0];
}

i32 main() {
    printf("%d\n", first(4));
    return 0;
}
42

Only if reached

A defer is registered when execution reaches it. One written after an early return doesn't run on that return:

void f(bool early) {
    if (early) {
        return;
    }
    defer printf("deferred\n");
    printf("late part\n");
}

i32 main() {
    f(true);
    f(false);
    return 0;
}
late part
deferred

So a defer that frees something goes after the check that it was acquired:

import mem;

bool fill(i64 n) {
    if (n <= 0) {
        return false;
    }
    u8 *buf = mem.alloc(n);
    defer {
        mem.free(buf);
        printf("freed %d\n", n);
    }
    printf("filled %d\n", n);
    return true;
}

i32 main() {
    fill(0);
    fill(16);
    return 0;
}
filled 16
freed 16

What a deferred statement can't do

A deferred statement can't leave itself with return, break, continue or goto (a loop inside it keeps its own break):

does not compilei32 main() {
    for (i64 i = 0; i < 2; i++) {
        defer {
            break;
        }
    }
    return 0;
}
`break` can't leave a deferred statement

scoped

scoped in front of a declaration means defer name.free(); right after it: the variable is freed when its block ends.

import str;

i32 main() {
    scoped String name = str.from("Ada");
    name.append(" Lovelace");
    printf("%.*s\n", name.len, name.ptr);
    return 0;
}
Ada Lovelace

It works for a String, any slice the library allocated, and any struct with a free() method that takes no arguments, or a pointer to such a struct:

import str;

struct Conn { i64 fd; };

void Conn.free() {
    printf("closing %d\n", self.fd);
}

i32 main() {
    scoped String name = str.from("server");
    scoped u8[][] words = str.split("a b c", " ");
    scoped Conn c = {7};
    printf("%.*s has %d words\n", name.len, name.ptr, words.len);
    return 0;
}
server has 3 words
closing 7

The library's containers all have free(), so scoped is the usual way to hold them:

import map, set, vec;

i32 main() {
    scoped vec.Vec<i64> v = vec.new<i64>();
    scoped map.Map<u8 *, i64> m = map.new<u8 *, i64>();
    scoped set.Set<i64> s = set.new<i64>();
    v.push(1);
    v.push(2);
    m.set("a", 1);
    s.add(1);
    printf("%d %d %d\n", v.len, m.len, s.len);
    return 0;
}
2 1 1

Like a defer, a scoped variable is freed on every way out of its block, a thrown error included:

struct Res { u8 *name; };

void Res.free() {
    printf("released %s\n", self.name);
}

enum Err { bad };

void use() {
    scoped Res r = {"lock"};
    throw(Err.bad, "failed while holding it");
}

i32 main() {
    try {
        use();
    } catch (Error e) {
        printf("caught: %s\n", e.message);
    }
    return 0;
}
released lock
caught: failed while holding it

Without a value

A scoped variable can be declared without a value and filled in later. Until then it is empty (every local starts zeroed), and freeing an empty String, slice or container does nothing:

import str;

i32 main() {
    scoped String s;
    if (s.len == 0) {
        s = str.from("filled later");
    }
    printf("%.*s\n", s.len, s.ptr);
    return 0;
}
filled later

A struct marked @json or @yaml that has no free() of its own is freed with the freeDecoded() those attributes generate (see Attributes), so a value decoded later is freed too:

import json;

@json struct User {
    String name;
    i64 age;
};

i32 main() {
    scoped User u;
    u = json.decode<User>("{\"name\": \"ada\", \"age\": 36}");
    printf("%.*s is %d\n", u.name.len, u.name.ptr, u.age);
    return 0;
}
ada is 36

A scoped pointer with no value is null, and is freed only if it holds something by the end of its block.

scoped and returning

Returning a scoped variable is an error: it would be freed as the function returns. A function that hands a value to its caller declares it without scoped, and the caller decides:

does not compileimport str;

String make() {
    scoped String s = str.from("x");
    return s;
}

i32 main() {
    return 0;
}
is declared `scoped`, so it is freed when this function returns
import str;

String greeting(u8[] who) {
    String s = str.format("hello, %.*s", who.len, who.ptr);
    return s;
}

i32 main() {
    scoped String g = greeting("ada");
    printf("%.*s\n", g.len, g.ptr);
    return 0;
}
hello, ada

A type with no free() can't be declared scoped:

does not compilestruct Point { i64 x, y; };

i32 main() {
    scoped Point p = {1, 2};
    return 0;
}
has no `free()` that takes no arguments

scoped rules

Rules