diff --git a/ez.lock.toml b/ez.lock.toml index 355ff53..ff1445d 100644 --- a/ez.lock.toml +++ b/ez.lock.toml @@ -109,8 +109,8 @@ LICENSE = "309f5aae946e4db157750e002fb179a74ed0fe27fce09a7be68a6977b14f205f" [tools] [tools.bolt] git = "https://github.com/Emerging-Patterns/bolt" -rev = "91efe4bbaad91953652794ad55be7b49c10a2eff" -tag = "v1.11.0" +rev = "d08744eccf71fda5cc2132fd9fbd733a08b83cb1" +tag = "v1.12.0" root = "." -narHash = "sha256-0xjoxoByI8PKBrbFy4eYGbKQ3ZVscknZLKkWQroYAUI=" +narHash = "sha256-WxS7HW29rbO5JKFCdEM7uIdZ1Dn43zj8/3N1h/xMWGo=" entry = "main.bend" diff --git a/ez.toml b/ez.toml index 609cba4..fc54c18 100644 --- a/ez.toml +++ b/ez.toml @@ -44,8 +44,8 @@ entry = "main.bend" [tools] [tools.bolt] git = "https://github.com/Emerging-Patterns/bolt" -rev = "91efe4bbaad91953652794ad55be7b49c10a2eff" -tag = "v1.11.0" +rev = "d08744eccf71fda5cc2132fd9fbd733a08b83cb1" +tag = "v1.12.0" root = "." -narHash = "sha256-0xjoxoByI8PKBrbFy4eYGbKQ3ZVscknZLKkWQroYAUI=" +narHash = "sha256-WxS7HW29rbO5JKFCdEM7uIdZ1Dn43zj8/3N1h/xMWGo=" entry = "main.bend" diff --git a/src/add/run.bend b/src/add/run.bend index 3bb24bc..5446f03 100644 --- a/src/add/run.bend +++ b/src/add/run.bend @@ -43,6 +43,12 @@ def loop.step(st: AP.Next, +lib: String, world: A.World, go: A.World -> IO(Unit) case AP.Ready{pl}: Run.exec.plan(lib, pl) +# the rounds the loop may take. The planner asks at most three rounds for a +# git target and two for a name, so this is only a guard against a planner +# that keeps asking, and running out of it dies loudly. +def rounds() -> Nat: + U32.to_nat(100) + # the loop, under fuel. Every round adds an answer, since the planner never # asks what the World answers already, and it asks at most three rounds for # a git target and two for a name, so the fuel is only a guard. @@ -61,5 +67,5 @@ def run(+target: String, +ref: String, +entry: String, +rename: String) -> IO(Un h : Result<&1, &1, U32 & String, String> <- IO.get_env("HOME") led : Maybe<&2, String> <- F.read(A.toml()) ig : Maybe<&2, String> <- F.read(A.ignore.file()) - loop(U32.to_nat(100), lib, A.World{A.Args{target, ref, entry, rename}, String.trim(R.text(pwd)), + loop(rounds(), lib, A.World{A.Args{target, ref, entry, rename}, String.trim(R.text(pwd)), Run.env.text(h), led, F.text_of(ig), [], []}) diff --git a/src/check/framing.bend b/src/check/framing.bend index 00e605c..b5934e3 100644 --- a/src/check/framing.bend +++ b/src/check/framing.bend @@ -156,6 +156,12 @@ def loop.on(state: Run, rest: Listener -> IO(Unit)) -> IO(Unit): case RGo{l}: rest(l) +# a thousand: the runs the server takes, and the connections each run serves. +# Bend has no unbounded loop, so "forever" is a count no test run will reach, a +# thousand runs of a thousand connections; a server has no input to size it by. +def thousand() -> Nat: + U32.to_nat(1000) + # connections served for as long as the fuel lasts. Bend has no unbounded loop, # so "forever" is a count no test run will reach: a thousand runs of a thousand # connections. The count is nested rather than written out because a Nat is @@ -167,7 +173,7 @@ def loop(fuel: Nat, listener: Listener) -> IO(Unit): Listener.close(listener) case 1n+f: do IO: - r : Run <- turn(U32.to_nat(1000), listener) + r : Run <- turn(thousand(), listener) loop.on(r, lis => loop(f, lis)) # a port that would not open, reported rather than served on @@ -183,7 +189,7 @@ def begin.at(bound: Result<&1, &1, U32 & String, Listener>, port: U32) -> IO(Uni case Done{l}: do IO: IO.print("framing: on port " ++ U32.show(port)) - loop(U32.to_nat(1000), l) + loop(thousand(), l) # the server, on a port def begin(+port: U32) -> IO(Unit): diff --git a/src/check/oracle.bend b/src/check/oracle.bend index b9bfa40..7004938 100644 --- a/src/check/oracle.bend +++ b/src/check/oracle.bend @@ -677,6 +677,12 @@ def loop.on(state: Run, dir: String, rest: String -> Listener -> IO(Unit)) -> IO case RGo{l}: rest(dir, l) +# a thousand: the runs the server takes, and the connections each run serves. +# Bend has no unbounded loop, so "forever" is a count no test run will reach, a +# thousand runs of a thousand connections; a server has no input to size it by. +def thousand() -> Nat: + U32.to_nat(1000) + # connections served for as long as the fuel lasts. Bend has no unbounded loop, # so "forever" is a count no test run will reach: a thousand runs of a thousand # connections. The count is nested rather than written out because a Nat is @@ -688,7 +694,7 @@ def loop(fuel: Nat, +dir: String, listener: Listener) -> IO(Unit): Listener.close(listener) case 1n+f: do IO: - r : Run <- turn(U32.to_nat(1000), dir, listener) + r : Run <- turn(thousand(), dir, listener) loop.on(r, dir, d => lis => loop(f, d, lis)) # a port that would not open, reported rather than served on @@ -706,7 +712,7 @@ def begin.at(bound: Result<&1, &1, U32 & String, Listener>, +dir: String, port: case Done{l}: do IO: IO.print("oracle: " ++ dir ++ " on port " ++ U32.show(port)) - loop(U32.to_nat(1000), dir, l) + loop(thousand(), dir, l) # the oracle, on a directory and a port def begin(dir: String, +port: U32) -> IO(Unit): diff --git a/src/check/serve.bend b/src/check/serve.bend index 7fecc71..0a110bf 100644 --- a/src/check/serve.bend +++ b/src/check/serve.bend @@ -246,6 +246,12 @@ def recv.go(fuel: Nat, sock: Socket, +acc: String) -> IO(Socket & String): case 1n+f: recv.more(complete(acc), sock, acc, s => a => recv.go(f, s, a)) +# the reads a request may take, 64 KiB at most each: 256 MiB, past any request +# a test sends. How long a request is is not known until its head has arrived, +# so the reads cannot be counted up front. +def reads() -> Nat: + U32.to_nat(4096) + # a whole request read off a socket, and the socket it came on def request(sock: Socket) -> IO(Socket & String): - recv.go(U32.to_nat(4096), sock, "") + recv.go(reads(), sock, "") diff --git a/src/doctor/run.bend b/src/doctor/run.bend index e0679a2..e1f2031 100644 --- a/src/doctor/run.bend +++ b/src/doctor/run.bend @@ -104,6 +104,13 @@ def loop.step(st: DP.Step, +lib: String, world: DW.World, go: DW.World -> IO(Uni case DP.Run{pl}: Run.exec.plan("", pl) +# the rounds the loop may take. The lock check's trees follow the sources +# and each other, and the packages they reach are not known until BEND_LIB +# has answered, so this is a guard against a planner that keeps asking, not +# a limit on a project: running out of it dies loudly. +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, and the lock check asks # about each package at most once. @@ -127,4 +134,4 @@ def run() -> IO(Unit): +lib : String <- var("BEND_LIB") led : Maybe<&2, String> <- F.read(DW.toml()) lk : Maybe<&2, String> <- F.read(DW.lockfile()) - loop(U32.to_nat(100000), DP.lib.at(lib), DW.World{cc, lib, led, lk, []}) + loop(rounds(), DP.lib.at(lib), DW.World{cc, lib, led, lk, []}) diff --git a/src/fetch/run.bend b/src/fetch/run.bend index 4ddde3e..41fae44 100644 --- a/src/fetch/run.bend +++ b/src/fetch/run.bend @@ -86,6 +86,13 @@ def loop.step(st: FP.Step, +lib: String, world: FW.World, go: FW.World -> IO(Uni case FP.Run{pl}: Run.exec.plan(lib, pl) +# the rounds the loop may take. A package asks at most twice, for its tree +# under BEND_LIB and for its fetch, and a round asks for every package at +# once, so this is only a guard against a planner that keeps asking, and +# running out of it dies loudly. +def rounds() -> Nat: + U32.to_nat(10) + # the loop, under fuel. A package asks at most twice, for its tree under # BEND_LIB and for its fetch, and every round answers what it asked, so the # fuel is only a guard. @@ -107,7 +114,7 @@ def fill(+here: String, +lib: String) -> IO(Unit): do IO: led : Maybe<&2, String> <- F.read(at(here, FW.toml())) lk : Maybe<&2, String> <- F.read(at(here, FW.lockfile())) - loop(U32.to_nat(10), lib, FW.World{here, Maybe.is_some(&2, String, led), lk, []}) + loop(rounds(), lib, FW.World{here, Maybe.is_some(&2, String, led), lk, []}) # `ez fetch`: BEND_LIB filled from the lock of the project ez runs in def run() -> IO(Unit): diff --git a/src/io/file.bend b/src/io/file.bend index 7f1fa72..be87689 100644 --- a/src/io/file.bend +++ b/src/io/file.bend @@ -48,6 +48,38 @@ def slurp(fuel: Nat, file: File, acc: String) -> IO(String): got : File & Result<&1, &1, U32 & String, String> <- File.read(file, 65536) slurp.next(got, acc, fl => a => slurp(f, fl, a)) +# the reads a file with no size to go by may take: a pipe, a device or a +# /proc file reports 0 bytes and a file past 4 GiB fails to report, so these +# are read until they end, up to 100000 reads of 64 KiB, past any file ez reads +def unsized() -> Nat: + U32.to_nat(100000) + +# a size of 0 is no size to go by; any other is read to +def reads.given(none: Bool, bytes: U32) -> Nat: + match none: + case True{}: + unsized() + case False{}: + U32.to_nat(U32.add(U32.div(bytes, 65536), 2)) + +# the reads a file of this many bytes takes: one for every 64 KiB, one for +# the part past the last whole 64 KiB, and one to spare for a short read +def reads(+bytes: U32) -> Nat: + reads.given(U32.is_zero(bytes), bytes) + +# the reads the size the host reported allows +def reads.of(res: Result<&1, &1, U32 & String, U32>) -> Nat: + match res: + case Fail{_e}: + unsized() + case Done{bytes}: + reads(bytes) + +# a file read whole, with fuel enough for the size it reported +def read.sized(got: File & Result<&1, &1, U32 & String, U32>) -> IO(String): + (file, r) = got + slurp(reads.of(r), file, "") + # what an opened file holds, or nothing when it would not open def read.opened(res: Result<&1, &1, U32 & String, File>) -> IO(Maybe<&2, String>): match res: @@ -55,7 +87,8 @@ def read.opened(res: Result<&1, &1, U32 & String, File>) -> IO(Maybe<&2, String> IO.pure(Maybe<&2, String>, None{}) case Done{file}: do IO>: - text : String <- slurp(U32.to_nat(100000), file, "") + got : File & Result<&1, &1, U32 & String, U32> <- File.size(file) + text : String <- read.sized(got) return Some{text} # a file's text, or None when it cannot be opened diff --git a/src/lock/run.bend b/src/lock/run.bend index 6732038..7c24602 100644 --- a/src/lock/run.bend +++ b/src/lock/run.bend @@ -676,6 +676,15 @@ def loop.step(st: Plan.Step, +lib: String, world: W.World, go: W.World -> IO(Uni 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): @@ -695,5 +704,5 @@ def run(up: Bool, +only: String) -> IO(Unit): +pwd : String <- R.exec(["pwd"]) led : Maybe<&2, String> <- read.ledger() ls : W.Listing <- read.listing() - loop(U32.to_nat(100000), lib, + loop(rounds(), lib, W.World{W.Args{up, only, String.trim(R.text(pwd))}, led, ls, [], []}) diff --git a/src/pkg/pkg.bend b/src/pkg/pkg.bend index 0202040..dc14984 100644 --- a/src/pkg/pkg.bend +++ b/src/pkg/pkg.bend @@ -1267,9 +1267,20 @@ def of.tree(fuel: Nat, +entry: String, +tree: Tree) -> Walked: def of.fuel(fuel: Nat, +entry: String, +fs: List<&2, Source>) -> Walked: of.tree(fuel, entry, Tree{True{}, fs, []}) -# the same, with the fuel `pkg_of` walks with +# fuel enough for a walk over these files: one step for the entry and one for +# every import it can queue, and every import is a line of some file. A +# function of the files rather than a number, so no checkout is too big for +# it and a proof about files it does not know leaves it unevaluated. +def walk.fuel(fs: List<&2, Source>) -> Nat: + match fs: + case []: + 1n + case Source{_at, text} <> t: + (List.length(&2, String, String.lines(text)) + walk.fuel(t) : Nat) + +# the same, with fuel enough for every file of the checkout def of(+entry: String, +fs: List<&2, Source>) -> Walked: - of.fuel(U32.to_nat(100000), entry, fs) + of.fuel(walk.fuel(fs), entry, fs) # --------------------------------------------------------------------------- # the walk answered from disk @@ -1338,14 +1349,29 @@ def pkg.next( case Unnamed{_nv}: made.io(top, made(entry, st)) +# fuel enough for a walk taken up again over the files read so far: a step +# for every file still queued, and `walk.fuel`'s step for every import those files +# can queue +def resumed.fuel(+tree: Tree, +st: State) -> Nat: + Tree{_whole, fs, _gone} = tree + State{_seen, _out, _stop, queue} = st + (List.length(&2, Task, queue) + walk.fuel(fs) : Nat) + +# how many files the walk from disk may ask for. It asks for one file a round +# and cannot know how many the checkout holds until it has read them, so this +# is a guard against a walk that keeps asking, not a limit on a package: a +# package of more files than this is refused, never hashed short. +def asks() -> Nat: + U32.to_nat(100000) + # the walk over what has been read so far, and each file it asks for read # and handed back, until it no longer asks -def pkg.ask(fuel: Nat, +top: String, +entry: String, +eb: String, +tree: Tree, st: State) -> IO(Pkg): +def pkg.ask(fuel: Nat, +top: String, +entry: String, +eb: String, +tree: Tree, +st: State) -> IO(Pkg): match fuel: case 0n: IO.die(Pkg, 1, "ez: the walk from " ++ entry ++ " asked too many questions") case 1n+f: - +now = walk(U32.to_nat(100000), tree, eb, st) + +now = walk(resumed.fuel(tree, st), tree, eb, st) pkg.next(state.stop(now), top, entry, tree, now, t => s => pkg.ask(f, top, entry, eb, t, s)) @@ -1353,7 +1379,7 @@ def pkg.ask(fuel: Nat, +top: String, +entry: String, +eb: String, +tree: Tree, s # entry and every file the walk reaches are paths from `top`, read from disk # as the walk asks for them. An import that climbs above `top` is refused. def walk.io(+top: String, +entry: String) -> IO(Pkg): - pkg.ask(U32.to_nat(100000), top, entry, P.base(entry), Tree{False{}, [], []}, + pkg.ask(asks(), top, entry, P.base(entry), Tree{False{}, [], []}, start(entry)) # the package an entry of a checkout would be published as diff --git a/src/pub/run.bend b/src/pub/run.bend index 698b669..e119256 100644 --- a/src/pub/run.bend +++ b/src/pub/run.bend @@ -86,6 +86,13 @@ def loop.step(st: PP.Step, +lib: String, world: PW.World, go: PW.World -> IO(Uni case PP.Run{pl}: Run.exec.plan(lib, pl) +# the rounds the loop may take. The walk asks for one file a round and cannot +# know how many the package holds until it has read them, so this is a guard +# against a planner that keeps asking, not a limit on a package: running out +# of it dies loudly. +def rounds() -> Nat: + U32.to_nat(100000) + # the loop, under fuel. The walk asks for one file a round, so the fuel is a # guard against a planner that keeps asking, not a limit on a package. def loop(fuel: Nat, +lib: String, +world: PW.World) -> IO(Unit): @@ -102,4 +109,4 @@ def run() -> IO(Unit): lib : String <- Env.lib() +pwd : String <- R.exec(["pwd"]) led : Maybe<&2, String> <- F.read(PW.toml()) - loop(U32.to_nat(100000), lib, PW.World{String.trim(R.text(pwd)), led, []}) + loop(rounds(), lib, PW.World{String.trim(R.text(pwd)), led, []}) diff --git a/src/tool/run.bend b/src/tool/run.bend index 6912e13..a32d318 100644 --- a/src/tool/run.bend +++ b/src/tool/run.bend @@ -274,6 +274,12 @@ def loop.step(st: TP.Step, world: TW.World, go: TW.World -> IO(Unit)) -> IO(Unit case TP.Run{pl}: exec.plan(pl) +# the rounds the loop may take. The planner asks at most five rounds, so this +# is only a guard against a planner that keeps asking, and running out of it +# dies loudly. +def rounds() -> Nat: + U32.to_nat(100) + # the loop, under fuel. Every round adds an answer, since the planner never # asks what the World answers already, and it asks at most five rounds, so # the fuel is only a guard. @@ -319,7 +325,7 @@ def go( +led: Maybe<&2, String>, +lock: Maybe<&2, String> ) -> IO(Unit): - loop(U32.to_nat(100), TW.World{act, target, want, here, vars, bend, led, lock, [], []}) + loop(rounds(), TW.World{act, target, want, here, vars, bend, led, lock, [], []}) # `ez tool run [--entry ] [-- args…]`, `install` and `upgrade` def run(act: TW.Act, +target: String, +want: String) -> IO(Unit):