Memory

One fixed arena, a free list per size class, and a reference count per scope that the compiler injects. There is no garbage collector and no malloc: an object is born at new and handed back the moment the last name holding it stops holding it.


The arena #

A block of 4 MiB in __bss, handed out 16-byte aligned. A free list per size class answers first — 16, 32, … up to 256 bytes — and only when the list for the size is empty does the bump pointer move. A block bigger than 256 bytes has no list and is not recycled; when the bump pointer would leave the block, the program panics with arena exhausted.

The consequence worth writing down: a loop that churns objects reuses one block forever.

// expect-exit: 42
#include "rt.tk"

i64 dtors = 0;

class Cell {
    public i64 v;

    public Cell(i64 x) {
        v = x;
    }

    ~Cell() {
        dtors = dtors + 1;
    }
}

i64 main() {
    i64 i = 0;
    loop {
        Cell c = new Cell(i);                    // born and released every iteration
        i = i + 1;
        if (i >= 1000000) break;
    }
    if (dtors != 1000000) return 1;
    if (rt_live() != 0) return 2;
    if (rt_peak() > 4096) return 3;              // one block, reused a million times
    return 42;
}

The object #

wordholds
+0the vtable pointer
+8the reference count
+16the fields, the base class's first

and the vtable itself starts with the class's own release function at +0 and its interface table (or 0) at +8, which is how the runtime frees an object whose class it knows nothing about. A T[] and a delegate value are objects of the same shape.

rt_alloc hands out zeroed bytes, so a field nobody assigned reads as 0 and a reference field reads as null.


What owns what #

the holderowns
a local of counted typereleases at the end of its block, and on the way out of a break, continue or return that leaves it
a field of counted typereleased when the object holding it dies, and when the field is overwritten
an element of a T[]the same
a capture by value in a closurereleased with the closure
a global, a static field, a Singletonnever released: it is a root
a parameter of class typeborrows — it carries no count of its own, and reassigning it is refused

"Counted" means a class, an interface, a delegate or a T[]. A value produced and handed straight to a call — f(new Cell(1)) — has no owner, so it is parked and released when the statement that built it ends, which is C#'s and C++'s rule for a temporary. At most 64 such temporaries may be alive in one statement.

A function that returns a counted value hands the caller a reference it already owns: the caller's slot takes it with no extra increment, and releases it in the ordinary way.

The destructor runs first #

At zero, the class's release function runs the destructor, then releases the class-typed fields, then hands the block back. A chain runs the derived class's destructor before the base's.

// expect-exit: 42
#include "rt.tk"

i64 dtors = 0;

class Cell {
    public i64 v;

    public Cell(i64 x) {
        v = x;
    }

    ~Cell() {
        dtors = dtors + 1;
    }
}

class Node {
    public Cell head;

    public Node(i64 x) {
        head = new Cell(x);
    }
}

i64 early(i64 want) {
    Cell outer = new Cell(1);
    {
        Cell inner = new Cell(2);
        if (inner.v == want) return outer.v;     // both released on the way out
    }
    return 0;
}

i64 main() {
    {
        Node n = new Node(5);
        if (rt_live() != 2) return 1;            // the node and the cell it built
        n.head = new Cell(6);                    // the old cell has no owner left
        if (dtors != 1) return 2;
        if (rt_live() != 2) return 3;
    }
    if (rt_live() != 0) return 4;
    if (dtors != 2) return 5;

    dtors = 0;
    if (early(2) != 1) return 6;
    if (dtors != 2) return 7;
    if (rt_live() != 0) return 8;

    return 42;
}

What is not reclaimed #

Declared debt, not silence — rt_live() counts all of it, so a program that mixes these with classes sees a floor above zero rather than a wrong answer.

why
a struct allocationa struct has no vtable, so there is no release function to reach and no count to keep
a static field of class typeit holds its reference correctly, and lives for the whole run
a global, and a global T[]a root by construction
a Singleton servicea root by design (di.md)

Reading the numbers #

callanswers
rt_live()blocks handed out and not yet given back
rt_used()how far the bump pointer has moved
rt_peak()the high-water mark of the bump pointer

They are ordinary functions of the runtime (runtime.md), and the fixtures use them as the oracle of every reclaim claim on this page.


Panics #

A panic writes teko: <cause> to standard error and exits 70. There is no recovery and no handler. panic("...") is a surface function a program may call itself.

messagewhen
call through a null delegatea delegate value that is null is called
index into a null arrayan index into a T[] that is null
index below zero into an arraya negative index into a T[]
index past the end of an arrayan index at or past Length
a negative array lengthnew T[n] with n < 0
interface dispatch on a class with no interface tablean interface call on an object whose class declares none
interface not implemented by this classthe table has no row for that interface
arena exhaustedthe 4 MiB block cannot serve the allocation
allocation of a negative sizert_alloc was asked for a negative size
reference count below zeroa release too many: a bug, not a program condition
too many temporaries of class type in one statementmore than 64 unowned values in one statement
// expect-exit: 70
#include "rt.tk"

class Cell {
    public i64 v;
}

i64 main() {
    Cell c = new Cell;
    if (c.v == 0) panic("nothing to do");
    return 42;
}

Limits #

limitvalue
the arena4 MiB, fixed
a recycled blockat most 256 bytes; anything larger is dropped when freed
temporaries of counted type in one statement64
stores into a slot of counted type, per unit128

Edit this page