packages feed

hs-bindgen-1.0.0.0: src-internal/HsBindgen/Config/MangleCandidate.hs

module HsBindgen.Config.MangleCandidate (
    -- * Definition
    MangleCandidate(..)
  , mangleCandidate
  , mangleCandidateDefaultFallback
    -- * Parse
  , ParseCandidateError(..)
  , parseCandidate
    -- * Standard instances
  , mangleCandidateDefault
  , mangleCandidateHaskell
    -- * Constructing new instances
    -- ** Dealing with invalid characters
  , dropInvalidChar
  , escapeInvalidChar
    -- ** Name rule sets
  , NameRuleSet(..)
  , NamespaceRuleSet
  , SNameRuleSet(..)
  , singNameRuleSet
  , modifyFirstLetter
  , prefixInvalidFirst
  , dropInvalidFirst
    -- ** Reserved names
  , appendSingleQuote
  ) where

import Control.Monad
import Data.Char qualified as Char
import Data.Proxy
import Data.Set qualified as Set
import Data.Text qualified as Text
import Text.SimplePrettyPrint qualified as PP

import HsBindgen.Config.MangleCandidate.ReservedNames (ReservedNames,
                                                       allReservedNames,
                                                       reservedNamesIn)
import HsBindgen.Imports
import HsBindgen.Language.Haskell qualified as Hs
import HsBindgen.Util.Tracer

import Numeric (showHex)

{-------------------------------------------------------------------------------
  Definition
-------------------------------------------------------------------------------}

-- | Mangle candidate name to conform to Haskell's naming rules
--
-- Name generation is allowed to fail (depending on the policy, there are
-- circumstances in which we cannot generate a name). When this happens, we
-- require a name override.
data MangleCandidate m = MangleCandidate {
      -- | Process invalid characters
      --
      -- Called on characters that are invalid anywhere in a Haskell identifier.
      --
      -- See 'dropInvalidChar' and 'escapeInvalidChar'.
      onInvalidChar :: Char -> m String

      -- | Make the name conform to the name rule set
      --
      --See 'modifyFirstLetter'
    , apply :: ApplyRuleset m

      -- | Reserved names
    , reservedNames :: ReservedNames

      -- | How to modify reserved names
      --
      -- The transformation function must return a valid Haskell name.
      --
      -- See 'appendSingleQuote'.
    , onReservedName :: Text -> Text
    }

newtype ApplyRuleset m = ApplyRuleset(
      forall ns. Hs.SingNamespace ns => Text -> m (Hs.Name ns)
    )

applyRuleset ::
     Hs.SingNamespace ns
  => ApplyRuleset m -> Text -> m (Hs.Name ns)
applyRuleset (ApplyRuleset f) = f

deriving stock instance Generic (MangleCandidate m)

{-------------------------------------------------------------------------------
  Instances
-------------------------------------------------------------------------------}

mangleCandidateDefault :: MangleCandidate Maybe
mangleCandidateDefault = MangleCandidate {
      onInvalidChar  = return . escapeInvalidChar
    , apply          = ApplyRuleset $
                         modifyFirstLetter (Just . prefixInvalidFirst "C" "c")
    , reservedNames  = allReservedNames
    , onReservedName = appendSingleQuote
    }

mangleCandidateHaskell :: MangleCandidate Maybe
mangleCandidateHaskell = mangleCandidateDefault {
      onInvalidChar = return . dropInvalidChar
    , apply         = ApplyRuleset $ modifyFirstLetter dropInvalidFirst
    }

{-------------------------------------------------------------------------------
  Execution
-------------------------------------------------------------------------------}

-- | Translate C name to Haskell name, observing Haskell naming conventions
--
-- If the C name is already a valid Haskell name for its namespace, it must be
-- preserved, provided it is not a reserved name (idempotent modulo reserved
-- names).
mangleCandidate :: forall ns m.
     (Monad m, Hs.SingNamespace ns)
  => MangleCandidate m -> Text -> m (Hs.Name ns)
mangleCandidate mc =
        pure . handleReservedNames
    <=< applyRuleset mc.apply
    <=< processInvalidChars
  where
    processInvalidChars :: Text -> m Text
    processInvalidChars = fmap Text.pack . go [] . Text.unpack
      where
        go :: [String] -> String -> m String
        go acc (c:cs)
          | isValidChar c = go ([c] : acc) cs
          | otherwise     = mc.onInvalidChar c >>= \c' -> go (c':acc) cs
        go acc []         = return $ concat (reverse acc)

        -- NOTE: @isAlphaNum@ is @True@ for non-ASCII characters too (e.g. '你')
        -- NOTE: C names cannot include single quotes, but they can arise when
        -- we define one class of name (say, a data constructor) from the name
        -- that we generate for another (say, the data type).
        isValidChar :: Char -> Bool
        isValidChar c = Char.isAlphaNum c || c == '_' || c == '\''

    handleReservedNames :: Hs.Name ns -> Hs.Name ns
    handleReservedNames name
      | name.text `Set.member` reservedNamesIn namespace mc.reservedNames
      = Hs.UnsafeName $ mc.onReservedName name.text
      | otherwise
      = name

    namespace :: Hs.Namespace
    namespace = Hs.namespaceOf (Hs.singNamespace @ns)

-- | Mangle a candidate
--
-- Use the default configuration 'mangleCandidateDefault'.
--
-- If mangling fails, return the @fallback@ value.
mangleCandidateDefaultFallback ::
     forall ns. Hs.SingNamespace ns
  => Hs.Name ns -> Text -> Hs.Name ns
mangleCandidateDefaultFallback fallback candidate =
  case mangleCandidate mangleCandidateDefault candidate of
    Nothing   -> fallback
    Just name -> name

{-------------------------------------------------------------------------------
  Parse
-------------------------------------------------------------------------------}

data ParseCandidateError =
  ParseNameMismatch Text Hs.SomeName
  deriving stock (Show, Eq, Ord)

instance PrettyForTrace ParseCandidateError where
  prettyForTrace = \case
    ParseNameMismatch candidate name -> PP.hcat [
        "Mismatch of candidate ("
      , PP.text candidate
      , ") and name ("
      , PP.text name.text
      , "; namespace "
      , prettyForTrace name.ns
      , ")"
      ]

-- | Parse a candidate and check if it adheres to the naming rules.
parseCandidate :: forall ns m.
     (Monad m, Hs.SingNamespace ns)
  => MangleCandidate m
  -> Text
  -> m (Either ParseCandidateError (Hs.Name ns))
parseCandidate mc candidate = do
    -- 'mangleCandidate' is idempotent, so applying the transformation again
    -- must yield the same identifier.
    name <- mangleCandidate @ns mc candidate
    pure $
      if candidate == name.text; then
        Right name
      else
        Left $ ParseNameMismatch candidate (Hs.demoteNs name)

{-------------------------------------------------------------------------------
  Dealing with invalid characters
-------------------------------------------------------------------------------}

-- | Drop invalid characters
dropInvalidChar :: Char -> String
dropInvalidChar = const ""

-- | Escape invalid characters
--
-- An invalid character transformed to a single quote (@'@) followed by the
-- Unicode code point (four lowercase hex digits).
escapeInvalidChar :: Char -> String
escapeInvalidChar c =
    let hex = showHex (Char.ord c) ""
    in  '\'' : replicate (max 0 (4 - length hex)) '0' ++ hex

{-------------------------------------------------------------------------------
  Name rule sets
-------------------------------------------------------------------------------}

data NameRuleSet =
    -- | Variables and type variables
    NameRuleSetVar

    -- | Constructors, type constructors, type classes and module names
  | NameRuleSetOther

type family NamespaceRuleSet (ns :: Hs.Namespace) :: NameRuleSet where
  NamespaceRuleSet Hs.NsTypeConstr = NameRuleSetOther
  NamespaceRuleSet Hs.NsConstr     = NameRuleSetOther
  NamespaceRuleSet Hs.NsVar        = NameRuleSetVar

data SNameRuleSet :: NameRuleSet -> Star where
  SNameRuleSetVar   :: SNameRuleSet NameRuleSetVar
  SNameRuleSetOther :: SNameRuleSet NameRuleSetOther

singNameRuleSet :: forall ns.
     Hs.SingNamespace ns
  => Proxy ns -> SNameRuleSet (NamespaceRuleSet ns)
singNameRuleSet _ =
    case Hs.singNamespace @ns of
      Hs.SNsTypeConstr -> SNameRuleSetOther
      Hs.SNsConstr     -> SNameRuleSetOther
      Hs.SNsVar        -> SNameRuleSetVar

{-------------------------------------------------------------------------------
  Applying name rule sets
-------------------------------------------------------------------------------}

data CannotApplyRuleset ns = CannotApplyRuleset{
      -- | The ruleset we failed to apply
      ruleset :: SNameRuleSet (NamespaceRuleSet ns)

      -- | The prefix we cannot handle
      --
      -- This contains characters for which 'Data.Char.toUpper' to 'Data.Char.toLower' is
      -- insufficient to enforce the ruleset.
    , unhandledPrefix :: Text

      -- | The remainder of the name
      --
      -- The rest of the name, after the unhandled prefix, if any; we single out
      -- the first character, both as it appears in the original name as well as
      -- (potentially) modified to adhere to the ruleset (or unmodified if the
      -- remainder happens to already adhere to the rule).
    , usableSuffix :: Maybe (Char, Char, Text)
    }

-- | Make first letter conform the name rule set
--
-- Haskell identifiers have different rules for the first character and the rest
-- of the identifier. In particular, names in 'NameRuleSetOther' must start with
-- an uppercase letter, whereas names in 'NameRuleSetVar' must start with a
-- not-uppercase letter. This is not the same as a lowercase letter!
--
-- > isAlphaNum '事' == True
-- > isUpper    '事' == False
-- > isLower    '事' == False
--
-- To ensure that this rule is satisfied, we
--
-- 1. First check if the rule is already satisfied; if so, nothing to do.
--
-- 2. If the rule is not satisfied, we attempt to change the first character
--    to uppercase or lowercase, as appropriate. If this succeeds, we are done.
--
-- 3. Step (2) may fail; for example;
--
--    > toUpper '_' == '_'
--    > toUpper '事' == '事'
--
--    When this happens, we call the supplied function with the unusable prefix
--    and the remainder of the identifier.
modifyFirstLetter :: forall ns.
     Hs.SingNamespace ns
  => (CannotApplyRuleset ns -> Maybe (Hs.Name ns))
     -- ^ @onInvalidFirst@ (see 'prefixInvalidFirst' and 'dropInvalidFirst')
  -> Text -> Maybe (Hs.Name ns)
modifyFirstLetter onInvalidFirst =
    aux (singNameRuleSet (Proxy @ns))
  where
    aux :: SNameRuleSet (NamespaceRuleSet ns) -> Text -> Maybe (Hs.Name ns)
    aux ruleset = \t -> do
        (firstChar, rest) <- Text.uncons t
        if | matchesRule firstChar ->
               Just . Hs.UnsafeName $
                 Text.cons firstChar rest
           | matchesRule (adjustForRule firstChar) ->
               Just . Hs.UnsafeName $
                 Text.cons (adjustForRule firstChar) rest
           | otherwise -> do
               let unhandled, afterUnhandled :: Text
                   (unhandled, afterUnhandled) = Text.span unusable t

                   usable :: Maybe (Char, Char, Text)
                   usable =
                       (\(c, cs) -> if matchesRule c
                                      then (c, c, cs)
                                      else (c, adjustForRule c, cs)
                       ) <$>
                          Text.uncons afterUnhandled

                   cannotApply :: CannotApplyRuleset ns
                   cannotApply = CannotApplyRuleset{
                         ruleset         = ruleset
                       , unhandledPrefix = unhandled
                       , usableSuffix    = usable
                       }
               onInvalidFirst cannotApply
      where
        matchesRule   :: Char -> Bool
        adjustForRule :: Char -> Char
        (matchesRule, adjustForRule) =
            case ruleset of
              SNameRuleSetVar   -> (not . Char.isUpper , Char.toLower)
              SNameRuleSetOther -> (      Char.isUpper , Char.toUpper)

        unusable :: Char -> Bool
        unusable c =
               not (matchesRule                 $ c)
            && not (matchesRule . adjustForRule $ c)

prefixInvalidFirst ::
     Text  -- ^ Prefix for 'SNameRuleSetOther'
  -> Text  -- ^ Prefix for 'SNameRuleSetVar' (rarely needed)
  -> CannotApplyRuleset ns -> Hs.Name ns
prefixInvalidFirst prefixOther prefixVar cannotApply =
     let prefix :: Text
         prefix = case cannotApply.ruleset of
               SNameRuleSetOther -> prefixOther
               SNameRuleSetVar   -> prefixVar
     in Hs.UnsafeName $ mconcat [
            prefix
          , cannotApply.unhandledPrefix
          , aux cannotApply.usableSuffix
          ]
  where
    aux :: Maybe (Char, Char, Text) -> Text
    aux Nothing           = ""
    aux (Just (_, c, cs)) = Text.cons c cs

dropInvalidFirst :: CannotApplyRuleset ns -> Maybe (Hs.Name ns)
dropInvalidFirst cannotApply =
    aux <$> cannotApply.usableSuffix
  where
    aux :: (Char, Char, Text) -> Hs.Name ns
    aux (_, c, cs) = Hs.UnsafeName $ Text.cons c cs

{-------------------------------------------------------------------------------
  Reserved names
-------------------------------------------------------------------------------}

-- | Append a single quote (@'@) to a name
appendSingleQuote :: Text -> Text
appendSingleQuote = (<> "'")