src/doctor/plan.bend source
src/doctor/plan.bend on the hub · documented module
# doctor/plan: `ez doctor` as a pure planner. It reads a World# (doctor/world.bend) and returns either the questions it still needs# answered (`step`) or a Plan: the lines to print and how the command ends# (`plan`). The interpreter (doctor/run.bend) loops on `step`, answering each# question by IO, until nothing is left to ask, then prints the plan.## Doctor says what would stop this project from building. Each line reports# what was found rather than a tick, because the useful answers here are# values: which bend, which compiler, which packages are on disk, and which# hashes the source and the ledger disagree about. A line that is a problem# fails the command, as `cargo check` fails on the first thing it cannot# build, though doctor goes on to say everything it found.## The plan is lines said and an outcome, and nothing else: doctor writes no# file, lays no tree and removes nothing (EZ-VEN-4). The questions it asks# write nothing either: a program asked for its version, `ls` of the library,# git's listing of the tracked sources, and a package's files read from under# BEND_LIB.## A named import, `import <name>@<version>/...`, is read the same way: its# name is what the import line names, reported when no dependency of the# ledger records it, and a dependency recorded by a name is used when an# import line names it by that name or by its hash. Each name the lock# records has its file under `BEND_LIB/names`, which bend reads the name's# hash from before it asks the hub; one missing, or naming another hash, is# reported with the library, and fails the command.## The drift report is the ledger against the import lines. A package's `0x`# hash is written in two places, the `[deps.<name>]` section of ez.toml and# the `import 0x...` line that uses it, so the two can disagree. ez treats the# ledger as the record and the import lines as the truth, and reports the# disagreement both ways rather than rewriting anyone's source (EZ-VEN-5). An# import line is one the lock reads as one: in the header of a `.bend` file# git tracks outside `.ez/`, found by the package walk's own scan (`P.roots`),# so doctor and `ez lock` never disagree about what the source imports. A# ledger's `[tools.*]` pins are not dependencies, and the report never reads# them (EZ-LED-5).## The lock is checked the way `cargo --locked` and `uv lock --check` check# one, without the network: doctor puts a plain `ez lock` to the lock's own# planner (lock/plan.bend), over the ledger as it is, tools and all, the# sources git lists, and the trees BEND_LIB holds, and compares the text it# would write with ez.lock.toml byte for byte (EZ-VEN-6). So every hash,# source, file sum and tool pin the lock records is checked, by the code that# writes them. A package the lock would read and BEND_LIB lacks is one doctor# cannot see, and the lock is then said to be unchecked rather than guessed# at; that, a lock ez lock would refuse to write, and a lock it would write# otherwise, each fail the command.## The Effect, Plan and Outcome types are the lock's (lock/plan.bend), as for# the other commands.import Baseimport ../toml/toml.bend as Timport ./world.bend as DWimport ../lock/plan.bend as Pimport ../lock/world.bend as Wimport ../lock/lock.bend as Limport ../ledger/manifest.bend as Mimport ../share/args.bend as Args# one thing doctor looked at: what to print, and whether it is a problemtype Note is Data: Note{wrong: Bool, line: String}# a count with its noun, kept in agreementdef plural(+count: Nat, one: String, many: String) -> String: Nat.show(count) ++ " " ++ Bool.pick(String, Nat.is_eq(count, 1n), one, many)# the lines a run printed, or none when it failed, so a program's complaint on# stderr is never mistaken for an answerdef answered(ok: Bool, +text: String) -> List<&2, String>: match ok: case True{}: Args.nonempty(String.lines(text)) case False{}: []# ---------------------------------------------------------------------------# the toolchain# the first line of what a program printeddef banner.of(ls: List<&2, String>) -> String: match ls: case []: "" case h <> _t: h# a version banner, which is one line however many a program printsdef banner(text: String) -> String: banner.of(String.lines(text))# what a version run said about the program it randef probe.note(ok: Bool, name: String, +text: String) -> Note: match ok: case True{}: Note{False{}, name ++ ": " ++ banner(text)} case False{}: Note{True{}, name ++ ": not on PATH"}# what the answer to a version question saysdef probe.answer(answer: DW.Answer, +name: String) -> Note: match answer: case DW.Printed{ok, text}: probe.note(ok, name, text) case _: Note{True{}, name ++ ": not on PATH"}# a program's line, once it was askeddef probe.heard(got: DW.Heard, +name: String) -> Note: match got: case DW.Heard{a}: probe.answer(a, name) case DW.Open{}: Note{True{}, name ++ ": not on PATH"}# the C compiler the native lane will use: `$CC` when it is set, else `cc`.# Same source and a different compiler is a different binary, so doctor names# it.def cc.name(+var: String) -> String: Bool.pick(String, String.is_empty(var), "cc", var)# the programs doctor asks for their versions. bend's check report changed# wording in 2.0.32, which the proof gate reads, and the native lane needs a C# compiler, so the version is the finding.def programs(+cc: String) -> List<&2, String>: ["bend", cc.name(cc), "git"]# every program's linedef probes(ns: List<&2, String>, +rs: List<&2, DW.Reply>) -> List<&2, Note>: match ns: case []: [] case +h <> t: probe.heard(DW.heard(rs, DW.Probe{h}), h) <> probes(t, rs)# ---------------------------------------------------------------------------# the ledger and the lock# the dependencies of a ledger that was readdef deps.of(manifest: M.Manifest) -> List<&2, M.Dep>: M.Manifest{_n, _e, _b, _h, _pa, _pv, ds, _ts} = manifest ds# how many dependencies a ledger names; one that did not read names nonedef deps.count(ledger: M.Read) -> Nat: match ledger: case M.Bad{_why}: 0n case M.Good{m}: List.length(&2, M.Dep, deps.of(m))# what the ledger turned out to bedef ledger.note(ledger: M.Read) -> Note: match ledger: case M.Bad{why}: Note{True{}, DW.toml() ++ ": " ++ why} case M.Good{m}: M.Manifest{n, e, _b, _h, _pa, _pv, ds, _ts} = m Note{False{}, DW.toml() ++ ": " ++ n ++ ", entry " ++ e ++ ", " ++ plural(List.length(&2, M.Dep, ds), "dependency", "dependencies")}# what the lock turned out to be: the packages it records, or why it could# not be read, or no lock at alltype Locked is Data: Locked{hashes: List<&2, String>} Unread{why: String} Unlocked{}# the verdict on a parse, with the answer in handdef locked.pick(ok: Bool, why: String, ss: List<&2, T.Sect>) -> Locked: match ok: case True{}: Locked{L.hashes(ss)} case False{}: Unread{why}# a lock that parsed, by the packages it records; one that did not, by whydef locked.parsed(toml: T.Toml) -> Locked: T.Toml{+bad, _name, _pairs, ss} = toml locked.pick(String.is_empty(bad), bad, ss)# the lock's text, readdef locked.of(got: Maybe<&2, String>) -> Locked: match got: case None{}: Unlocked{} case Some{text}: locked.parsed(T.parse(text))# the packages a lock records; none when it did not read or is not theredef locked.hashes(lk: Locked) -> List<&2, String>: match lk: case Locked{hs}: hs case Unread{_why}: [] case Unlocked{}: []# whether a ledger with this many dependencies needs a lock that names somedef lock.needs(deps: Nat) -> Bool: match deps: case 0n: False{} case 1n+_n: True{}# whether the lock is a problem: it names no package while the ledger has# dependencies it should have resolved. A project with no dependencies has# nothing to lock, so a lock naming nothing, or no lock at all, is its due.def lock.wrong(deps: Nat, hs: List<&2, String>) -> Bool: match hs: case []: lock.needs(deps) case _h <> _t: False{}# what the lock amounts to. A lock that is there and does not parse is a# problem whatever the ledger holds, since `ez fetch` refuses it, as cargo# refuses a Cargo.lock it cannot read.def lock.note(+deps: Nat, lk: Locked) -> Note: match lk: case Locked{+hs}: +wrong = lock.wrong(deps, hs) Note{wrong, DW.lockfile() ++ ": " ++ plural(List.length(&2, String, hs), "package", "packages") ++ Bool.pick(String, wrong, " (it names none; run ez lock)", "")} case Unread{why}: Note{True{}, DW.lockfile() ++ ": " ++ why} case Unlocked{}: +wrong = lock.needs(deps) Note{wrong, DW.lockfile() ++ ": " ++ Bool.pick(String, wrong, "none; run ez lock", "none, and nothing to lock")}# ---------------------------------------------------------------------------# BEND_LIB# where the packages this build resolves against live. It is what ez itself# passes to every bend it starts, so an unset BEND_LIB is not a finding: the# default is `.ez/lib` beside the project, and what matters is what is in it.def lib.at(+var: String) -> String: Bool.pick(String, String.is_empty(var), ".ez/lib", var)# the names `ls` gave for the library, none when it gave nonedef lib.have(got: DW.Heard) -> List<&2, String>: match got: case DW.Heard{a}: match a: case DW.Printed{ok, text}: answered(ok, text) case _: [] case DW.Open{}: []# the rest of a scan only when this one was not it. `Bool.or` reduces both# of its sides, so a scan written with it reads the whole list even when the# first entry answers. The rest arrives as a thunk.def has.step(here: Bool, rest: Unit -> Bool) -> Bool: match here: case True{}: True{} case False{}: rest(Unit{})# whether a list holds this stringdef has(ss: List<&2, String>, +want: String) -> Bool: match ss: case []: False{} case +h <> t: has.step(String.eq(h, want), _u => has(t, want))# a string kept when the other list lacks itdef missing.put(hit: Bool, item: String, rest: List<&2, String>) -> List<&2, String>: match hit: case True{}: rest case False{}: item <> rest# the strings of `xs` that `known` does not holddef missing(xs: List<&2, String>, +known: List<&2, String>) -> List<&2, String>: match xs: case []: [] case +h <> t: missing.put(has(known, h), h, missing(t, known))# the names the lock records, with the hash each names; none when there is# no lock, or when it does not readdef lock.names(lock: Maybe<&2, String>) -> List<&2, L.Name>: match lock: case None{}: [] case Some{text}: L.names.read(text)# what a name's file says against the lock: nothing when it holds the lock's# hash, as bend trims it, and the fault otherwisedef name.held(same: Bool, +nv: String, +hash: String, +was: String) -> List<&2, String>: match same: case True{}: [] case False{}: ["names/" ++ nv ++ " names " ++ was ++ " where the lock names " ++ hash]def name.got(got: Maybe<&2, String>, +nv: String, +hash: String) -> List<&2, String>: match got: case None{}: ["names/" ++ nv ++ " is not there"] case Some{text}: +was = String.trim(text) name.held(String.eq(was, hash), nv, hash, was)def name.heard(got: DW.Heard, +nv: String, +hash: String) -> List<&2, String>: match got: case DW.Heard{a}: match a: case DW.Held{m}: name.got(m, nv, hash) case _: ["names/" ++ nv ++ " was not read"] case DW.Open{}: ["names/" ++ nv ++ " was not read"]# every name of the lock whose file under BEND_LIB is missing, or names# another hash than the lock does, which bend would read instead of asking# the hubdef names.wrong(ns: List<&2, L.Name>, +rs: List<&2, DW.Reply>) -> List<&2, String>: match ns: case []: [] case L.Name{+nv, +hash} <> t: List.append(&2, String, name.heard(DW.heard(rs, DW.Name{nv}), nv, hash), names.wrong(t, rs))# what the library holds of the lock's names, said after its packagesdef names.said(+count: Nat) -> String: match count: case 0n: "" case 1n+_p: " with their " ++ plural(count, "name", "names")# what is wrong with the library, each fault in its placedef lib.faults(gone: List<&2, String>, bad: List<&2, String>) -> String: match gone: case []: String.join(bad, "; ") case _g <> _t: match bad: case []: "lacks " ++ String.join(gone, " ") case _b <> _u: "lacks " ++ String.join(gone, " ") ++ "; " ++ String.join(bad, "; ")# BEND_LIB judged against the lock: every package the lock names has to be on# disk, because bend resolves a hub import during the check and a sandbox has# no network to fall back on, and so does every name's file, since bend# would otherwise ask the hub what the name namesdef lib.said(all: Bool, +dir: String, +count: Nat, +names: Nat, gone: List<&2, String>, bad: List<&2, String>) -> Note: match all: case True{}: Note{False{}, "BEND_LIB: " ++ dir ++ ", all " ++ plural(count, "locked package", "locked packages") ++ " present" ++ names.said(names)} case False{}: Note{True{}, "BEND_LIB: " ++ dir ++ " " ++ lib.faults(gone, bad) ++ "; run ez fetch"}# the verdict, once what is wanted and what is there are both knowndef lib.judge( +dir: String, +want: List<&2, String>, +ns: List<&2, L.Name>, have: List<&2, String>, +rs: List<&2, DW.Reply>) -> Note: +gone = missing(want, have) +bad = names.wrong(ns, rs) lib.said(Bool.and(List.is_empty(&2, String, gone), List.is_empty(&2, String, bad)), dir, List.length(&2, String, want), List.length(&2, L.Name, ns), gone, bad)# the library's linedef lib.note(+dir: String, +want: List<&2, String>, +ns: List<&2, L.Name>, +rs: List<&2, DW.Reply>) -> Note: lib.judge(dir, want, ns, lib.have(DW.heard(rs, DW.Lib{dir})), rs)# ---------------------------------------------------------------------------# the drift report# a head kept when the rest does not hold it againdef once.put(dup: Bool, item: String, rest: List<&2, String>) -> List<&2, String>: match dup: case True{}: rest case False{}: item <> rest# every string of a list once, each where it last standsdef once(ss: List<&2, String>) -> List<&2, String>: match ss: case []: [] case +h <> +t: once.put(has(t, h), h, once(t))# every hub package the project's sources import, each once, by hash and by# name. These are the lock's roots: the header imports of every tracked# `.bend` file outside `.ez/`, as the package walk scans them.def imports(+fs: List<&2, W.Source>) -> List<&2, String>: once(List.append(&2, String, P.roots(fs), P.roots.named(fs)))# a dependency's name, when the ledger records it by onedef pinned.at(none: Bool, nv: String, rest: List<&2, String>) -> List<&2, String>: match none: case True{}: rest case False{}: nv <> restdef pinned.nv(+nv: String, rest: List<&2, String>) -> List<&2, String>: pinned.at(String.is_empty(nv), nv, rest)# the hashes a ledger's dependencies name, and the names it records them bydef pinned(ds: List<&2, M.Dep>) -> List<&2, String>: match ds: case []: [] case +h <> t: M.dep.hash(h) <> pinned.nv(M.source.nv(M.source.dep(h)), pinned(t))# whether an import line names a dependency, by its hash or by its namedef used(+hs: List<&2, String>, +dependency: M.Dep) -> Bool: +nv = M.source.nv(M.source.dep(dependency)) Bool.or(has(hs, M.dep.hash(dependency)), Bool.and(Bool.not(String.is_empty(nv)), has(hs, nv)))# how a hash imported and not in the ledger readsdef unrecorded.line(+hash: String) -> String: "drift: " ++ hash ++ " is imported but ez.toml does not name it"# every hash imported that the ledger does not namedef unrecorded(hs: List<&2, String>) -> List<&2, String>: match hs: case []: [] case h <> t: unrecorded.line(h) <> unrecorded(t)# how a ledger entry nobody imports readsdef unused.line(+dependency: M.Dep) -> String: "drift: " ++ M.dep.name(dependency) ++ " = " ++ M.dep.hash(dependency) ++ " is in ez.toml but no import line uses it"# a dependency reported when nothing imports itdef unused.put(hit: Bool, +dependency: M.Dep, rest: List<&2, String>) -> List<&2, String>: match hit: case True{}: rest case False{}: unused.line(dependency) <> rest# every dependency of the ledger that no import line usesdef unused(ds: List<&2, M.Dep>, +hs: List<&2, String>) -> List<&2, String>: match ds: case []: [] case +h <> t: unused.put(used(hs, h), h, unused(t, hs))# both directions of the disagreement: a hash the source imports that the# ledger never names, and a ledger entry no import line usesdef drift(+hs: List<&2, String>, +ds: List<&2, M.Dep>) -> List<&2, String>: List.append(&2, String, unrecorded(missing(hs, pinned(ds))), unused(ds, hs))# every line of a report, each a problemdef flag(ls: List<&2, String>) -> List<&2, Note>: match ls: case []: [] case h <> t: Note{True{}, h} <> flag(t)# the report, or the one line that says there is nothing to reportdef drift.notes(+hs: List<&2, String>, lines: List<&2, String>) -> List<&2, Note>: match lines: case []: [Note{False{}, "imports: " ++ plural(List.length(&2, String, hs), "package", "packages") ++ " imported, every one named in ez.toml"}] case h <> t: flag(h <> t)# the report over what git listed, or why it could not listdef listing.notes(listing: W.Listing, +ds: List<&2, M.Dep>) -> List<&2, Note>: match listing: case W.Listed{fs}: +hs = imports(fs) drift.notes(hs, drift(hs, ds)) case W.Unlisted{_why}: [Note{True{}, "imports: git could not list the .bend files it tracks here"}]# the report, once the sources were asked fordef sources.notes(got: DW.Heard, +ds: List<&2, M.Dep>) -> List<&2, Note>: match got: case DW.Heard{a}: match a: case DW.Listed{l}: listing.notes(l, ds) case _: [Note{True{}, "imports: the sources were not read"}] case DW.Open{}: [Note{True{}, "imports: the sources were not read"}]# the report over a ledger. One that did not read is its own problem, and# there is nothing to compare the source with.def imports.notes(ledger: M.Read, got: DW.Heard) -> List<&2, Note>: match ledger: case M.Bad{_why}: [Note{False{}, "imports: not compared, since ez.toml was not read"}] case M.Good{m}: sources.notes(got, deps.of(m))# ---------------------------------------------------------------------------# the lock against the lock `ez lock` would write# how a tree read from BEND_LIB arrived, as the lock judges it. A git# package's tree under BEND_LIB is one `ez lock` reads from there too. A hub# package's is one `ez lock` would have the hub serve: its name is the digest# of its manifest and each file is checked against its sum, so bytes that pass# are the bytes the hub serves under that name, and bytes that do not are# refused either way.def tree.how(src: L.Src) -> W.How: match src: case L.Hub{}: W.Served{} case L.Git{_url, _rev, _entry, _root, _nar, _tag}: W.Lib{}# the source a lock question namesdef ask.src(ask: W.Ask) -> L.Src: match ask: case W.Pkg{_hash, src, _hub}: src case W.Name{_nv, _hub}: L.Hub{} case W.Lock{}: L.Hub{}# a tree BEND_LIB held, as the answer the lock reads, ahead of the rest. One# it did not hold answers nothing, so the lock goes on asking for it.def tree.put(+ask: W.Ask, answer: DW.Answer, rest: List<&2, W.Reply>) -> List<&2, W.Reply>: match answer: case DW.Laid{manifest, ss}: W.Reply{ask, W.Got{tree.how(ask.src(ask)), manifest, ss}} <> rest case _: rest# a reply kept when it answers a lock questiondef tree.reply(ask: DW.Ask, answer: DW.Answer, rest: List<&2, W.Reply>) -> List<&2, W.Reply>: match ask: case DW.Tree{a}: tree.put(a, answer, rest) case _: rest# every tree BEND_LIB held, as the lock's answersdef trees(rs: List<&2, DW.Reply>) -> List<&2, W.Reply>: match rs: case []: [] case DW.Reply{ask, answer} <> t: tree.reply(ask, answer, trees(t))# the lock's text, "" when there is none, as the lock's own question reads itdef lock.text(lock: Maybe<&2, String>) -> String: match lock: case None{}: "" case Some{text}: text# a name's file BEND_LIB held, as the answer the lock reads for the name,# ahead of the rest. One it did not hold answers nothing, so the lock goes on# asking for it.def name.put(+nv: String, got: Maybe<&2, String>, rest: List<&2, W.Reply>) -> List<&2, W.Reply>: match got: case Some{text}: W.Reply{W.Name{nv, "BEND_LIB/names"}, W.Said{text}} <> rest case None{}: rest# a reply kept when it answers a name's questiondef name.reply(ask: DW.Ask, answer: DW.Answer, rest: List<&2, W.Reply>) -> List<&2, W.Reply>: match ask: case DW.Name{nv}: match answer: case DW.Held{got}: name.put(nv, got, rest) case _: rest case _: rest# every name's file BEND_LIB held, as the lock's answersdef held(rs: List<&2, DW.Reply>) -> List<&2, W.Reply>: match rs: case []: [] case DW.Reply{ask, answer} <> t: name.reply(ask, answer, held(t))# the World doctor puts to the lock planner: a plain lock over the ledger's# text as it is, the sources as git listed them, the lock as it is, which# answers the lock's question about the names it records, every tree# BEND_LIB held, and every name's file it held, never the network. The# directory a lock runs in only anchors the questions it asks, which doctor# answers by hash from BEND_LIB, so it is left empty.def relock.world( ledger: Maybe<&2, String>, lock: Maybe<&2, String>, listing: W.Listing, +rs: List<&2, DW.Reply>) -> W.World: W.World{W.Args{False{}, "", ""}, ledger, listing, List.append(&2, W.Reply, trees(rs), W.Reply{W.Lock{}, W.Locked{lock.text(lock)}} <> held(rs)), []}# that World, over the sources as a list of filesdef relock(+world: DW.World, fs: List<&2, W.Source>) -> W.World: relock.world(DW.ledger(world), DW.lock(world), W.Listed{fs}, DW.replies(world))# the line for a lock `ez lock` would write as it isdef fresh.line() -> String: DW.lockfile() ++ ": up to date with ez.toml and the sources"# the line for one it would write otherwisedef stale.line() -> String: DW.lockfile() ++ ": out of date with ez.toml and the sources; run ez lock"# the verdict, once the two texts were compareddef fresh.said(same: Bool) -> Note: match same: case True{}: Note{False{}, fresh.line()} case False{}: Note{True{}, stale.line()}# the lock against what the lock planner decided: the text it would write, or# why it would write nonedef fresh.doc(doc: P.Doc, +text: String) -> Note: match doc: case P.Doc{made}: fresh.said(String.eq(made, text)) case P.Stop{why}: Note{True{}, DW.lockfile() ++ ": not checked, since ez lock refuses here: " ++ why}# the hashes of lock questionsdef ask.hashes(as: List<&2, W.Ask>) -> List<&2, String>: match as: case []: [] case h <> t: W.ask.hash(h) <> ask.hashes(t)# the lock, once every name the lock planner asks about is known: when none# is left, what it decided; when some are, their files are not under# BEND_LIB, and doctor does not ask the hub for themdef fresh.named(as: List<&2, W.Ask>, +lw: W.World, +text: String) -> Note: match as: case []: fresh.doc(P.decide(W.inputs(lw)), text) case h <> t: Note{True{}, DW.lockfile() ++ ": not checked, since BEND_LIB lacks names/" ++ String.join(ask.hashes(h <> t), " names/") ++ " and doctor does not use the network"}# the lock, once every question the lock planner asks is known: when none is# left, its names, then what it decided; when some are, they are about# packages BEND_LIB does not hold, and doctor does not ask the network for# themdef fresh.away(as: List<&2, W.Ask>, +lw: W.World, +text: String) -> Note: match as: case []: fresh.named(P.wants.names(lw), lw, text) case h <> t: Note{True{}, DW.lockfile() ++ ": not checked, since BEND_LIB lacks " ++ String.join(ask.hashes(h <> t), " ") ++ " and doctor does not use the network"}# the check over the sources git listed. Sources git could not list are the# drift report's to say.def fresh.listing(+world: DW.World, listing: W.Listing, +text: String) -> List<&2, Note>: match listing: case W.Listed{fs}: +lw = relock(world, fs) [fresh.away(P.wants(lw), lw, text)] case W.Unlisted{_why}: []# the check, once the sources were heard. They are asked for only when the# ledger reads, so a ledger that does not is not checked against.def fresh.heard(got: DW.Heard, +world: DW.World, +text: String) -> List<&2, Note>: match got: case DW.Heard{a}: match a: case DW.Listed{l}: fresh.listing(world, l, text) case _: [] case DW.Open{}: []# the check over a lock that is there. With no lock there is nothing to# compare, and the lock's own line says whether that is a problem.def fresh.notes(lock: Maybe<&2, String>, +world: DW.World) -> List<&2, Note>: match lock: case None{}: [] case Some{text}: fresh.heard(DW.sources(world), world, text)# what doctor says of the lock against the lock `ez lock` would writedef checked(+world: DW.World) -> List<&2, Note>: fresh.notes(DW.lock(world), world)# each lock question, put to BEND_LIBdef trees.asked(as: List<&2, W.Ask>) -> List<&2, DW.Ask>: match as: case []: [] case h <> t: DW.Tree{h} <> trees.asked(t)# each of the lock's name questions, put to BEND_LIB as the name's file. The# lock's own question never reaches here, since the World answers it.def names.asked(as: List<&2, W.Ask>) -> List<&2, DW.Ask>: match as: case []: [] case h <> t: DW.Name{W.ask.hash(h)} <> names.asked(t)# the lock questions over the sources git listed, packages and then namesdef trees.world(+lw: W.World) -> List<&2, DW.Ask>: List.append(&2, DW.Ask, trees.asked(P.wants(lw)), names.asked(P.wants.names(lw)))def trees.listing( ledger: Maybe<&2, String>, lock: Maybe<&2, String>, listing: W.Listing, rs: List<&2, DW.Reply>) -> List<&2, DW.Ask>: match listing: case W.Listed{fs}: trees.world(relock.world(ledger, lock, W.Listed{fs}, rs)) case W.Unlisted{_why}: []# the lock questions, once the sources were hearddef trees.heard( got: DW.Heard, ledger: Maybe<&2, String>, lock: Maybe<&2, String>, rs: List<&2, DW.Reply>) -> List<&2, DW.Ask>: match got: case DW.Heard{a}: match a: case DW.Listed{l}: trees.listing(ledger, lock, l, rs) case _: [] case DW.Open{}: []# every package and name the lock planner asks about, while there is a lock# to check. These depend on the sources and on the trees already read, so# they come in rounds, as the lock's own do.def trees.of( ledger: Maybe<&2, String>, lock: Maybe<&2, String>, held: Maybe<&2, String>, +rs: List<&2, DW.Reply>) -> List<&2, DW.Ask>: match lock: case None{}: [] case Some{_text}: trees.heard(DW.heard(rs, DW.Sources{}), ledger, held, rs)# ---------------------------------------------------------------------------# the plan# the ledger as doctor sees it: its package, its entry and its dependencies.# Its `[tools.*]` pins are not dependencies, so they are dropped before# anything but the lock check is decided (EZ-LED-5). The lock check reads the# ledger's text as `ez lock` does, since the lock records the pins.def ledger.seen(ledger: M.Read) -> M.Read: match ledger: case M.Bad{why}: M.Bad{why} case M.Good{m}: M.Manifest{n, e, b, h, pa, pv, ds, _ts} = m M.Good{M.Manifest{n, e, b, h, pa, pv, ds, []}}# the toolchain, the ledger, the lock and the library, each on its line, and# the lock check's line after themdef head.notes( +ledger: M.Read, +cc: String, +lib: String, +lock: Maybe<&2, String>, +rs: List<&2, DW.Reply>, cs: List<&2, Note>) -> List<&2, Note>: +lk = locked.of(lock) List.append(&2, Note, probes(programs(cc), rs), List.append(&2, Note, [ledger.note(ledger), lock.note(deps.count(ledger), lk), lib.note(lib.at(lib), locked.hashes(lk), lock.names(lock), rs)], cs))# every line doctor says, in order: the toolchain, the ledger, the lock, the# library, the lock check, and last the drift report over the sources as they# were hearddef notes.of( +ledger: M.Read, got: DW.Heard, +cc: String, +lib: String, lock: Maybe<&2, String>, +rs: List<&2, DW.Reply>, cs: List<&2, Note>) -> List<&2, Note>: List.append(&2, Note, head.notes(ledger, cc, lib, lock, rs, cs), imports.notes(ledger, got))# whether a note is a problemdef note.wrong(finding: Note) -> Bool: Note{w, _l} = finding w# whether anything found was a problem. The rest arrives as a thunk and only# the arm that wants it applies it.def wrong(ns: List<&2, Note>) -> Bool: match ns: case []: False{} case h <> t: has.step(note.wrong(h), _u => wrong(t))# each line saiddef says(ns: List<&2, Note>) -> List<&2, P.Effect>: match ns: case []: [] case Note{_w, l} <> t: P.Say{l} <> says(t)# a problem must fail the command, or a script around it learns nothing. The# lines already said what the problem is.def outcome.of(bad: Bool) -> P.Outcome: match bad: case True{}: P.Refused{""} case False{}: P.Success{}# the plan for these notes: every line said, and the command failed when one# of them is a problemdef plan.notes(+ns: List<&2, Note>) -> P.Plan: P.Plan{says(ns), outcome.of(wrong(ns))}# the plan over what a World read, field by field, with the lock check's# linesdef plan.of( +ledger: M.Read, +cc: String, +lib: String, lock: Maybe<&2, String>, +rs: List<&2, DW.Reply>, cs: List<&2, Note>) -> P.Plan: plan.notes(notes.of(ledger, DW.heard(rs, DW.Sources{}), cc, lib, lock, rs, cs))# `ez doctor` over a Worlddef plan(+world: DW.World) -> P.Plan: plan.of(ledger.seen(DW.read(world)), DW.cc(world), DW.lib(world), DW.lock(world), DW.replies(world), checked(world))# whether a plan ends in a refusaldef refuses.plan(pl: P.Plan) -> Bool: P.Plan{_es, outcome} = pl match outcome: case P.Success{}: False{} case P.Refused{_why}: True{}# whether `ez doctor` fails on a Worlddef refuses(+world: DW.World) -> Bool: refuses.plan(plan(world))# ---------------------------------------------------------------------------# what is still to ask# a question kept when it has no answer yetdef open.put(got: DW.Heard, ask: DW.Ask, rest: List<&2, DW.Ask>) -> List<&2, DW.Ask>: match got: case DW.Heard{_a}: rest case DW.Open{}: ask <> rest# the questions of a list that have no answer yetdef open(asks: List<&2, DW.Ask>, +rs: List<&2, DW.Reply>) -> List<&2, DW.Ask>: match asks: case []: [] case +h <> t: open.put(DW.heard(rs, h), h, open(t, rs))# a version question for every programdef probe.asks(ns: List<&2, String>) -> List<&2, DW.Ask>: match ns: case []: [] case h <> t: DW.Probe{h} <> probe.asks(t)# the library is looked in only when the lock names a package to look fordef lib.asks(+dir: String, hs: List<&2, String>) -> List<&2, DW.Ask>: match hs: case []: [] case _h <> _t: [DW.Lib{dir}]# the sources are read only when there is a ledger to compare them withdef sources.asks(ledger: M.Read) -> List<&2, DW.Ask>: match ledger: case M.Bad{_why}: [] case M.Good{_m}: [DW.Sources{}]# a question for every name's file the lock recordsdef names.asks(ns: List<&2, L.Name>) -> List<&2, DW.Ask>: match ns: case []: [] case L.Name{nv, _hash} <> t: DW.Name{nv} <> names.asks(t)# every question doctor asks on what a World read, besides the lock check'sdef asks.of(ledger: M.Read, +cc: String, +lib: String, +lock: Maybe<&2, String>) -> List<&2, DW.Ask>: List.append(&2, DW.Ask, probe.asks(programs(cc)), List.append(&2, DW.Ask, lib.asks(lib.at(lib), locked.hashes(locked.of(lock))), List.append(&2, DW.Ask, names.asks(lock.names(lock)), sources.asks(ledger))))# the questions still open, or the plan once there are nonetype Step is Data: Asking{asks: List<&2, DW.Ask>} Run{plan: P.Plan}# the step, once the open questions are knowndef step.asks( asks: List<&2, DW.Ask>, +ledger: M.Read, +cc: String, +lib: String, lock: Maybe<&2, String>, +rs: List<&2, DW.Reply>, cs: List<&2, Note>) -> Step: match asks: case []: Run{plan.of(ledger, cc, lib, lock, rs, cs)} case h <> t: Asking{h <> t}# the step over what a World read, field by field, with the lock check's# questions and lines. The first round asks everything but the lock check's# trees, which follow the sources and each other, as the lock's own questions# do.def step.of( +ledger: M.Read, +cc: String, +lib: String, +lock: Maybe<&2, String>, +rs: List<&2, DW.Reply>, ts: List<&2, DW.Ask>, cs: List<&2, Note>) -> Step: step.asks(open(List.append(&2, DW.Ask, asks.of(ledger, cc, lib, lock), ts), rs), ledger, cc, lib, lock, rs, cs)# what the interpreter does next on a Worlddef step(+world: DW.World) -> Step: step.of(ledger.seen(DW.read(world)), DW.cc(world), DW.lib(world), DW.lock(world), DW.replies(world), trees.of(DW.ledger(world), DW.lock(world), DW.lock(world), DW.replies(world)), checked(world))# the questions a World still leaves opendef wants(+world: DW.World) -> List<&2, DW.Ask>: +ledger = ledger.seen(DW.read(world)) open(List.append(&2, DW.Ask, asks.of(ledger, DW.cc(world), DW.lib(world), DW.lock(world)), trees.of(DW.ledger(world), DW.lock(world), DW.lock(world), DW.replies(world))), DW.replies(world))# ---------------------------------------------------------------------------# law vocabulary: ez never calls these# whether an effect says this linedef said.one(effect: P.Effect, +line: String) -> Bool: match effect: case P.Say{text}: String.eq(text, line) case P.Write{_at, _text}: False{} case P.Lay{_place, _hash, _files}: False{} case P.Drop{_hash}: False{} case P.Name{_nv, _hash}: False{}# whether a list of effects says this linedef said.in(es: List<&2, P.Effect>, +line: String) -> Bool: match es: case []: False{} case e <> t: has.step(said.one(e, line), _u => said.in(t, line))# whether a plan says this linedef said(pl: P.Plan, +line: String) -> Bool: P.Plan{es, _outcome} = pl said.in(es, line)# the drift report doctor makes of a World's sources against a ledger's# dependenciesdef drift.of(fs: List<&2, W.Source>, +ds: List<&2, M.Dep>) -> List<&2, String>: +hs = imports(fs) drift(hs, ds)