Skip to main content

Meta Functions

Verum uses the @ prefix consistently for compile-time constructs: attributes (@derive(Debug)), user-defined macros (@sql_query("...")), and a set of compiler-built meta functions documented here. No Rust-style ! suffix exists anywhere in the language.

Every meta function:

  • Runs at compile time, not runtime.
  • Has zero runtime cost — its result is folded into the output.
  • Can appear anywhere an expression is expected (and some also in attribute positions).

Grammar:

meta_function = '@' , meta_function_name , [ '(' , [ argument_list ] , ')' ] ;
meta_function_name = 'const' | 'error' | 'warning' | 'stringify' | 'concat' | 'cfg'
| 'file' | 'line' | 'column' | 'module' | 'function'
| 'type_name' | 'type_fields' | 'field_access'
| 'type_of' | 'fields_of' | 'variants_of'
| 'is_struct' | 'is_enum' | 'is_tuple' | 'implements' ;

Evaluation and diagnostics

@const(expr)

Forces compile-time evaluation of an expression. Useful when the compiler has not automatically constant-folded a call in a context where constness is required (e.g. array sizes, refinements).

const MAX_WINDOW: Int = @const(compute_max_window(128, 16));

type Buffer is [Byte; @const(max_size())];

@const can only be applied to expressions that are semantically pure and evaluable with only compile-time inputs. A call that reads state (&mut, IO, tick counters) is a compile error.

@error("msg")

Emits a compile-time error with the given message and aborts compilation.

#[cfg(target_endian = "big")]
@error("verum_foo requires little-endian");

Often used conditionally under @cfg(...) to prevent unsupported builds.

@warning("msg")

Emits a compile-time warning but continues the compilation. The message appears in verum build output with a [W] marker.

@warning("Deprecated: use the new API");

Token manipulation

@stringify(tokens)

Takes arbitrary tokens and returns their source form as a Text literal, verbatim (with whitespace normalised).

let lhs = @stringify(x + 2 * y); // "x + 2 * y"

Primarily used inside macros to produce human-readable error messages.

@concat(a, b, ...)

Concatenates its arguments — all literals — into a single Text, Ident, or integer at compile time.

const GREETING: Text = @concat("Hello, ", "world!");

Used inside macros to synthesise new identifiers:

meta fn getter(name: ident) -> TokenStream {
let g = @concat("get_", name);
quote { fn $g(&self) -> Self.$name { self.$name } }
}

Build configuration

@cfg(condition)

Evaluates a compile-time configuration predicate. Returns a Bool.

Conditions can check:

  • A build flag: @cfg(feature = "async")
  • A target: @cfg(target_os = "linux")
  • A profile: @cfg(profile = "release")
  • A combination: @cfg(all(unix, feature = "foo"))
  • A negation: @cfg(not(windows))
  • Any of: @cfg(any(debug, test))
if @cfg(feature = "metrics") {
record_metric("request", duration);
}

// Compile-time branch — dead code eliminated:
type Backend is @cfg(target_os = "linux") { EpollBackend }
else { KqueueBackend };

See @cfg conditions in the full form at reference/attribute-registry.

Source location

These meta functions take no arguments and return the position of the call site.

@file()

Returns the source file path as a Text.

print(f"logged from {@file()}"); // logged from src/foo.vr

@line()

Returns the source line number as an Int.

print(f"at line {@line()}"); // at line 42

@column()

Returns the column (1-based) as an Int.

@module()

Returns the current module's dotted path as a Text.

@module() // "crate.net.server"

@function()

Returns the current function's name as a Text.

fn handle(req: Request) -> Response {
log(@function(), req); // "handle"
...
}

Type introspection

Compile-time type introspection lets macros inspect type structure. All these return compile-time values used inside meta fn bodies.

@type_name<T>()

Canonical name of T as a Text. Equivalent to T.name (see Type Properties).

@type_name<User>() // "crate.models.User"
@type_name<List<Int>>() // "core.collections.List<core.base.Int>"

@type_of(expr)

The type of expr, as a compile-time type value.

let t = @type_of(user.email); // Text
if @is_struct(@type_of(x)) { ... }

@type_fields<T>()

Returns a compile-time List<FieldDescriptor> describing T's fields — name, type, offset, attributes.

meta fn describe<T>() {
for field in @type_fields<T>() {
@println(f" {field.name}: {field.type_name}");
}
}

@field_access<T>(instance, field_name)

Produces an expression that accesses the named field on a value of type T. Equivalent to instance.field_name, but computable at compile time inside macros.

@fields_of<T>()

Returns a compile-time List<Text> of T's field names (in declaration order). For records and tuple-like types.

@variants_of<T>()

Returns the variant names of a variant (sum) type as a compile-time List<Text>.

type Event is Click | Keypress | Tick;

const NAMES: List<Text> = @variants_of<Event>(); // ["Click", "Keypress", "Tick"]

@is_struct<T>()

Is T a record type? Returns Bool at compile time.

@is_enum<T>()

Is T a variant (sum) type?

@is_tuple<T>()

Is T a tuple type?

@implements<T, P>()

Does T implement protocol P?

meta fn debug_if_possible<T>(x: T) {
if @implements<T, Debug>() {
@println("{x:?}");
} else {
@println("<{@type_name<T>()}>");
}
}

Build-asset embedding

Compile-time file loading is sandboxed behind the BuildAssets context. All paths are restricted to the project root and configured asset directories — absolute paths and .. traversal are rejected with a meta error.

@embed(path) — file bytes literal

Loads the file at path (relative to the project root) and substitutes its bytes as a Bytes constant in the AST. Equivalent to calling include_bytes(path) from inside a meta fn but spelled as an attribute-style macro call so it composes naturally with constant declarations.

const ICON: Bytes = @embed("assets/icon.png");
const FONT: Bytes = @embed("fonts/Inter-Regular.ttf");

The macro call evaluates at compile time; the resulting bytes are baked into the binary. Path traversal (..) and absolute paths produce a compile error rather than reaching the filesystem.

@embed_glob(pattern) — match-many bytes literal

Walk the project tree and return every file matching the pattern as a List<(Text, Bytes)> keyed on the relative path.

// Direct children of assets/icons/
const ICONS: List<(Text, Bytes)> = @embed_glob("assets/icons/*.png");

// Every PNG anywhere under assets/, at any depth
const ALL_IMAGES: List<(Text, Bytes)> = @embed_glob("assets/**/*.png");

// Every file in the project, at any depth
const ALL_FILES: List<(Text, Bytes)> = @embed_glob("**");

Pattern grammar:

TokenMeaning
*zero or more chars within one path component (does not cross /)
?exactly one char within one path component
**zero or more path components — recursive descent. Must be a standalone component (a**b is rejected)
literalverbatim match

Output is sorted lexicographically by relative path so generated bytecode is reproducible across platforms regardless of readdir ordering. Walk depth is capped at 64 levels to prevent symlink-induced cycles from exhausting the stack.

Sandbox: absolute paths and .. traversal are rejected before the filesystem walk starts; symlinks crossing the project root are rejected by the per-file resolve_path precheck.

include_bytes(path) — meta-fn form

meta fn embed_icon(name: Text) -> Bytes using [BuildAssets] {
let path = text_concat("icons/", name, ".png");
include_bytes(path)
}

Same dispatcher as @embed; pick whichever form composes better with the surrounding code. The meta-fn form is the right choice when the path is computed (loops, cog-info-driven prefixes, etc.); @embed is the right choice for a literal constant declaration.

load_text(path) / include_str(path) — UTF-8 text

Read a file as Text instead of Bytes. Both names register the same dispatcher; pick whichever reads better at the call site.

meta fn load_template(name: Text) -> Text using [BuildAssets] {
let path = text_concat("templates/", name, ".html");
include_str(path)
}

@codegen(path) / load_toml(path) — declarative spec parsing

Parse a TOML document at compile time and lift it into a Map<Text, Any> for downstream meta-fn consumption. The MVP foundation for @codegen user-meta-fn invocation: once a meta fn can take the parsed spec as a Map, it can build whatever declarations it needs from the data — record types from a schema table, intrinsic stubs from a syscall list, lookup tables from a config file.

const SPEC: Map<Text, Any> = @codegen("schemas/users.toml");
// SPEC["table"] == Text("users")
// SPEC["columns"] == Array([Map{name=..., kind=...}, ...])
TOML shapeMetaValue shape
stringText
integerInt
floatFloat
booleanBool
datetime (RFC 3339)Text (Display form)
arrayArray<MetaValue>
tableMap<Text, MetaValue>

The root document must be a top-level table. Bare values (name = "x" is a table with one key, but [42, 43] as the whole document) are rejected with a clear diagnostic.

Both load_toml (function-call form) and codegen (macro-call attribute form) resolve to the same dispatcher; pick whichever reads better at the call site. Both inherit the standard BuildAssets sandbox: absolute paths, .. traversal, and symlinks crossing the project root all fail before the filesystem is touched.

Asset queries

FunctionSignatureDescription
asset_exists(path)(Text) -> BoolTrue if the file exists
asset_list_dir(path)(Text) -> List<Text>List directory entries
asset_metadata(path)(Text) -> (UInt, UInt, Bool, Bool, Bool)size, mtime ns, is_dir/file/symlink

All four require using [BuildAssets] in a meta fn; the attribute forms (@embed, @include_str) inherit the same sandbox automatically.

Sandbox guarantees

  • Absolute paths rejected. /etc/passwd and similar fail the precheck before any filesystem syscall fires.
  • No .. traversal. A path containing .. returns MetaError.Other("Path traversal …").
  • No symlinks across the root boundary. A symlink under the project root that resolves outside the root fails the same precheck.
  • No environment variable expansion. Paths are taken as literal source-relative paths.

Version stamping

Compile-time injection of (cog version, git revision, build time) without forcing the build to drag in network or environment- variable access. The pipeline driver populates the underlying data substrate (ProjectInfoData.git_revision, build_time_unix_ms); the builtins read it.

@version_stamp() — canonical triple

const STAMP: (Text, Text, UInt) = @version_stamp();
// (cog version, git SHA-1, build time ms since unix epoch)

The triple is the canonical shape. Each component has a deterministic fallback so the generated bytecode is identical regardless of whether git is on PATH or whether the build is running in --no-version-stamp reproducible mode:

ComponentSourceFallback
versionversion field of Verum.tomlthe field itself (cogs MUST declare a version)
git revisiongit rev-parse HEAD at pipeline startempty string ""
build time msSystemTime.now() at pipeline start0

@project_git_revision() — bare SHA

const REV: Text = @project_git_revision();

Returns just the SHA-1 (or empty string when unavailable). Useful for log banners, HTTP User-Agent, panic-handler breadcrumbs — anywhere only the revision is needed and pulling in version + timestamp would be noise.

@project_build_time_ms() — bare timestamp

const BUILT_AT: UInt = @project_build_time_ms();

Returns just the millisecond stamp (or 0 when suppressed).

Usage in macros

Meta functions are at their most useful inside meta fn bodies, where they cooperate with quote { ... }:

meta fn derive_display<T>() -> TokenStream {
let name = @type_name<T>();
let fields = @type_fields<T>();

quote {
implement Display for $T {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
f.write(f"{$name} {{");
$[for field in fields {
f.write(f" {${field.name}}: {self.${field.name}}");
}]
f.write("}")
}
}
}
}

Summary

FunctionReturnsStage
@const(e)value of ecompile
@error(msg)! (aborts)compile
@warning(msg)()compile
@stringify(t)Textcompile
@concat(a, b, …)literalcompile
@cfg(cond)Boolcompile
@file()Textcompile
@line()Intcompile
@column()Intcompile
@module()Textcompile
@function()Textcompile
@type_name<T>()Textcompile
@type_of(e)typecompile
@type_fields<T>()List<FieldDescriptor>compile
@fields_of<T>()List<Text>compile
@variants_of<T>()List<Text>compile
@is_struct<T>()Boolcompile
@is_enum<T>()Boolcompile
@is_tuple<T>()Boolcompile
@implements<T,P>()Boolcompile
@field_access<T>(e, f)expressioncompile
@embed(path)Bytescompile (BuildAssets)
@embed_glob(pat)List<(Text, Bytes)>compile (BuildAssets)
@codegen(p) / load_toml(p)Map<Text, Any>compile (BuildAssets)
include_bytes(p)Bytescompile (BuildAssets, meta-fn form)
load_text(p) / include_str(p)Textcompile (BuildAssets)
asset_exists(p)Boolcompile (BuildAssets)
asset_list_dir(p)List<Text>compile (BuildAssets)
asset_metadata(p)(UInt, UInt, Bool, Bool, Bool)compile (BuildAssets)
@version_stamp()(Text, Text, UInt)compile (ProjectInfo)
@project_git_revision()Textcompile (ProjectInfo)
@project_build_time_ms()UIntcompile (ProjectInfo)

See also