From 8a793005dd8c68803b6c75dac8be00299a2caf66 Mon Sep 17 00:00:00 2001 From: Hitalo Souza Date: Fri, 26 Jun 2026 15:14:29 -0300 Subject: [PATCH] strings: add Builder.write_u_decimal and write_decimal JS-backend parity (#27522) --- vlib/strings/builder.c.v | 34 ++++++++++++++++++++---------- vlib/strings/builder.js.v | 41 +++++++++++++++++++++++++++++++++++++ vlib/strings/builder_test.v | 23 +++++++++++++++++++++ 3 files changed, 87 insertions(+), 11 deletions(-) diff --git a/vlib/strings/builder.c.v b/vlib/strings/builder.c.v index 9c239d02a..7e21bd153 100644 --- a/vlib/strings/builder.c.v +++ b/vlib/strings/builder.c.v @@ -74,20 +74,36 @@ pub fn (mut b Builder) write_byte(data u8) { // write_decimal appends a decimal representation of the number `n` into the builder `b`, // without dynamic allocation. The higher order digits come first, i.e. 6123 will be written // with the digit `6` first, then `1`, then `2` and `3` last. -@[direct_array_access] pub fn (mut b Builder) write_decimal(n i64) { if n == 0 { b.write_u8(0x30) return } - if n == min_i64 { - b.write_string(n.str()) + mut mag := u64(n) + if n < 0 { + b.write_u8(`-`) + // Wrapping unsigned negation yields the correct magnitude even for `min_i64`, + // whose absolute value does not fit in an i64, so this stays allocation-free for + // every input without a special case for the signed 64-bit minimum. + mag = u64(0) - mag + } + b.write_u_decimal(mag) +} + +// write_u_decimal appends a decimal representation of the unsigned number `n` into the +// builder `b`, without dynamic allocation. Unlike `write_decimal`, it covers the entire +// `u64` range (values above `max_i64`). The higher order digits come first, i.e. 6123 +// will be written with the digit `6` first, then `1`, then `2` and `3` last. +@[direct_array_access] +pub fn (mut b Builder) write_u_decimal(n u64) { + if n == 0 { + b.write_u8(0x30) return } - mut buf := [25]u8{} - mut x := if n < 0 { -n } else { n } - mut i := 24 + mut buf := [20]u8{} // max_u64 == 18446744073709551615, i.e. 20 digits + mut x := n + mut i := 19 for x != 0 { nextx := x / 10 r := x % 10 @@ -95,11 +111,7 @@ pub fn (mut b Builder) write_decimal(n i64) { x = nextx i-- } - if n < 0 { - buf[i] = `-` - i-- - } - unsafe { b.write_ptr(&buf[i + 1], 24 - i) } + unsafe { b.write_ptr(&buf[i + 1], 19 - i) } } // write implements the io.Writer interface, that is why it returns how many bytes were written to the string builder. diff --git a/vlib/strings/builder.js.v b/vlib/strings/builder.js.v index b41e99904..8134b51ef 100644 --- a/vlib/strings/builder.js.v +++ b/vlib/strings/builder.js.v @@ -26,6 +26,47 @@ pub fn (mut b Builder) write_u8(data u8) { b << data } +// write_decimal appends a decimal representation of the number `n` into the builder `b`. +// The higher order digits come first, i.e. 6123 will be written with the digit `6` first, +// then `1`, then `2` and `3` last. +pub fn (mut b Builder) write_decimal(n i64) { + if n == 0 { + b.write_u8(0x30) + return + } + mut mag := u64(n) + if n < 0 { + b.write_u8(`-`) + // Wrapping unsigned negation yields the correct magnitude even for `min_i64`, + // whose absolute value does not fit in an i64. It also avoids depending on the + // `min_i64` constant, which the JS backend currently lowers incorrectly, so a + // runtime `min_i64` (e.g. from `'-9223372036854775808'.i64()`) still formats right. + mag = u64(0) - mag + } + b.write_u_decimal(mag) +} + +// write_u_decimal appends a decimal representation of the unsigned number `n` into the +// builder `b`. Unlike `write_decimal`, it covers the entire `u64` range (values above +// `max_i64`). The higher order digits come first. +pub fn (mut b Builder) write_u_decimal(n u64) { + if n == 0 { + b.write_u8(0x30) + return + } + mut buf := [20]u8{} // max_u64 == 18446744073709551615, i.e. 20 digits + mut x := n + mut i := 19 + for x != 0 { + buf[i] = u8(x % 10) + 0x30 + x = x / 10 + i-- + } + for j := i + 1; j <= 19; j++ { + b.write_u8(buf[j]) + } +} + pub fn (mut b Builder) write(data []u8) ?int { if data.len == 0 { return 0 diff --git a/vlib/strings/builder_test.v b/vlib/strings/builder_test.v index c55a6e26c..49ed7d929 100644 --- a/vlib/strings/builder_test.v +++ b/vlib/strings/builder_test.v @@ -168,6 +168,29 @@ fn test_write_decimal() { assert sb_i64_str(9223372036854775807) == '9223372036854775807' assert sb_i64_str(-9223372036854775807) == '-9223372036854775807' assert sb_i64_str(min_i64) == '-9223372036854775808' + // runtime `min_i64` (parsed, not the constant), whose negation overflows i64: + assert sb_i64_str('-9223372036854775808'.i64()) == '-9223372036854775808' +} + +@[manualfree] +fn sb_u64_str(n u64) string { + mut sb := strings.new_builder(24) + defer { + unsafe { sb.free() } + } + sb.write_u_decimal(n) + return sb.str() +} + +fn test_write_u_decimal() { + assert sb_u64_str(0) == '0' + assert sb_u64_str(1) == '1' + assert sb_u64_str(1001) == '1001' + assert sb_u64_str(1234567890) == '1234567890' + assert sb_u64_str(u64(9223372036854775807)) == '9223372036854775807' + // values above max_i64, which write_decimal(i64) cannot represent: + assert sb_u64_str(u64(9223372036854775807) + 1) == '9223372036854775808' + assert sb_u64_str(max_u64) == '18446744073709551615' } fn test_grow_len() { -- 2.39.5