/
nv-lang
/
nova
Обзор
Документация
Войти
/
nv-lang
/
nova
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
main
std/src/runtime/fmt_buf/core.nv
471 строка
21 KB
Evgeniy Golovin
style(std): D452 — migrate to canonical match-arm/statement separators
10 авг 2026, 03:57
10 авг 2026, 03:57
685589a
Код
Авторство
О чём код?
// SPDX-License-Identifier: MIT OR Apache-2.0 // // std.runtime.fmt_buf — buffer-primitives for int/bool/char/radix/width // formatting. `${x}` interpolation and the `@display`/`@debug` protocol go // through the unified `FmtCtx` dispatch; primitive formatting renders // directly into caller-owned buffers (zero-alloc) through the functions in // this module. Exercised for now only by the inline `test { }` blocks at the // end of this file — a separate `fmt_buf_test.nv` peer would be a genuinely // different module, forcing these to be exported (module-privacy). // // Scope: // - `int_fmt`/`bool_fmt`/`char_fmt` — zero-alloc digit-loop / UTF-8 encode // directly into a caller-owned `*mut u8` buffer (no `nova_alloc`, no GC // pressure) — the `.nv` replacement for the equivalent int/bool/char // helpers in `conv.h` (`nova_fmt_int_body`/`nova_bool_to_str`/ // `nova_char_to_str`). // - `nova_f64_fmt` — the ONE C-extern this design keeps (dtoa/shortest- // round-trip is not portably expressible in `.nv`); literal-name // `extern "C" fn`, body in `nova_rt/nova_rt.h` (next to the existing // `nova_f64_shortest`/`nova_f32_shortest` it reuses). `f64_fmt` is the // `.nv`-side wrapper converting the `FloatKind` enum to the `int` the // C-ABI boundary carries (enums do not cross `extern "C"` directly). // - `Align`/`FloatKind` — the enum-marker types; `Align` // is exported here because `StringBuilder.@pad_in_place`/`@write_padded` // (`std/src/runtime/string_builder.nv`) take it directly. // `Sign`/`FmtKind` are NOT introduced here — those are `Fmt`-protocol axis // types. // // Everything except `Align`/`FloatKind` stays module-private (no `export`) — // "буфер-примитивы — ВНУТРЕННИЕ (.nv, не публичные)". // // Family norm: every `*_fmt` function (`int_fmt`/`bool_fmt`/`char_fmt`/ // `f64_fmt`/`f32_fmt`) // renders INTO a caller-owned `(buf, cap)` — writing-into-buffer is the // family's DEFINITION, not a suffix. There is no `_into` variant anywhere in // this family: a "simple render" is the SAME function // called with its axis parameters (`spec`/`kind`+`prec`) omitted, resolved // through ordinary free-fn default-arg backfill (`callnorm.rs`), not a // separate bridge function. // // `#no_prelude`: this module needs nothing from the default prelude (only // builtin primitive types + pointer intrinsics) and `Align` is imported by // `runtime.string_builder`, which already breaks the `prelude → collections → // string_builder` cycle via its own `#no_prelude` — staying prelude-free here // avoids re-opening that cycle through `fmt_buf → (default prelude) → … → // string_builder → fmt_buf`. #no_prelude module runtime.fmt_buf // ─── Enums ─────────────────────────────────────────────────────── /// Padding alignment axis — shared by `StringBuilder.@pad_in_place`/ /// `@write_padded` today, and by the `Fmt` protocol's /// `@align()` axis. #unstable export type Align enum Left | Right | Center /// Which float rendering `f64_fmt` should produce. Crosses the /// `extern "C" fn nova_f64_fmt` ABI boundary as a plain `int` /// (0=Shortest/1=Fixed/2=Sci) — this enum is the `.nv`-side /// convenience the `f64_fmt` wrapper below converts at the boundary. #unstable export type FloatKind enum Shortest | Fixed | Sci /// The `Fmt` protocol's `@sign()` axis — whether /// a non-negative numeric value forces a leading `+` (`{:+}` Rust parity). /// Lives here (not `prelude.protocols`) so it sits next to `Align`/`FloatKind` /// — one home for every format-axis enum. #unstable export type Sign enum Minus | Plus /// The `Fmt` protocol's `@kind()` axis — which /// representation `${x:SPEC}` requested. `Display`/`Debug` are the two a /// type's own `@display`/`@debug` body is invoked under today; `Hex`/`Oct`/ /// `Bin`/`Exp` are reserved for when radix/exponential specs route through /// `@kind()` instead of the compiler's direct fast path for primitives. #unstable export type FmtKind enum Display | Debug | Hex | Oct | Bin | Exp // ─── FmtSpec — internal spec-slice for `int_fmt` ─── // // Deliberately narrow: only the axes `int_fmt`'s digit-loop needs (radix, // zero-pad-to-width, forced `+`, alternate `0x`/`0o`/`0b` prefix). This is // NOT the compiler-facing `FormatSpec`/`Fmt` — no `width` meaning // beyond zero-pad target, no fill/precision (those live one layer up, at // the `write_padded`/`pad_in_place` StringBuilder level). // `value`: маленький POD (2×int + 4×bool) — на стеке, // БЕЗ GC-заголовка на каждый вызов рендера (последняя лишняя аллокация // zero-alloc-пути; value-семантика, structural ==). export type FmtSpec value { width int // zero-pad target total width (prefix+digits); only // consulted when `zero_pad` is true radix int // 10 (decimal) | 16 | 8 | 2 — the only supported radixes upper bool // uppercase hex digits (A-F); the `0x`/`0o`/`0b` prefix // itself always stays lowercase (Rust `{:#X}` parity) zero_pad bool // insert '0' between sign/prefix and digits up to // `width` (Rust `{:04}` semantics) sign_plus bool // force a leading '+' for non-negative DECIMAL values // (no effect on radix != 10 — those never carry a sign, // mirrors the existing `nova_fmt_int_radix_body` // two's-complement-bits convention) alt bool // alternate form: emit the `0x`/`0o`/`0b` prefix // (radix != 10 only) } /// Default spec: plain decimal, no width/pad/sign/alt. fn FmtSpec.new() -> Self => { width: 0, radix: 10, upper: false, zero_pad: false, sign_plus: false, alt: false } /// Single 0..15 digit value → ASCII byte. `upper` selects `A-F` vs `a-f` /// for the 10..15 range; digits 0..9 are unaffected. fn hex_digit(d u64, upper bool) -> u8 { if d < 10 { ('0' as u8) + (d as u8) } else if upper { ('A' as u8) + ((d - 10) as u8) } else { ('a' as u8) + ((d - 10) as u8) } } /// `int` → decimal/radix digit string, written directly into `buf[0..cap)` /// (zero-alloc — no `nova_alloc`, no heap traffic). Handles the sign (decimal) /// or two's-complement-bits reinterpretation (radix != 10, matching Rust /// `{:x}` of `-1i64` == `"ff..ff"` — same convention as the existing /// `nova_fmt_int_radix_body` in conv.h) and zero-padding between the /// sign/prefix and the digits. Returns the number of bytes written — always /// `<= cap`; TRUNCATES defensively (never overruns `buf`) if `cap` is smaller /// than the full rendered length, mirroring the buffer-safety discipline /// `f64_fmt` also follows. /// /// This is the SOLE public /// entry for int rendering; `spec` defaults to `FmtSpec.new()` /// so the simple call shape (`int_fmt(v, buf, cap)`) still works. `unsafe /// fn` (raw `buf` write) — call-sites wrap in `unsafe { }`, /// mirrors every other buffer-primitive here. export unsafe fn int_fmt(v int, buf *mut u8, cap int, spec FmtSpec = FmtSpec.new()) -> int requires cap >= 0 && (spec.radix == 10 || spec.radix == 16 || spec.radix == 8 || spec.radix == 2) { ro radix = spec.radix as u64 // 1. Magnitude (unsigned) — decimal negates via the INT64_MIN-safe // "negate-then-widen" idiom (mirrors `nova_fmt_int_body` in conv.h); // radix formatting reinterprets the raw two's-complement bit pattern // (`int as u64` is a direct bit-cast — no saturation). mut neg = false mut mag u64 = 0 if spec.radix == 10 { if v < 0 { neg = true mag = ((-(v + 1)) as u64) + 1 } else { mag = v as u64 } } else { mag = v as u64 } // 2. Prefix — sign (decimal only) XOR alt-radix marker (radix only); // never both (decimal has no 0x/0o/0b marker, radix has no sign). mut prefix0 u8 = 0 mut prefix1 u8 = 0 mut prefix_len = 0 if spec.radix == 10 { if neg { prefix0 = '-' as u8 prefix_len = 1 } else if spec.sign_plus { prefix0 = '+' as u8 prefix_len = 1 } } else if spec.alt { prefix0 = '0' as u8 prefix1 = if spec.radix == 16 { 'x' as u8 } else if spec.radix == 8 { 'o' as u8 } else { 'b' as u8 } prefix_len = 2 } // 3. Digit count (first pass, no writes) — lets us place zero-padding // and digits without a reverse-then-flip step. mut probe = mag mut digit_count = 0 if probe == 0 { digit_count = 1 } else { while probe > 0 { digit_count += 1 probe /= radix } } // 4. Zero-pad count (only when requested and the target width exceeds // what prefix+digits already cover). mut zero_count = 0 if spec.zero_pad && spec.width > prefix_len + digit_count { zero_count = spec.width - prefix_len - digit_count } ro total = prefix_len + zero_count + digit_count ro n = total.min(cap) unsafe { mut pos = 0 if prefix_len >= 1 && pos < n { buf.write_at(pos, prefix0); pos += 1 } if prefix_len >= 2 && pos < n { buf.write_at(pos, prefix1); pos += 1 } while pos < prefix_len + zero_count && pos < n { buf.write_at(pos, '0' as u8) pos += 1 } // 5. Digits — MSB-first placement computed directly from the known // `digit_count` (no reversal): position `k` (from the most- to // least-significant) holds `mag / radix^k mod radix`. ro digits_start = prefix_len + zero_count mut k = digit_count - 1 mut m = mag while k >= 0 { ro d = m % radix ro idx = digits_start + k if idx < n { buf.write_at(idx, hex_digit(d, spec.upper)) } m /= radix k -= 1 } } n } /// `bool` → `"true"`/`"false"`, written directly into `buf[0..cap)`. /// Returns bytes written (`<= cap`; truncates defensively). export fn bool_fmt(v bool, buf *mut u8, cap int) -> int requires cap >= 0 { unsafe { if v { ro want = 4 ro n = want.min(cap) if n >= 1 { buf.write_at(0, 't' as u8) } if n >= 2 { buf.write_at(1, 'r' as u8) } if n >= 3 { buf.write_at(2, 'u' as u8) } if n >= 4 { buf.write_at(3, 'e' as u8) } n } else { ro want = 5 ro n = want.min(cap) if n >= 1 { buf.write_at(0, 'f' as u8) } if n >= 2 { buf.write_at(1, 'a' as u8) } if n >= 3 { buf.write_at(2, 'l' as u8) } if n >= 4 { buf.write_at(3, 's' as u8) } if n >= 5 { buf.write_at(4, 'e' as u8) } n } } } /// `char` (codepoint) → UTF-8 bytes, written directly into `buf[0..cap)`. /// Invalid codepoints (out of range / surrogate half) are replaced with /// U+FFFD, mirroring `nova_char_to_str` (conv.h). Returns bytes written /// (1..4; `<= cap`, truncates defensively). export fn char_fmt(v char, buf *mut u8, cap int) -> int requires cap >= 0 { mut cp = v as int if cp < 0 || cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF) { cp = 0xFFFD } mut need = 1 if cp >= 0x10000 { need = 4 } else if cp >= 0x800 { need = 3 } else if cp >= 0x80 { need = 2 } ro n = need.min(cap) unsafe { if need == 1 { if n >= 1 { buf.write_at(0, cp as u8) } } else if need == 2 { if n >= 1 { buf.write_at(0, (0xC0 | (cp >> 6)) as u8) } if n >= 2 { buf.write_at(1, (0x80 | (cp & 0x3F)) as u8) } } else if need == 3 { if n >= 1 { buf.write_at(0, (0xE0 | (cp >> 12)) as u8) } if n >= 2 { buf.write_at(1, (0x80 | ((cp >> 6) & 0x3F)) as u8) } if n >= 3 { buf.write_at(2, (0x80 | (cp & 0x3F)) as u8) } } else { if n >= 1 { buf.write_at(0, (0xF0 | (cp >> 18)) as u8) } if n >= 2 { buf.write_at(1, (0x80 | ((cp >> 12) & 0x3F)) as u8) } if n >= 3 { buf.write_at(2, (0x80 | ((cp >> 6) & 0x3F)) as u8) } if n >= 4 { buf.write_at(3, (0x80 | (cp & 0x3F)) as u8) } } } n } // ─── float — the sole C-extern ───────────────────────────────────── // // Family norm: suffix `_into` is retired for the whole `*_fmt` family — // every member writes into `(buf, cap)` regardless of suffix; that IS the // family (not the suffix). `тип_fmt` + axes as DEFAULT ARGS // (`int_fmt`/`f64_fmt`/`f32_fmt` are the ONLY public entry points; // "simple render" = the same function called with its axis args omitted, // backed by callnorm's free-fn default-arg backfill). /// `extern "C" fn` — LITERAL C symbol name (`nova_`-prefixed header-style, /// matching `nova_f64_shortest`/`nova_print_f64` in the same header). /// Body: `nova_rt/nova_rt.h` (`nova_f64_fmt`, right after /// `nova_f32_shortest`), reusing the existing `nova_f64_shortest` engine for /// `kind == 0`. Writes at most `cap` bytes into `buf`, returns bytes written /// (truncates defensively). `kind`: 0=Shortest, 1=Fixed (`%.*f`), 2=Sci /// (`%.*e`) — see `f64_fmt` below for the enum-typed, default-argued public /// `.nv`-side wrapper. Module-private (not `export`) — callers reach this /// ONLY through `f64_fmt`. extern "C" fn nova_f64_fmt(v f64, buf *mut u8, cap int, kind int, prec int) -> int /// f32 counterpart of `nova_f64_fmt` (owner 2026-07-20): shortest via /// `nova_f32_shortest` — the same engine as f32 printing/`to_str` (NOT /// an f64 extension: no double tails). Module-private — callers reach this /// ONLY through `f32_fmt` below. extern "C" fn nova_f32_fmt(v f32, buf *mut u8, cap int) -> int /// `.nv`-side wrapper: converts `FloatKind` → the `int` `nova_f64_fmt` /// carries across the `extern "C"` ABI boundary (enums don't cross /// `extern "C"` directly). Defaults (`kind`/`prec`) make the simple call /// shape `f64_fmt(v, buf, cap)` render shortest round-trip. /// `unsafe fn` (raw `buf` write) — call-sites wrap in `unsafe { }`. export unsafe fn f64_fmt(v f64, buf *mut u8, cap int, kind FloatKind = FloatKind.Shortest, prec int = -1) -> int requires cap >= 0 { ro k = match kind { FloatKind.Shortest => 0 FloatKind.Fixed => 1 FloatKind.Sci => 2 } unsafe { nova_f64_fmt(v, buf, cap, k, prec) } } /// `.nv`-side wrapper over `nova_f32_fmt` — shortest-only (f32 has no /// Fixed/Sci axis; the user-facing rich-spec path widens through f64 /// instead). `unsafe fn` (raw `buf` write) — call-sites wrap in `unsafe { }`. export unsafe fn f32_fmt(v f32, buf *mut u8, cap int) -> int requires cap >= 0 => unsafe { nova_f32_fmt(v, buf, cap) } // ─── Debug-escape engine ────────────────────────────────────────────── // // Portable `.nv` replacement for `nova_rt/conv.h`'s `nova_str_to_debug_str`/ // `nova_char_to_debug_str`. // Byte-for-byte port of the C escape tables (see // `spec_tests/conformance/d422_f4r_baseline_strcharboolu64.nv`, the contract // these MUST reproduce): `"`/`\` → `\"`/`\\`; `\n`/`\t`/`\r`/NUL → // `\n`/`\t`/`\r`/`\0`; other control bytes (< 0x20) → `\xHH` (lowercase, // NO braces — conv.h's own header comment says `\x{HH}` but the actual C // code emits `\xHH`; this port matches the REAL behavior). `char` debug // additionally escapes `'` (not `"`) and `0x7F` (DEL), and wraps in single // quotes instead of double. // // Same truncate-defensively contract as `int_fmt`/`bool_fmt`/`char_fmt` // above (`buf[0..cap)`, never overruns, returns bytes actually written). /// Write `\` + `second` at `buf[pos..pos+2)` (bounds-checked per byte). /// Returns `pos + 2` UNCONDITIONALLY (even past `cap`) — mirrors every other /// `_fmt` primitive's "conceptual position keeps advancing past the /// truncation point" contract (final clamp happens once, at the caller). fn write_esc2_at(buf *mut u8, cap int, pos int, second u8) -> int { unsafe { if pos < cap { buf.write_at(pos, '\\' as u8) } if pos + 1 < cap { buf.write_at(pos + 1, second) } } pos + 2 } /// Write `\x` + 2 lowercase hex digits of `c` at `buf[pos..pos+4)`. fn write_hex_esc_at(buf *mut u8, cap int, pos int, c u8) -> int { ro ci = c as u64 unsafe { if pos < cap { buf.write_at(pos, '\\' as u8) } if pos + 1 < cap { buf.write_at(pos + 1, 'x' as u8) } if pos + 2 < cap { buf.write_at(pos + 2, hex_digit((ci / 16) & 0xF, false)) } if pos + 3 < cap { buf.write_at(pos + 3, hex_digit(ci & 0xF, false)) } } pos + 4 } /// UTF-8-encode `cp` into `buf[pos..)` at an ARBITRARY offset (bounds-checked /// truncate) — used by `char_debug_fmt`'s passthrough case. Deliberately a /// FRESH helper (not a refactor of `char_fmt` above, which stays untouched /// per Ш1's "don't change anything existing"), same encode table, same /// truncate-defensively contract. Returns bytes conceptually needed (1..4). fn utf8_encode_at(cp int, buf *mut u8, cap int, pos int) -> int { mut need = 1 if cp >= 0x10000 { need = 4 } else if cp >= 0x800 { need = 3 } else if cp >= 0x80 { need = 2 } unsafe { if need == 1 { if pos < cap { buf.write_at(pos, cp as u8) } } else if need == 2 { if pos < cap { buf.write_at(pos, (0xC0 | (cp >> 6)) as u8) } if pos + 1 < cap { buf.write_at(pos + 1, (0x80 | (cp & 0x3F)) as u8) } } else if need == 3 { if pos < cap { buf.write_at(pos, (0xE0 | (cp >> 12)) as u8) } if pos + 1 < cap { buf.write_at(pos + 1, (0x80 | ((cp >> 6) & 0x3F)) as u8) } if pos + 2 < cap { buf.write_at(pos + 2, (0x80 | (cp & 0x3F)) as u8) } } else { if pos < cap { buf.write_at(pos, (0xF0 | (cp >> 18)) as u8) } if pos + 1 < cap { buf.write_at(pos + 1, (0x80 | ((cp >> 12) & 0x3F)) as u8) } if pos + 2 < cap { buf.write_at(pos + 2, (0x80 | ((cp >> 6) & 0x3F)) as u8) } if pos + 3 < cap { buf.write_at(pos + 3, (0x80 | (cp & 0x3F)) as u8) } } } need } /// `str` → Debug-quoted-escaped form, written into `buf[0..cap)`. Mirrors /// `nova_str_to_debug_str` (conv.h:248) byte-for-byte: iterates the ALREADY /// UTF-8-encoded source bytes (multi-byte sequences pass through opaque — /// every continuation/lead byte is >= 0x80, so never matches a special case). export fn str_debug_fmt(s str, buf *mut u8, cap int) -> int requires cap >= 0 { ro bytes = s.bytes() ro blen = bytes.len() mut pos = 0 unsafe { if pos < cap { buf.write_at(pos, '"' as u8) } } pos += 1 for i in 0..blen { ro c = bytes[i] if c == ('"' as u8) { pos = write_esc2_at(buf, cap, pos, '"' as u8) } else if c == ('\\' as u8) { pos = write_esc2_at(buf, cap, pos, '\\' as u8) } else if c == ('\n' as u8) { pos = write_esc2_at(buf, cap, pos, 'n' as u8) } else if c == ('\t' as u8) { pos = write_esc2_at(buf, cap, pos, 't' as u8) } else if c == ('\r' as u8) { pos = write_esc2_at(buf, cap, pos, 'r' as u8) } else if c == (0 as u8) { pos = write_esc2_at(buf, cap, pos, '0' as u8) } else if c < (0x20 as u8) { pos = write_hex_esc_at(buf, cap, pos, c) } else { unsafe { if pos < cap { buf.write_at(pos, c) } } pos += 1 } } unsafe { if pos < cap { buf.write_at(pos, '"' as u8) } } pos += 1 pos.min(cap) } /// `char` (codepoint) → Debug single-quoted-escaped form, written into /// `buf[0..cap)`. Mirrors `nova_char_to_debug_str` (conv.h:317) byte-for- /// byte: escapes `\n`/`\t`/`\r`/NUL/`'`/`\`/other-control(<0x20)/DEL(0x7F); /// everything else passes through UTF-8-encoded (`utf8_encode_at` above). export fn char_debug_fmt(v char, buf *mut u8, cap int) -> int requires cap >= 0 { mut cp = v as int if cp < 0 || cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF) { cp = 0xFFFD } mut pos = 0 unsafe { if pos < cap { buf.write_at(pos, '\'' as u8) } } pos += 1 if cp == ('\n' as int) { pos = write_esc2_at(buf, cap, pos, 'n' as u8) } else if cp == ('\t' as int) { pos = write_esc2_at(buf, cap, pos, 't' as u8) } else if cp == ('\r' as int) { pos = write_esc2_at(buf, cap, pos, 'r' as u8) } else if cp == 0 { pos = write_esc2_at(buf, cap, pos, '0' as u8) } else if cp == ('\'' as int) { pos = write_esc2_at(buf, cap, pos, '\'' as u8) } else if cp == ('\\' as int) { pos = write_esc2_at(buf, cap, pos, '\\' as u8) } else if cp < 0x20 || cp == 0x7F { pos = write_hex_esc_at(buf, cap, pos, cp as u8) } else { pos += utf8_encode_at(cp, buf, cap, pos) } unsafe { if pos < cap { buf.write_at(pos, '\'' as u8) } } pos += 1 pos.min(cap) } // The `*_display_spec` family lives in `string_builder.nv` (its dependency // on this module is ONE-DIRECTIONAL — no import cycle): `int_fmt`/`f64_fmt`/ // `FmtSpec` are `export`ed above so `string_builder.nv` can reach them.