~/bend-docscommunity

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")