packages feed

moonlight-algebra-0.1.0.0: test/finite-lattice/PresentationSpec.hs

{-# LANGUAGE DerivingStrategies #-}

module PresentationSpec
  ( tests,
  )
where

import Data.Set qualified as Set
import Moonlight.FiniteLattice
  ( ContextLattice,
    ContextLatticeCompileError (..),
    LatticeBuildError (..),
    below,
    belowAll,
    boundedLatticeOf,
    clBottom,
    clTop,
    compileContextLattice,
    contextLatticeElements,
    contextOrderDecl,
    element,
    elements,
    joinContext,
    latticeOf,
    leqContext,
    meetContext,
  )
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit
  ( Assertion,
    assertFailure,
    testCase,
    (@?=),
  )

data DiamondContext
  = DiamondBottom
  | DiamondLeft
  | DiamondRight
  | DiamondTop
  deriving stock (Eq, Ord, Show, Read)

data BowtieContext
  = BowtieBottom
  | BowtieA
  | BowtieB
  | BowtieJoinLeft
  | BowtieJoinRight
  | BowtieTop
  deriving stock (Eq, Ord, Show, Read)

tests :: TestTree
tests =
  testGroup
    "finite lattice presentations"
    [ testCase "the diamond builder reproduces the hand-written declaration" testBuiltDiamondMatchesDeclaration,
      testCase "the bounded batched builder reproduces the hand-written declaration" testBoundedBatchedDiamondMatchesDeclaration,
      testCase "the builder infers top and bottom from the order" testInferredTopAndBottom,
      testCase "below a b means a is at or under b" testEdgeDirection,
      testCase "two maximal elements are an ambiguous top" testAmbiguousTop,
      testCase "a cyclic order has no maximal element" testCyclicOrderHasNoTop,
      testCase "an empty presentation is rejected" testEmptyPresentation,
      testCase "a duplicate element is rejected" testDuplicateElement,
      testCase "a non-lattice poset surfaces the compile obstruction" testNonLatticeSurfacesCompileError
    ]

-- | The centrepiece: a diamond declared by name binding produces the same lattice as
-- the hand-written 'contextOrderDecl', observed through top, bottom, the element list,
-- and the order\/join\/meet of every pair.
builtDiamond :: Either (LatticeBuildError DiamondContext) (ContextLattice DiamondContext)
builtDiamond =
  latticeOf $ do
    [bottom, left, right, top] <-
      elements [DiamondBottom, DiamondLeft, DiamondRight, DiamondTop]
    below bottom left
    below bottom right
    below left top
    below right top

boundedBatchedDiamond :: Either (LatticeBuildError DiamondContext) (ContextLattice DiamondContext)
boundedBatchedDiamond =
  boundedLatticeOf DiamondTop DiamondBottom $ do
    [bottom, left, right, top] <-
      elements [DiamondBottom, DiamondLeft, DiamondRight, DiamondTop]
    belowAll
      [ (bottom, left),
        (bottom, right),
        (left, top),
        (right, top)
      ]

declaredDiamond :: Either (ContextLatticeCompileError DiamondContext) (ContextLattice DiamondContext)
declaredDiamond =
  compileContextLattice
    (Set.fromList [DiamondBottom, DiamondLeft, DiamondRight, DiamondTop])
    ( contextOrderDecl
        DiamondTop
        DiamondBottom
        [ (DiamondBottom, DiamondLeft),
          (DiamondBottom, DiamondRight),
          (DiamondLeft, DiamondTop),
          (DiamondRight, DiamondTop)
        ]
    )

testBuiltDiamondMatchesDeclaration :: Assertion
testBuiltDiamondMatchesDeclaration =
  assertMatchesDeclaredDiamond builtDiamond

testBoundedBatchedDiamondMatchesDeclaration :: Assertion
testBoundedBatchedDiamondMatchesDeclaration =
  assertMatchesDeclaredDiamond boundedBatchedDiamond

assertMatchesDeclaredDiamond :: Either (LatticeBuildError DiamondContext) (ContextLattice DiamondContext) -> Assertion
assertMatchesDeclaredDiamond diamond =
  case (diamond, declaredDiamond) of
    (Right built, Right declared) -> do
      clTop built @?= clTop declared
      clBottom built @?= clBottom declared
      contextLatticeElements built @?= contextLatticeElements declared
      mapM_
        ( \(leftContext, rightContext) -> do
            leqContext built leftContext rightContext
              @?= leqContext declared leftContext rightContext
            joinContext built leftContext rightContext
              @?= joinContext declared leftContext rightContext
            meetContext built leftContext rightContext
              @?= meetContext declared leftContext rightContext
        )
        [ (leftContext, rightContext)
          | leftContext <- contextLatticeElements declared,
            rightContext <- contextLatticeElements declared
        ]
    (Left buildError, _) ->
      assertFailure ("expected the presentation diamond, got " <> show buildError)
    (_, Left compileError) ->
      assertFailure ("expected the declared diamond, got " <> show compileError)

testInferredTopAndBottom :: Assertion
testInferredTopAndBottom =
  case builtDiamond of
    Left buildError ->
      assertFailure ("expected the built diamond, got " <> show buildError)
    Right built -> do
      clTop built @?= DiamondTop
      clBottom built @?= DiamondBottom
      joinContext built DiamondLeft DiamondRight @?= Right DiamondTop
      meetContext built DiamondLeft DiamondRight @?= Right DiamondBottom

testEdgeDirection :: Assertion
testEdgeDirection =
  case builtDiamond of
    Left buildError ->
      assertFailure ("expected the built diamond, got " <> show buildError)
    Right built -> do
      leqContext built DiamondBottom DiamondTop @?= Right True
      leqContext built DiamondTop DiamondBottom @?= Right False

testAmbiguousTop :: Assertion
testAmbiguousTop =
  case latticeOf forkWithoutTop of
    Left (AmbiguousTop candidates) ->
      Set.fromList candidates @?= Set.fromList [DiamondLeft, DiamondRight]
    Left otherError ->
      assertFailure ("expected AmbiguousTop, got " <> show otherError)
    Right _ ->
      assertFailure "expected AmbiguousTop, got a compiled lattice"
  where
    forkWithoutTop = do
      [bottom, left, right] <- elements [DiamondBottom, DiamondLeft, DiamondRight]
      below bottom left
      below bottom right

testCyclicOrderHasNoTop :: Assertion
testCyclicOrderHasNoTop =
  latticeOf cyclicPresentation `shouldFailWith` NoTop
  where
    cyclicPresentation = do
      [a, b] <- elements [DiamondLeft, DiamondRight]
      below a b
      below b a

testEmptyPresentation :: Assertion
testEmptyPresentation =
  ( latticeOf (pure ()) ::
      Either (LatticeBuildError DiamondContext) (ContextLattice DiamondContext)
  )
    `shouldFailWith` EmptyLattice

testDuplicateElement :: Assertion
testDuplicateElement =
  latticeOf duplicatePresentation `shouldFailWith` DuplicateElement DiamondTop
  where
    duplicatePresentation = do
      _ <- element DiamondTop
      _ <- element DiamondTop
      pure ()

-- | A poset with a unique top and bottom whose incomparable pair @A@, @B@ has two
-- minimal upper bounds. Inference succeeds; @compileContextLattice@ rejects it, and the
-- builder forwards that obstruction verbatim through 'InvalidLattice'.
testNonLatticeSurfacesCompileError :: Assertion
testNonLatticeSurfacesCompileError =
  latticeOf bowtiePresentation
    `shouldFailWith` InvalidLattice
      (ContextLatticeJoinDoesNotExist BowtieA BowtieB (Set.fromList [BowtieJoinLeft, BowtieJoinRight]))
  where
    bowtiePresentation = do
      [bottom, a, b, joinLeft, joinRight, top] <-
        elements
          [ BowtieBottom,
            BowtieA,
            BowtieB,
            BowtieJoinLeft,
            BowtieJoinRight,
            BowtieTop
          ]
      below bottom a
      below bottom b
      below a joinLeft
      below b joinLeft
      below a joinRight
      below b joinRight
      below joinLeft top
      below joinRight top

-- | Assert a presentation fails with a given build error, inspecting only the 'Left'
-- so the lattice on the 'Right' (which has no 'Eq') is never compared.
shouldFailWith ::
  (Eq buildError, Show buildError) =>
  Either buildError lattice ->
  buildError ->
  Assertion
shouldFailWith result expected =
  case result of
    Left actual -> actual @?= expected
    Right _ -> assertFailure "expected a lattice presentation failure"