/
nv-lang
/
nova
Обзор
Документация
Войти
/
nv-lang
/
nova
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
main
std/src/io/core.nv
255 строк
10 KB
Evgeniy Golovin
merge: comment-hygiene-2 — чистка комментариев std (batch 1-20) + линт-свип 330→13
01 авг 2026, 18:13
01 авг 2026, 18:13
bb8b33d
Код
Авторство
О чём код?
// SPDX-License-Identifier: MIT OR Apache-2.0 // std/io/core.nv — byte I/O protocols + std loop helpers. // // The three protocols are the byte-I/O abstraction (Rust `Read`/`Write`/`Seek`, // Go `io.Reader`/`io.Writer`, Zig `std.Io`). They are **effect-agnostic**: a // conformer carries its OWN plumbing effect (`File`→`Fs`, `TcpStream`→`TcpNet`, // console→`Io`) which surfaces transitively at monomorphisation. Generic calls // through an io-bound are **mono-dispatch only** (no vtable for effectful // bounds). // // The loop helpers (`read_exact`/`read_to_end`/`read_to_string`/`write_all`/ // `write_str`/`copy`) are free generic functions over the protocol bounds // (mono-dispatch). The primitive loops live in `read_to_end`/`write_all`; // the rest FORWARD their bounded generic into them — the checker carries a // bound through such a forward. // `Interrupted` (EINTR) is retried inside the two primitive loops. // // **Sibling of the prelude text-sink `Write`** (`mut @write(s str)`): // that one is the `@display`/`Debug` formatting sink; THIS `io.Write` is the byte // sink. Reference this one module-qualified as `io.Write` (`import std.io`); the // text→bytes bridge is the explicit `write_str`. // // EOF / partial / EINTR contract: // - `read` returns `Ok(0)` for EOF **only when the buffer is non-empty**; // a short read (`0 < n < len`) is normal, not EOF. // - `write` may write fewer bytes than asked (partial write is legal); // `write_all` loops. `Ok(0)` mid-write → `WriteZero`. // - `Interrupted` (EINTR) is retried inside the loop helpers. module std.io // 8 KiB read chunk — matches Rust's `DEFAULT_BUF_SIZE`. Large enough to amortise // per-call overhead, small enough to bound transient memory. const DEFAULT_CHUNK = 8192 // ─── Protocols ─── /// Byte source. `@read(buf)` fills `buf[0..n]` and returns `n`. `Ok(0)` signals /// EOF **only** when `buf` is non-empty — never the Go `(n>0, EOF)` footgun. /// The buffer must already have length (e.g. `buf.resize(cap, 0)`); `n <= buf.len()`. #stable(since = "0.1") export type Read protocol { mut @read(buf mut []u8) -> Result[int, IoError] } /// Byte sink. `@write(data)` writes some prefix of `data` and returns the count /// written (a partial write is legal — `write_all` loops). `@flush()` pushes any /// buffered bytes to the underlying device. #stable(since = "0.1") export type Write protocol { mut @write(data []u8) -> Result[int, IoError] mut @flush() -> Result[(), IoError] } /// Random access. `@seek(pos)` moves the cursor and returns the new absolute /// position (a non-negative `int`, i64). `Start(neg)` → `InvalidInput`. #stable(since = "0.1") export type Seek protocol { mut @seek(pos SeekFrom) -> Result[int, IoError] } /// Seek origin. All offsets are `int` (i64, Nova convention). `Start(n)` is /// absolute (from the beginning); `End(n)`/`Current(n)` are relative (may be /// negative). `Start(<0)` (or any resolved negative position) → `InvalidInput`. #stable(since = "0.1") export type SeekFrom enum | Start(int) | End(int) | Current(int) // Static constructors. A payload-variant literal (`SeekFrom.Start(n)`) built in a // DIFFERENT module currently trips a checker gap on the constructor's return // type; these thin static methods build the variant HERE (same module) and are // plain cross-module-callable static functions, so callers write // `io.SeekFrom.start(5)` etc. Pattern-matching on the variants is unaffected. /// Seek to an absolute offset from the start. `Start(<0)` — a legal VALUE; /// the error is deferred to the seek: `InvalidInput` (D322, /// 04-effects.md «Start(<0) → InvalidInput») — NOT a requires-trap at /// construction (wave 185 added the requires against the spec — reverted). #stable(since = "0.1") export fn SeekFrom.start(n int) -> SeekFrom => Start(n) // [M-lint-findings-param-no-contract] /// Seek relative to the end (`n` is usually `<= 0`). #stable(since = "0.1") export fn SeekFrom.end(n int) -> SeekFrom => End(n) // [M-lint-findings-param-no-contract] /// Seek relative to the current position (`n` may be negative). #stable(since = "0.1") export fn SeekFrom.current(n int) -> SeekFrom => Current(n) // [M-lint-findings-param-no-contract] // ─── Read loop helpers ─── /// Read exactly `buf.len()` bytes into `buf`, looping over short reads and /// retrying `Interrupted`. `Ok(0)` before the buffer is full → `UnexpectedEof`. /// Rust `Read::read_exact`. #stable(since = "0.1") export fn read_exact[R Read](mut r R, buf mut []u8) -> Result[(), IoError] { ro total = buf.len() mut filled = 0 mut chunk []u8 = []u8.new() chunk.resize(total, 0 as u8) while filled < total { match r.read(chunk) { Ok(0) => return Err(IoError.new(ErrorKind.UnexpectedEof, "read_exact")) Ok(n) => { for i in 0..n { buf[filled + i] = chunk[i] } filled += n chunk.resize(total - filled, 0 as u8) } Err(e) => { if e.is_interrupted() { continue } return Err(e) } } } Ok(()) } /// Read to EOF, returning all bytes. Grows a `[]u8`; retries `Interrupted`. /// Rust `Read::read_to_end`. #stable(since = "0.1") export fn read_to_end[R Read](mut r R) -> Result[[]u8, IoError] { mut out []u8 = []u8.new() mut chunk []u8 = []u8.new() chunk.resize(DEFAULT_CHUNK, 0 as u8) loop { match r.read(chunk) { Ok(0) => return Ok(out) Ok(n) => out.append(chunk[..n]) Err(e) => { if e.is_interrupted() { continue } return Err(e) } } } } /// Read to EOF and decode as UTF-8. Invalid UTF-8 → `IoError{InvalidData}` /// (fallible, no silent `U+FFFD` like Node). Uses `[]u8 @to_str()`. /// Forwards to `read_to_end` (bound carries). #stable(since = "0.1") export fn read_to_string[R Read](mut r R) -> Result[str, IoError] { match read_to_end(r) { Ok(out) => match out.to_str() { Ok(s) => Ok(s) Err(_) => Err(IoError.new(ErrorKind.InvalidData, "read_to_string")) } Err(e) => Err(e) } } // ─── Write loop helpers ─── /// Write the whole of `data`, looping over partial writes and retrying /// `Interrupted`. `Ok(0)` mid-write → `WriteZero`. Rust `Write::write_all`. #stable(since = "0.1") export fn write_all[W Write](mut w W, data []u8) -> Result[(), IoError] { ro total = data.len() mut done = 0 while done < total { ro rest = data[done..total] match w.write(rest) { Ok(0) => return Err(IoError.new(ErrorKind.WriteZero, "write_all")) Ok(n) => { done += n } Err(e) => { if e.is_interrupted() { continue } return Err(e) } } } Ok(()) } /// Write a `str` to a byte sink as UTF-8 — the explicit bridge from text to the /// byte world (no implicit coupling with the `@display` text-sink). /// Forwards to `write_all` — the bound is carried through the generic forward. #stable(since = "0.1") export fn write_str[W Write](mut w W, s str) -> Result[(), IoError] => write_all(w, s.bytes()) // ─── Read → Write ─── /// Copy the whole reader into the writer, returning the total bytes moved. /// Reads a chunk, writes it fully (via `write_all` — the bound forwards), /// repeats. Rust `io::copy`. #stable(since = "0.1") export fn copy[R Read, W Write](mut r R, mut w W) -> Result[int, IoError] { mut moved = 0 mut chunk []u8 = []u8.new() chunk.resize(DEFAULT_CHUNK, 0 as u8) loop { match r.read(chunk) { Ok(0) => return Ok(moved) Ok(rn) => { // Write exactly the rn bytes just read (partial writes and // EINTR are handled inside write_all). match write_all(w, chunk.first_n(rn)) { Ok(_) => { moved += rn } Err(e) => return Err(e) } } Err(e) => { if e.is_interrupted() { continue } return Err(e) } } } } // ─── Line splitting ─── /// Read to EOF, decode as UTF-8, and split into lines (Rust `BufRead::lines`). /// Each line excludes its terminator; a trailing `\r` (of `\r\n`) is stripped; a /// final line without a newline is yielded; an embedded lone `\r` is NOT a /// separator (delegates to `str.@lines`). Invalid UTF-8 → /// `IoError{InvalidData}`. Forwards to `read_to_end` (bound carries through). #stable(since = "0.1") export fn lines[R Read](mut r R) -> Result[[]str, IoError] { match read_to_end(r) { Ok(out) => match out.to_str() { Ok(s) => Ok(s.lines()) Err(_) => Err(IoError.new(ErrorKind.InvalidData, "lines")) } Err(e) => Err(e) } } /// Read to EOF and split the RAW bytes on `\n` (0x0A) — no UTF-8 decode, no `\r` /// stripping (`byte_lines`). The terminator is not included; a final segment /// without a trailing `\n` is yielded. Forwards to `read_to_end` (bound carries /// through). #stable(since = "0.1") export fn byte_lines[R Read](mut r R) -> Result[[][]u8, IoError] { match read_to_end(r) { Ok(data) => { // Срез-вид: границы строк находятся сканом на `\n`, каждая // строка — один slice вместо поэлементного push-накопления. ro n = data.len() mut out [][]u8 = [] mut line_start = 0 for i in 0..n { if (data[i] as int) == 10 { out.push(data[line_start..i]) line_start = i + 1 } } if n > line_start { out.push(data[line_start..n]) } Ok(out) } Err(e) => Err(e) } }