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.
Always available
Section titled “Always available”| Builtin | Signature | Description |
|---|---|---|
alloc | alloc(value?) -> *T | heap allocate through the in scope allocator |
free | free(p: *T) -> void | deallocate through the in scope allocator |
print | print(...) -> void | print to stdout, handles all primitive types |
println | println(...) -> void | print to stdout with a newline |
printerr | printerr(...) -> void | println to stderr |
sizeof | sizeof(T) -> int64 | size of a type in bytes at compile time |
hash | hash(v) -> int64 | deterministic 64-bit hash of a hashable scalar or string |
spawn | spawn(f: () -> void) -> (thread, error) | start an OS thread running a lambda literal |
join | join(t: thread) -> error | wait for a thread; retires the handle |
submit | submit(f: () -> void) -> error | queue a lambda literal on the global thread pool |
async_run | async_run(g(args)) -> T | crank the event loop until a direct async call completes |
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, free, and sizeof
Section titled “alloc, free, and sizeof”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.
func main() -> int32 { p: *int64 = alloc(100) defer free(p) println(*p) println(sizeof(int64)) return 0}spawn, join, and submit
Section titled “spawn, join, and submit”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.
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.
async_run and the async keywords
Section titled “async_run and the async keywords”async_run, added in 0.4.2, is the bridge from synchronous code into the event loop. It takes a direct call of an async func written at the call site, cranks the loop until that call’s future completes, and yields the value. Like spawn, join, and submit, it is gated behind no paradigm, so any file can call it. It carries one placement rule: it runs only outside an async body, since a task frame can await a call directly instead.
async func and await are keywords, not builtins, also added in 0.4.2 and ungated. async func marks a function that compiles to a state machine over a heap frame, and await suspends inside one at exactly four statement positions. Both are covered in full in the async reference.
Numeric casts
Section titled “Numeric casts”int8, int16, int32, int64, char, rune, float32, and float64 double as builtin call forms that convert one scalar value explicitly. The first five arrived in 1.2.0 as integer width casts, and 1.5.0 added the other three and opened the cast across the integer and float boundary in both directions, so int32(v) and its siblings now take one integer, char, rune, bool, float32, or float64 and convert it to the named type. Each takes exactly one argument. A non scalar rejects, a numeric cast takes an integer, char, rune, or float value; string does not cast, and all eight names are reserved as builtin call forms, so a function cannot be declared with one of them, though a variable or field may still carry the name. See the type system and operators for the conversion rules per operand and target, including the float to integer saturation and the unchecked rune.
func main() -> int32 { a: int8 = int8(300) // 44, truncated to eight bits b: char = char(101) // 'e' c: float64 = float64(65) // 65, an integer read as a float d: int64 = int64(-2.9) // -2, truncated toward zero println(a) println(b) // e, a char prints as text println(c) println(d) return 0}hash(v), added in 1.5.1, returns a deterministic 64-bit hash of a hashable value: an integer of any width, a char, a rune, or a string. It is the key hash a generic map builds on, no paradigm gates it, and it takes exactly one argument. A float is refused, since a NaN is never equal to itself and would break the coherence between a hash and the equality a map compares its keys by. A struct, a pointer, a slice, and every other non scalar is refused too, cannot hash float64; a map key is an integer, char, rune, or string, naming the type it was handed. In a generic function hash(k) over a type parameter passes the surface pass, and a non hashable instantiation is rejected once the ground type is known, the same two pass shape a generic comparison takes.
A string hashes by its content rather than by its pointer, so two strings with the same bytes hash equal however they were built, matching the way == already compares strings. An integer, a char, or a rune hashes to its own value widened. The exact hash value is unspecified and may change between releases, so a program leans only on two guarantees: equal values hash equal, and a hash is stable within one run. Never store one or compare one against a number you wrote down.
hash reserves its name from a function declaration the same way the cast builtins do, 'hash' is a builtin; a function cannot take its name. That reject landed in 1.5.2, after a real soundness bug: the checker resolved a call to a user’s own hash while codegen resolved the bare name to the builtin unconditionally, so a two argument user hash silently hashed only its first argument and the program computed something the checker had never accepted. A variable or field named hash is still fine; only a function declaration collides.
@import std.string
func main() -> int32 { a: int64 = hash(int64(7)) b: int64 = hash(int64(7)) if a == b { println("equal ints hash equal") }
host: string = "xabcy" mid: string = substring(host, 1, 4) if hash("abc") == hash(mid) { println("a string hashes by content") } if hash("abc") != hash("abd") { println("distinct content hashes apart") } return 0}The print family
Section titled “The print family”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.
Since 1.1.0 the print family writes a char, a char[N], and a char[] as their text bytes, not as numbers. A char prints its one byte as a glyph, a char[N] and a char[] print their bytes straight through, and a format hole {} does the same when the value behind it is one of these three types. So s := "hi"; println(s[0]) prints h, not 104, a multibyte UTF-8 sequence prints its glyph whole, and an embedded NUL passes through. A rune is the exception: it still prints its codepoint number, since it is a 4 byte scalar rather than a byte of string text, and std.unicode’s encode_rune is what turns a scalar into displayed text. Reading a char’s numeric value is one annotated binding away, b: int64 = c.
The Display interface
Section titled “The Display interface”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 tuple, or a pointer. A slice is not printable either, with one exception: a char[], like a char[N] and a char itself, prints its bytes as text rather than being rejected, the rule described above. Print never emits silence for a value it cannot render. Like every dusk diagnostic, that error prints three lines: a header, the source line, and a caret run under the offending call, with the caret columns counted in Unicode scalar values.
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.
@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.
Functional builtins
Section titled “Functional builtins”These require @paradigm functional in the calling file.
| Builtin | Description |
|---|---|
map | applies a function to each element |
filter | filters a collection by predicate |
reduce | reduces a collection to one value |
fold | fold left or right |
foreach | iterates 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.
@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.
Procedural builtins
Section titled “Procedural builtins”These require @paradigm procedural. A file with no @paradigm directive defaults to procedural, so they are available by default.
| Builtin or keyword | Description |
|---|---|
for | for loop |
while | while loop |
do while | do while loop |
mut | declares a mutable variable |
Raw memory primitives
Section titled “Raw memory primitives”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.
| Builtin | Description |
|---|---|
alloc_bytes | alloc_bytes(n: int64) allocates n raw, uninitialized bytes through the in scope allocator |
ptr_add | byte arithmetic over a raw pointer; takes a *raw T or *void, not a managed *T, returns the same type |
cstr | reinterprets 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.
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.