packages feed

call-alloy 0.2.1.1 → 0.2.2.0

raw patch · 7 files changed

+662/−339 lines, 7 filesdep −lensdep ~Win32dep ~basedep ~bytestringPVP ok

version bump matches the API change (PVP)

Dependencies removed: lens

Dependency ranges changed: Win32, base, bytestring, hashable

API changes (from Hackage documentation)

+ Language.Alloy.Call: getDoubleAs :: (IsString s, MonadError s m, Ord a, Ord b) => String -> (String -> Int -> m a) -> (String -> Int -> m b) -> AlloySig -> m (Set (a, b))
+ Language.Alloy.Call: getSingleAs :: (IsString s, MonadError s m, Ord a) => String -> (String -> Int -> m a) -> AlloySig -> m (Set a)
+ Language.Alloy.Call: getTripleAs :: (IsString s, MonadError s m, Ord a, Ord b, Ord c) => String -> (String -> Int -> m a) -> (String -> Int -> m b) -> (String -> Int -> m c) -> AlloySig -> m (Set (a, b, c))
+ Language.Alloy.Call: int :: (IsString s, MonadError s m, Semigroup s) => String -> Int -> m Int
+ Language.Alloy.Call: object :: (IsString s, MonadError s m, Semigroup s) => String -> (Int -> a) -> String -> Int -> m a
+ Language.Alloy.Debug: getRawInstances :: Maybe Integer -> String -> IO [ByteString]
+ Language.Alloy.Debug: getRawInstancesWith :: CallAlloyConfig -> String -> IO [ByteString]

Files

ChangeLog.md view
@@ -4,6 +4,13 @@  ## Released changes +### 0.2.2.0++- deprecate 'relToMap'+- provide functions for returning raw output of instances+- provide functions for typed retrieval+- deprecate 'getSingle', 'getDouble', 'getTriple', 'objectName'+ ### 0.2.1.1  - fix errors due to long Alloy code by starting timeout after transferring code
README.md view
@@ -19,3 +19,129 @@  On every call the application checks the [`XdgDirectory`](https://hackage.haskell.org/package/directory/docs/System-Directory.html#t:XdgDirectory) if the libraries exist in a current version. If not they are placed there together with a version identifier.++## The library in action++This is a basic description on how to use the library.++### A specification example++Consider this Alloy specification of a simple Graph:++```Alloy+abstract sig Node {+  flow : Node -> lone Int,+  stored : one Int+} {+  stored >= 0+  all n : Node | some flow[n] implies flow[n] >= 0+  no flow[this]+}++fun currentFlow(x, y : one Node) : Int {+  let s = x.stored, f = x.flow[y] | s < f implies s else f+}++pred withFlow[x, y : one Node] {+  currentFlow[x, y] > 0+}++pred show {}++run withFlow for 3 Int, 2 Node+```++The graph is consisting of `Node`s, which might have some goods `stored` and may deliver them to other `Node`s (via `flow`).+`Node`s do not have `flow` to themselves.+The `currentFlow` is the minimum between the flow from the starting `Node` to the end `Node` and the currently `stored` goods at the starting `Node` (note: intermediate `Node`s are not allowed).+We call two `Nodes` `x` and `y` `withFlow` if `currentFlow` from `x` to `y` is greater than `0`.+We restrict our search to `3`-Bit signed `Int` values and `2` `Nodes`.++### An instance example++Calling Alloy using `getInstances` and the above program,+could return the following (abbreviated) instance:++``` Haskell+[(Signature {+    scope = Nothing,+    sigName = "$withFlow_x"+    },+  Entry {+    annotation = Just Skolem,+    relation = fromList [+      ("",Single (fromList [Object {objSig = "Node", identifier = 1}]))+      ]+    }),+ (Signature {+    scope = Nothing,+    sigName = "$withFlow_y"+    },+  Entry {+    annotation = Just Skolem,+    relation = fromList [+      ("",Single (fromList [Object {objSig = "Node", identifier = 0}]))+      ]+    }),+ ...+ (Signature {+    scope = Just "this",+    sigName = "Node"+    },+  Entry {+    annotation = Nothing,+    relation = fromList [+      ("",Single (fromList [+        Object {objSig = "Node", identifier = 0},+        Object {objSig = "Node", identifier = 1}+        ])),+      ("flow",Triple (fromList [+        (Object {objSig = "Node", identifier = 0},Object {objSig = "Node", identifier = 1},NumberObject {number = 0}),+        (Object {objSig = "Node", identifier = 1},Object {objSig = "Node", identifier = 0},NumberObject {number = 3})+        ])),+      ("stored",Double (fromList [+        (Object {objSig = "Node", identifier = 0},NumberObject {number = 0}),+        (Object {objSig = "Node", identifier = 1},NumberObject {number = 1})+        ]))+      ]+    })+ ]+```++### A retrieval example++Using this library we may retrieve returned signature values using `lookupSig`,+then query parameter variables of the queried predicate using `unscoped`,+and query signature sets and relations using `getSingleAs`, `getDoubleAs`, and `getTripleAs`.++The following Code might for instance be used for the graph example:++``` Haskell+newtype Node = Node Int deriving (Eq, Show, Ord)++instanceToNames+  :: AlloyInstance+  -> Either String (Set Node, Set (Node, Int), Set (Node, Node, Int), Set (Node), Set (Node))+instanceToNames insta = do+  let node :: String -> Int -> Either String Node+      node = object "Node" Node+  n     <- lookupSig (scoped "this" "Node") insta+  nodes <- getSingleAs "" node n+  store <- getDoubleAs "stored" node int n+  flow  <- getTripleAs "flow" node node int n+  x     <- lookupSig (unscoped "$withFlow_x") insta >>= getSingleAs "" node+  y     <- lookupSig (unscoped "$withFlow_y") insta >>= getSingleAs "" node+  return (nodes, store, flow, x, y)+```++Calling `instanceToNames` on the above instance would result in the following expression:++``` Haskell+Right (+  fromList [Node 0,Node 1],+  fromList [(Node 0,0),(Node 1,1)],+  fromList [(Node 0,Node 1,0),(Node 1,Node 0,3)],+  fromList [Node 1],+  fromList [Node 0]+  )+```
call-alloy.cabal view
@@ -1,13 +1,13 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.33.0.+-- This file has been generated from package.yaml by hpack version 0.34.4. -- -- see: https://github.com/sol/hpack ----- hash: a6397f9e52550d7e13e9e9b7b80fdbdc15cd29ab28396f6c2ad8dbd8cf0774b2+-- hash: 5c150629bcf8420718ec571afc36561ad329e6a349265b2719b219d59a96a022  name:           call-alloy-version:        0.2.1.1+version:        0.2.2.0 synopsis:       A simple library to call Alloy given a specification description:    Please see the README on GitHub at <https://github.com/marcellussiegburg/call-alloy#readme> category:       Language@@ -36,8 +36,10 @@ library   exposed-modules:       Language.Alloy.Call+      Language.Alloy.Debug   other-modules:       Language.Alloy.Functions+      Language.Alloy.Internal.Call       Language.Alloy.Parser       Language.Alloy.RessourceNames       Language.Alloy.Ressources@@ -45,26 +47,26 @@       Paths_call_alloy   hs-source-dirs:       src+  ghc-options: -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Widentities -Wredundant-constraints   build-depends:-      base >=4.7 && <5-    , bytestring >=0.10 && <0.12-    , containers >=0.6 && <0.7-    , directory >=1.3 && <1.4+      base >=4.12 && <5+    , bytestring >=0.10.4 && <0.12+    , containers ==0.6.*+    , directory ==1.3.*     , file-embed >=0.0.11 && <0.1-    , filepath >=1.4 && <1.5+    , filepath ==1.4.*     , hashable >=1.2 && <1.4-    , lens >=4.17 && <4.20-    , mtl >=2.2 && <2.3-    , process >=1.6 && <1.7-    , split >=0.2 && <0.3+    , mtl ==2.2.*+    , process ==1.6.*+    , split ==0.2.*     , trifecta >=2 && <2.2   if os(windows)     cpp-options: -DWINDOWS     build-depends:-        Win32 >=2.5 && <2.11+        Win32 >=2.5 && <2.14   else     build-depends:-        unix >=2.7 && <2.8+        unix ==2.7.*   default-language: Haskell2010  test-suite call-alloy-test@@ -72,7 +74,9 @@   main-is: Spec.hs   other-modules:       Language.Alloy.Call+      Language.Alloy.Debug       Language.Alloy.Functions+      Language.Alloy.Internal.Call       Language.Alloy.Parser       Language.Alloy.RessourceNames       Language.Alloy.Ressources@@ -82,27 +86,26 @@   hs-source-dirs:       src       test-  ghc-options: -threaded -rtsopts -with-rtsopts=-N+  ghc-options: -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Widentities -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N   build-depends:-      base >=4.7 && <5-    , bytestring >=0.10 && <0.12+      base >=4.12 && <5+    , bytestring >=0.10.4 && <0.12     , call-alloy-    , containers >=0.6 && <0.7-    , directory >=1.3 && <1.4+    , containers ==0.6.*+    , directory ==1.3.*     , file-embed >=0.0.11 && <0.1-    , filepath >=1.4 && <1.5+    , filepath ==1.4.*     , hashable >=1.2 && <1.4     , hspec-    , lens >=4.17 && <4.20-    , mtl >=2.2 && <2.3-    , process >=1.6 && <1.7-    , split >=0.2 && <0.3+    , mtl ==2.2.*+    , process ==1.6.*+    , split ==0.2.*     , trifecta >=2 && <2.2   if os(windows)     cpp-options: -DWINDOWS     build-depends:-        Win32 >=2.5 && <2.11+        Win32 >=2.5 && <2.14   else     build-depends:-        unix >=2.7 && <2.8+        unix ==2.7.*   default-language: Haskell2010
src/Language/Alloy/Call.hs view
@@ -1,8 +1,7 @@-{-# LANGUAGE ScopedTypeVariables #-} {-| Module      : Language.Alloy.Call Description : A simple library to call Alloy given a specification-Copyright   : (c) Marcellus Siegburg, 2019+Copyright   : (c) Marcellus Siegburg, 2019 - 2021 License     : MIT Maintainer  : marcellus.siegburg@uni-due.de @@ -12,8 +11,6 @@ A requirement for this library to work is a Java Runtime Environment (as it is required by Alloy). -}-{-# LANGUAGE CPP #-}-{-# LANGUAGE OverloadedStrings #-} module Language.Alloy.Call (   CallAlloyConfig (maxInstances, noOverflow, timeout),   defaultCallAlloyConfig,@@ -24,92 +21,13 @@   module Types,   ) where -import qualified Data.ByteString                  as BS-  (hGetLine, intercalate, isSuffixOf, writeFile)-import qualified Data.ByteString.Char8            as BS (unlines)--import Control.Concurrent (-  ThreadId,-  forkIO, killThread, newEmptyMVar, putMVar, takeMVar, threadDelay,-  )-import Control.Exception                (IOException)-import Control.Lens.Internal.ByteString (unpackStrict8)-import Control.Monad                    (unless, void, when)-import Data.ByteString                  (ByteString)-import Data.Hashable                    (hash)-import Data.IORef                       (IORef, newIORef, readIORef)-import Data.List.Split                  (splitOn)-import Data.Maybe                       (fromMaybe)-import System.Directory-  (XdgDirectory (..), createDirectory, doesFileExist, doesDirectoryExist,-   getTemporaryDirectory, getXdgDirectory)-import System.Directory.Internal        (setFileMode)-import System.Directory.Internal.Prelude-  (catch, isDoesNotExistError)-import System.Exit                      (ExitCode (..))-import System.FilePath-  ((</>), (<.>), searchPathSeparator, takeDirectory)-import System.IO-  (BufferMode (..), Handle, hClose, hFlush, hIsEOF, hPutStr, hSetBuffering)-import System.IO.Unsafe                 (unsafePerformIO)-import System.Process (-  CreateProcess (..), StdStream (..), ProcessHandle,-  createProcess, proc, terminateProcess, waitForProcess,-  )-#if defined(mingw32_HOST_OS)-import System.Win32.Info                (getUserName)-#else-import System.Posix.User                (getLoginName)-#endif- import Language.Alloy.Functions         as Functions+import Language.Alloy.Internal.Call import Language.Alloy.Parser            (parseInstance)-import Language.Alloy.RessourceNames (-  alloyJarName, className, classPackage, commonsCliJarName-  )-import Language.Alloy.Ressources        (alloyJar, classFile, commonsCliJar) import Language.Alloy.Types             as Types   (AlloyInstance, AlloySig, Entries, Object, Signature)  {-|-Configuration for calling alloy. These are:-- * maximal number of instances to retrieve ('Nothing' for all)- * wheather to not overflow when calculating numbers within Alloy- * an timeout after which to forcibly kill Alloy-   (retrieving only instances that were returned before killing the process)--}-data CallAlloyConfig = CallAlloyConfig {-  -- | maximal number of instances to retrieve ('Nothing' for all)-  maxInstances :: Maybe Integer,-  -- | wheather to not overflow when calculating numbers within Alloy-  noOverflow   :: Bool,-  -- | the time in microseconds after which to forcibly kill Alloy-  --   ('Nothing' for never)-  timeout      :: Maybe Int-  }--{-|-Default configuration for calling Alloy. Defaults to:-- * retrieve all instances- * do not overflow--}-defaultCallAlloyConfig :: CallAlloyConfig-defaultCallAlloyConfig = CallAlloyConfig {-  maxInstances = Nothing,-  noOverflow   = True,-  timeout      = Nothing-  }--{-# NOINLINE mclassPath #-}-{-|-'IORef' for storing the class path.--}-mclassPath :: IORef (Maybe FilePath)-mclassPath = unsafePerformIO (newIORef Nothing)--{-| This function may be used to get all model instances for a given Alloy specification. It calls Alloy via a Java interface and parses the raw instance answers before returning the resulting list of 'AlloyInstance's.@@ -136,212 +54,9 @@   -> String   -- ^ The Alloy specification which should be loaded.   -> IO [AlloyInstance]-getInstancesWith config content = do-  classPath <- getClassPath-  let callAlloy = proc "java"-        $ ["-cp", classPath, classPackage ++ '.' : className,-           "-i", show $ fromMaybe (-1) $ maxInstances config]-        ++ ["-o" | not $ noOverflow config]-  (Just hin, Just hout, Just herr, ph) <--    createProcess callAlloy {-        std_out = CreatePipe,-        std_in  = CreatePipe,-        std_err = CreatePipe-      }-  pout <- listenForOutput hout-  perr <- listenForOutput herr-#ifndef mingw32_HOST_OS-  hSetBuffering hin NoBuffering-#endif-  hPutStr hin content-  hFlush hin-  hClose hin-  maybe (return ()) (void . startTimeout hin hout herr ph) $ timeout config-  out <- getOutput pout-  err <- getOutput perr-  printContentOnError ph-  unless (null err) $ fail $ unpackStrict8 $ BS.unlines err-  let instas = fmap (BS.intercalate "\n") $ drop 1 $ splitOn [begin] out-  let finstas = filterLast (not . (partialInstance `BS.isSuffixOf`)) instas-  return $ either (error . show) id . parseInstance <$> finstas-  where-    begin :: ByteString-    begin = "---INSTANCE---"-    partialInstance :: ByteString-    partialInstance = "---PARTIAL_INSTANCE---"-    filterLast _ []     = []-    filterLast p x@[_]  = filter p x-    filterLast p (x:xs) = x:filterLast p xs-    getWholeOutput h = do-      eof <- hIsEOF h-      if eof-        then return []-      else catch-        ((:) <$> BS.hGetLine h <*> getWholeOutput h)-        (\(_ :: IOException) -> return [partialInstance])-    printContentOnError ph = do-      code <- waitForProcess ph-      when (code == ExitFailure 1)-        $ putStrLn $ "Failed parsing the Alloy code:\n" <> content-    listenForOutput h = do-      mvar <- newEmptyMVar-      pid <- forkIO $ getWholeOutput h >>= putMVar mvar-      return (pid, mvar)-    getOutput (pid, mvar) = do-      output <- takeMVar mvar-      killThread pid-      return output--{-|-Start a new process that aborts execution by closing all handles and-killing the processes after the given amount of time.--}-startTimeout-  :: Handle-  -- ^ the input handle to close-  -> Handle-  -- ^ the output handle to close-  -> Handle-  -- ^ the error handle to close-  -> ProcessHandle-  -- ^ the main process handle-  -> Int -> IO ThreadId-startTimeout i o e ph t = forkIO $ do-  threadDelay t-  void $ forkIO $ hClose e-  void $ forkIO $ hClose o-  terminateProcess ph-  hClose i--{-|-Check if the class path was determined already, if so use it, otherwise call-'readClassPath'.--Returns the class path.--}-getClassPath :: IO FilePath-getClassPath = do-  mclassPath' <- readIORef mclassPath-  maybe readClassPath return mclassPath'--{--{--data CallAlloyConfig = Config {-    alloyJarFile   :: FilePath,-    alloyClassFile :: FilePath,-    keepFiles      :: Bool-  }--}--jarFileEnv :: String-jarFileEnv = "ALLOY_JAR_FILE"--callAlloyEnv :: String-callAlloyEnv = "CALL_ALLOY_CLASS_FILE"--keepFilesEnv :: String-keepFilesEnv = "KEEP_ALLOY_FILES"--{-|-Lookup environment variables which are to prefer if present.--}-getEnvironmentInformation :: IO CallAlloyConfig-getEnvironmentInformation = do-  alloy     <- lookupEnv jarFileEnv-  callAlloy <- lookupEnv callAlloyEnv-  keep      <- lookupEnv keepFilesEnv-  let mconfig = Config <$> alloy <*> callAlloy <*> pure (isJust keep)-  case mconfig of-    Nothing -> do-      dataDir <- getXdgDirectory XdgData $ appName </> "dataDir"-    Just c  -> return c--}-{--getVersionFile :: IO FilePath-getVersionFile = do-  configDir <- getXdgDirectory XdgConfig appName-  let versionFile = configDir </> "version"-  exists <- doesFileExist versionFile-  if exists-    then do-    version <- read <$> readFile versionFile-    unless (version == versionHash) $ createVersionFile configDir versionFile-    else createVersionFile configDir versionFile--}--fallbackToTempDir :: IO FilePath -> IO FilePath-fallbackToTempDir m = catch m $ \e ->-  if isDoesNotExistError e-  then do-    tmp    <- getTemporaryDirectory-#if defined(mingw32_HOST_OS)-    login  <- getUserName-#else-    login  <- getLoginName-#endif-    let tmpDir = tmp </> show (hash login) </> appName-    createUserDirectoriesIfMissing tmpDir-    return tmpDir-  else error $ show e--{-|-Read the class path version specified in the user directory, if it is not-current or if it does not exist, call 'createVersionFile'.--Returns the class path.--}-readClassPath :: IO FilePath-readClassPath = do-  configDir <- fallbackToTempDir $ getXdgDirectory XdgConfig appName-  let versionFile = configDir </> "version"-  exists <- doesFileExist versionFile-  if exists-    then do-    version <- read <$> readFile versionFile-    unless (version == versionHash) $ createVersionFile configDir versionFile-    else createVersionFile configDir versionFile-  dataDir <- getXdgDirectory XdgData $ appName </> "dataDir"-  return $ dataDir ++ searchPathSeparator : dataDir </> alloyJarName-    ++ searchPathSeparator : dataDir </> commonsCliJarName--{-|-Create all library files within the users 'XdgDirectory' by calling-'createDataDir' then place the current version number into a configuration File.--}-createVersionFile :: FilePath -> FilePath -> IO ()-createVersionFile configDir versionFile = do-  createDataDir-  createUserDirectoriesIfMissing configDir-  writeFile versionFile $ show versionHash--{-|-Create all library files within the users 'XdgDirectory' based on the source-files enclosed into this library (see also 'Language.Alloy.RessourceNames' and-'Language.Alloy.Ressources').--}-createDataDir :: IO ()-createDataDir = do-  dataDir <- fallbackToTempDir $ getXdgDirectory XdgData $ appName </> "dataDir"-  createUserDirectoriesIfMissing $ dataDir </> classPackage-  BS.writeFile (dataDir </> classPackage </> className <.> "class") classFile-  BS.writeFile (dataDir </> alloyJarName) alloyJar-  BS.writeFile (dataDir </> commonsCliJarName) commonsCliJar--{-|-Creates user directories using the file permissions 700.-This function creates the specified directory and all its parent directories as-well (if they are also missing).--}-createUserDirectoriesIfMissing :: FilePath -> IO ()-createUserDirectoriesIfMissing fp = do-  isDir <- doesDirectoryExist fp-  let parent = takeDirectory fp-  unless (isDir || parent == fp) $ do-    createUserDirectoriesIfMissing parent-    createDirectory fp-#ifndef mingw32_HOST_OS-    setFileMode fp (7*8*8)-#endif+getInstancesWith config content =+  map (either (error . show) id . parseInstance)+  <$> getRawInstancesWith config content  {-| Check if there exists a model for the given specification. This function calls@@ -354,21 +69,3 @@   -> IO Bool   -- ^ Whether there exists an instance (within the relevant scope). existsInstance = fmap (not . null) . getInstances (Just 1)--{-|-The application name (used to store data in a specific directory.--}-appName :: String-appName = "call-alloy"--{-# INLINE versionHash #-}-{-|-Used to determine possible source code and Alloy version changes across multiple-versions of this library.--}-versionHash :: Int-versionHash = hash $ alloyHash + commonsCliHash + classFileHash-  where-    alloyHash = hash alloyJar-    commonsCliHash = hash commonsCliJar-    classFileHash = hash classFile
+ src/Language/Alloy/Debug.hs view
@@ -0,0 +1,14 @@+{-|+Module      : Language.Alloy.Call+Copyright   : (c) Marcellus Siegburg, 2019 - 2021+License     : MIT++This module provides functions to retrieve raw instances form Alloy,+i.e. as Alloy provides them.+-}+module Language.Alloy.Debug (+  getRawInstances,+  getRawInstancesWith,+  ) where++import Language.Alloy.Internal.Call     (getRawInstances, getRawInstancesWith)
src/Language/Alloy/Functions.hs view
@@ -11,6 +11,8 @@ -} module Language.Alloy.Functions (   getSingle, getDouble, getTriple,+  getSingleAs, getDoubleAs, getTripleAs,+  int, object,   lookupSig,   objectName,   relToMap,@@ -48,7 +50,83 @@ unscoped :: String -> Signature unscoped = Signature Nothing +{-+Might be introduced some time in Data.Set in the containers package+see https://github.com/haskell/containers/issues/779+-}+traverseSet+  :: (Ord a, Applicative f)+  => (a2 -> f a)+  -> Set a2+  -> f (Set a)+traverseSet f = fmap S.fromList . traverse f . S.toList+ {-|+For retrieval of 'Int' values using a get... function.++e.g. returning all (within Alloy) available Int values could look like this++> do n <- lookupSig (unscoped "Int")+>    getSingleAs "" int n+-}+int+  :: (IsString s, MonadError s m, Semigroup s)+  => String+  -> Int+  -> m Int+int = object "" id++{-|+For retrieval of an unmixed type of values using a get... function+(should be the case for uniformly base named values;+this is usually never true for the universe (@lookupSig (unscoped "univ")@))+I.e. setting and checking the 'String' for the base name of the value to look for,+but failing in case anything different appears (unexpectedly).+-}+object+  :: (IsString s, MonadError s m, Semigroup s)+  => String+  -> (Int -> a)+  -> String+  -> Int+  -> m a+object s f s' g =+  if s /= s+  then throwError $ "expected an object of name " <> fromString s+    <> " but got an object of name "+    <> fromString s' <> "."+  else return $ f g++specifyObject+  :: (String -> Int -> m a)+  -> Object+  -> m a+specifyObject f o = case o of+  NumberObject i -> f "" i+  Object n i -> f n i+  NamedObject g -> error $ "there is no way of converting Object "+    ++ g+    ++ "\nPlease open an issue at https://github.com/marcellussiegburg/call-alloy stating what you tried to attempt"++{-|+Retrieve a set of values of a given 'AlloySig'.+Values will be created by applying the given mapping function from object Name+and 'Int' to value.+The mapping has to be injective (for all expected cases).+Successful if the signature's relation is a set (or empty).+-}+getSingleAs+  :: (IsString s, MonadError s m, Ord a)+  => String+  -> (String -> Int -> m a)+  -> AlloySig+  -> m (Set a)+getSingleAs s f inst = do+  set <- getSingle s inst+  traverseSet (specifyObject f) set++{-# DEPRECATED getSingle "use the typed version getSingleAs instead" #-}+{-| Retrieve a set of objects of a given 'AlloySig'. Successful if the signature's relation is a set (or empty). -}@@ -60,6 +138,29 @@ getSingle = lookupRel single  {-|+Retrieve a binary relation of values of given 'AlloySig'.+Values will be created by applying the given mapping functions from object Name+and 'Int' to the value.+The mapping has to be injective (for all expected cases).+Successful if the signature's relation is binary (or empty).+-}+getDoubleAs+  :: (IsString s, MonadError s m, Ord a, Ord b)+  => String+  -> (String -> Int -> m a)+  -> (String -> Int -> m b)+  -> AlloySig+  -> m (Set (a, b))+getDoubleAs s f g inst = do+  set <- getDouble s inst+  traverseSet applyMapping set+  where+    applyMapping (x, y) = (,)+      <$> specifyObject f x+      <*> specifyObject g y++{-# DEPRECATED getDouble "use the typed version getDoubleAs instead" #-}+{-| Retrieve a binary relation of objects of a given 'AlloySig'. Successful if the signature's relation is binary (or empty). -}@@ -71,6 +172,31 @@ getDouble = lookupRel double  {-|+Retrieve a ternary relation of values of a given 'AlloySig'.+Values will be created by applying the given mapping functions from object Name+and 'Int' to the value.+The mapping has to be injective (for all expected cases).+Successful if the signature's relation is ternary (or empty).+-}+getTripleAs+  :: (IsString s, MonadError s m, Ord a, Ord b, Ord c)+  => String+  -> (String -> Int -> m a)+  -> (String -> Int -> m b)+  -> (String -> Int -> m c)+  -> AlloySig+  -> m (Set (a, b, c))+getTripleAs s f g h inst = do+  set <- getTriple s inst+  traverseSet applyMapping set+  where+    applyMapping (x, y, z) = (,,)+      <$> specifyObject f x+      <*> specifyObject g y+      <*> specifyObject h z++{-# DEPRECATED getTriple "use the typed version getTripleAs instead" #-}+{-| Retrieve a ternary relation of objects of a given 'AlloySig'. Successful if the signature's relation is ternary (or empty). -}@@ -81,16 +207,15 @@   -> m (Set (Object, Object, Object)) getTriple = lookupRel triple +{-|+Transforms a relation into a Mapping.+-} binaryToMap :: (Ord k, Ord v) => Set (k, v) -> Map k (Set v) binaryToMap bin = M.fromList   [(fst (head gs), S.fromList $ snd <$> gs)   | gs <- groupBy ((==) `on` fst) $ S.toList bin] -{-|-Transforms a relation into a Mapping.-Is only successful (i.e. returns 'return') if the given transformation function is-able to map the given values injectively.--}+{-# DEPRECATED relToMap "use binaryToMap instead" #-} relToMap   :: (IsString s, MonadError s m, Ord k, Ord v)   => (a -> (k, v))@@ -128,6 +253,7 @@     ++ " is missing in the Alloy instance"   Just e   -> return e +{-# DEPRECATED objectName "use the typed versions of get... e.g. getSingleAs instead of getSingle" #-} {-| Retrieve an object's name. -}
+ src/Language/Alloy/Internal/Call.hs view
@@ -0,0 +1,350 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-|+Module      : Language.Alloy.Internal.Call+Copyright   : (c) Marcellus Siegburg, 2019 - 2021+License     : MIT++This module provides the basic internal functionality to retrieve the raw+results from calling Alloy.+It provides data types and functions to interact with Alloy.+-}+module Language.Alloy.Internal.Call (+  CallAlloyConfig (maxInstances, noOverflow, timeout),+  defaultCallAlloyConfig,+  getRawInstances,+  getRawInstancesWith,+  ) where++import qualified Data.ByteString                  as BS+  (hGetLine, intercalate, isSuffixOf, writeFile)+import qualified Data.ByteString.Char8            as BS (unlines)++import Control.Concurrent (+  ThreadId,+  forkIO, killThread, newEmptyMVar, putMVar, takeMVar, threadDelay,+  )+import Control.Exception                (IOException)+import Control.Monad                    (unless, void, when)+import Data.ByteString                  (ByteString)+import Data.ByteString.Char8            (unpack)+import Data.Hashable                    (hash)+import Data.IORef                       (IORef, newIORef, readIORef)+import Data.List.Split                  (splitOn)+import Data.Maybe                       (fromMaybe)+import System.Directory+  (XdgDirectory (..), createDirectory, doesFileExist, doesDirectoryExist,+   getTemporaryDirectory, getXdgDirectory)+import System.Directory.Internal        (setFileMode)+import System.Directory.Internal.Prelude+  (catch, isDoesNotExistError)+import System.Exit                      (ExitCode (..))+import System.FilePath+  ((</>), (<.>), searchPathSeparator, takeDirectory)+import System.IO+  (BufferMode (..), Handle, hClose, hFlush, hIsEOF, hPutStr, hSetBuffering)+import System.IO.Unsafe                 (unsafePerformIO)+import System.Process (+  CreateProcess (..), StdStream (..), ProcessHandle,+  createProcess, proc, terminateProcess, waitForProcess,+  )+#if defined(mingw32_HOST_OS)+import System.Win32.Info                (getUserName)+#else+import System.Posix.User                (getLoginName)+#endif++import Language.Alloy.RessourceNames (+  alloyJarName, className, classPackage, commonsCliJarName+  )+import Language.Alloy.Ressources        (alloyJar, classFile, commonsCliJar)++{-|+Configuration for calling alloy. These are:++ * maximal number of instances to retrieve ('Nothing' for all)+ * whether to not overflow when calculating numbers within Alloy+ * an timeout after which to forcibly kill Alloy+   (retrieving only instances that were returned before killing the process)+-}+data CallAlloyConfig = CallAlloyConfig {+  -- | maximal number of instances to retrieve ('Nothing' for all)+  maxInstances :: Maybe Integer,+  -- | whether to not overflow when calculating numbers within Alloy+  noOverflow   :: Bool,+  -- | the time in microseconds after which to forcibly kill Alloy+  --   ('Nothing' for never)+  timeout      :: Maybe Int+  }++{-|+Default configuration for calling Alloy. Defaults to:++ * retrieve all instances+ * do not overflow+-}+defaultCallAlloyConfig :: CallAlloyConfig+defaultCallAlloyConfig = CallAlloyConfig {+  maxInstances = Nothing,+  noOverflow   = True,+  timeout      = Nothing+  }++{-# NOINLINE mclassPath #-}+{-|+'IORef' for storing the class path.+-}+mclassPath :: IORef (Maybe FilePath)+mclassPath = unsafePerformIO (newIORef Nothing)++{-|+This function may be used to get all raw model instances for a given Alloy+specification. It calls Alloy via a Java interface and splits the raw instance+answers before returning the resulting list of raw instances.+-}+getRawInstances+  :: Maybe Integer+  -- ^ How many instances to return; 'Nothing' for all.+  -> String+  -- ^ The Alloy specification which should be loaded.+  -> IO [ByteString]+getRawInstances maxIs = getRawInstancesWith defaultCallAlloyConfig {+  maxInstances = maxIs+  }++{-|+This function may be used to get all raw model instances for a given Alloy+specification. It calls Alloy via a Java interface and splits the raw instance+answers before returning the resulting list of raw instances.+Parameters are set using a 'CallAlloyConfig'.+-}+getRawInstancesWith+  :: CallAlloyConfig+  -- ^ The configuration to be used.+  -> String+  -- ^ The Alloy specification which should be loaded.+  -> IO [ByteString]+getRawInstancesWith config content = do+  classPath <- getClassPath+  let callAlloy = proc "java"+        $ ["-cp", classPath, classPackage ++ '.' : className,+           "-i", show $ fromMaybe (-1) $ maxInstances config]+        ++ ["-o" | not $ noOverflow config]+  (Just hin, Just hout, Just herr, ph) <-+    createProcess callAlloy {+        std_out = CreatePipe,+        std_in  = CreatePipe,+        std_err = CreatePipe+      }+  pout <- listenForOutput hout+  perr <- listenForOutput herr+#ifndef mingw32_HOST_OS+  hSetBuffering hin NoBuffering+#endif+  hPutStr hin content+  hFlush hin+  hClose hin+  maybe (return ()) (void . startTimeout hin hout herr ph) $ timeout config+  out <- getOutput pout+  err <- getOutput perr+  printContentOnError ph+  unless (null err) $ fail $ unpack $ BS.unlines err+  let instas = fmap (BS.intercalate "\n") $ drop 1 $ splitOn [begin] out+  return $ filterLast (not . (partialInstance `BS.isSuffixOf`)) instas+  where+    begin :: ByteString+    begin = "---INSTANCE---"+    partialInstance :: ByteString+    partialInstance = "---PARTIAL_INSTANCE---"+    filterLast _ []     = []+    filterLast p x@[_]  = filter p x+    filterLast p (x:xs) = x:filterLast p xs+    getWholeOutput h = do+      eof <- hIsEOF h+      if eof+        then return []+      else catch+        ((:) <$> BS.hGetLine h <*> getWholeOutput h)+        (\(_ :: IOException) -> return [partialInstance])+    printContentOnError ph = do+      code <- waitForProcess ph+      when (code == ExitFailure 1)+        $ putStrLn $ "Failed parsing the Alloy code:\n" <> content+    listenForOutput h = do+      mvar <- newEmptyMVar+      pid <- forkIO $ getWholeOutput h >>= putMVar mvar+      return (pid, mvar)+    getOutput (pid, mvar) = do+      output <- takeMVar mvar+      killThread pid+      return output++{-|+Start a new process that aborts execution by closing all handles and+killing the processes after the given amount of time.+-}+startTimeout+  :: Handle+  -- ^ the input handle to close+  -> Handle+  -- ^ the output handle to close+  -> Handle+  -- ^ the error handle to close+  -> ProcessHandle+  -- ^ the main process handle+  -> Int -> IO ThreadId+startTimeout i o e ph t = forkIO $ do+  threadDelay t+  void $ forkIO $ hClose e+  void $ forkIO $ hClose o+  terminateProcess ph+  hClose i++{-|+Check if the class path was determined already, if so use it, otherwise call+'readClassPath'.++Returns the class path.+-}+getClassPath :: IO FilePath+getClassPath = do+  mclassPath' <- readIORef mclassPath+  maybe readClassPath return mclassPath'++{-+{-+data CallAlloyConfig = Config {+    alloyJarFile   :: FilePath,+    alloyClassFile :: FilePath,+    keepFiles      :: Bool+  }+-}++jarFileEnv :: String+jarFileEnv = "ALLOY_JAR_FILE"++callAlloyEnv :: String+callAlloyEnv = "CALL_ALLOY_CLASS_FILE"++keepFilesEnv :: String+keepFilesEnv = "KEEP_ALLOY_FILES"++{-|+Lookup environment variables which are to prefer if present.+-}+getEnvironmentInformation :: IO CallAlloyConfig+getEnvironmentInformation = do+  alloy     <- lookupEnv jarFileEnv+  callAlloy <- lookupEnv callAlloyEnv+  keep      <- lookupEnv keepFilesEnv+  let mconfig = Config <$> alloy <*> callAlloy <*> pure (isJust keep)+  case mconfig of+    Nothing -> do+      dataDir <- getXdgDirectory XdgData $ appName </> "dataDir"+    Just c  -> return c+-}+{-+getVersionFile :: IO FilePath+getVersionFile = do+  configDir <- getXdgDirectory XdgConfig appName+  let versionFile = configDir </> "version"+  exists <- doesFileExist versionFile+  if exists+    then do+    version <- read <$> readFile versionFile+    unless (version == versionHash) $ createVersionFile configDir versionFile+    else createVersionFile configDir versionFile+-}++fallbackToTempDir :: IO FilePath -> IO FilePath+fallbackToTempDir m = catch m $ \e ->+  if isDoesNotExistError e+  then do+    tmp    <- getTemporaryDirectory+#if defined(mingw32_HOST_OS)+    login  <- getUserName+#else+    login  <- getLoginName+#endif+    let tmpDir = tmp </> show (hash login) </> appName+    createUserDirectoriesIfMissing tmpDir+    return tmpDir+  else error $ show e++{-|+Read the class path version specified in the user directory, if it is not+current or if it does not exist, call 'createVersionFile'.++Returns the class path.+-}+readClassPath :: IO FilePath+readClassPath = do+  configDir <- fallbackToTempDir $ getXdgDirectory XdgConfig appName+  let versionFile = configDir </> "version"+  exists <- doesFileExist versionFile+  if exists+    then do+    version <- read <$> readFile versionFile+    unless (version == versionHash) $ createVersionFile configDir versionFile+    else createVersionFile configDir versionFile+  dataDir <- getXdgDirectory XdgData $ appName </> "dataDir"+  return $ dataDir ++ searchPathSeparator : dataDir </> alloyJarName+    ++ searchPathSeparator : dataDir </> commonsCliJarName++{-|+Create all library files within the users 'XdgDirectory' by calling+'createDataDir' then place the current version number into a configuration File.+-}+createVersionFile :: FilePath -> FilePath -> IO ()+createVersionFile configDir versionFile = do+  createDataDir+  createUserDirectoriesIfMissing configDir+  writeFile versionFile $ show versionHash++{-|+Create all library files within the users 'XdgDirectory' based on the source+files enclosed into this library (see also 'Language.Alloy.RessourceNames' and+'Language.Alloy.Ressources').+-}+createDataDir :: IO ()+createDataDir = do+  dataDir <- fallbackToTempDir $ getXdgDirectory XdgData $ appName </> "dataDir"+  createUserDirectoriesIfMissing $ dataDir </> classPackage+  BS.writeFile (dataDir </> classPackage </> className <.> "class") classFile+  BS.writeFile (dataDir </> alloyJarName) alloyJar+  BS.writeFile (dataDir </> commonsCliJarName) commonsCliJar++{-|+Creates user directories using the file permissions 700.+This function creates the specified directory and all its parent directories as+well (if they are also missing).+-}+createUserDirectoriesIfMissing :: FilePath -> IO ()+createUserDirectoriesIfMissing fp = do+  isDir <- doesDirectoryExist fp+  let parent = takeDirectory fp+  unless (isDir || parent == fp) $ do+    createUserDirectoriesIfMissing parent+    createDirectory fp+#ifndef mingw32_HOST_OS+    setFileMode fp (7*8*8)+#endif++{-|+The application name (used to store data in a specific directory.+-}+appName :: String+appName = "call-alloy"++{-# INLINE versionHash #-}+{-|+Used to determine possible source code and Alloy version changes across multiple+versions of this library.+-}+versionHash :: Int+versionHash = hash $ alloyHash + commonsCliHash + classFileHash+  where+    alloyHash = hash alloyJar+    commonsCliHash = hash commonsCliJar+    classFileHash = hash classFile