Skip to content

SIMD, arrays, and pointers

API summary (generated from the Beans source by npm run coverage): 7 types · 6 static methods · 32 instance methods.

This page covers the low-level builtins: SIMD vectors, fixed-size arrays, slices, and raw pointers. These are for tight, hardware-close code.

SIMD (“single instruction, multiple data”) does the same math on several numbers at once. Each SIMD type packs a fixed number of equal-typed numbers, called lanes.

A SIMD type is named Simd + lane count + element type, for example:

  • Simd4i32: 4 lanes of i32
  • Simd16u8: 16 lanes of u8
  • Simd2f64: 2 lanes of f64
  • Simd4f32: 4 lanes of f32

Element types are i8/i16/i32/i64, their u forms, and f32/f64. The lane count is a power of two. Total width is 128 bits everywhere. A 256-bit width needs CPU features, for example Simd8i32 needs --features +avx2.

ConstructorNotes
splat(x)every lane set to x
of(...)one value per lane
load(ptr)read lanes from memory
load_unaligned(ptr)read from memory that may not be aligned
MethodReturnsNotes
lane(i)elementread lane i
with_lane(i, v)vectora copy with lane i set to v
lane_count()inthow many lanes

Arithmetic works lane by lane. You can use the operators + - * / or the named methods add, sub, mul, div, plus min and max.

Integer families also have bit_and, bit_or, bit_xor, bit_not, shl, and shr.

The comparisons eq, ne, lt, le, gt, ge return a mask (a per-lane true/false). With a mask you can:

  • mask.select(a, b): pick from a where true, else b
  • mask.any_true(): is any lane true
  • mask.all_true(): are all lanes true
MethodNotes
sum()add all lanes
product()multiply all lanes
store(ptr)write lanes to memory
store_unaligned(ptr)write to memory that may not be aligned

A SIMD value cannot be a Map key: it has no Hash.

[T; N] is a fixed inline array of N items of type T, where 1 <= N <= 4096. Unlike a List, it is not a handle: it copies by value.

N is an integer literal — decimal, hex, binary, digit separators and all — or a module constant that folds to an integer in that range, so const LIMIT: int = 128 lets you write [int; LIMIT]. A const cannot be a parameter default, which is read at a different stage.

  • Indexing is checked (panics if out of range).
  • You can assign an element when the binding is var.
  • array.len() -> int gives N.
  • array.get(int) -> Option<T> returns a copyable element when present and none for a negative or out-of-range index.
  • Two arrays compare equal with ==.
  • You can loop over one with for.

A stable for loop reads the inline array directly when its item binding cannot escape. If the loop can change the array, the compiler uses a snapshot.

A list literal takes on fixed-array meaning from the declared type:

var lanes: [f32; 4] = [1, 2, 3, 4]
lanes[0] = 9
let n: int = lanes.len()

Slice<T> is a non-owning view: a pointer plus a length. It does not own the data it points at. All of its operations require unsafe and are bounds-checked.

Slice.from_raw(ptr, len)
Slice<T>.get(int) -> Option<T>
Slice<T>.set(int, T)
Slice<T>.subslice(int, int) -> Slice<T>
Slice<T>.as_ptr() -> RawPtr<T>
Slice<T>.len() -> int
  • Slice.from_raw(ptr, len) makes a slice over len items at ptr. A non-empty slice rejects a null pointer.
  • get(i) returns some(item) when present and none for a negative or out-of-range index. s[i] requires the element and panics out of range.
  • set(i, v) and s[i] = v write an existing item and panic out of range.
  • get still requires unsafe: an in-range index cannot prove that the allocation is alive. To migrate a required read from 0.1.53, use s[i].
  • subslice(from, to) is a smaller view over the same storage.
  • as_ptr() hands back the underlying pointer; len() is the number of items.
  • You can loop over a slice with for.
unsafe {
let view: Slice<i32> = Slice.from_raw(ptr, 4)
let first: i32 = view[0]
}

RawPtr<T> is a raw pointer to memory of type T. Every use is inside an unsafe {} block. This is a brief list; see the unsafe guide for the full detail and the rules you must follow.

Statics make or name a pointer:

RawPtr.alloc(count)
RawPtr.alloc_aligned(count, align)
RawPtr.null()
RawPtr.from_address(u64)
RawPtr.with_local(inout local, fn(RawPtr<T>))
  • alloc(count) allocates room for count items; alloc_aligned(count, align) does the same with a chosen alignment.
  • null() is a null pointer; from_address(u64) is a pointer at a raw address.
  • with_local(inout local, fn(RawPtr<T>)) runs your function with a raw pointer to a stack local. The pointer is valid only for that call.

The instance methods:

RawPtr<T>.read() -> T
RawPtr<T>.write(T)
RawPtr<T>.read_volatile() -> T
RawPtr<T>.write_volatile(T)
RawPtr<T>.offset(int) -> RawPtr<T>
RawPtr<T>.address() -> u64
RawPtr<T>.is_null() -> bool
RawPtr<T>.element_size() -> int
RawPtr<T>.element_align() -> int
RawPtr<T>.copy_from(RawPtr<T>, int)
RawPtr<T>.fill_zero(int)
RawPtr<T>.free()
RawPtr<T>.atomic_load() -> T
RawPtr<T>.atomic_store(T)
RawPtr<T>.atomic_fetch_add(T) -> T
RawPtr<T>.atomic_compare_exchange(T, T) -> bool
  • read/write move a value in and out; the _volatile pair does the same without letting the compiler reorder or drop the access.
  • offset(n) moves n items along; address() is the raw address and is_null() tells you whether the pointer is null.
  • element_size() and element_align() are the size and alignment of T.
  • copy_from(src, n) copies n items from src; fill_zero(n) zeros n items; free() releases memory that came from alloc.
  • The atomic_* methods do sequentially consistent atomic access through the pointer.
unsafe {
let p: RawPtr<i32> = RawPtr.alloc(4)
p.write(7)
let x: i32 = p.read()
p.free()
}

RawSlice is the untyped, two-word {pointer, length} value that backs a Slice<T>. Slice<T> is the typed view you normally use; RawSlice is the element-agnostic form the compiler and low-level code use for the same pointer-plus-length shape. Like the other raw types it is only meaningful inside unsafe, and size_of(RawSlice) folds to two pointers for the selected target. Prefer Slice<T> in your own code, it carries the element type and bounds-checks its accesses.

Four f32 lanes multiplied and added, then stored through a RawPtr and read back. Everything low-level here sits inside unsafe:

import std.io
fn main() {
unsafe {
let source: Simd4f32 = Simd4f32.of(1.0, 2.0, 3.0, 4.0)
let scale: Simd4f32 = Simd4f32.splat(2.0)
let result: Simd4f32 = source * scale + source
io.println("lane0 {result.lane(0)} sum {result.sum()}")
let memory: RawPtr<f32> = RawPtr.alloc(4)
result.store(memory)
let view: Slice<f32> = Slice.from_raw(memory, 4)
io.println("view len {view.len()} first {view[0]}")
memory.free()
}
}