packages feed

streamly-core 0.3.1 → 0.3.2

raw patch · 31 files changed

+407/−156 lines, 31 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

+ Streamly.Internal.Data.IORef: mkWeakIORef :: IORef a -> IO () -> IO (Weak (IORef a))
+ Streamly.Internal.Data.IORef: touchIORef :: IORef a -> IO ()
- Streamly.Internal.Data.Unfold: before :: (a -> m c) -> Unfold m a b -> Unfold m a b
+ Streamly.Internal.Data.Unfold: before :: Monad m => (a -> m c) -> Unfold m a b -> Unfold m a b

Files

Changelog.md view
@@ -1,5 +1,41 @@ # Changelog +## 0.3.2 (Oct 2026)++### Bug Fixes++* On POSIX platforms, the `Path` based APIs that open an existing file in+  `WriteMode`, e.g. `write` and `withFile` in `Streamly.FileSystem.FileIO`,+  did not truncate it, the old contents beyond the newly written data+  remained. The file descriptor leaked when the file could not be opened as+  a Handle, e.g. if it was a directory.+* Fix `timer`, `timesWith`, `absTimesWith` and `relTimesWith` stopping after a+  major GC.+* Fix `enumerateFrom` and `enumerateFromTo` in Stream and Unfold not+  terminating for bounded integral types when the limit is `maxBound`.+* Fix `Unfold.enumerateFromTo` for `Identity` calling `enumerateFromThen`.+* Fix `Stream.splitEndBySeq` and `splitEndBySeq_` dropping the last segment+  for separators longer than a machine word.+* `chunksOf` in the array modules no longer emits a trailing empty array+  when the stream length is a multiple of the chunk size, or when the stream+  is empty.+* Fix `classify` and `classifyIO` in Fold and Scanl, and `Fold.classifyScan`,+  restarting the fold for a key that terminated on its first input.+* Fix `Scanl.uniqBy` failing before the first input.+* Fix `Stream.postscanl` emitting an output for a scan that terminates+  without consuming any input.+* Fix `Stream.isInfixOf` returning `False` for an empty infix.+* Fix `RingArray.getIndex`, `cast`, `fold`, `eqArray` and `eqArrayN` for+  out-of-range indices, misaligned heads, empty rings and array slices.+* Fix deserialization reading beyond the end of a corrupt or truncated buffer+  for lists and for derived `Serialize` instances.++### Internal API Bug Fixes++* Fix memory corruption in `MutArray.appendMax`, `MutArray.createMinOf` and+  `RingArray.Generic.copyToMutArray`, and resource release in+  `Fold.bracketIO` and `Fold.finallyIO`.+ ## 0.3.1 (May 2026)  * Fixed `Path.fromString` truncation when unicode chars are present.
configure view
@@ -1,6 +1,6 @@ #! /bin/sh # Guess values for system-dependent variables and create Makefiles.-# Generated by GNU Autoconf 2.72 for streamly-core 0.3.1.+# Generated by GNU Autoconf 2.72 for streamly-core 0.3.2. # # Report bugs to <streamly@composewell.com>. #@@ -603,8 +603,8 @@ # Identity of this package. PACKAGE_NAME='streamly-core' PACKAGE_TARNAME='streamly-core'-PACKAGE_VERSION='0.3.1'-PACKAGE_STRING='streamly-core 0.3.1'+PACKAGE_VERSION='0.3.2'+PACKAGE_STRING='streamly-core 0.3.2' PACKAGE_BUGREPORT='streamly@composewell.com' PACKAGE_URL='https://streamly.composewell.com' @@ -1249,7 +1249,7 @@   # Omit some internal or obsolete options to make the list less imposing.   # This message is too long to be a string in the A/UX 3.1 sh.   cat <<_ACEOF-'configure' configures streamly-core 0.3.1 to adapt to many kinds of systems.+'configure' configures streamly-core 0.3.2 to adapt to many kinds of systems.  Usage: $0 [OPTION]... [VAR=VALUE]... @@ -1311,7 +1311,7 @@  if test -n "$ac_init_help"; then   case $ac_init_help in-     short | recursive ) echo "Configuration of streamly-core 0.3.1:";;+     short | recursive ) echo "Configuration of streamly-core 0.3.2:";;    esac   cat <<\_ACEOF @@ -1397,7 +1397,7 @@ test -n "$ac_init_help" && exit $ac_status if $ac_init_version; then   cat <<\_ACEOF-streamly-core configure 0.3.1+streamly-core configure 0.3.2 generated by GNU Autoconf 2.72  Copyright (C) 2023 Free Software Foundation, Inc.@@ -1621,7 +1621,7 @@ This file contains any messages produced by compilers while running configure, to aid debugging if configure makes a mistake. -It was created by streamly-core $as_me 0.3.1, which was+It was created by streamly-core $as_me 0.3.2, which was generated by GNU Autoconf 2.72.  Invocation command line was    $ $0$ac_configure_args_raw@@ -3881,7 +3881,7 @@ # report actual input values of CONFIG_FILES etc. instead of their # values after options handling. ac_log="-This file was extended by streamly-core $as_me 0.3.1, which was+This file was extended by streamly-core $as_me 0.3.2, which was generated by GNU Autoconf 2.72.  Invocation command line was    CONFIG_FILES    = $CONFIG_FILES@@ -3937,7 +3937,7 @@ cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 ac_cs_config='$ac_cs_config_escaped' ac_cs_version="\\-streamly-core config.status 0.3.1+streamly-core config.status 0.3.2 configured by $0, generated by GNU Autoconf 2.72,   with options \\"\$ac_cs_config\\" 
configure.ac view
@@ -3,7 +3,7 @@ # See https://www.gnu.org/software/autoconf/manual/autoconf.html for help on # the macros used in this file. -AC_INIT([streamly-core], [0.3.1], [streamly@composewell.com], [streamly-core], [https://streamly.composewell.com])+AC_INIT([streamly-core], [0.3.2], [streamly@composewell.com], [streamly-core], [https://streamly.composewell.com])  # To suppress "WARNING: unrecognized options: --with-compiler" AC_ARG_WITH([compiler], [GHC])
docs/Changelog.md view
@@ -1,5 +1,41 @@ # Changelog +## 0.3.2 (Oct 2026)++### Bug Fixes++* On POSIX platforms, the `Path` based APIs that open an existing file in+  `WriteMode`, e.g. `write` and `withFile` in `Streamly.FileSystem.FileIO`,+  did not truncate it, the old contents beyond the newly written data+  remained. The file descriptor leaked when the file could not be opened as+  a Handle, e.g. if it was a directory.+* Fix `timer`, `timesWith`, `absTimesWith` and `relTimesWith` stopping after a+  major GC.+* Fix `enumerateFrom` and `enumerateFromTo` in Stream and Unfold not+  terminating for bounded integral types when the limit is `maxBound`.+* Fix `Unfold.enumerateFromTo` for `Identity` calling `enumerateFromThen`.+* Fix `Stream.splitEndBySeq` and `splitEndBySeq_` dropping the last segment+  for separators longer than a machine word.+* `chunksOf` in the array modules no longer emits a trailing empty array+  when the stream length is a multiple of the chunk size, or when the stream+  is empty.+* Fix `classify` and `classifyIO` in Fold and Scanl, and `Fold.classifyScan`,+  restarting the fold for a key that terminated on its first input.+* Fix `Scanl.uniqBy` failing before the first input.+* Fix `Stream.postscanl` emitting an output for a scan that terminates+  without consuming any input.+* Fix `Stream.isInfixOf` returning `False` for an empty infix.+* Fix `RingArray.getIndex`, `cast`, `fold`, `eqArray` and `eqArrayN` for+  out-of-range indices, misaligned heads, empty rings and array slices.+* Fix deserialization reading beyond the end of a corrupt or truncated buffer+  for lists and for derived `Serialize` instances.++### Internal API Bug Fixes++* Fix memory corruption in `MutArray.appendMax`, `MutArray.createMinOf` and+  `RingArray.Generic.copyToMutArray`, and resource release in+  `Fold.bracketIO` and `Fold.finallyIO`.+ ## 0.3.1 (May 2026)  * Fixed `Path.fromString` truncation when unicode chars are present.
src/Streamly/Internal/Data/Array/Type.hs view
@@ -641,7 +641,7 @@ unsnoc arr =     if null arr     then Nothing-    else Just (unsafeTail arr, unsafeGetIndexRev 0 arr)+    else Just (unsafeInit arr, unsafeGetIndexRev 0 arr)  ------------------------------------------------------------------------------- -- Streams of arrays
src/Streamly/Internal/Data/Fold/Combinators.hs view
@@ -1780,7 +1780,7 @@ -- >>> ref <- newIORef [Fold.take 2 Fold.sum, Fold.take 2 Fold.length :: Fold IO Int Int] -- >>> gen = atomicModifyIORef ref (\xs -> ([], xs)) -- >>> Stream.toList $ Stream.scanl (Fold.distributeScan gen) (Stream.enumerateFromTo 1 10)--- [[],[],[],[2,3],[],[],[],[],[],[],[]]+-- [[],[],[3,2],[],[],[],[],[],[],[],[]] -- {-# INLINE distributeScan #-} distributeScan :: Monad m => m [Fold m a b] -> Scanl m a [b]@@ -1796,7 +1796,11 @@         case res of             Partial fs -> do               r <- step fs a-              run (Tuple' (Fold step (return r) extr fin : ys) zs) xs a+              case r of+                  Partial fs1 ->+                      let fld = Fold step (return (Partial fs1)) extr fin+                       in run (Tuple' (fld : ys) zs) xs a+                  Done b -> run (Tuple' ys (b : zs)) xs a             Done b -> do               run (Tuple' ys (b : zs)) xs a @@ -2129,7 +2133,7 @@ {-# INLINE sampleFromthen #-} sampleFromthen :: Monad m => Int -> Int -> Fold m a b -> Fold m a b sampleFromthen offset size =-    with indexed filter (\(i, _) -> (i + offset) `mod` size == 0)+    with indexed filter (\(i, _) -> i >= offset && (i - offset) `mod` size == 0)  ------------------------------------------------------------------------------ -- Nesting
src/Streamly/Internal/Data/Fold/Container.hs view
@@ -929,7 +929,7 @@                           Partial s1 ->                             Tuple3' (IsMap.mapInsert k s1 kv) set Nothing                           Done b ->-                            Tuple3' kv set (Just (k, b))+                            Tuple3' kv (Set.insert k set) (Just (k, b))               Done b -> return (Tuple3' kv (Set.insert k set) (Just (k, b)))      step (Tuple3' kv set _) a = do@@ -1034,7 +1034,7 @@                           Partial s1 ->                             Tuple3' (IsMap.mapInsert k s1 kv) set Nothing                           Done b ->-                            Tuple3' kv set (Just (k, b))+                            Tuple3' kv (Set.insert k set) (Just (k, b))               Done b -> return (Tuple3' kv (Set.insert k set) (Just (k, b)))      step (Tuple3' kv set _) a = do@@ -1124,7 +1124,7 @@                         ref <- liftIO $ newIORef s1                         return $ Tuple3' (IsMap.mapInsert k ref kv) set Nothing                       Done b ->-                        return $ Tuple3' kv set (Just (k, b))+                        return $ Tuple3' kv (Set.insert k set) (Just (k, b))               Done b -> return (Tuple3' kv (Set.insert k set) (Just (k, b)))      step (Tuple3' kv set _) a = do@@ -1240,7 +1240,7 @@                         ref <- liftIO $ newIORef s1                         return $ Tuple3' (IsMap.mapInsert k ref kv) set Nothing                       Done b ->-                        return $ Tuple3' kv set (Just (k, b))+                        return $ Tuple3' kv (Set.insert k set) (Just (k, b))               Done b -> return (Tuple3' kv (Set.insert k set) (Just (k, b)))      step (Tuple3' kv set _) a = do
src/Streamly/Internal/Data/Fold/Exception.hs view
@@ -138,9 +138,14 @@         res <- initial1         case res of             Partial s -> do-                s1 <- step1 s a `MC.onException` liftIO (runIOFinalizer ref)-                let fld1 = Fold step1 (pure s1) extract1 final1-                pure $ Partial $ Tuple' ref fld1+                r <- step1 s a `MC.onException` liftIO (runIOFinalizer ref)+                case r of+                    Partial s1 -> do+                        let fld1 = Fold step1 (pure (Partial s1)) extract1 final1+                        pure $ Partial $ Tuple' ref fld1+                    Done b -> do+                        liftIO $ runIOFinalizer ref+                        pure $ Done b             Done b -> do                 liftIO $ runIOFinalizer ref                 pure $ Done b@@ -156,9 +161,11 @@         case res of             Partial s -> do                 val <- final1 s `MC.onException` liftIO (runIOFinalizer ref)-                runIOFinalizer ref+                liftIO $ runIOFinalizer ref                 pure val-            Done b -> pure b+            Done b -> do+                liftIO $ runIOFinalizer ref+                pure b  -- | Run a side effect whenever the fold stops normally, aborts due to an -- exception or is garbage collected.@@ -173,15 +180,19 @@     initial = do         ref <- liftIO $ newIOFinalizer aft         res <- initial1 `MC.onException` liftIO (runIOFinalizer ref)-        pure $ case res of-            Done b -> Done b-            Partial s -> Partial $ Tuple' ref s+        case res of+            Done b -> do+                liftIO $ runIOFinalizer ref+                pure $ Done b+            Partial s -> pure $ Partial $ Tuple' ref s      step (Tuple' ref s) a = do         res <- step1 s a `MC.onException` liftIO (runIOFinalizer ref)-        pure $ case res of-            Done b -> Done b-            Partial s1 -> Partial $ Tuple' ref s1+        case res of+            Done b -> do+                liftIO $ runIOFinalizer ref+                pure $ Done b+            Partial s1 -> pure $ Partial $ Tuple' ref s1      extract (Tuple' ref s) =         extract1 s `MC.onException` liftIO (runIOFinalizer ref)
src/Streamly/Internal/Data/Fold/Type.hs view
@@ -2435,7 +2435,8 @@             Partial s -> cextract s             Done b -> return b --- | Extract the output of a fold and refold it using a 'Refold'.+-- | Finalize a fold without any input and use its output as the initial+-- value of a 'Refold'. -- -- A fusible alternative to 'concatMap'. --@@ -2443,7 +2444,7 @@ {-# INLINE refold #-} refold :: Monad m => Refold m b a c -> Fold m a b -> Fold m a c refold (Refold step inject extract) f =-    Fold step (extractM f >>= inject) extract extract+    Fold step (finalM f >>= inject) extract extract  ------------------------------------------------------------------------------ -- morphInner
src/Streamly/Internal/Data/IORef.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE UnboxedTuples #-}+ -- | -- Module      : Streamly.Internal.Data.IORef -- Copyright   : (c) 2019 Composewell Technologies@@ -25,6 +27,8 @@      -- Construction     , newIORef+    , mkWeakIORef+    , touchIORef      -- Write     , writeIORef@@ -45,7 +49,10 @@ import Data.Kind (Type) #endif import Data.Proxy (Proxy(..))-import Streamly.Internal.Data.MutByteArray.Type (MutByteArray)+import GHC.Exts (mkWeak#)+import GHC.IO (IO(..))+import GHC.Weak (Weak(..))+import Streamly.Internal.Data.MutByteArray.Type (MutByteArray(..)) import Streamly.Internal.Data.Unbox (Unbox(..), sizeOf)  import qualified Streamly.Internal.Data.MutByteArray.Type as MBA@@ -66,6 +73,23 @@     var <- MBA.new (sizeOf (Proxy :: Proxy a))     pokeAt 0 var x     return $ IORef var++-- | Make a 'Weak' pointer to an 'IORef', with a finalizer that runs when the+-- 'IORef' is no longer reachable. The key is the underlying mutable byte+-- array, not the 'IORef' box, which the compiler may unbox and discard.+--+-- /Pre-release/+mkWeakIORef :: IORef a -> IO () -> IO (Weak (IORef a))+mkWeakIORef ref@(IORef (MutByteArray arr#)) (IO finalizer) = IO $ \s ->+    case mkWeak# arr# ref finalizer s of+        (# s1, w #) -> (# s1, Weak w #)++-- | Keep the 'IORef' reachable at least up to this point, see 'mkWeakIORef'.+--+-- /Pre-release/+{-# INLINE touchIORef #-}+touchIORef :: IORef a -> IO ()+touchIORef (IORef var) = MBA.touch var  -- | Write a value to an 'IORef'. --
src/Streamly/Internal/Data/MutArray/Generic.hs view
@@ -802,7 +802,10 @@                 return $ D.Skip (GroupBuffer s contents start end bound)             D.Stop ->                 return-                    $ D.Skip (GroupYield contents start end bound GroupFinish)+                    $ if end == start+                      then D.Stop+                      else D.Skip+                            (GroupYield contents start end bound GroupFinish)      step' _ (GroupYield contents start end bound next) =          return $ D.Yield (MutArray contents start end bound) next@@ -873,8 +876,9 @@ clone src = do     let len = length src     dst <- new len-    unsafePutSlice src 0 dst 0 len-    return dst+    let dst1 = dst { arrEnd = arrStart dst + len }+    unsafePutSlice src 0 dst1 0 len+    return dst1  ------------------------------------------------------------------------------- -- Size
src/Streamly/Internal/Data/MutArray/Type.hs view
@@ -1567,7 +1567,9 @@         r <- stepi defState st         case r of             D.Yield i s -> do-                x <- liftio $ getIndex i (MutArray contents start end undefined)+                -- We do not need arrBound but we cannot leave it undefined+                -- because it is a strict field.+                x <- liftio $ getIndex i (MutArray contents start end end)                 case x of                     Just v -> return $ D.Yield v (GetIndicesState contents start end s)                     Nothing -> error "Invalid Index"@@ -1916,7 +1918,10 @@                 return $ D.Skip (GroupBuffer s contents start end bound)             D.Stop ->                 return-                    $ D.Skip (GroupYield contents start end bound GroupFinish)+                    $ if end == start+                      then D.Stop+                      else D.Skip+                            (GroupYield contents start end bound GroupFinish)      step' _ (GroupYield contents start end bound next) =         return $ D.Yield (MutArray contents start end bound) next@@ -2392,8 +2397,9 @@     where      free_ = arrBound - arrEnd-    needed = n * SIZE_OF(a)-    bound = arrBound + needed - free_+    -- Keep "needed" to be at least free_ otherwise when recompute the bound in+    -- "final" we may lose some reserve space.+    needed = max (n * SIZE_OF(a)) free_      initial = do         assert (n >= 0) (return ())@@ -2408,8 +2414,9 @@         liftIO $ pokeAt end contents x         return $ ArrayUnsafe contents start (INDEX_NEXT(end,a)) +    -- If the array gets reallocated the original bound does not remain valid.     final (ArrayUnsafe contents start end) =-        MutArray contents start end bound+        MutArray contents start end (start + arrEnd - arrStart + needed)  {-# DEPRECATED writeAppendNUnsafe "Please use unsafeAppendN instead." #-} {-# INLINE writeAppendNUnsafe #-}@@ -2779,7 +2786,7 @@     step arr@(MutArray _ start end bound) x         | INDEX_NEXT(end,a) > bound = do         let oldSize = end - start-            newSize = max (oldSize * 2) 1+            newSize = max (oldSize * 2) (SIZE_OF(a))         arr1 <- liftIO $ reallocExplicitAs ps (SIZE_OF(a)) newSize arr         unsafeSnoc arr1 x     step arr x = unsafeSnoc arr x
src/Streamly/Internal/Data/MutByteArray/Type.hs view
@@ -360,16 +360,12 @@     :: MutByteArray -> Int -> MutByteArray -> Int -> Int -> IO Int unsafeByteCmp     (MutByteArray marr1) (I# st1#) (MutByteArray marr2) (I# st2#) (I# len#) =+    -- compareByteArrays# must be evaluated here, a lazy result would read the+    -- arrays after later mutations.     IO $ \s# ->-        let res =-                I#-                    (compareByteArrays#-                         (unsafeCoerce# marr1)-                         st1#-                         (unsafeCoerce# marr2)-                         st2#-                         len#)-         in (# s#, res #)+        case compareByteArrays#+                (unsafeCoerce# marr1) st1# (unsafeCoerce# marr2) st2# len# of+            res# -> (# s#, I# res# #)  ------------------------------------------------------------------------------- -- Pinning & Unpinning
src/Streamly/Internal/Data/RingArray.hs view
@@ -416,13 +416,14 @@  -- | /O(1)/ Lookup the element at the given index relative to the ring head. -- Index starts from 0, could be positive or negative. Returns Nothing if the--- index is more than or equal to the size of the ring.+-- index is more than or equal to the length of the ring, or less than the+-- negated length. -- {-# INLINE getIndex #-} getIndex :: forall m a. (MonadIO m, Unbox a) => Int -> RingArray a -> m (Maybe a) getIndex i ring =-    let rs = ringSize ring-     in if i < rs && i > -rs+    let len = length ring+     in if i < len && i >= -len         then Just <$> unsafeGetIndex i ring         else return Nothing @@ -623,15 +624,17 @@ asBytes = unsafeCast  -- | Cast a ring having elements of type @a@ into a ring having elements of--- type @b@. The length of the ring should be a multiple of the size of the--- target element otherwise 'Nothing' is returned.+-- type @b@. The byte length of the ring and the byte offset of the ring head+-- must be multiples of the size of the target element otherwise 'Nothing' is+-- returned. -- {-# INLINE cast #-} cast :: forall a b. (Unbox b) => RingArray a -> Maybe (RingArray b) cast ring =     let len = byteLength ring         r = len `mod` SIZE_OF(b)-     in if r /= 0+        rh = ringHead ring `mod` SIZE_OF(b)+     in if r /= 0 || rh /= 0         then Nothing         else Just $ unsafeCast ring @@ -653,14 +656,15 @@     | nBytes <= p1Len = do           part1 <-               MutByteArray.unsafeByteCmp-                  arrContents 0 ringContents ringHead nBytes+                  arrContents arrStart ringContents ringHead nBytes           pure $ part1 == 0     | otherwise = do           part1 <-               MutByteArray.unsafeByteCmp-                  arrContents 0 ringContents ringHead p1Len+                  arrContents arrStart ringContents ringHead p1Len           part2 <--              MutByteArray.unsafeByteCmp arrContents p1Len ringContents 0 p2Len+              MutByteArray.unsafeByteCmp+                  arrContents (arrStart + p1Len) ringContents 0 p2Len           pure $ part1 == 0 && part2 == 0     where     p1Len = ringSize - ringHead@@ -680,10 +684,10 @@     | otherwise = do           part1 <-               MutByteArray.unsafeByteCmp-                  arrContents 0 ringContents ringHead p1Len+                  arrContents arrStart ringContents ringHead p1Len           part2 <-               MutByteArray.unsafeByteCmp-                  arrContents p1Len ringContents 0 p2Len+                  arrContents (arrStart + p1Len) ringContents 0 p2Len           pure $ part1 == 0 && part2 == 0     where     p1Len = ringSize - ringHead@@ -707,7 +711,10 @@ fold (Fold step initial _ final) rb = do     res <- initial     case res of-        Fold.Partial fs -> go SPEC rh fs+        Fold.Partial fs ->+            if ringSize rb == 0+            then final fs+            else go SPEC rh fs         Fold.Done b -> return b      where@@ -767,8 +774,6 @@             go acc1 (index + SIZE_OF(a))  -- | Fold the entire length of a ring buffer starting at the current ring head.------ Note, this will crash on ring of 0 size. -- {-# INLINE foldlM' #-} foldlM' :: forall m a b. (MonadIO m, Unbox a)
src/Streamly/Internal/Data/RingArray/Generic.hs view
@@ -165,8 +165,12 @@             end = idx + len         arr <- MutArray.emptyOf len         arr1 <- MutArray.uninit arr len-        MutArray.unsafePutSlice ringArr idx arr1 0 (ringMax - idx)-        MutArray.unsafePutSlice ringArr 0 arr1 (ringMax - idx) (end - ringMax)+        if end <= ringMax+        then MutArray.unsafePutSlice ringArr idx arr1 0 len+        else do+            MutArray.unsafePutSlice ringArr idx arr1 0 (ringMax - idx)+            MutArray.unsafePutSlice+                ringArr 0 arr1 (ringMax - idx) (end - ringMax)         return arr1  -- This would be theoretically slower than toMutArray because of a branch
src/Streamly/Internal/Data/Scanl/Combinators.hs view
@@ -640,14 +640,16 @@ -- {-# INLINE uniqBy #-} uniqBy :: Monad m => (a -> a -> Bool) -> Scanl m a (Maybe a)-uniqBy eq = rollingMap f+uniqBy eq = fmap (\(Tuple' _ x) -> x) $ mkScanl step (Tuple' Nothing Nothing)      where -    f pre curr =-        case pre of-            Nothing -> Just curr-            Just x -> if x `eq` curr then Nothing else Just curr+    step (Tuple' pre _) curr =+        let out =+                case pre of+                    Just x | x `eq` curr -> Nothing+                    _ -> Just curr+         in Tuple' (Just curr) out  -- | See 'uniqBy'. --@@ -2110,7 +2112,7 @@ {-# INLINE sampleFromthen #-} sampleFromthen :: Monad m => Int -> Int -> Scanl m a b -> Scanl m a b sampleFromthen offset size =-    with indexed filter (\(i, _) -> (i + offset) `mod` size == 0)+    with indexed filter (\(i, _) -> i >= offset && (i - offset) `mod` size == 0)  ------------------------------------------------------------------------------ -- Nesting
src/Streamly/Internal/Data/Scanl/Container.hs view
@@ -600,7 +600,7 @@                     return                         $ Tuple3' (IsMap.mapInsert k s1 kv) set (Just (k, b))                   Done b ->-                    return $ Tuple3' kv set (Just (k, b))+                    return $ Tuple3' kv (Set.insert k set) (Just (k, b))               Done b -> return (Tuple3' kv (Set.insert k set) (Just (k, b)))      step (Tuple3' kv set _) a = do@@ -685,7 +685,7 @@                             $ Tuple3'                                 (IsMap.mapInsert k ref kv) set (Just (k, b))                       Done b ->-                        return $ Tuple3' kv set (Just (k, b))+                        return $ Tuple3' kv (Set.insert k set) (Just (k, b))               Done b -> return (Tuple3' kv (Set.insert k set) (Just (k, b)))      step (Tuple3' kv set _) a = do
src/Streamly/Internal/Data/Serialize/TH.hs view
@@ -201,7 +201,15 @@                      $(varE _initialOffset)                      $(varE _arr)                      $(varE _endOffset)-             $(multiIfE (map (guardCon conLen off1) cons ++ [catchAll]))|]+             -- xorCmp reads the constructor name without bounds checks, so+             -- check that the name fits in the buffer before calling it.+             if $(varE off1) + w8_int $(varE conLen) > $(varE _endOffset)+             then error+                 ("deserializeAt: constructor name at offset = "+                     ++ show $(varE off1)+                     ++ " exceeds the buffer length = "+                     ++ show $(varE _endOffset))+             else $(multiIfE (map (guardCon conLen off1) cons ++ [catchAll]))|]      catchAll =         normalGE
src/Streamly/Internal/Data/Serialize/TH/RecHeader.hs view
@@ -395,7 +395,11 @@              deserializeAt $(varE initialOff) $(varE _arr) $(varE endOff)          ($(varP hOff), hlen1 :: Word32) <-              deserializeAt hlenOff $(varE _arr) $(varE endOff)-         if (hlen1 == $(litIntegral hlen)) && $(xorCmp hval hOff _arr)+         -- xorCmp reads the header without bounds checks, so check that the+         -- header fits in the buffer before calling it.+         if (hlen1 == $(litIntegral hlen))+                && $(varE hOff) + $(litIntegral hlen) <= $(varE endOff)+                && $(xorCmp hval hOff _arr)          then do              let $(varP (makeI 0)) =                      $(varE initialOff) +
src/Streamly/Internal/Data/Serialize/Type.hs view
@@ -251,6 +251,8 @@     {-# INLINABLE deserializeAt #-}     deserializeAt off arr sz = do         (off1, len64) <- deserializeAt off arr sz :: IO (Int, Int64)+        when (len64 < 0) $+            error $ "deserializeAt: negative list length " ++ show len64         let len = (fromIntegral :: Int64 -> Int) len64             peekList f o i | i >= 3 = do               -- Unfold the loop three times
src/Streamly/Internal/Data/Stream/Eliminate.hs view
@@ -100,12 +100,13 @@  {-# INLINE_NORMAL foldr1 #-} foldr1 :: Monad m => (a -> a -> a) -> Stream m a -> m (Maybe a)-foldr1 f m = do-     r <- uncons m-     case r of-         Nothing   -> return Nothing-         Just (h, t) -> fmap Just (foldr f h t)+foldr1 f = foldr step Nothing +    where++    step x Nothing = Just x+    step x (Just acc) = Just (f x acc)+ ------------------------------------------------------------------------------ -- Parsers ------------------------------------------------------------------------------@@ -583,9 +584,16 @@     => Stream m a -> Stream m a -> m Bool isInfixOf infx stream = do     arr <- fold Array.create infx-    -- XXX can use breakOnSeq instead (when available)-    r <- null $ StreamD.drop 1 $ Nesting.splitSepBySeq_ arr Fold.drain stream-    return (not r)+    -- An empty infix occurs in every stream. The split below cannot detect+    -- it in a stream of fewer than two elements, which has fewer than two+    -- segments.+    if Array.length arr == 0+    then return True+    else do+        -- XXX can use breakOnSeq instead (when available)+        r <- null+            $ StreamD.drop 1 $ Nesting.splitSepBySeq_ arr Fold.drain stream+        return (not r)  -- Note: isPrefixOf uses the prefix stream only once. In contrast, isSuffixOf -- may use the suffix stream many times. To run in optimal memory we do not
src/Streamly/Internal/Data/Stream/Generate.hs view
@@ -500,7 +500,7 @@ {-# INLINE enumerateFromToIntegral #-} enumerateFromToIntegral :: (Monad m, Integral a) => a -> a -> Stream m a enumerateFromToIntegral from to =-    takeWhile (<= to) $ enumerateFromStepIntegral from 1+    takeWhile (<= to) $ takeEndBy (== to) $ enumerateFromStepIntegral from 1  -- | Enumerate an 'Integral' type. @enumerateFromIntegral from@ generates a -- stream whose first element is @from@ and the successive elements are in
src/Streamly/Internal/Data/Stream/Nesting.hs view
@@ -3542,11 +3542,8 @@                             let jump c = SplitOnSuffixSeqKRInit c s mba                             yieldReinit jump b                 Skip s -> go SPEC offset s fs-                Stop -> do-                    -- do not issue a blank segment when we end at pattern-                    if offset == maxOffset && A.byteEq arr patArr-                    then final fs >> return Stop-                    else if withSep+                Stop ->+                    if withSep                     then do                         r <- final fs                         skip $ SplitOnSuffixSeqYield r SplitOnSuffixSeqDone
src/Streamly/Internal/Data/Stream/Transform.hs view
@@ -502,17 +502,24 @@ -- Unfortunately, we cannot define lazy scans because the Partial constructor -- itself is strict. --- | Postscan a stream using the given fold. A postscan omits the initial--- (default) value of the accumulator and includes the final value.+-- | Postscan a stream using the given scan. A postscan emits one output per+-- input element, omitting the initial value of the accumulator. Equivalently,+-- @postscanl s = 'Data.List.drop' 1 . 'scanl' s@. -- -- >>> Stream.toList $ Stream.postscanl Scanl.latest (Stream.fromList []) -- [] ----- Compare with 'scan' which includes the initial value as well:+-- Compare with 'scanl' which additionally emits the initial value: -- -- >>> Stream.toList $ Stream.scanl Scanl.latest (Stream.fromList []) -- [Nothing] --+-- A scan that terminates at the initial step (e.g. @Scanl.take 0@) consumes no+-- input, so the postscan is empty:+--+-- >>> Stream.toList $ Stream.postscanl (Scanl.take 0 Scanl.toList) (Stream.fromList [1,2,3::Int])+-- []+-- -- The following example extracts the input stream up to a point where the -- running average of elements is no more than 10: --@@ -540,7 +547,12 @@         return             $ case res of                   FL.Partial fs -> Skip $ ScanDo st fs-                  FL.Done b -> Yield b ScanDone+                  -- A scan that is Done at the initial step has consumed no+                  -- input, so a postscan (one output per consumed input) emits+                  -- nothing. This keeps the invariant+                  -- @postscanl s = drop 1 . scanl s@: the initial value is+                  -- always dropped, whether it comes from a Partial or a Done.+                  FL.Done _ -> Stop     step gst (ScanDo st fs) = do         res <- sstep (adaptState gst) st         case res of
src/Streamly/Internal/Data/Stream/Type.hs view
@@ -1015,6 +1015,8 @@     showsPrec p dl = showParen (p > 10) $         showString "fromList " . shows (GHC.Exts.toList dl) +    show dl = "fromList " ++ show (GHC.Exts.toList dl)+ instance Read a => Read (Stream Identity a) where     readPrec = parens $ prec 10 $ do         Ident "fromList" <- lexP@@ -2422,7 +2424,7 @@ {-# ANN type CIterState Fuse #-} data CIterState s f fs b     = CIterInit s f-    | CIterConsume s fs+    | CIterConsume s fs Bool     | CIterYield b (CIterState s f fs b)     | CIterStop @@ -2444,20 +2446,27 @@         return             $ Skip             $ case res of-                  FL.Partial fs -> CIterConsume st fs+                  FL.Partial fs -> CIterConsume st fs False                   FL.Done fb -> CIterYield fb $ CIterInit st (return fb)      {-# INLINE_LATE stepOuter #-}     stepOuter _ (CIterInit st action) = do         iterStep st (action >>= finject)-    stepOuter gst (CIterConsume st fs) = do+    stepOuter gst (CIterConsume st fs consumed) = do         r <- step (adaptState gst) st         case r of-            Yield x s -> iterStep s (fstep fs x)-            Skip s -> return $ Skip $ CIterConsume s fs-            Stop -> do-                b <- fextract fs-                return $ Skip $ CIterYield b CIterStop+            Yield x s -> do+                res <- fstep fs x+                return $ Skip $ case res of+                    FL.Partial fs1 -> CIterConsume s fs1 True+                    FL.Done fb -> CIterYield fb $ CIterInit s (return fb)+            Skip s -> return $ Skip $ CIterConsume s fs consumed+            Stop ->+                if consumed+                then do+                    b <- fextract fs+                    return $ Skip $ CIterYield b CIterStop+                else return Stop     stepOuter _ (CIterYield a next) = return $ Yield a next     stepOuter _ CIterStop = return Stop @@ -2598,6 +2607,9 @@ -- Do not use automatic derivation for this to show as "fromList" rather than -- "fromList Identity". instance Show a => Show (Nested Identity a) where+    {-# INLINE showsPrec #-}+    showsPrec p (Nested xs) = showsPrec p xs+     {-# INLINE show #-}     show (Nested xs) = show xs 
src/Streamly/Internal/Data/StreamK/Type.hs view
@@ -1193,6 +1193,8 @@     showsPrec p dl = showParen (p > 10) $         showString "fromList " . shows (toList dl) +    show dl = "fromList " ++ show (toList dl)+ instance Read a => Read (StreamK Identity a) where     readPrec = parens $ prec 10 $ do         Ident "fromList" <- lexP@@ -2802,6 +2804,9 @@ -- Do not use automatic derivation for this to show as "fromList" rather than -- "fromList Identity". instance Show a => Show (Nested Identity a) where+    {-# INLINE showsPrec #-}+    showsPrec p (Nested xs) = showsPrec p xs+     {-# INLINE show #-}     show (Nested xs) = show xs 
src/Streamly/Internal/Data/Time/Clock.hs view
@@ -26,12 +26,12 @@     ) where -import Control.Concurrent (threadDelay, ThreadId)+import Control.Concurrent (forkIO, threadDelay, ThreadId) import Control.Concurrent.MVar (MVar, newEmptyMVar, takeMVar, tryPutMVar)-import Control.Monad (forever, when, void)+import Control.Monad (when, void) import Streamly.Internal.Data.Time.Units     (MicroSecond64(..), fromAbsTime, addToAbsTime, toRelTime)-import Streamly.Internal.Control.ForkIO (forkIOManaged)+import System.Mem.Weak (Weak, deRefWeak)  import qualified Streamly.Internal.Data.IORef as Unboxed @@ -47,12 +47,16 @@     t <- fromAbsTime <$> getTime clock     Unboxed.modifyIORef' timeVar (const t) +-- Returns False if the IORef is no longer reachable. {-# INLINE updateWithDelay #-} updateWithDelay :: RealFrac a =>-    Clock -> a -> Unboxed.IORef MicroSecond64 -> IO ()-updateWithDelay clock precision timeVar = do+    Clock -> a -> Weak (Unboxed.IORef MicroSecond64) -> IO Bool+updateWithDelay clock precision weakVar = do     threadDelay (delayTime precision)-    updateTimeVar clock timeVar+    r <- deRefWeak weakVar+    case r of+        Nothing -> return False+        Just timeVar -> updateTimeVar clock timeVar >> return True      where @@ -69,8 +73,8 @@  -- | @asyncClock g@ starts a clock thread that updates an IORef with current -- time as a 64-bit value in microseconds, every 'g' seconds. The IORef can be--- read asynchronously.  The thread exits automatically when the reference to--- the returned 'ThreadId' is lost.+-- read asynchronously.  The thread exits automatically when the IORef is no+-- longer reachable. -- -- Minimum granularity of clock update is 1 ms. Higher is better for -- performance.@@ -82,9 +86,19 @@ asyncClock clock g = do     timeVar <- Unboxed.newIORef 0     updateTimeVar clock timeVar-    tid <- forkIOManaged $ forever (updateWithDelay clock g timeVar)+    -- The thread holds only a weak reference to timeVar. The consumers may+    -- drop the returned ThreadId, so a finalizer on the ThreadId may kill the+    -- thread at the next GC while timeVar is still being read.+    weakVar <- Unboxed.mkWeakIORef timeVar (return ())+    tid <- forkIO $ loop weakVar     return (tid, timeVar) +    where++    loop weakVar = do+        continue <- updateWithDelay clock g weakVar+        when continue $ loop weakVar+ {-# INLINE readClock #-} readClock :: (ThreadId, Unboxed.IORef MicroSecond64) -> IO MicroSecond64 readClock (_, timeVar) = Unboxed.readIORef timeVar@@ -94,7 +108,7 @@ ------------------------------------------------------------------------------  -- | Adjustable periodic timer.-data Timer = Timer ThreadId (MVar ()) (IO ())+data Timer = Timer ThreadId (MVar ()) (Unboxed.IORef MicroSecond64) (IO ())  -- Set the expiry to current time + timer period {-# INLINE resetTimerExpiry #-}@@ -104,18 +118,29 @@     let t1 = addToAbsTime t (toRelTime period)     Unboxed.modifyIORef' timeVar (const (fromAbsTime t1)) +-- Returns False if the IORef is no longer reachable. {-# INLINE processTimerTick #-} processTimerTick :: RealFrac a =>-    Clock -> a -> Unboxed.IORef MicroSecond64 -> MVar () -> IO () -> IO ()-processTimerTick clock precision timeVar mvar reset = do+       Clock+    -> a+    -> MicroSecond64+    -> Weak (Unboxed.IORef MicroSecond64)+    -> MVar ()+    -> IO Bool+processTimerTick clock precision period weakVar mvar = do     threadDelay (delayTime precision)-    t <- fromAbsTime <$> getTime clock-    expiry <- Unboxed.readIORef timeVar-    when (t >= expiry) $ do-        -- non-blocking put so that we can process multiple timers in a-        -- non-blocking manner in future.-        void $ tryPutMVar mvar ()-        reset+    r <- deRefWeak weakVar+    case r of+        Nothing -> return False+        Just timeVar -> do+            t <- fromAbsTime <$> getTime clock+            expiry <- Unboxed.readIORef timeVar+            when (t >= expiry) $ do+                -- non-blocking put so that we can process multiple timers in+                -- a non-blocking manner in future.+                void $ tryPutMVar mvar ()+                resetTimerExpiry clock period timeVar+            return True      where @@ -142,21 +167,33 @@     timeVar <- Unboxed.newIORef 0     let p = round (period * 1e6) :: Int         p1 = fromIntegral p :: MicroSecond64-        reset = resetTimerExpiry clock p1 timeVar-        process = processTimerTick clock g timeVar mvar reset-    reset-    tid <- forkIOManaged $ forever process-    return $ Timer tid mvar reset+    resetTimerExpiry clock p1 timeVar+    -- The thread holds only a weak reference to timeVar, and exits when the+    -- Timer is no longer reachable. It holds mvar strongly, otherwise a thread+    -- blocked in waitTimer would be unreachable and get+    -- BlockedIndefinitelyOnMVar.+    weakVar <- Unboxed.mkWeakIORef timeVar (return ())+    tid <- forkIO $ loop p1 weakVar mvar+    return $ Timer tid mvar timeVar (resetTimerExpiry clock p1 timeVar) +    where++    loop p1 weakVar mvar = do+        continue <- processTimerTick clock g p1 weakVar mvar+        when continue $ loop p1 weakVar mvar+ -- | Blocking wait for a timer tick. {-# INLINE waitTimer #-} waitTimer :: Timer -> IO ()-waitTimer (Timer _ mvar _) = takeMVar mvar+waitTimer (Timer _ mvar timeVar _) = do+    takeMVar mvar+    -- Keep timeVar reachable, so that the timer thread keeps running+    Unboxed.touchIORef timeVar  -- | Resets the current period. {-# INLINE resetTimer #-} resetTimer :: Timer -> IO ()-resetTimer (Timer _ _ reset) = reset+resetTimer (Timer _ _ _ reset) = reset  -- | Elongates the current period by specified amount. --
src/Streamly/Internal/Data/Unfold.hs view
@@ -853,8 +853,8 @@ -- -- /Pre-release/ {-# INLINE_NORMAL before #-}-before :: (a -> m c) -> Unfold m a b -> Unfold m a b-before action (Unfold step inject) = Unfold step (action >> inject)+before :: Monad m => (a -> m c) -> Unfold m a b -> Unfold m a b+before action (Unfold step inject) = Unfold step (\a -> action a >> inject a)  -- The custom implementation of "after_" is slightly faster (5-7%) than -- "_after".  This is just to document and make sure that we can always use
src/Streamly/Internal/Data/Unfold/Enumeration.hs view
@@ -201,12 +201,27 @@ enumerateFromThenIntegral =     lmap (\(from, next) -> (from, next - from)) enumerateFromStepIntegral -{-# INLINE enumerateFromToIntegral #-}+data EnumToState a = EnumToYield !a !a | EnumToStop++-- Stops after yielding "to" instead of computing "to + 1", which overflows+-- when "to" is maxBound.+{-# INLINE_NORMAL enumerateFromToIntegral #-} enumerateFromToIntegral :: (Monad m, Integral a) => Unfold m (a, a) a-enumerateFromToIntegral =-    takeWhileMWithInput (\(_, to) b -> return $ b <= to)-        $ lmap (\(from, _) -> (from, 1)) enumerateFromStepIntegral+enumerateFromToIntegral = Unfold step inject +    where++    inject (from, to) =+        return $ if from > to then EnumToStop else EnumToYield from to++    {-# INLINE_LATE step #-}+    step (EnumToYield x to) =+        return+            $ if x == to+              then Yield x EnumToStop+              else Yield x (EnumToYield (x + 1) to)+    step EnumToStop = return Stop+ {-# INLINE enumerateFromThenToIntegral #-} enumerateFromThenToIntegral :: (Monad m, Integral a) => Unfold m (a, a, a) a enumerateFromThenToIntegral =@@ -533,7 +548,7 @@         map Identity $ lmap (bimap runIdentity runIdentity) enumerateFromThen     {-# INLINE enumerateFromTo #-}     enumerateFromTo  =-        map Identity $ lmap (bimap runIdentity runIdentity) enumerateFromThen+        map Identity $ lmap (bimap runIdentity runIdentity) enumerateFromTo     {-# INLINE enumerateFromThenTo #-}     enumerateFromThenTo  =         map Identity $
src/Streamly/Internal/FileSystem/Posix/File.hsc view
@@ -80,18 +80,24 @@ -- Imports ------------------------------------------------------------------------------- +import Control.Exception (onException)+import Control.Monad (when) import Data.Bits ((.|.), (.&.), complement) import Foreign.C.Error (throwErrnoIfMinus1_) import Foreign.C.String (CString) import Foreign.C.Types (CInt(..))-import GHC.IO.Handle.FD (fdToHandle)+import GHC.IO.Device (IODeviceType(..))+import GHC.IO.Handle.FD (mkHandleFromFD) import Streamly.Internal.FileSystem.Posix.Errno (throwErrnoPathIfMinus1Retry) import Streamly.Internal.FileSystem.PosixPath (PosixPath) import System.IO (IOMode(..), Handle) import System.Posix.Types (Fd(..), CMode(..)) +import qualified GHC.IO.Device as Device+import qualified GHC.IO.FD as FD import qualified Streamly.Internal.FileSystem.File.Common as File import qualified Streamly.Internal.FileSystem.PosixPath as Path+import qualified System.Posix.Internals as Posix  -- We want to remain close to the Posix C API. A function based API to set and -- clear the modes is simple, type safe and directly mirrors the C API. It does@@ -253,33 +259,26 @@         $ openAtCString fdMay cstr flags cmode  --- | Open a regular file, return an Fd.+-- | The open flags and the create mode for opening a regular file in the given+-- 'IOMode'. -- -- Sets O_NOCTTY, O_NONBLOCK flags to be compatible with the base openFile -- behavior. O_NOCTTY affects opening of terminal special files and O_NONBLOCK -- affects fifo special files, and mandatory locking. ---openFileFdWith :: OpenFlags -> PosixPath -> IOMode -> IO Fd-openFileFdWith oflags path iomode = do+openFileFlags :: OpenFlags -> IOMode -> (OpenFlags, Maybe FileMode)+openFileFlags oflags iomode =     case iomode of-        ReadMode -> open1 (setReadOnly oflags1) Nothing-        WriteMode ->-            open1 (setWriteOnly oflags1) (Just defaultCreateMode)+        ReadMode -> (setReadOnly oflags1, Nothing)+        WriteMode -> (setWriteOnly oflags1, Just defaultCreateMode)         AppendMode ->-            open1-                ((setAppend True . setWriteOnly) oflags1)-                (Just defaultCreateMode)-        ReadWriteMode ->-            open1 (setReadWrite oflags) (Just defaultCreateMode)+            ((setAppend True . setWriteOnly) oflags1, Just defaultCreateMode)+        ReadWriteMode -> (setReadWrite oflags1, Just defaultCreateMode)      where      oflags1 = setNoCtty True $ setNonBlock True oflags-    open1 = openAt Nothing path -openFileFd :: PosixPath -> IOMode -> IO Fd-openFileFd = openFileFdWith defaultOpenFlags- foreign import ccall unsafe "unistd.h close"    c_close :: CInt -> IO CInt @@ -290,11 +289,33 @@ -- base openFile compatible, Handle returning, APIs ------------------------------------------------------------------------------- --- | Open a regular file, return a Handle. The file is locked, the Handle is--- NOT set up to close the file on garbage collection.-{-# INLINE openFileHandle #-}+-- | Like openFile in base. open() can be slow, e.g. on NFS or FUSE file+-- systems, or block, so use an interruptible foreign call for it.+interruptibleOpen :: CString -> CInt -> CMode -> IO CInt+##if MIN_VERSION_base(4,16,0)+interruptibleOpen = Posix.c_interruptible_open+##else+interruptibleOpen = Posix.c_safe_open+##endif++-- | Open a file and return a Handle in binary mode. Like openFile in base,+-- the file is locked and a file opened in 'WriteMode' is truncated. openFileHandle :: PosixPath -> IOMode -> IO Handle-openFileHandle p x = openFileFd p x >>= fdToHandle . fromIntegral+openFileHandle path iomode = do+    let (flags, cmode) = openFileFlags defaultOpenFlags iomode+        OpenFlags flags1 = maybe flags (\_ -> setCreat True flags) cmode+        FileMode mode = maybe defaultCreateMode id cmode+    fd <- Path.asCString path $ \cstr ->+        throwErrnoPathIfMinus1Retry "openFile" path+            $ interruptibleOpen cstr flags1 mode+    (fD, fdType) <-+        FD.mkFD fd iomode Nothing False True `onException` c_close fd+    -- Like base, truncate after locking the file, ftruncate fails on special+    -- files like /dev/null.+    when (iomode == WriteMode && fdType == RegularFile)+        $ Device.setSize fD 0 `onException` Device.close fD+    mkHandleFromFD fD fdType (Path.toString path) iomode False Nothing+        `onException` Device.close fD  -- | Like openFile in base package but using Path instead of FilePath. -- Use hSetBinaryMode on the handle if you want to use binary mode.
streamly-core.cabal view
@@ -1,6 +1,6 @@ cabal-version:      2.4 name:               streamly-core-version:            0.3.1+version:            0.3.2 synopsis:           Streaming, parsers, arrays, serialization and more description:   For upgrading to streamly-0.9.0+ please read the