Skip to content

Standard library overview

The dusk standard library lives under lib/std in the repository and is written in Dusk itself. It is small and deliberate: console I/O, a string toolkit with a Unicode layer beside it, three collections and a command line flag parser, the process environment and the command shell, files and directories with a path layer over them, subprocesses, a UTC clock and the calendar over it, a JSON parser and emitter, allocators and the collected heap, the functional enums and monads, level gated logging, two numeric modules, the concurrency modules, and the async modules.

Import a module with a dotted path in an @import directive at the top of the file, before declarations.

@import std.io
@import std.functional.maybe

A dotted path resolves to a module (a directory or a file) or to a leaf symbol inside a file.

@import std.io // module
@import std.io.print_line // one symbol

After a module import the module’s exported names are in scope flat, so @import std.io lets you call print_int and print_line with no prefix. A qualified call through the module path reaches the same function, so std.io.print_line("hi") also works. Enum constructors keep their type name: after @import std.functional.maybe you write Maybe.Some(42) and Maybe.None.

A module’s own imports arrive with it, and they arrive just as flat, so importing one module can put more names in scope than that module’s page lists. std.string imports std.vector as of 1.5.3, which is what str_split and str_join are built on, so @import std.string alone is enough to make Vector<T> and the vec_* functions callable with no prefix of their own. That reach is wider than it sounds, since nearly every module in the library imports std.string in turn.

This is the one place recent library growth can break a program that used to compile. If you had written your own vec_len, or your own Vector, and the file imports any module that leads back to std.string, your name and the library’s now collide and the compiler stops with duplicate definition of 'vec_len' rather than picking one of them quietly. The fix is to rename your copy. A loud failure at the definition is the trade the flat import model makes for letting you call str_len without spelling the module every time.

Imports are independent of paradigm directives. Importing a module does not grant any paradigm; a file that wants while and mut still declares @paradigm procedural itself. See Source files for the full import and export rules.

Some names need no import at all. print, println, printerr, alloc, free, read_file, write_file, read_line, read_all, spawn, join, and submit are builtins, available everywhere. See Builtins.

Every module below is in the tree today, written in Dusk under lib/std.

ModuleWhat it holdsDetails
std.ioprint_int, print_line, typed line input with read_int and read_floatI/O
std.logginglog_debug through log_error to stderr, gated by a LogLevel thresholdLogging
std.stringsearch, compare, trim, split, join, replace, and ASCII case folding, beside the parsers and the growable StringBuilderStrings
std.unicodedecode_rune, encode_rune, and UTF-8 validation over the string’s byte viewUnicode
std.maththe libm scalar float64 functions, pi and e, and the is_nan/is_inf predicatesMath
std.randan xoshiro256** generator over a heap Rng, seeded through splitmix64Rand
std.vectorVector<T>, a growable heap arrayCollections
std.mapMap<K, V>, a hash map over any hashable key, returning Maybe<V> on lookupCollections
std.setSet<T>, an unordered set over the generic map, membership as key presenceSet
std.osthe command shell through run, the environment through env, and the errno conventionOS
std.fsfile and directory operations over libc’s own syscalls, plus pure Dusk path arithmeticFiles
std.processa shell command run as a child process, its output read back line by lineProcess
std.timeUTC clock reads paired with a proleptic Gregorian calendarTime
std.jsona JSON parser and emitter over a recursive Json enum, freed with json_freeJSON
std.flagsFlags, a long-option command line parser over argv with positionalsFlags
std.memory.allocatorThe Allocator interface with the Heap, FixedBuffer, and Debug allocatorsMemory
std.memory.arenaArena, a bump allocator reset or destroyed as a wholeMemory
std.memory.collectorgc_collect and the live counters for the collected heap behind collector<T>Memory
std.functional.maybeMaybe<T> with is_some and unwrap_orFunctional
std.functional.eitherEither<L, R> with is_left and left_orFunctional
std.functional.resultResult<T, E>, a success value or a typed error, with a monad blockFunctional
std.concurrent.threadsleep_ms, beside the spawn and join builtinsConcurrency
std.concurrent.atomicAtomicInt, sequentially consistent int64 atomicsConcurrency
std.concurrent.channelChannel<T>, a bounded, thread-safe queue, with try and timeout variantsConcurrency
std.concurrent.syncMutex and CondvarConcurrency
std.concurrent.poolpool_start, pool_shutdown, and ncpu for the global thread pool behind submitConcurrency
std.async.futureFuture<T>, a one shot completion slot minted, completed, and awaited onceAsync
std.async.looploop_init and loop_free for the process-wide event loopAsync
std.async.timesleep_async, a timer the loop completes as a Future<int64>Async
std.async.iothe readiness reactor, pipes, and the non-blocking read_nb and write_nbAsync
std.async.netTCP over the reactor, with non-blocking connect, accept, read, and writeAsync

A short program that touches both collections:

stdlib_tour.dusk
@paradigm procedural
@import std.vector
@import std.map
@import std.functional.maybe
func main() -> int32 {
v: *Vector<int64> = alloc(vec_new())
mut i: int64 = 0
while i < 3 {
vec_push(v, i * 10)
i = i + 1
}
println("vector holds {} elements", vec_len(v))
println("v[2] = {}", vec_get(v, 2))
m: *Map<string, int64> = alloc(map_new())
map_put(m, "answer", 42)
println("answer = {}", unwrap_or(map_get(m, "answer"), 0))
vec_free(v)
free(v)
map_free(m)
free(m)
return 0
}

The map is generic over its key as well as its value since 1.5.2. K can be an integer of any width, a char, a rune, or a string, which is exactly the set the hash builtin accepts, so a Map<int64, string> is as ordinary as the Map<string, int64> above. Every map written before that release was keyed by a string, and each of those spellings is Map<string, V> now.

Both collections follow the same shape: build the value on the heap with alloc, pass it by pointer so growth persists across calls, free the backing buffer with the module’s _free function, then free the struct itself with free. StringBuilder and Arena follow the same pattern. The memory guide covers the ownership rules behind it.

Most of the original plan has since shipped. std.functional.result and std.functional.io are in the tree as real monads, std.logging gates output to stderr by level, std.memory.collector drives the collected heap behind collector<T>, and std.unicode walks a string’s byte view scalar by scalar. The Maybe, Either, and Result helper surfaces have filled out, and the std.async line grew TCP networking and an awaitable channel across 0.4.3 and 0.4.4.

The language surface froze at 0.5.4 for the self-hosting bootstrap and stayed frozen across the whole 0.6.x through 0.9.x rewrite, which reimplemented the compiler in Dusk rather than changing the language. 1.0.0 lifted that freeze rather than sealing it. The surface has grown with every release since 1.1.0, and nearly all of that growth is additive, so a program that compiled at 0.5.4 almost always compiles today with no source change. The exceptions are narrow, and most are a name the language took for itself: a top level function cannot be called hash, which 1.5.2 refused by name once 1.5.1 had made it a builtin, and it cannot be called rune, float32, or float64 either, since 1.5.0 made those casts and joined them to the same reserved set the older cast names were already in. One more exception is a name the library took rather than the language: std.string picked up std.vector in 1.5.3, so a vec_* name or a Vector of your own now collides with the standard library’s, as the import section above describes. The last is a rename: 1.6.0 changed std.os’s errno read from errno to os_errno, so its bare symbol stops colliding with the C errno on a wasm target, and a program that called the old name updates the call.

1.11.0 is the current release, and the library is where most of the recent growth landed. 1.4.0 added the two numeric modules std.math and std.rand beside its foreign-boundary work, 1.4.1 brought std.fs and std.time, 1.4.2 added std.process, and 1.4.3 added std.json. 1.5.2 then made std.map generic over its key, which is the one change in that line to rewrite a signature already in use: a map spelled Map<V> before it is Map<string, V> now, and no call site around it has to move, since map_new and every accessor infer both parameters. 1.5.3 filled out std.string, which had grown up serving the compiler and so carried the parsers, the builder, and the foreign bridge but not the everyday manipulation a program reaches for first: searching from the tail, ordering, trimming, splitting and joining, replacing, repeating, and ASCII case folding are twelve new functions written in pure Dusk over the builder that was already there. The 1.8 line then added five more items in one release: std.flags for command line parsing, std.set over the generic map, vec_map and vec_filter beside the vector sort, std.time’s weekday and a strict parse_iso8601, and std.json’s json_free, the deep free that 1.8.0’s owning takes, vec_take and map_take, finally made expressible. The library kept filling in across 1.9 through 1.11: 1.9.1 added vec_fold, the reduce that finishes the map, filter, and fold trio over a vector, and moved vec_take and map_take onto the new owning func marker; 1.11.0 gave the map two allocation-free positional accessors, map_key_at and map_val_at, which back the new for k, v in m container loop. Three things the freeze always allowed still hold: diagnostics can sharpen, the standard library keeps growing, and a soundness fix can land, including the rare one that forces a surface change, which its release names in the changelog.

That makes the library the place near-term work still lands. List<T> and a wider spread of helpers are the obvious next reach, and more of the collection and std.async surface can fill in the same way, none of it touching the language core.