Language Tour

Syntax Basics

Semicolons are optional. You can use them if you prefer, but GX does not require them:

var a = 10          // fine
var b:f32 = 3.14;   // also fine

Variables

var a = 10          // i32
var b:f32 = 3.14
var name:str = "GX"

Constants

const PI = 3.14159265358979
const TAU = PI * 2.0
const MAX_ENTITIES:i32 = 1024
const FLAG_ALL = FLAG_READ | FLAG_WRITE | FLAG_EXEC

Constants are evaluated at compile time (constant folding). The type can be inferred or explicit. Assignment to a const is a compile error.

Slices

Views into contiguous data — no copying:

var arr: i32[5] = {10, 20, 30, 40, 50};
var s: []i32 = arr;
print("len={s.len}\n");    // 5
var mid: []i32 = s[1:3];   // {20, 30} — zero-copy

Strings

str is a fat string — carries pointer + length for O(1) .len access and value-based == comparison:

var s:str = "hello"
print("length: {s.len}\n")    // 5
s.cstr                         // raw const char* for C interop

String Methods

Built-in methods on str — most are zero-copy (no allocation):

var s = "  Hello, World!  "

// Search
s.find("World")          // 7 (byte index, or -1)
s.find_last("l")         // 10
s.contains("Hello")      // true
s.starts_with("  He")    // true
s.ends_with("!  ")       // true
s.count("l")             // 3

// Slice & trim (zero-copy)
s.sub(2, 5)              // "Hello"
s.sub(9)                 // "World!  " (to end)
s.trim()                 // "Hello, World!"
s.trim_left()            // "Hello, World!  "
s.trim_right()           // "  Hello, World!"

// Transform (uses scratch buffer)
s.upper()                // "  HELLO, WORLD!  "
s.lower()                // "  hello, world!  "
s.replace("World", "GX") // "  Hello, GX!  "
"-".repeat(20)           // "--------------------"

// Split
var parts = "a,b,c".split(",")  // ["a", "b", "c"]
parts.len                        // 3
for (var p in parts) { print(p) }

String Interpolation

Use {expr} inside string literals to embed expressions:

var x:i32 = 42;
var name:str = "GX";
print("Hello {name}, value={x}\n");
print("2+3={2 + 3}\n");

Expressions are automatically converted to strings via .str().

Input

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

var line:str = input();   // no prompt

input() reads a line from stdin (without the trailing newline). The optional string argument is a prompt.

Functions

fn add:i32(x:i32, y:i32) {
    return x + y;
}

Structs

struct Point {
    x:f32
    y:f32
}

Extensions

ex Point {
    fn length:f32() {
        return self.x * self.x + self.y * self.y;
    }
}

Enums

enum Color {
    Red
    Green
    Blue
}

Members are separated by newlines (commas optional). Stored as i32.

Loops

for (i = 1:5) { }                           // Inclusive range
for (var x in arr) { }                      // For-each
for (var x in arr) where (x > 3) { }       // Filtered for-each
par for (i = 0:9) { }                       // Parallel (OpenMP)

Collect (List Comprehensions)

Build a dynamic array from a filtered/transformed loop:

var nums:i32[6] = {1, 2, 3, 4, 5, 6};
var evens = for (n in nums) where (n % 2 == 0) collect n;
var doubled = for (n in nums) where (n <= 3) collect n * 10;
evens.free();
doubled.free();

The result is an array<T> (dynamic array) that must be freed when done.

Defer

fn process() {
    var nums:array<i32>;
    nums.init();
    defer nums.free();    // Runs at block exit, LIFO order

    nums.push(10);
    nums.push(20);
    return;               // defer fires before return
}

Vectors & Matrices

GX has built-in f32-based vector and matrix types — no imports needed:

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

var m:mat4 = identity_mat4();

// Component access
var px:f32 = a.x;
var col:vec4 = m.col[0];

// Built-in math
var d:f32 = dot(a, a);
var c:vec3 = cross(a, a);

// String interpolation works
print("a = {a}\n");    // a = vec3(1, 2, 3)

Types: vec2, vec3, vec4, mat2, mat3, mat4. Matrices are column-major.

Swizzle

var v = vec3{1.0, 2.0, 3.0};
var xy = v.xy;      // vec2(1, 2)
var rev = v.zyx;    // vec3(3, 2, 1)
var dup = v.xx;     // vec2(1, 1)

Components: x, y, z, w. Result type depends on swizzle length.

Modules

module util;
import util;
pub fn hello() {}

Module Resolution

Dotted module names map directly to directory structures.

Example:

import sokol.app

Resolves to:

./sokol/app/

All .gx files inside that directory are loaded as part of the module.

The compiler searches for modules starting from the current working directory and any paths specified with -I.

Import Loading

Imports are loaded using BFS (breadth-first) ordering:

  1. The entry file is parsed first.
  2. Its import statements are queued.
  3. Each imported module’s .gx files are parsed and their imports queued.
  4. This continues until all transitive dependencies are loaded.

All collected AST nodes are then resolved and type-checked together, so cross-module references (e.g., enum members defined in an imported module) are visible everywhere.

Module Package Layout

A typical module package (e.g., Sokol bindings) looks like:

modules/
  sokol/
    gx/
      app.gx          // module sokol.app (extern declarations)
      gfx.gx          // module sokol.gfx (extern declarations)
    c/
      sokol_app.h     // C headers
      sokol_gfx.h
      sokol_impl.c    // C implementation

Build with:

gx examples/triangle/input.gx -I modules --cfile modules/sokol/c/sokol_impl.c