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):

DirectiveTypeDescription
@osstr"windows", "linux", "macos"
@archstr"x86_64", "aarch64"
@debugbooltrue if -g flag passed
@opti64Optimization 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:

DirectiveUsage
@c_include("header.h")Emit #include in generated C
@c_abiMark 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:

DirectiveEffect
@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:

NameDescription
tccBundled TCC (default on Windows) — fast, no optimization
clangLLVM Clang — must be on PATH or installed with Visual Studio
gccGCC/MinGW — must be on PATH
ccSystem default (default on macOS/Linux)
Full pathAny 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("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.