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 |
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 |
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.
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.
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 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.
@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.