Skip to content

Latest commit

 

History

History
1168 lines (809 loc) · 29.2 KB

File metadata and controls

1168 lines (809 loc) · 29.2 KB

Knot Standard Library

Complete reference for all built-in functions, traits, and types.

Table of Contents


Query Pushdown & Auto-Indexing

Many relation operations over a source relation (*name : [T]) don't load rows into memory and process them in Knot — the compiler pushes them down to a single SQL query against the underlying SQLite table, and the runtime auto-indexes the columns involved.

What pushes down:

  • Filterswhere clauses and filter (\r -> r.f OP x) rows become WHERE.
  • Joins — a read-only comprehension binding two or more sources and relating them with an equi-join predicate plus single-table predicates compiles to one multi-table SELECT:
    with {result (do
      e <- *employees
      d <- *departments
      where e.dept == d.name
      where e.salary > 75
      yield {name e.name dept d.name})}
    yield result
    
    SELECT t0."name", t1."name"
    FROM "_knot_employees" AS t0, "_knot_departments" AS t1
    WHERE (t0."dept" = t1."name") AND (t0."salary" > ?)
  • Aggregatescount, countWhere, sum, avg, minOn, maxOn become SELECT COUNT(*)/SUM(...)/MIN(...)/MAX(...) ....
  • sortBy — becomes ORDER BY.
  • Pure helper functions used in a predicate are inlined, so where (salaryOf e) > 75 still becomes WHERE salary > 75.

Auto-indexing. Columns referenced in a pushed-down WHERE or ORDER BY clause — including both join columns of a multi-table join (employees.dept and departments.name) — get a CREATE INDEX IF NOT EXISTS on first use. ADT tables also index the _tag discriminator at creation. There is no CREATE INDEX syntax and nothing to declare; the runtime observes the queries and indexes accordingly.

Fallback. Any comprehension or operation the planner cannot translate falls back to reading the relation(s) and computing in memory (hash-join / nested-loop for joins). The results are identical — only the execution strategy differs — so you can write the natural comprehension and not worry about whether it pushed down.


Relation Operations

filter

filter : (a -> Bool) -> [a] -> [a]

Keep rows where the predicate returns True.

&seniors = *people |> filter (\p -> p.age > 65)

map

map : (a -> b) -> [a] -> [b]

Apply a function to each row. Results are deduplicated (relations are sets).

&names = *people |> map (\p -> {name p.name})

map is the Functor trait method for [].

match

match : Constructor -> [Constructor] -> [Payload]

Filter a relation to rows matching a constructor tag, extracting the payload.

&circles = *shapes |> match Circle    -- : [{radius: Float 1}]
&rects   = *shapes |> match Rect      -- : [{width: Float 1, height: Float 1}]

fold

fold : (b -> a -> b) -> b -> [a] -> b

Left fold over a relation. fold is the Foldable trait method for [].

totalAmount = \rel -> fold (\acc r -> acc + r.amount) 0 rel

single

single : [a] -> Maybe a

Extract the single element of a relation. Returns Just {value: x} for a singleton, Nothing {} for empty or multi-element relations.

single [{name "Alice"}]    -- Just {value {name "Alice"}}
single []                   -- Nothing {}
single [1 2]               -- Nothing {}

count

count : [a] -> Int u

Return the number of rows in a relation.

numPeople = count *people

When the argument is a source relation (or its bound alias), the compiler emits a single SELECT COUNT(*) query. Pipe forms like *people |> filter (\p -> p.age > 30) |> count collapse into one SELECT COUNT(*) FROM ... WHERE ....

countWhere

countWhere : (a -> Bool) -> [a] -> Int u

Count rows that satisfy a predicate. Equivalent to count . filter, but pushes down to a single SELECT COUNT(*) FROM ... WHERE pred when the predicate is SQL-compilable.

engHeadcount = do
  employees <- *employees
  yield (countWhere (\e -> e.dept == "Eng") employees)

sum

sum : [a] -> a

Sum of a numeric relation. Takes the relation directly — there is no projection argument. To sum a field of a record relation, project first with map. Works with Int 1, Float 1, and unit-annotated types — units are preserved.

total = sum [10 20 30]                          -- 60

-- Sum a record field by projecting first:
totalAge = sum (map (\p -> p.age) *people)

-- Unit-preserving:
totalDistance = sum (map (\t -> t.distance) *trips)   -- Float M if distance : Float M

avg

avg : (a -> Float u) -> [a] -> Float u

Average of a projected numeric field over a relation. Returns Float 1. Preserves units from the projection function — if the projection returns Float M, the average is Float M.

minOn

minOn : (a -> b) -> [a] -> b

Minimum of a projected field over a relation. The projection can return any orderable type — Int 1, Float 1, or Text (lexicographic ordering). Panics if the relation is empty.

lowestSalary = do
  employees <- *employees
  yield (minOn (\e -> e.salary) employees)

firstName = do
  employees <- *employees
  yield (minOn (\e -> e.name) employees)

When applied to a source (or bound source variable), it pushes down to SELECT MIN(col) FROM .... Combined with filter it becomes SELECT MIN(col) FROM ... WHERE ....

maxOn

maxOn : (a -> b) -> [a] -> b

Maximum of a projected field over a relation. Like minOn, works with any orderable type. Panics if the relation is empty. Pushes down to SELECT MAX(col) FROM ....

highestSalary = do
  employees <- *employees
  yield (maxOn (\e -> e.salary) employees)

min / max

min : a -> a -> a
max : a -> a -> a

Binary minimum and maximum of two values. Use minOn/maxOn to aggregate over a relation; min/max operate on two single values.

min 3 7         -- 3
max "a" "b"     -- "b"

union

union : [a] -> [a] -> [a]

Set union of two relations.

&all = union *employees *contractors

diff

diff : [a] -> [a] -> [a]

Set difference — rows in the first relation but not the second.

&nonManagers = diff *employees *managers

inter

inter : [a] -> [a] -> [a]

Set intersection — rows present in both relations.

head

head : [a] -> Maybe a

First row of a relation in iteration order, or Nothing {} if empty.

findFirst

findFirst : [a] -> (a -> Bool) -> Maybe a

First row matching the predicate (left-to-right), or Nothing {} when no row matches. Stops at the first hit.

findFirst [1 2 3 4 5] (\x -> x > 3)   -- Just {value 4}

any / all

any : (a -> Bool) -> [a] -> Bool
all : (a -> Bool) -> [a] -> Bool

any is True when some row matches; all is True only when every row matches (vacuously True on []).

elem

elem : a -> [a] -> Bool

Membership check by structural equality.

sortBy

sortBy : (a -> b) -> [a] -> [a]

Reorder rows by a projected key. The key type b must be Ord. Returns a new relation with rows in ascending key order. Sets have no inherent order; the result preserves the sorted order for downstream iteration (fold, map, forEach, etc.).

Pushes down to SQL ORDER BY when applied to a source relation. Combined with take it becomes ORDER BY ... LIMIT:

&topFive = do
  employees <- *employees
  yield (employees |> sortBy (\e -> -e.salary) |> take 5)
-- SQL: SELECT ... FROM _knot_employees ORDER BY -salary LIMIT 5

take / drop

take : Int 1 -> [a] -> [a]      -- Sequence.take
drop : Int 1 -> [a] -> [a]      -- Sequence.drop

First / drop n rows. take/drop are built-in polymorphic functions that work on both [a] (rows) and Text (characters).

upsertBy

upsertBy : (a -> Bool) -> a -> [a] -> [a]

Replace every element matching the predicate with the supplied value. If no element matches, append the value. Useful for "insert or update" patterns on source relations.

-- Bump or insert a per-user counter
bump = \user counters -> upsertBy (\c -> c.user == user)
                                   {user user count lookup user counters + 1}
                                   counters

Concurrency

fork

fork : IO {| r} a -> IO {| r} {}

Run an IO action on a new OS thread (fire-and-forget). The spawned action can return any value a (it is discarded) and may have any effect row r. That effect row propagates through fork to the caller, so a program that forks an IO performing println is visibly typed with {console} in its IO row. Each thread gets its own SQLite connection via WAL mode for safe concurrent access. The main thread waits for all spawned threads before exiting.

main = do
  fork do
    println "hello from thread 1"
  fork do
    println "hello from thread 2"
  println "hello from main"

Do blocks can be passed directly as arguments without parentheses.

race

race : IO {| r1} a -> IO {| r2} b -> IO {| r1 \/ r2} (Result a b)

Run two IO actions concurrently and return the winner. Each argument carries its own effect row; the result IO's row is the union of both (written r1 \/ r2). Effects required by either side flow into the result IO.

The winner is reported via the built-in Result a b ADT — Err {error: a} when the left action wins, Ok {value: b} when the right action wins.

slow = do
  sleep (1000 : Int Ms)
  yield "slow"

fast = do
  sleep (50 : Int Ms)
  yield "fast"

main = do
  r <- race slow fast
  case r of
    Err {error: a} -> println ("left won: " ++ a)
    Ok {value: b}  -> println ("right won: " ++ b)
  yield {}

Cancellation is cooperative but aggressive: the loser's knot_io_run checks its cancel token between every IO thunk, and sleep parks on a condvar that's signalled on cancel — so a loser stuck in a long sleep wakes immediately when the peer wins. The parent does not wait for the loser; it returns as soon as a winner is observed, and the loser unwinds at its next safe point (tracked for the final program-exit join).

race cannot be used inside atomic — its effects are not rollback-safe.

atomic

atomic : IO {} a -> IO {} a

Run an IO body in a database transaction. The body must contain only DB operations — no external effects (console, fs, etc.) are allowed. If the body calls retry, the transaction rolls back and waits for a relation change before re-executing.

transfer = \from to amount -> atomic do
  accounts <- *accounts
  *accounts = do
    a <- accounts
    yield (if a.name == from then {a | balance a.balance - amount}
           else if a.name == to then {a | balance a.balance + amount}
           else a)

retry

retry : a

Used inside atomic blocks only. Causes the transaction to rollback and wait until some relation changes, then re-executes the atomic block. Implements STM (Software Transactional Memory) style concurrency.

waitForTask = \id -> atomic do
  tasks <- *tasks
  with {done do
    t <- tasks
    where t.id == id
    where t.status == "done"
    yield t}
  (do
    where (count done) == 0
    retry
    yield done)

The compiler enforces that retry is only used inside atomic.

Row-level wakeup filtering. The runtime tracks which rows the atomic block actually read by inspecting WHERE/single (filter ...) patterns and the predicates inside them (equality, inequality, ordered comparisons, and IN sets). A parked retry is only woken when an UPDATE, DELETE, or INSERT touches a matching row. So a worker retrying on WHERE id = 1 is not woken by writes to id = 2, and a worker retrying on status IN ("queued", "running") is unaffected by writes that leave the status outside that set. Bulk replacements (*rel = ...) wake all watchers conservatively.


Text Operations

toUpper

toUpper : Text -> Text

Convert text to uppercase.

toLower

toLower : Text -> Text

Convert text to lowercase.

length

length : Text -> Int 1

Return the number of characters (Unicode-aware).

trim

trim : Text -> Text

Strip leading and trailing whitespace.

reverse

reverse : Text -> Text

Reverse text.

chars

chars : Text -> [Text]

Split text into a relation of single characters.

take / drop

take and drop are Sequence trait methods with built-in impls for both Text (characters) and relations (rows):

take : Int 1 -> Text -> Text         -- characters
take : Int 1 -> [a]  -> [a]          -- rows
drop : Int 1 -> Text -> Text
drop : Int 1 -> [a]  -> [a]
take 3 "hello"        -- "hel"
take 2 [10 20 30]   -- [10, 20]
drop 3 "hello"        -- "lo"
drop 1 [10 20 30]   -- [20, 30]

contains

contains : Text -> Text -> Bool

Check if the second argument contains the first as a substring.

has = contains "ell" "hello"   -- True

Console I/O

println

println : a -> IO {console} {}

Print a value to stdout followed by a newline. putLine is an alias.

print

print : a -> IO {console} {}

Print a value to stdout without a trailing newline.

logInfo / logWarn / logError / logDebug

logInfo  : a -> IO {console} {}
logWarn  : a -> IO {console} {}
logError : a -> IO {console} {}
logDebug : a -> IO {console} {}

Leveled logging to stderr (so output does not mix with println on stdout). When stderr is a TTY, output is colored; otherwise each record is written as one JSON line for log aggregators. logDebug only emits when the program is launched with --debug — debug records are dropped silently otherwise. (Every compiled Knot program accepts --debug automatically; see Runtime CLI.)

main = do
  logInfo "starting"
  logWarn {event "low memory" availableMb 64}
  yield {}

show

show : a -> Text

Convert any value to its text representation. This is a pure function (no IO).

readLine

readLine : IO {console} Text

Read a line of input from stdin.


Control Flow

when / unless

when   : Bool -> IO r {} -> IO r {}
unless : Bool -> IO r {} -> IO r {}

Run an IO action conditionally. when cond a runs a if cond is True {}; unless cond a runs a if cond is False {}. The skipped branch becomes yield {}. The action's effect row r propagates to the result.

when (n > 0) (println "positive")

unless verbose do
  println "(quiet mode)"

forEach

forEach : [a] -> (a -> IO r {}) -> IO r {}

Sequence an IO action over each row of a relation. Iteration follows the relation's deterministic order (after any sortBy).

forEach ["a" "b" "c"] (\s -> println s)

File System

All file system functions return IO {fs} values.

readFile

readFile : Text -> IO {fs} Text

Read an entire file's contents as text.

writeFile

writeFile : Text -> Text -> IO {fs} {}

Write text to a file (creates or overwrites). First argument is the path, second is the content.

appendFile

appendFile : Text -> Text -> IO {fs} {}

Append text to a file.

fileExists

fileExists : Text -> IO {fs} Bool

Check whether a file exists at the given path.

removeFile

removeFile : Text -> IO {fs} {}

Delete a file.

listDir

listDir : Text -> IO {fs} [Text]

List directory entries as a relation of filenames.

main = do
  files <- listDir "."
  yield (filter (\f -> contains ".knot" f) files)

Time

now

now : IO {clock} Int Ms

Return the current Unix timestamp in milliseconds. The result is tagged with the built-in Ms unit; use stripUnit if you need a plain Int 1.

sleep

sleep : Int Ms -> IO {clock} {}

Pause the current thread for the given number of milliseconds. Inside a race worker, sleep parks on the worker's cancel condvar and wakes immediately if the peer wins.


Random

randomInt

randomInt : Int u -> IO {random} Int u

Generate a random integer in the range [0, bound). Unit-polymorphic — the bound's unit is preserved in the result, so randomInt 100 Usd returns Int Usd.

randomFloat

randomFloat : IO {random} Float u

Generate a random float in the range [0.0, 1.0). Unit-polymorphic — the unit is inferred from context.

randomUuid

randomUuid : IO {random} Uuid

Generate a fresh UUID. The output is a RFC 9562 UUIDv7 — time-ordered, so values sort chronologically and are well-suited as primary keys.

main = do
  u <- randomUuid
  println u
  yield {}

Uuid values are stored as TEXT in SQLite and compare by their canonical string representation.


JSON

toJson

toJson : a -> Text

Encode any value as a JSON string.

parseJson

parseJson : Text -> Maybe a

Parse a JSON string into a value, returning Just value on success and Nothing on a parse failure. Objects become records, arrays become relations, strings become Text, numbers become Int 1 or Float 1, booleans become Bool, and null becomes Nothing {} (the Maybe wire convention). Decoding is type-directed where a target type can be inferred.


Bytes

textToBytes

textToBytes : Text -> Bytes

Encode text as UTF-8 bytes.

bytesToText

bytesToText : Bytes -> Maybe Text

UTF-8 decode bytes to text. Returns Nothing {} on invalid UTF-8.

bytesLength

bytesLength : Bytes -> Int u

Return the byte length.

bytesToHex

bytesToHex : Bytes -> Text

Encode bytes as a hexadecimal string. Always succeeds.

bytesFromHex

bytesFromHex : Text -> Maybe Bytes

Decode a hexadecimal string to bytes. Returns Nothing {} on odd-length, non-hex, or non-ASCII input. hexDecode is an alias.

bytesConcat

bytesConcat : Bytes -> Bytes -> Bytes

Concatenate two byte strings.

bytesGet

bytesGet : Int u1 -> Bytes -> Int u2

Get the byte value (0–255) at the given index.

bytesSlice

bytesSlice : Int u1 -> Int u2 -> Bytes -> Bytes

Extract a sub-range. Arguments: start index, length, bytes.

hash

hash : a -> Bytes

BLAKE3 hash of any value, returned as 32 bytes. Bytes and Text hash their raw contents; structured values (records, relations, constructors) hash a canonical serialisation, so equal logical values always produce equal digests.

bytesToHex (hash "hello")    -- "ea8f163..."

HTTP

The HTTP types and primitives are defined in the language spec (DESIGN.md). The standard library exposes:

listen / listenOn

listen   : Int u -> Server a r -> IO {network | r} {}
listenOn : Text   -> Int u -> Server a r -> IO {network | r} {}

Start an HTTP server built with serve API where .... listen binds to all interfaces; listenOn takes an explicit bind address. The r row variable unifies with the server's effect row, so handler effects (e.g. console from a handler that calls println) flow into the program's IO type.

fetch / fetchWith

fetch     : Text -> Endpoint -> IO {network} (Result HttpError T)
fetchWith : Text -> {headers: [{name: Text, value: Text}]}
                -> Endpoint -> IO {network} (Result HttpError T)

Type-safe HTTP client built from route declarations. Endpoint is a route constructor; the response type T is inferred from the route. fetchWith lets you add ad-hoc headers on top of the route's declared ones. When the route declares response headers, the success body wraps as {body: T, headers: H} inside Ok.


Cryptography

Knot provides elliptic-curve cryptography built-ins using X25519 (encryption) and Ed25519 (signing).

generateKeyPair

generateKeyPair : IO {random} {privateKey: Bytes, publicKey: Bytes}

Generate an X25519 key pair for encryption/decryption. Inside a do block, bind with keys <- generateKeyPair.

generateSigningKeyPair

generateSigningKeyPair : IO {random} {privateKey: Bytes, publicKey: Bytes}

Generate an Ed25519 key pair for signing/verification. Inside a do block, bind with keys <- generateSigningKeyPair.

encrypt

encrypt : Bytes -> Bytes -> IO {random} Bytes

Encrypt plaintext bytes with a public key (sealed-box: X25519 ECDH + ChaCha20-Poly1305). First argument is the public key, second is the plaintext. Returns IO because a fresh ephemeral key pair and nonce are generated per call.

decrypt

decrypt : Bytes -> Bytes -> Bytes

Decrypt ciphertext bytes with a private key. First argument is the private key, second is the ciphertext.

sign

sign : Bytes -> Bytes -> Bytes

Sign a message with a private key (Ed25519). First argument is the private key, second is the message. Returns a 64-byte signature.

verify

verify : Bytes -> Bytes -> Bytes -> Bool

Verify a signature. Arguments: public key, message, signature.


Utility Functions

id

id : a -> a

Identity function — returns its argument unchanged.

not

not : Bool -> Bool

Boolean negation.

stripUnit / withUnit / stripFloatUnit / withFloatUnit

stripUnit      : Int u -> Int 1
withUnit       : Int 1 -> Int u
stripFloatUnit : Float u -> Float 1
withFloatUnit  : Float 1 -> Float u

Drop or attach a unit tag. Identity at runtime — they only adjust the compile-time type. Use them when you need to rebrand a value with a different concrete unit (e.g. MsS).

strip / dress

strip : a u -> a 1
dress : a 1 -> a u

Generalized unit rebranding that works across both Int and Float with a single call. strip drops a value's unit; dress attaches one to a dimensionless value, the target pinned by context or annotation. The u is a unit variable (kind Unit), so only unit-carrying numerics qualify. Identity at runtime:

toS : Int Ms -> Int S
toS = \ms -> dress (strip ms / 1000)

toMiles : Float M -> Float Mi
toMiles = \d -> dress (strip d * 0.000621371)

A dimensionless Int 1/Float 1 does not unify with a concrete unit, so dress is the explicit escape hatch for re-attaching one — an annotation alone cannot rebrand a value already pinned to 1.


Operator Behavior (Intrinsic)

There is no user-facing trait system. You cannot declare a trait, write an impl, or put a Num a => bound on a function — the parser rejects all of it. The operators below are intrinsic: the compiler knows how to evaluate them directly on the supported types, with no trait dictionary involved. This table describes what each operator does and which types it works on.

== / != — equality

Works on Int 1, Float 1, Text, Bool, and unit-annotated numerics. Pushes down to = in SQL when used in a SQL-compilable comprehension.

< / > / <= / >= — ordering

Works on Int 1, Float 1, Text, and unit-annotated numerics. Ordering is the LT {} / EQ {} / GT {} ADT (see base.compare). Pushes down to SQL comparison operators.

+ / - / * / / and unary - — arithmetic

Works on Int 1, Float 1, and unit-annotated numerics. Int 1 arithmetic is checked and panics on overflow. Units compose algebraically (see Units of Measure). +/- require matching units; *// combine units. Pushes down to SQL arithmetic.

% — modulo

Int 1 remainder (sign follows the dividend) and Float 1 fmod. Modulo by zero panics. Handled by intrinsic codegen — there is no user-overridable operation. Pushes down to SQLite % in a SQL-compilable comprehension.

++ — concatenation

Works on Text (string append) and [a] (relation union). Pushes down to SQL || for text.

&& / || — boolean logic

Short-circuiting AND/OR on Bool. Pushes down to SQL AND/OR.

|> — pipe forward

x |> f is f x. Purely syntactic.

Higher-order functions without traits

Functions like base.map, base.fold, base.traverse are ordinary polymorphic functions over concrete types ([a], Maybe a, Result e a) — not trait methods. They appear in this reference with plain a/b type variables and no bounds.


Built-in Types

Type Description
Int 1 64-bit signed integer (i64); arithmetic is checked and panics on overflow
Float 1 64-bit floating point
Int u Integer with compile-time unit (e.g. Int Usd)
Float u Float with compile-time unit (e.g. Float M, Float (M/S^2))
Text Unicode string
Bool True {} or False {}
Bytes Byte string
Uuid RFC 9562 UUIDv7 identifier (TEXT in SQLite)
[a] Relation (set of values of type a)
IO {effects} a IO action with tracked effects
Ordering LT {}, EQ {}, or GT {}
Maybe a Nothing {} or Just {value: a} (supports do/<-)
Result e a Err {error: e} or Ok {value: a} (supports do/<-)

Units of Measure

Compile-time units on Int and Float. Fully erased at runtime — no performance cost, no runtime representation. Every numeric type carries a unit; write Int 1 / Float 1 for the dimensionless case.

No Declaration Needed

Units are not declared. Any name used in a unit position is a unit — there is nothing to declare since a unit has no body, only a name. Compound units are written inline as expressions.

Literals and Type Annotations

distance = (42.0 : Float M)   -- Float M
speed : Float (M / S)
force : Float (Kg * M / S^2)
cents : Int Usd

Arithmetic Rules

  • +/- require matching units
  • *// compose units algebraically
  • Unary negation preserves units
  • Scalar (dimensionless) multiplication preserves the other operand's unit
(10.0 : Float M) + (5.0 : Float M)   -- Float M
(10.0 : Float M) + (5.0 : Float S)   -- type error
(10.0 : Float M) * (5.0 : Float M)   -- Float (M^2)
(100.0 : Float M) / (10.0 : Float S) -- Float (M/S)
2.0 * (5.0 : Float M)                -- Float M
-((5.0 : Float M))                   -- Float M

Unit Polymorphism

Concrete units are uppercase; lowercase names are unit variables:

double : Float u -> Float u
double = \x -> x + x

Unit-Preserving Functions

avg, minOn, and maxOn preserve units from their projection function; sum preserves the units of the numeric relation it sums:

avg   (\t -> t.distance) *trips   -- Float M if distance : Float M
sum   (map (\t -> t.distance) *trips)   -- Float M if distance : Float M
minOn (\t -> t.distance) *trips   -- Float M if distance : Float M
maxOn (\t -> t.distance) *trips   -- Float M if distance : Float M

Operators

Operator Works on Behavior
+ - * / Int 1, Float 1, unit-annotated Arithmetic (checked on Int 1)
% Int 1, Float 1 Modulo / fmod
unary - Int 1, Float 1, unit-annotated Negation
== != Int 1, Float 1, Text, Bool Equality
< > <= >= Int 1, Float 1, Text Ordering
++ Text, [a] Concatenation / union
&& || Bool Short-circuiting logic
|> any Pipe forward (x |> f = f x)

All are intrinsic (no trait mechanism); see Operator Behavior (Intrinsic).


Runtime CLI

Every compiled Knot program accepts a common set of runtime flags and subcommands without any user wiring:

Argument Description
--debug Enable logDebug output; without this flag, logDebug calls are dropped silently.
--help Print usage including any compile-time overrides exposed by the program.
--http-max-body-bytes=N Cap HTTP request and response bodies. Suffixes: K, M, G. Default 16M. Applies to both listen and fetch.
--<name>=<value> Override a compile-time constant. The compiler exposes top-level constants annotated for override; see the program's own --help. The same flags may be set at build time via knot build … --<name>=<value>.
<program> db Launch a terminal-UI database explorer over the program's <name>.db SQLite file. Browses every source relation, paginates rows, and lets you drill into individual records.
<program> api <RouteName> Print an OpenAPI 3.0 JSON specification for the named route declaration. Useful for generating client SDKs or feeding into Swagger UI.

The compiler itself (knot) supports:

Command Description
knot build <file.knot> [-o <path>] [--<name>=<value> …] Compile to a native executable. Overrides supply compile-time constants.
knot fmt [--check] [--stdout] <file.knot> … Format source files in place. --check exits non-zero when files are unformatted; --stdout prints to stdout instead of rewriting.
knot help Show CLI usage.