Pointers
A pointer is a variable that stores the address of another variable. GX pointers give you C-level control with some safety rules on top.
Taking an Address
Use & to get the address of a variable:
fn main() {
var x:i32 = 42
var p:*i32 = &x
print("x = {x}\n")
print("address stored in p points to x\n")
}
The type *i32 reads as “pointer to i32.” p holds the memory address of x.
Dereferencing
Use * to read or write the value the pointer points to:
fn main() {
var x:i32 = 10
var p:*i32 = &x
print("before: x = {x}\n") // 10
*p = 99 // write through p
print("after: x = {x}\n") // 99
var y = *p // read through p
print("y = {y}\n") // 99
}
*p = 99 writes 99 to the memory location p points to — which is x. So modifying *p modifies x.
Pointers to Structs
For struct fields, GX auto-dereferences with .:
struct Point {
x:f32
y:f32
}
fn main() {
var pt = Point{3.0, 4.0}
var p:*Point = &pt
// No need for (*p).x — just p.x
print("x = {p.x}, y = {p.y}\n")
p.x = 10.0
print("modified: x = {pt.x}\n") // 10.0
}
This matches what you’d write in C with ->, but GX uses . for both cases to reduce syntactic noise.
Read-Only by Default
When you pass a pointer to a function, the function cannot modify it unless you say so:
fn print_point(p:*Point) {
print("({p.x}, {p.y})\n")
// p.x = 0.0 ← COMPILE ERROR: cannot write through read-only pointer
}
fn main() {
var pt = Point{1.0, 2.0}
print_point(&pt)
}
This is the opposite of C, where every T* is mutable by default. GX’s rule: if you don’t say out, nobody’s modifying anything.
The out Keyword for Writable Pointers
To let a function write through a pointer, mark the parameter out:
struct Point {
x:f32
y:f32
}
fn move_point(out p:*Point, dx:f32, dy:f32) {
p.x = p.x + dx
p.y = p.y + dy
}
fn main() {
var pt = Point{0.0, 0.0}
move_point(&pt, 5.0, 3.0)
print("({pt.x}, {pt.y})\n") // (5, 3)
}
Pointers Cannot Be Returned
A function can take pointers but cannot return them. This rule eliminates an entire class of C bugs:
// This function won't compile:
// fn make_point:*Point() {
// var pt = Point{1.0, 2.0}
// return &pt // ← pt is dead after return!
// }
In C, returning &pt where pt is local gives you a dangling pointer. GX refuses to compile such code. To create and hand back a value, return the value itself:
fn make_point:Point() {
return Point{1.0, 2.0}
}
fn main() {
var pt = make_point()
print("({pt.x}, {pt.y})\n")
}
For large data where copying is expensive, use an out parameter:
fn fill_point(out p:*Point) {
p.x = 10.0
p.y = 20.0
}
fn main() {
var pt:Point
fill_point(&pt)
print("({pt.x}, {pt.y})\n")
}
Pointer Indexing and Arithmetic
Pointers can be indexed like arrays, and you can add integer offsets:
fn main() {
var arr:i32[4] = {10, 20, 30, 40}
var p:*i32 = &arr[0]
print("p[0] = {p[0]}\n") // 10
print("p[2] = {p[2]}\n") // 30
var q:*i32 = p + 1
print("*q = {*q}\n") // 20
}
Try it — Write a function that takes
out value:f32and doubles it. Call it from main and print the result in the Playground.
Expert Corner
Why p.x and not p->x: C uses . for values and -> for pointers. GX unifies them — the compiler picks the right one based on type. This is pure syntactic sugar: p.x on a *Point generates p->x in C, pt.x on a Point generates pt.x. You never think about which operator to use.
The const-by-default rule and how it maps to C: fn foo(p:*T) becomes void foo(const T* p) in generated C. fn foo(out p:*T) becomes void foo(T* p). The out keyword is a GX-level concept enforced by the type checker — the generated C just drops the const.
Why GX forbids returning pointers: The single most common bug in C is returning &local_variable. The stack frame dies the moment the function returns, and the pointer becomes garbage. Crashes happen later, far from the cause, making them nearly impossible to debug. GX eliminates this bug class at the type checker level — no function can declare a pointer return type, full stop.
Pointer arithmetic safety: GX allows p + N and p[i] on pointers, matching C semantics. There’s no bounds checking on raw pointers — that’s the whole point of being a low-level language. If you want bounds-checked access, use fixed arrays (with -O0 bounds checks), slices (which carry length), or array<T> (dynamic array with .at(i)).
When to use pointers vs values: Pass by value for primitives and small structs (≤ 16 bytes typically). Pass by pointer for larger structs to avoid copying. Pass out pointers when the function needs to modify the caller’s data. GX structs are value types — assigning a struct copies it, which is usually what you want for correctness but costs memory bandwidth for large types.