1 # ws/moved: a workspace member imported by its hash, once its hash moved 2 # (EZ-WS-7). A member that publishes under no name is imported by its `0x` 3 # hash, and editing it moves that hash, so an import line written before the 4 # edit names the hash the lock being rewritten records for it. A plain lock 5 # writes no source (EZ-DOC-5), so it refuses, before it asks anything, and 6 # names the upgrade that rewrites the line. That upgrade rewrites every line 7 # naming a moved member's recorded hash, the way an upgrade rewrites a moved 8 # dependency's (EZ-VEN-2, EZ-VEN-3), and since a member whose files it 9 # rewrote has a new hash in turn, it goes round again until no member it 10 # moves moves any further. Everything here is pure: ws/up.bend plans with it. 11 import Base 12 import ../ledger/manifest.bend as M 13 import ../ledger/upgrade.bend as U 14 import ../lock/lock.bend as L 15 import ../lock/world.bend as LW 16 import ../lock/plan.bend as LP 17 import ../pkg/pkg.bend as K 18 import ../toml/toml.bend as T 19 import ./joint.bend as J 20 21 # a member's package: the directory it is written from, the name the joint 22 # ledger gives it ("" where the lock is read, which records none), and its 23 # hash 24 type Held is Data: 25 Held{path: String, name: String, hash: String} 26 27 # a member whose hash moves: its name, the hash the lock records, and the 28 # hash it has now 29 type Shift is Data: 30 Shift{name: String, old: String, new: String} 31 32 # --------------------------------------------------------------------------- 33 # what the lock records, and what the members are now 34 35 # a package of the lock being rewritten, when it is a member's 36 def locked.src(src: L.Src, +hash: String) -> List<&2, Held>: 37 match src: 38 case L.Hub{}: 39 [] 40 case L.Git{_url, _rev, _entry, _root, _nar, _tag}: 41 [] 42 case L.Member{path, _entry}: 43 [Held{path, "", hash}] 44 45 # every member package of a lock's packages 46 def locked.all(ps: List<&2, L.Pack>) -> List<&2, Held>: 47 match ps: 48 case []: 49 [] 50 case L.Pack{+hash, src, _files} <> tl: 51 List.append(&2, Held, locked.src(src, hash), locked.all(tl)) 52 53 # every member package the lock being rewritten records, from its text 54 def locked(+text: String) -> List<&2, Held>: 55 locked.all(L.packs(T.sects(T.parse(text)))) 56 57 # a dependency of the joint ledger, when it is a member's 58 def now.src(src: M.Source, +name: String, +hash: String) -> List<&2, Held>: 59 match src: 60 case M.Hub{_nv}: 61 [] 62 case M.Git{_url, _rev, _tag, _root, _nar, _vend}: 63 [] 64 case M.Member{path, _nv}: 65 [Held{path, name, hash}] 66 67 # every member the joint ledger depends on 68 def now.all(ds: List<&2, M.Dep>) -> List<&2, Held>: 69 match ds: 70 case []: 71 [] 72 case M.Dep{+nn, +hh, _ee, src} <> tl: 73 List.append(&2, Held, now.src(src, nn, hh), now.all(tl)) 74 75 # every member the joint ledger depends on, or none when there is none 76 def now(jj: J.Joint) -> List<&2, Held>: 77 match jj: 78 case J.Lone{}: 79 [] 80 case J.Clash{_why}: 81 [] 82 case J.Joint{mm}: 83 now.all(J.deps.of(mm)) 84 85 # the directory a member is written from 86 def held.path(hd: Held) -> String: 87 Held{hp, _hn, _hh} = hd 88 hp 89 90 # the member written from a directory, once it is known whether the head is 91 def at.pick(hit: Bool, hd: Held, rest: Maybe<&2, Held>) -> Maybe<&2, Held>: 92 match hit: 93 case True{}: 94 Some{hd} 95 case False{}: 96 rest 97 98 # the first member written from a directory, or none 99 def at(hs: List<&2, Held>, +path: String) -> Maybe<&2, Held>: 100 match hs: 101 case []: 102 None{} 103 case +hd <> tl: 104 rest = at(tl, path) 105 at.pick(String.eq(held.path(hd), path), hd, rest) 106 107 # a member's hash, or "" for none 108 def hash.of(found: Maybe<&2, Held>) -> String: 109 match found: 110 case None{}: 111 "" 112 case Some{hd}: 113 Held{_hp, _hn, hh} = hd 114 hh 115 116 # --------------------------------------------------------------------------- 117 # which members move 118 119 # a move kept, once it is known whether it is one 120 def move.put(hit: Bool, +name: String, +old: String, +new: String, rest: List<&2, Shift>) -> List<&2, Shift>: 121 match hit: 122 case True{}: 123 Shift{name, old, new} <> rest 124 case False{}: 125 rest 126 127 # the member the lock records at a directory, against what it is now: it 128 # moves when its hash is not the one the lock records, and it is chosen 129 # (`--package`, or every member when none is given) or its hash is not the 130 # one it had before any line was rewritten, which is a member whose files 131 # the upgrade rewrote 132 def move.found( 133 found: Maybe<&2, Held>, 134 +base: String, 135 +old: String, 136 +only: String, 137 rest: List<&2, Shift> 138 ) -> List<&2, Shift>: 139 match found: 140 case None{}: 141 rest 142 case Some{hd}: 143 Held{_hp, +name, +new} = hd 144 move.put(Bool.and(Bool.not(String.eq(new, old)), 145 Bool.or(U.chosen(only, name), Bool.not(String.eq(new, base)))), name, old, new, rest) 146 147 # every member the lock records that moves: `now` the members as they are, 148 # `base` as they were before any line was rewritten 149 def moves( 150 old: List<&2, Held>, 151 +now: List<&2, Held>, 152 +base: List<&2, Held>, 153 +only: String 154 ) -> List<&2, Shift>: 155 match old: 156 case []: 157 [] 158 case Held{+path, _name, +hash} <> tl: 159 rest = moves(tl, now, base, only) 160 move.found(at(now, path), hash.of(at(base, path)), hash, only, rest) 161 162 # the moves as the rewrites they make 163 def swaps(ms: List<&2, Shift>) -> List<&2, U.Swap>: 164 match ms: 165 case []: 166 [] 167 case Shift{_name, old, new} <> tl: 168 U.Swap{old, new} <> swaps(tl) 169 170 # every committed file with every moved hash rewritten 171 def rewrite(+ss: List<&2, U.Swap>, fs: List<&2, K.Source>) -> List<&2, K.Source>: 172 match fs: 173 case []: 174 [] 175 case K.Source{at, text} <> tl: 176 K.Source{at, U.reimport.many(ss, text)} <> rewrite(ss, tl) 177 178 # --------------------------------------------------------------------------- 179 # a plain lock: a line naming a moved member's recorded hash 180 181 # why a file stops a plain lock: the first move one of its lines names 182 def stale.moves(ms: List<&2, Shift>, +at: String, +text: String) -> String: 183 match ms: 184 case []: 185 "" 186 case Shift{+name, +old, +new} <> tl: 187 rest = stale.moves(tl, at, text) 188 Bool.pick(String, String.eq(U.reimport.many([U.Swap{old, new}], text), text), rest, 189 "ez: " ++ at ++ " imports workspace member " ++ name ++ " by " ++ old ++ 190 ", the hash the lock records for it, and the member is " ++ new ++ 191 " now; ez lock --upgrade --package " ++ name ++ " rewrites the import") 192 193 # why a plain lock refuses: the first committed file with a line naming a 194 # moved member's recorded hash, "" when none does 195 def stale(+ms: List<&2, Shift>, fs: List<&2, K.Source>) -> String: 196 match fs: 197 case []: 198 "" 199 case K.Source{+at, +text} <> tl: 200 +here = stale.moves(ms, at, text) 201 rest = stale(ms, tl) 202 Bool.pick(String, String.is_empty(here), rest, here) 203 204 # the members of a ledger's text, as the joint ledger renders them 205 def now.read(rd: M.Read) -> List<&2, Held>: 206 match rd: 207 case M.Bad{_why}: 208 [] 209 case M.Good{mm}: 210 now.all(J.deps.of(mm)) 211 212 # and of the World's ledger 213 def now.ledger(ledger: Maybe<&2, String>) -> List<&2, Held>: 214 match ledger: 215 case None{}: 216 [] 217 case Some{text}: 218 now.read(M.parse(text)) 219 220 # every member that moved since the lock being rewritten, against the 221 # joint ledger the lock is planned from 222 def moved(+lock: String, ledger: Maybe<&2, String>) -> List<&2, Shift>: 223 +now = now.ledger(ledger) 224 moves(locked(lock), now, now, "") 225 226 # why a plain lock at a workspace root refuses before it asks anything, "" 227 # when it does not 228 def stale.world(+lock: String, world: LW.World) -> String: 229 LW.World{_args, ledger, listing, _replies, _ups} = world 230 stale(moved(lock, ledger), J.files(listing)) 231 232 # --------------------------------------------------------------------------- 233 # the upgrade: rewrite, walk again, until nothing moves further 234 235 # whether two moves are one 236 def move.eq(aa: Shift, bb: Shift) -> Bool: 237 Shift{an, ao, aw} = aa 238 Shift{bn, bo, bw} = bb 239 Bool.and(String.eq(an, bn), Bool.and(String.eq(ao, bo), String.eq(aw, bw))) 240 241 # whether two lists of moves are one 242 def moves.eq(aa: List<&2, Shift>, bb: List<&2, Shift>) -> Bool: 243 match aa bb: 244 case Nil{} Nil{}: 245 True{} 246 case Nil{} Con{_b, _bt}: 247 False{} 248 case Con{_a, _at} Nil{}: 249 False{} 250 case Con{a, at} Con{b, bt}: 251 rest = moves.eq(at, bt) 252 Bool.and(move.eq(a, b), rest) 253 254 # what a round of the upgrade sees: the root's ledger text, every other 255 # ez.toml git lists, the committed files as they are, the members the lock 256 # records, the members before any line was rewritten, and `--package` 257 type Ctx is Data: 258 Ctx{text: String, found: List<&2, J.Found>, files: List<&2, K.Source>, old: List<&2, Held>, 259 base: List<&2, Held>, only: String} 260 261 # the moves the next round makes: the files rewritten with this round's 262 # moves, the members walked again over them, and the moves against those 263 def round(ctx: Ctx, ms: List<&2, Shift>) -> List<&2, Shift>: 264 Ctx{+text, +fs, +files, old, base, +only} = ctx 265 moves(old, now(J.joint(text, fs, rewrite(swaps(ms), files))), base, only) 266 267 # the moves once a round is known to change them or not; the rounds after it 268 # arrive as a thunk, so the rounds stop where the moves do 269 def cascade.pick(same: Bool, ms: List<&2, Shift>, rest: Unit -> List<&2, Shift>) -> List<&2, Shift>: 270 match same: 271 case True{}: 272 ms 273 case False{}: 274 rest(Unit{}) 275 276 # rounds, under fuel, until one moves no member further 277 def cascade.go(fuel: Nat, +ctx: Ctx, +ms: List<&2, Shift>) -> List<&2, Shift>: 278 match fuel: 279 case 0n: 280 ms 281 case 1n+f: 282 +next = round(ctx, ms) 283 cascade.pick(moves.eq(ms, next), ms, _u => cascade.go(f, ctx, next)) 284 285 # the context of an upgrade's rounds 286 def ctx(+text: String, +fs: List<&2, J.Found>, +files: List<&2, K.Source>, +lock: String, +only: String) -> Ctx: 287 Ctx{text, fs, files, locked(lock), now(J.joint(text, fs, files)), only} 288 289 # the members the upgrade moves, from the ones it is asked to. Each round 290 # moves at least one more member or ends, and a member moves once, so a 291 # round per member the lock records and one more is fuel enough. 292 def cascade(+cx: Ctx) -> List<&2, Shift>: 293 Ctx{text, fs, files, +old, +base, only} = cx 294 cascade.go((List.length(&2, Held, old) + 1n : Nat), Ctx{text, fs, files, old, base, only}, 295 moves(old, base, base, only)) 296 297 # a rewritten file written, when the rewrite changed it and it is not the lock 298 def edit.put(changed: Bool, +at: String, text: String, +rest: List<&2, LP.Effect>) -> List<&2, LP.Effect>: 299 match changed: 300 case True{}: 301 Bool.pick(List<&2, LP.Effect>, String.eq(at, LP.lockfile()), rest, LP.Write{at, text} <> rest) 302 case False{}: 303 rest 304 305 # every file the moves rewrite, written 306 def edits(+ss: List<&2, U.Swap>, fs: List<&2, K.Source>) -> List<&2, LP.Effect>: 307 match fs: 308 case []: 309 [] 310 case K.Source{+at, +text} <> tl: 311 +new = U.reimport.many(ss, text) 312 edit.put(Bool.not(String.eq(new, text)), at, new, edits(ss, tl)) 313 314 # the committed files as the lock is to read them 315 def sources(fs: List<&2, K.Source>) -> List<&2, LW.Source>: 316 match fs: 317 case []: 318 [] 319 case K.Source{at, text} <> tl: 320 LW.Source{at, text} <> sources(tl) 321 322 # the World the upgrade plans on: the committed files rewritten with every 323 # move, and the joint ledger and the answers for its members made again over 324 # them, so the lock walks the members as the rewrite leaves them 325 def world.moved( 326 +jj: J.Joint, 327 +files: List<&2, K.Source>, 328 world: LW.World 329 ) -> LW.World: 330 LW.World{args, +ledger, _listing, replies, ups} = world 331 LW.World{args, J.joint.text(jj, ledger), LW.Listed{sources(files)}, 332 List.append(&2, LW.Reply, replies, J.seeds(jj, files)), ups}