lang::comptime¶
Zig-style compile-time evaluation: comptime { ... } blocks, comptime fn calls, and comptime parameters run on the bytecode VM during compilation and fold to a literal, so every tier compiles the identical constant. typeInfo::<T>() reflects a type's fields, a for (name, ty) in typeInfo::<T>() loop unrolls into native per-field code, and codegen!(...) splices a comptime fn's String back as source. Includes the regex! / sql! build-time validation macros.
Zig-style compile-time evaluation. comptime runs ordinary Gossamer on
the bytecode VM during compilation and folds the result into the
program as a literal, so the bytecode VM, the Cranelift JIT, and the
LLVM AOT backend all compile the identical constant - comptime never
reaches a backend. There is no macro grammar, no hygiene model, and no
token-tree DSL: the metaprogramming you learn is just the language.
Comptime blocks and functions¶
A comptime { ... } block evaluates its body at compile time; a
comptime fn's calls are folded at every call site.
comptime fn fib(n: i64) -> i64 {
if n < 2 { n } else { fib(n - 1) + fib(n - 2) }
}
// The recursion runs at compile time; the binary embeds 6765.
const TABLE: i64 = comptime { fib(20) }
fn main() {
let triangular = comptime {
let mut acc = 0
for i in 1..=100 { acc += i }
acc
}
println!("{}", triangular) // 5050
println!("{}", fib(10)) // 55, computed at build time
}
A comptime region may use the full language - let, loops, if /
match, calls, string building - as long as every value it reads is
compile-time-known (literals, consts, comptime fn results).
Referencing a runtime binding is a compile error. A region must
evaluate to a scalar (i64 / u64 / bool / char / float) or a
String.
This also lets a const perform work the constant folder cannot do on
its own: const T: i64 = comptime { fib(20) } compiles natively, where
the bare const T: i64 = fib(20) form (a non-inlinable call in a const
initializer) does not.
Comptime parameters¶
A function parameter declared comptime has its argument evaluated at
compile time and replaced with the result literal, while the function
itself runs normally:
fn scale(comptime factor: i64, x: i64) -> i64 { factor * x }
fn main() {
let x = read_runtime_value()
// `BASE * 2 + 5` is folded to a constant at the call site.
println!("{}", scale(BASE * 2 + 5, x))
}
Passing a non-compile-time-known argument to a comptime parameter is
a compile error.
Reflection - typeInfo¶
typeInfo::<T>() reflects a struct's fields at compile time, returning
[(String, String)] of each field's (name, type). A comptime fn
consumes that to generate per-type code:
struct User { id: i64, name: String, active: bool }
comptime fn create_table(table: String, fields: [(String, String)]) -> String {
let mut cols = ""
let mut first = true
for (name, ty) in fields {
if !first { cols += ", " }
first = false
let sql_ty = match ty {
"i64" => "INTEGER",
"String" => "TEXT",
"bool" => "BOOLEAN",
_ => "BLOB",
}
cols += name + " " + sql_ty
}
"CREATE TABLE " + &table + " (" + &cols + ")"
}
// Evaluated at build time; the binary embeds the finished DDL string.
const USER_DDL: String = comptime { create_table("users", typeInfo::<User>()) }
// CREATE TABLE users (id INTEGER, name TEXT, active BOOLEAN)
The reflection is resolved at compile time and nothing about it survives into the running binary - the emitted code is exactly the hand-written string.
Code generation from reflection¶
Folding to a constant value is the floor; reflection also generates native code. Both shapes resolve entirely at compile time - the emitted body is ordinary native field code, identical on every tier with no runtime reflection.
A plain for (name, ty) in typeInfo::<T>() loop is unrolled once per
field in the single compile (no fold pass). name / ty are comptime,
field_of(v, name) projects the concrete field, and a match over the
comptime ty folds to the taken arm. Written once as a generic
fn rec<T> and specialised per turbofish call site, one reflection-driven
serializer covers every struct - the autoderive shape, in user space:
struct User { id: i64, name: String, active: bool }
fn record<T>(v: T) -> String {
let mut out = ""
for (name, ty) in typeInfo::<T>() {
let label = match ty {
"String" => "str",
"bool" => "bool",
_ => "int",
}
out += name + ":" + label + "=" + format!("{}", field_of(v, name)) + ";"
}
out
}
// id:int=7;name:str=jane;active:bool=true;
println!("{}", record::<User>(User { id: 7, name: "jane", active: true }))
A concrete-type loop (for (name, ty) in typeInfo::<Point>()) needs no
turbofish and is unrolled directly.
codegen!(...) - splices a comptime fn's String result back as raw
source, for generation beyond the field-loop shape. The spliced
expression is type-checked in place, so it must produce the type the call
site expects:
comptime fn gen_show(fields: [(String, String)], v: String) -> String {
let mut out = "\"\""
for (name, _) in fields {
out += " + \"" + name + "=\" + format!(\"{}\", " + v + "." + name + ") + \" \""
}
out
}
fn show(p: Point) -> String {
codegen!(gen_show(typeInfo::<Point>(), "p")) // emits: "" + "x=" + ... + " "
}
Compile-time validation - regex! / sql!¶
regex!("…") and sql!("…") validate their argument at build time and
fold to the validated string. A malformed pattern or statement fails the
build with a diagnostic rather than reaching runtime - the project's
"if it compiles, it works" goal:
let pattern = regex!("^\\d{4}-\\d{2}-\\d{2}$") // compiled + checked at build time
let query = sql!("SELECT id, name FROM users WHERE id = 1")
regex!("(unclosed") fails the build with unclosed group; an
unbalanced sql! fails with a parenthesis error. These are the only
compile-time-validation macros; every other name!(...) outside the six
format macros remains a parse error (GP0001).
How it works¶
After parsing, resolving, and typechecking, the compiler loads the
program onto the bytecode VM and evaluates every comptime region - a
comptime { ... } block, a comptime fn call, or a comptime
parameter's argument. Each result is spliced back into the source as a
literal, and the program is recompiled normally. A for over
typeInfo::<T>() skips the fold pass entirely: it is unrolled per field
in the single compile. Either way the substitution happens before the
tiers diverge, so all three tiers compile the same code: tier parity is
automatic.
Scope¶
The shipped surface is compile-time evaluation, reflection over
named structs, comptime parameters, build-time validation, and
code generation (a for over typeInfo::<T>() and codegen!).
Gossamer deliberately does not provide runtime reflection. Libraries that
need dynamic inspection must expose an explicit generated schema, tagged
representation, or comptime-generated adapter; typeInfo, autoderive,
and codegen! are the supported replacement for reflection-driven code.
Comptime regions fold to scalar or string results. You can now write the
reflection-driven autoderive shape yourself, but the built-in
to_json / from_json serializers remain built in (not yet re-expressed
as comptime library code); comptime { to_json(value) } runs them at
build time. See SPEC.md section 14.