Functions

Basic functions

tape
fn add(a: i64, b: i64) -> i64 {
    return a + b;
}

A function with a result returns a value on every path. A body that can reach its end without a return is a compile error, main included:

plaintext
'main' returns i32 but can reach the end of its body without a return: return a value on every path

An if with both branches returning, a match whose arms all return, a while (true) that no break leaves and @trap count as paths that do not reach the end. A function returning void or E may reach the end: that is success, as a bare return; is.

Default parameters

tape
fn greet(name: string, loud: bool = false) {
    if (loud) {
        io.println("HELLO, " + name + "!");
    } else {
        io.println("Hello, " + name);
    }
}

greet("world");         // uses default
greet("world", true);   // overrides

Default parameters must be trailing — once a parameter has a default, all subsequent parameters must also have defaults.

A default is a compile-time constant: a literal, a const of the module (declared before or after the function), an enum variant, a builtin such as @sizeof, or an array literal of these, N of them, for a [N]T parameter (up: [3]f64 = [0.0, 1.0, 0.0]). It must be of the parameter’s type: shout: bool = 1 is a compile error (the default of 'shout' is bool, given i64), and so is scale: f64 = 2 (write 2.0).

Named arguments

Named arguments allow you to skip middle defaults and improve readability at call sites:

tape
fn create_window(title: string, width: i64 = 800, height: i64 = 600, vsync: bool = true) -> Window { }

create_window("My App");                          // all defaults
create_window("My App", 1024, 768);              // positional
create_window("My App", height: 400);            // skip width, keep its default
create_window("My App", vsync: false);           // skip width and height
create_window("My App", height: 400, width: 1024); // named, any order

Rules:

  • Positional arguments come first. Once a named argument appears, all subsequent arguments must also be named.
  • Named arguments can be in any order. They are matched by name, not position.
  • Only parameters with defaults can be skipped. Required parameters must always be provided (positionally or by name).
  • Naming is never required. Pure positional calling always works.
  • Duplicate parameters are an error. Providing the same parameter both positionally and by name fails at compile time.
tape
fn connect(host: string, port: i64 = 443, timeout_ms: i64 = 5000, tls: bool = true) -> Connection or NetError { }

connect("api.example.com", tls: false, timeout_ms: 1000);  // host positional, rest named
connect(host: "localhost");                                  // all named, port/timeout/tls default
connect(port: 443);                                          // ERROR: host is required

Named arguments work with methods (the receiver, self, *self or *const self, is never named — it’s passed via dot notation):

tape
fn Buffer.resize(*self, new_cap: i64 = 4096, zero_fill: bool = true) { }

buf.resize();                         // both defaults
buf.resize(8192);                     // positional
buf.resize(zero_fill: false);         // named, skip new_cap

Named arguments are not available through function pointers — pointers erase parameter names.

Function pointers

A function’s name is a value of function type, fn(params) -> ret:

tape
fn twice(x: i64) -> i64 { return x * 2; }
fn apply(f: fn(i64) -> i64, v: i64) -> i64 { return f(v); }

let f: fn(i64) -> i64 = twice;
let n = apply(twice, 21);        // 42

A pub function of an imported module is a value the same way, through the module’s name: m.f.

tape
import fmt from "./fmt.tape";

let w: fn(i64) -> string = fmt.hex;
apic.on_tick(process.on_tick);   // pass another module's function

It goes wherever a function value goes (an argument, a variable, a struct field, a global, a return value, an event handler, a prop), and it is the same function as the module’s own hex: the two compare equal. A function that isn’t pub is an error ('helper' is not pub in module 'fmt').

The type is the whole signature, and a function only fits a slot of exactly its type: the same parameter types (no widening, []const u8 is not []u8) and the same return type (fn(i64) returns nothing, so it is not fn(i64) -> i64).

tape
fn shout(s: string) -> i64 { ... }
let g: fn(i64) -> i64 = shout;   // error: 'g' is fn(i64) -> i64, given fn(string) -> i64

The return type takes all that a function’s does: T or E, ?T, ?T or E, void or E. A call through the pointer is handled the same way as a direct call, with or return or or { ... }:

tape
enum GpuError : u8 { Lost = 1: "lost"; }

struct Pass { run: fn(*Device) -> void or GpuError; }

fn frame(p: Pass, d: *Device) -> void or GpuError {
    p.run(d) or return;
}

With two arrows the or belongs to the outer one: fn make() -> fn() -> i32 or E returns a fn() -> i32, or an E. For a function that returns a fn type which can fail, name that type:

tape
type Job = fn() -> i32 or E;
fn make() -> Job { ... }

Such a type can’t be a type value in @tape code, as T or E can’t. An extern fn type can’t have this result: C has no T or E.

When the two signatures differ only in const, the error says where: ...; the signatures differ only in const: parameter 1 is '*Point', expected '*const Point'. Change the function (a reader should take *const T), not the slot.

Calls through a pointer — a variable, parameter, struct field, array element — are checked like direct calls: the argument count and each argument’s type, with the same coercions. Every parameter is required (defaults and named arguments need a direct call).

null is the unbound function pointer. A function pointer compares with null or with another function pointer (== and != only; the same function gives equal pointers), never with an integer:

tape
var on_done: fn() = null;
if (on_done != null) { on_done(); }

Calling a null function pointer stops the program with a runtime trap, trap: call through a null function pointer at file:line:col and exit code 101 (t1/t2; t0 does not check).

A function pointer and an integer convert only with as, for FFI and vtables: let f: fn(*u8) = slot as fn(*u8);, cb as i64. A cast never changes a function pointer’s signature: f as fn(*T) with f: fn(*const T) is an error, because it would let the function write through what it promised to only read. When you really mean it, go through an integer, (f as i64) as fn(*T), so it shows.

fn(...) pointers use tape’s own calling convention. A pointer C calls (a callback) or a C function you call through a pointer has the type extern fn(...) instead; a function named where one is expected becomes its C entry (Linking & FFI):

tape
extern fn qsort(base: *u8, n: u64, size: u64, cmp: extern fn(*const u8, *const u8) -> i32);
fn compare(a: *const u8, b: *const u8) -> i32 { ... }
qsort(data, n, 8, compare);

Methods

Use Type.name syntax to declare a method on a struct:

tape
struct Vec2 { x: f64; y: f64; }

fn Vec2.length(*const self) -> f64 {
    return math.sqrt_f64(self.x * self.x + self.y * self.y);
}

fn Vec2.scale(*self, k: f64) {
    self.x = self.x * k;
    self.y = self.y * k;
}

let v = Vec2 { x: 3.0, y: 4.0 };
let len = v.length();  // 5.0
var w = v;
w.scale(2.0);          // w is (6, 8)

The first parameter is the receiver:

  • self takes the value, read-only;
  • *self takes a pointer and may change the value, so it is called only on something mutable: a var, a field or element of one, or a *T. v.scale(2.0) on the let above is a compile error;
  • *const self takes a read-only pointer: the method promises not to change the value (writing self.x is an error), and can be called on anything, a let or a *const T included. Use it for every method that only reads.

A parameter is read-only too, like a let: a struct or array argument is the caller’s own value, so p.x = 1 or a *self call on a parameter p: P is an error. Copy it into a var to change a copy, or take a *P to change the caller’s.

Visibility

tape
pub fn api_function() -> i64 { return 42; }
fn internal_helper() { }

pub makes a function visible to importers. Without it, the function is module-private.

Multiple functions can be exported together with a pub {} block:

tape
pub {
    fn init() { ... }
    fn update() { ... }
    fn shutdown() { ... }
}

One name, one declaration

A module declares each name once. A second function of a name is a compile error at the second, naming the first, whatever their parameters and whether or not one is pub; so is a function with the name of a constant, variable, type, component or import of the module:

tape
fn find(x: bool, a: i64, b: i64, c: i64) -> i64 { ... }
pub fn find(a: i64, b: i64) -> i64 { ... }
// error: `find` is already defined at dupa.tape:1

This is checked before any compile-time code runs: in a module with a generator the duplicate is the error you get, not something the generator’s code tripped over.

tape has no overloading: give the two functions two names. A method is named by its type (fn Point.len and fn Vec.len are two names, and a free fn len may sit beside them); only operator methods are overloaded, by the type of their right-hand operand (Operator overloading). Parameters of one function, fields of one struct, variants of one enum and the props and events of one component are unique the same way.

External functions (C FFI)

tape
@tape {
    if (@target == "win64") {
        @emit @link("kernel32");
        @emit extern fn GetTickCount() -> u32;
    }
}

Calls a C function using the platform ABI, in the VM and natively: structs pass by value, and a C variadic function is declared with ... (extern fn printf(fmt: *const u8, ...) -> i32;). A library of one platform, like kernel32, is linked in that target’s @tape branch; a bare @link in a t1 or t2 module is for a library every target has, named without an extension. See Linking & FFI.

Exported functions

tape
@export
pub fn tape_init() -> i32 {
    return 0;
}

Makes a tape function callable from C. The function uses the platform calling convention at the boundary, structs by value included; it cannot be variadic.

Operator overloading

Define methods named op_* to overload operators on structs:

tape
struct Vec2 { x: f64; y: f64; }

// Arithmetic
fn Vec2.op_add(self, other: Vec2) -> Vec2 { return Vec2 { x: self.x + other.x, y: self.y + other.y }; }
fn Vec2.op_sub(self, other: Vec2) -> Vec2 { return Vec2 { x: self.x - other.x, y: self.y - other.y }; }
fn Vec2.op_mul(self, other: Vec2) -> Vec2 { return Vec2 { x: self.x * other.x, y: self.y * other.y }; }
fn Vec2.op_div(self, other: Vec2) -> Vec2 { return Vec2 { x: self.x / other.x, y: self.y / other.y }; }
fn Vec2.op_mod(self, other: Vec2) -> Vec2 { return Vec2 { x: self.x % other.x, y: self.y % other.y }; }

// Unary negation
fn Vec2.op_neg(self) -> Vec2 { return Vec2 { x: -self.x, y: -self.y }; }

// Comparison
fn Vec2.op_eq(self, other: Vec2) -> bool { return self.x == other.x && self.y == other.y; }
fn Vec2.op_lt(self, other: Vec2) -> bool { return self.x < other.x; }
fn Vec2.op_lte(self, other: Vec2) -> bool { return self.x <= other.x; }
fn Vec2.op_gt(self, other: Vec2) -> bool { return self.x > other.x; }
fn Vec2.op_gte(self, other: Vec2) -> bool { return self.x >= other.x; }

Usage:

tape
let a = Vec2 { x: 1.0, y: 2.0 };
let b = Vec2 { x: 3.0, y: 4.0 };
let c = a + b;    // op_add
let d = -a;       // op_neg
let eq = a == b;  // op_eq
let ne = a != b;  // auto-derived from op_eq

!= is automatically derived from op_eq.

Operator methods belong to struct values, and any expression of the struct’s type has them: a local, a global, a field, an element, a call’s result, *p, opt! (*p == *q, -*p, 2.0 * *p). A struct with no method for an operator has no such operator, on either side: a == b, 1 == a and "x" == a are compile errors.

An operator method is pure: it may not call an extern fn, do I/O, change a global or allocate, and neither may anything it calls, in any module. It is a compile error that names the operator and the calls that lead there: operator method Vec2.op_add is not pure: it calls log_it, which mutates the global 'count'. Reading a global is fine.

On pointers to a struct (*Vec2, *const Vec2, ?*Vec2) ==, !=, <, <=, > and >= compare the addresses, whatever methods the struct has. A pointer does not compare with a struct value: that is a compile error.

A float literal given to an operator whose method takes f32 is that f32, on either side of the operator:

tape
struct V2 { x: f32; y: f32; }
fn V2.op_mul(self, s: f32) -> V2 { return V2 { x: self.x * s; y: self.y * s; }; }

let v = V2 { x: 3.0; y: 4.0; };
let a = v * 2.0;      // op_mul with 2.0 as f32
let b = 0.5 * v;      // the same, flipped

Last modified: