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
| Method | Description |
|---|---|
.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:
| Type | Components | Layout |
|---|---|---|
vec2 | x, y | 2 × f32 |
vec3 | x, y, z | 3 × f32 |
vec4 | x, y, z, w | 4 × 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:
| Type | Layout | Column type |
|---|---|---|
mat2 | 2 × 2 | vec2 |
mat3 | 3 × 3 | vec3 |
mat4 | 4 × 4 | vec4 |
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 (i8–i64, u8–u64):
| Operator | Description | Example |
|---|---|---|
& | Bitwise AND | flags & mask |
| | Bitwise OR | flags | FLAG_READ |
^ | Bitwise XOR | a ^ b |
~ | Bitwise NOT | ~mask |
<< | Left shift | 1 << 4 |
>> | Right shift | val >> 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)
=,+=,-=,*=,/=,%=,&=,|=,^=,<<=,>>=(assignment, right-associative)? :(ternary)||&&|^&==,!=<,<=,>,>=<<,>>+,-*,/,%- 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
returnstatements — return values are preserved - Block-scoped: defers in an
iforforbody 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
freeor bulkreset/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
i32internally - 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();