Skip to content

Commit 0bd68ca

Browse files
authored
Check the no-empty-chunks invariant in CI (#565)
* Thoroughly check the no-empty-chunks invariant in CI Fixes #564. * Avoid even temporary empty chunks in lazy foldl1 * Fiddle with formatting and documentation This also removes deriving Typeable, which does nothing since ghc-7.10. * Re-insert reference to invariant-checking functions
1 parent 50b07d9 commit 0bd68ca

File tree

6 files changed

+57
-22
lines changed

6 files changed

+57
-22
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ jobs:
159159
dist-newstyle
160160
key: ${{ runner.os }}-latest
161161
- name: Test
162-
run: cabal test --ghc-options='-fcheck-prim-bounds -fno-ignore-asserts'
162+
run: cabal test --ghc-options='-fcheck-prim-bounds -fno-ignore-asserts -DHS_BYTESTRING_ASSERTIONS'
163163

164164
old-gcc:
165165
needs: build

Data/ByteString/Internal/Type.hs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{-# LANGUAGE CPP, ForeignFunctionInterface, BangPatterns #-}
22
{-# LANGUAGE UnliftedFFITypes, MagicHash,
3-
UnboxedTuples, DeriveDataTypeable #-}
3+
UnboxedTuples #-}
44
{-# LANGUAGE TupleSections #-}
55
{-# LANGUAGE TypeFamilies #-}
66
{-# LANGUAGE PatternSynonyms, ViewPatterns #-}
@@ -135,7 +135,6 @@ import Data.Bits ((.&.))
135135
import Data.Char (ord)
136136
import Data.Word
137137

138-
import Data.Typeable (Typeable)
139138
import Data.Data (Data(..), mkNoRepType)
140139

141140
import GHC.Base (nullAddr#,realWorld#,unsafeChr)
@@ -242,7 +241,6 @@ pokeFpByteOff fp off val = unsafeWithForeignPtr fp $ \p ->
242241
data ByteString = BS {-# UNPACK #-} !(ForeignPtr Word8) -- payload
243242
{-# UNPACK #-} !Int -- length
244243
-- ^ @since 0.11.0.0
245-
deriving (Typeable)
246244

247245
-- | Type synonym for the strict flavour of 'ByteString'.
248246
--

Data/ByteString/Lazy.hs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -495,12 +495,22 @@ foldr' f a = go
495495
-- argument, and thus must be applied to non-empty 'ByteString's.
496496
foldl1 :: HasCallStack => (Word8 -> Word8 -> Word8) -> ByteString -> Word8
497497
foldl1 _ Empty = errorEmptyList "foldl1"
498-
foldl1 f (Chunk c cs) = foldl f (S.unsafeHead c) (Chunk (S.unsafeTail c) cs)
498+
foldl1 f (Chunk c cs) = go (S.unsafeHead c) (S.unsafeTail c) cs
499+
where
500+
go v x xs = let v' = S.foldl f v x
501+
in case xs of
502+
Empty -> v'
503+
Chunk x' xs' -> go v' x' xs'
499504

500505
-- | 'foldl1'' is like 'foldl1', but strict in the accumulator.
501506
foldl1' :: HasCallStack => (Word8 -> Word8 -> Word8) -> ByteString -> Word8
502507
foldl1' _ Empty = errorEmptyList "foldl1'"
503-
foldl1' f (Chunk c cs) = foldl' f (S.unsafeHead c) (Chunk (S.unsafeTail c) cs)
508+
foldl1' f (Chunk c cs) = go (S.unsafeHead c) (S.unsafeTail c) cs
509+
where
510+
go !v x xs = let v' = S.foldl' f v x
511+
in case xs of
512+
Empty -> v'
513+
Chunk x' xs' -> go v' x' xs'
504514

505515
-- | 'foldr1' is a variant of 'foldr' that has no starting value argument,
506516
-- and thus must be applied to non-empty 'ByteString's

Data/ByteString/Lazy/Internal.hs

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
{-# LANGUAGE CPP #-}
22
{-# LANGUAGE BangPatterns #-}
3-
{-# LANGUAGE DeriveDataTypeable #-}
43
{-# LANGUAGE DeriveLift #-}
4+
{-# LANGUAGE StandaloneDeriving #-}
55
{-# LANGUAGE TypeFamilies #-}
66
{-# LANGUAGE Unsafe #-}
7+
8+
#ifdef HS_BYTESTRING_ASSERTIONS
9+
{-# LANGUAGE PatternSynonyms #-}
10+
#endif
11+
712
{-# OPTIONS_HADDOCK not-home #-}
813

914
-- |
@@ -24,7 +29,7 @@
2429
module Data.ByteString.Lazy.Internal (
2530

2631
-- * The lazy @ByteString@ type and representation
27-
ByteString(..),
32+
ByteString(Empty, Chunk),
2833
LazyByteString,
2934
chunk,
3035
foldrChunks,
@@ -64,23 +69,45 @@ import Control.DeepSeq (NFData, rnf)
6469

6570
import Data.String (IsString(..))
6671

67-
import Data.Typeable (Typeable)
6872
import Data.Data (Data(..), mkNoRepType)
6973

7074
import GHC.Exts (IsList(..))
7175

7276
import qualified Language.Haskell.TH.Syntax as TH
7377

78+
#ifdef HS_BYTESTRING_ASSERTIONS
79+
import Control.Exception (assert)
80+
#endif
81+
82+
7483
-- | A space-efficient representation of a 'Word8' vector, supporting many
7584
-- efficient operations.
7685
--
7786
-- A lazy 'ByteString' contains 8-bit bytes, or by using the operations
7887
-- from "Data.ByteString.Lazy.Char8" it can be interpreted as containing
7988
-- 8-bit characters.
8089
--
81-
data ByteString = Empty | Chunk {-# UNPACK #-} !S.ByteString ByteString
82-
deriving (Typeable, TH.Lift)
83-
-- See 'invariant' function later in this module for internal invariants.
90+
#ifndef HS_BYTESTRING_ASSERTIONS
91+
data ByteString = Empty | Chunk {-# UNPACK #-} !S.ByteString ByteString
92+
-- INVARIANT: The S.ByteString field of any Chunk is not empty.
93+
-- (See also the 'invariant' and 'checkInvariant' functions.)
94+
95+
-- To make testing of this invariant convenient, we add an
96+
-- assertion to that effect when the HS_BYTESTRING_ASSERTIONS
97+
-- preprocessor macro is defined, by renaming the actual constructor
98+
-- and providing a pattern synonym that does the checking:
99+
#else
100+
data ByteString = Empty | Chunk_ {-# UNPACK #-} !S.ByteString ByteString
101+
102+
pattern Chunk :: S.ByteString -> ByteString -> ByteString
103+
pattern Chunk c cs <- Chunk_ c cs where
104+
Chunk c@(S.BS _ len) cs = assert (len > 0) Chunk_ c cs
105+
106+
{-# COMPLETE Empty, Chunk #-}
107+
#endif
108+
109+
deriving instance TH.Lift ByteString
110+
84111

85112
-- | Type synonym for the lazy flavour of 'ByteString'.
86113
--
@@ -158,15 +185,21 @@ unpackChars (Chunk c cs) = S.unpackAppendCharsLazy c (unpackChars cs)
158185

159186
------------------------------------------------------------------------
160187

188+
-- We no longer use these invariant-checking functions internally,
189+
-- preferring an assertion on `Chunk` itself, controlled by the
190+
-- HS_BYTESTRING_ASSERTIONS preprocessor macro.
191+
161192
-- | The data type invariant:
162-
-- Every ByteString is either 'Empty' or consists of non-null 'S.ByteString's.
163-
-- All functions must preserve this, and the QC properties must check this.
193+
-- Every ByteString is either 'Empty' or consists of non-null
194+
-- 'S.StrictByteString's. All functions must preserve this.
164195
--
165196
invariant :: ByteString -> Bool
166197
invariant Empty = True
167198
invariant (Chunk (S.BS _ len) cs) = len > 0 && invariant cs
168199

169-
-- | In a form that checks the invariant lazily.
200+
-- | Lazily checks that the given 'ByteString' satisfies the data type's
201+
-- "no empty chunks" invariant, raising an exception in place of the
202+
-- first chunk that does not satisfy the invariant.
170203
checkInvariant :: ByteString -> ByteString
171204
checkInvariant Empty = Empty
172205
checkInvariant (Chunk c@(S.BS _ len) cs)

tests/Properties/ByteString.hs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ import GHC.IO.Encoding
4343
module Properties.ByteStringLazy (tests) where
4444
#define BYTESTRING_TYPE B.ByteString
4545
import qualified Data.ByteString.Lazy as B
46-
import qualified Data.ByteString.Lazy.Internal as B (invariant)
4746
#endif
4847

4948
#else
@@ -55,7 +54,6 @@ import qualified Data.ByteString.Char8 as B
5554
#else
5655
module Properties.ByteStringLazyChar8 (tests) where
5756
import qualified Data.ByteString.Lazy.Char8 as B
58-
import qualified Data.ByteString.Lazy.Internal as B (invariant)
5957
#define BYTESTRING_TYPE B.ByteString
6058
#endif
6159

@@ -353,8 +351,6 @@ tests =
353351
\f x -> B.takeWhileEnd f x === B.reverse (B.takeWhile f (B.reverse x))
354352

355353
#ifdef BYTESTRING_LAZY
356-
, testProperty "invariant" $
357-
\x -> B.invariant x
358354
, testProperty "fromChunks . toChunks" $
359355
\x -> B.fromChunks (B.toChunks x) === x
360356
, testProperty "toChunks . fromChunks" $

tests/QuickCheckUtils.hs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import Foreign.C (CChar)
2222
import qualified Data.ByteString.Short as SB
2323
import qualified Data.ByteString as P
2424
import qualified Data.ByteString.Lazy as L
25-
import qualified Data.ByteString.Lazy.Internal as L (checkInvariant,ByteString(..))
2625

2726
import qualified Data.ByteString.Char8 as PC
2827
import qualified Data.ByteString.Lazy.Char8 as LC
@@ -46,8 +45,7 @@ instance Arbitrary L.ByteString where
4645
arbitrary = sized $ \n -> do numChunks <- choose (0, n)
4746
if numChunks == 0
4847
then return L.empty
49-
else fmap (L.checkInvariant .
50-
L.fromChunks .
48+
else fmap (L.fromChunks .
5149
filter (not . P.null)) $
5250
vectorOf numChunks
5351
(sizedByteString

0 commit comments

Comments
 (0)