Skip to content

Builtins

Builtins are functions the compiler provides directly, so you never import them. They are always available regardless of paradigm directives unless noted. Two groups are gated: the functional builtins require @paradigm functional, and the procedural constructs require @paradigm procedural. See Paradigm system for how directives stack.

BuiltinSignatureDescription
allocalloc(value?) -> *Theap allocate through the in scope allocator
freefree(p: *T) -> voiddeallocate through the in scope allocator
printprint(...) -> voidprint to stdout, handles all primitive types
printlnprintln(...) -> voidprint to stdout with a newline
printerrprinterr(...) -> voidprintln to stderr
sizeofsizeof(T) -> int64size of a type in bytes at compile time
spawnspawn(f: () -> void) -> (thread, error)start an OS thread running a lambda literal
joinjoin(t: thread) -> errorwait for a thread; retires the handle
submitsubmit(f: () -> void) -> errorqueue a lambda literal on the global thread pool

Beyond this table, a handful of I/O and diagnostic builtins are also available everywhere without an import: read_file, write_file, read_line, read_all, and parse_float are documented in stdlib I/O, the debug allocator counters in stdlib memory, and the move builtin for ownership transfer in Memory.

alloc and free are not a fixed implementation. They lower to a call on the allocator that is in scope, which is the default heap allocator unless a using parameter designates another. The allocation size is inferred from the declared type on the left hand side, so the programmer never passes a byte size. The uninitialized form alloc() requires the pointer annotation, since the annotation is what sizes the block. free must run under the allocator that produced the pointer. See Memory for the allocator interface, defer, and the generational safety checks.

sizeof(T) is evaluated at compile time and returns the size of a type in bytes as an int64.

builtins-alloc.dusk
func main() -> int32 {
p: *int64 = alloc(100)
defer free(p)
println(*p)
println(sizeof(int64))
return 0
}

spawn starts an OS thread and join waits for it. Both take no paradigm directive. spawn accepts only a lambda literal written at the call site, since only the literal site knows the environment layout the runtime copies; a closure variable cannot be spawned. Its error fires when the operating system refuses the thread. join blocks until the body returns and retires the handle, so a second join of the same handle faults through the same check a use after free hits.

builtins-spawn.dusk
func main() -> int32 {
t, e := spawn(lambda () -> void {
println("worker")
})
if e.exists() {
printerr(e)
return 1
}
je := join(t)
je.ignore()
return 0
}

submit, added in 0.3.3, queues a task on the global thread pool. It shares spawn’s whole argument rule, returns only an error, and never blocks the submitter; its error exists only when the pool is not running. The pool is started and shut down through std.concurrent.pool. Capture rules, the memory model, and the pool lifecycle are covered in Concurrency.

print writes to stdout with no newline, println appends one, and printerr writes to stderr with a newline. Each handles all primitive types, including strings.

Any type that implements the Display interface can be passed to print and println.

interface Display {
toString() -> string;
}

Passing a struct with no Display impl to a print builtin is a compile error, as is printing an enum, a slice, a tuple, or a pointer. Print never emits silence for a value it cannot render.

Declaring an interface and writing an impl require @paradigm oop, so a file that gives its structs a Display impl declares that directive. Printing the value afterward needs no paradigm.

builtins-display.dusk
@paradigm oop
interface Display {
toString() -> string
}
struct Point {
x: int64,
y: int64,
}
impl Display for Point {
func toString() -> string {
return "point"
}
}
func main() -> int32 {
p := Point { x: 1, y: 2 }
print("point is ")
println(p)
return 0
}

An error value can also be passed to the print builtins, as the printerr(e) call above shows; its text comes from its toString, described in Error handling.

These require @paradigm functional in the calling file.

BuiltinDescription
mapapplies a function to each element
filterfilters a collection by predicate
reducereduces a collection to one value
foldfold left or right
foreachiterates for side effects

They take lambdas, which capture outer variables by immutable copy. fold takes an explicit initial value. reduce returns a (T, error) pair and guards the empty slice, so the caller resolves the error like any other.

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

Gating is per file. A file without @paradigm functional cannot call map directly, but it can call a user defined function that internally uses map. See Functional for the full set of functional concepts, including monads and do notation.

These require @paradigm procedural. A file with no @paradigm directive defaults to procedural, so they are available by default.

Builtin or keywordDescription
forfor loop
whilewhile loop
do whiledo while loop
mutdeclares a mutable variable

The raw pointer layer, *raw T and *void, carries strings, slice data, and collection buffers, and comes with three primitives. The specification’s builtins table does not enumerate alloc_bytes and ptr_add; they appear in the specification’s concurrency examples, the changelog for 0.2.1, and throughout the standard library sources, and are documented here from those.

BuiltinDescription
alloc_bytesalloc_bytes(n: int64) allocates n raw, uninitialized bytes through the in scope allocator
ptr_addbyte arithmetic over a raw pointer; takes a *raw T or *void, not a managed *T, returns the same type
cstrreinterprets a NUL terminated *char buffer as a string at no runtime cost

alloc_bytes is the base primitive for arenas and growable buffers; std.vector, std.map, and StringBuilder are built on it. The binding’s raw pointer annotation types the result, the same way an annotation sizes alloc(), and the block is released with free. cstr is what sb_cstr in std.string uses to hand back a string view of a builder’s buffer.

builtins-raw.dusk
func main() -> int32 {
buf: *raw char = alloc_bytes(3)
buf[0] = 'h'
buf[1] = 'i'
buf[2] = '\0'
s: string = cstr(buf)
println(s)
tail: *raw char = ptr_add(buf, 1)
println(cstr(tail))
free(buf)
return 0
}

Raw pointers are one word and carry no generation, so nothing checks a dereference through them: use after free and double free on the raw layer are the programmer’s responsibility. The managed *T layer, where every dereference is checked, is described in Memory.