Capítulo 08: Tensores, Memoria Compilada RMM (RMM1/RMM2), Rexións e Sistemas
Ata aquí vimos o lado máis amable, elegante e matemático de Ridgun: funcións puras, tipos alxébricos, pattern matching, árbores de decisión con | / |. e grafos paralelos || con -> que aproveitan todos os núcleos do teu procesador sen pedirche nin un triste Mutex.
Chegados a este punto, o programador veterano de sistemas adoita erguer a man ao fondo da aula con medio sorriso e preguntar:
"Todo isto é precioso para calcular facturas ou compilar ASTs, pero que pasa cando preciso multiplicar matrices de millóns de flotantes a velocidade de vertixe? Que pasa coa memoria? Hai un colector de lixo (Garbage Collector) agochado debaixo da alfombra agardando o peor momento para conxelar o meu programa durante 200 milisegundos? E como falo co sistema operativo Unix de verdade?"
Excelentes preguntas. Imos levantar o capó de Ridgun para ver como se las gasta cando toca baixar ao barro dos sistemas, o cálculo numérico e a memoria determinista.
8.1. O dilema da memoria: nin Garbage Collector nin pelexas co Borrow Checker (RMM1 e RMM2)
No mundo das linguaxes modernas adoita haber dous bandos irreconciliables:
- O bando do Garbage Collector (Java, Go, Python, JS, Haskell, OCaml): ti pides memoria alegremente e, de cando en vez, un camión do lixo invisible detén o tráfico do teu programa para rastrexar que obxectos seguen vivos e cales non. É comodísimo... ata que estás facendo unha ferramenta de sistema ultrarrápida ou un servidor con latencias estritas e o camión decide aparcar en dobre fila.
- O bando do control manual / Borrow Checker (C, C++, Rust): non hai colector de lixo, pero a cambio tes que xestionar punteiros á man (xogándote un
Segmentation faultou un use-after-free) ou pasar media tarde negociando co borrow checker por que'anon vive tanto coma'b.
Ridgun resolve este dilema de raíz con Ridgun Memory Management (RMM), un sistema determinista de dúas políticas complementarias (RMM1 e RMM2) combinado con rexións léxicas (with region):
Política RMM1 — Xestión de Memoria Compilada (CMM Lineal e Condicional)
Cando o compilador coñece estaticamente o fluxo das variables locais dentro dunha función:
- CMM Lineal:
ridgunccalcula en tempo de compilación cal é a última liña na que se le cada variable local (a,b, ...) e insire xusto despois a súa liberación determinista (rg_cmm_drop), ou transfire a súa propiedade sen copias (rg_cmm_move) cando a variable se devolve como resultado. - CMM Condicional (
ptr(a) != ptr(c)): e se unha función comoescoller cond a bdevolveaoubsegundo unha condición en tempo de execución?ridguncconstrúe a táboa de devanceiros e emite unha garda de identidade de punteiros de 1 ciclo de CPU: sebdeixa de usarse mentrescsegue viva, comprobaif ptr(b) != ptr(c) { free(b) }. A rama non escollida libérase ao instante e a escollida sobrevive intacta.
Política RMM2 — Conta por Asignación e Fervenza Estrutural
E que ocorre cando metes estruturas dentro doutras estruturas (xs = [a, b]), reasignas variables mutables (mut x = ...) ou encadeas chamadas complexas? Aí entra RMM2, que goberna todo o ciclo de vida con catro regras matemáticas sen fisuras:
- Os argumentos de entrada dunha función non se tocan (
+0ao entrar,-0ao saír): pasar unha lista ou un rexistro a unha función xamais incrementa nin decrementa contadores en balde. - Toda asignación a unha variable local interna ou cela de contedor suma
+1:
a = [10, 20]$\rightarrow$ o bloqueM1nace con contador1.xs = [a, b]$\rightarrow$ a listaxsnace con1, e como garda referencias aaeb, ambos soben a2.- Se reasignas unha variable mutable (
mut x = ...e logox = novo),RMM2aplica primeiro+1ao valor novo antes de aplicar-1ao antigo.
- Ao saír da función, as variables locais que NON se devolven reciben
-1: se o contador dunha estrutura chega a0, libérase a súa memoria no acto e propaga-1en fervenza, un por un, a cada un dos seus fillos directos. - O valor devolto pola función exímese en tránsito: recibe o seu
-1de variable local pero non se destrúe ao cruzar oreturn, de xeito que cando quen chamou faic = mina funcion (), ese+1da asignación déixao exactamente en1.
escoller Bool [Int] [Int] => [Int]
escoller cond a b =
| cond = a
| otherwise = b
exemplo rmm2 Bool => Int
exemplo rmm2 cond =
a = [10, 20, 30] -- M1 (rc = 1)
b = [40, 50, 60] -- M2 (rc = 1)
c = escoller cond a b -- `c` recibe +1; o non escollido morre aquí mesmo (rc -> 0)
(contar a) + (contar c) -- Ao rematar, `a` e `c` reciben -1 e todo queda en 0 bloques vivos
8.2. Rexións Léxicas con Cota (with region): Devasas de memoria en $O(1)$
Ademais de RMM1 e RMM2, cando estás procesando un paquete de rede externo, un ficheiro xigante ou un lote de datos non confiables e queres poñer un teito estrito de memoria RAM, envolves o cálculo nun bloque with region:
procesar trama String => Result
procesar trama cru =
with region 64_KB do
limpas = map limpar lina (dividir linas cru)
contar validos limpas
- Cota garantida en hardware/runtime: se dentro do bloque se intenta reservar nin un só byte por riba de
64_KB(65.536bytes), o runtime aborta limpamente o bloque, libera toda a area temporal en tempo constante $O(1)$ e devolveErr OutOfMemorysen tombar o proceso. - Liberación instantánea ao saír: en canto remata o bloque
with region, toda a memoria temporal da area desaparece de golpe sen deixar fragmentación.
8.3. Tensores Nativos, Contracción de Einstein (@) e Dimensións Simbólicas
Se algunha vez programaches en Python con NumPy ou PyTorch, seguro que sufriches o clásico erro no minuto 41 de execución:
RuntimeError: mat1 and mat2 shapes cannot be multiplied (128x64 and 32x128)
En Ridgun, os Tensor son cidadáns nativos de primeira clase e as súas dimensións forman parte da sinatura de tipos:
-- Se entra (N × K) e (K × M), por forza sae (N × M)!
multiplicar matrices Tensor[N, K] Tensor[K, M] => Tensor[N, M]
multiplicar matrices a b = a_ik @ b_kj
Fíxate en dous detalles espectaculares:
- Verificación estática de dimensións (
Tensor[N, K]): o eixe internoKda primeira matriz ten que coincidir co primeiro eixeKda segunda. Se intentas pasar dimensións incompatibles,ridgunc checkdeténte antes de executar nada. - Notación de subíndices de Einstein (
a_ik @ b_kj) e Hadamard (a_ij * b_j):
- Con
@, os subíndices repetidos en todos os operandos (k) contráense (súmanse) e os únicos (i,j) sobreviven no tensor resultado. - Con
+,-,*,/, os subíndices aliñan dimensións e fan broadcasting automático (a_ij * b_jmultiplica cada filaideapolo vectorb_jsen necesidade de escribirunsqueezeninreshape).
- Tensores tipados de alto rendemento (
Tensor[U64 * N]) e mutación in-place cont_(i, j): En sistemas criptográficos, simulacións físicas ou álxebra linear podes declarar tensores planos sobre un bloque contiguo de enteiros sen signo de 64 bits (Tensor[U64 * 4],Tensor[U64 * 8 * 16]), operar en paralelo con^(XOR nativo de bloque) e ler ou escribir elementos en tempo $O(1)$ coa sintaxe de subíndice explícitot_(i)out_(i, j):
crear bloque estado => Tensor[U64 * 2 * 2]
crear bloque estado =
mut m = tensor fill [2, 2] (u64 0)
m_(0, 1) = u64 0x6a09e667f3bcc908
m
8.4. Acceso a Hardware (hardware ::) e Primitivas POSIX de Sistemas
O propio compilador ridgunc, o xestor stable e utilidades de sistemas Unix están escritos en Ridgun. Para iso, a linguaxe inclúe bloques de rexistros mapeados en memoria (hardware ::) e primitivas POSIX directas:
hardware :: UART0 @ 0x1000_0000
tx_data : Int @ 0x00
status : Int @ 0x04
E para traballar co sistema operativo:
| Primitiva | Sinatura | Que fai |
|---|---|---|
cli args | => [String] | Argumentos de liña de comandos (argv). |
read file / write file | String => String / String String => () | Lectura e escritura íntegra de ficheiros en disco. |
read bytes / write bytes | String => [Int] / String [Int] => () | Lectura e escritura binaria exacta byte a byte (0..255). |
list dir / file lstat | String => [String] / String => ... | Listaxe de directorios e inspección de metadatos POSIX (lstat). |
sha256 file / sha256 str / sha256 raw | String => String | Hash criptográfico SHA-256 nativo (en hexadecimal de 64 caracteres ou resumo binario cru de 32 bytes con sha256 raw / sha256 bytes). |
sys exec / shell exec | ... => Int | Execución de procesos do sistema operativo. |
sys getcwd / sys chdir | => String / String => Int | Consultar ou mudar o directorio de traballo. |
sys getenv / sys setenv | String => String / String String => Int | Variables de contorno do proceso. |
sys getuid / sys getgid | => Int | UID e GID reais do proceso (doas, ferramentas de seguridade). |
sys exit | Int => () | Rematar o proceso con código de saída. |
8.5. O Ecosistema de Bibliotecas Estándar (ridgun-libs)
Ademais das primitivas do compilador, o ecosistema oficial de Ridgun inclúe 32 bibliotecas puras (.rglib) escritas integramente en Ridgun que podes importar con import <nome> en calquera proxecto:
| Categoría | Bibliotecas dispoñibles | Que achegan |
|---|---|---|
| Coleccións, Texto e Datos | collections, set, string, regex, utf8, fmt, csv, json, toml, yaml, xml, html, markdown, base64, hex, compress, sql | Mapas, conxuntos, expresións regulares, parsers/serializadores estruturados, codificación e motores de consultas. |
| Matemáticas, Tempo e Aleatoriedade | math, random, datetime, uuid, log, cli, bench, test | Funcións matemáticas, PRNG determinista e de sistema, datas ISO-8601, UUIDs, marcos de probas e benchmarks con clock_gettime. |
| Sistemas, Rede e Web | fs, path, process, env, net, http, uri | Sistema de ficheiros de alto nivel, manipulación de rutas, sub-procesos, sockets TCP/UDP e cliente/servidor HTTP. |
Criptografía e Seguridade (crypto) | crypto (sha256, hmac, hkdf, pbkdf2, argon2id, chacha20, aes, x25519, ed25519, csprng) | Suite criptográfica verificada con vectores oficiais RFC/NIST, implementada sobre U64, Tensor[U64 * ...] e sha256 raw con optimización de inlining de tuplas sen reservas no heap. |
8.6. Caso de Estudo Real: Construíndo rg-wc, unha utilidade Unix paralela
Imos unir todo o aprendido desde o Capítulo 01 ata aquí nun programa real de sistemas: unha versión paralela da ferramenta Unix wc (contador de liñas e bytes para múltiples ficheiros) escrita en Ridgun puro:
-- 1. Modelamos as métricas dun ficheiro cun rexistro ADT (Capítulo 06)
type Metricas = ficheiro: String, linas: Int, bytes: Int
-- 2. Contamos saltos de liña (código ASCII 10) con recursión de cola TCO (Capítulo 04)
fn :: contar saltos String Int Int Int => Int
contar saltos texto pos fin acum =
| pos >= fin = acum
| (str char at texto pos) == 10 = contar saltos texto (pos + 1) fin (acum + 1)
| otherwise = contar saltos texto (pos + 1) fin acum
fn :: numero de linas String => Int
numero de linas texto = contar saltos texto 0 (str len texto) 0
-- 3. Analizamos un ficheiro e devolvemos a súa estrutura Metricas
fn :: analizar ficheiro String => Metricas
analizar ficheiro ruta =
contido = read file ruta
|| numero de linas contido -> n_linas
|| str len contido -> n_bytes ||.
Metricas ruta n_linas n_bytes
-- 4. Formatamos cada fila e sumamos totais con `fold` e `||` (Capítulo 07)
fn :: formatar metrica Metricas => String
formatar metrica m = "{linas m}\t{bytes m}\t{ficheiro m}"
fn :: sumar linas Int Metricas => Int
sumar linas acc m = acc + (linas m)
fn :: sumar bytes Int Metricas => Int
sumar bytes acc m = acc + (bytes m)
fn :: xerar informe [String] => String
xerar informe rutas =
metricas = ||map analizar ficheiro rutas
|| map formatar metrica metricas -> filas
|| fold sumar linas 0 metricas -> total_l
|| fold sumar bytes 0 metricas -> total_b ||.
"{filas}\nTotal: {total_l} liñas, {total_b} bytes"
fn :: main => ()
main =
echo (xerar informe ["README.md", "AGENTS.md"])
Párate un segundo a mirar esas ~40 liñas de código:
- Non hai nin un só
null, nin unha excepción descontrolada, nin unMutex. contar saltoscompílase a unloopnativo con 0 bytes de pila grazas ao TCO automático.||map analizar ficheiro rutasprocesa os ficheiros en paralelo en todos os núcleos da túa CPU, e o bloque||calcula simultaneamente as filas formatadas, o total de liñas e o total de bytes.- A memoria temporal libérase deterministicamente mediante
RMM1/RMM2. - E cando executas
ridgunc build rg_wc.rg -o rg-wc, obtés un binario nativo autónomo de ~350 KB listo para copiar a/usr/local/bin.
8.7. Folla de Ruta Final: As 10 Regras de Ouro do Programador Ridgun
- Escribe sempre primeiro o contrato (
=>) e despois as ecuacións (=): pensar nos tipos primeiro fai que o compilador traballe para ti. - Deixa que
stable formatmaquete as túas táboas: escribe cun espazo e deixa que a ferramenta aliñe columnas,=>,=e->. - Lembra a regra universal do
=: detrás de calquera=podes abrir un bloque secuencial, unha árbore de gardas|ou un grafo paralelo||. - Usa
|.e||.só cando peches un bloque aniñado para volver a un nivel superior: mantén as árbores de decisión profundas compactas e sen ambigüidade. - Inmutable por defecto (
x = ...), mutable explícito (mut x = ...): usamutunicamente cando un acumulador local o pida de verdade. - Usa operadores booleanos claros en minúsculas (
and,or,xor,not) e pecha sempre as túas gardas con| otherwise =. - Modela o teu dominio con ADTs (
type): usa rexistros conwith campo: valore tipos suma (Option,Result) en lugar denull. - Paraleliza declarando dependencias con
|| expr -> vare||map/||filter: deixa que o compilador verifique que non hai ciclos e reparta o traballo entre os núcleos. - Ponlle dimensións simbólicas aos teus Tensores (
Tensor[N, K]) e subíndices ás túas contraccións (a_ik @ b_kj). - Confía en
RMM(RMM1/RMM2), acouta entradas externas conwith regione verifica sempre constable checkestable test.
Feliz viaxe programando en Ridgun!