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:
- Names in the prelude are in scope implicitly. No
use— just writeprintln("hi"),sort(xs). - The rest come in with
use std/xand are then called qualified: afteruse std/str, writestr.trim(s). A single name can be imported on its own (use std/str.{trim}). - A function appearing once under "prelude" and once under its module is
normal:
sort(xs)andlist.sort(xs)are two ways of writing the same function. - In a signature,
[T: Ord]is a type parameter and its bound (spec §3.5),!iois an effect (spec §6), and!eis an effect variable — the function's effects are whatever the closure handed to it has. - What can fail returns
Result; what may have no answer returnsOption. When to assert, when to ask and when to clamp is decided in spec §4.8.
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.
| Type | In one line |
|---|---|
Int | 64-bit signed integer |
Float | double precision; rendering and parsing are pinned by std/fmt and do not follow the host (§4.3) |
Bool | true / false |
String | immutable string, measured in code points; there is no ill-formed UTF-8 inside one |
Unit | the 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) |
ForeignError | the 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 |
Bytes | immutable byte string, equal by content; binary data goes through it instead of borrowing a string (§9.5.1) |
Buf | a write cursor onto Bytes: bytes.buf() opens one, bytes.freeze() closes it |
Cursor | a position in a String, not an index; declared opaque by std/cursor |
(A, B, ...) | tuple |
fn(A, B) -> C !e | function 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
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
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)
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
option
conversion
control
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
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.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.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.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.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.
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.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.put
fn put(b: Buf, byte: Int) -> Buf
Append one byte. A value outside 0..255 is truncated, as a byte store is.
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.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.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.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.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.is_symlink
fn is_symlink(path: String) -> Bool !io
Whether path is a symbolic link. The link itself is examined, not its
target, so a link to a directory is a link and not a directory. Invalid host
path spellings, including an embedded NUL, answer false.
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
fsyncof 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
renamewins. 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.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.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.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.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).