@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 { data: [0; 4096]; len: 0; };
    }
}

@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 vars, props, 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.

Emit forms

SyntaxWhereInjects
@emit struct Name { ... }module @tapeStruct declaration
@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 @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
    }
}

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 _)
  • ${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.

@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 and *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.

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. _ is the wildcard (always matches).

@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 (integer or bool) replaces the init expression in the emitted AST.

Complete example

From userspace/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.

Last modified: