/
nv-lang
/
nova
Обзор
Документация
Войти
/
nv-lang
/
nova
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
main
std/src/runtime/string_builder.nv
635 строк
31 KB
Evgeniy Golovin
merge: comment-hygiene-2 — чистка комментариев std (batch 1-20) + линт-свип 330→13
01 авг 2026, 18:13
01 авг 2026, 18:13
bb8b33d
Код
Авторство
О чём код?
// StringBuilder: consume record с полем `mut buf []u8`; все методы на Nova-body. // // API-каноны: // - строкоподобная поверхность — только `@byte_len()` (как str, не len) // - `@char_len()` нет: счёт codepoint — линза через // `.clone().into_str().chars().count()` // - `@peek()` нет (unsound: pointer aliasing с realloc) // - финализация — consume `@into_str()` // - добавление сырых байтов — `@append([]u8)` overload; `@plus` нет — // используйте `@append` напрямую // - тип consume — требует явного consume // #no_prelude — break import cycle (prelude → collections → string_builder). // Explicit imports cover only what we need. #no_prelude module runtime.string_builder // One-directional import of the fmt_buf buffer-primitives: this file imports // `runtime.fmt_buf`; `fmt_buf` does NOT import back — no cycle. `Align` // (runtime.fmt_buf, also `#no_prelude` — see its header note) does not // reopen the prelude cycle this file already breaks out of. // // `int_fmt`/`f64_fmt`/`f32_fmt` are the sole entry points used by the // `*_display_spec` family further down (axis params default; `unsafe fn`). import std.runtime.fmt_buf.{Align, FloatKind, FmtSpec, int_fmt, f64_fmt, bool_fmt, char_fmt, f32_fmt, str_debug_fmt, char_debug_fmt} import std.runtime.raw_mem.{RawMem} import std.prelude.core.{Result} import std.runtime.string.{Utf8Error} // ─── Type ─── export type StringBuilder consume { mut buf []u8 } // ─── Constructors ─── // Default capacity для StringBuilder.new() — 16 bytes (1-line string). const INITIAL_CAPACITY = 16 // `new()`/`@clone()` keep single-expression arrow bodies: a multi-statement // BLOCK body (`{ mut _buf ...; ...; { buf: _buf } }`) broke the consume // checker's type resolution for every OTHER file's `consume sb = // StringBuilder.new()` call site in the same CU (a `not-consumed` diagnostic // with an EMPTY resolved type: the consume-checker's own type-resolution pass // didn't see through the block body, unlike the main type-checker). Kept as // private helpers returning plain `[]u8` (no consume concerns there) so the // constructors stay the single-expression bare-literal arrow-body shape the // consume checker expects. fn sb_init_buf(cap int) -> []u8 => []u8.new(cap: cap) // Deep-copy helper for `@clone`: fresh exact-capacity buffer + one bulk // `append` (RawMem.copy). Free fn on `[]u8` (NOT a StringBuilder method) so // `@clone`'s own body stays a single `=> { buf: … }` expression (same // consume-checker shape constraint as `new()`/sb_init_buf above), AND so the // `append` here dispatches on a plainly-typed `[]u8` local — calling // `.append(@buf)` on a `{ buf: … }` record-literal receiver mis-dispatched to // StringBuilder's own `@append(s str)` overload (generated C passed a // `Nova_Vec____nova_byte*` where `nova_str` was expected) — this was the root // of the `vec_of_empty_panic` CC-FAIL. fn sb_clone_buf(src []u8) -> []u8 { mut b []u8 = sb_init_buf(src.len()) b.append(src) b } // Пустой StringBuilder с pre-allocated capacity 16. export fn StringBuilder.new(cap int = INITIAL_CAPACITY) -> Self requires cap >= 0 => { buf: sb_init_buf(cap) } // StringBuilder из существующей строки (copy UTF-8 bytes). // // static-конверсия `StringBuilder.from(s)` не предоставляется — // запрещённая «пятая дверь» (nv-coding-style §1а). Канон: метод на источнике. export fn str @to_stringbuilder() -> StringBuilder => StringBuilder.new().append(@) // StringBuilder из одного codepoint (UTF-8 encode). // Fluent chain `StringBuilder.new().append(c)` (совместим с consume-каноном) // вместо `let mut sb = StringBuilder.new() + sb.append(c)` (sb consume- // obligation не satisfied при implicit fluent-return). // // static-конверсия `StringBuilder.from(c)` не предоставляется — та же // ретракция, что у `str @to_stringbuilder()` выше. Канон: метод на источнике. export fn char @to_stringbuilder() -> StringBuilder => StringBuilder.new().append(@) // ─── Query methods ─── // Длина буфера в байтах. O(1). Строкоподобная поверхность // (как str) — только byte_len(), не len(). export fn StringBuilder @byte_len() -> int => @buf.len() // Allocated capacity в байтах. // // `@cap()` — единственный 0-arg getter половины `cap`/`cap(n)` property-пары // (алиас `@capacity()` ретрактирован; `with_capacity` удалён — вместо него // `StringBuilder.new(cap: n)`). export fn StringBuilder @cap() -> int => @buf.cap() // Set capacity to exactly `n` bytes (delegates to the underlying `[]u8`'s // own exact `mut @cap(n)` — no rounding). Replacement for the removed // `StringBuilder.with_capacity` static constructor: `StringBuilder.new(cap: n)`. export fn StringBuilder mut @cap(n int) -> @ requires n >= @byte_len() { @buf.cap(n) } // true если буфер пуст. export fn StringBuilder @is_empty() -> bool => @buf.is_empty() // Независимая копия (deep copy buffer). Нужен для consume-типа при разветвлении. export fn StringBuilder @clone() -> Self => { buf: sb_clone_buf(@buf) } // Проверить prefix буфера. Slice-view + memcmp (zero-copy). export fn StringBuilder @starts_with(prefix str) -> bool { ro pbytes = prefix.bytes() ro plen = pbytes.len() plen <= @buf.len() && @buf[..plen].compare(pbytes) == 0 } // Проверить suffix буфера. Slice-view + memcmp (zero-copy). export fn StringBuilder @ends_with(suffix str) -> bool { ro sbytes = suffix.bytes() ro slen = sbytes.len() ro blen = @buf.len() slen <= blen && @buf[blen - slen..blen].compare(sbytes) == 0 } // ─── Mutating (-> @) ─── // Append UTF-8 bytes из str. Zero-copy view (as_bytes). export fn StringBuilder mut @append(s str) -> @ { @buf.append(s.bytes()) } // Append codepoint как UTF-8 (1-4 байта). Fluent push chain. export fn StringBuilder mut @append(c char) -> @ { ro (b, n) = c.encode_utf8() @buf.append(b[..n]) } // Append raw []u8 bytes. Caller отвечает за UTF-8 validity. export fn StringBuilder mut @append(arr []u8) -> @ { @buf.append(arr) } // Typed scalar overloads so `sb.append(x)` dispatches on the value's type // uniformly — no `append_int`-style names, no caller-side `str.from`. Used by // the primitive Display impls in std/prelude/protocols.nv // (`fn int @display(mut sb StringBuilder) { sb.append(@) }`), and convenient // for users directly. `bool` appends the literal "true"/"false" with no // intermediate; `int`/`f64` route through `@to_str()` (bare-T blanket, // `"${@}"` — same conv.h formatters the interp path uses), avoiding the temp // StringBuilder an interp-string body would otherwise allocate. // Параметр `x`: это ЗНАЧЕНИЕ для форматирования, не count/index/len — // имя `n` ложно матчило W_PARAM_NO_CONTRACT (линт по имени параметра, // не по семантике); `requires n >= 0` было бы НЕВЕРНО (запрещало бы append // отрицательных чисел). // Zero-alloc: reserve→spare→advance через fmt_buf-мосты — БЕЗ промежуточной // str-аллокации. Капы — честные максимумы рендера (без разнобоя на глаз): const INT_FMT_CAP = 20 // "-9223372036854775808" — ровно 20 const F64_FMT_CAP = 24 // "-2.2250738585072014e-308" — ровно 24 const F32_FMT_CAP = 16 // "-1.1754944e-38" = 14; +2 запас формата %g // `int_fmt(x, buf, cap)` — `spec` axis omitted, // defaults to plain decimal (`FmtSpec.new()`). export fn StringBuilder mut @append(x int) -> @ { @reserve(INT_FMT_CAP) unsafe { @advance(int_fmt(x, @spare(), INT_FMT_CAP)) } } // `f64_fmt(x, buf, cap)` — `kind`/`prec` axes omitted, default to // Shortest/-1. export fn StringBuilder mut @append(x f64) -> @ { @reserve(F64_FMT_CAP) unsafe { @advance(f64_fmt(x, @spare(), F64_FMT_CAP)) } } export fn StringBuilder mut @append(b bool) -> @ { if b { @append("true") } else { @append("false") } } // f32 via `f32_fmt` — the f32-precise shortest-round-trip formatter // (`nova_f32_shortest`). Widening to f64 first would surface the f32→f64 // mantissa tail now that f64's formatter is faithful (0.1f → "0.10000000149011612"); // the direct f32 path keeps "0.1". export fn StringBuilder mut @append(x f32) -> @ { @reserve(F32_FMT_CAP) unsafe { @advance(f32_fmt(x, @spare(), F32_FMT_CAP)) } } // Append строку s ровно n раз. n <= 0 → no-op. // Single reserve(total) → один realloc вместо n× амортизированного doubling. // `bytes()` — zero-copy view; `append` в loop без indirection. export fn StringBuilder mut @append_repeat(s str, n int) -> @ requires n >= 0 { if n <= 0 { return } ro bytes = s.bytes() @buf.reserve(bytes.len() * n) for _ in 0..n { @buf.append(bytes) } } // Обрезать буфер до len байт. len >= @byte_len() → no-op. Caller отвечает за UTF-8 границы. export fn StringBuilder mut @truncate(len int) -> @ requires len >= 0 { @buf.truncate(len) } // Write protocol implementation. `@write` takes `[]u8` directly — // delegates straight to `@buf.append`. Returns () — satisfies Write contract. export fn StringBuilder mut @write(bytes []u8) -> () { @buf.append(bytes) } // ─── Consume ─── // Финализировать в str. После @into_str() StringBuilder consumed. // Zero-copy steal: reuse @buf data ptr напрямую (writes '\0' in-place // если cap > len, иначе alloc+copy fallback). // Infallible: UTF-8 invariant поддерживается append-методами. // `#coerce`: declares the implicit finalize-lane pair `StringBuilder → str` // (owning zero-cost MOVE, receiver disarmed at the insertion point; use-after // is a compile error — linearity). // [M-174.1-into-str-unchecked-legacy-array-codegen-gap] обход: the zero-copy // steal path (`@buf.into_str_unchecked()`) mis-codegens here — this file is // `#no_prelude` and so `@buf` uses the LEGACY `NovaArray` representation // (prelude.nv: "#no_prelude units keep the legacy NovaArray path"), for which // the new consume-method dispatch doesn't wire correctly (generates a C body // returning `int` instead of `nova_str` — CC-FAIL). Falls back to the // non-consuming copy path (`to_str_unchecked`) until that gap is closed (see // backlog-followups.md marker); correctness is unaffected, only the // zero-copy-reuse optimization is foregone for this legacy-array call site. #coerce export fn StringBuilder consume @into_str() -> str => unsafe { @buf.to_str_unchecked() } // Checked twin of `@into_str()` — validates UTF-8 instead of assuming it. // Delegates to `[]u8 @to_str()` (std/runtime/string/core.nv). export fn StringBuilder consume @into_str_checked() -> Result[str, Utf8Error] => @buf.to_str() // ─── Buffer-primitive plumbing ─── // // ADDITIVE zero-copy/zero-alloc surface for the compiler's interpolation/ // format-spec emit path to call into. Nothing existing above is touched or // re-routed through them yet. // Expose a pointer into (at least) `n` bytes of SPARE capacity beyond the // current live length — the compiler-path zero-COPY digit-loop target // ("top-level компилятор-путь... digit-loop прямо в sb.reserve()/ // sb.advance()"). The bytes at `[@byte_len(), @byte_len()+n)` are NOT yet // live (`@byte_len()` unchanged) until a matching `@advance(n)` call — the // caller's contract is to write at most `n` bytes starting at the returned // pointer, then call `@advance` with the number actually written. // Разделение дверей: reserve БОЛЬШЕ НЕ возвращает указатель (ensure и // expose — разные дела); `-> @` — fluent-канон мутаторов // (push/reserve/truncate/fill), даёт `sb.reserve(n).spare()`. // Сырой хвост — явная дверь `@spare()` ниже. export fn StringBuilder mut @reserve(n int) -> @ requires n >= 0 { @buf.reserve(n) } /// Pointer to the FIRST free byte of the tail (`[byte_len(), cap)`) — the door /// of the raw-write protocol: `reserve(n)` → `spare()` → write ≤n bytes → /// `advance(k)`. Rust parity `spare_capacity_mut`. Dereferencing is unsafe; /// the pointer itself is valid until the next reallocation (`reserve`/`append`). export fn StringBuilder mut @spare() -> *mut u8 => @buf.ptr().offset(@buf.len()) // Commit `n` bytes of previously-`@reserve`d spare capacity to the live // length — the second half of the zero-copy digit-loop protocol. // Тонкий делегат в `Vec[T] mut @advance` (in-place `len += n`). // Обязательство инициализации n байт — unsafe на сайте (см. контракт Vec @advance). export fn StringBuilder mut @advance(n int) -> @ requires n >= 0 && n <= @buf.cap() - @buf.len() { unsafe { @buf.advance(n) } } // UTF-8 codepoint count of a byte run ("width = кодпоинты") — counts // non-continuation bytes (top two bits != 0b10), mirroring `conv.h`'s // `nova_fmt_char_count`. fn utf8_char_count(bytes []u8) -> int { mut n = 0 for b in bytes { if (b & 0xC0) != 0x80 { n += 1 } } n } // Known-length primitive/str write with width/align/fill padding, WITHOUT a // shift ("известная длина... write_padded... без сдвига"): `bytes` // has not been written to the main buffer yet, so the fill run and the // content can just be appended in the right order directly. export fn StringBuilder mut @write_padded(bytes []u8, width int, fill char, align Align) -> () requires width >= 0 { ro content_chars = utf8_char_count(bytes) ro pad_chars = (width - content_chars).max(0) match align { Align.Left => { @append(bytes) for k in 0..pad_chars { @append(fill) } } Align.Right => { for k in 0..pad_chars { @append(fill) } @append(bytes) } Align.Center => { ro left = pad_chars / 2 ro right = pad_chars - left for k1 in 0..left { @append(fill) } @append(bytes) for k2 in 0..right { @append(fill) } } } } // Streaming-composite width padding ("pad_in_place"): the body // starting at byte offset `mark` has ALREADY been streamed into the main // buffer (its length was not known ahead of the render — record/tuple/Vec/ // sum composites), so reaching `width` codepoints (when short) means // memmove-ing that body and splicing in `fill` — right/center shift the body // right via an overlap-safe `RawMem.copy` (memmove, not memcpy); left just // appends trailing fill (the body is already correctly positioned). export fn StringBuilder mut @pad_in_place(mark int, width int, fill char, align Align) -> () requires mark >= 0 && mark <= @byte_len() && width >= 0 { ro end = @byte_len() ro content_len = end - mark mut content_chars = 0 for i in mark..end { if (@buf[i] & 0xC0) != 0x80 { content_chars += 1 } } if content_chars >= width { return } ro pad_chars = width - content_chars mut left_chars = 0 mut right_chars = 0 match align { Align.Left => { right_chars = pad_chars } Align.Right => { left_chars = pad_chars } Align.Center => { left_chars = pad_chars / 2; right_chars = pad_chars - left_chars } } if left_chars > 0 { ro (fill_bytes, fill_len) = fill.encode_utf8() ro left_bytes = left_chars * fill_len @buf.reserve(left_bytes) unsafe { // memmove (overlap-safe) the existing tail right by `left_bytes`. RawMem.copy(@buf.ptr().offset(mark) as *u8, @buf.ptr().offset(mark + left_bytes) as *mut u8, content_len) // ASCII-fill (fill_len == 1 — пробел/'0'/… — типовой случай): // весь двойной цикл = один memset (RawMem.copy // из [4]u8 в лоб нельзя — у фикс-массива нет @ptr(), да и незачем: // внутренний цикл ≤4 байта, выигрыш — по ВНЕШНЕЙ оси width). if fill_len == 1 { RawMem.fill(@buf.ptr().offset(mark) as *mut u8, fill_bytes[0], left_chars) } else { mut pos = mark for c in 0..left_chars { for b in 0..fill_len { @buf.ptr().write_at(pos, fill_bytes[b]) pos += 1 } } } @buf.advance(left_bytes) // in-place len+=n вместо header-rebuild } } if right_chars > 0 { ro (fill_bytes, fill_len) = fill.encode_utf8() if fill_len == 1 { // ASCII-fill: reserve + один memset + advance вместо O(width) @append. @buf.reserve(right_chars) unsafe { RawMem.fill(@buf.ptr().offset(@buf.len()) as *mut u8, fill_bytes[0], right_chars) @buf.advance(right_chars) } } else { for k in 0..right_chars { @append(fill) } } } } // ─── `*_display_spec` family ───────────────────────────────────────── // // Flat-argument render-INTO-`StringBuilder` entry points — the SINGLE // rendering path the compiler's interp fast-path (devirtualized direct call, // resolved by declaration) and a primitive's own `@display`/`@debug` body are // meant to converge on. "Плоские аргументы" precedent: `FmtCtx.rich` // (prelude/protocols.nv) — same shape, primitive scalars only, no // `Option[T]`/enum literals crossing a hand-synthesized call boundary. // // LIVES HERE (not `fmt_buf.nv`) specifically to avoid an inter-module cycle // — see the import-block comment above this file's `import // std.runtime.fmt_buf.{...}` line. This file already has a clean one-way // dependency on `fmt_buf` for everything these functions need. // // Each function REPRODUCES `nova_rt/conv.h`'s existing `nova_fmt_*` chain // byte-for-byte (see `spec_tests/conformance/d422_f4r_baseline_*.nv`, the // conformance contract) — this is a CARRIER SWAP (Rust-emitted C chain → // `.nv` engine), not a behavior redesign. Two deliberate quirks are // REPRODUCED, not fixed (see the baseline fixtures' comments for why): // 1. `f64_display_spec`'s precision branch computes the sign prefix and // the magnitude body SEPARATELY (mirrors `nova_fmt_f64_prefix`/ // `nova_fmt_f64_body` exactly) — `-0.0` with an explicit precision // therefore double-signs (`--0.00`), because the magnitude negation // test is `v < 0.0` (false for `-0.0`) while the prefix test is // signbit-aware. // 2. `f64_display_spec`'s precision is clamped to 64 (matching // `nova_fmt_f64_body`'s own `if (prec > 64) prec = 64`) — DIFFERENT // from `f64_fmt`'s own internal Fixed-kind clamp (340). // Forwarding `prec` unclamped would diverge from the baseline for // `.65`+. const DISPLAY_INT_CAP = 20 // decimal: "-9223372036854775808" const DISPLAY_F64_FIXED_CAP = 380 // DBL_MAX (~309 int digits) + '.' + 64 decimals + slack const DISPLAY_F64_SHORTEST_CAP = 24 const DISPLAY_F32_CAP = 16 const DISPLAY_CHAR_CAP = 4 const DISPLAY_CHAR_DEBUG_CAP = 8 const DISPLAY_BOOL_CAP = 5 /// Natural (non-zero-pad) render cap for `int_fmt`, per radix — a 64-bit /// value needs UP TO 64 BINARY digits (`radix: 2`), 22 OCTAL digits, or 16 /// HEX digits, each plus a 2-byte alt-radix prefix (`0b`/`0o`/`0x`) when /// `alt` fires; DECIMAL needs at most 20 (`"-9223372036854775808"`, the /// ONLY radix with a sign). Found by direct testing: /// a first cut reused `DISPLAY_INT_CAP` (sized for decimal, 20) for EVERY /// radix — `int_fmt` "truncates defensively (never overruns buf)" per its /// own contract, so `${int.MIN:o}` (needs 22 octal digits) silently lost /// its last 2 digits instead of erroring, diverging from the baseline. fn int_display_natural_cap(radix int) -> int { if radix == 2 { 66 } else if radix == 8 { 24 } else if radix == 16 { 18 } else { DISPLAY_INT_CAP } } /// `int` display/debug/radix render (decimal is `radix: 10`; Debug for int /// is identical to Display per conv.h, so this ONE function backs both). /// `width`/`zero_pad` behave exactly like `int_fmt`'s own `FmtSpec.width`/ /// `.zero_pad` (a zero-pad TARGET, consulted only when `zero_pad` is true); /// when `zero_pad` is false, `width` instead drives EXTERNAL align/fill /// padding via `@pad_in_place` — mirrors `nova_fmt_pad`'s `zero_pad` branch /// always overriding `align` (conv.h:469, checked BEFORE the align switch). export fn int_display_spec(mut sb StringBuilder, v int, width int, radix int, upper bool, zero_pad bool, sign_plus bool, alt bool, align Align, fill char) -> () requires radix == 10 || radix == 16 || radix == 8 || radix == 2 { ro natural_cap = int_display_natural_cap(radix) if zero_pad { ro cap = width.max(natural_cap) ro spec = FmtSpec { width, radix, upper, zero_pad: true, sign_plus, alt } sb.reserve(cap) // `spec` has a default — a supplied value must be // passed BY NAME (keyword-only), not positionally. unsafe { sb.advance(int_fmt(v, sb.spare(), cap, spec: spec)) } } else { ro mark = sb.byte_len() ro spec = FmtSpec { width: 0, radix, upper, zero_pad: false, sign_plus, alt } sb.reserve(natural_cap) unsafe { sb.advance(int_fmt(v, sb.spare(), natural_cap, spec: spec)) } sb.pad_in_place(mark, width, fill, align) } } /// `f64` display/debug render (Debug for f64 is identical to Display per /// conv.h — `has_prec: false` covers both). `prec` only consulted when /// `has_prec` — precision=0 (`.0`) and no-precision are semantically /// DIFFERENT (fixed-0-decimals vs shortest-round-trip), hence the explicit /// bool rather than a `-1`-sentinel (mirrors `FmtCtx.rich`'s own /// `has_width`/`has_precision` bools). /// /// `zero_pad` mirrors `nova_fmt_pad`'s own zero-pad branch (conv.h:469, /// checked BEFORE the align switch) — zeros insert BETWEEN the prefix /// (explicit `-`/`+`, if any) and the body, never before the sign. Found by /// direct testing: a first cut used the generic /// `@pad_in_place(mark, width, fill, align)` unconditionally, which — for /// `align: Right, fill: '0'` — inserts zeros BEFORE the whole prefix+body /// run (`${-12.345:010.2}` → wrong `"0000-12.35"` instead of the pinned /// `"-000012.35"`). `prefix_len` tracks how many bytes the (possibly empty) /// explicit prefix occupied; the zero-pad case re-anchors `@pad_in_place` /// at `mark + prefix_len` (right after the prefix) instead of `mark`, /// forcing `Align.Right`/`'0'` — the SAME technique `int_display_spec` /// gets for free from `int_fmt`'s own `FmtSpec.zero_pad`. This ALSO covers /// the no-precision branch correctly: when `sign_plus` is false there is NO /// explicit prefix at all (`prefix_len == 0`) and the shortest-engine body /// carries its OWN embedded sign (e.g. `"-100000"`) — zero-padding then /// inserts BEFORE that embedded sign too (`${-1.0e5:012}` → confirmed: /// `"00000-100000"`, NOT `"-00000100000"` — /// pinned as-is, matches `nova_fmt_pad`'s prefix="" case exactly). export fn f64_display_spec(mut sb StringBuilder, v f64, width int, has_prec bool, prec int, zero_pad bool, sign_plus bool, align Align, fill char) -> () { ro mark = sb.byte_len() mut prefix_len = 0 if has_prec { // Quirk #1 (see file-header note): TWO DIFFERENT negative tests, // mirroring `nova_fmt_f64_body`/`nova_fmt_f64_prefix` exactly — // NOT the same test reused for both purposes. Both fire // independently for `-0.0`, concatenating into the pinned // double-minus `--0.00`. // - `mag_neg` (magnitude negation, `nova_fmt_f64_body`): plain // `v < 0.0` — FALSE for `-0.0`, so `mag` stays `-0.0` // (unchanged) and `fmt_f64`'s `%.*f` engine prints ITS OWN // sign from the bit pattern regardless. // - `prefix_neg` (sign prefix, `nova_fmt_f64_prefix`): signbit- // AWARE — `v < 0.0 || (v == 0.0 && 1.0/v < 0.0)` — TRUE for // `-0.0`. Both fire independently for `-0.0`, concatenating // into the pinned double-minus `--0.00`. ro mag_neg = v < 0.0 ro prefix_neg = v < 0.0 || (v == 0.0 && (1.0 / v) < 0.0) if prefix_neg { sb.append("-"); prefix_len = 1 } else if sign_plus { sb.append("+"); prefix_len = 1 } ro mag = if mag_neg { -v } else { v } // Quirk #2: clamp at 64 (matches nova_fmt_f64_body, NOT fmt_f64's // own internal 340 clamp). ro clamped_prec = prec.max(0).min(64) sb.reserve(DISPLAY_F64_FIXED_CAP) // `kind`/`prec` have defaults — supplied values // must be passed BY NAME (keyword-only), not positionally. unsafe { sb.advance(f64_fmt(mag, sb.spare(), DISPLAY_F64_FIXED_CAP, kind: FloatKind.Fixed, prec: clamped_prec)) } } else { // No-precision (shortest round-trip) path — the body ALREADY // carries its own sign (`fmt_f64` Shortest on the RAW value); // prefix is empty unless `sign_plus` AND the value is // non-negative — mirrors the `dv >= 0.0` ternary in // `emit_format_spec_value` exactly (asymmetric vs the precision // branch above by design — no double-sign // on `-0.0` in THIS branch). if sign_plus && v >= 0.0 { sb.append("+"); prefix_len = 1 } sb.reserve(DISPLAY_F64_SHORTEST_CAP) // Shortest/-1 ARE `f64_fmt`'s defaults — omit both axes // entirely rather than spell out the default by name. unsafe { sb.advance(f64_fmt(v, sb.spare(), DISPLAY_F64_SHORTEST_CAP)) } } if zero_pad { ro zp_width = (width - prefix_len).max(0) sb.pad_in_place(mark + prefix_len, zp_width, '0', Align.Right) } else { sb.pad_in_place(mark, width, fill, align) } } /// `f32` BARE display render — the f32-PRECISE shortest engine /// (`f32_fmt`/`nova_f32_shortest`), matching the bare `${f32val}` /// interp path (emit_c.rs `primitive_to_str_fn`'s `nova_f32_to_str`), NOT /// the widen-to-f64 path a RICH f32 spec currently takes /// (`f64_display_spec(sb, v as f64, ...)`) — both building blocks are /// provided here; this one is the f32-precise half. export fn f32_display_spec(mut sb StringBuilder, v f32, width int, align Align, fill char) -> () { ro mark = sb.byte_len() sb.reserve(DISPLAY_F32_CAP) unsafe { sb.advance(f32_fmt(v, sb.spare(), DISPLAY_F32_CAP)) } sb.pad_in_place(mark, width, fill, align) } /// `bool` display/debug render (Debug == Display for bool per conv.h). export fn bool_display_spec(mut sb StringBuilder, v bool, width int, align Align, fill char) -> () { ro mark = sb.byte_len() sb.reserve(DISPLAY_BOOL_CAP) unsafe { sb.advance(bool_fmt(v, sb.spare(), DISPLAY_BOOL_CAP)) } sb.pad_in_place(mark, width, fill, align) } /// `char` Display render (bare UTF-8, no escaping). export fn char_display_spec(mut sb StringBuilder, v char, width int, align Align, fill char) -> () { ro mark = sb.byte_len() sb.reserve(DISPLAY_CHAR_CAP) unsafe { sb.advance(char_fmt(v, sb.spare(), DISPLAY_CHAR_CAP)) } sb.pad_in_place(mark, width, fill, align) } /// `char` Debug render (single-quoted + escaped — `char_debug_fmt`, /// `fmt_buf.nv`). export fn char_debug_display_spec(mut sb StringBuilder, v char, width int, align Align, fill char) -> () { ro mark = sb.byte_len() sb.reserve(DISPLAY_CHAR_DEBUG_CAP) unsafe { sb.advance(char_debug_fmt(v, sb.spare(), DISPLAY_CHAR_DEBUG_CAP)) } sb.pad_in_place(mark, width, fill, align) } /// Byte offset of the first `nchars` codepoints in a UTF-8 byte run — /// mirrors `conv.h`'s `nova_fmt_bytes_for_chars` (used for `.N` string /// precision truncation). `nchars < 0` is unreachable in practice (the /// spec parser only ever produces `prec >= 0`) but returns the full length /// defensively rather than looping forever. fn display_spec_bytes_for_chars(bytes []u8, nchars int) -> int { if nchars < 0 { return bytes.len() } ro len = bytes.len() mut i = 0 mut seen = 0 while i < len && seen < nchars { ro b = bytes[i] mut step = 1 if b >= (0xF0 as u8) { step = 4 } else if b >= (0xE0 as u8) { step = 3 } else if b >= (0xC0 as u8) { step = 2 } if i + step > len { step = len - i } i += step seen += 1 } i } /// `str` Display render — identity content (zero-copy view of `v`'s own /// bytes), optional `.N` precision truncation (codepoints, mirrors /// `nova_fmt_str_precision`), then width/align/fill pad. Default align is /// caller-resolved (Rust convention: strings left-align by default) — /// same "resolve default before calling" contract every other codegen call /// site in this family follows (mirrors the pre-existing /// `align_code(spec.align, default_left)` Rust-side helper). export fn str_display_spec(mut sb StringBuilder, v str, width int, has_prec bool, prec int, align Align, fill char) -> () { ro mark = sb.byte_len() ro vb = v.bytes() if has_prec { ro cut = display_spec_bytes_for_chars(vb, prec) sb.write(vb[..cut]) } else { sb.write(vb) } sb.pad_in_place(mark, width, fill, align) } /// `str` Debug render — quoted+escaped content (`str_debug_fmt`, /// `fmt_buf.nv`, rendered into a scratch `[]u8` sized for the worst case: /// every source byte expands to a 4-byte `\xHH` escape, +2 for the /// surrounding quotes), then the SAME optional `.N` precision truncation + /// pad as Display above (mirrors the existing behavior: /// `emit_format_spec_value`'s `precision_consumed` is `false` for the /// primitive str/Debug arm too, so `.N` truncates the ESCAPED+QUOTED text, /// not the source — preserved as-is). export fn str_debug_display_spec(mut sb StringBuilder, v str, width int, has_prec bool, prec int, align Align, fill char) -> () { ro cap = v.byte_len() * 4 + 2 mut scratch []u8 = []u8.new(cap: cap) ro n = str_debug_fmt(v, scratch.ptr(), cap) unsafe { scratch.advance(n) } ro mark = sb.byte_len() if has_prec { ro cut = display_spec_bytes_for_chars(scratch, prec) sb.write(scratch[..cut]) } else { sb.write(scratch) } sb.pad_in_place(mark, width, fill, align) }