src/ws/moved.bend

ezx@1.11.0.0 · 332 lines · 35 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.11.0.0/src/ledger/manifest.bend as M
13import ezx@1.11.0.0/src/ledger/upgrade.bend as U
14import ezx@1.11.0.0/src/lock/lock.bend as L
15import ezx@1.11.0.0/src/lock/world.bend as LW
16import ezx@1.11.0.0/src/lock/plan.bend as LP
17import ezx@1.11.0.0/src/pkg/pkg.bend as K
18import ezx@1.11.0.0/src/toml/toml.bend as T
19import ezx@1.11.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}