1 # src/file: a file's text, read whole, or None when it cannot be opened. 2 import Base 3 4 # the handle closed, and whatever was gathered 5 def file.slurp.end(file: File, acc: String) -> IO(String): 6 do IO: 7 File.close(file) 8 return acc 9 10 # an empty chunk means the end of the file 11 def file.slurp.cut(chunk: String, file: File, acc: String, rest: File -> String -> IO(String)) -> IO(String): 12 match chunk: 13 case SNil{}: 14 file.slurp.end(file, acc) 15 case SCon{c, t}: 16 rest(file, acc ++ SCon{c, t}) 17 18 # a failed read ends the file as surely as an empty one 19 def file.slurp.more( 20 res: Result<&1, &1, U32 & String, String>, 21 file: File, 22 acc: String, 23 rest: File -> String -> IO(String) 24 ) -> IO(String): 25 match res: 26 case Fail{_e}: 27 file.slurp.end(file, acc) 28 case Done{chunk}: 29 file.slurp.cut(chunk, file, acc, rest) 30 31 # the handle comes back paired with what it read 32 def file.slurp.next( 33 got: File & Result<&1, &1, U32 & String, String>, 34 acc: String, 35 rest: File -> String -> IO(String) 36 ) -> IO(String): 37 (file, res) = got 38 file.slurp.more(res, file, acc, rest) 39 40 # the whole file, read in chunks until one is empty, or the fuel is 41 def file.slurp(fuel: Nat, file: File, acc: String) -> IO(String): 42 match fuel: 43 case 0n: 44 file.slurp.end(file, acc) 45 case 1n+f: 46 do IO: 47 got : File & Result<&1, &1, U32 & String, String> <- File.read(file, 65536) 48 file.slurp.next(got, acc, fl => a => file.slurp(f, fl, a)) 49 50 # the reads a file with no size to go by may take: a pipe, a device or a 51 # /proc file reports 0 bytes and a file past 4 GiB fails to report, so these 52 # are read until they end, up to 100000 reads of 64 KiB 53 def file.unsized() -> Nat: 54 U32.to_nat(100000) 55 56 # a size of 0 is no size to go by; any other is read to 57 def file.reads.given(none: Bool, bytes: U32) -> Nat: 58 match none: 59 case True{}: 60 file.unsized() 61 case False{}: 62 U32.to_nat(U32.add(U32.div(bytes, 65536), 2)) 63 64 # the reads a file of this many bytes takes: one for every 64 KiB, one for 65 # the part past the last whole 64 KiB, and one to spare for a short read 66 def file.reads(+bytes: U32) -> Nat: 67 file.reads.given(U32.is_zero(bytes), bytes) 68 69 # the reads the size the host reported allows 70 def file.reads.of(res: Result<&1, &1, U32 & String, U32>) -> Nat: 71 match res: 72 case Fail{_e}: 73 file.unsized() 74 case Done{bytes}: 75 file.reads(bytes) 76 77 # a file read whole, with fuel enough for the size it reported 78 def file.read.sized(got: File & Result<&1, &1, U32 & String, U32>) -> IO(String): 79 (file, res) = got 80 file.slurp(file.reads.of(res), file, "") 81 82 # what an opened file holds, or nothing when it would not open 83 def file.read.opened(res: Result<&1, &1, U32 & String, File>) -> IO(Maybe<&2, String>): 84 match res: 85 case Fail{_e}: 86 IO.pure(Maybe<&2, String>, None{}) 87 case Done{file}: 88 do IO>: 89 got : File & Result<&1, &1, U32 & String, U32> <- File.size(file) 90 text : String <- file.read.sized(got) 91 return Some{text} 92 93 # a file's text, or None when it cannot be opened 94 def file.read(path: String) -> IO(Maybe<&2, String>): 95 do IO>: 96 res : Result<&1, &1, U32 & String, File> <- File.open(path, "r") 97 file.read.opened(res) 98 99 # a file's text, or "" when there is none 100 def file.text_of(got: Maybe<&2, String>) -> String: 101 match got: 102 case None{}: 103 "" 104 case Some{s}: 105 s