packages feed

cleveland-0.2.0: src/Lorentz/Test/DupableScan.hs

-- SPDX-FileCopyrightText: 2022 Oxhead Alpha
-- SPDX-License-Identifier: LicenseRef-MIT-OA

-- | Test utilities to ensure the proper layout of non-dupable elements
-- within datatypes.
module Lorentz.Test.DupableScan
  ( CheckDupablesDistribution
  , VerifyDupablesDistribution
  , checkDupablesDistribution
  , testDupablesDistribution
  , BadElement (..)
  ) where

import Data.Typeable (TypeRep, typeRep)
import Fmt (Buildable(..), pretty, (+|), (|+))
import GHC.Generics qualified as G
import Prelude.Singletons (demote)
import Test.HUnit (assertFailure)
import Test.Tasty (TestTree)
import Test.Tasty.HUnit (testCase)

import Lorentz
import Morley.Michelson.Typed.Scope
import Morley.Util.Type
import Morley.Util.TypeLits

-- | Bind on 'Either's that return nothing.
type SumEithers :: Either l () -> Either l () -> Either l r
type family SumEithers e1 e2 where
  SumEithers ('Right '()) e = e
  SumEithers ('Left err) _ = 'Left err

-- | Check that the given generic subtree /corresponding to a product part/
-- contains only elements of given dupability (all dupable or all non-dupable).
type family GCheckDupability (expectedDupability :: Bool) (x :: Type -> Type)
    :: Either (Bool, Maybe Symbol, Type) () where
  GCheckDupability dup (x G.:*: y) =
    GCheckDupability dup x `SumEithers` GCheckDupability dup y
  GCheckDupability dup (G.S1 ('G.MetaSel mname _ _ _) (G.Rec0 a)) =
    If (IsDupableScope (ToT a) == dup)
      ('Right '())
      ('Left '(dup, mname, a))

-- | A "generics" helper for 'CheckDupablesDistribution'.
type family GCheckDupablesDistribution (x :: Type -> Type) where
  GCheckDupablesDistribution (G.D1 _ (G.C1 _ (l G.:*: r))) =
    GCheckDupability 'True l `SumEithers` GCheckDupability 'False r
  GCheckDupablesDistribution _ =
    TypeError ('Text "This works only on product types")

-- | For the given type, check that at the left subtree of pairs tree it
-- contains only dupable elements, and at the right - only non-dupable ones.
type family CheckDupablesDistribution (a :: Type) :: Either (Bool, Maybe Symbol, Type) () where
  CheckDupablesDistribution a = GCheckDupablesDistribution (G.Rep a)

-- | Error for 'checkDupablesDistribution'.
data BadElement = BadElement
  { beExpectedDupability :: Bool
  , beRecName :: Maybe Text
  , beRecType :: TypeRep
  } deriving stock (Show, Eq)

instance Buildable BadElement where
  build (BadElement expectedDupable recName recType) =
    "Expected " <> if expectedDupable then "dupable" else "non-dupable" <> "element \
    \in record field " +| (recName ?: "<no info>") |+ " of type " +| show @Text recType |+ ""

class DemoteRes (res :: Either (Bool, Maybe Symbol, Type) ()) where
  demoteRes :: Either BadElement ()
instance DemoteRes ('Right '()) where
  demoteRes = Right ()
instance (SingI dup, SingI mname, Typeable ty) => DemoteRes ('Left '(dup, mname, ty)) where
  demoteRes = Left BadElement
    { beExpectedDupability = demote @dup
    , beRecName = demote @mname
    , beRecType = typeRep (Proxy @ty)
    }

-- | 'CheckDupablesDistribution' for term-level.
checkDupablesDistribution
  :: forall a. (DemoteRes (CheckDupablesDistribution a))
  => Either BadElement ()
checkDupablesDistribution = demoteRes @(CheckDupablesDistribution a)

-- | A term-level version of 'VerifyDupablesDistribution' for tests.
testDupablesDistribution
  :: forall a. (Typeable a, DemoteRes (CheckDupablesDistribution a))
  => TestTree
testDupablesDistribution =
  testCase ("Dupables distribution for " <> show (typeRep $ Proxy @a)) $
    case checkDupablesDistribution @a of
      Right () -> pass
      Left err -> assertFailure $ "Bad dupables distribution: " <> pretty err

type family ResToConstraint (res :: Either (Bool, Maybe Symbol, Type) ()) :: Constraint where
  ResToConstraint ('Right '()) = ()
  ResToConstraint ('Left '(expectedDup, recName, recType)) =
    TypeError
      ( 'Text "Expected " ':<>: 'Text (DupableDesc expectedDup) ':<>: 'Text " element" ':$$:
        'Text "in record field " ':<>: 'ShowType (GetMaybeName recName)
          ':<>: 'Text " of type " ':<>: 'ShowType recType
      )

type family DupableDesc (dup :: Bool) :: Symbol where
  DupableDesc 'True = "dupable"
  DupableDesc 'False = "non-dupable"

type family GetMaybeName (mname :: Maybe Symbol) :: Symbol where
  GetMaybeName ('Just name) = name
  GetMaybeName 'Nothing = "<no info>"

-- | A constraint that, for the given type, checks that at the left subtree
-- of pairs tree it contains only dupable elements, and at the right - only
-- non-dupable ones.
type VerifyDupablesDistribution a =
  ResToConstraint (CheckDupablesDistribution a)