Overview and philosophy
Dusk is a small systems language that compiles to native code through textual LLVM IR. Each file declares the paradigms it uses with @paradigm procedural, functional, or oop directives that stack, and those choices unlock the matching builtins. Values are immutable by default, memory is explicit, and errors are values you handle.
This page lays out the philosophy that the rest of the reference builds on. The reference describes the 0.1.0 core with the changes every release since layered on top, through the 0.2.x, 0.3.x, 0.4.x, and 0.5.x lines and the 1.x growth that followed; each page notes where the current language differs from the baseline.
Core philosophy
Section titled “Core philosophy”The specification opens with six commitments, and the checker holds the line they draw.
- Immutability by default. All values are immutable unless explicitly declared mutable with
mut. Immutability covers element and field stores, not just rebinding. - Explicit over implicit. Allocations, dereferences, paradigm usage, and error handling are never hidden.
- Multiple paradigms with enforced discipline. Paradigms are opt in per file through directives. Using a paradigm feature the file has not declared is a compile error in that file. See Paradigm system.
- Systems level control. Manual memory management by default. No garbage collector unless explicitly opted into through the standard library.
- All declared variables must be used. An unused variable is a compile error. This is never suppressible.
- All errors must be handled. Ignoring an error return is a compile error. See Error handling.
A small complete program shows several of these at once: the file declares the functional paradigm before it may call map and foreach, every binding is immutable, and every binding is used.
@paradigm functional
func main() -> int32 { nums: int64[] = [1, 2, 3, 4, 5] doubled := map(nums, lambda (n: int64) -> int64 { return n * 2 }) foreach(doubled, lambda (n: int64) -> void { println(n) }) return 0}What the checker holds
Section titled “What the checker holds”The static checker holds the line the spec draws:
- Integer and float widths never mix silently.
- Immutability extends to element and field stores.
- Every array index and range slice is bounds checked, and a bound error must be handled.
- An allocation is sized by its declared type.
- Printing dispatches through
Displayor fails to compile. - A private name never leaves its file.
Beyond the static checks, the default heap since 0.2.x is generational: every managed pointer carries a generation that is checked at each dereference, so a use after free, a double free, or a stale pointer to a reused block faults instead of corrupting memory. See Memory management for the managed and raw pointer split.
Compilation model
Section titled “Compilation model”The compiler is written in Dusk itself. It runs the whole pipeline: it lexes, parses, resolves names, type checks, monomorphizes, and emits textual LLVM IR. The IR is handed to clang for native code generation, and each program links against a small C runtime. At 1.3.1, the release named “the retirement”, the original Rust compiler was deleted from the repository, leaving it pure Dusk: the compiler (root file compiler/dusk.dusk), the Dawn package tool, and the test runner are all written in Dusk and build with the dusk compiler itself. The deleted Rust implementation is archived at github.com/choice404/dusk-rust, a frozen full-history archive at tag v1.3.0 that stands as the reference for the language surface through 1.2.0; the status section below tells the bootstrap story.
Two consequences follow from this design:
- Building dusk programs requires
clangand LLVM on your path. The textual IR targets one LLVM major version; as of 1.11.0 that is LLVM 22.x. - The dusk compiler carries no dependency beyond Dusk and its own standard library.
The compiler finds its standard library and C runtime beside itself. The DUSK_HOME environment variable overrides the search when you want a binary to use a different toolchain tree, such as a source checkout. The standard library under lib/std is written in Dusk itself; see the standard library overview.
The dusk binary exposes each pipeline stage as a command, from lex and parse through check, build, and run, plus ir to print the generated LLVM IR straight to stdout with no clang step; see the CLI reference. An executable is not the only thing the pipeline can end at: since 1.4.3, dusk build --lib sends the same module out as a static archive and a generated C header instead, so a C program links Dusk the way it links any other library. See C libraries. Dawn is the accompanying package tool, which treats a package as a git repository; see Dawn and Packages.
Status and versioning
Section titled “Status and versioning”The current release is 1.11.0, and Dusk is self-hosting: the compiler is written in Dusk and builds itself. The surface froze at 0.5.4 for the bootstrap and stayed put across the whole 0.6.x through 0.9.x rewrite, and 1.0.0 declared that surface stable with the bootstrap done. Since 1.1.0 the surface has been growing again. The CHANGELOG records the release by release history; in outline:
- 0.1.0 is the core language the specification describes.
- 0.2.0 through 0.2.6 add memory safety: the
StringBuilder, the split between managed*Tand raw*raw Tpointers, the generational heap, single ownership withrefandmove, escape checking for the clear cases, andforeign "C"calls into libc across the raw pointer boundary. - 0.3.0 through 0.3.3 add concurrency:
spawnandjoinfor OS threads, thread safe generational checking, atomics, channels with blocking and non-blocking operations, mutexes and condition variables that fault by name on classic pthread misuse, and a global thread pool with thesubmitbuiltin. See Threads and the memory model. - 0.4.0 through 0.4.4 add the async layer: 0.4.0 lands futures and the event loop, 0.4.1 the epoll reactor that turns file descriptor readiness into a completion, and 0.4.2 the
async funcandawaitkeywords withasync_runas the sync to async bridge. 0.4.2 also lands the full operator set (bitwise, compound assignment, increment and decrement, exponent, pipe, and the inclusive range) and hardens escape checking and interface boxing. 0.4.3 puts TCP on the reactor withstd.async.net, adds an awaitable channel receive, and letsdocompose over any monad, and 0.4.4 ports the reactor to a second platform through a kqueue backend beside epoll and hardens the syscall layer. See the async guide, the async reference, and the operators reference. - 0.5.0 through 0.5.4 turn to soundness, memory, and text before the bootstrap: 0.5.0 makes escape analysis interprocedural, a per function summary the changelog calls the ledger, so a frame view laundered out through a call, a store, a channel send, or a closure is caught. 0.5.1 adds
collector<T>, a second conservative mark and sweep heap beside the generational one that you opt into per value (see the collector reference). 0.5.2 lands theruneprimitive, one Unicode scalar value, withr'...'and\u{...}literals and astd.unicodemodule (see the Unicode guide). 0.5.3 grows the standard library with a lazyIO<T>, aResult<T, E>monad, and leveled logging. 0.5.4 is an audit release that reserves the unsigned integer widths, gatesimplbehind@paradigm oop, adds caret diagnostics, and freezes the surface for the bootstrap. - 0.6.0 through 0.9.4 are the bootstrap: the compiler gets rewritten in Dusk itself, one pipeline stage at a time, with the language held still. 0.6.x scaffolds the front end and 0.7.0 the parser, 0.8.x ports the type checker and the interprocedural escape summary, and 0.9.x ports code generation, until 0.9.4 reaches the fixpoint where the dusk compiler builds itself to a byte identical result three stages deep.
- 1.0.0 declares the bootstrap done. No language surface change and no compiler behavior change: the compiler written in Dusk becomes canonical. 1.0.1 then gives that installed compiler the same asset search the seed had, so a packaged install finds its standard library and C runtime with no
DUSK_HOMEset. - 1.1.0 through 1.3.1 reopen the surface. 1.1.0 makes a
char, achar[N], and achar[]print as their text bytes, and adds a stringforloop, a bounds checked string range slice, andstr_from_chars. 1.2.0 is the daily driver: short-circuit&&and||, string==and!=by content and+for concatenation, explicit width casts,breakandcontinue, tighter comparison rules, and file and line locations on the common runtime faults. 1.3.0 lands a native test runner and a Dusk port of Dawn. 1.3.1, “the retirement”, deletes the Rust seed and archives it, leaving the repository pure Dusk. - 1.4.0 through 1.4.4 open the foreign boundary, and open it in both directions. Going out, 1.4.0 lands variadic
foreignfunctions, the@linkand@csourcedirectives that reach the linker, and the IEEE 754 fix that makes float!=answer unordered; 1.4.1 lets a C plain struct cross by value, classified the way clang’s own ABI places it; 1.4.2 lets aforeignparameter be a function type, so a capture free lambda or a named top level function crosses as a bare C function pointer that a C API calls back into. Coming in, 1.4.3 addsexport "C"anddusk build --lib, which compile a Dusk module into a static archive and a generated C header. 1.4.4 adds no surface at all and instead tests the whole boundary against adversarial input. The standard library grows alongside it, one module per release:std.mathandstd.randin 1.4.0,std.fsandstd.timein 1.4.1,std.processand thevec_sort,vec_contains, andvec_index_ofadditions tostd.vectorin 1.4.2, andstd.jsonin 1.4.3. See foreign functions and C libraries. - 1.5.0 through 1.5.3 widen the scalar set, generalize the map, and fill out the string module. 1.5.0 opens the numeric cast to the whole of it:
rune,float32, andfloat64join the cast builtins for eight cast names in total, and a cast now crosses between the integer and float families in both directions, a float to an integer truncating toward zero and saturating to the nearest bound rather than leaving an out of range input undefined, with a NaN casting to zero. 1.5.1 addshash(v), a deterministic 64-bit hash over a hashable value, which is an integer of any width, achar, arune, or astring. 1.5.2 spends it:std.mapis nowMap<K, V>, generic over its key as well as its value, with K drawn from that same hashable set. 1.5.3 changes no surface at all and instead fills a gap in the standard library:std.stringgrew up serving the compiler, so it had parsing, a builder, and the foreign bridge but not the everyday manipulation set a program reaches for first, and this release adds it, twelve functions for searching from the tail, ordering, trimming, splitting and joining, replacing, repeating, and ASCII case folding. See numeric casts, the hash builtin, std.vector and std.map, and std.string. - 1.6.0 through 1.8.1 add comments, tooling surfaces, an ownership refinement, and a run of standard library growth. 1.6.0 gives the language its
/* */block comment, nesting the way commented out code needs, and teachesdusk ira second target,--target=wasm32, the form the browser playground is built from; it also renamesstd.os’s errno read toos_errno. 1.6.1 adds the/** */doc comment and thedusk doccommand that renders it. 1.7.0 is a debt release that closes a live escape soundness hole, and 1.7.1 grows the machine face tooling reads:dusk check --jsonand a span on every doc model item, the two contracts the language server builds on. 1.8.0 adds owning removal from a container,vec_takeandmap_take, which hand back the element as an owner so a program can hand write the deep free of a heap tree. 1.8.1 spends that on the standard library:std.flags,std.set,vec_mapandvec_filter,std.time’sweekdayandparse_iso8601, andstd.json’sjson_free. See source files, the CLI, and the standard library overview. - 1.9.0 through 1.11.0 land the ownership marker, finish the match expression, and give
forthe two standard containers. 1.9.0 adds theowning funcmarker, the declared generalization of the take blessing, and squares away the match expression so its arms agree on type and ownership with one voice; it also addsvec_fold, which 1.9.1 completes while movingvec_takeandmap_takeonto the new marker. 1.10.0 makes block scoping and shadowing sound the whole way down to code generation, and 1.10.1 lets a match arm end in a barematchthat value-izes into the arm. 1.11.0 is the container loop:for x in vover a*Vector<T>andfor k in mandfor k, v in mover a*Map<K, V>, lowered before monomorphization into the exact index loop so code generation never learns container layout, withmap_key_atandmap_val_atbacking the pair form. See functions, enums, and collections.
The 0.5.4 surface this reference describes held unchanged across the whole bootstrap and through 1.0.0, so a program written then still compiles today with no source change, give or take a handful of names the implementation has since claimed: 1.5.0 reserved rune, float32, and float64 as cast names, 1.5.2 refused a top level function named hash, and 1.5.3 has std.string import std.vector, so a program carrying its own vec_len or Vector now fails with a duplicate definition where it compiled before, since nearly every module imports std.string; rename the private copy and it builds again. The one compat note in the 1.6 through 1.8 line is a library rename rather than a claimed name: 1.6.0 renamed std.os’s errno read to os_errno so it stays linkable on a target whose libc owns the symbol errno, so a program that called the old name updates the call. Block comments, doc comments, and the owning takes since then are purely additive and claim nothing. The 1.9 through 1.11 line is additive the same way: the owning func marker, the tightened match expression, and the container for loop each widen what checks rather than break what did, so the previous release builds the new compiler’s source unchanged. The one narrow flip is in 1.9.1, which deleted the by-name blessing of vec_take and map_take, so a user’s own function of that name that leaned on it now marks itself owning func to keep the blessing. Since 1.1.0 the surface has been growing again: char as text and checked string ranges in 1.1.0, the operators, comparison, and control flow work of 1.2.0, the foreign boundary across the whole 1.4.x line, and the wider numeric cast, the hash builtin, and the generic map in 1.5.x, each covered on its own reference page. See the roadmap for the release by release history.
The single largest change since 1.0.0 is the foreign boundary, and the thing to know about it is that it now runs in both directions. Dusk calls C, reaching a variadic function, a third party library named by @link, a plain struct passed by value, and a C API that calls back into a Dusk function you hand it as a function pointer. C calls Dusk, through an export "C" function and a dusk build --lib archive that any language with a C FFI links against. Neither direction asks you to give up what the rest of the language promises: a fault inside a callback body still aborts by name, and a fault inside an export still crosses back out as a clean abort rather than corruption. Two limitations are worth knowing before you lean on it. The archive is static only, since its objects are not position independent, so it does not load into a dlopen based FFI like Python’s ctypes. And a struct crosses by value on a foreign call but not on an export "C" signature, where it is still rejected by name.
Where this reference describes 0.1.0 behavior that a later release changed (immutable-only strings, a single pointer kind, debug-only memory safety), the affected page carries a version caveat.
Where to go next
Section titled “Where to go next”- New to the language: start with Getting started and the language tour.
- The reference proper begins with source files, directives, imports, and exports.
- Runnable programs live in the repository’s
examples/directory and in the examples guide. - The source is at github.com/choice404/dusk, dual licensed under MIT or Apache 2.0.