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). 5 import Base 6 7 # Code 8 # ==== 9 10 # This program's stdin. A File owns its descriptor: ask for it once. 11 def 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. 19 def open( 20 program: String, 21 args: List 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. 29 def 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. 34 def read_line.peeked( 35 next: File & Result<&1, &1, U32 & String, String>, 36 text: String 37 ) -> File 38 & Result<&1, &1, U32 & String, Maybe>: 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. 48 def 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 57 def read_line.if( 58 file: File, 59 ended: Bool, 60 piece: String 61 ) -> IO(File & Result<&1, &1, U32 & String, Maybe>): 62 match ended: 63 case True{}: 64 IO.pure( 65 File & Result<&1, &1, U32 & String, Maybe>, 66 (file, Done{Some{read_line.unended(piece)}}) 67 ) 68 case False{}: 69 do IO>>: 70 next: File & Result<&1, &1, U32 & String, String> <- File.read_line(file, 1) 71 return read_line.peeked(next, piece) 72 73 def read_line.got( 74 got: File & Result<&1, &1, U32 & String, String> 75 ) -> IO(File & Result<&1, &1, U32 & String, Maybe>): 76 match got: 77 case (file, Fail{error}): 78 IO.pure(File & Result<&1, &1, U32 & String, Maybe>, (file, Fail{error})) 79 case (file, Done{SNil{}}): 80 IO.pure(File & Result<&1, &1, U32 & String, Maybe>, (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. 86 def read_line(file: File, max: U32) -> IO(File & Result<&1, &1, U32 & String, Maybe>): 87 IO.bind( 88 File & Result<&1, &1, U32 & String, String>, 89 File & Result<&1, &1, U32 & String, Maybe>, 90 File.read_line(file, max), 91 read_line.got 92 ) 93 94 def write_lines.text(lines: List) -> 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. 102 def write_lines(file: File, lines: List) -> IO(File & Result<&1, &1, U32 & String, Unit>): 103 File.write(file, write_lines.text(lines))