diff-loc-0.1.0.0: src/DiffLoc/Diff.hs
{-# LANGUAGE
AllowAmbiguousTypes,
DerivingStrategies,
FlexibleContexts,
FlexibleInstances,
GeneralizedNewtypeDeriving,
MultiParamTypeClasses,
ScopedTypeVariables,
StandaloneDeriving,
TypeApplications,
TypeFamilies,
UndecidableInstances #-}
-- | Mapping intervals across diffs
module DiffLoc.Diff
( -- * Types
ADiff()
-- * Operations
, emptyDiff
, addDiff
, mapDiff
, comapDiff
, listToDiff
) where
import Data.Coerce
import Data.Foldable (toList)
import Data.Maybe (fromMaybe)
import Data.FingerTree (FingerTree)
import Text.Show.Combinators (showCon, (@|))
import qualified Data.FingerTree as FT
import DiffLoc.Shift
-- $setup
-- >>> import Control.Monad ((<=<))
-- >>> import Test.QuickCheck
-- >>> import Test.QuickCheck.HigherOrder
-- >>> import DiffLoc
-- >>> import DiffLoc.Unsafe
-- >>> import DiffLoc.Test
-- >>> type NN' = Colline N N'
-- >>> quickCheck = quickCheckWith' stdArgs{maxSuccess=3000}
-- | A diff represents a transformation from one file to another.
--
-- Example diff between "abcdefgh" and "appcfgzzh":
--
-- > source ab cdefg h
-- > - b de
-- > + pp zz
-- > target appc fgzzh
--
-- It consists of three replacements:
--
-- 1. replace "b" with "pp" at location 1, @mkReplace 1 1 2@;
-- 2. replace "de" with "" at location 3, @mkReplace 3 2 0@;
-- 3. replace "" with "zz" at location 7, @mkReplace 7 0 2@.
--
-- >>> :{
-- let d :: Diff N
-- d = addDiff (Replace 1 (offset 1) (offset 2)) -- at location 1, replace "b" (length 1) with "pp" (length 2)
-- $ addDiff (Replace 3 (offset 2) (offset 0)) -- at location 3, replace "de" with ""
-- $ addDiff (Replace 7 (offset 0) (offset 2)) -- at location 7, replace "" with "zz"
-- $ emptyDiff
-- -- N.B.: replacements should be inserted right to left.
-- :}
--
-- 'ADiff' is an abstract representation to be instantiated with
-- a concrete representation of atomic replacements.
--
-- == __Internal details__
--
-- Internally, a diff is a sequence of /disjoint/ and /nonempty/ replacements,
-- /ordered/ by their source locations.
-- The monoid annotation in the fingertree gives the endpoints of the replacements.
newtype ADiff r = ADiff (FingerTree (Maybe r) (R r))
deriving Eq
instance Show r => Show (ADiff r) where
showsPrec = flip $ \d -> showCon "listToDiff" @| diffToList d
-- | The empty diff.
emptyDiff :: Semigroup r => ADiff r
emptyDiff = ADiff FT.empty
-- | A newtype to carry a 'FT.Measured' instance.
newtype R r = R r
deriving newtype (Eq, Show)
instance Semigroup r => FT.Measured (Maybe r) (R r) where
measure (R r) = Just r
coshiftR' :: Shift r => Maybe r -> r -> r
coshiftR' Nothing = id
coshiftR' (Just r) = fromMaybe (error "failed to shift disjoint intervals") . coshiftR r
addDiffL :: forall r. Shift r => r -> ADiff r -> ADiff r
addDiffL r (ADiff d0) = case FT.viewl d0 of
FT.EmptyL -> ADiff (FT.singleton (R r))
R s FT.:< d | src r `distantlyPrecedes` src s -> ADiff (R r FT.<| d0)
| otherwise -> addDiffL (r <> s) (ADiff d)
-- | Add a replacement to a diff. The replacement is performed /after/ the diff.
--
-- === Properties
--
-- prop> not (isZeroLength x) ==> mapDiff (addDiff r d) x == (shiftBlock r <=< mapDiff (d :: Diff N)) x
-- prop> not (isZeroLength x) ==> comapDiff (addDiff r d) x == (comapDiff d <=< coshiftBlock (r :: Replace N)) x
addDiff :: forall r. Shift r => r -> ADiff r -> ADiff r
addDiff r (ADiff d) = case FT.search (\r1 _-> r1 `notPrecedes_` r) d of
FT.Position d1 s d2 -> coerce (d1 <>) (addDiffL (coshiftR' (FT.measure d1) r) (ADiff (s FT.<| d2)))
FT.OnLeft -> addDiffL r (ADiff d)
FT.OnRight -> ADiff (d FT.|> R (coshiftR' (FT.measure d) r))
FT.Nowhere -> error "Broken invariant"
where
notPrecedes_ Nothing _ = False
notPrecedes_ (Just r1) i = not (tgt r1 `distantlyPrecedes` tgt i)
-- Using distantlyPrecedes here and in addDiffL lets us merge adjacent intervals.
-- $hidden
-- prop> not (isZeroLength x) ==> mapDiff (addDiff r d) x == (shiftBlock r <=< mapDiff (d :: Diff NN')) x
-- prop> not (isZeroLength x) ==> comapDiff (addDiff r d) x == (comapDiff d <=< coshiftBlock (r :: Replace NN')) x
-- | Translate a span in the source of a diff to a span in its target.
-- @Nothing@ if the span overlaps with a replacement.
--
-- For exaple, given the following 'ADiff' (or 'DiffLoc.Interval.Replace') from "aAacCc" to "aAabbbcCc":
--
-- > source aAa cCc
-- > -
-- > + bbb
-- > target aAabbbcCc
--
-- >>> r0 = Replace 3 (offset 0) (offset 3) :: Replace N
-- >>> d0 = addDiff r0 emptyDiff
--
-- The span of \"A\" remains unchanged.
--
-- >>> mapDiff d0 (1 :.. offset 1)
-- Just (1 :.. offset 1)
-- >>> shiftBlock r0 (1 :.. offset 1)
-- Just (1 :.. offset 1)
-- >>> comapDiff d0 (1 :.. offset 1)
-- Just (1 :.. offset 1)
-- >>> coshiftBlock r0 (1 :.. offset 1)
-- Just (1 :.. offset 1)
--
-- The span of \"C\" is shifted by 3 characters.
--
-- >>> mapDiff d0 (4 :.. offset 1)
-- Just (7 :.. offset 1)
-- >>> shiftBlock r0 (4 :.. offset 1)
-- Just (7 :.. offset 1)
-- >>> comapDiff d0 (7 :.. offset 1)
-- Just (4 :.. offset 1)
-- >>> coshiftBlock r0 (7 :.. offset 1)
-- Just (4 :.. offset 1)
--
-- The span of "ac" overlaps with the replacement, so the mapping is undefined.
--
-- >>> mapDiff d0 (2 :.. offset 2)
-- Nothing
-- >>> shiftBlock r0 (2 :.. offset 2)
-- Nothing
-- >>> comapDiff d0 (2 :.. offset 5)
-- Nothing
-- >>> coshiftBlock r0 (2 :.. offset 5)
-- Nothing
--
-- === Properties
--
-- prop> \(FSN d s) -> not (isZeroLength s) ==> partialSemiInverse (mapDiff d) (comapDiff d) s
-- prop> \(FSN d s) -> not (isZeroLength s) ==> partialSemiInverse (comapDiff d) (mapDiff d) s
--
-- where @partialSemiInverse f g x@ is the property
--
-- > if f x == Just y -- for some y
-- > then g y == Just x
mapDiff :: Shift r => ADiff r -> Block r -> Maybe (Block r)
mapDiff = mapDiff_ Cov
-- $hidden
--
-- prop> \(FSV d s) -> not (isZeroLength s) ==> partialSemiInverse (mapDiff d) (comapDiff d) s
-- prop> \(FSV d s) -> not (isZeroLength s) ==> partialSemiInverse (comapDiff d) (mapDiff d) s
-- | Translate a span in the target of a diff to a span in its source.
-- @Nothing@ if the span overlaps with a replacement.
--
-- See also 'mapDiff'.
comapDiff :: Shift r => ADiff r -> Block r -> Maybe (Block r)
comapDiff = mapDiff_ Contrav
data Variance = Cov | Contrav
srcV :: Shift r => Variance -> r -> Block r
srcV Cov = src
srcV Contrav = tgt
shiftBlockV' :: Shift r => Variance -> Maybe r -> Block r -> Block r
shiftBlockV' _ Nothing = id
shiftBlockV' Cov (Just r) = fromMaybe (error "failed to shift disjoint intervals") . shiftBlock r
shiftBlockV' Contrav (Just r) = fromMaybe (error "failed to shift disjoint intervals") . coshiftBlock r
mapDiff_ :: forall r. Shift r => Variance -> ADiff r -> Block r -> Maybe (Block r)
mapDiff_ v (ADiff d) i = case FT.search (\r1 _ -> r1 `notPrecedes_` i) d of
FT.Position d1 (R s) _
| j `precedes` (srcV v s) -> Just i'
| otherwise -> Nothing
where i' = shiftBlockV' v (FT.measure d1) i
j = case v of Cov -> i ; Contrav -> i'
FT.OnLeft -> Just i
FT.OnRight -> Just (shiftBlockV' v (FT.measure d) i)
FT.Nowhere -> error "Broken invariant"
where
notPrecedes_ Nothing _ = False
notPrecedes_ (Just r1) i1 = not (srcV v r1 `precedes` i1)
-- |
--
-- @
-- 'listToDiff' = foldr 'addDiff' 'emptyDiff'
-- @
listToDiff :: Shift r => [r] -> ADiff r
listToDiff = foldr addDiff emptyDiff
diffToList :: ADiff r -> [r]
diffToList (ADiff d) = coerce (toList d)