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 import ../ws/run.bend as WsRun 27 import ../ws/doctor.bend as WsDoc 28 29 # how a program is asked for its version, once it is known whether this is 30 # bend. Everything else answers the flag. 31 def probe.ask(is_bend: Bool, name: String) -> IO(String): 32 match is_bend: 33 case True{}: 34 Bend.version() 35 case False{}: 36 R.exec([name, "--version"]) 37 38 # a tree under BEND_LIB, as it is: its manifest and every file it names, read 39 # from beside it. A file that is not there reads as "", which does not hash 40 # to its sum. 41 def tree.read(+dir: String, +manifest: String) -> IO(DW.Answer): 42 do IO: 43 ss : List<&2, String> <- Run.disk.all(dir, L.manifest.files(String.lines(manifest))) 44 return DW.Laid{manifest, ss} 45 46 # a tree whose manifest was looked for; none there is none at all 47 def tree.seen(got: Maybe<&2, String>, +dir: String) -> IO(DW.Answer): 48 match got: 49 case Some{text}: 50 tree.read(dir, text) 51 case None{}: 52 IO.pure(DW.Answer, DW.Away{}) 53 54 # a package's tree under the library directory 55 def tree(+dir: String) -> IO(DW.Answer): 56 do IO: 57 m : Maybe<&2, String> <- F.read(dir ++ "/manifest") 58 tree.seen(m, dir) 59 60 # one question, answered. `lib` is the library directory the planner names. 61 def answer(+lib: String, ask: DW.Ask) -> IO(DW.Answer): 62 match ask: 63 case DW.Probe{+name}: 64 do IO: 65 +out : String <- probe.ask(String.eq(name, "bend"), name) 66 return DW.Printed{R.ok(out), R.text(out)} 67 case DW.Lib{dir}: 68 do IO: 69 +out : String <- R.exec(["ls", dir]) 70 return DW.Printed{R.ok(out), R.text(out)} 71 case DW.Sources{}: 72 do IO: 73 l : W.Listing <- Run.read.listing() 74 return DW.Listed{l} 75 case DW.Tree{a}: 76 tree(lib ++ "/" ++ W.ask.hash(a)) 77 case DW.Name{nv}: 78 do IO: 79 m : Maybe<&2, String> <- F.read(lib ++ "/names/" ++ nv) 80 return DW.Held{m} 81 82 # every question, answered 83 def answer.all(+lib: String, asks: List<&2, DW.Ask>) -> IO(List<&2, DW.Reply>): 84 match asks: 85 case []: 86 IO.pure(List<&2, DW.Reply>, []) 87 case +h <> t: 88 do IO>: 89 a : DW.Answer <- answer(lib, h) 90 rest : List<&2, DW.Reply> <- answer.all(lib, t) 91 return DW.Reply{h, a} <> rest 92 93 # the World with more answers in it 94 def more(world: DW.World, rs: List<&2, DW.Reply>) -> DW.World: 95 DW.World{cc, lib, ledger, lock, replies} = world 96 DW.World{cc, lib, ledger, lock, List.append(&2, DW.Reply, replies, rs)} 97 98 # the planner's step: its questions answered and the loop gone round again, 99 # or its plan run 100 def loop.step(st: DP.Step, +lib: String, world: DW.World, go: DW.World -> IO(Unit)) -> IO(Unit): 101 match st: 102 case DP.Asking{asks}: 103 do IO: 104 rs : List<&2, DW.Reply> <- answer.all(lib, asks) 105 go(more(world, rs)) 106 case DP.Run{pl}: 107 Run.exec.plan("", pl) 108 109 # the rounds the loop may take. The lock check's trees follow the sources 110 # and each other, and the packages they reach are not known until BEND_LIB 111 # has answered, so this is a guard against a planner that keeps asking, not 112 # a limit on a project: running out of it dies loudly. 113 def rounds() -> Nat: 114 U32.to_nat(100000) 115 116 # the loop, under fuel. Every round adds at least one answer, since the 117 # planner never asks what the World answers already, and the lock check asks 118 # about each package at most once. 119 def loop(fuel: Nat, +lib: String, +back: WsDoc.Back, +world: DW.World) -> IO(Unit): 120 match fuel: 121 case 0n: 122 IO.die(Unit, 1, "ez: ez doctor kept asking questions") 123 case 1n+f: 124 loop.step(WsDoc.step(back, world), lib, world, w => loop(f, lib, back, w)) 125 126 # a variable's text, "" when it is unset 127 def var(name: String) -> IO(String): 128 do IO: 129 r : Result<&1, &1, U32 & String, String> <- IO.get_env(name) 130 return Args.text_of(r) 131 132 # the loop, on the ledger the doctor judges 133 def run.doctored(dd: WsDoc.Doctored, +cc: String, +lib: String, lk: Maybe<&2, String>) -> IO(Unit): 134 WsDoc.Doctored{led, back} = dd 135 loop(rounds(), DP.lib.at(lib), back, DW.World{cc, lib, led, lk, []}) 136 137 # `ez doctor`: the toolchain and the project, reported on 138 def run() -> IO(Unit): 139 do IO: 140 cc : String <- var("CC") 141 +lib : String <- var("BEND_LIB") 142 own : Maybe<&2, String> <- F.read(DW.toml()) 143 dd : WsDoc.Doctored <- WsRun.doctor.ledger(own) 144 lk : Maybe<&2, String> <- F.read(DW.lockfile()) 145 run.doctored(dd, cc, lib, lk)