Compile-Time Reflection
@typeof(T)
Returns a TypeInfo compound value at compile time:
@tape fn inspect(T: type) {
var info = @typeof(T);
@println("type: {info.name}, size: {info.size}, align: {info.align}");
}TypeInfo fields:
| Field | Type | Description |
|---|---|---|
name | string | Type name |
kind | int | Type kind, compared with TypeKind: info.kind == TypeKind.Struct |
size | int | Size in bytes |
align | int | Alignment requirement |
fields | array | Array of field descriptors |
variants | array | Enum and tagged-union variants, each with name, value (enum discriminant) and fields (tagged payload) |
is_const | bool | true for *const T and []const T |
is_volatile | bool | true for *volatile T and *const volatile T |
len | int | N for an array [N]T, else 0 |
is_coroutine | bool | true for a coroutine’s frame type: a generator saves it with @frame_save rather than field by field (coroutines) |
methods | array | The type’s methods, each a function descriptor; a component’s methods too |
props, vars, slots | array | A component’s props, vars and slots, each a field descriptor (name, type, has_default; a slot’s type is void); empty for any other type |
events | array | A component’s events, each a function descriptor of its parameters |
TypeKind is a builtin name in @tape code with the variants Primitive,
Struct, Enum, Tagged, Pointer, Slice, Array, Function,
Optional and Union. string is a Primitive; *T and *const T are
Pointer, []T and []const T are Slice, ?T is Optional, [N]T is
Array and fn(A) -> R is Function. See type dispatch
for branching on it in a generator.
Field descriptors
Each element in info.fields is a compound with:
| Field | Type | Description |
|---|---|---|
name | string | Field name |
type | type | Field type (as type value) |
offset | int | Byte offset in struct |
size | int | Field size in bytes |
__attrs | array | Attributes applied to this field |
has_default | bool | The field declares a default (x: i64 = 0;): a literal may leave it out |
@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}");
}
}A field descriptor’s type is named FieldInfo in @tape code (@tape fn width(f: FieldInfo) -> i64), a type descriptor’s TypeInfo, a function
descriptor’s FnInfo. Your module can still have its own struct FieldInfo
(or TypeInfo, TypeKind, …): in that module the name is yours, and
reflection keeps working, @typeof(T).fields included.
Function descriptors
@typeof(f) of a function, written by name (add, geo.dist) or a
generator’s fn parameter, gives a function descriptor, and so does each
element of info.methods, info.events and @moduleinfo(m).functions:
| Field | Type | Description |
|---|---|---|
name | string | The function’s name (a method’s without its type) |
params | array | Its parameters, each with name, type and keeps (marked @keeps); a method’s receiver is not one of them |
ret | type | Its result type, void when it returns nothing; T of a T or E result |
err | type | E of a T or E result, void otherwise |
recv | Receiver | How a method takes its receiver: Receiver.Value (self), Receiver.Ptr (*self), Receiver.ConstPtr (*const self); Receiver.None for a function or a method without one |
type | type | The function’s type, fn(A, B) -> R (void when the result is T or E, which is no type value) |
kind | TypeKind | Always TypeKind.Function |
A function’s attributes are asked as a field’s are, f.has_attr(a) and
f.attr(a).
struct Counter { n: i64; }
fn Counter.add(*self, d: i64) { self.n = self.n + d; }
fn Counter.get(*const self) -> i64 { return self.n; }
@tape {
for (m in @typeof(Counter).methods) {
@println("{m.name}: {m.params.len} parameter(s), returns {m.ret}");
}
// add: 1 parameter(s), returns void
// get: 0 parameter(s), returns i64
}With the parameter-list splice a generator can write a method
for every method a type has. This one forwards each to a field, as the
standard library’s forward module does:
@tape fn forward_to(T: type, field: string) {
for (m in @typeof(@field_type(T, field)).methods) {
let R: type = m.ret;
if (m.recv == Receiver.Ptr && R == void) {
@emit fn $T.${m.name}(*self, comptime for (p in m.params) { ${p.name}: ${p.type}, }) {
self.${field}.${m.name}(comptime for (p in m.params) { ${p.name}, });
}
} else if (m.recv != Receiver.Ptr && m.recv != Receiver.None && m.err == void) {
@emit fn $T.${m.name}(*const self, comptime for (p in m.params) { ${p.name}: ${p.type}, }) -> $R {
return self.${field}.${m.name}(comptime for (p in m.params) { ${p.name}, });
}
}
}
}A component’s members list the same way:
component Knob {
prop label: string = "knob";
var value: i64 = 0;
event on_change(v: i64);
slot body;
}
@tape {
let k = @typeof(Knob);
@assert(k.props[0].name == "label" && k.props[0].has_default);
@assert(k.events[0].params[0].type == i64);
@assert(k.vars.len == 1 && k.slots[0].name == "body");
}@sizeof(T)
Returns the size in bytes of a type:
@tape { @assert(@sizeof(u64) == 8); }
@tape { @assert(@sizeof(i32) == 4); }@alignof(T)
Returns the alignment requirement of a type:
@tape { @assert(@alignof(f64) == 8); }
@tape { @assert(@alignof(u8) == 1); }At runtime
@sizeof and @alignof work in ordinary code too, as integer constants: the
layout the compiler gives the type in memory, the same numbers as at compile
time. The type can be a name (Point, mod.Point), a primitive, or written
out (@sizeof([4]u16), @sizeof(*u8), @sizeof(?i64)). Structs are laid
out like C.
struct P { a: u8; b: i64; }
let bytes: i64 = count * @sizeof(P); // 16 per element
let al = @alignof(P); // 8@has_field(T, name)
Returns true if the struct type has a field with the given name:
@tape fn maybe_print_name(T: type) {
comptime if (@has_field(T, "name")) {
@emit fn print_name(obj: *${T}) {
io.println(obj.name);
}
}
}@has_method(T, name)
Returns true if a method with the given name exists on the type (checks receiver functions in the current module):
@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>"); }
}
}@field_type(T, name)
Returns the type of a named field as a type value:
@tape fn check_field(T: type) {
var ft = @field_type(T, "id");
comptime match(ft) {
i64 => { @println("id is i64"); }
_ => { @println("id is something else"); }
}
}@field_type returns a comptime type value. Bind it to a name and splice it into emitted declarations: let F: type = @field_type(T, "x"); then @emit fn get_x(p: *$T) -> $F { ... }.
@inner_type(T)
Returns the inner type of a pointer, slice, optional or array (const or not), written directly or held in a type parameter:
@tape fn elem_size(T: type) -> i64 {
@assert(@typeof(T).kind == TypeKind.Slice);
return @sizeof(@inner_type(T));
}
@tape { @assert(elem_size([]u32) == 4); }
@tape { @assert(@inner_type(*Point) == Point); }Any other type is a compile error.
@assert
Static assertion — produces a compile error if the condition is false:
@tape { @assert(@sizeof(Header) == 64); }
@tape { @assert(@alignof(Vec4) == 16); }Takes a single boolean expression.
@assert(cond, "message")) is not yet implemented.@compile_error
Abort compilation with a diagnostic message:
@tape fn require_field(T: type, name: string) {
comptime if (!@has_field(T, name)) {
@compile_error("type is missing required field");
}
}The error points at the @compile_error, with a note at the call that
reached it. @compile_error never returns, as @trap does not: a branch,
a @tape fn body or an or { } block that ends in one needs no return
and no value, so if (T == i64) { return 8; } else { @compile_error("no"); }
is a complete body for a @tape fn returning i64. Only a branch that is
taken fires.
@embed(path)
Read a file at compile time into a string value:
@tape {
var shader_src = @embed("shaders/vertex.glsl");
@emit const VERTEX_SHADER = shader_src;
}A path starting with ./ or ../ is relative to the directory of the source file; any other path is relative to the working directory (see @embed). Maximum file size: 16 MB.
@print / @println
Compile-time output (prints during compilation, not at runtime):
@tape {
@print("compiling...");
@println("struct size = {info.size}");
}Supports string interpolation with {expr} inside the message. Values of type int, string, bool, and type are printed. Type values print their name.
Attribute reflection
Field descriptors carry their __attrs array, accessible via .has_attr() and .attr():
attr serializable;
attr max_length(n: i64);
@tape fn gen_validate(T: type) {
var info = @typeof(T);
comptime for (field in info.fields) {
comptime if (field.has_attr(serializable)) {
@println(" {field.name} is serializable");
}
}
}| Method | Returns | Description |
|---|---|---|
field.has_attr(attr) | bool | Whether the field has this attribute |
field.attr(attr) | compound | The attribute’s arguments, each read by its parameter name from the attr declaration: field.attr(max_length).n. A compile error on a marker attribute (no parameters) |
@typeof(T).has_attr(attr), @typeof(T).attr(attr) | bool, compound | The same for the struct’s own attributes (@table("users") struct User) |
f.has_attr(attr), f.attr(attr) of @moduleinfo(m).functions | bool, compound | The same for a module’s function (@scriptable pub fn gold()) |
attr(a) names the attribute itself (max_length, serial.skip), so its
parameters, and so the fields of what it returns, are known. Asking for an
attribute the field does not have is a compile error at the call: ask
has_attr first.
// round.tape
pub attr scriptable;
@scriptable pub fn gold() -> i64 { return 300; }
pub fn serve() {}
// a generator: the marked functions of a module
@tape fn marked(m: module) {
for (f in @moduleinfo(m).functions) {
if (f.has_attr(round.scriptable)) { @println("{f.name}"); } // gold
}
}An attribute is matched by its declaration, not by the name a module
gives it. A caller that writes import s from "serial"; and marks a field
@s.skip has the attribute a generator asks for as serial.skip, and
that serial’s own @tape functions ask for as bare skip. So a module
can declare attributes together with @tape helpers that read them, and
any generator can use those helpers:
// serial.tape
pub attr skip;
pub @tape fn included(f: FieldInfo) -> bool { return !f.has_attr(skip); }
// a generator elsewhere
comptime for (f in @typeof(T).fields) {
comptime if (serial.included(f)) { /* ... */ }
}Array operations in comptime
Comptime arrays (like info.fields) support:
.len— element countarr[i]— index accessfor (elem in arr)— iterationfor (elem, i in arr)— iteration with index
Last modified: