/
nv-lang
/
nova
Обзор
Документация
Войти
/
nv-lang
/
nova
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
main
std/src/runtime/string/core.nv
376 строк
19 KB
Evgeniy Golovin
fix(std): is_char_boundary — repair broken nova:allow (W_PARAM_NO_CONTRACT)
07 авг 2026, 08:12
07 авг 2026, 08:12
0d2242b
Код
Авторство
О чём код?
// Co-equal file of module `runtime.string` (folder = one module). // Role: identity / length / bytes / conversions / constructors / low-level helpers. // // `str` Nova-body methods. #no_prelude breaks the prelude->string->prelude // import cycle. #no_prelude module runtime.string import std.prelude.core.{Result, Ok, Err} import std.collections.vec.{Vec} import std.runtime.raw_mem.{RawMem} // `panic` used below is an always-available compiler intrinsic — no // redeclaration needed even under `#no_prelude`. // Длина строки в БАЙТАХ. O(1). Единственный length-метод // на самом `str` — bare `@len()` / `@char_len()` ретайрнуты (lens-модель; bare // `s.len()` снаружи → E_STR_NO_LEN). codepoint-длина — `s.chars().count()` // (O(n)). Nova-body — читает priv-поле `@len` напрямую (str — declared lang-item; // receiver `str` ⇒ type-method privacy ⇒ видит priv-поля; bare // self-field-read `@len` НЕ ловится E_STR_NO_LEN — он для НЕ-self receiver'ов). // str.len = bytes. export fn str @byte_len() -> int => @len // `@char_len()` (codepoint count) and `@byte_at(i)` (byte // access) are RETIRED in favour of the lens model — codepoint count is // `s.chars().count()` (O(n), explicit), byte access is `s.bytes()[i]` // (O(1), bounds-checked). Bare `s.char_len()` / `s.byte_at(i)` → E_STR_NO_LEN. // True если строка пустая. O(1) через @byte_len (байты). export fn str @is_empty() -> bool => @byte_len() == 0 // Канонический дефолт. Возвращает пустую строку. export fn str.new() -> Self => "" // Convert a known-valid codepoint int → char. Return type `char` drives the // Ok-arm coercion (mirrors std/encoding/base64.nv encode_char). Invalid cp // cannot occur here — каждый cp получен из UTF-8 decode валидной строки. fn cp_to_char(cp int) -> char { cp.to_char() ?? '\u{FFFD}' } // Magic-методы @eq/@lt/@le/@gt/@ge удалены; `str.@compare` даёт все 6 // операторов через synthesis. // SipHash-1-3 + per-process random seed (DoS-resistant); используется в // std.collections.HashMap. Неустранимая C-граница (security) — // seed (nova_hash_seed_k0/k1, getrandom/BCryptGenRandom) НЕ экспонируется на // Nova-сторону; перенос в Nova = hash-flooding/DoS-регрессия HashMap. См. // nova_rt.h (nova_str_hash/nova_siphash13). export extern "nova" fn str @hash() -> u64 // `@to_bytes()` (allocating-copy twin of `@bytes()`) RETRACTED — // its exact meaning is `@bytes().clone()`, spelled explicitly at the call site now. // Zero-copy view of str UTF-8 bytes as readonly []u8 (no memcpy). // Nova-body. `str` is a declared lang-item with // priv fields `ptr *u8` / `len int`; this method's receiver is `str`, so it has // type-method priv access (privacy is type-based, not module-based) and can read // `@ptr` / `@len` (the fields) directly. The zero-copy view is built via the // 2-arg VIEW-overload `[]u8.new(ptr *u8, len)`: `unsafe // fn`, `cap == len`, `-> ro` — call-сайт несёт unsafe-обязательство (источник // переживает view), каст в *mut спрятан в new, а `ro []u8`-возврат запрещает // любую запись через view, сохраняя иммутабельность str-буфера. // `@len` is spelled `@len()` (the method, = the `len` // field, O(1)): the bare field read `@len` trips E_SIZE_ACCESSOR_FIELD — // size-like accessors are method-only. `@ptr` is not a size accessor, so the // direct priv-field read is allowed. // `#coerce`: declares the implicit view-lane pair `str → ro // []u8`. Any str // VALUE (not just a literal) now coerces at a call-arg/let-const/return/ // element `[]u8` position via this exact method, byte-in-byte identical to // the explicit `.bytes()` call it replaces. #coerce export fn str @bytes() -> ro []u8 => unsafe { []u8.new(@ptr, @byte_len()) } // Raw pointer to the first byte of the UTF-8 data (like Rust `str::as_ptr`). // The pointer is valid for @byte_len() bytes. Caller must not write through it // (str buffer is immutable) and must not retain past the str lifetime. // Primarily for `extern "C" fn` FFI that takes `const uint8_t* / size_t` pair. export fn str @ptr() -> *u8 => @ptr // Module-private: is the byte at `b` a UTF-8 continuation byte (0b10xxxxxx)? fn is_cont(b int) -> bool => (b & 0xC0) == 0x80 // Module-private: return the byte offset of the FIRST ill-formed UTF-8 sequence // in `bytes[0..n]`, or `-1` if the whole range is well-formed UTF-8. The offset // points at the lead byte of the bad sequence (or, for an incomplete trailing // sequence, at its lead byte). Overlong / surrogate / out-of-range sequences are // rejected. Powers both the bool // `validate_utf8` predicate (lossy fast-path pick) and the typed // `str.from_bytes` decode (`Utf8Error{byte_offset}`). fn first_invalid_utf8(bytes []u8, n int) -> int { mut i = 0 while i < n { ro c = bytes[i] as int mut seq = 0 if c < 0x80 { seq = 1 } else if (c & 0xE0) == 0xC0 && c >= 0xC2 { seq = 2 } else if (c & 0xF0) == 0xE0 { seq = 3 } else if (c & 0xF8) == 0xF0 && c <= 0xF4 { seq = 4 } else { return i } // Continuation bytes present + well-formed. for k in 1..seq { if i + k >= n || !is_cont(bytes[i + k] as int) { return i } } // Overlong / surrogate / range guards (mirror C lossy validator). if seq == 3 { if c == 0xE0 && (bytes[i+1] as int) < 0xA0 { return i } if c == 0xED && (bytes[i+1] as int) >= 0xA0 { return i } } if seq == 4 { if c == 0xF0 && (bytes[i+1] as int) < 0x90 { return i } if c == 0xF4 && (bytes[i+1] as int) >= 0x90 { return i } } i += seq } -1 } // Module-private: is `bytes[0..n]` well-formed UTF-8? Thin predicate over // `first_invalid_utf8` (single source of truth for the validation rules). Used // by @from_bytes_lossy to pick the copy-only fast path vs the FFFD-replace path. fn validate_utf8(bytes []u8, n int) -> bool => first_invalid_utf8(bytes, n) < 0 // Module-private str static method: build a freshly-allocated str by copying // `n` bytes from the raw source pointer `src` (`*u8`). The buffer is EXACTLY // `n` bytes — no trailing NUL. `str` is a pure `ptr[len]` view; C-FFI goes // through the explicit copy-based `str.to_cstr()` (std/ffi/cstr.nv), so no str // buffer reserves a terminator. The `*mut u8` buffer is re-labelled to the str // field's `*u8` (ro pointee) on construction — narrowing, always safe. The // returned str OWNS its buffer (no aliasing of the source). // // Declared as a `str.` STATIC method (not a free fn) so its receiver type is // `str` → `current_recv_type == "str"` → it may construct the `str{...}` // value-record (type-based privacy). A free fn has no receiver // and would trip E_PRIV_FIELD_INIT. // Контракт vs fast-path: `n < 0` = баг вызывающего → `requires` (trap), НЕ // тихое прощение; `n == 0` — легальный вход → fast-path `""` без аллокации. fn str.alloc_copy(src *u8, n int) -> Self requires n >= 0 { if n == 0 { // Empty string — interned literal, no allocation needed. return "" } ro buf = unsafe { RawMem.alloc(n) } unsafe { RawMem.copy_nonoverlapping(src, buf, n) } str { ptr: buf, len: n } } // []u8 → str conversions are METHODS ON THE SOURCE (`[]u8 @to_str()`/ // `@to_str_lossy()`/`@to_str_unchecked()`/`@into_str_unchecked()`), mirroring // the numeric `str @to_int()` family — one door for byte → string conversion. // Module-private ctor `str.new(buf, len)` (арность-сиблинг `str.new()`; // канон «конструктор-поглотитель = Type.new»). Обёртывает уже owned-буфер // как str-значение (zero-copy, без аллокации/копии). Receiver `str` ⇒ // type-based privacy, чтобы собрать `str{...}` напрямую (тот же приём, что // `str.alloc_copy` выше) — нужно, потому что `[]u8 @into_str_unchecked()` // (ниже) имеет receiver `[]u8`, а не `str`, и не может собрать литерал сам. // Параметр `*u8`: поле `str.ptr` — ro-pointee, mut-каст был лишним. // [M-d216-unsafe-map-single-file-gaps] обход: unsafe-атрибут отложен — карта // unsafe-энфорса ключует статики (тип,имя) БЕЗ арности, атрибут зацепил бы и // safe 0-арг `str.new()`; добавить атрибут ВМЕСТЕ с фиксом карты. fn str.new(buf *u8, len int) -> Self { str { ptr: buf, len } } // Unchecked: no validation, O(n) copy. Caller guarantees valid UTF-8. // Reads (ptr,len) via the public Vec getters and copies into a fresh owned // buffer (alloc_copy). export unsafe fn []u8 @to_str_unchecked() -> str { str.alloc_copy(@ptr(), @len()) } // Lossy: replaces invalid UTF-8 sequences with U+FFFD. Always succeeds. // Fast path — when the whole input is valid UTF-8, just copy (alloc_copy). // Slow path — emit each valid sequence verbatim and replace each invalid lead // byte with the 3-byte U+FFFD (0xEF 0xBF 0xBD), advancing 1 byte. export fn []u8 @to_str_lossy() -> str { ro n = @len() // FAST-PATH (перф, НЕ валидация — медленный путь ниже самодостаточен и сам // чинит FFFD): валидный вход (типовой) = один скан + один memcpy, вместо // цикла + cap(n*3)-над-аллокации + второй копии. Rust-парити from_utf8_lossy. if validate_utf8(@, n) { return str.alloc_copy(@ptr(), n) } // Worst case every byte → 3-byte FFFD. mut out []u8 = []u8.new(cap: n * 3) mut i = 0 while i < n { ro c = @[i] as int mut seq = 0 if c < 0x80 { seq = 1 } else if (c & 0xE0) == 0xC0 && c >= 0xC2 { seq = 2 } else if (c & 0xF0) == 0xE0 { seq = 3 } else if (c & 0xF8) == 0xF0 && c <= 0xF4 { seq = 4 } mut valid = seq > 0 if valid { mut k = 1 while k < seq && valid { if i + k >= n || !is_cont(@[i + k] as int) { valid = false } k += 1 } } if valid && seq == 3 { if c == 0xE0 && (@[i+1] as int) < 0xA0 { valid = false } else if c == 0xED && (@[i+1] as int) >= 0xA0 { valid = false } } if valid && seq == 4 { if c == 0xF0 && (@[i+1] as int) < 0x90 { valid = false } else if c == 0xF4 && (@[i+1] as int) >= 0x90 { valid = false } } if valid { for k in 0..seq { out.push(@[i + k]) } i += seq } else { out.push(0xEF as u8) out.push(0xBF as u8) out.push(0xBD as u8) i += 1 } } // Zero-copy кража буфера: `out` валиден по построению — `alloc_copy` // платил бы ВТОРУЮ полную копию; `into_str_unchecked` забирает буфер // как есть (cap-хвост висит на той же GC-аллокации). unsafe { out.into_str_unchecked() } } // Steal variant: consume []u8, zero-copy reuse of the data ptr. `consume` // receiver — the []u8 is unusable after the call. // // A `str` reserves no trailing NUL, so there is nothing to write past `len` // and no `cap > len` room requirement. The stolen buffer is ALWAYS reused in // place (`@ptr()` + `str.new(buf, len)`), regardless of capacity. `str` is a // pure `ptr[len]` view over the consumed bytes; ownership transfer lives in // this consume-receiver (the pointer is taken via the plain `@ptr()`). export unsafe fn []u8 consume @into_str_unchecked() -> str { ro n = @len() // Consume self discharges ownership; the raw buffer pointer wraps directly. // `ro`-биндинг → ro-оверлоад `@ptr() -> *u8` — точный тип параметра // `str.new(buf *u8, len)` (arg-binding не сужает *mut→*ro при подборе // оверлоада). ro buf = @ptr() str.new(buf, n) } // Fallible byte→str decode. // /// UTF-8 decode error for `[]u8.to_str()`: the input `[]u8` was not well-formed /// UTF-8. `byte_offset` is the index of the first byte where decoding failed — /// the lead byte of the ill-formed sequence, or the lead byte of an incomplete /// trailing sequence. Sibling of `encoding.utf16.Utf16Error`; homed here /// in `runtime.string` next to the `to_str*` family and `InvalidCodepoint`, /// so the `#no_prelude` core needs no cross-module `encoding` import (mirrors the /// `InvalidCodepoint` precedent — str-conversion errors live with `str`). export type Utf8Error value { ro byte_offset int } /// Checked `[]u8` → `str` (fallible UTF-8 decode). Returns `Ok(str)` when the /// bytes are well-formed UTF-8, else `Err(Utf8Error{byte_offset})` locating the /// first ill-formed byte — the `str` is NOT constructed on the error path /// (invariant: every `str` is valid UTF-8). /// /// Canonical form: the ordinary name `to_str` returns `Result` and pairs, /// by symmetry, with the infallible `to_str_unchecked`/`into_str_unchecked` /// and the always-succeeding `to_str_lossy`. Backs `io.read_to_string`. /// Precedents: Rust `str::from_utf8`, Go `utf8.Valid` + cast, Zig /// `utf8ValidateSlice`. export fn []u8 @to_str() -> Result[str, Utf8Error] { ro n = @len() ro off = first_invalid_utf8(@, n) if off < 0 { return Ok(str.alloc_copy(@ptr(), n)) } Err(Utf8Error { byte_offset: off }) } // Один публичный вход «скаляр → строка» — `@to_str()`. Bare-T blanket // (зеркалит `fn[T] T @identity() -> T => @`): "строка из значения = подставить // значение в интерполяцию". Инвариант рекурсии: `"${@}"` для ПРИМИТИВА // лоуэрится ПРЯМО в Display-хелпер (`nova_int_to_str`/`nova_f64_to_str`/…) — // НЕ через `.to_str()` снова, цикла нет. Для НЕ-примитива `"${@}"` идёт через // Display.@display (explicit или auto-derive) — тоже не через `.to_str()`. // Инвариант специализации: конкретный `T @to_str()` (напр. `char` ниже, // `[]u8 @to_str() -> Result[str, Utf8Error]` выше — другая arity/семантика, // decode а не format) ВСЕГДА побеждает этот blanket по receiver-типу — // blanket применяется только когда конкретной перегрузки на T нет. export fn[T] T @to_str() -> str => "${@}" // `@to_chars()` (materializing-copy twin of `@chars()`) RETRACTED — its exact // meaning is `@chars().collect()`, spelled explicitly at the call site now. // Lexicographic comparison, like C strcmp. Returns negative/0/positive. // Nova-body — byte-loop над @bytes() обоих операндов (как C strcmp / memcmp), // length-aware tiebreak. Возвращает (a_byte - b_byte) на первом различии // (u8 0..255 ⇒ разность даёт тот же знак, что memcmp), затем при равенстве // префикса — знак (@len() - other.len()). Метод вызываем напрямую // (`s.compare(t)`) и Compare-протоколом (`@compare(o) == 0` synthesis). // ОПЕРАТОРЫ `<`/`<=`/`>`/`>=` синтезируются из `@compare` // (`Nova_str_method_compare(l,r) OP 0`), `==`/`!=` из `@equal`, `+` из `@concat`. export fn str @compare(other str) -> int { ro (an, bn) = (@byte_len(), other.byte_len()) ro min = an.min(bn) // RawMem.compare (= memcmp) over the shared prefix instead of a byte-loop — // perf-parity with the C form, so the synthesized `<`/`<=`/`>`/`>=` // operators carry no penalty. `@ptr`/`other.ptr` are priv `*ro u8` // (type-method privacy, str's home module, same as @starts_with). memcmp // result is sign-correct (magnitude irrelevant for Ord); length-tiebreak // on a shared prefix. ro c = unsafe { RawMem.compare(@ptr, other.ptr, min) } if c != 0 { return c } if an < bn { return -1 } if an > bn { return 1 } 0 } // Content equality. Backs the `==`/`!=` operators // (`Nova_str_method_equal`) and the `Equal` protocol. // Length-first short-circuit (unequal length ⇒ not equal, O(1)) then // RawMem.compare (= memcmp) over the bytes. `@ptr`/`other.ptr` are priv // `*ro u8` (type-method privacy, str's home module). export fn str @equal(other str) -> bool { ro n = @byte_len() if n != other.byte_len() { return false } unsafe { RawMem.compare(@ptr, other.ptr, n) == 0 } } // True если `idx` — на границе codepoint'а (Rust `is_char_boundary`): idx==0, // idx==byte_len, или байт по idx НЕ continuation (`(b & 0xC0) != 0x80`). Для // безопасной нарезки: `s[a..b]` паникует при рассечении, `is_char_boundary` — // предварительная проверка без паники. O(1). // nova:allow W_PARAM_NO_CONTRACT -- намеренный total-предикат над ЛЮБЫМ idx (в т.ч. отрицательным/за-концом): именно на невалидном входе он обязан вернуть false, а не потребовать его валидность заранее — `requires` тут убил бы саму цель функции (safe pre-check перед `s[a..b]`) export fn str @is_char_boundary(idx int) -> bool { ro n = @byte_len() if idx == 0 || idx == n { return true } if idx < 0 || idx > n { return false } ro bytes = @bytes() (bytes[idx] as int & 0xC0) != 0x80 } // Checked codepoint → str conversion. // `str.from_codepoint` (char.nv) is unchecked — invalid cp → silent empty str. // `str.try_from_codepoint` validates via `char.from` and returns a Result. /// Error type for `str.try_from_codepoint`: the given int is not a valid Unicode scalar value. export type InvalidCodepoint enum InvalidCodepointError /// Checked codepoint → str. Returns `Err(InvalidCodepointError)` for surrogates, /// negative values, and values > 0x10FFFF. Valid codepoints map to a 1–4-byte str. export fn str.try_from_codepoint(cp int) -> Result[str, InvalidCodepoint] { match cp.to_char() { Ok(c) => Ok(c.to_str()) Err(_) => Err(InvalidCodepointError) } } // Module-private: ASCII lowercase of a single byte (A-Z → a-z; else unchanged). fn ascii_lower_byte(b int) -> int => if b >= 65 && b <= 90 { b + 32 } else { b } // ASCII case-insensitive equality. Без Unicode-таблиц: только // A-Z/a-z складываются. Длины должны совпадать (ASCII-case не меняет байт-длину). // Rust `str::eq_ignore_ascii_case`. Для Unicode case-folding — `eq_ignore_case` // (делегат std/unicode). export fn str @eq_ignore_ascii_case(other str) -> bool { ro n = @byte_len() if n != other.byte_len() { return false } ro (a, b) = (@bytes(), other.bytes()) for i in 0..n { if ascii_lower_byte(a[i] as int) != ascii_lower_byte(b[i] as int) { return false } } true }