futhark-0.17.3: src/Futhark/Util.hs
{-# LANGUAGE Trustworthy #-}
-- | Non-Futhark-specific utilities. If you find yourself writing
-- general functions on generic data structures, consider putting them
-- here.
--
-- Sometimes it is also preferable to copy a small function rather
-- than introducing a large dependency. In this case, make sure to
-- note where you got it from (and make sure that the license is
-- compatible).
module Futhark.Util
( mapAccumLM,
maxinum,
chunk,
chunks,
dropAt,
takeLast,
dropLast,
mapEither,
maybeNth,
maybeHead,
splitFromEnd,
splitAt3,
focusNth,
unixEnvironment,
isEnvVarSet,
fancyTerminal,
runProgramWithExitCode,
directoryContents,
roundFloat,
ceilFloat,
floorFloat,
roundDouble,
ceilDouble,
floorDouble,
lgamma,
lgammaf,
tgamma,
tgammaf,
fromPOSIX,
toPOSIX,
trim,
pmapIO,
UserString,
EncodedString,
zEncodeString,
)
where
import Control.Concurrent
import Control.Exception
import Control.Monad
import qualified Data.ByteString as BS
import Data.Char
import Data.Either
import Data.List (foldl', genericDrop, genericSplitAt)
import Data.Maybe
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.Text.Encoding.Error as T
import Numeric
import qualified System.Directory.Tree as Dir
import System.Environment
import System.Exit
import qualified System.FilePath as Native
import qualified System.FilePath.Posix as Posix
import System.IO (hIsTerminalDevice, stdout)
import System.IO.Unsafe
import System.Process.ByteString
-- | Like 'Data.Traversable.mapAccumL', but monadic.
mapAccumLM ::
Monad m =>
(acc -> x -> m (acc, y)) ->
acc ->
[x] ->
m (acc, [y])
mapAccumLM _ acc [] = return (acc, [])
mapAccumLM f acc (x : xs) = do
(acc', x') <- f acc x
(acc'', xs') <- mapAccumLM f acc' xs
return (acc'', x' : xs')
-- | @chunk n a@ splits @a@ into @n@-size-chunks. If the length of
-- @a@ is not divisible by @n@, the last chunk will have fewer than
-- @n@ elements (but it will never be empty).
chunk :: Int -> [a] -> [[a]]
chunk _ [] = []
chunk n xs =
let (bef, aft) = splitAt n xs
in bef : chunk n aft
-- | @chunks ns a@ splits @a@ into chunks determined by the elements
-- of @ns@. It must hold that @sum ns == length a@, or the resulting
-- list may contain too few chunks, or not all elements of @a@.
chunks :: [Int] -> [a] -> [[a]]
chunks [] _ = []
chunks (n : ns) xs =
let (bef, aft) = splitAt n xs
in bef : chunks ns aft
-- | Like 'maximum', but returns zero for an empty list.
maxinum :: (Num a, Ord a, Foldable f) => f a -> a
maxinum = foldl' max 0
-- | @dropAt i n@ drops @n@ elements starting at element @i@.
dropAt :: Int -> Int -> [a] -> [a]
dropAt i n xs = take i xs ++ drop (i + n) xs
-- | @takeLast n l@ takes the last @n@ elements of @l@.
takeLast :: Int -> [a] -> [a]
takeLast n = reverse . take n . reverse
-- | @dropLast n l@ drops the last @n@ elements of @l@.
dropLast :: Int -> [a] -> [a]
dropLast n = reverse . drop n . reverse
-- | A combination of 'map' and 'partitionEithers'.
mapEither :: (a -> Either b c) -> [a] -> ([b], [c])
mapEither f l = partitionEithers $ map f l
-- | Return the list element at the given index, if the index is valid.
maybeNth :: Integral int => int -> [a] -> Maybe a
maybeNth i l
| i >= 0, v : _ <- genericDrop i l = Just v
| otherwise = Nothing
-- | Return the first element of the list, if it exists.
maybeHead :: [a] -> Maybe a
maybeHead [] = Nothing
maybeHead (x : _) = Just x
-- | Like 'splitAt', but from the end.
splitFromEnd :: Int -> [a] -> ([a], [a])
splitFromEnd i l = splitAt (length l - i) l
-- | Like 'splitAt', but produces three lists.
splitAt3 :: Int -> Int -> [a] -> ([a], [a], [a])
splitAt3 n m l =
let (xs, l') = splitAt n l
(ys, zs) = splitAt m l'
in (xs, ys, zs)
-- | Return the list element at the given index, if the index is
-- valid, along with the elements before and after.
focusNth :: Integral int => int -> [a] -> Maybe ([a], a, [a])
focusNth i xs
| (bef, x : aft) <- genericSplitAt i xs = Just (bef, x, aft)
| otherwise = Nothing
{-# NOINLINE unixEnvironment #-}
-- | The Unix environment when the Futhark compiler started.
unixEnvironment :: [(String, String)]
unixEnvironment = unsafePerformIO getEnvironment
-- | Is an environment variable set to 0 or 1? If 0, return False; if
-- 1, True; otherwise the default value.
isEnvVarSet :: String -> Bool -> Bool
isEnvVarSet name default_val = fromMaybe default_val $ do
val <- lookup name unixEnvironment
case val of
"0" -> return False
"1" -> return True
_ -> Nothing
{-# NOINLINE fancyTerminal #-}
-- | Are we running in a terminal capable of fancy commands and
-- visualisation?
fancyTerminal :: Bool
fancyTerminal = unsafePerformIO $ do
isTTY <- hIsTerminalDevice stdout
isDumb <- (Just "dumb" ==) <$> lookupEnv "TERM"
return $ isTTY && not isDumb
-- | Like 'readProcessWithExitCode', but also wraps exceptions when
-- the indicated binary cannot be launched, or some other exception is
-- thrown. Also does shenanigans to handle improperly encoded outputs.
runProgramWithExitCode ::
FilePath ->
[String] ->
BS.ByteString ->
IO (Either IOException (ExitCode, String, String))
runProgramWithExitCode exe args inp =
(Right . postprocess <$> readProcessWithExitCode exe args inp)
`catch` \e -> return (Left e)
where
decode = T.unpack . T.decodeUtf8With T.lenientDecode
postprocess (code, out, err) =
(code, decode out, decode err)
-- | Every non-directory file contained in a directory tree.
directoryContents :: FilePath -> IO [FilePath]
directoryContents dir = do
_ Dir.:/ tree <- Dir.readDirectoryWith return dir
case Dir.failures tree of
Dir.Failed _ err : _ -> throw err
_ -> return $ mapMaybe isFile $ Dir.flattenDir tree
where
isFile (Dir.File _ path) = Just path
isFile _ = Nothing
foreign import ccall "nearbyint" c_nearbyint :: Double -> Double
foreign import ccall "nearbyintf" c_nearbyintf :: Float -> Float
foreign import ccall "ceil" c_ceil :: Double -> Double
foreign import ccall "ceilf" c_ceilf :: Float -> Float
foreign import ccall "floor" c_floor :: Double -> Double
foreign import ccall "floorf" c_floorf :: Float -> Float
-- | Round a single-precision floating point number correctly.
roundFloat :: Float -> Float
roundFloat = c_nearbyintf
-- | Round a single-precision floating point number upwards correctly.
ceilFloat :: Float -> Float
ceilFloat = c_ceilf
-- | Round a single-precision floating point number downwards correctly.
floorFloat :: Float -> Float
floorFloat = c_floorf
-- | Round a double-precision floating point number correctly.
roundDouble :: Double -> Double
roundDouble = c_nearbyint
-- | Round a double-precision floating point number upwards correctly.
ceilDouble :: Double -> Double
ceilDouble = c_ceil
-- | Round a double-precision floating point number downwards correctly.
floorDouble :: Double -> Double
floorDouble = c_floor
foreign import ccall "lgamma" c_lgamma :: Double -> Double
foreign import ccall "lgammaf" c_lgammaf :: Float -> Float
foreign import ccall "tgamma" c_tgamma :: Double -> Double
foreign import ccall "tgammaf" c_tgammaf :: Float -> Float
-- | The system-level @lgamma()@ function.
lgamma :: Double -> Double
lgamma = c_lgamma
-- | The system-level @lgammaf()@ function.
lgammaf :: Float -> Float
lgammaf = c_lgammaf
-- | The system-level @tgamma()@ function.
tgamma :: Double -> Double
tgamma = c_tgamma
-- | The system-level @tgammaf()@ function.
tgammaf :: Float -> Float
tgammaf = c_tgammaf
-- | Turn a POSIX filepath into a filepath for the native system.
toPOSIX :: Native.FilePath -> Posix.FilePath
toPOSIX = Posix.joinPath . Native.splitDirectories
-- | Some bad operating systems do not use forward slash as
-- directory separator - this is where we convert Futhark includes
-- (which always use forward slash) to native paths.
fromPOSIX :: Posix.FilePath -> Native.FilePath
fromPOSIX = Native.joinPath . Posix.splitDirectories
-- | Remove leading and trailing whitespace from a string. Not an
-- efficient implementation!
trim :: String -> String
trim = reverse . dropWhile isSpace . reverse . dropWhile isSpace
-- | Run various 'IO' actions concurrently, possibly with a bound on
-- the number of threads. The list must be finite. The ordering of
-- the result list is not deterministic - add your own sorting if
-- needed. If any of the actions throw an exception, then that
-- exception is propagated to this function.
pmapIO :: Maybe Int -> (a -> IO b) -> [a] -> IO [b]
pmapIO concurrency f elems = do
tasks <- newMVar elems
results <- newEmptyMVar
num_threads <- maybe getNumCapabilities pure concurrency
replicateM_ num_threads $ forkIO $ worker tasks results
replicateM (length elems) $ getResult results
where
worker tasks results = do
task <- modifyMVar tasks getTask
case task of
Nothing -> pure ()
Just x -> do
y <- (Right <$> f x) `catch` (pure . Left)
putMVar results y
worker tasks results
getTask [] = pure ([], Nothing)
getTask (task : tasks) = pure (tasks, Just task)
getResult results = do
res <- takeMVar results
case res of
Left err -> throw (err :: SomeException)
Right v -> pure v
-- Z-encoding from https://ghc.haskell.org/trac/ghc/wiki/Commentary/Compiler/SymbolNames
--
-- Slightly simplified as we do not need it to deal with tuples and
-- the like.
--
-- (c) The University of Glasgow, 1997-2006
-- | As the user typed it.
type UserString = String
-- | Encoded form.
type EncodedString = String
-- | Z-encode a string using a slightly simplified variant of GHC
-- Z-encoding. The encoded string is a valid identifier in most
-- programming languages.
zEncodeString :: UserString -> EncodedString
zEncodeString "" = ""
zEncodeString (c : cs) = encodeDigitChar c ++ concatMap encodeChar cs
unencodedChar :: Char -> Bool -- True for chars that don't need encoding
unencodedChar 'Z' = False
unencodedChar 'z' = False
unencodedChar '_' = True
unencodedChar c =
isAsciiLower c
|| isAsciiUpper c
|| isDigit c
-- If a digit is at the start of a symbol then we need to encode it.
-- Otherwise names like 9pH-0.1 give linker errors.
encodeDigitChar :: Char -> EncodedString
encodeDigitChar c
| isDigit c = encodeAsUnicodeCharar c
| otherwise = encodeChar c
encodeChar :: Char -> EncodedString
encodeChar c | unencodedChar c = [c] -- Common case first
-- Constructors
encodeChar '(' = "ZL" -- Needed for things like (,), and (->)
encodeChar ')' = "ZR" -- For symmetry with (
encodeChar '[' = "ZM"
encodeChar ']' = "ZN"
encodeChar ':' = "ZC"
encodeChar 'Z' = "ZZ"
-- Variables
encodeChar 'z' = "zz"
encodeChar '&' = "za"
encodeChar '|' = "zb"
encodeChar '^' = "zc"
encodeChar '$' = "zd"
encodeChar '=' = "ze"
encodeChar '>' = "zg"
encodeChar '#' = "zh"
encodeChar '.' = "zi"
encodeChar '<' = "zl"
encodeChar '-' = "zm"
encodeChar '!' = "zn"
encodeChar '+' = "zp"
encodeChar '\'' = "zq"
encodeChar '\\' = "zr"
encodeChar '/' = "zs"
encodeChar '*' = "zt"
encodeChar '_' = "zu"
encodeChar '%' = "zv"
encodeChar c = encodeAsUnicodeCharar c
encodeAsUnicodeCharar :: Char -> EncodedString
encodeAsUnicodeCharar c =
'z' :
if isDigit (head hex_str)
then hex_str
else '0' : hex_str
where
hex_str = showHex (ord c) "U"