packages feed

keiki-0.1.0.0: test/Keiki/CoreHiddenInputsGSMSpec.hs

module Keiki.CoreHiddenInputsGSMSpec (spec) where

import Data.List (isInfixOf)
import Data.Proxy (Proxy (..))
import Keiki.Core
import Test.Hspec

-- | A 3-slot input constructor used to stress the union check.
data MultiInput = Begin Int Int Int deriving (Eq, Show)

data MultiOutput
  = OutAB Int Int -- recovers slots a, b
  | OutBC Int Int -- recovers slots b, c
  | OutA Int -- recovers slot a only
  deriving (Eq, Show)

inCtorBegin :: InCtor MultiInput '[ '("a", Int), '("b", Int), '("c", Int)]
inCtorBegin =
  InCtor
    { icName = "Begin",
      icMatch = \case
        Begin a b c ->
          Just $
            RCons (Proxy @"a") a $
              RCons (Proxy @"b") b $
                RCons (Proxy @"c") c $
                  RNil,
      icBuild = \(RCons _ a (RCons _ b (RCons _ c RNil))) -> Begin a b c
    }

wcAB :: WireCtor MultiOutput (Int, (Int, ()))
wcAB =
  WireCtor
    { wcName = "OutAB",
      wcMatch = \case
        OutAB a b -> Just (a, (b, ()))
        _ -> Nothing,
      wcBuild = \(a, (b, ())) -> OutAB a b
    }

wcBC :: WireCtor MultiOutput (Int, (Int, ()))
wcBC =
  WireCtor
    { wcName = "OutBC",
      wcMatch = \case
        OutBC b c -> Just (b, (c, ()))
        _ -> Nothing,
      wcBuild = \(b, (c, ())) -> OutBC b c
    }

wcA :: WireCtor MultiOutput (Int, ())
wcA =
  WireCtor
    { wcName = "OutA",
      wcMatch = \case
        OutA a -> Just (a, ())
        _ -> Nothing,
      wcBuild = \(a, ()) -> OutA a
    }

-- | The "well-formed" multi-event edge: two OPacks whose union of
-- visited slots covers all three of @Begin@'s slots.
-- OPack #1 (OutAB) visits {a, b}; OPack #2 (OutBC) visits {b, c}.
-- Union = {a, b, c} = full InCtor coverage. No warning expected.
goodUnion :: SymTransducer (HsPred '[] MultiInput) '[] Bool MultiInput MultiOutput
goodUnion =
  SymTransducer
    { edgesOut = \case
        False ->
          [ Edge
              { guard = matchInCtor inCtorBegin,
                update = UKeep,
                output =
                  [ pack
                      inCtorBegin
                      wcAB
                      ( OFCons
                          (TInpCtorField inCtorBegin (#a :: Index '[ '("a", Int), '("b", Int), '("c", Int)] Int))
                          (OFCons (TInpCtorField inCtorBegin (#b :: Index '[ '("a", Int), '("b", Int), '("c", Int)] Int)) OFNil)
                      ),
                    pack
                      inCtorBegin
                      wcBC
                      ( OFCons
                          (TInpCtorField inCtorBegin (#b :: Index '[ '("a", Int), '("b", Int), '("c", Int)] Int))
                          (OFCons (TInpCtorField inCtorBegin (#c :: Index '[ '("a", Int), '("b", Int), '("c", Int)] Int)) OFNil)
                      )
                  ],
                target = True
              }
          ]
        True -> [],
      initial = False,
      initialRegs = RNil,
      isFinal = id
    }

-- | The "ill-formed" multi-event edge: two OPacks whose union of
-- visited slots is {a, b}, leaving slot @c@ unrecovered. Both OPacks
-- name the same InCtor (@Begin@); the union check should flag @c@.
badUnion :: SymTransducer (HsPred '[] MultiInput) '[] Bool MultiInput MultiOutput
badUnion =
  SymTransducer
    { edgesOut = \case
        False ->
          [ Edge
              { guard = matchInCtor inCtorBegin,
                update = UKeep,
                output =
                  [ pack
                      inCtorBegin
                      wcAB
                      ( OFCons
                          (TInpCtorField inCtorBegin (#a :: Index '[ '("a", Int), '("b", Int), '("c", Int)] Int))
                          (OFCons (TInpCtorField inCtorBegin (#b :: Index '[ '("a", Int), '("b", Int), '("c", Int)] Int)) OFNil)
                      ),
                    pack
                      inCtorBegin
                      wcA
                      (OFCons (TInpCtorField inCtorBegin (#a :: Index '[ '("a", Int), '("b", Int), '("c", Int)] Int)) OFNil)
                  ],
                target = True
              }
          ]
        True -> [],
      initial = False,
      initialRegs = RNil,
      isFinal = id
    }

-- | A single-event edge that does NOT cover all slots of its InCtor.
-- Legacy behaviour: the per-OPack check fires. Confirms the union
-- check is a strict generalisation (not a regression) of the legacy
-- single-event check.
badSingle :: SymTransducer (HsPred '[] MultiInput) '[] Bool MultiInput MultiOutput
badSingle =
  SymTransducer
    { edgesOut = \case
        False ->
          [ Edge
              { guard = matchInCtor inCtorBegin,
                update = UKeep,
                output =
                  [ pack
                      inCtorBegin
                      wcA
                      (OFCons (TInpCtorField inCtorBegin (#a :: Index '[ '("a", Int), '("b", Int), '("c", Int)] Int)) OFNil)
                  ],
                target = True
              }
          ]
        True -> [],
      initial = False,
      initialRegs = RNil,
      isFinal = id
    }

spec :: Spec
spec = do
  describe "checkHiddenInputs union strengthening (EP-19 M4)" $ do
    it "well-formed multi-event edge (union covers all slots) ⇒ no warnings" $
      checkHiddenInputs goodUnion `shouldBe` []

    it "ill-formed multi-event edge (union still misses slot c) ⇒ warning names c" $ do
      let warnings = checkHiddenInputs badUnion
      length warnings `shouldBe` 1
      case warnings of
        [w] -> do
          hiwEdgeSource w `shouldBe` "False"
          hiwReason w `shouldSatisfy` ("Begin" `isInfixOf`)
          hiwReason w `shouldSatisfy` ("\"c\"" `isInfixOf`)
        _ -> expectationFailure "expected exactly one warning"

    it "single-event edge missing slots fires too (legacy compat)" $ do
      let warnings = checkHiddenInputs badSingle
      length warnings `shouldBe` 1
      case warnings of
        [w] -> do
          hiwEdgeSource w `shouldBe` "False"
          hiwReason w `shouldSatisfy` ("Begin" `isInfixOf`)
          -- Legacy behaviour: missing both b and c.
          hiwReason w `shouldSatisfy` ("\"b\"" `isInfixOf`)
          hiwReason w `shouldSatisfy` ("\"c\"" `isInfixOf`)
        _ -> expectationFailure "expected exactly one warning"