packages feed

emacs-module-0.2: src/Emacs/Module/Monad/Common.hs

----------------------------------------------------------------------------
-- |
-- Module      :  Emacs.Module.Monad.Common
-- Copyright   :  (c) Sergey Vinokurov 2022
-- License     :  Apache-2.0 (see LICENSE)
-- Maintainer  :  serg.foo@gmail.com
----------------------------------------------------------------------------

{-# LANGUAGE CPP               #-}
{-# LANGUAGE DataKinds         #-}
{-# LANGUAGE MagicHash         #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE UnboxedTuples     #-}

module Emacs.Module.Monad.Common
  ( EmacsRes(..)
  , NonLocalState(..)
  , withNonLocalState
  , unpackEnumFuncallExit
  , unpackEnumFuncallExitSafe
  , Emacs.Module.Monad.Common.nonLocalExitGet
  , nonLocalExitSignal
  , extractText
  , extractShortByteString
  , checkNonLocalExitSignal
  , checkNonLocalExitFull
  , extractSignalInfo
  , extractTextUnsafe
  ) where

import Control.Exception
import Control.Monad.Primitive
import Data.ByteString.Short (ShortByteString)
import Data.ByteString.Short qualified as SBS
import Data.Text (Text)
import Data.Text.Array qualified as TA
import Data.Text.Internal qualified as T
import Data.Traversable
import Data.Tuple.Homogenous
import Data.Void
import Foreign.C.Types
import Foreign.Ptr
import Foreign.Storable
import GHC.Exts
import GHC.IO
import GHC.Stack (CallStack, callStack)
import Prettyprinter

#ifdef ASSERTIONS
import Data.ByteString.Internal qualified as BSI
import Data.Text.Encoding qualified as TE
import Foreign.ForeignPtr qualified as Foreign
#endif

import Data.Emacs.Module.Env.Functions
import Data.Emacs.Module.NonNullPtr
import Data.Emacs.Module.Raw.Env (EnumFuncallExit(..))
import Data.Emacs.Module.Raw.Env qualified as Env
import Data.Emacs.Module.Raw.Env.Internal
import Data.Emacs.Module.Raw.Value
import Data.Emacs.Module.SymbolName.Internal
import Data.Emacs.Module.SymbolName.Predefined qualified as Sym
import Emacs.Module.Assert
import Emacs.Module.Errors
import Foreign.Ptr.Builder as PtrBuilder

data EmacsRes s t a
  = EmacsSuccess a
  | EmacsExitSignal s
  | EmacsExitThrow t
  deriving (Functor, Foldable, Traversable)

data NonLocalState = NonLocalState
  { nlsErr  :: {-# UNPACK #-} !(NonNullPtr (RawValue 'Regular))
  , nlsData :: {-# UNPACK #-} !(NonNullPtr (RawValue 'Regular))
  , nlsSize :: {-# UNPACK #-} !(NonNullPtr CPtrdiff)
  }

withNonLocalState :: (NonLocalState -> IO a) -> IO a
withNonLocalState f =
  allocaNonNull $ \ !nlsErr ->
    allocaNonNull $ \ !nlsData ->
      allocaNonNull $ \ !nlsSize ->
        f NonLocalState{nlsErr, nlsData, nlsSize}

unpackEnumFuncallExit
  :: WithCallStack
  => EnumFuncallExit -> IO (FuncallExit ())
unpackEnumFuncallExit =
  either throwIO pure . unpackEnumFuncallExitSafe

unpackEnumFuncallExitSafe
  :: WithCallStack
  => EnumFuncallExit -> Either EmacsInternalError (FuncallExit ())
unpackEnumFuncallExitSafe (EnumFuncallExit (CInt !x)) =
  case funcallExitFromNum x of
    Nothing -> Left $ mkEmacsInternalError $
      "Unknown value of enum emacs_funcall_exit:" <+> pretty x
    Just y  -> Right y

{-# INLINE nonLocalExitGet #-}
nonLocalExitGet
  :: WithCallStack
  => Env
  -> NonLocalState
  -> IO (FuncallExit (RawValue 'Regular, RawValue 'Regular))
nonLocalExitGet env NonLocalState{nlsErr, nlsData} = do
  exit <- Env.nonLocalExitGet env nlsErr nlsData
  foldFuncallExitFromNum
    (unEnumFuncallExit exit)
    (throwIO $ mkEmacsInternalError $ "Unknown value of enum emacs_funcall_exit:" <+> pretty exit)
    (\x ->
      for x $ \(_ :: ()) ->
        (,) <$> peek (unNonNullPtr nlsErr) <*> peek (unNonNullPtr nlsData))
  -- x <- unpackEnumFuncallExit =<< Env.nonLocalExitGet env nlsErr nlsData
  -- for x $ \(_ :: ()) ->
  --   (,) <$> peek (unNonNullPtr nlsErr) <*> peek (unNonNullPtr nlsData)

{-# INLINE nonLocalExitSignal #-}
nonLocalExitSignal
  :: WithCallStack
  => BuilderCache (RawValue a)
  -> Env
  -> CallStack
  -> RawValue 'Unknown           -- ^ Error symbol
  -> Builder (RawValue 'Regular) -- ^ Error data
  -> IO EmacsSignal
nonLocalExitSignal cache env !emacsSignalOrigin !sym !dat = do
  listSym <- reifySymbolUnknown env Sym.list
  withPtrLenNonNull (coerceBuilderCache cache) dat $ \n args -> do
    dat'            <- Env.funcallPrimitive env listSym (fromIntegral n) args
    emacsSignalInfo <- extractSignalInfo cache env sym dat'
    Env.nonLocalExitSignal env sym dat'
    pure EmacsSignal
      { emacsSignalSym  = toUnknown sym
      , emacsSignalData = dat'
      , emacsSignalOrigin
      , emacsSignalInfo
      }

{-# INLINE extractStringWith #-}
extractStringWith
  :: WithCallStack
  => BuilderCache (RawValue a)
  -> Env
  -> NonLocalState
  -> RawValue p
  -> (Int# -> MutableByteArray# RealWorld -> IO b)
  -> IO (EmacsRes EmacsSignal Void b)
extractStringWith cache env !nls@NonLocalState{nlsSize} !x k = do
  res <- Env.copyStringContents env x nullPtr nlsSize
  if Env.isNonTruthy res
  then do
    Env.nonLocalExitClear env
    throwIO $ mkEmacsInternalError
      "Failed to obtain size when unpacking string. Probable cause: emacs object is not a string."
  else do
    I# size# <- fromIntegral <$> peek (unNonNullPtr nlsSize)
    IO $ \s1 -> case newPinnedByteArray# size# s1 of
      (# s2, mbarr# #) -> (\kk -> kk s2) (unIO (do
        !copyPerformed <- Env.copyStringContents env x (Ptr (mutableByteArrayContents# mbarr#)) nlsSize
        if Env.isTruthy copyPerformed
        then
          EmacsSuccess <$> k size# mbarr#
        else
         nonLocalExitGet env nls >>= \case
           FuncallExitSignal (sym, dat) -> do
             -- Important to clean up so that we can still call Emacs functions to make nil return value, etc
             Env.nonLocalExitClear env
             emacsSignalInfo <- extractSignalInfo cache env sym dat
             pure $ EmacsExitSignal $ EmacsSignal
               { emacsSignalSym    = toUnknown sym
               , emacsSignalData   = dat
               , emacsSignalOrigin = callStack
               , emacsSignalInfo
               }
           FuncallExitReturn            ->
             throwIO $ mkEmacsInternalError "Failed to unpack string"
           FuncallExitThrow{} ->
             throwIO $ mkEmacsInternalError
               "The copy string contents operation should have never exited via throw"))

{-# INLINE extractText #-}
extractText
  :: WithCallStack
  => BuilderCache (RawValue a)
  -> Env
  -> NonLocalState
  -> RawValue p
  -> IO (EmacsRes EmacsSignal Void Text)
extractText cache env nls x =
  extractStringWith cache env nls x $ \size# mbarr# ->
#ifdef ASSERTIONS
    do
      -- Should subtract 1 from size to avoid NULL terminator at the end.
      ptr <- Foreign.newForeignPtr_ (Ptr (mutableByteArrayContents# mbarr#))
      evaluate $ TE.decodeUtf8 $ BSI.BS ptr (I# (size# -# 1#))
#else
    IO $ \s1 ->
      case unsafeFreezeByteArray# mbarr# s1 of
        (# s2, barr #) ->
          -- Should subtract 1 from size to avoid NULL terminator at the end.
          (# s2, T.Text (TA.ByteArray barr) 0 (I# (size# -# 1#)) #)
#endif

{-# INLINE extractShortByteString #-}
extractShortByteString
  :: WithCallStack
  => BuilderCache (RawValue a)
  -> Env
  -> NonLocalState
  -> RawValue p
  -> IO (EmacsRes EmacsSignal Void ShortByteString)
extractShortByteString cache env nls x =
  extractStringWith cache env nls x $ \size# mbarr# ->
    IO $ \s3 ->
      -- Should subtract 1 from size to avoid NULL terminator at the end.
      case shrinkMutableByteArray# mbarr# (size# -# 1#) s3 of
        s4 ->
          case unsafeFreezeByteArray# mbarr# s4 of
            (# s5, barr #) ->
              (# s5, SBS.SBS barr #)

{-# INLINE checkNonLocalExitSignal #-}
checkNonLocalExitSignal
  :: WithCallStack
  => BuilderCache (RawValue b)
  -> Env
  -> NonLocalState
  -> Text
  -> a
  -> IO (EmacsRes EmacsSignal Void a)
checkNonLocalExitSignal cache env !nls !errMsg !res = do
  nonLocalExitGet env nls >>= \ case
    FuncallExitReturn            ->
      pure $ EmacsSuccess res
    FuncallExitSignal (sym, dat) -> do
      -- Important to clean up so that we can still call Emacs functions to make nil return value, etc
      Env.nonLocalExitClear env
      emacsSignalInfo <- extractSignalInfo cache env sym dat
      pure $ EmacsExitSignal $ EmacsSignal
        { emacsSignalSym    = toUnknown sym
        , emacsSignalData   = dat
        , emacsSignalOrigin = callStack
        , emacsSignalInfo
        }
    FuncallExitThrow{} ->
      throwIO $ mkEmacsInternalError $
        "The operation should have never exited via throw:" <> line <> pretty errMsg

{-# INLINE checkNonLocalExitFull #-}
checkNonLocalExitFull
  :: WithCallStack
  => BuilderCache (RawValue b)
  -> Env
  -> NonLocalState
  -> a
  -> IO (EmacsRes EmacsSignal EmacsThrow a)
checkNonLocalExitFull cache env !nls !res =
  nonLocalExitGet env nls >>= \case
    FuncallExitReturn            ->
      pure $ EmacsSuccess res
    FuncallExitSignal (sym, dat) -> do
      -- Important to clean up so that we can still call Emacs functions to make nil return value, etc
      Env.nonLocalExitClear env
      emacsSignalInfo <- extractSignalInfo cache env sym dat
      pure $ EmacsExitSignal $ EmacsSignal
        { emacsSignalSym    = toUnknown sym
        , emacsSignalData   = dat
        , emacsSignalOrigin = callStack
        , emacsSignalInfo
        }
      -- -- Important to clean up so that we can still call Emacs
      -- -- functions to make nil return value, etc
      -- Env.nonLocalExitClear env
    FuncallExitThrow (tag, value) -> do
      -- Important to clean up so that we can still call Emacs functions to make nil return value, etc
      Env.nonLocalExitClear env
      pure $ EmacsExitThrow $ EmacsThrow
        { emacsThrowTag    = tag
        , emacsThrowValue  = value
        , emacsThrowOrigin = callStack
        }

extractSignalInfo
  :: WithCallStack
  => BuilderCache (RawValue a) -> Env -> RawValue p -> RawValue 'Regular -> IO Text
extractSignalInfo cache env !sym !dat = do
  cons          <- reifySymbolUnknown env Sym.cons
  dat'          <- withPtrLenNonNull (coerceBuilderCache cache) (foldMap PtrBuilder.storable $ Tuple2 (toUnknown sym, toUnknown dat)) $ \n args ->
    Env.funcallPrimitive env cons (fromIntegral n) args
  prin1ToString <- reifySymbolUnknown env Sym.prin1ToString
  formatted     <- withPtrLenNonNull (coerceBuilderCache cache) (foldMap PtrBuilder.storable $ Tuple1 dat') $ \n args ->
    Env.funcallPrimitive env prin1ToString (fromIntegral n) args
  formatRes     <- unpackEnumFuncallExit =<< Env.nonLocalExitCheck env
  case formatRes of
    FuncallExitSignal{} -> do
      Env.nonLocalExitClear env
      throwIO $ mkEmacsInternalError "Failed to format Emacs signal data"
    FuncallExitThrow{}  -> do
      Env.nonLocalExitClear env
      throwIO $ mkEmacsInternalError "Failed to format Emacs signal data"
    FuncallExitReturn   ->
      extractTextUnsafe env formatted

extractTextUnsafe
  :: WithCallStack
  => Env
  -> RawValue p
  -> IO Text
extractTextUnsafe env !x = do
  allocaNonNull $ \pSize -> do
    res <- Env.copyStringContents env x nullPtr pSize
    if Env.isNonTruthy res
    then do
      Env.nonLocalExitClear env
      throwIO $ mkEmacsInternalError
        "Failed to obtain size when unpacking string. Probable cause: emacs object is not a string."
    else do
      !size@(I# size#) <- fromIntegral <$> peek (unNonNullPtr pSize)
      IO $ \s1 -> case newPinnedByteArray# size# s1 of
        (# s2, mbarr #) -> (\k -> k s2) (unIO (do
          !copyPerformed <- Env.copyStringContents env x (Ptr (mutableByteArrayContents# mbarr)) pSize
          if Env.isTruthy copyPerformed
          then
            IO $ \s3 ->
              case unsafeFreezeByteArray# mbarr s3 of
                (# s4, barr #) ->
                  -- Should subtract 1 from size to avoid NULL terminator at the end.
                  (# s4, T.Text (TA.ByteArray barr) 0 (size -  1) #)
          else do
            Env.nonLocalExitClear env
            throwIO $ mkEmacsInternalError "Failed to unpack string"))