From 2994683c5023b11c19799471db98f3d4659d5a8c Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Tue, 15 Apr 2025 06:36:59 +0300 Subject: [PATCH 01/10] Add Code object to avoid reencoding strings every time It also should be useful for file evaluation etc --- inline-python.cabal | 2 +- src/Python/Internal/EvalQQ.hs | 40 +++++++++++++++++++++-------------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/inline-python.cabal b/inline-python.cabal index e6c0db2..2d47b65 100644 --- a/inline-python.cabal +++ b/inline-python.cabal @@ -57,7 +57,7 @@ Library , stm >=2.4 , template-haskell -any , text >=2 - , bytestring + , bytestring >=0.11.2 , exceptions >=0.10 , vector >=0.13 hs-source-dirs: src diff --git a/src/Python/Internal/EvalQQ.hs b/src/Python/Internal/EvalQQ.hs index 98dde98..220e38a 100644 --- a/src/Python/Internal/EvalQQ.hs +++ b/src/Python/Internal/EvalQQ.hs @@ -7,6 +7,7 @@ module Python.Internal.EvalQQ , evaluatorPy_ , evaluatorPye , evaluatorPyf + , Code(..) -- * Code generation , expQQ , Mode(..) @@ -14,10 +15,12 @@ module Python.Internal.EvalQQ import Control.Monad.IO.Class import Control.Monad.Catch +import Control.Monad.Trans.Cont (ContT(..)) import Data.Bits import Data.Char import Data.List (intercalate) import Data.ByteString qualified as BS +import Data.ByteString.Unsafe qualified as BS import Data.Text qualified as T import Data.Text.Encoding qualified as T import Foreign.C.Types @@ -42,6 +45,13 @@ C.context (C.baseCtx <> pyCtx) C.include "" ---------------------------------------------------------------- +newtype Code = Code BS.ByteString + deriving stock (Show, TH.Lift) + +unsafeWithCode :: Code -> Program r (Ptr CChar) +unsafeWithCode (Code bs) = Program $ ContT $ \fun -> + Py (BS.unsafeUseAsCString bs $ unsafeRunPy . fun) + ---------------------------------------------------------------- -- Evaluators ---------------------------------------------------------------- @@ -51,12 +61,11 @@ C.include "" pyExecExpr :: Ptr PyObject -- ^ Globals -> Ptr PyObject -- ^ Locals - -> String -- ^ Python source code + -> Code -- ^ Python source code -> Py () pyExecExpr p_globals p_locals src = runProgram $ do - p_py <- withPyCString src - progPy $ do - Py [C.block| void { + p_py <- unsafeWithCode src + progIO [C.block| void { PyObject* globals = $(PyObject* p_globals); PyObject* locals = $(PyObject* p_locals); // Compile code @@ -69,18 +78,17 @@ pyExecExpr p_globals p_locals src = runProgram $ do Py_XDECREF(res); Py_DECREF(code); } |] - checkThrowPyError + progPy checkThrowPyError -- | Evaluate expression with fresh local environment pyEvalExpr :: Ptr PyObject -- ^ Globals -> Ptr PyObject -- ^ Locals - -> String -- ^ Python source code + -> Code -- ^ Python source code -> Py PyObject pyEvalExpr p_globals p_locals src = runProgram $ do - p_py <- withPyCString src - progPy $ do - p_res <- Py [C.block| PyObject* { + p_py <- unsafeWithCode src + p_res <- progIO [C.block| PyObject* { PyObject* globals = $(PyObject* p_globals); PyObject* locals = $(PyObject* p_locals); // Compile code @@ -93,29 +101,29 @@ pyEvalExpr p_globals p_locals src = runProgram $ do Py_DECREF(code); return r; }|] - checkThrowPyError - newPyObject p_res + progPy $ checkThrowPyError + progPy $ newPyObject p_res -evaluatorPymain :: (Ptr PyObject -> Py String) -> Py () +evaluatorPymain :: (Ptr PyObject -> Py Code) -> Py () evaluatorPymain getSource = do p_main <- basicMainDict src <- getSource p_main pyExecExpr p_main p_main src -evaluatorPy_ :: (Ptr PyObject -> Py String) -> Py () +evaluatorPy_ :: (Ptr PyObject -> Py Code) -> Py () evaluatorPy_ getSource = runProgram $ do p_globals <- progPy basicMainDict p_locals <- takeOwnership =<< progPy basicNewDict progPy $ pyExecExpr p_globals p_locals =<< getSource p_locals -evaluatorPye :: (Ptr PyObject -> Py String) -> Py PyObject +evaluatorPye :: (Ptr PyObject -> Py Code) -> Py PyObject evaluatorPye getSource = runProgram $ do p_globals <- progPy basicMainDict p_locals <- takeOwnership =<< progPy basicNewDict progPy $ pyEvalExpr p_globals p_locals =<< getSource p_locals -evaluatorPyf :: (Ptr PyObject -> Py String) -> Py PyObject +evaluatorPyf :: (Ptr PyObject -> Py Code) -> Py PyObject evaluatorPyf getSource = runProgram $ do p_globals <- progPy basicMainDict p_locals <- takeOwnership =<< progPy basicNewDict @@ -212,7 +220,7 @@ expQQ mode qq_src = do -- [| \p_dict -> do mapM_ ($ p_dict) $(TH.listE args) - pure $(TH.lift src_eval) + pure $(TH.lift $ Code $ T.encodeUtf8 $ T.pack src_eval) |] From 5e5c1c1381328965fd30ab239fd61e68a49aea7e Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Tue, 15 Apr 2025 20:40:04 +0300 Subject: [PATCH 02/10] Split python source code manipulation and variable binding --- src/Python/Inline/QQ.hs | 8 ++-- src/Python/Internal/EvalQQ.hs | 90 +++++++++++++++++++++++------------ 2 files changed, 63 insertions(+), 35 deletions(-) diff --git a/src/Python/Inline/QQ.hs b/src/Python/Inline/QQ.hs index fdefa12..122cd70 100644 --- a/src/Python/Inline/QQ.hs +++ b/src/Python/Inline/QQ.hs @@ -48,7 +48,7 @@ import Python.Internal.EvalQQ -- It creates value of type @Py ()@ pymain :: QuasiQuoter pymain = QuasiQuoter - { quoteExp = \txt -> [| evaluatorPymain $(expQQ Exec txt) |] + { quoteExp = \txt -> [| uncurry evaluatorPymain $(expQQ Exec txt) |] , quotePat = error "quotePat" , quoteType = error "quoteType" , quoteDec = error "quoteDec" @@ -61,7 +61,7 @@ pymain = QuasiQuoter -- It creates value of type @Py ()@ py_ :: QuasiQuoter py_ = QuasiQuoter - { quoteExp = \txt -> [| evaluatorPy_ $(expQQ Exec txt) |] + { quoteExp = \txt -> [| uncurry evaluatorPy_ $(expQQ Exec txt) |] , quotePat = error "quotePat" , quoteType = error "quoteType" , quoteDec = error "quoteDec" @@ -73,7 +73,7 @@ py_ = QuasiQuoter -- This quote creates object of type @Py PyObject@ pye :: QuasiQuoter pye = QuasiQuoter - { quoteExp = \txt -> [| evaluatorPye $(expQQ Eval txt) |] + { quoteExp = \txt -> [| uncurry evaluatorPye $(expQQ Eval txt) |] , quotePat = error "quotePat" , quoteType = error "quoteType" , quoteDec = error "quoteDec" @@ -85,7 +85,7 @@ pye = QuasiQuoter -- call return pyf :: QuasiQuoter pyf = QuasiQuoter - { quoteExp = \txt -> [| evaluatorPyf $(expQQ Fun txt) |] + { quoteExp = \txt -> [| uncurry evaluatorPyf $(expQQ Fun txt) |] , quotePat = error "quotePat" , quoteType = error "quoteType" , quoteDec = error "quoteDec" diff --git a/src/Python/Internal/EvalQQ.hs b/src/Python/Internal/EvalQQ.hs index 220e38a..a0b0a25 100644 --- a/src/Python/Internal/EvalQQ.hs +++ b/src/Python/Internal/EvalQQ.hs @@ -8,6 +8,7 @@ module Python.Internal.EvalQQ , evaluatorPye , evaluatorPyf , Code(..) + , DictBinder(..) -- * Code generation , expQQ , Mode(..) @@ -52,6 +53,42 @@ unsafeWithCode :: Code -> Program r (Ptr CChar) unsafeWithCode (Code bs) = Program $ ContT $ \fun -> Py (BS.unsafeUseAsCString bs $ unsafeRunPy . fun) + +newtype PyVarName = PyVarName BS.ByteString + deriving stock (Show, TH.Lift) + +varName :: String -> PyVarName +varName = PyVarName . T.encodeUtf8 . T.pack + +unsafeWithPyVarName :: PyVarName -> Program r (Ptr CChar) +unsafeWithPyVarName (PyVarName bs) = Program $ ContT $ \fun -> + Py (BS.unsafeUseAsCString bs $ unsafeRunPy . fun) + + + +newtype DictBinder = DictBinder { bind :: Ptr PyObject -> Py () } + +instance Semigroup DictBinder where + f <> g = DictBinder $ \p -> f.bind p >> g.bind p +instance Monoid DictBinder where + mempty = DictBinder $ \_ -> pure () + + +bindVar :: ToPy a => PyVarName -> a -> DictBinder +bindVar var a = DictBinder $ \p_dict -> runProgram $ do + p_key <- unsafeWithPyVarName var + p_obj <- takeOwnership =<< progPy (throwOnNULL =<< basicToPy a) + progPy $ do + r <- Py [C.block| int { + PyObject* p_obj = $(PyObject* p_obj); + return PyDict_SetItemString($(PyObject* p_dict), $(char* p_key), p_obj); + } |] + case r of + 0 -> pure () + _ -> mustThrowPyError + + + ---------------------------------------------------------------- -- Evaluators ---------------------------------------------------------------- @@ -105,53 +142,44 @@ pyEvalExpr p_globals p_locals src = runProgram $ do progPy $ newPyObject p_res -evaluatorPymain :: (Ptr PyObject -> Py Code) -> Py () -evaluatorPymain getSource = do +evaluatorPymain :: Code -> DictBinder -> Py () +evaluatorPymain code binder = do p_main <- basicMainDict - src <- getSource p_main - pyExecExpr p_main p_main src + binder.bind p_main + pyExecExpr p_main p_main code -evaluatorPy_ :: (Ptr PyObject -> Py Code) -> Py () -evaluatorPy_ getSource = runProgram $ do +evaluatorPy_ :: Code -> DictBinder -> Py () +evaluatorPy_ code binder = runProgram $ do p_globals <- progPy basicMainDict p_locals <- takeOwnership =<< progPy basicNewDict - progPy $ pyExecExpr p_globals p_locals =<< getSource p_locals + progPy $ do + binder.bind p_locals + pyExecExpr p_globals p_locals code -evaluatorPye :: (Ptr PyObject -> Py Code) -> Py PyObject -evaluatorPye getSource = runProgram $ do +evaluatorPye :: Code -> DictBinder -> Py PyObject +evaluatorPye code binder = runProgram $ do p_globals <- progPy basicMainDict p_locals <- takeOwnership =<< progPy basicNewDict - progPy $ pyEvalExpr p_globals p_locals =<< getSource p_locals + progPy $ do + binder.bind p_locals + pyEvalExpr p_globals p_locals code -evaluatorPyf :: (Ptr PyObject -> Py Code) -> Py PyObject -evaluatorPyf getSource = runProgram $ do +evaluatorPyf :: Code -> DictBinder -> Py PyObject +evaluatorPyf code binder = runProgram $ do p_globals <- progPy basicMainDict p_locals <- takeOwnership =<< progPy basicNewDict p_kwargs <- takeOwnership =<< progPy basicNewDict progPy $ do -- Create function in p_locals - pyExecExpr p_globals p_locals =<< getSource p_kwargs + pyExecExpr p_globals p_locals code -- Look up function + binder.bind p_kwargs p_fun <- getFunctionObject p_locals >>= \case NULL -> throwM $ PyInternalError "_inline_python_ must be present" p -> pure p -- Call python function we just constructed newPyObject =<< throwOnNULL =<< basicCallKwdOnly p_fun p_kwargs - -basicBindInDict :: ToPy a => String -> a -> Ptr PyObject -> Py () -basicBindInDict name a p_dict = runProgram $ do - p_key <- withPyCString name - p_obj <- takeOwnership =<< progPy (throwOnNULL =<< basicToPy a) - progPy $ do - r <- Py [C.block| int { - PyObject* p_obj = $(PyObject* p_obj); - return PyDict_SetItemString($(PyObject* p_dict), $(char* p_key), p_obj); - } |] - case r of - 0 -> pure () - _ -> mustThrowPyError - -- | Return dict of @__main__@ module basicMainDict :: Py (Ptr PyObject) basicMainDict = Py [CU.block| PyObject* { @@ -213,14 +241,14 @@ expQQ mode qq_src = do case code of ExitSuccess -> pure $ words stdout ExitFailure{} -> fail stderr - let args = [ [| basicBindInDict $(TH.lift nm) $(TH.dyn (chop nm)) |] + let args = [ [| bindVar $(TH.lift (varName nm)) $(TH.dyn (chop nm)) |] | nm <- antis ] src_eval = prepareForEval mode antis src -- - [| \p_dict -> do - mapM_ ($ p_dict) $(TH.listE args) - pure $(TH.lift $ Code $ T.encodeUtf8 $ T.pack src_eval) + [| ( $(TH.lift $ Code $ T.encodeUtf8 $ T.pack src_eval) + , mconcat $(TH.listE args) + ) |] From 27f6c887ade6767b4a8557f0f55094cbff300580 Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Wed, 16 Apr 2025 18:52:26 +0300 Subject: [PATCH 03/10] Introduce PyQuote It should be used as part of API for custom exec/eval --- src/Python/Inline/QQ.hs | 8 +++--- src/Python/Internal/EvalQQ.hs | 52 +++++++++++++++++++++++++---------- 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/src/Python/Inline/QQ.hs b/src/Python/Inline/QQ.hs index 122cd70..fdefa12 100644 --- a/src/Python/Inline/QQ.hs +++ b/src/Python/Inline/QQ.hs @@ -48,7 +48,7 @@ import Python.Internal.EvalQQ -- It creates value of type @Py ()@ pymain :: QuasiQuoter pymain = QuasiQuoter - { quoteExp = \txt -> [| uncurry evaluatorPymain $(expQQ Exec txt) |] + { quoteExp = \txt -> [| evaluatorPymain $(expQQ Exec txt) |] , quotePat = error "quotePat" , quoteType = error "quoteType" , quoteDec = error "quoteDec" @@ -61,7 +61,7 @@ pymain = QuasiQuoter -- It creates value of type @Py ()@ py_ :: QuasiQuoter py_ = QuasiQuoter - { quoteExp = \txt -> [| uncurry evaluatorPy_ $(expQQ Exec txt) |] + { quoteExp = \txt -> [| evaluatorPy_ $(expQQ Exec txt) |] , quotePat = error "quotePat" , quoteType = error "quoteType" , quoteDec = error "quoteDec" @@ -73,7 +73,7 @@ py_ = QuasiQuoter -- This quote creates object of type @Py PyObject@ pye :: QuasiQuoter pye = QuasiQuoter - { quoteExp = \txt -> [| uncurry evaluatorPye $(expQQ Eval txt) |] + { quoteExp = \txt -> [| evaluatorPye $(expQQ Eval txt) |] , quotePat = error "quotePat" , quoteType = error "quoteType" , quoteDec = error "quoteDec" @@ -85,7 +85,7 @@ pye = QuasiQuoter -- call return pyf :: QuasiQuoter pyf = QuasiQuoter - { quoteExp = \txt -> [| uncurry evaluatorPyf $(expQQ Fun txt) |] + { quoteExp = \txt -> [| evaluatorPyf $(expQQ Fun txt) |] , quotePat = error "quotePat" , quoteType = error "quoteType" , quoteDec = error "quoteDec" diff --git a/src/Python/Internal/EvalQQ.hs b/src/Python/Internal/EvalQQ.hs index a0b0a25..e881b29 100644 --- a/src/Python/Internal/EvalQQ.hs +++ b/src/Python/Internal/EvalQQ.hs @@ -7,8 +7,11 @@ module Python.Internal.EvalQQ , evaluatorPy_ , evaluatorPye , evaluatorPyf - , Code(..) - , DictBinder(..) + , Code + , codeFromText + , codeFromString + , DictBinder + , PyQuote(..) -- * Code generation , expQQ , Mode(..) @@ -46,14 +49,34 @@ C.context (C.baseCtx <> pyCtx) C.include "" ---------------------------------------------------------------- +data PyQuote = PyQuote + { code :: !Code + , binder :: !DictBinder + } + + +-- | UTF-8 encoded python source code. Usually it's produced by +-- Template Haskell's 'TH.lift' function. newtype Code = Code BS.ByteString deriving stock (Show, TH.Lift) +-- | Create properly encoded @Code@. This function doesn't check +-- syntactic validity. +codeFromText :: T.Text -> Code +codeFromText = Code . T.encodeUtf8 + +-- | Create properly encoded @Code@. This function doesn't check +-- syntactic validity. +codeFromString :: String -> Code +codeFromString = codeFromText . T.pack + unsafeWithCode :: Code -> Program r (Ptr CChar) unsafeWithCode (Code bs) = Program $ ContT $ \fun -> Py (BS.unsafeUseAsCString bs $ unsafeRunPy . fun) +-- | Python's variable name encoded using UTF-8. It exists in order to +-- avoid working with @String@ at runtime. newtype PyVarName = PyVarName BS.ByteString deriving stock (Show, TH.Lift) @@ -65,7 +88,7 @@ unsafeWithPyVarName (PyVarName bs) = Program $ ContT $ \fun -> Py (BS.unsafeUseAsCString bs $ unsafeRunPy . fun) - +-- | Closure which stores values in provided dictionary newtype DictBinder = DictBinder { bind :: Ptr PyObject -> Py () } instance Semigroup DictBinder where @@ -79,7 +102,7 @@ bindVar var a = DictBinder $ \p_dict -> runProgram $ do p_key <- unsafeWithPyVarName var p_obj <- takeOwnership =<< progPy (throwOnNULL =<< basicToPy a) progPy $ do - r <- Py [C.block| int { + r <- Py [CU.block| int { PyObject* p_obj = $(PyObject* p_obj); return PyDict_SetItemString($(PyObject* p_dict), $(char* p_key), p_obj); } |] @@ -142,30 +165,30 @@ pyEvalExpr p_globals p_locals src = runProgram $ do progPy $ newPyObject p_res -evaluatorPymain :: Code -> DictBinder -> Py () -evaluatorPymain code binder = do +evaluatorPymain :: PyQuote -> Py () +evaluatorPymain (PyQuote code binder) = do p_main <- basicMainDict binder.bind p_main pyExecExpr p_main p_main code -evaluatorPy_ :: Code -> DictBinder -> Py () -evaluatorPy_ code binder = runProgram $ do +evaluatorPy_ :: PyQuote -> Py () +evaluatorPy_ (PyQuote code binder) = runProgram $ do p_globals <- progPy basicMainDict p_locals <- takeOwnership =<< progPy basicNewDict progPy $ do binder.bind p_locals pyExecExpr p_globals p_locals code -evaluatorPye :: Code -> DictBinder -> Py PyObject -evaluatorPye code binder = runProgram $ do +evaluatorPye :: PyQuote -> Py PyObject +evaluatorPye (PyQuote code binder) = runProgram $ do p_globals <- progPy basicMainDict p_locals <- takeOwnership =<< progPy basicNewDict progPy $ do binder.bind p_locals pyEvalExpr p_globals p_locals code -evaluatorPyf :: Code -> DictBinder -> Py PyObject -evaluatorPyf code binder = runProgram $ do +evaluatorPyf :: PyQuote -> Py PyObject +evaluatorPyf (PyQuote code binder) = runProgram $ do p_globals <- progPy basicMainDict p_locals <- takeOwnership =<< progPy basicNewDict p_kwargs <- takeOwnership =<< progPy basicNewDict @@ -246,9 +269,8 @@ expQQ mode qq_src = do ] src_eval = prepareForEval mode antis src -- - [| ( $(TH.lift $ Code $ T.encodeUtf8 $ T.pack src_eval) - , mconcat $(TH.listE args) - ) + [| PyQuote ($(TH.lift $ codeFromString src_eval)) + (mconcat $(TH.listE args)) |] From 7bbf5f062a44ee12e261461b74a93816e3be9142 Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Fri, 18 Apr 2025 02:48:47 +0300 Subject: [PATCH 04/10] Add more flexible interface for eval/exec --- inline-python.cabal | 1 + src/Python/Inline/Eval.hs | 127 ++++++++++++++++++++++++++++++++++ src/Python/Internal/EvalQQ.hs | 3 +- 3 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 src/Python/Inline/Eval.hs diff --git a/inline-python.cabal b/inline-python.cabal index 2d47b65..c987d45 100644 --- a/inline-python.cabal +++ b/inline-python.cabal @@ -70,6 +70,7 @@ Library Python.Inline Python.Inline.Literal Python.Inline.QQ + Python.Inline.Eval Python.Inline.Types Other-modules: Python.Internal.CAPI diff --git a/src/Python/Inline/Eval.hs b/src/Python/Inline/Eval.hs new file mode 100644 index 0000000..886a748 --- /dev/null +++ b/src/Python/Inline/Eval.hs @@ -0,0 +1,127 @@ +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE TemplateHaskell #-} +-- | +-- Interface to python's @eval@ and @exec@ +module Python.Inline.Eval + ( -- * Python execution + eval + , exec + -- * Source code + , PyQuote(..) + , Namespace(..) + , Main(..) + , Temp(..) + -- ** Data types + , Code + , codeFromText + , codeFromString + , DictBinder + + ) where + +import Foreign.Ptr + +import Language.C.Inline qualified as C +import Language.C.Inline.Unsafe qualified as CU + +import Python.Internal.Types +import Python.Internal.Eval +import Python.Internal.EvalQQ +import Python.Internal.CAPI +import Python.Internal.Program + +---------------------------------------------------------------- +C.context (C.baseCtx <> pyCtx) +C.include "" +---------------------------------------------------------------- + + +-- | Evaluate python expression +eval :: (Namespace global, Namespace local) + => global -- ^ Data type providing global variables dictionary + -> local -- ^ Data type providing local variables dictionary + -> PyQuote -- ^ Source code + -> Py PyObject +eval globals locals q = runProgram $ do + p_py <- unsafeWithCode q.code + p_globals <- takeOwnership =<< progPy (basicNamespaceDict globals) + p_locals <- takeOwnership =<< progPy (basicNamespaceDict locals) + progPy $ do + q.binder.bind p_locals + p_res <- Py [C.block| PyObject* { + PyObject* globals = $(PyObject* p_globals); + PyObject* locals = $(PyObject* p_locals); + // Compile code + PyObject *code = Py_CompileString($(char* p_py), "", Py_eval_input); + if( PyErr_Occurred() ) { + return NULL; + } + // Evaluate expression + PyObject* r = PyEval_EvalCode(code, globals, locals); + Py_DECREF(code); + return r; + }|] + checkThrowPyError + newPyObject p_res +{-# SPECIALIZE eval :: Main -> Temp -> PyQuote -> Py PyObject #-} + +-- | Evaluate sequence of python statements +exec :: (Namespace global, Namespace local) + => global -- ^ Data type providing global variables dictionary + -> local -- ^ Data type providing local variables dictionary + -> PyQuote -- ^ Source code + -> Py () +exec globals locals q = runProgram $ do + p_py <- unsafeWithCode q.code + p_globals <- takeOwnership =<< progPy (basicNamespaceDict globals) + p_locals <- takeOwnership =<< progPy (basicNamespaceDict locals) + progPy $ do + q.binder.bind p_locals + Py[C.block| void { + PyObject* globals = $(PyObject* p_globals); + PyObject* locals = $(PyObject* p_locals); + // Compile code + PyObject *code = Py_CompileString($(char* p_py), "", Py_file_input); + if( PyErr_Occurred() ){ + return; + } + // Execute statements + PyObject* res = PyEval_EvalCode(code, globals, locals); + Py_XDECREF(res); + Py_DECREF(code); + } |] + checkThrowPyError +{-# SPECIALIZE exec :: Main -> Main -> PyQuote -> Py () #-} +{-# SPECIALIZE exec :: Main -> Temp -> PyQuote -> Py () #-} + + + +-- | Type class for values representing python dictionaries containing +-- global or local variables. +class Namespace a where + -- | Returns dictionary object. Caller takes ownership of returned + -- object. + basicNamespaceDict :: a -> Py (Ptr PyObject) + + +-- | Namespace for the top level code execution. +data Main = Main + + +instance Namespace Main where + -- NOTE: dupe of basicMainDict + basicNamespaceDict _ = + throwOnNULL =<< Py [CU.block| PyObject* { + PyObject* main_module = PyImport_AddModule("__main__"); + if( PyErr_Occurred() ) + return NULL; + PyObject* dict = PyModule_GetDict(main_module); + Py_XINCREF(dict); + return dict; + }|] + +-- | Temporary namespace which get destroyed after execution +data Temp = Temp + +instance Namespace Temp where + basicNamespaceDict _ = basicNewDict diff --git a/src/Python/Internal/EvalQQ.hs b/src/Python/Internal/EvalQQ.hs index e881b29..ea66b66 100644 --- a/src/Python/Internal/EvalQQ.hs +++ b/src/Python/Internal/EvalQQ.hs @@ -10,7 +10,8 @@ module Python.Internal.EvalQQ , Code , codeFromText , codeFromString - , DictBinder + , unsafeWithCode + , DictBinder(..) , PyQuote(..) -- * Code generation , expQQ From 4201e87ed912c0ac8a8d1a4ee44cdbc1146214c0 Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Sat, 19 Apr 2025 18:27:24 +0300 Subject: [PATCH 05/10] Add test for [pyf| --- test/TST/FromPy.hs | 6 +++--- test/TST/Run.hs | 6 ++++++ test/TST/Util.hs | 2 ++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/test/TST/FromPy.hs b/test/TST/FromPy.hs index 0727640..43ddce1 100644 --- a/test/TST/FromPy.hs +++ b/test/TST/FromPy.hs @@ -11,6 +11,9 @@ import Python.Inline import Python.Inline.QQ import Data.Complex (Complex((:+))) +import TST.Util + + tests :: TestTree tests = testGroup "FromPy" [ testGroup "Int" @@ -91,9 +94,6 @@ tests = testGroup "FromPy" ] ] -eq :: (Eq a, Show a, FromPy a) => Maybe a -> (Py PyObject) -> IO () -eq a action = assertEqual "fromPy: " a =<< runPy (fromPy =<< action) - failE :: forall a. (Eq a, Show a, FromPy a) => PyObject -> Py () failE p = fromPyEither @a p >>= \case Left PyError{} -> pure () diff --git a/test/TST/Run.hs b/test/TST/Run.hs index 6c8c442..5302779 100644 --- a/test/TST/Run.hs +++ b/test/TST/Run.hs @@ -131,6 +131,12 @@ tests = testGroup "Run python" except NameError: pass |] + , testCase "pyf works" $ do + let x = 12 :: Int + eq (Just (482412::Int)) [pyf| + xs = [i*x_hs for i in [1, 200, 40000]] + return sum(xs) + |] ] data Stop = Stop diff --git a/test/TST/Util.hs b/test/TST/Util.hs index df918e8..826dfe5 100644 --- a/test/TST/Util.hs +++ b/test/TST/Util.hs @@ -16,3 +16,5 @@ throwsPyIO :: IO () -> IO () throwsPyIO io = (io >> assertFailure "Evaluation should raise python exception") `catch` (\(_::PyError) -> pure ()) +eq :: (Eq a, Show a, FromPy a) => Maybe a -> (Py PyObject) -> IO () +eq a action = assertEqual "fromPy: " a =<< runPy (fromPy =<< action) From 67e11b0b27c3598272472e5f3701810c3194fb03 Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Sat, 19 Apr 2025 18:48:49 +0300 Subject: [PATCH 06/10] Implement quasiquotes in term of simple eval/exec Except pyf. It's more complicated --- src/Python/Inline/Eval.hs | 103 -------------------------- src/Python/Inline/QQ.hs | 7 +- src/Python/Internal/Eval.hs | 118 +++++++++++++++++++++++++++++ src/Python/Internal/EvalQQ.hs | 135 ++-------------------------------- src/Python/Internal/Types.hs | 48 +++++++++++- 5 files changed, 174 insertions(+), 237 deletions(-) diff --git a/src/Python/Inline/Eval.hs b/src/Python/Inline/Eval.hs index 886a748..6dac51b 100644 --- a/src/Python/Inline/Eval.hs +++ b/src/Python/Inline/Eval.hs @@ -19,109 +19,6 @@ module Python.Inline.Eval ) where -import Foreign.Ptr - -import Language.C.Inline qualified as C -import Language.C.Inline.Unsafe qualified as CU - import Python.Internal.Types import Python.Internal.Eval -import Python.Internal.EvalQQ -import Python.Internal.CAPI -import Python.Internal.Program - ----------------------------------------------------------------- -C.context (C.baseCtx <> pyCtx) -C.include "" ----------------------------------------------------------------- - - --- | Evaluate python expression -eval :: (Namespace global, Namespace local) - => global -- ^ Data type providing global variables dictionary - -> local -- ^ Data type providing local variables dictionary - -> PyQuote -- ^ Source code - -> Py PyObject -eval globals locals q = runProgram $ do - p_py <- unsafeWithCode q.code - p_globals <- takeOwnership =<< progPy (basicNamespaceDict globals) - p_locals <- takeOwnership =<< progPy (basicNamespaceDict locals) - progPy $ do - q.binder.bind p_locals - p_res <- Py [C.block| PyObject* { - PyObject* globals = $(PyObject* p_globals); - PyObject* locals = $(PyObject* p_locals); - // Compile code - PyObject *code = Py_CompileString($(char* p_py), "", Py_eval_input); - if( PyErr_Occurred() ) { - return NULL; - } - // Evaluate expression - PyObject* r = PyEval_EvalCode(code, globals, locals); - Py_DECREF(code); - return r; - }|] - checkThrowPyError - newPyObject p_res -{-# SPECIALIZE eval :: Main -> Temp -> PyQuote -> Py PyObject #-} - --- | Evaluate sequence of python statements -exec :: (Namespace global, Namespace local) - => global -- ^ Data type providing global variables dictionary - -> local -- ^ Data type providing local variables dictionary - -> PyQuote -- ^ Source code - -> Py () -exec globals locals q = runProgram $ do - p_py <- unsafeWithCode q.code - p_globals <- takeOwnership =<< progPy (basicNamespaceDict globals) - p_locals <- takeOwnership =<< progPy (basicNamespaceDict locals) - progPy $ do - q.binder.bind p_locals - Py[C.block| void { - PyObject* globals = $(PyObject* p_globals); - PyObject* locals = $(PyObject* p_locals); - // Compile code - PyObject *code = Py_CompileString($(char* p_py), "", Py_file_input); - if( PyErr_Occurred() ){ - return; - } - // Execute statements - PyObject* res = PyEval_EvalCode(code, globals, locals); - Py_XDECREF(res); - Py_DECREF(code); - } |] - checkThrowPyError -{-# SPECIALIZE exec :: Main -> Main -> PyQuote -> Py () #-} -{-# SPECIALIZE exec :: Main -> Temp -> PyQuote -> Py () #-} - - - --- | Type class for values representing python dictionaries containing --- global or local variables. -class Namespace a where - -- | Returns dictionary object. Caller takes ownership of returned - -- object. - basicNamespaceDict :: a -> Py (Ptr PyObject) - - --- | Namespace for the top level code execution. -data Main = Main - - -instance Namespace Main where - -- NOTE: dupe of basicMainDict - basicNamespaceDict _ = - throwOnNULL =<< Py [CU.block| PyObject* { - PyObject* main_module = PyImport_AddModule("__main__"); - if( PyErr_Occurred() ) - return NULL; - PyObject* dict = PyModule_GetDict(main_module); - Py_XINCREF(dict); - return dict; - }|] - --- | Temporary namespace which get destroyed after execution -data Temp = Temp -instance Namespace Temp where - basicNamespaceDict _ = basicNewDict diff --git a/src/Python/Inline/QQ.hs b/src/Python/Inline/QQ.hs index fdefa12..cc8f582 100644 --- a/src/Python/Inline/QQ.hs +++ b/src/Python/Inline/QQ.hs @@ -39,6 +39,7 @@ module Python.Inline.QQ import Language.Haskell.TH.Quote import Python.Internal.EvalQQ +import Python.Internal.Eval -- | Evaluate sequence of python statements. It works in the same way @@ -48,7 +49,7 @@ import Python.Internal.EvalQQ -- It creates value of type @Py ()@ pymain :: QuasiQuoter pymain = QuasiQuoter - { quoteExp = \txt -> [| evaluatorPymain $(expQQ Exec txt) |] + { quoteExp = \txt -> [| exec Main Main $(expQQ Exec txt) |] , quotePat = error "quotePat" , quoteType = error "quoteType" , quoteDec = error "quoteDec" @@ -61,7 +62,7 @@ pymain = QuasiQuoter -- It creates value of type @Py ()@ py_ :: QuasiQuoter py_ = QuasiQuoter - { quoteExp = \txt -> [| evaluatorPy_ $(expQQ Exec txt) |] + { quoteExp = \txt -> [| exec Main Temp $(expQQ Exec txt) |] , quotePat = error "quotePat" , quoteType = error "quoteType" , quoteDec = error "quoteDec" @@ -73,7 +74,7 @@ py_ = QuasiQuoter -- This quote creates object of type @Py PyObject@ pye :: QuasiQuoter pye = QuasiQuoter - { quoteExp = \txt -> [| evaluatorPye $(expQQ Eval txt) |] + { quoteExp = \txt -> [| eval Main Temp $(expQQ Eval txt) |] , quotePat = error "quotePat" , quoteType = error "quoteType" , quoteDec = error "quoteDec" diff --git a/src/Python/Internal/Eval.hs b/src/Python/Internal/Eval.hs index ca5970d..8c4eb8d 100644 --- a/src/Python/Internal/Eval.hs +++ b/src/Python/Internal/Eval.hs @@ -29,6 +29,14 @@ module Python.Internal.Eval , mustThrowPyError , checkThrowBadPyType , throwOnNULL + -- * Exec & eval + , Namespace(..) + , Main(..) + , Temp(..) + , PtrNamespace(..) + , unsafeWithCode + , eval + , exec -- * Debugging , debugPrintPy ) where @@ -42,6 +50,8 @@ import Control.Monad.IO.Class import Control.Monad.Trans.Cont import Data.Maybe import Data.Function +import Data.ByteString qualified as BS +import Data.ByteString.Unsafe qualified as BS import Foreign.Concurrent qualified as GHC import Foreign.Ptr import Foreign.ForeignPtr @@ -662,6 +672,114 @@ checkThrowBadPyType = do _ -> throwM BadPyType +---------------------------------------------------------------- +-- Eval/exec +---------------------------------------------------------------- + +-- | Type class for values representing python dictionaries containing +-- global or local variables. +class Namespace a where + -- | Returns dictionary object. Caller takes ownership of returned + -- object. + basicNamespaceDict :: a -> Py (Ptr PyObject) + + +-- | Namespace for the top level code execution. +data Main = Main + + +instance Namespace Main where + -- NOTE: almost dupe of basicMainDict + basicNamespaceDict _ = + throwOnNULL =<< Py [CU.block| PyObject* { + PyObject* main_module = PyImport_AddModule("__main__"); + if( PyErr_Occurred() ) + return NULL; + PyObject* dict = PyModule_GetDict(main_module); + Py_XINCREF(dict); + return dict; + }|] + +-- | Temporary namespace which get destroyed after execution +data Temp = Temp + +instance Namespace Temp where + basicNamespaceDict _ = basicNewDict + +-- | Newtype wrapper for bare python object. It's assumed to be a +-- dictionary. This is not checked. +newtype PtrNamespace = PtrNamespace (Ptr PyObject) + +instance Namespace PtrNamespace where + basicNamespaceDict (PtrNamespace p) = do + Py [CU.block| void { Py_XINCREF($(PyObject* p)); } |] + return p + + +-- | Evaluate python expression +eval :: (Namespace global, Namespace local) + => global -- ^ Data type providing global variables dictionary + -> local -- ^ Data type providing local variables dictionary + -> PyQuote -- ^ Source code + -> Py PyObject +eval globals locals q = runProgram $ do + p_py <- unsafeWithCode q.code + p_globals <- takeOwnership =<< progPy (basicNamespaceDict globals) + p_locals <- takeOwnership =<< progPy (basicNamespaceDict locals) + progPy $ do + q.binder.bind p_locals + p_res <- Py [C.block| PyObject* { + PyObject* globals = $(PyObject* p_globals); + PyObject* locals = $(PyObject* p_locals); + // Compile code + PyObject *code = Py_CompileString($(char* p_py), "", Py_eval_input); + if( PyErr_Occurred() ) { + return NULL; + } + // Evaluate expression + PyObject* r = PyEval_EvalCode(code, globals, locals); + Py_DECREF(code); + return r; + }|] + checkThrowPyError + newPyObject p_res +{-# SPECIALIZE eval :: Main -> Temp -> PyQuote -> Py PyObject #-} + +-- | Evaluate sequence of python statements +exec :: (Namespace global, Namespace local) + => global -- ^ Data type providing global variables dictionary + -> local -- ^ Data type providing local variables dictionary + -> PyQuote -- ^ Source code + -> Py () +exec globals locals q = runProgram $ do + p_py <- unsafeWithCode q.code + p_globals <- takeOwnership =<< progPy (basicNamespaceDict globals) + p_locals <- takeOwnership =<< progPy (basicNamespaceDict locals) + progPy $ do + q.binder.bind p_locals + Py[C.block| void { + PyObject* globals = $(PyObject* p_globals); + PyObject* locals = $(PyObject* p_locals); + // Compile code + PyObject *code = Py_CompileString($(char* p_py), "", Py_file_input); + if( PyErr_Occurred() ){ + return; + } + // Execute statements + PyObject* res = PyEval_EvalCode(code, globals, locals); + Py_XDECREF(res); + Py_DECREF(code); + } |] + checkThrowPyError +{-# SPECIALIZE exec :: Main -> Main -> PyQuote -> Py () #-} +{-# SPECIALIZE exec :: Main -> Temp -> PyQuote -> Py () #-} + +-- | Obtain pointer to code +unsafeWithCode :: Code -> Program r (Ptr CChar) +unsafeWithCode (Code bs) = Program $ ContT $ \fun -> + Py (BS.unsafeUseAsCString bs $ unsafeRunPy . fun) + + ---------------------------------------------------------------- -- Debugging ---------------------------------------------------------------- diff --git a/src/Python/Internal/EvalQQ.hs b/src/Python/Internal/EvalQQ.hs index ea66b66..da90914 100644 --- a/src/Python/Internal/EvalQQ.hs +++ b/src/Python/Internal/EvalQQ.hs @@ -3,16 +3,7 @@ -- | module Python.Internal.EvalQQ ( -- * Evaluators and QQ - evaluatorPymain - , evaluatorPy_ - , evaluatorPye - , evaluatorPyf - , Code - , codeFromText - , codeFromString - , unsafeWithCode - , DictBinder(..) - , PyQuote(..) + evaluatorPyf -- * Code generation , expQQ , Mode(..) @@ -50,32 +41,6 @@ C.context (C.baseCtx <> pyCtx) C.include "" ---------------------------------------------------------------- -data PyQuote = PyQuote - { code :: !Code - , binder :: !DictBinder - } - - --- | UTF-8 encoded python source code. Usually it's produced by --- Template Haskell's 'TH.lift' function. -newtype Code = Code BS.ByteString - deriving stock (Show, TH.Lift) - --- | Create properly encoded @Code@. This function doesn't check --- syntactic validity. -codeFromText :: T.Text -> Code -codeFromText = Code . T.encodeUtf8 - --- | Create properly encoded @Code@. This function doesn't check --- syntactic validity. -codeFromString :: String -> Code -codeFromString = codeFromText . T.pack - -unsafeWithCode :: Code -> Program r (Ptr CChar) -unsafeWithCode (Code bs) = Program $ ContT $ \fun -> - Py (BS.unsafeUseAsCString bs $ unsafeRunPy . fun) - - -- | Python's variable name encoded using UTF-8. It exists in order to -- avoid working with @String@ at runtime. newtype PyVarName = PyVarName BS.ByteString @@ -89,15 +54,6 @@ unsafeWithPyVarName (PyVarName bs) = Program $ ContT $ \fun -> Py (BS.unsafeUseAsCString bs $ unsafeRunPy . fun) --- | Closure which stores values in provided dictionary -newtype DictBinder = DictBinder { bind :: Ptr PyObject -> Py () } - -instance Semigroup DictBinder where - f <> g = DictBinder $ \p -> f.bind p >> g.bind p -instance Monoid DictBinder where - mempty = DictBinder $ \_ -> pure () - - bindVar :: ToPy a => PyVarName -> a -> DictBinder bindVar var a = DictBinder $ \p_dict -> runProgram $ do p_key <- unsafeWithPyVarName var @@ -117,102 +73,21 @@ bindVar var a = DictBinder $ \p_dict -> runProgram $ do -- Evaluators ---------------------------------------------------------------- --- | Evaluate expression within context of @__main__@ module. All --- variables defined in this evaluator persist. -pyExecExpr - :: Ptr PyObject -- ^ Globals - -> Ptr PyObject -- ^ Locals - -> Code -- ^ Python source code - -> Py () -pyExecExpr p_globals p_locals src = runProgram $ do - p_py <- unsafeWithCode src - progIO [C.block| void { - PyObject* globals = $(PyObject* p_globals); - PyObject* locals = $(PyObject* p_locals); - // Compile code - PyObject *code = Py_CompileString($(char* p_py), "", Py_file_input); - if( PyErr_Occurred() ){ - return; - } - // Execute statements - PyObject* res = PyEval_EvalCode(code, globals, locals); - Py_XDECREF(res); - Py_DECREF(code); - } |] - progPy checkThrowPyError - --- | Evaluate expression with fresh local environment -pyEvalExpr - :: Ptr PyObject -- ^ Globals - -> Ptr PyObject -- ^ Locals - -> Code -- ^ Python source code - -> Py PyObject -pyEvalExpr p_globals p_locals src = runProgram $ do - p_py <- unsafeWithCode src - p_res <- progIO [C.block| PyObject* { - PyObject* globals = $(PyObject* p_globals); - PyObject* locals = $(PyObject* p_locals); - // Compile code - PyObject *code = Py_CompileString($(char* p_py), "", Py_eval_input); - if( PyErr_Occurred() ) { - return NULL; - } - // Evaluate expression - PyObject* r = PyEval_EvalCode(code, globals, locals); - Py_DECREF(code); - return r; - }|] - progPy $ checkThrowPyError - progPy $ newPyObject p_res - - -evaluatorPymain :: PyQuote -> Py () -evaluatorPymain (PyQuote code binder) = do - p_main <- basicMainDict - binder.bind p_main - pyExecExpr p_main p_main code - -evaluatorPy_ :: PyQuote -> Py () -evaluatorPy_ (PyQuote code binder) = runProgram $ do - p_globals <- progPy basicMainDict - p_locals <- takeOwnership =<< progPy basicNewDict - progPy $ do - binder.bind p_locals - pyExecExpr p_globals p_locals code - -evaluatorPye :: PyQuote -> Py PyObject -evaluatorPye (PyQuote code binder) = runProgram $ do - p_globals <- progPy basicMainDict - p_locals <- takeOwnership =<< progPy basicNewDict - progPy $ do - binder.bind p_locals - pyEvalExpr p_globals p_locals code - evaluatorPyf :: PyQuote -> Py PyObject evaluatorPyf (PyQuote code binder) = runProgram $ do - p_globals <- progPy basicMainDict - p_locals <- takeOwnership =<< progPy basicNewDict - p_kwargs <- takeOwnership =<< progPy basicNewDict + p_locals <- takeOwnership =<< progPy basicNewDict + p_kwargs <- takeOwnership =<< progPy basicNewDict progPy $ do -- Create function in p_locals - pyExecExpr p_globals p_locals code + exec Main (PtrNamespace p_locals) (PyQuote code mempty) -- Look up function - binder.bind p_kwargs p_fun <- getFunctionObject p_locals >>= \case NULL -> throwM $ PyInternalError "_inline_python_ must be present" p -> pure p -- Call python function we just constructed + binder.bind p_kwargs newPyObject =<< throwOnNULL =<< basicCallKwdOnly p_fun p_kwargs --- | Return dict of @__main__@ module -basicMainDict :: Py (Ptr PyObject) -basicMainDict = Py [CU.block| PyObject* { - PyObject* main_module = PyImport_AddModule("__main__"); - if( PyErr_Occurred() ) - return NULL; - return PyModule_GetDict(main_module); - }|] - getFunctionObject :: Ptr PyObject -> Py (Ptr PyObject) getFunctionObject p_dict = do Py [CU.exp| PyObject* { PyDict_GetItemString($(PyObject *p_dict), "_inline_python_") } |] diff --git a/src/Python/Internal/Types.hs b/src/Python/Internal/Types.hs index 3e615fe..37283f1 100644 --- a/src/Python/Internal/Types.hs +++ b/src/Python/Internal/Types.hs @@ -16,6 +16,12 @@ module Python.Internal.Types , PyInternalError(..) , Py(..) , pyIO + -- ** Python code wrappers + , PyQuote(..) + , Code(..) + , codeFromText + , codeFromString + , DictBinder(..) -- * inline-C , pyCtx -- * Patterns @@ -33,9 +39,13 @@ import Control.Monad.Primitive (PrimMonad(..),RealWorld) import Control.Exception import Data.Coerce import Data.Int +import Data.ByteString qualified as BS import Data.Map.Strict qualified as Map +import Data.Text qualified as T +import Data.Text.Encoding qualified as T import Foreign.Ptr import Foreign.C.Types +import Language.Haskell.TH.Syntax qualified as TH import GHC.ForeignPtr import Language.C.Types @@ -73,7 +83,7 @@ data PyError | PythonNotInitialized -- ^ Python interpreter is not initialized | PythonIsFinalized - -- ^ Python interpreter is not initialized + -- ^ Python interpreter is not initialized deriving stock (Show) deriving anyclass (Exception) @@ -116,6 +126,42 @@ instance PrimMonad Py where {-# INLINE primitive #-} +---------------------------------------------------------------- +-- Code wrappers +---------------------------------------------------------------- + +-- | Quasiquoted python code. It contains source code and closure +-- which populates dictionary with local variables. +data PyQuote = PyQuote + { code :: !Code + , binder :: !DictBinder + } + + +-- | UTF-8 encoded python source code. Usually it's produced by +-- Template Haskell's 'TH.lift' function. +newtype Code = Code BS.ByteString + deriving stock (Show, TH.Lift) + +-- | Create properly encoded @Code@. This function doesn't check +-- syntactic validity. +codeFromText :: T.Text -> Code +codeFromText = Code . T.encodeUtf8 + +-- | Create properly encoded @Code@. This function doesn't check +-- syntactic validity. +codeFromString :: String -> Code +codeFromString = codeFromText . T.pack + +-- | Closure which stores values in provided dictionary +newtype DictBinder = DictBinder { bind :: Ptr PyObject -> Py () } + +instance Semigroup DictBinder where + f <> g = DictBinder $ \p -> f.bind p >> g.bind p +instance Monoid DictBinder where + mempty = DictBinder $ \_ -> pure () + + ---------------------------------------------------------------- -- inline-C ---------------------------------------------------------------- From 7265e1ebda6bf1b3ce58b0bd2d585126e185d30f Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Sat, 26 Apr 2025 16:54:29 +0300 Subject: [PATCH 07/10] Add withPyObject for symmetry --- src/Python/Internal/Types.hs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Python/Internal/Types.hs b/src/Python/Internal/Types.hs index 37283f1..c417bd2 100644 --- a/src/Python/Internal/Types.hs +++ b/src/Python/Internal/Types.hs @@ -9,6 +9,7 @@ module Python.Internal.Types ( -- * Data type PyObject(..) + , withPyObject , unsafeWithPyObject , PyThreadState , PyError(..) @@ -64,6 +65,9 @@ data PyThreadState newtype PyObject = PyObject (ForeignPtr PyObject) deriving stock Show +withPyObject :: forall a. PyObject -> (Ptr PyObject -> Py a) -> Py a +withPyObject = coerce (withForeignPtr @PyObject @a) + unsafeWithPyObject :: forall a. PyObject -> (Ptr PyObject -> Py a) -> Py a unsafeWithPyObject = coerce (unsafeWithForeignPtr @PyObject @a) From 63bce0f9a96951fb4291aa35e76f5e29ca4faa64 Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Sat, 26 Apr 2025 17:52:38 +0300 Subject: [PATCH 08/10] Rename PtrNamespace -> DictPtr and add other variants Modelu wrapper and both wrapping PyObject --- src/Python/Inline/Eval.hs | 3 ++- src/Python/Internal/Eval.hs | 46 +++++++++++++++++++++++++++++------ src/Python/Internal/EvalQQ.hs | 2 +- 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/src/Python/Inline/Eval.hs b/src/Python/Inline/Eval.hs index 6dac51b..6bd02af 100644 --- a/src/Python/Inline/Eval.hs +++ b/src/Python/Inline/Eval.hs @@ -11,12 +11,13 @@ module Python.Inline.Eval , Namespace(..) , Main(..) , Temp(..) + , Dict(..) + , Module(..) -- ** Data types , Code , codeFromText , codeFromString , DictBinder - ) where import Python.Internal.Types diff --git a/src/Python/Internal/Eval.hs b/src/Python/Internal/Eval.hs index 8c4eb8d..c5be339 100644 --- a/src/Python/Internal/Eval.hs +++ b/src/Python/Internal/Eval.hs @@ -33,7 +33,10 @@ module Python.Internal.Eval , Namespace(..) , Main(..) , Temp(..) - , PtrNamespace(..) + , Dict(..) + , DictPtr(..) + , Module(..) + , ModulePtr(..) , unsafeWithCode , eval , exec @@ -687,9 +690,7 @@ class Namespace a where -- | Namespace for the top level code execution. data Main = Main - instance Namespace Main where - -- NOTE: almost dupe of basicMainDict basicNamespaceDict _ = throwOnNULL =<< Py [CU.block| PyObject* { PyObject* main_module = PyImport_AddModule("__main__"); @@ -700,20 +701,49 @@ instance Namespace Main where return dict; }|] + -- | Temporary namespace which get destroyed after execution data Temp = Temp instance Namespace Temp where basicNamespaceDict _ = basicNewDict + +-- | Newtype wrapper for bare python object. It's assumed to be a +-- dictionary. This is not checked. +newtype DictPtr = DictPtr (Ptr PyObject) + +instance Namespace DictPtr where + basicNamespaceDict (DictPtr p) = p <$ incref p + + -- | Newtype wrapper for bare python object. It's assumed to be a -- dictionary. This is not checked. -newtype PtrNamespace = PtrNamespace (Ptr PyObject) +newtype Dict = Dict PyObject + +instance Namespace Dict where + basicNamespaceDict (Dict d) + -- NOTE: We're incrementing counter inside bracket so we're safe. + = unsafeWithPyObject d (basicNamespaceDict . DictPtr) + +-- | Newtype wrapper over module object. +newtype ModulePtr = ModulePtr (Ptr PyObject) + +instance Namespace ModulePtr where + basicNamespaceDict (ModulePtr p) = do + throwOnNULL =<< Py [CU.block| PyObject* { + PyObject* dict = PyModule_GetDict($(PyObject* p)); + Py_XINCREF(dict); + return dict; + }|] + +-- | Newtype wrapper over module object. +newtype Module = Module PyObject -instance Namespace PtrNamespace where - basicNamespaceDict (PtrNamespace p) = do - Py [CU.block| void { Py_XINCREF($(PyObject* p)); } |] - return p +instance Namespace Module where + basicNamespaceDict (Module d) + -- NOTE: We're incrementing counter inside bracket so we're safe. + = unsafeWithPyObject d (basicNamespaceDict . ModulePtr) -- | Evaluate python expression diff --git a/src/Python/Internal/EvalQQ.hs b/src/Python/Internal/EvalQQ.hs index da90914..ee475a6 100644 --- a/src/Python/Internal/EvalQQ.hs +++ b/src/Python/Internal/EvalQQ.hs @@ -79,7 +79,7 @@ evaluatorPyf (PyQuote code binder) = runProgram $ do p_kwargs <- takeOwnership =<< progPy basicNewDict progPy $ do -- Create function in p_locals - exec Main (PtrNamespace p_locals) (PyQuote code mempty) + exec Main (DictPtr p_locals) (PyQuote code mempty) -- Look up function p_fun <- getFunctionObject p_locals >>= \case NULL -> throwM $ PyInternalError "_inline_python_ must be present" From 4ed8547acf09636991f7eeb1f355a8cdf2e21bfb Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Sat, 26 Apr 2025 18:35:34 +0300 Subject: [PATCH 09/10] Add pycode quasiquoter and tests --- src/Python/Inline/QQ.hs | 13 +++++++++++++ test/TST/Run.hs | 27 +++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/src/Python/Inline/QQ.hs b/src/Python/Inline/QQ.hs index cc8f582..23c27fc 100644 --- a/src/Python/Inline/QQ.hs +++ b/src/Python/Inline/QQ.hs @@ -34,6 +34,7 @@ module Python.Inline.QQ , py_ , pye , pyf + , pycode ) where import Language.Haskell.TH.Quote @@ -91,3 +92,15 @@ pyf = QuasiQuoter , quoteType = error "quoteType" , quoteDec = error "quoteDec" } + +-- | Create quote of python code suitable for use with +-- 'Python.Inline.Eval.exec' +-- +-- It creates value of type @PyQuote@ +pycode :: QuasiQuoter +pycode = QuasiQuoter + { quoteExp = \txt -> expQQ Exec txt + , quotePat = error "quotePat" + , quoteType = error "quoteType" + , quoteDec = error "quoteDec" + } diff --git a/test/TST/Run.hs b/test/TST/Run.hs index 5302779..6f5892f 100644 --- a/test/TST/Run.hs +++ b/test/TST/Run.hs @@ -6,10 +6,12 @@ import Control.Concurrent import Control.Exception import Control.Monad import Control.Monad.IO.Class +import Data.Map.Strict qualified as Map import Test.Tasty import Test.Tasty.HUnit import Python.Inline import Python.Inline.QQ +import Python.Inline.Eval import TST.Util tests :: TestTree @@ -137,6 +139,31 @@ tests = testGroup "Run python" xs = [i*x_hs for i in [1, 200, 40000]] return sum(xs) |] + , testCase "exec with Dict" $ runPy $ do + dct <- [pye| {} |] + exec Main (Dict dct) [pycode| + a = 12 + b = 13 + |] + throwsPy $ exec Main (Module dct) [pycode| |] + d <- fromPy dct + liftIO $ assertEqual "dict" (Just (Map.fromList [("a",12::Int),("b",13)])) d + , testCase "exec with Module" $ runPy $ do + m <- [pyf| + import importlib.util + spec = importlib.util.spec_from_loader("dyn", loader=None) + return importlib.util.module_from_spec(spec) + |] + exec Main (Module m) [pycode| + a = 12 + b = 'asd' + |] + [py_| + import types + isinstance(m_hs, types.ModuleType) + assert m_hs.a == 12 + assert m_hs.b == 'asd' + |] ] data Stop = Stop From 51d4e28ac27203dcad29069456c5cbf57a51f51a Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Sat, 26 Apr 2025 18:37:49 +0300 Subject: [PATCH 10/10] Add since annotations --- src/Python/Inline/QQ.hs | 2 ++ src/Python/Internal/Eval.hs | 16 ++++++++++++++++ src/Python/Internal/Types.hs | 10 ++++++++++ 3 files changed, 28 insertions(+) diff --git a/src/Python/Inline/QQ.hs b/src/Python/Inline/QQ.hs index 23c27fc..7cb61d4 100644 --- a/src/Python/Inline/QQ.hs +++ b/src/Python/Inline/QQ.hs @@ -97,6 +97,8 @@ pyf = QuasiQuoter -- 'Python.Inline.Eval.exec' -- -- It creates value of type @PyQuote@ +-- +-- @since 0.2@ pycode :: QuasiQuoter pycode = QuasiQuoter { quoteExp = \txt -> expQQ Exec txt diff --git a/src/Python/Internal/Eval.hs b/src/Python/Internal/Eval.hs index c5be339..f538786 100644 --- a/src/Python/Internal/Eval.hs +++ b/src/Python/Internal/Eval.hs @@ -681,6 +681,8 @@ checkThrowBadPyType = do -- | Type class for values representing python dictionaries containing -- global or local variables. +-- +-- @since 0.2@ class Namespace a where -- | Returns dictionary object. Caller takes ownership of returned -- object. @@ -688,6 +690,8 @@ class Namespace a where -- | Namespace for the top level code execution. +-- +-- @since 0.2@ data Main = Main instance Namespace Main where @@ -703,6 +707,8 @@ instance Namespace Main where -- | Temporary namespace which get destroyed after execution +-- +-- @since 0.2@ data Temp = Temp instance Namespace Temp where @@ -711,6 +717,8 @@ instance Namespace Temp where -- | Newtype wrapper for bare python object. It's assumed to be a -- dictionary. This is not checked. +-- +-- @since 0.2@ newtype DictPtr = DictPtr (Ptr PyObject) instance Namespace DictPtr where @@ -719,6 +727,8 @@ instance Namespace DictPtr where -- | Newtype wrapper for bare python object. It's assumed to be a -- dictionary. This is not checked. +-- +-- @since 0.2@ newtype Dict = Dict PyObject instance Namespace Dict where @@ -727,6 +737,8 @@ instance Namespace Dict where = unsafeWithPyObject d (basicNamespaceDict . DictPtr) -- | Newtype wrapper over module object. +-- +-- @since 0.2@ newtype ModulePtr = ModulePtr (Ptr PyObject) instance Namespace ModulePtr where @@ -747,6 +759,8 @@ instance Namespace Module where -- | Evaluate python expression +-- +-- @since 0.2@ eval :: (Namespace global, Namespace local) => global -- ^ Data type providing global variables dictionary -> local -- ^ Data type providing local variables dictionary @@ -776,6 +790,8 @@ eval globals locals q = runProgram $ do {-# SPECIALIZE eval :: Main -> Temp -> PyQuote -> Py PyObject #-} -- | Evaluate sequence of python statements +-- +-- @since 0.2@ exec :: (Namespace global, Namespace local) => global -- ^ Data type providing global variables dictionary -> local -- ^ Data type providing local variables dictionary diff --git a/src/Python/Internal/Types.hs b/src/Python/Internal/Types.hs index c417bd2..d3db0f6 100644 --- a/src/Python/Internal/Types.hs +++ b/src/Python/Internal/Types.hs @@ -136,6 +136,8 @@ instance PrimMonad Py where -- | Quasiquoted python code. It contains source code and closure -- which populates dictionary with local variables. +-- +-- @since 0.2@ data PyQuote = PyQuote { code :: !Code , binder :: !DictBinder @@ -144,20 +146,28 @@ data PyQuote = PyQuote -- | UTF-8 encoded python source code. Usually it's produced by -- Template Haskell's 'TH.lift' function. +-- +-- @since 0.2@ newtype Code = Code BS.ByteString deriving stock (Show, TH.Lift) -- | Create properly encoded @Code@. This function doesn't check -- syntactic validity. +-- +-- @since 0.2@ codeFromText :: T.Text -> Code codeFromText = Code . T.encodeUtf8 -- | Create properly encoded @Code@. This function doesn't check -- syntactic validity. +-- +-- @since 0.2@ codeFromString :: String -> Code codeFromString = codeFromText . T.pack -- | Closure which stores values in provided dictionary +-- +-- @since 0.2@ newtype DictBinder = DictBinder { bind :: Ptr PyObject -> Py () } instance Semigroup DictBinder where