packages feed

moonlight-core-0.1.0.0: test/syntax/TheorySpec.hs

{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE DerivingStrategies #-}

module TheorySpec (tests) where

import Data.Set qualified as Set
import Moonlight.Core qualified as EGraph
import Moonlight.Core (IsLawName (..), constructorLawName)
import Moonlight.Core (Pattern (..))
import LawProperty (lawProperty)
import Moonlight.Core
  ( StructuralLaw (..),
    TheorySpec (..),
    canonicalizeLayerByTheory,
    canonicalizePatternByTheory,
    commutativeBinary,
    expandPatternByTheory,
  )
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit ((@?=), testCase)
import Test.Tasty.QuickCheck
  ( Arbitrary (..),
    Gen,
    Property,
    chooseInt,
    counterexample,
    frequency,
    oneof,
    property,
    sized,
    (===),
  )

data TheoryLaw
  = TheoryCanonicalizationIdempotent
  | TheoryCanonicalChoiceDeterministicByTermOrder
  | TheoryExpansionExactCommutativeOrbit
  | TheoryExpansionDuplicateFree
  | TheoryExpansionContainsCanonicalForm
  deriving stock (Bounded, Enum, Eq, Ord, Show)

instance IsLawName TheoryLaw where
  lawNameText =
    constructorLawName . show

data TheoryNode a
  = TheoryLeaf !Int
  | TheoryUnary !a
  | TheoryAdd !a !a
  | TheoryPair !a !a
  deriving stock (Eq, Ord, Show, Functor, Foldable, Traversable)

newtype SmallTheoryNode = SmallTheoryNode (TheoryNode Int)
  deriving stock (Eq, Ord, Show)

newtype SmallTheoryPattern = SmallTheoryPattern (Pattern TheoryNode)
  deriving stock (Eq, Ord, Show)

instance Arbitrary SmallTheoryNode where
  arbitrary =
    SmallTheoryNode <$> genTheoryNode genChild

instance Arbitrary SmallTheoryPattern where
  arbitrary =
    SmallTheoryPattern <$> sized (genPattern . min 5)

tests :: TestTree
tests =
  testGroup
    "Theory"
    [ lawProperty TheoryCanonicalizationIdempotent propCanonicalizationIdempotent,
      lawProperty TheoryCanonicalChoiceDeterministicByTermOrder propCanonicalChoiceDeterministicByTermOrder,
      lawProperty TheoryExpansionExactCommutativeOrbit propExpansionExactCommutativeOrbit,
      lawProperty TheoryExpansionDuplicateFree propExpansionDuplicateFree,
      lawProperty TheoryExpansionContainsCanonicalForm propExpansionContainsCanonicalForm,
      testCase "recursive canonicalization orients pattern variables before pattern nodes" testRecursiveCanonicalizationDeclarationOrder
    ]

propCanonicalizationIdempotent :: SmallTheoryNode -> Property
propCanonicalizationIdempotent (SmallTheoryNode node) =
  canonicalizeLayerByTheory commutativeTheory (canonicalizeLayerByTheory commutativeTheory node)
    === canonicalizeLayerByTheory commutativeTheory node

propCanonicalChoiceDeterministicByTermOrder :: SmallTheoryNode -> Property
propCanonicalChoiceDeterministicByTermOrder (SmallTheoryNode node) =
  canonicalizeLayerByTheory commutativeTheory node === expectedCanonicalNode node

propExpansionExactCommutativeOrbit :: SmallTheoryPattern -> Property
propExpansionExactCommutativeOrbit (SmallTheoryPattern patternValue) =
  Set.fromList (expandPatternByTheory commutativeTheory patternValue) === commutativeOrbit patternValue

propExpansionDuplicateFree :: SmallTheoryPattern -> Property
propExpansionDuplicateFree (SmallTheoryPattern patternValue) =
  Set.size expandedSet === length expandedPatterns
  where
    expandedPatterns =
      expandPatternByTheory commutativeTheory patternValue
    expandedSet =
      Set.fromList expandedPatterns

propExpansionContainsCanonicalForm :: SmallTheoryPattern -> Property
propExpansionContainsCanonicalForm (SmallTheoryPattern patternValue) =
  counterexample "expanded orbit does not contain the recursive canonical form" $
    property (Set.member (canonicalizePatternByTheory commutativeTheory patternValue) (Set.fromList (expandPatternByTheory commutativeTheory patternValue)))

testRecursiveCanonicalizationDeclarationOrder :: IO ()
testRecursiveCanonicalizationDeclarationOrder =
  canonicalizePatternByTheory commutativeTheory unorderedPattern
    @?= orderedPattern
  where
    unorderedPattern =
      PatternNode
        ( TheoryAdd
            (PatternNode (TheoryLeaf 1))
            (PatternVar (EGraph.mkPatternVar 0))
        )
    orderedPattern =
      PatternNode
        ( TheoryAdd
            (PatternVar (EGraph.mkPatternVar 0))
            (PatternNode (TheoryLeaf 1))
        )

commutativeTheory :: TheorySpec TheoryNode
commutativeTheory =
  TheorySpec
    { tsClassify = \node ->
        case node of
          TheoryAdd _left _right ->
            commutativeBinary TheoryAdd
          _ ->
            Ordinary
    }

expectedCanonicalNode :: Ord a => TheoryNode a -> TheoryNode a
expectedCanonicalNode node =
  case node of
    TheoryAdd left right ->
      TheoryAdd (min left right) (max left right)
    _ ->
      node

commutativeOrbit :: Pattern TheoryNode -> Set.Set (Pattern TheoryNode)
commutativeOrbit patternValue =
  case patternValue of
    PatternVar patternVar ->
      Set.singleton (PatternVar patternVar)
    PatternNode node ->
      Set.fromList (PatternNode <$> (expandedChildNodes >>= localNodeOrbit))
      where
        expandedChildNodes =
          traverse (Set.toList . commutativeOrbit) node

localNodeOrbit :: TheoryNode (Pattern TheoryNode) -> [TheoryNode (Pattern TheoryNode)]
localNodeOrbit node =
  case node of
    TheoryAdd left right
      | left == right ->
          [node]
      | otherwise ->
          [node, TheoryAdd right left]
    _ ->
      [node]

genChild :: Gen Int
genChild =
  chooseInt (0, 5)

genPatternVar :: Gen EGraph.PatternVar
genPatternVar =
  EGraph.mkPatternVar <$> chooseInt (0, 5)

genTheoryNode :: Gen a -> Gen (TheoryNode a)
genTheoryNode genValue =
  frequency
    [ (1, TheoryLeaf <$> chooseInt (0, 3)),
      (2, TheoryUnary <$> genValue),
      (3, TheoryAdd <$> genValue <*> genValue),
      (2, TheoryPair <$> genValue <*> genValue)
    ]

genPattern :: Int -> Gen (Pattern TheoryNode)
genPattern size =
  case compare size 0 of
    GT ->
      frequency
        [ (2, PatternVar <$> genPatternVar),
          (2, PatternNode . TheoryLeaf <$> chooseInt (0, 3)),
          (2, PatternNode . TheoryUnary <$> genSmallerPattern),
          (3, PatternNode <$> (TheoryAdd <$> genSmallerPattern <*> genSmallerPattern)),
          (2, PatternNode <$> (TheoryPair <$> genSmallerPattern <*> genSmallerPattern))
        ]
    _ ->
      oneof
        [ PatternVar <$> genPatternVar,
          PatternNode . TheoryLeaf <$> chooseInt (0, 3)
        ]
  where
    genSmallerPattern =
      genPattern (size `div` 2)