on master help sign in
Update documentation to match current implementation

- Corrected fork/race effect row signatures - Updated hash algorithm from SHA-256 to BLAKE3 - Added missing builtin functions documentation - Documented all runtime CLI flags and subcommands - Fixed constraint syntax in DESIGN.md to match parser - Expanded runtime crate description in CLAUDE.md

commit19f4d87abee4c04065d5c391341e2e902e871bed authoriko <mail@iko.soy> dateFri 29 May 2026, 17:46 (4 months ago) parentae8e1cb
6 files changed+131−48
CLAUDE.md+12−4 DESIGN.md+29−26 README.md+17 base.md+5−5 knot.md+5−5 stdlib.md+63−8
CLAUDE.md+12−4
@@ -60,7 +60,15 @@ let (module, parse_diags) = parser.parse_module();
6060
6161### Runtime (`crates/knot-runtime/`)
6262
63−Compiled as a `staticlib` (with `rlib` for workspace dependency resolution). All functions use `extern "C"` ABI, called by Cranelift-generated code via symbol references.
63+Compiled as a `staticlib` (with `rlib` for workspace dependency resolution). All functions use `extern "C"` ABI, called by Cranelift-generated code via symbol references. `lib.rs` is the main runtime; `log.rs` implements the leveled logger used by `logInfo`/`logWarn`/`logError`/`logDebug` (TTY-colored on stderr or one JSON record per line; `--debug` arg scanned by `debug_enabled()` gates `logDebug`); `tui.rs` is the `db` subcommand's terminal-UI database explorer (parses `_knot_schema`, paginates rows, drills into individual records).
64+
65+Compiled programs expose a common CLI without any user wiring:
66+- `--debug` — enable `logDebug` output
67+- `--help` — print usage including any compile-time overrides exposed via `knot_override_check_help` (the compiler emits this list at codegen time)
68+- `--http-max-body-bytes=N` — cap HTTP request and response bodies (`K`/`M`/`G` suffixes, default `16M`); applies to both `listen` and `fetch`
69+- `--<name>=<value>` — override a compile-time constant declared in the source (same flags accepted both at `knot build` and at run time)
70+- `<program> db` — dispatched by `knot_db_handle` to `tui::run_db_explorer`, opens the program's `<name>.db` in an interactive explorer
71+- `<program> api <RouteName>` — dispatched by `knot_api_handle`, prints an OpenAPI 3.0 JSON spec for the named route (generated by `generate_openapi`)
6472
6573- All Knot values are `*mut Value` (heap-allocated tagged enum); integers are unbounded (`num_bigint::BigInt`); IO actions are `Value::IO(fn_ptr, env)` thunks executed via `knot_io_run`
6674- `rusqlite` with `bundled` feature statically links SQLite into every compiled binary
@@ -78,12 +86,12 @@ Compiled as a `staticlib` (with `rlib` for workspace dependency resolution). All
7886### Compiler (`crates/knot-compiler/`)
7987
8088- **Desugaring** (`desugar.rs`): Transforms "pure comprehension" do blocks into nested calls to `__bind`, `__yield`, and `__empty`. A pure comprehension is a do block with only Bind/Where/Let/Yield statements (no bare side-effecting expressions or `groupBy`). Do blocks that are direct values of `set`/`replace` expressions are NOT desugared (preserves SQL optimization patterns). Do blocks containing `groupBy` are NOT desugared (require loop-based codegen for the collect/group/re-iterate pattern). Do blocks containing constructor pattern binds (`Ctor pat <- expr`) are NOT desugared (the desugarer cannot syntactically distinguish value pattern matches from monadic binds; direct codegen handles both). Do blocks containing IO-returning expressions are NOT desugared — the desugarer's `expr_is_io` recognizes both external IO builtins (`println`, `readFile`, `now`, etc.) and relation operations (`SourceRef`, `DerivedRef`, `Set`, `ReplaceSet`, `At`) as IO, excluding these do blocks from desugaring so they go through the dedicated IO codegen path (`compile_io_do`) which runs each IO action with `knot_io_run` and wraps the result in `knot_io_pure`; this ensures IO do blocks inside if/then/else branches are typed correctly as IO rather than defaulting to relational. Mixed/sequential do blocks remain as `Do(stmts)` nodes for imperative codegen. The desugared `__bind`/`__yield`/`__empty` dispatch through Monad/Applicative/Alternative trait impls — the monad type is resolved at compile time via HKT unification (`monad_info` maps spans to `MonadKind::Relation`, `MonadKind::Adt(name)`, or `MonadKind::IO`), and codegen calls the appropriate `Monad_{type}_bind`, `Applicative_{type}_yield`, or `Alternative_{type}_empty` mangled function. This allows `do` blocks to work with any type implementing the Monad/Applicative/Alternative traits, not just `[]`.
81−- **Type inference** (`infer.rs`): Hindley-Milner type inference with row-polymorphic records and row-polymorphic variants, let-generalization, unification with occurs check, ADT constructor typing, trait method registration, associated type erasure, higher-kinded type polymorphism (`TyCon`/`App` type nodes with normalization-based unification), IO monad type (`Ty::IO(BTreeSet<IoEffect>, Box<Ty>)` with effect tracking for `Console`, `Fs`, `Network`, `Clock`, `Random`), units of measure (`Ty::IntUnit(UnitTy)`/`Ty::FloatUnit(UnitTy)` with normalized exponent-map representation `BTreeMap<String, i32>` plus optional `UnitVar` for polymorphism; unit algebra: `+`/`-` require unit match, `*`/`/` compose units, negation preserves units; plain `Int`/`Float` are unit-agnostic and unify with any `IntUnit`/`FloatUnit`; `Scheme` quantifies over both `TyVar`s and `UnitVar`s — instantiation freshens both; all stdlib functions with numeric types are unit-polymorphic — `avg` registered as `∀a u. (a -> Float<u>) -> [a] -> Float<u>`; `sum` already fully polymorphic in return type; `randomInt` as `∀u. Int<u> -> IO {random} Int<u>` (preserves bound unit in result); `randomFloat` as `∀u. IO {random} Float<u>`; `now` as `IO {clock} Int<Ms>`; `sleep` as `Int<Ms> -> IO {clock} {}`; `Ms` registered as built-in unit (milliseconds) for clock operations; `count` as `∀a u. [a] -> Int<u>`; `length` as `∀u. Text -> Int<u>`; `take`/`drop` as `∀u. Int<u> -> Text -> Text`; `listen` as `∀a b u. Int<u> -> (a -> b) -> IO {network} {}`; `bytesLength` as `∀u. Bytes -> Int<u>`; `bytesSlice` as `∀u1 u2. Int<u1> -> Int<u2> -> Bytes -> Bytes`; `bytesGet` as `∀u1 u2. Int<u1> -> Bytes -> Int<u2>`; unit declarations collected in `collect_types`; derived unit aliases expand at use site), refined types (`type Nat = Int where \x -> x >= 0` — refined type aliases stay nominal as `Ty::Con(name, vec![])`, NOT resolved to base type; stored in `refined_types: HashMap<String, (Ty, Expr)>` mapping name to base type and predicate; subsumption rule in `unify`: when `Con(name, [])` is a refined type, unify with its base type — enables bidirectional compatibility `Nat ↔ Int`; `refine expr` typed as `Result RefinementError alpha` where `alpha` is fresh and constrained by context — post-inference resolves `alpha` to the refined type name for codegen; `RefinementError` registered as a type alias to `{typeName: Text, violations: [{field: Maybe Text, message: Text}]}`; `TypeKind::Refined { base, predicate }` in AST for inline refinements, stripped to base during schema resolution; `refine_vars: Vec<(Span, TyVar, Ty)>` tracks refine expressions for post-inference target resolution; `RefineTargets` and `RefinedTypeInfoMap` exported to codegen), and trait bound checking (explicit `TraitName a =>` constraints on function signatures are validated at call sites against known `impl` declarations; trait method calls automatically carry their trait's constraint). All relation operations return IO: `SourceRef` returns `IO {} [T]`, `DerivedRef` returns `IO {} [T]`, `Set`/`ReplaceSet` return `IO {} {}`; `Set`/`ReplaceSet` unify target and value by unwrapping IO from both sides. `Atomic` is typed as `IO {} a -> IO {} a` — body must be IO, enforced via unification; `in_atomic: bool` flag tracks context for `retry` validation. Built-in registrations include `fork : ∀r. IO r {} -> IO r {}` (the spawned IO's effect row propagates through `fork`, so its effects remain visible in the caller's IO type), `race : ∀a b r. IO r a -> IO r b -> IO r (Result a b)` (both args share an effect row that propagates to the result), `randomUuid : IO {random} Uuid`, and `retry : ∀a. a` (bottom type). New primitive `Ty::Uuid` in `infer.rs` with parser/printer/erasure plumbing parallel to `Ty::Bytes`. Built-in ADTs registered in `register_builtins`: `Maybe a` (`Nothing`/`Just`), `Result e a` (`Err`/`Ok`), `Bool` (`True`/`False`) — `Bool` constructors map to the primitive `Ty::Bool` (not `Ty::Con`), so `True {}` and `False {}` produce the same type as `true`/`false` literals; `Result` has built-in Functor/Applicative/Monad/Alternative impls for do-block support. Constructor patterns in `case` and do-bind create open variant types `<Ctor {} | r>` (the scrutinee only needs to *contain* the matched constructor), enabling row-polymorphic variant matching; when all constructors of a known data type are covered without a wildcard, the row variable is closed and exhaustiveness is checked normally; open variants require a wildcard `_` case
89+- **Type inference** (`infer.rs`): Hindley-Milner type inference with row-polymorphic records and row-polymorphic variants, let-generalization, unification with occurs check, ADT constructor typing, trait method registration, associated type erasure, higher-kinded type polymorphism (`TyCon`/`App` type nodes with normalization-based unification), IO monad type (`Ty::IO(BTreeSet<IoEffect>, Box<Ty>)` with effect tracking for `Console`, `Fs`, `Network`, `Clock`, `Random`), units of measure (`Ty::IntUnit(UnitTy)`/`Ty::FloatUnit(UnitTy)` with normalized exponent-map representation `BTreeMap<String, i32>` plus optional `UnitVar` for polymorphism; unit algebra: `+`/`-` require unit match, `*`/`/` compose units, negation preserves units; plain `Int`/`Float` are unit-agnostic and unify with any `IntUnit`/`FloatUnit`; `Scheme` quantifies over both `TyVar`s and `UnitVar`s — instantiation freshens both; all stdlib functions with numeric types are unit-polymorphic — `avg` registered as `∀a u. (a -> Float<u>) -> [a] -> Float<u>`; `sum` already fully polymorphic in return type; `randomInt` as `∀u. Int<u> -> IO {random} Int<u>` (preserves bound unit in result); `randomFloat` as `∀u. IO {random} Float<u>`; `now` as `IO {clock} Int<Ms>`; `sleep` as `Int<Ms> -> IO {clock} {}`; `Ms` registered as built-in unit (milliseconds) for clock operations; `count` as `∀a u. [a] -> Int<u>`; `length` as `∀u. Text -> Int<u>`; `take`/`drop` as `∀u. Int<u> -> Text -> Text`; `listen` as `∀a r u. Int<u> -> Server a r -> IO {network | r} {}` (and `listenOn` with an extra leading `Text` for the bind address); `bytesLength` as `∀u. Bytes -> Int<u>`; `bytesSlice` as `∀u1 u2. Int<u1> -> Int<u2> -> Bytes -> Bytes`; `bytesGet` as `∀u1 u2. Int<u1> -> Bytes -> Int<u2>`; unit declarations collected in `collect_types`; derived unit aliases expand at use site), refined types (`type Nat = Int where \x -> x >= 0` — refined type aliases stay nominal as `Ty::Con(name, vec![])`, NOT resolved to base type; stored in `refined_types: HashMap<String, (Ty, Expr)>` mapping name to base type and predicate; subsumption rule in `unify`: when `Con(name, [])` is a refined type, unify with its base type — enables bidirectional compatibility `Nat ↔ Int`; `refine expr` typed as `Result RefinementError alpha` where `alpha` is fresh and constrained by context — post-inference resolves `alpha` to the refined type name for codegen; `RefinementError` registered as a type alias to `{typeName: Text, violations: [{field: Maybe Text, message: Text}]}`; `TypeKind::Refined { base, predicate }` in AST for inline refinements, stripped to base during schema resolution; `refine_vars: Vec<(Span, TyVar, Ty)>` tracks refine expressions for post-inference target resolution; `RefineTargets` and `RefinedTypeInfoMap` exported to codegen), and trait bound checking (explicit `TraitName a =>` constraints on function signatures are validated at call sites against known `impl` declarations; trait method calls automatically carry their trait's constraint). All relation operations return IO: `SourceRef` returns `IO {} [T]`, `DerivedRef` returns `IO {} [T]`, `Set`/`ReplaceSet` return `IO {} {}`; `Set`/`ReplaceSet` unify target and value by unwrapping IO from both sides. `Atomic` is typed as `IO {} a -> IO {} a` — body must be IO, enforced via unification; `in_atomic: bool` flag tracks context for `retry` validation. Built-in registrations include `fork : ∀a r. IO {| r} a -> IO {| r} {}` (the spawned IO's effect row propagates through `fork`, so its effects remain visible in the caller's IO type), `race : ∀a b r1 r2. IO {| r1} a -> IO {| r2} b -> IO {| r1 \/ r2} (Result a b)` (each arg carries its own effect row; the result row is the union of both, expressed in the type with the `\/` operator — `ast_type_to_ty` registers the effect-union constraint and `generalize` captures it on the scheme), `randomUuid : IO {random} Uuid`, and `retry : ∀a. a` (bottom type). New primitive `Ty::Uuid` in `infer.rs` with parser/printer/erasure plumbing parallel to `Ty::Bytes`. Built-in ADTs registered in `register_builtins`: `Maybe a` (`Nothing`/`Just`), `Result e a` (`Err`/`Ok`), `Bool` (`True`/`False`) — `Bool` constructors map to the primitive `Ty::Bool` (not `Ty::Con`), so `True {}` and `False {}` produce the same type as `true`/`false` literals; `Result` has built-in Functor/Applicative/Monad/Alternative impls for do-block support. Constructor patterns in `case` and do-bind create open variant types `<Ctor {} | r>` (the scrutinee only needs to *contain* the matched constructor), enabling row-polymorphic variant matching; when all constructors of a known data type are covered without a wildcard, the row variable is closed and exhaustiveness is checked normally; open variants require a wildcard `_` case
8290- **Effect inference** (`effects.rs`): Infers per-declaration effects (`{r *rel}`, `{w *rel}`, `{rw *rel}` for both, `{console}`, `{clock}`, etc.), checks IO-in-atomic constraints (atomic blocks must only contain DB interactions — no console/fs/clock/random), validates that atomic blocks actually interact with relations (have reads or writes), validates explicit effect annotations against inferred effects. `rw *x` is shorthand for `r *x, w *x` — parsed as both effects, and the display coalesces matching pairs back to `rw *x`.
8391- **Type resolution** (`types.rs`): Resolves aliases (including multi-variant ADTs to `ResolvedType::Adt`), computes SQLite schemas from Knot type annotations, collects subset constraints; refined type aliases separated during collection — predicates stored in `refined_types: HashMap<String, Expr>`, base types stored in `aliases` for schema resolution; `TypeKind::Refined` transparent for schema — `resolve_type` and `apply_type_subst` recurse into base; `source_refinements: HashMap<String, Vec<(Option<String>, String, Expr)>>` maps source names to their field-level and whole-element refinement predicates (collected by `collect_source_refinements` walking source type ASTs)
8492- **Codegen** (`codegen.rs`): Cranelift IR generation — the `build_function` pattern moves `ctx`/`builder_ctx` out of `self` to avoid borrow conflicts while allowing `self.method()` calls during IR building
8593- **Linker** (`linker.rs`): Invokes `cc` with platform-appropriate flags
86−- **CLI** (`main.rs`): `knot build <file.knot>` entry point
94+- **CLI** (`main.rs`): subcommands `knot build <file.knot> [-o <path>] [--<name>=<value> …]` (compile to native executable; `--<name>=<value>` overrides compile-time constants), `knot fmt [--check] [--stdout] <file.knot> …` (in-place formatter; `--check` exits non-zero on diff, `--stdout` writes to stdout), and `knot help`. Object file is emitted next to the source then linked via `linker::link`; runtime is embedded via `build.rs` into `EMBEDDED_RUNTIME` and extracted to a temp file if `libknot_runtime.a` isn't found next to the compiler or via `KNOT_RUNTIME_LIB`.
8795
8896Key codegen patterns:
8997- All values are pointer-typed (`ptr_type`) in Cranelift IR — uniform representation
@@ -112,7 +120,7 @@ Key codegen patterns:
112120
113121## Supported Language Features
114122
115−Currently compiled: source declarations, type aliases, data declarations, functions, literals, records, field access, record updates, relation literals, nested relations (record fields holding `[T]` stored as JSON in SQLite, queryable via `m <- t.field` bind), binary/unary operations, if/else, do blocks (bind/where/yield/let/groupBy — pure comprehension do blocks are desugared to `__bind`/`__yield`/`__empty` calls that dispatch through Monad/Applicative/Alternative trait impls — works with any monad, not just `[]` (e.g., `Maybe`); monad type resolved at compile time via HKT unification; IO do blocks are excluded from desugaring and use the dedicated `compile_io_do` path; mixed/sequential do blocks use direct loop codegen; set-value do blocks preserved for SQL optimization; do blocks can appear in atom position as function arguments — `f do ...` and `f (do ...)` both work; `groupBy {key}` collects rows, groups by key, and rebinds the variable to each group sub-relation — field access on a group returns the first element's field, aggregates like `count` operate on the group), set expressions, lambdas, closures, function application, case expressions (with exhaustiveness checking for ADTs), constructors (including built-in `True {}`/`False {}` for `Bool` — interchangeable with `true`/`false` literals, usable in `case` patterns), atomic transactions, migrations (schema tracking + `migrate` blocks), schema lockfile (`<name>.schema.lock`), views (`*view = do { ... yield {...} }` with bidirectional read/write, constant column filtering, auto-fill on write), derived relations (`&name = expr` — read-only computed relations, compiled as 0-param functions recomputed on each access; recursive derived relations with self-references use Datalog-style fixpoint iteration via `knot_relation_fixpoint` runtime function, converging when the relation stabilizes), traits and impls (single-dispatch on runtime type tags for primitives and ADTs, default methods, deriving from defaults, supertrait enforcement, associated types with type-level pattern matching), trait bounds (`Display a => [a] -> [Text]` syntax — explicit constraints on function type signatures validated at call sites; trait method calls automatically carry constraints; multiple bounds via `Trait1 a => Trait2 a => Type`; compile-time error when a concrete type lacks a required impl), type inference (Hindley-Milner with row polymorphism for both records and variants, let-generalization, ADT/trait/view-aware, higher-kinded type polymorphism), higher-kinded types (trait params with kind annotations like `(f : Type -> Type)`, type-level application `f a` in method signatures, `[]` and ADT names as bare type constructors in impl args, `TyCon`/`App` internal representation with normalization-based unification), IO monad (`IO {effects} a` type for pure effect descriptions — all relation operations (`*rel`, `&rel`, `set`, `replace`) return `IO {} value` with empty effect set, external builtins like `println`, `readFile`, `now` return IO with specific effects; IO do-blocks sequence actions via bind; effects tracked in the type: `IO {console} {}`, `IO {fs} Text`, `IO {clock} Int`, `IO {} [T]` for DB ops; `knot_io_run` executes IO thunks at the top level in `main`), concurrency (`fork : ∀r. IO r {} -> IO r {}` fire-and-forget OS thread spawning with independent SQLite connections via WAL mode, deep-cloned environments — the spawned IO's effect row propagates through `fork`, so its effects remain visible in the caller's IO type; `race : ∀a b r. IO r a -> IO r b -> IO r (Result a b)` runs two IO actions concurrently with cooperative aggressive cancellation via per-thread `CancelToken` checked between IO thunks and inside `sleep`, winner is reported via the built-in `Result a b` ADT — `Err {error: a}` if left wins, `Ok {value: b}` if right wins; both args share an effect row that propagates to the result; `race` cannot appear inside `atomic`; `retry` for STM-style atomic retry — inside `atomic` blocks, causes rollback and waits for relation changes via Condvar with row-level read-filter wakeups so a watcher on `WHERE id = 1` is not woken by writes to `id = 2`, predicates support equality, ordered comparison (`<`/`<=`/`>`/`>=`), and `IN` sets; UPDATE/DELETE paths emit per-row `WriteEvent::Rows` so STM watchers can filter on those too; `atomic : IO {} a -> IO {} a` runs IO body in a transaction with savepoint-based retry loop, body must contain only DB interactions), pattern matching in do-bind (`Circle c <- *shapes` filters relation to matching constructors and destructures; `InProgress ip <- t.status` matches a single value and skips if no match), built-ins (println, print, show, union, count, countWhere, now, filter, match, map, fold, single, any, diff, inter, sum, avg, minOn, maxOn, min, max, toUpper, toLower, take, drop, length, trim, contains, reverse, chars, id, not, stripUnit/withUnit (Int<u> ↔ Int identity for unit conversion), stripFloatUnit/withFloatUnit (Float versions), toJson, parseJson, readFile, writeFile, appendFile, fileExists, removeFile, listDir, readLine, randomInt, randomFloat, randomUuid, fork, race, retry, sleep, logInfo/logWarn/logError/logDebug — leveled logging to stderr, colored when TTY / JSON otherwise; logDebug only emits when the program is run with `--debug` — IO-returning builtins produce `IO {effect} a` values; aggregates `count`/`countWhere`/`sum`/`avg`/`minOn`/`maxOn` push down to SQL (`COUNT(*)`/`COUNT(*) WHERE`/`SUM`/`AVG`/`MIN`/`MAX`) when applied to a source relation or its bound alias, including `filter`-then-aggregate, do-block-then-aggregate, and pipe forms (`*rel |> filter ... |> minOn ...`); `minOn`/`maxOn` use `knot_source_query_value` runtime which returns Int/Float/Text matching the SQLite column type and panics on empty relations; binary `min`/`max` (prelude functions, `Ord a => a -> a -> a`) operate on two values; modulo operator `%` dispatches through `Num.mod`, calls `knot_value_mod` at runtime (panics on division by zero), pushes down to SQLite's `%` operator inside SQL-compilable comprehensions; postfix type annotations on any expression (`expr : Type`) parse without surrounding parens — useful for unit ascription on literals; `Uuid` primitive type registered alongside `Bytes` in inference and resolution, stored as TEXT in SQLite, generated via `randomUuid : IO {random} Uuid` (RFC 9562 UUIDv7 — time-ordered, suitable as primary key); `Maybe` ships with built-in `Functor`/`Applicative`/`Monad`/`Alternative` impls in the prelude (parsed from `PRELUDE_SOURCE`, skipped when the user defines their own impl for the same `(trait, type)` pair via `user_impls` tracking in `inject_prelude`)), units of measure (`unit M`, `unit N = Kg * M / S^2` declarations — concrete units are uppercase; `Float<M>`, `Int<Usd>` type annotations; `42.0<M>` literal syntax; compile-time unit algebra — `+`/`-` require unit match, `*`/`/` compose units, negation preserves units; plain numeric types are unit-agnostic; unit polymorphism via lowercase unit variables in type annotations `Float<u> -> Float<u>` — lowercase names in `<...>` that are not declared units become polymorphic unit variables, freshened on each instantiation; units fully erased at runtime; all stdlib functions with numeric types are unit-polymorphic: `avg` as `∀a u. (a -> Float<u>) -> [a] -> Float<u>`, `sum`/`minOn`/`maxOn` fully polymorphic `(a -> b) -> [a] -> b` (units flow through naturally), `countWhere` as `∀a u. (a -> Bool) -> [a] -> Int<u>`, `randomInt` as `∀u. Int<u> -> IO {random} Int<u>` (preserves bound unit), `randomFloat` as `∀u. IO {random} Float<u>`, `now` as `IO {clock} Int<Ms>`, `sleep` as `Int<Ms> -> IO {clock} {}` (built-in `Ms` unit for clock ops), `count` as `∀a u. [a] -> Int<u>`, `length` as `∀u. Text -> Int<u>`, `take`/`drop` as `∀u. Int<u> -> Text -> Text`, `bytesLength`/`bytesSlice`/`bytesGet` also unit-polymorphic; derived unit aliases expand at use site; `(expr : Type)` annotation syntax for explicit unit ascription), refined types (`type Nat = Int where \x -> x >= 0` — base type restricted by predicate function; per-field `{age: Int where \x -> x >= 0}`, cross-field `{...} where \p -> p.lo <= p.hi`, ADT constructor `Circle {radius: Float where \r -> r > 0.0}` refinements; `refine expr` returns `Result RefinementError T` with target refined type inferred from context — `case refine x of Ok {value: n} -> ... | Err {error: e} -> ...`; refined types are nominal in inference (`Ty::Con(name, [])`) with subsumption in unification enabling `Nat ↔ Int` compatibility; `set` validates refined fields via `knot_refinement_validate_relation` before writes — panics on violation; route handlers auto-validate refined body fields after JSON decoding — return HTTP 400 on failure; `RefinementError = {typeName: Text, violations: [{field: Maybe Text, message: Text}]}`; `Result` has built-in monad impls via `knot_result_bind`/`knot_result_yield`/`knot_result_empty` for do-block support), effect inference (per-declaration effect tracking for reads/writes/console/clock/network/fs/random, IO-in-atomic validation, explicit annotation checking), subset constraints (`*orders.customer <= *people.name` for referential integrity, `*users <= *users.email` for uniqueness — validated at runtime before each write commits), routes (`route Name where` with HTTP method/path/body/query/response declarations, path prefix nesting, `route Name = A | B` composition, `headers {field: Type}` for typed request/response headers — request headers appear after query params and before `->`, response headers appear after the response type; header field names use camelCase and auto-convert to HTTP-Header-Case (`authorization` → `Authorization`, `contentType` → `Content-Type`); optional headers use `Maybe` type (`headers {authorization: Maybe Text}` — server gets `Nothing` if absent, `Just {value: "..."}` if present; fetch skips `Nothing` headers); request header fields become constructor fields like body/query/path params; routes desugar to ADTs whose constructors carry only request inputs (no `respond` field) — handlers are bound per-endpoint via `serve API where Ctor = handler` producing a value of type `Server API _` (a row variable when handlers have no concrete effects) or `Server API {effects}` (when handlers carry concrete effects), and each handler returns `Result HttpError T` where `T` is the response type declared in the route and `HttpError = {status: Int, message: Text}`. `Ok {value: T}` responds 200; `Err {error: {status, message}}` responds with the given status (clamped to 100..=599) and a JSON `{"error": message}` body. When response headers are declared, the success body is `{body: T, headers: H}` (still wrapped inside `Ok`); the listen loop extracts body for JSON and headers for HTTP response headers. Error responses do not carry custom headers. `listen : Int<u> -> Server a r -> IO {network | r} {}` starts a `tiny_http` server, dispatching requests to the appropriate handler with JSON request/response serialization; the row variable unifies with the server's effect row so handler effects (e.g. `console`) flow into the IO type of the program calling `listen`. `fetch url (Ctor {fields})` is a type-safe HTTP client reusing route declarations — response type inferred from route's `response:` annotation, returns `IO {network} (Result {status: Int, message: Text} ResponseType)` or `IO {network} (Result ... {body: ResponseType, headers: {h: T}})` when response headers declared. `fetchWith url {headers: [...]} (Ctor {fields})` variant merges ad-hoc headers with route-declared headers, uses `ureq` for HTTP requests. Per-endpoint rate limiting via the `rateLimit <expr>` clause placed after the response type/headers and before `=`: the expression has type `RateLimit input a = {key: input -> RequestCtx -> Maybe a, limit: {requests: Int, window: Int<Ms>}}` with `Ord a` enforced via a deferred constraint; `input` is the same record the handler receives (built from path params + query params + body fields + request headers — `route_input_record_ty` factored out of `serve_handler_type` for shared use); absence of the clause means unlimited; `key` returning `Nothing` exempts the request, `Just v` keys the bucket on `show v`. Token-bucket algorithm refilled lazily, persisted in `_knot_rate_limits (route TEXT, key TEXT, tokens REAL, last_refill INTEGER, PRIMARY KEY(route, key)) WITHOUT ROWID`, created on first use; check runs in `BEGIN IMMEDIATE` so concurrent same-key requests serialize. Codegen: parser introduces an `Option<Expr>` field on `RouteEntry`; type checker validates the expression in `check_rate_limit_expr` (called from `infer_declarations` Route arm); `can_start_type_atom` stops at `rateLimit` so the response type doesn't eat the keyword; runtime registers configs via `knot_route_set_rate_limit(table, ctor, rate_limit_value)` which unpacks the `{key, limit}` record; both the main_init route table loop and the `listen`/`listenOn` codegen path register them on their respective tables; check happens in the worker thread inside `catch_unwind`, after the input record is built but before refinement validation, so the user's `key_fn` sees the same destructured input the handler will. The worker's Err type is `(u16, String, Option<i64>)` where the third element is `retry_after_ms` — the response handler attaches `Retry-After: <seconds>` when present and uses `Content-Type: application/json` body `{"error":"..."}`; rate-limit rejections return `Err((429, "Rate limit exceeded", Some(retry_after_ms)))`).
123+Currently compiled: source declarations, type aliases, data declarations, functions, literals, records, field access, record updates, relation literals, nested relations (record fields holding `[T]` stored as JSON in SQLite, queryable via `m <- t.field` bind), binary/unary operations, if/else, do blocks (bind/where/yield/let/groupBy — pure comprehension do blocks are desugared to `__bind`/`__yield`/`__empty` calls that dispatch through Monad/Applicative/Alternative trait impls — works with any monad, not just `[]` (e.g., `Maybe`); monad type resolved at compile time via HKT unification; IO do blocks are excluded from desugaring and use the dedicated `compile_io_do` path; mixed/sequential do blocks use direct loop codegen; set-value do blocks preserved for SQL optimization; do blocks can appear in atom position as function arguments — `f do ...` and `f (do ...)` both work; `groupBy {key}` collects rows, groups by key, and rebinds the variable to each group sub-relation — field access on a group returns the first element's field, aggregates like `count` operate on the group), set expressions, lambdas, closures, function application, case expressions (with exhaustiveness checking for ADTs), constructors (including built-in `True {}`/`False {}` for `Bool` — interchangeable with `true`/`false` literals, usable in `case` patterns), atomic transactions, migrations (schema tracking + `migrate` blocks), schema lockfile (`<name>.schema.lock`), views (`*view = do { ... yield {...} }` with bidirectional read/write, constant column filtering, auto-fill on write), derived relations (`&name = expr` — read-only computed relations, compiled as 0-param functions recomputed on each access; recursive derived relations with self-references use Datalog-style fixpoint iteration via `knot_relation_fixpoint` runtime function, converging when the relation stabilizes), traits and impls (single-dispatch on runtime type tags for primitives and ADTs, default methods, deriving from defaults, supertrait enforcement, associated types with type-level pattern matching), trait bounds (`Display a => [a] -> [Text]` syntax — explicit constraints on function type signatures validated at call sites; trait method calls automatically carry constraints; multiple bounds via `Trait1 a => Trait2 a => Type`; compile-time error when a concrete type lacks a required impl), type inference (Hindley-Milner with row polymorphism for both records and variants, let-generalization, ADT/trait/view-aware, higher-kinded type polymorphism), higher-kinded types (trait params with kind annotations like `(f : Type -> Type)`, type-level application `f a` in method signatures, `[]` and ADT names as bare type constructors in impl args, `TyCon`/`App` internal representation with normalization-based unification), IO monad (`IO {effects} a` type for pure effect descriptions — all relation operations (`*rel`, `&rel`, `set`, `replace`) return `IO {} value` with empty effect set, external builtins like `println`, `readFile`, `now` return IO with specific effects; IO do-blocks sequence actions via bind; effects tracked in the type: `IO {console} {}`, `IO {fs} Text`, `IO {clock} Int`, `IO {} [T]` for DB ops; `knot_io_run` executes IO thunks at the top level in `main`), concurrency (`fork : ∀a r. IO {| r} a -> IO {| r} {}` fire-and-forget OS thread spawning with independent SQLite connections via WAL mode, deep-cloned environments — the spawned IO's effect row propagates through `fork`, so its effects remain visible in the caller's IO type; `race : ∀a b r1 r2. IO {| r1} a -> IO {| r2} b -> IO {| r1 \/ r2} (Result a b)` runs two IO actions concurrently with cooperative aggressive cancellation via per-thread `CancelToken` checked between IO thunks and inside `sleep`, winner is reported via the built-in `Result a b` ADT — `Err {error: a}` if left wins, `Ok {value: b}` if right wins; each arg carries its own effect row and the result row is the union of both (`r1 \/ r2`), so both sides' effects propagate to the caller; `race` cannot appear inside `atomic`; `retry` for STM-style atomic retry — inside `atomic` blocks, causes rollback and waits for relation changes via Condvar with row-level read-filter wakeups so a watcher on `WHERE id = 1` is not woken by writes to `id = 2`, predicates support equality, ordered comparison (`<`/`<=`/`>`/`>=`), and `IN` sets; UPDATE/DELETE paths emit per-row `WriteEvent::Rows` so STM watchers can filter on those too; `atomic : IO {} a -> IO {} a` runs IO body in a transaction with savepoint-based retry loop, body must contain only DB interactions), pattern matching in do-bind (`Circle c <- *shapes` filters relation to matching constructors and destructures; `InProgress ip <- t.status` matches a single value and skips if no match), built-ins (println, putLine (alias for println), print, show, union, count, countWhere, now, filter, match, map, fold, single, any, all, diff, inter, sum, avg, minOn, maxOn, min, max, toUpper, toLower, take, drop, length, trim, contains, elem, reverse, chars, sortBy, head, findFirst, upsertBy ((a -> Bool) -> a -> [a] -> [a] — replace matching rows or append if none match), when, unless, forEach, id, not, stripUnit/withUnit (Int<u> ↔ Int identity for unit conversion), stripFloatUnit/withFloatUnit (Float versions), toJson, parseJson, hash (BLAKE3, polymorphic over any value type — Bytes/Text hash raw contents, structured values hash a canonical serialization so equal logical values produce equal digests), generateKeyPair (X25519, IO {random}), generateSigningKeyPair (Ed25519, IO {random}), encrypt (sealed-box X25519+ChaCha20-Poly1305, IO {random}), decrypt, sign (Ed25519), verify (Ed25519), textToBytes, bytesToText (Maybe Text on UTF-8 error), bytesToHex, bytesFromHex/hexDecode, bytesConcat, bytesGet, bytesSlice, bytesLength, readFile, writeFile, appendFile, fileExists, removeFile, listDir, readLine, randomInt, randomFloat, randomUuid, fork, race, retry, sleep, logInfo/logWarn/logError/logDebug — leveled logging to stderr, colored when TTY / JSON otherwise; logDebug only emits when the program is run with `--debug` — IO-returning builtins produce `IO {effect} a` values; aggregates `count`/`countWhere`/`sum`/`avg`/`minOn`/`maxOn` push down to SQL (`COUNT(*)`/`COUNT(*) WHERE`/`SUM`/`AVG`/`MIN`/`MAX`) when applied to a source relation or its bound alias, including `filter`-then-aggregate, do-block-then-aggregate, and pipe forms (`*rel |> filter ... |> minOn ...`); `minOn`/`maxOn` use `knot_source_query_value` runtime which returns Int/Float/Text matching the SQLite column type and panics on empty relations; binary `min`/`max` (prelude functions, `Ord a => a -> a -> a`) operate on two values; modulo operator `%` dispatches through `Num.mod`, calls `knot_value_mod` at runtime (panics on division by zero), pushes down to SQLite's `%` operator inside SQL-compilable comprehensions; postfix type annotations on any expression (`expr : Type`) parse without surrounding parens — useful for unit ascription on literals; `Uuid` primitive type registered alongside `Bytes` in inference and resolution, stored as TEXT in SQLite, generated via `randomUuid : IO {random} Uuid` (RFC 9562 UUIDv7 — time-ordered, suitable as primary key); `Maybe` ships with built-in `Functor`/`Applicative`/`Monad`/`Alternative` impls in the prelude (parsed from `PRELUDE_SOURCE`, skipped when the user defines their own impl for the same `(trait, type)` pair via `user_impls` tracking in `inject_prelude`)), units of measure (`unit M`, `unit N = Kg * M / S^2` declarations — concrete units are uppercase; `Float<M>`, `Int<Usd>` type annotations; `42.0<M>` literal syntax; compile-time unit algebra — `+`/`-` require unit match, `*`/`/` compose units, negation preserves units; plain numeric types are unit-agnostic; unit polymorphism via lowercase unit variables in type annotations `Float<u> -> Float<u>` — lowercase names in `<...>` that are not declared units become polymorphic unit variables, freshened on each instantiation; units fully erased at runtime; all stdlib functions with numeric types are unit-polymorphic: `avg` as `∀a u. (a -> Float<u>) -> [a] -> Float<u>`, `sum`/`minOn`/`maxOn` fully polymorphic `(a -> b) -> [a] -> b` (units flow through naturally), `countWhere` as `∀a u. (a -> Bool) -> [a] -> Int<u>`, `randomInt` as `∀u. Int<u> -> IO {random} Int<u>` (preserves bound unit), `randomFloat` as `∀u. IO {random} Float<u>`, `now` as `IO {clock} Int<Ms>`, `sleep` as `Int<Ms> -> IO {clock} {}` (built-in `Ms` unit for clock ops), `count` as `∀a u. [a] -> Int<u>`, `length` as `∀u. Text -> Int<u>`, `take`/`drop` as `∀u. Int<u> -> Text -> Text`, `bytesLength`/`bytesSlice`/`bytesGet` also unit-polymorphic; derived unit aliases expand at use site; `(expr : Type)` annotation syntax for explicit unit ascription), refined types (`type Nat = Int where \x -> x >= 0` — base type restricted by predicate function; per-field `{age: Int where \x -> x >= 0}`, cross-field `{...} where \p -> p.lo <= p.hi`, ADT constructor `Circle {radius: Float where \r -> r > 0.0}` refinements; `refine expr` returns `Result RefinementError T` with target refined type inferred from context — `case refine x of Ok {value: n} -> ... | Err {error: e} -> ...`; refined types are nominal in inference (`Ty::Con(name, [])`) with subsumption in unification enabling `Nat ↔ Int` compatibility; `set` validates refined fields via `knot_refinement_validate_relation` before writes — panics on violation; route handlers auto-validate refined body fields after JSON decoding — return HTTP 400 on failure; `RefinementError = {typeName: Text, violations: [{field: Maybe Text, message: Text}]}`; `Result` has built-in monad impls via `knot_result_bind`/`knot_result_yield`/`knot_result_empty` for do-block support), effect inference (per-declaration effect tracking for reads/writes/console/clock/network/fs/random, IO-in-atomic validation, explicit annotation checking), subset constraints (`*orders.customer <= *people.name` for referential integrity, `*users <= *users.email` for uniqueness — validated at runtime before each write commits), routes (`route Name where` with HTTP method/path/body/query/response declarations, path prefix nesting, `route Name = A | B` composition, `headers {field: Type}` for typed request/response headers — request headers appear after query params and before `->`, response headers appear after the response type; header field names use camelCase and auto-convert to HTTP-Header-Case (`authorization` → `Authorization`, `contentType` → `Content-Type`); optional headers use `Maybe` type (`headers {authorization: Maybe Text}` — server gets `Nothing` if absent, `Just {value: "..."}` if present; fetch skips `Nothing` headers); request header fields become constructor fields like body/query/path params; routes desugar to ADTs whose constructors carry only request inputs (no `respond` field) — handlers are bound per-endpoint via `serve API where Ctor = handler` producing a value of type `Server API _` (a row variable when handlers have no concrete effects) or `Server API {effects}` (when handlers carry concrete effects), and each handler returns `Result HttpError T` where `T` is the response type declared in the route and `HttpError = {status: Int, message: Text}`. `Ok {value: T}` responds 200; `Err {error: {status, message}}` responds with the given status (clamped to 100..=599) and a JSON `{"error": message}` body. When response headers are declared, the success body is `{body: T, headers: H}` (still wrapped inside `Ok`); the listen loop extracts body for JSON and headers for HTTP response headers. Error responses do not carry custom headers. `listen : Int<u> -> Server a r -> IO {network | r} {}` starts a `tiny_http` server, dispatching requests to the appropriate handler with JSON request/response serialization; the row variable unifies with the server's effect row so handler effects (e.g. `console`) flow into the IO type of the program calling `listen`. `fetch url (Ctor {fields})` is a type-safe HTTP client reusing route declarations — response type inferred from route's `response:` annotation, returns `IO {network} (Result {status: Int, message: Text} ResponseType)` or `IO {network} (Result ... {body: ResponseType, headers: {h: T}})` when response headers declared. `fetchWith url {headers: [...]} (Ctor {fields})` variant merges ad-hoc headers with route-declared headers, uses `ureq` for HTTP requests. Per-endpoint rate limiting via the `rateLimit <expr>` clause placed after the response type/headers and before `=`: the expression has type `RateLimit input a = {key: input -> RequestCtx -> Maybe a, limit: {requests: Int, window: Int<Ms>}}` with `Ord a` enforced via a deferred constraint; `input` is the same record the handler receives (built from path params + query params + body fields + request headers — `route_input_record_ty` factored out of `serve_handler_type` for shared use); absence of the clause means unlimited; `key` returning `Nothing` exempts the request, `Just v` keys the bucket on `show v`. Token-bucket algorithm refilled lazily, persisted in `_knot_rate_limits (route TEXT, key TEXT, tokens REAL, last_refill INTEGER, PRIMARY KEY(route, key)) WITHOUT ROWID`, created on first use; check runs in `BEGIN IMMEDIATE` so concurrent same-key requests serialize. Codegen: parser introduces an `Option<Expr>` field on `RouteEntry`; type checker validates the expression in `check_rate_limit_expr` (called from `infer_declarations` Route arm); `can_start_type_atom` stops at `rateLimit` so the response type doesn't eat the keyword; runtime registers configs via `knot_route_set_rate_limit(table, ctor, rate_limit_value)` which unpacks the `{key, limit}` record; both the main_init route table loop and the `listen`/`listenOn` codegen path register them on their respective tables; check happens in the worker thread inside `catch_unwind`, after the input record is built but before refinement validation, so the user's `key_fn` sees the same destructured input the handler will. The worker's Err type is `(u16, String, Option<i64>)` where the third element is `retry_after_ms` — the response handler attaches `Retry-After: <seconds>` when present and uses `Content-Type: application/json` body `{"error":"..."}`; rate-limit rejections return `Err((429, "Rate limit exceeded", Some(retry_after_ms)))`).
116124
117125## Version Control
118126
DESIGN.md+29−26
@@ -740,10 +740,10 @@ loadConfig = \path -> do
740740
741741#### `fork`
742742
743−`fork` runs an IO action on a new OS thread. It is fire-and-forget — the forked action runs independently, but its effects are still visible in the caller's IO type. Each spawned thread gets its own SQLite connection (WAL mode enables concurrent access). The main thread waits for all spawned threads before exiting.
743+`fork` runs an IO action on a new OS thread. It is fire-and-forget — the forked action runs independently, but its effects are still visible in the caller's IO type. The spawned action can return any value (the result is discarded). Each spawned thread gets its own SQLite connection (WAL mode enables concurrent access). The main thread waits for all spawned threads before exiting.
744744
745745```knot
746−fork : IO r {} -> IO r {}
746+fork : IO {| r} a -> IO {| r} {}
747747```
748748
749749The spawned action's effect row `r` propagates through `fork` to the caller — a program that forks an IO that calls `println` is visibly typed with `{console}` in its IO row, so the effect system still reflects what the program can do. Do blocks can be passed as arguments without parentheses: `fork do ...`.
@@ -798,10 +798,15 @@ SQLite WAL mode ensures that concurrent readers and writers do not block each ot
798798
799799#### `race`
800800
801−`race` runs two IO actions concurrently and returns as soon as one wins. Both arguments share an effect row so any effects required by either side propagate 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.
801+`race` runs two IO actions concurrently and returns as soon as one wins. Each
802+argument carries its own effect row; the result IO's row is the union of both,
803+written `r1 \/ r2`. Any effects required by either side propagate into the
804+result IO. The winner is reported via the built-in `Result a b` ADT —
805+`Err {error: a}` when the left action wins, `Ok {value: b}` when the right
806+action wins.
802807
803808```knot
804−race : IO r a -> IO r b -> IO r (Result a b)
809+race : IO {| r1} a -> IO {| r2} b -> IO {| r1 \/ r2} (Result a b)
805810```
806811
807812```knot
@@ -1564,39 +1569,37 @@ data Shape
15641569
15651570#### Relation Constraints
15661571
1567−Source declarations support both value predicates and relational predicates:
1572+Source declarations carry refinement predicates for each field; cross-relation
1573+constraints (referential integrity, uniqueness) are written as top-level
1574+**subset constraints** with `<=`:
15681575
15691576```knot
15701577*people : [{
15711578 name: Text where \s -> length s > 0,
1572− age: Int where \x -> x >= 0
1579+ age: Int where \x -> x >= 0,
1580+ email: Text where \s -> contains "@" s
15731581}]
1574− where \p -> p.age >= 13 || p.email == "" -- cross-field value predicate
15751582
15761583*orders : [{customer: Text, amount: Int where \x -> x > 0}]
1577− where .customer in *people.email -- relational membership
1578− where unique .email -- uniqueness
1579− where \o -> o.amount <= 1000000 -- value predicate
1580−```
15811584
1582−Two relational constraint forms:
1585+-- Referential integrity: every orders.customer must appear in people.email
1586+*orders.customer <= *people.email
15831587
1584−- `.field in *rel.field` — every value of `.field` must appear in the referenced relation's field (referential integrity)
1585−- `unique .field` — field values must be unique across all rows
1588+-- Uniqueness: people.email values must not duplicate (relation-on-itself form)
1589+*people <= *people.email
1590+```
15861591
1587−These replace the old subset constraint syntax:
1592+Two subset-constraint shapes:
15881593
1589−```knot
1590−-- Old (removed):
1591−*orders.customer <= *people.name
1592−*users <= *users.email
1593−
1594−-- New:
1595−*orders : [...] where .customer in *people.name
1596−*users : [...] where unique .email
1597−```
1594+| Form | Meaning |
1595+|------|---------|
1596+| `*sub.field <= *sup.field` | Every value of `sub.field` must appear in `sup.field` (foreign key) |
1597+| `*rel <= *rel.field` | Field values are unique within `rel` |
15981598
1599−All constraints — field, cross-field, relational — are enforced by the Knot runtime at `set` boundaries. Nothing is pushed to SQLite. The runtime mediates all writes, so there is no escape hatch.
1599+Field-level and cross-field refinements are enforced row-by-row before each
1600+`set` commits. Subset constraints are enforced by runtime triggers maintained
1601+on the underlying SQLite tables. Either failure mode panics with a refinement
1602+error or a constraint-violation message.
16001603
16011604#### Subtyping
16021605
@@ -1697,7 +1700,7 @@ trySet : *[a] -> [a] -> IO {} (Result RefinementError {})
16971700
16981701Predicates in type-level refinements must be **pure** — no IO, no relation references. They are ordinary Knot functions with no restrictions on what pure operations they use (recursion, pattern matching, higher-order functions, etc.).
16991702
1700−Predicates in relation `where` clauses follow the same rule — they are pure functions over individual rows. Relational constraints (`.field in`, `unique`) are separate syntactic forms, not predicates.
1703+Predicates in relation `where` clauses follow the same rule — they are pure functions over individual rows. Relational constraints (subset and uniqueness via `<=`) are separate top-level declarations, not predicates.
17011704
17021705#### Interaction with Units
17031706
README.md+17
@@ -131,6 +131,23 @@ declarations. See `examples/modules/`.
131131JSON encode/decode, file I/O, leveled logging (`logInfo`/`logWarn`/...),
132132crypto (`generateKeyPair`/`encrypt`/`sign`/`verify`).
133133
134+**Runtime CLI.** Every compiled program accepts a common set of flags and
135+subcommands for free:
136+
137+```sh
138+./my_program # run main
139+./my_program --debug # turn on logDebug output
140+./my_program --help # print usage + any compile-time overrides
141+./my_program --http-max-body-bytes=32M
142+./my_program db # browse the .db file in a TUI
143+./my_program api MyRouteName # print OpenAPI 3.0 spec for a `route`
144+./my_program --my-flag=value # override a compile-time constant
145+```
146+
147+Constant overrides can also be supplied at build time
148+(`knot build foo.knot --my-flag=value`). The compiler also ships with
149+`knot fmt [--check] [--stdout] <file.knot>` for in-place formatting.
150+
134151## Project Structure
135152
136153```
base.md+5−5
@@ -27,12 +27,12 @@ show : a -> Text
2727Convert any value to its text representation. Records print as `{field: value, ...}`, relations as `[v1, v2, ...]`, constructors as `Tag {fields}`. This is a pure function (no IO).
2828
2929```knot
30−fork : IO r {} -> IO r {}
30+fork : IO {| r} a -> IO {| r} {}
3131```
3232Run an IO action on a new OS thread (fire-and-forget). The spawned action's effect row propagates through `fork`, so its effects remain visible in the caller's IO row. Each thread gets its own SQLite connection (WAL mode). Main waits for all threads before exiting.
3333
3434```knot
35−race : IO r a -> IO r b -> IO r (Result a b)
35+race : IO {| r1} a -> IO {| r2} b -> IO {| r1 \/ r2} (Result a b)
3636```
3737Run two IO actions concurrently and return the winner. The result is `Err {error: a}` if the left action wins, `Ok {value: b}` if the right wins. Both arguments share an effect row, so effects from either side propagate to the caller. Cancellation is cooperative but aggressive — the loser unwinds at its next IO thunk boundary and `sleep` wakes immediately on a cancel signal.
3838
@@ -494,7 +494,7 @@ case bytesFromHex "6869" of
494494```knot
495495hash : a -> Bytes
496496```
497−SHA-256 hash of any value, returned as 32 bytes. `Bytes` and `Text` hash their raw contents; records, relations, and constructors hash a canonical serialisation, so the same logical value always produces the same digest.
497+BLAKE3 hash of any value, returned as 32 bytes. `Bytes` and `Text` hash their raw contents; records, relations, and constructors hash a canonical serialisation, so the same logical value always produces the same digest.
498498
499499```knot
500500bytesToHex (hash "hello") -- "2cf24dba..."
@@ -608,7 +608,7 @@ route Api where
608608## Concurrency
609609
610610```knot
611−fork : IO r {} -> IO r {}
611+fork : IO {| r} a -> IO {| r} {}
612612```
613613Run an IO action on a new OS thread. Fire-and-forget — the spawned thread runs independently, but its effect row propagates through `fork` so the spawned action's effects remain visible in the caller's IO type. Each thread gets its own SQLite connection (WAL mode). Main waits for all threads before exiting. Do blocks can be passed without parentheses: `fork do ...`.
614614
@@ -621,7 +621,7 @@ main = do
621621```
622622
623623```knot
624−race : IO r a -> IO r b -> IO r (Result a b)
624+race : IO {| r1} a -> IO {| r2} b -> IO {| r1 \/ r2} (Result a b)
625625```
626626Run two IO actions concurrently and return the winner. The arguments share an effect row so effects from either side flow through to the caller's IO. The winner is reported via the built-in `Result` ADT — `Err {error: a}` when the left action wins, `Ok {value: b}` when the right wins.
627627
knot.md+5−5
@@ -632,7 +632,7 @@ The body of `atomic` must be an IO expression containing only DB operations. Ext
632632Fire-and-forget: runs an IO action on a new OS thread. Each thread gets its own SQLite connection (WAL mode).
633633
634634```knot
635−fork : IO r {} -> IO r {}
635+fork : IO {| r} a -> IO {| r} {}
636636
637637main = do
638638 fork do
@@ -672,7 +672,7 @@ conservatively.
672672#### `race`
673673
674674```knot
675−race : IO r a -> IO r b -> IO r (Result a b)
675+race : IO {| r1} a -> IO {| r2} b -> IO {| r1 \/ r2} (Result a b)
676676```
677677
678678Run two IO actions concurrently and return the winner. Both arguments share a
@@ -807,8 +807,8 @@ and the loser unwinds at its next safe point.
807807| `randomFloat` | `IO {random} Float<u>` | Random float `[0.0, 1.0)`, unit-polymorphic |
808808| `randomUuid` | `IO {random} Uuid` | Generate a RFC 9562 UUIDv7 |
809809| `atomic` | `IO {} a -> IO {} a` | Run DB operations in a transaction |
810−| `fork` | `IO r {} -> IO r {}` | Fire-and-forget on new OS thread; effects propagate |
811−| `race` | `IO r a -> IO r b -> IO r (Result a b)` | Run two IO actions, return the winner |
810+| `fork` | `IO {\| r} a -> IO {\| r} {}` | Fire-and-forget on new OS thread; effects propagate |
811+| `race` | `IO {\| r1} a -> IO {\| r2} b -> IO {\| r1 \/ r2} (Result a b)` | Run two IO actions, return the winner; effect rows union |
812812| `retry` | `a` | Rollback and wait (inside `atomic` only) |
813813| `when` | `Bool -> IO r {} -> IO r {}` | Run action when condition is true |
814814| `unless` | `Bool -> IO r {} -> IO r {}` | Run action when condition is false |
@@ -833,7 +833,7 @@ program.
833833| `bytesGet` | `Int<u1> -> Bytes -> Int<u2>` | Byte value (0–255) at index |
834834| `bytesToHex` | `Bytes -> Text` | Hex encode |
835835| `bytesFromHex` | `Text -> Maybe Bytes` | Hex decode (`Nothing` on bad input) |
836−| `hash` | `a -> Bytes` | SHA-256 hash (32 bytes) of any value |
836+| `hash` | `a -> Bytes` | BLAKE3 hash (32 bytes) of any value |
837837
838838### Cryptography
839839
stdlib.md+63−8
@@ -293,6 +293,23 @@ reverse : [a] -> [a]
293293
294294Reverse iteration order of a relation. Like `sortBy`, this controls iteration order; the underlying set is unchanged.
295295
296+### `upsertBy`
297+
298+```
299+upsertBy : (a -> Bool) -> a -> [a] -> [a]
300+```
301+
302+Replace every element matching the predicate with the supplied value. If no
303+element matches, append the value. Useful for "insert or update" patterns on
304+source relations.
305+
306+```knot
307+-- Bump or insert a per-user counter
308+bump = \user counters -> upsertBy (\c -> c.user == user)
309+ {user: user, count: lookup user counters + 1}
310+ counters
311+```
312+
296313---
297314
298315## Concurrency
@@ -300,10 +317,15 @@ Reverse iteration order of a relation. Like `sortBy`, this controls iteration or
300317### `fork`
301318
302319```
303−fork : IO r {} -> IO r {}
320+fork : IO {| r} a -> IO {| r} {}
304321```
305322
306−Run an IO action on a new OS thread (fire-and-forget). The spawned action's effect row `r` 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.
323+Run an IO action on a new OS thread (fire-and-forget). The spawned action can
324+return any value `a` (it is discarded) and may have any effect row `r`. That
325+effect row propagates through `fork` to the caller, so a program that forks an
326+IO performing `println` is visibly typed with `{console}` in its IO row. Each
327+thread gets its own SQLite connection via WAL mode for safe concurrent access.
328+The main thread waits for all spawned threads before exiting.
307329
308330```knot
309331main = do
@@ -319,10 +341,12 @@ Do blocks can be passed directly as arguments without parentheses.
319341### `race`
320342
321343```
322−race : IO r a -> IO r b -> IO r (Result a b)
344+race : IO {| r1} a -> IO {| r2} b -> IO {| r1 \/ r2} (Result a b)
323345```
324346
325−Run two IO actions concurrently and return the winner. Both arguments share a single effect row, so any effects required by either side flow into the result IO.
347+Run two IO actions concurrently and return the winner. Each argument carries
348+its own effect row; the result IO's row is the union of both (written
349+`r1 \/ r2`). Effects required by either side flow into the result IO.
326350
327351The 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.
328352
@@ -490,7 +514,7 @@ has = contains "ell" "hello" -- True
490514println : a -> IO {console} {}
491515```
492516
493−Print a value to stdout followed by a newline.
517+Print a value to stdout followed by a newline. `putLine` is an alias.
494518
495519### `print`
496520
@@ -509,7 +533,12 @@ logError : a -> IO {console} {}
509533logDebug : a -> IO {console} {}
510534```
511535
512−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.
536+Leveled logging to stderr (so output does not mix with `println` on stdout).
537+When stderr is a TTY, output is colored; otherwise each record is written as
538+one JSON line for log aggregators. `logDebug` only emits when the program is
539+launched with `--debug` — debug records are dropped silently otherwise. (Every
540+compiled Knot program accepts `--debug` automatically; see
541+[Runtime CLI](#runtime-cli).)
513542
514543```knot
515544main = do
@@ -777,10 +806,12 @@ Extract a sub-range. Arguments: start index, length, bytes.
777806hash : a -> Bytes
778807```
779808
780−SHA-256 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.
809+BLAKE3 hash of any value, returned as 32 bytes. `Bytes` and `Text` hash their
810+raw contents; structured values (records, relations, constructors) hash a
811+canonical serialisation, so equal logical values always produce equal digests.
781812
782813```knot
783−bytesToHex (hash "hello") -- "2cf24dba..."
814+bytesToHex (hash "hello") -- "ea8f163..."
784815```
785816
786817---
@@ -1125,3 +1156,27 @@ maxOn (\t -> t.distance) *trips -- Float<M> if distance : Float<M>
11251156| `&&` | — | Boolean AND (direct) |
11261157| `\|\|` | — | Boolean OR (direct) |
11271158| `\|>` | — | Pipe-forward (`x \|> f` = `f x`) |
1159+
1160+---
1161+
1162+## Runtime CLI
1163+
1164+Every compiled Knot program accepts a common set of runtime flags and
1165+subcommands without any user wiring:
1166+
1167+| Argument | Description |
1168+|----------|-------------|
1169+| `--debug` | Enable `logDebug` output; without this flag, `logDebug` calls are dropped silently. |
1170+| `--help` | Print usage including any compile-time overrides exposed by the program. |
1171+| `--http-max-body-bytes=N` | Cap HTTP request and response bodies. Suffixes: `K`, `M`, `G`. Default `16M`. Applies to both `listen` and `fetch`. |
1172+| `--<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>`. |
1173+| `<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. |
1174+| `<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. |
1175+
1176+The compiler itself (`knot`) supports:
1177+
1178+| Command | Description |
1179+|---------|-------------|
1180+| `knot build <file.knot> [-o <path>] [--<name>=<value> …]` | Compile to a native executable. Overrides supply compile-time constants. |
1181+| `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. |
1182+| `knot help` | Show CLI usage. |