标准库的名字有两个来源:编译器的内建表,以及随编译器捆绑的 std/ 模块(Dawn 源码)。
实现位置对使用者不可见:哪边实现不影响拼写、类型与语义,所以这一页不按「内建 / std」
分组,而按你写代码时怎么用它分组。
怎么读:
- prelude 里的名字隐式可见,不必
use,直接写println("hi")、sort(xs)。 - 其余以
use std/x引入,再限定调用:use std/str之后写str.trim(s); 常用的名字也可以选择性引入(use std/str.{trim})。 - 第一个参数就是被操作对象的函数也可以用点调用:
xs.sort()就是sort(xs);use std/str.{trim}之后,s.trim()就是trim(s)(规范 §4.3)。 只写整模块引入use std/str时只有限定写法str.trim(s)。 - 同一个函数在「prelude」和它所属模块里各出现一次是正常的:
sort(xs)与list.sort(xs)是同一个函数的两种写法。 - 签名里
[T: Ord]是类型参数与它的约束(规范 §3.5),!io是效果(规范 §6),!e是效果变量,表示该函数的效果由传进去的 闭包决定。 - 会失败的操作回
Result,可能没有答案的回Option;何时断言、何时问询、何时钳位, 判据在规范 §4.8。
规范中英两版都有:中文是正本,英文是它的译本。
内建类型#
这些类型是语言本身的一部分,不需要 use,也没有 std 源码可查。语义定义在规范里,
这里只给一句话的索引。
| 类型 | 一句话 |
|---|---|
Int | 64 位有符号整数 |
Float | 双精度浮点;渲染与解析由 std/fmt 定死,不随宿主变(§4.3) |
Bool | true / false |
String | 不可变字符串,按码点计量;里面没有非良构的 UTF-8 |
Unit | 唯一值 (),一等值,能出现在任何值能出现的位置 |
List[T] | 不可变持久列表(32 叉 trie + 尾块);acc ++ [x] 的累积是线性的 |
Option[T] | Some(T) 或 None;语言没有 null |
Result[T, E] | Ok(T) 或 Err(E);? 对它有语法支持(§8.1) |
ForeignError | 外部失败的结构化载荷:kind 与一句给人读的说明(§9.8.1) |
Map[K, V] | 不可变持久映射,迭代按插入序;键需 Eq + Hash(§2.2) |
Set[T] | 不可变持久集合,同上 |
Bytes | 不可变字节串,按内容相等;二进制数据走它而不借道字符串(§9.5.1) |
Buf | Bytes 的写入游标:bytes.buf() 造、bytes.freeze() 收 |
Cursor | String 里的位置,不是下标;由 std/cursor 声明为不透明类型 |
(A, B, ...) | 元组 |
fn(A, B) -> C !e | 函数类型;!e 可省略,表示纯 |
Map / Set 底下的持久 HAMT 与 List 底下的持久向量都是纯 Dawn 源码
(std/hamt、std/pvec)。它们是内部模块:表示要能整体换掉,而能换的前提是
没有程序依赖它,所以在 std 之外 use 它们是编译错误,这一页也不列它们。
prelude:不必 use 的名字#
下面这些名字隐式可见,直接写就是了。分组只为好找,不代表实现位置;其中在 std 模块里也有一份的(sort、println 等)会在下面「模块」一节里以限定拼写再出现一次。同一个函数,两种写法。
io
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
sort_by
fn sort_by[T, !e](xs: List[T], cmp: fn(T, T) -> Int !e) -> List[T] !e
stable sort with a two-argument cmp function
max
fn max[T: Ord](xs: List[T]) -> Option[T]
the first greatest element; None when empty (elements need Ord)
min
fn min[T: Ord](xs: List[T]) -> Option[T]
the first least element; None when empty (elements need Ord)
max_by
fn max_by[T, K: Ord, !e](xs: List[T], key: fn(T) -> K !e) -> Option[T] !e
the first element whose key is greatest; None when empty (keys need Ord)
min_by
fn min_by[T, K: Ord, !e](xs: List[T], key: fn(T) -> K !e) -> Option[T] !e
the first element whose key is least; None when empty (keys need Ord)
string
to_string
fn to_string[T: Show](x: T) -> String
render any printable value (numbers, strings, derive Show data)
parse_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
option
expect
fn expect[T](o: Option[T], msg: String, at: Loc = caller()) -> T
unwrap a Some or panic with a message
conversion
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)
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
预置 trait#
随 prelude 而来的五个 trait。它们的方法名同样进入函数命名空间,可被本模块的顶层声明遮蔽(规范 §10.6)。
Ord
trait Ord[T] {
fn cmp[T: Ord](a: T, b: T) -> Int
}
total order. cmp(a, b) answers negative / zero / positive, and is what < <= > >= mean past the scalars; sort and the extremes ask for it
Hash
trait Hash[T] {
fn hash[T: Hash](x: T) -> Int
}
a value's hash, asked for alongside Eq by Map keys and Set elements ([K: Eq + Hash])
Show
trait Show[T] {
fn show[T: Show](x: T) -> String
}
how to_string and string interpolation render a value
Iter
trait Iter[T] {
type Cur
type Item
fn iter_start[T: Iter](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
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
模块#
以 use std/x 引入,再限定调用;也可以选择性引入单个名字(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).
本模块登记的 impl(全程序可见,不必 use):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.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.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).
本模块登记的 impl(全程序可见,不必 use):Display[Char]、Show[Char]
char.of
fn of(n: Int) -> Option[Char]
The character with code point n, or None when n is not one.
Option rather than a panic because "is this integer a character" is a
question with a legitimate negative answer -- it is exactly what a parser
reading a \u escape needs to ask (spec §4.8, criterion 2). The bulk
conversion from_code_points is not asking, and keeps its panic.
Not a Unicode scalar value: negative, above U+10FFFF, or in the surrogate
range D800..DFFF. Those last are halves of a UTF-16 pair and never stand
for a character on their own, which is the whole reason Cursor exists.
std/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).
本模块登记的 impl(全程序可见,不必 use):Show[Cursor]、Ord[Cursor]
cursor.next
fn next(s: String, c: Cursor) -> Cursor
The position one character forward (clamped to the end).
cursor.prev
fn prev(s: String, c: Cursor) -> Cursor
The position one character back (clamped to the start).
cursor.slice
fn slice(s: String, from: Cursor, to: Cursor) -> String
The text between two positions.
cursor.skip
fn skip(s: String, c: Cursor, sub: String) -> Cursor
The position just past sub, which is known to start at c — the one
sanctioned "advance past a literal" that would otherwise be cursor arithmetic.
Walks both strings together rather than counting sub first, so it needs no
length and no arithmetic.
cursor.starts_at
fn starts_at(s: String, c: Cursor, sub: String) -> Bool
Whether sub occurs in s starting exactly at c. An empty sub is at
every position, including the end.
cursor.find
fn find(s: String, sub: String, from: Cursor = 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.
本模块登记的 impl(全程序可见,不必 use):HasDtype[Float]、HasDtype[Int]、HasDtype[Bool]、DeviceBits[Float]、DeviceBits[Int]
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.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.
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.
本模块登记的 impl(全程序可见,不必 use):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.
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
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_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.
参考 kernel:2 个宿主端参考答案,供 GPU 后端的测试对照
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.
本模块登记的 impl(全程序可见,不必 use):HasDtype[I16]、DeviceBits[I16]、FromInt[I16]、Show[I16]、Add[I16]、Sub[I16]、Mul[I16]、Div[I16]、Rem[I16]、Neg[I16]
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.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 >>).
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.
本模块登记的 impl(全程序可见,不必 use):HasDtype[I32]、DeviceBits[I32]、FromInt[I32]、Show[I32]、Add[I32]、Sub[I32]、Mul[I32]、Div[I32]、Rem[I32]、Neg[I32]
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.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 >>).
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.
本模块登记的 impl(全程序可见,不必 use):HasDtype[I8]、DeviceBits[I8]、FromInt[I8]、Show[I8]、Add[I8]、Sub[I8]、Mul[I8]、Div[I8]、Rem[I8]、Neg[I8]
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.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 >>).
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.
本模块登记的 impl(全程序可见,不必 use):FromInt[U16]、Show[U16]、Add[U16]、Sub[U16]、Mul[U16]、Div[U16]、Rem[U16]、Neg[U16]
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.
本模块登记的 impl(全程序可见,不必 use):FromInt[U32]、Show[U32]、Add[U32]、Sub[U32]、Mul[U32]、Div[U32]、Rem[U32]、Neg[U32]
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.
本模块登记的 impl(全程序可见,不必 use):FromInt[U64]、Show[U64]、Ord[U64]、Add[U64]、Sub[U64]、Mul[U64]、Div[U64]、Rem[U64]、Neg[U64]
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.to_int
fn to_int(x: U64) -> Int
The bit pattern of x as an Int: a value above 2^63 - 1 is negative.
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.
本模块登记的 impl(全程序可见,不必 use):HasDtype[U8]、DeviceBits[U8]、FromInt[U8]、Show[U8]、Add[U8]、Sub[U8]、Mul[U8]、Div[U8]、Rem[U8]、Neg[U8]
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.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.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.is_symlink
fn is_symlink(path: String) -> Bool !Fs
Whether path is a symbolic link. The link itself is examined, not its
target, so a link to a directory is a link and not a directory. Invalid host
path spellings, including an embedded NUL, answer false.
io.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
fsyncof the file and none of the directory. The guarantee is about what a reader can observe on a running system, which is what a manifest rewrite needs; surviving a crash is a different (and much more expensive) property. - Anything about concurrent writers. Two of these racing both succeed
and the last
renamewins. Detecting that someone else changed the file since it was read is a compare-and-set, and that is a different primitive with a different signature. - Link identity. A destination with other hard links to it is detached: this directory entry gets the new file and the other links keep the old one. Writing through to the shared inode is the opposite of what is wanted here -- it is exactly the non-atomic overwrite this replaces.
A destination that is a symbolic link is refused rather than followed
(io.atomic_write_target_is_symlink). Following would let whatever placed
the link choose which file gets written, and a caller publishing a file it
named is not asking to write wherever that name currently points.
The four failure kinds minted here are io.invalid_atomic_write_path,
io.atomic_write_target_is_symlink, io.atomic_write_staging_failed and
io.atomic_write_verify_failed; every other Err carries whatever the
backend called the failure, as everywhere else in this module.
io.atomic_write_staging_failed is the one place that rewrites a backend
message instead of passing it on, and it earns the exception: the failure is
about a randomly named file this function invented, so passing it on would
name that file to a caller who never asked for it. The backend's own words
are kept in cause.
io.read_stdin
fn read_stdin(n: Int) -> Bytes !io
Exactly n bytes of standard input, shorter only at end of input -- so a
short read is how end of input is reported.
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.
本模块登记的 impl(全程序可见,不必 use):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.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).
本模块登记的 impl(全程序可见,不必 use):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.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).
本模块登记的 impl(全程序可见,不必 use):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.is_empty
fn is_empty[K, V](m: Map[K, V]) -> Bool
Whether there are no entries. Cheaper to read than len(m) == 0 and, since
the trie keeps its own count, no cheaper to compute -- the point is that one
question has one spelling across every container (CONTRIBUTING §naming).
map.entries
fn entries[K, V](m: Map[K, V]) -> List[(K, V)]
The (key, value) pairs, in insertion order.
map.fold
fn fold[K, V, A, !e](m: Map[K, V], init: A, f: fn(A, K, V) -> A !e) -> A !e
Every entry combined into one value: f(acc, k, v), starting from init.
This walks the trie itself -- no entry list, no pair per entry -- which is
why it exists: folding entries(m) first builds and sorts the list this
avoids. The price is the order: f sees each entry exactly once but in
no promised order (the trie's own, which is not insertion order). A
fold whose answer depends on the order it met the entries in belongs on
entries(m). The key and value arrive as two arguments rather than a
pair, so the walk allocates nothing per entry; the row rides through f
the way list.fold's does. Unbounded like the other readers: visiting
entries hashes and compares no keys.
std/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).
本模块登记的 impl(全程序可见,不必 use):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.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.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.link
fn link(st: MemFs, at: String, target: String) -> MemFs
st with a symbolic link at at pointing to target; the directories
above at come along. The target is kept as spelled and is not required to
exist, as ln -s does not require it: a relative one is read from the
directory at sits in when something resolves through the link. Only
fs_is_symlink and fs_real_path see the link (the note at the top).
A file or a directory already at at 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.
本模块登记的 impl(全程序可见,不必 use):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]
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.
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.F8E4M3FN
const F8E4M3FN: Dtype[F8E4M3FN]
The f8E4M3FN device format, the witness of F8E4M3FN.
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_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_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).
本模块登记的 impl(全程序可见,不必 use):Len[Set[T]]、Eq[Set[T]]、Hash[Set[T]]、Show[Set[T]]、Iter[Set[T]]
set.from
fn from[T: Eq + Hash](xs: List[T]) -> Set[T]
A set from a list; duplicates collapse, first occurrence keeps its position.
set.insert
fn insert[T: Eq + Hash](s: Set[T], x: T) -> Set[T]
A copy with x added; an existing element keeps its insertion position.
set.remove
fn remove[T: Eq + Hash](s: Set[T], x: T) -> Set[T]
A copy without x; absent elements are fine.
set.is_empty
fn is_empty[T](s: Set[T]) -> Bool
Whether there are no elements. The same question str.is_empty answers, in
the same words (CONTRIBUTING §naming).
std/str
String operations, over code points: trimming, case, search, slicing, splitting and padding (spec §11).
本模块登记的 impl(全程序可见,不必 use):Len[String]、Iter[String]
str.to_lower
fn to_lower(s: String) -> String
s lowercased by simple (1:1) Unicode mapping: one code point in, one out,
no locale and no context, so the code-point count is unchanged.
str.to_upper
fn to_upper(s: String) -> String
s uppercased by simple (1:1) Unicode mapping. Not the length-changing full
mapping -- ß stays ß rather than becoming SS, because full mapping is
locale- and context-sensitive and so is not a function a second backend can
implement from a table (spec 11). That belongs in a library taking a locale.
str.ends_with
fn ends_with(s: String, suffix: String) -> Bool
Whether s ends with suffix.
O(len(suffix)), not O(len(s)). The walk back from the end was always the
short one; the cost was the guard in front of it, which asked for len(s)
and so read the whole string to answer a question about its last few
characters. The guard was redundant: back clamps to the start when
suffix is longer than s (criterion 3), and starts_at then runs out of
s before it runs out of suffix and answers false on its own.
str.strip_prefix
fn strip_prefix(s: String, prefix: String) -> Option[String]
s with prefix removed, or None when s does not begin with it.
Inquiry (spec §4.8, criterion 2): "no such prefix" is a branch the caller
takes, not a bug. Testing and peeling in one call is the point -- the pair
starts_with plus a hand-computed slice(s, 16, len(s)) is what put
magic offsets in 33 call sites, and an offset that is one out still compiles.
No arithmetic happens here either: cursor.skip walks past the prefix.
str.strip_suffix
fn strip_suffix(s: String, suffix: String) -> Option[String]
s with suffix removed, or None when s does not end with it.
The mirror of strip_prefix, and the answer to - 5 meaning len(".dawn").
str.index_of
fn index_of(s: String, sub: String) -> Option[Int]
Where the first occurrence of sub begins, in code points, or None.
The answer is a position in [0, len], not the index of an element: an
empty sub occurs at position 0 of the empty string, and len(s) is a
legal answer here (see last_index_of) though no character lives there.
"Index" in the name is the unit it counts in -- code points, not UTF-16
units and not bytes -- and cursor.find is the one that answers with a
position you can slice at directly.
str.last_index_of
fn last_index_of(s: String, sub: String) -> Option[Int]
Where the last occurrence of sub begins, in code points, or None. A
position in [0, len]: an empty sub is found at the end, which is
len(s) -- past the last character, which is why the answer is a position
and not an element index.
str.from_char
fn from_char(c: Char) -> String
The one-character string for c, asked for by name.
"${c}" is the same string, through std/char's impl Display[Char]
(spec §4.3). This one does not depend on an impl being in scope, and it is
the spelling to reach for where a Char is being converted rather than
rendered -- which is what nearly every caller in this repository is doing.
A Char nested inside a structure still renders as its code point, because
that is Show, and Show[Char] is still the Int's (spec §2.7).
str.repeat
fn repeat(s: String, n: Int) -> String
s repeated n times; n <= 0 yields the empty string.
Doubling: O(len(s) * n) on both backends, and stack-safe at any n. Why the
two obvious shapes are not, in docs/stdlib-impl-notes.md.
str.slice
fn slice(s: String, from: Int, to: Int) -> String
The code points of s in [from, to). Both ends are clamped into range
(a negative index reads as 0, one past the end as len) and from > to
yields the empty string, so this never panics — a range parameter selects
a stretch, it does not assert its endpoints exist (spec §4.8).
Spelled substring until v0.54.0; rationale in docs/stdlib-naming.md.
O(to), not O(len(s)). It walked the whole string until 2026-08-17, three
times over: len(s) to clamp with, code_points(s) to materialise every
character of it as a List[Char], and a rebuild. So slice(s, 0, 1) cost
and allocated in proportion to a string it was asked for one character of.
at went through cursor.seek and this did not; positions are what the
cursor layer is for, and it clamps on its own (criterion 3), which is why no
length is needed to clamp against.
str.take
fn take(s: String, n: Int) -> String
The first n characters, or all of them when there are fewer. n is
clamped into [0, len], so this never panics: it names a stretch rather
than asserting that position n exists (spec §4.8, criterion 3), the same
way slice and list.take do.
truncate would be this function under another name, so there is no
truncate.
str.drop
fn drop(s: String, n: Int) -> String
Everything after the first n characters; n is clamped into [0, len],
so this never panics. This is the "the rest of it" that 50 call sites spell
slice(x, i, 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.