Skip to content

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.

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.

overview-taste.dusk
@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
}

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 Display or 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.

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 clang and 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.

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 *T and raw *raw T pointers, the generational heap, single ownership with ref and move, escape checking for the clear cases, and foreign "C" calls into libc across the raw pointer boundary.
  • 0.3.0 through 0.3.3 add concurrency: spawn and join for 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 the submit builtin. 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 func and await keywords with async_run as 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 with std.async.net, adds an awaitable channel receive, and lets do compose 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 the rune primitive, one Unicode scalar value, with r'...' and \u{...} literals and a std.unicode module (see the Unicode guide). 0.5.3 grows the standard library with a lazy IO<T>, a Result<T, E> monad, and leveled logging. 0.5.4 is an audit release that reserves the unsigned integer widths, gates impl behind @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_HOME set.
  • 1.1.0 through 1.3.1 reopen the surface. 1.1.0 makes a char, a char[N], and a char[] print as their text bytes, and adds a string for loop, a bounds checked string range slice, and str_from_chars. 1.2.0 is the daily driver: short-circuit && and ||, string == and != by content and + for concatenation, explicit width casts, break and continue, 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 foreign functions, the @link and @csource directives 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 a foreign parameter 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 adds export "C" and dusk 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.math and std.rand in 1.4.0, std.fs and std.time in 1.4.1, std.process and the vec_sort, vec_contains, and vec_index_of additions to std.vector in 1.4.2, and std.json in 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, and float64 join 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 adds hash(v), a deterministic 64-bit hash over a hashable value, which is an integer of any width, a char, a rune, or a string. 1.5.2 spends it: std.map is now Map<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.string grew 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 teaches dusk ir a second target, --target=wasm32, the form the browser playground is built from; it also renames std.os’s errno read to os_errno. 1.6.1 adds the /** */ doc comment and the dusk doc command 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 --json and 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_take and map_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_map and vec_filter, std.time’s weekday and parse_iso8601, and std.json’s json_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 for the two standard containers. 1.9.0 adds the owning func marker, 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 adds vec_fold, which 1.9.1 completes while moving vec_take and map_take onto 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 bare match that value-izes into the arm. 1.11.0 is the container loop: for x in v over a *Vector<T> and for k in m and for k, v in m over a *Map<K, V>, lowered before monomorphization into the exact index loop so code generation never learns container layout, with map_key_at and map_val_at backing 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.