08 Boken om Ridgun
Tensorer, kompilerat RMM-minne (RMM1/RMM2), regioner och system
Hittills har vi sett den mest vänliga, eleganta och matematiska sidan av Ridgun: rena funktioner, algebraiska datatyper, mönstermatchning, beslutsträd med | / |. och parallella ||-grafer med -> som utnyttjar alla kärnor i din processor utan att kräva ett enda ynka Mutex.
Vid det här laget brukar den erfarna systemprogrammeraren längst bak i salen räcka upp handen med ett snett leende och fråga:
"Allt det här är jättefint för att räkna på fakturor eller kompilera AST-träd, men vad händer när jag behöver multiplicera matriser med miljontals flyttal i svindlande fart? Vad händer med minnet? Ligger det en skräpsamlare (Garbage Collector) gömd under mattan och väntar på sämsta tänkbara ögonblick för att frysa mitt program i 200 millisekunder? Och hur pratar jag med operativsystemet Unix på riktigt?"
Utmärkta frågor. Låt oss lyfta på motorhuven till Ridgun och se hur det beter sig när det är dags att kavla upp ärmarna i systemprogrammering, numeriska beräkningar och deterministiskt minne.
8.1. Minnesdilemmat: varken Garbage Collector eller bråk med Borrow Checker (RMM1 och RMM2)
I de moderna språkens värld brukar det finnas två oförenliga läger:
- Garbage Collector-lägret (Java, Go, Python, JS, Haskell, OCaml): du begär glatt mer minne och då och då stannar en osynlig sopbil all trafik i ditt program för att spåra vilka objekt som fortfarande lever och vilka som är döda. Det är hur bekvämt som helst... ända tills du bygger ett blixtsnabbt systemverktyg eller en server med strikta svarstider och sopbilen bestämmer sig för att dubbelparkera.
- Manuell kontroll / Borrow Checker-lägret (C, C++, Rust): det finns ingen skräpsamlare, men i gengäld måste du hantera pekare för hand (med risk för
Segmentation faulteller use-after-free) eller ägna en halv eftermiddag åt att förhandla med borrow checker om varför'ainte lever lika länge som'b.
Ridgun löser detta dilemma från grunden med Ridgun Memory Management (RMM), ett deterministiskt system med två kompletterande policyer (RMM1 och RMM2) kombinerat med lexikala regioner (with region):
Policy RMM1 — Kompilerad minneshantering (Linjär och villkorlig CMM)
När kompilatorn statiskt känner till flödet av lokala variabler inuti en funktion:
- Linjär CMM:
ridguncberäknar vid kompileringstillfället vilken som är den sista raden där varje lokal variabel (a,b, ...) läses och skjuter direkt efter den in dess deterministiska frigörning (rg_cmm_drop), eller överför ägarskapet utan kopiering (rg_cmm_move) när variabeln returneras som resultat. - Villkorlig CMM (
ptr(a) != ptr(c)): men vad händer om en funktion somvalj cond a breturneraraellerbberoende på ett villkor vid körning? Då byggerridguncupp härkomsttabellen och genererar en pekaridentitetsvakt på 1 CPU-cykel: ombslutar användas medancfortfarande lever, kontrollerar denif ptr(b) != ptr(c) { free(b) }. Den gren som inte valdes frigörs omedelbart och den valda överlever intakt.
Policy RMM2 — Tilldelningsräkning och strukturell kaskad
Och vad händer när du stoppar in strukturer i andra strukturer (xs = [a, b]), tilldelar om muterbara variabler (mut x = ...) eller kedjar ihop komplexa anrop? Där kliver RMM2 in och styr hela livscykeln med fyra vattentäta matematiska regler:
- Inkommande funktionsargument rörs inte (
+0vid inträde,-0vid utträde): att skicka en lista eller en post till en funktion ökar eller minskar aldrig några räknare i onödan. - Varje tilldelning till en intern lokal variabel eller behållarcell lägger till
+1:
a = [10, 20]$\rightarrow$ blocketM1föds med räknaren1.xs = [a, b]$\rightarrow$ listanxsföds med1, och eftersom den sparar referenser tillaochbstiger båda till2.- Om du tilldelar om en muterbar variabel (
mut x = ...och sedanx = ny), läggerRMM2först på+1på det nya värdet innan den drar av-1från det gamla.
- När funktionen avslutas får lokala variabler som INTE returneras
-1: om en strukturs räknare når0frigörs dess minne på fläcken och sprider-1i kaskad, ett i taget, till vart och ett av dess direkta barn. - Värdet som returneras av funktionen undantas under transporten: det får sitt
-1som lokal variabel men förstörs inte när det passerarreturn, så att när anroparen skriverc = min funktion (), lämnar tilldelningens+1räknaren på exakt1.
valj Bool [Int] [Int] => [Int]
valj cond a b =
| cond = a
| otherwise = b
exempel rmm2 Bool => Int
exempel rmm2 cond =
a = [10, 20, 30] -- M1 (rc = 1)
b = [40, 50, 60] -- M2 (rc = 1)
c = valj cond a b -- `c` får +1; den som inte valdes dör precis här (rc -> 0)
(rakna a) + (rakna c) -- Vid slutet får `a` och `c` -1 och 0 levande block återstår
8.2. Lexikala regioner med kvot (with region): Minnesbrandväggar i $O(1)$
Utöver RMM1 och RMM2 kan du, när du bearbetar ett externt nätverkspaket, en enorm fil eller ett parti opålitliga data och vill sätta ett strikt tak för RAM-minnet, kapsla in beräkningen i ett with region-block:
bearbeta ram String => Result
bearbeta ram radata =
with region 64_KB do
rena = map rensa rad (dela rader radata)
rakna giltiga rena
- Garanterad kvot i hårdvara/körtidsmiljö: om något inuti blocket försöker reservera ens en enda byte över
64_KB(65 536byte) avbryter körtidsmiljön blocket snyggt, frigör hela den tillfälliga arenan på konstant tid $O(1)$ och returnerarErr OutOfMemoryutan att krascha processen. - Omedelbar frigörning vid utträde: så fort
with region-blocket avslutas försvinner allt tillfälligt minne i arenan i ett enda slag utan att lämna någon fragmentering.
8.3. Inbyggda tensorer, Einsteinkontraktion (@) och symboliska dimensioner
Om du någonsin har programmerat i Python med NumPy eller PyTorch har du garanterat råkat ut för det klassiska felet efter 41 minuters körning:
RuntimeError: mat1 and mat2 shapes cannot be multiplied (128x64 and 32x128)
I Ridgun är Tensor förstklassiga inbyggda medborgare och deras dimensioner är en del av typsignaturen:
-- Om (N × K) och (K × M) går in, måste det ovillkorligen komma ut (N × M)!
multiplicera matriser Tensor[N, K] Tensor[K, M] => Tensor[N, M]
multiplicera matriser a b = a_ik @ b_kj
Lägg märke till två spektakulära detaljer:
- Statisk dimensionsverifiering (
Tensor[N, K]): den inre axelnKi den första matrisen måste stämma överens med den första axelnKi den andra. Om du försöker skicka in inkompatibla dimensioner stopparridgunc checkdig innan något ens körs. - Einsteins indexnotation (
a_ik @ b_kj) och Hadamard (a_ij * b_j):
- Med
@kontraheras (summeras) de index som upprepas i alla operander (k), medan de unika (i,j) överlever i resultattensorn. - Med
+,-,*,/justerar indexen dimensionerna och utför automatisk broadcasting (a_ij * b_jmultiplicerar varje radiiamed vektornb_jutan att du behöver skrivaunsqueezeellerreshape).
- Högpresterande typade tensorer (
Tensor[U64 * N]) och mutering in-place medt_(i, j): I kryptografiska system, fysiksimuleringar eller linjär algebra kan du deklarera platta tensorer över ett sammanhängande block av teckenlösa 64-bitars heltal (Tensor[U64 * 4],Tensor[U64 * 8 * 16]), operera parallellt med^(inbyggd block-XOR) och läsa eller skriva element på $O(1)$-tid med den uttryckliga indexsyntaxent_(i)ellert_(i, j):
skapa tillstandsblock => Tensor[U64 * 2 * 2]
skapa tillstandsblock =
mut m = tensor fill [2, 2] (u64 0)
m_(0, 1) = u64 0x6a09e667f3bcc908
m
8.4. Hårdvaruåtkomst (hardware ::) och POSIX-systemprimitiver
Själva kompilatorn ridgunc, bygghanteraren stable och Unix-systemverktyg är skrivna i Ridgun. För detta innehåller språket minnesmappade registerblock (hardware ::) och direkta POSIX-primitiver:
hardware :: UART0 @ 0x1000_0000
tx_data : Int @ 0x00
status : Int @ 0x04
Och för att arbeta med operativsystemet:
| Primitiv | Signatur | Vad den gör |
|---|---|---|
cli args | => [String] | Kommandoradsargument (argv). |
read file / write file | String => String / String String => () | Läsning och skrivning av hela filer på disk. |
read bytes / write bytes | String => [Int] / String [Int] => () | Exakt binär läsning och skrivning byte för byte (0..255). |
list dir / file lstat | String => [String] / String => ... | Listning av kataloger och inspektion av POSIX-metadata (lstat). |
sha256 file / sha256 str / sha256 raw | String => String | Inbyggd kryptografisk SHA-256-hash (64 tecken hexadecimalt eller rått 32-bytes binärt sammandrag med sha256 raw / sha256 bytes). |
sys exec / shell exec | ... => Int | Körning av operativsystemprocesser. |
sys getcwd / sys chdir | => String / String => Int | Fråga efter eller byt aktuell arbetskatalog. |
sys getenv / sys setenv | String => String / String String => Int | Processens miljövariabler. |
sys getuid / sys getgid | => Int | Processens faktiska UID och GID (doas, säkerhetsverktyg). |
sys exit | Int => () | Avsluta processen med returkod. |
8.5. Ekosystemet av standardbibliotek (ridgun-libs)
Utöver kompilatorns primitiver innehåller Ridguns officiella ekosystem 32 rena bibliotek (.rglib) skrivna helt i Ridgun som du kan importera med import <namn> i vilket projekt som helst:
| Kategori | Tillgängliga bibliotek | Vad de tillhandahåller |
|---|---|---|
| Samlingar, text och data | collections, set, string, regex, utf8, fmt, csv, json, toml, yaml, xml, html, markdown, base64, hex, compress, sql | Avbildningar, mängder, reguljära uttryck, strukturerade tolkar/serialiserare, kodning och frågemotorer. |
| Matematik, tid och slumptal | math, random, datetime, uuid, log, cli, bench, test | Matematiska funktioner, deterministisk och systembaserad PRNG, ISO-8601-datum, UUID, testramverk och prestandamätningar med clock_gettime. |
| System, nätverk och webb | fs, path, process, env, net, http, uri | Högnivåfilsystem, sökvägshantering, underprocesser, TCP/UDP-uttag samt HTTP-klient och -server. |
Kryptografi och säkerhet (crypto) | crypto (sha256, hmac, hkdf, pbkdf2, argon2id, chacha20, aes, x25519, ed25519, csprng) | Kryptografisk svit verifierad mot officiella RFC/NIST-vektorer, implementerad över U64, Tensor[U64 * ...] och sha256 raw med tupel-inlining utan heap-allokeringar. |
8.6. Verklig fallstudie: Vi bygger rg-wc, ett parallellt Unix-verktyg
Låt oss knyta ihop allt vi har lärt oss från Kapitel 01 fram till hit i ett riktigt systemprogram: en parallell version av Unix-verktyget wc (rad- och byteräknare för flera filer) skriven i ren Ridgun:
-- 1. Vi modellerar mätvärdena för en fil med en ADT-post (Kapitel 06)
type Matvarden = fil: String, rader: Int, bytes: Int
-- 2. Vi räknar radbrytningar (ASCII-kod 10) med TCO-svansrekursion (Kapitel 04)
fn :: rakna radbrytningar String Int Int Int => Int
rakna radbrytningar text pos slut ack =
| pos >= slut = ack
| (str char at text pos) == 10 = rakna radbrytningar text (pos + 1) slut (ack + 1)
| otherwise = rakna radbrytningar text (pos + 1) slut ack
fn :: antal rader String => Int
antal rader text = rakna radbrytningar text 0 (str len text) 0
-- 3. Vi analyserar en fil och returnerar dess Matvarden-struktur
fn :: analysera fil String => Matvarden
analysera fil sokvag =
innehall = read file sokvag
|| antal rader innehall -> n_rader
|| str len innehall -> n_bytes ||.
Matvarden sokvag n_rader n_bytes
-- 4. Vi formaterar varje rad och summerar totaler med `fold` och `||` (Kapitel 07)
fn :: formatera matvarde Matvarden => String
formatera matvarde m = "{rader m}\t{bytes m}\t{fil m}"
fn :: summera rader Int Matvarden => Int
summera rader acc m = acc + (rader m)
fn :: summera bytes Int Matvarden => Int
summera bytes acc m = acc + (bytes m)
fn :: generera rapport [String] => String
generera rapport sokvagar =
matvarden = ||map analysera fil sokvagar
|| map formatera matvarde matvarden -> rader_ut
|| fold summera rader 0 matvarden -> total_r
|| fold summera bytes 0 matvarden -> total_b ||.
"{rader_ut}\nTotalt: {total_r} rader, {total_b} byte"
fn :: main => ()
main =
echo (generera rapport ["README.md", "AGENTS.md"])
Stanna upp en sekund och titta på dessa ~40 rader kod:
- Det finns inte ett enda
null, inget okontrollerat undantag och ingetMutex. rakna radbrytningarkompileras till en inbyggdloopmed 0 byte stackminne tack vare automatisk TCO.||map analysera fil sokvagarbearbetar filerna parallellt på alla kärnor i din processor, och||-blocket beräknar samtidigt de formaterade raderna, totalt antal rader och totalt antal byte.- Det tillfälliga minnet frigörs deterministiskt via
RMM1/RMM2. - Och när du kör
ridgunc build rg_wc.rg -o rg-wcfår du en fristående inbyggd binär på ~350 KB, färdig att kopieras till/usr/local/bin.
8.7. Slutlig färdplan: Ridgun-programmerarens 10 gyllene regler
- Skriv alltid kontraktet (
=>) först och ekvationerna (=) sedan: att tänka på typerna först får kompilatorn att arbeta åt dig. - Låt
stable formatformge dina tabeller: skriv med ett mellanslag och låt verktyget rada upp kolumner,=>,=och->. - Kom ihåg den universella regeln för
=: efter vilket=som helst kan du öppna ett sekventiellt block, ett vaktträd|eller en parallell graf||. - Använd
|.och||.endast när du stänger ett nästlat block för att återvända till en högre nivå: håll djupa beslutsträd kompakta och entydiga. - Oföränderligt som standard (
x = ...), uttryckligt föränderligt (mut x = ...): användmutendast när en lokal ackumulator verkligen kräver det. - Använd tydliga booleska operatorer i gemener (
and,or,xor,not) och avsluta alltid dina vakter med| otherwise =. - Modellera din domän med ADT (
type): använd poster medwith falt: vardeoch summatyper (Option,Result) i stället förnull. - Parallellisera genom att deklarera beroenden med
|| expr -> varsamt||map/||filter: låt kompilatorn verifiera att det inte finns några cykler och fördela arbetet mellan kärnorna. - Sätt symboliska dimensioner på dina tensorer (
Tensor[N, K]) och index på dina kontraktioner (a_ik @ b_kj). - Lita på
RMM(RMM1/RMM2), avgränsa extern indata medwith regionoch verifiera alltid medstable checkochstable test.
Lycka till på resan med att programmera i Ridgun!