Skip to content

Examples tour

The dusk repository ships an examples/ directory of small, runnable programs. Each one exercises a specific slice of the language, and the compiler’s golden test suite compiles and runs them, so they stay current as the language changes. This page tours the most useful ones. Every program shown here type checks against dusk 0.3.3.

One file is the exception: examples/showcase.dusk is marked illustrative in its own header. It sketches the surface the milestones build toward, does not compile, and uses some provisional syntax. Skip it when you want working code.

dusk run compiles a file and executes it in one step, forwarding any trailing arguments to the program:

Terminal window
dusk run examples/hello.dusk

From a source checkout without a built binary, cargo run does the same:

Terminal window
cargo run --bin dusk -- run examples/hello.dusk

dusk check type checks without building, which is handy for the fault examples that are designed to trap at runtime. The CLI reference covers all eight commands.

The minimal program. A @paradigm directive, one imported symbol, and a main that returns an exit code. The argc and argv parameters are optional; this variant declares them.

hello.dusk
// hello.dusk, the minimal program. Imports one symbol from std.io and prints.
@paradigm procedural
@import std.io.print_line
func main(argc: int32, argv: string[]) -> int32 {
print_line("hello, world")
return 0
}

app.dusk is the repository’s flagship sample: one file pulling in five standard library modules and touching arenas, Maybe, Either, and string helpers in a single run. The README points to it as the multi-module sample, and it is a good first stop for seeing how stdlib pieces compose.

app.dusk
@paradigm procedural
@import std.io
@import std.string
@import std.functional.maybe
@import std.functional.either
@import std.memory.arena
func lookup(k: int64) -> Maybe<int64> {
if k == 1 {
return Maybe.Some(42)
}
return Maybe.None
}
func classify(n: int64) -> Either<int64, int64> {
if n < 0 {
return Either.Left(n)
}
return Either.Right(n)
}
func main() -> int32 {
// Arena: two bump allocations from one backing buffer.
a: *Arena = alloc(arena_new(1024))
p: *int64 = arena_alloc(a, 8)
*p = 7
q: *int64 = arena_alloc(a, 8)
*q = 35
print_int(*p + *q)
// Maybe: present and absent lookups.
print_int(unwrap_or(lookup(1), 0))
print_int(unwrap_or(lookup(2), 99))
// Either: error case carries its value.
print_int(left_or(classify(-5), 0))
print_int(left_or(classify(8), 0))
// String helpers walk the NUL terminated bytes.
print_int(str_len("hello"))
arena_destroy(a)
free(a)
return 0
}

Three things to notice. The arena is itself heap allocated with alloc and released with arena_destroy plus free, the two-step teardown the arena module documents. unwrap_or and left_or are plain functions from the maybe and either modules, not language features. And every import is a stdlib dotted path, so plain dusk run handles it with no package fetching.

std.vector is a generic growable array with amortized constant-time appends, written in dusk. The vector is passed by pointer so growth persists across calls, and it is torn down in two steps: vec_free releases the buffer, free releases the record.

vec.dusk
@paradigm procedural
@import std.vector
func main() -> int32 {
v: *Vector<int64> = alloc(vec_new())
mut i: int64 = 0
while i < 6 {
vec_push(v, i * 10)
i = i + 1
}
println(vec_len(v))
mut j: int64 = 0
while j < vec_len(v) {
println(vec_get(v, j))
j = j + 1
}
vec_free(v)
free(v)
return 0
}

The binding annotation *Vector<int64> pins the element type for vec_new(). See collections for the full API.

std.map is a string-keyed hash map with open addressing, also written in dusk. map_get returns a Maybe<V>, so a missing key is a value you handle rather than a crash.

map.dusk
@paradigm procedural
@import std.map
@import std.functional.maybe
// std.map: insert string keys, overwrite one, then read them back. Pass the map
// by pointer so growth and inserts persist across calls.
func main() -> int32 {
m: *Map<int64> = alloc(map_new())
map_put(m, "one", 1)
map_put(m, "two", 2)
map_put(m, "three", 3)
map_put(m, "two", 22)
println(map_len(m))
println(unwrap_or(map_get(m, "one"), 0))
println(unwrap_or(map_get(m, "two"), 0))
println(unwrap_or(map_get(m, "three"), 0))
println(unwrap_or(map_get(m, "missing"), -1))
map_free(m)
free(m)
return 0
}

The second map_put with key "two" overwrites, so map_len prints 3 and the lookup prints 22.

Arena implements the Allocator interface, so a function that takes using a: Arena reroutes the plain alloc builtin to bump allocation from the arena’s buffer. No allocator argument threads through the calls inside.

arena_use.dusk
@paradigm procedural
@import std.memory.arena
// Arena passed through `using`. It implements Allocator, so the builtins alloc
// and free dispatch to it. Two eight byte allocations leave used at 16.
func fill(using a: Arena) -> int64 {
p: *int64 = alloc(8)
*p = 1
q: *int64 = alloc(8)
*q = 2
return a.used
}
func main() -> int32 {
a: *Arena = alloc(arena_new(64))
println(fill(*a))
arena_destroy(a)
free(a)
return 0
}

The program prints 16: two eight-byte allocations from the arena. The memory guide explains the using mechanism and the untracked-generation rule that lets custom allocators hand back unchecked memory.

read_line returns a (string, error) pair, and the error exists at end of input. The same code reads from a terminal or from a redirected pipe. Note the done flag: dusk has no break, so the loop condition carries the exit.

input.dusk
@paradigm procedural
@import std.io
// User input from stdin. read_line hands back a (string, error) pair, the error
// existing at end of input. It reads from the terminal when stdin is not
// redirected, and from a pipe or file when it is, the same either way.
//
// First prompt for a name and echo it. Then read lines until end of input and
// count them, using a done flag since dusk has no break.
func main() -> int32 {
print_line("what is your name?")
name, err := read_line()
if err.exists() {
print_line("no input")
return 0
}
print_line(name)
print_line("enter lines, end with ctrl-d:")
mut count: int64 = 0
mut done: bool = false
while !done {
line, e := read_line()
if e.exists() {
done = true
} else {
print_line(line)
count = count + 1
}
}
print_int(count)
return 0
}

Try it interactively with dusk run examples/input.dusk, or pipe a file in to see the redirected path.

parse_int_radix takes an explicit base and accepts the matching prefix; parse_float reads a base-10 float. Each returns a value with an error, and the must-handle rule forces the caller to look at it.

parse.dusk
@paradigm procedural
@import std.string
// Parse numbers from strings. parse_int_radix takes an explicit base and accepts
// the matching prefix, parse_float reads a base 10 float. Each returns the value
// with an error, so a bad parse is caught, not guessed.
func show(s: string, base: int64) -> void {
n, e := parse_int_radix(s, base)
if e.exists() {
println(-1)
} else {
println(n)
}
}
func main() -> int32 {
show("255", 10) // 255
show("0xFF", 16) // 255
show("0b1010", 2) // 10
show("0o17", 8) // 15
show("-42", 10) // -42
show("0xFF", 10) // -1, the 0x prefix is not base 10
f, fe := parse_float("3.5")
fe.ignore()
println(f + 0.5) // 4
g, ge := parse_float("bad")
if ge.exists() {
println(-2) // -2
} else {
println(g)
}
return 0
}

e.exists(), e.ignore(), and e.check(...) are the three sanctioned ways to discharge an error; dropping one is a compile error.

Files named m5.dusk through m9e.dusk date from the compiler’s development milestones, the numbered stages that built up the 0.1.0 core. Each file exercises the feature set that landed at its milestone, and a suffix letter marks a variation on the same theme. They read as a graded tour of the core language:

SeriesExercises
m5Functions, arithmetic, the procedural baseline
m6, m6b, m6cStructs, fixed arrays T[N], slices T[]
m7 through m7hEnums with payloads, exhaustive match, monomorphized generics
m8, m8b, m8cThe oop paradigm, interfaces and vtables, lambdas and closures
m9 through m9eThe functional paradigm: builtins, do notation, monads, errors

m9.dusk runs the four core functional builtins over one slice: map, filter, fold, and reduce, with foreach to print. Note that reduce returns a (value, error) pair because the input slice could be empty.

m9.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) })
evens := filter(nums, lambda (n: int64) -> bool { return n % 2 == 0 })
foreach(evens, lambda (n: int64) -> void { println(n) })
sum := fold(nums, 0, lambda (acc: int64, n: int64) -> int64 { return acc + n })
println(sum)
prod, prod_err := reduce(nums, lambda (a: int64, b: int64) -> int64 { return a * b })
prod_err.ignore()
println(prod)
return 0
}

m9b.dusk adds do notation over a bind and unit defined as free functions, and m9c.dusk moves them into a named monad block that a do Name { ... } selects:

monad Identity {
func bind(x: int64, f: (int64) -> int64) -> int64 {
return f(x)
}
func unit(x: int64) -> int64 {
return x
}
}
func main() -> int32 {
r := do Identity {
a <- 10
b <- 20
a + b
}
println(r)
return 0
}

m9e.dusk puts two monad blocks in one program to show that several monads coexist, each do naming the one it wants. And m9d.dusk is the errors-as-values tour in the functional paradigm: a fallible function returning (int64, error), the three handling forms, and reduce guarding the empty slice.

m9d.dusk
@paradigm functional
// Fallible function returns (value, error). The caller must handle the error.
func safe_div(a: int64, b: int64) -> (int64, error) {
if b == 0 {
return (0, error { message: "divide by zero" })
}
return (a / b, error {})
}
func main() -> int32 {
q, e := safe_div(10, 2)
e.ignore()
println(q)
bad, e2 := safe_div(1, 0)
if e2.exists() {
println(-1)
} else {
println(bad)
}
bad2, e3 := safe_div(7, 0)
e3.check(lambda (er: error) -> void { println(bad2) })
empty: int64[] = []
r, rerr := reduce(empty, lambda (a: int64, b: int64) -> int64 { return a + b })
if rerr.exists() {
println(-2)
} else {
println(r)
}
nums: int64[] = [2, 3, 4]
p, perr := reduce(nums, lambda (a: int64, b: int64) -> int64 { return a * b })
perr.ignore()
println(p)
return 0
}

The functional reference covers the builtins and do notation in full.

Examples added after 0.1.0 carry descriptive names instead of milestone numbers. A few clusters worth browsing:

  • Concurrency, from the 0.3.x line: spawnjoin, pingpong, pipeline, fanin, countermutex, bank, bounded, poolsum, trypoll, recvtimeout, and the flagship offload, which rehearses the park, wake, and offload loop the 0.4.x async releases build on. See the concurrency guide.
  • Memory safety faults: uaf, doublefree, stalefree, doublejoin, mutexheld, and friends are deliberate misuse programs that compile and then fault by name at runtime, demonstrating the generational heap’s checks. Run them expecting a named fault, not clean output.
  • I/O and interop: fileio, args, strbuf, and foreign, the foreign "C" block calling into libc across the raw pointer boundary.

Every example is a single dusk run away. When one imports a quoted git path rather than the stdlib, fetch first with dawn get or use dawn run; see dawn.