/
nv-lang
/
nova
Обзор
Документация
Войти
/
nv-lang
/
nova
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
main
std/src/runtime/string/chars.nv
163 строки
8 KB
Evgeniy Golovin
docs(std/src): clean comments batch 5 — runtime/string core, parse, chars
31 июл 2026, 20:26
31 июл 2026, 20:26
aa88dfc
Код
Авторство
О чём код?
// Co-equal file of module `runtime.string` (folder = one module). // Role: codepoint layer — `CharsIter` decoding lens + `CharIndicesIter` // (byte_offset, char) pairs. Legacy positional access (`char_at`/`get`) is // retired — use `for c in chars()` / `.indices()`. // // `str` Nova-body methods. #no_prelude breaks the prelude->string->prelude // import cycle. #no_prelude module runtime.string import std.prelude.core.{Option, Some, None} // Module-private: decode one UTF-8 codepoint at bytes[i]. // Returns (codepoint, step_bytes). Assumes valid UTF-8. // On invalid lead byte / truncated tail: returns (U+FFFD, 1) — Unicode canon // (Rust from_utf8_lossy, Go range-over-string); повреждение ВИДИМО, а не // маскируется под легальный символ. // runtime.string decode; std.unicode has decode_at (intentional per-module copy). fn decode_utf8(bytes []u8, i int, n int) -> (int, int) { ro b = bytes[i] as int if b < 0x80 { return (b, 1) } if (b & 0xE0) == 0xC0 && i + 1 < n { return (((b & 0x1F) << 6) | (bytes[i+1] as int & 0x3F), 2) } if (b & 0xF0) == 0xE0 && i + 2 < n { return (((b & 0x0F) << 12) | ((bytes[i+1] as int & 0x3F) << 6) | (bytes[i+2] as int & 0x3F), 3) } if (b & 0xF8) == 0xF0 && i + 3 < n { return (((b & 0x07) << 18) | ((bytes[i+1] as int & 0x3F) << 12) | ((bytes[i+2] as int & 0x3F) << 6) | (bytes[i+3] as int & 0x3F), 4) } (0xFFFD, 1) } // ─── CharsIter — codepoint decoding lens ─── // // `chars()` is a DECODING lens (not a reinterpretation like `bytes()`): // codepoints are not stored contiguously — they are decoded on the fly. So the // honest primitive is a STREAM (iterator), not a collection: there is no cache, // `nth(i)`/`count()` are each O(n), and exposing positional `at(i)`+`len()` // would invite the `for i in 0..len { at(i) }` = O(n²) footgun that we removed // from `str[i]` (invariant I2 "no hidden O(n)"). Mirrors Rust `str::chars()`. // // Field-level `priv` (module-boundary): the iterator // holds `buf` (the source str, an alias of the same UTF-8 buffer — // conservative GC sees `ptr` through the str field and keeps the buffer // alive) + `pos` (current byte offset). No priv-poking, no `unsafe`: // decoding reads bytes via the public zero-copy `buf.bytes()` view. // // `value` = stack-allocated (no per-iteration heap churn). CharsIter is the first // general value-record iterator; it needs the for-in codegen support for // `NovaValue_*` iterator-struct derivation + by-pointer `&it` for // `mut @next()`; value-record self-return deref for `@iter() => @`. export type CharsIter value { priv buf str // borrows str (alias of the buffer; GC sees ptr through the field) priv pos int // current byte offset into buf } // Module-private constructor. A static method on `CharsIter` has // `current_recv_type == "CharsIter"` → it may initialise the `priv` fields // (type-based privacy). `str @chars()` (receiver `str`) // cannot init CharsIter's priv fields directly, so it routes through here — // same pattern as `str.alloc_copy` constructing `str{...}`. fn CharsIter.new(buf str) -> CharsIter => { buf, pos: 0 } // `Next[char]`: decode the codepoint at `buf[pos]`, advance `pos` by its UTF-8 // byte length, return it. `None` when exhausted (`pos >= byte_len`). Amortised // O(1). Reads bytes via the public `@buf.bytes()` view, no `unsafe`. // The source str is always valid UTF-8, so the decode is // total — no malformed-sequence branch is reachable (the `else` arm only guards // against a truncated tail and degrades to a single-byte step). #impl(Next[char]) export fn CharsIter mut @next() -> Option[char] { ro (bytes, n) = (@buf.bytes(), @buf.byte_len()) if @pos >= n { return None } ro (cp, step) = decode_utf8(bytes, @pos, n) @pos += step Some(cp_to_char(cp)) } // `Iter` (self-iterator): a CharsIter IS its own iterator, so `for c in it` and // `it.iter()` route through the single for-in path (D58). Trivial `=> self`. export fn CharsIter @iter() -> CharsIter => @ // Number of codepoints remaining from `pos` to the end. O(n). Counts UTF-8 // leading bytes (non-continuation: `(b & 0xC0) != 0x80`) without decoding — // identical count to walking `next()` to exhaustion, but cheaper. On a fresh // `s.chars()` (pos == 0) this is the full codepoint length of `s`. export fn CharsIter @count() -> int { ro (bytes, n) = (@buf.bytes(), @buf.byte_len()) mut count = 0 for i in @pos..n { ro b = bytes[i] as int if (b & 0xC0) != 0x80 { count += 1 } } count } // `@nth(idx int) -> Option[char]` — RETRACTED: a non-mut positional scan from // the current `pos` would be the reintroduced-forbidden codepoint `s[i]` // (`E_STR_NO_INT_INDEX`) wearing an iterator-method costume — O(n) per // call, O(n²) in a loop. Callers migrate to target iteration: `for c in // s.chars()`, `.indices()` for (offset, char) pairs, or `.skip(n)` + // `.next()`-style composition on adapters that support it. // True if no codepoints remain. O(1). (`pos >= byte_len` ⇔ exhausted, since a // valid UTF-8 buffer has no trailing continuation-only tail.) export fn CharsIter @is_empty() -> bool => @pos >= @buf.byte_len() // ─── str codepoint lenses ─── // Lazy codepoint iterator over the string (borrows `self`; O(1) to create). // The codepoint layer of the lens model: `s.chars().count()` for codepoint // length, `for c in s.chars()` (or just `for c in s`) to walk (positional // `.nth(i)` was retracted — use `for`/`.indices()` instead). // Bare noun = O(1) view/lens (borrows); `.collect()` at the call site // materialises an owned `[]char` (`s.chars().collect()`). export fn str @chars() -> CharsIter => CharsIter.new(@) // `Iter` for str: `s.iter()` => `s.chars()`, so `for c in s` decodes // codepoints (D58 amend). Single entry point — no separate byte/char ambiguity: // the char lens is the default iteration unit (matching Go `for range`, Rust // `.chars()` when spelled, Swift `for c in s`). export fn str @iter() -> CharsIter => @chars() // Raw int codepoints without the char wrapper — for low-level protocols/tables. // Values are identical to `@chars()` cast to int. // `"a😀".to_code_points() == [0x61, 0x1F600]`. export fn str @to_code_points() -> []int { mut out []int = []int.new(cap: @byte_len()) for c in @chars() { out.push(c as int) } out } // ─── CharIndicesIter — (byte_offset, char) pairs ─── // // `chars().indices()` adapts CharsIter into a stream of (byte-offset, char) // pairs. `off` is the byte index of the *start* of the codepoint — compatible // with the byte-range slice `s[off..]` / `s[prev..off]`. Mirrors Rust // `str::char_indices()`. Same field-level `priv` pattern as CharsIter. export type CharIndicesIter value { priv buf str priv pos int } // Adapter constructor on CharsIter: captures buf+pos at the point of call, so // the resulting iterator starts from wherever CharsIter is currently positioned. export fn CharsIter @indices() -> CharIndicesIter => { @buf, @pos } // Yield the next (byte_offset, char) pair. `off` is captured before advancing // so it points to the first byte of the returned codepoint. export fn CharIndicesIter mut @next() -> Option[(int, char)] { ro bytes = @buf.bytes() ro n = @buf.byte_len() if @pos >= n { return None } ro off = @pos ro (cp, step) = decode_utf8(bytes, @pos, n) @pos += step Some((off, cp_to_char(cp))) } // Self-iterator — `for (i, c) in s.chars().indices()` routes through here. export fn CharIndicesIter @iter() -> CharIndicesIter => @