|
| 1 | +-- | @diff.update@ input handler - shows a preview of what `update` would change. |
| 2 | +module Unison.Codebase.Editor.HandleInput.DiffUpdate |
| 3 | + ( handleDiffUpdate, |
| 4 | + ) |
| 5 | +where |
| 6 | + |
| 7 | +import Control.Monad.Reader.Class (ask) |
| 8 | +import Data.Map.Strict qualified as Map |
| 9 | +import Data.Set qualified as Set |
| 10 | +import U.Codebase.Reference (TermReferenceId, TypeReferenceId) |
| 11 | +import Unison.Cli.Monad (Cli, Env (..)) |
| 12 | +import Unison.Cli.Monad qualified as Cli |
| 13 | +import Unison.Cli.MonadUtils qualified as Cli |
| 14 | +import Unison.Cli.UpdateUtils (getNamespaceDependentsOf, hydrateRefs) |
| 15 | +import Unison.Codebase qualified as Codebase |
| 16 | +import Unison.Codebase.Branch qualified as Branch |
| 17 | +import Unison.Codebase.Branch.Names qualified as Branch |
| 18 | +import Unison.Codebase.Editor.Output qualified as Output |
| 19 | +import Unison.DataDeclaration (Decl, DeclOrBuiltin) |
| 20 | +import Unison.DeclCoherencyCheck qualified as DeclCoherencyCheck |
| 21 | +import Unison.Name (Name) |
| 22 | +import Unison.Names (Names (Names)) |
| 23 | +import Unison.Names qualified as Names |
| 24 | +import Unison.OrBuiltin (OrBuiltin (..)) |
| 25 | +import Unison.Parser.Ann (Ann) |
| 26 | +import Unison.Prelude |
| 27 | +import Unison.PrettyPrintEnv.Names qualified as PPE |
| 28 | +import Unison.PrettyPrintEnvDecl qualified as PPED |
| 29 | +import Unison.Reference qualified as Reference |
| 30 | +import Unison.Referent qualified as Referent |
| 31 | +import Unison.Symbol (Symbol) |
| 32 | +import Unison.Syntax.Name qualified as Name |
| 33 | +import Unison.Term (Term) |
| 34 | +import Unison.Type (Type) |
| 35 | +import Unison.UnconflictedLocalDefnsView (UnconflictedLocalDefnsView (..)) |
| 36 | +import Unison.UnisonFile qualified as UF |
| 37 | +import Unison.UnisonFile.Names qualified as UF |
| 38 | +import Unison.Util.BiMultimap qualified as BiMultimap |
| 39 | +import Unison.Util.Defns (Defns (..), DefnsF) |
| 40 | +import Unison.Util.Relation qualified as Relation |
| 41 | + |
| 42 | +handleDiffUpdate :: Cli () |
| 43 | +handleDiffUpdate = do |
| 44 | + env <- ask |
| 45 | + tuf <- Cli.expectLatestTypecheckedFile |
| 46 | + currentBranch <- Cli.getCurrentBranch |
| 47 | + let currentBranch0 = Branch.head currentBranch |
| 48 | + let namesIncludingLibdeps = Branch.toNames currentBranch0 |
| 49 | + |
| 50 | + -- Assert that the namespace doesn't have any conflicted names |
| 51 | + unconflictedView <- |
| 52 | + Branch.asUnconflicted currentBranch0 |
| 53 | + & onLeft (Cli.returnEarly . Output.ConflictedDefn) |
| 54 | + |
| 55 | + -- Assert that the namespace doesn't have any incoherent decls |
| 56 | + _declNameLookup <- |
| 57 | + Cli.runTransactionWithRollback \rollback -> do |
| 58 | + Codebase.getBranchDeclNameLookup env.codebase (Branch.namespaceHash currentBranch) unconflictedView |
| 59 | + & onLeftM (rollback . Output.IncoherentDeclDuringUpdate . DeclCoherencyCheck.asOneRandomIncoherentDeclReason) |
| 60 | + |
| 61 | + -- Get namespace bindings from the file (terms and types being added/updated) |
| 62 | + let namespaceBindings :: DefnsF Set Name Name |
| 63 | + namespaceBindings = |
| 64 | + bimap (Set.map Name.unsafeParseVar) (Set.map Name.unsafeParseVar) (UF.namespaceBindings tuf) |
| 65 | + |
| 66 | + -- Compute new vs updated definitions |
| 67 | + let existingTermNames = BiMultimap.ran unconflictedView.defns.terms |
| 68 | + let existingTypeNames = BiMultimap.ran unconflictedView.defns.types |
| 69 | + |
| 70 | + let newTermNames = Set.difference namespaceBindings.terms existingTermNames |
| 71 | + let newTypeNames = Set.difference namespaceBindings.types existingTypeNames |
| 72 | + let updatedTermNames = Set.intersection namespaceBindings.terms existingTermNames |
| 73 | + let updatedTypeNames = Set.intersection namespaceBindings.types existingTypeNames |
| 74 | + |
| 75 | + -- Get dependents that would need retypechecking |
| 76 | + dependents <- |
| 77 | + Cli.runTransaction do |
| 78 | + dependents0 <- |
| 79 | + getNamespaceDependentsOf |
| 80 | + unconflictedView.defns |
| 81 | + ( Names.references |
| 82 | + Names |
| 83 | + { terms = Relation.restrictDom namespaceBindings.terms unconflictedView.names.terms, |
| 84 | + types = Relation.restrictDom namespaceBindings.types unconflictedView.names.types |
| 85 | + } |
| 86 | + ) |
| 87 | + |
| 88 | + -- Remove dependents that are also being updated directly by the file, |
| 89 | + -- since they'll already appear in the "updated definitions" section |
| 90 | + let dependents1 :: DefnsF (Map Name) TermReferenceId TypeReferenceId |
| 91 | + dependents1 = |
| 92 | + bimap |
| 93 | + (`Map.withoutKeys` namespaceBindings.terms) |
| 94 | + (`Map.withoutKeys` namespaceBindings.types) |
| 95 | + dependents0 |
| 96 | + |
| 97 | + pure dependents1 |
| 98 | + |
| 99 | + -- Get the terms (body + type + refId) for new and updated terms from the typechecked file |
| 100 | + -- hashTermsId returns: (ann, TermReferenceId, Maybe WatchKind, Term v a, Type v a) |
| 101 | + let fileTermsWithRefIds :: Map Name (TermReferenceId, Term Symbol Ann, Type Symbol Ann) |
| 102 | + fileTermsWithRefIds = |
| 103 | + Map.fromList |
| 104 | + [ (Name.unsafeParseVar var, (refId, term, typ)) |
| 105 | + | (var, (_, refId, _, term, typ)) <- Map.toList (UF.hashTermsId tuf) |
| 106 | + ] |
| 107 | + |
| 108 | + let fileTerms :: Map Name (Term Symbol Ann, Type Symbol Ann) |
| 109 | + fileTerms = Map.map (\(_, term, typ) -> (term, typ)) fileTermsWithRefIds |
| 110 | + |
| 111 | + let newTerms :: Map Name (Term Symbol Ann, Type Symbol Ann) |
| 112 | + newTerms = Map.restrictKeys fileTerms newTermNames |
| 113 | + |
| 114 | + -- Terms from the file that are updates to existing codebase definitions (with new ref IDs) |
| 115 | + let updatedFileTerms :: Map Name (TermReferenceId, Term Symbol Ann, Type Symbol Ann) |
| 116 | + updatedFileTerms = Map.restrictKeys fileTermsWithRefIds updatedTermNames |
| 117 | + |
| 118 | + -- Get the old terms from the codebase for updated definitions |
| 119 | + -- First, get the term reference IDs for the updated names |
| 120 | + let updatedTermRefIds :: Map Name TermReferenceId |
| 121 | + updatedTermRefIds = |
| 122 | + Map.fromList |
| 123 | + [ (name, refId) |
| 124 | + | name <- Set.toList updatedTermNames, |
| 125 | + Just referent <- [Map.lookup name (BiMultimap.range unconflictedView.defns.terms)], |
| 126 | + Just refId <- [Referent.toTermReferenceId referent] |
| 127 | + ] |
| 128 | + |
| 129 | + -- Fetch the old terms from the codebase |
| 130 | + oldTerms <- Cli.runTransaction do |
| 131 | + let refIdSet = Set.fromList (Map.elems updatedTermRefIds) |
| 132 | + hydratedTerms <- hydrateRefs env.codebase (Defns refIdSet Set.empty) |
| 133 | + pure hydratedTerms.terms |
| 134 | + |
| 135 | + -- Intersect old and new terms to find updated definitions |
| 136 | + -- Only include terms where the reference ID actually changed |
| 137 | + let updatedTerms :: Map Name ((Term Symbol Ann, Type Symbol Ann), (Term Symbol Ann, Type Symbol Ann)) |
| 138 | + updatedTerms = |
| 139 | + Map.mapMaybe id $ |
| 140 | + Map.intersectionWith |
| 141 | + ( \oldRefId (newRefId, newTerm, newTyp) -> |
| 142 | + -- Skip terms where the hash hasn't changed (they're not actually updated) |
| 143 | + if oldRefId == newRefId |
| 144 | + then Nothing |
| 145 | + else case Map.lookup oldRefId oldTerms of |
| 146 | + Just oldTerm -> Just (oldTerm, (newTerm, newTyp)) |
| 147 | + Nothing -> Nothing |
| 148 | + ) |
| 149 | + updatedTermRefIds |
| 150 | + updatedFileTerms |
| 151 | + |
| 152 | + -- Get type declarations from the file (including reference IDs) |
| 153 | + let fileDataDecls :: Map Name (DeclOrBuiltin Symbol Ann) |
| 154 | + fileDataDecls = |
| 155 | + Map.fromList |
| 156 | + [ (Name.unsafeParseVar var, NotBuiltin (Right decl)) |
| 157 | + | (var, (_, decl)) <- Map.toList (UF.dataDeclarationsId' tuf) |
| 158 | + ] |
| 159 | + |
| 160 | + let fileEffectDecls :: Map Name (DeclOrBuiltin Symbol Ann) |
| 161 | + fileEffectDecls = |
| 162 | + Map.fromList |
| 163 | + [ (Name.unsafeParseVar var, NotBuiltin (Left decl)) |
| 164 | + | (var, (_, decl)) <- Map.toList (UF.effectDeclarationsId' tuf) |
| 165 | + ] |
| 166 | + |
| 167 | + let fileTypeDecls :: Map Name (DeclOrBuiltin Symbol Ann) |
| 168 | + fileTypeDecls = Map.union fileDataDecls fileEffectDecls |
| 169 | + |
| 170 | + -- File types with their reference IDs (for updated types rendering) |
| 171 | + let fileTypeDeclsWithRefIds :: Map Name (TypeReferenceId, Decl Symbol Ann) |
| 172 | + fileTypeDeclsWithRefIds = |
| 173 | + Map.fromList $ |
| 174 | + [ (Name.unsafeParseVar var, (refId, Right decl)) |
| 175 | + | (var, (refId, decl)) <- Map.toList (UF.dataDeclarationsId' tuf) |
| 176 | + ] |
| 177 | + ++ [ (Name.unsafeParseVar var, (refId, Left decl)) |
| 178 | + | (var, (refId, decl)) <- Map.toList (UF.effectDeclarationsId' tuf) |
| 179 | + ] |
| 180 | + |
| 181 | + let newTypes :: Map Name (DeclOrBuiltin Symbol Ann) |
| 182 | + newTypes = Map.restrictKeys fileTypeDecls newTypeNames |
| 183 | + |
| 184 | + -- Types from the file that are updates to existing codebase definitions |
| 185 | + let updatedFileTypes :: Map Name (TypeReferenceId, Decl Symbol Ann) |
| 186 | + updatedFileTypes = Map.restrictKeys fileTypeDeclsWithRefIds updatedTypeNames |
| 187 | + |
| 188 | + -- Get the old types from the codebase for updated definitions |
| 189 | + -- First, get the type reference IDs for the updated names |
| 190 | + let updatedTypeRefIds :: Map Name TypeReferenceId |
| 191 | + updatedTypeRefIds = |
| 192 | + Map.fromList |
| 193 | + [ (name, refId) |
| 194 | + | name <- Set.toList updatedTypeNames, |
| 195 | + Just typeRef <- [Map.lookup name (BiMultimap.range unconflictedView.defns.types)], |
| 196 | + Just refId <- [Reference.toId typeRef] |
| 197 | + ] |
| 198 | + |
| 199 | + -- Fetch the old types from the codebase |
| 200 | + oldTypes <- Cli.runTransaction do |
| 201 | + let refIdSet = Set.fromList (Map.elems updatedTypeRefIds) |
| 202 | + hydratedTypes <- hydrateRefs env.codebase (Defns Set.empty refIdSet) |
| 203 | + pure hydratedTypes.types |
| 204 | + |
| 205 | + -- Intersect old and new types to find updated definitions |
| 206 | + -- Only include types where the reference ID actually changed |
| 207 | + -- Result: Map Name ((old refId, old decl), (new refId, new decl)) |
| 208 | + let updatedTypes :: Map Name ((TypeReferenceId, Decl Symbol Ann), (TypeReferenceId, Decl Symbol Ann)) |
| 209 | + updatedTypes = |
| 210 | + Map.mapMaybe id $ |
| 211 | + Map.intersectionWith |
| 212 | + ( \oldRefId (newRefId, newDecl) -> |
| 213 | + -- Skip types where the hash hasn't changed (they're not actually updated) |
| 214 | + if oldRefId == newRefId |
| 215 | + then Nothing |
| 216 | + else case Map.lookup oldRefId oldTypes of |
| 217 | + Just oldDecl -> Just ((oldRefId, oldDecl), (newRefId, newDecl)) |
| 218 | + Nothing -> Nothing |
| 219 | + ) |
| 220 | + updatedTypeRefIds |
| 221 | + updatedFileTypes |
| 222 | + |
| 223 | + -- Build the PPEs: |
| 224 | + -- - ppedNew: for new definitions (file names shadowing namespace names) |
| 225 | + -- - ppedOld: for old definitions (just namespace names, so old refs resolve properly) |
| 226 | + let fileNames = UF.typecheckedToNames tuf |
| 227 | + let allNames = fileNames `Names.shadowing` namesIncludingLibdeps |
| 228 | + let ppedNew = |
| 229 | + PPED.makePPED |
| 230 | + (PPE.hqNamer 10 allNames) |
| 231 | + (PPE.suffixifyByHash allNames) |
| 232 | + let ppedOld = |
| 233 | + PPED.makePPED |
| 234 | + (PPE.hqNamer 10 namesIncludingLibdeps) |
| 235 | + (PPE.suffixifyByHash namesIncludingLibdeps) |
| 236 | + |
| 237 | + -- Respond with the diff |
| 238 | + Cli.respond $ |
| 239 | + Output.ShowUpdateDiff |
| 240 | + ppedNew |
| 241 | + ppedOld |
| 242 | + Defns {terms = newTerms, types = newTypes} |
| 243 | + Defns {terms = updatedTerms, types = updatedTypes} |
| 244 | + dependents |
0 commit comments