src/lock/run.bend source
src/lock/run.bend on the hub · documented module
# 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 Baseimport 0xabe575924687afad4cee1a2c1194d639/main.bend as Rimport ../io/file.bend as Fimport ../hub/hub.bend as Webimport ../hub/get.bend as Netimport ../git/git.bend as Gitimport ../git/exec.bend as GXimport ../pkg/pkg.bend as Kimport ../pkg/path.bend as Pimport ../share/say.bend as Sayimport ../share/spin.bend as Spinimport ./lock.bend as Limport ./world.bend as Wimport ./up.bend as Upimport ./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/libdef lib.or(+dir: String) -> String: Bool.pick(String, String.is_empty(dir), ".ez/lib", dir)# the BEND_LIB this project pinsdef bend.lib() -> IO(String): do IO<String>: r : Result<&1, &1, U32 & String, String> <- IO.get_env("BEND_LIB") return lib.or(String.trim(env.text(r)))# the non-empty linesdef 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.tomldef read.ledger() -> IO(Maybe<&2, String>): F.read("ez.toml")# every listed path, readdef 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<List<&2, W.Source>>: 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 notdef listed(ok: Bool, +text: String) -> IO(W.Listing): match ok: case True{}: do IO<W.Listing>: 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<W.Listing>: +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 missdef 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 sumdef 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<List<&2, Web.Got>>: 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 arriveddef 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<W.Answer>: 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 namesdef answer.hub(+hub: String, +hash: String) -> IO(W.Answer): do IO<W.Answer>: +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<List<&2, String>>: 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 isdef answer.lib(+dir: String, +manifest: String) -> IO(W.Answer): do IO<W.Answer>: 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 whydef clone.miss(+work: String, +why: String) -> IO(W.Answer): do IO<W.Answer>: _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 itdef 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 workeddef clone.run(+work: String, what: String, args: List<&2, String>, go: Unit -> IO(W.Answer)) -> IO(W.Answer): do IO<W.Answer>: +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 itdef clone.walk(+work: String, +entry: String, +nar: String) -> IO(W.Answer): do IO<W.Answer>: +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 walkdef 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 walkeddef clone.weigh(+work: String, +url: String, +rev: String, +entry: String) -> IO(W.Answer): do IO<W.Answer>: _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<W.Answer>: _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 cloneddef 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 packagedef answer.git(+lib: String, +hash: String, +url: String, +rev: String, +entry: String) -> IO(W.Answer): do IO<W.Answer>: m : Maybe<&2, String> <- F.read(lib ++ "/" ++ hash ++ "/manifest") git.seen(m, lib, hash, url, rev, entry)# a package, from where its source saysdef 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 itdef 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 <hub>/name/<nv>`def answer.name(+hub: String, +nv: String) -> IO(W.Answer): do IO<W.Answer>: +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 nonedef answer.lock() -> IO(W.Answer): do IO<W.Answer>: m : Maybe<&2, String> <- F.read(W.lockfile()) return W.Locked{F.text_of(m)}# one question, answereddef 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, answereddef 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<List<&2, W.Reply>>: 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 wasdef git.said(+what: String, args: List<&2, String>) -> IO(Up.Answer): do IO<Up.Answer>: +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 questiondef 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 todef answer.head(+url: String) -> IO(Up.Answer): git.said(Say.resolving.at(url), ["git", "ls-remote", "--symref", url, "HEAD"])# every tag on the remotedef 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 clonedef answer.above(+lib: String, +url: String, +pin: String, +tip: String) -> IO(Up.Answer): do IO<Up.Answer>: 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 repodef tree.walk(+work: String, +at: String, +nar: String) -> IO(Up.Answer): do IO<Up.Answer>: +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 walkdef 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<Up.Answer>: _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<Up.Answer>: _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 tooldef answer.weigh(+url: String, +rev: String) -> IO(Up.Answer): do IO<Up.Answer>: +nar : String <- GX.weigh(url, rev) return Up.Weighed{nar}# the hub's manifest for a hash a dependency moved todef answer.hubhas(+hub: String, +hash: String) -> IO(Up.Answer): do IO<Up.Answer>: g : Web.Got <- Net.fetch(L.url_of(hub, hash, "manifest"), L.want(hash)) return Up.HubSaid{g}# the ignore file, "" when it is missingdef answer.ignore() -> IO(Up.Answer): do IO<Up.Answer>: 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 readdef 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, readdef found.all(ps: List<&2, String>) -> IO(Up.Answer): match ps: case []: IO.pure(Up.Answer, Up.Files{[]}) case +h <> t: do IO<Up.Answer>: 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<Up.Answer>: +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 whydef whole.miss(+work: String, +why: String) -> IO(Up.Answer): do IO<Up.Answer>: _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# itdef 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 workeddef whole.run(+work: String, what: String, args: List<&2, String>, go: Unit -> IO(Up.Answer)) -> IO(Up.Answer): do IO<Up.Answer>: +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 restdef 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<List<&2, K.Source>>: 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<Up.Answer>: 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<Up.Answer>: _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<Up.Answer>: _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 bedef 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<Up.Answer>: +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<Up.Answer>: +tmp : String <- R.exec(["mktemp", "-d"]) whole.dir(R.ok(tmp), String.trim(R.text(tmp)), url, rev)# one of the upgrade's questions, answereddef 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, answereddef 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<List<&2, Up.Reply>>: 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 bydef 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<List<&2, Up.Reply>>: 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 itdef 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 commanddef 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 firstdef write(+at: String, text: String) -> IO(Unit): do IO<Unit>: _mk : String <- R.exec(["mkdir", "-p", P.dir(at)]) ok : Bool <- F.write(at, text) wrote(ok, at)# every file of a tree, in orderdef 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<Unit>: write(dir ++ "/" ++ at, text) lay.all(dir, t)# where a place isdef place(+lib: String, pl: Plan.Place) -> String: match pl: case Plan.Committed{}: ".ez/lib" case Plan.Cache{}: lib# one effectdef 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<Unit>: _rm : String <- R.exec(["rm", "-rf", dir]) lay.all(dir, fs) case Plan.Drop{hash}: do IO<Unit>: _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 orderdef exec.all(+lib: String, es: List<&2, Plan.Effect>) -> IO(Unit): match es: case []: IO.pure(Unit, Unit{}) case h <> t: do IO<Unit>: 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 reasondef 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 refuseddef end(+outcome: Plan.Outcome) -> IO(Unit): +code = Plan.status(outcome) end.at(U32.is_eq(code, 0), code, Plan.why(outcome))# a plan, executeddef exec.plan(+lib: String, pl: Plan.Plan) -> IO(Unit): Plan.Plan{es, outcome} = pl do IO<Unit>: exec.all(lib, es) end(outcome)# ---------------------------------------------------------------------------# the loop# the planner's step: its questions answered and the loop gone round again,# or its plan rundef 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<Unit>: 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 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# rundef run(up: Bool, +only: String) -> IO(Unit): do IO<Unit>: 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(U32.to_nat(100000), lib, W.World{W.Args{up, only, String.trim(R.text(pwd))}, led, ls, [], []})