Tilessa

8. The Nano Core🔗

Every chapter so far has treated the element as a small general-purpose processor. This one asks the sharper question the whole project kept circling: how little does a processor need to be, if what it mostly does is multiply and add? The answer turns out to dissolve a false choice — between a dense, dumb multiplier array and a flexible, sparse programmable core — because a Turing-complete machine costs almost nothing, and it can be built directly around the multiply-accumulate.

8.1. Turing-completeness is cheap🔗

A processor is "general purpose" when it can compute anything computable — and that bar is astonishingly low. A one-instruction computer, subleq (subtract, branch-if-≤-zero), is already Turing-complete. So "few instructions" and "general purpose" were never in tension. And the multiply-accumulate is a superset of subleq's arithmetic: acc += a × b gives multiplication, addition (×1), and subtraction (×−1). Add a conditional branch and indirect memory, and you are universal.

The Nano core is exactly that: an accumulator machine of seven instructions, built on the MAC.

namespace Nano /-- Eight registers is comfortable; four suffice. -/ abbrev NReg := Fin 8 /-- The whole machine: an accumulator, registers, unbounded memory (`Nat → Int` — genuinely infinite, which universality needs), and a pc. -/ structure Nano where acc : Int regs : NReg Int mem : Nat Int pc : Nat /-- The seven instructions. `mac` is the workhorse; the rest are the minimum for constants, memory, and control. -/ inductive NanoI where | mac (ra rb : NReg) -- acc += r[a] * r[b] | clracc -- acc := 0 | movacc (rd : NReg) -- r[d] := acc | li (rd : NReg) (imm : Int) -- r[d] := imm | ld (rd ra : NReg) -- r[d] := mem[r[a]] (indirect) | st (ra rs : NReg) -- mem[r[a]] := r[s] | blz (rs : NReg) (off : Int) -- if r[s] ≤ 0 then pc += off | halt deriving DecidableEq def upd (f : NReg Int) (i : NReg) (v : Int) : NReg Int := fun j => if j = i then v else f j def mupd (m : Nat Int) (a : Nat) (v : Int) : Nat Int := fun b => if b = a then v else m b /-- One instruction; `none` means halted. -/ def nstep (p : Nat NanoI) (s : Nano) : Option Nano := match p s.pc with | .mac ra rb => some { s with acc := s.acc + s.regs ra * s.regs rb, pc := s.pc + 1 } | .clracc => some { s with acc := 0, pc := s.pc + 1 } | .movacc rd => some { s with regs := upd s.regs rd s.acc, pc := s.pc + 1 } | .li rd imm => some { s with regs := upd s.regs rd imm, pc := s.pc + 1 } | .ld rd ra => some { s with regs := upd s.regs rd (s.mem (s.regs ra).toNat), pc := s.pc + 1 } | .st ra rs => some { s with mem := mupd s.mem (s.regs ra).toNat (s.regs rs), pc := s.pc + 1 } | .blz rs off => some { s with pc := if s.regs rs 0 then ((s.pc : Int) + off).toNat else s.pc + 1 } | .halt => none /-- Run to halt (or fuel out), returning the final state and cycle count. -/ def nrun (p : Nat NanoI) : Nat Nano Nano × Nat | 0, s => (s, 0) | fuel + 1, s => match nstep p s with | some s' => let (sf, k) := nrun p fuel s'; (sf, k + 1) | none => (s, 0) def prog (l : List NanoI) : Nat NanoI := fun pc => l.getD pc .halt def s0 : Nano := { acc := 0, regs := fun _ => 0, mem := fun _ => 0, pc := 0 }

8.2. It is a MAC engine🔗

The first thing it must do is the thing it exists for. Here is a dot product ⟨1,2,3⟩·⟨4,5,6⟩ = 32, run through the semantics while the book compiles:

def dot : List NanoI := [ .clracc, .li 0 1, .li 1 4, .mac 0 1, .li 0 2, .li 1 5, .mac 0 1, .li 0 3, .li 1 6, .mac 0 1, .movacc 2, .halt ] theorem dot_correct : (nrun (prog dot) 100 s0).1.regs 2 = 32 := (nrun (prog dot) 100 s0).fst.regs 2 = 32 All goals completed! 🐙

8.3. It is not fixed-function🔗

The line that separates a processor from a multiplier array is control flow — a loop whose length depends on the data. Here is Σ1..N as such a loop, on the very same seven instructions, computing Σ1..100 = 5050:

def sumN (N : Int) : List NanoI := [ .li 0 N, .li 1 0, .li 2 1, .li 3 (-1), .clracc, .mac 1 2, .mac 0 2, .movacc 1, -- s := s + i .clracc, .mac 0 2, .mac 2 3, .movacc 0, -- i := i − 1 .blz 0 3, -- if i ≤ 0 → halt .blz 3 (-9), -- else loop .halt, .halt ] theorem sum100 : (nrun (prog (sumN 100)) 100000 s0).1.regs 1 = 5050 := (nrun (prog (sumN 100)) 100000 s0).fst.regs 1 = 5050 All goals completed! 🐙

8.4. It is Turing-complete🔗

The clinching argument: the Nano core can execute subleq itself. Since subleq is universal, so is the Nano core. Here is the gadget — eight instructions computing mem[b] − mem[a] and branching on the sign — running on a concrete case (5 − 3 = 2):

def subleqGadget (c : Int) : List NanoI := [ .li 6 1, .li 7 (-1), .ld 2 0, .ld 3 1, .clracc, .mac 3 6, .mac 2 7, .movacc 3, -- r3 := mem[b] − mem[a] .st 1 3, .blz 3 c ] def subleqDemo : Nano := { s0 with regs := fun i => if i = 0 then 10 else if i = 1 then 11 else 0, mem := fun a => if a = 10 then 3 else if a = 11 then 5 else 0 } theorem subleq_correct : (nrun (prog (subleqGadget 99)) 100 subleqDemo).1.mem 11 = 2 := (nrun (prog (subleqGadget 99)) 100 subleqDemo).fst.mem 11 = 2 All goals completed! 🐙

And the semantics is not merely executable but symbolically tractable — the whole instruction set is seven one-line lemmas of this kind:

theorem mac_effect (p : Nat NanoI) (s : Nano) (ra rb : NReg) (h : p s.pc = .mac ra rb) : (nstep p s).map Nano.acc = some (s.acc + s.regs ra * s.regs rb) := p:Nat NanoIs:Nanora:NRegrb:NRegh:p s.pc = NanoI.mac ra rbOption.map Nano.acc (nstep p s) = some (s.acc + s.regs ra * s.regs rb) All goals completed! 🐙 end Nano

8.5. Why this is the tile🔗

Everything the element needs, it has, in seven instructions and a seven-line proof: it multiplies and adds at full rate, it runs arbitrary programs, and it is universal. What it does not carry — a thirty-two-entry register file, a sprawling opcode space, the machinery of a general CPU — was exactly the overhead that made the earlier element large and hard to route. Stripping to this core is not a compromise between density and generality; it is both at once. The next chapter measures precisely how much: in placed-and-routed silicon, and against the machines this is meant to compete with.