src/fetch/plan.bend

ezx@1.9.0.0 · 917 lines · 0 definitions

1# fetch/plan: `ez fetch` as a pure planner. It reads a World
2# (fetch/world.bend) and returns either the questions it still needs
3# answered (`step`) or a Plan: the effects to run and how the command ends
4# (`plan`). The interpreter (fetch/run.bend) loops on `step`, answering each
5# question by IO, until nothing is left to ask, then executes the plan.
6#
7# `ez fetch` fills BEND_LIB from the lock alone, re-resolving nothing. With no
8# ez.toml, or no ez.lock.toml, it refuses before it asks anything, as
9# `uv sync --frozen` does: a fetch never writes a lock, so it has nothing to
10# fill BEND_LIB from.
11#
12# For every package the lock records it asks first for the tree already under
13# BEND_LIB. A tree there that the lock's checks pass is kept as it is. Any
14# other is fetched: a git package as a shallow checkout at the pinned rev,
15# from its source anchored at the project (`P.anchor(here, url)`), and a hub
16# package from the lock's hub. Each file is then taken from what arrived by
17# its path, from the package's root inside the checkout for a git package,
18# and the package is checked before anything is laid (`good`): every file's
19# digest equals the sum the lock records for it (EZ-FETCH-1), no path leaves
20# the package, and the files hash to the name the lock records them under
21# (EZ-HASH-3). A git package's checkout must also weigh to the narHash the
22# lock records (`weighed`), so the files laid come from the tree the lock
23# pins, the tree nix rebuilds from the same pin. Only then is the tree laid
24# under BEND_LIB, the files the lock names and their manifest last.
25#
26# Every name the lock records is laid too, as the file bend reads the name's
27# hash from, `$BEND_LIB/names/<nv>` holding the hash and a newline, so bend
28# never asks the hub for it (EZ-HUB-4). A names file that already holds the
29# lock's hash is kept; any other is written, and one that held another hash
30# is said to have been rewritten, since the lock wins over it as it wins over
31# a tree that fails its checks. A name the lock records that bend would not
32# read back, one the hub rules out or a hash that is not a `0x` name, refuses
33# the fetch, since its file would not be the one bend reads.
34#
35# The plan of a fetch that succeeds lays every tree that was fetched and every
36# names file that is not already right, and nothing else: no ledger, no lock. A fetch that refuses, for any package,
37# has no effect at all, so a package fetched before the one that failed is
38# not laid either, and a tree already under BEND_LIB is never removed
39# (EZ-OUT-2).
40#
41# The Effect, Plan and Outcome types are the lock's (lock/plan.bend), as for
42# `ez add`, `ez remove` and `ez init`.
43import Base
44import ezx@1.9.0.0/src/toml/toml.bend as T
45import ezx@1.9.0.0/src/fetch/world.bend as F
46import ezx@1.9.0.0/src/lock/plan.bend as P
47import ezx@1.9.0.0/src/lock/world.bend as W
48import ezx@1.9.0.0/src/lock/up.bend as Up
49import ezx@1.9.0.0/src/lock/lock.bend as L
50import ezx@1.9.0.0/src/pkg/pkg.bend as K
51import ezx@1.9.0.0/src/pkg/path.bend as Path
52import ezx@1.9.0.0/src/share/sha.bend as Sha
53
54# ---------------------------------------------------------------------------
55# the questions
56
57# the question that fetches a package from where the lock says it comes from:
58# a checkout of a git package at its rev, from its source as git is to read
59# it, a relative path taken from the project; or a hub package from the hub
60def fetch.ask(src: L.Src, +here: String, +hub: String, +hash: String) -> F.Ask:
61  match src:
62    case L.Hub{}:
63      F.Hub{W.Pkg{hash, L.Hub{}, hub}}
64    case L.Git{url, rev, _entry, _root, _nar, _tag}:
65      F.Git{hash, Up.Clone{Path.anchor(here, url), rev}}
66
67# ---------------------------------------------------------------------------
68# what arrived
69
70# a package's files as they arrived, with the directory the lock's paths are
71# written from inside them, or why nothing arrived
72type Tree is Data:
73  Tree{root: String, files: List<&2, K.Source>}
74  Gone{why: String}
75
76# the files a manifest names, each with the text read for it
77def zip(fs: List<&2, K.Item>, ss: List<&2, String>) -> List<&2, K.Source>:
78  match fs ss:
79    case Nil{} Nil{}:
80      []
81    case Nil{} Con{_s, _t}:
82      []
83    case Con{_f, _g} Nil{}:
84      []
85    case Con{f, g} Con{s, t}:
86      K.Source{K.file.at(f), s} <> zip(g, t)
87
88# a manifest and the texts of the files it names, as a tree: what is under
89# BEND_LIB, or what the hub served
90def tree.read(answer: W.Answer) -> Tree:
91  match answer:
92    case W.Got{_how, manifest, ss}:
93      Tree{"", zip(L.manifest.files(String.lines(manifest)), ss)}
94    case W.Miss{why}:
95      Gone{why}
96    case _:
97      Gone{"ez: a tree was not answered with one"}
98
99# a checkout at the pinned rev, whose paths are the lock's from `root`
100def tree.said(answer: Up.Answer, +root: String) -> Tree:
101  match answer:
102    case Up.Cloned{_nar, fs}:
103      Tree{root, fs}
104    case Up.Miss{why}:
105      Gone{why}
106    case _:
107      Gone{"ez: a checkout was not answered with one"}
108
109# what arrived for a question
110def tree.of(answer: F.Answer, +root: String) -> Tree:
111  match answer:
112    case F.Read{a}:
113      tree.read(a)
114    case F.Said{a}:
115      tree.said(a, root)
116    case F.Held{_got}:
117      Gone{"ez: a tree was not answered with one"}
118
119# a text looked up, "" when it is not there
120def text.or(got: Maybe<&2, String>) -> String:
121  match got:
122    case None{}:
123      ""
124    case Some{text}:
125      text
126
127# where a path of the lock is in a tree: under its root inside a checkout,
128# and as it is in a tree a manifest names
129def key(+root: String, +at: String) -> String:
130  Bool.pick(String, String.is_empty(root), at, Path.join(root, at))
131
132# every file the lock names, with the text the tree has at its path. A file
133# the tree does not have is taken as empty, which does not match the lock
134# unless the lock records an empty file.
135def cand(fs: List<&2, K.Item>, +root: String, +tree: List<&2, K.Source>) -> List<&2, W.Source>:
136  match fs:
137    case []:
138      []
139    case K.Item{+at, _sum} <> t:
140      W.Source{at, text.or(K.look(tree, key(root, at)))} <> cand(t, root, tree)
141
142# ---------------------------------------------------------------------------
143# the checks
144
145# a file laid, weighed by its own text
146def weigh(src: W.Source) -> K.Item:
147  W.Source{at, +text} = src
148  K.Item{at, Sha.hex(text)}
149
150# every file laid, weighed by its own text
151def weighs(ls: List<&2, W.Source>) -> List<&2, K.Item>:
152  match ls:
153    case []:
154      []
155    case h <> t:
156      weigh(h) <> weighs(t)
157
158# whether one file is the one the lock names: the same path, and a text whose
159# digest is the sum the lock records
160def fit(want: K.Item, got: W.Source) -> Bool:
161  K.Item{+at, sum} = want
162  W.Source{lat, +text} = got
163  Bool.and(String.eq(lat, at), String.eq(Sha.hex(text), sum))
164
165# whether every file is the one the lock names, in the lock's order. A list
166# longer or shorter than the lock's is not the package.
167def fits(fs: List<&2, K.Item>, ls: List<&2, W.Source>) -> Bool:
168  match fs ls:
169    case Nil{} Nil{}:
170      True{}
171    case Nil{} Con{_l, _m}:
172      False{}
173    case Con{_f, _g} Nil{}:
174      False{}
175    case Con{f, g} Con{l, m}:
176      +rest = fits(g, m)
177      Bool.and(fit(f, l), rest)
178
179# whether a path stays inside the tree it is laid in: written the way the
180# path walk writes it, so no `.` or `//` in it, not empty, not absolute and
181# not climbing out with `..`. A lock is not trusted with where it writes.
182def path.safe(+at: String) -> Bool:
183  Bool.and(String.eq(Path.norm(at), at),
184    Bool.not(Bool.or(K.escapes(at), String.eq(at, "."))))
185
186# whether every path stays inside the tree
187def paths.safe(ls: List<&2, W.Source>) -> Bool:
188  match ls:
189    case []:
190      True{}
191    case W.Source{+at, _text} <> t:
192      +rest = paths.safe(t)
193      Bool.and(path.safe(at), rest)
194
195# the manifest of the files laid, weighed by their own texts
196def manifest(+ls: List<&2, W.Source>) -> String:
197  K.manifest_of(K.files_of(weighs(ls)))
198
199# the `0x` name of the files laid, weighed by their own texts
200def named(+ls: List<&2, W.Source>) -> String:
201  K.hash_of(K.files_of(weighs(ls)))
202
203# whether a package may be laid: every file is the one the lock names, no
204# path leaves the tree, and the files hash to the name the lock records them
205# under, so the tree laid under that name is that name's
206def good(fs: List<&2, K.Item>, +hash: String, +ls: List<&2, W.Source>) -> Bool:
207  +fit = fits(fs, ls)
208  +safe = paths.safe(ls)
209  Bool.and(fit, Bool.and(safe, String.eq(hash, named(ls))))
210
211# the first file that is not the one the lock names, or "" when none is
212# wrong. A path is never empty, so "" is never such a path.
213def unfit.pick(ok: Bool, +at: String, rest: String) -> String:
214  match ok:
215    case True{}:
216      rest
217    case False{}:
218      at
219
220# the first path that does not fit, "" when every file does
221def unfit(fs: List<&2, K.Item>, ls: List<&2, W.Source>) -> String:
222  match fs ls:
223    case Nil{} Nil{}:
224      ""
225    case Nil{} Con{l, _m}:
226      W.Source{at, _text} = l
227      at
228    case Con{f, _g} Nil{}:
229      K.file.at(f)
230    case Con{+f, g} Con{l, m}:
231      unfit.pick(fit(f, l), K.file.at(f), unfit(g, m))
232
233# why a package may not be laid, naming it
234def bad.why(fs: List<&2, K.Item>, +hash: String, +ls: List<&2, W.Source>) -> String:
235  +at = unfit(fs, ls)
236  Bool.pick(String, String.is_empty(at),
237    Bool.pick(String, paths.safe(ls),
238      "ez: " ++ hash ++ ": its files do not hash to its name",
239      "ez: " ++ hash ++ ": a path in the lock leaves the package"),
240    "ez: " ++ hash ++ ": " ++ at ++ " does not match the lock")
241
242# ---------------------------------------------------------------------------
243# one package
244
245# where one package stands: its tree under BEND_LIB still to read, its fetch
246# still to ask, a tree under BEND_LIB that passed and is kept, the files that
247# arrived, or why nothing arrived
248type Part is Data:
249  AskLaid{}
250  AskFetch{}
251  Have{}
252  Got{files: List<&2, W.Source>}
253  No{why: String}
254
255# the files that arrived for a package, or why none did
256def part.tree(tree: Tree, fs: List<&2, K.Item>) -> Part:
257  match tree:
258    case Tree{r, files}:
259      Got{cand(fs, r, files)}
260    case Gone{why}:
261      No{why}
262
263# a package fetched, once it is answered
264def part.fetch.heard(got: F.Heard, fs: List<&2, K.Item>, +root: String) -> Part:
265  match got:
266    case F.Open{}:
267      AskFetch{}
268    case F.Heard{answer}:
269      part.tree(tree.of(answer, root), fs)
270
271# a package fetched, from where the lock says it comes from
272def part.fetch(
273  +rs: List<&2, F.Reply>,
274  +src: L.Src,
275  fs: List<&2, K.Item>,
276  +here: String,
277  +hub: String,
278  +hash: String
279) -> Part:
280  part.fetch.heard(F.heard(rs, fetch.ask(src, here, hub, hash)), fs, L.src.root(src))
281
282# a tree under BEND_LIB that passes is kept; any other is fetched
283def part.kept(
284  ok: Bool,
285  +rs: List<&2, F.Reply>,
286  +src: L.Src,
287  fs: List<&2, K.Item>,
288  +here: String,
289  +hub: String,
290  +hash: String
291) -> Part:
292  match ok:
293    case True{}:
294      Have{}
295    case False{}:
296      part.fetch(rs, src, fs, here, hub, hash)
297
298# the tree under BEND_LIB, read: checked the way a fetched one is, and
299# fetched when it is not there
300def part.found(
301  tree: Tree,
302  +rs: List<&2, F.Reply>,
303  +src: L.Src,
304  +fs: List<&2, K.Item>,
305  +here: String,
306  +hub: String,
307  +hash: String
308) -> Part:
309  match tree:
310    case Tree{_r, files}:
311      part.kept(good(fs, hash, cand(fs, "", files)), rs, src, fs, here, hub, hash)
312    case Gone{_why}:
313      part.fetch(rs, src, fs, here, hub, hash)
314
315# the tree under BEND_LIB, once it is read
316def part.laid(
317  got: F.Heard,
318  +rs: List<&2, F.Reply>,
319  +src: L.Src,
320  +fs: List<&2, K.Item>,
321  +here: String,
322  +hub: String,
323  +hash: String
324) -> Part:
325  match got:
326    case F.Open{}:
327      AskLaid{}
328    case F.Heard{answer}:
329      part.found(tree.of(answer, ""), rs, src, fs, here, hub, hash)
330
331# where one package stands
332def part(
333  +rs: List<&2, F.Reply>,
334  +src: L.Src,
335  +fs: List<&2, K.Item>,
336  +here: String,
337  +hub: String,
338  +hash: String
339) -> Part:
340  part.laid(F.heard(rs, F.Laid{hash}), rs, src, fs, here, hub, hash)
341
342# ---------------------------------------------------------------------------
343# the checkout around a git package
344
345# why a checkout may not be laid for the narHash it weighed to, "" when it
346# may: the lock records a narHash and the checkout weighed to it
347def nar.why(
348  none: Bool,
349  same: Bool,
350  +hash: String,
351  +want: String,
352  +got: String,
353  +url: String,
354  +rev: String
355) -> String:
356  match none:
357    case True{}:
358      "ez: " ++ hash ++ ": the lock records no narHash for " ++ url ++ " at " ++ rev ++
359        ", so its checkout cannot be checked; run `ez lock --upgrade` to pin it"
360    case False{}:
361      match same:
362        case True{}:
363          ""
364        case False{}:
365          "ez: " ++ hash ++ ": " ++ url ++ " at " ++ rev ++ " has narHash " ++ got ++
366            ", and the lock records " ++ want
367
368# why a package stands without a weighed checkout
369def unweighed(+hash: String) -> String:
370  "ez: " ++ hash ++ ": its checkout was not weighed"
371
372# what a checkout question was answered with, against the lock's narHash
373def weighed.up(answer: Up.Answer, +hash: String, +nar: String, +url: String, +rev: String) -> String:
374  match answer:
375    case Up.Cloned{+got, _fs}:
376      nar.why(String.is_empty(nar), String.eq(got, nar), hash, nar, got, url, rev)
377    case _:
378      unweighed(hash)
379
380def weighed.answer(answer: F.Answer, +hash: String, +nar: String, +url: String, +rev: String) -> String:
381  match answer:
382    case F.Said{a}:
383      weighed.up(a, hash, nar, url, rev)
384    case F.Read{_a}:
385      unweighed(hash)
386    case F.Held{_got}:
387      unweighed(hash)
388
389def weighed.heard(got: F.Heard, +hash: String, +nar: String, +url: String, +rev: String) -> String:
390  match got:
391    case F.Heard{a}:
392      weighed.answer(a, hash, nar, url, rev)
393    case F.Open{}:
394      unweighed(hash)
395
396def weighed.src(src: L.Src, +rs: List<&2, F.Reply>, +here: String, +hash: String) -> String:
397  match src:
398    case L.Hub{}:
399      ""
400    case L.Git{+url, +rev, _entry, _root, nar, _tag}:
401      weighed.heard(F.heard(rs, F.Git{hash, Up.Clone{Path.anchor(here, url), rev}}),
402        hash, nar, url, rev)
403
404# why the checkout a package of the lock was fetched as may not be laid, ""
405# when it may. A hub package has no checkout. A git package's checkout, the
406# one its fetch question was answered with, must weigh to the narHash the
407# lock records, so what is laid comes from the tree the lock pins and not
408# only from files that happen to match their sums.
409def weighed(+ss: List<&2, T.Sect>, +rs: List<&2, F.Reply>, +here: String, +hash: String) -> String:
410  weighed.src(L.pack.src(L.pack_of(ss, hash)), rs, here, hash)
411
412def weigh.got(ok: Bool, +why: String, ls: List<&2, W.Source>) -> Part:
413  match ok:
414    case True{}:
415      Got{ls}
416    case False{}:
417      No{why}
418
419# files that arrived stand only when the checkout they came from weighed to
420# the lock's narHash. A tree kept from BEND_LIB was not fetched, and is
421# checked file by file as before.
422def weigh.part(+why: String, part: Part) -> Part:
423  match part:
424    case AskLaid{}:
425      AskLaid{}
426    case AskFetch{}:
427      AskFetch{}
428    case Have{}:
429      Have{}
430    case Got{ls}:
431      weigh.got(String.is_empty(why), why, ls)
432    case No{w}:
433      No{w}
434
435# one package of the lock, and where it stands
436type Pt is Data:
437  Pt{hash: String, src: L.Src, files: List<&2, K.Item>, part: Part}
438
439# every package of the lock, and where each stands
440def parts(
441  +ss: List<&2, T.Sect>,
442  +rs: List<&2, F.Reply>,
443  +here: String,
444  +hub: String,
445  hs: List<&2, String>
446) -> List<&2, Pt>:
447  match hs:
448    case []:
449      []
450    case +h <> t:
451      +pk = L.pack_of(ss, h)
452      +src = L.pack.src(pk)
453      +fs = L.pack.files(pk)
454      Pt{h, src, fs, weigh.part(weighed(ss, rs, here, h), part(rs, src, fs, here, hub, h))}
455        <> parts(ss, rs, here, hub, t)
456
457# ---------------------------------------------------------------------------
458# the names
459
460# where one name of the lock stands: its names file still to read, a file
461# that already holds the lock's hash, a file to write, with the hash the one
462# there held ("" when there was none), or why the name refuses the fetch
463type Nm is Data:
464  NmAsk{nv: String}
465  NmKeep{}
466  NmPut{nv: String, hash: String, was: String}
467  NmStop{why: String}
468
469# a names file that was there, against the lock's hash. bend trims the file
470# before it reads the hash, and so does this.
471def name.held(same: Bool, +nv: String, +hash: String, +was: String) -> Nm:
472  match same:
473    case True{}:
474      NmKeep{}
475    case False{}:
476      NmPut{nv, hash, was}
477
478# what BEND_LIB holds for a name
479def name.got(got: Maybe<&2, String>, +nv: String, +hash: String) -> Nm:
480  match got:
481    case None{}:
482      NmPut{nv, hash, ""}
483    case Some{text}:
484      +was = String.trim(text)
485      name.held(String.eq(was, hash), nv, hash, was)
486
487# a names file, once it is read
488def name.answer(answer: F.Answer, +nv: String, +hash: String) -> Nm:
489  match answer:
490    case F.Held{got}:
491      name.got(got, nv, hash)
492    case _:
493      NmStop{"ez: " ++ nv ++ ": its names file was not read"}
494
495def name.heard(got: F.Heard, +nv: String, +hash: String) -> Nm:
496  match got:
497    case F.Open{}:
498      NmAsk{nv}
499    case F.Heard{a}:
500      name.answer(a, nv, hash)
501
502# a name bend would read back is looked for under BEND_LIB; any other stops
503# the fetch, since the file laid for it would not be the one bend reads
504def name.valid(ok: Bool, +rs: List<&2, F.Reply>, +nv: String, +hash: String) -> Nm:
505  match ok:
506    case True{}:
507      name.heard(F.heard(rs, F.Named{nv}), nv, hash)
508    case False{}:
509      NmStop{"ez: the lock names " ++ nv ++ " as " ++ hash ++
510        ", which bend would not read back; run ez lock"}
511
512# where one name of the lock stands
513def name.part(+rs: List<&2, F.Reply>, name: L.Name) -> Nm:
514  L.Name{+nv, +hash} = name
515  name.valid(L.name.valid(L.Name{nv, hash}), rs, nv, hash)
516
517# where every name of the lock stands
518def name.parts(+rs: List<&2, F.Reply>, ns: List<&2, L.Name>) -> List<&2, Nm>:
519  match ns:
520    case []:
521      []
522    case h <> t:
523      name.part(rs, h) <> name.parts(rs, t)
524
525# how a rewritten names file is said to have been rewritten
526def name.rewrote(+nv: String, +hash: String, +was: String) -> String:
527  "ez: BEND_LIB/names/" ++ nv ++ " named " ++ was ++ ", and the lock names " ++ hash ++
528    "; rewrote it"
529
530# a names file written, said first when it held another hash
531def name.put.at(none: Bool, +nv: String, +hash: String, was: String, rest: List<&2, P.Effect>) -> List<&2, P.Effect>:
532  match none:
533    case True{}:
534      P.Name{nv, hash} <> rest
535    case False{}:
536      P.Say{name.rewrote(nv, hash, was)} <> P.Name{nv, hash} <> rest
537
538def name.put(+nv: String, +hash: String, +was: String, rest: List<&2, P.Effect>) -> List<&2, P.Effect>:
539  name.put.at(String.is_empty(was), nv, hash, was, rest)
540
541# every names file a fetch writes, ahead of what follows
542def names.lay(ms: List<&2, Nm>, rest: List<&2, P.Effect>) -> List<&2, P.Effect>:
543  match ms:
544    case []:
545      rest
546    case m <> t:
547      match m:
548        case NmPut{nv, hash, was}:
549          name.put(nv, hash, was, names.lay(t, rest))
550        case _:
551          names.lay(t, rest)
552
553# how the names end the fetch: the first that refuses, or `o`, how the
554# packages ended it, which comes first
555def names.outcome(ms: List<&2, Nm>) -> P.Outcome:
556  match ms:
557    case []:
558      P.Success{}
559    case m <> t:
560      match m:
561        case NmStop{why}:
562          P.Refused{why}
563        case NmAsk{nv}:
564          P.Refused{"ez: " ++ nv ++ " was never looked for under BEND_LIB"}
565        case _:
566          names.outcome(t)
567
568# the packages' end, or, when they succeed, the names'
569def outcome.then(first: P.Outcome, then: P.Outcome) -> P.Outcome:
570  match first:
571    case P.Success{}:
572      then
573    case P.Refused{why}:
574      P.Refused{why}
575
576# the names still to read under BEND_LIB
577def names.asks(ms: List<&2, Nm>) -> List<&2, F.Ask>:
578  match ms:
579    case []:
580      []
581    case m <> t:
582      match m:
583        case NmAsk{nv}:
584          F.Named{nv} <> names.asks(t)
585        case _:
586          names.asks(t)
587
588# every name the lock records, and where each stands
589def name.parts.of(+ss: List<&2, T.Sect>, +world: F.World) -> List<&2, Nm>:
590  name.parts(F.replies(world), L.names(ss))
591
592# ---------------------------------------------------------------------------
593# the verdict
594
595# what one package comes to: nothing to do, a tree to lay, or a refusal
596type Verdict is Data:
597  Keep{}
598  Put{hash: String, files: List<&2, W.Source>}
599  Stop{why: String}
600
601# the files that arrived, laid when they pass
602def judge.got(ok: Bool, fs: List<&2, K.Item>, +hash: String, +ls: List<&2, W.Source>) -> Verdict:
603  match ok:
604    case True{}:
605      Put{hash, ls}
606    case False{}:
607      Stop{bad.why(fs, hash, ls)}
608
609# what one package comes to, where it stands
610def judge(+fs: List<&2, K.Item>, +hash: String, part: Part) -> Verdict:
611  match part:
612    case AskLaid{}:
613      Stop{"ez: " ++ hash ++ " was never looked for under BEND_LIB"}
614    case AskFetch{}:
615      Stop{"ez: " ++ hash ++ " was never fetched"}
616    case Have{}:
617      Keep{}
618    case Got{+ls}:
619      judge.got(good(fs, hash, ls), fs, hash, ls)
620    case No{why}:
621      Stop{why}
622
623# what every package comes to
624def judged(ps: List<&2, Pt>) -> List<&2, Verdict>:
625  match ps:
626    case []:
627      []
628    case Pt{h, _src, fs, p} <> t:
629      judge(fs, h, p) <> judged(t)
630
631# a tree laid under BEND_LIB: the files the lock names, then their manifest,
632# so a tree that has one is a tree that finished
633def lay(+hash: String, +ls: List<&2, W.Source>) -> P.Effect:
634  P.Lay{P.Cache{}, hash, List.append(&2, W.Source, ls, [W.Source{"manifest", manifest(ls)}])}
635
636# one verdict's effect, ahead of the rest
637def effect.v(verdict: Verdict, rest: List<&2, P.Effect>) -> List<&2, P.Effect>:
638  match verdict:
639    case Keep{}:
640      rest
641    case Put{hash, ls}:
642      lay(hash, ls) <> rest
643    case Stop{_why}:
644      rest
645
646# every verdict's effect
647def effects.of(vs: List<&2, Verdict>) -> List<&2, P.Effect>:
648  match vs:
649    case []:
650      []
651    case v <> t:
652      effect.v(v, effects.of(t))
653
654# how the command ends: the first refusal, or success
655def outcome.of(vs: List<&2, Verdict>) -> P.Outcome:
656  match vs:
657    case []:
658      P.Success{}
659    case v <> t:
660      match v:
661        case Stop{why}:
662          P.Refused{why}
663        case _:
664          outcome.of(t)
665
666# a plan that refuses has no effect at all; its reason is how it ends
667def seal(es: List<&2, P.Effect>, outcome: P.Outcome) -> P.Plan:
668  match outcome:
669    case P.Success{}:
670      P.Plan{es, P.Success{}}
671    case P.Refused{why}:
672      P.Plan{[], P.Refused{why}}
673
674# the plan once every package and every name stands somewhere: the names
675# files, then the trees, and how the command ends. Each verdict is made once
676# and read by both halves.
677def parts.plan(ps: List<&2, Pt>, +ms: List<&2, Nm>) -> P.Plan:
678  +vs = judged(ps)
679  seal(names.lay(ms, effects.of(vs)), outcome.then(outcome.of(vs), names.outcome(ms)))
680
681# ---------------------------------------------------------------------------
682# the plan
683
684# every package of the lock on a World that has one, and where each stands
685def parts.of(+ss: List<&2, T.Sect>, +world: F.World) -> List<&2, Pt>:
686  parts(ss, F.replies(world), F.here(world), L.lock.hub(ss), L.hashes(ss))
687
688# why a fetch refuses before anything is asked: no ez.toml, or no lock
689def need.no() -> String:
690  "ez: no " ++ F.toml() ++ " here; ez fetch fills BEND_LIB for a project's lock, " ++
691    "and ez init makes one"
692
693def need.lock() -> String:
694  "ez: no " ++ F.lockfile() ++ " here; ez fetch reads the lock and resolves " ++
695    "nothing, and ez lock writes it"
696
697# the plan once the ledger and the lock are known to be there
698def plan.lock(got: Maybe<&2, String>, +world: F.World) -> P.Plan:
699  match got:
700    case None{}:
701      P.Plan{[], P.Refused{need.lock()}}
702    case Some{_text}:
703      +ss = F.sects(world)
704      parts.plan(parts.of(ss, world), name.parts.of(ss, world))
705
706# the plan: a missing ledger, then a missing lock, refused before the rest
707def plan.led(led: Bool, got: Maybe<&2, String>, +world: F.World) -> P.Plan:
708  match led:
709    case False{}:
710      P.Plan{[], P.Refused{need.no()}}
711    case True{}:
712      plan.lock(got, world)
713
714# `ez fetch` over a World
715def plan(+world: F.World) -> P.Plan:
716  plan.led(F.ledger(world), F.lock(world), world)
717
718# whether a plan ends in a refusal
719def refuses.plan(pl: P.Plan) -> Bool:
720  P.Plan{_es, outcome} = pl
721  match outcome:
722    case P.Success{}:
723      False{}
724    case P.Refused{_why}:
725      True{}
726
727# whether `ez fetch` refuses on a World
728def refuses(+world: F.World) -> Bool:
729  refuses.plan(plan(world))
730
731# ---------------------------------------------------------------------------
732# what is still to ask
733
734# one package's open question, ahead of the rest
735def ask.pt(pt: Pt, +here: String, +hub: String, rest: List<&2, F.Ask>) -> List<&2, F.Ask>:
736  Pt{+h, src, _fs, p} = pt
737  match p:
738    case AskLaid{}:
739      F.Laid{h} <> rest
740    case AskFetch{}:
741      fetch.ask(src, here, hub, h) <> rest
742    case _:
743      rest
744
745# every open question
746def asks.of(ps: List<&2, Pt>, +here: String, +hub: String) -> List<&2, F.Ask>:
747  match ps:
748    case []:
749      []
750    case p <> t:
751      ask.pt(p, here, hub, asks.of(t, here, hub))
752
753# the open questions once the ledger and the lock are known to be there
754def wants.lock(got: Maybe<&2, String>, +world: F.World) -> List<&2, F.Ask>:
755  match got:
756    case None{}:
757      []
758    case Some{_text}:
759      +ss = F.sects(world)
760      List.append(&2, F.Ask, asks.of(parts.of(ss, world), F.here(world), L.lock.hub(ss)),
761        names.asks(name.parts.of(ss, world)))
762
763def wants.led(led: Bool, got: Maybe<&2, String>, +world: F.World) -> List<&2, F.Ask>:
764  match led:
765    case False{}:
766      []
767    case True{}:
768      wants.lock(got, world)
769
770# the questions a World still leaves open: none when the fetch refuses before
771# it asks, and otherwise one for each package that does not yet stand
772# anywhere
773def wants(+world: F.World) -> List<&2, F.Ask>:
774  wants.led(F.ledger(world), F.lock(world), world)
775
776# the questions still open, or the plan once there are none
777type Step is Data:
778  Asking{asks: List<&2, F.Ask>}
779  Run{plan: P.Plan}
780
781def step.asks(asks: List<&2, F.Ask>, ps: List<&2, Pt>, ms: List<&2, Nm>) -> Step:
782  match asks:
783    case []:
784      Run{parts.plan(ps, ms)}
785    case h <> t:
786      Asking{h <> t}
787
788def step.lock(got: Maybe<&2, String>, +world: F.World) -> Step:
789  match got:
790    case None{}:
791      Run{P.Plan{[], P.Refused{need.lock()}}}
792    case Some{_text}:
793      +ss = F.sects(world)
794      +ps = parts.of(ss, world)
795      +ms = name.parts.of(ss, world)
796      step.asks(List.append(&2, F.Ask, asks.of(ps, F.here(world), L.lock.hub(ss)), names.asks(ms)),
797        ps, ms)
798
799def step.led(led: Bool, got: Maybe<&2, String>, +world: F.World) -> Step:
800  match led:
801    case False{}:
802      Run{P.Plan{[], P.Refused{need.no()}}}
803    case True{}:
804      step.lock(got, world)
805
806# what the interpreter does next on a World. Every package is placed once per
807# round, and both the questions and the plan are read from that.
808def step(+world: F.World) -> Step:
809  step.led(F.ledger(world), F.lock(world), world)
810
811# ---------------------------------------------------------------------------
812# law vocabulary
813
814# the effects of a plan
815def effects(pl: P.Plan) -> List<&2, P.Effect>:
816  P.Plan{es, _outcome} = pl
817  es
818
819# every file of a tree but the last, which a `Lay` writes as the manifest
820def front(fs: List<&2, W.Source>) -> List<&2, W.Source>:
821  match fs:
822    case []:
823      []
824    case h <> t:
825      match t:
826        case []:
827          []
828        case h2 <> t2:
829          h <> front(h2 <> t2)
830
831# the files the lock records under a hash
832def locked(+ss: List<&2, T.Sect>, +hash: String) -> List<&2, K.Item>:
833  L.pack.files(L.pack_of(ss, hash))
834
835# whether an effect that lays a tree lays the files the lock records under
836# its hash, each with a text whose digest is the lock's sum for it
837def lays.one(+ss: List<&2, T.Sect>, effect: P.Effect) -> Bool:
838  match effect:
839    case P.Lay{_place, hash, fs}:
840      fits(locked(ss, hash), front(fs))
841    case _:
842      True{}
843
844# whether every tree a list of effects lays is the one the lock records
845def lays.locked(+ss: List<&2, T.Sect>, es: List<&2, P.Effect>) -> Bool:
846  match es:
847    case []:
848      True{}
849    case h <> t:
850      +rest = lays.locked(ss, t)
851      Bool.and(lays.one(ss, h), rest)
852
853# the source a checkout is asked of, "" for any other question
854def clone.url(ask: Up.Ask) -> String:
855  match ask:
856    case Up.Clone{url, _rev}:
857      url
858    case _:
859      ""
860
861# the package a question is about
862def ask.hash(ask: F.Ask) -> String:
863  match ask:
864    case F.Laid{hash}:
865      hash
866    case F.Git{hash, _q}:
867      hash
868    case F.Hub{q}:
869      W.ask.hash(q)
870    case F.Named{nv}:
871      nv
872
873# whether a git question asks for the source the lock records for its
874# package, taken from the project: the lock's url anchored at `here`
875def ask.anchored(+here: String, src: L.Src, ask: F.Ask) -> Bool:
876  match ask:
877    case F.Git{_hash, q}:
878      String.eq(clone.url(q), Path.anchor(here, L.src.url(src)))
879    case _:
880      True{}
881
882# whether every question does
883def asks.anchored(+here: String, +ss: List<&2, T.Sect>, as: List<&2, F.Ask>) -> Bool:
884  match as:
885    case []:
886      True{}
887    case +h <> t:
888      +rest = asks.anchored(here, ss, t)
889      Bool.and(ask.anchored(here, L.pack.src(L.pack_of(ss, ask.hash(h))), h), rest)
890
891# whether an effect that lays a tree lays a package whose checkout, when it
892# has one, weighed to the narHash the lock records
893def weighed.lay(
894  +ss: List<&2, T.Sect>,
895  +rs: List<&2, F.Reply>,
896  +here: String,
897  effect: P.Effect
898) -> Bool:
899  match effect:
900    case P.Lay{_place, hash, _fs}:
901      String.is_empty(weighed(ss, rs, here, hash))
902    case _:
903      True{}
904
905# whether every tree a list of effects lays does
906def lays.weighed(
907  +ss: List<&2, T.Sect>,
908  +rs: List<&2, F.Reply>,
909  +here: String,
910  es: List<&2, P.Effect>
911) -> Bool:
912  match es:
913    case []:
914      True{}
915    case h <> t:
916      +rest = lays.weighed(ss, rs, here, t)
917      Bool.and(weighed.lay(ss, rs, here, h), rest)