packages feed

keiki-0.9.0.0: src/Keiki/Generics.hs

{-# LANGUAGE TypeFamilies #-}

-- | DX spike (EP-2 follow-up): generic-derived inversions between
-- Haskell records and 'RegFile's so users can build 'InCtor' values
-- without hand-rolling RCons-towers.
--
-- Status: experimental. See the EP-2 retrospective for context.
--
-- == Trust model
--
-- Trusted structural schemas minted by the @Via@ producers are rooted in the
-- lawfulness of the consumer type's 'GHC.Generics.Generic' instance: the
-- constructor path, field spine, and the match\/build closures are all read
-- from the same @Rep@. A derived instance is always lawful, so
-- @deriving stock Generic@ types get honest evidence by construction. A
-- hand-written, deliberately unlawful 'GHC.Generics.Generic' instance can
-- pair trusted evidence with behavior it does not describe; that path is
-- outside the threat model in exactly the way 'Unsafe.Coerce.unsafeCoerce'
-- is. Keiki's sealed boundaries defend against /accidental/ decoupling —
-- record updates, closure-taking helpers, forged capabilities — not against a
-- consumer determined to lie to the compiler.
module Keiki.Generics
  ( -- * Generic-derived InCtor
    mkInCtor,
    mkInCtor0,
    mkInCtorVia,
    mkInCtorRecordVia,

    -- * Generic-derived WireCtor
    mkWireCtor,
    mkWireCtor0,
    mkWireCtorVia,
    mkWireCtor0Via,
    mkWireCtorRecordVia,
    FieldsOf,
    FieldsOfRep,

    -- * Slot-list deriving
    RegFieldsOf,
    RegFieldsOfRep,

    -- * Empty register file
    EmptyRegFile (..),

    -- * Internals
    GRecord (..),
    GTuple (..),
    Append,
    appendRegFile,
    SplitRegFile (..),
    ConcatT,
    SplitT (..),
  )
where

import Data.Kind (Type)
import Data.Proxy (Proxy (..))
import Data.Type.Bool (type (||))
import Data.Typeable (Typeable)
import GHC.Generics
import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)
import Keiki.Core
import Keiki.Internal.ConstructorEvidence (constructorEvidence)
import Keiki.Internal.WireSchema
  ( AppendInCtorFields,
    AppendWireFields,
    InCtorFieldSchema,
    WireCtorPath,
    appendInCtorFieldSchema,
    appendWireFieldSchema,
    genericPrefixWireCtorPathLeft,
    genericPrefixWireCtorPathRight,
    inCtorFieldsCons,
    inCtorFieldsNil,
    trustedInCtorSchema,
    trustedWireSchema,
    wireCtorPathRoot,
    wireFieldsCons,
    wireFieldsNil,
  )

-- | Walk a 'GHC.Generics' record representation to/from a 'RegFile'.
-- Slot lists are derived from the record's field metadata: every
-- selector @M1 S ('MetaSel ('Just name) ...)@ contributes a slot
-- @'(name, fieldType)@; products concatenate.
class GRecord (rep :: Type -> Type) (ifs :: [Slot]) | rep -> ifs where
  gToRegFile :: rep a -> RegFile ifs
  gFromRegFile :: RegFile ifs -> rep a

-- M1 D wrapper (data type metadata): pass through.
instance (GRecord inner ifs) => GRecord (M1 D meta inner) ifs where
  gToRegFile (M1 r) = gToRegFile r
  gFromRegFile rf = M1 (gFromRegFile rf)

-- M1 C wrapper (constructor metadata): pass through.
instance (GRecord inner ifs) => GRecord (M1 C meta inner) ifs where
  gToRegFile (M1 r) = gToRegFile r
  gFromRegFile rf = M1 (gFromRegFile rf)

-- Named selector with a leaf field: one slot.
instance
  (KnownSymbol name) =>
  GRecord
    (M1 S ('MetaSel ('Just name) su ss ds) (K1 r t))
    '[ '(name, t)]
  where
  gToRegFile (M1 (K1 v)) = RCons (Proxy @name) v RNil
  gFromRegFile (RCons _ v _) = M1 (K1 v)

-- No-arg constructor: empty slot list.
instance GRecord U1 '[] where
  gToRegFile U1 = RNil
  gFromRegFile RNil = U1

-- Product: concatenate slot lists.
instance
  ( GRecord l ls,
    GRecord r rs,
    Append ls rs ~ ifs,
    SplitRegFile ls rs
  ) =>
  GRecord (l :*: r) ifs
  where
  gToRegFile (a :*: b) = appendRegFile (gToRegFile a) (gToRegFile b)
  gFromRegFile rf = case splitRegFile @ls @rs rf of
    (lrf, rrf) -> gFromRegFile lrf :*: gFromRegFile rrf

-- | Type-level append for slot lists.
type family Append (xs :: [Slot]) (ys :: [Slot]) :: [Slot] where
  Append '[] ys = ys
  Append (x ': xs) ys = x ': Append xs ys

-- | Value-level append for register files.
appendRegFile :: RegFile ls -> RegFile rs -> RegFile (Append ls rs)
appendRegFile RNil rs = rs
appendRegFile (RCons p v xs) rs = RCons p v (appendRegFile xs rs)

-- | Split a register file at the boundary between two slot lists.
class SplitRegFile (ls :: [Slot]) (rs :: [Slot]) where
  splitRegFile :: RegFile (Append ls rs) -> (RegFile ls, RegFile rs)

instance SplitRegFile '[] rs where
  splitRegFile rf = (RNil, rf)

instance (SplitRegFile ls rs) => SplitRegFile ('(s, t) ': ls) rs where
  splitRegFile (RCons p v rest) =
    case splitRegFile @ls @rs rest of
      (lrf, rrf) -> (RCons p v lrf, rrf)

-- | Build an 'InCtor' from a constructor name, a sum-side matcher,
-- and a pack function. The 'RegFile' inversion is derived from the
-- record's 'GHC.Generics.Generic' instance — no RCons-tower required.
--
-- Example:
--
-- > inCtorStart :: InCtor UserCmd
-- >                       '[ '("email",       Email)
-- >                        , '("confirmCode", ConfirmationCode)
-- >                        , '("at",          UTCTime)
-- >                        ]
-- > inCtorStart = mkInCtor "StartRegistration"
-- >                        (\case StartRegistration d -> Just d; _ -> Nothing)
-- >                        StartRegistration
--
-- The slot list is inferred from @StartRegistrationData@'s 'Generic'
-- field metadata. The record type must have @deriving (Generic)@.
mkInCtor ::
  forall ci d ifs.
  ( Generic d,
    GRecord (Rep d) ifs,
    AssembleRegFile ifs,
    KnownSlotNames ifs
  ) =>
  String ->
  (ci -> Maybe d) ->
  (d -> ci) ->
  InCtor ci ifs
{-# DEPRECATED mkInCtor "Use mkInCtorVia for Generic constructors, or unavailableInCtor for manual behavior." #-}
mkInCtor name match wrap =
  unavailableInCtor
    name
    ( \ci -> case match ci of
        Just d -> Just (gToRegFile (from d))
        Nothing -> Nothing
    )
    (\rf -> wrap (to (gFromRegFile rf)))

-- | Build an 'InCtor' for a no-payload (singleton) constructor. The
-- 'icMatch' compares against the named singleton via 'Eq'; 'icBuild'
-- ignores the empty 'RegFile' and returns the singleton.
--
-- Example:
--
-- > inCtorContinue :: InCtor UserCmd '[]
-- > inCtorContinue = mkInCtor0 "Continue" Continue
mkInCtor0 :: forall ci. (Eq ci) => String -> ci -> InCtor ci '[]
{-# DEPRECATED mkInCtor0 "Use mkInCtorVia for Generic nullary constructors, or unavailableInCtor for manual behavior." #-}
mkInCtor0 name singleton =
  unavailableInCtor
    name
    (\ci -> if ci == singleton then Just RNil else Nothing)
    (\RNil -> singleton)

-- * Generic-derived WireCtor ----------------------------------------------

-- | Walk a 'GHC.Generics' record representation to/from the nested-
-- pair tuple shape that 'WireCtor' / 'OutFields' carry. A record with
-- fields @f1, f2, f3@ corresponds to the tuple @(f1, (f2, (f3, ())))@.
class GTuple (rep :: Type -> Type) (fs :: Type) | rep -> fs where
  gToTuple :: rep a -> fs
  gFromTuple :: fs -> rep a

instance (GTuple inner fs) => GTuple (M1 D meta inner) fs where
  gToTuple (M1 r) = gToTuple r
  gFromTuple t = M1 (gFromTuple t)

instance (GTuple inner fs) => GTuple (M1 C meta inner) fs where
  gToTuple (M1 r) = gToTuple r
  gFromTuple t = M1 (gFromTuple t)

instance GTuple (M1 S meta (K1 r t)) (t, ()) where
  gToTuple (M1 (K1 v)) = (v, ())
  gFromTuple (v, ()) = M1 (K1 v)

instance GTuple U1 () where
  gToTuple U1 = ()
  gFromTuple () = U1

instance
  ( GTuple l ls,
    GTuple r rs,
    ConcatT ls rs ~ fs,
    SplitT ls rs
  ) =>
  GTuple (l :*: r) fs
  where
  gToTuple (a :*: b) = appendT (gToTuple a) (gToTuple b)
  gFromTuple t = case splitT @ls @rs t of
    (lt, rt) -> gFromTuple lt :*: gFromTuple rt

-- | Type-level concat for nested-pair tuples. @ConcatT (f1, (f2, ())) (f3, ()) ~ (f1, (f2, (f3, ())))@.
type ConcatT (a :: Type) (b :: Type) = AppendWireFields a b

-- | Split a concatenated nested-pair tuple back into its halves; also
-- the inverse direction (append).
class SplitT (a :: Type) (b :: Type) where
  splitT :: ConcatT a b -> (a, b)
  appendT :: a -> b -> ConcatT a b

instance SplitT () b where
  splitT b = ((), b)
  appendT () b = b

instance (SplitT xs b) => SplitT (x, xs) b where
  splitT (x, rest) = case splitT @xs @b rest of
    (a, c) -> ((x, a), c)
  appendT (x, xs) b = (x, appendT xs b)

-- | Resolve a record type to its nested-pair field tuple. With this
-- alias, @WireCtor UserEvent (FieldsOf RegistrationStartedData)@
-- replaces the hand-written @WireCtor UserEvent (Email,
-- (ConfirmationCode, (UTCTime, ())))@.
type FieldsOf d = FieldsOfRep (Rep d)

-- | The nested-pair tuple shape derived from a 'GHC.Generics' Rep.
type family FieldsOfRep (rep :: Type -> Type) :: Type where
  FieldsOfRep (M1 D _ inner) = FieldsOfRep inner
  FieldsOfRep (M1 C _ inner) = FieldsOfRep inner
  FieldsOfRep (M1 S _ (K1 _ t)) = (t, ())
  FieldsOfRep U1 = ()
  FieldsOfRep (l :*: r) = ConcatT (FieldsOfRep l) (FieldsOfRep r)

-- | Resolve a record type to its 'RegFile' slot list. With this
-- alias, @InCtor UserCmd (RegFieldsOf StartRegistrationData)@
-- replaces the hand-written @InCtor UserCmd '[ '("email", Email),
-- '("confirmCode", ConfirmationCode), '("at", UTCTime) ]@.
type RegFieldsOf d = RegFieldsOfRep (Rep d)

-- | The slot-list shape derived from a 'GHC.Generics' Rep. Mirrors
-- 'FieldsOfRep' but emits @[Slot]@ instead of a nested-pair tuple,
-- preserving the selector name on every field.
type family RegFieldsOfRep (rep :: Type -> Type) :: [Slot] where
  RegFieldsOfRep (M1 D _ inner) = RegFieldsOfRep inner
  RegFieldsOfRep (M1 C _ inner) = RegFieldsOfRep inner
  RegFieldsOfRep (M1 S ('MetaSel ('Just n) _ _ _) (K1 _ t)) =
    '[ '(n, t)]
  RegFieldsOfRep U1 = '[]
  RegFieldsOfRep (l :*: r) =
    Append
      (RegFieldsOfRep l)
      (RegFieldsOfRep r)

-- | Build a 'WireCtor' from a constructor name, a sum-side matcher,
-- and a pack function. The nested-pair conversion is derived from the
-- record's 'GHC.Generics.Generic' instance.
--
-- Example:
--
-- > wireRegistrationStarted
-- >   :: WireCtor UserEvent (FieldsOf RegistrationStartedData)
-- > wireRegistrationStarted = mkWireCtor "RegistrationStarted"
-- >   (\case RegistrationStarted d -> Just d; _ -> Nothing)
-- >   RegistrationStarted
{-# DEPRECATED mkWireCtor "Use mkWireCtorVia for Generic constructors, or unavailableWireCtor for manual behavior." #-}
mkWireCtor ::
  forall co d fs.
  ( Generic d,
    GTuple (Rep d) fs
  ) =>
  String ->
  (co -> Maybe d) ->
  (d -> co) ->
  WireCtor co fs
mkWireCtor name match wrap =
  unavailableWireCtor
    name
    ( \co -> case match co of
        Just d -> Just (gToTuple (from d))
        Nothing -> Nothing
    )
    (\fs -> wrap (to (gFromTuple fs)))

-- | Build a 'WireCtor' for a no-payload (singleton) event constructor —
-- the event-side twin of 'mkInCtor0'. Its field tuple is @()@ (a
-- payload-free event carries nothing), matching @'OutFields' rs ci ()@ /
-- @OFNil@. 'wcMatch' compares against the named singleton via 'Eq';
-- 'wcBuild' ignores the empty tuple and returns the singleton. The
-- @'Eq' co@ constraint matches 'mkInCtor0'\'s @'Eq' ci@; event sums in
-- this codebase already derive 'Eq', so it is not a new burden.
--
-- Example:
--
-- > wireOpened :: WireCtor DoorEvent ()
-- > wireOpened = mkWireCtor0 "Opened" Opened
{-# DEPRECATED mkWireCtor0 "Use mkWireCtor0Via for Generic nullary constructors, or unavailableWireCtor for manual behavior." #-}
mkWireCtor0 :: forall co. (Eq co) => String -> co -> WireCtor co ()
mkWireCtor0 name singleton =
  unavailableWireCtor
    name
    (\co -> if co == singleton then Just () else Nothing)
    (\() -> singleton)

-- * Empty register file ---------------------------------------------------

-- | Derive an initial 'RegFile' for any slot list. Every slot is
-- pre-bound to a deferred error tagged with the slot's name so reads
-- of an uninitialized slot crash with a targeted message instead of
-- a silent bottom. Every slot must be written before the register file is
-- read or encoded; 'emptyRegFile' is an initialization scaffold, not an
-- encodable snapshot.
class EmptyRegFile (rs :: [Slot]) where
  emptyRegFile :: RegFile rs

instance EmptyRegFile '[] where
  emptyRegFile = RNil

instance
  (KnownSymbol s, EmptyRegFile rs) =>
  EmptyRegFile ('(s, r) ': rs)
  where
  emptyRegFile =
    RCons
      (Proxy @s)
      ( error
          ( "uninit: "
              ++ symbolVal (Proxy @s)
              ++ " (slot read before first write; a RegFile must be fully initialized before it is read or encoded)"
          )
      )
      emptyRegFile

-- * Sum-walking machinery -------------------------------------------------

-- | Does the constructor named @n@ appear anywhere in the 'Generic'
-- representation @rep@? Used to dispatch sum-side resolution to the
-- correct branch.
type family NameInRep (n :: Symbol) (rep :: Type -> Type) :: Bool where
  NameInRep n (M1 D _ inner) = NameInRep n inner
  NameInRep n (l :+: r) = NameInRep n l || NameInRep n r
  NameInRep n (M1 C ('MetaCons n _ _) _) = 'True
  NameInRep _ _ = 'False

-- | Walk a 'Generic' representation looking for a constructor named
-- @name@; resolve its payload type @d@. Two payload shapes are
-- supported:
--
--   * @M1 C ('MetaCons name _ _) (M1 S _ (K1 _ d))@ — single-field
--     constructor wrapping a record value of type @d@.
--   * @M1 C ('MetaCons name _ _) U1@ — no-payload constructor;
--     @d ~ ()@.
--
-- The functional dependency @name rep -> d@ pins the payload uniquely.
class
  GHasCtor (name :: Symbol) (rep :: Type -> Type) (d :: Type)
    | name rep -> d
  where
  gMatchCtor :: rep a -> Maybe d
  gBuildCtor :: d -> rep a

-- | Derive the ordinal sum path of a named constructor. This class is kept
-- private so consumers cannot supply a dishonest path instance.
class GWireCtorPath (name :: Symbol) (rep :: Type -> Type) where
  gWireCtorPath :: WireCtorPath carrier

instance (GWireCtorPath name inner) => GWireCtorPath name (M1 D meta inner) where
  gWireCtorPath = gWireCtorPath @name @inner

instance GWireCtorPath name (M1 C ('MetaCons name fix lazy) payload) where
  gWireCtorPath = wireCtorPathRoot

instance
  ( hasLeft ~ NameInRep name left,
    GWireCtorPathIf hasLeft name left right
  ) =>
  GWireCtorPath name (left :+: right)
  where
  gWireCtorPath = gWireCtorPathIf @hasLeft @name @left @right

class
  GWireCtorPathIf
    (hasLeft :: Bool)
    (name :: Symbol)
    (left :: Type -> Type)
    (right :: Type -> Type)
  where
  gWireCtorPathIf :: WireCtorPath carrier

instance (GWireCtorPath name left) => GWireCtorPathIf 'True name left right where
  gWireCtorPathIf =
    genericPrefixWireCtorPathLeft (gWireCtorPath @name @left)

instance (GWireCtorPath name right) => GWireCtorPathIf 'False name left right where
  gWireCtorPathIf =
    genericPrefixWireCtorPathRight (gWireCtorPath @name @right)

-- | Derive a typed, source-ordered field spine from a payload's Generic
-- representation. Like 'GWireCtorPath', this class is private so only the
-- library's Generic instances can mint trusted evidence.
class GWireFieldSchema (rep :: Type -> Type) (fields :: Type) | rep -> fields where
  gWireFieldSchema :: WireFieldSchema fields

instance (GWireFieldSchema inner fields) => GWireFieldSchema (M1 D meta inner) fields where
  gWireFieldSchema = gWireFieldSchema @inner

instance (GWireFieldSchema inner fields) => GWireFieldSchema (M1 C meta inner) fields where
  gWireFieldSchema = gWireFieldSchema @inner

instance
  (Typeable field, Selector meta) =>
  GWireFieldSchema (M1 S meta (K1 r field)) (field, ())
  where
  gWireFieldSchema =
    wireFieldsCons selectorLabel wireFieldsNil
    where
      selectorText = selName (undefined :: M1 S meta (K1 r field) ())
      selectorLabel
        | null selectorText = Nothing
        | otherwise = Just selectorText

instance GWireFieldSchema U1 () where
  gWireFieldSchema = wireFieldsNil

instance
  ( GWireFieldSchema left leftFields,
    GWireFieldSchema right rightFields,
    ConcatT leftFields rightFields ~ fields
  ) =>
  GWireFieldSchema (left :*: right) fields
  where
  gWireFieldSchema =
    appendWireFieldSchema
      (gWireFieldSchema @left)
      (gWireFieldSchema @right)

-- | Derive a typed, source-ordered slot spine from an input payload's
-- Generic representation. This class is private so only Keiki's Generic
-- implementation can mint trusted input-constructor evidence.
class
  GInCtorFieldSchema
    (rep :: Type -> Type)
    (fields :: [Slot])
    | rep -> fields
  where
  gInCtorFieldSchema :: InCtorFieldSchema fields

instance
  (GInCtorFieldSchema inner fields) =>
  GInCtorFieldSchema (M1 D meta inner) fields
  where
  gInCtorFieldSchema = gInCtorFieldSchema @inner

instance
  (GInCtorFieldSchema inner fields) =>
  GInCtorFieldSchema (M1 C meta inner) fields
  where
  gInCtorFieldSchema = gInCtorFieldSchema @inner

instance
  (KnownSymbol name, Typeable field) =>
  GInCtorFieldSchema
    (M1 S ('MetaSel ('Just name) su ss ds) (K1 r field))
    '[ '(name, field)]
  where
  gInCtorFieldSchema =
    inCtorFieldsCons
      (Just (symbolVal (Proxy @name)))
      inCtorFieldsNil

instance GInCtorFieldSchema U1 '[] where
  gInCtorFieldSchema = inCtorFieldsNil

instance
  ( GInCtorFieldSchema left leftFields,
    GInCtorFieldSchema right rightFields,
    AppendInCtorFields leftFields rightFields ~ fields
  ) =>
  GInCtorFieldSchema (left :*: right) fields
  where
  gInCtorFieldSchema =
    appendInCtorFieldSchema
      (gInCtorFieldSchema @left)
      (gInCtorFieldSchema @right)

-- Pass through the data-type wrapper.
instance (GHasCtor n inner d) => GHasCtor n (M1 D meta inner) d where
  gMatchCtor (M1 r) = gMatchCtor @n r
  gBuildCtor d = M1 (gBuildCtor @n d)

-- Match a constructor whose payload is a single record value.
instance GHasCtor n (M1 C ('MetaCons n fix lazy) (M1 S meta (K1 r d))) d where
  gMatchCtor (M1 (M1 (K1 d))) = Just d
  gBuildCtor d = M1 (M1 (K1 d))

-- Match a no-payload constructor; payload is the unit type '()'.
instance GHasCtor n (M1 C ('MetaCons n fix lazy) U1) () where
  gMatchCtor (M1 U1) = Just ()
  gBuildCtor () = M1 U1

-- Sum dispatch: pick the side that contains the named constructor.
instance
  ( hasL ~ NameInRep n l,
    GHasCtorIf hasL n l r d
  ) =>
  GHasCtor n (l :+: r) d
  where
  gMatchCtor x = gMatchCtorIf @hasL @n @l @r x
  gBuildCtor d = gBuildCtorIf @hasL @n @l @r d

-- | Sum-dispatch helper: reduce 'GHasCtor' on @l :+: r@ to a
-- 'GHasCtor' on the side that contains the named constructor.
class
  GHasCtorIf
    (b :: Bool)
    (n :: Symbol)
    (l :: Type -> Type)
    (r :: Type -> Type)
    (d :: Type)
    | b n l r -> d
  where
  gMatchCtorIf :: (l :+: r) a -> Maybe d
  gBuildCtorIf :: d -> (l :+: r) a

instance (GHasCtor n l d) => GHasCtorIf 'True n l r d where
  gMatchCtorIf (L1 x) = gMatchCtor @n x
  gMatchCtorIf (R1 _) = Nothing
  gBuildCtorIf d = L1 (gBuildCtor @n d)

instance (GHasCtor n r d) => GHasCtorIf 'False n l r d where
  gMatchCtorIf (L1 _) = Nothing
  gMatchCtorIf (R1 x) = gMatchCtor @n x
  gBuildCtorIf d = R1 (gBuildCtor @n d)

-- | Direct-record counterpart of 'GHasCtor'. Instead of resolving one
-- wrapped payload value, this class derives a 'RegFile' directly from the
-- named constructor's own record fields.
class
  GRecordCtor
    (name :: Symbol)
    (rep :: Type -> Type)
    (fields :: [Slot])
    | name rep -> fields
  where
  gMatchRecordCtor :: rep a -> Maybe (RegFile fields)
  gBuildRecordCtor :: RegFile fields -> rep a
  gRecordCtorSchema :: InCtorFieldSchema fields

instance
  (GRecordCtor name inner fields) =>
  GRecordCtor name (M1 D meta inner) fields
  where
  gMatchRecordCtor (M1 representation) = gMatchRecordCtor @name representation
  gBuildRecordCtor fields = M1 (gBuildRecordCtor @name fields)
  gRecordCtorSchema = gRecordCtorSchema @name @inner

instance
  ( GRecord inner fields,
    GInCtorFieldSchema inner fields
  ) =>
  GRecordCtor name (M1 C ('MetaCons name fix lazy) inner) fields
  where
  gMatchRecordCtor (M1 representation) = Just (gToRegFile representation)
  gBuildRecordCtor fields = M1 (gFromRegFile fields)
  gRecordCtorSchema = gInCtorFieldSchema @inner

instance
  ( hasLeft ~ NameInRep name left,
    GRecordCtorIf hasLeft name left right fields
  ) =>
  GRecordCtor name (left :+: right) fields
  where
  gMatchRecordCtor = gMatchRecordCtorIf @hasLeft @name
  gBuildRecordCtor = gBuildRecordCtorIf @hasLeft @name
  gRecordCtorSchema = gRecordCtorSchemaIf @hasLeft @name @left @right

class
  GRecordCtorIf
    (hasLeft :: Bool)
    (name :: Symbol)
    (left :: Type -> Type)
    (right :: Type -> Type)
    (fields :: [Slot])
    | hasLeft name left right -> fields
  where
  gMatchRecordCtorIf :: (left :+: right) a -> Maybe (RegFile fields)
  gBuildRecordCtorIf :: RegFile fields -> (left :+: right) a
  gRecordCtorSchemaIf :: InCtorFieldSchema fields

instance (GRecordCtor name left fields) => GRecordCtorIf 'True name left right fields where
  gMatchRecordCtorIf (L1 representation) = gMatchRecordCtor @name representation
  gMatchRecordCtorIf (R1 _) = Nothing
  gBuildRecordCtorIf fields = L1 (gBuildRecordCtor @name fields)
  gRecordCtorSchemaIf = gRecordCtorSchema @name @left

instance (GRecordCtor name right fields) => GRecordCtorIf 'False name left right fields where
  gMatchRecordCtorIf (L1 _) = Nothing
  gMatchRecordCtorIf (R1 representation) = gMatchRecordCtor @name representation
  gBuildRecordCtorIf fields = R1 (gBuildRecordCtor @name fields)
  gRecordCtorSchemaIf = gRecordCtorSchema @name @right

-- | Direct-record wire counterpart of 'GRecordCtor'.
class
  GTupleCtor
    (name :: Symbol)
    (rep :: Type -> Type)
    (fields :: Type)
    | name rep -> fields
  where
  gMatchTupleCtor :: rep a -> Maybe fields
  gBuildTupleCtor :: fields -> rep a
  gTupleCtorSchema :: WireFieldSchema fields

instance
  (GTupleCtor name inner fields) =>
  GTupleCtor name (M1 D meta inner) fields
  where
  gMatchTupleCtor (M1 representation) = gMatchTupleCtor @name representation
  gBuildTupleCtor fields = M1 (gBuildTupleCtor @name fields)
  gTupleCtorSchema = gTupleCtorSchema @name @inner

instance
  ( GTuple inner fields,
    GWireFieldSchema inner fields
  ) =>
  GTupleCtor name (M1 C ('MetaCons name fix lazy) inner) fields
  where
  gMatchTupleCtor (M1 representation) = Just (gToTuple representation)
  gBuildTupleCtor fields = M1 (gFromTuple fields)
  gTupleCtorSchema = gWireFieldSchema @inner

instance
  ( hasLeft ~ NameInRep name left,
    GTupleCtorIf hasLeft name left right fields
  ) =>
  GTupleCtor name (left :+: right) fields
  where
  gMatchTupleCtor = gMatchTupleCtorIf @hasLeft @name
  gBuildTupleCtor = gBuildTupleCtorIf @hasLeft @name
  gTupleCtorSchema = gTupleCtorSchemaIf @hasLeft @name @left @right

class
  GTupleCtorIf
    (hasLeft :: Bool)
    (name :: Symbol)
    (left :: Type -> Type)
    (right :: Type -> Type)
    (fields :: Type)
    | hasLeft name left right -> fields
  where
  gMatchTupleCtorIf :: (left :+: right) a -> Maybe fields
  gBuildTupleCtorIf :: fields -> (left :+: right) a
  gTupleCtorSchemaIf :: WireFieldSchema fields

instance (GTupleCtor name left fields) => GTupleCtorIf 'True name left right fields where
  gMatchTupleCtorIf (L1 representation) = gMatchTupleCtor @name representation
  gMatchTupleCtorIf (R1 _) = Nothing
  gBuildTupleCtorIf fields = L1 (gBuildTupleCtor @name fields)
  gTupleCtorSchemaIf = gTupleCtorSchema @name @left

instance (GTupleCtor name right fields) => GTupleCtorIf 'False name left right fields where
  gMatchTupleCtorIf (L1 _) = Nothing
  gMatchTupleCtorIf (R1 representation) = gMatchTupleCtor @name representation
  gBuildTupleCtorIf fields = R1 (gBuildTupleCtor @name fields)
  gTupleCtorSchemaIf = gTupleCtorSchema @name @right

-- * Generic-derived InCtor / WireCtor (Via builders) ----------------------

-- | Build an 'InCtor' from a constructor name alone. The sum-side
-- match\/wrap pair and the record-side RegFile inversion are both
-- derived from the 'Generic' representations of @ci@ and the inferred
-- payload @d@. With no-payload constructors (e.g. 'Continue') the
-- inferred slot list is @\'[]@.
--
-- Example:
--
-- > inCtorStart    :: InCtor UserCmd StartFields
-- > inCtorStart     = mkInCtorVia @"StartRegistration"
-- >
-- > inCtorContinue :: InCtor UserCmd '[]
-- > inCtorContinue  = mkInCtorVia @"Continue"
mkInCtorVia ::
  forall (name :: Symbol) ci d ifs.
  ( KnownSymbol name,
    Generic ci,
    GHasCtor name (Rep ci) d,
    GWireCtorPath name (Rep ci),
    Generic d,
    GRecord (Rep d) ifs,
    GInCtorFieldSchema (Rep d) ifs,
    AssembleRegFile ifs,
    KnownSlotNames ifs
  ) =>
  InCtor ci ifs
mkInCtorVia =
  trustedInCtorInternal
    constructorEvidence
    (symbolVal (Proxy @name))
    ( trustedInCtorSchema
        (gWireCtorPath @name @(Rep ci))
        (gInCtorFieldSchema @(Rep d))
    )
    ( \ci -> case gMatchCtor @name (from ci) of
        Just d -> Just (gToRegFile (from d))
        Nothing -> Nothing
    )
    (\rf -> to (gBuildCtor @name (to (gFromRegFile rf) :: d)))

-- | Build a trusted 'InCtor' for a constructor whose payload fields are
-- declared directly with record syntax. Use 'mkInCtorVia' when the sum
-- constructor instead wraps a separate record value.
mkInCtorRecordVia ::
  forall (name :: Symbol) ci ifs.
  ( KnownSymbol name,
    Generic ci,
    GRecordCtor name (Rep ci) ifs,
    GWireCtorPath name (Rep ci),
    AssembleRegFile ifs,
    KnownSlotNames ifs
  ) =>
  InCtor ci ifs
mkInCtorRecordVia =
  trustedInCtorInternal
    constructorEvidence
    (symbolVal (Proxy @name))
    ( trustedInCtorSchema
        (gWireCtorPath @name @(Rep ci))
        (gRecordCtorSchema @name @(Rep ci))
    )
    (gMatchRecordCtor @name . from)
    (to . gBuildRecordCtor @name)

-- | Build a trusted 'WireCtor' from a constructor name alone. Mirrors
-- 'mkInCtorVia' on the wire side: the nested-pair field tuple comes
-- from the inferred payload's 'Generic' field metadata, while the carrier's
-- Generic sum path and ordered field types become its structural schema.
--
-- Example:
--
-- > wireRegistrationStarted
-- >   :: WireCtor UserEvent (FieldsOf RegistrationStartedData)
-- > wireRegistrationStarted = mkWireCtorVia @"RegistrationStarted"
mkWireCtorVia ::
  forall (name :: Symbol) co d fs.
  ( KnownSymbol name,
    Generic co,
    GHasCtor name (Rep co) d,
    GWireCtorPath name (Rep co),
    Generic d,
    GTuple (Rep d) fs,
    GWireFieldSchema (Rep d) fs
  ) =>
  WireCtor co fs
mkWireCtorVia =
  trustedWireCtorInternal
    constructorEvidence
    (symbolVal (Proxy @name))
    ( trustedWireSchema
        (gWireCtorPath @name @(Rep co))
        (gWireFieldSchema @(Rep d))
    )
    ( \co -> case gMatchCtor @name (from co) of
        Just d -> Just (gToTuple (from d))
        Nothing -> Nothing
    )
    (\fs -> to (gBuildCtor @name (to (gFromTuple fs) :: d)))

-- | Build a trusted 'WireCtor' for a named no-payload constructor using
-- structural Generic matching. Unlike 'mkWireCtor0', matching is not
-- mediated by the carrier's 'Eq' instance.
mkWireCtor0Via ::
  forall (name :: Symbol) co.
  ( KnownSymbol name,
    Generic co,
    GHasCtor name (Rep co) (),
    GWireCtorPath name (Rep co)
  ) =>
  WireCtor co ()
mkWireCtor0Via = mkWireCtorVia @name @co @() @()

-- | Build a trusted 'WireCtor' for a constructor whose fields are declared
-- directly with record syntax. Use 'mkWireCtorVia' for a constructor that
-- wraps a separate record value.
mkWireCtorRecordVia ::
  forall (name :: Symbol) co fields.
  ( KnownSymbol name,
    Generic co,
    GTupleCtor name (Rep co) fields,
    GWireCtorPath name (Rep co)
  ) =>
  WireCtor co fields
mkWireCtorRecordVia =
  trustedWireCtorInternal
    constructorEvidence
    (symbolVal (Proxy @name))
    ( trustedWireSchema
        (gWireCtorPath @name @(Rep co))
        (gTupleCtorSchema @name @(Rep co))
    )
    (gMatchTupleCtor @name . from)
    (to . gBuildTupleCtor @name)