Slices
A slice is a view into a contiguous sequence of elements: a pointer plus a length. Slices don’t own memory — they’re lightweight references to data that lives elsewhere (a string literal, an array, a heap allocation).
Slice Types
[]u8 // mutable slice of bytes
[]const u8 // immutable slice of bytes (read-only)
[]i32 // mutable slice of 32-bit integers
[]const i32 // immutable slice of integers
[]T is a mutable slice — you can read and write elements. []const T is an immutable slice — you can only read. A []T coerces to []const T (giving up write permission), but not the reverse.
String Literals
String literals are []const u8 — an immutable slice of UTF-8 bytes:
let greeting = "hello" // type: []const u8
greeting.len // 5
greeting[0] // 104 (ASCII 'h')
There’s no special String type for literals. A string literal is just bytes with a known length.
Built-in Fields
Slices have two built-in fields:
let s = "hello world"
s.len // usize — number of elements
s.ptr // *const u8 — raw pointer to the first element
.len returns the number of elements as usize. .ptr returns a raw pointer to the underlying data — use this for FFI interop.
Indexing
Use [i] to access individual elements:
let s = "hello"
s[0] // 104 ('h')
s[4] // 111 ('o')
s[s.len - 1] // 111 ('o') — last element
Indexing on []const T returns the element value. You cannot assign through a const slice:
let s = "hello"
s[0] = 65 // ERROR: cannot write through const pointer/slice
Subslicing (Range Syntax)
Use [start..end] to create a subslice:
let s = "hello world"
let hello = s[0..5] // []const u8, "hello"
let world = s[6..11] // []const u8, "world"
hello.len // 5
world[0] // 119 ('w')
The subslice shares the underlying data — no copy is made. The new slice is { ptr + start, end - start }.
Passing Slices to Functions
Slices are value types (pointer + length, two machine words). Pass them by value:
fn byte_count(s: []const u8) -> i32
s.len as i32
byte_count("hello") // 5
byte_count("hello world") // 11
Use []const T for read-only parameters. Use []T when the function needs to modify elements.
FFI Interop
For C functions that take a pointer and length separately, extract .ptr and .len:
// write(2) expects a raw pointer and byte count
write(1, s.ptr, s.len)
The .ptr field on []const T returns *const T. On []T it returns *T.
Creating Slices from Pointers
When working with C APIs that return raw pointers, use range syntax to create a slice:
extern fn strlen(s: *const u8) -> usize
// argv[0] is a *u8 (C string, null-terminated)
let name_ptr = argv[0]
let name = name_ptr[0..strlen(name_ptr)] // []const u8
This is the explicit boundary between “raw pointer world” (no length, no safety) and “slice world” (known length, safe indexing).
Coercion Rules
[]Tcoerces to[]const T(safe: giving up write permission)[]const Tdoes NOT coerce to[]T(would allow writing read-only data)*Tdoes NOT implicitly become[]T(must use range syntax with explicit length)
Many-Item Pointers ([*]T)
[*]T is a many-item pointer — a Perceus-managed (reference counted) heap allocation. It’s the ONE managed pointer type. Created by ore_alloc:
fn alloc(count: i32) -> [*]a
ore_alloc(count * @sizeof(a)) as [*]a
Creating Slices from [*]T
Use range syntax to create a slice view of managed memory:
let buf: [*]u8 = alloc(100)
let view: []u8 = buf[0..50] // slice view into buf
The slice borrows from the [*]T — Argus (the escape checker) ensures the slice doesn’t outlive the buffer.
Ownership Model
[*]T → owns memory (Perceus RC, heap allocated)
[]T → views memory (ptr + len, no ownership, 2 words)
*T → raw pointer (no ownership, no length, 1 word)
[*]T is the owner. []T is a view. You can create views from owners, but views cannot escape the owner’s scope.
Library Convention
Functions that allocate often return a named product with the owner and size:
read_file :: fn(path: []const u8) -> !.{ .buf: [*]u8, .size: i32 }
// ...
.{ .buf = buf, .size = size }
file := try read_file("input.txt")
view := file.buf[0..file.size as usize]
The caller creates views as needed. The [*]T is dropped when the last reference dies.
Arrays vs Slices
Array [N]T | Slice []T | |
|---|---|---|
| Size | Fixed, compile-time | Runtime length |
| Memory | Inline (stack) | Fat pointer (ptr + len) |
| Ownership | Owns data | Borrows data |
| Syntax | [i32; 5] | []i32 |
Arrays have a fixed size known at compile time. Slices have a runtime-known length and don’t own the underlying memory.
Summary
| Operation | Example | Result |
|---|---|---|
| Length | s.len | usize |
| Raw pointer | s.ptr | *T or *const T |
| Index | s[i] | Element T |
| Subslice | s[a..b] | []T or []const T |
| String literal | "hello" | []const u8 |