main.bend source
main.bend on the hub · documented module
# snap: program runner for Bend 2. `run` (and `exec`) run a# program with argv as a list of strings. `start` leaves one running and# answers its pid. `par` runs several at once. The wire is newline-joined# argv, execvp with no shell, and an answer whose first line is the exit# status, then stdout and stderr together.import Base# the program and its arguments, newline separated, run with no shell between.# The answer is the exit status on its own first line, then stdout and stderr.def snaprun.exec(cmd: String) -> IO(String): import "./exec.c" import "./exec.js"# a program started and left running, answering with its pid. The effect above# waits for what it runs and reads it to the end; this one waits for nothing,# and gives the child a session and a /dev/null of its own so it holds no pipe# open behind it.def snaprun.start(cmd: String) -> IO(String): import "./start.c" import "./start.js"# several programs run at once, answering with every exit status in the order# the jobs were given. The children write into a directory rather than down a# pipe, and `par` below reads those files back, so this answers statuses alone.def snaprun.par(cmd: String) -> IO(String): import "./par.c" import "./par.js"# the arguments as the effect wants them. Newline separated and execvp'd# directly, so no argument is ever read as shell syntax.def cmd(argv: List<&2, String>) -> String: String.join(argv, "\n")# the arguments as the effect wants them; the same as `cmd`def line(argv: List<&2, String>) -> String: cmd(argv)# a program run with its argumentsdef run(argv: List<&2, String>) -> IO(String): snaprun.exec(cmd(argv))# a program run with its arguments; the same as `run`def exec(argv: List<&2, String>) -> IO(String): run(argv)# one job as the effect wants it: how many arguments, then each of them. The# count is what lets an argument hold anything a line may hold, so no separator# between jobs has to be invented and then kept out of the arguments.def job(+argv: List<&2, String>) -> String: Nat.show(List.length(&2, String, argv)) ++ "\n" ++ cmd(argv)# the jobs in the order they were given, each behind the newline that ends the# field before itdef plan.go(js: List<&2, List<&2, String>>) -> String: match js: case Nil{}: "" case Con{h, t}: "\n" ++ job(h) ++ plan.go(t)# what the effect is handed: how many may run at once, how many gigabytes each# was given, where they write, the second they must all be finished by, and# then every jobdef plan(js: List<&2, List<&2, String>>, +width: String, +gb: String, +at: String, +by: String) -> String: String.join([width, gb, at, by], "\n") ++ plan.go(js)# a program started with its arguments and left running; the answer is its piddef start(argv: List<&2, String>) -> IO(String): snaprun.start(cmd(argv))# the handle closed, and whatever was gathereddef file.slurp.end(file: File, acc: String) -> IO(String): do IO<String>: File.close(file) return acc# an empty chunk means the end of the filedef file.slurp.cut(chunk: String, file: File, acc: String, rest: File -> String -> IO(String)) -> IO(String): match chunk: case SNil{}: file.slurp.end(file, acc) case SCon{c, t}: rest(file, acc ++ SCon{c, t})# a failed read ends the file as surely as an empty onedef file.slurp.more(r: Result<&1, &1, U32 & String, String>, file: File, acc: String, rest: File -> String -> IO(String)) -> IO(String): match r: case Fail{_e}: file.slurp.end(file, acc) case Done{chunk}: file.slurp.cut(chunk, file, acc, rest)# the handle comes back paired with what it readdef file.slurp.next(got: File & Result<&1, &1, U32 & String, String>, acc: String, rest: File -> String -> IO(String)) -> IO(String): (file, r) = got file.slurp.more(r, file, acc, rest)# the whole file, read in chunks until one is empty, or the fuel isdef file.slurp(fuel: Nat, file: File, acc: String) -> IO(String): match fuel: case 0n: file.slurp.end(file, acc) case 1n+f: do IO<String>: got : File & Result<&1, &1, U32 & String, String> <- File.read(file, 65536) file.slurp.next(got, acc, fl => a => file.slurp(f, fl, a))# what an opened file holds, or nothing when it would not opendef file.read.opened(r: Result<&1, &1, U32 & String, File>) -> IO(Maybe<&2, String>): match r: case Fail{_e}: IO.pure(Maybe<&2, String>, None{}) case Done{file}: do IO<Maybe<&2, String>>: text : String <- file.slurp(U32.to_nat(100000), file, "") return Some{text}# a file's text, or None when it cannot be openeddef file.read(path: String) -> IO(Maybe<&2, String>): do IO<Maybe<&2, String>>: r : Result<&1, &1, U32 & String, File> <- File.open(path, "r") file.read.opened(r)# a file's text, or "" when there is nonedef file.text_of(m: Maybe<&2, String>) -> String: match m: case None{}: "" case Some{s}: s# the strings that are not empty, which is what a list of statuses amounts to# once a trailing newline has been counted as a linedef par.nonblank(ss: List<&2, String>) -> List<&2, String>: match ss: case Nil{}: Nil{} case Con{+h, t}: +more = par.nonblank(t) Bool.pick(List<&2, String>, String.is_empty(h), more, h <> more)# one job's answer, in the shape `run` gives: the status on its own first# line, then everything the program printed, read back out of the file it was# pointed atdef par.of(+code: String, m: Maybe<&2, String>) -> String: code ++ "\n" ++ file.text_of(m)# every job's answer, paired with the file it wrote by its place in the orderdef par.read(cs: List<&2, String>, +at: String, +i: Nat) -> IO(List<&2, String>): match cs: case Nil{}: IO.pure(List<&2, String>, Nil{}) case Con{+h, t}: do IO<List<&2, String>>: src : Maybe<&2, String> <- file.read(at ++ "/" ++ Nat.show(i)) +more : List<&2, String> <- par.read(t, at, (1n + i : Nat)) return par.of(h, src) <> more# several programs run at once, each answered exactly as `run` answers one, in# the order the jobs were given. `at` is a directory the caller owns and this# fills: job n writes to `at/n`. A width of "" leaves the count to the effect,# which reads the cores and divides them by `gb`, the memory cap each job runs# under. `by` is the second every job must be finished by, or "0" for none.def par(js: List<&2, List<&2, String>>, +width: String, +gb: String, +at: String, +by: String) -> IO(List<&2, String>): do IO<List<&2, String>>: +out : String <- snaprun.par(plan(js, width, gb, at, by)) par.read(par.nonblank(String.lines(out)), at, 0n)# the first line of an answer, which is the exit statusdef code.of(ls: List<&2, String>) -> String: match ls: case []: "127" case h <> t: h# everything after the first line, which is what the program printeddef text.of(ls: List<&2, String>) -> String: match ls: case []: "" case h <> t: String.join(t, "\n")# the exit status a run answered withdef code(out: String) -> String: code.of(String.lines(out))# what the program printed, stdout and stderr togetherdef text(out: String) -> String: text.of(String.lines(out))# a run that finished with status 0def ok(+out: String) -> Bool: String.eq(code(out), "0")