packages feed

moonlight-category-0.1.0.0: test/abstract/AdhesiveSpec.hs

{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}

module AdhesiveSpec
  ( tests,
  )
where

import Data.List (isInfixOf)
import Data.Foldable (traverse_)
import Moonlight.Category
  ( AdhesiveCategory (..),
    Category (..),
    HasPullbacks (..),
    HasPushouts (..),
    MonicMatchComponents (..),
    PBPOAdhesiveCategory (..),
    PBPOComplementComponents (..),
    PushoutComplementComponents (..),
    monicMatchArrow,
    pbpoComplement,
    pbpoComplementBorrowedLeg,
    pbpoComplementMonicMatch,
    pbpoComplementPullbackObject,
    pbpoComplementPullbackToBorrowed,
    pbpoComplementPullbackToMatch,
    pbpoComplementPushoutFromComplement,
    pbpoComplementPushoutFromMatch,
    pbpoComplementPushoutObject,
    pbpoComplementResidualLeg,
    pbpoComplementRuleLeg,
    pbpoPullbackSquareCommutes,
    pbpoPushoutSquareCommutes,
    pushoutComplement,
    pushoutComplementSquareCommutes,
    composeMor,
    witnessMonic,
  )
import Moonlight.Category.Effect.Harness.Adhesive qualified as AdhesiveHarness
import Moonlight.Category.Effect.Harness.Category qualified as CategoryHarness
import Moonlight.Category.Effect.Harness.Core (CategoryLaws (categoryAssociativity))
import Moonlight.Category.Effect.Harness.Limits qualified as LimitsHarness
import Moonlight.Category.Pure.Adhesive
  ( denseIntSetInterval,
    denseIntSetMember,
  )
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit ((@?=), assertBool, assertFailure, testCase)

data TestCategory = TestCategory

data NativePBPOCategory = NativePBPOCategory

data MissingMediatorCategory = MissingMediatorCategory

data BrokenCompositionCategory = BrokenCompositionCategory

data TestObject
  = ObjectK
  | ObjectL
  | ObjectD
  | ObjectG
  | ObjectP
  | ObjectQ
  | ObjectNativePullback
  | ObjectNativePushout
  deriving stock (Eq, Show)

data TestMorphism = TestMorphism
  { testMorphismSource :: !TestObject,
    testMorphismTarget :: !TestObject
  }
  deriving stock (Eq, Show)

data TestTwoMor

data TestCompositor = TestCompositor

newtype BrokenObject = BrokenObject TestObject
  deriving stock (Eq, Show)

data BrokenMorphism = BrokenMorphism
  { brokenMorphismSource :: !BrokenObject,
    brokenMorphismTarget :: !BrokenObject
  }
  deriving stock (Eq, Show)

data BrokenTwoMor

data BrokenCompositor

newtype NativeObject = NativeObject TestObject
  deriving stock (Eq, Show)

data NativeMorphism = NativeMorphism
  { nativeMorphismSource :: !NativeObject,
    nativeMorphismTarget :: !NativeObject
  }
  deriving stock (Eq, Show)

data NativeTwoMor

data NativeCompositor = NativeCompositor

newtype MissingMediatorObject = MissingMediatorObject TestObject
  deriving stock (Eq, Show)

data MissingMediatorMorphism = MissingMediatorMorphism
  { missingMediatorMorphismSource :: !MissingMediatorObject,
    missingMediatorMorphismTarget :: !MissingMediatorObject
  }
  deriving stock (Eq, Show)

data MissingMediatorTwoMor

data MissingMediatorCompositor = MissingMediatorCompositor

instance Category TestCategory where
  type Ob TestCategory = TestObject
  type Mor TestCategory = TestMorphism
  type TwoMor TestCategory = TestTwoMor
  type Compositor TestCategory = TestCompositor

  identity _ objectValue =
    Right (TestMorphism objectValue objectValue)

  compose _ leftMorphism rightMorphism
    | testMorphismTarget rightMorphism == testMorphismSource leftMorphism =
        Right (TestMorphism (testMorphismSource rightMorphism) (testMorphismTarget leftMorphism), TestCompositor)
    | otherwise =
        Left ()

  source _ =
    Right . testMorphismSource

  target _ =
    Right . testMorphismTarget

instance Category BrokenCompositionCategory where
  type Ob BrokenCompositionCategory = BrokenObject
  type Mor BrokenCompositionCategory = BrokenMorphism
  type TwoMor BrokenCompositionCategory = BrokenTwoMor
  type Compositor BrokenCompositionCategory = BrokenCompositor

  identity _ objectValue = Right (BrokenMorphism objectValue objectValue)
  compose _ _ _ = Left ()
  source _ = Right . brokenMorphismSource
  target _ = Right . brokenMorphismTarget

instance HasPullbacks TestCategory where
  pullback _ leftMorphism rightMorphism
    | testMorphismTarget leftMorphism == testMorphismTarget rightMorphism =
        Just
          ( ObjectP,
            TestMorphism ObjectP (testMorphismSource leftMorphism),
            TestMorphism ObjectP (testMorphismSource rightMorphism)
          )
    | otherwise =
        Nothing

  pullbackMediator _ leftMorphism rightMorphism coneLeft coneRight
    | testMorphismTarget leftMorphism == testMorphismTarget rightMorphism
        && testMorphismTarget coneLeft == testMorphismSource leftMorphism
        && testMorphismTarget coneRight == testMorphismSource rightMorphism
        && testMorphismSource coneLeft == testMorphismSource coneRight
        && composeMor @TestCategory TestCategory leftMorphism coneLeft == composeMor @TestCategory TestCategory rightMorphism coneRight =
        Just (TestMorphism (testMorphismSource coneLeft) ObjectP)
    | otherwise =
        Nothing

instance HasPushouts TestCategory where
  pushout _ leftMorphism rightMorphism
    | testMorphismSource leftMorphism == testMorphismSource rightMorphism =
        Just
          ( ObjectQ,
            TestMorphism (testMorphismTarget leftMorphism) ObjectQ,
            TestMorphism (testMorphismTarget rightMorphism) ObjectQ
          )
    | otherwise =
        Nothing

instance AdhesiveCategory TestCategory where
  monicMatchComponents _ morphism =
    Just (MonicMatchComponents morphism)

  pushoutComplementComponents _ _ _ =
    Just
      PushoutComplementComponents
        { pushoutComplementComponentObject = ObjectD,
          pushoutComplementComponentBorrowedLeg = TestMorphism ObjectD ObjectG,
          pushoutComplementComponentResidualLeg = TestMorphism ObjectK ObjectD
        }

instance PBPOAdhesiveCategory TestCategory

instance Category NativePBPOCategory where
  type Ob NativePBPOCategory = NativeObject
  type Mor NativePBPOCategory = NativeMorphism
  type TwoMor NativePBPOCategory = NativeTwoMor
  type Compositor NativePBPOCategory = NativeCompositor

  identity _ objectValue =
    Right (NativeMorphism objectValue objectValue)

  compose _ leftMorphism rightMorphism
    | nativeMorphismTarget rightMorphism == nativeMorphismSource leftMorphism =
        Right (NativeMorphism (nativeMorphismSource rightMorphism) (nativeMorphismTarget leftMorphism), NativeCompositor)
    | otherwise =
        Left ()

  source _ =
    Right . nativeMorphismSource

  target _ =
    Right . nativeMorphismTarget

instance HasPullbacks NativePBPOCategory where
  pullback _ _ _ =
    Nothing

  pullbackMediator _ _ _ _ _ =
    Nothing

instance HasPushouts NativePBPOCategory where
  pushout _ _ _ =
    Nothing

instance AdhesiveCategory NativePBPOCategory where
  monicMatchComponents _ morphism =
    Just (MonicMatchComponents morphism)

  pushoutComplementComponents _ _ _ =
    Nothing

instance PBPOAdhesiveCategory NativePBPOCategory where
  pbpoComplementComponents _ _ _ =
    Just
      PBPOComplementComponents
        { pbpoComplementComponentPullbackObject = NativeObject ObjectNativePullback,
          pbpoComplementComponentPullbackToBorrowed = NativeMorphism (NativeObject ObjectNativePullback) (NativeObject ObjectD),
          pbpoComplementComponentPullbackToMatch = NativeMorphism (NativeObject ObjectNativePullback) (NativeObject ObjectL),
          pbpoComplementComponentPushoutObject = NativeObject ObjectNativePushout,
          pbpoComplementComponentPushoutFromComplement = NativeMorphism (NativeObject ObjectD) (NativeObject ObjectNativePushout),
          pbpoComplementComponentPushoutFromMatch = NativeMorphism (NativeObject ObjectL) (NativeObject ObjectNativePushout),
          pbpoComplementComponentBorrowedLeg = NativeMorphism (NativeObject ObjectD) (NativeObject ObjectG),
          pbpoComplementComponentResidualLeg = NativeMorphism (NativeObject ObjectK) (NativeObject ObjectD)
        }

instance Category MissingMediatorCategory where
  type Ob MissingMediatorCategory = MissingMediatorObject
  type Mor MissingMediatorCategory = MissingMediatorMorphism
  type TwoMor MissingMediatorCategory = MissingMediatorTwoMor
  type Compositor MissingMediatorCategory = MissingMediatorCompositor

  identity _ objectValue =
    Right (MissingMediatorMorphism objectValue objectValue)

  compose _ leftMorphism rightMorphism
    | missingMediatorMorphismTarget rightMorphism == missingMediatorMorphismSource leftMorphism =
        Right
          ( MissingMediatorMorphism
              (missingMediatorMorphismSource rightMorphism)
              (missingMediatorMorphismTarget leftMorphism),
            MissingMediatorCompositor
          )
    | otherwise =
        Left ()

  source _ =
    Right . missingMediatorMorphismSource

  target _ =
    Right . missingMediatorMorphismTarget

instance HasPullbacks MissingMediatorCategory where
  pullback _ leftMorphism rightMorphism
    | missingMediatorMorphismTarget leftMorphism == missingMediatorMorphismTarget rightMorphism =
        Just
          ( MissingMediatorObject ObjectP,
            MissingMediatorMorphism (MissingMediatorObject ObjectP) (missingMediatorMorphismSource leftMorphism),
            MissingMediatorMorphism (MissingMediatorObject ObjectP) (missingMediatorMorphismSource rightMorphism)
          )
    | otherwise =
        Nothing

  pullbackMediator _ _ _ _ _ =
    Nothing

tests :: TestTree
tests =
  testGroup
    "Adhesive"
    [ testCase "PBPO complement carries pullback and pushout squares" testPBPOComplement,
      testCase "PBPO complement can be native rather than DPO-derived" testNativePBPOComplement,
      testCase "pushout complement witness square commutes" testPushoutComplementSquare,
      testCase "pullback mediator law rejects missing mediators for valid cones" testPullbackMediatorLawRejectsMissingMediator,
      testCase "pullback law rejects a missing construction on a valid cospan" testPullbackLawRejectsMissingConstruction,
      testCase "associativity law rejects failed composition on valid boundaries" testAssociativityLawRejectsFailedComposition,
      testCase "dense interval rejects overflowing bounds" testDenseIntervalRejectsOverflow,
      testCase "public adhesive surface keeps witness constructors opaque" testWitnessSurfaceOpaque
    ]

testDenseIntervalRejectsOverflow :: IO ()
testDenseIntervalRejectsOverflow = do
  denseIntSetInterval 512 maxBound 2 @?= Nothing
  denseIntSetInterval 512 511 2 @?= Nothing
  case denseIntSetInterval 512 510 2 of
    Nothing -> assertFailure "expected the final two in-range elements"
    Just interval -> do
      denseIntSetMember 510 interval @?= True
      denseIntSetMember 511 interval @?= True

testPBPOComplement :: IO ()
testPBPOComplement =
  let ruleLeg = TestMorphism ObjectK ObjectL
      matchArrow = TestMorphism ObjectL ObjectG
   in case witnessMonic @TestCategory TestCategory matchArrow >>= pbpoComplement @TestCategory TestCategory ruleLeg of
        Nothing ->
          assertFailure "expected PBPO complement witness"
        Just witness -> do
          pbpoComplementRuleLeg witness @?= ruleLeg
          monicMatchArrow (pbpoComplementMonicMatch witness) @?= matchArrow
          pbpoComplementPullbackObject witness @?= ObjectP
          pbpoComplementPullbackToBorrowed witness @?= TestMorphism ObjectP ObjectD
          pbpoComplementPullbackToMatch witness @?= TestMorphism ObjectP ObjectL
          pbpoComplementPushoutObject witness @?= ObjectQ
          pbpoComplementPushoutFromComplement witness @?= TestMorphism ObjectD ObjectQ
          pbpoComplementPushoutFromMatch witness @?= TestMorphism ObjectL ObjectQ
          pbpoComplementBorrowedLeg witness @?= TestMorphism ObjectD ObjectG
          pbpoComplementResidualLeg witness @?= TestMorphism ObjectK ObjectD
          pbpoPullbackSquareCommutes TestCategory witness @?= True
          pbpoPushoutSquareCommutes TestCategory witness @?= True

testNativePBPOComplement :: IO ()
testNativePBPOComplement =
  let ruleLeg = NativeMorphism (NativeObject ObjectK) (NativeObject ObjectL)
      matchArrow = NativeMorphism (NativeObject ObjectL) (NativeObject ObjectG)
   in case witnessMonic @NativePBPOCategory NativePBPOCategory matchArrow >>= pbpoComplement @NativePBPOCategory NativePBPOCategory ruleLeg of
        Nothing ->
          assertFailure "expected native PBPO complement witness"
        Just witness -> do
          pbpoComplementPullbackObject witness @?= NativeObject ObjectNativePullback
          pbpoComplementPushoutObject witness @?= NativeObject ObjectNativePushout

testPushoutComplementSquare :: IO ()
testPushoutComplementSquare =
  let ruleLeg = TestMorphism ObjectK ObjectL
      matchArrow = TestMorphism ObjectL ObjectG
   in case witnessMonic @TestCategory TestCategory matchArrow >>= pushoutComplement @TestCategory TestCategory ruleLeg of
        Nothing ->
          assertFailure "expected pushout complement witness"
        Just witness ->
          pushoutComplementSquareCommutes TestCategory witness @?= True

testPullbackMediatorLawRejectsMissingMediator :: IO ()
testPullbackMediatorLawRejectsMissingMediator =
  let objectValue =
        MissingMediatorObject
      morphismValue sourceValue targetValue =
        MissingMediatorMorphism (objectValue sourceValue) (objectValue targetValue)
      leftBase = morphismValue ObjectL ObjectG
      rightBase = morphismValue ObjectD ObjectG
      coneLeft = morphismValue ObjectK ObjectL
      coneRight = morphismValue ObjectK ObjectD
   in AdhesiveHarness.pullbackMediatorCommutes @MissingMediatorCategory MissingMediatorCategory leftBase rightBase coneLeft coneRight
        @?= False

testPullbackLawRejectsMissingConstruction :: IO ()
testPullbackLawRejectsMissingConstruction =
  let targetObject = NativeObject ObjectG
      leftMorphism = NativeMorphism (NativeObject ObjectL) targetObject
      rightMorphism = NativeMorphism (NativeObject ObjectD) targetObject
   in LimitsHarness.pullbackCommutative @NativePBPOCategory NativePBPOCategory leftMorphism rightMorphism
        @?= False

testAssociativityLawRejectsFailedComposition :: IO ()
testAssociativityLawRejectsFailedComposition =
  let laws = CategoryHarness.mkCategoryLaws @BrokenCompositionCategory BrokenCompositionCategory
      objectValue = BrokenObject
      morphismValue sourceValue targetValue =
        BrokenMorphism (objectValue sourceValue) (objectValue targetValue)
      firstMorphism = morphismValue ObjectK ObjectL
      secondMorphism = morphismValue ObjectL ObjectD
      thirdMorphism = morphismValue ObjectD ObjectG
   in categoryAssociativity laws firstMorphism secondMorphism thirdMorphism
        @?= False

testWitnessSurfaceOpaque :: IO ()
testWitnessSurfaceOpaque = do
  sourceText <- readFile "src-abstract/Moonlight/Category/Pure/Adhesive.hs"
  traverse_
    (\forbidden -> assertBool ("public surface contains " <> forbidden) (not (forbidden `isInfixOf` sourceText)))
    [ "MonicMatchWitness (..)",
      "PushoutComplementWitness (..)",
      "PBPOComplementWitness (..)",
      "data MonicMatchWitness c = MonicMatchWitness\n  {",
      "data PushoutComplementWitness c = PushoutComplementWitness\n  {",
      "data PBPOComplementWitness c = PBPOComplementWitness\n  {"
    ]