Compile-Time System
GX supports compile-time evaluation — code that runs during compilation, not at runtime. The result is zero-overhead: the C compiler sees only flat, fully-resolved code.
Two prefixes mark compile-time features:
#— Compile-time constructs (control flow that executes during compilation)@— Compile-time directives (annotations, queries, and metadata for the compiler)
# — Compile-Time Constructs
#if / #else — Compile-Time Conditional
Evaluates a condition at compile time. The dead branch is removed entirely — the resolver never sees it.
// Declaration-level (top-level)
#if (@os == "windows") {
const PLATFORM = "Windows";
} #else {
const PLATFORM = "Other";
}
// Statement-level (inside functions)
fn init() {
#if (@os == "windows") {
init_win32();
} #else {
init_posix();
}
}
// Inside struct bodies — conditional fields
struct Connection {
id: i32
#if (@os == "windows") {
handle: i64
} #else {
fd: i32
}
}
// Inside enum bodies — conditional members
enum LogLevel {
Error
Warning
Info
#if (@debug) {
Debug
Trace
}
}
#for — Compile-Time Loop (Unrolling)
Unrolls a loop at compile time. Uses the same syntax as GX’s for-range (var=start:end). Each iteration is stamped as separate statements — zero runtime loop overhead.
// Statement-level
#for (i=0:4) {
print("channel {i}");
}
// Unrolls to:
// print("channel 0");
// print("channel 1");
// print("channel 2");
// print("channel 3");
// Inside struct bodies — generated fields with name interpolation
struct Pixel {
#for (i=0:4) {
channel_i: f32
}
}
// Produces: struct Pixel { float channel_0; float channel_1; float channel_2; float channel_3; };
// Nested #for
#for (row=0:3) {
#for (col=0:3) {
print("({row},{col})");
}
}
// Const bounds
const N = 8;
#for (i=0:N) {
print("bit {i}");
}
#fn — Compile-Time Function
Defines a function that runs entirely at compile time. Called from const initializers to compute values during compilation.
#fn align_up:i32(size:i32, align:i32) {
return (size + align - 1) & ~(align - 1);
}
#fn fib:i32(n:i32) {
if (n <= 1) { return n; }
return fib(n - 1) + fib(n - 2);
}
const BUF_SIZE = align_up(100, 64); // 128 — computed at compile time
const FIB_10 = fib(10); // 55 — computed at compile time
Supports: local variables, if/else, while loops, recursion, arithmetic, comparisons, casts. No I/O, no pointers, no runtime calls.
#type — Compile-Time Type Parameter
Declares a type parameter for monomorphized generics.
#type T
struct List {
data: *T
len: i32
cap: i32
}
var nums: List<i32> // generates List_i32 struct
var names: List<str> // generates List_str struct
@ — Compile-Time Directives
The @ prefix marks compiler directives — annotations, platform queries, type reflection, and build configuration. None produce runtime code.
Platform & Build Info
Available everywhere (including #if conditions):
| Directive | Type | Description |
|---|---|---|
@os | str | "windows", "linux", "macos" |
@arch | str | "x86_64", "aarch64" |
@debug | bool | true if -g flag passed |
@opt | i64 | Optimization level (0-3) |
#if (@os == "windows" && @arch == "x86_64") {
// Windows x64 specific code
}
C Interop Annotations
Control how GX types and functions map to C:
| Directive | Usage |
|---|---|
@c_include("header.h") | Emit #include in generated C |
@c_abi | Mark function as C ABI callable |
@c_prefix("PREFIX_") | Map enum member names with prefix |
@c_name("exact_name") | Override C name for an enum member |
See 05_C_Interop.md for details.
Build Directives
Make .gx files self-contained — no CLI flags needed for linking or compilation:
| Directive | Effect |
|---|---|
@link("lib") | Add -llib to linker command |
@cflags("...") | Add flags to C compiler |
@ldflags("...") | Add flags to linker |
@cfile("path.c") | Compile a C source file alongside the GX output |
@compiler("name") | Set the C compiler (overridden by --cc) |
@cfile — Include C Source Files
Compiles a C file as part of the build. Path is resolved relative to the .gx
file containing the directive. Used for single-file C libraries:
@cfile("../c/sokol_impl.c")
@cflags("-Imodules/sokol/c")
@compiler — Set the C Compiler
Sets which C compiler to use. Accepts a short name (clang, gcc) or a full
path. The --cc CLI flag always takes priority over @compiler.
#if (@os == "windows") {
@compiler("clang")
}
Recognized compiler names:
| Name | Description |
|---|---|
tcc | Bundled TCC (default on Windows) — fast, no optimization |
clang | LLVM Clang — must be on PATH or installed with Visual Studio |
gcc | GCC/MinGW — must be on PATH |
cc | System default (default on macOS/Linux) |
| Full path | Any C compiler, e.g. "C:/raylib/w64devkit/bin/gcc.exe" |
Resolution order: --cc (CLI) > @compiler (directive) > -O1+ auto-upgrade > default (tcc/cc)
Why use it: Some modules require a specific compiler. For example, raylib
uses @compiler("clang") because TCC cannot link MSVC .lib files.
@link, @cflags, @ldflags — Linking and Flags
@link("opengl32")
#if (@os == "windows") {
@link("user32")
@link("gdi32")
} #else {
@ldflags("-lX11 -lGL")
}
@c_include("sokol_app.h")
All build directives compose with #if for platform-conditional builds. They propagate across module imports — import sokol.app inherits its @link flags automatically.
Reflection Introspection
Compile-time access to type metadata:
Struct / Union fields:
@fields(T) — field count (i64)
@field(T, i) — field name at index i (str)
@field_type(T, i) — field type name (str)
@field_is_ptr(T, i) — is pointer? (bool)
@field_is_array(T, i) — is fixed array? (bool)
obj.@field(i) — access field by compile-time index
Enum members:
@members(E) — member count (i64)
@member(E, i) — member name at index i (str)
Function params:
@params(F) — parameter count (i64)
@param(F, i) — parameter name (str)
@param_type(F, i) — parameter type name (str)
@return_type(F) — return type name (str)
Type meta:
@type_name(T) — type name as string
@type_kind(T) — "struct" / "enum" / "union" / "builtin"
Pipeline
Compile-time evaluation happens at two points in the compilation pipeline:
Lexer → Parser → pre_pass → Resolver → Type Checker → post_pass → C Transpiler
↑ ↑
#if (declaration-level) #if (statement-level)
#for (struct fields) #for (loop unrolling)
@link collection @fields / @field evaluation
Pre-pass (before resolution): Evaluates #if at declaration level, expands struct/enum compile-time constructs, collects build directives. Only built-in @ variables are available.
Post-pass (after type checking): Evaluates #if at statement level, unrolls #for loops, evaluates reflection intrinsics. User const values and type info are available.