08 The Ridgun Book
Tensors, RMM Compiled Memory (RMM1/RMM2), Regions, and Systems
So far we have seen the friendly, elegant, mathematical side of Ridgun: pure functions, algebraic data types, pattern matching, decision trees with | / |., and parallel || graphs with -> that put every core of your processor to work without asking you for a single Mutex.
At this point, a veteran systems programmer at the back of the room usually raises a hand with a half-smile and asks:
"All of this is lovely for computing invoices or compiling ASTs, but what happens when I need to multiply matrices of millions of floats at breakneck speed? What about memory? Is there a Garbage Collector hiding under the rug waiting for the worst possible moment to freeze my program for 200 milliseconds? And how do I talk to a real Unix operating system?"
Excellent questions. Let's pop the hood of Ridgun and see how it handles low-level systems work, numerical computing, and deterministic memory.
8.1. The Memory Dilemma: Neither Garbage Collector Nor Borrow Checker Fights (RMM1 and RMM2)
In modern programming languages there are usually two irreconcilable camps:
- The Garbage Collector camp (Java, Go, Python, JS, Haskell, OCaml): you allocate memory freely and, every now and then, an invisible garbage truck stops all traffic in your program to trace which objects are still alive and which are dead. It is wonderfully convenient... until you are building an ultra-fast systems tool or a strict-latency server and the garbage truck decides to double-park.
- The manual control / Borrow Checker camp (C, C++, Rust): there is no garbage collector, but in exchange you either juggle raw pointers by hand (risking a
Segmentation faultor use-after-free) or spend half the afternoon arguing with the borrow checker about why'adoes not live as long as'b.
Ridgun solves this dilemma at the root with Ridgun Memory Management (RMM), a deterministic system of two complementary policies (RMM1 and RMM2) combined with lexical regions (with region):
Policy RMM1 — Compiled Memory Management (Linear and Conditional CMM)
When the compiler statically knows the flow of local variables inside a function:
- Linear CMM:
ridgunccomputes at compile time the exact last line where each local variable (a,b, ...) is read and inserts its deterministic deallocation (rg_cmm_drop) immediately afterward, or transfers ownership with zero copies (rg_cmm_move) when the variable is returned as the result. - Conditional CMM (
ptr(a) != ptr(c)): what if a function likechoose cond a breturns eitheraorbdepending on a runtime condition?ridguncbuilds the ancestor table and emits a 1-CPU-cycle pointer-identity guard: whenbis no longer used whilecremains alive, it checksif ptr(b) != ptr(c) { free(b) }. The unchosen branch is freed immediately and the chosen one survives intact.
Policy RMM2 — Assignment Counting and Structural Cascade
And what happens when you nest data structures inside other data structures (xs = [a, b]), reassign mutable variables (mut x = ...), or chain complex calls? That is where RMM2 steps in, governing the entire lifecycle through four airtight mathematical rules:
- Incoming function arguments are untouched (
+0on entry,-0on exit): passing a list or a record into a function never increments or decrements reference counters needlessly. - Every assignment to a local variable or container slot adds
+1:
a = [10, 20]$\rightarrow$ blockM1is born with counter1.xs = [a, b]$\rightarrow$ listxsis born with1, and because it holds references toaandb, both climb to2.- If you reassign a mutable variable (
mut x = ...and laterx = new_val),RMM2applies+1to the new value first before applying-1to the old one.
- Upon exiting the function, local variables that are NOT returned receive
-1: if a structure's counter reaches0, its memory is freed on the spot and it cascades-1, one by one, to each of its direct children. - The value returned by the function is exempt in transit: it receives its local-variable
-1without being destroyed as it crosses the return boundary, so when the caller writesc = my func (), the+1from that assignment leaves its counter at1.
choose Bool [Int] [Int] => [Int]
choose cond a b =
| cond = a
| otherwise = b
rmm2 example Bool => Int
rmm2 example cond =
a = [10, 20, 30] -- M1 (rc = 1)
b = [40, 50, 60] -- M2 (rc = 1)
c = choose cond a b -- `c` gets +1; the unchosen list dies right here (rc -> 0)
(count a) + (count c) -- On exit, `a` and `c` get -1 and 0 live blocks remain
8.2. Quota-Bounded Lexical Regions (with region): $O(1)$ Memory Firewalls
Alongside RMM1 and RMM2, whenever you are parsing an external network packet, a massive file, or a batch of untrusted data and want to enforce a strict RAM ceiling, you wrap the computation in a with region block:
process frame String => Result
process frame raw =
with region 64_KB do
clean = map clean line (split lines raw)
count valid clean
- Hardware/runtime-guaranteed quota: if code inside the block attempts to allocate even a single byte beyond
64_KB(65,536bytes), the runtime cleanly aborts the block, frees the entire temporary arena in constant $O(1)$ time, and returnsErr OutOfMemorywithout crashing the process. - Instantaneous deallocation on exit: the moment the
with regionblock finishes, all temporary memory in the arena vanishes in one stroke with zero fragmentation.
8.3. Native Tensors, Einstein Contraction (@), and Symbolic Dimensions
If you have ever programmed in Python with NumPy or PyTorch, you have almost certainly suffered the classic crash 41 minutes into a training run:
RuntimeError: mat1 and mat2 shapes cannot be multiplied (128x64 and 32x128)
In Ridgun, Tensor values are first-class native citizens and their dimensions are part of the type signature:
-- If (N × K) and (K × M) go in, (N × M) MUST come out!
multiply matrices Tensor[N, K] Tensor[K, M] => Tensor[N, M]
multiply matrices a b = a_ik @ b_kj
Notice three standout details:
- Static dimension verification (
Tensor[N, K]): the inner axisKof the first matrix must match the leading axisKof the second. If you try to pass incompatible dimensions,ridgunc checkstops you before executing a single instruction. - Einstein index notation (
a_ik @ b_kj) and Hadamard broadcasting (a_ij * b_j):
- With
@, indices repeated across all operands (k) are contracted (summed over) while unique indices (i,j) survive in the output tensor. - With
+,-,*,/, indices align dimensions and perform automatic broadcasting (a_ij * b_jmultiplies each rowiofaby vectorb_jwithout needingunsqueezeorreshape).
- Typed Tensors (
Tensor[T * d1 * d2]), Mathematical Subscriptst_(i, j), Slice Viewst_(i,), and In-Place Axis Mapping (in): You can allocate zero-initialized tensors with explicit element types (U64,Int,Float,Byte), index them with mathematical subscriptst_(i, j), take zero-copy row (t_(i,)) or column (t_(,j)) views, destructure multi-return calls directly into tensor elements with zero heap allocations, and map functions in-place along an axis usingmap ... inor||map ... in:
fn :: blamka p16 Tensor[U64 * 16] => ()
blamka p16 v =
v_(0), v_(4), v_(8), v_(12) = blamka g v_(0) v_(4) v_(8) v_(12)
fn :: compress block Tensor[U64 * 8 * 16] Tensor[U64 * 8 * 16] => Tensor[U64 * 8 * 16]
compress block x y =
r_ij = x_ij xor y_ij
z_ij = r_ij
||map blamka p16 in z_(i,)
||map blamka p16 in z_(,j)
z_ij xor r_ij
8.4. Hardware Access (hardware ::) and POSIX Systems Primitives
The ridgunc compiler itself, the stable build manager, and Unix systems utilities are written in Ridgun. To make that possible, the language includes memory-mapped register blocks (hardware ::) and direct POSIX primitives:
hardware :: UART0 @ 0x1000_0000
tx_data : Int @ 0x00
status : Int @ 0x04
And for interacting with the operating system:
| Primitive | Signature | What It Does |
|---|---|---|
cli args | => [String] | Command-line arguments (argv). |
read file / write file | String => String / String String => () | Full file reading and writing on disk. |
read bytes / write bytes | String => [Int] / String [Int] => () | Exact byte-by-byte binary reading and writing (0..255). |
list dir / file lstat | String => [String] / String => ... | Directory listing and POSIX metadata inspection (lstat). |
sha256 file / sha256 str | String => String | Native 64-character hex SHA-256 cryptographic hash. |
sha256 raw / sha256 bytes | String => String | Raw 32-byte binary SHA-256 digest (supports embedded 0x00 bytes with zero hex conversion overhead). |
sys exec / shell exec | ... => Int | Operating system process execution. |
sys getcwd / sys chdir | => String / String => Int | Query or change the current working directory. |
sys getenv / sys setenv | String => String / String String => Int | Process environment variables. |
sys getuid / sys getgid | => Int | Real process UID and GID (doas, security tools). |
sys exit | Int => () | Terminate the process with an exit code. |
8.5. Real-World Case Study: Building rg-wc, a Parallel Unix Utility
Let's bring together everything we have learned from Chapter 01 to Chapter 08 in a real systems program: a parallel version of the Unix wc utility (line and byte counter across multiple files) written in pure Ridgun:
-- 1. Model file metrics using an ADT record (Chapter 06)
type Metrics = file: String, lines: Int, bytes: Int
-- 2. Count newline characters (ASCII code 10) with TCO tail recursion (Chapter 04)
fn :: count newlines String Int Int Int => Int
count newlines text pos end acc =
| pos >= end = acc
| (str char at text pos) == 10 = count newlines text (pos + 1) end (acc + 1)
| otherwise = count newlines text (pos + 1) end acc
fn :: line count String => Int
line count text = count newlines text 0 (str len text) 0
-- 3. Analyze a file and return its Metrics record
fn :: analyze file String => Metrics
analyze file path =
content = read file path
|| line count content -> n_lines
|| str len content -> n_bytes ||.
Metrics path n_lines n_bytes
-- 4. Format each row and sum totals using `fold` and `||` (Chapter 07)
fn :: format metric Metrics => String
format metric m = "{lines m}\t{bytes m}\t{file m}"
fn :: sum lines Int Metrics => Int
sum lines acc m = acc + (lines m)
fn :: sum bytes Int Metrics => Int
sum bytes acc m = acc + (bytes m)
fn :: generate report [String] => String
generate report paths =
metrics = ||map analyze file paths
|| map format metric metrics -> rows
|| fold sum lines 0 metrics -> total_l
|| fold sum bytes 0 metrics -> total_b ||.
"{rows}\nTotal: {total_l} lines, {total_b} bytes"
fn :: main => ()
main =
echo (generate report ["README.md", "AGENTS.md"])
Pause for a second and look at those ~40 lines of code:
- There is not a single
null, unchecked exception, orMutex. count newlinescompiles down to a nativeloopusing 0 bytes of stack thanks to automatic TCO.||map analyze file pathsprocesses files in parallel across all cores of your CPU, and the||block computes the formatted rows, total lines, and total bytes simultaneously.- Temporary memory is freed deterministically via
RMM1/RMM2. - And when you run
ridgunc build rg_wc.rg -o rg-wc, you get a self-contained ~350 KB native binary ready to copy into/usr/local/bin.
8.6. Final Roadmap: The 10 Golden Rules of the Ridgun Programmer
- Always write the contract (
=>) first and the equations (=) second: thinking about types first makes the compiler work for you. - Let
stable formattypeset your tables: type with single spaces and let the tool align columns,=>,=, and->. - Remember the universal
=rule: after any=you can open a sequential block, a|guard tree, or a parallel||graph. - Use
|.and||.only when closing a nested block to return to an outer level: keep deep decision trees compact and unambiguous. - Immutable by default (
x = ...), explicitly mutable (mut x = ...): reach formutonly when a local accumulator genuinely calls for it. - Use clear lowercase boolean operators (
and,or,xor,not) and always close your guards with| otherwise =. - Model your domain with ADTs (
type): use records withwith field: valueand sum types (Option,Result) instead ofnull. - Parallelize by declaring dependencies with
|| expr -> varand||map/||filter: let the compiler verify acyclicity and distribute work across cores. - Give your Tensors symbolic dimensions (
Tensor[N, K]) and your contractions explicit indices (a_ik @ b_kj). - Trust
RMM(RMM1/RMM2), bound untrusted inputs withwith region, and always verify withstable checkandstable test.
Happy hacking in Ridgun!