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. The compiler tree-walks the AST — arithmetic, variables, loops, conditionals, function calls, and @emit are all supported.
@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 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 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, strings → string value, integers → decimal text.
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. Each field element has: name, type, offset, size, __attrs.
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
- Arithmetic, comparisons, logical operators
let/vardeclarations and assignmentsif/else,while,forloops- Function calls to
@tape fn - String interpolation in
@print/@println @emitto generate declarations@typeof,@sizeof,@alignoffor type reflection@has_field,@has_method,@inner_type,@field_typefor type queries@assertfor static verification@embedto read files into compile-time strings@compile_errorto abort with a diagnosticcomptime match(T)for type-based dispatchfield.has_attr(attr_name)for attribute queries
Fuel limit
Compile-time evaluation is limited to 1 million operations. Exceeding this produces an error. This prevents accidental infinite loops from stalling compilation.
Last modified: