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.81and 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:
- Constant folding — the simpler one. Works on expressions with no side effects, resolves to a literal.
- 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 outunistd.hfunctions. - Binary size: Dead code elimination isn’t perfect, especially in debug builds.
#ifis guaranteed to remove the unused branch. - Type checking: Code inside a non-matching
#ifbranch 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.