impl.bend

bend-stdio-pipes@0.2.0.0 · 103 lines · 10 definitions

1# Pipes to a started program, and this program's stdin, as Files of UTF-8 lines.
2# A POLYFILL: Bend has no pipes to a started program, no way to kill one, and no stdin handle yet.
3# Delete this module when Base has them, and call Base instead.
4# A line has at most max bytes with its LF or CRLF: a longer one fails with E2BIG (7).
5import Base
6
7# Code
8# ====
9
10# This program's stdin. A File owns its descriptor: ask for it once.
11def stdin() -> IO(File):
12  import "./std.c"
13  import "./std.js"
14
15# Start a program, with its stdin and stdout as pipes: its process id, the File that writes its
16# stdin, and the File that reads its stdout. Its stderr is ours. Closing the stdin File gives it
17# EOF; nothing waits for it to exit. On JS, it runs on Bun on Linux and macOS only; elsewhere it
18# fails with ENOSYS.
19def open(
20  program: String,
21  args: List<String>
22) -> IO(Result<&1, &1, U32 & String, U32 & (File & File)>):
23  import "./open.c"
24  import "./open.js"
25
26# Kills the program that open started with pid, unless it has ended: then it fails with ESRCH (3).
27# It never kills a program that open did not start, though the system gives an ended program's pid
28# to another one.
29def kill(pid: U32) -> IO(Result<&1, &1, U32 & String, Unit>):
30  import "./open.c"
31  import "./open.js"
32
33# A line with no LF that ends the input is the last line; one that does not is too long.
34def read_line.peeked(
35  next: File & Result<&1, &1, U32 & String, String>,
36  text: String
37) -> File
38  & Result<&1, &1, U32 & String, Maybe<String>>:
39  match next:
40    case (file, Fail{error}):
41      (file, Fail{error})
42    case (file, Done{SNil{}}):
43      (file, Done{Some{text}})
44    case (file, Done{SCon{_, _}}):
45      (file, Fail{(7, "a line is longer than its cap")})
46
47# The line of a piece that ends with its LF: the piece without the LF, and the CR before it, if any.
48def read_line.unended(+piece: String) -> String:
49  +line = String.take(piece, Nat.sub(String.length(piece), 1n))
50  Bool.pick(
51    String,
52    String.ends_with(line, "\r"),
53    String.take(line, Nat.sub(String.length(line), 1n)),
54    line
55  )
56
57def read_line.if(
58  file: File,
59  ended: Bool,
60  piece: String
61) -> IO(File & Result<&1, &1, U32 & String, Maybe<String>>):
62  match ended:
63    case True{}:
64      IO.pure(
65        File & Result<&1, &1, U32 & String, Maybe<String>>,
66        (file, Done{Some{read_line.unended(piece)}})
67      )
68    case False{}:
69      do IO<File & Result<&1, &1, U32 & String, Maybe<String>>>:
70        next: File & Result<&1, &1, U32 & String, String> <- File.read_line(file, 1)
71        return read_line.peeked(next, piece)
72
73def read_line.got(
74  got: File & Result<&1, &1, U32 & String, String>
75) -> IO(File & Result<&1, &1, U32 & String, Maybe<String>>):
76  match got:
77    case (file, Fail{error}):
78      IO.pure(File & Result<&1, &1, U32 & String, Maybe<String>>, (file, Fail{error}))
79    case (file, Done{SNil{}}):
80      IO.pure(File & Result<&1, &1, U32 & String, Maybe<String>>, (file, Done{None{}}))
81    case (file, Done{SCon{h, t}}):
82      +piece: String = SCon{h, t}
83      read_line.if(file, String.ends_with(piece, "\n"), piece)
84
85# The next line of file, as it comes, without its LF or CRLF; None at the end of the file.
86def read_line(file: File, max: U32) -> IO(File & Result<&1, &1, U32 & String, Maybe<String>>):
87  IO.bind(
88    File & Result<&1, &1, U32 & String, String>,
89    File & Result<&1, &1, U32 & String, Maybe<String>>,
90    File.read_line(file, max),
91    read_line.got
92  )
93
94def write_lines.text(lines: List<String>) -> String:
95  match lines:
96    case Nil{}:
97      ""
98    case Con{line, rest}:
99      line ++ "\n" ++ write_lines.text(rest)
100
101# Writes each line to file, with its LF.
102def write_lines(file: File, lines: List<String>) -> IO(File & Result<&1, &1, U32 & String, Unit>):
103  File.write(file, write_lines.text(lines))