@emit & Code Generation
Basic @emit
@tape {
@emit const BUFFER_SIZE: i64 = 4096;
@emit struct Buffer {
data: [4096]u8;
len: i64;
}
@emit fn buffer_new() -> Buffer {
return Buffer { len: 0; .. }; // `..` zeroes `data`
}
}@emit takes a declaration and injects it into the enclosing scope, as if you’d written it directly.
Where @emit works
@tape { ... } blocks can appear at:
- Module level — emits module-level declarations (structs, fns, consts, globals)
- Inside struct bodies — emits struct fields
- Inside component bodies — emits component props, vars, states, events, slots, methods and dispatch declarations
- Inside function bodies — emits local bindings (var, let) into the function scope
The key principle: @emit injects code at whatever scope it appears in. A module-level @emit var creates a global. A function-level @emit let creates a local binding.
Every declaration you can write is an @emit form with the same syntax, its names free to splice (Shape_$T). An @emit in a scope where that declaration can’t be written is a compile error naming the scope: @emit tagged in a struct’s @tape block, for instance. The one declaration that is not an @emit form is import: a generator’s emitted code reaches the generator’s own imports by their alias (below).
Emit forms
| Syntax | Where | Injects |
|---|---|---|
@emit struct Name { ... } | module @tape | Struct declaration |
@emit enum Name { ... } | module @tape | Enum declaration (variants from a list: below) |
@emit tagged Name { ... } | module @tape | Tagged union declaration |
@emit union Name { ... } | module @tape | Untagged union declaration (t0/t1, like a written one) |
@emit type Name = T; | module @tape | Type alias ($T and types built of it on the right) |
@emit component Name { ... } | module @tape | Component, any members, view { } and @tape blocks included |
@emit test "name" { ... } | module @tape | A test in the caller’s #test region |
@emit @tape fn name(...) { ... } | module @tape | A generator later @tape blocks call |
@emit attr name;, @emit attr name(params); | module @tape | An attribute declaration |
@emit @packed struct, @emit @export pub fn, … | module @tape | Attributes before an emitted struct or fn, as written |
@emit fn name(...) { ... } | module @tape | Function declaration |
@emit fn Type.method(...) { ... } | module @tape | Method on a type |
@emit const NAME = expr; | module @tape | Constant (evaluated at comptime) |
@emit var name: Type = expr; | module @tape | Module-level mutable global |
@emit let name: Type = expr; | module @tape | Module-level immutable global |
@emit var name: Type = expr; | function @tape | Local mutable binding |
@emit let name: Type = expr; | function @tape | Local immutable binding |
@emit field: Type; | struct @tape | Struct field |
@emit prop name: Type; | component @tape | Component prop |
@emit var name: Type; | component @tape | Component var |
@emit state name: bool = v; | component @tape | Interaction state flag |
@emit event name(params); | component @tape | Event |
@emit slot name; | component @tape | Slot |
@emit fn name(...) { ... } | component @tape | Method of the component |
@emit @dispatch fn name(...) | component @tape | Dispatch declaration |
@emit var / let at module level
Emit global variables:
@tape {
@emit var counter: i64 = 0;
@emit let max_retries: i64 = 3;
}
fn main() -> i64 {
counter = counter + 1;
return counter + max_retries;
}@emit var creates a mutable global. @emit let creates an immutable global.
@emit var / let inside functions
Emit local bindings:
@tape fn declare_temp(prefix: string, T: type) {
@emit var ${prefix}_temp: ${T} = 0;
}
fn compute() -> i64 {
@tape { declare_temp("x", i64); }
// now x_temp is a local variable
x_temp = 42;
return x_temp;
}This is useful for metaprogramming that generates boilerplate locals, loop variables, or temporary buffers.
@emit pub
Emitted functions and structs can be public:
@tape fn gen_api(T: type) {
@emit pub fn create_${T}() -> ${T} {
// exported to importers
}
}When the generator’s caller decides, splice a compile-time bool where
pub goes: true makes the declaration pub, false private to the
calling module.
@tape fn gen_table(name: string, visible: bool) {
@emit $visible fn ${name}_size() -> i64 { return 16; }
}
@tape { gen_table("open", true); gen_table("own", false); }
// pub fn open_size(), and fn own_size() only this module seesNames are declared once
An emitted declaration is a declaration of the module like any other: emitting
a name the module already has (written by hand or emitted before, of any kind)
is a compile error at the @emit, naming the first declaration and the
generator call:
gen.tape:2:5: error: `Box_i64` is already defined at gen.tape:2 (emitted by the call of gen at gen.tape:5)The same holds for fields emitted into a struct and vars and props emitted into
a component. Calling a generator twice for one type in one module, or for two
types that splice to the same names (geo.Point and shapes.Point both give
Point), is such an error. So is calling it for a type and for an alias of it
(gpu.Mat4 and math3d.Mat4): they are one type.
Modules the emitted code uses
Code a generator emits can call into the modules the generator’s own file
imports. A caller of collections.gen_vec doesn’t import mem, even though
the emitted functions call mem.alloc_or_trap:
// collections.tape
import mem from "mem";
pub @tape fn gen_vec(T: type) {
@emit pub fn vec_${T}_grow(v: *Vec_${T}, cap: i64) {
let p: *u8 = mem.alloc_or_trap(cap * @sizeof(${T})); // collections' mem
// ...
}
}
// app.tape
import collections from "collections";
@tape { collections.gen_vec(i64); } // no `import mem` neededInside emitted code, a name like mem.alloc means the generator’s mem
even if the caller has its own, different mem. Every other name
(unqualified names, ${T} splices) resolves where the generator is called.
This applies to the generator file’s imports by bare name ("mem"); a
relative import ("./helpers.tape") must also be imported by the caller.
The $ splice in emitted declarations
Insert compile-time values into identifiers and types:
@tape fn gen_getter(T: type) {
@emit fn get_x(obj: *${T}) -> i64 {
return obj.x;
}
}
@tape { gen_getter(Point); }
// generates: fn get_x(obj: *Point) -> i64 { return obj.x; }Splice forms:
$var— simple (greedy identifier characters:a-z A-Z 0-9 _, sovec_$T_pushreads a variableT_push; writevec_${T}_push)${var}— braced (explicit boundary)${var.field}— dotted path into compound values
Splice works in: struct names, function names, receiver types, parameter types, return types, field names, and within function bodies — in names and types there too (times_$n(x), let t: ${T}, let r_$j: i64 = ...; and its uses r_$j). Where an expression goes, a whole splice of a compile-time integer, float or bool is its literal (x * $n, s[$j] == ${w[j]}); a string’s is a name, and "${s}" is its literal. An array size may be spliced too: [$n]u16.
A splice’s path reads a field (${f.name}), a string’s or array’s length (${w.len}) and an element or byte (${ws[i]}, ${w[j]}), the index a number or a compile-time variable. Splice text joins the name before it (times_$n), not text after ${...}: write slots_${f}, not ${f}_slots.
A type splice that makes up a whole type (item: $T, -> ?$T,
data: *$T, 0 as *$T, @sizeof($T)) is the type itself, whatever it
is: with T bound to []const u8, fn len_of(s: $T) takes a
[]const u8. A type of another module keeps the name the calling module
uses for it (geo.Point). Inside a name, $T is the type’s name:
| Type | In a name | Type | In a name |
|---|---|---|---|
i64, Point | i64, Point | geo.Point | Point |
*T | ptr_T | *const T | cptr_T |
*volatile T | vptr_T | *const volatile T | cvptr_T |
[]T | slice_T | []const T | cslice_T |
?T | opt_T | [4]T | arr4_T |
fn(A, B) -> R | fn_A_B_to_R | fn() | fn_to_void |
They nest: *[]const u8 is ptr_cslice_u8, and a generator that emits
Map_${K}_${V} names it Map_cslice_u8_i64 for []const u8 keys.
An alias is the type it names, through any number of aliases and modules, so a
generator takes one like any type and its names are the type’s own:
bytes.gen_of(gpu.Mat4) makes of_Mat4 (gpu.Mat4 is math3d.Mat4), and with
type Id = i64;, gen_vec(Id) makes Vec_i64. Your module needn’t import the
module that declares the type.
Write $T, not T, in emitted code: emitted code is ordinary code at the
call site, where the generator’s parameter T does not exist.
In a string in emitted code, ${...} puts the compile-time value’s text
into the string, and a plain {...} stays an ordinary interpolation that
runs with the program:
@tape fn gen_describe(T: type) {
@emit fn describe_$T(v: *const $T) -> string {
var out: string = "${T}:";
comptime for (f in @typeof(T).fields) {
out = out + " ${f.name}={v.${f.name}}"; // " x=" then the value
}
return out;
}
}
// describe_Point(&p) is "Point: x=1 y=2"Attributes on emitted declarations
Attributes go before an emitted struct or function as they do on a written
one, with pub anywhere among them; an emitted struct’s fields take theirs
too, and a generator may declare the attribute itself:
@tape fn gen_table(name: string, n: i64) {
@emit pub attr primary;
@emit @align($n) pub struct ${name} { @primary id: i64; label: string; }
@emit @export pub fn ${name}_count() -> i64 { return 0; }
}A later @tape block reads them like written ones:
for (f in @typeof(T).fields) { if (f.has_attr(primary)) { ... } }.
@emit fields with attributes
Inside a struct’s @tape {} block, emit fields with attributes:
attr primary;
attr auto_timestamp;
@tape fn add_dto_fields(T: type) {
@emit @primary id: i64;
@emit @auto_timestamp created_at: i64;
@emit @auto_timestamp updated_at: i64;
}
struct UserDto {
@tape { add_dto_fields(UserDto); }
name: string;
age: i64;
}Multiple attributes can be stacked on a single @emit field. Attribute arguments are also supported (@attr(value)).
@emit methods (Type.method syntax)
Emit methods attached to a type:
@tape fn add_dto_fields(T: type) {
@emit fn $T.is_new(self) -> bool {
return self.id == 0;
}
@emit fn $T.touch(*self) {
self.updated_at = 42;
}
}The receiver type before the dot is spliced. self, *self and *const self receivers work as in regular methods.
comptime for
Unroll a loop at compile time — each iteration emits independently:
@tape fn gen_all_getters(T: type) {
var info = @typeof(T);
comptime for (field in info.fields) {
@println(" emitting getter for {field.name}");
@emit fn get_${field.name}(obj: *${T}) -> i64 {
return obj.${field.name};
}
}
}The iterable must evaluate to a comptime array (e.g., @typeof(T).fields). An optional index variable is supported:
comptime for (field, i in info.fields) {
@println("field {i}: {field.name}");
}comptime if
Conditional emission based on compile-time values:
@tape fn gen_print(T: type) {
comptime if (@has_method(T, "to_string")) {
@emit fn print_val(v: *${T}) { io.println(v.to_string()); }
} else {
@emit fn print_val(v: *${T}) { io.println("<opaque>"); }
}
}The condition is evaluated at compile time. Only the matching branch is emitted.
Inside an @emit body, comptime if chooses which statements the emitted
function gets. An else if after it is part of the same compile-time chain:
@tape fn gen_zero(T: type) {
@emit fn zero_$T() -> $T {
comptime if (T == string) {
return "";
} else if (T == f64) {
return 0.0;
} else {
return 0 as $T;
}
}
}comptime match
Type-based dispatch at compile time:
@tape fn gen_default(T: type) {
comptime match(T) {
i64, i32 => { @emit const DEFAULT = 0; }
f64 => { @emit const DEFAULT = 0.0; }
_ => { @compile_error("unsupported type for gen_default"); }
}
}Arms match by type name. Multiple types per arm separated by commas. Arms
are tried in order; _ is the wildcard (matches any type). comptime match
also works inside an @emit body, where the selected arm’s statements are
emitted.
Two mistakes are compile errors rather than silent misses:
- an arm naming something that isn’t a type (
strng => { ... }); - a type no arm names, when there is no
_arm. A generator that supports only some types can say so:_ => { @compile_error("..."); }.
Type dispatch
Generators often need different code for different types: strings compare
by content, integers by value, structs field by field. Branch on the type
value with ordinary compile-time code:
pub @tape fn gen_eq(T: type) {
if (T == string) {
@emit pub fn eq_$T(a: string, b: string) -> bool { return a == b; }
} else if (@typeof(T).kind == TypeKind.Struct) {
@emit pub fn eq_$T(a: *$T, b: *$T) -> bool {
comptime for (f in @typeof(T).fields) {
if (a.${f.name} != b.${f.name}) { return false; }
}
return true;
}
} else {
@emit pub fn eq_$T(a: $T, b: $T) -> bool { return a == b; }
}
}
@tape { gen_eq(string); gen_eq(Point); gen_eq(i64); }What you can do with a type value:
| Expression | Meaning |
|---|---|
T == U, T != U | Same type. *S == *S, *S != S. |
@typeof(T).kind == TypeKind.Struct | The kind of type: Primitive, Struct, Enum, Tagged, Pointer, Slice, Array, Function, Optional, Union |
@sizeof(T), @alignof(T) | Size and alignment |
@typeof(T).is_const, @typeof(T).len | true for *const S and []const S; the N of [N]S |
@typeof(T).is_volatile | true for *volatile S and *const volatile S |
@inner_type(T) | S when T is *S, *const S, []S, []const S, ?S or [N]S |
@has_field(T, "x"), @has_method(T, "x") | Structural queries |
Everything else is an error: T < U, T == 8, or a misspelt type name
(T == strng is an error, not false).
Type values you can write in @tape code:
| Written | Type |
|---|---|
i64, string, Point, T | a type name or a type parameter |
geo.Point | a pub struct or enum of the imported module geo |
*T, *const T, []T, []const T, ?T | pointers, slices, optionals |
[4]u32, [SIZE]u8 | arrays (the length a literal or a constant) |
fn(i64, i64) -> bool, fn() | function types |
They nest, and pass to generators like any type: gen_map([]const u8, i64). Built types are equal when their
parts are (*const u8 != *u8, [4]u32 != [5]u32). Named types are equal
only if they are the same declaration: geo.Point, shapes.Point and your
own Point are three different types. *!T and T or E aren’t type
values.
Names in a generator’s @tape code are the generator module’s own: Cell
in gen.tape is gen.Cell to a caller of gen.gen_cells(...), and
utf8.Rune there is gen.tape’s import of utf8. When such a type ends up
in emitted code, the compiler writes it the way the calling module reaches
it.
In a generator body, plain if decides what gets emitted. Inside an
@emit body, use comptime if or comptime match: a plain
if (T == string) there would be runtime code, and T is not a runtime
value.
This is not generics: there are no type constraints and nothing is inferred. The caller names every type, and the generator’s own code decides what to emit for it.
Function parameters
A generator can take a function as well as types. The parameter’s type is
the full signature, and it may use the generator’s type parameters:
pub @tape fn gen_sort_by(T: type, less: fn(*const T, *const T) -> bool) {
@emit pub fn sort_by_$less(s: []$T) {
var i: i64 = 1;
while (i < s.len) {
var j: i64 = i;
while (j > 0 && less(&s[j], &s[j - 1])) {
let tmp: $T = s[j];
s[j] = s[j - 1];
s[j - 1] = tmp;
j = j - 1;
}
i = i + 1;
}
}
}
pub struct Point { x: i64; y: i64; }
fn point_less(a: *const Point, b: *const Point) -> bool { return a.x < b.x; }
fn by_y(a: *const Point, b: *const Point) -> bool { return a.y < b.y; }
@tape {
gen_sort_by(Point, point_less); // emits sort_by_point_less
gen_sort_by(Point, by_y); // emits sort_by_by_y
}- In emitted code,
less(...)calls the function directly. Nothing is captured and there is no function pointer: it is the same call you would write by hand. $lesssplices the function’s name, so each instantiation gets its own.- The argument must be a named function: one in your module, or a
pubfunction of an imported module (geo.point_less; emitted code calls it asgeo.point_less, and$lessis stillpoint_less). A generator can also pass its own function parameter on to another generator. - Function-pointer variables, methods,
@tape fns,extern fns and other expressions are rejected at the@tapecall. - The signature must match exactly, with the type parameters filled in.
gen_sort_by(i64, point_less)fails with “expected fn(*const i64, *const i64) -> bool, got fn(*const Point, *const Point) -> bool”. Const counts: afn(a: *Point, b: *Point) -> booldoesn’t match either, and the error says the two differ only in const. Comparison functions take*constpointers (the stdlib’ssort.gen_sort_bydoes too), so emitted code can pass pointers into a[]const T. @tapecode can call the function when its signature names notypeparameter: withf: fn(i64) -> i64,f(1)runs it at compile time, under compile-time code’s rules (it may not read a modulevaror call a capability module). A function whose signature names one (lessabove takes*const T) is not callable in@tapecode: a generator is compiled once, not once for eachT, so the call is a compile error that says so; call it in the emitted code.
@tape fn table(f: fn(i64) -> i64, n: i64) -> i64 {
var sum: i64 = 0;
for (i in 0..n) { sum = sum + f(i); }
return sum;
}
fn square(x: i64) -> i64 { return x * x; }
@tape const SQUARES: i64 = table(square, 4); // 0 + 1 + 4 + 9 = 14-
"{less}"in@tapecode is the function’s name as a string, the same name$lesssplices (as"{T}"is$T’s). A generator can hand it to a helper generator that builds its names from a string:tape@tape fn gen_core(T: type, sfx: string) { @emit pub fn is_sorted_${sfx}(s: []const $T) -> bool { /* ... */ } } pub @tape fn gen_sort_by(T: type, less: fn(*const T, *const T) -> bool) { gen_core(T, "by_{less}"); // emits is_sorted_by_point_less }The helper can stay private: a generator’s code calls its own module’s
@tape fns, whichever module it runs for. -
A generator can pass its own module’s
pubfunctions to a helper the same way, by their bare names: the emitted code calls them through the caller’s import (json.object()for a caller that wroteimport json from "json";, whatever alias it chose). This is how a generator’s emitted code reaches its own module’s functions:tape// json.tape pub fn object() -> Value { /* ... */ } pub @tape fn gen_codec(T: type) { emit_codec(T, object); } @tape fn emit_codec(T: type, mk_object: fn() -> Value) { var V: type = Value; // json.Value at the call site @emit fn to_json_$T(v: *const $T) -> $V { var o: $V = mk_object(); // json.object() /* ... */ } }
Generating containers of managed values
A generated container works for any element type, strings and structs with strings included, when its emitted code follows the pointer rules of Ownership:
pub @tape fn gen_stack(T: type) {
@emit pub struct Stack_$T { data: *$T; len: i64; cap: i64; }
@emit pub fn stack_${T}_push(s: *Stack_$T, item: $T) {
// ... grow with mem.copy (elements move as bytes) ...
var slot: *$T = (s.data as i64 + s.len * @sizeof($T)) as *$T;
*slot = item; // stores a copy
s.len = s.len + 1;
}
// The hooks the compiler calls to copy and drop a Stack_$T value
@emit pub fn Stack_${T}_drop(s: *Stack_$T) {
var i: i64 = 0;
while (i < s.len) {
@drop((s.data as i64 + i * @sizeof($T)) as *$T); // nothing for i64
i = i + 1;
}
// ... free the buffer, set len and cap to 0 ...
}
@emit pub fn Stack_${T}_copy(src: *Stack_$T) -> Stack_$T { /* *dst = *src per element */ }
}- A struct
Swith a functionS_drop(v: *S)is managed: the compiler callsS_dropwhen a value goes out of scope andS_copywhen one is copied. @sizeof($T)in emitted code is the size in the compiled program, right even whenTcontains the container being generated (atagged Valuewith aVec_Valuevariant).collections.gen_vecandgen_mapare written this way.
@emit const evaluation
Constants emitted with @emit const have their initializer evaluated at compile time:
@tape fn sizes(T: type) {
@emit const SIZE_${T} = @sizeof(T);
@emit const ALIGN_${T} = @alignof(T);
}The initializer expression is evaluated in the comptime scope, and the result (an integer, float, bool or string) replaces the init expression in the emitted AST.
Complete example
From programs/collections.tape:
struct Event {
id: i64;
name: [16]u8;
}
@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); }This generates Queue_i64, Queue_Event, and their push/pop functions.
@emit tagged and @emit union
A tagged union is emitted as it is written, and emitted or written code matches over it:
@tape fn gen_shape(T: type) {
@emit pub tagged Shape_$T { Circle { r: $T; } Rect { w: $T; h: $T; } Dot; }
@emit fn area_$T(s: Shape_$T) -> $T {
match (s) {
Shape_$T.Circle { r } => { return r * r * 3; }
Shape_$T.Rect { w; h } => { return w * h; }
Shape_$T.Dot => { return 0; }
}
return 0;
}
}
@tape { gen_shape(i64); } // Shape_i64 and area_i64@emit union Name { ... } declares an untagged union the same way. Both go
at module level, and a later @tape block reflects on them like on written
ones (@typeof(Shape_i64).variants, @sizeof).
Component members
In a component’s @tape block, a generator adds members the way they are
written: prop, var, state (a bool), event, slot and plain fn,
which is a method of the component. Names splice, in fire too:
@tape fn gen_action(name: string) {
@emit event on_$name(n: i64);
@emit state busy: bool = false;
@emit var count: i64 = 0;
@emit fn $name() { count = count + 1; fire on_$name(count); }
}
component Job {
@tape { gen_action("run"); } // event on_run, state busy, var count, fn run
}A prop’s and a var’s name splice like the others, with the type and the
default: one prop per state and color, for #style’s :hovered { bg: ...; }
to set:
@tape fn state_looks(states: []const string, props: []const string) {
for (s in states) {
for (p in props) { @emit prop ${s}_${p}: color = #00000000; }
}
}
component Chip {
@tape { state_looks(["hovered", "active"], ["bg", "fg"]); }
// props hovered_bg, hovered_fg, active_bg, active_fg
}They go only into a component: @emit event at module level, in a struct
or in a function is a compile error naming where the block runs.
@emit component
At module level a generator emits a whole component, written as you would
write it in #code, view tree included:
import ui from "ui";
import gfx from "gfx";
pub @tape fn gen_swatch(name: string, size: i64) {
@emit pub component $name {
@tape { ui.drawable(); } // runs once the component is emitted
prop size: i64 = ${size};
fn paint(s: *gfx.Surface, x: i64, y: i64, w: i64, h: i64) { /* ... */ }
}
}
@tape { gen_swatch("Swatch", 4); }
component Palette {
view { ui.Row { Swatch {} Swatch { size: 8; } } }
}The component’s own @tape blocks run after the generator’s block, in the
calling module, and reach the generator’s imports (ui above) by the
caller’s alias.
@emit test
A generator can ship tests with what it emits. Each lands in the #test
region of the module that called the generator, and runs with tape test
like a test written there:
pub @tape fn gen_box(T: type) {
@emit pub struct Box_$T { v: $T; }
@emit pub fn box_${T}_get(b: Box_$T) -> $T { return b.v; }
@emit test "box ${T} get" {
@assert(box_${T}_get(Box_$T { v: 7 as $T; }) == 7 as $T);
}
}
@tape { gen_box(i64); } // test "box i64 get"
@tape { gen_box(u8); } // test "box u8 get"
#test
test "written" { @assert(box_i64_get(Box_i64 { v: 1; }) == 1); }The test is compiled only when the #test region is (tape test,
tape check): a module without one, or a tape build, leaves it out.
A @tape block inside the #test region can emit helper functions next to
its tests; they are test code too.
@emit @tape fn
A generator can emit a generator. The emitted one is a @tape fn of the
calling module, for its later @tape blocks (and its importers’, when
pub):
@tape fn make_adder(prefix: string, k: i64) {
@emit pub @tape fn ${prefix}_gen(T: type, p: string = "${prefix}", n: i64 = $k) {
@emit pub fn ${p}_$T(x: $T) -> $T { return x + ($n as $T); }
}
}
@tape { make_adder("add", 5); } // the generator add_gen
@tape { add_gen(i64); } // fn add_i64(x: i64) -> i64Its name and parameter list splice when it is emitted; its body is copied as
written, so its $ splices are its own and run when it is called. It does
not see the emitting generator’s variables: hand it a value as a parameter
default, as n: i64 = $k does above.
@emit type
A type alias, pub or not, of a spliced type or any type built of one:
@tape fn gen_names(T: type, n: i64) {
@emit pub type Id_$T = $T;
@emit type Many_$T = [$n]$T;
@emit pub type Pred_$T = fn($T) -> bool;
}
@tape { gen_names(i64, 3); } // Id_i64, Many_i64 ([3]i64), Pred_i64@emit enum
An emitted enum takes its variants from a list: those written first, then a
comptime for (its last item) giving one or more per element, named by
splices and numbered on:
@tape fn gen_kinds(Name: []const u8, names: [][]const u8) {
@emit pub enum ${Name} : u16 {
None = 0;
comptime for (n in names) { ${n}; }
}
}
@tape { gen_kinds("Kw", ["Break", "Case"]); }
// enum Kw : u16 { None; Break; Case; }A variant takes a description as in a written enum, None = 0: "nothing";,
and in the comptime for it can splice the element:
comptime for (n in names) { ${n}: "the ${n} keyword"; } gives
Kw.desc(Kw.Case) == "the Case keyword".
A variant name must be an identifier and appear once. The stdlib’s
words builds word lookup tables this way.
Expression splices
${...} holds any compile-time expression of integer, bool or string type,
not only a name or a field path, wherever a splice goes:
@tape fn gen(n: i64, prefix: string) {
@emit fn ${prefix + "_double"}() -> i64 { return ${n * 2}; }
@emit var buf: [${n * 2}]u8;
@emit const LABEL: string = "n${n + 1}";
}List splices: one template for every arity
A comptime for can stand in a parameter list, a function type’s parameters
and a call’s arguments; each iteration adds its body’s items:
@tape fn gen_wrapper(name: string, ps: []const string, ts: []const string) {
let n = ps.len;
@emit fn ${name}(comptime for (i in 0..n) { ${ps[i]}: ${ts[i]}, }) -> i64 {
let f: fn(comptime for (t in ts) { $t, }) -> i64 = p_${name};
return f(comptime for (i in 0..n) { ${ps[i]}, });
}
}
@tape { gen_wrapper("add3", ["x", "y", "z"], ["i64", "i64", "i64"]); }
// fn add3(x: i64, y: i64, z: i64) -> i64 { ... return f(x, y, z); }A whole parameter list can come from reflection: ${f.params} in a
parameter list is one parameter per ParamInfo, and in an argument list one
argument per name:
@tape fn wrap_all(m: module) {
for (f in @moduleinfo(m).functions) {
@emit fn w_${f.name}(${f.params}) -> ${f.ret} { return $m.${f.name}(${f.params}); }
}
}The stdlib’s gl binding generates every GL function, whatever its number
of parameters, from one such template.
comptime for over a range
comptime for (j in a..b) unrolls once per integer, and $j is that integer
where an expression goes. With a compile-time string w, this emits a check
of every byte:
@tape fn gen_is(w: []const u8) {
@emit fn is_word(s: []const u8) -> bool {
if (s.len != ${w.len}) { return false; }
comptime for (j in 0..w.len) {
if (s[$j] != ${w[j]}) { return false; }
}
return true;
}
}The body is pasted once per iteration into the block around the loop. A local whose name splices the loop variable is one local per iteration of that block, and code after the loop reads it; a local with a plain name is the iteration’s own, as in an ordinary loop body:
@tape fn gen_sum4() {
@emit fn sum4(p: *const [4]i64) -> i64 {
comptime for (j in 0..4) {
let t: i64 = p[$j] * 2; // each iteration's own t
let v_$j: i64 = t + 1; // v_0 .. v_3, the function's
}
return v_0 + v_1 + v_2 + v_3;
}
}Last modified: