packages feed

keiki-0.2.0.0: test/Keiki/BuilderTypeErrorsSpec.hs

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE QualifiedDo #-}
{-# LANGUAGE TemplateHaskell #-}
-- Constructor derivation emits helpers beyond those needed by these
-- compile-time error fixtures.
{-# OPTIONS_GHC -fdefer-type-errors -Wno-deferred-type-errors -Wno-unused-top-binds #-}

module Keiki.BuilderTypeErrorsSpec (spec) where

import Control.Exception (TypeError, evaluate)
import Data.Proxy (Proxy (..))
import GHC.Generics (Generic)
import Keiki.Builder qualified as B
import Keiki.Core (HsPred, OutFields (..), RegFile (..), SymTransducer)
import Keiki.Generics (emptyRegFile)
import Keiki.Generics.TH (deriveAggregateCtors, deriveWireCtors)
import Test.Hspec

type Regs = '[ '("value", Int)]

data Vertex = Start | Done
  deriving (Eq, Show)

data OneData = OneData {value :: Int}
  deriving (Eq, Show, Generic)

data TwoData = TwoData {other :: Int}
  deriving (Eq, Show, Generic)

data Cmd = One OneData | Two TwoData
  deriving (Eq, Show, Generic)

data EventData = EventData {value :: Int}
  deriving (Eq, Show, Generic)

data Event = Emitted EventData
  deriving (Eq, Show, Generic)

$(deriveAggregateCtors ''Cmd ''Regs [("One", "One"), ("Two", "Two")])
$(deriveWireCtors ''Event [("Emitted", "Emitted")])

mismatchedSchemaEmit :: SymTransducer (HsPred Regs Cmd) Regs Vertex Cmd Event
mismatchedSchemaEmit =
  B.buildTransducer Start (emptyRegFile :: RegFile Regs) (const False) do
    B.from Start do
      B.onCmd inCtorOne $ \_d -> B.do
        B.emit wireEmitted (OFCons (inpTwo #other) OFNil)
        B.goto Done

isTypeError :: TypeError -> Bool
isTypeError _ = True

type DupRegs = '[ '("dup", Int), '("dup", Bool)]

_dupRegs :: RegFile DupRegs
_dupRegs = RCons (Proxy @"dup") 0 (RCons (Proxy @"dup") False RNil)

_duplicateSlots :: SymTransducer (HsPred DupRegs ()) DupRegs Vertex () ()
_duplicateSlots =
  B.buildTransducer Start _dupRegs (const False) do
    B.from Start (pure ())

-- Removing -fdefer-type-errors makes '_duplicateSlots' fail compilation with:
--
-- Keiki: register file declares slot "dup" more than once.
-- Slot names in a register file must be pairwise distinct;
-- a duplicated name silently shadows the later slot.
--
-- GHC erases the unsatisfied type-family dictionary under deferred errors,
-- so evaluating the binding cannot catch it as 'TypeError'. Keeping the
-- binding here preserves a compile-only regression alongside the executable
-- mismatched-schema test.

spec :: Spec
spec = do
  it "rejects emit fields projected from a different command schema" $
    evaluate mismatchedSchemaEmit `shouldThrow` isTypeError