~/bend-docscommunity

src/lock/world.bend source

src/lock/world.bend on the hub · documented module

# lock/world: everything `ez lock` reads, as one value, and what the lock may# depend on in it. The planner (lock/plan.bend) is a pure function of a# World; the interpreter (lock/run.bend) gathers one, answering each question# the planner asks by IO, and executes the plan the planner returns. The# design is docs/rfc/ez-lock-planner.md.## A World is the ledger's text, the committed `.bend` files, one answer per# package the planner asked about, and, under `--upgrade`, one answer per# question the upgrade asked (lock/up.bend). There is no field for the# environment, the clock, untracked files, `.ez/origins.toml` or BEND_LIB's# path, because the lock reads none of them. A package a dependency imports# by `<name>@<version>` adds two questions, asked only when the ledger does# not name it: the lock being rewritten, which already records every name it# resolved, and, for a name it does not record, what the hub says the name# names. Both are answers like any other, and enter the lock through their# verdicts.import Baseimport ./lock.bend as Limport ./up.bend as Upimport ../ledger/upgrade.bend as Uimport ../ledger/manifest.bend as Mimport ../pkg/pkg.bend as Kimport ../hub/hub.bend as Webimport ../share/sha.bend as Shaimport ../share/say.bend as Say# the command as it was run: `--upgrade`, the name `--package` gave ("" for# none), and the directory it runs in, which is the project root. A relative# path source is recorded as it was given, relative to that root, and every# question the lock asks about it names it anchored there, as the planner# computes it (`P.anchor.src`, `Up.anchor`).type Args is Data:  Args{upgrade: Bool, only: String, here: String}# one committed `.bend` file: its path and its texttype Source is Data:  Source{at: String, text: String}# what `git ls-files '*.bend'` answered: the committed sources, or why git# could not list themtype Listing is Data:  Listed{files: List<&2, Source>}  Unlisted{why: String}# how the interpreter came by a package's bytes. A tree already under# BEND_LIB, vendored or left by an earlier lock, was read from there. One that# was not was cloned at the rev the ledger pins, and weighed. A hub package was# served by the hub.type How is Data:  Lib{}  Clone{nar: String}  Served{}# a question the planner asks, answered by one IO action: a package's# manifest and the text of each file it names, where the source says to read# it from, and the hub is where a hub package is served; the hash the hub# says a `<name>@<version>` names, `GET <hub>/name/<nv>`; or the lock being# rewritten, ez.lock.toml as it is.type Ask is Data:  Pkg{hash: String, src: L.Src, hub: String}  Name{nv: String, hub: String}  Lock{}# the answer to one question: the manifest found and the text of each file it# names, in the manifest's order; what the hub said a name names, as it said# it; the lock's text, "" when there is none; or why IO could not answertype Answer is Data:  Got{how: How, manifest: String, srcs: List<&2, String>}  Miss{why: String}  Said{text: String}  Locked{text: String}# one question, answeredtype Reply is Data:  Reply{ask: Ask, answer: Answer}# everything `ez lock` reads. The ledger is ez.toml's text, or None when# there is no ez.toml. `ups` answers the upgrade's questions, and is empty for# a plain lock, which asks none.type World is Data:  World{args: Args, ledger: Maybe<&2, String>, listing: Listing,    replies: List<&2, Reply>, ups: List<&2, Up.Reply>}# what the planner makes of one answer: the files and their texts, once every# check passed, or why it refused them; or, for a name or the lock, the names# it resolves, each to its hashtype Verdict is Data:  Ok{files: List<&2, K.Item>, srcs: List<&2, String>}  No{why: String}  Named{names: List<&2, L.Name>}# one package hash and its verdicttype Judged is Data:  Judged{hash: String, verdict: Verdict}# the part of a World the lock may depend on: the ledger the lock is made# from, read, which under `--upgrade` is the one the upgrade leaves; why the# upgrade refuses, "" when it does not; the committed sources; and the verdict# on every package. A reply enters only through its verdict, so how the bytes# arrived does not, and bytes that failed a check enter only as the refusal# they caused.type Inputs is Data:  Inputs{ledger: M.Read, stop: String, listing: Listing, judged: List<&2, Judged>}# whether the command was `ez lock --upgrade`def args.upgrade(world: World) -> Bool:  World{args, _ledger, _listing, _replies, _ups} = world  Args{up, _only, _here} = args  up# the name `--package` gave, "" for nonedef args.only(world: World) -> String:  World{args, _ledger, _listing, _replies, _ups} = world  Args{_up, only, _here} = args  only# the directory the command runs indef here.of(args: Args) -> String:  Args{_up, _only, here} = args  here# the directory the lock runs in, which is the project rootdef args.here(world: World) -> String:  World{args, _ledger, _listing, _replies, _ups} = world  here.of(args)# the ledger's textdef ledger.of(world: World) -> Maybe<&2, String>:  World{_args, ledger, _listing, _replies, _ups} = world  ledger# the lock's path, which is fixeddef lockfile() -> String:  "ez.lock.toml"# what a question asks about, which is the key its answer is judged and# looked up under: a package's hash, a name, or the lock's path. A `0x` name,# a `<name>@<version>` and the lock's path are three different strings.def ask.hash(ask: Ask) -> String:  match ask:    case Pkg{hash, _src, _hub}:      hash    case Name{nv, _hub}:      nv    case Lock{}:      lockfile()# ---------------------------------------------------------------------------# judging one answer# whether every text hashes to the sum its file is recorded with. A list of# texts that is longer or shorter than the files is not the package.def sums.ok(fs: List<&2, K.Item>, ss: List<&2, String>) -> Bool:  match fs ss:    case Nil{} Nil{}:      True{}    case Nil{} Con{_h, _t}:      False{}    case Con{_f, _g} Nil{}:      False{}    case Con{f, g} Con{s, t}:      +rest = sums.ok(g, t)      Bool.and(String.eq(Sha.hex(s), K.file.sum(f)), rest)# each file with its text, as it is laiddef laid(fs: List<&2, K.Item>, ss: List<&2, String>) -> List<&2, Source>:  match fs ss:    case Nil{} Nil{}:      []    case Nil{} Con{_s, _t}:      []    case Con{_f, _g} Nil{}:      []    case Con{f, g} Con{s, t}:      Source{K.file.at(f), s} <> laid(g, t)# a laid file weighed by its own textdef weigh(src: Source) -> K.Item:  Source{at, +text} = src  K.Item{at, Sha.hex(text)}# every laid file weighed by its own textdef weighs(ls: List<&2, Source>) -> List<&2, K.Item>:  match ls:    case []:      []    case h <> t:      weigh(h) <> weighs(t)# the manifest of the files laid, weighed by their own texts, which is the# manifest a tree is laid withdef manifest(+ls: List<&2, Source>) -> String:  K.manifest_of(K.files_of(weighs(ls)))# the `0x` name of that manifestdef named(+ls: List<&2, Source>) -> String:  K.hash_of(K.files_of(weighs(ls)))# the verdict once the files, weighed by their own texts, were asked whether# they hash to the name. Their manifest is the one the tree is laid with, and# the manifest that was read hashes to the name already, so a package whose# manifest is not written the way ez writes one (a line per file, in path# order, each file once) is refused here rather than laid under a name its# laid manifest does not hash to.def body.laid(ok: Bool, +hash: String, fs: List<&2, K.Item>, ss: List<&2, String>) -> Verdict:  match ok:    case True{}:      Ok{fs, ss}    case False{}:      No{"ez: " ++ hash ++ ": its manifest is not written the way its name is taken" ++        " (one line per file, in path order, each file once)"}# the verdict once the texts were weighed against the manifestdef body.sums(ok: Bool, +hash: String, +fs: List<&2, K.Item>, +ss: List<&2, String>) -> Verdict:  match ok:    case True{}:      body.laid(String.eq(hash, named(laid(fs, ss))), hash, fs, ss)    case False{}:      No{"ez: " ++ hash ++ ": a file does not match its manifest"}# the verdict once each path was asked whether it escapes the packagedef body.escape.go(clean: Bool, +bad: String, +hash: String, +fs: List<&2, K.Item>, +ss: List<&2, String>) -> Verdict:  match clean:    case True{}:      body.sums(sums.ok(fs, ss), hash, fs, ss)    case False{}:      No{"ez: " ++ hash ++ ": its manifest escapes the package (" ++ bad ++ ")"}# the verdict once no path was found to escape the packagedef body.escape(+bad: String, +hash: String, +fs: List<&2, K.Item>, +ss: List<&2, String>) -> Verdict:  body.escape.go(String.is_empty(bad), bad, hash, fs, ss)# the verdict once the manifest was known to be the one the hash namesdef body.named(ok: Bool, +hash: String, +manifest: String, +ss: List<&2, String>) -> Verdict:  match ok:    case True{}:      +fs = L.manifest.files(String.lines(manifest))      body.escape(K.escaped(fs), hash, fs, ss)    case False{}:      No{"ez: " ++ hash ++ ": its manifest does not hash to its name"}# a package's bytes, checked the same way whatever they came from: the# manifest is the one the `0x` name is the digest of, no path leaves the# package, each text hashes to its file's sum, and the files, weighed by their# own texts, hash to the name, which is `ez fetch`'s check too. A tree under# BEND_LIB is checked against its name like any other, so a stale or edited# tree there is refused rather than locked.def body(+hash: String, +manifest: String, +ss: List<&2, String>) -> Verdict:  body.named(Web.matches(L.want(hash), manifest), hash, manifest, ss)# a git package that arrived with no files is refused, never locked as an# empty tabledef git.body(empty: Bool, +hash: String, +manifest: String, +ss: List<&2, String>) -> Verdict:  match empty:    case True{}:      No{"ez: " ++ hash ++ ": its manifest names no files"}    case False{}:      body(hash, manifest, ss)# a git package's bytes under the rule its name was taken by. Since bend# 2.0.27 a package holds the LICENSE files beside its sources, and the walk# takes them along. A ledger written before names the same checkout without# them, and that package is the checkout's too: when the walk's manifest does# not hash to the name, the package without its LICENSE files is judged# instead, and locked when that is the one the name is the digest of.def git.rule(named: Bool, +hash: String, +manifest: String, +ss: List<&2, String>) -> Verdict:  match named:    case True{}:      git.body(List.is_empty(&2, K.Item, L.manifest.files(String.lines(manifest))),        hash, manifest, ss)    case False{}:      +fs = L.manifest.files(String.lines(manifest))      +bs = K.bare(fs)      git.body(List.is_empty(&2, K.Item, bs), hash, K.manifest.lines(bs),        K.bare.srcs(fs, ss))# a git package's bytes once its checkout was weighed. A tree read from# BEND_LIB is judged as a clone that weighed to the narHash the ledger# records, so the same bytes lock the same way whichever way they came.def git.weighed.go(same: Bool, +why: String, +hash: String, +manifest: String, +ss: List<&2, String>) -> Verdict:  match same:    case True{}:      git.rule(Web.matches(L.want(hash), manifest), hash, manifest, ss)    case False{}:      No{why}def git.weighed(+why: String, +hash: String, +manifest: String, +ss: List<&2, String>) -> Verdict:  git.weighed.go(String.is_empty(why), why, hash, manifest, ss)# a git package, by how it arriveddef git.how(  how: How,  +hash: String,  +url: String,  +rev: String,  +nar: String,  +manifest: String,  +ss: List<&2, String>) -> Verdict:  match how:    case Lib{}:      git.weighed(L.nar.why(nar, nar, url, rev), hash, manifest, ss)    case Clone{got}:      git.weighed(L.nar.why(nar, got, url, rev), hash, manifest, ss)    case Served{}:      No{"ez: " ++ hash ++ " is a git package and was not read from git"}# a hub package, by how it arrived. Only the hub serves one.def hub.how(how: How, +hash: String, +manifest: String, +ss: List<&2, String>) -> Verdict:  match how:    case Lib{}:      No{"ez: " ++ hash ++ " is a hub package and was not served by the hub"}    case Clone{_got}:      No{"ez: " ++ hash ++ " is a hub package and was not served by the hub"}    case Served{}:      body(hash, manifest, ss)# a package's answer against where the ledger says it comes fromdef accept.src(src: L.Src, how: How, +hash: String, +manifest: String, +ss: List<&2, String>) -> Verdict:  match src:    case L.Hub{}:      hub.how(how, hash, manifest, ss)    case L.Git{url, rev, _entry, _root, nar, _tag}:      git.how(how, hash, url, rev, nar, manifest, ss)# why a hub package could not be read. One the ledger names is explained the# way a hub miss always was. One it does not name is said to be unnamed, since# that, and not the hub, is what a person has to fix.def hub.why(named: Bool, +hash: String, +hub: String, +why: String) -> String:  match named:    case True{}:      Say.hub.explain(hash, L.url_of(hub, hash, "manifest"), why)    case False{}:      "ez: " ++ hash ++ " is imported, ez.toml does not name it, and the hub at " ++        hub ++ " did not serve it (" ++ why ++ "). A package taken from git is recorded with `ez add`."# why IO could not answer, said for where the package comes fromdef miss.src(src: L.Src, named: Bool, +hash: String, +hub: String, +why: String) -> String:  match src:    case L.Hub{}:      hub.why(named, hash, hub, why)    case L.Git{_url, _rev, _entry, _root, _nar, _tag}:      why# what the hub said a name names, once it is known whether the answer is a# `0x` namedef said.ok(ok: Bool, +nv: String, +hash: String, +hub: String) -> Verdict:  match ok:    case True{}:      Named{[L.Name{nv, hash}]}    case False{}:      No{"ez: the hub at " ++ hub ++ " names no package " ++ nv ++ " (it answered `" ++        hash ++ "`)"}# the hub's answer for a name: the `0x` name it gives, trimmed as bend trims# it, or a refusal when it gives nonedef said(+nv: String, +hub: String, +text: String) -> Verdict:  +hash = String.trim(text)  said.ok(L.hash.ok(hash), nv, hash, hub)# the lock being rewritten: every name it records that bend would read backdef locked(text: String) -> Verdict:  Named{L.names.read(text)}# one answer judgeddef accept(+orgs: List<&2, L.Origin>, +hub: String, +hash: String, answer: Answer) -> Verdict:  match answer:    case Miss{why}:      No{miss.src(L.origin(orgs, hash), L.known(orgs, hash), hash, hub, why)}    case Got{how, manifest, ss}:      accept.src(L.origin(orgs, hash), how, hash, manifest, ss)    case Said{text}:      said(hash, hub, text)    case Locked{text}:      locked(text)# every reply judged, in the order they were givendef judge(+orgs: List<&2, L.Origin>, +hub: String, rs: List<&2, Reply>) -> List<&2, Judged>:  match rs:    case []:      []    case Reply{ask, answer} <> t:      +hash = ask.hash(ask)      Judged{hash, accept(orgs, hub, hash, answer)} <> judge(orgs, hub, t)# the names a verdict resolves: those of a name's or the lock's answer, and# none for a packagedef verdict.names(verdict: Verdict) -> List<&2, L.Name>:  match verdict:    case Ok{_fs, _ss}:      []    case No{_why}:      []    case Named{ns}:      ns# every name the answers resolve, in the order they were given. The lock's# answer is asked for before any name's, so a name it records is never put# to the hub.def names.of(js: List<&2, Judged>) -> List<&2, L.Name>:  match js:    case []:      []    case Judged{_h, v} <> t:      List.append(&2, L.Name, verdict.names(v), names.of(t))# the ledger, read. A missing one is refused as a ledger that does not# parse: only `ez init` makes a ledger, and a lock of none is not a lock.def parsed(ledger: Maybe<&2, String>) -> M.Read:  match ledger:    case None{}:      M.Bad{"no ez.toml here; ez lock works on a project's ledger, and ez init makes one"}    case Some{text}:      M.parse(text)# the origins a ledger recordsdef origins(+ledger: Maybe<&2, String>) -> List<&2, L.Origin>:  L.ledger.read(parsed(ledger))# the dependency ez.toml names `name`, as `M.dep` finds itdef dep(world: World, name: String) -> M.Dep:  World{_args, ledger, _listing, _replies, _ups} = world  M.dep(parsed(ledger), name)# the hub a ledger namesdef ledger.hub(+ledger: Maybe<&2, String>) -> String:  M.hub_of(parsed(ledger))# ---------------------------------------------------------------------------# the ledger the lock is made from# a plain lock's is ez.toml as it is. An upgrade's is what the upgrade# decides on its answers: the ledger it writes, or its refusal, or the# questions it still has.def next.up(up: Bool, +only: String, +ledger: Maybe<&2, String>, ups: List<&2, Up.Reply>) -> Up.Next:  match up:    case False{}:      Up.plain(parsed(ledger))    case True{}:      Up.next(only, parsed(ledger), ups)# what the upgrade decides on a World, or nothing around a plain lockdef next(args: Args, +ledger: Maybe<&2, String>, ups: List<&2, Up.Reply>) -> Up.Next:  Args{up, +only, _here} = args  next.up(up, only, ledger, ups)# the origins the lock reads packages bydef next.origins(+nx: Up.Next) -> List<&2, L.Origin>:  L.ledger.read(Up.next.read(nx))# the hub it namesdef next.hub(+nx: Up.Next) -> String:  M.hub_of(Up.next.read(nx))# a tree the upgrade checked out at a new rev, as the answer to the package# question the lock would ask about it: a clone, weigheddef laid.reply(laid: Up.Laid, +orgs: List<&2, L.Origin>, +hub: String) -> Reply:  Up.Laid{+hash, nar, manifest, srcs} = laid  Reply{Pkg{hash, L.origin(orgs, hash), hub}, Got{Clone{nar}, manifest, srcs}}def laid.replies(ls: List<&2, Up.Laid>, +orgs: List<&2, L.Origin>, +hub: String) -> List<&2, Reply>:  match ls:    case []:      []    case h <> t:      laid.reply(h, orgs, hub) <> laid.replies(t, orgs, hub)# the package answers the lock reads: the trees the upgrade checked out, then# every answer the World holdsdef replies.of(+nx: Up.Next, replies: List<&2, Reply>) -> List<&2, Reply>:  List.append(&2, Reply,    laid.replies(Up.next.trees(nx), next.origins(nx), next.hub(nx)), replies)# every package answer judged against the ledger the lock is made fromdef judged.of(+nx: Up.Next, replies: List<&2, Reply>) -> List<&2, Judged>:  List.append(&2, Judged,    judge(next.origins(nx), next.hub(nx),      laid.replies(Up.next.trees(nx), next.origins(nx), next.hub(nx))),    judge(next.origins(nx), next.hub(nx), replies))# every committed source with each hash the upgrade moved rewritten in its# imports, as the upgrade writes itdef sources.swap(+ss: List<&2, U.Swap>, fs: List<&2, Source>) -> List<&2, Source>:  match fs:    case []:      []    case Source{at, text} <> t:      Source{at, U.reimport.many(ss, text)} <> sources.swap(ss, t)def listing.swap(+ss: List<&2, U.Swap>, listing: Listing) -> Listing:  match listing:    case Listed{fs}:      Listed{sources.swap(ss, fs)}    case Unlisted{why}:      Unlisted{why}# the committed sources as the lock reads them: as the upgrade leaves them,# which is as they are when no hash moved, and always for a plain lockdef listing.moved(ss: List<&2, U.Swap>, listing: Listing) -> Listing:  match ss:    case []:      listing    case h <> t:      listing.swap(h <> t, listing)def listing.of(+nx: Up.Next, listing: Listing) -> Listing:  listing.moved(Up.next.swaps(nx), listing)# what a World is, as far as the lock may tell, once the upgrade decideddef inputs.of(+nx: Up.Next, listing: Listing, replies: List<&2, Reply>) -> Inputs:  Inputs{Up.next.read(nx), Up.next.stop(nx), listing.of(nx, listing),    judged.of(nx, replies)}# what a World is, as far as the lock may telldef inputs(world: World) -> Inputs:  World{args, +ledger, listing, replies, ups} = world  inputs.of(next(args, ledger, ups), listing, replies)# ---------------------------------------------------------------------------# a fresh clone's World# one answer as a fresh clone would have it: a tree read from BEND_LIB is# instead cloned at the rev the ledger pins and weighed to the narHash the# ledger records. Anything else is what it was.def reclone.how(how: How, +src: L.Src) -> How:  match how:    case Lib{}:      Clone{L.src.nar(src)}    case Clone{got}:      Clone{got}    case Served{}:      Served{}def reclone.one(+orgs: List<&2, L.Origin>, +hash: String, answer: Answer) -> Answer:  match answer:    case Miss{why}:      Miss{why}    case Got{how, manifest, ss}:      Got{reclone.how(how, L.origin(orgs, hash)), manifest, ss}    case Said{text}:      Said{text}    case Locked{text}:      Locked{text}# every answer as a fresh clone would have it. A tree the upgrade checked# out is a clone already.def reclone.all(+orgs: List<&2, L.Origin>, rs: List<&2, Reply>) -> List<&2, Reply>:  match rs:    case []:      []    case Reply{+ask, answer} <> t:      Reply{ask, reclone.one(orgs, ask.hash(ask), answer)} <> reclone.all(orgs, t)# the World a fresh clone of the same commit gathers: the same ledger, the# same committed sources, and every tree it would have read from BEND_LIB# cloned and weighed instead, since a clone has nothing theredef reclone(world: World) -> World:  World{+args, +ledger, listing, replies, +ups} = world  World{args, ledger, listing,    reclone.all(next.origins(next(args, ledger, ups)), replies), ups}