Skip to content

Commit

Permalink
Merge pull request #3798 from unisonweb/delete-many-3673
Browse files Browse the repository at this point in the history
Enables deletion of multiple terms and types in one command
  • Loading branch information
rlmark authored Feb 22, 2023
2 parents 6c6fb53 + 02ac7cd commit b5fca58
Show file tree
Hide file tree
Showing 10 changed files with 527 additions and 81 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,8 @@ dist-newstyle
# GHC
*.hie
*.prof

# Mac developers
**/.DS_Store

/libb2.dylib
196 changes: 128 additions & 68 deletions unison-cli/src/Unison/Codebase/Editor/HandleInput.hs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}

{-# HLINT ignore "Use tuple-section" #-}
module Unison.Codebase.Editor.HandleInput
( loop,
)
Expand Down Expand Up @@ -117,6 +120,7 @@ import qualified Unison.Codebase.Metadata as Metadata
import Unison.Codebase.Patch (Patch (..))
import qualified Unison.Codebase.Patch as Patch
import Unison.Codebase.Path (Path, Path' (..))
import qualified Unison.Codebase.Path as HQSplit'
import qualified Unison.Codebase.Path as Path
import qualified Unison.Codebase.Path.Parse as Path
import Unison.Codebase.PushBehavior (PushBehavior)
Expand Down Expand Up @@ -145,6 +149,7 @@ import Unison.Hash32 (Hash32)
import qualified Unison.Hash32 as Hash32
import qualified Unison.HashQualified as HQ
import qualified Unison.HashQualified' as HQ'
import qualified Unison.HashQualified' as HashQualified
import qualified Unison.Hashing.V2.Convert as Hashing
import Unison.LabeledDependency (LabeledDependency)
import qualified Unison.LabeledDependency as LD
Expand Down Expand Up @@ -349,44 +354,6 @@ loop e = do
names <- displayNames uf
ppe <- PPE.suffixifiedPPE <$> prettyPrintEnvDecl names
Cli.respond $ Typechecked (Text.pack sourceName) ppe sr uf

delete ::
DeleteOutput ->
((Path.Absolute, HQ'.HQSegment) -> Cli (Set Referent)) -> -- compute matching terms
((Path.Absolute, HQ'.HQSegment) -> Cli (Set Reference)) -> -- compute matching types
Path.HQSplit' ->
Cli ()
delete doutput getTerms getTypes hq' = do
hq <- Cli.resolveSplit' hq'
terms <- getTerms hq
types <- getTypes hq
when (Set.null terms && Set.null types) (Cli.returnEarly (NameNotFound hq'))
-- Mitchell: stripping hash seems wrong here...
resolvedPath <- Path.convert <$> Cli.resolveSplit' (HQ'.toName <$> hq')
rootNames <- Branch.toNames <$> Cli.getRootBranch0
let name = Path.unsafeToName (Path.unsplit resolvedPath)
toRel :: (Ord ref) => Set ref -> R.Relation Name ref
toRel = R.fromList . fmap (name,) . toList
-- these names are relative to the root
toDelete = Names (toRel terms) (toRel types)
endangerments <- Cli.runTransaction (getEndangeredDependents toDelete rootNames)
if null endangerments
then do
let makeDeleteTermNames = map (BranchUtil.makeDeleteTermName resolvedPath) . Set.toList $ terms
let makeDeleteTypeNames = map (BranchUtil.makeDeleteTypeName resolvedPath) . Set.toList $ types
before <- Cli.getRootBranch0
description <- inputDescription input
Cli.stepManyAt description (makeDeleteTermNames ++ makeDeleteTypeNames)
case doutput of
DeleteOutput'Diff -> do
after <- Cli.getRootBranch0
(ppe, diff) <- diffHelper before after
Cli.respondNumbered (ShowDiffAfterDeleteDefinitions ppe diff)
DeleteOutput'NoDiff -> do
Cli.respond Success
else do
ppeDecl <- currentPrettyPrintEnvDecl Backend.Within
Cli.respondNumbered (CantDeleteDefinitions ppeDecl endangerments)
in Cli.time "InputPattern" case input of
ApiI -> do
Cli.Env {serverBaseUrl} <- ask
Expand Down Expand Up @@ -500,7 +467,6 @@ loop e = do
headb <- getBranch headRepo
mergedb <- liftIO (Branch.merge'' (Codebase.lca codebase) Branch.RegularMerge baseb headb)
squashedb <- liftIO (Branch.merge'' (Codebase.lca codebase) Branch.SquashMerge headb baseb)
-- Perform all child updates in a single step.
Cli.updateAt description destAbs $ Branch.step \destBranch0 ->
destBranch0
& Branch.children
Expand Down Expand Up @@ -881,9 +847,9 @@ loop e = do
]
Cli.respond Success
DeleteI dtarget -> case dtarget of
DeleteTarget'TermOrType doutput hq -> delete doutput Cli.getTermsAt Cli.getTypesAt hq
DeleteTarget'Type doutput hq -> delete doutput (const (pure Set.empty)) Cli.getTypesAt hq
DeleteTarget'Term doutput hq -> delete doutput Cli.getTermsAt (const (pure Set.empty)) hq
DeleteTarget'TermOrType doutput hqs -> delete input doutput Cli.getTermsAt Cli.getTypesAt hqs
DeleteTarget'Type doutput hqs -> delete input doutput (const (pure Set.empty)) Cli.getTypesAt hqs
DeleteTarget'Term doutput hqs -> delete input doutput Cli.getTermsAt (const (pure Set.empty)) hqs
DeleteTarget'Patch src' -> do
_ <- Cli.expectPatchAt src'
description <- inputDescription input
Expand All @@ -910,7 +876,7 @@ loop e = do
(Branch.toNames (Branch.head branch))
afterDelete <- do
rootNames <- Branch.toNames <$> Cli.getRootBranch0
endangerments <- Cli.runTransaction (getEndangeredDependents toDelete rootNames)
endangerments <- Cli.runTransaction (getEndangeredDependents toDelete Set.empty rootNames)
case (null endangerments, insistence) of
(True, _) -> pure (Cli.respond Success)
(False, Force) -> do
Expand Down Expand Up @@ -1473,24 +1439,24 @@ inputDescription input =
pure ("copy.patch " <> src <> " " <> dest)
DeleteI dtarget -> do
case dtarget of
DeleteTarget'TermOrType DeleteOutput'NoDiff thing0 -> do
thing <- hqs' thing0
pure ("delete " <> thing)
DeleteTarget'TermOrType DeleteOutput'Diff thing0 -> do
thing <- hqs' thing0
pure ("delete.verbose " <> thing)
DeleteTarget'Term DeleteOutput'NoDiff thing0 -> do
thing <- hqs' thing0
pure ("delete.term " <> thing)
DeleteTarget'Term DeleteOutput'Diff thing0 -> do
thing <- hqs' thing0
pure ("delete.term.verbose " <> thing)
DeleteTarget'TermOrType DeleteOutput'NoDiff things0 -> do
thing <- traverse hqs' things0
pure ("delete " <> Text.intercalate " " thing)
DeleteTarget'TermOrType DeleteOutput'Diff things0 -> do
thing <- traverse hqs' things0
pure ("delete.verbose " <> Text.intercalate " " thing)
DeleteTarget'Term DeleteOutput'NoDiff things0 -> do
thing <- traverse hqs' things0
pure ("delete.term " <> Text.intercalate " " thing)
DeleteTarget'Term DeleteOutput'Diff things0 -> do
thing <- traverse hqs' things0
pure ("delete.term.verbose " <> Text.intercalate " " thing)
DeleteTarget'Type DeleteOutput'NoDiff thing0 -> do
thing <- hqs' thing0
pure ("delete.type " <> thing)
thing <- traverse hqs' thing0
pure ("delete.type " <> Text.intercalate " " thing)
DeleteTarget'Type DeleteOutput'Diff thing0 -> do
thing <- hqs' thing0
pure ("delete.type.verbose " <> thing)
thing <- traverse hqs' thing0
pure ("delete.type.verbose " <> Text.intercalate " " thing)
DeleteTarget'Branch Try opath0 -> do
opath <- ops' opath0
pure ("delete.namespace " <> opath)
Expand Down Expand Up @@ -2965,24 +2931,118 @@ loadPropagateDiffDefaultPatch inputDescription maybeDest0 dest = do
(ppe, diff) <- diffHelper original (Branch.head patched)
Cli.respondNumbered (ShowDiffAfterMergePropagate dest0 dest patchPath ppe diff)

delete ::
Input ->
DeleteOutput ->
((Path.Absolute, HQ'.HQSegment) -> Cli (Set Referent)) -> -- compute matching terms
((Path.Absolute, HQ'.HQSegment) -> Cli (Set Reference)) -> -- compute matching types
[Path.HQSplit'] -> -- targets for deletion
Cli ()
delete input doutput getTerms getTypes hqs' = do
-- persists the original hash qualified entity for error reporting
typesTermsTuple <-
traverse
( \hq -> do
absolute <- Cli.resolveSplit' hq
types <- getTypes absolute
terms <- getTerms absolute
return (hq, types, terms)
)
hqs'
let notFounds = List.filter (\(_, types, terms) -> Set.null terms && Set.null types) typesTermsTuple
-- if there are any entities which cannot be deleted because they don't exist, short circuit.
if not $ null notFounds
then do
let toName :: [(Path.HQSplit', Set Reference, Set referent)] -> [Name]
toName notFounds =
mapMaybe (\(split, _, _) -> Path.toName' $ HashQualified.toName (HQSplit'.unsplitHQ' split)) notFounds
Cli.returnEarly $ NamesNotFound (toName notFounds)
else do
checkDeletes typesTermsTuple doutput input

checkDeletes :: [(Path.HQSplit', Set Reference, Set Referent)] -> DeleteOutput -> Input -> Cli ()
checkDeletes typesTermsTuples doutput inputs = do
let toSplitName ::
(Path.HQSplit', Set Reference, Set Referent) ->
Cli (Path.Split, Name, Set Reference, Set Referent)
toSplitName hq = do
resolvedPath <- Path.convert <$> Cli.resolveSplit' (HQ'.toName <$> hq ^. _1)
return (resolvedPath, Path.unsafeToName (Path.unsplit resolvedPath), hq ^. _2, hq ^. _3)
-- get the splits and names with terms and types
splitsNames <- traverse toSplitName typesTermsTuples
let toRel :: (Ord ref) => Set ref -> Name -> R.Relation Name ref
toRel setRef name = R.fromList (fmap (name,) (toList setRef))
let toDelete = fmap (\(_, names, types, terms) -> Names (toRel terms names) (toRel types names)) splitsNames
-- make sure endangered is compeletely contained in paths
rootNames <- Branch.toNames <$> Cli.getRootBranch0
-- get only once for the entire deletion set
let allTermsToDelete :: Set LabeledDependency
allTermsToDelete = Set.unions (fmap Names.labeledReferences toDelete)
-- get the endangered dependencies for each entity to delete
endangered <-
Cli.runTransaction $
traverse
( \targetToDelete ->
getEndangeredDependents targetToDelete (allTermsToDelete) rootNames
)
toDelete
-- If the overall dependency map is not completely empty, abort deletion
let endangeredDeletions = List.filter (\m -> not $ null m || Map.foldr (\s b -> null s || b) False m) endangered
if null endangeredDeletions
then do
let deleteTypesTerms =
splitsNames
>>= ( \(split, _, types, terms) ->
(map (BranchUtil.makeDeleteTypeName split) . Set.toList $ types)
++ (map (BranchUtil.makeDeleteTermName split) . Set.toList $ terms)
)
before <- Cli.getRootBranch0
description <- inputDescription inputs
Cli.stepManyAt description deleteTypesTerms
case doutput of
DeleteOutput'Diff -> do
after <- Cli.getRootBranch0
(ppe, diff) <- diffHelper before after
Cli.respondNumbered (ShowDiffAfterDeleteDefinitions ppe diff)
DeleteOutput'NoDiff -> do
Cli.respond Success
else do
ppeDecl <- currentPrettyPrintEnvDecl Backend.Within
let combineRefs = List.foldl (Map.unionWith NESet.union) Map.empty endangeredDeletions
Cli.respondNumbered (CantDeleteDefinitions ppeDecl combineRefs)

-- | Goal: When deleting, we might be removing the last name of a given definition (i.e. the
-- definition is going "extinct"). In this case we may wish to take some action or warn the
-- user about these "endangered" definitions which would now contain unnamed references.
-- The argument `otherDesiredDeletions` is included in this function because the user might want to
-- delete a term and all its dependencies in one command, so we give this function access to
-- the full set of entities that the user wishes to delete.
getEndangeredDependents ::
-- | Which names we want to delete
-- | Prospective target for deletion
Names ->
-- | All entities we want to delete (including the target)
Set LabeledDependency ->
-- | All names from the root branch
Names ->
-- | map from references going extinct to the set of endangered dependents
Sqlite.Transaction (Map LabeledDependency (NESet LabeledDependency))
getEndangeredDependents namesToDelete rootNames = do
getEndangeredDependents targetToDelete otherDesiredDeletions rootNames = do
-- names of terms left over after target deletion
let remainingNames :: Names
remainingNames = rootNames `Names.difference` namesToDelete
refsToDelete, remainingRefs, extinct :: Set LabeledDependency
refsToDelete = Names.labeledReferences namesToDelete
remainingRefs = Names.labeledReferences remainingNames -- left over after delete
extinct = refsToDelete `Set.difference` remainingRefs -- deleting and not left over
accumulateDependents :: LabeledDependency -> Sqlite.Transaction (Map LabeledDependency (Set LabeledDependency))
remainingNames = rootNames `Names.difference` targetToDelete
-- target refs for deletion
let refsToDelete :: Set LabeledDependency
refsToDelete = Names.labeledReferences targetToDelete
-- refs left over after deleting target
let remainingRefs :: Set LabeledDependency
remainingRefs = Names.labeledReferences remainingNames
-- remove the other targets for deletion from the remaining terms
let remainingRefsWithoutOtherTargets :: Set LabeledDependency
remainingRefsWithoutOtherTargets = Set.difference remainingRefs otherDesiredDeletions
-- deleting and not left over
let extinct :: Set LabeledDependency
extinct = refsToDelete `Set.difference` remainingRefs
let accumulateDependents :: LabeledDependency -> Sqlite.Transaction (Map LabeledDependency (Set LabeledDependency))
accumulateDependents ld =
let ref = LD.fold id Referent.toReference ld
in Map.singleton ld . Set.map LD.termRef <$> Codebase.dependents Queries.ExcludeOwnComponent ref
Expand All @@ -2995,7 +3055,7 @@ getEndangeredDependents namesToDelete rootNames = do
let extinctToEndangered :: Map LabeledDependency (NESet LabeledDependency)
extinctToEndangered =
allDependentsOfExtinct & Map.mapMaybe \endangeredDeps ->
let remainingEndangered = endangeredDeps `Set.intersection` remainingRefs
let remainingEndangered = endangeredDeps `Set.intersection` remainingRefsWithoutOtherTargets
in NESet.nonEmptySet remainingEndangered
pure extinctToEndangered

Expand Down
6 changes: 3 additions & 3 deletions unison-cli/src/Unison/Codebase/Editor/Input.hs
Original file line number Diff line number Diff line change
Expand Up @@ -270,9 +270,9 @@ data DeleteOutput
deriving stock (Eq, Show)

data DeleteTarget
= DeleteTarget'TermOrType DeleteOutput Path.HQSplit'
| DeleteTarget'Term DeleteOutput Path.HQSplit'
| DeleteTarget'Type DeleteOutput Path.HQSplit'
= DeleteTarget'TermOrType DeleteOutput [Path.HQSplit']
| DeleteTarget'Term DeleteOutput [Path.HQSplit']
| DeleteTarget'Type DeleteOutput [Path.HQSplit']
| DeleteTarget'Branch Insistence (Maybe Path.Split')
| DeleteTarget'Patch Path.Split'
deriving stock (Eq, Show)
2 changes: 2 additions & 0 deletions unison-cli/src/Unison/Codebase/Editor/Output.hs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ data Output
| BranchNotFound Path'
| EmptyPush Path'
| NameNotFound Path.HQSplit'
| NamesNotFound [Name]
| PatchNotFound Path.Split'
| TypeNotFound Path.HQSplit'
| TermNotFound Path.HQSplit'
Expand Down Expand Up @@ -365,6 +366,7 @@ isFailure o = case o of
BadNamespace {} -> True
BranchNotFound {} -> True
NameNotFound {} -> True
NamesNotFound _ -> True
PatchNotFound {} -> True
TypeNotFound {} -> True
TypeNotFound' {} -> True
Expand Down
11 changes: 5 additions & 6 deletions unison-cli/src/Unison/CommandLine/InputPatterns.hs
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,7 @@ renameType =
"`rename.type` takes two arguments, like `rename.type oldname newname`."
)

deleteGen :: Maybe String -> String -> (Path.HQSplit' -> DeleteTarget) -> InputPattern
deleteGen :: Maybe String -> String -> ([Path.HQSplit'] -> DeleteTarget) -> InputPattern
deleteGen suffix target mkTarget =
let cmd = maybe "delete" ("delete." <>) suffix
info =
Expand All @@ -631,11 +631,10 @@ deleteGen suffix target mkTarget =
[(OnePlus, exactDefinitionTermQueryArg)]
info
( \case
[query] -> first fromString $ do
p <- Path.parseHQSplit' query
pure $ Input.DeleteI (mkTarget p)
_ ->
Left . P.warnCallout $ P.wrap warn
[] -> Left . P.warnCallout $ P.wrap warn
queries -> first fromString $ do
paths <- traverse Path.parseHQSplit' queries
pure $ Input.DeleteI (mkTarget paths)
)

delete :: InputPattern
Expand Down
20 changes: 20 additions & 0 deletions unison-cli/src/Unison/CommandLine/OutputMessages.hs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,21 @@ import qualified Unison.Builtin.Decls as DD
import Unison.Codebase.Editor.DisplayObject (DisplayObject (BuiltinObject, MissingObject, UserObject))
import qualified Unison.Codebase.Editor.Input as Input
import Unison.Codebase.Editor.Output
( DisplayDefinitionsOutput (..),
NumberedArgs,
NumberedOutput (..),
Output (..),
ShareError
( ShareErrorCheckAndSetPush,
ShareErrorFastForwardPush,
ShareErrorGetCausalHashByPath,
ShareErrorPull,
ShareErrorTransport
),
TestReportStats (CachedTests, NewlyComputed),
UndoFailureReason (CantUndoPastMerge, CantUndoPastStart),
WhichBranchEmpty (..),
)
import qualified Unison.Codebase.Editor.Output as E
import qualified Unison.Codebase.Editor.Output.BranchDiff as OBD
import qualified Unison.Codebase.Editor.Output.PushPull as PushPull
Expand Down Expand Up @@ -782,6 +797,11 @@ notifyUser dir o = case o of
pure . P.warnCallout $ "I don't know about that patch."
NameNotFound _ ->
pure . P.warnCallout $ "I don't know about that name."
NamesNotFound hqs ->
pure $
P.warnCallout "The following names were not found in the codebase. Check your spelling."
<> P.newline
<> (P.syntaxToColor $ P.indent " " (P.lines (fmap prettyName hqs)))
TermNotFound _ ->
pure . P.warnCallout $ "I don't know about that term."
TypeNotFound _ ->
Expand Down
3 changes: 2 additions & 1 deletion unison-src/transcripts/delete-silent.output.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
⚠️
I don't know about that name.
The following names were not found in the codebase. Check your spelling.
foo
```
```unison
Expand Down
Loading

0 comments on commit b5fca58

Please sign in to comment.