From b47eb89201bc3390d4cd086f0569c992f7a05020 Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Sat, 4 Jan 2025 23:21:43 +0300 Subject: [PATCH 01/17] Improve unindent It's still broken --- src/Python/Internal/EvalQQ.hs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/Python/Internal/EvalQQ.hs b/src/Python/Internal/EvalQQ.hs index dff2988..0da32d8 100644 --- a/src/Python/Internal/EvalQQ.hs +++ b/src/Python/Internal/EvalQQ.hs @@ -203,10 +203,19 @@ chop name = take (length name - length antiSuffix) name -- Python source code transform ---------------------------------------------------------------- +-- Python is indentation based and quasiquotes do not strip leading +-- space. We have to do that ourself unindent :: String -> String -unindent py = case ls of - [] -> "" - _ -> unlines $ drop n <$> ls - where - n = minimum [ length (takeWhile (==' ') s) | s <- ls ] - ls = filter (any (not . isSpace)) $ lines py +unindent py_src = case lines py_src of + [] -> "" + -- Strip all leading space for 1-line scripts + [l] -> dropWhile isSpace l + -- For multiline script we require that first line should be empty + l:ls + | any (not . isSpace) l -> error "First line of multiline quasiquote must be empty" + -- FIXME: We break multiline strings here. Badly. We need proper python lexer + -- FIXME: We probably should just forbid tabs + | otherwise -> + let non_empty = filter (any (not . isSpace)) ls + n = minimum [ length (takeWhile (==' ') s) | s <- non_empty ] + in unlines $ drop n <$> ls From fd72a4b02e8d6f3d3e3a48b92b086d04e1fc5262 Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Sat, 4 Jan 2025 23:31:12 +0300 Subject: [PATCH 02/17] Make compilation mode an enumeration --- src/Python/Inline/QQ.hs | 6 +++--- src/Python/Internal/EvalQQ.hs | 13 +++++++++++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Python/Inline/QQ.hs b/src/Python/Inline/QQ.hs index fe5ed75..9ff2fbb 100644 --- a/src/Python/Inline/QQ.hs +++ b/src/Python/Inline/QQ.hs @@ -23,7 +23,7 @@ import Python.Internal.Eval pymain :: QuasiQuoter pymain = QuasiQuoter { quoteExp = \txt -> [| runPy $ do p_main <- basicMainDict - src <- $(expQQ "exec" (unindent txt)) p_main + src <- $(expQQ Exec (unindent txt)) p_main pyEvalInMain p_main p_main src |] , quotePat = error "quotePat" @@ -40,7 +40,7 @@ py_ :: QuasiQuoter py_ = QuasiQuoter { quoteExp = \txt -> [| runPy $ do p_globals <- basicMainDict p_locals <- basicNewDict - src <- $(expQQ "exec" (unindent txt)) p_locals + src <- $(expQQ Exec (unindent txt)) p_locals res <- pyEvalInMain p_globals p_locals src basicDecref p_locals return res @@ -57,7 +57,7 @@ py_ = QuasiQuoter pye :: QuasiQuoter pye = QuasiQuoter { quoteExp = \txt -> [| runPy $ do p_env <- basicNewDict - src <- $(expQQ "eval" (unindent txt)) p_env + src <- $(expQQ Eval (unindent txt)) p_env res <- pyEvalExpr p_env src basicDecref p_env return res diff --git a/src/Python/Internal/EvalQQ.hs b/src/Python/Internal/EvalQQ.hs index 0da32d8..5323694 100644 --- a/src/Python/Internal/EvalQQ.hs +++ b/src/Python/Internal/EvalQQ.hs @@ -6,6 +6,7 @@ module Python.Internal.EvalQQ pyEvalInMain , pyEvalExpr , expQQ + , Mode(..) , basicNewDict , basicMainDict , basicBindInDict @@ -158,9 +159,13 @@ script = $( do let path = "py/bound-vars.py" TH.lift =<< TH.runIO (readFile path) ) +data Mode + = Eval + | Exec + -- | Generate TH splice which updates python environment dictionary -- and returns python source code. -expQQ :: String -- ^ Python evaluation mode: @exec@/@eval@ +expQQ :: Mode -- ^ Python evaluation mode: @exec@/@eval@ -> String -- ^ Python source code -> TH.Q TH.Exp expQQ mode src = do @@ -169,7 +174,11 @@ expQQ mode src = do -- code of QQ to a script. It can contain whatever symbols so to -- be safe it's base16 encode. This encoding is very simple and we -- don't care much about efficiency here - (code, stdout, stderr) <- readProcessWithExitCode "python" ["-", mode] + (code, stdout, stderr) <- readProcessWithExitCode "python" + [ "-" + , case mode of Eval -> "eval" + Exec -> "exec" + ] $ unlines [ script , "decode_and_print('" <> concat [ [ intToDigit $ fromIntegral (w `shiftR` 4) From 3e39fe1e7232e159b15fc948c819c1cb9b995fd5 Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Sat, 4 Jan 2025 23:38:51 +0300 Subject: [PATCH 03/17] Preprocess python source code in EvalQQ --- src/Python/Inline/QQ.hs | 6 +++--- src/Python/Internal/EvalQQ.hs | 9 ++++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Python/Inline/QQ.hs b/src/Python/Inline/QQ.hs index 9ff2fbb..5467af7 100644 --- a/src/Python/Inline/QQ.hs +++ b/src/Python/Inline/QQ.hs @@ -23,7 +23,7 @@ import Python.Internal.Eval pymain :: QuasiQuoter pymain = QuasiQuoter { quoteExp = \txt -> [| runPy $ do p_main <- basicMainDict - src <- $(expQQ Exec (unindent txt)) p_main + src <- $(expQQ Exec txt) p_main pyEvalInMain p_main p_main src |] , quotePat = error "quotePat" @@ -40,7 +40,7 @@ py_ :: QuasiQuoter py_ = QuasiQuoter { quoteExp = \txt -> [| runPy $ do p_globals <- basicMainDict p_locals <- basicNewDict - src <- $(expQQ Exec (unindent txt)) p_locals + src <- $(expQQ Exec txt) p_locals res <- pyEvalInMain p_globals p_locals src basicDecref p_locals return res @@ -57,7 +57,7 @@ py_ = QuasiQuoter pye :: QuasiQuoter pye = QuasiQuoter { quoteExp = \txt -> [| runPy $ do p_env <- basicNewDict - src <- $(expQQ Eval (unindent txt)) p_env + src <- $(expQQ Eval txt) p_env res <- pyEvalExpr p_env src basicDecref p_env return res diff --git a/src/Python/Internal/EvalQQ.hs b/src/Python/Internal/EvalQQ.hs index 5323694..dfdedfc 100644 --- a/src/Python/Internal/EvalQQ.hs +++ b/src/Python/Internal/EvalQQ.hs @@ -168,7 +168,9 @@ data Mode expQQ :: Mode -- ^ Python evaluation mode: @exec@/@eval@ -> String -- ^ Python source code -> TH.Q TH.Exp -expQQ mode src = do +expQQ mode qq_src = do + -- We need to preprocess before passing it to python. + let src = prepareSource mode qq_src antis <- liftIO $ do -- We've embedded script into library and we need to pass source -- code of QQ to a script. It can contain whatever symbols so to @@ -212,6 +214,11 @@ chop name = take (length name - length antiSuffix) name -- Python source code transform ---------------------------------------------------------------- +prepareSource :: Mode -> String -> String +prepareSource = \case + Eval -> dropWhile isSpace + Exec -> unindent + -- Python is indentation based and quasiquotes do not strip leading -- space. We have to do that ourself unindent :: String -> String From c537010c436fb874e038fd5132c136cc913548db Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Sat, 4 Jan 2025 23:44:16 +0300 Subject: [PATCH 04/17] Make preparation for adding pyf quasiquote It requires special handling --- src/Python/Internal/EvalQQ.hs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/Python/Internal/EvalQQ.hs b/src/Python/Internal/EvalQQ.hs index dfdedfc..311f59e 100644 --- a/src/Python/Internal/EvalQQ.hs +++ b/src/Python/Internal/EvalQQ.hs @@ -170,7 +170,8 @@ expQQ :: Mode -- ^ Python evaluation mode: @exec@/@eval@ -> TH.Q TH.Exp expQQ mode qq_src = do -- We need to preprocess before passing it to python. - let src = prepareSource mode qq_src + let src = prepareSource mode qq_src + src_var = prepareForVarLookup mode src antis <- liftIO $ do -- We've embedded script into library and we need to pass source -- code of QQ to a script. It can contain whatever symbols so to @@ -185,7 +186,7 @@ expQQ mode qq_src = do , "decode_and_print('" <> concat [ [ intToDigit $ fromIntegral (w `shiftR` 4) , intToDigit $ fromIntegral (w .&. 15) ] - | w <- BS.unpack $ T.encodeUtf8 $ T.pack src + | w <- BS.unpack $ T.encodeUtf8 $ T.pack src_var ] <> "')" ] @@ -195,10 +196,11 @@ expQQ mode qq_src = do let args = [ [| basicBindInDict $(TH.lift 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 src) + pure $(TH.lift src_eval) |] @@ -219,6 +221,17 @@ prepareSource = \case Eval -> dropWhile isSpace Exec -> unindent +prepareForVarLookup :: Mode -> String -> String +prepareForVarLookup = \case + Eval -> id + Exec -> id + +prepareForEval :: Mode -> [String] -> String -> String +prepareForEval mode _ = case mode of + Eval -> id + Exec -> id + + -- Python is indentation based and quasiquotes do not strip leading -- space. We have to do that ourself unindent :: String -> String From 41981e803784d01394db164d5eeeaffffb5905f5 Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Sat, 4 Jan 2025 23:50:18 +0300 Subject: [PATCH 05/17] We already have decref in Eval --- src/Python/Inline/QQ.hs | 4 ++-- src/Python/Internal/EvalQQ.hs | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Python/Inline/QQ.hs b/src/Python/Inline/QQ.hs index 5467af7..75ccc42 100644 --- a/src/Python/Inline/QQ.hs +++ b/src/Python/Inline/QQ.hs @@ -42,7 +42,7 @@ py_ = QuasiQuoter p_locals <- basicNewDict src <- $(expQQ Exec txt) p_locals res <- pyEvalInMain p_globals p_locals src - basicDecref p_locals + decref p_locals return res |] , quotePat = error "quotePat" @@ -59,7 +59,7 @@ pye = QuasiQuoter { quoteExp = \txt -> [| runPy $ do p_env <- basicNewDict src <- $(expQQ Eval txt) p_env res <- pyEvalExpr p_env src - basicDecref p_env + decref p_env return res |] , quotePat = error "quotePat" diff --git a/src/Python/Internal/EvalQQ.hs b/src/Python/Internal/EvalQQ.hs index 311f59e..92a2636 100644 --- a/src/Python/Internal/EvalQQ.hs +++ b/src/Python/Internal/EvalQQ.hs @@ -10,7 +10,6 @@ module Python.Internal.EvalQQ , basicNewDict , basicMainDict , basicBindInDict - , basicDecref -- * Python transformations , unindent ) where @@ -145,9 +144,6 @@ basicMainDict = Py [CU.block| PyObject* { return PyModule_GetDict(main_module); }|] -basicDecref :: Ptr PyObject -> Py () -basicDecref o = Py [CU.exp| void { Py_DECREF($(PyObject* o)) } |] - ---------------------------------------------------------------- -- TH generator From d5f04a9d864159cf8f158b2e2f231ffe06b81bda Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Sat, 4 Jan 2025 23:53:26 +0300 Subject: [PATCH 06/17] Rename function --- src/Python/Inline/QQ.hs | 4 ++-- src/Python/Internal/EvalQQ.hs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Python/Inline/QQ.hs b/src/Python/Inline/QQ.hs index 75ccc42..42360fd 100644 --- a/src/Python/Inline/QQ.hs +++ b/src/Python/Inline/QQ.hs @@ -24,7 +24,7 @@ pymain :: QuasiQuoter pymain = QuasiQuoter { quoteExp = \txt -> [| runPy $ do p_main <- basicMainDict src <- $(expQQ Exec txt) p_main - pyEvalInMain p_main p_main src + pyExec p_main p_main src |] , quotePat = error "quotePat" , quoteType = error "quoteType" @@ -41,7 +41,7 @@ py_ = QuasiQuoter { quoteExp = \txt -> [| runPy $ do p_globals <- basicMainDict p_locals <- basicNewDict src <- $(expQQ Exec txt) p_locals - res <- pyEvalInMain p_globals p_locals src + res <- pyExec p_globals p_locals src decref p_locals return res |] diff --git a/src/Python/Internal/EvalQQ.hs b/src/Python/Internal/EvalQQ.hs index 92a2636..ad169a9 100644 --- a/src/Python/Internal/EvalQQ.hs +++ b/src/Python/Internal/EvalQQ.hs @@ -3,7 +3,7 @@ -- | module Python.Internal.EvalQQ ( -- * Evaluators and QQ - pyEvalInMain + pyExec , pyEvalExpr , expQQ , Mode(..) @@ -51,12 +51,12 @@ C.include "" -- | Evaluate expression within context of @__main__@ module. All -- variables defined in this evaluator persist. -pyEvalInMain +pyExec :: Ptr PyObject -- ^ Globals -> Ptr PyObject -- ^ Locals -> String -> Py () -pyEvalInMain p_globals p_locals src = evalContT $ do +pyExec p_globals p_locals src = evalContT $ do p_py <- withPyCString src r <- liftIO [C.block| int { PyObject *code = Py_CompileString($(char* p_py), "", Py_file_input); From 3195e430f46bad783b82f2170bca6b2a74c43525 Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Sun, 5 Jan 2025 00:58:24 +0300 Subject: [PATCH 07/17] Add and use INCREF --- src/Python/Inline/Literal.hs | 7 ++----- src/Python/Internal/Eval.hs | 4 ++++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Python/Inline/Literal.hs b/src/Python/Inline/Literal.hs index e60ef75..dff3515 100644 --- a/src/Python/Inline/Literal.hs +++ b/src/Python/Inline/Literal.hs @@ -109,12 +109,9 @@ toPy a = runPy $ basicToPy a >>= \case ---------------------------------------------------------------- instance ToPy PyObject where - basicToPy o = unsafeWithPyObject o $ \p -> - p <$ Py [CU.exp| void { Py_INCREF($(PyObject* p)) } |] + basicToPy o = unsafeWithPyObject o $ \p -> p <$ incref p instance FromPy PyObject where - basicFromPy p = do - Py [CU.exp| void { Py_INCREF($(PyObject* p)) } |] - newPyObject p + basicFromPy p = incref p >> newPyObject p instance ToPy () where basicToPy () = Py [CU.exp| PyObject* { Py_None } |] diff --git a/src/Python/Internal/Eval.hs b/src/Python/Internal/Eval.hs index 65b82be..8f348a5 100644 --- a/src/Python/Internal/Eval.hs +++ b/src/Python/Internal/Eval.hs @@ -15,6 +15,7 @@ module Python.Internal.Eval -- * PyObject wrapper , newPyObject , decref + , incref , takeOwnership , ensureGIL , dropGIL @@ -241,6 +242,9 @@ doFinalizePython = [C.block| void { decref :: Ptr PyObject -> Py () decref p = Py [CU.exp| void { Py_DECREF($(PyObject* p)) } |] +incref :: Ptr PyObject -> Py () +incref p = Py [CU.exp| void { Py_INCREF($(PyObject* p)) } |] + -- | Ensure that we hold GIL for duration of action ensureGIL :: Py a -> Py a ensureGIL action = do From e14b3c1f30280853371af5e62b954ed638c3e8cb Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Sun, 5 Jan 2025 01:04:59 +0300 Subject: [PATCH 08/17] Add debugPrintPy. It's quite handy --- src/Python/Internal/Eval.hs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Python/Internal/Eval.hs b/src/Python/Internal/Eval.hs index 8f348a5..6113ce0 100644 --- a/src/Python/Internal/Eval.hs +++ b/src/Python/Internal/Eval.hs @@ -24,6 +24,8 @@ module Python.Internal.Eval , convertPy2Haskell , throwPyError , throwPyConvesionFailed + -- * Debugging + , debugPrintPy ) where import Control.Concurrent @@ -364,3 +366,14 @@ throwPyConvesionFailed = do case r of 0 -> pure () _ -> throwPy FromPyFailed + + +---------------------------------------------------------------- +-- Debugging +---------------------------------------------------------------- + +debugPrintPy :: Ptr PyObject -> Py () +debugPrintPy p = Py [CU.block| void { + PyObject_Print($(PyObject *p), stdout, 0); + printf(" [REF=%li]\n", Py_REFCNT($(PyObject *p)) ); + } |] From d5d4227ee954639db7574d1939f690bad8df8479 Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Sun, 5 Jan 2025 01:05:50 +0300 Subject: [PATCH 09/17] Add pyf quasiquoter --- src/Python/Inline/QQ.hs | 29 +++++++++++++++++++++++++++- src/Python/Internal/EvalQQ.hs | 36 +++++++++++++++++++++++++++++------ 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/src/Python/Inline/QQ.hs b/src/Python/Inline/QQ.hs index 42360fd..e4044e5 100644 --- a/src/Python/Inline/QQ.hs +++ b/src/Python/Inline/QQ.hs @@ -7,13 +7,14 @@ module Python.Inline.QQ ( pymain , py_ , pye + , pyf ) where import Language.Haskell.TH.Quote import Python.Internal.EvalQQ import Python.Internal.Eval - +import Python.Internal.Types -- | Evaluate python code in context of main module. All variables -- defined in this block will remain visible. This quasiquote @@ -66,3 +67,29 @@ pye = QuasiQuoter , quoteType = error "quoteType" , quoteDec = error "quoteDec" } + + + +-- This quote creates object of type @IO PyObject@ +pyf :: QuasiQuoter +pyf = QuasiQuoter + { quoteExp = \txt -> + [| runPy $ do p_globals <- basicMainDict + p_locals <- basicNewDict + p_kwargs <- basicNewDict + src <- $(expQQ Fun txt) p_kwargs + pyExec p_globals p_locals src + -- Now we need to look up _inline_python_ in p_env + p_fun <- getFunctionObject p_locals >>= \case + NULL -> error "INTERNAL ERROR: _inline_python_ must be present" + p -> pure p + -- Call python function object + r <- callFunctionObject p_fun p_kwargs + case r of + NULL -> throwPy =<< convertPy2Haskell + _ -> newPyObject r + |] + , quotePat = error "quotePat" + , quoteType = error "quoteType" + , quoteDec = error "quoteDec" + } diff --git a/src/Python/Internal/EvalQQ.hs b/src/Python/Internal/EvalQQ.hs index ad169a9..79e9912 100644 --- a/src/Python/Internal/EvalQQ.hs +++ b/src/Python/Internal/EvalQQ.hs @@ -10,8 +10,8 @@ module Python.Internal.EvalQQ , basicNewDict , basicMainDict , basicBindInDict - -- * Python transformations - , unindent + , getFunctionObject + , callFunctionObject ) where import Control.Monad.IO.Class @@ -19,6 +19,7 @@ import Control.Monad.Trans.Class import Control.Monad.Trans.Cont import Data.Bits import Data.Char +import Data.List (intercalate) import Data.ByteString qualified as BS import Data.Text qualified as T import Data.Text.Encoding qualified as T @@ -144,6 +145,17 @@ basicMainDict = Py [CU.block| PyObject* { 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_") } |] + +callFunctionObject :: Ptr PyObject -> Ptr PyObject -> Py (Ptr PyObject) +callFunctionObject fun kwargs = Py [CU.block| PyObject* { + PyObject* args = PyTuple_Pack(0); + return PyObject_Call($(PyObject *fun), args, $(PyObject *kwargs)); + } |] + + ---------------------------------------------------------------- -- TH generator @@ -158,6 +170,7 @@ script = $( do let path = "py/bound-vars.py" data Mode = Eval | Exec + | Fun -- | Generate TH splice which updates python environment dictionary -- and returns python source code. @@ -177,6 +190,7 @@ expQQ mode qq_src = do [ "-" , case mode of Eval -> "eval" Exec -> "exec" + Fun -> "exec" ] $ unlines [ script , "decode_and_print('" <> @@ -216,17 +230,22 @@ prepareSource :: Mode -> String -> String prepareSource = \case Eval -> dropWhile isSpace Exec -> unindent + Fun -> unindent prepareForVarLookup :: Mode -> String -> String prepareForVarLookup = \case Eval -> id Exec -> id + Fun -> ("def __dummy__():\n"++) . indent prepareForEval :: Mode -> [String] -> String -> String -prepareForEval mode _ = case mode of - Eval -> id - Exec -> id - +prepareForEval mode vars src = case mode of + Eval -> src + Exec -> src + Fun -> "def _inline_python_("<>args<>"):\n" + <> indent src + where + args = intercalate "," vars -- Python is indentation based and quasiquotes do not strip leading -- space. We have to do that ourself @@ -244,3 +263,8 @@ unindent py_src = case lines py_src of let non_empty = filter (any (not . isSpace)) ls n = minimum [ length (takeWhile (==' ') s) | s <- non_empty ] in unlines $ drop n <$> ls + +indent :: String -> String +indent = unlines + . map (" "++) + . lines From a2fa7923c7f6709e35912a2024e470c537017f72 Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Sun, 5 Jan 2025 01:21:18 +0300 Subject: [PATCH 10/17] Plug leaks in quasiquoters --- src/Python/Inline/QQ.hs | 77 +++++++++++++++++++---------------- src/Python/Internal/EvalQQ.hs | 8 ++-- 2 files changed, 45 insertions(+), 40 deletions(-) diff --git a/src/Python/Inline/QQ.hs b/src/Python/Inline/QQ.hs index e4044e5..263c553 100644 --- a/src/Python/Inline/QQ.hs +++ b/src/Python/Inline/QQ.hs @@ -10,6 +10,8 @@ module Python.Inline.QQ , pyf ) where +import Control.Monad.Trans.Cont +import Control.Monad.Trans.Class import Language.Haskell.TH.Quote import Python.Internal.EvalQQ @@ -23,10 +25,11 @@ import Python.Internal.Types -- This quote creates object of type @IO ()@ pymain :: QuasiQuoter pymain = QuasiQuoter - { quoteExp = \txt -> [| runPy $ do p_main <- basicMainDict - src <- $(expQQ Exec txt) p_main - pyExec p_main p_main src - |] + { quoteExp = \txt -> [| runPy $ do + p_main <- basicMainDict + src <- $(expQQ Exec txt) p_main + pyExecExpr p_main p_main src + |] , quotePat = error "quotePat" , quoteType = error "quoteType" , quoteDec = error "quoteDec" @@ -39,13 +42,13 @@ pymain = QuasiQuoter -- This quote creates object of type @IO ()@ py_ :: QuasiQuoter py_ = QuasiQuoter - { quoteExp = \txt -> [| runPy $ do p_globals <- basicMainDict - p_locals <- basicNewDict - src <- $(expQQ Exec txt) p_locals - res <- pyExec p_globals p_locals src - decref p_locals - return res - |] + { quoteExp = \txt -> [| runPy $ evalContT $ do + p_globals <- lift basicMainDict + p_locals <- takeOwnership =<< lift basicNewDict + lift $ do + src <- $(expQQ Exec txt) p_locals + pyExecExpr p_globals p_locals src + |] , quotePat = error "quotePat" , quoteType = error "quoteType" , quoteDec = error "quoteDec" @@ -57,38 +60,40 @@ py_ = QuasiQuoter -- This quote creates object of type @IO PyObject@ pye :: QuasiQuoter pye = QuasiQuoter - { quoteExp = \txt -> [| runPy $ do p_env <- basicNewDict - src <- $(expQQ Eval txt) p_env - res <- pyEvalExpr p_env src - decref p_env - return res - |] + { quoteExp = \txt -> [| runPy $ evalContT $ do + p_env <- takeOwnership =<< lift basicNewDict + lift $ do + src <- $(expQQ Eval txt) p_env + pyEvalExpr p_env src + |] , quotePat = error "quotePat" , quoteType = error "quoteType" , quoteDec = error "quoteDec" } - - --- This quote creates object of type @IO PyObject@ +-- | Another quasiquoter which works around that sequence of python +-- statements doesn't have any value associated with it. Content of +-- quasiquote is function body. So to get value out of it one must +-- call return pyf :: QuasiQuoter pyf = QuasiQuoter - { quoteExp = \txt -> - [| runPy $ do p_globals <- basicMainDict - p_locals <- basicNewDict - p_kwargs <- basicNewDict - src <- $(expQQ Fun txt) p_kwargs - pyExec p_globals p_locals src - -- Now we need to look up _inline_python_ in p_env - p_fun <- getFunctionObject p_locals >>= \case - NULL -> error "INTERNAL ERROR: _inline_python_ must be present" - p -> pure p - -- Call python function object - r <- callFunctionObject p_fun p_kwargs - case r of - NULL -> throwPy =<< convertPy2Haskell - _ -> newPyObject r - |] + { quoteExp = \txt -> [| runPy $ evalContT $ do + p_globals <- lift basicMainDict + p_locals <- takeOwnership =<< lift basicNewDict + p_kwargs <- takeOwnership =<< lift basicNewDict + lift $ do + -- Create function in p_locals + src <- $(expQQ Fun txt) p_kwargs + pyExecExpr p_globals p_locals src + -- Look up function + p_fun <- getFunctionObject p_locals >>= \case + NULL -> error "INTERNAL ERROR: _inline_python_ must be present" + p -> pure p + -- Call python function we just constructed + callFunctionObject p_fun p_kwargs >>= \case + NULL -> throwPy =<< convertPy2Haskell + p_res -> newPyObject p_res + |] , quotePat = error "quotePat" , quoteType = error "quoteType" , quoteDec = error "quoteDec" diff --git a/src/Python/Internal/EvalQQ.hs b/src/Python/Internal/EvalQQ.hs index 79e9912..352f2a4 100644 --- a/src/Python/Internal/EvalQQ.hs +++ b/src/Python/Internal/EvalQQ.hs @@ -3,7 +3,7 @@ -- | module Python.Internal.EvalQQ ( -- * Evaluators and QQ - pyExec + pyExecExpr , pyEvalExpr , expQQ , Mode(..) @@ -52,12 +52,12 @@ C.include "" -- | Evaluate expression within context of @__main__@ module. All -- variables defined in this evaluator persist. -pyExec +pyExecExpr :: Ptr PyObject -- ^ Globals -> Ptr PyObject -- ^ Locals - -> String + -> String -- ^ Python source code -> Py () -pyExec p_globals p_locals src = evalContT $ do +pyExecExpr p_globals p_locals src = evalContT $ do p_py <- withPyCString src r <- liftIO [C.block| int { PyObject *code = Py_CompileString($(char* p_py), "", Py_file_input); From 6c74cb0eca275b18ac77b23566d2ff42cd9da93a Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Sun, 5 Jan 2025 01:29:22 +0300 Subject: [PATCH 11/17] We were leaking code objects during evaluation --- src/Python/Inline/QQ.hs | 7 ++++--- src/Python/Internal/EvalQQ.hs | 38 +++++++++++++++-------------------- 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/src/Python/Inline/QQ.hs b/src/Python/Inline/QQ.hs index 263c553..9fe5be9 100644 --- a/src/Python/Inline/QQ.hs +++ b/src/Python/Inline/QQ.hs @@ -61,10 +61,11 @@ py_ = QuasiQuoter pye :: QuasiQuoter pye = QuasiQuoter { quoteExp = \txt -> [| runPy $ evalContT $ do - p_env <- takeOwnership =<< lift basicNewDict + p_globals <- lift basicMainDict + p_locals <- takeOwnership =<< lift basicNewDict lift $ do - src <- $(expQQ Eval txt) p_env - pyEvalExpr p_env src + src <- $(expQQ Eval txt) p_locals + pyEvalExpr p_globals p_locals src |] , quotePat = error "quotePat" , quoteType = error "quoteType" diff --git a/src/Python/Internal/EvalQQ.hs b/src/Python/Internal/EvalQQ.hs index 352f2a4..f7a2ef8 100644 --- a/src/Python/Internal/EvalQQ.hs +++ b/src/Python/Internal/EvalQQ.hs @@ -60,47 +60,41 @@ pyExecExpr pyExecExpr p_globals p_locals src = evalContT $ do p_py <- withPyCString src r <- liftIO [C.block| int { + 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 IPY_ERR_COMPILE; } - // Execute in context of main - PyObject* globals = $(PyObject* p_globals); - PyObject* locals = $(PyObject* p_locals); - PyObject* r = PyEval_EvalCode(code, globals, locals); - Py_XDECREF(r); - if( PyErr_Occurred() ) { - return IPY_ERR_PYTHON; - } - return IPY_OK; + // Execute statements + PyObject* res = PyEval_EvalCode(code, globals, locals); + Py_XDECREF(res); + Py_DECREF(code); + return PyErr_Occurred() ? IPY_ERR_PYTHON : IPY_OK; } |] lift $ finiEval r (pure ()) -- | Evaluate expression with fresh local environment pyEvalExpr - :: Ptr PyObject -- ^ Dictionary with local + :: Ptr PyObject -- ^ Globals + -> Ptr PyObject -- ^ Locals -> String -- ^ Python source code -> Py PyObject -pyEvalExpr p_env src = evalContT $ do +pyEvalExpr p_globals p_locals src = evalContT $ do p_py <- withPyCString src p_res <- withPyAlloca @(Ptr PyObject) r <- liftIO [C.block| int { + 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 IPY_ERR_COMPILE; } - // Execute in context of main - PyObject* main_module = PyImport_AddModule("__main__"); - if( PyErr_Occurred() ) { - return IPY_ERR_PYTHON; - } - PyObject* globals = PyModule_GetDict(main_module); - if( PyErr_Occurred() ) { - return IPY_ERR_PYTHON; - } - // - PyObject* r = PyEval_EvalCode(code, globals, $(PyObject* p_env)); + // Evaluate expression + PyObject* r = PyEval_EvalCode(code, globals, locals); + Py_DECREF(code); if( PyErr_Occurred() ) { return IPY_ERR_PYTHON; } From 760b0e1325e528ee824ef858ff281a42677bd6bc Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Sun, 5 Jan 2025 01:37:29 +0300 Subject: [PATCH 12/17] Check that we actually can decrement counter And do this with GIL taken --- src/Python/Internal/Eval.hs | 24 ++++++++++++------------ src/Python/Internal/EvalQQ.hs | 1 - 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/Python/Internal/Eval.hs b/src/Python/Internal/Eval.hs index 6113ce0..a8e02c6 100644 --- a/src/Python/Internal/Eval.hs +++ b/src/Python/Internal/Eval.hs @@ -94,6 +94,9 @@ C.include "" -- 2. Overhead of `runInBoundThread` is significant for GC (~1μs) -- will this cause problem or if there're only few object on -- haskell heap it would be fine? +-- +-- In addition we must not do anything after interpreter shutdown. +-- It already released memory. Most of it at least. @@ -272,19 +275,16 @@ takeOwnership p = ContT $ \c -> c p `finallyPy` decref p -- | Wrap raw python object into newPyObject :: Ptr PyObject -> Py PyObject --- We need to use different implementation for different RTS -- See NOTE: [GC] -newPyObject p - | rtsSupportsBoundThreads = Py $ do - fptr <- newForeignPtr_ p - GHC.addForeignPtrFinalizer fptr $ runInBoundThread $ unPy $ decref p - pure $ PyObject fptr - | otherwise = Py $ do - fptr <- newForeignPtr_ p - PyObject fptr <$ addForeignPtrFinalizer py_XDECREF fptr - -py_XDECREF :: FunPtr (Ptr PyObject -> IO ()) -py_XDECREF = [C.funPtr| void inline_py_XDECREF(PyObject* p) { Py_XDECREF(p); } |] +newPyObject p = Py $ do + fptr <- newForeignPtr_ p + -- FIXME: We still have race between check and interpreter + -- shutdown. At least it's narrow race + GHC.addForeignPtrFinalizer fptr $ do + [CU.exp| int { Py_IsInitialized() } |] >>= \case + 0 -> pure () + _ -> runPy $ decref p + pure $ PyObject fptr diff --git a/src/Python/Internal/EvalQQ.hs b/src/Python/Internal/EvalQQ.hs index f7a2ef8..e0d57df 100644 --- a/src/Python/Internal/EvalQQ.hs +++ b/src/Python/Internal/EvalQQ.hs @@ -98,7 +98,6 @@ pyEvalExpr p_globals p_locals src = evalContT $ do if( PyErr_Occurred() ) { return IPY_ERR_PYTHON; } - Py_INCREF(r); *$(PyObject **p_res) = r; return IPY_OK; }|] From 223ec2a8155e6999f2bc6fe259497e2441fbdfed Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Sun, 5 Jan 2025 01:43:05 +0300 Subject: [PATCH 13/17] Simplify evaluators --- include/inline-python.h | 9 +--- src/Python/Internal/EvalQQ.hs | 82 +++++++++++++++-------------------- 2 files changed, 36 insertions(+), 55 deletions(-) diff --git a/include/inline-python.h b/include/inline-python.h index 28fa2aa..1f5efcc 100644 --- a/include/inline-python.h +++ b/include/inline-python.h @@ -5,18 +5,11 @@ #include -// Use new stable API from +// Use new stable API from 3.13 #ifndef PyCFunctionFast typedef _PyCFunctionFast PyCFunctionFast; #endif -// ---------------------------------------------------------------- -// Standard status codes - -#define IPY_OK 0 -#define IPY_ERR_PYTHON 1 -#define IPY_ERR_COMPILE 2 - // ================================================================ diff --git a/src/Python/Internal/EvalQQ.hs b/src/Python/Internal/EvalQQ.hs index e0d57df..cbf248f 100644 --- a/src/Python/Internal/EvalQQ.hs +++ b/src/Python/Internal/EvalQQ.hs @@ -25,7 +25,6 @@ import Data.Text qualified as T import Data.Text.Encoding qualified as T import Foreign.C.Types import Foreign.Ptr -import Foreign.Storable import System.Exit import System.Process (readProcessWithExitCode) @@ -58,22 +57,22 @@ pyExecExpr -> String -- ^ Python source code -> Py () pyExecExpr p_globals p_locals src = evalContT $ do - p_py <- withPyCString src - r <- liftIO [C.block| int { - 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 IPY_ERR_COMPILE; - } - // Execute statements - PyObject* res = PyEval_EvalCode(code, globals, locals); - Py_XDECREF(res); - Py_DECREF(code); - return PyErr_Occurred() ? IPY_ERR_PYTHON : IPY_OK; - } |] - lift $ finiEval r (pure ()) + p_py <- withPyCString src + lift $ do + 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); + } |] + throwPyError -- | Evaluate expression with fresh local environment pyEvalExpr @@ -83,36 +82,25 @@ pyEvalExpr -> Py PyObject pyEvalExpr p_globals p_locals src = evalContT $ do p_py <- withPyCString src - p_res <- withPyAlloca @(Ptr PyObject) - r <- liftIO [C.block| int { - 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 IPY_ERR_COMPILE; - } - // Evaluate expression - PyObject* r = PyEval_EvalCode(code, globals, locals); - Py_DECREF(code); - if( PyErr_Occurred() ) { - return IPY_ERR_PYTHON; - } - *$(PyObject **p_res) = r; - return IPY_OK; - }|] - lift $ finiEval r (newPyObject =<< liftIO (peek p_res)) - --- | Convert evaluation result and -finiEval - :: CInt - -> Py a - -> Py a -finiEval r fini = case r of - IPY_OK -> fini - IPY_ERR_COMPILE -> throwPy =<< convertPy2Haskell - IPY_ERR_PYTHON -> throwPy =<< convertPy2Haskell - _ -> error $ "pyEvalStr: unexpected error: " ++ show r + lift $ do + 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; + }|] + throwPyError + newPyObject p_res + + + basicBindInDict :: ToPy a => String -> a -> Ptr PyObject -> Py () basicBindInDict name a p_dict = evalContT $ do From 77f137611dfbb1ab35e7eb3fbb9367602aa17a9e Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Sun, 5 Jan 2025 01:51:50 +0300 Subject: [PATCH 14/17] Move evaluators to EvalQQ modules It's better to have less code in splice. It least it would be possible to read if need arises --- src/Python/Inline/QQ.hs | 44 ++++------------------------------- src/Python/Internal/EvalQQ.hs | 44 +++++++++++++++++++++++++++++++---- 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/src/Python/Inline/QQ.hs b/src/Python/Inline/QQ.hs index 9fe5be9..d2d3f4b 100644 --- a/src/Python/Inline/QQ.hs +++ b/src/Python/Inline/QQ.hs @@ -10,13 +10,11 @@ module Python.Inline.QQ , pyf ) where -import Control.Monad.Trans.Cont -import Control.Monad.Trans.Class import Language.Haskell.TH.Quote import Python.Internal.EvalQQ import Python.Internal.Eval -import Python.Internal.Types + -- | Evaluate python code in context of main module. All variables -- defined in this block will remain visible. This quasiquote @@ -25,11 +23,7 @@ import Python.Internal.Types -- This quote creates object of type @IO ()@ pymain :: QuasiQuoter pymain = QuasiQuoter - { quoteExp = \txt -> [| runPy $ do - p_main <- basicMainDict - src <- $(expQQ Exec txt) p_main - pyExecExpr p_main p_main src - |] + { quoteExp = \txt -> [| runPy $ evaluatorPymain $(expQQ Exec txt) |] , quotePat = error "quotePat" , quoteType = error "quoteType" , quoteDec = error "quoteDec" @@ -42,13 +36,7 @@ pymain = QuasiQuoter -- This quote creates object of type @IO ()@ py_ :: QuasiQuoter py_ = QuasiQuoter - { quoteExp = \txt -> [| runPy $ evalContT $ do - p_globals <- lift basicMainDict - p_locals <- takeOwnership =<< lift basicNewDict - lift $ do - src <- $(expQQ Exec txt) p_locals - pyExecExpr p_globals p_locals src - |] + { quoteExp = \txt -> [| runPy $ evaluatorPy_ $(expQQ Exec txt) |] , quotePat = error "quotePat" , quoteType = error "quoteType" , quoteDec = error "quoteDec" @@ -60,13 +48,7 @@ py_ = QuasiQuoter -- This quote creates object of type @IO PyObject@ pye :: QuasiQuoter pye = QuasiQuoter - { quoteExp = \txt -> [| runPy $ evalContT $ do - p_globals <- lift basicMainDict - p_locals <- takeOwnership =<< lift basicNewDict - lift $ do - src <- $(expQQ Eval txt) p_locals - pyEvalExpr p_globals p_locals src - |] + { quoteExp = \txt -> [| runPy $ evaluatorPye $(expQQ Eval txt) |] , quotePat = error "quotePat" , quoteType = error "quoteType" , quoteDec = error "quoteDec" @@ -78,23 +60,7 @@ pye = QuasiQuoter -- call return pyf :: QuasiQuoter pyf = QuasiQuoter - { quoteExp = \txt -> [| runPy $ evalContT $ do - p_globals <- lift basicMainDict - p_locals <- takeOwnership =<< lift basicNewDict - p_kwargs <- takeOwnership =<< lift basicNewDict - lift $ do - -- Create function in p_locals - src <- $(expQQ Fun txt) p_kwargs - pyExecExpr p_globals p_locals src - -- Look up function - p_fun <- getFunctionObject p_locals >>= \case - NULL -> error "INTERNAL ERROR: _inline_python_ must be present" - p -> pure p - -- Call python function we just constructed - callFunctionObject p_fun p_kwargs >>= \case - NULL -> throwPy =<< convertPy2Haskell - p_res -> newPyObject p_res - |] + { quoteExp = \txt -> [| runPy $ evalutorPyf $(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 cbf248f..f8511db 100644 --- a/src/Python/Internal/EvalQQ.hs +++ b/src/Python/Internal/EvalQQ.hs @@ -5,13 +5,13 @@ module Python.Internal.EvalQQ ( -- * Evaluators and QQ pyExecExpr , pyEvalExpr + , evaluatorPymain + , evaluatorPy_ + , evaluatorPye + , evaluatorPyf + -- * Code generation , expQQ , Mode(..) - , basicNewDict - , basicMainDict - , basicBindInDict - , getFunctionObject - , callFunctionObject ) where import Control.Monad.IO.Class @@ -100,6 +100,40 @@ pyEvalExpr p_globals p_locals src = evalContT $ do newPyObject p_res +evaluatorPymain :: (Ptr PyObject -> Py String) -> Py () +evaluatorPymain getSource = do + p_main <- basicMainDict + src <- getSource p_main + pyExecExpr p_main p_main src + +evaluatorPy_ :: (Ptr PyObject -> Py String) -> Py () +evaluatorPy_ getSource = evalContT $ do + p_globals <- lift basicMainDict + p_locals <- takeOwnership =<< lift basicNewDict + lift $ pyExecExpr p_globals p_locals =<< getSource p_locals + +evaluatorPye :: (Ptr PyObject -> Py String) -> Py PyObject +evaluatorPye getSource = evalContT $ do + p_globals <- lift basicMainDict + p_locals <- takeOwnership =<< lift basicNewDict + lift $ pyEvalExpr p_globals p_locals =<< getSource p_locals + +evaluatorPyf :: (Ptr PyObject -> Py String) -> Py PyObject +evaluatorPyf getSource = evalContT $ do + p_globals <- lift basicMainDict + p_locals <- takeOwnership =<< lift basicNewDict + p_kwargs <- takeOwnership =<< lift basicNewDict + lift $ do + -- Create function in p_locals + pyExecExpr p_globals p_locals =<< getSource p_kwargs + -- Look up function + p_fun <- getFunctionObject p_locals >>= \case + NULL -> error "INTERNAL ERROR: _inline_python_ must be present" + p -> pure p + -- Call python function we just constructed + callFunctionObject p_fun p_kwargs >>= \case + NULL -> throwPy =<< convertPy2Haskell + p_res -> newPyObject p_res basicBindInDict :: ToPy a => String -> a -> Ptr PyObject -> Py () From 7d466bf08ee655ba7babd6f176e4b0be6f9db6a7 Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Sun, 5 Jan 2025 02:32:25 +0300 Subject: [PATCH 15/17] Add error handling in basicBindInDict --- src/Python/Internal/EvalQQ.hs | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/Python/Internal/EvalQQ.hs b/src/Python/Internal/EvalQQ.hs index f8511db..a081a91 100644 --- a/src/Python/Internal/EvalQQ.hs +++ b/src/Python/Internal/EvalQQ.hs @@ -138,17 +138,18 @@ evaluatorPyf getSource = evalContT $ do basicBindInDict :: ToPy a => String -> a -> Ptr PyObject -> Py () basicBindInDict name a p_dict = evalContT $ do - -- FIXME: error handling - -- FIXME: meanining of errors in PyUnicode_DecodeUTF8? - (p_key,len) <- withPyCStringLen name - p_obj <- lift $ basicToPy a - let c_len = fromIntegral len :: CLong - liftIO [C.block| void { - PyObject* p_obj = $(PyObject* p_obj); - PyObject* key = PyUnicode_DecodeUTF8( $(char* p_key), $(long c_len), 0); - PyDict_SetItem($(PyObject* p_dict), key, p_obj); - Py_DECREF(p_obj); - } |] + (p_key) <- withPyCString name + p_obj <- takeOwnership =<< lift (basicToPy a) + lift $ case p_obj of + NULL -> throwPyError + _ -> 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 () + _ -> throwPyError basicNewDict :: Py (Ptr PyObject) basicNewDict = Py [CU.exp| PyObject* { PyDict_New() } |] @@ -157,6 +158,8 @@ basicNewDict = Py [CU.exp| PyObject* { PyDict_New() } |] 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); }|] From 83691f5b9dc830c7da66ad703273918690e50097 Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Sun, 5 Jan 2025 02:36:01 +0300 Subject: [PATCH 16/17] Add mustThrowPyError --- src/Python/Internal/Eval.hs | 9 +++++++++ src/Python/Internal/EvalQQ.hs | 6 +++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Python/Internal/Eval.hs b/src/Python/Internal/Eval.hs index a8e02c6..65e74f5 100644 --- a/src/Python/Internal/Eval.hs +++ b/src/Python/Internal/Eval.hs @@ -23,6 +23,7 @@ module Python.Internal.Eval , convertHaskell2Py , convertPy2Haskell , throwPyError + , mustThrowPyError , throwPyConvesionFailed -- * Debugging , debugPrintPy @@ -354,6 +355,14 @@ throwPyError = NULL -> pure () _ -> throwPy =<< convertPy2Haskell +-- | Throw python error as haskell exception if it's raised. If it's +-- not that internal error. Another exception will be raised +mustThrowPyError :: String -> Py a +mustThrowPyError msg = + Py [CU.exp| PyObject* { PyErr_Occurred() } |] >>= \case + NULL -> error $ "mustThrowPyError: no python exception raised. " ++ msg + _ -> throwPy =<< convertPy2Haskell + throwPyConvesionFailed :: Py () throwPyConvesionFailed = do r <- Py [CU.block| int { diff --git a/src/Python/Internal/EvalQQ.hs b/src/Python/Internal/EvalQQ.hs index a081a91..a26f803 100644 --- a/src/Python/Internal/EvalQQ.hs +++ b/src/Python/Internal/EvalQQ.hs @@ -132,7 +132,7 @@ evaluatorPyf getSource = evalContT $ do p -> pure p -- Call python function we just constructed callFunctionObject p_fun p_kwargs >>= \case - NULL -> throwPy =<< convertPy2Haskell + NULL -> mustThrowPyError "evaluatorPyf" p_res -> newPyObject p_res @@ -141,7 +141,7 @@ basicBindInDict name a p_dict = evalContT $ do (p_key) <- withPyCString name p_obj <- takeOwnership =<< lift (basicToPy a) lift $ case p_obj of - NULL -> throwPyError + NULL -> mustThrowPyError "basicBindInDict" _ -> do r <- Py [C.block| int { PyObject* p_obj = $(PyObject* p_obj); @@ -149,7 +149,7 @@ basicBindInDict name a p_dict = evalContT $ do } |] case r of 0 -> pure () - _ -> throwPyError + _ -> mustThrowPyError "basicBindInDict" basicNewDict :: Py (Ptr PyObject) basicNewDict = Py [CU.exp| PyObject* { PyDict_New() } |] From 4096ed79788968dffd70e4c5db6fcb1e7cb21cfe Mon Sep 17 00:00:00 2001 From: Alexey Khudyakov Date: Sun, 5 Jan 2025 02:37:13 +0300 Subject: [PATCH 17/17] Add test for pyf --- src/Python/Inline/QQ.hs | 2 +- test/TST/Run.hs | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/Python/Inline/QQ.hs b/src/Python/Inline/QQ.hs index d2d3f4b..dc49815 100644 --- a/src/Python/Inline/QQ.hs +++ b/src/Python/Inline/QQ.hs @@ -60,7 +60,7 @@ pye = QuasiQuoter -- call return pyf :: QuasiQuoter pyf = QuasiQuoter - { quoteExp = \txt -> [| runPy $ evalutorPyf $(expQQ Fun txt) |] + { quoteExp = \txt -> [| runPy $ evaluatorPyf $(expQQ Fun txt) |] , quotePat = error "quotePat" , quoteType = error "quoteType" , quoteDec = error "quoteDec" diff --git a/test/TST/Run.hs b/test/TST/Run.hs index b66fc52..419fc32 100644 --- a/test/TST/Run.hs +++ b/test/TST/Run.hs @@ -81,4 +81,26 @@ tests = testGroup "Run python" except NameError: pass |] + , testCase "Scope pyf->any" $ do + _ <- [pyf| + x = 12 + x + return 12 + |] + -- Not visible + throwsPy $ void [pye| x |] + [py_| + try: + x + assert False, "x shouln't be visible (1)" + except NameError: + pass + |] + [pymain| + try: + x + assert False, "x shouln't be visible (2)" + except NameError: + pass + |] ]