Core Language

Semicolons are optional. GX accepts ; at the end of statements but does not require them.

Types

Scalars

i8, i16, i32, i64
u8, u16, u32, u64
f16, f32, f64
bool, str (fat string: {ptr, len}), char

Vectors & Matrices

vec2, vec3, vec4
mat2, mat3, mat4

All are f32-based value types defined at the language level. See Vectors and Matrices below.

Pointers

var p:*i32;
var cp:const *char;

Arrays

Fixed-size Arrays

var arr:i32[4] = {1, 2, 3, 4};

Dynamic Arrays

Dynamic arrays (array<T>) auto-initialize on declaration — no .init() call needed:

var nums:array<i32>;
nums.push(10);
nums.push(20);
defer nums.free();

You can also initialize with values, just like fixed arrays:

var scores:array<i32> = {95, 82, 74, 88, 91};
var names:array<str> = {"Afan", "Sanela", "Ajdin"};

Iterating Arrays

Both fixed and dynamic arrays support for-each:

var data:i32[3] = {10, 20, 30};
for (var x in data) {
    print("{x}\n");
}

var items:array<str> = {"a", "b", "c"};
for (var item in items) {
    print("{item}\n");
}

Dynamic Array Methods

MethodDescription
.push(val)Append element
.at(i)Access element at index
.len()Current length
.cap()Current capacity
.sort()Sort (i32, u32, i64)
.clear()Remove all elements
.remove_swap(i)O(1) remove (swaps with last)
.remove_shift(i)O(n) remove (preserves order)
.free()Free backing memory

Collect Expressions

Build a dynamic array from a filtered loop:

var data:i32[6] = {5, 12, 3, 18, 7, 20};
var big = for (x in data) where (x >= 10) collect x;
// big is array<i32> containing {12, 18, 20}
big.free();

Vectors

GX has built-in vector types for 2D, 3D, and 4D math — no imports needed:

TypeComponentsLayout
vec2x, y2 × f32
vec3x, y, z3 × f32
vec4x, y, z, w4 × f32

Construction

var a:vec2 = vec2{1.0, 2.0};
var b:vec3 = vec3{1.0, 2.0, 3.0};
var c:vec4 = vec4{1.0, 2.0, 3.0, 4.0};

Component Access

var v:vec3 = vec3{1.0, 2.0, 3.0};
var px:f32 = v.x;    // 1.0
var py:f32 = v.y;    // 2.0
v.z = 5.0;           // set z component

Arithmetic

Vector arithmetic uses standard runtime helpers:

var a:vec3 = vec3{1.0, 2.0, 3.0};
var b:vec3 = vec3{4.0, 5.0, 6.0};

var d:f32 = dot(a, b);        // dot product → f32
var c:vec3 = cross(a, b);     // cross product → vec3

String Conversion

Vectors support .str() and work in string interpolation:

var v:vec3 = vec3{1.0, 2.0, 3.0};
print("v = {v}\n");  // v = vec3(1, 2, 3)

Matrices

GX has built-in matrix types for 2D, 3D, and 4D transforms:

TypeLayoutColumn type
mat22 × 2vec2
mat33 × 3vec3
mat44 × 4vec4

All matrices are column-major (col[N] of vecN), matching OpenGL conventions.

Construction

var m:mat4 = identity_mat4();    // built-in identity constructor

Column Access

var m:mat4 = identity_mat4();
var first:vec4 = m.col[0];      // first column
var tx:f32 = m.col[3].x;        // translation X

Indexing returns the matching vector type: mat4.col[i] → vec4, mat3.col[i] → vec3, mat2.col[i] → vec2.

String Conversion

Matrices support .str() and string interpolation:

var m:mat2;
print("m = {m}\n");  // mat2([1,0], [0,1])

For transform helpers (perspective, look_at, rotation, etc.), see Math Module.

Operators

Arithmetic

+, -, *, /, %

Comparison

==, !=, <, <=, >, >=

Logical

&&, ||, !

Bitwise

Bitwise operators work on integer types (i8i64, u8u64):

OperatorDescriptionExample
&Bitwise ANDflags & mask
|Bitwise ORflags | FLAG_READ
^Bitwise XORa ^ b
~Bitwise NOT~mask
<<Left shift1 << 4
>>Right shiftval >> 8
var flags:u32 = 0;
var FLAG_READ:u32 = 1 << 0;
var FLAG_WRITE:u32 = 1 << 1;
flags = flags | FLAG_READ;
if (flags & FLAG_READ) {
    print("readable\n");
}
var low_byte:i32 = 0xABCD & 0xFF;      // 205 (0xCD)
var high_byte:i32 = (0xABCD >> 8) & 0xFF;  // 171 (0xAB)

Hex literals are supported: 0xFF, 0xABCD.

Compound Assignment

+=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=

var x:i32 = 10;
x += 5;       // x = 15
x <<= 2;      // x = 60
x &= 0xFF;    // x = 60

Increment / Decrement

++ and -- work as both prefix and postfix operators:

var i:i32 = 0;
i++;           // postfix: evaluates to 0, then i = 1
++i;           // prefix: i = 2, evaluates to 2
i--;           // postfix: evaluates to 2, then i = 1

Ternary Operator

condition ? then_value : else_value

var x:i32 = 10;
var abs:i32 = x >= 0 ? x : -x;
var label:str = x > 5 ? "high" : "low";

Precedence (low to high)

  1. =, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>= (assignment, right-associative)
  2. ? : (ternary)
  3. ||
  4. &&
  5. |
  6. ^
  7. &
  8. ==, !=
  9. <, <=, >, >=
  10. <<, >>
  11. +, -
  12. *, /, %
  13. Unary: -, !, ~, & (address-of), * (deref), ++, --

Constants

Declare compile-time constants with const. The type can be inferred or explicitly annotated:

const PI = 3.14159265358979;
const TAU = PI * 2.0;
const SCREEN_WIDTH = 1920;
const MAX_ENTITIES:i32 = 1024;
const BUFFER_SIZE:i64 = MAX_ENTITIES * 64;

Constant Folding

The compiler evaluates constant expressions at compile time. Supported operations:

  • Arithmetic: +, -, *, /, %
  • Bitwise: &, |, ^, ~, <<, >>
  • Comparison: ==, !=, <, <=, >, >=
  • Logical: &&, ||, !
  • Unary: -, ~, !
  • References to other constants
  • Casts and ternary expressions
const FLAG_READ = 1;
const FLAG_WRITE = 2;
const FLAG_EXEC = 4;
const FLAG_ALL = FLAG_READ | FLAG_WRITE | FLAG_EXEC;  // folded to 7

const IS_WIDE = SCREEN_WIDTH > 1000;  // folded to true
const NEG_ONE = -1;
const NOT_FLAG = ~FLAG_READ;           // folded to -2

Scope

Constants work at both global and local scope:

const GLOBAL_MAX = 100;   // global

fn main() {
    const LOCAL_HALF = GLOBAL_MAX / 2;  // local
    print("{LOCAL_HALF}");
}

Assignment Protection

Assigning to a constant is a compile error:

const X = 10;
X = 20;  // ERROR: Cannot assign to constant

C Output

  • Global constants emit as static const type name = value;
  • Local constants emit as const type name = value;

The transpiler emits the folded literal value, not the original expression tree.

Defer

The defer statement schedules cleanup code to run when the enclosing block exits:

defer print("cleanup\n");
defer nums.free();
  • Multiple defers execute in LIFO (last-in, first-out) order
  • Defers run before return statements — return values are preserved
  • Block-scoped: defers in an if or for body run when that block ends

Allocators

GX provides three built-in allocator types for explicit memory management without new/delete:

Arena

A bump allocator — fast allocation, bulk-free only:

var arena:Arena;
arena.init(4096);
defer arena.destroy();

var p:*Point = arena.alloc(Point);
p.x = 3.14;

arena.mark();           // save position
var tmp = arena.alloc(Point);
arena.reset_to_mark();  // free everything since mark

arena.reset();          // free all allocations

Pool

Fixed-size slot allocator — O(1) alloc and free, ideal for uniform objects:

var pool:Pool;
pool.init(Point, 256);  // 256 slots of sizeof(Point)
defer pool.destroy();

var p:*Point = pool.alloc(Point);
pool.free(p);
pool.reset();  // free all slots

FreeList

Variable-size allocator with free-list coalescing:

var fl:FreeList;
fl.init(65536);
defer fl.destroy();

var p:*Point = fl.alloc(Point);
fl.free(p, Point);  // must specify type for size
fl.reset();
  • All allocators use defer allocator.destroy() for cleanup
  • alloc(T) returns *T — the type argument determines size and alignment
  • No garbage collection — explicit free or bulk reset/destroy

Swizzle Operations

Reorder, duplicate, or extract components from vector types:

var v = vec3{1.0, 2.0, 3.0};
var a = v.xy;    // vec2(1, 2)
var b = v.zyx;   // vec3(3, 2, 1)
var c = v.xx;    // vec2(1, 1)
  • Valid components: x, y, z, w (must exist on the source type)
  • Length 1 → f32, length 2 → vec2, length 3 → vec3, length 4 → vec4
  • Components can repeat: v.xx, v.xxy

Implicit Widening

GX allows safe widening conversions automatically:

  • i8 → i16 → i32 → i64
  • u8 → u16 → u32 → u64
  • f16 → f32 → f64

Example:

var x:i32 = 100;
var y:i64 = x;   // OK

var f:f32 = 3.14;
var d:f64 = f;   // OK

Narrowing conversions require explicit casts.

Casting

var x:f32 = 3.14;
var i:i32 = (i32)x;

Sizeof

sizeof(T) returns the size in bytes of a type or expression as i64. Maps directly to C’s sizeof operator.

// Built-in types
var s1:i64 = sizeof(i32);     // 4
var s2:i64 = sizeof(f64);     // 8
var s3:i64 = sizeof(vec3);    // 12

// User-defined types (via expression)
var p:Point = Point{1.0, 2.0};
var s4:i64 = sizeof(p);       // 8 (two f32 fields)

// In expressions
var buf_size:i64 = 100 * sizeof(f32);

Enums

enum Direction {
    North
    South
    East
    West
}
  • Stored as i32 internally
  • Members separated by newlines (commas optional)
  • Supports explicit values: North = 0

Unions

Unions allow multiple fields to share the same memory — only one field is valid at a time. They map directly to C union types.

union IntFloat {
    i: i32
    f: f32
}

Type Punning

Reinterpret raw bits between types:

var pun: IntFloat;
pun.i = 1065353216;     // IEEE 754 bits for 1.0
print("{pun.f}\n");     // 1.0

Tagged Union Pattern

Pair an enum tag with a union for safe variant storage:

enum ValueKind { IntVal FloatVal StrVal }

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

struct Value {
    kind: ValueKind
    data: ValueData
}

fn run() {
    var v: Value;
    v.kind = IntVal;
    v.data.i = 42;
}

Extern Unions

For C interop, declare external unions:

extern union my_c_union {
    value: i32
    bytes: u8[4]
}
  • Same field syntax as structs: name: Type
  • All fields overlap in memory (size = largest field)
  • Supports pointers, arrays, nested structs
  • Compiles to C union — zero overhead

Slices

A slice []T is a view into contiguous data — {ptr, len} — with no ownership and no copying:

var arr: i32[5] = {10, 20, 30, 40, 50};
var s: []i32 = arr;       // create slice from fixed array

s[0]                       // element access
s.len                      // 5 — O(1) length
s.ptr                      // typed pointer (*i32)
s[1:4]                     // subslice → {20, 30, 40}, zero-copy

Functions can take slices — fixed arrays and dynamic arrays implicitly convert:

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

var arr: i32[3] = {1, 2, 3};
sum(arr);                  // T[N] → []T implicit conversion

String Interpolation

Embed expressions directly in string literals with {expr}:

var name:str = "GX";
var val:i32 = 42;
print("Hello {name}, val={val}\n");
print("sum={10 + 20}\n");

Interpolated expressions are automatically wrapped in .str() calls and concatenated at compile time. This works with any type that supports .str().

Input

The input() built-in reads a line from stdin and returns it as str. An optional string argument is printed as a prompt before reading:

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

var line:str = input();   // no prompt, just reads
  • Returns the line without the trailing newline
  • The prompt (if given) is flushed to stdout before blocking on stdin
  • Max line length: 1023 characters (uses the runtime temp buffer)

Control Flow

if (a > 5) { }
elif (a > 3) { }
else { }
while (a < 10) { }
for (i = 1:10) { }              // Inclusive range
for (var x in arr) { }          // For-each
for (var x in arr) where (x > 3) { }  // Filtered

Break and Continue

Use break to exit a loop early and continue to skip to the next iteration:

for (i = 0:100) {
    if (i > 10) { break; }
    if (i % 2 == 0) { continue; }
    print("{i}\n");
}

Match

The match statement provides clean multi-way branching — no fallthrough, no break needed:

var x:i32 = 2;
match (x) {
    1: print("one\n")
    2: print("two\n")
    3: print("three\n")
    default: print("other\n")
}

Comma-separated patterns match multiple values in one arm:

match (code) {
    1, 2, 3: print("low\n")
    4, 5, 6: print("mid\n")
    default: print("other\n")
}

Range patterns match a contiguous range of integers:

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")
}

Match on enums for clean state/event dispatch:

enum State { Idle Running Stopped }

match (state) {
    Idle: start()
    Running: update()
    Stopped: cleanup()
}

Use braces for multi-statement arm bodies:

match (n) {
    1: {
        print("one ");
        print("(single)\n");
    }
    default: print("other\n")
}

Compiles to C switch/case with auto-break — zero overhead, jump-table eligible.

Collect Expressions

The collect keyword transforms a for-each loop into a value that produces a dynamic array:

var nums:i32[8] = {1, 2, 3, 4, 5, 6, 7, 8};

// Filter: keep only even numbers
var evens = for (n in nums) where (n % 2 == 0) collect n;

// Transform: double each element
var doubled = for (n in nums) collect n * 2;

// Filter + transform
var big = for (n in nums) where (n > 5) collect n * 10;

// Result is array<T> — use .at(), .len(), .free()
print("count={evens.len()}\n");
evens.free();
doubled.free();
big.free();