proarrow-0.4.0.0: src/Proarrow/Category/Instance/Circuit.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
-- | Synchronous digital circuits. The tensor preserves the grouping of bit bundles; copying is
-- fan-out, and tracing connects wires without adding a delay. 'register' delays a bit by one
-- clock cycle. 'prepare' rejects observable combinational cycles before simulation.
--
-- >>> simulate notGate [False, True]
-- Right [True,False]
-- >>> simulate (register False) [True, False, True]
-- Right [False,True,False]
module Proarrow.Category.Instance.Circuit
( Wires (..)
, KnownWires
, Value
, Sample
, PREPARED (..)
, Prepared
, Circuit
, constant
, notGate
, andGate
, orGate
, xorGate
, mux
, register
, Netlist
, netlist
, renderNetlist
, toDot
, toVerilog
, CircuitError (..)
, Simulator
, prepare
, step
, simulate
) where
import Control.Monad ((>=>))
import Data.Graph (SCC (..), stronglyConnComp)
import Data.IntMap.Strict qualified as IM
import Data.IntSet qualified as IS
import Data.Kind (Constraint, Type)
import Data.List qualified as List
import Prelude hiding (Functor, fmap, id, (**), (.))
import Prelude qualified as P
import Proarrow.Category.Instance.Simulator (SIMULATOR (..), Simulator (..), runSimulator, step)
import Proarrow.Category.Monoidal (Monoidal (..), MonoidalProfunctor (..), SymMonoidal (..), Tensor)
import Proarrow.Category.Monoidal.CopyDiscard (CopyDiscard)
import Proarrow.Category.Monoidal.Strength (Costrong (..))
import Proarrow.Core (CAT, CategoryOf (..), Is, Profunctor (..), Promonad (..), UN, dimapDefault, type (+->))
import Proarrow.Functor (FunctorForRep (..))
import Proarrow.Monoid (CocommutativeComonoid, Comonoid (..))
import Proarrow.Profunctor.Representable (Rep (..))
-- | A bit, no wires, or two bundles. Tensor grouping is retained in the type.
type data Wires = Bit | Empty | Pair Wires Wires
type Shape :: Wires -> Type
data Shape a where
BitS :: Shape Bit
EmptyS :: Shape Empty
PairS :: Shape a -> Shape b -> Shape (Pair a b)
type KnownWires :: Wires -> Constraint
class (Is Sim (Value @ Ready a)) => KnownWires a where
shape :: Shape a
instance KnownWires Bit where shape = BitS
instance KnownWires Empty where shape = EmptyS
instance (KnownWires a, KnownWires b) => KnownWires (Pair a b) where shape = PairS (shape @a) (shape @b)
-- | Objects of validated circuits, with the same wire shapes as 'Wires'.
type data PREPARED = Ready Wires
-- | Interpret validated circuits as state machines. Bits become 'Bool', the unit becomes @()@,
-- and a tensor becomes a pair, all in the 'SIMULATOR' category.
data family Value :: PREPARED +-> SIMULATOR
-- | The Haskell value for one clock cycle at a wire shape.
type Sample :: Wires -> Type
type Sample a = UN Sim (Value @ Ready a)
type Bundle :: Wires -> Type
data Bundle a where
BBit :: Int -> Bundle Bit
BEmpty :: Bundle Empty
BPair :: Bundle a -> Bundle b -> Bundle (Pair a b)
ports :: Bundle a -> [Int]
ports bundle = go bundle []
where
go :: Bundle b -> [Int] -> [Int]
go (BBit n) rest = n : rest
go BEmpty rest = rest
go (BPair a b) rest = go a (go b rest)
inputValues :: Bundle a -> Sample a -> [(Int, Bool)]
inputValues bundle value = go bundle value []
where
go :: Bundle b -> Sample b -> [(Int, Bool)] -> [(Int, Bool)]
go (BBit n) bit rest = (n, bit) : rest
go BEmpty () rest = rest
go (BPair a b) (x, y) rest = go a x (go b y rest)
output :: Bundle a -> IM.IntMap Bool -> Sample a
output (BBit n) bs = bs IM.! n
output BEmpty _ = ()
output (BPair a b) bs = (output a bs, output b bs)
-- Each bit has one driver. Aliases connect the placeholders allocated by a trace.
data Driver
= Input
| Constant Bool
| Not Int
| And Int Int
| Or Int Int
| Xor Int Int
| Mux Int Int Int
| Register Bool Int
| Alias Int
deriving (Show)
inputs :: Driver -> [Int]
inputs Input = []
inputs (Constant _) = []
inputs (Not a) = [a]
inputs (And a b) = [a, b]
inputs (Or a b) = [a, b]
inputs (Xor a b) = [a, b]
inputs (Mux s a b) = [s, a, b]
inputs (Register _ a) = [a]
inputs (Alias a) = [a]
data BuildState = BuildState Int (IM.IntMap Driver)
newtype Build a = Build {runBuild :: BuildState -> (a, BuildState)}
instance P.Functor Build where
fmap f (Build g) = Build \s -> let (x, s') = g s in (f x, s')
instance Applicative Build where
pure x = Build (x,)
f <*> x = do g <- f; g <$> x
instance Monad Build where
Build f >>= k = Build \s -> let (x, s') = f s in runBuild (k x) s'
fresh :: Driver -> Build Int
fresh d = Build \(BuildState n ds) -> (n, BuildState (n + 1) (IM.insert n d ds))
freshBundle :: Driver -> Shape a -> Build (Bundle a)
freshBundle d BitS = BBit <$> fresh d
freshBundle _ EmptyS = pure BEmpty
freshBundle d (PairS a b) = BPair <$> freshBundle d a <*> freshBundle d b
connect :: Bundle a -> Bundle a -> Build ()
connect (BBit a) (BBit b) = Build \(BuildState n ds) -> ((), BuildState n (IM.insert a (Alias b) ds))
connect BEmpty BEmpty = pure ()
connect (BPair a b) (BPair c d) = connect a c >> connect b d
-- | A typed circuit builder. Constructors are hidden so every output bit has one driver.
type Circuit :: CAT Wires
data Circuit a b where
Circuit :: (KnownWires a, KnownWires b) => (Bundle a -> Build (Bundle b)) -> Circuit a b
instance CategoryOf Wires where
type (~>) = Circuit
type Ob a = KnownWires a
instance Profunctor Circuit where
dimap = dimapDefault
r \\ Circuit{} = r
instance Promonad Circuit where
id = Circuit pure
Circuit f . Circuit g = Circuit (g >=> f)
instance MonoidalProfunctor Circuit where
one = Circuit pure
Circuit f ** Circuit g = Circuit \(BPair a b) -> BPair <$> f a <*> g b
instance Monoidal Wires where
type Unit = Empty
type a ** b = Pair a b
withOb2 r = r
leftUnitor = Circuit \(BPair BEmpty a) -> pure a
leftUnitorInv = Circuit \a -> pure (BPair BEmpty a)
rightUnitor = Circuit \(BPair a BEmpty) -> pure a
rightUnitorInv = Circuit \a -> pure (BPair a BEmpty)
associator = Circuit \(BPair (BPair a b) c) -> pure (BPair a (BPair b c))
associatorInv = Circuit \(BPair a (BPair b c)) -> pure (BPair (BPair a b) c)
instance SymMonoidal Wires where
swap = Circuit \(BPair a b) -> pure (BPair b a)
instance (KnownWires a) => Comonoid a where
counit = Circuit \_ -> pure BEmpty
comult = Circuit \a -> pure (BPair a a)
instance (KnownWires a) => CocommutativeComonoid a
instance CopyDiscard Wires
instance Costrong Tensor Circuit where
coact @a (Circuit f) = Circuit \x -> do
feedback <- freshBundle Input (shape @a)
result <- f (BPair feedback x)
case result of
BPair feedback' y -> do
connect feedback feedback'
pure y
-- | A constant bit, independent of the clock.
constant :: Bool -> Empty ~> Bit
constant b = Circuit \BEmpty -> BBit <$> fresh (Constant b)
notGate :: Bit ~> Bit
notGate = Circuit \(BBit a) -> BBit <$> fresh (Not a)
andGate :: Bit ** Bit ~> Bit
andGate = Circuit \(BPair (BBit a) (BBit b)) -> BBit <$> fresh (And a b)
orGate :: Bit ** Bit ~> Bit
orGate = Circuit \(BPair (BBit a) (BBit b)) -> BBit <$> fresh (Or a b)
xorGate :: Bit ** Bit ~> Bit
xorGate = Circuit \(BPair (BBit a) (BBit b)) -> BBit <$> fresh (Xor a b)
-- | Select the second input when the first is true, and the third when it is false.
mux :: Bit ** Bit ** Bit ~> Bit
mux = Circuit \(BPair (BPair (BBit s) (BBit a)) (BBit b)) -> BBit <$> fresh (Mux s a b)
-- | A register's output starts at the given value. Each clock edge replaces it with its input
-- from the preceding cycle. All registers update simultaneously.
register :: Bool -> Bit ~> Bit
register initial = Circuit \(BBit a) -> BBit <$> fresh (Register initial a)
-- | A netlist with typed boundaries. Disconnected components are omitted.
type Netlist :: Wires -> Wires -> Type
data Netlist a b where
Netlist :: (KnownWires a, KnownWires b) => Bundle a -> Bundle b -> IM.IntMap Driver -> Netlist a b
-- | Build a netlist, retaining precisely the components that can influence an output, including
-- through a register on a later cycle.
netlist :: Circuit a b -> Netlist a b
netlist (Circuit f) = Netlist ins outs (IM.restrictKeys ds (reachable IS.empty (ports outs)))
where
((ins, outs), BuildState _ ds) = runBuild build (BuildState 0 IM.empty)
build = do
ins' <- freshBundle Input shape
outs' <- f ins'
pure (ins', outs')
reachable seen [] = seen
reachable seen (n : ns)
| IS.member n seen = reachable seen ns
| otherwise = reachable (IS.insert n seen) (inputs (ds IM.! n) ++ ns)
-- | A text netlist. Bit numbers are local to this netlist; bundle ports are ordered left to right.
renderNetlist :: Netlist a b -> String
renderNetlist (Netlist ins outs ds) =
unlines $
["inputs " ++ show (ports ins), "outputs " ++ show (ports outs)]
++ [show n ++ " = " ++ show d | (n, d) <- IM.toAscList ds]
-- | A Graphviz graph of the same netlist used by the simulator. Register nodes show their
-- initial value. Copying is fan-out of an edge, and feedback aliases are small connection points.
toDot :: Netlist a b -> String
toDot (Netlist ins outs ds) =
unlines $
[ "digraph circuit {"
, "rankdir=LR; bgcolor=transparent;"
, "node [shape=box, style=rounded, fontname=Helvetica];"
, "edge [fontname=Helvetica, fontsize=10];"
]
++ [wire n ++ " [shape=circle, label=\"in " ++ show i ++ "\"];" | (i, n) <- zip [0 :: Int ..] (ports ins), IM.member n ds]
++ ["{rank=same; " ++ unwords [wire n ++ ";" | (n, Register{}) <- IM.toAscList ds] ++ "}"]
++ ["{rank=sink; " ++ unwords ["o" ++ show i ++ ";" | i <- [0 .. length (ports outs) - 1]] ++ "}"]
++ concatMap draw (IM.toAscList ds)
++ concat
[ ["o" ++ show i ++ " [shape=circle, label=\"out " ++ show i ++ "\"];", wire n ++ " -> o" ++ show i ++ ";"]
| (i, n) <- zip [0 :: Int ..] (ports outs)
]
++ ["}"]
where
wire n = "w" ++ show n
draw (_, Input) = []
draw (n, d) =
(wire n ++ " [" ++ attrs d ++ "];")
: [ wire i ++ " -> " ++ wire n ++ " [headlabel=\"" ++ label ++ "\"" ++ layout d ++ "];"
| (i, label) <- zip (inputs d) (portNames d)
]
layout Register{} = ", constraint=false"
layout _ = ""
attrs (Alias _) = "shape=point, label=\"\""
attrs (Register b _) = "shape=box, style=\"rounded,filled\", fillcolor=\"#dbeafe\", label=\"D " ++ bit b ++ "\""
attrs d = "label=\"" ++ name d ++ "\""
name (Constant b) = bit b
name Not{} = "NOT"
name And{} = "AND"
name Or{} = "OR"
name Xor{} = "XOR"
name Mux{} = "MUX"
name _ = ""
bit False = "0"
bit True = "1"
portNames Mux{} = ["sel", "1", "0"]
portNames Register{} = ["D"]
portNames _ = repeat ""
-- | Verilog with scalar ports @in0@, @in1@, ... and @out0@, @out1@, ... in bundle order,
-- and a shared rising-edge clock @clk@. The caller supplies a valid Verilog module name.
-- Combinational cycles are rejected, and disconnected components are omitted.
--
-- Registers use @initial@ assignments to preserve their starting values in simulation;
-- synthesis support for initialization depends on the hardware target.
toVerilog :: String -> Netlist a b -> Either CircuitError String
toVerilog name graph@(Netlist ins outs ds) = do
_ <- prepare graph
pure $
unlines $
["module " ++ name ++ "(" ++ List.intercalate ", " interface ++ ");"]
++ [" " ++ declaration n d | (n, d) <- drivers]
++ [" assign " ++ wire n ++ " = in" ++ show i ++ ";" | (i, n) <- inputPorts, IM.member n ds]
++ concatMap emit drivers
++ [" assign out" ++ show i ++ " = " ++ wire n ++ ";" | (i, n) <- outputPorts]
++ ["endmodule"]
where
inputPorts = zip [0 :: Int ..] (ports ins)
outputPorts = zip [0 :: Int ..] (ports outs)
interface =
["input wire clk"]
++ ["input wire in" ++ show i | (i, _) <- inputPorts]
++ ["output wire out" ++ show i | (i, _) <- outputPorts]
drivers = IM.toAscList ds
wire n = "w" ++ show n
declaration n Register{} = "reg " ++ wire n ++ ";"
declaration n _ = "wire " ++ wire n ++ ";"
emit (n, d) = case d of
Input -> []
Constant b -> assign (bit b)
Not a -> assign ("~" ++ wire a)
And a b -> assign (binary " & " a b)
Or a b -> assign (binary " | " a b)
Xor a b -> assign (binary " ^ " a b)
Mux s a b -> assign (wire s ++ " ? " ++ wire a ++ " : " ++ wire b)
Alias a -> assign (wire a)
Register initial a ->
[ " initial " ++ wire n ++ " = " ++ bit initial ++ ";"
, " always @(posedge clk) " ++ wire n ++ " <= " ++ wire a ++ ";"
]
where
assign expression = [" assign " ++ wire n ++ " = " ++ expression ++ ";"]
binary op a b = wire a ++ op ++ wire b
bit False = "1'b0"
bit True = "1'b1"
-- | Bits participating in a combinational cycle. Every observable cycle must cross a register.
newtype CircuitError = CombinationalCycle [Int]
deriving (Eq, Show)
-- | A circuit that passed combinational-cycle validation, carrying its current simulation state.
-- Composition and tensor preserve validity; unrestricted trace is available only before validation.
type Prepared :: CAT PREPARED
data Prepared a b where
Prepared
:: (KnownWires a, KnownWires b)
=> Simulator (Value @ Ready a) (Value @ Ready b) -> Prepared (Ready a) (Ready b)
instance CategoryOf PREPARED where
type (~>) = Prepared
type Ob a = (Is Ready a, KnownWires (UN Ready a))
instance Profunctor Prepared where
dimap = dimapDefault
r \\ Prepared{} = r
instance Promonad Prepared where
id = Prepared id
Prepared f . Prepared g = Prepared (f . g)
instance MonoidalProfunctor Prepared where
one = Prepared one
Prepared f ** Prepared g = Prepared (f ** g)
instance Monoidal PREPARED where
type Unit = Ready Empty
type a ** b = Ready (Pair (UN Ready a) (UN Ready b))
withOb2 r = r
leftUnitor = Prepared leftUnitor
leftUnitorInv = Prepared leftUnitorInv
rightUnitor = Prepared rightUnitor
rightUnitorInv = Prepared rightUnitorInv
associator = Prepared associator
associatorInv = Prepared associatorInv
instance SymMonoidal PREPARED where
swap = Prepared swap
instance (Ob a) => Comonoid (a :: PREPARED) where
counit = Prepared counit
comult = Prepared comult
instance (Ob a) => CocommutativeComonoid (a :: PREPARED)
instance CopyDiscard PREPARED
instance FunctorForRep Value where
type Value @ Ready Bit = Sim Bool
type Value @ Ready Empty = Sim ()
type Value @ Ready (Pair a b) = Sim (Sample a, Sample b)
fmap (Prepared machine) = machine
instance MonoidalProfunctor (Rep Value) where
one = Rep one
Rep f ** Rep g = Rep (f ** g)
-- | Cut the dependencies through registers and topologically order the combinational nodes.
-- Multiplexers depend on both data inputs, even when their selector is constant.
prepare :: Netlist a b -> Either CircuitError (Prepared (Ready a) (Ready b))
prepare (Netlist ins outs ds) = do
order <- concat <$> traverse acyclic (stronglyConnComp [((n, d), n, deps d) | (n, d) <- IM.toAscList ds])
let run state = Simulator \input ->
let initial = IM.union (IM.fromList (inputValues ins input)) state
evaluated = List.foldl' evaluate initial order
in (output outs evaluated, run (IM.map (evaluated IM.!) registerInputs))
pure (Prepared (run initialRegisters))
where
registers = IM.mapMaybe (\case Register b i -> Just (b, i); _ -> Nothing) ds
initialRegisters = IM.map fst registers
registerInputs = IM.map snd registers
deps Register{} = []
deps d = inputs d
acyclic (AcyclicSCC (_, Input)) = Right []
acyclic (AcyclicSCC (_, Register{})) = Right []
acyclic (AcyclicSCC node) = Right [node]
acyclic (CyclicSCC ns) = Left (CombinationalCycle (map fst ns))
evaluate bs (n, d) = IM.insert n (value d) bs
where
get a = bs IM.! a
value (Constant b) = b
value (Not a) = not (get a)
value (And a b) = get a && get b
value (Or a b) = get a || get b
value (Xor a b) = get a /= get b
value (Mux s a b) = if get s then get a else get b
value (Alias a) = get a
value _ = get n
-- | Prepare a circuit once and simulate a sequence of clock cycles. Each output is observed
-- before the clock edge for the corresponding input.
simulate :: Circuit a b -> [Sample a] -> Either CircuitError [Sample b]
simulate circuit xs = do
sim <- prepare (netlist circuit)
pure (runSimulator (fmap @Value sim) xs) \\ sim