cabal-gild-0.2.0.1: source/library/VersionInterval.hs
{-# LANGUAGE BangPatterns #-}
-- |
--
-- Author: Oleg Grenrus <oleg.grenrus@iki.fi>
-- SPDX-License-Id: GPL-2.0-or-later
--
-- This module implements a view of a 'VersionRange' as a finite
-- list of separated version intervals.
--
-- In conversion from and to 'VersionRange' it makes some effort to
-- preserve the caret operator @^>=x.y@. This constraint a priori
-- specifies the same interval as @==x.y.*@, but indicates that newer
-- versions could be acceptable (@allow-newer: ^@).
module VersionInterval
( -- * Version intervals
VersionIntervals,
-- * Conversions
toVersionIntervals,
fromVersionIntervals,
ConversionProblem (..),
-- ** Normalisation
normaliseVersionRange,
-- * Version intervals view
VersionInterval (..),
LB (..),
MB (..),
UB (..),
Bound (..),
-- * For testing
validVersionInterval,
validVersionIntervals,
intersectInterval,
stage1,
stage2,
stage3,
)
where
import Control.Monad (join)
import Data.List (sortOn)
import Data.List.NonEmpty (NonEmpty (..), cons)
import Data.Maybe (catMaybes)
import Distribution.Types.Version
( Version,
mkVersion,
validVersion,
version0,
versionNumbers,
)
import Distribution.Types.VersionRange.Internal
( VersionRange,
VersionRangeF (..),
cataVersionRange,
earlierVersion,
intersectVersionRanges,
majorBoundVersion,
majorUpperBound,
noVersion,
orLaterVersion,
thisVersion,
unionVersionRanges,
)
singleton :: a -> NonEmpty a
singleton x = x :| []
-------------------------------------------------------------------------------
-- Data
-------------------------------------------------------------------------------
-- | A complementary representation of a 'VersionRange'. Instead of a boolean
-- version predicate it uses an increasing sequence of non-overlapping,
-- non-empty intervals.
--
-- This version is different than in @Cabal-3.8@ and previous,
-- as it tries to preserve @^>=@ version ranges under default and @transformCaretUpper@ semantics.
-- Slighly simplifying, 'normalizeVersionRange' shouldn't destroy @^>=@ in version range expressions.
newtype VersionIntervals = VersionIntervals [VersionInterval]
deriving (Eq, Show)
-- | Version interval.
--
-- Invariants:
--
-- * Interval is non-empty
-- * 'MB' is between 'LB' and 'UB'.
data VersionInterval = VI !LB !MB !UB
deriving (Eq, Show)
-- | Lower bound. For intervals it always exist: 'zeroLB' i.e. @>= 0@.
--
-- All lower bound intervals are inclusive, i.e. @>=v@. @>x.y.z@ is converted into @>=x.y.z.0@.
newtype LB = LB Version
deriving (Eq, Ord, Show)
-- | Upper bound.
--
-- All upper bounds are exclusive, i.e. @<v@. @<=x.y.z@ is converted to @<x.y.z.0@.
data UB
= -- | upper bound
UB !Version
| -- | no upper bound (i.e. infinite)
NoUB
deriving (Eq, Ord, Show)
-- | Bound variant.
data Bound
= -- | inclusive: @>=@ or @<=@
Incl
| -- | exclusive: @>@ or @<@
Excl
deriving (Eq, Ord, Show)
-- | Middle bound.
data MB
= -- | major bound.
MB !Version
| -- | no major bound (i.e. infinite)
NoMB
deriving (Eq, Ord, Show)
-- | @>=0@
zeroLB :: LB
zeroLB = LB version0
-- | Whether the version is @0@.
isVersion0 :: Version -> Bool
isVersion0 = (==) version0
-- | Versions are not separated type.
succVersion :: Version -> Version
succVersion v = mkVersion (versionNumbers v ++ [0])
-------------------------------------------------------------------------------
-- Stage1
-------------------------------------------------------------------------------
stage1 :: ([VersionInterval] -> [VersionInterval]) -> VersionRange -> [VersionInterval]
stage1 opt = cataVersionRange alg
where
-- version range leafs transform into singleton intervals
alg (ThisVersionF v) = [VI (LB v) (MB (succVersion v)) (UB (succVersion v))]
alg (LaterVersionF v) = [VI (LB (succVersion v)) NoMB NoUB]
alg (OrLaterVersionF v) = [VI (LB v) NoMB NoUB]
alg (EarlierVersionF v)
| isVersion0 v = []
| otherwise = [VI zeroLB (MB v) (UB v)]
alg (OrEarlierVersionF v) = [VI zeroLB (MB (succVersion v)) (UB (succVersion v))]
-- \^>= version-range's upper bound should be MajorBound
alg (MajorBoundVersionF v) = [VI (LB v) (MB (majorUpperBound v)) NoUB]
-- union: just merge the version intervals
alg (UnionVersionRangesF v1 v2) = v1 ++ v2
-- intersection: pairwise intersect. Strip empty intervals. Sort to restore the invariant.
alg (IntersectVersionRangesF v1 v2) = catMaybes $ intersectInterval <$> opt v1 <*> opt v2
-------------------------------------------------------------------------------
-- Stage2
-------------------------------------------------------------------------------
stage2 :: [VersionInterval] -> [VersionInterval]
stage2 = sortOn (\(VI l _ _) -> l)
-------------------------------------------------------------------------------
-- Postprocess
-------------------------------------------------------------------------------
stage2and3 :: [VersionInterval] -> [VersionInterval]
stage2and3 = stage3 . stage2
stage3 :: [VersionInterval] -> [VersionInterval]
stage3 [] = []
stage3 (VI l m u : rest) = stage3go l m u rest
stage3go :: LB -> MB -> UB -> [VersionInterval] -> [VersionInterval]
stage3go l m u [] = [VI l m u]
stage3go l m u (VI l' m' u' : is)
| l == l' = stage3go l' (unionMB m m') (unionUB u u') is
| otherwise = case overlap m u l' of
NoOverlap -> VI l m u : stage3go l' m' u' is
OverlapU -> viCons (VI l m (trimLB u l')) (stage3go l' m' (unionUB u u') is)
OverlapM -> stage3go l (unionMB m m') (unionUB u u') is
where
viCons :: VersionInterval -> [VersionInterval] -> [VersionInterval]
viCons i | nonEmptyVI i = (i :)
viCons _ = id
trimLB :: UB -> LB -> UB
trimLB _ (LB l) = UB l
-------------------------------------------------------------------------------
-- Intersections
-------------------------------------------------------------------------------
intersectInterval :: VersionInterval -> VersionInterval -> Maybe VersionInterval
intersectInterval (VI xl xm xu) (VI yl ym yu)
| nonEmptyVI xy = Just xy
| otherwise = Nothing
where
l = intersectLB xl yl
m = intersectMB xm ym
u = intersectUB xu yu
-- make middle bound be between l and u
m' = rtrimMB (ltrimMB l m) u
xy = VI l m' u
ltrimMB :: LB -> MB -> MB
ltrimMB _ NoMB = NoMB
ltrimMB (LB l) (MB m) = case compare l m of
LT -> MB m
EQ -> MB m
GT -> MB l
rtrimMB :: MB -> UB -> MB
rtrimMB m NoUB = m
rtrimMB NoMB (UB u) = MB u
rtrimMB (MB m) (UB u) = MB (min m u)
intersectLB :: LB -> LB -> LB
intersectLB (LB v) (LB u) = LB (max v u)
intersectMB :: MB -> MB -> MB
intersectMB NoMB b = b
intersectMB b NoMB = b
intersectMB (MB v) (MB u) = MB (min v u)
intersectUB :: UB -> UB -> UB
intersectUB NoUB b = b
intersectUB b NoUB = b
intersectUB (UB v) (UB u) = UB (min v u)
intersectMBandUB :: MB -> UB -> UB
intersectMBandUB NoMB b = b
intersectMBandUB (MB v) NoUB = UB v
intersectMBandUB (MB v) (UB u) = UB (min v u)
-------------------------------------------------------------------------------
-- Unions
-------------------------------------------------------------------------------
unionMB :: MB -> MB -> MB
unionMB NoMB _ = NoMB
unionMB _ NoMB = NoMB
unionMB (MB v) (MB u) = MB (max v u)
unionUB :: UB -> UB -> UB
unionUB NoUB _ = NoUB
unionUB _ NoUB = NoUB
unionUB (UB v) (UB u) = UB (max v u)
-------------------------------------------------------------------------------
-- Helpers
-------------------------------------------------------------------------------
-- | Overlaps.
data Overlap
= -- | no overlap, next interval's @l@ is greater than @u@
NoOverlap
| -- | overlaps, next interval's @l@ is less than @m@
OverlapM
| -- | overlaps, next interval's @l@ is less than @u@ (but greater than @m@)
OverlapU
deriving (Eq, Show)
overlap :: MB -> UB -> LB -> Overlap
overlap _ (UB u) (LB l) | u < l = NoOverlap
overlap (MB m) _ (LB l) | m < l = OverlapU
overlap _ _ _ = OverlapM
-------------------------------------------------------------------------------
-- Invariants
-------------------------------------------------------------------------------
-- | 'VersionIntervals' invariant:
--
-- * all intervals are valid (lower bound is less then upper bound, middle bound is in between)
-- * intervals doesn't touch each other (distinct)
validVersionIntervals :: VersionIntervals -> Bool
validVersionIntervals (VersionIntervals intervals) =
all validVersionInterval intervals
&& all doesNotTouch' (pairs intervals)
where
doesNotTouch' :: (VersionInterval, VersionInterval) -> Bool
doesNotTouch' (VI l m u, VI l' _ _) =
l < l' && case overlap m u l' of
NoOverlap -> True
OverlapM -> False
OverlapU -> case u of
NoUB -> True
UB uv -> case l' of LB lv -> uv == lv
pairs :: [a] -> [(a, a)]
pairs xs = zip xs (drop 1 xs)
validLB :: LB -> Bool
validLB (LB v) = validVersion v
validUB :: UB -> Bool
validUB NoUB = True
validUB (UB v) = validVersion v
validMB :: MB -> Bool
validMB NoMB = True
validMB (MB v) = validVersion v
validVersionInterval :: VersionInterval -> Bool
validVersionInterval i@(VI l m u) = validLB l && validMB m && validUB u && nonEmptyVI i && lbLessThanMB l m && mbLessThanUB m u
mbLessThanUB :: MB -> UB -> Bool
mbLessThanUB (MB m) (UB u) = m <= u
mbLessThanUB NoMB (UB _) = False
mbLessThanUB _ NoUB = True
lbLessThanMB :: LB -> MB -> Bool
lbLessThanMB _ NoMB = True
lbLessThanMB (LB l) (MB m) = l <= m
-- Check an interval is non-empty
--
nonEmptyVI :: VersionInterval -> Bool
nonEmptyVI (VI (LB _) _ NoUB) = True
nonEmptyVI (VI (LB l) _ (UB u)) = l < u
-------------------------------------------------------------------------------
-- Conversions
-------------------------------------------------------------------------------
-- | Convert a 'VersionRange' to a sequence of version intervals.
toVersionIntervals :: VersionRange -> VersionIntervals
toVersionIntervals = VersionIntervals . stage2and3 . stage1 stage2and3
data ConversionProblem
= IntervalsEmpty
| OtherConversionProblem
deriving (Eq, Show)
-- | Convert a 'VersionIntervals' value back into a 'VersionRange' expression
-- representing the version intervals.
fromVersionIntervals :: VersionIntervals -> Either ConversionProblem VersionRange
fromVersionIntervals (VersionIntervals []) = Right noVersion
fromVersionIntervals (VersionIntervals (x : xs)) =
case join <$> traverse intervalToVersionRange (preprocess x xs) of
Just vrs -> Right (foldr1 unionVersionRanges vrs)
Nothing ->
Left $
if all seemsEmpty (x : xs)
then IntervalsEmpty
else OtherConversionProblem
where
-- we can remove upper bounds, if they touch next interval, and the next interval doesn't have upper bound
preprocess :: VersionInterval -> [VersionInterval] -> NonEmpty VersionInterval
preprocess i [] = i :| []
preprocess i@(VI l m u) (j : js) = case u' of
NoUB | touchesUB u l' -> cons (VI l m NoUB) js'
_ -> cons i js'
where
js'@(VI l' _ u' :| _) = preprocess j js
seemsEmpty :: VersionInterval -> Bool
seemsEmpty (VI l m u) = not (nonEmptyVI (VI l NoMB (intersectMBandUB m u)))
touchesUB :: UB -> LB -> Bool
touchesUB NoUB _ = True
touchesUB (UB u) (LB l) = u >= l
lbToVR :: LB -> VersionRange
lbToVR (LB l) = orLaterVersion l
ubToVR :: UB -> VersionRange -> VersionRange
ubToVR NoUB vr = vr
ubToVR (UB u) vr = intersectVersionRanges vr (earlierVersion u)
mbEqUB :: MB -> UB -> Bool
mbEqUB NoMB NoUB = True
mbEqUB NoMB (UB _) = False
mbEqUB (MB m) (UB u) = m == u
mbEqUB (MB _) NoUB = False
-- return the unions of version ranges.
intervalToVersionRange :: VersionInterval -> Maybe (NonEmpty VersionRange)
intervalToVersionRange (VI l m u) | mbEqUB m u = Just (singleton (intervalToVersionRange1 l u))
intervalToVersionRange (VI l m u) = fmap (fmap (ubToVR u)) (intervalToVersionRange2 l m)
intervalToVersionRange1 :: LB -> UB -> VersionRange
intervalToVersionRange1 (LB v) upper' = case upper' of
NoUB ->
lowerBound
UB u
| succVersion v == u ->
thisVersion v
UB u -> withLowerBound (makeUpperBound u)
where
lowerBound :: VersionRange
lowerBound = lbToVR (LB v)
withLowerBound :: VersionRange -> VersionRange
withLowerBound vr
| isVersion0 v = vr
| otherwise = intersectVersionRanges lowerBound vr
makeUpperBound :: Version -> VersionRange
makeUpperBound = earlierVersion
intervalToVersionRange2 :: LB -> MB -> Maybe (NonEmpty VersionRange)
intervalToVersionRange2 (LB l) NoMB = Just (singleton lowerBound)
where
lowerBound :: VersionRange
lowerBound = lbToVR (LB l)
intervalToVersionRange2 (LB l) (MB m)
| supermajor l == supermajor m =
go (l :|) (majorUpperBound l)
| [a, b] <- versionNumbers m,
let m' = mkVersion [a, b - 1],
b >= 1,
m' > l =
Just $
ubToVR (UB m') (lbToVR (LB l))
:| [majorBoundVersion (mkVersion [a, b - 1])]
| otherwise =
Nothing
where
go :: ([Version] -> NonEmpty Version) -> Version -> Maybe (NonEmpty VersionRange)
go !acc v = case compare v m of
LT -> go (snoc acc v) (majorUpperBound v)
EQ -> Just (fmap majorBoundVersion (acc []))
GT -> Nothing
snoc :: ([a] -> c) -> a -> [a] -> c
snoc xs x = xs . (x :)
supermajor :: Version -> Int
supermajor v = case versionNumbers v of
[] -> -1
s : _ -> s
-------------------------------------------------------------------------------
-- Normalisation
-------------------------------------------------------------------------------
-- | Convert 'VersionRange' to 'VersionIntervals' and back.
normaliseVersionRange :: VersionRange -> Either ConversionProblem VersionRange
normaliseVersionRange = fromVersionIntervals . toVersionIntervals