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 538 # the source a lock question names 539 def ask.src(ask: W.Ask) -> L.Src: 540 match ask: 541 case W.Pkg{_hash, src, _hub}: 542 src 543 case W.Name{_nv, _hub}: 544 L.Hub{} 545 case W.Lock{}: 546 L.Hub{} 547 548 # a tree BEND_LIB held, as the answer the lock reads, ahead of the rest. One 549 # it did not hold answers nothing, so the lock goes on asking for it. 550 def tree.put(+ask: W.Ask, answer: DW.Answer, rest: List<&2, W.Reply>) -> List<&2, W.Reply>: 551 match answer: 552 case DW.Laid{manifest, ss}: 553 W.Reply{ask, W.Got{tree.how(ask.src(ask)), manifest, ss}} <> rest 554 case _: 555 rest 556 557 # a reply kept when it answers a lock question 558 def tree.reply(ask: DW.Ask, answer: DW.Answer, rest: List<&2, W.Reply>) -> List<&2, W.Reply>: 559 match ask: 560 case DW.Tree{a}: 561 tree.put(a, answer, rest) 562 case _: 563 rest 564 565 # every tree BEND_LIB held, as the lock's answers 566 def trees(rs: List<&2, DW.Reply>) -> List<&2, W.Reply>: 567 match rs: 568 case []: 569 [] 570 case DW.Reply{ask, answer} <> t: 571 tree.reply(ask, answer, trees(t)) 572 573 # the lock's text, "" when there is none, as the lock's own question reads it 574 def lock.text(lock: Maybe<&2, String>) -> String: 575 match lock: 576 case None{}: 577 "" 578 case Some{text}: 579 text 580 581 # a name's file BEND_LIB held, as the answer the lock reads for the name, 582 # ahead of the rest. One it did not hold answers nothing, so the lock goes on 583 # asking for it. 584 def name.put(+nv: String, got: Maybe<&2, String>, rest: List<&2, W.Reply>) -> List<&2, W.Reply>: 585 match got: 586 case Some{text}: 587 W.Reply{W.Name{nv, "BEND_LIB/names"}, W.Said{text}} <> rest 588 case None{}: 589 rest 590 591 # a reply kept when it answers a name's question 592 def name.reply(ask: DW.Ask, answer: DW.Answer, rest: List<&2, W.Reply>) -> List<&2, W.Reply>: 593 match ask: 594 case DW.Name{nv}: 595 match answer: 596 case DW.Held{got}: 597 name.put(nv, got, rest) 598 case _: 599 rest 600 case _: 601 rest 602 603 # every name's file BEND_LIB held, as the lock's answers 604 def held(rs: List<&2, DW.Reply>) -> List<&2, W.Reply>: 605 match rs: 606 case []: 607 [] 608 case DW.Reply{ask, answer} <> t: 609 name.reply(ask, answer, held(t)) 610 611 # the World doctor puts to the lock planner: a plain lock over the ledger's 612 # text as it is, the sources as git listed them, the lock as it is, which 613 # answers the lock's question about the names it records, every tree 614 # BEND_LIB held, and every name's file it held, never the network. The 615 # directory a lock runs in only anchors the questions it asks, which doctor 616 # answers by hash from BEND_LIB, so it is left empty. 617 def relock.world( 618 ledger: Maybe<&2, String>, 619 lock: Maybe<&2, String>, 620 listing: W.Listing, 621 +rs: List<&2, DW.Reply> 622 ) -> W.World: 623 W.World{W.Args{False{}, "", ""}, ledger, listing, 624 List.append(&2, W.Reply, trees(rs), W.Reply{W.Lock{}, W.Locked{lock.text(lock)}} <> held(rs)), 625 []} 626 627 # that World, over the sources as a list of files 628 def relock(+world: DW.World, fs: List<&2, W.Source>) -> W.World: 629 relock.world(DW.ledger(world), DW.lock(world), W.Listed{fs}, DW.replies(world)) 630 631 # the line for a lock `ez lock` would write as it is 632 def fresh.line() -> String: 633 DW.lockfile() ++ ": up to date with ez.toml and the sources" 634 635 # the line for one it would write otherwise 636 def stale.line() -> String: 637 DW.lockfile() ++ ": out of date with ez.toml and the sources; run ez lock" 638 639 # the verdict, once the two texts were compared 640 def fresh.said(same: Bool) -> Note: 641 match same: 642 case True{}: 643 Note{False{}, fresh.line()} 644 case False{}: 645 Note{True{}, stale.line()} 646 647 # the lock against what the lock planner decided: the text it would write, or 648 # why it would write none 649 def fresh.doc(doc: P.Doc, +text: String) -> Note: 650 match doc: 651 case P.Doc{made}: 652 fresh.said(String.eq(made, text)) 653 case P.Stop{why}: 654 Note{True{}, DW.lockfile() ++ ": not checked, since ez lock refuses here: " ++ why} 655 656 # the hashes of lock questions 657 def ask.hashes(as: List<&2, W.Ask>) -> List<&2, String>: 658 match as: 659 case []: 660 [] 661 case h <> t: 662 W.ask.hash(h) <> ask.hashes(t) 663 664 # the lock, once every name the lock planner asks about is known: when none 665 # is left, what it decided; when some are, their files are not under 666 # BEND_LIB, and doctor does not ask the hub for them 667 def fresh.named(as: List<&2, W.Ask>, +lw: W.World, +text: String) -> Note: 668 match as: 669 case []: 670 fresh.doc(P.decide(W.inputs(lw)), text) 671 case h <> t: 672 Note{True{}, DW.lockfile() ++ ": not checked, since BEND_LIB lacks names/" ++ 673 String.join(ask.hashes(h <> t), " names/") ++ " and doctor does not use the network"} 674 675 # the lock, once every question the lock planner asks is known: when none is 676 # left, its names, then what it decided; when some are, they are about 677 # packages BEND_LIB does not hold, and doctor does not ask the network for 678 # them 679 def fresh.away(as: List<&2, W.Ask>, +lw: W.World, +text: String) -> Note: 680 match as: 681 case []: 682 fresh.named(P.wants.names(lw), lw, text) 683 case h <> t: 684 Note{True{}, DW.lockfile() ++ ": not checked, since BEND_LIB lacks " ++ 685 String.join(ask.hashes(h <> t), " ") ++ " and doctor does not use the network"} 686 687 # the check over the sources git listed. Sources git could not list are the 688 # drift report's to say. 689 def fresh.listing(+world: DW.World, listing: W.Listing, +text: String) -> List<&2, Note>: 690 match listing: 691 case W.Listed{fs}: 692 +lw = relock(world, fs) 693 [fresh.away(P.wants(lw), lw, text)] 694 case W.Unlisted{_why}: 695 [] 696 697 # the check, once the sources were heard. They are asked for only when the 698 # ledger reads, so a ledger that does not is not checked against. 699 def fresh.heard(got: DW.Heard, +world: DW.World, +text: String) -> List<&2, Note>: 700 match got: 701 case DW.Heard{a}: 702 match a: 703 case DW.Listed{l}: 704 fresh.listing(world, l, text) 705 case _: 706 [] 707 case DW.Open{}: 708 [] 709 710 # the check over a lock that is there. With no lock there is nothing to 711 # compare, and the lock's own line says whether that is a problem. 712 def fresh.notes(lock: Maybe<&2, String>, +world: DW.World) -> List<&2, Note>: 713 match lock: 714 case None{}: 715 [] 716 case Some{text}: 717 fresh.heard(DW.sources(world), world, text) 718 719 # what doctor says of the lock against the lock `ez lock` would write 720 def checked(+world: DW.World) -> List<&2, Note>: 721 fresh.notes(DW.lock(world), world) 722 723 # each lock question, put to BEND_LIB 724 def trees.asked(as: List<&2, W.Ask>) -> List<&2, DW.Ask>: 725 match as: 726 case []: 727 [] 728 case h <> t: 729 DW.Tree{h} <> trees.asked(t) 730 731 # each of the lock's name questions, put to BEND_LIB as the name's file. The 732 # lock's own question never reaches here, since the World answers it. 733 def names.asked(as: List<&2, W.Ask>) -> List<&2, DW.Ask>: 734 match as: 735 case []: 736 [] 737 case h <> t: 738 DW.Name{W.ask.hash(h)} <> names.asked(t) 739 740 # the lock questions over the sources git listed, packages and then names 741 def trees.world(+lw: W.World) -> List<&2, DW.Ask>: 742 List.append(&2, DW.Ask, trees.asked(P.wants(lw)), names.asked(P.wants.names(lw))) 743 744 def trees.listing( 745 ledger: Maybe<&2, String>, 746 lock: Maybe<&2, String>, 747 listing: W.Listing, 748 rs: List<&2, DW.Reply> 749 ) -> List<&2, DW.Ask>: 750 match listing: 751 case W.Listed{fs}: 752 trees.world(relock.world(ledger, lock, W.Listed{fs}, rs)) 753 case W.Unlisted{_why}: 754 [] 755 756 # the lock questions, once the sources were heard 757 def trees.heard( 758 got: DW.Heard, 759 ledger: Maybe<&2, String>, 760 lock: Maybe<&2, String>, 761 rs: List<&2, DW.Reply> 762 ) -> List<&2, DW.Ask>: 763 match got: 764 case DW.Heard{a}: 765 match a: 766 case DW.Listed{l}: 767 trees.listing(ledger, lock, l, rs) 768 case _: 769 [] 770 case DW.Open{}: 771 [] 772 773 # every package and name the lock planner asks about, while there is a lock 774 # to check. These depend on the sources and on the trees already read, so 775 # they come in rounds, as the lock's own do. 776 def trees.of( 777 ledger: Maybe<&2, String>, 778 lock: Maybe<&2, String>, 779 held: Maybe<&2, String>, 780 +rs: List<&2, DW.Reply> 781 ) -> List<&2, DW.Ask>: 782 match lock: 783 case None{}: 784 [] 785 case Some{_text}: 786 trees.heard(DW.heard(rs, DW.Sources{}), ledger, held, rs) 787 788 # --------------------------------------------------------------------------- 789 # the plan 790 791 # the ledger as doctor sees it: its package, its entry and its dependencies. 792 # Its `[tools.*]` pins are not dependencies, so they are dropped before 793 # anything but the lock check is decided (EZ-LED-5). The lock check reads the 794 # ledger's text as `ez lock` does, since the lock records the pins. 795 def ledger.seen(ledger: M.Read) -> M.Read: 796 match ledger: 797 case M.Bad{why}: 798 M.Bad{why} 799 case M.Good{m}: 800 M.Manifest{n, e, b, h, pa, pv, ds, _ts} = m 801 M.Good{M.Manifest{n, e, b, h, pa, pv, ds, []}} 802 803 # the toolchain, the ledger, the lock and the library, each on its line, and 804 # the lock check's line after them 805 def head.notes( 806 +ledger: M.Read, 807 +cc: String, 808 +lib: String, 809 +lock: Maybe<&2, String>, 810 +rs: List<&2, DW.Reply>, 811 cs: List<&2, Note> 812 ) -> List<&2, Note>: 813 +lk = locked.of(lock) 814 List.append(&2, Note, probes(programs(cc), rs), 815 List.append(&2, Note, 816 [ledger.note(ledger), lock.note(deps.count(ledger), lk), 817 lib.note(lib.at(lib), locked.hashes(lk), lock.names(lock), rs)], cs)) 818 819 # every line doctor says, in order: the toolchain, the ledger, the lock, the 820 # library, the lock check, and last the drift report over the sources as they 821 # were heard 822 def notes.of( 823 +ledger: M.Read, 824 got: DW.Heard, 825 +cc: String, 826 +lib: String, 827 lock: Maybe<&2, String>, 828 +rs: List<&2, DW.Reply>, 829 cs: List<&2, Note> 830 ) -> List<&2, Note>: 831 List.append(&2, Note, head.notes(ledger, cc, lib, lock, rs, cs), imports.notes(ledger, got)) 832 833 # whether a note is a problem 834 def note.wrong(finding: Note) -> Bool: 835 Note{w, _l} = finding 836 w 837 838 # whether anything found was a problem. The rest arrives as a thunk and only 839 # the arm that wants it applies it. 840 def wrong(ns: List<&2, Note>) -> Bool: 841 match ns: 842 case []: 843 False{} 844 case h <> t: 845 has.step(note.wrong(h), _u => wrong(t)) 846 847 # each line said 848 def says(ns: List<&2, Note>) -> List<&2, P.Effect>: 849 match ns: 850 case []: 851 [] 852 case Note{_w, l} <> t: 853 P.Say{l} <> says(t) 854 855 # a problem must fail the command, or a script around it learns nothing. The 856 # lines already said what the problem is. 857 def outcome.of(bad: Bool) -> P.Outcome: 858 match bad: 859 case True{}: 860 P.Refused{""} 861 case False{}: 862 P.Success{} 863 864 # the plan for these notes: every line said, and the command failed when one 865 # of them is a problem 866 def plan.notes(+ns: List<&2, Note>) -> P.Plan: 867 P.Plan{says(ns), outcome.of(wrong(ns))} 868 869 # the plan over what a World read, field by field, with the lock check's 870 # lines 871 def plan.of( 872 +ledger: M.Read, 873 +cc: String, 874 +lib: String, 875 lock: Maybe<&2, String>, 876 +rs: List<&2, DW.Reply>, 877 cs: List<&2, Note> 878 ) -> P.Plan: 879 plan.notes(notes.of(ledger, DW.heard(rs, DW.Sources{}), cc, lib, lock, rs, cs)) 880 881 # `ez doctor` over a World 882 def plan(+world: DW.World) -> P.Plan: 883 plan.of(ledger.seen(DW.read(world)), DW.cc(world), DW.lib(world), DW.lock(world), 884 DW.replies(world), checked(world)) 885 886 # whether a plan ends in a refusal 887 def refuses.plan(pl: P.Plan) -> Bool: 888 P.Plan{_es, outcome} = pl 889 match outcome: 890 case P.Success{}: 891 False{} 892 case P.Refused{_why}: 893 True{} 894 895 # whether `ez doctor` fails on a World 896 def refuses(+world: DW.World) -> Bool: 897 refuses.plan(plan(world)) 898 899 # --------------------------------------------------------------------------- 900 # what is still to ask 901 902 # a question kept when it has no answer yet 903 def open.put(got: DW.Heard, ask: DW.Ask, rest: List<&2, DW.Ask>) -> List<&2, DW.Ask>: 904 match got: 905 case DW.Heard{_a}: 906 rest 907 case DW.Open{}: 908 ask <> rest 909 910 # the questions of a list that have no answer yet 911 def open(asks: List<&2, DW.Ask>, +rs: List<&2, DW.Reply>) -> List<&2, DW.Ask>: 912 match asks: 913 case []: 914 [] 915 case +h <> t: 916 open.put(DW.heard(rs, h), h, open(t, rs)) 917 918 # a version question for every program 919 def probe.asks(ns: List<&2, String>) -> List<&2, DW.Ask>: 920 match ns: 921 case []: 922 [] 923 case h <> t: 924 DW.Probe{h} <> probe.asks(t) 925 926 # the library is looked in only when the lock names a package to look for 927 def lib.asks(+dir: String, hs: List<&2, String>) -> List<&2, DW.Ask>: 928 match hs: 929 case []: 930 [] 931 case _h <> _t: 932 [DW.Lib{dir}] 933 934 # the sources are read only when there is a ledger to compare them with 935 def sources.asks(ledger: M.Read) -> List<&2, DW.Ask>: 936 match ledger: 937 case M.Bad{_why}: 938 [] 939 case M.Good{_m}: 940 [DW.Sources{}] 941 942 # a question for every name's file the lock records 943 def names.asks(ns: List<&2, L.Name>) -> List<&2, DW.Ask>: 944 match ns: 945 case []: 946 [] 947 case L.Name{nv, _hash} <> t: 948 DW.Name{nv} <> names.asks(t) 949 950 # every question doctor asks on what a World read, besides the lock check's 951 def asks.of(ledger: M.Read, +cc: String, +lib: String, +lock: Maybe<&2, String>) -> List<&2, DW.Ask>: 952 List.append(&2, DW.Ask, probe.asks(programs(cc)), 953 List.append(&2, DW.Ask, lib.asks(lib.at(lib), locked.hashes(locked.of(lock))), 954 List.append(&2, DW.Ask, names.asks(lock.names(lock)), sources.asks(ledger)))) 955 956 # the questions still open, or the plan once there are none 957 type Step is Data: 958 Asking{asks: List<&2, DW.Ask>} 959 Run{plan: P.Plan} 960 961 # the step, once the open questions are known 962 def step.asks( 963 asks: List<&2, DW.Ask>, 964 +ledger: M.Read, 965 +cc: String, 966 +lib: String, 967 lock: Maybe<&2, String>, 968 +rs: List<&2, DW.Reply>, 969 cs: List<&2, Note> 970 ) -> Step: 971 match asks: 972 case []: 973 Run{plan.of(ledger, cc, lib, lock, rs, cs)} 974 case h <> t: 975 Asking{h <> t} 976 977 # the step over what a World read, field by field, with the lock check's 978 # questions and lines. The first round asks everything but the lock check's 979 # trees, which follow the sources and each other, as the lock's own questions 980 # do. 981 def step.of( 982 +ledger: M.Read, 983 +cc: String, 984 +lib: String, 985 +lock: Maybe<&2, String>, 986 +rs: List<&2, DW.Reply>, 987 ts: List<&2, DW.Ask>, 988 cs: List<&2, Note> 989 ) -> Step: 990 step.asks(open(List.append(&2, DW.Ask, asks.of(ledger, cc, lib, lock), ts), rs), 991 ledger, cc, lib, lock, rs, cs) 992 993 # what the interpreter does next on a World 994 def step(+world: DW.World) -> Step: 995 step.of(ledger.seen(DW.read(world)), DW.cc(world), DW.lib(world), DW.lock(world), 996 DW.replies(world), trees.of(DW.ledger(world), DW.lock(world), DW.lock(world), DW.replies(world)), 997 checked(world)) 998 999 # the questions a World still leaves open 1000 def wants(+world: DW.World) -> List<&2, DW.Ask>: 1001 +ledger = ledger.seen(DW.read(world)) 1002 open(List.append(&2, DW.Ask, asks.of(ledger, DW.cc(world), DW.lib(world), DW.lock(world)), 1003 trees.of(DW.ledger(world), DW.lock(world), DW.lock(world), DW.replies(world))), DW.replies(world)) 1004 1005 # --------------------------------------------------------------------------- 1006 # law vocabulary: ez never calls these 1007 1008 # whether an effect says this line 1009 def said.one(effect: P.Effect, +line: String) -> Bool: 1010 match effect: 1011 case P.Say{text}: 1012 String.eq(text, line) 1013 case P.Write{_at, _text}: 1014 False{} 1015 case P.Lay{_place, _hash, _files}: 1016 False{} 1017 case P.Drop{_hash}: 1018 False{} 1019 case P.Name{_nv, _hash}: 1020 False{} 1021 1022 # whether a list of effects says this line 1023 def said.in(es: List<&2, P.Effect>, +line: String) -> Bool: 1024 match es: 1025 case []: 1026 False{} 1027 case e <> t: 1028 has.step(said.one(e, line), _u => said.in(t, line)) 1029 1030 # whether a plan says this line 1031 def said(pl: P.Plan, +line: String) -> Bool: 1032 P.Plan{es, _outcome} = pl 1033 said.in(es, line) 1034 1035 # the drift report doctor makes of a World's sources against a ledger's 1036 # dependencies 1037 def drift.of(fs: List<&2, W.Source>, +ds: List<&2, M.Dep>) -> List<&2, String>: 1038 +hs = imports(fs) 1039 drift(hs, ds)