{-# LANGUAGE QuantifiedConstraints #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE PolyKinds #-} {-# LANGUAGE GADTs #-} module Data.HDiff.Diff ( diffOpts' , diffOpts , diff , DiffMode(..) , module Data.HDiff.Diff.Types ) where import qualified Data.Set as S import Data.Void import Data.Functor.Const import Data.Functor.Sum import Control.Monad.State import Generics.MRSOP.Base import Generics.MRSOP.Holes import Generics.MRSOP.HDiff.Digest import qualified Data.WordTrie as T import Data.HDiff.Diff.Types import Data.HDiff.Diff.Modes import Data.HDiff.Diff.Preprocess import Data.HDiff.Patch import Data.HDiff.MetaVar import Data.HDiff.Change -- |We use a number of 'PrePatch'es, that is, a utx with a distinguished prefix -- and some pair of 'Holes's inside. type PrePatch ki codes phi = Holes ki codes (Holes ki codes phi :*: Holes ki codes phi) -- * Diffing -- -- |Given a merkelized fixpoint, builds a trie of hashes of -- every subtree, as long as they are taller than -- minHeight. This trie keeps track of the arity, so -- we can later annotate the trees that can be propper shares. buildArityTrie :: DiffOptions -> PrepFix a ki codes phi ix -> T.Trie Int buildArityTrie opts df = go df T.empty where ins :: Digest -> T.Trie Int -> T.Trie Int ins = T.insertWith 1 (+1) . toW64s minHeight = doMinHeight opts go :: PrepFix a ki codes phi ix -> T.Trie Int -> T.Trie Int go (HOpq (Const prep) _) t -- We only populat the sharing map if opaques are supposed -- to be handled as recursive trees | doOpaqueHandling opts == DO_AsIs = ins (treeDigest prep) t | otherwise = t -- TODO: think about holes. I'm posponing this until -- we actually use diffing things holes. go (Hole (Const _) _) t = t go (HPeel (Const prep) _ p) t | treeHeight prep <= minHeight = t | otherwise = ins (treeDigest prep) $ getConst $ cataNP (\af -> Const . go af . getConst) (Const t) p -- |Given two merkelized trees, returns the trie that indexes -- the subtrees that belong in both, ie, -- -- @forall t . t `elem` buildSharingTrie x y -- ==> t `subtree` x && t `subtree` y@ -- -- Moreover, we keep track of both the metavariable supposed -- to be associated with a tree and the tree's arity. -- buildSharingTrie :: DiffOptions -> PrepFix a ki codes phi ix -> PrepFix a ki codes phi ix -> (Int , IsSharedMap) buildSharingTrie opts x y = T.mapAccum (\i ar -> (i+1 , MAA i ar) ) 0 $ T.zipWith (+) (buildArityTrie opts x) (buildArityTrie opts y) -- |Given two treefixes, we will compute the longest path from -- the root that they overlap and will factor it out. -- This is somehow analogous to a @zipWith@. Moreover, however, -- we also copy the opaque values present in the spine by issuing -- /"copy"/ changes extractSpine :: forall ki codes phi at . (EqHO ki) => DiffOpaques -> (forall ix . phi ix -> MetaVarIK ki ix) -> Int -> Holes ki codes phi at -> Holes ki codes phi at -> Holes ki codes (Sum (OChange ki codes) (CChange ki codes)) at extractSpine dopq meta maxI dx dy = holesMap (uncurry' change) $ issueOpqCopiesSpine $ holesLCP dx dy where issueOpqCopiesSpine = flip evalState maxI . holesRefineAnnM (\_ (x :*: y) -> return $ Hole' $ holesMap meta x :*: holesMap meta y) (const $ if dopq == DO_OnSpine then doCopy else noCopy) noCopy :: ki k -> State Int (PrePatch ki codes (MetaVarIK ki) ('K k)) noCopy kik = return (HOpq' kik) doCopy :: ki k -> State Int (PrePatch ki codes (MetaVarIK ki) ('K k)) doCopy ki = do i <- get put (i+1) let ann = NA_K . Annotate i $ ki return $ Hole' (Hole' ann :*: Hole' ann) -- |Combines changes until they are closed close :: Holes ki codes (Sum (OChange ki codes) (CChange ki codes)) at -> Holes ki codes (CChange ki codes) at close utx = case closure utx of InR cc -> cc InL (OMatch used unused del ins) -> Hole' $ CMatch (S.union used unused) del ins where closure :: Holes ki codes (Sum (OChange ki codes) (CChange ki codes)) at -> Sum (OChange ki codes) (Holes ki codes (CChange ki codes)) at closure (HOpq _ k) = InR $ HOpq' k closure (Hole _ (InL oc)) = InL oc closure (Hole _ (InR cc)) = InR $ Hole' cc -- There is some magic going on here. First we compute -- the recursive closures and check whether any of them is open. -- If not, we are done. -- Otherwise, we apply a bunch of "monad distributive properties" around. closure (HPeel _ cx dx) = let aux = mapNP closure dx in case mapNPM fromInR aux of Just np -> InR $ HPeel' cx np Nothing -> let chgs = mapNP (either' InL (InR . distrCChange)) aux dels = mapNP (either' oCtxDel cCtxDel) chgs inss = mapNP (either' oCtxIns cCtxIns) chgs vx = S.unions $ elimNP (either'' oCtxVDel cCtxVars) chgs vy = S.unions $ elimNP (either'' oCtxVIns cCtxVars) chgs in if vx == vy then InR (Hole' $ CMatch vx (HPeel' cx dels) (HPeel' cx inss)) else InL (OMatch vx vy (HPeel' cx dels) (HPeel' cx inss)) fromInR :: Sum f g x -> Maybe (g x) fromInR (InL _) = Nothing fromInR (InR x) = Just x -- |Diffs two generic merkelized structures. -- The outline of the process is: -- -- i) Annotate each tree with the info we need (digest and height) -- ii) Build the sharing trie -- iii) Identify the proper shares -- iv) Substitute the proper shares by a metavar in -- both the source and deletion context -- v) Extract the spine and compute the closure. -- diffOpts' :: forall ki codes phi at . (EqHO ki , DigestibleHO ki , DigestibleHO phi) => DiffOptions -> Holes ki codes phi at -> Holes ki codes phi at -> (Int , Delta (Holes ki codes (Sum phi (MetaVarIK ki))) at) diffOpts' opts x y = let dx = preprocess x dy = preprocess y (i, sh) = buildSharingTrie opts dx dy dx' = tagProperShare sh dx dy' = tagProperShare sh dy delins = extractHoles (doMode opts) mkCanShare sh (dx' :*: dy') in (i , delins) where mkCanShare :: forall a ix . PrepFix a ki codes phi ix -> Bool mkCanShare (HOpq _ _) = doOpaqueHandling opts == DO_AsIs mkCanShare pr = doMinHeight opts < treeHeight (getConst $ holesAnn pr) -- |When running the diff for two fixpoints, we can -- cast the resulting deletion and insertion context into -- an actual patch. diffOpts :: (EqHO ki , DigestibleHO ki , IsNat ix) => DiffOptions -> Fix ki codes ix -> Fix ki codes ix -> Patch ki codes ix diffOpts opts x y = let (i , del :*: ins) = diffOpts' opts (na2holes $ NA_I x) (na2holes $ NA_I y) in close (extractSpine (doOpaqueHandling opts) cast i del ins) where cast :: Sum (Const Void) f i -> f i cast (InR fi) = fi cast (InL _) = error "impossible" diff :: (EqHO ki , DigestibleHO ki , IsNat ix) => MinHeight -> Fix ki codes ix -> Fix ki codes ix -> Patch ki codes ix diff h = diffOpts (diffOptionsDefault { doMinHeight = h})