@@ -526,12 +526,17 @@ Grouping is executed via SQLite — key columns are inserted into a temp table a
526526
527527## Effects and IO
528528
529−### Unified Effect Model
529+### Unified IO Model
530530
531−All state operations in Knot return IO values. The IO type carries an effect set that distinguishes DB operations from external effects:
531+All state operations in Knot return IO values. `IO a` is a single type
532+constructor taking one argument — the result type — with no effect-row
533+parameter:
532534
533−- **DB operations** return `IO {} value` — the empty effect set `{}` indicates pure database interaction with no external side effects. Source refs (`*rel`), derived refs (`&rel`), and relation writes (`*rel = expr`, `replace *rel = expr`) all return `IO {} value`.
534−- **External effects** carry specific tags: `IO {}`, `IO Text`, `IO Result`, `IO Int Ms`, `IO Float 1`.
535+- **DB operations** return `IO value`. Source refs (`*rel`), derived refs
536+ (`&rel`), and relation writes (`*rel = expr`, `replace *rel = expr`) all
537+ return `IO value`.
538+- **External effects** are also plain `IO`: `IO {}`, `IO Text`, `IO Result`,
539+ `IO (Int Ms)`, `IO Float 1`.
535540
536541This unified model means all stateful code lives in IO do-blocks, while pure comprehensions over plain values remain non-IO.
537542
@@ -540,18 +545,15 @@ This unified model means all stateful code lives in IO do-blocks, while pure com
540545Effectful functions return descriptions of effects (`IO a`) rather than performing them. IO values are thunks that execute when run.
541546
542547```knot
543−-- DB operations return IO with empty effects
548+-- DB operations return IO
544549*people : [Person]
545−-- *people : IO {} [Person]
550+-- a read of *people yields IO [Person]
546551
547−-- println returns an IO action with console effect
548552println : a -> IO {}
549553
550−-- readFile returns an IO action with fs effect
551554readFile : Text -> IO Text
552555
553−-- now returns an IO action with clock effect, tagged with the built-in Ms unit
554−now : IO Int Ms
556+now : IO (Int Ms)
555557```
556558
557559### IO Do-Blocks
@@ -622,11 +624,8 @@ If the body uses a capability not listed in the signature, the compiler rejects
622624
623625### IO and Transactions
624626
625−`atomic` takes an IO body and runs it in a transaction. The body must only contain DB interactions (empty effect set) — no external IO (console, fs, etc.) is allowed inside `atomic`.
626−
627−```knot
628−atomic : IO {} a -> IO {} a
629−```
627+`atomic` takes an IO body and runs it in a transaction. `atomic do ...` is a
628+keyword form (not a `base.` function); the block is an `IO a`.
630629
631630```knot
632631-- DB writes go in `atomic`, IO happens after commit
@@ -727,7 +726,7 @@ loadConfig \path -> do
727726fork : IO a -> IO {}
728727```
729728
730−The 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 ...`.
729+`fork` spawns an IO action on a new OS thread and returns `IO {}`. Do blocks can be passed as arguments without parentheses: `fork do ...`.
731730
732731```knot
733732*counter : [{n: Int 1}]
@@ -780,10 +779,8 @@ SQLite WAL mode ensures that concurrent readers and writers do not block each ot
780779
781780#### `race`
782781
783−`race` runs two IO actions concurrently and returns as soon as one wins. Each
784−argument carries its own effect row; the result IO's row is the union of both,
785−written `r1 \/ r2`. Any effects required by either side propagate into the
786−result IO. The winner is reported via the built-in `Result a b` ADT —
782+`race` runs two IO actions concurrently and returns as soon as one wins. The
783+winner is reported via the built-in `Result a b` ADT —
787784`Err {error: a}` when the left action wins, `Ok {value: b}` when the right
788785action wins.
789786
@@ -859,7 +856,7 @@ api (serve Api where)
859856main (listen 8080 api)
860857```
861858
862−`serve API where` produces a value of type `Server API _` (a polymorphic row variable when handlers have no concrete effects) or `Server API {effects}` when handlers carry concrete effects — e.g. `Server API {console}` if a handler calls `println`. Each handler receives the request record (path/query/body/header fields) and returns `Result HttpError T`, where `T` is the response type declared on the endpoint and `HttpError = {status: Int 1, message: Text}`. Handler effects propagate through `listen` into the program's IO type. No string routes, no untyped params, no missing handlers.
859+`serve API where` produces a value of type `Server API`. Each handler receives the request record (path/query/body/header fields) and returns `Result HttpError T`, where `T` is the response type declared on the endpoint and `HttpError = {status: Int 1, message: Text}`. `listen : Int u -> Server a -> IO {}` binds the server to a port. No string routes, no untyped params, no missing handlers.
863860
864861#### HTTP Status Codes
865862
@@ -1008,7 +1005,7 @@ CREATE TABLE _knot_rate_limits (
10081005
10091006The check runs in a `BEGIN IMMEDIATE` transaction so concurrent requests for the same client serialize correctly; different keys do not contend.
10101007
1011−**Effects.** Rate limiting reads and writes a hidden table; these effects are internal and not surfaced in user-visible effect rows.
1008+**Effects.** Rate limiting reads and writes a hidden internal table.
10121009
10131010#### Path Prefixes
10141011
@@ -1853,7 +1850,7 @@ type-directedly and would silently match the wrong conversion. See
18531850
18541851### Type Inference
18551852
1856−Full Hindley-Milner style inference extended with row polymorphism (record fields, effect rows, and unit variables) and implicit-dictionary constraints. Type signatures are always optional — the compiler infers everything from usage.
1853+Full Hindley-Milner style inference extended with row polymorphism (record fields and unit variables) and implicit-dictionary constraints. Type signatures are always optional — the compiler infers everything from usage.
18571854
18581855## Full Example
18591856