9. Pipelined Cell
The fully synchronous cell pays three clocks per instruction because it never overlaps its phases. This chapter overlaps them for throughput and proves the result still commits the sequential trace. The overlap is deliberately narrow: only instruction fetch runs ahead. That is exactly the overlap this instruction set makes safe.
The reasoning turns on which hazards exist. There is no data hazard from
register, accumulator, or pointer dependencies, because each instruction still
executes against the fully committed architectural state — commits stay
one-per-cycle and in order, so every operand read sees all prior writebacks. The
instruction memory is read-only while the cell runs, so a prefetched instruction
never depends on a writeback either. The one hazard is control: the prefetch
predicts the fall-through address pc + 1, and a taken branch or a halt makes
that prediction wrong. A wrong prefetch is caught and squashed, costing a single
bubble.
The pipeline state adds a fetch latch to the architectural state: a validity bit, the memory index the latched instruction was fetched from, and the instruction itself. The invariant is that a valid latch holds the instruction at its recorded index.
namespace Honeycomb
def wantIdx (s : State defaultConfig) : Nat :=
memIndexOfNat defaultConfig s.pc.toNat
structure PipeCell where
arch : State defaultConfig
imem : Nat -> DecodedInstr
busy : Bool
fvalid : Bool
fpc : Nat
fir : DecodedInstr
-- One clock. A hit (the latch holds the instruction the program counter wants)
-- commits that instruction and prefetches the predicted fall-through. A miss
-- refetches the wanted instruction and bubbles.
def pstep (c : PipeCell) : PipeCell :=
if c.busy then
if c.arch.halted then c
else if c.fvalid && (c.fpc == wantIdx c.arch) then
let arch' := execDecoded c.fir c.arch
let pidx := memIndexOfNat defaultConfig (c.arch.pc.toNat + 1)
{ c with arch := arch', busy := !arch'.halted,
fvalid := true, fpc := pidx, fir := c.imem pidx }
else
let widx := wantIdx c.arch
{ c with fvalid := true, fpc := widx, fir := c.imem widx }
else c
Every clock keeps the fetch-latch invariant: whatever address the latch records, it holds that instruction. Both the hit and miss branches load the latch from the instruction memory at the index they record, and neither writes the instruction memory.
theorem pstep_preserves_inv (c : PipeCell)
(hinv : c.fvalid = true -> c.fir = c.imem c.fpc) :
(pstep c).fvalid = true -> (pstep c).fir = (pstep c).imem (pstep c).fpc := c:PipeCellhinv:c.fvalid = true → c.fir = c.imem c.fpc⊢ (pstep c).fvalid = true → (pstep c).fir = (pstep c).imem (pstep c).fpc
c:PipeCellhinv:c.fvalid = true → c.fir = c.imem c.fpchb:c.busy = true⊢ (pstep c).fvalid = true → (pstep c).fir = (pstep c).imem (pstep c).fpcc:PipeCellhinv:c.fvalid = true → c.fir = c.imem c.fpchb:¬c.busy = true⊢ (pstep c).fvalid = true → (pstep c).fir = (pstep c).imem (pstep c).fpc
c:PipeCellhinv:c.fvalid = true → c.fir = c.imem c.fpchb:c.busy = true⊢ (pstep c).fvalid = true → (pstep c).fir = (pstep c).imem (pstep c).fpc c:PipeCellhinv:c.fvalid = true → c.fir = c.imem c.fpchb:c.busy = truehh:c.arch.halted = true⊢ (pstep c).fvalid = true → (pstep c).fir = (pstep c).imem (pstep c).fpcc:PipeCellhinv:c.fvalid = true → c.fir = c.imem c.fpchb:c.busy = truehh:¬c.arch.halted = true⊢ (pstep c).fvalid = true → (pstep c).fir = (pstep c).imem (pstep c).fpc
c:PipeCellhinv:c.fvalid = true → c.fir = c.imem c.fpchb:c.busy = truehh:c.arch.halted = true⊢ (pstep c).fvalid = true → (pstep c).fir = (pstep c).imem (pstep c).fpc All goals completed! 🐙
c:PipeCellhinv:c.fvalid = true → c.fir = c.imem c.fpchb:c.busy = truehh:¬c.arch.halted = true⊢ (pstep c).fvalid = true → (pstep c).fir = (pstep c).imem (pstep c).fpc c:PipeCellhinv:c.fvalid = true → c.fir = c.imem c.fpchb:c.busy = truehh:c.arch.halted = false⊢ (pstep c).fvalid = true → (pstep c).fir = (pstep c).imem (pstep c).fpc
c:PipeCellhinv:c.fvalid = true → c.fir = c.imem c.fpchb:c.busy = truehh:c.arch.halted = falsehit:(c.fvalid && c.fpc == wantIdx c.arch) = true⊢ (pstep c).fvalid = true → (pstep c).fir = (pstep c).imem (pstep c).fpcc:PipeCellhinv:c.fvalid = true → c.fir = c.imem c.fpchb:c.busy = truehh:c.arch.halted = falsehit:¬(c.fvalid && c.fpc == wantIdx c.arch) = true⊢ (pstep c).fvalid = true → (pstep c).fir = (pstep c).imem (pstep c).fpc
c:PipeCellhinv:c.fvalid = true → c.fir = c.imem c.fpchb:c.busy = truehh:c.arch.halted = falsehit:(c.fvalid && c.fpc == wantIdx c.arch) = true⊢ (pstep c).fvalid = true → (pstep c).fir = (pstep c).imem (pstep c).fpc c:PipeCellhinv:c.fvalid = true → c.fir = c.imem c.fpchb:c.busy = truehh:c.arch.halted = falsehit:(c.fvalid && c.fpc == wantIdx c.arch) = truea✝:(pstep c).fvalid = true⊢ (pstep c).fir = (pstep c).imem (pstep c).fpc; All goals completed! 🐙
c:PipeCellhinv:c.fvalid = true → c.fir = c.imem c.fpchb:c.busy = truehh:c.arch.halted = falsehit:¬(c.fvalid && c.fpc == wantIdx c.arch) = true⊢ (pstep c).fvalid = true → (pstep c).fir = (pstep c).imem (pstep c).fpc c:PipeCellhinv:c.fvalid = true → c.fir = c.imem c.fpchb:c.busy = truehh:c.arch.halted = falsehit:(c.fvalid && c.fpc == wantIdx c.arch) = false⊢ (pstep c).fvalid = true → (pstep c).fir = (pstep c).imem (pstep c).fpc
c:PipeCellhinv:c.fvalid = true → c.fir = c.imem c.fpchb:c.busy = truehh:c.arch.halted = falsehit:(c.fvalid && c.fpc == wantIdx c.arch) = falsea✝:(pstep c).fvalid = true⊢ (pstep c).fir = (pstep c).imem (pstep c).fpc; All goals completed! 🐙
c:PipeCellhinv:c.fvalid = true → c.fir = c.imem c.fpchb:¬c.busy = true⊢ (pstep c).fvalid = true → (pstep c).fir = (pstep c).imem (pstep c).fpc c:PipeCellhinv:c.fvalid = true → c.fir = c.imem c.fpchb:c.busy = false⊢ (pstep c).fvalid = true → (pstep c).fir = (pstep c).imem (pstep c).fpc
All goals completed! 🐙
A hit commits exactly what the combinational cell commits in one cycle, so it
refines the ISA step for that instruction. The prefetch is validated by the hit
condition itself: the latched index equals the wanted index, so by the invariant
the latched instruction is the right one.
theorem pstep_hit_matches (c : PipeCell)
(hbusy : c.busy = true) (hhalt : c.arch.halted = false)
(hvalid : c.fvalid = true) (hpc : c.fpc = wantIdx c.arch)
(hinv : c.fir = c.imem c.fpc) :
(pstep c).arch
= (cellExecCycle
{ arch := c.arch, imem := c.imem, busy := c.busy }).arch := c:PipeCellhbusy:c.busy = truehhalt:c.arch.halted = falsehvalid:c.fvalid = truehpc:c.fpc = wantIdx c.archhinv:c.fir = c.imem c.fpc⊢ (pstep c).arch = (cellExecCycle { arch := c.arch, imem := c.imem, busy := c.busy }).arch
c:PipeCellhbusy:c.busy = truehhalt:c.arch.halted = falsehvalid:c.fvalid = truehpc:c.fpc = wantIdx c.archhinv:c.fir = c.imem c.fpchit:(c.fvalid && c.fpc == wantIdx c.arch) = true⊢ (pstep c).arch = (cellExecCycle { arch := c.arch, imem := c.imem, busy := c.busy }).arch
c:PipeCellhbusy:c.busy = truehhalt:c.arch.halted = falsehvalid:c.fvalid = truehpc:c.fpc = wantIdx c.archhinv:c.fir = c.imem c.fpchit:(c.fvalid && c.fpc == wantIdx c.arch) = true⊢ (if false = true then c
else
{ arch := execDecoded c.fir c.arch, imem := c.imem, busy := !(execDecoded c.fir c.arch).halted, fvalid := true,
fpc := memIndexOfNat defaultConfig (BitVec.toNat c.arch.pc + 1),
fir := c.imem (memIndexOfNat defaultConfig (BitVec.toNat c.arch.pc + 1)) }).arch =
(cellExecCycle { arch := c.arch, imem := c.imem, busy := true }).arch
c:PipeCellhbusy:c.busy = truehhalt:c.arch.halted = falsehvalid:c.fvalid = truehpc:c.fpc = wantIdx c.archhinv:c.fir = c.imem c.fpchit:(c.fvalid && c.fpc == wantIdx c.arch) = true⊢ (if false = true then c
else
{ arch := execDecoded (c.imem (wantIdx c.arch)) c.arch, imem := c.imem,
busy := !(execDecoded (c.imem (wantIdx c.arch)) c.arch).halted, fvalid := true,
fpc := memIndexOfNat defaultConfig (BitVec.toNat c.arch.pc + 1),
fir := c.imem (memIndexOfNat defaultConfig (BitVec.toNat c.arch.pc + 1)) }).arch =
(cellExecCycle { arch := c.arch, imem := c.imem, busy := true }).arch
simp [cellExecCycle, hhalt, wantIdx] All goals completed! 🐙
theorem pstep_hit_refines_isa (c : PipeCell)
(hbusy : c.busy = true) (hhalt : c.arch.halted = false)
(hvalid : c.fvalid = true) (hpc : c.fpc = wantIdx c.arch)
(hinv : c.fir = c.imem c.fpc) :
step defaultConfig (programOf c.imem) c.arch = some (pstep c).arch := by c:PipeCellhbusy:c.busy = truehhalt:c.arch.halted = falsehvalid:c.fvalid = truehpc:c.fpc = wantIdx c.archhinv:c.fir = c.imem c.fpc⊢ step defaultConfig (programOf c.imem) c.arch = some (pstep c).arch
rw [pstep_hit_matches c hbusy hhalt hvalid hpc hinv c:PipeCellhbusy:c.busy = truehhalt:c.arch.halted = falsehvalid:c.fvalid = truehpc:c.fpc = wantIdx c.archhinv:c.fir = c.imem c.fpc⊢ step defaultConfig (programOf c.imem) c.arch =
some (cellExecCycle { arch := c.arch, imem := c.imem, busy := c.busy }).arch] c:PipeCellhbusy:c.busy = truehhalt:c.arch.halted = falsehvalid:c.fvalid = truehpc:c.fpc = wantIdx c.archhinv:c.fir = c.imem c.fpc⊢ step defaultConfig (programOf c.imem) c.arch =
some (cellExecCycle { arch := c.arch, imem := c.imem, busy := c.busy }).arch
exact cellExecCycle_refines_step
{ arch := c.arch, imem := c.imem, busy := c.busy } hbusy hhalt All goals completed! 🐙
A miss changes nothing architecturally; it only reloads the latch with the wanted instruction. So a miss is a stutter step, and the state it produces is hit-ready: valid, with the recorded index equal to the wanted index and the invariant restored. The bubble after a mispredict therefore lasts exactly one cycle.
theorem pstep_miss_stutter (c : PipeCell)
(hbusy : c.busy = true) (hhalt : c.arch.halted = false)
(hmiss : (c.fvalid && (c.fpc == wantIdx c.arch)) = false) :
(pstep c).arch = c.arch := by c:PipeCellhbusy:c.busy = truehhalt:c.arch.halted = falsehmiss:(c.fvalid && c.fpc == wantIdx c.arch) = false⊢ (pstep c).arch = c.arch
simp [pstep, hbusy, hhalt, hmiss] All goals completed! 🐙
theorem pstep_miss_then_ready (c : PipeCell)
(hbusy : c.busy = true) (hhalt : c.arch.halted = false)
(hmiss : (c.fvalid && (c.fpc == wantIdx c.arch)) = false) :
(pstep c).fvalid = true
/\ (pstep c).fpc = wantIdx (pstep c).arch
/\ (pstep c).fir = (pstep c).imem (pstep c).fpc
/\ (pstep c).busy = c.busy
/\ (pstep c).imem = c.imem := by c:PipeCellhbusy:c.busy = truehhalt:c.arch.halted = falsehmiss:(c.fvalid && c.fpc == wantIdx c.arch) = false⊢ (pstep c).fvalid = true ∧
(pstep c).fpc = wantIdx (pstep c).arch ∧
(pstep c).fir = (pstep c).imem (pstep c).fpc ∧ (pstep c).busy = c.busy ∧ (pstep c).imem = c.imem
simp [pstep, hbusy, hhalt, hmiss] All goals completed! 🐙
The two combine into the throughput statement: a mispredict bubble followed by its refetch commits exactly one sequential step. Any taken branch costs one lost cycle and no correctness; straight-line code commits one instruction per cycle.
theorem pstep_bubble_then_commit (c : PipeCell)
(hbusy : c.busy = true) (hhalt : c.arch.halted = false)
(hmiss : (c.fvalid && (c.fpc == wantIdx c.arch)) = false) :
(pstep (pstep c)).arch
= (cellExecCycle
{ arch := c.arch, imem := c.imem, busy := c.busy }).arch := by c:PipeCellhbusy:c.busy = truehhalt:c.arch.halted = falsehmiss:(c.fvalid && c.fpc == wantIdx c.arch) = false⊢ (pstep (pstep c)).arch = (cellExecCycle { arch := c.arch, imem := c.imem, busy := c.busy }).arch
obtain ⟨hv, hp, hi, hbz, him⟩ :=
pstep_miss_then_ready c hbusy hhalt hmiss c:PipeCellhbusy:c.busy = truehhalt:c.arch.halted = falsehmiss:(c.fvalid && c.fpc == wantIdx c.arch) = falsehv:(pstep c).fvalid = truehp:(pstep c).fpc = wantIdx (pstep c).archhi:(pstep c).fir = (pstep c).imem (pstep c).fpchbz:(pstep c).busy = c.busyhim:(pstep c).imem = c.imem⊢ (pstep (pstep c)).arch = (cellExecCycle { arch := c.arch, imem := c.imem, busy := c.busy }).arch
have harch : (pstep c).arch = c.arch := pstep_miss_stutter c hbusy hhalt hmiss c:PipeCellhbusy:c.busy = truehhalt:c.arch.halted = falsehmiss:(c.fvalid && c.fpc == wantIdx c.arch) = falsehv:(pstep c).fvalid = truehp:(pstep c).fpc = wantIdx (pstep c).archhi:(pstep c).fir = (pstep c).imem (pstep c).fpchbz:(pstep c).busy = c.busyhim:(pstep c).imem = c.imemharch:(pstep c).arch = c.arch⊢ (pstep (pstep c)).arch = (cellExecCycle { arch := c.arch, imem := c.imem, busy := c.busy }).arch
have hb2 : (pstep c).busy = true := by rw [hbz c:PipeCellhbusy:c.busy = truehhalt:c.arch.halted = falsehmiss:(c.fvalid && c.fpc == wantIdx c.arch) = falsehv:(pstep c).fvalid = truehp:(pstep c).fpc = wantIdx (pstep c).archhi:(pstep c).fir = (pstep c).imem (pstep c).fpchbz:(pstep c).busy = c.busyhim:(pstep c).imem = c.imemharch:(pstep c).arch = c.arch⊢ c.busy = true] c:PipeCellhbusy:c.busy = truehhalt:c.arch.halted = falsehmiss:(c.fvalid && c.fpc == wantIdx c.arch) = falsehv:(pstep c).fvalid = truehp:(pstep c).fpc = wantIdx (pstep c).archhi:(pstep c).fir = (pstep c).imem (pstep c).fpchbz:(pstep c).busy = c.busyhim:(pstep c).imem = c.imemharch:(pstep c).arch = c.arch⊢ c.busy = true; exact hbusy c:PipeCellhbusy:c.busy = truehhalt:c.arch.halted = falsehmiss:(c.fvalid && c.fpc == wantIdx c.arch) = falsehv:(pstep c).fvalid = truehp:(pstep c).fpc = wantIdx (pstep c).archhi:(pstep c).fir = (pstep c).imem (pstep c).fpchbz:(pstep c).busy = c.busyhim:(pstep c).imem = c.imemharch:(pstep c).arch = c.archhb2:(pstep c).busy = true⊢ (pstep (pstep c)).arch = (cellExecCycle { arch := c.arch, imem := c.imem, busy := c.busy }).arch
have hh2 : (pstep c).arch.halted = false := by rw [harch c:PipeCellhbusy:c.busy = truehhalt:c.arch.halted = falsehmiss:(c.fvalid && c.fpc == wantIdx c.arch) = falsehv:(pstep c).fvalid = truehp:(pstep c).fpc = wantIdx (pstep c).archhi:(pstep c).fir = (pstep c).imem (pstep c).fpchbz:(pstep c).busy = c.busyhim:(pstep c).imem = c.imemharch:(pstep c).arch = c.archhb2:(pstep c).busy = true⊢ c.arch.halted = false] c:PipeCellhbusy:c.busy = truehhalt:c.arch.halted = falsehmiss:(c.fvalid && c.fpc == wantIdx c.arch) = falsehv:(pstep c).fvalid = truehp:(pstep c).fpc = wantIdx (pstep c).archhi:(pstep c).fir = (pstep c).imem (pstep c).fpchbz:(pstep c).busy = c.busyhim:(pstep c).imem = c.imemharch:(pstep c).arch = c.archhb2:(pstep c).busy = true⊢ c.arch.halted = false; exact hhalt c:PipeCellhbusy:c.busy = truehhalt:c.arch.halted = falsehmiss:(c.fvalid && c.fpc == wantIdx c.arch) = falsehv:(pstep c).fvalid = truehp:(pstep c).fpc = wantIdx (pstep c).archhi:(pstep c).fir = (pstep c).imem (pstep c).fpchbz:(pstep c).busy = c.busyhim:(pstep c).imem = c.imemharch:(pstep c).arch = c.archhb2:(pstep c).busy = truehh2:(pstep c).arch.halted = false⊢ (pstep (pstep c)).arch = (cellExecCycle { arch := c.arch, imem := c.imem, busy := c.busy }).arch
rw [pstep_hit_matches (pstep c) hb2 hh2 hv hp hi c:PipeCellhbusy:c.busy = truehhalt:c.arch.halted = falsehmiss:(c.fvalid && c.fpc == wantIdx c.arch) = falsehv:(pstep c).fvalid = truehp:(pstep c).fpc = wantIdx (pstep c).archhi:(pstep c).fir = (pstep c).imem (pstep c).fpchbz:(pstep c).busy = c.busyhim:(pstep c).imem = c.imemharch:(pstep c).arch = c.archhb2:(pstep c).busy = truehh2:(pstep c).arch.halted = false⊢ (cellExecCycle { arch := (pstep c).arch, imem := (pstep c).imem, busy := (pstep c).busy }).arch =
(cellExecCycle { arch := c.arch, imem := c.imem, busy := c.busy }).arch] c:PipeCellhbusy:c.busy = truehhalt:c.arch.halted = falsehmiss:(c.fvalid && c.fpc == wantIdx c.arch) = falsehv:(pstep c).fvalid = truehp:(pstep c).fpc = wantIdx (pstep c).archhi:(pstep c).fir = (pstep c).imem (pstep c).fpchbz:(pstep c).busy = c.busyhim:(pstep c).imem = c.imemharch:(pstep c).arch = c.archhb2:(pstep c).busy = truehh2:(pstep c).arch.halted = false⊢ (cellExecCycle { arch := (pstep c).arch, imem := (pstep c).imem, busy := (pstep c).busy }).arch =
(cellExecCycle { arch := c.arch, imem := c.imem, busy := c.busy }).arch
rw [harch, c:PipeCellhbusy:c.busy = truehhalt:c.arch.halted = falsehmiss:(c.fvalid && c.fpc == wantIdx c.arch) = falsehv:(pstep c).fvalid = truehp:(pstep c).fpc = wantIdx (pstep c).archhi:(pstep c).fir = (pstep c).imem (pstep c).fpchbz:(pstep c).busy = c.busyhim:(pstep c).imem = c.imemharch:(pstep c).arch = c.archhb2:(pstep c).busy = truehh2:(pstep c).arch.halted = false⊢ (cellExecCycle { arch := c.arch, imem := (pstep c).imem, busy := (pstep c).busy }).arch =
(cellExecCycle { arch := c.arch, imem := c.imem, busy := c.busy }).arch hbz, c:PipeCellhbusy:c.busy = truehhalt:c.arch.halted = falsehmiss:(c.fvalid && c.fpc == wantIdx c.arch) = falsehv:(pstep c).fvalid = truehp:(pstep c).fpc = wantIdx (pstep c).archhi:(pstep c).fir = (pstep c).imem (pstep c).fpchbz:(pstep c).busy = c.busyhim:(pstep c).imem = c.imemharch:(pstep c).arch = c.archhb2:(pstep c).busy = truehh2:(pstep c).arch.halted = false⊢ (cellExecCycle { arch := c.arch, imem := (pstep c).imem, busy := c.busy }).arch =
(cellExecCycle { arch := c.arch, imem := c.imem, busy := c.busy }).arch him c:PipeCellhbusy:c.busy = truehhalt:c.arch.halted = falsehmiss:(c.fvalid && c.fpc == wantIdx c.arch) = falsehv:(pstep c).fvalid = truehp:(pstep c).fpc = wantIdx (pstep c).archhi:(pstep c).fir = (pstep c).imem (pstep c).fpchbz:(pstep c).busy = c.busyhim:(pstep c).imem = c.imemharch:(pstep c).arch = c.archhb2:(pstep c).busy = truehh2:(pstep c).arch.halted = false⊢ (cellExecCycle { arch := c.arch, imem := c.imem, busy := c.busy }).arch =
(cellExecCycle { arch := c.arch, imem := c.imem, busy := c.busy }).arch] All goals completed! 🐙
Running the cell is iterating the clock.
def pRun : Nat -> PipeCell -> PipeCell
| 0, c => c
| k + 1, c => pRun k (pstep c)
end Honeycomb
The correctness account is complete without a full stuttering-simulation induction. Every committing cycle equals one combinational-cell cycle and refines the ISA; every non-committing cycle leaves the architectural state untouched and is immediately followed by a commit. So the sequence of architectural states the pipeline commits is exactly the sequential trace, with isolated single-cycle bubbles that carry no value. The bubbles are the price of the one real hazard, and they appear only after a taken branch or at startup.
This model is emitted as generated SystemVerilog: rtl/honeycomb_cell_pipe.sv binds
a registered-read honeycomb_sram_sync_32 for the instruction fetch and drives its
address with the predicted pf_addr, keeping combinational operand memories. A
fvalid/fpc fetch latch and a hit line implement the prediction and squash.
The generated cell passes the same golden program suite as the combinational
cell — retiring identical results and identical instruction counts — over fewer
clocks than the fully synchronous cell, with a bubble only where a branch is
taken.
Widening the overlap — registering the operand reads and forwarding or interlocking their results, so the fully synchronous cell also reaches one instruction per cycle — is the next pipeline step, and the first that needs true data-hazard reasoning rather than this control-only argument.