Compile-Time Overview

Why not generics?

Tape does not have generics (<T>). Instead, compile-time code execution provides the same power:

  • No type inference complexity
  • No monomorphization
  • No trait bounds or variance
  • Explicit, debuggable, and visible

@tape blocks

tape
@tape {
    // this code runs at compile time
    gen_queue(i64);
    gen_queue(Event);
}

Code inside @tape { } executes during compilation. The compiler tree-walks the AST — arithmetic, variables, loops, conditionals, function calls, and @emit are all supported.

@tape fn

Compile-time functions are declared with @tape fn. They can be called from @tape {} blocks:

tape
@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); }

@emit

@emit injects declarations into the module being compiled:

FormInjects
@emit struct Name { ... }Struct declaration
@emit fn name(...) { ... }Function declaration
@emit const NAME = expr;Constant
@emit field: Type;Field (inside struct @tape {})
@emit prop name: Type;Prop (inside component @tape {})
@emit var name: Type;Var (inside component @tape {})
@emit @dispatch fn ...Dispatch (inside component @tape {})

$ splicing

Names containing $ are substituted at emit time:

  • $var — simple variable substitution (greedy identifier chars)
  • ${var} — braced substitution (explicit boundary)
  • ${var.field} — dotted path (access compound value fields)
tape
@tape fn gen_getter(T: type, field_name: string) {
    @emit fn get_${field_name}(obj: *${T}) -> i64 {
        return obj.${field_name};
    }
}

Values are spliced as: types → type name, strings → string value, integers → decimal text.

type as a value

At compile time, type is a first-class value. Type names resolve to type values:

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}");
    }
}

@typeof(T) returns a TypeInfo compound with fields: name, kind, size, align, fields. Each field element has: name, type, offset, size, __attrs.

comptime match

Match on a type value at compile time:

tape
@tape fn emit_default(T: type) {
    comptime match(T) {
        i64, i32 => { @emit const DEFAULT = 0; }
        f64      => { @emit const DEFAULT = 0.0; }
        _        => { @compile_error("unsupported type"); }
    }
}

Arms match type names. _ is the wildcard. Multiple types per arm separated by commas.

What can run at compile time

  • Arithmetic, comparisons, logical operators
  • let / var declarations and assignments
  • if / else, while, for loops
  • Function calls to @tape fn
  • String interpolation in @print / @println
  • @emit to generate declarations
  • @typeof, @sizeof, @alignof for type reflection
  • @has_field, @has_method, @inner_type, @field_type for type queries
  • @assert for static verification
  • @embed to read files into compile-time strings
  • @compile_error to abort with a diagnostic
  • comptime match(T) for type-based dispatch
  • field.has_attr(attr_name) for attribute queries

Fuel limit

Compile-time evaluation is limited to 1 million operations. Exceeding this produces an error. This prevents accidental infinite loops from stalling compilation.

Last modified: