mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
253 lines
7.7 KiB
Text
253 lines
7.7 KiB
Text
/-
|
||
GoxelGridBus.lean - Goxel grid state machines over an internal serial bus
|
||
|
||
This module makes the goxel workbench idea executable: a goxel field is a
|
||
series of finite grid-local state machines, and cells communicate by addressed
|
||
packets carried through the existing braid serial transport surface.
|
||
-/
|
||
|
||
import Semantics.BraidSerial
|
||
|
||
set_option linter.dupNamespace false
|
||
|
||
namespace Semantics.GoxelGridBus
|
||
|
||
open Semantics.BraidSerial
|
||
open Semantics.FixedPoint
|
||
|
||
/-- Local finite phase of one goxel cell. -/
|
||
inductive GoxelPhase where
|
||
| empty
|
||
| idle
|
||
| active
|
||
| settled
|
||
| hold
|
||
deriving Repr, DecidableEq, BEq
|
||
|
||
/-- Finite bus commands. -/
|
||
inductive GoxelCommand where
|
||
| ping
|
||
| activate
|
||
| settle
|
||
| hold
|
||
deriving Repr, DecidableEq, BEq
|
||
|
||
namespace GoxelCommand
|
||
|
||
def toByte : GoxelCommand → UInt8
|
||
| ping => 0
|
||
| activate => 1
|
||
| settle => 2
|
||
| hold => 3
|
||
|
||
def ofByte (b : UInt8) : GoxelCommand :=
|
||
match b.toNat with
|
||
| 1 => activate
|
||
| 2 => settle
|
||
| 3 => hold
|
||
| _ => ping
|
||
|
||
end GoxelCommand
|
||
|
||
/-- Bounded grid address. Values are byte-sized at the serial boundary. -/
|
||
structure GoxelAddr where
|
||
row : UInt8
|
||
col : UInt8
|
||
deriving Repr, DecidableEq, BEq
|
||
|
||
namespace GoxelAddr
|
||
|
||
def zero : GoxelAddr := { row := 0, col := 0 }
|
||
|
||
def seqNum (a : GoxelAddr) : UInt16 :=
|
||
UInt16.ofNat (a.row.toNat * 256 + a.col.toNat)
|
||
|
||
def fromSeqNum (n : UInt16) : GoxelAddr :=
|
||
{ row := UInt8.ofNat (n.toNat / 256)
|
||
, col := UInt8.ofNat (n.toNat % 256) }
|
||
|
||
end GoxelAddr
|
||
|
||
/-- One goxel-local grid state machine. -/
|
||
structure GoxelCell where
|
||
addr : GoxelAddr
|
||
phase : GoxelPhase
|
||
scalar : Q16_16
|
||
residual : Q16_16
|
||
ticks : Nat
|
||
deriving Repr, DecidableEq, BEq
|
||
|
||
namespace GoxelCell
|
||
|
||
def empty (addr : GoxelAddr) : GoxelCell :=
|
||
{ addr := addr
|
||
, phase := GoxelPhase.empty
|
||
, scalar := Q16_16.zero
|
||
, residual := Q16_16.zero
|
||
, ticks := 0 }
|
||
|
||
def idle (addr : GoxelAddr) : GoxelCell :=
|
||
{ addr := addr
|
||
, phase := GoxelPhase.idle
|
||
, scalar := Q16_16.zero
|
||
, residual := Q16_16.zero
|
||
, ticks := 0 }
|
||
|
||
end GoxelCell
|
||
|
||
/-- Addressed packet on the internal bus. -/
|
||
structure GoxelBusPacket where
|
||
source : GoxelAddr
|
||
target : GoxelAddr
|
||
command : GoxelCommand
|
||
arg0 : UInt8
|
||
arg1 : UInt8
|
||
deriving Repr, DecidableEq, BEq
|
||
|
||
namespace GoxelBusPacket
|
||
|
||
/-- Encode into one braid serial packet. Payload is exactly four bytes. -/
|
||
def toSerialPacket (p : GoxelBusPacket) : SerialPacket :=
|
||
{ header := { packetType := p.command.toByte, seqNum := p.source.seqNum, length := 4 }
|
||
, payload := PacketPayload.fromBytes [p.target.row, p.target.col, p.arg0, p.arg1]
|
||
, bracket := BraidBracket.BraidBracket.zero
|
||
, residual := Q16_16.zero }
|
||
|
||
def byteAt (bytes : List UInt8) (idx : Nat) : UInt8 :=
|
||
(bytes[idx]?).getD 0
|
||
|
||
/-- Decode from one braid serial packet. Missing payload bytes fail closed to 0. -/
|
||
def fromSerialPacket (pkt : SerialPacket) : GoxelBusPacket :=
|
||
{ source := GoxelAddr.fromSeqNum pkt.header.seqNum
|
||
, target := { row := byteAt pkt.payload.bytes 0, col := byteAt pkt.payload.bytes 1 }
|
||
, command := GoxelCommand.ofByte pkt.header.packetType
|
||
, arg0 := byteAt pkt.payload.bytes 2
|
||
, arg1 := byteAt pkt.payload.bytes 3 }
|
||
|
||
def encodeFrame (p : GoxelBusPacket) (frameNum : UInt32) : BraidFrame :=
|
||
encodePacket p.toSerialPacket frameNum
|
||
|
||
def decodeFrame (frame : BraidFrame) : GoxelBusPacket × Bool :=
|
||
let decoded := BraidSerial.decodeFrame frame
|
||
(fromSerialPacket decoded.1, decoded.2)
|
||
|
||
end GoxelBusPacket
|
||
|
||
/-- Apply a command to one cell. -/
|
||
def applyCommand (cell : GoxelCell) (cmd : GoxelCommand) (arg0 arg1 : UInt8) : GoxelCell :=
|
||
let nextTicks := cell.ticks + 1
|
||
match cmd with
|
||
| GoxelCommand.ping =>
|
||
{ cell with ticks := nextTicks }
|
||
| GoxelCommand.activate =>
|
||
{ cell with
|
||
phase := GoxelPhase.active
|
||
scalar := Q16_16.ofRatio arg0.toNat 255
|
||
residual := Q16_16.ofRatio arg1.toNat 255
|
||
ticks := nextTicks }
|
||
| GoxelCommand.settle =>
|
||
{ cell with phase := GoxelPhase.settled, ticks := nextTicks }
|
||
| GoxelCommand.hold =>
|
||
{ cell with phase := GoxelPhase.hold, ticks := nextTicks }
|
||
|
||
/--
|
||
One local bus step. Addressed packets are consumed by the matching cell;
|
||
non-addressed packets are forwarded unchanged.
|
||
-/
|
||
def cellBusStep (cell : GoxelCell) (pkt : GoxelBusPacket) : GoxelCell × Option GoxelBusPacket :=
|
||
if cell.addr == pkt.target then
|
||
(applyCommand cell pkt.command pkt.arg0 pkt.arg1, none)
|
||
else
|
||
(cell, some pkt)
|
||
|
||
/-- A serial goxel grid is stored row-major. -/
|
||
structure GoxelGrid where
|
||
rows : Nat
|
||
cols : Nat
|
||
cells : List GoxelCell
|
||
deriving Repr, DecidableEq, BEq
|
||
|
||
namespace GoxelGrid
|
||
|
||
def addrOfIndex (cols idx : Nat) : GoxelAddr :=
|
||
{ row := UInt8.ofNat (idx / cols)
|
||
, col := UInt8.ofNat (idx % cols) }
|
||
|
||
def mkIdle (rows cols : Nat) : GoxelGrid :=
|
||
let count := rows * cols
|
||
let cells := (List.range count).map (fun idx => GoxelCell.idle (addrOfIndex cols idx))
|
||
{ rows := rows, cols := cols, cells := cells }
|
||
|
||
def cellAt (g : GoxelGrid) (idx : Nat) : Option GoxelCell :=
|
||
g.cells[idx]?
|
||
|
||
/-- Route one packet through the row-major serial bus. -/
|
||
def routePacket (g : GoxelGrid) (pkt : GoxelBusPacket) : GoxelGrid × Option GoxelBusPacket :=
|
||
let rec go (current : GoxelBusPacket) : List GoxelCell → List GoxelCell × Option GoxelBusPacket
|
||
| [] => ([], some current)
|
||
| cell :: rest =>
|
||
let stepped := cellBusStep cell current
|
||
match stepped.2 with
|
||
| none => (stepped.1 :: rest, none)
|
||
| some forwarded =>
|
||
let tail := go forwarded rest
|
||
(stepped.1 :: tail.1, tail.2)
|
||
let routed := go pkt g.cells
|
||
({ g with cells := routed.1 }, routed.2)
|
||
|
||
end GoxelGrid
|
||
|
||
/-- Sample addressed packet from cell (0,0) to cell (0,1). -/
|
||
def witnessActivatePacket : GoxelBusPacket :=
|
||
{ source := { row := 0, col := 0 }
|
||
, target := { row := 0, col := 1 }
|
||
, command := GoxelCommand.activate
|
||
, arg0 := 128
|
||
, arg1 := 7 }
|
||
|
||
/-- Encoding through the internal serial bus preserves the target address. -/
|
||
theorem serialRoundtripPreservesTarget :
|
||
let decoded := (GoxelBusPacket.decodeFrame (GoxelBusPacket.encodeFrame witnessActivatePacket 4)).1
|
||
decoded.target = witnessActivatePacket.target := by
|
||
native_decide
|
||
|
||
/-- Encoding through the internal serial bus preserves the command. -/
|
||
theorem serialRoundtripPreservesCommand :
|
||
let decoded := (GoxelBusPacket.decodeFrame (GoxelBusPacket.encodeFrame witnessActivatePacket 4)).1
|
||
decoded.command = witnessActivatePacket.command := by
|
||
native_decide
|
||
|
||
/-- A targeted packet activates the target cell and is consumed. -/
|
||
theorem targetedPacketActivatesAndConsumes :
|
||
let routed := GoxelGrid.routePacket (GoxelGrid.mkIdle 1 2) witnessActivatePacket
|
||
routed.2 = none ∧ (routed.1.cellAt 1).map (fun c => c.phase) = some GoxelPhase.active := by
|
||
native_decide
|
||
|
||
/-- A packet with no matching address stays on the serial bus. -/
|
||
theorem unmatchedPacketForwards :
|
||
let pkt : GoxelBusPacket :=
|
||
{ source := { row := 0, col := 0 }
|
||
, target := { row := 9, col := 9 }
|
||
, command := GoxelCommand.activate
|
||
, arg0 := 1
|
||
, arg1 := 0 }
|
||
(GoxelGrid.routePacket (GoxelGrid.mkIdle 1 2) pkt).2 = some pkt := by
|
||
native_decide
|
||
|
||
/-- HOLD is an explicit phase, not an implicit failure. -/
|
||
theorem holdCommandSetsHoldPhase :
|
||
let pkt : GoxelBusPacket :=
|
||
{ source := { row := 0, col := 0 }
|
||
, target := { row := 0, col := 0 }
|
||
, command := GoxelCommand.hold
|
||
, arg0 := 0
|
||
, arg1 := 0 }
|
||
let routed := GoxelGrid.routePacket (GoxelGrid.mkIdle 1 1) pkt
|
||
(routed.1.cellAt 0).map (fun c => c.phase) = some GoxelPhase.hold := by
|
||
native_decide
|
||
|
||
#eval (GoxelBusPacket.decodeFrame (GoxelBusPacket.encodeFrame witnessActivatePacket 4)).1.command
|
||
#eval (GoxelGrid.routePacket (GoxelGrid.mkIdle 1 2) witnessActivatePacket).2
|
||
#eval (GoxelGrid.routePacket (GoxelGrid.mkIdle 1 2) witnessActivatePacket).1.cellAt 1 |>.map (fun c => c.phase)
|
||
|
||
end Semantics.GoxelGridBus
|