# src/file: a file's text, read whole, or None when it cannot be opened. import Base # the handle closed, and whatever was gathered def file.slurp.end(file: File, acc: String) -> IO(String): do IO: File.close(file) return acc # an empty chunk means the end of the file def file.slurp.cut(chunk: String, file: File, acc: String, rest: File -> String -> IO(String)) -> IO(String): match chunk: case SNil{}: file.slurp.end(file, acc) case SCon{c, t}: rest(file, acc ++ SCon{c, t}) # a failed read ends the file as surely as an empty one def file.slurp.more( res: Result<&1, &1, U32 & String, String>, file: File, acc: String, rest: File -> String -> IO(String) ) -> IO(String): match res: case Fail{_e}: file.slurp.end(file, acc) case Done{chunk}: file.slurp.cut(chunk, file, acc, rest) # the handle comes back paired with what it read def file.slurp.next( got: File & Result<&1, &1, U32 & String, String>, acc: String, rest: File -> String -> IO(String) ) -> IO(String): (file, res) = got file.slurp.more(res, file, acc, rest) # the whole file, read in chunks until one is empty, or the fuel is def file.slurp(fuel: Nat, file: File, acc: String) -> IO(String): match fuel: case 0n: file.slurp.end(file, acc) case 1n+f: do IO: got : File & Result<&1, &1, U32 & String, String> <- File.read(file, 65536) file.slurp.next(got, acc, fl => a => file.slurp(f, fl, a)) # the reads a file with no size to go by may take: a pipe, a device or a # /proc file reports 0 bytes and a file past 4 GiB fails to report, so these # are read until they end, up to 100000 reads of 64 KiB def file.unsized() -> Nat: U32.to_nat(100000) # a size of 0 is no size to go by; any other is read to def file.reads.given(none: Bool, bytes: U32) -> Nat: match none: case True{}: file.unsized() case False{}: U32.to_nat(U32.add(U32.div(bytes, 65536), 2)) # the reads a file of this many bytes takes: one for every 64 KiB, one for # the part past the last whole 64 KiB, and one to spare for a short read def file.reads(+bytes: U32) -> Nat: file.reads.given(U32.is_zero(bytes), bytes) # the reads the size the host reported allows def file.reads.of(res: Result<&1, &1, U32 & String, U32>) -> Nat: match res: case Fail{_e}: file.unsized() case Done{bytes}: file.reads(bytes) # a file read whole, with fuel enough for the size it reported def file.read.sized(got: File & Result<&1, &1, U32 & String, U32>) -> IO(String): (file, res) = got file.slurp(file.reads.of(res), file, "") # what an opened file holds, or nothing when it would not open def file.read.opened(res: Result<&1, &1, U32 & String, File>) -> IO(Maybe<&2, String>): match res: case Fail{_e}: IO.pure(Maybe<&2, String>, None{}) case Done{file}: do IO>: got : File & Result<&1, &1, U32 & String, U32> <- File.size(file) text : String <- file.read.sized(got) return Some{text} # a file's text, or None when it cannot be opened def file.read(path: String) -> IO(Maybe<&2, String>): do IO>: res : Result<&1, &1, U32 & String, File> <- File.open(path, "r") file.read.opened(res) # a file's text, or "" when there is none def file.text_of(got: Maybe<&2, String>) -> String: match got: case None{}: "" case Some{s}: s