src/lock/plan.bend
ezx@1.8.0.0 · 1,400 lines · 0 definitions
1# lock/plan: `ez lock` as a pure planner. It reads a World (lock/world.bend)
2# and returns either the questions it still needs answered (`step`) or a Plan:
3# the effects to run and how the command ends (`plan`). The interpreter
4# (lock/run.bend) loops on `step`, answering each question by IO, until
5# nothing is left to ask, then executes the plan.
6#
7# Under `--upgrade` the upgrade's questions come first (lock/up.bend): what
8# the remote says each selected pin moves to. Once they are answered, the
9# lock is planned exactly as a plain lock is, over the ledger the upgrade
10# leaves rather than ez.toml as it was, and the upgrade's writes go around it
11# in one plan: trees laid, ez.toml written once, `.gitignore`, the rewritten
12# sources, the committed trees that moved removed, and the lock last.
13#
14# `wants` only scans the answers it has for imports; `plan` checks every
15# package once, so a lock pays for each file's SHA-256 once however deep the
16# import graph is. A package that fails a check may make `wants` ask for more
17# than the lock needs, which costs a fetch and changes nothing, since `plan`
18# refuses it.
19#
20# A named import, `import <name>@<version>/...`, is a hub package too
21# (EZ-HUB-3). One the project's own sources import must be a dependency
22# ez.toml records by that name, and resolves to the hash ez.toml records
23# beside it; one a package imports resolves the same way when ez.toml names
24# it, then by the lock being rewritten, and only then by asking the hub, once
25# (EZ-HUB-2). The package it resolves to is walked and checked like any hub
26# package, and the lock records every name it resolved under `[names]`.
27#
28# What a lock decides depends on a World only through `W.inputs`: the ledger
29# it is made from, the upgrade's refusal if any, the committed sources, and
30# the verdict on every answer. How a tree arrived only decides whether it is
31# laid under BEND_LIB for the build that follows, which is a cache and not
32# something the lock records.
33import Base
34import ezx@1.8.0.0/src/lock/world.bend as W
35import ezx@1.8.0.0/src/lock/up.bend as Up
36import ezx@1.8.0.0/src/lock/lock.bend as L
37import ezx@1.8.0.0/src/ledger/manifest.bend as M
38import ezx@1.8.0.0/src/pkg/pkg.bend as K
39import ezx@1.8.0.0/src/pkg/path.bend as Path
40import ezx@1.8.0.0/src/share/pin.bend as Pin
41
42# ---------------------------------------------------------------------------
43# the walk
44
45# a verdict looked up, or none when nothing has been asked about the hash yet
46type Look is Data:
47 Found{verdict: W.Verdict}
48 Absent{}
49
50# this verdict when it is the hash asked for, otherwise the rest's
51def look.at(hit: Bool, verdict: W.Verdict, rest: Look) -> Look:
52 match hit:
53 case True{}:
54 Found{verdict}
55 case False{}:
56 rest
57
58# the first verdict recorded for a hash
59def look(js: List<&2, W.Judged>, +hash: String) -> Look:
60 match js:
61 case []:
62 Absent{}
63 case W.Judged{+h, v} <> t:
64 look.at(String.eq(h, hash), v, look(t, hash))
65
66# a walk in progress: the hashes still queued, the packages resolved, the
67# hashes still to be asked about, and the first refusal, "" while there is
68# none. The walk is one step, which does not recurse, iterated; so a fact
69# about the walk is a fact about one step, carried by induction on the fuel.
70type Walk is Data:
71 Walk{queue: List<&2, String>, packs: List<&2, L.Pack>, asks: List<&2, String>, bad: String}
72
73# the first refusal wins
74def first(+bad: String, why: String) -> String:
75 Bool.pick(String, String.is_empty(bad), why, bad)
76
77# one package, by its verdict. Its imports are queued by hash, and by the
78# hash the table of names gives each name it imports.
79def step.verdict(
80 verdict: W.Verdict,
81 +ns: List<&2, L.Name>,
82 +src: L.Src,
83 +hash: String,
84 rest: List<&2, String>,
85 packs: List<&2, L.Pack>,
86 asks: List<&2, String>,
87 bad: String
88) -> Walk:
89 match verdict:
90 case W.No{why}:
91 Walk{rest, packs, asks, first(bad, why)}
92 case W.Ok{fs, ss}:
93 Walk{List.append(&2, String, rest, L.kids.in(ns, ss)), L.Pack{hash, src, fs} <> packs,
94 asks, bad}
95 case W.Named{_names}:
96 Walk{rest, packs, asks, first(bad, "ez: " ++ hash ++ " is imported as a package, and it is not one")}
97
98# one package, asked about when nothing has answered for it yet
99def step.look(
100 found: Look,
101 +ns: List<&2, L.Name>,
102 +src: L.Src,
103 +hash: String,
104 rest: List<&2, String>,
105 packs: List<&2, L.Pack>,
106 asks: List<&2, String>,
107 bad: String
108) -> Walk:
109 match found:
110 case Absent{}:
111 Walk{rest, packs, hash <> asks, bad}
112 case Found{v}:
113 step.verdict(v, ns, src, hash, rest, packs, asks, bad)
114
115# a hash already resolved, or already asked about, is passed over
116def step.pick(
117 seen: Bool,
118 +orgs: List<&2, L.Origin>,
119 +ns: List<&2, L.Name>,
120 +js: List<&2, W.Judged>,
121 +hash: String,
122 rest: List<&2, String>,
123 packs: List<&2, L.Pack>,
124 asks: List<&2, String>,
125 bad: String
126) -> Walk:
127 match seen:
128 case True{}:
129 Walk{rest, packs, asks, bad}
130 case False{}:
131 step.look(look(js, hash), ns, L.origin(orgs, hash), hash, rest, packs, asks, bad)
132
133# the next hash of the queue, or the walk as it is when the queue is empty
134def step.pop(
135 +orgs: List<&2, L.Origin>,
136 +ns: List<&2, L.Name>,
137 +js: List<&2, W.Judged>,
138 queue: List<&2, String>,
139 +packs: List<&2, L.Pack>,
140 +asks: List<&2, String>,
141 bad: String
142) -> Walk:
143 match queue:
144 case []:
145 Walk{[], packs, asks, bad}
146 case +h <> t:
147 step.pick(Bool.or(L.has(packs, h), L.seen.holds(asks, h)), orgs, ns, js, h, t,
148 packs, asks, bad)
149
150# one step of the walk
151def walk.step(+orgs: List<&2, L.Origin>, +ns: List<&2, L.Name>, +js: List<&2, W.Judged>, wk: Walk) -> Walk:
152 Walk{queue, packs, asks, bad} = wk
153 step.pop(orgs, ns, js, queue, packs, asks, bad)
154
155# whether a walk has nothing left in its queue
156def walk.done(+wk: Walk) -> Bool:
157 Walk{queue, _packs, _asks, _bad} = wk
158 List.is_empty(&2, String, queue)
159
160# a walk that ran out of fuel with its queue not empty, refused
161def walk.spent(wk: Walk) -> Walk:
162 Walk{queue, packs, asks, bad} = wk
163 Walk{queue, packs, asks, first(bad, "ez: the import graph is too deep to lock")}
164
165# the walk as it is when it is done, or one step further when it is not. The
166# rest of the walk arrives as a function, since a def may not call itself from
167# another def.
168def walk.more(done: Bool, wk: Walk, go: Walk -> Walk) -> Walk:
169 match done:
170 case True{}:
171 wk
172 case False{}:
173 go(wk)
174
175# the fuel ran out: refused, unless the walk had just finished
176def walk.out(done: Bool, wk: Walk) -> Walk:
177 match done:
178 case True{}:
179 wk
180 case False{}:
181 walk.spent(wk)
182
183# every package the queue reaches, each one once. A walk that runs out of fuel
184# with the queue not empty is a refusal, never a lock of what it had so far.
185def walk(fuel: Nat, +orgs: List<&2, L.Origin>, +ns: List<&2, L.Name>, +js: List<&2, W.Judged>, +wk: Walk) -> Walk:
186 match fuel:
187 case 0n:
188 walk.out(walk.done(wk), wk)
189 case 1n+f:
190 walk.more(walk.done(wk), wk, now => walk(f, orgs, ns, js, walk.step(orgs, ns, js, now)))
191
192# the packages a walk has resolved
193def walk.packs(wk: Walk) -> List<&2, L.Pack>:
194 Walk{_queue, packs, _asks, _bad} = wk
195 packs
196
197# every hub package one committed source imports. The committed trees under
198# `.ez/` are packages, not the project's own sources.
199def roots.one(+src: W.Source) -> List<&2, String>:
200 W.Source{at, text} = src
201 Bool.pick(List<&2, String>, String.starts_with(at, ".ez/"), [],
202 L.specs(K.scan.mods(K.scan(text))))
203
204# every hub package the committed sources import. Every committed `.bend`
205# file is a root, so no local import has to be followed, and a file that is
206# not committed is never read.
207def roots(fs: List<&2, W.Source>) -> List<&2, String>:
208 match fs:
209 case []:
210 []
211 case +h <> t:
212 List.append(&2, String, roots.one(h), roots(t))
213
214# every name one committed source imports, outside `.ez/`
215def roots.named.one(+src: W.Source) -> List<&2, String>:
216 W.Source{at, text} = src
217 Bool.pick(List<&2, String>, String.starts_with(at, ".ez/"), [],
218 L.named(K.scan.mods(K.scan(text))))
219
220# every name the committed sources import. Each must be one ez.toml records
221# a dependency by.
222def roots.named(fs: List<&2, W.Source>) -> List<&2, String>:
223 match fs:
224 case []:
225 []
226 case +h <> t:
227 List.append(&2, String, roots.named.one(h), roots.named(t))
228
229# the table of names a lock resolves names by: those ez.toml records, each
230# with the hash beside it, first, then those the answers resolve, the lock
231# being rewritten's before the hub's
232def table(+led: M.Read, js: List<&2, W.Judged>) -> List<&2, L.Name>:
233 List.append(&2, L.Name, L.ledger.names(led), W.names.of(js))
234
235# how many imports one verdict queues: a package's, or none
236def fuel.verdict(verdict: W.Verdict, ns: List<&2, L.Name>) -> Nat:
237 match verdict:
238 case W.Ok{_fs, ss}:
239 List.length(&2, String, L.kids.in(ns, ss))
240 case W.No{_why}:
241 0n
242 case W.Named{_names}:
243 0n
244
245# how many imports every verdict queues between them
246def fuel.judged(js: List<&2, W.Judged>, +ns: List<&2, L.Name>) -> Nat:
247 match js:
248 case []:
249 0n
250 case W.Judged{_h, v} <> t:
251 Nat.add(fuel.verdict(v, ns), fuel.judged(t, ns))
252
253# how many steps the walk may take: one for each hash the queue starts with,
254# one for each import a verdict can queue, since a hash is resolved at most
255# once, and one more to spare. That is a step for every hash the walk can
256# queue, and it is a function of the walk's inputs rather than a fixed
257# number, as the add walk's is: a proof about inputs it does not know leaves it unevaluated,
258# where bend 2.0.33 and 2.0.34 compare a fixed fuel's walk one stack frame a
259# step and overflow the checker past about 10000.
260def walk.fuel(queue: List<&2, String>, ns: List<&2, L.Name>, js: List<&2, W.Judged>) -> Nat:
261 Nat.add(1n, Nat.add(List.length(&2, String, queue), fuel.judged(js, ns)))
262
263# the walk from every committed source: every package they import by hash,
264# and every one they import by a name the table resolves. A listing git
265# could not give is a walk refused before its first step.
266def walk.of(+led: M.Read, listing: W.Listing, +ns: List<&2, L.Name>, +js: List<&2, W.Judged>) -> Walk:
267 match listing:
268 case W.Unlisted{why}:
269 Walk{[], [], [], why}
270 case W.Listed{+fs}:
271 walk(walk.fuel(List.append(&2, String, roots(fs), L.name.hashes(ns, roots.named(fs))), ns, js),
272 L.ledger.read(led), ns, js,
273 Walk{List.append(&2, String, roots(fs), L.name.hashes(ns, roots.named(fs))), [], [], ""})
274
275# the committed sources a listing holds, none when git could not list them
276def listing.files(listing: W.Listing) -> List<&2, W.Source>:
277 match listing:
278 case W.Unlisted{_why}:
279 []
280 case W.Listed{fs}:
281 fs
282
283# the texts of a package the walk resolved, as its verdict holds them
284def pack.srcs(found: Look) -> List<&2, String>:
285 match found:
286 case Absent{}:
287 []
288 case Found{v}:
289 match v:
290 case W.Ok{_fs, ss}:
291 ss
292 case _:
293 []
294
295# every name the packages a walk resolved import
296def packs.named(+js: List<&2, W.Judged>, ps: List<&2, L.Pack>) -> List<&2, String>:
297 match ps:
298 case []:
299 []
300 case +h <> t:
301 List.append(&2, String, L.kids.named(pack.srcs(look(js, L.pack.hash(h)))),
302 packs.named(js, t))
303
304# every name a lock needs resolved: those the committed sources import, then
305# those the packages it reaches import
306def needed(listing: W.Listing, +js: List<&2, W.Judged>, ps: List<&2, L.Pack>) -> List<&2, String>:
307 List.append(&2, String, roots.named(listing.files(listing)), packs.named(js, ps))
308
309# why a plain lock cannot copy the ledger's tool pins, or "" when it can. A
310# ledger that does not parse is refused here too.
311def need.why(ledger: M.Read) -> String:
312 match ledger:
313 case M.Bad{why}:
314 "ez: " ++ M.show(M.Bad{why})
315 case M.Good{m}:
316 M.Manifest{_n, _e, _b, _h, _pa, _pv, _ds, ts} = m
317 Pin.gaps(ts)
318
319# ---------------------------------------------------------------------------
320# what the lock may hold
321
322# a char the lock's TOML cannot carry inside a quoted key or value: eztoml
323# neither writes nor reads escapes
324def char.bad(+ch: Char) -> Bool:
325 Bool.or(Char.is_eq(ch, '"'), Bool.or(Char.is_eq(ch, '\\'), Char.is_eq(ch, '\n')))
326
327# whether a key or value can be written as it is
328def clean(text: String) -> Bool:
329 match text:
330 case SNil{}:
331 True{}
332 case SCon{+h, t}:
333 +rest = clean(t)
334 Bool.and(Bool.not(char.bad(h)), rest)
335
336# whether every string in a list can be written as it is
337def clean.all(ss: List<&2, String>) -> Bool:
338 match ss:
339 case []:
340 True{}
341 case h <> t:
342 +rest = clean.all(t)
343 Bool.and(clean(h), rest)
344
345# every value a package's source writes
346def src.values(source: L.Src) -> List<&2, String>:
347 match source:
348 case L.Hub{}:
349 []
350 case L.Git{url, rev, entry, root, nar, tag}:
351 [url, rev, entry, root, nar, tag]
352
353# every path and sum a package's files write
354def file.values(fs: List<&2, K.Item>) -> List<&2, String>:
355 match fs:
356 case []:
357 []
358 case K.Item{at, sum} <> t:
359 at <> (sum <> file.values(t))
360
361# every hash, key and value a package writes
362def pack.values(pack: L.Pack) -> List<&2, String>:
363 L.Pack{hash, src, fs} = pack
364 hash <> List.append(&2, String, src.values(src), file.values(fs))
365
366# whether a file path holds no `=`. A path is written as a quoted key, and
367# `bootstrap.sh`, which reads the lock without ez, cuts a pair line at its
368# first `=`, so a path holding one would read back there as another path with
369# another sum. eztoml 0.4 reads a quoted key whole; the restriction lifts when
370# bootstrap.sh does too.
371def path.eqless(text: String) -> Bool:
372 match text:
373 case SNil{}:
374 True{}
375 case SCon{+h, t}:
376 +rest = path.eqless(t)
377 Bool.and(Bool.not(Char.is_eq(h, '=')), rest)
378
379# whether no file path of a package holds `=`
380def paths.ok(fs: List<&2, K.Item>) -> Bool:
381 match fs:
382 case []:
383 True{}
384 case K.Item{at, _sum} <> t:
385 +rest = paths.ok(t)
386 Bool.and(path.eqless(at), rest)
387
388# whether every package can be written, and no hash is written twice
389def packs.ok(ps: List<&2, L.Pack>) -> Bool:
390 match ps:
391 case []:
392 True{}
393 case +h <> +t:
394 +rest = packs.ok(t)
395 Bool.and(Bool.and(paths.ok(L.pack.files(h)),
396 Bool.and(clean.all(pack.values(h)), Bool.not(L.has(t, L.pack.hash(h))))),
397 rest)
398
399# every name, key and value a tool pin writes
400def tool.values(pin: M.Tool) -> List<&2, String>:
401 M.Tool{n, e, b, s} = pin
402 match s:
403 case M.Hub{_named}:
404 [n, e, b]
405 case M.Git{url, rev, tag, root, nar, _vend}:
406 [n, e, b, url, rev, tag, root, nar]
407
408# whether every tool pin can be written
409def tools.ok(ts: List<&2, M.Tool>) -> Bool:
410 match ts:
411 case []:
412 True{}
413 case h <> t:
414 +rest = tools.ok(t)
415 Bool.and(clean.all(tool.values(h)), rest)
416
417# whether a lock can be written so that it reads back: every hash is written
418# once, no hash, key or value holds `"`, `\` or a newline, and no file path
419# holds `=` (eztoml#24, above). The planner
420# refuses a lock that is not, so every lock ez writes is one of these.
421def lockable(ps: List<&2, L.Pack>, ts: List<&2, M.Tool>, hub: String) -> Bool:
422 Bool.and(clean(hub), Bool.and(packs.ok(ps), tools.ok(ts)))
423
424# the first path among a package's files that holds `=`, or "" when none
425# does. A path that holds one is not empty, so "" is never such a path.
426def path.eq.pick(ok: Bool, at: String, rest: String) -> String:
427 match ok:
428 case True{}:
429 rest
430 case False{}:
431 at
432
433def path.eq.first(fs: List<&2, K.Item>) -> String:
434 match fs:
435 case []:
436 ""
437 case K.Item{+at, _sum} <> t:
438 path.eq.pick(path.eqless(at), at, path.eq.first(t))
439
440# why a package with such a path cannot be locked, naming it and the path
441def path.eq.why(+hash: String, +at: String) -> String:
442 "ez: ez.lock.toml cannot be written: package " ++ hash ++ " has a file " ++
443 at ++ " whose path holds `=`, which the lock's TOML reader would cut the" ++
444 " key at (https://github.com/Emerging-Patterns/eztoml/issues/24)"
445
446# the reason for the first package with such a path, or the rest's
447def packs.eq.pick(none: Bool, +hash: String, +at: String, rest: String) -> String:
448 match none:
449 case True{}:
450 rest
451 case False{}:
452 path.eq.why(hash, at)
453
454# why the first package with a path holding `=` cannot be locked, or "" when
455# no package has one
456def packs.eq(ps: List<&2, L.Pack>) -> String:
457 match ps:
458 case []:
459 ""
460 case L.Pack{+hash, _src, fs} <> t:
461 +at = path.eq.first(fs)
462 packs.eq.pick(String.is_empty(at), hash, at, packs.eq(t))
463
464# the reason a lock that is not `lockable` gives: the package and path when a
465# path holds `=`, which is the one case a clean-looking lock hides
466def lockable.pick(none: Bool, why: String) -> String:
467 match none:
468 case True{}:
469 "ez: ez.lock.toml cannot be written: a hash repeats, or a hash, path or value holds a quote, a backslash or a newline"
470 case False{}:
471 why
472
473def lockable.why(ps: List<&2, L.Pack>) -> String:
474 +why = packs.eq(ps)
475 lockable.pick(String.is_empty(why), why)
476
477# whether every name the lock records can be written so that it reads back:
478# a name is written as a quoted key and its hash as a value, the way a
479# package's file is, so it is held to what a file is held to
480def names.ok(ns: List<&2, L.Name>) -> Bool:
481 +fs = L.name.files(ns)
482 Bool.and(paths.ok(fs), clean.all(file.values(fs)))
483
484# whether a lock with names can be written so that it reads back
485def lockable.named(ns: List<&2, L.Name>, ps: List<&2, L.Pack>, ts: List<&2, M.Tool>, hub: String) -> Bool:
486 Bool.and(names.ok(ns), lockable(ps, ts, hub))
487
488# the reason a lock with names that is not lockable gives
489def lockable.why.pick(ok: Bool, ps: List<&2, L.Pack>) -> String:
490 match ok:
491 case True{}:
492 lockable.why(ps)
493 case False{}:
494 "ez: ez.lock.toml cannot be written: a name, or the hash ez.toml records for it, holds `=`, a quote, a backslash or a newline"
495
496def lockable.why.named(ns: List<&2, L.Name>, ps: List<&2, L.Pack>) -> String:
497 lockable.why.pick(names.ok(ns), ps)
498
499# ---------------------------------------------------------------------------
500# the names a lock needs
501
502# a name, when it is the first the search wants, or the rest's
503def names.missing.pick(gone: Bool, +nv: String, rest: Unit -> String) -> String:
504 match gone:
505 case True{}:
506 nv
507 case False{}:
508 rest(Unit{})
509
510# the first name of a list the hub rules out, or "" when it rules them all in
511def names.ruled(nvs: List<&2, String>) -> String:
512 match nvs:
513 case []:
514 ""
515 case +h <> t:
516 names.missing.pick(Bool.not(K.name.ok(h)), h, _u => names.ruled(t))
517
518# the first name of a list the table does not resolve, or "" when it
519# resolves them all
520def names.missing(+ns: List<&2, L.Name>, nvs: List<&2, String>) -> String:
521 match nvs:
522 case []:
523 ""
524 case +h <> t:
525 names.missing.pick(String.is_empty(L.name.find(ns, h)), h, _u => names.missing(ns, t))
526
527# why a name nothing resolved stops the lock: what its answer said, or that
528# it was never asked
529def names.open.why(found: Look, +nv: String) -> String:
530 match found:
531 case Found{v}:
532 match v:
533 case W.No{why}:
534 why
535 case _:
536 "ez: " ++ nv ++ " was never resolved, so there is nothing to lock it from"
537 case Absent{}:
538 "ez: " ++ nv ++ " was never resolved, so there is nothing to lock it from"
539
540# why a name the project imports and ez.toml does not record stops the lock
541def names.unledgered.why(+nv: String) -> String:
542 "ez: " ++ nv ++ " is imported, and ez.toml records no dependency by that name; " ++
543 "a named package is recorded as `hub = \"" ++ nv ++ "\"` beside the hash it names"
544
545# a reason, or when there is none the next one, looked at only then
546def why.then.at(none: Bool, why: String, rest: Unit -> String) -> String:
547 match none:
548 case True{}:
549 rest(Unit{})
550 case False{}:
551 why
552
553def why.then(+why: String, rest: Unit -> String) -> String:
554 why.then.at(String.is_empty(why), why, rest)
555
556# no reason when no name is at fault, and the fault's otherwise
557def why.missing.at(none: Bool, reason: Unit -> String) -> String:
558 match none:
559 case True{}:
560 ""
561 case False{}:
562 reason(Unit{})
563
564def why.missing(+nv: String, reason: Unit -> String) -> String:
565 why.missing.at(String.is_empty(nv), reason)
566
567# why the names a lock needs stop it, or "" when they do not: a name the hub
568# rules out, as bend refuses it; a name the project imports that ez.toml does
569# not record, which is the lock's own rule, since only a dependency ez.toml
570# records vouches for where a name came from; and a name nothing resolved
571def names.why(
572 +lns: List<&2, L.Name>,
573 +ns: List<&2, L.Name>,
574 +js: List<&2, W.Judged>,
575 +roots: List<&2, String>,
576 +need: List<&2, String>
577) -> String:
578 +bad = names.ruled(need)
579 why.then(why.missing(bad, _u => K.unnamed.why(bad)), _v =>
580 why.then(why.missing(names.missing(lns, roots), _w =>
581 names.unledgered.why(names.missing(lns, roots))), _x =>
582 why.missing(names.missing(ns, need), _y =>
583 names.open.why(look(js, names.missing(ns, need)), names.missing(ns, need)))))
584
585# ---------------------------------------------------------------------------
586# the lock's text, or why there is none
587
588# what a plain lock decides: the text of ez.lock.toml, or why it refuses
589type Doc is Data:
590 Doc{text: String}
591 Stop{why: String}
592
593# a lock that can be written, written, with the names it resolved
594def doc.lockable(
595 ok: Bool,
596 +ns: List<&2, L.Name>,
597 +hub: String,
598 packs: List<&2, L.Pack>,
599 ts: List<&2, M.Tool>
600) -> Doc:
601 match ok:
602 case True{}:
603 Doc{L.render.names(ns, ts, hub, packs)}
604 case False{}:
605 Stop{lockable.why.named(ns, packs)}
606
607# a walk with nothing left to ask is the lock. One that still has a question
608# is a World the interpreter did not finish, and is refused rather than
609# guessed at.
610def doc.asks(
611 asks: List<&2, String>,
612 +ns: List<&2, L.Name>,
613 +hub: String,
614 +packs: List<&2, L.Pack>,
615 +ts: List<&2, M.Tool>
616) -> Doc:
617 match asks:
618 case []:
619 doc.lockable(lockable.named(ns, packs, ts, hub), ns, hub, packs, ts)
620 case h <> _t:
621 Stop{"ez: " ++ h ++ " was never read, so there is nothing to lock it from"}
622
623# the names settled: a name that stops the lock stops it, and otherwise the
624# lock records each name it needed with the hash the table gives it
625def doc.named(
626 clean: Bool,
627 why: String,
628 asks: List<&2, String>,
629 +ns: List<&2, L.Name>,
630 +need: List<&2, String>,
631 +hub: String,
632 packs: List<&2, L.Pack>,
633 ts: List<&2, M.Tool>
634) -> Doc:
635 match clean:
636 case True{}:
637 doc.asks(asks, L.name.pairs(ns, need), hub, packs, ts)
638 case False{}:
639 Stop{why}
640
641# a refusal wins over everything after it
642def doc.bad(
643 clean: Bool,
644 +bad: String,
645 asks: List<&2, String>,
646 +led: M.Read,
647 listing: W.Listing,
648 +ns: List<&2, L.Name>,
649 +js: List<&2, W.Judged>,
650 +packs: List<&2, L.Pack>
651) -> Doc:
652 match clean:
653 case True{}:
654 +fs = listing.files(listing)
655 +need = List.append(&2, String, roots.named(fs), packs.named(js, packs))
656 +why = names.why(L.ledger.names(led), ns, js, roots.named(fs), need)
657 doc.named(String.is_empty(why), why, asks, ns, need, M.hub_of(led), packs,
658 M.tools_of(led))
659 case False{}:
660 Stop{bad}
661
662# the walk's result
663def doc.walk(wk: Walk, +led: M.Read, listing: W.Listing, +ns: List<&2, L.Name>, +js: List<&2, W.Judged>) -> Doc:
664 Walk{_queue, packs, asks, +bad} = wk
665 doc.bad(String.is_empty(bad), bad, asks, led, listing, ns, js, packs)
666
667# a tool pin a plain lock would have to fill stops it before the walk counts
668def doc.gap(
669 clean: Bool,
670 +gap: String,
671 wk: Walk,
672 +led: M.Read,
673 listing: W.Listing,
674 +ns: List<&2, L.Name>,
675 +js: List<&2, W.Judged>
676) -> Doc:
677 match clean:
678 case True{}:
679 doc.walk(wk, led, listing, ns, js)
680 case False{}:
681 Stop{gap}
682
683# plain `ez lock` over a parsed ledger
684def decide.led(+led: M.Read, +listing: W.Listing, +js: List<&2, W.Judged>) -> Doc:
685 +gap = need.why(led)
686 +ns = table(led, js)
687 doc.gap(String.is_empty(gap), gap, walk.of(led, listing, ns, js), led, listing, ns, js)
688
689# an upgrade that refused, or still asks, stops the lock before anything else
690def decide.stop(clean: Bool, +stop: String, +led: M.Read, listing: W.Listing, +js: List<&2, W.Judged>) -> Doc:
691 match clean:
692 case True{}:
693 decide.led(led, listing, js)
694 case False{}:
695 Stop{stop}
696
697# `ez lock` over what the lock may depend on
698def decide(ins: W.Inputs) -> Doc:
699 W.Inputs{+ledger, +stop, listing, +js} = ins
700 decide.stop(String.is_empty(stop), stop, ledger, listing, js)
701
702# ---------------------------------------------------------------------------
703# the plan
704
705# where a package is laid: `.ez/lib`, which is committed for a vendored
706# dependency, or `$BEND_LIB`, which is a cache
707type Place is Data:
708 Committed{}
709 Cache{}
710
711# one thing the interpreter does. A `Lay` writes each file under
712# `<place>/<hash>`, in order; the manifest comes last, so a tree that has one
713# is a tree that finished. A `Drop` removes `.ez/lib/<hash>`, a committed tree
714# an upgrade moved off. A `Name` writes `$BEND_LIB/names/<nv>` holding the
715# hash and a newline, the file bend reads a name's hash from before it would
716# ask the hub (EZ-HUB-4). A `Say` is a line a person is told.
717type Effect is Data:
718 Write{path: String, text: String}
719 Lay{place: Place, hash: String, files: List<&2, W.Source>}
720 Drop{hash: String}
721 Say{text: String}
722 Name{nv: String, hash: String}
723
724# how the command ends: exit 0, or exit 1 with the reason
725type Outcome is Data:
726 Success{}
727 Refused{why: String}
728
729# the effects, in order, and how the command ends after them
730type Plan is Data:
731 Plan{effects: List<&2, Effect>, outcome: Outcome}
732
733# the status a command exits with (EZ-OUT-1): 0 when it succeeds, 1 when it
734# refuses. Every planner's plan ends with an Outcome, and the interpreter
735# exits with this status of it (`Run.end`), so this is the one place the
736# mapping lives.
737def status(outcome: Outcome) -> U32:
738 match outcome:
739 case Success{}:
740 0
741 case Refused{_why}:
742 1
743
744# the status of a command that refused, or did not
745def status.of(refused: Bool) -> U32:
746 Bool.pick(U32, refused, 1, 0)
747
748# what a command that ends is said to have ended on: nothing when it
749# succeeded, and the reason when it refused
750def why(outcome: Outcome) -> String:
751 match outcome:
752 case Success{}:
753 ""
754 case Refused{why}:
755 why
756
757# the status a plan ends with
758def code(pl: Plan) -> U32:
759 Plan{_es, o} = pl
760 status(o)
761
762# one of the upgrade's questions, as the upgrade looks its answer up (`ask`,
763# with the source the ledger records) and as git is asked it (`git`, with
764# that source anchored at the directory the lock runs in, `Up.anchor`). The
765# interpreter answers `git` and records the answer under `ask`.
766type Query is Data:
767 Query{ask: Up.Ask, git: Up.Ask}
768
769# the questions still open, the lock's and the upgrade's, or the plan once
770# there are none. The lock's are about packages, and, apart from them, about
771# names: the lock being rewritten, and what the hub says a name names.
772type Step is Data:
773 Asking{asks: List<&2, W.Ask>, ups: List<&2, Query>, names: List<&2, W.Ask>}
774 Run{plan: Plan}
775
776# what a plan leaves at one path: nothing written, or this text
777type Wrote is Data:
778 Kept{}
779 Put{text: String}
780
781# the lock's path, which is fixed
782def lockfile() -> String:
783 "ez.lock.toml"
784
785# every hash a clone answered: those are the trees a lock lays under BEND_LIB
786# for the build that follows
787def cloned.how(how: W.How, +hash: String) -> List<&2, String>:
788 match how:
789 case W.Lib{}:
790 []
791 case W.Clone{_nar}:
792 [hash]
793 case W.Served{}:
794 []
795
796def cloned.one(+hash: String, answer: W.Answer) -> List<&2, String>:
797 match answer:
798 case W.Miss{_why}:
799 []
800 case W.Got{how, _manifest, _ss}:
801 cloned.how(how, hash)
802 case W.Said{_text}:
803 []
804 case W.Locked{_text}:
805 []
806
807# every hash a clone answered, in the order they were answered
808def cloned(rs: List<&2, W.Reply>) -> List<&2, String>:
809 match rs:
810 case []:
811 []
812 case W.Reply{ask, answer} <> t:
813 List.append(&2, String, cloned.one(W.ask.hash(ask), answer), cloned(t))
814
815# where a tree is laid: under `.ez/lib`, committed, when the ledger the lock
816# is made from vendors its hash, and under BEND_LIB otherwise
817def place.of(vend: Bool) -> Place:
818 match vend:
819 case True{}:
820 Committed{}
821 case False{}:
822 Cache{}
823
824# a checked tree laid, ahead of what follows: its files with their texts,
825# then the manifest of those texts, which is the manifest whose name the
826# verdict checked (`W.body.laid`)
827def lay.verdict(verdict: W.Verdict, place: Place, +hash: String, rest: List<&2, Effect>) -> List<&2, Effect>:
828 match verdict:
829 case W.No{_why}:
830 rest
831 case W.Ok{fs, ss}:
832 +ls = W.laid(fs, ss)
833 Lay{place, hash, List.append(&2, W.Source, ls,
834 [W.Source{"manifest", W.manifest(ls)}])} <> rest
835 case W.Named{_ns}:
836 rest
837
838def lay.pick(hit: Bool, verdict: W.Verdict, place: Place, +hash: String, rest: List<&2, Effect>) -> List<&2, Effect>:
839 match hit:
840 case True{}:
841 lay.verdict(verdict, place, hash, rest)
842 case False{}:
843 rest
844
845# every cloned tree that passed its checks, laid ahead of what follows. `vs`
846# are the hashes committed under `.ez/lib`; a plain lock has none, and lays
847# every tree in the cache.
848def lays(
849 +cs: List<&2, String>,
850 +vs: List<&2, String>,
851 js: List<&2, W.Judged>,
852 rest: List<&2, Effect>
853) -> List<&2, Effect>:
854 match js:
855 case []:
856 rest
857 case W.Judged{+h, v} <> t:
858 lay.pick(L.seen.holds(cs, h), v, place.of(L.seen.holds(vs, h)), h,
859 lays(cs, vs, t, rest))
860
861# a names file for every name a lock records, ahead of what follows, so bend
862# reads each name's hash from BEND_LIB and never asks the hub for it
863def names.lay(ns: List<&2, L.Name>, rest: List<&2, Effect>) -> List<&2, Effect>:
864 match ns:
865 case []:
866 rest
867 case L.Name{nv, hash} <> t:
868 Name{nv, hash} <> names.lay(t, rest)
869
870# the lines a person is told, ahead of what follows
871def says.of(ss: List<&2, String>, rest: List<&2, Effect>) -> List<&2, Effect>:
872 match ss:
873 case []:
874 rest
875 case h <> t:
876 Say{h} <> says.of(t, rest)
877
878# the lines a list of effects says, in order
879def said.in(es: List<&2, Effect>) -> List<&2, String>:
880 match es:
881 case []:
882 []
883 case e <> t:
884 match e:
885 case Say{text}:
886 text <> said.in(t)
887 case _:
888 said.in(t)
889
890# the lines a plan says, in order, which is what a person is told
891def said(pl: Plan) -> List<&2, String>:
892 Plan{es, _outcome} = pl
893 said.in(es)
894
895# the files the upgrade writes, in order, ahead of what follows
896def edits.of(es: List<&2, Up.Edit>, rest: List<&2, Effect>) -> List<&2, Effect>:
897 match es:
898 case []:
899 rest
900 case Up.Edit{at, text} <> t:
901 Write{at, text} <> edits.of(t, rest)
902
903# the committed trees the upgrade moved off, removed ahead of what follows
904def drops.of(ds: List<&2, String>, rest: List<&2, Effect>) -> List<&2, Effect>:
905 match ds:
906 case []:
907 rest
908 case h <> t:
909 Drop{h} <> drops.of(t, rest)
910
911# the plan for what was decided. A refusal has no effect at all, so a refused
912# lock, plain or upgrade, writes nothing, lays nothing and removes nothing; its
913# reason is how it ends. A lock that is made runs, in order: what a person is
914# told, the trees laid, a names file for every name the lock records (read
915# back from the lock's own text, so the files and the lock never disagree),
916# the upgrade's files (ez.toml once, `.gitignore`, the rewritten sources), the
917# committed trees removed, and the lock last. A plain lock has only the
918# trees, the names and the lock.
919def made(+nx: Up.Next, cs: List<&2, String>, js: List<&2, W.Judged>, doc: Doc) -> Plan:
920 match doc:
921 case Doc{+text}:
922 Plan{says.of(Up.next.says(nx),
923 lays(cs, Up.next.vends(nx), js, names.lay(L.names.read(text),
924 edits.of(Up.next.edits(nx),
925 edits.of(Up.edit.sources(Up.next.found(nx), Up.next.swaps(nx)),
926 drops.of(Up.next.drops(nx), [Write{lockfile(), text}])))))), Success{}}
927 case Stop{why}:
928 Plan{[], Refused{why}}
929
930# `ez lock` once the upgrade decided: every package checked once, and the plan
931def plan.of(+nx: Up.Next, listing: W.Listing, +replies: List<&2, W.Reply>) -> Plan:
932 +js = W.judged.of(nx, replies)
933 made(nx, cloned(W.replies.of(nx, replies)), js,
934 decide(W.Inputs{Up.next.read(nx), Up.next.stop(nx), W.listing.of(nx, listing), js}))
935
936# `ez lock` over a World
937def plan(world: W.World) -> Plan:
938 W.World{args, +ledger, listing, replies, ups} = world
939 plan.of(W.next(args, ledger, ups), listing, replies)
940
941# whether what was decided is a refusal
942def refuses.doc(doc: Doc) -> Bool:
943 match doc:
944 case Doc{_text}:
945 False{}
946 case Stop{_why}:
947 True{}
948
949# whether `ez lock` refuses on a World
950def refuses(world: W.World) -> Bool:
951 refuses.doc(decide(W.inputs(world)))
952
953# the packages `ez lock` resolves once the upgrade decided
954def packs.of(+nx: Up.Next, listing: W.Listing, replies: List<&2, W.Reply>) -> List<&2, L.Pack>:
955 +js = W.judged.of(nx, replies)
956 walk.packs(walk.of(Up.next.read(nx), W.listing.of(nx, listing),
957 table(Up.next.read(nx), js), js))
958
959# the packages `ez lock` resolves on a World, which are the packages its lock
960# renders when it writes one
961def packs(world: W.World) -> List<&2, L.Pack>:
962 W.World{args, +ledger, listing, replies, ups} = world
963 packs.of(W.next(args, ledger, ups), listing, replies)
964
965# the ledger model an upgrade renders into ez.toml; ez.toml as it was when
966# the upgrade refuses, still asks, or moves nothing, and for a plain lock. The
967# upgrade's decisions (EZ-RES-4 to 6 and 8) are stated over it, and hold of
968# ez.toml's bytes, which read back as it (EZ-LED-4).
969def ledger.next(world: W.World) -> M.Read:
970 W.World{args, +ledger, _listing, _replies, ups} = world
971 Up.next.model(W.next(args, ledger, ups))
972
973# ---------------------------------------------------------------------------
974# what a plan writes
975
976# one effect's part in what a path is left holding, with what the effects
977# after it leave already known. A later write wins.
978def put.one(effect: Effect, +path: String, rest: Wrote) -> Wrote:
979 match effect:
980 case Write{at, text}:
981 match rest:
982 case Kept{}:
983 Bool.pick(Wrote, String.eq(at, path), Put{text}, Kept{})
984 case Put{later}:
985 Put{later}
986 case Lay{_place, _hash, _files}:
987 rest
988 case Drop{_hash}:
989 rest
990 case Say{_text}:
991 rest
992 case Name{_nv, _hash}:
993 rest
994
995# what a list of effects leaves at a path
996def put.in(es: List<&2, Effect>, +path: String) -> Wrote:
997 match es:
998 case []:
999 Kept{}
1000 case e <> t:
1001 put.one(e, path, put.in(t, path))
1002
1003# what a plan leaves at a path
1004def put.plan(pl: Plan, +path: String) -> Wrote:
1005 Plan{es, _outcome} = pl
1006 put.in(es, path)
1007
1008# the bytes `ez lock` writes to a path on a World, or Kept when it writes
1009# none
1010def put(world: W.World, +path: String) -> Wrote:
1011 put.plan(plan(world), path)
1012
1013# whether an effect writes, lays or removes anything
1014def writes.one(effect: Effect) -> Bool:
1015 match effect:
1016 case Write{_at, _text}:
1017 True{}
1018 case Lay{_place, _hash, _files}:
1019 True{}
1020 case Drop{_hash}:
1021 True{}
1022 case Say{_text}:
1023 False{}
1024 case Name{_nv, _hash}:
1025 True{}
1026
1027# whether a plan writes, lays or removes anything
1028def writes.in(es: List<&2, Effect>) -> Bool:
1029 match es:
1030 case []:
1031 False{}
1032 case e <> t:
1033 +rest = writes.in(t)
1034 Bool.or(writes.one(e), rest)
1035
1036# whether a plan writes, lays or removes anything at all
1037def writes(pl: Plan) -> Bool:
1038 Plan{es, _outcome} = pl
1039 writes.in(es)
1040
1041# ---------------------------------------------------------------------------
1042# the trees a plan lays, named (EZ-HASH-3). Law vocabulary for `ez lock`,
1043# `ez add` and `ez fetch`, which lay trees through the same effect.
1044
1045# every file of a tree but the last, which a `Lay` writes as the manifest
1046def front(fs: List<&2, W.Source>) -> List<&2, W.Source>:
1047 match fs:
1048 case []:
1049 []
1050 case h <> t:
1051 match t:
1052 case []:
1053 []
1054 case h2 <> t2:
1055 h <> front(h2 <> t2)
1056
1057# a file's text
1058def text.of(src: W.Source) -> String:
1059 W.Source{_at, text} = src
1060 text
1061
1062# the last file's text, "" for no file
1063def last(fs: List<&2, W.Source>) -> String:
1064 match fs:
1065 case []:
1066 ""
1067 case h <> t:
1068 match t:
1069 case []:
1070 text.of(h)
1071 case h2 <> t2:
1072 last(h2 <> t2)
1073
1074# whether a laid tree is named by the manifest of the texts it lays: its
1075# last file is the manifest of the others, each weighed by its own text,
1076# and its hash is the `0x` name of that manifest
1077def lay.named(+hash: String, +fs: List<&2, W.Source>) -> Bool:
1078 +sums = K.files_of(W.weighs(front(fs)))
1079 Bool.and(String.eq(hash, K.hash_of(sums)), String.eq(last(fs), K.manifest_of(sums)))
1080
1081# whether an effect that lays a tree lays a named one
1082def lays.one(effect: Effect) -> Bool:
1083 match effect:
1084 case Lay{_place, hash, fs}:
1085 lay.named(hash, fs)
1086 case _:
1087 True{}
1088
1089# whether every tree a list of effects lays is named by its manifest
1090def lays.named(es: List<&2, Effect>) -> Bool:
1091 match es:
1092 case []:
1093 True{}
1094 case h <> t:
1095 +rest = lays.named(t)
1096 Bool.and(lays.one(h), rest)
1097
1098# the effects of a plan
1099def effects(pl: Plan) -> List<&2, Effect>:
1100 Plan{es, _outcome} = pl
1101 es
1102
1103# ---------------------------------------------------------------------------
1104# what is still to ask
1105
1106# an answer as far as `wants` needs it: the texts it may import from, taken
1107# as they are and not checked, or the miss
1108def scan.one(+hash: String, +hub: String, answer: W.Answer) -> W.Verdict:
1109 match answer:
1110 case W.Miss{why}:
1111 W.No{why}
1112 case W.Got{_how, _manifest, ss}:
1113 W.Ok{[], ss}
1114 case W.Said{text}:
1115 W.said(hash, hub, text)
1116 case W.Locked{text}:
1117 W.locked(text)
1118
1119# the hub a question names, "" for the lock's
1120def ask.hub(ask: W.Ask) -> String:
1121 match ask:
1122 case W.Pkg{_hash, _src, hub}:
1123 hub
1124 case W.Name{_nv, hub}:
1125 hub
1126 case W.Lock{}:
1127 ""
1128
1129# every reply, scanned
1130def scan(rs: List<&2, W.Reply>) -> List<&2, W.Judged>:
1131 match rs:
1132 case []:
1133 []
1134 case W.Reply{+ask, answer} <> t:
1135 W.Judged{W.ask.hash(ask), scan.one(W.ask.hash(ask), ask.hub(ask), answer)} <> scan(t)
1136
1137# a package's source as git is to read it: a path anchored at the directory
1138# the lock runs in (`Path.anchor`), and a URL or an absolute path as it is. A
1139# hub package names no source. The lock records the source as the ledger
1140# gives it; only the question carries this.
1141def anchor.src(+here: String, src: L.Src) -> L.Src:
1142 match src:
1143 case L.Hub{}:
1144 L.Hub{}
1145 case L.Git{url, rev, entry, root, nar, tag}:
1146 L.Git{Path.anchor(here, url), rev, entry, root, nar, tag}
1147
1148# each hash still to ask about, as the question for it, with its source
1149# anchored
1150def asks.of(hs: List<&2, String>, +here: String, +orgs: List<&2, L.Origin>, +hub: String) -> List<&2, W.Ask>:
1151 match hs:
1152 case []:
1153 []
1154 case +h <> t:
1155 W.Pkg{h, anchor.src(here, L.origin(orgs, h)), hub} <> asks.of(t, here, orgs, hub)
1156
1157# nothing more is asked once the lock is known to refuse
1158def wants.bad(
1159 clean: Bool,
1160 hs: List<&2, String>,
1161 +here: String,
1162 +orgs: List<&2, L.Origin>,
1163 +hub: String
1164) -> List<&2, W.Ask>:
1165 match clean:
1166 case True{}:
1167 asks.of(List.reverse(&2, String, hs), here, orgs, hub)
1168 case False{}:
1169 []
1170
1171def wants.walk(wk: Walk, +here: String, +orgs: List<&2, L.Origin>, +hub: String) -> List<&2, W.Ask>:
1172 Walk{_queue, _packs, hs, +bad} = wk
1173 wants.bad(String.is_empty(bad), hs, here, orgs, hub)
1174
1175def wants.gap(clean: Bool, wk: Walk, +here: String, +orgs: List<&2, L.Origin>, +hub: String) -> List<&2, W.Ask>:
1176 match clean:
1177 case True{}:
1178 wants.walk(wk, here, orgs, hub)
1179 case False{}:
1180 []
1181
1182def wants.led(+here: String, +led: M.Read, listing: W.Listing, +js: List<&2, W.Judged>) -> List<&2, W.Ask>:
1183 wants.gap(String.is_empty(need.why(led)), walk.of(led, listing, table(led, js), js), here,
1184 L.ledger.read(led), M.hub_of(led))
1185
1186# nothing is asked about packages while the upgrade refuses or still asks
1187def wants.stop(clean: Bool, +here: String, +led: M.Read, listing: W.Listing, js: List<&2, W.Judged>) -> List<&2, W.Ask>:
1188 match clean:
1189 case True{}:
1190 wants.led(here, led, listing, js)
1191 case False{}:
1192 []
1193
1194# the package questions still open once the upgrade decided
1195def wants.of(+here: String, +nx: Up.Next, listing: W.Listing, replies: List<&2, W.Reply>) -> List<&2, W.Ask>:
1196 wants.stop(String.is_empty(Up.next.stop(nx)), here, Up.next.read(nx),
1197 W.listing.of(nx, listing), scan(W.replies.of(nx, replies)))
1198
1199# the package questions a World still leaves open: every package the walk
1200# reaches that nothing has answered for yet. None once the lock is known to
1201# refuse, and none while the upgrade still asks.
1202def wants(world: W.World) -> List<&2, W.Ask>:
1203 W.World{+args, +ledger, listing, replies, ups} = world
1204 wants.of(W.here.of(args), W.next(args, ledger, ups), listing, replies)
1205
1206# the upgrade's questions a World still leaves open; none for a plain lock
1207def wants.up(world: W.World) -> List<&2, Up.Ask>:
1208 W.World{args, +ledger, _listing, _replies, ups} = world
1209 Up.next.asks(W.next(args, ledger, ups))
1210
1211# whether the upgrade has every answer it needs
1212def answered(world: W.World) -> Bool:
1213 List.is_empty(&2, Up.Ask, wants.up(world))
1214
1215# each of the upgrade's questions, with the question git is asked for it
1216def queries(+here: String, as: List<&2, Up.Ask>) -> List<&2, Query>:
1217 match as:
1218 case []:
1219 []
1220 case +h <> t:
1221 Query{h, Up.anchor(here, h)} <> queries(here, t)
1222
1223# ---------------------------------------------------------------------------
1224# the names still to resolve
1225
1226# a name kept among the ones to ask about when it is not one already kept
1227def names.once.put(dup: Bool, +nv: String, rest: List<&2, String>) -> List<&2, String>:
1228 match dup:
1229 case True{}:
1230 rest
1231 case False{}:
1232 nv <> rest
1233
1234# every name of a list once, each where it first stands
1235def names.once(nvs: List<&2, String>, +seen: List<&2, String>) -> List<&2, String>:
1236 match nvs:
1237 case []:
1238 []
1239 case +h <> t:
1240 names.once.put(L.seen.holds(seen, h), h, names.once(t, h <> seen))
1241
1242# whether nothing has answered for a key
1243def is.absent(found: Look) -> Bool:
1244 match found:
1245 case Absent{}:
1246 True{}
1247 case Found{_v}:
1248 False{}
1249
1250# whether a name is still to be resolved: the hub rules it in, the table does
1251# not resolve it, the project does not import it (that one ez.toml must
1252# record, and the lock refuses it), and nothing has answered for it yet
1253def name.open(+ns: List<&2, L.Name>, +js: List<&2, W.Judged>, +roots: List<&2, String>, +nv: String) -> Bool:
1254 Bool.and(K.name.ok(nv), Bool.and(String.is_empty(L.name.find(ns, nv)),
1255 Bool.and(Bool.not(L.seen.holds(roots, nv)), is.absent(look(js, nv)))))
1256
1257def names.open.put(open: Bool, +nv: String, rest: List<&2, String>) -> List<&2, String>:
1258 match open:
1259 case True{}:
1260 nv <> rest
1261 case False{}:
1262 rest
1263
1264# every name of a list still to be resolved
1265def names.open(
1266 +ns: List<&2, L.Name>,
1267 +js: List<&2, W.Judged>,
1268 +roots: List<&2, String>,
1269 nvs: List<&2, String>
1270) -> List<&2, String>:
1271 match nvs:
1272 case []:
1273 []
1274 case +h <> t:
1275 names.open.put(name.open(ns, js, roots, h), h, names.open(ns, js, roots, t))
1276
1277# each name as the question the hub is asked about it
1278def names.asked(+hub: String, nvs: List<&2, String>) -> List<&2, W.Ask>:
1279 match nvs:
1280 case []:
1281 []
1282 case h <> t:
1283 W.Name{h, hub} <> names.asked(hub, t)
1284
1285# the questions for the names still open: none when there are none; the lock
1286# being rewritten first, since a name it records is not put to the hub
1287# (EZ-HUB-2); and then each name the lock does not record, once
1288def names.ask.lock(read: Bool, +hub: String, nvs: List<&2, String>) -> List<&2, W.Ask>:
1289 match read:
1290 case True{}:
1291 names.asked(hub, names.once(nvs, []))
1292 case False{}:
1293 [W.Lock{}]
1294
1295def names.ask(+hub: String, +js: List<&2, W.Judged>, nvs: List<&2, String>) -> List<&2, W.Ask>:
1296 match nvs:
1297 case []:
1298 []
1299 case h <> t:
1300 names.ask.lock(Bool.not(is.absent(look(js, W.lockfile()))), hub, h <> t)
1301
1302def names.wants.bad(
1303 clean: Bool,
1304 +led: M.Read,
1305 listing: W.Listing,
1306 +ns: List<&2, L.Name>,
1307 +js: List<&2, W.Judged>,
1308 packs: List<&2, L.Pack>
1309) -> List<&2, W.Ask>:
1310 match clean:
1311 case True{}:
1312 names.ask(M.hub_of(led), js,
1313 names.open(ns, js, roots.named(listing.files(listing)), packs.named(js, packs)))
1314 case False{}:
1315 []
1316
1317# the name questions of a walk, which ask nothing once the lock is known to
1318# refuse
1319def names.wants.walk(
1320 wk: Walk,
1321 +led: M.Read,
1322 +listing: W.Listing,
1323 +ns: List<&2, L.Name>,
1324 +js: List<&2, W.Judged>
1325) -> List<&2, W.Ask>:
1326 Walk{_queue, packs, _asks, +bad} = wk
1327 names.wants.bad(String.is_empty(bad), led, listing, ns, js, packs)
1328
1329def names.wants.gap(clean: Bool, +led: M.Read, +listing: W.Listing, +js: List<&2, W.Judged>) -> List<&2, W.Ask>:
1330 match clean:
1331 case True{}:
1332 +ns = table(led, js)
1333 names.wants.walk(walk.of(led, listing, ns, js), led, listing, ns, js)
1334 case False{}:
1335 []
1336
1337def names.wants.stop(clean: Bool, +led: M.Read, listing: W.Listing, +js: List<&2, W.Judged>) -> List<&2, W.Ask>:
1338 match clean:
1339 case True{}:
1340 names.wants.gap(String.is_empty(need.why(led)), led, listing, js)
1341 case False{}:
1342 []
1343
1344# the name questions still open once the upgrade decided, over the answers
1345# scanned as `wants` scans them
1346def names.wants.of(+nx: Up.Next, listing: W.Listing, replies: List<&2, W.Reply>) -> List<&2, W.Ask>:
1347 names.wants.stop(String.is_empty(Up.next.stop(nx)), Up.next.read(nx),
1348 W.listing.of(nx, listing), scan(W.replies.of(nx, replies)))
1349
1350# the name questions a World still leaves open: for every name a package the
1351# walk reaches imports, that ez.toml does not record and nothing has
1352# answered for yet, the lock being rewritten, and once it is read, the hub
1353def wants.names(world: W.World) -> List<&2, W.Ask>:
1354 W.World{+args, +ledger, listing, replies, ups} = world
1355 names.wants.of(W.next(args, ledger, ups), listing, replies)
1356
1357# the package questions still open, or the name questions, or the plan once
1358# there are none
1359def step.names(names: List<&2, W.Ask>, +nx: Up.Next, listing: W.Listing, replies: List<&2, W.Reply>) -> Step:
1360 match names:
1361 case []:
1362 Run{plan.of(nx, listing, replies)}
1363 case h <> t:
1364 Asking{[], [], h <> t}
1365
1366def step.pkgs(
1367 asks: List<&2, W.Ask>,
1368 names: List<&2, W.Ask>,
1369 +nx: Up.Next,
1370 listing: W.Listing,
1371 replies: List<&2, W.Reply>
1372) -> Step:
1373 match asks:
1374 case []:
1375 step.names(names, nx, listing, replies)
1376 case h <> t:
1377 Asking{h <> t, [], names}
1378
1379# the upgrade's questions first, since the packages the lock reads depend on
1380# where the pins move
1381def step.ups(
1382 ups: List<&2, Up.Ask>,
1383 +here: String,
1384 +nx: Up.Next,
1385 +listing: W.Listing,
1386 +replies: List<&2, W.Reply>
1387) -> Step:
1388 match ups:
1389 case []:
1390 step.pkgs(wants.of(here, nx, listing, replies), names.wants.of(nx, listing, replies), nx,
1391 listing, replies)
1392 case h <> t:
1393 Asking{[], queries(here, h <> t), []}
1394
1395# what the interpreter does next on a World. The upgrade is decided once per
1396# round, and every answer of the round is read from that one decision.
1397def step(world: W.World) -> Step:
1398 W.World{+args, +ledger, +listing, +replies, ups} = world
1399 +nx = W.next(args, ledger, ups)
1400 step.ups(Up.next.asks(nx), W.here.of(args), nx, listing, replies)