hanki

4. Built-in types

TypePurpose
booltrue / false
stringUTF-8 string
bytesimmutable contiguous byte buffer - binary I/O and codecs
()unit - both the type and the value
List<T>persistent ordered collection
Map<K, V>persistent key-value
Set<T>persistent set of Hash elements
Option<T>`Some(T) \None`
Result<T, E>`Ok(T) \Err(E)` - success or typed failure (§7)
Fileopen OS file - a runtime-managed native resource, not sendable (§15)
TcpStreamopen TCP connection - a runtime-managed native resource, not sendable (§15)
TcpListenerbound TCP listening socket - a runtime-managed native resource, not sendable (§15)
Neverbottom type - the result of crash! (§6). Uninhabited; coerces to any type

Never is the bottom type: no value ever has it. It is produced by the diverging crash! primitive (§6) and by the ??? typed hole (§6, Typed holes), and an expression of type Never is accepted wherever any type is expected - so crash!(...) or ??? fits any branch or match arm without inventing a placeholder value. You may write it in a signature (def boom!() -> Never [Crash]) to mark a function that never returns normally.

bytes is an immutable contiguous byte buffer - the raw counterpart to string, for binary I/O and codecs. Like string it is a value (its operations return new buffers; nothing mutates in place), and it carries no UTF-8 guarantee. There are no bytes literals: construct one by UTF-8-encoding a string (s.to_bytes()) or from a codec. Decode back with b.to_string() -> Result<string, Utf8Error> (the Err carries the first invalid byte offset). The buffer ops are byte-offset based: length, slice(start, stop), concat, get(i) -> Option<u8>, is_empty, and hash_u64. Equality is byte-wise. To assemble a bytes incrementally, reach for a BytesBuilder (below): appending to the immutable bytes is O(n²), a builder is O(n).

Resources. A native resource type is a handle to live native state whose lifetime the per-actor memory manager owns (released the moment the last handle drops - reclamation is reference counting, §12): File (an open file descriptor), TcpStream (an open TCP connection), TcpListener (a bound listening socket), BytesBuilder (a pure-memory growable byte buffer), BytesReader (a pure-memory read cursor over a bytes), and Module<T> (a handle to a dynamically loaded module, from module.load!, released by module.unload!). Unlike every other value, a resource is stateful, single-owner, and not copyable - it can be neither duplicated nor shared. Two rules follow:

Open a file with fs.open_file! -> Result<File, FsError>, read it with f.read_all!() -> Result<bytes, FsError>, and release it with f.close!() (§17). Sockets work the same way through extra/net: net.connect! -> Result<TcpStream, NetError> / net.listen! -> Result<TcpListener, NetError>, then read! / write! / accept! / close! (§17).

BytesBuilder is the pure-memory resource: a growable byte buffer for assembling a bytes in O(n). Create one with BytesBuilder.new!(), append with push!(b: u8) and extend!(b: bytes), and read the accumulated buffer with finish!() -> bytes - non-consuming, so the builder keeps growing after a read. Every operation is an action with an empty effect row: it mutates runtime-managed state but commits no OS capability, so the ! is here marking the referential-transparency boundary (a mutation is not substitutable, and new! mints a fresh mutable identity), not a tracked effect (§5–6). The mutation stays sealed behind these actions; no value-level mutability leaks past them. Having no OS handle, it needs no close!; its buffer is freed the moment the last handle drops. It runs on both tiers, AOT included.

BytesReader is its read counterpart: a forward cursor over an immutable bytes, so a decoder reads through it instead of threading a byte offset by hand. Create one with BytesReader.new!(input: bytes), then take!(n: int) -> bytes (up to n bytes, advancing past them - clamped to what remains, so a short read yields fewer bytes rather than failing and the primitive stays total), peek!() -> Option<u8> (the next byte without advancing - None at the end, so a self-describing decoder branches before committing a read; Option<u8>, not a -1 sentinel), remaining!() -> int, and position!() -> int (the offset to report in a decode error). Same model as BytesBuilder: empty-effect-row actions (reads of the advancing cursor aren't substitutable, so they carry !), runtime-managed, no close!, both tiers.

Naming convention: lowercase = ubiquitous primitive or default arbitrary-precision flavor; PascalCase = abstract, generic, or specialized. The lowercase tier is bool, string, bytes, the fixed-width primitives (i32, u64, f64, …), and the everyday arbitrary-precision numerics (int, decimal, rational). The PascalCase tier is abstract containers (List, Map, Set, Option), domain-specialized numerics (Complex<T>), and all user-defined types. Type parameters (T, K, V, Item) are PascalCase. The everyday numerics get the unadorned names because arbitrary precision is the right default under our correctness > performance order - code that doesn't care about width should reach for the type that just works.