Standard library

This page is the complete API reference for the Dawn standard library, generated straight from the compiler by dawn doc --stdlib: every signature and every paragraph below is the one the compiler holds right now.

The names come from two places — the compiler's builtin table, and the std/ modules bundled with it (Dawn source). Which of the two implements a name is not something a caller can observe: it changes neither the spelling nor the type nor the semantics. So this page is not grouped by builtin/std, but by how the name is written.

How to read it:

The specification those links lead to is written in Chinese. This page, the tutorial and the examples come in both languages.

Built-in types

These types are part of the language itself. They need no use and have no std source to read; their semantics are defined in the specification, and what follows is a one-line index.

TypeIn one line
Int64-bit signed integer
Floatdouble precision; rendering and parsing are pinned by std/fmt and do not follow the host (§4.3)
Booltrue / false
Stringimmutable string, measured in code points; there is no ill-formed UTF-8 inside one
Unitthe single value (), first class, allowed wherever a value is
List[T]immutable persistent list (32-way trie + tail block); accumulating with acc ++ [x] is linear
Option[T]Some(T) or None; the language has no null
Result[T, E]Ok(T) or Err(E); ? has syntax for it (§8.1)
ForeignErrorthe structured payload of a foreign failure: a kind and one human-readable line (§9.8.1)
Map[K, V]immutable persistent map, iterated in insertion order; keys need Eq + Hash (§2.2)
Set[T]immutable persistent set, likewise
Bytesimmutable byte string, equal by content; binary data goes through it instead of borrowing a string (§9.5.1)
Bufa write cursor onto Bytes: bytes.buf() opens one, bytes.freeze() closes it
Cursora position in a String, not an index; declared opaque by std/cursor
(A, B, ...)tuple
fn(A, B) -> C !efunction type; !e may be left off, which means pure

The persistent HAMT under Map / Set and the persistent vector under List are pure Dawn source (std/hamt, std/pvec). They are internal modules — the representation has to be replaceable, and replaceable means no program depends on it, so use-ing them outside std is a compile error and this page does not list them.

prelude: the names that need no use

These names are in scope implicitly — just write them. The groups are there to make things findable and say nothing about where a name is implemented; the ones that also live in a std module (sort, println and so on) appear once more under Modules below in their qualified spelling — the same function, two ways of writing it.

io

println

fn println(s: String) -> Unit !io

print a string followed by a newline

print

fn print(s: String) -> Unit !io

print a string without a newline

args

fn args() -> List[String] !io

command line arguments

catch_fault

fn catch_fault[T, !e](f: fn() -> T !e) -> Result[T, ForeignError] !io

run a closure, turning an external failure into Err (panics pass through)

catch_panic

fn catch_panic[T, !e](f: fn() -> T !e) -> Result[T, ForeignError] !io

run a closure at an isolation boundary, catching a panic or an exception into Err

bracket

fn bracket[A, B, !e](resource: A, release: fn(A) -> Unit !e, body: fn(A) -> B !e) -> B !e

use an acquired resource and release it on every way out -- the value, a panic or a fault; the failure carries on unchanged (this catches nothing)

list

map

fn map[T, U, !e](xs: List[T], f: fn(T) -> U !e) -> List[U] !e

a new list with a function applied to every element

filter

fn filter[T, !e](xs: List[T], f: fn(T) -> Bool !e) -> List[T] !e

the elements satisfying a predicate, in order

fold

fn fold[T, A, !e](xs: List[T], init: A, f: fn(A, T) -> A !e) -> A !e

reduce from the left with an accumulator

len

fn len[T](xs: List[T]) -> Int

number of elements

get

fn get[T](xs: List[T], i: Int) -> Option[T]

element at an index; None when out of bounds

range

fn range(from: Int, to: Int) -> List[Int]

the integers from..to (exclusive)

sort

fn sort[T: Ord](xs: List[T]) -> List[T]

ascending stable sort (elements need Ord)

sort_by

fn sort_by[T, !e](xs: List[T], cmp: fn(T, T) -> Int !e) -> List[T] !e

stable sort with a two-argument cmp function

max

fn max[T: Ord](xs: List[T]) -> Option[T]

the first greatest element; None when empty (elements need Ord)

min

fn min[T: Ord](xs: List[T]) -> Option[T]

the first least element; None when empty (elements need Ord)

max_by

fn max_by[T, K: Ord, !e](xs: List[T], key: fn(T) -> K !e) -> Option[T] !e

the first element whose key is greatest; None when empty (keys need Ord)

min_by

fn min_by[T, K: Ord, !e](xs: List[T], key: fn(T) -> K !e) -> Option[T] !e

the first element whose key is least; None when empty (keys need Ord)

string

to_string

fn to_string[T: Show](x: T) -> String

render any printable value (numbers, strings, derive Show data)

join

fn join(xs: List[String], sep: String) -> String

concatenate with a separator

parse_int

fn parse_int(s: String) -> Option[Int]

parse a decimal integer; None on malformed input

parse_float

fn parse_float(s: String) -> Option[Float]

parse a floating point number; None on malformed input

parse_int_radix

fn parse_int_radix(s: String, radix: Int) -> Option[Int]

parse an integer in base 2 through 36; None on malformed input

interop

cast

fn cast[T](x: Object) -> Result[T, ForeignError]

reclaim an erased Java Object as a concrete reference type T, taken from the expected type at the call site (interop escape; a runtime CHECKCAST guards it, and a miss is an Err rather than a throw)

char

code_points

fn code_points(s: String) -> List[Char]

split a string into Unicode scalar-value characters

from_code_points

fn from_code_points(cs: List[Char]) -> String

assemble a string from characters

char_is_letter

fn char_is_letter(c: Char) -> Bool

whether a character is a Unicode letter

char_is_digit

fn char_is_digit(c: Char) -> Bool

whether a character is a decimal digit

char_is_alnum

fn char_is_alnum(c: Char) -> Bool

whether a character is a letter or decimal digit

char_is_upper

fn char_is_upper(c: Char) -> Bool

whether a character is upper-case

char_is_lower

fn char_is_lower(c: Char) -> Bool

whether a character is lower-case

char_is_space

fn char_is_space(c: Char) -> Bool

whether a character is whitespace

option

expect

fn expect[T](o: Option[T], msg: String) -> T

unwrap a Some or panic with a message

unwrap_or

fn unwrap_or[T](o: Option[T], fallback: T) -> T

the Some value, or a fallback for None

conversion

to_float

fn to_float(n: Int) -> Float

Int to Float

to_int

fn to_int(x: Float) -> Int

Float to Int (truncates)

control

panic

fn panic(msg: String) -> Never

abort with a message (diverges, so usable at any type)

todo

fn todo() -> Never

placeholder that panics when reached

Prelude traits

The five traits the prelude brings. Their method names enter the function namespace as well, and a top-level declaration in the calling module shadows them (spec §10.6).

Ord

trait Ord[T] {
  fn cmp[T: Ord](a: T, b: T) -> Int
}

total order. cmp(a, b) answers negative / zero / positive, and is what < <= > >= mean past the scalars; sort and the extremes ask for it

Eq

trait Eq[T] {
  fn eq[T: Eq](a: T, b: T) -> Bool
}

equality, behind == and !=

Hash

trait Hash[T] {
  fn hash[T: Hash](x: T) -> Int
}

a value's hash, asked for alongside Eq by Map keys and Set elements ([K: Eq + Hash])

Show

trait Show[T] {
  fn show[T: Show](x: T) -> String
}

how to_string and string interpolation render a value

Iter

trait Iter[T] {
  type Cur
  type Item
  fn iter_start[T: Iter](c: T) -> T.Cur
  fn iter_done[T: Iter](c: T, k: T.Cur) -> Bool
  fn iter_next[T: Iter](c: T, k: T.Cur) -> T.Cur
  fn iter_get[T: Iter](c: T, k: T.Cur) -> T.Item
}

what for x in c walks: a cursor type, an item type, and the four moves over them

Index

trait Index[T] {
  type Idx
  type Item
  fn index[T: Index](c: T, i: T.Idx) -> T.Item
}

what c[i] resolves to: an index type, an item type, and one method. List and Map come with an impl; a type that writes one gets []

Display

trait Display[T] {
  fn display[T: Display](x: T) -> String
}

the top-level rendering: an impl decides what to_string and string interpolation produce for the type, in place of the Show they would otherwise fall back on. Not derivable, and Show is still the bound to_string asks for

Modules

Brought in with use std/x and then called qualified; single names can be imported on their own (use std/str.{trim}).

std/cursor

Positions in a String. A cursor is a position rather than a count, so stepping through a string costs the same at every offset — where a code-point index costs a walk from the start (spec §11).

impls registered by this module (visible program-wide, no use needed): Show[Cursor], Ord[Cursor]

Cursor

type Cursor

A position in a String: an index the language will not let you compute.

cursor.start

fn start(s: String) -> Cursor

The position before the first character.

cursor.end

fn end(s: String) -> Cursor

The position after the last character.

cursor.done

fn done(s: String, c: Cursor) -> Bool

Whether c is at the end of s.

cursor.char

fn char(s: String, c: Cursor) -> Int

The code point at c, or -1 at the end.

cursor.next

fn next(s: String, c: Cursor) -> Cursor

The position one character forward (clamped to the end).

cursor.prev

fn prev(s: String, c: Cursor) -> Cursor

The position one character back (clamped to the start).

cursor.slice

fn slice(s: String, from: Cursor, to: Cursor) -> String

The text between two positions.

cursor.skip

fn skip(s: String, c: Cursor, sub: String) -> Cursor

The position just past sub, which is known to start at c — the one sanctioned "advance past a literal" that would otherwise be cursor arithmetic.

Walks both strings together rather than counting sub first, so it needs no length and no arithmetic.

cursor.starts_at

fn starts_at(s: String, c: Cursor, sub: String) -> Bool

Whether sub occurs in s starting exactly at c. An empty sub is at every position, including the end.

cursor.find

fn find(s: String, sub: String, from: Cursor) -> Option[Cursor]

The position of the first occurrence of sub at or after from, or None.

cursor.back

fn back(s: String, c: Cursor, n: Int) -> Cursor

The position n characters back from c (clamped to the start).

cursor.seek

fn seek(s: String, i: Int) -> Cursor

The position i characters from the start of s.

Clamped, not asserted: a negative i is the start and an i past the end is the end (spec §4.8, criterion 3, the same clause next, prev and back already answer to). Every position in a string is a legal answer here -- the guarantee Cursor carries is that arithmetic cannot land you between the halves of a surrogate pair, and walking i characters cannot.

Named seek and not at because those two words are the language's two out-of-range policies and one name cannot hold both: at is criterion 1 (str.at, bytes.at, xs[i] -- the caller asserts the position exists and a miss panics), this is criterion 3. Spelled cursor.at until v0.54.0; rationale in docs/stdlib-naming.md.

O(i). Use it once, at the point where an Int index arrives from outside; a loop that calls it per character is the O(n^2) it exists to remove.

cursor.offset

fn offset(s: String, c: Cursor) -> Int

How many characters lie before c: the code-point index seek would take.

One pass, counting forward from the start, rather than the two-scan version packages/json wrote for itself.

c must be a position in s. Handing it one from another string is not a thing this function has an answer for -- see the module note above.

std/str

String operations, over code points: trimming, case, search, slicing, splitting and padding (spec §11).

impls registered by this module (visible program-wide, no use needed): Iter[String]

str.len

fn len(s: String) -> Int

Number of Unicode code points in s (not UTF-16 units, and not chars' list length).

str.is_empty

fn is_empty(s: String) -> Bool

True when the string has no characters.

str.trim

fn trim(s: String) -> String

s without leading or trailing whitespace.

str.trim_start

fn trim_start(s: String) -> String

s without leading whitespace.

str.trim_end

fn trim_end(s: String) -> String

s without trailing whitespace.

str.to_lower

fn to_lower(s: String) -> String

s lowercased by simple (1:1) Unicode mapping: one code point in, one out, no locale and no context, so the code-point count is unchanged.

str.to_upper

fn to_upper(s: String) -> String

s uppercased by simple (1:1) Unicode mapping. Not the length-changing full mapping -- ß stays ß rather than becoming SS, because full mapping is locale- and context-sensitive and so is not a function a second backend can implement from a table (spec 11). That belongs in a library taking a locale.

str.contains

fn contains(s: String, sub: String) -> Bool

Whether sub occurs anywhere in s.

str.starts_with

fn starts_with(s: String, prefix: String) -> Bool

Whether s begins with prefix.

str.ends_with

fn ends_with(s: String, suffix: String) -> Bool

Whether s ends with suffix.

O(len(suffix)), not O(len(s)). The walk back from the end was always the short one; the cost was the guard in front of it, which asked for len(s) and so read the whole string to answer a question about its last few characters. The guard was redundant: back clamps to the start when suffix is longer than s (criterion 3), and starts_at then runs out of s before it runs out of suffix and answers false on its own.

str.strip_prefix

fn strip_prefix(s: String, prefix: String) -> Option[String]

s with prefix removed, or None when s does not begin with it.

Inquiry (spec §4.8, criterion 2): "no such prefix" is a branch the caller takes, not a bug. Testing and peeling in one call is the point -- the pair starts_with plus a hand-computed slice(s, 16, len(s)) is what put magic offsets in 33 call sites, and an offset that is one out still compiles. No arithmetic happens here either: cursor.skip walks past the prefix.

str.strip_suffix

fn strip_suffix(s: String, suffix: String) -> Option[String]

s with suffix removed, or None when s does not end with it. The mirror of strip_prefix, and the answer to - 5 meaning len(".dawn").

str.index_of

fn index_of(s: String, sub: String) -> Option[Int]

Where the first occurrence of sub begins, in code points, or None.

The answer is a position in [0, len], not the index of an element: an empty sub occurs at position 0 of the empty string, and len(s) is a legal answer here (see last_index_of) though no character lives there. "Index" in the name is the unit it counts in -- code points, not UTF-16 units and not bytes -- and cursor.find is the one that answers with a position you can slice at directly.

str.last_index_of

fn last_index_of(s: String, sub: String) -> Option[Int]

Where the last occurrence of sub begins, in code points, or None. A position in [0, len]: an empty sub is found at the end, which is len(s) -- past the last character, which is why the answer is a position and not an element index.

str.from_char

fn from_char(c: Char) -> String

The one-character string for c, asked for by name.

"${c}" is the same string, through std/char's impl Display[Char] (spec §4.3). This one does not depend on an impl being in scope, and it is the spelling to reach for where a Char is being converted rather than rendered -- which is what nearly every caller in this repository is doing. A Char nested inside a structure still renders as its code point, because that is Show, and Show[Char] is still the Int's (spec §2.7).

str.repeat

fn repeat(s: String, n: Int) -> String

s repeated n times; n <= 0 yields the empty string.

Doubling: O(len(s) * n) on both backends, and stack-safe at any n. Why the two obvious shapes are not, in docs/stdlib-impl-notes.md.

str.slice

fn slice(s: String, from: Int, to: Int) -> String

The code points of s in [from, to). Both ends are clamped into range (a negative index reads as 0, one past the end as len) and from > to yields the empty string, so this never panics — a range parameter selects a stretch, it does not assert its endpoints exist (spec §4.8).

Spelled substring until v0.54.0; rationale in docs/stdlib-naming.md.

O(to), not O(len(s)). It walked the whole string until 2026-08-17, three times over: len(s) to clamp with, code_points(s) to materialise every character of it as a List[Char], and a rebuild. So slice(s, 0, 1) cost and allocated in proportion to a string it was asked for one character of. at went through cursor.seek and this did not; positions are what the cursor layer is for, and it clamps on its own (criterion 3), which is why no length is needed to clamp against.

str.take

fn take(s: String, n: Int) -> String

The first n characters, or all of them when there are fewer. n is clamped into [0, len], so this never panics: it names a stretch rather than asserting that position n exists (spec §4.8, criterion 3), the same way slice and list.take do.

truncate would be this function under another name, so there is no truncate.

str.drop

fn drop(s: String, n: Int) -> String

Everything after the first n characters; n is clamped into [0, len], so this never panics. This is the "the rest of it" that 50 call sites spell slice(x, i, str.len(x)).

O(len(s) - n): what it returns, and nothing on top. Spelled through slice it also paid a len(s) to name the far end, which is the one thing the end cursor already is.

str.at

fn at(s: String, i: Int) -> Char

The character at i. Panics when i is out of range -- i is a position the caller says exists, exactly like xs[i] and bytes.at (spec §4.8, criterion 1); get-style inquiry about a character is cursor.char on a position you already hold.

A Char, not a one-character string: it is the currency code_points and from_char trade in, and chars is the function that answers with strings. cursor.char stays an Int one layer down, because it answers -1 past the end and a sentinel cannot live in a type whose every value is a character (spec §4.8).

str.pad_start

fn pad_start(s: String, width: Int, pad: String) -> String

s left-padded with pad until it is width code points wide. Returns s unchanged when it is already wide enough or pad is empty; the filler is truncated to land exactly on width.

str.pad_end

fn pad_end(s: String, width: Int, pad: String) -> String

s right-padded with pad until it is width code points wide. The mirror of pad_start, with the same clauses: s comes back unchanged when it is already wide enough or pad is empty, and the filler is truncated to land exactly on width.

str.reverse

fn reverse(s: String) -> String

s with its code points in reverse order (surrogate pairs stay intact).

str.chars

fn chars(s: String) -> List[String]

The characters of s, each as a one-character string. A surrogate pair stays one element — this splits by character, not by unit.

str.split

fn split(s: String, sep: String) -> List[String]

s cut at each occurrence of sep, left to right. Adjacent separators yield empty pieces, and the result always has one more piece than there were separators. An empty sep yields the characters, as chars does.

str.split_once

fn split_once(s: String, sep: String) -> Option[(String, String)]

s cut in two at the first occurrence of sep: what came before it and what came after, or None when sep does not occur.

Inquiry (spec §4.8, criterion 2), the same shape as strip_prefix: "no separator" is a branch the caller takes. split answers a different question -- it has no way to say "there was no separator", because one piece is also what a string with no separator yields, and the two are different answers to key=value parsing.

Testing and cutting in one call is the point. The pair index_of plus two slice calls is three passes and two chances to be one out; this is one pass and no arithmetic. An empty sep occurs at the start, so it answers ("", s).

str.rsplit_once

fn rsplit_once(s: String, sep: String) -> Option[(String, String)]

s cut in two at the last occurrence of sep. The mirror of split_once, and the one to reach for when the separator also occurs inside the left part: rsplit_once("a/b/c.d", "/") is ("a/b", "c.d").

str.replace

fn replace(s: String, from: String, to: String) -> String

s with every occurrence of from replaced by to, left to right and non-overlapping. An empty from splices to between the characters, the same way split with an empty separator yields them.

std/char

Characters: the type 'a' is heading for, and the questions worth asking about one (spec §1.5, §11).

impls registered by this module (visible program-wide, no use needed): Display[Char], Show[Char]

char.code

fn code(c: Char) -> Int

The code point c stands for.

char.of

fn of(n: Int) -> Option[Char]

The character with code point n, or None when n is not one.

Option rather than a panic because "is this integer a character" is a question with a legitimate negative answer -- it is exactly what a parser reading a \u escape needs to ask (spec §4.8, criterion 2). The bulk conversion from_code_points is not asking, and keeps its panic.

Not a Unicode scalar value: negative, above U+10FFFF, or in the surrogate range D800..DFFF. Those last are halves of a UTF-16 pair and never stand for a character on their own, which is the whole reason Cursor exists.

char.is_letter

fn is_letter(c: Char) -> Bool

Whether c is a letter.

char.is_digit

fn is_digit(c: Char) -> Bool

Whether c is a decimal digit.

char.is_alnum

fn is_alnum(c: Char) -> Bool

Whether c is a letter or a digit.

char.is_upper

fn is_upper(c: Char) -> Bool

Whether c is an upper-case letter.

char.is_lower

fn is_lower(c: Char) -> Bool

Whether c is a lower-case letter.

char.is_space

fn is_space(c: Char) -> Bool

Whether c is whitespace.

std/fmt

The language's own number rendering and parsing: to_string(Float) and the parse_* family are answered here, by Dawn source rather than by whichever host is running (spec §4.3 and §11's grammar).

fmt.dtoa

fn dtoa(v: Float) -> String

to_string(Float): the shortest decimal string that reads back as exactly v, in the two forms and three special spellings spec §4.3 fixes. This is what both backends and comptime folding call; user code goes through to_string and interpolation rather than calling it directly.

std/list

Operations on the persistent List: mapping, folding, searching, sorting, and the quantifiers. map, filter, fold and the sort family are in the prelude and need no use.

impls registered by this module (visible program-wide, no use needed): Iter[List[T]], Eq[List[T]], Hash[List[T]], Ord[List[T]], Show[List[T]]

list.is_empty

fn is_empty[T](xs: List[T]) -> Bool

Whether xs has no elements. The prelude answers "how many" (len) for every container; this is the other half, spelled the same way for all of them (CONTRIBUTING §naming). It asks the cursor rather than comparing a count, so it stays O(1) on a representation whose length is not.

list.map

fn map[T, U, !e](xs: List[T], f: fn(T) -> U !e) -> List[U] !e

f applied to every element, in order.

list.filter

fn filter[T, !e](xs: List[T], f: fn(T) -> Bool !e) -> List[T] !e

The elements satisfying f, in order.

list.fold

fn fold[T, A, !e](xs: List[T], init: A, f: fn(A, T) -> A !e) -> A !e

Reduce from the left: f(f(f(init, x0), x1), ...).

list.find

fn find[T, !e](xs: List[T], pred: fn(T) -> Bool !e) -> Option[T] !e

The first element satisfying pred, or None.

list.any

fn any[T, !e](xs: List[T], f: fn(T) -> Bool !e) -> Bool !e

Whether f holds for at least one element. false for the empty list. Short-circuits: f runs on the elements up to the first that satisfies it and no further, which is observable when f has an effect.

list.all

fn all[T, !e](xs: List[T], f: fn(T) -> Bool !e) -> Bool !e

Whether f holds for every element. true for the empty list. Short-circuits at the first element that fails, as any does.

list.none

fn none[T, !e](xs: List[T], f: fn(T) -> Bool !e) -> Bool !e

Whether f holds for no element. true for the empty list. Short-circuits at the first element that satisfies f, as any does.

The third of the three quantifiers, and the one a caller otherwise spells not any(xs, f) -- which reads as a negated question rather than as the question it is, and puts the negation where a reader has to unwind it. Defined as that negation, so the short-circuit and the empty-list answer are any's by construction rather than by a second walk agreeing with it.

list.index_of

fn index_of[T: Eq](xs: List[T], x: T) -> Option[Int]

The index of the first element equal to x, or None.

An element index in [0, len), and not a position: the index_of that search for a subsequence (str, bytes) can answer len, because an empty needle matches past the last element. This one compares elements, so every answer it gives names one.

Inquiry (spec §4.8, criterion 2): a miss is a branch the caller takes. The currency is str.index_of's -- Option[Int], never a -1, which is the one sentinel this library does not hand out.

list.contains

fn contains[T: Eq](xs: List[T], x: T) -> Bool

Whether some element equals x. Stops at the first match; the same answer as index_of(xs, x) being Some, and the same walk.

list.unique

fn unique[T: Eq + Hash](xs: List[T]) -> List[T]

The elements with later duplicates dropped: every value survives once, at the position of its first occurrence.

Eq + Hash and not Eq alone, which is the one place this family asks for more than the audit did. An Eq-only dedupe rescans the output for each element and is quadratic, and what callers deduplicate here are lists of module paths, file names and diagnostics -- the sizes where that starts to cost. A Set is Eq + Hash by construction, and it already answers the ordering clause: a set collapses duplicates keeping the first occurrence's position and gives them back in that order, so this is one line rather than a second implementation of the same rule.

set_from/set_to_list are the same primitives std/set forwards to, not a second route to the structure; why this module calls them directly is in docs/stdlib-impl-notes.md.

list.take

fn take[T](xs: List[T], n: Int) -> List[T]

The first n elements; n is clamped into range, so this never panics.

list.drop

fn drop[T](xs: List[T], n: Int) -> List[T]

Everything after the first n elements; n is clamped into range.

list.slice

fn slice[T](xs: List[T], from: Int, to: Int) -> List[T]

The elements in [from, to). Both ends are clamped into range, so out-of-bounds indices never panic; from >= to yields the empty list.

list.reverse

fn reverse[T](xs: List[T]) -> List[T]

A reversed copy; the input is not changed.

list.sort

fn sort[T: Ord](xs: List[T]) -> List[T]

Ascending stable sort; elements need Ord. Delegates to sort_by with the bound cmp — the dictionary crosses as an ordinary captured value, which is what let this leave the builtin table (docs/pure-ffi-design.md section 14).

list.max_by

fn max_by[T, K: Ord, !e](xs: List[T], key: fn(T) -> K !e) -> Option[T] !e

The first element whose key is greatest; None when empty. Keys need Ord; key runs once per element.

list.min_by

fn min_by[T, K: Ord, !e](xs: List[T], key: fn(T) -> K !e) -> Option[T] !e

The first element whose key is least; None when empty. Keys need Ord; key runs once per element.

list.max

fn max[T: Ord](xs: List[T]) -> Option[T]

The first greatest element; None when empty. Elements need Ord.

list.min

fn min[T: Ord](xs: List[T]) -> Option[T]

The first least element; None when empty. Elements need Ord.

std/bytes

Byte strings: UTF-8 and Latin-1 decoding, slicing, search, and the hex and base64 text encodings (spec §9.5.1).

impls registered by this module (visible program-wide, no use needed): Show[Utf8Error], Iter[Bytes]

Buf

type Buf

A byte buffer under construction.

Bytes is finished and immutable, and until this existed it was also unproduceable: you could take a string's UTF-8, slice it and read it, but a byte you computed had no spelling at all -- 0x80 would need a string whose UTF-8 encoding is that single byte, and there is none. Anything that decompresses writes bytes it computed, so this is the missing half.

It is a value like everything else -- put returns the buffer rather than mutating it -- and it inherits array_push's clause: the backend extends in place exactly when the version it is handed is the only one that could be reading the frontier, so building a megabyte is linear and not quadratic. buf_at is here because the callers that need this are decompressors, and an LZ77 back-reference reads output that was written a moment ago.

Utf8Error

type Utf8Error = { offset: Int }

Why decode_utf8_checked refused. offset is the byte position where the first malformed sequence begins, which is also how many leading bytes of the input were valid UTF-8.

No kind field. The distinctions a UTF-8 decoder could report (a tail cut short, an overlong form, a surrogate half, a lead byte that leads nothing) are not ones a caller acts on differently, and the standing test for that is packages/json's JsonErrorKind: kinds are the branches a caller takes, not one per message. A struct is the shape that can gain such a field later without breaking a caller that only reads offset.

bytes.buf

fn buf() -> Buf

An empty buffer.

bytes.put

fn put(b: Buf, byte: Int) -> Buf

Append one byte. A value outside 0..255 is truncated, as a byte store is.

bytes.put_bytes

fn put_bytes(b: Buf, src: Bytes) -> Buf

Append all of src.

bytes.size

fn size(b: Buf) -> Int

How many bytes have been written.

size and not len: a named exception to one-concept-one-name, because len(b: Bytes) above already holds that name and Dawn has no overloading (CONTRIBUTING §naming; history in docs/stdlib-naming.md).

bytes.buf_at

fn buf_at(b: Buf, i: Int) -> Int

The byte at i, as 0..255. Out of range panics, exactly as at(b: Bytes, i) does — it is the same question asked of the half-built buffer.

buf_at and not at for the reason size is not len, and not get because get is criterion 2's word and this asserts (spec §4.8). The second named exception to one-concept-one-name (CONTRIBUTING §naming; history and the rejected alternative in docs/stdlib-naming.md).

bytes.freeze

fn freeze(b: Buf) -> Bytes

The finished bytes.

bytes.utf8

fn utf8(s: String) -> Bytes

The UTF-8 bytes of s.

bytes.decode_utf8_lossy

fn decode_utf8_lossy(b: Bytes) -> String

b decoded as UTF-8, with each malformed sequence replaced by U+FFFD.

Total, and lossy in the literal sense: the caller cannot tell a replacement from a U+FFFD that was really in the input, and the bytes it stood for are gone. That is the right answer for text on its way to a human and the wrong one for input something is about to trust, which is how the overlong c0 af turns into / on any path that decodes before it checks. decode_utf8_checked is the other half.

bytes.decode_utf8_checked

fn decode_utf8_checked(b: Bytes) -> Result[String, Utf8Error]

b decoded as UTF-8, or Err at the first sequence that is not.

The exact complement of decode_utf8_lossy: this answers Ok for precisely the inputs that one returns unchanged, and the Ok string is the one it would have produced. Nothing is rewritten on the way to the caller, so an application that must not act on bytes it was not sent asks this one.

bytes.decode_latin1

fn decode_latin1(b: Bytes) -> String

b decoded as ISO-8859-1: one code point per byte, 0..255.

bytes.len

fn len(b: Bytes) -> Int

Number of bytes in b.

bytes.is_empty

fn is_empty(b: Bytes) -> Bool

Whether b has no bytes. The same question str.is_empty answers, in the same words (CONTRIBUTING §naming).

bytes.at

fn at(b: Bytes, i: Int) -> Int

The byte at i as 0..255. Panics when i is out of range — a programming error, like indexing a list past its end. The -1 the intrinsic returns is that signal, and cannot collide with a real (unsigned) byte value.

bytes.slice

fn slice(b: Bytes, start: Int, end: Int) -> Bytes

The bytes in [start, end). Both ends are clamped into range and start > end yields empty, so this never panics.

bytes.index_of

fn index_of(b: Bytes, needle: Bytes, from: Int) -> Option[Int]

Where the first occurrence of needle at or after from begins, or None.

A position in [0, len] counted in bytes, not the index of an element: an empty needle matches at the start when that position exists, and len(b) is a legal answer though no byte lives there. A negative from is clamped to zero; a from past len(b) is a miss, including for an empty needle. Pure Dawn over bytes_len/bytes_at — the search loop is not an intrinsic.

bytes.to_hex

fn to_hex(b: Bytes) -> String

b as hexadecimal: two digits per byte, most significant first, nothing between them. Lower case, which is the canonical spelling this library produces -- from_hex reads either case, to_hex writes one.

bytes.from_hex

fn from_hex(s: String) -> Option[Bytes]

s read as hexadecimal, or None when it is not an even-length run of hex digits. Either case is accepted; nothing else is, not even whitespace.

Inquiry (spec §4.8, criterion 2): text arriving from a file or a header is checked, not asserted, so a malformed input is a branch and not a panic.

bytes.to_base64

fn to_base64(b: Bytes) -> String

b as Base64 with the standard alphabet (A-Za-z0-9+/, RFC 4648 §4) and = padding to a multiple of four characters.

bytes.from_base64

fn from_base64(s: String) -> Option[Bytes]

s read as standard-alphabet Base64, or None when it is not.

Inquiry (spec §4.8, criterion 2). Padding is optional on the way in even though to_base64 always writes it; the url-safe alphabet is not accepted here -- from_base64_url is the function that reads it.

bytes.to_base64_url

fn to_base64_url(b: Bytes) -> String

b as Base64 with the url- and filename-safe alphabet (A-Za-z0-9-_, RFC 4648 §5) and no padding, which is the shape a JWT segment, a signature parameter and a path component all want -- = has to be percent-encoded in a query and means nothing in a file name.

bytes.from_base64_url

fn from_base64_url(s: String) -> Option[Bytes]

s read as url-safe Base64, with or without padding, or None when it is not. The standard alphabet's + and / are rejected here, as - and _ are by from_base64: which alphabet a string is in is the caller's claim, and a decoder that guesses turns a mismatch into wrong bytes.

std/io

The outside world: console, files, environment and subprocesses. Everything here is !io, and everything that can fail answers Result[T, ForeignError] (spec §9.8.1).

DeleteOutcome

type DeleteOutcome = Deleted | NotFound

The two normal outcomes of deleting a path. Host failures are not outcomes: they stay in delete's Err branch.

io.println

fn println(s: String) -> Unit !io

Print a string followed by a newline.

io.print

fn print(s: String) -> Unit !io

Print a string without a newline.

io.eprintln

fn eprintln(s: String) -> Unit !io

Print a string to stderr, followed by a newline.

io.eprint

fn eprint(s: String) -> Unit !io

Print a string to stderr without a newline.

io.exit

fn exit(code: Int) -> Unit !io

End the process with an exit status. Nothing after the call runs.

The result remains Unit: io_exit is the existing void/Unit intrinsic ABI. Naming return-only Never does not change that public contract.

io.read_line

fn read_line() -> Option[String] !io

Read one line from stdin; None at end of input. The intrinsic maps Java's null-or-line straight onto Option.

io.is_dir

fn is_dir(path: String) -> Bool !io

Whether path names a directory. Invalid host path spellings, including an embedded NUL, answer false rather than escaping this Bool interface.

io.exists

fn exists(path: String) -> Bool !io

Whether anything exists at path -- a file, a directory, anything. Invalid host path spellings, including an embedded NUL, answer false.

io.mkdirs

fn mkdirs(path: String) -> Result[Unit, ForeignError] !io

Create path and every missing directory above it. An existing directory is not a failure; an existing file is.

io.read_file

fn read_file(path: String) -> Result[String, ForeignError] !io

Read a whole file as a UTF-8 string.

io.write_file

fn write_file(path: String, content: String) -> Result[Unit, ForeignError] !io

Write a string to a file, creating missing parent directories. Ok carries nothing: the only count worth returning would be bytes on disk.

io.cwd

fn cwd() -> String !io

The process's working directory, as an absolute path.

io.getenv

fn getenv(name: String) -> Option[String] !io

The value of the environment variable name, or None when it is unset or cannot be represented by the host environment API, including an embedded NUL.

io.read_bytes

fn read_bytes(path: String) -> Result[Bytes, ForeignError] !io

Read a whole file as bytes. The text half is read_file.

io.write_bytes

fn write_bytes(path: String, content: Bytes) -> Result[Unit, ForeignError] !io

Write bytes to a file, creating missing parent directories.

io.delete

fn delete(path: String) -> Result[DeleteOutcome, ForeignError] !io

Delete a file or an empty directory. A missing path is Ok(NotFound); every other host refusal, including a non-empty directory, is an Err.

The empty path and a path ending in / are rejected before a backend sees them. Hosts normalize those spellings differently, and delete must never change which filesystem object the caller named as a side effect of choosing a backend.

io.rename

fn rename(src: String, dst: String) -> Result[Unit, ForeignError] !io

Move src onto dst, replacing it. Atomic when the two are on one filesystem and an error when they are not -- which is the whole point: the caller is publishing something it has already verified, and a move that can be observed half-done would defeat that. Stage with temp_dir under the destination's own directory to stay on one filesystem.

io.temp_dir

fn temp_dir(parent: String, prefix: String) -> Result[String, ForeignError] !io

A fresh directory under parent, named after prefix. An empty parent means the system temporary directory.

io.temp_file

fn temp_file(parent: String, prefix: String) -> Result[String, ForeignError] !io

A fresh, empty file under parent, named after prefix. An empty parent means the system temporary directory.

The host picks the name and creates the file in one step (mkstemp(3), Files.createTempFile), which is the difference that matters: a caller that spelled a name and opened it afterwards has a window in which another process can put something else -- a symbolic link, say -- at that name. On a POSIX host the file arrives readable and writable by its owner alone.

io.copy_permissions

fn copy_permissions(src: String, dst: String) -> Result[Unit, ForeignError] !io

Give dst the permissions src has.

src is inspected without following a link. POSIX hosts copy the mode bits; a host with no POSIX permission model copies the read-only flag, which is the whole of what it has. Neither copies ACLs or extended attributes.

This exists because publishing by rename loses them: the file that ends up at the destination is a new file, whose mode came from the call that created it and the umask, and nothing else remembers what it replaced.

io.atomic_write_file

fn atomic_write_file(path: String, content: String) -> Result[Unit, ForeignError] !io

Write content to path so that a reader ever only sees the old file or the new one, never a half-written one and never nothing at all.

The staging file is created by temp_file in the destination's own directory -- the host picks its name, so nothing can be squatting on it, and the shared directory is what keeps the final rename on one filesystem and therefore atomic. What is written is read back and compared before anything is published, the destination's permissions are carried over when it already exists, and only then does a single rename install it. Any step that fails takes the staging file away and leaves the destination exactly as it was.

What this does not promise:

  • Durability. It orders nothing against a power cut: no fsync of the file and none of the directory. The guarantee is about what a reader can observe on a running system, which is what a manifest rewrite needs; surviving a crash is a different (and much more expensive) property.
  • Anything about concurrent writers. Two of these racing both succeed and the last rename wins. Detecting that someone else changed the file since it was read is a compare-and-set, and that is a different primitive with a different signature.
  • Link identity. A destination with other hard links to it is detached: this directory entry gets the new file and the other links keep the old one. Writing through to the shared inode is the opposite of what is wanted here -- it is exactly the non-atomic overwrite this replaces.

A destination that is a symbolic link is refused rather than followed (io.atomic_write_target_is_symlink). Following would let whatever placed the link choose which file gets written, and a caller publishing a file it named is not asking to write wherever that name currently points.

The four failure kinds minted here are io.invalid_atomic_write_path, io.atomic_write_target_is_symlink, io.atomic_write_staging_failed and io.atomic_write_verify_failed; every other Err carries whatever the backend called the failure, as everywhere else in this module.

io.atomic_write_staging_failed is the one place that rewrites a backend message instead of passing it on, and it earns the exception: the failure is about a randomly named file this function invented, so passing it on would name that file to a caller who never asked for it. The backend's own words are kept in cause.

io.read_stdin

fn read_stdin(n: Int) -> Bytes !io

Exactly n bytes of standard input, shorter only at end of input -- so a short read is how end of input is reported.

This reads the stream directly while read_line reads a buffered reader over it. A program that used both would lose whatever the reader had already buffered; pick one.

io.stdin_ready

fn stdin_ready(timeout_ms: Int) -> Bool !io

Whether at least one byte of standard input can be read right now, waiting up to timeout_ms for one to arrive.

End of input is not readiness. When the writer has gone away this answers false, exactly as a silent-but-open stream does; the difference is reported by read_stdin, which stays the only reader. A loop driven by this alone would therefore spin once its input ended -- the third branch is not optional:

if have_work_pending { if stdin_ready(window) { read a message } else { do the work } } else { read a message # nothing to do, so blocking is the right thing }

timeout_ms is an upper bound, not a lower one. Returning false early is always allowed -- a regular file at end of input answers immediately -- and it is safe because the only thing a caller may do with false is stop waiting. true is the answer that is never given on a guess.

io.list_dir

fn list_dir(path: String) -> Result[List[String], ForeignError] !io

The entry names directly under path, sorted in code-point order. A non-directory is reported as an Err, not a panic, and — matching the old behaviour — is checked before anything is read, so a missing path and a plain file give the same message.

The sort happens here, not in the backends: the io_list_names intrinsic answers whatever order the OS gave, and one list.sort over the language's own Ord[String] is what makes every backend agree.

Its kind is "io.not_a_directory", one of the three failure kinds std mints itself (run's io.no_program and delete's io.invalid_delete_path are the others): every other one in this module is whatever the backend called it, and this failure never reaches a backend, because the check happens first. It is spelled as a name rather than a sentence for the same reason every other kind is — a caller may match on it.

io.run

fn run(argv: List[String], out_path: String, err_path: String) -> Result[Int, ForeignError] !io

Run argv[0] (found on PATH) with argv as its arguments, wait for it, and answer its exit status. A non-zero status is not an error -- it is the answer; an Err means the program could not be started at all.

Each stream goes to the file named by its path, truncating it, and an empty path leaves that stream pointing wherever this process's already points. Files rather than pipes: see the note on the io_run intrinsic.

An empty argv is "io.no_program", another failure kind std mints itself, and for list_dir's reason: the check happens first, so the failure never reaches a backend and every backend words it the same.

std/map

The persistent hash map. Insert and remove answer a new map and leave the old one intact; keys need Eq + Hash (spec §2.2, §11).

impls registered by this module (visible program-wide, no use needed): Eq[Map[K, V]], Hash[Map[K, V]], Show[Map[K, V]], Iter[Map[K, V]]

map.empty

fn empty[K, V]() -> Map[K, V]

A map with no entries. The key/value types come from the use site.

map.from

fn from[K: Eq + Hash, V](entries: List[(K, V)]) -> Map[K, V]

A map from a list of (key, value) pairs; a repeated key keeps the last value.

map.insert

fn insert[K: Eq + Hash, V](m: Map[K, V], key: K, value: V) -> Map[K, V]

A copy with key bound to value; an existing key keeps its insertion position.

map.remove

fn remove[K: Eq + Hash, V](m: Map[K, V], key: K) -> Map[K, V]

A copy without key; absent keys are fine.

map.get

fn get[K: Eq + Hash, V](m: Map[K, V], key: K) -> Option[V]

The value bound to key, or None.

map.has

fn has[K: Eq + Hash, V](m: Map[K, V], key: K) -> Bool

Whether key is bound.

map.len

fn len[K, V](m: Map[K, V]) -> Int

Number of entries.

map.is_empty

fn is_empty[K, V](m: Map[K, V]) -> Bool

Whether there are no entries. Cheaper to read than len(m) == 0 and, since the trie keeps its own count, no cheaper to compute -- the point is that one question has one spelling across every container (CONTRIBUTING §naming).

map.keys

fn keys[K, V](m: Map[K, V]) -> List[K]

The keys, in insertion order.

map.values

fn values[K, V](m: Map[K, V]) -> List[V]

The values, in insertion order of their keys.

map.entries

fn entries[K, V](m: Map[K, V]) -> List[(K, V)]

The (key, value) pairs, in insertion order.

map.fold

fn fold[K, V, A, !e](m: Map[K, V], init: A, f: fn(A, K, V) -> A !e) -> A !e

Every entry combined into one value: f(acc, k, v), starting from init. This walks the trie itself -- no entry list, no pair per entry -- which is why it exists: folding entries(m) first builds and sorts the list this avoids. The price is the order: f sees each entry exactly once but in no promised order (the trie's own, which is not insertion order). A fold whose answer depends on the order it met the entries in belongs on entries(m). The key and value arrive as two arguments rather than a pair, so the walk allocates nothing per entry; the row rides through f the way list.fold's does. Unbounded like the other readers: visiting entries hashes and compares no keys.

std/set

The persistent hash set. Insert and remove answer a new set and leave the old one intact; elements need Eq + Hash (spec §2.2, §11).

impls registered by this module (visible program-wide, no use needed): Eq[Set[T]], Hash[Set[T]], Show[Set[T]], Iter[Set[T]]

set.empty

fn empty[T]() -> Set[T]

A set with no elements. The element type comes from the use site.

set.from

fn from[T: Eq + Hash](xs: List[T]) -> Set[T]

A set from a list; duplicates collapse, first occurrence keeps its position.

set.insert

fn insert[T: Eq + Hash](s: Set[T], x: T) -> Set[T]

A copy with x added; an existing element keeps its insertion position.

set.remove

fn remove[T: Eq + Hash](s: Set[T], x: T) -> Set[T]

A copy without x; absent elements are fine.

set.has

fn has[T: Eq + Hash](s: Set[T], x: T) -> Bool

Whether x is an element.

set.len

fn len[T](s: Set[T]) -> Int

Number of elements.

set.is_empty

fn is_empty[T](s: Set[T]) -> Bool

Whether there are no elements. The same question str.is_empty answers, in the same words (CONTRIBUTING §naming).

set.to_list

fn to_list[T](s: Set[T]) -> List[T]

The elements, in insertion order.