Skip to main content

core.io — Files, paths, streams, processes

File I/O, path manipulation, standard streams, processes, and the Read/Write/Seek/BufRead protocol family. Both sync and async variants are provided.

Submodule status​

ModuleStatusConformance suite
protocols.vrpartialcore-tests/io/protocols — 60+ tests pin IoErrorKind 20-variant, StreamError, SeekFrom, POSIX-stable errno table, IoResult. 11 @ignore'd on Reader/Writer/Cursor method dispatch (#io-1).
buffer.vrpartialcore-tests/io/buffer — constants (DEFAULT_BUF_CAPACITY=8192, MIN_BUF_CAPACITY=64) + FdReader, BufferCursor, BufReader, BufWriter construction. 7 @ignore'd on method surface (#io-1).
path.vrpartialcore-tests/io/path — 40+ tests pin Path immutable surface (parent, file_name, file_stem, extension, starts_with, ends_with, strip_prefix), Component 5-variant, PathBuf construction. PathBuf mutable surface (push/pop/set_extension) partially gated by #io-4.
stdio.vrpartialcore-tests/io/stdio — factory functions (stdin/stdout/stderr), lock construction, print/println/eprint/eprintln (live, no-panic). Read/write methods gated by #io-1.
file.vrpartialcore-tests/io/file — O_* constants (POSIX-stable values), OpenOptions fluent builder, File.options factory. File I/O gated by #io-1 + #io-8 (temp-dir harness).
fs.vrpartialcore-tests/io/fs — FileType 4-variant + predicates, Permissions mode round-trip. Live fs ops gated by #io-1 + #io-8.
process.vrpartialcore-tests/io/process — Stdio 3-variant, ExitStatus over POSIX waitpid encoding, Command fluent builder. Live spawn gated by #io-1 + #io-8.
async_protocols.vrpartialcore-tests/io/async_protocols — ReadFuture / WriteFuture / FlushFuture construction + read_async/write_async/flush_async factories. Polling gated by #io-1 + executor.
engine.vrpartialcore-tests/io/engine — capacity constants + IoEvent Read/Write/ReadWrite flag laws. Engine lifecycle gated by #io-16 (sandboxed test harness).
mod.vrpartialcore-tests/io/mod — IoError = StreamError alias verified; IoErrorKind / SeekFrom / IoResult / print fn reachability through core.io.*.
Source FileWhat's in it
protocols.vrRead, Write, Seek, BufRead protocols; StreamError, IoErrorKind (20 variants), IoResult<T>, SeekFrom; EmptyReader, ByteRepeat, Sink, Cursor<T>, Chain, Take; BytesIter, LinesIter, SplitIter
async_protocols.vrAsyncRead, AsyncWrite, AsyncBufRead; ReadFuture<R>, WriteFuture<W>, FlushFuture<W>; read_async, write_async, flush_async
file.vrFile, OpenOptions, BufReader, BufWriter; O_RDONLY/O_WRONLY/O_RDWR/O_CREAT/O_EXCL/O_TRUNC/O_APPEND; read, read_to_string, write, write_bytes
stdio.vrStdin, Stdout, Stderr (and locks); print/println/eprint/eprintln; read_line, read_int, read_float
path.vrPath, PathBuf, Component (5 variants: Prefix, RootDir, CurDir, ParentDir, Normal); MAIN_SEPARATOR; normalize, join, file_name, file_stem, extension, parent
fs.vrMetadata, FileType (4 variants: File, Dir, Symlink, Unknown), DirEntry, ReadDir, WalkDir, Permissions; filesystem operations (metadata/exists/is_file/is_dir/is_symlink/create_dir/remove_dir/read_dir/rename/copy/canonicalize/temp_dir/current_dir/walk_dir)
buffer.vrBufReader<R>, BufWriter<W>, LineWriter<W>, BufferCursor<T> (alias Cursor<T>), FdReader; IntoInnerError<W>; copy, read_all; DEFAULT_BUF_CAPACITY=8192, MIN_BUF_CAPACITY=64
process.vrCommand, Child, ExitStatus, Output, Stdio (3 variants: Inherit, Piped, Null); run
engine.vrIoEngine, IoEvent; IO_ENGINE_DEFAULT_CAPACITY=256, IO_ENGINE_MAX_CAPACITY=65536, IO_ENGINE_MAX_POLL_EVENTS=4096; per-platform multiplexer (kqueue / epoll / IOCP)

Error types​

type StreamError is {
kind: IoErrorKind,
message: Maybe<Text>,
};

type IoErrorKind is
| NotFound // ENOENT
| PermissionDenied // EACCES
| ConnectionRefused // ECONNREFUSED
| ConnectionReset // ECONNRESET
| ConnectionAborted // ECONNABORTED
| NotConnected // ENOTCONN
| AddrInUse // EADDRINUSE
| AddrNotAvailable // EADDRNOTAVAIL
| BrokenPipe // EPIPE
| AlreadyExists // EEXIST
| WouldBlock // EAGAIN
| InvalidInput // EINVAL
| InvalidData
| TimedOut // ETIMEDOUT
| WriteZero
| Interrupted // EINTR
| UnexpectedEof
| OutOfMemory // ENOMEM
| Unsupported
| Other; // catch-all (carries optional message field)

type IoResult<T> = Result<T, StreamError>;

// Public alias: IoError == StreamError. Both names accept the same value.
type IoError = StreamError;

Constructors​

StreamError.new(kind: IoErrorKind) -> StreamError
StreamError.with_message(kind: IoErrorKind, msg: Text) -> StreamError
StreamError.Other(msg: Text) -> StreamError // = with_message(Other, msg)
StreamError.from_raw_os_error(code: Int) -> StreamError // POSIX-stable codes pinned
StreamError.from_errno(errno: Int) -> StreamError // alias for from_raw_os_error
StreamError.from_os(err: OSError) -> StreamError // from a platform OS error

Accessors​

e.kind() -> IoErrorKind
e.message() -> Maybe<&Text>

POSIX-stable errno table​

These codes produce identical IoErrorKind on every platform:

errnocodekind
ENOENT2NotFound
EINTR4Interrupted
ENOMEM12OutOfMemory
EACCES13PermissionDenied
EEXIST17AlreadyExists
EINVAL22InvalidInput
EPIPE32BrokenPipe

Codes outside this table dispatch through io_error_kind_from_os_code, which is @cfg-gated per platform — Linux and macOS overlap in numerically-conflicting ways (e.g. Linux EAGAIN=11 collides with macOS EDEADLK=11), so a single shared table would silently mis-classify. See core/io/protocols.vr:127-213 for the full per-platform dispatcher.


Read / Write / Seek / BufRead protocols​

Read​

type Read is protocol {
fn read(&mut self, buf: &mut [Byte]) -> IoResult<Int>;
fn read_exact(&mut self, buf: &mut [Byte]) -> IoResult<()>; // default
fn read_to_end(&mut self, buf: &mut List<Byte>) -> IoResult<Int>; // default; retries EINTR
fn read_to_string(&mut self, out: &mut Text) -> IoResult<Int>; // default
fn bytes(self) -> BytesIter<Self>;
fn chain<R: Read>(self, next: R) -> Chain<Self, R>;
fn take(self, limit: Int) -> Take<Self>;
}

Write​

type Write is protocol {
fn write(&mut self, buf: &[Byte]) -> IoResult<Int>;
fn flush(&mut self) -> IoResult<()>;
fn write_all(&mut self, buf: &[Byte]) -> IoResult<()>; // default; retries EINTR
fn write_fmt(&mut self, s: &Text) -> IoResult<()>; // default
}

Seek​

type Seek is protocol {
fn seek(&mut self, pos: SeekFrom) -> IoResult<Int>;
fn stream_position(&mut self) -> IoResult<Int>; // default
fn rewind(&mut self) -> IoResult<()>; // default
fn stream_len(&mut self) -> IoResult<Int>; // default
}

type SeekFrom is Start(Int) | End(Int) | Current(Int);

BufRead​

type BufRead is protocol extends Read {
fn fill_buf(&mut self) -> IoResult<&[Byte]>;
fn consume(&mut self, amt: Int);
fn has_data_left(&mut self) -> IoResult<Bool>; // default
fn read_until(&mut self, delim: Byte, buf: &mut List<Byte>) -> IoResult<Int>;
fn read_line(&mut self, buf: &mut Text) -> IoResult<Int>;
fn lines(self) -> LinesIter<Self>;
fn split(self, delim: Byte) -> SplitIter<Self>;
}

Async variants​

// core/async/io_protocols.vr — the protocols are POLL-based. They are
// what you IMPLEMENT, and none of their methods is an `async fn`.
public type AsyncRead is protocol {
fn poll_read(&mut self, cx: &mut Context, buf: &mut List<Byte>)
-> Poll<IoResult<Int>>;
};

public type AsyncWrite is protocol {
// `buf` is `&List<Byte>`, not a slice — byte-typed end to end, and
// the declaration says so in its own comment. Reaching for
// `&[Byte]` here is the reflex this block is correcting.
fn poll_write(&mut self, cx: &mut Context, buf: &List<Byte>)
-> Poll<IoResult<Int>>;
fn poll_flush(&mut self, cx: &mut Context) -> Poll<IoResult<()>>;
fn poll_shutdown(&mut self, cx: &mut Context) -> Poll<IoResult<()>>;
};

public type AsyncBufRead is protocol {
fn poll_fill_buf(&mut self, cx: &mut Context) -> Poll<IoResult<List<Byte>>>;
fn consume(&mut self, amt: Int);
};

What you CALL is a set of free functions, not methods. Awaiting happens on the future they hand back:

mount core.async.io_protocols.{read_async, write_async, flush_async};

let n = read_async(reader, buf).await?; // ReadFuture<R>
let n = write_async(writer, buf).await?; // WriteFuture<W>
flush_async(writer).await?; // FlushFuture<W>
The convenience names are on the TYPES, not the protocols

read_to_end_async, write_all_async, shutdown_async, read_line_async and lines_async appear nowhere in core/, and this block used to declare all five as protocol methods. Three other pages were written against it, so if you copied reader.read_line_async(&mut line).await from this site, that is where it came from.

The CAPABILITIES exist — under different names, on concrete types:

what you wantedwhat to callon
read_line_asyncnext_line_async() → IoResult<Maybe<Text>>BufReader
read_to_end_asyncread_to_end(&mut List<Byte>)AsyncFile
write_all_asyncwrite_all(...)AsyncFile
read_to_stringread_to_string(&mut Text)AsyncFile

Note the SHAPE change on the first row: next_line_async answers Maybe<Text> — Maybe.None is EOF — where the fictional read_line_async answered a byte count and 0 meant EOF. Code translated name-for-name will loop forever.

lines_async and the AsyncLines type have no counterpart: write the loop. And AsyncFile's async methods are sync-under-async today (next_line_async is literally self.next_line()), which is the same caveat as the paragraph below.

The current async I/O implementation runs sync I/O on a worker thread ("sync-under-async") — real io_uring / kqueue / IOCP routing through core.io.engine.IoEngine.poll is on the roadmap (task #io-15).


File operations​

Opening​

File.open(path: &Text) -> IoResult<File> // read-only
File.create(path: &Text) -> IoResult<File> // truncate/create write
File.create_new(path: &Text) -> IoResult<File> // fail if exists

OpenOptions.new()
.read(true)
.write(true)
.append(false)
.create(true)
.create_new(false) // fail if exists
.truncate(false)
.open(path: &Text) -> IoResult<File>

File methods​

f.size() -> IoResult<Int>
f.set_len(size: Int) -> IoResult<()>
f.sync_all() -> IoResult<()> // fsync
f.sync_data() -> IoResult<()> // fdatasync
f.try_clone() -> IoResult<File>
f.as_raw_fd() -> Int

// Implements Read, Write, Seek, AsyncRead, AsyncWrite

Convenience helpers (file-level)​

file.read(path: &Text) -> IoResult<List<Byte>>
file.read_to_string(path: &Text) -> IoResult<Text>
file.write(path: &Text, contents: &Text) -> IoResult<()>
file.write_bytes(path: &Text, bytes: &[Byte]) -> IoResult<()>

Open flag constants (POSIX-stable)​

const O_RDONLY: Int = 0x0000;
const O_WRONLY: Int = 0x0001;
const O_RDWR: Int = 0x0002;
const O_CREAT: Int = 0x0200;
const O_EXCL: Int = 0x0800;
const O_TRUNC: Int = 0x0400;
const O_APPEND: Int = 0x0008;

Modifier bits (CREAT/EXCL/TRUNC/APPEND) are each single-bit set and pairwise-disjoint — verified by core-tests/io/file/property_test.vr::law_modifier_flags_pairwise_disjoint.


Buffered I/O​

BufReader.new(R) -> BufReader<R> // default cap 8192
BufReader.with_capacity(capacity: Int, R) // clamped to MIN=64
br.capacity() -> Int
br.get_ref() -> &R
br.get_mut() -> &mut R
br.into_inner() -> R

BufWriter.new(W) -> BufWriter<W> // default cap 8192
BufWriter.with_capacity(capacity: Int, W)
bw.capacity() -> Int
bw.buffer_len() -> Int // currently-buffered
bw.flush() -> IoResult<()>
bw.into_inner() -> Result<W, IntoInnerError<W>>

LineWriter.new(W) -> LineWriter<W> // flushes on '\n'

// Helpers
copy<R: Read, W: Write>(reader: &mut R, writer: &mut W) -> IoResult<Int>
read_all<R: Read>(reader: &mut R) -> IoResult<List<Byte>>

const DEFAULT_BUF_CAPACITY: Int = 8192;
const MIN_BUF_CAPACITY: Int = 64;

Cursor​

BufferCursor.new(inner: T) -> BufferCursor<T>
// `Cursor<T>` is an alias for `BufferCursor<T>` (see open defect §1 — alias collision with protocols.vr Cursor).
c.position() -> Int
c.set_position(pos: Int)
c.get_ref() / c.get_mut() / c.into_inner()

// For Cursor<List<Byte>>: implements Read + Write + Seek + BufRead.

FdReader​

FdReader.from_borrowed_fd(fd: Int) -> FdReader // caller closes
FdReader.from_owned_fd(fd: Int) -> FdReader // drop closes
r.raw_fd() -> Int

// Implements Read. Bridges to the runtime's verum_fd_read_chunk
// which honours EINTR retries internally.

Idiomatic line processing (post-#io-1)​

Open defect — this pattern crashes under AOT. Checked on docs/by-example/19-file-io: the interpreter prints every line and the AOT binary writes and reads the file correctly, then faults inside BufReader.read_until on the first iteration. The fault address IS the data — a file beginning line one… faults at 0x6c ('l'), one beginning Zebra… at 0x5a ('Z'), same instruction both times — so the loop dereferences the element VALUE where it expects a reference. The cause is one layer down: read_until walks available.iter().enumerate(), and a generic iterator adaptor erases which reference convention its inner producer used. Until that closes, line-oriented reading is interpreter-only; read_to_string and write are unaffected at both tiers.

fn process_lines(path: &Path) -> IoResult<Int> {
let f = File.open(path.as_str())?;
let mut reader = BufReader.new(f);
let mut count = 0;
for line in reader.lines() {
let line = line?;
if line.starts_with("ERROR") { count = count + 1; }
}
Ok(count)
}

Standard streams​

stdin() -> Stdin
stdout() -> Stdout
stderr() -> Stderr

// Locking (currently a no-op marker — no actual mutual exclusion, see task #io-7):
stdin().lock() -> StdinLock
stdout().lock() -> StdoutLock
stderr().lock() -> StderrLock
print(s: &Text)
println(s: &Text)
println_empty()
eprint(s: &Text)
eprintln(s: &Text)

Read helpers​

read_line() -> IoResult<Text> // newline-trimmed
read_int() -> IoResult<Int>
read_float() -> IoResult<Float>

Path and PathBuf​

Path is an immutable Verum value holding a Text; PathBuf is its mutable counterpart. Both are valid UTF-8.

Construction​

Path.new(s: &Text) -> Path
Path.from_str(s: &str) -> Path // raw string literal

PathBuf.new() -> PathBuf
PathBuf.with_capacity(capacity: Int) -> PathBuf
PathBuf.from(s: &Text) -> PathBuf
PathBuf.from_str(s: &str) -> PathBuf
PathBuf.default() -> PathBuf // == new()

Inspection​

p.as_str() -> &Text
p.to_text() -> Text
p.to_path_buf() -> PathBuf
p.is_empty() -> Bool
p.is_absolute() -> Bool // Unix: starts / ; Windows: drive-letter or UNC
p.is_relative() -> Bool // !is_absolute()
p.has_root() -> Bool // alias for is_absolute()

p.parent() -> Maybe<Path>
p.file_name() -> Maybe<Text>
p.file_stem() -> Maybe<Text> // file_name without extension
p.extension() -> Maybe<Text>
p.ancestors() -> Ancestors
p.components() -> Components
p.iter() -> PathIter

p.starts_with(prefix: &Path) -> Bool
p.ends_with(suffix: &Path) -> Bool // component-wise
p.strip_prefix(prefix: &Path) -> Maybe<Path>

p.exists() -> Bool // hits fs
p.is_file() -> Bool / is_dir() / is_symlink()

Mutation (PathBuf)​

pb.push(component: &Path) // appends with separator
pb.push_str(component: &Text)
pb.pop() -> Bool // returns false if at root
pb.set_file_name(name: &Text)
pb.set_extension(ext: &Text) -> Bool
pb.clear() / pb.reserve(additional: Int)
pb.capacity() -> Int

Open defect (#io-4): PathBuf.push of a relative component has a Text-equality drift after the separator+suffix concatenation. The byte content is correct (length 15 for /home/user + docs), but direct Text.eq between the push-built form and the literal-built form fails — likely a Text invariant (small-string-inline state or hash cache) not preserved through the push/push_str chain via nested struct field access. Surgical fix landed for the length (was 28 → now 15); equality drift pinned in core-tests/io/path/regression_test.vr §A.

Component​

type Component is
| Prefix(Text) // Windows drive letter (e.g. "C:") or UNC
| RootDir // leading "/" or "\\"
| CurDir // "."
| ParentDir // ".."
| Normal(Text);

c.as_text() -> Maybe<&Text> // Some for Prefix/Normal, None for the rest
c.to_text() -> Text // RootDir → "/", CurDir → ".", etc.

Canonicalisation​

normalize(path: &Path) -> PathBuf // lexical "." / ".." resolution, no fs hit
fs.canonicalize(&path) -> IoResult<PathBuf> // resolves symlinks (hits disk)

Constants​

const MAIN_SEPARATOR: Char = '/'; // Unix
const MAIN_SEPARATOR: Char = '\\'; // Windows

Filesystem operations​

fs.metadata(&path) -> IoResult<Metadata>
fs.symlink_metadata(&path) -> IoResult<Metadata> // doesn't follow symlinks
fs.exists(&path) -> Bool
fs.is_file(&path) -> Bool
fs.is_dir(&path) -> Bool
fs.is_symlink(&path) -> Bool

fs.create_dir(&path) -> IoResult<()>
fs.create_dir_all(&path) -> IoResult<()> // like mkdir -p
fs.remove_file(&path) -> IoResult<()>
fs.remove_dir(&path) -> IoResult<()> // must be empty
fs.remove_dir_all(&path) -> IoResult<()> // recursive
fs.rename(&from, &to) -> IoResult<()>
fs.copy(&from, &to) -> IoResult<Int> // returns bytes copied
fs.hard_link(&src, &dst) -> IoResult<()>
fs.symlink(&src, &dst) -> IoResult<()>
fs.read_link(&path) -> IoResult<PathBuf>
fs.canonicalize(&path) -> IoResult<PathBuf>

fs.read_dir(&path) -> IoResult<ReadDir> // iterator of DirEntry
fs.walk_dir(&path) -> IoResult<WalkDir> // recursive

fs.temp_dir() -> PathBuf
fs.current_dir() -> IoResult<PathBuf>
fs.set_current_dir(&path) -> IoResult<()>

fs.set_permissions(&path, perm: Permissions) -> IoResult<()>

Metadata​

m.len() -> Int
m.is_file() / m.is_dir() / m.is_symlink() -> Bool
m.file_type() -> FileType
m.permissions() -> Permissions
m.modified_secs() -> Int
m.accessed_secs() -> Int
m.created_secs() -> Int
m.is_readonly() -> Bool

FileType​

type FileType is
| File
| Dir
| Symlink
| Unknown;
ft.is_file() -> Bool
ft.is_dir() -> Bool
ft.is_symlink() -> Bool

Note (#io-10): The 4-variant FileType collapses POSIX's 8 types (block / char device / fifo / socket dropped into Unknown). Consumers needing the full set must inspect the platform's file mode bits through its low-level stat interface. The POSIX S_IS* tests describe bit classifications; they do not themselves require linking libc.

Permissions (Unix)​

type Permissions is { mode: Int };

Permissions.from_mode(mode: Int) -> Permissions
p.mode() -> Int // bitwise-masked to PERM_MASK
p.readonly() -> Bool // no write bits set
p.set_readonly(readonly: Bool)

Note (#io-11): No Windows equivalent yet.

DirEntry​

entry.path() -> Path
entry.file_name() -> &Text
entry.file_type() -> FileType
entry.metadata() -> IoResult<Metadata>

WalkDir​

Recursive directory traversal yields IoResult<DirEntry>.


Processes​

type Stdio is Inherit | Piped | Null;

type ExitStatus is { raw: Int };
es.success() -> Bool // exited normally with code 0
es.is_exited() -> Bool // WIFEXITED
es.is_signaled() -> Bool // WIFSIGNALED
es.signal() -> Int // 0 if not signal-terminated
es.code() -> Int // WEXITSTATUS, only meaningful if is_exited()

type Output is { ... };
o.stdout() -> Text
o.stderr() -> Text

type Command is { ... }; // fluent builder
type Child is { ... };

Command​

Command.new(program: Text) -> Command

c.arg(arg: Text) -> &mut Command
c.args(args: &[Text]) -> &mut Command
c.env(key: Text, val: Text) -> &mut Command
c.current_dir(dir: Text) -> &mut Command
c.stdout(cfg: Stdio) -> &mut Command
c.stderr(cfg: Stdio) -> &mut Command
c.stdin(cfg: Stdio) -> &mut Command

c.output() -> Result<Output, Text>
c.status() -> Result<ExitStatus, Text>
c.spawn() -> Result<Child, Text>

Open defect (#io-13): Command returns Result<_, Text> not IoResult<_> — drops structured IoErrorKind information that the rest of core.io carries. Migrate to IoResult<T> in a future release.

Child​

child.id() -> Int
child.wait() -> Result<ExitStatus, Text>
child.write_stdin(data: &[Byte]) -> Result<Int, Text>
child.close_stdin() -> &mut Self
child.signal(sig: Int) -> Result<(), Text>
child.kill() -> Result<(), Text> // SIGKILL
child.terminate() -> Result<(), Text> // SIGTERM

Run convenience​

run(cmd: Text) -> IoResult<Output>

I/O engine (async multiplexer)​

Platform-native async I/O via kqueue (macOS/BSD), epoll (Linux), or IOCP (Windows). Used internally by core.net.* and the long-term async I/O routing path.

type IoEngine is { handle: Int };
type IoEvent is { flags: Int };

const IO_ENGINE_DEFAULT_CAPACITY: Int = 256;
const IO_ENGINE_MAX_CAPACITY: Int = 65536;
const IO_ENGINE_MAX_POLL_EVENTS: Int = 4096;

IoEngine.new() -> IoEngine
IoEngine.with_capacity(capacity: Int) -> IoEngine

IoEvent.Read() -> IoEvent // flags = 1
IoEvent.Write() -> IoEvent // flags = 2
IoEvent.ReadWrite() -> IoEvent // flags = 3 = Read | Write

e.register(fd: Int, event: IoEvent) -> Int
e.deregister(fd: Int) -> Int
e.modify(fd: Int, event: IoEvent) -> Int
e.poll(max: Int, timeout_ns: Int) -> Int
e.is_ready(fd: Int, event: IoEvent) -> Bool
e.take_ready(fd: Int, event: IoEvent) -> Bool
e.destroy()
e.is_valid() -> Bool

Utility types (in protocols.vr)​

type EmptyReader is (); // always 0 bytes
type ByteRepeat is { byte: Byte }; // forever returns the given byte
type Sink is (); // writer that discards all input
type Cursor<T> is { inner: T, pos: Int }; // in-memory Read/Write/Seek

empty_reader() -> EmptyReader
repeat_byte(byte: Byte) -> ByteRepeat
sink() -> Sink

Open defect (#io-2): Cursor<T> is also defined in buffer.vr via the alias Cursor<T> = BufferCursor<T>. First-wins resolution at the mount site silently picks one definition over the other. Tactical fix: import directly from one specific path.

Adapter readers​

type Chain<T: Read, U: Read> is { ... }; // T fully, then U
type Take<T: Read> is { inner: T, limit: Int };

t.limit() -> Int
t.set_limit(limit: Int)
t.get_ref() / t.get_mut() / t.into_inner()

Iterators​

type BytesIter<R: Read>; // Iterator<IoResult<Byte>>
type LinesIter<R: BufRead>; // Iterator<IoResult<Text>>
type SplitIter<R: BufRead>; // Iterator<IoResult<List<Byte>>>

Open defects​

The following defect classes are tracked in core-tests/io/<sub>/audit.md:

TagTitleScope
#io-1Mount-scope-aware lookup_function for method dispatchVBC codegen 3-5 days; gates Read/Write/Seek/BufRead method dispatch on protocols.vr-defined types
#io-2Deduplicate Cursor<T> between protocols.vr and buffer.vrstdlib refactor 0.5 day after #io-1
#io-4PathBuf.push Text-equality drift via nested struct fieldpartial surgical fix landed; deeper VBC codegen work 2-3 days
#io-5Path.file_name trailing-separator char-byte hazardstdlib 0.5 day
#io-7Real exclusive locking for Stdin.lock / Stdout.lock / Stderr.lockstdlib 1-2 days
#io-8Temp-dir / fixture harness in core-testsinfra 1 day
#io-9Windows open-flag mapping (O_BINARY etc.)stdlib 0.5 day
#io-10Decide POSIX 8-type FileType expansionAPI design
#io-11Permissions implementation for Windowsstdlib 1-2 days
#io-12Output.stdout_bytes / stderr_bytes raw accessorsstdlib 0.5 day
#io-13Migrate Command.{spawn,output,status} to IoResult<T>stdlib 1 day
#io-14Full 32-bit Windows exit code in ExitStatusstdlib 0.5 day
#io-15Real async I/O via io_uring / kqueue / IOCPcore.io.engine 1-2 weeks
#io-16Sandboxed IoEngine test harnessinfra 1 day
Windows file I/O is not implemented

The two Windows rows above (#io-9 open-flag mapping, #io-11 permissions) read as finishing touches on a working port. They are not. There is no Windows file layer at all, and the number is easy to check: core/sys/windows/kernel32.vr declares three public fn safe_* wrappers in total, of which exactly one — safe_close — is a file operation. open, read, write, lseek, fstat, fsync, ftruncate and dup have no wrapper to call, and neither core/io/file.vr nor core/io/fs.vr can bind them. file.vr's @cfg(target_os = "windows") mount block binds safe_close, Stat and the flag constants, and nothing else. fs.vr's binds one name, GetFileAttributesW — a raw extern declaration with no body — and then never calls it, so the whole Windows filesystem surface is one mount of one function that nothing reaches.

Everything on this page describing files, directories and paths is therefore Linux and macOS. Path parsing — Path, PathBuf, Prefix, MAIN_SEPARATOR — is pure text manipulation and does work everywhere, which is why the Windows spellings above are real. Opening a file is not.

It is stated here rather than left to be inferred from the gaps table, because a reader who ships to Windows on the strength of #io-9's "0.5 day" estimate will find out at runtime.


Cross-references​

  • net — TCP/UDP sockets implement Read/Write/AsyncRead/AsyncWrite.
  • async — the executor driving async I/O.
  • text — parsing / formatting text read from files.
  • sys — V-LLSI syscalls underlying fs.*.