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 (like const Counter* in C)
  • out self = writable (like Counter* 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 Rect struct and add extension methods contains(x, y) (checks if a point is inside) and intersects(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.