Docs · Standard library
`@core`
@core is the standard library's core package. It ships in two tiers:
| Tier | Names | How you get them |
|---|---|---|
| Prelude | Optional, Result, Array, ArrayPointer, Range | Always in scope — no import. These are the types behind the language's type sugar (?T, E!T, [*]T, a..b), which has no import site to hang a name on. |
| Import-required | String, Vec, HashMap (plus Entry, cPtrOf) | import { String, Vec, HashMap } from "@core" |
This page is the API catalog. For the tutorial treatment read guide 07 — Optionals and errors and guide 10 — Collections and strings.
Prelude types
Optional(T) — ?T
enum Optional(T) { None, Some(T) }?T desugars to Optional(T); instantiation is memoized, so the two spellings
are the same type and values flow freely between them. Some and None are
ordinary enum variants — not keywords — resolved bare against the expected type,
exactly as any enum's variants are. Construction is always explicit: a T never
silently becomes a Some(T).
Result(E, T) — E!T and !T
enum Result(E, T) { Ok(T), Err(E) }E!T desugars to Result(E, T); bare !T desugars to Result(_, T) with the
error set inferred as the minimal union of what the body produces and propagates.
Any type can sit on the error side, and the error travels in the Err payload
by value.
Both enums are consumed with switch, if-bindings, try (propagate), and
catch (handle) — the precise rules live in the
error-model reference. A round trip through the
surface:
import { stdout } from "@io"
fn pick(n: i32) ?i32 {
if n == 0 { return None }
return Some(n * 2)
}
fn main() !u8 {
con a = pick(3) catch 0 // 6
con o: Optional(i32) = pick(0) // ?i32 and Optional(i32) are the same type
con r: Result(bool, i32) = Ok(a) // bool!i32, spelled out
mut out = 0
if Ok(v) = r { out = v }
switch o {
Some(v): { out = out + v },
None: { out = out + 1 },
}
stdout.writeInt(i64(out)) // 7
stdout.write("
")
return Ok(0)
}ArrayPointer(T) — [*]T
The runtime-sized contiguous view: a pointer plus an element count in one value.
[*]T desugars to ArrayPointer(T), and the two spellings interchange freely.
String literals, allocator results, and every slice are [*]T values.
Fields (both pub):
| Field | Type | Meaning |
|---|---|---|
ptr | c_ptr T | raw pointer to the first element |
len | i32 | element count |
Methods and operators (xs[i], xs[i] = v, xs[lo..hi], a == b desugar to
the @-prefixed operator methods):
| Surface | Signature | Behavior |
|---|---|---|
xs.len / xs.len() | fn (xs: [*]T) len() i32 | element count — the field and the method both work |
xs[i] | fn (xs: [*]T) @index(i: i32) T | bounds-checked read; panics out of bounds |
xs[i] = v | fn (xs: [*]T) @setIndex(i: i32, v: T) | bounds-checked write |
xs[lo..hi] | fn (xs: [*]T) @slice(r: Range) [*]T | zero-copy sub-view of the same storage; empty ranges allowed |
a == b | fn (a: [*]u8) @eq(b: [*]u8) bool | content equality — same length and same bytes. Byte views only; there is no element-generic == on [*]T |
| — | fn (ArrayPointer(T)) of(ptr: c_ptr T, len: i32) [*]T | the trusted entry point pairing raw memory with a length |
| — | fn cPtrOf(known T: type, p: *mut T | [*]T) c_ptr T | the dual exit point: extract the raw pointer for FFI. Import-required (import { cPtrOf } from "@core") — only the prelude type names are ambient |
Negative indices and bounds count from the end throughout: xs[-1] is the last
element, xs[-3..] the last three, xs[1..-1] drops the first and last. Open
range endpoints default at the index site — start to 0, end to xs.len:
import { stdout } from "@io"
fn main() !u8 {
con b = "hello world" // a [*]u8 view of baked bytes
con head = b[..5] // "hello"
con tail = b[6..] // "world"
con last3 = b[-3..] // "rld" — negative bounds count from the end
con h = b[0] // 104 — the byte 'h'
mut acc = i32(h) - i32(h)
if head == "hello" && tail == "world" { acc = 1 }
stdout.writeInt(i64(b.len + head.len() + acc)) // 11 + 5 + 1
stdout.write("
")
return Ok(0)
}[*]T satisfies the iteration protocol (@index plus len()), so
for x in xs walks its elements.
ArrayPointer(T).of is the one sanctioned way to turn raw memory into a [*]T —
every call site is the programmer asserting "these len Ts at ptr are valid
storage", which keeps the trust boundary greppable:
import { systemAllocator, ArenaAllocator } from "@mem"
import { cPtrOf } from "@core"
fn run() i32 {
mut arena = ArenaAllocator.new(systemAllocator)
con buf = arena.new(u8, 4) catch { panic "oom" }
con raw = cPtrOf(u8, buf) // out to the raw pointer…
con view = ArrayPointer(u8).of(raw, 4) // …and back to a checked view
return view.len
}
fn main() !u8 {
run()
return Ok(0)
}Array(T, N) and fixed arrays
Array(T, N) is the prelude's named fixed-size container: N consecutive T
values, with N part of the type. Its one method is len(), which folds to the
literal N at compile time because N is a known parameter of the type.
The language-level fixed-array form [N]T — literals [4]i32[1, 2, 3, 4],
[N]T[] zero-fill, [_]T[…] length inference, indexing, iteration, and the
casts between [N]T, *[N]T, and [*]T — is covered in the
types reference. [N]T and
Array(T, N) are distinct types: a [3]i32 value does not flow into an
Array(i32, 3) slot, and [N]T values do not have the len() method (their
length is a known value you already have from the type).
import { stdout } from "@io"
fn main() !u8 {
con a = [4]i32[10, 20, 30, 40] // typed literal: [N]T then the elements
mut b = [2]i32[] // empty element list zero-fills
b[0] = a[1] // indexed read and write
mut sum = 0
for x in a { sum = sum + x } // fixed arrays iterate: 100
stdout.writeInt(i64(sum + b[0] - 100)) // 20
stdout.write("
")
return Ok(0)
}Range
struct Range { pub start: i32, pub end: i32, pub inclusive: bool = false }The value behind every range expression — a..b, a...b, and the open forms
..b, a.., ... The endpoints are stored as written, and inclusive
records which token built the range, so a bound range means exactly what the
inline spelling means. Consumers read inclusive to decide whether end
itself is in the interval; open ends fill in at the use site as exclusive.
import { stdout } from "@io"
fn main() !u8 {
con r = 1..4 // Range{ start: 1, end: 4, inclusive: false }
con ri = 1...4 // Range{ start: 1, end: 4, inclusive: true }
con width = r.end - r.start // 3
mut sum = 0
for i in 2..5 { sum = sum + i } // 2 + 3 + 4 = 9
con xs: [*]u8 = "abcdef"
con mid = xs[r] // a Range value works at an index site
stdout.writeInt(i64(width + sum + mid.len + ri.end))
stdout.write("
")
return Ok(0)
}Two boundaries:
- Open endpoints need context. The defaults (start
0, endsubject.len)
are filled at an indexing site; a standalone con r = 0.. has nothing to
default against and is rejected.
foriterates range syntax, notRangevalues.for i in a..bwalks
a, a+1, …, b-1, but a Range bound to a name does not satisfy the
iteration protocol (it has no @index or len() method), so
for i in r is rejected. Use the range expression directly in the loop
header, or index a container with the value (xs[r]).
Import-required containers
String, Vec(T), and HashMap(K, V) have no sugar, so they are imported like
anything else:
import { String, Vec, HashMap } from "@core"All three are allocator-backed: you hand one an allocator at construction and
every byte it owns comes from there (see the @mem reference and
guide 09 — Pointers and memory).
Failure behavior: none of the container methods return an error union. On
allocation failure they panic with "out of memory" — the fallible channel is
at the allocator level (Allocator.new returns OutOfMemory![*]T), and the
containers settle it internally. The only optional in the surface is
HashMap.get, which returns ?V for a missing key.
String
Allocator-backed UTF-8 text, presented as a sequence of Unicode codepoints.
String literals are not Strings — they are [*]u8 byte views; construct an
owned String by copying one in with fromUtf8.
struct String { pub bytes: [*]u8, pub alloc: Allocator }Construction:
| Function | Signature | Behavior |
|---|---|---|
String.new | fn (String) new(alloc: Allocator) String | empty string backed by alloc — a starting accumulator |
String.fromUtf8 | fn (String) fromUtf8(alloc: Allocator, src: [*]u8) String | copies src into fresh alloc-owned memory. Trusted: the caller asserts the bytes are valid UTF-8 |
Operators (indexing is in codepoint units; negative indices and bounds count from the end):
| Surface | Signature | Behavior |
|---|---|---|
s[i] | fn (s: String) @index(i: i32) char | the i-th codepoint. O(i) — decodes from the start |
s[lo..hi] | fn (s: String) @slice(r: Range) String | zero-copy view of a codepoint range; shares storage and allocator with the source |
s == t | fn (s: String) @eq(other: String) bool | byte equality — no Unicode normalisation |
s + t | fn (s: String) @add(other: String) String | concatenation into a fresh buffer from s.alloc — which is why a String carries its allocator |
| — | fn (s: String) @hash() u32 | FNV-1a over the bytes; lets String be a HashMap key |
import { String } from "@core"
import { ArenaAllocator, systemAllocator } from "@mem"
fn run() i32 {
mut arena = ArenaAllocator.new(systemAllocator)
con name = String.fromUtf8(&arena, "café")
con bytes = name.byteLen() // 5 — 'é' is two bytes of UTF-8
con chars = name.length() // 4 — but one codepoint
con last = name[-1] // 'é' — codepoint indexing
con head = name[0..3] // "caf" — zero-copy codepoint slice
con twice = name + name // fresh buffer from name.alloc
mut acc = bytes + chars + twice.byteLen()
if last == 'é' && head == String.fromUtf8(&arena, "caf") { acc = acc + 1 }
return acc
}
fn main() !u8 {
run()
return Ok(0)
}Queries and builders:
| Method | Signature | Behavior |
|---|---|---|
byteLen | fn (s: String) byteLen() i32 | UTF-8 byte count. O(1) |
length | fn (s: String) length() i32 | codepoint count. O(n) — walks the UTF-8 stream |
byteAt | fn (s: String) byteAt(i: i32) u8 | raw byte at a byte offset, no decoding |
asBytes | fn (s: String) asBytes() [*]u8 | the underlying byte view. Zero-copy |
isEmpty | fn (s: String) isEmpty() bool | true when there are no bytes |
startsWith | fn (s: String) startsWith(prefix: String) bool | byte-prefix test; an empty prefix matches everything |
endsWith | fn (s: String) endsWith(suffix: String) bool | byte-suffix test |
indexOfByte | fn (s: String) indexOfByte(b: u8) i32 | byte index of the first occurrence, or -1 when absent |
repeat | fn (s: String) repeat(n: i32) String | the bytes tiled n times in a fresh buffer; repeat(0) is empty |
import { String } from "@core"
import { ArenaAllocator, systemAllocator } from "@mem"
fn run() i32 {
mut arena = ArenaAllocator.new(systemAllocator)
mut line = String.new(&arena) // empty accumulator
con word = String.fromUtf8(&arena, "ha")
line = line + word
line += word // compound assignment works too
con laugh = word.repeat(3) // "hahaha"
mut acc = line.byteLen() + laugh.byteLen() // 4 + 6
if line.startsWith(word) && laugh.endsWith(word) { acc = acc + 1 }
if line.indexOfByte(u8('a')) == 1 { acc = acc + 1 }
if String.new(&arena).isEmpty() { acc = acc + 1 }
return acc + i32(line.byteAt(0)) + line.asBytes().len
}
fn main() !u8 {
run()
return Ok(0)
}String deliberately has no len field — "length" is ambiguous for UTF-8 — so
it does not satisfy the for-in protocol directly. Iterate its bytes with
for b in s.asBytes(), or its codepoints by indexing 0..s.length().
Vec(T)
The growable array: a contiguous heap buffer of Ts, a len for the in-use
count, and a cap for the allocated slots. Elements are stored inline — a
Vec of structs is an array of structs, with no boxing.
struct Vec(T) { pub items: [*]T, pub len: i32, pub cap: i32, pub alloc: Allocator }| Surface | Signature | Behavior |
|---|---|---|
Vec(T).new | fn (Vec(T)) new(alloc: Allocator, initialCap: i32 = 16) Vec(T) | empty Vec with initialCap slots reserved |
push | fn (v: *mut Vec(T)) push(val: T) | append after the last in-use element; doubles the buffer when full. Amortised O(1) |
v.len | field | in-use element count (v.cap, v.items, v.alloc are also public) |
v[i] | fn (v: Vec(T)) @index(i: i32) T | bounds-checked against len, not cap; negative counts from the end |
v[i] = x | fn (v: Vec(T)) @setIndex(i: i32, val: T) | write to an in-use slot; same bounds rule |
v[lo..hi] | fn (v: Vec(T)) @slice(r: Range) [*]T | zero-copy view of the in-use region |
import { Vec } from "@core"
import { ArenaAllocator, systemAllocator } from "@mem"
fn run() i32 {
mut arena = ArenaAllocator.new(systemAllocator)
mut v = Vec(i32).new(&arena) // 16 slots reserved; len starts at 0
v.push(10)
v.push(20)
v.push(30)
v[0] = 11 // write through @setIndex
con last = v[-1] // 30 — negative counts from the end
con view = v[1..] // [*]i32 view of the in-use tail
mut sum = 0
for x in v { sum = sum + x } // 11 + 20 + 30
return sum + last + view.len + v.len
}
fn main() !u8 {
run()
return Ok(0)
}Two aliasing facts:
- A slice is a view of the current buffer. If a later
pushgrows the Vec,
the Vec moves to a fresh buffer and existing views keep pointing at the old bytes.
- The buffer a growth step abandons is left to the allocator's reclamation
policy — a no-op for arena/bump allocators.
HashMap(K, V)
An open-addressing hash table in the Swiss-table style: a compact array of one-byte hash tags is scanned first, so most probes never touch a full key. It grows (doubling, with rehash) when an insert would pass a 7/8 load factor.
| Surface | Signature | Behavior |
|---|---|---|
HashMap(K, V).new | fn (HashMap(K, V)) new(alloc: Allocator, initialCap: i32 = 16) HashMap(K, V) | empty map; initialCap is used as-is and must be a power of two |
len | fn (m: HashMap(K, V)) len() i32 | number of live entries |
get | fn (m: HashMap(K, V)) get(key: K) ?V | the value for key, or None when absent |
contains | fn (m: HashMap(K, V)) contains(key: K) bool | membership test |
put | fn (m: *mut HashMap(K, V)) put(key: K, val: V) | insert, or update in place when the key exists |
remove | fn (m: *mut HashMap(K, V)) remove(key: K) bool | delete the entry; true when one was removed |
keys | fn (m: HashMap(K, V)) keys() Vec(K) | live keys, snapshotted into a fresh Vec backed by the map's allocator |
values | fn (m: HashMap(K, V)) values() Vec(V) | live values, same snapshot semantics |
entries | fn (m: HashMap(K, V)) entries() Vec(Entry(K, V)) | live pairs; Entry(K, V) is a struct with pub key: K and pub value: V |
import { HashMap } from "@core"
import { ArenaAllocator, systemAllocator } from "@mem"
fn run() i32 {
mut arena = ArenaAllocator.new(systemAllocator)
mut ages = HashMap(i32, i32).new(&arena)
ages.put(1, 36) // insert
ages.put(2, 41)
ages.put(2, 42) // same key: update in place
con hit = ages.get(2) catch 0 // 42
con miss = ages.get(9) catch 0 // 0 — get returns ?V
mut acc = hit + miss + ages.len() // 42 + 0 + 2
if ages.contains(1) { acc = acc + 1 }
if ages.remove(1) { acc = acc + 1 }
return acc + ages.len() // 46 + 1
}
fn main() !u8 {
run()
return Ok(0)
}get returns ?V, so a missing key is handled like any optional — switch
over it, or catch a default (guide
07).
Key requirements: @hash and equality
Keys are hashed by calling the key type's own @hash() u32 method and compared
with == (which for non-primitive types desugars to the type's @eq). Both
resolve through ordinary method lookup — there is no trait to implement. Any
type with the two methods qualifies:
| Key type | Provided by |
|---|---|
i8–i64, u8–u64, char | @core — @hash for every integer width; == is built in |
String | @core — byte-level @hash (FNV-1a) and @eq |
| your own type | you — declare @hash() u32 and @eq(other: T) bool |
The contract is the usual one: keys that compare equal must hash equal.
import { HashMap } from "@core"
import { ArenaAllocator, systemAllocator } from "@mem"
struct Point { x: i32, y: i32 }
fn (p: Point) @hash() u32 {
return u32(p.x) * 31 + u32(p.y)
}
fn (p: Point) @eq(other: Point) bool {
return p.x == other.x && p.y == other.y
}
fn run() i32 {
mut arena = ArenaAllocator.new(systemAllocator)
mut cells = HashMap(Point, i32).new(&arena)
cells.put(Point{ x: 1, y: 2 }, 12)
return cells.get(Point{ x: 1, y: 2 }) catch 0 // 12
}
fn main() !u8 {
run()
return Ok(0)
}A key type missing either method is rejected when the map's internals try to use it:
import { HashMap } from "@core"
import { ArenaAllocator, systemAllocator } from "@mem"
struct Point { x: i32, y: i32 } // no @hash, no @eq
fn run() {
mut arena = ArenaAllocator.new(systemAllocator)
mut cells = HashMap(Point, i32).new(&arena)
cells.put(Point{ x: 1, y: 2 }, 12) // error: operator `==` is not implemented for type Point
}
fn main() !u8 {
run()
return Ok(0)
}Iterating a map
keys(), values(), and entries() each collect the live contents into a
fresh Vec — a snapshot: later mutation of the map does not affect it. Order is
the table's internal slot order — unspecified, and not stable across growth.
import { HashMap } from "@core"
import { ArenaAllocator, systemAllocator } from "@mem"
fn run() i32 {
mut arena = ArenaAllocator.new(systemAllocator)
mut squares = HashMap(i32, i32).new(&arena)
squares.put(2, 4)
squares.put(3, 9)
mut keySum = 0
for k in squares.keys() { keySum = keySum + k } // 5
mut valSum = 0
for v in squares.values() { valSum = valSum + v } // 13
mut pairSum = 0
for e in squares.entries() { pairSum = pairSum + e.key * e.value } // 35
return keySum + valSum + pairSum
}
fn main() !u8 {
run()
return Ok(0)
}Entry(K, V) is exported (import { Entry } from "@core") for when you need to
name the pair type in a signature or annotation.
See also
consuming ?T and E!T in practice
the tutorial pass over these containers, including the iteration protocol
- Error-model reference — desugaring, inference,
try/catch rules
- Types reference —
[N]Tand[*]Tas type
forms, and the casts between the array shapes
@memreference — the allocators every container is built on