Docs · Standard library

`@core`

@core is the standard library's core package. It ships in two tiers:

TierNamesHow you get them
PreludeOptional, Result, Array, ArrayPointer, RangeAlways 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-requiredString, 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

yaml
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

yaml
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:

vyi
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):

FieldTypeMeaning
ptrc_ptr Traw pointer to the first element
leni32element count

Methods and operators (xs[i], xs[i] = v, xs[lo..hi], a == b desugar to the @-prefixed operator methods):

SurfaceSignatureBehavior
xs.len / xs.len()fn (xs: [*]T) len() i32element count — the field and the method both work
xs[i]fn (xs: [*]T) @index(i: i32) Tbounds-checked read; panics out of bounds
xs[i] = vfn (xs: [*]T) @setIndex(i: i32, v: T)bounds-checked write
xs[lo..hi]fn (xs: [*]T) @slice(r: Range) [*]Tzero-copy sub-view of the same storage; empty ranges allowed
a == bfn (a: [*]u8) @eq(b: [*]u8) boolcontent 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) [*]Tthe trusted entry point pairing raw memory with a length
fn cPtrOf(known T: type, p: *mut T | [*]T) c_ptr Tthe 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:

vyi
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:

vyi
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).

vyi
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

yaml
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.

vyi
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, end subject.len)

are filled at an indexing site; a standalone con r = 0.. has nothing to default against and is rejected.

  • for iterates range syntax, not Range values. for i in a..b walks

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:

yaml
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.

yaml
struct String { pub bytes: [*]u8, pub alloc: Allocator }

Construction:

FunctionSignatureBehavior
String.newfn (String) new(alloc: Allocator) Stringempty string backed by alloc — a starting accumulator
String.fromUtf8fn (String) fromUtf8(alloc: Allocator, src: [*]u8) Stringcopies 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):

SurfaceSignatureBehavior
s[i]fn (s: String) @index(i: i32) charthe i-th codepoint. O(i) — decodes from the start
s[lo..hi]fn (s: String) @slice(r: Range) Stringzero-copy view of a codepoint range; shares storage and allocator with the source
s == tfn (s: String) @eq(other: String) boolbyte equality — no Unicode normalisation
s + tfn (s: String) @add(other: String) Stringconcatenation into a fresh buffer from s.alloc — which is why a String carries its allocator
fn (s: String) @hash() u32FNV-1a over the bytes; lets String be a HashMap key
vyi
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:

MethodSignatureBehavior
byteLenfn (s: String) byteLen() i32UTF-8 byte count. O(1)
lengthfn (s: String) length() i32codepoint count. O(n) — walks the UTF-8 stream
byteAtfn (s: String) byteAt(i: i32) u8raw byte at a byte offset, no decoding
asBytesfn (s: String) asBytes() [*]u8the underlying byte view. Zero-copy
isEmptyfn (s: String) isEmpty() booltrue when there are no bytes
startsWithfn (s: String) startsWith(prefix: String) boolbyte-prefix test; an empty prefix matches everything
endsWithfn (s: String) endsWith(suffix: String) boolbyte-suffix test
indexOfBytefn (s: String) indexOfByte(b: u8) i32byte index of the first occurrence, or -1 when absent
repeatfn (s: String) repeat(n: i32) Stringthe bytes tiled n times in a fresh buffer; repeat(0) is empty
vyi
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.

yaml
struct Vec(T) { pub items: [*]T, pub len: i32, pub cap: i32, pub alloc: Allocator }
SurfaceSignatureBehavior
Vec(T).newfn (Vec(T)) new(alloc: Allocator, initialCap: i32 = 16) Vec(T)empty Vec with initialCap slots reserved
pushfn (v: *mut Vec(T)) push(val: T)append after the last in-use element; doubles the buffer when full. Amortised O(1)
v.lenfieldin-use element count (v.cap, v.items, v.alloc are also public)
v[i]fn (v: Vec(T)) @index(i: i32) Tbounds-checked against len, not cap; negative counts from the end
v[i] = xfn (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) [*]Tzero-copy view of the in-use region
vyi
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 push grows 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.

SurfaceSignatureBehavior
HashMap(K, V).newfn (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
lenfn (m: HashMap(K, V)) len() i32number of live entries
getfn (m: HashMap(K, V)) get(key: K) ?Vthe value for key, or None when absent
containsfn (m: HashMap(K, V)) contains(key: K) boolmembership test
putfn (m: *mut HashMap(K, V)) put(key: K, val: V)insert, or update in place when the key exists
removefn (m: *mut HashMap(K, V)) remove(key: K) booldelete the entry; true when one was removed
keysfn (m: HashMap(K, V)) keys() Vec(K)live keys, snapshotted into a fresh Vec backed by the map's allocator
valuesfn (m: HashMap(K, V)) values() Vec(V)live values, same snapshot semantics
entriesfn (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
vyi
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 typeProvided by
i8i64, u8u64, char@core@hash for every integer width; == is built in
String@core — byte-level @hash (FNV-1a) and @eq
your own typeyou — declare @hash() u32 and @eq(other: T) bool

The contract is the usual one: keys that compare equal must hash equal.

vyi
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:

vyi-error
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.

vyi
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

try/catch rules

forms, and the casts between the array shapes