1 # doctor/run: the interpreter for `ez doctor`. It reads `$CC`, `$BEND_LIB`, 2 # the ledger's text and the lock's text into a World, then loops: it asks 3 # the planner (doctor/plan.bend) what is still open, answers each question, 4 # and adds the answers to the World, until nothing is left to ask. Then it 5 # runs the planner's plan with the lock's effect runner (`Run.exec.plan`), 6 # which prints each line and exits 1 when the plan fails the command. 7 # 8 # A version question runs the program with `--version`, except bend, which 9 # is asked through run/bend since it has answered the question two ways over 10 # its versions. The library is listed with `ls`, the sources are read as 11 # `ez lock` reads them (`Run.read.listing`), and a package's tree is read 12 # from under BEND_LIB as `ez lock` reads one there (`Run.disk.all`), and 13 # never fetched, and a name's file under `BEND_LIB/names` is read as it 14 # is. None of these writes anything, and the plan writes nothing either. It 15 # decides nothing; that it reads and executes faithfully is EZ-TRUST-2. 16 import Base 17 import 0xead4c7939c9eaca8d47da53b4496b731/main.bend as R 18 import ../run/bend.bend as Bend 19 import ../io/file.bend as F 20 import ../share/args.bend as Args 21 import ../lock/run.bend as Run 22 import ../lock/world.bend as W 23 import ../lock/lock.bend as L 24 import ./world.bend as DW 25 import ./plan.bend as DP 26 27 # how a program is asked for its version, once it is known whether this is 28 # bend. Everything else answers the flag. 29 def probe.ask(is_bend: Bool, name: String) -> IO(String): 30 match is_bend: 31 case True{}: 32 Bend.version() 33 case False{}: 34 R.exec([name, "--version"]) 35 36 # a tree under BEND_LIB, as it is: its manifest and every file it names, read 37 # from beside it. A file that is not there reads as "", which does not hash 38 # to its sum. 39 def tree.read(+dir: String, +manifest: String) -> IO(DW.Answer): 40 do IO: 41 ss : List<&2, String> <- Run.disk.all(dir, L.manifest.files(String.lines(manifest))) 42 return DW.Laid{manifest, ss} 43 44 # a tree whose manifest was looked for; none there is none at all 45 def tree.seen(got: Maybe<&2, String>, +dir: String) -> IO(DW.Answer): 46 match got: 47 case Some{text}: 48 tree.read(dir, text) 49 case None{}: 50 IO.pure(DW.Answer, DW.Away{}) 51 52 # a package's tree under the library directory 53 def tree(+dir: String) -> IO(DW.Answer): 54 do IO: 55 m : Maybe<&2, String> <- F.read(dir ++ "/manifest") 56 tree.seen(m, dir) 57 58 # one question, answered. `lib` is the library directory the planner names. 59 def answer(+lib: String, ask: DW.Ask) -> IO(DW.Answer): 60 match ask: 61 case DW.Probe{+name}: 62 do IO: 63 +out : String <- probe.ask(String.eq(name, "bend"), name) 64 return DW.Printed{R.ok(out), R.text(out)} 65 case DW.Lib{dir}: 66 do IO: 67 +out : String <- R.exec(["ls", dir]) 68 return DW.Printed{R.ok(out), R.text(out)} 69 case DW.Sources{}: 70 do IO: 71 l : W.Listing <- Run.read.listing() 72 return DW.Listed{l} 73 case DW.Tree{a}: 74 tree(lib ++ "/" ++ W.ask.hash(a)) 75 case DW.Name{nv}: 76 do IO: 77 m : Maybe<&2, String> <- F.read(lib ++ "/names/" ++ nv) 78 return DW.Held{m} 79 80 # every question, answered 81 def answer.all(+lib: String, asks: List<&2, DW.Ask>) -> IO(List<&2, DW.Reply>): 82 match asks: 83 case []: 84 IO.pure(List<&2, DW.Reply>, []) 85 case +h <> t: 86 do IO>: 87 a : DW.Answer <- answer(lib, h) 88 rest : List<&2, DW.Reply> <- answer.all(lib, t) 89 return DW.Reply{h, a} <> rest 90 91 # the World with more answers in it 92 def more(world: DW.World, rs: List<&2, DW.Reply>) -> DW.World: 93 DW.World{cc, lib, ledger, lock, replies} = world 94 DW.World{cc, lib, ledger, lock, List.append(&2, DW.Reply, replies, rs)} 95 96 # the planner's step: its questions answered and the loop gone round again, 97 # or its plan run 98 def loop.step(st: DP.Step, +lib: String, world: DW.World, go: DW.World -> IO(Unit)) -> IO(Unit): 99 match st: 100 case DP.Asking{asks}: 101 do IO: 102 rs : List<&2, DW.Reply> <- answer.all(lib, asks) 103 go(more(world, rs)) 104 case DP.Run{pl}: 105 Run.exec.plan("", pl) 106 107 # the rounds the loop may take. The lock check's trees follow the sources 108 # and each other, and the packages they reach are not known until BEND_LIB 109 # has answered, so this is a guard against a planner that keeps asking, not 110 # a limit on a project: running out of it dies loudly. 111 def rounds() -> Nat: 112 U32.to_nat(100000) 113 114 # the loop, under fuel. Every round adds at least one answer, since the 115 # planner never asks what the World answers already, and the lock check asks 116 # about each package at most once. 117 def loop(fuel: Nat, +lib: String, +world: DW.World) -> IO(Unit): 118 match fuel: 119 case 0n: 120 IO.die(Unit, 1, "ez: ez doctor kept asking questions") 121 case 1n+f: 122 loop.step(DP.step(world), lib, world, w => loop(f, lib, w)) 123 124 # a variable's text, "" when it is unset 125 def var(name: String) -> IO(String): 126 do IO: 127 r : Result<&1, &1, U32 & String, String> <- IO.get_env(name) 128 return Args.text_of(r) 129 130 # `ez doctor`: the toolchain and the project, reported on 131 def run() -> IO(Unit): 132 do IO: 133 cc : String <- var("CC") 134 +lib : String <- var("BEND_LIB") 135 led : Maybe<&2, String> <- F.read(DW.toml()) 136 lk : Maybe<&2, String> <- F.read(DW.lockfile()) 137 loop(rounds(), DP.lib.at(lib), DW.World{cc, lib, led, lk, []})