Slices
A slice is a lightweight view into a contiguous block of memory. It holds a pointer and a length, so any function that takes a slice can work with fixed arrays, dynamic arrays, or sub-ranges — all without copying.
The Slice Type
Slice syntax: []T — “a slice of T”:
fn main() {
var arr:i32[5] = {10, 20, 30, 40, 50}
var s:[]i32 = arr // fixed array auto-converts to slice
print("len = {s.len}\n") // 5
print("s[0] = {s[0]}\n") // 10
print("s[4] = {s[4]}\n") // 50
}
The slice carries its length, so s.len tells you how many elements you can access.
Automatic Conversion
GX auto-converts between array forms at function boundaries. A function that takes []i32 accepts any of the three:
fn sum:i32(values:[]i32) {
var total:i32 = 0
for (var v in values) {
total = total + v
}
return total
}
fn main() {
// Fixed array → slice
var fixed:i32[3] = {1, 2, 3}
print("fixed: {sum(fixed)}\n")
// Dynamic array → slice
var dyn:array<i32>
dyn.init()
defer { dyn.free() }
dyn.push(10)
dyn.push(20)
dyn.push(30)
print("dynamic: {sum(dyn)}\n")
}
The function doesn’t care where the data came from — it just iterates.
Slicing Sub-Ranges
Use [start:end] to take a sub-slice (end is exclusive):
fn main() {
var data:i32[6] = {10, 20, 30, 40, 50, 60}
var first_three:[]i32 = data[0:3] // elements 0, 1, 2
var middle:[]i32 = data[2:5] // elements 2, 3, 4
var last_two:[]i32 = data[4:6] // elements 4, 5
print("first three: ")
for (var v in first_three) { print("{v} ") }
print("\n")
print("middle: ")
for (var v in middle) { print("{v} ") }
print("\n")
}
Sub-slices don’t copy — they just point into the same memory with a different length.
Slices as Function Parameters
Any function that processes “a list of T” should take []T rather than a specific array type:
fn find_max:i32(values:[]i32) {
var max = values[0]
for (var v in values) {
if (v > max) {
max = v
}
}
return max
}
fn main() {
var scores:i32[5] = {72, 95, 68, 87, 91}
print("max: {find_max(scores)}\n") // works with fixed array
var partial:[]i32 = scores[1:4]
print("max (middle 3): {find_max(partial)}\n") // works with sub-slice
}
Slice Methods
Slices support built-in methods like sort:
fn main() {
var nums:i32[6] = {42, 17, 93, 5, 68, 31}
var s:[]i32 = nums
s.sort()
print("sorted: ")
for (var v in s) { print("{v} ") }
print("\n")
}
Note: sort modifies the underlying array in place, since the slice is a view.
String Slices
str itself is essentially a slice — a pointer + length. Splitting a string returns slices into the original:
fn main() {
var csv = "alice,bob,charlie,dave"
var names = csv.split(",")
print("got {names.len} names:\n")
for (var name in names) {
print(" - {name}\n")
}
}
The individual names are slices into the original csv — no copies.
Practical Example: Processing Sub-Ranges
fn average:f32(values:[]f32) {
if (values.len == 0) { return 0.0 }
var sum:f32 = 0.0
for (var v in values) {
sum = sum + v
}
return sum / values.len
}
fn main() {
var temps:f32[7] = {18.5, 21.0, 23.5, 27.0, 29.5, 24.0, 19.5}
var weekday:[]f32 = temps[0:5] // Mon-Fri
var weekend:[]f32 = temps[5:7] // Sat-Sun
print("weekday avg: {average(weekday)}\n")
print("weekend avg: {average(weekend)}\n")
print("whole week: {average(temps)}\n") // whole array too
}
Try it — Write a function
contains:bool(values:[]i32, target:i32)and call it with both a fixed array and a sliced sub-range in the Playground.
Expert Corner
Slice representation: Under the hood, a slice is just { void* ptr; int64_t len } — 16 bytes on 64-bit systems. No length prefix, no capacity, no reference counting. When you pass a slice to a function, you pass those 16 bytes by value; the pointed-to data is not copied.
Why slices over pointer+length pairs: In C, you’d write void process(int* arr, size_t len). Easy to mess up — pass the wrong length, or the wrong pointer, or forget to check for null. Slices bundle the two into one type that the compiler can verify. s.len is always the length of s.ptr, guaranteed.
Auto-conversion rules: The type checker inserts a conversion at the call site:
T[N]→[]T: creates slice with{ arr, N }array<T>→[]T: creates slice with{ arr.data, arr.len }- Sub-range
arr[a:b]→[]T: creates slice with{ &arr[a], b - a }
These conversions are zero-cost — just computing a pointer and storing a length.
Slices don’t own memory: A slice is a borrowed view. If you hold onto a slice after the backing array is freed or goes out of scope, you get a dangling reference. GX doesn’t track lifetimes (no borrow checker), so discipline is on you. Rule of thumb: only pass slices as function parameters, don’t store them in long-lived structs.
Zero-copy string methods: Most str methods return slices into the original string rather than allocating new memory. sub, trim, find, starts_with, ends_with, contains, and split all return views. Only methods that must produce new content — upper, lower, replace, repeat — allocate into a scratch buffer.
Sub-slice bounds: arr[a:b] requires 0 <= a <= b <= len. In debug builds (-O0), GX can emit bounds checks; in release builds, out-of-range slicing is undefined behavior (like C). Use slices as borrowed views, not to bypass safety checks.