Lowent Manual←↑→

32 A map of the standard library

What to know first

chapter 21, Modules · standard library names are brought in with use <name> .
chapter 16, Capabilities · capabilities are handed over, not picked up
chapter 17, Designing failure · at the boundary, failure is returned as a value

Looking back

In chapter 21, does use look for a file name or a module name? Why did it say the difference matters in the standard library?

A. The module declaration inside the file. In the standard library a few file names differ from module names (lib/alloc.low is allocs, lib/str.low is strings), so trying to import by file name leads astray. This chapter’s first table is that list.

The need for this chapter, and its context

Part IX tours the standard library. Detailed descriptions of each module live in Appendix E, one page each, and this part does not repeat them. Instead it sets out which module sits on which layer, which conventions they all follow, and where to look for a given job. This first chapter is the map — the boundary between language and library, the criteria for entering the library, and the layers and shared conventions of modules.

By the end of this chapter

You will learn the question that separates the three layers of language, leaf and library. You will pick up the criteria (the charter) and maturity ladder for entering the standard library, and a map dividing modules into a layer that does not allocate, a layer needing storage and a layer needing capabilities. You will also see the shared conventions — the caller’s buffer, failure returned as a value, all or nothing, ownership that must be settled — and the places where file names and module names differ.

The questions this chapter answers

  1. Why leave as a library a feature that would be convenient in the language?

32.1 Three layers — language, leaf, library#

LayerWho makes itQualification
LanguageThe specificationCannot be expressed as a library, or expressing it would hide a cost
LeafThe processorReaches the operating system or hardware. Cannot be written in Lowent
LibraryAnyoneEverything else. Written in Lowent

Table 32.1 — Three layers

There is one question deciding whether a piece is a leaf — “can this be written in Lowent?” If it can, it is library. Opening a file and receiving an integer handle reaches the kernel, so it is a leaf. Wrapping that handle in an owned value that cannot be forgotten can be written in Lowent, so it is library. A convenience op that reads a whole file is also library. Convenience does not qualify something as a leaf.

The standard library has no privileges. It gets the same rules as code the author writes — contracts, effects, capabilities. Here is the source of strings.starts_with.

export fn starts_with input s str . input prefix str . output bool . do
  guard le (len prefix) (len s) . else return false .
  return eq_str (subslice s 0 (len prefix)) prefix .
end

It reads with only what this book has taught. If there were a hidden passage only the library could use, a program’s guarantees would break the moment it passed through.

Q. Why leave as a library a feature that would be convenient in the language?

A. Adding a word or builtin to the language means every program must learn it and the processor must treat it specially. As a library it is checked by the same rules, and programs that do not use it do not have it. So the language accepts only what cannot be expressed. The standard library’s allocators, containers and file handles all passed this criterion and live in the library. Convenience is not a reason to enter the language.

32.2 What gets in#

Things enter the library not because they exist but on evidence. The specification’s charter sets six measures.

MeasureMeaning
If it can be written in this language, write it in this languageLeaves only when using outside authority or when this language cannot express it
The signature is the ledger of cost and authorityEffects, capabilities, storage, allocation, ownership and completion are visible at the boundary
Separate computation from authorityFormatting vs. emitting, parsing vs. opening files, the next step of a random generator vs. getting entropy are different
Place along several axesAuthority, storage, execution tier and profile are not squeezed into one ladder
Keep what is inside smallUp to a portable base and a thin host adapter
Every module has evidenceUnit tests, rejection tests, cross-checks and failure tests

Table 32.2 — The library charter

Every module writes its maturity — experimental (merely written), incubating (has a decision document), standard (documented and named by regression tests) and deprecated (says what to move to). The value of the ladder is in the lower rungs, not the upper. Writing experimental writes “do not trust this yet” in the source. A rung is not a ranking of quality but the size of a promise.

The specification does not list the library’s contents. What exists is authoritatively recounted from the source each time, and a document copying it diverges from the moment it copies. Three APIs that did not actually exist were once read as fact because they were in a table. The tables in this part were also made by looking at this edition’s source, and detailed per-module pages are in Appendix E.

32.3 The map by layer#

LayerCharacterRepresentative modules
L0 pure computationeffects none · the caller’s bufferstrings strbuf fmt utf8 utf16 unicode codec regex hash math random sortlib sortgen searchlib hashmap strmap spsc term · cryptographic modules
L1 storageNeeds the allocation capability or borrowed bytesallocs pool shard budget segarena pagecache growvec vecgen mapgen nodelist segview wire flags lifemode
L2 hostNeeds capabilities (cap io, file_system, tty, net, clock)io outbuf files tty net clock

Table 32.3 — Layers of the standard library

The remaining chapters of this part group by use: text and encodings (chapter 33), containers and sorting (chapter 34), storage and handles (chapter 35), input/output, networking and cryptography (chapter 36), and the terminal (chapter 37).

32.4 File names and module names#

What use looks for is the module declaration inside the file. In a few places it differs from the file name.

Source fileModule name
lib/alloc.lowallocs
lib/str.lowstrings
lib/vec.lowvecs
lib/sort.lowsortlib
lib/search.lowsearchlib
lib/out.lowoutbuf
lib/file.lowfiles

Table 32.4 — Module names that differ from file names

examples/ch32/names.low

module names .
rem run: is_get [71,69,84,32,47]
rem run: is_get [80,79,83,84]

use strings .

fn is_get input line slice u8 . output bool .
do
  return strings.starts_with line "GET " .
end

Output

$ lowentc --run is_get names.low [71,69,84,32,47]
is_get([71,69,84,32,47]) = 1
  arg0 (written) = [71,69,84,32,47]
$ lowentc --run is_get names.low [80,79,83,84]
is_get([80,79,83,84]) = 0
  arg0 (written) = [80,79,83,84]

use strings . resolves from the standard module location without from. Imported names are always called qualified by module, as in strings.starts_with. There is no glob import. Importing by file name finds no such module.

examples/ch32/wrong_name.low

module wrong_name .
rem expect: W-USE-EXTERNAL

use str .

fn is_get input line slice u8 . output bool .
do
  return true .
end

Output

$ lowentc --check wrong_name.low
wrong_name.low:4:0 W-USE-EXTERNAL: this module is not in the compilation unit — there is no module search path, so nothing here can confirm it exists. Pass the file that declares it and it WILL be checked (several .low files link into one unit)

The diagnostic is a warning rather than an error because there is no search path, so looking at this translation unit alone cannot tell whether the name exists somewhere (chapter 21). The moment the name is actually used, it is rejected.

32.5 Conventions every module follows#

ConventionMeaning
Buffers belong to the callerL0 modules do not allocate. They take output buffers and workspace as parameters. So reentrancy is free and the VM/native cross-check runs as is
Failure is a valueoption (could not) or result (what failed). No traps, no silent truncation, no replacement characters
All or nothingIf there is not enough room, not a single byte is written. A half-written buffer is silently wrong output
Ownership must be settledThings needing completion (outbuf’s remaining bytes, files’ handles) are owned values, so forgetting them is rejected at translation

Table 32.5 — Shared conventions of the standard library

A common misconception. It is the standard library, so every module can be trusted equally

Maturity differs per module, and the top of each module document states what it does not do. For example, tlssrv performs the whole handshake but not yet transport, and der is a minimal parser extracting only public keys, not a certificate infrastructure (PKI). Reading the boundaries a module writes for itself before importing it is how this library is meant to be used.

32.6 The list of leaf ops — what lies beneath the modules#

Library modules ultimately call leaf builtin ops provided by the processor. Normally you use the module and do not call the leaves directly — the module dresses them in conventions such as ownership, three-place answers and all-or-nothing. Still, knowing which module stands on what keeps you oriented when reading module documentation or building a new module.

Leaf opWhat it doesWrapping module · capability
file_open · file_read · file_write · file_seek · file_closeopen, read, write, move the position of, and close a filefiles · cap file_system
dir_make · dir_read · dir_closemake a directory, read its entries, close itfiles · cap file_system
path_remove · path_rename · link_typeremove or rename a path, and ask what the path itself is without following symlinksfiles · cap file_system
net_listen · net_accept · net_connect · net_portlistening socket · accept · connect to an address and port · ask the portnet · cap net
net_resolvea name to an IPv4 address — DNS. A separate leaf from connecting, so a program that already has an address never touches name resolutionnet · cap net
net_send · net_recv · net_close · net_pairsend · receive · close · a connected pairnet · cap net
reactor_new · r_read · r_writemake a reactor · read through a reactor · write through a reactorno module — cap io
env_getread an environment variableno module — cap env
rng_nextnext random number from a state (splitmix64)random — no capability (pure)
hash_bytes · crc32FNV-1a 64 hash · CRC-32 checksumhash — no capability
sha256 · sha384 · sha512SHA-256 · SHA-384 · SHA-512 — SHA-384 is SHA-512 with a different start value, keeping the first 48 byteshash·hmac·tls13 · ed25519 — no capability
aes_ctr · ghashAES-128-CTR keystream · GHASH accumulation — key, counter and accumulator are all 16 bytes. The counter and the accumulator are updated in place, so successive calls continue where the last one stoppedgcm — no capability
chacha_polyone round of ChaCha20-Poly1305 — keystream and accumulation in one pass. Its meaning is exactly chacha20 followed by poly1305 over the ciphertext it just made. The two computations use different execution resources (vector vs the integer multiplier), so interleaving overlaps them (measured 1,080 → 1,199 MB/s)aead — no capability
aes_gcmone round of AES-128-GCM — keystream and accumulation in one pass. Its meaning is exactly aes_ctr followed by ghash over the ciphertext it just made; only the speed differs (measured 2848 → 3661 MB/s). Sealing only — opening must attest the tag before it unseals, so it stays two passesgcm — no capability
chacha20 · poly1305ChaCha20 keystream · Poly1305 accumulation — key 32, counter block 16 (its first four bytes are the block number, little-endian), and the counter moves in place. poly1305 accumulates into the state (five limbs of h, five of r) and zero-pads a tail that is not a multiple of sixteen. The width is chosen by the build (lowentc --hw sse2 · avx2)chacha · poly · aead — no capability
aes_round · aes_round_lastone AES round — the 16-byte state in place. When the machine has the instruction the processor lowers to it (lowentc --hw aes); when it does not, to the same answer computed from tablesaes — no capability
str_from_cstrscan a NUL-terminated C string into a strC boundary — the VM says it cannot, with E-VM-CSTR

Table 32.6 — Leaf builtin ops and the modules that wrap them

Computation leaves stand only after call_builtin — call_builtin sha256 msg out. The name lives in that position and never becomes a global word: a word a program uses once should not cost every reader a name to remember. The stage names inside pipe and the type slot of cast u8 x already work this way. Leaves that touch the operating system (file_open, net_send, …) are called plainly — their first operand is a capability, so their specialness already shows.

The qualification for a leaf is one question: “can it be written in Lowent?” rng_next and the three hashes are pure computation yet leaves, because the processor fixes their algorithms so that the VM and the native build give bit-identical answers. Every leaf that needs a capability takes it as the first operand (chapter 16).

32.7 Common mistakes#

Counter-example. Chaining writes that return option without asking

examples/ch32/mistake_chainnone.low

module mistake_chainnone .
rem run: greet [0,0,0,0,0,0,0,0,0,0,0,0]
rem trap: greet [0,0,0,0,0,0]

use fmt .

proc greet input buf mut slice u8 . output u64 . effects none .
do
  rem ✘ trusts that the previous write succeeded and chains on with `some_value`
  let p u64 be some_value (fmt.put_str buf 0 "hello") .
  let q u64 be some_value (fmt.put_byte buf p 32) .
  return some_value (fmt.put_u64 buf q 42) .
end

Output

$ lowentc --run greet mistake_chainnone.low [0,0,0,0,0,0,0,0,0,0,0,0]
greet([104,101,108,108,111,32,52,50,0,0,0,0]) = 8
  arg0 (written) = [104,101,108,108,111,32,52,50,0,0,0,0]
$ lowentc --run greet mistake_chainnone.low [0,0,0,0,0,0]
== ir diagnostics (1) ==
0:0 E-VM-NONE: some_value of none (panic)

The write ops of fmt return the next position as an option u64. With a twelve-slot buffer all three succeed and return 8, but with six slots put_u64 runs out of room, returns none, and some_value stops. Failure in L0 modules is a value, not a trap, so the receiver asks at every step.

examples/ch32/chainnone_fixed.low

module chainnone_fixed .
rem run: greet [0,0,0,0,0,0,0,0,0,0,0,0]
rem run: greet [0,0,0,0,0,0]

use fmt .

proc greet input buf mut slice u8 . output u64 . effects none .
do
  rem ask at every step --- when short, return 0 to report "could not write"
  let p option u64 be fmt.put_str buf 0 "hello" .
  guard is_some p . else return 0 .
  let q option u64 be fmt.put_byte buf (some_value p) 32 .
  guard is_some q . else return 0 .
  let r option u64 be fmt.put_u64 buf (some_value q) 42 .
  guard is_some r . else return 0 .
  return some_value r .
end

Output

$ lowentc --run greet chainnone_fixed.low [0,0,0,0,0,0,0,0,0,0,0,0]
greet([104,101,108,108,111,32,52,50,0,0,0,0]) = 8
  arg0 (written) = [104,101,108,108,111,32,52,50,0,0,0,0]
$ lowentc --run greet chainnone_fixed.low [0,0,0,0,0,0]
greet([104,101,108,108,111,32]) = 0
  arg0 (written) = [104,101,108,108,111,32]

The fixed version returns 0 with the six-slot buffer. Notice that hello remains in the buffer. “All or nothing” is the promise of a single op call, not of a whole sequence of calls. If the whole sequence must be undone, treat only the positions that succeeded (p, q) as the valid length.

A common misconception. When the buffer is short, as much as fits is written

examples/ch32/all_or_nothing.low

module all_or_nothing .
rem run: try_put [0,0,0,0]

use fmt .

proc try_put input buf mut slice u8 . output u64 . effects none .
do
  rem six bytes into four slots --- instead of writing the first four, it writes none
  let p option u64 be fmt.put_str buf 0 "Lowent" .
  guard is_some p . else return 99 .
  return some_value p .
end

Output

$ lowentc --run try_put all_or_nothing.low [0,0,0,0]
try_put([0,0,0,0]) = 99
  arg0 (written) = [0,0,0,0]

C’s snprintf writes what fits and truncates. The L0 modules of the standard library are all or nothing. Asked to write six bytes into four slots, it writes no byte at all and returns none, leaving the buffer as [0,0,0,0]. A half-written buffer becomes silently wrong output, and the caller can rely on “none means the buffer is untouched” to continue down another path.

32.8 This chapter’s syntax at a glance#

ShapeMeaningWhy
use strings .import a standard module — the name is the file’s module declarationthe module in lib/str.low is strings
strings.starts_with line "GET "imported names are qualified by moduleno glob imports
fmt.put_str buf pos s → option u64write into the caller’s buffer and return the next positionL0 never allocates — reentrancy is free
none (not enough room)not a single byte was writtenall or nothing — the promise of one call
result t ea failure that says what failedno traps, no silent truncation
owned handles · pending bytesownership that must be repaidforgetting it is rejected at translation
experimental · incubating · standard · deprecatedmaturity each module declaresa rung is the size of a promise, not a ranking

Table 32.7 — Shapes for using the standard library — shape · meaning · why it looks this way

Recap

The language takes only what cannot be expressed, leaves only thin pieces touching the outside, and the rest is library under the same rules. Libraries enter on the charter’s evidence and write their maturity in the source. Modules divide into pure computation (L0), storage (L1) and host (L2) layers. Module names may differ from file names. Every module follows the caller’s buffer, failure as a value, all or nothing, and ownership that must be settled.