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