From 7b307e8869e280c720c485442ec9e10749f18a83 Mon Sep 17 00:00:00 2001 From: jappeace-sloth Date: Fri, 27 Mar 2026 18:07:56 +0000 Subject: [PATCH 1/3] Add slice and unsafeSlice for ByteArray (closes #7) - slice: extracts sub-range [start, end) with Word indices, returns Maybe (Nothing when indices exceed length) - unsafeSlice: same but clamps out-of-bounds indices to valid range - Both swap indices if end < start - Added property tests: equivalence with take/drop, out-of-bounds returns Nothing, clamping behaviour, and index swap symmetry Prompt: Implement the plan to add slice/unsafeSlice to ram (issue #7) Co-Authored-By: Claude Opus 4.6 --- Data/ByteArray/Methods.hs | 27 +++++++++++++++++++++++++++ tests/Imports.hs | 2 +- tests/Tests.hs | 22 ++++++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/Data/ByteArray/Methods.hs b/Data/ByteArray/Methods.hs index f93e4cb..d9cef7d 100644 --- a/Data/ByteArray/Methods.hs +++ b/Data/ByteArray/Methods.hs @@ -40,6 +40,8 @@ module Data.ByteArray.Methods , append , concat , map + , slice + , unsafeSlice ) where import Data.ByteArray.Types @@ -313,3 +315,28 @@ map f ba = copyAndFreeze ba $ loop 0 -- | Convert a bytearray to another type of bytearray convert :: (ByteArrayAccess bin, ByteArray bout) => bin -> bout convert bs = inlineUnsafeCreate (length bs) (copyByteArrayToPtr bs) + +-- | Extract a slice from index @start@ (inclusive) to index @end@ (exclusive). +-- Indices are swapped if @end < start@. +-- Returns 'Nothing' if indices are out of bounds. +slice :: ByteArray bs => bs -> Word -> Word -> Maybe bs +slice bs start end + | hi > len = Nothing + | otherwise = Just $ unsafeSlice bs start end + where + hi = fromIntegral (max start end) :: Int + len = length bs + +-- | Like 'slice' but clamps out-of-bounds indices to the byte array length. +unsafeSlice :: ByteArray bs => bs -> Word -> Word -> bs +unsafeSlice bs start end + | sliceLen <= 0 = empty + | otherwise = unsafeCreate sliceLen $ \d -> + withByteArray bs $ \s -> memCopy d (s `plusPtr` lo') sliceLen + where + lo = fromIntegral (min start end) :: Int + hi = fromIntegral (max start end) :: Int + len = length bs + lo' = min lo len + hi' = min hi len + sliceLen = hi' - lo' diff --git a/tests/Imports.hs b/tests/Imports.hs index 4108852..9d68e7e 100644 --- a/tests/Imports.hs +++ b/tests/Imports.hs @@ -16,7 +16,7 @@ import Test.Tasty as X (TestTree, testGroup, defaultMain, Test import Test.QuickCheck as X ( Arbitrary(..), Gen, Property , (===), (.&&.) - , elements, choose, forAll, property, ioProperty + , elements, choose, forAll, property, ioProperty, (==>) , Testable ) diff --git a/tests/Tests.hs b/tests/Tests.hs index b47e1d0..d029039 100644 --- a/tests/Tests.hs +++ b/tests/Tests.hs @@ -252,4 +252,26 @@ main = defaultMain $ testGroup "memory" let b = witnessID (B.pack l) f x = x + fromIntegral w :: Word8 in B.map f b == (witnessID . B.pack . Prelude.map f $ l) + , testProperty "slice == Just (take len . drop start)" $ \(Words8 l) -> + let bs = witnessID (B.pack l) + len = fromIntegral (B.length bs) :: Word + in len > 0 ==> + forAll (choose (0, len)) $ \start -> + forAll (choose (start, len)) $ \end -> + B.slice bs start end == Just (B.take (fromIntegral (end - start)) (B.drop (fromIntegral start) bs)) + , testProperty "slice out of bounds == Nothing" $ \(Words8 l) -> + let bs = witnessID (B.pack l) + len = fromIntegral (B.length bs) :: Word + in B.slice bs 0 (len + 1) == Nothing + , testProperty "unsafeSlice clamps to valid range" $ \(Words8 l) -> + let bs = witnessID (B.pack l) + len = fromIntegral (B.length bs) :: Word + in B.length (B.unsafeSlice bs 0 (len + 100)) == B.length bs + , testProperty "slice start end == slice end start" $ \(Words8 l) -> + let bs = witnessID (B.pack l) + len = fromIntegral (B.length bs) :: Word + in len > 0 ==> + forAll (choose (0, len)) $ \a -> + forAll (choose (0, len)) $ \b -> + B.slice bs a b == B.slice bs b a ] From ed7338255271ef8f460d51bd5e849f070a66c3ef Mon Sep 17 00:00:00 2001 From: jappeace-sloth Date: Fri, 27 Mar 2026 18:30:33 +0000 Subject: [PATCH 2/3] Add slice/unsafeSlice to CHANGELOG for 0.22.0 Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9489d74..ced93ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,10 @@ ## 0.22.0 ++ Added `slice :: ByteArray bs => bs -> Word -> Word -> Maybe bs` + and `unsafeSlice :: ByteArray bs => bs -> Word -> Word -> bs` + to `Data.ByteArray.Methods` (re-exported via `Data.ByteArray`). + Extract a sub-range `[start, end)` from a byte array. `slice` returns + `Nothing` for out-of-bounds indices; `unsafeSlice` clamps them. + Both swap indices if end < start. (closes #7) + Added `map :: (ByteArrayAccess ba, ByteArray ba) => (Word8 -> Word8) -> ba -> ba` to `Data.ByteArray.Methods` (re-exported via `Data.ByteArray`). Applies a function to each byte of a byte array. (closes #5) From 6cf194ea30755a161ac89bf2c41b5b98127a7ba0 Mon Sep 17 00:00:00 2001 From: jappeace-sloth Date: Sat, 28 Mar 2026 12:11:51 +0000 Subject: [PATCH 3/3] Address PR #8 review: Int types, offset+length semantics, error on invalid - Change slice/unsafeSlice signatures from Word to Int - Change from (start, end) to (offset, length) semantics - unsafeSlice now calls error instead of clamping on invalid args - slice returns Nothing for negative offset/length or out-of-bounds - Update tests: new semantics, negative input tests, error assertion - Update CHANGELOG entry Addresses review comments from kazu-yamamoto on PR #8. Prompt: Implement the following plan: Address PR #8 review comments Tokens: ~40k input, ~8k output Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 10 ++++----- Data/ByteArray/Methods.hs | 45 ++++++++++++++++++++------------------- tests/Imports.hs | 11 +++++++++- tests/Tests.hs | 34 ++++++++++++++--------------- 4 files changed, 55 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ced93ee..876ab78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,10 @@ ## 0.22.0 -+ Added `slice :: ByteArray bs => bs -> Word -> Word -> Maybe bs` - and `unsafeSlice :: ByteArray bs => bs -> Word -> Word -> bs` ++ Added `slice :: ByteArray bs => bs -> Int -> Int -> Maybe bs` + and `unsafeSlice :: ByteArray bs => bs -> Int -> Int -> bs` to `Data.ByteArray.Methods` (re-exported via `Data.ByteArray`). - Extract a sub-range `[start, end)` from a byte array. `slice` returns - `Nothing` for out-of-bounds indices; `unsafeSlice` clamps them. - Both swap indices if end < start. (closes #7) + `slice bs offset len` extracts `len` bytes starting at `offset`. + Returns `Nothing` for negative offset/length or out-of-bounds access. + `unsafeSlice` calls `error` on invalid arguments. (closes #7) + Added `map :: (ByteArrayAccess ba, ByteArray ba) => (Word8 -> Word8) -> ba -> ba` to `Data.ByteArray.Methods` (re-exported via `Data.ByteArray`). Applies a function to each byte of a byte array. (closes #5) diff --git a/Data/ByteArray/Methods.hs b/Data/ByteArray/Methods.hs index d9cef7d..61a8790 100644 --- a/Data/ByteArray/Methods.hs +++ b/Data/ByteArray/Methods.hs @@ -316,27 +316,28 @@ map f ba = copyAndFreeze ba $ loop 0 convert :: (ByteArrayAccess bin, ByteArray bout) => bin -> bout convert bs = inlineUnsafeCreate (length bs) (copyByteArrayToPtr bs) --- | Extract a slice from index @start@ (inclusive) to index @end@ (exclusive). --- Indices are swapped if @end < start@. --- Returns 'Nothing' if indices are out of bounds. -slice :: ByteArray bs => bs -> Word -> Word -> Maybe bs -slice bs start end - | hi > len = Nothing - | otherwise = Just $ unsafeSlice bs start end +-- | Extract @len@ bytes starting at byte @offset@. +-- Returns 'Nothing' if @offset@ or @len@ is negative, or if @offset + len@ +-- exceeds the byte array length. +slice :: ByteArray bs => bs -> Int -> Int -> Maybe bs +slice bs offset len + | offset < 0 = Nothing + | len < 0 = Nothing + | offset + len > bsLen = Nothing + | otherwise = Just $ unsafeCreate len $ \d -> + withByteArray bs $ \s -> memCopy d (s `plusPtr` offset) len where - hi = fromIntegral (max start end) :: Int - len = length bs - --- | Like 'slice' but clamps out-of-bounds indices to the byte array length. -unsafeSlice :: ByteArray bs => bs -> Word -> Word -> bs -unsafeSlice bs start end - | sliceLen <= 0 = empty - | otherwise = unsafeCreate sliceLen $ \d -> - withByteArray bs $ \s -> memCopy d (s `plusPtr` lo') sliceLen + bsLen = length bs + +-- | Like 'slice' but calls 'error' when arguments are out of bounds. +-- This includes negative @offset@, negative @len@, or @offset + len@ +-- exceeding the byte array length. +unsafeSlice :: ByteArray bs => bs -> Int -> Int -> bs +unsafeSlice bs offset len + | offset < 0 = error "unsafeSlice: negative offset" + | len < 0 = error "unsafeSlice: negative length" + | offset + len > bsLen = error "unsafeSlice: offset + length exceeds byte array size" + | otherwise = unsafeCreate len $ \d -> + withByteArray bs $ \s -> memCopy d (s `plusPtr` offset) len where - lo = fromIntegral (min start end) :: Int - hi = fromIntegral (max start end) :: Int - len = length bs - lo' = min lo len - hi' = min hi len - sliceLen = hi' - lo' + bsLen = length bs diff --git a/tests/Imports.hs b/tests/Imports.hs index 9d68e7e..b4f74b5 100644 --- a/tests/Imports.hs +++ b/tests/Imports.hs @@ -5,6 +5,7 @@ module Imports , testCase , assertBool , assertEqual + , assertException , (@?=) ) where @@ -22,7 +23,7 @@ import Test.QuickCheck as X import Test.Tasty.Providers (singleTest, IsTest(..), testPassed, testFailed) import Test.QuickCheck (quickCheckWithResult, stdArgs, isSuccess, Args(..)) -import Control.Exception (SomeException, try) +import Control.Exception (SomeException, ErrorCall, try, evaluate) -- | QuickCheck property test provider for tasty newtype QCTest = QCTest Property @@ -71,3 +72,11 @@ infix 1 @?= actual @?= expected | actual == expected = return () | otherwise = fail ("expected: " ++ show expected ++ "\n but got: " ++ show actual) + +-- | Assert that evaluating a value throws an 'ErrorCall' exception. +assertException :: forall a . a -> IO () +assertException val = do + r <- try (evaluate val) :: IO (Either ErrorCall a) + case r of + Left _ -> return () + Right _ -> fail "expected an exception but none was thrown" diff --git a/tests/Tests.hs b/tests/Tests.hs index d029039..aa43220 100644 --- a/tests/Tests.hs +++ b/tests/Tests.hs @@ -252,26 +252,26 @@ main = defaultMain $ testGroup "memory" let b = witnessID (B.pack l) f x = x + fromIntegral w :: Word8 in B.map f b == (witnessID . B.pack . Prelude.map f $ l) - , testProperty "slice == Just (take len . drop start)" $ \(Words8 l) -> + , testProperty "slice == Just (take len . drop offset)" $ \(Words8 l) -> let bs = witnessID (B.pack l) - len = fromIntegral (B.length bs) :: Word - in len > 0 ==> - forAll (choose (0, len)) $ \start -> - forAll (choose (start, len)) $ \end -> - B.slice bs start end == Just (B.take (fromIntegral (end - start)) (B.drop (fromIntegral start) bs)) + bsLen = B.length bs + in bsLen > 0 ==> + forAll (choose (0, bsLen)) $ \offset -> + forAll (choose (0, bsLen - offset)) $ \len -> + B.slice bs offset len == Just (B.take len (B.drop offset bs)) , testProperty "slice out of bounds == Nothing" $ \(Words8 l) -> let bs = witnessID (B.pack l) - len = fromIntegral (B.length bs) :: Word - in B.slice bs 0 (len + 1) == Nothing - , testProperty "unsafeSlice clamps to valid range" $ \(Words8 l) -> + bsLen = B.length bs + in B.slice bs 0 (bsLen + 1) == Nothing + , testProperty "slice negative offset == Nothing" $ \(Words8 l) -> let bs = witnessID (B.pack l) - len = fromIntegral (B.length bs) :: Word - in B.length (B.unsafeSlice bs 0 (len + 100)) == B.length bs - , testProperty "slice start end == slice end start" $ \(Words8 l) -> + in B.slice bs (-1) 0 == Nothing + , testProperty "slice negative length == Nothing" $ \(Words8 l) -> let bs = witnessID (B.pack l) - len = fromIntegral (B.length bs) :: Word - in len > 0 ==> - forAll (choose (0, len)) $ \a -> - forAll (choose (0, len)) $ \b -> - B.slice bs a b == B.slice bs b a + in B.slice bs 0 (-1) == Nothing + , testCase "unsafeSlice errors on out of bounds" $ do + let bs = witnessID (B.pack [1,2,3,4,5]) + assertException (B.unsafeSlice bs (-1) 1) + assertException (B.unsafeSlice bs 0 (-1)) + assertException (B.unsafeSlice bs 0 (B.length bs + 1)) ]