Extension Methods
Extension methods let you attach functions to struct types so they look like methods. GX has no classes or inheritance — this is how you do “methods” without OOP.
Basic Extension Block
Use ex TypeName { ... } to add methods:
struct Player {
name:str
health:i32
score:i32
}
ex Player {
fn is_alive:bool(self) {
return self.health > 0
}
fn add_score(out self, points:i32) {
self.score = self.score + points
}
}
fn main() {
var p = Player{"Alice", 100, 0}
p.add_score(50)
print("{p.name} score: {p.score}\n")
if (p.is_alive()) {
print("{p.name} is alive\n")
}
}
Inside ex Player, every function takes self as an implicit first parameter. You call them with dot syntax: p.is_alive() instead of is_alive(&p).
self vs out self
The receiver follows the same rules as regular pointer parameters:
struct Counter {
value:i32
}
ex Counter {
// Read-only — cannot modify self
fn get:i32(self) {
return self.value
}
// Mutable — can modify self
fn increment(out self) {
self.value = self.value + 1
}
fn reset(out self) {
self.value = 0
}
}
fn main() {
var c = Counter{0}
c.increment()
c.increment()
c.increment()
print("count: {c.get()}\n") // 3
c.reset()
print("after reset: {c.get()}\n") // 0
}
self= read-only view (likeconst Counter*in C)out self= writable (likeCounter*in C)
Method Chaining Style
Since methods just take self, you can organize complex operations cleanly:
struct Vec3 {
x:f32
y:f32
z:f32
}
ex Vec3 {
fn length_sq:f32(self) {
return self.x * self.x + self.y * self.y + self.z * self.z
}
fn scale(out self, factor:f32) {
self.x = self.x * factor
self.y = self.y * factor
self.z = self.z * factor
}
fn set(out self, x:f32, y:f32, z:f32) {
self.x = x
self.y = y
self.z = z
}
}
fn main() {
var v:Vec3
v.set(3.0, 4.0, 0.0)
print("length^2 = {v.length_sq()}\n") // 25
v.scale(2.0)
print("scaled length^2 = {v.length_sq()}\n") // 100
}
Extensions Across Files
You can put the ex block in a different file from the struct definition — even in a different module — as long as both are imported:
// vec.gx
struct Vec3 {
x:f32
y:f32
z:f32
}
// vec_math.gx
ex Vec3 {
fn length_sq:f32(self) {
return self.x * self.x + self.y * self.y + self.z * self.z
}
}
This is how the GX math module adds length, normalize, dot, etc. to the built-in vec2/vec3/vec4 types.
Multiple Extension Blocks
You can have more than one ex block per type, splitting methods by concern:
struct Shape {
x:f32
y:f32
width:f32
height:f32
}
ex Shape {
fn area:f32(self) {
return self.width * self.height
}
fn perimeter:f32(self) {
return 2.0 * (self.width + self.height)
}
}
ex Shape {
fn move_by(out self, dx:f32, dy:f32) {
self.x = self.x + dx
self.y = self.y + dy
}
fn resize(out self, w:f32, h:f32) {
self.width = w
self.height = h
}
}
fn main() {
var s = Shape{0.0, 0.0, 10.0, 5.0}
print("area = {s.area()}\n")
s.move_by(3.0, 4.0)
print("position = ({s.x}, {s.y})\n")
}
Practical Example: Stack Implementation
struct Stack {
items:i32[100]
count:i32
}
ex Stack {
fn push(out self, value:i32) {
if (self.count < 100) {
self.items[self.count] = value
self.count = self.count + 1
}
}
fn pop:i32(out self) {
if (self.count > 0) {
self.count = self.count - 1
return self.items[self.count]
}
return 0
}
fn is_empty:bool(self) {
return self.count == 0
}
fn size:i32(self) {
return self.count
}
}
fn main() {
var s:Stack
s.count = 0
s.push(10)
s.push(20)
s.push(30)
print("size: {s.size()}\n") // 3
print("pop: {s.pop()}\n") // 30
print("pop: {s.pop()}\n") // 20
print("size: {s.size()}\n") // 1
}
Try it — Create a
Rectstruct and add extension methodscontains(x, y)(checks if a point is inside) andintersects(other)in the Playground.
Expert Corner
What extensions compile to: An extension method fn foo(self) on type Point becomes a C function named Point_foo(const Point* self). Calling p.foo() becomes Point_foo(&p). It’s pure syntactic sugar over name-mangled free functions. No vtable, no dispatch overhead, no hidden allocations.
Why ex blocks instead of methods inside structs: Keeping methods separate from struct definitions lets you add methods to types you don’t own — including the built-in vec2/vec3/vec4 types and types from imported modules. The math module extends vec3 with length, normalize, dot in a separate file. In Java or C++ you’d need inheritance, mixins, or wrapper classes. GX gives you the same capability with a 2-line ex block.
No inheritance by design: GX has no subclassing, no virtual methods, no abstract base classes. Every method call is a direct function call resolved at compile time. This keeps the language simple and the runtime fast, at the cost of some polymorphism flexibility. For runtime polymorphism, use function pointers in a struct (a C-style vtable) — explicit and under your control.
self is a pointer, not a reference: self inside an ex block is *T (or const *T without out). You access fields with self.x but this is auto-deref — it becomes self->x in the generated C. You can take &self to pass the pointer to another function, useful for helpers.
Value receiver vs pointer receiver: All GX extension methods use pointer receivers (the receiver is *T). There’s no “value receiver” option — that would copy the struct on every call. For small types where copying is cheap, modern C compilers often inline the pointer dereference anyway, so there’s no performance difference.
Method resolution: When you write p.foo(), the compiler searches all ex Point blocks for foo. If multiple match, it’s a compile error. If none match, it’s “unknown method.” There’s no inheritance walk — just a flat lookup in the extension table for the exact type.