packages feed

hs-bindgen-1.0.0.0: src-internal/HsBindgen/BindingSpec/Private/V1.hs

{-# LANGUAGE TemplateHaskell #-}

-- | Binding specification
--
-- This /private/ module may only be used by "HsBindgen.BindingSpec" and
-- sub-modules.
--
-- Intended for qualified import.
--
-- When defining the current public interface:
--
-- > import HsBindgen.BindingSpec.Private.V1 qualified as BindingSpec
--
-- When distinguishing separate versions:
--
-- > import HsBindgen.BindingSpec.Private.V1 (V1)
-- > import HsBindgen.BindingSpec.Private.V1 qualified as V1
module HsBindgen.BindingSpec.Private.V1 (
    -- * Version
    currentBindingSpecVersion
    -- * Types
  , BindingSpec(..)
  , UnresolvedBindingSpec
  , ResolvedBindingSpec
  , CTypeSpec(..)
  , CEnumSpec(..)
  , HsTypeSpec(..)
  , HsTypeRep(..)
  , HsRecordRep(..)
  , HsNewtypeRep(..)
  , HsFFIType(..)
  , hsSpecFFIType
    -- ** Instances
  , InstanceSpec(..)
    -- * API
  , empty
  , getCTypes
  , lookupCTypeSpec
  , lookupHsTypeSpec
    -- ** YAML/JSON
  , readFile
  , encode
    -- ** Header resolution
  , resolve
    -- ** Merging
  , MergedBindingSpecs
  , merge
  , lookupMergedBindingSpecs
    -- ** Aeson representation
  , V1
  ) where

import Prelude hiding (readFile)

import Data.Aeson ((.!=), (.:), (.:?), (.=))
import Data.Aeson qualified as Aeson
import Data.Aeson.KeyMap qualified as KM
import Data.Aeson.Types qualified as Aeson
import Data.ByteString (ByteString)
import Data.ByteString.Lazy qualified as BSL
import Data.Function (on)
import Data.List qualified as List
import Data.Map.Strict qualified as Map
import Data.Ord qualified as Ord
import Data.Set qualified as Set
import Data.Text qualified as Text
import Data.Yaml.Pretty qualified
import Text.Read (readMaybe)
import Text.SimplePrettyPrint qualified as PP

import Clang.Args
import Clang.Paths

import HsBindgen.BindingSpec.Private.Common
import HsBindgen.BindingSpec.Private.Version
import HsBindgen.Config.MangleCandidate (MangleCandidate (..))
import HsBindgen.Config.MangleCandidate qualified as MangleCandidate
import HsBindgen.Errors
import HsBindgen.Imports
import HsBindgen.Instances qualified as Inst
import HsBindgen.IR.C qualified as C
import HsBindgen.Language.Haskell qualified as Hs
import HsBindgen.Orphans ()
import HsBindgen.Resolve
import HsBindgen.Util.Monad
import HsBindgen.Util.Tracer

{-------------------------------------------------------------------------------
  Version
-------------------------------------------------------------------------------}

-- | Binding specification version
currentBindingSpecVersion :: BindingSpecVersion
currentBindingSpecVersion = $$(constBindingSpecVersion 1 0)

{-------------------------------------------------------------------------------
  Types
-------------------------------------------------------------------------------}

-- | Binding specification
--
-- This type serves two purposes:
--
-- * A /prescriptive binding specification/ is used to configure how bindings
--   are generated.
-- * An /external binding specification/ is used to specify existing bindings
--   that should be used, /external/ from the module being generated.
--
-- Note that a /generated binding specification/ may be used for either/both of
-- these two purposes.
--
-- The @header@ type parameter determines the representation of header paths.
-- See 'UnresolvedBindingSpec' and 'ResolvedBindingSpec'.
data BindingSpec header = BindingSpec {
      -- | Binding specification module
      --
      -- Each binding specification is specific to a Haskell module.
      --
      -- The module name is optional in prescriptive binding specifications.  If
      -- one is specified, it must match the current module.  If not specified,
      -- the current module is used.
      --
      -- The module name is required in external binding specifications.
      moduleName :: Hs.ModuleName

      -- | C type specifications
      --
      -- A C type is identified using a 'C.DeclId' and a set of headers that
      -- provide the type.  For a given 'C.DeclId', the sets of headers are
      -- disjoint.  The type of this map is therefore equivalent to
      -- @'Map' 'C.DeclId' ('Map' header ('Omittable' t'CTypeSpec'))@, but this
      -- type is used as an optimization.
    , cTypes :: Map C.DeclId [(Set header, Omittable CTypeSpec)]

      -- | Haskell type specifications
    , hsTypes :: Map (Hs.Name Hs.NsTypeConstr) HsTypeSpec
    }
  deriving stock (Eq, Generic, Show)

-- | Binding specification with unresolved headers
--
-- The headers are as specified in a C include directive, relative to a
-- directory in the C include search path.
type UnresolvedBindingSpec = BindingSpec C.HashIncludeArg

-- | Binding specification with resolved headers
--
-- The resolved header is the canonical on-disk path in the current environment.
type ResolvedBindingSpec = BindingSpec (C.HashIncludeArg, RealPath)

--------------------------------------------------------------------------------

-- | Binding specification for a C type
data CTypeSpec = CTypeSpec {
      hsName :: Maybe (Hs.Name Hs.NsTypeConstr)
    , enum   :: Maybe CEnumSpec -- ^ Specified for C @enum@ types
    }
  deriving stock (Show, Eq, Ord, Generic)

--------------------------------------------------------------------------------

-- | C @enum@ specification
data CEnumSpec =
    CEnumOpen   -- ^ C @enum@ may have values other than those declared
  | CEnumClosed -- ^ C @enum@ may only have declared vlaues
  deriving stock (Bounded, Enum, Eq, Generic, Ord, Show)

instance Default CEnumSpec where
  def = CEnumOpen

--------------------------------------------------------------------------------

-- | Binding specification for a Haskell type
data HsTypeSpec = HsTypeSpec {
      -- | Haskell type representation
      hsRep :: Maybe HsTypeRep

       -- | Instance specification
    , instances :: Map Inst.TypeClass (Omittable InstanceSpec)
    }
  deriving stock (Show, Eq, Ord, Generic)

instance Default HsTypeSpec where
  def = HsTypeSpec{
      hsRep     = Nothing
    , instances = Map.empty
    }

hsSpecFFIType :: HsTypeSpec -> Maybe HsFFIType
hsSpecFFIType hsSpec = do
    rep <- hsSpec.hsRep
    case rep of
      HsTypeRepNewtype ntRep -> ntRep.ffiType
      _ -> Nothing

--------------------------------------------------------------------------------

-- | Haskell type representation
data HsTypeRep =
    -- | Record representation
    --
    -- A type and constructor is generated using @data@.
    HsTypeRepRecord HsRecordRep

  | -- | Newtype representation
    --
    -- A type and constructor is generated using @newtype@.
    HsTypeRepNewtype HsNewtypeRep

  | -- | Empty data representation
    --
    -- A type but no constructor is generated using @data@.
    HsTypeRepEmptyData

  | -- | Type alias representation
    --
    -- A type is generated using @type@.
    HsTypeRepTypeAlias
  deriving stock (Show, Eq, Ord, Generic)

-- | Haskell record representation
data HsRecordRep = HsRecordRep {
      constructor :: Maybe (Hs.Name Hs.NsConstr)
    , fields      :: Maybe [Hs.Name Hs.NsVar]
    }
  deriving stock (Show, Eq, Ord, Generic)

instance Default HsRecordRep where
  def = HsRecordRep{
        constructor = Nothing
      , fields      = Nothing
      }

-- | Haskell newtype representation
data HsNewtypeRep = HsNewtypeRep {
      -- | Constructor name
      constructor :: Maybe (Hs.Name Hs.NsConstr)

      -- | Field name
    , field :: Maybe (Hs.Name Hs.NsVar)

      -- | FFI type
    , ffiType :: Maybe HsFFIType
    }
  deriving stock (Show, Eq, Ord, Generic)

data HsFFIType = HsFFIType { unwrap :: Hs.ExtRef }
  deriving stock (Show, Eq, Ord, Generic)

instance Default HsNewtypeRep where
  def = HsNewtypeRep{
        constructor = Nothing
      , field       = Nothing
      , ffiType     = Nothing
      }

{-------------------------------------------------------------------------------
  Types: Instances
-------------------------------------------------------------------------------}

-- | Instance specification
data InstanceSpec = InstanceSpec {
      -- | Strategy used to generate/derive the instance
      --
      -- A 'Nothing' value indicates that @hs-bindgen@ defaults should be used.
      strategy :: Maybe Inst.Strategy

      -- | Instance constraints
      --
      -- If specified, /all/ constraints must be listed.
    , constraints :: [Inst.Constraint]
    }
  deriving stock (Show, Eq, Ord, Generic)

instance Default InstanceSpec where
  def = InstanceSpec{
      strategy    = Nothing
    , constraints = []
    }

{-------------------------------------------------------------------------------
  API
-------------------------------------------------------------------------------}

-- | Construct an empty binding specification for the given module
empty :: Hs.ModuleName -> BindingSpec header
empty hsModuleName = BindingSpec{
      moduleName = hsModuleName
    , cTypes     = Map.empty
    , hsTypes    = Map.empty
    }

-- | Get the C types in a binding specification
getCTypes :: ResolvedBindingSpec -> Map C.DeclId [Set RealPath]
getCTypes spec = map (Set.map snd . fst) <$> spec.cTypes

-- | Lookup a C type in a 'ResolvedBindingSpec'
lookupCTypeSpec ::
     C.DeclId
  -> Set RealPath
  -> ResolvedBindingSpec
  -> Maybe (Hs.ModuleName, Omittable CTypeSpec)
lookupCTypeSpec cDeclId headers spec = do
    ps <- Map.lookup cDeclId spec.cTypes
    oCTypeSpec <- lookupBy (not . Set.disjoint headers . Set.map snd) ps
    return (spec.moduleName, oCTypeSpec)

-- | Lookup a Haskell type in a 'ResolvedBindingSpec'
lookupHsTypeSpec ::
     Hs.Name Hs.NsTypeConstr
  -> ResolvedBindingSpec
  -> Maybe HsTypeSpec
lookupHsTypeSpec hsName spec = Map.lookup hsName spec.hsTypes

{-------------------------------------------------------------------------------
  API: YAML/JSON
-------------------------------------------------------------------------------}

-- | Read a binding specification file
--
-- The format is determined by the filename extension.
readFile ::
     Tracer BindingSpecReadMsg
  -> BindingSpecCompatibility
  -> Maybe Hs.ModuleName
  -> FilePath
  -> IO (Maybe UnresolvedBindingSpec)
readFile tracer cmpt mHsModuleName path = readVersion tracer path >>= \case
    Nothing -> return Nothing
    Just (aVersion, value)
      | isCompatBindingSpecVersions
          cmpt
          aVersion.bindingSpec
          currentBindingSpecVersion -> do
            traceWith tracer $ withCallStack $ BindingSpecReadParseVersion path aVersion
            case Aeson.fromJSON value of
              Aeson.Success arep ->
                case fromABindingSpec mHsModuleName path arep of
                  Right (errs, spec) -> do
                    mapM_ (traceWith tracer . withCallStack) errs
                    return (Just spec)
                  Left err -> do
                    traceWith tracer (withCallStack err)
                    return Nothing
              Aeson.Error err -> do
                traceWith tracer $ withCallStack $ BindingSpecReadAesonError path err
                return Nothing
      | otherwise -> do
          traceWith tracer $ withCallStack $ BindingSpecReadIncompatibleVersion path aVersion
          return Nothing

-- | Encode a binding specification
encode ::
     (C.DeclId -> C.DeclId -> Ordering)
  -> Format
  -> UnresolvedBindingSpec
  -> ByteString
encode compareCDeclId = \case
    FormatJSON -> encodeJson' . toABindingSpec compareCDeclId
    FormatYAML -> encodeYaml' . toABindingSpec compareCDeclId

encodeJson' :: ARep V1 UnresolvedBindingSpec -> ByteString
encodeJson' = BSL.toStrict . Aeson.encode

encodeYaml' :: ARep V1 UnresolvedBindingSpec -> ByteString
encodeYaml' = Data.Yaml.Pretty.encodePretty yamlConfig
  where
    yamlConfig :: Data.Yaml.Pretty.Config
    yamlConfig =
          Data.Yaml.Pretty.setConfCompare (compare `on` keyPosition)
        $ Data.Yaml.Pretty.defConfig

    keyPosition :: Text -> Int
    keyPosition = \case
      -- ABindingSpec:1
      "version"               ->  0
      -- AVersion:1
      "hs_bindgen"            ->  1
      -- AVersion:2
      "binding_specification" ->  2
      -- AOmittable:1
      "omit"                  ->  3
      -- AInstanceSpec:1, AConstraintSpec:1
      "class"                 ->  4
      -- AInstanceSpec:2
      "strategy"              ->  5
      -- AInstanceSpec:3
      "constraints"           ->  6
      -- ABindingSpec:2, AConstraintSpec:2, HsFFIType:1
      "hsmodule"              ->  7
      -- ABindingSpec:3
      "ctypes"                ->  8
      -- ABindingSpec:4
      "hstypes"               ->  9
      -- ACTypeSpec:1
      "headers"               -> 10
      -- ACTypeSpec:2
      "cname"                 -> 11
      -- ACTypeSpec:3, AHsTypeSpec:1, AConstraintSpec:3, HsFFIType:2
      "hsname"                -> 12
      -- ACTypeSpec:4
      "enum"                  -> 13
      -- AHsTypeSpec:2
      "representation"        -> 14
      -- AHsTypeSpec:3
      "instances"             -> 15
      -- AHsTypeRep:1
      "record"                -> 16
      -- AHsTypeRep:2
      "newtype"               -> 17
      -- HsRecordRep:1, HsNewtypeRep:1
      "constructor"           -> 18
      -- HsRecordRep:2, HsNewtypeRep:2
      "fields"                -> 19
      -- HsNewtypeRep:3
      "ffitype"               -> 20
      key -> panicPure $ "Unknown key: " ++ show key

{-------------------------------------------------------------------------------
  API: Header resolution
-------------------------------------------------------------------------------}

-- | Resolve headers in a binding specification
resolve ::
     Tracer BindingSpecResolveMsg
  -> (ResolveHeaderMsg -> BindingSpecResolveMsg)
  -> ClangArgs
  -> UnresolvedBindingSpec
  -> IO ResolvedBindingSpec
resolve tracer injResolveHeader args uSpec = do
    headerMap <-
      resolveHeaders (contramap injResolveHeader tracer) args allHeaders

    let lookup' :: C.HashIncludeArg -> Maybe (C.HashIncludeArg, RealPath)
        lookup' uHeader = (uHeader,) <$> Map.lookup uHeader headerMap

        resolveSet ::
             Set C.HashIncludeArg
          -> Maybe (Set (C.HashIncludeArg, RealPath))
        resolveSet uHeaders =
          case mapMaybe lookup' (Set.toList uHeaders) of
            []       -> Nothing
            rHeaders -> Just (Set.fromList rHeaders)

        resolveType ::
             C.DeclId
          -> (Set C.HashIncludeArg, a)
          -> IO (Maybe (Set (C.HashIncludeArg, RealPath), a))
        resolveType cDeclId (uHeaders, x) = case resolveSet uHeaders of
          Just rHeaders -> return $ Just (rHeaders, x)
          Nothing       -> do
            traceWith tracer $ withCallStack $ BindingSpecResolveTypeDropped cDeclId
            return Nothing

        resolveTypes ::
             C.DeclId
          -> [(Set C.HashIncludeArg, a)]
          -> IO
               ( Maybe
                   (C.DeclId, [(Set (C.HashIncludeArg, RealPath), a)])
               )
        resolveTypes cDeclId uKVs =
          mapMaybeM (resolveType cDeclId) uKVs >>= \case
            rKVs
              | null rKVs -> return Nothing
              | otherwise -> return $ Just (cDeclId, rKVs)

    cTypes <- Map.fromList <$>
      mapMaybeM (uncurry resolveTypes) (Map.toList uSpec.cTypes)
    return BindingSpec{
        moduleName = uSpec.moduleName
      , cTypes     = cTypes
      , hsTypes    = uSpec.hsTypes
      }
  where
    allHeaders :: Set C.HashIncludeArg
    allHeaders = mconcat $ fst <$> concat (Map.elems uSpec.cTypes)

{-------------------------------------------------------------------------------
  API: Merging
-------------------------------------------------------------------------------}

-- | Merged (external) binding specifications
--
-- While a tt'HsBindgen.BindingSpec.Private.V1.BindingSpec' is specific to a Haskell module, this type supports
-- binding specifications across multiple Haskell modules.  It is a performance
-- optimization for resolving external binding specifications.
newtype MergedBindingSpecs = MergedBindingSpecs {
      map :: Map C.DeclId [(Set RealPath, ResolvedBindingSpec)]
    }
  deriving stock (Show)

-- | Merge (external) binding specifications
merge ::
     [ResolvedBindingSpec]
  -> ([BindingSpecMergeMsg], MergedBindingSpecs)
merge =
      bimap mkTypeErrs (MergedBindingSpecs . snd)
    . foldl' mergeSpec (Set.empty, (Map.empty, Map.empty))
  where
    mkTypeErrs :: Set C.DeclId -> [BindingSpecMergeMsg]
    mkTypeErrs = fmap BindingSpecMergeConflict . Set.toList

    mergeSpec ::
         ( Set C.DeclId
         , ( Map C.DeclId (Set C.HashIncludeArg)
           , Map C.DeclId [(Set RealPath, ResolvedBindingSpec)]
           )
         )
      -> ResolvedBindingSpec
      -> ( Set C.DeclId
         , ( Map C.DeclId (Set C.HashIncludeArg)
           , Map C.DeclId [(Set RealPath, ResolvedBindingSpec)]
           )
         )
    mergeSpec ctx spec =
        foldl' (mergeType spec) ctx
      . map (fmap (map fst))
      $ Map.toList spec.cTypes

    mergeType ::
         ResolvedBindingSpec
      -> ( Set C.DeclId
         , ( Map C.DeclId (Set C.HashIncludeArg)
           , Map C.DeclId [(Set RealPath, ResolvedBindingSpec)]
           )
         )
      -> (C.DeclId, [Set (C.HashIncludeArg, RealPath)])
      -> ( Set C.DeclId
         , ( Map C.DeclId (Set C.HashIncludeArg)
           , Map C.DeclId [(Set RealPath, ResolvedBindingSpec)]
           )
         )
    mergeType spec (dupSet, (seenMap, acc)) (cDeclId, sourceSets) =
      let seenS = Set.unions $ map (Set.map fst) sourceSets
          keyS  = Set.unions $ map (Set.map snd) sourceSets
          acc'  = Map.insertWith (++) cDeclId [(keyS, spec)] acc
      in  case Map.insertLookupWithKey (const (<>)) cDeclId seenS seenMap of
            (Nothing, seenMap') -> (dupSet, (seenMap', acc'))
            (Just eS, seenMap')
              | Set.disjoint eS seenS -> (dupSet, (seenMap', acc'))
              | otherwise -> (Set.insert cDeclId dupSet, (seenMap', acc'))

-- | Lookup type specs in t'MergedBindingSpecs'
lookupMergedBindingSpecs ::
     C.DeclId
  -> Set RealPath
  -> MergedBindingSpecs
  -> Maybe (Hs.ModuleName, Omittable CTypeSpec, Maybe HsTypeSpec)
lookupMergedBindingSpecs cDeclId headers specs = do
    spec <- lookupBy (not . Set.disjoint headers)
      =<< Map.lookup cDeclId specs.map
    (hsModuleName, oCTypeSpec) <- lookupCTypeSpec cDeclId headers spec
    let mHsTypeSpec = case oCTypeSpec of
          Require cTypeSpec -> do
            hsName <- cTypeSpec.hsName
            lookupHsTypeSpec hsName spec
          Omit -> Nothing
    return (hsModuleName, oCTypeSpec, mHsTypeSpec)

{-------------------------------------------------------------------------------
  Aeson representation
-------------------------------------------------------------------------------}

-- | Aeson representation version
type V1 :: ARepV
data V1 a

-- | Convert from the Aeson representation /for this version/
fromARep' :: ARepIso V1 a => ARep V1 a -> a
fromARep' = fromARep

-- | Convert to the Aeson representation /for this version/
toARep' :: ARepIso V1 a => a -> ARep V1 a
toARep' = toARep

--------------------------------------------------------------------------------

data instance ARep V1 UnresolvedBindingSpec = ABindingSpec {
      version  :: AVersion
    , hsModule :: Maybe Hs.ModuleName
    , cTypes   :: [AOCTypeSpec]
    , hsTypes  :: [ARep V1 HsTypeSpec]
    }
  deriving stock (Show)

instance Aeson.FromJSON (ARep V1 UnresolvedBindingSpec) where
  parseJSON = Aeson.withObject "BindingSpec" $ \o -> do
    aBindingSpecVersion  <- o .:  "version"
    aBindingSpecHsModule <- o .:? "hsmodule"
    aBindingSpecCTypes   <- o .:? "ctypes"  .!= []
    aBindingSpecHsTypes  <- o .:? "hstypes" .!= []
    return ABindingSpec{
        version  = aBindingSpecVersion
      , hsModule = fromARep' <$> aBindingSpecHsModule
      , cTypes   = aBindingSpecCTypes
      , hsTypes  = aBindingSpecHsTypes
      }

instance Aeson.ToJSON (ARep V1 UnresolvedBindingSpec) where
  toJSON spec = Aeson.Object . KM.fromList $ catMaybes [
      Just ("version" .= spec.version)
    , ("hsmodule" .=) . toARep' <$> spec.hsModule
    , ("ctypes"   .=) <$> omitWhenNull spec.cTypes
    , ("hstypes"  .=) <$> omitWhenNull spec.hsTypes
    ]

fromABindingSpec ::
     Maybe Hs.ModuleName
  -> FilePath
  -> ARep V1 UnresolvedBindingSpec
  -> Either BindingSpecReadMsg ([BindingSpecReadMsg], UnresolvedBindingSpec)
fromABindingSpec mHsModuleName path arep = do
    (moduleErrs, hsModuleName) <- case (arep.hsModule, mHsModuleName) of
      (Just bsModule, Just curModule)
        | bsModule == curModule -> return ([], bsModule)
        | otherwise -> return
            ([BindingSpecReadModuleMismatch path bsModule curModule], bsModule)
      (Just bsModule, Nothing) -> return ([], bsModule)
      (Nothing, Just curModule) -> return ([], curModule)
      (Nothing, Nothing) -> Left $ BindingSpecReadModuleNotSpecified path
    let (cTypeErrs, hsNms, bindingSpecCTypes) =
          fromAOCTypeSpecs path arep.cTypes
        (hsTypeErrs, bindingSpecHsTypes) =
          fromAHsTypeSpecs path hsNms arep.hsTypes
    return
      ( moduleErrs ++ cTypeErrs ++ hsTypeErrs
      , BindingSpec{
            moduleName = hsModuleName
          , cTypes     = bindingSpecCTypes
          , hsTypes    = bindingSpecHsTypes
          }
      )

toABindingSpec ::
     (C.DeclId -> C.DeclId -> Ordering)
  -> UnresolvedBindingSpec
  -> ARep V1 UnresolvedBindingSpec
toABindingSpec compareCDeclId spec = ABindingSpec{
      version  = mkAVersion currentBindingSpecVersion
    , hsModule = Just spec.moduleName
    , cTypes   = toAOCTypeSpecs compareCDeclId spec.cTypes
    , hsTypes  = toAHsTypeSpecs spec.hsTypes
    }

--------------------------------------------------------------------------------

newtype instance ARep V1 Hs.ModuleName = AModuleName Hs.ModuleName
  deriving stock (Show)

instance ARepIso V1 Hs.ModuleName

instance Aeson.FromJSON (ARep V1 Hs.ModuleName) where
  parseJSON = Aeson.withText "ModuleName" $ return . AModuleName . Hs.ModuleName

instance Aeson.ToJSON (ARep V1 Hs.ModuleName) where
  toJSON (AModuleName moduleName) = Aeson.String moduleName.text

--------------------------------------------------------------------------------

data instance ARep V1 CTypeSpec = ACTypeSpec {
      headers :: [FilePath]
    , cName   :: Text
    , hsName  :: Maybe (Hs.Name Hs.NsTypeConstr)
    , enum    :: Maybe CEnumSpec
    }
  deriving stock (Show)

instance Aeson.FromJSON (ARep V1 CTypeSpec) where
  parseJSON = Aeson.withObject "CTypeSpec" $ \o -> do
    aCTypeSpecHeaders <- o .:  "headers" >>= listFromJSON
    aCTypeSpecCName   <- o .:  "cname"
    aCTypeSpecHsName  <- o .:? "hsname"
    aCTypeSpecEnum    <- o .:? "enum"
    return ACTypeSpec{
        headers = aCTypeSpecHeaders
      , cName   = aCTypeSpecCName
      , hsName  = fromARep' <$> aCTypeSpecHsName
      , enum    = fromARep' <$> aCTypeSpecEnum
      }

instance Aeson.ToJSON (ARep V1 CTypeSpec) where
  toJSON arep = Aeson.Object . KM.fromList $ catMaybes [
      Just ("headers" .= listToJSON arep.headers)
    , Just ("cname"   .= arep.cName)
    , ("hsname" .=) . toARep' <$> arep.hsName
    , ("enum"   .=) . toARep' <$> arep.enum
    ]

instance ARepKV V1 CTypeSpec where
  data ARepK V1 CTypeSpec = AKCTypeSpec {
      headers :: [FilePath]
    , cName   :: Text
    }

  fromARepKV arep =
    ( AKCTypeSpec{
          headers = arep.headers
        , cName   = arep.cName
        }
    , CTypeSpec{
          hsName = arep.hsName
        , enum   = arep.enum
        }
    )

  toARepKV k v = ACTypeSpec{
      headers = k.headers
    , cName   = k.cName
    , hsName  = v.hsName
    , enum    = v.enum
    }

deriving stock instance Show (ARepK V1 CTypeSpec)

instance Aeson.FromJSON (ARepK V1 CTypeSpec) where
  parseJSON = Aeson.withObject "AKCTypeSpec" $ \o -> do
    akCTypeSpecHeaders <- o .: "headers" >>= listFromJSON
    akCTypeSpecCName   <- o .: "cname"
    return AKCTypeSpec{
        headers = akCTypeSpecHeaders
      , cName   = akCTypeSpecCName
      }

instance Aeson.ToJSON (ARepK V1 CTypeSpec) where
  toJSON key = Aeson.Object $ KM.fromList [
      "headers" .= listToJSON key.headers
    , "cname"   .= key.cName
    ]

type AOCTypeSpec = AOmittable (ARepK V1 CTypeSpec) (ARep V1 CTypeSpec)

fromAOCTypeSpecs ::
     FilePath
  -> [AOCTypeSpec]
  -> ( [BindingSpecReadMsg]
     , Set (Hs.Name Hs.NsTypeConstr)
     , Map C.DeclId [(Set C.HashIncludeArg, Omittable CTypeSpec)]
     )
fromAOCTypeSpecs path =
    fin . foldr auxInsert (Set.empty, [], Map.empty, Set.empty, Map.empty)
  where
    fin ::
         ( Set Text
         , [C.HashIncludeArgMsg]
         , Map C.DeclId (Set C.HashIncludeArg)
         , Set (Hs.Name Hs.NsTypeConstr)
         , Map C.DeclId [(Set C.HashIncludeArg, Omittable CTypeSpec)]
         )
      -> ( [BindingSpecReadMsg]
         , Set (Hs.Name Hs.NsTypeConstr)
         , Map C.DeclId [(Set C.HashIncludeArg, Omittable CTypeSpec)]
         )
    fin (invalids, msgs, conflicts, hsNms, cTypeMap) =
      let invalidErrs = BindingSpecReadInvalidCName path <$> Set.toList invalids
          argErrs = BindingSpecReadHashIncludeArg path <$> msgs
          conflictErrs = [
              BindingSpecReadCTypeConflict path cDeclId header
            | (cDeclId, headers) <- Map.toList conflicts
            , header <- Set.toList headers
            ]
      in  (invalidErrs ++ argErrs ++ conflictErrs, hsNms, cTypeMap)

    auxInsert ::
         AOCTypeSpec
      -> ( Set Text
         , [C.HashIncludeArgMsg]
         , Map C.DeclId (Set C.HashIncludeArg)
         , Set (Hs.Name Hs.NsTypeConstr)
         , Map C.DeclId [(Set C.HashIncludeArg, Omittable CTypeSpec)]
         )
      -> ( Set Text
         , [C.HashIncludeArgMsg]
         , Map C.DeclId (Set C.HashIncludeArg)
         , Set (Hs.Name Hs.NsTypeConstr)
         , Map C.DeclId [(Set C.HashIncludeArg, Omittable CTypeSpec)]
         )
    auxInsert aoCTypeMapping (invalids, msgs, conflicts, hsNms, acc) =
      let (cname, headers, mHsNm, oCTypeSpec) = case aoCTypeMapping of
            ARequire arep ->
              let (k, cTypeSpec) = fromARepKV arep
              in  (k.cName, k.headers, cTypeSpec.hsName, Require cTypeSpec)
            AOmit k -> (k.cName, k.headers, Nothing, Omit)
          (msgs', headers') = bimap ((msgs ++) . concat) Set.fromList $
            unzip (map C.hashIncludeArg headers)
          hsNms' = maybe hsNms (`Set.insert` hsNms) mHsNm
          newV = [(headers', oCTypeSpec)]
      in  case C.parseDeclId cname of
            Nothing ->
              (Set.insert cname invalids, msgs', conflicts, hsNms', acc)
            Just cDeclId ->
              case Map.insertLookupWithKey (const (++)) cDeclId newV acc of
                (Nothing, acc') -> (invalids, msgs', conflicts, hsNms', acc')
                (Just oldV, acc') ->
                  let conflicts' = auxDup cDeclId newV oldV conflicts
                  in  (invalids, msgs', conflicts', hsNms', acc')

    auxDup ::
         C.DeclId
      -> [(Set C.HashIncludeArg, a)]
      -> [(Set C.HashIncludeArg, a)]
      -> Map C.DeclId (Set C.HashIncludeArg)
      -> Map C.DeclId (Set C.HashIncludeArg)
    auxDup cDeclId newV oldV =
      case Set.intersection (mconcat (fst <$> newV)) (mconcat (fst <$> oldV)) of
        commonHeaders
          | Set.null commonHeaders -> id
          | otherwise -> Map.insertWith Set.union cDeclId commonHeaders

toAOCTypeSpecs ::
     (C.DeclId -> C.DeclId -> Ordering)
  -> Map C.DeclId [(Set C.HashIncludeArg, Omittable CTypeSpec)]
  -> [AOCTypeSpec]
toAOCTypeSpecs compareCDeclId cTypeMap = map snd $ List.sortBy aux [
      (cDeclId,) $ case oCTypeSpec of
        Require spec -> ARequire ACTypeSpec{
            headers = map (.path) (Set.toAscList headers)
          , cName   = C.renderDeclId cDeclId
          , hsName  = spec.hsName
          , enum    = spec.enum
          }
        Omit -> AOmit AKCTypeSpec{
            headers = map (.path) (Set.toAscList headers)
          , cName   = C.renderDeclId cDeclId
          }
    | (cDeclId, xs) <- Map.toAscList cTypeMap
    , (headers, oCTypeSpec) <- xs
    ]
  where
    aux :: (C.DeclId, AOCTypeSpec) -> (C.DeclId, AOCTypeSpec) -> Ordering
    aux (cDeclIdL, xL) (cDeclIdR, xR) =
      case compareCDeclId cDeclIdL cDeclIdR of
        LT -> LT
        GT -> GT
        EQ -> Ord.comparing headersOf xL xR

    headersOf :: AOCTypeSpec -> [FilePath]
    headersOf = \case
      ARequire x -> x.headers
      AOmit    x -> x.headers

--------------------------------------------------------------------------------

newtype instance ARep V1 CEnumSpec = ACEnumSpec CEnumSpec
  deriving stock (Show)

instance ARepIso V1 CEnumSpec

instance Aeson.FromJSON (ARep V1 CEnumSpec) where
  parseJSON = Aeson.withText "CEnumSpec" $ \t ->
    case Map.lookup t cEnumSpecFromText of
      Just cEnumSpec -> return (ACEnumSpec cEnumSpec)
      Nothing        -> Aeson.parseFail $
        "unknown C enum specification: " ++ Text.unpack t

instance Aeson.ToJSON (ARep V1 CEnumSpec) where
  toJSON (ACEnumSpec cEnumSpec) = Aeson.String (cEnumSpecToText cEnumSpec)

cEnumSpecToText :: CEnumSpec -> Text
cEnumSpecToText = \case
    CEnumOpen   -> "open"
    CEnumClosed -> "closed"

cEnumSpecFromText :: Map Text CEnumSpec
cEnumSpecFromText = Map.fromList [
      (cEnumSpecToText cEnumSpec, cEnumSpec)
    | cEnumSpec <- [minBound..]
    ]

--------------------------------------------------------------------------------

newtype instance ARep V1 (Hs.Name ns) =
    AHsName (Hs.Name ns)
  deriving stock (Show)

instance ARepIso V1 (Hs.Name ns)

instance Hs.SingNamespace ns => Aeson.FromJSON (ARep V1 (Hs.Name ns)) where
  parseJSON = case Hs.singNamespace @ns of
    Hs.SNsTypeConstr ->
      Aeson.withText "Hs.Name Hs.NsTypeConstr" $ \text ->
        case parseHsName (Proxy @Hs.NsTypeConstr) text of
          Left err ->
            Aeson.parseFail $
              "failed parsing type constructor name: " <> show (prettyForTrace err)
          Right hsName ->
            pure $ AHsName hsName
    Hs.SNsConstr ->
      Aeson.withText "Hs.Name Hs.NsConstr" $ \text ->
        case parseHsName (Proxy @Hs.NsConstr) text of
          Left err ->
            Aeson.parseFail $
              "failed parsing data constructor name: " <> show (prettyForTrace err)
          Right hsName ->
            pure $ AHsName hsName
    Hs.SNsVar ->
      Aeson.withText "Hs.Name Hs.NsVar" $ \text ->
        case parseHsName (Proxy @Hs.NsVar) text of
          Left err ->
            Aeson.parseFail $
              "failed parsing variable name: " <> show (prettyForTrace err)
          Right hsName ->
            pure $ AHsName hsName

instance Aeson.ToJSON (ARep V1 (Hs.Name ns)) where
  toJSON (AHsName hsName) = Aeson.String (hsName.text)

--------------------------------------------------------------------------------

data instance ARep V1 HsTypeSpec = AHsTypeSpec {
      hsName    :: Hs.Name Hs.NsTypeConstr
    , hsRep     :: Maybe HsTypeRep
    , instances :: [AOInstanceSpec]
    }
  deriving stock (Show)

instance Aeson.FromJSON (ARep V1 HsTypeSpec) where
  parseJSON = Aeson.withObject "HsTypeSpec" $ \o -> do
    aHsTypeSpecHsName    <- o .:  "hsname"
    aHsTypeSpecHsRep     <- o .:? "representation"
    aHsTypeSpecInstances <- o .:? "instances" .!= []
    return AHsTypeSpec{
        hsName    = fromARep' aHsTypeSpecHsName
      , hsRep     = fromARep' <$> aHsTypeSpecHsRep
      , instances = aHsTypeSpecInstances
      }

instance Aeson.ToJSON (ARep V1 HsTypeSpec) where
  toJSON arep = Aeson.Object . KM.fromList $ catMaybes [
      Just ("hsname" .= toARep' arep.hsName)
    , ("representation" .=) . toARep' <$> arep.hsRep
    , ("instances" .=) <$> omitWhenNull arep.instances
    ]

instance ARepKV V1 HsTypeSpec where
  newtype ARepK V1 HsTypeSpec = AKHsTypeSpec { unwrap :: Hs.Name Hs.NsTypeConstr }

  fromARepKV arep =
    ( AKHsTypeSpec arep.hsName
    , HsTypeSpec{
          hsRep     = arep.hsRep
        , instances = fromAOInstanceSpecs arep.instances
        }
    )

  toARepKV k v = AHsTypeSpec{
      hsName    = k.unwrap
    , hsRep     = v.hsRep
    , instances = toAOInstanceSpecs v.instances
    }

deriving stock instance Show (ARepK V1 HsTypeSpec)

instance Aeson.FromJSON (ARepK V1 HsTypeSpec) where
  parseJSON = fmap (AKHsTypeSpec . fromARep') . Aeson.parseJSON

instance Aeson.ToJSON (ARepK V1 HsTypeSpec) where
  toJSON = Aeson.toJSON . toARep' . (.unwrap)

fromAHsTypeSpecs ::
     FilePath
  -> Set (Hs.Name Hs.NsTypeConstr)
  -> [ARep V1 HsTypeSpec]
  -> ([BindingSpecReadMsg], Map (Hs.Name Hs.NsTypeConstr) HsTypeSpec)
fromAHsTypeSpecs path hsNms = fin . foldr auxInsert (Set.empty, Map.empty)
  where
    fin ::
         ( Set (Hs.Name Hs.NsTypeConstr)
         , Map (Hs.Name Hs.NsTypeConstr) HsTypeSpec )
      -> ([BindingSpecReadMsg], Map (Hs.Name Hs.NsTypeConstr) HsTypeSpec)
    fin (conflicts, hsTypeMap) =
      let conflictErrs = BindingSpecReadHsTypeConflict path <$>
            Set.toList conflicts
          noRefErrs = BindingSpecReadHsTypeNameNoRef path <$>
            Set.toList (Map.keysSet hsTypeMap `Set.difference` hsNms)
      in  (conflictErrs ++ noRefErrs, hsTypeMap)

    auxInsert ::
         ARep V1 HsTypeSpec
      -> (Set (Hs.Name Hs.NsTypeConstr), Map (Hs.Name Hs.NsTypeConstr) HsTypeSpec)
      -> (Set (Hs.Name Hs.NsTypeConstr), Map (Hs.Name Hs.NsTypeConstr) HsTypeSpec)
    auxInsert arep (conflicts, acc) =
      let (k, hsTypeSpec) = fromARepKV arep
      in  case Map.insertLookupWithKey (\_ n _ -> n) k.unwrap hsTypeSpec acc of
            (Nothing, acc') -> (conflicts,                     acc')
            (Just{},  acc') -> (Set.insert k.unwrap conflicts, acc')

toAHsTypeSpecs ::
     Map (Hs.Name Hs.NsTypeConstr) HsTypeSpec
  -> [ARep V1 HsTypeSpec]
toAHsTypeSpecs hsTypeMap = [
      toARepKV (AKHsTypeSpec hsName) spec
    | (hsName, spec) <- Map.toAscList hsTypeMap
    ]

--------------------------------------------------------------------------------

newtype instance ARep V1 HsTypeRep = AHsTypeRep HsTypeRep
  deriving stock (Show)

instance ARepIso V1 HsTypeRep

instance Aeson.FromJSON (ARep V1 HsTypeRep) where
  parseJSON = fmap AHsTypeRep . parseHsTypeRep
    where
      parseHsTypeRep :: Aeson.Value -> Aeson.Parser HsTypeRep
      parseHsTypeRep = \case
        Aeson.Object o | KM.size o == 1 && KM.member "record" o ->
          HsTypeRepRecord
            <$> Aeson.explicitParseField parseHsRecordRep o "record"
        Aeson.Object o | KM.size o == 1 && KM.member "newtype" o ->
          HsTypeRepNewtype
            <$> Aeson.explicitParseField parseHsNewtypeRep o "newtype"
        v -> Aeson.withText "HsTypeRep" parseHsTypeRepText v

      parseHsTypeRepText :: Text -> Aeson.Parser HsTypeRep
      parseHsTypeRepText t = case t of
        "record"    -> return (HsTypeRepRecord def)
        "newtype"   -> return (HsTypeRepNewtype def)
        "emptydata" -> return HsTypeRepEmptyData
        "typealias" -> return HsTypeRepTypeAlias
        _           ->
          Aeson.parseFail $ "unknown Haskell representation: " ++ Text.unpack t

      parseHsRecordRep :: Aeson.Value -> Aeson.Parser HsRecordRep
      parseHsRecordRep = Aeson.withObject "HsRecordRep" $ \o -> do
        hsRecordRepConstructor <- o .:? "constructor"
        hsRecordRepFields      <- o .:? "fields"
        return HsRecordRep{
            constructor = fromARep' <$> hsRecordRepConstructor
          , fields      = map fromARep' <$> hsRecordRepFields
          }

      parseHsNewtypeRep :: Aeson.Value -> Aeson.Parser HsNewtypeRep
      parseHsNewtypeRep = Aeson.withObject "HsNewtypeRep" $ \o -> do
        hsNewtypeRepConstructor <- o .:? "constructor"
        fields <- o .:? "fields"
        hsNewtypeRepField <- case fields of
          Nothing          -> return Nothing
          Just [fieldName] -> return (Just fieldName)
          Just []          ->
            Aeson.parseFail "newtype representation with no fields"
          Just{}           ->
            Aeson.parseFail "newtype representation with more than one field"
        hsNewtypeRepFFIType <- o .:? "ffitype"
        return HsNewtypeRep{
            constructor = fromARep' <$> hsNewtypeRepConstructor
          , field       = fromARep' <$> hsNewtypeRepField
          , ffiType     = fromARep' <$> hsNewtypeRepFFIType
          }

instance Aeson.ToJSON (ARep V1 HsTypeRep) where
  toJSON (AHsTypeRep hsTypeRep) = case hsTypeRep of
    HsTypeRepRecord x
      | x == def  -> Aeson.String "record"
      | otherwise ->
            Aeson.Object . KM.singleton "record"
          . Aeson.Object . KM.fromList
          $ catMaybes [
                ("constructor" .=) . toARep' <$> x.constructor
              , ("fields"      .=) . map toARep' <$> x.fields
              ]
    HsTypeRepNewtype x
      | x == def  -> Aeson.String "newtype"
      | otherwise ->
            Aeson.Object . KM.singleton "newtype"
          . Aeson.Object . KM.fromList
          $ catMaybes [
                ("constructor" .=) . toARep' <$> x.constructor
              , ("fields"      .=) . (: []) . toARep' <$> x.field
              , ("ffitype"     .=) . toARep' <$> x.ffiType
              ]
    HsTypeRepEmptyData -> Aeson.String "emptydata"
    HsTypeRepTypeAlias -> Aeson.String "typealias"

--------------------------------------------------------------------------------

newtype instance ARep V1 HsFFIType = AHsFFIType HsFFIType
  deriving stock (Show)

instance ARepIso V1 HsFFIType

instance Aeson.FromJSON (ARep V1 HsFFIType) where
  parseJSON = fmap AHsFFIType . parseFFIType
    where
      parseFFIType = Aeson.withObject "HsFFIType" $ \o -> do
          moduleName <- o .: "hsmodule"
          typeName   <- o .: "hsname"
          pure $ HsFFIType $ Hs.ExtRef {
              moduleName = fromARep' moduleName
            , name = fromARep' typeName
            }

instance Aeson.ToJSON (ARep V1 HsFFIType) where
  toJSON (AHsFFIType ffiType) = Aeson.object [
        "hsmodule" .= toARep' ffiType.unwrap.moduleName
      , "hsname"   .= toARep' ffiType.unwrap.name
      ]

--------------------------------------------------------------------------------

data instance ARep V1 InstanceSpec = AInstanceSpec {
      clss        :: Inst.TypeClass
    , strategy    :: Maybe Inst.Strategy
    , constraints :: [Inst.Constraint]
    }
  deriving stock (Show)

instance Aeson.FromJSON (ARep V1 InstanceSpec) where
  parseJSON = \case
    s@Aeson.String{} -> do
      aInstanceSpecClass <- Aeson.parseJSON s
      let aInstanceSpecStrategy    = Nothing
          aInstanceSpecConstraints = []
      return AInstanceSpec{
          clss        = fromARep' aInstanceSpecClass
        , strategy    = aInstanceSpecStrategy
        , constraints = aInstanceSpecConstraints
        }
    Aeson.Object o -> do
      aInstanceSpecClass       <- o .:  "class"
      aInstanceSpecStrategy    <- o .:? "strategy"
      aInstanceSpecConstraints <- o .:? "constraints" .!= []
      return AInstanceSpec{
          clss        = fromARep' aInstanceSpecClass
        , strategy    = fromARep' <$> aInstanceSpecStrategy
        , constraints = fromARep' <$> aInstanceSpecConstraints
        }
    v -> Aeson.parseFail $
      "expected InstanceSpec String or Object, but encountered " ++ typeOf v

instance Aeson.ToJSON (ARep V1 InstanceSpec) where
  toJSON arep
    | isNothing arep.strategy && null arep.constraints =
        Aeson.toJSON (toARep' arep.clss)
    | otherwise = Aeson.Object . KM.fromList $ catMaybes [
          Just ("class" .= toARep' arep.clss)
        , ("strategy"    .=) . toARep' <$> arep.strategy
        , ("constraints" .=) . fmap toARep' <$> omitWhenNull arep.constraints
        ]

instance ARepKV V1 InstanceSpec where
  newtype ARepK V1 InstanceSpec = AKInstanceSpec { unwrap :: Inst.TypeClass }

  fromARepKV arep =
    ( AKInstanceSpec arep.clss
    , InstanceSpec{
          strategy    = arep.strategy
        , constraints = arep.constraints
        }
    )

  toARepKV k v = AInstanceSpec{
      clss        = k.unwrap
    , strategy    = v.strategy
    , constraints = v.constraints
    }

deriving stock instance Show (ARepK V1 InstanceSpec)

instance Aeson.FromJSON (ARepK V1 InstanceSpec) where
  parseJSON = fmap (AKInstanceSpec . fromARep') . Aeson.parseJSON

instance Aeson.ToJSON (ARepK V1 InstanceSpec) where
  toJSON = Aeson.toJSON . toARep' . (.unwrap)

type AOInstanceSpec = AOmittable (ARepK V1 InstanceSpec) (ARep V1 InstanceSpec)

-- duplicates ignored, last value retained
fromAOInstanceSpecs ::
     [AOInstanceSpec]
  -> Map Inst.TypeClass (Omittable InstanceSpec)
fromAOInstanceSpecs xs = Map.fromList . flip map xs $ \case
    ARequire arep -> bimap (.unwrap) Require (fromARepKV arep)
    AOmit    k    -> (k.unwrap, Omit)

toAOInstanceSpecs ::
     Map Inst.TypeClass (Omittable InstanceSpec)
  -> [AOInstanceSpec]
toAOInstanceSpecs instMap = [
      case oInstSpec of
        Require spec -> ARequire $ toARepKV (AKInstanceSpec hsTypeClass) spec
        Omit         -> AOmit (AKInstanceSpec hsTypeClass)
    | (hsTypeClass, oInstSpec) <- Map.toAscList instMap
    ]

--------------------------------------------------------------------------------

newtype instance ARep V1 Inst.TypeClass = ATypeClass Inst.TypeClass
  deriving stock (Show)

instance ARepIso V1 Inst.TypeClass

instance Aeson.FromJSON (ARep V1 Inst.TypeClass) where
  parseJSON = Aeson.withText "TypeClass" $ \t ->
    let s = Text.unpack t
    in  case readMaybe s of
          Just clss -> return (ATypeClass clss)
          Nothing   -> Aeson.parseFail $ "unknown type class: " ++ s

instance Aeson.ToJSON (ARep V1 Inst.TypeClass) where
  toJSON (ATypeClass clss) = Aeson.String $ Text.pack (show clss)

--------------------------------------------------------------------------------

newtype instance ARep V1 Inst.Strategy = AStrategy Inst.Strategy
  deriving stock (Show)

instance ARepIso V1 Inst.Strategy

instance Aeson.FromJSON (ARep V1 Inst.Strategy) where
  parseJSON = Aeson.withText "Strategy" $ \t ->
    case Map.lookup t strategyFromText of
      Just strat -> return (AStrategy strat)
      Nothing    -> Aeson.parseFail $ "unknown strategy: " ++ Text.unpack t

instance Aeson.ToJSON (ARep V1 Inst.Strategy) where
  toJSON (AStrategy strat) = Aeson.String (strategyToText strat)

strategyToText :: Inst.Strategy -> Text
strategyToText = \case
    Inst.HsBindgen -> "hs-bindgen"
    Inst.Newtype   -> "newtype"
    Inst.Stock     -> "stock"

strategyFromText :: Map Text Inst.Strategy
strategyFromText = Map.fromList [
      (strategyToText strat, strat)
    | strat <- [minBound..]
    ]

--------------------------------------------------------------------------------

newtype instance ARep V1 Inst.Constraint = AConstraint Inst.Constraint
  deriving stock (Show)

instance ARepIso V1 Inst.Constraint

instance Aeson.FromJSON (ARep V1 Inst.Constraint) where
  parseJSON = Aeson.withObject "Constraint" $ \o -> do
      constraintClass <- o .: "class"
      extRefModule    <- o .: "hsmodule"
      extRefName      <- o .: "hsname"
      let hsName :: Hs.Name Hs.NsTypeConstr
          hsName = fromARep' extRefName
          -- TODO https://github.com/well-typed/hs-bindgen/issues/423
          --
          -- At the moment, we only handle external references to types types.
          -- Later we may support external references to variables (e.g., in
          -- macros).
          constraintRef = Hs.ExtRef{
              moduleName = fromARep' extRefModule
            , name       = hsName
            }
      return $ AConstraint Inst.Constraint{
          clss = fromARep' constraintClass
        , ref  = constraintRef
        }

instance Aeson.ToJSON (ARep V1 Inst.Constraint) where
  toJSON (AConstraint constraint) = Aeson.object [
        "class"    .= toARep' constraint.clss
      , "hsmodule" .= toARep' constraint.ref.moduleName
        -- TODO https://github.com/well-typed/hs-bindgen/issues/423
        --
        -- At the moment, we only handle external references to types types.
        -- Later we may support external references to variables (e.g., in
        -- macros).
      , "hsname"   .= toARep' constraint.ref.name
      ]

{-------------------------------------------------------------------------------
  Auxiliary functions
-------------------------------------------------------------------------------}

-- 'List.lookup' using a predicate
lookupBy :: (a -> Bool) -> [(a, b)] -> Maybe b
lookupBy p = fmap snd . List.find (p . fst)

data MangleCandidateError =
    MangleCandidateCouldNotMangleError Text
  | MangleCandidateParseError          MangleCandidate.ParseCandidateError

instance PrettyForTrace MangleCandidateError where
  prettyForTrace = \case
    MangleCandidateCouldNotMangleError x -> PP.hsep [
        "Could not mangle:"
      , PP.text x
      ]
    MangleCandidateParseError err -> PP.hsep [
        "Name does not adhere to naming rules:"
      , prettyForTrace err
      ]

parseHsName :: forall ns.
     Hs.SingNamespace ns
  => Proxy ns
  -> Text
  -> Either MangleCandidateError (Hs.Name ns)
parseHsName _ hsNameCandidate =
    case MangleCandidate.parseCandidate mangleCandidateConfig hsNameCandidate of
      Nothing ->
        Left $ MangleCandidateCouldNotMangleError hsNameCandidate
      Just (Left  err) ->
        Left $ MangleCandidateParseError err
      Just (Right hsName) ->
        Right $ hsName
  where
    mangleCandidateConfig :: MangleCandidate Maybe
    mangleCandidateConfig =
      MangleCandidate.mangleCandidateDefault
        -- Names in binding specs are supplied by the user and are expected to
        -- be valid Haskell identifiers. We therefore do not check reserved
        -- names: the user is trusted to avoid them, and checking would reject
        -- names like @type@ that are legitimately used in external packages.
        & #reservedNames .~ mempty