packages feed

proarrow-0.4.0.0: test/Props/Circuit.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# OPTIONS_GHC -Wno-orphans #-}

module Props.Circuit (test) where

import Data.Kind (Constraint)
import Test.Tasty (TestTree, testGroup)
import Prelude hiding (fmap, id, (**), (.))

import Proarrow.Category.Instance.Circuit
import Proarrow.Category.Monoidal (Monoidal (..), MonoidalProfunctor (..), Tensor)
import Proarrow.Category.Monoidal.Strength (Costrong (..))
import Proarrow.Core (Is, Promonad (..), UN, obj)
import Proarrow.Functor (FunctorForRep (..))
import Proarrow.Monoid (Comonoid (..))
import Proarrow.Profunctor.Representable (Rep)
import Proarrow.Testing
  ( GenTotal
  , Testable (..)
  , TestableProfunctor
  , TestableType (..)
  , TestingEqShow (..)
  , genNamed
  , genSomeDef
  , oneOfTotal
  , optGen
  )
import Proarrow.Testing.Laws
  ( testCategory
  , testCopyDiscard
  , testMonoidal
  , testMonoidalProfunctor
  , testProfunctor
  , testRepresentable
  , testSymMonoidal
  , testTraced
  )
import Props.Hask ()
import Props.Simulator ()

test :: TestTree
test =
  testGroup
    "Circuit"
    [ testCategory @Wires
    , testMonoidal @Wires (\r -> r)
    , testSymMonoidal @Wires (\r -> r)
    , testCopyDiscard @Wires (\r -> r)
    , testTraced @Wires (\r -> r)
    , testGroup
        "Prepared"
        [ testCategory @PREPARED
        , testMonoidal @PREPARED (\r -> r)
        , testSymMonoidal @PREPARED (\r -> r)
        , testCopyDiscard @PREPARED (\r -> r)
        ]
    , testGroup
        "Value"
        [ testProfunctor @(Rep Value)
        , testRepresentable @(Rep Value) (\r -> r)
        , testMonoidalProfunctor @(Rep Value) (\r -> r) (\r -> r)
        ]
    ]

-- Generate circuits through the public API, preserving the grouping of their boundaries.
type TestWires :: Wires -> Constraint
class (KnownWires a, TestOb (Sample a)) => TestWires a where
  wireName :: String
  projections :: [Circuit a Bit]
  assemble :: (KnownWires b) => GenTotal (Circuit b Bit) -> GenTotal (Circuit b a)
instance TestWires Bit where
  wireName = "Bit"
  projections = [id]
  assemble = id
instance TestWires Empty where
  wireName = "Empty"
  projections = []
  assemble _ = pure counit
instance (TestWires a, TestWires b) => TestWires (Pair a b) where
  wireName = "(" ++ wireName @a ++ " ** " ++ wireName @b ++ ")"
  projections =
    map (. (rightUnitor . (obj @a ** counit @b))) (projections @a)
      ++ map (. (leftUnitor . (counit @a ** obj @b))) (projections @b)
  assemble bit = (\f g -> (f ** g) . comult) <$> assemble @a bit <*> assemble @b bit

instance Testable Wires where
  type TestOb a = TestWires a
  obFromTestOb r = r
  showOb @a = wireName @a
  genSome = genSomeDef @'[Empty, Bit, Pair Bit Bit]

genBit :: forall a. (TestWires a) => Int -> GenTotal (Circuit a Bit)
genBit depth = oneOfTotal (leaves ++ gates)
  where
    leaves = ((\b -> constant b . counit) <$> gen @Bool) : map pure (projections @a)
    child = genBit @a (depth - 1)
    gates
      | depth <= 0 = []
      | otherwise =
          [ (notGate .) <$> child
          , (\gate f g -> gate . (f ** g) . comult) <$> optGen [andGate, orGate, xorGate] <*> child <*> child
          , (\s f g -> mux . ((s ** f) ** g) . (comult ** id) . comult) <$> child <*> child <*> child
          , (\initial f -> register initial . f) <$> gen @Bool <*> child
          , (\initial f -> coact @Tensor @Circuit @Bit (comult . register initial . xorGate . (obj @Bit ** f)))
              <$> gen @Bool
              <*> child
          ]

instance (TestWires a, TestWires b) => TestableType (Circuit a b) where
  gen = assemble @b (genBit @a 2)

instance (TestWires a, TestWires b) => TestingEqShow (Circuit a b) where
  showP = renderNetlist . netlist
  eqP f g = do
    xs <- genNamed @[Sample a] "clock inputs"
    -- Trace laws can create unguarded cycles. Compare their rejection status rather than local
    -- wire numbers; executable circuits are compared by their outputs over the sampled clocks.
    case (simulate f xs, simulate g xs) of
      (Right ys, Right zs) -> eqP ys zs
      (Left _, Left _) -> pure True
      _ -> pure False

instance TestableProfunctor Circuit

instance Testable PREPARED where
  type TestOb a = (Is Ready a, TestWires (UN Ready a))
  obFromTestOb r = r
  showOb @(Ready a) = "Ready " ++ wireName @a
  genSome = genSomeDef @'[Ready Empty, Ready Bit, Ready (Pair Bit Bit)]

instance (TestWires a, TestWires b) => TestableType (Prepared (Ready a) (Ready b)) where
  -- Generated circuits contain only guarded feedback; rejection is a generator bug.
  gen = either (error . show) id . prepare . netlist <$> gen @(Circuit a b)

instance (TestWires a, TestWires b) => TestingEqShow (Prepared (Ready a) (Ready b)) where
  showP = showP . fmap @Value
  eqP f g = eqP (fmap @Value f) (fmap @Value g)

instance TestableProfunctor Prepared
instance TestableProfunctor (Rep Value)