Ashes Standard Library
This document summarizes the current compiler-provided standard-library surface. It is user-facing guidance for the shipped modules; the authoritative language semantics remain in the Language Reference.
Built-in Runtime Types
These types are always available without imports:
UnitMaybe(T)with constructorsNoneandSome(T)Result(E, A)with constructorsError(E)andOk(A)List<T>as the type shown for list syntax and list valuesSocketTlsSocketProcessFileHandle
Module Overview
The standard library is organized under ten top-level namespaces: Ashes.IO (console, file, and process I/O), Ashes.Net (networking and wire protocols), Ashes.Number (numeric helpers), Ashes.Collection (containers), Ashes.Text (strings and text formats), Ashes.Byte (binary data), Ashes.Task (concurrency), Ashes.Core (core value helpers), Ashes.Test (assertions), and the internal-only Ashes.Internal. Some modules are compiler intrinsics, some are shipped Ashes code from lib/Ashes/, and some are both; that distinction is an implementation detail — imports work identically for all of them.
Ashes.IO — console, file, and process I/O
Ashes.IO
print(value)returningUnit— write a printable scalar (Int,Str, orBool) to stdout, followed by a newlinepanic(message)returninga— printmessageand abort the program; never returns, so it is usable at any typeargsreturningList(Str)— the command-line arguments passed to the programwrite(text)returningUnit— writetextto stdout with no trailing newlinewriteBytes(bytes)returningUnit— write a rawBytesbuffer to stdout verbatim, with no trailing newline and no UTF-8 constraint (unlikewrite, which takes aStr). Use this for binary output such as a packed image or any non-text byte streamwriteLine(text)returningUnit— writetextto stdout followed by a newlinereadLine()returningMaybe(Str)readExact(n)returningResult(Str, Str)— read exactlynbytes from stdin
Ashes.IO.Console
Interactive terminal input for real-time programs (games, TUIs): raw keyboard/mouse byte streams and a monotonic clock for frame pacing. Rendering needs no dedicated support — ANSI escape output through Ashes.IO.write already works; this module is about input, which is line-buffered and blocking without it.
enableRawInput()returningBool— switch stdin to raw mode: no line buffering, no echo, no signal keys (press-by-press delivery;Ctrl-Carrives as byte0x03for the program to handle). On Linux this saves and rewrites the termios state; on Windows it saves and rewrites the console input mode and enables VT input sequences (arrow keys and mouse arrive as the same escape bytes as on Linux), and enables VT processing on stdout so ANSI rendering works in the classic console host. Returnsfalse(and changes nothing) when stdin is not a terminal — for example when input is piped, as in tests and CI — so programs can fall back or exit cleanly.restoreInput()returningUnit— restore the mode saved byenableRawInput. A no-op if raw mode was never enabled. Programs must call this before exiting; the runtime does not restore the terminal on its own.pollInput(timeoutMs)returningMaybe(Str)— wait up totimeoutMsmilliseconds for input and return whatever bytes are pending:Some(bytes)when input arrived (possibly several keys, or a partial escape sequence — accumulate and decode in the program),Some("")when the timeout elapsed with no input,Nonewhen stdin is closed (end of input on a pipe). AtimeoutMsof0is a non-blocking check. In raw mode each keypress arrives immediately; arrow keys arrive asESC [ A…ESC [ D, and mouse reporting (opted into by writing the standardESC [ ? 1003 h/ESC [ ? 1006 hsequences) arrives as SGRESC [ < b ; x ; y M/msequences. Decoding is ordinary string processing. On Windows, when stdin is a pipe rather than a console, the timeout is not honored: the call reads directly and blocks until bytes or end of input arrive (pipes are not waitable objects; on Linuxppollcovers pipes and ttys alike).monotonicMillis()returningInt— milliseconds on a monotonic clock (unaffected by wall clock changes;CLOCK_MONOTONICon Linux,GetTickCount64on Windows). The zero point is arbitrary; use differences for frame pacing and timeouts.
Supported on Linux x64, Linux arm64, and Windows x64.
Ashes.IO.File
readText(path)returningResult(Str, Str)— UTF-8-validated; caps at 1 MiB.readAllBytes(path)returningResult(Str, Bytes)— read a whole file into aByteswith no UTF-8 validation. Uncapped on Linux (the buffer is a standalonemmap, so it can exceed one managed chunk); on Windows it currently shares thereadTextbuffer and so caps at the same 1 MiB. The buffer is read-only and program-lifetime;Bytes.subTextmaterializes an independent managed String.mmap(path)returningResult(Str, Bytes)— memory-map a file read-only and return a zero-copyBytesview over the mapping (no read, no copy). On Linux the pages fault in on access, so a data-parallel fold that touches different chunks faults them in in parallel, and the mapping is shared read-only across worker threads. The mapping is program-lifetime, so slices/views into it stay valid. On Windows this falls back to the cappedreadAllBytesread. Preferred overreadAllBytesfor random-access / chunked processing (e.g.challenges/1brc/brc.ash).writeText(path, text)returningResult(Str, Unit)writeBytes(path, bytes)returningResult(Str, Unit)exists(path)returningResult(Str, Bool)open(path)returningResult(Str, FileHandle)— open a file for reading; the handle is a resource type, automatically closed when it goes out of scope.readChunk(fh)(maxBytes)returningResult(Str, Str)— read up tomaxBytesbytes; returnsOk("")at end of file. Lets a large file be streamed without loading it whole (cf.readText, which allocates the entire file at once).readLine(fh)returningMaybe(Str)— read one line (the trailing\n, and a preceding\r, are stripped) through a refillable module-global buffer; returnsNoneat end of file. UnlikereadChunkit threads no buffer state through the caller, so a whole-file fold can be a single loop carrying only its accumulator — which is what keeps such a fold constant-memory (a per-chunk re-entry structure re-copies a reuse accumulator each chunk). The buffer is guarded by the handle it holds: callingreadLineon a different handle resets it (any read-ahead for the previous handle is discarded), so it is for reading one file to completion, not interleaving line-reads across handles.close(fh)returningResult(Str, Unit)— close explicitly (also automatic on scope exit).
Ashes.IO.Process
Synchronous subprocess control with piped stdin/stdout/stderr. Process is a resource type; it is automatically closed when it goes out of scope.
spawn(exe)(args)returningResult(Str, Process)— launchexewith argument listargswriteStdin(proc)(text)returningUnit— write bytes to the process's stdin pipereadStdoutLine(proc)returningMaybe(Str)— read one line from stdout (Noneon EOF)readStderrLine(proc)returningMaybe(Str)— read one line from stderr (Noneon EOF)waitForExit(proc)returningInt— block until the process exits, return its exit codekill(proc)returningUnit— send SIGTERM (Linux) orTerminateProcess(Windows)
Supported on Linux x64, Linux arm64, and Windows x64.
Ashes.Net — networking and wire protocols
Ashes.Net.Http
get(url)returningTask(Str, Str)post(url, body)returningTask(Str, Str)
All networking APIs return Task(...) and are consumed with await.
Current HTTP support is intentionally small:
http://andhttps://URLs are supported.https://defaults to port 443 and currently ships on the Linux x64, Linux arm64, and Windows x64 backends via the hermetic Mbed TLS runtime linked into TLS-using executables.- Other backends may still return a runtime error for
https://until their TLS runtime support lands. - Responses are expected to be plain HTTP/1.1 responses terminated by connection close.
Transfer-Encoding: chunkedcurrently returnsError("unsupported transfer encoding").
Ashes.Net.Http.Server
A minimal HTTP/1.1 server layered over Ashes.Net.Tcp.Server. Pure Ashes over the TCP layer, so it runs on every target the TCP server does (Linux x64, Linux arm64, Windows x64).
HttpRequest— a parsed request (method, path, headers, body).HttpResponse— a response (status, headers, body).method(req)—HttpRequest -> Str, the request method (e.g."GET").target(req)—HttpRequest -> Str, the raw request target ("/users?id=42", path plus query).path(req)—HttpRequest -> Str, the path with any?querystripped (for routing).query(req)—HttpRequest -> Str, the raw query string (after?), or"".queryParam(req)(name)—HttpRequest -> Str -> Maybe(Str), a query parameter's percent-decoded value looked up by (decoded) name; a bare key yieldsSome(""), absent yieldsNone.percentDecode(s)—Str -> Str, decode%XXand+(byte-accurate); also usable on the path.body(req)—HttpRequest -> Str, the request body (Content-Length- or chunked-framed).header(req)(name)—HttpRequest -> Str -> Maybe(Str), the value of a request header, matched case-insensitively by name, orNone.rawHeaders(req)—HttpRequest -> Str, the rawName: value-per-line header block, for callers that want to scan it directly.text(status)(body)— a response withContent-Type: text/plain; charset=utf-8.json(status)(body)— a response withContent-Type: application/json.respond(status)(headerBlock)(body)— a response with an explicit header block ("Name: value\r\n"per line, or""for none).withHeader(name)(value)(response)— add a response header.Content-LengthandConnectionare always set by the server and must not be added here.streamed(status)(headerBlock)(seed)(step)— a streaming response. The body is produced incrementally:step : Str -> Task(E, StreamStep)is pulled starting fromseed, and eachStreamChunk(bytes, nextAcc)is written as oneTransfer-Encoding: chunkedframe (the producer runs async, so a chunk may come from a file, a socket, or a computation);StreamDoneends the body. The response carriesTransfer-Encoding: chunkedinstead ofContent-Length.StreamStepis| StreamChunk(Str, Str) | StreamDone.serve(port)(handler)—Int -> (HttpRequest -> Task(E, HttpResponse)) -> Task(Str, Unit). Binds the port and, per request, reads it, parses request line + headers + body, runs the handler (which mayawaitasync work), and writes the response. The connection is kept alive (HTTP/1.1 default), closing onConnection: close, on handler failure, or when the peer disconnects. A handler that completes withErroryields a plain500. Consumed withAshes.Task.run; serves connections concurrently like the plaintext TCP server.
Reads are buffered until a full request has arrived — the header block plus the body (framed by Content-Length or Transfer-Encoding: chunked) — so requests larger than one read and slow/split requests are handled. A request is capped at 8 MiB: a declared Content-Length over the cap is rejected with 413 Payload Too Large on the header (before the body is buffered), and an unbounded chunked/no-length stream is capped by the buffered size. Streaming a request body into the handler (rather than buffering it) is the one deferred piece; it needs a stream-lifetime API that prevents a handler from retaining a reader after its connection closes.
import Ashes.IO
import Ashes.Net.Http.Server
import Ashes.Task
let route req =
async(match Ashes.Net.Http.Server.path(req) with
| "/health" -> Ashes.Net.Http.Server.text(200)("ok")
| "/echo" -> Ashes.Net.Http.Server.text(200)(Ashes.Net.Http.Server.body(req))
| "/data" -> Ashes.Net.Http.Server.json(200)("{\"ok\":true}")
| _p -> Ashes.Net.Http.Server.text(404)("not found"))
in match Ashes.Task.run(Ashes.Net.Http.Server.serve(8080)(route)) with
| Ok(_u) -> Ashes.IO.print("stopped")
| Error(e) -> Ashes.IO.print(e)Ashes.Net.Tcp
connect(host)(port)returningTask(Str, Socket)send(socket)(text)returningTask(Str, Int)receive(socket)(maxBytes)returningTask(Str, Str)close(socket)returningTask(Str, Unit)
Ashes.Net.Tcp.Server
TCP server support. listen/accept are the primitives; serve is the combinator built on them.
listen(port)returningTask(Str, Socket)— bindINADDR_ANY:port,listen, and return the listening socket (non-blocking).accept(listener)returningTask(Str, Socket)— accept one connection, returning the client socket. Suspends (parks on the listener) until a connection is ready, so it is used insideasync.serve(port)(handler)returningTask(Str, Unit)— the server lifecycle. Binds the port, then loops accepting connections, spawninghandleron each (Ashes.Task.spawn), so connections are served concurrently: a slow handler never blocks the accept loop.handler : Socket -> Task(E, A)owns its connection and runs detached — it must close the socket itself, its result is dropped, and its failure is isolated to its connection, never stopping the loop. A bind/listener failure ends the server withError. Consumed withawaitinsideAshes.Task.run(async ...).serveis parallel by default: it runs one independent reactor per online CPU (see below), so an endpoint scales across cores without the program choosing a worker count.serveParallel(port)(workers)(handler)— the same asservewith an explicit worker count (serveisserveParallel(port)(0)(handler); a count<= 0means one worker per online CPU).serveWithDrainTimeout(port)(drainMs)(handler)— the same asservewith an explicit graceful-shutdown drain bound. On the firstSIGINT/SIGTERM(console-ctrl on Windows) the server stops accepting and lets in-flight handlers finish for up todrainMsmilliseconds (default 10000 forserve), then returnsOk(()); a second signal exits immediately. A multi-reactor parent forwards the signal to its workers and reaps them before returning.setDrainTimeout(ms)returningUnit— sets the drain bound for this process (the primitive underserveWithDrainTimeout; call beforeserveso forked workers inherit it).
Programmatic shutdown is the built-in Stop.stop(Unit) capability operation (not a Ashes.Net.Tcp.Server function): performing it from inside a handler requests graceful shutdown of the whole server through the same drain path as a signal, and the server's serve lifecycle completes with Ok(()). See the language reference, section 20.8.
serve is a fork-based multi-reactor: it forks one reactor process per online CPU up front, each binding the port with SO_REUSEPORT so the kernel load-balances new connections across the workers, and each worker serves its connections concurrently on its own thread (cooperative scheduling via Ashes.Task.spawn; each spawned handler gets a private region, freed when it completes, so memory stays bounded under sustained load). Because the workers are separate processes and Ashes is pure, the connections are genuinely independent — there is no shared mutable state, and equally no cross-worker aggregation. The worker count defaults to the online-CPU count and honors the --parallel-workers compile cap. Multi-core on all three targets: Linux (x64/arm64) forks the workers with a SO_REUSEPORT listener each; Windows relaunches itself with CreateProcessA sharing one inherited listener. send / receive / close from Ashes.Net.Tcp operate on the accepted client socket. Supported on Linux x64, Linux arm64, and Windows x64 (the accept path uses WSAPoll on Windows, matching the client).
import Ashes.Net.Tcp
import Ashes.Net.Tcp.Server
let onClient client =
async(match await Ashes.Net.Tcp.receive(client)(4096) with
| Error(e) -> Error(e)
| Ok(msg) ->
match await Ashes.Net.Tcp.send(client)(msg) with
| Error(e2) -> Error(e2)
| Ok(_n) -> await Ashes.Net.Tcp.close(client))
in match Ashes.Task.run(Ashes.Net.Tcp.Server.serve(8080)(onClient)) with
| Ok(_u) -> Ashes.IO.print("server stopped")
| Error(e) -> Ashes.IO.print(e)Ashes.Net.Tls
connect(host)(port)returningTask(Str, TlsSocket)send(socket)(text)returningTask(Str, Int)receive(socket)(maxBytes)returningTask(Str, Str)close(socket)returningTask(Str, Unit)
Ashes.Net.Tls uses the same TLS runtime path as https:// in Ashes.Net.Http. On Linux x64, Linux arm64, and Windows x64 that currently means the hermetic Mbed TLS runtime linked into each TLS-using executable. No external OpenSSL installation is required. Hostname verification and system-trust validation are mandatory for successful TLS connections.
Ashes.Net.Tls.Server
Server-side TLS termination, layered on Ashes.Net.Tcp.Server.
handshake(socket)(certPem)(keyPem)returningTask(Str, TlsSocket)— runs the server half of a TLS handshake over an accepted TCP socket.certPem/keyPemare the certificate-chain and private-key PEM contents (not paths). The TLS server config is built once from these and cached for the process; the accepted socket is consumed into the returnedTlsSocket, on which the ordinaryAshes.Net.Tls.send/receive/closeoperate.serveTls(port)(certPath)(keyPath)(handler)returningTask(Str, Unit)— the TLS server lifecycle. Reads the certificate and key PEM files up front (a read failure ends the server with a readableError), then serves likeAshes.Net.Tcp.Server.serve— concurrently, one spawned handler per connection — running the handshake in front of each handler.handler : TlsSocket -> Task(E, A)owns its connection and must close it (the socket auto-drops otherwise). Consumed withAshes.Task.run.
The handshake reuses the scheduler's WaitTlsWantRead / WaitTlsWantWrite parking, so a TLS server serves concurrently on a single thread exactly as the plaintext server does. Same three targets and hermetic Mbed TLS runtime as the TLS client.
import Ashes.IO
import Ashes.Net.Tls
import Ashes.Net.Tls.Server
import Ashes.Task
let onClient tls =
async(match await Ashes.Net.Tls.receive(tls)(4096) with
| Error(e) -> Error(e)
| Ok(msg) ->
match await Ashes.Net.Tls.send(tls)("echo: " + msg) with
| Error(e2) -> Error(e2)
| Ok(_n) -> await Ashes.Net.Tls.close(tls))
in match Ashes.Task.run(Ashes.Net.Tls.Server.serveTls(8443)("cert.pem")("key.pem")(onClient)) with
| Ok(_u) -> Ashes.IO.print("stopped")
| Error(e) -> Ashes.IO.print(e)Ashes.Net.Rpc
Stdio JSON-RPC 2.0 Content-Length framing for LSP/DAP transports.
readMessage()returningResult(Str, Str)— read one framed message from stdin (reads theContent-Length:header, then exactly that many bytes viaAshes.IO.readExact)writeMessage(msg)returningUnit— write a framed message to stdout with aContent-Length:header
Ashes.Number — numbers
Ashes.Number.Math
All functions are curried. Layer 1 is hermetic (no native payload). Layer 2 transcendentals are backed by a vendored openlibm compiled to LLVM bitcode and linked into the program only when a transcendental is used, so hermetic-only programs carry no math payload and there is never a runtime dependency. See the Math runtime model in Architecture for the mechanism.
Integer:
abs(n)returningInt— absolute valuesignum(n)returningInt—-1,0, or1min(a)(b)/max(a)(b)returningIntclamp(lo)(hi)(n)returningInt—nconfined to[lo, hi]gcd(a)(b)returningInt— greatest common divisor (non-negative)lcm(a)(b)returningInt— least common multiple (non-negative;0if either is0)divMod(a)(b)returning(Int, Int)— Euclidean quotient and remainder (0 <= r < |b|)pow(base)(exp)returningInt— exponentiation by squaring (exp >= 0)isqrt(n)returningInt— integer (floor) square root (n >= 0)
Float:
absF(x)/signumF(x)returningFloatminF(a)(b)/maxF(a)(b)returningFloatclampF(lo)(hi)(x)returningFloatsqrt(x)returningFloat— hardware square root (llvm.sqrt), no libraryfloor(x)/ceil(x)/round(x)/trunc(x)returningFloatpi/e/tau—Floatconstants
Conversions:
toFloat(n)returningFloat— widen anIntfloorToInt(x)/roundToInt(x)/truncToInt(x)returningInt— narrow aFloat
Transcendentals (Layer 2, openlibm-backed):
- Trigonometric:
sin(x),cos(x),tan(x),asin(x),acos(x),atan(x),atan2(y)(x) - Hyperbolic:
sinh(x),cosh(x),tanh(x) - Exponential / logarithmic:
exp(x),expm1(x),ln(x),log2(x),log10(x),log1p(x) - Powers / roots:
powF(base)(exp),cbrt(x),hypot(x)(y) - Remainder:
fmod(x)(y)
Domain errors follow IEEE-754 (sqrt(-1.0) is NaN), so the Float functions stay total.
Ashes.Number.BigInt
Native arbitrary-precision signed integers (BigInt). Values are immutable and compiler-managed; each operation returns a fresh, normalized value. Escaping results can be RC-owned while arithmetic scratch remains scoped. The arithmetic is emitted directly as LLVM-IR runtime helpers by the backend (no runtime dependency, no external library); see the architecture notes.
fromInt(n)returningBigInt— widen a 64-bitInttoInt(a)returningResult(Str, Int)—Okwhen it fits anInt, elseErroradd(a)(b),sub(a)(b),mul(a)(b)returningBigIntdiv(a)(b)returningBigInt— quotient, truncated toward zero (division by zero yields0)mod(a)(b)returningBigInt— remainder; its sign follows the dividendcompare(a)(b)returningInt—-1,0, or1
BigInt is a distinct primitive with no implicit conversion to/from Int. It supports N literals (123N) and the operators + - * / % plus the comparisons == != < <= > >=, so the named functions above are rarely needed directly. Decimal string conversions live in Ashes.Text: fromBigInt(a) (→ Str) and parseBigInt(s) (→ Result(Str, BigInt)), matching fromInt/parseInt.
import Ashes.Number.BigInt as big
let squared = 1000000000000N * 1000000000000N
Ashes.IO.print(Ashes.Text.fromBigInt(squared))
// 1000000000000000000000000Ashes.Number.UInt
toInt(value)returningInt— widen an unsigned integer (u8/u16/u32/u64) to a signedInt. Value-preserving foru8/u16/u32(and a bit-reinterpret foru64); it is the bridge that lets a byte fromAshes.Byte.getbe used inIntarithmetic, enabling byte-level integer parsing without routing through strings.fromInt(value)returningu8— narrow anIntto an unsigned byte, wrapping modulo 256 (the low 8 bits). The inverse oftoInt; lets a computed byte value be written withAshes.Byte.appendByte/Ashes.Byte.singleton(e.g. building a percent-decoded string byte by byte).
Ashes.Collection — containers
Ashes.Collection.List
append—List(a) -> List(a) -> List(a), the elements ofleftfollowed by those ofrightfilter—(a -> Bool) -> List(a) -> List(a), the elements satisfyingpredicate, in orderfoldLeft—(b -> a -> b) -> b -> List(a) -> b, left fold frominitialover the listfold— alias forfoldLefthead—List(a) -> Maybe(a), the first element, orNoneif emptyisEmpty—List(a) -> Bool, whether the list has no elementslength—List(a) -> Int, number of elementsmap—(a -> b) -> List(a) -> List(b), applyfto each elementreverse—List(a) -> List(a), the elements in reverse ordersortBy—(a -> a -> Bool) -> List(a) -> List(a), a stableO(n log n)merge sort ordered by the comparatorbefore:before(x)(y)istruewhenxshould not come aftery(e.g.given (a) -> given (b) -> a <= bfor ascending). Provide your own comparator since the language has no built-in ordering typeclasstail—List(a) -> Maybe(List(a)), all but the first element, orNoneif empty
Ashes.Collection.Array
Immutable indexed array backed by a persistent balanced tree.
empty— empty immutable arrayisEmpty(array)returningBoollength(array)returningIntget(index)(array)returningMaybe(T)—Nonefor out-of-bounds indicesset(index)(value)(array)returning a new array (out-of-bounds indices leave the array unchanged)append(value)(array)returning a new array with value appended at the endtoList(array)returningList(T)in index orderfromList(list)returning a new array preserving input order
Ashes.Collection.Map
empty— empty immutable mapisEmpty(map)returningBool— whether the map has no entriesget(compare)(key)(map)returningMaybe(V)getStr(key)(map)returningMaybe(V)—Str-keyed lookup ordered by UTF-8 byte order (Ashes.Byte.compareinline; no comparator closure, so it is markedly faster thanget(Ashes.Text.compare))contains(compare)(key)(map)returningBoolset(compare)(key)(value)(map)returning a new map valuesetStr(key)(value)(map)returning a new map value —Str-keyedset, same ordering and performance rationale asgetStrupsertStr(key)(missValue)(onHit)(map)returning a new map value — single-traversal insert-or-update: insertsmissValuewhenkeyis absent, else replaces the stored value withonHit(oldValue). Halves the tree work of agetStr-then-setStrpair in accumulation loops.insert(compare)(key)(value)(map)returning a new map valuesize(map)returningIntfoldLeft(folder)(state)(map)returning the folded state in key ordertoList(map)returningList((K, V))in key orderfromList(compare)(entries)returning a new map value
Ashes.Collection.Map is implemented as a persistent AVL tree. Because Ashes does not yet have a built-in ordering abstraction, callers supply a total ordering function (K -> K -> Int) to lookup and update helpers.
Ashes.Collection.HashMap
A persistent map keyed by Str that needs no caller-supplied ordering. Internally an AVL tree ordered by the composite key (FNV-1a hash, key), so navigation is dominated by cheap 64-bit integer comparisons and only falls back to string comparison on a hash collision. Same persistent-structure cost model as Ashes.Collection.Map (O(log K) nodes per update).
empty— empty hash mapget(key)(map)returningMaybe(V)contains(key)(map)returningBoolset(key)(value)(map)returning a new mapinsert— alias ofsetsize(map)returningIntfoldLeft(folder)(state)(map)returning the folded state (key order is by hash, not lexical)
Ashes.Collection.HashTrie
A persistent 16-ary hash trie keyed by Str — the constant-factor alternative to Ashes.Collection.Map for large keyed accumulations. Each internal node carries its own nibble shift, so a lookup or upsert costs ~4-5 dependent node loads at tens of thousands of keys (vs ~17 for the AVL Ashes.Collection.Map), at the price of hash iteration order (re-sort at the end when ordered output is needed). Keys compare by UTF-8 bytes at the leaf; equal-hash collisions chain through the leaf. Update loops get the same in-place reuse specialization as Map.set, so hot folds are constant-memory.
empty— empty triehashText(text)returningInt— the key hash (Ashes.Byte.hashof the UTF-8 bytes); compute once per key and pass to the operations belowupsertHashed(hash)(key)(missValue)(onHit)(trie)returning a new trie — single-traversal insert-or-update: insertsmissValuewhen absent, else replaces the stored value withonHit(oldValue)getHashed(hash)(key)(trie)returningMaybe(V)foldLeft(folder)(state)(trie)returning the folded state (hash order, not key order)toList(trie)returningList((K, V))in hash ordersize(trie)returningInt
Ashes.Text — strings and text formats
Ashes.Text
The single string module: character/codepoint helpers, search and slicing, parsing and formatting. The conversion and case functions are compiler intrinsics; the search/slice/split layer is shipped Ashes code built on Ashes.Byte.
uncons(text)returningMaybe((Str, Str))parseInt(text)returningResult(Str, Int)parseFloat(text)returningResult(Str, Float)parseBigInt(text)returningResult(Str, BigInt)— decimal parse into aBigIntfromInt(value)returningStrfromFloat(value)returningStrfromBigInt(value)returningStr— decimal rendering of aBigIntformatFloat(value)(decimals)returningStr— fixed-precision decimal formatting: exactlydecimalsfractional digits, trailing zeros kept (formatFloat(1.5)(9)is1.500000000,formatFloat(2.5)(0)is3).decimalsis clamped to the range 0–18. Rounding is half-away-from-zero on the magnitude. Magnitudes too large for the fixed path fall back to the same scientific notation asfromFloat.toHex(value)returningStrbyteLength(text)returningInt— UTF-8 byte length of a stringasciiUpper(text)returningStr— ASCII-only uppercase:a–zmap toA–Zin a single O(N) byte pass; every byte of a multibyte UTF-8 sequence is>= 0x80and passes through byte-identical, so non-ASCII text is untouched (no Unicode case folding — the ASCII scope is in the name, following OCaml'suppercase_ascii/ Rust'sto_ascii_uppercase)asciiLower(text)returningStr— ASCII-only lowercase, the inverse ofasciiUpperlength—Str -> Int, number of characterssubstring—Str -> Int -> Int -> Str,countcharacters starting at indexstart. Codepoint- indexed, so it walks the string to thestart-th codepoint —O(start + count). For repeated indexed slicing of the same buffer (e.g. a sliding k-mer window), materialize it once withAshes.Byte.fromTextand use the byte-indexedAshes.Byte.subText(O(count)per slice)take—Str -> Int -> Str, the firstcountcharactersdrop—Str -> Int -> Str, all but the firstcountcharactersindexOf—Str -> Str -> Int, index of the first occurrence ofneedle, or-1if absentstartsWith—Str -> Str -> Bool, whethertextbegins withprefixcontains—Str -> Str -> Bool, whetherneedleoccurs anywhere intextsplit—Str -> Str -> List(Str), splittexton each occurrence ofseparatorjoin—Str -> List(Str) -> Str, concatenatepartswithseparatorbetween themtrim—Str -> Str, strip leading and trailing whitespacetrimStart—Str -> Str, strip leading whitespacetrimEnd—Str -> Str, strip trailing whitespaceisLetter—Str -> Bool, whether the single charactertextis an ASCII letter (a–z,A–Z)isDigit—Str -> Bool, whether the single charactertextis a decimal digit (0–9)isWhiteSpace—Str -> Bool, whether the single charactertextis space, tab, newline, or carriage returncompare—Str -> Str -> Inttotal order returning-1/0/1. Compares by UTF-8 bytes (viaAshes.Byte.fromText), which equals Unicode codepoint order, so it is a correct total order over all strings — suitable directly as the ordering function forAshes.Collection.Map/Ashes.Collection.Array.
Ashes.Text.Regex
Regular expressions backed by PCRE2 (Perl-compatible syntax). The 8-bit PCRE2 library is compiled to LLVM bitcode and linked directly into the executable when a program uses this module — everything unreachable from the exposed API is stripped, and there is no runtime dependency, exactly like the rest of Ashes. Pattern and subject are treated as UTF-8 with Unicode property support (\d, \w, \p{L}, Unicode-aware case handling). Offsets are byte offsets into the subject.
- Type:
Regex— an opaque compiled pattern. compile(pattern)returningResult(Str, Regex)— compile a pattern once.Errorcarries the PCRE2 diagnostic for an invalid pattern;Okwraps a reusable compiledRegex.isMatch(regex)(text)returningBool— true if the pattern matches anywhere intext.find(regex)(text)returningMaybe((Int, Int))— the first match asSome((start, end))byte offsets, orNone.findAll(regex)(text)returningList((Int, Int))— every non-overlapping match as(start, end)byte offsets.captures(regex)(text)returningMaybe(List(Maybe(Str)))— the first match's capture groups. Group 0 is the whole match; each group isSome(text)orNoneif it did not participate.replace(regex)(text)(replacement)returningStr— replace every match. The replacement string uses PCRE2 substitution syntax ($1,${name}group references).
import Ashes.Text.Regex
match Ashes.Text.Regex.compile("([a-z]+)=([0-9]+)") with
| Error(message) -> Ashes.IO.print("bad pattern: " + message)
| Ok(re) ->
match Ashes.Text.Regex.captures(re)("port=8080") with
| None -> Ashes.IO.print("no match")
| Some(_groups) -> Ashes.IO.print("matched")Compile a pattern once and reuse the Regex across many subjects; compilation is the expensive step. Matching is memory-bounded even in a tight recursion over many subjects.
Ashes.Text.Json
Full JSON value type with a recursive-descent parser and serializer. Objects and arrays are represented as their own cons-style ADT (no dependency on Ashes.Collection.Map).
- Type:
Json— ADT with constructorsJsonNull,JsonBool(Bool),JsonInt(Int),JsonFloat(Float),JsonStr(Str),JsonArray(Json, Json)/JsonArrayEnd(head/tail list),JsonObject(Str, Json, Json)/JsonObjectEnd(key/value/rest list) parse(text)returningResult(Str, Json)— parse a JSON documentstringify(value)returningStr— serialize a JSON valueget(key)(json)returningJson— object field lookup (JsonNullwhen absent)index(i)(json)returningJson— array element lookup (JsonNullwhen out of range)asStr/asInt/asFloat/asBool— extract a scalar with a sensible defaultisNull(json)returningBool
Ashes.Byte — binary data
Ashes.Byte
An immutable byte sequence with O(1) indexed access and O(1) length.
empty()returningBytes— empty byte sequencesingleton(byte)returningBytes— one-byte sequencelength(bytes)returningIntget(bytes, index)returningu8— panics if index out of boundsindexOf(bytes)(needle)(from)returningInt— index of the first byte equal toneedle(anIntbyte value) at or afterfrom, or-1if none. O(len − from), no allocation — a memchr for scanning a buffer by integer position without materializing views.compare(left)(right)returningInt— three-way lexicographic byte order, normalized to-1/0/1. Onememcmpover the common prefix plus a length tie-break — far faster than a byte-at-a-time loop. WithfromTextthis underliesAshes.Text.compare.subText(bytes)(start)(len)returningStr— copylenbytes starting atstartinto a freshStr. O(len); the range is clamped into the source so it never reads out of bounds. The caller must ensure the range lies on valid UTF-8 boundaries (slicing at ASCII delimiters like;/\nalways does). WithindexOfthis lets a buffer be scanned by integer index instead of a shrinkingStrview.subView(bytes)(start)(len)returningStr— a zero-copy VIEW over the same rangesubTextwould copy (O(1), no byte copy; same clamping and UTF-8 caveat). The backing bytes must outlive the view: a view over anAshes.IO.File.mmapmapping is valid for the program's lifetime, and a view stored into an escaping structure is materialized by RC graph normalization or by the specialized persistent collection blob path. Prefer it for transient per-record slices in hot scan loops.append(left, right)returningBytes— concatenate two sequencesappendByte(bytes, byte)returningBytes— append one bytefromList(list)returningBytes— convertList(u8)toBytesfromText(text)returningBytes— expose aStr's UTF-8 bytes (O(1);StrandBytesshare an in-memory layout). Byte order over the result equals Unicode codepoint order, so this is the basis for a correct string ordering (seeAshes.Text.compare).hash(bytes)returningInt— 64-bit FNV-1a hash of the byte payload. WithfromTextthis gives string hashing; it underliesAshes.Collection.HashMap.u16Le(value)returningBytes— encodeu16little-endian (2 bytes)u32Le(value)returningBytes— encodeu32little-endian (4 bytes)u64Le(value)returningBytes— encodeu64little-endian (8 bytes)getU16Le(bytes, offset)returningu16— decode little-endianu16at offsetgetU32Le(bytes, offset)returningu32— decode little-endianu32at offsetgetU64Le(bytes, offset)returningu64— decode little-endianu64at offset
Ashes.Task — concurrency
Ashes.Task
Asynchronous tasks: the Task(E, A) values consumed with await inside async(...) blocks (or the let! sugar). All networking APIs return tasks; this module creates and drives them.
run(task)returningResult(E, A)— drive a task to completion on the scheduler (program entry point for async code)task(value)returningTask(E, A)— wrap a pure value as an immediately-completed taskfromResult(result)returningTask(E, A)— lift aResultinto a tasksleep(ms)returningTask(Str, Int)— complete aftermsmillisecondsall(tasks)returningTask(E, List(A))— run all tasks, collect results in orderrace(tasks)returningTask(E, A)— first task to complete winsspawn(task)returningUnit— fire-and-forget a task on the scheduler (used by the socket servers to run one handler per connection)
Supported on Linux x64, Linux arm64, and Windows x64 (run-queue scheduler on all three).
Ashes.Task.Parallel
Structured, deterministic parallelism over pure functions (see the compiler changelog). Every result is identical to the sequential equivalent. both is a genuinely parallel fork/join primitive on all three targets (worker-owned heaps + deep-copy-on-join), forking at concrete result types and running sequentially for abstract ones. map/reduce fork at concrete element types via call-site monomorphization, degrading to a (correct) sequential evaluation when used polymorphically or partially applied.
both(left)(right)returning(A, B)— fork/join two pure thunks(Unit -> A),(Unit -> B)map(f)(list)returningList(B)— order-preserving map, split-and-fork shapedreduce(combine)(identity)(f)(list)returningB— parallel map-then-fold for associativecombine(the shard-and-merge shape for data-parallel aggregation)mapGrained(grain)(f)(list)/reduceGrained(grain)(combine)(identity)(f)(list)— the same operations with an explicit grain size: shards ofgrainelements or fewer are processed sequentially instead of split further, trading split overhead against parallelism.map/reduceare exactlymapGrained(1)/reduceGrained(1). The result is always identical to the sequential equivalent, whatever the grain (grains< 1behave as1).
Chunking a byte buffer
splitChunks(bytes)(sep)(n)returningList((Bytes, Int, Int))— splitbytesinto up toncontiguous(bytes, lo, hi)sub-ranges, each ending just after an occurrence of the record separator bytesep, so no record straddles a chunk boundary (the last chunk runs to the buffer end). It is the record-boundary chunker for the data-parallel byte-scan pattern: feed the result straight toreduce—reduce(merge)(identity)(foldChunk)(splitChunks(bytes)(sep)(n))— so thereducecall stays at the caller's concrete result type and genuinely forks.splitChunksitself is pure and does the boundary-aligned splitting only (no parallel work).
Scoped worker overrides
--parallel-workers sets the executable's compiled maximum (its hard ceiling, or the detected core count when unset). A program can request fewer workers for a specific computation with a dynamically-scoped override; the effective count is min(override, compiledMax), so a request above the ceiling still clamps and one below it reduces the local limit. The override is restored when the scope returns.
withWorkers(count)(action)returningA— run the pure thunkaction : Unit -> Awith the worker cap scoped tocount(clamped to the compiled maximum).countmust be positive (a non-positive count panics). NestedwithWorkersscopes apply the inner value inside and restore the outer on return. The result is identical to runningactionwithout the override — only the parallelism used to compute it changes.bothWithWorkers(count)(left)(right),mapWithWorkers(count)(f)(list),mapGrainedWithWorkers(count)(grain)(f)(list),reduceWithWorkers(count)(combine)(identity)(f)(list),reduceGrainedWithWorkers(count)(grain)(combine)(identity)(f)(list)— convenience wrappers, each equal towithWorkers(count)around the corresponding operation.
Ashes.Core — core value helpers
Ashes.Core.Maybe
map—(a -> b) -> Maybe(a) -> Maybe(b), applyfto the contained value ifSomeflatMap—(a -> Maybe(b)) -> Maybe(a) -> Maybe(b), applyfto the contained value, flatteninggetOrElse—a -> Maybe(a) -> a, the contained value, orfallbackifNonedefault— alias forgetOrElseunwrapOr— alias forgetOrElseisSome—Maybe(a) -> Bool, whether the value isSomeisNone—Maybe(a) -> Bool, whether the value isNone
Ashes.Core.Result
map—(a -> b) -> Result(e, a) -> Result(e, b), applyfto theOkvalueflatMap—(a -> Result(e, b)) -> Result(e, a) -> Result(e, b), applyfto theOkvalue, flatteningbind— alias forflatMapmapError—(e -> f) -> Result(e, a) -> Result(f, a), applyfto theErrorvaluegetOrElse—a -> Result(e, a) -> a, theOkvalue, orfallbackifErrordefault— alias forgetOrElseisOk—Result(e, a) -> Bool, whether the value isOkisError—Result(e, a) -> Bool, whether the value isError
Testing and internals
Ashes.Test
assertEqual(expected, actual)returningUnit— panic with an assertion failure unlessexpected == actual. Works atStr,Int,Float, andBool, and different types may be asserted within the same program.fail(message)returninga— abort withmessage; never returns, so it is usable at any type
assertEqual(expected, actual) is the preferred surface form. Like other multi-argument calls in Ashes, it is syntax sugar for curried application.
Canonical example:
import Ashes.Test
let checked = assertEqual(3, 3)
in Ashes.IO.print("ok")Ashes.Test is ordinary shipped library code, not a special compiler intrinsic.
Ashes.Internal
Compiler-foundation primitives (not intended for everyday use).
deepCopy(value)returning the same type — an independent deep copy of any value (strings, tuples, lists, closures, and recursive ADTs such asMap/HashMap). Semantically the identity for immutable values; it is used for explicit isolation, reuse defense, and parallel publication.
