src/fetch/plan.bend

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