@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 { 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:
- Module level — emits module-level declarations (structs, fns, consts, globals)
- Inside struct bodies — emits struct fields
- Inside component bodies — emits component vars, props, 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.
Emit forms
| Syntax | Where | Injects |
|---|---|---|
@emit struct Name { ... } | module @tape | Struct declaration |
@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 @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
}
}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 _)${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:
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 and *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.
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. _ is the wildcard (always matches).
@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 (integer or bool) replaces the init expression in the emitted AST.
Complete example
From userspace/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.
Last modified: