impl.bend

bend-stdio-pipes@0.1.0.0 · 98 lines · 0 definitions

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