Variables & Types

Bindings

Tape has two binding forms:

tape
let x: i64 = 42;    // immutable — cannot be reassigned
var y: i64 = 0;     // mutable — can be reassigned

Use let by default. Use var when you need to reassign.

A let binding freezes its whole value, not just the name: an array’s elements and a struct’s fields can’t be changed through it either.

tape
let a: [3]i64 = [1, 2, 3];
a[0] = 5;              // error: a 'let' binding is read-only, its elements included
let p: Point = Point { x: 1.0; y: 2.0; };
p.x = 3.0;             // error: declare it 'var' to change it
let q: *i64 = &a[0];   // error: &a[0] is a *const i64

var b: [3]i64 = [1, 2, 3];
b[0] = 5;              // fine

A pointer or slice held in a let still writes what it points at: the binding (the pointer) is frozen, not its target. Declare *const T or []const T when the target must not change either.

The same goes for methods: one that takes *self changes its value, so it can’t be called on a let; one that only reads takes *const self and works on anything (see Methods).

tape
let v: collections.Vec_i64 = load();
let n: i64 = v.len();  // fine: len takes *const self
v.push(4);             // error: 'v' is a let: push changes it

Parameters, for loop variables and a component’s props are read-only in the same way: copy one into a var to change it.

A var without an initializer starts at zero: every number 0, every pointer null, an array’s every element. Zeroing a big scratch array on every call of a hot function can cost more than the work it does; mark it @uninitialized and write it before you read it:

tape
fn filter_row(src: []const u8, out: []u8) {
    @uninitialized var tmp: [441]u8;    // not zeroed: every element written below
    for (i in 0..441) { tmp[i] = src[i] >> 1; }
    // ...
}

Its type is written and holds plain data only (numbers, bools, pointers, enums, and arrays and structs of them); a string, slice, optional or tagged union always starts empty. An element read before it is written holds whatever was there.

Scope

A local lives from its declaration to the end of its block; a for loop variable lives for the loop. A local can’t take the name of one still in scope, an outer block’s or a parameter: that is a compile error at the second, naming the first, not a second variable hiding the first.

tape
var k: i64 = 5;
if (ready) {
    var k: i64 = 1;    // error: `k` is already defined at gpud.tape:1
}

Blocks side by side, and a block after another has ended, may use the same name; _ may repeat. A local may take the name of a module variable or of a component’s var: it hides it inside its block, and after the block the name means the variable again. Its own initializer comes before it, so there the name still means what it meant: var local: L = local(); calls the function local.

Types

See Primitive Types for the full list of built-in types (i8–i64, u8–u64, f32, f64, bool, string, color, void).

Example

tape
@profile(t1);
import io from "io";

pub fn main() -> i32 {
    let x: i64 = 42;
    var y: i64 = 0;
    y = y + x;
    io.println("{y}");
    return 0;
}

Constants

Compile-time constants use const:

tape
const MAX_SIZE: i64 = 1024;
const PI: f64 = 3.14159265358979;

Constants are evaluated once, at compile time; every use reads that value (see Constants).

Type inference

When an initializer is present, the type annotation can be omitted:

tape
let x = 42;          // inferred as i64
var name = "tape";   // inferred as string
let r = 2.5;         // inferred as f64
let a: f32 = 1.5;
let b = a * 2.0;     // inferred as f32: a float literal takes the f32 side's type
let d: f64 = 3.0;
let e = a + d;       // inferred as f64: an f32 meeting an f64 widens

The inferred type is the initializer’s type: a call’s return type, a field’s or element’s type, the result of the arithmetic. A module-level var or let is inferred the same way, a component instance included: var counter = Counter {}; is a Counter, with its vars and methods. With an annotation, its initializer must be of that type, as for a local: var limit: i64 = "10"; is a compile error ('limit' is i64, given string), and so is var scale: f64 = 1; (write 1.0).

Without an initializer, a type annotation is required, in every profile.

Last modified: