如何使用GHC API动态编译并导入一个模块 – LTS Haskell 20.20(ghc-9.2.7)?

huangapple go评论117阅读模式
英文:

How to dynamically compile and import a module using GHC API - LTS Haskell 20.20 (ghc-9.2.7)?

问题

背景

我想在Haskell中实现一个程序,该程序可以动态生成Haskell模块(hs-文件),然后编译并将其导入到生成代码的同一应用程序中。

因此,我尝试理解Stack Overflow和文档中的一些示例代码片段。但它既不能工作也不能编译。

在对给定示例的某些修改之后,我创建了以下源代码Main.hs:

  1. import qualified GhcApiWrap as Ghw
  2. main :: IO ()
  3. main = Ghw.msLoadModuleAndExecute "../dyn/" "DynExample.hs" "nFromChar" 'A'

GhcApiWrap.hs:

  1. module GhcApiWrap
  2. (
  3. msLoadModuleAndExecute
  4. ) where
  5. import GHC
  6. msLoadModuleAndExecute :: String -> String -> String -> Char -> IO ()
  7. msLoadModuleAndExecute _ _ _ _ = do
  8. value' <- runGhc (Just "./src/") $ do
  9. dflags <- getSessionDynFlags
  10. setSessionDynFlags $ dflags {
  11. ghcLink = LinkInMemory,
  12. ghcMode = CompManager,
  13. objectDir = Just "../dyn/",
  14. hiDir = Just "../dyn/"
  15. }
  16. target <- guessTarget "DynExample.hs" Nothing
  17. setTargets [target]
  18. ret <- load LoadAllTargets
  19. case ret of
  20. Succeeded -> do
  21. importDecl_RdrName <- parseImportDecl "import DynExample"
  22. setContext [IIDecl importDecl_RdrName]
  23. value <- dynCompileExpr "DynExample.nFromChar"
  24. return value
  25. _ ->
  26. return undefined
  27. print $ value'

DynExample.hs:

  1. module DynExample
  2. (
  3. nFromChar
  4. ) where
  5. nFromChar :: Char -> Int
  6. nFromChar _ = 33

... 这个程序能够编译但无法工作。

它的输出是:

  1. ExprmntGhcApi-exe: Missing file: src/settings

如您所见,我注释掉了 import GHC.Paths (libdir),因为它不再存在。因此,我谨慎地猜测,我使用了 (Just "./src/") 代替了 (Just libdir)

我尝试了几个版本,但没有一个能与GHC 9.2.7一起工作。

问题

是否可以使用最新的Haskell Stack环境以及以下函数类型,以动态方式使用 DynExample.nFromChar 这种方式?

  1. msLoadModuleAndExecute :: String -> String -> String -> Char -> ... Int

环境

目前,我正在使用带有以下配置的Stack:

  1. resolver:
  2. url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/20/20.yaml

... 这对应于使用GHC 9.2.7。

英文:

Background

I would like to implement a program in Haskell that can generate Haskell module (hs-File) dynamically and then compile, and import it into the same application that generated the code.

Therefore, I tried to understand some example code snippets in SO and documentation. But it doesn't work nor compiles.

After some modifications of one of the given examples, I created the following souce code, Main.hs:

  1. import qualified GhcApiWrap as Ghw
  2. main :: IO ()
  3. main = Ghw.msLoadModuleAndExecute &quot;../dyn/&quot; &quot;DynExample.hs&quot; &quot;nFromChar&quot; &#39;A&#39;

GhcApiWrap.hs:

  1. module GhcApiWrap
  2. (
  3. msLoadModuleAndExecute
  4. ) where
  5. import GHC
  6. --import GHC.Paths (libdir)
  7. msLoadModuleAndExecute :: String -&gt; String -&gt; String -&gt; Char -&gt; IO ()
  8. msLoadModuleAndExecute _ _ _ _ = do
  9. value&#39; &lt;- runGhc (Just &quot;./src/&quot;) $ do
  10. dflags &lt;- getSessionDynFlags
  11. setSessionDynFlags $ dflags {
  12. ghcLink = LinkInMemory,
  13. ghcMode = CompManager,
  14. objectDir = Just &quot;../dyn/&quot;,
  15. hiDir = Just &quot;../dyn/&quot;
  16. }
  17. target &lt;- guessTarget (&quot;DynExample.hs&quot;) Nothing
  18. setTargets [target]
  19. ret &lt;- load LoadAllTargets
  20. case ret of
  21. Succeeded -&gt; do
  22. importDecl_RdrName &lt;- parseImportDecl $ &quot;import DynExample&quot;
  23. setContext [IIDecl importDecl_RdrName]
  24. value &lt;- dynCompileExpr (&quot;DynExample.nFromChar&quot;)
  25. return value
  26. _ -&gt;
  27. return undefined
  28. print $ value&#39;

DynExample.hs:

  1. module DynExample
  2. (
  3. nFromChar
  4. ) where
  5. nFromChar :: Char -&gt; Int
  6. nFromChar _ = 33

...which compiles but doesn't work.

Its ouput is:

  1. ExprmntGhcApi-exe: Missing file: src/settings

As you can see, I commented out import GHC.Paths (libdir) because it doesn't exist anymore. Therefore, as a humble guess I used (Just &quot;./src/&quot;) instead of (Just libdir).

I tried several versions that are "flying around", but none of them work with GHC 9.2.7.

Question

Is it, and how is it possible to use DynExample.nFromChar dynamically this way using an up-to-date Haskell Stack environment, and having the following type of the function?

  1. msLoadModuleAndExecute :: String -&gt; String -&gt; String -&gt; Char -&gt; ... Int

Environment

At the moment, I am using Stack with:

  1. resolver:
  2. url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/20/20.yaml

...which translates to usage of GHC 9.2.7.

答案1

得分: 2

所以,通过添加对ghc-paths包的依赖,解决了import GHC.Paths (libdir)的问题。

接下来的问题是正确地为guessTarget提供参数。从文档中可以看到:

尝试猜测字符串引用的目标是什么。这个函数实现了--make/GHCi命令行语法用于文件名。

因此,需要提供模块的路径而不仅仅是名称。

最后的问题是,dynCompileExpr返回一个Dynamic对象,应该将其转换为您的函数类型Char -> Int,然后进行计算。

将所有这些组合起来,我们可以得到:

  1. module GhcApiWrap
  2. (
  3. msLoadModuleAndExecute
  4. ) where
  5. import Control.Exception.Safe
  6. import Data.Dynamic
  7. import GHC
  8. import GHC.Paths (libdir)
  9. import System.FilePath
  10. msLoadModuleAndExecute :: FilePath -> FilePath -> String -> Char -> IO Int
  11. msLoadModuleAndExecute buildDir modulePath funcName arg = do
  12. dynFunc <- runGhc (Just libdir) $ do
  13. dflags <- getSessionDynFlags
  14. setSessionDynFlags $ dflags { ghcLink = LinkInMemory
  15. , ghcMode = CompManager
  16. , objectDir = Just buildDir
  17. , hiDir = Just buildDir
  18. }
  19. target <- guessTarget modulePath Nothing
  20. setTargets [target]
  21. loadStatus <- load LoadAllTargets
  22. case loadStatus of
  23. Succeeded -> do
  24. let moduleName = dropExtension $ takeFileName modulePath
  25. importDecl_RdrName <- parseImportDecl $ "import " ++ moduleName
  26. setContext [IIDecl importDecl_RdrName]
  27. dynCompileExpr $ moduleName ++ "." ++ funcName
  28. Failed -> throwString $ "could not load the module: " ++ modulePath
  29. case fromDynamic dynFunc of
  30. Just func -> pure $ func arg
  31. Nothing -> throwString $ funcName ++ " has type '" ++ show dynFunc ++ "' but expected type `Char -> Int'"

可以通过以下方式运行它:

  1. import qualified GhcApiWrap as Ghw
  2. main :: IO ()
  3. main = print =<< Ghw.msLoadModuleAndExecute "../dyn/" "DynExample.hs" "nFromChar" 'A'

但我认为更好的做法是简化这个函数,只保留compileAndLoad,使其更通用。

  1. {-# LANGUAGE ScopedTypeVariables #-}
  2. module GhcApiWrap
  3. (
  4. msCompileAndLoad
  5. ) where
  6. import Control.Exception.Safe
  7. import Data.Dynamic
  8. import Data.Proxy
  9. import Data.Typeable
  10. import GHC
  11. import GHC.Paths (libdir)
  12. import System.FilePath
  13. msCompileAndLoad :: forall a. Typeable a => FilePath -> FilePath -> String -> IO a
  14. msCompileAndLoad buildDir modulePath symbolName = do
  15. dynSymbol <- runGhc (Just libdir) $ do
  16. dflags <- getSessionDynFlags
  17. setSessionDynFlags $ dflags { ghcLink = LinkInMemory
  18. , ghcMode = CompManager
  19. , objectDir = Just buildDir
  20. , hiDir = Just buildDir
  21. }
  22. target <- guessTarget modulePath Nothing
  23. setTargets [target]
  24. loadStatus <- load LoadAllTargets
  25. case loadStatus of
  26. Succeeded -> do
  27. let moduleName = dropExtension $ takeFileName modulePath
  28. importDecl_RdrName <- parseImportDecl $ "import " ++ moduleName
  29. setContext [IIDecl importDecl_RdrName]
  30. dynCompileExpr $ moduleName ++ "." ++ symbolName
  31. Failed -> throwString $ "could not load the module: " ++ modulePath
  32. case fromDynamic dynSymbol of
  33. Just x -> pure x
  34. Nothing -> throwString $ symbolName ++ " has type '" ++ show dynSymbol
  35. ++ "' but expected type '" ++ show symbolType ++ "'"
  36. where
  37. symbolType = typeRep (Proxy :: Proxy a)

然后我们可以使用它:

  1. import qualified GhcApiWrap as Ghw
  2. main :: IO ()
  3. main = do
  4. nFromChar <- Ghw.msCompileAndLoad "../dyn/" "DynExample.hs" "nFromChar"
  5. print $ (nFromChar :: Char -> Int) 'A'

希望这对你有帮助。

英文:

So, issue with import GHC.Paths (libdir) solved by adding a dependence to the ghc-paths package.

Next issue was with provide correct argument to the guessTarget. From the doc:

> Attempts to guess what Target a string refers to. This function implements the --make/GHCi command-line syntax for filenames.

So, need provide path to the module not just the name.

And last problem is that dynCompileExpr returns Dynamic object which should be casted to type of your function Char -&gt; Int and then calculate.

Combine all together we can get:

  1. module GhcApiWrap
  2. (
  3. msLoadModuleAndExecute
  4. ) where
  5. import Control.Exception.Safe
  6. import Data.Dynamic
  7. import GHC
  8. import GHC.Paths (libdir)
  9. import System.FilePath
  10. msLoadModuleAndExecute :: FilePath -&gt; FilePath -&gt; String -&gt; Char -&gt; IO Int
  11. msLoadModuleAndExecute buildDir modulePath funcName arg = do
  12. dynFunc &lt;- runGhc (Just libdir) $ do
  13. dflags &lt;- getSessionDynFlags
  14. setSessionDynFlags $ dflags { ghcLink = LinkInMemory
  15. , ghcMode = CompManager
  16. , objectDir = Just buildDir
  17. , hiDir = Just buildDir
  18. }
  19. target &lt;- guessTarget modulePath Nothing
  20. setTargets [target]
  21. loadStatus &lt;- load LoadAllTargets
  22. case loadStatus of
  23. Succeeded -&gt; do
  24. let moduleName = dropExtension $ takeFileName modulePath
  25. importDecl_RdrName &lt;- parseImportDecl $ &quot;import &quot; ++ moduleName
  26. setContext [IIDecl importDecl_RdrName]
  27. dynCompileExpr $ moduleName ++ &quot;.&quot; ++ funcName
  28. Failed -&gt; throwString $ &quot;could not load the module: &quot; ++ modulePath
  29. case fromDynamic dynFunc of
  30. Just func -&gt; pure $ func arg
  31. Nothing -&gt; throwString $ funcName ++ &quot; has type &#39;&quot; ++ show dynFunc ++ &quot; but expected type `Char -&gt; Int&#39;&quot;

which can be run with:

  1. import qualified GhcApiWrap as Ghw
  2. main :: IO ()
  3. main = print =&lt;&lt; Ghw.msLoadModuleAndExecute &quot;../dyn/&quot; &quot;DynExample.hs&quot; &quot;nFromChar&quot; &#39;A&#39;

But I think will be better simplify this function on just compileAndLoad that make it more generic.

  1. {-# LANGUAGE ScopedTypeVariables #-}
  2. module GhcApiWrap
  3. (
  4. msCompileAndLoad
  5. ) where
  6. import Control.Exception.Safe
  7. import Data.Dynamic
  8. import Data.Proxy
  9. import Data.Typeable
  10. import GHC
  11. import GHC.Paths (libdir)
  12. import System.FilePath
  13. msCompileAndLoad :: forall a. Typeable a =&gt; FilePath -&gt; FilePath -&gt; String -&gt; IO a
  14. msCompileAndLoad buildDir modulePath symbolName = do
  15. dynSymbol &lt;- runGhc (Just libdir) $ do
  16. dflags &lt;- getSessionDynFlags
  17. setSessionDynFlags $ dflags { ghcLink = LinkInMemory
  18. , ghcMode = CompManager
  19. , objectDir = Just buildDir
  20. , hiDir = Just buildDir
  21. }
  22. target &lt;- guessTarget modulePath Nothing
  23. setTargets [target]
  24. loadStatus &lt;- load LoadAllTargets
  25. case loadStatus of
  26. Succeeded -&gt; do
  27. let moduleName = dropExtension $ takeFileName modulePath
  28. importDecl_RdrName &lt;- parseImportDecl $ &quot;import &quot; ++ moduleName
  29. setContext [IIDecl importDecl_RdrName]
  30. dynCompileExpr $ moduleName ++ &quot;.&quot; ++ symbolName
  31. Failed -&gt; throwString $ &quot;could not load the module: &quot; ++ modulePath
  32. case fromDynamic dynSymbol of
  33. Just x -&gt; pure x
  34. Nothing -&gt; throwString $ symbolName ++ &quot; has type &#39;&quot; ++ show dynSymbol
  35. ++ &quot;&#39; but expected type &#39;&quot; ++ show symbolType ++ &quot;&#39;&quot;
  36. where
  37. symbolType = typeRep (Proxy :: Proxy a)

Then we can use it:

  1. import qualified GhcApiWrap as Ghw
  2. main :: IO ()
  3. main = do
  4. nFromChar &lt;- Ghw.msCompileAndLoad &quot;../dyn/&quot; &quot;DynExample.hs&quot; &quot;nFromChar&quot;
  5. print $ (nFromChar :: Char -&gt; Int) &#39;A&#39;

答案2

得分: 0

以下是您提供的内容的中文翻译:

"freestyle" 和 "K. A. Buhr" 的答案有效。

但是,我创建了一个备用版本,应该具有以下特点:

  • 不抛出异常,而是通过 Either 转发错误消息
  • 捕获所有异常以通过 Either 转发其错误消息
  • 仅在需要时修改 dflags
  • 避免使用 do 表示法
  • 避免使用 case 构造
  • 在合理情况下使用限定的导入
  • 描述所有必需的内容,包括堆栈设置细节
  • 使用模块名称来暗示文件的扩展名为“hs”
  • 无 hlint 提示

我对代码进行了测试,包括以下方面:

  • 函数
  • 模块文件不可用
    (例如,“错误:异常:找不到文件:./dyn/DynExample.hs”)
  • 编译器错误与模块文件相关
    (例如,“错误:无法加载模块“DynExample”!”)
  • 与导入符号相关的类型检查错误
    (例如,“错误:符号 nFromChar 的类型为“<<Char -> Int>>”,但期望类型为“Char -> Integer””)

GhcApiWrap

  1. {-# LANGUAGE ScopedTypeVariables #-}
  2. module GhcApiWrap
  3. (
  4. compileAndLoad
  5. ) where
  6. import qualified Control.Exception.Safe as Exc
  7. import qualified Data.Dynamic as Dyn
  8. import qualified Data.Proxy as Px
  9. import qualified Data.Typeable as T
  10. import qualified GHC as Ghc
  11. import qualified GHC.Paths as Pth
  12. import Data.Functor ((<&>))
  13. compileAndLoad :: forall a. T.Typeable a => String -> String -> String -> IO (Either String a)
  14. compileAndLoad dir mdl sym = runGhcCatched >>= ethSym
  15. where
  16. runGhcCatched = Ghc.runGhc (Just Pth.libdir) (loadModule dir mdl sym)
  17. `Exc.catchAny`
  18. (\ex -> return (Left ("Exception: " ++ show ex)))
  19. ethSym (Right dynSym) = return (ethSym' (Dyn.fromDynamic dynSym) (sErrFromDynSym dynSym))
  20. ethSym (Left sErr) = return (Left sErr)
  21. sErrFromDynSym dynSym = "Symbol " ++ sym ++ " has type \"" ++ show dynSym ++
  22. "\" but expected type \"" ++ show symbolType ++ "\""
  23. ethSym' (Just dynSym) _ = Right dynSym
  24. ethSym' Nothing sErr = Left sErr
  25. symbolType = T.typeRep (Px.Proxy :: Px.Proxy a)
  26. loadModule :: Ghc.GhcMonad m => String -> String -> String -> m (Either String Dyn.Dynamic)
  27. loadModule dir mdl sym =
  28. Ghc.getSessionDynFlags >>=
  29. Ghc.setSessionDynFlags >>
  30. Ghc.guessTarget (dir ++ mdl ++ ".hs") Nothing >>=
  31. (\target -> Ghc.setTargets [target]) >>
  32. Ghc.load Ghc.LoadAllTargets >>=
  33. returnFromLoadStatus
  34. where
  35. returnFromLoadStatus Ghc.Succeeded = dynSym <&> Right
  36. returnFromLoadStatus Ghc.Failed = return (Left ("Could not load the module \"" ++ mdl ++ "\"!"))
  37. dynSym = Ghc.parseImportDecl ("import " ++ mdl) >>=
  38. (\importDecl -> Ghc.setContext [Ghc.IIDecl importDecl]) >>
  39. Ghc.dynCompileExpr (mdl ++ "." ++ sym)

Usage

  1. {-# LANGUAGE LambdaCase #-}
  2. import qualified GhcApiWrap as Ghw
  3. main :: IO ()
  4. main = Ghw.compileAndLoad "./dyn/" "DynExample" "nFromChar" >>=
  5. (\case
  6. (Right nFromChar) -> putStrLn ("nFromChar 'A' := " ++ show ((nFromChar :: Char -> Integer) 'A'))
  7. (Left sErrorMessage) -> putStrLn ("Error: " ++ sErrorMessage))

动态文件以供阅读(“DynExample.hs”)

注意:文件必须位于与目录“src”并排的目录“dyn”中,以与上面的代码(“Usage”)兼容。

  1. module DynExample
  2. (
  3. nFromChar
  4. ) where
  5. import qualified Data.Char as Chr
  6. nFromChar :: Char -> Int
  7. nFromChar = Chr.ord

输出

  1. nFromChar 'A' := 65

package.yaml

  1. ...
  2. description:...
  3. dependencies:
  4. - base >= 4.7 && < 5
  5. - ghc
  6. - ghc-paths
  7. - safe-exceptions
  8. ghc-options:
  9. ...

stack.yaml

  1. ...
  2. resolver:
  3. url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/20/20.yaml
  4. ...

希望这些翻译对您有所帮助。

英文:

The answer from freestyle and K. A. Buhr works.

However, I created an alternative version, which should have the following characteristics:

  • not throwing exceptions but forwarding error messages via Either
  • catching all exceptions to forward its error messages via Either
  • not modifying the dflags if not needed
  • avoiding the do notation
  • avoiding case constructions
  • using qualified imports, where reasonably possible
  • describing all necessities including stack setup details
  • usage of the module name implying extension “hs” for the file
  • hlint free

I tested the code regarding

  • function
  • unavailability of the module file
    (e.g. "Error: Exception: can't find file: ./dyn/DynExample.hs")
  • compiler error regarding the module file
    (e.g. "Error: Could not load the module "DynExample"!")
  • error regarding type check vs. imported symbol
    (e.g. "Error: Symbol nFromChar has type "<<Char -> Int>>" but expected type "Char -> Integer"")

GhcApiWrap

  1. {-# LANGUAGE ScopedTypeVariables #-}
  2. module GhcApiWrap
  3. (
  4. compileAndLoad
  5. ) where
  6. import qualified Control.Exception.Safe as Exc
  7. import qualified Data.Dynamic as Dyn
  8. import qualified Data.Proxy as Px
  9. import qualified Data.Typeable as T
  10. import qualified GHC as Ghc
  11. import qualified GHC.Paths as Pth
  12. import Data.Functor ((&lt;&amp;&gt;))
  13. compileAndLoad :: forall a. T.Typeable a =&gt; String -&gt; String -&gt; String -&gt; IO (Either String a)
  14. compileAndLoad dir mdl sym = runGhcCatched &gt;&gt;= ethSym
  15. where
  16. runGhcCatched = Ghc.runGhc (Just Pth.libdir) (loadModule dir mdl sym)
  17. `Exc.catchAny`
  18. (\ex -&gt; return (Left (&quot;Exception: &quot; ++ show ex)))
  19. ethSym (Right dynSym) = return (ethSym&#39; (Dyn.fromDynamic dynSym) (sErrFromDynSym dynSym))
  20. ethSym (Left sErr) = return (Left sErr)
  21. sErrFromDynSym dynSym = &quot;Symbol &quot; ++ sym ++ &quot; has type \&quot;&quot; ++ show dynSym ++
  22. &quot;\&quot; but expected type \&quot;&quot; ++ show symbolType ++ &quot;\&quot;&quot;
  23. ethSym&#39; (Just dynSym) _ = Right dynSym
  24. ethSym&#39; Nothing sErr = Left sErr
  25. symbolType = T.typeRep (Px.Proxy :: Px.Proxy a)
  26. loadModule :: Ghc.GhcMonad m =&gt; String -&gt; String -&gt; String -&gt; m (Either String Dyn.Dynamic)
  27. loadModule dir mdl sym =
  28. Ghc.getSessionDynFlags &gt;&gt;=
  29. Ghc.setSessionDynFlags &gt;&gt;
  30. Ghc.guessTarget (dir ++ mdl ++ &quot;.hs&quot;) Nothing &gt;&gt;=
  31. (\target -&gt; Ghc.setTargets [target]) &gt;&gt;
  32. Ghc.load Ghc.LoadAllTargets &gt;&gt;=
  33. returnFromLoadStatus
  34. where
  35. returnFromLoadStatus Ghc.Succeeded = dynSym &lt;&amp;&gt; Right
  36. returnFromLoadStatus Ghc.Failed = return (Left (&quot;Could not load the module \&quot;&quot; ++ mdl ++ &quot;\&quot;!&quot;))
  37. dynSym = Ghc.parseImportDecl (&quot;import &quot; ++ mdl) &gt;&gt;=
  38. (\importDecl -&gt; Ghc.setContext [Ghc.IIDecl importDecl]) &gt;&gt;
  39. Ghc.dynCompileExpr (mdl ++ &quot;.&quot; ++ sym)

Usage

  1. {-# LANGUAGE LambdaCase #-}
  2. import qualified GhcApiWrap as Ghw
  3. main :: IO ()
  4. main = Ghw.compileAndLoad &quot;./dyn/&quot; &quot;DynExample&quot; &quot;nFromChar&quot; &gt;&gt;=
  5. (\case
  6. (Right nFromChar) -&gt; putStrLn (&quot;nFromChar &#39;A&#39; := &quot; ++ show ((nFromChar :: Char -&gt; Integer) &#39;A&#39;))
  7. (Left sErrorMassage) -&gt; putStrLn (&quot;Error: &quot; ++ sErrorMassage))

Dynamic file to read ("DynExample.hs")

NOTE: The file has to be located in diretcory dyn side by side to the directory src, in order to be compatible with the code above ("Usage").

  1. module DynExample
  2. (
  3. nFromChar
  4. ) where
  5. import qualified Data.Char as Chr
  6. nFromChar :: Char -&gt; Int
  7. nFromChar = Chr.ord

Output

  1. nFromChar &#39;A&#39; := 65

package.yaml

  1. ...
  2. description:...
  3. dependencies:
  4. - base &gt;= 4.7 &amp;&amp; &lt; 5
  5. - ghc
  6. - ghc-paths
  7. - safe-exceptions
  8. ghc-options:
  9. ...

stack.yaml

  1. ...
  2. resolver:
  3. url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/20/20.yaml
  4. ...

huangapple
  • 本文由 发表于 2023年8月4日 23:30:01
  • 转载请务必保留本文链接:https://go.coder-hub.com/76837326.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定