Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Revamp file reading and writing #4906

Merged
merged 8 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 25 additions & 16 deletions src/Init/System/IO.lean
Original file line number Diff line number Diff line change
Expand Up @@ -473,22 +473,11 @@ partial def Handle.readBinToEnd (h : Handle) : IO ByteArray := do
loop (acc ++ buf)
loop ByteArray.empty

partial def Handle.readToEnd (h : Handle) : IO String := do
let rec loop (s : String) := do
let line ← h.getLine
if line.isEmpty then
return s
else
loop (s ++ line)
loop ""

def readBinFile (fname : FilePath) : IO ByteArray := do
let h ← Handle.mk fname Mode.read
h.readBinToEnd

def readFile (fname : FilePath) : IO String := do
let h ← Handle.mk fname Mode.read
h.readToEnd
def Handle.readToEnd (h : Handle) : IO String := do
let data ← h.readBinToEnd
match String.fromUTF8? data with
| some s => return s
| none => throw <| .userError s!"Tried to read from handle containing non UTF-8 data."
hargoniX marked this conversation as resolved.
Show resolved Hide resolved

partial def lines (fname : FilePath) : IO (Array String) := do
let h ← Handle.mk fname Mode.read
Expand Down Expand Up @@ -594,6 +583,26 @@ end System.FilePath

namespace IO

namespace FS

def readBinFile (fname : FilePath) : IO ByteArray := do
-- Requires metadata so defined after metadata
let mdata ← fname.metadata
let size := mdata.byteSize.toUSize
if size > 0 then
let handle ← IO.FS.Handle.mk fname .read
handle.read mdata.byteSize.toUSize
hargoniX marked this conversation as resolved.
Show resolved Hide resolved
else
return ByteArray.mkEmpty 0

def readFile (fname : FilePath) : IO String := do
let data ← readBinFile fname
match String.fromUTF8? data with
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this make a copy of the data?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, unfortunately the runtime representation of ByteArray has the data part inlined instead of as a pointer and the layout of the ByteArray and String object are not compatible :/.

| some s => return s
| none => throw <| .userError s!"Tried to read file '{fname}' containing non UTF-8 data."

end FS

def withStdin [Monad m] [MonadFinally m] [MonadLiftT BaseIO m] (h : FS.Stream) (x : m α) : m α := do
let prev ← setStdin h
try x finally discard <| setStdin prev
Expand Down
45 changes: 16 additions & 29 deletions src/runtime/io.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -485,43 +485,30 @@ extern "C" LEAN_EXPORT obj_res lean_io_prim_handle_write(b_obj_arg h, b_obj_arg
}
}

/*
Handle.getLine : (@& Handle) → IO Unit
The line returned by `lean_io_prim_handle_get_line`
is truncated at the first '\0' character and the
rest of the line is discarded. */
/* Handle.getLine : (@& Handle) → IO Unit */
extern "C" LEAN_EXPORT obj_res lean_io_prim_handle_get_line(b_obj_arg h, obj_arg /* w */) {
FILE * fp = io_get_handle(h);
const int buf_sz = 64;
char buf_str[buf_sz]; // NOLINT
std::string result;
bool first = true;
while (true) {
char * out = std::fgets(buf_str, buf_sz, fp);
if (out != nullptr) {
if (strlen(buf_str) < buf_sz-1 || buf_str[buf_sz-2] == '\n') {
if (first) {
return io_result_mk_ok(mk_string(out));
} else {
result.append(out);
return io_result_mk_ok(mk_string(result));
}
}
result.append(out);
} else if (std::feof(fp)) {
clearerr(fp);
return io_result_mk_ok(mk_string(result));
} else {
return io_result_mk_error(decode_io_error(errno, nullptr));
}
first = false;
char* buf = NULL;
size_t n = 0;
ssize_t read = getline(&buf, &n, fp);
if (read != -1) {
obj_res ret = io_result_mk_ok(mk_string_from_bytes(buf, read));
free(buf);
return ret;
} else if (std::feof(fp)) {
clearerr(fp);
return io_result_mk_ok(mk_string(""));
} else {
return io_result_mk_error(decode_io_error(errno, nullptr));
}
}

/* Handle.putStr : (@& Handle) → (@& String) → IO Unit */
extern "C" LEAN_EXPORT obj_res lean_io_prim_handle_put_str(b_obj_arg h, b_obj_arg s, obj_arg /* w */) {
FILE * fp = io_get_handle(h);
if (std::fputs(lean_string_cstr(s), fp) != EOF) {
usize n = lean_string_size(s) - 1; // - 1 to ignore the terminal NULL byte.
usize m = std::fwrite(lean_string_cstr(s), 1, n, fp);
if (m == n) {
return io_result_mk_ok(box(0));
} else {
return io_result_mk_error(decode_io_error(errno, nullptr));
Expand Down
1 change: 1 addition & 0 deletions src/runtime/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ inline size_t string_capacity(object * o) { return lean_string_capacity(o); }
inline uint32 char_default_value() { return lean_char_default_value(); }
inline obj_res alloc_string(size_t size, size_t capacity, size_t len) { return lean_alloc_string(size, capacity, len); }
inline obj_res mk_string(char const * s) { return lean_mk_string(s); }
inline obj_res mk_string_from_bytes(char const * s, size_t sz) { return lean_mk_string_from_bytes(s, sz); }
LEAN_EXPORT obj_res mk_ascii_string_unchecked(std::string const & s);
LEAN_EXPORT obj_res mk_string(std::string const & s);
LEAN_EXPORT std::string string_to_std(b_obj_arg o);
Expand Down
12 changes: 12 additions & 0 deletions tests/lean/3546.lean
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
def test : IO Unit := do
let tmpFile := "3546.tmp"
let firstLine := "foo\u0000bar\n"
let content := firstLine ++ "hello world\nbye"
IO.FS.writeFile tmpFile content
let handle ← IO.FS.Handle.mk tmpFile .read
let firstReadLine ← handle.getLine
let cond := firstLine == firstReadLine && firstReadLine.length == 8 -- paranoid
IO.println cond
IO.FS.removeFile tmpFile

#eval test
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could use #guard_msgs, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might as well move this to tests/run now?

1 change: 1 addition & 0 deletions tests/lean/3546.lean.expected.out
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
true
Loading