// Copyright 2026 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
struct StringBuilder {
// Only 0.. StringBuilder {
guard size_hint >= 0 else { abort("size_hint must be non-negative") }
let data : UninitializedArray[UInt16] = UninitializedArray::make(size_hint)
{ data, len: 0, }
}
///|
/// Return whether the given buffer is empty.
pub fn StringBuilder::is_empty(self : StringBuilder) -> Bool {
self.len == 0
}
///|
/// Compute the next capacity without allocating. Since appends never shrink the
/// builder, `required < len` means the required-size calculation overflowed.
#inline
fn stringbuilder_growth_capacity(
current : Int32,
len : Int32,
required : Int32,
) -> Int32 {
if required < len {
abort("StringBuilder capacity overflow")
}
// Start doubling from at least 1 so a zero-length initial buffer can grow.
for space = current.max(1); space < required; {
let next = space * 2
if next <= space {
break required
}
continue next
} nobreak {
space
}
}
///|
/// Grow the builder to at least `required`. Callers keep the capacity check on
/// their fast path and enter here only when growth or overflow handling is
/// needed. The builder invariant `0 <= len <= data.length()` lets fixed-size
/// appends compare against the remaining capacity without overflowing.
#inline(never)
fn StringBuilder::grow(self : StringBuilder, required : Int32) -> Unit {
let new_capacity = stringbuilder_growth_capacity(
self.data.length(),
self.len,
required,
)
let new_data = UninitializedArray::make_and_blit(
self.data,
allocate_len=new_capacity,
len=self.len,
)
self.data = new_data
}
///|
fn UninitializedArray::unsafe_blit_from_string_uninit(
self : UninitializedArray[UInt16],
dst_offset : Int32,
str : String,
str_offset : Int32,
len : Int32,
) -> Unit {
unsafe_uint16_buffer_as_fixedarray(self).unsafe_blit_from_string(
dst_offset, str, str_offset, len,
)
}
///|
#cfg(not(target="wasm"))
fn FixedArray::unsafe_blit_from_string(
self : FixedArray[UInt16],
dst_offset : Int32,
str : String,
str_offset : Int32,
len : Int32,
) -> Unit {
let end_str_offset = str_offset + len
for i = str_offset, j = dst_offset; i < end_str_offset; i = i + 1, j = j + 1 {
self.unsafe_set(j, str.unsafe_get(i))
}
}
///|
#cfg(target="wasm")
fn FixedArray::unsafe_blit_from_string(
self : FixedArray[UInt16],
dst_offset : Int32,
str : String,
str_offset : Int32,
len : Int32,
) -> Unit {
if len >= 8 {
return self.unsafe_blit_from_string_simd(dst_offset, str, str_offset, len)
}
let end_str_offset = str_offset + len
for i = str_offset, j = dst_offset; i < end_str_offset; i = i + 1, j = j + 1 {
self.unsafe_set(j, str.unsafe_get(i))
}
}
///|
#cfg(target="wasm")
fn FixedArray::unsafe_blit_from_string_simd(
self : FixedArray[UInt16],
dst_offset : Int32,
str : String,
str_offset : Int32,
len : Int32,
) -> Unit {
let vector_len = len - (len & 7)
// Source strings have separate storage from the builder's mutable buffer.
// SAFETY: both ranges are in bounds. Each vector stays inside the requested
// range; the remainder is copied one unit at a time.
for i = 0; i < vector_len; i = i + 8 {
v128_store_i16x8(self, dst_offset + i, v128_load_i16x8(str, str_offset + i))
}
for i in vector_len.. FixedArray[UInt16] = "%identity"
///|
/// Writes a string to the StringBuilder.
pub impl Logger for StringBuilder with fn write_string(self, str) {
let str_len = str.length()
if str_len == 0 {
return
}
let required = self.len + str_len
if required > self.data.length() || required < self.len {
self.grow(required)
}
self.data.unsafe_blit_from_string_uninit(self.len, str, 0, str_len)
self.len += str_len
}
///|
/// Writes a character to the StringBuilder.
pub impl Logger for StringBuilder with fn write_char(self, ch) {
let code = ch.to_uint()
if code <= 0xFFFFU {
if self.len >= self.data.length() {
self.grow(self.len + 1)
}
self.data[self.len] = code.to_uint16()
self.len += 1
} else if code <= 0x10FFFFU {
if self.data.length() - self.len < 2 {
self.grow(self.len + 2)
}
let code = code - 0x10000U
self.data[self.len] = (0xD800U + (code >> 10)).to_uint16()
self.data[self.len + 1] = (0xDC00U + code.land(0x3FFU)).to_uint16()
self.len += 2
} else {
abort("invalid code point")
}
}
///|
/// Writes a string view to the StringBuilder.
///
/// Parameters:
///
/// * `self` : The StringBuilder to write to.
/// * `str` : The view of the string to write.
///
/// Example:
///
/// ```mbt check
/// test {
/// let sb = StringBuilder()
/// sb.write_view("Hello, world!"[:5])
/// @test.assert_eq(sb.to_string(), "Hello")
/// }
/// ```
pub impl Logger for StringBuilder with fn write_view(
self : StringBuilder,
str : StringView,
) -> Unit {
let str_len = str.length()
if str_len == 0 {
return
}
let required = self.len + str_len
if required > self.data.length() || required < self.len {
self.grow(required)
}
self.data.unsafe_blit_from_string_uninit(
self.len,
str.data(),
str.start_offset(),
str_len,
)
self.len += str_len
}
///|
/// Returns the current content of the StringBuilder as a string.
pub fn StringBuilder::to_string(self : StringBuilder) -> String {
if self.len == 0 {
""
} else {
let data = UninitializedArray::make_and_blit(
self.data,
allocate_len=self.len,
len=self.len,
)
unsafe_fixedarray_uint16_to_string(unsafe_uint16_buffer_as_fixedarray(data))
}
}
///|
#doc(hidden)
pub fn StringBuilder::write_end(self : StringBuilder) -> String {
self.to_string()
}
///|
pub impl Show for StringBuilder with fn to_string(self) {
StringBuilder::to_string(self)
}
///|
/// Resets the string builder to an empty state.
///
/// Keeps the allocated capacity by default for reuse. Set `keep_capacity=false`
/// to discard the backing buffer. Previously returned strings are unaffected.
/// This option has no effect on JS, which does not retain a mutable buffer.
///
/// ```mbt check
/// test {
/// let builder = StringBuilder()
/// builder.write_string("hello")
/// builder.reset(keep_capacity=false)
/// inspect(builder.to_string(), content="")
/// }
/// ```
pub fn StringBuilder::reset(
self : StringBuilder,
keep_capacity~ : Bool = true,
) -> Unit {
self.len = 0
if !keep_capacity {
self.data = UninitializedArray::make(0)
}
}