packages feed

keiki-0.9.0.0: test/Keiki/FullSymbolicReplayInversionSpec.hs

{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE TypeFamilies #-}

module Keiki.FullSymbolicReplayInversionSpec (spec) where

import Data.List.NonEmpty (NonEmpty (..))
import Data.Proxy (Proxy (..))
import GHC.Generics (Generic)
import Keiki.Core
import Keiki.Generics
  ( FieldsOf,
    RegFieldsOf,
    mkInCtorVia,
    mkWireCtor0Via,
    mkWireCtorVia,
  )
import Keiki.Symbolic
import Test.Hspec

data AmountData = AmountData {amount :: Int}
  deriving stock (Eq, Show, Generic)

data Command
  = Submit AmountData
  | Alternate AmountData
  deriving stock (Eq, Show, Generic)

data RecordedData = RecordedData
  { amount :: Int,
    checked :: Int
  }
  deriving stock (Eq, Show, Generic)

data Event
  = Recorded RecordedData
  | OtherRecorded RecordedData
  | FirstNullary
  | SecondNullary
  | ThirdNullary
  deriving stock (Eq, Show, Generic)

data Unsupported = Unsupported Int
  deriving stock (Eq, Show)

data UnsupportedData = UnsupportedData
  { recoveredAmount :: Int,
    unsupportedValue :: Unsupported
  }
  deriving stock (Eq, Show, Generic)

data UnsupportedEvent = UnsupportedRecorded UnsupportedData
  deriving stock (Eq, Show, Generic)

data ProjectedData = ProjectedData
  { projectedAmount :: Int,
    projectedFlag :: Bool
  }
  deriving stock (Eq, Show, Generic)

data ProjectedEvent = ProjectedRecorded ProjectedData
  deriving stock (Eq, Show, Generic)

data FlagProjection

instance FieldProjection FlagProjection where
  type FieldName FlagProjection = "flag"
  type FieldOwner FlagProjection = Bool
  type FieldResult FlagProjection = Bool
  fieldShapeId _ = "bool/identity"
  projectFieldValue _ = id

instance ExactFieldProjection FlagProjection where
  fieldProjectionDomain _ = finiteProjectionDomain (False :| [True])
  reconstructFieldOwner _ = Just

data Vertex = Only
  deriving stock (Eq, Show, Enum, Bounded)

data PairVertex = RegisterVertex | StructuralVertex
  deriving stock (Eq, Show, Enum, Bounded)

type AmountFields = RegFieldsOf AmountData

inSubmit :: InCtor Command AmountFields
inSubmit = mkInCtorVia @"Submit"

inAlternate :: InCtor Command AmountFields
inAlternate = mkInCtorVia @"Alternate"

wireRecorded :: WireCtor Event (FieldsOf RecordedData)
wireRecorded = mkWireCtorVia @"Recorded"

wireOtherRecorded :: WireCtor Event (FieldsOf RecordedData)
wireOtherRecorded = mkWireCtorVia @"OtherRecorded"

wireUnsupported :: WireCtor UnsupportedEvent (FieldsOf UnsupportedData)
wireUnsupported = mkWireCtorVia @"UnsupportedRecorded"

wireProjected :: WireCtor ProjectedEvent (FieldsOf ProjectedData)
wireProjected = mkWireCtorVia @"ProjectedRecorded"

wireFirstNullary :: WireCtor Event ()
wireFirstNullary = mkWireCtor0Via @"FirstNullary"

wireSecondNullary :: WireCtor Event ()
wireSecondNullary = mkWireCtor0Via @"SecondNullary"

wireThirdNullary :: WireCtor Event ()
wireThirdNullary = mkWireCtor0Via @"ThirdNullary"

submitAmount :: Term rs Command AmountFields Int
submitAmount = TInpCtorField inSubmit (#amount)

alternateAmount :: Term rs Command AmountFields Int
alternateAmount = TInpCtorField inAlternate (#amount)

recordedWith ::
  InCtor Command AmountFields ->
  Term rs Command AmountFields Int ->
  Term rs Command AmountFields Int ->
  OutTerm rs Command Event
recordedWith inputCtor recovered derived =
  pack inputCtor wireRecorded (recovered *: derived *: oNil)

recordedPlus :: Int -> OutTerm rs Command Event
recordedPlus increment =
  recordedWith
    inSubmit
    submitAmount
    (TArith OpAdd submitAmount (TLit increment))

recordedLiteral :: Int -> OutTerm rs Command Event
recordedLiteral value = recordedWith inSubmit submitAmount (TLit value)

recordedOpaqueApplication :: (Int -> Int) -> OutTerm rs Command Event
recordedOpaqueApplication function =
  recordedWith inSubmit submitAmount (TApp1 function submitAmount)

recordedAlternate :: OutTerm rs Command Event
recordedAlternate =
  recordedWith inAlternate alternateAmount alternateAmount

edge ::
  EdgeMode ->
  HsPred rs Command ->
  OutTerm rs Command event ->
  Edge (HsPred rs Command) rs Command event Vertex
edge edgeMode predicate emitted =
  Edge
    { guard = predicate,
      update = UKeep,
      output = [emitted],
      target = Only,
      mode = edgeMode
    }

pairEdge ::
  PairVertex ->
  EdgeMode ->
  HsPred rs Command ->
  OutTerm rs Command event ->
  Edge (HsPred rs Command) rs Command event PairVertex
pairEdge targetVertex edgeMode predicate emitted =
  Edge
    { guard = predicate,
      update = UKeep,
      output = [emitted],
      target = targetVertex,
      mode = edgeMode
    }

machine ::
  RegFile rs ->
  [Edge (HsPred rs Command) rs Command event Vertex] ->
  SymTransducer (HsPred rs Command) rs Vertex Command event
machine registers outgoing =
  SymTransducer
    { edgesOut = \Only -> outgoing,
      initial = Only,
      initialRegs = registers,
      isFinal = const True
    }

noRegsMachine ::
  [Edge (HsPred '[] Command) '[] Command event Vertex] ->
  SymTransducer (HsPred '[] Command) '[] Vertex Command event
noRegsMachine = machine RNil

onlyDetail :: [InversionAnalysisDetail Vertex] -> InversionAnalysisDetail Vertex
onlyDetail [detail] = detail
onlyDetail details = error ("expected exactly one inversion detail, got " <> show (length details))

warningKey ::
  (Enum s) =>
  TransducerValidationWarning s ->
  (Int, Int, Int)
warningKey InversionAmbiguity {tvwSource, tvwEdgeA, tvwEdgeB} =
  (fromEnum tvwSource, tvwEdgeA, tvwEdgeB)
warningKey _ = error "expected an inversion warning"

detailKey ::
  (Enum s) =>
  InversionAnalysisDetail s ->
  (Int, Int, Int)
detailKey detail =
  ( fromEnum detail.iadSource,
    detail.iadLeftEdge.edgeIndex,
    detail.iadRightEdge.edgeIndex
  )

isDefiniteUnsat :: InversionAnalysisDetail s -> Bool
isDefiniteUnsat detail =
  iadSolverStatus detail == InversionSolverUnsatisfiable
    && iadVerdict detail == InversionProvedDisjoint

spec :: Spec
spec = describe "full symbolic replay inversion" $ do
  it "proves an output-dependent pair disjoint and removes its compatibility warning" $ do
    let transducer =
          noRegsMachine
            [ edge Live (PInCtor inSubmit) (recordedPlus 0),
              edge Live (PInCtor inSubmit) (recordedPlus 1)
            ]
    length (inversionAmbiguityWarnings transducer) `shouldBe` 1
    detail <- onlyDetail <$> checkInversionAmbiguitySymDetailed transducer
    iadHeadRelation detail `shouldBe` WireHeadsStructurallyEqual
    iadSolverStatus detail `shouldBe` InversionSolverUnsatisfiable
    iadVerdict detail `shouldBe` InversionProvedDisjoint
    checkInversionAmbiguitySym transducer `shouldReturn` []

  it "matches mixed solver verdicts to warning pairs by source and edge indices" $ do
    let registers = RCons (Proxy @"limit") 0 RNil
        lower =
          PAnd
            (PInCtor inSubmit)
            (PCmp CmpLt (TReg (#limit)) (TLit (0 :: Int)))
        upper =
          PAnd
            (PInCtor inSubmit)
            (PCmp CmpGe (TReg (#limit)) (TLit (0 :: Int)))
        transducer =
          machine
            registers
            [ edge Live lower (recordedPlus 0),
              edge Live upper (recordedPlus 1),
              edge Live (PInCtor inSubmit) (recordedPlus 2),
              edge Live (PInCtor inSubmit) (recordedPlus 3)
            ]
        pureWarnings = inversionAmbiguityWarnings transducer
    details <- checkInversionAmbiguitySymDetailed transducer
    compatibilityWarnings <- checkInversionAmbiguitySym transducer
    length pureWarnings `shouldBe` 5
    fmap detailKey details `shouldMatchList` fmap warningKey pureWarnings
    fmap isDefiniteUnsat details `shouldBe` replicate 5 True
    compatibilityWarnings `shouldBe` []

  it "does not let an unrelated UNSAT detail suppress a renamed structural pair" $ do
    let registers = RCons (Proxy @"limit") 0 RNil
        lower =
          PAnd
            (PInCtor inSubmit)
            (PCmp CmpLt (TReg (#limit)) (TLit (0 :: Int)))
        upper =
          PAnd
            (PInCtor inSubmit)
            (PCmp CmpGe (TReg (#limit)) (TLit (0 :: Int)))
        renamedRecorded = renameWireCtor "RenamedRecorded" wireRecorded
        renamedOutput =
          pack inSubmit renamedRecorded (submitAmount *: submitAmount *: oNil)
        transducer =
          SymTransducer
            { edgesOut = \case
                RegisterVertex ->
                  [ pairEdge RegisterVertex Live lower (recordedPlus 0),
                    pairEdge RegisterVertex Live upper (recordedPlus 1)
                  ]
                StructuralVertex ->
                  [ pairEdge StructuralVertex Live (PInCtor inSubmit) (recordedPlus 0),
                    pairEdge StructuralVertex Live (PInCtor inSubmit) renamedOutput
                  ],
              initial = RegisterVertex,
              initialRegs = registers,
              isFinal = const True
            }
        pureWarnings = inversionAmbiguityWarnings transducer
    details <- checkInversionAmbiguitySymDetailed transducer
    compatibilityWarnings <- checkInversionAmbiguitySym transducer
    fmap warningKey pureWarnings `shouldBe` [(fromEnum StructuralVertex, 0, 1)]
    fmap detailKey details `shouldBe` [(fromEnum StructuralVertex, 0, 1)]
    fmap iadSolverStatus details `shouldBe` [InversionSolverSatisfiable]
    compatibilityWarnings `shouldBe` pureWarnings

  it "retains a real overlap and does not call SAT a concrete witness" $ do
    let transducer =
          noRegsMachine
            [ edge Live (PInCtor inSubmit) (recordedPlus 0),
              edge Live (PInCtor inSubmit) (recordedPlus 0)
            ]
    detail <- onlyDetail <$> checkInversionAmbiguitySymDetailed transducer
    iadSolverStatus detail `shouldBe` InversionSolverSatisfiable
    iadVerdict detail `shouldBe` InversionNotProvedDisjoint
    length <$> checkInversionAmbiguitySym transducer `shouldReturn` 1

  it "shares registers and proves guard-only disjointness" $ do
    let registers = RCons (Proxy @"limit") 0 RNil
        lower =
          PAnd
            (PInCtor inSubmit)
            (PNot (PCmp CmpGe (TReg (#limit)) (TLit (0 :: Int))))
        upper = PAnd (PInCtor inSubmit) (PCmp CmpGe (TReg (#limit)) (TLit (0 :: Int)))
        transducer =
          machine
            registers
            [ edge Live lower (recordedPlus 0),
              edge Live upper (recordedPlus 0)
            ]
    iadVerdict . onlyDetail <$> checkInversionAmbiguitySymDetailed transducer
      `shouldReturn` InversionProvedDisjoint

  it "keeps candidate commands independent" $ do
    let transducer =
          noRegsMachine
            [ edge Live (PInCtor inSubmit) (recordedPlus 0),
              edge Live (PInCtor inAlternate) recordedAlternate
            ]
    iadSolverStatus . onlyDetail <$> checkInversionAmbiguitySymDetailed transducer
      `shouldReturn` InversionSolverSatisfiable

  it "leaves literal output positions unconstrained" $ do
    let transducer =
          noRegsMachine
            [ edge Live (PInCtor inSubmit) (recordedLiteral 0),
              edge Live (PInCtor inSubmit) (recordedLiteral 1)
            ]
    iadSolverStatus . onlyDetail <$> checkInversionAmbiguitySymDetailed transducer
      `shouldReturn` InversionSolverSatisfiable

  it "leaves TReg audit outputs unconstrained and keys duplicate labels by position" $ do
    let firstDuplicate = ZIdx :: Index '[ '("dup", Int), '("dup", Int)] Int
        secondDuplicate = SIdx ZIdx :: Index '[ '("dup", Int), '("dup", Int)] Int
        registers =
          RCons (Proxy @"dup") 0 (RCons (Proxy @"dup") 1 RNil)
        leftGuard =
          PAnd
            (PInCtor inSubmit)
            (PEq (TReg firstDuplicate) (TLit (0 :: Int)))
        rightGuard =
          PAnd
            (PInCtor inSubmit)
            (PEq (TReg secondDuplicate) (TLit (1 :: Int)))
        leftOutput = recordedWith inSubmit submitAmount (TReg firstDuplicate)
        rightOutput = recordedWith inSubmit submitAmount (TReg secondDuplicate)
        transducer =
          machine
            registers
            [ edge Live leftGuard leftOutput,
              edge Live rightGuard rightOutput
            ]
    iadSolverStatus . onlyDetail <$> checkInversionAmbiguitySymDetailed transducer
      `shouldReturn` InversionSolverSatisfiable

  it "widens opaque TApp verification and records both candidates" $ do
    let transducer =
          noRegsMachine
            [ edge Live (PInCtor inSubmit) (recordedOpaqueApplication id),
              edge Live (PInCtor inSubmit) (recordedOpaqueApplication (+ 1))
            ]
    detail <- onlyDetail <$> checkInversionAmbiguitySymDetailed transducer
    iadSolverStatus detail `shouldBe` InversionSolverSatisfiable
    iadTranslationIssues detail
      `shouldContain` [InversionOpaqueDerivedOutput InversionCandidateA 1]
    iadTranslationIssues detail
      `shouldContain` [InversionOpaqueDerivedOutput InversionCandidateB 1]

  it "does not run a solver when structural evidence is missing" $ do
    let unavailable =
          unavailableWireCtor
            (wcName wireRecorded)
            (wcMatch wireRecorded)
            (wcBuild wireRecorded)
        outputWith wire = pack inSubmit wire (submitAmount *: submitAmount *: oNil)
        transducer =
          noRegsMachine
            [ edge Live (PInCtor inSubmit) (outputWith wireRecorded),
              edge Live (PInCtor inSubmit) (outputWith unavailable)
            ]
    detail <- onlyDetail <$> checkInversionAmbiguitySymDetailed transducer
    iadHeadRelation detail `shouldBe` WireHeadsUnwitnessed
    iadSolverStatus detail `shouldBe` InversionSolverNotRun
    iadTranslationIssues detail `shouldBe` [InversionWireSchemasUnwitnessed]

  it "widens unsupported observed carriers" $ do
    let outputWith function =
          pack
            inSubmit
            wireUnsupported
            (submitAmount *: TApp1 function submitAmount *: oNil)
        transducer =
          noRegsMachine
            [ edge Live (PInCtor inSubmit) (outputWith Unsupported),
              edge Live (PInCtor inSubmit) (outputWith (Unsupported . (+ 1)))
            ]
    detail <- onlyDetail <$> checkInversionAmbiguitySymDetailed transducer
    iadSolverStatus detail `shouldBe` InversionSolverSatisfiable
    iadTranslationIssues detail
      `shouldSatisfy` any (\case InversionUnsupportedObservedFieldCarrier 1 _ -> True; _ -> False)

  it "relates exact structural projections to the shared observed field" $ do
    let flagA = ZIdx :: Index '[ '("flag", Bool), '("flag", Bool)] Bool
        flagB = SIdx ZIdx :: Index '[ '("flag", Bool), '("flag", Bool)] Bool
        projected index = TFieldProj (exactFieldWitness @FlagProjection) (PBReg index)
        outputWith index =
          pack inSubmit wireProjected (submitAmount *: projected index *: oNil)
        leftGuard =
          PAnd
            (PInCtor inSubmit)
            (PEq (projected flagA) (TLit True))
        rightGuard =
          PAnd
            (PInCtor inSubmit)
            (PEq (projected flagB) (TLit False))
        registers = RCons (Proxy @"flag") False (RCons (Proxy @"flag") False RNil)
        transducer =
          machine
            registers
            [ edge Live leftGuard (outputWith flagA),
              edge Live rightGuard (outputWith flagB)
            ]
    detail <- onlyDetail <$> checkInversionAmbiguitySymDetailed transducer
    iadSolverStatus detail `shouldBe` InversionSolverUnsatisfiable
    iadTranslationIssues detail
      `shouldSatisfy` all (\case InversionUnsupportedDerivedProjection {} -> False; _ -> True)

  it "does not analyze structurally different heads even when short names collide" $ do
    let sameNameRecorded = renameWireCtor "Same" wireRecorded
        sameNameOther = renameWireCtor "Same" wireOtherRecorded
        firstOutput = pack inSubmit sameNameRecorded (submitAmount *: submitAmount *: oNil)
        secondOutput = pack inSubmit sameNameOther (submitAmount *: submitAmount *: oNil)
        transducer =
          noRegsMachine
            [ edge Live (PInCtor inSubmit) firstOutput,
              edge Live (PInCtor inSubmit) secondOutput
            ]
    inversionAmbiguityWarnings transducer `shouldBe` []
    checkInversionAmbiguitySymDetailed transducer `shouldReturn` []

  it "analyzes live with live and replay-only with replay-only only" $ do
    let one = edge Live (PInCtor inSubmit) (recordedPlus 0)
        two = edge Live (PInCtor inSubmit) (recordedPlus 0)
        three = edge ReplayOnly (PInCtor inSubmit) (recordedPlus 0)
        four = edge ReplayOnly (PInCtor inSubmit) (recordedPlus 0)
        transducer = noRegsMachine [one, two, three, four]
    details <- checkInversionAmbiguitySymDetailed transducer
    ((iadLeftEdge &&& iadRightEdge) <$> details)
      `shouldBe` [(EdgeRef Only 0, EdgeRef Only 1), (EdgeRef Only 2, EdgeRef Only 3)]

  it "derives pairwise-distinct trusted schemas for an all-nullary event sum" $ do
    classifyWireHeads wireFirstNullary wireSecondNullary
      `shouldBe` WireHeadsStructurallyDifferent
    classifyWireHeads wireFirstNullary wireThirdNullary
      `shouldBe` WireHeadsStructurallyDifferent
    classifyWireHeads wireSecondNullary wireThirdNullary
      `shouldBe` WireHeadsStructurallyDifferent

  it "agrees with a finite concrete no-double-candidate oracle for every UNSAT pair" $ do
    let leftOutput = recordedPlus 0 :: OutTerm '[] Command Event
        rightOutput = recordedPlus 1 :: OutTerm '[] Command Event
        transducer =
          noRegsMachine
            [ edge Live (PInCtor inSubmit) leftOutput,
              edge Live (PInCtor inSubmit) rightOutput
            ]
        events =
          [ Recorded (RecordedData amountValue checkedValue)
          | amountValue <- [-2 .. 2],
            checkedValue <- [-2 .. 3]
          ]
        candidate outputTerm event =
          case solveOutput outputTerm RNil event of
            Nothing -> False
            Just command -> evalPred (PInCtor inSubmit) RNil command
        concreteDoubleCandidate event =
          candidate leftOutput event && candidate rightOutput event
    detail <- onlyDetail <$> checkInversionAmbiguitySymDetailed transducer
    iadSolverStatus detail `shouldBe` InversionSolverUnsatisfiable
    any concreteDoubleCandidate events `shouldBe` False

infixr 3 &&&

(&&&) :: (value -> left) -> (value -> right) -> value -> (left, right)
(left &&& right) value = (left value, right value)