packages feed

emacs-module-0.2: src/Data/Emacs/Module/SymbolName/Internal.hs

----------------------------------------------------------------------------
-- |
-- Module      :  Data.Emacs.Module.SymbolName.Internal
-- Copyright   :  (c) Sergey Vinokurov 2018
-- License     :  Apache-2.0 (see LICENSE)
-- Maintainer  :  serg.foo@gmail.com
----------------------------------------------------------------------------

{-# LANGUAGE DataKinds        #-}
{-# LANGUAGE MagicHash        #-}
{-# LANGUAGE TypeFamilies     #-}
{-# LANGUAGE UnliftedNewtypes #-}

{-# OPTIONS_HADDOCK not-home #-}

module Data.Emacs.Module.SymbolName.Internal
  ( Static(..)
  , Dynamic(..)
  , SymbolName(..)
  , mkSymbolName
  , mkSymbolNameString
  , mkSymbolNameShortByteString
  , mkSymbolNameUnsafe

  , mkSymbolNameCache
  , mkCachedSymbolName
  , reifySymbolRaw
  , reifySymbolUnknown
  , reifySymbol
  ) where

import Data.ByteString.Internal qualified as BS
import Data.ByteString.Short qualified as BSS
import Data.Char
import Data.Coerce
import Data.IORef
import Data.String
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.Encoding qualified as TE
import Data.Text.Encoding.Error qualified as TE
import Data.Text.Foreign qualified as T
import Foreign.C.Types
import Foreign.Storable
import GHC.Exts (Addr#, unpackCString#)
import GHC.Ptr
import Prettyprinter
import System.IO.Unsafe

import Data.Emacs.Module.NonNullPtr
import Data.Emacs.Module.Raw.Env qualified as Raw
import Data.Emacs.Module.Raw.Env.Internal
import Data.Emacs.Module.Raw.Value
import Emacs.Module.Assert

import Data.Emacs.Module.SymbolName.Predefined.Funcall

-- | Symbols that are known at compile time.
--
-- Will just pass pointer to 0-terminated statically-allocated string
-- to Emacs API when used.
newtype Static = Static { unStatic :: Ptr CChar }
  deriving (Eq, Ord, Show)

newtype Dynamic = Dynamic { unDynamic :: Text }
  deriving (Eq, Ord, Show, Pretty)

data SymbolName
  = StaticSymbol  {-# UNPACK #-} !(Ptr CChar)
  | DynamicSymbol {-# UNPACK #-} !Text
  | CachedSymbol  (IORef (Env -> IO (RawValue 'Pinned))) SymbolName
  deriving (Eq)

instance Show SymbolName where
  show = \case
    StaticSymbol (Ptr addr)
      -> show $ TE.decodeUtf8With TE.lenientDecode $ BS.unsafePackLiteral addr
    DynamicSymbol str  -> show str
    CachedSymbol _ sym -> show sym

instance Pretty SymbolName where
  pretty = \case
    StaticSymbol (Ptr addr)
      -> pretty $ TE.decodeUtf8With TE.lenientDecode $ BS.unsafePackLiteral addr
    DynamicSymbol str  -> pretty str
    CachedSymbol _ sym -> pretty sym

mkSymbolNameCache :: SymbolName -> IO (IORef (Env -> IO (RawValue 'Pinned)))
mkSymbolNameCache = go
  where
    go :: SymbolName -> IO (IORef (Env -> IO (RawValue 'Pinned)))
    go !name =
      unsafeFixIO $ \ ref ->
        newIORef $ \env -> do
          !global <- Raw.makeGlobalRef env =<< reifySymbolRaw env name
          writeIORef ref $ \_env -> pure global
          pure global

{-# INLINE mkCachedSymbolName #-}
mkCachedSymbolName :: IORef (Env -> IO (RawValue 'Pinned)) -> SymbolName -> SymbolName
mkCachedSymbolName = CachedSymbol

-- | Should be applied to unboxed string literals like this
--
-- @
-- mkSymbolNameUnsafe "foo"#
-- @
--
-- Can be safely applied to non-literals (e.g. arbitrary pointers) if
-- it's guaranteed that address points to a null-terminated strings.
-- Otherwise behaviour is undefined.
--
-- The string literal must only contain ASCII symbols. This condition
-- is required by the Emacs API and results in undefined behaviour if
-- violated.
{-# INLINE mkSymbolNameUnsafe #-}
mkSymbolNameUnsafe :: Addr# -> SymbolName
mkSymbolNameUnsafe addr = StaticSymbol (Ptr addr)

{-# INLINE mkSymbolName #-}
mkSymbolName :: Text -> SymbolName
mkSymbolName = DynamicSymbol

{-# INLINE mkSymbolNameShortByteString #-}
mkSymbolNameShortByteString :: BSS.ShortByteString -> SymbolName
mkSymbolNameShortByteString = DynamicSymbol . TE.decodeUtf8With TE.lenientDecode . BSS.fromShort

{-# INLINE [0] mkSymbolNameString #-}
mkSymbolNameString :: String -> SymbolName
mkSymbolNameString = mkSymbolName . T.pack

instance IsString SymbolName where
  {-# INLINE fromString #-}
  fromString = mkSymbolNameString

{-# RULES
"SymbolName string literal" forall s .
   mkSymbolNameString (unpackCString# s) = mkSymbolNameUnsafe s
 #-}

{-# INLINE reifySymbolRaw #-}
reifySymbolRaw :: Env -> SymbolName -> IO (RawValue 'Regular)
reifySymbolRaw env sym = reifySymbol env sym id coerce

{-# INLINE reifySymbolUnknown #-}
reifySymbolUnknown :: Env -> SymbolName -> IO (RawValue 'Unknown)
reifySymbolUnknown env sym = reifySymbol env sym coerce coerce

{-# INLINE reifySymbol #-}
reifySymbol
  :: WithCallStack
  => Env -> SymbolName -> (RawValue 'Regular -> a) -> (RawValue 'Pinned -> a) -> IO a
reifySymbol env sym f g = case sym of
  StaticSymbol addr ->
    f <$> Raw.intern env addr
  DynamicSymbol str
    -- If it's only ASCII then can run FFI intern, otherwise have to go via funcall
    -- TODO: cache this check
    | T.all (\c -> ord c < 128) str ->
      f <$> T.withCString str (Raw.intern env)
    | otherwise                    ->
      T.withCStringLen str $ \(ptr, len) -> do
        str' <- emacsAssert (len >= 0) "Symbol text length must be non-negative" $
          Raw.makeString env ptr (fromIntegral len)
        funcall' <- reifySymbolUnknown env funcall
        allocaNonNull $ \args -> do
          poke (unNonNullPtr args) str'
          f <$> Raw.funcallPrimitive env funcall' 1 args

  CachedSymbol ref _ ->
    g <$> ((\k -> k env) =<< readIORef ref)