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.

  • generated by dawn doc --stdlib
  • 24 modules

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 comes in both languages: the Chinese text is the original and the English text, which these links lead to, is its translation.

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] !e

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] !e

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

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, radix: Int = 10) -> Option[Int]

parse an integer, decimal unless a radix 2 through 36 is given; None on malformed input

parse_float

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

parse a floating point number; 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, at: Loc = caller()) -> 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, at: Loc = caller()) -> Never

abort with a message and the call's site, or the at passed on (diverges, so usable at any type)

todo

fn todo(at: Loc = caller()) -> Never

placeholder that panics when reached

caller

fn caller() -> Loc

the site of each call, as the whole default of a parameter at: Loc = caller(); legal nowhere else

dbg

fn dbg[T: Show](x: T) -> T

debug print: write [path:line:col] <argument text> = <show> to stderr and return the argument unchanged; pure by type, a no-op in a const, and an error in std and in [deps] packages

discard

fn discard[T, U, !e](k: fn(T) -> U !e) -> Unit !e

abandon the computation a continuation would have resumed, running every bracket release inside it; the ticket is spent, so resuming afterwards panics

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](it: T) -> T.Cur
  fn iter_done[T: Iter](it: T, c: T.Cur) -> Bool
  fn iter_next[T: Iter](it: T, c: T.Cur) -> T.Cur
  fn iter_get[T: Iter](it: T, c: 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](it: 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

Add

trait Add[T] {
  fn add[T: Add](a: T, b: T) -> T !T.Add
}

what a + b means past Int and Float: both operands and the result are one type. Int and Float come with an impl; a type that writes one gets +, and the effect its impl binds as Add is what + costs (pure unless bound)

Sub

trait Sub[T] {
  fn sub[T: Sub](a: T, b: T) -> T !T.Sub
}

binary - past Int and Float, shaped like Add

Mul

trait Mul[T] {
  fn mul[T: Mul](a: T, b: T) -> T !T.Mul
}

* past Int and Float, shaped like Add

Div

trait Div[T] {
  fn div[T: Div](a: T, b: T) -> T !T.Div
}

/ past Int and Float, shaped like Add

Rem

trait Rem[T] {
  fn rem[T: Rem](a: T, b: T) -> T !T.Rem
}

% past Int and Float, shaped like Add; the remainder takes the dividend's sign, as on Int

Neg

trait Neg[T] {
  fn neg[T: Neg](a: T) -> T !T.Neg
}

unary - past Int and Float: one operand, shaped like Add otherwise

FromInt

trait FromInt[T] {
  fn from_int[T: FromInt](n: Int) -> T !T.FromInt
}

what an integer literal means at a type of the program's own: let m: Money = 5 is from_int(5). A pure impl is evaluated at compile time, so a panic in it is a compile error at the literal; Int and Float come with an impl

FromFloat

trait FromFloat[T] {
  fn from_float[T: FromFloat](x: Float) -> T !T.FromFloat
}

what a float literal means at a type of the program's own, shaped like FromInt; Float comes with an impl, and an integer literal never goes through it

StagedIter

trait StagedIter[R] {
  type Item
  fn staged_for[R: StagedIter](r: R, body: fn(R.Item) -> Unit !R.StagedIter) -> Unit !R.StagedIter
}

what for x in r means when r describes a loop a library records rather than runs: the body becomes a closure handed to staged_for, once. No type comes with an impl, and a type cannot have both this and Iter

StagedVar

trait StagedVar[T] {
  type Cell
  fn var_open[T: StagedVar](v: T) -> T.Cell !T.StagedVar
  fn var_get[T: StagedVar](c: T.Cell) -> T !T.StagedVar
  fn var_set[T: StagedVar](c: T.Cell, v: T) -> Unit !T.StagedVar
}

how an outer var crosses a staged loop body: opened as a cell before the loop, read and written through it inside, read back after. A var whose type has no impl cannot be mentioned in a staged body

Len

trait Len[T] {
  fn len[T: Len](x: T) -> Int
}

the number of elements a container holds, behind len(c) and c.len(). List comes with an impl; a type that writes one gets len without taking the name from any other module's

Modules

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

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], Len[Buf], Len[Bytes], 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.

Base64

type Base64 = Standard | StandardRaw | Url | UrlRaw

One of RFC 4648's Base64 encodings: an alphabet and whether = pads the text to a multiple of four characters. These are Go's four encoding/base64 values under the same names.

  • Standard: the standard alphabet (A-Za-z0-9+/, §4), padded.
  • StandardRaw: the standard alphabet, unpadded.
  • Url: the url- and filename-safe alphabet (A-Za-z0-9-_, §5), padded.
  • UrlRaw: the url-safe alphabet, unpadded, which is the shape a JWT segment, a signature parameter and a path component want: = has to be percent-encoded in a query and means nothing in a file name.

Endian

type Endian = Little | Big

Which end of a number comes first in memory: Little writes the least significant byte first, Big the most significant.

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.

A whole chunk that starts on a chunk boundary is sliced, unboxed. Anything else goes into the tail, and it goes in as one bytes_unpack_int of as many bytes as the tail has room for, then one array_extend, not a push per byte: a put that starts mid-chunk used to cost a bounds-checked bytes_at and an array_push for each of its bytes, and the many small puts a decompressor or a framing layer makes are all of that kind.

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 because at(b: Bytes, i) already holds that name and Dawn has no overloading, and not get because get is criterion 2's word and this asserts (spec §4.8). The one 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.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, from: Int, to: Int) -> Bytes

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

bytes.index_of

fn index_of(b: Bytes, sub: Bytes, from: Int = 0) -> Option[Int]

Where the first occurrence of sub at or after from (the start unless given) 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, enc: Base64 = Standard) -> String

b as Base64 in the encoding enc.

bytes.from_base64

fn from_base64(s: String, enc: Base64 = Standard) -> Option[Bytes]

s read as Base64 in the encoding enc, or None when it is not.

Inquiry (spec §4.8, criterion 2). Only the spellings to_base64(_, enc) writes are accepted: a padded encoding requires its padding and a raw one refuses it, and neither alphabet is read for the other. Which encoding a string is in is the caller's claim, and a decoder that guesses turns a mismatch into wrong bytes.

bytes.pack_ints

fn pack_ints(xs: List[Int], width: Int, order: Endian = Little) -> Bytes

Each element of xs as width bytes, which must be 1, 2, 4 or 8.

Wrapping, never clamping: an element keeps its low 8 * width bits, so 300 at width 1 is 44 and -1 at width 2 is ff ff. Any other width panics (spec §4.8, criterion 3: a width is the caller's decision about layout, and no byte string means the wrong one).

bytes.unpack_ints

fn unpack_ints(b: Bytes, width: Int, signed: Bool, order: Endian = Little) -> List[Int]

The integers b holds at width (1, 2, 4 or 8), sign-extended when signed and zero-extended when not; at width 8 the two agree.

A length that is not a multiple of width panics. Dropping the tail would hand back a shorter list with no sign that bytes went missing.

bytes.pack_floats

fn pack_floats(xs: List[Float], width: Int, order: Endian = Little) -> Bytes

Each element of xs as an IEEE 754 float of width bytes, 4 or 8.

Width 8 is bit-exact: a NaN keeps its payload and sign and -0.0 stays -0.0. Width 4 rounds to the nearest float, ties to even, overflows to an infinity, and does not promise a NaN's payload. Any other width panics.

bytes.unpack_floats

fn unpack_floats(b: Bytes, width: Int, order: Endian = Little) -> List[Float]

The floats b holds at width (4 or 8). A width-4 float widens exactly. A length that is not a multiple of width panics, as in unpack_ints.

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/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 = start(s)) -> Option[Cursor]

The position of the first occurrence of sub at or after from, or None. from defaults to the start of s, so find(s, sub) searches all of it.

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/dtype

The format a value has on a device: Dtype[T] names the format of T.

impls registered by this module (visible program-wide, no use needed): HasDtype[Float], HasDtype[Int], HasDtype[Bool], DeviceBits[Float], DeviceBits[Int]

Dtype

type Dtype[T]

A device format for values of type T.

HasDtype

trait HasDtype[T] {
  fn dtype_of[T: HasDtype]() -> Dtype[T]
}

A type with a device format. T is only in the return position, so the expected type picks the impl in any other module: let f: Dtype[Float] = dtype_of(). (Inside this module an opaque type is its target, a String, so T is unconstrained and nothing here can infer it; scripts/spike-native/std_dtype.dawn covers the inference.)

DeviceBits

trait DeviceBits[T] {
  fn to_bits[T: DeviceBits](v: T) -> Int
  fn from_bits[T: DeviceBits](bits: Int) -> T
}

The bit pattern a value has as an element of a device buffer. A format with no host value that can travel (I1, I4, F4E2M1FN, F8E5M3FNU, and the unsigned widths, which have no format at all) has no impl, so moving one is a type error and not a refusal at run time.

The contract is one Int per element: the low width bits are the format's pattern, read unsigned, and an 8-byte format fills the whole Int (so Float and Int may answer a negative one). from_bits ignores the bits above width. The width is not a method: a method without a T in it cannot pick its impl, and the width is element_bytes of the format's name. The wire form is these words, little-endian, packed once in std/gpu (docs/gpu-typed-transfer-design.md section 2).

A type's impl is written in its own module, beside its HasDtype; the builtins Float and Int belong to no module, so theirs are here.

dtype.F64

const F64: Dtype[Float]

The format of Float, 64-bit IEEE.

dtype.I64

const I64: Dtype[Int]

The format of Int, 64-bit two's complement.

dtype.I1

const I1: Dtype[Bool]

The format of Bool in a tile: a one-bit mask.

dtype.dtype_name

fn dtype_name[T](d: Dtype[T]) -> String

The name the format goes by in a tile dialect, such as f64 or bf16.

std/float

The two Float values that no literal spells: positive infinity and NaN.

float.INFINITY

const INFINITY: Float

Positive infinity. Greater than every finite Float; -INFINITY is less.

float.NAN

const NAN: Float

Not a number. Unequal to every Float, itself included.

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(x: Float) -> String

to_string(Float): the shortest decimal string that reads back as exactly x, 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.

fmt.parse_int

fn parse_int(s: String, radix: Int = 10) -> Option[Int]

An optional sign, then digits in radix and nothing else (spec §11), after trimming by Dawn's own space table. Digits are 0-9a-zA-Z cut off at the radix, case-insensitive. None for malformed text, for a value outside Int, and for a radix outside 2..36.

std/gpu

The host side of a GPU: a Gpu effect whose operations a handler answers, typed device buffers over it, a fake device that answers every operation from host memory so a !Gpu program runs on a machine with no GPU at all, and the real device, with_gpu_real, which answers the same operations from the CUDA driver through the native runtime. The device side (kernels, Tile IR) is not here; see docs/tile-backend-design.md.

impls registered by this module (visible program-wide, no use needed): Show[LaunchArg], HasDtype[I4], Len[DeviceBuffer[D]], ScalarDtype[Float], ScalarDtype[Int], ScalarDtype[I32], ScalarDtype[F32]

I4

type I4

The 4-bit signed integer format: two elements to the byte, the low nibble first (Types.td spells the order out for a tensor view of it, and pack / unpack lay a tile down the same way). A two's-complement value in [-8, 7]. It is a tile format and never a buffer format (element_bytes refuses it), so there is no host value of this type: the type and its witness exist to name the format.

DeviceBuffer

type DeviceBuffer[D]

A buffer on the device: the handle the handler issued and its element count. D is a phantom (spec §2.7): DeviceBuffer[Float] and DeviceBuffer[F32] are two types over one representation, so a kernel argument of the wrong format is a type error and not a launch failure.

LaunchArg

type LaunchArg = Buffer(handle: Int) | Word(dtype: String, bits: Int)

One argument of a launch as the device takes it: a buffer by the handle the device issued, or a scalar by value. A Word is the scalar's dtype name and its bit pattern, in the low 32 bits of bits for i32 and f32 and all 64 for i64 and f64, which is the form the argument area of cuLaunchKernel takes (every parameter there is one 8-byte word, and the driver reads the width the kernel declared).

Launch

type Launch[A]

One argument of a typed launch, its format A in the type: a buffer made by buffer_arg or a scalar made by scalar_arg. What the type cannot say, which of the two the kernel wants at a position, is checked above the effect by launch_entry1 and its siblings (gpu.bad_entry).

EntryArg

type EntryArg

One argument of an entry: what the kernel does to a buffer and, when it cuts the buffer into cells, how, or that the argument is a scalar passed by value. Made by arg_in, arg_in_whole, arg_out, arg_shared and arg_scalar.

Entry1

type Entry1[A]

A kernel of one buffer argument of format A, as its recorder answers it: the kernel's name and what it does to the buffer. launch_entry1 runs it. Opaque, so a program holds one only as a recorder made it.

Entry2

type Entry2[A, B]

Entry1 for two arguments, of formats A and B in order.

Entry3

type Entry3[A, B, C]

Entry1 for three arguments.

Entry4

type Entry4[A, B, C, D]

Entry1 for four arguments.

Entry5

type Entry5[A, B, C, D, E]

Entry1 for five arguments.

RefFn

type RefFn

A host reference for a kernel that writes one buffer: given each argument's dtype name and every argument buffer's contents, what the LAST buffer holds afterwards. last_out makes one a WideRefFn.

WideRefFn

type WideRefFn

A host reference as the fake device runs it: the same two arguments, the launch's scalars as Floats in the order the launch passed them (an i32 or i64 value as the integer it is, an f32 as the binary32 value its bits spell), and the argument POSITIONS among the BUFFERS the kernel wrote paired with what each holds afterwards.

ScalarRefFn

type ScalarRefFn

A host reference for a kernel that takes scalars and writes one buffer: RefFn with the launch's scalars as a third argument. last_out_with makes one a WideRefFn.

ScalarDtype

trait ScalarDtype[A] {
  fn scalar_dtype[A: ScalarDtype]() -> Dtype[A]
  fn scalar_word[A: ScalarDtype](v: A) -> LaunchArg
}

The formats a kernel parameter may be passed in BY VALUE: I32, I64 (Int), F32 and F64 (Float), the four whose argument-area layout is a plain 4 or 8 bytes. scalar_dtype is the format's witness, which packages/tileir reads to name the format of a scalar parameter, and scalar_word is the value as the launch passes it. A trait of its own and not HasDtype because Dawn has no supertraits, so a subset of the formats HasDtype covers can only be named by a bound of its own. An opaque type inherits only Eq, Hash and Ord from its target, so scalar_arg of an F16, a BF16 or an I8 does not check.

gpu.I4

const I4: Dtype[I4]

The 4-bit signed integer device format, the witness of I4.

Gpu

effect Gpu {
  fn gpu_alloc(dtype: String, len: Int) -> Result[Int, ForeignError] !Gpu
  fn gpu_copy_from_host(handle: Int, data: Bytes) -> Result[Unit, ForeignError] !Gpu
  fn gpu_copy_to_host(handle: Int) -> Result[Bytes, ForeignError] !Gpu
  fn gpu_launch(kernel: String, gx: Int, gy: Int, gz: Int, args: List[LaunchArg]) -> Result[Unit, ForeignError] !Gpu
  fn gpu_module_global(kernel: String, name: String, dtype: String) -> Result[(Int, Int), ForeignError] !Gpu
  fn gpu_free(handle: Int) -> Result[Unit, ForeignError] !Gpu
  fn gpu_sync() -> Result[Unit, ForeignError] !Gpu
}

The device, as operations a handler answers. Names carry a gpu_ prefix because use std/gpu.{Gpu} brings every operation into the importing module's own namespace (spec §6.5).

Every operation answers Result, with an Err for what the device refused. gpu_alloc answers a fresh handle for len elements of the named format; gpu_copy_from_host and gpu_copy_to_host move a whole buffer; gpu_launch runs the kernel of that name over a gx by gy by gz grid of tile blocks with the given arguments, buffers and scalars, in order; gpu_sync waits for every launch so far. gpu_module_global looks one MODULE-LEVEL SYMBOL up in the module that defines kernel and answers a handle over its storage together with the number of elements of dtype it holds; a symbol the module does not export answers the device's own refusal. Handles are opaque integers a program never invents.

gpu.element_bytes

fn element_bytes(dtype: String) -> Option[Int]

The bytes one element of the named format occupies in a device buffer, or None for a format this release's handlers do not move: f64 and i64 (8), i32 and tf32 (4), bf16, f16 and i16 (2), i8, u8, f8E4M3FN, f8E5M2 and f8E8M0FNU (1). f32 is not among them, nor are the sub-byte tile formats i4 and f4E2M1FN. Both handlers consult this and nothing else to accept a gpu_alloc, so they refuse the same formats with gpu.unsupported_dtype.

gpu.element_bits

fn element_bits(dtype: String) -> Option[Int]

The BITS one element of the named format occupies, or None for a name this release does not know. Every format element_bytes accepts answers eight times what it answers; the two that answer here and not there are the sub-byte ones, i4 and f4E2M1FN, which are tile formats and never buffer formats.

The two functions are therefore not one function twice. element_bytes says what gpu_alloc will hand out and element_bits says how wide a lane is, and pack / unpack are the operations that need the second one: they scale a rank-1 tile's lane count by the ratio of two widths, and half a byte is a ratio the first function cannot express.

gpu.round_to

fn round_to(dtype: String, x: Float) -> Float

x as a buffer or tile of the named format holds it. The float formats round through std/narrow's function for that format: bf16, f16, f32, tf32, f8E4M3FN, f8E5M2, f8E8M0FNU and f4E2M1FN (the fp8 and fp4 ones saturate, and f8E8M0FNU rounds toward zero to a power of two, as std/narrow documents). The integer formats truncate toward zero and wrap into their range: i32, i16 and i8 as two's complement, u8 into [0, 255]; i64 truncates and does not wrap. Any other name, f64 among them, answers x unchanged. What a store into such a buffer does to a value, and what a reference implementation does after every operation whose result the device would keep at that width.

The i32 arm is the whole of what "a 32-bit buffer holds" means here: to_int truncates toward zero (spec: to_int(-2.7) == -2, which is the dialect's own nearest_int_to_zero), and wrap_i32 is the two's complement wrap the device's addi and muli perform. A reference that truncated without wrapping would disagree with the device on every sum that leaves the range, which is the case a corpus above 2^31 exists for.

gpu.wrap_i32

fn wrap_i32(n: Int) -> Int

n as a two's-complement 32-bit signed integer: the low 32 bits, read as a signed value. What every integer operation on the device answers, and what a host reference must apply after each of its own so that the two agree where the arithmetic overflows.

gpu.wrap_i16

fn wrap_i16(n: Int) -> Int

n as a two's-complement 16-bit signed integer: the low sixteen bits, read as a signed value, in [-32768, 32767]. wrap_i32's shape two octets wide, and what the device's addi and muli over an i16 tile do.

gpu.wrap_i8

fn wrap_i8(n: Int) -> Int

n as a two's-complement 8-bit SIGNED integer: the low eight bits read as a signed value, in [-128, 127]. wrap_i32's shape one octet wide.

gpu.wrap_u8

fn wrap_u8(n: Int) -> Int

n as an 8-bit UNSIGNED integer: the low eight bits, in [0, 255]. The same octet wrap_i8 answers, read the other way: wrap_u8(-56) is 200 and wrap_i8(200) is -56.

gpu.wrap_i4

fn wrap_i4(n: Int) -> Int

n as a two's-complement 4-bit SIGNED integer: the low nibble read as a value in [-8, 7]. wrap_i32's shape at half an octet, and what the device's trunc_i4 leaves behind.

gpu.nibble_at

fn nibble_at(w: Int, k: Int) -> Int

The kth 4-bit lane of the 32-bit word w, counted from the low end: k in [0, 8), the answer in [0, 15].

gpu.nibble_into

fn nibble_into(w: Int, k: Int, v: Int) -> Int

w with its kth 4-bit lane set to the low nibble of v; the lane is assumed clear, so this is the inverse of nibble_at when a word is built up from zero.

gpu.nibble_word

fn nibble_word(f: fn(Int) -> Int) -> Float

The 32-bit word whose eight lanes are f(0) to f(7), as a Float an i32 buffer holds.

gpu.handle_of

fn handle_of[D](b: DeviceBuffer[D]) -> Int

The handle a handler issued for b, as Buffer wants it.

gpu.buffer_arg

fn buffer_arg[A](b: DeviceBuffer[A]) -> Launch[A]

t as an argument of a launch: the handle, and the element count a typed launch holds the entry's cells to.

gpu.scalar_arg

fn scalar_arg[A: ScalarDtype](v: A) -> Launch[A]

The value v as an argument of a launch, passed by value. A is its host value type, which is the kernel parameter's format: an F32 is an f32 parameter, a Float an f64 one, an Int an i64 one.

gpu.erase

fn erase[A](l: Launch[A]) -> LaunchArg

l without its format, which is the form launch takes: buffers and scalars of any formats in one list.

gpu.alloc

fn alloc[D](d: Dtype[D], len: Int) -> Result[DeviceBuffer[D], ForeignError] !Gpu

A buffer of len elements of format d. Err(kind: "gpu.bad_length") for a len below one, before any handler is asked.

gpu.copy_from_host

fn copy_from_host[T: HasDtype + DeviceBits](b: DeviceBuffer[T], data: List[T]) -> Result[Unit, ForeignError] !Gpu

Copy data into b: each value is encoded as the pattern of the buffer's format (DeviceBits), little-endian, so a value of the element type reaches the device as it is (nothing is rounded here: a BF16 is already a bfloat16, and an Int crosses to an i64 buffer whole). Only a T with both a format and a pattern can be moved; Bool, I4 and F4E2M1FN have a format and no buffer, so moving one is a type error. Err(kind: "gpu.length_mismatch") when data does not have exactly len(t) elements, before any handler is asked.

gpu.copy_to_host

fn copy_to_host[T: HasDtype + DeviceBits](b: DeviceBuffer[T]) -> Result[List[T], ForeignError] !Gpu

The contents of b, len(b) elements, decoded from the buffer's format (DeviceBits). Exact for every value the format holds, an i64 past 2^53 included.

gpu.free

fn free[D](b: DeviceBuffer[D]) -> Result[Unit, ForeignError] !Gpu

Release b. Its handle answers nothing afterwards.

gpu.launch

fn launch(kernel: String, grid: Int, args: List[LaunchArg], gy: Int = 1, gz: Int = 1) -> Result[Unit, ForeignError] !Gpu

Run the kernel named kernel over a grid by gy by gz grid of tile blocks with args as its arguments, in order: erase(buffer_arg(t)) for a tensor, erase(scalar_arg(v)) for a value passed by value. A kernel reads the three axes with block_id(0) / block_id(1) / block_id(2); a one-dimensional launch leaves gy and gz at 1, so a kernel that only reads block_id(0) sees the grid it always did. Err(kind: "gpu.bad_grid") for an axis below one, or above what a launch can have (2147483647 blocks on axis 0, 65535 on axes 1 and 2, CUDA's limits), before any handler is asked, so every handler refuses the same grid with the same words; a name no handler knows is the handler's refusal (gpu.no_kernel from the fake device).

gpu.arg_in

fn arg_in(extent: List[Int], tile: List[Int], along: List[Int]) -> EntryArg

An argument the kernel only reads, cut into cells of tile over a tensor of extent (row major). along[j] is the grid axis dimension j of the cells follows, or -1 for a dimension the kernel indexes some other way; a dimension that follows an axis has as many cells as the grid has blocks there. An extent of -1 is dynamic: grid[along[j]] * tile[j], which is why a dynamic dimension must follow an axis.

gpu.arg_in_whole

fn arg_in_whole() -> EntryArg

An argument the kernel only reads, and reads as a whole: no cells, so nothing about it is held to the grid.

gpu.arg_out

fn arg_out(extent: List[Int], tile: List[Int]) -> EntryArg

An argument each tile block writes one cell of: cells of tile over a tensor of extent, dimension k along grid axis k. The cells of the Out arguments ARE the grid; an extent of -1 makes that axis dynamic.

gpu.arg_shared

fn arg_shared() -> EntryArg

An argument the kernel reads and writes in ways no cell describes (atomics, scatters, a stencil's neighbours): nothing about it is held to the grid, and no other argument may be its buffer.

gpu.arg_scalar

fn arg_scalar() -> EntryArg

An argument that is a scalar the launch passes by value: no buffer, no cells, nothing about it held to the grid. The launch must pass a scalar_arg there, and a buffer_arg anywhere else.

gpu.entry_grid

fn entry_grid(args: List[EntryArg]) -> Result[List[Int], String]

The grid an entry's arguments make, checked: each argument well formed, the Out arguments agreeing on one grid, and every In dimension that follows a grid axis cut into as many cells as the grid has blocks there, wherever both are known before the launch. Three axes, -1 on an axis an Out extent leaves to the launch; [] when no argument is an Out and the grid is the caller's. Err says what is wrong, naming the argument. packages/tileir calls this when it records a kernel, and every launch_entry calls it again.

gpu.entry1

fn entry1[A](kernel: String, a: EntryArg) -> Entry1[A]

The entry of kernel, whose one argument is a. The format is the type's: a recorder fixes it from the kernel's own parameter.

gpu.entry2

fn entry2[A, B](kernel: String, a: EntryArg, b: EntryArg) -> Entry2[A, B]

entry1 for two arguments.

gpu.entry3

fn entry3[A, B, C](kernel: String, a: EntryArg, b: EntryArg, c: EntryArg) -> Entry3[A, B, C]

entry1 for three arguments.

gpu.entry4

fn entry4[A, B, C, D](kernel: String, a: EntryArg, b: EntryArg, c: EntryArg, d: EntryArg) -> Entry4[A, B, C, D]

entry1 for four arguments.

gpu.entry5

fn entry5[A, B, C, D, E](kernel: String, a: EntryArg, b: EntryArg, c: EntryArg, d: EntryArg, x: EntryArg) -> Entry5[A, B, C, D, E]

entry1 for five arguments.

gpu.launch_entry1

fn launch_entry1[A](e: Entry1[A], a: Launch[A], grid: List[Int] = []) -> Result[Unit, ForeignError] !Gpu

Run e with a as its argument, a buffer_arg or a scalar_arg as the entry says. The grid is the one the Out cells make; grid names the axes the entry leaves to the launch (a dynamic Out extent), or all of them when no argument is an Out, and an axis it names that the entry knows must agree with it. Before any handler is asked: Err(kind: "gpu.bad_entry") for an entry whose cells are malformed or whose argument kinds disagree with the launch (a scalar_arg where the kernel reads a buffer, a buffer_arg where it takes a value), gpu.aliased_argument when an Out or Shared argument is the buffer of another argument, gpu.grid_mismatch when grid or an In argument's cells disagree with the Out cells, gpu.short_tensor when a tensor is shorter than its cells reach, and gpu.bad_grid as launch has it.

gpu.launch_entry2

fn launch_entry2[A, B](e: Entry2[A, B], a: Launch[A], b: Launch[B], grid: List[Int] = []) -> Result[Unit, ForeignError] !Gpu

launch_entry1 for two arguments.

gpu.launch_entry3

fn launch_entry3[A, B, C](e: Entry3[A, B, C], a: Launch[A], b: Launch[B], c: Launch[C], grid: List[Int] = []) -> Result[Unit, ForeignError] !Gpu

launch_entry1 for three arguments.

gpu.launch_entry4

fn launch_entry4[A, B, C, D](e: Entry4[A, B, C, D], a: Launch[A], b: Launch[B], c: Launch[C], d: Launch[D], grid: List[Int] = []) -> Result[Unit, ForeignError] !Gpu

launch_entry1 for four arguments.

gpu.launch_entry5

fn launch_entry5[A, B, C, D, E](e: Entry5[A, B, C, D, E], a: Launch[A], b: Launch[B], c: Launch[C], d: Launch[D], x: Launch[E], grid: List[Int] = []) -> Result[Unit, ForeignError] !Gpu

launch_entry1 for five arguments.

gpu.module_global

fn module_global[D](d: Dtype[D], kernel: String, name: String) -> Result[DeviceBuffer[D], ForeignError] !Gpu

The module-level global named name, in the module that defines kernel, as a tensor of format d over the global's own storage.

A global a kernel declared private is not part of its module's interface, and a device answers this with its own refusal rather than a handle (cuda.CUDA_ERROR_NOT_FOUND from the CUDA driver, whose cuModuleGetGlobal is the operation underneath; gpu.no_module_symbol from the fake device). That is the whole observable difference between the two symbol visibilities, and it is the reason this operation exists.

The tensor SHARES the module's storage: copy_from_host writes what the next launch reads and copy_to_host reads what the last one wrote. Two lookups of one symbol answer the SAME handle, so a program cannot hold two aliases of one global and see them disagree, and free on that handle is ACCEPTED AND DOES NOTHING: the memory belongs to the module and the handle to the installation, so it is still the handle the next lookup answers.

Err(kind: "gpu.bad_global_size") when the global's byte size is not a whole number of elements of d, and Err(kind: "gpu.bad_global_dtype") when the same symbol was already looked up as another format: the handle carries the format it was issued with, and reinterpreting one global's storage two ways is not something either handler offers.

gpu.sync

fn sync() -> Result[Unit, ForeignError] !Gpu

Wait until every launch so far has finished. A launch returning is not the kernel having run; a download after sync sees the kernel's writes.

gpu.last_out

fn last_out(f: fn(List[String], List[List[Float]]) -> List[Float]) -> fn(List[String], List[List[Float]], List[Float]) -> List[(Int, List[Float])]

f, which answers the contents of the LAST argument buffer, as a reference the fake device can run: it writes back to len(ins) - 1 and nowhere else. The kernel takes no scalars; a launch that passes some is the reference's to refuse, and this one ignores them.

gpu.last_out_with

fn last_out_with(f: fn(List[String], List[List[Float]], List[Float]) -> List[Float]) -> fn(List[String], List[List[Float]], List[Float]) -> List[(Int, List[Float])]

last_out for a reference that reads the launch's scalars.

gpu.reference_kernels

fn reference_kernels() -> Map[String, (Int, fn(List[String], List[List[Float]], List[Float]) -> List[(Int, List[Float])])]

The reference implementations the fake device answers launch with, by kernel name. Each entry carries the exact number of buffers the device kernel takes beside its body: three for vadd / vadd_bf16 (two inputs and one output), two for sum (one input and one output). Every other kernel's reference is in packages/tileref, and most of them need the length or the constants their kernel was recorded for, so a caller adds them to this map with those fixed.

gpu.with_gpu_fake

fn with_gpu_fake[T](kernels: Map[String, (Int, fn(List[String], List[List[Float]], List[Float]) -> List[(Int, List[Float])])], body: fn() -> T !Gpu, globals: Map[String, List[(String, String, List[Float])]] = map.empty()) -> T

Run body against a device that is a table in host memory. Handles count up from 1 per installation; a buffer is its format and the Bytes it holds (exactly what copy_from_host wrote, so an upload followed by a download is the identity on every format and every value, an i64 past 2^53 included), and the formats are element_bytes's (gpu.unsupported_dtype otherwise); a handle that was never issued or was freed answers gpu.no_such_buffer. gpu_launch looks kernel up in kernels, hands the reference implementation the format and the contents of every argument buffer in order, decoded to List[Float] (decode_floats), and stores each (position, contents) pair it returns (WideRefFn), encoded back (encode_floats), as the contents of the argument at that position. An i64 buffer holding an element a Float cannot hold exactly (past +/-2^53) is refused at the launch with gpu.fake_inexact, naming the buffer and the element: a reference sees Floats, and a silently rounded input would look like a correct one. A position outside the arguments answers gpu.bad_write_back and nothing is written. Every input is read before any write-back, so an in-place kernel sees what its loads would. A RefFn that answers only the last argument is wrapped with last_out. Before the body is called, its entry's arity must equal the number of buffers (gpu.bad_arity otherwise); the grid is not consulted, a reference implementation computes the whole buffer at once; an unknown name answers gpu.no_kernel. A buffer holds what memory of its format would: what gpu_copy_from_host stores is the bytes as given, and what a launch writes is rounded to the buffer's format (round_to, through encode_floats), which for f64 is the data as given. The length check is copy_from_host's, above the operation, and is the same code under every handler.

gpu_module_global answers from globals, and a name that is not in that kernel's list is gpu.no_module_symbol: a fake device's list holds the globals a module EXPORTS, which is the same set a driver's symbol lookup can see, so a private global is left out of it exactly as it is left out of a cubin's exported symbols. The handle it answers is an ordinary buffer of this store holding the global's declared contents, so copy_from_host and copy_to_host reach it the way they reach any other; the element FORMAT it is asked for must be the one the declaration used (gpu.bad_global_dtype otherwise), because the handle carries the format it was issued with and neither handler reinterprets one format's storage as another's (the bytes could be, and this is a place where the model is narrower than the thing modelled).

globals maps a kernel name to the STATIC GLOBALS its module exports, each a symbol name, an element format and the contents its declaration gives them, in declaration order. A kernel with no entry exports none, which is every kernel when globals is left out.

Pure. Two installations share nothing.

gpu.encode_floats

fn encode_floats(dtype: String, xs: List[Float]) -> Bytes

xs as the bytes a buffer of the named format holds: each Float rounded or wrapped into the format (the rule round_to states) and written as that format's pattern, little-endian, element_bytes bytes an element. Every format element_bytes accepts has an arm, f64 and i64 included, and any other name panics.

This is the Float view by NAME, for callers whose buffers have no common element type: a driver that allocates a list of (format name, contents) buffers, and the fake device at the line where it hands a reference implementation Floats. A program that knows its element type uses copy_from_host and copy_to_host, which are exact for an Int past 2^53 where this cannot be (decode_floats of an i64 rounds there).

The pairing with decode_floats is what makes a format's round trip a property of one line rather than of the handler.

gpu.decode_floats

fn decode_floats(dtype: String, b: Bytes) -> List[Float]

The values a buffer of the named format holds, from its bytes: the inverse of encode_floats on every value the format can hold. An i64 beyond +/-2^53 comes back rounded to the nearest Float.

gpu.with_gpu_real

fn with_gpu_real[T, !e](kernels: Map[String, Bytes], body: fn() -> T !(Gpu|e)) -> T !io

Run body against the machine's GPU through the CUDA driver. kernels maps a kernel name to the cubin that defines an entry of that name (the tileiras output for one packages/tileir program), the way with_gpu_fake's table maps a name to a reference implementation. Assemble those cubins with the arguments packages/tileir's asm.tileiras_args answers: tileiras 13.4.92 needs --opt-level 0 on sm_90 and sm_100 or it miscompiles a loop exit value (#558).

Handles are the same currency as the fake device's: they count up from 1 per installation, a buffer is (device pointer, element count, format) in a table the handler owns, and a handle never issued or already freed answers gpu.no_such_buffer before the device is asked. gpu_alloc accepts the formats element_bytes knows, and answers gpu.unsupported_dtype otherwise, as the fake device does. EVERY format crosses to the runtime as Bytes (little-endian, f64 and i64 included), laid out by copy_from_host and decoded by copy_to_host, so the runtime copies bytes and knows nothing about the format. The handler refuses a byte count that is not the buffer's (gpu.length_mismatch) before the driver is asked, because a copy that long would write past the allocation. gpu_launch looks the name up first (gpu.no_kernel), then the arguments (gpu.no_arguments, gpu.no_such_buffer), and only then touches the device: the cubin is handed to the driver the first time its kernel is launched and the module handle is kept for the rest of the installation, so a program that never launches never loads a module. The three grid axes count tile blocks; the intrinsic launches with block dims (1, 1, 1) and no shared memory, which is how a Tile IR kernel is launched.

gpu_module_global loads that kernel's module the same way (so a lookup on a program that never launches loads it, and a launch afterwards reuses it) and asks the driver for the symbol: cuModuleGetGlobal answers a device address and a byte size, and the handle registered over them is a borrowed one. gpu_free on it does nothing and leaves both the handle and the storage alone, since the memory belongs to the module and goes when the context does; every other operation treats it as any other buffer. A symbol the module does not export is the driver's own cuda.CUDA_ERROR_NOT_FOUND, which is what a private global answers.

The seam is below the error surface: everything the driver refuses arrives as the ForeignError the runtime built from the CUresult, with kind cuda.<CUresult name> (gpu.no_driver when there is no libcuda to load, gpu.unsupported_backend on the JVM, which has no device runtime), unchanged. The first operation that needs the device opens it; when body has returned the context and the library are released, which also frees any buffer the program did not.

Reference kernels: 2 host-side answers the GPU backend's tests compare against

gpu.vadd_ref

fn vadd_ref(dtypes: List[String], ins: List[List[Float]]) -> List[Float]

Element-wise addition of the first two arguments, each sum rounded to the format of the last (the output): the host-side reference for the vadd kernels of every format, and the first entry of reference_kernels. dtypes is the format of each argument buffer in order, as the fake device passes it; the device's addf rounds its result to the tile's format, and so does this.

gpu.sum_ref

fn sum_ref(dtypes: List[String], ins: List[List[Float]]) -> List[Float]

Block-wise tile sum of the first argument into the second: the host-side reference for the sum kernel. out has 128 * blocks elements and x has chunks times as many; block b folds the chunks consecutive 128-wide tiles of x numbered b * chunks onward into one tile of out, lane by lane: out[b * 128 + j] is the first of those tiles' lane j, plus the second's, plus the third's, and so on in tile order. That is the order the kernel adds in (its loop carries the first tile in and adds one tile per iteration), so the two agree bit for bit under IEEE double addition; a fold from zero would differ on -0.0, a pairwise or reversed fold on carries. chunks is len(x) / len(out); a shape the kernel would read out of bounds on (len(out) not a multiple of 128, len(x) not a multiple of len(out)) is the caller's bug and reads out of range here as it would there. Every addition is rounded to the output's format (dtypes[1]), the way the kernel's accumulator tile is at that width.

std/int/i16

Signed 16-bit integers: I16, an opaque type over Int holding -32768..32767, with arithmetic that wraps the way Int's does.

impls registered by this module (visible program-wide, no use needed): HasDtype[I16], DeviceBits[I16], FromInt[I16], Show[I16], Add[I16], Sub[I16], Mul[I16], Div[I16], Rem[I16], Neg[I16]

I16

type I16

A signed 16-bit integer.

i16.MIN

const MIN: I16

The least I16, -32768.

i16.MAX

const MAX: I16

The greatest I16, 32767.

i16.I16

const I16: Dtype[I16]

The device format of I16: i16.

i16.of

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

n as a I16, or None when it is outside -32768..32767.

i16.wrap

fn wrap(n: Int) -> I16

The I16 with the low 16 bits of n, read as two's complement: wrap(32768) is -32768.

i16.to_int

fn to_int(x: I16) -> Int

The value of x as an Int.

i16.checked_add

fn checked_add(a: I16, b: I16) -> Option[I16]

a + b, or None where it wraps.

i16.checked_sub

fn checked_sub(a: I16, b: I16) -> Option[I16]

a - b, or None where it wraps.

i16.checked_mul

fn checked_mul(a: I16, b: I16) -> Option[I16]

a * b, or None where it wraps.

i16.and

fn and(a: I16, b: I16) -> I16

The bitwise and.

i16.or

fn or(a: I16, b: I16) -> I16

The bitwise or.

i16.xor

fn xor(a: I16, b: I16) -> I16

The bitwise exclusive or.

i16.complement

fn complement(a: I16) -> I16

Every bit flipped.

i16.shl

fn shl(a: I16, n: Int) -> I16

a shifted left by n modulo 16 places, zeros in at the bottom.

i16.shr

fn shr(a: I16, n: Int) -> I16

a shifted right by n modulo 16 places, copies of the sign bit in at the top (arithmetic, as Int's >>).

i16.rotl

fn rotl(a: I16, n: Int) -> I16

a's 16 bits rotated left by n modulo 16 places.

i16.rotr

fn rotr(a: I16, n: Int) -> I16

a's 16 bits rotated right by n modulo 16 places.

std/int/i32

Signed 32-bit integers: I32, an opaque type over Int holding -2147483648..2147483647, with arithmetic that wraps the way Int's does.

impls registered by this module (visible program-wide, no use needed): HasDtype[I32], DeviceBits[I32], FromInt[I32], Show[I32], Add[I32], Sub[I32], Mul[I32], Div[I32], Rem[I32], Neg[I32]

I32

type I32

A signed 32-bit integer.

i32.MIN

const MIN: I32

The least I32, -2147483648.

i32.MAX

const MAX: I32

The greatest I32, 2147483647.

i32.I32

const I32: Dtype[I32]

The device format of I32: i32.

i32.of

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

n as a I32, or None when it is outside -2147483648..2147483647.

i32.wrap

fn wrap(n: Int) -> I32

The I32 with the low 32 bits of n, read as two's complement: wrap(2147483648) is -2147483648.

i32.to_int

fn to_int(x: I32) -> Int

The value of x as an Int.

i32.checked_add

fn checked_add(a: I32, b: I32) -> Option[I32]

a + b, or None where it wraps.

i32.checked_sub

fn checked_sub(a: I32, b: I32) -> Option[I32]

a - b, or None where it wraps.

i32.checked_mul

fn checked_mul(a: I32, b: I32) -> Option[I32]

a * b, or None where it wraps.

i32.and

fn and(a: I32, b: I32) -> I32

The bitwise and.

i32.or

fn or(a: I32, b: I32) -> I32

The bitwise or.

i32.xor

fn xor(a: I32, b: I32) -> I32

The bitwise exclusive or.

i32.complement

fn complement(a: I32) -> I32

Every bit flipped.

i32.shl

fn shl(a: I32, n: Int) -> I32

a shifted left by n modulo 32 places, zeros in at the bottom.

i32.shr

fn shr(a: I32, n: Int) -> I32

a shifted right by n modulo 32 places, copies of the sign bit in at the top (arithmetic, as Int's >>).

i32.rotl

fn rotl(a: I32, n: Int) -> I32

a's 32 bits rotated left by n modulo 32 places.

i32.rotr

fn rotr(a: I32, n: Int) -> I32

a's 32 bits rotated right by n modulo 32 places.

std/int/i8

Signed 8-bit integers: I8, an opaque type over Int holding -128..127, with arithmetic that wraps the way Int's does.

impls registered by this module (visible program-wide, no use needed): HasDtype[I8], DeviceBits[I8], FromInt[I8], Show[I8], Add[I8], Sub[I8], Mul[I8], Div[I8], Rem[I8], Neg[I8]

I8

type I8

A signed 8-bit integer.

i8.MIN

const MIN: I8

The least I8, -128.

i8.MAX

const MAX: I8

The greatest I8, 127.

i8.I8

const I8: Dtype[I8]

The device format of I8: i8.

i8.of

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

n as a I8, or None when it is outside -128..127.

i8.wrap

fn wrap(n: Int) -> I8

The I8 with the low 8 bits of n, read as two's complement: wrap(128) is -128.

i8.to_int

fn to_int(x: I8) -> Int

The value of x as an Int.

i8.checked_add

fn checked_add(a: I8, b: I8) -> Option[I8]

a + b, or None where it wraps.

i8.checked_sub

fn checked_sub(a: I8, b: I8) -> Option[I8]

a - b, or None where it wraps.

i8.checked_mul

fn checked_mul(a: I8, b: I8) -> Option[I8]

a * b, or None where it wraps.

i8.and

fn and(a: I8, b: I8) -> I8

The bitwise and.

i8.or

fn or(a: I8, b: I8) -> I8

The bitwise or.

i8.xor

fn xor(a: I8, b: I8) -> I8

The bitwise exclusive or.

i8.complement

fn complement(a: I8) -> I8

Every bit flipped.

i8.shl

fn shl(a: I8, n: Int) -> I8

a shifted left by n modulo 8 places, zeros in at the bottom.

i8.shr

fn shr(a: I8, n: Int) -> I8

a shifted right by n modulo 8 places, copies of the sign bit in at the top (arithmetic, as Int's >>).

i8.rotl

fn rotl(a: I8, n: Int) -> I8

a's 8 bits rotated left by n modulo 8 places.

i8.rotr

fn rotr(a: I8, n: Int) -> I8

a's 8 bits rotated right by n modulo 8 places.

std/int/u16

Unsigned 16-bit integers: U16, an opaque type over Int holding 0..65535, with arithmetic that wraps the way Int's does.

impls registered by this module (visible program-wide, no use needed): FromInt[U16], Show[U16], Add[U16], Sub[U16], Mul[U16], Div[U16], Rem[U16], Neg[U16]

U16

type U16

A unsigned 16-bit integer.

u16.MIN

const MIN: U16

The least U16, 0.

u16.MAX

const MAX: U16

The greatest U16, 65535.

u16.of

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

n as a U16, or None when it is outside 0..65535.

u16.wrap

fn wrap(n: Int) -> U16

The U16 with the low 16 bits of n: wrap(-1) is 65535.

u16.to_int

fn to_int(x: U16) -> Int

The value of x as an Int.

u16.checked_add

fn checked_add(a: U16, b: U16) -> Option[U16]

a + b, or None where it wraps.

u16.checked_sub

fn checked_sub(a: U16, b: U16) -> Option[U16]

a - b, or None where it wraps.

u16.checked_mul

fn checked_mul(a: U16, b: U16) -> Option[U16]

a * b, or None where it wraps.

u16.and

fn and(a: U16, b: U16) -> U16

The bitwise and.

u16.or

fn or(a: U16, b: U16) -> U16

The bitwise or.

u16.xor

fn xor(a: U16, b: U16) -> U16

The bitwise exclusive or.

u16.complement

fn complement(a: U16) -> U16

Every bit flipped.

u16.shl

fn shl(a: U16, n: Int) -> U16

a shifted left by n modulo 16 places, zeros in at the bottom.

u16.shr

fn shr(a: U16, n: Int) -> U16

a shifted right by n modulo 16 places, zeros in at the top.

u16.rotl

fn rotl(a: U16, n: Int) -> U16

a's 16 bits rotated left by n modulo 16 places.

u16.rotr

fn rotr(a: U16, n: Int) -> U16

a's 16 bits rotated right by n modulo 16 places.

std/int/u32

Unsigned 32-bit integers: U32, an opaque type over Int holding 0..4294967295, with arithmetic that wraps the way Int's does.

impls registered by this module (visible program-wide, no use needed): FromInt[U32], Show[U32], Add[U32], Sub[U32], Mul[U32], Div[U32], Rem[U32], Neg[U32]

U32

type U32

A unsigned 32-bit integer.

u32.MIN

const MIN: U32

The least U32, 0.

u32.MAX

const MAX: U32

The greatest U32, 4294967295.

u32.of

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

n as a U32, or None when it is outside 0..4294967295.

u32.wrap

fn wrap(n: Int) -> U32

The U32 with the low 32 bits of n: wrap(-1) is 4294967295.

u32.to_int

fn to_int(x: U32) -> Int

The value of x as an Int.

u32.checked_add

fn checked_add(a: U32, b: U32) -> Option[U32]

a + b, or None where it wraps.

u32.checked_sub

fn checked_sub(a: U32, b: U32) -> Option[U32]

a - b, or None where it wraps.

u32.checked_mul

fn checked_mul(a: U32, b: U32) -> Option[U32]

a * b, or None where it wraps.

u32.and

fn and(a: U32, b: U32) -> U32

The bitwise and.

u32.or

fn or(a: U32, b: U32) -> U32

The bitwise or.

u32.xor

fn xor(a: U32, b: U32) -> U32

The bitwise exclusive or.

u32.complement

fn complement(a: U32) -> U32

Every bit flipped.

u32.shl

fn shl(a: U32, n: Int) -> U32

a shifted left by n modulo 32 places, zeros in at the bottom.

u32.shr

fn shr(a: U32, n: Int) -> U32

a shifted right by n modulo 32 places, zeros in at the top.

u32.rotl

fn rotl(a: U32, n: Int) -> U32

a's 32 bits rotated left by n modulo 32 places.

u32.rotr

fn rotr(a: U32, n: Int) -> U32

a's 32 bits rotated right by n modulo 32 places.

std/int/u64

Unsigned 64-bit integers: U64, an opaque type over Int that reads the same 64 bits as 0..2^64 - 1, with arithmetic that wraps.

impls registered by this module (visible program-wide, no use needed): FromInt[U64], Show[U64], Ord[U64], Add[U64], Sub[U64], Mul[U64], Div[U64], Rem[U64], Neg[U64]

U64

type U64

A unsigned 64-bit integer.

u64.MIN

const MIN: U64

The least U64, 0.

u64.MAX

const MAX: U64

The greatest U64, 2^64 - 1; its bit pattern is the Int -1.

u64.of

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

n as a U64, or None when n is negative: a negative Int is not a number in 0..2^64 - 1. wrap reads its bits instead.

u64.wrap

fn wrap(n: Int) -> U64

The U64 with the bit pattern of n: wrap(-1) is MAX.

u64.to_int

fn to_int(x: U64) -> Int

The bit pattern of x as an Int: a value above 2^63 - 1 is negative.

u64.checked_add

fn checked_add(a: U64, b: U64) -> Option[U64]

a + b, or None where it wraps.

u64.checked_sub

fn checked_sub(a: U64, b: U64) -> Option[U64]

a - b, or None where it wraps.

u64.checked_mul

fn checked_mul(a: U64, b: U64) -> Option[U64]

a * b, or None where it wraps.

u64.and

fn and(a: U64, b: U64) -> U64

The bitwise and.

u64.or

fn or(a: U64, b: U64) -> U64

The bitwise or.

u64.xor

fn xor(a: U64, b: U64) -> U64

The bitwise exclusive or.

u64.complement

fn complement(a: U64) -> U64

Every bit flipped.

u64.shl

fn shl(a: U64, n: Int) -> U64

a shifted left by n modulo 64 places, zeros in at the bottom.

u64.shr

fn shr(a: U64, n: Int) -> U64

a shifted right by n modulo 64 places, zeros in at the top.

u64.rotl

fn rotl(a: U64, n: Int) -> U64

a's 64 bits rotated left by n modulo 64 places.

u64.rotr

fn rotr(a: U64, n: Int) -> U64

a's 64 bits rotated right by n modulo 64 places.

std/int/u8

Unsigned 8-bit integers: U8, an opaque type over Int holding 0..255, with arithmetic that wraps the way Int's does.

impls registered by this module (visible program-wide, no use needed): HasDtype[U8], DeviceBits[U8], FromInt[U8], Show[U8], Add[U8], Sub[U8], Mul[U8], Div[U8], Rem[U8], Neg[U8]

U8

type U8

A unsigned 8-bit integer.

u8.MIN

const MIN: U8

The least U8, 0.

u8.MAX

const MAX: U8

The greatest U8, 255.

u8.U8

const U8: Dtype[U8]

The device format of U8: u8.

u8.of

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

n as a U8, or None when it is outside 0..255.

u8.wrap

fn wrap(n: Int) -> U8

The U8 with the low 8 bits of n: wrap(-1) is 255.

u8.to_int

fn to_int(x: U8) -> Int

The value of x as an Int.

u8.checked_add

fn checked_add(a: U8, b: U8) -> Option[U8]

a + b, or None where it wraps.

u8.checked_sub

fn checked_sub(a: U8, b: U8) -> Option[U8]

a - b, or None where it wraps.

u8.checked_mul

fn checked_mul(a: U8, b: U8) -> Option[U8]

a * b, or None where it wraps.

u8.and

fn and(a: U8, b: U8) -> U8

The bitwise and.

u8.or

fn or(a: U8, b: U8) -> U8

The bitwise or.

u8.xor

fn xor(a: U8, b: U8) -> U8

The bitwise exclusive or.

u8.complement

fn complement(a: U8) -> U8

Every bit flipped.

u8.shl

fn shl(a: U8, n: Int) -> U8

a shifted left by n modulo 8 places, zeros in at the bottom.

u8.shr

fn shr(a: U8, n: Int) -> U8

a shifted right by n modulo 8 places, zeros in at the top.

u8.rotl

fn rotl(a: U8, n: Int) -> U8

a's 8 bits rotated left by n modulo 8 places.

u8.rotr

fn rotr(a: U8, n: Int) -> U8

a's 8 bits rotated right by n modulo 8 places.

std/io

The outside world: console, files, environment, clocks, random bytes and subprocesses. The file system is the named effect Fs, running another program is Proc, the process environment is Env, the host's two clocks are Clock and unpredictable bytes are Random, each with a production handler with_*_real. The console and ending the process are !io today; the effects that will carry them, Console and Exit, are declared below and their consumers are a release away. 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.

Instant

type Instant

A reading of the monotonic clock. The distance to another reading is the one thing it means, and elapsed_ns is how to get it. Being opaque, it cannot be mistaken for a wall reading or passed where a count of nanoseconds is wanted.

Fs

effect Fs {
  fn fs_is_dir(path: String) -> Bool !Fs
  fn fs_exists(path: String) -> Bool !Fs
  fn fs_is_symlink(path: String) -> Bool !Fs
  fn fs_mkdirs(path: String) -> Result[Unit, ForeignError] !Fs
  fn fs_read_file(path: String) -> Result[String, ForeignError] !Fs
  fn fs_write_file(path: String, content: String) -> Result[Unit, ForeignError] !Fs
  fn fs_read_bytes(path: String) -> Result[Bytes, ForeignError] !Fs
  fn fs_write_bytes(path: String, content: Bytes) -> Result[Unit, ForeignError] !Fs
  fn fs_delete(path: String) -> Result[Bool, ForeignError] !Fs
  fn fs_rename(src: String, dst: String) -> Result[Unit, ForeignError] !Fs
  fn fs_temp_dir(parent: String, prefix: String) -> Result[String, ForeignError] !Fs
  fn fs_temp_file(parent: String, prefix: String) -> Result[String, ForeignError] !Fs
  fn fs_copy_permissions(src: String, dst: String) -> Result[Unit, ForeignError] !Fs
  fn fs_list_names(path: String) -> Result[List[String], ForeignError] !Fs
  fn fs_real_path(path: String) -> Result[String, ForeignError] !Fs
}

The file system, as operations a handler answers. Names carry an fs_ prefix because use std/io.{Fs} brings every operation into the importing module's own namespace (spec §6.5), where mkdirs or read_bytes is a name a module may well have taken for a helper of its own.

The Bool queries answer false for whatever the host cannot spell, as their io.* twins do; everything else answers Result, with an Err for what the host refused. fs_delete answers whether something was removed (true) or was already absent (false); fs_list_names answers the entries in whatever order the host gave them.

Proc

effect Proc {
  fn proc_run(argv: List[String], out_path: String, err_path: String) -> Result[Int, ForeignError] !Proc
}

Running another program, as the one operation a handler answers. The name carries a proc_ prefix for the reason the fs_ ones do: use std/io.{Proc} brings the operation into the importing module's own namespace (spec §6.5), where run is a name a module may well have taken.

The arguments are run's, and so is the answer: an exit status for a child that ran, an Err for one that could not be started. Nothing here declares effects of its own (spec §6.5); what an arm does is charged where the handler is installed.

Env

effect Env {
  fn env_cwd() -> String !Env
  fn env_get(name: String) -> Option[String] !Env
}

The process environment, as the two operations a handler answers. The names carry an env_ prefix for the reason the fs_ and proc_ ones do: use std/io.{Env} brings every operation into the importing module's own namespace (spec §6.5), where cwd and get are names a module may well have taken for something of its own.

env_get answers None for a name the host has no value for and for one it cannot represent, including an embedded NUL, as its io.* twin does. Nothing here declares effects of its own (spec §6.5); what an arm does is charged where the handler is installed.

Exit

effect Exit {
  fn exit_now(code: Int) -> Never !Exit
}

Ending the process, as the one operation a handler answers. The name carries an exit_ prefix for the reason the fs_, proc_ and env_ ones do: use std/io.{Exit} brings the operation into the importing module's own namespace (spec §6.5), and here the bare name is taken by this very module.

ctl is what licenses an arm to bind the continuation instead of resuming it (spec §6.5), and it is what this effect is for: an arm that does not resume is how a process ends. The answer type is Never because a caller has no answer to read, and an arm that ends the block owes none.

Console

effect Console {
  fn console_print(s: String) -> Unit !Console
  fn console_println(s: String) -> Unit !Console
  fn console_eprint(s: String) -> Unit !Console
  fn console_eprintln(s: String) -> Unit !Console
}

The console, as the four operations a handler answers. The names carry a console_ prefix for the reason the fs_, proc_ and env_ ones do: use std/io.{Console} brings every operation into the importing module's own namespace (spec §6.5), and here all four bare names are taken by this very module.

Each operation is one host write. The pairing is the public functions': console_println and console_eprintln end the line, console_print and console_eprint do not, and the e names write to stderr. Nothing here declares effects of its own (spec §6.5); what an arm does is charged where the handler is installed.

Clock

effect Clock {
  fn clock_wall_ns() -> Int !Clock
  fn clock_mono() -> Instant !Clock
}

The host's two clocks, as the two operations a handler answers. The names carry a clock_ prefix for the reason the fs_ and env_ ones do: use std/io.{Clock} brings every operation into the importing module's own namespace (spec §6.5).

clock_wall_ns is nanoseconds since 1970-01-01T00:00:00Z. The host may set that clock, so a later reading can be smaller and a reading can be negative. clock_mono is a point on a line that only moves forward, from an origin nobody knows; how far apart two of them are is elapsed_ns.

Random

effect Random {
  fn random_bytes(n: Int) -> Bytes !Random
}

Unpredictable bytes. random_bytes(n) is exactly n of them: empty for 0, and a panic for a negative n, which is the caller's bug. The prefix is the one the other effects' operations carry (spec §6.5).

The production handler with_random_real answers from the host's cryptographically secure generator, so the bytes are suitable for tokens and keys. This is not a seedable PRNG.

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 !Exit

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

The result remains Unit, as it was when the body was io_exit directly: exit_now answers Never, and Never is under every type, so the public contract this function had before the operation existed is the one it still has. What changed is the row -- a caller now owes !Exit, and somebody above it has to say what ending the process means.

io.read_line

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

Read one line from stdin; None at end of input. A line ends at \n, at \r, or at \r\n, and the terminator is not part of it; a last line with no terminator is still a line. These are BufferedReader.readLine's rules on both backends. A \r ends the line without waiting for the next byte, and a \n right after it is dropped by the next call.

io.is_dir

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

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 !Fs

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] !Fs

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] !Fs

Read a whole file as a UTF-8 string.

io.write_file

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

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 !Env

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

io.getenv

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

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] !Fs

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] !Fs

Write bytes to a file, creating missing parent directories.

io.delete

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

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] !Fs

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] !Fs

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] !Fs

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] !Fs

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

fn real_path(path: String) -> Result[String, ForeignError] !Fs

path with every symbolic link followed and every . and .. resolved by the host, as realpath(3) resolves them: a .. after a link leaves the link's target, not the directory the link sits in, which is the case a textual normalization gets wrong.

path has to be absolute and has to exist. A relative one is "io.relative_path", another failure kind std mints itself, and for list_dir's reason: the check happens first, so it never reaches a backend and every backend words it the same. It is refused rather than resolved because resolving it would read the working directory, which is Env's answer and not Fs's; a caller that wants a relative path resolved joins it to cwd() itself and owes !Env where it does. A path that does not exist, or that the host cannot spell (an embedded NUL), is the Err the backend raised.

io.atomic_write_file

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

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.

n is the most the caller will take, not a size to allocate: a huge n on a short input returns what there is, and n <= 0 returns empty Bytes. On the JVM one call returns at most 2^31 - 1 bytes, the most a byte array holds.

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] !Fs

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] !Proc

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.

io.with_fs_real

fn with_fs_real[T, !e](body: fn() -> T !(Fs|e)) -> T !io

Run body against the real file system. Every arm is the host primitive behind the io.* function of the same name, through the barrier that used to sit inside the function.

This is the production handler: a program's main (or the innermost boundary that owns a whole unit of file work) installs it once, and everything beneath sees Fs. A test installs arms of its own instead.

io.with_proc_real

fn with_proc_real[T, !e](body: fn() -> T !(Proc|e)) -> T !io

Run body against real processes. The arm is the host primitive behind run, through the barrier that sits inside run today.

This is the production handler: a program installs it at the boundary that owns a whole unit of work, and everything beneath sees Proc. A test installs an arm of its own instead.

io.with_env_real

fn with_env_real[T, !e](body: fn() -> T !(Env|e)) -> T !io

Run body against the real process environment. Each arm is the host primitive behind the io.* function of the same meaning.

This is the production handler: a program installs it at the boundary that owns a whole unit of work, innermost of the three this module offers, and everything beneath sees Env. A test installs arms of its own instead.

io.with_exit_real

fn with_exit_real[T, !e](body: fn() -> T !(Exit|e)) -> T !io

Run body on a host that can really end this process. The arm calls the exit primitive and never resumes, so nothing after a call to exit_now runs, here or anywhere below.

This is the production handler: a program installs it at the boundary that owns a whole unit of work, and everything beneath sees Exit. A test installs an arm that answers the status as a value instead, and the test process goes on running.

io.with_console_real

fn with_console_real[T, !e](body: fn() -> T !(Console|e)) -> T !io

Run body against the real console. Each arm is the host primitive behind the io.* function of the same meaning.

This is the production handler: a program installs it at the boundary that owns a whole unit of work, and everything beneath sees Console. A test installs arms of its own instead, and can then say what a program printed and which stream it went to.

io.with_clock_real

fn with_clock_real[T, !e](body: fn() -> T !(Clock|e)) -> T !io

Run body against the host's clocks. Each arm is one host reading.

This is the production handler: a program installs it at the boundary that owns a whole unit of work, and everything beneath sees Clock. A test installs arms of its own instead, and time is then whatever the test says.

io.now_wall_ns

fn now_wall_ns() -> Int !Clock

Nanoseconds since 1970-01-01T00:00:00Z, as the Clock handler answers. The host may set this clock, so a duration is now and elapsed_ns, never two of these subtracted.

io.now

fn now() -> Instant !Clock

The current point on the monotonic clock, as the Clock handler answers.

io.elapsed_ns

fn elapsed_ns(start: Instant, stop: Instant) -> Int

Nanoseconds from start to stop, and 0 when stop is the earlier one.

The subtraction stays right across a wrap of the Int, which is why two readings are compared this way and not with <. The floor at 0 is Rust's Instant::duration_since rule: a monotonic clock that went backwards is a hardware, virtualisation or OS fault, and a negative duration handed on would make it the caller's bug.

io.instant_at_ns

fn instant_at_ns(ns: Int) -> Instant

An Instant ns nanoseconds from the clock's origin, for an arm that answers clock_mono outside this module: a test's fake clock. A program that wants the time calls now.

io.with_random_real

fn with_random_real[T, !e](body: fn() -> T !(Random|e)) -> T !io

Run body against the host's secure random source. Each call to random_bytes is one request to the host, so a caller that wants a lot of bytes asks once for all of them.

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]], Len[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], f: fn(T) -> Bool !e) -> Option[T] !e

The first element satisfying f, 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/loc

Call sites: the Loc a caller() default hands a function, and what there is to ask of one (spec §8.4).

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

loc.here

fn here(at: Loc = caller()) -> Loc

The location of the call of here(): the one place caller() is wanted somewhere other than a parameter's default, given by a function whose default it is.

loc.path

fn path(l: Loc) -> String

The path of the file the call is in, as the program names it: relative to the project root (src/main.dawn), to the entry file's directory for a single file, <package>/<path> in a [deps] package.

loc.line

fn line(l: Loc) -> Int

The 1-based line of the call.

loc.col

fn col(l: Loc) -> Int

The 1-based column of the call, counted in code points, the column the compiler's diagnostics print.

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): Len[Map[K, V]], 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.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/mem

Flat, mutable buffers of 64-bit integers (I64Buf) and 64-bit floats (F64Buf), for the hot loop that has to write as it reads (a cipher's S-boxes, a sliding window, a numeric kernel's working array).

impls registered by this module (visible program-wide, no use needed): Len[I64Buf], Len[F64Buf]

Mem

effect Mem {
  fn mem_alloc(n: Int) -> I64Buf !Mem
  fn mem_alloc_f64(n: Int) -> F64Buf !Mem
}

The operations of Mem: a fresh buffer of either kind. The handler in with_mem answers it; nothing else may install one.

mem.with_mem

fn with_mem[T, !e](body: fn() -> T !(Mem|e)) -> T !e

Runs body with a fresh memory scope. Every buffer made inside is private to this call, and the result cannot mention one (the checker enforces it). It is pure when !e is empty, so a computation that fills and reads buffers inside can still be folded and shared like any other pure call.

mem.new

fn new(n: Int) -> I64Buf !Mem

A buffer of n zeros. Panics when n is negative or above the limit.

mem.at

fn at(b: I64Buf, i: Int) -> Int !Mem

The element at i. Panics when i is outside [0, len(b)).

mem.set

fn set(b: I64Buf, i: Int, v: Int) -> Unit !Mem

Stores v at i. Panics when i is outside [0, len(b)).

mem.fill

fn fill(b: I64Buf, from: Int, to: Int, v: Int) -> Unit !Mem

Stores v in every position of [from, to). Panics unless 0 <= from <= to <= len(b); it never clamps.

mem.copy_from

fn copy_from(dst: I64Buf, dst_at: Int, src: I64Buf, src_at: Int, n: Int) -> Unit !Mem

Copies n elements from src starting at src_at to dst starting at dst_at. The destination comes first, as in memcpy. The ranges may overlap when both buffers are the same one: the result is as if the source were read in full before anything was written. Panics when n is negative or either range leaves its buffer; it never clamps.

mem.copy_within

fn copy_within(b: I64Buf, from: Int, to_at: Int, n: Int) -> Unit !Mem

Copies n elements of b from from to to_at, within the same buffer. copy_from with one buffer, and as safe under overlap.

mem.clone

fn clone(b: I64Buf) -> I64Buf !Mem

A new buffer with the same contents. The two do not see each other's writes.

mem.from_list

fn from_list(xs: List[Int]) -> I64Buf !Mem

A new buffer holding xs, in order.

mem.to_list

fn to_list(b: I64Buf) -> List[Int] !Mem

The elements of b, in order, as a list. It goes through the bulk byte path at width 8, which measures faster than a per-element loop on both backends.

mem.to_bytes

fn to_bytes(b: I64Buf, width: Int, order: Endian = Little) -> Bytes !Mem

Each element of b as width bytes, which must be 1, 2, 4 or 8. An element keeps its low 8 * width bits, so the result of pack_ints on the same elements, byte for byte. Any other width panics.

mem.from_bytes

fn from_bytes(bs: Bytes, width: Int, signed: Bool, order: Endian = Little) -> I64Buf !Mem

The bytes of bs as a buffer of width-byte integers, signed choosing sign or zero extension; the inverse of to_bytes. Panics on a width other than 1, 2, 4 or 8, and on a length that is not a multiple of width.

mem.f64_new

fn f64_new(n: Int) -> F64Buf !Mem

A buffer of n zeros (+0.0). Panics when n is negative or above the limit.

mem.f64_at

fn f64_at(b: F64Buf, i: Int) -> Float !Mem

The element at i, bit for bit. Panics when i is outside [0, len(b)).

mem.f64_set

fn f64_set(b: F64Buf, i: Int, v: Float) -> Unit !Mem

Stores v at i, bit for bit. Panics when i is outside [0, len(b)).

mem.f64_fill

fn f64_fill(b: F64Buf, from: Int, to: Int, v: Float) -> Unit !Mem

Stores v in every position of [from, to). Panics unless 0 <= from <= to <= len(b); it never clamps.

mem.f64_copy_from

fn f64_copy_from(dst: F64Buf, dst_at: Int, src: F64Buf, src_at: Int, n: Int) -> Unit !Mem

copy_from for doubles: the destination comes first, overlap is safe, and a range that leaves its buffer panics.

mem.f64_copy_within

fn f64_copy_within(b: F64Buf, from: Int, to_at: Int, n: Int) -> Unit !Mem

copy_within for doubles.

mem.f64_clone

fn f64_clone(b: F64Buf) -> F64Buf !Mem

A new buffer with the same contents. The two do not see each other's writes.

mem.f64_from_list

fn f64_from_list(xs: List[Float]) -> F64Buf !Mem

A new buffer holding xs, in order.

mem.f64_to_list

fn f64_to_list(b: F64Buf) -> List[Float] !Mem

The elements of b, in order, as a list.

mem.f64_to_bytes

fn f64_to_bytes(b: F64Buf, width: Int, order: Endian = Little) -> Bytes !Mem

Each element of b as width bytes, which must be 4 or 8, as pack_floats does: width 8 keeps the bits (a NaN's payload and sign, -0.0), width 4 rounds to the nearest float. Any other width panics.

mem.f64_from_bytes

fn f64_from_bytes(bs: Bytes, width: Int, order: Endian = Little) -> F64Buf !Mem

The bytes of bs as a buffer of doubles read from width-byte floats (4 or 8); the inverse of f64_to_bytes. Panics on another width and on a length that is not a multiple of width.

std/memfs

A file tree held in memory, answering std/io's Fs effect without a host.

[with_fs] installs a handler whose arms read and write one table, so a test declares the tree it wants, runs code that takes Fs over it, and reads the table back afterwards: the code can see nothing the test did not put there, and what it wrote is a value the test can compare.

The semantics are the table's, not a kernel's. Directories are implied by the paths in the table, so fs_is_dir, fs_exists and fs_list_names answer what a real file system would answer for the same tree. A symbolic link is an entry of its own, made by [link], and only two operations know it is there: fs_is_symlink answers whether a path is one, and fs_real_path follows every one on the way, as realpath(3) does. The other operations do not follow links and do not see them. Paths are normalized against the base directory named at [empty], and nothing here reads the host, not even for the current directory. Files hold bytes, so binary writes survive unchanged; only the text operations encode or decode UTF-8, and reading text never changes the stored bytes. A refusal is an Err whose kind starts with memfs..

MemFs

type MemFs = { base: String, files: Map[String, Bytes], dirs: Map[String, Bool], links: Map[String, String], seq: Int }

A file tree, plus the directory relative paths resolve against and the counter fs_temp_dir / fs_temp_file hand out names from. links maps a link's absolute path to its target, spelled as it was given.

memfs.BASE

const BASE: String

The base a table uses when its caller has no reason to name one of its own. Absolute, so it is a base at all, and a directory no host has, so a path built from it cannot be confused with one that names a real file.

memfs.empty

fn empty(base: String) -> MemFs

An empty tree whose relative paths resolve against base.

The base is a parameter rather than the working directory: reading that is an effect, and what the base has to be is the same string for the whole life of one table, which a parameter says and a host query only happens to provide.

memfs.put

fn put(st: MemFs, path: String, contents: String) -> MemFs

st with one more file in it, holding contents as UTF-8; the directories above it come along. A directory already at path leaves st as it was.

memfs.text

fn text(st: MemFs, path: String) -> Option[String]

The file at path as UTF-8 text, or None when nothing is there. Malformed sequences are replaced, just as in the text-reading arm.

memfs.file_paths

fn file_paths(st: MemFs) -> List[String]

Every file path in the table, absolute and sorted. Directories are not listed: they are implied by these.

memfs.with_fs

fn with_fs[T, !e](seed: MemFs, body: fn() -> T !(Fs|e)) -> (T, MemFs) !e

Run body with Fs answered from seed, and hand back the table as it stands afterwards.

Every arm resumes in tail position. The state is a handler cell (spec §6.5), which is what lets a write in one arm be read by the next one and by the caller here. The body row is !e, as every std handler's is, so this nests inside or outside the other wrappers in either order.

std/narrow

Narrow binary floating point: bfloat16, IEEE binary16 and binary32 as opaque types over Float, with correctly rounded arithmetic on each, and the five storage-only formats the tile dialect also has.

impls registered by this module (visible program-wide, no use needed): HasDtype[BF16], DeviceBits[BF16], HasDtype[F16], DeviceBits[F16], HasDtype[F32], DeviceBits[F32], HasDtype[TF32], DeviceBits[TF32], HasDtype[F8E4M3FN], DeviceBits[F8E4M3FN], HasDtype[F8E5M2], DeviceBits[F8E5M2], HasDtype[F8E8M0FNU], DeviceBits[F8E8M0FNU], HasDtype[F4E2M1FN], HasDtype[F8E5M3FNU], Show[BF16], Show[F16], Show[F32], Narrow[BF16], Narrow[F16], Narrow[F32], Add[BF16], Sub[BF16], Mul[BF16], Div[BF16], Rem[BF16], Neg[BF16], FromFloat[BF16], Add[F16], Sub[F16], Mul[F16], Div[F16], Rem[F16], Neg[F16], FromFloat[F16], Add[F32], Sub[F32], Mul[F32], Div[F32], Rem[F32], Neg[F32], FromFloat[F32], FromFloat[TF32], FromFloat[F8E4M3FN], FromFloat[F8E5M2], FromFloat[F8E8M0FNU], FromFloat[F4E2M1FN], Show[TF32], Show[F8E4M3FN], Show[F8E5M2], Show[F8E8M0FNU], Show[F4E2M1FN]

BF16

type BF16

A bfloat16 value: 8 significand bits, exponents -126..127.

F16

type F16

An IEEE binary16 value: 11 significand bits, exponents -14..15.

F32

type F32

An IEEE binary32 value: 24 significand bits, exponents -126..127.

TF32

type TF32

A TensorFloat-32 value: round_tf32's grid.

F8E4M3FN

type F8E4M3FN

An OCP fp8 value with four exponent bits and three significand bits and no infinity: round_f8e4m3fn's grid.

F8E5M2

type F8E5M2

An OCP fp8 value with five exponent bits and two significand bits: round_f8e5m2's grid.

F8E8M0FNU

type F8E8M0FNU

An fp8 exponent value, a power of two: round_f8e8m0's grid.

F4E2M1FN

type F4E2M1FN

An OCP MX fp4 value: round_f4e2m1's grid.

F8E5M3FNU

type F8E5M3FNU

An unsigned fp8 value with five exponent bits and three significand bits (bytecode 13.4's f8E5M3FNU): the scale format of mma_scaled over f4E2M1FN operands on sm_107. It is a TILE format only, like F4E2M1FN and for a different reason: it has a name and a type tag and no host value. There is no round_ function and no FromFloat because what ftof does at its edges (a negative input, the saturation above the largest finite value, NaN) is a device behaviour, and only sm_107 has the device; nobody here has measured it. The value set is LLVM's Float8E5M3FNU: bias 15, subnormals down to 2^-17, the largest finite value 1.75 * 2^16, and the all-ones octet is the only NaN.

Rounding

type Rounding = NearestEven | NearestAway | TowardZero | Down | Up

How a value between two neighbours of a format picks one: the five rounding-direction attributes of IEEE 754 section 4.3, under short names.

  • NearestEven: roundTiesToEven, the nearest neighbour, a tie to the one with an even last digit. The default everywhere, and the one Figueroa's theorem covers.
  • NearestAway: roundTiesToAway, the nearest neighbour, a tie to the one of larger magnitude.
  • TowardZero: roundTowardZero, the neighbour of smaller magnitude.
  • Down: roundTowardNegative, the neighbour below.
  • Up: roundTowardPositive, the neighbour above.

The Tile IR dialect (AttrDefs.td's RoundingMode) spells the same five nearest_even, nearest_away, zero, negative_inf and positive_inf.

Narrow

trait Narrow[T] {
  fn to_f64[T: Narrow](a: T) -> Float
  fn add[T: Narrow](a: T, b: T) -> T
  fn sub[T: Narrow](a: T, b: T) -> T
  fn mul[T: Narrow](a: T, b: T) -> T
  fn div[T: Narrow](a: T, b: T) -> T
  fn sqrt[T: Narrow](a: T) -> T
  fn neg[T: Narrow](a: T) -> T
  fn abs[T: Narrow](a: T) -> T
}

The operations every narrow format offers. add, sub, mul, div and sqrt are the format's correctly rounded operations (ties to even, overflow to infinity, gradual underflow); neg and abs are exact. Equality and order are the Float's, as on any opaque type, so == and < need no member. a + b, a - b, a * b, a / b and -a are add, sub, mul, div and neg under the operator's name, and a % b is the exact truncating remainder.

fn to_f64[T: Narrow](a: T) -> Float

The value as a Float; exact, since every value of the format is one.

fn add[T: Narrow](a: T, b: T) -> T

Correctly rounded sum.

fn sub[T: Narrow](a: T, b: T) -> T

Correctly rounded difference.

fn mul[T: Narrow](a: T, b: T) -> T

Correctly rounded product.

fn div[T: Narrow](a: T, b: T) -> T

Correctly rounded quotient.

fn sqrt[T: Narrow](a: T) -> T

Correctly rounded square root.

fn neg[T: Narrow](a: T) -> T

The negation.

fn abs[T: Narrow](a: T) -> T

The magnitude.

narrow.BF16

const BF16: Dtype[BF16]

The bfloat16 device format, the witness of BF16.

narrow.F16

const F16: Dtype[F16]

The binary16 device format, the witness of F16.

narrow.F32

const F32: Dtype[F32]

The binary32 device format, the witness of F32.

narrow.TF32

const TF32: Dtype[TF32]

The TensorFloat-32 device format, the witness of TF32.

narrow.F8E4M3FN

const F8E4M3FN: Dtype[F8E4M3FN]

The f8E4M3FN device format, the witness of F8E4M3FN.

narrow.F8E5M2

const F8E5M2: Dtype[F8E5M2]

The f8E5M2 device format, the witness of F8E5M2.

narrow.F8E8M0FNU

const F8E8M0FNU: Dtype[F8E8M0FNU]

The f8E8M0FNU device format, the witness of F8E8M0FNU.

narrow.F4E2M1FN

const F4E2M1FN: Dtype[F4E2M1FN]

The f4E2M1FN device format, the witness of F4E2M1FN.

narrow.F8E5M3FNU

const F8E5M3FNU: Dtype[F8E5M3FNU]

The f8E5M3FNU device format, the witness of F8E5M3FNU.

narrow.round_binary

fn round_binary(x: Float, p: Int, emin: Int, emax: Int, mode: Rounding = NearestEven) -> Float

The value of the binary format with p significand bits (hidden bit included), minimum normal exponent emin and maximum exponent emax that mode picks for x. Below the normal range the answer lies on the subnormal grid 2^(emin - p + 1), and may be zero of the sign. NaN, the two zeros and the two infinities pass through unchanged.

A finite x too big for the format is where the modes differ in kind, not just in which neighbour: the two nearest modes overflow to the infinity of the sign, and of the directed ones, rounding TOWARDS the value's own infinity overflows to it while rounding away from it answers the largest finite value. IEEE 754 section 4.3 says so, and a caller comparing against hardware would otherwise read an infinity where the device holds a finite number.

narrow.round_bf16

fn round_bf16(x: Float) -> Float

x rounded to bfloat16.

narrow.round_f16

fn round_f16(x: Float) -> Float

x rounded to binary16.

narrow.round_f32

fn round_f32(x: Float, mode: Rounding = NearestEven) -> Float

x rounded to binary32 in the direction mode names.

narrow.round_tf32

fn round_tf32(x: Float, mode: Rounding = NearestEven) -> Float

x rounded to TensorFloat-32 in the direction mode names: binary32's exponent range with eleven significand bits instead of twenty-four. It is a rounding and not a storage format in this module's sense -- tf32_bits puts it in a 32-bit word with thirteen zero bits at the bottom, which is where the hardware keeps it. NearestAway is what the device answers for ftof from f32 to tf32 under nearest_away (measured, knife T17).

narrow.round_f8e5m2

fn round_f8e5m2(x: Float) -> Float

x rounded to f8E5M2: three significand bits, exponents -14..15, and SATURATING at the largest finite value.

The format is an ordinary IEEE binary format at eight bits, infinities and all, so round_binary is the arithmetic; what is not IEEE is the top end. An IEEE conversion sends everything above the midpoint 61440 to infinity, and this format has an infinity to send it to (f8e5m2_bits and f8e5m2_of_bits keep the two 0x7C encodings, because a value ALREADY in the format can be one). The device does not: knife TA converted 1e30, -1e30 and an infinity on a B200 and got 57344, -57344 and 57344.

So the codec and this function part company at exactly two inputs, and the reason is that they answer different questions. f8e5m2_of_bits says what an octet MEANS; this says what ftof DOES, and ftof saturates. The two fp8 formats below say the same thing and f4E2M1FN says it too, so saturation is this assembler's rule for the narrow floats and not a property of any one of them.

narrow.round_f8e4m3fn

fn round_f8e4m3fn(x: Float) -> Float

x rounded to f8E4M3FN: four significand bits, exponents -6..8.

FN is the whole of what this function adds to round_binary. The format has no infinity: the all-ones exponent field is a normal exponent, and only the single encoding with an all-ones significand under it is taken, as NaN. So the largest finite value is 1.75 2^8 = 448 rather than the 1.875 2^8 = 480 an IEEE format of these widths would have, and there is nothing above it to overflow to except NaN.

THE OVERFLOW ANSWER IS SATURATION, AND IT IS NOW MEASURED. The dialect gives ftof no saturation attribute, so this function used to read IEEE's rule with the infinity replaced by the only encoding left over: anything above the midpoint 464 answered NaN. That was a guess, written down as one, because no device this tree could reach accepted the type. Knife TA reached one: on a B200 (sm_100) ftof answers 448 for 480, -448 for -480 and 448 for an infinity, and NaN only for NaN.

464 is unchanged and was never the question: it is the midpoint between 448 and the 480 an IEEE format of these widths would have, and 448's significand is the even one, so it ties DOWN under either reading. What the measurement settled is everything above it.

narrow.round_f4e2m1

fn round_f4e2m1(x: Float) -> Float

x rounded to f4E2M1FN: two significand bits, exponents 0..2, and SIXTEEN VALUES IN ALL: +/-0, +/-0.5, +/-1, +/-1.5, +/-2, +/-3, +/-4, +/-6. The whole value set fits on one line, which is what makes this format different from the eight-bit ones rather than merely smaller.

FN here is stronger than f8E4M3FN's. That format has no infinity and keeps two NaN encodings; this one has NEITHER, so every one of the sixteen bit patterns is a finite number and there is nothing to answer with when a value does not fit. So the two answers below are CHOICES, and this tree cannot measure either: tileiras 13.3.36 refuses the type at sm_86 and sm_89 and takes it only from sm_100, and the machine of the ledger is an RTX 3080 (docs/tile-backend-design.md 6.12).

a magnitude above the largest finite value SATURATES to it, sign kept, rather than overflowing. There is no infinity to overflow to, and the OCP MX specification's conversions saturate. NaN answers +0.0. There is no NaN encoding either, and zero is the one value that is not a plausible answer to any finite input, so a corpus that reads it back knows it hit this arm.

The rounding itself is round_binary's: two significand bits (the hidden one included), minimum normal exponent 0 and maximum 2, so the subnormal grid is 2^-1 and 0.5 is the one subnormal. Types.td's own example says the same in bits: 0001 is 0.5 and 0011 is 1.5.

narrow.round_f8e8m0

fn round_f8e8m0(x: Float) -> Float

x rounded to f8E8M0FNU: the exponent alone.

The value set is the 255 powers 2^-127 .. 2^127 and NaN, and that is all of it: no sign bit, no significand, no zero, no infinity. So this is not a round_binary at some width, it is a different function, and its rounding mode is different too -- ftof accepts only zero and positive_inf for this target (Ops.td's FToFOp), never the nearest_even every other conversion here takes. This is the zero one, which is what packages/tileir writes: the answer is the largest power of two at or below x.

NOTHING BUT NaN ANSWERS NaN, and that too is measured rather than chosen. This function used to send zero, every negative number and the infinities to NaN, on the reading that a format with no encoding for them has nothing else to answer. Knife TA asked a B200: zero answers 2^-127, -1 answers 1, an infinity answers 2^127, and 2^140 answers 2^127. So ftof into this format takes the MAGNITUDE and then clamps into the range, which is the same saturation the two fp8 formats above do, with the sign dropped because there is no sign bit to keep it in.

A positive number below 2^-127 has no representable value below it and clamps UP to 2^-127; that arm was a guess too and the device agrees with it (2^-140 answers 2^-127).

f8e8m0_bits is NOT changed to match: it answers what an octet means, and zero and the negatives are still not values of this format. The two functions answer different questions, and round_f8e5m2 records the same split at the other end of the range.

narrow.bf16

fn bf16(x: Float) -> BF16

The bfloat16 nearest to x.

narrow.f16

fn f16(x: Float) -> F16

The binary16 nearest to x.

narrow.f32

fn f32(x: Float) -> F32

The binary32 nearest to x.

narrow.bf16_bits

fn bf16_bits(x: Float) -> Int

The 16-bit pattern of the bfloat16 value x: the sign bit, eight exponent bits biased by 127, seven significand bits, as the value sits in memory. x is a value of the format, as round_bf16 leaves it. For a Float off the bf16 grid the answer is the pattern with the extra significand bits dropped (truncation toward zero, what reading the high half of a binary32 gives), NOT the nearest bf16: round first. A value beyond the format's range answers the infinity of its sign. NaN answers the canonical quiet NaN 0x7FC0: a Float carries no payload a program can read, so none can be preserved.

narrow.f16_bits

fn f16_bits(x: Float) -> Int

The 16-bit pattern of the IEEE binary16 value x: the sign bit, five exponent bits biased by 15, ten significand bits, as the value sits in memory. The bfloat16 function's shape at binary16's widths, and the same contract: x is a value of the format, as round_f16 leaves it, and a Float off the binary16 grid answers the pattern with the extra significand bits dropped rather than the nearest binary16. A value beyond the format's range answers the infinity of its sign; NaN answers the canonical quiet NaN 0x7E00.

The subnormal arm counts quanta of 2^-24, which is binary16's least positive value, where bfloat16 counts 2^-133; the two formats differ in every constant here and in nothing else.

narrow.f16_of_bits

fn f16_of_bits(bits: Int) -> Float

The IEEE binary16 value whose 16-bit pattern is the low 16 bits of bits, exactly, as a Float (every binary16 value is one). An exponent field of all ones is the infinity of the sign when the significand field is zero and NaN otherwise; an exponent field of zero is a subnormal, or the zero of the sign.

narrow.bf16_of_bits

fn bf16_of_bits(bits: Int) -> Float

The bfloat16 value whose 16-bit pattern is the low 16 bits of bits, exactly, as a Float (every bf16 value is one). An exponent field of all ones is the infinity of the sign when the significand field is zero and NaN otherwise; an exponent field of zero is a subnormal, or the zero of the sign.

narrow.f32_bits

fn f32_bits(x: Float) -> Int

The 32-bit pattern of the binary32 value x: the sign bit, eight exponent bits biased by 127, twenty-three significand bits. The bfloat16 function's shape at binary32's widths and the same contract: x is a value of the format, as round_f32 leaves it, and a Float off the grid answers the pattern with the extra bits dropped rather than the nearest binary32.

This is the codec the module's header said would arrive "when something needs it". tf32_bits is what needs it: TensorFloat-32 lives in a binary32 word.

narrow.f32_of_bits

fn f32_of_bits(bits: Int) -> Float

The binary32 value whose 32-bit pattern is the low 32 bits of bits, exactly, as a Float (every binary32 value is one).

narrow.f64_bits

fn f64_bits(x: Float) -> Int

The 64-bit pattern of the binary64 value x, in an Int (the top bit is the sign, so a negative value answers a negative Int): the sign bit, eleven exponent bits biased by 1023, fifty-two significand bits. Every Float is a binary64 value, so there is no off-the-grid case. The pattern is the raw one: a NaN keeps its payload and its sign (what a NaN computed as 0.0 / 0.0 carries is the host's, so this module's tests do not pin it).

narrow.f64_of_bits

fn f64_of_bits(bits: Int) -> Float

The binary64 value whose 64-bit pattern is bits, exactly, a NaN's payload included (the inverse of f64_bits on every pattern).

narrow.tf32_bits

fn tf32_bits(x: Float) -> Int

The 32-bit pattern of the TensorFloat-32 value x: a binary32 word whose bottom THIRTEEN bits are zero. tf32 is nineteen bits of information (a sign, binary32's eight exponent bits and ten significand bits) and the hardware keeps it in a 32-bit slot, so a tf32 buffer is a buffer of 32-bit words and this is how one is laid out. x is a value of the format, as round_tf32 leaves it.

narrow.tf32_of_bits

fn tf32_of_bits(bits: Int) -> Float

The TensorFloat-32 value whose 32-bit pattern is the low 32 bits of bits. The bottom thirteen bits are not part of the format and are ignored, so this is total on every word and tf32_of_bits(tf32_bits(v)) is v for every value of the format.

narrow.f8e5m2_bits

fn f8e5m2_bits(x: Float) -> Int

The octet of the f8E5M2 value x: the sign bit, five exponent bits biased by 15, two significand bits. The binary16 function's shape at eight bits; the subnormal arm counts quanta of 2^-16 and the canonical quiet NaN is 0x7E.

narrow.f8e5m2_of_bits

fn f8e5m2_of_bits(bits: Int) -> Float

The f8E5M2 value whose octet is the low eight bits of bits, exactly.

narrow.f8e4m3fn_bits

fn f8e4m3fn_bits(x: Float) -> Int

The octet of the f8E4M3FN value x: the sign bit, four exponent bits biased by 7, three significand bits. The subnormal arm counts quanta of 2^-9.

THE ALL-ONES EXPONENT IS A NORMAL EXPONENT here, which is the whole difference from f8e5m2_bits: 0x7F and 0xFF are the format's only two NaN encodings and everything below them is finite, so 0x7E is 448 and not an infinity. A value beyond the range answers the canonical NaN 0x7F, because there is no infinity to answer with.

narrow.f8e4m3fn_of_bits

fn f8e4m3fn_of_bits(bits: Int) -> Float

The f8E4M3FN value whose octet is the low eight bits of bits, exactly. 0x7F and 0xFF are NaN and every other octet is a finite value.

narrow.f4e2m1_bits

fn f4e2m1_bits(x: Float) -> Int

The NIBBLE of the f4E2M1FN value x: the sign bit, two exponent bits biased by 1, one significand bit, in the low four bits of the answer. The value set is small enough to lay out whole, and this does: the magnitude picks one of eight patterns and the sign is the top bit.

x is a value of the format, as round_f4e2m1 leaves it. Anything else is not on the grid and answers the nibble of the nearest value below it in magnitude, which is what f8e8m0_bits does with the same problem.

narrow.f4e2m1_of_bits

fn f4e2m1_of_bits(bits: Int) -> Float

The f4E2M1FN value whose nibble is the low four bits of bits, exactly. Every one of the sixteen patterns is a finite number, so this is total and has no NaN arm; 0x8 is negative zero.

narrow.f8e8m0_bits

fn f8e8m0_bits(x: Float) -> Int

The octet of the f8E8M0FNU value x: the biased exponent alone, e + 127 for x = 2^e, and 0xFF for NaN. There is no sign bit and no significand, so an octet is a value and the codec is a shift. x is a value of the format, as round_f8e8m0 leaves it; anything else answers NaN rather than the nearest power of two.

narrow.f8e8m0_of_bits

fn f8e8m0_of_bits(bits: Int) -> Float

The f8E8M0FNU value whose octet is the low eight bits of bits: 2^(b - 127), and NaN for 0xFF. Every one of the other 255 octets is a distinct positive power of two, so this format has no zero.

std/reactor

Run a synchronous newline protocol whose state survives calls to main.

On native and JVM targets, a session lasts for the process. On wasm, it lasts for one module instance, across separate calls to dawn_turn. [serve] calls step with None until an invocation returns a replacement state. Afterwards it supplies Some(current_state). Returning Some(next) replaces the retained state only after that invocation succeeds; returning None, refusing an input, or panicking preserves the previous state. Optional replies are printed one per line, and on_panic supplies the reply for a caught panic.

A session must use exactly one state type. Calling serve[S] and later serve[T] in the same process or wasm module instance is a program error: the installed root continues dispatching lines to the S step. Start a new process or instantiate a new wasm module to change state type.

reactor.serve

fn serve[S](step: fn(Option[S], String) -> (Option[S], Option[String]), on_panic: fn(ForeignError) -> String) -> Unit !io

Read one input line, run one typed step, optionally print one reply, and continue to EOF.

step sees None before the first successful init. Its optional first result is a replacement, not the current state: a failure or ordinary event returns None, preserving the root already installed by an earlier init. on_panic renders a caught application panic without receiving any state; the root therefore remains installed on that path too.

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): Len[Set[T]], 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.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.

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): Len[String], Iter[String]

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, 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, a space unless given, 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, a space unless given, 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.