Memory

Memory errors represent a large proportion of all programming errors. The pitfalls are well-known:

  • Buffer overflow: out-of-bounds writes
  • Dangling pointer: pointer has been freed but still has its address
    • Also double free()
  • Invalid free()
  • Dereferencing a null pointer
  • Reading uninitialized memory/variables
  • Losing track of memory (memory leak)

Ki aims to resolve all these issues, so that a program that compiles has none of these problems.

Uninitialized Reads

Ki simply keeps track of what is initialized and what isn't. Attempting to use a variable before it's initialized is an easily detected error. However, bounds-checking is more complicated. Any index-based access to memory must be done with an index whose range cannot exceed its bounds. For example:

fn main() {
    var week_days: array([
        "Sunday"
        "Monday"
        "Tuesday"
        "Wednesday"
        "Thursday"
        "Friday"
        "Saturday"
    ])

    for i in [0, week_days.length) {
        refval week_day: week_days[i]

        echo(week_day)
    }
}

...of course, normally you would use for week_day in week_days {...

Use after free()

The following code typifies this problem:

void player_move(Player *p) {
    p->x += 14;

    if (map_is_lava(p->x, p->y))
        free(p);
}

void player_shoot(Player *p) {
    int enemy_count = get_enemy_count();

    p->ammo--;

    for (int i = 0; i < enemy_count; i++) {
        if (enemies[i].x == p->x) {
            free(&enemies[i]);
        }
    }
}

int main(void) {
    Player *p = calloc(1, sizeof(Player));

    player_move(p);
    player_shoot(p);

    return EXIT_SUCCESS;
}

There are a number of problems with this snippet. The first is the most obvious: the use after free() (dangling pointer). If the player fell in the lava, player_move calls free() on the player. Everything that uses p afterwards invokes undefined behavior.

The other problem is that player_shoot calls free() on enemies when the player shoots them. This function also leaves dangling pointers throughout the enemies array.

Finally, there are two potential integer overflow bugs. The first is in player_move: the line p->x += 14 is unbounded and, given time, will overflow. The second is in player_shoot, at the line p->ammo--.

Let's try this example in Ki, with a little more backstory:

fn move_players(Map &map) {
    for (&player! in map.players) {
        p.x != 14;
  
        if (map.is_lava(p.x, p.y)) {
            player.health = 0
        }
    }
}

fn player_shoot(Player &p!) {
    val enemy_count = get_enemy_count

The Ki Memory Model

Four pointer types:

  • val [identifier]: *[initializer] (immutable unique pointer)
  • var [identifier]: *[initializer] (mutable unique pointer)
  • refval (immutable reference to a stack-allocated value)
  • refvar (mutable reference to a stack-allocated value)
  • sharedval (immutable shared pointer)
  • sharedvar (mutable shared pointer)
  • weakval (immutable weak pointer)
  • weakval (mutable weak pointer)

References

References are to stack-allocated memory only. Otherwise they function exactly like weak pointers that must be checked before use.

The only exception is a reference to a unique pointer, which cannot be converted to a shared pointer inside the required with block.

Unique pointers will not be deallocated as long as they are in scope, but creating a reference to them means that they can be accessed outside their scope. This means code could access that memory via the reference after it was deallocated elsewhere. Avoiding this requires locking the memory, which is what shared pointers do.

Restrictions

In order to preserve safety, there are a number of restrictions on using memory.

References may not be passed to async functions

Passing a reference to an async function could possibly lead to use-after-free errors.

References may not be stored

Storing a reference in a struct/type may also lead to use-after-free errors.

References may not be return values

Generally this is to prevent returning a reference to a stack-allocated variable that immediately goes out of scope.

Mutable paths to memory must be unique

At any given time, there may be only a single mutable path to memory. This avoids concurrent modification bugs. Concordantly:

  • Mutable pointer types may only either be copied to an immutable type, moved to an immutable type, or moved to a mutable type.

Aliases

Now, aliases are something I really like the idea of. You might not want to type @globals.players[consoleplayer].left_arm.hand.thumb all the time, so there should be something like alias thumb: @globals.players[consoleplayer].left_arm.hand.thumb that doesn't make a pointer, reference, nothing. It's a macro, but a SUPER SIMPLE one. Uh-oh. Let's make some rules:

  • alias may only be used in a function
  • alias may only contain identifiers, neither operators nor keywords