Compile-Time Overview
Why not generics?
Tape does not have generics (<T>). Instead, compile-time code execution provides the same power:
- No type inference complexity
- No monomorphization
- No trait bounds or variance
- Explicit, debuggable, and visible
@tape blocks
@tape {
// this code runs at compile time
gen_queue(i64);
gen_queue(Event);
}Code inside @tape { } executes during compilation. It is ordinary tape: every block, @tape const and @tape fn, generators included, runs in the compiler’s VM (the interpreter tape run uses), so it can use the whole language and call the program’s own functions. There is no second, smaller interpreter for compile-time code: what it can do is what the language does, less the few things listed under What can run at compile time.
@tape fn
Compile-time functions are declared with @tape fn. They can be called from @tape {} blocks:
@tape fn gen_queue(T: type) {
@println("Generating Queue_{T}");
@emit struct Queue_${T} {
items: *${T}; count: i64; cap: i64;
}
@emit fn queue_${T}_push(q: *Queue_${T}, item: ${T}) {
// grow + append
}
@emit fn queue_${T}_pop(q: *Queue_${T}) -> ?${T} {
// remove + return
}
}
@tape { gen_queue(i64); }
@tape { gen_queue(Event); }@emit
@emit injects declarations into the module being compiled:
| Form | Injects |
|---|---|
@emit struct Name { ... } | Struct declaration |
@emit tagged Name { ... }, @emit union Name { ... } | Tagged or untagged union declaration |
@emit type Name = T; | Type alias |
@emit component Name { ... } | Component (view tree and @tape blocks included) |
@emit test "name" { ... } | A test in the caller’s #test region |
@emit @tape fn name(...) { ... } | A generator for later @tape blocks |
@emit attr name;, @emit @packed struct ... | Attribute declaration; attributes on emitted declarations |
@emit fn name(...) { ... } | Function declaration |
@emit const NAME = expr; | Constant |
@emit field: Type; | Field (inside struct @tape {}) |
@emit prop name: Type; | Prop (inside component @tape {}) |
@emit state, @emit event, @emit slot, @emit fn | State flag, event, slot, method (inside component @tape {}) |
@emit var name: Type; | Var (inside component @tape {}) |
@emit @dispatch fn ... | Dispatch (inside component @tape {}) |
$ splicing
Names containing $ are substituted at emit time:
$var— simple variable substitution (greedy identifier chars)${var}— braced substitution (explicit boundary)${var.field}— dotted path (access compound value fields)
@tape fn gen_getter(T: type, field_name: string) {
@emit fn get_${field_name}(obj: *${T}) -> i64 {
return obj.${field_name};
}
}Values are spliced as: types → type name (i64, Point, cslice_u8 for []const u8), strings → string value, integers → decimal text. A type splice that is a whole type (obj: *${T}, -> $T) is the type itself, so $T works for []const u8 or geo.Point too.
type as a value
At compile time, type is a first-class value. Type names resolve to type values:
@tape fn print_fields(T: type) {
var info = @typeof(T);
@println("struct {info.name} (size={info.size}, align={info.align}):");
for (field in info.fields) {
@println(" {field.name} : offset={field.offset}, size={field.size}");
}
}@typeof(T) returns a TypeInfo compound with fields: name, kind, size, align, fields, variants, is_const, len, is_volatile. Each field element has: name, type, offset, size, __attrs, has_default.
comptime match
Match on a type value at compile time:
@tape fn emit_default(T: type) {
comptime match(T) {
i64, i32 => { @emit const DEFAULT = 0; }
f64 => { @emit const DEFAULT = 0.0; }
_ => { @compile_error("unsupported type"); }
}
}Arms match type names. _ is the wildcard. Multiple types per arm separated by commas.
What can run at compile time
Compile-time code is checked and run as t1 code, whatever the module’s profile: in a t0 module’s @tape code string is the owned string, and the view is written []const u8. It is typechecked like any code, a @tape fn before anything calls it (tape check reports a broken generator nobody calls). Blocks, @tape consts and @tape fns, generators included, can use:
- The whole language: structs, enums, tagged unions and
match,?T,T or Ewithor return/or { },defer, pointers to locals, strings, slices and allocation - Ordinary functions: the program’s own, and those of pure stdlib modules (
str,json,bytes,collections’ emitted code, …), so one function serves both sides:
fn fib(n: i64) -> i64 { if (n < 2) { return n; } return fib(n - 1) + fib(n - 2); }
@tape const F20: i64 = fib(20); // 6765, computed by the compiler
import json from "json";
@tape fn port(text: []const u8) -> i64 {
let v = json.parse(text) or { return -1; };
let p = v.get("port") or { return -1; };
return p.as_i64() or { return -1; };
}
@tape const PORT: i64 = port(@embed("./config.json")); // {"port": 8080}- A
@tape constof a struct, enum or tagged union, as well as numbers, bools, strings and arrays - A plain
constwhose initializer calls a function, computed by the compiler like a@tape const(the same rules for what the call reaches). Aconstwithout a call is folded and runs nothing:
fn sq(x: i64) -> i64 { return x * x; }
const AREA: i64 = sq(4); // 16
const ROOT: f64 = math.sqrt_f64(2.0); // 1.4142...
const NEXT = AREA + 1; // 17
const MASK = 1 << 4; // folded: no code runsThey cannot call a capability module’s functions (io, fs, net, os, …: a compile error naming the module), an extern fn, or read or write the module’s own vars (it has no state before the program runs; its lets and consts are readable). The same holds for everything a call reaches: a program function that reads a var or calls io.println is a compile error when compile-time code calls it, noted at the call. A function that uses what a later @tape block emits is not ready yet: calling it is a compile error that names what it uses and the block that emits it:
import collections from "collections";
fn total() -> i64 { var v: Vec_i64 = vec_i64_new(); v.push(1); return v.len; }
@tape const T: i64 = total();
// error: 'total' cannot run at compile time here: it uses 'Vec_i64', which a
// later @tape block emits (the block at line 6)
@tape { collections.gen_vec(i64); }Once that block has run, compile-time code after it may call total.
A generator also has what it needs to write declarations:
@emitto generate declarations; a splice (${expr}) is any expression of the generator’s code, computed when the@emitruns, so it may call a helper or an ordinary function (fn_${name_of(T)})@typeof,@sizeof,@alignoffor type reflection: a type’s fields, variants and methods (@typeof(T).methods), a component’s props, events, slots and vars, and a function’s parameters and result (@typeof(f)), so a generator can write one declaration per member (reflection);@has_field,@has_method,@inner_type,@field_typefor type queriescomptime match(T)for type-based dispatch,T == string,@typeof(T).kind == TypeKind.Structfor type dispatch- A type an earlier statement of the same code emitted, named where a type goes (
@emit struct Vec3 { ... }then@sizeof(Vec3)): names resolve when the code runs - Function parameters (
less: fn(*const T, *const T) -> bool) that emitted code calls (function parameters); in@tapecode such a function is spliced, passed on, or called when its signature names notypeparameter (f: fn(i64) -> i64, thenf(1)) - Module parameters (
m: module) with@moduleinfo(m)and@module_api(m, names) field.has_attr(a)andfield.attr(a)for attribute queries, on a struct’s fields, the type itself (@typeof(T)) and a module’s functions (@moduleinfo(m).functions)@assert,@compile_error,@print/@println,@embed
Fuel limit
Each top-level compile-time execution (a @tape block, a constant’s initializer, a @tape block inside a function body) may run 10 million operations (in the VM, one operation is one instruction); a generator it calls spends from the same budget. Exceeding it is a compile error at the operation that ran out, which names the flag that raises it: --tape-fuel <n> on tape build, run, check and test. This prevents accidental infinite loops from stalling compilation.
A trap in compile-time code (an index out of bounds, ! on null) is a compile error where it trapped, trap at compile time: index out of bounds, with a note at each call that led there.
Last modified: