-- SPDX-FileCopyrightText: 2021 Oxhead Alpha -- SPDX-License-Identifier: LicenseRef-MIT-OA {-# OPTIONS_GHC -Wno-orphans #-} {-# OPTIONS_GHC -Wno-unused-top-binds #-} -- This is needed because `singletons-th` generates redundant constraints. -- GHC may not throw the warning always, see #791. {-# OPTIONS_GHC -Wno-redundant-constraints #-} {-# LANGUAGE UndecidableSuperClasses #-} {-# LANGUAGE DeriveLift #-} -- | Type-nat utilities. -- -- We take Peano numbers as base for operations because they make it -- much easer to prove things to compiler. Their performance does not -- seem to introduce a problem, because we use nats primarily along with -- stack which is a linked list with similar performance characteristics. -- -- Many of things we introduce here are covered in @type-natural@ package, -- but unfortunatelly it does not work with GHC 8.6 at the moment of writing -- this module. We use "Data.Vinyl" as source of Peano @Nat@ for now. module Morley.Util.Peano ( -- * General Peano , pattern S , pattern Z , ToPeano , FromPeano , SingNat (SZ, SS) , peanoSing , peanoSing' , withPeanoSingI , withSomePeano -- * Utility type synonyms , IsoNatPeano , SingIPeano -- * Peano Arithmetic , type (>) , type (>=) , peanoSingDecrement , peanoSingAdd , Decrement , AddPeano , SubPeano , MinPeano , MaxPeano -- * Lists , Length , At , Drop , Take -- * Morley-specific utils , IsLongerThan , LongerThan , RequireLongerThan , IsLongerOrSameLength , LongerOrSameLength , RequireLongerOrSameLength -- * Length constraints 'Dict'ionaries , requireLongerThan , requireLongerOrSameLength -- * Length constraints 'Dict'ionaries , isGreaterThan , isGreaterEqualThan -- * Inductive proofs , additivity , associativity , minIdempotency , commutativity , transitivity , (|-) -- * Helpers , toNatural , someSingNat ) where import Data.Constraint (Dict(..), (\\)) import Data.Singletons (Sing, SingI(..), SomeSing(..)) import Data.Type.Equality (gcastWith, type (:~:)(..)) import Data.Vinyl (Rec(..)) import Data.Vinyl.TypeLevel (Nat(..), RLength) import GHC.TypeLits (ErrorMessage(..), TypeError) import GHC.TypeNats (type (+), type (-)) import GHC.TypeNats qualified as GHC import Language.Haskell.TH.Lift (Lift) import Unsafe.Coerce (unsafeCoerce) import Morley.Util.Sing (genSingletonsType) import Morley.Util.Type (If, MockableConstraint(..)) -- This is very obviously a false positive. {-# ANN module ("HLint: ignore Use 'natVal' from Universum" :: Text) #-} {-# ANN module ("HLint: ignore Use 'someNatVal' from Universum" :: Text) #-} ---------------------------------------------------------------------------- -- General ---------------------------------------------------------------------------- -- | A convenient alias. -- -- We are going to use 'Peano' numbers for type-dependent logic and -- normal 'Nat's in user API, need to distinguish them somehow. type Peano = Nat deriving stock instance Eq Nat deriving stock instance Show Nat deriving stock instance Generic Nat deriving anyclass instance NFData Nat $(genSingletonsType ''Nat) deriving stock instance Show (SingNat (n :: Nat)) deriving stock instance Eq (SingNat (n :: Nat)) deriving stock instance Lift (SingNat (n :: Nat)) instance NFData (SingNat (n :: Nat)) where rnf SZ = () rnf (SS n) = rnf n -- | A constraint asserting that GHC's @Nat@ @n@ and @Peano@ @p@ are the same (up to an -- isomorphism) type IsoNatPeano (n :: GHC.Nat) (p :: Peano) = (n ~ FromPeano p, ToPeano n ~ p) -- | A synonym for @SingI (ToPeano n)@. Essentialy requires that we can construct a 'Peano' -- singleton for a given 'Nat' type SingIPeano (n :: GHC.Nat) = SingI (ToPeano n) type family ToPeano (n :: GHC.Nat) :: Peano where ToPeano 0 = 'Z ToPeano a = 'S (ToPeano (a - 1)) type family FromPeano (n :: Peano) :: GHC.Nat where FromPeano 'Z = 0 FromPeano ('S a) = 1 + FromPeano a -- | Get the peano singleton for a given type-level nat literal. -- -- >>> peanoSing @2 -- SS (SS SZ) peanoSing :: forall (n :: GHC.Nat). SingIPeano n => SingNat (ToPeano n) peanoSing = sing @(ToPeano n) -- | Same as 'peanoSing', but only requires 'KnownNat' instance. -- -- Witnesses half the equivalence between @KnownNat n@ and @SingI (ToPeano n)@ peanoSing' :: forall (n :: GHC.Nat). KnownNat n => SingNat (ToPeano n) peanoSing' = go (natVal @n Proxy) where go :: forall m. Natural -> SingNat m go = \case 0 -> (unsafeCoerce Refl :: m :~: 'Z) |- SZ n -> (unsafeCoerce Refl :: m :~: 'S (Decrement m)) |- SS $ go @(Decrement m) (n - 1) -- | Run a computation requiring @SingI (ToPeano n)@ in a context which only has -- @KnownNat n@. Mostly useful when used with 'SomeNat' withPeanoSingI :: forall (n :: GHC.Nat) r. KnownNat n => (SingIPeano n => r) -> r withPeanoSingI act = act \\ go @(ToPeano n) (natVal @n Proxy) where go :: forall (m :: Peano). Natural -> Dict (SingI m) go = \case 0 -> (unsafeCoerce Refl :: m :~: 'Z) |- Dict n -> (unsafeCoerce Refl :: m :~: 'S (Decrement m)) |- (Dict \\ go @(Decrement m) (n - 1)) -- | Lift a given term-level 'Natural' to the type level for a given computation. The computation is -- expected to accept a 'Proxy' for the sake of convenience: it's easier to get at the type-level -- natural with @ScopedTypeVariables@ when pattern-matching on the proxy, e.g. -- -- > (x :: Natural) `withSomePeano` \(_ :: Proxy n) -> doSomeNatComputation @n withSomePeano :: Natural -> (forall n. (KnownNat n, SingIPeano n) => Proxy n -> r) -> r withSomePeano n f = case someNatVal n of SomeNat (pn :: Proxy n) -> withPeanoSingI @n $ f pn ---------------------------------------------------------------------------- -- Peano Arithmetic ---------------------------------------------------------------------------- type family Decrement (a :: Peano) :: Peano where Decrement 'Z = TypeError ('Text "Expected n > 0") Decrement ('S n) = n -- | Utility to 'Decrement' a Peano 'Sing'leton. -- -- Useful when dealing with the constraint peanoSingDecrement :: Sing n -> Maybe (Sing (Decrement n)) peanoSingDecrement = \case SZ -> Nothing SS n -> pure n -- | 'Peano' naturals comparisson type family (>) (x :: Peano) (y :: Peano) :: Bool where 'Z > _ = 'False 'S _ > 'Z = 'True 'S x > 'S y = x > y -- | 'Peano' naturals comparisson type family (>=) (x :: Peano) (y :: Peano) :: Bool where _ >= 'Z = 'True 'Z >= _ = 'False ('S x) >= ('S y) = x >= y -- | 'Peano' naturals addition type family AddPeano (n :: Peano) (m :: Peano) :: Peano where AddPeano 'Z x = x AddPeano ('S x) y = 'S (AddPeano x y) -- | 'Peano' naturals subtraction type family SubPeano (n :: Peano) (m :: Peano) :: Peano where SubPeano 'Z ('S m) = TypeError ('Text "Subtracting " ':<>: 'ShowType (FromPeano ('S m)) ':<>: 'Text " from zero") SubPeano n 'Z = n SubPeano ('S n) ('S m) = SubPeano n m -- | Out of two 'Peano' naturals, return the smaller one type family MinPeano (n :: Peano) (m :: Peano) :: Peano where MinPeano _ 'Z = 'Z MinPeano 'Z _ = 'Z MinPeano ('S n) ('S m) = 'S (MinPeano n m) -- | Out of two 'Peano' naturals, return the larger one type family MaxPeano (n :: Peano) (m :: Peano) :: Peano where MaxPeano n 'Z = n MaxPeano 'Z m = m MaxPeano ('S n) ('S m) = 'S (MaxPeano n m) -- | Singleton addition peanoSingAdd :: SingNat n -> SingNat m -> SingNat (AddPeano n m) peanoSingAdd (SS n) (SS m) = associativity n m |- SS $ SS $ peanoSingAdd n m peanoSingAdd SZ m = m peanoSingAdd n SZ = commutativity n SZ |- n ---------------------------------------------------------------------------- -- Lists ---------------------------------------------------------------------------- type family Length l :: Peano where Length l = RLength l type family At (n :: Peano) s where At 'Z (x ': _) = x At ('S n) (_ ': xs) = At n xs At a '[] = TypeError ('Text "You tried to access a non-existing element of the stack, n = " ':<>: 'ShowType (FromPeano a)) type family Drop (n :: Peano) (s :: [k]) :: [k] where Drop 'Z s = s Drop ('S _) '[] = '[] Drop ('S n) (_ ': s) = Drop n s type family Take (n :: Peano) (s :: [k]) :: [k] where Take 'Z _ = '[] Take _ '[] = '[] Take ('S n) (a ': s) = a ': Take n s ---------------------------------------------------------------------------- -- Morley-specific utils ---------------------------------------------------------------------------- -- Note that we could define type families to return 'Constraint' instead -- of defining standalone constraint in form `c ~ 'True`, but apparently -- such constraint would be weaker, e. g. there is an example when with -- current approach there is no warning, but if we change the approach -- to return 'Constraint' from type family then GHC complains about -- non-exhaustive patterns (@gromak). -- Also we use these `Bool` type families in more than one place, we generate -- two constraints: one gives more information to GHC and another one produces -- better error messages on failure. -- | Comparison of type-level naturals, as a function. -- -- It is as lazy on the list argument as possible - there is no -- need to know the whole list if the natural argument is small enough. -- This property is important if we want to be able to extract reusable -- parts of code which are aware only of relevant part of stack. type family IsLongerThan (l :: [k]) (a :: Peano) :: Bool where IsLongerThan (_ ': _) 'Z = 'True IsLongerThan (_ ': xs) ('S a) = IsLongerThan xs a IsLongerThan '[] _ = 'False -- | Comparison of type-level naturals, as a constraint. type LongerThan l a = IsLongerThan l a ~ 'True -- | Similar to 'IsLongerThan', but returns 'True' when list length -- equals to the passed number. type family IsLongerOrSameLength (l :: [k]) (a :: Peano) :: Bool where IsLongerOrSameLength _ 'Z = 'True IsLongerOrSameLength (_ ': xs) ('S a) = IsLongerOrSameLength xs a IsLongerOrSameLength '[] ('S _) = 'False -- | 'IsLongerOrSameLength' in form of constraint that gives most -- information to GHC. type LongerOrSameLength l a = IsLongerOrSameLength l a ~ 'True {- | Evaluates list length. This type family is a best-effort attempt to display neat error messages when list is known only partially. For instance, when called on @Int ': Int ': s@, the result will be @OfLengthWithTail 2 s@ - compare with result of simple 'Length' - @1 + 1 + Length s@. For concrete types this will be identical to calling @FromPeano (Length l)@. -} type family OfLengthWithTail (acc :: GHC.Nat) (l :: [k]) :: GHC.Nat where OfLengthWithTail a '[] = a OfLengthWithTail a (_ ': xs) = OfLengthWithTail (a + 1) xs type LengthWithTail l = OfLengthWithTail 0 l -- | Comparison of type-level naturals, raises human-readable compile error -- when does not hold. -- -- Here we use the same approach as for 'RequireLongerOrSameLength', this -- type family is internal. type family RequireLongerThan' (l :: [k]) (a :: Nat) :: Constraint where RequireLongerThan' l a = If (IsLongerThan l a) (() :: Constraint) (TypeError ('Text "Stack element #" ':<>: 'ShowType (FromPeano a) ':<>: 'Text " is not accessible" ':$$: 'Text "Current stack has size of only " ':<>: 'ShowType (LengthWithTail l) ':<>: 'Text ":" ':$$: 'ShowType l )) class (RequireLongerThan' l a, LongerThan l a) => RequireLongerThan (l :: [k]) (a :: Peano) instance (RequireLongerThan' l a, LongerThan l a) => RequireLongerThan l a -- | 'IsLongerOrSameLength' in form of constraint that produces -- good error message. Should be used together with 'LongerThan' -- because 'LongerThan' gives GHC more information. -- We use it in combination, so that it gives enough information to -- GHC and also producess good error messages. type family RequireLongerOrSameLength' (l :: [k]) (a :: Peano) :: Constraint where RequireLongerOrSameLength' l a = If (IsLongerOrSameLength l a) (() :: Constraint) (TypeError ('Text "Expected stack with length >= " ':<>: 'ShowType (FromPeano a) ':$$: 'Text "Current stack has size of only " ':<>: 'ShowType (LengthWithTail l) ':<>: 'Text ":" ':$$: 'ShowType l )) -- | We can have -- `RequireLongerOrSameLength = (RequireLongerOrSameLength' l a, LongerOrSameLength l a)`, -- but apparently the printed error message can be caused by `LongerOrSameLength` -- rather than `RequireLongerOrSameLength'`. -- We do not know for sure how it all works, but we think that if we require constraint X before -- Y (using multiple `=>`s) then X will always be evaluated first. class (RequireLongerOrSameLength' l a, LongerOrSameLength l a) => RequireLongerOrSameLength (l :: [k]) (a :: Peano) instance (RequireLongerOrSameLength' l a, LongerOrSameLength l a) => RequireLongerOrSameLength l a instance MockableConstraint (RequireLongerOrSameLength l a) where unsafeProvideConstraint = unsafeCoerce $ Dict @(RequireLongerOrSameLength '[] 'Z) instance MockableConstraint (RequireLongerThan l a) where unsafeProvideConstraint = unsafeCoerce $ Dict @(RequireLongerThan '[()] 'Z) ---------------------------------------------------------------------------- -- Length constraints 'Dict'ionaries ---------------------------------------------------------------------------- requireLongerThan :: Rec any stk -> Sing n -> Maybe (Dict (RequireLongerThan stk n)) requireLongerThan RNil _ = Nothing requireLongerThan (_ :& _xs) SZ = Just Dict requireLongerThan (_ :& xs) (SS n) = do Dict <- requireLongerThan xs n return Dict requireLongerOrSameLength :: Rec any stk -> Sing n -> Maybe (Dict (RequireLongerOrSameLength stk n)) requireLongerOrSameLength _ SZ = Just Dict requireLongerOrSameLength RNil (SS _) = Nothing requireLongerOrSameLength (_ :& xs) (SS n) = do Dict <- requireLongerOrSameLength xs n return Dict ---------------------------------------------------------------------------- -- Arith constraints 'Dict'ionaries ---------------------------------------------------------------------------- isGreaterThan :: Sing a -> Sing b -> Maybe (Dict ((a > b) ~ 'True)) isGreaterThan SZ _ = Nothing isGreaterThan (SS _) SZ = pure Dict isGreaterThan (SS a) (SS b) = isGreaterThan a b isGreaterEqualThan :: Sing a -> Sing b -> Maybe (Dict ((a >= b) ~ 'True)) isGreaterEqualThan _ SZ = pure Dict isGreaterEqualThan SZ _ = Nothing isGreaterEqualThan (SS a) (SS b) = isGreaterEqualThan a b ---------------------------------------------------------------------------- -- Inductive proofs ---------------------------------------------------------------------------- -- | Convenience synonym for 'gcastWith' (|-) :: forall k (a :: k) (b :: k) r. (a :~: b) -> ((a ~ b) => r) -> r (|-) = gcastWith infixr 1 |- -- | Proof that for naturals, @k + (m + 1) = n@ entails @n > k@ {-# RULES "additivity" forall k m n. additivity k m n = unsafeCoerce Refl #-} {-# INLINE[1] additivity #-} additivity :: forall k m n. AddPeano k ('S m) ~ n => SingNat m -> SingNat n -> SingNat k -> n > k :~: 'True additivity _ (SS _) SZ = Refl additivity m (SS _) k = associativity k m |- lemma2 @_ @m k |- lemma k |- Refl where lemma2 :: forall k' m'. SingNat k' -> 'S (AddPeano k' m') > k' :~: 'True lemma2 SZ = Refl lemma2 (SS k') = lemma2 @_ @m' k' |- Refl lemma :: SingNat k' -> 'S k' > k' :~: 'True lemma SZ = Refl lemma (SS n) = lemma n |- Refl -- | Proof that for naturals, @x + (y + 1) = (x + y) + 1@ {-# RULES "associativity" forall x y. associativity x y = unsafeCoerce Refl #-} {-# INLINE[1] associativity #-} associativity :: SingNat x -> SingNat y -> AddPeano x ('S y) :~: 'S (AddPeano x y) associativity SZ _ = Refl associativity (SS x) y = associativity x y |- Refl -- | Proof that @x + y = y + x@ {-# RULES "commutativity" forall x y. commutativity x y = unsafeCoerce Refl #-} {-# INLINE[1] commutativity #-} commutativity :: SingNat x -> SingNat y -> AddPeano x y :~: AddPeano y x commutativity SZ SZ = Refl commutativity SZ (SS y) = commutativity SZ y |- Refl commutativity (SS x) y = commutativity x y |- associativity y x |- Refl -- | Proof that for naturals, @min(n, n) = n@ {-# RULES "minIdempotency" forall x. minIdempotency x = unsafeCoerce Refl #-} {-# INLINE[1] minIdempotency #-} minIdempotency :: SingNat n -> MinPeano n n :~: n minIdempotency SZ = Refl minIdempotency (SS n) = minIdempotency n |- Refl -- | Proof that for naturals, @x >= y > z@ implies @x > z@ {-# RULES "transitivity" forall x y z. transitivity x y z = unsafeCoerce Refl #-} {-# INLINE[1] transitivity #-} transitivity :: (x >= y ~ 'True, y > z ~ 'True) => SingNat x -> SingNat y -> SingNat z -> x > z :~: 'True transitivity (SS _) (SS _) SZ = Refl transitivity (SS x) (SS y) (SS z) = transitivity x y z |- Refl ---------------------------------------------------------------------------- -- Helpers ---------------------------------------------------------------------------- toNatural :: Peano -> Natural toNatural Z = 0 toNatural (S x) = 1 + toNatural x someSingNat :: Natural -> SomeSing Peano someSingNat 0 = SomeSing SZ someSingNat n = case someSingNat (n - 1) of SomeSing sn -> SomeSing (SS sn)