packages feed

moonlight-pale-0.1.0.0: test/test-support/Recursion/RecursionSpec.hs

{-# LANGUAGE DerivingStrategies #-}

module Recursion.RecursionSpec
  ( tests,
  )
where

import Hedgehog qualified as HH
import Hedgehog.Gen qualified as Gen
import Hedgehog.Range qualified as Range
import Moonlight.Pale.Test.Recursion
  ( cataAfterAnaIdentity,
    interpreterCoherence,
  )
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.Hedgehog qualified as TH
import Test.Tasty.HUnit ((@?=), testCase)
import Test.Tasty.QuickCheck qualified as QC

newtype RecursionBound = RecursionBound
  { recursionBoundValue :: Int
  }
  deriving stock (Eq, Show)

data RecursionTrace = RecursionTrace
  { recursionTraceConfiguredBound :: RecursionBound,
    recursionTraceVisitedFrames :: [Int]
  }
  deriving stock (Eq, Show)

data RecursionReport = RecursionReport
  { recursionReportSteps :: Int,
    recursionReportStoppedAtBound :: Bool
  }
  deriving stock (Eq, Show)

tests :: TestTree
tests =
  testGroup
    "Moonlight.Pale.Test.Recursion"
    [ testCase "cata-after-ana distinguishes coherent and incoherent inverses" $ do
        cataAfterAnaIdentity boundedAna traceConfiguredBound configuredBound @?= True
        cataAfterAnaIdentity boundedAna underreportedTraceBound configuredBound @?= False,
      testCase "interpreter coherence distinguishes matching and mismatched reports" $ do
        interpreterCoherence boundedAna boundedCata boundedHylo configuredBound @?= True
        interpreterCoherence boundedAna mismatchedCata boundedHylo configuredBound @?= False,
      QC.testProperty "QuickCheck: bounded recursion reports its configured limit" $
        QC.property boundedReportMatchesNonNegative,
      TH.testProperty "Hedgehog: bounded recursion reports its configured limit" $
        HH.property (HH.forAll boundedGenerator >>= HH.assert . boundedReportMatchesBound)
    ]

configuredBound :: RecursionBound
configuredBound =
  RecursionBound 4

boundedGenerator :: HH.Gen RecursionBound
boundedGenerator =
  RecursionBound <$> Gen.int (Range.linear 0 16)

boundedReportMatchesNonNegative :: QC.NonNegative Int -> Bool
boundedReportMatchesNonNegative rawBound =
  boundedReportMatchesBound (smallRecursionBound rawBound)

boundedReportMatchesBound :: RecursionBound -> Bool
boundedReportMatchesBound bound =
  boundedHylo bound
    == RecursionReport
      { recursionReportSteps = recursionBoundValue bound,
        recursionReportStoppedAtBound = True
      }

smallRecursionBound :: QC.NonNegative Int -> RecursionBound
smallRecursionBound (QC.NonNegative rawBound) =
  RecursionBound (rawBound `mod` 17)

boundedAna :: RecursionBound -> RecursionTrace
boundedAna bound =
  RecursionTrace
    { recursionTraceConfiguredBound = bound,
      recursionTraceVisitedFrames = [0 .. recursionBoundValue bound - 1]
    }

boundedCata :: RecursionTrace -> RecursionReport
boundedCata trace =
  RecursionReport
    { recursionReportSteps = length (recursionTraceVisitedFrames trace),
      recursionReportStoppedAtBound = length (recursionTraceVisitedFrames trace) == recursionBoundValue (recursionTraceConfiguredBound trace)
    }

boundedHylo :: RecursionBound -> RecursionReport
boundedHylo =
  boundedCata . boundedAna

traceConfiguredBound :: RecursionTrace -> RecursionBound
traceConfiguredBound =
  recursionTraceConfiguredBound

underreportedTraceBound :: RecursionTrace -> RecursionBound
underreportedTraceBound trace =
  RecursionBound (recursionBoundValue (recursionTraceConfiguredBound trace) - 1)

mismatchedCata :: RecursionTrace -> RecursionReport
mismatchedCata trace =
  RecursionReport
    { recursionReportSteps = recursionReportSteps (boundedCata trace) + 1,
      recursionReportStoppedAtBound = False
    }