packages feed

moonlight-pale-0.1.0.0: src-test-laws/Moonlight/Pale/Test/Laws/Restriction.hs

{-| Checked finite restriction systems and their functoriality law suites. -}
module Moonlight.Pale.Test.Laws.Restriction
  ( FiniteRestrictionLaw,
    FiniteRestrictionError (..),
    compileFiniteRestrictionLaw,
    finiteRestrictionLaws,
  )
where

import Data.Foldable (traverse_)
import Data.IntSet (IntSet)
import Data.IntSet qualified as IntSet
import Data.Kind (Type)
import Data.List.NonEmpty (NonEmpty)
import Data.List.NonEmpty qualified as NonEmpty
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Vector (Vector)
import Data.Vector qualified as Vector
import Moonlight.Pale.Test.Laws.Suite (LawSuite, hUnitLaw, lawGroup)
import Test.Tasty.HUnit (Assertion, assertEqual, assertFailure)

type FiniteRestrictionLaw :: Type -> Type -> Type
data FiniteRestrictionLaw cell val = FiniteRestrictionLaw
  { finiteRestrictionName :: String,
    finiteRestrictionCells :: !(Vector cell),
    finiteRestrictionUpperSets :: !(Vector IntSet),
    finiteRestrictionSections :: !(Vector [val]),
    finiteRestrictionMap :: cell -> cell -> val -> val
  }

type FiniteRestrictionError :: Type -> Type
data FiniteRestrictionError cell
  = DuplicateRestrictionCell cell !Int !Int
  | SectionCellOutsideUniverse cell
  | RestrictionRelationNotReflexive cell
  | RestrictionRelationNotAntisymmetric cell cell
  | RestrictionRelationNotTransitive cell cell
  deriving stock (Eq, Show)

data RestrictionUniverseCompilation cell = RestrictionUniverseCompilation
  { restrictionUniverseIndex :: !(Map cell Int),
    restrictionUniverseErrorsReversed :: ![FiniteRestrictionError cell]
  }

data RestrictionDenseObstruction
  = RestrictionCellIndexOutOfBounds !Int
  | RestrictionUpperSetIndexOutOfBounds !Int
  deriving stock (Eq, Show)

compileFiniteRestrictionLaw ::
  Ord cell =>
  String ->
  NonEmpty cell ->
  (cell -> cell -> Bool) ->
  [(cell, val)] ->
  (cell -> cell -> val -> val) ->
  Either (NonEmpty (FiniteRestrictionError cell)) (FiniteRestrictionLaw cell val)
compileFiniteRestrictionLaw name cellUniverse leq sections restrict = do
  let cells = Vector.fromList (NonEmpty.toList cellUniverse)
  cellIndex <- compileRestrictionUniverse cells
  let upperSets = compileUpperSets leq cells
      validationErrors =
        validateFinitePoset cells upperSets
          <> unknownSectionErrors cellIndex sections
  case NonEmpty.nonEmpty validationErrors of
    Just errors -> Left errors
    Nothing ->
      Right
        FiniteRestrictionLaw
          { finiteRestrictionName = name,
            finiteRestrictionCells = cells,
            finiteRestrictionUpperSets = upperSets,
            finiteRestrictionSections = compileSectionsByCell cells sections,
            finiteRestrictionMap = restrict
          }

compileRestrictionUniverse ::
  Ord cell =>
  Vector cell ->
  Either (NonEmpty (FiniteRestrictionError cell)) (Map cell Int)
compileRestrictionUniverse cells =
  let compilation =
        Vector.ifoldl'
          insertRestrictionCell
          (RestrictionUniverseCompilation Map.empty [])
          cells
   in case NonEmpty.nonEmpty (reverse (restrictionUniverseErrorsReversed compilation)) of
        Just errors -> Left errors
        Nothing -> Right (restrictionUniverseIndex compilation)

insertRestrictionCell ::
  Ord cell =>
  RestrictionUniverseCompilation cell ->
  Int ->
  cell ->
  RestrictionUniverseCompilation cell
insertRestrictionCell compilation duplicatePosition cell =
  case Map.lookup cell (restrictionUniverseIndex compilation) of
    Just originalPosition ->
      compilation
        { restrictionUniverseErrorsReversed =
            DuplicateRestrictionCell cell originalPosition duplicatePosition
              : restrictionUniverseErrorsReversed compilation
        }
    Nothing ->
      compilation
        { restrictionUniverseIndex =
            Map.insert cell duplicatePosition (restrictionUniverseIndex compilation)
        }

compileUpperSets :: (cell -> cell -> Bool) -> Vector cell -> Vector IntSet
compileUpperSets leq cells =
  Vector.map
    (\sourceCell ->
       Vector.ifoldl'
         (\upperSet targetIndex targetCell ->
            if leq sourceCell targetCell
              then IntSet.insert targetIndex upperSet
              else upperSet
         )
         IntSet.empty
         cells
    )
    cells

validateFinitePoset :: Vector cell -> Vector IntSet -> [FiniteRestrictionError cell]
validateFinitePoset cells upperSets =
  validateReflexivity indexedRows
    <> validateAntisymmetry indexedRows
    <> validateTransitivity indexedRows
  where
    indexedRows = Vector.indexed (Vector.zip cells upperSets)

validateReflexivity ::
  Vector (Int, (cell, IntSet)) ->
  [FiniteRestrictionError cell]
validateReflexivity =
  Vector.foldr
    (\(cellIndex, (cell, upperSet)) errors ->
       if IntSet.member cellIndex upperSet
         then errors
         else RestrictionRelationNotReflexive cell : errors
    )
    []

validateAntisymmetry ::
  Vector (Int, (cell, IntSet)) ->
  [FiniteRestrictionError cell]
validateAntisymmetry indexedRows =
  reverse $
    Vector.foldl'
      (\errors (leftIndex, (leftCell, leftUpperSet)) ->
         Vector.foldl'
           (\nestedErrors (rightIndex, (rightCell, rightUpperSet)) ->
              if
                  leftIndex < rightIndex
                    && IntSet.member rightIndex leftUpperSet
                    && IntSet.member leftIndex rightUpperSet
                then
                  RestrictionRelationNotAntisymmetric leftCell rightCell
                    : nestedErrors
                else nestedErrors
           )
           errors
           indexedRows
      )
      []
      indexedRows

validateTransitivity ::
  Vector (Int, (cell, IntSet)) ->
  [FiniteRestrictionError cell]
validateTransitivity indexedRows =
  reverse $
    Vector.foldl'
      (\errors (_, (sourceCell, sourceUpperSet)) ->
         Vector.foldl'
           (\nestedErrors (middleIndex, (middleCell, middleUpperSet)) ->
              if
                  IntSet.member middleIndex sourceUpperSet
                    && not (middleUpperSet `IntSet.isSubsetOf` sourceUpperSet)
                then
                  RestrictionRelationNotTransitive sourceCell middleCell
                    : nestedErrors
                else nestedErrors
           )
           errors
           indexedRows
      )
      []
      indexedRows

unknownSectionErrors ::
  Ord cell =>
  Map cell Int ->
  [(cell, val)] ->
  [FiniteRestrictionError cell]
unknownSectionErrors cellIndex =
  foldr
    (\(cell, _) errors ->
       if Map.member cell cellIndex
         then errors
         else SectionCellOutsideUniverse cell : errors
    )
    []

compileSectionsByCell ::
  Ord cell =>
  Vector cell ->
  [(cell, val)] ->
  Vector [val]
compileSectionsByCell cells sections =
  let reversedSections =
        foldl'
          (\sectionsByCell (cell, value) ->
             Map.insertWith (<>) cell [value] sectionsByCell
          )
          Map.empty
          sections
   in Vector.map
        (\cell -> reverse (Map.findWithDefault [] cell reversedSections))
        cells

finiteRestrictionLaws ::
  (Show cell, Show val, Eq val) =>
  FiniteRestrictionLaw cell val ->
  [LawSuite]
finiteRestrictionLaws restrictionLaw =
  [ lawGroup
      (finiteRestrictionName restrictionLaw <> " restriction laws")
      [ restrictionIdentity restrictionLaw,
        restrictionComposition restrictionLaw,
        restrictionSourceIdentity restrictionLaw,
        restrictionTargetIdentity restrictionLaw
      ]
  ]

restrictionIdentity ::
  (Show cell, Show val, Eq val) =>
  FiniteRestrictionLaw cell val ->
  LawSuite
restrictionIdentity restrictionLaw =
  hUnitLaw "restriction identity" $
    traverse_
      (\(cell, sections) ->
         traverse_
           (\section ->
              assertEqual
                ("identity at " <> show cell)
                section
                (finiteRestrictionMap restrictionLaw cell cell section)
           )
           sections
      )
      (Vector.zip (finiteRestrictionCells restrictionLaw) (finiteRestrictionSections restrictionLaw))

restrictionComposition ::
  (Show cell, Show val, Eq val) =>
  FiniteRestrictionLaw cell val ->
  LawSuite
restrictionComposition restrictionLaw =
  relatedRestrictionTripleLaw restrictionLaw "restriction composition" $
    \sourceCell middleCell targetCell section ->
      assertEqual
        ("composition along " <> show (sourceCell, middleCell, targetCell))
        (finiteRestrictionMap restrictionLaw sourceCell targetCell section)
        ( finiteRestrictionMap restrictionLaw middleCell targetCell
            (finiteRestrictionMap restrictionLaw sourceCell middleCell section)
        )

restrictionSourceIdentity ::
  (Show cell, Show val, Eq val) =>
  FiniteRestrictionLaw cell val ->
  LawSuite
restrictionSourceIdentity restrictionLaw =
  relatedRestrictionPairLaw restrictionLaw "restriction source identity" $
    \sourceCell targetCell section ->
      assertEqual
        ("source identity along " <> show (sourceCell, targetCell))
        (finiteRestrictionMap restrictionLaw sourceCell targetCell section)
        ( finiteRestrictionMap restrictionLaw sourceCell targetCell
            (finiteRestrictionMap restrictionLaw sourceCell sourceCell section)
        )

restrictionTargetIdentity ::
  (Show cell, Show val, Eq val) =>
  FiniteRestrictionLaw cell val ->
  LawSuite
restrictionTargetIdentity restrictionLaw =
  relatedRestrictionPairLaw restrictionLaw "restriction target identity" $
    \sourceCell targetCell section ->
      assertEqual
        ("target identity along " <> show (sourceCell, targetCell))
        (finiteRestrictionMap restrictionLaw sourceCell targetCell section)
        ( finiteRestrictionMap restrictionLaw targetCell targetCell
            (finiteRestrictionMap restrictionLaw sourceCell targetCell section)
        )

relatedRestrictionPairLaw ::
  FiniteRestrictionLaw cell val ->
  String ->
  (cell -> cell -> val -> Assertion) ->
  LawSuite
relatedRestrictionPairLaw restrictionLaw label check =
  hUnitLaw label $
    traverse_
      (\(sourceCell, sourceUpperSet, sourceSections) ->
         traverseIntSet_ sourceUpperSet $ \targetIndex ->
           withRestrictionCell restrictionLaw targetIndex $ \targetCell ->
             traverse_ (check sourceCell targetCell) sourceSections
      )
      (restrictionRows restrictionLaw)

relatedRestrictionTripleLaw ::
  FiniteRestrictionLaw cell val ->
  String ->
  (cell -> cell -> cell -> val -> Assertion) ->
  LawSuite
relatedRestrictionTripleLaw restrictionLaw label check =
  hUnitLaw label $
    traverse_
      (\(sourceCell, sourceUpperSet, sourceSections) ->
         traverseIntSet_ sourceUpperSet $ \middleIndex ->
           withRestrictionCell restrictionLaw middleIndex $ \middleCell ->
             withRestrictionUpperSet restrictionLaw middleIndex $ \middleUpperSet ->
               traverseIntSet_ middleUpperSet $ \targetIndex ->
                 withRestrictionCell restrictionLaw targetIndex $ \targetCell ->
                   traverse_ (check sourceCell middleCell targetCell) sourceSections
      )
      (restrictionRows restrictionLaw)

restrictionRows ::
  FiniteRestrictionLaw cell val ->
  Vector (cell, IntSet, [val])
restrictionRows restrictionLaw =
  Vector.zip3
    (finiteRestrictionCells restrictionLaw)
    (finiteRestrictionUpperSets restrictionLaw)
    (finiteRestrictionSections restrictionLaw)

traverseIntSet_ :: IntSet -> (Int -> Assertion) -> Assertion
traverseIntSet_ indices action =
  IntSet.foldr (\denseIndex rest -> action denseIndex *> rest) (pure ()) indices

withRestrictionCell ::
  FiniteRestrictionLaw cell val ->
  Int ->
  (cell -> Assertion) ->
  Assertion
withRestrictionCell restrictionLaw denseIndex useCell =
  case finiteRestrictionCells restrictionLaw Vector.!? denseIndex of
    Nothing ->
      assertFailure
        ( "finite restriction cell obstruction: "
            <> show (RestrictionCellIndexOutOfBounds denseIndex)
        )
    Just cell -> useCell cell

withRestrictionUpperSet ::
  FiniteRestrictionLaw cell val ->
  Int ->
  (IntSet -> Assertion) ->
  Assertion
withRestrictionUpperSet restrictionLaw denseIndex useUpperSet =
  case finiteRestrictionUpperSets restrictionLaw Vector.!? denseIndex of
    Nothing ->
      assertFailure
        ( "finite restriction relation obstruction: "
            <> show (RestrictionUpperSetIndexOutOfBounds denseIndex)
        )
    Just upperSet -> useUpperSet upperSet