@emit & Code Generation

Basic @emit

tape
@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:

  1. Module level — emits module-level declarations (structs, fns, consts, globals)
  2. Inside struct bodies — emits struct fields
  3. Inside component bodies — emits component props, vars, states, events, slots, methods and dispatch declarations
  4. 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

SyntaxWhereInjects
@emit struct Name { ... }module @tapeStruct declaration
@emit enum Name { ... }module @tapeEnum declaration (variants from a list: below)
@emit tagged Name { ... }module @tapeTagged union declaration
@emit union Name { ... }module @tapeUntagged union declaration (t0/t1, like a written one)
@emit type Name = T;module @tapeType alias ($T and types built of it on the right)
@emit component Name { ... }module @tapeComponent, any members, view { } and @tape blocks included
@emit test "name" { ... }module @tapeA test in the caller’s #test region
@emit @tape fn name(...) { ... }module @tapeA generator later @tape blocks call
@emit attr name;, @emit attr name(params);module @tapeAn attribute declaration
@emit @packed struct, @emit @export pub fn, …module @tapeAttributes before an emitted struct or fn, as written
@emit fn name(...) { ... }module @tapeFunction declaration
@emit fn Type.method(...) { ... }module @tapeMethod on a type
@emit const NAME = expr;module @tapeConstant (evaluated at comptime)
@emit var name: Type = expr;module @tapeModule-level mutable global
@emit let name: Type = expr;module @tapeModule-level immutable global
@emit var name: Type = expr;function @tapeLocal mutable binding
@emit let name: Type = expr;function @tapeLocal immutable binding
@emit field: Type;struct @tapeStruct field
@emit prop name: Type;component @tapeComponent prop
@emit var name: Type;component @tapeComponent var
@emit state name: bool = v;component @tapeInteraction state flag
@emit event name(params);component @tapeEvent
@emit slot name;component @tapeSlot
@emit fn name(...) { ... }component @tapeMethod of the component
@emit @dispatch fn name(...)component @tapeDispatch declaration

@emit var / let at module level

Emit global variables:

tape
@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
@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
@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
@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 sees

Names 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:

plaintext
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:

tape
// 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` needed

Inside 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
@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 _, so vec_$T_push reads a variable T_push; write vec_${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:

TypeIn a nameTypeIn a name
i64, Pointi64, Pointgeo.PointPoint
*Tptr_T*const Tcptr_T
*volatile Tvptr_T*const volatile Tcvptr_T
[]Tslice_T[]const Tcslice_T
?Topt_T[4]Tarr4_T
fn(A, B) -> Rfn_A_B_to_Rfn()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
@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
@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:

tape
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
@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
@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:

tape
comptime for (field, i in info.fields) {
    @println("field {i}: {field.name}");
}

comptime if

Conditional emission based on compile-time values:

tape
@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
@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
@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:

tape
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:

ExpressionMeaning
T == U, T != USame type. *S == *S, *S != S.
@typeof(T).kind == TypeKind.StructThe 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).lentrue for *const S and []const S; the N of [N]S
@typeof(T).is_volatiletrue 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:

WrittenType
i64, string, Point, Ta type name or a type parameter
geo.Pointa pub struct or enum of the imported module geo
*T, *const T, []T, []const T, ?Tpointers, slices, optionals
[4]u32, [SIZE]u8arrays (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:

tape
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.
  • $less splices the function’s name, so each instantiation gets its own.
  • The argument must be a named function: one in your module, or a pub function of an imported module (geo.point_less; emitted code calls it as geo.point_less, and $less is still point_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 @tape call.
  • 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: a fn(a: *Point, b: *Point) -> bool doesn’t match either, and the error says the two differ only in const. Comparison functions take *const pointers (the stdlib’s sort.gen_sort_by does too), so emitted code can pass pointers into a []const T.
  • @tape code can call the function when its signature names no type parameter: with f: fn(i64) -> i64, f(1) runs it at compile time, under compile-time code’s rules (it may not read a module var or call a capability module). A function whose signature names one (less above takes *const T) is not callable in @tape code: a generator is compiled once, not once for each T, so the call is a compile error that says so; call it in the emitted code.
tape
@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 @tape code is the function’s name as a string, the same name $less splices (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 pub functions 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 wrote import 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:

tape
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 S with a function S_drop(v: *S) is managed: the compiler calls S_drop when a value goes out of scope and S_copy when one is copied.
  • @sizeof($T) in emitted code is the size in the compiled program, right even when T contains the container being generated (a tagged Value with a Vec_Value variant).
  • collections.gen_vec and gen_map are written this way.

@emit const evaluation

Constants emitted with @emit const have their initializer evaluated at compile time:

tape
@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:

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
@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
@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
@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:

tape
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:

tape
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
@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) -> i64

Its 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
@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
@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
@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
@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
@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
@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
@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: