Constants

Basic constants

tape
const MAX_SIZE: i64 = 1024;
const PI: f64 = 3.14159265358979;
const APP_NAME: string = "MyApp";

Constants are evaluated at compile time: the initializer never runs again at a use.

The value must be of the declared type, as for a let: const K: i64 = "s"; is a compile error ('K' is i64, given string), and so is each element of an array literal that isn’t (const T: [2]f32 = [K, 1.0]; with K an f64 needs K as f32). An integer literal into a narrower integer wraps, and a float literal, or arithmetic on them, is an f32 where one is declared (const THIRD: f32 = 1.0 / 3.0;).

A constant may be computed from other constants, its own module’s or an imported one’s:

tape
import keys from "keys";

const SLOTS: i64 = 4;
const BYTES: i64 = SLOTS * 16;
const KEY_A: u16 = keys.KEY_A;

var buf: [BYTES]u8;       // 64 bytes

An integer constant computed this way is a compile-time constant like a literal, so it can size an array. An array size is an integer literal or the name of an integer constant, this module’s ([N]u8) or an imported module’s ([keys.COUNT]u8).

The type can be left out. The constant then has its initializer’s type, and an array literal’s element type is its first element’s:

tape
const STEPS = [0.5, 1.5];      // [2]f64
const PORTS = [KEY_A, 7];      // [2]u16, as KEY_A is a u16

An initializer whose type can’t be written, such as const NONE = []; (an empty literal has no element type), is a compile error: write the type.

A constant is used like its value: ORIGIN.x of a struct constant, TABLE[i].name and TABLE.len of a constant table, NAME.len of a string.

Computed once

The compiler computes a constant’s value once; every use reads that value. Strings joined with + or built by interpolation are one literal:

tape
const VERSION: i64 = 2;
const NAME: string = "tape";
const BANNER: string = "{NAME} v{VERSION}";            // "tape v2"
const CURSOR: []const u8 = "0011" + "0110" + "1100";   // one 12-byte literal

for (i in 0..CURSOR.len) { draw(CURSOR[i]); }          // reads it, builds nothing

The same holds for numbers and bools computed from constants (const HALF: f64 = W / 2.0;) and for the strings in a constant table or struct (const T: [2]string = [PRE + "a", PRE + "b"];). A string the compiler can’t compute, such as one formatting a float, is a compile error in a constant: build it at run time into a let or var.

Computed by a function

A constant’s initializer may call a function. The compiler runs the call once, at compile time, and the constant holds the result:

tape
import math from "math";

fn sq(x: i64) -> i64 { return x * x; }

const AREA: i64 = sq(4);                  // 16
const ROOT: f64 = math.sqrt_f64(2.0);     // 1.4142...

The call follows the rules of compile-time code: your own functions and those of pure modules (math, str, json, …) run; a function that reads or writes a module var, or reaches io, fs, net or another capability module, is a compile error at the constant. An initializer without a call is computed by the compiler directly, without running any code.

Constant tables

tape
const PRIMES: [8]i64 = [2, 3, 5, 7, 11, 13, 17, 19];
const GAMMA: [256]u8 = [ /* ... 256 values ... */ ];

A constant array of literals is stored once, read-only, in the program (its .rodata, aligned to its elements; one shared copy under tape run). Using it reads that copy: nothing is rebuilt per function or per call, however large the table. Array literals have no element limit. PRIMES.len is the element count. Tables may be nested:

tape
const SBOX: [2][4]u8 = [[1, 2, 3, 4], [5, 6, 7, 8]];
let x = SBOX[row][col];

Constants inside functions

tape
fn weekday_len(d: i64) -> i64 {
    const NAMES_LEN: [7]i64 = [6, 7, 9, 8, 6, 8, 6];
    return NAMES_LEN[d];
}

A const in a function body is scoped to its block like a let. A table of literals there is the same read-only copy as at module level: the statement binds its address and builds nothing, however often it runs. A const can’t be assigned, and neither can a const table’s elements.

A function-body const may also use the function’s parameters and locals; it is then computed when its statement runs, like a let. One made of literals and module constants is computed by the compiler: const P: string = ROOT + "/bin"; binds the one literal each time.

Computed constants with @tape

tape
@tape const CRC_TABLE: [256]u32 = {
    var t: [256]u32;
    for (i in 0..256) {
        var c: u32 = i as u32;
        for (j in 0..8) {
            if (c & 1 != 0) {
                c = 0xEDB88320 ^ (c >> 1);
            } else {
                c = c >> 1;
            }
        }
        t[i] = c;
    }
    return t;
};

The @tape prefix means the value is computed by running tape code at compile time. The block can declare arrays, assign elements (t[i] = c, t[i] += 1), loop over ranges and cast with as; the table it returns is stored like any other constant array.

Embedded file constants

tape
@tape const SHADER_SRC: string = @embed("shaders/blur.glsl");
@tape const ICON_DATA: []const u8 = @embed("assets/icon.png");

@embed reads a file at compile time and stores its contents as a constant. A path starting with ./ or ../ is relative to the source file, any other to the working directory; @tape pub const exports the constant to importers (see @embed).

Last modified: