31 Boken om Ridgun
Lib:en io — Typade IoResult-fel, with timeout-I/O och Processer
31.1. Varför finns lib:en io? (Det verkliga problemet och Ridguns filosofi)
”Varför behöver jag en
io-lib skriven från grunden i ren Ridgun i stället för att bara länka in ett gammalt C-bibliotek?”
Det är den första fråga varje systemingenjör ställer sig när man möter io. Och svaret sammanfattar hela filosofin bakom ridgun-libs:
Låt oss vara ärliga: ett read()-anrop eller en barnprocess utan tidsgräns (timeout) är en öppen inbjudan för en enda hängande resurs att tömma alla arbetare i din server.
Lib:en io modellerar varje systemoperation med explicita ADT:er (IoError, IoResult[A]), kombinerar icke-blockerande ppoll med Ridguns lexikala with timeout wait_ms do-kvoter och erbjuder mixfix-operatorerna read fd ... timeout och write fd ... timeout.
I ett traditionellt ekosystem innebär uppgifter inom domänen IO ofta att man drar in externa C-beroenden, brottas med råa pekare, ogenomskinliga typkonverteringar, okontrollerade undantag eller oförutsägbara skräpsamlarpauser. I Ridgun löser lib:en io (src/main/ridgun/lib/io.rg) problemet genom fyra kompromisslösa principer:
- Deklarera
import io(ellerimport io as alias) överst i filen: varje lib definierar sinmodule :: io-namnrymd och i--strict-läge anropas flerordsfunktioner alltid med naturliga mellanslag. - Kedja dina datapipelines från vänster till höger med
->: skrivindata steg ett -> steg två -> resultat, använd(+ _ _ +)-sekvensmasker för att packa upp tupler och utelämna_ =vid sekventiella satser (eller kasta med-> ()). - Svansrekursion med TCO (Tail-Call Optimization): alla interna genomlöpningar i
iokompileras till inbyggda hårdvaruloopar med $O(1)$ stackutrymme. - Deterministiskt minne (
RMM1/RMM2): varje mellanliggande struktur frigörs i exakt det ögonblick dess sista läsning avslutas, helt utan Garbage Collector.
31.2. Inbyggda artefakter, import och komplett API-katalog för io
I ridgun-libs är io en förstklassig lib placerad i src/main/ridgun/lib/io.rg (domän IO). När du bygger med stable bundle paketeras den i tre inbyggda format:
| Artefakt | Sökväg | Syfte |
|---|---|---|
| Ridgun-bundle | target/bundles/io.rglib | Portabel distribution och direkt import med import io |
| Kompilerat arkiv | target/io.rgc | Typkontrollerad mellanartefakt verifierad av ridgunc |
| Delat ELF64-objekt | target/libio.so | Inbyggt dynamiskt bibliotek helt utan C- eller libc-beroenden |
Varje lib deklarerar sin egen namnrymd med
module :: io: skriv baraimport io(ellerimport io as alias) överst och anropa dess funktioner med naturliga mellanslag (min funktion x) och->-pipelines (x min funktion -> resultat).
31.2.1. Översikt över funktionella familjer i io
| Signatur / Funktion / Typ | Vad den gör och när den används |
|---|---|
type IoError / type IoResult[A] | `IoNotFound |
io read with timeout / io write with timeout | Icke-blockerande läsning/skrivning på deskriptor begränsad av wait_ms |
read fd N max M timeout T / write fd N data S timeout T | Ergonomiska mixfix-operatorer (op ::) för tidsbegränsad deskriptor-I/O |
io read file / io write file / io read lines | Säkra fil- och radströmsfunktioner som returnerar IoResult |
exec with timeout : String -> Int -> IoResult[ProcStatus] | Kör ett externt kommando begränsat av en lexikal with timeout-kvot |
31.2.2. Komplett katalog över typer och signaturer i src/main/ridgun/lib/io.rg
Nedan följer hela förteckningen över algebraiska datatyper (type), mixfix-operatorer (op ::) och funktionssignaturer (fn ::) som definieras i src/main/ridgun/lib/io.rg (2 typer, 2 operatorer och 42 funktioner):
Algebraiska datatyper och poster (type) i io:
type IoError = Timeout Int | Eof | SysErr Int String | NotFound String
type IoResult = String | Int | Timeout Int | Eof | SysErr Int String | NotFound String | IoErr IoError | IoOk String | IoTimeout | IoEof | IoError Int
Mixfix-operatorer (op ::) i io:
op :: read fd Int max Int timeout Int => IoResult
op :: write fd Int data String timeout Int => IoResult
| # | Funktion / Signatur (fn ::) | Returtyp |
|---|---|---|
1 | fn :: format io error IoError | String |
2 | fn :: format io result IoResult | String |
3 | fn :: map read bytes String | IoResult |
4 | fn :: read fd after poll PollStatus Int Int Int | IoResult |
5 | fn :: read fd Int Int Int | IoResult |
6 | fn :: map write rc Int | IoResult |
7 | fn :: write fd after poll PollStatus Int String Int | IoResult |
8 | fn :: write fd Int String Int | IoResult |
9 | fn :: has null char String Int Int | Bool |
10 | fn :: pad ms three Int | String |
11 | fn :: format timeout sec Int | String |
12 | fn :: map exec rc Int Int | IoResult |
13 | fn :: io exec timeout String Int | IoResult |
14 | fn :: exec with timeout String Int | IoResult |
15 | fn :: read existing or empty String | String |
16 | fn :: append file String String | Int |
17 | fn :: file exists String | Bool |
18 | fn :: is file String | Bool |
19 | fn :: is dir String | Bool |
20 | fn :: is symlink String | Bool |
21 | fn :: extract size (Bool String Int Int Int String String Int String) | Int |
22 | fn :: file size String | Int |
23 | fn :: extract mtime (Bool String Int Int Int String String Int String) | Int |
24 | fn :: file mtime String | Int |
25 | fn :: extract mode (Bool String Int Int Int String String Int String) | Int |
26 | fn :: file mode String | Int |
27 | fn :: io read file safe String | IoResult |
28 | fn :: split lines loop String Int Int Int | [String] |
29 | fn :: split non empty lines String Int | [String] |
30 | fn :: io read lines String | [String] |
31 | fn :: str list is empty [String] | Bool |
32 | fn :: join lines nl [String] | String |
33 | fn :: io write lines String [String] | Int |
34 | fn :: io wrap chunk String | IoResult |
35 | fn :: io read after poll Int Int | IoResult |
36 | fn :: io read poll step Int | IoResult |
37 | fn :: io read with timeout String Int | IoResult |
31.3. Djupdykning i io: Steg-för-steg-exempel för varje funktion
Det räcker inte med en signaturtabell: låt oss utforska varje funktionalitet i lib:en io genom progressiva, körbara Ridgun-exempel. Vi börjar med två tabellbaserade vardagsrecept och går därefter igenom alla funktionella scenarier, transformationer och gränsfall som verifieras i src/test/ridgun/test io.rg.
31.3.1. Exempel 1: Huvudflöde i tabellform med io
Lägg märke till hur =-tecknen och transformationsstegen med io linjeras som en ren tabell utan onödiga muterbara variabler eller dolda sidoeffekter:
import io
fn :: leer peticion segura Int => String
leer peticion segura fd =
read fd fd max 4096 timeout 250 -> res
res format io result
31.3.2. Exempel 2: Säker komposition och hantering av gränsfall
Varje funktion i lib:en io är utformad för att kombineras sömlöst med booleska vakter | och med övriga 32 libs i ridgun-libs:
import io
fn :: describir fallo => String
describir fallo =
format io error (Timeout 250)
31.3.3. Exempel 3: Utbyggbar IoError-hierarki (Timeout, Eof, InvalidInput, PermissionDenied, NotFound, PosixErrno) och io error message-formatering
I detta praktiska exempel undersöker vi grundligt hur io beter sig inom detta funktionsområde, inklusive standardindata, parameterkombinationer och gränsvärden:
fn :: test io error formatting => ()
test io error formatting =
assert str eq (format io error (Timeout 500)) "timeout after 500 ms" "format Timeout 500"
assert str eq (format io error Eof) "unexpected end of file" "format Eof"
assert str eq (format io error (SysErr 9 "poll")) "syscall error 9 in poll" "format SysErr"
assert str eq (format io error (NotFound "/missing")) "file not found: /missing" "format NotFound"
()
31.3.4. Exempel 4: Tidsbegränsad läsning och skrivning på deskriptorer (read fd ... timeout, write fd ... timeout, with timeout) och Eof-detektering
I detta praktiska exempel undersöker vi grundligt hur io beter sig inom detta funktionsområde, inklusive standardindata, parameterkombinationer och gränsvärden:
fn :: test io pipe timeout and eof => ()
test io pipe timeout and eof =
pt = posix pipe
rc = pipe rc3 pt
rfd = pipe rfd3 pt
wfd = pipe wfd3 pt
assert eq rc 0
-- 1. Empty pipe read with 50 ms timeout must time out within [40, 300) ms (TS-PERF-001 AC2)
t3 = sys monotonic ms
r_to = read fd rfd max 32 timeout 50
t4 = sys monotonic ms
dt = t4 - t3
assert str eq (format io result r_to) "err:timeout after 50 ms" "empty pipe read Timeout 50"
assert true (dt >= 40 and dt < 300) "dt in [40, 300): {dt}"
-- 2. Write payload via mixfix `write fd ... data ... timeout ...`
w_res = write fd wfd data "stream-ok" timeout 100
assert str eq (format io result w_res) "ok:int:9" "write fd returns 9 bytes"
-- 3. Read payload via mixfix `read fd ... max ... timeout ...`
r_ok = read fd rfd max 64 timeout 100
assert str eq (format io result r_ok) "ok:str:stream-ok" "read fd returns stream-ok"
-- 4. Close write end and verify subsequent read returns Eof
posix close wfd -> ()
r_eof = read fd rfd max 64 timeout 100
assert str eq (format io result r_eof) "err:unexpected end of file" "closed pipe read returns Eof"
posix close rfd -> ()
-- 5. Bad fd / invalid max_bytes
r_bad = read fd (0 - 1) max 64 timeout 50
assert str eq (format io result r_bad) "err:syscall error 9 in read" "negative fd read SysErr 9"
r_zero = read fd rfd max 0 timeout 50
assert str eq (format io result r_zero) "err:syscall error 22 in read" "zero max_bytes read SysErr 22"
()
31.3.5. Exempel 5: Säker körning av underprocesser med strikt tidsgräns (io exec timeout) och automatisk städning av barnprocessen
I detta praktiska exempel undersöker vi grundligt hur io beter sig inom detta funktionsområde, inklusive standardindata, parameterkombinationer och gränsvärden:
fn :: test io exec timeout => ()
test io exec timeout =
ok_res = io exec timeout "true" 500
assert str eq (format io result ok_res) "ok:int:0" "io exec timeout true succeeds"
to_res = io exec timeout "sleep 2" 40
assert str eq (format io result to_res) "err:timeout after 40 ms" "io exec timeout sleep 2 times out"
()
31.3.6. Exempel 6: Filsystemspredikat (file exists, is dir, is regular file, file size), radvis läsning (io read lines) och filkopiering (copy file)
I detta praktiska exempel undersöker vi grundligt hur io beter sig inom detta funktionsområde, inklusive standardindata, parameterkombinationer och gränsvärden:
fn :: test io file and metadata => ()
test io file and metadata =
pid = sys getpid
dpath = "/tmp/rg_io_dir_{pid}"
fpath = "{dpath}/file.txt"
spath = "{dpath}/sym.txt"
bpath = "{dpath}/bytes.bin"
lpath = "{dpath}/lines.txt"
sys rmdir dpath -> ()
posix mkdir dpath -> ()
assert true (is dir dpath) "is dir dpath"
assert false (is file dpath) "not is file dpath"
-- 1. write file + append file + read file
write file fpath "hello "
w2 = append file fpath "world"
assert eq w2 0
assert str eq (read file fpath) "hello world" "write_file + append_file content"
-- 2. write_bytes + read_bytes
write bytes bpath [65, 66, 67, 0, 255]
rb = read bytes bpath
assert eq (int list len rb) 5
-- 3. symlink + metadata predicates
assert true (sys symlink fpath spath) "sys symlink fpath spath"
assert true (file exists fpath) "file exists fpath"
assert true (is file fpath) "is file fpath"
assert false (is symlink fpath) "not is symlink fpath"
assert true (is symlink spath) "is symlink spath"
assert eq (file size fpath) 11
assert true ((file mtime fpath) > 1700000000) "file mtime > 1700000000"
assert true ((file mode fpath) > 0) "file mode > 0"
assert false (file exists "/tmp/rg_io_nonexistent_999999.txt") "missing file exists False"
-- 4. io read file safe + io write lines / io read lines
miss = io read file safe "/tmp/rg_io_missing_999.txt"
assert str eq (format io result miss) "err:file not found: /tmp/rg_io_missing_999.txt" "NotFound missing file"
hit = io read file safe fpath
assert str eq (format io result hit) "ok:str:hello world" "safe read existing file"
io write lines lpath ["alpha", "beta", "gamma"] -> ()
lines = io read lines lpath
assert eq (str list len lines) 3
-- 5. v0.9.5 PAL `io read with timeout` (`with timeout wait_ms do` + `sys open path 2048 0` + `sys poll fd 1 -1`)
r_amb = io read with timeout fpath 200_ms
assert str eq (format io result r_amb) "ok:str:hello world" "io read with timeout 200_ms"
r_miss = io read with timeout "/tmp/rg_io_missing_999.txt" 50_ms
assert true (str starts with (format io result r_miss) "err:io error -") "io read with timeout missing file -> IoError"
-- Cleanup
sys unlink spath -> ()
sys unlink fpath -> ()
sys unlink bpath -> ()
sys unlink lpath -> ()
sys rmdir dpath -> ()
()
31.4. Under huven: hur io är byggd i ren Ridgun (src/main/ridgun/lib/io.rg)
En av de största fördelarna med ridgun-libs är att det inte finns några svarta lådor i C. Du kan öppna src/main/ridgun/lib/io.rg och läsa exakt hur varje operation är implementerad med mönstermatchning, | / |.-vaktträd och svansrekursion (TCO). Låt oss titta på nyckeldelarna i den faktiska implementationen:
31.4.1. Intern implementation: Extensible I/O Error & Result Sum Types (§5.1)
Se hur io.rg deklarerar explicita fn :: ... => ...-kontrakt först och löser varje fall genom linjerade ekvationer och svansanrop:
module :: io
type IoError = Timeout Int | Eof | SysErr Int String | NotFound String
type IoResult = String | Int | Timeout Int | Eof | SysErr Int String | NotFound String | IoErr IoError | IoOk String | IoTimeout | IoEof | IoError Int
fn :: format io error IoError => String
format io error (Timeout ms) = "timeout after {ms} ms"
format io error Eof = "unexpected end of file"
format io error (SysErr c op_name) = "syscall error {c} in {op_name}"
format io error (NotFound path) = "file not found: {path}"
format io error _ = "unknown io error"
fn :: format io result IoResult => String
format io result (String s) = "ok:str:{s}"
format io result (IoOk s) = "ok:str:{s}"
format io result (Int n) = "ok:int:{n}"
format io result (Timeout ms) = "err:timeout after {ms} ms"
format io result IoTimeout = "err:timeout"
format io result Eof = "err:unexpected end of file"
format io result IoEof = "err:unexpected end of file"
format io result (IoError c) = "err:io error {c}"
format io result (SysErr c op_name) = "err:syscall error {c} in {op_name}"
format io result (NotFound path) = "err:file not found: {path}"
format io result (IoErr err) = "err:" + (format io error err)
format io result _ = "err:unknown io error"
31.4.2. Intern implementation: Timeout-Bounded Command Execution (§5.2)
Se hur io.rg deklarerar explicita fn :: ... => ...-kontrakt först och löser varje fall genom linjerade ekvationer och svansanrop:
fn :: has null char String Int Int => Bool
has null char s idx slen =
| idx >= slen = False
| (str char at s idx) == 0 = True
| otherwise = has null char s (idx + 1) slen
fn :: pad ms three Int => String
pad ms three rem =
| rem < 10 = "00{rem}"
| rem < 100 = "0{rem}"
| otherwise = "{rem}"
fn :: format timeout sec Int => String
format timeout sec ms =
secs = ms / 1000
rem = ms % 1000
p = pad ms three rem
"{secs}.{p}s"
fn :: map exec rc Int Int => IoResult
map exec rc rc timeout_ms =
| rc == 124 or rc == 137 = Timeout timeout_ms
| rc < 0 = SysErr (0 - rc) "exec"
| otherwise = Int rc
fn :: io exec timeout String Int => IoResult
io exec timeout cmd timeout_ms =
| (str len cmd) == 0 or (has null char cmd 0 (str len cmd)) = SysErr 22 "exec"
| timeout_ms <= 0 = Timeout timeout_ms
| otherwise =
tspec = format timeout sec timeout_ms
rc = shell exec "timeout -k 0.01s {tspec} sh -c \"{cmd}\" >/dev/null 2>&1"
map exec rc rc timeout_ms
fn :: exec with timeout String Int => IoResult
exec with timeout cmd timeout_ms =
io exec timeout cmd timeout_ms
31.4.3. Intern implementation: Safe File Read & Line-Stream Helpers
Se hur io.rg deklarerar explicita fn :: ... => ...-kontrakt först och löser varje fall genom linjerade ekvationer och svansanrop:
fn :: io read file safe String => IoResult
io read file safe path =
| not (is safe posix path path) = SysErr 22 "read_file"
| not (lstat exists (file lstat path)) = NotFound path
| otherwise = String (read file path)
fn :: split lines loop String Int Int Int => [String]
split lines loop s idx start slen =
| idx >= slen =
| start == slen = []
| otherwise = [str slice s start slen] |.
| (str char at s idx) == 10 =
line = str slice s start idx
[line] :: (split lines loop s (idx + 1) (idx + 1) slen)
| otherwise =
split lines loop s (idx + 1) start slen
fn :: split non empty lines String Int => [String]
split non empty lines raw rlen =
| rlen == 0 = []
| otherwise = split lines loop raw 0 0 rlen
fn :: io read lines String => [String]
io read lines path =
| not (is safe posix path path) = []
| not (lstat exists (file lstat path)) = []
| otherwise =
raw = read file path
split non empty lines raw (str len raw)
fn :: str list is empty [String] => Bool
str list is empty [] = True
str list is empty (_:_) = False
fn :: join lines nl [String] => String
join lines nl [] = ""
join lines nl (line:rest) =
| str list is empty rest = line + "\n"
| otherwise = line + "\n" + (join lines nl rest)
fn :: io write lines step String [String] => Int
io write lines step path lines =
lines join lines nl -> body
write file path body
0
fn :: io write lines String [String] => Int
io write lines path lines =
| not (is safe posix path path) = 0 - 22
| otherwise = io write lines step path lines
31.4.4. Intern implementation: Ridgun v0.9.5 PAL & Ambient Deadline io read with timeout (§3.1)
Se hur io.rg deklarerar explicita fn :: ... => ...-kontrakt först och löser varje fall genom linjerade ekvationer och svansanrop:
fn :: io wrap chunk String => IoResult
io wrap chunk "" = IoEof
io wrap chunk s = IoOk s
fn :: io close and return Int IoResult => IoResult
io close and return fd res =
sys close fd -> ()
res
fn :: io read chunk and close Int => IoResult
io read chunk and close fd =
sys read fd fd 65536 -> chunk
sys close fd -> ()
chunk io wrap chunk
fn :: io read after poll Int Int => IoResult
io read after poll fd ev =
| ev == 0 = io close and return fd IoTimeout
| ev < 0 = io close and return fd (IoError ev)
| otherwise = io read chunk and close fd
fn :: io poll and read Int => IoResult
io poll and read fd =
sys poll fd 1 (0 - 1) -> ev
io read after poll fd ev
fn :: io read poll step Int => IoResult
io read poll step fd =
| fd < 0 = IoError fd
| otherwise = io poll and read fd
fn :: io read with timeout String Int => IoResult
io read with timeout path wait_ms =
with timeout wait_ms do
fd = sys open path 2048 0
io read poll step fd
31.5. Algoritmiska garantier, RMM1/RMM2-minne och de 5 Gyllene Reglerna för io
- Algoritmisk garanti och säkerhet: Ingen läsning, skrivning eller barnprocess som körs via
iomed enwait_ms-kvot kan någonsin blockera den anropande tråden eller aktören på obestämd tid. - $O(1)$ stackkomplexitet med TCO: Alla rekursiva loopar i
src/main/ridgun/lib/io.rgligger i svansposition, såridgunckompilerar dem till inbyggda hopp utan stacktillväxt. - Deterministiskt minne (
RMM1/RMM2): Noll skräpsamlarpauser; alla temporära strukturer frigörs deterministiskt när räckvidden lämnas. - Kontinuerlig verifiering: Statiskt kontrollerad med
stable checkoch verifierad av den automatiserade testsvitensrc/test/ridgun/test io.rg.
De 5 Gyllene Reglerna när du programmerar med io
- Deklarera
import ioöverst i filen och utnyttja att mellanslag och understreck är utbytbara så att koden läses som naturlig prosa. - Linjera dina datapipelines med
=: kedja sammanio-anrop i namngivna mellansteg och låtstable formatlinjera kolumnerna. - Utnyttja
io:s totala funktioner i dina|-vakter: kontrollera returvärden eller packa upp ADT:er med mönstermatchning i stället för att anta att extern indata alltid är giltig. - Undvik att kopiera stora strukturer när du bara läser ett fält: kom ihåg att i
RMM2medför inargument noll referensräkningskostnad (+0/-0). - Kombinera
iomedtest(import test): lås alltid fast dina domäninvarianter medassert equalsellerassert true.
I Kapitel 32 går vi vidare och utforskar lib:en crypto på djupet (Lib:en crypto — SHA-256, HMAC, PBKDF2, Argon2id och ChaCha20-Poly1305 AEAD).