{-# LANGUAGE UndecidableInstances #-}
module Dino.AST.Diff where
import Prelude
import Control.Monad (guard, zipWithM)
import Data.Coerce (coerce)
import Data.Foldable (asum)
import Data.Hashable (Hashable)
import qualified Data.HashMap.Strict as HM
import Data.List (sortOn)
import Data.Text (Text)
import qualified Data.Text as Text
import Text.PrettyPrint.ANSI.Leijen (Doc, Pretty (..), (<+>))
import qualified Text.PrettyPrint.ANSI.Leijen as PP
import Dino.Pretty
import Dino.AST
dropEnd :: Int -> [a] -> [a]
dropEnd n as = take (length as - n) as
data Replace a = Replace
{ original :: a
, new :: a
} deriving (Eq, Show, Functor)
data ElemOp a
= AddElem a
| RemoveElem a
| EditElem (Diff a)
deriving instance (Eq a, Eq (Diff a)) => Eq (ElemOp a)
deriving instance (Show a, Show (Diff a)) => Show (ElemOp a)
data EndOp a
= Append [a]
| DropEnd [a]
deriving (Eq, Show, Functor)
data ListOp a =
ListOp
[Maybe (Diff a)]
(Maybe (EndOp a))
deriving instance (Eq a, Eq (Diff a)) => Eq (ListOp a)
deriving instance (Show a, Show (Diff a)) => Show (ListOp a)
data Edit a
= Replacement (Replace (AST a))
| EditApp Constr [Maybe (Edit a)]
| EditList (Diff [AST a])
| EditLet (Diff (Text, AST a, AST a))
| EditRecord (Diff (Mapping Field (AST a)))
deriving (Eq, Show)
newtype Monolithic a = Monolithic {unMonolithic :: a}
class Diffable a where
type Diff a
type instance Diff a = Replace a
diff ::
a
-> a
-> Maybe (Diff a)
default diff :: (Eq a, Diff a ~ Replace a) => a -> a -> Maybe (Diff a)
diff original new = do
guard (original /= new)
return $ Replace {original, new}
applyDiff :: Diff a -> a -> Maybe a
default applyDiff :: (Eq a, Diff a ~ Replace a) => Diff a -> a -> Maybe a
applyDiff (Replace {original, new}) a
| a == original = Just new
| otherwise = Nothing
applyDiffWhen :: Diffable a => Maybe (Diff a) -> a -> Maybe a
applyDiffWhen Nothing a = Just a
applyDiffWhen (Just d) a = applyDiff d a
instance Diffable ()
instance Diffable Bool
instance Diffable Text
instance Diffable Int
instance Diffable Integer
instance Diffable Float
instance Diffable Double
instance Diffable Rational
instance Eq a => Diffable (Monolithic a) where
type Diff (Monolithic a) = Replace a
diff (Monolithic original) (Monolithic new) = do
guard (original /= new)
return $ Replace {original, new}
applyDiff (Replace {original, new}) a
| unMonolithic a == original = Just $ Monolithic new
| otherwise = Nothing
instance Diffable a => Diffable (Maybe a) where
type Diff (Maybe a) = ElemOp a
diff Nothing Nothing = Nothing
diff (Just a') Nothing = Just $ RemoveElem a'
diff Nothing (Just b') = Just $ AddElem b'
diff (Just a') (Just b') = EditElem <$> diff a' b'
applyDiff (RemoveElem _) Nothing = Nothing
applyDiff (RemoveElem _) (Just _) = Just Nothing
applyDiff (AddElem a) Nothing = Just (Just a)
applyDiff (AddElem _) (Just _) = Nothing
applyDiff (EditElem d) (Just a) = Just <$> applyDiff d a
applyDiff (EditElem _) Nothing = Nothing
instance Diffable a => Diffable [a] where
type Diff [a] = ListOp a
diff o n
| Nothing <- asum es, Nothing <- endOp = Nothing
| otherwise = Just $ ListOp es endOp
where
es = zipWith diff o n
lo = length o
ln = length n
endOp
| lo < ln = Just $ Append (drop lo n)
| ln < lo = Just $ DropEnd (dropEnd ln o)
| otherwise = Nothing
applyDiff (ListOp es endOp) as
| le < la, maybe False isAppend endOp = Nothing
| le <= la = applyEndOp <$> zipWithM applyDiffWhen es as
| otherwise = Nothing
where
le = length es
la = length as
isAppend (Append _) = True
isAppend _ = False
applyEndOp = case endOp of
Just (Append bs) -> (++ bs)
_ -> id
instance (Diffable a, Diffable b) => Diffable (a, b) where
type Diff (a, b) = (Maybe (Diff a), Maybe (Diff b))
diff (oa, ob) (na, nb)
| Nothing <- da, Nothing <- db = Nothing
| otherwise = Just (da, db)
where
da = diff oa na
db = diff ob nb
applyDiff (da, db) (a, b) = (,) <$> applyDiffWhen da a <*> applyDiffWhen db b
instance (Diffable a, Diffable b, Diffable c) => Diffable (a, b, c) where
type Diff (a, b, c) = (Maybe (Diff a), Maybe (Diff b), Maybe (Diff c))
diff (oa, ob, oc) (na, nb, nc)
| Nothing <- da, Nothing <- db, Nothing <- dc = Nothing
| otherwise = Just (da, db, dc)
where
da = diff oa na
db = diff ob nb
dc = diff oc nc
applyDiff (da, db, dc) (a, b, c) =
(,,) <$> applyDiffWhen da a <*> applyDiffWhen db b <*> applyDiffWhen dc c
instance (Eq k, Hashable k, Diffable a) => Diffable (Mapping k a) where
type Diff (Mapping k a) = Mapping k (ElemOp a)
diff (Mapping oi o) (Mapping ni n)
| null e = Nothing
| otherwise = Just $ Mapping (oi <> ni) e
where
e = flip HM.mapMaybeWithKey (HM.union o n) $ \k _ ->
diff (HM.lookup k o) (HM.lookup k n)
applyDiff (Mapping imp e) (Mapping _ m) =
fmap (Mapping imp . HM.union additions . HM.mapMaybe id) $
HM.traverseWithKey applyElem m
where
applyElem k v =
case HM.lookup k e of
Nothing -> Just $ Just v
Just (AddElem _) -> Nothing
Just (RemoveElem _) -> Just Nothing
Just (EditElem d) -> Just <$> applyDiff d v
additions =
flip HM.mapMaybe e $ \d ->
case d of
AddElem v -> Just v
_ -> Nothing
instance Eq a => Diffable (AST a) where
type Diff (AST a) = Edit a
diff (App List o) (App List n) = EditList <$> diff o n
diff (App co os) (App cn ns)
| co == cn && length os == length ns =
(\(ListOp es _) -> EditApp co es) <$> diff os ns
diff (Let vo o bo) (Let vn n bn) = EditLet <$> diff (vo, o, bo) (vn, n, bn)
diff (Record o) (Record n) = EditRecord <$> diff o n
diff o n = Replacement <$> diff (Monolithic o) (Monolithic n)
applyDiff (Replacement d) a = coerce $ applyDiff d (Monolithic a)
applyDiff (EditList e) (App List as) = App List <$> applyDiff e as
applyDiff (EditApp c es) (App c' as)
| c == c' = App c <$> applyDiff (ListOp es Nothing) as
applyDiff (EditLet e) (Let v a b) =
(\(v', a', b') -> Let v' a' b') <$> applyDiff e (v, a, b)
applyDiff (EditRecord e) (Record rec) = Record <$> applyDiff e rec
applyDiff _ _ = Nothing
instance {-# OVERLAPPING #-}
(Pretty a, Pretty (Diff a), Show k, Ord k) =>
Pretty (Mapping k (ElemOp a)) where
pretty (Mapping imp m) =
verticalList PP.lbrace PP.comma PP.rbrace $
map prettyField $ sortOn fst $ HM.toList m
where
prettyField (f, AddElem v) =
PP.green $
(PP.text "+" <+>
emphasize imp (PP.string (show f)) <+> PP.text "=") PP.<$>
PP.text " " <>
PP.align (pretty v)
prettyField (f, RemoveElem v) =
PP.red $
(PP.text "-" <+>
emphasize imp (PP.string (show f)) <+> PP.text "=") PP.<$>
PP.text " " <>
PP.align (pretty v)
prettyField (f, EditElem e) =
underHeader (emphasize imp (PP.string (show f)) <+> PP.text "=") $
pretty e
instance Pretty a => Pretty (Replace a) where
pretty Replace {original, new} =
PP.red (PP.char '-' <+> PP.align (pretty original)) PP.<$>
PP.green (PP.char '+' <+> PP.align (pretty new))
prettyEditTuple :: Pretty a => Doc -> Doc -> Doc -> [Maybe a] -> Doc
prettyEditTuple l sep r = verticalList l sep r . map (maybe unchanged pretty)
prettyEditApp :: Pretty a => NameType -> Text -> [Maybe a] -> Doc
prettyEditApp t c [] = prettyNamed t c
prettyEditApp t c as =
underHeader (prettyNamed t c) $ PP.vcat $ map (maybe unchanged pretty) as
instance (Pretty a, Pretty (Diff a)) => Pretty (ListOp a) where
pretty (ListOp es endOp) =
verticalList PP.lbracket PP.comma PP.rbracket (es' ++ os)
where
es' =
[ underHeader (PP.magenta $ PP.text ("edit @" <> show i)) (pretty e)
| (i :: Int, Just e) <- zip [0 ..] es
]
os = case endOp of
Nothing -> []
Just (Append vs) ->
[ underHeader (PP.magenta $ PP.text "append") $
PP.green (PP.char '+' <+> PP.align (pretty v))
| v <- vs
]
Just (DropEnd vs) ->
[ underHeader (PP.magenta $ PP.text "drop from end") $
PP.red (PP.char '-' <+> PP.align (pretty v))
| v <- vs
]
instance Show a => Pretty (Edit a) where
pretty (EditApp Tuple es) =
verticalList PP.lparen PP.comma PP.rparen $ map (maybe unchanged pretty) es
pretty (Replacement e) = pretty e
pretty (EditApp List es) = pretty $ EditList (ListOp es Nothing)
pretty (EditApp (Named t c) es) = prettyEditApp t c es
pretty (EditList e) = pretty e
pretty (EditLet (v, a, b)) =
underHeader (PP.string "let" PP.<+> PP.align var PP.<+> "=") (maybe unchanged pretty a)
PP.<$>
underHeader (PP.string " in") (maybe unchanged pretty b)
where
var = maybe unchanged (pretty . fmap (PP.string . Text.unpack)) v
pretty (EditRecord (Mapping imp erec)) =
verticalList PP.lbrace PP.comma PP.rbrace $
map prettyField $ sortOn fst $ HM.toList erec
where
prettyField (f, AddElem v) =
PP.green $
(PP.text "+" <+>
emphasize imp (PP.string (unField f)) <+> PP.text "=") PP.<$>
PP.text " " <>
PP.align (pretty v)
prettyField (f, RemoveElem v) =
PP.red $
(PP.text "-" <+>
emphasize imp (PP.string (unField f)) <+> PP.text "=") PP.<$>
PP.text " " <>
PP.align (pretty v)
prettyField (f, EditElem e) =
underHeader (emphasize imp (PP.string (unField f)) <+> PP.text "=") $
pretty e
printEdit :: Show a => Edit a -> IO ()
printEdit e = PP.putDoc (pretty e) >> putStrLn ""
diffAsTestResult :: Show a => Maybe (Edit a) -> Doc
diffAsTestResult Nothing = PP.green $ PP.string "OK"
diffAsTestResult (Just e) = underHeader (PP.red (PP.string "Fail")) (PP.pretty e)