Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions ez.lock.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
6 changes: 3 additions & 3 deletions ez.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
8 changes: 7 additions & 1 deletion src/add/run.bend
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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), [], []})
10 changes: 8 additions & 2 deletions src/check/framing.bend
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -167,7 +173,7 @@ def loop(fuel: Nat, listener: Listener) -> IO(Unit):
Listener.close(listener)
case 1n+f:
do IO<Unit>:
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
Expand All @@ -183,7 +189,7 @@ def begin.at(bound: Result<&1, &1, U32 & String, Listener>, port: U32) -> IO(Uni
case Done{l}:
do IO<Unit>:
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):
Expand Down
10 changes: 8 additions & 2 deletions src/check/oracle.bend
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -688,7 +694,7 @@ def loop(fuel: Nat, +dir: String, listener: Listener) -> IO(Unit):
Listener.close(listener)
case 1n+f:
do IO<Unit>:
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
Expand All @@ -706,7 +712,7 @@ def begin.at(bound: Result<&1, &1, U32 & String, Listener>, +dir: String, port:
case Done{l}:
do IO<Unit>:
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):
Expand Down
8 changes: 7 additions & 1 deletion src/check/serve.bend
Original file line number Diff line number Diff line change
Expand Up @@ -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, "")
9 changes: 8 additions & 1 deletion src/doctor/run.bend
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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, []})
9 changes: 8 additions & 1 deletion src/fetch/run.bend
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -107,7 +114,7 @@ def fill(+here: String, +lib: String) -> IO(Unit):
do IO<Unit>:
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):
Expand Down
35 changes: 34 additions & 1 deletion src/io/file.bend
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,47 @@ 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:
case Fail{_e}:
IO.pure(Maybe<&2, String>, None{})
case Done{file}:
do IO<Maybe<&2, String>>:
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
Expand Down
11 changes: 10 additions & 1 deletion src/lock/run.bend
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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, [], []})
36 changes: 31 additions & 5 deletions src/pkg/pkg.bend
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1338,22 +1349,37 @@ 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))

# the package an entry would be published as, walked over its checkout: the
# 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
Expand Down
9 changes: 8 additions & 1 deletion src/pub/run.bend
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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, []})
8 changes: 7 additions & 1 deletion src/tool/run.bend
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 <file>] <target> [-- args…]`, `install` and `upgrade`
def run(act: TW.Act, +target: String, +want: String) -> IO(Unit):
Expand Down
Loading