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). 7 import Base 8 9 # Code 10 # ==== 11 12 # This program's stdin. A File owns its descriptor: ask for it once. 13 def 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. 21 def open( 22 program: String, 23 args: List 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. 29 def read_line.peeked( 30 next: File & Result<&1, &1, U32 & String, String>, 31 text: String 32 ) -> File 33 & Result<&1, &1, U32 & String, Maybe>: 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. 43 def 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 52 def read_line.if( 53 file: File, 54 ended: Bool, 55 piece: String 56 ) -> IO(File & Result<&1, &1, U32 & String, Maybe>): 57 match ended: 58 case True{}: 59 IO.pure( 60 File & Result<&1, &1, U32 & String, Maybe>, 61 (file, Done{Some{read_line.unended(piece)}}) 62 ) 63 case False{}: 64 do IO>>: 65 next: File & Result<&1, &1, U32 & String, String> <- File.read_line(file, 1) 66 return read_line.peeked(next, piece) 67 68 def read_line.got( 69 got: File & Result<&1, &1, U32 & String, String> 70 ) -> IO(File & Result<&1, &1, U32 & String, Maybe>): 71 match got: 72 case (file, Fail{error}): 73 IO.pure(File & Result<&1, &1, U32 & String, Maybe>, (file, Fail{error})) 74 case (file, Done{SNil{}}): 75 IO.pure(File & Result<&1, &1, U32 & String, Maybe>, (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. 81 def read_line(file: File, max: U32) -> IO(File & Result<&1, &1, U32 & String, Maybe>): 82 IO.bind( 83 File & Result<&1, &1, U32 & String, String>, 84 File & Result<&1, &1, U32 & String, Maybe>, 85 File.read_line(file, max), 86 read_line.got 87 ) 88 89 def write_lines.text(lines: List) -> 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. 97 def write_lines(file: File, lines: List) -> IO(File & Result<&1, &1, U32 & String, Unit>): 98 File.write(file, write_lines.text(lines))