1 # doctor/plan: `ez doctor` as a pure planner. It reads a World 2 # (doctor/world.bend) and returns either the questions it still needs 3 # answered (`step`) or a Plan: the lines to print and how the command ends 4 # (`plan`). The interpreter (doctor/run.bend) loops on `step`, answering each 5 # question by IO, until nothing is left to ask, then prints the plan. 6 # 7 # Doctor says what would stop this project from building. Each line reports 8 # what was found rather than a tick, because the useful answers here are 9 # values: which bend, which compiler, which packages are on disk, and which 10 # hashes the source and the ledger disagree about. A line that is a problem 11 # fails the command, as `cargo check` fails on the first thing it cannot 12 # build, though doctor goes on to say everything it found. 13 # 14 # The plan is lines said and an outcome, and nothing else: doctor writes no 15 # file, lays no tree and removes nothing (EZ-VEN-4). The questions it asks 16 # write nothing either: a program asked for its version, `ls` of the library, 17 # git's listing of the tracked sources, and a package's files read from under 18 # BEND_LIB. 19 # 20 # A named import, `import @/...`, is read the same way: its 21 # name is what the import line names, reported when no dependency of the 22 # ledger records it, and a dependency recorded by a name is used when an 23 # import line names it by that name or by its hash. Each name the lock 24 # records has its file under `BEND_LIB/names`, which bend reads the name's 25 # hash from before it asks the hub; one missing, or naming another hash, is 26 # reported with the library, and fails the command. 27 # 28 # The drift report is the ledger against the import lines. A package's `0x` 29 # hash is written in two places, the `[deps.]` section of ez.toml and 30 # the `import 0x...` line that uses it, so the two can disagree. ez treats the 31 # ledger as the record and the import lines as the truth, and reports the 32 # disagreement both ways rather than rewriting anyone's source (EZ-VEN-5). An 33 # import line is one the lock reads as one: in the header of a `.bend` file 34 # git tracks outside `.ez/`, found by the package walk's own scan (`P.roots`), 35 # so doctor and `ez lock` never disagree about what the source imports. A 36 # ledger's `[tools.*]` pins are not dependencies, and the report never reads 37 # them (EZ-LED-5). 38 # 39 # The lock is checked the way `cargo --locked` and `uv lock --check` check 40 # one, without the network: doctor puts a plain `ez lock` to the lock's own 41 # planner (lock/plan.bend), over the ledger as it is, tools and all, the 42 # sources git lists, and the trees BEND_LIB holds, and compares the text it 43 # would write with ez.lock.toml byte for byte (EZ-VEN-6). So every hash, 44 # source, file sum and tool pin the lock records is checked, by the code that 45 # writes them. A package the lock would read and BEND_LIB lacks is one doctor 46 # cannot see, and the lock is then said to be unchecked rather than guessed 47 # at; that, a lock ez lock would refuse to write, and a lock it would write 48 # otherwise, each fail the command. 49 # 50 # The Effect, Plan and Outcome types are the lock's (lock/plan.bend), as for 51 # the other commands. 52 import Base 53 import ../toml/toml.bend as T 54 import ./world.bend as DW 55 import ../lock/plan.bend as P 56 import ../lock/world.bend as W 57 import ../lock/lock.bend as L 58 import ../ledger/manifest.bend as M 59 import ../share/args.bend as Args 60 61 # one thing doctor looked at: what to print, and whether it is a problem 62 type Note is Data: 63 Note{wrong: Bool, line: String} 64 65 # a count with its noun, kept in agreement 66 def plural(+count: Nat, one: String, many: String) -> String: 67 Nat.show(count) ++ " " ++ Bool.pick(String, Nat.is_eq(count, 1n), one, many) 68 69 # the lines a run printed, or none when it failed, so a program's complaint on 70 # stderr is never mistaken for an answer 71 def answered(ok: Bool, +text: String) -> List<&2, String>: 72 match ok: 73 case True{}: 74 Args.nonempty(String.lines(text)) 75 case False{}: 76 [] 77 78 # --------------------------------------------------------------------------- 79 # the toolchain 80 81 # the first line of what a program printed 82 def banner.of(ls: List<&2, String>) -> String: 83 match ls: 84 case []: 85 "" 86 case h <> _t: 87 h 88 89 # a version banner, which is one line however many a program prints 90 def banner(text: String) -> String: 91 banner.of(String.lines(text)) 92 93 # what a version run said about the program it ran 94 def probe.note(ok: Bool, name: String, +text: String) -> Note: 95 match ok: 96 case True{}: 97 Note{False{}, name ++ ": " ++ banner(text)} 98 case False{}: 99 Note{True{}, name ++ ": not on PATH"} 100 101 # what the answer to a version question says 102 def probe.answer(answer: DW.Answer, +name: String) -> Note: 103 match answer: 104 case DW.Printed{ok, text}: 105 probe.note(ok, name, text) 106 case _: 107 Note{True{}, name ++ ": not on PATH"} 108 109 # a program's line, once it was asked 110 def probe.heard(got: DW.Heard, +name: String) -> Note: 111 match got: 112 case DW.Heard{a}: 113 probe.answer(a, name) 114 case DW.Open{}: 115 Note{True{}, name ++ ": not on PATH"} 116 117 # the C compiler the native lane will use: `$CC` when it is set, else `cc`. 118 # Same source and a different compiler is a different binary, so doctor names 119 # it. 120 def cc.name(+var: String) -> String: 121 Bool.pick(String, String.is_empty(var), "cc", var) 122 123 # the programs doctor asks for their versions. bend's check report changed 124 # wording in 2.0.32, which the proof gate reads, and the native lane needs a C 125 # compiler, so the version is the finding. 126 def programs(+cc: String) -> List<&2, String>: 127 ["bend", cc.name(cc), "git"] 128 129 # every program's line 130 def probes(ns: List<&2, String>, +rs: List<&2, DW.Reply>) -> List<&2, Note>: 131 match ns: 132 case []: 133 [] 134 case +h <> t: 135 probe.heard(DW.heard(rs, DW.Probe{h}), h) <> probes(t, rs) 136 137 # --------------------------------------------------------------------------- 138 # the ledger and the lock 139 140 # the dependencies of a ledger that was read 141 def deps.of(manifest: M.Manifest) -> List<&2, M.Dep>: 142 M.Manifest{_n, _e, _b, _h, _pa, _pv, ds, _ts} = manifest 143 ds 144 145 # how many dependencies a ledger names; one that did not read names none 146 def deps.count(ledger: M.Read) -> Nat: 147 match ledger: 148 case M.Bad{_why}: 149 0n 150 case M.Good{m}: 151 List.length(&2, M.Dep, deps.of(m)) 152 153 # what the ledger turned out to be 154 def ledger.note(ledger: M.Read) -> Note: 155 match ledger: 156 case M.Bad{why}: 157 Note{True{}, DW.toml() ++ ": " ++ why} 158 case M.Good{m}: 159 M.Manifest{n, e, _b, _h, _pa, _pv, ds, _ts} = m 160 Note{False{}, DW.toml() ++ ": " ++ n ++ ", entry " ++ e ++ ", " ++ 161 plural(List.length(&2, M.Dep, ds), "dependency", "dependencies")} 162 163 # what the lock turned out to be: the packages it records, or why it could 164 # not be read, or no lock at all 165 type Locked is Data: 166 Locked{hashes: List<&2, String>} 167 Unread{why: String} 168 Unlocked{} 169 170 # the verdict on a parse, with the answer in hand 171 def locked.pick(ok: Bool, why: String, ss: List<&2, T.Sect>) -> Locked: 172 match ok: 173 case True{}: 174 Locked{L.hashes(ss)} 175 case False{}: 176 Unread{why} 177 178 # a lock that parsed, by the packages it records; one that did not, by why 179 def locked.parsed(toml: T.Toml) -> Locked: 180 T.Toml{+bad, _name, _pairs, ss} = toml 181 locked.pick(String.is_empty(bad), bad, ss) 182 183 # the lock's text, read 184 def locked.of(got: Maybe<&2, String>) -> Locked: 185 match got: 186 case None{}: 187 Unlocked{} 188 case Some{text}: 189 locked.parsed(T.parse(text)) 190 191 # the packages a lock records; none when it did not read or is not there 192 def locked.hashes(lk: Locked) -> List<&2, String>: 193 match lk: 194 case Locked{hs}: 195 hs 196 case Unread{_why}: 197 [] 198 case Unlocked{}: 199 [] 200 201 # whether a ledger with this many dependencies needs a lock that names some 202 def lock.needs(deps: Nat) -> Bool: 203 match deps: 204 case 0n: 205 False{} 206 case 1n+_n: 207 True{} 208 209 # whether the lock is a problem: it names no package while the ledger has 210 # dependencies it should have resolved. A project with no dependencies has 211 # nothing to lock, so a lock naming nothing, or no lock at all, is its due. 212 def lock.wrong(deps: Nat, hs: List<&2, String>) -> Bool: 213 match hs: 214 case []: 215 lock.needs(deps) 216 case _h <> _t: 217 False{} 218 219 # what the lock amounts to. A lock that is there and does not parse is a 220 # problem whatever the ledger holds, since `ez fetch` refuses it, as cargo 221 # refuses a Cargo.lock it cannot read. 222 def lock.note(+deps: Nat, lk: Locked) -> Note: 223 match lk: 224 case Locked{+hs}: 225 +wrong = lock.wrong(deps, hs) 226 Note{wrong, 227 DW.lockfile() ++ ": " ++ plural(List.length(&2, String, hs), "package", "packages") ++ 228 Bool.pick(String, wrong, " (it names none; run ez lock)", "")} 229 case Unread{why}: 230 Note{True{}, DW.lockfile() ++ ": " ++ why} 231 case Unlocked{}: 232 +wrong = lock.needs(deps) 233 Note{wrong, DW.lockfile() ++ ": " ++ 234 Bool.pick(String, wrong, "none; run ez lock", "none, and nothing to lock")} 235 236 # --------------------------------------------------------------------------- 237 # BEND_LIB 238 239 # where the packages this build resolves against live. It is what ez itself 240 # passes to every bend it starts, so an unset BEND_LIB is not a finding: the 241 # default is `.ez/lib` beside the project, and what matters is what is in it. 242 def lib.at(+var: String) -> String: 243 Bool.pick(String, String.is_empty(var), ".ez/lib", var) 244 245 # the names `ls` gave for the library, none when it gave none 246 def lib.have(got: DW.Heard) -> List<&2, String>: 247 match got: 248 case DW.Heard{a}: 249 match a: 250 case DW.Printed{ok, text}: 251 answered(ok, text) 252 case _: 253 [] 254 case DW.Open{}: 255 [] 256 257 # the rest of a scan only when this one was not it. `Bool.or` reduces both 258 # of its sides, so a scan written with it reads the whole list even when the 259 # first entry answers. The rest arrives as a thunk. 260 def has.step(here: Bool, rest: Unit -> Bool) -> Bool: 261 match here: 262 case True{}: 263 True{} 264 case False{}: 265 rest(Unit{}) 266 267 # whether a list holds this string 268 def has(ss: List<&2, String>, +want: String) -> Bool: 269 match ss: 270 case []: 271 False{} 272 case +h <> t: 273 has.step(String.eq(h, want), _u => has(t, want)) 274 275 # a string kept when the other list lacks it 276 def missing.put(hit: Bool, item: String, rest: List<&2, String>) -> List<&2, String>: 277 match hit: 278 case True{}: 279 rest 280 case False{}: 281 item <> rest 282 283 # the strings of `xs` that `known` does not hold 284 def missing(xs: List<&2, String>, +known: List<&2, String>) -> List<&2, String>: 285 match xs: 286 case []: 287 [] 288 case +h <> t: 289 missing.put(has(known, h), h, missing(t, known)) 290 291 # the names the lock records, with the hash each names; none when there is 292 # no lock, or when it does not read 293 def lock.names(lock: Maybe<&2, String>) -> List<&2, L.Name>: 294 match lock: 295 case None{}: 296 [] 297 case Some{text}: 298 L.names.read(text) 299 300 # what a name's file says against the lock: nothing when it holds the lock's 301 # hash, as bend trims it, and the fault otherwise 302 def name.held(same: Bool, +nv: String, +hash: String, +was: String) -> List<&2, String>: 303 match same: 304 case True{}: 305 [] 306 case False{}: 307 ["names/" ++ nv ++ " names " ++ was ++ " where the lock names " ++ hash] 308 309 def name.got(got: Maybe<&2, String>, +nv: String, +hash: String) -> List<&2, String>: 310 match got: 311 case None{}: 312 ["names/" ++ nv ++ " is not there"] 313 case Some{text}: 314 +was = String.trim(text) 315 name.held(String.eq(was, hash), nv, hash, was) 316 317 def name.heard(got: DW.Heard, +nv: String, +hash: String) -> List<&2, String>: 318 match got: 319 case DW.Heard{a}: 320 match a: 321 case DW.Held{m}: 322 name.got(m, nv, hash) 323 case _: 324 ["names/" ++ nv ++ " was not read"] 325 case DW.Open{}: 326 ["names/" ++ nv ++ " was not read"] 327 328 # every name of the lock whose file under BEND_LIB is missing, or names 329 # another hash than the lock does, which bend would read instead of asking 330 # the hub 331 def names.wrong(ns: List<&2, L.Name>, +rs: List<&2, DW.Reply>) -> List<&2, String>: 332 match ns: 333 case []: 334 [] 335 case L.Name{+nv, +hash} <> t: 336 List.append(&2, String, name.heard(DW.heard(rs, DW.Name{nv}), nv, hash), names.wrong(t, rs)) 337 338 # what the library holds of the lock's names, said after its packages 339 def names.said(+count: Nat) -> String: 340 match count: 341 case 0n: 342 "" 343 case 1n+_p: 344 " with their " ++ plural(count, "name", "names") 345 346 # what is wrong with the library, each fault in its place 347 def lib.faults(gone: List<&2, String>, bad: List<&2, String>) -> String: 348 match gone: 349 case []: 350 String.join(bad, "; ") 351 case _g <> _t: 352 match bad: 353 case []: 354 "lacks " ++ String.join(gone, " ") 355 case _b <> _u: 356 "lacks " ++ String.join(gone, " ") ++ "; " ++ String.join(bad, "; ") 357 358 # BEND_LIB judged against the lock: every package the lock names has to be on 359 # disk, because bend resolves a hub import during the check and a sandbox has 360 # no network to fall back on, and so does every name's file, since bend 361 # would otherwise ask the hub what the name names 362 def lib.said(all: Bool, +dir: String, +count: Nat, +names: Nat, gone: List<&2, String>, bad: List<&2, String>) -> Note: 363 match all: 364 case True{}: 365 Note{False{}, "BEND_LIB: " ++ dir ++ ", all " ++ 366 plural(count, "locked package", "locked packages") ++ " present" ++ names.said(names)} 367 case False{}: 368 Note{True{}, "BEND_LIB: " ++ dir ++ " " ++ lib.faults(gone, bad) ++ "; run ez fetch"} 369 370 # the verdict, once what is wanted and what is there are both known 371 def lib.judge( 372 +dir: String, 373 +want: List<&2, String>, 374 +ns: List<&2, L.Name>, 375 have: List<&2, String>, 376 +rs: List<&2, DW.Reply> 377 ) -> Note: 378 +gone = missing(want, have) 379 +bad = names.wrong(ns, rs) 380 lib.said(Bool.and(List.is_empty(&2, String, gone), List.is_empty(&2, String, bad)), dir, 381 List.length(&2, String, want), List.length(&2, L.Name, ns), gone, bad) 382 383 # the library's line 384 def lib.note(+dir: String, +want: List<&2, String>, +ns: List<&2, L.Name>, +rs: List<&2, DW.Reply>) -> Note: 385 lib.judge(dir, want, ns, lib.have(DW.heard(rs, DW.Lib{dir})), rs) 386 387 # --------------------------------------------------------------------------- 388 # the drift report 389 390 # a head kept when the rest does not hold it again 391 def once.put(dup: Bool, item: String, rest: List<&2, String>) -> List<&2, String>: 392 match dup: 393 case True{}: 394 rest 395 case False{}: 396 item <> rest 397 398 # every string of a list once, each where it last stands 399 def once(ss: List<&2, String>) -> List<&2, String>: 400 match ss: 401 case []: 402 [] 403 case +h <> +t: 404 once.put(has(t, h), h, once(t)) 405 406 # every hub package the project's sources import, each once, by hash and by 407 # name. These are the lock's roots: the header imports of every tracked 408 # `.bend` file outside `.ez/`, as the package walk scans them. 409 def imports(+fs: List<&2, W.Source>) -> List<&2, String>: 410 once(List.append(&2, String, P.roots(fs), P.roots.named(fs))) 411 412 # a dependency's name, when the ledger records it by one 413 def pinned.at(none: Bool, nv: String, rest: List<&2, String>) -> List<&2, String>: 414 match none: 415 case True{}: 416 rest 417 case False{}: 418 nv <> rest 419 420 def pinned.nv(+nv: String, rest: List<&2, String>) -> List<&2, String>: 421 pinned.at(String.is_empty(nv), nv, rest) 422 423 # the hashes a ledger's dependencies name, and the names it records them by 424 def pinned(ds: List<&2, M.Dep>) -> List<&2, String>: 425 match ds: 426 case []: 427 [] 428 case +h <> t: 429 M.dep.hash(h) <> pinned.nv(M.source.nv(M.source.dep(h)), pinned(t)) 430 431 # whether an import line names a dependency, by its hash or by its name 432 def used(+hs: List<&2, String>, +dependency: M.Dep) -> Bool: 433 +nv = M.source.nv(M.source.dep(dependency)) 434 Bool.or(has(hs, M.dep.hash(dependency)), 435 Bool.and(Bool.not(String.is_empty(nv)), has(hs, nv))) 436 437 # how a hash imported and not in the ledger reads 438 def unrecorded.line(+hash: String) -> String: 439 "drift: " ++ hash ++ " is imported but ez.toml does not name it" 440 441 # every hash imported that the ledger does not name 442 def unrecorded(hs: List<&2, String>) -> List<&2, String>: 443 match hs: 444 case []: 445 [] 446 case h <> t: 447 unrecorded.line(h) <> unrecorded(t) 448 449 # how a ledger entry nobody imports reads 450 def unused.line(+dependency: M.Dep) -> String: 451 "drift: " ++ M.dep.name(dependency) ++ " = " ++ M.dep.hash(dependency) ++ 452 " is in ez.toml but no import line uses it" 453 454 # a dependency reported when nothing imports it 455 def unused.put(hit: Bool, +dependency: M.Dep, rest: List<&2, String>) -> List<&2, String>: 456 match hit: 457 case True{}: 458 rest 459 case False{}: 460 unused.line(dependency) <> rest 461 462 # every dependency of the ledger that no import line uses 463 def unused(ds: List<&2, M.Dep>, +hs: List<&2, String>) -> List<&2, String>: 464 match ds: 465 case []: 466 [] 467 case +h <> t: 468 unused.put(used(hs, h), h, unused(t, hs)) 469 470 # both directions of the disagreement: a hash the source imports that the 471 # ledger never names, and a ledger entry no import line uses 472 def drift(+hs: List<&2, String>, +ds: List<&2, M.Dep>) -> List<&2, String>: 473 List.append(&2, String, unrecorded(missing(hs, pinned(ds))), unused(ds, hs)) 474 475 # every line of a report, each a problem 476 def flag(ls: List<&2, String>) -> List<&2, Note>: 477 match ls: 478 case []: 479 [] 480 case h <> t: 481 Note{True{}, h} <> flag(t) 482 483 # the report, or the one line that says there is nothing to report 484 def drift.notes(+hs: List<&2, String>, lines: List<&2, String>) -> List<&2, Note>: 485 match lines: 486 case []: 487 [Note{False{}, "imports: " ++ plural(List.length(&2, String, hs), "package", "packages") ++ 488 " imported, every one named in ez.toml"}] 489 case h <> t: 490 flag(h <> t) 491 492 # the report over what git listed, or why it could not list 493 def listing.notes(listing: W.Listing, +ds: List<&2, M.Dep>) -> List<&2, Note>: 494 match listing: 495 case W.Listed{fs}: 496 +hs = imports(fs) 497 drift.notes(hs, drift(hs, ds)) 498 case W.Unlisted{_why}: 499 [Note{True{}, "imports: git could not list the .bend files it tracks here"}] 500 501 # the report, once the sources were asked for 502 def sources.notes(got: DW.Heard, +ds: List<&2, M.Dep>) -> List<&2, Note>: 503 match got: 504 case DW.Heard{a}: 505 match a: 506 case DW.Listed{l}: 507 listing.notes(l, ds) 508 case _: 509 [Note{True{}, "imports: the sources were not read"}] 510 case DW.Open{}: 511 [Note{True{}, "imports: the sources were not read"}] 512 513 # the report over a ledger. One that did not read is its own problem, and 514 # there is nothing to compare the source with. 515 def imports.notes(ledger: M.Read, got: DW.Heard) -> List<&2, Note>: 516 match ledger: 517 case M.Bad{_why}: 518 [Note{False{}, "imports: not compared, since ez.toml was not read"}] 519 case M.Good{m}: 520 sources.notes(got, deps.of(m)) 521 522 # --------------------------------------------------------------------------- 523 # the lock against the lock `ez lock` would write 524 525 # how a tree read from BEND_LIB arrived, as the lock judges it. A git 526 # package's tree under BEND_LIB is one `ez lock` reads from there too. A hub 527 # package's is one `ez lock` would have the hub serve: its name is the digest 528 # of its manifest and each file is checked against its sum, so bytes that pass 529 # are the bytes the hub serves under that name, and bytes that do not are 530 # refused either way. 531 def tree.how(src: L.Src) -> W.How: 532 match src: 533 case L.Hub{}: 534 W.Served{} 535 case L.Git{_url, _rev, _entry, _root, _nar, _tag}: 536 W.Lib{} 537 case L.Member{_path, _entry}: 538 W.Lib{} 539 540 # the source a lock question names 541 def ask.src(ask: W.Ask) -> L.Src: 542 match ask: 543 case W.Pkg{_hash, src, _hub}: 544 src 545 case W.Name{_nv, _hub}: 546 L.Hub{} 547 case W.Lock{}: 548 L.Hub{} 549 550 # a tree BEND_LIB held, as the answer the lock reads, ahead of the rest. One 551 # it did not hold answers nothing, so the lock goes on asking for it. 552 def tree.put(+ask: W.Ask, answer: DW.Answer, rest: List<&2, W.Reply>) -> List<&2, W.Reply>: 553 match answer: 554 case DW.Laid{manifest, ss}: 555 W.Reply{ask, W.Got{tree.how(ask.src(ask)), manifest, ss}} <> rest 556 case _: 557 rest 558 559 # a reply kept when it answers a lock question 560 def tree.reply(ask: DW.Ask, answer: DW.Answer, rest: List<&2, W.Reply>) -> List<&2, W.Reply>: 561 match ask: 562 case DW.Tree{a}: 563 tree.put(a, answer, rest) 564 case _: 565 rest 566 567 # every tree BEND_LIB held, as the lock's answers 568 def trees(rs: List<&2, DW.Reply>) -> List<&2, W.Reply>: 569 match rs: 570 case []: 571 [] 572 case DW.Reply{ask, answer} <> t: 573 tree.reply(ask, answer, trees(t)) 574 575 # the lock's text, "" when there is none, as the lock's own question reads it 576 def lock.text(lock: Maybe<&2, String>) -> String: 577 match lock: 578 case None{}: 579 "" 580 case Some{text}: 581 text 582 583 # a name's file BEND_LIB held, as the answer the lock reads for the name, 584 # ahead of the rest. One it did not hold answers nothing, so the lock goes on 585 # asking for it. 586 def name.put(+nv: String, got: Maybe<&2, String>, rest: List<&2, W.Reply>) -> List<&2, W.Reply>: 587 match got: 588 case Some{text}: 589 W.Reply{W.Name{nv, "BEND_LIB/names"}, W.Said{text}} <> rest 590 case None{}: 591 rest 592 593 # a reply kept when it answers a name's question 594 def name.reply(ask: DW.Ask, answer: DW.Answer, rest: List<&2, W.Reply>) -> List<&2, W.Reply>: 595 match ask: 596 case DW.Name{nv}: 597 match answer: 598 case DW.Held{got}: 599 name.put(nv, got, rest) 600 case _: 601 rest 602 case _: 603 rest 604 605 # every name's file BEND_LIB held, as the lock's answers 606 def held(rs: List<&2, DW.Reply>) -> List<&2, W.Reply>: 607 match rs: 608 case []: 609 [] 610 case DW.Reply{ask, answer} <> t: 611 name.reply(ask, answer, held(t)) 612 613 # the World doctor puts to the lock planner: a plain lock over the ledger's 614 # text as it is, the sources as git listed them, the lock as it is, which 615 # answers the lock's question about the names it records, every tree 616 # BEND_LIB held, and every name's file it held, never the network. The 617 # directory a lock runs in only anchors the questions it asks, which doctor 618 # answers by hash from BEND_LIB, so it is left empty. 619 def relock.world( 620 ledger: Maybe<&2, String>, 621 lock: Maybe<&2, String>, 622 listing: W.Listing, 623 +rs: List<&2, DW.Reply> 624 ) -> W.World: 625 W.World{W.Args{False{}, "", ""}, ledger, listing, 626 List.append(&2, W.Reply, trees(rs), W.Reply{W.Lock{}, W.Locked{lock.text(lock)}} <> held(rs)), 627 []} 628 629 # that World, over the sources as a list of files 630 def relock(+world: DW.World, fs: List<&2, W.Source>) -> W.World: 631 relock.world(DW.ledger(world), DW.lock(world), W.Listed{fs}, DW.replies(world)) 632 633 # the line for a lock `ez lock` would write as it is 634 def fresh.line() -> String: 635 DW.lockfile() ++ ": up to date with ez.toml and the sources" 636 637 # the line for one it would write otherwise 638 def stale.line() -> String: 639 DW.lockfile() ++ ": out of date with ez.toml and the sources; run ez lock" 640 641 # the verdict, once the two texts were compared 642 def fresh.said(same: Bool) -> Note: 643 match same: 644 case True{}: 645 Note{False{}, fresh.line()} 646 case False{}: 647 Note{True{}, stale.line()} 648 649 # the lock against what the lock planner decided: the text it would write, or 650 # why it would write none 651 def fresh.doc(doc: P.Doc, +text: String) -> Note: 652 match doc: 653 case P.Doc{made}: 654 fresh.said(String.eq(made, text)) 655 case P.Stop{why}: 656 Note{True{}, DW.lockfile() ++ ": not checked, since ez lock refuses here: " ++ why} 657 658 # the hashes of lock questions 659 def ask.hashes(as: List<&2, W.Ask>) -> List<&2, String>: 660 match as: 661 case []: 662 [] 663 case h <> t: 664 W.ask.hash(h) <> ask.hashes(t) 665 666 # the lock, once every name the lock planner asks about is known: when none 667 # is left, what it decided; when some are, their files are not under 668 # BEND_LIB, and doctor does not ask the hub for them 669 def fresh.named(as: List<&2, W.Ask>, +lw: W.World, +text: String) -> Note: 670 match as: 671 case []: 672 fresh.doc(P.decide(W.inputs(lw)), text) 673 case h <> t: 674 Note{True{}, DW.lockfile() ++ ": not checked, since BEND_LIB lacks names/" ++ 675 String.join(ask.hashes(h <> t), " names/") ++ " and doctor does not use the network"} 676 677 # the lock, once every question the lock planner asks is known: when none is 678 # left, its names, then what it decided; when some are, they are about 679 # packages BEND_LIB does not hold, and doctor does not ask the network for 680 # them 681 def fresh.away(as: List<&2, W.Ask>, +lw: W.World, +text: String) -> Note: 682 match as: 683 case []: 684 fresh.named(P.wants.names(lw), lw, text) 685 case h <> t: 686 Note{True{}, DW.lockfile() ++ ": not checked, since BEND_LIB lacks " ++ 687 String.join(ask.hashes(h <> t), " ") ++ " and doctor does not use the network"} 688 689 # the check over the sources git listed. Sources git could not list are the 690 # drift report's to say. 691 def fresh.listing(+world: DW.World, listing: W.Listing, +text: String) -> List<&2, Note>: 692 match listing: 693 case W.Listed{fs}: 694 +lw = relock(world, fs) 695 [fresh.away(P.wants(lw), lw, text)] 696 case W.Unlisted{_why}: 697 [] 698 699 # the check, once the sources were heard. They are asked for only when the 700 # ledger reads, so a ledger that does not is not checked against. 701 def fresh.heard(got: DW.Heard, +world: DW.World, +text: String) -> List<&2, Note>: 702 match got: 703 case DW.Heard{a}: 704 match a: 705 case DW.Listed{l}: 706 fresh.listing(world, l, text) 707 case _: 708 [] 709 case DW.Open{}: 710 [] 711 712 # the check over a lock that is there. With no lock there is nothing to 713 # compare, and the lock's own line says whether that is a problem. 714 def fresh.notes(lock: Maybe<&2, String>, +world: DW.World) -> List<&2, Note>: 715 match lock: 716 case None{}: 717 [] 718 case Some{text}: 719 fresh.heard(DW.sources(world), world, text) 720 721 # what doctor says of the lock against the lock `ez lock` would write 722 def checked(+world: DW.World) -> List<&2, Note>: 723 fresh.notes(DW.lock(world), world) 724 725 # each lock question, put to BEND_LIB 726 def trees.asked(as: List<&2, W.Ask>) -> List<&2, DW.Ask>: 727 match as: 728 case []: 729 [] 730 case h <> t: 731 DW.Tree{h} <> trees.asked(t) 732 733 # each of the lock's name questions, put to BEND_LIB as the name's file. The 734 # lock's own question never reaches here, since the World answers it. 735 def names.asked(as: List<&2, W.Ask>) -> List<&2, DW.Ask>: 736 match as: 737 case []: 738 [] 739 case h <> t: 740 DW.Name{W.ask.hash(h)} <> names.asked(t) 741 742 # the lock questions over the sources git listed, packages and then names 743 def trees.world(+lw: W.World) -> List<&2, DW.Ask>: 744 List.append(&2, DW.Ask, trees.asked(P.wants(lw)), names.asked(P.wants.names(lw))) 745 746 def trees.listing( 747 ledger: Maybe<&2, String>, 748 lock: Maybe<&2, String>, 749 listing: W.Listing, 750 rs: List<&2, DW.Reply> 751 ) -> List<&2, DW.Ask>: 752 match listing: 753 case W.Listed{fs}: 754 trees.world(relock.world(ledger, lock, W.Listed{fs}, rs)) 755 case W.Unlisted{_why}: 756 [] 757 758 # the lock questions, once the sources were heard 759 def trees.heard( 760 got: DW.Heard, 761 ledger: Maybe<&2, String>, 762 lock: Maybe<&2, String>, 763 rs: List<&2, DW.Reply> 764 ) -> List<&2, DW.Ask>: 765 match got: 766 case DW.Heard{a}: 767 match a: 768 case DW.Listed{l}: 769 trees.listing(ledger, lock, l, rs) 770 case _: 771 [] 772 case DW.Open{}: 773 [] 774 775 # every package and name the lock planner asks about, while there is a lock 776 # to check. These depend on the sources and on the trees already read, so 777 # they come in rounds, as the lock's own do. 778 def trees.of( 779 ledger: Maybe<&2, String>, 780 lock: Maybe<&2, String>, 781 held: Maybe<&2, String>, 782 +rs: List<&2, DW.Reply> 783 ) -> List<&2, DW.Ask>: 784 match lock: 785 case None{}: 786 [] 787 case Some{_text}: 788 trees.heard(DW.heard(rs, DW.Sources{}), ledger, held, rs) 789 790 # --------------------------------------------------------------------------- 791 # the plan 792 793 # the ledger as doctor sees it: its package, its entry and its dependencies. 794 # Its `[tools.*]` pins are not dependencies, so they are dropped before 795 # anything but the lock check is decided (EZ-LED-5). The lock check reads the 796 # ledger's text as `ez lock` does, since the lock records the pins. 797 def ledger.seen(ledger: M.Read) -> M.Read: 798 match ledger: 799 case M.Bad{why}: 800 M.Bad{why} 801 case M.Good{m}: 802 M.Manifest{n, e, b, h, pa, pv, ds, _ts} = m 803 M.Good{M.Manifest{n, e, b, h, pa, pv, ds, []}} 804 805 # the toolchain, the ledger, the lock and the library, each on its line, and 806 # the lock check's line after them 807 def head.notes( 808 +ledger: M.Read, 809 +cc: String, 810 +lib: String, 811 +lock: Maybe<&2, String>, 812 +rs: List<&2, DW.Reply>, 813 cs: List<&2, Note> 814 ) -> List<&2, Note>: 815 +lk = locked.of(lock) 816 List.append(&2, Note, probes(programs(cc), rs), 817 List.append(&2, Note, 818 [ledger.note(ledger), lock.note(deps.count(ledger), lk), 819 lib.note(lib.at(lib), locked.hashes(lk), lock.names(lock), rs)], cs)) 820 821 # every line doctor says, in order: the toolchain, the ledger, the lock, the 822 # library, the lock check, and last the drift report over the sources as they 823 # were heard 824 def notes.of( 825 +ledger: M.Read, 826 got: DW.Heard, 827 +cc: String, 828 +lib: String, 829 lock: Maybe<&2, String>, 830 +rs: List<&2, DW.Reply>, 831 cs: List<&2, Note> 832 ) -> List<&2, Note>: 833 List.append(&2, Note, head.notes(ledger, cc, lib, lock, rs, cs), imports.notes(ledger, got)) 834 835 # whether a note is a problem 836 def note.wrong(finding: Note) -> Bool: 837 Note{w, _l} = finding 838 w 839 840 # whether anything found was a problem. The rest arrives as a thunk and only 841 # the arm that wants it applies it. 842 def wrong(ns: List<&2, Note>) -> Bool: 843 match ns: 844 case []: 845 False{} 846 case h <> t: 847 has.step(note.wrong(h), _u => wrong(t)) 848 849 # each line said 850 def says(ns: List<&2, Note>) -> List<&2, P.Effect>: 851 match ns: 852 case []: 853 [] 854 case Note{_w, l} <> t: 855 P.Say{l} <> says(t) 856 857 # a problem must fail the command, or a script around it learns nothing. The 858 # lines already said what the problem is. 859 def outcome.of(bad: Bool) -> P.Outcome: 860 match bad: 861 case True{}: 862 P.Refused{""} 863 case False{}: 864 P.Success{} 865 866 # the plan for these notes: every line said, and the command failed when one 867 # of them is a problem 868 def plan.notes(+ns: List<&2, Note>) -> P.Plan: 869 P.Plan{says(ns), outcome.of(wrong(ns))} 870 871 # the plan over what a World read, field by field, with the lock check's 872 # lines 873 def plan.of( 874 +ledger: M.Read, 875 +cc: String, 876 +lib: String, 877 lock: Maybe<&2, String>, 878 +rs: List<&2, DW.Reply>, 879 cs: List<&2, Note> 880 ) -> P.Plan: 881 plan.notes(notes.of(ledger, DW.heard(rs, DW.Sources{}), cc, lib, lock, rs, cs)) 882 883 # `ez doctor` over a World 884 def plan(+world: DW.World) -> P.Plan: 885 plan.of(ledger.seen(DW.read(world)), DW.cc(world), DW.lib(world), DW.lock(world), 886 DW.replies(world), checked(world)) 887 888 # whether a plan ends in a refusal 889 def refuses.plan(pl: P.Plan) -> Bool: 890 P.Plan{_es, outcome} = pl 891 match outcome: 892 case P.Success{}: 893 False{} 894 case P.Refused{_why}: 895 True{} 896 897 # whether `ez doctor` fails on a World 898 def refuses(+world: DW.World) -> Bool: 899 refuses.plan(plan(world)) 900 901 # --------------------------------------------------------------------------- 902 # what is still to ask 903 904 # a question kept when it has no answer yet 905 def open.put(got: DW.Heard, ask: DW.Ask, rest: List<&2, DW.Ask>) -> List<&2, DW.Ask>: 906 match got: 907 case DW.Heard{_a}: 908 rest 909 case DW.Open{}: 910 ask <> rest 911 912 # the questions of a list that have no answer yet 913 def open(asks: List<&2, DW.Ask>, +rs: List<&2, DW.Reply>) -> List<&2, DW.Ask>: 914 match asks: 915 case []: 916 [] 917 case +h <> t: 918 open.put(DW.heard(rs, h), h, open(t, rs)) 919 920 # a version question for every program 921 def probe.asks(ns: List<&2, String>) -> List<&2, DW.Ask>: 922 match ns: 923 case []: 924 [] 925 case h <> t: 926 DW.Probe{h} <> probe.asks(t) 927 928 # the library is looked in only when the lock names a package to look for 929 def lib.asks(+dir: String, hs: List<&2, String>) -> List<&2, DW.Ask>: 930 match hs: 931 case []: 932 [] 933 case _h <> _t: 934 [DW.Lib{dir}] 935 936 # the sources are read only when there is a ledger to compare them with 937 def sources.asks(ledger: M.Read) -> List<&2, DW.Ask>: 938 match ledger: 939 case M.Bad{_why}: 940 [] 941 case M.Good{_m}: 942 [DW.Sources{}] 943 944 # a question for every name's file the lock records 945 def names.asks(ns: List<&2, L.Name>) -> List<&2, DW.Ask>: 946 match ns: 947 case []: 948 [] 949 case L.Name{nv, _hash} <> t: 950 DW.Name{nv} <> names.asks(t) 951 952 # every question doctor asks on what a World read, besides the lock check's 953 def asks.of(ledger: M.Read, +cc: String, +lib: String, +lock: Maybe<&2, String>) -> List<&2, DW.Ask>: 954 List.append(&2, DW.Ask, probe.asks(programs(cc)), 955 List.append(&2, DW.Ask, lib.asks(lib.at(lib), locked.hashes(locked.of(lock))), 956 List.append(&2, DW.Ask, names.asks(lock.names(lock)), sources.asks(ledger)))) 957 958 # the questions still open, or the plan once there are none 959 type Step is Data: 960 Asking{asks: List<&2, DW.Ask>} 961 Run{plan: P.Plan} 962 963 # the step, once the open questions are known 964 def step.asks( 965 asks: List<&2, DW.Ask>, 966 +ledger: M.Read, 967 +cc: String, 968 +lib: String, 969 lock: Maybe<&2, String>, 970 +rs: List<&2, DW.Reply>, 971 cs: List<&2, Note> 972 ) -> Step: 973 match asks: 974 case []: 975 Run{plan.of(ledger, cc, lib, lock, rs, cs)} 976 case h <> t: 977 Asking{h <> t} 978 979 # the step over what a World read, field by field, with the lock check's 980 # questions and lines. The first round asks everything but the lock check's 981 # trees, which follow the sources and each other, as the lock's own questions 982 # do. 983 def step.of( 984 +ledger: M.Read, 985 +cc: String, 986 +lib: String, 987 +lock: Maybe<&2, String>, 988 +rs: List<&2, DW.Reply>, 989 ts: List<&2, DW.Ask>, 990 cs: List<&2, Note> 991 ) -> Step: 992 step.asks(open(List.append(&2, DW.Ask, asks.of(ledger, cc, lib, lock), ts), rs), 993 ledger, cc, lib, lock, rs, cs) 994 995 # what the interpreter does next on a World 996 def step(+world: DW.World) -> Step: 997 step.of(ledger.seen(DW.read(world)), DW.cc(world), DW.lib(world), DW.lock(world), 998 DW.replies(world), trees.of(DW.ledger(world), DW.lock(world), DW.lock(world), DW.replies(world)), 999 checked(world)) 1000 1001 # the questions a World still leaves open 1002 def wants(+world: DW.World) -> List<&2, DW.Ask>: 1003 +ledger = ledger.seen(DW.read(world)) 1004 open(List.append(&2, DW.Ask, asks.of(ledger, DW.cc(world), DW.lib(world), DW.lock(world)), 1005 trees.of(DW.ledger(world), DW.lock(world), DW.lock(world), DW.replies(world))), DW.replies(world)) 1006 1007 # --------------------------------------------------------------------------- 1008 # law vocabulary: ez never calls these 1009 1010 # whether an effect says this line 1011 def said.one(effect: P.Effect, +line: String) -> Bool: 1012 match effect: 1013 case P.Say{text}: 1014 String.eq(text, line) 1015 case P.Write{_at, _text}: 1016 False{} 1017 case P.Lay{_place, _hash, _files}: 1018 False{} 1019 case P.Drop{_hash}: 1020 False{} 1021 case P.Name{_nv, _hash}: 1022 False{} 1023 1024 # whether a list of effects says this line 1025 def said.in(es: List<&2, P.Effect>, +line: String) -> Bool: 1026 match es: 1027 case []: 1028 False{} 1029 case e <> t: 1030 has.step(said.one(e, line), _u => said.in(t, line)) 1031 1032 # whether a plan says this line 1033 def said(pl: P.Plan, +line: String) -> Bool: 1034 P.Plan{es, _outcome} = pl 1035 said.in(es, line) 1036 1037 # the drift report doctor makes of a World's sources against a ledger's 1038 # dependencies 1039 def drift.of(fs: List<&2, W.Source>, +ds: List<&2, M.Dep>) -> List<&2, String>: 1040 +hs = imports(fs) 1041 drift(hs, ds)