packages feed

data-elevator 0.1.0.2 → 0.2

raw patch · 5 files changed

+146/−4 lines, 5 filesdep +inspection-testingdep +primitivedep ~basePVP ok

version bump matches the API change (PVP)

Dependencies added: inspection-testing, primitive

Dependency ranges changed: base

API changes (from Hackage documentation)

- Data.Elevator: class LevitySubsumption (a :: TYPE (BoxedRep l)) (b :: TYPE (BoxedRep r))
+ Data.Elevator: class LevitySubsumption (a :: TYPE 'BoxedRep l) (b :: TYPE 'BoxedRep r)
- Data.Elevator: data Strict (a :: LiftedType)
+ Data.Elevator: data Strict a :: UnliftedType
- Data.Elevator.Internal: [Lazy_] :: Any @LiftedType -> Lazy a
+ Data.Elevator.Internal: [Lazy_] :: forall (a :: UnliftedType). (Any :: LiftedType) -> Lazy a
- Data.Elevator.Internal: [Strict_] :: Any @UnliftedType -> Strict a
+ Data.Elevator.Internal: [Strict_] :: forall a. (Any :: UnliftedType) -> Strict a
- Data.Elevator.Internal: class LevitySubsumption (a :: TYPE (BoxedRep l)) (b :: TYPE (BoxedRep r))
+ Data.Elevator.Internal: class LevitySubsumption (a :: TYPE 'BoxedRep l) (b :: TYPE 'BoxedRep r)
- Data.Elevator.Internal: fromLazy# :: Lazy a -> a
+ Data.Elevator.Internal: fromLazy# :: forall (a :: UnliftedType). Lazy a -> a
- Data.Elevator.Internal: newtype Strict (a :: LiftedType)
+ Data.Elevator.Internal: newtype Strict a :: UnliftedType
- Data.Elevator.Internal: toLazy# :: a -> Lazy a
+ Data.Elevator.Internal: toLazy# :: forall (a :: UnliftedType). a -> Lazy a

Files

CHANGELOG.md view
@@ -1,5 +1,12 @@ # Revision history for data-elevator +## 0.2 -- 2024-09-16++* Fix issue #4, which makes `toStrict#` and `fromLazy#` NOINLINE.+  We now have rewrite rules to cancel away, e.g., `fromStrict# . toStrict# = id`.+  This is expected to be a bit more brittle performance-wise compared to the+  0.1.*, but it fixes a major soundness bug (#4).+ ## 0.1.0.2 -- 2024-08-02  * Added support for GHC versions 9.6, 9.8, and 9.10
data-elevator.cabal view
@@ -1,6 +1,6 @@ cabal-version:      2.4 name:               data-elevator-version:            0.1.0.2+version:            0.2  tested-with: GHC ==9.2.8 || ==9.4.8 || ==9.6.6 || ==9.8.2 || ==9.10.1 @@ -24,10 +24,8 @@ -- An email address to which users can send suggestions, bug reports, and patches. maintainer: sgraf1337@gmail.com --- A copyright notice.-copyright: 2022 category: Data-extra-source-files: CHANGELOG.md+extra-doc-files: CHANGELOG.md  bug-reports: https://github.com/sgraf812/data-elevator/issues @@ -56,6 +54,9 @@     -- hs-source-dirs:     main-is:          Main.hs     hs-source-dirs:   test+    other-modules:    Issue4     build-depends:    base >=4.16 && <4.21                     , data-elevator+                    , primitive                     , hspec+                    , inspection-testing
src/Data/Elevator/Internal.hs view
@@ -49,9 +49,13 @@  toStrict# :: a -> Strict a toStrict# = unsafeCoerce id+{-# NOINLINE toStrict# #-} -- See Note [NOINLINE toStrict#/fromLazy#]+{-# RULES "fromStrict#.toStrict#" forall x. fromStrict# (toStrict# x) = x #-}+{-# RULES "toLazy#.toStrict#" forall x. toLazy# (toStrict# x) = unsafeCoerce id x #-}  fromStrict# :: Strict a -> a fromStrict# = unsafeCoerce id+{-# INLINE[0] fromStrict# #-} -- See Note [NOINLINE toStrict#/fromLazy#]  pattern Strict :: a -> Strict a pattern Strict x <- (fromStrict# -> x) where@@ -75,9 +79,13 @@  toLazy# :: a -> Lazy a toLazy# = unsafeCoerce id+{-# INLINE[0] toLazy# #-} -- See Note [NOINLINE toStrict#/fromLazy#]  fromLazy# :: Lazy a -> a fromLazy# = unsafeCoerce id+{-# NOINLINE fromLazy# #-} -- See Note [NOINLINE toStrict#/fromLazy#]+{-# RULES "toLazy#.fromLazy#" forall x. toLazy# (fromLazy# x) = x #-}+{-# RULES "fromStrict#.fromLazy#" forall x. fromStrict# (fromLazy# x) = unsafeCoerce id x #-}  pattern Lazy :: a -> Lazy a pattern Lazy x <- (fromLazy# -> x) where@@ -217,3 +225,38 @@   -- Specification:   -- > levCoerce# f x = levCoerce# (f (levCoerce# x :: a1))   levCoerce# f x = unsafeCoerce# f x++{- Note [NOINLINE toStrict#/fromLazy#]+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+It is important GHC does not regard the expression `Strict x` as trivial.+This is because CorePrep will not case-bind `Strict x` in `f (Strict x)` when it+is trivial, in which case we do not get proper call-by-value for the call to `f`.++However, if we inline the Strict and the toStrict#, what we get is+`f (x |> co)`, and `x |> co` is trivial.+So, sadly we need to mark `toStrict#` as NOINLINE; in which case we get+`f (toStrict# x)`, where `toStrict# x` is non-trivial and thus properly+case-bound by CorePrep: `case toStrict# x of x' { __DEFAULT -> f x' }`.++But this means that expressions such as `toLazy# (toStrict# x)` are no longer+optimised to `x`. We fix that by introducing the rewrite rules++  forall x. fromStrict# (toStrict# x) = x+  forall x. toLazy# (toStrict# x) = unsafeCoerce id x -- will desugar to x |> co++Thus we keep the zero-cost promise.++This applies to `fromLazy#` as well. Note the following desugaring++  case x of Lazy y -> f y+  ==> {INLINE, including `fromLazy#`, CorePrep}+  f (x |> co)++Note the absence of a seq on `x`. If we do not inline `fromLazy#`, we get++  f (fromLazy# x)+  ==> {CorePrep}+  case fromLazy# x of y { __DEFAULT -> f y }++Much better. We need similar rewrite rules, however.+-}
+ test/Issue4.hs view
@@ -0,0 +1,59 @@+{-# OPTIONS_GHC -O2              #-}+{-# LANGUAGE MagicHash           #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications    #-}+{-# LANGUAGE UnboxedTuples       #-}+module Issue4 (+    StrictArray,+    primArrayToStrictArray,+    indexStrictArray,+    sizeofStrictArray,+) where++import           Data.Elevator (Strict (Strict))+import qualified Data.Primitive as P+import qualified GHC.Exts as GHC+import           GHC.Exts ((+#))+import           GHC.ST (ST(ST), runST)++data StrictArray a = StrictArray !(GHC.Array# (Strict a))++primArrayToStrictArray :: forall a. P.Array a -> StrictArray a+primArrayToStrictArray (P.Array a) =+    runST $ ST $ \s0 ->+    case GHC.newArray# (GHC.sizeofArray# a) (Strict (P.indexArray (P.Array a) 0)) s0 of+      (# s1, a#  #) ->+        case go a# 0# s1 of+          s2 -> case GHC.unsafeFreezeArray# a# s2 of+                  (# s3, a'# #) -> (# s3, StrictArray a'# #)+  where+    go :: forall s.+          GHC.MutableArray# s (Strict a)+       -> GHC.Int#+       -> GHC.State# s+       -> GHC.State# s+    go a# i# s =+      case i# GHC.<# GHC.sizeofArray# a of+        1# ->+          case GHC.indexArray# a i# of+            (# x #) ->+              -- We have to use seq# here to force the array element to WHNF+              -- before putting it into the strict array. This should not be+              -- necessary. https://github.com/sgraf812/data-elevator/issues/4+{-              case GHC.seq# x s of+                (# s', x' #) ->+-}+                  case GHC.writeArray# a# i# (Strict x) s of+                    s' -> go a# (i# +# 1#) s'+        _  -> s++{-# INLINE indexStrictArray #-}+indexStrictArray :: StrictArray a -> Int -> a+indexStrictArray (StrictArray a#) (GHC.I# i#) =+    case GHC.indexArray# a# i# of+      (# Strict x #) -> x++{-# INLINE sizeofStrictArray #-}+sizeofStrictArray :: StrictArray a -> Int+sizeofStrictArray (StrictArray a#) =+    GHC.I# (GHC.sizeofArray# a#)
test/Main.hs view
@@ -1,12 +1,21 @@+{-# OPTIONS_GHC -fplugin=Test.Inspection.Plugin #-}+ {-# LANGUAGE TypeApplications #-} {-# LANGUAGE MagicHash        #-}+{-# LANGUAGE UnboxedTuples    #-}+{-# LANGUAGE TemplateHaskell  #-}  module Main where  import Data.Elevator.Internal import Test.Hspec+import Test.Inspection import Control.Exception (evaluate, SomeException (SomeException)) import GHC.Exts+import GHC.IO+import Data.IORef+import qualified Data.Primitive as P+import qualified Issue4  main :: IO () main = hspec $ do@@ -40,3 +49,26 @@         levCoerce @(Int -> Strict Int) @(Strict Int -> Int) (\x -> Strict (x+1)) (Strict 42) `shouldBe` (43 :: Int)       it "rank2 co: (Int -> Int) -> Int ~ (Strict Int -> Int) -> Int" $ do         levCoerce @((Strict Int -> Int) -> Int) @((Int -> Strict Int) -> Int) (\f -> f (Strict 42)) (\x -> Strict x) `shouldBe` (42 :: Int)++  describe "Issue 4" $ do+    it "should not crash" $ do+      let arr = P.createArray 5 (even (sum [0..1000]) :: Bool) (\_arr -> return ())+      let sarr = Issue4.primArrayToStrictArray arr+      Issue4.indexStrictArray sarr 3 `shouldBe` True++from_to_strict :: Bool -> Bool+from_to_strict x = fromStrict# (toStrict# x)++from_to_strict2 :: Bool -> Bool+from_to_strict2 x = case Strict x of Strict x -> x++to_from_lazy :: Lazy a -> Lazy a+to_from_lazy x = toLazy# (fromLazy# x)++to_from_lazy2 :: Lazy a -> Lazy a+to_from_lazy2 (Lazy x) = Lazy x++inspect $ 'from_to_strict `doesNotUse` 'toStrict#+inspect $ ('from_to_strict2 `doesNotUse` 'toStrict#) { expectFail = True } -- https://gitlab.haskell.org/ghc/ghc/-/issues/25261+inspect $ 'to_from_lazy `doesNotUse` 'fromLazy#+inspect $ ('to_from_lazy2 `doesNotUse` 'fromLazy#) { expectFail = True } -- https://gitlab.haskell.org/ghc/ghc/-/issues/25261