compile-time evaluation and metaprogramming
α2compile-time evaluation and metaprogramming
this feature is planned for α2. the spec describes what it will be, not what's implemented now.
no macros
ferrule has no macro system. no proc macros, no macro_rules!, no C preprocessor. instead, each use case for macros is handled by a dedicated, less-powerful mechanism:
- code generation:
derive+ comptime functions - conditional compilation:
whenblocks - DSLs (SQL, regex, format strings): tagged string literals
- boilerplate reduction: good generics + derive + effect polymorphism
- AST transformation: typed transforms (operate on typed IR, must produce valid output)
this keeps the language learnable and tooling-friendly.
comptime functions
functions marked comptime run at compile time:
comptime function crc16_table(poly: u16) -> Array<u16, 256> {
var table: Array<u16, 256> = [0; 256];
var i: u32 = 0;
while i < 256 {
var crc: u16 = u16(i) << 8;
var j: u32 = 0;
while j < 8 {
if (crc & 0x8000) != 0 {
crc = (crc << 1) ^ poly;
} else {
crc = crc << 1;
}
j = j + 1;
}
table[i] = crc;
i = i + 1;
}
return table;
}
const CRC16 = comptime crc16_table(0x1021);the result is computed at compile time and embedded in the binary.
rules
comptime functions must be pure and deterministic:
- no ambient io
- no effects (no
effects [...]declaration) - no error clause (can't fail)
- results are memoized by arguments
- results are cacheable across builds
this ensures builds are reproducible.
invocation
use the comptime keyword to evaluate at compile time:
const PAGE_SIZE = comptime layout.page_size();
const LOOKUP_TABLE = comptime generate_table();the result must be a constant-evaluable value.
when blocks for conditional compilation
when blocks select code paths at compile time based on the target platform or build configuration:
when platform.os == .linux {
function get_time() -> u64 effects [time] {
return syscall.clock_gettime(CLOCK_MONOTONIC)
}
}
when platform.os == .macos {
function get_time() -> u64 effects [time] {
return syscall.mach_absolute_time()
}
}
when platform.arch == .wasm32 {
function get_time() -> u64 effects [time] {
return wasi.clock_time_get(CLOCK_MONOTONIC)
}
}rules:
- evaluated at compile time
- the active branch is type-checked; dead branches are dropped
- not a preprocessor; operates on the AST, not text
- available conditions:
platform.os,platform.arch,platform.endian,build.mode(debug/release), custom build options
tagged string literals
tagged string literals invoke comptime functions that parse and validate the string at compile time. they return typed values.
const pattern = re"[a-z]+@[a-z]+\.[a-z]+"
const query = sql"SELECT id, name FROM users WHERE age > $1"
const msg = fmt"hello {name}, you are {age} years old"the re, sql, fmt prefixes are comptime functions. compile-time errors if the literal is invalid.
// how it works:
comptime function re(pattern: String) -> Regex {
// parse, validate at compile time
// compile error if invalid regex
}typed transforms
typed transforms operate on the typed IR (not raw syntax):
transform derive_serialize<T> {
// generates serialization code for type T
// output must pass all type checks
}use cases:
- ffi shim generation
- serialization/deserialization codecs
- cli argument parsers
- wasm component interfaces
transforms:
- receive typed ast nodes
- must produce valid, type-checked output
- are applied at compile time
reflection
query type layouts at compile time:
const page: usize = layout.page_size();
const alignOfBlob: usize = layout.alignof<Blob>();
const sizeOfBlob: usize = layout.sizeof<Blob>();available queries:
| function | returns |
|---|---|
layout.sizeof<T>() | size in bytes |
layout.alignof<T>() | alignment in bytes |
layout.page_size() | system page size |
layout.cache_line_size() | cache line size |
type introspection
limited introspection for transforms:
comptime function field_names<T>() -> Array<String, n> {
// returns field names of record type T
}
comptime function variant_names<T>() -> Array<String, n> {
// returns variant names of union type T
}example: lookup table
comptime function sin_table(steps: u32) -> Array<f32, steps> {
var table: Array<f32, steps> = [0.0; steps];
var i: u32 = 0;
while i < steps {
const angle = (f32(i) / f32(steps)) * 2.0 * math.PI;
table[i] = math.sin(angle);
i = i + 1;
}
return table;
}
const SIN_256 = comptime sin_table(256);what this enables
comptime and its related mechanisms are essential for:
- embedded: compute lookup tables at build time, not runtime
- zero-cost abstractions: generate specialized code
- derive: auto-generate serialization, comparison, etc.
- validation: ensure constants are valid at build time
- platform targeting:
whenblocks for clean conditional compilation - safe DSLs: tagged literals for compile-time-validated SQL, regex, format strings
the key is: if the compiler can compute it, do it at compile time, without a macro system.