Variables & Types
Bindings
Tape has two binding forms:
let x: i64 = 42; // immutable — cannot be reassigned
var y: i64 = 0; // mutable — can be reassignedUse 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.
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; // fineA 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).
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 itParameters, 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:
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.
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
@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:
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:
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 widensThe 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: