keiki-0.2.0.0: test/Keiki/CoreSpec.hs
module Keiki.CoreSpec (spec) where
import Control.Exception (evaluate)
import Data.Proxy (Proxy (..))
import Keiki.Core
import Test.Hspec
-- | A two-constructor input symbol used by the 'TInpCtorField' tests.
data TinyCmd = TinyFoo Int Int | TinyBar Int deriving (Eq, Show)
type SnapshotRegs =
'[ '("x", Int),
'("y", Int)
]
inCtorTinyFoo ::
InCtor
TinyCmd
'[ '("a", Int), '("b", Int)]
inCtorTinyFoo =
InCtor
{ icName = "TinyFoo",
icMatch = \case
TinyFoo a b ->
Just
( RCons (Proxy @"a") a $
RCons (Proxy @"b") b $
RNil
)
_ -> Nothing,
icBuild = \(RCons _ a (RCons _ b RNil)) -> TinyFoo a b
}
-- The synthetic transducer's input-side singleton: matches 'True' only,
-- with an empty payload. 'icName' aligns with the wire-side 'wcName'
-- so 'solveOutput' on the OPack walks an empty 'OutFields' against an
-- empty slot list and recovers 'True'.
inCtorTrue :: InCtor Bool '[]
inCtorTrue =
InCtor
{ icName = "True",
icMatch = \case
True -> Just RNil
False -> Nothing,
icBuild = \RNil -> True
}
-- The synthetic transducer's wire-side singleton: a one-constructor
-- 'WireCtor' over 'String' carrying no fields, recognising the literal
-- "true". Paired with 'inCtorTrue' under 'OPack' to give the synthetic
-- edge a structural output term (no opaque 'mkOut').
wcStringTrue :: WireCtor String ()
wcStringTrue =
WireCtor
{ wcName = "True",
wcMatch = \s -> if s == "true" then Just () else Nothing,
wcBuild = \() -> "true"
}
-- A minimal 2-vertex transducer over 'Bool' input, 'String' output, no
-- registers. Edges:
--
-- False --[guard ci=True / output \"true\"]--> True
--
-- This is the smallest example that exercises 'delta', 'omega', and
-- 'evalOut' on a real edge while keeping the type machinery simple.
synthetic :: SymTransducer (HsPred '[] Bool) '[] Bool Bool String
synthetic =
SymTransducer
{ edgesOut = \case
False ->
[ Edge
{ guard = matchInCtor inCtorTrue,
update = UKeep,
output = [pack inCtorTrue wcStringTrue OFNil],
target = True
}
]
True -> [],
initial = False,
initialRegs = RNil,
isFinal = id
}
spec :: Spec
spec = do
describe "evalTerm" $ do
it "evaluates TLit" $
evalTerm (TLit (42 :: Int)) RNil () `shouldBe` 42
it "evaluates TApp1" $
evalTerm (TApp1 (+ 1) (TLit (5 :: Int)) :: Term '[] () '[] Int) RNil () `shouldBe` 6
it "evaluates TApp2" $
evalTerm
(TApp2 (+) (TLit (5 :: Int)) (TLit 10) :: Term '[] () '[] Int)
RNil
()
`shouldBe` 15
describe "TInpCtorField (structural input projection)" $ do
it "evaluates field #a on the matching constructor" $
evalTerm
( TInpCtorField inCtorTinyFoo (#a :: Index '[ '("a", Int), '("b", Int)] Int) ::
Term '[] TinyCmd '[ '("a", Int), '("b", Int)] Int
)
RNil
(TinyFoo 7 9)
`shouldBe` 7
it "evaluates field #b on the matching constructor" $
evalTerm
( TInpCtorField inCtorTinyFoo (#b :: Index '[ '("a", Int), '("b", Int)] Int) ::
Term '[] TinyCmd '[ '("a", Int), '("b", Int)] Int
)
RNil
(TinyFoo 7 9)
`shouldBe` 9
it "errors with the icName when the input is the wrong constructor" $
evaluate
( evalTerm
( TInpCtorField inCtorTinyFoo (#a :: Index '[ '("a", Int), '("b", Int)] Int) ::
Term '[] TinyCmd '[ '("a", Int), '("b", Int)] Int
)
RNil
(TinyBar 0)
)
`shouldThrow` errorCall "evalTerm: TInpCtorField guard violation: TinyFoo"
it "termReadsInput is True for a TInpCtorField term" $
termReadsInput
( TInpCtorField inCtorTinyFoo (#a :: Index '[ '("a", Int), '("b", Int)] Int) ::
Term '[] TinyCmd '[ '("a", Int), '("b", Int)] Int
)
`shouldBe` True
describe "evalPred" $ do
it "PTop is True; PBot is False" $ do
evalPred (PTop :: HsPred '[] ()) RNil () `shouldBe` True
evalPred (PBot :: HsPred '[] ()) RNil () `shouldBe` False
it "PEq compares equal terms" $
evalPred (TLit (1 :: Int) .== TLit 1 :: HsPred '[] ()) RNil () `shouldBe` True
describe "runUpdate snapshot semantics" $ do
let regs = RCons (Proxy @"x") 0 (RCons (Proxy @"y") 99 RNil)
it "evaluates sibling right-hand sides against the entry snapshot" $ do
let update :: Update SnapshotRegs '["x", "y"] ()
update =
UCombine
(USet (#x :: IndexN "x" SnapshotRegs Int) (lit 1))
( USet
(#y :: IndexN "y" SnapshotRegs Int)
(proj (#x :: Index SnapshotRegs Int))
)
result = runUpdate update regs ()
result ! (#x :: Index SnapshotRegs Int) `shouldBe` 1
result ! (#y :: Index SnapshotRegs Int) `shouldBe` 0
it "preserves self-read increments" $ do
let update :: Update SnapshotRegs '["x"] ()
update =
USet
(#x :: IndexN "x" SnapshotRegs Int)
(proj (#x :: Index SnapshotRegs Int) .+ lit 1)
result = runUpdate update regs ()
result ! (#x :: Index SnapshotRegs Int) `shouldBe` 1
result ! (#y :: Index SnapshotRegs Int) `shouldBe` 99
describe "synthetic 2-vertex transducer" $ do
it "delta moves False -> True on input True (state)" $
fmap fst (delta synthetic False RNil True) `shouldBe` Just True
it "omega emits \"true\" on the matching edge" $
omega synthetic False RNil True `shouldBe` ["true"]
it "delta returns Nothing when the guard is unsatisfied" $
fmap fst (delta synthetic False RNil False) `shouldBe` Nothing
it "delta returns Nothing in the True (sink) vertex" $
fmap fst (delta synthetic True RNil True) `shouldBe` Nothing
describe "step" $ do
it "produces (s', _, Just co) on a matching output edge" $ do
case step synthetic (False, RNil) True of
Just (s', _, [co]) -> (s', co) `shouldBe` (True, "true")
other -> expectationFailure (show3 other)
it "returns Nothing in the sink vertex" $
case step synthetic (True, RNil) True of
Nothing -> pure ()
other -> expectationFailure (show3 other)
describe "reconstitute" $ do
it "returns the initial state for the empty log" $
case reconstitute synthetic ([] :: [String]) of
Just (s, _) -> s `shouldBe` False
Nothing -> expectationFailure "expected Just (initial, _)"
describe "solveOutput structural path (TInpCtorField)" $ do
let -- An output sum mirroring TinyCmd's payload (ci-determined wire).
wireTinyFoo :: WireCtor TinyCmdOut (Int, (Int, ()))
wireTinyFoo =
WireCtor
{ wcName = "TinyFooOut",
wcMatch = \(TinyFooOut a b) -> Just (a, (b, ())),
wcBuild = \(a, (b, ())) -> TinyFooOut a b
}
-- Complete OPack: both fields read from inCtorTinyFoo.
outComplete :: OutTerm '[] TinyCmd TinyCmdOut
outComplete =
OPack
inCtorTinyFoo
wireTinyFoo
( OFCons
( TInpCtorField
inCtorTinyFoo
(#a :: Index '[ '("a", Int), '("b", Int)] Int)
)
( OFCons
( TInpCtorField
inCtorTinyFoo
(#b :: Index '[ '("a", Int), '("b", Int)] Int)
)
OFNil
)
)
-- Incomplete OPack: only #a is in OutFields; #b is a constant.
outIncomplete :: OutTerm '[] TinyCmd TinyCmdOut
outIncomplete =
OPack
inCtorTinyFoo
wireTinyFoo
( OFCons
( TInpCtorField
inCtorTinyFoo
(#a :: Index '[ '("a", Int), '("b", Int)] Int)
)
(OFCons (TLit (0 :: Int)) OFNil)
)
-- EP-53: an InCtor with the SAME field schema as inCtorTinyFoo but
-- a different icName. Because 'OutFields' is now indexed by the
-- input field schema and 'OPack' ties it to the InCtor, a field
-- projection whose *schema* differs from the OPack's InCtor is a
-- compile error (un-representable) — the old runtime collision
-- hazard is gone. The icName is retained only as a runtime
-- diagnostic: a same-schema projection naming a different
-- constructor is a clean replay failure ('Nothing'), never a
-- type-unsound coercion.
inCtorTinyFooOther :: InCtor TinyCmd '[ '("a", Int), '("b", Int)]
inCtorTinyFooOther =
InCtor
{ icName = "OtherName",
icMatch = \case
TinyFoo a b ->
Just
( RCons (Proxy @"a") a $
RCons (Proxy @"b") b $
RNil
)
_ -> Nothing,
icBuild = \(RCons _ a (RCons _ b RNil)) -> TinyFoo a b
}
outNameMismatch :: OutTerm '[] TinyCmd TinyCmdOut
outNameMismatch =
OPack
inCtorTinyFoo
wireTinyFoo
( OFCons
( TInpCtorField
inCtorTinyFooOther
(#a :: Index '[ '("a", Int), '("b", Int)] Int)
)
( OFCons
( TInpCtorField
inCtorTinyFooOther
(#b :: Index '[ '("a", Int), '("b", Int)] Int)
)
OFNil
)
)
it "evalOut produces TinyFooOut on a matching ci" $
evalOut outComplete RNil (TinyFoo 7 11) `shouldBe` TinyFooOut 7 11
it "solveOutput recovers ci structurally (no legacy inverse)" $
solveOutput outComplete RNil (TinyFooOut 7 11) `shouldBe` Just (TinyFoo 7 11)
it "solveOutput returns Nothing on incomplete coverage" $
solveOutput outIncomplete RNil (TinyFooOut 7 0) `shouldBe` Nothing
it "rejects a same-schema TInpCtorField whose icName differs (EP-53 diagnostic)" $
solveOutput outNameMismatch RNil (TinyFooOut 7 11) `shouldBe` Nothing
it "detectMissingInCtorFields names the missing slot" $
let fs =
OFCons
( TInpCtorField
inCtorTinyFoo
(#a :: Index '[ '("a", Int), '("b", Int)] Int)
)
(OFCons (TLit (0 :: Int)) OFNil) ::
OutFields '[] TinyCmd '[ '("a", Int), '("b", Int)] (Int, (Int, ()))
in detectMissingInCtorFields inCtorTinyFoo fs
`shouldBe` Just (MissingInCtorFields "TinyFoo" ["b"])
it "detectMissingInCtorFields is Nothing on complete coverage" $
let fs =
OFCons
( TInpCtorField
inCtorTinyFoo
(#a :: Index '[ '("a", Int), '("b", Int)] Int)
)
( OFCons
( TInpCtorField
inCtorTinyFoo
(#b :: Index '[ '("a", Int), '("b", Int)] Int)
)
OFNil
) ::
OutFields '[] TinyCmd '[ '("a", Int), '("b", Int)] (Int, (Int, ()))
in detectMissingInCtorFields inCtorTinyFoo fs `shouldBe` Nothing
it "outFieldsHaveInpCtorField is True when at least one TInpCtorField appears" $
let fs =
OFCons
( TInpCtorField
inCtorTinyFoo
(#a :: Index '[ '("a", Int), '("b", Int)] Int)
)
OFNil ::
OutFields '[] TinyCmd '[ '("a", Int), '("b", Int)] (Int, ())
in outFieldsHaveInpCtorField fs `shouldBe` True
where
-- 'show' over `Maybe (s, RegFile rs, Maybe co)` is awkward because
-- RegFile has no Show. Use a thin coercion to a printable summary.
show3 :: (Show s) => (Show co) => Maybe (s, x, [co]) -> String
show3 Nothing = "Nothing"
show3 (Just (s, _, cos_)) = "Just (" ++ show s ++ ", _, " ++ show cos_ ++ ")"
-- | Output sum mirroring 'TinyCmd' for the M3 structural-path tests.
data TinyCmdOut = TinyFooOut Int Int deriving (Eq, Show)