Every standard-library function in Knot is reached through the base
record. The compiler injects a single top-level binding — base — into every
program; nothing else enters scope by default. You call base.println,
base.map, base.listen, and so on.
The bare forms are gated. You cannot write
println x— the compiler rejects it and asks forbase.println x. This keeps the global namespace clean: any name you define is yours, and onlybase.<name>reaches the standard library. (The one exception is constructors and keywords —case,do,yield,Just,Nothing,Ok,Err,True,False— which are language, not library.)
This file is the complete reference for every field of base, grouped by
purpose, with the full path, type, and a description. Types are written in
Knot's own notation:
a,b,r,u, … are type / effect / unit variables (universally quantified).[a]is a relation (a set) ofa.IO {effects} ais an IO action carrying an effect row.Maybe aisNothing {}orJust {value: a};Result e aisErr {error: e}orOk {value: a}.
The user-facing trait system is gone; the comparison/arithmetic operators
(<, +, ==, ++, …) are intrinsic, and the few functions below that once
carried a trait bound are now plain polymorphic functions. Where a function is
restricted in practice (e.g. minOn needs an orderable projection), that is
noted in prose rather than as a type constraint.
Relations are the core data structure. Comprehensions over a source
relation (*name) push down to SQL — filters become WHERE, aggregates
become SELECT COUNT/SUM/MIN/MAX, sortBy becomes ORDER BY, and a
read-only comprehension binding two or more sources with an equi-join
predicate (where e.dept == d.name) becomes a single multi-table SELECT
with both join columns auto-indexed. The same functions over an in-memory
relation run natively. See stdlib.md — Query Pushdown &
Auto-Indexing for the full picture.
base.filter : (a -> Bool) -> [a] -> [a]
Keep the rows where the predicate returns True. Pushes down to WHERE over
a source.
&seniors = *people |> base.filter (\p -> p.age > 65)
base.map : (a -> b) -> [a] -> [b]
Apply a function to each row. Results are deduplicated (relations are sets).
base.fold : (b -> a -> b) -> b -> [a] -> b
Left fold over a relation: combine every row into an accumulator.
totalAmount = \rel -> base.fold (\acc r -> acc + r.amount) 0 rel
base.count : [a] -> Int u
Number of rows. Over a source this is a single SELECT COUNT(*); a
base.filter / base.count pipe collapses into one
SELECT COUNT(*) ... WHERE ....
base.countWhere : (a -> Bool) -> [a] -> Int u
Count rows satisfying a predicate — base.count . base.filter — but pushes
down to one SELECT COUNT(*) ... WHERE pred when the predicate compiles to
SQL.
base.sum : [a] -> a
Sum a numeric relation. Project a record field first with base.map. Units
are preserved: summing [Float M] gives Float M.
totalDist = base.sum (base.map (\t -> t.distance) *trips)
base.avg : (a -> Float u) -> [a] -> Float u
Average of a projected numeric field. Units are preserved from the projection.
base.minOn : (a -> b) -> [a] -> b
base.maxOn : (a -> b) -> [a] -> b
Minimum / maximum of a projected field. The projection must be orderable
(Int, Float, or Text). Panics on an empty relation. Pushes down to
SELECT MIN(col) / SELECT MAX(col).
base.min : a -> a -> a
base.max : a -> a -> a
Binary minimum / maximum of two values (not a relation). Use
base.minOn / base.maxOn to aggregate over a relation.
base.min 3 7 -- 3
base.max "a" "b" -- "b"
base.union : [a] -> [a] -> [a]
Set union of two relations.
base.diff : [a] -> [a] -> [a]
Set difference — rows in the first relation but not the second.
base.inter : [a] -> [a] -> [a]
Set intersection — rows present in both.
base.bind : (a -> Maybe b) -> Maybe a -> Maybe b
Monadic bind. Over relations it is flatMap
((a -> [b]) -> [a] -> [b], compiled to knot_relation_bind); over Maybe
it threads a Just or short-circuits a Nothing. This is the function <-
desugars to in do notation.
base.head : [a] -> Maybe a
First row in iteration order, or Nothing {} if empty.
base.single : [a] -> Maybe a
Just {value: x} for a singleton relation, Nothing {} for empty or
multi-element relations.
base.findFirst : [a] -> (a -> Bool) -> Maybe a
First row matching the predicate (left-to-right), stopping at the first hit,
or Nothing {}.
base.any : (a -> Bool) -> [a] -> Bool
base.all : (a -> Bool) -> [a] -> Bool
any is True when some row matches; all is True only when every row
matches (vacuously True on []).
base.elem : a -> [a] -> Bool
Membership test by structural equality.
base.sortBy : (a -> b) -> [a] -> [a]
Reorder rows by a projected key (ascending). Pushes down to ORDER BY; with
base.take it becomes ORDER BY ... LIMIT.
&top5 = *people |> base.sortBy (\p -> (0 - p.age)) |> base.take 5
base.take : Int -> s -> s
base.drop : Int -> s -> s
First / skip n items. Polymorphic over the sequence type s — works on
relations (rows) and on Text (characters).
base.match : (a -> b) -> [b] -> [a]
Filter a relation of ADT values to one constructor, extracting the payload.
base.match Shape.Circle *shapes yields the Circle payloads.
base.upsertBy : (a -> Bool) -> a -> [a] -> [a]
Replace every element matching the predicate with the given value, or append it if none match — the "insert or update" pattern.
base.traverse : (a -> f b) -> [a] -> f [b]
Walk a relation left-to-right, sequencing through an effectful or short-circuiting step
(typically Maybe — validate every row and collect, or get the first
Nothing).
base.println : a -> IO {console} {}
Print any value to stdout followed by a newline (uses base.show
formatting). base.putLine is an alias.
base.putLine : a -> IO {console} {}
Alias for base.println.
base.print : a -> IO {console} {}
Print without a trailing newline.
base.readLine : IO {console} Text
Read a line from stdin.
base.logInfo : a -> IO {}
base.logWarn : a -> IO {}
base.logError : a -> IO {}
base.logDebug : a -> IO {}
Leveled logging to stderr (kept separate from println's stdout). Colored
on a TTY, one JSON line per record otherwise. logDebug is silent unless the
program is run with --debug.
base.show : a -> Text
Convert any value to its text representation. Pure (no IO).
base.when : Bool -> IO {} -> IO {}
base.unless : Bool -> IO {} -> IO {}
Run an IO action conditionally — base.when when the condition is True,
base.unless when False. The skipped branch yields {}.
base.when (n > 0) (base.println "positive")
base.forEach : [a] -> (a -> IO r {}) -> IO r {}
Sequence an IO action over each row (in iteration order). The effect row r
propagates.
base.not : Bool -> Bool
Boolean negation.
base.id : a -> a
Identity function.
All return IO {fs} values.
base.readFile : Text -> IO {fs} Text
Read an entire file as text.
base.writeFile : Text -> Text -> IO {fs} {}
Write text to a file (path, then contents); creates or overwrites.
base.appendFile : Text -> Text -> IO {fs} {}
Append text to a file.
base.fileExists : Text -> IO {fs} Bool
Whether a file exists at the path.
base.removeFile : Text -> IO {fs} {}
Delete a file.
base.listDir : Text -> IO {fs} [Text]
List directory entries as a relation of filenames.
base.now : IO {clock} Int Ms
Current Unix timestamp in milliseconds (Int Ms). base.now is a
0-argument IO thunk — forcing it twice runs it twice. Use base.stripUnit
for a plain Int 1.
base.sleep : Int Ms -> IO {clock} {}
Pause the current thread for the given milliseconds. Inside base.race a
sleeping loser wakes immediately when the peer wins.
base.randomInt : Int u -> IO {random} Int u
Random integer in [0, bound). Unit-polymorphic — base.randomInt 100 Usd
returns Int Usd.
base.randomFloat : IO {random} Float u
Random float in [0.0, 1.0). The unit is inferred from context.
base.randomUuid : IO {random} Uuid
A fresh RFC 9562 UUIDv7 — time-ordered, so values sort chronologically (good primary keys). Stored as TEXT in SQLite.
base.toJson : a -> Text
Encode any value as a JSON string.
base.parseJson : Text -> Maybe a
Parse JSON into a value — Just decoded on success, Nothing {} on malformed
input. Objects → records, arrays → relations, strings → Text, numbers →
Int 1 / Float 1, booleans → Bool, null → Nothing {}. Decoding is
type-directed where a target type can be inferred.
base.toUpper : Text -> Text
base.toLower : Text -> Text
Uppercase / lowercase.
base.length : Text -> Int u
Number of characters (Unicode-aware).
base.trim : Text -> Text
Strip leading and trailing whitespace.
base.reverse : Text -> Text
Reverse text.
base.chars : Text -> [Text]
Split into a relation of single characters.
base.contains : Text -> Text -> Bool
base.contains needle haystack — whether haystack contains needle.
base.textToBytes : Text -> Bytes
Encode text as UTF-8 bytes.
base.bytesToText : Bytes -> Maybe Text
UTF-8 decode; Nothing {} on invalid UTF-8.
base.bytesLength : Bytes -> Int u
Byte length.
base.bytesToHex : Bytes -> Text
Hex-encode (always succeeds).
base.bytesFromHex : Text -> Maybe Bytes
base.hexDecode : Text -> Maybe Bytes
Hex-decode; Nothing {} on odd-length, non-hex, or non-ASCII input.
hexDecode is an alias.
base.bytesConcat : Bytes -> Bytes -> Bytes
Concatenate two byte strings.
base.bytesGet : Int u1 -> Bytes -> Maybe Int u2
Byte value (0–255) at an index, or Nothing {} when out of bounds (safe for
attacker-supplied indices).
base.bytesSlice : Int u1 -> Int u2 -> Bytes -> Bytes
Sub-range: start index, length, bytes.
base.hash : a -> Bytes
BLAKE3 hash of any value, as 32 bytes. Bytes/Text hash their raw contents;
structured values hash a canonical serialisation, so equal values give equal
digests.
X25519 (encryption) and Ed25519 (signing).
base.generateKeyPair : IO {random} {privateKey: Bytes, publicKey: Bytes}
X25519 key pair for encryption/decryption.
base.generateSigningKeyPair : IO {random} {privateKey: Bytes, publicKey: Bytes}
Ed25519 key pair for signing/verification.
base.encrypt : Bytes -> Bytes -> IO {random} (Maybe Bytes)
Encrypt plaintext with a public key (sealed box: X25519 ECDH + ChaCha20-Poly1305). Args: public key, plaintext. IO because a fresh ephemeral key/nonce is generated per call.
base.decrypt : Bytes -> Bytes -> Maybe Bytes
Decrypt with a private key. Args: private key, ciphertext. Nothing {} on
failure.
base.sign : Bytes -> Bytes -> Maybe Bytes
Sign a message (Ed25519). Args: private key, message. 64-byte signature, or
Nothing {} on failure.
base.verify : Bytes -> Bytes -> Bytes -> Bool
Verify a signature. Args: public key, message, signature.
base.fork : IO r a -> IO r {}
Run an IO action on a new OS thread (fire-and-forget; the result is discarded). The effect row propagates. Each thread gets its own SQLite connection (WAL). The main thread joins all spawned threads before exit.
base.race : IO a -> IO b -> IO (Result a b)
Run two IO actions concurrently, return the winner:
Err {error: a} if the left wins, Ok {value: b} if the right wins. The
loser is cancelled cooperatively (a sleeping loser wakes immediately). Cannot
be used inside atomic.
atomic is a keyword, not a base field — you write atomic do ...,
never base.atomic. It runs an IO body as a database transaction:
atomic : IO {} a -> IO {} a
The body must be DB-only (no console/fs/network) — the compiler rejects other
effects inside. With base.retry it implements STM.
base.retry (below) is the one STM primitive that is gated to the base.
prefix, even though it's a compile-time macro rather than a record field.
base.retry : a
STM retry, only inside atomic: roll back, wait for a relation change,
re-run. Row-level filtering means a watcher on WHERE id = 1 is not woken by
writes to id = 2. (A compile-time macro, not a record field, but gated to
base.retry like the rest.)
The HTTP types (route, serve, Endpoint, Server) are part of the
language spec (DESIGN.md); these are the entry points. They are
compile-time special forms gated to the base. prefix rather than ordinary
record fields.
base.listen : Int u -> Server a -> IO {}
base.listenOn : Text -> Int u -> Server a -> IO {}
Start an HTTP server built with serve Api where .... base.listen binds all
interfaces; base.listenOn takes an explicit bind address. Handler effects
flow into the program's IO type.
(base.listen 8080 api)
base.fetch : Text -> a -> IO {network} (Result {status: Int 1, message: Text} b)
base.fetchWith : Text -> c -> a -> IO {network} (Result {status: Int 1, message: Text} b)
Type-safe HTTP client from a route declaration. The first argument is the base
URL, the last is the route's Endpoint constructor; the response type b is
inferred from the route. base.fetchWith adds ad-hoc headers (the middle
argument). Response-header routes wrap the success body as
{body: b, headers: H} inside Ok.
Two gated special forms for development-time debugging. Both print a report with the relevant type, source location (file:line:col + caret), and every in-scope binding with its type and runtime value — and both emit a compile-time warning so they aren't shipped accidentally.
base.todo : a
An unimplemented hole. Type-checks as any type, so you can stub a definition and fill it in later; the program compiles, but aborts (exit 1) with the context report when evaluation reaches it.
base.trace : a -> a
A transparent probe: prints the report plus the traced value, then returns it unchanged. Drop it into an expression without changing its meaning.
Identity at runtime — these only adjust the compile-time unit tag. See stdlib.md for the units system.
base.stripUnit : Int u -> Int 1
base.withUnit : Int 1 -> Int u
Drop / attach a unit on an Int.
base.stripFloatUnit : Float u -> Float 1
base.withFloatUnit : Float 1 -> Float u
Drop / attach a unit on a Float.
base.strip : a u -> a 1
base.dress : a 1 -> a u
Generalized unit rebranding across both Int and Float. base.strip drops
a unit; base.dress attaches one to a dimensionless value (target pinned by
context/annotation).
toS : Int Ms -> Int S
toS = \ms -> base.dress (base.strip ms / 1000)
base is an ordinary record value. The prelude assembles it from the nested
helpers (min/max/when/unless, defined as lambdas) plus every stdlib
builtin as a field, and binds it globally. Because it's a record you can pass
it, project from it, or — in a with block — shadow individual fields for
testing. Your own top-level bindings never collide with it: only the
base.<name> path reaches the standard library.
The data type base.Ordering (LT {} / EQ {} / GT {}) is also reachable
through the record for ordering-returning operations.