ktm v0.28.3

Variables

Declaring local and global variables, the zero they start with, letting the compiler pick the type, static locals, constants, and where a name is visible.

Declaring a variable

A declaration is a type, a name and, optionally, a first value. A local can be declared anywhere in a block, and is visible from there to the end of the block.

void main() {
    i64 count = 3;
    f64 ratio = 0.25;
    u8 *name = "ktm";
    bool ready = true;
    printf("%d %.2f %s %d\n", count, ratio, name, ready);
}
3 0.25 ktm 1

Several variables of one type can share a declaration. Each * belongs to the name after it, as in C.

void main() {
    i64 a = 1, b = 2, c;
    c = a + b;
    u8 *name = "ktm", letter = 'h';    // name is a u8 *, letter a u8
    printf("%d %s %c\n", c, name, letter);
}
3 ktm h

Locals start zeroed

A local declared without a value is zero: 0, 0.0, false, null, an empty String or slice, a struct or array with every byte 0. You never read garbage left on the stack.

struct Report { i64 lines; f64 ratio; u8 *name; };

void main() {
    i64 n;
    f64 f;
    bool b;
    u8 *p;
    Report r;
    i64 nums[4];
    printf("%d %.1f %d %d\n", n, f, b, p == null);
    printf("%d %.1f %d\n", r.lines, r.ratio, r.name == null);
    printf("%d %d %d %d\n", nums[0], nums[1], nums[2], nums[3]);
}
0 0.0 0 1
0 0.0 1
0 0 0 0

It is zero again each time its declaration runs, so a variable declared inside a loop starts fresh on every pass:

void main() {
    for (i64 i = 0; i < 3; i++) {
        i64 total;
        total += i;
        printf("pass %d: total %d\n", i, total);
    }
}
pass 0: total 0
pass 1: total 1
pass 2: total 2

One exception: an array bigger than 4 KiB with no String or slice in it, such as u8 buf[65536];, is a buffer you are about to fill, and is left as it is so that its memory isn't written for nothing. Write u8 buf[65536] = {0}; when you need it zeroed.

Global variables

A variable declared outside every function is a global: it lives for the whole program, and every function below its declaration sees it. A global without a value is zero.

i64 gTotal = 100, gCount;
u8 *gName = "server";

void bump() { gCount++; }

void main() {
    bump();
    bump();
    printf("%s: %d of %d\n", gName, gCount, gTotal);
}
server: 2 of 100

A global's first value can be any expression, function calls included. These run in the order the globals are written, before main:

i64 triple(i64 n) {
    printf("computing %d\n", n);
    return n * 3;
}

i64 gA = triple(1);
i64 gB = triple(gA);

void main() {
    printf("main sees %d and %d\n", gA, gB);
}
computing 1
computing 3
main sees 3 and 9

A global can also be one copy per thread: see threadlocal.

auto

auto takes the type of the first value.

void main() {
    auto count = 3;
    auto price = 9.99;
    auto label = "total";
    auto half = count / 2.0;
    auto big = count * 1000000000000;
    printf("%s %s %s %s %s\n", typeof(count), typeof(price), typeof(label), typeof(half), typeof(big));
}
i64 f64 u8[6] f64 i64

It saves the most typing with long type names, such as generic containers, and in a range-based for:

import map;

void main() {
    auto ages = map.new<u8[], i64>();
    ages.set("ada", 36);
    ages.set("linus", 54);
    i64 nums[] = {1, 2, 3};
    for (auto x : nums) {
        printf("%d ", x);
    }
    printf("| ada is %d\n", ages.getOr("ada", 0));
    ages.free();
}
1 2 3 | ada is 36

An auto variable needs a first value to take its type from:

does not compilevoid main() {
    auto x;
}
error:

static locals

A static local keeps its value between calls: it is created once, like a global, but only its function can see it.

i64 nextId() {
    static i64 id = 0;
    id++;
    return id;
}

void main() {
    nextId();
    nextId();
    printf("%d\n", nextId());
}
3
void report(u8 *what) {
    static i64 calls;
    calls++;
    printf("call %d: %s\n", calls, what);
}

void main() {
    report("start");
    report("middle");
    report("end");
}
call 1: start
call 2: middle
call 3: end

On a function or a global, static means something else: the name stays private to its file. See Modules and packages.

Constants

const makes a declaration read-only. With a value known at compile time, it is a compile-time constant: it can size an array, label a case, or build another constant.

const i64 MAX_USERS = 4;
const i64 DOUBLE_MAX = MAX_USERS * 2;
const f64 RATE = 0.5;
const u8 *GREETING = "hello";

i64 gUsers[MAX_USERS];

i64 classify(i64 n) {
    switch (n) {
        case MAX_USERS: return 1;
        default: return 0;
    }
}

void main() {
    printf("%d %d %.1f %s\n", MAX_USERS, DOUBLE_MAX, RATE, GREETING);
    printf("%d %d\n", classify(4), sizeof(gUsers));
}
4 8 0.5 hello
1 32

Assigning to a constant is an error, with =, += or ++:

does not compileconst i64 LIMIT = 10;

void main() {
    LIMIT = 11;
}
error:

A local can be const too, and so can a parameter, which then can't be assigned inside the function:

i64 twice(const i64 n) {
    return n * 2;
}

void main() {
    const i64 start = 21;
    printf("%d\n", twice(start));
}
42

Scope and shadowing

A name is visible from its declaration to the end of its block. An inner block can declare a name that an outer one has; inside the inner block, the name means the inner variable.

i64 gLevel = 1;

void main() {
    printf("global: %d\n", gLevel);
    i64 gLevel = 2;
    printf("local: %d\n", gLevel);
    {
        i64 gLevel = 3;
        printf("inner block: %d\n", gLevel);
    }
    printf("local again: %d\n", gLevel);
}
global: 1
local: 2
inner block: 3
local again: 2

Declaring the same name twice in one block is an error:

does not compilevoid main() {
    i64 x = 1;
    i64 x = 2;
}
error:

A local may even have the name of an imported module. For its scope, the name means the local:

import str;

void main() {
    u8[] str = "a local called str";
    printf("%s, %d bytes\n", str, str.len);
}
a local called str, 18 bytes

A global can't: i64 str; at file scope would hide the module for the whole file, and is an error.