Compile-Time Evaluation
Vex splits compile-time features into two groups:
#name(...)style compile-time functions, intrinsic helpers, and queries.#if,#elif,#else,#for,#while, and#constcompile-time control-flow and evaluation blocks.
Design Principle: No Runtime Cost
Vex uses the # prefix for all compile-time expressions and control flow. The $ prefix is reserved exclusively for runtime features that call compiler-internal runtime helper intrinsics (such as $print and $println), indicating they have a runtime execution cost.
Layout and Reflection
let size_a = #sizeof<i64>()
let align_a = #alignof<f64>()
let ty_name = #typeName<Vec<i32>>()
let field_count = #fieldCount<User>()
let variant_count = #variantCount<Result<i32, string>>()
let array_len = #arrayLen<[f32; 16]>()
let implements_copy = #implements<User, Copy>()The compiler supports additional compile-time type and layout reflection helpers:
#len<T>()/#length<T>()— Length of static arrays, tuples, etc.#rank<T>()/#ndim<T>()— Number of dimensions of a static tensor/array.#shape<T>()— Shape of a static tensor/array as a compile-time tuple.#tupleLen<T>()— Length of a tuple type.#arrayLen<T>()— Length of an array type.#elementType<T>()— Element type of static arrays, spans, or vectors.#implements<T, Contract>()— Compile-time check ifTimplements a contract.#fieldNames<T>()#hasField<T>(name)#fieldType<T>(name)#fieldTag<T>(field, key)#hasFieldTag<T>(field, key)#fieldTags<T>(field)#variantNames<E>()#hasVariant<E>(name)#variantDiscriminant<E>(name)#variantHasPayload<E>(name)#variantPayload<E>(name)
Compile-Time Type Predicates
Type predicates allow checking characteristics of types at compile time (e.g., inside #if statements):
#if #isStruct<T>() {
// T is a struct type
}
#if #needsDrop<T>() {
// T has drop semantics and owns resources
}
#if #sameType<T, U>() {
// T and U are the same type
}The full list of compile-time type predicates:
#isStruct<T>(): ReturnstrueifTis a struct or named user type.#isEnum<T>(): ReturnstrueifTis an enum type.#isPrimitive<T>(): ReturnstrueifTis a primitive type (scalar, boolean, char).#isInteger<T>(): ReturnstrueifTis an integer type.#isFloat<T>(): ReturnstrueifTis a floating-point type.#isSigned<T>(): ReturnstrueifTis a signed numeric type.#isPointer<T>(): ReturnstrueifTis a raw pointer type (*Uorptr).#isArray<T>(): ReturnstrueifTis a compile-time sized array.#isTuple<T>(): ReturnstrueifTis a tuple.#isCopy<T>(): ReturnstrueifTimplements the$Copycontract (trivially copyable).#needsDrop<T>(): ReturnstrueifThas custom drop logic or contains fields requiring drop.#isReference<T>(): ReturnstrueifTis a borrowed reference type (&U).#isFunction<T>(): ReturnstrueifTis a function pointer or function signature type.#isGeneric<T>(): ReturnstrueifTis unresolved or generic.#sameType<T, U>(): Returnstrueif typeTand typeUare identical.
#typeInfo<T>()
#typeInfo<T>() is the structured reflection entry point.
struct User {
id: i32 `json:"user_id" db:"primary_key"`,
name: string `json:"username"`,
}
fn dump_user_fields(u: User) {
#for f in #typeInfo<User>().fields {
$println("field: ", f.name, " type: ", f.type_name)
#for t in f.tags {
$println(" tag ", t.key, " = ", t.value)
}
let value = #getField(u, f)
$println(" value = ", value)
}
}Inside a #for f in #typeInfo<Point>().fields loop, the compiler also supports #setField:
fn rewrite_all_fields() {
let! p = Point { x: 1, y: 2 }
#for f in #typeInfo<Point>().fields {
#setField(p, f, 99)
}
}Compile-Time Control Flow
The parser and codegen support the following compile-time blocks:
#if #fieldCount<User>() > 0 {
#warning("User has fields")
} #elif #fieldCount<User>() == 0 {
#warning("User is empty")
} #else {
#compileError("unreachable comptime branch")
}
#for f in #typeInfo<User>().fields {
$println(f.name)
}
#while condition {
break
}
let value = #const {
2 + 2
}Diagnostics and Embedding
#staticAssert(#fieldCount<User>() == 2, "User shape changed")
#warning("legacy path still compiled")
#debugExpr(5 + 3)
let home = #env("HOME")
let config = #includeStr("config.json")
let raw = #includeBytes("blob.bin")
let source = #stringify(User { id: 1, name: "A" })
let ident = #concatIdents(foo, bar)The primary diagnostics and meta helpers documented by the compiler are:
#staticAssert#compileError#warning#debugExpr#env#includeStr#includeBytes#concat#stringify#concatIdents
Compile-Time Math and Bit Operations
let pow = #constPow(2, 10)
let abs = #abs(-42)
let min = #min(3, 5)
let max = #max(3, 5)
let clamp = #clamp(15, 0, 10)
let sqrt = #constSqrt(144)
let gcd = #gcd(48, 18)
let lcm = #lcm(4, 6)
let log2 = #log2(256)
let next = #nextPowerOf2(5)
let pop = #bitCount(0b1010101)
let clz = #leadingZeros(16)
let ctz = #trailingZeros(16)
let swapped = #bswap(0x12345678)
let is_pow2 = #isPowerOf2(64)Accepted aliases in the compiler include:
#constAbsfor#abs#constMinfor#min#constMaxfor#max#constClampfor#clamp#constLog2for#log2#nextPow2for#nextPowerOf2#popcountfor#bitCount#clzfor#leadingZeros#ctzfor#trailingZeros#reverseBytesfor#bswap#isPowerOfTwofor#isPowerOf2
Forcing Evaluation
let a = #eval(10 + 20)
let b = #constEval(50 + 50)#eval evaluates a comptime-capable expression. #constEval is the strict form and produces an error when the expression cannot be folded at compile time.
Defaults and Zeroed Values
let default_i32 = #default<i32>()
let zeroed_user = #zeroed<User>()Practical Guidance
- Prefer
#typeInfo<T>()when you need structured field iteration instead of parsing#fieldNames<T>()strings yourself. - Prefer
#staticAssertover comment-based assumptions. - Treat compatibility aliases as compatibility aliases; document the primary spelling you actually want other code to use.
- Keep
#forand#ifbodies simple. They are easiest to reason about when they expand straightforward source.