# lock/run: the interpreter for `ez lock`. It reads the ledger and the # committed sources into a World, then loops: it asks the planner # (lock/plan.bend) what is still open, answers each question by IO, and adds # the answers to the World, until nothing is left to ask. Then it runs the # planner's plan. It decides nothing: which packages to read, whether their # bytes are the ones the ledger names, where an upgrade moves a pin, and what # is written are the planner's. That it reads and executes faithfully is # EZ-TRUST-2. It answers a name's question by asking the hub once, and the # lock's by reading ez.lock.toml. Under `--upgrade` it also answers the # upgrade's questions (lock/up.bend): what git prints for refs, the default # branch and tags, ancestry, checkouts at a rev, the hub's manifest for a # moved hash, `.gitignore`, and the project's sources. A new checkout's package is walked # from its entry here, by `K.pkg_of`, which is interpreter code under # EZ-TRUST-2 until `ez add` is converted. import Base import 0xabe575924687afad4cee1a2c1194d639/main.bend as R import ../io/file.bend as F import ../hub/hub.bend as Web import ../hub/get.bend as Net import ../git/git.bend as Git import ../git/exec.bend as GX import ../pkg/pkg.bend as K import ../pkg/path.bend as P import ../share/say.bend as Say import ../share/spin.bend as Spin import ./lock.bend as L import ./world.bend as W import ./up.bend as Up import ./plan.bend as Plan # --------------------------------------------------------------------------- # the World's first fields # an environment variable's text; unset, "" def env.text(res: Result<&1, &1, U32 & String, String>) -> String: match res: case Fail{_e}: "" case Done{s}: s # where packages live, which is with the project rather than in ~/.bend/lib def lib.or(+dir: String) -> String: Bool.pick(String, String.is_empty(dir), ".ez/lib", dir) # the BEND_LIB this project pins def bend.lib() -> IO(String): do IO: r : Result<&1, &1, U32 & String, String> <- IO.get_env("BEND_LIB") return lib.or(String.trim(env.text(r))) # the non-empty lines def nonempty(ss: List<&2, String>) -> List<&2, String>: match ss: case []: [] case +h <> t: +more = nonempty(t) Bool.pick(List<&2, String>, String.is_empty(h), more, h <> more) # the ledger's text, or None when there is no ez.toml def read.ledger() -> IO(Maybe<&2, String>): F.read("ez.toml") # every listed path, read def read.all(ps: List<&2, String>) -> IO(List<&2, W.Source>): match ps: case []: IO.pure(List<&2, W.Source>, []) case +h <> t: do IO>: m : Maybe<&2, String> <- F.read(h) rest : List<&2, W.Source> <- read.all(t) return W.Source{h, F.text_of(m)} <> rest # what git listed, or why it could not def listed(ok: Bool, +text: String) -> IO(W.Listing): match ok: case True{}: do IO: fs : List<&2, W.Source> <- read.all(nonempty(String.lines(text))) return W.Listed{fs} case False{}: IO.pure(W.Listing, W.Unlisted{ "ez: ez lock reads the .bend files git tracks, and git could not list them: " ++ String.trim(text)}) # every `.bend` file git tracks under the current directory, with its text. # Untracked files are not a clone's, so they are not the lock's. def read.listing() -> IO(W.Listing): do IO: +out : String <- R.exec(["git", "-c", "core.quotepath=off", "ls-files", "--", "*.bend"]) listed(R.ok(out), R.text(out)) # --------------------------------------------------------------------------- # answering one question # every text a fetch answered, or the first miss def gots(+how: W.How, +manifest: String, gs: List<&2, Web.Got>, acc: List<&2, String>) -> W.Answer: match gs: case []: W.Got{how, manifest, List.reverse(&2, String, acc)} case g <> t: match g: case Web.Miss{why}: W.Miss{"ez: " ++ why} case Web.Have{body}: gots(how, manifest, t, body <> acc) # every file of a hub package, fetched and checked against its sum def hub.all(+hub: String, +hash: String, fs: List<&2, K.Item>) -> IO(List<&2, Web.Got>): match fs: case []: IO.pure(List<&2, Web.Got>, []) case +h <> t: do IO>: g : Web.Got <- Net.fetch(L.url_of(hub, hash, K.file.at(h)), K.file.sum(h)) rest : List<&2, Web.Got> <- hub.all(hub, hash, t) return g <> rest # a hub package's files, once its manifest arrived def hub.got(got: Web.Got, +hub: String, +hash: String) -> IO(W.Answer): match got: case Web.Miss{why}: IO.pure(W.Answer, W.Miss{why}) case Web.Have{+body}: do IO: gs : List<&2, Web.Got> <- hub.all(hub, hash, L.manifest.files(String.lines(body))) return gots(W.Served{}, body, gs, []) # a hub package: its manifest, then every file it names def answer.hub(+hub: String, +hash: String) -> IO(W.Answer): do IO: +s : Say.Spin <- Spin.hold(Say.dep.hub(hash)) g : Web.Got <- Net.fetch(L.url_of(hub, hash, "manifest"), L.want(hash)) a : W.Answer <- hub.got(g, hub, hash) Spin.done(s) return a # every file a tree's manifest names, read from beside it. A file that is not # there reads as "", which does not hash to its sum. def disk.all(+dir: String, fs: List<&2, K.Item>) -> IO(List<&2, String>): match fs: case []: IO.pure(List<&2, String>, []) case +h <> t: do IO>: m : Maybe<&2, String> <- F.read(P.join(dir, K.file.at(h))) rest : List<&2, String> <- disk.all(dir, t) return F.text_of(m) <> rest # a tree under BEND_LIB, as it is def answer.lib(+dir: String, +manifest: String) -> IO(W.Answer): do IO: ss : List<&2, String> <- disk.all(dir, L.manifest.files(String.lines(manifest))) return W.Got{W.Lib{}, manifest, ss} # a checkout that could not be made or read is removed, and the question is # answered with why def clone.miss(+work: String, +why: String) -> IO(W.Answer): do IO: _rm : String <- R.exec(["rm", "-rf", work]) return W.Miss{why} # a command of a clone that worked goes on with the clone; one that failed # ends it def clone.ran(ok: Bool, +work: String, +why: String, go: Unit -> IO(W.Answer)) -> IO(W.Answer): match ok: case True{}: go(Unit{}) case False{}: clone.miss(work, why) # one git command of a clone, and the rest of the clone when it worked def clone.run(+work: String, what: String, args: List<&2, String>, go: Unit -> IO(W.Answer)) -> IO(W.Answer): do IO: +out : String <- R.exec(args) clone.ran(R.ok(out), work, "ez: git " ++ what ++ ": " ++ R.text(out), go) # the checkout's package, walked from its entry the way `ez add` walked it def clone.walk(+work: String, +entry: String, +nar: String) -> IO(W.Answer): do IO: +p : K.Pkg <- K.pkg_in(work, entry) +fs : List<&2, K.Item> <- IO.pure(List<&2, K.Item>, K.pkg.files(p)) ss : List<&2, String> <- disk.all(K.pkg.root(p), fs) _rm : String <- R.exec(["rm", "-rf", work]) return W.Got{W.Clone{nar}, K.manifest_of(fs), ss} # the entry has to be in the checkout for there to be a package to walk def clone.entry(ok: Bool, +work: String, +entry: String, +url: String, +rev: String, +nar: String) -> IO(W.Answer): match ok: case True{}: clone.walk(work, entry, nar) case False{}: clone.miss(work, "ez: " ++ entry ++ " is not in " ++ url ++ " at " ++ rev) # the checkout, weighed and walked def clone.weigh(+work: String, +url: String, +rev: String, +entry: String) -> IO(W.Answer): do IO: _dg : String <- R.exec(["rm", "-rf", work ++ "/.git"]) +s : Say.Spin <- Spin.hold(Say.hashing(rev)) +nar : String <- GX.nar(work) Spin.done(s) +ck : String <- R.exec(["test", "-f", P.join(work, entry)]) clone.entry(R.ok(ck), work, entry, url, rev, nar) # a git package whose tree is not under BEND_LIB: a shallow clone at the rev # the ledger pins, the way `ez add` and `ez fetch` clone, from the source the # question names, which the planner anchored at the project root. Nothing is # laid here; the plan lays the tree once it has checked it. def answer.clone(+lib: String, +hash: String, +url: String, +rev: String, +entry: String) -> IO(W.Answer): +work = lib ++ "/.work-" ++ hash do IO: _rm : String <- R.exec(["rm", "-rf", work]) _mk : String <- R.exec(["mkdir", "-p", work]) +s : Say.Spin <- Spin.hold(Say.dep.git(Say.label(hash, entry), url)) a : W.Answer <- clone.run(work, "init", ["git", "init", "-q", work], _u => clone.run(work, "fetch", ["git", "-C", work, "fetch", "-q", "--depth", "1", url, rev], _v => clone.run(work, "checkout", ["git", "-C", work, "checkout", "-q", rev], _w => clone.weigh(work, url, rev, entry)))) Spin.done(s) return a # a tree already under BEND_LIB is read; a missing one is cloned def git.seen( got: Maybe<&2, String>, +lib: String, +hash: String, +url: String, +rev: String, +entry: String ) -> IO(W.Answer): match got: case Some{text}: answer.lib(lib ++ "/" ++ hash, text) case None{}: answer.clone(lib, hash, url, rev, entry) # a git package def answer.git(+lib: String, +hash: String, +url: String, +rev: String, +entry: String) -> IO(W.Answer): do IO: m : Maybe<&2, String> <- F.read(lib ++ "/" ++ hash ++ "/manifest") git.seen(m, lib, hash, url, rev, entry) # a package, from where its source says def answer.pkg(+lib: String, +hash: String, src: L.Src, +hub: String) -> IO(W.Answer): match src: case L.Hub{}: answer.hub(hub, hash) case L.Git{url, rev, entry, _root, _nar, _tag}: answer.git(lib, hash, url, rev, entry) # what the hub says a name names, as it said it; the planner reads the hash # out of it def said(got: Web.Got) -> W.Answer: match got: case Web.Miss{why}: W.Miss{"ez: " ++ why} case Web.Have{body}: W.Said{body} # a name, asked of the hub once: `GET /name/` def answer.name(+hub: String, +nv: String) -> IO(W.Answer): do IO: +s : Say.Spin <- Spin.hold(Say.dep.hub(nv)) g : Web.Got <- Net.fetch(hub ++ "/name/" ++ nv, "") Spin.done(s) return said(g) # the lock being rewritten, "" when there is none def answer.lock() -> IO(W.Answer): do IO: m : Maybe<&2, String> <- F.read(W.lockfile()) return W.Locked{F.text_of(m)} # one question, answered def answer(+lib: String, ask: W.Ask) -> IO(W.Answer): match ask: case W.Pkg{+hash, src, +hub}: answer.pkg(lib, hash, src, hub) case W.Name{+nv, +hub}: answer.name(hub, nv) case W.Lock{}: answer.lock() # every question, answered def answer.all(+lib: String, asks: List<&2, W.Ask>) -> IO(List<&2, W.Reply>): match asks: case []: IO.pure(List<&2, W.Reply>, []) case +h <> t: do IO>: a : W.Answer <- answer(lib, h) rest : List<&2, W.Reply> <- answer.all(lib, t) return W.Reply{h, a} <> rest # --------------------------------------------------------------------------- # answering the upgrade's questions # what git printed, and whether it succeeded, kept as it was def git.said(+what: String, args: List<&2, String>) -> IO(Up.Answer): do IO: +s : Say.Spin <- Spin.hold(what) +out : String <- R.exec(args) Spin.done(s) return Up.Printed{R.ok(out), R.text(out)} # The upgrade's questions, and `ez add`'s and `ez fetch`'s, name the source # as git is to read it, anchored at the project by their planners # (`Up.anchor`, `P.Query`), so each is handed to git as it is. # the refs a ref could name: a tag, its peeled form, and a branch, all at # once, because which of them exists is the question def answer.refs(+url: String, +ref: String) -> IO(Up.Answer): git.said(Say.resolving.at(url), ["git", "ls-remote", url, "refs/tags/" ++ ref, "refs/tags/" ++ ref ++ "^{}", "refs/heads/" ++ ref]) # HEAD on the remote, with the branch it is a symref to def answer.head(+url: String) -> IO(Up.Answer): git.said(Say.resolving.at(url), ["git", "ls-remote", "--symref", url, "HEAD"]) # every tag on the remote def answer.tags(+url: String) -> IO(Up.Answer): git.said(Say.resolving.at(url), ["git", "ls-remote", "--tags", url]) # whether `tip` descends from `pin`, asked of a blobless bare clone def answer.above(+lib: String, +url: String, +pin: String, +tip: String) -> IO(Up.Answer): do IO: yes : Bool <- GX.descendant.at(lib ++ "/.work-above", url, pin, tip) return Up.Onward{yes} # the checkout's package, walked from its entry the way `ez add` walks it, with # the directory its paths are written from, against the repo def tree.walk(+work: String, +at: String, +nar: String) -> IO(Up.Answer): do IO: +p : K.Pkg <- K.pkg_in(work, at) +fs : List<&2, K.Item> <- IO.pure(List<&2, K.Item>, K.pkg.files(p)) ss : List<&2, String> <- disk.all(K.pkg.root(p), fs) +rt : String <- IO.pure(String, Git.root.rel(P.join(work, at), K.pkg.root(p), at)) _rm : String <- R.exec(["rm", "-rf", work]) return Up.Checkout{nar, rt, K.manifest_of(fs), ss} # the entry has to be in the checkout for there to be a package to walk def tree.entry(ok: Bool, +work: String, +at: String, +url: String, +rev: String, +nar: String) -> IO(Up.Answer): match ok: case True{}: tree.walk(work, at, nar) case False{}: do IO: _rm : String <- R.exec(["rm", "-rf", work]) return Up.Miss{"ez: " ++ at ++ " is not in " ++ url ++ " at " ++ rev} # a checkout at a rev: cloned, its `.git` removed, weighed, and walked. An # entry that was not recorded is read from the checkout. Nothing is laid and # nothing is noted: a tree the upgrade moves to is laid by the plan, once the # upgrade and the lock both succeed. def answer.tree(+lib: String, +url: String, +rev: String, +entry: String) -> IO(Up.Answer): +work = lib ++ "/.work-" ++ rev do IO: _rm : String <- R.exec(["rm", "-rf", work]) _mk : String <- R.exec(["mkdir", "-p", work]) GX.clone(work, url, rev) +at : String <- Git.entry.for(Bool.not(String.is_empty(entry)), entry, work) _dg : String <- R.exec(["rm", "-rf", work ++ "/.git"]) +s : Say.Spin <- Spin.hold(Say.hashing(rev)) +nar : String <- GX.nar(work) Spin.done(s) +ck : String <- R.exec(["test", "-f", P.join(work, at)]) tree.entry(R.ok(ck), work, at, url, rev, nar) # a commit's NAR hash, for a tool def answer.weigh(+url: String, +rev: String) -> IO(Up.Answer): do IO: +nar : String <- GX.weigh(url, rev) return Up.Weighed{nar} # the hub's manifest for a hash a dependency moved to def answer.hubhas(+hub: String, +hash: String) -> IO(Up.Answer): do IO: g : Web.Got <- Net.fetch(L.url_of(hub, hash, "manifest"), L.want(hash)) return Up.HubSaid{g} # the ignore file, "" when it is missing def answer.ignore() -> IO(Up.Answer): do IO: m : Maybe<&2, String> <- F.read(".gitignore") return Up.Text{F.text_of(m)} # one source in front of the rest, or the first that could not be read def found.put(got: Maybe<&2, String>, +at: String, rest: Up.Answer) -> Up.Answer: match got: case None{}: Up.Miss{"ez: " ++ at ++ " could not be read"} case Some{text}: match rest: case Up.Files{fs}: Up.Files{Up.Found{at, text} <> fs} case _: rest # every listed source, read def found.all(ps: List<&2, String>) -> IO(Up.Answer): match ps: case []: IO.pure(Up.Answer, Up.Files{[]}) case +h <> t: do IO: m : Maybe<&2, String> <- F.read(h) rest : Up.Answer <- found.all(t) return found.put(m, h, rest) def found.listed(ok: Bool, +text: String) -> IO(Up.Answer): match ok: case True{}: found.all(nonempty(String.lines(text))) case False{}: IO.pure(Up.Answer, Up.Miss{"ez: could not list sources: " ++ String.trim(text)}) # every Bend source of the project. Vendored trees and the gate's shadow live # under .ez, and a checkout's .git is not source, so neither is rewritten. def answer.sources() -> IO(Up.Answer): do IO: +out : String <- R.exec(["find", ".", "-name", "*.bend", "-not", "-path", "*/.ez/*", "-not", "-path", "*/.git/*"]) found.listed(R.ok(out), R.text(out)) # a checkout `ez add` walks, which could not be made or read: removed, and # the question answered with why def whole.miss(+work: String, +why: String) -> IO(Up.Answer): do IO: _rm : String <- R.exec(["rm", "-rf", work]) return Up.Miss{why} # a command of the checkout that worked goes on with it; one that failed ends # it def whole.ran(ok: Bool, +work: String, +why: String, go: Unit -> IO(Up.Answer)) -> IO(Up.Answer): match ok: case True{}: go(Unit{}) case False{}: whole.miss(work, why) # one git command of the checkout, and the rest of it when it worked def whole.run(+work: String, what: String, args: List<&2, String>, go: Unit -> IO(Up.Answer)) -> IO(Up.Answer): do IO: +out : String <- R.exec(args) whole.ran(R.ok(out), work, "ez: git " ++ what ++ ": " ++ String.trim(R.text(out)), go) # a file read, in front of the rest def whole.put(got: Maybe<&2, String>, +at: String, rest: List<&2, K.Source>) -> List<&2, K.Source>: match got: case None{}: rest case Some{text}: K.Source{at, text} <> rest # every listed file of the checkout, read, with its path from the checkout's # top. One that cannot be read as text, such as a link to a directory, is # left out, as the walk would find it missing. def whole.read(+work: String, ps: List<&2, String>) -> IO(List<&2, K.Source>): match ps: case []: IO.pure(List<&2, K.Source>, []) case +h <> t: do IO>: m : Maybe<&2, String> <- F.read(h) rest : List<&2, K.Source> <- whole.read(work, t) return whole.put(m, String.drop(h, (1n + String.length(work) : Nat)), rest) # the checkout, its `.git` removed, weighed, and every file of it read; then # removed. Nothing is laid: `ez add` lays the tree from its plan. # the files of a checkout, read, once `find` has listed them. A listing that # failed is refused rather than read: `find` writes what it could not reach # to stderr, which snap answers beside the paths (SNAP-ANS-4), and such a # line is no path of the package. def whole.listed(ok: Bool, +work: String, +nar: String, +text: String) -> IO(Up.Answer): match ok: case True{}: do IO: fs : List<&2, K.Source> <- whole.read(work, nonempty(String.lines(text))) _rm : String <- R.exec(["rm", "-rf", work]) return Up.Cloned{nar, fs} case False{}: do IO: _rm : String <- R.exec(["rm", "-rf", work]) return Up.Miss{"ez: could not list the checkout: " ++ String.trim(text)} def whole.weigh(+work: String, +rev: String) -> IO(Up.Answer): do IO: _dg : String <- R.exec(["rm", "-rf", work ++ "/.git"]) +s : Say.Spin <- Spin.hold(Say.hashing(rev)) +nar : String <- GX.nar(work) Spin.done(s) +out : String <- R.exec(["find", work, "(", "-type", "f", "-o", "-type", "l", ")"]) whole.listed(R.ok(out), work, nar, R.text(out)) # a scratch directory made, or why it could not be def whole.dir(ok: Bool, +work: String, +url: String, +rev: String) -> IO(Up.Answer): match ok: case False{}: IO.pure(Up.Answer, Up.Miss{"ez: no scratch directory to clone into: " ++ work}) case True{}: do IO: +s : Say.Spin <- Spin.hold(Say.git.fetch(url)) a : Up.Answer <- whole.run(work, "init", ["git", "init", "-q", work], _u => whole.run(work, "fetch", ["git", "-C", work, "fetch", "-q", "--depth", "1", url, rev], _v => whole.run(work, "checkout", ["git", "-C", work, "checkout", "-q", rev], _w => whole.weigh(work, rev)))) Spin.done(s) return a # a checkout at a rev for `ez add`: a shallow clone into a scratch directory # outside the project and BEND_LIB, weighed, and every file's text read, so # the planner walks the package itself. The scratch directory is removed # whether or not the clone worked, so a refused add leaves no trace. def answer.whole(+url: String, +rev: String) -> IO(Up.Answer): do IO: +tmp : String <- R.exec(["mktemp", "-d"]) whole.dir(R.ok(tmp), String.trim(R.text(tmp)), url, rev) # one of the upgrade's questions, answered def answer.up(+lib: String, ask: Up.Ask) -> IO(Up.Answer): match ask: case Up.Refs{url, ref}: answer.refs(url, ref) case Up.Head{url}: answer.head(url) case Up.Tags{url}: answer.tags(url) case Up.Above{url, pin, tip}: answer.above(lib, url, pin, tip) case Up.Tree{url, rev, entry}: answer.tree(lib, url, rev, entry) case Up.Weigh{url, rev}: answer.weigh(url, rev) case Up.HubHas{hub, hash}: answer.hubhas(hub, hash) case Up.Ignore{}: answer.ignore() case Up.Sources{}: answer.sources() case Up.Clone{url, rev}: answer.whole(url, rev) # every one of them, answered def answer.ups(+lib: String, asks: List<&2, Up.Ask>) -> IO(List<&2, Up.Reply>): match asks: case []: IO.pure(List<&2, Up.Reply>, []) case +h <> t: do IO>: a : Up.Answer <- answer.up(lib, h) rest : List<&2, Up.Reply> <- answer.ups(lib, t) return Up.Reply{h, a} <> rest # every query of the lock's upgrade: git is asked the question as the planner # anchored it, and the answer is recorded under the question the upgrade # looks it up by def answer.queries(+lib: String, qs: List<&2, Plan.Query>) -> IO(List<&2, Up.Reply>): match qs: case []: IO.pure(List<&2, Up.Reply>, []) case Plan.Query{ask, git} <> t: do IO>: a : Up.Answer <- answer.up(lib, git) rest : List<&2, Up.Reply> <- answer.queries(lib, t) return Up.Reply{ask, a} <> rest # the World with more answers in it def more(world: W.World, rs: List<&2, W.Reply>, us: List<&2, Up.Reply>) -> W.World: W.World{args, ledger, listing, replies, ups} = world W.World{args, ledger, listing, List.append(&2, W.Reply, replies, rs), List.append(&2, Up.Reply, ups, us)} # --------------------------------------------------------------------------- # executing a plan # a write that did not happen stops the command def wrote(ok: Bool, +at: String) -> IO(Unit): match ok: case True{}: IO.pure(Unit, Unit{}) case False{}: IO.die(Unit, 1, "ez: " ++ at ++ " could not be written") # one file written, with the directory it goes in made first def write(+at: String, text: String) -> IO(Unit): do IO: _mk : String <- R.exec(["mkdir", "-p", P.dir(at)]) ok : Bool <- F.write(at, text) wrote(ok, at) # every file of a tree, in order def lay.all(+dir: String, fs: List<&2, W.Source>) -> IO(Unit): match fs: case []: IO.pure(Unit, Unit{}) case W.Source{at, text} <> t: do IO: write(dir ++ "/" ++ at, text) lay.all(dir, t) # where a place is def place(+lib: String, pl: Plan.Place) -> String: match pl: case Plan.Committed{}: ".ez/lib" case Plan.Cache{}: lib # one effect def exec(+lib: String, effect: Plan.Effect) -> IO(Unit): match effect: case Plan.Write{at, text}: write(at, text) case Plan.Lay{pl, hash, fs}: +dir = place(lib, pl) ++ "/" ++ hash do IO: _rm : String <- R.exec(["rm", "-rf", dir]) lay.all(dir, fs) case Plan.Drop{hash}: do IO: _rm : String <- R.exec(["rm", "-rf", ".ez/lib/" ++ hash]) return Unit{} case Plan.Say{text}: IO.print(text) case Plan.Name{nv, hash}: write(lib ++ "/names/" ++ nv, L.name.text(hash)) # every effect, in order def exec.all(+lib: String, es: List<&2, Plan.Effect>) -> IO(Unit): match es: case []: IO.pure(Unit, Unit{}) case h <> t: do IO: exec(lib, h) exec.all(lib, t) # a status that is 0 ends nothing, so the program runs out and exits 0; any # other exits with that status and the reason def end.at(zero: Bool, +code: U32, +why: String) -> IO(Unit): match zero: case True{}: IO.pure(Unit, Unit{}) case False{}: IO.die(Unit, code, why) # how the command ends: with the status the planner gave its outcome # (`Plan.status`, EZ-OUT-1), and the reason when it refused def end(+outcome: Plan.Outcome) -> IO(Unit): +code = Plan.status(outcome) end.at(U32.is_eq(code, 0), code, Plan.why(outcome)) # a plan, executed def exec.plan(+lib: String, pl: Plan.Plan) -> IO(Unit): Plan.Plan{es, outcome} = pl do IO: exec.all(lib, es) end(outcome) # --------------------------------------------------------------------------- # the loop # the planner's step: its questions answered and the loop gone round again, # or its plan run def loop.step(st: Plan.Step, +lib: String, world: W.World, go: W.World -> IO(Unit)) -> IO(Unit): match st: case Plan.Asking{asks, ups, names}: do IO: rs : List<&2, W.Reply> <- answer.all(lib, asks) us : List<&2, Up.Reply> <- answer.queries(lib, ups) ns : List<&2, W.Reply> <- answer.all(lib, names) go(more(world, List.append(&2, W.Reply, rs, ns), us)) case Plan.Run{pl}: exec.plan(lib, pl) # the rounds the loop may take. Each round's questions come from the last # round's answers (a package's imports are known once git has answered for # it), so how many there will be is not known up front. This is a guard # against a planner that keeps asking, not a limit on a lock: running out of # it dies loudly. The walk inside each round takes its fuel from its inputs # (`Plan.walk.fuel`). def rounds() -> Nat: U32.to_nat(100000) # the loop, under fuel. Every round adds at least one answer, since the # planner never asks what the World answers already. def loop(fuel: Nat, +lib: String, +world: W.World) -> IO(Unit): match fuel: case 0n: IO.die(Unit, 1, "ez: the lock kept asking questions") case 1n+f: loop.step(Plan.step(world), lib, world, w => loop(f, lib, w)) # `ez lock` and `ez lock --upgrade [--package only]`: the directory it runs # in, the ledger and the listing read, every question answered, and the plan # run def run(up: Bool, +only: String) -> IO(Unit): do IO: IO.print_err(Say.resolving()) lib : String <- bend.lib() +pwd : String <- R.exec(["pwd"]) led : Maybe<&2, String> <- read.ledger() ls : W.Listing <- read.listing() loop(rounds(), lib, W.World{W.Args{up, only, String.trim(R.text(pwd))}, led, ls, [], []})