Skip to content

Language tour

This is a whirlwind pass over dusk as of 0.3.3. Each stop shows a small program that compiles today and links to the reference page that covers the details. If you have not built the toolchain yet, start with Getting started. Every complete program below runs with dusk run <file>.

tour_hello.dusk
func main() -> int32 {
println("hello, dusk")
return 0
}

main returns an int32 exit code, and 0 means success. println is a builtin, always available with no import. A file with no @paradigm directive defaults to procedural, so this file needs no top-of-file syntax at all. See Builtins and Source files.

:= infers a variable’s type from the right-hand side at compile time. Integer literals default to int64 and float literals to float64; an annotation or a literal suffix picks any other width. Variables are immutable by default, and an unused variable is a compile error.

variables.dusk
func main() -> int32 {
x := 5 // inferred int64, the default integer type
y := 3.14 // inferred float64, the default float type
ok := true // inferred bool
small: int32 = 5 // explicit annotation picks another width
f := 2.5f32 // literal suffix picks float32
println(x)
println(y)
println(ok)
println(small)
println(f)
return 0
}

See Types for the full primitive table.

Numeric widths never mix silently. Arithmetic, comparison, assignment, and argument passing all take operands of one width, so there is no implicit widening or truncation.

a: int32 = 1
b: int64 = 2
c := a + b // error: arithmetic mixes int32 and int64; match the widths

This fragment is a compile error, shown here as a fragment for exactly that reason. A bare literal adapts to the width beside it, so a + 1 is fine, and a literal that cannot fit its annotated width is rejected. See Types.

Functions are declared with func. All parameters are passed by value, always; there are no reference types and no overloading. A struct is a plain data container, available in every paradigm.

point.dusk
struct Point {
x: int64,
y: int64,
}
func manhattan(p: Point) -> int64 {
return p.x + p.y
}
func main() -> int32 {
p := Point { x: 3, y: 4 }
println(manhattan(p))
return 0
}

Generic functions are written func id<T>(x: T) -> T and are monomorphized per use. Methods attach to structs through impl, and interfaces provide polymorphism under @paradigm oop. See Functions and OOP.

An enum is a tagged union: a value is exactly one of several named variants, each optionally carrying payload fields. match inspects one and must be exhaustive, so a missing variant is a compile error.

shapes.dusk
enum Shape {
Circle(radius: int64),
Rect(w: int64, h: int64),
Empty,
}
func area(s: Shape) -> int64 {
match s {
Circle(r) => return r * r * 3,
Rect(w, h) => return w * h,
Empty => return 0,
}
}
func main() -> int32 {
println(area(Shape.Circle(5)))
println(area(Shape.Rect(4, 6)))
println(area(Shape.Empty))
return 0
}

Enums are paradigm-agnostic and back the standard library’s Maybe and Either. See Enums.

There are no exceptions and no panic. A fallible function returns a (T, error) tuple, and the call site must bind and use the error: inspect it with exists(), handle it with check(...), or discard it explicitly with ignore(). An unhandled error binding is a compile error.

tour_divide.dusk
func safe_div(a: int64, b: int64) -> (int64, error) {
if b == 0 {
return (0, error { message: "division by zero" })
}
return (a / b, error {})
}
func main() -> int32 {
q, e := safe_div(10, 2)
if e.exists() {
printerr(e)
return 1
}
println(q)
q2, e2 := safe_div(1, 0)
e2.check(lambda (err: error) -> void { printerr(err) })
println(q2)
return 0
}

error {} is the empty, non-error value. There is no _ suppression; ignore() is the visible, greppable replacement. See Error handling and the errors guide.

Memory is explicit. alloc heap-allocates through the in-scope allocator, sized by the declared type on the left-hand side, so you never pass a byte count. Dereference is explicit with the * prefix. defer runs cleanup at function exit, in reverse order of registration, including on an early return.

tour_heap.dusk
struct Point {
x: int64,
y: int64,
}
func main() -> int32 {
n: *int64 = alloc(41)
defer free(n)
println(*n + 1)
p: *Point = alloc(Point { x: 3, y: 4 })
defer free(p)
println((*p).x + (*p).y)
return 0
}

Since 0.2.0 the default heap is generational: every managed pointer carries a generation checked at each dereference, so a use after free or double free faults instead of corrupting memory. See Memory and the memory guide.

A lambda is an anonymous function and a first-class value. It captures outer variables by immutable copy, taken at creation. There is no capture by reference.

factor := 3
triple := lambda (n: int64) -> int64 { return n * factor }

The functional builtins map, filter, reduce, fold, and foreach take lambdas and require @paradigm functional:

tour_pipeline.dusk
@paradigm functional
func main() -> int32 {
nums: int64[] = [1, 2, 3, 4, 5]
doubled := map(nums, lambda (n: int64) -> int64 { return n * 2 })
evens := filter(doubled, lambda (n: int64) -> bool { return n % 4 == 0 })
sum := fold(evens, 0, lambda (acc: int64, n: int64) -> int64 { return acc + n })
foreach(doubled, lambda (n: int64) -> void { println(n) })
println(sum)
return 0
}

See Functional concepts for monads and do notation.

Every file declares the paradigms it uses, and the directives gate builtins and syntax within that file: functional unlocks the builtins above, procedural unlocks for, while, and mut, and oop unlocks interface and composition. Directives stack, and a file with none defaults to procedural.

tour_stacked.dusk
@paradigm procedural
@paradigm functional
func main() -> int32 {
nums: int64[] = [1, 2, 3, 4]
doubled := map(nums, lambda (n: int64) -> int64 { return n * 2 })
mut i: int64 = 0
while i < 4 {
println(doubled[i])
i = i + 1
}
return 0
}

Gating is per file only. Functions and types you define are paradigm-agnostic and callable from any file, whatever its directives. See Paradigm system and the paradigms guide.

  • The memory guide covers arenas, the allocator interface, and the generational heap in depth.
  • The concurrency guide covers spawn, join, channels, mutexes, and the thread pool added across the 0.3.x line.
  • The packages guide covers imports and the dawn package tool.
  • Examples walks the runnable programs in the repository’s examples/ directory.
  • Or try code in the playground without installing anything.