Examples

Learn GX by example. Each snippet is a complete, runnable program.

Basics

Hello World 01_hello_world.gx
// 01_hello_world.gx
//
// Where everything starts. Build and run:
//   gx 01_hello_world.gx -o hello

fn main() {
    print("Hello, GXers!\n")
}
Variables 02_variables.gx
// 02_variables.gx
//
// var infers the type from the initializer, or you write it out with a colon.
// Either way the type is fixed at compile time. {expr} inside a string prints
// the value, and .str() converts a number when you want the text itself.

fn main() {
    // Type inference with var
    var a = 10;
    var b = 3.14;
    var name = "GX";

    // Explicit types
    var x:i32 = 100;
    var y:f32 = 2.718;
    var big:i64 = 9999999999;
    var precise:f64 = 3.14159265358979;

    // String interpolation
    print("a={a}\n");
    print("b={b}\n");
    print("name={name}\n");
    print("x={x} y={y}\n");
    print("big={big}\n");
    print("precise={precise}\n");

    print("i32 as string: " + x.str() + "\n");
    print("f64 as string: " + precise.str() + "\n");

    // Assignment
    x = x + 1;
    print("x after +1: {x}\n");
}
Print And Strings 03_print_and_strings.gx
// 03_print_and_strings.gx
//
// print, concatenation with +, and {expr} interpolation. Strings are UTF-8.
// A str is a pointer plus a length, so getting the length costs nothing.
// .str() turns any value into text, vectors included.

fn main() {
    // Basic print
    print("Hello GXers!\n");
    var s:str = "Family";
    // String concatenation with +
    var greeting = "Hello" + ", " + "GX {s}!!! \n";
    print(greeting)

    // String interpolation with {expr}
    var x:i32 = 42;


    var f:f32 = 3.14;
    print("int={x} float={f}\n");

    // .str() converts any type to string
    var v = vec3{1.0, 2.0, 3.0};
    print("vec3={v}\n");
    print("vec3 via concat: " + v.str() + "\n");

    // UTF-8 strings
    var utf = "Hello UTF-8: cafe\n";
    print(utf);

    // Escape sequences
    print("Tab:\there\n");
    print("Quote: \"hello\"\n");

    // String + number auto-converts
    print("result=" + (10 + 20).str() + "\n");
}

Data Types

Control Flow 04_control_flow.gx
// 04_control_flow.gx
//
// if, elif, else, while, and the for loop in its three forms: a range,
// a for-each over an array, and a for-each filtered with where.
//
// where and collect are the two things here you will not find in C.
// I took them from MAXScript, where they make everyday array work
// short and readable, and kept that while making them compile to the
// plain loop you would have written by hand. where filters values as
// the loop runs, no allocation, no cost. collect turns a loop into an
// array builder: it keeps every value that passes the filter and hands
// you a new array. That is the one construct in this file that touches
// the heap, and the array is yours, so you free it when you are done.
// There is no garbage collector coming to do it for you.

fn main() {
    // --- If / elif / else ---
    var score:i32 = 75;
    var x:i32 = 10;

    print("Helloo\n");
    if (score >= 90) {
        print("Grade: A\n");
    } elif (score >= 80) {
        print("Grade: B\n");
    } elif (score >= 70) {
        print("Grade: C\n");
    } else {
        print("Grade: F\n");
    }

    // --- While loop ---
    var count:i32 = 0;
    while (count < 5) {
        print("count={count}\n");
        count = count + 1;
    }

    // --- For-range (inclusive) ---
    print("\nFor-range 1 to 5:\n");
    for (i = 1:5) {
        print("  i={i}\n");
    }

    // --- For-each over array ---
    print("\nFor-each:\n");
    var arr:i32[5] = {10, 20, 30, 40, 50};
    for (var x in arr) {
        print("  x={x}\n");
    }

    // --- For-each with where filter ---
    print("\nFiltered (even only):\n");
    var nums:i32[8] = {1, 2, 3, 4, 5, 6, 7, 8};

    for (var n in nums) where (n % 2 == 0) {
        print("  even={n}\n");
    }

    // --- Collect: build an array from a loop ---
    // One line replaces the usual count-allocate-fill dance.
    var evens = for (n in nums) where (n % 2 == 0) collect n;

    print("\nCollected evens ({evens.len()}):");

    for (var e in evens) {
        print(" {e}");
    }

    print("\n");

    // The collected expression can be any transform, not just the value.
    var doubled = for (n in nums) where (n <= 4) collect n * 10;

    print("Doubled small ({doubled.len()}):");

    for (var d in doubled) {
        print(" {d}");
    }
    print("\n");

    // collect allocated these, so they are ours to release.
    evens.free();
    doubled.free();
}
Arrays 05_arrays.gx
// 05_arrays.gx
//
// Fixed-size arrays. The layout is exactly C: five i32s are twenty
// contiguous bytes, no header, no hidden field. The difference is that
// the compiler knows the size. It is part of the type, .len hands it to
// you, and slicing gives a pointer plus length view instead of a bare
// pointer that forgot how long it is.
//
// A word on safety, because I want to be precise about what you get.
// An index the compiler can prove wrong does not compile at all:
// scores[9] on a five element array is an error in every build mode,
// since both the index and the size are sitting right there. Indexes
// only known at runtime are checked on every access, and going out of
// range panics with the index and the length instead of reading
// whatever sits past the array. Those checks stay in at every
// optimization level. If a profiler shows you a hot loop that cannot
// afford them, build with --unchecked and the program will trust your
// indexes the way C does, but that is a flag you type, not something
// -O2 does behind your back. GX also has raw pointers, and nothing
// checks those. Not a memory-safe language, and I will not pretend
// otherwise. But the default is the safe build, and leaving it is
// always your own explicit decision.

fn main() {
    // Fixed-size array with initializer
    var scores:i32[5] = {95, 82, 74, 88, 91};

    // A slice is a view into the array, no copy. Ranges are inclusive,
    // so 0:3 is four elements.
    var win:[]i32 = scores[0:3];

    print("Winners:\n");
    for (var s in win) {
        print(" win {s}\n");
    }

    var colors:f32[3] = {1.0, 0.0, 0.0};
    print("\nRGB: {colors[0]}, {colors[1]}, {colors[2]}\n");

    // Modify by index
    scores[0] = 100;
    print("\nUpdated first score: {scores[0]}\n");

    // scores[9] = 0;   // try it: does not compile, index 9 out of bounds (len 5)

    // .len is known at compile time, no sentinel, no separate size variable
    print("\nAll {scores.len} scores after update:\n");
    for (i = 0:scores.len-1) {
        print("  scores[{i}] = {scores[i]}\n");
    }
}
Dynamic Arrays 06_dynamic_arrays.gx
// 06_dynamic_arrays.gx
//
// array<T> is the growable one. It allocates, so you free it when you are
// done, and defer makes that a one-liner. push, at, sort, remove_swap,
// clear, and both loop styles all work on it. remove_swap gives up element
// order to stay O(1), usually the right trade.

fn main() {
    // --- Static array for-each ---
    print("Static array\n");
    var week_days:str[7] = {"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"};
    for (item:str in week_days) {
        print("Day: {item}\n");
    }

    // --- Dynamic array with initializer syntax ---
    print("\nDynamic array (initializer syntax)\n");
    var darr:array<str> = {"Afan", "Sanela", "Ajdin"};
    defer darr.free();

    // For-each works on dynamic arrays
    for (var item in darr) {
        print("  item: {item}\n");
    }

    // Multiple iterations work fine
    print("\nSecond iteration with index:\n");
    var cnt = 0;
    for (var item in darr) {
        print("  [{cnt}] {item}\n");
        cnt = cnt + 1;
    }

    // Index-based loop also works
    print("\nIndex-based loop:\n");
    for (j = 0:darr.len()-1) {
        print("  {darr.at(j)}\n");
    }

    // --- Dynamic array without initializer (auto-init) ---
    var a:array<i32>;
    defer a.free();

    // Push elements
    a.push(30);
    a.push(10);
    a.push(50);
    a.push(20);
    a.push(40);
    print("\nAfter push: len={a.len()} cap={a.cap()}\n");

    // Access elements
    print("a[0]={a.at(0)} a[1]={a.at(1)} a[2]={a.at(2)}\n");

    // For-each over i32 dynamic array
    print("\nFor-each over i32 array:\n");
    for (var n in a) {
        print("  {n}\n");
    }

    // Sort
    a.sort();
    print("\nAfter sort:\n");
    for (var n in a) {
        print("  {n}\n");
    }

    // Remove (swap with last - O(1))
    a.remove_swap(0);
    print("\nAfter remove_swap(0): len={a.len()}\n");
    for (var n in a) {
        print("  {n}\n");
    }

    // Clear
    a.clear();
    print("\nAfter clear: len={a.len()}\n");

    // --- Collect: create dynamic array from loop ---
    var data:i32[6] = {5, 12, 3, 18, 7, 20};
    var big = for (x in data) where (x >= 10) collect x;
    print("\nCollected (>= 10): ");
    for (var n in big) {
        print("{n} ");
    }
    print("(len={big.len()})\n");
    big.free();

    print("\nDone.\n");
}
Structs 07_structs.gx
// 07_structs.gx
//
// Structs are plain data with C layout, and nothing else lives inside them.
// Methods go in an ex block, short for extension, and that split
// is deliberate: the data stays a dumb C struct you can hand to any C
// library, while the code that works on it lives next door. Inside an
// ex block the fields are in scope directly, so p.greet() reads like
// the object style people enjoy writing, without classes, inheritance,
// or vtables sneaking into the layout. Separate the data from the code,
// keep the joy of using it.

struct Player {
    id:i32
    name:str
    score:f32
}

ex Player {
    fn greet() {
        print("Player {name} (id={id}) score={score}\n");
    }

    fn add_score(points:f32) {
        score = score + points;
    }
}

struct Vec2i {
    x:i32
    y:i32
}

ex Vec2i {
    fn to_str:str() {
        return "({x}, {y})";
    }
}

fn main() {
    // Create with initializer
    var p = Player{1, "Alice", 0.0};
    p.greet();

    // Modify via extension method
    p.add_score(100.5);
    p.add_score(50.0);
    p.greet();

    // Direct field access
    p.name = "Bob";
    print("Renamed to: {p.name}\n");

    // Custom struct with str conversion
    var pos = Vec2i{10, 20};
    print("Position: " + pos.to_str() + "\n");
}
Enums 08_enums.gx
// 08_enums.gx
//
// Enums are named integers, same as in C. Compare them, branch on them,
// and if you print one you get the number back.

enum Direction {
    North
    South
    East
    West
}

enum Color {
    Red = 0
    Green = 1
    Blue = 2
}

fn main() {
    // Declare enum variables
    var dir:Direction = East;

    // Compare enum values
    if (dir == North) {
        print("Heading North\n");
    } elif (dir == East) {
        print("Heading East\n");
    } elif (dir == South) {
        print("Heading South\n");
    } else {
        print("Heading West\n");
    }

    // Enum to string (shows integer value)
    print("East = {dir}\n");

    // Another enum
    var c:Color = Blue;
    print("Blue = {c}\n");
}
Map 39_map.gx
// 39_map.gx
//
// map<K,V> is the built-in hash map. Declare it and it is ready. You get
// set, get, has, remove, len, clear, and free when you are done, because
// nothing frees itself behind your back. Iteration comes in two forms,
// destructured (var k, v in m) or entry-based with item.key and item.value,
// and both take a where filter. Order is hash order, so don't rely on it.

// Test 1: String-keyed map
fn test_str_map() {
    var scores:map<str, i32>

    var names:map<str,str>

    names.set("Afan","OLOVCIC")
    names.set("Sanela","GLUHOVIC")
    names.set("Ajdin","OLOVCIC")

    for (var k, v in names) {
        print("name: {k} {v}\n")
    }
    for (var item in names) {
        print("name: {item.key} {item.value}\n")
    }

    scores.set("alice", 95)
    scores.set("bob", 82)
    scores.set("charlie", 91)

    var a:str = "alice"
    var b:str = "bob"
    var c:str = "charlie"
    var d:str = "dave"

    print("alice: ")
    print("{scores.get(a)}\n")
    print("bob: ")
    print("{scores.get(b)}\n")
    print("has charlie: {scores.has(c)}\n")
    print("has dave: {scores.has(d)}\n")
    print("len: {scores.len()}\n")

    // Update existing key
    scores.set("bob", 88)
    print("bob updated: {scores.get(b)}\n")

    // Remove
    scores.remove(c)
    print("after remove, has charlie: {scores.has(c)}\n")
    print("len after remove: {scores.len()}\n")

    scores.free()
}

// Test 2: Integer-keyed map
fn test_int_map() {
    var names:map<i32, str>
    names.set(1, "one")
    names.set(2, "two")
    names.set(3, "three")

    print("1 -> ")
    print(names.get(1))
    print("\n")
    print("2 -> ")
    print(names.get(2))
    print("\n")
    print("3 -> ")
    print(names.get(3))
    print("\n")
    print("has 4: {names.has(4)}\n")

    names.free()
}

// Test 3: Many entries (tests grow/rehash)
fn test_grow() {
    var m:map<i32, i32>
    for (i = 0:99) {
        m.set(i, i * i)
    }
    print("len after 100 inserts: {m.len()}\n")
    print("m[50] = {m.get(50)}\n")
    print("m[99] = {m.get(99)}\n")

    // Clear and verify
    m.clear()
    print("len after clear: {m.len()}\n")

    m.free()
}

// Test 4: Map iteration - destructured (var k, v)
fn test_iter_kv() {
    var scores:map<str, i32>
    scores.set("alice", 95)
    scores.set("bob", 62)
    scores.set("charlie", 91)
    scores.set("dave", 55)

    print("All scores (k, v):\n")
    for (var name, score in scores) {
        print("  {name}: {score}\n")
    }

    print("High scores (>= 80):\n")
    for (var name, score in scores) where (score >= 80) {
        print("  {name}: {score}\n")
    }

    scores.free()
}

// Test 5: Map iteration - entry-based (var item → item.key, item.value)
fn test_iter_entry() {
    var scores:map<str, i32>
    scores.set("alice", 95)
    scores.set("bob", 62)
    scores.set("charlie", 91)
    scores.set("dave", 55)

    print("All scores (entry):\n")
    for (var entry in scores) {
        print("  {entry.key}: {entry.value}\n")
    }

    print("High scores (>= 80):\n")
    for (var entry in scores) where (entry.value >= 80) {
        print("  {entry.key}: {entry.value}\n")
    }

    // Integer-keyed iteration
    var m:map<i32, i32>
    for (i = 0:9) {
        m.set(i, i * i)
    }
    print("Squares:\n")
    for (var item in m) {
        print("  {item.key}^2 = {item.value}\n")
    }

    scores.free()
    m.free()
}

fn main() {
    print("=== String-keyed map ===\n")
    test_str_map()
    print("\n=== Integer-keyed map ===\n")
    test_int_map()
    print("\n=== Grow/rehash test ===\n")
    test_grow()
    print("\n=== Map iteration (k, v) ===\n")
    test_iter_kv()
    print("\n=== Map iteration (entry) ===\n")
    test_iter_entry()
}

Functions & Control

Vec Mat Math 09_vec_mat_math.gx
// 09_vec_mat_math.gx
//
// vec2, vec3, vec4 and mat4 are built into the language, not a library,
// because graphics code is what GX is for. Arithmetic works componentwise
// and swizzles like a.zyx read the way they do in shaders. A swizzle is
// just a shuffle, it does not allocate anything.

fn main() {
    // --- Vectors ---
    var a = vec3{1.0, 2.0, 3.0};
    var b = vec3{4.0, 5.0, 6.0};

    // Vector addition
    var c = a + b;
    print("a + b = {c}\n");

    // Scalar multiply
    var d = a * 2.0;
    print("a * 2 = {d}\n");

    // Negate
    var e = -a;

    var len = 32;
    for (i=0:len) {
        print("i = {i}\n");
    }

    print("-a = {e}\n");

    // Field access
    print("a.x={a.x} a.y={a.y} a.z={a.z}\n");

    // --- vec2 and vec4 ---
    var v2 = vec2{10.0, 20.0};
    print("vec2: {v2}\n");

    var v4 = vec4{1.0, 0.0, 0.0, 1.0};
    print("vec4: {v4}\n");

    // --- Swizzle ---
    var sw1 = a.xy;
    print("a.xy = {sw1}\n");

    var sw2 = a.zyx;
    print("a.zyx = {sw2}\n");

    var sw3 = v4.wz;
    print("v4.wz = {sw3}\n");

    var sw4 = v2.yx;
    print("v2.yx = {sw4}\n");

    // Repeat components
    var sw5 = a.xx;
    print("a.xx = {sw5}\n");

    // --- Matrix ---
    var M = mat4{
        vec4{1,0,0,0},
        vec4{0,1,0,0},
        vec4{0,0,1,0},
        vec4{0,0,0,1},
    };
    print("Identity: {M}\n");
}
Functions 10_functions.gx
// 10_functions.gx
//
// Functions, recursion, and the pointer rules. A pointer parameter is
// read-only unless you mark it out, so the signature says who can write
// what.
//
// Functions cannot return pointers at all, and I want to explain that
// decision, because it looks strict until you see what it removes. A
// returned pointer always carries the same questions in C: who owns
// this now, who frees it, and how long does it live. Half the time it
// is a dead stack address before you even get to ask. GX cuts the
// whole knot: return by value, or write through an out parameter the
// caller handed you. Either way the memory always belongs to the
// caller, it is visible at the call site, and the ownership argument
// never starts. One small rule, a whole class of bugs and confusion
// gone.

// ============================================================================
// Part 1: Basic functions
// ============================================================================

fn add:i32(a: i32, b: i32) {
    return a + b
}

fn larger:i32(a: i32, b: i32) {
    if (a > b) { return a }
    return b
}

fn factorial:i32(n: i32) {
    if (n <= 1) { return 1 }
    return n * factorial(n - 1)
}

fn greet(name: str) {
    print("Hello, {name}!\n")
}

// ============================================================================
// Part 2: Pointer parameters (read-only by default)
// ============================================================================

struct Vec3 {
    x: f32
    y: f32
    z: f32
}

// Pointer parameter WITHOUT 'out' = read-only.
// Transpiles to 'const Vec3*' in C.
// You can READ fields through the pointer but CANNOT write.
fn vec3_length:f32(v: *Vec3) {
    // OK: reading fields
    var x = v.x
    var y = v.y
    var z = v.z
    // COMPILE ERROR if you tried: v.x = 0.0
    // "Cannot write through read-only pointer"
    return x * x + y * y + z * z  // squared length for simplicity
}

fn vec3_print(v: *Vec3) {
    print("({v.x}, {v.y}, {v.z})\n")
}

// ============================================================================
// Part 3: 'out' parameters - writable pointers
// ============================================================================

// 'out' marks the parameter as mutable. Only inside the function body can
// you write through this pointer. Caller must pass with '&'.
fn vec3_scale(out v: *Vec3, factor: f32) {
    v.x = v.x * factor
    v.y = v.y * factor
    v.z = v.z * factor
}

fn vec3_set(out v: *Vec3, x: f32, y: f32, z: f32) {
    v.x = x
    v.y = y
    v.z = z
}

// 'out' scalar parameter for multi-return values.
// Return the success flag, write the result through the out pointer.
fn safe_divide:bool(a: f32, b: f32, out result: f32) {
    if (b == 0.0) {
        return false
    }
    *result = a / b
    return true
}

// Compute min and max in one pass using two out parameters.
fn min_max(values: *Vec3, out lo: f32, out hi: f32) {
    *lo = values.x;
    *hi = values.x;
    if (values.y < *lo) { *lo = values.y }
    if (values.y > *hi) { *hi = values.y }
    if (values.z < *lo) { *lo = values.z }
    if (values.z > *hi) { *hi = values.z }
}

// ============================================================================
// Part 4: Why GX disallows returning pointers
// ============================================================================
//
// In C you can write this - and it's UNDEFINED BEHAVIOR:
//
//     int* make_value() {
//         int x = 42;
//         return &x;   // returns pointer to a dead stack variable
//     }
//
// GX refuses to compile any function that returns a pointer type.
// The rule is enforced at the type checker level - no exceptions.
// Instead, you return the value by value, or pass an 'out' parameter:
//
//     fn make_value:i32()                   // return by value ✓
//     fn make_value(out x: i32)             // write through out param ✓
//     fn make_value:*i32()                  // ✗ COMPILE ERROR
//
// This eliminates an entire class of dangling-pointer bugs by construction.

// CORRECT: return by value
fn make_origin:Vec3() {
    var v: Vec3
    v.x = 0.0
    v.y = 0.0
    v.z = 0.0
    return v
}

// CORRECT: fill an out parameter provided by the caller
fn make_unit_x(out v: *Vec3) {
    v.x = 1.0
    v.y = 0.0
    v.z = 0.0
}

// ============================================================================
// main
// ============================================================================

fn main() {
    print("=== Part 1: Basic functions ===\n")
    var sum = add(10, 20)
    print("add(10, 20) = {sum}\n")

    var m = larger(42, 17)
    print("larger(42, 17) = {m}\n")

    print("\nFactorials:\n")
    for (i = 1 : 6) {
        print("  {i}! = {factorial(i)}\n")
    }

    greet("GX")

    print("\n=== Part 2: Read-only pointer parameters ===\n")
    var position: Vec3
    position.x = 3.0
    position.y = 4.0
    position.z = 0.0

    print("position = ")
    vec3_print(&position)

    var len_sq = vec3_length(&position)
    print("length_squared = {len_sq}\n")
    // vec3_length can only READ through its pointer - it can't modify 'position'

    print("\n=== Part 3: out parameters (writable) ===\n")
    var scaled = position      // copy
    vec3_scale(&scaled, 2.0)   // 'out' lets vec3_scale write through the pointer
    print("scaled = ")
    vec3_print(&scaled)

    var zeroed: Vec3
    vec3_set(&zeroed, 7.0, 8.0, 9.0)
    print("after set = ")
    vec3_print(&zeroed)

    // Multi-return via out param
    var result: f32 = 0.0
    if (safe_divide(10.0, 3.0, &result)) {
        print("10 / 3 = {result}\n")
    }
    if (!safe_divide(5.0, 0.0, &result)) {
        print("division by zero caught\n")
    }

    // Two out values from one call
    var lo: f32 = 0.0
    var hi: f32 = 0.0
    min_max(&scaled, &lo, &hi)
    print("min = {lo}, max = {hi}\n")

    print("\n=== Part 4: No returning pointers ===\n")
    // Returning by value - always safe, no dangling references
    var origin = make_origin()
    print("origin = ")
    vec3_print(&origin)

    // Or initialize via out param
    var unit_x: Vec3
    make_unit_x(&unit_x)
    print("unit_x = ")
    vec3_print(&unit_x)

    print("\nDone!\n")
}
Bool 11_bool.gx
// 11_bool.gx
//
// bool in GX is what it compiles to: 1 or 0. Nothing hidden behind it.

fn main() {
    var a:bool = true;
    var b:bool = false;

    print("a = {a}\n");
    print("b = {b}\n");

    if (a) {
        print("a is true\n");
    }

    if (!b) {
        print("b is false\n");
    }

    // Boolean logic
    var c = a && b;
    var d = a || b;
    print("true && false = {c}\n");
    print("true || false = {d}\n");

    // Comparison produces bool
    var x = 5;
    var gt = x > 3;
    print("5 > 3 = {gt}\n");

    // Use in conditions directly
    var done = false;
    var count = 0;
    while (!done) {
        count = count + 1;
        if (count == 3) {
            done = true;
        }
    }
    print("counted to {count}\n");
}
Input 12_input.gx
// 12_input.gx
//
// input() reads a line from stdin and hands it back as a str. Pass a
// prompt string or don't.

fn main() {
    var name:str = input("What is your name? ");
    print("Hello, {name}!\n");

    var color:str = input("Favorite color? ");
    print("{name} likes {color}\n");

    // input() with no prompt
    print("Type something: ");
    var line:str = input();
    print("You said: {line}\n");
}
Bitwise 13_bitwise.gx
// 13_bitwise.gx
//
// The bitwise operators, straight from C: & | ^ ~ and both shifts, plus
// hex literals. The second half is the patterns you actually use them for,
// flag sets, byte extraction, popcount, the power-of-two trick.

fn main() {
    // --- Hex literals ---
    var a:i32 = 0xFF;
    var b:i32 = 0x0F;
    var c:i32 = 0x0A;

    print("Hex literals: a=0xFF={a}, b=0x0F={b}\n");

    // --- Bitwise AND ---
    var and_result:i32 = a & b & c;
    print("\nBitwise AND: 0xFF & 0x0F = {and_result}\n");

    // --- Bitwise OR ---
    var or_result:i32 = 0xF0 | 0x0F;
    print("Bitwise OR:  0xF0 | 0x0F = {or_result}\n");

    // --- Bitwise XOR ---
    var xor_result:i32 = 0xFF ^ 0x0F;
    print("Bitwise XOR: 0xFF ^ 0x0F = {xor_result}\n");

    // --- Bitwise NOT ---
    var not_result:i32 = ~0;
    print("Bitwise NOT: ~0 = {not_result}\n");

    // --- Left shift ---
    var lshift:i32 = 1 << 4;
    print("Left shift:  1 << 4 = {lshift}\n");

    // --- Right shift ---
    var rshift:i32 = 256 >> 3;
    print("Right shift: 256 >> 3 = {rshift}\n");

    // --- Flags pattern ---
    print("\n--- Flags pattern ---\n");
    var FLAG_READ:u32  = 1 << 0;
    var FLAG_WRITE:u32 = 1 << 1;
    var FLAG_EXEC:u32  = 1 << 2;

    var perms:u32 = FLAG_READ | FLAG_EXEC;
    print("Permissions: {perms}\n");
    print("Has READ:  {perms & FLAG_READ}\n");
    print("Has WRITE: {perms & FLAG_WRITE}\n");
    print("Has EXEC:  {perms & FLAG_EXEC}\n");

    // Add write permission
    perms = perms | FLAG_WRITE;
    print("\nAfter adding WRITE: {perms}\n");
    print("Has WRITE: {perms & FLAG_WRITE}\n");

    // Remove exec permission (AND with inverted flag)
    perms = perms & ~FLAG_EXEC;
    print("\nAfter removing EXEC: {perms}\n");
    print("Has EXEC: {perms & FLAG_EXEC}\n");

    // Toggle read with XOR
    perms = perms ^ FLAG_READ;
    print("\nAfter toggling READ: {perms}\n");
    print("Has READ: {perms & FLAG_READ}\n");

    // --- Byte extraction ---
    print("\n--- Byte extraction ---\n");
    var val:i32 = 0xABCD;
    var low:i32 = val & 0xFF;
    var high:i32 = (val >> 8) & 0xFF;
    print("0xABCD low byte:  {low}\n");
    print("0xABCD high byte: {high}\n");

    // --- Bit counting (popcount) ---
    print("\n--- Bit counting ---\n");
    var num:i32 = 0xFF;
    var count:i32 = 0;
    var tmp:i32 = num;
    for (i = 0:31) {
        if (tmp & 1) {
            count = count + 1;
        }
        tmp = tmp >> 1;
    }
    print("Bits set in 0xFF: {count}\n");

    // --- Power of two check ---
    print("\n--- Power of two check ---\n");
    var vals:i32[6] = {1, 2, 3, 4, 16, 255};
    for (var v in vals) {
        var is_pow2:i32 = 0;
        if (v & (v - 1)) {
            is_pow2 = 0;
        } else {
            is_pow2 = 1;
        }
        print("{v} is power of 2: {is_pow2}\n");
    }

    print("\nDone.\n");
}

Advanced Features

Break Continue 21_break_continue.gx
// 21_break_continue.gx
//
// break and continue in both for and while loops, exactly like C.
// break leaves the loop, continue jumps to the next iteration.
// Nothing new to learn here, which is the point.

fn main() {
    // Break: exit loop early
    print("Break test: ");
    for (i = 0:20) {
        if (i > 4) {
            break;
        }
        print("{i} ");
    }
    print("\n");

    // Continue: skip even numbers
    print("Continue test: ");
    for (i = 0:10) {
        if (i % 2 == 0) {
            continue;
        }
        print("{i} ");
    }
    print("\n");

    var a:i32 = 10;
    a++;
    a--;
    ++a;
    --a;
    print("a={a}\n");
    // Break in while loop
    print("While break: ");
    var n:i32 = 0;
    while (n < 100) {
        if (n > 5) {
            break;
        }
        print("{n} ");
        n = n + 1;
    }
    print("\n");

    // Continue in while loop
    print("While continue: ");
    var m:i32 = 0;
    while (m < 10) {
        m = m + 1;
        if (m % 3 == 0) {
            continue;
        }
        print("{m} ");
    }
    print("\n");
}
Match 21_match.gx
// 21_match.gx
//
// match is a multi-way branch. Arms take single values, comma lists,
// inclusive ranges like 0..59, or enum members, and default catches the
// rest. An arm body is a statement or a block, and there is no
// fallthrough, so you never write break. It compiles down to the plain
// branching you would have written in C by hand.

// Test 1: Match on integer with single values
fn test_int() {
    var x:i32 = 2;
    match (x) {
        1: print("one\n")
        2: print("two\n")
        3: print("three\n")
        default: print("other\n")
    }
}

// Test 2: Match with comma-separated patterns
fn test_comma() {
    var code:i32 = 5;
    match (code) {
        1, 2, 3: print("low\n")
        4, 5, 6: print("mid\n")
        7, 8, 9: print("high\n")
        default: print("out of range\n")
    }
}

// Test 3: Match with range
fn test_range() {
    var score:i32 = 85;
    match (score) {
        0..59: print("F\n")
        60..69: print("D\n")
        70..79: print("C\n")
        80..89: print("B\n")
        90..100: print("A\n")
        default: print("invalid\n")
    }
}

// Test 4: Match on enum
enum Color { Red Green Blue }

fn test_enum() {
    var c:Color = Green;
    match (c) {
        Red: print("red\n")
        Green: print("green\n")
        Blue: print("blue\n")
    }
}

// Test 5: Match with block body
fn test_block() {
    var n:i32 = 3;
    match (n) {
        1: {
            print("one ");
            print("(single)\n");
        }
        2, 3: {
            print("two or three ");
            print("(group)\n");
        }
        default: print("other\n")
    }
}

fn main() {
    print("--- int match ---\n");
    test_int();
    print("--- comma match ---\n");
    test_comma();
    print("--- range match ---\n");
    test_range();
    print("--- enum match ---\n");
    test_enum();
    print("--- block match ---\n");
    test_block();
}
Func Ptr 22_func_ptr.gx
// 22_func_ptr.gx
//
// Function pointers with a readable type: fn(i32):i32 instead of C's
// inside-out declarator syntax. Assign a function, pass it as an
// argument, call through it. Underneath it is still a plain C function
// pointer, no closures, no captured state.

fn add:i32(a:i32, b:i32) {
    return a + b;
}

fn negate:i32(x:i32) {
    return 0 - x;
}

fn apply:i32(f:fn(i32):i32, val:i32) {
    return f(val);
}

fn main() {
    // Valid: assign function matching signature
    var op:fn(i32, i32):i32 = add;
    print("add(3,4) = {op(3, 4)}\n");

    // Valid: pass function pointer as argument
    var result:i32 = apply(negate, 42);
    print("negate(42) = {result}\n");

    // Call through function pointer
    var f:fn(i32):i32 = negate;
    print("f(10) = {f(10)}\n");
}
Increment 23_increment.gx
// 23_increment.gx
//
// ++ and -- in prefix and postfix form. Postfix hands you the old value,
// prefix the new one, same rules as C.

fn main() {
    // Postfix
    var a:i32 = 5;
    a++;
    print("a++ => {a}\n");

    var b:i32 = 10;
    b--;
    print("b-- => {b}\n");

    var h1:i32 = b<11?10:20;
    print("h={h1}\n");
    // Prefix
    var c:i32 = 0;
    ++c;
    ++c;
    ++c;
    c++;
    --c;
    c--;
    print("++c x3 => {c}\n");

    // Prefix vs postfix in expressions
    var e:i32 = 5;
    var f:i32 = e++;
    print("postfix: e={e}, f={f}\n");

    var g:i32 = 5;
    var h:i32 = ++g;
    print("prefix:  g={g}, h={h}\n");
}
Union 24_union.gx
// 24_union.gx
//
// Unions share memory between fields, with the exact C layout. This file
// uses them three ways: reinterpreting i32 bits as f32, a tagged union
// built from an enum plus a match, and a color written as one u32 and
// stored as four bytes. Only one field is valid at a time, and keeping
// track of which one is your job. That is the deal with unions.

// --- Basic union: type punning ---
union IntFloat {
    i: i32
    f: f32
}

// --- Tagged union pattern: manual tag + union ---
enum ValueKind {
    IntVal
    FloatVal
    StrVal
}

union ValueData {
    i: i32
    f: f32
    s: str
}

union Actor {
    id:i32
    name:str
}

struct Value {
    kind: ValueKind
    data: ValueData
}

fn print_value(v: Value) {
    match (v.kind) {
        IntVal:   print("int: {v.data.i}\n")
        FloatVal: print("float: {v.data.f}\n")
        StrVal:   print("str: {v.data.s}\n")
    }
}

// --- Color as packed bytes ---
union Color {
    rgba: u32
    bytes: u8[4]
}

fn main() {
    // Type punning: reinterpret i32 bits as f32
    var pun: IntFloat;
    pun.i = 1065353216;  // IEEE 754 for 1.0f
    print("int bits: {pun.i}\n");
    print("as float: {pun.f}\n");

    // Tagged union: variant storage
    var v1: Value;
    v1.kind = IntVal;
    v1.data.i = 42;
    print_value(v1);

    var v2: Value;
    v2.kind = FloatVal;
    v2.data.f = 3.14;
    print_value(v2);

    var v3: Value;
    v3.kind = StrVal;
    v3.data.s = "hello";
    print_value(v3);

    // Packed color: write as u32, read bytes
    var c: Color;
    c.rgba = 0xFF8040FF;
    print("color rgba: {c.rgba}\n");
}
Fat Str 25_fat_str.gx
// 25_fat_str.gx
//
// str is a fat pointer, ptr plus len. That makes .len O(1) and lets ==
// compare contents instead of addresses, which is what you meant anyway.
// Concatenation with + and interpolation with {expr} build on the same
// representation.

fn main() {
    // Basic str with .len and .cstr
    var s: str = "hello";
    print("s = {s}\n");
    print("s.len = {s.len}\n");

    // String comparison (uses ec_str_eq, not pointer comparison!)
    var a: str = "hello";
    var b: str = "hello";
    var c: str = "world";

    if (a == b) {
        print("a == b: true (correct!)\n");
    }
    if (a != c) {
        print("a != c: true (correct!)\n");
    }

    // String concatenation
    var greeting: str = "Hello, " + "GX!";
    print("greeting = {greeting}\n");
    print("greeting.len = {greeting.len}\n");

    // String interpolation
    var x: i32 = 42;
    var msg: str = "The answer is {x}";
    print("{msg}\n");

    // Empty string
    var empty: str = "";
    print("empty.len = {empty.len}\n");

    // Escape sequences - sizeof handles them correctly
    var newline: str = "a\nb";
    print("newline.len = {newline.len}\n");

    print("\nAll fat string tests passed!\n");
}
Sort 25_sort.gx
// 25_sort.gx
//
// .sort() works on everything that stores elements in a row: fixed
// arrays, slices, and array<T>. Sorting a slice sorts that window of the
// backing array in place, the elements outside it stay put.

fn print_arr(label: str, s: []i32) {
    print("{label}: ");
    for (var x in s) {
        print("{x} ");
    }
    print("\n");
}

fn main() {
    // === Fixed array sort ===
    var fixed:i32[6] = {42, 7, 99, 3, 55, 1};
    print_arr("before fixed", fixed);
    fixed.sort();
    print_arr("after  fixed", fixed);

    // === Slice sort ===
    var data:i32[8] = {80, 20, 60, 10, 90, 40, 70, 30};
    var sl:[]i32 = data[2:6];

    print_arr("before slice", sl);
    sl.sort();

    print_arr("after  slice", sl);
    print_arr("full   data ", data);

    // === Dynamic array sort ===
    var dyn:array<i32>;
    dyn.init();
    dyn.push(33);
    dyn.push(11);
    dyn.push(77);
    dyn.push(22);
    dyn.push(55);

    // print via slice
    var ds:[]i32 = dyn;
    print_arr("before dyn  ", ds);
    dyn.sort();

    var ds2:[]i32 = dyn;
    print_arr("after  dyn  ", ds2);
    dyn.free();
}
Sizeof 26_sizeof.gx
// 26_sizeof.gx
//
// sizeof works on types and on expressions, and it folds to a constant.
// Structs keep C-compatible layout, so the number you get here is the
// number any C code linked against you would see.

struct Point {
    x: f32
    y: f32
}

struct Color {
    r: u8
    g: u8
    b: u8
    a: u8
}

fn main() {
    // sizeof with built-in types
    print("sizeof(i8)    = {sizeof(i8)}\n");
    print("sizeof(i16)   = {sizeof(i16)}\n");
    print("sizeof(i32)   = {sizeof(i32)}\n");
    print("sizeof(i64)   = {sizeof(i64)}\n");
    print("sizeof(f32)   = {sizeof(f32)}\n");
    print("sizeof(f64)   = {sizeof(f64)}\n");
    print("sizeof(bool)  = {sizeof(bool)}\n");

    // sizeof with user-defined structs (via expression)
    var p:Point = Point{1.0, 2.0};
    print("sizeof(Point) = {sizeof(p)}\n");

    var c:Color = Color{255, 128, 0, 255};
    print("sizeof(Color) = {sizeof(c)}\n");

    // sizeof with vec types
    print("sizeof(vec2)  = {sizeof(vec2)}\n");
    print("sizeof(vec3)  = {sizeof(vec3)}\n");
    print("sizeof(vec4)  = {sizeof(vec4)}\n");

    // sizeof in expressions
    var arr_size:i64 = 10 * sizeof(i32);
    print("10 * sizeof(i32) = {arr_size}\n");
}
Slice 26_slice.gx
// 26_slice.gx
//
// A slice is a view into an array, pointer plus length, nothing copied.
// Arrays carry .len and convert to slices at function boundaries on their
// own. A slice is also what to reach for where C would use a pointer and
// a count: same job, but the length travels with the data and indexing
// is checked. One thing to get right: slice ranges are inclusive on both
// ends, matching the loop convention for (i = a:b), so arr[1:4] is
// indices 1 through 4, four elements. And remember a slice is a view, it
// owns nothing, so it must not outlive the memory it looks into.

fn sum:i32(data: []i32) {
    var total: i32 = 0;
    for (var x in data) {
        total += x;
    }
    return total;
}

fn print_slice(data: []i32) {
    print("[");
    for (var x in data) {
        print("{x} ");
    }
    print("]\n");
}

fn main() {
    var arr: i32[5] = {10, 20, 30, 40, 50};

    // Arrays have .len - compile-time constant, no struct needed
    print("arr.len = {arr.len}\n");

    // Arrays implicitly convert to slices at function boundaries
    print("sum(arr) = {sum(arr)}\n");
    print("arr = ");
    print_slice(arr);

    // Subslicing works directly on arrays. Inclusive ranges:
    // arr[1:4] takes indices 1, 2, 3, 4 → 4 elements.
    var mid: []i32 = arr[1:4];
    print("arr[1:4] = ");
    print_slice(mid);
    print("mid.len = {mid.len}\n");

    // Slices work the same way
    var s: []i32 = arr;
    print("s[0] = {s[0]}\n");
    print("s.len = {s.len}\n");

    // Subslice of subslice - mid[0:2] takes indices 0, 1, 2 → 3 elements
    var inner: []i32 = mid[0:2];
    print("inner = ");
    print_slice(inner);

    print("\nAll tests passed!\n");
}
Bounds Check 27_bounds_check.gx
// 27_bounds_check.gx
//
// Bounds checking happens as early as the compiler can manage. A
// constant index into a fixed-size array is rejected at compile time,
// arr[10] on a three element array never builds. A slice is different,
// its length is runtime information, so slice indexing is checked as
// the program runs. That is what this file shows: it goes out of range
// through a slice on purpose, so expect it to die at s[10]. The
// runtime checks stay in at every -O level. The --unchecked flag
// strips them, for the rare program that has measured them and cannot
// afford them.

fn main() {
    // Fixed array bounds check
    var arr: i32[3] = {10, 20, 30};
    print("arr[0] = {arr[0]}\n");
    print("arr[2] = {arr[2]}\n");
    print("arr.len = {arr.len}\n");

    // Slice bounds check
    var s: []i32 = arr;
    print("s[0] = {s[0]}\n");
    print("s[2] = {s[2]}\n");

    // This will PANIC:
    print("About to access s[10]...\n");
    print("s[10] = {s[10]}\n");
    print("This should never print!\n");
}
Const 28_const.gx
// 28_const.gx
//
// const declarations are folded at compile time. Arithmetic, bitwise
// ops, comparisons, references to other consts, all of it collapses to a
// literal in the output, so PI * 2.0 costs nothing at runtime. Const
// arrays work too, sized explicitly or inferred from the initializer.

// Global constants (no type annotation - inferred)
const PI = 3.14159265358979;
const TAU = PI * 2.0;
const SCREEN_WIDTH = 1920;
const SCREEN_HEIGHT = 1080;
const ASPECT_RATIO = 16.0 / 9.0;

// Global constants with type annotation
const MAX_ENTITIES:i32 = 1024;
const BUFFER_SIZE:i64 = MAX_ENTITIES * 64;

// Const expressions with bitwise ops
const FLAG_READ = 1;
const FLAG_WRITE = 2;
const FLAG_EXEC = 4;
const FLAG_ALL = FLAG_READ | FLAG_WRITE | FLAG_EXEC;

// Boolean consts
const DEBUG_MODE = false;
const IS_WIDE = SCREEN_WIDTH > 1000;

// Const arrays - explicit size or inferred with []
const PRIMES:i32[5] = { 2, 3, 5, 7, 11 };
const COLORS:str[]  = { "red", "green", "blue" };  // size inferred to 3
const FIB:i64[]     = { 1, 1, 2, 3, 5, 8, 13, 21 };


fn main() {
    print("=== Const Declarations ===");
    // Using global consts
    print("PI = {PI}");
    print("TAU = {TAU}");
    print("Screen: {SCREEN_WIDTH}x{SCREEN_HEIGHT}");
    print("Aspect ratio: {ASPECT_RATIO}");
    print("Max entities: {MAX_ENTITIES}");
    print("Buffer size: {BUFFER_SIZE}");

    print("");
    print("=== Const Folding ===");

    // Const folding: these should be computed at compile time
    const AREA = SCREEN_WIDTH * SCREEN_HEIGHT;
    print("Screen area: {AREA}");

    const HALF_PI = PI / 2.0;
    print("PI/2 = {HALF_PI}");

    // Bitwise const
    print("FLAG_ALL = {FLAG_ALL}");

    // Boolean const
    print("Debug mode: {DEBUG_MODE}");
    print("Is wide: {IS_WIDE}");

    print("");
    print("=== Const in Expressions ===");

    // Const used in regular expressions
    var x = SCREEN_WIDTH / 2;
    var y = SCREEN_HEIGHT / 2;
    print("Center: ({x}, {y})");

    // Const in loop bounds
    const LOOP_COUNT = 5;
    for (i=0:LOOP_COUNT) {
        print("  i = {i}");
    }

    print("");
    print("=== Negative and Unary ===");
    const NEG_ONE = -1;
    const NOT_FLAG = ~FLAG_READ;
    print("NEG_ONE = {NEG_ONE}");
    print("~FLAG_READ = {NOT_FLAG}");

    print("");
    print("=== Const Arrays ===");
    print("First prime: {PRIMES[0]}");
    print("Last color:  {COLORS[2]}");
    print("FIB[7] = {FIB[7]}");

    print("Done!");
}

Compile-Time

Comptime If 29_comptime_if.gx
// 29_comptime_if.gx
//
// #if is decided while compiling, and the losing branch never reaches
// the output. This is GX's answer to the C preprocessor, and the
// decision behind it matters: the preprocessor is a second language
// living on top of the first, one that cannot see types, consts, or
// scopes, which is exactly how #ifdef soup happens. In GX the condition
// is a normal expression over your own consts and build facts like @os,
// @debug, and @opt. One language, deciding earlier.

// Platform detection with @os
#if (@os == "windows") {
    const PLATFORM = "Windows";
}
#else {
    const PLATFORM = "Other";
}

fn main() {
    print("Platform: {PLATFORM}");

    // Statement-level #if with @os
    #if (@os == "windows") {
        print("Running on Windows");
    }
    #else {
        print("Running on non-Windows");
    }

    // #if with user const
    const ENABLE_FEATURE = true;
    #if (ENABLE_FEATURE) {
        print("Feature enabled");
    }

    // @debug check (false unless --debug flag)
    #if (@debug) {
        print("Debug mode ON");
    }
    #else {
        print("Debug mode OFF");
    }

    // @opt level check
    #if (@opt > 0) {
        print("Optimized build");
    }
    #else {
        print("Unoptimized build");
    }

    print("Done!");
}
Comptime Fn 30_comptime_fn.gx
// 30_comptime_fn.gx
//
// #fn marks a function the compiler can run while compiling. Call one
// in a const initializer and the binary holds the result as a literal,
// recursion and loops included. The decision behind it: magic numbers
// rot. Without this you compute a mask or an aligned size by hand once,
// paste the number, and the formula is gone. Here the formula stays in
// the code where it can be read and changed, the computation costs
// nothing at runtime, and it is the same language running earlier, not
// a macro dialect bolted on top.

// Align a size up to the given alignment
#fn align_up:i32(size:i32, align:i32) {
    return (size + align - 1) & ~(align - 1);
}

// Create a bitmask with N bits set
#fn make_bitmask:i32(bits:i32) {
    if (bits <= 0) { return 0; }
    if (bits >= 32) { return -1; }
    return (1 << bits) - 1;
}

// Compile-time fibonacci
#fn fib:i32(n:i32) {
    if (n <= 1) { return n; }
    return fib(n - 1) + fib(n - 2);
}

// Compile-time max
#fn max:i32(a:i32, b:i32) {
    if (a > b) { return a; }
    return b;
}

// Compile-time power
#fn pow:i32(base:i32, exp:i32) {
    var result:i32 = 1;
    var i:i32 = 0;
    while (i < exp) {
        result = result * base;
        i = i + 1;
    }
    return result;
}

// All evaluated at compile time - no runtime computation!
const BUF_SIZE = align_up(100, 64);
const CHANNEL_MASK = make_bitmask(8);
const PIXEL_MASK = make_bitmask(24);
const FIB_10 = fib(10);
const MAX_DIM = max(1920, 1080);
const TWO_TO_10 = pow(2, 10);
const COMBINED = align_up(FIB_10, 16);

fn main() {
    print("=== Compile-Time Functions (#fn) ===");
    print("align_up(100, 64) = {BUF_SIZE}");
    print("make_bitmask(8)   = {CHANNEL_MASK}");
    print("make_bitmask(24)  = {PIXEL_MASK}");
    print("fib(10)           = {FIB_10}");
    print("max(1920, 1080)   = {MAX_DIM}");
    print("pow(2, 10)        = {TWO_TO_10}");
    print("align_up(fib(10), 16) = {COMBINED}");
    print("Done!");
}
Comptime For 31_comptime_for.gx
// 31_comptime_for.gx
//
// #for runs the loop while compiling and stamps the body out once per
// iteration, with the index as a literal. No counter, no branch in the
// output; the nested version below lands as nine plain statements
// before the backend ever sees them. In C this job goes to X-macros or
// an external codegen script, both of which hide what the output
// actually is. GX's position is that generating repetitive code is a
// job for the language itself, written in the language itself.

fn main() {
    // Basic #for - unrolls to 5 print calls
    print("=== Compile-Time Loop Unrolling (#for) ===");

    #for (i=0:5) {
        print("iteration {i}");
    }

    // #for with expressions using the loop variable
    var sum:i32 = 0;
    #for (i=1:6) {
        sum += i * i;
    }
    print("sum of squares 1..5 = {sum}");

    // Nested #for - generates 3x3 = 9 iterations
    #for (row=0:3) {
        #for (col=0:3) {
            var val:i32 = row * 3 + col;
            print("  ({row},{col}) = {val}");
        }
    }

    // #for with const bounds
    const N = 4;
    #for (i=0:N) {
        print("channel {i}");
    }

    print("Done!");
}
Struct Comptime 32_struct_comptime.gx
// 32_struct_comptime.gx
//
// #if and #for work inside struct and enum bodies too. PlatformInfo
// gets different fields per OS, Pixel gets four generated channels,
// LogLevel carries Debug and Trace only in debug builds. The reasoning:
// data has a platform shape as often as code does, and in C that means
// preprocessor blocks in the middle of declarations, which every tool
// that reads your code then has to understand. Here it is the same #if
// you use everywhere, aimed at the data. The expansion runs before the
// resolver, so the rest of the compiler sees an ordinary struct and
// the binary gets an ordinary C layout.

// Struct with conditional fields based on platform.
//
// PlatformInfo has different fields depending on @os: on Windows it
// gets (handle, registry_key); elsewhere it gets (fd, path). The
// expansion happens at compile time during the comptime pre-pass,
// before the resolver runs.
//
// Note: comptime statement-level #if inside fn bodies runs in the
// post-pass (after type checking), so main() below only touches the
// unconditional fields (name, version). Reading or writing the
// platform-specific fields directly would force the type checker to
// see both branches of an #if and reject the inactive one. The struct
// expansion itself works fine - it's just the access pattern in
// fn-body code that has to stay platform-agnostic.
struct PlatformInfo {
    name: str
    #if (@os == "windows") {
        handle: i64
        registry_key: str
    } #else {
        fd: i32
        path: str
    }
    version: i32
}

// Struct with generated fields via #for
struct Pixel {
    #for (i=0:4) {
        channel_i: f32
    }
}

// Enum with conditional members
enum LogLevel {
    Error
    Warning
    Info
    #if (@debug) {
        Debug
        Trace
    }
}

fn main() {
    print("=== Struct/Enum Compile-Time Expansion ===");

    // PlatformInfo: only access fields that are unconditional.
    var info: PlatformInfo;
    info.name = "test";
    info.version = 1;
    print("name = {info.name}");
    print("version = {info.version.str()}");

    // Pixel has channel_0 through channel_3 (generated by #for)
    var px: Pixel;
    px.channel_0 = 1.0;
    px.channel_1 = 0.5;
    px.channel_2 = 0.25;
    px.channel_3 = 1.0;
    print("pixel = ({px.channel_0.str()}, {px.channel_1.str()}, {px.channel_2.str()}, {px.channel_3.str()})");

    // LogLevel enum - Debug/Trace only exist in debug builds
    var level: LogLevel = Error;
    print("log level = {level.str()}");

    print("Done!");
}
Reflection 33_reflection.gx
// 33_reflection.gx
//
// Compile-time reflection. @fields, @field, @field_type and friends
// answer questions about your types, and #for turns the answers into
// code. The decision here: the compiler already knows every field and
// every member, so the language should let you ask while it still
// knows. Runtime reflection would mean type tables carried in the
// binary and lookups paid on every use, which is how languages grow
// invisible weight. This way the walk happens at build time and ships
// as zero bytes of type information.
//
// The payoff is code that cannot drift. color_name below is the
// enum-to-string function every C codebase maintains by hand in a
// switch, except here the compiler writes it from the enum itself.
// Add a member and the function already knows it.

struct Point {
    x: f32
    y: f32
    z: f32
}

struct Node {
    value: i32
    next: *Node
}

enum Color { Red  Green  Blue }

// Generated enum-to-name: #for unrolls one comparison per member, and
// @member drops the name in as a string literal. Plain branches in the
// output, no table, no maintenance.
fn color_name:str(c: Color) {
    #for (i=0:@members(Color)) {
        if ((i32)c == i) { return "{@member(Color, i)}"; }
    }
    return "?";
}

fn main() {
    print("=== Compile-Time Reflection ===\n");

    // --- Struct field reflection ---
    print("Point has {@fields(Point)} fields:\n");
    #for (i=0:@fields(Point)) {
        print("  {@field(Point, i)}: {@field_type(Point, i)}\n");
    }

    // --- Field detail intrinsics ---
    print("Node has {@fields(Node)} fields:\n");
    #for (i=0:@fields(Node)) {
        print("  {@field(Node, i)}: ptr={@field_is_ptr(Node, i)}\n");
    }

    // --- Enum member reflection ---
    print("Color has {@members(Color)} members:\n");
    #for (i=0:@members(Color)) {
        print("  {@member(Color, i)}\n");
    }

    // --- The generated function in use ---
    var picked: Color = Green;
    print("picked = {color_name(picked)}\n");
    print("by value = {color_name(Blue)}\n");

    // --- Type meta ---
    print("@type_kind(Point) = {@type_kind(Point)}\n");
    print("@type_kind(Color) = {@type_kind(Color)}\n");
    print("@type_name(Point) = {@type_name(Point)}\n");

    // Note: Function reflection (@params, @param, @return_type) works
    // but requires multi-file module setup (single-file function
    // declarations have a pre-existing resolver limitation)

    print("Done!");
}
Build Directives 34_build_directives.gx
// 34_build_directives.gx
//
// @link puts the link line in the source file, where it belongs. Wrap it
// in #if (@os == ...) for libraries that only exist on one platform. The
// file carries everything its build needs, no extra CLI flags.

// Platform-conditional linking via #if - required for libs that only
// exist on one OS. ucrtbase is Microsoft's Universal C Runtime: it lives
// in Windows but doesn't exist on Linux/macOS, so an unconditional
// @link("ucrtbase") would fail to link on POSIX.
#if (@os == "windows") {
    @link("ucrtbase")
    @link("kernel32")
}

fn main() {
    print("=== Build Directives (Phase 7) ===");
    print("@link and @cflags are collected from source");
    print("No CLI flags needed for linking!");
    print("Done!");


}
Templates 35_templates.gx
// 35_templates.gx
//
// #type T declares a type parameter. Every concrete use like Pair<i32>
// stamps out a real struct at compile time, so the C output holds
// struct Pair_i32 with two int32_t fields, exactly what you would have
// written by hand. That mechanism, substitution and nothing else, is
// the whole feature, and the restraint is deliberate.
//
// C++ went the other way. Its templates grew into a second language,
// with specialization, overload resolution games, and error messages
// measured in pages, and most of that machinery exists to smuggle
// computation into the type system. GX already has a compile-time
// language, the # system, so templates do not need to be one. When you
// want logic at compile time you write #if, #fn, or #for. #type only
// fills in types.
//
// What that buys you: generics with zero runtime cost, no boxing and
// no vtables. Every instance is an ordinary named C struct, so it can
// cross the C boundary like anything else, and you can open the
// generated C and read what you got. T is unconstrained, so code that
// does not fit the type fails at the use that stamped it, the same
// trade C++ lived with before concepts. I keep it because it keeps
// the feature small.
//
// The same #type works on a function, and there is no angle-bracket
// call syntax, also on purpose: larger<i32>(a, b) is the classic C++
// ambiguity with comparison chains, and the parser stays out of that
// game. You call larger(3, 5), the compiler reads the argument types,
// stamps larger_i32, and checks it like a function you wrote by hand.
// Arguments that disagree are told exactly where the inference broke.

// The smallest possible template: one type parameter, one field. Box<i32>
// becomes struct Box_i32 { int32_t value; }, nothing more.
#type T
struct Box {
    value: T
}

#type T
struct Pair {
    first: T
    second: T
}

#type T
fn larger:T(a: T, b: T) {
    if (a > b) { return a }
    return b
}

fn main() {
    print("=== Compile-Time Templates ===\n");

    // Box in its simplest form: same code, three concrete structs.
    var bi: Box<i32>;
    bi.value = 7;
    var bf: Box<f32>;
    bf.value = 3.14;
    var bs: Box<str>;
    bs.value = "boxed";
    print("Box<i32>: {bi.value.str()}\n");
    print("Box<f32>: {bf.value.str()}\n");
    print("Box<str>: {bs.value.str()}\n");

    // Pair<i32> - generates struct Pair_i32 { int32_t first; int32_t second; }
    var pi: Pair<i32>;
    pi.first = 10;
    pi.second = 20;
    print("Pair<i32>: ({pi.first.str()}, {pi.second.str()})\n");

    // Pair<i64> - generates struct Pair_i64 { int64_t first; int64_t second; }
    var pl: Pair<i64>;
    pl.first = 100;
    pl.second = 200;
    print("Pair<i64>: ({pl.first.str()}, {pl.second.str()})\n");

    var pf: Pair<f32>;
    pf.first = 100.32;
    pf.second = 200.32;
    print("Pair<f32>: ({pf.first.str()}, {pf.second.str()})\n");

    // Non-scalar type arguments work the same way.
    var ps: Pair<str>;
    ps.first = "Afan";
    ps.second = "Olovcic";
    print("Pair<str>: ({ps.first.str()}, {ps.second.str()})\n");
    {
        var ps:Pair< str >;
        ps.first = "Sanela";
        ps.second = "Gluhovic";
        print("Pair<str>: ({ps.first.str()}, {ps.second.str()})\n");
    }
    // Function templates: T comes from the arguments, nothing to spell out.
    var big:i32 = larger(10, 20);
    var bigf:f32 = larger(2.5, 1.5);
    print("larger(10, 20) = {big}\n");
    print("larger(2.5, 1.5) = {bigf}\n");
    // larger(3, 2.5)   // does not compile: T is i32 from argument 1, but argument 2 is f32

    print("Done!\n");
}

Standard Library

Pointers 36_pointers.gx
// 36_pointers.gx
//
// Pointers here are C pointers, and they behave like it. Take an address,
// dereference, add an offset, subtract two of them, index. None of it is
// checked. The arithmetic in this file stays inside a real array, because
// stepping past one object's storage is undefined behavior in GX exactly
// as it is in C.
//
// Honest advice: this is the least safe corner of the language, and you
// need it less often than you think. Pointers are in GX because GX
// speaks the C ABI and C libraries hand you pointers all day. Use them
// at that boundary, and for handing out a single value to be written
// through. The moment you are walking a run of elements, take a slice
// instead. It knows its length, indexing it is checked, and it costs
// the same. Odin went as far as dropping pointer arithmetic from the
// language and leaning on slices. GX keeps it for the C boundary, but
// pointer arithmetic in everyday GX code usually means a slice was the
// right tool.

fn main() {
    print("=== Pointer Arithmetic ===");

    // Basic dereference
    var x:i32 = 42;
    var p:*i32 = &x;
    print("*p = {(*p).str()}");
    *p = 99;
    print("after *p=99: x = {x.str()}#\n");

    // Use an actual array so pointer arithmetic has defined semantics -
    // reading *(pa + N) for individually-declared `var a/b/c` would be
    // UB past the end of `a`'s storage.
    var arr:i32[3]
    arr[0] = 311
    arr[1] = 20
    arr[2] = 30

    var pa:*i32 = &arr[0];
    // Test pointer + int type
    var pb:*i32 = pa + 1;
    print("pa+1 deref = {(*pb).str()}");

    // Pointer difference
    var diff:i64 = pb - pa;
    print("pb - pa = {diff.str()}");

    // Pointer indexing
    print("pa[0] = {pa[0].str()}");
    print("pa[2] = {pa[2].str()}");

    print("Done!");
}
Main Args 37_main_args.gx
// 37_main_args.gx
//
// fn main(args: str[]) gets you the command line. args[0] is the program's
// own path, same convention as C argv, so real input starts at args[1].
// Build it, then run the exe with a few words after it.

fn main(args: str[]) {
    // Skip args[0] - it's the program's own path, which differs between
    // binaries and isn't meaningful user input. Use slice syntax to show
    // only the user-supplied arguments.
    var user_arg_count = args.len - 1
    print("Got {user_arg_count} arguments:\n")
    var i: i64 = 1
    while (i < args.len) {
        print("  - {args[i]}\n")
        i = i + 1
    }
}
Par For 37_par_for.gx
// 37_par_for.gx
//
// par for splits the iterations across threads, using OpenMP underneath.
// It is only correct when the iterations are independent, like doubling
// each slot here. With the default TCC backend it just runs sequentially.
// Compile with -O1 or higher (clang/gcc) to get real parallelism.

fn main() {
    var data:i32[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // Parallel for-range: each iteration is independent
    par for (i = 0:9) {
        data[i] = data[i] * 2;
        print("{data[i]}\n");
    }

    // Print results
    /*for (i = 0:9) {
        print("{data[i]}\n");
    }*/
}
Strings 38_strings.gx
// 38_strings.gx
//
// str is a fat pointer, a char pointer plus a length. Length is O(1), and
// slicing methods like sub, trim, find and contains just hand back a view
// of the same bytes, no allocation. Methods that produce new text, like
// replace, upper and split, write into a scratch buffer. Chaining works
// because every method takes and returns plain str values.

fn main() {
    var s = "  Hello, World!  "

    // ── trim ─────────────────────────────────
    var trimmed = s.trim()
    var trim_l = s.trim_left()
    var trim_r = s.trim_right()
    print("trim:       '" + trimmed + "'\n")
    print("trim_left:  '" + trim_l + "'\n")
    print("trim_right: '" + trim_r + "'\n")

    var hw = s.trim()

    // ── find / contains ──────────────────────
    var needle1 = "World"
    var needle2 = "xyz"
    var needle3 = "l"
    print("find World:     {hw.find(needle1)}\n")
    print("find xyz:       {hw.find(needle2)}\n")
    print("find_last l:    {hw.find_last(needle3)}\n")
    print("contains Hello: {hw.contains(needle1)}\n")
    print("contains xyz:   {hw.contains(needle2)}\n")

    // ── starts_with / ends_with ──────────────
    var prefix = "Hello"
    var suffix = "!"
    print("starts_with Hello: {hw.starts_with(prefix)}\n")
    print("ends_with !:       {hw.ends_with(suffix)}\n")

    // ── sub ──────────────────────────────────
    var sub1 = hw.sub(0, 5)
    var sub2 = hw.sub(7)
    print("sub(0,5): '" + sub1 + "'\n")
    print("sub(7):   '" + sub2 + "'\n")

    // ── upper / lower ────────────────────────
    var up = hw.upper()
    var lo = hw.lower()
    print("upper: '" + up + "'\n")
    print("lower: '" + lo + "'\n")

    // ── replace ──────────────────────────────
    var old_s = "World"
    var new_s = "GX"
    var rep = hw.replace(old_s, new_s)
    print("replace: '" + rep + "'\n")

    // ── count ────────────────────────────────
    var cnt = hw.count(needle3)
    print("count l: {cnt}\n")

    // ── repeat ───────────────────────────────
    var dash_str = "-"
    var dash = dash_str.repeat(20)
    print(dash + "\n")

    // ── split ────────────────────────────────
    var csv = "one,two,three,four"
    var delim = ","
    var parts = csv.split(delim)
    print("split count: {parts.len}\n")
    for (var p in parts) {
        print("  part: '" + p + "'\n")
    }

    // ── chaining ─────────────────────────────
    var input_str = "  HELLO world  "
    var hello = "hello"
    var hi = "hi"
    var result = input_str.trim().lower().replace(hello, hi)
    print("chained: '" + result + "'\n")

    // ── string interpolation to var ──────────
    var name = "GX"
    var greeting:str = "Hello {name}!"
    print(greeting + "\n")
}
Io 39_io.gx
// 39_io.gx
//
// The io module: console printing, path helpers, filesystem checks, and
// whole-file read, write and append. The path functions are pure string
// work, they never touch the disk. The low-level handle API goes through
// the extern gx_fopen, since GX functions themselves cannot return pointers.

import io.io
import io.file
import io.fs
import io.path

fn main() {
    // --- Console I/O ---
    println("=== Console I/O ===")
    println("Hello from println!")
    eprintln("This goes to stderr")

    // --- Path utilities ---
    println("\n=== Path Utilities ===")

    var p = "src/frontend/parser/parser.cpp"
    print("  path:      ") println(p)
    print("  dirname:   ") println(dirname(p))
    print("  basename:  ") println(basename(p))
    print("  extension: ") println(extension(p))
    print("  stem:      ") println(stem(p))

    var win = "C:\\Users\\test\\file.txt"
    print("  win path:  ") println(win)
    print("  dirname:   ") println(dirname(win))
    print("  basename:  ") println(basename(win))

    print("  is_absolute('/usr/bin'):  ")
    if (is_absolute("/usr/bin")) { println("true") } else { println("false") }

    print("  is_absolute('C:\\foo'):    ")
    if (is_absolute("C:\\foo")) { println("true") } else { println("false") }

    print("  is_absolute('relative'):  ")
    if (is_absolute("relative")) { println("true") } else { println("false") }

    print("  join('foo','bar'):        ") println(join("foo", "bar"))
    print("  join('foo/','bar'):       ") println(join("foo/", "bar"))

    // --- Filesystem ---
    println("\n=== Filesystem ===")

    print("  exists('examples/39_io.gx'):  ")
    if (exists("examples/39_io.gx")) { println("true") } else { println("false") }

    print("  is_file('examples/39_io.gx'): ")
    if (is_file("examples/39_io.gx")) { println("true") } else { println("false") }

    print("  is_dir('examples'):           ")
    if (is_dir("examples")) { println("true") } else { println("false") }

    print("  is_file('nonexistent'):       ")
    if (is_file("nonexistent")) { println("true") } else { println("false") }

    var sz = file_size("examples/39_io.gx")
    print("  file_size: ") print("{sz}") println(" bytes")

    // --- File I/O ---
    println("\n=== File I/O ===")

    // Write a test file
    var ok = write_all("_test_io.txt", "Hello from GX io module!\nLine 2\n")
    print("  write_all: ") if (ok) { println("ok") } else { println("failed") }

    // Read it back
    var content = read_all("_test_io.txt")
    print("  read_all:  ") print(content)

    // Append to it
    ok = append_all("_test_io.txt", "Line 3 appended\n")
    print("  append:    ") if (ok) { println("ok") } else { println("failed") }

    // Read again
    content = read_all("_test_io.txt")
    print("  after append:\n") print(content)

    // Low-level file API - call the extern gx_fopen directly because
    // GX forbids non-extern fns from returning pointer types.
    var f = gx_fopen("_test_io.txt".cstr, "r".cstr)
    if (f != 0) {
        var pos = file_tell(f)
        print("  tell at start: ") println("{pos}")
        file_close(f)
    }

    // Clean up test file
    ok = remove_file("_test_io.txt")
    print("  remove:    ") if (ok) { println("ok") } else { println("failed") }

    print("  exists after remove: ")
    if (exists("_test_io.txt")) { println("true") } else { println("false") }

    // make_dir / remove_dir
    ok = make_dir("_test_dir")
    print("\n  make_dir:  ") if (ok) { println("ok") } else { println("failed") }
    print("  is_dir:    ") if (is_dir("_test_dir")) { println("true") } else { println("false") }
    ok = remove_dir("_test_dir")
    print("  remove_dir:") if (ok) { println("ok") } else { println("failed") }

    println("\n=== Done ===")
}
Random 40_random.gx
// 40_random.gx
//
// The random module: integers, floats, bools, and a seed you control. Same
// seed, same sequence, which is what you want for anything reproducible.
// The noise module adds simplex noise in one to four dimensions plus fBm,
// enough to print a small terrain map straight to the terminal.

import random.random
import random.noise

fn main() {
    // --- Random numbers ---
    print("=== Random (default seed) ===\n")
    print("  rand_int(100):     ") print("{rand_int(100)}") print("\n")
    print("  rand_int(100):     ") print("{rand_int(100)}") print("\n")
    print("  rand_range(10,20): ") print("{rand_range(10, 20)}") print("\n")
    print("  rand_f32():        ") print("{rand_f32()}") print("\n")
    print("  rand_bool():       ")
    if (rand_bool()) { print("true\n") } else { print("false\n") }

    // --- Seeded (deterministic) ---
    print("\n=== Seeded (42) ===\n")
    seed(42)
    print("  rand_int(100):     ") print("{rand_int(100)}") print("\n")
    print("  rand_int(100):     ") print("{rand_int(100)}") print("\n")

    // Same seed = same sequence
    print("\n=== Seeded (42) again ===\n")
    seed(42)
    print("  rand_int(100):     ") print("{rand_int(100)}") print("\n")
    print("  rand_int(100):     ") print("{rand_int(100)}") print("\n")

    // --- Float range ---
    print("\n=== Float range ===\n")
    seed(123)
    var i:i32 = 0
    while (i < 5) {
        var v:f32 = rand_f32_range(-1.0, 1.0)
        print("  rand_f32_range(-1,1): ") print("{v}") print("\n")
        i = i + 1
    }

    // --- Simplex noise ---
    print("\n=== Simplex Noise ===\n")
    print("  simplex1(0.5):           ") print("{simplex1(0.5)}") print("\n")
    print("  simplex1(1.0):           ") print("{simplex1(1.0)}") print("\n")
    print("  simplex1(1.5):           ") print("{simplex1(1.5)}") print("\n")

    print("  simplex2(0.5, 0.5):      ") print("{simplex2(0.5, 0.5)}") print("\n")
    print("  simplex2(1.0, 2.0):      ") print("{simplex2(1.0, 2.0)}") print("\n")

    print("  simplex3(0.5, 0.5, 0.5): ") print("{simplex3(0.5, 0.5, 0.5)}") print("\n")
    print("  simplex4(1,2,3,4):       ") print("{simplex4(1.0, 2.0, 3.0, 4.0)}") print("\n")

    // --- fBm ---
    print("\n=== Fractal Brownian Motion ===\n")
    print("  fbm2(0.5, 0.5, 4, 2.0, 0.5): ") print("{fbm2(0.5, 0.5, 4, 2.0, 0.5)}") print("\n")
    print("  fbm3(1, 2, 3, 6, 2.0, 0.5):  ") print("{fbm3(1.0, 2.0, 3.0, 6, 2.0, 0.5)}") print("\n")

    // --- 2D noise map (ASCII) ---
    // Quantize n to an integer (n * 100 truncated) before binning so
    // tiny float-rounding differences between TCC-compiled and
    // clang-compiled simplex don't flip a near-boundary cell. The
    // content of the map is still PRNG-driven; we just compare on the
    // rounded value.
    print("\n=== 2D Noise Map (10x5) ===\n")
    var y:i32 = 0
    while (y < 5) {
        var x:i32 = 0
        while (x < 20) {
            var n:f32 = simplex2(x * 0.2, y * 0.2)
            var q:i32 = (i32)(n * 100.0)
            if (q > 30)       { print("#") }
            else if (q > 0)   { print("+") }
            else if (q > -30) { print(".") }
            else              { print(" ") }
            x = x + 1
        }
        print("\n")
        y = y + 1
    }

    print("\n=== Done ===\n")
}
Out Params 41_out_params.gx
// 41_out_params.gx
//
// Pointer parameters are read-only unless you say otherwise. 'out' marks
// the ones a function may write through, so the signature tells you which
// arguments come back changed. GX functions cannot return pointers, out
// params are how results leave by reference. In the generated C this is
// nothing more than const T* versus T*.

struct Point {
    x: f32
    y: f32
}

struct Result {
    value: f32
    ok: bool
}

// 'out' pointer param: function CAN modify what the pointer points to
fn init_point(out p: *Point, x: f32, y: f32) {
    p.x = x
    p.y = y
}

fn scale_point(out p: *Point, factor: f32) {
    p.x = p.x * factor
    p.y = p.y * factor
}

// No 'out': pointer is read-only (const Point* in C)
fn print_point(p: *Point) {
    print("({p.x.str()}, {p.y.str()})\n")
    // p.x = 99.0  ← compile error: Cannot write through read-only pointer
}

// out value param: multi-return pattern
// 'out result:f32' becomes 'float*' in C
fn divide:bool(a: f32, b: f32, out result: f32) {
    if (b == 0.0) { return false }
    *result = a / b
    return true
}

// out struct param: fill a result struct
fn compute(a: f32, b: f32, out r: *Result) {
    if (b == 0.0) {
        r.ok = false
        r.value = 0.0
    } else {
        r.ok = true
        r.value = a / b
    }
}

fn main() {
    // === Out pointer param: init and modify ===
    var pt = Point{0.0, 0.0}
    init_point(&pt, 3.0, 4.0)
    print("After init: ")
    print_point(&pt)

    scale_point(&pt, 2.0)
    print("After scale: ")
    print_point(&pt)

    // === Out value param (multi-return) ===
    var result: f32 = 0.0
    if (divide(10.0, 3.0, &result)) {
        print("10 / 3 = {result.str()}\n")
    }
    if (!divide(5.0, 0.0, &result)) {
        print("Division by zero caught!\n")
    }

    // === Out struct param ===
    var r = Result{0.0, false}
    compute(10.0, 4.0, &r)
    if (r.ok) {
        print("10 / 4 = {r.value.str()}\n")
    }
}
Typedef 42_typedef.gx
// 42_typedef.gx
//
// type NewName:ExistingType makes an alias, fully interchangeable with the
// original type. It compiles to a C typedef, so it costs nothing and
// changes nothing about the layout. Use it to give a type a name that
// matches how you actually think about it.

// Alias to built-in types
type byte:u8
type size:u64
type real:f32

// Alias to pointer type
type cstring:*c_char

struct Point {
    x: real
    y: real
}

type Vec2D:Point

fn distance:real(a: *Vec2D, b: *Vec2D) {
    var dx:real = b.x - a.x
    var dy:real = b.y - a.y
    return dx * dx + dy * dy
}

fn main() {
    var b:byte = 255
    var s:size = 1024
    var r:real = 3.14

    print("byte: {b.str()}\n")
    print("size: {s.str()}\n")
    print("real: {r.str()}\n")

    var p1 = Vec2D{1.0, 2.0}
    var p2 = Vec2D{4.0, 6.0}
    var d = distance(&p1, &p2)
    print("distance^2: {d.str()}\n")
}
Errors 43_errors.gx
// 43_errors.gx
//
// GX has no exceptions, and that is a decision, not a gap. An exception
// is invisible control flow: any call might jump out of your function,
// and the call site gives no hint which ones. The machinery that makes
// it work, stack unwinding, is exactly the kind of hidden cost GX
// exists to avoid. So errors in GX are values. A function that can
// fail says so in its signature, returns a bool or an error enum, and
// hands the result out through an out parameter. The caller checks,
// right there, in code you can read top to bottom.
//
// If you come from Python or JavaScript: everywhere you would reach
// for try/except, you check a return value instead. A little more
// typing at the call site, far fewer surprises everywhere else.
//
// Panics are the other half of the story. Indexing out of bounds
// panics (27_bounds_check.gx shows it), because that is a bug, not a
// condition your program should handle. The rule of thumb: expected
// failure is a value you check, impossible situations panic.

// --- Pattern 1: bool return, value through an out param ---
// The GX version of Python's int(s) with its ValueError: the bool says
// whether the parse worked, the out param carries the number.
fn parse_i32:bool(s: str, out result: i32) {
    if (s.len == 0) { return false }
    var value:i32 = 0
    var i:i64 = 0
    var negative:bool = false
    if (s[0] == 45) {              // leading '-'
        negative = true
        i = 1
        if (s.len == 1) { return false }
    }
    while (i < s.len) {
        var c:i32 = (i32)s[i]
        if (c < 48 || c > 57) { return false }   // outside '0'..'9'
        value = value * 10 + (c - 48)
        i = i + 1
    }
    if (negative) { value = 0 - value }
    *result = value
    return true
}

// --- Pattern 2: an error enum, when "failed" needs more than one flavor ---
enum LoadError {
    None
    NotFound
    Corrupt
}

fn load_level:LoadError(id: i32) {
    if (id < 0)   { return NotFound }
    if (id > 100) { return Corrupt }
    return None
}

fn main() {
    // Pattern 1 in use: check, then trust the value.
    var n:i32 = 0
    if (parse_i32("1984", &n)) {
        print("parsed: {n}\n")
    }
    if (!parse_i32("19x4", &n)) {
        print("'19x4' is not a number\n")
    }
    if (parse_i32("-42", &n)) {
        print("parsed: {n}\n")
    }

    // Pattern 2 in use: match makes every failure case visible,
    // and handling them all is right there to see.
    var err = load_level(7)
    match (err) {
        None:     print("level 7 loaded\n")
        NotFound: print("level 7 missing\n")
        Corrupt:  print("level 7 damaged\n")
    }

    err = load_level(-1)
    match (err) {
        None:     print("level -1 loaded\n")
        NotFound: print("level -1 missing\n")
        Corrupt:  print("level -1 damaged\n")
    }
}
Microaudio 44_microaudio.gx
// 44_microaudio.gx
//
// Load a WAV file, set the volume, play it, free it. That is the whole
// microaudio API surface this needs. Sound does not have to be complicated.

import microaudio.audio

fn main() {
    audio_init()
    defer audio_shutdown()

    // Load a WAV file (place a .wav file next to the executable)
    var snd = audio_load("test.wav")
    if (snd < 0) {
        print("Could not load test.wav\n")
        print("Place a WAV file named test.wav in the working directory.\n")
        return
    }

    print("Sound loaded (id={snd.str()})\n")

    // Set volume to 80%
    audio_set_volume(snd, 0.8)

    // Play the sound
    audio_play(snd)
    print("Playing... press Enter to stop.\n")

    input()

    audio_stop(snd)
    audio_free(snd)
    print("Done.\n")
}
Json 45_json.gx
// 45_json.gx
//
// Parse a JSON string, pull values out of it, then build a new document
// from scratch and serialize both. The json module is a thin layer over
// yyjson, a C library, so you hold handles into the document and every
// call goes straight through. Parsed documents are read-only; building
// happens on a separate mutable document. Free what you parse.

import json

fn main() {
    // ---- Parsing JSON ----
    var text = "{\"name\": \"Alice\", \"age\": 30, \"scores\": [95, 87, 92], \"active\": true}"

    var doc = json.gx_json_parse(text.cstr, text.len, 0)
    if (doc == 0) {
        print("Failed to parse JSON\n")
        return
    }
    defer json.gx_json_free(doc)

    var r = json.gx_json_root(doc)

    // ---- Reading values ----
    var name = json.gx_json_get_str(json.gx_json_obj_get(r, "name"))
    var age  = json.gx_json_get_int(json.gx_json_obj_get(r, "age"))
    var active = json.gx_json_get_bool(json.gx_json_obj_get(r, "active"))
    print("Name: {name}\n")
    print("Age:  {age}\n")
    print("Active: {active}\n")

    // ---- Array access ----
    var scores = json.gx_json_obj_get(r, "scores")
    print("Scores:\n")
    // Array access by index
    var s0 = json.gx_json_get_int(json.gx_json_arr_get(scores, 0))
    var s1 = json.gx_json_get_int(json.gx_json_arr_get(scores, 1))
    var s2 = json.gx_json_get_int(json.gx_json_arr_get(scores, 2))
    print("  [0] = {s0}\n")
    print("  [1] = {s1}\n")
    print("  [2] = {s2}\n")

    // ---- Type inspection ----
    var name_type = json.gx_json_type_desc(json.gx_json_obj_get(r, "name"))
    var age_type = json.gx_json_type_desc(json.gx_json_obj_get(r, "age"))
    var scores_type = json.gx_json_type_desc(scores)
    print("Type of name: {name_type}\n")
    print("Type of age:  {age_type}\n")
    print("Type of scores: {scores_type}\n")

    // ---- Serialize back (pretty) ----
    var pretty = json.gx_json_write(doc, json.WRITE_PRETTY)
    print("\nPretty printed:\n{pretty}\n")

    // ---- Build JSON from scratch ----
    print("--- Building JSON ---\n")
    var mdoc = json.gx_json_new_doc()
    defer json.gx_json_mut_free(mdoc)

    var obj = json.gx_json_new_obj(mdoc)
    json.gx_json_set_root(mdoc, obj)

    json.gx_json_obj_add_str(mdoc, obj, "language", "GX")
    json.gx_json_obj_add_int(mdoc, obj, "version", 2)
    json.gx_json_obj_add_bool(mdoc, obj, "fast", true)
    json.gx_json_obj_add_float(mdoc, obj, "pi", 3.14159)

    // Nested array
    var arr = json.gx_json_obj_add_arr(mdoc, obj, "features")
    json.gx_json_arr_add_str(mdoc, arr, "transpiles to C")
    json.gx_json_arr_add_str(mdoc, arr, "zero-cost abstractions")
    json.gx_json_arr_add_str(mdoc, arr, "yyjson powered")

    // Nested object
    var meta = json.gx_json_obj_add_obj(mdoc, obj, "meta")
    json.gx_json_obj_add_str(mdoc, meta, "author", "GX Team")
    json.gx_json_obj_add_int(mdoc, meta, "year", 2026)

    var result = json.gx_json_mut_write(mdoc, json.WRITE_PRETTY)
    print("{result}\n")
}
Time 46_time.gx
// 46_time.gx
//
// Dates, formatting, a nanosecond clock, and sleep from the time module.
// The output is deterministic on purpose: dates come from a fixed epoch
// and the timing checks print pass or fail instead of raw numbers, so
// two runs produce identical output.

import time

fn main() {
    // ---- Date/time components against a fixed epoch ----
    // Jan 1, 2026 00:00:00 local time. Deterministic across runs.
    var epoch = time.gx_time_make(2026, 1, 1, 0, 0, 0)

    var y = time.gx_time_year(epoch)
    var mo = time.gx_time_month(epoch)
    var d = time.gx_time_day(epoch)
    print("Fixed epoch year/month/day: {y}-{mo}-{d}\n")

    // ---- Formatting against the fixed epoch ----
    var formatted = time.gx_time_format(epoch, "%Y-%m-%d %H:%M:%S")
    print("Formatted: {formatted}\n")

    var day_name = time.gx_time_format(epoch, "%A")
    print("Weekday name: {day_name}\n")

    // ---- Benchmarking: verify only that elapsed > 0, not the value ----
    print("\n--- Benchmark ---\n")
    var t0 = time.gx_time_now_ns()
    var sum = 0
    for (i = 0 : 999999) {
        sum = sum + i
    }
    var t1 = time.gx_time_now_ns()
    var ms = time.gx_time_elapsed_ms(t0, t1)
    print("Sum of 0..999999 = {sum}\n")
    if (ms >= 0.0) {
        print("Elapsed: measurable\n")
    } else {
        print("Elapsed: ERROR negative duration\n")
    }

    // ---- Sleep: verify only that we slept at least ~95ms ----
    print("\n--- Sleep test ---\n")
    var s0 = time.gx_time_now_ns()
    time.gx_time_sleep_ms(100)
    var s1 = time.gx_time_now_ns()
    var slept = time.gx_time_elapsed_ms(s0, s1)
    if (slept >= 95.0) {
        print("Slept ~100ms: OK (>= 95ms actually elapsed)\n")
    } else {
        print("Slept ~100ms: FAIL (only {slept} ms)\n")
    }

    // ---- Named date ----
    var new_year = time.gx_time_format(epoch, "%Y-%m-%d %H:%M:%S (%A)")
    print("\nNew Year 2026: {new_year}\n")
}
Xml 47_xml.gx
// 47_xml.gx
//
// Parse an XML document, walk its children, read attributes and text,
// then build a fresh tree and serialize it. Nodes are handles, siblings
// come back one at a time, and freeing what you parsed or built is your
// job. No surprises there.

import xml

fn main() {
    // ---- Parse XML from string ----
    var data = "<library name='City Library'><book id='1'><title>The Rust Programming Language</title><author>Steve Klabnik</author><year>2019</year></book><book id='2'><title>The C Programming Language</title><author>Kernighan and Ritchie</author><year>1978</year></book><book id='3'><title>Structure and Interpretation</title><author>Abelson and Sussman</author><year>1996</year></book></library>"

    var doc = xml.gx_xml_parse(data.cstr, data.len)
    if (xml.gx_xml_has_error(doc)) {
        print("Parse error: {xml.gx_xml_error(doc)}\n")
        xml.gx_xml_free(doc)
        return
    }

    // Root element
    print("Root tag: {xml.gx_xml_name(doc)}\n")

    var lib_name = xml.gx_xml_attr(doc, "name")
    print("Library: {lib_name}\n")

    var num_books = xml.gx_xml_child_count(doc, "book")
    print("Book count: {num_books}\n\n")

    // Iterate children with same tag name
    var book = xml.gx_xml_child(doc, "book")
    while (book != 0) {
        var id = xml.gx_xml_attr(book, "id")
        var title = xml.gx_xml_txt(xml.gx_xml_child(book, "title"))
        var author = xml.gx_xml_txt(xml.gx_xml_child(book, "author"))
        var year = xml.gx_xml_txt(xml.gx_xml_child(book, "year"))
        print("Book {id}: {title} by {author} ({year})\n")
        book = xml.gx_xml_next(book)
    }

    // ---- Build XML from scratch ----
    print("\n--- Building XML ---\n")
    var root = xml.gx_xml_new("config")
    xml.gx_xml_set_attr(root, "version", "1.0")

    var window = xml.gx_xml_add_child(root, "window")
    xml.gx_xml_set_attr(window, "width", "1280")
    xml.gx_xml_set_attr(window, "height", "720")
    xml.gx_xml_set_txt(window, "Main Window")

    var gfx = xml.gx_xml_add_child(root, "graphics")
    xml.gx_xml_set_attr(gfx, "backend", "opengl")

    var res = xml.gx_xml_add_child(gfx, "resolution")
    xml.gx_xml_set_txt(res, "1920x1080")

    var aa = xml.gx_xml_add_child(gfx, "antialiasing")
    xml.gx_xml_set_txt(aa, "4x MSAA")

    // Serialize to string
    var xml_str = xml.gx_xml_to_str(root)
    print("{xml_str}\n")

    xml.gx_xml_free_str(xml_str)
    xml.gx_xml_free(root)
    xml.gx_xml_free(doc)
}
Clay 48_clay.gx
// 48_clay.gx
//
// Clay is a C layout library: you declare a tree of containers and text,
// it hands back a flat list of render commands. This file builds a small
// UI, header, sidebar, main panel, then prints every command instead of
// drawing it. No window, no renderer, just the layout math. That split is
// the whole point of Clay, and it binds to GX through the clay module.

import clay

fn main() {
    // Initialize Clay with 800x600 layout
    var ctx = clay.gx_clay_init(800.0, 600.0)
    clay.gx_clay_use_default_measure()

    // Simulate mouse at center, not pressed
    clay.gx_clay_set_pointer(400.0, 300.0, false)
    clay.gx_clay_update_scroll(false, 0.0, 0.0, 0.016)

    // Begin layout
    clay.gx_clay_begin_layout()

    // Root container: full-size column, dark background
    clay.gx_clay_open_id("root")
    clay.gx_clay_config_full(
        clay.COLUMN,
        clay.SIZING_GROW, 0.0, 0.0,
        clay.SIZING_GROW, 0.0, 0.0,
        16, 16, 16, 16,
        8,
        clay.ALIGN_LEFT, clay.ALIGN_TOP,
        40.0, 40.0, 40.0, 255.0,
        0.0, 0.0, 0.0, 0.0
    )

        // Header bar
        clay.gx_clay_open_id("header")
        clay.gx_clay_config_full(
            clay.ROW,
            clay.SIZING_GROW, 0.0, 0.0,
            clay.SIZING_FIT, 0.0, 0.0,
            12, 12, 8, 8,
            0,
            clay.ALIGN_LEFT, clay.ALIGN_CENTER,
            60.0, 60.0, 60.0, 255.0,
            8.0, 8.0, 8.0, 8.0
        )
            clay.gx_clay_text("GX + Clay UI Demo", 255.0, 255.0, 255.0, 255.0, 0, 24)
        clay.gx_clay_close()

        // Content area: two columns
        clay.gx_clay_open_id("content")
        clay.gx_clay_config_row(12, 0, 0)

            // Sidebar
            clay.gx_clay_open_id("sidebar")
            clay.gx_clay_config_full(
                clay.COLUMN,
                clay.SIZING_FIXED, 200.0, 200.0,
                clay.SIZING_GROW, 0.0, 0.0,
                8, 8, 8, 8,
                4,
                clay.ALIGN_LEFT, clay.ALIGN_TOP,
                50.0, 50.0, 55.0, 255.0,
                6.0, 6.0, 6.0, 6.0
            )
                clay.gx_clay_text("Menu", 200.0, 200.0, 200.0, 255.0, 0, 18)
                clay.gx_clay_text("- Home", 180.0, 180.0, 180.0, 255.0, 0, 16)
                clay.gx_clay_text("- Settings", 180.0, 180.0, 180.0, 255.0, 0, 16)
                clay.gx_clay_text("- About", 180.0, 180.0, 180.0, 255.0, 0, 16)
            clay.gx_clay_close()

            // Main panel
            clay.gx_clay_open_id("main")
            clay.gx_clay_config_full(
                clay.COLUMN,
                clay.SIZING_GROW, 0.0, 0.0,
                clay.SIZING_GROW, 0.0, 0.0,
                12, 12, 12, 12,
                8,
                clay.ALIGN_LEFT, clay.ALIGN_TOP,
                50.0, 55.0, 50.0, 255.0,
                6.0, 6.0, 6.0, 6.0
            )
                clay.gx_clay_text("Welcome to Clay!", 255.0, 255.0, 200.0, 255.0, 0, 20)
                clay.gx_clay_text("This is a UI layout computed by Clay.", 200.0, 200.0, 200.0, 255.0, 0, 16)
                clay.gx_clay_text("Render commands below:", 200.0, 200.0, 200.0, 255.0, 0, 16)
            clay.gx_clay_close()

        clay.gx_clay_close()

    clay.gx_clay_close()

    // End layout and get render commands
    var count: i32 = 0
    var cmds = clay.gx_clay_end_layout(0.016, &count)

    print("Clay generated {count} render commands:\n\n")

    for (i = 0 : count) {
        var cmd = clay.gx_clay_get_command(cmds, i)
        var cmd_type = clay.gx_clay_cmd_type(cmd)
        var x = clay.gx_clay_cmd_x(cmd)
        var y = clay.gx_clay_cmd_y(cmd)
        var w = clay.gx_clay_cmd_w(cmd)
        var h = clay.gx_clay_cmd_h(cmd)

        if (cmd_type == clay.CMD_RECTANGLE) {
            var r = clay.gx_clay_cmd_rect_r(cmd)
            var g = clay.gx_clay_cmd_rect_g(cmd)
            var b = clay.gx_clay_cmd_rect_b(cmd)
            print("  RECT  ({x}, {y}) {w}x{h}  color=({r},{g},{b})\n")
        }
        if (cmd_type == clay.CMD_TEXT) {
            var text = clay.gx_clay_cmd_text_chars(cmd)
            var fs = clay.gx_clay_cmd_text_font_size(cmd)
            print("  TEXT  ({x}, {y}) {w}x{h}  \"{text}\" size={fs}\n")
        }
        if (cmd_type == clay.CMD_BORDER) {
            print("  BORDER ({x}, {y}) {w}x{h}\n")
        }
        if (cmd_type == clay.CMD_SCISSOR_START) {
            print("  SCISSOR_START ({x}, {y}) {w}x{h}\n")
        }
        if (cmd_type == clay.CMD_SCISSOR_END) {
            print("  SCISSOR_END\n")
        }
    }
}
Clay Raylib 49_clay_raylib.gx
// 49_clay_raylib.gx
//
// The same Clay idea as 48, but drawn for real. Raylib opens the window,
// Clay computes the layout every frame, and clay_raylib turns the command
// list into draw calls with a single function. Mouse and scroll feed into
// Clay, F1 toggles its debug overlay. Uses the clay and raylib modules.

import clay
import clay_raylib
import raylib.core
import raylib.input
import raylib.text
import raylib.shapes
import raylib.colors

fn main() {
    InitWindow(800, 600, "GX + Clay + Raylib")
    SetTargetFPS(60)

    // Initialize Clay
    var ctx = clay.gx_clay_init(800.0, 600.0)

    // Set up font table (index 0 = default font)
    var font = GetFontDefault()
    clay_raylib.gx_clay_raylib_init(&font, 1)

    while (!WindowShouldClose()) {
        var dt = GetFrameTime()

        // Feed input to Clay
        var mx = GetMouseX()
        var my = GetMouseY()
        clay.gx_clay_set_pointer(mx, my, IsMouseButtonDown(MOUSE_BUTTON_LEFT))
        clay.gx_clay_update_scroll(true, 0.0, GetMouseWheelMove() * 30.0, dt)

        // Toggle debug view with F1
        if (IsKeyPressed(KEY_F1)) {
            clay.gx_clay_set_debug(!clay.gx_clay_is_debug())
        }

        // ---- Declare UI ----
        clay.gx_clay_begin_layout()

        // Root: full-screen dark column
        clay.gx_clay_open_id("root")
        clay.gx_clay_config_full(
            clay.COLUMN,
            clay.SIZING_GROW, 0.0, 0.0,
            clay.SIZING_GROW, 0.0, 0.0,
            16, 16, 16, 16, 12,
            clay.ALIGN_LEFT, clay.ALIGN_TOP,
            35.0, 35.0, 38.0, 255.0,
            0.0, 0.0, 0.0, 0.0
        )

            // Header bar
            clay.gx_clay_open_id("header")
            clay.gx_clay_config_full(
                clay.ROW,
                clay.SIZING_GROW, 0.0, 0.0,
                clay.SIZING_FIT, 0.0, 0.0,
                16, 16, 10, 10, 0,
                clay.ALIGN_LEFT, clay.ALIGN_CENTER,
                55.0, 55.0, 60.0, 255.0,
                8.0, 8.0, 8.0, 8.0
            )
                clay.gx_clay_text("Clay + Raylib Renderer", 230.0, 230.0, 240.0, 255.0, 0, 20)
            clay.gx_clay_close()

            // Body: sidebar + main
            clay.gx_clay_open_id("body")
            clay.gx_clay_config_full(
                clay.ROW,
                clay.SIZING_GROW, 0.0, 0.0,
                clay.SIZING_GROW, 0.0, 0.0,
                0, 0, 0, 0, 12,
                clay.ALIGN_LEFT, clay.ALIGN_TOP,
                0.0, 0.0, 0.0, 0.0,
                0.0, 0.0, 0.0, 0.0
            )

                // Sidebar
                clay.gx_clay_open_id("sidebar")
                clay.gx_clay_config_full(
                    clay.COLUMN,
                    clay.SIZING_FIXED, 180.0, 180.0,
                    clay.SIZING_GROW, 0.0, 0.0,
                    12, 12, 12, 12, 6,
                    clay.ALIGN_LEFT, clay.ALIGN_TOP,
                    45.0, 45.0, 50.0, 255.0,
                    8.0, 8.0, 8.0, 8.0
                )
                    clay.gx_clay_text("Navigation", 200.0, 200.0, 210.0, 255.0, 0, 16)

                    // Menu items
                    for (i = 0 : 4) {
                        clay.gx_clay_open_idi("menu", i)
                        clay.gx_clay_config_full(
                            clay.ROW,
                            clay.SIZING_GROW, 0.0, 0.0,
                            clay.SIZING_FIT, 0.0, 0.0,
                            8, 8, 6, 6, 0,
                            clay.ALIGN_LEFT, clay.ALIGN_CENTER,
                            55.0, 55.0, 60.0, 255.0,
                            4.0, 4.0, 4.0, 4.0
                        )
                            if (i == 0) { clay.gx_clay_text("Dashboard", 180.0, 180.0, 190.0, 255.0, 0, 14) }
                            if (i == 1) { clay.gx_clay_text("Settings", 180.0, 180.0, 190.0, 255.0, 0, 14) }
                            if (i == 2) { clay.gx_clay_text("Profile", 180.0, 180.0, 190.0, 255.0, 0, 14) }
                            if (i == 3) { clay.gx_clay_text("About", 180.0, 180.0, 190.0, 255.0, 0, 14) }
                        clay.gx_clay_close()
                    }
                clay.gx_clay_close()

                // Main content area
                clay.gx_clay_open_id("main")
                clay.gx_clay_config_full(
                    clay.COLUMN,
                    clay.SIZING_GROW, 0.0, 0.0,
                    clay.SIZING_GROW, 0.0, 0.0,
                    16, 16, 16, 16, 10,
                    clay.ALIGN_LEFT, clay.ALIGN_TOP,
                    42.0, 42.0, 46.0, 255.0,
                    8.0, 8.0, 8.0, 8.0
                )
                    clay.gx_clay_text("Welcome to Clay!", 255.0, 220.0, 150.0, 255.0, 0, 22)
                    clay.gx_clay_text("This UI is laid out by Clay and rendered by Raylib.", 190.0, 190.0, 200.0, 255.0, 0, 14)
                    clay.gx_clay_text("Press F1 to toggle the debug overlay.", 160.0, 160.0, 170.0, 255.0, 0, 14)

                    // Status cards row
                    clay.gx_clay_open_id("cards")
                    clay.gx_clay_config_full(
                        clay.ROW,
                        clay.SIZING_GROW, 0.0, 0.0,
                        clay.SIZING_FIT, 0.0, 0.0,
                        0, 0, 0, 0, 10,
                        clay.ALIGN_LEFT, clay.ALIGN_TOP,
                        0.0, 0.0, 0.0, 0.0,
                        0.0, 0.0, 0.0, 0.0
                    )
                        // Card 1
                        clay.gx_clay_open_id("card1")
                        clay.gx_clay_config_full(
                            clay.COLUMN,
                            clay.SIZING_GROW, 0.0, 0.0,
                            clay.SIZING_FIT, 0.0, 0.0,
                            12, 12, 10, 10, 4,
                            clay.ALIGN_LEFT, clay.ALIGN_TOP,
                            60.0, 80.0, 60.0, 255.0,
                            6.0, 6.0, 6.0, 6.0
                        )
                            clay.gx_clay_text("FPS", 150.0, 200.0, 150.0, 255.0, 0, 12)
                            clay.gx_clay_text("60", 100.0, 255.0, 100.0, 255.0, 0, 28)
                        clay.gx_clay_close()

                        // Card 2
                        clay.gx_clay_open_id("card2")
                        clay.gx_clay_config_full(
                            clay.COLUMN,
                            clay.SIZING_GROW, 0.0, 0.0,
                            clay.SIZING_FIT, 0.0, 0.0,
                            12, 12, 10, 10, 4,
                            clay.ALIGN_LEFT, clay.ALIGN_TOP,
                            60.0, 60.0, 80.0, 255.0,
                            6.0, 6.0, 6.0, 6.0
                        )
                            clay.gx_clay_text("Elements", 150.0, 150.0, 200.0, 255.0, 0, 12)
                            clay.gx_clay_text("12", 100.0, 150.0, 255.0, 255.0, 0, 28)
                        clay.gx_clay_close()

                        // Card 3
                        clay.gx_clay_open_id("card3")
                        clay.gx_clay_config_full(
                            clay.COLUMN,
                            clay.SIZING_GROW, 0.0, 0.0,
                            clay.SIZING_FIT, 0.0, 0.0,
                            12, 12, 10, 10, 4,
                            clay.ALIGN_LEFT, clay.ALIGN_TOP,
                            80.0, 60.0, 60.0, 255.0,
                            6.0, 6.0, 6.0, 6.0
                        )
                            clay.gx_clay_text("Layout", 200.0, 150.0, 150.0, 255.0, 0, 12)
                            clay.gx_clay_text("OK", 255.0, 150.0, 100.0, 255.0, 0, 28)
                        clay.gx_clay_close()
                    clay.gx_clay_close()
                clay.gx_clay_close()

            clay.gx_clay_close()

        clay.gx_clay_close()

        // ---- Compute layout & render ----
        var count: i32 = 0
        var cmds = clay.gx_clay_end_layout(dt, &count)

        BeginDrawing()
        ClearBackground(BLACK)
        clay_raylib.gx_clay_render_raylib(cmds, count)
        EndDrawing()
    }
    CloseWindow()
}

External Libraries

Enet 50_enet.gx
// 50_enet.gx
//
// ENet server and client in one process. The client dials 127.0.0.1:7777,
// sends one reliable packet, the server receives it, both disconnect and
// tear down. The enet module binds the C library directly: ENetAddress
// and ENetEvent are the real C structs, hosts and peers are plain
// pointers, nothing sits between your code and the library.

import enet

fn main() {
    var ok: bool = true

    // ---- Init ----
    if (enet_initialize() != 0) {
        print("ENet init FAILED\n")
        ok = false
    }
    if (ok) { print("ENet init OK\n") }

    // Friction #8 RESOLVED: `null` literal is now in the language.
    var server: *void = null
    var client: *void = null
    var peer:   *void = null

    if (ok) {
        var server_addr: ENetAddress
        server_addr.host = ENET_HOST_ANY
        server_addr.port = 7777
        server = enet_host_create(&server_addr, 4, 2, 0, 0)
        if (server == null) {
            print("server create FAILED\n")
            ok = false
        }
    }
    if (ok) { print("server listening on port 7777\n") }

    if (ok) {
        client = enet_host_create(0, 1, 2, 0, 0)
        if (client == null) {
            print("client create FAILED\n")
            ok = false
        }
    }

    if (ok) {
        var connect_addr: ENetAddress
        enet_address_set_host(&connect_addr, "127.0.0.1")
        connect_addr.port = 7777
        peer = enet_host_connect(client, &connect_addr, 2, 0)
        if (peer == null) {
            print("client connect FAILED\n")
            ok = false
        }
    }
    if (ok) { print("client dialing 127.0.0.1:7777...\n") }

    // ---- Pump until handshake completes ----
    var ev: ENetEvent
    var connected: bool = false
    var iters: i32 = 0
    while (ok && !connected) {
        if (iters > 500) {
            print("handshake TIMEOUT\n")
            ok = false
        } else {
            var sr: i32 = enet_host_service(server, &ev, 5)
            if (sr > 0) {
                if (ev.type == ENET_EVENT_TYPE_CONNECT) {
                    print("[server] peer connected\n")
                }
            }
            var cr: i32 = enet_host_service(client, &ev, 5)
            if (cr > 0) {
                if (ev.type == ENET_EVENT_TYPE_CONNECT) {
                    print("[client] connected to server\n")
                    connected = true
                }
            }
            iters = iters + 1
        }
    }

    // ---- Send one reliable packet ----
    if (ok) {
        var msg: cstr = "hello from gx!"
        // cstr passes directly to an extern fn taking *void - the transpiler
        // no longer wraps it in ec_str_make for non-str pointer params.
        var pkt: *void = enet_packet_create(msg, 14, ENET_PACKET_FLAG_RELIABLE)
        enet_peer_send(peer, 0, pkt)
        enet_host_flush(client)
        print("[client] sent packet\n")
    }

    // ---- Wait for receive ----
    var got:bool = false
    iters = 0
    while (ok && !got) {
        if (iters > 200) {
            print("receive TIMEOUT\n")
            ok = false
        } else {
            var sr: i32 = enet_host_service(server, &ev, 5)
            if (sr > 0) {
                if (ev.type == ENET_EVENT_TYPE_RECEIVE) {
                    print("[server] got packet, channel={ev.channelID}\n")
                    enet_packet_destroy(ev.packet)
                    got = true
                }
            }
            enet_host_service(client, &ev, 0)
            iters = iters + 1
        }
    }

    // ---- Disconnect cleanly ----
    if (ok && peer != null) {
        enet_peer_disconnect(peer, 0)
        iters = 0
        var done: bool = false
        while (!done && iters < 50) {
            var sr: i32 = enet_host_service(server, &ev, 5)
            if (sr > 0) {
                if (ev.type == ENET_EVENT_TYPE_DISCONNECT) {
                    print("[server] peer disconnected\n")
                    done = true
                }
            }
            enet_host_service(client, &ev, 5)
            iters = iters + 1
        }
    }

    // ---- Tear down ----
    if (client != null) { enet_host_destroy(client) }
    if (server != null) { enet_host_destroy(server) }
    enet_deinitialize()
    if (ok) {
        print("smoke test PASSED\n")
    } else {
        print("smoke test FAILED\n")
    }
}
Threading 51_threading.gx
// 51_threading.gx
//
// Eight worker threads each push 100,000 ints into one chan<i32> while
// main drains it, then joins them all and checks the count. The channel
// is bounded at 64 slots, so producers block when it fills and wake as
// main reads. That back-pressure is the load-bearing part. No atomics,
// no raw shared memory, the channel is the only thing the threads share.

import threading

const NUM_THREADS = 8
const ITERS_PER_THREAD = 100000

var g_ch: chan<i32>(capacity = 64)

fn worker:i32(arg: *void) {
    var i: i32 = 0
    while (i < ITERS_PER_THREAD) {
        g_ch.send(i)
        i = i + 1
    }
    return 0
}

fn main() {
    // Spawn N workers - typed handle array, no *void in user code.
    var threads: Thread[8]
    var i: i32 = 0
    while (i < NUM_THREADS) {
        gx_thread_start(&threads[i], worker, 0)
        i = i + 1
    }

    // Drain in parallel with workers - required for back-pressure
    // to release. Producers block on full sends until we recv.
    var got_count: i32 = 0
    var expected: i32 = NUM_THREADS * ITERS_PER_THREAD
    var v: i32
    while (got_count < expected) {
        if (g_ch.recv(&v)) { got_count = got_count + 1 }
    }

    // All values delivered - workers are done. Join.
    i = 0
    while (i < NUM_THREADS) {
        gx_thread_join(threads[i])
        i = i + 1
    }
    g_ch.close()

    print("received = {got_count}, expected = {expected}\n")
    if (got_count == expected) {
        print("smoke test PASSED\n")
    } else {
        print("smoke test FAILED\n")
    }
}
Sdl3 52_sdl3.gx
// 52_sdl3.gx
//
// A sideways Breakout in one file, rendered with SDL3. Bricks on the left,
// your paddle on the right, up and down to move, ESC to quit. Clear the
// wall and a new one appears. The sdl3 module binds the C API directly,
// SDL_Event and SDL_FRect are the real structs, and vsync paces the loop
// instead of SDL_Delay. Needs SDL3.dll beside the executable or on PATH.

import sdl3

const SCREEN_W: f32 = 800.0
const SCREEN_H: f32 = 600.0
const PADDLE_W: f32 = 14.0
const PADDLE_H: f32 = 100.0
const PADDLE_SPEED: f32 = 6.0
const BALL_SIZE: f32 = 14.0

// Brick wall - 5 columns × 8 rows on the left third of the screen.
const BRICK_COLS: i32 = 5
const BRICK_ROWS: i32 = 8
const BRICK_W: f32 = 40.0
const BRICK_H: f32 = 50.0
const BRICK_GAP: f32 = 4.0
const BRICK_X0: f32 = 20.0
const BRICK_Y0: f32 = 80.0
const BRICK_COUNT: i32 = 40   // BRICK_COLS * BRICK_ROWS

fn clamp_paddle:f32(y: f32) {
    if (y < 0.0) { return 0.0 }
    var max_y: f32 = SCREEN_H - PADDLE_H
    if (y > max_y) { return max_y }
    return y
}

fn main() {
    var ok: bool = true
    var window: *void = null
    var renderer: *void = null

    if (!SDL_Init(SDL_INIT_VIDEO)) {
        print("SDL_Init failed: {SDL_GetError()}\n")
        ok = false
    }
    if (ok) {
        window = SDL_CreateWindow("GX Breakout", 800, 600, 0)
        if (window == null) { print("CreateWindow failed: {SDL_GetError()}\n"); ok = false }
    }
    if (ok) {
        renderer = SDL_CreateRenderer(window, null)
        if (renderer == null) { print("CreateRenderer failed: {SDL_GetError()}\n"); ok = false }
    }
    if (ok) {
        // vsync removes the 15-ms granularity jitter SDL_Delay would
        // otherwise introduce on Windows. Caps to monitor refresh.
        SDL_SetRenderVSync(renderer, 1)
    }

    // ---- Game state ----
    var paddle_y: f32 = (SCREEN_H - PADDLE_H) * 0.5
    var ball_x:   f32 = SCREEN_W * 0.5
    var ball_y:   f32 = SCREEN_H * 0.5
    var ball_vx:  f32 = 0.0 - 4.0   // start heading toward bricks
    var ball_vy:  f32 = 3.0
    var miss:     i32 = 0
    var cleared:  i32 = 0

    // Brick aliveness - flat array indexed by row*COLS + col.
    var bricks: bool[40]
    var bi: i32 = 0
    while (bi < BRICK_COUNT) { bricks[bi] = true; bi = bi + 1 }
    var bricks_left: i32 = BRICK_COUNT

    var quit: bool = !ok
    var ev: SDL_Event
    while (!quit) {
        while (SDL_PollEvent(&ev)) {
            if (ev.type == SDL_EVENT_QUIT) { quit = true }
            if (ev.type == SDL_EVENT_KEY_DOWN) {
                if (ev.key.scancode == SDL_SCANCODE_ESCAPE) { quit = true }
            }
        }

        var keys: *c_bool = SDL_GetKeyboardState(null)
        if (keys[SDL_SCANCODE_UP])   { paddle_y = paddle_y - PADDLE_SPEED }
        if (keys[SDL_SCANCODE_DOWN]) { paddle_y = paddle_y + PADDLE_SPEED }
        paddle_y = clamp_paddle(paddle_y)

        // ---- Ball physics ----
        ball_x = ball_x + ball_vx
        ball_y = ball_y + ball_vy

        // Top/bottom wall bounce
        if (ball_y < 0.0)                 { ball_y = 0.0;                  ball_vy = 0.0 - ball_vy }
        if (ball_y + BALL_SIZE > SCREEN_H) { ball_y = SCREEN_H - BALL_SIZE; ball_vy = 0.0 - ball_vy }

        // Left wall bounce (in case the ball passes all the bricks)
        if (ball_x < 0.0) { ball_x = 0.0; ball_vx = 0.0 - ball_vx }

        // Brick collision - scan the alive bricks; first AABB overlap wins.
        // To pick the right axis to reverse, we look at where the ball was
        // BEFORE this frame's step. Whichever axis it was already overlapping
        // on is the axis along which it entered → reverse the OTHER axis's
        // velocity. This correctly handles top/bottom hits (reverse vy) vs
        // side hits (reverse vx).
        var prev_x: f32 = ball_x - ball_vx
        var r: i32 = 0
        var hit: bool = false
        while (r < BRICK_ROWS && !hit) {
            var c: i32 = 0
            while (c < BRICK_COLS && !hit) {
                var idx: i32 = r * BRICK_COLS + c
                if (bricks[idx]) {
                    var bx: f32 = BRICK_X0 + c * (BRICK_W + BRICK_GAP)
                    var by: f32 = BRICK_Y0 + r * (BRICK_H + BRICK_GAP)
                    if (ball_x + BALL_SIZE > bx) {
                        if (ball_x < bx + BRICK_W) {
                            if (ball_y + BALL_SIZE > by) {
                                if (ball_y < by + BRICK_H) {
                                    bricks[idx] = false
                                    bricks_left = bricks_left - 1
                                    // Was the ball overlapping this brick's
                                    // x-range last frame? If so, it entered
                                    // vertically - reverse vy. Otherwise it
                                    // entered horizontally - reverse vx.
                                    var was_in_x: bool = (prev_x + BALL_SIZE > bx) && (prev_x < bx + BRICK_W)
                                    if (was_in_x) { ball_vy = 0.0 - ball_vy }
                                    else          { ball_vx = 0.0 - ball_vx }
                                    hit = true
                                }
                            }
                        }
                    }
                }
                c = c + 1
            }
            r = r + 1
        }

        // Right paddle collision
        if (ball_x + BALL_SIZE > SCREEN_W - PADDLE_W) {
            if (ball_y + BALL_SIZE >= paddle_y) {
                if (ball_y <= paddle_y + PADDLE_H) {
                    ball_x = SCREEN_W - PADDLE_W - BALL_SIZE
                    ball_vx = 0.0 - ball_vx
                    var hit_pos: f32 = (ball_y + BALL_SIZE * 0.5) - (paddle_y + PADDLE_H * 0.5)
                    ball_vy = ball_vy + hit_pos * 0.05
                }
            }
        }

        // Ball off the right edge → miss; respawn ball center, heading left.
        if (ball_x > SCREEN_W) {
            miss = miss + 1
            print("miss  total:{miss}  cleared waves:{cleared}\n")
            ball_x = SCREEN_W * 0.5; ball_y = SCREEN_H * 0.5
            ball_vx = 0.0 - 4.0;     ball_vy = 3.0
        }

        // All bricks down - reset wave.
        if (bricks_left <= 0) {
            cleared = cleared + 1
            print("WAVE CLEARED  total cleared:{cleared}  misses:{miss}\n")
            var i: i32 = 0
            while (i < BRICK_COUNT) { bricks[i] = true; i = i + 1 }
            bricks_left = BRICK_COUNT
            ball_x = SCREEN_W * 0.5; ball_y = SCREEN_H * 0.5
            ball_vx = 0.0 - 4.0;     ball_vy = 3.0
        }

        // ---- Render ----
        SDL_SetRenderDrawColor(renderer, 12, 14, 22, 255)
        SDL_RenderClear(renderer)

        // Bricks - alternating row tints, simple but readable
        var rr: i32 = 0
        while (rr < BRICK_ROWS) {
            var cc: i32 = 0
            while (cc < BRICK_COLS) {
                var idx2: i32 = rr * BRICK_COLS + cc
                if (bricks[idx2]) {
                    var rect: SDL_FRect
                    rect.x = BRICK_X0 + cc * (BRICK_W + BRICK_GAP)
                    rect.y = BRICK_Y0 + rr * (BRICK_H + BRICK_GAP)
                    rect.w = BRICK_W; rect.h = BRICK_H
                    if (rr % 2 == 0) { SDL_SetRenderDrawColor(renderer, 220, 80, 80, 255) }
                    else             { SDL_SetRenderDrawColor(renderer, 220, 160, 60, 255) }
                    SDL_RenderFillRect(renderer, &rect)
                }
                cc = cc + 1
            }
            rr = rr + 1
        }

        // Paddle + ball in white
        SDL_SetRenderDrawColor(renderer, 220, 220, 230, 255)
        var rp: SDL_FRect; rp.x = SCREEN_W - PADDLE_W; rp.y = paddle_y; rp.w = PADDLE_W; rp.h = PADDLE_H
        var ba: SDL_FRect; ba.x = ball_x;              ba.y = ball_y;   ba.w = BALL_SIZE; ba.h = BALL_SIZE
        SDL_RenderFillRect(renderer, &rp)
        SDL_RenderFillRect(renderer, &ba)

        SDL_RenderPresent(renderer)
        // No SDL_Delay - vsync (set above) paces the loop to monitor refresh.
    }

    if (renderer != null) { SDL_DestroyRenderer(renderer) }
    if (window != null)   { SDL_DestroyWindow(window) }
    SDL_Quit()
}
Cimgui 53_cimgui.gx
// 53_cimgui.gx
//
// The smallest Dear ImGui program: print the version, create a context,
// destroy it. Nothing renders. The point is the build chain. ImGui is
// C++, cimgui wraps it in C, and GX calls the C layer through the cimgui
// module, so if this runs, the whole C++ to C to GX pipeline compiles,
// links against the C++ runtime, and works.

import cimgui

fn main() {
    var ver: cstr = igGetVersion()
    print("imgui version = {ver}\n")

    var ctx: *void = igCreateContext(null)
    if (ctx == null) {
        print("CreateContext FAILED\n")
    } else {
        print("created ImGuiContext OK\n")
        igDestroyContext(ctx)
        print("smoke test PASSED\n")
    }
}
F16 54_f16.gx
// 54_f16.gx
//
// f16 is a 16-bit IEEE-754 float, the half type every shading language
// has. It is a storage type: vertex attributes, weight tables, big buffers
// where 2-byte elements halve the bandwidth. Arithmetic promotes to f32
// and narrows back on store, matching C's usual conversions. The C backend
// lowers it to _Float16 where the compiler has one, falling back to float;
// the LLVM backend always uses a true half.

// f16 globals - the literal is f32, narrowed into the annotated f16.
const HALF_ONE:f16  = 1.0
const HALF_PI:f16   = 3.140625   // nearest binary16 value to pi

// f16 fields pack tightly - a Vertex here is 6 bytes with native f16.
struct Vertex {
    x: f16
    y: f16
    z: f16
}

// f16 arguments and return type. Arithmetic on f16 promotes to f32
// (matching C's usual conversions), and narrows back on the typed return.
fn midpoint:f16(a: f16, b: f16) {
    var m: f16 = (a + b) * 0.5
    return m
}

fn main() {
    print("=== f16 / half precision ===\n")

    var h: f16 = 2.5
    print("h         = {h}\n")
    print("HALF_ONE  = {HALF_ONE}\n")
    print("HALF_PI   = {HALF_PI}\n")

    // f16 arithmetic - the result of a + b is f32, stored back into f16.
    var a: f16 = 1.0
    var b: f16 = 4.0
    var mid: f16 = midpoint(a, b)
    print("midpoint(1.0, 4.0) = {mid}\n")

    // f16 in a struct, ready to hand to a GPU vertex buffer.
    var v: Vertex
    v.x = 0.5
    v.y = -0.5
    v.z = 1.0
    print("Vertex = ({v.x}, {v.y}, {v.z})\n")

    // f16 fixed-size array - a tightly packed weight table.
    var weights: f16[4]
    weights[0] = 0.1
    weights[1] = 0.2
    weights[2] = 0.3
    weights[3] = 0.4
    var sum: f16 = 0.0
    for (i = 0:3) {
        sum = sum + weights[i]
    }
    print("weight sum = {sum}\n")
}
Test Math test_math.gx
// test_math.ec - Verify vec and mat math modules compile and produce correct results

import math.scalar
import math.vec
import math.mat

fn main() {
    // --- scalar tests ---
    print("=== Scalar ===\n");
    var s:f32 = sinf(PI() / 2.0);
    print("sin(PI/2) = {s}\n");

    var c:f32 = cosf(0.0);
    print("cos(0) = {c}\n");

    var sq:f32 = sqrtf(9.0);
    print("sqrt(9) = {sq}\n");
    var sw_pos:vec3 = {1,2,3};
    sw_pos.xy += 10;
    print("sw_pos after .xy += 10: {sw_pos}\n");
    var cl:f32 = clamp_f(5.0, 0.0, 1.0);
    print("clamp(5, 0, 1) = {cl}\n");

    var lr:f32 = lerp_f(0.0, 10.0, 0.25);
    print("lerp(0, 10, 0.25) = {lr}\n");

    var rad:f32 = radians(180.0);
    print("radians(180) = {rad}\n");

    // --- vec3 tests ---
    print("\n=== Vec3 ===\n");
    var a:vec3;
    a.x = 3.0;
    a.y = 0.0;
    a.z = 4.0;

    var len:f32 = length_v3(a);
    print("length(3,0,4) = {len}\n");

    var n:vec3 = normalize_v3(a);
    print("normalize(3,0,4) = {n}\n");

    var b:vec3;
    b.x = 1.0;
    b.y = 2.0;
    b.z = 3.0;

    var d:f32 = distance_v3(a, b);
    print("distance = {d}\n");

    var mid:vec3 = lerp_v3(a, b, 0.5);
    print("lerp(a, b, 0.5) = {mid}\n");

    // --- vec2 tests ---
    print("\n=== Vec2 ===\n");
    var v2:vec2;
    v2.x = 3.0;
    v2.y = 4.0;
    var len2:f32 = length_v2(v2);
    print("length(3,4) = {len2}\n");

    var n2:vec2 = normalize_v2(v2);
    print("normalize(3,4) = {n2}\n");

    // --- mat4 tests ---
    print("\n=== Mat4 ===\n");
    var id:mat4 = identity_mat4();
    print("identity = {id}\n");

    // Test translation
    var pos:vec3;
    pos.x = 10.0;
    pos.y = 20.0;
    pos.z = 30.0;
    var tm:mat4 = translation(pos);
    var tc:vec4 = tm.col[3];
    print("translation(10,20,30) col3 = {tc}\n");

    // Test mul_m4 (identity * translation = translation)
    var result:mat4 = mul_m4(id, tm);
    var rc:vec4 = result.col[3];
    print("I * T col3 = {rc}\n");

    // Test mul_m4v4
    var v:vec4;
    v.x = 1.0;
    v.y = 0.0;
    v.z = 0.0;
    v.w = 1.0;
    var tv:vec4 = mul_m4v4(tm, v);
    print("T * (1,0,0,1) = {tv}\n");

    // Test perspective
    var proj:mat4 = perspective(radians(60.0), 1.333, 0.1, 100.0);
    var p00:f32 = proj.col[0].x;
    print("perspective[0][0] = {p00}\n");

    // Test look_at
    var eye:vec3;
    eye.x = 0.0;
    eye.y = 0.0;
    eye.z = 5.0;
    var target:vec3;
    target.x = 0.0;
    target.y = 0.0;
    target.z = 0.0;
    var up:vec3;
    up.x = 0.0;
    up.y = 1.0;
    up.z = 0.0;
    var view:mat4 = look_at(eye, target, up);
    var vc:vec4 = view.col[3];
    print("look_at col3 = {vc}\n");

    print("\nAll math tests done.\n");
}

Graphics — Raylib

Ball Physics raylib/ball_physics.gx
// ball_physics.gx - Raylib ball physics sandbox
// Port of raylib [shapes] example - ball physics
// Build: gx ball_physics.gx -I modules -o ball_physics.exe
// Web:   gx ball_physics.gx -I modules --target web -o ball_physics.html
//
// Controls:
//   Left click (on ball)   — grab and throw
//   Right click            — create new random ball at cursor
//   Left Ctrl + Right hold — spawn many balls
//   Middle click           — shake all balls
//   Mouse wheel            — adjust gravity

import raylib

const MAX_BALLS = 200
const SCREEN_W = 800
const SCREEN_H = 600

struct Ball {
    position: Vector2
    speed: Vector2
    prev_position: Vector2
    radius: f32
    friction: f32
    elasticity: f32
    color: Color
    grabbed: bool
}

// Global ball pool (fixed size, no dynamic allocation)
var g_balls: Ball[200]
var g_ball_count: i32 = 0
var g_grabbed_idx: i32 = 0 - 1   // -1 = none grabbed
var g_press_offset_x: f32 = 0.0
var g_press_offset_y: f32 = 0.0

fn spawn_ball(px: f32, py: f32, vx: f32, vy: f32, radius: f32, r: u8, g: u8, b: u8) {
    if (g_ball_count >= MAX_BALLS) { return }
    var i: i32 = g_ball_count
    g_balls[i].position.x = px
    g_balls[i].position.y = py
    g_balls[i].speed.x = vx
    g_balls[i].speed.y = vy
    g_balls[i].prev_position.x = px
    g_balls[i].prev_position.y = py
    g_balls[i].radius = radius
    g_balls[i].friction = 0.985
    g_balls[i].elasticity = 0.75
    g_balls[i].color.r = r
    g_balls[i].color.g = g
    g_balls[i].color.b = b
    g_balls[i].color.a = 255
    g_balls[i].grabbed = false
    g_ball_count = g_ball_count + 1
}

fn main() {
    InitWindow(SCREEN_W, SCREEN_H, "raylib [shapes] example - ball physics")
    SetTargetFPS(60)

    // Initial ball: big blue one in the center
    spawn_ball(SCREEN_W * 0.5, SCREEN_H * 0.5, 200.0, 200.0, 40.0, 0, 121, 241)

    var gravity: f32 = 100.0
    var sw: f32 = SCREEN_W * 1.0
    var sh: f32 = SCREEN_H * 1.0

    while (!WindowShouldClose()) {
        var delta: f32 = GetFrameTime()
        var mouse: Vector2 = GetMousePosition()

        // ---- Grab a ball on left-click ----
        if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
            var i: i32 = g_ball_count - 1
            while (i >= 0) {
                var dx: f32 = mouse.x - g_balls[i].position.x
                var dy: f32 = mouse.y - g_balls[i].position.y
                var dist_sq: f32 = dx * dx + dy * dy
                var r_sq: f32 = g_balls[i].radius * g_balls[i].radius
                if (dist_sq <= r_sq) {
                    g_balls[i].grabbed = true
                    g_grabbed_idx = i
                    g_press_offset_x = dx
                    g_press_offset_y = dy
                    i = 0 - 1    // break
                }
                i = i - 1
            }
        }

        // ---- Release grabbed ball ----
        if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT)) {
            if (g_grabbed_idx >= 0) {
                g_balls[g_grabbed_idx].grabbed = false
                g_grabbed_idx = 0 - 1
            }
        }

        // ---- Spawn new random ball on right-click ----
        var spawn_new: bool = false
        if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT)) { spawn_new = true }
        if (IsKeyDown(KEY_LEFT_CONTROL)) {
            if (IsMouseButtonDown(MOUSE_BUTTON_RIGHT)) { spawn_new = true }
        }
        if (spawn_new) {
            var vx: f32 = (f32)GetRandomValue(0 - 300, 300)
            var vy: f32 = (f32)GetRandomValue(0 - 300, 300)
            var rad: f32 = 20.0 + (f32)GetRandomValue(0, 30)
            var cr: c_int = GetRandomValue(0, 255)
            var cg: c_int = GetRandomValue(0, 255)
            var cb: c_int = GetRandomValue(0, 255)
            spawn_ball(mouse.x, mouse.y, vx, vy, rad, (u8)cr, (u8)cg, (u8)cb)
        }

        // ---- Shake all balls on middle-click ----
        if (IsMouseButtonPressed(MOUSE_BUTTON_MIDDLE)) {
            for (i = 0 : g_ball_count - 1) {
                if (!g_balls[i].grabbed) {
                    g_balls[i].speed.x = (f32)GetRandomValue(0 - 2000, 2000)
                    g_balls[i].speed.y = (f32)GetRandomValue(0 - 2000, 2000)
                }
            }
        }

        // ---- Adjust gravity with mouse wheel ----
        gravity = gravity + GetMouseWheelMove() * 5.0

        // ---- Update ball physics ----
        for (i = 0 : g_ball_count - 1) {
            if (!g_balls[i].grabbed) {
                g_balls[i].position.x = g_balls[i].position.x + g_balls[i].speed.x * delta
                g_balls[i].position.y = g_balls[i].position.y + g_balls[i].speed.y * delta

                // Wall collision: right
                if ((g_balls[i].position.x + g_balls[i].radius) >= sw) {
                    g_balls[i].position.x = sw - g_balls[i].radius
                    g_balls[i].speed.x = 0.0 - g_balls[i].speed.x * g_balls[i].elasticity
                }
                // Wall collision: left
                if ((g_balls[i].position.x - g_balls[i].radius) <= 0.0) {
                    g_balls[i].position.x = g_balls[i].radius
                    g_balls[i].speed.x = 0.0 - g_balls[i].speed.x * g_balls[i].elasticity
                }
                // Wall collision: bottom
                if ((g_balls[i].position.y + g_balls[i].radius) >= sh) {
                    g_balls[i].position.y = sh - g_balls[i].radius
                    g_balls[i].speed.y = 0.0 - g_balls[i].speed.y * g_balls[i].elasticity
                }
                // Wall collision: top
                if ((g_balls[i].position.y - g_balls[i].radius) <= 0.0) {
                    g_balls[i].position.y = g_balls[i].radius
                    g_balls[i].speed.y = 0.0 - g_balls[i].speed.y * g_balls[i].elasticity
                }

                // Friction + gravity
                g_balls[i].speed.x = g_balls[i].speed.x * g_balls[i].friction
                g_balls[i].speed.y = g_balls[i].speed.y * g_balls[i].friction + gravity
            } else {
                // Ball is being dragged
                var new_x: f32 = mouse.x - g_press_offset_x
                var new_y: f32 = mouse.y - g_press_offset_y
                g_balls[i].speed.x = (new_x - g_balls[i].prev_position.x) / delta
                g_balls[i].speed.y = (new_y - g_balls[i].prev_position.y) / delta
                g_balls[i].prev_position.x = new_x
                g_balls[i].prev_position.y = new_y
                g_balls[i].position.x = new_x
                g_balls[i].position.y = new_y
            }
        }

        // ---- Ball-to-ball collision (O(n^2) pairwise) ----
        // Uses equal-mass elastic collision along the collision normal.
        // Mass is approximated as radius (bigger balls push harder).
        for (a = 0 : g_ball_count - 1) {
            for (b = a + 1 : g_ball_count - 1) {
                var dx: f32 = g_balls[b].position.x - g_balls[a].position.x
                var dy: f32 = g_balls[b].position.y - g_balls[a].position.y
                var min_dist: f32 = g_balls[a].radius + g_balls[b].radius
                var dist_sq: f32 = dx * dx + dy * dy
                var min_sq: f32 = min_dist * min_dist

                if (dist_sq < min_sq) {
                    if (dist_sq > 0.0001) {
                        // Collision normal (unit vector from a to b)
                        // sqrt approximation: Newton's method would be more accurate,
                        // but for stability we use inverse sqrt via division.
                        var dist: f32 = dist_sq
                        // Fast approximate sqrt — just use raylib-provided one via math
                        // Simple approach: iterative refinement
                        var guess: f32 = min_dist
                        guess = (guess + dist / guess) * 0.5
                        guess = (guess + dist / guess) * 0.5
                        guess = (guess + dist / guess) * 0.5
                        dist = guess

                        var nx: f32 = dx / dist
                        var ny: f32 = dy / dist

                        // Separate overlapping balls (split the overlap proportionally)
                        var overlap: f32 = (min_dist - dist) * 0.5
                        if (!g_balls[a].grabbed) {
                            g_balls[a].position.x = g_balls[a].position.x - nx * overlap
                            g_balls[a].position.y = g_balls[a].position.y - ny * overlap
                        }
                        if (!g_balls[b].grabbed) {
                            g_balls[b].position.x = g_balls[b].position.x + nx * overlap
                            g_balls[b].position.y = g_balls[b].position.y + ny * overlap
                        }

                        // Relative velocity along the normal
                        var dvx: f32 = g_balls[b].speed.x - g_balls[a].speed.x
                        var dvy: f32 = g_balls[b].speed.y - g_balls[a].speed.y
                        var vel_along: f32 = dvx * nx + dvy * ny

                        // Only resolve if balls are moving toward each other
                        if (vel_along < 0.0) {
                            // Elastic collision with mass = radius
                            var ma: f32 = g_balls[a].radius
                            var mb: f32 = g_balls[b].radius
                            var e: f32 = 0.55   // elasticity (lower = more damping)
                            var j: f32 = (0.0 - (1.0 + e)) * vel_along / (1.0 / ma + 1.0 / mb)
                            var impulse_x: f32 = j * nx
                            var impulse_y: f32 = j * ny

                            if (!g_balls[a].grabbed) {
                                g_balls[a].speed.x = g_balls[a].speed.x - impulse_x / ma
                                g_balls[a].speed.y = g_balls[a].speed.y - impulse_y / ma
                            }
                            if (!g_balls[b].grabbed) {
                                g_balls[b].speed.x = g_balls[b].speed.x + impulse_x / mb
                                g_balls[b].speed.y = g_balls[b].speed.y + impulse_y / mb
                            }
                        }
                    }
                }
            }
        }

        // ---- Rest clamp: kill tiny velocities to let balls settle ----
        // Without this, balls near the floor vibrate forever due to gravity
        // fighting the elastic response.
        for (k = 0 : g_ball_count - 1) {
            if (!g_balls[k].grabbed) {
                var vx: f32 = g_balls[k].speed.x
                var vy: f32 = g_balls[k].speed.y
                var speed_sq: f32 = vx * vx + vy * vy

                // If ball is touching the floor and moving slowly, clamp to rest
                var on_floor: bool = (g_balls[k].position.y + g_balls[k].radius) >= (sh - 1.0)
                if (on_floor) {
                    if (speed_sq < 400.0) {
                        g_balls[k].speed.x = vx * 0.8    // horizontal damping
                        g_balls[k].speed.y = 0.0         // kill vertical bounce
                    }
                }
                // Also kill horizontal micro-movement to prevent sliding forever
                if (vx < 1.5) {
                    if (vx > 0.0 - 1.5) {
                        g_balls[k].speed.x = 0.0
                    }
                }
            }
        }

        // ---- Draw ----
        BeginDrawing()
        ClearBackground(RAYWHITE)

        for (i = 0 : g_ball_count - 1) {
            DrawCircleV(g_balls[i].position, g_balls[i].radius, g_balls[i].color)
            DrawCircleLinesV(g_balls[i].position, g_balls[i].radius, BLACK)
        }

        DrawText("Left-click + drag a ball to grab and throw", 10, 10, 14, DARKGRAY)
        DrawText("Right-click to spawn (hold Left Ctrl for rapid-fire)", 10, 30, 14, DARKGRAY)
        DrawText("Mouse wheel: gravity    Middle-click: shake", 10, 50, 14, DARKGRAY)

        var count = g_ball_count
        var grav_i = (i32)gravity
        var info1 = "BALL COUNT: {count}"
        var info2 = "GRAVITY: {grav_i}"
        DrawText(info1.cstr, 10, SCREEN_H - 60, 20, BLACK)
        DrawText(info2.cstr, 10, SCREEN_H - 30, 20, BLACK)

        EndDrawing()
    }

    CloseWindow()
}
Boids raylib/boids.gx
// boids.gx - Classic boids flocking simulation
// Based on Craig Reynolds' 1987 "Flocks, Herds, and Schools" paper.
// Three steering rules:
//   1. Separation  — avoid crowding local flockmates
//   2. Alignment   — steer toward average heading of neighbors
//   3. Cohesion    — move toward average position of neighbors
//
// Build: gx boids.gx -I modules -o boids.exe
// Web:   gx boids.gx -I modules --target web -o boids.html
//
// Controls:
//   Left click     — attract boids to cursor
//   Right click    — scatter boids from cursor
//   Mouse wheel    — adjust boid count (adds/removes 10)

import raylib
import math.scalar

const MAX_BOIDS = 300
const SCREEN_W = 800
const SCREEN_H = 600

struct Boid {
    position: Vector2
    velocity: Vector2
    color: Color
    sprint_timer: f32   // >0 = sprinting at 2x speed (escaping predator)
}

struct Predator {
    position: Vector2
    velocity: Vector2
    attack_timer: f32   // >0 = chasing (increased speed)
    scan_timer: f32     // countdown between prey-scans
    active: bool
}

var g_boids: Boid[300]
var g_boid_count: i32 = 0
var g_predator: Predator
var g_spawn_timer: f32 = 0.0   // counts up until next predator spawn

// Flocking parameters
const VIEW_RADIUS: f32 = 60.0
const SEPARATION_RADIUS: f32 = 25.0
const MAX_SPEED: f32 = 90.0
const MAX_FORCE: f32 = 3.5

const SEP_WEIGHT: f32 = 1.8
const ALI_WEIGHT: f32 = 1.0
const COH_WEIGHT: f32 = 0.8

// Predator parameters
const PRED_SIZE: f32 = 21.0              // 3x the boid triangle size (7)
const PRED_CRUISE_SPEED: f32 = 45.0      // slow patrol speed
const PRED_ATTACK_SPEED: f32 = 150.0     // sprint when hunting
const PRED_DETECT_RADIUS: f32 = 140.0    // prey detection range
const PRED_FLEE_RADIUS: f32 = 120.0      // how close predator is before prey flee
const PRED_SPAWN_INTERVAL: f32 = 12.0    // seconds between spawns

// --- Simple LCG PRNG ---
var g_rng: i32 = 12345

fn rand_next:i32() {
    g_rng = g_rng * 1103515245 + 12345
    var v: i32 = (g_rng / 65536) & 32767
    return v
}

fn rand_range_f:f32(lo: f32, hi: f32) {
    var v: f32 = (rand_next() % 10000) / 10000.0
    return lo + v * (hi - lo)
}

fn rand_range_i:i32(lo: i32, hi: i32) {
    var range: i32 = hi - lo + 1
    return lo + (rand_next() % range)
}

fn spawn_boid() {
    if (g_boid_count >= MAX_BOIDS) { return }
    var i: i32 = g_boid_count
    g_boids[i].position.x = rand_range_f(0.0, SCREEN_W * 1.0)
    g_boids[i].position.y = rand_range_f(0.0, SCREEN_H * 1.0)
    g_boids[i].velocity.x = rand_range_f(0.0 - MAX_SPEED, MAX_SPEED)
    g_boids[i].velocity.y = rand_range_f(0.0 - MAX_SPEED, MAX_SPEED)

    // Color gradient based on position — cool blues/purples
    var hue: f32 = rand_range_f(140.0, 255.0)
    g_boids[i].color.r = (u8)(40 + rand_range_i(0, 60))
    g_boids[i].color.g = (u8)(120 + rand_range_i(0, 80))
    g_boids[i].color.b = (u8)hue
    g_boids[i].color.a = 255

    g_boid_count = g_boid_count + 1
}

// Clamp a velocity magnitude in-place (via ball index since GX doesn't deref pointers easily)
fn limit_boid_speed(i: i32, max: f32) {
    var vx: f32 = g_boids[i].velocity.x
    var vy: f32 = g_boids[i].velocity.y
    var mag_sq: f32 = vx * vx + vy * vy
    var max_sq: f32 = max * max
    if (mag_sq > max_sq) {
        var mag: f32 = sqrtf(mag_sq)
        g_boids[i].velocity.x = (vx / mag) * max
        g_boids[i].velocity.y = (vy / mag) * max
    }
}

// Spawn a predator from a random edge, heading toward screen center
fn spawn_predator() {
    var edge: i32 = rand_next() % 4
    var cx: f32 = SCREEN_W * 0.5
    var cy: f32 = SCREEN_H * 0.5

    if (edge == 0) {
        // Top edge
        g_predator.position.x = rand_range_f(0.0, SCREEN_W * 1.0)
        g_predator.position.y = 0.0 - PRED_SIZE
    }
    if (edge == 1) {
        // Right edge
        g_predator.position.x = SCREEN_W + PRED_SIZE
        g_predator.position.y = rand_range_f(0.0, SCREEN_H * 1.0)
    }
    if (edge == 2) {
        // Bottom edge
        g_predator.position.x = rand_range_f(0.0, SCREEN_W * 1.0)
        g_predator.position.y = SCREEN_H + PRED_SIZE
    }
    if (edge == 3) {
        // Left edge
        g_predator.position.x = 0.0 - PRED_SIZE
        g_predator.position.y = rand_range_f(0.0, SCREEN_H * 1.0)
    }

    // Initial heading: toward screen center
    var dx: f32 = cx - g_predator.position.x
    var dy: f32 = cy - g_predator.position.y
    var mag: f32 = sqrtf(dx * dx + dy * dy)
    if (mag < 0.001) { mag = 0.001 }
    g_predator.velocity.x = (dx / mag) * PRED_CRUISE_SPEED
    g_predator.velocity.y = (dy / mag) * PRED_CRUISE_SPEED

    g_predator.attack_timer = 0.0
    g_predator.scan_timer = 2.0
    g_predator.active = true
}

fn main() {
    InitWindow(SCREEN_W, SCREEN_H, "GX + Raylib - Classic Boids")
    SetTargetFPS(60)

    // Spawn initial flock
    for (i = 0 : 99) {
        spawn_boid()
    }

    // Predator starts inactive; will spawn after interval
    g_predator.active = false

    var bg: Color
    bg.r = 12
    bg.g = 18
    bg.b = 32
    bg.a = 255

    while (!WindowShouldClose()) {
        var delta: f32 = GetFrameTime()
        var mouse: Vector2 = GetMousePosition()
        var mouse_attract: bool = IsMouseButtonDown(MOUSE_BUTTON_LEFT)
        var mouse_repel: bool = IsMouseButtonDown(MOUSE_BUTTON_RIGHT)

        // Adjust count with mouse wheel
        var wheel: f32 = GetMouseWheelMove()
        if (wheel > 0.0) {
            for (k = 0 : 9) { spawn_boid() }
        }
        if (wheel < 0.0) {
            if (g_boid_count >= 10) { g_boid_count = g_boid_count - 10 }
        }

        // ---- Predator spawn timer ----
        if (!g_predator.active) {
            g_spawn_timer = g_spawn_timer + delta
            if (g_spawn_timer >= PRED_SPAWN_INTERVAL) {
                g_spawn_timer = 0.0
                spawn_predator()
            }
        }

        // ---- Predator update ----
        if (g_predator.active) {
            // Periodic target scan — enter attack mode if prey is close enough
            g_predator.scan_timer = g_predator.scan_timer - delta
            if (g_predator.scan_timer <= 0.0) {
                g_predator.scan_timer = 3.0  // scan every 3 seconds

                // Find the nearest boid within detection radius
                var best_idx: i32 = 0 - 1
                var best_dist_sq: f32 = PRED_DETECT_RADIUS * PRED_DETECT_RADIUS
                for (i = 0 : g_boid_count - 1) {
                    var bdx: f32 = g_boids[i].position.x - g_predator.position.x
                    var bdy: f32 = g_boids[i].position.y - g_predator.position.y
                    var bsq: f32 = bdx * bdx + bdy * bdy
                    if (bsq < best_dist_sq) {
                        best_dist_sq = bsq
                        best_idx = i
                    }
                }
                if (best_idx >= 0) {
                    // Attack mode for 2.5 seconds — steer toward target
                    g_predator.attack_timer = 2.5
                    var tdx: f32 = g_boids[best_idx].position.x - g_predator.position.x
                    var tdy: f32 = g_boids[best_idx].position.y - g_predator.position.y
                    var tmag: f32 = sqrtf(tdx * tdx + tdy * tdy)
                    if (tmag < 0.001) { tmag = 0.001 }
                    g_predator.velocity.x = (tdx / tmag) * PRED_ATTACK_SPEED
                    g_predator.velocity.y = (tdy / tmag) * PRED_ATTACK_SPEED
                }
            }

            // Decay attack timer and slow back to cruise when it expires
            if (g_predator.attack_timer > 0.0) {
                g_predator.attack_timer = g_predator.attack_timer - delta
                if (g_predator.attack_timer <= 0.0) {
                    // Return to cruise speed
                    var cvx: f32 = g_predator.velocity.x
                    var cvy: f32 = g_predator.velocity.y
                    var cmag: f32 = sqrtf(cvx * cvx + cvy * cvy)
                    if (cmag > 0.001) {
                        g_predator.velocity.x = (cvx / cmag) * PRED_CRUISE_SPEED
                        g_predator.velocity.y = (cvy / cmag) * PRED_CRUISE_SPEED
                    }
                }
            }

            // Move predator
            g_predator.position.x = g_predator.position.x + g_predator.velocity.x * delta
            g_predator.position.y = g_predator.position.y + g_predator.velocity.y * delta

            // Kill predator when it exits the screen
            var off: f32 = PRED_SIZE * 2.0
            if (g_predator.position.x < (0.0 - off)) { g_predator.active = false }
            if (g_predator.position.x > (SCREEN_W + off)) { g_predator.active = false }
            if (g_predator.position.y < (0.0 - off)) { g_predator.active = false }
            if (g_predator.position.y > (SCREEN_H + off)) { g_predator.active = false }
        }

        // ---- Boid flocking update ----
        for (i = 0 : g_boid_count - 1) {
            // Accumulators for the three rules
            var sep_x: f32 = 0.0
            var sep_y: f32 = 0.0
            var ali_x: f32 = 0.0
            var ali_y: f32 = 0.0
            var coh_x: f32 = 0.0
            var coh_y: f32 = 0.0
            var neighbors: i32 = 0
            var sep_count: i32 = 0

            // Scan all other boids
            for (j = 0 : g_boid_count - 1) {
                if (i != j) {
                    var dx: f32 = g_boids[j].position.x - g_boids[i].position.x
                    var dy: f32 = g_boids[j].position.y - g_boids[i].position.y
                    var dist_sq: f32 = dx * dx + dy * dy

                    if (dist_sq < VIEW_RADIUS * VIEW_RADIUS) {
                        if (dist_sq > 0.01) {
                            // Alignment & cohesion: all boids in view radius
                            ali_x = ali_x + g_boids[j].velocity.x
                            ali_y = ali_y + g_boids[j].velocity.y
                            coh_x = coh_x + g_boids[j].position.x
                            coh_y = coh_y + g_boids[j].position.y
                            neighbors = neighbors + 1

                            // Separation: only very close boids
                            if (dist_sq < SEPARATION_RADIUS * SEPARATION_RADIUS) {
                                // Push away, weighted by inverse distance
                                var dist: f32 = sqrtf(dist_sq)
                                sep_x = sep_x - (dx / dist) / dist
                                sep_y = sep_y - (dy / dist) / dist
                                sep_count = sep_count + 1
                            }
                        }
                    }
                }
            }

            // Compute steering forces
            var steer_x: f32 = 0.0
            var steer_y: f32 = 0.0

            if (sep_count > 0) {
                steer_x = steer_x + sep_x * SEP_WEIGHT * 200.0
                steer_y = steer_y + sep_y * SEP_WEIGHT * 200.0
            }

            if (neighbors > 0) {
                var inv_n: f32 = 1.0 / (neighbors * 1.0)
                // Alignment: steer toward average velocity of neighbors
                var avg_vx: f32 = ali_x * inv_n
                var avg_vy: f32 = ali_y * inv_n
                steer_x = steer_x + (avg_vx - g_boids[i].velocity.x) * ALI_WEIGHT * 0.05
                steer_y = steer_y + (avg_vy - g_boids[i].velocity.y) * ALI_WEIGHT * 0.05

                // Cohesion: steer toward average position of neighbors
                var center_x: f32 = coh_x * inv_n
                var center_y: f32 = coh_y * inv_n
                steer_x = steer_x + (center_x - g_boids[i].position.x) * COH_WEIGHT * 0.02
                steer_y = steer_y + (center_y - g_boids[i].position.y) * COH_WEIGHT * 0.02
            }

            // Mouse interaction
            if (mouse_attract) {
                var mdx: f32 = mouse.x - g_boids[i].position.x
                var mdy: f32 = mouse.y - g_boids[i].position.y
                steer_x = steer_x + mdx * 0.8
                steer_y = steer_y + mdy * 0.8
            }
            if (mouse_repel) {
                var mdx: f32 = g_boids[i].position.x - mouse.x
                var mdy: f32 = g_boids[i].position.y - mouse.y
                var mdist_sq: f32 = mdx * mdx + mdy * mdy
                if (mdist_sq < 40000.0) {
                    steer_x = steer_x + mdx * 3.0
                    steer_y = steer_y + mdy * 3.0
                }
            }

            // Predator flee: if predator is active and close, steer away hard + sprint
            if (g_predator.active) {
                var pdx: f32 = g_boids[i].position.x - g_predator.position.x
                var pdy: f32 = g_boids[i].position.y - g_predator.position.y
                var pdist_sq: f32 = pdx * pdx + pdy * pdy
                var flee_sq: f32 = PRED_FLEE_RADIUS * PRED_FLEE_RADIUS
                if (pdist_sq < flee_sq) {
                    // Stronger flee the closer the predator is
                    var pdist: f32 = sqrtf(pdist_sq)
                    if (pdist < 0.001) { pdist = 0.001 }
                    var flee_force: f32 = (PRED_FLEE_RADIUS - pdist) * 4.0
                    steer_x = steer_x + (pdx / pdist) * flee_force
                    steer_y = steer_y + (pdy / pdist) * flee_force
                    // Activate sprint for 1.2 seconds
                    g_boids[i].sprint_timer = 1.2
                }
            }

            // Decay sprint timer
            if (g_boids[i].sprint_timer > 0.0) {
                g_boids[i].sprint_timer = g_boids[i].sprint_timer - delta
            }

            // Apply steering force
            g_boids[i].velocity.x = g_boids[i].velocity.x + steer_x * delta
            g_boids[i].velocity.y = g_boids[i].velocity.y + steer_y * delta

            // Limit speed (2x while sprinting to escape predator)
            var speed_cap: f32 = MAX_SPEED
            if (g_boids[i].sprint_timer > 0.0) {
                speed_cap = MAX_SPEED * 2.0
            }
            limit_boid_speed(i, speed_cap)

            // Update position
            g_boids[i].position.x = g_boids[i].position.x + g_boids[i].velocity.x * delta
            g_boids[i].position.y = g_boids[i].position.y + g_boids[i].velocity.y * delta

            // Wrap around screen edges
            if (g_boids[i].position.x < 0.0) { g_boids[i].position.x = g_boids[i].position.x + SCREEN_W }
            if (g_boids[i].position.x > SCREEN_W) { g_boids[i].position.x = g_boids[i].position.x - SCREEN_W }
            if (g_boids[i].position.y < 0.0) { g_boids[i].position.y = g_boids[i].position.y + SCREEN_H }
            if (g_boids[i].position.y > SCREEN_H) { g_boids[i].position.y = g_boids[i].position.y - SCREEN_H }
        }

        // ---- Draw ----
        BeginDrawing()
        ClearBackground(bg)

        // Draw each boid as a triangle pointing in its velocity direction
        for (i = 0 : g_boid_count - 1) {
            var vx: f32 = g_boids[i].velocity.x
            var vy: f32 = g_boids[i].velocity.y
            var vmag: f32 = sqrtf(vx * vx + vy * vy)
            if (vmag < 0.001) { vmag = 0.001 }

            // Unit direction
            var dx: f32 = vx / vmag
            var dy: f32 = vy / vmag
            // Perpendicular
            var px: f32 = 0.0 - dy
            var py: f32 = dx

            var cx: f32 = g_boids[i].position.x
            var cy: f32 = g_boids[i].position.y
            var size: f32 = 7.0

            // Triangle: tip forward, two base points behind
            // Winding MUST be counter-clockwise for raylib (otherwise back-face culled)
            var p1: Vector2
            p1.x = cx + dx * size
            p1.y = cy + dy * size
            // Back-right (CCW from tip in screen space where y goes down)
            var p2: Vector2
            p2.x = cx - dx * size - px * size * 0.6
            p2.y = cy - dy * size - py * size * 0.6
            // Back-left
            var p3: Vector2
            p3.x = cx - dx * size + px * size * 0.6
            p3.y = cy - dy * size + py * size * 0.6

            DrawTriangle(p1, p2, p3, g_boids[i].color)
        }

        // Draw predator (big red triangle)
        if (g_predator.active) {
            var pvx: f32 = g_predator.velocity.x
            var pvy: f32 = g_predator.velocity.y
            var pvmag: f32 = sqrtf(pvx * pvx + pvy * pvy)
            if (pvmag < 0.001) { pvmag = 0.001 }
            var pdx: f32 = pvx / pvmag
            var pdy: f32 = pvy / pvmag
            var ppx: f32 = 0.0 - pdy
            var ppy: f32 = pdx
            var pcx: f32 = g_predator.position.x
            var pcy: f32 = g_predator.position.y
            var psize: f32 = PRED_SIZE

            var pred_color: Color
            if (g_predator.attack_timer > 0.0) {
                // Brighter red while attacking
                pred_color.r = 255
                pred_color.g = 40
                pred_color.b = 40
            } else {
                pred_color.r = 200
                pred_color.g = 30
                pred_color.b = 30
            }
            pred_color.a = 255

            var pp1: Vector2
            pp1.x = pcx + pdx * psize
            pp1.y = pcy + pdy * psize
            var pp2: Vector2
            pp2.x = pcx - pdx * psize - ppx * psize * 0.6
            pp2.y = pcy - pdy * psize - ppy * psize * 0.6
            var pp3: Vector2
            pp3.x = pcx - pdx * psize + ppx * psize * 0.6
            pp3.y = pcy - pdy * psize + ppy * psize * 0.6
            DrawTriangle(pp1, pp2, pp3, pred_color)
        }

        DrawText("Left-click: attract    Right-click: scatter    Wheel: +/- 10 boids", 10, 10, 14, RAYWHITE)
        var count = g_boid_count
        var info = "BOIDS: {count}"
        DrawText(info.cstr, 10, SCREEN_H - 30, 20, RAYWHITE)
        DrawFPS(SCREEN_W - 90, 10)

        EndDrawing()
    }

    CloseWindow()
}
Bullet Hell raylib/bullet_hell.gx
// bullet_hell.gx - Raylib bullet hell shmup demo
// Port of raylib [shapes] example - bullet hell
// Build: gx bullet_hell.gx -I modules -o bullet_hell.exe
// Web:   gx bullet_hell.gx -I modules --target web -o bullet_hell.html
//
// Controls:
//   Right/Left or A/D  — Change number of bullet rows
//   Up/Down or W/S     — Change bullet speed
//   Z / X              — Change spawn cooldown
//   Space (hold)       — Change angle increment per frame
//   Enter              — Switch draw method (texture vs DrawCircle)
//   C                  — Clear bullets

import raylib
import math.scalar

const MAX_BULLETS = 100000

struct Bullet {
    position: Vector2
    acceleration: Vector2
    disabled: bool
    color: Color
}

// Global bullet pool — fixed size array (no dynamic allocation)
var g_bullets: Bullet[100000]

fn main() {
    var screen_width: i32 = 800
    var screen_height: i32 = 600

    InitWindow(screen_width, screen_height, "raylib [shapes] example - bullet hell")

    var bullet_count: i32 = 0
    var bullet_disabled_count: i32 = 0
    var bullet_radius: i32 = 10
    var bullet_speed: f32 = 3.0
    var bullet_rows: i32 = 6

    // Spawner state
    var base_direction: f32 = 0.0
    var angle_increment: i32 = 5
    var spawn_cooldown: f32 = 2.0
    var spawn_cooldown_timer: f32 = 2.0

    // Magic circle animation
    var magic_circle_rotation: f32 = 0.0

    // Pre-rendered bullet texture for performance
    var bullet_texture = LoadRenderTexture(24, 24)
    BeginTextureMode(bullet_texture)
        DrawCircle(12, 12, 10.0, WHITE)
        DrawCircleLines(12, 12, 10.0, BLACK)
    EndTextureMode()

    var draw_perf_mode: bool = true

    // Colors array (can't easily use const arrays in GX, use locals)
    var color_a = RED
    var color_b = BLUE

    SetTargetFPS(60)

    while (!WindowShouldClose()) {
        // ----- Update -----

        // Reset ring buffer when full
        if (bullet_count >= MAX_BULLETS) {
            bullet_count = 0
            bullet_disabled_count = 0
        }

        spawn_cooldown_timer = spawn_cooldown_timer - 1.0
        if (spawn_cooldown_timer < 0.0) {
            spawn_cooldown_timer = spawn_cooldown

            // Spawn one bullet per row, distributed around 360 degrees
            var degrees_per_row: f32 = 360.0 / (bullet_rows * 1.0)
            var row: i32 = 0
            while (row < bullet_rows) {
                if (bullet_count < MAX_BULLETS) {
                    var pos: Vector2
                    pos.x = screen_width * 0.5
                    pos.y = screen_height * 0.5
                    g_bullets[bullet_count].position = pos
                    g_bullets[bullet_count].disabled = false

                    if ((row % 2) == 0) {
                        g_bullets[bullet_count].color = color_a
                    } else {
                        g_bullets[bullet_count].color = color_b
                    }

                    var bullet_direction: f32 = base_direction + (degrees_per_row * (row * 1.0))
                    var rad: f32 = radians(bullet_direction)

                    var acc: Vector2
                    acc.x = bullet_speed * cosf(rad)
                    acc.y = bullet_speed * sinf(rad)
                    g_bullets[bullet_count].acceleration = acc

                    bullet_count = bullet_count + 1
                }
                row = row + 1
            }

            base_direction = base_direction + (angle_increment * 1.0)
        }

        // Update bullet positions
        var i: i32 = 0
        while (i < bullet_count) {
            if (!g_bullets[i].disabled) {
                g_bullets[i].position.x = g_bullets[i].position.x + g_bullets[i].acceleration.x
                g_bullets[i].position.y = g_bullets[i].position.y + g_bullets[i].acceleration.y

                var off: f32 = bullet_radius * 2.0
                if (g_bullets[i].position.x < (0.0 - off)) {
                    g_bullets[i].disabled = true
                    bullet_disabled_count = bullet_disabled_count + 1
                }
                if (g_bullets[i].position.x > (screen_width + off)) {
                    if (!g_bullets[i].disabled) {
                        g_bullets[i].disabled = true
                        bullet_disabled_count = bullet_disabled_count + 1
                    }
                }
                if (g_bullets[i].position.y < (0.0 - off)) {
                    if (!g_bullets[i].disabled) {
                        g_bullets[i].disabled = true
                        bullet_disabled_count = bullet_disabled_count + 1
                    }
                }
                if (g_bullets[i].position.y > (screen_height + off)) {
                    if (!g_bullets[i].disabled) {
                        g_bullets[i].disabled = true
                        bullet_disabled_count = bullet_disabled_count + 1
                    }
                }
            }
            i = i + 1
        }

        // ----- Input -----
        if (IsKeyPressed(KEY_RIGHT)) {
            if (bullet_rows < 359) { bullet_rows = bullet_rows + 1 }
        }
        if (IsKeyPressed(KEY_D)) {
            if (bullet_rows < 359) { bullet_rows = bullet_rows + 1 }
        }
        if (IsKeyPressed(KEY_LEFT)) {
            if (bullet_rows > 1) { bullet_rows = bullet_rows - 1 }
        }
        if (IsKeyPressed(KEY_A)) {
            if (bullet_rows > 1) { bullet_rows = bullet_rows - 1 }
        }
        if (IsKeyPressed(KEY_UP)) { bullet_speed = bullet_speed + 0.25 }
        if (IsKeyPressed(KEY_W)) { bullet_speed = bullet_speed + 0.25 }
        if (IsKeyPressed(KEY_DOWN)) {
            if (bullet_speed > 0.5) { bullet_speed = bullet_speed - 0.25 }
        }
        if (IsKeyPressed(KEY_S)) {
            if (bullet_speed > 0.5) { bullet_speed = bullet_speed - 0.25 }
        }
        if (IsKeyPressed(KEY_Z)) {
            if (spawn_cooldown > 1.0) { spawn_cooldown = spawn_cooldown - 1.0 }
        }
        if (IsKeyPressed(KEY_X)) { spawn_cooldown = spawn_cooldown + 1.0 }
        if (IsKeyPressed(KEY_ENTER)) { draw_perf_mode = !draw_perf_mode }
        if (IsKeyDown(KEY_SPACE)) {
            angle_increment = angle_increment + 1
            angle_increment = angle_increment % 360
        }
        if (IsKeyPressed(KEY_C)) {
            bullet_count = 0
            bullet_disabled_count = 0
        }

        // ----- Draw -----
        BeginDrawing()
        ClearBackground(RAYWHITE)

        // Magic circle
        magic_circle_rotation = magic_circle_rotation + 1.0
        var rect: Rectangle
        rect.x = screen_width * 0.5
        rect.y = screen_height * 0.5
        rect.width = 120.0
        rect.height = 120.0
        var origin: Vector2
        origin.x = 60.0
        origin.y = 60.0
        DrawRectanglePro(rect, origin, magic_circle_rotation, PURPLE)
        DrawRectanglePro(rect, origin, magic_circle_rotation + 45.0, PURPLE)
        DrawCircleLines(screen_width / 2, screen_height / 2, 70.0, BLACK)
        DrawCircleLines(screen_width / 2, screen_height / 2, 50.0, BLACK)
        DrawCircleLines(screen_width / 2, screen_height / 2, 30.0, BLACK)

        // Draw bullets
        if (draw_perf_mode) {
            var tw: f32 = 12.0
            var th: f32 = 12.0
            var j: i32 = 0
            while (j < bullet_count) {
                if (!g_bullets[j].disabled) {
                    var fx: f32 = g_bullets[j].position.x - tw
                    var fy: f32 = g_bullets[j].position.y - th
                    DrawTextureV(bullet_texture.texture, vec2{fx, fy}, g_bullets[j].color)
                }
                j = j + 1
            }
        } else {
            var j: i32 = 0
            while (j < bullet_count) {
                if (!g_bullets[j].disabled) {
                    DrawCircleV(g_bullets[j].position, bullet_radius * 1.0, g_bullets[j].color)
                    DrawCircleLinesV(g_bullets[j].position, bullet_radius * 1.0, BLACK)
                }
                j = j + 1
            }
        }

        // UI panel background
        var panel_bg: Color
        panel_bg.r = 0
        panel_bg.g = 0
        panel_bg.b = 0
        panel_bg.a = 200

        DrawRectangle(10, 10, 280, 150, panel_bg)
        DrawText("Controls:", 20, 20, 10, LIGHTGRAY)
        DrawText("- Right/Left or A/D: Change rows number", 40, 40, 10, LIGHTGRAY)
        DrawText("- Up/Down or W/S: Change bullet speed", 40, 60, 10, LIGHTGRAY)
        DrawText("- Z or X: Change spawn cooldown", 40, 80, 10, LIGHTGRAY)
        DrawText("- Space (Hold): Change the angle increment", 40, 100, 10, LIGHTGRAY)
        DrawText("- Enter: Switch draw method (Performance)", 40, 120, 10, LIGHTGRAY)
        DrawText("- C: Clear bullets", 40, 140, 10, LIGHTGRAY)

        DrawRectangle(610, 10, 170, 30, panel_bg)
        if (draw_perf_mode) {
            DrawText("Draw method: DrawTexture(*)", 620, 20, 10, GREEN)
        } else {
            DrawText("Draw method: DrawCircle(*)", 620, 20, 10, RED)
        }

        DrawRectangle(135, 560, 530, 30, panel_bg)
        DrawText("[ FPS stats ]", 155, 570, 10, GREEN)

        EndDrawing()
    }

    UnloadRenderTexture(bullet_texture)
    CloseWindow()
}
Hello raylib/hello.gx
// hello.gx - GX + Raylib with built-in math types
// Build: gx hello.gx -I modules -o hello.exe
//
// Vector2/3/4 are aliases to GX's vec2/vec3/vec4 — you get
// operator overloading, swizzle ops, and GX math for free.
// Colors are uppercase constants: RED, BLUE, WHITE, etc.

import raylib


fn main() {
    InitWindow(800, 450, "GX + Raylib")
    SetTargetFPS(60)

    while (!WindowShouldClose()) {
        if (IsKeyPressed(KEY_F)) {
            ToggleFullscreen()
        }

        BeginDrawing()
        ClearBackground(RAYWHITE)

        DrawText("Hello from GX + Raylib!", 240, 40, 30, DARKGRAY)

        // GX vec2 literals work directly as Vector2
        DrawRectangle(50, 120, 200, 100, RED)
        DrawCircleV(vec2{450.0, 170.0}, 60.0, BLUE)

        // vec2 math: operator overloading
        var a = vec2{600.0, 120.0}
        var b = vec2{550.0, 220.0}
        var c = vec2{650.0, 220.0}
        DrawTriangle(a, b, c, GREEN)

        // vec2 arithmetic works naturally
        var center = vec2{400.0, 350.0}
        var offset = vec2{50.0, 0.0}

        DrawCircleV(center - offset, 20.0, ORANGE)
        DrawCircleV(center, 20.0, PURPLE)
        DrawCircleV(center + offset, 20.0, SKYBLUE)

        DrawFPS(10, 10)
        DrawText("Press F for fullscreen", 10, 420, 16, GRAY)

        DrawCircleV(center, 20.0, PURPLE)
        EndDrawing()
    }

    CloseWindow()
}
import raylib.colors
Image Generation raylib/image_generation.gx
// image_generation.gx - Raylib procedural texture generation
// Port of raylib [textures] example — image generation
// Build: gx image_generation.gx -I modules -o image_generation.exe
// Web:   gx image_generation.gx -I modules --target web -o image_generation.html
//
// Demonstrates raylib's procedural image generators:
//   - Linear gradients (vertical, horizontal, diagonal)
//   - Radial / Square gradients
//   - Checkerboard
//   - White noise / Perlin noise / Cellular noise
//
// Controls: Left click or RIGHT arrow to cycle textures

import raylib

const NUM_TEXTURES = 9

fn main() {
    var screen_width: c_int = 800
    var screen_height: c_int = 600

    InitWindow(screen_width, screen_height, "raylib [textures] example - image generation")

    // Generate 9 procedural images
    var vertical_gradient   = GenImageGradientLinear(screen_width, screen_height, 0, RED, BLUE)
    var horizontal_gradient = GenImageGradientLinear(screen_width, screen_height, 90, RED, BLUE)
    var diagonal_gradient   = GenImageGradientLinear(screen_width, screen_height, 45, RED, BLUE)
    var radial_gradient     = GenImageGradientRadial(screen_width, screen_height, 0.0, WHITE, BLACK)
    var square_gradient     = GenImageGradientSquare(screen_width, screen_height, 0.0, WHITE, BLACK)
    var checked             = GenImageChecked(screen_width, screen_height, 32, 32, RED, BLUE)
    var white_noise         = GenImageWhiteNoise(screen_width, screen_height, 0.5)
    var perlin_noise        = GenImagePerlinNoise(screen_width, screen_height, 50, 50, 4.0)
    var cellular            = GenImageCellular(screen_width, screen_height, 32)

    // Upload all images to GPU as textures
    var textures: Texture[9]
    textures[0] = LoadTextureFromImage(vertical_gradient)
    textures[1] = LoadTextureFromImage(horizontal_gradient)
    textures[2] = LoadTextureFromImage(diagonal_gradient)
    textures[3] = LoadTextureFromImage(radial_gradient)
    textures[4] = LoadTextureFromImage(square_gradient)
    textures[5] = LoadTextureFromImage(checked)
    textures[6] = LoadTextureFromImage(white_noise)
    textures[7] = LoadTextureFromImage(perlin_noise)
    textures[8] = LoadTextureFromImage(cellular)

    // Free CPU image data (textures live on GPU now)
    UnloadImage(vertical_gradient)
    UnloadImage(horizontal_gradient)
    UnloadImage(diagonal_gradient)
    UnloadImage(radial_gradient)
    UnloadImage(square_gradient)
    UnloadImage(checked)
    UnloadImage(white_noise)
    UnloadImage(perlin_noise)
    UnloadImage(cellular)

    var current_texture: i32 = 0
    var auto_timer: i32 = 0

    SetTargetFPS(60)

    while (!WindowShouldClose()) {
        // Auto-cycle every 120 frames (~2 seconds at 60 FPS)
        auto_timer = auto_timer + 1
        if (auto_timer >= 120) {
            current_texture = (current_texture + 1) % NUM_TEXTURES
            auto_timer = 0
        }

        // Manual cycle on mouse click or right arrow (resets timer)
        if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
            current_texture = (current_texture + 1) % NUM_TEXTURES
            auto_timer = 0
        }
        if (IsKeyPressed(KEY_RIGHT)) {
            current_texture = (current_texture + 1) % NUM_TEXTURES
            auto_timer = 0
        }

        BeginDrawing()
        ClearBackground(RAYWHITE)

        DrawTexture(textures[current_texture], 0, 0, WHITE)

        // Info panel with faded background
        DrawRectangle(30, 550, 325, 30, Fade(SKYBLUE, 0.5))
        DrawRectangleLines(30, 550, 325, 30, Fade(WHITE, 0.5))
        DrawText("MOUSE LEFT BUTTON to CYCLE PROCEDURAL TEXTURES", 40, 560, 10, WHITE)

        // Texture label
        if (current_texture == 0) { DrawText("VERTICAL GRADIENT", 560, 10, 20, RAYWHITE) }
        if (current_texture == 1) { DrawText("HORIZONTAL GRADIENT", 540, 10, 20, RAYWHITE) }
        if (current_texture == 2) { DrawText("DIAGONAL GRADIENT", 540, 10, 20, RAYWHITE) }
        if (current_texture == 3) { DrawText("RADIAL GRADIENT", 580, 10, 20, LIGHTGRAY) }
        if (current_texture == 4) { DrawText("SQUARE GRADIENT", 580, 10, 20, LIGHTGRAY) }
        if (current_texture == 5) { DrawText("CHECKED", 680, 10, 20, RAYWHITE) }
        if (current_texture == 6) { DrawText("WHITE NOISE", 640, 10, 20, RED) }
        if (current_texture == 7) { DrawText("PERLIN NOISE", 640, 10, 20, RED) }
        if (current_texture == 8) { DrawText("CELLULAR", 670, 10, 20, RAYWHITE) }

        EndDrawing()
    }

    // Cleanup — unload GPU textures
    for (i = 0 : 8) {
        UnloadTexture(textures[i])
    }

    CloseWindow()
}
Logo Gx Anim raylib/logo_gx_anim.gx
// logo_gx_anim.gx - Animated GX logo loaded from LogoLow.png
// Each pixel in the low-res logo becomes a quad that falls from above.
// Preserves the exact shape of the brand logo.
//
// Build: gx logo_gx_anim.gx -I modules -o logo_gx_anim.exe
// Web:   gx logo_gx_anim.gx -I modules --target web -o logo_gx_anim.html --preload-file website/resources/LogoLow.png

import raylib

const BLOCK_SIZE = 10           // pixel size of each quad on screen
const MAX_QUADS = 4000

struct Quad {
    target_x: f32
    target_y: f32
    current_y: f32
    velocity: f32
    delay: i32       // frames to wait before starting to fall
    landed: bool
    r: u8
    g: u8
    b: u8
}

// Simple LCG for randomized delays
var g_rng: i32 = 12345

fn rand_range:i32(max: i32) {
    g_rng = g_rng * 1103515245 + 12345
    var v: i32 = (g_rng / 65536) & 32767
    return v % max
}

var g_quads: Quad[4000]
var g_quad_count: i32 = 0

// Load logo image, sample every pixel, spawn a quad for each non-transparent one
fn build_logo_from_image(img: Image, center_x: i32, center_y: i32) {
    g_quad_count = 0

    var img_w: i32 = (i32)img.width
    var img_h: i32 = (i32)img.height
    var total_w: i32 = img_w * BLOCK_SIZE
    var total_h: i32 = img_h * BLOCK_SIZE
    var start_x: i32 = center_x - total_w / 2
    var start_y: i32 = center_y - total_h / 2

    // Old-school chroma key: top-left pixel is the transparent color
    var bg_color: Color = GetImageColor(img, 0, 0)

    for (y = 0 : img_h - 1) {
        for (x = 0 : img_w - 1) {
            var c: Color = GetImageColor(img, x, y)
            // Skip pixels matching the top-left (background) color
            var is_bg: bool = false
            if (c.r == bg_color.r) {
                if (c.g == bg_color.g) {
                    if (c.b == bg_color.b) {
                        is_bg = true
                    }
                }
            }
            if (!is_bg) {
                if (g_quad_count < MAX_QUADS) {
                    g_quads[g_quad_count].target_x = (start_x + x * BLOCK_SIZE) * 1.0
                    g_quads[g_quad_count].target_y = (start_y + y * BLOCK_SIZE) * 1.0
                    // Start well above the screen
                    g_quads[g_quad_count].current_y = 0.0 - 200.0 - (rand_range(300) * 1.0)
                    g_quads[g_quad_count].velocity = 0.0
                    // Random delay (0-90 frames) so quads fall in scattered order
                    g_quads[g_quad_count].delay = rand_range(90)
                    g_quads[g_quad_count].landed = false
                    g_quads[g_quad_count].r = c.r
                    g_quads[g_quad_count].g = c.g
                    g_quads[g_quad_count].b = c.b
                    g_quad_count = g_quad_count + 1
                }
            }
        }
    }
}

fn update_quads:bool() {
    var all_landed: bool = true
    for (i = 0 : g_quad_count - 1) {
        if (!g_quads[i].landed) {
            if (g_quads[i].delay > 0) {
                g_quads[i].delay = g_quads[i].delay - 1
                all_landed = false
            } else {
                g_quads[i].velocity = g_quads[i].velocity + 0.55
                g_quads[i].current_y = g_quads[i].current_y + g_quads[i].velocity

                if (g_quads[i].current_y >= g_quads[i].target_y) {
                    g_quads[i].current_y = g_quads[i].target_y
                    g_quads[i].landed = true
                } else {
                    all_landed = false
                }
            }
        }
    }
    return all_landed
}

fn draw_quads(alpha: f32) {
    for (i = 0 : g_quad_count - 1) {
        // Skip quads still waiting for their delay to expire
        if (g_quads[i].delay > 0) {
            continue
        }
        var col: Color
        col.r = g_quads[i].r
        col.g = g_quads[i].g
        col.b = g_quads[i].b
        col.a = 255
        var tint: Color = Fade(col, alpha)

        var rec: Rectangle
        rec.x = g_quads[i].target_x
        rec.y = g_quads[i].current_y
        rec.width = BLOCK_SIZE * 1.0
        rec.height = BLOCK_SIZE * 1.0
        DrawRectangleRec(rec, tint)
    }
}

fn main() {
    var screen_width: i32 = 800
    var screen_height: i32 = 600

    InitWindow(screen_width, screen_height, "GX logo animation")
    SetTargetFPS(60)

    // Load the low-res logo
    var logo_img: Image = LoadImage("website/resources/LogoLow.png")

    var bg: Color
    bg.r = 8
    bg.g = 15
    bg.b = 35
    bg.a = 255

    var state: i32 = 0
    var alpha: f32 = 1.0
    var hold_timer: i32 = 0

    build_logo_from_image(logo_img, screen_width / 2, screen_height / 2)

    while (!WindowShouldClose()) {
        if (state == 0) {
            var landed: bool = update_quads()
            if (landed) {
                state = 1
                hold_timer = 0
            }
        }
        if (state == 1) {
            hold_timer = hold_timer + 1
            if (hold_timer > 120) { state = 2 }
        }
        if (state == 2) {
            alpha = alpha - 0.015
            if (alpha <= 0.0) {
                alpha = 0.0
                state = 3
            }
        }
        if (state == 3) {
            build_logo_from_image(logo_img, screen_width / 2, screen_height / 2)
            alpha = 1.0
            hold_timer = 0
            state = 0
        }

        BeginDrawing()
        ClearBackground(bg)
        draw_quads(alpha)
        EndDrawing()
    }

    UnloadImage(logo_img)
    CloseWindow()
}

Graphics — Sokol

Input sokol/input.gx
// input.ec - Handling input events with Sokol
// Build: ec examples/sokol/input.ec -I modules --cfile modules/sokol/c/sokol_impl.c --cflags "-Imodules/sokol/c" --ldflags "-lopengl32 -lgdi32 -luser32 -lshell32" -o build/input.exe

import sokol.gfx
import sokol.app
import sokol.glue

// Global state for background color
var g_r:f32 = 0.2;
var g_g:f32 = 0.2;
var g_b:f32 = 0.2;

@c_abi
fn init() {
    var gfx_desc:sg_desc;
    gfx_desc.environment = sglue_environment();
    sg_setup(&gfx_desc);
    print("Input demo initialized. Press keys or click mouse!\n");
}

@c_abi
fn frame() {
    var pass:sg_pass;
    pass.action.colors[0].load_action = 1;  // SG_LOADACTION_CLEAR
    pass.action.colors[0].clear_value.r = g_r;
    pass.action.colors[0].clear_value.g = g_g;
    pass.action.colors[0].clear_value.b = g_b;
    pass.action.colors[0].clear_value.a = 1.0;
    pass.swapchain = sglue_swapchain();
    
    sg_begin_pass(&pass);
    sg_end_pass();
    sg_commit();
}

@c_abi
fn event(ev: const *sapp_event) {
    if (ev.type == EVENTTYPE_KEY_DOWN) {
        if (ev.key_code == KEYCODE_ESCAPE) {
            sapp_quit();
        }
        print("Key pressed!\n");
    }
    elif (ev.type == EVENTTYPE_MOUSE_DOWN) {
        if (ev.mouse_button == MOUSEBUTTON_LEFT) {
            g_r = 0.8; g_g = 0.2; g_b = 0.2;
        }
        elif (ev.mouse_button == MOUSEBUTTON_RIGHT) {
            g_r = 0.2; g_g = 0.8; g_b = 0.2;
        }
        print("Mouse down!\n");
    }
    elif (ev.type == EVENTTYPE_MOUSE_MOVE) {
        // print("Mouse move: " + ev.mouse_x.str() + ", " + ev.mouse_y.str() + "\n");
    }
}

@c_abi
fn cleanup() {
    sg_shutdown();
}

fn main() {
    var desc:sapp_desc;
    desc.init_cb = init;
    desc.frame_cb = frame;
    desc.event_cb = event;
    desc.cleanup_cb = cleanup;
    desc.width = 640;
    desc.height = 480;
    desc.sample_count = 1;
    desc.swap_interval = 1;
    desc.window_title = "EC Sokol Input Demo";
    sapp_run(&desc);
}
Shapes sokol/shapes.gx
// shapes.ec - Drawing 2D shapes with Sokol GP
// Build: ec examples/sokol/shapes.ec -I modules --cfile modules/sokol/c/sokol_impl.c --cflags "-Imodules/sokol/c" --ldflags "-lopengl32 -lgdi32 -luser32 -lshell32" -o build/shapes.exe

import sokol.gfx
import sokol.app
import sokol.glue
import sokol.gp
import math.scalar

// Animation state
var g_time:f32 = 0.0;
var g_frame:i32 = 0;

@c_abi
fn init() {
    var gfx_desc:sg_desc;
    gfx_desc.environment = sglue_environment();
    sg_setup(&gfx_desc);

    var gp_desc:sgp_desc;
    sgp_setup(&gp_desc);

    var err:sgp_error = sgp_get_last_error();
    var msg:cstr = sgp_get_error_message(err);
    print("SGP status: ");
    print(msg);
    print("\n");
}

@c_abi
fn frame() {
    var w:c_int = sapp_width();
    var h:c_int = sapp_height();
    var fw:f32 = 800.0;
    var fh:f32 = 600.0;

    g_time = g_time + 0.016;
    g_frame = g_frame + 1;

    sgp_begin(w, h);
    sgp_viewport(0, 0, w, h);
    sgp_project(0.0, fw, 0.0, fh);

    // Dark background
    sgp_set_color(0.08, 0.08, 0.12, 1.0);
    sgp_clear();

    // --- Grid of small dots ---
    var gx:i32 = 0;
    while (gx < 20) {
        var gy:i32 = 0;
        while (gy < 15) {
            sgp_set_color(0.15, 0.15, 0.2, 1.0);
            sgp_draw_filled_rect(gx * 40 + 19, gy * 40 + 19, 2.0, 2.0);
            gy = gy + 1;
        }
        gx = gx + 1;
    }

    // --- Animated rectangles (color wheel) ---
    var i:i32 = 0;
    while (i < 6) {
        var fi:f32 = i * 1.0;
        var angle:f32 = fi * 1.0472 + g_time;  // 60 degrees apart + rotation
        var cx:f32 = 200.0 + cosf(angle) * 80.0;
        var cy:f32 = 150.0 + sinf(angle) * 80.0;
        var r:f32 = sinf(g_time + fi) * 0.5 + 0.5;
        var g:f32 = sinf(g_time + fi + 2.094) * 0.5 + 0.5;
        var b:f32 = sinf(g_time + fi + 4.189) * 0.5 + 0.5;
        sgp_set_blend_mode(BLENDMODE_BLEND);
        sgp_set_color(r, g, b, 0.7);
        sgp_draw_filled_rect(cx - 20.0, cy - 20.0, 40.0, 40.0);
        i = i + 1;
    }

    // --- Spinning triangles ---
    sgp_set_blend_mode(BLENDMODE_BLEND);
    var t:i32 = 0;
    while (t < 4) {
        var ft:f32 = t * 1.0;
        var ta:f32 = g_time * 1.5 + ft * 1.5708;
        var tcx:f32 = 600.0;
        var tcy:f32 = 150.0;
        var tr:f32 = 60.0;
        var ax:f32 = tcx + cosf(ta) * tr;
        var ay:f32 = tcy + sinf(ta) * tr;
        var bx:f32 = tcx + cosf(ta + 2.094) * tr;
        var by:f32 = tcy + sinf(ta + 2.094) * tr;
        var ccx:f32 = tcx + cosf(ta + 4.189) * tr;
        var ccy:f32 = tcy + sinf(ta + 4.189) * tr;

        var cr:f32 = 0.2;
        var cg:f32 = 0.6;
        var cb:f32 = 0.9;
        if (t == 1) { cr = 0.9; cg = 0.3; cb = 0.2; }
        elif (t == 2) { cr = 0.2; cg = 0.9; cb = 0.3; }
        elif (t == 3) { cr = 0.9; cg = 0.9; cb = 0.2; }
        sgp_set_color(cr, cg, cb, 0.6);
        sgp_draw_filled_triangle(ax, ay, bx, by, ccx, ccy);
        t = t + 1;
    }

    // --- Pulsing center rectangle ---
    var pulse:f32 = sinf(g_time * 3.0) * 0.3 + 0.7;
    sgp_set_color(1.0, 1.0, 1.0, pulse);
    var pw:f32 = 60.0 + sinf(g_time * 2.0) * 20.0;
    var ph:f32 = 60.0 + cosf(g_time * 2.0) * 20.0;
    sgp_draw_filled_rect(400.0 - pw / 2.0, 300.0 - ph / 2.0, pw, ph);

    // --- Line star pattern ---
    sgp_set_color(0.5, 0.8, 1.0, 0.5);
    var li:i32 = 0;
    while (li < 12) {
        var la:f32 = li * 0.5236 + g_time * 0.3;  // 30 degrees apart
        var lx:f32 = 400.0 + cosf(la) * 120.0;
        var ly:f32 = 450.0 + sinf(la) * 80.0;
        sgp_draw_line(400.0, 450.0, lx, ly);
        li = li + 1;
    }

    // --- Bouncing row of rectangles ---
    var bi:i32 = 0;
    while (bi < 8) {
        var bf:f32 = bi * 1.0;
        var bx:f32 = 50.0 + bf * 90.0;
        var bounce:f32 = sinf(g_time * 4.0 + bf * 0.8);
        if (bounce < 0.0) { bounce = 0.0 - bounce; }
        var by:f32 = 520.0 - bounce * 40.0;
        var br:f32 = sinf(bf * 0.7) * 0.5 + 0.5;
        var bg:f32 = sinf(bf * 0.7 + 2.0) * 0.5 + 0.5;
        var bb:f32 = sinf(bf * 0.7 + 4.0) * 0.5 + 0.5;
        sgp_set_color(br, bg, bb, 0.9);
        sgp_draw_filled_rect(bx, by, 50.0, 20.0);
        bi = bi + 1;
    }

    // --- Concentric rectangles (top right) ---
    var ci:i32 = 0;
    while (ci < 5) {
        var cf:f32 = ci * 1.0;
        var cs:f32 = 20.0 + cf * 20.0;
        var ca:f32 = 1.0 - cf * 0.18;
        sgp_set_color(sinf(g_time + cf) * 0.5 + 0.5, 0.3, 0.8, ca);
        sgp_draw_filled_rect(400.0 - cs / 2.0, 100.0 - cs / 2.0, cs, cs);
        ci = ci + 1;
    }

    // Flush SGP into Sokol GFX pass
    var pass:sg_pass;
    pass.action.colors[0].load_action = 1;  // SG_LOADACTION_CLEAR
    pass.action.colors[0].clear_value.r = 0.0;
    pass.action.colors[0].clear_value.g = 0.0;
    pass.action.colors[0].clear_value.b = 0.0;
    pass.action.colors[0].clear_value.a = 1.0;
    pass.swapchain = sglue_swapchain();
    sg_begin_pass(&pass);
    sgp_flush();
    sgp_end();
    sg_end_pass();
    sg_commit();
}

@c_abi
fn event(ev: const *sapp_event) {
    if (ev.type == EVENTTYPE_KEY_DOWN) {
        if (ev.key_code == KEYCODE_ESCAPE) {
            sapp_quit();
        }
    }
}

@c_abi
fn cleanup() {
    sgp_shutdown();
    sg_shutdown();
}

fn main() {
    var desc:sapp_desc;
    desc.init_cb = init;
    desc.frame_cb = frame;
    desc.event_cb = event;
    desc.cleanup_cb = cleanup;
    desc.width = 800;
    desc.height = 600;
    desc.sample_count = 1;
    desc.swap_interval = 1;
    desc.window_title = "EC Sokol GP - Shapes Demo";
    sapp_run(&desc);
}
Snake sokol/snake.gx
// snake.ec - Classic Snake game in EC using Sokol
// Build: ec examples/sokol/snake.ec -I modules --cfile modules/sokol/c/sokol_impl.c --cflags "-Imodules/sokol/c" --ldflags "-lopengl32 -lgdi32 -luser32 -lshell32" -o build/snake.exe

import sokol.app
import sokol.gfx
import sokol.glue

// ============================================================================
// Constants
// ============================================================================
// Grid: 20x20 cells
// Each cell in NDC: 2.0/20 = 0.1 units
// Max snake length: 400 (full grid)
// Vertex format: (x, y, r, g, b) = 5 floats per vertex
// Each cell quad: 2 triangles = 6 vertices = 30 floats
// Max cells rendered: ~410 (snake + food + border)
// Max floats: 410 * 30 = 12300

// ============================================================================
// Game State
// ============================================================================
var g_sx:i32[400];       // snake X positions (grid coords)
var g_sy:i32[400];       // snake Y positions (grid coords)
var g_slen:i32 = 3;      // current snake length
var g_dir:i32 = 1;       // direction: 0=up, 1=right, 2=down, 3=left
var g_next_dir:i32 = 1;  // buffered direction (applied on next tick)
var g_food_x:i32 = 10;
var g_food_y:i32 = 10;
var g_score:i32 = 0;
var g_alive:i32 = 1;     // 1=playing, 0=game over
var g_tick:i32 = 0;       // frame counter for movement timing
var g_speed:i32 = 8;      // frames between moves (lower = faster)
var g_rng:i32 = 42;       // simple PRNG state
var g_started:i32 = 0;    // 0=title screen, 1=playing

// ============================================================================
// Vertex buffer (dynamic, rebuilt each frame)
// ============================================================================
var g_verts:f32[12600];   // max 420 quads * 30 floats
var g_vert_count:i32 = 0; // number of floats written
var g_num_verts:i32 = 0;  // number of vertices (vert_count / 5)

// ============================================================================
// Sokol resources
// ============================================================================
var g_pip:sg_pipeline;
var g_bind:sg_bindings;
var g_vbuf:sg_buffer;

// Shaders
var g_vs:cstr = "#version 330\nin vec2 position;\nin vec3 color0;\nout vec3 color;\nvoid main() { gl_Position = vec4(position, 0.0, 1.0); color = color0; }\n";
var g_fs:cstr = "#version 330\nin vec3 color;\nout vec4 frag_color;\nvoid main() { frag_color = vec4(color, 1.0); }\n";

// ============================================================================
// PRNG (simple LCG)
// ============================================================================
fn rng_next:i32() {
    g_rng = g_rng * 1103515245 + 12345;
    var v:i32 = g_rng / 65536;
    if (v < 0) { v = 0 - v; }
    return v;
}

fn rng_range:i32(max:i32) {
    var v:i32 = rng_next();
    return v % max;
}

// ============================================================================
// Vertex helpers: push a colored quad at grid position
// ============================================================================
fn push_vert(x:f32, y:f32, r:f32, g:f32, b:f32) {
    g_verts[g_vert_count] = x;      g_vert_count = g_vert_count + 1;
    g_verts[g_vert_count] = y;      g_vert_count = g_vert_count + 1;
    g_verts[g_vert_count] = r;      g_vert_count = g_vert_count + 1;
    g_verts[g_vert_count] = g;      g_vert_count = g_vert_count + 1;
    g_verts[g_vert_count] = b;      g_vert_count = g_vert_count + 1;
}

fn push_quad(gx:i32, gy:i32, r:f32, g:f32, b:f32) {
    // Convert grid coords to NDC
    // Grid is 20x20, mapped to [-0.95, 0.95] to leave a small border
    var cell:f32 = 1.9 / 20.0;
    var gap:f32 = 0.01;
    var x0:f32 = -0.95 + gx * cell + gap;
    var y0:f32 = 0.95 - gy * cell - gap;
    var x1:f32 = x0 + cell - gap * 2.0;
    var y1:f32 = y0 - cell + gap * 2.0;

    // Triangle 1: top-left, top-right, bottom-left
    push_vert(x0, y0, r, g, b);
    push_vert(x1, y0, r, g, b);
    push_vert(x0, y1, r, g, b);
    // Triangle 2: top-right, bottom-right, bottom-left
    push_vert(x1, y0, r, g, b);
    push_vert(x1, y1, r, g, b);
    push_vert(x0, y1, r, g, b);



}

// ============================================================================
// Game Logic
// ============================================================================
fn game_reset() {
    g_slen = 3;
    g_dir = 1;
    g_next_dir = 1;
    g_score = 0;
    g_alive = 1;
    g_tick = 0;
    g_speed = 8;
    g_started = 1;

    // Start snake in center, going right
    g_sx[0] = 10; g_sy[0] = 10;  // head
    g_sx[1] = 9;  g_sy[1] = 10;
    g_sx[2] = 8;  g_sy[2] = 10;

    spawn_food();
    print("Snake game started! Score: 0\n");
}

fn spawn_food() {
    var valid:i32 = 0;
    while (valid == 0) {
        g_food_x = rng_range(20);
        g_food_y = rng_range(20);
        // Make sure food doesn't overlap snake
        valid = 1;
        var i:i32 = 0;
        while (i < g_slen) {
            if (g_sx[i] == g_food_x) {
                if (g_sy[i] == g_food_y) {
                    valid = 0;
                }
            }
            i = i + 1;
        }
    }
}

fn game_tick() {
    if (g_alive == 0) { return; }
    if (g_started == 0) { return; }

    g_tick = g_tick + 1;
    if (g_tick < g_speed) { return; }
    g_tick = 0;

    // Apply buffered direction
    g_dir = g_next_dir;

    // Calculate new head position
    var hx:i32 = g_sx[0];
    var hy:i32 = g_sy[0];
    if (g_dir == 0) { hy = hy - 1; }
    if (g_dir == 1) { hx = hx + 1; }
    if (g_dir == 2) { hy = hy + 1; }
    if (g_dir == 3) { hx = hx - 1; }

    // Wall collision
    if (hx < 0)  { g_alive = 0; print("Game Over! Hit wall. Score: {g_score}\n"); return; }
    if (hx > 19) { g_alive = 0; print("Game Over! Hit wall. Score: {g_score}\n"); return; }
    if (hy < 0)  { g_alive = 0; print("Game Over! Hit wall. Score: {g_score}\n"); return; }
    if (hy > 19) { g_alive = 0; print("Game Over! Hit wall. Score: {g_score}\n"); return; }

    // Self collision
    var i:i32 = 0;
    while (i < g_slen) {
        if (g_sx[i] == hx) {
            if (g_sy[i] == hy) {
                g_alive = 0;
                print("Game Over! Hit self. Score: {g_score}\n");
                return;
            }
        }
        i = i + 1;
    }

    // Check food
    var ate:i32 = 0;
    if (hx == g_food_x) {
        if (hy == g_food_y) {
            ate = 1;
            g_score = g_score + 1;
            print("Score: {g_score}\n");
            // Speed up every 5 points
            if (g_score % 5 == 0) {
                if (g_speed > 3) {
                    g_speed = g_speed - 1;
                }
            }
        }
    }

    // Move body: shift all segments down
    if (ate == 1) {
        // Grow: don't remove tail
        var j:i32 = g_slen;
        while (j > 0) {
            g_sx[j] = g_sx[j - 1];
            g_sy[j] = g_sy[j - 1];
            j = j - 1;
        }
        g_slen = g_slen + 1;
    } else {
        // No growth: shift body, tail disappears
        var j:i32 = g_slen - 1;
        while (j > 0) {
            g_sx[j] = g_sx[j - 1];
            g_sy[j] = g_sy[j - 1];
            j = j - 1;
        }
    }

    // Place new head
    g_sx[0] = hx;
    g_sy[0] = hy;

    // Spawn new food if eaten
    if (ate == 1) {
        spawn_food();
    }
}

// ============================================================================
// Rendering
// ============================================================================
fn build_vertices() {
    g_vert_count = 0;

    if (g_started == 0) {
        // Title screen: draw "S N A K E" pattern using cells
        // Letter S (col 2-5, row 6-12)
        push_quad(2, 6, 0.2, 0.8, 0.2);  push_quad(3, 6, 0.2, 0.8, 0.2);  push_quad(4, 6, 0.2, 0.8, 0.2);
        push_quad(2, 7, 0.2, 0.8, 0.2);
        push_quad(2, 8, 0.2, 0.8, 0.2);  push_quad(3, 8, 0.2, 0.8, 0.2);  push_quad(4, 8, 0.2, 0.8, 0.2);
        push_quad(4, 9, 0.2, 0.8, 0.2);
        push_quad(2, 10, 0.2, 0.8, 0.2); push_quad(3, 10, 0.2, 0.8, 0.2); push_quad(4, 10, 0.2, 0.8, 0.2);

        // Letter N (col 6-9, row 6-10)
        push_quad(6, 6, 0.2, 0.8, 0.2);  push_quad(6, 7, 0.2, 0.8, 0.2);  push_quad(6, 8, 0.2, 0.8, 0.2);
        push_quad(6, 9, 0.2, 0.8, 0.2);  push_quad(6, 10, 0.2, 0.8, 0.2);
        push_quad(7, 7, 0.2, 0.8, 0.2);
        push_quad(8, 8, 0.2, 0.8, 0.2);
        push_quad(9, 6, 0.2, 0.8, 0.2);  push_quad(9, 7, 0.2, 0.8, 0.2);  push_quad(9, 8, 0.2, 0.8, 0.2);
        push_quad(9, 9, 0.2, 0.8, 0.2);  push_quad(9, 10, 0.2, 0.8, 0.2);

        // Letter A (col 11-14, row 6-10)
        push_quad(12, 6, 0.2, 0.8, 0.2); push_quad(13, 6, 0.2, 0.8, 0.2);
        push_quad(11, 7, 0.2, 0.8, 0.2); push_quad(14, 7, 0.2, 0.8, 0.2);
        push_quad(11, 8, 0.2, 0.8, 0.2); push_quad(12, 8, 0.2, 0.8, 0.2);
        push_quad(13, 8, 0.2, 0.8, 0.2); push_quad(14, 8, 0.2, 0.8, 0.2);
        push_quad(11, 9, 0.2, 0.8, 0.2); push_quad(14, 9, 0.2, 0.8, 0.2);
        push_quad(11, 10, 0.2, 0.8, 0.2); push_quad(14, 10, 0.2, 0.8, 0.2);

        // Letter K (col 16-19, row 6-10)
        push_quad(16, 6, 0.2, 0.8, 0.2);  push_quad(16, 7, 0.2, 0.8, 0.2);
        push_quad(16, 8, 0.2, 0.8, 0.2);  push_quad(16, 9, 0.2, 0.8, 0.2);
        push_quad(16, 10, 0.2, 0.8, 0.2);
        push_quad(18, 6, 0.2, 0.8, 0.2);  push_quad(17, 7, 0.2, 0.8, 0.2);
        push_quad(17, 8, 0.2, 0.8, 0.2);
        push_quad(17, 9, 0.2, 0.8, 0.2);  push_quad(18, 10, 0.2, 0.8, 0.2);

        // "Press SPACE" hint - small dots
        push_quad(6, 14, 0.5, 0.5, 0.5);
        push_quad(8, 14, 0.5, 0.5, 0.5);
        push_quad(10, 14, 0.5, 0.5, 0.5);
        push_quad(12, 14, 0.5, 0.5, 0.5);
        push_quad(14, 14, 0.5, 0.5, 0.5);

        g_num_verts = g_vert_count / 5;
        return;
    }

    // Draw food (pulsing red — alternate brightness)
    var food_r:f32 = 0.9;
    var food_g:f32 = 0.15;
    var food_b:f32 = 0.15;
    if (g_tick % 4 < 2) {
        food_r = 1.0;
        food_g = 0.25;
        food_b = 0.2;
    }
    push_quad(g_food_x, g_food_y, food_r, food_g, food_b);

    // Draw snake body (gradient from bright to dark green)
    var i:i32 = g_slen - 1;
    while (i >= 0) {
        var t:f32 = 0.3;
        if (g_slen > 1) {
            // Gradient: head is bright, tail is darker
            t = 0.3 + 0.6 * (1.0 - i * 1.0 / g_slen);
        }
        var sr:f32 = 0.1;
        var sg:f32 = t;
        var sb:f32 = 0.1;
        if (i == 0) {
            // Head is brightest
            sr = 0.2;
            sg = 0.95;
            sb = 0.2;
        }
        // Game over: snake turns red
        if (g_alive == 0) {
            sr = 0.7;
            sg = 0.15;
            sb = 0.1;
            if (i == 0) {
                sr = 1.0;
                sg = 0.2;
                sb = 0.15;
            }
        }
        push_quad(g_sx[i], g_sy[i], sr, sg, sb);
        i = i - 1;
    }

    // Draw border (subtle gray outline around play area)
    var bx:i32 = 0;
    while (bx < 20) {
        // Top and bottom border markers (just outside play area — we'll draw at grid edge)
        // Since we can't draw outside the grid, draw a subtle border at the edges
        if (g_alive == 1) {
            // Dim grid dots at corners only to keep it subtle
        }
        bx = bx + 1;
    }

    g_num_verts = g_vert_count / 5;
}

// ============================================================================
// Sokol Callbacks
// ============================================================================
@c_abi
fn init() {
    // Setup sokol_gfx
    var gfx_desc:sg_desc;
    gfx_desc.environment = sglue_environment();
    sg_setup(&gfx_desc);

    // Create a STREAM vertex buffer (updated every frame)
    var buf_desc:sg_buffer_desc;
    buf_desc.size = 50400;              // 12600 floats * 4 bytes = 50400 bytes
    buf_desc.usage.stream_update = 1;    // enable streaming
    g_vbuf = sg_make_buffer(&buf_desc);
    g_bind.vertex_buffers[0] = g_vbuf;

    // Create shader
    var shd_desc:sg_shader_desc;
    shd_desc.vertex_func.source = g_vs;
    shd_desc.fragment_func.source = g_fs;
    shd_desc.attrs[0].glsl_name = "position";
    shd_desc.attrs[1].glsl_name = "color0";
    var shd:sg_shader = sg_make_shader(&shd_desc);

    // Create pipeline
    var pip_desc:sg_pipeline_desc;
    pip_desc.shader = shd;
    pip_desc.layout.attrs[0].format = 2;  // SG_VERTEXFORMAT_FLOAT2
    pip_desc.layout.attrs[1].format = 3;  // SG_VERTEXFORMAT_FLOAT3
    g_pip = sg_make_pipeline(&pip_desc);

    // Seed PRNG with something
    g_rng = 7919;

    print("=== EC Snake ===\n");
    print("Arrow keys or WASD to move\n");
    print("Press SPACE to start\n");
    print("ESC to quit\n\n");
}

@c_abi
fn frame() {
    // Game logic
    game_tick();

    // Build vertices
    build_vertices();

    // Update vertex buffer
    var range:sg_range;
    range.ptr = &g_verts[0];
    range.size = g_vert_count * 4;  // 4 bytes per float
    if (g_vert_count > 0) {
        sg_update_buffer(g_vbuf, &range);
    }

    // Background color
    var bg_r:f32 = 0.08;
    var bg_g:f32 = 0.08;
    var bg_b:f32 = 0.1;
    if (g_alive == 0) {
        // Reddish tint on game over
        bg_r = 0.15;
        bg_g = 0.05;
        bg_b = 0.05;
    }

    // Render pass
    var pass:sg_pass;
    pass.action.colors[0].load_action = 1;  // SG_LOADACTION_CLEAR
    pass.action.colors[0].clear_value.r = bg_r;
    pass.action.colors[0].clear_value.g = bg_g;
    pass.action.colors[0].clear_value.b = bg_b;
    pass.action.colors[0].clear_value.a = 1.0;
    pass.swapchain = sglue_swapchain();

    sg_begin_pass(&pass);
    if (g_num_verts > 0) {
        sg_apply_pipeline(g_pip);
        sg_apply_bindings(&g_bind);
        sg_draw(0, g_num_verts, 1);
    }
    sg_end_pass();
    sg_commit();
}

@c_abi
fn event(ev: const *sapp_event) {
    if (ev.type == EVENTTYPE_KEY_DOWN) {
        // Quit
        if (ev.key_code == KEYCODE_ESCAPE) {
            sapp_quit();
        }

        // Start / restart
        if (ev.key_code == KEYCODE_SPACE) {
            if (g_started == 0) {
                game_reset();
            }
            elif (g_alive == 0) {
                game_reset();
            }
        }
        if (ev.key_code == KEYCODE_ENTER) {
            if (g_started == 0) {
                game_reset();
            }
            elif (g_alive == 0) {
                game_reset();
            }
        }

        // Direction input (prevent 180-degree turns)
        if (g_alive == 1) {
            if (ev.key_code == KEYCODE_UP) {
                if (g_dir != 2) { g_next_dir = 0; }
            }
            if (ev.key_code == KEYCODE_W) {
                if (g_dir != 2) { g_next_dir = 0; }
            }
            if (ev.key_code == KEYCODE_RIGHT) {
                if (g_dir != 3) { g_next_dir = 1; }
            }
            if (ev.key_code == KEYCODE_D) {
                if (g_dir != 3) { g_next_dir = 1; }
            }
            if (ev.key_code == KEYCODE_DOWN) {
                if (g_dir != 0) { g_next_dir = 2; }
            }
            if (ev.key_code == KEYCODE_S) {
                if (g_dir != 0) { g_next_dir = 2; }
            }
            if (ev.key_code == KEYCODE_LEFT) {
                if (g_dir != 1) { g_next_dir = 3; }
            }
            if (ev.key_code == KEYCODE_A) {
                if (g_dir != 1) { g_next_dir = 3; }
            }
        }
    }
}

@c_abi
fn cleanup() {
    sg_shutdown();
}

fn main() {
    var desc:sapp_desc;
    desc.init_cb = init;
    desc.frame_cb = frame;
    desc.event_cb = event;
    desc.cleanup_cb = cleanup;
    desc.width = 640;
    desc.height = 640;
    desc.sample_count = 1;
    desc.swap_interval = 1;
    desc.window_title = "EC Snake";
    sapp_run(&desc);
}
Triangle sokol/triangle.gx
// triangle.ec - 100% EC triangle demo using sokol wrappers
// Build: ec examples/sokol/triangle.ec -I modules --cfile modules/sokol/c/sokol_impl.c --cflags "-Imodules/sokol/c" --ldflags "-lopengl32 -lgdi32 -luser32 -lshell32" -o build/triangle.exe

import sokol.app
import sokol.gfx
import sokol.glue

// ============================================================================
// Vertex data: 3 vertices * (2 pos + 3 color) = 15 floats
// ============================================================================
var g_vertices:f32[15];

// Global state
var g_pip:sg_pipeline;
var g_bind:sg_bindings;

// ============================================================================
// Shader sources (GLSL 330)
// ============================================================================
var g_vs_source:cstr = "#version 330\nin vec2 position;\nin vec3 color0;\nout vec3 color;\nvoid main() { gl_Position = vec4(position, 0.0, 1.0); color = color0; }\n";
var g_fs_source:cstr = "#version 330\nin vec3 color;\nout vec4 frag_color;\nvoid main() { frag_color = vec4(color, 1.0); }\n";

@c_abi
fn init() {
    print("Initializing...\n");
    
    // Setup sokol_gfx
    var gfx_desc:sg_desc;
    gfx_desc.environment = sglue_environment();
    sg_setup(&gfx_desc);
    
    // Initialize vertex data: positions + colors
    // Top vertex (red)
    g_vertices[0] = 0.0;   g_vertices[1] = 0.5;
    g_vertices[2] = 1.0;   g_vertices[3] = 0.0;   g_vertices[4] = 0.0;
    // Right vertex (green)
    g_vertices[5] = 0.5;   g_vertices[6] = -0.5;
    g_vertices[7] = 0.0;   g_vertices[8] = 1.0;   g_vertices[9] = 0.0;
    // Left vertex (blue)
    g_vertices[10] = -0.5; g_vertices[11] = -0.5;
    g_vertices[12] = 0.0;  g_vertices[13] = 0.0;  g_vertices[14] = 1.0;
    
    // Create vertex buffer
    var buf_desc:sg_buffer_desc;
    buf_desc.data.ptr = &g_vertices[0];
    buf_desc.data.size = 60;  // 15 floats * 4 bytes
    var vbuf:sg_buffer = sg_make_buffer(&buf_desc);
    g_bind.vertex_buffers[0] = vbuf;
    
    // Create shader
    var shd_desc:sg_shader_desc;
    shd_desc.vertex_func.source = g_vs_source;
    shd_desc.fragment_func.source = g_fs_source;
    shd_desc.attrs[0].glsl_name = "position";
    shd_desc.attrs[1].glsl_name = "color0";
    var shd:sg_shader = sg_make_shader(&shd_desc);
    
    // Create pipeline
    var pip_desc:sg_pipeline_desc;
    pip_desc.shader = shd;
    pip_desc.layout.attrs[0].format = 2;  // SG_VERTEXFORMAT_FLOAT2
    pip_desc.layout.attrs[1].format = 3;  // SG_VERTEXFORMAT_FLOAT3
    g_pip = sg_make_pipeline(&pip_desc);
    
    print("Ready!\n");
}

@c_abi
fn frame() {
    var pass:sg_pass;
    pass.action.colors[0].load_action = 1;  // SG_LOADACTION_CLEAR
    pass.action.colors[0].clear_value.r = 0.2;
    pass.action.colors[0].clear_value.g = 0.2;
    pass.action.colors[0].clear_value.b = 0.2;
    pass.action.colors[0].clear_value.a = 1.0;
    pass.swapchain = sglue_swapchain();
    
    sg_begin_pass(&pass);
    sg_apply_pipeline(g_pip);
    sg_apply_bindings(&g_bind);
    sg_draw(0, 3, 1);
    sg_end_pass();
    sg_commit();
}

@c_abi
fn cleanup() {
    print("Cleanup...\n");
    sg_shutdown();
}

fn main() {
    print("EC Sokol Triangle (100% EC)\n");
    var desc:sapp_desc;
    desc.init_cb = init;
    desc.frame_cb = frame;
    desc.cleanup_cb = cleanup;
    desc.width = 640;
    desc.height = 480;
    desc.sample_count = 1;
    desc.swap_interval = 1;
    desc.window_title = "EC Triangle (Pure EC)";
    sapp_run(&desc);
}