Functions
Basic functions
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:
'main' returns i32 but can reach the end of its body without a return: return a value on every pathAn 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
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); // overridesDefault 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:
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 orderRules:
- 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.
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 requiredNamed arguments work with methods (the receiver, self, *self or *const self, is never named — it’s passed via dot notation):
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_capNamed 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:
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); // 42A pub function of an imported module is a value the same way, through the
module’s name: m.f.
import fmt from "./fmt.tape";
let w: fn(i64) -> string = fmt.hex;
apic.on_tick(process.on_tick); // pass another module's functionIt 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).
fn shout(s: string) -> i64 { ... }
let g: fn(i64) -> i64 = shout; // error: 'g' is fn(i64) -> i64, given fn(string) -> i64The 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 { ... }:
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:
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:
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):
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:
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:
selftakes the value, read-only;*selftakes a pointer and may change the value, so it is called only on something mutable: avar, a field or element of one, or a*T.v.scale(2.0)on theletabove is a compile error;*const selftakes a read-only pointer: the method promises not to change the value (writingself.xis an error), and can be called on anything, aletor a*const Tincluded. 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
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:
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:
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:1This 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 {
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
@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:
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:
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:
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, flippedLast modified: