1 # tool/plan: `ez tool run`, `install` and `upgrade` as a pure planner, and 2 # `ez tool sync` as the list of installs it runs. The planner reads a World 3 # (tool/world.bend) and returns either the questions it still needs answered 4 # (`step`) or a Plan. The interpreter (tool/run.bend) loops on `step`, 5 # answering each question by IO, then executes the plan. 6 # 7 # Before it asks anything the planner refuses what needs no answer: an 8 # install or upgrade with nowhere to link (EZ-TOOL-1), no cache root, a 9 # project ledger that does not parse, a target that names nothing or a cache 10 # slug that could leave the cache (EZ-TOOL-8), a name the ledger pins that 11 # the lock does not, or pins at no commit. Then it resolves the commit 12 # (EZ-TOOL-4): a `[tools.*]` pin is the lock's rev, url, entry and bin; a 13 # remote is what its HEAD names; a path is its clean HEAD, and a path with 14 # uncommitted or untracked changes, or one that is not the top of a 15 # checkout, is no commit (EZ-TOOL-3). Then the checkout: a remote's is 16 # cached per commit and read from there when its record says it finished, 17 # otherwise asked of git with every file's text; a path is read where it is. 18 # Then its ledger, which names the file built (EZ-TOOL-7) and the link, and 19 # a name that is not a TOML bare key is refused. A checkout with no ez.toml 20 # is a plain Bend repository: it builds `main.bend`, or the file `--entry` 21 # names, needs no lock, and is linked under that file's stem, or under the 22 # repository's name when the stem is `main`. A file to build that is not in 23 # the checkout is refused. Then the binary's record: 24 # the binary is reused only when it is there and its record is this commit, 25 # this file and this bend (EZ-TOOL-2). Every binary lives in a directory 26 # named by its record, so no two records share one, and a link made by an 27 # install is never rebuilt under it by a run of another commit or file. 28 # 29 # The plan of a command that goes on, in order: the checkout laid under the 30 # cache when it was cloned, with its record written last; the lock fetched, 31 # for an ez project, and the binary built with its record, unless it is 32 # reused; then the 33 # program started, for `run`, or the link written, for `install` and 34 # `upgrade`. A refusal has no effect at all. The program is started only as 35 # the last thing, so its status is the command's (EZ-TOOL-5), and `install` 36 # and `upgrade` never start it (EZ-TOOL-6). 37 import Base 38 import ./world.bend as TW 39 import ../lock/plan.bend as P 40 import ../lock/up.bend as Up 41 import ../ledger/manifest.bend as M 42 import ../toml/toml.bend as T 43 import ../pkg/pkg.bend as K 44 import ../pkg/path.bend as Path 45 import ../git/git.bend as Git 46 import ../ez/target.bend as Tgt 47 import ../ez/named.bend as Named 48 import ../ez/key.bend as Key 49 import ../share/say.bend as Say 50 import ../share/sha.bend as Sha 51 52 # --------------------------------------------------------------------------- 53 # small pieces 54 55 # whether a Maybe holds nothing 56 def none(got: Maybe<&2, String>) -> Bool: 57 match got: 58 case None{}: 59 True{} 60 case Some{_t}: 61 False{} 62 63 # whether an act links the binary: `install` and `upgrade` do, `run` does not 64 def links.act(act: TW.Act) -> Bool: 65 match act: 66 case TW.Run{_args}: 67 False{} 68 case TW.Install{}: 69 True{} 70 case TW.Upgrade{}: 71 True{} 72 73 # the tools of a ledger that was read 74 def tools.of(manifest: M.Manifest) -> List<&2, M.Tool>: 75 M.Manifest{_n, _e, _b, _h, _pa, _pv, _ds, ts} = manifest 76 ts 77 78 # the pins a lock's text records 79 def locked(+body: String) -> List<&2, M.Tool>: 80 M.tools(T.sects(T.parse(body))) 81 82 # --------------------------------------------------------------------------- 83 # what the target names 84 85 # a target resolved as far as the project can say: a `[tools.*]` pin, with 86 # the lock's url and rev and the file its `bin` or `entry` names; a free 87 # target, with the place its word names; or why there is none 88 type Src is Data: 89 Pin{name: String, url: String, rev: String, over: String} 90 Free{place: Tgt.Place} 91 NoSrc{why: String} 92 93 # the file a pin names: its `bin`, else its `entry`, else "" so the 94 # checkout's own ledger chooses 95 def over.of(+binary: String, +entry: String) -> String: 96 Bool.pick(String, String.is_empty(binary), entry, binary) 97 98 # a pin the lock has, or a refusal when it has none by that name 99 def pin.go(empty: Bool, +tool: M.Tool, +name: String) -> Src: 100 match empty: 101 case True{}: 102 NoSrc{"ez: " ++ name ++ " is not pinned in ez.lock.toml"} 103 case False{}: 104 Pin{name, M.source.url(M.tool.source(tool)), M.source.rev(M.tool.source(tool)), 105 over.of(M.tool.bin(tool), M.tool.entry(tool))} 106 107 # the pin the lock has for a name, or a refusal 108 def pin.of(+tool: M.Tool, +name: String) -> Src: 109 pin.go(String.is_empty(M.tool.name(tool)), tool, name) 110 111 # the pin the lock records under a name, one with empty fields for none 112 def pin.in(+body: String, +name: String) -> M.Tool: 113 M.tool.find(locked(body), name) 114 115 # a name the ledger pins is built as the lock pins it 116 def pinned(got: Maybe<&2, String>, +name: String) -> Src: 117 match got: 118 case None{}: 119 NoSrc{"ez: ez.lock.toml is not there"} 120 case Some{body}: 121 pin.of(pin.in(body, name), name) 122 123 # a name the ledger pins, or a free target 124 def src.pick(free: Bool, +world: TW.World) -> Src: 125 match free: 126 case True{}: 127 Free{Tgt.classify(TW.target(world))} 128 case False{}: 129 pinned(TW.lock(world), TW.target(world)) 130 131 # whether a ledger pins nothing under this name 132 def unpinned(+manifest: M.Manifest, +name: String) -> Bool: 133 String.is_empty(M.tool.name(M.tool.find(tools.of(manifest), name))) 134 135 # a project ledger that does not parse is refused 136 def src.read(led: M.Read, +world: TW.World) -> Src: 137 match led: 138 case M.Bad{why}: 139 NoSrc{"ez: error: " ++ why} 140 case M.Good{m}: 141 src.pick(unpinned(m, TW.target(world)), world) 142 143 # what the target names (EZ-TOOL-4) 144 def src(+world: TW.World) -> Src: 145 src.read(TW.project(world), world) 146 147 # a pin names a git repository; a path is no pin 148 def place.pin(pl: Tgt.Place) -> Tgt.Place: 149 match pl: 150 case Tgt.Bad{why}: 151 Tgt.Bad{why} 152 case Tgt.Local{_p}: 153 Tgt.Bad{"a pinned tool names a git repo"} 154 case Tgt.Remote{url, slug}: 155 Tgt.Remote{url, slug} 156 157 # where a resolved target is 158 def place.of(found: Src) -> Tgt.Place: 159 match found: 160 case Pin{_n, url, _r, _o}: 161 place.pin(Tgt.classify(url)) 162 case Free{pl}: 163 pl 164 case NoSrc{why}: 165 Tgt.Bad{why} 166 167 # where the target is: a remote with its cache slug, a path, or nowhere 168 def place(+world: TW.World) -> Tgt.Place: 169 place.of(src(world)) 170 171 # the file a pin names, "" for a free target 172 def over.src(found: Src) -> String: 173 match found: 174 case Pin{_n, _u, _r, o}: 175 o 176 case Free{_pl}: 177 "" 178 case NoSrc{_why}: 179 "" 180 181 # the file the target names for itself, "" when the checkout chooses 182 def over(+world: TW.World) -> String: 183 over.src(src(world)) 184 185 # --------------------------------------------------------------------------- 186 # the commit 187 188 # what git said of a local path, still to ask, or why there is nothing 189 type Loc is Data: 190 Loc{abs: String, top: String, head: String, ok: Bool, status: String} 191 LocWait{ask: TW.Ask} 192 LocNo{why: String} 193 194 # what git said of a path, looked up 195 def loc.of(got: TW.Heard, ask: TW.Ask) -> Loc: 196 match got: 197 case TW.Open{}: 198 LocWait{ask} 199 case TW.Heard{answer}: 200 match answer: 201 case TW.Located{abs, top, head, ok, status}: 202 Loc{abs, top, head, ok, status} 203 case TW.Miss{why}: 204 LocNo{why} 205 case _: 206 LocNo{"ez: a path was not answered with what git says of it"} 207 208 # the question about a local path, `~` expanded 209 def locate(+path: String, +world: TW.World) -> TW.Ask: 210 TW.Locate{Tgt.expand(TW.home(world), path)} 211 212 # what git said of a local path 213 def loc(+path: String, +world: TW.World) -> Loc: 214 loc.of(TW.heard(world, locate(path, world)), locate(path, world)) 215 216 # the real path of a local target, "" while it is not known 217 def loc.abs(spot: Loc) -> String: 218 match spot: 219 case Loc{abs, _t, _h, _ok, _s}: 220 abs 221 case LocWait{_a}: 222 "" 223 case LocNo{_why}: 224 "" 225 226 # the commit a local path builds: HEAD, when `git status` answered and named 227 # nothing, and the path is the top of its checkout. A path in a checkout 228 # whose top is elsewhere may be ignored there, or hold files the commit does 229 # not; either way it is no commit (EZ-TOOL-3). 230 def local.same(same: Bool, +head: String) -> String: 231 match same: 232 case True{}: 233 head 234 case False{}: 235 "" 236 237 def local.clean(clean: Bool, same: Bool, +head: String) -> String: 238 match clean: 239 case True{}: 240 local.same(same, head) 241 case False{}: 242 "" 243 244 def local.go(ok: Bool, clean: Bool, same: Bool, +head: String) -> String: 245 match ok: 246 case True{}: 247 local.clean(clean, same, head) 248 case False{}: 249 "" 250 251 def local.rev(+abs: String, +top: String, +head: String, ok: Bool, +status: String) -> String: 252 local.go(ok, String.is_empty(status), String.eq(top, abs), head) 253 254 # the commit of a local target, "" while it is not known 255 def loc.rev(spot: Loc) -> String: 256 match spot: 257 case Loc{abs, top, head, ok, status}: 258 local.rev(abs, top, head, ok, status) 259 case LocWait{_a}: 260 "" 261 case LocNo{_why}: 262 "" 263 264 # what a remote's HEAD printed 265 def head.said(+world: TW.World, +url: String) -> Up.Said: 266 Up.printed(TW.ups(world), Up.Head{url}, "ls-remote " ++ url) 267 268 # the commit HEAD names, "" while it is not known 269 def head.rev(said: Up.Said) -> String: 270 match said: 271 case Up.Said{text}: 272 Git.tip.rev(Git.rows(String.lines(text))) 273 case Up.Unsaid{_why}: 274 "" 275 case Up.Unasked{_ask}: 276 "" 277 278 # the commit of a free target 279 def rev.free(pl: Tgt.Place, +world: TW.World) -> String: 280 match pl: 281 case Tgt.Bad{_why}: 282 "" 283 case Tgt.Local{p}: 284 loc.rev(loc(p, world)) 285 case Tgt.Remote{url, _slug}: 286 head.rev(head.said(world, url)) 287 288 # the commit of a resolved target 289 def rev.of(found: Src, +world: TW.World) -> String: 290 match found: 291 case Pin{_n, _u, r, _o}: 292 r 293 case Free{pl}: 294 rev.free(pl, world) 295 case NoSrc{_why}: 296 "" 297 298 # the commit built, "" for none (EZ-TOOL-4) 299 def rev(+world: TW.World) -> String: 300 rev.of(src(world), world) 301 302 # --------------------------------------------------------------------------- 303 # where things are 304 305 # the cache root, "" for none 306 def root(+world: TW.World) -> String: 307 TW.text.or(TW.cache.of(world)) 308 309 # the cache directory of a target: `/` for a remote, and 310 # `/local` for a path 311 def dir.of(pl: Tgt.Place, +root: String, +world: TW.World) -> String: 312 match pl: 313 case Tgt.Bad{_why}: 314 root 315 case Tgt.Local{p}: 316 root ++ "/local" ++ loc.abs(loc(p, world)) 317 case Tgt.Remote{_url, slug}: 318 root ++ "/" ++ slug 319 320 # the target's cache directory 321 def dir(+world: TW.World) -> String: 322 dir.of(place(world), root(world), world) 323 324 # a remote's checkout of a commit lives in its own directory, so a pin and a 325 # free run at other commits never clone over each other 326 def revdir(+world: TW.World) -> String: 327 dir(world) ++ "/" ++ rev(world) 328 329 # the checkout's record, written once every file of it is laid 330 def stamp.at(+world: TW.World) -> String: 331 revdir(world) ++ "/rev" 332 333 # the checkout: under the cache for a remote, the path itself for a path 334 def top.of(pl: Tgt.Place, +world: TW.World) -> String: 335 match pl: 336 case Tgt.Bad{_why}: 337 "" 338 case Tgt.Local{p}: 339 loc.abs(loc(p, world)) 340 case Tgt.Remote{_url, _slug}: 341 revdir(world) ++ "/src" 342 343 # where the checkout is 344 def top(+world: TW.World) -> String: 345 top.of(place(world), world) 346 347 # the library the checkout's lock is fetched into: the checkout's own for a 348 # remote, and one under the cache for a path, which is never written into 349 def lib.of(pl: Tgt.Place, +world: TW.World) -> String: 350 match pl: 351 case Tgt.Bad{_why}: 352 "" 353 case Tgt.Local{_p}: 354 dir(world) ++ "/lib" 355 case Tgt.Remote{_url, _slug}: 356 top(world) ++ "/.ez/lib" 357 358 # --------------------------------------------------------------------------- 359 # answers read 360 361 # a file's text as it was answered, None when it is missing or not answered 362 def text.of(got: TW.Heard) -> Maybe<&2, String>: 363 match got: 364 case TW.Open{}: 365 None{} 366 case TW.Heard{answer}: 367 match answer: 368 case TW.Plain{m}: 369 m 370 case _: 371 None{} 372 373 def read.got(+world: TW.World, +path: String) -> Maybe<&2, String>: 374 text.of(TW.heard(world, TW.Read{path})) 375 376 # whether a question is still open 377 def open.of(got: TW.Heard) -> Bool: 378 match got: 379 case TW.Open{}: 380 True{} 381 case TW.Heard{_a}: 382 False{} 383 384 # whether a question of ours is still open 385 def open(+world: TW.World, +ask: TW.Ask) -> Bool: 386 open.of(TW.heard(world, ask)) 387 388 # a question kept when it is still open 389 def opens.step(open: Bool, ask: TW.Ask, rest: List<&2, TW.Ask>) -> List<&2, TW.Ask>: 390 match open: 391 case True{}: 392 ask <> rest 393 case False{}: 394 rest 395 396 # the questions of these still open 397 def opens(+world: TW.World, asks: List<&2, TW.Ask>) -> List<&2, TW.Ask>: 398 match asks: 399 case []: 400 [] 401 case +h <> t: 402 opens.step(open(world, h), h, opens(world, t)) 403 404 # whether an answer said yes 405 def yes.of(got: TW.Heard) -> Bool: 406 match got: 407 case TW.Open{}: 408 False{} 409 case TW.Heard{answer}: 410 match answer: 411 case TW.Is{yes}: 412 yes 413 case _: 414 False{} 415 416 # --------------------------------------------------------------------------- 417 # the checkout 418 419 # the checkout's ledger and lock, read where it is; the files of one cloned 420 # now; the questions still open; or why there is none 421 type Co is Data: 422 Kept{toml: Maybe<&2, String>, lock: Maybe<&2, String>} 423 Laid{files: List<&2, K.Source>} 424 CoWait{asks: List<&2, TW.Ask>, ups: List<&2, Up.Ask>} 425 CoNo{why: String} 426 427 # the files a checkout at a rev is made of, asked of git 428 def clone.ask(+world: TW.World, +url: String) -> Up.Ask: 429 Up.Clone{url, rev(world)} 430 431 # the checkout git gave, still to ask, or why there is none 432 def co.clone(got: Up.Heard, ask: Up.Ask) -> Co: 433 match got: 434 case Up.Open{}: 435 CoWait{[], [ask]} 436 case Up.Heard{answer}: 437 match answer: 438 case Up.Cloned{_nar, fs}: 439 Laid{fs} 440 case Up.Miss{why}: 441 CoNo{why} 442 case _: 443 CoNo{"ez: a checkout was not answered with one"} 444 445 # the three files of a cached checkout 446 def reads(+world: TW.World) -> List<&2, TW.Ask>: 447 [TW.Read{stamp.at(world)}, TW.Read{top(world) ++ "/ez.toml"}, 448 TW.Read{top(world) ++ "/ez.lock.toml"}] 449 450 # the cached checkout is this commit and finished, or it is cloned again 451 def co.fresh(fresh: Bool, +url: String, +world: TW.World) -> Co: 452 match fresh: 453 case True{}: 454 Kept{read.got(world, top(world) ++ "/ez.toml"), read.got(world, top(world) ++ "/ez.lock.toml")} 455 case False{}: 456 co.clone(Up.heard(TW.ups(world), clone.ask(world, url)), clone.ask(world, url)) 457 458 # whether the cached checkout's record names this commit. The record is 459 # written after every file, so a checkout it names is whole, with or without 460 # a ledger. 461 def fresh(+world: TW.World) -> Bool: 462 Key.checkout(String.trim(TW.text.or(read.got(world, stamp.at(world)))), rev(world)) 463 464 # a remote's cached checkout, once its three files are read 465 def co.remote(open: List<&2, TW.Ask>, +url: String, +world: TW.World) -> Co: 466 match open: 467 case []: 468 co.fresh(fresh(world), url, world) 469 case h <> t: 470 CoWait{h <> t, []} 471 472 # a path's ledger and lock, once they are read 473 def co.local(open: List<&2, TW.Ask>, +world: TW.World) -> Co: 474 match open: 475 case []: 476 Kept{read.got(world, top(world) ++ "/ez.toml"), read.got(world, top(world) ++ "/ez.lock.toml")} 477 case h <> t: 478 CoWait{h <> t, []} 479 480 # the checkout of a place 481 def co.of(pl: Tgt.Place, +world: TW.World) -> Co: 482 match pl: 483 case Tgt.Bad{why}: 484 CoNo{"ez: " ++ why} 485 case Tgt.Local{_p}: 486 co.local(opens(world, [TW.Read{top(world) ++ "/ez.toml"}, 487 TW.Read{top(world) ++ "/ez.lock.toml"}]), world) 488 case Tgt.Remote{url, _slug}: 489 co.remote(opens(world, reads(world)), url, world) 490 491 # the checkout of the target 492 def co(+world: TW.World) -> Co: 493 co.of(place(world), world) 494 495 # the checkout's ez.toml 496 def co.toml(checkout: Co) -> Maybe<&2, String>: 497 match checkout: 498 case Kept{t, _l}: 499 t 500 case Laid{fs}: 501 K.look(fs, "ez.toml") 502 case CoWait{_as, _us}: 503 None{} 504 case CoNo{_why}: 505 None{} 506 507 # the checkout's ez.lock.toml 508 def co.lock(checkout: Co) -> Maybe<&2, String>: 509 match checkout: 510 case Kept{_t, l}: 511 l 512 case Laid{fs}: 513 K.look(fs, "ez.lock.toml") 514 case CoWait{_as, _us}: 515 None{} 516 case CoNo{_why}: 517 None{} 518 519 # --------------------------------------------------------------------------- 520 # the checkout's ledger, and the binary 521 522 # the checkout, as a person knows it: a remote at its commit, or a path 523 def label.of(pl: Tgt.Place, +world: TW.World) -> String: 524 match pl: 525 case Tgt.Bad{why}: 526 why 527 case Tgt.Local{_p}: 528 top(world) 529 case Tgt.Remote{url, _slug}: 530 url ++ " at " ++ rev(world) 531 532 # the checkout, as a person knows it 533 def label(+world: TW.World) -> String: 534 label.of(place(world), world) 535 536 # the checkout's ledger as read. A checkout with no ez.toml is a plain Bend 537 # repository, whose ledger names nothing: no package, no entry, no deps. 538 def led.of(got: Maybe<&2, String>) -> M.Read: 539 match got: 540 case None{}: 541 M.Good{M.Manifest{"", "", "", "", "", "", [], []}} 542 case Some{text}: 543 M.parse(text) 544 545 # the checkout's ledger, read 546 def led(+world: TW.World) -> M.Read: 547 led.of(co.toml(co(world))) 548 549 # whether the checkout is a plain Bend repository: it has no ez.toml, so no 550 # git dependencies and no lock. Its program's hub imports are bend's to 551 # fetch, into a library of its own under the cache. 552 def plain(+world: TW.World) -> Bool: 553 none(co.toml(co(world))) 554 555 # where the checkout's lock is fetched, or, for a plain repository, where 556 # bend fetches its hub imports: a library of the target's own under the 557 # cache, beside its binaries 558 def lib(+world: TW.World) -> String: 559 Bool.pick(String, plain(world), dir(world) ++ "/lib", lib.of(place(world), world)) 560 561 # the model of a ledger that read, an empty one otherwise 562 def man.of(parsed: M.Read) -> M.Manifest: 563 match parsed: 564 case M.Bad{_why}: 565 M.Manifest{"", "", "", "", "", "", [], []} 566 case M.Good{m}: 567 m 568 569 # the checkout's ledger, empty when it did not read 570 def man(+world: TW.World) -> M.Manifest: 571 man.of(led(world)) 572 573 # the checkout's `[package] name`, `bin` and `entry` 574 def man.name(manifest: M.Manifest) -> String: 575 M.Manifest{n, _e, _b, _h, _pa, _pv, _ds, _ts} = manifest 576 n 577 578 # a ledger's `bin` 579 def man.bin(manifest: M.Manifest) -> String: 580 M.Manifest{_n, _e, b, _h, _pa, _pv, _ds, _ts} = manifest 581 b 582 583 # a ledger's `entry` 584 def man.entry(manifest: M.Manifest) -> String: 585 M.Manifest{_n, e, _b, _h, _pa, _pv, _ds, _ts} = manifest 586 e 587 588 # a file's last component without its `.bend`: `src/cli.bend` is `cli` 589 def stem(+file: String) -> String: 590 Path.unbend(Path.base(file)) 591 592 # a repository's name: the last component of its URL or path, a trailing 593 # slash and `.git` taken off, so `https://github.com/o/bolt.git` is `bolt` 594 def repo.name(+url: String) -> String: 595 Path.base(Tgt.ungit(Tgt.unslash(url))) 596 597 # the link's name for a plain repository: the stem of the file built, or the 598 # repository's name when that is `main`, since every plain repository has one 599 def plain.name(+file: String, +url: String) -> String: 600 Bool.pick(String, String.eq(stem(file), "main"), repo.name(url), stem(file)) 601 602 # where the checkout came from, as a person named it: a remote's URL, or a 603 # path's real path 604 def origin.of(pl: Tgt.Place, +world: TW.World) -> String: 605 match pl: 606 case Tgt.Bad{_why}: 607 "" 608 case Tgt.Local{_p}: 609 top(world) 610 case Tgt.Remote{url, _slug}: 611 url 612 613 # where the checkout came from 614 def origin(+world: TW.World) -> String: 615 origin.of(place(world), world) 616 617 # the checkout's `bin` 618 def bin(+world: TW.World) -> String: 619 man.bin(man(world)) 620 621 # the checkout's `entry` 622 def entry(+world: TW.World) -> String: 623 man.entry(man(world)) 624 625 # whether a package name can name the link and the binary: none at all, or a 626 # TOML bare key, so it holds no `/` and cannot be `..` 627 def name.ok(+name: String) -> Bool: 628 Bool.or(String.is_empty(name), Named.bare(name)) 629 630 # the file built: the pin's `bin`, then its `entry`, then the checkout's 631 # `bin`, then its `entry`, then `main.bend` (EZ-TOOL-7) 632 def chosen(+over: String, +binary: String, +entry: String) -> String: 633 Bool.pick(String, String.is_empty(over), Tgt.file.of(binary, entry), over) 634 635 # the file asked for ahead of the checkout's own: the pin's `bin` or 636 # `entry`, else the file `--entry` names, else "" so the checkout chooses. A 637 # pin is what the project decided, so a flag does not move it; over a 638 # checkout's ledger the flag wins, as cargo's `--bin` does. 639 def wish(+over: String, +want: String) -> String: 640 Bool.pick(String, String.is_empty(over), want, over) 641 642 # the file built, relative to the checkout: `main.bend` for a plain 643 # repository unless `--entry` names another (EZ-TOOL-7) 644 def choice(+world: TW.World) -> String: 645 chosen(wish(over(world), TW.want(world)), bin(world), entry(world)) 646 647 # the file built, in the checkout 648 def file(+world: TW.World) -> String: 649 Path.join(top(world), choice(world)) 650 651 # a path's part under a directory, "" when it is not under it 652 def inside(+dir: String, +path: String) -> String: 653 Bool.pick(String, String.starts_with(path, dir ++ "/"), 654 String.drop(path, Nat.add(String.length(dir), 1n)), "") 655 656 # whether the file built is in the checkout: among the files git gave, or 657 # read where the checkout is. A file outside the checkout is not in it. 658 def present.of(checkout: Co, +world: TW.World) -> Bool: 659 match checkout: 660 case Laid{fs}: 661 Bool.not(none(K.look(fs, inside(top(world), file(world))))) 662 case Kept{_t, _l}: 663 Bool.and(Bool.not(String.is_empty(inside(top(world), file(world)))), 664 Bool.not(none(read.got(world, file(world))))) 665 case CoWait{_as, _us}: 666 False{} 667 case CoNo{_why}: 668 False{} 669 670 # whether the file built is there 671 def present(+world: TW.World) -> Bool: 672 present.of(co(world), world) 673 674 # the question whether the file built is there, for a checkout read where it 675 # is; git gave every file of one cloned now 676 def file.asks.of(checkout: Co, +world: TW.World) -> List<&2, TW.Ask>: 677 match checkout: 678 case Kept{_t, _l}: 679 opens(world, [TW.Read{file(world)}]) 680 case Laid{_fs}: 681 [] 682 case CoWait{_as, _us}: 683 [] 684 case CoNo{_why}: 685 [] 686 687 def file.asks(+world: TW.World) -> List<&2, TW.Ask>: 688 file.asks.of(co(world), world) 689 690 # the name the link and the binary are given: for a plain repository the 691 # one `plain.name` gives it, and the package's for an ez project 692 def name.of(bare: Bool, +world: TW.World) -> String: 693 match bare: 694 case True{}: 695 plain.name(choice(world), origin(world)) 696 case False{}: 697 man.name(man(world)) 698 699 # the name the link and the binary are given 700 def name.raw(+world: TW.World) -> String: 701 name.of(plain(world), world) 702 703 # the link's name: the package's, or `app` (EZ-TOOL-7) 704 def name(+world: TW.World) -> String: 705 Tgt.out.name(name.raw(world)) 706 707 # what the binary is made from: the commit, the file and the bend (EZ-TOOL-2) 708 def key(+world: TW.World) -> Key.Stamp: 709 Key.Stamp{rev(world), file(world), TW.bend(world)} 710 711 # the directory a record's binary lives in, named by the record 712 def stamp.of(key: Key.Stamp) -> String: 713 String.take(Sha.hex(Key.show(key)), 16n) 714 715 # the directory of this record's binary 716 def bdir(+world: TW.World) -> String: 717 dir(world) ++ "/bin/" ++ stamp.of(key(world)) 718 719 # the binary 720 def out(+world: TW.World) -> String: 721 bdir(world) ++ "/" ++ name(world) ++ ".out" 722 723 def key.at(+world: TW.World) -> String: 724 bdir(world) ++ "/key" 725 726 # the binary's record as it was written, none when there is none 727 def was(+world: TW.World) -> Key.Stamp: 728 Key.read(String.trim(TW.text.or(read.got(world, key.at(world))))) 729 730 # whether the binary is there 731 def there(+world: TW.World) -> Bool: 732 yes.of(TW.heard(world, TW.Exec{out(world)})) 733 734 # the binary is reused when it is there and its record is this one 735 def reuse(+world: TW.World) -> Bool: 736 Bool.and(there(world), Key.reuse(was(world), key(world))) 737 738 # --------------------------------------------------------------------------- 739 # the plan 740 741 # the checkout laid under the cache, its record last, or nothing laid 742 type Lay is Data: 743 Lay{dir: String, writes: List<&2, P.Effect>} 744 Still{} 745 746 # the binary: the lock of the checkout at `top` fetched into the library 747 # when `fetch` says it has one, the file built into `out` with that library 748 # as BEND_LIB, and the record written at `at`; or the one there reused; or 749 # nothing, for a command that refuses 750 type Make is Data: 751 Make{top: String, lib: String, fetch: Bool, file: String, out: String, at: String, 752 key: String, name: String} 753 Reuse{name: String} 754 Skip{} 755 756 # what is done with the binary: started with these words, as the last thing; 757 # linked into `dir` under `name`, with the lines a person is told; or nothing 758 type Finish is Data: 759 Launch{bin: String, args: List<&2, String>} 760 Link{bin: String, dir: String, name: String, says: List<&2, String>} 761 Idle{} 762 763 # everything a tool command does, in order, and how it ends 764 type Plan is Data: 765 Plan{lay: Lay, make: Make, finish: Finish, outcome: P.Outcome} 766 767 # every file of a checkout written under its top 768 def writes.of(+top: String, fs: List<&2, K.Source>) -> List<&2, P.Effect>: 769 match fs: 770 case []: 771 [] 772 case K.Source{at, text} <> t: 773 P.Write{top ++ "/" ++ at, text} <> writes.of(top, t) 774 775 # the checkout laid, when git gave it now 776 def lay.of(checkout: Co, +world: TW.World) -> Lay: 777 match checkout: 778 case Laid{fs}: 779 Lay{revdir(world), List.append(&2, P.Effect, writes.of(top(world), fs), 780 [P.Write{stamp.at(world), rev(world) ++ "\n"}])} 781 case Kept{_t, _l}: 782 Still{} 783 case CoWait{_as, _us}: 784 Still{} 785 case CoNo{_why}: 786 Still{} 787 788 # the checkout laid under the cache, when it was cloned now 789 def lay(+world: TW.World) -> Lay: 790 lay.of(co(world), world) 791 792 # the binary reused, or built with its record 793 def make.of(reused: Bool, +world: TW.World) -> Make: 794 match reused: 795 case True{}: 796 Reuse{name(world)} 797 case False{}: 798 Make{top(world), lib(world), Bool.not(plain(world)), file(world), out(world), 799 key.at(world), Key.show(key(world)), name(world)} 800 801 # the binary built, or reused 802 def make(+world: TW.World) -> Make: 803 make.of(reuse(world), world) 804 805 # one step of a scan that may stop at the first hit 806 def seen.step(here: Bool, rest: Unit -> Bool) -> Bool: 807 match here: 808 case True{}: 809 True{} 810 case False{}: 811 rest(Unit{}) 812 813 # a directory with no trailing slash 814 def bare(+dir: String) -> String: 815 Bool.pick(String, String.ends_with(dir, "/"), 816 String.take(dir, Nat.sub(String.length(dir), 1n)), dir) 817 818 # whether this directory is one of the entries 819 def seen(+dir: String, ps: List<&2, String>) -> Bool: 820 match ps: 821 case []: 822 False{} 823 case +h <> t: 824 seen.step(String.eq(bare(h), bare(dir)), _u => seen(dir, t)) 825 826 # whether a directory is an entry of `PATH` 827 def on.path(+dir: String, +path: String) -> Bool: 828 seen(dir, String.split(path, ':')) 829 830 # the link directory, "" for none 831 def bin.at(+world: TW.World) -> String: 832 TW.text.or(TW.bin.of(TW.env(world))) 833 834 # what a person is told once the link is written: that the directory is not 835 # on PATH, when it is not, and what was linked where 836 def says(install: Bool, +world: TW.World) -> List<&2, String>: 837 +called = name(world) 838 +at = bin.at(world) ++ "/" ++ called 839 +done = Bool.pick(String, install, Say.installed(called, at), Say.linked(called, at)) 840 Bool.pick(List<&2, String>, on.path(bin.at(world), TW.env.path(TW.env(world))), [done], 841 ["warning: " ++ bin.at(world) ++ " is not on PATH", done]) 842 843 # the program started, or the link written 844 def finish.of(act: TW.Act, +world: TW.World) -> Finish: 845 match act: 846 case TW.Run{args}: 847 Launch{out(world), args} 848 case TW.Install{}: 849 Link{out(world), bin.at(world), name(world), says(True{}, world)} 850 case TW.Upgrade{}: 851 Link{out(world), bin.at(world), name(world), says(False{}, world)} 852 853 # what is done with the binary 854 def finish(+world: TW.World) -> Finish: 855 finish.of(TW.act(world), world) 856 857 # the plan once every answer is in and nothing refused 858 def made(+world: TW.World) -> Plan: 859 Plan{lay(world), make(world), finish(world), P.Success{}} 860 861 # --------------------------------------------------------------------------- 862 # where a World stands 863 864 # every answer is in, a question is still open, or the command refuses 865 type Verdict is Data: 866 Go{} 867 Pending{asks: List<&2, TW.Ask>, ups: List<&2, Up.Ask>} 868 Stop{why: String} 869 870 # a lock is needed only for a binary that is built, and only by an ez 871 # project: a plain repository has none to have 872 def decide.lock(missing: Bool, +world: TW.World) -> Verdict: 873 match missing: 874 case True{}: 875 Stop{"ez: " ++ label(world) ++ " has an ez.toml but no ez.lock.toml; its author " ++ 876 "runs ez lock to write one"} 877 case False{}: 878 Go{} 879 880 # whether the binary is to be built from an ez project with no lock 881 def unlocked(+world: TW.World) -> Bool: 882 Bool.and(Bool.not(reuse(world)), 883 Bool.and(Bool.not(plain(world)), none(co.lock(co(world))))) 884 885 # the binary's record and whether it is there, asked together 886 def cache.asks(+world: TW.World) -> List<&2, TW.Ask>: 887 opens(world, [TW.Read{key.at(world)}, TW.Exec{out(world)}]) 888 889 # the binary's record and whether it is there, still to ask 890 def decide.cache(open: List<&2, TW.Ask>, +world: TW.World) -> Verdict: 891 match open: 892 case []: 893 decide.lock(unlocked(world), world) 894 case h <> t: 895 Pending{h <> t, []} 896 897 # why a missing file is refused: a file a pin names is the project's to fix, 898 # and any other can be named with `--entry` 899 def missing.why(+world: TW.World) -> String: 900 +why = "ez: " ++ label(world) ++ " has no " ++ choice(world) 901 Bool.pick(String, String.is_empty(over(world)), 902 why ++ "; name the file to build with --entry ", why) 903 904 # a file to build that is not in the checkout is refused 905 def decide.present(ok: Bool, +world: TW.World) -> Verdict: 906 match ok: 907 case True{}: 908 decide.cache(cache.asks(world), world) 909 case False{}: 910 Stop{missing.why(world)} 911 912 # whether the file to build is there, still to ask 913 def decide.file(open: List<&2, TW.Ask>, +world: TW.World) -> Verdict: 914 match open: 915 case []: 916 decide.present(present(world), world) 917 case h <> t: 918 Pending{h <> t, []} 919 920 # what a name that could not name a file is: a package's, or the one a 921 # plain repository would be linked as 922 def name.what(+world: TW.World) -> String: 923 Bool.pick(String, plain(world), " would be linked as ", " names its package ") 924 925 # a name that could not name a file is refused 926 def decide.name(ok: Bool, +world: TW.World) -> Verdict: 927 match ok: 928 case True{}: 929 decide.file(file.asks(world), world) 930 case False{}: 931 Stop{"ez: " ++ label(world) ++ name.what(world) ++ name.raw(world) ++ 932 ", which is not a TOML bare key"} 933 934 # a checkout ledger that does not parse is refused 935 def decide.led(parsed: M.Read, +world: TW.World) -> Verdict: 936 match parsed: 937 case M.Bad{why}: 938 Stop{"ez: error: " ++ why} 939 case M.Good{_m}: 940 decide.name(name.ok(name.raw(world)), world) 941 942 # the checkout answered, still to ask, or not to be had 943 def decide.co(checkout: Co, +world: TW.World) -> Verdict: 944 match checkout: 945 case Kept{_t, _l}: 946 decide.led(led(world), world) 947 case Laid{_fs}: 948 decide.led(led(world), world) 949 case CoWait{as, us}: 950 Pending{as, us} 951 case CoNo{why}: 952 Stop{why} 953 954 # what git says of a path, still to ask, or not to be had 955 def decide.loc(spot: Loc, +world: TW.World) -> Verdict: 956 match spot: 957 case Loc{_a, _t, _h, _ok, _s}: 958 decide.co(co(world), world) 959 case LocWait{ask}: 960 Pending{[ask], []} 961 case LocNo{why}: 962 Stop{why} 963 964 # a remote's HEAD has to name a commit 965 def decide.tip(ok: Bool, +url: String, +world: TW.World) -> Verdict: 966 match ok: 967 case True{}: 968 decide.co(co(world), world) 969 case False{}: 970 Stop{"ez: " ++ url ++ " has no HEAD"} 971 972 def decide.said(said: Up.Said, +url: String, +world: TW.World) -> Verdict: 973 match said: 974 case Up.Said{_text}: 975 decide.tip(Git.is_rev(rev(world)), url, world) 976 case Up.Unsaid{why}: 977 Stop{why} 978 case Up.Unasked{ask}: 979 Pending{[], [ask]} 980 981 # a pin's commit is the lock's; a free remote's is asked of its HEAD 982 def decide.head(found: Src, +url: String, +world: TW.World) -> Verdict: 983 match found: 984 case Pin{_n, _u, _r, _o}: 985 decide.co(co(world), world) 986 case Free{_pl}: 987 decide.said(head.said(world, url), url, world) 988 case NoSrc{why}: 989 Stop{why} 990 991 # a slug that could leave the cache is refused (EZ-TOOL-8) 992 def decide.slug(ok: Bool, +url: String, +world: TW.World) -> Verdict: 993 match ok: 994 case True{}: 995 decide.head(src(world), url, world) 996 case False{}: 997 Stop{"ez: not a repository"} 998 999 def decide.place(pl: Tgt.Place, +world: TW.World) -> Verdict: 1000 match pl: 1001 case Tgt.Bad{why}: 1002 Stop{"ez: " ++ why} 1003 case Tgt.Local{p}: 1004 decide.loc(loc(p, world), world) 1005 case Tgt.Remote{url, slug}: 1006 decide.slug(Tgt.slug.ok(slug), url, world) 1007 1008 # a pin at no commit cannot be built; its rev is a directory name, so it has 1009 # to be one 1010 def decide.pin(ok: Bool, +name: String, +rev: String, +world: TW.World) -> Verdict: 1011 match ok: 1012 case True{}: 1013 decide.place(place(world), world) 1014 case False{}: 1015 Stop{"ez: tools." ++ name ++ " in ez.lock.toml has no commit" ++ 1016 Bool.pick(String, String.is_empty(rev), "", ": " ++ rev)} 1017 1018 # what the target names, or why it names nothing 1019 def decide.src(found: Src, +world: TW.World) -> Verdict: 1020 match found: 1021 case Pin{+n, _u, +r, _o}: 1022 decide.pin(Git.is_rev(r), n, r, world) 1023 case Free{_pl}: 1024 decide.place(place(world), world) 1025 case NoSrc{why}: 1026 Stop{why} 1027 1028 # no cache root is refused 1029 def decide.root(got: Maybe<&2, String>, +world: TW.World) -> Verdict: 1030 match got: 1031 case None{}: 1032 Stop{"ez: HOME is unset, so there is no cache"} 1033 case Some{_r}: 1034 decide.src(src(world), world) 1035 1036 # nowhere to link is refused before anything is built 1037 def decide.bin(nowhere: Bool, +world: TW.World) -> Verdict: 1038 match nowhere: 1039 case True{}: 1040 Stop{"ez: HOME is unset, so there is no bin directory"} 1041 case False{}: 1042 decide.root(TW.cache.of(world), world) 1043 1044 # where a World stands 1045 def decide(+world: TW.World) -> Verdict: 1046 decide.bin(Bool.and(links.act(TW.act(world)), none(TW.bin.of(TW.env(world)))), world) 1047 1048 # the plan for where the World stands. One that still asks is a World the 1049 # interpreter did not finish, and is refused rather than guessed at. 1050 def plan.v(verdict: Verdict, +world: TW.World) -> Plan: 1051 match verdict: 1052 case Go{}: 1053 made(world) 1054 case Pending{_as, _us}: 1055 Plan{Still{}, Skip{}, Idle{}, P.Refused{"ez: " ++ TW.target(world) ++ " was never resolved"}} 1056 case Stop{why}: 1057 Plan{Still{}, Skip{}, Idle{}, P.Refused{why}} 1058 1059 # `ez tool run`, `install` or `upgrade` over a World 1060 def plan(+world: TW.World) -> Plan: 1061 plan.v(decide(world), world) 1062 1063 # whether a plan ends in a refusal 1064 def refuses.plan(pl: Plan) -> Bool: 1065 Plan{_l, _m, _f, outcome} = pl 1066 match outcome: 1067 case P.Success{}: 1068 False{} 1069 case P.Refused{_why}: 1070 True{} 1071 1072 # whether a tool command refuses on a World 1073 def refuses(+world: TW.World) -> Bool: 1074 refuses.plan(plan(world)) 1075 1076 # the questions still open, or the plan once there are none 1077 type Step is Data: 1078 Asking{asks: List<&2, TW.Ask>, ups: List<&2, Up.Ask>} 1079 Run{plan: Plan} 1080 1081 # the step for where the World stands 1082 def step.v(verdict: Verdict, +world: TW.World) -> Step: 1083 match verdict: 1084 case Go{}: 1085 Run{made(world)} 1086 case Pending{as, us}: 1087 Asking{as, us} 1088 case Stop{why}: 1089 Run{plan.v(Stop{why}, world)} 1090 1091 # what the interpreter does next on a World 1092 def step(+world: TW.World) -> Step: 1093 step.v(decide(world), world) 1094 1095 # --------------------------------------------------------------------------- 1096 # `ez tool sync` 1097 1098 # the pins a sync installs, in the order the ledger names them, or why it 1099 # installs none 1100 type Sync is Data: 1101 Sync{names: List<&2, String>} 1102 SyncNo{why: String} 1103 1104 # a pin the lock lacks, or has at no commit, stops the scan; any other goes 1105 # on to the rest 1106 def sync.step(bad: Bool, +name: String, rest: Unit -> String) -> String: 1107 match bad: 1108 case True{}: 1109 "ez: tools." ++ name ++ " is not pinned at a commit in ez.lock.toml" 1110 case False{}: 1111 rest(Unit{}) 1112 1113 # whether the lock pins a tool at a commit 1114 def sync.pinned(+tool: M.Tool) -> Bool: 1115 Bool.and(Bool.not(String.is_empty(M.tool.name(tool))), 1116 Git.is_rev(M.source.rev(M.tool.source(tool)))) 1117 1118 # every tool the ledger names has to be pinned at a commit in the lock; why 1119 # the first that is not is refused, "" when every one is 1120 def sync.check(ts: List<&2, M.Tool>, +body: String) -> String: 1121 match ts: 1122 case []: 1123 "" 1124 case +h <> t: 1125 sync.step(Bool.not(sync.pinned(pin.in(body, M.tool.name(h)))), M.tool.name(h), 1126 _u => sync.check(t, body)) 1127 1128 # the names of the tools a ledger pins 1129 def names.of(ts: List<&2, M.Tool>) -> List<&2, String>: 1130 match ts: 1131 case []: 1132 [] 1133 case h <> t: 1134 M.tool.name(h) <> names.of(t) 1135 1136 # every name installed, or the refusal the scan found 1137 def sync.why(ok: Bool, +why: String, ts: List<&2, M.Tool>) -> Sync: 1138 match ok: 1139 case True{}: 1140 Sync{names.of(ts)} 1141 case False{}: 1142 SyncNo{why} 1143 1144 # a ledger with pins needs a lock that pins each at a commit 1145 def sync.lock(got: Maybe<&2, String>, +ts: List<&2, M.Tool>) -> Sync: 1146 match got: 1147 case None{}: 1148 Bool.pick(Sync, List.is_empty(&2, M.Tool, ts), Sync{[]}, 1149 SyncNo{"ez: ez.lock.toml is not there"}) 1150 case Some{body}: 1151 +why = sync.check(ts, body) 1152 sync.why(String.is_empty(why), why, ts) 1153 1154 # a project ledger that does not parse is refused 1155 def sync.read(led: M.Read, got: Maybe<&2, String>) -> Sync: 1156 match led: 1157 case M.Bad{why}: 1158 SyncNo{"ez: error: " ++ why} 1159 case M.Good{m}: 1160 sync.lock(got, tools.of(m)) 1161 1162 # `ez tool sync` over the project's ledger and lock. With no ledger there is 1163 # no project to sync, and it is refused, as `ez lock` and `ez fetch` refuse 1164 # one. 1165 def sync(ledger: Maybe<&2, String>, lock: Maybe<&2, String>) -> Sync: 1166 match ledger: 1167 case None{}: 1168 SyncNo{"ez: no ez.toml here; ez tool sync installs the tools a project's " ++ 1169 "ledger pins, and ez init makes one"} 1170 case Some{text}: 1171 sync.read(M.parse(text), lock) 1172 1173 # how a sync ends once every install it runs has ended: 1 with the reason 1174 # when it refused, and 0 when it did not (EZ-OUT-1). An install that refuses 1175 # ends the sync there, with its own status. 1176 def sync.outcome(synced: Sync) -> P.Outcome: 1177 match synced: 1178 case Sync{_ns}: 1179 P.Success{} 1180 case SyncNo{why}: 1181 P.Refused{why} 1182 1183 # whether a sync refuses 1184 def sync.refuses(synced: Sync) -> Bool: 1185 match synced: 1186 case Sync{_ns}: 1187 False{} 1188 case SyncNo{_why}: 1189 True{} 1190 1191 # the names a sync installs, none when it refuses 1192 def sync.names(synced: Sync) -> List<&2, String>: 1193 match synced: 1194 case Sync{ns}: 1195 ns 1196 case SyncNo{_why}: 1197 [] 1198 1199 # --------------------------------------------------------------------------- 1200 # law vocabulary 1201 1202 # whether a checkout is laid 1203 def lays(layout: Lay) -> Bool: 1204 match layout: 1205 case Lay{_d, _ws}: 1206 True{} 1207 case Still{}: 1208 False{} 1209 1210 # whether a binary is built 1211 def makes(making: Make) -> Bool: 1212 match making: 1213 case Make{_lk, _lb, _fe, _f, _o, _at, _k, _n}: 1214 True{} 1215 case Reuse{_n}: 1216 False{} 1217 case Skip{}: 1218 False{} 1219 1220 # whether a link is written 1221 def linked(fin: Finish) -> Bool: 1222 match fin: 1223 case Launch{_b, _as}: 1224 False{} 1225 case Link{_b, _d, _n, _s}: 1226 True{} 1227 case Idle{}: 1228 False{} 1229 1230 # whether a plan writes anything: a checkout laid, a binary built, a link 1231 def writes(pl: Plan) -> Bool: 1232 Plan{l, m, f, _o} = pl 1233 Bool.or(lays(l), Bool.or(makes(m), linked(f))) 1234 1235 # whether a plan builds the binary 1236 def builds(pl: Plan) -> Bool: 1237 Plan{_l, m, _f, _o} = pl 1238 makes(m) 1239 1240 # the file a plan builds, "" when it builds none 1241 def built(pl: Plan) -> String: 1242 Plan{_l, m, _f, _o} = pl 1243 match m: 1244 case Make{_lk, _lb, _fe, f, _o, _at, _k, _n}: 1245 f 1246 case Reuse{_n}: 1247 "" 1248 case Skip{}: 1249 "" 1250 1251 # whether a plan fetches a lock before it builds 1252 def fetches(pl: Plan) -> Bool: 1253 Plan{_l, m, _f, _o} = pl 1254 match m: 1255 case Make{_lk, _lb, fe, _f, _o, _at, _k, _n}: 1256 fe 1257 case Reuse{_n}: 1258 False{} 1259 case Skip{}: 1260 False{} 1261 1262 # the library a plan builds with, "" when it builds nothing 1263 def built.lib(pl: Plan) -> String: 1264 Plan{_l, m, _f, _o} = pl 1265 match m: 1266 case Make{_lk, lb, _fe, _f, _o, _at, _k, _n}: 1267 lb 1268 case Reuse{_n}: 1269 "" 1270 case Skip{}: 1271 "" 1272 1273 # whether a plan starts the program 1274 def launches(pl: Plan) -> Bool: 1275 Plan{_l, _m, f, _o} = pl 1276 match f: 1277 case Launch{_b, _as}: 1278 True{} 1279 case Link{_b, _d, _n, _s}: 1280 False{} 1281 case Idle{}: 1282 False{} 1283 1284 # the path a plan links, "" when it links none 1285 def link.at(pl: Plan) -> String: 1286 Plan{_l, _m, f, _o} = pl 1287 match f: 1288 case Launch{_b, _as}: 1289 "" 1290 case Link{_b, d, n, _s}: 1291 d ++ "/" ++ n 1292 case Idle{}: 1293 "" 1294 1295 # how a command ends: exit 1, having started nothing; exit 0; or with the 1296 # status of this program, started with these words as the last thing 1297 type Exit is Data: 1298 Refusal{} 1299 Finished{} 1300 Exits{bin: String, args: List<&2, String>} 1301 1302 # how a plan that goes on ends 1303 def exit.of(fin: Finish) -> Exit: 1304 match fin: 1305 case Launch{b, as}: 1306 Exits{b, as} 1307 case Link{_b, _d, _n, _s}: 1308 Finished{} 1309 case Idle{}: 1310 Finished{} 1311 1312 # how a plan ends 1313 def exit(pl: Plan) -> Exit: 1314 Plan{_l, _m, f, o} = pl 1315 match o: 1316 case P.Success{}: 1317 exit.of(f) 1318 case P.Refused{_why}: 1319 Refusal{} 1320 1321 # the status a tool command exits with, given the status the program it 1322 # started exited with (EZ-OUT-1): 1 for a refusal, 0 for a command that 1323 # ends without starting one, and the program's own status for `ez tool run`. 1324 # A command that starts nothing is handed 0 for it. 1325 def code(ended: Exit, program: U32) -> U32: 1326 match ended: 1327 case Refusal{}: 1328 1 1329 case Finished{}: 1330 0 1331 case Exits{_b, _as}: 1332 program