Compile-Time Reflection

@typeof(T)

Returns a TypeInfo compound value at compile time:

tape
@tape fn inspect(T: type) {
    var info = @typeof(T);
    @println("type: {info.name}, size: {info.size}, align: {info.align}");
}

TypeInfo fields:

FieldTypeDescription
namestringType name
kindintType kind, compared with TypeKind: info.kind == TypeKind.Struct
sizeintSize in bytes
alignintAlignment requirement
fieldsarrayArray of field descriptors
variantsarrayEnum and tagged-union variants, each with name, value (enum discriminant) and fields (tagged payload)
is_constbooltrue for *const T and []const T
is_volatilebooltrue for *volatile T and *const volatile T
lenintN for an array [N]T, else 0
is_coroutinebooltrue for a coroutine’s frame type: a generator saves it with @frame_save rather than field by field (coroutines)
methodsarrayThe type’s methods, each a function descriptor; a component’s methods too
props, vars, slotsarrayA 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
eventsarrayA 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:

FieldTypeDescription
namestringField name
typetypeField type (as type value)
offsetintByte offset in struct
sizeintField size in bytes
__attrsarrayAttributes applied to this field
has_defaultboolThe field declares a default (x: i64 = 0;): a literal may leave it out
tape
@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:

FieldTypeDescription
namestringThe function’s name (a method’s without its type)
paramsarrayIts parameters, each with name, type and keeps (marked @keeps); a method’s receiver is not one of them
rettypeIts result type, void when it returns nothing; T of a T or E result
errtypeE of a T or E result, void otherwise
recvReceiverHow 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
typetypeThe function’s type, fn(A, B) -> R (void when the result is T or E, which is no type value)
kindTypeKindAlways TypeKind.Function

A function’s attributes are asked as a field’s are, f.has_attr(a) and f.attr(a).

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

tape
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
@tape { @assert(@sizeof(u64) == 8); }
@tape { @assert(@sizeof(i32) == 4); }

@alignof(T)

Returns the alignment requirement of a type:

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

tape
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
@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
@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
@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
@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
@tape { @assert(@sizeof(Header) == 64); }
@tape { @assert(@alignof(Vec4) == 16); }

Takes a single boolean expression.

PlannedA second argument for a custom error message (@assert(cond, "message")) is not yet implemented.

@compile_error

Abort compilation with a diagnostic message:

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

Compile-time output (prints during compilation, not at runtime):

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

tape
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");
        }
    }
}
MethodReturnsDescription
field.has_attr(attr)boolWhether the field has this attribute
field.attr(attr)compoundThe 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, compoundThe same for the struct’s own attributes (@table("users") struct User)
f.has_attr(attr), f.attr(attr) of @moduleinfo(m).functionsbool, compoundThe 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.

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

tape
// 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 count
  • arr[i] — index access
  • for (elem in arr) — iteration
  • for (elem, i in arr) — iteration with index

Last modified: