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>.
Hello, dusk
Section titled “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.
Variables and inference
Section titled “Variables and inference”:= 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.
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.
Widths never mix
Section titled “Widths never mix”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 = 1b: int64 = 2c := a + b // error: arithmetic mixes int32 and int64; match the widthsThis 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 and structs
Section titled “Functions and structs”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.
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.
Enums and match
Section titled “Enums and match”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.
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.
Errors are values
Section titled “Errors are values”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.
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.
Manual memory
Section titled “Manual memory”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.
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.
Lambdas and functional builtins
Section titled “Lambdas and functional builtins”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 := 3triple := lambda (n: int64) -> int64 { return n * factor }The functional builtins map, filter, reduce, fold, and foreach take lambdas and require @paradigm functional:
@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.
One paradigm per file
Section titled “One paradigm per file”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.
@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.
Where next
Section titled “Where next”- 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.