Ridgun
The language itself: a compiler that compiles itself, the stable project tool, the specification and the book
| 1 | # Capítulo 08: Tensores, Memoria Compilada RMM (`RMM1`/`RMM2`), Regiones y Sistemas |
| 2 | |
| 3 | Hasta aquí hemos visto el lado más amable, elegante y matemático de Ridgun: funciones puras, tipos algebraicos, *pattern matching*, árboles de decisión con `|` / `|.` y grafos paralelos `||` con `->` que aprovechan todos los núcleos de tu procesador sin pedirte ni un triste `Mutex`. |
| 4 | |
| 5 | Llegados a este punto, el programador veterano de sistemas suele levantar la mano al fondo del aula con media sonrisa y preguntar: |
| 6 | |
| 7 | > *"Todo esto es precioso para calcular facturas o compilar ASTs, pero ¿qué pasa cuando necesito multiplicar matrices de millones de flotantes a velocidad de vértigo? ¿Qué pasa con la memoria? ¿Hay un recolector de basura (Garbage Collector) escondido debajo de la alfombra esperando el peor momento para congelar mi programa durante 200 milisegundos? ¿Y cómo hablo con el sistema operativo Unix de verdad?"* |
| 8 | |
| 9 | Excelentes preguntas. Vamos a levantar el capó de Ridgun para ver cómo se las gasta cuando toca bajar al barro de los sistemas, el cálculo numérico y la memoria determinista. |
| 10 | |
| 11 | --- |
| 12 | |
| 13 | ## 8.1. El dilema de la memoria: ni Garbage Collector ni peleas con el Borrow Checker (`RMM1` y `RMM2`) |
| 14 | |
| 15 | En el mundo de los lenguajes modernos suele haber dos bandos irreconciliables: |
| 16 | |
| 17 | 1. **El bando del Garbage Collector (Java, Go, Python, JS, Haskell, OCaml):** tú pides memoria alegremente y, de vez en cuando, un camión de la basura invisible detiene el tráfico de tu programa para rastrear qué objetos siguen vivos y cuáles no. Es comodísimo... hasta que estás haciendo una herramienta de sistema ultrarrápida o un servidor con latencias estrictas y el camión decide aparcar en doble fila. |
| 18 | 2. **El bando del control manual / Borrow Checker (C, C++, Rust):** no hay recolector de basura, pero a cambio tienes que gestionar punteros a mano (jugándote un `Segmentation fault` o un *use-after-free*) o pasarte media tarde negociando con el *borrow checker* por qué `'a` no vive tanto como `'b`. |
| 19 | |
| 20 | Ridgun resuelve este dilema de raíz con **Ridgun Memory Management (`RMM`)**, un sistema determinista de dos políticas complementarias (**`RMM1`** y **`RMM2`**) combinado con **regiones léxicas (`with region`)**: |
| 21 | |
| 22 | ### Política `RMM1` — Gestión de Memoria Compilada (CMM Lineal y Condicional) |
| 23 | Cuando el compilador conoce estáticamente el flujo de las variables locales dentro de una función: |
| 24 | - **CMM Lineal**: `ridgunc` calcula en tiempo de compilación cuál es la **última línea** en la que se lee cada variable local (`a`, `b`, ...) e inserta justo después su liberación determinista (`rg_cmm_drop`), o transfiere su propiedad sin copias (`rg_cmm_move`) cuando la variable se devuelve como resultado. |
| 25 | - **CMM Condicional (`ptr(a) != ptr(c)`)**: ¿y si una función como `elegir cond a b` devuelve `a` o `b` según una condición en tiempo de ejecución? `ridgunc` construye la tabla de ancestros y emite una guarda de identidad de punteros de **1 ciclo de CPU**: si `b` deja de usarse mientras `c` sigue viva, comprueba `if ptr(b) != ptr(c) { free(b) }`. La rama no elegida se libera al instante y la elegida sobrevive intacta. |
| 26 | |
| 27 | ### Política `RMM2` — Conteo por Asignación y Cascada Estructural |
| 28 | ¿Y qué ocurre cuando metes estructuras dentro de otras estructuras (`xs = [a, b]`), reasignas variables mutables (`mut x = ...`) o encadenas llamadas complejas? Ahí entra **`RMM2`**, que gobierna todo el ciclo de vida con cuatro reglas matemáticas sin fisuras: |
| 29 | |
| 30 | 1. **Los argumentos de entrada de una función no se tocan (`+0` al entrar, `-0` al salir)**: pasar una lista o un registro a una función jamás incrementa ni decrementa contadores en vano. |
| 31 | 2. **Toda asignación a una variable local interna o celda de contenedor suma `+1`**: |
| 32 | - `a = [10, 20]` $\rightarrow$ el bloque `M1` nace con contador `1`. |
| 33 | - `xs = [a, b]` $\rightarrow$ la lista `xs` nace con `1`, y como guarda referencias a `a` y `b`, ambos suben a `2`. |
| 34 | - Si reasignas una variable mutable (`mut x = ...` y luego `x = nuevo`), `RMM2` aplica primero **`+1`** al valor nuevo antes de aplicar **`-1`** al antiguo. |
| 35 | 3. **Al salir de la función, las variables locales que NO se devuelven reciben `-1`**: si el contador de una estructura llega a `0`, se libera su memoria en el acto y **propaga `-1` en cascada, uno por uno, a cada uno de sus hijos directos**. |
| 36 | 4. **El valor devuelto por la función se exime en tránsito**: recibe su `-1` de variable local pero no se destruye al cruzar el `return`, de modo que cuando quien llamó hace `c = mi funcion ()`, ese `+1` de la asignación lo deja exactamente en `1`. |
| 37 | |
| 38 | ```ridgun |
| 39 | elegir Bool [Int] [Int] => [Int] |
| 40 | elegir cond a b = |
| 41 | | cond = a |
| 42 | | otherwise = b |
| 43 | |
| 44 | ejemplo rmm2 Bool => Int |
| 45 | ejemplo rmm2 cond = |
| 46 | a = [10, 20, 30] -- M1 (rc = 1) |
| 47 | b = [40, 50, 60] -- M2 (rc = 1) |
| 48 | c = elegir cond a b -- `c` recibe +1; el no elegido muere aquí mismo (rc -> 0) |
| 49 | (contar a) + (contar c) -- Al terminar, `a` y `c` reciben -1 y todo queda en 0 bloques vivos |
| 50 | ``` |
| 51 | |
| 52 | --- |
| 53 | |
| 54 | ## 8.2. Regiones Léxicas con Cuota (`with region`): Cortafuegos de memoria en $O(1)$ |
| 55 | |
| 56 | Además de `RMM1` y `RMM2`, cuando estás procesando un paquete de red externo, un archivo gigante o un lote de datos no confiables y quieres poner un **techo estricto de memoria RAM**, envuelves el cálculo en un bloque **`with region`**: |
| 57 | |
| 58 | ```ridgun |
| 59 | procesar trama String => Result |
| 60 | procesar trama crudo = |
| 61 | with region 64_KB do |
| 62 | limpias = map limpiar linea (dividir lineas crudo) |
| 63 | contar validos limpias |
| 64 | ``` |
| 65 | |
| 66 | 1. **Cuota garantizada en hardware/runtime**: si dentro del bloque se intenta reservar ni un solo byte por encima de `64_KB` (`65.536` bytes), el runtime aborta limpiamente el bloque, libera toda la arena temporal en tiempo constante $O(1)$ y devuelve `Err OutOfMemory` sin tumbar el proceso. |
| 67 | 2. **Liberación instantánea al salir**: en cuanto termina el bloque `with region`, toda la memoria temporal de la arena desaparece de golpe sin dejar fragmentación. |
| 68 | |
| 69 | --- |
| 70 | |
| 71 | ## 8.3. Tensores Nativos, Contracción de Einstein (`@`) yDimensiones Simbólicas |
| 72 | |
| 73 | Si alguna vez has programado en Python con NumPy o PyTorch, seguro que has sufrido el clásico error en el minuto 41 de ejecución: |
| 74 | |
| 75 | ```text |
| 76 | RuntimeError: mat1 and mat2 shapes cannot be multiplied (128x64 and 32x128) |
| 77 | ``` |
| 78 | |
| 79 | En Ridgun, los **`Tensor`** son ciudadanos nativos de primera clase y sus dimensiones forman parte de la firma de tipos: |
| 80 | |
| 81 | ```ridgun |
| 82 | -- Si entra (N × K) y (K × M), ¡por narices sale (N × M)! |
| 83 | multiplicar matrices Tensor[N, K] Tensor[K, M] => Tensor[N, M] |
| 84 | multiplicar matrices a b = a_ik @ b_kj |
| 85 | ``` |
| 86 | |
| 87 | Fíjate en dos detalles espectaculares: |
| 88 | 1. **Verificación estática de dimensiones (`Tensor[N, K]`)**: el eje interno `K` de la primera matriz tiene que coincidir con el primer eje `K` de la segunda. Si intentas pasar dimensiones incompatibles, `ridgunc check` te detiene antes de ejecutar nada. |
| 89 | 2. **Notación de subíndices de Einstein (`a_ik @ b_kj`) y Hadamard (`a_ij * b_j`)**: |
| 90 | - Con **`@`**, los subíndices repetidos en todos los operandos (`k`) se contraen (se suman) y los únicos (`i`, `j`) sobreviven en el tensor resultado. |
| 91 | - Con **`+`, `-`, `*`, `/`**, los subíndices alinean dimensiones y hacen *broadcasting* automático (`a_ij * b_j` multiplica cada fila `i` de `a` por el vector `b_j` sin necesidad de escribir `unsqueeze` ni `reshape`). |
| 92 | 3. **Tensores tipados (`Tensor[T * d1 * d2]`), subíndices matemáticos `t_(i, j)`, vistas `t_(i,)` y mapeo *in-place* (`in`)**: |
| 93 | Puedes declarar tensores multidimensionales con tipo escalar explícito (`U64`, `Int`, `Float`, `Byte`) inicializados a cero en una sola línea, indexarlos con subíndices matemáticos `t_(i, j)`, extraer vistas de fila `t_(i,)` o columna `t_(,j)` sin copiar memoria, desestructurar retornos múltiples directamente sobre celdas del tensor sin alojar tuplas en el heap, y aplicar funciones *in-place* sobre un eje con `map ... in` o `||map ... in`: |
| 94 | ```ridgun |
| 95 | fn :: blamka p16 Tensor[U64 * 16] => () |
| 96 | blamka p16 v = |
| 97 | v_(0), v_(4), v_(8), v_(12) = blamka g v_(0) v_(4) v_(8) v_(12) |
| 98 | |
| 99 | fn :: comprimir bloque Tensor[U64 * 8 * 16] Tensor[U64 * 8 * 16] => Tensor[U64 * 8 * 16] |
| 100 | comprimir bloque x y = |
| 101 | r_ij = x_ij xor y_ij |
| 102 | z_ij = r_ij |
| 103 | ||map blamka p16 in z_(i,) |
| 104 | ||map blamka p16 in z_(,j) |
| 105 | z_ij xor r_ij |
| 106 | ``` |
| 107 | |
| 108 | --- |
| 109 | |
| 110 | ## 8.4. Acceso a Hardware (`hardware ::`) y Primitivas POSIX de Sistemas |
| 111 | |
| 112 | El propio compilador `ridgunc`, el gestor `stable` y utilidades de sistemas Unix están escritos en Ridgun. Para ello, el lenguaje incluye bloques de registros mapeados en memoria (`hardware ::`) y primitivas POSIX directas: |
| 113 | |
| 114 | ```ridgun |
| 115 | hardware :: UART0 @ 0x1000_0000 |
| 116 | tx_data : Int @ 0x00 |
| 117 | status : Int @ 0x04 |
| 118 | ``` |
| 119 | |
| 120 | Y para trabajar con el sistema operativo: |
| 121 | |
| 122 | | Primitiva | Firma | Qué hace | |
| 123 | |---|---|---| |
| 124 | | `cli args` | `=> [String]` | Argumentos de línea de comandos (`argv`). | |
| 125 | | `read file` / `write file` | `String => String` / `String String => ()` | Lectura y escritura íntegra de archivos en disco. | |
| 126 | | `read bytes` / `write bytes` | `String => [Int]` / `String [Int] => ()` | Lectura y escritura binaria exacta byte a byte (`0..255`). | |
| 127 | | `list dir` / `file lstat` | `String => [String]` / `String => ...` | Listado de directorios e inspección de metadatos POSIX (`lstat`). | |
| 128 | | `sha256 file` / `sha256 str` | `String => String` | Hash criptográfico SHA-256 nativo en hexadecimal (64 caracteres). | |
| 129 | | `sha256 raw` / `sha256 bytes` | `String => String` | Digest SHA-256 binario crudo de 32 bytes (con soporte completo para bytes `0x00` intermedios y cero conversiones hex). | |
| 130 | | `sys exec` / `shell exec` | `... => Int` | Ejecución de procesos del sistema operativo. | |
| 131 | | `sys getcwd` / `sys chdir` | `=> String` / `String => Int` | Consultar o cambiar el directorio de trabajo. | |
| 132 | | `sys getenv` / `sys setenv` | `String => String` / `String String => Int` | Variables de entorno del proceso. | |
| 133 | | `sys getuid` / `sys getgid` | `=> Int` | UID y GID reales del proceso (`doas`, herramientas de seguridad). | |
| 134 | | `sys open mode` / `sys close` / `sys poll` | `String String => Int` / `Int => Int` / `Int Int Int => Int` | Descriptores PAL multiplataforma y `poll` acotado por `with timeout`. | |
| 135 | | `sys exit` | `Int => ()` | Terminar el proceso con código de salida. | |
| 136 | |
| 137 | --- |
| 138 | |
| 139 | ## 8.5. Caso de Estudio Real: Construyendo `rg-wc`, una utilidad Unix paralela |
| 140 | |
| 141 | Vamos a unir todo lo aprendido desde el Capítulo 01 hasta aquí en un programa real de sistemas: una versión paralela de la herramienta Unix `wc` (contador de líneas y bytes para múltiples archivos) escrita en Ridgun puro: |
| 142 | |
| 143 | ```ridgun |
| 144 | -- 1. Modelamos las métricas de un archivo con un registro ADT (Capítulo 06) |
| 145 | type Metricas = archivo: String, lineas: Int, bytes: Int |
| 146 | |
| 147 | -- 2. Contamos saltos de línea (código ASCII 10) con recursión de cola TCO (Capítulo 04) |
| 148 | fn :: contar saltos String Int Int Int => Int |
| 149 | contar saltos texto pos fin acum = |
| 150 | | pos >= fin = acum |
| 151 | | (str char at texto pos) == 10 = contar saltos texto (pos + 1) fin (acum + 1) |
| 152 | | otherwise = contar saltos texto (pos + 1) fin acum |
| 153 | |
| 154 | fn :: numero de lineas String => Int |
| 155 | numero de lineas texto = contar saltos texto 0 (str len texto) 0 |
| 156 | |
| 157 | -- 3. Analizamos un archivo y devolvemos su estructura Metricas |
| 158 | fn :: analizar archivo String => Metricas |
| 159 | analizar archivo ruta = |
| 160 | contenido = read file ruta |
| 161 | || numero de lineas contenido -> n_lineas |
| 162 | || str len contenido -> n_bytes ||. |
| 163 | Metricas ruta n_lineas n_bytes |
| 164 | |
| 165 | -- 4. Formateamos cada fila y sumamos totales con `fold` y `||` (Capítulo 07) |
| 166 | fn :: formatear metrica Metricas => String |
| 167 | formatear metrica m = "{lineas m}\t{bytes m}\t{archivo m}" |
| 168 | |
| 169 | fn :: sumar lineas Int Metricas => Int |
| 170 | sumar lineas acc m = acc + (lineas m) |
| 171 | |
| 172 | fn :: sumar bytes Int Metricas => Int |
| 173 | sumar bytes acc m = acc + (bytes m) |
| 174 | |
| 175 | fn :: generar informe [String] => String |
| 176 | generar informe rutas = |
| 177 | metricas = ||map analizar archivo rutas |
| 178 | || map formatear metrica metricas -> filas |
| 179 | || fold sumar lineas 0 metricas -> total_l |
| 180 | || fold sumar bytes 0 metricas -> total_b ||. |
| 181 | "{filas}\nTotal: {total_l} líneas, {total_b} bytes" |
| 182 | |
| 183 | fn :: main => () |
| 184 | main = |
| 185 | echo (generar informe ["README.md", "AGENTS.md"]) |
| 186 | ``` |
| 187 | |
| 188 | Párate un segundo a mirar esas ~40 líneas de código: |
| 189 | - No hay ni un solo `null`, ni una excepción descontrolada, ni un `Mutex`. |
| 190 | - `contar saltos` se compila a un `loop` nativo con 0 bytes de pila gracias al TCO automático. |
| 191 | - `||map analizar archivo rutas` procesa los archivos en paralelo en todos los núcleos de tu CPU, y el bloque `||` calcula simultáneamente las filas formateadas, el total de líneas y el total de bytes. |
| 192 | - La memoria temporal se libera determinísticamente mediante **`RMM1`/`RMM2`**. |
| 193 | - Y cuando ejecutas `ridgunc build rg_wc.rg -o rg-wc`, obtienes un binario nativo autónomo de ~350 KB listo para copiar a `/usr/local/bin`. |
| 194 | |
| 195 | --- |
| 196 | |
| 197 | ## 8.6. Hoja de Ruta Final: Las 10 Reglas de Oro del Programador Ridgun |
| 198 | |
| 199 | 1. **Escribe siempre primero el contrato (`=>`) y después las ecuaciones (`=`):** pensar en los tipos primero hace que el compilador trabaje para ti. |
| 200 | 2. **Deja que `stable format` maquete tus tablas:** escribe con un espacio y deja que la herramienta alinee columnas, `=>`, `=` y `->`. |
| 201 | 3. **Recuerda la regla universal del `=`:** detrás de cualquier `=` puedes abrir un bloque secuencial, un árbol de guardas `|` o un grafo paralelo `||`. |
| 202 | 4. **Usa `|.` y `||.` solo cuando cierres un bloque anidado para volver a un nivel superior:** mantén los árboles de decisión profundos compactos y sin ambigüedad. |
| 203 | 5. **Inmutable por defecto (`x = ...`), mutable explícito (`mut x = ...`):** usa `mut` únicamente cuando un acumulador local lo pida de verdad. |
| 204 | 6. **Usa operadores booleanos claros en minúsculas (`and`, `or`, `xor`, `not`) y cierra siempre tus guardas con `| otherwise =`.** |
| 205 | 7. **Modela tu dominio con ADTs (`type`):** usa registros con `with campo: valor` y tipos suma (`Option`, `Result`) en lugar de `null`. |
| 206 | 8. **Paraleliza declarando dependencias con `|| expr -> var` y `||map` / `||filter`:** deja que el compilador verifique que no hay ciclos y reparta el trabajo entre los núcleos. |
| 207 | 9. **Ponle dimensiones simbólicas a tus Tensores (`Tensor[N, K]`) y subíndices a tus contracciones (`a_ik @ b_kj`).** |
| 208 | 10. **Confía en `RMM` (`RMM1`/`RMM2`), acota entradas externas con `with region` y verifica siempre con `stable check` y `stable test`.** |
| 209 | |
| 210 | ¡Feliz viaje programando en Ridgun! |