packages feed

keiki-0.9.0.0: test/Keiki/CompositionMultiEventSpec.hs

{-# OPTIONS_GHC -Wno-partial-fields #-}

-- | EP-19 M6 acceptance: 'Keiki.Composition.compose' on a multi-event
-- first-edge produces a length-N composite edge via library-side
-- chain expansion. The fixture is intentionally minimal: t1 has one
-- vertex (Q) with a self-loop edge emitting two mid-symbols; t2 has
-- one vertex (Z) with a self-loop edge that consumes any mid and
-- emits one wire event. The composite's single edge from Composite
-- Q Z therefore has output of length 2.
module Keiki.CompositionMultiEventSpec (spec) where

import Data.Proxy (Proxy (..))
import GHC.Generics (Generic)
import Keiki.Composition
  ( ComposeAlignmentWarning (..),
    Composite (..),
    compose,
    composeChecked,
  )
import Keiki.Core
import Keiki.FieldProjSpec qualified as FieldProj
import Keiki.Generics
  ( mkInCtorRecordVia,
    mkInCtorVia,
    mkWireCtor0Via,
    mkWireCtorRecordVia,
  )
import Test.Hspec

-- * t1 ---------------------------------------------------------------------

-- | t1's input alphabet: a single trigger constructor carrying an Int payload.
data T1Cmd = T1Trigger Int deriving (Eq, Show)

inCtorT1Trigger :: InCtor T1Cmd '[ '("payload", Int)]
inCtorT1Trigger =
  unavailableInCtor
    "T1Trigger"
    (\case T1Trigger n -> Just (RCons (Proxy @"payload") n RNil))
    (\(RCons _ n RNil) -> T1Trigger n)

-- | t1's mid (output) alphabet: two constructors A and B.
data Mid = MidA {a :: Int} | MidB {b :: Int}
  deriving stock (Eq, Show, Generic)

inCtorMidA :: InCtor Mid '[ '("a", Int)]
inCtorMidA = mkInCtorRecordVia @"MidA"

inCtorMidB :: InCtor Mid '[ '("b", Int)]
inCtorMidB = mkInCtorRecordVia @"MidB"

wcMidA :: WireCtor Mid (Int, ())
wcMidA = mkWireCtorRecordVia @"MidA"

wcMidB :: WireCtor Mid (Int, ())
wcMidB = mkWireCtorRecordVia @"MidB"

-- | t1's transducer: a single vertex Q with a self-loop edge that
-- emits two mid-symbols ([MidA n, MidB n]) from one T1Trigger input.
data Q = Q deriving (Eq, Ord, Show, Bounded, Enum)

t1 :: SymTransducer (HsPred '[] T1Cmd) '[] Q T1Cmd Mid
t1 =
  SymTransducer
    { edgesOut = \Q ->
        [ Edge
            { guard = matchInCtor inCtorT1Trigger,
              update = UKeep,
              output =
                [ pack
                    inCtorT1Trigger
                    wcMidA
                    ( OFCons
                        ( TInpCtorField
                            inCtorT1Trigger
                            (#payload :: Index '[ '("payload", Int)] Int)
                        )
                        OFNil
                    ),
                  pack
                    inCtorT1Trigger
                    wcMidB
                    ( OFCons
                        ( TInpCtorField
                            inCtorT1Trigger
                            (#payload :: Index '[ '("payload", Int)] Int)
                        )
                        OFNil
                    )
                ],
              target = Q,
              mode = Live
            }
        ],
      initial = Q,
      initialRegs = RNil,
      isFinal = const True
    }

-- * t2 ---------------------------------------------------------------------

-- | t2's output alphabet: one constructor.
data Echo = EchoA Int | EchoB Int deriving (Eq, Show)

wcEchoA :: WireCtor Echo (Int, ())
wcEchoA =
  unavailableWireCtor
    "EchoA"
    ( \case
        EchoA n -> Just (n, ())
        _ -> Nothing
    )
    (\(n, ()) -> EchoA n)

wcEchoB :: WireCtor Echo (Int, ())
wcEchoB =
  unavailableWireCtor
    "EchoB"
    ( \case
        EchoB n -> Just (n, ())
        _ -> Nothing
    )
    (\(n, ()) -> EchoB n)

-- | t2's vertex (single).
data Z = Z deriving (Eq, Ord, Show, Bounded, Enum)

-- | t2's transducer: two edges from Z, one per mid-symbol.
--   Z on MidA → Z / [EchoA payload]
--   Z on MidB → Z / [EchoB payload]
t2 :: SymTransducer (HsPred '[] Mid) '[] Z Mid Echo
t2 =
  SymTransducer
    { edgesOut = \Z ->
        [ Edge
            { guard = matchInCtor inCtorMidA,
              update = UKeep,
              output =
                [ pack
                    inCtorMidA
                    wcEchoA
                    ( OFCons
                        ( TInpCtorField
                            inCtorMidA
                            (#a :: Index '[ '("a", Int)] Int)
                        )
                        OFNil
                    )
                ],
              target = Z,
              mode = Live
            },
          Edge
            { guard = matchInCtor inCtorMidB,
              update = UKeep,
              output =
                [ pack
                    inCtorMidB
                    wcEchoB
                    ( OFCons
                        ( TInpCtorField
                            inCtorMidB
                            (#b :: Index '[ '("b", Int)] Int)
                        )
                        OFNil
                    )
                ],
              target = Z,
              mode = Live
            }
        ],
      initial = Z,
      initialRegs = RNil,
      isFinal = const True
    }

data PendingSourceCmd = PendingSourceCmd FieldProj.DocInfo
  deriving stock (Eq, Show)

pendingSourceCtor :: InCtor PendingSourceCmd '[ '("doc", FieldProj.DocInfo)]
pendingSourceCtor =
  unavailableInCtor
    "PendingSourceCmd"
    (\(PendingSourceCmd doc) -> Just (RCons (Proxy @"doc") doc RNil))
    (\(RCons _ doc RNil) -> PendingSourceCmd doc)

data PendingMid
  = PendingLoad {doc :: FieldProj.DocInfo}
  | PendingCheck
  deriving stock (Eq, Show, Generic)

pendingLoadCtor :: InCtor PendingMid '[ '("doc", FieldProj.DocInfo)]
pendingLoadCtor = mkInCtorRecordVia @"PendingLoad"

pendingCheckCtor :: InCtor PendingMid '[]
pendingCheckCtor = mkInCtorVia @"PendingCheck"

pendingLoadWire :: WireCtor PendingMid (FieldProj.DocInfo, ())
pendingLoadWire = mkWireCtorRecordVia @"PendingLoad"

pendingCheckWire :: WireCtor PendingMid ()
pendingCheckWire = mkWireCtor0Via @"PendingCheck"

pendingSource ::
  SymTransducer (HsPred '[] PendingSourceCmd) '[] Q PendingSourceCmd PendingMid
pendingSource =
  SymTransducer
    { edgesOut = \Q ->
        [ Edge
            { guard = matchInCtor pendingSourceCtor,
              update = UKeep,
              output =
                [ pack
                    pendingSourceCtor
                    pendingLoadWire
                    (OFCons (TInpCtorField pendingSourceCtor #doc) OFNil),
                  pack pendingSourceCtor pendingCheckWire OFNil
                ],
              target = Q,
              mode = Live
            }
        ],
      initial = Q,
      initialRegs = RNil,
      isFinal = const True
    }

pendingSink ::
  SymTransducer
    (HsPred FieldProj.DocRegs PendingMid)
    FieldProj.DocRegs
    Z
    PendingMid
    ()
pendingSink =
  SymTransducer
    { edgesOut = \Z ->
        [ Edge
            { guard = matchInCtor pendingLoadCtor,
              update =
                USet
                  FieldProj.docN
                  (TApp1 id (TInpCtorField pendingLoadCtor #doc)),
              output = [],
              target = Z,
              mode = Live
            },
          Edge
            { guard =
                PAnd
                  (matchInCtor pendingCheckCtor)
                  ( regProj FieldProj.docHashW FieldProj.docIx
                      .== TLit "pending-match"
                  ),
              update = UKeep,
              output = [],
              target = Z,
              mode = Live
            }
        ],
      initial = Z,
      initialRegs =
        RCons (Proxy @"doc") FieldProj.initialDocInfo RNil,
      isFinal = const True
    }

-- * Specs ------------------------------------------------------------------

spec :: Spec
spec = do
  describe "compose t1 t2 with t1 having one length-2 edge" $ do
    it "every composite edge from (Q, Z) has a length-2 output list" $ do
      -- The chain expansion produces one composite edge per t2-edge
      -- choice per mid-symbol — 2 mid-symbols × 2 t2-edges = 4
      -- composite edges. Three of the four have unsatisfiable
      -- substituted guards (`substPred (PInCtor MidA) MidB ≡ PBot`),
      -- but they're structurally present. All four have a length-2
      -- output list — the chain expansion concatenates per-step
      -- substituted outputs.
      let pipeline = compose t1 t2
          edges = edgesOut pipeline (initial pipeline)
      length edges `shouldBe` 4
      mapM_ (\e -> length (output e) `shouldBe` 2) edges

    it "omega on T1Trigger 42 yields [EchoA 42, EchoB 42]" $ do
      let pipeline = compose t1 t2
      omega pipeline (initial pipeline) (initialRegs pipeline) (T1Trigger 42)
        `shouldBe` [EchoA 42, EchoB 42]

    it "applyEvents round-trips the 2-event chunk to the initial composite state" $ do
      let pipeline = compose t1 t2
          chunk = [EchoA 7, EchoB 7]
      case applyEvents pipeline (initial pipeline, initialRegs pipeline) chunk of
        Just (Composite Q Z, _) -> pure ()
        other ->
          expectationFailure
            ( "expected Just (Composite Q Z, _), got "
                <> show (fmap (\(s, _) -> s) other)
            )

  describe "projection through a multi-event pending write" $ do
    let matchingDoc = FieldProj.DocInfo "pending-match" "title" []

    it "raw composition preserves forward behavior and makes the loss auditable" $ do
      let pipeline = compose pendingSource pendingSink
      opaqueGuardWarnings pipeline `shouldSatisfy` (not . null)
      case stepEither
        pipeline
        (initial pipeline, initialRegs pipeline)
        (PendingSourceCmd matchingDoc) of
        Left failure -> expectationFailure ("pending-write pipeline failed: " <> show failure)
        Right _ -> pure ()

    it "composeChecked rejects the computed pending-write projection" $
      case composeChecked pendingSource pendingSink of
        Right _ -> expectationFailure "computed pending write passed composeChecked"
        Left warnings ->
          warnings
            `shouldSatisfy` any
              ( \case
                  NonStructuralProjectionBoundary
                    { cawProjectionReason = "pending write"
                    } -> True
                  _ -> False
              )