keiki-0.7.0.0: test/Keiki/StepEitherSpec.hs
module Keiki.StepEitherSpec (spec) where
import Keiki.Core
import Test.Hspec
-- Vertices: 0 has two always-true edges (ambiguous); 1 has one
-- always-false edge (no match); 2 has no edges; 3 has one always-true
-- edge (the normal accepting case).
data V = V0 | V1 | V2 | V3 | VReplay | VEnd
deriving stock (Eq, Show)
-- A no-op output term is awkward to build generically; instead each
-- edge below uses an empty output list ([]), so a successful step emits
-- no events. That keeps the fixture free of WireCtor/InCtor plumbing
-- while still exercising the Right path.
fixture :: SymTransducer (HsPred '[] Bool) '[] V Bool String
fixture =
SymTransducer
{ edgesOut = \case
V0 ->
[ Edge {guard = PTop, update = UKeep, output = [], target = VEnd, mode = Live},
Edge {guard = PTop, update = UKeep, output = [], target = V3, mode = Live}
]
V1 -> [Edge {guard = PBot, update = UKeep, output = [], target = VEnd, mode = Live}]
V2 -> []
V3 -> [Edge {guard = PTop, update = UKeep, output = [], target = VEnd, mode = Live}]
VReplay -> [Edge {guard = PTop, update = UKeep, output = [], target = VEnd, mode = ReplayOnly}]
VEnd -> [],
initial = V0,
initialRegs = RNil,
isFinal = (== VEnd)
}
data IdentityCommand = ChooseFirst | ChooseSecond
deriving stock (Eq, Show)
data IdentityEvent = Chosen
deriving stock (Eq, Show)
firstCtor :: InCtor IdentityCommand '[]
firstCtor =
InCtor
{ icName = "ChooseFirst",
icMatch = \case ChooseFirst -> Just RNil; _ -> Nothing,
icBuild = \RNil -> ChooseFirst
}
secondCtor :: InCtor IdentityCommand '[]
secondCtor =
InCtor
{ icName = "ChooseSecond",
icMatch = \case ChooseSecond -> Just RNil; _ -> Nothing,
icBuild = \RNil -> ChooseSecond
}
chosenWire :: WireCtor IdentityEvent ()
chosenWire =
WireCtor
{ wcName = "Chosen",
wcMatch = \case Chosen -> Just (),
wcBuild = \() -> Chosen
}
-- The two live siblings are behaviorally indistinguishable after erasure:
-- they preserve the same registers, reach the same target, and emit equal
-- values. Only their local edge references distinguish them.
identityFixture :: SymTransducer (HsPred '[] IdentityCommand) '[] V IdentityCommand IdentityEvent
identityFixture =
SymTransducer
{ edgesOut = \case
V0 ->
[ Edge
{ guard = matchInCtor firstCtor,
update = UKeep,
output = [pack firstCtor chosenWire oNil],
target = VEnd,
mode = Live
},
Edge
{ guard = matchInCtor secondCtor,
update = UKeep,
output = [pack secondCtor chosenWire oNil],
target = VEnd,
mode = Live
}
]
_ -> [],
initial = V0,
initialRegs = RNil,
isFinal = (== VEnd)
}
-- NOTE: 'RegFile' has no 'Eq'/'Show' instance (verified 2026-06-06), so we
-- cannot 'shouldBe' a whole 'Either (StepFailure V) (V, RegFile '[], [String])'.
-- The failure ('Left') values carry no register data and ARE 'Eq'/'Show', so
-- we pattern-match the result and compare only the inspectable parts. The
-- register file for the empty slot list @'[]@ has exactly one inhabitant
-- ('RNil'), so register equality on the success path is trivially preserved.
spec :: Spec
spec = do
describe "stepEither" $ do
it "reports NoOutgoingEdges for a vertex with no edges" $
case stepEither fixture (V2, RNil) True of
Left f -> f `shouldBe` NoOutgoingEdges V2
Right _ -> expectationFailure "expected Left NoOutgoingEdges"
it "reports NoMatchingEdge with one rejected summary per edge" $
case stepEither fixture (V1, RNil) True of
Left f ->
f
`shouldBe` NoMatchingEdge
V1
[ RejectedEdgeSummary
{ rejectedEdge = EdgeRef {edgeSource = V1, edgeIndex = 0},
rejectedTarget = VEnd,
rejectedGuard = False
}
]
Right _ -> expectationFailure "expected Left NoMatchingEdge"
it "reports AmbiguousEdges listing every matched edge" $
case stepEither fixture (V0, RNil) True of
Left f ->
f
`shouldBe` AmbiguousEdges
V0
[ MatchedEdgeSummary
{ matchedEdge = EdgeRef {edgeSource = V0, edgeIndex = 0},
matchedTarget = VEnd
},
MatchedEdgeSummary
{ matchedEdge = EdgeRef {edgeSource = V0, edgeIndex = 1},
matchedTarget = V3
}
]
Right _ -> expectationFailure "expected Left AmbiguousEdges"
it "returns Right with the same target/regs/events as a normal edge" $
case stepEither fixture (V3, RNil) True of
Right (tgt, _regs, evs) -> (tgt, evs) `shouldBe` (VEnd, [])
Left _ -> expectationFailure "expected Right"
it "Right payload matches step exactly on the accepting edge" $
case (step fixture (V3, RNil) True, stepEither fixture (V3, RNil) True) of
(Just (s1, _r1, e1), Right (s2, _r2, e2)) -> (s1, e1) `shouldBe` (s2, e2)
(Nothing, _) -> expectationFailure "step returned Nothing on the accepting edge"
(_, Left f) -> expectationFailure ("stepEither returned Left: " <> show f)
describe "stepDetailedEither" $ do
it "distinguishes behaviorally identical guarded siblings by local EdgeRef" $
case ( stepDetailedEither identityFixture (V0, RNil) ChooseFirst,
stepDetailedEither identityFixture (V0, RNil) ChooseSecond,
stepEither identityFixture (V0, RNil) ChooseFirst,
stepEither identityFixture (V0, RNil) ChooseSecond
) of
(Right first, Right second, Right (firstState, _, firstOut), Right (secondState, _, secondOut)) -> do
stepSuccessEdge first `shouldBe` EdgeRef V0 0
stepSuccessEdge second `shouldBe` EdgeRef V0 1
stepSuccessMode first `shouldBe` Live
stepSuccessMode second `shouldBe` Live
(stepSuccessState first, stepSuccessOutputs first)
`shouldBe` (firstState, firstOut)
(stepSuccessState second, stepSuccessOutputs second)
`shouldBe` (secondState, secondOut)
(firstState, firstOut) `shouldBe` (secondState, secondOut)
_ -> expectationFailure "expected four successful stepping results"
it "attributes an accepted epsilon-output edge" $
case stepDetailedEither fixture (V3, RNil) True of
Right success -> do
stepSuccessEdge success `shouldBe` EdgeRef V3 0
stepSuccessMode success `shouldBe` Live
(stepSuccessState success, stepSuccessOutputs success) `shouldBe` (VEnd, [])
Left failure -> expectationFailure ("expected Right, got " <> show failure)
it "never selects a replay-only edge during forward stepping" $ do
let expected =
NoMatchingEdge
VReplay
[ RejectedEdgeSummary
{ rejectedEdge = EdgeRef VReplay 0,
rejectedTarget = VEnd,
rejectedGuard = False
}
]
case (stepDetailedEither fixture (VReplay, RNil) True, stepEither fixture (VReplay, RNil) True) of
(Left detailedFailure, Left compatibilityFailure) -> do
detailedFailure `shouldBe` expected
compatibilityFailure `shouldBe` expected
_ -> expectationFailure "expected replay-only edge rejection"
it "returns exactly the compatibility failures" $ do
let cases = [(V0, True), (V1, True), (V2, True)]
mapM_
( \(source, command) ->
case ( stepDetailedEither fixture (source, RNil) command,
stepEither fixture (source, RNil) command
) of
(Left detailedFailure, Left compatibilityFailure) ->
detailedFailure `shouldBe` compatibilityFailure
_ -> expectationFailure "expected paired failures"
)
cases