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()
- Also double
- 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 functionalias
may only contain identifiers, neither operators nor keywords