packages feed

keiki-0.9.0.0: test/Keiki/CompositionAlignmentSpec.hs

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

module Keiki.CompositionAlignmentSpec (spec) where

import Data.Proxy (Proxy (..))
import Data.Text qualified as T
import GHC.Generics (Generic)
import Keiki.Composition
import Keiki.Core
import Keiki.FieldProjSpec qualified as FieldProj
import Keiki.Fixtures.ComposeStateful
import Keiki.Fixtures.CounterPipeline
import Keiki.Generics (mkInCtorRecordVia, mkWireCtorRecordVia)
import Keiki.Profunctor (rmapCo)
import Keiki.Render.Pretty (prettyPred, prettyTerm)
import Test.Hspec

type Payload1 = '[ '("payload", Int)]

type Payload2 = '[ '("first", Int), '("second", Int)]

typoInMsgB :: InCtor MsgB Payload1
typoInMsgB =
  unavailableInCtor
    "MsgTypo"
    (\(MsgB n) -> Just (RCons (Proxy @"payload") n RNil))
    (\(RCons _ n RNil) -> MsgB n)

twoFieldInMsgB :: InCtor MsgB Payload2
twoFieldInMsgB =
  unavailableInCtor
    "MsgB"
    ( \(MsgB n) ->
        Just
          ( RCons
              (Proxy @"first")
              n
              (RCons (Proxy @"second") n RNil)
          )
    )
    (\(RCons _ n (RCons _ _ RNil)) -> MsgB n)

misnamedStageB :: SymTransducer (HsPred BRegs MsgB) BRegs StageVertex MsgB MsgC
misnamedStageB =
  SymTransducer
    { edgesOut = \StageVertex ->
        [ Edge
            { guard = PInCtor typoInMsgB,
              update = UKeep,
              output = [],
              target = StageVertex,
              mode = Live
            }
        ],
      initial = StageVertex,
      initialRegs = RCons (Proxy @"regB") 0 RNil,
      isFinal = const True
    }

arityStageB :: SymTransducer (HsPred BRegs MsgB) BRegs StageVertex MsgB MsgC
arityStageB =
  SymTransducer
    { edgesOut = \StageVertex ->
        [ Edge
            { guard =
                PAnd
                  (PInCtor twoFieldInMsgB)
                  ( PEq
                      (TInpCtorField twoFieldInMsgB (SIdx ZIdx))
                      (TLit (0 :: Int))
                  ),
              update = UKeep,
              output = [],
              target = StageVertex,
              mode = Live
            }
        ],
      initial = StageVertex,
      initialRegs = RCons (Proxy @"regB") 0 RNil,
      isFinal = const True
    }

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

type ProjectionSourceFields = '[ '("doc", FieldProj.DocInfo)]

projectionSourceCtor :: InCtor ProjectionSourceCmd ProjectionSourceFields
projectionSourceCtor =
  unavailableInCtor
    "ProjectionSourceCmd"
    ( \(ProjectionSourceCmd doc) ->
        Just (RCons (Proxy @"doc") doc RNil)
    )
    (\(RCons _ doc RNil) -> ProjectionSourceCmd doc)

data ProjectionMid = ProjectionMid {doc :: FieldProj.DocInfo}
  deriving stock (Eq, Show, Generic)

projectionMidCtor :: InCtor ProjectionMid '[ '("doc", FieldProj.DocInfo)]
projectionMidCtor = mkInCtorRecordVia @"ProjectionMid"

projectionMidWire :: WireCtor ProjectionMid (FieldProj.DocInfo, ())
projectionMidWire = mkWireCtorRecordVia @"ProjectionMid"

data ProjectionVertex = ProjectionVertex
  deriving stock (Eq, Ord, Show, Enum, Bounded)

projectionSource ::
  Term '[] ProjectionSourceCmd ProjectionSourceFields FieldProj.DocInfo ->
  SymTransducer
    (HsPred '[] ProjectionSourceCmd)
    '[]
    ProjectionVertex
    ProjectionSourceCmd
    ProjectionMid
projectionSource ownerTerm =
  SymTransducer
    { edgesOut = \ProjectionVertex ->
        [ Edge
            { guard = matchInCtor projectionSourceCtor,
              update = UKeep,
              output =
                [ pack
                    projectionSourceCtor
                    projectionMidWire
                    (OFCons ownerTerm OFNil)
                ],
              target = ProjectionVertex,
              mode = Live
            }
        ],
      initial = ProjectionVertex,
      initialRegs = RNil,
      isFinal = const True
    }

projectionSink ::
  SymTransducer
    (HsPred '[] ProjectionMid)
    '[]
    ProjectionVertex
    ProjectionMid
    ()
projectionSink =
  SymTransducer
    { edgesOut = \ProjectionVertex ->
        [ Edge
            { guard =
                PAnd
                  (matchInCtor projectionMidCtor)
                  ( inpProj FieldProj.docHashW projectionMidCtor #doc
                      .== TLit "match"
                  ),
              update = UKeep,
              output = [],
              target = ProjectionVertex,
              mode = Live
            }
        ],
      initial = ProjectionVertex,
      initialRegs = RNil,
      isFinal = const True
    }

data CollisionMid
  = CollisionWire {wireValue :: Int}
  | CollisionInput {inputValue :: Int}
  deriving stock (Eq, Show, Generic)

collisionWire :: WireCtor CollisionMid (Int, ())
collisionWire =
  renameWireCtor "Collision" (mkWireCtorRecordVia @"CollisionWire")

collisionInput :: InCtor CollisionMid '[ '("inputValue", Int)]
collisionInput =
  renameInCtor "Collision" (mkInCtorRecordVia @"CollisionInput")

collisionSource ::
  SymTransducer
    (HsPred '[] ProjectionSourceCmd)
    '[]
    ProjectionVertex
    ProjectionSourceCmd
    CollisionMid
collisionSource =
  SymTransducer
    { edgesOut = \ProjectionVertex ->
        [ Edge
            { guard = matchInCtor projectionSourceCtor,
              update = UKeep,
              output =
                [ pack
                    projectionSourceCtor
                    collisionWire
                    (OFCons (TLit (1 :: Int)) OFNil)
                ],
              target = ProjectionVertex,
              mode = Live
            }
        ],
      initial = ProjectionVertex,
      initialRegs = RNil,
      isFinal = const True
    }

collisionSink ::
  SymTransducer
    (HsPred '[] CollisionMid)
    '[]
    ProjectionVertex
    CollisionMid
    ()
collisionSink =
  SymTransducer
    { edgesOut = \ProjectionVertex ->
        [ Edge
            { guard =
                PAnd
                  (matchInCtor collisionInput)
                  (PEq (TInpCtorField collisionInput #inputValue) (TLit (1 :: Int))),
              update = UKeep,
              output = [],
              target = ProjectionVertex,
              mode = Live
            }
        ],
      initial = ProjectionVertex,
      initialRegs = RNil,
      isFinal = const True
    }

manualBoundaryWire :: WireCtor CollisionMid (Int, ())
manualBoundaryWire =
  unavailableWireCtor
    "ManualBoundary"
    (\case CollisionWire value -> Just (value, ()); CollisionInput {} -> Nothing)
    (\(value, ()) -> CollisionWire value)

manualBoundaryInput :: InCtor CollisionMid '[ '("payload", Int)]
manualBoundaryInput =
  unavailableInCtor
    "ManualBoundary"
    (\case CollisionWire value -> Just (RCons (Proxy @"payload") value RNil); CollisionInput {} -> Nothing)
    (\(RCons _ value RNil) -> CollisionWire value)

manualBoundarySource ::
  SymTransducer
    (HsPred '[] ProjectionSourceCmd)
    '[]
    ProjectionVertex
    ProjectionSourceCmd
    CollisionMid
manualBoundarySource =
  SymTransducer
    { edgesOut = \ProjectionVertex ->
        [ Edge
            { guard = matchInCtor projectionSourceCtor,
              update = UKeep,
              output =
                [ pack
                    projectionSourceCtor
                    manualBoundaryWire
                    (OFCons (TLit (1 :: Int)) OFNil)
                ],
              target = ProjectionVertex,
              mode = Live
            }
        ],
      initial = ProjectionVertex,
      initialRegs = RNil,
      isFinal = const True
    }

manualBoundarySink ::
  SymTransducer
    (HsPred '[] CollisionMid)
    '[]
    ProjectionVertex
    CollisionMid
    ()
manualBoundarySink =
  SymTransducer
    { edgesOut = \ProjectionVertex ->
        [ Edge
            { guard =
                PAnd
                  (matchInCtor manualBoundaryInput)
                  (PEq (TInpCtorField manualBoundaryInput #payload) (TLit (1 :: Int))),
              update = UKeep,
              output = [],
              target = ProjectionVertex,
              mode = Live
            }
        ],
      initial = ProjectionVertex,
      initialRegs = RNil,
      isFinal = const True
    }

containsPBot :: HsPred rs ci -> Bool
containsPBot PBot = True
containsPBot (PAnd left right) = containsPBot left || containsPBot right
containsPBot (POr left right) = containsPBot left || containsPBot right
containsPBot (PNot predicate) = containsPBot predicate
containsPBot _ = False

spec :: Spec
spec = do
  describe "checkComposeAlignment" $ do
    it "accepts aligned fixture pairs and composeChecked builds them" $ do
      checkComposeAlignment stageA stageB `shouldBe` []
      checkComposeAlignment stageB stageC `shouldBe` []
      checkComposeAlignment counterSource lastValueSink `shouldBe` []
      checkComposeAlignment pairSource twoPhaseSink `shouldBe` []
      case composeChecked stageA stageB of
        Right _ -> pure ()
        Left warnings -> expectationFailure ("aligned pair warned: " <> show warnings)

    it "reports both sides of a constructor-name drift with exact edges" $ do
      checkComposeAlignment stageA misnamedStageB
        `shouldBe` [ UnconsumedWireOutput
                       (EdgeRef StageVertex 0)
                       "MsgB"
                       StageVertex,
                     UnmatchedInCtorExpectation
                       (EdgeRef StageVertex 0)
                       "MsgTypo"
                       StageVertex
                   ]
      case composeChecked stageA misnamedStageB of
        Left _ -> pure ()
        Right _ -> expectationFailure "misnamed pair passed composeChecked"

    it "reports an out-of-range field read before evaluation" $
      checkComposeAlignment stageA arityStageB
        `shouldContain` [ FieldArityMismatch
                            (EdgeRef StageVertex 0)
                            (EdgeRef StageVertex 0)
                            "MsgB"
                            1
                            1
                        ]

    it "flags stamped mapped names explicitly" $ do
      let warnings = checkComposeAlignment (rmapCo id stageA) stageB
      warnings
        `shouldSatisfy` any (\case PoisonedNameInComposition "MsgB#rmapped" "upstream output" -> True; _ -> False)

    it "walks every symbol in a multi-event source chain" $
      checkComposeAlignment pairSource twoPhaseSink `shouldBe` []

    it "rejects a same-name structurally different input/wire boundary" $ do
      checkComposeAlignment collisionSource collisionSink
        `shouldBe` [ StructurallyDifferentInputWire
                       (EdgeRef ProjectionVertex 0)
                       (EdgeRef ProjectionVertex 0)
                       "Collision"
                       "Collision"
                   ]
      case composeChecked collisionSource collisionSink of
        Left warnings ->
          warnings
            `shouldContain` [ StructurallyDifferentInputWire
                                (EdgeRef ProjectionVertex 0)
                                (EdgeRef ProjectionVertex 0)
                                "Collision"
                                "Collision"
                            ]
        Right _ -> expectationFailure "same-name structural collision passed composeChecked"

    it "rejects an unwitnessed manual input/wire boundary and poisons raw composition" $ do
      let warning =
            UnwitnessedInputWireAlignment
              (EdgeRef ProjectionVertex 0)
              (EdgeRef ProjectionVertex 0)
              "ManualBoundary"
              "ManualBoundary"
      checkComposeAlignment manualBoundarySource manualBoundarySink
        `shouldBe` [warning]
      case edgesOut
        (compose manualBoundarySource manualBoundarySink)
        (Composite ProjectionVertex ProjectionVertex) of
        [edge] -> containsPBot (guard edge) `shouldBe` True
        edges -> expectationFailure ("manual raw composition produced " <> show (length edges) <> " edges")
      case composeChecked manualBoundarySource manualBoundarySink of
        Left warnings -> warnings `shouldBe` [warning]
        Right _ -> expectationFailure "unwitnessed manual boundary passed composeChecked"

  describe "typed field projection composition" $ do
    let matchingDoc = FieldProj.DocInfo "match" "title" []
        inputTerm = TInpCtorField projectionSourceCtor #doc
        passThrough = projectionSource inputTerm
        literalOwner = projectionSource (TLit matchingDoc)
        opaqueOwner = projectionSource (opaqueLit matchingDoc)
        computedOwner = projectionSource (TApp1 id inputTerm)

    it "preserves a stable input-field owner through checked composition" $ do
      checkComposeAlignment passThrough projectionSink `shouldBe` []
      case composeChecked passThrough projectionSink of
        Left warnings -> expectationFailure ("stable projection warned: " <> show warnings)
        Right pipeline -> opaqueGuardWarnings pipeline `shouldBe` []

    it "constant-folds a literal owner with display opacity but exact semantics" $ do
      case composeChecked literalOwner projectionSink of
        Left warnings -> expectationFailure ("literal fold failed checked composition: " <> show warnings)
        Right pipeline -> do
          opaqueGuardWarnings pipeline `shouldBe` []
          case edgesOut pipeline (initial pipeline) of
            [edge] ->
              prettyPred (guard edge)
                `shouldSatisfy` T.isInfixOf (T.pack "<lit>")
            _ -> expectationFailure "literal-folded pipeline no longer has one edge"
          case stepEither
            pipeline
            (initial pipeline, initialRegs pipeline)
            (ProjectionSourceCmd (FieldProj.DocInfo "ignored" "" [])) of
            Left failure -> expectationFailure ("literal-folded pipeline failed: " <> show failure)
            Right _ -> pure ()

    it "preserves literal display evidence through positional substitution" $ do
      let midRead = TInpCtorField projectionMidCtor #doc
      case edgesOut literalOwner (initial literalOwner) of
        [Edge {output = [out]}] -> do
          let substituted =
                substTerm @'[] @'[] midRead out ::
                  Term '[] ProjectionSourceCmd ProjectionSourceFields FieldProj.DocInfo
          prettyTerm substituted `shouldBe` T.pack (show matchingDoc)
        _ -> expectationFailure "readable literal source no longer has one output"
      case edgesOut opaqueOwner (initial opaqueOwner) of
        [Edge {output = [out]}] -> do
          let substituted =
                substTerm @'[] @'[] midRead out ::
                  Term '[] ProjectionSourceCmd ProjectionSourceFields FieldProj.DocInfo
          prettyTerm substituted `shouldBe` T.pack "<lit>"
        _ -> expectationFailure "opaque literal source no longer has one output"

    it "keeps raw composition forward-correct but rejects a computed owner at the checked boundary" $ do
      let pipeline = compose computedOwner projectionSink
      opaqueGuardWarnings pipeline `shouldSatisfy` (not . null)
      case stepEither
        pipeline
        (initial pipeline, initialRegs pipeline)
        (ProjectionSourceCmd matchingDoc) of
        Left failure -> expectationFailure ("raw projected pipeline failed: " <> show failure)
        Right _ -> pure ()
      case composeChecked computedOwner projectionSink of
        Right _ -> expectationFailure "computed owner passed composeChecked"
        Left warnings ->
          warnings
            `shouldSatisfy` any
              ( \case
                  NonStructuralProjectionBoundary
                    { cawProjectionReason = "upstream computed output"
                    } -> True
                  _ -> False
              )