packages feed

inline-python-0.1: src/Python/Internal/Program.hs

{-# LANGUAGE QuasiQuotes     #-}
{-# LANGUAGE TemplateHaskell #-}
-- |
module Python.Internal.Program
  ( Program(..)
  , runProgram
  , progPy
  , progIO
    -- * Control flow
  , abort
  , abortM
  , abortOnNull
  , checkNull
  , finallyProg
  , onExceptionProg
  , takeOwnership
    -- * Allocators
  , withPyAlloca
  , withPyAllocaArray
  , withPyCString
  , withPyCStringLen
  , withPyWCString
    -- * Helpers
  , pyobjectStrAsHask
  ) where

import Control.Monad
import Control.Monad.Trans.Cont
import Control.Monad.Trans.Class
import Control.Monad.Catch
import Data.Coerce
import Foreign.Ptr
import Foreign.Marshal.Array
import Foreign.Marshal
import Foreign.C.String
import Foreign.C.Types
import Foreign.Storable

import Language.C.Inline          qualified as C
import Language.C.Inline.Unsafe   qualified as CU

import Python.Internal.Types
import Python.Internal.Util
import Python.Internal.CAPI

----------------------------------------------------------------
C.context (C.baseCtx <> pyCtx)
C.include "<inline-python.h>"
----------------------------------------------------------------


-- | This monad wraps 'Py' into 'ContT' in order get early exit,
--   applying @finally@ while avoiding building huge ladders.
newtype Program r a = Program (ContT r Py a)
  deriving newtype (Functor, Applicative, Monad)

runProgram :: Program a a -> Py a
runProgram (Program m) = evalContT m

-- | Does not change masking state
progIO :: IO a -> Program r a
progIO = Program . lift . pyIO

progPy :: Py a -> Program r a
progPy = Program . lift

-- | Early exit from continuation monad.
abort :: r -> Program r a
abort r = Program $ ContT $ \_ -> pure r

-- | Early exit from continuation monad.
abortM :: Py r -> Program r a
abortM m = Program $ ContT $ \_ -> m

-- | Perform early exit if pointer is null
abortOnNull :: r -> Py (Ptr a) -> Program r (Ptr a)
abortOnNull r action = Program $ ContT $ \cnt -> action >>= \case
  NULL -> pure r
  p    -> cnt p

-- | If result of computation is NULL return NULL immediately.
checkNull :: Py (Ptr a) -> Program (Ptr a) (Ptr a)
checkNull = abortOnNull nullPtr

-- | Evaluate finalizer even if exception is thrown.
finallyProg
  :: Py b -- ^ Finalizer
  -> Program r ()
finallyProg fini = Program $ ContT $ \c -> c () `finally` fini

-- | Evaluate finalizer if exception is thrown.
onExceptionProg
  :: Py b -- ^ Finalizer
  -> Program r ()
onExceptionProg fini = Program $ ContT $ \c -> c () `onException` fini

-- | Decrement reference counter at end of ContT block
takeOwnership :: Ptr PyObject -> Program r (Ptr PyObject)
takeOwnership p = Program $ ContT $ \c -> c p `finally` decref p


----------------------------------------------------------------
-- Allocation in context of `ContT _ Py`
----------------------------------------------------------------

withPyAlloca :: forall a r. Storable a => Program r (Ptr a)
withPyAlloca = coerce (alloca @a @r)

withPyAllocaArray :: forall a r. Storable a => Int -> Program r (Ptr a)
withPyAllocaArray = coerce (allocaArray @a @r)

withPyCString :: forall r. String -> Program r CString
withPyCString = coerce (withCString @r)

withPyWCString :: forall r. String -> Program r (Ptr CWchar)
withPyWCString = coerce (withWCString @r)

withPyCStringLen :: forall r. String -> Program r CStringLen
withPyCStringLen = coerce (withCStringLen @r)


----------------------------------------------------------------
-- More complicated helpers
----------------------------------------------------------------

-- | Call @__str__@ method of object and return haskell
--   string. Returns Nothing if exception was raisede
pyobjectStrAsHask :: Ptr PyObject -> Py (Maybe String)
pyobjectStrAsHask p_obj = runProgram $ do
  p_str <- takeOwnership <=< abortOnNull Nothing $ Py [CU.block| PyObject* {
    PyObject *s = PyObject_Str($(PyObject *p_obj));
    if( PyErr_Occurred() ) {
        PyErr_Clear();
    }
    return s;
    } |]
  c_str <- abortOnNull Nothing $ Py [CU.block| const char* {
    const char* s = PyUnicode_AsUTF8($(PyObject *p_str));
    if( PyErr_Occurred() ) {
        PyErr_Clear();
    }
    return s;
    } |]
  progIO $ Just <$> peekCString c_str