Streaming I/O
Part of: Reef Language Reference Last reviewed on version: 0.8.0
Reef provides three levels of file I/O, each suited to different use cases. Choosing the right one can dramatically reduce memory usage and GC pressure.
Result/Option migration (Phase C): io.file, io.buffer, and io.stream
functions that can fail now return Result[T, core.error.Error] instead of a
sentinel ("", -1, false). Reads in particular return
Result[Option[T], Error]: Err means the read failed, Ok(Some(data))
means data was read, and Ok(None) means clean EOF. Import the helpers you
need:
import core.result as result
import core.option as option
import core.error as error
When to Use Each Approach
| Approach | Module | Best For |
|---|---|---|
readFile() |
io.file |
Small files (< 64KB), config files, templates |
| Buffered reader | io.buffer |
Line-by-line processing, log parsing, CSV/TSV |
| Raw fd I/O | sys.fd |
Binary protocols, precise control, interop |
Rule of thumb: If you're reading a file larger than 64KB, or processing it
line-by-line, use io.buffer.BufferedReader instead of readFile().
GC Impact
The key difference is heap allocation. Reef's collector marks with the world stopped, then reclaims lazily — memory is freed incrementally inside the allocator as later allocations look for blocks, not in one pass at the end of a collection. So "GC pressure" here means two distinct costs, and neither is a long sweep pause:
- More marking work. Every live object is visited during the stop-the-world mark, so a larger live set lengthens the pause.
- More reclamation work spread across later allocations. Garbage is paid for by whichever allocation happens to need a block, so heavy churn shows up as allocation cost rather than as a single stall.
Reducing allocation reduces both.
-
readFile()+str.split(): Creates N+1 heap objects (file string + N line strings). If called in a loop, all become garbage simultaneously. -
BufferedReader.read_line(): Reuses an internal buffer. Only the returned line string is heap-allocated. Previous lines become garbage incrementally. -
sys.fdread/write: Minimal heap allocation — eachfd_read()call allocates exactly one string sized to the bytes actually read (there is no per-line splitting, and no intermediate buffer array).
Example: Allocation Comparison
Processing a 1000-line file 100 times:
| Method | Heap Objects Created | GC Pressure |
|---|---|---|
readFile + split |
100 × (1 + 1000) = 100,100 | Very high |
BufferedReader |
100 × 1000 = 100,000 lines | Moderate (incremental) |
sys.fd + manual parse |
~1 per fd_read() call |
Low |
The readFile approach also keeps the entire file content alive until the
enclosing scope exits, while the buffered reader only keeps one line at a time.
Line-by-Line Processing
Before (high allocation)
import io.file
import core.str
import core.result as result
fn count_matches(path: string, pattern: string): int
let content_r = file.readFile(path)
if result.is_err(content_r)
return 0
end if
let content = result.unwrap_ok(content_r)
// str.split takes a char delimiter and writes into a caller-provided
// array; it returns the part count, not the array itself.
mut lines = new [string](1000)
let n = str.split(content, '\n', lines, 1000)
mut count = 0
mut i = 0
while i < n
if str.contains(lines[i], pattern)
count = count + 1
end if
i = i + 1
end while
return count
end count_matches
After (streaming)
import io.buffer
import core.str
import core.result as result
import core.option as option
fn count_matches(path: string, pattern: string): int
let open_r = buffer.open_buffered_reader(path)
if result.is_err(open_r)
return 0
end if
let reader = result.unwrap_ok(open_r)
mut count = 0
while true
let line_r = buffer.reader_read_line(reader)
if result.is_err(line_r)
break
end if
let line_opt = result.unwrap_ok(line_r)
if option.is_none(line_opt)
break // EOF
end if
let line = option.unwrap(line_opt)
if str.contains(line, pattern)
count = count + 1
end if
end while
buffer.reader_close(reader)
return count
end count_matches
Writing with Auto-Flush
import io.buffer
import core.result as result
proc write_report(path: string, lines: [string], count: int)
let open_r = buffer.open_buffered_writer(path)
if result.is_err(open_r)
return
end if
let writer = result.unwrap_ok(open_r)
mut i = 0
while i < count
if result.is_err(buffer.writer_write_line(writer, lines[i]))
break
end if
i = i + 1
end while
buffer.writer_close(writer) // flushes remaining buffer
end write_report
Chunked Binary Reading
For binary files or when you need fixed-size chunks:
import sys.fd
import core.str
fn read_file_chunks(path: string, chunk_size: int): int
let f = fd.fd_open(path, fd.O_RDONLY(), 0)
if f < 0
return -1
end if
mut total = 0
// fd_read(fd, max_len) returns up to max_len bytes as a freshly
// allocated string; it does not take a caller-provided buffer. It
// returns "" on EOF or error, so length-zero ends the loop.
mut chunk = fd.fd_read(f, chunk_size)
while str.length(chunk) > 0
total = total + str.length(chunk)
chunk = fd.fd_read(f, chunk_size)
end while
fd.fd_close(f)
return total
end read_file_chunks
Common Anti-Patterns
Repeated file reads in a loop
// BAD: Reads and parses /etc/passwd 5000 times
mut i = 0
while i < 5000
let name = uid_to_name_via_readfile(entries[i].uid)
i = i + 1
end while
// GOOD: Use the stdlib function (zero allocation, uses POSIX getpwuid)
import fs.stat
import core.option as option
mut i = 0
while i < 5000
let name = option.unwrap_or(stat.uid_name(entries[i].uid), "unknown")
i = i + 1
end while
stat.uid_name/gid_name return Option[string] — None when the UID/GID
can't be resolved to a name (e.g. no matching /etc/passwd entry), rather
than an empty string.
Large file into string for single-value extraction
// BAD: Reads entire 10MB file to find one line
let content = result.unwrap_ok(file.readFile("/var/log/large.log"))
let lines = str.split(content, "\n")
let last = lines[lines.length() - 1]
// GOOD: Stream to the end, keep only last line
let reader = result.unwrap_ok(buffer.open_buffered_reader("/var/log/large.log"))
mut last = ""
while true
let line_r = buffer.reader_read_line(reader)
if result.is_err(line_r)
break
end if
let line_opt = result.unwrap_ok(line_r)
if option.is_none(line_opt)
break // EOF
end if
last = option.unwrap(line_opt)
end while
buffer.reader_close(reader)
See Also
io.file— simple file read/write for small filesio.buffer— buffered reader/writer for streamingsys.fd— raw file descriptor operationsfs.stat— file metadata and uid/gid resolution