packages feed

polytropos-dhall-0.1.0.2: src/Polytropos/Dhall.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE EmptyCase #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -Wno-orphans #-}

{-|
Module      : Polytropos.Dhall
Description : High-performance Dhall codecs and generic adapters for Polytropos open records and variants
Copyright   : (c) 2026 The Polytropos Authors
License     : EUPL-1.2 OR Apache-2.0
Maintainer  : nadia.yvette.chambers@ik.me
Stability   : experimental
Portability : non-portable (GHC extensions)

Linear \(O(N)\) 'Dhall.FromDhall' and 'Dhall.ToDhall' codecs for heterogeneous open
'Record', 'Variant', and higher-kinded 'RecordF' types, alongside generic adapters
powered by "Dhall.Marshal.Decode.GenericFromDhall" and "Dhall.Marshal.Encode.GenericToDhall".

= Architectural Overview & Rationale

== Flat Memory Physics vs. GHC.Generics Binary Trees

Standard "GHC.Generics" deriving for Dhall walks a deeply nested binary tree of
@(:*:)@, @(:+:)@, @M1@, and @K1@ constructors. On larger configuration schemas
(20+ fields or constructors), this induces substantial compile-time overhead and
allocates intermediate representation nodes for every parsed field.

In contrast, @polytropos-dhall@ unrolls the flat contiguous
'Data.Primitive.SmallArray.SmallArray' payload directly into and out of Dhall's
abstract syntax tree ('Dhall.Syntax.Expr'):

* __Records__: An open 'Record' encodes into a native Dhall record literal
  @RecordLit { x = ..., y = ... }@ in exact row order, and decodes directly from
  the field map into flat memory with zero intermediate binary tree allocations.
* __Variants__: An open 'Variant' encodes into a native Dhall union literal
  @< Ctor : { ... } | ... >@ with \(O(1)\) constructor tag dispatch, avoiding
  linear cascading searches.
* __Higher-Kinded Data (HKD)__: 'RecordF' (such as @RecordF Maybe r@) integrates
  natively, with automatic 'Nothing' fallback semantics for omitted configuration fields.

== Generic Adapters for Nominal Schemas and Gachi Substrates

In addition to open rows, this package provides turnkey generic adapters
('GenericDhall', 'genericFromDhall', 'genericToDhall') leveraging Dhall's internal
'GenericFromDhall' and 'GenericToDhall' classes. Nominal Haskell records or
osteological structures (such as in @gachi@) can derive 'FromDhall' and 'ToDhall'
via 'GenericDhall' through @DerivingVia@:

> data ServerConfig = ServerConfig
>   { host :: Text
>   , port :: Natural
>   } deriving stock (Generic, Show, Eq)
>     deriving (FromDhall, ToDhall) via (GenericDhall ServerConfig)

= When to Use This Module

* When reading or writing configuration files, environment profiles, or service
  manifests modeled with @polytropos@ open records or variants.
* When deriving Dhall encoders and decoders for nominal records via 'GenericDhall'.
* When building extensible, multi-agent configuration pipelines (such as in
  @sarutahiko@ or @gachi@) requiring deterministic schemas.

= Worked Example

Open records are constructed using the @REC { ... }@ syntax enabled by the
@polytropos@ source plugin (via @-fplugin=Polytropos.Plugin@):

> {-# LANGUAGE DataKinds #-}
> {-# LANGUAGE OverloadedStrings #-}
> {-# OPTIONS_GHC -fplugin=Polytropos.Plugin #-}
>
> import Dhall (input, auto, embed, inject, Natural)
> import Data.Text (Text)
> import Polytropos
> import Polytropos.Dhall
>
> type AppConfig = Record '["host" ':= Text, "port" ':= Natural, "debug" ':= Bool]
>
> cfg :: AppConfig
> cfg = REC { host = "localhost", port = 8080, debug = True }
>
> -- Embed to Dhall expression:
> dhallExpr = embed inject cfg
>
> -- Decode from Dhall text:
> cfg' <- input auto "{ host = \"0.0.0.0\", port = 9000, debug = False }"
-}
module Polytropos.Dhall
  ( -- * High-Level Convenience API
    encodeDhall
  , injectDhall
  , decodeDhall
  , extractDhall

    -- * Heterogeneous Row Codecs
  , AllToDhall (..)
  , AllFromDhall (..)
  , AllCtorsToDhall (..)
  , AllCtorsFromDhall (..)
  , DhallFieldFallback (..)

    -- * Higher-Kinded Data Codecs
  , AllToDhallHKD (..)
  , AllFromDhallHKD (..)

    -- * Generic Adapters & DerivingVia
  , GenericDhall (..)
  , genericFromDhall
  , genericFromDhallWith
  , genericToDhall
  , genericToDhallWith
  , GenericFromDhall (..)
  , GenericFromDhallUnion (..)
  , GenericToDhall (..)

    -- * Homogeneity Evidence and Row Introspection
    -- $homogeneity
  , AllFieldsAre
  , AllCtorsHom (..)
  , FieldNames (..)
  , RowField
  , SumField
  ) where

import Data.Functor ((<&>))
import Data.Functor.Contravariant (contramap)
import Data.Functor.Identity (Identity (..))
import Data.Kind (Constraint, Type)
import Data.Primitive.SmallArray (smallArrayFromList)
import Data.Proxy (Proxy (..))
import Data.Text (Text)
import qualified Data.Text as T
import Data.Void (Void)
import GHC.Exts (Any, toList)
import GHC.Generics (Generic)
import qualified GHC.Generics as GHC
import GHC.TypeLits (KnownSymbol, symbolVal)

import Dhall
  ( Decoder (..)
  , Encoder (..)
  , FromDhall (..)
  , InputNormalizer (..)
  , InterpretOptions (..)
  , ToDhall (..)
  , defaultInterpretOptions
  , genericAuto
  , genericAutoWithInputNormalizer
  , genericToDhall
  , genericToDhallWith
  , genericToDhallWithInputNormalizer
  )
import qualified Dhall
import qualified Dhall.Core as Core
import qualified Dhall.Map as Dhall.Map
import Dhall.Marshal.Decode
  ( Expector
  , Extractor
  , GenericFromDhall (..)
  , GenericFromDhallUnion (..)
  , extractError
  , typeError
  )
import Dhall.Marshal.Encode (GenericToDhall (..))
import Dhall.Core
  ( Expr (..)
  , FieldSelection (..)
  )
import Dhall.Src (Src (..))

import Polytropos (Record, RecordF, Variant)
import Polytropos.Core
  ( Pair (..)
  , ProductRow
  , Rep (..)
  , Row
  , RowRep (..)
  , SumRow
  , injectRep
  , noInlineUnsafeCo
  , productRep
  , repTag
  , repVals
  )

{-------------------------------------------------------------------------------
  High-Level Convenience API
-------------------------------------------------------------------------------}

-- | Encode any 'ToDhall' value directly into a formatted Dhall text string.
encodeDhall :: ToDhall a => a -> Text
encodeDhall = Core.pretty . injectDhall

-- | Embed any 'ToDhall' value into a Dhall abstract syntax tree ('Expr Src Void').
injectDhall :: ToDhall a => a -> Expr Src Void
injectDhall = Dhall.embed Dhall.inject

-- | Decode a Dhall text expression directly into a Haskell value.
decodeDhall :: FromDhall a => Text -> IO a
decodeDhall = Dhall.input Dhall.auto

-- | Extract a Haskell value from a parsed Dhall expression.
extractDhall :: FromDhall a => Expr Src Void -> Extractor Src Void a
extractDhall = extract Dhall.auto

{-------------------------------------------------------------------------------
  Field Fallback Handling
-------------------------------------------------------------------------------}

-- | Provides a default fallback value when a field is missing from a Dhall record literal.
--
-- By default, missing fields produce an extraction error ('extractError').
-- For 'Maybe' fields, missing fields gracefully evaluate to 'Nothing', aligning
-- with Dhall's native optional field conventions.
class DhallFieldFallback a where
  dhallMissingField :: Maybe (Extractor Src Void a)

instance {-# OVERLAPPABLE #-} DhallFieldFallback a where
  dhallMissingField = Nothing

instance DhallFieldFallback (Maybe a) where
  dhallMissingField = Just (pure Nothing)

{-------------------------------------------------------------------------------
  Heterogeneous Row Codecs
-------------------------------------------------------------------------------}

-- | Evidence that every field of the row @r@ has a 'ToDhall' instance
-- at that field's own concrete type. Unrolls payload slots in row order.
class AllToDhall (r :: Row) where
  declaredRecordFields :: InputNormalizer -> Proxy r -> [(Text, Core.RecordField Src Void)]
  encRecordFields :: InputNormalizer -> Proxy r -> [Identity Any] -> [(Text, Core.RecordField Src Void)]

instance AllToDhall '[] where
  declaredRecordFields _ _ = []
  encRecordFields _ _ _ = []

instance (KnownSymbol n, ToDhall a, AllToDhall r) => AllToDhall ((n ':= a) ': r) where
  declaredRecordFields norm _ =
    let key = T.pack (symbolVal (Proxy @n))
        Encoder _ declA = injectWith norm :: Encoder a
    in (key, Core.makeRecordField declA) : declaredRecordFields norm (Proxy @r)

  encRecordFields norm _ (Identity x : xs) =
    let key = T.pack (symbolVal (Proxy @n))
        Encoder embedA _ = injectWith norm :: Encoder a
    in (key, Core.makeRecordField (embedA (noInlineUnsafeCo x :: a)))
       : encRecordFields norm (Proxy @r) xs
  encRecordFields _ _ [] = error "polytropos-dhall: payload arity mismatch"

-- | Evidence that every field of the row @r@ has a 'FromDhall' instance
-- at that field's own concrete type. Decodes each field by name from the Dhall record.
class AllFromDhall (r :: Row) where
  expectedRecordFields :: InputNormalizer -> Proxy r -> Expector [(Text, Core.RecordField Src Void)]
  decRecordFields :: InputNormalizer -> Proxy r -> Dhall.Map.Map Text (Core.RecordField Src Void) -> Extractor Src Void [Identity Any]

instance AllFromDhall '[] where
  expectedRecordFields _ _ = pure []
  decRecordFields _ _ _ = pure []

instance (KnownSymbol n, FromDhall a, DhallFieldFallback a, AllFromDhall r) => AllFromDhall ((n ':= a) ': r) where
  expectedRecordFields norm _ =
    let key = T.pack (symbolVal (Proxy @n))
        Decoder _ expectedA = autoWith norm :: Decoder a
        expectedRest = expectedRecordFields norm (Proxy @r)
    in liftA2 (\tyA rest -> (key, Core.makeRecordField tyA) : rest) expectedA expectedRest

  decRecordFields norm _ kvs =
    let key = T.pack (symbolVal (Proxy @n))
        Decoder extractA _ = autoWith norm :: Decoder a
        valA = case Dhall.Map.lookup key kvs of
                 Just rf -> extractA (Core.recordFieldValue rf)
                 Nothing -> case dhallMissingField @a of
                              Just fallback -> fallback
                              Nothing -> extractError ("Record is missing required field: " <> key)
        valRest = decRecordFields norm (Proxy @r) kvs
    in liftA2 (\x xs -> Identity (noInlineUnsafeCo x :: Any) : xs) valA valRest

-- | Helper class for permissive extraction of single-field variant payloads.
class DecSingleField (fs :: Row) where
  decRecordFieldsSingle :: InputNormalizer -> Proxy fs -> Expr Src Void -> Maybe (Extractor Src Void [Identity Any])

instance {-# OVERLAPPABLE #-} DecSingleField fs where
  decRecordFieldsSingle _ _ _ = Nothing

instance (KnownSymbol n, FromDhall a) => DecSingleField '[(n ':= a)] where
  decRecordFieldsSingle norm _ expr =
    let Decoder extractA _ = autoWith norm :: Decoder a
    in Just ((\x -> [Identity (noInlineUnsafeCo x :: Any)]) <$> extractA expr)

-- | Evidence that every constructor of the sum row @s@ has a heterogeneous
-- field row satisfying 'AllToDhall', paired with its constructor tag index.
class AllCtorsToDhall (s :: SumRow) where
  declaredUnionAlts :: InputNormalizer -> Proxy s -> [(Text, Maybe (Expr Src Void))]
  encVariantTag :: InputNormalizer -> Dhall.Map.Map Text (Maybe (Expr Src Void)) -> Proxy s -> Word -> [Identity Any] -> Expr Src Void

instance AllCtorsToDhall '[] where
  declaredUnionAlts _ _ = []
  encVariantTag _ _ _ _ _ = error "polytropos-dhall: empty variant has no constructors"

instance (KnownSymbol c, AllToDhall fs, AllCtorsToDhall s)
      => AllCtorsToDhall ((c ':= fs) ': s) where
  declaredUnionAlts norm _ =
    let cName = T.pack (symbolVal (Proxy @c))
        fsDecls = declaredRecordFields norm (Proxy @fs)
        altType = case fsDecls of
                    [] -> Nothing
                    _  -> Just (Record (Dhall.Map.fromList fsDecls))
    in (cName, altType) : declaredUnionAlts norm (Proxy @s)

  encVariantTag norm fullUnion _ 0 slots =
    let cName = T.pack (symbolVal (Proxy @c))
        fieldSel = Core.makeFieldSelection cName
        fsFields = encRecordFields norm (Proxy @fs) slots
    in case fsFields of
         [] -> Field (Union fullUnion) fieldSel
         _  -> App (Field (Union fullUnion) fieldSel) (RecordLit (Dhall.Map.fromList fsFields))

  encVariantTag norm fullUnion _ tag slots =
    encVariantTag norm fullUnion (Proxy @s) (tag - 1) slots

-- | Evidence that every constructor of the sum row @s@ has a heterogeneous
-- field row satisfying 'AllFromDhall', paired with its constructor tag index.
class AllCtorsFromDhall (s :: SumRow) where
  expectedUnionAlts :: InputNormalizer -> Proxy s -> Expector [(Text, Maybe (Expr Src Void))]
  decVariantVal :: InputNormalizer -> Proxy s -> Word -> Text -> Expr Src Void -> Extractor Src Void (Word, [Identity Any])

instance AllCtorsFromDhall '[] where
  expectedUnionAlts _ _ = pure []
  decVariantVal _ _ _ cName _ = extractError ("Unknown variant constructor: " <> cName)

instance (KnownSymbol c, AllFromDhall fs, DecSingleField fs, AllCtorsFromDhall s)
      => AllCtorsFromDhall ((c ':= fs) ': s) where
  expectedUnionAlts norm _ =
    let cName = T.pack (symbolVal (Proxy @c))
        fsExpected = expectedRecordFields norm (Proxy @fs)
        altType = fsExpected <&> \case
                    [] -> Nothing
                    fields -> Just (Record (Dhall.Map.fromList fields))
        restAlts = expectedUnionAlts norm (Proxy @s)
    in liftA2 (\alt rest -> (cName, alt) : rest) altType restAlts

  decVariantVal norm _ idx cKey val
    | cKey == T.pack (symbolVal (Proxy @c)) = case val of
        RecordLit kvs ->
          (\slots -> (idx, slots)) <$> decRecordFields norm (Proxy @fs) kvs
        _ -> case decRecordFieldsSingle norm (Proxy @fs) val of
               Just slotsExtractor ->
                 (\slots -> (idx, slots)) <$> slotsExtractor
               Nothing -> extractError ("Expected a record literal of fields for constructor: " <> cKey)
    | otherwise =
        decVariantVal norm (Proxy @s) (idx + 1) cKey val

{-------------------------------------------------------------------------------
  Record Codecs
-------------------------------------------------------------------------------}

instance AllToDhall r => ToDhall (Record r) where
  injectWith norm = Encoder
    { embed = \(RowRep rep) ->
        RecordLit (Dhall.Map.fromList (encRecordFields norm (Proxy @r) (toList (repVals rep))))
    , declared = Record (Dhall.Map.fromList (declaredRecordFields norm (Proxy @r)))
    }

instance AllFromDhall r => FromDhall (Record r) where
  autoWith norm =
    let expectedFields = expectedRecordFields norm (Proxy @r)
        expectedRec = Record . Dhall.Map.fromList <$> expectedFields
    in Decoder
      { extract = \expr -> case expr of
          RecordLit kvs -> RowRep . productRep <$> decRecordFields norm (Proxy @r) kvs
          _             -> typeError expectedRec expr
      , expected = expectedRec
      }

{-------------------------------------------------------------------------------
  Variant Codecs
-------------------------------------------------------------------------------}

-- | Deconstruct an AST node into its constructor name and payload expression.
extractUnionTag :: Expr s a -> Maybe (Text, Expr s a)
extractUnionTag = \case
  App (Field _ (FieldSelection _ fld _)) e -> Just (fld, e)
  Field _ (FieldSelection _ fld _)        -> Just (fld, RecordLit mempty)
  _                                       -> Nothing

instance AllCtorsToDhall s => ToDhall (Variant s) where
  injectWith norm =
    let alts = Dhall.Map.fromList (declaredUnionAlts norm (Proxy @s))
    in Encoder
      { embed = \(RowRep rep) ->
          encVariantTag norm alts (Proxy @s) (repTag rep) (toList (repVals rep))
      , declared = Union alts
      }

instance AllCtorsFromDhall s => FromDhall (Variant s) where
  autoWith norm =
    let expectedU = Union . Dhall.Map.fromList <$> expectedUnionAlts norm (Proxy @s)
    in Decoder
      { extract = \expr -> case extractUnionTag expr of
          Just (cKey, val) ->
            (\(idx, slots) -> RowRep (injectRep (fromIntegral idx) slots))
              <$> decVariantVal norm (Proxy @s) 0 cKey val
          Nothing -> typeError expectedU expr
      , expected = expectedU
      }

{-------------------------------------------------------------------------------
  Higher-Kinded Data Codecs (RecordF f r)
-------------------------------------------------------------------------------}

-- | Evidence that every field of an HKD row @r@ has a 'ToDhall' instance
-- for the functor-wrapped payload @f a@.
class AllToDhallHKD (f :: Type -> Type) (r :: ProductRow f) where
  declaredRecordFieldsHKD :: InputNormalizer -> Proxy f -> Proxy r -> [(Text, Core.RecordField Src Void)]
  encRecordFieldsHKD :: InputNormalizer -> Proxy f -> Proxy r -> [f Any] -> [(Text, Core.RecordField Src Void)]

instance AllToDhallHKD f '[] where
  declaredRecordFieldsHKD _ _ _ = []
  encRecordFieldsHKD _ _ _ _ = []

instance (KnownSymbol n, ToDhall (f a), AllToDhallHKD f r)
      => AllToDhallHKD f ((n ':= f a) ': r) where
  declaredRecordFieldsHKD norm pf _ =
    let key = T.pack (symbolVal (Proxy @n))
        Encoder _ declA = injectWith norm :: Encoder (f a)
    in (key, Core.makeRecordField declA) : declaredRecordFieldsHKD norm pf (Proxy @r)

  encRecordFieldsHKD norm pf _ (x : xs) =
    let key = T.pack (symbolVal (Proxy @n))
        Encoder embedA _ = injectWith norm :: Encoder (f a)
    in (key, Core.makeRecordField (embedA (noInlineUnsafeCo x :: f a)))
       : encRecordFieldsHKD norm pf (Proxy @r) xs
  encRecordFieldsHKD _ _ _ [] = error "polytropos-dhall: HKD payload arity mismatch"

-- | Evidence that every field of an HKD row @r@ has a 'FromDhall' instance
-- for the functor-wrapped payload @f a@.
class AllFromDhallHKD (f :: Type -> Type) (r :: ProductRow f) where
  expectedRecordFieldsHKD :: InputNormalizer -> Proxy f -> Proxy r -> Expector [(Text, Core.RecordField Src Void)]
  decRecordFieldsHKD :: InputNormalizer -> Proxy f -> Proxy r -> Dhall.Map.Map Text (Core.RecordField Src Void) -> Extractor Src Void [f Any]

instance AllFromDhallHKD f '[] where
  expectedRecordFieldsHKD _ _ _ = pure []
  decRecordFieldsHKD _ _ _ _ = pure []

instance (KnownSymbol n, FromDhall (f a), DhallFieldFallback (f a), AllFromDhallHKD f r)
      => AllFromDhallHKD f ((n ':= f a) ': r) where
  expectedRecordFieldsHKD norm pf _ =
    let key = T.pack (symbolVal (Proxy @n))
        Decoder _ expectedA = autoWith norm :: Decoder (f a)
        expectedRest = expectedRecordFieldsHKD norm pf (Proxy @r)
    in liftA2 (\tyA rest -> (key, Core.makeRecordField tyA) : rest) expectedA expectedRest

  decRecordFieldsHKD norm pf _ kvs =
    let key = T.pack (symbolVal (Proxy @n))
        Decoder extractA _ = autoWith norm :: Decoder (f a)
        valA = case Dhall.Map.lookup key kvs of
                 Just rf -> extractA (Core.recordFieldValue rf)
                 Nothing -> case dhallMissingField @(f a) of
                              Just fallback -> fallback
                              Nothing -> extractError ("Record is missing required field: " <> key)
        valRest = decRecordFieldsHKD norm pf (Proxy @r) kvs
    in liftA2 (\x xs -> (noInlineUnsafeCo x :: f Any) : xs) valA valRest

instance AllToDhallHKD f r => ToDhall (RecordF f r) where
  injectWith norm = Encoder
    { embed = \(RowRep rep) ->
        RecordLit (Dhall.Map.fromList (encRecordFieldsHKD norm (Proxy @f) (Proxy @r) (toList (repVals rep))))
    , declared = Record (Dhall.Map.fromList (declaredRecordFieldsHKD norm (Proxy @f) (Proxy @r)))
    }

instance AllFromDhallHKD f r => FromDhall (RecordF f r) where
  autoWith norm =
    let expectedFields = expectedRecordFieldsHKD norm (Proxy @f) (Proxy @r)
        expectedRec = Record . Dhall.Map.fromList <$> expectedFields
    in Decoder
      { extract = \expr -> case expr of
          RecordLit kvs -> RowRep . Rep 0 . smallArrayFromList <$> decRecordFieldsHKD norm (Proxy @f) (Proxy @r) kvs
          _             -> typeError expectedRec expr
      , expected = expectedRec
      }

{-------------------------------------------------------------------------------
  Generic Adapters & DerivingVia
-------------------------------------------------------------------------------}

-- | Newtype wrapper providing generic 'FromDhall' and 'ToDhall' instances
-- for any data type with a 'Generic' instance, via 'GenericFromDhall'
-- and 'GenericToDhall'.
--
-- Usage with @DerivingVia@:
--
-- > data Config = Config
-- >   { host :: Text
-- >   , port :: Natural
-- >   } deriving stock (Generic, Show, Eq)
-- >     deriving (FromDhall, ToDhall) via (GenericDhall Config)
newtype GenericDhall a = GenericDhall { unGenericDhall :: a }

instance (Generic a, GenericFromDhall a (GHC.Rep a)) => FromDhall (GenericDhall a) where
  autoWith norm = fmap GenericDhall (genericAutoWithInputNormalizer defaultInterpretOptions norm)

instance (Generic a, GenericToDhall (GHC.Rep a)) => ToDhall (GenericDhall a) where
  injectWith norm = contramap unGenericDhall (genericToDhallWithInputNormalizer defaultInterpretOptions norm)

-- | Construct a 'Decoder' for any 'Generic' data type using Dhall's generic adapter
-- 'GenericFromDhall'.
genericFromDhall :: forall a. (Generic a, GenericFromDhall a (GHC.Rep a)) => Decoder a
genericFromDhall = genericAuto

-- | Construct a 'Decoder' for any 'Generic' data type using custom 'InterpretOptions'
-- and 'InputNormalizer'.
genericFromDhallWith
  :: forall a. (Generic a, GenericFromDhall a (GHC.Rep a))
  => InterpretOptions
  -> InputNormalizer
  -> Decoder a
genericFromDhallWith = genericAutoWithInputNormalizer


{-------------------------------------------------------------------------------
  Homogeneity evidence and row introspection (Reflection helpers)
-------------------------------------------------------------------------------}

-- $homogeneity
--
-- === Homogeneity Evidence and Introspection Helpers
--
-- In addition to the primary heterogeneous codecs ('AllToDhall', 'AllFromDhall'),
-- this module provides closed type families and reflection classes for
-- inspecting row metadata and enforcing uniform payload types.
--
-- 1. 'AllFieldsAre' is a closed type family asserting that /every/ field in a row
--    has the uniform type @a@.
-- 2. 'RowField' and 'SumField' compute the uniform payload type directly from
--    the row or sum row, eliminating type ambiguity for custom instances.
-- 3. 'FieldNames' reifies type-level 'GHC.TypeLits.Symbol' field labels into
--    runtime @['String']@ keys in exact row order.
-- 4. 'AllCtorsHom' reifies the constructor topology of a homogeneous sum row into
--    @[(String, [String])]@ (mapping each constructor name to its list of field names).

-- | Evidence that every field of the row @r@ has type @a@.
type family AllFieldsAre (a :: Type) (r :: Row) :: Constraint where
  AllFieldsAre _  '[]                = ()
  AllFieldsAre a ((n ':= a) ': rest) = AllFieldsAre a rest

-- | Extracts the uniform field type of a homogeneous row: the type of /every/
-- field, or 'Void' for the empty row @'[]@.
type family RowField (r :: Row) :: Type where
  RowField '[]                 = Void
  RowField ((n ':= a) ': rest) = a

-- | Extracts the uniform field type shared by every constructor of a homogeneous
-- sum row, or 'Void' for the empty sum @'[]@.
type family SumField (s :: SumRow) :: Type where
  SumField '[]                = Void
  SumField ((c ':= fs) ': s)  = RowField fs

-- | The field names of a row, in exact row order, reified from type-level
-- 'GHC.TypeLits.Symbol' labels into runtime @['String']@ keys.
class FieldNames (r :: Row) where
  fieldNames :: Proxy r -> [String]

instance FieldNames '[] where
  fieldNames _ = []

instance (KnownSymbol n, FieldNames r) => FieldNames ((n ':= a) ': r) where
  fieldNames _ = symbolVal (Proxy @n) : fieldNames (Proxy @r)

-- | Reflection dictionary for homogeneous sum rows (variants).
--
-- Reifies the full constructor topology into a runtime list pairing each
-- constructor symbol with its ordered field symbols: @[(String, [String])]@.
class AllCtorsHom (a :: Type) (s :: SumRow) where
  ctorSpecs :: Proxy a -> Proxy s -> [(String, [String])]

instance AllCtorsHom a '[] where
  ctorSpecs _ _ = []

instance (KnownSymbol c, FieldNames fs, AllFieldsAre a fs, AllCtorsHom a s)
      => AllCtorsHom a ((c ':= fs) ': s) where
  ctorSpecs pa _ =
    (symbolVal (Proxy @c), fieldNames (Proxy @fs)) : ctorSpecs pa (Proxy @s)