Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

  • []T coerces to []const T (safe: giving up write permission)
  • []const T does NOT coerce to []T (would allow writing read-only data)
  • *T does 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]TSlice []T
SizeFixed, compile-timeRuntime length
MemoryInline (stack)Fat pointer (ptr + len)
OwnershipOwns dataBorrows 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

OperationExampleResult
Lengths.lenusize
Raw pointers.ptr*T or *const T
Indexs[i]Element T
Subslices[a..b][]T or []const T
String literal"hello"[]const u8