ferrule

compile-time evaluation and metaprogramming

α2
comptime-functionstyped-transformsreflectiontype-introspectionwhen-blockstagged-literals

compile-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: when blocks
  • 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:

functionreturns
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: when blocks 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.

On this page