src/ws/moved.bend
ezx@1.10.0.0 · 332 lines · 0 definitions
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.
11import Base
12import ezx@1.10.0.0/src/ledger/manifest.bend as M
13import ezx@1.10.0.0/src/ledger/upgrade.bend as U
14import ezx@1.10.0.0/src/lock/lock.bend as L
15import ezx@1.10.0.0/src/lock/world.bend as LW
16import ezx@1.10.0.0/src/lock/plan.bend as LP
17import ezx@1.10.0.0/src/pkg/pkg.bend as K
18import ezx@1.10.0.0/src/toml/toml.bend as T
19import ezx@1.10.0.0/src/ws/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
24type 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
29type 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
36def 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
46def 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
54def 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
58def 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
68def 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
76def 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
86def 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
91def 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
99def 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
108def 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
120def 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
132def 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
149def 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
163def 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
171def 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
182def 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
195def 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
205def 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
213def 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
222def 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
228def 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
236def 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
242def 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`
257type 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
263def 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
269def 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
277def 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
286def 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.
292def 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
298def 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
306def 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
315def 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
325def 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}