Compile-Time Basics

GX evaluates a lot of work at compile time so your runtime is faster. Constants get folded into literals, conditional blocks get eliminated, and platform-specific code gets stripped from the output.

Constants

const declares a value that’s known at compile time:

const PI = 3.14159265
const MAX_PLAYERS = 8
const WINDOW_TITLE = "My Game"

fn main() {
    print("pi = {PI}\n")
    print("max = {MAX_PLAYERS}\n")
    print("{WINDOW_TITLE}\n")
}

Unlike var, a constant cannot be reassigned. Unlike runtime values, the compiler substitutes the literal directly into the generated C code.

Constant Folding

Arithmetic on constants is computed at compile time:

const SCREEN_W = 1920
const SCREEN_H = 1080
const HALF_W = SCREEN_W / 2
const AREA = SCREEN_W * SCREEN_H

fn main() {
    print("half width: {HALF_W}\n")    // compiler emits 960
    print("area: {AREA}\n")              // compiler emits 2073600
}

The generated C contains 960 and 2073600 as literals — no runtime multiplication.

Compile-Time Variables

Special variables starting with @ give you info about the build environment:

fn main() {
    print("os: {@os}\n")           // "windows", "linux", "macos", or "web"
    print("arch: {@arch}\n")       // "x86_64" or "aarch64"
    print("debug build: {@debug}\n")
    print("opt level: {@opt}\n")   // 0, 1, 2, or 3
}

These are resolved by the compiler to literals before the program runs.

Conditional Compilation with #if

Use #if to include or exclude code based on compile-time conditions:

fn main() {
    #if (@os == "windows") {
        print("running on Windows\n")
    }
    #if (@os == "linux") {
        print("running on Linux\n")
    }
    #if (@os == "macos") {
        print("running on macOS\n")
    }
}

When building on Windows, only the Windows branch appears in the generated C — the other branches are completely removed, not just skipped at runtime.

Debug-Only Code

Strip expensive debug output from release builds:

fn expensive_check:bool(x:i32) {
    return x > 0
}

fn main() {
    var value = 42

    #if (@debug) {
        if (!expensive_check(value)) {
            print("debug: bad value\n")
        }
    }

    print("result: {value}\n")
}

Build with gx file.gx -g to enable debug mode. The debug block disappears in optimized builds.

Platform-Specific Module Directives

The most common use of #if is in modules to select the right libraries per platform:

module mymodule

@cflags("-Imodules/mymodule/c")

#if (@os == "windows") {
    @link("user32")
    @link("gdi32")
}
#if (@os == "linux") {
    @link("X11")
    @link("pthread")
}
#if (@os == "macos") {
    @ldflags("-framework Cocoa")
}

This lets one source file build cleanly on every platform.

Comptime Constants in Expressions

Constants can be used anywhere a literal would be:

const BUFFER_SIZE = 256
const NUM_SLOTS = 8

fn main() {
    var buffer:u8[256]      // size must be a constant
    var slots:i32[8]

    print("buffer: {BUFFER_SIZE} bytes\n")
    print("slots: {NUM_SLOTS}\n")
}

Array sizes must be known at compile time, which is exactly what const provides.

Practical Example: Build Configuration

const GAME_VERSION = "1.2.0"
const MAX_ENTITIES = 4096
const TICK_RATE = 60
const TICK_MS = 1000 / TICK_RATE   // 16

fn main() {
    print("=== My Game v{GAME_VERSION} ===\n")
    print("Max entities: {MAX_ENTITIES}\n")
    print("Tick rate: {TICK_RATE} Hz ({TICK_MS}ms per tick)\n")

    #if (@debug) {
        print("[debug build]\n")
    }
    #if (@os == "windows") {
        print("Platform: Windows\n")
    }
    #if (@os == "linux") {
        print("Platform: Linux\n")
    }
}

Try it — Define const GRAVITY = 9.81 and use it in a computation. Add a #if (@debug) block that prints extra info in the Playground.


Expert Corner

What gets folded: The constant folder handles arithmetic (+ - * / %), bitwise ops (& | ^ ~ << >>), comparisons (< > <= >= == !=), boolean logic (&& || !), ternary ? :, sizeof, and casts. Any expression built from literals and other constants can be folded. The result appears in the generated C as a raw literal — no runtime computation.

Compile-time vs run-time evaluation: GX has two levels of evaluation:

  1. Constant folding — the simpler one. Works on expressions with no side effects, resolves to a literal.
  2. Compile-time functions (#fn) — the more powerful one. Runs arbitrary logic during compilation. See the “Compile-Time Functions” tutorial.

Both produce values that appear as literals in the generated C.

How #if differs from if: if is a runtime check — both branches appear in the generated code, but only one executes. #if is a compile-time check — only the matching branch appears in the generated code. The difference matters for:

  • Platform code: #if (@os == "windows") strips Unix-only code from Windows builds, so you don’t need to stub out unistd.h functions.
  • Binary size: Dead code elimination isn’t perfect, especially in debug builds. #if is guaranteed to remove the unused branch.
  • Type checking: Code inside a non-matching #if branch isn’t even type-checked, so you can reference platform-specific symbols that don’t exist on other platforms.

Compile-time variables are read-only: You can’t write to @os or @debug. They’re set by the compiler based on the build environment. The --target web flag makes @os == "web" evaluate true — handy for conditional graphics shaders.

Constants vs #define: GX constants are typed and scoped. C #define is text substitution with no type info. GX constants show up with their type in error messages, integrate with the type checker, and don’t suffer from the classic #define MAX(a, b) ((a) > (b) ? (a) : (b)) double-evaluation bugs.

Constant folding reaches across functions: If function foo returns a constant expression that depends on other constants, calling foo in another constant expression may also be folded (via #fn). This gives you compile-time computation without the verbosity of C++ constexpr.